Browse Source

feat(product-manage): 新增竞品监控页面

- 添加竞品监控相关的 API 接口
- 实现竞品监控的数据表格组件
- 实现批量删除操作
- 优化表格列的展示逻辑
WanGxC 7 months ago
parent
commit
1601ebe1b1

+ 95 - 0
src/views/product-manage/competitor-monitor/ColumnsTsx.tsx

@@ -0,0 +1,95 @@
+export const productColumns = [
+  { type: 'checkbox', minWidth: 50, align: 'center', fixed: 'left' },
+  {
+    field: 'product_info', title: '商品信息', minWidth: 240, align: 'center', fixed: 'left',
+    slots: { default: 'product_info' }
+  },
+  {
+    field: 'country_code', title: '国 家', width: 'auto', align: 'center',
+    slots: { default: 'country_code' }
+  },
+  {
+    field: 'brand', title: '品 牌', width: 'auto', align: 'center',
+    slots: { default: 'brand' }
+  },
+  {
+    field: 'shop_name', title: '店 铺', width: 'auto', align: 'center',
+    slots: { default: 'shop_name' }
+  },
+  {
+    field: 'tag', title: '分 组', width: 'auto', align: 'center',
+    slots: { default: 'tag' }
+  },
+  {
+    field: 'price_info', title: '价 格',  width: 'auto', headerAlign: 'center', align: 'left',
+    slots: { default: 'price_info' }
+  },
+  {
+    field: 'show_price', title: '展示价格', width: 'auto', align: 'center',
+    slots: { default: 'show_price' }
+  },
+  {
+    field: 'activity_price', title: '平时活动售价', width: 'auto', align: 'center',
+    slots: { default: 'activity_price' }
+  },
+  {
+    field: 'minimum_price', title: '最低活动售价', width: 'auto', align: 'center',
+    slots: { default: 'minimum_price' }
+  },
+  {
+    field: 'ratings', title: '子ASIN评分人数', width: 'auto', align: 'center', sortable: true,
+    slots: { default: 'ratings' }
+  },
+  {
+    field: 'all_ratings', title: '亚马逊显示评分人数', width: 'auto', align: 'center', sortable: true,
+    slots: { default: 'all_ratings' }
+  },
+  {
+    field: 'reviews', title: '子ASIN评论人数', width: 'auto', align: 'center', sortable: true,
+    slots: { default: 'reviews' }
+  },
+  {
+    field: 'all_reviews', title: '亚马逊显示评论人数', width: 'auto', align: 'center', sortable: true,
+    slots: { default: 'all_reviews' }
+  },
+  {
+    field: 'score', title: '子ASIN计算评分', width: 'auto', align: 'center', sortable: true,
+    slots: { default: 'score' }
+  },
+  {
+    field: 'all_score', title: '亚马逊显示评分', width: 'auto', align: 'center', sortable: true,
+    slots: { default: 'all_score' }
+  },
+  {
+    field: 'stars', title: '子ASIN星级分布', width: 'auto', headerAlign: 'center', align: 'center',
+    slots: { default: 'stars' }
+  },
+  {
+    field: 'all_stars', title: '亚马逊星级分布', width: 'auto', headerAlign: 'center', align: 'center',
+    slots: { default: 'all_stars' }
+  },
+  {
+    field: 'launch_date', title: '上架日期', width: 'auto', align: 'center', sortable: true, 
+    slots: { default: 'launch_date' }
+  },
+  {
+    field: 'category', title: '类 目', width: 'auto', align: 'center',
+    slots: { default: 'category' }
+  },
+  {
+    field: 'status', title: '状 态', width: 'auto', align: 'center',
+    slots: { default: 'status' }
+  },
+  {
+    field: 'update_datetime', title: '更新时间', width: 'auto', align: 'center', showOverflow: true,
+    slots: { default: 'update_datetime' }
+  },
+  {
+    field: 'create_datetime', title: '创建时间', width: 'auto', align: 'center', showOverflow: true,
+    slots: { default: 'create_datetime' }
+  },
+  {
+    field: 'operate', title: '操 作', minWidth: 100, align: 'center', fixed: 'right',
+    slots: { default: 'operate' }
+  }
+];

+ 88 - 0
src/views/product-manage/competitor-monitor/api.ts

@@ -0,0 +1,88 @@
+import { request } from '/@/utils/service';
+
+
+const apiPrefix = '/api/choice/competitor_monitor/';
+
+export function getTableData(query: any) {
+  return request({
+    url: apiPrefix,
+    method: 'GET',
+    params: query
+  });
+}
+
+export function createProductMonitor(body: any) {
+  return request({
+    url: '/api/choice/reviews_monitor/',
+    method: 'POST',
+    data: body
+  });
+}
+
+export function getGroupOptions(query: any) {
+  return request({
+    url: '/api/choice/goods/tags/',
+    method: 'GET',
+    params: query
+  });
+}
+
+export function getBrandsOptions(query: any) {
+  return request({
+    url: '/api/choice/goods/brands/',
+    method: 'GET',
+    params: query
+  });
+}
+
+export function getShopsOptions(query: any) {
+  return request({
+    url: '/api/choice/marketplace_shops/select/',
+    method: 'GET',
+    params: query
+  });
+}
+
+export function updateRow(body: any) {
+  return request({
+    url: apiPrefix + `${body.id}/` ,
+    method: 'PUT',
+    params: { partial: 1 },
+    data: body
+  });
+}
+
+export function deleteRow(body: any) {
+  return request({
+    url: apiPrefix + `${body.id}/` ,
+    method: 'DELETE',
+    data: body
+  });
+}
+
+export function batchDeleteRow(body: any) {
+  return request({
+    url: apiPrefix + 'multiple_delete/',
+    method: 'DELETE',
+    data: body
+  });
+}
+
+// 导入
+export function upload(body: any){
+  return request({
+    url: '/api/choice/reviews_monitor/import_data/',
+    method: 'POST',
+    data: body,
+  });
+}
+
+// 导出
+export function exportData(query: any) {
+  return request({
+    url: '/api/choice/reviews_monitor/export_data/',
+    method: 'GET',
+    params: query,
+    responseType: 'blob'
+  });
+}

+ 310 - 0
src/views/product-manage/competitor-monitor/component/DataTable.vue

