12 次代碼提交 6224d532c0 ... d3d381e882

作者 SHA1 備註 提交日期
  wenjie d3d381e882 Merge branch 'wj' 1 周之前
  wenjie c4da4f4436 ``` 1 周之前
  zouzexu ecb71297d1 feat(tenant): 新增租户切换功能模块 2 周之前
  zouzexu d0b1b37b9d feat(tenant): 实现租户及租户套餐管理功能 2 周之前
  zouzexu 8711ed9f23 feat(tenant): 添加租户管理相关功能和路由 2 周之前
  wenjie 5d289395bb ``` 2 周之前
  wenjie 215f56104b Merge branch 'wj' of http://git.zonelife.cn:3000/zhangtao/city-gather-admin into wj 3 周之前
  wenjie cf6fb71f66 ``` 3 周之前
  Sheep 74ba023d0c refactor(member-center): 优化赠品选择商品过滤逻辑 3 周之前
  zouzexu 398b88ad0e refactor(member-center): 优化权益配置数据结构及详情赋值逻辑 3 周之前
  zouzexu 8d8cfa470c feat(member-center): 优化权益及赠品配置结构与UI展示 3 周之前
  wenjie 02e57c19e9 ``` 3 周之前
共有 47 個文件被更改,包括 5600 次插入298 次删除
  1. 2 1
      .env.test
  2. 247 251
      pnpm-lock.yaml
  3. 1 0
      src/components/zt/ApiSelect/api-select.vue
  4. 6 0
      src/components/zt/Table/hooks/useTable.ts
  5. 3 1
      src/components/zt/Table/types/index.ts
  6. 50 6
      src/components/zt/Table/z-table.vue
  7. 17 1
      src/locales/langs/en-us.ts
  8. 17 1
      src/locales/langs/zh-cn.ts
  9. 13 0
      src/router/elegant/imports.ts
  10. 151 5
      src/router/elegant/routes.ts
  11. 16 0
      src/router/elegant/transform.ts
  12. 20 2
      src/service/api/auth.ts
  13. 26 0
      src/service/api/goods-center/scenic-goods/index.ts
  14. 82 0
      src/service/api/h5-manage/channel-manage/index.ts
  15. 26 0
      src/service/api/h5-manage/user-list/index.ts
  16. 60 0
      src/service/api/member-center/member-list/index.ts
  17. 24 0
      src/service/api/member-center/member-order/index.ts
  18. 94 0
      src/service/api/member-center/member-type/index.ts
  19. 122 0
      src/service/api/operation/coupon-manage/index.ts
  20. 147 0
      src/service/api/tenant/index.ts
  21. 4 3
      src/store/modules/auth/index.ts
  22. 1 1
      src/typings/api.d.ts
  23. 2 0
      src/typings/components.d.ts
  24. 32 0
      src/typings/elegant-router.d.ts
  25. 25 1
      src/views/_builtin/login/modules/pwd-login.vue
  26. 2 2
      src/views/djk-manage/edit-activity/index.vue
  27. 3 1
      src/views/film-manage/film-list/index.vue
  28. 0 1
      src/views/goods-center/edit-health-goods/index.vue
  29. 240 0
      src/views/goods-center/scenic-goods/index.vue
  30. 13 0
      src/views/goods-center/virtual-goods/index.vue
  31. 194 0
      src/views/h5-manage/channel-manage/index.vue
  32. 339 0
      src/views/h5-manage/edit-channel/index.vue
  33. 123 0
      src/views/h5-manage/user-list/index.vue
  34. 1 1
      src/views/jy-manage/activity/index.vue
  35. 1107 0
      src/views/member-center/edit-member-type/index.vue
  36. 357 0
      src/views/member-center/member-list/index.vue
  37. 250 0
      src/views/member-center/member-order/index.vue
  38. 235 0
      src/views/member-center/member-type/index.vue
  39. 371 0
      src/views/operation/coupon-issuance/index.vue
  40. 423 0
      src/views/operation/coupon-manage/index.vue
  41. 10 4
      src/views/order-manage/after-sales-order-detail/index.vue
  42. 1 1
      src/views/order-manage/after-sales-order/index.vue
  43. 21 5
      src/views/order-manage/normal-order/index.vue
  44. 73 10
      src/views/order-manage/order-detail/index.vue
  45. 181 0
      src/views/tenant/tenant-change/index.vue
  46. 184 0
      src/views/tenant/tenant-package/index.vue
  47. 284 0
      src/views/tenant/tenant/index.vue

+ 2 - 1
.env.test

@@ -2,11 +2,12 @@
 
 # VITE_SERVICE_BASE_URL=https://522d2ea1.r39.cpolar.top #王
 # VITE_SERVICE_BASE_URL=http://89561bkaq794.vicp.fun:53846 #张
-# VITE_SERVICE_BASE_URL=http://192.168.1.89:8080#田
+# VITE_SERVICE_BASE_URL=https://5cb31c38.r3.cpolar.top#田
 # VITE_SERVICE_BASE_URL=https://425f86e6.r24.cpolar.top #邓
 # VITE_SERVICE_BASE_URL=http://74949mkfh190.vicp.fun #付
 # VITE_SERVICE_BASE_URL=https://smqjh.api.zswlgz.com
 # VITE_SERVICE_BASE_URL=https://735a1bda.r24.cpolar.top #黄
+# VITE_SERVICE_BASE_URL=http://192.168.0.11:8080 #wzq
 # VITE_SERVICE_BASE_URL=http://89561bkaq794.vicp.fun:53846
 
 VITE_SERVICE_BASE_URL=http://47.109.84.152:8081#打包测试本地服务器

文件差異過大導致無法顯示
+ 247 - 251
pnpm-lock.yaml


+ 1 - 0
src/components/zt/ApiSelect/api-select.vue

@@ -57,6 +57,7 @@ async function fetchApi() {
 
     nextTick(() => {
       modelVlaue.value = options.value[0][unref(bindValue).valueFeild as string];
+      console.log(modelVlaue.value, '默认值');
     });
   }
   if (props.getOptions) {

+ 6 - 0
src/components/zt/Table/hooks/useTable.ts

@@ -51,6 +51,10 @@ export function useTable(tableProps: ztTableProps): UseTableReturnType {
     },
     setFieldsValue(values: Recordable) {
       return tableRef.value?.setFieldsValue(values);
+    },
+    // 添加设置已选中行的方法
+    setTableCheckedRowKeys(keys: string[]) {
+      tableRef.value?.setTableCheckedRowKeys(keys);
     }
   };
   return [register, methods];
@@ -66,6 +70,8 @@ export interface TableMethods {
   getSeachForm: () => Recordable;
   setFieldsValue: (values: Recordable) => void;
   getTableLoding: ComputedRef<boolean>;
+  // 添加设置已选中行的方法接口
+  setTableCheckedRowKeys: (keys: string[]) => void;
 }
 
 export type RegisterFn = (TableInstance: TableMethods) => void;

+ 3 - 1
src/components/zt/Table/types/index.ts

