Parcourir la source

feat(store-manage): 新增在线商品管理功能

- 添加在线商品管理相关的 API 接口
- 实现在线商品管理的表格组件和相关功能
- 新增在线商品管理的路由和页面组件
- 优化市场店铺表格组件,支持更多筛选条件
WanGxC il y a 7 mois
Parent
commit
009f7f8450

Fichier diff supprimé car celui-ci est trop grand
+ 295 - 294
package-lock.json


+ 2 - 2
package.json

@@ -71,11 +71,11 @@
 		"eslint": "^8.54.0",
 		"eslint-plugin-vue": "^9.8.0",
 		"prettier": "^2.8.1",
-		"sass": "^1.80.1",
+		"sass": "1.56.2",
 		"typescript": "^5.6.2",
 		"unplugin-auto-import": "^0.18.3",
 		"unplugin-vue-components": "^0.27.4",
-		"vite": "^5.4.9",
+		"vite": "^4.5.3",
 		"vite-plugin-lazy-import": "^1.0.7",
 		"vite-plugin-vue-setup-extend": "^0.4.0",
 		"vue-eslint-parser": "^9.1.0"

+ 1 - 2
src/views/product-manage/product-list/component/DataTable.vue

@@ -5,7 +5,7 @@
  * @Author: Cheney
  */
 
-import { Download, Message, Money, Open, Refresh } from '@element-plus/icons-vue';
+import { Download, InfoFilled, Message, Money, Open, Refresh } from '@element-plus/icons-vue';
 import { ElMessage } from 'element-plus';
 import { usePagination } from '/@/utils/usePagination';
 import { useTableData } from '/@/utils/useTableData';
@@ -257,7 +257,6 @@ defineExpose({ fetchList });
 				:icon="InfoFilled"
 				icon-color="#626AEF"
 				title="是否确认导出当前时间内所有数据项?"
-				@cancel="onCancel"
 				@confirm="handleDownload"
 			>
 				<template #reference>

+ 45 - 0
src/views/store-manage/Columns.ts

@@ -29,3 +29,48 @@ export const MarketStoreColumns = [
     slots: { default: 'create_datetime' }
   }
 ];
+
+export const OnlineMerchandiseColumns = [
+  { type: 'seq', title: 'No.', width: 60, align: 'center' },
+  { field: 'asin', title: 'ASIN', minWidth: 'auto', align: 'center', slots: { default: 'asin' } },
+  { field: 'sku', title: 'SKU', minWidth: 'auto', align: 'center', slots: { default: 'sku' } },
+  { field: 'platform_number', title: '平台编号', minWidth: 'auto', align: 'center',
+    slots: { default: 'platform_number' }
+  },
+  {
+    field: 'shop_name', title: '店 铺', minWidth: 'auto', align: 'center',
+    slots: { default: 'shop_name' }
+  },
+  {
+    field: 'country_code', title: '国 家', minWidth: 'auto', align: 'center',
+    slots: { default: 'country_code' }
+  },
+  {
+    field: 'region', title: '站 点', minWidth: 'auto', align: 'center',
+    slots: { default: 'region' }
+  },
+  {
+    field: 'quantity', title: '可售数量', width: 'auto', align: 'center',
+    slots: { default: 'quantity' }
+  },
+  {
+    field: 'fulfillment_channel', title: '配送方式', width: 'auto', align: 'center',
+    slots: { default: 'fulfillment_channel' }
+  },
+  {
+    field: 'raw_launch_datetime', title: '上架时间', minWidth: 'auto', align: 'center',
+    slots: { default: 'raw_launch_datetime' }
+  },
+  {
+    field: 'price', title: '价 格', minWidth: 'auto', align: 'center',
+    slots: { default: 'price' }
+  },
+  {
+    field: 'status', title: '状 态', width: 80, align: 'center',
+    slots: { default: 'status' }
+  },
+  {
+    field: 'create_datetime', title: '创建时间', minWidth: 'auto', align: 'center',
+    slots: { default: 'create_datetime' }
+  }
+];

+ 0 - 96
src/views/store-manage/market-store/api.ts

@@ -11,102 +11,6 @@ export function getTableData(query: any) {
   });
 }
 