@@ -0,0 +1,310 @@
+<script lang="ts" setup>
+/**
+ * @Name: Table.vue
+ * @Description: 商品监控表格
+ * @Author: Cheney
+ */
+
+import { Delete, Download, InfoFilled, Plus, Refresh, Upload } from '@element-plus/icons-vue';
+import * as api from '../api';
+import PermissionButton from '/src/components/PermissionButton/index.vue';
+import EditDrawer from './EditDrawer.vue';
+import ImportButton from '/src/components/ImportButton/index.vue';
+import VerticalDivider from '/src/components/VerticalDivider/index.vue';
+import { productColumns } from '../ColumnsTsx';
+import DataTableSlot from './DataTableSlot.vue';
+import CreateDialog from '/@/views/product-manage/product-monitor/component/createDialog.vue';
+import { ElMessage } from 'element-plus';
+import { batchDeleteRow } from '../api';
+
+
+interface Parameter {
+  country: string,
+  brand: string,
+  group: string,
+  status: string,
+  shop: string
+  asin: string,
+  sku: string,
+  platformId: string,
+  scoreNumber: string,
+  commentNumber: string,
+  displayScore: string,
+}
+
+const queryParameter: Parameter | undefined = inject('query-parameter');
+const { tableOptions, handlePageChange } = usePagination(fetchList);
+
+const gridRef = ref();
+const gridOptions: any = reactive({
+  size: 'mini',
+  border: false,
+  round: true,
+  stripe: true,
+  showHeader: true,
+  currentRowHighLight: true,
+  height: '100%',
+  toolbarConfig: {
+    size: 'large',
+    custom: true,
+    slots: {
+      buttons: 'toolbar_buttons',
+      tools: 'toolbar_tools'
+    }
+  },
+  rowConfig: {
+    isHover: true
+  },
+  columnConfig: {
+    resizable: true
+  },
+  pagerConfig: {
+    total: tableOptions.value.total,
+    page: tableOptions.value.page,
+    limit: tableOptions.value.limit
+  },
+  loading: false,
+  loadingConfig: {
+    icon: 'vxe-icon-indicator roll',
+    text: '正在拼命加载中...'
+  },
+  columns: '',
+  data: ''
+});
+
+const checkedList = ref<Set<number>>(new Set());
+
+const editOpen = ref(false);
+const createOpen = ref(false);
+const rowData = ref({});
+
+const dialogVisible = ref(false);
+
+const templateType = ref();
+
+onMounted(() => {
+  fetchList();
+});
+
+// TODO: 删除goods
+async function fetchList() {
+  gridOptions.data = [];
+  gridOptions.columns = [];
+
+  const query = {
+    country_code: queryParameter?.country,
+    goods__brand: queryParameter?.brand,
+    goods__tag: queryParameter?.group,
+    status: queryParameter?.status,
+    shop_id: queryParameter?.shop,
+    asin: queryParameter?.asin,
+    goods__sku: queryParameter?.sku,
+    platform_number: queryParameter?.platformId,
+    goods__all_ratings: queryParameter?.scoreNumber,
+    goods__all_reviews: queryParameter?.commentNumber,
+    goods__all_score: queryParameter?.displayScore
+  };
+
+  await useTableData(api.getTableData, query, gridOptions);
+  await gridRef.value.loadColumn(productColumns);
+  gridOptions.showHeader = Boolean(gridOptions.data?.length);
+
+}
+
+function handleRefresh() {
+  fetchList();
+}
+
+async function handleDownload() {
+  const confirmed = await ElMessageBox.confirm('是否确认导出当前时间内所有数据项?', '警告', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  });
+
+  if (confirmed) {
+    gridOptions.loading = true;
+    try {
+      const query = {
+        country_code: queryParameter?.country,
+        goods__brand: queryParameter?.brand,
+        goods__tag: queryParameter?.group,
+        status: queryParameter?.status,
+        shop_id: queryParameter?.shop,
+        asin: queryParameter?.asin,
+        goods__sku: queryParameter?.sku,
+        platform_number: queryParameter?.platformId,
+        goods__all_ratings: queryParameter?.scoreNumber,
+        goods__all_reviews: queryParameter?.commentNumber,
+        goods__all_score: queryParameter?.displayScore
+      };
+      const response = await api.exportData(query);
+      const url = window.URL.createObjectURL(new Blob([ response.data ]));
+      const link = document.createElement('a');
+      link.href = url;
+      link.setAttribute('download', '商品监控数据.xlsx');
+      document.body.appendChild(link);
+      link.click();
+      ElMessage.success('数据导出成功!');
+    } catch (error) {
+      ElMessage.error('数据导出失败,请重试!');
+      console.error(error);
+    } finally {
+      gridOptions.loading = false; // 结束加载状态
+    }
+  }
+}
+
+async function batchDelete() {
+  const ids = Array.from(checkedList.value);
+  const res = await useResponse(api.batchDeleteRow, { keys: ids });
+  checkedList.value.clear();
+  if (res.code === 2000) {
+    ElMessage.success({ message: '删除成功', plain: true });
+    handleRefresh();
+  }
+}
+
+function selectChangeEvent({ checked, row }: any) {
+  if (checked) {
+    checkedList.value.add(row.id); // 获取单个数据
+  } else {
+    checkedList.value.delete(row.id);
+  }
+}
+
+function selectAllChangeEvent({ checked }: any) {
+  const $grid = gridRef.value;
+  if ($grid) {
+    const records = $grid.getData(); // 获取所有数据
+    if (checked) {
+      records.forEach((item: any) => {
+        checkedList.value.add(item.id);
+      });
+    } else {
+      checkedList.value.clear();
+    }
+  }
+}
+
+function handleEdit(row: any) {
+  editOpen.value = true;
+  rowData.value = row;
+}
+
+async function singleDelete(row: any) {
+  const res = await useResponse(api.deleteRow, row);
+  if (res.code === 2000) {
+    ElMessage.success({ message: '删除成功', plain: true });
+    handleRefresh();
+  }
+}
+
+function handleCreate() {
+  createOpen.value = true;
+}
+
+function downloadTemplate() {
+  // console.log('111=> ');
+}
+
+defineExpose({ fetchList });
+
+</script>
+
+<template>
+  <vxe-grid ref="gridRef" v-bind="gridOptions"
+            @checkbox-change="selectChangeEvent"
+            @checkbox-all="selectAllChangeEvent">
+    <template #toolbar_buttons>
+      <div class="flex gap-2">
+        <el-popconfirm
+            :icon="InfoFilled"
+            icon-color="#626AEF"
+            title="你确定要删除此项吗?"
+            width="220"
+            @confirm="batchDelete"
+        >
+          <template #reference>
+            <PermissionButton :disabled="!checkedList.size" :icon="Delete" plain round type="danger">
+              批量删除
+            </PermissionButton>
+          </template>
+          <template #actions="{ confirm, cancel }">
+            <el-button size="small" @click="cancel">No!</el-button>
+            <el-button
+                size="small"
+                type="danger"
+                @click="confirm"
+            >
+              Yes?
+            </el-button>
+          </template>
+        </el-popconfirm>
+        <PermissionButton :icon="Plus" plain round type="primary" @click="handleCreate">
+          新 增
+        </PermissionButton>
+        <div class="custom-el-input">
+          <el-select v-model="templateType" placeholder="Select" style="width: 190px">
+            <template #prefix>
+              <div class="flex items-center">
+                <el-button bg size="small" style="margin-left: -7px; font-size: 14px; border-radius: 29px;" text
+                           type="success"
+                           @click.stop="downloadTemplate">下载
+                </el-button>
+                <VerticalDivider style="margin-left: 7px" />
+              </div>
+            </template>
+            <el-option label="商品通知模板" value="item1" />
+            <el-option label="商品模板" value="item2" />
+            <el-option label="指导价格模板" value="item3" />
+          </el-select>
+        </div>
+        <VerticalDivider class="px-1" style="margin-left: 7px;" />
+        <ImportButton :icon="Upload" :uploadFunction="api.upload" bg text>导 入</ImportButton>
+      </div>
+    </template>
+    <template #toolbar_tools>
+      <el-button circle class="toolbar-btn" @click="handleRefresh">
+        <el-icon>
+          <Refresh />
+        </el-icon>
+      </el-button>
+      <el-button circle class="mr-3 toolbar-btn" @click="handleDownload">
+        <el-icon>
+          <Download />
+        </el-icon>
+      </el-button>
+    </template>
+    <template #top>
+      <div class="mb-2"></div>
+    </template>
+    <template #pager>
+      <vxe-pager
+          v-model:currentPage="gridOptions.pagerConfig.page"
+          v-model:pageSize="gridOptions.pagerConfig.limit"
+          :total="gridOptions.pagerConfig.total"
+          class="mt-1.5"
+          @page-change="handlePageChange"
+      />
+    </template>
+    <!-- 自定义列插槽 -->
+    <template v-for="col in productColumns" #[`${col.field}`]="{ row }">
+      <DataTableSlot :key="row.id" :field="col.field" :row="row" @edit-row="handleEdit" @handle-delete="singleDelete" />
+    </template>
+  </vxe-grid>
+  <EditDrawer v-if="editOpen" v-model="editOpen" :row-data="rowData" @refresh="handleRefresh" />
+  <CreateDialog v-if="createOpen" v-model="createOpen" @refresh="fetchList" />
+</template>
+
+<style scoped>
+.toolbar-btn {
+  width: 34px;
+  height: 34px;
+  font-size: 18px
+}
+
+:deep(.custom-el-input .el-select__wrapper) {
+  border-radius: 20px;
+}
+</style>

+ 210 - 0
src/views/product-manage/competitor-monitor/component/DataTableSlot.vue

@@ -0,0 +1,210 @@
+<script lang="ts" setup>
+/**
+ * @Name: DataTableSlot.vue
+ * @Description: 商品监控-表格插槽
+ * @Author: Cheney
+ */
+
+import { useCountryInfoStore } from '/@/stores/countryInfo';
+import { Delete, InfoFilled, Operation } from '@element-plus/icons-vue';
+import PermissionButton from '/@/components/PermissionButton/index.vue';
+import ProductInfo from '/@/views/product-manage/product-list/component/ProductInfo.vue';
+import { getTagType } from '/@/utils/useTagColor';
+import ProgressBar from '/@/views/product-manage/product-monitor/component/ProgressBar.vue';
+
+
+const props = defineProps<{
+  row: any,
+  field: any
+}>();
+const { row, field } = props;
+
+const emit = defineEmits([ 'edit-row', 'handle-delete' ]);
+
+const countryInfoStore = useCountryInfoStore();
+const country = countryInfoStore.countries.find(c => c.code == row.country_code);
+const color = country ? country.color : '#3875F6';
+
+const statusText = row.status === 1 ? '在售' : '停售';
+const statusType = row.status === 1 ? 'success' : 'info';
+
+function handleEdit() {
+  emit('edit-row', row);
+}
+
+function onConfirm() {
+  emit('handle-delete', row);
+}
+
+function starsPercent(goods: any) {
+  const total = goods.ratings;
+  const ret = [ 0, 0, 0, 0, 0 ];
+  let sum = 0;
+  if (total <= 0) {
+    return ret;
+  }
+  for (const index in ret) {
+    const star = parseInt(index) + 1;
+    if (star === 5) {
+      ret[4] = 100 - sum;
+    } else {
+      const v = Math.round(goods[`ratings${ star }`] / total * 100);
+      ret[index] = v;
+      sum += v;
+    }
+  }
+  return ret;
+}
+</script>
+
+<template>
+  <div class="font-medium">
+    <div v-if="field === 'product_info'">
+      <ProductInfo :img-width="50" :item="row.goods" />
+    </div>
+    <div v-else-if="field === 'country_code'">
+      <el-tag :disable-transitions="true" :style="{ color: color, borderColor: color }" effect="plain" round>
+        {{ country ? country.name : '-' }}
+      </el-tag>
+    </div>
+    <div v-else-if="field === 'shop_name'">
+      <el-tag v-if="row.goods.shop_name" :disable-transitions="true" :type=getTagType(row.shop_name)>
+        {{ row.shop_name }}
+      </el-tag>
+      <div v-else>-</div>
+    </div>
+    <div v-else-if="field === 'tag'">
+      <el-tag v-if="row.goods.tag" :disable-transitions="true" :type=getTagType(row.goods.tag)>
+        {{ row.goods.tag }}
+      </el-tag>
+      <div v-else>-</div>
+    </div>
+    <div v-else-if="field === 'brand'">
+      <el-tag v-if="row.goods.brand" :disable-transitions="true" :type=getTagType(row.goods.brand) effect="plain" round>
+        {{ row.goods.brand }}
+      </el-tag>
+      <div v-else>-</div>
+    </div>
+    <div v-else-if="field === 'price_info'">
+      <div v-if="row.goods.price > 0" class="font-medium">
+        <p>现 价:{{ row.goods.currency_code + '‎' + row.goods.price }}</p>
+        <p>折 扣:{{ row.goods.discount > 0 ? row.goods.discount + '%' : '-' }}</p>
+        <p>优惠劵:{{ !row || row.goods.coupon <= 0 ? '-' : row.goods.currency_code + '‎' + row.goods.coupon }}</p>
+      </div>
+    </div>
+    <div v-else-if="field === 'show_price'">
+      <div class="font-medium">
+        {{ row.goods.show_price ? row.goods.currency_code + row.goods.show_price : '-' }}
+      </div>
+    </div>
+    <div v-else-if="field === 'activity_price'">
+      <div class="font-medium">
+        {{ row.goods.activity_price ? row.goods.currency_code + row.goods.activity_price : '-' }}
+      </div>
+    </div>
+    <div v-else-if="field === 'minimum_price'">
+      <div class="font-medium">
+        {{ row.goods.minimum_price ? row.goods.currency_code + row.goods.minimum_price : '-' }}
+      </div>
+    </div>
+    <div v-else-if="field === 'score'">
+      <template v-if="row.goods.score !== null && row.goods.score !== undefined && row.goods.score !== ''">
+        <el-rate
+            v-if="row.goods.score > 0"
+            v-model="row.goods.score"
+            :colors="['#FF0000', '#FF9900', '#67C23A']"
+            disabled
+            show-score
+            text-color="#1e293b"
+        />
+        <span v-else>{{ row.goods.score }}</span> <!-- 值为0时显示0 -->
+      </template>
+      <template v-else>
+        <span>-</span> <!-- 无值时显示'--' -->
+      </template>
+    </div>
+
+    <div v-else-if="field === 'all_score'">
+      <template v-if="row.goods.all_score !== null && row.goods.all_score !== undefined && row.goods.all_score !== ''">
+        <el-rate
+            v-if="row.goods.all_score > 0"
+            v-model="row.goods.all_score"
+            :colors="['#FF0000', '#FF9900', '#67C23A']"
+            disabled
+            show-score
+            text-color="#1e293b"
+        />
+        <span v-else>{{ row.goods.all_score }}</span>
+      </template>
+      <template v-else>
+        <span>-</span>
+      </template>
+    </div>
+    <div v-else-if="field === 'stars'" class="flex flex-col font-normal" style="min-width: 170px">
+      <div v-for="(percent, index) in starsPercent(row.goods).reverse()" :key="index" class="w-full flex items-center">
+        <span class="w-10 text-right mr-2">{{ 5 - index }}星</span>
+        <el-tooltip :content="String(row.goods[`ratings${5 - index}`])" :show-after="300" effect="dark" placement="top">
+          <el-progress
+              :color="'#3A8EE6'"
+              :percentage="percent"
+              :stroke-width="10"
+              class="flex-1"
+              striped
+              striped-flow
+          />
+        </el-tooltip>
+      </div>
+    </div>
+    <div v-else-if="field === 'all_stars'" class="flex flex-col font-normal" style="min-width: 170px">
+      <ProgressBar :row="row" percentage="all_rate" />
+    </div>
+    <div v-else-if="field === 'status'">
+      <el-tag :disable-transitions="true" :type=statusType>
+        {{ statusText }}
+      </el-tag>
+    </div>
+    <div v-else-if="field === 'operate'">
+      <div class="flex justify-center gap-2">
+        <PermissionButton circle plain type="warning" @click="handleEdit">
+          <el-icon>
+            <Operation />
+          </el-icon>
+        </PermissionButton>
+        <el-popconfirm
+            :icon="InfoFilled"
+            icon-color="#626AEF"
+            title="你确定要删除此项吗?"
+            width="220"
+            @confirm="onConfirm"
+        >
+          <template #reference>
+            <PermissionButton circle plain type="danger">
+              <el-icon>
+                <Delete />
+              </el-icon>
+            </PermissionButton>
+          </template>
+          <template #actions="{ confirm, cancel }">
+            <el-button size="small" @click="cancel">No!</el-button>
+            <el-button
+                size="small"
+                type="danger"
+                @click="confirm"
+            >
+              Yes?
+            </el-button>
+          </template>
+        </el-popconfirm>
+      </div>
+    </div>
+    <div v-else>
+      {{ row.goods[field] }}
+    </div>
+  </div>
+</template>
+
+<style scoped>
+:deep(.flex-1 .el-progress__text) {
+  font-size: 14px !important;
+}
+</style>

+ 150 - 0
src/views/product-manage/competitor-monitor/component/EditDrawer.vue

