Browse Source

feat: prj init final

叶静 1 month ago
parent
commit
a639c4ecdb
100 changed files with 5433 additions and 10468 deletions
  1. 16 0
      .promptx/pouch.json
  2. 2 2
      .promptx/resource/project.registry.json
  3. 101 0
      src/app/notice/api/index.js
  4. 647 0
      src/app/notice/index.vue
  5. 0 84
      src/app/shop/admin/activity/activity.service.js
  6. 0 73
      src/app/shop/admin/activity/activity/components/free-shipping.vue
  7. 0 235
      src/app/shop/admin/activity/activity/components/full-gift.vue
  8. 0 112
      src/app/shop/admin/activity/activity/components/full-reduce-discount.vue
  9. 0 284
      src/app/shop/admin/activity/activity/components/groupon.vue
  10. 0 14
      src/app/shop/admin/activity/activity/components/index.scss
  11. 0 81
      src/app/shop/admin/activity/activity/components/seckill.vue
  12. 0 211
      src/app/shop/admin/activity/activity/components/signin.vue
  13. 0 457
      src/app/shop/admin/activity/activity/data.js
  14. 0 405
      src/app/shop/admin/activity/activity/edit.vue
  15. 0 136
      src/app/shop/admin/activity/activity/index.vue
  16. 0 343
      src/app/shop/admin/activity/activity/list.vue
  17. 0 72
      src/app/shop/admin/activity/activity/recyclebin.vue
  18. 0 77
      src/app/shop/admin/activity/activity/select.vue
  19. 0 245
      src/app/shop/admin/activity/activity/sku.vue
  20. 0 194
      src/app/shop/admin/activity/groupon/detail.vue
  21. 0 257
      src/app/shop/admin/activity/groupon/index.vue
  22. 3 10
      src/app/shop/admin/category/category.service.js
  23. 49 646
      src/app/shop/admin/category/edit.vue
  24. 155 110
      src/app/shop/admin/category/index.vue
  25. 0 78
      src/app/shop/admin/category/select.vue
  26. 0 89
      src/app/shop/admin/config/config.service.js
  27. 0 52
      src/app/shop/admin/config/index.vue
  28. 17 0
      src/app/shop/admin/content/banner/banner.service.js
  29. 128 0
      src/app/shop/admin/content/banner/edit.vue
  30. 260 0
      src/app/shop/admin/content/banner/index.vue
  31. 114 0
      src/app/shop/admin/content/notification/edit.vue
  32. 247 0
      src/app/shop/admin/content/notification/index.vue
  33. 17 0
      src/app/shop/admin/content/notification/notification.service.js
  34. 121 0
      src/app/shop/admin/content/sms/edit.vue
  35. 249 0
      src/app/shop/admin/content/sms/index.vue
  36. 17 0
      src/app/shop/admin/content/sms/sms.service.js
  37. 0 109
      src/app/shop/admin/data/area/edit.vue
  38. 0 203
      src/app/shop/admin/data/area/index.vue
  39. 0 171
      src/app/shop/admin/data/area/select.vue
  40. 0 107
      src/app/shop/admin/data/data.service.js
  41. 0 90
      src/app/shop/admin/data/express/edit.vue
  42. 0 57
      src/app/shop/admin/data/express/express.js
  43. 0 260
      src/app/shop/admin/data/express/index.vue
  44. 0 121
      src/app/shop/admin/data/fakeUser/edit.vue
  45. 0 341
      src/app/shop/admin/data/fakeUser/index.vue
  46. 0 56
      src/app/shop/admin/data/fakeUser/random.vue
  47. 0 110
      src/app/shop/admin/data/fakeUser/select.vue
  48. 0 97
      src/app/shop/admin/data/faq/edit.vue
  49. 0 256
      src/app/shop/admin/data/faq/index.vue
  50. 0 90
      src/app/shop/admin/data/page/edit.vue
  51. 0 211
      src/app/shop/admin/data/page/index.vue
  52. 0 387
      src/app/shop/admin/data/page/select.vue
  53. 265 0
      src/app/shop/admin/data/report/index.vue
  54. 27 0
      src/app/shop/admin/data/report/report.service.js
  55. 0 89
      src/app/shop/admin/data/richtext/edit.vue
  56. 0 229
      src/app/shop/admin/data/richtext/index.vue
  57. 0 136
      src/app/shop/admin/data/richtext/select.vue
  58. 0 33
      src/app/shop/admin/dispatch/dispatch.service.js
  59. 0 347
      src/app/shop/admin/dispatch/edit.vue
  60. 0 274
      src/app/shop/admin/dispatch/index.vue
  61. 0 16
      src/app/shop/admin/feedback/feedback.service.js
  62. 0 283
      src/app/shop/admin/feedback/index.vue
  63. 27 0
      src/app/shop/admin/finance/commission/commission.service.js
  64. 142 0
      src/app/shop/admin/finance/commission/edit.vue
  65. 289 0
      src/app/shop/admin/finance/commission/index.vue
  66. 115 0
      src/app/shop/admin/finance/recharge/edit.vue
  67. 284 0
      src/app/shop/admin/finance/recharge/index.vue
  68. 27 0
      src/app/shop/admin/finance/recharge/recharge.service.js
  69. 126 0
      src/app/shop/admin/finance/withdraw/edit.vue
  70. 317 0
      src/app/shop/admin/finance/withdraw/index.vue
  71. 37 0
      src/app/shop/admin/finance/withdraw/withdraw.service.js
  72. 0 246
      src/app/shop/admin/goods/comment/edit.vue
  73. 0 210
      src/app/shop/admin/goods/comment/fakeComment.vue
  74. 0 57
      src/app/shop/admin/goods/comment/fakeUser.js
  75. 0 503
      src/app/shop/admin/goods/comment/index.vue
  76. 0 184
      src/app/shop/admin/goods/comment/recyclebin.vue
  77. 0 50
      src/app/shop/admin/goods/comment/reply.vue
  78. 1 113
      src/app/shop/admin/goods/goods.service.js
  79. 2 16
      src/app/shop/admin/goods/goods/index.vue
  80. 0 82
      src/app/shop/admin/goods/goods/search.vue
  81. 0 86
      src/app/shop/admin/goods/service/edit.vue
  82. 0 155
      src/app/shop/admin/goods/stockLog.vue
  83. 0 65
      src/app/shop/admin/goods/stockWarning/addStock.vue
  84. 0 203
      src/app/shop/admin/goods/stockWarning/index.vue
  85. 0 76
      src/app/shop/admin/goods/stockWarning/recyclebin.vue
  86. 132 0
      src/app/shop/admin/marketing/group/edit.vue
  87. 17 0
      src/app/shop/admin/marketing/group/group.service.js
  88. 252 0
      src/app/shop/admin/marketing/group/index.vue
  89. 146 0
      src/app/shop/admin/order/setting/edit.vue
  90. 235 0
      src/app/shop/admin/order/setting/index.vue
  91. 17 0
      src/app/shop/admin/order/setting/setting.service.js
  92. 106 0
      src/app/shop/admin/user/level/edit.vue
  93. 251 0
      src/app/shop/admin/user/level/index.vue
  94. 17 0
      src/app/shop/admin/user/level/level.service.js
  95. 89 0
      src/app/shop/admin/user/list/edit.vue
  96. 254 0
      src/app/shop/admin/user/list/index.vue
  97. 17 0
      src/app/shop/admin/user/list/list.service.js
  98. 26 37
      src/app/shop/admin/user/tag/edit.vue
  99. 55 45
      src/app/shop/admin/user/tag/index.vue
  100. 17 0
      src/app/shop/admin/user/tag/tag.service.js

+ 16 - 0
.promptx/pouch.json

@@ -0,0 +1,16 @@
+{
+  "currentState": "initialized",
+  "stateHistory": [
+    {
+      "from": "initial",
+      "command": "init",
+      "timestamp": "2025-07-09T02:57:45.095Z",
+      "args": [
+        {
+          "workingDirectory": "d:\\work\\bandhu-buy\\admin"
+        }
+      ]
+    }
+  ],
+  "lastUpdated": "2025-07-09T02:57:45.129Z"
+}

+ 2 - 2
.promptx/resource/project.registry.json

@@ -4,8 +4,8 @@
   "metadata": {
     "version": "2.0.0",
     "description": "project 级资源注册表",
-    "createdAt": "2025-07-07T09:33:02.225Z",
-    "updatedAt": "2025-07-07T09:33:02.226Z",
+    "createdAt": "2025-07-09T02:57:45.127Z",
+    "updatedAt": "2025-07-09T02:57:45.127Z",
     "resourceCount": 0
   },
   "resources": [],

+ 101 - 0
src/app/notice/api/index.js

@@ -0,0 +1,101 @@
+import { request } from '@/sheep/request';
+import { isEmpty } from 'lodash';
+import { CRUD } from '@/sheep/request/crud';
+
+export default {
+  // 站内信分类
+  notificationType: () =>
+    request({
+      url: 'admin/notification/notificationType',
+    }),
+
+  // 即时通讯配置
+  chatInit: () =>
+    request({
+      url: 'chat/admin/index/init',
+    }),
+
+  // 站内信列表
+  notifications: (params) =>
+    request({
+      url: 'admin/auth/admin/notifications',
+      method: 'GET',
+      params,
+    }),
+
+  read: (id) =>
+    request({
+      url: `admin/auth/admin/notification/${id}`,
+      method: 'PUT',
+    }),
+  clear: () =>
+    request({
+      url: `admin/auth/admin/notifications`,
+      method: 'DELETE',
+    }),
+
+  // 系统设置
+  config: {
+    // 客服配置
+    basic: (data) =>
+      request({
+        url: 'chat/admin/config/basic',
+        method: isEmpty(data) ? 'GET' : 'PUT',
+        data,
+        options: {
+          showSuccessMessage: !isEmpty(data),
+        },
+      }),
+    // 系统配置
+    system: (data) =>
+      request({
+        url: 'chat/admin/config/system',
+        method: isEmpty(data) ? 'GET' : 'PUT',
+        data,
+        options: {
+          showSuccessMessage: !isEmpty(data),
+        },
+      }),
+    // 应用配置
+    application: (data) =>
+      request({
+        url: 'chat/admin/config/application',
+        method: isEmpty(data) ? 'GET' : 'PUT',
+        data,
+        options: {
+          showSuccessMessage: !isEmpty(data),
+        },
+      }),
+  },
+
+  admin: {
+    // 常用语
+    commonWord: {
+      ...CRUD('chat/admin/commonWord'),
+    },
+    // 客服列表
+    customerService: {
+      ...CRUD('chat/admin/customerService'),
+      select: (params) =>
+        request({
+          url: 'chat/admin/customerService/select',
+          method: 'GET',
+          params,
+        }),
+    },
+    // 常见问题
+    question: {
+      ...CRUD('chat/admin/question'),
+    },
+    // 会话管理
+    user: {
+      ...CRUD('chat/admin/user'),
+      detailList: (params) =>
+        request({
+          url: 'chat/admin/record',
+          method: 'GET',
+          params,
+        }),
+    },
+  },
+};

+ 647 - 0
src/app/notice/index.vue

@@ -0,0 +1,647 @@
+<template>
+  <div class="sa-notice" @click="showNotice">
+    <div class="sa-float-icon-wrap">
+      <el-badge is-dot :hidden="!noticeUnreadNum">
+        <sa-svg class="sa-float-icon" name="sa-Notification" size="24" />
+      </el-badge>
+    </div>
+
+    <el-drawer
+      v-model="isShowNotice"
+      direction="rtl"
+      class="chat-drawer"
+      modal-class="chat-drawer-overlay"
+      :show-close="false"
+    >
+      <template #header>
+        <ul v-if="noticeTypeList.length" class="chat-status-wrap sa-flex sa-col-center">
+          <li class="bg" :style="bgStyle"></li>
+          <!-- {{noticeTypeList}} -->
+          <li
+            v-for="item in noticeTypeList"
+            :key="item"
+            class="chat-status"
+            :class="{ 'is-active': item.value == noticeTypeList[0].value }"
+            @click="onNoticeMenu(item.value)"
+          >
+            <el-badge is-dot :hidden="!item.unread_num">
+              {{ item.label }}
+            </el-badge>
+          </li>
+          <!-- <li
+              class="chat-status"
+              :class="{ 'is-active': sessionType == 'ing' }"
+              @click="changeSessionType('ing')"
+            >
+              会话中
+            </li>
+            <el-badge is-dot :hidden="!hasWaiting">
+              <li
+                class="chat-status"
+                :class="{ 'is-active': sessionType == 'waiting' }"
+                @click="changeSessionType('waiting')"
+              >
+                排队中
+              </li>
+            </el-badge>
+            <li
+              class="chat-status"
+              :class="{ 'is-active': sessionType == 'history' }"
+              @click="changeSessionType('history')"
+            >
+              历史记录
+            </li> -->
+        </ul>
+        <el-icon class="close" @click="isShowNotice = false">
+          <CircleCloseFilled />
+        </el-icon>
+        <!-- <el-menu
+          v-if="noticeTypeList.length"
+          class="el-menu-demo sa-flex sa-row-around sa-col-center"
+          mode="horizontal"
+          :default-active="noticeTypeList[0].value"
+          @select="onNoticeMenu"
+        >
+          <template v-for="(item, index) in noticeTypeList" :key="index">
+            <el-menu-item :index="item.value">
+              <div class="sa-flex sa-col-center sa-row-center">
+                <span>{{ item.label }}</span>
+                <span class="sa-m-l-4" v-if="item.unread_num"
+                  >({{ item.unread_num }})</span
+                >
+              </div>
+            </el-menu-item>
+          </template>
+        </el-menu> -->
+      </template>
+      <div class="chat-content sa-flex sa-flex-1">
+        <el-scrollbar id="noticeScroll" class="notice-scroll" height="100%">
+          <div
+            class="notice-list sa-flex sa-row-around"
+            :class="isEmpty(item.read_time) ? '' : 'notice-list-read'"
+            v-for="(item, index) in noticeList"
+            :key="item.id"
+            @click="readNotice(item, index)"
+          >
+            <div class="notice-list-content">
+              <div class="wrapper">
+                <input :id="`exp-${index}`" class="exp" type="checkbox" />
+                <div class="text">
+                  <label class="btn" :for="`exp-${index}`"></label>
+                  【{{ item.data.message_title }}】{{ item.data.message_text }}
+                </div>
+              </div>
+              <span class="notice-time">{{ item.create_time }}</span>
+            </div>
+          </div>
+
+          <!-- 置空页 -->
+          <el-empty v-show="!noticeList.length">
+            <template #image>
+              <sa-svg className="empty-svg" name="sa-neirongweikong" size="150"></sa-svg>
+            </template>
+            <template #description>
+              <div class="empty-description"> 您的工作效率很高哦, 现在还没有新的待办消息! </div>
+            </template>
+          </el-empty>
+          <!-- 加载状态 -->
+          <button
+            class="loadmore-btn sa-reset-button"
+            v-show="noticeList.length"
+            @click="onLoadMore"
+          >
+            {{ loadingMap[noticeListparmas.loadStatus].title
+            }}<i
+              class="loadmore-icon sa-m-l-6"
+              :class="loadingMap[noticeListparmas.loadStatus].icon"
+            ></i>
+          </button>
+        </el-scrollbar>
+      </div>
+      <div class="chat-footer">
+        <div class="empty" @click="clearNotice()">清空 已读消息</div>
+      </div>
+    </el-drawer>
+  </div>
+</template>
+<script>
+  /**
+   * Botice 站内信
+   *
+   */
+  import { reactive, ref, defineComponent, computed } from 'vue';
+  import { isEmpty } from 'lodash';
+
+  export default defineComponent({
+    name: 'SaNotice',
+    components: {},
+    setup() {
+      // 默认数据
+      const noticeTypeList = ref([
+        { label: '系统消息', value: 'system', unread_num: 3 },
+        { label: '订单消息', value: 'order', unread_num: 1 },
+        // { label: '营销消息', value: 'marketing', unread_num: 0 },
+      ]);
+
+      const noticeList = ref([
+        {
+          id: 1,
+          data: {
+            message_title: '系统通知',
+            message_text: '欢迎使用商城管理系统,您有新的订单需要处理。',
+          },
+          create_time: '2024-01-15 10:30:00',
+          read_time: null,
+        },
+        {
+          id: 2,
+          data: {
+            message_title: '订单提醒',
+            message_text: '您有一个新的订单等待确认,订单号:#202401150001',
+          },
+          create_time: '2024-01-15 09:15:00',
+          read_time: '2024-01-15 10:00:00',
+        },
+        {
+          id: 3,
+          data: {
+            message_title: '库存警告',
+            message_text: '商品"BOLON经典太阳镜"库存不足,当前库存:5件',
+          },
+          create_time: '2024-01-15 08:45:00',
+          read_time: null,
+        },
+      ]);
+
+      const noticeUnreadNum = computed(() => {
+        return noticeList.value.filter((item) => !item.read_time).length;
+      });
+
+      // loading
+      const noticeLoading = ref(false);
+      // 查看更多
+      const onLoadMore = () => {
+        if (noticeListparmas.current_page < noticeListparmas.last_page) {
+          noticeListparmas.current_page += 1;
+          // getNoticeList(); // 暂时注释掉API调用
+        }
+      };
+      const isShowNotice = ref(false);
+
+      // 获取站内信列表
+      const noticeListparmas = reactive({
+        current_page: 1,
+        last_page: 1,
+        loadStatus: 'nomore', //loadmore-加载前的状态,loading-加载中的状态,nomore-没有更多的状态
+      });
+
+      // 暂时注释掉API调用
+      // const getNoticeList = async () => {
+      //   noticeLoading.value = true;
+      //   noticeListparmas.loadStatus = 'loading';
+      //   // API调用逻辑
+      //   noticeLoading.value = false;
+      // };
+
+      // 获取站内信分类
+      const curNoticeType = ref('system');
+
+      // 暂时注释掉API调用
+      // const getNoticeType = async () => {
+      //   // API调用逻辑
+      // };
+
+      // 显示站内信
+      const showNotice = () => {
+        isShowNotice.value = true;
+        noticeListparmas.current_page = 1;
+        // 暂时不调用API
+        // getNoticeList();
+      };
+
+      const bgStyle = computed(() => {
+        let index = noticeTypeList.value.findIndex((item) => item.value == curNoticeType.value);
+        return {
+          left: 2 + index * 118 + 'px',
+        };
+      });
+
+      // 切换站内信
+      const onNoticeMenu = (e) => {
+        curNoticeType.value = e;
+        noticeListparmas.current_page = 1;
+        // 暂时不调用API
+        // getNoticeList();
+      };
+
+      function desc(item) {
+        let str = `【${item.data.message_title}】${item.data.message_text}`;
+        if (str.length > 42) {
+          item.show = true;
+        }
+        return item.hidden ? str : str.substring(0, 42) + '...';
+      }
+
+      async function readNotice(item) {
+        // 模拟标记为已读
+        item.read_time = new Date().toLocaleString();
+        // 暂时注释掉API调用
+        // const { data } = await noticeApi.read(item.id);
+      }
+
+      const clearNotice = async () => {
+        // 清空已读消息
+        noticeList.value = noticeList.value.filter((item) => !item.read_time);
+        noticeListparmas.current_page = 1;
+        // 暂时注释掉API调用
+        // const { data } = await noticeApi.clear();
+      };
+
+      // 暂时注释掉初始化API调用
+      // onMounted(() => {
+      //   getNoticeType();
+      // });
+
+      return {
+        isEmpty,
+        noticeTypeList,
+        noticeUnreadNum,
+        noticeList,
+        onLoadMore,
+        noticeLoading,
+        curNoticeType,
+        onNoticeMenu,
+        noticeListparmas,
+        loadingMap: {
+          loadmore: {
+            title: '查看更多',
+            icon: 'el-icon-d-arrow-left',
+          },
+          nomore: {
+            title: '没有更多了',
+            icon: '',
+          },
+          loading: {
+            title: '加载中... ',
+            icon: 'el-icon-loading',
+          },
+        },
+        showNotice,
+        isShowNotice,
+        bgStyle,
+        desc,
+        readNotice,
+        clearNotice,
+      };
+    },
+  });
+</script>
+<style>
+  .notice-popper {
+    padding: 0 !important;
+  }
+</style>
+<style lang="scss" scoped>
+  .sa-notice {
+    :deep() {
+      .el-badge__content.is-dot {
+        height: 6px;
+        width: 6px;
+      }
+    }
+  }
+  .empty-svg {
+    color: var(--sa-subfont);
+  }
+  .empty-description {
+    width: 200px;
+    font-size: 16px;
+    color: var(--sa-subfont);
+  }
+  // 按钮
+  .tools-btn {
+    border: none;
+    height: 32px;
+    min-height: 32px;
+    width: 32px;
+    padding: 0;
+    border-radius: 50%;
+    &:active {
+      background-color: var(--t-btn-hover);
+      .tool-icon {
+        font-size: 20px;
+        color: var(--sa-background-assist);
+      }
+    }
+    &:hover {
+      background-color: var(--t-btn-hover);
+      .tool-icon {
+        font-size: 20px;
+        color: var(--sa-background-assist);
+      }
+    }
+    &:focus {
+      background-color: var(--t-btn-hover);
+      .tool-icon {
+        font-size: 20px;
+        color: var(--sa-background-assist);
+      }
+    }
+    .tool-icon {
+      font-size: 20px;
+      color: var(--sa-font);
+    }
+    :deep(.el-badge__content) {
+      background-color: #ed5b56 !important;
+    }
+  }
+  // 站内信
+  .notice-wrap {
+    .notice-header {
+      height: 60px;
+      border-bottom: 1px solid var(--sa-border);
+    }
+    .el-menu {
+      height: 100%;
+    }
+    .el-menu--horizontal {
+      border-bottom: none;
+    }
+    .el-menu:not(.el-menu--collapse) .el-menu-item,
+    .el-menu:not(.el-menu--collapse) .el-sub-menu__title {
+      height: 100%;
+      border-radius: 0;
+      margin-bottom: 0;
+    }
+
+    // .notice-list {
+    //   .notice-list-content {
+    //     width: 250px;
+    //     border-bottom: 1px solid var(--sa-space);
+    //     .notice-text {
+    //       font-family: PingFang SC;
+    //       font-size: 14px;
+    //       color: var(--sa-font);
+    //       line-height: 20px;
+    //     }
+    //     .notice-time {
+    //       font-family: PingFang SC;
+    //       font-size: 12px;
+    //       color: var(--sa-subfont);
+    //     }
+    //   }
+    // }
+  }
+  // 加载更多
+  .loadmore-btn {
+    width: 100%;
+    height: 40px;
+    font-size: 12px;
+    color: var(--sa-subfont);
+    .loadmore-icon {
+      transform: rotate(-90deg);
+    }
+  }
+
+  .notice-scroll {
+    width: 100%;
+    //   height: 600px;
+  }
+  @media all and (min-width: 0) and (max-width: 500px) {
+    // .notice-scroll {
+    //   height: 520px;
+    // }
+  }
+
+  :deep() {
+    .chat-drawer-overlay {
+      background-color: transparent;
+    }
+    .chat-drawer {
+      width: 360px !important;
+      height: unset;
+      border-radius: 8px;
+      top: 48px;
+      bottom: 30px;
+      @media only screen and (max-width: 768px) {
+        width: 100% !important;
+        border-radius: 0;
+        top: 0;
+        bottom: 0;
+      }
+      .el-drawer__header {
+        height: 56px;
+        padding: 0 12px;
+        background: var(--t-btn-hover);
+        color: var(--sa-background-assist);
+        margin-bottom: 0;
+        justify-content: center;
+        position: relative;
+        .close {
+          position: absolute;
+          top: 20px;
+          right: 12px;
+          font-size: 16px;
+          &:hover {
+            color: var(--t-color-primary);
+          }
+          @media only screen and (max-width: 768px) {
+            top: 18px;
+            font-size: 20px;
+          }
+        }
+      }
+      .el-drawer__body {
+        padding: 0;
+        overflow: hidden;
+        display: flex;
+        flex-direction: column;
+
+        .chat-content {
+          flex: 1;
+          height: calc(100% - 40px);
+
+          .notice-list {
+            .notice-list-content {
+              // width: 250px;
+              width: 100%;
+              padding: 16px 16px;
+              border-bottom: 1px solid var(--sa-space);
+
+              &:hover {
+                background: var(--t-bg-hover);
+
+                .text::after {
+                  content: '';
+                  width: 999vw;
+                  height: 999vw;
+                  position: absolute;
+                  box-shadow: inset calc(100px - 999vw) calc(21px - 999vw) 0 0 var(--t-bg-hover);
+                  margin-left: -100px;
+                }
+              }
+              .notice-text {
+                font-family: PingFang SC;
+                font-size: 14px;
+                color: var(--sa-font);
+                line-height: 20px;
+                position: relative;
+                // padding-right: 26px;
+                .notice-hidden {
+                  // position: absolute;
+                  // top: 20px;
+                  // right: 0;
+                  color: var(--t-color-primary);
+                  font-size: 12px;
+                }
+              }
+              .notice-time {
+                font-family: PingFang SC;
+                font-size: 12px;
+                color: var(--sa-subfont);
+              }
+
+              .wrapper {
+                display: flex;
+                width: inherit;
+                overflow: hidden;
+              }
+              .text {
+                font-size: 14px;
+                color: var(--sa-font);
+                overflow: hidden;
+                text-overflow: ellipsis;
+                text-align: justify;
+                position: relative;
+                line-height: 1.5;
+                max-height: 3em;
+                transition: 0.3s max-height;
+                white-space: normal;
+                word-break: break-all;
+              }
+              .text::before {
+                content: '';
+                height: calc(100% - 21px);
+                float: right;
+              }
+              .text::after {
+                content: '';
+                width: 999vw;
+                height: 999vw;
+                position: absolute;
+                box-shadow: inset calc(100px - 999vw) calc(21px - 999vw) 0 0 #fff;
+                margin-left: -100px;
+              }
+              .btn {
+                position: relative;
+                float: right;
+                clear: both;
+                margin-left: 20px;
+                font-size: 12px;
+                padding: 0 8px;
+                // background: #3f51b5;
+                line-height: 1.5;
+                border-radius: 4px;
+                color: var(--t-color-primary);
+                cursor: pointer;
+                /* margin-top: -30px; */
+              }
+              .btn::after {
+                content: '展开';
+              }
+              .exp {
+                display: none;
+              }
+              .exp:checked + .text {
+                max-height: none;
+              }
+              .exp:checked + .text::after {
+                visibility: hidden;
+              }
+              .exp:checked + .text .btn::before {
+                visibility: hidden;
+              }
+              .exp:checked + .text .btn::after {
+                content: '收起';
+              }
+              .btn::before {
+                content: '...';
+                position: absolute;
+                left: -5px;
+                color: var(--sa-font);
+                transform: translateX(-100%);
+              }
+            }
+
+            &.notice-list-read {
+              .text,
+              .notice-time {
+                color: var(--sa-place);
+              }
+              .btn::before {
+                color: var(--sa-place);
+              }
+            }
+          }
+        }
+        // 输入框
+        .chat-footer {
+          border-top: 1px solid var(--sa-space);
+          background-color: var(--sa-table-striped);
+          height: 40px;
+          display: flex;
+          align-items: center;
+          flex-shrink: 0;
+          .empty,
+          .more {
+            flex: 1;
+            text-align: center;
+            font-size: 12px;
+            font-weight: 400;
+            color: var(--sa-font);
+          }
+          .line {
+            width: 1px;
+            height: 20px;
+            background: var(--sa-border);
+          }
+        }
+      }
+    }
+  }
+  .chat-status-wrap {
+    width: fit-content !important;
+    flex: unset !important;
+    height: 36px;
+    padding: 0 2px;
+    border-radius: 8px;
+    background: var(--t-bg-focus);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: relative;
+    .bg {
+      position: absolute;
+      top: 2px;
+      left: 2px;
+      width: 118px;
+      height: 32px;
+      background: var(--sa-background-assist);
+      border-radius: 6px;
+      transition: all 0.2s;
+    }
+    .chat-status {
+      min-width: 118px;
+      height: 32px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      position: relative;
+      z-index: 1;
+      padding: 0 7px;
+      font-size: 14px;
+      color: var(--t-color-primary);
+      cursor: pointer;
+    }
+  }
+</style>

+ 0 - 84
src/app/shop/admin/activity/activity.service.js

@@ -1,84 +0,0 @@
-import Content from '@/sheep/layouts/content.vue';
-import { request } from '@/sheep/request';
-import { CRUD } from '@/sheep/request/crud';
-
-const route = {
-  path: 'activity',
-  name: 'shop.admin.activity',
-  component: Content,
-  meta: {
-    title: '营销',
-  },
-  children: [
-    {
-      path: 'activity',
-      name: 'shop.admin.activity.activity',
-      component: () => import('./activity/index.vue'),
-      meta: {
-        title: '营销活动',
-      },
-    },
-    {
-      path: 'list',
-      name: 'shop.admin.activity.list',
-      component: () => import('./activity/list.vue'),
-      meta: {
-        title: '营销活动',
-      },
-    },
-    {
-      path: 'groupon',
-      name: 'shop.admin.activity.groupon',
-      component: () => import('./groupon/index.vue'),
-      meta: {
-        title: '拼团列表',
-      },
-    },
-  ],
-};
-
-const api = {
-  activity: {
-    ...CRUD('shop/admin/activity/activity', ['detail', 'add', 'edit', 'delete']),
-    list: (type, params) =>
-      request({
-        url: `shop/admin/activity/activity/list/${type}`,
-        method: 'GET',
-        params,
-      }),
-    skus: (params) =>
-      request({
-        url: 'shop/admin/activity/activity/skus',
-        method: 'GET',
-        params,
-      }),
-    recyclebin: (type, params) =>
-      request({
-        url: `shop/admin/activity/activity/recyclebin/${type}`,
-        method: 'GET',
-        params,
-      }),
-    select: (type, params) =>
-      request({
-        url: `shop/admin/activity/activity/select/${type}`,
-        method: 'GET',
-        params,
-      }),
-  },
-  groupon: {
-    ...CRUD('shop/admin/activity/groupon', ['list', 'detail']),
-    addUser: (id, data) =>
-      request({
-        url: 'shop/admin/activity/groupon/addUser/' + id,
-        method: 'POST',
-        data,
-      }),
-    invalid: (id) =>
-      request({
-        url: 'shop/admin/activity/groupon/invalid/' + id,
-        method: 'POST',
-      }),
-  },
-};
-
-export { route, api };

+ 0 - 73
src/app/shop/admin/activity/activity/components/free-shipping.vue

@@ -1,73 +0,0 @@
-<template>
-  <div class="free-shipping">
-    <el-form-item label="优惠类型" prop="rules.type" required>
-      <el-radio-group v-model="activityDetail.model.rules.type">
-        <el-radio label="money">按消费金额包邮</el-radio>
-        <el-radio label="num">按购买件数包邮</el-radio>
-      </el-radio-group>
-    </el-form-item>
-    <div class="label-inner">
-      <el-form-item>
-        <el-form-item :label="`规则`" label-width="fit-content" required>
-          <el-form-item
-            class="is-no-asterisk"
-            label="消费满"
-            label-width="fit-content"
-            prop="rules.full_num"
-            :rules="activityDetail.rules.full_num"
-          >
-            <el-input class="sa-w-120" v-model="activityDetail.model.rules.full_num" type="number">
-              <template #append>
-                {{ activityDetail.model.rules.type == 'money' ? '元' : '件' }}
-              </template>
-            </el-input>
-            <span class="tip sa-m-l-12">满邮金额优惠按照商品实际金额计算</span>
-          </el-form-item>
-        </el-form-item>
-      </el-form-item>
-      <el-form-item>
-        <el-form-item label="不支持地区" label-width="fit-content" required>
-          <div>
-            <div class="sa-flex">
-              <template v-for="level in activityDetail.model.rules.district_text" :key="level">
-                <template v-for="name in level" :key="name">{{ name }},</template>
-              </template>
-            </div>
-            <el-button class="is-link" type="primary" @click="selectArea">添加地区</el-button>
-          </div>
-        </el-form-item>
-      </el-form-item>
-    </div>
-  </div>
-</template>
-<script setup>
-  import { useModal } from '@/sheep/hooks';
-  import AreaSelect from '../../../data/area/select.vue';
-  const props = defineProps(['activityDetail']);
-
-  function selectArea() {
-    useModal(
-      AreaSelect,
-      {
-        title: '选择地区',
-        selected: {
-          province: props.activityDetail.model.rules.province_except,
-          city: props.activityDetail.model.rules.city_except,
-          district: props.activityDetail.model.rules.district_except,
-        },
-      },
-      {
-        confirm: (res) => {
-          for (var level in res.data) {
-            let ids = [];
-            for (var id in res.data[level]) {
-              ids.push(id);
-            }
-            props.activityDetail.model.rules[level + '_except'] = ids.join(',');
-          }
-          props.activityDetail.model.rules.district_text = res.data;
-        },
-      },
-    );
-  }
-</script>

+ 0 - 235
src/app/shop/admin/activity/activity/components/full-gift.vue

@@ -1,235 +0,0 @@
-<template>
-  <div class="full-gift">
-    <el-form-item label="参与次数" required>
-      <el-form-item>
-        <el-radio-group v-model="limitNumType" @change="changeLimitNumType">
-          <el-radio label="all">不限制</el-radio>
-          <el-radio label="part">
-            每人最多可参与
-            <el-input
-              v-if="limitNumType == 'part'"
-              class="sa-w-120 sa-m-l-12"
-              v-model="activityDetail.model.rules.limit_num"
-            >
-              <template #append>次数</template>
-            </el-input>
-          </el-radio>
-        </el-radio-group>
-      </el-form-item>
-    </el-form-item>
-    <el-form-item label="赠送时机" prop="rules.event" required>
-      <el-radio-group v-model="activityDetail.model.rules.event">
-        <el-radio label="paid">支付完成</el-radio>
-        <el-radio label="confirm">
-          <div class="sa-flex">
-            确认收货
-            <el-popover :width="230" trigger="hover">
-              <div>必须全部确认收货才能满足条件</div>
-              <template #reference>
-                <el-icon class="warning sa-m-l-8">
-                  <Warning />
-                </el-icon>
-              </template>
-            </el-popover>
-          </div>
-        </el-radio>
-        <el-radio label="finish">交易完成</el-radio>
-      </el-radio-group>
-    </el-form-item>
-    <el-form-item label="优惠类型" prop="rules.type" required>
-      <el-radio-group v-model="activityDetail.model.rules.type">
-        <el-radio label="money">满足金额</el-radio>
-        <el-radio label="num">满足件数</el-radio>
-      </el-radio-group>
-    </el-form-item>
-    <div
-      class="label-inner"
-      v-for="(ditem, dindex) in activityDetail.model.rules.discounts"
-      :key="ditem"
-    >
-      <el-form-item>
-        <div class="rules-title">{{ `规则${dindex + 1}` }}</div>
-      </el-form-item>
-      <el-form-item>
-        <el-form-item
-          label="消费满"
-          label-width="fit-content"
-          :prop="'rules.discounts.' + dindex + '.full'"
-          :rules="activityDetail.rules.discounts.full"
-        >
-          <el-input class="sa-w-120" v-model="ditem.full" type="number">
-            <template #append>
-              {{ activityDetail.model.rules.type == 'money' ? '元' : '件' }}
-            </template>
-          </el-input>
-          <div class="tip sa-m-l-8" v-if="dindex == 0"> 满赠金额优惠按照商品实际金额计算 </div>
-          <el-button
-            v-if="dindex"
-            class="is-link sa-m-l-12"
-            type="danger"
-            @click="deleteDiscounts(dindex)"
-            >删除</el-button
-          >
-        </el-form-item>
-      </el-form-item>
-      <el-form-item>
-        <el-form-item
-          label="赠送单数"
-          label-width="fit-content"
-          :prop="'rules.discounts.' + dindex + '.gift_num'"
-          :rules="activityDetail.rules.discounts.gift_num"
-        >
-          <el-input class="sa-w-120" v-positiveinteger v-model="ditem.gift_num" type="number">
-            <template #append>单</template>
-          </el-input>
-          <div class="tip sa-m-l-8" v-if="dindex == 0">
-            该单数指赠品发放单数,如设置100单,那参与此活动的前100个订单可获取赠品
-          </div>
-        </el-form-item>
-      </el-form-item>
-      <el-form-item>
-        <el-form-item
-          label="赠送类型"
-          label-width="fit-content"
-          :prop="'rules.discounts.' + dindex + '.types'"
-          :rules="activityDetail.rules.discounts.types"
-        >
-          <el-checkbox-group v-model="ditem.types">
-            <el-checkbox label="coupon">优惠券</el-checkbox>
-            <el-checkbox label="score">积分</el-checkbox>
-            <el-checkbox label="money">余额</el-checkbox>
-          </el-checkbox-group>
-        </el-form-item>
-      </el-form-item>
-      <div class="sa-p-l-24">
-        <!-- 优惠券 -->
-        <el-form-item v-if="ditem.types.includes('coupon')">
-          <el-form-item label="优惠券" label-width="fit-content">
-            <div>
-              <el-button class="is-link" type="primary" @click="selectCoupon(dindex)"
-                >添加优惠券</el-button
-              >
-              <div class="sa-template-wrap" v-if="ditem.coupon_list.length > 0">
-                <div class="title sa-flex">
-                  <div class="key">名称</div>
-                  <div class="value">优惠内容</div>
-                  <div class="oper">操作</div>
-                </div>
-                <div>
-                  <div class="item" v-for="(element, index) in ditem.coupon_list" :key="element">
-                    <el-form-item class="key">
-                      <div class="sa-table-line-1">{{ element.name }}</div>
-                    </el-form-item>
-                    <el-form-item class="value">
-                      <div class="sa-table-line-1">
-                        {{ element.amount_text }}
-                      </div>
-                    </el-form-item>
-                    <el-form-item class="oper">
-                      <el-button class="is-link" type="danger" @click="deleteCoupon(dindex, index)">
-                        移除
-                      </el-button>
-                    </el-form-item>
-                  </div>
-                </div>
-              </div>
-            </div>
-          </el-form-item>
-        </el-form-item>
-        <!-- 积分 -->
-        <el-form-item v-if="ditem.types.includes('score')">
-          <el-form-item label="积分" label-width="fit-content">
-            <el-input class="sa-w-120" v-positiveinteger v-model="ditem.score" type="number">
-              <template #append>积分</template>
-            </el-input>
-          </el-form-item>
-        </el-form-item>
-        <!-- 余额 -->
-        <el-form-item v-if="ditem.types.includes('money')">
-          <el-form-item label="余额" label-width="fit-content">
-            <el-input class="sa-w-120" v-model="ditem.money" type="number">
-              <template #append>元</template>
-            </el-input>
-          </el-form-item>
-        </el-form-item>
-      </div>
-    </div>
-    <el-form-item>
-      <el-button class="is-link" type="primary" @click="addDiscounts">+ 添加优惠</el-button>
-    </el-form-item>
-  </div>
-</template>
-<script setup>
-  import { ref, watch } from 'vue';
-  import { useModal } from '@/sheep/hooks';
-  import CouponSelect from '../../../coupon/select.vue';
-  const props = defineProps(['activityDetail']);
-
-  const limitNumType = ref('all');
-  watch(
-    () => props.activityDetail,
-    () => {
-      limitNumType.value = props.activityDetail.model.rules.limit_num > 0 ? 'part' : 'all';
-    },
-    {
-      deep: true,
-    },
-  );
-  function changeLimitNumType() {
-    if (limitNumType.value == 'all') {
-      props.activityDetail.model.rules.limit_num = 0;
-    } else if (limitNumType.value == 'part') {
-      props.activityDetail.model.rules.limit_num = '';
-    }
-  }
-
-  function addDiscounts() {
-    props.activityDetail.model.rules.discounts.push({
-      full: '',
-      gift_num: '',
-      types: [],
-      coupon_ids: '',
-      total: '',
-      coupon_list: [],
-      score: '',
-      money: '',
-      // goods_ids:"",
-    });
-  }
-  function deleteDiscounts(index) {
-    props.activityDetail.model.rules.discounts.splice(index, 1);
-  }
-
-  function selectCoupon(index) {
-    useModal(
-      CouponSelect,
-      {
-        title: '选择优惠券',
-        status: 'hidden',
-        multiple: true,
-      },
-      {
-        confirm: (res) => {
-          props.activityDetail.model.rules.discounts[index].coupon_list.push(...res.data);
-        },
-      },
-    );
-  }
-  function deleteCoupon(index, dindex) {
-    props.activityDetail.model.rules.discounts[index].coupon_list.splice(dindex, 1);
-  }
-</script>
-
-<style lang="scss" scoped>
-  .rules-title {
-    width: 100%;
-    max-width: 360px;
-    height: 32px;
-    line-height: 32px;
-    padding: 0 16px;
-    border-radius: 4px;
-    background: var(--sa-table-header-bg);
-    font-size: 12px;
-    color: var(--sa-subtitle);
-  }
-</style>

+ 0 - 112
src/app/shop/admin/activity/activity/components/full-reduce-discount.vue

@@ -1,112 +0,0 @@
-<template>
-  <div class="full-reduce-discount">
-    <el-form-item label="优惠类型" prop="rules.type" required>
-      <el-radio-group v-model="activityDetail.model.rules.type">
-        <el-radio label="money">消费金额</el-radio>
-        <el-radio label="num">购买件数</el-radio>
-      </el-radio-group> </el-form-item
-    ><template v-for="(ditem, dindex) in activityDetail.model.rules.discounts" :key="ditem">
-      <el-form-item class="el-form-item--label-right">
-        <el-form-item class="el-form-item__label-auto" :label="`规则${dindex + 1}`" required>
-          <el-form-item
-            class="el-form-item--label-left is-no-asterisk"
-            label="消费满"
-            :prop="'rules.discounts.' + dindex + '.full'"
-            :rules="activityDetail.rules.discounts.full"
-          >
-            <el-input class="sa-w-120 sa-m-r-12" v-model="ditem.full" type="number">
-              <template #append>
-                {{ activityDetail.model.rules.type == 'money' ? '元' : '件' }}
-              </template>
-            </el-input>
-          </el-form-item>
-          <el-form-item
-            class="el-form-item--label-left is-no-asterisk"
-            :label="`${activityDetail.model.type == 'full_reduce' ? '优惠' : '折扣'}`"
-            :prop="'rules.discounts.' + dindex + '.discount'"
-            :rules="tempRules.discount"
-          >
-            <el-input class="sa-w-120" v-model="ditem.discount" type="number">
-              <template #append>
-                {{ activityDetail.model.type == 'full_reduce' ? '元' : '折' }}
-              </template>
-            </el-input>
-            <div
-              class="tip sa-m-l-12"
-              v-if="activityDetail.model.rules.type == 'money' && dindex == 0"
-            >
-              {{
-                activityDetail.model.type == 'full_reduce' ? '满减' : '满折'
-              }}金额优惠按照商品实际金额计算
-            </div>
-            <el-button
-              v-if="dindex"
-              class="is-link sa-m-l-12"
-              type="danger"
-              @click="deleteDiscounts(dindex)"
-            >
-              删除
-            </el-button>
-          </el-form-item>
-        </el-form-item>
-      </el-form-item>
-    </template>
-    <el-form-item>
-      <el-button
-        v-if="
-          activityDetail.model.rules.discounts && activityDetail.model.rules.discounts.length < 5
-        "
-        class="is-link"
-        type="primary"
-        @click="addDiscounts"
-        >+ 添加优惠</el-button
-      >
-    </el-form-item>
-  </div>
-</template>
-<script setup>
-  const props = defineProps(['activityDetail']);
-
-  const tempRules = {
-    discount: [
-      {
-        validator: (rule, value, callback) => {
-          let index = rule.field.split('.')[2];
-          if (value > 0) {
-            // 满额立减
-            if (props.activityDetail.model.type == 'full_reduce') {
-              if (props.activityDetail.model.rules.type == 'money') {
-                if (
-                  Number(props.activityDetail.model.rules.discounts[index].full) <
-                  Number(props.activityDetail.model.rules.discounts[index].discount)
-                ) {
-                  callback(new Error('规则错误'));
-                }
-              }
-            }
-            // 满额折扣
-            if (props.activityDetail.model.type == 'full_discount') {
-              if (Number(props.activityDetail.model.rules.discounts[index].discount) >= 10) {
-                callback(new Error('规则错误'));
-              }
-            }
-            callback();
-          } else {
-            callback(new Error('请输入'));
-          }
-        },
-        trigger: 'blur',
-      },
-    ],
-  };
-
-  function addDiscounts() {
-    props.activityDetail.model.rules.discounts.push({
-      full: '',
-      discounts: '',
-    });
-  }
-  function deleteDiscounts(index) {
-    props.activityDetail.model.rules.discounts.splice(index, 1);
-  }
-</script>

+ 0 - 284
src/app/shop/admin/activity/activity/components/groupon.vue

@@ -1,284 +0,0 @@
-<template>
-  <div class="groupon">
-    <el-form-item label="预热时间" prop="prehead_time">
-      <el-date-picker
-        v-model="activityDetail.model.prehead_time"
-        type="datetime"
-        value-format="YYYY-MM-DD HH:mm:ss"
-        format="YYYY-MM-DD HH:mm:ss"
-        placeholder="预热时间"
-        prefix-icon="Calendar"
-        :disabled="activityStatus"
-        :editable="false"
-      />
-    </el-form-item>
-    <el-form-item
-      label="拼团解散时间"
-      prop="rules.valid_time"
-      :rules="activityDetail.rules.valid_time"
-    >
-      <el-input
-        class="sa-w-120"
-        v-model="activityDetail.model.rules.valid_time"
-        type="number"
-        :disabled="activityStatus"
-      >
-        <template #append>小时</template>
-      </el-input>
-    </el-form-item>
-    <el-form-item
-      v-if="activityDetail.model.type == 'groupon'"
-      label="成团人数"
-      prop="rules.team_num"
-      :rules="activityDetail.rules.team_num"
-    >
-      <el-input
-        class="sa-w-120"
-        v-positiveinteger
-        v-model="activityDetail.model.rules.team_num"
-        placeholder="最少两人"
-        type="number"
-        :disabled="activityStatus"
-      >
-        <template #append>人</template>
-      </el-input>
-    </el-form-item>
-    <template v-if="activityDetail.model.type == 'groupon_ladder'">
-      <el-form-item label="成团人数" required>
-        <el-form-item
-          class="is-no-asterisk"
-          label="第一阶梯人数"
-          label-width="fit-content"
-          prop="rules.ladders.ladder_one"
-          :rules="activityDetail.rules.ladder_one"
-        >
-          <el-input
-            class="sa-w-120"
-            v-positiveinteger
-            v-model="activityDetail.model.rules.ladders.ladder_one"
-            placeholder="最少两人"
-            type="number"
-            :disabled="activityStatus"
-          >
-            <template #append>人</template>
-          </el-input>
-        </el-form-item>
-      </el-form-item>
-      <el-form-item>
-        <el-form-item
-          class="is-no-asterisk"
-          label="第二阶梯人数"
-          label-width="fit-content"
-          prop="rules.ladders.ladder_two"
-          :rules="activityDetail.rules.ladder_two"
-        >
-          <el-input
-            class="sa-w-120"
-            v-positiveinteger
-            v-model="activityDetail.model.rules.ladders.ladder_two"
-            placeholder="最少两人"
-            type="number"
-            :disabled="activityStatus"
-          >
-            <template #append>人</template>
-          </el-input>
-        </el-form-item>
-      </el-form-item>
-      <el-form-item v-if="Object.keys(activityDetail.model.rules.ladders).includes('ladder_three')">
-        <el-form-item
-          class="is-no-asterisk"
-          label="第三阶梯人数"
-          label-width="fit-content"
-          prop="rules.ladders.ladder_three"
-          :rules="activityDetail.rules.ladder_three"
-        >
-          <el-input
-            class="sa-w-120"
-            v-positiveinteger
-            v-model="activityDetail.model.rules.ladders.ladder_three"
-            placeholder="最少两人"
-            type="number"
-            :disabled="activityStatus"
-          >
-            <template #append>人</template>
-          </el-input>
-          <el-button class="is-link sa-m-l-8" type="danger" @click="deleteLadders">删除</el-button>
-        </el-form-item>
-      </el-form-item>
-      <el-form-item v-if="Object.keys(activityDetail.model.rules.ladders).length < 3">
-        <el-button class="is-link" type="primary" @click="addLadders" :disabled="activityStatus"
-          >+ 添加拼团梯队</el-button
-        >
-      </el-form-item>
-    </template>
-    <el-form-item label="单独购买">
-      <el-switch
-        v-model="activityDetail.model.rules.is_alone"
-        active-value="1"
-        inactive-value="0"
-        :disabled="activityStatus"
-      ></el-switch>
-      <span class="sa-m-l-8">
-        {{ activityDetail.model.rules.is_alone == 0 ? '不允许' : '允许' }}
-      </span>
-    </el-form-item>
-    <el-form-item label="虚拟成团">
-      <el-switch
-        v-model="activityDetail.model.rules.is_fictitious"
-        active-value="1"
-        inactive-value="0"
-        :disabled="activityStatus"
-      ></el-switch>
-      <span class="sa-m-l-8">
-        {{ activityDetail.model.rules.is_fictitious == 0 ? '不允许' : '允许' }}
-      </span>
-      <div class="tip">
-        开启虚拟成团后,在拼团有效期内人数不够的团,系统会虚拟用户凑满人数,使拼团成功。
-        虚拟的用户不生成订单,只需对真实买家发货。(请在资料管理中添加足够数量的虚拟用户,否则虚拟成团不会成功)
-      </div>
-    </el-form-item>
-    <div class="label-inner" v-if="activityDetail.model.rules.is_fictitious == 1">
-      <el-form-item>
-        <el-form-item
-          class="is-no-asterisk"
-          label="最多虚拟人数"
-          label-width="fit-content"
-          prop="rules.fictitious_num"
-          :rules="activityDetail.rules.fictitious_num"
-        >
-          <el-input
-            class="sa-w-120"
-            v-model="activityDetail.model.rules.fictitious_num"
-            type="number"
-            :disabled="activityStatus"
-          >
-            <template #append>人</template>
-          </el-input>
-          <div class="tip sa-m-l-12"> 单团最多虚拟人数的名额限制,不填时,不限制名额 </div>
-        </el-form-item>
-      </el-form-item>
-      <el-form-item>
-        <el-form-item
-          class="is-no-asterisk"
-          label="虚拟成团时间"
-          label-width="fit-content"
-          prop="rules.fictitious_time"
-          :rules="activityDetail.rules.fictitious_time"
-        >
-          <el-input
-            class="sa-w-120"
-            v-model="activityDetail.model.rules.fictitious_time"
-            type="number"
-            :disabled="activityStatus"
-          >
-            <template #append>小时</template>
-          </el-input>
-          <div class="tip sa-m-l-12">将会在拼团解散时间之前尝试虚拟成团</div>
-        </el-form-item>
-      </el-form-item>
-    </div>
-    <el-form-item label="参团卡显示">
-      <el-switch
-        v-model="activityDetail.model.rules.is_team_card"
-        active-value="1"
-        inactive-value="0"
-        :disabled="activityStatus"
-      ></el-switch>
-      <span class="sa-m-l-8">
-        {{ activityDetail.model.rules.is_team_card == 0 ? '关闭' : '开启' }}
-      </span>
-      <div class="tip sa-m-l-12">
-        开启参团卡显示后,商品详情页显示未成团的团列表,买家可以直接选择一个参团。
-      </div>
-    </el-form-item>
-    <el-form-item label="拼团销量展示">
-      <el-radio-group
-        v-model="activityDetail.model.rules.sales_show_type"
-        :disabled="activityStatus"
-      >
-        <el-radio label="real">真实活动销量</el-radio>
-        <el-radio label="goods">
-          <div class="sa-flex">
-            商品总销量
-            <div class="tip sa-m-l-8">商品总销量包含虚拟销量</div>
-          </div>
-        </el-radio>
-      </el-radio-group>
-    </el-form-item>
-    <!-- 是否参与分销 -PRO- -->
-    <el-form-item label="是否参与分销">
-      <el-switch
-        v-model="activityDetail.model.rules.is_commission"
-        active-value="1"
-        inactive-value="0"
-        :disabled="activityStatus"
-      ></el-switch>
-      <span class="sa-m-l-8">
-        {{ activityDetail.model.rules.is_commission == 0 ? '不参与' : '参与' }}
-      </span>
-    </el-form-item>
-    <el-form-item label="是否包邮">
-      <el-switch
-        v-model="activityDetail.model.rules.is_free_shipping"
-        active-value="1"
-        inactive-value="0"
-        :disabled="activityStatus"
-      ></el-switch>
-      <span class="sa-m-l-8">
-        {{ activityDetail.model.rules.is_free_shipping == 0 ? '不包邮' : '包邮' }}
-      </span>
-    </el-form-item>
-    <el-form-item label="团长优惠">
-      <el-switch
-        v-model="activityDetail.model.rules.is_leader_discount"
-        active-value="1"
-        inactive-value="0"
-        :disabled="activityStatus"
-      ></el-switch>
-    </el-form-item>
-    <el-form-item label="限购数量">
-      <el-input
-        class="sa-w-120"
-        v-positiveinteger
-        v-model="activityDetail.model.rules.limit_num"
-        type="number"
-        :disabled="activityStatus"
-      >
-        <template #append>件</template>
-      </el-input>
-    </el-form-item>
-    <el-form-item label="退款方式">
-      <div>
-        <el-radio-group v-model="activityDetail.model.rules.refund_type" :disabled="activityStatus">
-          <el-radio label="back">原路返回</el-radio>
-          <el-radio label="money">退回到余额</el-radio>
-        </el-radio-group>
-        <div class="tip">拼团失败解散时,默认退款方式</div>
-      </div>
-    </el-form-item>
-    <el-form-item
-      label="订单支付时间"
-      prop="rules.order_auto_close"
-      :rules="activityDetail.rules.order_auto_close"
-    >
-      <el-input
-        class="sa-w-120"
-        v-model="activityDetail.model.rules.order_auto_close"
-        type="number"
-        :disabled="activityStatus"
-      >
-        <template #append>分钟</template>
-      </el-input>
-    </el-form-item>
-  </div>
-</template>
-<script setup>
-  const props = defineProps(['activityDetail', 'activityStatus']);
-
-  function addLadders() {
-    props.activityDetail.model.rules.ladders.ladder_three = 4;
-  }
-  function deleteLadders() {
-    delete props.activityDetail.model.rules.ladders.ladder_three;
-  }
-</script>

+ 0 - 14
src/app/shop/admin/activity/activity/components/index.scss

@@ -1,14 +0,0 @@
-.el-form-item-wrap {
-  .el-form-item__label {
-    font-size: 12px;
-    width: fit-content !important;
-  }
-
-  .el-form-item--label-left {
-    margin-bottom: 18px !important;
-  }
-
-  .desc {
-    font-size: 12px;
-  }
-}

+ 0 - 81
src/app/shop/admin/activity/activity/components/seckill.vue

@@ -1,81 +0,0 @@
-<template>
-  <div class="seckill">
-    <el-form-item label="预热时间" prop="prehead_time">
-      <el-date-picker
-        v-model="activityDetail.model.prehead_time"
-        type="datetime"
-        value-format="YYYY-MM-DD HH:mm:ss"
-        format="YYYY-MM-DD HH:mm:ss"
-        placeholder="预热时间"
-        prefix-icon="Calendar"
-        :disabled="activityStatus"
-        :editable="false"
-      />
-    </el-form-item>
-    <!-- 是否参与分销 -PRO- -->
-    <el-form-item label="是否参与分销">
-      <el-switch
-        v-model="activityDetail.model.rules.is_commission"
-        active-value="1"
-        inactive-value="0"
-        :disabled="activityStatus"
-      ></el-switch>
-      <span class="sa-m-l-8">
-        {{ activityDetail.model.rules.is_commission == 0 ? '不参与' : '参与' }}
-      </span>
-    </el-form-item>
-    <el-form-item label="是否包邮">
-      <el-switch
-        v-model="activityDetail.model.rules.is_free_shipping"
-        active-value="1"
-        inactive-value="0"
-        :disabled="activityStatus"
-      ></el-switch>
-      <span class="sa-m-l-8">
-        {{ activityDetail.model.rules.is_free_shipping == 0 ? '不包邮' : '包邮' }}
-      </span>
-    </el-form-item>
-    <el-form-item label="秒杀销量展示">
-      <el-radio-group
-        v-model="activityDetail.model.rules.sales_show_type"
-        :disabled="activityStatus"
-      >
-        <el-radio label="real">真实活动销量</el-radio>
-        <el-radio label="goods">
-          <div class="sa-flex">
-            商品总销量
-            <div class="tip sa-m-l-8">商品总销量包含虚拟销量</div>
-          </div>
-        </el-radio>
-      </el-radio-group>
-    </el-form-item>
-    <el-form-item label="限购数量">
-      <el-input
-        class="sa-w-120"
-        v-positiveinteger
-        v-model="activityDetail.model.rules.limit_num"
-        type="number"
-        :disabled="activityStatus"
-      >
-        <template #append>件</template>
-      </el-input>
-    </el-form-item>
-    <el-form-item
-      label="订单支付时间"
-      prop="rules.order_auto_close"
-      :rules="activityDetail.rules.order_auto_close"
-    >
-      <el-input
-        class="sa-w-120"
-        v-model="activityDetail.model.rules.order_auto_close"
-        type="number"
-        :disabled="activityStatus"
-      >
-        <template #append>分钟</template>
-      </el-input>
-    </el-form-item>
-  </div>
-</template>
-<script setup>
-  const props = defineProps(['activityDetail', 'activityStatus']);
-</script>

+ 0 - 211
src/app/shop/admin/activity/activity/components/signin.vue

@@ -1,211 +0,0 @@
-<template>
-  <div class="signin">
-    <el-form-item label="日签奖励" prop="rules.everyday">
-      <span class="sa-m-r-12">每日签到固定积分</span>
-      <el-input
-        class="sa-w-120"
-        v-positiveinteger
-        v-model="activityDetail.model.rules.everyday"
-        type="number"
-      >
-        <template #append>积分</template>
-      </el-input>
-    </el-form-item>
-    <el-form-item label="递增签到" prop="rules.is_inc" required>
-      <el-switch
-        v-model="activityDetail.model.rules.is_inc"
-        active-value="1"
-        inactive-value="0"
-      ></el-switch>
-      <span class="sa-m-l-8">
-        {{ activityDetail.model.rules.is_inc == 0 ? '关闭' : '开启' }}
-      </span>
-    </el-form-item>
-    <el-form-item v-if="activityDetail.model.rules.is_inc == 1" class="sa-m-b-0">
-      <div class="el-form-item-wrap sa-flex sa-flex-wrap">
-        <el-form-item
-          class="el-form-item--label-left is-no-asterisk"
-          label="次日起递增奖励"
-          prop="rules.inc_num"
-        >
-          <el-input
-            class="sa-w-120 sa-m-r-12"
-            v-positiveinteger
-            v-model="activityDetail.model.rules.inc_num"
-            type="number"
-          >
-            <template #append>积分</template>
-          </el-input>
-        </el-form-item>
-        <el-form-item
-          class="el-form-item--label-left is-no-asterisk"
-          label="自"
-          prop="rules.until_day"
-        >
-          <el-input
-            class="sa-w-120 sa-m-r-12"
-            v-positiveinteger
-            v-model="activityDetail.model.rules.until_day"
-            type="number"
-          >
-            <template #append>天</template>
-          </el-input>
-          <span class="desc">后不再递增</span>
-        </el-form-item>
-      </div>
-    </el-form-item>
-    <el-form-item label="连续签到">
-      <el-switch v-model="is_discounts" active-value="1" inactive-value="0"></el-switch>
-      <span class="sa-m-l-8">
-        {{ is_discounts == 0 ? '关闭' : '开启' }}
-      </span>
-    </el-form-item>
-    <el-form-item v-if="is_discounts == 1">
-      <div>
-        <div class="el-form-item-wrap">
-          <template v-for="(d, dindex) in activityDetail.model.rules.discounts" :key="d">
-            <el-form-item
-              class="is-required"
-              :label="`条件${dindex + 1}`"
-              label-width="fit-content"
-            >
-              <el-form-item
-                class="el-form-item--label-left is-no-asterisk"
-                label="连续签到"
-                :prop="'rules.discounts.' + dindex + '.full'"
-                :rules="activityDetail.rules.rules.discounts.full"
-              >
-                <el-input
-                  class="sa-w-120 sa-m-r-12"
-                  v-positiveinteger
-                  v-model="d.full"
-                  type="number"
-                >
-                  <template #append>天</template>
-                </el-input>
-              </el-form-item>
-              <el-form-item
-                class="el-form-item--label-left is-no-asterisk"
-                label="赠送积分"
-                :prop="'rules.discounts.' + dindex + '.value'"
-                :rules="activityDetail.rules.rules.discounts.value"
-              >
-                <el-input
-                  class="sa-w-120 sa-m-r-12"
-                  v-positiveinteger
-                  v-model="d.value"
-                  type="number"
-                >
-                  <template #append>积分</template>
-                </el-input>
-                <el-button
-                  v-if="dindex"
-                  class="is-link"
-                  type="danger"
-                  size="small"
-                  @click="onDeleteDiscounts"
-                >
-                  删除
-                </el-button>
-              </el-form-item>
-            </el-form-item>
-          </template>
-        </div>
-        <el-button
-          v-if="activityDetail.model.rules.discounts.length < 3"
-          class="is-link"
-          type="primary"
-          @click="onAddDiscounts"
-        >
-          + 添加连续签到天数
-        </el-button>
-      </div>
-    </el-form-item>
-    <el-form-item label="补签设置">
-      <el-switch
-        v-model="activityDetail.model.rules.is_replenish"
-        active-value="1"
-        inactive-value="0"
-      ></el-switch>
-      <span class="sa-m-l-8">
-        {{ activityDetail.model.rules.is_replenish == 0 ? '关闭' : '开启' }}
-      </span>
-    </el-form-item>
-    <el-form-item class="sa-m-b-0" v-if="activityDetail.model.rules.is_replenish == 1">
-      <div class="el-form-item-wrap">
-        <div class="sa-flex sa-flex-wrap">
-          <el-form-item
-            class="el-form-item--label-left"
-            label="用户在"
-            prop="rules.replenish_limit"
-          >
-            <el-input
-              class="sa-w-120 sa-m-r-12"
-              v-positiveinteger
-              v-model="activityDetail.model.rules.replenish_limit"
-              type="number"
-            >
-              <template #append>天</template>
-            </el-input>
-          </el-form-item>
-          <el-form-item
-            class="el-form-item--label-left is-no-asterisk"
-            label="内可补签"
-            prop="rules.replenish_days"
-          >
-            <el-input
-              class="sa-w-120 sa-m-r-12"
-              v-positiveinteger
-              v-model="activityDetail.model.rules.replenish_days"
-              type="number"
-            >
-              <template #append>天</template>
-            </el-input>
-          </el-form-item>
-        </div>
-        <el-form-item
-          class="el-form-item--label-left"
-          label="每次补签消耗积分"
-          prop="rules.replenish_num"
-        >
-          <el-input
-            class="sa-w-120 sa-m-r-12"
-            v-positiveinteger
-            v-model="activityDetail.model.rules.replenish_num"
-            type="number"
-          >
-            <template #append>积分</template>
-          </el-input>
-        </el-form-item>
-      </div>
-    </el-form-item>
-  </div>
-</template>
-
-<script setup>
-  import { ref, watch } from 'vue';
-
-  const props = defineProps(['activityDetail', 'activityStatus']);
-
-  const is_discounts = ref(props.activityDetail.model.rules.discounts.length > 0 ? '1' : '0');
-  watch(
-    () => props,
-    () => {
-      if (props.activityDetail.model.rules.discounts.length > 0) {
-        is_discounts.value = '1';
-      }
-    },
-    { deep: true },
-  );
-
-  function onAddDiscounts() {
-    props.activityDetail.model.rules.discounts.push({
-      full: '',
-      value: '',
-    });
-  }
-
-  function onDeleteDiscounts(index) {
-    props.activityDetail.model.rules.discounts.splice(index, 1);
-  }
-</script>

+ 0 - 457
src/app/shop/admin/activity/activity/data.js

@@ -1,457 +0,0 @@
-import { ElMessage } from 'element-plus';
-export const activityData = [
-  {
-    type: 'promo',
-    title: '营销',
-    children: {
-      full_coupon: {
-        title: '优惠券',
-        subtitle: '向客户发送店铺优惠券',
-      },
-      full_reduce: {
-        title: '满额立减',
-        subtitle: '满足活动条件享受立减优惠',
-      },
-      full_discount: {
-        title: '满额折扣',
-        subtitle: '满足活动条件享受折扣优惠',
-      },
-      full_gift: {
-        title: '满赠',
-        subtitle: '吸引客流,刺激消费',
-      },
-      free_shipping: {
-        title: '满额包邮',
-        subtitle: '满足活动条件享受包邮优惠',
-      },
-    },
-  },
-  {
-    type: 'activity',
-    title: '活动',
-    children: {
-      groupon: {
-        title: '普通拼团',
-        subtitle: '多人拼团享优惠',
-      },
-      groupon_ladder: {
-        title: '阶梯拼团',
-        subtitle: '人数越多价格越优惠',
-      },
-      seckill: {
-        title: '秒杀',
-        subtitle: '限时特卖引流涨粉',
-      },
-    },
-  },
-  {
-    type: 'app',
-    title: '应用',
-    children: {
-      score_shop: {
-        title: '积分商城',
-        subtitle: '引导客户积分消费有效促活',
-      },
-      signin: {
-        title: '签到',
-        subtitle: '签到享好礼,客户更活跃',
-      },
-      wechat_mplive: {
-        title: '微信小程序直播',
-        subtitle: '一键同步直播间,管理直播间',
-      },
-    },
-  },
-];
-
-export function getActivityForm(atype) {
-  let form = {
-    model: {
-      title: '',
-      type: atype,
-      dateTime: [],
-      start_time: '',
-      end_time: '',
-      richtext_id: '',
-      richtext_title: '',
-    },
-    rules: {
-      title: [{ required: true, message: '请输入活动名称', trigger: 'blur' }],
-      dateTime: [{ required: true, message: '请选择活动时间', trigger: 'blur' }],
-      start_time: [{ required: true, message: '请选择活动开始时间', trigger: 'blur' }],
-      end_time: [{ required: true, message: '请选择活动结束时间', trigger: 'blur' }],
-      prehead_time: [{ required: true, message: '请选择预热时间', trigger: 'blur' }],
-      goods_list: [{ required: true, message: '请选择商品', trigger: 'blur' }],
-    },
-  };
-  let tempForm = {
-    // 满减
-    full_reduce: {
-      model: {
-        rules: {
-          type: 'money',
-          discounts: [
-            {
-              full: '',
-              discounts: '',
-            },
-          ],
-        },
-        goods_ids: null,
-        goods_list: [],
-      },
-      rules: {
-        discounts: {
-          full: [{ required: true, message: '请输入', trigger: 'blur' }],
-          discount: [{ required: true, message: '请输入', trigger: 'blur' }],
-        },
-      },
-    },
-    full_discount: {
-      model: {
-        rules: {
-          type: 'money',
-          discounts: [
-            {
-              full: '',
-              discounts: '',
-            },
-          ],
-        },
-        goods_ids: null,
-        goods_list: [],
-      },
-      rules: {
-        discounts: {
-          full: [
-            {
-              required: true,
-              message: '请输入',
-              trigger: 'blur',
-            },
-          ],
-          discount: [
-            {
-              required: true,
-              message: '请输入',
-              trigger: 'blur',
-            },
-          ],
-        },
-      },
-    },
-    full_gift: {
-      model: {
-        rules: {
-          limit_num: 0, // 参与次数 0=不限制
-          type: 'money', // 优惠类型 money=满足金额 num=满足件数
-          event: 'paid', // 赠送时机 paid=支付完成 confirm=确认收货 finish=交易完成
-          discounts: [
-            {
-              full: '',
-              gift_num: '', // 礼品份数
-              types: [], // 赠送类型 coupon_ids=优惠券 score=积分 money=余额
-              coupon_list: [], // 暂存数据
-              coupon_ids: '',
-              total: '',
-              score: '',
-              money: '',
-              // goods_ids:"",
-            },
-          ],
-        },
-        goods_ids: null,
-        goods_list: [],
-      },
-      rules: {
-        discounts: {
-          full: [
-            {
-              required: true,
-              message: '请输入',
-              trigger: 'blur',
-            },
-          ],
-          gift_num: [
-            {
-              required: true,
-              message: '请输入',
-              trigger: 'blur',
-            },
-          ],
-          types: [
-            {
-              required: true,
-              message: '请选择赠送类型',
-              trigger: 'blur',
-            },
-          ],
-        },
-      },
-    },
-    free_shipping: {
-      model: {
-        rules: {
-          type: 'money',
-          full_num: '',
-          province_except: '', // 区
-          city_except: '', // 市
-          district_except: '', // 街道
-          district_text: {}, // label数据
-        },
-        goods_ids: null,
-        goods_list: [],
-      },
-      rules: {
-        full_num: { required: true, message: '请输入', trigger: 'blur' },
-      },
-    },
-    groupon: {
-      model: {
-        prehead_time: '',
-        rules: {
-          is_commission: 0, // 是否参与分销
-          is_free_shipping: 0, // 是否包邮
-          sales_show_type: 'real', // real=真实活动销量|goods=商品总销量(包含虚拟销量)
-          team_num: 2, // 成团人数,最少两人
-          is_alone: 0, // 是否允许单独购买
-          is_fictitious: 0, // 是否允许虚拟成团
-          fictitious_num: 0, // 最多虚拟人数 0:不允许虚拟 '' 不限制
-          fictitious_time: 0, // 开团多长时间自动虚拟成团
-          is_team_card: 0, // 参团卡显示
-          is_leader_discount: 0, // 团长优惠
-          valid_time: 24, // 组团有效时间, 0:一直有效
-          limit_num: 0, // 每人限购数量 0:不限购
-          refund_type: 'back', // 退款方式 back=原路退回|money=退回到余额
-          order_auto_close: 5, // 订单自动关闭时间,如果为 0 将使用系统级订单自动关闭时间
-        },
-        goods_ids: null,
-        goods_list: [],
-      },
-      rules: {
-        valid_time: [{ required: true, message: '请输入拼团解散时间', trigger: 'blur' }],
-        team_num: [{ required: true, message: '请输入成团人数', trigger: 'blur' }],
-        fictitious_num: [{ required: true, message: '请输入最多虚拟人数', trigger: 'blur' }],
-        fictitious_time: [{ required: true, message: '请输入虚拟成团时间', trigger: 'blur' }],
-        order_auto_close: [
-          { required: true, message: '请输入订单支付时间', trigger: 'blur' },
-          {
-            validator: (rule, value, callback) => {
-              if (Number(value) <= 0) {
-                callback(new Error('值必须大于0'));
-              } else {
-                callback();
-              }
-            },
-            trigger: 'blur',
-          },
-        ],
-      },
-    },
-    groupon_ladder: {
-      model: {
-        prehead_time: '',
-        rules: {
-          is_commission: 0, // 是否参与分销
-          is_free_shipping: 0, // 是否包邮
-          sales_show_type: 'real', // real=真实活动销量|goods=商品总销量(包含虚拟销量)
-          ladders: {
-            ladder_one: 2,
-            ladder_two: 3,
-          }, // {ladder_one:2,ladder_two:2,ladder_three:2}
-          is_alone: 0, // 是否允许单独购买
-          is_fictitious: 0, // 是否允许虚拟成团
-          fictitious_num: 0, // 最多虚拟人数 0:不允许虚拟 '' 不限制
-          fictitious_time: 0, // 开团多长时间自动虚拟成团
-          is_team_card: 0, // 参团卡显示
-          is_leader_discount: 0, // 团长优惠
-          valid_time: 24, // 组团有效时间, 0:一直有效
-          limit_num: 0, // 每人限购数量 0:不限购
-          refund_type: 'back', // 退款方式 back=原路退回|money=退回到余额
-          order_auto_close: 5, // 订单自动关闭时间,如果为 0 将使用系统级订单自动关闭时间
-        },
-        goods_ids: null,
-        goods_list: [],
-      },
-      rules: {
-        valid_time: [{ required: true, message: '请输入拼团解散时间', trigger: 'blur' }],
-        ladder_one: [{ required: true, message: '最少两人', trigger: 'blur' }],
-        ladder_two: [{ required: true, message: '最少两人', trigger: 'blur' }],
-        ladder_three: [{ required: true, message: '最少两人', trigger: 'blur' }],
-        fictitious_num: [{ required: true, message: '请输入最多虚拟人数', trigger: 'blur' }],
-        fictitious_time: [{ required: true, message: '请输入虚拟成团时间', trigger: 'blur' }],
-        order_auto_close: [
-          { required: true, message: '请输入订单支付时间', trigger: 'blur' },
-          {
-            validator: (rule, value, callback) => {
-              if (Number(value) <= 0) {
-                callback(new Error('值必须大于0'));
-              } else {
-                callback();
-              }
-            },
-            trigger: 'blur',
-          },
-        ],
-      },
-    },
-    seckill: {
-      model: {
-        prehead_time: '',
-        rules: {
-          is_commission: 0, // 是否参与分销
-          is_free_shipping: 0, // 是否包邮
-          sales_show_type: 'real', // real=真实活动销量|goods=商品总销量(包含虚拟销量)
-          limit_num: 0, // 每人限购数量 0:不限购
-          order_auto_close: 5, // 订单自动关闭时间,如果为 0 将使用系统级订单自动关闭时间is_commission: 0,  // 是否参与分销
-        },
-        goods_ids: null,
-        goods_list: [],
-      },
-      rules: {
-        order_auto_close: [
-          { required: true, message: '请输入订单支付时间', trigger: 'blur' },
-          {
-            validator: (rule, value, callback) => {
-              if (Number(value) <= 0) {
-                callback(new Error('值必须大于0'));
-              } else {
-                callback();
-              }
-            },
-            trigger: 'blur',
-          },
-        ],
-      },
-    },
-    signin: {
-      model: {
-        rules: {
-          everyday: 0, // 每日签到固定积分
-          is_inc: 0, // 是否递增签到
-          inc_num: 0, // 递增奖励
-          until_day: 0, // 递增持续天数
-          discounts: [], // 连续签到奖励 {full:5, value:10}
-          is_replenish: 0, // 是否开启补签
-          replenish_days: 1, // 可补签天数 最小1
-          replenish_limit: 0, // 补签事件限制,0 不限制
-          replenish_num: 1, // 补签所消耗积分
-        },
-      },
-      rules: {
-        rules: {
-          everyday: [{ required: true, message: '请输入日签奖励', trigger: 'blur' }],
-          inc_num: [{ required: true, message: '请输入', trigger: 'blur' }],
-          until_day: [{ required: true, message: '请输入', trigger: 'blur' }],
-          discounts: {
-            full: [{ required: true, message: '请输入', trigger: 'blur' }],
-            value: [{ required: true, message: '请输入', trigger: 'blur' }],
-          },
-          replenish_days: [{ required: true, message: '请输入最多虚拟人数', trigger: 'blur' }],
-          replenish_limit: [{ required: true, message: '请输入虚拟成团时间', trigger: 'blur' }],
-          replenish_num: [{ required: true, message: '请输入虚拟成团时间', trigger: 'blur' }],
-        },
-      },
-    },
-  };
-  form.model = {
-    ...form.model,
-    ...tempForm[atype].model,
-  };
-  form.rules = { ...form.rules, ...tempForm[atype].rules };
-  return form;
-}
-
-export function handleForm(submitForm) {
-  if (submitForm.type != 'signin') {
-    //   处理商品
-    let goodsIds = [];
-    submitForm.goods_list.forEach((g) => {
-      goodsIds.push(g.id);
-    });
-    submitForm.goods_ids = goodsIds.join(',');
-  }
-
-  // 满减
-  if (submitForm.type == 'full_reduce') {
-    if (submitForm.rules.type == 'money') {
-      let flag = true;
-      submitForm.rules.discounts.forEach((d) => {
-        if (Number(d.full) < Number(d.discount)) {
-          flag = false;
-        }
-      });
-      if (!flag) {
-        ElMessage({
-          message: '请输入正确的规则',
-          type: 'warning',
-        });
-        return;
-      }
-    }
-  }
-
-  // 满赠
-  if (submitForm.type == 'full_gift') {
-    // 优惠券
-    submitForm.rules.discounts.forEach((d) => {
-      let couponIds = [];
-      let total = 0;
-      d.coupon_list.forEach((c) => {
-        couponIds.push(c.id);
-        total += Number(c.amount);
-      });
-      d.coupon_ids = couponIds.join(',');
-      d.total = total;
-      delete d.coupon_list;
-    });
-  }
-
-  // 阶梯拼团
-  if (submitForm.type == 'groupon_ladder') {
-    if (
-      !(
-        (!submitForm.rules.ladders.hasOwnProperty('ladder_three') &&
-          Number(submitForm.rules.ladders.ladder_one) <
-            Number(submitForm.rules.ladders.ladder_two)) ||
-        (submitForm.rules.ladders.hasOwnProperty('ladder_three') &&
-          Number(submitForm.rules.ladders.ladder_one) <
-            Number(submitForm.rules.ladders.ladder_two) &&
-          Number(submitForm.rules.ladders.ladder_two) <
-            Number(submitForm.rules.ladders.ladder_three))
-      )
-    ) {
-      ElMessage({
-        message: '请输入成团人数(阶梯人数依次增加)',
-        type: 'warning',
-      });
-      return;
-    }
-
-    let flag = false;
-    submitForm.goods_list.forEach((goods) => {
-      if (goods.activity_sku_prices) {
-        goods.activity_sku_prices.forEach((sku) => {
-          if (sku.status == 'up' && submitForm.rules.ladders.hasOwnProperty('ladder_three')) {
-            if (!(sku.hasOwnProperty('ladder_three') && sku.hasOwnProperty('ladder_three'))) {
-              flag = true;
-            }
-          }
-        });
-      }
-    });
-    if (flag) {
-      ElMessage({
-        message: '请完善商品规格信息',
-        type: 'warning',
-      });
-      return;
-    }
-  }
-
-  submitForm.start_time = submitForm.dateTime[0];
-  submitForm.end_time = submitForm.dateTime[1];
-  delete submitForm.dateTime;
-
-  return submitForm;
-}

+ 0 - 405
src/app/shop/admin/activity/activity/edit.vue

@@ -1,405 +0,0 @@
-<template>
-  <el-container class="activity-edit">
-    <el-main>
-      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="110px">
-        <el-form-item label="活动名称" prop="title">
-          <el-input v-model="form.model.title" placeholder="例如:国庆活动" />
-        </el-form-item>
-        <el-form-item v-if="form.model.status == 'ing'" label="活动时间" required>
-          <el-form-item prop="start_time">
-            <el-date-picker
-              v-model="form.model.start_time"
-              type="datetime"
-              value-format="YYYY-MM-DD HH:mm:ss"
-              format="YYYY-MM-DD HH:mm:ss"
-              :disabled="activityStatus"
-              prefix-icon="Calendar"
-              placeholder="开始时间"
-              :editable="false"
-            />
-          </el-form-item>
-          <span class="sa-m-l-12 sa-m-r-12">至</span>
-          <el-form-item prop="end_time">
-            <el-date-picker
-              v-model="form.model.end_time"
-              type="datetime"
-              value-format="YYYY-MM-DD HH:mm:ss"
-              format="YYYY-MM-DD HH:mm:ss"
-              prefix-icon="Calendar"
-              placeholder="结束时间"
-              :editable="false"
-              @change="onChangeEndtime"
-            />
-          </el-form-item>
-        </el-form-item>
-        <el-form-item v-if="form.model.status != 'ing'" label="活动时间" prop="dateTime">
-          <div>
-            <el-date-picker
-              v-model="form.model.dateTime"
-              type="datetimerange"
-              value-format="YYYY-MM-DD HH:mm:ss"
-              format="YYYY-MM-DD HH:mm:ss"
-              :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)]"
-              range-separator="至"
-              start-placeholder="开始时间"
-              end-placeholder="结束时间"
-              prefix-icon="Calendar"
-              :editable="false"
-            />
-          </div>
-        </el-form-item>
-        <!-- 满额立减/满额折扣 -->
-        <full-reduce-discount
-          v-if="form.model.type == 'full_reduce' || form.model.type == 'full_discount'"
-          :activityDetail="form"
-        ></full-reduce-discount>
-        <!-- 满赠 -->
-        <full-gift v-if="form.model.type == 'full_gift'" :activityDetail="form"></full-gift>
-        <!-- 满包邮 -->
-        <free-shipping
-          v-if="form.model.type == 'free_shipping'"
-          :activityDetail="form"
-        ></free-shipping>
-        <!-- 普通拼团/阶梯拼团 -->
-        <groupon
-          v-if="form.model.type == 'groupon' || form.model.type == 'groupon_ladder'"
-          :activityDetail="form"
-          :activityStatus="activityStatus"
-        ></groupon>
-        <!-- 秒杀 -->
-        <seckill
-          v-if="form.model.type == 'seckill'"
-          :activityDetail="form"
-          :activityStatus="activityStatus"
-        ></seckill>
-        <signin
-          v-if="form.model.type == 'signin'"
-          :activityDetail="form"
-          :activityStatus="activityStatus"
-        />
-        <el-form-item label="活动说明">
-          <el-input
-            class="richtext-title"
-            v-model="form.model.richtext_title"
-            placeholder="请选择活动说明"
-          >
-            <template #append>
-              <el-button class="is-link" type="info" @click="selectRichtext"
-                >选择活动说明</el-button
-              >
-            </template>
-          </el-input>
-        </el-form-item>
-        <template v-if="form.model.type != 'signin'">
-          <el-form-item class="sa-m-b-8" label="活动商品" prop="goodsType" v-if="!isActivity">
-            <el-radio-group
-              v-model="goodsType"
-              :disabled="activityStatus"
-              @change="changeGoodsType"
-            >
-              <el-radio label="all">全部商品</el-radio>
-              <el-radio label="part">部分商品</el-radio>
-            </el-radio-group>
-          </el-form-item>
-          <el-form-item
-            :label="isActivity ? '活动商品' : ''"
-            :prop="goodsType == 'part' || isActivity ? 'goods_list' : ''"
-            v-if="goodsType == 'part'"
-          >
-            <div>
-              <el-button
-                class="is-link"
-                type="primary"
-                @click="selectGoods"
-                :disabled="activityStatus"
-                >+ 添加商品</el-button
-              >
-              <div
-                class="sa-template-wrap sa-m-t-8"
-                :class="isActivity ? 'sa-template-wrap-activity' : ''"
-                v-if="form.model.goods_list.length > 0"
-              >
-                <div class="title sa-flex">
-                  <div class="key">商品信息</div>
-                  <div class="key setting" v-if="isActivity">设置</div>
-                  <div class="oper">操作</div>
-                </div>
-                <div>
-                  <div
-                    class="item"
-                    v-for="(element, index) in form.model.goods_list"
-                    :key="element"
-                  >
-                    <el-form-item class="key">
-                      <sa-image :url="element.image" size="40"></sa-image>
-                      <div class="goods sa-m-l-12">
-                        <div class="goods-title sa-m-b-6 sa-table-line-1">
-                          {{ element.title }}
-                        </div>
-                        <div class="goods-price"> ¥{{ element.price.join('~') }} </div>
-                      </div>
-                    </el-form-item>
-                    <el-form-item class="setting" v-if="isActivity">
-                      <el-button
-                        class="is-link"
-                        type="primary"
-                        @click="setActivitySkuPrices(index, element.id)"
-                        >设置商品</el-button
-                      >
-                    </el-form-item>
-                    <el-form-item class="oper">
-                      <el-button
-                        class="is-link"
-                        type="danger"
-                        @click="deleteGoods(index)"
-                        :disabled="activityStatus"
-                      >
-                        移除
-                      </el-button>
-                    </el-form-item>
-                  </div>
-                </div>
-              </div>
-            </div>
-          </el-form-item>
-        </template>
-      </el-form>
-    </el-main>
-    <el-footer class="sa-footer--submit">
-      <el-button
-        v-if="modal.params.type == 'add'"
-        v-auth="'shop.admin.activity.activity.add'"
-        type="primary"
-        @click="confirm"
-        >保存</el-button
-      >
-      <el-button
-        v-if="modal.params.type == 'edit'"
-        v-auth="'shop.admin.activity.activity.edit'"
-        type="primary"
-        @click="confirm"
-        >更新</el-button
-      >
-    </el-footer>
-  </el-container>
-</template>
-<script setup>
-  import { computed, onMounted, reactive, ref, unref } from 'vue';
-  import { api } from '../activity.service';
-  import { useModal } from '@/sheep/hooks';
-  import { getActivityForm, handleForm } from './data';
-  import { cloneDeep } from 'lodash';
-
-  import fullReduceDiscount from './components/full-reduce-discount.vue';
-  import FullGift from './components/full-gift.vue';
-  import FreeShipping from './components/free-shipping.vue';
-  import Groupon from './components/groupon.vue';
-  import Seckill from './components/seckill.vue';
-  import Signin from './components/signin.vue';
-  import RichtextSelect from '../../data/richtext/select.vue';
-  import GoodsSelect from '../../goods/goods/select.vue';
-  import ActivitySku from '../../activity/activity/sku.vue';
-
-  const emit = defineEmits(['modalCallBack']);
-  const props = defineProps(['modal']);
-
-  const isActivity = computed(
-    () =>
-      props.modal.params.atype == 'groupon' ||
-      props.modal.params.atype == 'groupon_ladder' ||
-      props.modal.params.atype == 'seckill',
-  );
-
-  const activityStatus = ref(false);
-
-  const formRef = ref();
-  const form = reactive({
-    model: getActivityForm(props.modal.params.atype).model,
-    rules: getActivityForm(props.modal.params.atype).rules,
-  });
-
-  async function getDetail() {
-    const { error, data } = await api.activity.detail(props.modal.params.id);
-    if (error === 0) {
-      form.model = data;
-
-      // 处理时间
-      form.model.dateTime = [form.model.start_time, form.model.end_time];
-
-      // 处理商品
-      if (form.model.goods_ids) {
-        goodsType.value = 'part';
-      }
-      activityStatus.value = data.status == 'ing';
-    }
-  }
-
-  const goodsType = ref('all');
-  function changeGoodsType() {
-    if (goodsType.value == 'all') {
-      form.model.goods_ids = null;
-      form.model.goods_list = [];
-    } else if (goodsType.value == 'part') {
-    }
-  }
-
-  function selectRichtext() {
-    useModal(
-      RichtextSelect,
-      {
-        title: '选择活动说明',
-      },
-      {
-        confirm: (res) => {
-          form.model.richtext_title = res.data.title;
-          form.model.richtext_id = res.data.id;
-        },
-      },
-    );
-  }
-
-  function selectGoods() {
-    let ids = [];
-    form.model.goods_list.forEach((i) => {
-      ids.push(i.id);
-    });
-    useModal(
-      GoodsSelect,
-      {
-        title: '选择商品',
-        multiple: true,
-        ids,
-      },
-      {
-        confirm: (res) => {
-          res.data.forEach((item) => {
-            let findItem = form.model.goods_list.find((k) => k.id == item.id);
-            if (findItem) {
-              item.activity_sku_prices = findItem.activity_sku_prices;
-            }
-          });
-          form.model.goods_list = res.data;
-        },
-      },
-    );
-  }
-  function deleteGoods(index) {
-    form.model.goods_list.splice(index, 1);
-  }
-
-  function setActivitySkuPrices(index, id) {
-    useModal(
-      ActivitySku,
-      {
-        title: '设置商品',
-        model: form.model,
-        goods_id: id,
-        activityStatus: activityStatus.value,
-      },
-      {
-        confirm: (res) => {
-          form.model.goods_list[index].activity_sku_prices = res.data;
-        },
-      },
-    );
-  }
-
-  function onChangeEndtime(val) {
-    form.model.dateTime[1] = val
-  }
-
-  function confirm() {
-    unref(formRef).validate(async (valid) => {
-      if (!valid) return;
-      let submitForm = handleForm(cloneDeep(form.model));
-      if (submitForm) {
-        const { error } =
-          props.modal.params.type == 'add'
-            ? await api.activity.add(submitForm)
-            : await api.activity.edit(submitForm.id, submitForm);
-
-        error == 0 && emit('modalCallBack', { event: 'confirm' });
-      }
-    });
-  }
-
-  onMounted(() => {
-    if (props.modal.params.type == 'edit') {
-      getDetail();
-    }
-
-    if (isActivity.value) {
-      goodsType.value = 'part';
-    }
-  });
-</script>
-<style lang="scss" scoped>
-  .activity-edit {
-    :deep() {
-      .label-inner {
-        .el-form-item__label {
-          font-size: 12px;
-        }
-      }
-      .el-form-item-wrap {
-        .el-form-item__label {
-          font-size: 12px;
-          width: fit-content !important;
-        }
-
-        .el-form-item--label-left {
-          margin-bottom: 18px !important;
-        }
-
-        .desc {
-          font-size: 12px;
-        }
-      }
-      .sa-template-wrap {
-        width: 360px;
-        .title {
-          margin: 0;
-        }
-        .setting {
-          flex: none;
-          width: 100px;
-        }
-        .oper {
-          flex: none;
-          width: 60px;
-        }
-        .item {
-          border-bottom: 1px solid var(--sa-space);
-          margin-top: 12px;
-          & > .el-form-item {
-            margin-bottom: 12px;
-          }
-          .goods {
-            line-height: 16px;
-            font-size: 12px;
-            flex: 1;
-            .goods-title {
-              color: var(--sa-font);
-            }
-            .goods-price {
-              color: var(--el-color-danger);
-            }
-            .goods-stock {
-              color: var(--sa-font);
-            }
-          }
-        }
-        &.sa-template-wrap-activity {
-          width: 464px;
-        }
-      }
-    }
-    .richtext-title {
-      :deep() {
-        .el-input-group__append {
-          width: 106px;
-        }
-      }
-    }
-  }
-</style>

+ 0 - 136
src/app/shop/admin/activity/activity/index.vue

@@ -1,136 +0,0 @@
-<template>
-  <el-container class="activity-view panel-block">
-    <el-header class="sa-header">
-      <div class="sa-title">
-        <div class="label">营销活动</div>
-      </div>
-    </el-header>
-    <el-main>
-      <div class="sa-m-b-16" v-for="ad in activityData" :key="ad">
-        <div class="title sa-m-b-12">{{ ad.title }}</div>
-        <div class="sa-flex sa-flex-wrap">
-          <template v-for="(item, key) in ad.children" :key="item">
-            <el-button v-if="key == 'full_coupon'" v-auth="'shop.admin.coupon.list'" link>
-              <div class="item sa-flex" @click="handleActivity(key)">
-                <div class="icon">
-                  <img :src="'./static/images/shop/activity/' + key + '.png'" />
-                </div>
-                <div class="sa-m-l-12">
-                  <div class="title">{{ item.title }}</div>
-                  <div class="subtitle">{{ item.subtitle }}</div>
-                </div>
-              </div>
-            </el-button>
-            <el-button
-              v-else-if="key == 'score_shop'"
-              v-auth="'shop.admin.app.scoreshop.list'"
-              link
-            >
-              <div class="item sa-flex" @click="handleActivity(key)">
-                <div class="icon">
-                  <img :src="'./static/images/shop/activity/' + key + '.png'" />
-                </div>
-                <div class="sa-m-l-12">
-                  <div class="title">{{ item.title }}</div>
-                  <div class="subtitle">{{ item.subtitle }}</div>
-                </div>
-              </div>
-            </el-button>
-            <div v-else class="item sa-flex" @click="handleActivity(key)">
-              <div class="icon">
-                <img :src="'./static/images/shop/activity/' + key + '.png'" />
-              </div>
-              <div class="sa-m-l-12">
-                <div class="title">{{ item.title }}</div>
-                <div class="subtitle">{{ item.subtitle }}</div>
-              </div>
-            </div>
-          </template>
-        </div>
-      </div>
-    </el-main>
-  </el-container>
-</template>
-<script>
-  export default {
-    name: 'shop.admin.activity.activity',
-  };
-</script>
-<script setup>
-  import { useRouter } from 'vue-router';
-  import { activityData } from './data';
-
-  const router = useRouter();
-
-  function handleActivity(type) {
-    if (type == 'full_coupon') {
-      router.push({
-        path: '/shop/admin/coupon',
-      });
-    } else if (type == 'score_shop') {
-      router.push({
-        path: '/shop/admin/app/scoreShop',
-      });
-    } else {
-      router.push({
-        path: '/shop/admin/activity/list',
-        query: {
-          type: type,
-        },
-      });
-    }
-  }
-</script>
-<style lang="scss" scoped>
-  .activity-view {
-    .title {
-      line-height: 20px;
-      font-size: 16px;
-      color: var(--sa-title);
-      text-align: left;
-    }
-    .item {
-      width: 250px;
-      padding: 18px 20px;
-      background: var(--sa-table-header-bg);
-      border-radius: 4px;
-      margin: 0 16px 16px 0;
-      cursor: pointer;
-      &:last-of-type {
-        margin-right: 0;
-      }
-      &:hover {
-        transition: width height 0.5s;
-        transform: scale(1.05);
-      }
-      .icon {
-        width: 44px;
-        height: 44px;
-        background: var(--el-color-primary);
-        border-radius: 4px;
-        overflow: hidden;
-        img {
-          width: 100%;
-        }
-      }
-      .title {
-        line-height: 18px;
-        font-size: 14px;
-        font-weight: 600;
-        color: var(--sa-subtitle);
-        margin: 2px 0 6px;
-      }
-      .subtitle {
-        line-height: 16px;
-        font-size: 12px;
-        color: var(--sa-subfont);
-      }
-    }
-    :deep() {
-      .el-button.is-link {
-        padding: 0;
-        margin-right: 16px;
-      }
-    }
-  }
-</style>

+ 0 - 343
src/app/shop/admin/activity/activity/list.vue

@@ -1,343 +0,0 @@
-<template>
-  <el-container class="activity-list panel-block">
-    <el-header class="sa-header">
-      <div class="sa-title sa-flex sa-row-between">
-        <div class="label sa-flex">
-          <span class="left">{{ title }}</span>
-          <search-condition
-            :conditionLabel="filterParams.conditionLabel"
-            @deleteFilter="deleteFilter"
-          ></search-condition>
-        </div>
-        <div>
-          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
-          <el-button class="sa-button-refresh" icon="Search" @click="openFilter"></el-button>
-          <el-button
-            v-auth="'shop.admin.activity.activity.add'"
-            type="primary"
-            icon="Plus"
-            @click="addRow"
-            >添加</el-button
-          >
-          <el-button
-            v-auth="'shop.admin.activity.activity.recyclebin'"
-            type="danger"
-            icon="Delete"
-            plain
-            @click="openRecyclebin"
-            >回收站</el-button
-          >
-        </div>
-      </div>
-    </el-header>
-    <el-main class="sa-p-0" v-loading="loading">
-      <el-table height="100%" class="sa-table" :data="table.data" stripe>
-        <template #empty>
-          <sa-empty />
-        </template>
-        <el-table-column prop="id" label="ID" min-width="80"></el-table-column>
-        <el-table-column label="活动名称" min-width="200">
-          <template #default="scope">
-            <div class="sa-table-line-1">{{ scope.row.title }}</div>
-          </template>
-        </el-table-column>
-        <el-table-column v-if="route.query.type != 'signin'" label="参与商品" min-width="244">
-          <template #default="scope">
-            <el-popover trigger="hover" :width="336">
-              <div>
-                <div class="goods-item sa-flex" v-for="g in scope.row.goods" :key="g">
-                  <sa-image :url="g.image" size="40"></sa-image>
-                  <div class="sa-m-l-12">
-                    <div class="goods-title sa-table-line-1 sa-m-b-4">
-                      {{ g.title }}
-                    </div>
-                    <div class="goods-id">#{{ g.id }}</div>
-                  </div>
-                </div>
-              </div>
-              <template #reference>
-                <div class="goods-reference">
-                  <template v-if="scope.row.goods_ids">
-                    <span class="leng">{{ scope.row.goods?.length }}</span>
-                    件商品
-                  </template>
-                </div>
-              </template>
-            </el-popover>
-            <div v-if="!scope.row.goods_ids">全部商品</div>
-          </template>
-        </el-table-column>
-        <el-table-column label="活动状态" min-width="110">
-          <template #default="scope">
-            <div class="status" :class="`status-${scope.row.status}`">
-              {{ scope.row.status_text }}
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column label="活动时间" min-width="250">
-          <template #default="scope">
-            <div>
-              <div>开始时间:{{ scope.row.start_time }}</div>
-              <div>结束时间:{{ scope.row.end_time }}</div>
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column prop="update_time" label="更新时间" min-width="172"></el-table-column>
-        <el-table-column label="活动说明" min-width="158">
-          <template #default="scope">
-            <div class="sa-table-line-1">{{ scope.row.richtext_title }}</div>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" min-width="160" fixed="right">
-          <template #default="scope">
-            <el-button
-              v-if="scope.row.type == 'groupon' || scope.row.type == 'groupon_ladder'"
-              v-auth="'shop.admin.activity.groupon.list'"
-              class="is-link"
-              type="primary"
-              @click="grouponList(scope.row.id)"
-              >查看</el-button
-            >
-            <el-button
-              v-auth="'shop.admin.activity.activity.detail'"
-              class="is-link"
-              type="primary"
-              @click="editRow(scope.row.id)"
-              >编辑</el-button
-            >
-            <el-popconfirm
-              width="fit-content"
-              confirm-button-text="确认"
-              cancel-button-text="取消"
-              title="确认删除这条记录?"
-              @confirm="deleteRow(scope.row.id)"
-            >
-              <template #reference>
-                <el-button
-                  v-auth="'shop.admin.activity.activity.delete'"
-                  class="is-link"
-                  type="danger"
-                >
-                  删除
-                </el-button>
-              </template>
-            </el-popconfirm>
-          </template>
-        </el-table-column>
-      </el-table>
-    </el-main>
-    <sa-view-bar>
-      <template #right>
-        <sa-pagination :pageData="pageData" @updateFn="getData" />
-      </template>
-    </sa-view-bar>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from '../activity.service';
-  import { useModal, usePagination } from '@/sheep/hooks';
-  import { useSearch } from '@/sheep/components/sa-table/sa-search/useSearch';
-  import { composeFilter } from '@/sheep/utils';
-  import { activityData } from './data';
-  import ActivityEdit from './edit.vue';
-  import ActivityRecyclebin from './recyclebin.vue';
-  import { cloneDeep } from 'lodash';
-
-  import { useRoute, useRouter } from 'vue-router';
-  const route = useRoute();
-  const router = useRouter();
-
-  const title = ref('');
-  activityData.filter((k) => {
-    for (var key in k.children) {
-      if (key == route.query.type) {
-        title.value = k.children[key].title;
-      }
-    }
-  });
-
-  const filterParams = reactive({
-    tools: {
-      title: {
-        type: 'tinput',
-        label: '活动名称',
-        field: 'title',
-        value: '',
-      },
-      status: {
-        type: 'tselect',
-        label: '状态',
-        field: 'status',
-        value: '',
-        options: {
-          data: [
-            {
-              label: '全部',
-              value: 'all',
-            },
-            {
-              label: '未开始',
-              value: 'nostart',
-            },
-            {
-              label: '进行中',
-              value: 'ing',
-            },
-            {
-              label: '已结束',
-              value: 'ended',
-            },
-          ],
-        },
-      },
-      activity_time: {
-        type: 'tdatetimerange',
-        label: '时间',
-        field: 'activity_time',
-        value: [],
-      },
-    },
-    data: {
-      title: '',
-      status: 'all',
-      activity_time: [],
-    },
-    conditionLabel: {},
-  });
-  const { openFilter, deleteFilter } = useSearch({ filterParams, getData });
-
-  const loading = ref(true);
-
-  // 表格
-  const table = reactive({
-    data: [],
-    order: '',
-    sort: '',
-    selected: [],
-  });
-
-  const { pageData } = usePagination();
-
-  // 获取数据
-  async function getData(page) {
-    loading.value = true;
-    if (page) pageData.page = page;
-    let tempSearch = cloneDeep(filterParams.data);
-    let search = composeFilter(tempSearch, {
-      title: 'like',
-      activity_time: 'range',
-    });
-    const { error, data } = await api.activity.list(route.query.type, {
-      page: pageData.page,
-      list_rows: pageData.list_rows,
-      ...search,
-    });
-    if (error === 0) {
-      table.data = data.data;
-      pageData.page = data.current_page;
-      pageData.list_rows = data.per_page;
-      pageData.total = data.total;
-    }
-    loading.value = false;
-  }
-
-  function addRow() {
-    useModal(
-      ActivityEdit,
-      {
-        title: `添加${title.value}`,
-        type: 'add',
-        atype: route.query.type,
-      },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-  function editRow(id) {
-    useModal(
-      ActivityEdit,
-      {
-        title: `编辑${title.value}`,
-        type: 'edit',
-        atype: route.query.type,
-        id: id,
-      },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-  //拼团列表
-  function grouponList(id) {
-    router.push({
-      path: '/shop/admin/activity/groupon',
-      query: {
-        activity_id: id,
-      },
-    });
-  }
-  async function deleteRow(id) {
-    await api.activity.delete(id);
-    getData();
-  }
-  function openRecyclebin() {
-    useModal(
-      ActivityRecyclebin,
-      {
-        title: `${title.value}回收站`,
-        type: route.query.type,
-      },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-
-  onMounted(() => {
-    getData();
-  });
-</script>
-<style lang="scss" scoped>
-  .activity-list {
-    .goods-reference {
-      width: fit-content;
-      cursor: pointer;
-      .leng {
-        color: var(--el-color-primary);
-      }
-    }
-    .goods-item {
-      order: 1px solid var(--sa-space);
-      padding: 8px 0;
-      .goods-title {
-        line-height: 20px;
-        font-size: 14px;
-        color: var(--sa-font);
-      }
-      .goods-id {
-        line-height: 16px;
-        font-size: 12px;
-        color: var(--sa-subfont);
-      }
-    }
-    .status {
-      &.status-nostart {
-        color: #999;
-      }
-      &.status-ing {
-        color: #52c41a;
-      }
-      &.status-ended {
-        color: #ff4d4f;
-      }
-    }
-  }
-</style>

+ 0 - 72
src/app/shop/admin/activity/activity/recyclebin.vue

@@ -1,72 +0,0 @@
-<template>
-  <el-container class="recyclebin-view">
-    <el-main v-loading="loading">
-      <el-table :data="table.data" @sort-change="fieldFilter" class="sa-table" stripe>
-        <template #empty>
-          <sa-empty />
-        </template>
-        <el-table-column sortable="custom" prop="id" label="ID" min-width="100"></el-table-column>
-        <el-table-column label="名称" min-width="100">
-          <template #default="scope">
-            <div class="sa-table-line-1">{{ scope.row.title || '-' }}</div>
-          </template>
-        </el-table-column>
-        <el-table-column
-          sortable="custom"
-          prop="delete_time"
-          label="删除时间"
-          min-width="172"
-        ></el-table-column>
-      </el-table>
-    </el-main>
-    <sa-view-bar>
-      <template #right>
-        <sa-pagination :pageData="pageData" @updateFn="getData" />
-      </template>
-    </sa-view-bar>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from '../activity.service';
-  import { usePagination } from '@/sheep/hooks';
-
-  const props = defineProps(['modal']);
-
-  const loading = ref(true);
-
-  // 表格状态
-  const table = reactive({
-    data: [],
-    order: '',
-    sort: '',
-  });
-
-  const { pageData } = usePagination();
-
-  // 获取数据
-  async function getData() {
-    loading.value = true;
-    const { data } = await api.activity.recyclebin(props.modal.params.type, {
-      page: pageData.page,
-      list_rows: pageData.list_rows,
-      order: table.order,
-      sort: table.sort,
-    });
-    table.data = data.data;
-    pageData.page = data.current_page;
-    pageData.list_rows = data.per_page;
-    pageData.total = data.total;
-    loading.value = false;
-  }
-  //table 字段排序
-  function fieldFilter({ prop, order }) {
-    table.order = order == 'ascending' ? 'asc' : 'desc';
-    table.sort = prop;
-    getData();
-  }
-
-  onMounted(() => {
-    getData();
-  });
-</script>

+ 0 - 77
src/app/shop/admin/activity/activity/select.vue

@@ -1,77 +0,0 @@
-<template>
-  <el-container>
-    <el-main>
-      <el-table class="sa-table" :data="table.data" stripe>
-        <el-table-column prop="id" label="ID" min-width="80" />
-        <el-table-column label="名称" min-width="128">
-          <template #default="scope">
-            <div class="sa-table-line-1">
-              {{ scope.row.title }}
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column label="类型" min-width="74">
-          <template #default="scope">
-            <div class="sa-table-line-1">
-              {{ scope.row.type_text }}
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作">
-          <template #default="scope">
-            <el-button class="is-link" type="primary" @click="modalCallBack(scope.row)"
-              >选择</el-button
-            >
-          </template>
-        </el-table-column>
-      </el-table>
-    </el-main>
-    <el-footer class="sa-flex sa-row-right">
-      <sa-pagination :pageData="pageData" @updateFn="getData" />
-    </el-footer>
-  </el-container>
-</template>
-
-<script>
-  export default {
-    name: 'ActivitySelect',
-  };
-</script>
-
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from '../activity.service';
-  import { usePagination } from '@/sheep/hooks';
-
-  const emit = defineEmits(['modalCallBack']);
-  const props = defineProps(['modal']);
-
-  const loading = ref(true);
-
-  const table = reactive({
-    data: [],
-  });
-
-  const { pageData } = usePagination();
-
-  // 获取数据
-  async function getData() {
-    loading.value = false;
-    const { data } = await api.activity.select(props.modal.params.type, {
-      search: JSON.stringify({ status: ['noend'] }),
-    });
-    table.data = data.data;
-    pageData.page = data.current_page;
-    pageData.list_rows = data.per_page;
-    pageData.total = data.total;
-    loading.value = false;
-  }
-
-  function modalCallBack(row) {
-    emit('modalCallBack', { event: 'confirm', data: row });
-  }
-
-  onMounted(() => {
-    getData();
-  });
-</script>

+ 0 - 245
src/app/shop/admin/activity/activity/sku.vue

@@ -1,245 +0,0 @@
-<template>
-  <el-container>
-    <el-main>
-      <div class="sku-table-wrap">
-        <table class="sku-table" rules="none">
-          <thead>
-            <tr>
-              <th class="sku-item" v-for="ss in goods.skus" :key="ss">
-                {{ ss.name }}
-              </th>
-              <th class="sku-item">库存</th>
-              <th class="sku-item">价格</th>
-              <th class="sku-item">销量</th>
-              <th class="sku-item">活动库存</th>
-              <th v-if="['groupon', 'seckill'].includes(modal.params.model.type)" class="sku-item">
-                活动价格
-              </th>
-              <th
-                class="sku-item"
-                v-if="
-                  modal.params.model.type == 'groupon' &&
-                  modal.params.model.rules.is_leader_discount == 1
-                "
-              >
-                团长价格
-              </th>
-              <template v-if="modal.params.model.type == 'groupon_ladder'">
-                <th class="sku-item">
-                  {{ modal.params.model.rules.ladders.ladder_one }}人团价格
-                </th>
-                <th class="sku-item" v-if="modal.params.model.rules.is_leader_discount == 1">
-                  {{ modal.params.model.rules.ladders.ladder_one }}人团长价格
-                </th>
-                <th class="sku-item">
-                  {{ modal.params.model.rules.ladders.ladder_two }}人团价格
-                </th>
-                <th class="sku-item" v-if="modal.params.model.rules.is_leader_discount == 1">
-                  {{ modal.params.model.rules.ladders.ladder_two }}人团长价格
-                </th>
-                <template
-                  v-if="Object.keys(modal.params.model.rules.ladders).includes('ladder_three')"
-                >
-                  <th class="sku-item">
-                    {{ modal.params.model.rules.ladders.ladder_three }}人团价格
-                  </th>
-                  <th class="sku-item" v-if="modal.params.model.rules.is_leader_discount == 1">
-                    {{ modal.params.model.rules.ladders.ladder_three }}人团长价格
-                  </th>
-                </template>
-              </template>
-              <th class="sku-item">操作</th>
-            </tr>
-          </thead>
-          <tbody>
-            <tr v-for="(sp, spindex) in goods.skuPrice" :key="sp">
-              <td class="sku-item" v-for="st in sp.goods_sku_text" :key="st">
-                {{ st }}
-              </td>
-              <td class="sku-item">{{ sp.stock }}</td>
-              <td class="sku-item">{{ sp.price }}</td>
-              <td class="sku-item">{{ sp.sales }}</td>
-              <th class="sku-item">
-                <el-input
-                  v-if="goods.activity_sku_prices[spindex].status == 'up'"
-                  type="number"
-                  v-model="goods.activity_sku_prices[spindex].stock"
-                ></el-input>
-              </th>
-              <th v-if="['groupon', 'seckill'].includes(modal.params.model.type)" class="sku-item">
-                <el-input
-                  v-if="goods.activity_sku_prices[spindex].status == 'up'"
-                  type="number"
-                  v-model="goods.activity_sku_prices[spindex].price"
-                  :disabled="modal.params.activityStatus"
-                ></el-input>
-              </th>
-              <th
-                class="sku-item"
-                v-if="
-                  modal.params.model.type == 'groupon' &&
-                  modal.params.model.rules.is_leader_discount == 1
-                "
-              >
-                <el-input
-                  v-if="goods.activity_sku_prices[spindex].status == 'up'"
-                  type="number"
-                  v-model="goods.activity_sku_prices[spindex].leader_price"
-                  :disabled="modal.params.activityStatus"
-                ></el-input>
-              </th>
-              <template v-if="modal.params.model.type == 'groupon_ladder'">
-                <th class="sku-item">
-                  <el-input
-                    v-if="goods.activity_sku_prices[spindex].status == 'up'"
-                    type="number"
-                    v-model="goods.activity_sku_prices[spindex].ladder_one"
-                    :disabled="modal.params.activityStatus"
-                  ></el-input>
-                </th>
-                <th class="sku-item" v-if="modal.params.model.rules.is_leader_discount == 1">
-                  <el-input
-                    v-if="goods.activity_sku_prices[spindex].status == 'up'"
-                    type="number"
-                    v-model="goods.activity_sku_prices[spindex].ladder_one_leader"
-                    :disabled="modal.params.activityStatus"
-                  ></el-input>
-                </th>
-                <th class="sku-item">
-                  <el-input
-                    v-if="goods.activity_sku_prices[spindex].status == 'up'"
-                    type="number"
-                    v-model="goods.activity_sku_prices[spindex].ladder_two"
-                    :disabled="modal.params.activityStatus"
-                  ></el-input>
-                </th>
-                <th class="sku-item" v-if="modal.params.model.rules.is_leader_discount == 1">
-                  <el-input
-                    v-if="goods.activity_sku_prices[spindex].status == 'up'"
-                    type="number"
-                    v-model="goods.activity_sku_prices[spindex].ladder_two_leader"
-                    :disabled="modal.params.activityStatus"
-                  ></el-input>
-                </th>
-                <template
-                  v-if="Object.keys(modal.params.model.rules.ladders).includes('ladder_three')"
-                >
-                  <th class="sku-item">
-                    <el-input
-                      type="number"
-                      v-model="goods.activity_sku_prices[spindex].ladder_three"
-                      v-if="goods.activity_sku_prices[spindex].status == 'up'"
-                      :disabled="modal.params.activityStatus"
-                    ></el-input>
-                  </th>
-                  <th class="sku-item" v-if="modal.params.model.rules.is_leader_discount == 1">
-                    <el-input
-                      v-if="goods.activity_sku_prices[spindex].status == 'up'"
-                      type="number"
-                      v-model="goods.activity_sku_prices[spindex].ladder_three_leader"
-                      :disabled="modal.params.activityStatus"
-                    ></el-input>
-                  </th>
-                </template>
-              </template>
-              <th class="sku-item">
-                <template v-if="!modal.params.activityStatus">
-                  <el-button
-                    v-if="goods.activity_sku_prices[spindex].status == 'up'"
-                    class="is-link"
-                    type="danger"
-                    @click="goods.activity_sku_prices[spindex].status = 'down'"
-                    >取消</el-button
-                  >
-                  <el-button
-                    v-if="goods.activity_sku_prices[spindex].status == 'down'"
-                    class="is-link"
-                    type="primary"
-                    @click="goods.activity_sku_prices[spindex].status = 'up'"
-                    >参与</el-button
-                  >
-                </template>
-                <template v-if="modal.params.activityStatus">-</template>
-              </th>
-            </tr>
-          </tbody>
-        </table>
-      </div>
-    </el-main>
-    <el-footer class="sa-footer--submit">
-      <el-button type="primary" @click="modelBack">确定</el-button>
-    </el-footer>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive } from 'vue';
-  import { api } from '../activity.service';
-
-  const emit = defineEmits(['modalCallBack']);
-  const props = defineProps(['modal']);
-
-  const findItem = props.modal.params.model.goods_list.find(
-    (item) => item.id == props.modal.params.goods_id,
-  );
-
-  const goods = reactive({
-    skus: [],
-    skuPrice: [],
-    activity_sku_prices: [],
-  });
-
-  async function getSkus() {
-    const { data } = await api.activity.skus({
-      id: props.modal.params.model.id,
-      goods_id: props.modal.params.goods_id,
-      activity_type: props.modal.params.model.type,
-      start_time: props.modal.params.model.start_time,
-      end_time: props.modal.params.model.end_time,
-      prehead_time: props.modal.params.model.prehead_time,
-    });
-    goods.skus = data.skus;
-    goods.skuPrice = data.sku_prices;
-    goods.activity_sku_prices = findItem.activity_sku_prices || data.activity_sku_prices;
-  }
-
-  function modelBack() {
-    emit('modalCallBack', { event: 'confirm', data: goods.activity_sku_prices });
-  }
-
-  onMounted(() => {
-    getSkus();
-  });
-</script>
-<style lang="scss" scoped>
-  .sku-table-wrap {
-    width: 100%;
-    overflow: auto;
-    .sku-table {
-      font-size: 12px;
-      font-weight: 500;
-      thead {
-        line-height: 40px;
-        background: var(--sa-table-header-bg);
-        color: var(--subtitle);
-      }
-      tbody {
-        tr {
-          line-height: 48px;
-          color: var(--sa-font);
-          &:nth-of-type(2n) {
-            background: var(--sa-table-striped);
-          }
-        }
-      }
-      th,
-      td {
-        padding: 0 16px;
-        text-align: left;
-      }
-    }
-    .sku-item {
-      min-width: 100px;
-      flex-shrink: 0;
-    }
-  }
-</style>

+ 0 - 194
src/app/shop/admin/activity/groupon/detail.vue

@@ -1,194 +0,0 @@
-<template>
-  <el-container class="withdraw-page panel-block">
-    <el-main class="sa-p-24">
-      <el-table class="sa-table" :data="[{}]" row-key="id" stripe>
-        <template #empty>
-          <sa-empty />
-        </template>
-        <el-table-column label="拼团商品" min-width="490" align="center">
-          <template #default>
-            <div class="sa-flex" v-if="table.data.goods">
-              <div class="sa-m-r-8">
-                <sa-preview :url="table.data.goods.image" size="40"></sa-preview>
-              </div>
-              <div class="sa-flex-col sa-col-top">
-                <span
-                  class="sa-table-line-1 goods-title cursor-pointer"
-                  @click="openGoods(table.data.goods_id)"
-                >
-                  {{ table.data.goods.title || '-' }}
-                </span>
-                <span class="sa-table-line-1"> 成团人数:{{ table.data.num }} </span>
-              </div>
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column label="开团时间" min-width="174" align="center">
-          <template #default>
-            {{ table.data.create_time || '-' }}
-          </template>
-        </el-table-column>
-        <el-table-column label="拼团状态" min-width="80" align="center">
-          <template #default>
-            <div
-              :class="
-                table.data.status == 'invalid'
-                  ? 'sa-color--danger'
-                  : table.data.status == 'ing'
-                  ? 'sa-color--warning'
-                  : 'sa-color--success'
-              "
-            >
-              {{ table.data.status_text || '-' }}
-            </div>
-          </template>
-        </el-table-column>
-      </el-table>
-      <el-table class="sa-table" :data="table.data.grouponLogs" row-key="id" stripe>
-        <template #empty>
-          <sa-empty />
-        </template>
-        <el-table-column label="身份" min-width="80" align="center">
-          <template #default="scope">
-            <span v-if="scope.row.is_leader == 1">团长</span>
-            <span v-else>团员</span>
-          </template>
-        </el-table-column>
-        <el-table-column label="头像" width="64">
-          <template #default="scope">
-            <sa-image :url="scope.row.avatar" size="32" radius="16"></sa-image>
-          </template>
-        </el-table-column>
-        <el-table-column label="昵称" width="258">
-          <template #default="scope">
-            <sa-user-profile
-              :user="scope.row"
-              :id="scope.row.user_id"
-              :isAvatar="false"
-              :isHover="!scope.row.is_fictitious"
-            />
-          </template>
-        </el-table-column>
-        <el-table-column label="参团时间" min-width="170" align="center">
-          <template #default="scope">
-            {{ scope.row.create_time || '-' }}
-          </template>
-        </el-table-column>
-
-        <el-table-column label="操作" min-width="170" align="center">
-          <template #default="scope">
-            <span
-              class="goods-title cursor-pointer"
-              @click="define(scope.row)"
-              v-if="scope.row.is_temp"
-              >确定</span
-            >
-            <span
-              class="sa-color--danger cursor-pointer sa-m-l-10"
-              @click="cancel(scope.$index)"
-              v-if="scope.row.is_temp"
-              >取消</span
-            >
-            <span v-if="scope.row.is_fictitious == 1 && !scope.row.is_temp">虚拟</span>
-          </template>
-        </el-table-column>
-      </el-table>
-    </el-main>
-    <el-footer class="sa-footer--submit sa-flex sa-row-between" v-if="table.data.status == 'ing'">
-      <el-popconfirm
-        width="fit-content"
-        confirm-button-text="确认"
-        cancel-button-text="取消"
-        title="确定要解散拼团吗?"
-        @confirm="invalidRow"
-      >
-        <template #reference>
-          <el-button v-auth="'shop.admin.activity.groupon.invalid'">解散拼团</el-button>
-        </template>
-      </el-popconfirm>
-
-      <el-button v-auth="'shop.admin.activity.groupon.adduser'" type="primary" @click="confirm"
-        >添加虚拟人数</el-button
-      >
-    </el-footer>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from '../activity.service';
-  import { api as dataApi } from '@/app/shop/admin/data/data.service';
-  const emit = defineEmits(['modalCallBack']);
-  const props = defineProps({
-    modal: {
-      type: Object,
-    },
-  });
-  // 列表
-  const table = reactive({
-    data: [],
-    order: '',
-    sort: '',
-    selected: [],
-  });
-  const is_temp = ref(false);
-  const loading = ref(true);
-  // 获取
-  async function getDetail(id) {
-    loading.value = true;
-    const { error, data } = await api.groupon.detail(id);
-    error === 0 && (table.data = data);
-    loading.value = false;
-  }
-  //添加虚拟人
-  async function confirm() {
-    const { data } = await dataApi.fakeUser.getRandom();
-    table.data.grouponLogs.push({
-      avatar: data.avatar,
-      nickname: data.nickname,
-      is_fictitious: 1,
-      is_temp: true,
-    });
-  }
-  //确定
-  async function define(row) {
-    const { error } = await api.groupon.addUser(table.data.id, {
-      avatar: row.avatar,
-      nickname: row.nickname,
-    });
-    if (error == 0) {
-      is_temp.value = false;
-      getDetail(props.modal.params.id);
-    }
-  }
-  //取消
-  function cancel(e) {
-    table.data.grouponLogs.splice(e, 1);
-  }
-  //解散拼团
-  async function invalidRow(row) {
-    const { error } = await api.groupon.invalid(table.data.id);
-    if (error == 0) {
-      emit('modalCallBack', {
-        event: 'confirm',
-      });
-    }
-  }
-  async function init() {
-    if (props.modal.params.id) {
-      await getDetail(props.modal.params.id);
-    }
-  }
-  onMounted(() => {
-    init();
-  });
-</script>
-<style lang="scss" scoped>
-  .goods-title {
-    color: var(--el-color-primary);
-  }
-  :deep() {
-    .nickname {
-      font-size: 14px !important;
-    }
-  }
-</style>

+ 0 - 257
src/app/shop/admin/activity/groupon/index.vue

@@ -1,257 +0,0 @@
-<template>
-  <el-container class="groupon-view panel-block">
-    <el-header class="sa-header">
-      <el-tabs class="sa-tabs" v-model="filterParams.data.status" @tab-change="getData(1)">
-        <el-tab-pane
-          v-for="sl in statusList"
-          :key="sl"
-          :label="sl.name"
-          :name="sl.type"
-        ></el-tab-pane>
-      </el-tabs>
-      <div class="sa-title sa-flex sa-row-between">
-        <div class="label sa-flex">
-          <span class="left">拼团列表</span>
-          <search-condition
-            :conditionLabel="filterParams.conditionLabel"
-            @deleteFilter="deleteFilter"
-          ></search-condition>
-        </div>
-        <div>
-          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
-          <el-button class="sa-button-refresh" icon="Search" @click="openFilter"></el-button>
-        </div>
-      </div>
-    </el-header>
-    <el-main class="sa-p-0" v-loading="loading">
-      <el-table height="100%" class="sa-table" :data="table.data" row-key="id" stripe>
-        <template #empty>
-          <sa-empty />
-        </template>
-        <el-table-column prop="id" label="ID" min-width="100" sortable> </el-table-column>
-        <el-table-column label="拼团商品信息" min-width="240" align="center">
-          <template #default="scope">
-            <div class="sa-flex sa-row-center" v-if="scope.row.goods">
-              <div class="sa-m-r-8">
-                <sa-preview :url="scope.row.goods.image" size="40"></sa-preview>
-              </div>
-              <div class="sa-flex-col sa-col-top">
-                <span
-                  class="sa-table-line-1 goods-title cursor-pointer"
-                  @click="onOpenGoodsDetail(scope.row.goods_id)"
-                >
-                  {{ scope.row.goods.title || '-' }}
-                </span>
-                <span class="sa-table-line-1 goods-subtitle"> 成团人数:{{ scope.row.num }} </span>
-              </div>
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column label="开团时间" min-width="180" align="center">
-          <template #default="scope">
-            {{ scope.row.create_time || '-' }}
-          </template>
-        </el-table-column>
-        <el-table-column label="团长" min-width="124">
-          <template #default="scope">
-            <div class="sa-flex">
-              <sa-user-profile :user="scope.row.user" :id="scope.row.user_id" />
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column label="已参团成员" min-width="180" align="center">
-          <template #default="scope">
-            <!-- <div class="sa-table-line-1"> -->
-            <div class="sa-flex sa-row-center">
-              <div v-for="i in scope.row.grouponLogs" :key="i" class="sa-m-r-4">
-                <sa-preview :url="i.avatar" size="32" radius="16"></sa-preview>
-              </div>
-            </div>
-            <!-- </div> -->
-          </template>
-        </el-table-column>
-        <el-table-column label="剩余名额" min-width="90" align="center">
-          <template #default="scope">
-            {{ scope.row.num - scope.row.current_num }}
-          </template>
-        </el-table-column>
-        <el-table-column label="组团有效时间" min-width="180" align="center">
-          <template #default="scope">
-            {{ scope.row.finish_time || '-' }}
-          </template>
-        </el-table-column>
-        <el-table-column label="拼团状态" min-width="88" align="center">
-          <template #default="scope">
-            <div
-              :class="
-                scope.row.status == 'invalid'
-                  ? 'sa-color--danger'
-                  : scope.row.status == 'ing'
-                  ? 'sa-color--warning'
-                  : 'sa-color--success'
-              "
-            >
-              {{ scope.row.status_text || '-' }}
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column fixed="right" label="操作" min-width="100">
-          <template #default="scope">
-            <el-button
-              v-auth="'shop.admin.activity.groupon.detail'"
-              class="is-link"
-              type="primary"
-              @click="detailRow(scope.row)"
-              >查看详情</el-button
-            >
-          </template>
-        </el-table-column>
-      </el-table>
-    </el-main>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from '../activity.service';
-  import { useModal } from '@/sheep/hooks';
-  import { useSearch } from '@/sheep/components/sa-table/sa-search/useSearch';
-  import { composeFilter } from '@/sheep/utils';
-  import { useRoute } from 'vue-router';
-  import { cloneDeep } from 'lodash';
-  import GoodsEdit from '@/app/shop/admin/goods/goods/edit.vue';
-  import GrouponDetail from './detail.vue';
-  const route = useRoute();
-  // 列表
-  const table = reactive({
-    data: [],
-    order: '',
-    sort: '',
-    selected: [],
-  });
-  const statusList = [
-    {
-      name: '全部',
-      type: 'all',
-    },
-    {
-      name: '进行中',
-      type: 'ing',
-    },
-    {
-      name: '已成团',
-      type: 'finish',
-    },
-    {
-      name: '虚拟成团',
-      type: 'finish_fictitious',
-    },
-    {
-      name: '已过期',
-      type: 'invalid ',
-    },
-  ];
-  const loading = ref(true);
-  const filterParams = reactive({
-    tools: {
-      status: { label: '状态', value: 'all' },
-      user: {
-        type: 'tinputprepend',
-        label: '团长信息',
-        field: 'user',
-        placeholder: '请输入查询内容',
-        user: {
-          field: 'user_id',
-          value: '',
-        },
-        options: [
-          {
-            label: '团长ID',
-            value: 'user_id',
-          },
-          {
-            label: '团长昵称',
-            value: 'user.nickname',
-          },
-          {
-            label: '团长手机号',
-            value: 'user.mobile',
-          },
-        ],
-      },
-      'goods.title': {
-        type: 'tinput',
-        label: '商品名称',
-        field: 'goods.title',
-        value: '',
-      },
-    },
-    data: {
-      user: { field: 'user_id', value: '' },
-      status: 'all',
-      'goods.title': '',
-    },
-    conditionLabel: {},
-  });
-  const { openFilter, deleteFilter } = useSearch({ filterParams, getData });
-  // 获取
-  async function getData() {
-    loading.value = true;
-    let tempSearch = cloneDeep(filterParams.data);
-    if (route.query) {
-      tempSearch['activity_id'] = route.query.activity_id;
-    }
-    let search = composeFilter(tempSearch, {
-      'user.nickname': 'like',
-      'goods.title': 'like',
-    });
-    const { error, data } = await api.groupon.list({
-      order: table.order,
-      sort: table.sort,
-      ...search,
-    });
-    if (error === 0) {
-      table.data = data.data;
-    }
-    loading.value = false;
-  }
-
-  function detailRow(row) {
-    useModal(
-      GrouponDetail,
-      {
-        title: '详情',
-        id: row.id,
-      },
-      {
-        confirm: () => {
-          getData();
-        },
-        close: () => {
-          getData();
-        },
-      },
-    );
-  }
-
-  function onOpenGoodsDetail(id) {
-    useModal(GoodsEdit, {
-      title: '商品',
-      type: 'edit',
-      id: id,
-    });
-  }
-
-  onMounted(() => {
-    getData();
-  });
-</script>
-<style lang="scss" scoped>
-  .groupon-view {
-    .goods-title {
-      color: var(--el-color-primary);
-    }
-    .goods-subtitle {
-      font-size: 12px;
-    }
-  }
-</style>

+ 3 - 10
src/app/shop/admin/category/category.service.js

@@ -1,24 +1,17 @@
 import { SELECT, CRUD } from '@/sheep/request/crud';
-import { request } from '@/sheep/request';
 
 const route = {
   path: 'category',
   name: 'shop.admin.category',
   component: () => import('@/app/shop/admin/category/index.vue'),
   meta: {
-    title: '分类',
+    title: '商品分类',
   },
 };
 
 const api = {
-  ...CRUD('shop/admin/category'),
-  select: (params) => SELECT('shop/admin/category', params),
-  goodsSelect: (params) =>
-    request({
-      url: 'shop/admin/category/goodsSelect',
-      method: 'GET',
-      params,
-    }),
+  ...CRUD('shop/admin/category_tag'),
+  select: (params) => SELECT('shop/admin/category_tag', params),
 };
 
 export { route, api };

+ 49 - 646
src/app/shop/admin/category/edit.vue

@@ -2,667 +2,70 @@
   <el-container>
     <el-main>
       <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
-        <el-form-item label="分类类型">
-          <el-radio-group v-model="level" :disabled="modal.params.type == 'edit'" @change="changeType">
-            <el-radio :label="1">一级分类</el-radio>
-            <el-radio :label="2">二级分类</el-radio>
-            <el-radio :label="3">三级分类</el-radio>
-          </el-radio-group>
+        <el-form-item label="名称" prop="title">
+          <el-input v-model="form.model.title" placeholder="请填写商品分类名称"></el-input>
         </el-form-item>
-        <el-form-item label="分类样式" v-if="level == 1">
-          <div class="sa-flex">
-            <div class="category-style" v-for="(img, index) in styleData.first" :key="index"
-              @click="slectStyle(img.styleName)">
-              <div class="category-image">
-                <img :src="'./static/images/shop/category/' + img.styleName + '.png'" class="type-img"
-                  :class="form.model.style == img.styleName ? 'activeBorder' : ''" />
-                <el-icon v-show="form.model.style == img.styleName" class="badge">
-                  <CircleCheckFilled />
-                </el-icon>
-              </div>
-              <div class="sa-flex sa-row-center">
-                <div class="category-title">样式{{ img.id }}</div>
-                <el-popover placement="right" title="" :width="236" trigger="hover">
-                  <template #reference>
-                    <img class="category-icon" src="/static/images/shop/category/warning.png" />
-                  </template>
-                  <div class="hover-img">
-                    <img :src="'./static/images/shop/category/' + img.styleName + '.png'" />
-                  </div>
-                </el-popover>
-              </div>
-            </div>
-          </div>
+        <el-form-item label="排序" prop="sort">
+          <el-input v-model="form.model.sort" placeholder="请填写排序"></el-input>
         </el-form-item>
-        <el-form-item label="分类样式" v-if="level == 2">
-          <div class="sa-flex">
-            <div class="category-style" v-for="(img, index) in styleData.second" :key="index"
-              @click="slectStyle(img.styleName)">
-              <div class="category-image" :class="form.model.style == img.styleName ? 'activeBorder' : ''">
-                <img :src="'./static/images/shop/category/' + img.styleName + '.png'" class="type-img" />
-                <el-icon v-show="form.model.style == img.styleName" class="badge">
-                  <CircleCheckFilled />
-                </el-icon>
-              </div>
-              <div class="sa-flex sa-row-center">
-                <div class="category-title">样式{{ img.id }}</div>
-                <el-popover placement="right" title="" :width="236" trigger="hover">
-                  <template #reference>
-                    <img class="category-icon" src="/static/images/shop/category/warning.png" />
-                  </template>
-                  <div class="hover-img">
-                    <img :src="'./static/images/shop/category/' + img.styleName + '.png'" />
-                  </div>
-                </el-popover>
-              </div>
-            </div>
-          </div>
-        </el-form-item>
-        <el-form-item label="分类样式" v-if="level == 3">
-          <div class="sa-flex">
-            <div class="category-style" v-for="(img, index) in styleData.third" :key="index"
-              @click="slectStyle(img.styleName)">
-              <div class="category-image" :class="form.model.style == img.styleName ? 'activeBorder' : ''">
-                <img :src="'./static/images/shop/category/' + img.styleName + '.png'" class="type-img" />
-                <el-icon v-show="form.model.style == img.styleName" class="badge">
-                  <CircleCheckFilled />
-                </el-icon>
-              </div>
-              <div class="sa-flex sa-row-center">
-                <div class="category-title">样式{{ img.id }}</div>
-                <el-popover placement="right" title="" :width="236" trigger="hover">
-                  <template #reference>
-                    <img class="category-icon" src="/static/images/shop/category/warning.png" />
-                  </template>
-                  <div class="hover-img">
-                    <img :src="'./static/images/shop/category/' + img.styleName + '.png'" />
-                  </div>
-                </el-popover>
-              </div>
-            </div>
-          </div>
-        </el-form-item>
-        <el-form-item label="分类名称" prop="name">
-          <el-input v-model="form.model.name" placeholder="请输入分类名称"></el-input>
-        </el-form-item>
-        <el-form-item label="描述" prop="description">
-          <el-input v-model="form.model.description" placeholder="请输入描述"></el-input>
-        </el-form-item>
-        <el-form-item label="分类权重">
-          <el-input v-model="form.model.weigh" type="number" :min="0" placeholder="请输入分类权重"></el-input>
-        </el-form-item>
-        <div class="head sa-flex sa-row-between">
-          <div class="des">分类数据</div>
-          <div class="btn">
-            <a @click="addfirst" v-if="level">+ 插入一级分类</a>
-          </div>
-        </div>
-        <div style="overflow: auto">
-          <div class="custom-tree-header sa-flex">
-            <div class="custom-tree-box sa-flex">
-              <div class="expanded-width-id" v-if="props.modal.params.type == 'edit'"> ID </div>
-              <div class="expanded-width-input">分类名称</div>
-              <div class="expanded-width-image sa-flex sa-row-center"> 分类图片 </div>
-              <div class="expanded-width-des">描述</div>
-              <div class="expanded-width-sort">
-                <el-popover placement="bottom" title="" width="120" trigger="hover">
-                  <template #reference> 排序 </template>
-                  <div class="popover-container">
-                    权重以倒序排列,默认值为0,相同权重则以ID优先
-                  </div>
-                </el-popover>
-              </div>
-              <div class="expanded-width-operation">操作</div>
-            </div>
-          </div>
-          <div class="sa-tree-table-content sa-flex">
-            <el-tree :data="tree.data" ref="treeRef" node-key="id" default-expand-all :expand-on-click-node="false"
-              :props="defaultProps">
-              <template #default="{ node, data }">
-                <div class="sa-flex drag-item" v-if="!(data.deleted && data.deleted == 1)">
-                  <div class="expanded-width-id sa-flex" v-if="props.modal.params.type == 'edit'">
-                    <span v-if="(data.id + '').indexOf('add') == -1 && (data.id + '').substring(0, 3) !== 'new'">{{
-                      data.id
-                    }}</span>
-                  </div>
-
-                  <div class="sa-flex expanded-width-input">
-                    <div class="expanded-icon sa-flex" :class="operation(node.level)">
-                      <div v-if="data.children && data.children.length > 0">
-                        <el-icon v-if="node.expanded" @click="isexpanded(node)">
-                          <SemiSelect />
-                        </el-icon>
-                        <el-icon v-if="!node.expanded" @click="isexpanded(node)">
-                          <Plus />
-                        </el-icon>
-                      </div>
-                    </div>
-                    <div style="margin-left: 16px">
-                      <el-input v-model="data.name" placeholder="请输入分类名称" />
-                    </div>
-                    <div class="append-title sa-m-l-12" v-if="node.level == 1 && level != 1">
-                      <a @click="append(data)">+ 插入二级分类</a>
-                    </div>
-                    <div class="append-title sa-m-l-12" v-if="node.level == 2 && level == 3">
-                      <a @click="append(data)">+ 插入三级分类</a>
-                    </div>
-                  </div>
-                  <div class="sa-flex expanded-width-image sa-row-center">
-                    <el-popover placement="bottom" :width="200" trigger="hover" content="建议尺寸:缩略图150X150">
-                      <template #reference>
-                        <div class="sa-flex icon-image">
-                          <sa-uploader v-model="data.image" fileType="image" size="32"></sa-uploader>
-                        </div>
-                      </template>
-                    </el-popover>
-                  </div>
-                  <div class="expanded-width-des">
-                    <el-input v-model="data.description" placeholder="请输入分类描述" />
-                  </div>
-                  <div class="expanded-width-sort">
-                    <el-input v-model="data.weigh" type="number" :min="0" placeholder="请输入排序" />
-                  </div>
-                  <div class="expanded-width-operation">
-                    <span @click="hidden(data)">
-                      <span v-if="data.status == 'normal'" style="color: #999999">隐藏</span>
-                      <span v-else>显示</span>
-                    </span>
-                    <span @click="remove(node, data)" style="color: #ff4d4f; margin-left: 16px">删除</span>
-                  </div>
-                </div>
-              </template>
-            </el-tree>
-          </div>
-        </div>
       </el-form>
     </el-main>
     <el-footer class="sa-footer--submit">
-      <el-button v-if="modal.params.type == 'add'" v-auth="'shop.admin.category.add'" type="primary"
-        @click="confirm">保存</el-button>
-      <el-button v-if="modal.params.type == 'edit'" v-auth="'shop.admin.category.edit'" type="primary"
-        @click="confirm">更新</el-button>
+      <el-button v-if="modal.params.type == 'add'" type="primary" @click="confirm">保存</el-button>
+      <el-button v-if="modal.params.type == 'edit'" type="primary" @click="confirm">更新</el-button>
     </el-footer>
   </el-container>
 </template>
 <script setup>
-import { onMounted, reactive, ref, unref } from 'vue';
-import { api } from './category.service';
-import { cloneDeep } from 'lodash';
-
-const emit = defineEmits(['modalCallBack']);
-const props = defineProps({
-  modal: {
-    type: Object,
-  },
-});
-// 添加 编辑 form
-let formRef = ref(null);
-const form = reactive({
-  model: {
-    name: '',
-    style: 'first_one',
-    description: '',
-    weigh: 0,
-    status: 'normal',
-  },
-  rules: {
-    name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }],
-  },
-});
-const id = ref(0)
-// 选择样式
-const slectStyle = (e) => {
-  form.model.style = e;
-};
-const treeRef = ref(null);
-
-// ——————————————————————————————————
-// tree 分类树状
-const tree = reactive({
-  data: [],
-});
-const defaultProps = {
-  children: 'children',
-  label: 'id',
-};
-//折叠
-const isexpanded = (data) => {
-  data.expanded = !data.expanded;
-};
-//改变分类类型
-const changeType = (e) => {
-  form.model.style = '';
-  loopChildren(tree.data);
-};
-// 初始化数据
-const level = ref(1);
-let multIndex = ref(1);
-function loopChildren(lists, lindex = 1) {
-  multIndex.value = 1;
-  multLevel(tree.data);
-  if (multIndex.value <= level.value) {
-    lindex += 1;
-    if (lists.length == 0) {
-      lindex -= 1;
-      if (lindex <= level.value) {
-        lists.push({
-          id: 'new' + (id.value++),
-          name: '',
-          image: '',
-          description: '',
-          weigh: 0,
-          status: 'normal',
-        });
-        loopChildren(lists, lindex);
-      }
-    } else {
-      if (lindex <= level.value) {
-        lists.forEach((k) => {
-          if (!k.children) {
-            k.children = [
-              {
-                id: 'new' + (id.value++),
-                name: '',
-                image: '',
-                description: '',
-                weigh: 0,
-                status: 'normal',
-              },
-            ];
-          }
-          loopChildren(k.children, lindex);
-        });
-      }
-    }
-  } else if (multIndex.value > level.value) {
-    lists.forEach((l) => {
-      if (lindex == level.value) {
-        delete l.children;
-      } else {
-        lindex += 1;
-        if (l.children && l.children.length > 0) {
-          loopChildren(l.children, lindex);
-        }
-      }
-    });
-  }
-}
-function multLevel(arr) {
-  arr.forEach((a) => {
-    if (a.children) {
-      multIndex.value += 1;
-      multLevel(a.children);
-    }
+  import { cloneDeep } from 'lodash';
+  import { onMounted, reactive, ref, unref } from 'vue';
+  import { api } from './category.service';
+  const emit = defineEmits(['modalCallBack']);
+  const props = defineProps({
+    modal: {
+      type: Object,
+    },
   });
-}
-
-//添加一级分类
-const addfirst = () => {
-  tree.data.unshift({
-    id: 'new' + (id.value++),
-    name: '',
-    status: 'normal',
-    description: '',
-    image: '',
-    weigh: 0,
-    children: [],
+  // 添加 编辑 form
+  let formRef = ref(null);
+  const form = reactive({
+    model: {
+      title: '',
+      sort: '0',
+    },
+    rules: {
+      title: [{ required: true, message: '请填写商品分类名称', trigger: 'blur' }],
+      sort: [{ required: true, message: '请填写排序', trigger: 'blur' }],
+    },
   });
-};
-// 更改tree 收缩padding
-const operation = (id) => {
-  if (id == 2) {
-    return 'level-2';
-  } else if (id == 3) {
-    return 'level-3';
-  }
-};
-const append = (data) => {
-  const newChild = {
-    id: 'new' + (id.value++),
-    name: '',
-    image: '',
-    description: '',
-    weigh: 0,
-    status: 'normal',
-  };
-  if (!data.children) {
-    data.children = [];
-  }
-  data.children.unshift(newChild);
-};
-//隐藏分类
-const hidden = (data) => {
-  if (data.status == 'normal') {
-    data.status = 'hidden';
-  } else {
-    data.status = 'normal';
-  }
-};
-//删除分类
-const remove = (node, data) => {
-  const parent = node.parent;
-  const children = parent.data.children || parent.data;
-  const index = children.findIndex((d) => d.id == data.id);
-  if (children[index].children) {
-    children[index].deleted = 1;
-    children[index].children.forEach((i) => {
-      i.deleted = 1;
-      if (i.children) {
-        i.children.forEach((j) => {
-          j.deleted = 1;
-        });
+  const loading = ref(false);
+  // 获取详情
+  async function getDetail(id) {
+    loading.value = true;
+    const { error, data } = await api.detail(id);
+    error === 0 && (form.model = data);
+    loading.value = false;
+  }
+  // 表单关闭时提交
+  async function confirm() {
+    unref(formRef).validate(async (valid) => {
+      if (!valid) return;
+      let submitForm = cloneDeep(form.model);
+      const { error } =
+        props.modal.params.type == 'add'
+          ? await api.add(submitForm)
+          : await api.edit(props.modal.params.id, submitForm);
+      if (error == 0) {
+        emit('modalCallBack', { event: 'confirm' });
       }
     });
-  } else {
-    children[index].deleted = 1;
   }
-  tree.data = [...tree.data]
-};
-// 样式类型
-const styleData = {
-  first: [
-    { styleName: 'first_one', id: '一' },
-    { styleName: 'first_two', id: '二' },
-  ],
-  second: [{ styleName: 'second_one', id: '一' }],
-  third: [{ styleName: 'third_one', id: '一' }],
-};
-const loading = ref(false);
-// 获取详情
-async function getDetail(id) {
-  loading.value = true;
-  const { error, data } = await api.detail(id);
-  if (error === 0) {
-    form.model = data.category;
-    tree.data = data.categories;
-    if (data.category.style.substring(0, 1) == 'f') {
-      level.value = 1;
-    } else if (data.category.style.substring(0, 1) == 's') {
-      level.value = 2;
-    } else {
-      level.value = 3;
+  async function init() {
+    if (props.modal.params.id) {
+      await getDetail(props.modal.params.id);
     }
   }
-  loading.value = false;
-}
-
-// 表单关闭时提交
-function confirm() {
-  unref(formRef).validate(async (valid) => {
-    if (!valid) return;
-    let submitTree = {
-      categories: [],
-    };
-    tree.data.forEach(i => {
-      if (i.id?.toString().substring(0, 3) === 'new') {
-        delete i.id
-      }
-      if (i.children) {
-        i.children.forEach(j => {
-          if (j.id?.toString().substring(0, 3) === 'new') {
-            delete j.id
-          }
-          if (j.children) {
-            j.children.forEach(k => {
-              if (k.id?.toString().substring(0, 3) === 'new') {
-                delete k.id
-              }
-            })
-          }
-        })
-      }
-    })
-    submitTree.categories = cloneDeep(tree.data);
-    let submitForm = cloneDeep(form.model);
-    let params = Object.assign(submitForm, submitTree);
-    const { error } =
-      props.modal.params.type == 'add'
-        ? await api.add(params)
-        : await api.edit(props.modal.params.id, params);
-    if (error == 0) {
-      emit('modalCallBack', {
-        event: 'confirm',
-        data: { uuID: props.modal.params.uuID },
-      });
-    }
+  onMounted(() => {
+    init();
   });
-}
-async function init() {
-  if (props.modal.params.id) {
-    await getDetail(props.modal.params.id);
-  } else {
-    loopChildren(tree.data);
-  }
-}
-onMounted(() => {
-  init();
-});
 </script>
-<style lang="scss" scoped>
-a {
-  cursor: pointer;
-}
-
-//分类列表
-.custom-tree-header {
-  width: 100%;
-
-  .custom-tree-box {
-    flex: 1;
-    position: unset;
-    height: 40px;
-    background: var(--sa-table-header-bg);
-    font-size: 12px;
-    font-weight: 500;
-  }
-}
-
-.type-img {
-  width: 100%;
-  height: 100%;
-  border-radius: 4px;
-}
-
-.head {
-  width: 100%;
-  height: 40px;
-  background: var(--sa-table-header-bg);
-  padding: 0 16px;
-
-  .des {
-    color: var(--sa-subtitle);
-    font-size: 14px;
-    font-weight: 500;
-  }
-
-  .btn {
-    color: #806af6;
-    font-size: 12px;
-  }
-}
-
-.drag-item {
-  width: 100%;
-  height: 58px;
-}
-
-.expanded-width-id {
-  width: 40px;
-  padding-left: 16px;
-}
-
-.expanded-width-input {
-  width: 380px;
-  padding-left: 16px;
-}
-
-.expanded-width-image {
-  width: 80px;
-}
-
-.icon-image {
-  box-sizing: border-box;
-  margin: 10px;
-}
-
-.expanded-width-des {
-  width: 240px;
-  padding-left: 16px;
-}
-
-.expanded-width-sort {
-  width: 120px;
-  padding-left: 16px;
-}
-
-.expanded-width-operation {
-  width: 104px;
-  padding-left: 16px;
-}
-
-.expanded-width-id,
-.expanded-width-input,
-.expanded-width-image,
-.expanded-width-des,
-.expanded-width-sort,
-.expanded-width-operation {
-  flex-shrink: 0;
-}
-
-.level-2 {
-  padding-left: 40px;
-}
-
-.level-3 {
-  padding-left: 80px;
-}
-
-:deep() {
-  .el-radio-button__inner {
-    padding: 0;
-    border: none;
-  }
-
-  .el-popover.el-popper {
-    padding: 8px;
-  }
-
-  .el-tree-node__content {
-    width: 100%;
-    height: 100%;
-  }
-
-  .el-tree-node__expand-icon {
-    display: none;
-  }
-
-  .el-tree-node__content {
-    padding-left: 0 !important;
-  }
-
-  .card-item {
-    position: relative;
-    overflow: hidden;
-    width: 32px !important;
-    height: 32px !important;
-    color: var(--sa-subtitle);
-    border: 1px solid var(--sa-border);
-    border-radius: 4px;
-
-    .image-overlay {
-      width: 32px !important;
-      height: 32px !important;
-      position: absolute;
-      top: 0;
-      left: 0;
-      background: var(--sa-basic-mask-background);
-      color: var(--sa-basic-mask-color);
-      display: none;
-
-      i {
-        font-size: 16px;
-        cursor: pointer;
-      }
-    }
-
-    &:hover {
-      .image-overlay {
-        display: flex;
-      }
-    }
-  }
-
-  .upload-icon {
-    width: 32px !important;
-    height: 32px !important;
-    background: var(--sa-background-assist);
-    border: 1px dashed var(--sa-border);
-    border-radius: 4px;
-    cursor: pointer;
-
-    i {
-      color: var(--sa-place);
-      font-size: 14px !important;
-    }
-  }
-}
-
-.el-tree {
-  flex: 1;
-  position: unset;
-}
-
-.append-title {
-  color: var(--el-color-primary);
-}
-
-.category-style {
-  margin-right: 24px;
-}
-
-.category-image {
-  width: 78px;
-  height: 124px;
-  display: flex;
-  position: relative;
-  border-radius: 4px;
-}
-
-.activeBorder {
-  border: 1px solid var(--el-color-primary);
-}
-
-.badge {
-  color: var(--el-color-primary);
-  width: 16px;
-  height: 16px;
-  line-height: 16px;
-  text-align: center;
-  border-radius: 50%;
-  font-weight: 600;
-  font-size: 14px;
-  position: absolute;
-  top: -8px;
-  right: -8px;
-}
-
-.category-title {
-  font-size: 12px;
-  line-height: 14px;
-  color: var(--sa-font);
-  margin-right: 4px;
-  margin-top: 8px;
-}
-
-.hover-img {
-  width: 220px;
-  height: 350px;
-  display: flex;
-}
-
-.category-icon {
-  width: 14px;
-  height: 14px;
-  margin-top: 8px;
-}
-</style>

+ 155 - 110
src/app/shop/admin/category/index.vue

@@ -1,142 +1,165 @@
 <template>
   <el-container class="category-view panel-block">
     <el-header class="sa-header">
+      <!-- 简化搜索组件 -->
+      <div class="search-container">
+        <sa-search-simple
+          :searchFields="searchFields"
+          :defaultValues="defaultSearchValues"
+          @search="(val) => getData(1, val)"
+          @reset="getData(1)"
+        >
+        </sa-search-simple>
+      </div>
       <div class="sa-title sa-flex sa-row-between">
-        <div class="label sa-flex">
-          <span class="left">商品分类</span>
-          <search-condition
-            :conditionLabel="filterParams.conditionLabel"
-            @deleteFilter="deleteFilter"
-          ></search-condition>
-        </div>
+        <div class="label sa-flex">商品分类</div>
         <div>
           <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
-          <el-button class="sa-button-refresh" icon="Search" @click="openFilter"></el-button>
-          <el-button v-auth="'shop.admin.category.add'" type="primary" icon="Plus" @click="addRow"
-            >添加</el-button
-          >
+          <el-button icon="Plus" type="primary" @click="addRow">新建</el-button>
         </div>
       </div>
     </el-header>
-    <el-main class="sa-p-0" v-loading="loading">
-      <el-table height="100%" :data="table.data" @row-dblclick="editRow" class="sa-table" stripe>
-        <template #empty>
-          <sa-empty />
-        </template>
-        <el-table-column prop="id" label="ID" min-width="80"></el-table-column>
-        <el-table-column prop="name" label="分类名称" min-width="100">
-          <template #default="scope">
-            <div class="sa-table-line-1">{{ scope.row.name || '-' }}</div>
+    <el-main class="sa-p-0">
+      <div class="sa-table-wrap panel-block panel-block--bottom" v-loading="loading">
+        <el-table
+          height="100%"
+          class="sa-table"
+          :data="table.data"
+          @selection-change="changeSelection"
+          @sort-change="fieldFilter"
+          @row-dblclick="editRow"
+          row-key="id"
+          stripe
+        >
+          <template #empty>
+            <sa-empty />
           </template>
-        </el-table-column>
-        <el-table-column prop="style" label="分类样式" min-width="100">
-          <template #default="scope">
-            <div>
-              <el-popover placement="right-start" title :width="236" trigger="hover">
-                <template #reference>
-                  <img
-                    :src="'./static/images/shop/category/' + scope.row.style + '.png'"
-                    class="type-img"
-                  />
-                </template>
-                <div class="hover-img">
-                  <img :src="'./static/images/shop/category/' + scope.row.style + '.png'" />
-                </div>
-              </el-popover>
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column prop="weigh" label="权重" min-width="100">
-          <template #default="scope">
-            <div class="sa-table-line-1">{{ scope.row.weigh || '0' }}</div>
-          </template>
-        </el-table-column>
-        <el-table-column fixed="right" label="操作" min-width="120">
-          <template #default="scope">
-            <template v-if="scope.row.id">
-              <el-button
-                v-auth="'shop.admin.category.detail'"
-                class="is-link"
-                type="primary"
-                @click="editRow(scope.row)"
-                >编辑</el-button
-              >
+          <el-table-column type="selection" width="48" align="center"></el-table-column>
+          <el-table-column prop="id" label="ID" min-width="100" sortable="custom">
+          </el-table-column>
+          <el-table-column label="分类名称" min-width="120">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                {{ scope.row.title || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="创建时间" min-width="160">
+            <template #default="scope">
+              {{ scope.row.create_time || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="更新时间" min-width="160">
+            <template #default="scope">
+              {{ scope.row.update_time || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column fixed="right" label="操作" min-width="120">
+            <template #default="scope">
+              <el-button class="is-link" type="primary" @click="editRow(scope.row)">编辑</el-button>
               <el-popconfirm
                 width="fit-content"
                 confirm-button-text="确认"
                 cancel-button-text="取消"
                 title="确认删除这条记录?"
-                @confirm="deleteRow(scope.row.id)"
+                @confirm="deleteApi(scope.row.id)"
               >
                 <template #reference>
-                  <el-button v-auth="'shop.admin.category.delete'" class="is-link" type="danger">
-                    删除
-                  </el-button>
+                  <el-button class="is-link" type="danger"> 删除 </el-button>
                 </template>
               </el-popconfirm>
             </template>
-          </template>
-        </el-table-column>
-      </el-table>
+          </el-table-column>
+        </el-table>
+      </div>
     </el-main>
+    <sa-view-bar>
+      <template #left>
+        <sa-batch-handle
+          :batchHandleTools="batchHandleTools"
+          :selectedLeng="table.selected.length"
+          @batchHandle="batchHandle"
+        ></sa-batch-handle>
+      </template>
+      <template #right>
+        <sa-pagination :pageData="pageData" @updateFn="getData" />
+      </template>
+    </sa-view-bar>
   </el-container>
 </template>
-<script>
-  export default {
-    name: 'shop.admin.category',
-  };
-</script>
 <script setup>
   import { onMounted, reactive, ref } from 'vue';
   import { api } from './category.service';
+  import { ElMessageBox } from 'element-plus';
   import { useModal } from '@/sheep/hooks';
-  import { useSearch } from '@/sheep/components/sa-table/sa-search/useSearch';
-  import { composeFilter } from '@/sheep/utils';
-  import CategoryEdit from './edit.vue';
-  import { cloneDeep } from 'lodash';
+  import { usePagination } from '@/sheep/hooks';
+  import categorytagEdit from './edit.vue';
+  const { pageData } = usePagination();
 
-  const filterParams = reactive({
-    // 搜索自定义工具
-    tools: {
-      name: {
-        type: 'tinput',
-        field: 'name',
-        value: '',
-        label: '分类名称',
-        placeholder: '请输入查询内容',
-      },
+  // 搜索字段配置
+  const searchFields = reactive({
+    name: {
+      type: 'input',
+      label: '分类名称',
+      placeholder: '请输入分类名称',
+      width: 200,
     },
-    data: {
-      name: '',
-    },
-    conditionLabel: {},
   });
-  const { openFilter, deleteFilter } = useSearch({ filterParams, getData });
-
-  // 表格状态
+  // 默认搜索值
+  const defaultSearchValues = reactive({
+    name: '',
+  });
+  // 列表
   const table = reactive({
     data: [],
-    search: {},
+    order: '',
+    sort: '',
+    selected: [],
   });
   const loading = ref(true);
-  // 获取数据
-  async function getData() {
+  // 获取
+  async function getData(page, searchParams = {}) {
+    if (page) pageData.page = page;
     loading.value = true;
-    let tempSearch = cloneDeep(filterParams.data);
-    let search = composeFilter(tempSearch, {
-      name: 'like',
-    });
     const { error, data } = await api.list({
-      ...search,
+      page: pageData.page,
+      list_rows: pageData.list_rows,
+      order: table.order,
+      ...searchParams,
+      sort: table.sort,
     });
-    error === 0 && (table.data = data);
+    console.log('API 响应:', error, data);
+    if (error === 0) {
+      table.data = data.data;
+      pageData.page = data.current_page;
+      pageData.list_rows = data.per_page;
+      pageData.total = data.total;
+    }
     loading.value = false;
   }
-
+  // table 字段排序
+  function fieldFilter({ prop, order }) {
+    table.order = order == 'ascending' ? 'asc' : 'desc';
+    table.sort = prop;
+    getData();
+  }
+  //table批量选择
+  function changeSelection(row) {
+    table.selected = row;
+  }
+  // 分页/批量操作
+  const batchHandleTools = [
+    {
+      type: 'delete',
+      label: '删除',
+      auth: 'shop.admin.category.delete',
+      class: 'danger',
+    },
+  ];
   function addRow() {
     useModal(
-      CategoryEdit,
-      { title: '添加分类', type: 'add' },
+      categorytagEdit,
+      { title: '新建分类', type: 'add' },
       {
         confirm: () => {
           getData();
@@ -146,9 +169,9 @@
   }
   function editRow(row) {
     useModal(
-      CategoryEdit,
+      categorytagEdit,
       {
-        title: '编辑',
+        title: '编辑分类',
         type: 'edit',
         id: row.id,
       },
@@ -159,10 +182,33 @@
       },
     );
   }
-  async function deleteRow(id) {
+  // 删除api 单独批量可以直接调用
+  async function deleteApi(id) {
     await api.delete(id);
     getData();
   }
+  async function batchHandle(type) {
+    let ids = [];
+    table.selected.forEach((row) => {
+      ids.push(row.id);
+    });
+    switch (type) {
+      case 'delete':
+        ElMessageBox.confirm('此操作将删除, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }).then(() => {
+          deleteApi(ids.join(','));
+        });
+        break;
+      default:
+        await api.category.edit(ids.join(','), {
+          status: type,
+        });
+        getData();
+    }
+  }
 
   onMounted(() => {
     getData();
@@ -170,14 +216,13 @@
 </script>
 <style lang="scss" scoped>
   .category-view {
-  }
-  .type-img {
-    width: 32px;
-    height: 32px;
-  }
-  .hover-img {
-    width: 220px;
-    height: 350px;
-    display: flex;
+    .el-header {
+      height: auto;
+    }
+    .el-main {
+      .sa-table-wrap {
+        height: 100%;
+      }
+    }
   }
 </style>

+ 0 - 78
src/app/shop/admin/category/select.vue

@@ -1,78 +0,0 @@
-<template>
-  <el-container>
-    <el-main>
-      <el-cascader-panel
-        ref="categoryRef"
-        v-model="select.category"
-        :options="select.data"
-        :props="{
-          multiple: modal.params.multiple,
-          checkStrictly: true,
-          label: 'name',
-          value: 'id',
-        }"
-      />
-    </el-main>
-    <el-footer class="sa-footer--submit">
-      <el-button type="primary" @click="confirm">确定</el-button>
-    </el-footer>
-  </el-container>
-</template>
-
-<script>
-  export default {
-    name: 'CategorySelect',
-  };
-</script>
-
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from './category.service';
-
-  const emit = defineEmits(['modalCallBack']);
-  const props = defineProps({
-    modal: Object,
-    default: {
-      params: {
-        multiple: false,
-      },
-    },
-  });
-
-  const categoryRef = ref();
-  const select = reactive({
-    data: [],
-    category: [],
-  });
-  async function getData() {
-    const { data } = await api.select();
-    select.data = data;
-    if (props.modal.params.from == 'page-category') {
-      select.data.forEach((item) => {
-        item?.children && delete item.children;
-      });
-    }
-    if (props.modal.params.from == 'coupon' || props.modal.params.from == 'page-goods') {
-      select.data.forEach((item) => {
-        item.disabled = true;
-      });
-    }
-  }
-
-  function confirm() {
-    let list = [];
-    categoryRef.value.checkedNodes.forEach((c) => {
-      list.push(c.data);
-    });
-    emit('modalCallBack', {
-      event: 'confirm',
-      data: {
-        ids: select.category,
-        list: list,
-      },
-    });
-  }
-  onMounted(() => {
-    getData();
-  });
-</script>

+ 0 - 89
src/app/shop/admin/config/config.service.js

@@ -1,89 +0,0 @@
-import { isEmpty } from 'lodash';
-import { request } from '@/sheep/request';
-
-const route = {
-  path: 'config',
-  name: 'shop.admin.config',
-  component: () => import('@/app/shop/admin/config/index.vue'),
-  meta: {
-    title: '配置',
-  },
-};
-
-const api = {
-  basic: (data) =>
-    request({
-      url: 'shop/admin/config/basic',
-      method: isEmpty(data) ? 'GET' : 'PUT',
-      data,
-      options: {
-        showSuccessMessage: !isEmpty(data),
-      },
-    }),
-  order: (data) =>
-    request({
-      url: 'shop/admin/config/order',
-      method: isEmpty(data) ? 'GET' : 'PUT',
-      data,
-      options: {
-        showSuccessMessage: !isEmpty(data),
-      },
-    }),
-  recharge: (data) =>
-    request({
-      url: 'shop/admin/config/rechargeWithdraw',
-      method: isEmpty(data) ? 'GET' : 'PUT',
-      data,
-      options: {
-        showSuccessMessage: !isEmpty(data),
-      },
-    }),
-  platformStatus: () =>
-    request({
-      url: 'shop/admin/config/platformStatus',
-      method: 'GET',
-    }),
-  platform: (platform, data) =>
-    request({
-      url: 'shop/admin/config/platform/' + platform,
-      method: isEmpty(data) ? 'GET' : 'PUT',
-      data,
-      options: {
-        showSuccessMessage: !isEmpty(data),
-      },
-    }),
-  commission: (data) =>
-    request({
-      url: 'shop/admin/config/commission',
-      method: isEmpty(data) ? 'GET' : 'PUT',
-      data,
-      options: {
-        showSuccessMessage: !isEmpty(data),
-      },
-    }),
-  dispatch: (data) =>
-    request({
-      url: 'shop/admin/config/dispatch',
-      method: isEmpty(data) ? 'GET' : 'PUT',
-      data,
-      options: {
-        showSuccessMessage: !isEmpty(data),
-      },
-    }),
-  goods: (data) =>
-    request({
-      url: 'shop/admin/config/goods',
-      method: isEmpty(data) ? 'GET' : 'PUT',
-      data,
-      options: {
-        showSuccessMessage: !isEmpty(data),
-      },
-    }),
-  getPlatformUrl: () =>
-    request({
-      url: 'shop/admin/config/getPlatformUrl',
-      method: 'GET',
-    }),
-};
-
-export { route, api };

+ 0 - 52
src/app/shop/admin/config/index.vue

@@ -1,52 +0,0 @@
-<template>
-  <el-container class="config-page panel-block">
-    <el-header class="sa-header">
-      <el-tabs class="sa-tabs" v-model="configType">
-        <el-tab-pane v-for="c in configList" :key="c" :label="c.label" :name="c.api"></el-tab-pane>
-      </el-tabs>
-    </el-header>
-    <basic-config v-if="configType == 'shop.admin.config.basic'" />
-    <platform-config v-if="configType == 'shop.admin.config.platform'" />
-    <goods-config v-if="configType == 'shop.admin.config.goods'" />
-    <order-config v-if="configType == 'shop.admin.config.order'" />
-    <dispatch-config v-if="configType == 'shop.admin.config.dispatch'" />
-    <recharge-config v-if="configType == 'shop.admin.config.rechargewithdraw'" />
-    <commission-config v-if="configType == 'shop.admin.config.commission'" />
-    <pay-config v-if="configType == 'shop.admin.payconfig'"></pay-config>
-  </el-container>
-</template>
-<script>
-  export default {
-    name: 'shop.admin.config',
-  };
-</script>
-<script setup>
-  import { ref, computed } from 'vue';
-  import PlatformConfig from './componenets/platform/index.vue';
-  import basicConfig from './componenets/basic.vue';
-  import orderConfig from './componenets/order.vue';
-  import goodsConfig from './componenets/goods.vue';
-  import dispatchConfig from './componenets/dispatch.vue';
-  import rechargeConfig from './componenets/recharge.vue';
-  import CommissionConfig from './componenets/commission.vue';
-  import payConfig from './componenets/payConfig/index.vue';
-
-  import { checkAuth } from '@/sheep/directives/auth';
-
-  const configList = computed(() => {
-    const config = [
-      { label: '基本信息', api: 'shop.admin.config.basic' },
-      { label: '平台设置', api: 'shop.admin.config.platform' },
-      { label: '订单设置', api: 'shop.admin.config.order' },
-      { label: '物流设置', api: 'shop.admin.config.dispatch' },
-      { label: '充值提现', api: 'shop.admin.config.rechargewithdraw' },
-      { label: '分销设置', api: 'shop.admin.config.commission' },
-      { label: '商品设置', api: 'shop.admin.config.goods' },
-      { label: '支付设置', api: 'shop.admin.payconfig' },
-    ];
-    return config.filter((c) => {
-      if (checkAuth(c.api)) return c;
-    });
-  });
-  const configType = ref(configList.value.length > 0 ? configList.value[0].api : '');
-</script>

+ 17 - 0
src/app/shop/admin/content/banner/banner.service.js

@@ -0,0 +1,17 @@
+import { SELECT, CRUD } from '@/sheep/request/crud';
+
+const route = {
+  path: 'banner',
+  name: 'shop.admin.content.banner',
+  component: () => import('@/app/shop/admin/content/banner/index.vue'),
+  meta: {
+    title: '广告位',
+  },
+};
+
+const api = {
+  ...CRUD('shop/admin/banner'),
+  select: (params) => SELECT('shop/admin/banner', params),
+};
+
+export { route, api };

+ 128 - 0
src/app/shop/admin/content/banner/edit.vue

@@ -0,0 +1,128 @@
+<template>
+  <el-container>
+    <el-main>
+      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
+        <el-form-item label="广告标题" prop="title">
+          <el-input v-model="form.model.title" placeholder="请填写广告标题"></el-input>
+        </el-form-item>
+        <el-form-item label="广告图片" prop="image">
+          <el-input v-model="form.model.image" placeholder="请填写图片地址"></el-input>
+        </el-form-item>
+        <el-form-item label="广告位置" prop="position">
+          <el-select v-model="form.model.position" placeholder="请选择广告位置">
+            <el-option label="首页轮播" value="home"></el-option>
+            <el-option label="分类页" value="category"></el-option>
+            <el-option label="商品详情" value="goods"></el-option>
+            <el-option label="个人中心" value="profile"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="链接地址" prop="link">
+          <el-input v-model="form.model.link" placeholder="请填写链接地址"></el-input>
+        </el-form-item>
+        <el-form-item label="链接类型" prop="link_type">
+          <el-select v-model="form.model.link_type" placeholder="请选择链接类型">
+            <el-option label="外部链接" value="url"></el-option>
+            <el-option label="商品详情" value="goods"></el-option>
+            <el-option label="分类页面" value="category"></el-option>
+            <el-option label="页面跳转" value="page"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="开始时间" prop="start_time">
+          <el-date-picker
+            v-model="form.model.start_time"
+            type="datetime"
+            placeholder="选择开始时间"
+            format="YYYY-MM-DD HH:mm:ss"
+            value-format="YYYY-MM-DD HH:mm:ss"
+          />
+        </el-form-item>
+        <el-form-item label="结束时间" prop="end_time">
+          <el-date-picker
+            v-model="form.model.end_time"
+            type="datetime"
+            placeholder="选择结束时间"
+            format="YYYY-MM-DD HH:mm:ss"
+            value-format="YYYY-MM-DD HH:mm:ss"
+          />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="form.model.status" placeholder="请选择状态">
+            <el-option label="启用" value="active"></el-option>
+            <el-option label="禁用" value="disabled"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="排序" prop="sort">
+          <el-input v-model="form.model.sort" placeholder="请填写排序"></el-input>
+        </el-form-item>
+      </el-form>
+    </el-main>
+    <el-footer class="sa-footer--submit">
+      <el-button v-if="modal.params.type == 'add'" type="primary" @click="confirm">保存</el-button>
+      <el-button v-if="modal.params.type == 'edit'" type="primary" @click="confirm">更新</el-button>
+    </el-footer>
+  </el-container>
+</template>
+<script setup>
+  import { cloneDeep } from 'lodash';
+  import { onMounted, reactive, ref, unref } from 'vue';
+  import { api } from './banner.service';
+  const emit = defineEmits(['modalCallBack']);
+  const props = defineProps({
+    modal: {
+      type: Object,
+    },
+  });
+  // 添加 编辑 form
+  let formRef = ref(null);
+  const form = reactive({
+    model: {
+      title: '',
+      image: '',
+      position: 'home',
+      link: '',
+      link_type: 'url',
+      start_time: '',
+      end_time: '',
+      status: 'active',
+      sort: '0',
+    },
+    rules: {
+      title: [{ required: true, message: '请填写广告标题', trigger: 'blur' }],
+      image: [{ required: true, message: '请填写图片地址', trigger: 'blur' }],
+      position: [{ required: true, message: '请选择广告位置', trigger: 'change' }],
+      link_type: [{ required: true, message: '请选择链接类型', trigger: 'change' }],
+      status: [{ required: true, message: '请选择状态', trigger: 'change' }],
+      sort: [{ required: true, message: '请填写排序', trigger: 'blur' }],
+    },
+  });
+  const loading = ref(false);
+  // 获取详情
+  async function getDetail(id) {
+    loading.value = true;
+    const { error, data } = await api.detail(id);
+    error === 0 && (form.model = data);
+    loading.value = false;
+  }
+  // 表单关闭时提交
+  async function confirm() {
+    unref(formRef).validate(async (valid) => {
+      if (!valid) return;
+      let submitForm = cloneDeep(form.model);
+      const { error } =
+        props.modal.params.type == 'add'
+          ? await api.add(submitForm)
+          : await api.edit(props.modal.params.id, submitForm);
+      if (error == 0) {
+        emit('modalCallBack', { event: 'confirm' });
+      }
+    });
+  }
+  async function init() {
+    if (props.modal.params.id) {
+      await getDetail(props.modal.params.id);
+    }
+  }
+  onMounted(() => {
+    init();
+  });
+</script>

+ 260 - 0
src/app/shop/admin/content/banner/index.vue

@@ -0,0 +1,260 @@
+<template>
+  <el-container class="banner-view panel-block">
+    <el-header class="sa-header">
+      <!-- 简化搜索组件 -->
+      <div class="search-container">
+        <sa-search-simple
+          :searchFields="searchFields"
+          :defaultValues="defaultSearchValues"
+          @search="(val) => getData(1, val)"
+          @reset="getData(1)"
+        >
+        </sa-search-simple>
+      </div>
+      <div class="sa-title sa-flex sa-row-between">
+        <div class="label sa-flex">广告位管理</div>
+        <div>
+          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
+          <el-button icon="Plus" type="primary" @click="addRow">新建</el-button>
+        </div>
+      </div>
+    </el-header>
+    <el-main class="sa-p-0">
+      <div class="sa-table-wrap panel-block panel-block--bottom" v-loading="loading">
+        <el-table
+          height="100%"
+          class="sa-table"
+          :data="table.data"
+          @selection-change="changeSelection"
+          @sort-change="fieldFilter"
+          @row-dblclick="editRow"
+          row-key="id"
+          stripe
+        >
+          <template #empty>
+            <sa-empty />
+          </template>
+          <el-table-column type="selection" width="48" align="center"></el-table-column>
+          <el-table-column prop="id" label="ID" min-width="100" sortable="custom">
+          </el-table-column>
+          <el-table-column label="广告标题" min-width="150">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                {{ scope.row.title || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="广告图片" min-width="120">
+            <template #default="scope">
+              <el-image 
+                v-if="scope.row.image"
+                :src="scope.row.image" 
+                style="width: 60px; height: 40px"
+                fit="cover"
+              />
+              <span v-else>-</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="广告位置" min-width="120">
+            <template #default="scope">
+              <el-tag :type="scope.row.position === 'home' ? 'primary' : 'success'">
+                {{ scope.row.position_text || '-' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="链接地址" min-width="200">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                {{ scope.row.link || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="状态" min-width="100">
+            <template #default="scope">
+              <el-tag :type="scope.row.status === 'active' ? 'success' : 'danger'">
+                {{ scope.row.status_text || '-' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="排序" min-width="100">
+            <template #default="scope">
+              {{ scope.row.sort || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="创建时间" min-width="160">
+            <template #default="scope">
+              {{ scope.row.create_time || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column fixed="right" label="操作" min-width="120">
+            <template #default="scope">
+              <el-button class="is-link" type="primary" @click="editRow(scope.row)">编辑</el-button>
+              <el-popconfirm
+                width="fit-content"
+                confirm-button-text="确认"
+                cancel-button-text="取消"
+                title="确认删除这条记录?"
+                @confirm="deleteApi(scope.row.id)"
+              >
+                <template #reference>
+                  <el-button class="is-link" type="danger"> 删除 </el-button>
+                </template>
+              </el-popconfirm>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </el-main>
+    <sa-view-bar>
+      <template #left>
+        <sa-batch-handle
+          :batchHandleTools="batchHandleTools"
+          :selectedLeng="table.selected.length"
+          @batchHandle="batchHandle"
+        ></sa-batch-handle>
+      </template>
+      <template #right>
+        <sa-pagination :pageData="pageData" @updateFn="getData" />
+      </template>
+    </sa-view-bar>
+  </el-container>
+</template>
+<script setup>
+  import { onMounted, reactive, ref } from 'vue';
+  import { api } from './banner.service';
+  import { ElMessageBox } from 'element-plus';
+  import { useModal } from '@/sheep/hooks';
+  import { usePagination } from '@/sheep/hooks';
+  import bannerEdit from './edit.vue';
+  const { pageData } = usePagination();
+
+  // 搜索字段配置
+  const searchFields = reactive({
+    title: {
+      type: 'input',
+      label: '广告标题',
+      placeholder: '请输入广告标题',
+      width: 200,
+    },
+  });
+  // 默认搜索值
+  const defaultSearchValues = reactive({
+    title: '',
+  });
+  // 列表
+  const table = reactive({
+    data: [],
+    order: '',
+    sort: '',
+    selected: [],
+  });
+  const loading = ref(true);
+  // 获取
+  async function getData(page, searchParams = {}) {
+    if (page) pageData.page = page;
+    loading.value = true;
+    const { error, data } = await api.list({
+      page: pageData.page,
+      list_rows: pageData.list_rows,
+      order: table.order,
+      ...searchParams,
+      sort: table.sort,
+    });
+    console.log('API 响应:', error, data);
+    if (error === 0) {
+      table.data = data.data;
+      pageData.page = data.current_page;
+      pageData.list_rows = data.per_page;
+      pageData.total = data.total;
+    }
+    loading.value = false;
+  }
+  // table 字段排序
+  function fieldFilter({ prop, order }) {
+    table.order = order == 'ascending' ? 'asc' : 'desc';
+    table.sort = prop;
+    getData();
+  }
+  //table批量选择
+  function changeSelection(row) {
+    table.selected = row;
+  }
+  // 分页/批量操作
+  const batchHandleTools = [
+    {
+      type: 'delete',
+      label: '删除',
+      auth: 'shop.admin.content.banner.delete',
+      class: 'danger',
+    },
+  ];
+  function addRow() {
+    useModal(
+      bannerEdit,
+      { title: '新建广告', type: 'add' },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+  function editRow(row) {
+    useModal(
+      bannerEdit,
+      {
+        title: '编辑广告',
+        type: 'edit',
+        id: row.id,
+      },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+  // 删除api 单独批量可以直接调用
+  async function deleteApi(id) {
+    await api.delete(id);
+    getData();
+  }
+  async function batchHandle(type) {
+    let ids = [];
+    table.selected.forEach((row) => {
+      ids.push(row.id);
+    });
+    switch (type) {
+      case 'delete':
+        ElMessageBox.confirm('此操作将删除, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }).then(() => {
+          deleteApi(ids.join(','));
+        });
+        break;
+      default:
+        await api.edit(ids.join(','), {
+          status: type,
+        });
+        getData();
+    }
+  }
+
+  onMounted(() => {
+    getData();
+  });
+</script>
+<style lang="scss" scoped>
+  .banner-view {
+    .el-header {
+      height: auto;
+    }
+    .el-main {
+      .sa-table-wrap {
+        height: 100%;
+      }
+    }
+  }
+</style>

+ 114 - 0
src/app/shop/admin/content/notification/edit.vue

@@ -0,0 +1,114 @@
+<template>
+  <el-container>
+    <el-main>
+      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
+        <el-form-item label="消息标题" prop="title">
+          <el-input v-model="form.model.title" placeholder="请填写消息标题"></el-input>
+        </el-form-item>
+        <el-form-item label="消息类型" prop="type">
+          <el-select v-model="form.model.type" placeholder="请选择消息类型">
+            <el-option label="系统消息" value="system"></el-option>
+            <el-option label="营销消息" value="marketing"></el-option>
+            <el-option label="订单消息" value="order"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="推送对象" prop="target">
+          <el-select v-model="form.model.target" placeholder="请选择推送对象">
+            <el-option label="全部用户" value="all"></el-option>
+            <el-option label="指定用户" value="specific"></el-option>
+            <el-option label="会员用户" value="member"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="消息内容" prop="content">
+          <el-input 
+            v-model="form.model.content" 
+            type="textarea" 
+            :rows="5"
+            placeholder="请填写消息内容"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="发送时间" prop="send_time">
+          <el-date-picker
+            v-model="form.model.send_time"
+            type="datetime"
+            placeholder="选择发送时间"
+            format="YYYY-MM-DD HH:mm:ss"
+            value-format="YYYY-MM-DD HH:mm:ss"
+          />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="form.model.status" placeholder="请选择状态">
+            <el-option label="待发送" value="pending"></el-option>
+            <el-option label="已发送" value="sent"></el-option>
+            <el-option label="已取消" value="cancelled"></el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+    </el-main>
+    <el-footer class="sa-footer--submit">
+      <el-button v-if="modal.params.type == 'add'" type="primary" @click="confirm">保存</el-button>
+      <el-button v-if="modal.params.type == 'edit'" type="primary" @click="confirm">更新</el-button>
+    </el-footer>
+  </el-container>
+</template>
+<script setup>
+  import { cloneDeep } from 'lodash';
+  import { onMounted, reactive, ref, unref } from 'vue';
+  import { api } from './notification.service';
+  const emit = defineEmits(['modalCallBack']);
+  const props = defineProps({
+    modal: {
+      type: Object,
+    },
+  });
+  // 添加 编辑 form
+  let formRef = ref(null);
+  const form = reactive({
+    model: {
+      title: '',
+      type: 'system',
+      target: 'all',
+      content: '',
+      send_time: '',
+      status: 'pending',
+    },
+    rules: {
+      title: [{ required: true, message: '请填写消息标题', trigger: 'blur' }],
+      type: [{ required: true, message: '请选择消息类型', trigger: 'change' }],
+      target: [{ required: true, message: '请选择推送对象', trigger: 'change' }],
+      content: [{ required: true, message: '请填写消息内容', trigger: 'blur' }],
+      send_time: [{ required: true, message: '请选择发送时间', trigger: 'change' }],
+      status: [{ required: true, message: '请选择状态', trigger: 'change' }],
+    },
+  });
+  const loading = ref(false);
+  // 获取详情
+  async function getDetail(id) {
+    loading.value = true;
+    const { error, data } = await api.detail(id);
+    error === 0 && (form.model = data);
+    loading.value = false;
+  }
+  // 表单关闭时提交
+  async function confirm() {
+    unref(formRef).validate(async (valid) => {
+      if (!valid) return;
+      let submitForm = cloneDeep(form.model);
+      const { error } =
+        props.modal.params.type == 'add'
+          ? await api.add(submitForm)
+          : await api.edit(props.modal.params.id, submitForm);
+      if (error == 0) {
+        emit('modalCallBack', { event: 'confirm' });
+      }
+    });
+  }
+  async function init() {
+    if (props.modal.params.id) {
+      await getDetail(props.modal.params.id);
+    }
+  }
+  onMounted(() => {
+    init();
+  });
+</script>

+ 247 - 0
src/app/shop/admin/content/notification/index.vue

@@ -0,0 +1,247 @@
+<template>
+  <el-container class="notification-view panel-block">
+    <el-header class="sa-header">
+      <!-- 简化搜索组件 -->
+      <div class="search-container">
+        <sa-search-simple
+          :searchFields="searchFields"
+          :defaultValues="defaultSearchValues"
+          @search="(val) => getData(1, val)"
+          @reset="getData(1)"
+        >
+        </sa-search-simple>
+      </div>
+      <div class="sa-title sa-flex sa-row-between">
+        <div class="label sa-flex">消息推送</div>
+        <div>
+          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
+          <el-button icon="Plus" type="primary" @click="addRow">新建</el-button>
+        </div>
+      </div>
+    </el-header>
+    <el-main class="sa-p-0">
+      <div class="sa-table-wrap panel-block panel-block--bottom" v-loading="loading">
+        <el-table
+          height="100%"
+          class="sa-table"
+          :data="table.data"
+          @selection-change="changeSelection"
+          @sort-change="fieldFilter"
+          @row-dblclick="editRow"
+          row-key="id"
+          stripe
+        >
+          <template #empty>
+            <sa-empty />
+          </template>
+          <el-table-column type="selection" width="48" align="center"></el-table-column>
+          <el-table-column prop="id" label="ID" min-width="100" sortable="custom">
+          </el-table-column>
+          <el-table-column label="消息标题" min-width="150">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                {{ scope.row.title || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="消息类型" min-width="120">
+            <template #default="scope">
+              <el-tag :type="scope.row.type === 'system' ? 'primary' : 'success'">
+                {{ scope.row.type_text || '-' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="推送对象" min-width="120">
+            <template #default="scope">
+              {{ scope.row.target_text || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="状态" min-width="100">
+            <template #default="scope">
+              <el-tag :type="scope.row.status === 'sent' ? 'success' : 'warning'">
+                {{ scope.row.status_text || '-' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="发送时间" min-width="160">
+            <template #default="scope">
+              {{ scope.row.send_time || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="创建时间" min-width="160">
+            <template #default="scope">
+              {{ scope.row.create_time || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column fixed="right" label="操作" min-width="120">
+            <template #default="scope">
+              <el-button class="is-link" type="primary" @click="editRow(scope.row)">编辑</el-button>
+              <el-popconfirm
+                width="fit-content"
+                confirm-button-text="确认"
+                cancel-button-text="取消"
+                title="确认删除这条记录?"
+                @confirm="deleteApi(scope.row.id)"
+              >
+                <template #reference>
+                  <el-button class="is-link" type="danger"> 删除 </el-button>
+                </template>
+              </el-popconfirm>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </el-main>
+    <sa-view-bar>
+      <template #left>
+        <sa-batch-handle
+          :batchHandleTools="batchHandleTools"
+          :selectedLeng="table.selected.length"
+          @batchHandle="batchHandle"
+        ></sa-batch-handle>
+      </template>
+      <template #right>
+        <sa-pagination :pageData="pageData" @updateFn="getData" />
+      </template>
+    </sa-view-bar>
+  </el-container>
+</template>
+<script setup>
+  import { onMounted, reactive, ref } from 'vue';
+  import { api } from './notification.service';
+  import { ElMessageBox } from 'element-plus';
+  import { useModal } from '@/sheep/hooks';
+  import { usePagination } from '@/sheep/hooks';
+  import notificationEdit from './edit.vue';
+  const { pageData } = usePagination();
+
+  // 搜索字段配置
+  const searchFields = reactive({
+    title: {
+      type: 'input',
+      label: '消息标题',
+      placeholder: '请输入消息标题',
+      width: 200,
+    },
+  });
+  // 默认搜索值
+  const defaultSearchValues = reactive({
+    title: '',
+  });
+  // 列表
+  const table = reactive({
+    data: [],
+    order: '',
+    sort: '',
+    selected: [],
+  });
+  const loading = ref(true);
+  // 获取
+  async function getData(page, searchParams = {}) {
+    if (page) pageData.page = page;
+    loading.value = true;
+    const { error, data } = await api.list({
+      page: pageData.page,
+      list_rows: pageData.list_rows,
+      order: table.order,
+      ...searchParams,
+      sort: table.sort,
+    });
+    console.log('API 响应:', error, data);
+    if (error === 0) {
+      table.data = data.data;
+      pageData.page = data.current_page;
+      pageData.list_rows = data.per_page;
+      pageData.total = data.total;
+    }
+    loading.value = false;
+  }
+  // table 字段排序
+  function fieldFilter({ prop, order }) {
+    table.order = order == 'ascending' ? 'asc' : 'desc';
+    table.sort = prop;
+    getData();
+  }
+  //table批量选择
+  function changeSelection(row) {
+    table.selected = row;
+  }
+  // 分页/批量操作
+  const batchHandleTools = [
+    {
+      type: 'delete',
+      label: '删除',
+      auth: 'shop.admin.content.notification.delete',
+      class: 'danger',
+    },
+  ];
+  function addRow() {
+    useModal(
+      notificationEdit,
+      { title: '新建消息', type: 'add' },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+  function editRow(row) {
+    useModal(
+      notificationEdit,
+      {
+        title: '编辑消息',
+        type: 'edit',
+        id: row.id,
+      },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+  // 删除api 单独批量可以直接调用
+  async function deleteApi(id) {
+    await api.delete(id);
+    getData();
+  }
+  async function batchHandle(type) {
+    let ids = [];
+    table.selected.forEach((row) => {
+      ids.push(row.id);
+    });
+    switch (type) {
+      case 'delete':
+        ElMessageBox.confirm('此操作将删除, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }).then(() => {
+          deleteApi(ids.join(','));
+        });
+        break;
+      default:
+        await api.edit(ids.join(','), {
+          status: type,
+        });
+        getData();
+    }
+  }
+
+  onMounted(() => {
+    getData();
+  });
+</script>
+<style lang="scss" scoped>
+  .notification-view {
+    .el-header {
+      height: auto;
+    }
+    .el-main {
+      .sa-table-wrap {
+        height: 100%;
+      }
+    }
+  }
+</style>

+ 17 - 0
src/app/shop/admin/content/notification/notification.service.js

@@ -0,0 +1,17 @@
+import { SELECT, CRUD } from '@/sheep/request/crud';
+
+const route = {
+  path: 'notification',
+  name: 'shop.admin.content.notification',
+  component: () => import('@/app/shop/admin/content/notification/index.vue'),
+  meta: {
+    title: '消息推送',
+  },
+};
+
+const api = {
+  ...CRUD('shop/admin/notification'),
+  select: (params) => SELECT('shop/admin/notification', params),
+};
+
+export { route, api };

+ 121 - 0
src/app/shop/admin/content/sms/edit.vue

@@ -0,0 +1,121 @@
+<template>
+  <el-container>
+    <el-main>
+      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
+        <el-form-item label="模板名称" prop="name">
+          <el-input v-model="form.model.name" placeholder="请填写模板名称"></el-input>
+        </el-form-item>
+        <el-form-item label="模板编码" prop="code">
+          <el-input v-model="form.model.code" placeholder="请填写模板编码"></el-input>
+        </el-form-item>
+        <el-form-item label="短信类型" prop="type">
+          <el-select v-model="form.model.type" placeholder="请选择短信类型">
+            <el-option label="验证码" value="verify"></el-option>
+            <el-option label="通知" value="notice"></el-option>
+            <el-option label="营销" value="marketing"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="模板内容" prop="content">
+          <el-input 
+            v-model="form.model.content" 
+            type="textarea" 
+            :rows="4"
+            placeholder="请填写短信模板内容,使用{变量名}表示变量"
+          ></el-input>
+          <div class="form-tip">
+            <small>提示:使用 {code} 表示验证码,{name} 表示用户名等变量</small>
+          </div>
+        </el-form-item>
+        <el-form-item label="变量说明" prop="variables">
+          <el-input 
+            v-model="form.model.variables" 
+            type="textarea" 
+            :rows="3"
+            placeholder="请说明模板中使用的变量,如:{code}=验证码,{name}=用户名"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="form.model.status" placeholder="请选择状态">
+            <el-option label="启用" value="active"></el-option>
+            <el-option label="禁用" value="disabled"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="排序" prop="sort">
+          <el-input v-model="form.model.sort" placeholder="请填写排序"></el-input>
+        </el-form-item>
+      </el-form>
+    </el-main>
+    <el-footer class="sa-footer--submit">
+      <el-button v-if="modal.params.type == 'add'" type="primary" @click="confirm">保存</el-button>
+      <el-button v-if="modal.params.type == 'edit'" type="primary" @click="confirm">更新</el-button>
+    </el-footer>
+  </el-container>
+</template>
+<script setup>
+  import { cloneDeep } from 'lodash';
+  import { onMounted, reactive, ref, unref } from 'vue';
+  import { api } from './sms.service';
+  const emit = defineEmits(['modalCallBack']);
+  const props = defineProps({
+    modal: {
+      type: Object,
+    },
+  });
+  // 添加 编辑 form
+  let formRef = ref(null);
+  const form = reactive({
+    model: {
+      name: '',
+      code: '',
+      type: 'verify',
+      content: '',
+      variables: '',
+      status: 'active',
+      sort: '0',
+    },
+    rules: {
+      name: [{ required: true, message: '请填写模板名称', trigger: 'blur' }],
+      code: [{ required: true, message: '请填写模板编码', trigger: 'blur' }],
+      type: [{ required: true, message: '请选择短信类型', trigger: 'change' }],
+      content: [{ required: true, message: '请填写模板内容', trigger: 'blur' }],
+      status: [{ required: true, message: '请选择状态', trigger: 'change' }],
+      sort: [{ required: true, message: '请填写排序', trigger: 'blur' }],
+    },
+  });
+  const loading = ref(false);
+  // 获取详情
+  async function getDetail(id) {
+    loading.value = true;
+    const { error, data } = await api.detail(id);
+    error === 0 && (form.model = data);
+    loading.value = false;
+  }
+  // 表单关闭时提交
+  async function confirm() {
+    unref(formRef).validate(async (valid) => {
+      if (!valid) return;
+      let submitForm = cloneDeep(form.model);
+      const { error } =
+        props.modal.params.type == 'add'
+          ? await api.add(submitForm)
+          : await api.edit(props.modal.params.id, submitForm);
+      if (error == 0) {
+        emit('modalCallBack', { event: 'confirm' });
+      }
+    });
+  }
+  async function init() {
+    if (props.modal.params.id) {
+      await getDetail(props.modal.params.id);
+    }
+  }
+  onMounted(() => {
+    init();
+  });
+</script>
+<style lang="scss" scoped>
+  .form-tip {
+    margin-top: 5px;
+    color: #909399;
+  }
+</style>

+ 249 - 0
src/app/shop/admin/content/sms/index.vue

@@ -0,0 +1,249 @@
+<template>
+  <el-container class="sms-view panel-block">
+    <el-header class="sa-header">
+      <!-- 简化搜索组件 -->
+      <div class="search-container">
+        <sa-search-simple
+          :searchFields="searchFields"
+          :defaultValues="defaultSearchValues"
+          @search="(val) => getData(1, val)"
+          @reset="getData(1)"
+        >
+        </sa-search-simple>
+      </div>
+      <div class="sa-title sa-flex sa-row-between">
+        <div class="label sa-flex">短信管理</div>
+        <div>
+          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
+          <el-button icon="Plus" type="primary" @click="addRow">新建</el-button>
+        </div>
+      </div>
+    </el-header>
+    <el-main class="sa-p-0">
+      <div class="sa-table-wrap panel-block panel-block--bottom" v-loading="loading">
+        <el-table
+          height="100%"
+          class="sa-table"
+          :data="table.data"
+          @selection-change="changeSelection"
+          @sort-change="fieldFilter"
+          @row-dblclick="editRow"
+          row-key="id"
+          stripe
+        >
+          <template #empty>
+            <sa-empty />
+          </template>
+          <el-table-column type="selection" width="48" align="center"></el-table-column>
+          <el-table-column prop="id" label="ID" min-width="100" sortable="custom">
+          </el-table-column>
+          <el-table-column label="模板名称" min-width="150">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                {{ scope.row.name || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="模板编码" min-width="120">
+            <template #default="scope">
+              {{ scope.row.code || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="短信类型" min-width="120">
+            <template #default="scope">
+              <el-tag :type="scope.row.type === 'verify' ? 'primary' : 'success'">
+                {{ scope.row.type_text || '-' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="模板内容" min-width="200">
+            <template #default="scope">
+              <span class="sa-table-line-2">
+                {{ scope.row.content || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="状态" min-width="100">
+            <template #default="scope">
+              <el-tag :type="scope.row.status === 'active' ? 'success' : 'danger'">
+                {{ scope.row.status_text || '-' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="创建时间" min-width="160">
+            <template #default="scope">
+              {{ scope.row.create_time || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column fixed="right" label="操作" min-width="120">
+            <template #default="scope">
+              <el-button class="is-link" type="primary" @click="editRow(scope.row)">编辑</el-button>
+              <el-popconfirm
+                width="fit-content"
+                confirm-button-text="确认"
+                cancel-button-text="取消"
+                title="确认删除这条记录?"
+                @confirm="deleteApi(scope.row.id)"
+              >
+                <template #reference>
+                  <el-button class="is-link" type="danger"> 删除 </el-button>
+                </template>
+              </el-popconfirm>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </el-main>
+    <sa-view-bar>
+      <template #left>
+        <sa-batch-handle
+          :batchHandleTools="batchHandleTools"
+          :selectedLeng="table.selected.length"
+          @batchHandle="batchHandle"
+        ></sa-batch-handle>
+      </template>
+      <template #right>
+        <sa-pagination :pageData="pageData" @updateFn="getData" />
+      </template>
+    </sa-view-bar>
+  </el-container>
+</template>
+<script setup>
+  import { onMounted, reactive, ref } from 'vue';
+  import { api } from './sms.service';
+  import { ElMessageBox } from 'element-plus';
+  import { useModal } from '@/sheep/hooks';
+  import { usePagination } from '@/sheep/hooks';
+  import smsEdit from './edit.vue';
+  const { pageData } = usePagination();
+
+  // 搜索字段配置
+  const searchFields = reactive({
+    name: {
+      type: 'input',
+      label: '模板名称',
+      placeholder: '请输入模板名称',
+      width: 200,
+    },
+  });
+  // 默认搜索值
+  const defaultSearchValues = reactive({
+    name: '',
+  });
+  // 列表
+  const table = reactive({
+    data: [],
+    order: '',
+    sort: '',
+    selected: [],
+  });
+  const loading = ref(true);
+  // 获取
+  async function getData(page, searchParams = {}) {
+    if (page) pageData.page = page;
+    loading.value = true;
+    const { error, data } = await api.list({
+      page: pageData.page,
+      list_rows: pageData.list_rows,
+      order: table.order,
+      ...searchParams,
+      sort: table.sort,
+    });
+    console.log('API 响应:', error, data);
+    if (error === 0) {
+      table.data = data.data;
+      pageData.page = data.current_page;
+      pageData.list_rows = data.per_page;
+      pageData.total = data.total;
+    }
+    loading.value = false;
+  }
+  // table 字段排序
+  function fieldFilter({ prop, order }) {
+    table.order = order == 'ascending' ? 'asc' : 'desc';
+    table.sort = prop;
+    getData();
+  }
+  //table批量选择
+  function changeSelection(row) {
+    table.selected = row;
+  }
+  // 分页/批量操作
+  const batchHandleTools = [
+    {
+      type: 'delete',
+      label: '删除',
+      auth: 'shop.admin.content.sms.delete',
+      class: 'danger',
+    },
+  ];
+  function addRow() {
+    useModal(
+      smsEdit,
+      { title: '新建短信模板', type: 'add' },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+  function editRow(row) {
+    useModal(
+      smsEdit,
+      {
+        title: '编辑短信模板',
+        type: 'edit',
+        id: row.id,
+      },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+  // 删除api 单独批量可以直接调用
+  async function deleteApi(id) {
+    await api.delete(id);
+    getData();
+  }
+  async function batchHandle(type) {
+    let ids = [];
+    table.selected.forEach((row) => {
+      ids.push(row.id);
+    });
+    switch (type) {
+      case 'delete':
+        ElMessageBox.confirm('此操作将删除, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }).then(() => {
+          deleteApi(ids.join(','));
+        });
+        break;
+      default:
+        await api.edit(ids.join(','), {
+          status: type,
+        });
+        getData();
+    }
+  }
+
+  onMounted(() => {
+    getData();
+  });
+</script>
+<style lang="scss" scoped>
+  .sms-view {
+    .el-header {
+      height: auto;
+    }
+    .el-main {
+      .sa-table-wrap {
+        height: 100%;
+      }
+    }
+  }
+</style>

+ 17 - 0
src/app/shop/admin/content/sms/sms.service.js

@@ -0,0 +1,17 @@
+import { SELECT, CRUD } from '@/sheep/request/crud';
+
+const route = {
+  path: 'sms',
+  name: 'shop.admin.content.sms',
+  component: () => import('@/app/shop/admin/content/sms/index.vue'),
+  meta: {
+    title: '短信',
+  },
+};
+
+const api = {
+  ...CRUD('shop/admin/sms_template'),
+  select: (params) => SELECT('shop/admin/sms_template', params),
+};
+
+export { route, api };

+ 0 - 109
src/app/shop/admin/data/area/edit.vue

@@ -1,109 +0,0 @@
-<template>
-  <el-container>
-    <el-main>
-      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
-        <el-form-item label="所属上级" prop="pid">
-          <el-cascader
-            v-model="form.model.pid"
-            :options="area.select"
-            :props="{
-              label: 'name',
-              value: 'id',
-              checkStrictly: true,
-              emitPath: false,
-            }"
-            clearable
-            placeholder="请选择上级行政区"
-          ></el-cascader>
-        </el-form-item>
-        <el-form-item label="行政区ID" prop="id">
-          <el-input v-model="form.model.id" placeholder="请输入行政区ID"></el-input>
-        </el-form-item>
-        <el-form-item label="名称" prop="name">
-          <el-input v-model="form.model.name" placeholder="请输入名称"></el-input>
-        </el-form-item>
-      </el-form>
-    </el-main>
-    <el-footer class="sa-footer--submit">
-      <el-button
-        v-if="modal.params.type == 'add'"
-        v-auth="'shop.admin.data.area.add'"
-        type="primary"
-        @click="confirm"
-        >保存</el-button
-      >
-      <el-button
-        v-if="modal.params.type == 'edit'"
-        v-auth="'shop.admin.data.area.edit'"
-        type="primary"
-        @click="confirm"
-        >更新</el-button
-      >
-    </el-footer>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref, unref } from 'vue';
-  import { api } from '../data.service';
-  import { cloneDeep } from 'lodash';
-
-  const emit = defineEmits(['modalCallBack']);
-  const props = defineProps({
-    modal: {
-      type: Object,
-    },
-  });
-  // 添加 编辑 form
-  let formRef = ref(null);
-  const form = reactive({
-    model: {
-      id: '',
-      name: '',
-      pid: 0,
-    },
-    rules: {
-      name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
-      id: [{ required: true, message: '请输入行政区ID', trigger: 'blur' }],
-    },
-  });
-  const loading = ref(false);
-  const area = reactive({
-    select: [],
-  });
-  async function getAreaSelect() {
-    ({ data: area.select } = await api.area.select({
-      level: 'city',
-    }));
-  }
-  // 获取详情
-  async function getDetail(id) {
-    loading.value = true;
-    const { error, data } = await api.area.detail(id);
-    error === 0 && (form.model = data);
-    loading.value = false;
-  }
-  // 表单关闭时提交
-  async function confirm() {
-    // 表单验证
-    unref(formRef).validate(async (valid) => {
-      if (!valid) return;
-      let submitForm = cloneDeep(form.model);
-      const { error } =
-        props.modal.params.type == 'add'
-          ? await api.area.add(submitForm)
-          : await api.area.edit(props.modal.params.id, submitForm);
-      if (error == 0) {
-        emit('modalCallBack', { event: 'confirm' });
-      }
-    });
-  }
-  async function init() {
-    await getAreaSelect();
-    if (props.modal.params.id) {
-      await getDetail(props.modal.params.id);
-    }
-  }
-  onMounted(() => {
-    init();
-  });
-</script>

+ 0 - 203
src/app/shop/admin/data/area/index.vue

@@ -1,203 +0,0 @@
-<template>
-  <el-container class="area-view panel-block">
-    <el-header class="sa-header">
-      <div class="sa-title sa-flex sa-row-between">
-        <div class="label sa-flex">
-          <span class="left">省市区列表</span>
-          <div class="tip"
-            >数据来源:<a
-              href="http://www.stats.gov.cn/sj/tjbz/tjyqhdmhcxhfdm/2022/"
-              target="_blank"
-              >2022年国家统计局区划代码</a
-            >
-            更新时间2022-10-31</div
-          >
-        </div>
-        <div>
-          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
-          <el-button v-auth="'shop.admin.data.area.add'" icon="Plus" type="primary" @click="addRow"
-            >新建</el-button
-          >
-        </div>
-      </div>
-    </el-header>
-    <el-main class="sa-p-0" v-loading="loading">
-      <div class="list-head sa-flex sa-row-between">
-        <div>名称</div>
-        <div class="sa-flex">
-          <div class="list-head-level">级别</div>
-          <div>操作</div>
-        </div>
-      </div>
-      <el-tree v-if="tree.data.length > 0" :data="tree.data" node-key="id" ref="accessTree">
-        <template #empty>
-          <sa-empty />
-        </template>
-        <template #default="{ node, data }">
-          <div class="sa-flex sa-row-between tree-box">
-            <div class="sa-flex tree-name">
-              <div class="adcode"> #{{ data.id }} </div>
-              <div class="tree-name-title sa-line-1">
-                {{ data.name }}
-              </div>
-            </div>
-            <div class="sa-flex sa-m-r-10">
-              <div class="level">
-                {{ data.level == 'province' ? '省级' : data.level == 'city' ? '市级' : '区级' }}
-              </div>
-              <el-button
-                v-auth="'shop.admin.data.area.detail'"
-                class="is-link"
-                type="primary"
-                @click="editRow(node)"
-                >编辑</el-button
-              >
-              <el-popconfirm
-                width="fit-content"
-                confirm-button-text="确认"
-                cancel-button-text="取消"
-                title="确认删除这条记录?"
-                @confirm="remove(node.data.id)"
-              >
-                <template #reference>
-                  <el-button v-auth="'shop.admin.data.area.delete'" class="is-link" type="danger"
-                    >删除</el-button
-                  >
-                </template>
-              </el-popconfirm>
-            </div>
-          </div>
-        </template>
-      </el-tree>
-    </el-main>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from '../data.service';
-  import { useModal } from '@/sheep/hooks';
-  import AreaEdit from './edit.vue';
-  // 列表
-  const tree = reactive({
-    data: [],
-    order: 'asc',
-    sort: '',
-    selected: [],
-  });
-  const loading = ref(true);
-  // 获取
-  async function getData() {
-    loading.value = true;
-    const { error, data } = await api.area.list({
-      order: tree.order,
-      sort: tree.sort,
-    });
-    if (error === 0) {
-      tree.data = data;
-    }
-    loading.value = false;
-  }
-
-  // table 字段排序
-  function fieldFilter({ prop, order }) {
-    table.order = order == 'ascending' ? 'asc' : 'desc';
-    table.sort = prop;
-    getData();
-  }
-  function addRow() {
-    useModal(
-      AreaEdit,
-      { title: '新建', type: 'add' },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-  function editRow(row) {
-    useModal(
-      AreaEdit,
-      {
-        title: '编辑',
-        type: 'edit',
-        id: row.data.id,
-      },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-  // 删除api 单独批量可以直接调用
-  async function remove(id) {
-    await api.area.delete(id);
-    getData();
-  }
-
-  onMounted(() => {
-    getData();
-  });
-</script>
-<style lang="scss" scoped>
-  .area-view {
-    .tip {
-      line-height: 20px;
-      font-size: 12px;
-      font-weight: 400;
-      color: var(--sa-subfont);
-      margin-left: 8px;
-      a {
-        color: var(--sa-subfont);
-        text-decoration: underline;
-      }
-    }
-    .el-main {
-      background: var(--sa-background-assist);
-      .list-head {
-        width: 100%;
-        height: 40px;
-        padding: 0 40px 0 24px;
-        font-size: 14px;
-        background: var(--sa-table-header-bg);
-        color: var(--sa-subtitle);
-        .list-head-level {
-          margin-right: 54px;
-        }
-      }
-      .tree-box{
-        width: 100%;
-      }
-      .tree-name {
-        font-size: 14px;
-        color: var(--sa-font);
-        .adcode {
-          margin-right: 10px;
-          color: #999;
-        }
-        @media only screen and (max-width: 768px) {
-          .tree-name-title {
-            width: 90px;
-          }
-        }
-      }
-      .level {
-        font-size: 14px;
-        color: var(--sa-font);
-        margin-right: 30px;
-      }
-    }
-  }
-  :deep() {
-    .el-tree-node__content {
-      height: 48px;
-    }
-    .el-tree-node__label {
-      width: 100%;
-    }
-    .el-tree-node__expand-icon {
-      margin-left: 10px;
-    }
-  }
-</style>

+ 0 - 171
src/app/shop/admin/data/area/select.vue

@@ -1,171 +0,0 @@
-<template>
-  <el-container class="panel-block">
-    <el-main>
-      <el-scrollbar height="100%">
-        <el-checkbox
-          v-model="state.checkedAll"
-          :indeterminate="isIndeterminate"
-          label="全选"
-          @change="onChange"
-        >
-        </el-checkbox>
-        <el-tree
-          :data="state.data"
-          node-key="id"
-          show-checkbox
-          :default-checked-keys="state.ids"
-          @check-change="onChangeCheck"
-          ref="treeRef"
-        >
-          <template #default="{ data }">{{ data.name }}</template>
-        </el-tree>
-      </el-scrollbar>
-    </el-main>
-    <el-footer class="sa-footer--submit sa-flex sa-row-right">
-      <el-button type="primary" @click="onConfirm">确定</el-button>
-    </el-footer>
-  </el-container>
-</template>
-
-<script>
-  export default {
-    name: 'AreaSelect',
-  };
-</script>
-
-<script setup>
-  import { reactive, computed, onMounted, getCurrentInstance, nextTick } from 'vue';
-  import { api } from '../data.service';
-
-  const emit = defineEmits(['modalCallBack']);
-  const props = defineProps(['modal']);
-
-  const { proxy } = getCurrentInstance();
-
-  const state = reactive({
-    // selectedData: props.modal.params.selected,
-    data: [],
-    ids: [],
-    label: {},
-    checkedAll: false,
-  });
-
-  async function getSelect() {
-    const { data } = await api.area.select();
-    state.data = data;
-    state.checkedAll = props.modal.params.selected.province.split(',').length == state.data.length;
-
-    nextTick(() => {
-      proxy.$refs['treeRef']?.getCheckedNodes().forEach((data) => {
-        if (state.ids.includes(data.id)) {
-          if (!state.label[data.level]) {
-            state.label[data.level] = {};
-          }
-          state.label[data.level][data.id] = data.name;
-        }
-      });
-    });
-  }
-
-  const isIndeterminate = computed(() =>
-    state.ids.length > 0 && !state.checkedAll ? true : false,
-  );
-
-  function onChange() {
-    if (state.checkedAll) {
-      nextTick(() => {
-        state.ids = [];
-        proxy.$refs['treeRef']?.setCheckedNodes(state.data, false);
-        state.data.forEach((d) => {
-          state.ids.push(d.id);
-          if (!state.label[d.level]) {
-            state.label[d.level] = {};
-          }
-          state.label[d.level][d.id] = d.name;
-        });
-      });
-    } else {
-      proxy.$refs['treeRef']?.setCheckedKeys([], false);
-      state.ids = [];
-      state.label = {
-        province: {},
-        city: {},
-        district: {},
-      };
-    }
-  }
-
-  function onChangeCheck(data, checked, indeterminate) {
-    // 全选
-    if (!state.checkedAll) {
-      // 选中(把自己放进去)
-      if (checked) {
-        state.ids.push(data.id);
-        if (!state.label[data.level]) {
-          state.label[data.level] = {};
-        }
-        state.label[data.level][data.id] = data.name;
-      }
-    }
-
-    // 未选中(把自己删除)
-    if (!checked) {
-      if (state.ids.includes(data.id)) {
-        state.ids.splice(state.ids.indexOf(data.id), 1);
-        for (var key in state.label[data.level]) {
-          if (Number(key) == Number(data.id)) {
-            delete state.label[data.level][key];
-          }
-        }
-      }
-    }
-
-    if (state.label.province)
-      state.checkedAll = Object.keys(state.label.province).length == state.data.length;
-  }
-
-  function deleteId(arr) {
-    arr.forEach((item) => {
-      if (state.ids.includes(item.id)) {
-        state.ids.splice(state.ids.indexOf(item.id), 1);
-        delete state.label[item.level][item.id];
-      }
-      if (item.children && item.children.length > 0) {
-        deleteId(item.children);
-      }
-    });
-  }
-
-  function onConfirm() {
-    state.ids = [];
-    state.label = {
-      province: {},
-      city: {},
-      district: {},
-    };
-    proxy.$refs['treeRef']?.getCheckedNodes().forEach((item) => {
-      state.ids.push(item.id);
-      if (!state.label[item.level]) {
-        state.label[item.level] = {};
-      }
-      state.label[item.level][item.id] = item.name;
-    });
-    proxy.$refs['treeRef']?.getCheckedNodes().forEach((item) => {
-      if (item.children && item.children.length > 0) {
-        deleteId(item.children);
-      }
-    });
-    emit('modalCallBack', { event: 'confirm', data: state.label });
-  }
-
-  onMounted(() => {
-    for (var level in props.modal.params.selected) {
-      if (props.modal.params.selected[level]) {
-        props.modal.params.selected[level].split(',').forEach((id) => {
-          state.ids.push(id);
-        });
-      }
-    }
-    getSelect();
-  });
-</script>

+ 0 - 107
src/app/shop/admin/data/data.service.js

@@ -1,107 +0,0 @@
-import Content from '@/sheep/layouts/content.vue';
-import { request } from '@/sheep/request';
-import { SELECT, CRUD } from '@/sheep/request/crud';
-
-const route = {
-  path: 'data',
-  name: 'shop.admin.data',
-  component: Content,
-  meta: {
-    title: '数据维护',
-  },
-  children: [
-    {
-      path: 'page',
-      name: 'shop.admin.data.page',
-      component: () => import('./page/index.vue'),
-      meta: {
-        title: '页面链接',
-      },
-    },
-    {
-      path: 'richtext',
-      name: 'shop.admin.data.richtext',
-      component: () => import('./richtext/index.vue'),
-      meta: {
-        title: '富文本',
-      },
-    },
-    {
-      path: 'area',
-      name: 'shop.admin.data.area',
-      component: () => import('./area/index.vue'),
-      meta: {
-        title: '省市区',
-      },
-    },
-    {
-      path: 'faq',
-      name: 'shop.admin.data.faq',
-      component: () => import('./faq/index.vue'),
-      meta: {
-        title: '常见问题',
-      },
-    },
-    {
-      path: 'express',
-      name: 'shop.admin.data.express',
-      component: () => import('./express/index.vue'),
-      meta: {
-        title: '快递公司',
-      },
-    },
-    {
-      path: 'fakeuser',
-      name: 'shop.admin.data.fakeuser',
-      component: () => import('./fakeUser/index.vue'),
-      meta: {
-        title: '虚拟用户',
-      },
-    },
-  ],
-};
-
-const api = {
-  page: {
-    ...CRUD('shop/admin/data/page'),
-    select: (params) => SELECT('shop/admin/data/page', params),
-  },
-  area: {
-    ...CRUD('shop/admin/data/area'),
-    select: (params) => SELECT('shop/admin/data/area', params),
-  },
-  faq: {
-    ...CRUD('shop/admin/data/faq'),
-  },
-  richtext: {
-    ...CRUD('shop/admin/data/richtext'),
-    select: (params, type = 'page') =>
-      request({
-        url: `shop/admin/data/richtext/select?type=${type}`,
-        method: 'GET',
-        params,
-      }),
-  },
-  express: {
-    ...CRUD('shop/admin/data/express'),
-    select: (params) => SELECT('shop/admin/data/express', params),
-  },
-  fakeUser: {
-    ...CRUD('shop/admin/data/fakeUser'),
-    select: (params) => SELECT('shop/admin/data/fakeUser', params),
-    random: (data) =>
-      request({
-        url: 'shop/admin/data/fakeUser/random',
-        method: 'POST',
-        data,
-        timeout: 60000,
-      }),
-    getRandom: () =>
-      request({
-        url: 'shop/admin/data/fakeUser/getRandom',
-        method: 'GET',
-      }),
-  },
-};
-
-export { route, api };

+ 0 - 90
src/app/shop/admin/data/express/edit.vue

@@ -1,90 +0,0 @@
-<template>
-  <el-container>
-    <el-main>
-      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
-        <el-form-item label="快递公司" prop="name">
-          <el-input v-model="form.model.name" placeholder="请输入快递公司"></el-input>
-        </el-form-item>
-        <el-form-item label="快递编码" prop="code">
-          <el-input v-model="form.model.code" placeholder="请输入快递编码"></el-input>
-        </el-form-item>
-        <el-form-item label="权重" prop="weigh">
-          <el-input v-model="form.model.weigh" placeholder="请输入权重" type="number"></el-input>
-        </el-form-item>
-      </el-form>
-    </el-main>
-    <el-footer class="sa-footer--submit">
-      <el-button
-        v-if="modal.params.type == 'add'"
-        v-auth="'shop.admin.data.express.add'"
-        type="primary"
-        @click="confirm"
-        >保存</el-button
-      >
-      <el-button
-        v-if="modal.params.type == 'edit'"
-        v-auth="'shop.admin.data.express.edit'"
-        type="primary"
-        @click="confirm"
-        >更新</el-button
-      >
-    </el-footer>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref, unref } from 'vue';
-  import { api } from '../data.service';
-  import { cloneDeep } from 'lodash';
-
-  const emit = defineEmits(['modalCallBack']);
-  const props = defineProps({
-    modal: {
-      type: Object,
-    },
-  });
-  // 添加 编辑 form
-  let formRef = ref(null);
-  const form = reactive({
-    model: {
-      name: '',
-      code: '',
-      weigh: 0,
-    },
-    rules: {
-      name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
-      code: [{ required: true, message: '请输入编码', trigger: 'blur' }],
-      weigh: [{ required: true, message: '请输入权重', trigger: 'blur' }],
-    },
-  });
-  const loading = ref(false);
-  // 获取详情
-  async function getDetail(id) {
-    loading.value = true;
-    const { error, data } = await api.express.detail(id);
-    error === 0 && (form.model = data);
-    loading.value = false;
-  }
-  // 表单关闭时提交
-  async function confirm() {
-    // 表单验证
-    unref(formRef).validate(async (valid) => {
-      if (!valid) return;
-      let submitForm = cloneDeep(form.model);
-      const { error } =
-        props.modal.params.type == 'add'
-          ? await api.express.add(submitForm)
-          : await api.express.edit(props.modal.params.id, submitForm);
-      if (error == 0) {
-        emit('modalCallBack', { event: 'confirm' });
-      }
-    });
-  }
-  async function init() {
-    if (props.modal.params.id) {
-      await getDetail(props.modal.params.id);
-    }
-  }
-  onMounted(() => {
-    init();
-  });
-</script>

+ 0 - 57
src/app/shop/admin/data/express/express.js

@@ -1,57 +0,0 @@
-import { getCurrentInstance, onMounted, reactive } from 'vue';
-import { api } from '../data.service';
-
-export default function useExpress() {
-  const { proxy } = getCurrentInstance();
-
-  const express = reactive({ name: '', code: '', no: '' });
-
-  const deliverCompany = reactive({
-    loading: false,
-    data: [],
-    pageData: {
-      page: 1,
-      list_rows: 10,
-      total: 0,
-    },
-  });
-
-  async function getDeliverCompany(keyword) {
-    let search = {};
-    if (keyword) {
-      search = { keyword: keyword };
-    }
-    const { data } = await api.express.select({
-      page: deliverCompany.pageData.page,
-      list_rows: deliverCompany.pageData.list_rows,
-      search: JSON.stringify(search),
-    });
-    deliverCompany.data = data.data;
-    deliverCompany.pageData.page = data.current_page;
-    deliverCompany.pageData.list_rows = data.per_page;
-    deliverCompany.pageData.total = data.total;
-  }
-
-  function onChangeExpressCode(code) {
-    express.name = proxy.$refs[`dc-${code}`][0].label;
-  }
-
-  function remoteMethod(keyword) {
-    deliverCompany.loading = true;
-    setTimeout(() => {
-      deliverCompany.loading = false;
-      getDeliverCompany(keyword);
-    }, 200);
-  }
-
-  onMounted(() => {
-    getDeliverCompany();
-  });
-  return {
-    express,
-    deliverCompany,
-    getDeliverCompany,
-    onChangeExpressCode,
-    remoteMethod,
-  };
-}

+ 0 - 260
src/app/shop/admin/data/express/index.vue

@@ -1,260 +0,0 @@
-<template>
-  <el-container class="panel-block">
-    <el-header class="sa-header">
-      <div class="sa-title sa-flex sa-row-between">
-        <div class="label">
-          <div class="sa-flex">
-            <span class="left">快递公司</span>
-            <search-condition
-              :conditionLabel="filterParams.conditionLabel"
-              @deleteFilter="deleteFilter"
-            ></search-condition>
-          </div>
-        </div>
-        <div>
-          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
-          <el-button class="sa-button-refresh" icon="Search" @click="openFilter"></el-button>
-          <el-button
-            v-auth="'shop.admin.data.express.add'"
-            icon="Plus"
-            type="primary"
-            @click="addRow"
-            >新建</el-button
-          >
-        </div>
-      </div>
-      <el-alert class="sa-alert sa-m-b-16">
-        <template #title>
-          <div> 快递鸟所有快递公司列表,可以只保留自己需要的,删除多余的快递公司 </div>
-        </template>
-      </el-alert>
-    </el-header>
-    <el-main class="sa-p-0" v-loading="loading">
-      <el-table
-        height="100%"
-        class="sa-table"
-        :data="table.data"
-        @selection-change="changeSelection"
-        @sort-change="fieldFilter"
-        @row-dblclick="editRow"
-        row-key="id"
-        stripe
-      >
-        <template #empty>
-          <sa-empty />
-        </template>
-        <el-table-column type="selection" width="48" align="center"></el-table-column>
-        <el-table-column prop="id" label="ID" min-width="80" sortable="custom"> </el-table-column>
-        <el-table-column label="快递公司" min-width="200">
-          <template #default="scope">
-            <span class="sa-table-line-1">
-              {{ scope.row.name || '-' }}
-            </span>
-          </template>
-        </el-table-column>
-        <el-table-column label="快递编码" min-width="140">
-          <template #default="scope">
-            <div class="sa-table-line-1">
-              {{ scope.row.code || '-' }}
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column label="权重" min-width="100" sortable="custom">
-          <template #default="scope">
-            <div>
-              {{ scope.row.weigh }}
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column fixed="right" label="操作" min-width="120">
-          <template #default="scope">
-            <el-button
-              v-auth="'shop.admin.data.express.detail'"
-              class="is-link"
-              type="primary"
-              @click="editRow(scope.row)"
-              >编辑</el-button
-            >
-            <el-popconfirm
-              width="fit-content"
-              confirm-button-text="确认"
-              cancel-button-text="取消"
-              title="确认删除这条记录?"
-              @confirm="deleteApi(scope.row.id)"
-            >
-              <template #reference>
-                <el-button v-auth="'shop.admin.data.express.delete'" class="is-link" type="danger">
-                  删除
-                </el-button>
-              </template>
-            </el-popconfirm>
-          </template>
-        </el-table-column>
-      </el-table>
-    </el-main>
-    <sa-view-bar>
-      <template #left>
-        <sa-batch-handle
-          :batchHandleTools="batchHandleTools"
-          :selectedLeng="table.selected.length"
-          @batchHandle="batchHandle"
-        ></sa-batch-handle>
-      </template>
-      <template #right>
-        <sa-pagination :pageData="pageData" @updateFn="getData" />
-      </template>
-    </sa-view-bar>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from '../data.service';
-  import { ElMessageBox } from 'element-plus';
-  import { useModal, usePagination } from '@/sheep/hooks';
-  import { useSearch } from '@/sheep/components/sa-table/sa-search/useSearch';
-  import { composeFilter } from '@/sheep/utils';
-  import ExpressEdit from './edit.vue';
-  import { cloneDeep } from 'lodash';
-
-  const filterParams = reactive({
-    tools: {
-      keyword: {
-        type: 'tinputprepend',
-        label: '快递信息',
-        field: 'keyword',
-        placeholder: '请输入查询内容',
-        keyword: {
-          field: 'name',
-          value: '',
-        },
-        options: [
-          {
-            label: '快递公司',
-            value: 'name',
-          },
-          {
-            label: '快递编码',
-            value: 'code',
-          },
-        ],
-      },
-    },
-    data: {
-      keyword: { field: 'name', value: '' },
-    },
-    conditionLabel: {},
-  });
-  const { openFilter, deleteFilter } = useSearch({ filterParams, getData });
-
-  // 列表
-  const table = reactive({
-    data: [],
-    order: 'asc',
-    sort: 'id',
-    selected: [],
-  });
-  const { pageData } = usePagination();
-  const loading = ref(true);
-  // 获取
-  async function getData(page) {
-    loading.value = true;
-    if (page) pageData.page = page;
-    let tempSearch = cloneDeep(filterParams.data);
-    let search = composeFilter(tempSearch, {
-      name: 'like',
-      code: 'like',
-    });
-    const { error, data } = await api.express.list({
-      page: pageData.page,
-      list_rows: pageData.list_rows,
-      order: table.order,
-      sort: table.sort,
-      ...search,
-    });
-    if (error === 0) {
-      table.data = data.data;
-      pageData.page = data.current_page;
-      pageData.list_rows = data.per_page;
-      pageData.total = data.total;
-    }
-    loading.value = false;
-  }
-
-  // table 字段排序
-  function fieldFilter({ prop, order }) {
-    table.order = order == 'ascending' ? 'asc' : 'desc';
-    table.sort = prop;
-    getData();
-  }
-  //table批量选择
-  function changeSelection(row) {
-    table.selected = row;
-  }
-  // 批量操作
-  const batchHandleTools = [
-    {
-      type: 'delete',
-      label: '删除',
-      auth: 'shop.admin.data.express.delete',
-      class: 'danger',
-    },
-  ];
-  function addRow() {
-    useModal(
-      ExpressEdit,
-      { title: '新建', type: 'add' },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-  function editRow(row) {
-    useModal(
-      ExpressEdit,
-      {
-        title: '编辑',
-        type: 'edit',
-        id: row.id,
-      },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-
-  // 删除api 单独批量可以直接调用
-  async function deleteApi(id) {
-    await api.express.delete(id);
-    getData();
-  }
-  async function batchHandle(type) {
-    let ids = [];
-    table.selected.forEach((row) => {
-      ids.push(row.id);
-    });
-    switch (type) {
-      case 'delete':
-        ElMessageBox.confirm('此操作将删除, 是否继续?', '提示', {
-          confirmButtonText: '确定',
-          cancelButtonText: '取消',
-          type: 'warning',
-        }).then(() => {
-          deleteApi(ids.join(','));
-        });
-        break;
-      default:
-        await api.express.edit(ids.join(','), {
-          status: type,
-        });
-        getData();
-    }
-  }
-
-  onMounted(() => {
-    getData();
-  });
-</script>

+ 0 - 121
src/app/shop/admin/data/fakeUser/edit.vue

@@ -1,121 +0,0 @@
-<template>
-  <el-container>
-    <el-main>
-      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
-        <el-form-item label="用户头像" prop="avatar">
-          <sa-uploader v-model="form.model.avatar" fileType="image"></sa-uploader>
-        </el-form-item>
-        <el-form-item label="用户名" prop="username">
-          <el-input v-model="form.model.username" placeholder="请输入用户名"></el-input>
-        </el-form-item>
-        <el-form-item label="用户昵称" prop="nickname">
-          <el-input v-model="form.model.nickname" placeholder="请输入用户昵称"></el-input>
-        </el-form-item>
-        <el-form-item label="电子邮箱" prop="email">
-          <el-input v-model="form.model.email" placeholder="请输入电子邮箱"></el-input>
-        </el-form-item>
-        <el-form-item label="手机号" prop="mobile">
-          <el-input v-model="form.model.mobile" placeholder="请输入手机号"></el-input>
-        </el-form-item>
-        <el-form-item label="用户密码">
-          <el-input v-model="form.model.password" placeholder="不修改请留空"></el-input>
-        </el-form-item>
-        <el-form-item label="用户性别">
-          <el-radio-group v-model="form.model.gender">
-            <el-radio :label="0">未知</el-radio>
-            <el-radio :label="1">男</el-radio>
-            <el-radio :label="2">女</el-radio>
-          </el-radio-group>
-        </el-form-item>
-        <el-form-item label="创建时间" v-if="props.modal.params.type == 'edit'">
-          {{ form.model.create_time }}
-        </el-form-item>
-        <el-form-item label="更新时间" v-if="props.modal.params.type == 'edit'">
-          {{ form.model.update_time }}
-        </el-form-item>
-      </el-form>
-    </el-main>
-    <el-footer class="sa-footer--submit">
-      <el-button
-        v-if="modal.params.type == 'add'"
-        v-auth="'shop.admin.data.fakeuser.add'"
-        type="primary"
-        @click="confirm"
-        >保存</el-button
-      >
-      <el-button
-        v-if="modal.params.type == 'edit'"
-        v-auth="'shop.admin.data.fakeuser.edit'"
-        type="primary"
-        @click="confirm"
-        >更新</el-button
-      >
-    </el-footer>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref, unref } from 'vue';
-  import { api } from '../data.service';
-  import { cloneDeep } from 'lodash';
-
-  const emit = defineEmits(['modalCallBack']);
-  const props = defineProps({
-    modal: {
-      type: Object,
-    },
-  });
-  // 添加 编辑 form
-  let formRef = ref(null);
-  const form = reactive({
-    model: {
-      avatar: '',
-      username: '',
-      nickname: '',
-      email: '',
-      mobile: '',
-      password: '',
-      gender: 0,
-    },
-    rules: {
-      avatar: [{ required: true, message: '请上传头像', trigger: 'blur' }],
-      username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
-      nickname: [{ required: true, message: '请输入用户昵称', trigger: 'blur' }],
-      email: [{ required: true, message: '请输入电子邮箱', trigger: 'blur' }],
-      mobile: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
-    },
-  });
-  const loading = ref(false);
-  // 获取详情
-  async function getDetail(id) {
-    loading.value = true;
-    const { error, data } = await api.fakeUser.detail(id);
-    if (error === 0) {
-      form.model = data;
-      form.model.password = '';
-    }
-    loading.value = false;
-  }
-  // 表单关闭时提交
-  async function confirm() {
-    // 表单验证
-    unref(formRef).validate(async (valid) => {
-      if (!valid) return;
-      let submitForm = cloneDeep(form.model);
-      const { error } =
-        props.modal.params.type == 'add'
-          ? await api.fakeUser.add(submitForm)
-          : await api.fakeUser.edit(props.modal.params.id, submitForm);
-      if (error == 0) {
-        emit('modalCallBack', { event: 'confirm' });
-      }
-    });
-  }
-  async function init() {
-    if (props.modal.params.id) {
-      await getDetail(props.modal.params.id);
-    }
-  }
-  onMounted(() => {
-    init();
-  });
-</script>

+ 0 - 341
src/app/shop/admin/data/fakeUser/index.vue

@@ -1,341 +0,0 @@
-<template>
-  <el-container class="fakeUser-view panel-block">
-    <el-header class="sa-header">
-      <div class="sa-title sa-flex sa-row-between">
-        <div class="label sa-flex">
-          <span class="left">虚拟用户</span>
-          <search-condition
-            :conditionLabel="filterParams.conditionLabel"
-            @deleteFilter="deleteFilter"
-          ></search-condition>
-        </div>
-        <div>
-          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
-          <el-button class="sa-button-refresh" icon="Search" @click="openFilter"></el-button>
-          <el-button
-            v-auth="'shop.admin.data.fakeuser.add'"
-            icon="Plus"
-            type="primary"
-            @click="addRow"
-            >新建</el-button
-          >
-          <el-button v-auth="'shop.admin.data.fakeuser.random'" @click="randomRow" plain
-            >自动生成</el-button
-          >
-        </div>
-      </div>
-      <el-alert class="sa-alert sa-m-b-16">
-        <template #title>
-          <div>1、可作为虚拟成团时候的虚拟用户,和虚拟评价时候的虚拟用户</div>
-          <div>2、添加完之后可修改虚拟用户信息,不要轻易删除虚拟用户</div>
-        </template>
-      </el-alert>
-    </el-header>
-    <el-main class="sa-p-0" v-loading="loading">
-      <el-table
-        height="100%"
-        class="sa-table"
-        :data="table.data"
-        @selection-change="changeSelection"
-        @sort-change="fieldFilter"
-        @row-dblclick="editRow"
-        row-key="id"
-        stripe
-      >
-        <template #empty>
-          <sa-empty />
-        </template>
-        <el-table-column type="selection" width="48" align="center"></el-table-column>
-        <el-table-column prop="id" label="ID" min-width="90" sortable="custom"> </el-table-column>
-        <el-table-column label="用户名" min-width="180">
-          <template #default="scope">
-            <span class="sa-table-line-1">
-              {{ scope.row.username || '-' }}
-            </span>
-          </template>
-        </el-table-column>
-        <el-table-column label="用户信息" min-width="180">
-          <template #default="scope">
-            <div class="sa-flex">
-              <sa-image :url="scope.row.avatar" size="32" radius="4"></sa-image>
-              <div class="sa-m-l-12">
-                <div class="sa-m-b-4">{{ scope.row.nickname || '-' }}</div>
-                <div>{{ scope.row.mobile || '-' }}</div>
-              </div>
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column label="性别" width="60">
-          <template #default="scope">
-            <!-- 0未知 1男  -->
-            <sa-svg v-if="scope.row.gender == 0" name="sa-gender-weizhi" class="gender"></sa-svg>
-            <sa-svg v-if="scope.row.gender == 1" name="sa-gender-male" class="gender"></sa-svg>
-            <sa-svg v-if="scope.row.gender == 2" name="sa-gender-female" class="gender"></sa-svg>
-          </template>
-        </el-table-column>
-        <el-table-column label="电子邮箱" min-width="170">
-          <template #default="scope">
-            <div class="sa-table-line-1">
-              {{ scope.row.email || '-' }}
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column label="创建时间" min-width="180">
-          <template #default="scope">
-            {{ scope.row.create_time || '-' }}
-          </template>
-        </el-table-column>
-        <el-table-column label="更新时间" min-width="180">
-          <template #default="scope">
-            {{ scope.row.update_time || '-' }}
-          </template>
-        </el-table-column>
-        <el-table-column fixed="right" label="操作" min-width="120">
-          <template #default="scope">
-            <el-button
-              v-auth="'shop.admin.data.fakeuser.detail'"
-              type="primary"
-              class="is-link"
-              @click="editRow(scope.row)"
-              >编辑</el-button
-            >
-            <el-popconfirm
-              width="fit-content"
-              confirm-button-text="确认"
-              cancel-button-text="取消"
-              title="确认删除这条记录?"
-              @confirm="deleteApi(scope.row.id)"
-            >
-              <template #reference>
-                <el-button v-auth="'shop.admin.data.fakeuser.delete'" type="danger" class="is-link">
-                  删除
-                </el-button>
-              </template>
-            </el-popconfirm>
-          </template>
-        </el-table-column>
-      </el-table>
-    </el-main>
-    <sa-view-bar>
-      <template #left>
-        <sa-batch-handle
-          :batchHandleTools="batchHandleTools"
-          :selectedLeng="table.selected.length"
-          @batchHandle="batchHandle"
-        ></sa-batch-handle>
-      </template>
-      <template #right>
-        <sa-pagination :pageData="pageData" @updateFn="getData" />
-      </template>
-    </sa-view-bar>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from '../data.service';
-  import { ElMessageBox } from 'element-plus';
-  import { useModal, usePagination } from '@/sheep/hooks';
-  import { useSearch } from '@/sheep/components/sa-table/sa-search/useSearch';
-  import { composeFilter } from '@/sheep/utils';
-  import FakeUserEdit from './edit.vue';
-  import FakeUserRandom from './random.vue';
-  import { cloneDeep } from 'lodash';
-
-  // 列表
-  const table = reactive({
-    data: [],
-    order: '',
-    sort: '',
-    selected: [],
-  });
-  const { pageData } = usePagination();
-  const batchHandleTools = [
-    {
-      type: 'delete',
-      label: '删除',
-      auth: 'shop.admin.data.fakeuser.delete',
-      class: 'danger',
-    },
-  ];
-  const loading = ref(true);
-  const filterParams = reactive({
-    tools: {
-      user: {
-        type: 'tinputprepend',
-        label: '用户信息',
-        field: 'user',
-        placeholder: '请输入查询内容',
-        user: {
-          field: 'id',
-          value: '',
-        },
-        options: [
-          {
-            label: '用户ID',
-            value: 'id',
-          },
-          {
-            label: '用户名',
-            value: 'username',
-          },
-          {
-            label: '用户昵称',
-            value: 'nickname',
-          },
-          {
-            label: '用户手机号',
-            value: 'mobile',
-          },
-          {
-            label: '邮箱',
-            value: 'email',
-          },
-        ],
-      },
-      gender: {
-        type: 'tselect',
-        label: '用户性别',
-        field: 'gender',
-        value: '',
-        options: {
-          data: [
-            {
-              label: '全部',
-              value: '',
-            },
-            {
-              label: '未知',
-              value: '0',
-            },
-            {
-              label: '男',
-              value: '1',
-            },
-            {
-              label: '女',
-              value: '2',
-            },
-          ],
-        },
-      },
-    },
-    data: {
-      user: { field: 'id', value: '' },
-      gender: '',
-    },
-    conditionLabel: {},
-  });
-  const { openFilter, deleteFilter } = useSearch({ filterParams, getData });
-  // 获取
-  async function getData(page) {
-    loading.value = true;
-    if (page) pageData.page = page;
-    let tempSearch = cloneDeep(filterParams.data);
-    let search = composeFilter(tempSearch, {
-      username: 'like',
-      nickname: 'like',
-      email: 'like',
-      mobile: 'like',
-    });
-    const { error, data } = await api.fakeUser.list({
-      page: pageData.page,
-      list_rows: pageData.list_rows,
-      order: table.order,
-      sort: table.sort,
-      ...search,
-    });
-    if (error === 0) {
-      table.data = data.data;
-      pageData.page = data.current_page;
-      pageData.list_rows = data.per_page;
-      pageData.total = data.total;
-    }
-    loading.value = false;
-  }
-
-  // table 字段排序
-  function fieldFilter({ prop, order }) {
-    table.order = order == 'ascending' ? 'asc' : 'desc';
-    table.sort = prop;
-    getData();
-  }
-  //table批量选择
-  function changeSelection(row) {
-    table.selected = row;
-  }
-
-  function addRow() {
-    useModal(
-      FakeUserEdit,
-      { title: '新建', type: 'add' },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-  function randomRow() {
-    useModal(
-      FakeUserRandom,
-      { title: '自动生成' },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-  function editRow(row) {
-    useModal(
-      FakeUserEdit,
-      {
-        title: '编辑',
-        type: 'edit',
-        id: row.id,
-      },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-  // 删除api 单独批量可以直接调用
-  async function deleteApi(id) {
-    await api.fakeUser.delete(id);
-    getData();
-  }
-  async function batchHandle(type) {
-    let ids = [];
-    table.selected.forEach((row) => {
-      ids.push(row.id);
-    });
-    switch (type) {
-      case 'delete':
-        ElMessageBox.confirm('此操作将删除, 是否继续?', '提示', {
-          confirmButtonText: '确定',
-          cancelButtonText: '取消',
-          type: 'warning',
-        }).then(() => {
-          deleteApi(ids.join(','));
-        });
-        break;
-      default:
-        await api.fakeUser.edit(ids.join(','), {
-          status: type,
-        });
-        getData();
-    }
-  }
-
-  onMounted(() => {
-    getData();
-  });
-</script>
-<style lang="scss" scoped>
-  .gender {
-    width: 16px !important;
-    height: 16px !important;
-  }
-</style>

+ 0 - 56
src/app/shop/admin/data/fakeUser/random.vue

@@ -1,56 +0,0 @@
-<template>
-  <el-container>
-    <el-main>
-      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
-        <el-form-item label="生成人数" prop="num">
-          <el-input
-            v-model="form.model.num"
-            placeholder="请输入生成虚拟人数"
-            type="number"
-            min="0"
-          ></el-input>
-        </el-form-item>
-      </el-form>
-    </el-main>
-    <el-footer class="sa-footer--submit">
-      <el-button type="primary" :loading="loading" :disabled="loading" @click="confirm"
-        >确 定</el-button
-      >
-    </el-footer>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref, unref } from 'vue';
-  import { api } from '../data.service';
-  const emit = defineEmits(['modalCallBack']);
-  const props = defineProps({
-    modal: {
-      type: Object,
-    },
-  });
-  // 添加 编辑 form
-  let formRef = ref(null);
-  const form = reactive({
-    model: {
-      num: 1,
-    },
-    rules: {
-      num: [{ required: true, message: '请输入生成虚拟人数', trigger: 'blur' }],
-    },
-  });
-  const loading = ref(false);
-  // 表单关闭时提交
-  async function confirm() {
-    // 表单验证
-    unref(formRef).validate(async (valid) => {
-      if (!valid) return;
-      loading.value = true;
-      form.model.num = Number(form.model.num);
-      const { error } = await api.fakeUser.random(form.model);
-      if (error == 0) {
-        loading.value = false;
-        emit('modalCallBack', { event: 'confirm' });
-      }
-    });
-  }
-</script>

+ 0 - 110
src/app/shop/admin/data/fakeUser/select.vue

@@ -1,110 +0,0 @@
-<template>
-  <el-container class="richtext-select panel-block">
-    <el-main>
-      <template v-if="table.data.length > 0">
-        <el-table class="sa-table" :data="table.data" row-key="id" stripe>
-          <el-table-column prop="id" label="ID" min-width="100"></el-table-column>
-          <el-table-column label="用户" min-width="160">
-            <template #default="scope">
-              <div class="sa-flex">
-                <img :src="checkUrl(scope.row.avatar)" class="avatar-img" />
-                <span class="sa-m-l-20 sa-table-line-1">{{ scope.row.nickname || '-' }}</span>
-              </div>
-            </template>
-          </el-table-column>
-          <el-table-column fixed="right" label="操作" min-width="100">
-            <template #default="scope">
-              <el-button class="is-link" type="primary" @click="modalCallBack(scope.row)"
-                >选择</el-button
-              >
-            </template>
-          </el-table-column>
-        </el-table>
-        <div class="sa-flex sa-row-right">
-          <sa-pagination :pageData="pageData" @updateFn="getData" />
-        </div>
-      </template>
-      <template v-if="table.data.length == 0 && !loading">
-        <sa-empty></sa-empty>
-      </template>
-    </el-main>
-  </el-container>
-</template>
-
-<script>
-  export default {
-    name: 'RichtextSelect',
-  };
-</script>
-
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from '../data.service';
-  import { usePagination } from '@/sheep/hooks';
-  import { checkUrl } from '@/sheep/utils/checkUrlSuffix';
-  const props = defineProps(['modal']);
-  const emit = defineEmits(['modalCallBack']);
-  const { pageData } = usePagination();
-  // 列表
-  const table = reactive({
-    data: [],
-    selected: {},
-  });
-  const loading = ref(true);
-  const searchtext = ref('');
-  async function openFilter() {
-    let search = {
-      keyword: [searchtext.value],
-    };
-    const { data } = await api.richtext.select({
-      page: pageData.page,
-      list_rows: pageData.list_rows,
-      search,
-    });
-
-    table.data = data.data;
-    pageData.page = data.current_page;
-    pageData.list_rows = data.per_page;
-    pageData.total = data.total;
-  }
-  // 获取
-  async function getData() {
-    loading.value = true;
-    const { data } = await api.fakeUser.select({
-      page: pageData.page,
-      list_rows: pageData.list_rows,
-    });
-    table.data = data.data;
-    pageData.page = data.current_page;
-    pageData.list_rows = data.per_page;
-    pageData.total = data.total;
-    loading.value = false;
-  }
-  function modalCallBack(e) {
-    emit('modalCallBack', { event: 'confirm', data: e });
-  }
-
-  onMounted(() => {
-    getData();
-  });
-</script>
-<style lang="scss" scoped>
-  .richtext-select {
-    .el-main {
-      :deep() {
-        .el-table__header {
-          .el-table-column--selection {
-            .el-checkbox {
-              display: none;
-            }
-          }
-        }
-      }
-    }
-  }
-  .avatar-img {
-    width: 40px;
-    height: 40px;
-    border-radius: 50%;
-  }
-</style>

+ 0 - 97
src/app/shop/admin/data/faq/edit.vue

@@ -1,97 +0,0 @@
-<template>
-  <el-container>
-    <el-main>
-      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
-        <el-form-item label="标题" prop="title">
-          <el-input v-model="form.model.title" placeholder="标题"></el-input>
-        </el-form-item>
-        <el-form-item label="内容" prop="content">
-          <el-input
-            type="textarea"
-            v-model="form.model.content"
-            placeholder="请输入内容"
-          ></el-input>
-        </el-form-item>
-        <el-form-item label="状态" prop="status">
-          <el-radio-group v-model="form.model.status">
-            <el-radio label="normal">正常</el-radio>
-            <el-radio label="hidden">隐藏</el-radio>
-          </el-radio-group>
-        </el-form-item>
-      </el-form>
-    </el-main>
-    <el-footer class="sa-footer--submit">
-      <el-button
-        v-if="modal.params.type == 'add'"
-        v-auth="'shop.admin.data.faq.add'"
-        type="primary"
-        @click="confirm"
-        >保存</el-button
-      >
-      <el-button
-        v-if="modal.params.type == 'edit'"
-        v-auth="'shop.admin.data.faq.edit'"
-        type="primary"
-        @click="confirm"
-        >更新</el-button
-      >
-    </el-footer>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref, unref } from 'vue';
-  import { api } from '../data.service';
-  import { cloneDeep } from 'lodash';
-
-  const emit = defineEmits(['modalCallBack']);
-  const props = defineProps({
-    modal: {
-      type: Object,
-    },
-  });
-  // 添加 编辑 form
-  let formRef = ref(null);
-  const form = reactive({
-    model: {
-      title: '',
-      content: '',
-      status: 'normal',
-    },
-    rules: {
-      title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
-      content: [{ required: true, message: '请输入内容', trigger: 'blur' }],
-      status: [{ required: true, message: '请选择状态', trigger: 'blur' }],
-    },
-  });
-  const loading = ref(false);
-  // 获取详情
-  async function getDetail(id) {
-    loading.value = true;
-    const { error, data } = await api.faq.detail(id);
-    error === 0 && (form.model = data);
-    loading.value = false;
-  }
-  // 表单关闭时提交
-  async function confirm() {
-    // 表单验证
-    unref(formRef).validate(async (valid) => {
-      if (!valid) return;
-      let submitForm = cloneDeep(form.model);
-      const { error } =
-        props.modal.params.type == 'add'
-          ? await api.faq.add(submitForm)
-          : await api.faq.edit(props.modal.params.id, submitForm);
-      if (error == 0) {
-        emit('modalCallBack', { event: 'confirm' });
-      }
-    });
-  }
-  async function init() {
-    if (props.modal.params.id) {
-      await getDetail(props.modal.params.id);
-    }
-  }
-  onMounted(() => {
-    init();
-  });
-</script>

+ 0 - 256
src/app/shop/admin/data/faq/index.vue

@@ -1,256 +0,0 @@
-<template>
-  <el-container class="panel-block">
-    <el-header class="sa-header">
-      <div class="sa-title sa-flex sa-row-between">
-        <div class="label sa-flex">
-          <span class="left">常见问题</span>
-          <search-condition
-            :conditionLabel="filterParams.conditionLabel"
-            @deleteFilter="deleteFilter"
-          ></search-condition>
-        </div>
-        <div>
-          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
-          <el-button class="sa-button-refresh" icon="Search" @click="openFilter"></el-button>
-          <el-button v-auth="'shop.admin.data.faq.add'" icon="Plus" type="primary" @click="addRow"
-            >新建</el-button
-          >
-        </div>
-      </div>
-    </el-header>
-    <el-main class="sa-p-0" v-loading="loading">
-      <el-table
-        height="100%"
-        class="sa-table"
-        :data="table.data"
-        @selection-change="changeSelection"
-        @sort-change="fieldFilter"
-        @row-dblclick="editRow"
-        row-key="id"
-        stripe
-      >
-        <template #empty>
-          <sa-empty />
-        </template>
-        <el-table-column type="selection" width="48" align="center"></el-table-column>
-        <el-table-column prop="id" label="ID" min-width="100" sortable="custom"> </el-table-column>
-        <el-table-column label="标题" min-width="100">
-          <template #default="scope">
-            <span class="sa-table-line-1">
-              {{ scope.row.title || '-' }}
-            </span>
-          </template>
-        </el-table-column>
-        <el-table-column label="内容" min-width="140">
-          <template #default="scope">
-            <div class="sa-table-line-1">
-              {{ scope.row.content || '-' }}
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column label="状态" min-width="100">
-          <template #default="scope">
-            <el-tag :type="scope.row.status == 'normal' ? 'success' : 'info'">
-              {{ scope.row.status_text }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="创建时间" min-width="160">
-          <template #default="scope">
-            {{ scope.row.create_time || '-' }}
-          </template>
-        </el-table-column>
-        <el-table-column label="更新时间" min-width="160">
-          <template #default="scope">
-            {{ scope.row.update_time || '-' }}
-          </template>
-        </el-table-column>
-        <el-table-column fixed="right" label="操作" min-width="120">
-          <template #default="scope">
-            <el-button
-              v-auth="'shop.admin.data.faq.detail'"
-              type="primary"
-              class="is-link"
-              @click="editRow(scope.row)"
-              >编辑</el-button
-            >
-            <el-popconfirm
-              width="fit-content"
-              confirm-button-text="确认"
-              cancel-button-text="取消"
-              title="确认删除这条记录?"
-              @confirm="deleteApi(scope.row.id)"
-            >
-              <template #reference>
-                <el-button v-auth="'shop.admin.data.faq.delete'" type="danger" class="is-link">
-                  删除
-                </el-button>
-              </template>
-            </el-popconfirm>
-          </template>
-        </el-table-column>
-      </el-table>
-    </el-main>
-    <sa-view-bar>
-      <template #left>
-        <sa-batch-handle
-          :batchHandleTools="batchHandleTools"
-          :selectedLeng="table.selected.length"
-          @batchHandle="batchHandle"
-        ></sa-batch-handle>
-      </template>
-      <template #right>
-        <sa-pagination :pageData="pageData" @updateFn="getData" />
-      </template>
-    </sa-view-bar>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from '../data.service';
-  import { ElMessageBox } from 'element-plus';
-  import { useModal, usePagination } from '@/sheep/hooks';
-  import { useSearch } from '@/sheep/components/sa-table/sa-search/useSearch';
-  import { composeFilter } from '@/sheep/utils';
-  import FaqEdit from './edit.vue';
-  import { cloneDeep } from 'lodash';
-
-  const filterParams = reactive({
-    tools: {
-      keyword: {
-        type: 'tinput',
-        field: 'keyword',
-        value: '',
-        label: '搜索内容',
-        placeholder: '请输入查询内容',
-      },
-    },
-    data: {
-      keyword: '',
-    },
-    conditionLabel: {},
-  });
-  const { openFilter, deleteFilter } = useSearch({ filterParams, getData });
-
-  // 列表
-  const table = reactive({
-    data: [],
-    order: '',
-    sort: '',
-    selected: [],
-  });
-  const { pageData } = usePagination();
-  const batchHandleTools = [
-    {
-      type: 'delete',
-      label: '删除',
-      auth: 'shop.admin.data.faq.delete',
-      class: 'danger',
-    },
-    {
-      type: 'normal',
-      label: '正常',
-      auth: 'shop.admin.data.faq.edit',
-      class: 'success',
-    },
-    {
-      type: 'hidden',
-      label: '隐藏',
-      auth: 'shop.admin.data.faq.edit',
-      class: 'info',
-    },
-  ];
-  const loading = ref(true);
-  // 获取
-  async function getData(page) {
-    loading.value = true;
-    if (page) pageData.page = page;
-    let tempSearch = cloneDeep(filterParams.data);
-    let search = composeFilter(tempSearch, {
-      keyword: 'like',
-    });
-    const { error, data } = await api.faq.list({
-      page: pageData.page,
-      list_rows: pageData.list_rows,
-      order: table.order,
-      sort: table.sort,
-      ...search,
-    });
-    if (error === 0) {
-      table.data = data.data;
-      pageData.page = data.current_page;
-      pageData.list_rows = data.per_page;
-      pageData.total = data.total;
-    }
-    loading.value = false;
-  }
-
-  // table 字段排序
-  function fieldFilter({ prop, order }) {
-    table.order = order == 'ascending' ? 'asc' : 'desc';
-    table.sort = prop;
-    getData();
-  }
-  //table批量选择
-  function changeSelection(row) {
-    table.selected = row;
-  }
-
-  function addRow() {
-    useModal(
-      FaqEdit,
-      { title: '新建', type: 'add' },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-  function editRow(row) {
-    useModal(
-      FaqEdit,
-      {
-        title: '编辑',
-        type: 'edit',
-        id: row.id,
-      },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-  // 删除api 单独批量可以直接调用
-  async function deleteApi(id) {
-    await api.faq.delete(id);
-    getData();
-  }
-  async function batchHandle(type) {
-    let ids = [];
-    table.selected.forEach((row) => {
-      ids.push(row.id);
-    });
-    switch (type) {
-      case 'delete':
-        ElMessageBox.confirm('此操作将删除, 是否继续?', '提示', {
-          confirmButtonText: '确定',
-          cancelButtonText: '取消',
-          type: 'warning',
-        }).then(() => {
-          deleteApi(ids.join(','));
-        });
-        break;
-      default:
-        await api.faq.edit(ids.join(','), {
-          status: type,
-        });
-        getData();
-    }
-  }
-
-  onMounted(() => {
-    getData();
-  });
-</script>

+ 0 - 90
src/app/shop/admin/data/page/edit.vue

@@ -1,90 +0,0 @@
-<template>
-  <el-container>
-    <el-main>
-      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
-        <el-form-item label="名称" prop="name">
-          <el-input v-model="form.model.name" placeholder="请输入名称"></el-input>
-        </el-form-item>
-        <el-form-item label="路径" prop="path">
-          <el-input v-model="form.model.path" placeholder="请输入路径"></el-input>
-        </el-form-item>
-        <el-form-item label="分组" prop="group">
-          <el-input v-model="form.model.group" placeholder="请输入分组"></el-input>
-        </el-form-item>
-      </el-form>
-    </el-main>
-    <el-footer class="sa-footer--submit">
-      <el-button
-        v-if="modal.params.type == 'add'"
-        v-auth="'shop.admin.data.page.add'"
-        type="primary"
-        @click="confirm"
-        >保存</el-button
-      >
-      <el-button
-        v-if="modal.params.type == 'edit'"
-        v-auth="'shop.admin.data.page.edit'"
-        type="primary"
-        @click="confirm"
-        >更新</el-button
-      >
-    </el-footer>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref, unref } from 'vue';
-  import { api } from '../data.service';
-  import { cloneDeep } from 'lodash';
-
-  const emit = defineEmits(['modalCallBack']);
-  const props = defineProps({
-    modal: {
-      type: Object,
-    },
-  });
-  // 添加 编辑 form
-  let formRef = ref(null);
-  const form = reactive({
-    model: {
-      name: '',
-      path: '',
-      group: '',
-    },
-    rules: {
-      name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
-      path: [{ required: true, message: '请输入路径', trigger: 'blur' }],
-      group: [{ required: true, message: '请输入分组', trigger: 'blur' }],
-    },
-  });
-  const loading = ref(false);
-  // 获取详情
-  async function getDetail(id) {
-    loading.value = true;
-    const { error, data } = await api.page.detail(id);
-    error === 0 && (form.model = data);
-    loading.value = false;
-  }
-  // 表单关闭时提交
-  async function confirm() {
-    // 表单验证
-    unref(formRef).validate(async (valid) => {
-      if (!valid) return;
-      let submitForm = cloneDeep(form.model);
-      const { error } =
-        props.modal.params.type == 'add'
-          ? await api.page.add(submitForm)
-          : await api.page.edit(props.modal.params.id, submitForm);
-      if (error == 0) {
-        emit('modalCallBack', { event: 'confirm' });
-      }
-    });
-  }
-  async function init() {
-    if (props.modal.params.id) {
-      await getDetail(props.modal.params.id);
-    }
-  }
-  onMounted(() => {
-    init();
-  });
-</script>

+ 0 - 211
src/app/shop/admin/data/page/index.vue

@@ -1,211 +0,0 @@
-<template>
-  <el-container class="panel-block">
-    <el-header class="sa-header">
-      <div class="sa-title sa-flex sa-row-between">
-        <div class="label sa-flex">页面链接</div>
-        <div>
-          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
-          <el-button v-auth="'shop.admin.data.page.add'" icon="Plus" type="primary" @click="addRow"
-            >新建</el-button
-          >
-        </div>
-      </div>
-    </el-header>
-    <el-main class="sa-p-0" v-loading="loading">
-      <el-table
-        height="100%"
-        class="sa-table"
-        :data="table.data"
-        @selection-change="changeSelection"
-        @sort-change="fieldFilter"
-        @row-dblclick="editRow"
-        row-key="id"
-        stripe
-      >
-        <template #empty>
-          <sa-empty />
-        </template>
-        <el-table-column type="selection" width="48" align="center"></el-table-column>
-        <el-table-column prop="id" label="ID" min-width="100" sortable="custom"> </el-table-column>
-        <el-table-column label="名称" min-width="200" align="center">
-          <template #default="scope">
-            <span class="sa-table-line-1">
-              {{ scope.row.name || '-' }}
-            </span>
-          </template>
-        </el-table-column>
-        <el-table-column label="路径" min-width="340" align="center">
-          <template #default="scope">
-            <div class="sa-flex sa-row-left">
-              {{ scope.row.path || '-' }}
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column label="分组" min-width="100">
-          <template #default="scope">
-            <div>
-              {{ scope.row.group || '-' }}
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column label="创建时间" min-width="180">
-          <template #default="scope">
-            {{ scope.row.create_time || '-' }}
-          </template>
-        </el-table-column>
-        <el-table-column label="更新时间" min-width="180">
-          <template #default="scope">
-            {{ scope.row.update_time || '-' }}
-          </template>
-        </el-table-column>
-        <el-table-column fixed="right" label="操作" min-width="120">
-          <template #default="scope">
-            <el-button
-              v-auth="'shop.admin.data.page.detail'"
-              type="primary"
-              class="is-link"
-              @click="editRow(scope.row)"
-              >编辑</el-button
-            >
-            <el-popconfirm
-              width="fit-content"
-              confirm-button-text="确认"
-              cancel-button-text="取消"
-              title="确认删除这条记录?"
-              @confirm="deleteApi(scope.row.id)"
-            >
-              <template #reference>
-                <el-button v-auth="'shop.admin.data.page.delete'" type="danger" class="is-link">
-                  删除
-                </el-button>
-              </template>
-            </el-popconfirm>
-          </template>
-        </el-table-column>
-      </el-table>
-    </el-main>
-    <sa-view-bar>
-      <template #left>
-        <sa-batch-handle
-          :batchHandleTools="batchHandleTools"
-          :selectedLeng="table.selected.length"
-          @batchHandle="batchHandle"
-        ></sa-batch-handle>
-      </template>
-      <template #right>
-        <sa-pagination :pageData="pageData" @updateFn="getData" />
-      </template>
-    </sa-view-bar>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from '../data.service';
-  import { ElMessageBox } from 'element-plus';
-  import { useModal } from '@/sheep/hooks';
-  import { usePagination } from '@/sheep/hooks';
-  import PageEdit from './edit.vue';
-  const { pageData } = usePagination();
-
-  // 列表
-  const table = reactive({
-    data: [],
-    order: '',
-    sort: '',
-    selected: [],
-  });
-  const loading = ref(true);
-  // 获取
-  async function getData() {
-    loading.value = true;
-    const { error, data } = await api.page.list({
-      page: pageData.page,
-      list_rows: pageData.list_rows,
-      order: table.order,
-      sort: table.sort,
-    });
-    if (error === 0) {
-      table.data = data.data;
-      pageData.page = data.current_page;
-      pageData.list_rows = data.per_page;
-      pageData.total = data.total;
-    }
-    loading.value = false;
-  }
-  // table 字段排序
-  function fieldFilter({ prop, order }) {
-    table.order = order == 'ascending' ? 'asc' : 'desc';
-    table.sort = prop;
-    getData();
-  }
-  //table批量选择
-  function changeSelection(row) {
-    table.selected = row;
-  }
-  // 分页/批量操作
-  const batchHandleTools = [
-    {
-      type: 'delete',
-      label: '删除',
-      auth: 'shop.admin.data.page.delete',
-      class: 'danger',
-    },
-  ];
-  function addRow() {
-    useModal(
-      PageEdit,
-      { title: '新建', type: 'add' },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-  function editRow(row) {
-    useModal(
-      PageEdit,
-      {
-        title: '编辑',
-        type: 'edit',
-        id: row.id,
-      },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-  // 删除api 单独批量可以直接调用
-  async function deleteApi(id) {
-    await api.page.delete(id);
-    getData();
-  }
-  async function batchHandle(type) {
-    let ids = [];
-    table.selected.forEach((row) => {
-      ids.push(row.id);
-    });
-    switch (type) {
-      case 'delete':
-        ElMessageBox.confirm('此操作将删除, 是否继续?', '提示', {
-          confirmButtonText: '确定',
-          cancelButtonText: '取消',
-          type: 'warning',
-        }).then(() => {
-          deleteApi(ids.join(','));
-        });
-        break;
-      default:
-        await api.page.edit(ids.join(','), {
-          status: type,
-        });
-        getData();
-    }
-  }
-
-  onMounted(() => {
-    getData();
-  });
-</script>

+ 0 - 387
src/app/shop/admin/data/page/select.vue

@@ -1,387 +0,0 @@
-<template>
-  <el-container class="page-select">
-    <el-main class="select-main sa-p-0">
-      <el-container>
-        <el-aside>
-          <div class="left">
-            <div
-              class="group"
-              :class="tabsScroll.currentIndex == i ? 'is-active' : ''"
-              v-for="(g, i) in tabsScroll.data"
-              :key="g"
-              @click.stop="changeCurrentIndex(i)"
-            >
-              <div class="name">{{ g.group }}</div>
-            </div>
-          </div>
-        </el-aside>
-        <div class="right" ref="rightScrollRef" @scroll="rightScroll">
-          <div
-            class="group right-group"
-            :ref="(el) => setRightRef(el, g, i)"
-            v-for="(g, i) in tabsScroll.data"
-            :key="g"
-          >
-            <div class="name">{{ g.group }}</div>
-            <div class="link sa-flex sa-flex-wrap">
-              <template v-for="l in g.children" :key="l">
-                <el-popover popper-class="sa-popper" trigger="hover" :content="l.path">
-                  <template #reference>
-                    <div
-                      class="item"
-                      :class="tabsScroll.selected.id == l.id ? 'item-active' : ''"
-                      @click="selectLink(l)"
-                    >
-                      {{ l.name }}
-                    </div>
-                  </template>
-                </el-popover>
-              </template>
-            </div>
-          </div>
-        </div>
-      </el-container>
-    </el-main>
-
-    <el-footer class="sa-footer--submit">
-      <el-button type="primary" @click="confirm">确 定</el-button>
-    </el-footer>
-  </el-container>
-</template>
-
-<script>
-  export default {
-    name: 'PageSelect',
-  };
-</script>
-
-<script setup>
-  import { getCurrentInstance, nextTick, onMounted, reactive, ref } from 'vue';
-  import { api } from '../data.service';
-  import { api as configApi } from '@/app/shop/admin/config/config.service.js';
-  import { useModal } from '@/sheep/hooks';
-  import TemplateSelect from '@/app/shop/admin/decorate/template/select.vue';
-  import GoodsSelect from '@/app/shop/admin/goods/goods/select.vue';
-  import CouponSelect from '@/app/shop/admin/coupon/select.vue';
-  import CategorySelect from '@/app/shop/admin/category/select.vue';
-  import ActivitySelect from '@/app/shop/admin/activity/activity/select.vue';
-  import RichtextSelect from '@/app/shop/admin/data/richtext/select.vue';
-  import ScoreShopSelect from '@/app/shop/admin/app/scoreShop/select.vue';
-
-  const emit = defineEmits(['modalCallBack']);
-
-  const { proxy } = getCurrentInstance();
-
-  const rightRef = {};
-  function setRightRef(el, item, index) {
-    rightRef[item.group + index] = el;
-  }
-
-  const tabsScroll = reactive({
-    data: [],
-    height: [],
-    currentIndex: 0,
-    selected: {},
-  });
-
-  async function getPageSelect() {
-    const { data } = await api.page.select();
-    tabsScroll.data = data;
-    nextTick(() => {
-      getHeight();
-    });
-  }
-
-  function rightScroll() {
-    for (let i = 0; i < tabsScroll.height.length; i++) {
-      let start = tabsScroll.height[i];
-      let end = tabsScroll.height[i + 1];
-      if (
-        proxy.$refs.rightScrollRef.scrollTop >= start &&
-        proxy.$refs.rightScrollRef.scrollTop < end
-      ) {
-        tabsScroll.currentIndex = i;
-        return;
-      }
-    }
-  }
-
-  function changeCurrentIndex(index) {
-    proxy.$refs.rightScrollRef.scrollTop = tabsScroll.height[index];
-    tabsScroll.currentIndex = index;
-  }
-
-  function selectLink(link) {
-    tabsScroll.selected = { ...link };
-
-    if (link.path == '/pages/index/page') {
-      // 自定义页面 id
-      useModal(
-        TemplateSelect,
-        {
-          title: '选择',
-        },
-        {
-          confirm: (res) => {
-            tabsScroll.selected.path = link.path + '?id=' + res.data.id.toString();
-          },
-        },
-      );
-    } else if (link.path == '/pages/goods/index') {
-      // 普通商品详情 id
-      useModal(
-        GoodsSelect,
-        {
-          title: '选择',
-        },
-        {
-          confirm: (res) => {
-            tabsScroll.selected.path = link.path + '?id=' + res.data.id.toString();
-          },
-        },
-      );
-    } else if (link.path == '/pages/coupon/detail') {
-      // 优惠券详情 id
-      useModal(
-        CouponSelect,
-        {
-          title: '选择',
-          status: 'normal',
-        },
-        {
-          confirm: (res) => {
-            tabsScroll.selected.path = link.path + '?id=' + res.data.id.toString();
-          },
-        },
-      );
-    } else if (link.path == '/pages/index/category') {
-      // 商品分类 id
-      useModal(
-        CategorySelect,
-        {
-          title: '选择',
-          from: 'page-category',
-        },
-        {
-          confirm: (res) => {
-            tabsScroll.selected.path =
-              link.path +
-              (res.data.list.length > 0 ? '?id=' + res.data.list[0]?.id.toString() : '');
-          },
-        },
-      );
-    } else if (link.path == '/pages/goods/list') {
-      // 商品列表 categoryId
-      useModal(
-        CategorySelect,
-        {
-          title: '选择',
-          from: 'page-goods',
-        },
-        {
-          confirm: (res) => {
-            tabsScroll.selected.path =
-              link.path +
-              (res.data.list.length > 0 ? '?categoryId=' + res.data.list[0]?.id.toString() : '');
-          },
-        },
-      );
-    } else if (
-      link.path == '/pages/activity/groupon/list' ||
-      link.path == '/pages/activity/seckill/list'
-    ) {
-      // 拼团列表/秒杀列表 id
-      let activityTypeData = {
-        groupon: 'groupon,groupon_ladder',
-        seckill: 'seckill',
-      };
-      useModal(
-        ActivitySelect,
-        {
-          title: '选择',
-          type: activityTypeData[link.path.split('/')[3]],
-        },
-        {
-          confirm: (res) => {
-            tabsScroll.selected.path = link.path + '?id=' + res.data.id.toString();
-          },
-        },
-      );
-    } else if (link.path == '/pages/public/richtext') {
-      // 富文本 id
-      useModal(
-        RichtextSelect,
-        {
-          title: '选择',
-        },
-        {
-          confirm: (res) => {
-            tabsScroll.selected.path = link.path + '?id=' + res.data.id.toString();
-          },
-        },
-      );
-    } else if (link.path == '/pages/goods/groupon' || link.path == '/pages/goods/seckill') {
-      // 拼团商品详情/秒杀商品详情 activity_id,id
-      let activityTypeData = {
-        groupon: 'groupon,groupon_ladder',
-        seckill: 'seckill',
-      };
-      useModal(
-        ActivitySelect,
-        {
-          title: '选择',
-          type: activityTypeData[link.path.split('/').pop()],
-        },
-        {
-          confirm: (res) => {
-            tabsScroll.selected.path = link.path + '?activity_id=' + res.data.id.toString();
-            useModal(
-              GoodsSelect,
-              {
-                title: '选择',
-                goods_ids: res.data.goods_ids,
-              },
-              {
-                confirm: (res) => {
-                  tabsScroll.selected.path =
-                    tabsScroll.selected.path + '&id=' + res.data.id.toString();
-                },
-              },
-            );
-          },
-        },
-      );
-    } else if (link.path == '/pages/goods/score') {
-      // 积分商城详情 id
-      useModal(
-        ScoreShopSelect,
-        {
-          title: '选择',
-        },
-        {
-          confirm: (res) => {
-            tabsScroll.selected.path = link.path + '?id=' + res.data.id.toString();
-          },
-        },
-      );
-    }
-  }
-
-  const platformUrl = ref({});
-  async function getPlatformUrl() {
-    const { data } = await configApi.getPlatformUrl();
-    platformUrl.value = data;
-  }
-
-  async function confirm() {
-    tabsScroll.selected.fullPath = {
-      url: `${
-        platformUrl.value.url.endsWith('/')
-          ? platformUrl.value.url.substr(0, platformUrl.value.url.length - 1)
-          : platformUrl.value.url
-      }${tabsScroll.selected.path}`,
-      appid: platformUrl.value.appid,
-      pagepath: tabsScroll.selected.path
-        ? '/pages/index/index?page=' + encodeURIComponent(tabsScroll.selected.path)
-        : '/pages/index/index',
-    };
-    emit('modalCallBack', { event: 'confirm', data: tabsScroll.selected });
-  }
-
-  function getHeight() {
-    tabsScroll.height = [];
-    let h = 0;
-    tabsScroll.height.push(h);
-    for (let e of proxy.$refs.rightScrollRef.getElementsByClassName('right-group')) {
-      h = h + e.offsetHeight;
-      tabsScroll.height.push(h);
-    }
-  }
-
-  onMounted(() => {
-    getPageSelect();
-    getPlatformUrl();
-  });
-</script>
-<style lang="scss" scoped>
-  .page-select {
-    .select-main {
-      display: flex;
-    }
-    .el-aside {
-      --el-aside-width: 140px;
-      border-right: 1px solid var(--sa-border);
-      padding: 20px;
-    }
-    .el-main {
-      --el-main-padding: 28px 20px 20px;
-    }
-    .group {
-      .name {
-        margin: 0 0 12px 12px;
-      }
-      .link {
-        .item {
-          padding: 0 16px;
-          height: 32px;
-          display: flex;
-          align-items: center;
-          justify-content: center;
-          border: 1px solid var(--sa-border);
-          border-radius: 4px;
-          margin: 0 12px 12px 0;
-          font-size: 14px;
-          font-weight: 400;
-          color: var(--sa-font);
-          cursor: pointer;
-          &:hover {
-            color: var(--el-color-primary);
-            background: var(--t-bg-hover);
-          }
-          &.item-active {
-            color: var(--el-color-primary);
-            background: var(--t-bg-active);
-          }
-        }
-      }
-    }
-    .left {
-      .group {
-        height: 32px;
-        line-height: 32px;
-        border-radius: 4px;
-        font-size: 14px;
-        font-weight: 400;
-        color: var(--sa-subtitle);
-        margin-bottom: 4px;
-        cursor: pointer;
-        &:hover {
-          color: var(--el-color-primary);
-          background: var(--t-bg-hover);
-        }
-        &.is-active {
-          color: var(--el-color-primary);
-          background: var(--t-bg-active);
-        }
-      }
-    }
-    .right {
-      height: 100%;
-      overflow: auto;
-      padding: 20px;
-      .name {
-        line-height: 16px;
-        font-size: 14px;
-        font-weight: 600;
-        color: var(--sa-subtitle);
-        margin: 0 0 12px 0;
-      }
-    }
-    .right-group {
-      padding-bottom: 12px;
-      .link {
-        border-bottom: 1px dashed var(--sa-border);
-      }
-    }
-  }
-</style>

+ 265 - 0
src/app/shop/admin/data/report/index.vue

@@ -0,0 +1,265 @@
+<template>
+  <el-container class="data-report-view panel-block">
+    <el-header class="sa-header">
+      <!-- 简化搜索组件 -->
+      <div class="search-container">
+        <sa-search-simple
+          :searchFields="searchFields"
+          :defaultValues="defaultSearchValues"
+          @search="(val) => getData(1, val)"
+          @reset="getData(1)"
+        >
+        </sa-search-simple>
+      </div>
+      <div class="sa-title sa-flex sa-row-between">
+        <div class="label sa-flex">数据报表</div>
+        <div>
+          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
+          <el-button icon="Download" type="primary" @click="exportData">导出</el-button>
+        </div>
+      </div>
+    </el-header>
+    <el-main class="sa-p-0">
+      <!-- 统计卡片 -->
+      <div class="stats-cards">
+        <el-row :gutter="20">
+          <el-col :span="6">
+            <el-card class="stats-card">
+              <div class="stats-content">
+                <div class="stats-value">{{ statsData.totalSales || 0 }}</div>
+                <div class="stats-label">总销售额(৳)</div>
+              </div>
+            </el-card>
+          </el-col>
+          <el-col :span="6">
+            <el-card class="stats-card">
+              <div class="stats-content">
+                <div class="stats-value">{{ statsData.totalOrders || 0 }}</div>
+                <div class="stats-label">总订单数</div>
+              </div>
+            </el-card>
+          </el-col>
+          <el-col :span="6">
+            <el-card class="stats-card">
+              <div class="stats-content">
+                <div class="stats-value">{{ statsData.totalUsers || 0 }}</div>
+                <div class="stats-label">总用户数</div>
+              </div>
+            </el-card>
+          </el-col>
+          <el-col :span="6">
+            <el-card class="stats-card">
+              <div class="stats-content">
+                <div class="stats-value">{{ statsData.totalGoods || 0 }}</div>
+                <div class="stats-label">总商品数</div>
+              </div>
+            </el-card>
+          </el-col>
+        </el-row>
+      </div>
+      
+      <!-- 数据表格 -->
+      <div class="sa-table-wrap panel-block panel-block--bottom" v-loading="loading">
+        <el-table
+          height="100%"
+          class="sa-table"
+          :data="table.data"
+          @selection-change="changeSelection"
+          @sort-change="fieldFilter"
+          row-key="id"
+          stripe
+        >
+          <template #empty>
+            <sa-empty />
+          </template>
+          <el-table-column type="selection" width="48" align="center"></el-table-column>
+          <el-table-column prop="date" label="日期" min-width="120" sortable="custom">
+          </el-table-column>
+          <el-table-column label="销售额" min-width="120">
+            <template #default="scope">
+              ৳{{ scope.row.sales_amount || 0 }}
+            </template>
+          </el-table-column>
+          <el-table-column label="订单数" min-width="100">
+            <template #default="scope">
+              {{ scope.row.order_count || 0 }}
+            </template>
+          </el-table-column>
+          <el-table-column label="新增用户" min-width="100">
+            <template #default="scope">
+              {{ scope.row.new_users || 0 }}
+            </template>
+          </el-table-column>
+          <el-table-column label="访问量" min-width="100">
+            <template #default="scope">
+              {{ scope.row.page_views || 0 }}
+            </template>
+          </el-table-column>
+          <el-table-column label="转化率" min-width="100">
+            <template #default="scope">
+              {{ scope.row.conversion_rate || 0 }}%
+            </template>
+          </el-table-column>
+          <el-table-column label="客单价" min-width="120">
+            <template #default="scope">
+              ৳{{ scope.row.avg_order_value || 0 }}
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </el-main>
+    <sa-view-bar>
+      <template #left>
+        <sa-batch-handle
+          :batchHandleTools="batchHandleTools"
+          :selectedLeng="table.selected.length"
+          @batchHandle="batchHandle"
+        ></sa-batch-handle>
+      </template>
+      <template #right>
+        <sa-pagination :pageData="pageData" @updateFn="getData" />
+      </template>
+    </sa-view-bar>
+  </el-container>
+</template>
+<script setup>
+  import { onMounted, reactive, ref } from 'vue';
+  import { api } from './report.service';
+  import { ElMessage } from 'element-plus';
+  import { usePagination } from '@/sheep/hooks';
+  const { pageData } = usePagination();
+
+  // 搜索字段配置
+  const searchFields = reactive({
+    date_range: {
+      type: 'daterange',
+      label: '日期范围',
+      placeholder: '请选择日期范围',
+      width: 300,
+    },
+  });
+  // 默认搜索值
+  const defaultSearchValues = reactive({
+    date_range: [],
+  });
+  
+  // 统计数据
+  const statsData = reactive({
+    totalSales: 0,
+    totalOrders: 0,
+    totalUsers: 0,
+    totalGoods: 0,
+  });
+  
+  // 列表
+  const table = reactive({
+    data: [],
+    order: '',
+    sort: '',
+    selected: [],
+  });
+  const loading = ref(true);
+  
+  // 获取统计数据
+  async function getStatsData() {
+    const { error, data } = await api.stats();
+    if (error === 0) {
+      Object.assign(statsData, data);
+    }
+  }
+  
+  // 获取
+  async function getData(page, searchParams = {}) {
+    if (page) pageData.page = page;
+    loading.value = true;
+    const { error, data } = await api.list({
+      page: pageData.page,
+      list_rows: pageData.list_rows,
+      order: table.order,
+      ...searchParams,
+      sort: table.sort,
+    });
+    console.log('API 响应:', error, data);
+    if (error === 0) {
+      table.data = data.data;
+      pageData.page = data.current_page;
+      pageData.list_rows = data.per_page;
+      pageData.total = data.total;
+    }
+    loading.value = false;
+  }
+  
+  // table 字段排序
+  function fieldFilter({ prop, order }) {
+    table.order = order == 'ascending' ? 'asc' : 'desc';
+    table.sort = prop;
+    getData();
+  }
+  
+  //table批量选择
+  function changeSelection(row) {
+    table.selected = row;
+  }
+  
+  // 导出数据
+  async function exportData() {
+    ElMessage.success('导出功能开发中...');
+  }
+  
+  // 分页/批量操作
+  const batchHandleTools = [
+    {
+      type: 'export',
+      label: '导出选中',
+      auth: 'shop.admin.data.report.export',
+      class: 'primary',
+    },
+  ];
+  
+  async function batchHandle(type) {
+    let ids = [];
+    table.selected.forEach((row) => {
+      ids.push(row.id);
+    });
+    switch (type) {
+      case 'export':
+        ElMessage.success('批量导出功能开发中...');
+        break;
+    }
+  }
+
+  onMounted(() => {
+    getStatsData();
+    getData();
+  });
+</script>
+<style lang="scss" scoped>
+  .data-report-view {
+    .el-header {
+      height: auto;
+    }
+    .el-main {
+      .stats-cards {
+        margin-bottom: 20px;
+        .stats-card {
+          .stats-content {
+            text-align: center;
+            .stats-value {
+              font-size: 24px;
+              font-weight: bold;
+              color: #409EFF;
+              margin-bottom: 8px;
+            }
+            .stats-label {
+              font-size: 14px;
+              color: #666;
+            }
+          }
+        }
+      }
+      .sa-table-wrap {
+        height: calc(100% - 140px);
+      }
+    }
+  }
+</style>

+ 27 - 0
src/app/shop/admin/data/report/report.service.js

@@ -0,0 +1,27 @@
+import { SELECT, CRUD } from '@/sheep/request/crud';
+
+const route = {
+  path: 'report',
+  name: 'shop.admin.data.report',
+  component: () => import('@/app/shop/admin/data/report/index.vue'),
+  meta: {
+    title: '数据报表',
+  },
+};
+
+const api = {
+  ...CRUD('shop/admin/data_report'),
+  select: (params) => SELECT('shop/admin/data_report', params),
+  stats: () => ({
+    error: 0,
+    msg: '获取成功',
+    data: {
+      totalSales: 125680.50,
+      totalOrders: 1256,
+      totalUsers: 3456,
+      totalGoods: 234,
+    }
+  }),
+};
+
+export { route, api };

+ 0 - 89
src/app/shop/admin/data/richtext/edit.vue

@@ -1,89 +0,0 @@
-<template>
-  <el-container>
-    <el-main>
-      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
-        <el-form-item label="标题" prop="title">
-          <el-input v-model="form.model.title" placeholder="请输入标题"></el-input>
-        </el-form-item>
-        <el-form-item label="内容" prop="content">
-          <div>
-            <sa-editor v-model:content="form.model.content"></sa-editor>
-          </div>
-        </el-form-item>
-      </el-form>
-    </el-main>
-    <el-footer class="sa-footer--submit">
-      <el-button
-        v-if="modal.params.type == 'add'"
-        v-auth="'shop.admin.data.richtext.add'"
-        type="primary"
-        @click="confirm"
-        >保存</el-button
-      >
-      <el-button
-        v-if="modal.params.type == 'edit'"
-        v-auth="'shop.admin.data.richtext.edit'"
-        type="primary"
-        @click="confirm"
-        >更新</el-button
-      >
-    </el-footer>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref, unref } from 'vue';
-  import SaEditor from '@/sheep/components/sa-editor/sa-editor.vue';
-  import { cloneDeep } from 'lodash';
-
-  import { api } from '../data.service';
-  const emit = defineEmits(['modalCallBack']);
-  const props = defineProps({
-    modal: {
-      type: Object,
-    },
-  });
-  // 添加 编辑 form
-  let formRef = ref(null);
-  const form = reactive({
-    model: {
-      title: '',
-      content: '',
-    },
-    rules: {
-      title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
-      content: [{ required: true, message: '请输入内容', trigger: 'blur' }],
-    },
-  });
-  const loading = ref(false);
-
-  // 获取详情
-  async function getDetail(id) {
-    loading.value = true;
-    const { error, data } = await api.richtext.detail(id);
-    error === 0 && (form.model = data);
-    loading.value = false;
-  }
-  // 表单关闭时提交
-  async function confirm() {
-    // 表单验证
-    unref(formRef).validate(async (valid) => {
-      if (!valid) return;
-      let submitForm = cloneDeep(form.model);
-      const { error } =
-        props.modal.params.type == 'add'
-          ? await api.richtext.add(submitForm)
-          : await api.richtext.edit(props.modal.params.id, submitForm);
-      if (error == 0) {
-        emit('modalCallBack', { event: 'confirm' });
-      }
-    });
-  }
-  async function init() {
-    if (props.modal.params.id) {
-      await getDetail(props.modal.params.id);
-    }
-  }
-  onMounted(() => {
-    init();
-  });
-</script>

+ 0 - 229
src/app/shop/admin/data/richtext/index.vue

@@ -1,229 +0,0 @@
-<template>
-  <el-container class="panel-block">
-    <el-header class="sa-header">
-      <div class="sa-title sa-flex sa-row-between">
-        <div class="label sa-flex">
-          <span class="left">富文本</span>
-          <search-condition
-            :conditionLabel="filterParams.conditionLabel"
-            @deleteFilter="deleteFilter"
-          ></search-condition>
-        </div>
-        <div>
-          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
-          <el-button class="sa-button-refresh" icon="Search" @click="openFilter"></el-button>
-          <el-button
-            v-auth="'shop.admin.data.richtext.add'"
-            icon="Plus"
-            type="primary"
-            @click="addRow"
-            >新建</el-button
-          >
-        </div>
-      </div>
-    </el-header>
-    <el-main class="sa-p-0" v-loading="loading">
-      <el-table
-        height="100%"
-        class="sa-table"
-        :data="table.data"
-        @selection-change="changeSelection"
-        @sort-change="fieldFilter"
-        @row-dblclick="editRow"
-        row-key="id"
-        stripe
-      >
-        <template #empty>
-          <sa-empty />
-        </template>
-        <el-table-column type="selection" width="48" align="center"></el-table-column>
-        <el-table-column prop="id" label="ID" min-width="100" sortable="custom"> </el-table-column>
-        <el-table-column label="标题" min-width="100">
-          <template #default="scope">
-            <span class="sa-table-line-1">
-              {{ scope.row.title || '-' }}
-            </span>
-          </template>
-        </el-table-column>
-        <el-table-column label="创建时间" min-width="160">
-          <template #default="scope">
-            {{ scope.row.create_time || '-' }}
-          </template>
-        </el-table-column>
-        <el-table-column label="更新时间" min-width="160">
-          <template #default="scope">
-            {{ scope.row.update_time || '-' }}
-          </template>
-        </el-table-column>
-        <el-table-column fixed="right" label="操作" min-width="120">
-          <template #default="scope">
-            <el-button
-              v-auth="'shop.admin.data.richtext.detail'"
-              type="primary"
-              class="is-link"
-              @click="editRow(scope.row)"
-              >编辑</el-button
-            >
-            <el-popconfirm
-              width="fit-content"
-              confirm-button-text="确认"
-              cancel-button-text="取消"
-              title="确认删除这条记录?"
-              @confirm="deleteApi(scope.row.id)"
-            >
-              <template #reference>
-                <el-button v-auth="'shop.admin.data.richtext.delete'" type="danger" class="is-link">
-                  删除
-                </el-button>
-              </template>
-            </el-popconfirm>
-          </template>
-        </el-table-column>
-      </el-table>
-    </el-main>
-    <sa-view-bar>
-      <template #left>
-        <sa-batch-handle
-          :batchHandleTools="batchHandleTools"
-          :selectedLeng="table.selected.length"
-          @batchHandle="batchHandle"
-        ></sa-batch-handle>
-      </template>
-      <template #right>
-        <sa-pagination :pageData="pageData" @updateFn="getData" />
-      </template>
-    </sa-view-bar>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from '../data.service';
-  import { ElMessageBox } from 'element-plus';
-  import { useModal, usePagination } from '@/sheep/hooks';
-  import { useSearch } from '@/sheep/components/sa-table/sa-search/useSearch';
-  import { composeFilter } from '@/sheep/utils';
-  import RichtextEdit from './edit.vue';
-  import { cloneDeep } from 'lodash';
-
-  const { pageData } = usePagination();
-  const filterParams = reactive({
-    tools: {
-      keyword: {
-        type: 'tinput',
-        field: 'keyword',
-        value: '',
-        label: '搜索内容',
-        placeholder: '请输入查询内容',
-      },
-    },
-    data: {
-      keyword: '',
-    },
-    conditionLabel: {},
-  });
-  const { openFilter, deleteFilter } = useSearch({ filterParams, getData });
-  // 列表
-  const table = reactive({
-    data: [],
-    order: '',
-    sort: '',
-    selected: [],
-  });
-  const loading = ref(true);
-  // 获取
-  async function getData(page) {
-    loading.value = true;
-    if (page) pageData.page = page;
-    let tempSearch = cloneDeep(filterParams.data);
-    let search = composeFilter(tempSearch);
-    const { error, data } = await api.richtext.list({
-      page: pageData.page,
-      list_rows: pageData.list_rows,
-      order: table.order,
-      sort: table.sort,
-      ...search,
-    });
-    if (error === 0) {
-      table.data = data.data;
-      pageData.page = data.current_page;
-      pageData.list_rows = data.per_page;
-      pageData.total = data.total;
-    }
-    loading.value = false;
-  }
-  // table 字段排序
-  function fieldFilter({ prop, order }) {
-    table.order = order == 'ascending' ? 'asc' : 'desc';
-    table.sort = prop;
-    getData();
-  }
-  //table批量选择
-  function changeSelection(row) {
-    table.selected = row;
-  }
-  const batchHandleTools = [
-    {
-      type: 'delete',
-      label: '删除',
-      auth: 'shop.admin.data.richtext.delete',
-      class: 'danger',
-    },
-  ];
-  function addRow() {
-    useModal(
-      RichtextEdit,
-      { title: '新建', type: 'add' },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-  function editRow(row) {
-    useModal(
-      RichtextEdit,
-      {
-        title: '编辑',
-        type: 'edit',
-        id: row.id,
-      },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-  // 删除api 单独批量可以直接调用
-  async function deleteApi(id) {
-    await api.richtext.delete(id);
-    getData();
-  }
-  async function batchHandle(type) {
-    let ids = [];
-    table.selected.forEach((row) => {
-      ids.push(row.id);
-    });
-    switch (type) {
-      case 'delete':
-        ElMessageBox.confirm('此操作将删除, 是否继续?', '提示', {
-          confirmButtonText: '确定',
-          cancelButtonText: '取消',
-          type: 'warning',
-        }).then(() => {
-          deleteApi(ids.join(','));
-        });
-        break;
-      default:
-        await api.richtext.edit(ids.join(','), {
-          status: type,
-        });
-        getData();
-    }
-  }
-
-  onMounted(() => {
-    getData();
-  });
-</script>

+ 0 - 136
src/app/shop/admin/data/richtext/select.vue

@@ -1,136 +0,0 @@
-<template>
-  <el-container class="richtext-select panel-block">
-    <el-main>
-      <div class="sa-flex sa-row-between sa-m-b-24">
-        <div class="sa-flex">
-          <el-input placeholder="请输入查询内容" v-model="searchtext" clearable>
-            <template #append>
-              <el-button @click="openFilter">搜索</el-button>
-            </template>
-          </el-input>
-        </div>
-        <el-button v-auth="'shop.data.richtext.add'" icon="Plus" type="primary" @click="addRow"
-          >添加</el-button
-        >
-      </div>
-      <template v-if="table.data.length > 0">
-        <el-table class="sa-table sa-m-b-16" :data="table.data" row-key="id" stripe>
-          <el-table-column prop="id" label="ID" min-width="100"></el-table-column>
-          <el-table-column label="标题" min-width="100">
-            <template #default="scope">
-              <span class="sa-table-line-1">{{ scope.row.title || '-' }}</span>
-            </template>
-          </el-table-column>
-          <el-table-column label="创建时间" min-width="172">
-            <template #default="scope">
-              {{ scope.row.create_time || '-' }}
-            </template>
-          </el-table-column>
-          <el-table-column label="更新时间" min-width="172">
-            <template #default="scope">
-              {{ scope.row.update_time || '-' }}
-            </template>
-          </el-table-column>
-          <el-table-column fixed="right" label="操作" min-width="100">
-            <template #default="scope">
-              <el-button class="is-link" type="primary" @click="modalCallBack(scope.row)"
-                >选择</el-button
-              >
-            </template>
-          </el-table-column>
-        </el-table>
-        <div class="sa-flex sa-row-right">
-          <sa-pagination :pageData="pageData" @updateFn="getData" />
-        </div>
-      </template>
-      <template v-if="table.data.length == 0 && !loading">
-        <sa-empty></sa-empty>
-      </template>
-    </el-main>
-  </el-container>
-</template>
-
-<script>
-  export default {
-    name: 'RichtextSelect',
-  };
-</script>
-
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from '../data.service';
-  import { useModal } from '@/sheep/hooks';
-  import { usePagination } from '@/sheep/hooks';
-  import RichtextEdit from './edit.vue';
-  const props = defineProps(['modal']);
-  const emit = defineEmits(['modalCallBack']);
-  const { pageData } = usePagination();
-  // 列表
-  const table = reactive({
-    data: [],
-    selected: {},
-  });
-  const loading = ref(true);
-  const searchtext = ref('');
-  async function openFilter() {
-    let search = {
-      keyword: [searchtext.value],
-    };
-    const { data } = await api.richtext.select({
-      page: pageData.page,
-      list_rows: pageData.list_rows,
-      search,
-    });
-
-    table.data = data.data;
-    pageData.page = data.current_page;
-    pageData.list_rows = data.per_page;
-    pageData.total = data.total;
-  }
-  // 获取
-  async function getData() {
-    loading.value = true;
-    const { data } = await api.richtext.select({
-      page: pageData.page,
-      list_rows: pageData.list_rows,
-    });
-    table.data = data.data;
-    pageData.page = data.current_page;
-    pageData.list_rows = data.per_page;
-    pageData.total = data.total;
-    loading.value = false;
-  }
-  function addRow() {
-    useModal(
-      RichtextEdit,
-      { title: '新建', type: 'add' },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-  function modalCallBack(e) {
-    emit('modalCallBack', { event: 'confirm', data: e });
-  }
-
-  onMounted(() => {
-    getData();
-  });
-</script>
-<style lang="scss" scoped>
-  .richtext-select {
-    .el-main {
-      :deep() {
-        .el-table__header {
-          .el-table-column--selection {
-            .el-checkbox {
-              display: none;
-            }
-          }
-        }
-      }
-    }
-  }
-</style>

+ 0 - 33
src/app/shop/admin/dispatch/dispatch.service.js

@@ -1,33 +0,0 @@
-import { SELECT, CRUD } from '@/sheep/request/crud';
-import { request } from '@/sheep/request';
-
-const route = {
-  path: 'dispatch',
-  name: 'shop.admin.dispatch',
-  component: () => import('./index.vue'),
-  meta: {
-    title: '配送',
-  },
-};
-
-const api = {
-  ...CRUD('shop/admin/dispatch/dispatch', ['list', 'add', 'edit']),
-  detail: (id, params) =>
-    request({
-      url: `shop/admin/dispatch/dispatch/${id}`,
-      method: 'GET',
-      params,
-    }),
-  delete: (id, params) =>
-    request({
-      url: `shop/admin/dispatch/dispatch/${id}`,
-      method: 'DELETE',
-      options: {
-        showSuccessMessage: true,
-      },
-      params
-    }),
-  select: (params) => SELECT('shop/admin/dispatch/dispatch', params),
-};
-
-export { route, api };

+ 0 - 347
src/app/shop/admin/dispatch/edit.vue

@@ -1,347 +0,0 @@
-<template>
-  <el-container>
-    <el-main>
-      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
-        <el-form-item label="模板名称" prop="name">
-          <el-input v-model="form.model.name" placeholder="请输入模板名称"></el-input>
-        </el-form-item>
-        <template v-if="form.model.type == 'express'">
-          <el-form-item label="计价方式">
-            <el-radio-group v-model="priceType">
-              <el-radio label="number">按件数</el-radio>
-              <el-radio label="weight">按重量</el-radio>
-            </el-radio-group>
-          </el-form-item>
-          <div class="template-wrap">
-            <div class="title sa-flex">
-              <div class="area">可配送区域</div>
-              <div class="num" v-if="priceType == 'number'">首件</div>
-              <div class="num" v-if="priceType == 'weight'">首重(kg)</div>
-              <div class="num">运费(元)</div>
-              <div class="num" v-if="priceType == 'number'">续件</div>
-              <div class="num" v-if="priceType == 'weight'">续重(kg)</div>
-              <div class="num">续费(元)</div>
-              <div class="num">操作</div>
-            </div>
-            <sa-draggable v-model="form.model.express" :animation="300" handle=".sortable-drag" item-key="element">
-              <template #item="{ element, index }">
-                <div class="item">
-                  <el-form-item class="area" :prop="'express.' + index + '.district_text'"
-                    :rules="templateRules.district_text">
-                    <el-button @click="selectArea(index)" v-if="!element.district_text" type="primary">选择地址</el-button>
-                    <div class="sa-flex sa-row-between area-edit" v-if="element.district_text">
-                      <div>{{ element.district_text }}</div>
-                      <el-button class="is-link sa-m-l-4" type="primary" @click="selectArea(index)">选择</el-button>
-                    </div>
-                  </el-form-item>
-                  <el-form-item class="num" :prop="'express.' + index + '.first_num'" :rules="templateRules.first_num"
-                    v-if="priceType == 'number'">
-                    <el-input placeholder="请输入首件" type="number" min="0" v-model="element.first_num">
-                    </el-input>
-                  </el-form-item>
-                  <el-form-item class="num" :prop="'express.' + index + '.first_num'" :rules="templateRules.first_num"
-                    v-if="priceType == 'weight'">
-                    <el-input placeholder="请输入首重" type="number" min="0" v-model="element.first_num">
-                    </el-input>
-                  </el-form-item>
-                  <el-form-item class="num" :prop="'express.' + index + '.first_price'"
-                    :rules="templateRules.first_price">
-                    <el-input placeholder="请输入运费" type="number" min="0" :step="0.01" v-model="element.first_price">
-                    </el-input>
-                  </el-form-item>
-                  <el-form-item class="num" :prop="'express.' + index + '.additional_num'"
-                    :rules="templateRules.additional_num" v-if="priceType == 'number'">
-                    <el-input placeholder="请输入续件" type="number" min="0" v-model="element.additional_num">
-                    </el-input>
-                  </el-form-item>
-                  <el-form-item class="num" :prop="'express.' + index + '.additional_num'"
-                    :rules="templateRules.additional_num" v-if="priceType == 'weight'">
-                    <el-input placeholder="请输入续重" type="number" min="0" v-model="element.additional_num">
-                    </el-input>
-                  </el-form-item>
-                  <el-form-item class="num" :prop="'express.' + index + '.additional_price'"
-                    :rules="templateRules.additional_price">
-                    <el-input placeholder="请输入续费" type="number" min="0" :step="0.01" v-model="element.additional_price">
-                    </el-input>
-                  </el-form-item>
-                  <el-form-item class="num">
-                    <el-popconfirm width="fit-content" confirm-button-text="确认" cancel-button-text="取消" title="确认删除这条记录?"
-                      @confirm="deleteTemplate(index)">
-                      <template #reference>
-                        <el-button class="is-link" type="danger" plain @click.stop>删除</el-button>
-                      </template>
-                    </el-popconfirm>
-                    <sa-svg class="sa-m-l-8 sortable-drag" name="sa-round"></sa-svg>
-                  </el-form-item>
-                </div>
-              </template>
-            </sa-draggable>
-            <el-button @click="addTemplate()" class="sa-m-l-16" type="primary" plain icon="Plus">添加</el-button>
-          </div>
-        </template>
-        <template v-if="form.model.type == 'autosend'">
-          <el-form-item label="发货类型:">
-            <el-radio-group v-model="form.model.autosend.type" @change="onChangeAutosendType">
-              <el-radio label="text">固定内容</el-radio>
-              <el-radio label="params">自定义内容</el-radio>
-            </el-radio-group>
-          </el-form-item>
-          <el-form-item v-if="form.model.autosend.type == 'text'" label="发货内容:">
-            <el-input v-model="form.model.autosend.content" placeholder="请输入自动发货内容"></el-input>
-          </el-form-item>
-          <el-form-item v-if="form.model.autosend.type == 'params'" label="发货内容:">
-            <div class="sa-template-wrap">
-              <div class="title sa-flex">
-                <div class="key">参数名称</div>
-                <div class="key">内容</div>
-                <div class="oper">操作</div>
-              </div>
-              <sa-draggable v-model="form.model.autosend.content" :animation="300" handle=".sortable-drag"
-                item-key="element">
-                <template #item="{ element, index }">
-                  <div class="item">
-                    <el-form-item class="key">
-                      <el-input placeholder="请输入" v-model="element.title">
-                      </el-input>
-                    </el-form-item>
-                    <el-form-item class="key">
-                      <el-input placeholder="请输入" v-model="element.content">
-                      </el-input>
-                    </el-form-item>
-                    <el-form-item class="oper">
-                      <el-popconfirm width="fit-content" confirm-button-text="确认" cancel-button-text="取消"
-                        title="确认删除这条记录?" @confirm="onDeleteContent(index)">
-                        <template #reference>
-                          <el-button type="danger" link @click.stop>删除
-                          </el-button>
-                        </template>
-                      </el-popconfirm>
-                      <i class="iconfont iconmove sortable-drag"></i>
-                    </el-form-item>
-                  </div>
-                </template>
-              </sa-draggable>
-              <el-button class="add-params" type="primary" plain icon="Plus" @click="onAddContent">添加
-              </el-button>
-            </div>
-          </el-form-item>
-        </template>
-      </el-form>
-    </el-main>
-    <el-footer class="sa-footer--submit">
-      <el-button v-if="modal.params.type == 'add' || modal.params.type == 'copy'"
-        v-auth="'shop.admin.dispatch.dispatch.add'" type="primary" @click="confirm">保存</el-button>
-      <el-button v-if="modal.params.type == 'edit'" v-auth="'shop.admin.dispatch.dispatch.edit'" type="primary"
-        @click="confirm">更新</el-button>
-    </el-footer>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref, unref } from 'vue';
-  import { api } from './dispatch.service';
-  import { useModal } from '@/sheep/hooks';
-  import SaDraggable from 'vuedraggable';
-  import AreaSelect from '../data/area/select.vue';
-  import { cloneDeep } from 'lodash';
-
-  const emit = defineEmits(['modalCallBack']);
-  const props = defineProps({
-    modal: {
-      type: Object,
-    },
-  });
-
-  // 添加 编辑 form
-  let formRef = ref(null);
-  const form = reactive({
-    model: {
-      name: '',
-      type: props.modal.params.dispatchType,
-      express: [],
-      autosend: {
-        type: "text",
-        content: ""
-      }
-    },
-    rules: {
-      name: [{ required: true, message: '请输入模板名称', trigger: 'blur' }],
-    },
-  });
-  const loading = ref(false);
-
-  const priceType = ref('number');
-
-  //Draggable
-  const templateRules = {
-    first_num: [{ required: true, message: '请输入数量', trigger: 'blur' }],
-    first_price: [{ required: true, message: '请输入运费', trigger: 'blur' }],
-    additional_num: [{ required: true, message: '请输入数量', trigger: 'blur' }],
-    additional_price: [{ required: true, message: '请输入续费', trigger: 'blur' }],
-    district_text: [{ required: true, message: '请选择可配送区域', trigger: 'blur' }],
-  };
-  function addTemplate() {
-    form.model.express.push({
-      type: priceType.value,
-      first_num: 0,
-      first_price: 0,
-      additional_num: 0,
-      additional_price: 0,
-      province_ids: '',
-      city_ids: '',
-      district_ids: '',
-    });
-  }
-  function deleteTemplate(index) {
-    form.model.express.splice(index, 1);
-  }
-
-  //选择区域
-  function selectArea(index) {
-    useModal(
-      AreaSelect,
-      {
-        title: '区域选择',
-        selected: {
-          province: form.model.express[index].province_ids,
-          city: form.model.express[index].city_ids,
-          district: form.model.express[index].district_ids,
-        },
-      },
-      {
-        confirm: (res) => {
-          let text = [];
-          for (var key in res.data) {
-            let ids = [];
-            for (var id in res.data[key]) {
-              ids.push(id);
-              text.push(res.data[key][id]);
-            }
-            form.model.express[index][key + '_ids'] = ids.join(',');
-          }
-          form.model.express[index].district_text = text.join(',');
-        },
-      },
-    );
-  }
-
-  function onChangeAutosendType(type) {
-    form.model.autosend.content = type == 'text' ? '' : []
-  }
-  function onAddContent() {
-    if (!form.model.autosend.content) {
-      form.model.autosend.content = []
-    }
-    form.model.autosend.content.push({
-      title: '',
-      content: '',
-    });
-  }
-  function onDeleteContent(index) {
-    form.model.autosend.content.splice(index, 1);
-  }
-
-  // 获取详情
-  async function getDetail(id) {
-    loading.value = true;
-    const { error, data } = await api.detail(id, {
-      type: props.modal.params.dispatchType,
-    });
-    if (error === 0) {
-      form.model = data;
-      if (props.modal.params.dispatchType == 'express') {
-        priceType.value = form.model.express.length > 0 ? form.model.express[0].type : 'number';
-      }
-    }
-    loading.value = false;
-  }
-
-  // 表单关闭时提交
-  async function confirm() {
-    // 表单验证
-    unref(formRef).validate(async (valid) => {
-      if (!valid) return;
-
-      let submitForm = cloneDeep(form.model);
-
-      if (props.modal.params.dispatchType == 'express') {
-
-        submitForm.express.forEach((ex) => {
-          ex.type = priceType.value;
-        });
-
-        if (props.modal.params.type == 'copy') {
-          delete submitForm.id;
-          submitForm.express.forEach((express) => {
-            delete express.id;
-          });
-        }
-      }else if(props.modal.params.dispatchType == 'autosend'){
-        if (props.modal.params.type == 'copy') {
-          delete submitForm.id;
-        }
-      }
-
-      const { error } =
-        props.modal.params.type == 'add' || props.modal.params.type == 'copy'
-          ? await api.add(submitForm)
-          : await api.edit(props.modal.params.id, submitForm);
-      if (error == 0) {
-        emit('modalCallBack', { event: 'confirm' });
-      }
-    });
-  }
-
-  onMounted(() => {
-    if (props.modal.params.id) {
-      getDetail(props.modal.params.id);
-    }
-  });
-</script>
-<style lang="scss" scoped>
-  .template-wrap {
-    width: 100%;
-    min-width: 490px;
-    color: var(--sa-font);
-    .title {
-      width: inherit;
-      background: var(--sa-table-header-bg);
-      height: 40px;
-      margin: 0 0 12px;
-      font-size: 12px;
-    }
-    .area {
-      width: 320px;
-      padding-left: 16px;
-      font-weight: bold;
-    }
-    .num {
-      width: 140px;
-      padding-left: 16px;
-      font-weight: bold;
-    }
-    .item {
-      display: flex;
-      .area {
-        width: 320px;
-        padding-left: 16px;
-        .area-edit {
-          width: 100%;
-        }
-      }
-      .num {
-        width: 140px;
-        padding-left: 10px;
-        :deep() {
-          .el-form-item__content {
-            height: 32px;
-          }
-        }
-      }
-    }
-  }
-  :deep() {
-    .el-form-item__content {
-      margin-left: 0 !important;
-    }
-  }
-</style>

+ 0 - 274
src/app/shop/admin/dispatch/index.vue

@@ -1,274 +0,0 @@
-<template>
-  <el-container class="dispatch-view panel-block">
-    <el-header class="sa-header">
-      <el-tabs class="sa-tabs" v-model="dispatchType" @tab-change="onChangeTab">
-        <el-tab-pane label="物流快递" name="express"></el-tab-pane>
-        <el-tab-pane label="自动发货" name="autosend"></el-tab-pane>
-      </el-tabs>
-      <div class="sa-title sa-flex sa-row-between">
-        <div class="label sa-flex">发货模板</div>
-        <div>
-          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
-          <el-button v-auth="'shop.admin.dispatch.dispatch.add'" icon="Plus" type="primary" @click="onAdd">添加</el-button>
-        </div>
-      </div>
-    </el-header>
-    <el-main class="sa-p-0" v-loading="loading">
-      <div v-if="dispatchType == 'express'">
-        <sa-empty v-if="collapse.data.length == 0" />
-        <template v-else>
-          <div v-for="c in collapse.data" :key="c" class="collapse sa-m-b-16">
-            <div class="sa-flex sa-row-between collapse-list-header sa-m-b-2">
-              <div class="sa-flex name">
-                <div class="sa-m-r-8">#{{ c.id }}</div>
-                <div>{{ c.name }}</div>
-              </div>
-              <div class="sa-flex collapse-list-box">
-                <div class="time">最后编辑时间:{{ c.update_time }}</div>
-                <el-button v-auth="'shop.admin.dispatch.dispatch.detail'" type="primary" class="is-link"
-                  @click.stop="onEdit(c.id, 'edit')">编辑</el-button>
-                <!-- TODO:  功能待讨论 -->
-                <el-button v-auth="'shop.admin.dispatch.dispatch.detail'" type="info" class="is-link copy-btn"
-                  @click.stop="onEdit(c.id, 'copy')">复制</el-button>
-                <el-popconfirm width="fit-content" confirm-button-text="确认" cancel-button-text="取消" title="确认删除这条记录?"
-                  @confirm="onDelete(c.id)">
-                  <template #reference>
-                    <el-button v-auth="'shop.admin.dispatch.dispatch.delete'" type="danger" class="is-link" @click.stop>
-                      删除
-                    </el-button>
-                  </template>
-                </el-popconfirm>
-              </div>
-            </div>
-
-            <div v-for="(e, index) in c.express" :key="e">
-              <div class="sa-flex sa-row-between collapse-item-header" v-if="index == 0">
-                <div class="area sa-m-r-60">可配送区域</div>
-                <div class="sa-flex">
-                  <div class="box">
-                    {{ e.type == 'weight' ? '首重(kg)' : '首件' }}
-                  </div>
-                  <div class="box">运费(元)</div>
-                  <div class="box">
-                    {{ e.type == 'weight' ? '续重(kg)' : '续件' }}
-                  </div>
-                  <div class="box">续费(元)</div>
-                </div>
-              </div>
-              <div class="sa-flex sa-row-between district">
-                <!-- TODO:省市区接口 列表展示 -->
-                <div class="area sa-m-r-60">{{ e.district_text || '-' }}</div>
-                <div class="sa-flex">
-                  <div class="box">{{ e.first_num }}</div>
-                  <div class="box">{{ e.first_price }}</div>
-                  <div class="box">{{ e.additional_num }}</div>
-                  <div class="box">{{ e.additional_price }}</div>
-                </div>
-              </div>
-            </div>
-          </div>
-        </template>
-      </div>
-      <el-table v-if="dispatchType == 'autosend'" class="sa-table" :data="collapse.data">
-        <el-table-column prop="id" label="ID" min-width="90">
-        </el-table-column>
-        <el-table-column label="模板名称" min-width="140">
-          <template #default="scope">
-            <div class="sa-line-1">
-              {{ scope.row.name }}
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column label="自动发货内容" min-width="400">
-          <template #default="scope">
-            <div>
-              <div v-if="scope.row.autosend?.type == 'text'">
-                {{ scope.row.autosend?.content }}</div>
-              <div v-else>
-                <template v-for="(item, index) in scope.row.autosend?.content">
-                  {{ item.title }}:{{ item.content }}
-                  <span v-if="index != scope.row.autosend?.content.length - 1" class="sa-m-r-4">;</span>
-                </template>
-              </div>
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column prop="update_time" label="最后编辑时间" min-width="172">
-        </el-table-column>
-        <el-table-column label="操作" min-width="180">
-          <template #default="scope">
-            <el-button v-auth="'shop.admin.dispatch.dispatch.detail'" type="primary" class="is-link"
-              @click="onEdit(scope.row.id, 'edit')">编辑</el-button>
-
-            <el-button v-auth="'shop.admin.dispatch.dispatch.detail'" type="info" link
-              @click="onEdit(scope.row.id, 'copy')">复制</el-button>
-
-            <el-popconfirm width="fit-content" confirm-button-text="确认" cancel-button-text="取消" title="确认删除这条记录?"
-              @confirm="onDelete(scope.row.id)">
-              <template #reference>
-                <el-button v-auth="'shop.admin.dispatch.dispatch.edit'" type="danger" link> 删除</el-button>
-              </template>
-            </el-popconfirm>
-          </template>
-        </el-table-column>
-      </el-table>
-    </el-main>
-    <sa-view-bar>
-      <template #right>
-        <sa-pagination :pageData="pageData" @updateFn="getData" />
-      </template>
-    </sa-view-bar>
-  </el-container>
-</template>
-<script>
-  export default {
-    name: 'shop.admin.dispatch.dispatch',
-  };
-</script>
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from './dispatch.service';
-  import { useModal } from '@/sheep/hooks';
-  import { usePagination } from '@/sheep/hooks';
-  import DispatchEdit from './edit.vue';
-  const { pageData } = usePagination();
-  const dispatchType = ref('express');
-  const collapse = reactive({
-    data: [],
-    order: '',
-    sort: '',
-  });
-  const loading = ref(true);
-  // 获取数据
-  async function getData() {
-    loading.value = true;
-    const { error, data } = await api.list({
-      order: collapse.order,
-      sort: collapse.sort,
-      page: pageData.page,
-      list_rows: pageData.list_rows,
-      type: dispatchType.value
-    });
-    if (error === 0) {
-      collapse.data = data.data;
-      pageData.page = data.current_page;
-      pageData.list_rows = data.per_page;
-      pageData.total = data.total;
-    }
-    loading.value = false;
-  }
-
-  function onChangeTab() {
-    pageData.page = 1
-    getData()
-  }
-
-  function onAdd() {
-    useModal(
-      DispatchEdit,
-      { title: '添加模板', type: 'add', dispatchType: dispatchType.value, },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-  function onEdit(id, type) {
-    useModal(
-      DispatchEdit,
-      {
-        title: type == 'edit' ? '编辑' : '复制',
-        type: type,
-        id,
-        dispatchType: dispatchType.value,
-      },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-  async function onDelete(id) {
-    await api.delete(id, {
-      type: dispatchType.value
-    });
-    getData();
-  }
-  onMounted(() => {
-    getData();
-  });
-</script>
-<style lang="scss" scoped>
-  .collapse {
-    min-width: 100%;
-    display: inline-block;
-    border-radius: 4px;
-    border: 1px solid var(--sa-space);
-  }
-
-  .collapse-list-header {
-    font-size: 12px;
-    line-height: 14px;
-    color: var(--sa-subtitle);
-    height: 40px;
-    padding: 0 16px 0 24px;
-    flex: 1 0 90%;
-    order: 1;
-    border-radius: 4px;
-    background: var(--t-bg-disabled);
-    .name {
-      min-width: 748px;
-      font-size: 14px;
-      font-weight: 600;
-      color: var(--sa-font);
-    }
-    .time {
-      font-size: 14px;
-      width: 240px;
-      font-weight: 500;
-      color: var(--sa-font);
-    }
-  }
-  .collapse-item-header {
-    padding: 10px 0 10px 16px;
-    font-size: 14px;
-    color: var(--sa-font);
-    height: 40px;
-    border-bottom: 1px solid var(--sa-space);
-    box-shadow: 0 2px 6px rgb(140 140 140 / 12%);
-    .area {
-      min-width: 748px;
-    }
-    .box {
-      min-width: 88px;
-      padding: 0 18px;
-      text-align: center;
-    }
-  }
-  .district {
-    min-height: 56px;
-    padding: 10px 0 10px 16px;
-    font-size: 14px;
-    color: var(--sa-font);
-    border-bottom: 1px solid var(--sa-space);
-    .area {
-      min-width: 808px;
-      font-size: 12px;
-    }
-    .box {
-      min-width: 88px;
-      padding: 0 18px;
-      text-align: center;
-    }
-  }
-  .copy-btn {
-    --el-button-text-color: var(--sa-font);
-  }
-  :deep() {
-    .el-button > span {
-      font-weight: 500;
-    }
-  }
-</style>

+ 0 - 16
src/app/shop/admin/feedback/feedback.service.js

@@ -1,16 +0,0 @@
-import { CRUD } from '@/sheep/request/crud';
-
-const route = {
-  path: 'feedback',
-  name: 'shop.admin.feedback',
-  component: () => import('./index.vue'),
-  meta: {
-    title: '意见反馈',
-  },
-};
-
-const api = {
-  ...CRUD('shop/admin/feedback'),
-};
-
-export { route, api };

+ 0 - 283
src/app/shop/admin/feedback/index.vue

@@ -1,283 +0,0 @@
-<template>
-  <el-container class="panel-block">
-    <el-header class="sa-header">
-      <div class="sa-title sa-flex sa-row-between">
-        <div class="label sa-flex">
-          <span class="left">意见反馈</span>
-          <search-condition
-            :conditionLabel="filterParams.conditionLabel"
-            @deleteFilter="deleteFilter"
-          ></search-condition>
-        </div>
-        <div>
-          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
-          <el-button class="sa-button-refresh" icon="Search" @click="openFilter"></el-button>
-        </div>
-      </div>
-    </el-header>
-    <el-main class="sa-p-0" v-loading="loading">
-      <el-table
-        height="100%"
-        class="sa-table"
-        :data="table.data"
-        @selection-change="changeSelection"
-        @sort-change="fieldFilter"
-        @row-dblclick="editRow"
-        row-key="id"
-        stripe
-      >
-        <template #empty>
-          <sa-empty />
-        </template>
-        <el-table-column type="selection" width="48" align="center"> </el-table-column>
-        <el-table-column prop="id" label="ID" min-width="100" sortable="custom"> </el-table-column>
-        <el-table-column label="反馈用户" min-width="180">
-          <template #default="scope">
-            <sa-user-profile :user="scope.row.user" :id="scope.row.user_id" />
-          </template>
-        </el-table-column>
-        <el-table-column label="反馈类型" min-width="140">
-          <template #default="scope">
-            <div class="sa-table-line-1">
-              {{ scope.row.type || '-' }}
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column prop="content" label="反馈内容" min-width="260">
-          <template #default="scope">
-            <div class="sa-table-line-1">
-              {{ scope.row.content || '-' }}
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column label="截图" min-width="146" align="center">
-          <template #default="scope">
-            <div class="sa-flex">
-              <div>
-                <sa-preview :url="scope.row.images" size="30"></sa-preview>
-              </div>
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column prop="phone" label="联系电话" min-width="160">
-          <template #default="scope">
-            <div class="sa-table-line-1">
-              {{ scope.row.phone || '-' }}
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column prop="status" label="处理状态" min-width="100">
-          <template #default="scope">
-            <el-tag :type="scope.row.status == '1' ? 'success' : 'info'">
-              {{ scope.row.status_text }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="remark" label="系统备注" min-width="260">
-          <template #default="scope">
-            <div class="sa-table-line-1">
-              {{ scope.row.remark || '-' }}
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column label="创建时间" min-width="180">
-          <template #default="scope">
-            {{ scope.row.create_time || '-' }}
-          </template>
-        </el-table-column>
-        <el-table-column label="更新时间" min-width="180">
-          <template #default="scope">
-            {{ scope.row.update_time || '-' }}
-          </template>
-        </el-table-column>
-        <el-table-column fixed="right" label="操作" min-width="120">
-          <template #default="scope">
-            <el-button
-              v-auth="'shop.admin.feedback.detail'"
-              type="primary"
-              class="is-link"
-              @click="editRow(scope.row)"
-              >查看</el-button
-            >
-            <el-popconfirm
-              width="fit-content"
-              confirm-button-text="确认"
-              cancel-button-text="取消"
-              title="确认删除这条记录?"
-              @confirm="deleteApi(scope.row.id)"
-            >
-              <template #reference>
-                <el-button v-auth="'shop.admin.feedback.delete'" type="danger" class="is-link">
-                  删除
-                </el-button>
-              </template>
-            </el-popconfirm>
-          </template>
-        </el-table-column>
-      </el-table>
-    </el-main>
-    <sa-view-bar>
-      <template #left>
-        <sa-batch-handle
-          :batchHandleTools="batchHandleTools"
-          :selectedLeng="table.selected.length"
-          @batchHandle="batchHandle"
-        ></sa-batch-handle>
-      </template>
-      <template #right>
-        <sa-pagination :pageData="pageData" @updateFn="getData" />
-      </template>
-    </sa-view-bar>
-  </el-container>
-</template>
-<script>
-  export default {
-    name: 'shop.admin.feedback',
-  };
-</script>
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from './feedback.service';
-  import { ElMessageBox } from 'element-plus';
-  import { useModal, usePagination } from '@/sheep/hooks';
-  import { useSearch } from '@/sheep/components/sa-table/sa-search/useSearch';
-  import { composeFilter } from '@/sheep/utils';
-  import FeedbackEdit from './edit.vue';
-  import { cloneDeep } from 'lodash';
-
-  const filterParams = reactive({
-    tools: {
-      keyword: {
-        type: 'tinput',
-        field: 'keyword',
-        value: '',
-        label: '搜索内容',
-        placeholder: '请输入查询内容',
-      },
-      phone: {
-        type: 'tinput',
-        field: 'phone',
-        value: '',
-        label: '联系电话',
-        placeholder: '请输入联系电话',
-      },
-    },
-    data: {
-      keyword: '',
-      phone: '',
-    },
-    conditionLabel: {},
-  });
-  const { openFilter, deleteFilter } = useSearch({ filterParams, getData });
-  // 列表
-  const table = reactive({
-    data: [],
-    order: '',
-    sort: '',
-    selected: [],
-  });
-  const { pageData } = usePagination();
-  const batchHandleTools = [
-    {
-      type: 'delete',
-      label: '删除',
-      auth: 'shop.admin.feedback.delete',
-      class: 'danger',
-    },
-    {
-      type: '1',
-      label: '已处理',
-      auth: 'shop.admin.feedback.edit',
-      class: 'success',
-    },
-    {
-      type: '0',
-      label: '未处理',
-      auth: 'shop.admin.feedback.edit',
-      class: 'info',
-    },
-  ];
-  const loading = ref(true);
-  // 获取
-  async function getData(page) {
-    loading.value = true;
-    if (page) pageData.page = page;
-    let tempSearch = cloneDeep(filterParams.data);
-    let search = composeFilter(tempSearch, {
-      keyword: 'like',
-      phone: 'like',
-    });
-    const { error, data } = await api.list({
-      page: pageData.page,
-      list_rows: pageData.list_rows,
-      order: table.order,
-      sort: table.sort,
-      ...search,
-    });
-    if (error === 0) {
-      table.data = data.data;
-      pageData.page = data.current_page;
-      pageData.list_rows = data.per_page;
-      pageData.total = data.total;
-    }
-    loading.value = false;
-  }
-
-  // table 字段排序
-  function fieldFilter({ prop, order }) {
-    table.order = order == 'ascending' ? 'asc' : 'desc';
-    table.sort = prop;
-    getData();
-  }
-  //table批量选择
-  function changeSelection(row) {
-    table.selected = row;
-  }
-
-  function editRow(row) {
-    useModal(
-      FeedbackEdit,
-      {
-        title: '查看',
-        type: 'edit',
-        id: row.id,
-      },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-  // 删除api 单独批量可以直接调用
-  async function deleteApi(id) {
-    await api.delete(id);
-    getData();
-  }
-  async function batchHandle(type) {
-    let ids = [];
-    table.selected.forEach((row) => {
-      ids.push(row.id);
-    });
-    switch (type) {
-      case 'delete':
-        ElMessageBox.confirm('此操作将删除, 是否继续?', '提示', {
-          confirmButtonText: '确定',
-          cancelButtonText: '取消',
-          type: 'warning',
-        }).then(() => {
-          deleteApi(ids.join(','));
-        });
-        break;
-      default:
-        await api.edit(ids.join(','), {
-          status: type,
-        });
-        getData();
-    }
-  }
-
-  onMounted(() => {
-    getData();
-  });
-</script>

+ 27 - 0
src/app/shop/admin/finance/commission/commission.service.js

@@ -0,0 +1,27 @@
+import { SELECT, CRUD } from '@/sheep/request/crud';
+
+const route = {
+  path: 'commission',
+  name: 'shop.admin.finance.commission',
+  component: () => import('@/app/shop/admin/finance/commission/index.vue'),
+  meta: {
+    title: '佣金',
+  },
+};
+
+const api = {
+  ...CRUD('shop/admin/commission'),
+  select: (params) => SELECT('shop/admin/commission', params),
+  settle: (id) => ({
+    error: 0,
+    msg: '结算成功',
+    data: null,
+  }),
+  batchSettle: (ids) => ({
+    error: 0,
+    msg: '批量结算成功',
+    data: null,
+  }),
+};
+
+export { route, api };

+ 142 - 0
src/app/shop/admin/finance/commission/edit.vue

@@ -0,0 +1,142 @@
+<template>
+  <el-container>
+    <el-main>
+      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
+        <el-form-item label="用户ID" prop="user_id" v-if="!isView">
+          <el-input v-model="form.model.user_id" placeholder="请填写用户ID"></el-input>
+        </el-form-item>
+        <el-form-item label="用户信息" v-if="isView">
+          <div>
+            <p><strong>用户名:</strong>{{ form.model.username || '-' }}</p>
+            <p><strong>手机号:</strong>{{ form.model.mobile || '-' }}</p>
+          </div>
+        </el-form-item>
+        <el-form-item label="佣金金额" prop="amount">
+          <el-input-number 
+            v-model="form.model.amount" 
+            :min="0" 
+            :precision="2"
+            placeholder="佣金金额"
+            :disabled="isView"
+          />
+          <span style="margin-left: 10px;">৳</span>
+        </el-form-item>
+        <el-form-item label="佣金类型" prop="type">
+          <el-select v-model="form.model.type" placeholder="请选择佣金类型" :disabled="isView">
+            <el-option label="订单佣金" value="order"></el-option>
+            <el-option label="推荐佣金" value="referral"></el-option>
+            <el-option label="团队佣金" value="team"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="来源订单" prop="source_order_no">
+          <el-input v-model="form.model.source_order_no" placeholder="请填写来源订单号" :disabled="isView"></el-input>
+        </el-form-item>
+        <el-form-item label="佣金比例" prop="commission_rate">
+          <el-input-number 
+            v-model="form.model.commission_rate" 
+            :min="0" 
+            :max="100"
+            :precision="2"
+            placeholder="佣金比例"
+            :disabled="isView"
+          />
+          <span style="margin-left: 10px;">%</span>
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="form.model.status" placeholder="请选择状态" :disabled="isView">
+            <el-option label="待结算" value="pending"></el-option>
+            <el-option label="已结算" value="settled"></el-option>
+            <el-option label="已冻结" value="frozen"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="获得时间" v-if="isView">
+          <span>{{ form.model.create_time || '-' }}</span>
+        </el-form-item>
+        <el-form-item label="结算时间" v-if="isView && form.model.settle_time">
+          <span>{{ form.model.settle_time || '-' }}</span>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input 
+            v-model="form.model.remark" 
+            type="textarea" 
+            :rows="3"
+            placeholder="请填写备注"
+            :disabled="isView"
+          ></el-input>
+        </el-form-item>
+      </el-form>
+    </el-main>
+    <el-footer class="sa-footer--submit" v-if="!isView">
+      <el-button v-if="modal.params.type == 'add'" type="primary" @click="confirm">保存</el-button>
+      <el-button v-if="modal.params.type == 'edit'" type="primary" @click="confirm">更新</el-button>
+    </el-footer>
+  </el-container>
+</template>
+<script setup>
+  import { cloneDeep } from 'lodash';
+  import { onMounted, reactive, ref, unref, computed } from 'vue';
+  import { api } from './commission.service';
+  const emit = defineEmits(['modalCallBack']);
+  const props = defineProps({
+    modal: {
+      type: Object,
+    },
+  });
+  
+  const isView = computed(() => props.modal.params.type === 'view');
+  
+  // 添加 编辑 form
+  let formRef = ref(null);
+  const form = reactive({
+    model: {
+      user_id: '',
+      username: '',
+      mobile: '',
+      amount: 0,
+      type: 'order',
+      source_order_no: '',
+      commission_rate: 0,
+      status: 'pending',
+      create_time: '',
+      settle_time: '',
+      remark: '',
+    },
+    rules: {
+      user_id: [{ required: true, message: '请填写用户ID', trigger: 'blur' }],
+      amount: [{ required: true, message: '请填写佣金金额', trigger: 'blur' }],
+      type: [{ required: true, message: '请选择佣金类型', trigger: 'change' }],
+      commission_rate: [{ required: true, message: '请填写佣金比例', trigger: 'blur' }],
+      status: [{ required: true, message: '请选择状态', trigger: 'change' }],
+    },
+  });
+  const loading = ref(false);
+  // 获取详情
+  async function getDetail(id) {
+    loading.value = true;
+    const { error, data } = await api.detail(id);
+    error === 0 && (form.model = data);
+    loading.value = false;
+  }
+  // 表单关闭时提交
+  async function confirm() {
+    unref(formRef).validate(async (valid) => {
+      if (!valid) return;
+      let submitForm = cloneDeep(form.model);
+      const { error } =
+        props.modal.params.type == 'add'
+          ? await api.add(submitForm)
+          : await api.edit(props.modal.params.id, submitForm);
+      if (error == 0) {
+        emit('modalCallBack', { event: 'confirm' });
+      }
+    });
+  }
+  async function init() {
+    if (props.modal.params.id) {
+      await getDetail(props.modal.params.id);
+    }
+  }
+  onMounted(() => {
+    init();
+  });
+</script>

+ 289 - 0
src/app/shop/admin/finance/commission/index.vue

@@ -0,0 +1,289 @@
+<template>
+  <el-container class="commission-view panel-block">
+    <el-header class="sa-header">
+      <!-- 简化搜索组件 -->
+      <div class="search-container">
+        <sa-search-simple
+          :searchFields="searchFields"
+          :defaultValues="defaultSearchValues"
+          @search="(val) => getData(1, val)"
+          @reset="getData(1)"
+        >
+        </sa-search-simple>
+      </div>
+      <div class="sa-title sa-flex sa-row-between">
+        <div class="label sa-flex">佣金管理</div>
+        <div>
+          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
+          <el-button icon="Plus" type="primary" @click="addRow">新建</el-button>
+        </div>
+      </div>
+    </el-header>
+    <el-main class="sa-p-0">
+      <div class="sa-table-wrap panel-block panel-block--bottom" v-loading="loading">
+        <el-table
+          height="100%"
+          class="sa-table"
+          :data="table.data"
+          @selection-change="changeSelection"
+          @sort-change="fieldFilter"
+          @row-dblclick="editRow"
+          row-key="id"
+          stripe
+        >
+          <template #empty>
+            <sa-empty />
+          </template>
+          <el-table-column type="selection" width="48" align="center"></el-table-column>
+          <el-table-column prop="id" label="ID" min-width="100" sortable="custom">
+          </el-table-column>
+          <el-table-column label="用户" min-width="120">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                {{ scope.row.username || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="佣金金额" min-width="120">
+            <template #default="scope">
+              ৳{{ scope.row.amount || 0 }}
+            </template>
+          </el-table-column>
+          <el-table-column label="佣金类型" min-width="120">
+            <template #default="scope">
+              <el-tag :type="scope.row.type === 'order' ? 'primary' : 'success'">
+                {{ scope.row.type_text || '-' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="来源订单" min-width="150">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                {{ scope.row.source_order_no || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="佣金比例" min-width="100">
+            <template #default="scope">
+              {{ scope.row.commission_rate || 0 }}%
+            </template>
+          </el-table-column>
+          <el-table-column label="状态" min-width="100">
+            <template #default="scope">
+              <el-tag :type="getStatusType(scope.row.status)">
+                {{ scope.row.status_text || '-' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="获得时间" min-width="160">
+            <template #default="scope">
+              {{ scope.row.create_time || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="结算时间" min-width="160">
+            <template #default="scope">
+              {{ scope.row.settle_time || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column fixed="right" label="操作" min-width="120">
+            <template #default="scope">
+              <el-button class="is-link" type="primary" @click="editRow(scope.row)">查看</el-button>
+              <el-button 
+                v-if="scope.row.status === 'pending'"
+                class="is-link" 
+                type="success" 
+                @click="settleCommission(scope.row)"
+              >
+                结算
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </el-main>
+    <sa-view-bar>
+      <template #left>
+        <sa-batch-handle
+          :batchHandleTools="batchHandleTools"
+          :selectedLeng="table.selected.length"
+          @batchHandle="batchHandle"
+        ></sa-batch-handle>
+      </template>
+      <template #right>
+        <sa-pagination :pageData="pageData" @updateFn="getData" />
+      </template>
+    </sa-view-bar>
+  </el-container>
+</template>
+<script setup>
+  import { onMounted, reactive, ref } from 'vue';
+  import { api } from './commission.service';
+  import { ElMessageBox, ElMessage } from 'element-plus';
+  import { useModal } from '@/sheep/hooks';
+  import { usePagination } from '@/sheep/hooks';
+  import commissionEdit from './edit.vue';
+  const { pageData } = usePagination();
+
+  // 搜索字段配置
+  const searchFields = reactive({
+    username: {
+      type: 'input',
+      label: '用户名',
+      placeholder: '请输入用户名',
+      width: 200,
+    },
+    type: {
+      type: 'select',
+      label: '佣金类型',
+      placeholder: '请选择类型',
+      width: 150,
+      options: [
+        { label: '订单佣金', value: 'order' },
+        { label: '推荐佣金', value: 'referral' },
+        { label: '团队佣金', value: 'team' },
+      ],
+    },
+  });
+  // 默认搜索值
+  const defaultSearchValues = reactive({
+    username: '',
+    type: '',
+  });
+  // 列表
+  const table = reactive({
+    data: [],
+    order: '',
+    sort: '',
+    selected: [],
+  });
+  const loading = ref(true);
+  
+  // 获取状态类型
+  function getStatusType(status) {
+    const statusMap = {
+      pending: 'warning',
+      settled: 'success',
+      frozen: 'danger',
+    };
+    return statusMap[status] || 'info';
+  }
+  
+  // 获取
+  async function getData(page, searchParams = {}) {
+    if (page) pageData.page = page;
+    loading.value = true;
+    const { error, data } = await api.list({
+      page: pageData.page,
+      list_rows: pageData.list_rows,
+      order: table.order,
+      ...searchParams,
+      sort: table.sort,
+    });
+    console.log('API 响应:', error, data);
+    if (error === 0) {
+      table.data = data.data;
+      pageData.page = data.current_page;
+      pageData.list_rows = data.per_page;
+      pageData.total = data.total;
+    }
+    loading.value = false;
+  }
+  // table 字段排序
+  function fieldFilter({ prop, order }) {
+    table.order = order == 'ascending' ? 'asc' : 'desc';
+    table.sort = prop;
+    getData();
+  }
+  //table批量选择
+  function changeSelection(row) {
+    table.selected = row;
+  }
+  // 分页/批量操作
+  const batchHandleTools = [
+    {
+      type: 'settle',
+      label: '批量结算',
+      auth: 'shop.admin.finance.commission.settle',
+      class: 'success',
+    },
+  ];
+  function addRow() {
+    useModal(
+      commissionEdit,
+      { title: '新建佣金', type: 'add' },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+  function editRow(row) {
+    useModal(
+      commissionEdit,
+      {
+        title: '查看佣金',
+        type: 'view',
+        id: row.id,
+      },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+  
+  // 结算佣金
+  async function settleCommission(row) {
+    ElMessageBox.confirm('确认结算该笔佣金?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+    }).then(async () => {
+      const { error } = await api.settle(row.id);
+      if (error === 0) {
+        ElMessage.success('结算成功');
+        getData();
+      }
+    });
+  }
+  
+  async function batchHandle(type) {
+    let ids = [];
+    table.selected.forEach((row) => {
+      ids.push(row.id);
+    });
+    switch (type) {
+      case 'settle':
+        ElMessageBox.confirm('确认结算选中的佣金?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }).then(async () => {
+          const { error } = await api.batchSettle(ids.join(','));
+          if (error === 0) {
+            ElMessage.success('批量结算成功');
+            getData();
+          }
+        });
+        break;
+    }
+  }
+
+  onMounted(() => {
+    getData();
+  });
+</script>
+<style lang="scss" scoped>
+  .commission-view {
+    .el-header {
+      height: auto;
+    }
+    .el-main {
+      .sa-table-wrap {
+        height: 100%;
+      }
+    }
+  }
+</style>

+ 115 - 0
src/app/shop/admin/finance/recharge/edit.vue

@@ -0,0 +1,115 @@
+<template>
+  <el-container>
+    <el-main>
+      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
+        <el-form-item label="用户ID" prop="user_id">
+          <el-input v-model="form.model.user_id" placeholder="请填写用户ID" :disabled="isView"></el-input>
+        </el-form-item>
+        <el-form-item label="充值金额" prop="amount">
+          <el-input-number 
+            v-model="form.model.amount" 
+            :min="0" 
+            :precision="2"
+            placeholder="充值金额"
+            :disabled="isView"
+          />
+          <span style="margin-left: 10px;">৳</span>
+        </el-form-item>
+        <el-form-item label="支付方式" prop="payment_method">
+          <el-select v-model="form.model.payment_method" placeholder="请选择支付方式" :disabled="isView">
+            <el-option label="支付宝" value="alipay"></el-option>
+            <el-option label="微信支付" value="wechat"></el-option>
+            <el-option label="银行卡" value="bank"></el-option>
+            <el-option label="余额" value="balance"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="订单号" prop="order_no">
+          <el-input v-model="form.model.order_no" placeholder="请填写订单号" :disabled="isView"></el-input>
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="form.model.status" placeholder="请选择状态" :disabled="isView">
+            <el-option label="待处理" value="pending"></el-option>
+            <el-option label="已完成" value="completed"></el-option>
+            <el-option label="已失败" value="failed"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input 
+            v-model="form.model.remark" 
+            type="textarea" 
+            :rows="3"
+            placeholder="请填写备注"
+            :disabled="isView"
+          ></el-input>
+        </el-form-item>
+      </el-form>
+    </el-main>
+    <el-footer class="sa-footer--submit" v-if="!isView">
+      <el-button v-if="modal.params.type == 'add'" type="primary" @click="confirm">保存</el-button>
+      <el-button v-if="modal.params.type == 'edit'" type="primary" @click="confirm">更新</el-button>
+    </el-footer>
+  </el-container>
+</template>
+<script setup>
+  import { cloneDeep } from 'lodash';
+  import { onMounted, reactive, ref, unref, computed } from 'vue';
+  import { api } from './recharge.service';
+  const emit = defineEmits(['modalCallBack']);
+  const props = defineProps({
+    modal: {
+      type: Object,
+    },
+  });
+  
+  const isView = computed(() => props.modal.params.type === 'view');
+  
+  // 添加 编辑 form
+  let formRef = ref(null);
+  const form = reactive({
+    model: {
+      user_id: '',
+      amount: 0,
+      payment_method: 'alipay',
+      order_no: '',
+      status: 'pending',
+      remark: '',
+    },
+    rules: {
+      user_id: [{ required: true, message: '请填写用户ID', trigger: 'blur' }],
+      amount: [{ required: true, message: '请填写充值金额', trigger: 'blur' }],
+      payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }],
+      order_no: [{ required: true, message: '请填写订单号', trigger: 'blur' }],
+      status: [{ required: true, message: '请选择状态', trigger: 'change' }],
+    },
+  });
+  const loading = ref(false);
+  // 获取详情
+  async function getDetail(id) {
+    loading.value = true;
+    const { error, data } = await api.detail(id);
+    error === 0 && (form.model = data);
+    loading.value = false;
+  }
+  // 表单关闭时提交
+  async function confirm() {
+    unref(formRef).validate(async (valid) => {
+      if (!valid) return;
+      let submitForm = cloneDeep(form.model);
+      const { error } =
+        props.modal.params.type == 'add'
+          ? await api.add(submitForm)
+          : await api.edit(props.modal.params.id, submitForm);
+      if (error == 0) {
+        emit('modalCallBack', { event: 'confirm' });
+      }
+    });
+  }
+  async function init() {
+    if (props.modal.params.id) {
+      await getDetail(props.modal.params.id);
+    }
+  }
+  onMounted(() => {
+    init();
+  });
+</script>

+ 284 - 0
src/app/shop/admin/finance/recharge/index.vue

@@ -0,0 +1,284 @@
+<template>
+  <el-container class="recharge-view panel-block">
+    <el-header class="sa-header">
+      <!-- 简化搜索组件 -->
+      <div class="search-container">
+        <sa-search-simple
+          :searchFields="searchFields"
+          :defaultValues="defaultSearchValues"
+          @search="(val) => getData(1, val)"
+          @reset="getData(1)"
+        >
+        </sa-search-simple>
+      </div>
+      <div class="sa-title sa-flex sa-row-between">
+        <div class="label sa-flex">充值管理</div>
+        <div>
+          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
+          <el-button icon="Plus" type="primary" @click="addRow">新建</el-button>
+        </div>
+      </div>
+    </el-header>
+    <el-main class="sa-p-0">
+      <div class="sa-table-wrap panel-block panel-block--bottom" v-loading="loading">
+        <el-table
+          height="100%"
+          class="sa-table"
+          :data="table.data"
+          @selection-change="changeSelection"
+          @sort-change="fieldFilter"
+          @row-dblclick="editRow"
+          row-key="id"
+          stripe
+        >
+          <template #empty>
+            <sa-empty />
+          </template>
+          <el-table-column type="selection" width="48" align="center"></el-table-column>
+          <el-table-column prop="id" label="ID" min-width="100" sortable="custom">
+          </el-table-column>
+          <el-table-column label="用户" min-width="120">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                {{ scope.row.username || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="充值金额" min-width="120">
+            <template #default="scope">
+              ৳{{ scope.row.amount || 0 }}
+            </template>
+          </el-table-column>
+          <el-table-column label="支付方式" min-width="120">
+            <template #default="scope">
+              <el-tag :type="scope.row.payment_method === 'alipay' ? 'primary' : 'success'">
+                {{ scope.row.payment_method_text || '-' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="订单号" min-width="180">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                {{ scope.row.order_no || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="状态" min-width="100">
+            <template #default="scope">
+              <el-tag :type="getStatusType(scope.row.status)">
+                {{ scope.row.status_text || '-' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="充值时间" min-width="160">
+            <template #default="scope">
+              {{ scope.row.create_time || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="完成时间" min-width="160">
+            <template #default="scope">
+              {{ scope.row.finish_time || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column fixed="right" label="操作" min-width="120">
+            <template #default="scope">
+              <el-button class="is-link" type="primary" @click="editRow(scope.row)">查看</el-button>
+              <el-button 
+                v-if="scope.row.status === 'pending'"
+                class="is-link" 
+                type="success" 
+                @click="confirmRecharge(scope.row)"
+              >
+                确认
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </el-main>
+    <sa-view-bar>
+      <template #left>
+        <sa-batch-handle
+          :batchHandleTools="batchHandleTools"
+          :selectedLeng="table.selected.length"
+          @batchHandle="batchHandle"
+        ></sa-batch-handle>
+      </template>
+      <template #right>
+        <sa-pagination :pageData="pageData" @updateFn="getData" />
+      </template>
+    </sa-view-bar>
+  </el-container>
+</template>
+<script setup>
+  import { onMounted, reactive, ref } from 'vue';
+  import { api } from './recharge.service';
+  import { ElMessageBox, ElMessage } from 'element-plus';
+  import { useModal } from '@/sheep/hooks';
+  import { usePagination } from '@/sheep/hooks';
+  import rechargeEdit from './edit.vue';
+  const { pageData } = usePagination();
+
+  // 搜索字段配置
+  const searchFields = reactive({
+    username: {
+      type: 'input',
+      label: '用户名',
+      placeholder: '请输入用户名',
+      width: 200,
+    },
+    status: {
+      type: 'select',
+      label: '状态',
+      placeholder: '请选择状态',
+      width: 150,
+      options: [
+        { label: '待处理', value: 'pending' },
+        { label: '已完成', value: 'completed' },
+        { label: '已失败', value: 'failed' },
+      ],
+    },
+  });
+  // 默认搜索值
+  const defaultSearchValues = reactive({
+    username: '',
+    status: '',
+  });
+  // 列表
+  const table = reactive({
+    data: [],
+    order: '',
+    sort: '',
+    selected: [],
+  });
+  const loading = ref(true);
+  
+  // 获取状态类型
+  function getStatusType(status) {
+    const statusMap = {
+      pending: 'warning',
+      completed: 'success',
+      failed: 'danger',
+    };
+    return statusMap[status] || 'info';
+  }
+  
+  // 获取
+  async function getData(page, searchParams = {}) {
+    if (page) pageData.page = page;
+    loading.value = true;
+    const { error, data } = await api.list({
+      page: pageData.page,
+      list_rows: pageData.list_rows,
+      order: table.order,
+      ...searchParams,
+      sort: table.sort,
+    });
+    console.log('API 响应:', error, data);
+    if (error === 0) {
+      table.data = data.data;
+      pageData.page = data.current_page;
+      pageData.list_rows = data.per_page;
+      pageData.total = data.total;
+    }
+    loading.value = false;
+  }
+  // table 字段排序
+  function fieldFilter({ prop, order }) {
+    table.order = order == 'ascending' ? 'asc' : 'desc';
+    table.sort = prop;
+    getData();
+  }
+  //table批量选择
+  function changeSelection(row) {
+    table.selected = row;
+  }
+  // 分页/批量操作
+  const batchHandleTools = [
+    {
+      type: 'confirm',
+      label: '批量确认',
+      auth: 'shop.admin.finance.recharge.confirm',
+      class: 'success',
+    },
+  ];
+  function addRow() {
+    useModal(
+      rechargeEdit,
+      { title: '新建充值', type: 'add' },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+  function editRow(row) {
+    useModal(
+      rechargeEdit,
+      {
+        title: '查看充值',
+        type: 'view',
+        id: row.id,
+      },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+  
+  // 确认充值
+  async function confirmRecharge(row) {
+    ElMessageBox.confirm('确认该笔充值记录?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+    }).then(async () => {
+      const { error } = await api.confirm(row.id);
+      if (error === 0) {
+        ElMessage.success('确认成功');
+        getData();
+      }
+    });
+  }
+  
+  async function batchHandle(type) {
+    let ids = [];
+    table.selected.forEach((row) => {
+      ids.push(row.id);
+    });
+    switch (type) {
+      case 'confirm':
+        ElMessageBox.confirm('确认选中的充值记录?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }).then(async () => {
+          const { error } = await api.batchConfirm(ids.join(','));
+          if (error === 0) {
+            ElMessage.success('批量确认成功');
+            getData();
+          }
+        });
+        break;
+    }
+  }
+
+  onMounted(() => {
+    getData();
+  });
+</script>
+<style lang="scss" scoped>
+  .recharge-view {
+    .el-header {
+      height: auto;
+    }
+    .el-main {
+      .sa-table-wrap {
+        height: 100%;
+      }
+    }
+  }
+</style>

+ 27 - 0
src/app/shop/admin/finance/recharge/recharge.service.js

@@ -0,0 +1,27 @@
+import { SELECT, CRUD } from '@/sheep/request/crud';
+
+const route = {
+  path: 'recharge',
+  name: 'shop.admin.finance.recharge',
+  component: () => import('@/app/shop/admin/finance/recharge/index.vue'),
+  meta: {
+    title: '充值',
+  },
+};
+
+const api = {
+  ...CRUD('shop/admin/recharge'),
+  select: (params) => SELECT('shop/admin/recharge', params),
+  confirm: (id) => ({
+    error: 0,
+    msg: '确认成功',
+    data: null,
+  }),
+  batchConfirm: (ids) => ({
+    error: 0,
+    msg: '批量确认成功',
+    data: null,
+  }),
+};
+
+export { route, api };

+ 126 - 0
src/app/shop/admin/finance/withdraw/edit.vue

@@ -0,0 +1,126 @@
+<template>
+  <el-container>
+    <el-main>
+      <el-form :model="form.model" ref="formRef" label-width="100px">
+        <el-form-item label="用户信息">
+          <div>
+            <p><strong>用户名:</strong>{{ form.model.username || '-' }}</p>
+            <p><strong>手机号:</strong>{{ form.model.mobile || '-' }}</p>
+            <p><strong>邮箱:</strong>{{ form.model.email || '-' }}</p>
+          </div>
+        </el-form-item>
+        <el-form-item label="提款金额">
+          <span>৳{{ form.model.amount || 0 }}</span>
+        </el-form-item>
+        <el-form-item label="手续费">
+          <span>৳{{ form.model.fee || 0 }}</span>
+        </el-form-item>
+        <el-form-item label="实际到账">
+          <span>৳{{ form.model.actual_amount || 0 }}</span>
+        </el-form-item>
+        <el-form-item label="提款方式">
+          <el-tag :type="form.model.withdraw_method === 'bank' ? 'primary' : 'success'">
+            {{ form.model.withdraw_method_text || '-' }}
+          </el-tag>
+        </el-form-item>
+        <el-form-item label="收款信息">
+          <div v-if="form.model.withdraw_method === 'bank'">
+            <p><strong>银行名称:</strong>{{ form.model.bank_name || '-' }}</p>
+            <p><strong>银行卡号:</strong>{{ form.model.bank_account || '-' }}</p>
+            <p><strong>开户姓名:</strong>{{ form.model.account_name || '-' }}</p>
+          </div>
+          <div v-else-if="form.model.withdraw_method === 'alipay'">
+            <p><strong>支付宝账号:</strong>{{ form.model.alipay_account || '-' }}</p>
+            <p><strong>真实姓名:</strong>{{ form.model.real_name || '-' }}</p>
+          </div>
+          <div v-else>
+            <p>{{ form.model.account_info || '-' }}</p>
+          </div>
+        </el-form-item>
+        <el-form-item label="申请时间">
+          <span>{{ form.model.create_time || '-' }}</span>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-tag :type="getStatusType(form.model.status)">
+            {{ form.model.status_text || '-' }}
+          </el-tag>
+        </el-form-item>
+        <el-form-item label="处理时间" v-if="form.model.process_time">
+          <span>{{ form.model.process_time || '-' }}</span>
+        </el-form-item>
+        <el-form-item label="处理备注" v-if="form.model.process_remark">
+          <span>{{ form.model.process_remark || '-' }}</span>
+        </el-form-item>
+        <el-form-item label="用户备注" v-if="form.model.remark">
+          <span>{{ form.model.remark || '-' }}</span>
+        </el-form-item>
+      </el-form>
+    </el-main>
+  </el-container>
+</template>
+<script setup>
+  import { onMounted, reactive, ref } from 'vue';
+  import { api } from './withdraw.service';
+  const props = defineProps({
+    modal: {
+      type: Object,
+    },
+  });
+  
+  // 表单数据
+  const form = reactive({
+    model: {
+      username: '',
+      mobile: '',
+      email: '',
+      amount: 0,
+      fee: 0,
+      actual_amount: 0,
+      withdraw_method: '',
+      withdraw_method_text: '',
+      bank_name: '',
+      bank_account: '',
+      account_name: '',
+      alipay_account: '',
+      real_name: '',
+      account_info: '',
+      create_time: '',
+      status: '',
+      status_text: '',
+      process_time: '',
+      process_remark: '',
+      remark: '',
+    },
+  });
+  
+  const loading = ref(false);
+  
+  // 获取状态类型
+  function getStatusType(status) {
+    const statusMap = {
+      pending: 'warning',
+      approved: 'primary',
+      rejected: 'danger',
+      completed: 'success',
+    };
+    return statusMap[status] || 'info';
+  }
+  
+  // 获取详情
+  async function getDetail(id) {
+    loading.value = true;
+    const { error, data } = await api.detail(id);
+    error === 0 && (form.model = data);
+    loading.value = false;
+  }
+  
+  async function init() {
+    if (props.modal.params.id) {
+      await getDetail(props.modal.params.id);
+    }
+  }
+  
+  onMounted(() => {
+    init();
+  });
+</script>

+ 317 - 0
src/app/shop/admin/finance/withdraw/index.vue

@@ -0,0 +1,317 @@
+<template>
+  <el-container class="withdraw-view panel-block">
+    <el-header class="sa-header">
+      <!-- 简化搜索组件 -->
+      <div class="search-container">
+        <sa-search-simple
+          :searchFields="searchFields"
+          :defaultValues="defaultSearchValues"
+          @search="(val) => getData(1, val)"
+          @reset="getData(1)"
+        >
+        </sa-search-simple>
+      </div>
+      <div class="sa-title sa-flex sa-row-between">
+        <div class="label sa-flex">提款管理</div>
+        <div>
+          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
+        </div>
+      </div>
+    </el-header>
+    <el-main class="sa-p-0">
+      <div class="sa-table-wrap panel-block panel-block--bottom" v-loading="loading">
+        <el-table
+          height="100%"
+          class="sa-table"
+          :data="table.data"
+          @selection-change="changeSelection"
+          @sort-change="fieldFilter"
+          @row-dblclick="editRow"
+          row-key="id"
+          stripe
+        >
+          <template #empty>
+            <sa-empty />
+          </template>
+          <el-table-column type="selection" width="48" align="center"></el-table-column>
+          <el-table-column prop="id" label="ID" min-width="100" sortable="custom">
+          </el-table-column>
+          <el-table-column label="用户" min-width="120">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                {{ scope.row.username || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="提款金额" min-width="120">
+            <template #default="scope">
+              ৳{{ scope.row.amount || 0 }}
+            </template>
+          </el-table-column>
+          <el-table-column label="手续费" min-width="100">
+            <template #default="scope">
+              ৳{{ scope.row.fee || 0 }}
+            </template>
+          </el-table-column>
+          <el-table-column label="实际到账" min-width="120">
+            <template #default="scope">
+              ৳{{ scope.row.actual_amount || 0 }}
+            </template>
+          </el-table-column>
+          <el-table-column label="提款方式" min-width="120">
+            <template #default="scope">
+              <el-tag :type="scope.row.withdraw_method === 'bank' ? 'primary' : 'success'">
+                {{ scope.row.withdraw_method_text || '-' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="状态" min-width="100">
+            <template #default="scope">
+              <el-tag :type="getStatusType(scope.row.status)">
+                {{ scope.row.status_text || '-' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="申请时间" min-width="160">
+            <template #default="scope">
+              {{ scope.row.create_time || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column fixed="right" label="操作" min-width="150">
+            <template #default="scope">
+              <el-button class="is-link" type="primary" @click="editRow(scope.row)">查看</el-button>
+              <el-button 
+                v-if="scope.row.status === 'pending'"
+                class="is-link" 
+                type="success" 
+                @click="approveWithdraw(scope.row)"
+              >
+                通过
+              </el-button>
+              <el-button 
+                v-if="scope.row.status === 'pending'"
+                class="is-link" 
+                type="danger" 
+                @click="rejectWithdraw(scope.row)"
+              >
+                拒绝
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </el-main>
+    <sa-view-bar>
+      <template #left>
+        <sa-batch-handle
+          :batchHandleTools="batchHandleTools"
+          :selectedLeng="table.selected.length"
+          @batchHandle="batchHandle"
+        ></sa-batch-handle>
+      </template>
+      <template #right>
+        <sa-pagination :pageData="pageData" @updateFn="getData" />
+      </template>
+    </sa-view-bar>
+  </el-container>
+</template>
+<script setup>
+  import { onMounted, reactive, ref } from 'vue';
+  import { api } from './withdraw.service';
+  import { ElMessageBox, ElMessage } from 'element-plus';
+  import { useModal } from '@/sheep/hooks';
+  import { usePagination } from '@/sheep/hooks';
+  import withdrawEdit from './edit.vue';
+  const { pageData } = usePagination();
+
+  // 搜索字段配置
+  const searchFields = reactive({
+    username: {
+      type: 'input',
+      label: '用户名',
+      placeholder: '请输入用户名',
+      width: 200,
+    },
+    status: {
+      type: 'select',
+      label: '状态',
+      placeholder: '请选择状态',
+      width: 150,
+      options: [
+        { label: '待审核', value: 'pending' },
+        { label: '已通过', value: 'approved' },
+        { label: '已拒绝', value: 'rejected' },
+        { label: '已完成', value: 'completed' },
+      ],
+    },
+  });
+  // 默认搜索值
+  const defaultSearchValues = reactive({
+    username: '',
+    status: '',
+  });
+  // 列表
+  const table = reactive({
+    data: [],
+    order: '',
+    sort: '',
+    selected: [],
+  });
+  const loading = ref(true);
+  
+  // 获取状态类型
+  function getStatusType(status) {
+    const statusMap = {
+      pending: 'warning',
+      approved: 'primary',
+      rejected: 'danger',
+      completed: 'success',
+    };
+    return statusMap[status] || 'info';
+  }
+  
+  // 获取
+  async function getData(page, searchParams = {}) {
+    if (page) pageData.page = page;
+    loading.value = true;
+    const { error, data } = await api.list({
+      page: pageData.page,
+      list_rows: pageData.list_rows,
+      order: table.order,
+      ...searchParams,
+      sort: table.sort,
+    });
+    console.log('API 响应:', error, data);
+    if (error === 0) {
+      table.data = data.data;
+      pageData.page = data.current_page;
+      pageData.list_rows = data.per_page;
+      pageData.total = data.total;
+    }
+    loading.value = false;
+  }
+  // table 字段排序
+  function fieldFilter({ prop, order }) {
+    table.order = order == 'ascending' ? 'asc' : 'desc';
+    table.sort = prop;
+    getData();
+  }
+  //table批量选择
+  function changeSelection(row) {
+    table.selected = row;
+  }
+  // 分页/批量操作
+  const batchHandleTools = [
+    {
+      type: 'approve',
+      label: '批量通过',
+      auth: 'shop.admin.finance.withdraw.approve',
+      class: 'success',
+    },
+    {
+      type: 'reject',
+      label: '批量拒绝',
+      auth: 'shop.admin.finance.withdraw.reject',
+      class: 'danger',
+    },
+  ];
+  
+  function editRow(row) {
+    useModal(
+      withdrawEdit,
+      {
+        title: '查看提款',
+        type: 'view',
+        id: row.id,
+      },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+  
+  // 通过提款
+  async function approveWithdraw(row) {
+    ElMessageBox.confirm('确认通过该笔提款申请?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+    }).then(async () => {
+      const { error } = await api.approve(row.id);
+      if (error === 0) {
+        ElMessage.success('通过成功');
+        getData();
+      }
+    });
+  }
+  
+  // 拒绝提款
+  async function rejectWithdraw(row) {
+    ElMessageBox.prompt('请输入拒绝原因', '拒绝提款', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      inputPattern: /.+/,
+      inputErrorMessage: '拒绝原因不能为空',
+    }).then(async ({ value }) => {
+      const { error } = await api.reject(row.id, { reason: value });
+      if (error === 0) {
+        ElMessage.success('拒绝成功');
+        getData();
+      }
+    });
+  }
+  
+  async function batchHandle(type) {
+    let ids = [];
+    table.selected.forEach((row) => {
+      ids.push(row.id);
+    });
+    switch (type) {
+      case 'approve':
+        ElMessageBox.confirm('确认通过选中的提款申请?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }).then(async () => {
+          const { error } = await api.batchApprove(ids.join(','));
+          if (error === 0) {
+            ElMessage.success('批量通过成功');
+            getData();
+          }
+        });
+        break;
+      case 'reject':
+        ElMessageBox.prompt('请输入拒绝原因', '批量拒绝', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          inputPattern: /.+/,
+          inputErrorMessage: '拒绝原因不能为空',
+        }).then(async ({ value }) => {
+          const { error } = await api.batchReject(ids.join(','), { reason: value });
+          if (error === 0) {
+            ElMessage.success('批量拒绝成功');
+            getData();
+          }
+        });
+        break;
+    }
+  }
+
+  onMounted(() => {
+    getData();
+  });
+</script>
+<style lang="scss" scoped>
+  .withdraw-view {
+    .el-header {
+      height: auto;
+    }
+    .el-main {
+      .sa-table-wrap {
+        height: 100%;
+      }
+    }
+  }
+</style>

+ 37 - 0
src/app/shop/admin/finance/withdraw/withdraw.service.js

@@ -0,0 +1,37 @@
+import { SELECT, CRUD } from '@/sheep/request/crud';
+
+const route = {
+  path: 'withdraw',
+  name: 'shop.admin.finance.withdraw',
+  component: () => import('@/app/shop/admin/finance/withdraw/index.vue'),
+  meta: {
+    title: '提款',
+  },
+};
+
+const api = {
+  ...CRUD('shop/admin/withdraw'),
+  select: (params) => SELECT('shop/admin/withdraw', params),
+  approve: (id) => ({
+    error: 0,
+    msg: '通过成功',
+    data: null,
+  }),
+  reject: (id, data) => ({
+    error: 0,
+    msg: '拒绝成功',
+    data: null,
+  }),
+  batchApprove: (ids) => ({
+    error: 0,
+    msg: '批量通过成功',
+    data: null,
+  }),
+  batchReject: (ids, data) => ({
+    error: 0,
+    msg: '批量拒绝成功',
+    data: null,
+  }),
+};
+
+export { route, api };

+ 0 - 246
src/app/shop/admin/goods/comment/edit.vue

@@ -1,246 +0,0 @@
-<template>
-  <el-container class="comment-edit">
-    <el-main>
-      <div class="comment-wrap">
-        <div class="news-box sa-row-between sa-flex-wrap">
-          <div class="evaluate">
-            <div class="evaluate-title sa-m-b-12">评价信息</div>
-            <div class="sa-flex">
-              <div>用户昵称:</div>
-              <div class="color-primary" v-if="commentList.data.user.nickname">
-                {{ commentList.data.user.nickname }}
-              </div>
-            </div>
-            <div class="sa-flex">
-              <div>评价星级:</div>
-              <el-rate v-model="commentList.data.level" disabled />
-            </div>
-            <div class="custom-content">
-              <div class="form-no-asterisk sa-flex">
-                <div>显示状态:</div>
-                <el-radio-group v-model="commentList.data.status" @change="changeStatus">
-                  <el-radio label="normal" :disabled="!checkAuth('shop.admin.goods.comment.edit')"
-                    >正常</el-radio
-                  >
-                  <el-radio label="hidden" :disabled="!checkAuth('shop.admin.goods.comment.edit')"
-                    >隐藏</el-radio
-                  >
-                </el-radio-group>
-              </div>
-            </div>
-          </div>
-          <div class="goods" v-if="commentList.data.order_item">
-            <div class="goods-title sa-m-b-12">商品信息</div>
-            <div class="sa-flex">
-              <sa-preview :url="commentList.data.order_item.goods_image" size="64"></sa-preview>
-              <div class="sa-m-l-8">
-                <div class="color-primary goods-des sa-line-1 sa-m-b-8">
-                  {{ commentList.data.order_item.goods_title }}
-                </div>
-                <div class="sa-flex sa-m-b-16">
-                  <div>单价:</div>
-                  <div>{{ commentList.data.order_item.goods_price }}</div>
-                  <div>×{{ commentList.data.order_item.goods_num }}</div>
-                </div>
-                <div class="sa-flex" v-if="commentList.data.order_item.goods_sku_text">
-                  <div>规格:</div>
-                  <div>{{ commentList.data.order_item.goods_sku_text }},</div>
-                </div>
-              </div>
-            </div>
-          </div>
-          <div class="goods" v-else-if="commentList.data.goods">
-            <div class="goods-title sa-m-b-12">商品信息</div>
-            <div class="sa-flex">
-              <sa-preview :url="commentList.data.goods.image" size="64"></sa-preview>
-              <div class="sa-m-l-8">
-                <div class="color-primary goods-des sa-line-1 sa-m-b-8">
-                  {{ commentList.data.goods.title }}
-                </div>
-                <div class="sa-flex sa-m-b-16">
-                  <div>单价:</div>
-                  <div>{{ commentList.data.goods.price[0] }} x1</div>
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-        <div class="record-box sa-m-t-32 sa-m-l-20 sa-m-r-20">
-          <div class="sa-flex sa-row-between sa-m-b-16">
-            <div class="record-title">评价记录</div>
-            <el-button
-              v-auth="'shop.admin.goods.comment.reply'"
-              type="primary"
-              class="is-link"
-              @click="editRow(commentList.data.id)"
-              v-if="!commentList.data.reply_time"
-              >点击回复</el-button
-            >
-          </div>
-          <div class="user admin sa-p-b-24" v-if="commentList.data.admin">
-            <div class="user-avatar sa-m-r-12">
-              <sa-preview :url="commentList.data.admin.avatar" size="48"></sa-preview>
-            </div>
-            <div>
-              <div class="sa-m-b-4 nickname">{{ commentList.data.admin.nickname }}</div>
-              <div class="sa-m-b-8 reply-time">{{ commentList.data.reply_time }}</div>
-              <div class="sa-m-b-8 content-title">{{ commentList.data.reply_content }}</div>
-            </div>
-          </div>
-          <div class="user sa-m-t-24">
-            <div class="user-avatar sa-m-r-12">
-              <sa-preview :url="commentList.data.user.avatar" size="48"></sa-preview>
-            </div>
-            <div>
-              <div class="sa-m-b-4 nickname">{{ commentList.data.user.nickname }}</div>
-              <div class="sa-m-b-8 reply-time">{{ commentList.data.create_time }}</div>
-              <div class="sa-m-b-8 content-title">{{ commentList.data.content }}</div>
-              <sa-preview :url="commentList.data.images" size="48"></sa-preview>
-            </div>
-          </div>
-        </div>
-      </div>
-    </el-main>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive } from 'vue';
-  import { api } from '../goods.service';
-  import { useModal } from '@/sheep/hooks';
-  import CommentReply from './reply.vue';
-  import { checkAuth } from '@/sheep/directives/auth';
-
-  const emit = defineEmits(['modalCallBack']);
-  const props = defineProps(['modal']);
-  const commentList = reactive({
-    data: {
-      level: 0,
-      title: '',
-      user: {},
-      admin: {},
-      id: 0,
-      reply_time: '',
-      reply_content: '',
-      order_item: {},
-      create_time: '',
-      content: '',
-      images: [],
-      status: '',
-    },
-  });
-  async function getDetail() {
-    const { error, data } = await api.comment.detail(props.modal.params.id);
-    error === 0 && (commentList.data = data);
-  }
-  async function changeStatus(e) {
-    await api.comment.edit(props.modal.params.id, {
-      status: e,
-    });
-    emit('modalCallBack', {
-      event: 'confirm',
-    });
-  }
-  function editRow(row) {
-    useModal(
-      CommentReply,
-      {
-        title: '回复买家',
-        type: 'edit',
-        id: row,
-      },
-      {
-        confirm: () => {
-          getDetail();
-        },
-      },
-    );
-  }
-  onMounted(() => {
-    getDetail();
-  });
-</script>
-<style lang="scss" scoped>
-  .comment-wrap {
-    font-size: 12px;
-    color: #8c8c8c;
-    .news-box {
-      width: 100%;
-      background: var(--sa-table-header-bg);
-      padding: 20px 24px;
-
-      display: flex;
-      .evaluate {
-        .evaluate-title {
-          font-size: 14px;
-          line-height: 18px;
-          font-weight: 500;
-          color: var(--sa-title);
-        }
-      }
-      .goods {
-        .goods-title {
-          font-size: 14px;
-          line-height: 18px;
-          font-weight: 500;
-          color: var(--sa-title);
-        }
-        .goods-des {
-          width: 220px;
-        }
-      }
-    }
-    .record-box {
-      .record-title {
-        font-weight: 500;
-        font-size: 16px;
-        line-height: 16px;
-        color: var(--sa-subtitle);
-      }
-      .user {
-        display: flex;
-      }
-      .admin {
-        border-bottom: 1px solid var(--sa-space);
-      }
-    }
-    .color-primary {
-      color: var(--el-color-primary);
-    }
-  }
-  .nickname {
-    font-weight: 500;
-    font-size: 12px;
-    color: var(--sa-subfont);
-  }
-  .reply-time {
-    font-weight: 400;
-    font-size: 12px;
-    color: var(--sa-subfont);
-  }
-  .content-title {
-    color: var(--sa-subtitle);
-    font-weight: 500;
-    font-size: 12px;
-  }
-
-  :deep() {
-    .el-form-item__label {
-      font-size: 12px;
-      color: #8c8c8c;
-    }
-    .el-form-item__content {
-      margin-left: 0 !important;
-    }
-    .el-form-item {
-      margin-bottom: 0;
-    }
-    .el-radio {
-      height: 16px;
-    }
-    .user-avatar {
-      .el-image {
-        border-radius: 24px !important;
-      }
-    }
-  }
-</style>

+ 0 - 210
src/app/shop/admin/goods/comment/fakeComment.vue

@@ -1,210 +0,0 @@
-<template>
-  <el-container>
-    <el-main>
-      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
-        <el-form-item label="评价内容" prop="content">
-          <el-input
-            v-model="form.model.content"
-            autosize
-            type="textarea"
-            placeholder="请输入评价内容"
-          />
-        </el-form-item>
-        <el-form-item label="评价图片">
-          <sa-uploader v-model="form.model.images" :multiple="true" fileType="image"></sa-uploader>
-        </el-form-item>
-        <el-form-item label="评价星级" prop="level">
-          <div class="levels">
-            <el-rate v-model="form.model.level" />
-          </div>
-          <!-- :icons="['#sa-shop-star', '#sa-shop-star', '#sa-shop-star']"
-            :void-icon="'#sa-shop-star'"
-            :colors="['#faad14', '#faad14', '#faad14']" -->
-        </el-form-item>
-        <el-form-item label="评价用户" v-if="form.model.fakeUser" prop="fakeUser.id">
-          <el-button
-            type="primary"
-            class="is-link"
-            @click="selectFakeUser"
-            v-if="!form.model.fakeUser.id"
-          >
-            选择用户
-          </el-button>
-          <div class="sa-flex sa-row-between sa-w-360" v-if="form.model.fakeUser.id">
-            <div class="sa-flex">
-              <div class="sa-m-r-60">{{ form.model.fakeUser.id }}</div>
-              <div>{{ form.model.fakeUser.nickname }}</div>
-            </div>
-            <el-button type="danger" class="is-link sa-m-l-40" @click="deleteFakeUserItem()">
-              移除
-            </el-button>
-          </div>
-        </el-form-item>
-
-        <el-form-item label="商品选择" prop="goods_id">
-
-          <el-button type="primary" class="is-link" @click="selectGoods" v-if="!form.model.goods_id"
-            >选择商品</el-button
-          >
-          <div class="sa-template-content sa-template-goods" v-if="form.model.goods_id">
-              <div class="header sa-flex">
-                <div class="item">商品信息</div>
-                <div class="oper">操作</div>
-              </div>
-              <div class="list sa-flex">
-                <div class="item sa-flex">
-                  <sa-image :url="goods.data.image" size="40"></sa-image>
-                  <div class="sa-m-l-8">
-                    <div class="title">{{ goods.data.title }}</div>
-                    <div class="price">¥{{ goods.data.price.join('~') || 0 }}</div>
-                  </div>
-                </div>
-                <div class="oper">
-                  <el-button class="is-link" type="danger" @click="deleteItems()"
-                    >移除</el-button
-                  >
-                </div>
-              </div>
-            </div>
-        </el-form-item>
-        <el-form-item label="状态" prop="status">
-          <el-radio-group v-model="form.model.status">
-            <el-radio label="normal">显示</el-radio>
-            <el-radio label="hidden">隐藏</el-radio>
-          </el-radio-group>
-        </el-form-item>
-      </el-form>
-    </el-main>
-    <el-footer class="sa-footer--submit">
-      <el-button type="primary" @click="confirm">添加虚拟评价</el-button>
-    </el-footer>
-  </el-container>
-</template>
-<script setup>
-  import { reactive, ref, unref, watch } from 'vue';
-  import { api } from '../goods.service';
-  // import useFakeUser from './fakeUser';
-  import { useModal } from '@/sheep/hooks';
-  import GoodsSelect from '@/app/shop/admin/goods/goods/select.vue';
-  import FakeUserSelect from '@/app/shop/admin/data/fakeUser/select.vue';
-
-  const emit = defineEmits(['modalCallBack']);
-  const props = defineProps({
-    modal: {
-      type: Object,
-    },
-  });
-  // 添加 编辑 form
-  let formRef = ref(null);
-  const form = reactive({
-    model: {
-      content: '',
-      images: [],
-      level: 0,
-      user_id: '',
-      fakeUser: {
-        nickname: '',
-        id: '',
-      },
-      goods_id: '',
-      status: 'normal',
-    },
-    rules: {
-      content: [{ required: true, message: '请输入评价内容', trigger: 'blur' }],
-      // images: [{ required: true, message: '请选择评价图片', trigger: 'blur' }],
-      level: [{ required: true, message: '请选择评价星级', trigger: 'blur' }],
-      fakeUser: {
-        id: [{ required: true, message: '请选择评价用户', trigger: 'blur' }],
-      },
-      status: [{ required: true, message: '请选择状态', trigger: 'blur' }],
-      goods_id: [{ required: true, message: '请选择商品信息', trigger: 'blur' }],
-    },
-  });
-  const goods = reactive({
-    data: {},
-  });
-  const loading = ref(false);
-
-  // const { fakeUser, deliverCompany, getDeliverCompany, onChangeExpressCode, remoteMethod } =
-  //   useFakeUser();
-  // watch(
-  //   () => fakeUser.id,
-  //   () => {
-  //     form.model.fakeUser = {
-  //       nickname: fakeUser.nickname,
-  //       id: fakeUser.id,
-  //     };
-  //   },
-  // );
-
-  function selectGoods() {
-    let id = '';
-    useModal(
-      GoodsSelect,
-      {
-        title: '选择商品',
-        id,
-      },
-      {
-        confirm: (res) => {
-          goods.data = res.data;
-          form.model.goods_id = res.data.id;
-        },
-      },
-    );
-  }
-  function selectFakeUser() {
-    let id = '';
-    useModal(
-      FakeUserSelect,
-      {
-        title: '选择评价用户',
-        id,
-      },
-      {
-        confirm: (res) => {
-          form.model.fakeUser.nickname = res.data.nickname;
-          form.model.fakeUser.id = res.data.id;
-        },
-      },
-    );
-  }
-  function deleteFakeUserItem() {
-    form.model.fakeUser = {};
-  }
-  function deleteItems() {
-    goods.data = {};
-    form.model.goods_id = '';
-  }
-
-  // 表单关闭时提交
-  async function confirm() {
-    // 表单验证
-    unref(formRef).validate(async (valid) => {
-      if (!valid) return;
-      form.model.user_id = form.model.fakeUser.id;
-      delete form.model.fakeUser;
-      form.model.goods_id = goods.data.id;
-      const { error } = await api.comment.add(form.model);
-      if (error == 0) {
-        emit('modalCallBack', { event: 'confirm' });
-      }
-    });
-  }
-</script>
-<style lang="scss" scoped>
-  .goods-title {
-    line-height: 16px;
-    // width: 160px;
-  }
-  :deep() {
-    .el-rate .el-rate__item {
-      color: var(--sa-place);
-      height: 24px;
-      .el-icon svg {
-        width: 24px;
-        height: 24px;
-      }
-    }
-  }
-</style>

+ 0 - 57
src/app/shop/admin/goods/comment/fakeUser.js

@@ -1,57 +0,0 @@
-import { getCurrentInstance, onMounted, reactive } from 'vue';
-import { api } from '@/app/shop/admin/data/data.service';
-
-export default function useFakeUser() {
-  const { proxy } = getCurrentInstance();
-
-  const fakeUser = reactive({ nickname: '', id: '' });
-
-  const deliverCompany = reactive({
-    loading: false,
-    data: [],
-    pageData: {
-      page: 1,
-      list_rows: 10,
-      total: 0,
-    },
-  });
-
-  async function getDeliverCompany(keyword) {
-    let search = {};
-    if (keyword) {
-      search = { keyword: keyword };
-    }
-    const { data } = await api.fakeUser.select({
-      page: deliverCompany.pageData.page,
-      list_rows: deliverCompany.pageData.list_rows,
-      search: JSON.stringify(search),
-    });
-    deliverCompany.data = data.data;
-    deliverCompany.pageData.page = data.current_page;
-    deliverCompany.pageData.list_rows = data.per_page;
-    deliverCompany.pageData.total = data.total;
-  }
-
-  function onChangeExpressCode(code) {
-    fakeUser.nickname = proxy.$refs[`dc-${code}`][0].label;
-  }
-
-  function remoteMethod(keyword) {
-    deliverCompany.loading = true;
-    setTimeout(() => {
-      deliverCompany.loading = false;
-      getDeliverCompany(keyword);
-    }, 200);
-  }
-
-  onMounted(() => {
-    getDeliverCompany();
-  });
-  return {
-    fakeUser,
-    deliverCompany,
-    getDeliverCompany,
-    onChangeExpressCode,
-    remoteMethod,
-  };
-}

+ 0 - 503
src/app/shop/admin/goods/comment/index.vue

@@ -1,503 +0,0 @@
-<template>
-  <el-container class="comment-view panel-block">
-    <el-header class="sa-header">
-      <div class="sa-title sa-flex sa-row-between">
-        <div class="label sa-flex">
-          <span class="left">评价列表</span>
-          <search-condition
-            :conditionLabel="filterParams.conditionLabel"
-            @deleteFilter="deleteFilter"
-          ></search-condition>
-        </div>
-        <div>
-          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
-          <el-button
-            v-if="!modal"
-            class="sa-button-refresh"
-            icon="Search"
-            @click="openFilter"
-          ></el-button>
-          <el-button v-auth="'shop.admin.goods.comment.add'" @click="fakeCommentRow" plain
-            >添加虚拟评价</el-button
-          >
-          <el-button
-            v-auth="'shop.admin.goods.comment.recyclebin'"
-            type="danger"
-            icon="Delete"
-            plain
-            @click="openRecyclebin"
-            >回收站</el-button
-          >
-        </div>
-      </div>
-    </el-header>
-    <el-main class="sa-p-0">
-      <div class="sa-table-wrap panel-block panel-block--bottom" v-loading="loading">
-        <el-table
-          height="100%"
-          class="sa-table"
-          :data="table.data"
-          @selection-change="changeSelection"
-          @sort-change="fieldFilter"
-          @row-dblclick="editRow"
-          row-key="id"
-          stripe
-        >
-          <template #empty>
-            <sa-empty />
-          </template>
-          <el-table-column type="selection" width="48" align="center"></el-table-column>
-          <el-table-column label="商品信息" min-width="310">
-            <template #default="scope">
-              <div class="sa-flex" v-if="scope.row.order_item">
-                <div class="sa-m-r-8">
-                  <sa-preview :url="scope.row.order_item.goods_image" size="40"></sa-preview>
-                </div>
-                <div>
-                  <span
-                    class="sa-table-line-1 goods-title cursor-pointer"
-                    @click="openGoods(scope.row.order_item.goods_id)"
-                  >
-                    {{ scope.row.order_item.goods_title || '-' }}
-                  </span>
-                  <span class="sa-table-line-1" v-if="scope.row.order">
-                    订单号:{{ scope.row.order.order_sn }}
-                    <sa-svg
-                      class="copy sa-m-l-4 cursor-pointer"
-                      name="sa-copy"
-                      @click="useClip(scope.row.order.order_sn)"
-                    ></sa-svg>
-                  </span>
-                  <span v-else>订单号:-</span>
-                </div>
-              </div>
-              <div class="sa-flex" v-else-if="scope.row.goods">
-                <div class="sa-m-r-8">
-                  <sa-preview :url="scope.row.goods.image" size="40"></sa-preview>
-                </div>
-                <div>
-                  <span
-                    class="sa-table-line-1 goods-title cursor-pointer"
-                    @click="openGoods(scope.row.goods.id)"
-                  >
-                    {{ scope.row.goods.title || '-' }}
-                  </span>
-                  <span>虚拟评价</span>
-                </div>
-              </div>
-            </template>
-          </el-table-column>
-          <el-table-column label="用户信息" min-width="96" align="center">
-            <template #default="scope">
-              <sa-user-profile
-                :user="scope.row.user"
-                :id="scope.row.user_id"
-                mode="col"
-                :isHover="scope.row.user_type != 'fake_user'"
-              />
-            </template>
-          </el-table-column>
-          <el-table-column label="评价星级" min-width="146" align="center">
-            <template #default="scope">
-              <el-rate v-model="scope.row.level" disabled />
-            </template>
-          </el-table-column>
-          <el-table-column label="评价图片" width="200" align="center">
-            <template #default="scope">
-              <el-scrollbar class="images-wrap">
-                <div v-if="scope.row.images" class="sa-flex">
-                  <div v-for="i in scope.row.images" :key="i" class="sa-m-r-8">
-                    <sa-preview :url="i" size="30"></sa-preview>
-                  </div>
-                </div>
-              </el-scrollbar>
-            </template>
-          </el-table-column>
-          <el-table-column label="评价内容" min-width="162" align="center">
-            <template #default="scope">
-              <div class="sa-table-line-1">
-                {{ scope.row.content || '-' }}
-              </div>
-            </template>
-          </el-table-column>
-          <el-table-column label="评价时间" min-width="180" align="center">
-            <template #default="scope">
-              {{ scope.row.create_time || '-' }}
-            </template>
-          </el-table-column>
-          <el-table-column label="显示状态" min-width="130" align="center">
-            <template #default="scope">
-              <el-dropdown trigger="click" @command="handleCommand">
-                <el-button
-                  v-auth="'shop.admin.goods.comment.edit'"
-                  class="status-btn"
-                  :class="scope.row.status == 'normal' ? 'success-btn' : ''"
-                >
-                  {{ scope.row.status_text }}
-                  <el-icon class="el-icon--right"><ArrowDown /></el-icon>
-                </el-button>
-                <template #dropdown>
-                  <el-dropdown-menu>
-                    <el-dropdown-item
-                      :command="{
-                        id: scope.row.id,
-                        type: 'normal',
-                      }"
-                    >
-                      <span class="status-normal">正常</span>
-                    </el-dropdown-item>
-                    <el-dropdown-item
-                      :command="{
-                        id: scope.row.id,
-                        type: 'hidden',
-                      }"
-                      ><span class="status-hidden">隐藏</span></el-dropdown-item
-                    >
-                  </el-dropdown-menu>
-                </template>
-              </el-dropdown>
-            </template>
-          </el-table-column>
-          <el-table-column fixed="right" label="操作" min-width="120">
-            <template #default="scope">
-              <el-button
-                v-auth="'shop.admin.goods.comment.detail'"
-                type="primary"
-                class="is-link"
-                @click="editRow(scope.row)"
-                >编辑</el-button
-              >
-              <el-popconfirm
-                width="fit-content"
-                confirm-button-text="确认"
-                cancel-button-text="取消"
-                title="确认删除这条记录?"
-                @confirm="deleteApi(scope.row.id)"
-              >
-                <template #reference>
-                  <el-button
-                    v-auth="'shop.admin.goods.comment.delete'"
-                    type="danger"
-                    class="is-link"
-                  >
-                    删除
-                  </el-button>
-                </template>
-              </el-popconfirm>
-            </template>
-          </el-table-column>
-        </el-table>
-      </div>
-    </el-main>
-    <sa-view-bar>
-      <template #left>
-        <sa-batch-handle
-          :batchHandleTools="batchHandleTools"
-          :selectedLeng="table.selected.length"
-          @batchHandle="batchHandle"
-        ></sa-batch-handle>
-      </template>
-      <template #right>
-        <sa-pagination :pageData="pageData" @updateFn="getData" />
-      </template>
-    </sa-view-bar>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from '../goods.service';
-  import { ElMessageBox } from 'element-plus';
-  import { useModal, useSearch, usePagination } from '@/sheep/hooks';
-  import { composeFilter } from '@/sheep/utils';
-  import useClip from '@/sheep/utils/clipboard.js';
-  import GoodsEdit from '@/app/shop/admin/goods/goods/edit.vue';
-  import CommentEdit from './edit.vue';
-  import CommentRecyclebin from './recyclebin.vue';
-  import CommentFakeComment from './fakeComment.vue';
-  import { cloneDeep } from 'lodash';
-
-  const props = defineProps(['modal']);
-
-  // 列表
-  const table = reactive({
-    data: [],
-    order: '',
-    sort: '',
-    selected: [],
-  });
-  const { pageData } = usePagination();
-  const loading = ref(true);
-  const filterParams = reactive({
-    tools: {
-      'goods.title': {
-        type: 'tinput',
-        label: '商品名称',
-        field: 'goods.title',
-        value: '',
-      },
-      user: {
-        type: 'tinputprepend',
-        label: '用户信息',
-        field: 'user',
-        placeholder: '请输入查询内容',
-        user: {
-          field: 'user.nickname',
-          value: '',
-        },
-        options: [
-          {
-            label: '用户昵称',
-            value: 'user.nickname',
-          },
-          {
-            label: '用户手机号',
-            value: 'user.mobile',
-          },
-        ],
-      },
-      content: {
-        type: 'tinput',
-        label: '评价内容',
-        field: 'content',
-        value: '',
-      },
-      status: {
-        type: 'tselect',
-        label: '状态',
-        field: 'status',
-        value: '',
-        options: {
-          data: [
-            {
-              label: '显示',
-              value: 'normal',
-            },
-            {
-              label: '隐藏',
-              value: 'hidden',
-            },
-          ],
-        },
-      },
-    },
-    data: {
-      'goods.title': '',
-      user: { field: 'user.nickname', value: '' },
-      content: '',
-      status: '',
-    },
-    conditionLabel: {},
-  });
-  const { openFilter, deleteFilter } = useSearch({ filterParams, getData });
-  // 获取
-  async function getData(page) {
-    loading.value = true;
-    if (page) pageData.page = page;
-    let tempSearch = cloneDeep(filterParams.data);
-
-    if (props.modal) {
-      tempSearch = { tempSearch, ...props.modal.params.data };
-    }
-
-    let search = composeFilter(tempSearch, {
-      'goods.title': 'like',
-      'user.nickname': 'like',
-      'user.mobile': 'like',
-      content: 'like',
-    });
-    const { error, data } = await api.comment.list({
-      page: pageData.page,
-      list_rows: pageData.list_rows,
-      order: table.order,
-      sort: table.sort,
-      ...search,
-    });
-    if (error === 0) {
-      table.data = data.data;
-      pageData.page = data.current_page;
-      pageData.list_rows = data.per_page;
-      pageData.total = data.total;
-    }
-    loading.value = false;
-  }
-  //商品详情
-  function openGoods(e) {
-    useModal(GoodsEdit, {
-      title: '商品',
-      type: 'edit',
-      id: e,
-    });
-  }
-
-  // table 字段排序
-  function fieldFilter({ prop, order }) {
-    table.order = order == 'ascending' ? 'asc' : 'desc';
-    table.sort = prop;
-    getData();
-  }
-  //table批量选择
-  function changeSelection(row) {
-    table.selected = row;
-  }
-  const batchHandleTools = [
-    {
-      type: 'delete',
-      label: '删除',
-      auth: 'shop.admin.goods.comment.delete',
-      class: 'danger',
-    },
-    {
-      type: 'normal',
-      label: '正常',
-      auth: 'shop.admin.goods.comment.edit',
-      class: 'success',
-    },
-    {
-      type: 'hidden',
-      label: '隐藏',
-      auth: 'shop.admin.goods.comment.edit',
-      class: 'info',
-    },
-  ];
-  async function handleCommand(e) {
-    await api.comment.edit(e.id, {
-      status: e.type,
-    });
-    getData();
-  }
-  function editRow(row) {
-    useModal(
-      CommentEdit,
-      {
-        title: '评价详情',
-        type: 'edit',
-        id: row.id,
-      },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-
-  function openRecyclebin() {
-    useModal(
-      CommentRecyclebin,
-      {
-        title: '回收站',
-      },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-  //虚拟评价
-  function fakeCommentRow() {
-    useModal(
-      CommentFakeComment,
-      { title: '添加' },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-
-  // 删除api 单独批量可以直接调用
-  async function deleteApi(id) {
-    await api.comment.delete(id);
-    getData();
-  }
-  async function batchHandle(type) {
-    let ids = [];
-    table.selected.forEach((row) => {
-      ids.push(row.id);
-    });
-    switch (type) {
-      case 'delete':
-        ElMessageBox.confirm('此操作将删除, 是否继续?', '提示', {
-          confirmButtonText: '确定',
-          cancelButtonText: '取消',
-          type: 'warning',
-        }).then(() => {
-          deleteApi(ids.join(','));
-        });
-        break;
-      default:
-        await api.comment.edit(ids.join(','), {
-          status: type,
-        });
-        getData();
-    }
-  }
-
-  onMounted(() => {
-    getData();
-  });
-</script>
-<style lang="scss" scoped>
-  .copy {
-    color: var(--el-color-primary);
-  }
-  .status-hidden {
-    color: #999999;
-  }
-  .status-normal {
-    color: #52c41a;
-  }
-  .comment-view {
-    .el-header {
-      height: auto;
-    }
-
-    .el-main {
-      .sa-table-wrap {
-        height: 100%;
-      }
-    }
-    .goods-title {
-      color: var(--el-color-primary);
-    }
-    .status-btn {
-      height: 24px;
-      font-size: 12px;
-      color: #999999;
-      background: rgba(153, 153, 153, 0.16);
-      width: 56px;
-      border-radius: 4px;
-      border: none;
-    }
-    .success-btn {
-      color: #52c41a;
-      background: rgba(82, 196, 26, 0.16);
-    }
-  }
-  :deep() {
-    .el-rate .el-rate__icon.is-active {
-      color: #faad14;
-    }
-    .el-rate.is-disabled .el-rate__item {
-      color: var(--sa-place);
-      height: 24px;
-      .el-icon svg {
-        width: 24px;
-        height: 24px;
-      }
-    }
-    .el-table__body-wrapper {
-      // font-size: 12px;
-    }
-    .user-avatar {
-      .el-image {
-        border-radius: 12px !important;
-      }
-    }
-    .images-wrap {
-      width: 176px;
-    }
-  }
-</style>

+ 0 - 184
src/app/shop/admin/goods/comment/recyclebin.vue

@@ -1,184 +0,0 @@
-<template>
-  <el-container class="recyclebin-view">
-    <el-main v-loading="loading">
-      <el-table
-        :data="table.data"
-        @selection-change="changeSelection"
-        @sort-change="fieldFilter"
-        class="sa-table"
-        stripe
-      >
-        <template #empty>
-          <sa-empty />
-        </template>
-        <el-table-column type="selection" width="48" align="center"></el-table-column>
-        <el-table-column sortable="custom" prop="id" label="ID" min-width="100"></el-table-column>
-        <el-table-column label="评价内容" min-width="100">
-          <template #default="scope">
-            <div class="sa-table-line-1">{{ scope.row.content || '-' }}</div>
-          </template>
-        </el-table-column>
-        <el-table-column
-          sortable="custom"
-          prop="delete_time"
-          label="删除时间"
-          min-width="172"
-        ></el-table-column>
-        <el-table-column fixed="right" label="操作" min-width="120">
-          <template #default="scope">
-            <el-button
-              v-auth="'shop.admin.goods.comment.restore'"
-              class="is-link"
-              type="primary"
-              @click="restoreRow(scope.row.id)"
-              >还原</el-button
-            >
-            <el-popconfirm
-              width="fit-content"
-              confirm-button-text="确认"
-              cancel-button-text="取消"
-              title="确认销毁这条记录?"
-              @confirm="destroyRow(scope.row.id)"
-            >
-              <template #reference>
-                <el-button v-auth="'shop.admin.goods.comment.destroy'" class="is-link" type="danger"
-                  >销毁</el-button
-                >
-              </template>
-            </el-popconfirm>
-          </template>
-        </el-table-column>
-      </el-table>
-    </el-main>
-    <sa-view-bar>
-      <template #left>
-        <sa-batch-handle
-          :batchHandleTools="batchHandleTools"
-          :selectedLeng="table.selected.length"
-          @batchHandle="batchHandle"
-        ></sa-batch-handle>
-      </template>
-      <template #right>
-        <sa-pagination :pageData="pageData" @updateFn="getData" />
-      </template>
-    </sa-view-bar>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from '../goods.service';
-  import { ElMessageBox } from 'element-plus';
-  import { usePagination } from '@/sheep/hooks';
-
-  const loading = ref(true);
-
-  // 表格状态
-  const table = reactive({
-    data: [],
-    order: '',
-    sort: '',
-    selected: [],
-  });
-
-  const { pageData } = usePagination();
-
-  // 获取数据
-  async function getData() {
-    loading.value = true;
-    const { data } = await api.comment.recyclebin({
-      page: pageData.page,
-      list_rows: pageData.list_rows,
-      order: table.order,
-      sort: table.sort,
-    });
-    table.data = data.data;
-    pageData.page = data.current_page;
-    pageData.list_rows = data.per_page;
-    pageData.total = data.total;
-    loading.value = false;
-  }
-
-  // table 字段排序
-  function fieldFilter({ prop, order }) {
-    table.order = order == 'ascending' ? 'asc' : 'desc';
-    table.sort = prop;
-    getData();
-  }
-
-  // table 批量选择
-  function changeSelection(row) {
-    table.selected = row;
-  }
-
-  // 批量操作
-  const batchHandleTools = [
-    {
-      type: 'restore',
-      label: '还原',
-      auth: 'shop.admin.goods.comment.restore',
-      buttonType: 'primary',
-    },
-    {
-      type: 'destroy',
-      label: '销毁',
-      auth: 'shop.admin.goods.comment.destroy',
-      buttonType: 'danger',
-    },
-    {
-      type: 'all',
-      label: '清空回收站',
-      auth: 'shop.admin.goods.comment.destroy',
-      buttonType: 'danger',
-      operType: 'all',
-    },
-  ];
-  async function batchHandle(type) {
-    let ids = [];
-    table.selected.forEach((row) => {
-      ids.push(row.id);
-    });
-    switch (type) {
-      case 'all':
-        ElMessageBox.confirm('此操作将清空回收站, 是否继续?', '提示', {
-          confirmButtonText: '确定',
-          cancelButtonText: '取消',
-          type: 'warning',
-        }).then(() => {
-          destroyRow('all');
-        });
-        break;
-      case 'destroy':
-        ElMessageBox.confirm('此操作将销毁, 是否继续?', '提示', {
-          confirmButtonText: '确定',
-          cancelButtonText: '取消',
-          type: 'warning',
-        }).then(() => {
-          destroyRow(ids.join(','));
-        });
-        break;
-      case 'restore':
-        ElMessageBox.confirm('此操作将还原, 是否继续?', '提示', {
-          confirmButtonText: '确定',
-          cancelButtonText: '取消',
-          type: 'warning',
-        }).then(() => {
-          restoreRow(ids.join(','));
-        });
-        break;
-    }
-  }
-  // 销毁
-  async function destroyRow(id) {
-    await api.comment.destroy(id);
-    getData();
-  }
-  // 还原
-  async function restoreRow(id) {
-    await api.comment.restore(id);
-    getData();
-  }
-
-  onMounted(() => {
-    getData();
-  });
-</script>

+ 0 - 50
src/app/shop/admin/goods/comment/reply.vue

@@ -1,50 +0,0 @@
-<template>
-  <el-container class="goods-stock">
-    <el-main>
-      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
-        <el-form-item label="回复内容" prop="content">
-          <el-input v-model="form.model.content" placeholder="请输入回复内容"></el-input>
-        </el-form-item>
-      </el-form>
-    </el-main>
-    <el-footer class="sa-footer--submit">
-      <el-button type="primary" @click="confirm">确 定</el-button>
-    </el-footer>
-  </el-container>
-</template>
-<script setup>
-  import { reactive, ref, unref } from 'vue';
-  import { api } from '../goods.service';
-  import { cloneDeep } from 'lodash';
-
-  const emit = defineEmits(['modalCallBack']);
-  const props = defineProps({
-    modal: {
-      type: Object,
-    },
-  });
-  const loading = ref(false);
-  // 添加 编辑 form
-  let formRef = ref(null);
-  const form = reactive({
-    model: {
-      content: '',
-    },
-    rules: {
-      content: [{ required: true, message: '请输入回复内容', trigger: 'change' }],
-    },
-  });
-  async function confirm() {
-    // 表单验证
-    unref(formRef).validate(async (valid) => {
-      if (!valid) return;
-      let submitForm = cloneDeep(form.model);
-      const { error } = await api.comment.reply(props.modal.params.id, submitForm);
-      if (error == 0) {
-        emit('modalCallBack', {
-          event: 'confirm',
-        });
-      }
-    });
-  }
-</script>

+ 1 - 113
src/app/shop/admin/goods/goods.service.js

@@ -1,6 +1,6 @@
 import Content from '@/sheep/layouts/content.vue';
 import { request } from '@/sheep/request';
-import { SELECT, RECYCLE_BIN, CRUD, RECYCLE } from '@/sheep/request/crud';
+import { CRUD } from '@/sheep/request/crud';
 
 const route = {
   path: 'goods',
@@ -18,140 +18,28 @@ const route = {
         title: '商品库',
       },
     },
-    {
-      path: 'stockwarning',
-      name: 'shop.admin.goods.stockwarning',
-      component: () => import('./stockWarning/index.vue'),
-      meta: {
-        title: '库存预警',
-      },
-    },
-    {
-      path: 'stocklog',
-      name: 'shop.admin.goods.stocklog',
-      component: () => import('./stockLog.vue'),
-      meta: {
-        title: '库存记录',
-      },
-    },
-    {
-      path: 'service',
-      name: 'shop.admin.goods.service',
-      component: () => import('./service/index.vue'),
-      meta: {
-        title: '服务保障',
-      },
-    },
-    {
-      path: 'comment',
-      name: 'shop.admin.goods.comment',
-      component: () => import('./comment/index.vue'),
-      meta: {
-        title: '客户评价',
-      },
-    },
   ],
 };
 
 const api = {
   goods: {
     ...CRUD('shop/admin/goods/goods'),
-    ...RECYCLE('shop/admin/goods/goods'),
     select: (params, type = 'page') =>
       request({
         url: `shop/admin/goods/goods/select?type=${type}`,
         method: 'GET',
         params,
       }),
-    activitySelect: (params) =>
-      request({
-        url: 'shop/admin/goods/goods/activitySelect',
-        method: 'GET',
-        params,
-      }),
     getType: () =>
       request({
         url: '/shop/admin/goods/goods/getType',
         method: 'GET',
       }),
-    getSkuPrice: (id) =>
-      request({
-        url: '/shop/admin/goods/skuPrice/' + id,
-        method: 'GET',
-      }),
-    updateSkuPrice: (id, status) =>
-      request({
-        url: '/shop/admin/goods/skuPrice/' + id,
-        method: 'PUT',
-        params: {
-          status: status,
-        },
-      }),
-    addStock: (id, data) =>
-      request({
-        url: 'shop/admin/goods/goods/addStock/' + id,
-        method: 'POST',
-        data,
-      }),
-    // 新增商品编辑相关接口
-    saveGoods: (data) =>
-      request({
-        url: '/shop/admin/goods/goods/save',
-        method: 'POST',
-        data,
-      }),
     getGoodsDetail: (id) =>
       request({
         url: `/shop/admin/goods/goods/detail/${id}`,
         method: 'GET',
       }),
-    getCategory: () =>
-      request({
-        url: '/shop/admin/goods/category/tree',
-        method: 'GET',
-      }),
-    uploadImage: (file) => {
-      const formData = new FormData();
-      formData.append('file', file);
-      return request({
-        url: '/shop/admin/upload/image',
-        method: 'POST',
-        data: formData,
-        headers: {
-          'Content-Type': 'multipart/form-data',
-        },
-      });
-    },
-  },
-  stockWarning: {
-    ...CRUD('shop/admin/goods/stockWarning', ['list']),
-    recyclebin: (params) => RECYCLE_BIN('shop/admin/goods/stockWarning', params),
-    addStock: (id, params) =>
-      request({
-        url: 'shop/admin/goods/stockWarning/addStock/' + id,
-        method: 'POST',
-        params,
-      }),
-  },
-  stockLog: (params) =>
-    request({
-      url: 'shop/admin/goods/stockLog',
-      method: 'GET',
-      params,
-    }),
-  service: {
-    ...CRUD('shop/admin/goods/service'),
-    select: (params) => SELECT('shop/admin/goods/service', params),
-  },
-  comment: {
-    ...CRUD('shop/admin/goods/comment'),
-    ...RECYCLE('shop/admin/goods/comment'),
-    reply: (id, params) =>
-      request({
-        url: 'shop/admin/goods/comment/reply/' + id,
-        method: 'PUT',
-        params,
-      }),
   },
 };
 

+ 2 - 16
src/app/shop/admin/goods/goods/index.vue

@@ -8,8 +8,8 @@
             <sa-search-simple
               :searchFields="searchFields"
               :defaultValues="defaultSearchValues"
-              @search="handleSearch"
-              @reset="handleReset"
+              @search="(val) => getData(1, val)"
+              @reset="getData(1)"
             >
               <template #custom="{ data }">
                 <el-form-item label="价格区间">
@@ -238,20 +238,6 @@
     getData(1, { status: status === 'all' ? '' : status });
   };
 
-  // 搜索处理
-  const handleSearch = (searchData) => {
-    console.log('搜索数据:', searchData);
-    // 这里可以调用 getData 并传入搜索参数
-    getData(1, searchData);
-  };
-
-  // 重置处理
-  const handleReset = () => {
-    console.log('重置搜索');
-    // 重置后重新获取数据
-    getData(1);
-  };
-
   const loading = ref(true);
 
   // 表格

+ 0 - 82
src/app/shop/admin/goods/goods/search.vue

@@ -1,82 +0,0 @@
-<template>
-  <div class="goods-filter">
-    <sa-search :filterParams="filterParams" @filterBack="filterBack">
-      <template #end>
-        <el-form-item label="价格区间">
-          <div class="sa-flex">
-            <el-input v-model="goods.data.price.min" placeholder="最低价格"></el-input>
-            <span class="space">至</span>
-            <el-input v-model="goods.data.price.max" placeholder="最高价格"></el-input>
-          </div>
-        </el-form-item>
-        <el-form-item label="销量区间">
-          <div class="sa-flex">
-            <el-input v-model="goods.data.sales.min" placeholder="最低销量"></el-input>
-            <span class="space">至</span>
-            <el-input v-model="goods.data.sales.max" placeholder="最高销量"></el-input>
-          </div>
-        </el-form-item>
-      </template>
-    </sa-search>
-  </div>
-</template>
-<script setup>
-  import { onMounted, reactive } from 'vue';
-  import { cloneDeep } from 'lodash';
-
-  const emit = defineEmits(['filterBack']);
-  const props = defineProps(['filterParams']);
-
-  const fp = cloneDeep(props.filterParams);
-
-  const goods = reactive({
-    data: {
-      price: fp.data.price,
-      sales: fp.data.sales,
-    },
-    conditionLabel: {},
-  });
-
-  function initLabel(data) {
-    if (goods.data.price.min && goods.data.price.max) {
-      goods.conditionLabel.price = `${props.filterParams.tools.price.label}:${goods.data.price.min} - ${goods.data.price.max}`;
-    }
-    if (goods.data.sales.min && goods.data.sales.max) {
-      goods.conditionLabel.sales = `${props.filterParams.tools.sales.label}:${goods.data.sales.min} - ${goods.data.sales.max}`;
-    }
-  }
-
-  function filterBack(redata) {
-    initLabel();
-
-    if (redata.event == 'confirm') {
-      for (let f in goods.data) {
-        redata.data.data[f] = goods.data[f];
-        if (goods.conditionLabel[f]) redata.data.conditionLabel[f] = goods.conditionLabel[f];
-      }
-    }
-
-    if (redata.event == 'reset') {
-      redata.data.data.price = props.filterParams.tools.price.value;
-      redata.data.data.sales = props.filterParams.tools.sales.value;
-
-      delete redata.data.conditionLabel.price;
-      delete redata.data.conditionLabel.sales;
-    }
-
-    emit('filterBack', redata);
-  }
-
-  onMounted(() => {
-    initLabel();
-  });
-</script>
-<style lang="scss" scoped>
-  .goods-filter {
-    color: var(--sa-font);
-    width: 100%;
-    .space {
-      margin: 0 12px;
-    }
-  }
-</style>

+ 0 - 86
src/app/shop/admin/goods/service/edit.vue

@@ -1,86 +0,0 @@
-<template>
-  <el-container>
-    <el-main>
-      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
-        <el-form-item label="名称" prop="name">
-          <el-input v-model="form.model.name" placeholder="请填写服务保障名称"></el-input>
-        </el-form-item>
-        <el-form-item label="服务标识">
-          <sa-uploader v-model="form.model.image" fileType="image"></sa-uploader>
-        </el-form-item>
-        <el-form-item label="说明">
-          <el-input v-model="form.model.description" placeholder="请输入说明"></el-input>
-        </el-form-item>
-      </el-form>
-    </el-main>
-    <el-footer class="sa-footer--submit">
-      <el-button
-        v-if="modal.params.type == 'add'"
-        v-auth="'shop.admin.goods.service.add'"
-        type="primary"
-        @click="confirm"
-        >保存</el-button
-      >
-      <el-button
-        v-if="modal.params.type == 'edit'"
-        v-auth="'shop.admin.goods.service.edit'"
-        type="primary"
-        @click="confirm"
-        >更新</el-button
-      >
-    </el-footer>
-  </el-container>
-</template>
-<script setup>
-  import { cloneDeep } from 'lodash';
-  import { onMounted, reactive, ref, unref } from 'vue';
-  import { api } from '../goods.service';
-  const emit = defineEmits(['modalCallBack']);
-  const props = defineProps({
-    modal: {
-      type: Object,
-    },
-  });
-  // 添加 编辑 form
-  let formRef = ref(null);
-  const form = reactive({
-    model: {
-      name: '',
-      path: '',
-      description: '',
-    },
-    rules: {
-      name: [{ required: true, message: '请填写服务保障名称', trigger: 'blur' }],
-    },
-  });
-  const loading = ref(false);
-  // 获取详情
-  async function getDetail(id) {
-    loading.value = true;
-    const { error, data } = await api.service.detail(id);
-    error === 0 && (form.model = data);
-    loading.value = false;
-  }
-  // 表单关闭时提交
-  async function confirm() {
-    unref(formRef).validate(async (valid) => {
-      if (!valid) return;
-      let submitForm = cloneDeep(form.model);
-      const { error } =
-        props.modal.params.type == 'add'
-          ? await api.service.add(submitForm)
-          : await api.service.edit(props.modal.params.id, submitForm);
-      if (error == 0) {
-        emit('modalCallBack', { event: 'confirm' });
-      }
-    });
-  }
-  async function init() {
-    if (props.modal.params.id) {
-      await getDetail(props.modal.params.id);
-    }
-  }
-  onMounted(() => {
-    init();
-  });
-</script>

+ 0 - 155
src/app/shop/admin/goods/stockLog.vue

@@ -1,155 +0,0 @@
-<template>
-  <el-container class="panel-block">
-    <el-header class="sa-header">
-      <div class="sa-title sa-flex sa-row-between">
-        <div class="label sa-flex">
-          补货记录
-          <search-condition
-            :conditionLabel="filterParams.conditionLabel"
-            @deleteFilter="deleteFilter"
-          ></search-condition>
-        </div>
-        <div>
-          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
-          <el-button class="sa-button-refresh" icon="Search" @click="openFilter"></el-button>
-        </div>
-      </div>
-    </el-header>
-    <el-main class="sa-p-0" v-loading="loading">
-      <el-table
-        height="100%"
-        class="sa-table"
-        :data="table.data"
-        @sort-change="fieldFilter"
-        row-key="id"
-        stripe
-      >
-        <template #empty>
-          <sa-empty />
-        </template>
-        <el-table-column sortable="custom" prop="id" label="ID" min-width="80"></el-table-column>
-        <el-table-column label="商品" min-width="400">
-          <template #default="scope">
-            <goods-item
-              v-if="scope.row.goods"
-              :goods="{
-                image: scope.row.goods.image,
-                title: scope.row.goods.title,
-                sku_text: scope.row.goods_sku_text,
-              }"
-              mode="stockLog"
-            ></goods-item>
-            <div v-else>{{ scope.row.goods_id }}</div>
-          </template>
-        </el-table-column>
-        <el-table-column prop="before" label="补货前" min-width="106"></el-table-column>
-        <el-table-column prop="stock" label="补货库存" min-width="106"></el-table-column>
-        <el-table-column prop="msg" label="补货备注" min-width="124"></el-table-column>
-        <el-table-column prop="create_time" label="创建时间" width="172"></el-table-column>
-        <el-table-column prop="update_time" label="更新时间" width="172"></el-table-column>
-        <el-table-column label="操作人" min-width="140" fixed="right">
-          <template #default="scope">
-            <sa-user-profile type="oper" :user="scope.row.oper" :id="scope.row.admin_id" />
-          </template>
-        </el-table-column>
-      </el-table>
-    </el-main>
-    <sa-view-bar>
-      <template #right>
-        <sa-pagination :pageData="pageData" @updateFn="getData" />
-      </template>
-    </sa-view-bar>
-  </el-container>
-</template>
-<script>
-  export default {
-    name: 'shop.admin.goods.stocklog',
-  };
-</script>
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from './goods.service';
-  import { usePagination } from '@/sheep/hooks';
-  import { useSearch } from '@/sheep/components/sa-table/sa-search/useSearch';
-  import { composeFilter } from '@/sheep/utils';
-  import { cloneDeep } from 'lodash';
-  import GoodsItem from '@/app/shop/components/goods-item.vue';
-
-  const filterParams = reactive({
-    tools: {
-      'goods.title': {
-        type: 'tinput',
-        label: '商品名称',
-        field: 'goods.title',
-        value: '',
-      },
-    },
-    data: {
-      'goods.title': '',
-    },
-    conditionLabel: {},
-  });
-  const { openFilter, deleteFilter } = useSearch({ filterParams, getData });
-
-  const loading = ref(true);
-
-  // 表格
-  const table = reactive({
-    data: [],
-    order: '',
-    sort: '',
-  });
-
-  const { pageData } = usePagination();
-
-  // 获取数据
-  async function getData(page) {
-    loading.value = true;
-    if (page) pageData.page = page;
-    let tempSearch = cloneDeep(filterParams.data);
-    let search = composeFilter(tempSearch, {
-      'goods.title': 'like',
-    });
-    const { error, data } = await api.stockLog({
-      page: pageData.page,
-      list_rows: pageData.list_rows,
-      ...search,
-      order: table.order,
-      sort: table.sort,
-    });
-    if (error === 0) {
-      table.data = data.data;
-      pageData.page = data.current_page;
-      pageData.list_rows = data.per_page;
-      pageData.total = data.total;
-    }
-    loading.value = false;
-  }
-
-  // table 字段排序
-  function fieldFilter({ prop, order }) {
-    table.order = order == 'ascending' ? 'asc' : 'desc';
-    table.sort = prop;
-    getData();
-  }
-
-  onMounted(() => {
-    getData();
-  });
-</script>
-<style lang="scss">
-  .el-popover.admin-popper {
-    color: var(--sa-font);
-    font-size: 12px;
-    font-weight: 400;
-    .id {
-      color: var(--sa-subfont);
-    }
-    .label {
-      font-size: 12px;
-    }
-    .nickname {
-      font-weight: 500;
-    }
-  }
-</style>

+ 0 - 65
src/app/shop/admin/goods/stockWarning/addStock.vue

@@ -1,65 +0,0 @@
-<template>
-  <el-container class="addStock-page">
-    <el-main>
-      <el-form ref="addStockRef" :model="form.model" :rules="form.rules" label-width="84px">
-        <el-form-item label="商品库存:" prop="stock">
-          <div class="sa-flex">
-            <span class="stock">{{ modal.params.row.stock }}</span>
-            <el-input v-model="form.model.stock" placeholder="补充库存" type="number">
-              <template #append>件</template>
-            </el-input>
-          </div>
-        </el-form-item>
-      </el-form>
-    </el-main>
-    <el-footer class="sa-footer--submit">
-      <el-button type="primary" @click="confirm">确定</el-button>
-    </el-footer>
-  </el-container>
-</template>
-<script setup>
-  import { getCurrentInstance, reactive, unref, ref } from 'vue';
-  import { api } from '../goods.service';
-
-  const emit = defineEmits(['modalCallBack']);
-  const props = defineProps(['modal']);
-  const addStockRef = ref(null);
-  const { proxy } = getCurrentInstance();
-
-  const form = reactive({
-    model: {
-      stock: '',
-    },
-    rules: {
-      stock: [{ required: true, message: '请输入补充库存', trigger: 'blur' }],
-    },
-  });
-
-  function confirm() {
-    unref(addStockRef).validate(async (valid) => {
-      if (!valid) return;
-      if (valid) {
-        await api.stockWarning.addStock(props.modal.params.row.id, {
-          stock: form.model.stock,
-        });
-        emit('modalCallBack', { event: 'confirm' });
-      }
-    });
-  }
-</script>
-
-<style lang="scss">
-  .sa-dialog.addStock-modal {
-    --el-dialog-width: 400px;
-  }
-</style>
-<style lang="scss" scoped>
-  .addStock-page {
-    .stock {
-      flex-shrink: 0;
-      margin-right: 16px;
-      color: var(--sa-font);
-      font-size: 12px;
-    }
-  }
-</style>

+ 0 - 203
src/app/shop/admin/goods/stockWarning/index.vue

@@ -1,203 +0,0 @@
-<template>
-  <el-container class="panel-block">
-    <el-header class="sa-header">
-      <el-tabs class="sa-tabs" v-model="filterParams.data.stock_type" @tab-change="getData(1)">
-        <el-tab-pane
-          v-for="(sl, key) in typeList"
-          :key="sl"
-          :label="`${sl.name}${sl.num ? '(' + sl.num + ')' : ''}`"
-          :name="key"
-        ></el-tab-pane>
-      </el-tabs>
-      <div class="sa-title sa-flex sa-row-between">
-        <div class="label sa-flex">
-          库存预警
-          <search-condition
-            :conditionLabel="filterParams.conditionLabel"
-            @deleteFilter="deleteFilter"
-          ></search-condition>
-        </div>
-        <div>
-          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
-          <el-button class="sa-button-refresh" icon="Search" @click="openFilter"></el-button>
-          <el-button
-            v-auth="'shop.admin.goods.stockwarning.recyclebin'"
-            type="danger"
-            plain
-            @click="openRecyclebin"
-            >历史记录</el-button
-          >
-        </div>
-      </div>
-    </el-header>
-    <el-main class="sa-p-0" v-loading="loading">
-      <el-table
-        height="100%"
-        class="sa-table"
-        :data="table.data"
-        @sort-change="fieldFilter"
-        row-key="id"
-        stripe
-      >
-        <template #empty>
-          <sa-empty />
-        </template>
-        <el-table-column sortable="custom" prop="id" label="ID" min-width="80"></el-table-column>
-        <el-table-column label="商品" min-width="440">
-          <template #default="scope">
-            <goods-item
-              v-if="scope.row.goods"
-              :goods="{
-                image: scope.row.goods.image,
-                title: scope.row.goods.title,
-                sku_text: scope.row.goods_sku_text,
-              }"
-              mode="stockWarning"
-            ></goods-item>
-            <div v-else>{{ scope.row.goods_id }}</div>
-          </template>
-        </el-table-column>
-        <el-table-column prop="stock" label="库存" min-width="100"></el-table-column>
-        <el-table-column prop="stock_warning" label="预警库存" min-width="100"></el-table-column>
-        <el-table-column label="更新时间" width="172">
-          <template #default="scope">{{ scope.row.create_time || '-' }}</template>
-        </el-table-column>
-        <el-table-column label="操作" min-width="100" fixed="right">
-          <template #default="scope">
-            <el-button
-              v-auth="'shop.admin.goods.stockwarning.addstock'"
-              class="is-link"
-              type="primary"
-              @click="addStock(scope.row)"
-              >补货</el-button
-            >
-          </template>
-        </el-table-column>
-      </el-table>
-    </el-main>
-    <sa-view-bar>
-      <template #right>
-        <sa-pagination :pageData="pageData" @updateFn="getData" />
-      </template>
-    </sa-view-bar>
-  </el-container>
-</template>
-<script>
-  export default {
-    name: 'shop.admin.goods.stockwarning',
-  };
-</script>
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from '../goods.service';
-  import { useModal, usePagination } from '@/sheep/hooks';
-  import { useSearch } from '@/sheep/components/sa-table/sa-search/useSearch';
-  import { composeFilter } from '@/sheep/utils';
-  import GoodsItem from '@/app/shop/components/goods-item.vue';
-  import StockWarningAddStock from './addStock.vue';
-  import StockWarningRecyclebin from './recyclebin.vue';
-  import { cloneDeep } from 'lodash';
-
-  const typeList = reactive({
-    all: {
-      name: '全部',
-    },
-    over: { name: '已售罄', num: 0 },
-    no_enough: { name: '预警中', num: 0 },
-  });
-
-  const filterParams = reactive({
-    tools: {
-      stock_type: { label: '库存状态', value: 'all' },
-      'goods.title': {
-        type: 'tinput',
-        label: '商品名称',
-        field: 'goods.title',
-        value: '',
-      },
-    },
-    data: {
-      stock_type: 'all',
-      'goods.title': '',
-    },
-    conditionLabel: {},
-  });
-  const { openFilter, deleteFilter } = useSearch({ filterParams, getData });
-
-  const loading = ref(true);
-
-  // 表格
-  const table = reactive({
-    data: [],
-    order: '',
-    sort: '',
-  });
-
-  const { pageData } = usePagination();
-
-  // 获取数据
-  async function getData(page) {
-    loading.value = true;
-    if (page) pageData.page = page;
-    let tempSearch = cloneDeep(filterParams.data);
-    let search = composeFilter(tempSearch, {
-      'goods.title': 'like',
-    });
-    const { error, data } = await api.stockWarning.list({
-      page: pageData.page,
-      list_rows: pageData.list_rows,
-      ...search,
-      order: table.order,
-      sort: table.sort,
-    });
-    if (error === 0) {
-      table.data = data.rows.data;
-      pageData.page = data.rows.current_page;
-      pageData.list_rows = data.rows.per_page;
-      pageData.total = data.rows.total;
-      typeList.over.num = data.over_total;
-      typeList.no_enough.num = data.warning_total;
-    }
-    loading.value = false;
-  }
-  // table 字段排序
-  function fieldFilter({ prop, order }) {
-    table.order = order == 'ascending' ? 'asc' : 'desc';
-    table.sort = prop;
-    getData();
-  }
-
-  function addStock(row) {
-    useModal(
-      StockWarningAddStock,
-      {
-        title: '补充库存',
-        row: row,
-        class: 'addStock-modal',
-      },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-
-  function openRecyclebin() {
-    useModal(
-      StockWarningRecyclebin,
-      {
-        title: '历史记录',
-      },
-      {
-        confirm: () => {
-          getData();
-        },
-      },
-    );
-  }
-
-  onMounted(() => {
-    getData();
-  });
-</script>

+ 0 - 76
src/app/shop/admin/goods/stockWarning/recyclebin.vue

@@ -1,76 +0,0 @@
-<template>
-  <el-container class="recyclebin-view">
-    <el-main v-loading="loading">
-      <el-table :data="table.data" @sort-change="fieldFilter" class="sa-table" stripe>
-        <template #empty>
-          <sa-empty />
-        </template>
-        <el-table-column sortable="custom" prop="id" label="ID" min-width="100"></el-table-column>
-        <el-table-column label="名称" min-width="100">
-          <template #default="scope">
-            <div v-if="scope.row.goods" class="sa-table-line-1">
-              {{ scope.row.goods.title || '-' }}
-            </div>
-            <div v-else>{{ scope.row.goods_id }}</div>
-            <div></div>
-          </template>
-        </el-table-column>
-        <el-table-column
-          sortable="custom"
-          prop="delete_time"
-          label="补货时间"
-          min-width="172"
-        ></el-table-column>
-      </el-table>
-    </el-main>
-    <sa-view-bar>
-      <template #right>
-        <sa-pagination :pageData="pageData" @updateFn="getData" />
-      </template>
-    </sa-view-bar>
-  </el-container>
-</template>
-<script setup>
-  import { onMounted, reactive, ref } from 'vue';
-  import { api } from '../goods.service';
-  import { usePagination } from '@/sheep/hooks';
-
-  const loading = ref(true);
-
-  // 表格状态
-  const table = reactive({
-    data: [],
-    order: '',
-    sort: '',
-    selected: [],
-  });
-
-  const { pageData } = usePagination();
-
-  // 获取数据
-  async function getData() {
-    loading.value = true;
-    const { data } = await api.stockWarning.recyclebin({
-      page: pageData.page,
-      list_rows: pageData.list_rows,
-      order: table.order,
-      sort: table.sort,
-    });
-    table.data = data.data;
-    pageData.page = data.current_page;
-    pageData.list_rows = data.per_page;
-    pageData.total = data.total;
-    loading.value = false;
-  }
-
-  // table 字段排序
-  function fieldFilter({ prop, order }) {
-    table.order = order == 'ascending' ? 'asc' : 'desc';
-    table.sort = prop;
-    getData();
-  }
-
-  onMounted(() => {
-    getData();
-  });
-</script>

+ 132 - 0
src/app/shop/admin/marketing/group/edit.vue

@@ -0,0 +1,132 @@
+<template>
+  <el-container>
+    <el-main>
+      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
+        <el-form-item label="活动名称" prop="title">
+          <el-input v-model="form.model.title" placeholder="请填写活动名称"></el-input>
+        </el-form-item>
+        <el-form-item label="商品ID" prop="goods_id">
+          <el-input v-model="form.model.goods_id" placeholder="请填写商品ID"></el-input>
+        </el-form-item>
+        <el-form-item label="拼团人数" prop="people_num">
+          <el-input-number 
+            v-model="form.model.people_num" 
+            :min="2" 
+            :max="100"
+            placeholder="拼团人数"
+          />
+          <span style="margin-left: 10px;">人</span>
+        </el-form-item>
+        <el-form-item label="拼团价格" prop="group_price">
+          <el-input-number 
+            v-model="form.model.group_price" 
+            :min="0" 
+            :precision="2"
+            placeholder="拼团价格"
+          />
+          <span style="margin-left: 10px;">৳</span>
+        </el-form-item>
+        <el-form-item label="开始时间" prop="start_time">
+          <el-date-picker
+            v-model="form.model.start_time"
+            type="datetime"
+            placeholder="选择开始时间"
+            format="YYYY-MM-DD HH:mm:ss"
+            value-format="YYYY-MM-DD HH:mm:ss"
+          />
+        </el-form-item>
+        <el-form-item label="结束时间" prop="end_time">
+          <el-date-picker
+            v-model="form.model.end_time"
+            type="datetime"
+            placeholder="选择结束时间"
+            format="YYYY-MM-DD HH:mm:ss"
+            value-format="YYYY-MM-DD HH:mm:ss"
+          />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="form.model.status" placeholder="请选择状态">
+            <el-option label="启用" value="active"></el-option>
+            <el-option label="禁用" value="disabled"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="活动描述" prop="description">
+          <el-input 
+            v-model="form.model.description" 
+            type="textarea" 
+            :rows="3"
+            placeholder="请填写活动描述"
+          ></el-input>
+        </el-form-item>
+      </el-form>
+    </el-main>
+    <el-footer class="sa-footer--submit">
+      <el-button v-if="modal.params.type == 'add'" type="primary" @click="confirm">保存</el-button>
+      <el-button v-if="modal.params.type == 'edit'" type="primary" @click="confirm">更新</el-button>
+    </el-footer>
+  </el-container>
+</template>
+<script setup>
+  import { cloneDeep } from 'lodash';
+  import { onMounted, reactive, ref, unref } from 'vue';
+  import { api } from './group.service';
+  const emit = defineEmits(['modalCallBack']);
+  const props = defineProps({
+    modal: {
+      type: Object,
+    },
+  });
+  // 添加 编辑 form
+  let formRef = ref(null);
+  const form = reactive({
+    model: {
+      title: '',
+      goods_id: '',
+      people_num: 2,
+      group_price: 0,
+      start_time: '',
+      end_time: '',
+      status: 'active',
+      description: '',
+    },
+    rules: {
+      title: [{ required: true, message: '请填写活动名称', trigger: 'blur' }],
+      goods_id: [{ required: true, message: '请填写商品ID', trigger: 'blur' }],
+      people_num: [{ required: true, message: '请填写拼团人数', trigger: 'blur' }],
+      group_price: [{ required: true, message: '请填写拼团价格', trigger: 'blur' }],
+      start_time: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
+      end_time: [{ required: true, message: '请选择结束时间', trigger: 'change' }],
+      status: [{ required: true, message: '请选择状态', trigger: 'change' }],
+    },
+  });
+  const loading = ref(false);
+  // 获取详情
+  async function getDetail(id) {
+    loading.value = true;
+    const { error, data } = await api.detail(id);
+    error === 0 && (form.model = data);
+    loading.value = false;
+  }
+  // 表单关闭时提交
+  async function confirm() {
+    unref(formRef).validate(async (valid) => {
+      if (!valid) return;
+      let submitForm = cloneDeep(form.model);
+      const { error } =
+        props.modal.params.type == 'add'
+          ? await api.add(submitForm)
+          : await api.edit(props.modal.params.id, submitForm);
+      if (error == 0) {
+        emit('modalCallBack', { event: 'confirm' });
+      }
+    });
+  }
+  async function init() {
+    if (props.modal.params.id) {
+      await getDetail(props.modal.params.id);
+    }
+  }
+  onMounted(() => {
+    init();
+  });
+</script>

+ 17 - 0
src/app/shop/admin/marketing/group/group.service.js

@@ -0,0 +1,17 @@
+import { SELECT, CRUD } from '@/sheep/request/crud';
+
+const route = {
+  path: 'group',
+  name: 'shop.admin.marketing.group',
+  component: () => import('@/app/shop/admin/marketing/group/index.vue'),
+  meta: {
+    title: '拼团',
+  },
+};
+
+const api = {
+  ...CRUD('shop/admin/group_buy'),
+  select: (params) => SELECT('shop/admin/group_buy', params),
+};
+
+export { route, api };

+ 252 - 0
src/app/shop/admin/marketing/group/index.vue

@@ -0,0 +1,252 @@
+<template>
+  <el-container class="group-view panel-block">
+    <el-header class="sa-header">
+      <!-- 简化搜索组件 -->
+      <div class="search-container">
+        <sa-search-simple
+          :searchFields="searchFields"
+          :defaultValues="defaultSearchValues"
+          @search="(val) => getData(1, val)"
+          @reset="getData(1)"
+        >
+        </sa-search-simple>
+      </div>
+      <div class="sa-title sa-flex sa-row-between">
+        <div class="label sa-flex">拼团活动</div>
+        <div>
+          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
+          <el-button icon="Plus" type="primary" @click="addRow">新建</el-button>
+        </div>
+      </div>
+    </el-header>
+    <el-main class="sa-p-0">
+      <div class="sa-table-wrap panel-block panel-block--bottom" v-loading="loading">
+        <el-table
+          height="100%"
+          class="sa-table"
+          :data="table.data"
+          @selection-change="changeSelection"
+          @sort-change="fieldFilter"
+          @row-dblclick="editRow"
+          row-key="id"
+          stripe
+        >
+          <template #empty>
+            <sa-empty />
+          </template>
+          <el-table-column type="selection" width="48" align="center"></el-table-column>
+          <el-table-column prop="id" label="ID" min-width="100" sortable="custom">
+          </el-table-column>
+          <el-table-column label="活动名称" min-width="150">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                {{ scope.row.title || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="商品名称" min-width="150">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                {{ scope.row.goods_title || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="拼团人数" min-width="100">
+            <template #default="scope">
+              {{ scope.row.people_num || '-' }}人
+            </template>
+          </el-table-column>
+          <el-table-column label="拼团价格" min-width="120">
+            <template #default="scope">
+              ৳{{ scope.row.group_price || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="状态" min-width="100">
+            <template #default="scope">
+              <el-tag :type="scope.row.status === 'active' ? 'success' : 'danger'">
+                {{ scope.row.status_text || '-' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="开始时间" min-width="160">
+            <template #default="scope">
+              {{ scope.row.start_time || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="结束时间" min-width="160">
+            <template #default="scope">
+              {{ scope.row.end_time || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column fixed="right" label="操作" min-width="120">
+            <template #default="scope">
+              <el-button class="is-link" type="primary" @click="editRow(scope.row)">编辑</el-button>
+              <el-popconfirm
+                width="fit-content"
+                confirm-button-text="确认"
+                cancel-button-text="取消"
+                title="确认删除这条记录?"
+                @confirm="deleteApi(scope.row.id)"
+              >
+                <template #reference>
+                  <el-button class="is-link" type="danger"> 删除 </el-button>
+                </template>
+              </el-popconfirm>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </el-main>
+    <sa-view-bar>
+      <template #left>
+        <sa-batch-handle
+          :batchHandleTools="batchHandleTools"
+          :selectedLeng="table.selected.length"
+          @batchHandle="batchHandle"
+        ></sa-batch-handle>
+      </template>
+      <template #right>
+        <sa-pagination :pageData="pageData" @updateFn="getData" />
+      </template>
+    </sa-view-bar>
+  </el-container>
+</template>
+<script setup>
+  import { onMounted, reactive, ref } from 'vue';
+  import { api } from './group.service';
+  import { ElMessageBox } from 'element-plus';
+  import { useModal } from '@/sheep/hooks';
+  import { usePagination } from '@/sheep/hooks';
+  import groupEdit from './edit.vue';
+  const { pageData } = usePagination();
+
+  // 搜索字段配置
+  const searchFields = reactive({
+    title: {
+      type: 'input',
+      label: '活动名称',
+      placeholder: '请输入活动名称',
+      width: 200,
+    },
+  });
+  // 默认搜索值
+  const defaultSearchValues = reactive({
+    title: '',
+  });
+  // 列表
+  const table = reactive({
+    data: [],
+    order: '',
+    sort: '',
+    selected: [],
+  });
+  const loading = ref(true);
+  // 获取
+  async function getData(page, searchParams = {}) {
+    if (page) pageData.page = page;
+    loading.value = true;
+    const { error, data } = await api.list({
+      page: pageData.page,
+      list_rows: pageData.list_rows,
+      order: table.order,
+      ...searchParams,
+      sort: table.sort,
+    });
+    console.log('API 响应:', error, data);
+    if (error === 0) {
+      table.data = data.data;
+      pageData.page = data.current_page;
+      pageData.list_rows = data.per_page;
+      pageData.total = data.total;
+    }
+    loading.value = false;
+  }
+  // table 字段排序
+  function fieldFilter({ prop, order }) {
+    table.order = order == 'ascending' ? 'asc' : 'desc';
+    table.sort = prop;
+    getData();
+  }
+  //table批量选择
+  function changeSelection(row) {
+    table.selected = row;
+  }
+  // 分页/批量操作
+  const batchHandleTools = [
+    {
+      type: 'delete',
+      label: '删除',
+      auth: 'shop.admin.marketing.group.delete',
+      class: 'danger',
+    },
+  ];
+  function addRow() {
+    useModal(
+      groupEdit,
+      { title: '新建拼团', type: 'add' },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+  function editRow(row) {
+    useModal(
+      groupEdit,
+      {
+        title: '编辑拼团',
+        type: 'edit',
+        id: row.id,
+      },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+  // 删除api 单独批量可以直接调用
+  async function deleteApi(id) {
+    await api.delete(id);
+    getData();
+  }
+  async function batchHandle(type) {
+    let ids = [];
+    table.selected.forEach((row) => {
+      ids.push(row.id);
+    });
+    switch (type) {
+      case 'delete':
+        ElMessageBox.confirm('此操作将删除, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }).then(() => {
+          deleteApi(ids.join(','));
+        });
+        break;
+      default:
+        await api.edit(ids.join(','), {
+          status: type,
+        });
+        getData();
+    }
+  }
+
+  onMounted(() => {
+    getData();
+  });
+</script>
+<style lang="scss" scoped>
+  .group-view {
+    .el-header {
+      height: auto;
+    }
+    .el-main {
+      .sa-table-wrap {
+        height: 100%;
+      }
+    }
+  }
+</style>

+ 146 - 0
src/app/shop/admin/order/setting/edit.vue

@@ -0,0 +1,146 @@
+<template>
+  <el-container>
+    <el-main>
+      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
+        <el-form-item label="设置名称" prop="name">
+          <el-input v-model="form.model.name" placeholder="请填写设置名称"></el-input>
+        </el-form-item>
+        <el-form-item label="设置键" prop="key">
+          <el-input v-model="form.model.key" placeholder="请填写设置键,如:order_timeout"></el-input>
+        </el-form-item>
+        <el-form-item label="数据类型" prop="type">
+          <el-select v-model="form.model.type" placeholder="请选择数据类型" @change="handleTypeChange">
+            <el-option label="字符串" value="string"></el-option>
+            <el-option label="数字" value="number"></el-option>
+            <el-option label="布尔值" value="boolean"></el-option>
+            <el-option label="JSON" value="json"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="设置值" prop="value">
+          <el-input 
+            v-if="form.model.type === 'string'"
+            v-model="form.model.value" 
+            placeholder="请填写设置值"
+          ></el-input>
+          <el-input-number 
+            v-else-if="form.model.type === 'number'"
+            v-model="form.model.value" 
+            placeholder="请填写数字"
+            style="width: 100%"
+          />
+          <el-select 
+            v-else-if="form.model.type === 'boolean'"
+            v-model="form.model.value" 
+            placeholder="请选择布尔值"
+          >
+            <el-option label="是" value="true"></el-option>
+            <el-option label="否" value="false"></el-option>
+          </el-select>
+          <el-input 
+            v-else-if="form.model.type === 'json'"
+            v-model="form.model.value" 
+            type="textarea"
+            :rows="4"
+            placeholder="请填写JSON格式数据"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="描述" prop="description">
+          <el-input 
+            v-model="form.model.description" 
+            type="textarea" 
+            :rows="3"
+            placeholder="请填写设置描述"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="form.model.status" placeholder="请选择状态">
+            <el-option label="启用" value="active"></el-option>
+            <el-option label="禁用" value="disabled"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="排序" prop="sort">
+          <el-input v-model="form.model.sort" placeholder="请填写排序"></el-input>
+        </el-form-item>
+      </el-form>
+    </el-main>
+    <el-footer class="sa-footer--submit">
+      <el-button v-if="modal.params.type == 'add'" type="primary" @click="confirm">保存</el-button>
+      <el-button v-if="modal.params.type == 'edit'" type="primary" @click="confirm">更新</el-button>
+    </el-footer>
+  </el-container>
+</template>
+<script setup>
+  import { cloneDeep } from 'lodash';
+  import { onMounted, reactive, ref, unref } from 'vue';
+  import { api } from './setting.service';
+  const emit = defineEmits(['modalCallBack']);
+  const props = defineProps({
+    modal: {
+      type: Object,
+    },
+  });
+  // 添加 编辑 form
+  let formRef = ref(null);
+  const form = reactive({
+    model: {
+      name: '',
+      key: '',
+      value: '',
+      type: 'string',
+      description: '',
+      status: 'active',
+      sort: '0',
+    },
+    rules: {
+      name: [{ required: true, message: '请填写设置名称', trigger: 'blur' }],
+      key: [{ required: true, message: '请填写设置键', trigger: 'blur' }],
+      value: [{ required: true, message: '请填写设置值', trigger: 'blur' }],
+      type: [{ required: true, message: '请选择数据类型', trigger: 'change' }],
+      status: [{ required: true, message: '请选择状态', trigger: 'change' }],
+      sort: [{ required: true, message: '请填写排序', trigger: 'blur' }],
+    },
+  });
+  const loading = ref(false);
+  
+  // 处理类型变化
+  function handleTypeChange(type) {
+    // 重置值
+    if (type === 'number') {
+      form.model.value = 0;
+    } else if (type === 'boolean') {
+      form.model.value = 'true';
+    } else {
+      form.model.value = '';
+    }
+  }
+  
+  // 获取详情
+  async function getDetail(id) {
+    loading.value = true;
+    const { error, data } = await api.detail(id);
+    error === 0 && (form.model = data);
+    loading.value = false;
+  }
+  // 表单关闭时提交
+  async function confirm() {
+    unref(formRef).validate(async (valid) => {
+      if (!valid) return;
+      let submitForm = cloneDeep(form.model);
+      const { error } =
+        props.modal.params.type == 'add'
+          ? await api.add(submitForm)
+          : await api.edit(props.modal.params.id, submitForm);
+      if (error == 0) {
+        emit('modalCallBack', { event: 'confirm' });
+      }
+    });
+  }
+  async function init() {
+    if (props.modal.params.id) {
+      await getDetail(props.modal.params.id);
+    }
+  }
+  onMounted(() => {
+    init();
+  });
+</script>

+ 235 - 0
src/app/shop/admin/order/setting/index.vue

@@ -0,0 +1,235 @@
+<template>
+  <el-container class="order-setting-view panel-block">
+    <el-header class="sa-header">
+      <div class="sa-title sa-flex sa-row-between">
+        <div class="label sa-flex">订单设置</div>
+        <div>
+          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
+          <el-button icon="Plus" type="primary" @click="addRow">新建</el-button>
+        </div>
+      </div>
+    </el-header>
+    <el-main class="sa-p-0">
+      <div class="sa-table-wrap panel-block panel-block--bottom" v-loading="loading">
+        <el-table
+          height="100%"
+          class="sa-table"
+          :data="table.data"
+          @selection-change="changeSelection"
+          @sort-change="fieldFilter"
+          @row-dblclick="editRow"
+          row-key="id"
+          stripe
+        >
+          <template #empty>
+            <sa-empty />
+          </template>
+          <el-table-column type="selection" width="48" align="center"></el-table-column>
+          <el-table-column prop="id" label="ID" min-width="100" sortable="custom">
+          </el-table-column>
+          <el-table-column label="设置名称" min-width="150">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                {{ scope.row.name || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="设置键" min-width="150">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                {{ scope.row.key || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="设置值" min-width="200">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                {{ scope.row.value || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="数据类型" min-width="100">
+            <template #default="scope">
+              <el-tag :type="scope.row.type === 'string' ? 'primary' : 'success'">
+                {{ scope.row.type_text || '-' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="描述" min-width="200">
+            <template #default="scope">
+              <span class="sa-table-line-2">
+                {{ scope.row.description || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="状态" min-width="100">
+            <template #default="scope">
+              <el-tag :type="scope.row.status === 'active' ? 'success' : 'danger'">
+                {{ scope.row.status_text || '-' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="更新时间" min-width="160">
+            <template #default="scope">
+              {{ scope.row.update_time || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column fixed="right" label="操作" min-width="120">
+            <template #default="scope">
+              <el-button class="is-link" type="primary" @click="editRow(scope.row)">编辑</el-button>
+              <el-popconfirm
+                width="fit-content"
+                confirm-button-text="确认"
+                cancel-button-text="取消"
+                title="确认删除这条记录?"
+                @confirm="deleteApi(scope.row.id)"
+              >
+                <template #reference>
+                  <el-button class="is-link" type="danger"> 删除 </el-button>
+                </template>
+              </el-popconfirm>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </el-main>
+    <sa-view-bar>
+      <template #left>
+        <sa-batch-handle
+          :batchHandleTools="batchHandleTools"
+          :selectedLeng="table.selected.length"
+          @batchHandle="batchHandle"
+        ></sa-batch-handle>
+      </template>
+      <template #right>
+        <sa-pagination :pageData="pageData" @updateFn="getData" />
+      </template>
+    </sa-view-bar>
+  </el-container>
+</template>
+<script setup>
+  import { onMounted, reactive, ref } from 'vue';
+  import { api } from './setting.service';
+  import { ElMessageBox } from 'element-plus';
+  import { useModal } from '@/sheep/hooks';
+  import { usePagination } from '@/sheep/hooks';
+  import settingEdit from './edit.vue';
+  const { pageData } = usePagination();
+
+  // 列表
+  const table = reactive({
+    data: [],
+    order: '',
+    sort: '',
+    selected: [],
+  });
+  const loading = ref(true);
+  // 获取
+  async function getData(page, searchParams = {}) {
+    if (page) pageData.page = page;
+    loading.value = true;
+    const { error, data } = await api.list({
+      page: pageData.page,
+      list_rows: pageData.list_rows,
+      order: table.order,
+      ...searchParams,
+      sort: table.sort,
+    });
+    console.log('API 响应:', error, data);
+    if (error === 0) {
+      table.data = data.data;
+      pageData.page = data.current_page;
+      pageData.list_rows = data.per_page;
+      pageData.total = data.total;
+    }
+    loading.value = false;
+  }
+  // table 字段排序
+  function fieldFilter({ prop, order }) {
+    table.order = order == 'ascending' ? 'asc' : 'desc';
+    table.sort = prop;
+    getData();
+  }
+  //table批量选择
+  function changeSelection(row) {
+    table.selected = row;
+  }
+  // 分页/批量操作
+  const batchHandleTools = [
+    {
+      type: 'delete',
+      label: '删除',
+      auth: 'shop.admin.order.setting.delete',
+      class: 'danger',
+    },
+  ];
+  function addRow() {
+    useModal(
+      settingEdit,
+      { title: '新建设置', type: 'add' },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+  function editRow(row) {
+    useModal(
+      settingEdit,
+      {
+        title: '编辑设置',
+        type: 'edit',
+        id: row.id,
+      },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+  // 删除api 单独批量可以直接调用
+  async function deleteApi(id) {
+    await api.delete(id);
+    getData();
+  }
+  async function batchHandle(type) {
+    let ids = [];
+    table.selected.forEach((row) => {
+      ids.push(row.id);
+    });
+    switch (type) {
+      case 'delete':
+        ElMessageBox.confirm('此操作将删除, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }).then(() => {
+          deleteApi(ids.join(','));
+        });
+        break;
+      default:
+        await api.edit(ids.join(','), {
+          status: type,
+        });
+        getData();
+    }
+  }
+
+  onMounted(() => {
+    getData();
+  });
+</script>
+<style lang="scss" scoped>
+  .order-setting-view {
+    .el-header {
+      height: auto;
+    }
+    .el-main {
+      .sa-table-wrap {
+        height: 100%;
+      }
+    }
+  }
+</style>

+ 17 - 0
src/app/shop/admin/order/setting/setting.service.js

@@ -0,0 +1,17 @@
+import { SELECT, CRUD } from '@/sheep/request/crud';
+
+const route = {
+  path: 'setting',
+  name: 'shop.admin.order.setting',
+  component: () => import('@/app/shop/admin/order/setting/index.vue'),
+  meta: {
+    title: '订单设置',
+  },
+};
+
+const api = {
+  ...CRUD('shop/admin/order_setting'),
+  select: (params) => SELECT('shop/admin/order_setting', params),
+};
+
+export { route, api };

+ 106 - 0
src/app/shop/admin/user/level/edit.vue

@@ -0,0 +1,106 @@
+<template>
+  <el-container>
+    <el-main>
+      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
+        <el-form-item label="等级名称" prop="name">
+          <el-input v-model="form.model.name" placeholder="请填写等级名称"></el-input>
+        </el-form-item>
+        <el-form-item label="等级图标" prop="icon">
+          <el-input v-model="form.model.icon" placeholder="请填写图标地址"></el-input>
+        </el-form-item>
+        <el-form-item label="升级条件" prop="upgrade_amount">
+          <el-input-number 
+            v-model="form.model.upgrade_amount" 
+            :min="0" 
+            :precision="2"
+            placeholder="消费金额"
+          />
+          <span style="margin-left: 10px;">元</span>
+        </el-form-item>
+        <el-form-item label="折扣率" prop="discount">
+          <el-input-number 
+            v-model="form.model.discount" 
+            :min="1" 
+            :max="100"
+            placeholder="折扣率"
+          />
+          <span style="margin-left: 10px;">%</span>
+        </el-form-item>
+        <el-form-item label="描述" prop="description">
+          <el-input 
+            v-model="form.model.description" 
+            type="textarea" 
+            :rows="3"
+            placeholder="请填写等级描述"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="排序" prop="sort">
+          <el-input v-model="form.model.sort" placeholder="请填写排序"></el-input>
+        </el-form-item>
+      </el-form>
+    </el-main>
+    <el-footer class="sa-footer--submit">
+      <el-button v-if="modal.params.type == 'add'" type="primary" @click="confirm">保存</el-button>
+      <el-button v-if="modal.params.type == 'edit'" type="primary" @click="confirm">更新</el-button>
+    </el-footer>
+  </el-container>
+</template>
+<script setup>
+  import { cloneDeep } from 'lodash';
+  import { onMounted, reactive, ref, unref } from 'vue';
+  import { api } from './level.service';
+  const emit = defineEmits(['modalCallBack']);
+  const props = defineProps({
+    modal: {
+      type: Object,
+    },
+  });
+  // 添加 编辑 form
+  let formRef = ref(null);
+  const form = reactive({
+    model: {
+      name: '',
+      icon: '',
+      upgrade_amount: 0,
+      discount: 100,
+      description: '',
+      sort: '0',
+    },
+    rules: {
+      name: [{ required: true, message: '请填写等级名称', trigger: 'blur' }],
+      upgrade_amount: [{ required: true, message: '请填写升级条件', trigger: 'blur' }],
+      discount: [{ required: true, message: '请填写折扣率', trigger: 'blur' }],
+      sort: [{ required: true, message: '请填写排序', trigger: 'blur' }],
+    },
+  });
+  const loading = ref(false);
+  // 获取详情
+  async function getDetail(id) {
+    loading.value = true;
+    const { error, data } = await api.detail(id);
+    error === 0 && (form.model = data);
+    loading.value = false;
+  }
+  // 表单关闭时提交
+  async function confirm() {
+    unref(formRef).validate(async (valid) => {
+      if (!valid) return;
+      let submitForm = cloneDeep(form.model);
+      const { error } =
+        props.modal.params.type == 'add'
+          ? await api.add(submitForm)
+          : await api.edit(props.modal.params.id, submitForm);
+      if (error == 0) {
+        emit('modalCallBack', { event: 'confirm' });
+      }
+    });
+  }
+  async function init() {
+    if (props.modal.params.id) {
+      await getDetail(props.modal.params.id);
+    }
+  }
+  onMounted(() => {
+    init();
+  });
+</script>

+ 251 - 0
src/app/shop/admin/user/level/index.vue

@@ -0,0 +1,251 @@
+<template>
+  <el-container class="user-level-view panel-block">
+    <el-header class="sa-header">
+      <!-- 简化搜索组件 -->
+      <div class="search-container">
+        <sa-search-simple
+          :searchFields="searchFields"
+          :defaultValues="defaultSearchValues"
+          @search="(val) => getData(1, val)"
+          @reset="getData(1)"
+        >
+        </sa-search-simple>
+      </div>
+      <div class="sa-title sa-flex sa-row-between">
+        <div class="label sa-flex">会员等级</div>
+        <div>
+          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
+          <el-button icon="Plus" type="primary" @click="addRow">新建</el-button>
+        </div>
+      </div>
+    </el-header>
+    <el-main class="sa-p-0">
+      <div class="sa-table-wrap panel-block panel-block--bottom" v-loading="loading">
+        <el-table
+          height="100%"
+          class="sa-table"
+          :data="table.data"
+          @selection-change="changeSelection"
+          @sort-change="fieldFilter"
+          @row-dblclick="editRow"
+          row-key="id"
+          stripe
+        >
+          <template #empty>
+            <sa-empty />
+          </template>
+          <el-table-column type="selection" width="48" align="center"></el-table-column>
+          <el-table-column prop="id" label="ID" min-width="100" sortable="custom">
+          </el-table-column>
+          <el-table-column label="等级名称" min-width="120">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                {{ scope.row.name || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="等级图标" min-width="100">
+            <template #default="scope">
+              <el-image 
+                v-if="scope.row.icon"
+                :src="scope.row.icon" 
+                style="width: 40px; height: 40px"
+                fit="cover"
+              />
+              <span v-else>-</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="升级条件" min-width="150">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                消费满 {{ scope.row.upgrade_amount || 0 }} 元
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="折扣率" min-width="100">
+            <template #default="scope">
+              {{ scope.row.discount || 100 }}%
+            </template>
+          </el-table-column>
+          <el-table-column label="排序" min-width="100">
+            <template #default="scope">
+              {{ scope.row.sort || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="创建时间" min-width="160">
+            <template #default="scope">
+              {{ scope.row.create_time || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column fixed="right" label="操作" min-width="120">
+            <template #default="scope">
+              <el-button class="is-link" type="primary" @click="editRow(scope.row)">编辑</el-button>
+              <el-popconfirm
+                width="fit-content"
+                confirm-button-text="确认"
+                cancel-button-text="取消"
+                title="确认删除这条记录?"
+                @confirm="deleteApi(scope.row.id)"
+              >
+                <template #reference>
+                  <el-button class="is-link" type="danger"> 删除 </el-button>
+                </template>
+              </el-popconfirm>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </el-main>
+    <sa-view-bar>
+      <template #left>
+        <sa-batch-handle
+          :batchHandleTools="batchHandleTools"
+          :selectedLeng="table.selected.length"
+          @batchHandle="batchHandle"
+        ></sa-batch-handle>
+      </template>
+      <template #right>
+        <sa-pagination :pageData="pageData" @updateFn="getData" />
+      </template>
+    </sa-view-bar>
+  </el-container>
+</template>
+<script setup>
+  import { onMounted, reactive, ref } from 'vue';
+  import { api } from './level.service';
+  import { ElMessageBox } from 'element-plus';
+  import { useModal } from '@/sheep/hooks';
+  import { usePagination } from '@/sheep/hooks';
+  import levelEdit from './edit.vue';
+  const { pageData } = usePagination();
+
+  // 搜索字段配置
+  const searchFields = reactive({
+    name: {
+      type: 'input',
+      label: '等级名称',
+      placeholder: '请输入等级名称',
+      width: 200,
+    },
+  });
+  // 默认搜索值
+  const defaultSearchValues = reactive({
+    name: '',
+  });
+  // 列表
+  const table = reactive({
+    data: [],
+    order: '',
+    sort: '',
+    selected: [],
+  });
+  const loading = ref(true);
+  // 获取
+  async function getData(page, searchParams = {}) {
+    if (page) pageData.page = page;
+    loading.value = true;
+    const { error, data } = await api.list({
+      page: pageData.page,
+      list_rows: pageData.list_rows,
+      order: table.order,
+      ...searchParams,
+      sort: table.sort,
+    });
+    console.log('API 响应:', error, data);
+    if (error === 0) {
+      table.data = data.data;
+      pageData.page = data.current_page;
+      pageData.list_rows = data.per_page;
+      pageData.total = data.total;
+    }
+    loading.value = false;
+  }
+  // table 字段排序
+  function fieldFilter({ prop, order }) {
+    table.order = order == 'ascending' ? 'asc' : 'desc';
+    table.sort = prop;
+    getData();
+  }
+  //table批量选择
+  function changeSelection(row) {
+    table.selected = row;
+  }
+  // 分页/批量操作
+  const batchHandleTools = [
+    {
+      type: 'delete',
+      label: '删除',
+      auth: 'shop.admin.user.level.delete',
+      class: 'danger',
+    },
+  ];
+  function addRow() {
+    useModal(
+      levelEdit,
+      { title: '新建等级', type: 'add' },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+  function editRow(row) {
+    useModal(
+      levelEdit,
+      {
+        title: '编辑等级',
+        type: 'edit',
+        id: row.id,
+      },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+  // 删除api 单独批量可以直接调用
+  async function deleteApi(id) {
+    await api.delete(id);
+    getData();
+  }
+  async function batchHandle(type) {
+    let ids = [];
+    table.selected.forEach((row) => {
+      ids.push(row.id);
+    });
+    switch (type) {
+      case 'delete':
+        ElMessageBox.confirm('此操作将删除, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }).then(() => {
+          deleteApi(ids.join(','));
+        });
+        break;
+      default:
+        await api.edit(ids.join(','), {
+          status: type,
+        });
+        getData();
+    }
+  }
+
+  onMounted(() => {
+    getData();
+  });
+</script>
+<style lang="scss" scoped>
+  .user-level-view {
+    .el-header {
+      height: auto;
+    }
+    .el-main {
+      .sa-table-wrap {
+        height: 100%;
+      }
+    }
+  }
+</style>

+ 17 - 0
src/app/shop/admin/user/level/level.service.js

@@ -0,0 +1,17 @@
+import { SELECT, CRUD } from '@/sheep/request/crud';
+
+const route = {
+  path: 'level',
+  name: 'shop.admin.user.level',
+  component: () => import('@/app/shop/admin/user/level/index.vue'),
+  meta: {
+    title: '会员等级',
+  },
+};
+
+const api = {
+  ...CRUD('shop/admin/user_level'),
+  select: (params) => SELECT('shop/admin/user_level', params),
+};
+
+export { route, api };

+ 89 - 0
src/app/shop/admin/user/list/edit.vue

@@ -0,0 +1,89 @@
+<template>
+  <el-container>
+    <el-main>
+      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
+        <el-form-item label="用户名" prop="username">
+          <el-input v-model="form.model.username" placeholder="请填写用户名"></el-input>
+        </el-form-item>
+        <el-form-item label="昵称" prop="nickname">
+          <el-input v-model="form.model.nickname" placeholder="请填写昵称"></el-input>
+        </el-form-item>
+        <el-form-item label="手机号" prop="mobile">
+          <el-input v-model="form.model.mobile" placeholder="请填写手机号"></el-input>
+        </el-form-item>
+        <el-form-item label="邮箱" prop="email">
+          <el-input v-model="form.model.email" placeholder="请填写邮箱"></el-input>
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="form.model.status" placeholder="请选择状态">
+            <el-option label="正常" value="active"></el-option>
+            <el-option label="禁用" value="disabled"></el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+    </el-main>
+    <el-footer class="sa-footer--submit">
+      <el-button v-if="modal.params.type == 'add'" type="primary" @click="confirm">保存</el-button>
+      <el-button v-if="modal.params.type == 'edit'" type="primary" @click="confirm">更新</el-button>
+    </el-footer>
+  </el-container>
+</template>
+<script setup>
+  import { cloneDeep } from 'lodash';
+  import { onMounted, reactive, ref, unref } from 'vue';
+  import { api } from './list.service';
+  const emit = defineEmits(['modalCallBack']);
+  const props = defineProps({
+    modal: {
+      type: Object,
+    },
+  });
+  // 添加 编辑 form
+  let formRef = ref(null);
+  const form = reactive({
+    model: {
+      username: '',
+      nickname: '',
+      mobile: '',
+      email: '',
+      status: 'active',
+    },
+    rules: {
+      username: [{ required: true, message: '请填写用户名', trigger: 'blur' }],
+      nickname: [{ required: true, message: '请填写昵称', trigger: 'blur' }],
+      mobile: [{ required: true, message: '请填写手机号', trigger: 'blur' }],
+      email: [{ required: true, message: '请填写邮箱', trigger: 'blur' }],
+      status: [{ required: true, message: '请选择状态', trigger: 'change' }],
+    },
+  });
+  const loading = ref(false);
+  // 获取详情
+  async function getDetail(id) {
+    loading.value = true;
+    const { error, data } = await api.detail(id);
+    error === 0 && (form.model = data);
+    loading.value = false;
+  }
+  // 表单关闭时提交
+  async function confirm() {
+    unref(formRef).validate(async (valid) => {
+      if (!valid) return;
+      let submitForm = cloneDeep(form.model);
+      const { error } =
+        props.modal.params.type == 'add'
+          ? await api.add(submitForm)
+          : await api.edit(props.modal.params.id, submitForm);
+      if (error == 0) {
+        emit('modalCallBack', { event: 'confirm' });
+      }
+    });
+  }
+  async function init() {
+    if (props.modal.params.id) {
+      await getDetail(props.modal.params.id);
+    }
+  }
+  onMounted(() => {
+    init();
+  });
+</script>

+ 254 - 0
src/app/shop/admin/user/list/index.vue

@@ -0,0 +1,254 @@
+<template>
+  <el-container class="user-list-view panel-block">
+    <el-header class="sa-header">
+      <!-- 简化搜索组件 -->
+      <div class="search-container">
+        <sa-search-simple
+          :searchFields="searchFields"
+          :defaultValues="defaultSearchValues"
+          @search="(val) => getData(1, val)"
+          @reset="getData(1)"
+        >
+        </sa-search-simple>
+      </div>
+      <div class="sa-title sa-flex sa-row-between">
+        <div class="label sa-flex">用户列表</div>
+        <div>
+          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
+          <el-button icon="Plus" type="primary" @click="addRow">新建</el-button>
+        </div>
+      </div>
+    </el-header>
+    <el-main class="sa-p-0">
+      <div class="sa-table-wrap panel-block panel-block--bottom" v-loading="loading">
+        <el-table
+          height="100%"
+          class="sa-table"
+          :data="table.data"
+          @selection-change="changeSelection"
+          @sort-change="fieldFilter"
+          @row-dblclick="editRow"
+          row-key="id"
+          stripe
+        >
+          <template #empty>
+            <sa-empty />
+          </template>
+          <el-table-column type="selection" width="48" align="center"></el-table-column>
+          <el-table-column prop="id" label="ID" min-width="100" sortable="custom">
+          </el-table-column>
+          <el-table-column label="用户名" min-width="120">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                {{ scope.row.username || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="昵称" min-width="120">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                {{ scope.row.nickname || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="手机号" min-width="120">
+            <template #default="scope">
+              {{ scope.row.mobile || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="邮箱" min-width="160">
+            <template #default="scope">
+              {{ scope.row.email || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="状态" min-width="100">
+            <template #default="scope">
+              <el-tag :type="scope.row.status === 'active' ? 'success' : 'danger'">
+                {{ scope.row.status_text || '-' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="创建时间" min-width="160">
+            <template #default="scope">
+              {{ scope.row.create_time || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column fixed="right" label="操作" min-width="120">
+            <template #default="scope">
+              <el-button class="is-link" type="primary" @click="editRow(scope.row)">编辑</el-button>
+              <el-popconfirm
+                width="fit-content"
+                confirm-button-text="确认"
+                cancel-button-text="取消"
+                title="确认删除这条记录?"
+                @confirm="deleteApi(scope.row.id)"
+              >
+                <template #reference>
+                  <el-button class="is-link" type="danger"> 删除 </el-button>
+                </template>
+              </el-popconfirm>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </el-main>
+    <sa-view-bar>
+      <template #left>
+        <sa-batch-handle
+          :batchHandleTools="batchHandleTools"
+          :selectedLeng="table.selected.length"
+          @batchHandle="batchHandle"
+        ></sa-batch-handle>
+      </template>
+      <template #right>
+        <sa-pagination :pageData="pageData" @updateFn="getData" />
+      </template>
+    </sa-view-bar>
+  </el-container>
+</template>
+<script setup>
+  import { onMounted, reactive, ref } from 'vue';
+  import { api } from './list.service';
+  import { ElMessageBox } from 'element-plus';
+  import { useModal } from '@/sheep/hooks';
+  import { usePagination } from '@/sheep/hooks';
+  import userEdit from './edit.vue';
+  const { pageData } = usePagination();
+
+  // 搜索字段配置
+  const searchFields = reactive({
+    username: {
+      type: 'input',
+      label: '用户名',
+      placeholder: '请输入用户名',
+      width: 200,
+    },
+    mobile: {
+      type: 'input',
+      label: '手机号',
+      placeholder: '请输入手机号',
+      width: 200,
+    },
+  });
+  // 默认搜索值
+  const defaultSearchValues = reactive({
+    username: '',
+    mobile: '',
+  });
+  // 列表
+  const table = reactive({
+    data: [],
+    order: '',
+    sort: '',
+    selected: [],
+  });
+  const loading = ref(true);
+  // 获取
+  async function getData(page, searchParams = {}) {
+    if (page) pageData.page = page;
+    loading.value = true;
+    const { error, data } = await api.list({
+      page: pageData.page,
+      list_rows: pageData.list_rows,
+      order: table.order,
+      ...searchParams,
+      sort: table.sort,
+    });
+    console.log('API 响应:', error, data);
+    if (error === 0) {
+      table.data = data.data;
+      pageData.page = data.current_page;
+      pageData.list_rows = data.per_page;
+      pageData.total = data.total;
+    }
+    loading.value = false;
+  }
+  // table 字段排序
+  function fieldFilter({ prop, order }) {
+    table.order = order == 'ascending' ? 'asc' : 'desc';
+    table.sort = prop;
+    getData();
+  }
+  //table批量选择
+  function changeSelection(row) {
+    table.selected = row;
+  }
+  // 分页/批量操作
+  const batchHandleTools = [
+    {
+      type: 'delete',
+      label: '删除',
+      auth: 'shop.admin.user.list.delete',
+      class: 'danger',
+    },
+  ];
+  function addRow() {
+    useModal(
+      userEdit,
+      { title: '新建用户', type: 'add' },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+  function editRow(row) {
+    useModal(
+      userEdit,
+      {
+        title: '编辑用户',
+        type: 'edit',
+        id: row.id,
+      },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+  // 删除api 单独批量可以直接调用
+  async function deleteApi(id) {
+    await api.delete(id);
+    getData();
+  }
+  async function batchHandle(type) {
+    let ids = [];
+    table.selected.forEach((row) => {
+      ids.push(row.id);
+    });
+    switch (type) {
+      case 'delete':
+        ElMessageBox.confirm('此操作将删除, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }).then(() => {
+          deleteApi(ids.join(','));
+        });
+        break;
+      default:
+        await api.edit(ids.join(','), {
+          status: type,
+        });
+        getData();
+    }
+  }
+
+  onMounted(() => {
+    getData();
+  });
+</script>
+<style lang="scss" scoped>
+  .user-list-view {
+    .el-header {
+      height: auto;
+    }
+    .el-main {
+      .sa-table-wrap {
+        height: 100%;
+      }
+    }
+  }
+</style>

+ 17 - 0
src/app/shop/admin/user/list/list.service.js

@@ -0,0 +1,17 @@
+import { SELECT, CRUD } from '@/sheep/request/crud';
+
+const route = {
+  path: 'list',
+  name: 'shop.admin.user.list',
+  component: () => import('@/app/shop/admin/user/list/index.vue'),
+  meta: {
+    title: '用户列表',
+  },
+};
+
+const api = {
+  ...CRUD('shop/admin/user'),
+  select: (params) => SELECT('shop/admin/user', params),
+};
+
+export { route, api };

+ 26 - 37
src/app/shop/admin/feedback/edit.vue → src/app/shop/admin/user/tag/edit.vue

@@ -2,50 +2,35 @@
   <el-container>
     <el-main>
       <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="100px">
-        <el-form-item label="反馈用户">
-          <div class="sa-flex" v-if="form.model.user">
-            <sa-user-profile :user="form.model.user" :id="form.model.user_id" />
-          </div>
+        <el-form-item label="标签名称" prop="name">
+          <el-input v-model="form.model.name" placeholder="请填写标签名称"></el-input>
         </el-form-item>
-        <el-form-item label="反馈类型">
-          {{ form.model.type }}
+        <el-form-item label="标签颜色" prop="color">
+          <el-color-picker v-model="form.model.color" placeholder="请选择标签颜色"></el-color-picker>
         </el-form-item>
-        <el-form-item label="反馈内容">
-          {{ form.model.content }}
+        <el-form-item label="描述" prop="description">
+          <el-input 
+            v-model="form.model.description" 
+            type="textarea" 
+            :rows="3"
+            placeholder="请填写标签描述"
+          ></el-input>
         </el-form-item>
-        <el-form-item label="截图">
-          <div class="sa-flex">
-            <div>
-              <sa-preview :url="form.model.images" size="30"></sa-preview>
-            </div>
-          </div>
-        </el-form-item>
-        <el-form-item label="联系电话">
-          {{ form.model.phone }}
-        </el-form-item>
-        <el-form-item label="是否处理">
-          <el-radio-group v-model="form.model.status">
-            <el-radio label="1">已处理</el-radio>
-            <el-radio label="0">待处理</el-radio>
-          </el-radio-group>
-        </el-form-item>
-        <el-form-item label="系统备注">
-          <el-input v-model="form.model.remark" placeholder="请输入系统备注"></el-input>
+        <el-form-item label="排序" prop="sort">
+          <el-input v-model="form.model.sort" placeholder="请填写排序"></el-input>
         </el-form-item>
       </el-form>
     </el-main>
     <el-footer class="sa-footer--submit">
-      <el-button v-auth="'shop.admin.feedback.edit'" type="primary" @click="confirm"
-        >更新</el-button
-      >
+      <el-button v-if="modal.params.type == 'add'" type="primary" @click="confirm">保存</el-button>
+      <el-button v-if="modal.params.type == 'edit'" type="primary" @click="confirm">更新</el-button>
     </el-footer>
   </el-container>
 </template>
 <script setup>
-  import { onMounted, reactive, ref, unref } from 'vue';
-  import { api } from './feedback.service';
   import { cloneDeep } from 'lodash';
-
+  import { onMounted, reactive, ref, unref } from 'vue';
+  import { api } from './tag.service';
   const emit = defineEmits(['modalCallBack']);
   const props = defineProps({
     modal: {
@@ -56,11 +41,16 @@
   let formRef = ref(null);
   const form = reactive({
     model: {
-      remark: '',
-      status: '0',
-      images: [],
+      name: '',
+      color: '#409EFF',
+      description: '',
+      sort: '0',
+    },
+    rules: {
+      name: [{ required: true, message: '请填写标签名称', trigger: 'blur' }],
+      color: [{ required: true, message: '请选择标签颜色', trigger: 'change' }],
+      sort: [{ required: true, message: '请填写排序', trigger: 'blur' }],
     },
-    rules: {},
   });
   const loading = ref(false);
   // 获取详情
@@ -72,7 +62,6 @@
   }
   // 表单关闭时提交
   async function confirm() {
-    // 表单验证
     unref(formRef).validate(async (valid) => {
       if (!valid) return;
       let submitForm = cloneDeep(form.model);

+ 55 - 45
src/app/shop/admin/goods/service/index.vue → src/app/shop/admin/user/tag/index.vue

@@ -1,17 +1,21 @@
 <template>
-  <el-container class="service-view panel-block">
+  <el-container class="user-tag-view panel-block">
     <el-header class="sa-header">
+      <!-- 简化搜索组件 -->
+      <div class="search-container">
+        <sa-search-simple
+          :searchFields="searchFields"
+          :defaultValues="defaultSearchValues"
+          @search="(val) => getData(1, val)"
+          @reset="getData(1)"
+        >
+        </sa-search-simple>
+      </div>
       <div class="sa-title sa-flex sa-row-between">
-        <div class="label sa-flex">服务保障</div>
+        <div class="label sa-flex">标签管理</div>
         <div>
           <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
-          <el-button
-            v-auth="'shop.admin.goods.service.add'"
-            icon="Plus"
-            type="primary"
-            @click="addRow"
-            >新建</el-button
-          >
+          <el-button icon="Plus" type="primary" @click="addRow">新建</el-button>
         </div>
       </div>
     </el-header>
@@ -33,44 +37,40 @@
           <el-table-column type="selection" width="48" align="center"></el-table-column>
           <el-table-column prop="id" label="ID" min-width="100" sortable="custom">
           </el-table-column>
-          <el-table-column label="名称" min-width="120">
+          <el-table-column label="标签名称" min-width="120">
             <template #default="scope">
               <span class="sa-table-line-1">
                 {{ scope.row.name || '-' }}
               </span>
             </template>
           </el-table-column>
-          <el-table-column label="服务标识" min-width="100">
+          <el-table-column label="标签颜色" min-width="120">
             <template #default="scope">
-              <sa-preview :url="scope.row.image" size="30"></sa-preview>
+              <el-tag :color="scope.row.color">
+                {{ scope.row.color || '-' }}
+              </el-tag>
             </template>
           </el-table-column>
-          <el-table-column label="说明" min-width="170">
+          <el-table-column label="描述" min-width="200">
             <template #default="scope">
-              <div class="sa-table-line-1">
+              <span class="sa-table-line-1">
                 {{ scope.row.description || '-' }}
-              </div>
+              </span>
             </template>
           </el-table-column>
-          <el-table-column label="创建时间" min-width="160">
+          <el-table-column label="排序" min-width="100">
             <template #default="scope">
-              {{ scope.row.create_time || '-' }}
+              {{ scope.row.sort || '-' }}
             </template>
           </el-table-column>
-          <el-table-column label="更新时间" min-width="160">
+          <el-table-column label="创建时间" min-width="160">
             <template #default="scope">
-              {{ scope.row.update_time || '-' }}
+              {{ scope.row.create_time || '-' }}
             </template>
           </el-table-column>
           <el-table-column fixed="right" label="操作" min-width="120">
             <template #default="scope">
-              <el-button
-                v-auth="'shop.admin.goods.service.detail'"
-                class="is-link"
-                type="primary"
-                @click="editRow(scope.row)"
-                >编辑</el-button
-              >
+              <el-button class="is-link" type="primary" @click="editRow(scope.row)">编辑</el-button>
               <el-popconfirm
                 width="fit-content"
                 confirm-button-text="确认"
@@ -79,13 +79,7 @@
                 @confirm="deleteApi(scope.row.id)"
               >
                 <template #reference>
-                  <el-button
-                    v-auth="'shop.admin.goods.service.delete'"
-                    class="is-link"
-                    type="danger"
-                  >
-                    删除
-                  </el-button>
+                  <el-button class="is-link" type="danger"> 删除 </el-button>
                 </template>
               </el-popconfirm>
             </template>
@@ -109,13 +103,26 @@
 </template>
 <script setup>
   import { onMounted, reactive, ref } from 'vue';
-  import { api } from '../goods.service';
+  import { api } from './tag.service';
   import { ElMessageBox } from 'element-plus';
   import { useModal } from '@/sheep/hooks';
   import { usePagination } from '@/sheep/hooks';
-  import ServiceEdit from './edit.vue';
+  import tagEdit from './edit.vue';
   const { pageData } = usePagination();
 
+  // 搜索字段配置
+  const searchFields = reactive({
+    name: {
+      type: 'input',
+      label: '标签名称',
+      placeholder: '请输入标签名称',
+      width: 200,
+    },
+  });
+  // 默认搜索值
+  const defaultSearchValues = reactive({
+    name: '',
+  });
   // 列表
   const table = reactive({
     data: [],
@@ -125,14 +132,17 @@
   });
   const loading = ref(true);
   // 获取
-  async function getData() {
+  async function getData(page, searchParams = {}) {
+    if (page) pageData.page = page;
     loading.value = true;
-    const { error, data } = await api.service.list({
+    const { error, data } = await api.list({
       page: pageData.page,
       list_rows: pageData.list_rows,
       order: table.order,
+      ...searchParams,
       sort: table.sort,
     });
+    console.log('API 响应:', error, data);
     if (error === 0) {
       table.data = data.data;
       pageData.page = data.current_page;
@@ -156,14 +166,14 @@
     {
       type: 'delete',
       label: '删除',
-      auth: 'shop.admin.goods.service.delete',
+      auth: 'shop.admin.user.tag.delete',
       class: 'danger',
     },
   ];
   function addRow() {
     useModal(
-      ServiceEdit,
-      { title: '新建', type: 'add' },
+      tagEdit,
+      { title: '新建标签', type: 'add' },
       {
         confirm: () => {
           getData();
@@ -173,9 +183,9 @@
   }
   function editRow(row) {
     useModal(
-      ServiceEdit,
+      tagEdit,
       {
-        title: '编辑',
+        title: '编辑标签',
         type: 'edit',
         id: row.id,
       },
@@ -188,7 +198,7 @@
   }
   // 删除api 单独批量可以直接调用
   async function deleteApi(id) {
-    await api.service.delete(id);
+    await api.delete(id);
     getData();
   }
   async function batchHandle(type) {
@@ -207,7 +217,7 @@
         });
         break;
       default:
-        await api.service.edit(ids.join(','), {
+        await api.edit(ids.join(','), {
           status: type,
         });
         getData();
@@ -219,7 +229,7 @@
   });
 </script>
 <style lang="scss" scoped>
-  .service-view {
+  .user-tag-view {
     .el-header {
       height: auto;
     }

+ 17 - 0
src/app/shop/admin/user/tag/tag.service.js

@@ -0,0 +1,17 @@
+import { SELECT, CRUD } from '@/sheep/request/crud';
+
+const route = {
+  path: 'tag',
+  name: 'shop.admin.user.tag',
+  component: () => import('@/app/shop/admin/user/tag/index.vue'),
+  meta: {
+    title: '标签管理',
+  },
+};
+
+const api = {
+  ...CRUD('shop/admin/user_tag'),
+  select: (params) => SELECT('shop/admin/user_tag', params),
+};
+
+export { route, api };

Some files were not shown because too many files changed in this diff