-export function updateRow(body: any) {
-  return request({
-    url: apiPrefix + body.id + '/',
-    method: 'PUT',
-    data: body
-  });
-}
-
-export function getRegionOptions() {
-  return request({
-    url: apiPrefix + 'tags/',
-    method: 'GET'
-  });
-}
-
-export function getBrandsOptions() {
-  return request({
-    url: apiPrefix + 'brands/',
-  });
-}
-
-export function getShopOptions() {
-  return request({
-    url: '/api/choice/marketplace_shops/select/',
-    method: 'GET',
-  });
-}
-
-export function getStaffsOptions(query: any) {
-  return request({
-    url: '/api/system/user/select/',
-    params: query,
-    method: 'GET'
-  })
-}
-
-export function getExistingStaffs(query: any) {
-  return request({
-    url: apiPrefix + 'alarm-manage/',
-    params: query,
-    method: 'GET'
-  })
-}
-
-export function postMonitor(body: any) {
-  return request({
-    url: apiPrefix + 'monitor/',
-    data: body,
-    method: 'POST'
-  })
-}
-
-export function postStaffs(body: any) {
-  return request({
-    url: apiPrefix + 'alarm-manage/',
-    data: body,
-    method: 'post'
-  })
-}
-
-export function uploadFile(body: any){
-  return request({
-    url: 'api/system/file/',
-    method: 'POST',
-    data: body,
-    headers: { 'Content-Type':'multipart/form-data' },
-  });
-}
-
-// 商品变更通知导入
-export function uploadChangeNotice(body: any){
-  return request({
-    url: '/api/choice/goods/alarm/import_data/',
-    method: 'POST',
-    data: body,
-  });
-}
-
-// 商品导入
-export function uploadProducts(body: any){
-  return request({
-    url: '/api/choice/goods/import_data/',
-    method: 'POST',
-    data: body,
-  });
-}
-
-// 指导价格导入
-export function uploadPrice(body: any){
-  return request({
-    url: '/api/choice/goods/GuidancePrice/import_data/',
-    method: 'POST',
-    data: body,
-  });
-}
-
 // 导出
 export function exportData(query: any) {
   return request({

+ 2 - 3
src/views/store-manage/market-store/component/DataTable.vue

@@ -1,12 +1,11 @@
 <script lang="ts" setup>
 /**
  * @Name: Table.vue
- * @Description: 商品列表表格
+ * @Description: 市场店铺表格
  * @Author: Cheney
  */
 
 import { Refresh } from '@element-plus/icons-vue';
-import { ElMessage } from 'element-plus';
 import { usePagination } from '/@/utils/usePagination';
 import { useTableData } from '/@/utils/useTableData';
 import { MarketStoreColumns } from '/@/views/store-manage/Columns';
@@ -44,7 +43,7 @@ const gridOptions: any = reactive({
     isHover: true
   },
   columnConfig: {
-    resizable: true,
+    resizable: true
   },
   pagerConfig: {
     total: tableOptions.value.total,

+ 29 - 0
src/views/store-manage/online-merchandise/api.ts

@@ -0,0 +1,29 @@
+import { request } from '/@/utils/service';
+
+
+const apiPrefix = '/api/choice/listings/';
+
+export function getTableData(query: any) {
+  return request({
+    url: apiPrefix,
+    method: 'GET',
+    params: query
+  });
+}
+
+export function getShopOptions() {
+  return request({
+    url: '/api/choice/marketplace_shops/',
+    method: 'GET',
+  });
+}
+
+// 导出
+export function exportData(query: any) {
+  return request({
+    url: '/api/choice/goods/export_data/',
+    method: 'GET',
+    params: query,
+    responseType: 'blob'
+  });
+}

+ 168 - 0
src/views/store-manage/online-merchandise/component/DataTable.vue

@@ -0,0 +1,168 @@
+<script lang="ts" setup>
+/**
+ * @Name: Table.vue
+ * @Description: 市场店铺表格
+ * @Author: Cheney
+ */
+
+import { Download, InfoFilled, Refresh } from '@element-plus/icons-vue';
+import { usePagination } from '/@/utils/usePagination';
+import { useTableData } from '/@/utils/useTableData';
+import { OnlineMerchandiseColumns } from '/@/views/store-manage/Columns';
+import DataTableSlot from './DataTableSlot.vue';
+import EditDrawer from '/src/views/product-manage/product-list/component/EditDrawer.vue';
+import NoticeDialog from '/src/views/product-manage/product-list/component/NoticeDialog.vue';
+import * as api from '../api';
+
+
+interface Parameter {
+  country: string,
+  shop: string,
+  region: string,
+  delivery: string,
+  status: string,
+  asin: string,
+  sku: string
+}
+
+const queryParameter: Parameter | undefined = inject('query-parameter');
+const { tableOptions, handlePageChange } = usePagination(fetchList);
+
+const gridRef = ref();
+const gridOptions: any = reactive({
+  size: 'small',
+  border: false,
+  round: true,
+  stripe: true,
+  currentRowHighLight: true,
+  height: '100%',
+  toolbarConfig: {
+    size: 'large',
+    custom: true,
+    slots: {
+      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 editOpen = ref(false);
+const rowData = ref({});
+
+const dialogVisible = ref(false);
+
+onMounted(() => {
+  fetchList();
+});
+
+async function fetchList() {
+  gridOptions.data = [];
+  gridOptions.columns = [];
+
+  const query = {
+    asin: queryParameter?.asin,
+    sku__startswith: queryParameter?.sku,
+    country_code: queryParameter?.country,
+    shop_id: queryParameter?.shop,
+    shop__region: queryParameter?.region,
+    fulfillment_channel: queryParameter?.delivery,
+    status: queryParameter?.status,
+  };
+
+  await useTableData(api.getTableData, query, gridOptions);
+  await gridRef.value.loadColumn(OnlineMerchandiseColumns);
+  gridOptions.showHeader = Boolean(gridOptions.data?.length);
+}
+
+function handleRefresh() {
+  fetchList();
+}
+
+async function handleDownload() {
+}
+
+defineExpose({ fetchList });
+</script>
+
+<template>
+  <vxe-grid ref="gridRef" v-bind="gridOptions">
+    
+    <!-- 工具栏右侧 -->
+    <template #toolbar_tools>
+      <el-button circle class="toolbar-btn" @click="handleRefresh">
+        <el-icon>
+          <Refresh />
+        </el-icon>
+      </el-button>
+      <el-popconfirm
+          width="220"
+          :icon="InfoFilled"
+          icon-color="#626AEF"
+          title="是否确认导出当前时间内所有数据项?"
+          @confirm="handleDownload"
+      >
+        <template #reference>
+          <el-button circle class="mr-3 toolbar-btn">
+            <el-icon>
+              <Download />
+            </el-icon>
+          </el-button>
+        </template>
+        <template #actions="{ confirm, cancel }">
+          <el-button size="small" @click="cancel">No!</el-button>
+          <el-button
+              type="danger"
+              size="small"
+              @click="confirm"
+          >
+            Yes?
+          </el-button>
+        </template>
+      </el-popconfirm>
+    </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 OnlineMerchandiseColumns" #[`${col.field}`]="{ row }">
+      <DataTableSlot :key="row.id" :field="col.field" :row="row" />
+    </template>
+  </vxe-grid>
+  <EditDrawer v-if="editOpen" v-model="editOpen" :row-data="rowData" @refresh="handleRefresh" />
+  <NoticeDialog v-if="dialogVisible" v-model="dialogVisible" :row-data="rowData" />
+</template>
+
+<style scoped>
+.toolbar-btn {
+  width: 34px;
+  height: 34px;
+  font-size: 18px
+}
+
+:deep(.custom-el-input .el-select__wrapper) {
+  border-radius: 20px;
+}
+</style>

+ 62 - 0
src/views/store-manage/online-merchandise/component/DataTableSlot.vue

@@ -0,0 +1,62 @@
+<script lang="ts" setup>
+/**
+ * @Name: DataTableSlot.vue
+ * @Description: 商品列表-单元格插槽
+ * @Author: Cheney
+ */
+
+import { useCountryInfoStore } from '/@/stores/countryInfo';
+import { getTagType } from '/@/utils/useTagColor';
+
+
+const props = defineProps<{
+  row: any,
+  field: any
+}>();
+const { row, field } = props;
+
+const countryInfoStore = useCountryInfoStore();
+const country = countryInfoStore.Countries.find(c => c.code == row.country_code);
+const color = country ? country.color : '#3875F6';
+const region = countryInfoStore.Region.find(r => r.code == row.shop.region);
+</script>
+
+<template>
+  <div class="font-medium">
+    <div v-if="field === 'region'">
+      <el-tag :disable-transitions="true" :type=getTagType(row.shop.region)>
+        {{ region ? region.name : '-' }}
+      </el-tag>
+    </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 === 'platform_number'">
+      {{ row.shop.platform_number }}
+    </div>
+    <div v-else-if="field === 'shop_name'">
+      {{ row.shop.name }}
+    </div>
+    <div v-else-if="field === 'fulfillment_channel'">
+      <el-tag :disable-transitions="true" :type="row.fulfillment_channel === 'FBM' ? 'primary' : 'warning'">
+        {{ row.fulfillment_channel }}
+      </el-tag>
+    </div>
+    <div v-else-if="field === 'status'">
+      <el-tag :disable-transitions="true"
+              :type="row.status === 'Active' ? 'success' : row.status === 'Inactive' ? 'warning' : 'danger'">
+        {{ row.status === 'Active' ? '在售' : row.status === 'Inactive' ? '不在售' : '不完整' }}
+      </el-tag>
+    </div>
+    <!-- 动态获取 -->
+    <div v-else>
+      {{ row[field] }}
+    </div>
+  </div>
+</template>
+
+<style scoped>
+
+</style>

+ 157 - 0
src/views/store-manage/online-merchandise/index.vue

@@ -0,0 +1,157 @@
+<script lang="ts" setup>
+/**
+ * @Name: index.vue
+ * @Description: 市场店铺
+ * @Author: Cheney
+ */
+
+import VerticalDivider from '/src/components/VerticalDivider/index.vue';
+import { RefreshRight, Search } from '@element-plus/icons-vue';
+import { useTableHeight } from '/@/utils/useTableHeight';
+import DataTable from './component/DataTable.vue';
+import { DictionaryStore } from '/@/stores/dictionary';
+import { useTemplateRef } from 'vue';
+import { useCountryInfoStore } from '/@/stores/countryInfo';
+import { useResponse } from '/@/utils/useResponse';
+import * as api from './api';
+
+
+const { data: staticData } = DictionaryStore();
+const countryInfoStore = useCountryInfoStore();
+
+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 btnLoading = ref(false);
+
+const formInline = reactive<any>({
+  country: '',
+  shop: '',
+  region: '',
+  delivery: '',
+  status: '',
+  asin: '',
+  sku: '',
+});
+provide('query-parameter', formInline);
+
+const shopOptions = ref<any>([]);
+
+onBeforeMount(() => {
+  fetchShopOptions();
+})
+
+async function fetchShopOptions() {
+  const res = await useResponse(api.getShopOptions)
+  shopOptions.value = res.data
+}
+
+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.shop" clearable placeholder="请选择店铺">
+                    <el-option v-for="item in shopOptions" :key="item.id" :label="item.name"
+                               :value="item.id" />
+                  </el-select>
+                </div>
+              </el-col>
+              <el-col :span="5">
+                <div class="flex items-center">
+                  <span class="mr-2">站 点</span>
+                  <el-select v-model="formInline.region" clearable placeholder="请选择站点">
+                    <el-option v-for="item in countryInfoStore.Region" :label="item.name" :value="item.code" />
+                  </el-select>
+                </div>
+              </el-col>
+              <el-col :span="5">
+                <div class="flex items-center">
+                  <span class="mr-2">配送方式</span>
+                  <el-select v-model="formInline.delivery" clearable placeholder="请选择配送方式">
+                    <el-option label="FBM" value="FBM" />
+                    <el-option label="FBA" value="FBA" />
+                  </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 label="在售" value="Active" />
+                    <el-option label="不在售" value="Inactive" />
+                    <el-option label="不完整" value="Incomplete" />
+                  </el-select>
+                </div>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20" style="margin-bottom: 16px;">
+              <el-col :span="6">
+                <div class="flex items-center">
+                  <span class="mr-2">ASIN</span>
+                  <el-input v-model="formInline.asin" clearable placeholder="请输入ASIN" />
+                </div>
+              </el-col>
+              <el-col :span="6">
+                <div class="flex items-center">
+                  <span class="mr-2">SKU</span>
+                  <el-input v-model="formInline.sku" clearable placeholder="请输入SKU" />
+                </div>
+              </el-col>
+            </el-row>
+          </div>
+        </div>
+        <VerticalDivider />
+        <div class="flex flex-col gap-1.5 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>
+
+</style>

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff