Browse Source

```
feat(goods): 新增分类与删除分类接口及功能实现

- 新增 `fetchAddCategory` 和 `fetchDeleteCategory` API 接口
- 在门店前台类目管理中支持添加三级分类和删除操作
- 更新表单逻辑以区分新增与编辑场景
- 增加面包屑显示当前分类层级路径
- 完善关联商品弹窗中的分类层级展示
- 优化组件类型定义和数据处理逻辑
```

zhangtao 3 days ago
parent
commit
48e80a205f

+ 21 - 0
src/service/api/goods/desk-category/index.ts

@@ -44,6 +44,27 @@ export function fetchUpdateCategory(data: any) {
     data
   });
 }
+/**
+ * 新增分类
+ * @param data
+ * @returns
+ */
+export function fetchAddCategory(data: any) {
+  return request({
+    url: '/platform/shopCategory',
+    method: 'POST',
+    data
+  });
+}
+/**
+ * 删除分类
+ */
+export function fetchDeleteCategory(id: any) {
+  return request({
+    url: `/platform/shopCategory/${id}`,
+    method: 'DELETE'
+  });
+}
 
 /**
  * 查询门店前台类目导入记录

+ 47 - 1
src/typings/api.d.ts

@@ -1393,6 +1393,14 @@ declare namespace Api {
       cancel: number;
     }
     interface OrderRefund {
+      /**
+       * 退款单总分销金额
+       */
+      refundTotalMoney: number;
+      /**
+       * 退款轨迹
+       */
+      orderRefundRecordList?: OrderRefundRecord[];
       /**
        * 申请时间
        */
@@ -1532,7 +1540,45 @@ declare namespace Api {
        * 买家ID
        */
       userId?: string;
-
+      /**
+       * 订单抵扣积分
+       */
+      OrderOffsetPoints: number;
+      /**
+       * 订单实际总值(用户实付)积分
+       */
+      orderActualTotal: number;
+      [property: string]: any;
+    }
+    interface OrderRefundRecord {
+      /**
+       * 审核状态(1-申请原因,2-商家待审核,5-用户待发货,7-待商家收货,10-审核通过,20-驳回,30-退款成功)
+       */
+      auditStatus?: number;
+      /**
+       * 创建时间
+       */
+      createTime?: string;
+      /**
+       * 退款记录ID
+       */
+      id?: number;
+      /**
+       * 说明
+       */
+      instructions?: string;
+      /**
+       * 退款单ID
+       */
+      orderRefundId?: number;
+      /**
+       * 排序
+       */
+      sort?: number;
+      /**
+       * 修改时间
+       */
+      updateTime?: string;
       [property: string]: any;
     }
 

+ 2 - 0
src/typings/components.d.ts

@@ -117,6 +117,8 @@ declare module 'vue' {
     NText: typeof import('naive-ui')['NText']
     NTh: typeof import('naive-ui')['NTh']
     NThead: typeof import('naive-ui')['NThead']
+    NTimeline: typeof import('naive-ui')['NTimeline']
+    NTimelineItem: typeof import('naive-ui')['NTimelineItem']
     NTooltip: typeof import('naive-ui')['NTooltip']
     NTr: typeof import('naive-ui')['NTr']
     NTransfer: typeof import('naive-ui')['NTransfer']

+ 50 - 1
src/views/delivery/after-sales-order/after-sales-order.ts

@@ -174,6 +174,55 @@ export const orderColumns: NaiveUI.TableColumn<Api.delivery.OrderRefundSku>[] =
   {
     title: '退款金额',
     key: 'skuPrice',
-    width: 100
+    width: 100,
+    render: row => {
+      return (Number(row.skuPrice) * Number(row.productCount)).toFixed(2);
+    }
   }
 ];
+// 1-申请原因,2-商家待审核,5-用户待发货,7-待商家收货,10-审核通过,20-驳回,30-退款成功
+export enum refundTimeEnum {
+  /**
+   * 申请原因
+   */
+  Reason = 1,
+  /**
+   * 商家待审核
+   */
+  WaitAudit = 2,
+  /**
+   * 申请通过
+   */
+  AuditPass = 3,
+  /**
+   * 用户待发货
+   */
+  WaitDelivery = 5,
+  /**
+   * 待商家收货
+   */
+  WaitReceive = 7,
+  /**
+   * 审核通过
+   */
+  AuditSuccess = 10,
+  /**
+   * 驳回
+   */
+  AuditReject = 20,
+  /**
+   * 退款成功
+   */
+  RefundSuccess = 30
+}
+
+export const refundTime = {
+  [refundTimeEnum.Reason]: '申请原因',
+  [refundTimeEnum.WaitAudit]: '商家待审核',
+  [refundTimeEnum.WaitDelivery]: '用户待发货',
+  [refundTimeEnum.WaitReceive]: '待商家收货',
+  [refundTimeEnum.AuditSuccess]: '审核通过',
+  [refundTimeEnum.AuditReject]: '驳回',
+  [refundTimeEnum.RefundSuccess]: '退款成功',
+  [refundTimeEnum.AuditPass]: '申请通过'
+};

+ 38 - 16
src/views/delivery/after-sales-order/order-modal.vue

@@ -4,7 +4,7 @@ import { fetchGetAfterSalesOrderDetail } from '@/service/api/delivery/after-sale
 import { useAppStore } from '@/store/modules/app';
 import { copyTextToClipboard } from '@/utils/zt';
 import { useModal } from '@/components/zt/Modal/hooks/useModal';
-import { TypeText, orderColumns, refundEnum, refundStatus } from './after-sales-order';
+import { TypeText, orderColumns, refundEnum, refundStatus, refundTime, refundTimeEnum } from './after-sales-order';
 const [registerModal, { openModal, setModalLoading }] = useModal({
   title: '处理退款',
   width: 1200,
@@ -22,12 +22,7 @@ const goodsMoney = computed(() => {
     return acc + Number(item.orderItem.productTotalAmount) * Number(item.orderItem.prodCount);
   }, 0);
 });
-const refundTMoney = computed(() => {
-  // 后端不算,让前端算
-  return orderInfo.value?.orderRefundSkuList?.reduce((acc, item) => {
-    return acc + Number(item.skuPrice) * Number(item.productCount);
-  }, 0);
-});
+
 defineExpose({
   handleOpenOrder
 });
@@ -41,6 +36,11 @@ async function handleOpenOrder(refundId: number) {
   }
   setModalLoading(false);
 }
+function getImgFils() {
+  return orderInfo.value?.photoFiles?.split(',').map(item => {
+    return `${import.meta.env.VITE_OSS_BASE_URL}${item}`;
+  });
+}
 </script>
 
 <template>
@@ -56,7 +56,7 @@ async function handleOpenOrder(refundId: number) {
               </div>
             </div>
           </NTag>
-          <NTag>申请人: | 申请时间:{{ orderInfo.applyTime }}</NTag>
+          <NTag>申请人: {{ orderInfo.receiver }} | 申请时间:{{ orderInfo.applyTime }}</NTag>
         </NFlex>
         <NFlex vertical>
           <div class="text-16px font-semibold">
@@ -89,13 +89,13 @@ async function handleOpenOrder(refundId: number) {
         <NDescriptionsItem label="售后信息">
           <div>售后方式:{{ TypeText[orderInfo.applyType - 1] || '未知状况' }}</div>
           <div>退款原因:{{ orderInfo.buyerReason || '---' }}</div>
-          <div>退款件数: {{ orderInfo.goodsNum + '件' || '---' }}</div>
+          <div>退款件数: {{ orderInfo.goodsNum || '---' }}</div>
         </NDescriptionsItem>
         <NDescriptionsItem label="购买信息">
-          <div>商品总额: {{ goodsMoney + '元' || '---' }}</div>
-          <div>配送费(快递): {{ orderInfo.freightAmount + '元' || '---' }}</div>
-          <div>积分抵扣: {{ orderInfo.offsetPoints + '元' || '---' }}</div>
-          <div>实际付款: {{ goodsMoney + '元' || '---' }}</div>
+          <div>商品总额: {{ goodsMoney || '---' }}</div>
+          <div>配送费(快递): {{ orderInfo.freightAmount || '---' }}</div>
+          <div>积分抵扣: {{ orderInfo.OrderOffsetPoints || '---' }}元</div>
+          <div>实际付款: {{ orderInfo.orderActualTotal || '---' }}元</div>
         </NDescriptionsItem>
       </NDescriptions>
       <NDivider />
@@ -103,14 +103,36 @@ async function handleOpenOrder(refundId: number) {
         <NDataTable :columns="orderColumns" :data="orderInfo.orderRefundSkuList" :bordered="false" />
       </NCard>
       <div class="flex items-center justify-end">
-        <div class="flex items-center">
-          <div class="text-16px font-semibold">退款总金额: {{ refundTMoney?.toFixed(2) }}元</div>
+        <div class="flex flex-wrap items-center">
+          <div class="text-16px font-semibold">退款总金额: {{ orderInfo.refundTotalMoney || '--' }}元</div>
           <div v-if="orderInfo.returnMoneySts == refundEnum.REFUND_SUCCESS" class="ml2 flex items-center text-gray">
-            <div>退还金额:---</div>
+            <div>退还金额:{{ orderInfo.refundAmount }}</div>
             <div class="ml2">退还积分:{{ orderInfo.offsetPoints }} (已过期{{ orderInfo.refundExpiredScore }})</div>
           </div>
         </div>
       </div>
+      <div v-if="orderInfo.orderRefundRecordList" class="mt3">
+        <!-- 1-申请原因,2-商家待审核,5-用户待发货,7-待商家收货,10-审核通过,20-驳回,30-退款成功 -->
+        <NTimeline>
+          <NTimelineItem
+            v-for="item in orderInfo.orderRefundRecordList"
+            :key="item.id"
+            :type="item.auditStatus == refundTimeEnum.RefundSuccess ? 'success' : 'default'"
+            :title="refundTime[item.auditStatus as keyof typeof refundTime]"
+            :time="item.createTime"
+          >
+            <div>{{ item.instructions }}</div>
+            <div
+              v-if="item.auditStatus == refundTimeEnum.Reason && orderInfo.photoFiles"
+              class="mt2 flex flex-wrap items-center"
+            >
+              <template v-for="items in getImgFils()" :key="items">
+                <NImage :width="100" :height="100" :src="items" lazy class="mb2 mr3 h100px w100px" object-fit="cover" />
+              </template>
+            </div>
+          </NTimelineItem>
+        </NTimeline>
+      </div>
     </div>
   </BasicModal>
 </template>

+ 1 - 1
src/views/delivery/normal-order/component/normal-modal.vue

@@ -186,7 +186,7 @@ function handleCopy() {
             <NTbody>
               <NTr>
                 <NTd>商品总额</NTd>
-                <NTd>{{ orderInfo.actualTotal }}</NTd>
+                <NTd>{{ orderInfo.total }}</NTd>
               </NTr>
               <NTr>
                 <NTd>配送费(快递)</NTd>

+ 1 - 2
src/views/goods/desk-category/components/related-goods-modal.vue

@@ -46,8 +46,7 @@ async function handleSave() {
   });
   emit('submit');
   closeModal();
-
-  console.log(111);
+  value.value = [];
 }
 </script>
 

+ 8 - 0
src/views/goods/desk-category/components/related-goods.vue

@@ -89,6 +89,14 @@ function handleRefsh() {
 
 <template>
   <BasicModal @register="register">
+    <div class="flex items-center pl3">
+      分类:
+      <NBreadcrumb separator=">">
+        <NBreadcrumbItem>{{ searchForm?.level1Name }}</NBreadcrumbItem>
+        <NBreadcrumbItem>{{ searchForm?.level2Name }}</NBreadcrumbItem>
+        <NBreadcrumbItem>{{ searchForm?.level3Name }}</NBreadcrumbItem>
+      </NBreadcrumb>
+    </div>
     <LayoutTable>
       <ZTable
         :columns="columns"

+ 192 - 63
src/views/goods/desk-category/index.vue

@@ -2,6 +2,8 @@
 import { nextTick, ref, useTemplateRef } from 'vue';
 import { NButton, NImage, NTag } from 'naive-ui';
 import {
+  fetchAddCategory,
+  fetchDeleteCategory,
   fetchGategoryImport,
   fetchGetAllStoreList,
   fetchGetDeskCategoryList,
@@ -56,69 +58,16 @@ const [registerSearchForm, { getFieldsValue: getSearchForm, setFieldsValue }] =
   collapsedRows: 1
 });
 const importTemplateRef = useTemplateRef('importTemplateRef');
-const tableColumns: NaiveUI.TableColumn<Api.goods.ShopCategory>[] = [
-  {
-    title: '分类名称',
-    key: 'name'
-  },
-  {
-    title: '分类图标',
-    key: 'icon',
-    render(row) {
-      return <NImage src={row.icon} class="h-[40px] w-[40px]" />;
-    }
-  },
-  {
-    title: '关联商品',
-    key: 'prodCount',
-    render: row => {
-      return row.level == 3 ? `${row.prodCount}个商品` : '';
-    }
-  },
-  {
-    title: '标签',
-    key: 'labelName',
-    render: row => {
-      return row.labelName ? <NTag> {row.labelName} </NTag> : '';
-    }
-  },
-  {
-    title: '排序',
-    key: 'num'
-  },
-  {
-    title: '操作',
-    key: 'op',
-    fixed: 'right',
-    align: 'center',
-    render: row => (
-      <div class="flex-center gap-8px">
-        {
-          <NButton type="primary" size="small" quaternary onClick={() => edit(row)}>
-            编辑
-          </NButton>
-        }
-        {row.level == 3 && (
-          <NButton
-            type="primary"
-            size="small"
-            quaternary
-            onClick={() => relatedGoodsModalRef.value?.handleOpenModal(row)}
-          >
-            关联商品
-          </NButton>
-        )}
-      </div>
-    )
-  }
-];
+
 const [
   registerModalForm,
   {
     openModal: openModalForm,
     setFieldsValue: setModalFormValue,
     getFieldsValue: getModalFormValue,
-    closeModal: closeModalForm
+    closeModal: closeModalForm,
+    setModalProps,
+    setSubLoading
   }
 ] = useModalFrom({
   modalConfig: {
@@ -129,13 +78,26 @@ const [
     schemas: [
       { label: '', field: 'id', show: false, component: 'NInput' },
       { label: '', field: 'level', show: false, component: 'NInput' },
+      { label: '', field: 'pid', show: false, component: 'NInput' },
+      { label: '', field: 'parentCode', show: false, component: 'NInput' },
+      {
+        label: '所属二级分类',
+        field: 'BreadName',
+        component: 'NGradientText',
+        render({ model, field }) {
+          return <div> {model[field]} </div>;
+        },
+        ifShow({ model, field }) {
+          return model[field];
+        }
+      },
       {
         label: '分类名称',
         field: 'name',
         component: 'NInput',
         required: true,
-        componentProps: {
-          disabled: true
+        ifShow({ model }) {
+          return model.level == 3;
         }
       },
       {
@@ -148,7 +110,10 @@ const [
           aspectRatioH: 1,
           max: 1
         },
-        required: true
+        required: true,
+        ifShow({ model }) {
+          return model.level != 3;
+        }
       },
       {
         label: '标签',
@@ -184,6 +149,69 @@ const [
     }
   }
 });
+const tableColumns: NaiveUI.TableColumn<Api.goods.ShopCategory>[] = [
+  {
+    title: '分类名称',
+    key: 'name'
+  },
+  {
+    title: '分类图标',
+    key: 'icon',
+    render(row) {
+      return <NImage src={row.icon} class="h-[40px] w-[40px]" />;
+    }
+  },
+  {
+    title: '关联商品',
+    key: 'prodCount',
+    render: row => {
+      return row.level == 3 ? `${row.prodCount}个商品` : '';
+    }
+  },
+  {
+    title: '标签',
+    key: 'labelName',
+    render: row => {
+      return row.labelName ? <NTag> {row.labelName} </NTag> : '';
+    }
+  },
+  {
+    title: '排序',
+    key: 'num'
+  },
+  {
+    title: '操作',
+    key: 'op',
+    fixed: 'right',
+    align: 'center',
+    width: 230,
+    render: row => (
+      <div class="flex-center gap-8px">
+        {row.level == 2 && (
+          <NButton type="primary" size="small" quaternary onClick={() => add(row)}>
+            添加三级分类
+          </NButton>
+        )}
+        {row.level == 3 && (
+          <NButton type="primary" size="small" quaternary onClick={() => handleOpenRelatedGoods(row)}>
+            关联商品
+          </NButton>
+        )}
+        {
+          <NButton type="primary" size="small" quaternary onClick={() => edit(row)}>
+            编辑
+          </NButton>
+        }
+
+        {row.level == 3 && (
+          <NButton type="primary" size="small" quaternary onClick={() => del(row)}>
+            删除
+          </NButton>
+        )}
+      </div>
+    )
+  }
+];
 const failColumns: NaiveUI.TableColumn<Api.goods.ShopCategoryLogVO>[] = [
   {
     key: 'index',
@@ -263,25 +291,58 @@ async function handleSubmit(file: File) {
   }
   importTemplateRef.value?.setSubLoading(false);
 }
-function edit(row: Recordable) {
+function edit(row: Api.goods.ShopCategory) {
   openModalForm(row);
+  setModalProps({ title: `修改${row.level}级分类` });
   setModalFormValue({ ...row, label: row.label });
 }
-
+function add(row: Api.goods.ShopCategory) {
+  const { ancestors, current } = getRowHierarchyInfo(row, deskData.value);
+  const level1Name = ancestors[0].name;
+  const level2Name = current.name;
+  openModalForm();
+  setModalProps({ title: `新增三级分类` });
+  setModalFormValue({ level: 3, pid: row.id, parentCode: row.code, BreadName: `${level1Name} > ${level2Name}` });
+}
+function del(row: Api.goods.ShopCategory) {
+  window.$dialog?.info({
+    title: '删除分类',
+    content: '你确定要删除吗?',
+    positiveText: '确定',
+    negativeText: '取消',
+    onPositiveClick: async () => {
+      const { error } = await fetchDeleteCategory(row.id);
+      if (!error) {
+        getData();
+      }
+    }
+  });
+}
 async function getData() {
   const { data, error } = await fetchGetDeskCategoryList(getSearchForm());
+
   if (!error) {
     deskData.value = buildTree(data).sort((a, b) => b.num - a.num);
     console.log(deskData.value);
   }
 }
+function handleOpenRelatedGoods(row: Api.goods.ShopCategory) {
+  const { ancestors, current } = getRowHierarchyInfo(row, deskData.value);
+  const level1Name = ancestors[0].name;
+  const level2Name = ancestors[0].name;
+  const level3Name = current.name;
+  relatedGoodsModalRef.value?.handleOpenModal({ ...row, level1Name, level2Name, level3Name });
+}
 async function handleSubmitForm() {
   const form = await getModalFormValue();
-  const { error } = await fetchUpdateCategory(form);
+  const level = form.level;
+  const { error } =
+    level != 3 ? await fetchUpdateCategory(form) : await fetchAddCategory({ ...form, shopId: getSearchForm().shopId });
   if (!error) {
     closeModalForm();
     getData();
   }
+  setSubLoading(false);
 }
 
 function handleOpen() {
@@ -345,6 +406,74 @@ function buildTree<T>(items: T[], options: BuildTreeOptions = {}): T[] {
 
   return tree;
 }
+function getRowHierarchyInfo(
+  row: Api.goods.ShopCategory,
+  allData: Api.goods.ShopCategory[]
+): {
+  current: Api.goods.ShopCategory;
+  parent: Api.goods.ShopCategory | null;
+  children: Api.goods.ShopCategory[];
+  ancestors: Api.goods.ShopCategory[];
+  descendants: Api.goods.ShopCategory[];
+} {
+  // 查找父节点
+  const findParent = (data: Api.goods.ShopCategory[], targetPid: number): Api.goods.ShopCategory | null => {
+    for (const item of data) {
+      if (item.id === targetPid) {
+        return item;
+      }
+      if (item.children && item.children.length > 0) {
+        const found = findParent(item.children, targetPid);
+        if (found) return found;
+      }
+    }
+    return null;
+  };
+
+  // 查找所有祖先节点
+  const findAncestors = (data: Api.goods.ShopCategory[], targetPid: number): Api.goods.ShopCategory[] => {
+    const ancestors: Api.goods.ShopCategory[] = [];
+    let currentPid = targetPid;
+
+    while (currentPid !== 0) {
+      const parent = findParent(data, currentPid);
+      if (parent) {
+        ancestors.unshift(parent); // 添加到数组开头以保持层级顺序
+        currentPid = parent.pid;
+      } else {
+        break;
+      }
+    }
+
+    return ancestors;
+  };
+
+  // 获取所有子孙节点
+  const getAllDescendants = (node: Api.goods.ShopCategory): Api.goods.ShopCategory[] => {
+    let descendants: Api.goods.ShopCategory[] = [];
+
+    if (node.children && node.children.length > 0) {
+      node.children.forEach(child => {
+        descendants.push(child);
+        descendants = descendants.concat(getAllDescendants(child));
+      });
+    }
+
+    return descendants;
+  };
+
+  const parent = row.pid === 0 ? null : findParent(allData, row.pid);
+  const ancestors = findAncestors(allData, row.pid);
+  const descendants = getAllDescendants(row);
+
+  return {
+    current: row,
+    parent,
+    children: row.children || [],
+    ancestors,
+    descendants
+  };
+}
 </script>
 
 <template>