@@ -0,0 +1,150 @@
+<script lang="ts" setup>
+/**
+ * @Name: EditDrawer.vue
+ * @Description: 商品监控-行编辑
+ * @Author: Cheney
+ */
+
+import { ElMessage, FormInstance, FormRules } from 'element-plus';
+import { Close, Finished } from '@element-plus/icons-vue';
+import { DictionaryStore } from '/@/stores/dictionary';
+import * as api from '../api';
+
+const { data: staticData } = DictionaryStore();
+
+const btnLoading = ref(false);
+
+const editOpen = defineModel({ default: false });
+
+const editDrawer = <Ref>useTemplateRef('editDrawer');
+
+const props = defineProps({
+  rowData: Object
+});
+const { rowData } = props;
+const emit = defineEmits([ 'refresh' ]);
+
+interface RuleForm {
+  asin: any,
+  country_code: any
+  shop_name: any
+  tag: any
+  status: any
+  freq: any
+}
+
+const ruleFormRef = ref<FormInstance>();
+const ruleForm = reactive<RuleForm>({
+  asin: rowData?.asin,
+  country_code: rowData?.country_code,
+  shop_name: rowData?.shop_name,
+  tag: rowData?.goods.tag,
+  status: rowData?.status.toString(),
+  freq: rowData?.freq,
+});
+
+const rules = reactive<FormRules<RuleForm>>({
+  shop_name: [ { required: true, message: '请输入店铺', trigger: 'blur' } ],
+  tag: [ { required: true, message: '请输入分组', trigger: 'blur' } ],
+});
+
+const submitForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return;
+  await formEl.validate(async (valid, fields) => {
+    if (valid) {
+      try {
+        const res = await useResponse(api.updateRow, { id: rowData?.id, ...ruleForm }, btnLoading);
+        if (res && res.code == 2000) {
+          editOpen.value = false;
+          ElMessage.success('编辑成功');
+          emit('refresh');
+        }
+      } catch (error) {
+        console.error('Error==>', error);
+      }
+    } else {
+      console.log('error submit!', fields);
+    }
+  });
+};
+
+const resetForm = (formEl: FormInstance | undefined) => {
+  if (!formEl) return;
+  formEl.resetFields();
+};
+
+function closeDrawer() {
+  editDrawer.value.handleClose();
+}
+
+</script>
+
+<template>
+  <div class="drawer-container">
+    <el-drawer ref="editDrawer"
+               v-model="editOpen"
+               :close-on-click-modal="false"
+               :close-on-press-escape="false"
+               :title="`商品监控 - 编辑 `"
+               size="25%">
+      <el-form
+          ref="ruleFormRef"
+          :model="ruleForm"
+          :rules="rules"
+          class="mx-2.5 mt-7"
+          label-position="top"
+          label-width="auto"
+          status-icon>
+        <el-form-item class="font-medium" label="ASIN" prop="asin">
+          <el-input v-model="ruleForm.asin" :disabled="true" />
+        </el-form-item>
+        <el-form-item class="font-medium" label="店 铺" prop="shop">
+          <el-input v-model="ruleForm.shop_name" />
+        </el-form-item>
+        <el-form-item class="font-medium" label="分 组" prop="tag">
+          <el-input v-model="ruleForm.tag" />
+        </el-form-item>
+        <el-form-item class="font-medium" label="状 态" prop="status">
+          <el-select v-model="ruleForm.status" :disabled="true">
+            <el-option label="启用" value="1" />
+            <el-option label="暂停" value="2" />
+          </el-select>
+        </el-form-item>
+        <el-form-item class="font-medium" label="国 家" prop="country_code">
+          <el-select v-model="ruleForm.country_code" placeholder="请选择国家" :disabled="true">
+            <el-option
+                v-for="item in staticData.country_code"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value">
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item class="font-medium" label="更新频率" prop="freq">
+          <el-input-number v-model="ruleForm.freq" />
+        </el-form-item>
+        <el-form-item>
+          <el-divider />
+          <div class="flex flex-1 justify-end">
+            <el-button :icon="Close" @click="closeDrawer">取 消</el-button>
+            <el-button :icon="Finished" :loading="btnLoading" type="primary" @click="submitForm(ruleFormRef)">
+              确 定
+            </el-button>
+          </div>
+        </el-form-item>
+      </el-form>
+    </el-drawer>
+  </div>
+
+</template>
+
+<style scoped>
+.drawer-container :deep(.el-drawer__header) {
+  border-bottom: none;
+  font-weight: 500;
+}
+
+.drawer-container :deep(.el-drawer__title) {
+  font-size: 18px;
+}
+</style>

+ 175 - 6
src/views/product-manage/competitor-monitor/index.vue

@@ -1,16 +1,185 @@
-<script setup lang="ts">
+<script lang="ts" setup>
 /**
  * @Name: index.vue
- * @Description:
- * @Author: xinyan
+ * @Description: 竞品监控
+ * @Author: Cheney
  */
 
+import { RefreshRight, Search } from '@element-plus/icons-vue';
+import VerticalDivider from '/src/components/VerticalDivider/index.vue';
+import DataTable from './component/DataTable.vue';
+import { useTableHeight } from '/@/utils/useTableHeight';
+import { useResponse } from '/@/utils/useResponse';
+import * as api from './api';
+import { useTemplateRef } from 'vue';
+import { DictionaryStore } from '/@/stores/dictionary';
+
+
+const { data: staticData } = DictionaryStore();
+
+const titleContainer: Ref<HTMLElement | null> = useTemplateRef('titleContainer');
+const queryContainer: Ref<HTMLElement | null> = useTemplateRef('queryContainer');
+const { tableHeight } = useTableHeight(titleContainer, queryContainer);
+
+const tableRef: Ref<any> = useTemplateRef('table');
+
+const formInline = reactive<any>({
+  country: '',
+  brand: '',
+  group: '',
+  status: '',
+  shop: '',
+  asin: '',
+  scoreNumber: '-',
+  commentNumber: '-',
+  displayScore: '-'
+});
+provide('query-parameter', formInline);
+
+const groupOptions: any = ref([]);
+const brandsOptions: any = ref([]);
+const shopsOptions: any = ref([]);
+
+provide('groupOptions', groupOptions);
+provide('brandsOptions', brandsOptions);
+provide('shopOptions', shopsOptions);
+
+onBeforeMount(() => {
+  fetchGroupOptions();
+  fetchBrandsOptions();
+  fetchShopsOptions();
+});
+
+async function fetchGroupOptions() {
+  const res = await useResponse(api.getGroupOptions);
+  groupOptions.value = res.data;
+}
+
+async function fetchBrandsOptions() {
+  const res = await useResponse(api.getBrandsOptions);
+  brandsOptions.value = res.data;
+}
+
+async function fetchShopsOptions() {
+  const res = await useResponse(api.getShopsOptions);
+  shopsOptions.value = res.data;
+}
+
+const btnLoading = ref(false);
+
+async function handleQuery() {
+  btnLoading.value = true;
+  await tableRef.value?.fetchList();
+  btnLoading.value = false;
+}
+
+function resetParameter() {
+  for (const key in formInline) {
+    formInline[key] = '';
+  }
+}
 </script>
 
 <template>
