瀏覽代碼

feat(sku-manage): 添加公司SKU管理功能

- 新增公司SKU管理页面和相关组件
- 实现品牌选项、种类选项的获取
- 添加表格数据的获取和展示
WanGxC 6 月之前
父節點
當前提交
ff1c456f92

+ 9 - 9
src/views/product-manage/Columns.ts

@@ -12,6 +12,10 @@ export const ProductColumns = [
     field: 'sku', title: 'SKU', minWidth: 'auto', align: 'center', showOverflow: true,
     slots: { default: 'sku' }
   },
+  {
+    field: 'platform_number', title: '平台编号', width: 'auto', align: 'center',
+    slots: { default: 'platform_number' }
+  },
   {
     field: 'country_code', title: '国 家', width: 'auto', align: 'center',
     slots: { default: 'country_code' }
@@ -20,10 +24,6 @@ export const ProductColumns = [
     field: 'brand', title: '品 牌', width: 'auto', align: 'center',
     slots: { default: 'brand' }
   },
-  {
-    field: 'platform_number', title: '平台编号', width: 'auto', align: 'center',
-    slots: { default: 'platform_number' }
-  },
   {
     field: 'shop_name', title: '店 铺', width: 'auto', align: 'center',
     slots: { default: 'shop_name' }
@@ -76,6 +76,10 @@ export const ProductMonitorColumns = [
   //   field: 'sku', title: 'SKU', width: 'auto', align: 'center', showOverflow: true,
   //   slots: { default: 'sku' }
   // },
+  {
+    field: 'platform_number', title: '平台编号', width: 'auto', align: 'center',
+    slots: { default: 'platform_number' }
+  },
   {
     field: 'country_code', title: '国 家', width: 'auto', align: 'center',
     slots: { default: 'country_code' }
@@ -84,10 +88,6 @@ export const ProductMonitorColumns = [
     field: 'brand', title: '品 牌', width: 'auto', align: 'center',
     slots: { default: 'brand' }
   },
-  {
-    field: 'platform_number', title: '平台编号', width: 'auto', align: 'center',
-    slots: { default: 'platform_number' }
-  },
   {
     field: 'shop_name', title: '店 铺', width: 'auto', align: 'center',
     slots: { default: 'shop_name' }
@@ -320,4 +320,4 @@ export const NegativeLabelColumns = [
   {field:'update_datetime',title: '更新时间',minWidth:96,align: 'center',},
   {field:'create_datetime',title: '创建时间',minWidth:96,align: 'center',},
   {field:'operate',title: '操作',align: 'center',slots: { default: 'operate' }}
-]
+]

+ 5 - 2
src/views/product-manage/historical-detail/component/DataTable.vue

@@ -67,9 +67,12 @@ const tagMap: any = {
   reviews: { label: '子asin评论数', color: 'success' }
 };
 
-onBeforeMount(() => {
+onMounted(() => {
+  setTimeout(() => {
+    
   fetchList();
-});
+  }, 500)
+})
 
 async function fetchList() {
   gridOptions.data = [];

+ 6 - 5
src/views/product-manage/historical-detail/component/PriceChart.vue

@@ -21,12 +21,13 @@ let chart: echarts.ECharts | null = null;
 let resizeObserver: ResizeObserver | null = null;
 let chartData: any = ref([]);
 const loading = ref(false)
-
-onBeforeMount(() => {
-  fetchChartData();
-});
+//
+// onBeforeMount(() => {
+//  
+// });
 
 onMounted(() => {
+  fetchChartData();
   initChart();
 });
 
@@ -133,7 +134,7 @@ async function fetchChartData() {
 <template>
   <el-card class="border-none mt-2">
     <div v-show="chartData.length > 0" ref="chartRef" class="chart"></div>
-    <el-empty v-show="chartData.length == 0" description="暂无数据" class="chart" />
+    <el-empty v-if="chartData.length == 0" description="暂无数据" class="chart" />
   </el-card>
 </template>
 

+ 2 - 2
src/views/product-manage/historical-detail/index.vue

@@ -5,7 +5,6 @@
  * @Author: Cheney
  */
 
-import { useTableHeight } from '/@/utils/useCustomHeight';
 import PriceChart from '/@/views/product-manage/historical-detail/component/PriceChart.vue';
 import DataTable from '/@/views/product-manage/historical-detail/component/DataTable.vue';
 
@@ -24,12 +23,13 @@ const { rowData, title } = props;
 <template>
   <div class="drawer-container">
     <el-drawer
+        :destroy-on-close="true"
         ref="editDrawer"
         v-model="isShowHistory"
         :show-close="false"
         :title="`${title} - 历史详情`"
         direction="btt"
-        size="85%"
+        size="80%"
         style="background-color:#F3F4FB;">
       <div class="sticky top-0" style="background-color:#F3F4FB; min-height: 20px; z-index: 99"></div>
       <div class="px-5">

+ 51 - 0
src/views/sku-manage/company-sku/api.ts

@@ -0,0 +1,51 @@
+import { request } from '/@/utils/service';
+
+
+const apiPrefix = '/api/cms/sku-brand/';
+
+export function getTableData(query: any) {
+  return request({
+    url: '/api/cms/sku/',
+    method: 'GET',
+    params: query
+  });
+}
+
+export function getBrandOptions() {
+  return request({
+    url: apiPrefix + 'select/',
+    method: 'GET',
+  });
+}
+
+export function getKindOptions() {
+  return request({
+    url: '/api/cms/sku_kind/dictionary/',
+    method: 'GET',
+  });
+}
+
+export function deleteRow(query: any) {
+  return request({
+    url: apiPrefix + query.id + '/',
+    method: 'DELETE',
+    params: query
+  });
+}
+
+export function updateRow(query: any) {
+  return request({
+    url: apiPrefix + query.id + '/',
+    method: 'PUT',
+    data: query
+  });
+}
+
+export function createAttr(data:any) {
+  return request({
+    url: apiPrefix,
+    method: 'post',
+    data: data
+  })
+}
+

+ 87 - 0
src/views/sku-manage/company-sku/component/CreateDialog.vue

@@ -0,0 +1,87 @@
+<script lang="ts" setup>
+/**
+ * @Name: CreateDialog.vue
+ * @Description: 产品属性-创建对话框
+ * @Author: xinyan
+ */
+
+import { ElMessage, FormInstance, FormRules } from 'element-plus';
+import { DictionaryStore } from '/@/stores/dictionary';
+import { useResponse } from '/@/utils/useResponse';
+import * as api from '../api';
+import { Close, Finished } from '@element-plus/icons-vue';
+import { createAttr, createBrand } from '../api';
+
+const loading = ref(false);
+const createDialog = <Ref>useTemplateRef('createDialog');
+const createOpen = defineModel({ default: false });
+
+const emit = defineEmits(['refresh']);
+
+interface RuleForm {
+	name: any;
+	key: any;
+}
+
+const ruleFormRef = ref<FormInstance>();
+const ruleForm = reactive<RuleForm>({
+	name: '',
+	key: '',
+});
+
+const rules = reactive<FormRules<RuleForm>>({
+	name: [{ required: true, message: '请输入属性名称', trigger: 'blur' }],
+	key: [{ required: true, message: '请输入属性标识', trigger: 'blur' }],
+});
+
+const submitForm = async (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	await formEl.validate(async (valid, fields) => {
+		if (valid) {
+			const body = {
+				name: ruleForm.name,
+				key: ruleForm.key,
+			};
+			const res = await useResponse(api.createAttr, body, loading);
+			if (res.code === 2000) {
+				ElMessage.success('创建成功');
+				createOpen.value = false;
+				emit('refresh');
+			}
+		} else {
+			// createOpen.value = false;
+			ElMessage.error('创建失败,请检查表单');
+		}
+	});
+};
+
+function cancelDialog() {
+	resetForm(ruleFormRef.value);
+	createDialog.value.visible = false;
+}
+
+const resetForm = (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	formEl.resetFields();
+};
+</script>
+
+<template>
+	<el-dialog ref="createDialog" v-model="createOpen" :close-on-click-modal="false" :close-on-press-escape="false" :title="`产品品牌 - 创建 `" style="width: 30%">
+		<el-form ref="ruleFormRef" :model="ruleForm" :rules="rules" class="mx-2.5 mt-5" label-width="auto" status-icon>
+					<el-form-item class="font-medium" label="属性名称" prop="name">
+						<el-input v-model="ruleForm.name" placeholder="请输入属性名称" style="width:328px"/>
+					</el-form-item>
+				<el-form-item class="font-medium" label="属性标识" prop="key">
+						<el-input v-model="ruleForm.key" placeholder="请输入属性标识" style="width:328px"/>
+					</el-form-item>
+		</el-form>
+		<template #footer>
+			<el-button :icon="Close" @click="cancelDialog">取 消</el-button>
+			<el-button :icon="Finished" :loading="loading" type="primary" @click="submitForm(ruleFormRef)">确 定</el-button>
+		</template>
+	</el-dialog>
+</template>
+
+<style scoped>
+</style>

+ 182 - 0
src/views/sku-manage/company-sku/component/DataTable.vue

@@ -0,0 +1,182 @@
+<script lang="ts" setup>
+/**
+ * @Name: Table.vue
+ * @Description: 产品属性表格
+ * @Author: Cheney
+ */
+
+import { ElMessage } from 'element-plus';
+import { Download, InfoFilled, Refresh, Plus } from '@element-plus/icons-vue';
+import { usePagination } from '/@/utils/usePagination';
+import { useTableData } from '/@/utils/useTableData';
+import { useResponse } from '/@/utils/useResponse';
+import { AttributeColumns } from '/@/views/sku-manage/Columns';
+import PermissionButton from '/@/components/PermissionButton/index.vue';
+import DataTableSlot from './DataTableSlot.vue';
+import EditDrawer from './EditDrawer.vue';
+import NoticeDialog from '/src/views/product-manage/product-list/component/NoticeDialog.vue';
+import * as api from '../api';
+import CreateDialog from '/src/views/sku-manage/product-attribute/component/CreateDialog.vue';
+
+
+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({
+  id: 'product-attribute-table',
+  keepSource: true,
+  size: 'small',
+  border: false,
+  round: true,
+  stripe: true,
+  currentRowHighLight: true,
+  height: '100%',
+  customConfig: {
+    // mode: 'drawer',
+    // immediate: true,
+    storage: true,
+  },
+  toolbarConfig: {
+    size: 'large',
+    custom: true,
+    slots: {
+      tools: 'toolbar_tools',
+      buttons: 'toolbar_buttons'
+    }
+  },
+  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 createOpen = ref(false);
+const rowData = ref({});
+
+const dialogVisible = ref(false);
+
+onMounted(() => {
+  fetchList();
+});
+
+async function fetchList() {
+  gridOptions.data = [];
+  gridOptions.columns = [];
+
+  const query = {
+    asin: queryParameter?.asin,
+    sku: queryParameter?.sku,
+    country_code: queryParameter?.country,
+    shop_id: queryParameter?.shop,
+    region: queryParameter?.region,
+    delivery: queryParameter?.delivery,
+    status: queryParameter?.status
+  };
+
+  await useTableData(api.getTableData, query, gridOptions);
+  await gridRef.value.loadColumn(AttributeColumns);
+  gridOptions.showHeader = Boolean(gridOptions.data?.length);
+}
+
+function handleRefresh() {
+  fetchList();
+}
+
+function handleEdit(row: any) {
+  editOpen.value = true;
+  rowData.value = row;
+}
+
+function handleCreate() {
+	createOpen.value = true;
+}
+
+async function singleDelete(row: any) {
+  const res = await useResponse(api.deleteRow, row);
+  if (res.code === 2000) {
+    ElMessage.success({ message: '删除成功', plain: true });
+    handleRefresh();
+  }
+}
+
+const gridEvents = {
+  custom ({ type }: any) {
+    // console.log(`点击 ${type}`)
+    if (type == 'confirm') {
+      fetchList();
+    }
+  }
+}
+
+defineExpose({ fetchList });
+</script>
+
+<template>
+  <vxe-grid ref="gridRef" v-bind="gridOptions" v-on="gridEvents">
+    <template #toolbar_buttons>
+      <PermissionButton type="primary" :icon="Plus" plain round @click="handleCreate">新 增</PermissionButton>
+    </template>
+    <!-- 工具栏右侧 -->
+    <template #toolbar_tools>
+      <el-button circle class="toolbar-btn mr-3" @click="handleRefresh">
+        <el-icon>
+          <Refresh />
+        </el-icon>
+      </el-button>
+    </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 AttributeColumns" #[`${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" />
+  <NoticeDialog v-if="dialogVisible" v-model="dialogVisible" :row-data="rowData" />
+	<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>

+ 73 - 0
src/views/sku-manage/company-sku/component/DataTableSlot.vue

@@ -0,0 +1,73 @@
+<script lang="ts" setup>
+/**
+ * @Name: DataTableSlot.vue
+ * @Description: 产品属性-单元格插槽
+ * @Author: Cheney
+ */
+
+import { Delete, InfoFilled, Operation } from '@element-plus/icons-vue';
+import PermissionButton from '/@/components/PermissionButton/index.vue';
+
+
+const props = defineProps<{
+  row: any,
+  field: any
+}>();
+const { row, field } = props;
+
+const emit = defineEmits([ 'edit-row', 'handle-delete' ]);
+
+function handleEdit() {
+  emit('edit-row', row);
+}
+
+function onConfirm() {
+  emit('handle-delete', row);
+}
+</script>
+
+<template>
+  <div class="font-medium">
+    <div v-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[field] }}
+    </div>
+  </div>
+</template>
+
+<style scoped>
+
+</style>

+ 112 - 0
src/views/sku-manage/company-sku/component/EditDrawer.vue

@@ -0,0 +1,112 @@
+<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 { useResponse } from '/@/utils/useResponse';
+import * as api from '../api';
+
+
+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 {
+  name: any,
+  key: any
+}
+
+const ruleFormRef = ref<FormInstance>();
+const ruleForm = reactive<RuleForm>({
+  name: rowData?.name,
+  key: rowData?.key,
+});
+
+const rules = reactive<FormRules<RuleForm>>({
+  // shop_name: [ { 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);
+    }
+  });
+};
+
+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="属性名称" prop="name">
+          <el-input v-model="ruleForm.name" />
+        </el-form-item>
+        <el-form-item class="font-medium" label="属性标识" prop="key">
+          <el-input v-model="ruleForm.key" />
+        </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>

+ 122 - 0
src/views/sku-manage/company-sku/index.vue

@@ -0,0 +1,122 @@
+<script lang="ts" setup>
+/**
+ * @Name: index.vue
+ * @Description: 公司SKU
+ * @Author: Cheney
+ */
+import { RefreshRight, Search } from '@element-plus/icons-vue';
+import DataTable from './component/DataTable.vue';
+import VerticalDivider from '/@/components/VerticalDivider/index.vue';
+import { useTableHeight } from '/@/utils/useTableHeight';
+import { useResponse } from '/@/utils/useResponse';
+import * as api from './api';
+
+
+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>({
+  brandName: '',
+  kind: '',
+  status: '',
+  sku: ''
+});
+provide('query-parameter', formInline);
+
+const brandOptions: any = ref([]);
+const kindOptions: any = ref([]);
+const statusOptions: any = ref([
+  { key: 1, name: '草 稿' },
+  { key: 3, name: '已发布' }
+]);
+
+onBeforeMount(() => {
+  fetchOptions();
+});
+
+async function fetchOptions() {
+  brandOptions.value = (await useResponse(api.getBrandOptions)).data;
+  kindOptions.value = (await useResponse(api.getKindOptions)).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">
+    <el-card class="h-full" style="color: rgba(0, 0, 0, 0.88);">
+      <div ref="titleContainer" class="text-xl font-semibold pb-5">公司SKU</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: 5px;">
+              <el-col :span="4">
+                <div class="flex items-center">
+                  <span class="mr-2">品牌名称</span>
+                  <el-select v-model="formInline.brandName" clearable placeholder="请选择品牌名称">
+                    <el-option v-for="item in brandOptions" :key="item.id" :label="item.brand_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.kind" clearable placeholder="请选择种类">
+                    <el-option v-for="item in kindOptions" :key="item.id" :label="item.name" :value="item.id" />
+                  </el-select>
+                </div>
+              </el-col>
+              <el-col :span="3">
+                <div class="flex items-center">
+                  <span class="mr-2">状 态</span>
+                  <el-select v-model="formInline.status" clearable placeholder="请选择">
+                    <el-option v-for="item in statusOptions" :key="item.key" :label="item.name" :value="item.key" />
+                  </el-select>
+                </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 gap-1.5 ml-5">
+          <el-button :icon="Search" :loading="btnLoading" 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>