@@ -54,7 +54,9 @@ export interface tableProp extends DataTableProps {
    * 默认参数,重置的时候不重置这个参数
    *
    */
-  defaultParamsNotReset?: string;
+  // defaultParamsNotReset?: string;
+  // 默认参数,重置的时候不重置这个参数 新增可以传多个
+  defaultParamsNotReset?: string | string[];
 }
 
 export interface ztTableProps {

+ 50 - 6
src/components/zt/Table/z-table.vue

@@ -65,6 +65,10 @@ export default defineComponent({
       columns: () => handleGetColumns()
     });
     const { checkedRowKeys } = useTableOperate(data, getTableProps.value.keyField, getData);
+    // 添加设置已选中行的方法
+    function setTableCheckedRowKeys(keys: string[]) {
+      checkedRowKeys.value = keys;
+    }
     function handleGetColumns() {
       const columnsData = [...propsData.columns];
       if (propsData.showTableAction) {
@@ -125,7 +129,9 @@ export default defineComponent({
       getTableData,
       getSeachForm: () => handleCommonTime(getSeachForm()),
       setFieldsValue,
-      getTableLoding
+      getTableLoding,
+      // 添加设置已选中行的方法
+      setTableCheckedRowKeys
     };
     function setTableLoading(flage: boolean) {
       loading.value = flage;
@@ -139,12 +145,28 @@ export default defineComponent({
     function handleDelete() {
       emit('delete', checkedRowKeys.value);
     }
+    // async function handleReset() {
+    //   getForm.value = getSeachForm();
+    //   if (getTableProps.value.defaultParamsNotReset) {
+    //     const keySearch = getTableProps.value.defaultParamsNotReset;
+    //     getForm.value[keySearch] = commonKeyData.value;
+    //     await setFieldsValue({ [`${keySearch}`]: commonKeyData.value });
+    //   }
+
+    //   getData();
+    // }
+    // 重置 修改为可传多个
     async function handleReset() {
       getForm.value = getSeachForm();
       if (getTableProps.value.defaultParamsNotReset) {
-        const keySearch = getTableProps.value.defaultParamsNotReset;
-        getForm.value[keySearch] = commonKeyData.value;
-        await setFieldsValue({ [`${keySearch}`]: commonKeyData.value });
+        const keysToPreserve = Array.isArray(getTableProps.value.defaultParamsNotReset)
+          ? getTableProps.value.defaultParamsNotReset
+          : [getTableProps.value.defaultParamsNotReset];
+
+        keysToPreserve.forEach(key => {
+          getForm.value[key] = commonKeyData.value?.[key];
+          setFieldsValue({ [key]: commonKeyData.value?.[key] });
+        });
       }
 
       getData();
@@ -153,13 +175,35 @@ export default defineComponent({
       handleSearch();
     }
 
+    // function handleSearch() {
+    //   const form = handleCommonTime(getSeachForm());
+    //   console.log(propsData.defaultParams, 'propsData.defaultParams');
+
+    //   // 查询默认值,重置的时候不重置这个参数
+    //   if (getTableProps.value.defaultParamsNotReset) {
+    //     commonKeyData.value = form[getTableProps.value.defaultParamsNotReset];
+    //   }
+    //   console.log(form, '查询请求参数');
+
+    //   getForm.value = form;
+    //   getDataByPage(1);
+    //   emit('search');
+    // }
+    //  新增可传多个
     function handleSearch() {
       const form = handleCommonTime(getSeachForm());
       console.log(propsData.defaultParams, 'propsData.defaultParams');
 
-      // 查询默认值,重置的时候不重置这个参数
+      // 查询默认值,重置的时候不重置这参数
       if (getTableProps.value.defaultParamsNotReset) {
-        commonKeyData.value = form[getTableProps.value.defaultParamsNotReset];
+        const keysToPreserve = Array.isArray(getTableProps.value.defaultParamsNotReset)
+          ? getTableProps.value.defaultParamsNotReset
+          : [getTableProps.value.defaultParamsNotReset];
+
+        commonKeyData.value = {};
+        keysToPreserve.forEach(key => {
+          commonKeyData.value[key] = form[key];
+        });
       }
       console.log(form, '查询请求参数');
 

+ 17 - 1
src/locales/langs/en-us.ts

@@ -290,7 +290,23 @@ const local: App.I18n.Schema = {
     'jy-manage_activity': '',
     'order-manage_ponits-details': '',
     operation_coupon: '',
-    'order-manage_recharge-records': ''
+    'order-manage_recharge-records': '',
+    'goods-center_scenic-goods': '',
+    'operation_coupon-issuance': '',
+    'operation_coupon-manage': '',
+    'member-center': '',
+    'member-center_edit-member-type': '',
+    'member-center_member-list': '',
+    'member-center_member-order': '',
+    'member-center_member-type': '',
+    tenant: 'Tenant Management',
+    tenant_tenant: 'Tenant Management',
+    'tenant_tenant-package': 'Tenant Package',
+    'tenant_tenant-change': '',
+    'h5-manage': '',
+    'h5-manage_channel-manage': '',
+    'h5-manage_edit-channel': '',
+    'h5-manage_user-list': ''
   },
   page: {
     login: {

+ 17 - 1
src/locales/langs/zh-cn.ts

@@ -287,7 +287,23 @@ const local: App.I18n.Schema = {
     'jy-manage_activity': '',
     'order-manage_ponits-details': '',
     operation_coupon: '',
-    'order-manage_recharge-records': ''
+    'order-manage_recharge-records': '',
+    'goods-center_scenic-goods': '',
+    'operation_coupon-issuance': '',
+    'operation_coupon-manage': '',
+    'member-center': '',
+    'member-center_edit-member-type': '',
+    'member-center_member-list': '',
+    'member-center_member-order': '',
+    'member-center_member-type': '',
+    tenant: '租户管理',
+    tenant_tenant: '租户管理',
+    'tenant_tenant-package': '租户套餐',
+    'tenant_tenant-change': '租户切换',
+    'h5-manage': '',
+    'h5-manage_channel-manage': '',
+    'h5-manage_edit-channel': '',
+    'h5-manage_user-list': ''
   },
   page: {
     login: {

+ 13 - 0
src/router/elegant/imports.ts

@@ -30,6 +30,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
   "film-manage_setprice": () => import("@/views/film-manage/setprice/index.vue"),
   "goods-center_edit-health-goods": () => import("@/views/goods-center/edit-health-goods/index.vue"),
   "goods-center_health-goods": () => import("@/views/goods-center/health-goods/index.vue"),
+  "goods-center_scenic-goods": () => import("@/views/goods-center/scenic-goods/index.vue"),
   "goods-center_store-goods": () => import("@/views/goods-center/store-goods/index.vue"),
   "goods-center_type-admin": () => import("@/views/goods-center/type-admin/index.vue"),
   "goods-center_virtual-goods": () => import("@/views/goods-center/virtual-goods/index.vue"),
@@ -37,6 +38,9 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
   "government_government-list": () => import("@/views/government/government-list/index.vue"),
   government_points: () => import("@/views/government/points/index.vue"),
   "government_user-list": () => import("@/views/government/user-list/index.vue"),
+  "h5-manage_channel-manage": () => import("@/views/h5-manage/channel-manage/index.vue"),
+  "h5-manage_edit-channel": () => import("@/views/h5-manage/edit-channel/index.vue"),
+  "h5-manage_user-list": () => import("@/views/h5-manage/user-list/index.vue"),
   home: () => import("@/views/home/index.vue"),
   "jy-manage_activity": () => import("@/views/jy-manage/activity/index.vue"),
   manage_config: () => import("@/views/manage/config/index.vue"),
@@ -47,7 +51,13 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
   manage_role: () => import("@/views/manage/role/index.vue"),
   manage_schedule: () => import("@/views/manage/schedule/index.vue"),
   manage_user: () => import("@/views/manage/user/index.vue"),
+  "member-center_edit-member-type": () => import("@/views/member-center/edit-member-type/index.vue"),
+  "member-center_member-list": () => import("@/views/member-center/member-list/index.vue"),
+  "member-center_member-order": () => import("@/views/member-center/member-order/index.vue"),
+  "member-center_member-type": () => import("@/views/member-center/member-type/index.vue"),
   "operation_accounting-strategy": () => import("@/views/operation/accounting-strategy/index.vue"),
+  "operation_coupon-issuance": () => import("@/views/operation/coupon-issuance/index.vue"),
+  "operation_coupon-manage": () => import("@/views/operation/coupon-manage/index.vue"),
   operation_coupon: () => import("@/views/operation/coupon/index.vue"),
   "order-manage_after-sales-order-detail": () => import("@/views/order-manage/after-sales-order-detail/index.vue"),
   "order-manage_after-sales-order": () => import("@/views/order-manage/after-sales-order/index.vue"),
@@ -55,6 +65,9 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
   "order-manage_order-detail": () => import("@/views/order-manage/order-detail/index.vue"),
   "order-manage_ponits-details": () => import("@/views/order-manage/ponits-details/index.vue"),
   "order-manage_recharge-records": () => import("@/views/order-manage/recharge-records/index.vue"),
+  "tenant_tenant-change": () => import("@/views/tenant/tenant-change/index.vue"),
+  "tenant_tenant-package": () => import("@/views/tenant/tenant-package/index.vue"),
+  tenant_tenant: () => import("@/views/tenant/tenant/index.vue"),
   "user-center": () => import("@/views/user-center/index.vue"),
   "user-management_user-list": () => import("@/views/user-management/user-list/index.vue"),
   "xsb-manage_advertisement": () => import("@/views/xsb-manage/advertisement/index.vue"),

+ 151 - 5
src/router/elegant/routes.ts

@@ -1,8 +1,3 @@
-/* eslint-disable */
-/* prettier-ignore */
-// Generated by elegant-router
-// Read more: https://github.com/soybeanjs/elegant-router
-
 import type { GeneratedRoute } from '@elegant-router/types';
 
 export const generatedRoutes: GeneratedRoute[] = [
@@ -171,6 +166,15 @@ export const generatedRoutes: GeneratedRoute[] = [
           i18nKey: 'route.goods-center_health-goods'
         }
       },
+      {
+        name: 'goods-center_scenic-goods',
+        path: '/goods-center/scenic-goods',
+        component: 'view.goods-center_scenic-goods',
+        meta: {
+          title: 'goods-center_scenic-goods',
+          i18nKey: 'route.goods-center_scenic-goods'
+        }
+      },
       {
         name: 'goods-center_store-goods',
         path: '/goods-center/store-goods',
@@ -247,6 +251,44 @@ export const generatedRoutes: GeneratedRoute[] = [
       }
     ]
   },
+  {
+    name: 'h5-manage',
+    path: '/h5-manage',
+    component: 'layout.base',
+    meta: {
+      title: 'h5-manage',
+      i18nKey: 'route.h5-manage'
+    },
+    children: [
+      {
+        name: 'h5-manage_channel-manage',
+        path: '/h5-manage/channel-manage',
+        component: 'view.h5-manage_channel-manage',
+        meta: {
+          title: 'h5-manage_channel-manage',
+          i18nKey: 'route.h5-manage_channel-manage'
+        }
+      },
+      {
+        name: 'h5-manage_edit-channel',
+        path: '/h5-manage/edit-channel',
+        component: 'view.h5-manage_edit-channel',
+        meta: {
+          title: 'h5-manage_edit-channel',
+          i18nKey: 'route.h5-manage_edit-channel'
+        }
+      },
+      {
+        name: 'h5-manage_user-list',
+        path: '/h5-manage/user-list',
+        component: 'view.h5-manage_user-list',
+        meta: {
+          title: 'h5-manage_user-list',
+          i18nKey: 'route.h5-manage_user-list'
+        }
+      }
+    ]
+  },
   {
     name: 'home',
     path: '/home',
@@ -397,6 +439,53 @@ export const generatedRoutes: GeneratedRoute[] = [
       }
     ]
   },
+  {
+    name: 'member-center',
+    path: '/member-center',
+    component: 'layout.base',
+    meta: {
+      title: 'member-center',
+      i18nKey: 'route.member-center'
+    },
+    children: [
+      {
+        name: 'member-center_edit-member-type',
+        path: '/member-center/edit-member-type',
+        component: 'view.member-center_edit-member-type',
+        meta: {
+          title: 'member-center_edit-member-type',
+          i18nKey: 'route.member-center_edit-member-type'
+        }
+      },
+      {
+        name: 'member-center_member-list',
+        path: '/member-center/member-list',
+        component: 'view.member-center_member-list',
+        meta: {
+          title: 'member-center_member-list',
+          i18nKey: 'route.member-center_member-list'
+        }
+      },
+      {
+        name: 'member-center_member-order',
+        path: '/member-center/member-order',
+        component: 'view.member-center_member-order',
+        meta: {
+          title: 'member-center_member-order',
+          i18nKey: 'route.member-center_member-order'
+        }
+      },
+      {
+        name: 'member-center_member-type',
+        path: '/member-center/member-type',
+        component: 'view.member-center_member-type',
+        meta: {
+          title: 'member-center_member-type',
+          i18nKey: 'route.member-center_member-type'
+        }
+      }
+    ]
+  },
   {
     name: 'operation',
     path: '/operation',
@@ -423,6 +512,24 @@ export const generatedRoutes: GeneratedRoute[] = [
           title: 'operation_coupon',
           i18nKey: 'route.operation_coupon'
         }
+      },
+      {
+        name: 'operation_coupon-issuance',
+        path: '/operation/coupon-issuance',
+        component: 'view.operation_coupon-issuance',
+        meta: {
+          title: 'operation_coupon-issuance',
+          i18nKey: 'route.operation_coupon-issuance'
+        }
+      },
+      {
+        name: 'operation_coupon-manage',
+        path: '/operation/coupon-manage',
+        component: 'view.operation_coupon-manage',
+        meta: {
+          title: 'operation_coupon-manage',
+          i18nKey: 'route.operation_coupon-manage'
+        }
       }
     ]
   },
@@ -491,6 +598,45 @@ export const generatedRoutes: GeneratedRoute[] = [
       }
     ]
   },
+  {
+    name: 'tenant',
+    path: '/tenant',
+    component: 'layout.base',
+    meta: {
+      title: 'tenant',
+      i18nKey: 'route.tenant',
+      order: 90
+    },
+    children: [
+      {
+        name: 'tenant_tenant',
+        path: '/tenant/tenant',
+        component: 'view.tenant_tenant',
+        meta: {
+          title: 'tenant_tenant',
+          i18nKey: 'route.tenant_tenant'
+        }
+      },
+      {
+        name: 'tenant_tenant-change',
+        path: '/tenant/tenant-change',
+        component: 'view.tenant_tenant-change',
+        meta: {
+          title: 'tenant_tenant-change',
+          i18nKey: 'route.tenant_tenant-change'
+        }
+      },
+      {
+        name: 'tenant_tenant-package',
+        path: '/tenant/tenant-package',
+        component: 'view.tenant_tenant-package',
+        meta: {
+          title: 'tenant_tenant-package',
+          i18nKey: 'route.tenant_tenant-package'
+        }
+      }
+    ]
+  },
   {
     name: 'user-center',
     path: '/user-center',

+ 16 - 0
src/router/elegant/transform.ts

@@ -195,6 +195,7 @@ const routeMap: RouteMap = {
   "goods-center": "/goods-center",
   "goods-center_edit-health-goods": "/goods-center/edit-health-goods",
   "goods-center_health-goods": "/goods-center/health-goods",
+  "goods-center_scenic-goods": "/goods-center/scenic-goods",
   "goods-center_store-goods": "/goods-center/store-goods",
   "goods-center_type-admin": "/goods-center/type-admin",
   "goods-center_virtual-goods": "/goods-center/virtual-goods",
@@ -203,6 +204,10 @@ const routeMap: RouteMap = {
   "government_government-list": "/government/government-list",
   "government_points": "/government/points",
   "government_user-list": "/government/user-list",
+  "h5-manage": "/h5-manage",
+  "h5-manage_channel-manage": "/h5-manage/channel-manage",
+  "h5-manage_edit-channel": "/h5-manage/edit-channel",
+  "h5-manage_user-list": "/h5-manage/user-list",
   "home": "/home",
   "iframe-page": "/iframe-page/:url",
   "jy-manage": "/jy-manage",
@@ -217,9 +222,16 @@ const routeMap: RouteMap = {
   "manage_role": "/manage/role",
   "manage_schedule": "/manage/schedule",
   "manage_user": "/manage/user",
+  "member-center": "/member-center",
+  "member-center_edit-member-type": "/member-center/edit-member-type",
+  "member-center_member-list": "/member-center/member-list",
+  "member-center_member-order": "/member-center/member-order",
+  "member-center_member-type": "/member-center/member-type",
   "operation": "/operation",
   "operation_accounting-strategy": "/operation/accounting-strategy",
   "operation_coupon": "/operation/coupon",
+  "operation_coupon-issuance": "/operation/coupon-issuance",
+  "operation_coupon-manage": "/operation/coupon-manage",
   "order-manage": "/order-manage",
   "order-manage_after-sales-order": "/order-manage/after-sales-order",
   "order-manage_after-sales-order-detail": "/order-manage/after-sales-order-detail",
@@ -227,6 +239,10 @@ const routeMap: RouteMap = {
   "order-manage_order-detail": "/order-manage/order-detail",
   "order-manage_ponits-details": "/order-manage/ponits-details",
   "order-manage_recharge-records": "/order-manage/recharge-records",
+  "tenant": "/tenant",
+  "tenant_tenant": "/tenant/tenant",
+  "tenant_tenant-change": "/tenant/tenant-change",
+  "tenant_tenant-package": "/tenant/tenant-package",
   "user-center": "/user-center",
   "user-management": "/user-management",
   "user-management_user-list": "/user-management/user-list",

+ 20 - 2
src/service/api/auth.ts

@@ -1,5 +1,13 @@
 import { request } from '../request';
 
+/** 获取租户选项列表 */
+export function fetchGetTenantOptions() {
+  return request<{ id: number; tenantCode: string; tenantName: string }[]>({
+    url: '/smqjh-system/api/v1/tenants/options',
+    method: 'get'
+  });
+}
+
 // /**
 //  * Login
 //  *
@@ -26,19 +34,29 @@ import { request } from '../request';
  * @param data
  * @returns
  */
-export function fetchLogin(data: { username: string; password: string; captchaCode: string; captchaKey: string }) {
+export function fetchLogin(data: {
+  username: string;
+  password: string;
+  captchaCode: string;
+  captchaKey: string;
+  tenantCode?: string;
+}) {
   const formData = new FormData();
   formData.append('username', data.username);
   formData.append('password', data.password);
   formData.append('captchaId', data.captchaKey as string);
   formData.append('captchaCode', data.captchaCode as string);
   formData.append('grant_type', 'password');
+  if (data.tenantCode) {
+    formData.append('tenantCode', data.tenantCode);
+  }
   return request<Api.Auth.LoginToken>({
     url: '/smqjh-auth/oauth2/token',
     method: 'post',
     data: formData,
     headers: {
-      'Content-Type': 'multipart/form-data'
+      'Content-Type': 'multipart/form-data',
+      'X-Tenant-Domain': '192.168.0.11'
     }
   });
 }

+ 26 - 0
src/service/api/goods-center/scenic-goods/index.ts

@@ -0,0 +1,26 @@
+import { request } from '@/service/request';
+/**
+ * 分页获取
+ * @param params
+ * @returns
+ */
+export function fetchList(data: any) {
+  return request({
+    url: '/smqjh-pms/api/scenery/prod/findAppByPage',
+    method: 'get',
+    params: data
+  });
+}
+
+/**
+ * 获取价格
+ * @param params
+ * @returns
+ */
+export function fetchPriceList(data: any) {
+  return request({
+    url: '/smqjh-pms/api/scenery/prod/price',
+    method: 'post',
+    data
+  });
+}

+ 82 - 0
src/service/api/h5-manage/channel-manage/index.ts

@@ -0,0 +1,82 @@
+import { request } from '@/service/request';
+/**
+ * 分页获取
+ * @param params
+ * @returns
+ */
+export function fetchChannelList(data: any) {
+  return request({
+    url: '/smqjh-system/api/v1/access/page',
+    method: 'get',
+    params: data
+  });
+}
+
+/**
+ *
+ * 删除
+ * @returns
+ */
+export function fetchDelChannel(ids: string) {
+  return request({
+    url: `/smqjh-system/api/v1/access/${ids}`,
+    method: 'delete'
+  });
+}
+
+/**
+ * 保存
+ * @returns
+ */
+export function fetchAddChannel(data: any) {
+  return request({
+    url: '/smqjh-system/api/v1/access',
+    method: 'post',
+    data
+  });
+}
+
+/**
+ * 编辑
+ * @returns
+ */
+export function fetchEditChannel(data: any) {
+  return request({
+    url: `/smqjh-system/api/v1/access/${data.id}`,
+    method: 'put',
+    data
+  });
+}
+
+/**
+ * 详情
+ * @returns
+ */
+export function fetchChannelDetail(id: any) {
+  return request({
+    url: `/smqjh-system/api/v1/access/${id}`,
+    method: 'get'
+  });
+}
+
+/**
+ * 查看秘钥
+ * @returns
+ */
+export function fetchAppSecret(id: any) {
+  return request({
+    url: `/smqjh-system/api/v1/access/${id}/app-secret`,
+    method: 'get'
+  });
+}
+
+/**
+ * 重置秘钥
+ * @returns
+ */
+export function fetchResetAppSecret(id: any) {
+  return request({
+    url: `/smqjh-system/api/v1/access/${id}/app-secret/reset`,
+    method: 'patch'
+  });
+}

+ 26 - 0
src/service/api/h5-manage/user-list/index.ts

@@ -0,0 +1,26 @@
+import { request } from '@/service/request';
+/**
+ * 分页获取
+ * @param params
+ * @returns
+ */
+export function fetchUserList(data: any) {
+  return request({
+    url: '/smqjh-system/app-api/v1/members/page/accessPage',
+    method: 'get',
+    params: data
+  });
+}
+
+/**
+ * 获取当前所属渠道
+ * @param params
+ * @returns
+ */
+export function fetchUserAccess(data: any) {
+  return request({
+    url: '/smqjh-system/app-api/v1/members/access',
+    method: 'get',
+    params: data
+  });
+}

+ 60 - 0
src/service/api/member-center/member-list/index.ts

@@ -0,0 +1,60 @@
+import { request } from '@/service/request';
+/**
+ * 分页获取会员列表
+ * @param params
+ * @returns
+ */
+export function fetchMemberList(data: any) {
+  return request({
+    url: '/smqjh-system/api/v1/member/account/page',
+    method: 'get',
+    params: data
+  });
+}
+/**
+ * 开通记录
+ * @param params
+ * @returns
+ */
+export function fetchOpenRecord(data: any) {
+  return request({
+    url: '/smqjh-system/api/v1/member/open-record/page',
+    method: 'get',
+    params: data
+  });
+}
+
+/**
+ * 导出
+ * @returns
+ */
+export function fetchExport(data: any) {
+  return request({
+    url: `/smqjh-system/api/v1/member/export/task/start`,
+    method: 'post',
+    data
+  });
+}
+
+/**
+ * 导出记录
+ * @returns
+ */
+export function fetchExportRecord(data: any) {
+  return request({
+    url: `/smqjh-system/api/v1/member/export/task/page`,
+    method: 'get',
+    params: data
+  });
+}
+
+/**
+ * 下载
+ * @returns
+ */
+export function fetchDownload(taskId: any) {
+  return request({
+    url: `/smqjh-system/api/v1/member/export/task/download/${taskId}`,
+    method: 'get'
+  });
+}

+ 24 - 0
src/service/api/member-center/member-order/index.ts

@@ -0,0 +1,24 @@
+import { request } from '@/service/request';
+/**
+ * 分页获取
+ * @param params
+ * @returns
+ */
+export function fetchOrderList(data: any) {
+  return request({
+    url: '/smqjh-system/api/v1/member/order/page',
+    method: 'get',
+    params: data
+  });
+}
+
+/**
+ * 详情
+ * @returns
+ */
+export function fetchOrderDetail(id: any) {
+  return request({
+    url: `/smqjh-system/api/v1/member/order/detail/${id}`,
+    method: 'get'
+  });
+}

+ 94 - 0
src/service/api/member-center/member-type/index.ts

@@ -0,0 +1,94 @@
+import { request } from '@/service/request';
+/**
+ * 分页获取
+ * @param params
+ * @returns
+ */
+export function fetchTypeList(data: any) {
+  return request({
+    url: '/smqjh-pms/api/v1/member/type/page',
+    method: 'get',
+    params: data
+  });
+}
+
+/**
+ *
+ * 删除
+ * @returns
+ */
+export function fetchDelType(id: string) {
+  return request({
+    url: `/smqjh-pms/api/v1/member/type/delete/${id}`,
+    method: 'delete'
+  });
+}
+
+/**
+ * 保存
+ * @returns
+ */
+export function fetchAddType(data: any) {
+  return request({
+    url: '/smqjh-pms/api/v1/member/type/save',
+    method: 'post',
+    data
+  });
+}
+
+/**
+ * 编辑
+ * @returns
+ */
+export function fetchEditType(data: any) {
+  return request({
+    url: '/smqjh-pms/api/v1/member/type/update',
+    method: 'put',
+    data
+  });
+}
+
+/**
+ * 详情
+ * @returns
+ */
+export function fetchTypeDetail(id: any) {
+  return request({
+    url: `/smqjh-pms/api/v1/member/type/detail/${id}`,
+    method: 'get'
+  });
+}
+
+/**
+ * 商品分页
+ * @returns
+ */
+export function fetchGoodsList(data: any) {
+  return request({
+    url: '/smqjh-pms/api/v1/product/page',
+    method: 'post',
+    data
+  });
+}
+
+/**
+ * 企业渠道
+ * @returns
+ */
+export function fetchFirstChannel() {
+  return request({
+    url: `/smqjh-system/api/v1/channel/listFirstChannel`,
+    method: 'get'
+  });
+}
+
+/**
+ * 切换状态
+ * @returns
+ */
+export function fetchUpdateStatus(id: any, status: any) {
+  return request({
+    url: `/smqjh-pms/api/v1/member/type/changeStatus/${id}/${status}`,
+    method: 'put'
+  });
+}

+ 122 - 0
src/service/api/operation/coupon-manage/index.ts

@@ -0,0 +1,122 @@
+import { request } from '@/service/request';
+
+/**
+ * 所有列表
+ * @returns
+ */
+export function fetchGetActivityList(data: any) {
+  return request({
+    url: `/smqjh-system/sys-api/couponLocal/page`,
+    method: 'get',
+    params: data
+  });
+}
+
+/**
+ *
+    新增
+ * @param data
+ * @returns
+ */
+
+export function fetchAddActivity(data: any) {
+  return request({
+    url: `/smqjh-system/sys-api/couponLocal/save`,
+    method: 'post',
+    data
+  });
+}
+
+/**
+ *
+    编辑
+ * @param data
+ * @returns
+ */
+
+export function fetchEditActivity(data: any) {
+  return request({
+    url: `/smqjh-system/sys-api/couponLocal/update`,
+    method: 'PUT',
+    data
+  });
+}
+
+/**
+ *
+    删除
+ * @param data
+ * @returns
+ */
+
+export function delActivity(id: any) {
+  return request({
+    url: `/smqjh-system/sys-api/couponLocal/delete/${id}`,
+    method: 'DELETE'
+  });
+}
+
+/**
+ * 获取领取记录
+ * @returns
+ */
+export function fetchList(data: any) {
+  return request({
+    url: `/smqjh-system/sys-api/memberCoupon/page`,
+    method: 'get',
+    params: data
+  });
+}
+
+/**
+ * 导入失败记录
+ * @returns
+ */
+export function fetchFailList(data: any) {
+  return request({
+    url: `/smqjh-system/sys-api/memberCoupon/findByTypePage`,
+    method: 'get',
+    params: data
+  });
+}
+
+/**
+ *批量导入
+ * @returns
+ */
+export function fetchImport(data: any) {
+  return request({
+    url: `/smqjh-system/sys-api/memberCoupon/uploadExcelGoods`,
+    method: 'post',
+    data,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  });
+}
+
+/**
+ * 下载失败记录
+ * @returns
+ */
+export function fetchExportFailure(data: any) {
+  return request({
+    url: `/smqjh-system/sys-api/memberCoupon/downloadErrorExcel`,
+    method: 'get',
+    params: data
+  });
+}
+
+/**
+ *
+    删除优惠券
+ * @param data
+ * @returns
+ */
+
+export function delCoupon(id: any) {
+  return request({
+    url: `/smqjh-system/sys-api/memberCoupon/delete/${id}`,
+    method: 'DELETE'
+  });
+}

+ 147 - 0
src/service/api/tenant/index.ts

@@ -0,0 +1,147 @@
+import { request } from '../../request';
+
+// ============ 租户管理 ============
+
+/** 获取租户分页列表 */
+export function fetchGetTenantList(params?: { keywords?: string; pageNum: number; pageSize: number }) {
+  return request<any>({
+    url: '/smqjh-system/api/v1/tenants/page',
+    method: 'get',
+    params
+  });
+}
+
+/** 新增租户 */
+export function fetchAddTenant(data: any) {
+  return request({
+    url: '/smqjh-system/api/v1/tenants',
+    method: 'post',
+    data
+  });
+}
+
+/** 修改租户 */
+export function fetchEditTenant(data: any) {
+  return request({
+    url: `/smqjh-system/api/v1/tenants/${data.id}`,
+    method: 'put',
+    data
+  });
+}
+
+/** 删除租户 */
+export function fetchDeleteTenant(ids: string) {
+  return request({
+    url: `/smqjh-system/api/v1/tenants/${ids}`,
+    method: 'delete'
+  });
+}
+
+// ============ 租户套餐 ============
+
+/** 获取租户套餐分页列表 */
+export function fetchGetTenantPackageList(params?: { pageNum: number; pageSize: number }) {
+  return request<any>({
+    url: '/smqjh-system/api/v1/tenant-packages/page',
+    method: 'get',
+    params
+  });
+}
+
+/** 新增租户套餐 */
+export function fetchAddTenantPackage(data: any) {
+  return request({
+    url: '/smqjh-system/api/v1/tenant-packages',
+    method: 'post',
+    data
+  });
+}
+
+/** 修改租户套餐 */
+export function fetchEditTenantPackage(data: any) {
+  return request({
+    url: `/smqjh-system/api/v1/tenant-packages/${data.id}`,
+    method: 'put',
+    data
+  });
+}
+
+/** 获取套餐关联的菜单列表 */
+export function fetchGetTenantPackageMenus(packageId: number) {
+  return request<number[]>({
+    url: `/smqjh-system/api/v1/tenant-packages/${packageId}/menus`,
+    method: 'get'
+  });
+}
+
+/** 为套餐分配菜单权限 */
+export function fetchUpdateTenantPackageMenus(packageId: number, data: number[]) {
+  return request({
+    url: `/smqjh-system/api/v1/tenant-packages/${packageId}/menus`,
+    method: 'put',
+    data
+  });
+}
+
+/** 删除租户套餐 */
+export function fetchDeleteTenantPackage(ids: string) {
+  return request({
+    url: `/smqjh-system/api/v1/tenant-packages/${ids}`,
+    method: 'delete'
+  });
+}
+
+// ============ 租户切换 ============
+
+/** 获取可切换的租户列表(适配ZTable分页格式) */
+export async function fetchGetActiveTenants(_params?: any) {
+  const res = await request<any[]>({
+    url: '/smqjh-system/api/v1/tenants/active',
+    method: 'get'
+  });
+  // ZTable的defaultTransform期望 data 格式为 { list, total, current, size }
+  if (!res.error && Array.isArray(res.data)) {
+    return {
+      ...res,
+      data: {
+        list: res.data,
+        total: res.data.length,
+        current: 1,
+        size: res.data.length
+      }
+    };
+  }
+  return {
+    ...res,
+    data: {
+      list: [],
+      total: 0,
+      current: 1,
+      size: 10
+    }
+  };
+}
+
+/** 切换租户 */
+export function fetchSwitchTenant(tenantCode: string) {
+  return request({
+    url: `/smqjh-system/api/v1/tenants/switch/${tenantCode}`,
+    method: 'post'
+  });
+}
+
+/** 获取当前切换的租户 */
+export function fetchGetCurrentSwitchTenant() {
+  return request<any>({
+    url: '/smqjh-system/api/v1/tenants/switch/current',
+    method: 'get'
+  });
+}
+
+/** 清除租户切换 */
+export function fetchClearSwitchTenant() {
+  return request({
+    url: '/smqjh-system/api/v1/tenants/switch/clear',
+    method: 'post'
+  });
+}

+ 4 - 3
src/store/modules/auth/index.ts

@@ -102,10 +102,10 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
    * @param [redirect=true] Whether to redirect after login. Default is `true`
    */
   async function login(
-    model: { userName: string; password: string; captchaCode: string; captchaKey: string },
+    model: { userName: string; password: string; captchaCode: string; captchaKey: string; tenantCode?: string },
     redirect = true
   ) {
-    const { userName, password, captchaCode, captchaKey } = model;
+    const { userName, password, captchaCode, captchaKey, tenantCode } = model;
     console.log(model, 'model');
 
     startLoading();
@@ -113,7 +113,8 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
       username: userName,
       password,
       captchaCode,
-      captchaKey
+      captchaKey,
+      tenantCode
     });
     console.log('login', data);
 

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

@@ -733,7 +733,7 @@ declare namespace Api {
        */
       dvyTime?: string;
       /**
-       * 配送类型(1:快递 2:自提 3:及时配送)
+       * 配送类型(1:快递 2:自提 3:及时配送 10:商家自送
        */
       dvyType: number;
       /**

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

@@ -31,6 +31,7 @@ declare module 'vue' {
     'IconFileIcons:microsoftExcel': typeof import('~icons/file-icons/microsoft-excel')['default']
     IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
     IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
+    'IconIconParkSolid:reduceOne': typeof import('~icons/icon-park-solid/reduce-one')['default']
     'IconIconParkTwotone:mailDownload': typeof import('~icons/icon-park-twotone/mail-download')['default']
     IconIcRoundDelete: typeof import('~icons/ic/round-delete')['default']
     IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default']
@@ -51,6 +52,7 @@ declare module 'vue' {
     NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
     NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
     NButton: typeof import('naive-ui')['NButton']
+    NCalendar: typeof import('naive-ui')['NCalendar']
     NCard: typeof import('naive-ui')['NCard']
     NCheckbox: typeof import('naive-ui')['NCheckbox']
     NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup']

+ 32 - 0
src/typings/elegant-router.d.ts

@@ -49,6 +49,7 @@ declare module "@elegant-router/types" {
     "goods-center": "/goods-center";
     "goods-center_edit-health-goods": "/goods-center/edit-health-goods";
     "goods-center_health-goods": "/goods-center/health-goods";
+    "goods-center_scenic-goods": "/goods-center/scenic-goods";
     "goods-center_store-goods": "/goods-center/store-goods";
     "goods-center_type-admin": "/goods-center/type-admin";
     "goods-center_virtual-goods": "/goods-center/virtual-goods";
@@ -57,6 +58,10 @@ declare module "@elegant-router/types" {
     "government_government-list": "/government/government-list";
     "government_points": "/government/points";
     "government_user-list": "/government/user-list";
+    "h5-manage": "/h5-manage";
+    "h5-manage_channel-manage": "/h5-manage/channel-manage";
+    "h5-manage_edit-channel": "/h5-manage/edit-channel";
+    "h5-manage_user-list": "/h5-manage/user-list";
     "home": "/home";
     "iframe-page": "/iframe-page/:url";
     "jy-manage": "/jy-manage";
@@ -71,9 +76,16 @@ declare module "@elegant-router/types" {
     "manage_role": "/manage/role";
     "manage_schedule": "/manage/schedule";
     "manage_user": "/manage/user";
+    "member-center": "/member-center";
+    "member-center_edit-member-type": "/member-center/edit-member-type";
+    "member-center_member-list": "/member-center/member-list";
+    "member-center_member-order": "/member-center/member-order";
+    "member-center_member-type": "/member-center/member-type";
     "operation": "/operation";
     "operation_accounting-strategy": "/operation/accounting-strategy";
     "operation_coupon": "/operation/coupon";
+    "operation_coupon-issuance": "/operation/coupon-issuance";
+    "operation_coupon-manage": "/operation/coupon-manage";
     "order-manage": "/order-manage";
     "order-manage_after-sales-order": "/order-manage/after-sales-order";
     "order-manage_after-sales-order-detail": "/order-manage/after-sales-order-detail";
@@ -81,6 +93,10 @@ declare module "@elegant-router/types" {
     "order-manage_order-detail": "/order-manage/order-detail";
     "order-manage_ponits-details": "/order-manage/ponits-details";
     "order-manage_recharge-records": "/order-manage/recharge-records";
+    "tenant": "/tenant";
+    "tenant_tenant": "/tenant/tenant";
+    "tenant_tenant-change": "/tenant/tenant-change";
+    "tenant_tenant-package": "/tenant/tenant-package";
     "user-center": "/user-center";
     "user-management": "/user-management";
     "user-management_user-list": "/user-management/user-list";
@@ -143,13 +159,16 @@ declare module "@elegant-router/types" {
     | "film-manage"
     | "goods-center"
     | "government"
+    | "h5-manage"
     | "home"
     | "iframe-page"
     | "jy-manage"
     | "login"
     | "manage"
+    | "member-center"
     | "operation"
     | "order-manage"
+    | "tenant"
     | "user-center"
     | "user-management"
     | "xsb-manage"
@@ -186,6 +205,7 @@ declare module "@elegant-router/types" {
     | "film-manage_setprice"
     | "goods-center_edit-health-goods"
     | "goods-center_health-goods"
+    | "goods-center_scenic-goods"
     | "goods-center_store-goods"
     | "goods-center_type-admin"
     | "goods-center_virtual-goods"
@@ -193,6 +213,9 @@ declare module "@elegant-router/types" {
     | "government_government-list"
     | "government_points"
     | "government_user-list"
+    | "h5-manage_channel-manage"
+    | "h5-manage_edit-channel"
+    | "h5-manage_user-list"
     | "home"
     | "jy-manage_activity"
     | "manage_config"
@@ -203,7 +226,13 @@ declare module "@elegant-router/types" {
     | "manage_role"
     | "manage_schedule"
     | "manage_user"
+    | "member-center_edit-member-type"
+    | "member-center_member-list"
+    | "member-center_member-order"
+    | "member-center_member-type"
     | "operation_accounting-strategy"
+    | "operation_coupon-issuance"
+    | "operation_coupon-manage"
     | "operation_coupon"
     | "order-manage_after-sales-order-detail"
     | "order-manage_after-sales-order"
@@ -211,6 +240,9 @@ declare module "@elegant-router/types" {
     | "order-manage_order-detail"
     | "order-manage_ponits-details"
     | "order-manage_recharge-records"
+    | "tenant_tenant-change"
+    | "tenant_tenant-package"
+    | "tenant_tenant"
     | "user-center"
     | "user-management_user-list"
     | "xsb-manage_advertisement"

+ 25 - 1
src/views/_builtin/login/modules/pwd-login.vue

@@ -2,6 +2,7 @@
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { Crypto } from '@sa/utils';
 import { fetchGetCaptcha } from '@/service/api/common';
+import { fetchGetTenantOptions } from '@/service/api/auth';
 import { useAuthStore } from '@/store/modules/auth';
 import { useFormRules, useNaiveForm } from '@/hooks/common/form';
 import { localStg } from '@/utils/storage';
@@ -15,6 +16,22 @@ const authStore = useAuthStore();
 const { formRef, validate } = useNaiveForm();
 const isRememberMe = ref(false);
 const imgPath = ref('');
+
+// 租户选项
+const tenantOptions = ref<{ label: string; value: string }[]>([]);
+const selectedTenantCode = ref<string | null>(null);
+
+async function loadTenantOptions() {
+  const { data } = await fetchGetTenantOptions();
+  if (data) {
+    tenantOptions.value = (data as any[]).map((item: any) => ({
+      label: item.tenantName,
+      value: item.tenantCode
+    }));
+  }
+}
+loadTenantOptions();
+
 interface FormModel {
   userName: string;
   password: string;
@@ -51,12 +68,16 @@ const rules = computed<Record<keyof FormModel, App.Global.FormRule[]>>(() => {
 const captchaKey = ref('');
 async function handleSubmit() {
   await validate();
+  if (!selectedTenantCode.value) {
+    window.$message?.error('请选择租户');
+    return;
+  }
   if (isRememberMe.value) {
     const pwd = pd.encrypt({ data: model.password });
     localStg.set('userName', model.userName);
     localStg.set('password', pwd);
   }
-  await authStore.login({ ...model, captchaKey: captchaKey.value });
+  await authStore.login({ ...model, captchaKey: captchaKey.value, tenantCode: selectedTenantCode.value || undefined });
 }
 async function getDataCode() {
   const { data } = await fetchGetCaptcha();
@@ -77,6 +98,9 @@ watch(
 
 <template>
   <NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
+    <NFormItem :rule="{ required: false, message: '请选择租户', trigger: 'change' }">
+      <NSelect v-model:value="selectedTenantCode" :options="tenantOptions" placeholder="请选择租户" />
+    </NFormItem>
     <NFormItem path="userName">
       <NInput v-model:value="model.userName" :placeholder="$t('page.login.common.userNamePlaceholder')" />
     </NFormItem>

+ 2 - 2
src/views/djk-manage/edit-activity/index.vue

@@ -1,10 +1,9 @@
 <script setup lang="tsx">
 import { nextTick, onMounted, ref } from 'vue';
-import { useRoute } from 'vue-router';
+import { useRoute, useRouter } from 'vue-router';
 import type { DataTableColumns, FormInst, FormRules } from 'naive-ui';
 import { NForm, NImage, NInput, useMessage } from 'naive-ui';
 import dayjs from 'dayjs';
-import { router } from '@/router';
 import { fetchActivityDetail, fetchAddActivity, fetchEditActivity } from '@/service/api/djk-manage/activity';
 import { fetchProductList } from '@/service/api/goods-center/health-goods';
 import { fetchGetStoreList } from '@/service/api/xsb-manage/store-info';
@@ -12,6 +11,7 @@ import { useTable } from '@/components/zt/Table/hooks/useTable';
 import { useModal } from '@/components/zt/Modal/hooks/useModal';
 import type { FormSchema } from '@/components/zt/Form/types/form';
 import ZUpload from '../../../components/zt/upload/z-upload.vue';
+const router = useRouter();
 const current = ref('A');
 const route = useRoute();
 const formRef = ref<FormInst | null>(null);

+ 3 - 1
src/views/film-manage/film-list/index.vue

@@ -1,10 +1,12 @@
 <script setup lang="tsx">
 import { ref, useTemplateRef } from 'vue';
+import { useRouter } from 'vue-router';
 import { NButton } from 'naive-ui';
-import { router } from '@/router';
 import { fetchGetChannelList, fetchImportGoods } from '@/service/api/goods-center/store-goods';
 import { fetchMoivecinemaList, fetchMovieList } from '@/service/api/film-manage/film-list';
 import { useTable } from '@/components/zt/Table/hooks/useTable';
+
+const router = useRouter();
 const importTemplateRef = useTemplateRef('importTemplateRef');
 
 const options = ref<Api.goods.Channel[]>([]);

+ 0 - 1
src/views/goods-center/edit-health-goods/index.vue

@@ -3,7 +3,6 @@ import { computed, ref } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import type { FormInst, FormRules } from 'naive-ui';
 import { NForm, NInput, NSwitch, useMessage } from 'naive-ui';
-// import { router } from '@/router';
 import { fetchAddGoods, fetchEditGoods, fetchGoodsDetail } from '@/service/api/goods-center/health-goods';
 import { fetchGetStoreList } from '@/service/api/xsb-manage/store-info';
 import ZUpload from '../../../components/zt/upload/z-upload.vue';

+ 240 - 0
src/views/goods-center/scenic-goods/index.vue

@@ -0,0 +1,240 @@
+<script setup lang="tsx">
+import { computed, ref } from 'vue';
+import { NButton, NImage } from 'naive-ui';
+import dayjs from 'dayjs';
+// import { fetchGetStoreList } from '@/service/api/xsb-manage/store-info';
+import { fetchList, fetchPriceList } from '@/service/api/goods-center/scenic-goods';
+import { commonExport } from '@/utils/common';
+import { useTable } from '@/components/zt/Table/hooks/useTable';
+import ButtonIcon from '@/components/custom/button-icon.vue';
+import { useModal } from '@/components/zt/Modal/hooks/useModal';
+const productNo = ref();
+let travelDate = dayjs().format('YYYY-MM-DD');
+const priceObj = ref<
+  Record<
+    string,
+    {
+      settlementPrice: number | string;
+      salePrice: number | string;
+      ticketAvailability: number | string;
+    }
+  >
+>({});
+const columns: NaiveUI.TableColumn<Api.goods.ShopSku>[] = [
+  {
+    key: 'productNo',
+    title: '产品编号',
+    align: 'center',
+    width: 100
+  },
+  {
+    key: 'productName',
+    title: '产品名称',
+    align: 'center',
+    width: 200
+  },
+  {
+    key: 'mobile',
+    title: '产品图片',
+    align: 'center',
+    width: 100,
+    render: row => {
+      return <NImage src={row.img} width="100" height="100" />;
+    }
+  },
+  {
+    key: 'viewName',
+    title: '景区名称',
+    align: 'center',
+    width: 100
+  },
+  {
+    key: 'receiveTime',
+    title: '业务类型',
+    align: 'center',
+    width: 100,
+    render: () => {
+      return '景区门票';
+    }
+  },
+  {
+    key: 'price',
+    title: '价格',
+    align: 'center',
+    width: 70,
+    render: row => {
+      return (
+        <ButtonIcon
+          text
+          type="primary"
+          onClick={() => handlePrice(row.productNo)}
+          icon="material-symbols:calendar-month-rounded"
+          tooltip-content="价格"
+        />
+      );
+    }
+  },
+  {
+    key: 'state',
+    title: '状态',
+    align: 'center',
+    width: 100,
+    render: row => {
+      return row.state == 1 ? '下架' : '上架';
+    }
+  },
+  {
+    key: 'updateTime',
+    title: '更新时间',
+    align: 'center',
+    width: 120
+  }
+];
+
+const [registerTable, { getTableData, getSeachForm, setTableLoading }] = useTable({
+  searchFormConfig: {
+    schemas: [
+      {
+        label: '产品编号',
+        component: 'NInput',
+        field: 'productNo'
+      },
+      {
+        label: '产品名称',
+        component: 'NInput',
+        field: 'productName'
+      },
+      {
+        label: '景区名称',
+        component: 'NInput',
+        field: 'viewName'
+      },
+      {
+        label: '状态',
+        component: 'NSelect',
+        field: 'state',
+        componentProps: {
+          options: [
+            { label: '上架', value: '0' },
+            { label: '下架', value: '1' }
+          ]
+        }
+      }
+    ],
+    inline: false,
+    size: 'small',
+    labelPlacement: 'left',
+    isFull: false
+  },
+  tableConfig: {
+    keyField: 'skuId',
+    title: '领取列表',
+    showAddButton: false,
+    scrollX: 1800,
+    fieldMapToTime: [['createTime', ['receiveStartTime', 'receiveEndTime']]]
+  }
+});
+
+const [registerModalPrice, { openModal, setModalLoading }] = useModal({
+  title: '价格',
+  width: 800,
+  height: 700,
+  showFooter: false
+});
+
+const tableData = computed(() => {
+  return getTableData();
+});
+
+async function handleExport() {
+  setTableLoading(true);
+  try {
+    await commonExport('/smqjh-pms/api/scenery/prod/export', getSeachForm(), '景区产品.xlsx');
+  } finally {
+    setTableLoading(false);
+  }
+}
+
+async function handlePrice(no: any) {
+  productNo.value = no;
+  openModal();
+  setModalLoading(true);
+  const { data } = await fetchPriceList({ travelDate, productNo: productNo.value });
+  console.log(data);
+  priceObj.value = {};
+  data.ticketPrices.forEach((item: any) => {
+    priceObj.value[item.date] = {
+      settlementPrice: item.settlementPrice,
+      salePrice: item.salePrice,
+      ticketAvailability: item.ticketAvailability
+    };
+  });
+
+  setModalLoading(false);
+}
+
+function panelChange(date: any) {
+  console.log('panelChange', date.year, date.month);
+  travelDate = `${date.year}-${date.month < 10 ? `0${date.month}` : date.month}-01`;
+  handlePrice(productNo.value);
+}
+
+function getColorName(state: any) {
+  if (state == '充足') {
+    return { text: 'blue', bg: '#B4E8F4' };
+  } else if (state == '紧张') {
+    return { text: 'orange', bg: '#F6DFB1' };
+  }
+
+  return { text: 'green', bg: '#FCC9C9' };
+}
+
+function isDateDisabled(timestamp: any) {
+  if (dayjs(timestamp).add(1, 'day').isBefore(dayjs())) {
+    return true;
+  }
+  return false;
+}
+</script>
+
+<template>
+  <LayoutTable>
+    <ZTable :columns="columns" :show-table-action="false" :api="fetchList" @register="registerTable">
+      <template #prefix="{ loading }">
+        <NSpace>
+          <NButton size="small" :disabled="tableData.length == 0" :loading="loading" @click="handleExport">
+            导出全部
+          </NButton>
+        </NSpace>
+      </template>
+    </ZTable>
+
+    <BasicModal @register="registerModalPrice">
+      <NCalendar v-slot="{ year, month, date }" :is-date-disabled="isDateDisabled" @panel-change="panelChange">
+        <div
+          v-if="priceObj[dayjs(`${year}-${month}-${date}`).format('YYYY-MM-DD')]"
+          class="h-full flex flex-col justify-between"
+        >
+          <div class="mt-10px text-12px">
+            <div>结算价: {{ priceObj[dayjs(`${year}-${month}-${date}`).format('YYYY-MM-DD')]?.settlementPrice }}</div>
+            <div>零售价: {{ priceObj[dayjs(`${year}-${month}-${date}`).format('YYYY-MM-DD')]?.salePrice }}</div>
+          </div>
+          <div
+            class="mb-2px text-center"
+            :style="{
+              backgroundColor: getColorName(
+                priceObj[dayjs(`${year}-${month}-${date}`).format('YYYY-MM-DD')]?.ticketAvailability
+              ).bg,
+              color: getColorName(priceObj[dayjs(`${year}-${month}-${date}`).format('YYYY-MM-DD')]?.ticketAvailability)
+                .text
+            }"
+          >
+            {{ priceObj[dayjs(`${year}-${month}-${date}`).format('YYYY-MM-DD')]?.ticketAvailability }}
+          </div>
+        </div>
+      </NCalendar>
+    </BasicModal>
+  </LayoutTable>
+</template>
+
+<style scoped></style>

+ 13 - 0
src/views/goods-center/virtual-goods/index.vue

@@ -162,6 +162,19 @@ const columns: NaiveUI.TableColumn<Api.goods.ShopSku>[] = [
       );
     }
   },
+
+  {
+    key: 'productStatusMsg',
+    title: '变更原因',
+    align: 'center',
+    width: 120,
+    ellipsis: {
+      tooltip: true
+    },
+    render: (row: any) => {
+      return row.productStatus == 1 ? row.productStatusMsg : '';
+    }
+  },
   {
     key: 'updateTime',
     title: '更新时间',

+ 194 - 0
src/views/h5-manage/channel-manage/index.vue

@@ -0,0 +1,194 @@
+<script setup lang="tsx">
+import { useRouter } from 'vue-router';
+import { NButton, NSwitch } from 'naive-ui';
+// import dayjs from 'dayjs';
+import { fetchChannelList, fetchDelChannel } from '@/service/api/h5-manage/channel-manage';
+import { useTable } from '@/components/zt/Table/hooks/useTable';
+const router = useRouter();
+
+const columns: NaiveUI.TableColumn<Api.goods.ShopSku>[] = [
+  {
+    key: 'accessId',
+    title: '渠道ID',
+    align: 'center',
+    width: 100
+  },
+  {
+    key: 'accessName',
+    title: '渠道名称',
+    align: 'center',
+    width: 100
+  },
+  {
+    key: 'tenantCode',
+    title: '租户编号',
+    align: 'center',
+    width: 100
+  },
+  {
+    key: 'payOwnership',
+    title: '支付归属',
+    align: 'center',
+    width: 100,
+    render: row => {
+      return row.payOwnership === 1 ? '平台支付' : '第三方支付';
+    }
+  },
+  {
+    key: 'status',
+    title: '状态',
+    align: 'center',
+    width: 100,
+    render: row => {
+      return <NSwitch uncheckedValue={0} checkedValue={1} value={row.status} disabled={true}></NSwitch>;
+    }
+  },
+
+  {
+    key: 'themeMainColor',
+    title: '主题样式',
+    align: 'center',
+    width: 100,
+    render: (row: any) => {
+      return (
+        <div>
+          <div>主色:{row.themeMainColor}</div>
+          <div>辅色:{row.themeSubColor}</div>
+        </div>
+      );
+    }
+  },
+  {
+    key: 'updateTime',
+    title: '更新时间',
+    align: 'center',
+    width: 100
+  }
+];
+
+const [registerTable, { refresh }] = useTable({
+  searchFormConfig: {
+    schemas: [
+      {
+        label: '渠道名称',
+        component: 'NInput',
+        field: 'accessName'
+      },
+      // {
+      //   label: '开放渠道',
+      //   component: 'dictSelect',
+      //   field: 'openChannel',
+      //   componentProps: {
+      //     dictCode: 'open_channel',
+      //     immediate: true
+      //   }
+      // },
+      {
+        label: '状态',
+        field: 'status',
+        component: 'NSelect',
+        componentProps: {
+          options: [
+            {
+              label: '禁用',
+              value: 0
+            },
+            {
+              label: '启用',
+              value: 1
+            }
+          ]
+        }
+      },
+      {
+        label: '支付归属',
+        field: 'payOwnership',
+        component: 'NSelect',
+        componentProps: {
+          options: [
+            {
+              label: '平台支付',
+              value: 1
+            },
+            {
+              label: '第三方支付',
+              value: 2
+            }
+          ]
+        }
+      }
+    ],
+    inline: false,
+    size: 'small',
+    labelPlacement: 'left',
+    isFull: false
+  },
+  tableConfig: {
+    opWdith: 160,
+    keyField: 'skuId',
+    title: '活动列表',
+    showAddButton: true,
+    scrollX: 1800,
+    fieldMapToTime: [
+      ['price', ['minPrice', 'maxPrice']],
+      ['createTime', ['startTime', 'endTime']]
+    ]
+  }
+});
+
+function handleAdd() {
+  router.push({
+    path: '/h5-manage/edit-channel'
+  });
+}
+function handleDetail(row: any) {
+  router.push({
+    path: '/h5-manage/edit-channel',
+    query: {
+      id: row.id,
+      mode: 'detail'
+    }
+  });
+}
+function handleEdit(row: any) {
+  console.log('edit', row);
+  router.push({
+    path: '/h5-manage/edit-channel',
+    query: {
+      id: row.id,
+      mode: 'edit'
+    }
+  });
+}
+
+function handleDelete(row: any) {
+  window.$dialog?.info({
+    title: '提示',
+    content: `你确定要删除吗?`,
+    positiveText: '确定',
+    negativeText: '取消',
+    onPositiveClick: async () => {
+      const { error } = await fetchDelChannel(row.id as string);
+      if (!error) {
+        refresh();
+      }
+    }
+  });
+}
+</script>
+
+<template>
+  <LayoutTable>
+    <ZTable :columns="columns" :api="fetchChannelList" @add="handleAdd" @register="registerTable">
+      <template #op="{ row }">
+        <NSpace>
+          <NButton size="small" @click="handleEdit(row)">编辑</NButton>
+          <NButton size="small" @click="handleDelete(row)">删除</NButton>
+          <NButton size="small" @click="handleDetail(row)">详情</NButton>
+        </NSpace>
+      </template>
+    </ZTable>
+  </LayoutTable>
+</template>
+
+<style scoped></style>

+ 339 - 0
src/views/h5-manage/edit-channel/index.vue

@@ -0,0 +1,339 @@
+<script setup lang="tsx">
+import { onMounted, ref, watch } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import type { FormInst } from 'naive-ui';
+import { NForm, NInput, useMessage } from 'naive-ui';
+import {
+  fetchAddChannel,
+  fetchAppSecret,
+  fetchChannelDetail,
+  fetchEditChannel,
+  fetchResetAppSecret
+} from '@/service/api/h5-manage/channel-manage';
+import { fetchGetTenantList } from '@/service/api/tenant';
+import ZUpload from '../../../components/zt/upload/z-upload.vue';
+const router = useRouter();
+const route = useRoute();
+const formRef = ref<FormInst | null>(null);
+const message = useMessage();
+const disabled = ref(false);
+const isShow = ref(false);
+const payOwnershipDisabled = ref(false);
+const loading = ref(false);
+
+const model = ref({
+  appId: '',
+  appSecret: '',
+  id: 0,
+  accessName: '',
+  tenantId: undefined,
+  status: 0,
+  logoUrl: '',
+  remark: '',
+  payOwnership: 1,
+  shareRatio: 0,
+  thirdCallbackUrl: '',
+  themeMainColor: '',
+  themeSubColor: '',
+  h5Url: '',
+  authCallbackUrl: '',
+  payCallbackUrl: ''
+});
+
+const rules = ref({
+  accessName: {
+    required: true,
+    trigger: ['blur', 'input'],
+    message: '请输入'
+  },
+
+  logoUrl: {
+    required: true,
+    trigger: ['blur', 'change'],
+    message: '请上传'
+  },
+  remark: {
+    required: true,
+    trigger: ['blur', 'change'],
+    message: '请输入'
+  },
+  tenantId: {
+    type: 'number' as const,
+    required: true,
+    trigger: ['blur', 'change'],
+    message: '请输入'
+  },
+  themeMainColor: {
+    required: true,
+    trigger: ['blur', 'change'],
+    message: '请选择'
+  },
+  themeSubColor: {
+    required: true,
+    trigger: ['blur', 'change'],
+    message: '请输入'
+  },
+  authCallbackUrl: {
+    required: true,
+    trigger: ['blur', 'change'],
+    message: '请输入'
+  },
+  payCallbackUrl: {
+    required: true,
+    trigger: ['blur', 'change'],
+    message: '请选择'
+  },
+  shareRatio: {
+    type: 'number' as const,
+    required: true,
+    trigger: ['blur', 'change'],
+    message: '请输入分账比例'
+  },
+  thirdCallbackUrl: {
+    required: true,
+    trigger: ['blur', 'change'],
+    message: '请输入回调地址'
+  }
+  // thirdCallbackUrl: {}
+});
+
+watch(
+  () => model.value.payOwnership,
+  newVal => {
+    if (newVal === 1) {
+      rules.value.shareRatio.required = true;
+      rules.value.thirdCallbackUrl.required = false;
+    } else if (newVal === 2) {
+      // 第三方账户:显示回调地址
+      rules.value.thirdCallbackUrl.required = true;
+      rules.value.shareRatio.required = false;
+    }
+  },
+  { immediate: true }
+);
+
+function handleValidateButtonClick(e: MouseEvent) {
+  e.preventDefault();
+  formRef.value?.validate(async errors => {
+    if (!errors) {
+      loading.value = true;
+
+      const form = JSON.parse(JSON.stringify(model.value));
+
+      let res;
+      if (route.query.mode == 'edit') {
+        res = await fetchEditChannel(form);
+      } else {
+        res = await fetchAddChannel(form);
+      }
+      loading.value = false;
+      console.log(res);
+
+      if (!res.error) {
+        router.push({
+          path: '/h5-manage/channel-manage'
+        });
+      }
+    } else {
+      console.log(errors);
+      message.error('验证失败');
+    }
+  });
+}
+
+async function getSecret() {
+  if (isShow.value) {
+    return;
+  }
+
+  // 获取appSecret接口
+  const res = await fetchAppSecret(route.query.id);
+  if (res.data) {
+    model.value.appSecret = res.data.appSecret;
+    isShow.value = true;
+  }
+}
+
+async function handleResetSecret() {
+  window.$dialog?.info({
+    title: '提示',
+    content: `你确定要重置秘钥吗?`,
+    positiveText: '确定',
+    negativeText: '取消',
+    onPositiveClick: async () => {
+      // 重置秘钥接口
+      const res = await fetchResetAppSecret(route.query.id);
+      if (res.data) {
+        model.value.appSecret = res.data.appSecret;
+        isShow.value = true;
+        message.success('重置成功');
+      }
+    }
+  });
+}
+
+async function getDetail() {
+  // 获取详情接口
+  const res = await fetchChannelDetail(route.query.id);
+  if (res.data) {
+    model.value = res.data;
+    model.value.id = Number(route.query.id);
+  }
+}
+
+onMounted(() => {
+  if (route.query.id) {
+    payOwnershipDisabled.value = true;
+    if (route.query.mode === 'detail') {
+      disabled.value = true;
+    }
+    getDetail();
+  }
+  console.log(route.query);
+});
+</script>
+
+<template>
+  <div class="edit-health-goods pl-20px pt-20px">
+    <NForm
+      ref="formRef"
+      :model="model"
+      :rules="rules"
+      label-placement="left"
+      label-width="120px"
+      require-mark-placement="right-hanging"
+      size="medium"
+      class="max-w-840px"
+      :disabled="disabled"
+    >
+      <div class="font-600">基本信息</div>
+      <template v-if="disabled">
+        <NFormItem label="app_id">
+          <div>{{ model.appId }}</div>
+          <!-- <NInput v-model:value="model.appId" placeholder="" /> -->
+        </NFormItem>
+        <NFormItem label="app_secret">
+          <div class="flex items-center">
+            <div>{{ model.appSecret }}</div>
+            <div @click="getSecret">
+              <SvgIcon v-if="!isShow" icon="weui:eyes-off-filled" class="ml-10px"></SvgIcon>
+            </div>
+            <NButton class="ml-10px" round type="primary" size="small" :loading="loading" @click="handleResetSecret">
+              重置秘钥
+            </NButton>
+          </div>
+          <!-- <NInput v-model:value="model.appSecret" placeholder="" /> -->
+        </NFormItem>
+      </template>
+      <NFormItem label="渠道名称" path="accessName">
+        <div class="w-100%">
+          <NInput v-model:value="model.accessName" :maxlength="20" placeholder="" />
+          <div class="text-12px">最多支持20个汉字;</div>
+        </div>
+      </NFormItem>
+
+      <NFormItem label="租户编号" path="tenantId">
+        <ApiSelect
+          v-model:value="model.tenantId"
+          :api="fetchGetTenantList"
+          result-feild="data.list"
+          label-field="tenantCode"
+          value-field="id"
+        ></ApiSelect>
+      </NFormItem>
+
+      <NFormItem label="状态">
+        <NSwitch v-model:value="model.status" :unchecked-value="0" :checked-value="1" />
+      </NFormItem>
+
+      <NFormItem label="渠道LOGO" path="logoUrl">
+        <ZUpload v-model:value="model.logoUrl" :max="1"></ZUpload>
+      </NFormItem>
+      <NFormItem label="备注" path="remark">
+        <div class="w-100%">
+          <NInput v-model:value="model.remark" type="textarea" show-count :maxlength="500" placeholder="" />
+          <div class="text-12px">最多支持500个汉字;</div>
+        </div>
+      </NFormItem>
+
+      <div class="flex">
+        <div class="items-center font-600">支付归属</div>
+        <div class="ml-20px text-12px">支付归属影响资金流向,一旦确定不可变更。如需切换,建议新建渠道后迁移。</div>
+      </div>
+
+      <NFormItem label="支付方式设置" path="payType">
+        <!-- 平台支付 -->
+        <NRadioGroup v-model:value="model.payOwnership" :disabled="payOwnershipDisabled" name="radiogroup">
+          <NFormItem label="" path="shareRatio">
+            <div class="flex-col">
+              <div class="flex items-center">
+                <NRadio :value="1" class="mr-2" />
+                <span class="text-sm">平台支付(资金进入平台商户号,平台统一收款)</span>
+              </div>
+
+              <div v-if="model.payOwnership === 1" class="flex items-center gap-2 pl-6 text-sm">
+                <span>分账比例设置</span>
+                <NInputNumber
+                  v-model:value="model.shareRatio"
+                  :disabled="payOwnershipDisabled"
+                  :min="0"
+                  :max="100"
+                  :precision="2"
+                />
+                <span>% 给渠道方</span>
+              </div>
+              <p v-if="model.payOwnership === 1" class="ml-120px text-xs text-gray-500">范围:0-100,保留两位小数</p>
+            </div>
+          </NFormItem>
+          <!-- 第三方支付 -->
+          <NFormItem label="" path="thirdCallbackUrl">
+            <div class="flex items-center">
+              <NRadio :value="2" class="mr-2" />
+              <span class="text-sm">第三方支付(资金进入第三方商户,平台只提供技术服务)</span>
+            </div>
+
+            <div v-if="model.payOwnership === 2" class="flex items-center gap-2 pl-6 text-sm">
+              <span>第三方回调地址</span>
+              <NInput
+                v-model:value="model.thirdCallbackUrl"
+                :disabled="payOwnershipDisabled"
+                placeholder="请输入回调地址"
+                class="flex-1"
+              />
+            </div>
+          </NFormItem>
+        </NRadioGroup>
+      </NFormItem>
+
+      <div class="font-600">主题样式</div>
+      <NFormItem label="主色" path="themeMainColor">
+        <NInput v-model:value="model.themeMainColor" placeholder="" />
+      </NFormItem>
+      <NFormItem label="辅色" path="themeSubColor">
+        <NInput v-model:value="model.themeSubColor" placeholder="" />
+      </NFormItem>
+
+      <div class="font-600">对接配置</div>
+
+      <NFormItem label="授权回调地址" path="authCallbackUrl">
+        <NInput v-model:value="model.authCallbackUrl" placeholder="" />
+      </NFormItem>
+      <NFormItem label="支付回调地址" path="payCallbackUrl">
+        <NInput v-model:value="model.payCallbackUrl" placeholder="" />
+      </NFormItem>
+
+      <div v-if="!disabled" class="flex justify-end">
+        <NButton round type="primary" :loading="loading" @click="handleValidateButtonClick">保存</NButton>
+      </div>
+    </NForm>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+::v-deep .n-form-item-blank {
+  z-index: 6 !important;
+}
+.edit-health-goods {
+  background: #fff;
+}
+</style>

+ 123 - 0
src/views/h5-manage/user-list/index.vue

@@ -0,0 +1,123 @@
+<script setup lang="tsx">
+import { NImage } from 'naive-ui';
+import { fetchUserList } from '@/service/api/h5-manage/user-list';
+import { fetchChannelList } from '@/service/api/h5-manage/channel-manage';
+import { fetchBackendChannelSelect } from '@/service/api/xsb-manage/store-info';
+import { useTable } from '@/components/zt/Table/hooks/useTable';
+
+const columns: NaiveUI.TableColumn<Api.goods.ShopSku>[] = [
+  {
+    key: 'id',
+    title: '用户ID',
+    align: 'center'
+  },
+  {
+    key: 'accessId',
+    title: '渠道ID',
+    align: 'center'
+  },
+  {
+    key: 'accessName',
+    title: '渠道',
+    align: 'center'
+  },
+
+  {
+    key: 'realName',
+    title: '姓名',
+    align: 'center'
+  },
+  {
+    key: 'nickName',
+    title: '用户昵称',
+    align: 'center'
+  },
+  {
+    key: 'avatarUrl',
+    title: '头像',
+    align: 'center',
+    render: row => <NImage src={row.avatarUrl} class="h-[80px] min-w-80px w-[80px]" lazy />
+  },
+  {
+    key: 'mobile',
+    title: '手机号码',
+    align: 'center'
+  },
+  {
+    key: 'channelName',
+    title: '企业',
+    align: 'center'
+  },
+  {
+    key: 'createTime',
+    title: '注册时间',
+    align: 'center'
+  }
+];
+
+const [registerTable] = useTable({
+  searchFormConfig: {
+    schemas: [
+      {
+        field: 'mobile',
+        label: '用户手机号',
+        component: 'NInput'
+      },
+      {
+        field: 'id',
+        label: '用户ID',
+        component: 'NInput'
+      },
+      {
+        field: 'accessId',
+        label: '渠道',
+        component: 'ApiSelect',
+        componentProps: {
+          api: fetchChannelList,
+          resultFeild: 'data.list',
+          labelFeild: 'accessName',
+          valueFeild: 'accessId'
+        }
+      },
+      {
+        label: '企业',
+        field: 'channelId',
+        component: 'ApiSelect',
+        componentProps: {
+          api: fetchBackendChannelSelect,
+          multiple: true,
+          labelFeild: 'name',
+          valueFeild: 'id'
+        }
+      },
+      {
+        label: '注册时间',
+        component: 'NDatePicker',
+        field: 'createTime',
+        componentProps: {
+          type: 'datetimerange',
+          defaultTime: ['00:00:00', '23:59:59']
+        }
+      }
+    ],
+    inline: false,
+    size: 'small',
+    labelPlacement: 'left',
+    isFull: false
+  },
+  tableConfig: {
+    keyField: 'id',
+    title: '用户列表',
+    showAddButton: false,
+    fieldMapToTime: [['createTime', ['startTime', 'endTime']]]
+  }
+});
+</script>
+
+<template>
+  <LayoutTable>
+    <ZTable :columns="columns" :api="fetchUserList" :show-table-action="false" @register="registerTable"></ZTable>
+  </LayoutTable>
+</template>
+
+<style scoped></style>

+ 1 - 1
src/views/jy-manage/activity/index.vue

@@ -123,7 +123,7 @@ const [
   }
 ] = useModalFrom({
   modalConfig: {
-    title: '一级分类',
+    title: '优惠券',
     isShowHeaderText: true
   },
   formConfig: {

+ 1107 - 0
src/views/member-center/edit-member-type/index.vue

@@ -0,0 +1,1107 @@
+<script setup lang="tsx">
+import { computed, nextTick, onMounted, ref, watch } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import type { DataTableColumns, FormInst, FormRules } from 'naive-ui';
+import { NForm, NImage, NInput, NSelect, useMessage } from 'naive-ui';
+import {
+  fetchAddType,
+  fetchEditType,
+  fetchFirstChannel,
+  fetchGoodsList,
+  fetchTypeDetail
+} from '@/service/api/member-center/member-type';
+import { fetchGetActivityList } from '@/service/api/operation/coupon-manage';
+import { fetchGetStoreList } from '@/service/api/xsb-manage/store-info';
+import { useTable } from '@/components/zt/Table/hooks/useTable';
+import { useModal } from '@/components/zt/Modal/hooks/useModal';
+import ZUpload from '../../../components/zt/upload/z-upload.vue';
+const router = useRouter();
+const current = ref('A');
+const route = useRoute();
+const formRef = ref<FormInst | null>(null);
+const message = useMessage();
+const disabled = ref(false);
+const loading = ref(false);
+
+const cancelOptions = [
+  { label: '立即生效', value: 'IMMEDIATE' },
+  { label: '次月生效', value: 'NEXT_MONTH' }
+];
+// 1 厂家直冲,2 卖家直发,3 官方直充,4 扫码直充,5 卖家代充,6 卡密
+const rechargeOptions = [
+  { label: '厂家直充', value: 1 },
+  { label: '卖家直发', value: 2 },
+  { label: '官方直充', value: 3 },
+  { label: '扫码直充', value: 4 },
+  { label: '卖家代充', value: 5 },
+  { label: '卡密', value: 6 }
+];
+
+const channelOptions = ref<{ label: string; value: string; id: number }[]>([]);
+
+const model = ref({
+  id: 0,
+  memberName: '',
+  coverImg: '',
+  faceAmount: 0,
+  stock: 0,
+  settleAmount: 0,
+  rechargeType: null,
+  brandName: '市民请集合',
+  businessType: 'XSB',
+  useValidDays: 0,
+  cancelRule: '',
+  useRule: '',
+  rightsDesc: '',
+  openChannelCodes: '',
+  priceChannelId: '',
+  benefitConfigJson: {
+    oilPerLiterDiscount: 0,
+    oilIcon: '',
+    oilRoutePath: '',
+    mallDiscountRate: 0,
+    mallIcon: '',
+    mallRoutePath: '',
+    chargePerKwhDiscount: 0,
+    chargeIcon: '',
+    chargeRoutePath: '',
+    parkingDiscountRate: 0,
+    parkingIcon: '',
+    parkingRoutePath: ''
+  },
+  couponIcon: '',
+  couponRoutePath: '',
+  couponConfigJson: [{ couponId: '', count: 1 }] as any,
+  giftIcon: '',
+  giftRoutePath: '',
+  giftConfigJson: [] as any,
+  status: 0
+});
+
+const isCheckedOilPerLiterDiscount = ref(false);
+const isCheckedMallDiscountRate = ref(false);
+const isCheckedChargePerKwhDiscount = ref(false);
+const isCheckedParkingDiscountRate = ref(false);
+const isCheckedCouponConfigJson = ref(false);
+const isCheckedGiftConfigJson = ref(false);
+const isCheckedStock = computed(() => {
+  if (model.value.stock === -1) {
+    return true;
+  }
+  return false;
+});
+
+const currentChannel = ref<any>();
+const selectedChannelForSearch = ref<any>();
+
+const rules: FormRules = {
+  memberName: {
+    required: true,
+    trigger: ['blur', 'input'],
+    message: '请输入'
+  },
+  coverImg: {
+    type: 'string',
+    required: true,
+    trigger: ['blur', 'change'],
+    message: '请上传'
+  },
+  faceAmount: {
+    type: 'number',
+    min: 1,
+    max: 100000,
+    required: true,
+    trigger: ['blur', 'change'],
+    message: '请输入'
+  },
+  settleAmount: {
+    min: 1,
+    max: 100000,
+    type: 'number',
+    required: true,
+    trigger: ['blur', 'input'],
+    message: '请输入'
+  },
+  brandName: {
+    type: 'string',
+    required: true,
+    trigger: ['blur', 'change'],
+    message: '请输入'
+  },
+  rechargeType: {
+    type: 'number',
+    required: true,
+    trigger: ['blur', 'change'],
+    message: '请选择'
+  },
+  stock: {
+    type: 'number',
+    required: true,
+    trigger: ['blur', 'change'],
+    message: '请输入'
+  },
+  useValidDays: {
+    type: 'number',
+    required: true,
+    trigger: ['blur', 'change'],
+    message: '请输入'
+  },
+  cancelRule: {
+    required: true,
+    trigger: ['blur', 'change'],
+    message: '请选择'
+  },
+  useRule: {
+    required: true,
+    trigger: ['blur', 'change'],
+    message: '请输入'
+  },
+  openChannelCodes: {
+    required: true,
+    trigger: ['blur', 'change'],
+    message: '请输入'
+  },
+  priceChannelId: {
+    type: 'number',
+    required: true,
+    trigger: ['blur', 'change'],
+    message: '请输入'
+  },
+  benefitConfigJson: {
+    required: true,
+    trigger: ['blur', 'change'],
+    message: '请输入'
+  }
+};
+
+interface ProductItem {
+  goodsImg: string;
+  goodsName: string;
+  goodsCode: string;
+  name?: string;
+}
+
+const columns: DataTableColumns<ProductItem> = [
+  {
+    type: 'selection',
+    multiple: true
+  },
+  {
+    key: 'hbSkuId',
+    title: '商品ID',
+    align: 'left',
+    width: 200
+  },
+  {
+    key: 'skuCode',
+    title: '商品编码',
+    align: 'center',
+    width: 120,
+    ellipsis: {
+      tooltip: true
+    }
+  },
+
+  {
+    key: 'pic',
+    title: '商品图片',
+    align: 'center',
+    width: 120,
+    render: (row: any) => {
+      return <n-image src={row.pic} width={60} height={60}></n-image>;
+    }
+  },
+
+  {
+    key: 'prodName',
+    title: '商品名称',
+    align: 'center',
+    width: 120,
+    ellipsis: {
+      tooltip: true
+    }
+  },
+  {
+    key: 'shopSkuStocks',
+    title: '库存',
+    align: 'center',
+    width: 120,
+    ellipsis: {
+      tooltip: true
+    }
+  },
+  {
+    key: 'channelVOS',
+    title: '价格',
+    align: 'center',
+    width: 120,
+    render: (row: any) => {
+      return (
+        <div>
+          {row.channelVOS.map((it: Api.government.ChannelVO) => {
+            if (it.channelId == currentChannel.value?.id) {
+              return (
+                <div>
+                  {it.channelName}:¥{it.price}
+                </div>
+              );
+            }
+            return null;
+          })}
+        </div>
+      );
+    }
+  },
+  {
+    key: 'shopName',
+    title: '门店名称',
+    align: 'center',
+    width: 120,
+    ellipsis: {
+      tooltip: true
+    }
+  }
+];
+
+const [
+  registerModalTable,
+  { refresh, getTableCheckedRowKeys, getTableData, setFieldsValue, setTableCheckedRowKeys, getSeachForm }
+] = useTable({
+  searchFormConfig: {
+    schemas: [
+      // {
+      //   label: '销售渠道',
+      //   component: 'ApiSelect',
+      //   field: 'channelNo',
+      //   componentProps: {
+      //     api: fetchFirstChannel,
+      //     resultFeild: 'data',
+      //     labelFeild: 'channelName',
+      //     valueFeild: 'channelNo'
+      //   }
+      // },
+      {
+        label: '销售渠道',
+        component: 'NSelect',
+        field: 'channelNo',
+        render: form => {
+          return (
+            <NSelect
+              v-model:value={form.model.channelNo}
+              placeholder="请选择销售渠道"
+              options={channelOptions.value}
+              on-update:value={changeChannel}
+            ></NSelect>
+          );
+        }
+      },
+      {
+        label: '门店名称',
+        component: 'ApiSelect',
+        field: 'shopId',
+        componentProps: {
+          api: fetchGetStoreList,
+          clearable: false,
+          resultFeild: 'data.list',
+          labelFeild: 'shopName',
+          valueFeild: 'shopId',
+          getOptions: (options: any) => {
+            setFieldsValue({
+              shopId: options[0]?.shopId || ''
+            });
+          }
+        }
+      },
+      {
+        label: '商品名称',
+        component: 'NInput',
+        field: 'skuName',
+        componentProps: {
+          placeholder: '请输入商品名称'
+        }
+      },
+      {
+        label: '商品ID',
+        component: 'NInput',
+        field: 'keywords'
+      },
+      {
+        label: '商品编码',
+        component: 'NInput',
+        field: 'skuCode'
+      }
+    ],
+    inline: false,
+    size: 'small',
+    labelPlacement: 'left',
+    isFull: false
+  },
+  tableConfig: {
+    keyField: 'skuId',
+    title: '商品列表',
+    showAddButton: false,
+    minHeight: 650,
+    defaultParamsNotReset: ['channelNo', 'shopId']
+  }
+});
+
+const [registerModal, { openModal, closeModal, setModalProps }] = useModal({
+  title: `商品选择(已选${model.value.giftConfigJson.length}项)`,
+  height: 800,
+  width: 1200
+});
+watch(
+  () => getTableCheckedRowKeys(),
+  val => {
+    setModalProps({
+      title: `商品选择(已选${val.length}项)`
+    });
+  }
+);
+function changeChannel(value: string, opt: any) {
+  setFieldsValue({
+    channelNo: value
+  });
+  console.log(11111111111, value, opt);
+  // currentChannel.value = opt;
+  // 修改:不直接更新currentChannel,而是更新待搜索的渠道
+  selectedChannelForSearch.value = opt;
+}
+function getChannel() {
+  fetchFirstChannel().then(res => {
+    channelOptions.value = res.data.map((item: any) => {
+      return {
+        label: item.channelName,
+        value: item.channelNo,
+        id: item.id
+      };
+    });
+    currentChannel.value = channelOptions.value[0];
+
+    console.log(222222222, res);
+  });
+}
+
+getChannel();
+function handleValidateButtonClick(e: MouseEvent) {
+  e.preventDefault();
+  formRef.value?.validate(async errors => {
+    if (!errors) {
+      loading.value = true;
+
+      const form = JSON.parse(JSON.stringify(model.value));
+      let rightsDesc = '';
+      let num = 0;
+
+      // 组装权益配置为扁平对象格式
+      const benefitObj: any = {};
+      if (isCheckedOilPerLiterDiscount.value) {
+        num += 1;
+        rightsDesc += `${num}、全省中国石化加油站加油优惠${form.benefitConfigJson.oilPerLiterDiscount}元/升;\n `;
+        benefitObj.oilPerLiterDiscount = form.benefitConfigJson.oilPerLiterDiscount;
+        benefitObj.oilIcon = form.benefitConfigJson.oilIcon;
+        benefitObj.oilRoutePath = form.benefitConfigJson.oilRoutePath;
+      }
+      if (isCheckedMallDiscountRate.value) {
+        num += 1;
+        rightsDesc += `${num}、市民请结合平台超6000+快消品优惠${form.benefitConfigJson.mallDiscountRate}%;\n `;
+        benefitObj.mallDiscountRate = form.benefitConfigJson.mallDiscountRate;
+        benefitObj.mallIcon = form.benefitConfigJson.mallIcon;
+        benefitObj.mallRoutePath = form.benefitConfigJson.mallRoutePath;
+      }
+      if (isCheckedChargePerKwhDiscount.value) {
+        num += 1;
+        rightsDesc += `${num}、全省贵阳城投充电桩充电优惠${form.benefitConfigJson.chargePerKwhDiscount}元/度;\n `;
+        benefitObj.chargePerKwhDiscount = form.benefitConfigJson.chargePerKwhDiscount;
+        benefitObj.chargeIcon = form.benefitConfigJson.chargeIcon;
+        benefitObj.chargeRoutePath = form.benefitConfigJson.chargeRoutePath;
+      }
+      if (isCheckedParkingDiscountRate.value) {
+        num += 1;
+        rightsDesc += `${num}、全场18000+贵阳城投停车位停车优惠${form.benefitConfigJson.parkingDiscountRate}%;\n `;
+        benefitObj.parkingDiscountRate = form.benefitConfigJson.parkingDiscountRate;
+        benefitObj.parkingIcon = form.benefitConfigJson.parkingIcon;
+        benefitObj.parkingRoutePath = form.benefitConfigJson.parkingRoutePath;
+      }
+
+      form.benefitConfigJson = JSON.stringify(benefitObj);
+
+      // 处理优惠券配置
+      if (!isCheckedCouponConfigJson.value) {
+        form.couponConfigJson = '';
+      } else {
+        num += 1;
+        const couponArr = [{ icon: form.couponIcon, routePath: form.couponRoutePath }, ...form.couponConfigJson];
+        rightsDesc += `${num}、市民请集合本地生活服务平台${form.couponConfigJson.length}张优惠券;\n `;
+        form.couponConfigJson = JSON.stringify(couponArr);
+      }
+      delete form.couponIcon;
+      delete form.couponRoutePath;
+
+      // 处理赠品配置
+      if (!isCheckedGiftConfigJson.value) {
+        form.giftConfigJson = '';
+      } else {
+        num += 1;
+        const giftArr = [{ icon: form.giftIcon, routePath: form.giftRoutePath }, ...form.giftConfigJson];
+        rightsDesc += `${num}、提供${form.giftConfigJson.length}种选1种商品赠送;\n `;
+        form.giftConfigJson = JSON.stringify(giftArr);
+      }
+      delete form.giftIcon;
+      delete form.giftRoutePath;
+      form.rightsDesc = rightsDesc;
+      let res;
+      if (route.query.mode == 'edit') {
+        res = await fetchEditType(form);
+      } else {
+        res = await fetchAddType(form);
+      }
+      loading.value = false;
+      console.log(res);
+
+      if (!res.error) {
+        router.push({
+          path: '/member-center/member-type'
+        });
+      }
+    } else {
+      console.log(errors);
+      message.error('验证失败');
+    }
+  });
+}
+
+/** 解析权益配置JSON(数组/对象格式转为扁平对象) */
+function parseBenefitConfig(jsonStr: string | undefined) {
+  const benefitFlat: any = {
+    oilPerLiterDiscount: 0,
+    oilIcon: '',
+    oilRoutePath: '',
+    mallDiscountRate: 0,
+    mallIcon: '',
+    mallRoutePath: '',
+    chargePerKwhDiscount: 0,
+    chargeIcon: '',
+    chargeRoutePath: '',
+    parkingDiscountRate: 0,
+    parkingIcon: '',
+    parkingRoutePath: ''
+  };
+  if (!jsonStr) return benefitFlat;
+  const benefitArr = JSON.parse(jsonStr);
+  if (Array.isArray(benefitArr)) {
+    benefitArr.forEach((item: any) => Object.assign(benefitFlat, item));
+  } else {
+    Object.assign(benefitFlat, benefitArr);
+  }
+  return benefitFlat;
+}
+
+/** 解析带图标路由配置的数组JSON(首项为icon/routePath) */
+function parseConfigWithIcon(jsonStr: string | undefined) {
+  if (!jsonStr) return { icon: '', routePath: '', list: [] as any[] };
+  const arr = JSON.parse(jsonStr);
+  if (Array.isArray(arr) && arr.length > 0 && arr[0].icon !== undefined) {
+    return { icon: arr[0].icon || '', routePath: arr[0].routePath || '', list: arr.slice(1) };
+  }
+  return { icon: '', routePath: '', list: arr };
+}
+
+async function getDetail() {
+  // 获取详情接口
+  const res = await fetchTypeDetail(route.query.id);
+  if (res.data) {
+    // 先解析所有JSON字段,避免直接赋值字符串触发模板v-for报错
+    const benefitConfig = parseBenefitConfig(res.data.benefitConfigJson);
+    const couponParsed = parseConfigWithIcon(res.data.couponConfigJson);
+    const giftParsed = parseConfigWithIcon(res.data.giftConfigJson);
+
+    // 赋值前先处理好JSON字段
+    const data = { ...res.data };
+    data.benefitConfigJson = benefitConfig;
+    data.couponIcon = couponParsed.icon;
+    data.couponRoutePath = couponParsed.routePath;
+    data.couponConfigJson = couponParsed.list.length ? couponParsed.list : [{ couponId: '', count: 1 }];
+    data.giftIcon = giftParsed.icon;
+    data.giftRoutePath = giftParsed.routePath;
+    data.giftConfigJson = giftParsed.list;
+
+    model.value = data;
+
+    // 处理配置权益勾选状态
+    if (benefitConfig.oilPerLiterDiscount) isCheckedOilPerLiterDiscount.value = true;
+    if (benefitConfig.mallDiscountRate) isCheckedMallDiscountRate.value = true;
+    if (benefitConfig.chargePerKwhDiscount) isCheckedChargePerKwhDiscount.value = true;
+    if (benefitConfig.parkingDiscountRate) isCheckedParkingDiscountRate.value = true;
+    if (res.data.couponConfigJson) isCheckedCouponConfigJson.value = true;
+    if (res.data.giftConfigJson) isCheckedGiftConfigJson.value = true;
+  }
+}
+
+async function handleAdd(type: string) {
+  if (type === 'A') {
+    // 添加新的优惠券配置项
+    model.value.couponConfigJson.push({
+      couponId: null,
+      count: null
+    });
+  } else {
+    current.value = type;
+    openModal();
+    await nextTick();
+    setFieldsValue({
+      channelNo: channelOptions.value[0]?.value
+    });
+
+    await nextTick();
+    // setTimeout(() => {
+    //   refresh();
+    // }, 1000);
+    fetchRefresh();
+    if (model.value.giftConfigJson.length) {
+      const selectedKeys = model.value.giftConfigJson.map((item: any) => item.skuId);
+      setTableCheckedRowKeys(selectedKeys);
+    }
+  }
+}
+// 确保商店已选择再刷新
+function fetchRefresh() {
+  setTimeout(() => {
+    if (getSeachForm().shopId) {
+      refresh();
+    } else {
+      fetchRefresh();
+    }
+  }, 500);
+}
+
+/**
+ * 商品项去重:根据 skuId + shopId 唯一判断
+ * @param {Array} list 原始数组(你的结构)
+ * @returns {Array} 去重后的新数组
+ */
+function uniqueGoodsList(list: any[]) {
+  // 用 Map 存储唯一键:skuId_shopId
+  const map = new Map();
+
+  // 遍历数组,保留【最后一次】出现的项
+  list.forEach((item: any) => {
+    // 组合唯一标识
+    const key = `${item.skuId}_${item.shopId}`;
+    map.set(key, item);
+  });
+
+  // 转成数组返回
+  return Array.from(map.values());
+}
+function choose() {
+  const keys = getTableCheckedRowKeys();
+  const data = getTableData();
+
+  // 找出当前页选中的商品完整数据
+  const selectedItems = data.filter((item: any) => keys.includes(item.skuId));
+
+  // 当前对话框搜索表单中选定的渠道 ID(行数据无单独 channelId 字段,以此作为渠道依据)
+  const currentChannelId: number | undefined = currentChannel.value?.id;
+
+  // 校验1:当前选中的商品必须来自同一门店
+  if (selectedItems.length > 1) {
+    const firstShopId = selectedItems[0].shopId;
+    if (!selectedItems.every((item: any) => item.shopId === firstShopId)) {
+      window.$message?.error('只能选择同一渠道、同一门店的商品,请重新选择');
+      return;
+    }
+  }
+
+  // 校验2:新选商品与已配置赠品必须同一门店、同一渠道
+  if (selectedItems.length > 0 && model.value.giftConfigJson.length > 0) {
+    const firstSelectedShopId = selectedItems[0]?.shopId;
+    // 取已配置中不在本次选中范围内的项作为基准
+    const existingItem = model.value.giftConfigJson.find(
+      (g: any) => !selectedItems.some((s: any) => s.skuId === g.skuId)
+    );
+    if (existingItem) {
+      // 门店不一致
+      if (existingItem.shopId !== firstSelectedShopId) {
+        window.$message?.error('只能选择同一渠道、同一门店的商品,请重新选择');
+        return;
+      }
+      // 渠道不一致:已有配置项的 channelId 与当前选择的渠道不同
+      if (currentChannelId && existingItem.channelId && existingItem.channelId !== currentChannelId) {
+        window.$message?.error('只能选择同一渠道、同一门店的商品,请重新选择');
+        return;
+      }
+    }
+  }
+
+  closeModal();
+
+  selectedItems.forEach(item => {
+    model.value.giftConfigJson.push({
+      skuId: item.skuId,
+      quantity: 1,
+      shopId: item.shopId,
+      channelId: currentChannelId ?? 1, // 使用搜索表单中实际选择的渠道,而非硬编码 1
+      skuName: item.skuName,
+      skuCode: item.skuCode,
+      specName: '-',
+      pic: item.pic,
+      stock: item.shopSkuStocks
+    });
+    model.value.giftConfigJson = uniqueGoodsList(model.value.giftConfigJson);
+  });
+
+  console.log(keys, '选择的商品', data);
+}
+
+function handleDelete(type: string, index: number) {
+  if (type === 'A') {
+    if (model.value.couponConfigJson.length == 1) {
+      window.$message?.error('请至少保留一个优惠券配置项');
+      return;
+    }
+    // 删除优惠券配置项
+    model.value.couponConfigJson.splice(index, 1);
+  } else {
+    model.value.giftConfigJson.splice(index, 1);
+  }
+}
+
+function handleCheckedChange(checked: boolean) {
+  if (checked) {
+    model.value.stock = -1; // 不限制库存时,设置stock为-1
+  } else {
+    model.value.stock = 0; // 恢复默认库存值
+  }
+}
+
+function handleSearch() {
+  console.log('handleSearch');
+  if (selectedChannelForSearch.value) {
+    currentChannel.value = selectedChannelForSearch.value;
+  }
+}
+
+onMounted(() => {
+  if (route.query.id) {
+    if (route.query.mode === 'detail') {
+      disabled.value = true;
+    }
+    getDetail();
+  }
+  console.log(route.query);
+});
+</script>
+
+<template>
+  <div class="edit-health-goods pl-20px pt-20px">
+    <NForm
+      ref="formRef"
+      :model="model"
+      :rules="rules"
+      label-placement="left"
+      label-width="100px"
+      require-mark-placement="right-hanging"
+      size="medium"
+      class="max-w-840px"
+      :disabled="disabled"
+    >
+      <div class="font-600">基本信息</div>
+      <NFormItem label="会员名称" path="memberName">
+        <div class="w-100%">
+          <NInput v-model:value="model.memberName" :maxlength="20" placeholder="" />
+          <div class="text-12px">最多支持20个汉字;</div>
+        </div>
+      </NFormItem>
+      <NFormItem label="主图" path="coverImg">
+        <ZUpload v-model:value="model.coverImg" :max="1"></ZUpload>
+      </NFormItem>
+      <NFormItem label="面额(元)" path="faceAmount">
+        <div class="w-100%">
+          <NInputNumber v-model:value="model.faceAmount" :precision="2" />
+          <div class="text-12px">数值范围0-100000,保留两位小数;</div>
+        </div>
+      </NFormItem>
+      <NFormItem label="库存" path="stock">
+        <div class="w-100%">
+          <NInputNumber v-model:value="model.stock" :min="1" :disabled="isCheckedStock" :max="100000" :precision="0" />
+          <NCheckbox class="ml-6px" :checked="isCheckedStock" @update:checked="handleCheckedChange">不限制</NCheckbox>
+          <div class="text-12px">整数,数值范围1-100000</div>
+        </div>
+      </NFormItem>
+
+      <NFormItem label="结算价(元)" path="settleAmount">
+        <div class="w-100%">
+          <NInputNumber v-model:value="model.settleAmount" :min="0" :max="model.faceAmount || 100000" :precision="2" />
+          <div class="text-12px">需小于等于面额金额。数值范围0-100000,保留两位小数;</div>
+        </div>
+      </NFormItem>
+      <NFormItem label="充值类型" path="rechargeType">
+        <NSelect v-model:value="model.rechargeType" placeholder="" :options="rechargeOptions" />
+      </NFormItem>
+      <NFormItem label="品牌" path="brandName">
+        <NInput v-model:value="model.brandName" placeholder="" />
+      </NFormItem>
+      <div class="font-600">权益配置</div>
+      <div class="my-6px text-12px">全省中石化加油优惠</div>
+      <div class="mb-20px border rounded-16px border-dashed pt-24px">
+        <NFormItem
+          label="每升立减"
+          path="benefitConfigJson.oilPerLiterDiscount"
+          :rule="
+            isCheckedOilPerLiterDiscount ? [{ type: 'number', required: true, message: '请输入', trigger: 'blur' }] : []
+          "
+        >
+          <div>
+            <div class="flex items-center">
+              <NInputNumber
+                v-model:value="model.benefitConfigJson.oilPerLiterDiscount"
+                :disabled="!isCheckedOilPerLiterDiscount"
+                :precision="2"
+                :min="0"
+                :max="50"
+              />
+              元
+              <div class="ml-10px text-12px">(保留2位小数,范围0-50)</div>
+            </div>
+          </div>
+        </NFormItem>
+        <NFormItem label="图标" path="benefitConfigJson.oilIcon">
+          <ZUpload
+            v-model:value="model.benefitConfigJson.oilIcon"
+            :max="1"
+            :disabled="!isCheckedOilPerLiterDiscount"
+          ></ZUpload>
+        </NFormItem>
+        <NFormItem label="路由配置" path="benefitConfigJson.oilRoutePath">
+          <NInput
+            v-model:value="model.benefitConfigJson.oilRoutePath"
+            :disabled="!isCheckedOilPerLiterDiscount"
+            placeholder="请输入路由路径,如:/pages/oil/index"
+          />
+        </NFormItem>
+        <div class="mt-6px">
+          <NCheckbox v-model:checked="isCheckedOilPerLiterDiscount" class="ml-6px">启用该权益</NCheckbox>
+        </div>
+      </div>
+
+      <div class="my-6px text-12px">市民请集合平台快消品</div>
+      <div class="mb-20px border rounded-16px border-dashed pt-24px">
+        <NFormItem
+          label="折扣率"
+          path="benefitConfigJson.mallDiscountRate"
+          :rule="
+            isCheckedMallDiscountRate ? [{ type: 'number', required: true, message: '请输入', trigger: 'blur' }] : []
+          "
+        >
+          <div>
+            <div class="flex items-center">
+              <NInputNumber
+                v-model:value="model.benefitConfigJson.mallDiscountRate"
+                :disabled="!isCheckedMallDiscountRate"
+                :precision="0"
+                :min="0"
+                :max="100"
+              />
+              %
+              <div class="ml-10px text-12px">(输入95表示9.5折,范围0-100)</div>
+            </div>
+          </div>
+        </NFormItem>
+        <NFormItem label="图标" path="benefitConfigJson.mallIcon">
+          <ZUpload
+            v-model:value="model.benefitConfigJson.mallIcon"
+            :max="1"
+            :disabled="!isCheckedMallDiscountRate"
+          ></ZUpload>
+        </NFormItem>
+        <NFormItem label="路由配置" path="benefitConfigJson.mallRoutePath">
+          <NInput
+            v-model:value="model.benefitConfigJson.mallRoutePath"
+            :disabled="!isCheckedMallDiscountRate"
+            placeholder="请输入路由路径,如:/pages/mall/index"
+          />
+        </NFormItem>
+        <div class="mt-6px">
+          <NCheckbox v-model:checked="isCheckedMallDiscountRate" class="ml-6px">启用该权益</NCheckbox>
+        </div>
+      </div>
+
+      <div class="my-6px text-12px">贵阳城投充电桩优惠</div>
+      <div class="mb-20px border rounded-16px border-dashed pt-24px">
+        <NFormItem
+          label="每度电立减"
+          path="benefitConfigJson.chargePerKwhDiscount"
+          :rule="
+            isCheckedChargePerKwhDiscount
+              ? [{ type: 'number', required: true, message: '请输入', trigger: 'blur' }]
+              : []
+          "
+        >
+          <div>
+            <div class="flex items-center">
+              <NInputNumber
+                v-model:value="model.benefitConfigJson.chargePerKwhDiscount"
+                :disabled="!isCheckedChargePerKwhDiscount"
+                :precision="2"
+                :min="0"
+                :max="50"
+              />
+              元
+              <div class="ml-10px text-12px">(保留2位小数,范围0-50)</div>
+            </div>
+          </div>
+        </NFormItem>
+        <NFormItem label="图标" path="benefitConfigJson.chargeIcon">
+          <ZUpload
+            v-model:value="model.benefitConfigJson.chargeIcon"
+            :max="1"
+            :disabled="!isCheckedChargePerKwhDiscount"
+          ></ZUpload>
+        </NFormItem>
+        <NFormItem label="路由配置" path="benefitConfigJson.chargeRoutePath">
+          <NInput
+            v-model:value="model.benefitConfigJson.chargeRoutePath"
+            :disabled="!isCheckedChargePerKwhDiscount"
+            placeholder="请输入路由路径,如:/pages/charge/index"
+          />
+        </NFormItem>
+        <div class="mt-6px">
+          <NCheckbox v-model:checked="isCheckedChargePerKwhDiscount" class="ml-6px">启用该权益</NCheckbox>
+        </div>
+      </div>
+
+      <div class="my-6px text-12px">贵阳城投停车位停车优惠</div>
+      <div class="mb-20px border rounded-16px border-dashed pt-24px">
+        <NFormItem
+          label="折扣率"
+          path="benefitConfigJson.parkingDiscountRate"
+          :rule="
+            isCheckedParkingDiscountRate ? [{ type: 'number', required: true, message: '请输入', trigger: 'blur' }] : []
+          "
+        >
+          <div>
+            <div class="flex items-center">
+              <NInputNumber
+                v-model:value="model.benefitConfigJson.parkingDiscountRate"
+                :disabled="!isCheckedParkingDiscountRate"
+                :precision="0"
+                :min="0"
+                :max="100"
+              />
+              %
+              <div class="ml-10px text-12px">(输入95表示9.5折,范围0-100)</div>
+            </div>
+          </div>
+        </NFormItem>
+        <NFormItem label="图标" path="benefitConfigJson.parkingIcon">
+          <ZUpload
+            v-model:value="model.benefitConfigJson.parkingIcon"
+            :max="1"
+            :disabled="!isCheckedParkingDiscountRate"
+          ></ZUpload>
+        </NFormItem>
+        <NFormItem label="路由配置" path="benefitConfigJson.parkingRoutePath">
+          <NInput
+            v-model:value="model.benefitConfigJson.parkingRoutePath"
+            :disabled="!isCheckedParkingDiscountRate"
+            placeholder="请输入路由路径,如:/pages/parking/index"
+          />
+        </NFormItem>
+        <div class="mt-6px">
+          <NCheckbox v-model:checked="isCheckedParkingDiscountRate" class="ml-6px">启用该权益</NCheckbox>
+        </div>
+      </div>
+
+      <div class="my-6px text-12px">市民请集合平台优惠券</div>
+      <div class="mb-20px border rounded-16px border-dashed pt-24px">
+        <NButton class="mb-20px ml-30px" icon-placement="left" secondary strong @click="handleAdd('A')">
+          <template #icon>
+            <icon-material-symbols:add-circle class="text-icon" />
+          </template>
+          添加优惠券
+        </NButton>
+        <div v-for="(item, index) in model.couponConfigJson" :key="index" class="flex">
+          <NFormItem
+            label="优惠券"
+            :path="`couponConfigJson[${index}].couponId`"
+            :rule="isCheckedCouponConfigJson ? [{ required: true, message: '请选择优惠券', trigger: 'change' }] : []"
+          >
+            <!-- <NSelect v-model:value="item.couponId" class="min-w-170px" placeholder="" :options="options" /> -->
+            <ApiSelect
+              v-model:value="item.couponId"
+              class="min-w-170px"
+              :api="fetchGetActivityList"
+              result-feild="data.list"
+              label-field="activityName"
+              value-field="id"
+            ></ApiSelect>
+          </NFormItem>
+
+          <NFormItem
+            label="发放张数"
+            :path="`couponConfigJson[${index}].count`"
+            :rule="
+              isCheckedCouponConfigJson
+                ? [{ required: true, message: '请选择优惠券', trigger: 'change', type: 'number' }]
+                : []
+            "
+          >
+            <NInputNumber v-model:value="item.count" :precision="0" :min="0" :max="100000" />
+            张
+            <div class="ml-10px text-12px">(整数,范围0-100000)</div>
+          </NFormItem>
+          <NButton
+            class="mb-20px ml-30px"
+            icon-placement="left"
+            secondary
+            strong
+            @click="handleDelete('A', index as number)"
+          >
+            <template #icon>
+              <icon-icon-park-solid:reduce-one class="text-icon" />
+            </template>
+          </NButton>
+        </div>
+
+        <NFormItem label="图标" path="couponIcon">
+          <ZUpload v-model:value="model.couponIcon" :max="1" :disabled="!isCheckedCouponConfigJson"></ZUpload>
+        </NFormItem>
+        <NFormItem label="路由配置" path="couponRoutePath">
+          <NInput
+            v-model:value="model.couponRoutePath"
+            :disabled="!isCheckedCouponConfigJson"
+            placeholder="请输入路由路径,如:/pages/coupon/index"
+          />
+        </NFormItem>
+        <div class="mt-6px">
+          <NCheckbox v-model:checked="isCheckedCouponConfigJson" class="ml-100px">启用该权益</NCheckbox>
+        </div>
+      </div>
+
+      <div class="my-6px text-12px">自选赠品</div>
+      <div class="mb-20px border rounded-16px border-dashed pt-24px">
+        <NButton class="mb-20px ml-30px" icon-placement="left" secondary strong @click="handleAdd('B')">
+          <template #icon>
+            <icon-material-symbols:add-circle class="text-icon" />
+          </template>
+          添加商品
+        </NButton>
+        <NFormItem
+          path="giftConfigJson"
+          class="ml-60px"
+          :rule="
+            isCheckedGiftConfigJson
+              ? [{ required: true, type: 'array', min: 1, message: '请选择商品', trigger: 'change' }]
+              : []
+          "
+        >
+          <div class="flex flex-wrap">
+            <div
+              v-for="(item, index) in model.giftConfigJson"
+              :key="index"
+              class="relative mb-20px mr-20px flex border-1px rounded-8px p-10px pr-40px"
+            >
+              <NImage :src="item.pic" class="h90px w90px"></NImage>
+              <div class="ml-10px flex-1 text-12px">
+                <div class="w-200px overflow-hidden text-ellipsis text-nowrap">{{ item.skuName }}</div>
+                <div>商品编码:{{ item.skuCode }}</div>
+                <div>规格:{{ item.specName }}</div>
+                <div>库存:{{ item.stock }}</div>
+                <div class="flex items-center">
+                  赠送数量:
+                  <NInputNumber v-model:value="item.quantity" class="w-100px" :precision="0" :min="1" :max="100" />
+                </div>
+              </div>
+              <NButton
+                class="absolute bottom-10px right-10px text-12px"
+                text
+                @click="handleDelete('B', index as number)"
+              >
+                删除
+              </NButton>
+            </div>
+          </div>
+        </NFormItem>
+
+        <NFormItem label="图标" path="giftIcon">
+          <ZUpload v-model:value="model.giftIcon" :max="1" :disabled="!isCheckedGiftConfigJson"></ZUpload>
+        </NFormItem>
+        <NFormItem label="路由配置" path="giftRoutePath">
+          <NInput
+            v-model:value="model.giftRoutePath"
+            :disabled="!isCheckedGiftConfigJson"
+            placeholder="请输入路由路径,如:/pages/gift/index"
+          />
+        </NFormItem>
+        <div class="mt-6px">
+          <NCheckbox v-model:checked="isCheckedGiftConfigJson" class="ml-100px">启用该权益</NCheckbox>
+        </div>
+      </div>
+
+      <div class="font-600">其他条件</div>
+
+      <NFormItem label="开发渠道" path="openChannelCodes">
+        <DictSelect v-model:value="model.openChannelCodes" dict-code="open_channel" :immediate="true"></DictSelect>
+      </NFormItem>
+
+      <NFormItem label="价格体系" path="priceChannelId">
+        <div>
+          <ApiSelect
+            v-model:value="model.priceChannelId"
+            :api="fetchFirstChannel"
+            label-field="channelName"
+            value-field="id"
+          ></ApiSelect>
+          <div class="mt-10px text-12px">用哪个企业的价格体系来进行计算;</div>
+        </div>
+      </NFormItem>
+
+      <NFormItem label="使用有效期" path="useValidDays">
+        领取之日起
+        <NInputNumber v-model:value="model.useValidDays" />
+        日有效
+      </NFormItem>
+
+      <NFormItem label="取消规则" path="cancelRule">
+        <div>
+          <NSelect v-model:value="model.cancelRule" placeholder="" :options="cancelOptions" />
+          <div class="mt-10px text-12px">次月生效:取消成功后,下个自然月生效;立即生效,取消成功后,立即生效;</div>
+        </div>
+      </NFormItem>
+
+      <NFormItem label="使用规则" path="useRule">
+        <NInput v-model:value="model.useRule" type="textarea" show-count :maxlength="500" placeholder="" />
+      </NFormItem>
+      <NFormItem label="状态">
+        <NSwitch v-model:value="model.status" :unchecked-value="0" :checked-value="1" />
+      </NFormItem>
+      <div v-if="!disabled" class="flex justify-end">
+        <NButton round type="primary" :loading="loading" @click="handleValidateButtonClick">保存</NButton>
+      </div>
+    </NForm>
+
+    <BasicModal @register="registerModal" @ok="choose">
+      <LayoutTable>
+        <ZTable
+          :immediate="false"
+          :show-table-action="false"
+          :columns="columns"
+          :api="fetchGoodsList"
+          :default-params="{ channelCode: 'XSB' }"
+          @register="registerModalTable"
+          @search="handleSearch"
+        ></ZTable>
+      </LayoutTable>
+    </BasicModal>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+::v-deep .n-form-item-blank {
+  z-index: 6 !important;
+}
+.edit-health-goods {
+  background: #fff;
+}
+</style>

+ 357 - 0
src/views/member-center/member-list/index.vue

@@ -0,0 +1,357 @@
+<script setup lang="tsx">
+import { computed, nextTick, ref } from 'vue';
+import { NButton, NTag } from 'naive-ui';
+import {
+  fetchExport,
+  fetchExportRecord,
+  fetchMemberList,
+  fetchOpenRecord
+} from '@/service/api/member-center/member-list';
+import { commonExport } from '@/utils/common';
+import SvgIcon from '@/components/custom/svg-icon.vue';
+// import dayjs from 'dayjs';
+import { useTable } from '@/components/zt/Table/hooks/useTable';
+import { useModal } from '@/components/zt/Modal/hooks/useModal';
+const mobile = ref('');
+const columns: NaiveUI.TableColumn<Api.goods.ShopSku>[] = [
+  {
+    key: 'mobile',
+    title: '手机号',
+    align: 'center',
+    width: 100
+  },
+  {
+    key: 'status',
+    title: '当前状态',
+    align: 'center',
+    width: 100,
+    render: row => {
+      return <NTag class={'mt7'}>{row.status == 1 ? '生效中' : '已失效'}</NTag>;
+    }
+  },
+  {
+    key: 'expireTime',
+    title: '有效期至',
+    align: 'center',
+    width: 100
+  },
+  {
+    key: 'openTimes',
+    title: '开通次数',
+    align: 'center',
+    width: 100
+  },
+  {
+    key: 'effectiveTime',
+    title: '最近开通时间',
+    align: 'center',
+    width: 100
+  },
+  {
+    key: 'openSource',
+    title: '来源',
+    align: 'center',
+    width: 100,
+    render: row => {
+      return <dictTag value={row.openSource} dictCode="open_channel" immediate={true}></dictTag>;
+    }
+  }
+];
+
+const [registerTable, { getTableData }] = useTable({
+  searchFormConfig: {
+    schemas: [
+      {
+        label: '手机号',
+        component: 'NInput',
+        field: 'mobile'
+      },
+      {
+        label: '会员状态',
+        field: 'status',
+        component: 'NSelect',
+        componentProps: {
+          options: [
+            {
+              label: '生效中',
+              value: 1
+            },
+            {
+              label: '已失效',
+              value: 2
+            }
+          ]
+        }
+      },
+      {
+        label: '开放渠道',
+        component: 'dictSelect',
+        field: 'openSource',
+        componentProps: {
+          dictCode: 'open_channel',
+          immediate: true
+        }
+      },
+      {
+        label: '最近开通时间',
+        component: 'NDatePicker',
+        field: 'createTime',
+        componentProps: {
+          type: 'datetimerange',
+          defaultTime: ['00:00:00', '23:59:59']
+        }
+      }
+    ],
+    inline: false,
+    size: 'small',
+    labelPlacement: 'left',
+    isFull: false
+  },
+  tableConfig: {
+    opWdith: 160,
+    keyField: 'skuId',
+    title: '用户列表',
+    showAddButton: false,
+    scrollX: 1800,
+    fieldMapToTime: [['createTime', ['effectiveTimeStart', 'effectiveTimeEnd']]]
+  }
+});
+
+const failColumns: NaiveUI.TableColumn<Api.government.PointsFailureRecordVO>[] = [
+  {
+    key: 'index',
+    title: '序号',
+    align: 'center',
+    width: 80,
+    render(_, rowIndex) {
+      return rowIndex + 1;
+    }
+  },
+  {
+    key: 'taskName',
+    title: '任务名称',
+    align: 'center'
+  },
+  {
+    key: 'createTime',
+    title: '时间',
+    align: 'center',
+    width: 300,
+    render(row) {
+      return (
+        <div>
+          <div>创建时间:{row.createTime}</div>
+          <div>完成时间:{row.updateTime}</div>
+        </div>
+      );
+    }
+  },
+  {
+    key: 'operatorName',
+    title: '操作人',
+    align: 'center',
+    width: 120
+  },
+  {
+    key: 'successStatus',
+    title: '状态',
+    align: 'center',
+    width: 240,
+    render(row) {
+      const statusList = ['已创建', '进行中', '导出完成', '导出失败'];
+      return (
+        <div class={'flex items-center'}>
+          <span class={'mr-2'}>{statusList[row.status]}</span>共{Number(row.totalCount)}条,
+          <span class={'flex items-center'}>
+            {row.status == 2 && (
+              <div onClick={() => hanleExport(row.id)}>
+                <SvgIcon icon={'tdesign:download'} class={'ml-1 cursor-pointer text-20px'}></SvgIcon>
+              </div>
+            )}
+          </span>
+        </div>
+      );
+    }
+  }
+];
+const openColumns: NaiveUI.TableColumn<Api.government.PointsFailureRecordVO>[] = [
+  {
+    key: 'index',
+    title: '序号',
+    align: 'center',
+    width: 80,
+    render(_, rowIndex) {
+      return rowIndex + 1;
+    }
+  },
+  {
+    key: 'effectiveTime',
+    title: '激活时间',
+    align: 'center'
+  },
+  {
+    key: 'expireTime',
+    title: '到期时间',
+    align: 'center'
+  },
+  {
+    key: 'statusAfterAction',
+    title: '状态',
+    align: 'center',
+    render: row => {
+      // 操作后状态:1 生效中,2 已取消/已失效。
+      return row.statusAfterAction === 1 ? '生效中' : '已取消/已失效';
+    }
+  },
+  {
+    key: 'source',
+    title: '来源',
+    align: 'center',
+    render: row => {
+      return <dictTag value={row.source} dictCode="open_channel" immediate={true}></dictTag>;
+    }
+  }
+];
+
+const [registerLogTable, { refresh: refreshLog, setTableLoading: setLogTableLoading }] = useTable({
+  tableConfig: {
+    keyField: 'skuId',
+    title: '',
+    minHeight: 200,
+    showAddButton: false,
+    showSearch: false
+  }
+});
+
+const [registerOpenTable, { refresh: refreshOpen }] = useTable({
+  tableConfig: {
+    keyField: 'skuId',
+    title: '',
+    minHeight: 200,
+    showAddButton: false,
+    showSearch: false
+  }
+});
+
+const [registerModalFail, { openModal: openModalFail }] = useModal({
+  title: '导出记录',
+  height: 400,
+  width: 1200,
+  showFooter: false
+});
+const [registerOpenModal, { openModal }] = useModal({
+  title: `用户开通记录`,
+  height: 400,
+  width: 1200,
+  showFooter: false
+});
+
+const loading = ref(false);
+
+const tableData = computed(() => {
+  return getTableData();
+});
+
+async function handleExport() {
+  try {
+    const res = await fetchExport({});
+    console.log(res);
+
+    // if (!res.error) {
+    //   importTemplateRef.value?.closeModal();
+    // }
+    // window.$message?.success(res.data);
+    window.$dialog?.success({
+      title: '提示',
+      content: () => {
+        return (
+          <div>
+            <p>导入操作进行中......</p>
+            <p>是否进入导入记录</p>
+          </div>
+        );
+      },
+      positiveText: '确定',
+      negativeText: '取消',
+      onPositiveClick: async () => {
+        openExportLogModal();
+      },
+      onNegativeClick: () => {}
+    });
+  } finally {
+    console.log('导出失败');
+  }
+}
+
+async function hanleExport(code: string) {
+  if (loading.value) {
+    window.$message?.error('正在导出,请勿重复点击');
+    return;
+  }
+  loading.value = true;
+  setLogTableLoading(true);
+  try {
+    await commonExport(`/smqjh-system/api/v1/member/export/task/download/${code}`, {}, '导出记录.xlsx');
+  } finally {
+    loading.value = false;
+    setLogTableLoading(false);
+  }
+}
+
+async function openExportLogModal() {
+  openModalFail();
+  await nextTick();
+
+  refreshLog();
+}
+
+async function openRecordModal(id: string) {
+  openModal();
+  mobile.value = id;
+  await nextTick();
+  refreshOpen();
+}
+</script>
+
+<template>
+  <LayoutTable>
+    <ZTable :columns="columns" :api="fetchMemberList" @register="registerTable">
+      <template #prefix="{ loading }">
+        <NSpace>
+          <NButton size="small" :disabled="tableData.length == 0" :loading="loading" @click="handleExport">
+            导出
+          </NButton>
+          <NButton size="small" @click="openExportLogModal">导出记录</NButton>
+        </NSpace>
+      </template>
+      <template #op="{ row }">
+        <NButton size="small" @click="openRecordModal(row.mobile)">开通记录</NButton>
+      </template>
+    </ZTable>
+
+    <!-- 导出记录 -->
+    <BasicModal @register="registerModalFail">
+      <ZTable
+        :columns="failColumns"
+        :immediate="false"
+        :show-table-action="false"
+        :api="fetchExportRecord"
+        @register="registerLogTable"
+      ></ZTable>
+    </BasicModal>
+
+    <!-- 记录 -->
+    <BasicModal @register="registerOpenModal">
+      <ZTable
+        :columns="openColumns"
+        :immediate="false"
+        :show-table-action="false"
+        :api="fetchOpenRecord"
+        :default-params="{ mobile }"
+        @register="registerOpenTable"
+      ></ZTable>
+    </BasicModal>
+  </LayoutTable>
+</template>
+
+<style scoped></style>

+ 250 - 0
src/views/member-center/member-order/index.vue

@@ -0,0 +1,250 @@
+<script setup lang="tsx">
+import { ref } from 'vue';
+import { NButton } from 'naive-ui';
+// import dayjs from 'dayjs';
+import { fetchOrderDetail, fetchOrderList } from '@/service/api/member-center/member-order';
+import { useTable } from '@/components/zt/Table/hooks/useTable';
+import { useModal } from '@/components/zt/Modal/hooks/useModal';
+
+const orderInfo = ref({
+  id: 0,
+  memberOrderId: 0,
+  memberOpenRecordId: 0,
+  memberAccountId: 0,
+  orderNumber: 'string',
+  thirdOrderNo: 'string',
+  memberId: 0,
+  mobile: 'string',
+  memberTypeId: 0,
+  memberTypeName: 'string',
+  businessType: 'string',
+  orderAmount: 0,
+  rechargePrice: 0,
+  faceAmount: 0,
+  settleAmount: 0,
+  orderStatus: 0,
+  orderStatusName: 'string',
+  status: 0,
+  createTime: '2019-08-24T14:15:22.123Z',
+  activateTime: '2019-08-24T14:15:22.123Z',
+  effectiveTime: '2019-08-24T14:15:22.123Z',
+  expireTime: '2019-08-24T14:15:22.123Z',
+  openSource: 'string',
+  thirdPlatform: 'string',
+  openTimes: 0,
+  updateTime: '2019-08-24T14:15:22.123Z',
+  lastCancelReason: 'string',
+  actionType: 0,
+  actionTypeName: 'string',
+  remark: 'string',
+  benefitRecords: [] as Array<any>
+});
+const types: Record<string, string> = {
+  COUPON: '优惠券',
+  GIFT: '自选赠品'
+};
+const columns: NaiveUI.TableColumn<Api.goods.ShopSku>[] = [
+  {
+    key: 'orderNumber',
+    title: '订单号',
+    align: 'center'
+  },
+  {
+    key: 'mobile',
+    title: '手机号',
+    align: 'center'
+  },
+  {
+    key: 'memberTypeName',
+    title: '会员信息',
+    align: 'center'
+  },
+  {
+    key: 'orderAmount',
+    title: '订单金额(元)',
+    align: 'center'
+  },
+  {
+    key: 'orderStatusName',
+    title: '订单状态',
+    align: 'center'
+    // render: row => {
+    //   const orderStatus = {
+    //     '1': '生效中',
+    //     '2': '已取消',
+    //     '3': '已过期'
+    //   };
+    //   return <NTag class={'mt7'}>{orderStatus[row.status as keyof typeof orderStatus]}</NTag>;
+    // }
+  },
+  {
+    key: 'createTime',
+    title: '创建时间',
+    align: 'center',
+    width: 100
+  },
+  {
+    key: 'openSource',
+    title: '开通来源',
+    align: 'center',
+    render: row => {
+      return <dictTag value={row.openSource} dictCode="open_channel" immediate={true}></dictTag>;
+    }
+  },
+  {
+    key: 'activateTime',
+    title: '激活时间',
+    align: 'center',
+    width: 100
+  }
+];
+
+const [registerTable] = useTable({
+  searchFormConfig: {
+    schemas: [
+      {
+        label: '订单号',
+        component: 'NInput',
+        field: 'orderNumber'
+      },
+      {
+        label: '手机号',
+        component: 'NInput',
+        field: 'mobile'
+      },
+      {
+        label: '订单状态',
+        field: 'status',
+        component: 'NSelect',
+        componentProps: {
+          options: [
+            {
+              label: '生效中',
+              value: 1
+            },
+            {
+              label: '已取消',
+              value: 2
+            },
+            {
+              label: '已过期',
+              value: 3
+            }
+          ]
+        }
+      },
+      {
+        label: '开放渠道',
+        component: 'dictSelect',
+        field: 'openSource',
+        componentProps: {
+          dictCode: 'open_channel',
+          immediate: true
+        }
+      },
+      {
+        label: '会员类型ID',
+        component: 'NInput',
+        field: 'memberTypeId'
+      },
+      {
+        label: '会员名称',
+        component: 'NInput',
+        field: 'memberTypeName'
+      },
+
+      {
+        label: '创建时间',
+        component: 'NDatePicker',
+        field: 'createTime',
+        componentProps: {
+          type: 'datetimerange',
+          defaultTime: ['00:00:00', '23:59:59']
+        }
+      }
+    ],
+    inline: false,
+    size: 'small',
+    labelPlacement: 'left',
+    isFull: false
+  },
+  tableConfig: {
+    opWdith: 160,
+    keyField: 'skuId',
+    title: '活动列表',
+    showAddButton: false,
+    scrollX: 1800,
+    fieldMapToTime: [['createTime', ['createTimeStart', 'createTimeEnd']]]
+  }
+});
+
+const [registerModal, { openModal }] = useModal({
+  title: `订单详情`,
+  height: 600,
+  width: 1200,
+  showFooter: false
+});
+
+async function getDetail(id: any) {
+  const res = await fetchOrderDetail(id);
+  orderInfo.value = res.data;
+}
+
+function handleDetail(id: any) {
+  openModal();
+  getDetail(id);
+}
+</script>
+
+<template>
+  <LayoutTable>
+    <ZTable :columns="columns" :api="fetchOrderList" @register="registerTable">
+      <template #op="{ row }">
+        <NButton size="small" @click="handleDetail(row.id)">查看详情</NButton>
+      </template>
+    </ZTable>
+
+    <BasicModal @register="registerModal">
+      <div class="py-20px font-semibold">基本信息</div>
+
+      <div class="flex">
+        <div class="flex-1">
+          <div>订单号:{{ orderInfo.orderNumber }}</div>
+          <div>手机号:{{ orderInfo.mobile }}</div>
+          <div>订单金额:{{ orderInfo.orderAmount }}</div>
+          <div>合作方订单号:{{ orderInfo.thirdOrderNo }}</div>
+          <div>激活时间:{{ orderInfo.activateTime }}</div>
+        </div>
+        <div class="flex-1">
+          <div>状态:{{ orderInfo.orderStatusName }}</div>
+          <div>用户ID:{{ orderInfo.memberAccountId }}</div>
+          <div>开通来源:{{ orderInfo.openSource }}</div>
+          <div>创建时间:{{ orderInfo.createTime }}</div>
+          <div>到期时间:{{ orderInfo.expireTime }}</div>
+        </div>
+      </div>
+
+      <div class="py-20px font-semibold">权益发放记录</div>
+      <NTable class="" :single-line="false">
+        <NThead>
+          <NTr>
+            <NTh>权益类型</NTh>
+            <NTh>发放时间</NTh>
+            <NTh>发放结果</NTh>
+            <NTh>关联业务单号</NTh>
+          </NTr>
+        </NThead>
+        <NTbody>
+          <NTr v-for="(item, index) in orderInfo.benefitRecords" :key="index">
+            <NTd>{{ types[item.benefitType] }}</NTd>
+            <NTd>{{ item.createTime }}</NTd>
+            <NTd>{{ item.grantStatus == 1 ? '成功' : '失败' }}</NTd>
+            <NTd>{{ item.businessNo }}</NTd>
+          </NTr>
+        </NTbody>
+      </NTable>
+    </BasicModal>
+  </LayoutTable>
+</template>
+
+<style scoped></style>

+ 235 - 0
src/views/member-center/member-type/index.vue

@@ -0,0 +1,235 @@
+<script setup lang="tsx">
+import { useRouter } from 'vue-router';
+import { NButton, NImage, NSwitch } from 'naive-ui';
+// import dayjs from 'dayjs';
+import { fetchDelType, fetchTypeList, fetchUpdateStatus } from '@/service/api/member-center/member-type';
+import { useTable } from '@/components/zt/Table/hooks/useTable';
+const router = useRouter();
+
+const columns: NaiveUI.TableColumn<Api.goods.ShopSku>[] = [
+  {
+    key: 'id',
+    title: 'ID',
+    align: 'center',
+    width: 100
+  },
+  {
+    key: 'memberName',
+    title: '会员名称',
+    align: 'center',
+    width: 100
+  },
+  {
+    key: 'openChannelCodes',
+    title: '开放渠道',
+    align: 'center',
+    width: 100,
+    render: row => {
+      return <dictTag value={row.openChannelCodes} dictCode="open_channel" immediate={true}></dictTag>;
+    }
+  },
+  {
+    key: 'status',
+    title: '状态',
+    align: 'center',
+    width: 100,
+    render: row => {
+      return (
+        <NSwitch
+          uncheckedValue={0}
+          checkedValue={1}
+          value={row.status}
+          onUpdate:value={val => {
+            row.status = val;
+            fetchUpdateStatus(row.id, row.status);
+          }}
+        ></NSwitch>
+      );
+    }
+  },
+
+  {
+    key: 'coverImg',
+    title: '活动封面',
+    align: 'center',
+    width: 100,
+    render: (row: any) => {
+      return <NImage src={row.coverImg} class={'h90px w90px'}></NImage>;
+    }
+  },
+  {
+    key: 'priceChannelId',
+    title: '价格体系',
+    align: 'center',
+    width: 100
+  },
+  {
+    key: 'faceAmount',
+    title: '面额(元)',
+    align: 'center',
+    width: 100
+  },
+  {
+    key: 'stock',
+    title: '库存',
+    align: 'center',
+    width: 100,
+    render: row => {
+      return <div>{row.stock == -1 ? '不限制' : row.stock}</div>;
+    }
+  },
+  {
+    key: 'settleAmount',
+    title: '结算价(元)',
+    align: 'center',
+    width: 100
+  },
+  {
+    key: 'useValidDays',
+    title: '使用有效期',
+    align: 'center',
+    width: 100,
+    render: row => {
+      return <div>领取之日起{row.useValidDays}天有效</div>;
+    }
+  },
+  {
+    key: 'cancelRule',
+    title: '取消规则',
+    align: 'center',
+    width: 100,
+    render: row => {
+      const cancelOptions = {
+        IMMEDIATE: '立即生效',
+        NEXT_MONTH: '次月生效'
+      };
+      return <div> {cancelOptions[row.cancelRule as keyof typeof cancelOptions]}</div>;
+    }
+  },
+
+  {
+    key: 'rightsDesc',
+    title: '权益描述',
+    align: 'center',
+    width: 230,
+    ellipsis: {
+      lineClamp: 2, // 最多显示2行
+      tooltip: {
+        // 控制 tooltip 样式/宽高
+        contentStyle: {
+          maxWidth: '300px' // tooltip 最大宽度
+        },
+        scrollable: true // 内容过长可滚动
+      }
+    }
+  }
+];
+
+const [registerTable, { refresh }] = useTable({
+  searchFormConfig: {
+    schemas: [
+      {
+        label: '会员名称',
+        component: 'NInput',
+        field: 'memberName'
+      },
+      {
+        label: '开放渠道',
+        component: 'dictSelect',
+        field: 'openChannel',
+        componentProps: {
+          dictCode: 'open_channel',
+          immediate: true
+        }
+      },
+      {
+        label: '状态',
+        field: 'status',
+        component: 'NSelect',
+        componentProps: {
+          options: [
+            {
+              label: '下架',
+              value: 0
+            },
+            {
+              label: '上架',
+              value: 1
+            }
+          ]
+        }
+      }
+    ],
+    inline: false,
+    size: 'small',
+    labelPlacement: 'left',
+    isFull: false
+  },
+  tableConfig: {
+    opWdith: 160,
+    keyField: 'skuId',
+    title: '活动列表',
+    showAddButton: true,
+    scrollX: 1800,
+    fieldMapToTime: [
+      ['price', ['minPrice', 'maxPrice']],
+      ['createTime', ['startTime', 'endTime']]
+    ]
+  }
+});
+
+function handleAdd() {
+  router.push({
+    path: '/member-center/edit-member-type'
+  });
+}
+// function handleDetail(row: any) {
+//   router.push({
+//     path: '/member-center/edit-member-type',
+//     query: {
+//       id: row.id,
+//       mode: 'detail'
+//     }
+//   });
+// }
+function handleEdit(row: any) {
+  console.log('edit', row);
+  router.push({
+    path: '/member-center/edit-member-type',
+    query: {
+      id: row.id,
+      mode: 'edit'
+    }
+  });
+}
+
+function handleDelete(row: any) {
+  window.$dialog?.info({
+    title: '提示',
+    content: `你确定要删除吗?`,
+    positiveText: '确定',
+    negativeText: '取消',
+    onPositiveClick: async () => {
+      const { error } = await fetchDelType(row.id as string);
+      if (!error) {
+        refresh();
+      }
+    }
+  });
+}
+</script>
+
+<template>
+  <LayoutTable>
+    <ZTable :columns="columns" :api="fetchTypeList" @add="handleAdd" @register="registerTable">
+      <template #op="{ row }">
+        <NSpace>
+          <NButton size="small" @click="handleEdit(row)">编辑</NButton>
+          <NButton size="small" @click="handleDelete(row)">删除</NButton>
+        </NSpace>
+      </template>
+    </ZTable>
+  </LayoutTable>
+</template>
+
+<style scoped></style>

+ 371 - 0
src/views/operation/coupon-issuance/index.vue

@@ -0,0 +1,371 @@
+<script setup lang="tsx">
+import { nextTick, onMounted, ref, useTemplateRef } from 'vue';
+import { useRoute } from 'vue-router';
+import { NButton } from 'naive-ui';
+import { delCoupon, fetchFailList, fetchImport, fetchList } from '@/service/api/operation/coupon-manage';
+import { commonExport } from '@/utils/common';
+import { useTable } from '@/components/zt/Table/hooks/useTable';
+import SvgIcon from '@/components/custom/svg-icon.vue';
+import { useModal } from '@/components/zt/Modal/hooks/useModal';
+const route = useRoute();
+const importTemplateRef = useTemplateRef('importTemplateRef');
+const [registerModalTable, { refresh, setFieldsValue }] = useTable({
+  searchFormConfig: {
+    schemas: [
+      {
+        field: 'activityId',
+        component: 'NInput',
+        label: '券ID'
+      },
+      {
+        field: 'activityName',
+        component: 'NInput',
+        label: '名称'
+      },
+      {
+        field: 'promotionType',
+        component: 'NSelect',
+        componentProps: {
+          options: [
+            { label: '满减券', value: 1 },
+            { label: '立减券', value: 2 }
+          ]
+        },
+        label: '类型'
+      },
+      {
+        // 优惠券状态 1-已使用 2-可使用 5-已过期 7-未生效
+        field: 'useStatus',
+        component: 'NSelect',
+        componentProps: {
+          options: [
+            { label: '已使用', value: 1 },
+            { label: '可使用', value: 2 },
+            { label: '冻结', value: 3 },
+            { label: '已过期', value: 5 },
+            { label: '未生效', value: 7 }
+          ]
+        },
+        label: '状态'
+      }
+    ],
+    inline: false,
+    size: 'small',
+    labelPlacement: 'left',
+    isFull: false
+  },
+  tableConfig: {
+    keyField: 'id',
+    title: '优惠券列表',
+    minHeight: 400,
+    showAddButton: false
+  }
+});
+const loading = ref(false);
+const tableColumns: NaiveUI.TableColumn<Api.goods.ShopCategory>[] = [
+  {
+    title: '所属企业',
+    align: 'center',
+    key: 'channelName'
+  },
+  {
+    title: '用户手机号',
+    align: 'center',
+    key: 'memberMobile'
+  },
+  {
+    title: '券ID',
+    align: 'center',
+    key: 'couponId'
+  },
+
+  {
+    title: '券名称',
+    align: 'center',
+    key: 'couponName'
+  },
+  {
+    title: '券信息',
+    align: 'center',
+    key: 'couponInfo'
+    // render: row => {
+    //   let content = '';
+    //   if (row.promotionType === 1) {
+    //     content = `满${row.amountMoney}元减${row.discountMoney}元`;
+    //   } else {
+    //     content = `立减${row.discountMoney}元`;
+    //   }
+    //   return <div>{content}</div>;
+    // }
+  },
+  {
+    title: '状态',
+    align: 'center',
+    key: 'useStatus',
+    render: row => {
+      let status = '';
+      switch (row.useStatus) {
+        case 1:
+          status = '已使用';
+          break;
+        case 2:
+          status = '可使用';
+          break;
+        case 3:
+          status = '冻结';
+          break;
+        case 5:
+          status = '已过期';
+          break;
+        case 7:
+          status = '未生效';
+          break;
+        default:
+          status = '未知状态';
+          break;
+      }
+      return <div>{status}</div>;
+    }
+  },
+  {
+    title: '创建时间',
+    align: 'center',
+    key: 'createTime'
+  },
+  {
+    title: '券使用有效期',
+    align: 'center',
+    key: 'expirationTime'
+  },
+  {
+    title: '订单编号',
+    align: 'center',
+    key: 'lockOrderId'
+  },
+  {
+    title: '操作',
+    key: 'op',
+    fixed: 'right',
+    align: 'center',
+    width: 230,
+    render: row => (
+      <div class="flex-center gap-8px">
+        <NButton type="primary" size="small" ghost onClick={() => del(row)}>
+          删除
+        </NButton>
+      </div>
+    )
+  }
+];
+
+const [registerModalFail, { openModal: openModalFail }] = useModal({
+  title: '导入记录',
+  height: 650,
+  width: 1200,
+  showFooter: false
+});
+
+const [registerLogTable, { refresh: refreshLog, setTableLoading: setLogTableLoading }] = useTable({
+  tableConfig: {
+    keyField: 'skuId',
+    title: '',
+    minHeight: 660,
+    showAddButton: false,
+    showSearch: false
+  }
+});
+
+const failColumns: NaiveUI.TableColumn<Api.government.PointsFailureRecordVO>[] = [
+  {
+    key: 'index',
+    title: '序号',
+    align: 'center',
+    width: 80,
+    render(_, rowIndex) {
+      return rowIndex + 1;
+    }
+  },
+  {
+    key: 'taskName',
+    title: '任务名称',
+    align: 'center'
+  },
+  {
+    key: 'createTime',
+    title: '时间',
+    align: 'center',
+    width: 300,
+    render(row) {
+      return (
+        <div>
+          <div>创建时间:{row.createTime}</div>
+          <div>完成时间:{row.createTime}</div>
+        </div>
+      );
+    }
+  },
+  {
+    key: 'operator',
+    title: '操作人',
+    align: 'center',
+    width: 120
+    // render(row) {
+    //   return (
+    //     <div>
+    //       <div>{row.createByRole}</div>
+    //       <div>({row.createByName})</div>
+    //     </div>
+    //   );
+    // }
+  },
+  {
+    key: 'successStatus',
+    title: '状态',
+    align: 'center',
+    width: 240,
+    render(row) {
+      return (
+        <div class={'flex items-center'}>
+          共{Number(row.failNum) + Number(row.successNum)}条,成功:{row.successNum},
+          <span class={'flex items-center text-red-500'}>
+            失败:
+            {row.failNum}
+            {row.failNum != 0 && (
+              <div onClick={() => hanleExportFailure(row.batchNo)}>
+                <SvgIcon
+                  icon={'tdesign:download'}
+                  class={'ml-1 cursor-pointer text-20px'}
+                  style={'color:red'}
+                ></SvgIcon>
+              </div>
+            )}
+          </span>
+        </div>
+      );
+    }
+  }
+];
+
+function del(row: Api.goods.ShopCategory) {
+  window.$dialog?.info({
+    title: '删除活动',
+    content: '你确定要删除吗?',
+    positiveText: '确定',
+    negativeText: '取消',
+    onPositiveClick: async () => {
+      const { error } = await delCoupon(row.id);
+      if (!error) {
+        refresh();
+      }
+    }
+  });
+}
+
+// async function handleSubmitImport(file: File) {
+//   const res = await fetchImportGoods({ file });
+//   console.log(res);
+
+//   if (!res.error) {
+//     importTemplateRef.value?.closeModal();
+//   }
+//   window.$message?.success(res.data);
+//   importTemplateRef.value?.setSubLoading(false);
+// }
+function openImportModal() {
+  importTemplateRef.value?.openModal();
+}
+async function openImportLogModal() {
+  openModalFail();
+  await nextTick();
+  refreshLog();
+}
+async function handleSubmitImport(file: File) {
+  console.log(file);
+
+  // setTableLoading(true);
+  try {
+    const res = await fetchImport({ file });
+    console.log(res);
+
+    // if (!res.error) {
+    //   importTemplateRef.value?.closeModal();
+    // }
+    window.$message?.success(res.data);
+    importTemplateRef.value?.setSubLoading(false);
+    window.$dialog?.success({
+      title: '提示',
+      content: () => {
+        return (
+          <div>
+            <p>导入操作进行中......</p>
+            <p>是否进入导入记录</p>
+          </div>
+        );
+      },
+      positiveText: '确定',
+      negativeText: '取消',
+      onPositiveClick: async () => {
+        importTemplateRef.value?.closeModal();
+        openImportLogModal();
+      },
+      onNegativeClick: () => {}
+    });
+  } finally {
+    importTemplateRef.value?.setSubLoading(false);
+  }
+}
+async function hanleExportFailure(batchNo: string) {
+  if (loading.value) {
+    window.$message?.error('正在导出,请勿重复点击');
+    return;
+  }
+  loading.value = true;
+  setLogTableLoading(true);
+  try {
+    await commonExport('/smqjh-system/sys-api/memberCoupon/downloadErrorExcel', { batchNo }, '失败的记录.xlsx');
+  } finally {
+    loading.value = false;
+    setLogTableLoading(false);
+  }
+}
+
+onMounted(() => {
+  setTimeout(async () => {
+    await setFieldsValue({ activityId: route.query.activityId });
+    refresh();
+  }, 1000);
+});
+</script>
+
+<template>
+  <LayoutTable>
+    <ZTable :show-table-action="false" :columns="tableColumns" :api="fetchList" @register="registerModalTable">
+      <template #prefix>
+        <NSpace>
+          <NButton size="small" @click="openImportModal">批量发放</NButton>
+          <NButton size="small" @click="openImportLogModal">导入记录</NButton>
+        </NSpace>
+      </template>
+    </ZTable>
+
+    <ZImportTemplate
+      ref="importTemplateRef"
+      url="/smqjh-system/sys-api/memberCoupon/downloadXlsx"
+      template-text="批量发放导入模版.xlsx"
+      modal-text="批量发放"
+      @submit="handleSubmitImport"
+    ></ZImportTemplate>
+
+    <BasicModal @register="registerModalFail">
+      <ZTable
+        :immediate="false"
+        :columns="failColumns"
+        :show-table-action="false"
+        :api="fetchFailList"
+        @register="registerLogTable"
+      ></ZTable>
+    </BasicModal>
+  </LayoutTable>
+</template>
+
+<style scoped></style>

+ 423 - 0
src/views/operation/coupon-manage/index.vue

@@ -0,0 +1,423 @@
+<script setup lang="tsx">
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+import { NButton } from 'naive-ui';
+import {
+  delActivity,
+  fetchAddActivity,
+  fetchEditActivity,
+  fetchGetActivityList
+} from '@/service/api/operation/coupon-manage';
+import { useModalFrom } from '@/components/zt/ModalForm/hooks/useModalForm';
+import { useTable } from '@/components/zt/Table/hooks/useTable';
+const router = useRouter();
+const isLimitReceiveNum = ref(false);
+const isLimitInventory = ref(false);
+
+const [registerModalTable, { refresh }] = useTable({
+  searchFormConfig: {
+    schemas: [
+      {
+        field: 'activityId',
+        component: 'NInput',
+        label: '券ID'
+      },
+      {
+        field: 'activityName',
+        component: 'NInput',
+        label: '名称'
+      },
+      {
+        field: 'promotionType',
+        component: 'NSelect',
+        componentProps: {
+          options: [
+            { label: '满减券', value: 1 },
+            { label: '立减券', value: 2 }
+          ]
+        },
+        label: '类型'
+      }
+    ],
+    inline: false,
+    size: 'small',
+    labelPlacement: 'left',
+    isFull: false
+  },
+  tableConfig: {
+    keyField: 'id',
+    title: '优惠券列表',
+    minHeight: 400
+  }
+});
+
+const [
+  registerModalForm,
+  {
+    openModal: openModalForm,
+    setFieldsValue: setModalFormValue,
+    getFieldsValue: getModalFormValue,
+    closeModal: closeModalForm,
+    setSubLoading
+  }
+] = useModalFrom({
+  modalConfig: {
+    title: '优惠券',
+    isShowHeaderText: true
+  },
+  formConfig: {
+    schemas: [
+      {
+        field: 'id',
+        label: 'ID',
+        show: false,
+        component: 'NInput'
+      },
+
+      {
+        field: 'activityName',
+        component: 'NInput',
+        label: '活动名称',
+        required: true,
+        componentProps: {
+          maxlength: 20
+        }
+      },
+      {
+        field: 'promotionType',
+        component: 'NRadioGroup',
+        label: '类型',
+        required: true,
+        defaultValue: 1,
+        componentProps: {
+          options: [
+            {
+              label: '满减券',
+              value: 1
+            },
+            {
+              label: '立减券',
+              value: 2
+            }
+          ]
+        }
+      },
+      {
+        field: 'amountMoney',
+        component: 'NInputNumber',
+        label: '门槛(元)',
+        required: true,
+        ifShow: ({ model }) => model.promotionType === 1,
+        componentProps: {
+          min: 0,
+          max: 1000,
+          precision: 0
+        }
+      },
+      {
+        field: 'discountMoney',
+        component: 'NInputNumber',
+        label: '面额(元)',
+        rules: { validator: validateDiscountMoney, trigger: 'input' },
+        componentProps: {
+          min: 0,
+          max: 1000,
+          precision: 0
+        }
+      },
+      {
+        field: 'inventory',
+        component: 'NInputNumber',
+        label: '总量(张)',
+        required: true,
+        render: ({ model, field }) => {
+          return (
+            <div class="flex items-center">
+              <n-input-number
+                v-model:value={model[field]}
+                min={1}
+                max={100000}
+                disabled={isLimitInventory.value}
+                clearable
+              />
+              <n-checkbox
+                class="ml-6px"
+                checked={isLimitInventory.value}
+                onUpdateChecked={(val: any) => (isLimitInventory.value = val)((model[field] = 99999))}
+              >
+                不限制
+              </n-checkbox>
+            </div>
+          );
+        }
+      },
+      {
+        field: 'limitReceiveNum',
+        component: 'NInputNumber',
+        label: '每人限领(张)',
+        required: true,
+        render: ({ model, field }) => {
+          return (
+            <div class="flex items-center">
+              <n-input-number
+                v-model:value={model[field]}
+                min={1}
+                max={model.inventory || 100000}
+                disabled={isLimitReceiveNum.value}
+                clearable
+              />
+              <n-checkbox
+                class="ml-6px"
+                checked={isLimitReceiveNum.value}
+                onUpdateChecked={(val: any) => (isLimitReceiveNum.value = val)((model[field] = 99999))}
+              >
+                不限制
+              </n-checkbox>
+            </div>
+          );
+        }
+      },
+      {
+        field: 'businessType',
+        component: 'NRadioGroup',
+        label: '使用限制',
+        required: true,
+        defaultValue: 'XSB',
+        componentProps: {
+          options: [
+            {
+              label: '星闪豹',
+              value: 'XSB'
+            }
+          ]
+        }
+      },
+      {
+        field: 'expirationDate',
+        component: 'NInputNumber',
+        label: '券使用有效期',
+        required: true,
+        componentProps: {
+          min: 0,
+          max: 100000
+        },
+        render: ({ model, field }) => {
+          return (
+            <div class="flex items-center">
+              领取后 <n-input-number v-model:value={model[field]} min={0} max={10000} clearable /> 天有效
+            </div>
+          );
+        }
+      }
+    ],
+    labelWidth: 120,
+    layout: 'horizontal',
+    gridProps: {
+      cols: '1',
+      itemResponsive: true
+    }
+  }
+});
+
+const tableColumns: NaiveUI.TableColumn<Api.goods.ShopCategory>[] = [
+  {
+    title: '券ID',
+    align: 'center',
+    key: 'activityId'
+  },
+  {
+    title: '活动名称',
+    align: 'center',
+    key: 'activityName'
+  },
+  {
+    title: '类型',
+    align: 'center',
+    key: 'promotionType',
+    render: row => {
+      return row.promotionType === 1 ? '满减券' : '无门槛券';
+    }
+  },
+  {
+    title: '门槛(元)',
+    align: 'center',
+    key: 'amountMoney'
+  },
+  {
+    title: '面额(元)',
+    align: 'center',
+    key: 'discountMoney'
+  },
+  {
+    title: '总量(张)',
+    align: 'center',
+    key: 'inventory',
+    render: row => {
+      return <div>{row.inventory || '不限制'}</div>;
+    }
+  },
+  {
+    title: '未发放(张)',
+    align: 'center',
+    key: 'inventoryActual',
+    render: row => {
+      return <div>{row.inventoryActual === -1 ? '-' : row.inventoryActual}</div>;
+    }
+  },
+  {
+    title: '每人限领(张)',
+    align: 'center',
+    key: 'limitReceiveNum',
+    render: row => {
+      return <div>{row.limitReceiveNum || '不限制'}</div>;
+    }
+  },
+  {
+    title: '券使用有效期',
+    align: 'center',
+    key: 'expirationDate'
+  },
+  {
+    title: '创建时间',
+    align: 'center',
+    key: 'updateTime'
+  },
+  {
+    title: '操作',
+    key: 'op',
+    fixed: 'right',
+    align: 'center',
+    width: 230,
+    render: row => (
+      <div class="flex-center gap-8px">
+        {
+          <NButton type="primary" size="small" ghost onClick={() => handleRecord(row)}>
+            查看领取情况
+          </NButton>
+        }
+        {
+          <NButton type="primary" size="small" ghost onClick={() => edit(row)}>
+            编辑
+          </NButton>
+        }
+        <NButton type="primary" size="small" ghost onClick={() => del(row)}>
+          删除
+        </NButton>
+      </div>
+    )
+  }
+];
+
+function handleRecord(row: any) {
+  if (!row.activityId) {
+    window.$message?.error('订单异常');
+  }
+  router.push({
+    path: '/operation/coupon-issuance',
+    query: {
+      activityId: row.activityId
+    }
+  });
+}
+
+// async function validatePrice(_: any, value: any) {
+//   const form = await getModalFormValue();
+//   if (value <= form.discountMoney) {
+//     console.log(1111111);
+
+//     return; // 验证通过时返回undefined
+//   }
+
+//   throw new Error('应小于等于面额'); // 验证失败时抛出错误
+// }
+async function validateDiscountMoney(_: any, value: any) {
+  const form = await getModalFormValue();
+  if (value <= form.amountMoney || form.promotionType === 2) {
+    return; // 验证通过时返回undefined
+  }
+  throw new Error('需要小于等于门槛金额。数值范围0-1000,整数'); // 验证失败时抛出错误
+}
+
+async function edit(row: Api.goods.ShopCategory) {
+  if (row.inventoryActual < row.inventoryTotal) {
+    window.$dialog?.info({
+      title: '提示',
+      content: '已有人领取,不允许修改',
+      positiveText: '确定'
+    });
+    return;
+  }
+  openModalForm(row);
+  // setModalProps({ title: `修改${Number(row.level)}级分类` });
+  setModalFormValue(row);
+}
+function add(row: Api.goods.ShopCategory) {
+  openModalForm();
+  console.log(row, 'row-key');
+  // updateSchema({ field: 'iconUrl', required: true });
+  // setModalProps({ title: `新增二级分类` });
+  // setModalFormValue({ parentName: row.name, name: '', parentId: row.id });
+}
+function del(row: Api.goods.ShopCategory) {
+  if (row.inventoryActual < row.inventoryTotal) {
+    window.$dialog?.info({
+      title: '提示',
+      content: '已有人领取,不允许删除',
+      positiveText: '确定'
+    });
+    return;
+  }
+  window.$dialog?.info({
+    title: '删除活动',
+    content: '你确定要删除吗?',
+    positiveText: '确定',
+    negativeText: '取消',
+    onPositiveClick: async () => {
+      const { error } = await delActivity(row.id);
+      if (!error) {
+        refresh();
+      }
+    }
+  });
+}
+
+async function handleSubmit() {
+  const form = await getModalFormValue();
+  setSubLoading(false);
+  if (isLimitInventory.value) {
+    form.inventory = 0;
+  }
+  if (isLimitReceiveNum.value) {
+    form.limitReceiveNum = 0;
+  }
+  if (!form.id) {
+    const { error } = await fetchAddActivity(form);
+    if (error) {
+      return;
+    }
+  } else {
+    // 修改
+    const { error } = await fetchEditActivity(form);
+    if (error) {
+      return;
+    }
+  }
+  closeModalForm();
+  refresh();
+}
+</script>
+
+<template>
+  <LayoutTable>
+    <ZTable
+      :show-table-action="false"
+      :columns="tableColumns"
+      :api="fetchGetActivityList"
+      @register="registerModalTable"
+      @add="add"
+    ></ZTable>
+    <BasicModelForm @register-modal-form="registerModalForm" @submit-form="handleSubmit"></BasicModelForm>
+  </LayoutTable>
+</template>
+
+<style scoped></style>

+ 10 - 4
src/views/order-manage/after-sales-order-detail/index.vue

@@ -111,7 +111,8 @@ function handleDetail(orderNumber: string) {
         <NCard size="small" class="mt-20px" title="客户信息" :bordered="false">
           <div>客户姓名:{{ orderInfo?.consigneeName || '---' }}</div>
           <div>客户手机号:{{ orderInfo?.consigneeMobile || '---' }}</div>
-          <div>客户地址:{{ orderInfo?.consigneeAddress || '---' }}</div>
+          <div>客户地址:{{ orderInfo?.dvyType == 2 ? '自提' : orderInfo?.consigneeAddress || '---' }}</div>
+
           <div>用户昵称:{{ orderInfo.userName || '---' }}</div>
           <div>用户电话:{{ orderInfo.consigneeMobile }}</div>
           <div>企业身份:{{ orderInfo.channelName || '---' }}</div>
@@ -151,9 +152,14 @@ function handleDetail(orderNumber: string) {
 
           <div class="mt-20px flex items-center justify-end">
             <div class="text-20px font-semibold">退款总金额:{{ orderInfo.refundMoney }} 元</div>
-            <div class="ml-20px text-14px text-gray">退还金额:{{ orderInfo.userRefundMoney }}</div>
-            <div class="text-14px text-gray">退还积分:{{ orderInfo.refundScore }}</div>
-            <div class="text-14px text-gray">已过期:({{ orderInfo.refundExpiredScore }} )</div>
+            <template v-if="orderInfo.returnMoneySts === 70">
+              <div class="ml-20px text-14px text-gray">退还金额:{{ orderInfo.userRefundMoney }}</div>
+              <div class="text-14px text-gray">退还积分:{{ orderInfo.refundScore }}</div>
+              <div class="text-14px text-gray">已过期:({{ orderInfo.refundExpiredScore }} )</div>
+              <div v-if="orderInfo.couponBaseInfoDTO" class="text-14px text-gray">
+                退还优惠券:{{ orderInfo.couponBaseInfoDTO?.discountMoney || 0 }}
+              </div>
+            </template>
           </div>
 
           <div class="py-20px font-semibold">03 退款记录</div>

+ 1 - 1
src/views/order-manage/after-sales-order/index.vue

@@ -57,7 +57,7 @@ const columns: NaiveUI.TableColumn<Api.delivery.deliveryOrder>[] = [
             {row.consigneeName}
             {row.consigneeMobile}
           </div>
-          <div>{row.consigneeAddress}</div>
+          {row.dvyType == 2 ? <div>自提</div> : <div>{row.consigneeAddress}</div>}
         </div>
       );
     }

+ 21 - 5
src/views/order-manage/normal-order/index.vue

@@ -52,6 +52,12 @@ const columns: NaiveUI.TableColumn<Api.delivery.deliveryOrder>[] = [
       return <NTag>{businessType[row.businessType as keyof typeof businessType] || row.businessType}</NTag>;
     }
   },
+  {
+    key: 'shopName',
+    title: '门店名称',
+    align: 'center',
+    width: 120
+  },
   {
     key: 'channelName',
     title: '所属企业',
@@ -70,7 +76,7 @@ const columns: NaiveUI.TableColumn<Api.delivery.deliveryOrder>[] = [
             {row.consigneeName}
             {row.consigneeMobile}
           </div>
-          <div>{row.consigneeAddress}</div>
+          {row.dvyType == 2 ? <div>自提</div> : <div>{row.consigneeAddress}</div>}
         </div>
       );
     }
@@ -83,7 +89,7 @@ const columns: NaiveUI.TableColumn<Api.delivery.deliveryOrder>[] = [
   },
   {
     key: 'status',
-    title: '订单状态',
+    title: '业务状态',
     align: 'center',
     width: 120,
     render: row => {
@@ -343,7 +349,7 @@ async function handleExport(exportType: any) {
     //   { ...getFieldsValue(), orderStatus: activeTab.value },
     //   '正常订单列表.xlsx'
     // );
-    await fetchExportOrderList({ exportType });
+    await fetchExportOrderList({ exportType, ...getSeachForm(), orderStatus: activeTab.value });
     dialog.success({
       title: '提示',
       content: () => {
@@ -440,14 +446,24 @@ function handleship(row: Api.delivery.deliveryOrder) {
           </NButton>
 
           <NButton
-            v-if="row.dvyType === 10 && row.hbOrderStatus === 40"
+            v-if="row.dvyType === 10 && row.hbOrderStatus === 40 && !row.dvyNo"
+            class="mt-10px"
+            size="small"
+            type="primary"
+            ghost
+            @click="handleship(row)"
+          >
+            发货
+          </NButton>
+          <NButton
+            v-if="row.dvyType === 10 && (row.hbOrderStatus === 40 || row.hbOrderStatus === 70) && row.dvyNo"
             class="mt-10px"
             size="small"
             type="primary"
             ghost
             @click="handleship(row)"
           >
-            {{ row.dvyno ? '修改发货信息' : '发货' }}
+            修改发货信息
           </NButton>
         </div>
         <!--

+ 73 - 10
src/views/order-manage/order-detail/index.vue

@@ -299,7 +299,7 @@ function handleSaleOrder(row: Api.delivery.deliveryOrder) {
   router.push({
     path: '/order-manage/after-sales-order',
     query: {
-      orderNumber: orderInfo.value?.orderNumber
+      orderNumber: row.orderNumber
     }
   });
 }
@@ -368,7 +368,7 @@ function getRefundRecordText() {
         <NCard size="small" class="mt-20px" title="客户信息" :bordered="false">
           <div>客户姓名:{{ orderInfo?.consigneeName || '---' }}</div>
           <div>客户手机号:{{ orderInfo?.consigneeMobile || '---' }}</div>
-          <div>客户地址:{{ orderInfo?.consigneeAddress || '---' }}</div>
+          <div>客户地址:{{ orderInfo?.dvyType == 2 ? '自提' : orderInfo?.consigneeAddress || '---' }}</div>
           <div>用户昵称:{{ orderInfo.userName || '---' }}</div>
           <div>用户电话:{{ orderInfo.consigneeMobile }}</div>
           <div>企业身份:{{ orderInfo.channelName || '---' }}</div>
@@ -406,8 +406,12 @@ function getRefundRecordText() {
 -->
           <div class="flex items-center">
             <span>退款记录:{{ getRefundRecordText() }}</span>
+            <!-- v-if="getRefundRecordCount() > 0" -->
             <NButton
-              v-if="getRefundRecordCount() > 0"
+              v-if="
+                Number(orderInfo.backendOrderRefundLogCount) ||
+                (orderInfo.refundOrderList && orderInfo.refundOrderList.length > 0)
+              "
               size="small"
               quaternary
               type="primary"
@@ -443,10 +447,37 @@ function getRefundRecordText() {
             </div>
           </div>
 
-          <!-- v-if="orderInfo.dvyType === 10 && orderInfo.hbOrderStatus === 40" -->
-          <NButton size="small" type="primary" @click="handleShip">
+          <!--
+ <NButton v-if="orderInfo.dvyType === 10 && orderInfo.hbOrderStatus === 40" size="small" type="primary"
+            @click="handleShip">
             {{ orderInfo.dvyNo ? '修改发货信息' : '发货' }}
           </NButton>
+-->
+
+          <NButton
+            v-if="orderInfo.dvyType === 10 && orderInfo.hbOrderStatus === 40 && !orderInfo.dvyNo"
+            class="mt-10px"
+            size="small"
+            type="primary"
+            ghost
+            @click="handleShip"
+          >
+            发货
+          </NButton>
+          <NButton
+            v-if="
+              orderInfo.dvyType === 10 &&
+              (orderInfo.hbOrderStatus === 40 || orderInfo.hbOrderStatus === 70) &&
+              orderInfo.dvyNo
+            "
+            class="mt-10px"
+            size="small"
+            type="primary"
+            ghost
+            @click="handleShip"
+          >
+            修改发货信息
+          </NButton>
         </div>
         <NCard size="small" title="业务信息" :bordered="false">
           <template v-if="orderInfo.businessType == 'XSB'">
@@ -462,20 +493,33 @@ function getRefundRecordText() {
                 <NTr>
                   <NTh>费用类型</NTh>
                   <NTh>金额/元</NTh>
+                  <NTh>备注</NTh>
                 </NTr>
               </NThead>
               <NTbody>
                 <NTr>
                   <NTd>商品总额</NTd>
                   <NTd>{{ orderInfo.total }}</NTd>
+                  <NTd></NTd>
                 </NTr>
-                <NTr>
+                <NTr v-if="orderInfo.dvyType !== 2">
                   <NTd>配送费(快递)</NTd>
                   <NTd>{{ orderInfo.freightAmount }}</NTd>
+                  <NTd></NTd>
+                </NTr>
+                <NTr v-if="orderInfo.memberDiscountAmount">
+                  <NTd>{{ orderInfo.memberDiscountDesc }}</NTd>
+                  <NTd>-{{ orderInfo.memberDiscountAmount }}</NTd>
+                </NTr>
+                <NTr v-if="orderInfo.couponBaseInfoDTO">
+                  <NTd>优惠券</NTd>
+                  <NTd>-{{ orderInfo.couponBaseInfoDTO?.discountMoney || 0 }}</NTd>
+                  <NTd>{{ orderInfo.couponBaseInfoDTO?.allowanceId }}</NTd>
                 </NTr>
                 <NTr>
                   <NTd>积分</NTd>
                   <NTd>-{{ (Number(orderInfo.offsetPoints) / 100).toFixed(2) || 0 }}</NTd>
+                  <NTd></NTd>
                 </NTr>
                 <NTr>
                   <NTd v-if="orderInfo.hbOrderStatus == orderStatusEnum.WAIT_PAY">需付款</NTd>
@@ -506,6 +550,21 @@ function getRefundRecordText() {
                 />
               </NTimeline>
             </template>
+            <template v-else-if="orderInfo.dvyType == 2">
+              <div class="py-20px font-semibold">03 自提信息</div>
+              <div>预留电话:{{ orderInfo.consigneeMobile || '---' }}</div>
+              <div>自提点:{{ orderInfo.consigneeAddress || '---' }}</div>
+              <template
+                v-if="
+                  orderInfo.hbOrderStatus == orderStatusEnum.ORDER_WAIT_DELIVERY ||
+                  orderInfo.hbOrderStatus == orderStatusEnum.ORDER_COMPLETE
+                "
+              >
+                <div>自提核验码:{{ orderInfo.selfPickCode || '---' }}</div>
+                <div>核验状态:{{ orderInfo.isWriteOff == 1 ? '待核销' : '已核销' }}</div>
+                <div>核验时间:{{ orderInfo.completeTime || '---' }}</div>
+              </template>
+            </template>
           </template>
           <template v-else-if="orderInfo.businessType == 'DYY'">
             <div class="pb-20px font-semibold">01 影片与场次信息</div>
@@ -601,11 +660,11 @@ function getRefundRecordText() {
                 <NTbody>
                   <NTr>
                     <NTd>电费</NTd>
-                    <NTd>{{ orderInfo.chargeOrder.totalMoney }}</NTd>
+                    <NTd>{{ orderInfo.chargeOrder.totalElecMoney || 0 }}</NTd>
                   </NTr>
                   <NTr>
                     <NTd>结算服务费</NTd>
-                    <NTd>0</NTd>
+                    <NTd>{{ orderInfo.chargeOrder.totalSeviceMoney || 0 }}</NTd>
                   </NTr>
                   <NTr>
                     <NTd>运营服务费</NTd>
@@ -613,7 +672,11 @@ function getRefundRecordText() {
                   </NTr>
                   <NTr>
                     <NTd>订单金额</NTd>
-                    <NTd>{{ orderInfo.actualTotal }}</NTd>
+                    <NTd>{{ orderInfo.orderMoney }}</NTd>
+                  </NTr>
+                  <NTr v-if="orderInfo.chargeOrder.memberDiscountAmount">
+                    <NTd>会员权益(每度电立减{{ orderInfo.chargeOrder.memberPerKwhDiscount }}元)</NTd>
+                    <NTd>-{{ orderInfo.chargeOrder.memberDiscountAmount }}</NTd>
                   </NTr>
                   <NTr>
                     <NTd>积分抵扣</NTd>
@@ -754,7 +817,7 @@ function getRefundRecordText() {
               :key="index"
               class="mt-10px border rounded-4px p-8px"
             >
-              <div>游客姓名{{ index + 1 }}:{{ item.linkMan || '---' }}</div>
+              <div>游客姓名{{ (index as number) + 1 }}:{{ item.linkMan || '---' }}</div>
               <div>证据类型:{{ filterType(item.linkCreditType) || '---' }}</div>
               <div>证件号码:{{ item.linkCreditNo || '---' }}</div>
             </div>

+ 181 - 0
src/views/tenant/tenant-change/index.vue

@@ -0,0 +1,181 @@
+<script setup lang="tsx">
+import { onMounted, ref } from 'vue';
+import { NButton, NCard, NInput, NSpin, NTag } from 'naive-ui';
+import {
+  fetchClearSwitchTenant,
+  fetchGetActiveTenants,
+  fetchGetCurrentSwitchTenant,
+  fetchSwitchTenant
+} from '@/service/api/tenant';
+import { useTable } from '@/components/zt/Table/hooks/useTable';
+import { $t } from '@/locales';
+
+interface TenantData {
+  tenantId: number;
+  tenantCode: string;
+  tenantDomain: string;
+  tenantName: string;
+  packageId: number;
+  status: number;
+  expireTime: string;
+}
+
+const columns: NaiveUI.TableColumn<any>[] = [
+  {
+    key: 'index',
+    title: $t('common.index'),
+    align: 'center',
+    width: 64,
+    render: (_, index) => index + 1
+  },
+  {
+    key: 'tenantCode',
+    title: '租户编号',
+    align: 'center',
+    minWidth: 120
+  },
+  {
+    key: 'tenantName',
+    title: '企业名称',
+    align: 'center',
+    minWidth: 150
+  },
+  {
+    key: 'tenantDomain',
+    title: '绑定域名',
+    align: 'center',
+    minWidth: 150
+  },
+  {
+    key: 'expireTime',
+    title: '过期时间',
+    align: 'center',
+    minWidth: 170
+  },
+  {
+    key: 'status',
+    title: '租户状态',
+    align: 'center',
+    width: 100,
+    render: row => {
+      if (row.status === null || row.status === undefined) {
+        return null;
+      }
+      const tagMap: Record<number, NaiveUI.ThemeColor> = {
+        1: 'success',
+        0: 'error'
+      };
+      const labelMap: Record<number, string> = {
+        1: '正常',
+        0: '停用'
+      };
+      return <NTag type={tagMap[row.status]}>{labelMap[row.status]}</NTag>;
+    }
+  }
+];
+
+const [registerTable, { setTableLoading }] = useTable({
+  searchFormConfig: {
+    schemas: [],
+    inline: false,
+    size: 'small',
+    labelPlacement: 'left',
+    isFull: false
+  },
+  tableConfig: {
+    keyField: 'tenantId',
+    title: '租户列表',
+    showAddButton: false,
+    scrollX: 800,
+    showSearch: false
+  }
+});
+
+// 当前选中的租户
+const currentTenant = ref<TenantData | null>(null);
+const currentTenantDisplay = ref('');
+const loadingCurrentTenant = ref(false);
+
+// 获取当前切换的租户
+async function loadCurrentTenant() {
+  loadingCurrentTenant.value = true;
+  try {
+    const { data, error } = await fetchGetCurrentSwitchTenant();
+    if (!error && data) {
+      currentTenant.value = data;
+      currentTenantDisplay.value = data.tenantName || '';
+    } else {
+      currentTenant.value = null;
+      currentTenantDisplay.value = '';
+    }
+  } catch (err) {
+    console.error(err);
+    currentTenant.value = null;
+    currentTenantDisplay.value = '';
+  } finally {
+    loadingCurrentTenant.value = false;
+  }
+}
+
+// 清除租户切换
+async function handleClearTenant() {
+  try {
+    const { error } = await fetchClearSwitchTenant();
+    if (!error) {
+      currentTenant.value = null;
+      currentTenantDisplay.value = '';
+      window.$message?.success('已清除');
+    }
+  } catch (err) {
+    console.error(err);
+    window.$message?.error('清除失败');
+  }
+}
+
+// 切换租户
+async function handleSwitch(row: TenantData) {
+  setTableLoading(true);
+  try {
+    const { error } = await fetchSwitchTenant(row.tenantCode);
+    if (!error) {
+      window.$message?.success('切换成功');
+      await loadCurrentTenant();
+    }
+  } catch (err) {
+    console.error(err);
+    window.$message?.error('切换失败');
+  } finally {
+    setTableLoading(false);
+  }
+}
+
+onMounted(() => {
+  loadCurrentTenant();
+});
+</script>
+
+<template>
+  <LayoutTable>
+    <NCard size="small" class="mb-16px">
+      <div class="flex items-center gap-12px">
+        <span class="whitespace-nowrap text-14px">当前租户:</span>
+        <NSpin :show="loadingCurrentTenant" size="small" class="w-300px">
+          <NInput
+            :value="currentTenantDisplay"
+            placeholder="暂未选择租户"
+            readonly
+            clearable
+            @clear="handleClearTenant"
+          />
+        </NSpin>
+      </div>
+    </NCard>
+    <ZTable :columns="columns as any" :api="fetchGetActiveTenants" @register="registerTable">
+      <template #op="{ row }">
+        <NButton size="small" ghost type="primary" @click="handleSwitch(row)">切换</NButton>
+      </template>
+    </ZTable>
+  </LayoutTable>
+</template>
+
+<style scoped></style>

+ 184 - 0
src/views/tenant/tenant-package/index.vue

@@ -0,0 +1,184 @@
+<script setup lang="tsx">
+import { ref } from 'vue';
+import { NButton, NInput, NPopconfirm, NSwitch, NTree } from 'naive-ui';
+import {
+  fetchAddTenantPackage,
+  fetchDeleteTenantPackage,
+  fetchEditTenantPackage,
+  fetchGetTenantPackageList,
+  fetchGetTenantPackageMenus,
+  fetchUpdateTenantPackageMenus
+} from '@/service/api/tenant';
+import { fetchGetMenuTree } from '@/service/api';
+import { useTable } from '@/components/zt/Table/hooks/useTable';
+import { $t } from '@/locales';
+import { useModal } from '@/components/zt/Modal/hooks/useModal';
+
+const columns: NaiveUI.TableColumn<any>[] = [
+  {
+    type: 'selection',
+    align: 'center',
+    width: 48
+  },
+  {
+    key: 'index',
+    title: $t('common.index'),
+    align: 'center',
+    width: 64,
+    render: (_, index) => index + 1
+  },
+  {
+    key: 'packageName',
+    title: '套餐名称',
+    align: 'center',
+    minWidth: 200
+  },
+  {
+    key: 'status',
+    title: '状态',
+    align: 'center',
+    width: 120,
+    render: row => {
+      return <NSwitch value={row.status === 1} />;
+    }
+  },
+  {
+    key: 'remark',
+    title: '备注',
+    align: 'center',
+    minWidth: 200
+  }
+];
+
+const [registerTable, { refresh, setTableLoading }] = useTable({
+  searchFormConfig: {
+    schemas: [],
+    inline: false,
+    size: 'small',
+    labelPlacement: 'left',
+    isFull: false
+  },
+  tableConfig: {
+    keyField: 'id',
+    title: '租户套餐列表',
+    showAddButton: true,
+    scrollX: 800,
+    showSearch: false
+  }
+});
+
+async function handleDelete(row: Recordable) {
+  setTableLoading(true);
+  await fetchDeleteTenantPackage([row.id].join(','));
+  refresh();
+}
+
+// 新增/编辑套餐 - 带菜单权限树
+const menuData = ref<any[]>([]);
+const checkedKeys = ref<number[]>([]);
+const currentEditId = ref<number>();
+const packageName = ref('');
+const packageRemark = ref('');
+
+const [registerModalMenu, { openModal: openMenuModal, closeModal: closeMenuModal, setSubLoading }] = useModal({
+  title: '新增租户套餐',
+  height: 600
+});
+
+async function handleSubmitWithMenu() {
+  // 提交新增/编辑套餐并分配菜单
+  if (!packageName.value) {
+    window.$message?.error('请输入套餐名称');
+    return;
+  }
+  setSubLoading(true);
+  try {
+    const formData: any = {
+      packageName: packageName.value,
+      remark: packageRemark.value,
+      status: 1
+    };
+    if (currentEditId.value) {
+      formData.id = currentEditId.value;
+      await fetchEditTenantPackage(formData);
+      await fetchUpdateTenantPackageMenus(currentEditId.value, checkedKeys.value);
+    } else {
+      const res = await fetchAddTenantPackage(formData);
+      // 如果返回了id,分配菜单
+      if (res?.data?.id) {
+        await fetchUpdateTenantPackageMenus(res.data.id, checkedKeys.value);
+      }
+    }
+    closeMenuModal();
+    refresh();
+  } finally {
+    setSubLoading(false);
+  }
+}
+
+async function handleOpenAdd() {
+  currentEditId.value = undefined;
+  packageName.value = '';
+  packageRemark.value = '';
+  checkedKeys.value = [];
+  openMenuModal();
+  const { data } = await fetchGetMenuTree();
+  menuData.value = data as any[];
+}
+
+async function edit(row: Recordable) {
+  currentEditId.value = row.id;
+  packageName.value = row.packageName || '';
+  packageRemark.value = row.remark || '';
+  openMenuModal();
+  const { data: treeData } = await fetchGetMenuTree();
+  menuData.value = treeData as any[];
+  const { data: menuIds } = await fetchGetTenantPackageMenus(row.id);
+  checkedKeys.value = menuIds || [];
+}
+</script>
+
+<template>
+  <LayoutTable>
+    <ZTable :columns="columns as any" :api="fetchGetTenantPackageList" @register="registerTable" @add="handleOpenAdd">
+      <template #op="{ row }">
+        <NButton size="small" ghost type="primary" @click="edit(row)">编辑</NButton>
+        <NPopconfirm @positive-click="handleDelete(row)">
+          <template #trigger>
+            <NButton size="small" type="error" ghost>删除</NButton>
+          </template>
+          确定删除吗?
+        </NPopconfirm>
+      </template>
+    </ZTable>
+    <BasicModal @register="registerModalMenu" @ok="handleSubmitWithMenu">
+      <div class="p-4">
+        <div class="mb-4">
+          <div class="mb-2 font-bold">
+            套餐名称
+            <span class="text-red-500">*</span>
+          </div>
+          <NInput v-model:value="packageName" placeholder="请输入套餐名称" />
+        </div>
+        <div class="mb-4">
+          <div class="mb-2 font-bold">菜单权限</div>
+          <NTree
+            v-model:checked-keys="checkedKeys"
+            :data="menuData"
+            key-field="value"
+            checkable
+            cascade
+            default-expand-all
+            block-line
+          />
+        </div>
+        <div class="mb-4">
+          <div class="mb-2 font-bold">备注</div>
+          <NInput v-model:value="packageRemark" type="textarea" :rows="3" placeholder="请输入备注" />
+        </div>
+      </div>
+    </BasicModal>
+  </LayoutTable>
+</template>
+
+<style scoped></style>

+ 284 - 0
src/views/tenant/tenant/index.vue

@@ -0,0 +1,284 @@
+<script setup lang="tsx">
+import { NButton, NPopconfirm, NTag } from 'naive-ui';
+import {
+  fetchAddTenant,
+  fetchDeleteTenant,
+  fetchEditTenant,
+  fetchGetTenantList,
+  fetchGetTenantPackageList
+} from '@/service/api/tenant';
+import { useTable } from '@/components/zt/Table/hooks/useTable';
+import { $t } from '@/locales';
+import { useModalFrom } from '@/components/zt/ModalForm/hooks/useModalForm';
+
+const columns: NaiveUI.TableColumn<any>[] = [
+  {
+    type: 'selection',
+    align: 'center',
+    width: 48
+  },
+  {
+    key: 'index',
+    title: $t('common.index'),
+    align: 'center',
+    width: 64,
+    render: (_, index) => index + 1
+  },
+  {
+    key: 'tenantCode',
+    title: '租户编号',
+    align: 'center',
+    minWidth: 120
+  },
+  {
+    key: 'contactName',
+    title: '联系人',
+    align: 'center',
+    minWidth: 100
+  },
+  {
+    key: 'contactMobile',
+    title: '联系电话',
+    align: 'center',
+    minWidth: 130
+  },
+  {
+    key: 'tenantName',
+    title: '企业名称',
+    align: 'center',
+    minWidth: 150
+  },
+  {
+    key: 'expireTime',
+    title: '过期时间',
+    align: 'center',
+    minWidth: 170
+  },
+  {
+    key: 'status',
+    title: '租户状态',
+    align: 'center',
+    width: 100,
+    render: row => {
+      if (row.status === null || row.status === undefined) {
+        return null;
+      }
+      const tagMap: Record<number, NaiveUI.ThemeColor> = {
+        1: 'success',
+        0: 'error'
+      };
+      const labelMap: Record<number, string> = {
+        1: '正常',
+        0: '停用'
+      };
+      return <NTag type={tagMap[row.status]}>{labelMap[row.status]}</NTag>;
+    }
+  }
+];
+
+const [registerTable, { refresh, setTableLoading }] = useTable({
+  searchFormConfig: {
+    schemas: [
+      {
+        field: 'keywords',
+        label: '关键字',
+        component: 'NInput',
+        componentProps: {
+          placeholder: '请输入租户名称/编码/域名'
+        }
+      }
+    ],
+    inline: false,
+    size: 'small',
+    labelPlacement: 'left',
+    isFull: false
+  },
+  tableConfig: {
+    keyField: 'id',
+    title: '租户列表',
+    showAddButton: true,
+    scrollX: 1200
+  }
+});
+
+async function handleDelete(row: Recordable) {
+  setTableLoading(true);
+  await fetchDeleteTenant([row.id].join(','));
+  refresh();
+}
+
+const [registerModalForm, { openModal, closeModal, getFieldsValue, setFieldsValue, setModalLoading }] = useModalFrom({
+  modalConfig: {
+    title: '租户',
+    width: 800,
+    isShowHeaderText: true
+  },
+  formConfig: {
+    schemas: [
+      {
+        field: 'id',
+        label: '',
+        component: 'NInput',
+        show: false
+      },
+      // 基本信息
+      {
+        field: 'tenantName',
+        label: '企业名称',
+        component: 'NInput',
+        required: true,
+        componentProps: {
+          placeholder: '请输入企业名称'
+        }
+      },
+      {
+        field: 'contactName',
+        label: '联系人',
+        component: 'NInput',
+        required: true,
+        componentProps: {
+          placeholder: '请输入联系人'
+        }
+      },
+      {
+        field: 'contactMobile',
+        label: '联系电话',
+        component: 'NInput',
+        required: true,
+        componentProps: {
+          placeholder: '请输入联系电话',
+          maxlength: 11
+        }
+      },
+      // 管理员信息
+      {
+        field: 'adminUsername',
+        label: '管理员账号',
+        component: 'NInput',
+        required: true,
+        componentProps: {
+          placeholder: '请输入管理员账号'
+        }
+      },
+      {
+        field: 'adminPassword',
+        label: '管理员密码',
+        component: 'NInput',
+        required: true,
+        componentProps: {
+          placeholder: '请输入管理员密码',
+          type: 'password',
+          showPasswordOn: 'click'
+        }
+      },
+      // 租户设置
+      {
+        field: 'packageId',
+        label: '租户套餐',
+        component: 'ApiSelect',
+        required: true,
+        componentProps: {
+          api: () => fetchGetTenantPackageList({ pageNum: 1, pageSize: 100 }),
+          labelFeild: 'packageName',
+          valueFeild: 'id',
+          resultFeild: 'data.list',
+          placeholder: '请选择租户套餐'
+        }
+      },
+      {
+        field: 'tenantCode',
+        label: '租户编号',
+        component: 'NInput',
+        required: true,
+        componentProps: {
+          placeholder: '请输入租户编号'
+        }
+      },
+      {
+        field: 'expireTime',
+        label: '过期时间',
+        component: 'NDatePicker',
+        componentProps: {
+          type: 'datetime',
+          placeholder: '选择日期时间'
+        } as any
+      },
+      {
+        field: 'tenantDomain',
+        label: '绑定域名',
+        component: 'NInput',
+        required: true,
+        componentProps: {
+          placeholder: '请输入'
+        }
+      },
+      {
+        field: 'status',
+        label: '租户状态',
+        component: 'NRadioGroup',
+        required: true,
+        componentProps: {
+          options: [
+            {
+              label: '正常',
+              value: 1
+            },
+            {
+              label: '停用',
+              value: 0
+            }
+          ]
+        },
+        defaultValue: 1
+      }
+    ],
+    labelWidth: 120,
+    gridProps: {
+      cols: '1'
+    }
+  }
+});
+
+async function handleSubmit() {
+  const form = await getFieldsValue();
+  if (form.id) {
+    const { error } = await fetchEditTenant(form);
+    if (!error) {
+      closeModal();
+      refresh();
+    }
+  } else {
+    const { error } = await fetchAddTenant(form);
+    if (!error) {
+      closeModal();
+      refresh();
+    }
+  }
+}
+
+async function edit(row: Recordable) {
+  setModalLoading(true);
+  openModal(row);
+  setFieldsValue({ ...row });
+  setModalLoading(false);
+}
+</script>
+
+<template>
+  <LayoutTable>
+    <ZTable :columns="columns as any" :api="fetchGetTenantList" @register="registerTable" @add="openModal">
+      <template #op="{ row }">
+        <NButton size="small" ghost type="primary" @click="edit(row)">编辑</NButton>
+        <NPopconfirm @positive-click="handleDelete(row)">
+          <template #trigger>
+            <NButton size="small" type="error" ghost>删除</NButton>
+          </template>
+          确定删除吗?
+        </NPopconfirm>
+      </template>
+    </ZTable>
+    <BasicModelForm @register-modal-form="registerModalForm" @submit-form="handleSubmit"></BasicModelForm>
+  </LayoutTable>
+</template>
+
+<style scoped></style>

部分文件因文件數量過多而無法顯示