-
+  <div class="p-5 flex-grow">
+    <el-card class="h-full" style="color: rgba(0, 0, 0, 0.88);">
+      <div ref="titleContainer" class="text-xl font-semibold pb-5">竞品监控</div>
+      <!-- 查询条件 -->
+      <div ref="queryContainer" class="flex justify-between">
+        <div class="flex flex-1">
+          <div class="w-full whitespace-nowrap">
+            <el-row :gutter="20" style="margin-bottom: 16px;">
+              <el-col :span="4">
+                <div class="flex items-center">
+                  <span class="mr-2">国 家</span>
+                  <el-select v-model="formInline.country" clearable placeholder="请选择国家">
+                    <el-option v-for="item in staticData.country_code" :key="item.value" :label="item.label"
+                               :value="item.value" />
+                  </el-select>
+                </div>
+              </el-col>
+              <el-col :span="5">
+                <div class="flex items-center">
+                  <span class="mr-2">品 牌</span>
+                  <el-select v-model="formInline.brand" clearable placeholder="请选择品牌">
+                    <el-option v-for="item in brandsOptions" :label="item.brand" :value="item.brand" />
+                  </el-select>
+                </div>
+              </el-col>
+              <el-col :span="5">
+                <div class="flex items-center">
+                  <span class="mr-2">分 组</span>
+                  <el-select v-model="formInline.group" clearable placeholder="请选择分组">
+                    <el-option v-for="item in groupOptions" :label="item.tag" :value="item.tag" />
+                  </el-select>
+                </div>
+              </el-col>
+              <el-col :span="4">
+                <div class="flex items-center">
+                  <span class="mr-2">状 态</span>
+                  <el-select v-model="formInline.status" clearable placeholder="请选择状态">
+                    <el-option v-for="item in staticData.goods_status" :key="item.value" :label="item.label"
+                               :value="item.value" />
+                  </el-select>
+                </div>
+              </el-col>
+              <el-col :span="6" class="flex">
+                <div class="flex items-center">
+                  <span class="mr-2">店 铺</span>
+                  <el-input v-model="formInline.shop" clearable placeholder="请输入店铺" />
+                </div>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20">
+              <el-col :span="6" class="flex">
+                <div class="flex items-center">
+                  <span class="mr-2">亚马逊显示评分人数</span>
+                  <el-input-number v-model="formInline.scoreNumber" :min="0"
+                                   placeholder="请输入亚马逊显示评分人数"></el-input-number>
+                </div>
+              </el-col>
+              <el-col :span="6">
+                <div class="flex items-center">
+                  <span class="mr-2">亚马逊显示评论人数</span>
+                  <el-input-number v-model="formInline.commentNumber" :min="0"
+                                   placeholder="请输入亚马逊显示评论人数"></el-input-number>
+                </div>
+              </el-col>
+              <el-col :span="6">
+                <div class="flex items-center">
+                  <span class="mr-2">亚马逊显示评分</span>
+                  <el-input-number v-model="formInline.displayScore" :min="0"
+                                   placeholder="请输入亚马逊显示评分"></el-input-number>
+                </div>
+              </el-col>
+              <el-col :span="6">
+                <div class="flex items-center">
+                  <span class="mr-2">ASIN</span>
+                  <el-input v-model="formInline.asin" clearable placeholder="请输入ASIN"></el-input>
+                </div>
+              </el-col>
+            </el-row>
+          </div>
+        </div>
+        <VerticalDivider />
+        <div class="flex flex-col  items-end">
+          <el-button :icon="Search" :loading="btnLoading" class="mb-4" type="primary" @click="handleQuery">
+            查 询
+          </el-button>
+          <el-button :icon="RefreshRight" color="#ECECF1C9" style="width: 88px; color: #3c3c3c;" 
+                     @click="resetParameter">
+            重 置
+          </el-button>
+        </div>
+      </div>
+      <el-divider ref="dividerContainer" style="margin: 20px 0 12px 0;" />
+      <div :style="{ height: tableHeight + 'px' }">
+        <DataTable ref="table" />
+      </div>
+    </el-card>
+  </div>
 </template>
 
-<style scoped lang="scss">
+<style scoped>
 
-</style>
+</style>

+ 2 - 11
src/views/product-manage/product-list/api.ts

@@ -71,15 +71,6 @@ export function postStaffs(body: any) {
   })
 }
 
-export function updateShopDetail(body: any) {
-  return request({
-    url: apiPrefix + `${ body.id }/`,
-    method: 'POST',
-    params: { partial: body.partial },
-    data: body.formData
-  });
-}
-
 export function uploadFile(body: any){
   return request({
     url: 'api/system/file/',
@@ -117,11 +108,11 @@ export function uploadPrice(body: any){
 }
 
 // 导出
-export function exportData(query) {
+export function exportData(query: any) {
   return request({
     url: '/api/choice/goods/export_data/',
     method: 'GET',
     params: query,
     responseType: 'blob'
   });
-}
+}

+ 44 - 46
src/views/product-manage/product-list/component/DataTable.vue

@@ -33,7 +33,7 @@ const { tableOptions, handlePageChange } = usePagination(fetchList);
 
 const gridRef = ref();
 const gridOptions: any = reactive({
-  size: "mini",
+  size: 'mini',
   border: false,
   round: true,
   stripe: true,
@@ -85,7 +85,7 @@ onMounted(() => {
 async function fetchList() {
   gridOptions.data = [];
   gridOptions.columns = [];
-  
+
   const query = {
     country_code: queryParameter?.country,
     brand: queryParameter?.brand,
@@ -95,7 +95,7 @@ async function fetchList() {
     sku: queryParameter?.sku,
     shop_id: queryParameter?.shop
   };
-  
+
   await useTableData(api.getTableData, query, gridOptions);
   await gridRef.value.loadColumn(productColumns);
   gridOptions.showHeader = Boolean(gridOptions.data?.length);
@@ -106,39 +106,39 @@ function handleRefresh() {
 }
 
 async function handleDownload() {
-	const confirmed = await ElMessageBox.confirm('是否确认导出当前时间内所有数据项?', '警告', {
-		confirmButtonText: '确定',
-		cancelButtonText: '取消',
-		type: 'warning'
-	});
+  const confirmed = await ElMessageBox.confirm('是否确认导出当前时间内所有数据项?', '警告', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  });
 
-	if (confirmed) {
-		gridOptions.loading = true;
-		try {
-			const query = {
-				country_code: queryParameter?.country,
-				brand: queryParameter?.brand,
-				tag: queryParameter?.group,
-				status: queryParameter?.status,
-				asin: queryParameter?.asin,
-				sku: queryParameter?.sku,
-				shop_id: queryParameter?.shop
-			};
-			const response = await api.exportData(query);
-			const url = window.URL.createObjectURL(new Blob([response.data]));
-			const link = document.createElement('a');
-			link.href = url;
-			link.setAttribute('download', '商品列表数据.xlsx');
-			document.body.appendChild(link);
-			link.click();
-			ElMessage.success('数据导出成功!');
-		} catch (error) {
-			ElMessage.error('数据导出失败,请重试!');
-			console.error(error);
-		} finally {
-			gridOptions.loading = false; // 结束加载状态
-		}
-	}
+  if (confirmed) {
+    gridOptions.loading = true;
+    try {
+      const query = {
+        country_code: queryParameter?.country,
+        brand: queryParameter?.brand,
+        tag: queryParameter?.group,
+        status: queryParameter?.status,
+        asin: queryParameter?.asin,
+        sku: queryParameter?.sku,
+        shop_id: queryParameter?.shop
+      };
+      const response = await api.exportData(query);
+      const url = window.URL.createObjectURL(new Blob([ response.data ]));
+      const link = document.createElement('a');
+      link.href = url;
+      link.setAttribute('download', '商品列表数据.xlsx');
+      document.body.appendChild(link);
+      link.click();
+      ElMessage.success('数据导出成功!');
+    } catch (error) {
+      ElMessage.error('数据导出失败,请重试!');
+      console.error(error);
+    } finally {
+      gridOptions.loading = false; // 结束加载状态
+    }
+  }
 }
 
 function selectChangeEvent({ checked, row }: any) {
@@ -174,7 +174,7 @@ function handleNotice(row: any) {
 }
 
 async function switchMonitor(row: any) {
-  const res = await useResponse(api.postMonitor, { ids: [row.id], status: row.is_monitor ? 1 : 0 })
+  const res = await useResponse(api.postMonitor, { ids: [ row.id ], status: row.is_monitor ? 1 : 0 });
   if (res && res.code === 2000) {
     ElMessage.success('操作成功!');
   } else {
@@ -183,7 +183,6 @@ async function switchMonitor(row: any) {
   }
 }
 
-
 async function batchOpen() {
   const ids = Array.from(checkedList.value);
   const res = await useResponse(api.postMonitor, { ids, status: 1 }, btnLoading);
@@ -194,7 +193,6 @@ async function batchOpen() {
   await fetchList();
 }
 
-
 function downloadTemplate() {
   const urlMap: { [key: string]: string } = {
     notice: '/api/choice/goods/alarm/import_data/',
@@ -224,7 +222,7 @@ defineExpose({ fetchList });
     <!-- 工具栏左侧 -->
     <template #toolbar_buttons>
       <div class="flex gap-2">
-        <PermissionButton :disabled="!checkedList.size" :icon="Open" :loading="btnLoading" plain round type="primary" 
+        <PermissionButton :disabled="!checkedList.size" :icon="Open" :loading="btnLoading" plain round type="primary"
                           @click="batchOpen">
           批量开启
         </PermissionButton>
@@ -232,9 +230,9 @@ defineExpose({ fetchList });
           <el-select v-model="templateType" style="width: 190px">
             <template #prefix>
               <div class="flex items-center">
-                <el-button size="small"
-                           type="success" bg text
-                           style="margin-left: -7px; font-size: 14px; border-radius: 29px;"
+                <el-button bg
+                           size="small" style="margin-left: -7px; font-size: 14px; border-radius: 29px;" text
+                           type="success"
                            @click.stop="downloadTemplate">下载
                 </el-button>
                 <VerticalDivider style="margin-left: 7px" />
@@ -247,12 +245,12 @@ defineExpose({ fetchList });
         </div>
         <VerticalDivider class="px-1" style="margin-left: 7px;" />
 
-        <ImportButton :icon="Message" bg text :uploadFunction="api.uploadChangeNotice">变更通知导入</ImportButton>
-        <ImportButton bg text :uploadFunction="api.uploadProducts">
+        <ImportButton :icon="Message" :uploadFunction="api.uploadChangeNotice" bg text>变更通知导入</ImportButton>
+        <ImportButton :uploadFunction="api.uploadProducts" bg text>
           <i class="bi bi-box-seam mr-3"></i>
           商品导入
         </ImportButton>
-        <ImportButton :icon="Money" bg text :uploadFunction="api.uploadPrice">指导价格导入</ImportButton>
+        <ImportButton :icon="Money" :uploadFunction="api.uploadPrice" bg text>指导价格导入</ImportButton>
       </div>
     </template>
     <!-- 工具栏右侧 -->
@@ -284,7 +282,7 @@ defineExpose({ fetchList });
     <!-- 自定义列插槽 -->
     <template v-for="col in productColumns" #[`${col.field}`]="{ row }">
       <DataTableSlot :key="row.id" :field="col.field" :row="row" @edit-row="handleEdit" @handle-notice="handleNotice"
-     @handle-monitor="switchMonitor" />
+                     @handle-monitor="switchMonitor" />
     </template>
   </vxe-grid>
   <EditDrawer v-if="editOpen" v-model="editOpen" :row-data="rowData" @refresh="handleRefresh" />

+ 20 - 27
src/views/product-manage/product-list/component/DataTableSlot.vue

@@ -27,15 +27,15 @@ const color = country ? country.color : '#3875F6';
 const statusText = row.status === 1 ? '在售' : '停售';
 const statusType = row.status === 1 ? 'success' : 'info';
 
-function handleEdit(row: any) {
+function handleEdit() {
   emit('edit-row', row);
 }
 
-function handleNotice(row: any) {
+function handleNotice() {
   emit('handle-notice', row);
 }
 
-function handleMonitor(row: any) {
+function handleMonitor() {
   emit('handle-monitor', row);
 }
 </script>
@@ -43,33 +43,36 @@ function handleMonitor(row: any) {
 <template>
   <div class="font-medium">
     <div v-if="field === 'is_monitor'">
-      <el-switch v-model=row.is_monitor @change="handleMonitor(row)" />
+      <el-switch v-model=row.is_monitor @change="handleMonitor" />
     </div>
     <div v-else-if="field === 'product_info'">
       <ProductInfo :img-width="50" :item="row" />
     </div>
     <div v-else-if="field === 'country_code'">
       <el-tag :disable-transitions="true" :style="{ color: color, borderColor: color }" effect="plain" round>
-        {{ country ? country.name : '--' }}
+        {{ country ? country.name : '-' }}
       </el-tag>
     </div>
     <div v-else-if="field === 'shop_name'">
-      <el-tag :disable-transitions="true" :type=getTagType(row.shop_name)>
-        {{ row.shop_name ?? '--' }}
+      <el-tag v-if="row.shop_name" :disable-transitions="true" :type=getTagType(row.shop_name)>
+        {{ row.shop_name }}
       </el-tag>
+      <div v-else>-</div>
     </div>
     <div v-else-if="field === 'tag'">
-      <el-tag :disable-transitions="true" :type=getTagType(row.tag)>
-        {{ row.tag ?? '--' }}
+      <el-tag v-if="row.tag" :disable-transitions="true" :type=getTagType(row.tag)>
+        {{ row.tag }}
       </el-tag>
+      <div v-else>-</div>
     </div>
     <div v-else-if="field === 'brand'">
-      <el-tag :disable-transitions="true" :type=getTagType(row.brand) effect="plain" round>
-        {{ row.brand ?? '--' }}
+      <el-tag v-if="row.brand" :disable-transitions="true" :type=getTagType(row.brand) effect="plain" round>
+        {{ row.brand }}
       </el-tag>
+      <div v-else>-</div>
     </div>
     <div v-else-if="field === 'price_info'">
-      <div v-if="row.price > 0" class="font-medium">
+      <div v-if="row.price >= 0" class="font-medium">
         <p>现 价:{{ row.currency_code + '‎' + row.price }}</p>
         <p>折 扣:{{ row.discount > 0 ? row.discount + '%' : '-' }}</p>
         <p>优惠劵:{{ !row || row.coupon <= 0 ? '-' : row.currency_code + '‎' + row.coupon }}</p>
@@ -77,21 +80,11 @@ function handleMonitor(row: any) {
     </div>
     <div v-else-if="field === 'show_price'">
       <div class="font-medium">
-        <p>展示价格:{{ row.show_price ? row.currency_code + row.show_price : '--' }}</p>
-        <p>平时活动售价:{{ row.activity_price ? row.currency_code + row.activity_price : '--' }}</p>
-        <p>最低活动售价:{{ row.minimum_price ? row.currency_code + row.minimum_price : '--' }}</p>
+        <p>展示价格:{{ row.show_price ? row.currency_code + row.show_price : '-' }}</p>
+        <p>平时活动售价:{{ row.activity_price ? row.currency_code + row.activity_price : '-' }}</p>
+        <p>最低活动售价:{{ row.minimum_price ? row.currency_code + row.minimum_price : '-' }}</p>
       </div>
     </div>
-    <!--<div v-else-if="field === 'activity_price'">-->
-    <!--  <div class="font-medium">-->
-    <!--    {{ row.activity_price ? row.currency_code + row.activity_price : '&#45;&#45;' }}-->
-    <!--  </div>-->
-    <!--</div>-->
-    <!--<div v-else-if="field === 'minimum_price'">-->
-    <!--  <div class="font-medium">-->
-    <!--    {{ row.minimum_price ? row.currency_code + row.minimum_price : '&#45;&#45;' }}-->
-    <!--  </div>-->
-    <!--</div>-->
     <div v-else-if="field === 'status'">
       <el-tag :disable-transitions="true" :type=statusType>
         {{ statusText }}
@@ -99,12 +92,12 @@ function handleMonitor(row: any) {
     </div>
     <div v-else-if="field === 'operate'">
       <div class="flex justify-center gap-2">
-        <PermissionButton circle plain type="warning" @click="handleEdit(row)">
+        <PermissionButton circle plain type="warning" @click="handleEdit">
           <el-icon>
             <Operation />
           </el-icon>
         </PermissionButton>
-        <PermissionButton circle plain type="info" @click="handleNotice(row)">
+        <PermissionButton circle plain type="info" @click="handleNotice">
           <el-icon>
             <Message />
           </el-icon>

+ 0 - 4
src/views/product-manage/product-list/component/EditDrawer.vue

@@ -87,10 +87,6 @@ function closeDrawer() {
         :close-on-press-escape="false"
         size="30%"
         title="商品列表- 编辑">
-      <!--<div class="mx-2.5">-->
-      <!--  -->
-      <!--<el-divider style="margin: 0"  />-->
-      <!--</div>-->
       <el-form
           ref="ruleFormRef"
           :model="ruleForm"

+ 10 - 4
src/views/product-manage/product-list/index.vue

@@ -25,9 +25,9 @@ const tableRef: Ref<any> = useTemplateRef('table');
 
 const btnLoading = ref(false);
 
-const formInline = reactive({
-  country: 'US',
-  brand: 'ZOSI',
+const formInline = reactive<any>({
+  country: '',
+  brand: '',
   group: '',
   status: '',
   asin: '',
@@ -69,6 +69,12 @@ async function handleQuery() {
   await tableRef.value?.fetchList();
   btnLoading.value = false;
 }
+
+function resetParameter() {
+  for (const key in formInline) {
+    formInline[key] = '';
+  }
+}
 </script>
 
 <template>
@@ -142,7 +148,7 @@ async function handleQuery() {
           <el-button :icon="Search" :loading="btnLoading" class="mb-4" type="primary" @click="handleQuery">
             查 询
           </el-button>
-          <el-button :icon="RefreshRight" color="#ECECF1C9" style="width: 88px; color: #3c3c3c;">
+          <el-button :icon="RefreshRight" color="#ECECF1C9" style="width: 88px; color: #3c3c3c;" @click="resetParameter">
             重 置
           </el-button>
         </div>

+ 5 - 6
src/views/product-manage/product-monitor/api.ts

@@ -60,12 +60,11 @@ export function deleteRow(body: any) {
   });
 }
 
-export function updateShopDetail(body: any) {
+export function batchDeleteRow(body: any) {
   return request({
-    url: apiPrefix + `${ body.id }/`,
-    method: 'POST',
-    params: { partial: body.partial },
-    data: body.formData
+    url: apiPrefix + 'reviews_monitor/multiple_delete/',
+    method: 'DELETE',
+    data: body
   });
 }
 
@@ -79,7 +78,7 @@ export function upload(body: any){
 }
 
 // 导出
-export function exportData(query) {
+export function exportData(query: any) {
   return request({
     url: '/api/choice/reviews_monitor/export_data/',
     method: 'GET',

+ 69 - 48
src/views/product-manage/product-monitor/component/DataTable.vue

@@ -5,7 +5,7 @@
  * @Author: Cheney
  */
 
-import { Delete, Download, Plus, Refresh, Upload } from '@element-plus/icons-vue';
+import { Delete, Download, InfoFilled, Plus, Refresh, Upload } from '@element-plus/icons-vue';
 import * as api from '../api';
 import PermissionButton from '/src/components/PermissionButton/index.vue';
 import EditDrawer from './EditDrawer.vue';
@@ -115,50 +115,53 @@ function handleRefresh() {
 }
 
 async function handleDownload() {
-	const confirmed = await ElMessageBox.confirm('是否确认导出当前时间内所有数据项?', '警告', {
-		confirmButtonText: '确定',
-		cancelButtonText: '取消',
-		type: 'warning'
-	});
+  const confirmed = await ElMessageBox.confirm('是否确认导出当前时间内所有数据项?', '警告', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  });
 
-	if (confirmed) {
-		gridOptions.loading = true;
-		try {
-			const query = {
-				country_code: queryParameter?.country,
-				goods__brand: queryParameter?.brand,
-				goods__tag: queryParameter?.group,
-				status: queryParameter?.status,
-				shop_id: queryParameter?.shop,
-				asin: queryParameter?.asin,
-				goods__sku: queryParameter?.sku,
-				platform_number: queryParameter?.platformId,
-				goods__all_ratings: queryParameter?.scoreNumber,
-				goods__all_reviews: queryParameter?.commentNumber,
-				goods__all_score: queryParameter?.displayScore
-			};
-			const response = await api.exportData(query);
-			const url = window.URL.createObjectURL(new Blob([response.data]));
-			const link = document.createElement('a');
-			link.href = url;
-			link.setAttribute('download', '商品监控数据.xlsx');
-			document.body.appendChild(link);
-			link.click();
-			ElMessage.success('数据导出成功!');
-		} catch (error) {
-			ElMessage.error('数据导出失败,请重试!');
-			console.error(error);
-		} finally {
-			gridOptions.loading = false; // 结束加载状态
-		}
-	}
+  if (confirmed) {
+    gridOptions.loading = true;
+    try {
+      const query = {
+        country_code: queryParameter?.country,
+        goods__brand: queryParameter?.brand,
+        goods__tag: queryParameter?.group,
+        status: queryParameter?.status,
+        shop_id: queryParameter?.shop,
+        asin: queryParameter?.asin,
+        goods__sku: queryParameter?.sku,
+        platform_number: queryParameter?.platformId,
+        goods__all_ratings: queryParameter?.scoreNumber,
+        goods__all_reviews: queryParameter?.commentNumber,
+        goods__all_score: queryParameter?.displayScore
+      };
+      const response = await api.exportData(query);
+      const url = window.URL.createObjectURL(new Blob([ response.data ]));
+      const link = document.createElement('a');
+      link.href = url;
+      link.setAttribute('download', '商品监控数据.xlsx');
+      document.body.appendChild(link);
+      link.click();
+      ElMessage.success('数据导出成功!');
+    } catch (error) {
+      ElMessage.error('数据导出失败,请重试!');
+      console.error(error);
+    } finally {
+      gridOptions.loading = false; // 结束加载状态
+    }
+  }
 }
 
-async function batchOpen() {
+async function batchDelete() {
   const ids = Array.from(checkedList.value);
-  await useResponse(api.updateShopDetail, { ids, status: 1 });
+  const res = await useResponse(api.batchDeleteRow, { keys: ids });
   checkedList.value.clear();
-  await fetchList();
+  if (res.code === 2000) {
+    ElMessage.success({ message: '删除成功', plain: true });
+    handleRefresh();
+  }
 }
 
 function selectChangeEvent({ checked, row }: any) {
@@ -197,7 +200,7 @@ async function singleDelete(row: any) {
 }
 
 function handleCreate() {
-	createOpen.value = true;
+  createOpen.value = true;
 }
 
 function downloadTemplate() {
@@ -210,15 +213,33 @@ defineExpose({ fetchList });
 
 <template>
   <vxe-grid ref="gridRef" v-bind="gridOptions"
-            :auto-resize="true"
-            :sync-resize="true"
             @checkbox-change="selectChangeEvent"
             @checkbox-all="selectAllChangeEvent">
     <template #toolbar_buttons>
       <div class="flex gap-2">
-        <PermissionButton :disabled="!checkedList.size" :icon="Delete" plain round type="danger" @click="batchOpen">
-          批量删除
-        </PermissionButton>
+        <el-popconfirm
+            :icon="InfoFilled"
+            icon-color="#626AEF"
+            title="你确定要删除此项吗?"
+            width="220"
+            @confirm="batchDelete"
+        >
+          <template #reference>
+            <PermissionButton :disabled="!checkedList.size" :icon="Delete" plain round type="danger">
+              批量删除
+            </PermissionButton>
+          </template>
+          <template #actions="{ confirm, cancel }">
+            <el-button size="small" @click="cancel">No!</el-button>
+            <el-button
+                size="small"
+                type="danger"
+                @click="confirm"
+            >
+              Yes?
+            </el-button>
+          </template>
+        </el-popconfirm>
         <PermissionButton :icon="Plus" plain round type="primary" @click="handleCreate">
           新 增
         </PermissionButton>
@@ -239,7 +260,7 @@ defineExpose({ fetchList });
           </el-select>
         </div>
         <VerticalDivider class="px-1" style="margin-left: 7px;" />
-        <ImportButton :uploadFunction="api.upload" :icon="Upload" bg text>导 入</ImportButton>
+        <ImportButton :icon="Upload" :uploadFunction="api.upload" bg text>导 入</ImportButton>
       </div>
     </template>
     <template #toolbar_tools>
@@ -272,7 +293,7 @@ defineExpose({ fetchList });
     </template>
   </vxe-grid>
   <EditDrawer v-if="editOpen" v-model="editOpen" :row-data="rowData" @refresh="handleRefresh" />
-	<CreateDialog v-if="createOpen" v-model="createOpen" @refresh="fetchList" />
+  <CreateDialog v-if="createOpen" v-model="createOpen" @refresh="fetchList" />
 </template>
 
 <style scoped>

+ 18 - 25
src/views/product-manage/product-monitor/component/DataTableSlot.vue

@@ -28,12 +28,6 @@ const color = country ? country.color : '#3875F6';
 const statusText = row.status === 1 ? '在售' : '停售';
 const statusType = row.status === 1 ? 'success' : 'info';
 
-const clicked = ref(false)
-
-function onCancel(val) {
-  console.log('val=> ', val);
-}
-
 function handleEdit() {
   emit('edit-row', row);
 }
@@ -50,23 +44,26 @@ function onConfirm() {
     </div>
     <div v-else-if="field === 'country_code'">
       <el-tag :disable-transitions="true" :style="{ color: color, borderColor: color }" effect="plain" round>
-        {{ country ? country.name : '--' }}
+        {{ country ? country.name : '-' }}
       </el-tag>
     </div>
     <div v-else-if="field === 'shop_name'">
-      <el-tag :disable-transitions="true" :type=getTagType(row.shop_name)>
-        {{ row.shop_name ?? '--' }}
+      <el-tag v-if="row.goods.shop_name" :disable-transitions="true" :type=getTagType(row.shop_name)>
+        {{ row.shop_name }}
       </el-tag>
+      <div v-else>-</div>
     </div>
     <div v-else-if="field === 'tag'">
-      <el-tag :disable-transitions="true" :type=getTagType(row.goods.tag)>
-        {{ row.goods.tag ?? '--' }}
+      <el-tag v-if="row.goods.tag" :disable-transitions="true" :type=getTagType(row.goods.tag)>
+        {{ row.goods.tag }}
       </el-tag>
+      <div v-else>-</div>
     </div>
     <div v-else-if="field === 'brand'">
-      <el-tag :disable-transitions="true" :type=getTagType(row.goods.brand) effect="plain" round>
-        {{ row.goods.brand ?? '--' }}
+      <el-tag v-if="row.goods.brand" :disable-transitions="true" :type=getTagType(row.goods.brand) effect="plain" round>
+        {{ row.goods.brand }}
       </el-tag>
+      <div v-else>-</div>
     </div>
     <div v-else-if="field === 'price_info'">
       <div v-if="row.goods.price > 0" class="font-medium">
@@ -77,17 +74,17 @@ function onConfirm() {
     </div>
     <div v-else-if="field === 'show_price'">
       <div class="font-medium">
-        {{ row.goods.show_price ? row.goods.currency_code + row.goods.show_price : '--' }}
+        {{ row.goods.show_price ? row.goods.currency_code + row.goods.show_price : '-' }}
       </div>
     </div>
     <div v-else-if="field === 'activity_price'">
       <div class="font-medium">
-        {{ row.goods.activity_price ? row.goods.currency_code + row.goods.activity_price : '--' }}
+        {{ row.goods.activity_price ? row.goods.currency_code + row.goods.activity_price : '-' }}
       </div>
     </div>
     <div v-else-if="field === 'minimum_price'">
       <div class="font-medium">
-        {{ row.goods.minimum_price ? row.goods.currency_code + row.goods.minimum_price : '--' }}
+        {{ row.goods.minimum_price ? row.goods.currency_code + row.goods.minimum_price : '-' }}
       </div>
     </div>
     <div v-else-if="field === 'score'">
@@ -103,10 +100,9 @@ function onConfirm() {
         <span v-else>{{ row.goods.score }}</span> <!-- 值为0时显示0 -->
       </template>
       <template v-else>
-        <span>--</span> <!-- 无值时显示'--' -->
+        <span>-</span> <!-- 无值时显示'--' -->
       </template>
     </div>
-
     <div v-else-if="field === 'all_score'">
       <template v-if="row.goods.all_score !== null && row.goods.all_score !== undefined && row.goods.all_score !== ''">
         <el-rate
@@ -120,7 +116,7 @@ function onConfirm() {
         <span v-else>{{ row.goods.all_score }}</span>
       </template>
       <template v-else>
-        <span>--</span>
+        <span>-</span>
       </template>
     </div>
     <div v-else-if="field === 'stars'" class="flex flex-col font-normal" style="min-width: 170px">
@@ -136,19 +132,16 @@ function onConfirm() {
     </div>
     <div v-else-if="field === 'operate'">
       <div class="flex justify-center gap-2">
-        <PermissionButton circle plain type="warning" @click="handleEdit()">
+        <PermissionButton circle plain type="warning" @click="handleEdit">
           <el-icon>
             <Operation />
           </el-icon>
         </PermissionButton>
-        
-
         <el-popconfirm
-            width="220"
             :icon="InfoFilled"
             icon-color="#626AEF"
             title="你确定要删除此项吗?"
-            @cancel="onCancel"
+            width="220"
             @confirm="onConfirm"
         >
           <template #reference>
@@ -161,8 +154,8 @@ function onConfirm() {
           <template #actions="{ confirm, cancel }">
             <el-button size="small" @click="cancel">No!</el-button>
             <el-button
-                type="danger"
                 size="small"
+                type="danger"
                 @click="confirm"
             >
               Yes?

+ 9 - 12
src/views/product-manage/product-monitor/component/EditDrawer.vue

@@ -22,11 +22,8 @@ const props = defineProps({
   rowData: Object
 });
 const { rowData } = props;
-const emit = defineEmits([ 'refresh' ]);
 
-onMounted(() => {
-  console.log('rowData=> ', rowData);
-});
+const emit = defineEmits([ 'refresh' ]);
 
 interface RuleForm {
   asin: any,
@@ -100,29 +97,29 @@ function closeDrawer() {
           ref="ruleFormRef"
           :model="ruleForm"
           :rules="rules"
-          class="mx-2.5 mt-2.5"
+          class="mx-2.5 mt-7"
           label-position="top"
           label-width="auto"
           status-icon>
-        <el-form-item label="ASIN" prop="asin">
+        <el-form-item class="font-medium" label="ASIN" prop="asin">
           <el-input v-model="ruleForm.asin" :disabled="true" />
         </el-form-item>
-        <el-form-item label="SKU" prop="sku">
+        <el-form-item class="font-medium" label="SKU" prop="sku">
           <el-input v-model="ruleForm.sku" />
         </el-form-item>
-        <el-form-item label="店 铺" prop="shop">
+        <el-form-item class="font-medium" label="店 铺" prop="shop">
           <el-input v-model="ruleForm.shop" />
         </el-form-item>
-        <el-form-item label="分 组" prop="tag">
+        <el-form-item class="font-medium" label="分 组" prop="tag">
           <el-input v-model="ruleForm.tag" />
         </el-form-item>
-        <el-form-item label="状 态" prop="status">
+        <el-form-item class="font-medium" label="状 态" prop="status">
           <el-select v-model="ruleForm.status" :disabled="true">
             <el-option label="启用" value="1" />
             <el-option label="暂停" value="2" />
           </el-select>
         </el-form-item>
-        <el-form-item label="国 家" prop="country_code">
+        <el-form-item class="font-medium" label="国 家" prop="country_code">
           <el-select v-model="ruleForm.country_code" placeholder="请选择国家" :disabled="true">
             <el-option
                 v-for="item in staticData.country_code"
@@ -132,7 +129,7 @@ function closeDrawer() {
             </el-option>
           </el-select>
         </el-form-item>
-        <el-form-item label="更新频率" prop="freq">
+        <el-form-item class="font-medium" label="更新频率" prop="freq">
           <el-input-number v-model="ruleForm.freq" />
         </el-form-item>
         <el-form-item>

+ 12 - 6
src/views/product-manage/product-monitor/index.vue

@@ -23,18 +23,18 @@ const { tableHeight } = useTableHeight(titleContainer, queryContainer);
 
 const tableRef: Ref<any> = useTemplateRef('table');
 
-const formInline = reactive({
+const formInline = reactive<any>({
   country: '',
   brand: '',
   group: '',
   status: '',
-  shop: '5',
+  shop: '',
   asin: '',
   sku: '',
   platformId: '',
-  scoreNumber: '',
-  commentNumber: '',
-  displayScore: ''
+  scoreNumber: '-',
+  commentNumber: '-',
+  displayScore: '-'
 });
 provide('query-parameter', formInline);
 
@@ -74,6 +74,12 @@ async function handleQuery() {
   await tableRef.value?.fetchList();
   btnLoading.value = false;
 }
+
+function resetParameter() {
+  for (const key in formInline) {
+    formInline[key] = '';
+  }
+}
 </script>
 
 <template>
@@ -178,7 +184,7 @@ async function handleQuery() {
           <el-button :icon="Search" :loading="btnLoading" class="mb-4" type="primary" @click="handleQuery">
             查 询
           </el-button>
-          <el-button :icon="RefreshRight" color="#ECECF1C9" style="width: 88px; color: #3c3c3c;">
+          <el-button :icon="RefreshRight" color="#ECECF1C9" style="width: 88px; color: #3c3c3c;" @click="resetParameter">
             重 置
           </el-button>
         </div>