Explorar o código

feat:新增消息推送模块,crud_v2

叶静 hai 1 día
pai
achega
dd9217dfe8

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

@@ -1,5 +1,6 @@
 import Content from '@/sheep/layouts/content.vue';
-import { SELECT, CRUD } from '@/sheep/request/crud';
+import { SELECT, CRUD, CRUD_V2 } from '@/sheep/request/crud';
+import { request } from '@/sheep/request';
 
 const route = {
   path: 'content',
@@ -45,8 +46,14 @@ const api = {
 
   // 消息推送相关 API
   notification: {
-    ...CRUD('shop/admin/notification'),
-    select: (params) => SELECT('shop/admin/notification', params),
+    // 使用新版本CRUD规范(V2)
+    ...CRUD_V2('mall/notice'),
+    sendNotice: (data) =>
+      request({
+        url: 'mall/notice/sendNotice',
+        method: 'POST',
+        data,
+      }),
   },
 
   // 短信相关 API

+ 60 - 89
src/app/shop/admin/content/notification/edit.vue

@@ -1,47 +1,15 @@
 <template>
-  <el-container>
+  <el-container v-loading="loading" element-loading-text="加载中...">
     <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 label="消息内容" prop="message">
+          <el-input v-model="form.model.message" type="textarea" :rows="5" placeholder="请填写消息内容"></el-input>
         </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 label="页面路径" prop="pages">
+          <el-input v-model="form.model.pages" placeholder="请填写页面路径(可选)"></el-input>
         </el-form-item>
       </el-form>
     </el-main>
@@ -52,63 +20,66 @@
   </el-container>
 </template>
 <script setup>
-  import { cloneDeep } from 'lodash';
-  import { onMounted, reactive, ref, unref } from 'vue';
-  import { api } from '../content.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 { code, data } = await api.detail(id);
-    code == '200' && (form.model = data);
-    loading.value = false;
+import { cloneDeep } from 'lodash';
+import { onMounted, reactive, ref, unref } from 'vue';
+import { api } from '../content.service';
+const emit = defineEmits(['modalCallBack']);
+const props = defineProps({
+  modal: {
+    type: Object,
+  },
+});
+// 添加 编辑 form
+let formRef = ref(null);
+const form = reactive({
+  model: {
+    title: '',
+    message: '',
+    pages: '',
+  },
+  rules: {
+    title: [{ required: true, message: '请填写消息标题', trigger: 'blur' }],
+    message: [{ required: true, message: '请填写消息内容', trigger: 'blur' }],
+  },
+});
+const loading = ref(false);
+// 获取详情
+async function getDetail(id) {
+  loading.value = true;
+  try {
+    const { code, data } = await api.notification.detail(id);
+    if (code == 200) {
+      form.model = data;
+    }
+  } catch (error) {
+    console.error('获取详情失败:', error);
   }
-  // 表单关闭时提交
-  async function confirm() {
-    unref(formRef).validate(async (valid) => {
-      if (!valid) return;
-      let submitForm = cloneDeep(form.model);
+  loading.value = false;
+}
+// 表单关闭时提交
+async function confirm() {
+  unref(formRef).validate(async (valid) => {
+    if (!valid) return;
+    let submitForm = cloneDeep(form.model);
+    try {
       const { code } =
         props.modal.params.type == 'add'
-          ? await api.add(submitForm)
-          : await api.edit(props.modal.params.id, submitForm);
-      if (code == '200') {
+          ? await api.notification.add(submitForm)
+          : await api.notification.edit(props.modal.params.id, submitForm);
+      if (code == 200) {
         emit('modalCallBack', { event: 'confirm' });
       }
-    });
-  }
-  async function init() {
-    if (props.modal.params.id) {
-      await getDetail(props.modal.params.id);
+    } catch (error) {
+      console.error('提交失败:', error);
     }
-  }
-  onMounted(() => {
-    init();
   });
+}
+async function init() {
+  if (props.modal.params.id) {
+    await getDetail(props.modal.params.id);
+  }
+}
+onMounted(() => {
+  init();
+});
 </script>

+ 61 - 77
src/app/shop/admin/content/notification/index.vue

@@ -17,12 +17,11 @@
     </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-key="id" stripe>
+        <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 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">
@@ -32,38 +31,34 @@
               </span>
             </template>
           </el-table-column>
-          <el-table-column label="消息类型" min-width="120">
+          <el-table-column label="消息内容" min-width="200">
             <template #default="scope">
-              <el-tag :type="scope.row.type === 'system' ? 'primary' : 'success'">
-                {{ scope.row.type_text || '-' }}
-              </el-tag>
+              <span class="sa-table-line-2">
+                {{ scope.row.message || '-' }}
+              </span>
             </template>
           </el-table-column>
-          <el-table-column label="推送对象" min-width="120">
+          <el-table-column label="页面路径" min-width="150">
             <template #default="scope">
-              {{ scope.row.target_text || '-' }}
+              <span class="sa-table-line-1">
+                {{ scope.row.pages || '-' }}
+              </span>
             </template>
           </el-table-column>
-          <el-table-column label="状态" min-width="100">
+          <el-table-column label="创建时间" min-width="160">
             <template #default="scope">
-              <el-tag :type="scope.row.status === 'sent' ? 'success' : 'warning'">
-                {{ scope.row.status_text || '-' }}
-              </el-tag>
+              {{ scope.row.createTime || '-' }}
             </template>
           </el-table-column>
-          <el-table-column label="发送时间" min-width="160">
+          <el-table-column label="更新时间" min-width="160">
             <template #default="scope">
-              {{ scope.row.send_time || '-' }}
+              {{ scope.row.updateTime || '-' }}
             </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">
+          <el-table-column fixed="right" label="操作" min-width="180">
             <template #default="scope">
               <el-button class="is-link" type="primary" @click="editRow(scope.row)">编辑</el-button>
+              <el-button class="is-link" type="success" @click="sendRow(scope.row)">发送</el-button>
               <el-popconfirm width="fit-content" confirm-button-text="确认" cancel-button-text="取消" title="确认删除这条记录?"
                 @confirm="deleteApi(scope.row.id)">
                 <template #reference>
@@ -76,10 +71,6 @@
       </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>
@@ -89,10 +80,11 @@
 <script setup>
 import { onMounted, reactive, ref } from 'vue';
 import { api } from '../content.service';
-import { ElMessageBox } from 'element-plus';
+import { ElMessage } from 'element-plus';
 import { useModal } from '@/sheep/hooks';
 import { usePagination } from '@/sheep/hooks';
 import notificationEdit from './edit.vue';
+import notificationSend from './send.vue';
 const { pageData } = usePagination();
 
 // 搜索字段配置
@@ -117,7 +109,6 @@ const table = reactive({
   data: [],
   order: '',
   sort: '',
-  selected: [],
 });
 const loading = ref(true);
 // 获取数据
@@ -128,19 +119,21 @@ async function getData(page, searchParams = null) {
   // 构建请求参数 - 优先使用传入的参数,否则使用双向绑定的搜索条件
   const finalSearchParams = searchParams !== null ? searchParams : currentSearchParams.value;
 
-  const { code, data } = await api.list({
-    page: pageData.page,
-    size: pageData.size,
-    order: table.order,
-    ...finalSearchParams,
-    sort: table.sort,
-  });
-  console.log('API 响应:', error, data);
-  if (code == 200) {
-    table.data = data.data;
-    pageData.page = data.current_page;
-    pageData.size = data.per_page;
-    pageData.total = data.total;
+  try {
+    const { code, data } = await api.notification.list({
+      page: pageData.page,
+      size: pageData.size,
+      queryStr: finalSearchParams.title || '',
+    });
+    if (code == 200) {
+      table.data = data.records || [];
+      pageData.page = data.current || 1;
+      pageData.size = data.size || 10;
+      pageData.total = data.total || 0;
+    }
+  } catch (error) {
+    console.error('获取数据失败:', error);
+    table.data = [];
   }
   loading.value = false;
 }
@@ -150,19 +143,7 @@ function fieldFilter({ prop, order }) {
   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,
@@ -189,36 +170,39 @@ function editRow(row) {
     },
   );
 }
-// 删除api 单独批量可以直接调用
-async function deleteApi(id) {
-  await api.delete(id);
-  getData();
+
+function sendRow(row) {
+  useModal(
+    notificationSend,
+    {
+      title: '发送消息',
+      id: row.id,
+      templateData: row,
+    },
+    {
+      confirm: () => {
+        // 发送成功后可以刷新数据或显示提示
+        console.log('消息发送完成');
+      },
+    },
+  );
 }
-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,
-      });
+// 删除api
+async function deleteApi(id) {
+  try {
+    const { code } = await api.notification.delete(id);
+    if (code == 200) {
+      ElMessage.success('删除成功');
       getData();
+    }
+  } catch (error) {
+    console.error('删除失败:', error);
   }
 }
 
+
 // 搜索处理
-const handleSearch = (searchParams) => {
+const handleSearch = () => {
   // 由于使用了 v-model,currentSearchParams 会自动更新
   // 直接调用 getData,会自动使用当前的搜索条件
   getData(1);

+ 127 - 0
src/app/shop/admin/content/notification/send.vue

@@ -0,0 +1,127 @@
+<template>
+  <el-container>
+    <el-main>
+      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="120px">
+        <el-form-item label="消息标题">
+          <el-input v-model="templateTitle" readonly />
+        </el-form-item>
+
+        <el-form-item label="消息内容">
+          <el-input v-model="templateMessage" type="textarea" :rows="3" readonly />
+        </el-form-item>
+
+        <el-form-item label="页面路径" v-if="templatePages">
+          <el-input v-model="templatePages" readonly />
+        </el-form-item>
+
+        <el-form-item label="接收用户" prop="uids">
+          <el-button type="primary" @click="openUserSelect" plain>
+            <el-icon>
+              <Plus />
+            </el-icon>
+            选择用户 ({{ form.model.uids.length }})
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </el-main>
+    <el-footer class="sa-footer--submit">
+      <el-button type="primary" @click="confirm" :loading="sending">发送消息</el-button>
+    </el-footer>
+  </el-container>
+</template>
+
+<script setup>
+import { reactive, ref, computed } from 'vue';
+import { ElMessage } from 'element-plus';
+import { Plus } from '@element-plus/icons-vue';
+import { useModal } from '@/sheep/hooks';
+import { api } from '../content.service';
+import UserSelect from '@/app/shop/admin/user/list/select.vue';
+
+const emit = defineEmits(['modalCallBack']);
+const props = defineProps({
+  modal: {
+    type: Object,
+  },
+});
+
+const formRef = ref(null);
+const sending = ref(false);
+
+// 模板数据计算属性
+const templateTitle = computed(() => props.modal.params.templateData?.title || '');
+const templateMessage = computed(() => props.modal.params.templateData?.message || '');
+const templatePages = computed(() => props.modal.params.templateData?.pages || '');
+
+const form = reactive({
+  model: {
+    id: props.modal.params.id,
+    uids: [],
+  },
+  rules: {
+    uids: [
+      { required: true, message: '请选择接收用户', trigger: 'blur' },
+      {
+        validator: (_, value, callback) => {
+          if (!value || value.length === 0) {
+            callback(new Error('请选择至少一个用户'));
+          } else {
+            callback();
+          }
+        },
+        trigger: 'blur'
+      }
+    ],
+  },
+});
+
+// 打开用户选择弹窗
+function openUserSelect() {
+  useModal(
+    UserSelect,
+    {
+      title: '选择用户',
+      multiple: true,
+      ids: form.model.uids
+    },
+    {
+      confirm: (data) => {
+        if (data && Array.isArray(data)) {
+          form.model.uids = data.map(user => user.id);
+        }
+      }
+    }
+  );
+}
+
+// 发送消息
+async function confirm() {
+  formRef.value.validate(async (valid) => {
+    if (!valid) return;
+
+    sending.value = true;
+    try {
+      const { code, message } = await api.notification.sendNotice({
+        id: form.model.id,
+        uids: form.model.uids,
+      });
+
+      if (code == 200) {
+        ElMessage.success('消息发送成功');
+        emit('modalCallBack', { event: 'confirm' });
+      }
+    } catch (error) {
+      console.error('发送失败:', error);
+    }
+    sending.value = false;
+  });
+}
+</script>
+
+<style lang="scss" scoped>
+.el-main {
+  .el-form {
+    max-width: 100%;
+  }
+}
+</style>

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

@@ -8,12 +8,6 @@ const { t } = i18n.global;
 
 // 财务模块公共配置
 const financeConfig = {
-  // 渠道配置
-  channels: {
-    1: 'TKPAY',
-    2: '3QPAY',
-  },
-
   // 币种配置
   currencies: {
     1: 'BDT',
@@ -58,9 +52,6 @@ const financeConfig = {
 
 // 公共工具函数
 const financeUtils = {
-  // 获取渠道文本
-  getChannelText: (channel) => financeConfig.channels[channel] || t('common.unknown'),
-
   // 获取币种文本
   getCurrencyText: (currency) => financeConfig.currencies[currency] || 'BDT',
 

+ 2 - 14
src/app/shop/admin/finance/recharge/detail.vue

@@ -48,7 +48,7 @@
           <el-col :span="6">
             <div class="info-item" :style="{ '--label-width': formLabelWidth }">
               <span class="label">{{ t('modules.recharge.channel') }}:</span>
-              <span class="value">{{ getChannelText(rechargeDetail.channel) }}</span>
+              <span class="value">{{ rechargeDetail.channelName || '--' }}</span>
             </div>
           </el-col>
           <el-col :span="6">
@@ -62,7 +62,7 @@
           <el-col :span="6">
             <div class="info-item" :style="{ '--label-width': formLabelWidth }">
               <span class="label">{{ t('modules.recharge.method') }}:</span>
-              <span class="value">{{ getMethodText(rechargeDetail.method) }}</span>
+              <span class="value">{{ rechargeDetail.methodName || '--' }}</span>
             </div>
           </el-col>
           <el-col :span="6">
@@ -173,21 +173,9 @@ const operationLogs = reactive({
 // 使用公共配置的函数
 const getStatusType = (status) => financeUtils.getStatusType(status, 'recharge');
 const getStatusText = financeUtils.getRechargeStatusText;
-const getChannelText = financeUtils.getChannelText;
 const getCurrencyText = financeUtils.getCurrencyText;
 const getLogTypeText = financeUtils.getLogTypeText;
 
-// 充值方式文本函数(临时定义,等后端确认后更新)
-const getMethodText = (method) => {
-  const methodMap = {
-    1: t('modules.recharge.bankTransfer'),
-    2: t('modules.recharge.onlinePayment'),
-    3: t('modules.recharge.mobilePayment'),
-    4: t('modules.recharge.walletPayment'),
-  };
-  return methodMap[method] || t('common.unknown');
-};
-
 // 获取充值详情
 async function getRechargeDetail() {
   if (!props.modal?.params?.id) return;

+ 2 - 29
src/app/shop/admin/finance/recharge/index.vue

@@ -51,16 +51,12 @@
           </el-table-column>
           <el-table-column :label="t('modules.recharge.channel')" min-width="100" align="center">
             <template #default="scope">
-              <el-tag :type="getChannelType(scope.row.channel)">
-                {{ getChannelText(scope.row.channel) }}
-              </el-tag>
+              {{ scope.row.channelName || '-' }}
             </template>
           </el-table-column>
           <el-table-column :label="t('modules.recharge.method')" min-width="100" align="center">
             <template #default="scope">
-              <el-tag :type="getMethodType(scope.row.method)">
-                {{ getMethodText(scope.row.method) }}
-              </el-tag>
+              {{ scope.row.methodName || '-' }}
             </template>
           </el-table-column>
           <el-table-column :label="t('modules.recharge.currency')" min-width="80" align="center">
@@ -194,31 +190,8 @@ const loading = ref(true);
 // 使用公共配置的函数
 const getStatusType = (status) => financeUtils.getStatusType(status, 'recharge');
 const getStatusText = financeUtils.getRechargeStatusText;
-const getChannelType = financeUtils.getChannelType;
-const getChannelText = financeUtils.getChannelText;
 const getCurrencyText = financeUtils.getCurrencyText;
 
-// 充值方式相关函数(临时定义,等后端确认后更新)
-const getMethodType = (method) => {
-  const typeMap = {
-    1: 'primary',
-    2: 'success',
-    3: 'warning',
-    4: 'info',
-  };
-  return typeMap[method] || '';
-};
-
-const getMethodText = (method) => {
-  const methodMap = {
-    1: t('modules.recharge.bankTransfer'),
-    2: t('modules.recharge.onlinePayment'),
-    3: t('modules.recharge.mobilePayment'),
-    4: t('modules.recharge.walletPayment'),
-  };
-  return methodMap[method] || t('common.unknown');
-};
-
 // 获取数据
 async function getData(page, searchParams = {}) {
   if (page) pageData.page = page;

+ 3 - 18
src/app/shop/admin/finance/withdraw/detail.vue

@@ -8,7 +8,7 @@
           <!-- 操作按钮 -->
           <div class="operation-buttons mb-40px" v-if="withdrawDetail.status === 1">
             <el-button type="success" plain @click="handleApprove" class="mr-10px">{{ t('modules.withdraw.approve')
-              }}</el-button>
+            }}</el-button>
             <el-button type="danger" plain @click="handleReject">{{ t('modules.withdraw.reject') }}</el-button>
           </div>
         </div>
@@ -64,9 +64,7 @@
           <el-col :span="6">
             <div class="info-item" :style="{ '--label-width': formLabelWidth }">
               <span class="label">{{ t('modules.withdraw.channel') }}:</span>
-              <el-tag :type="getChannelType(withdrawDetail.channel)">
-                {{ getChannelText(withdrawDetail.channel) }}
-              </el-tag>
+              <span class="value">{{ withdrawDetail.channelName || '--' }}</span>
             </div>
           </el-col>
         </el-row>
@@ -74,7 +72,7 @@
           <el-col :span="6">
             <div class="info-item" :style="{ '--label-width': formLabelWidth }">
               <span class="label">{{ t('modules.withdraw.method') }}:</span>
-              <span class="value">{{ getMethodText(withdrawDetail.method) }}</span>
+              <span class="value">{{ withdrawDetail.methodName || '--' }}</span>
             </div>
           </el-col>
           <el-col :span="6">
@@ -190,8 +188,6 @@ const operationLogs = reactive({
 const getStatusType = (status) => financeUtils.getStatusType(status, 'withdraw');
 const getStatusText = financeUtils.getWithdrawStatusText;
 const getAccountTypeText = financeUtils.getAccountTypeText;
-const getChannelType = financeUtils.getChannelType;
-const getChannelText = financeUtils.getChannelText;
 const getCurrencyText = financeUtils.getCurrencyText;
 const getLogTypeText = financeUtils.getLogTypeText;
 
@@ -205,17 +201,6 @@ const getAccountTypeTextLocal = (accountType) => {
   return typeFn ? typeFn() : t('common.unknown');
 };
 
-// 提款方式文本函数(临时定义,等后端确认后更新)
-const getMethodText = (method) => {
-  const methodMap = {
-    1: t('modules.withdraw.bankTransfer'),
-    2: t('modules.withdraw.onlinePayment'),
-    3: t('modules.withdraw.mobilePayment'),
-    4: t('modules.withdraw.walletPayment'),
-  };
-  return methodMap[method] || t('common.unknown');
-};
-
 // 获取提款详情
 async function getWithdrawDetail() {
   if (!props.modal?.params?.id) return;

+ 2 - 29
src/app/shop/admin/finance/withdraw/index.vue

@@ -59,16 +59,12 @@
           </el-table-column>
           <el-table-column :label="t('modules.withdraw.channel')" min-width="130" align="center">
             <template #default="scope">
-              <el-tag :type="getChannelType(scope.row.channel)">
-                {{ getChannelText(scope.row.channel) }}
-              </el-tag>
+              {{ scope.row.channelName || '-' }}
             </template>
           </el-table-column>
           <el-table-column :label="t('modules.withdraw.method')" min-width="130" align="center">
             <template #default="scope">
-              <el-tag :type="getMethodType(scope.row.method)">
-                {{ getMethodText(scope.row.method) }}
-              </el-tag>
+              {{ scope.row.methodName || '-' }}
             </template>
           </el-table-column>
           <el-table-column :label="t('modules.withdraw.currency')" min-width="80" align="center">
@@ -216,32 +212,9 @@ const loading = ref(true);
 // 使用公共配置的函数
 const getStatusType = (status) => financeUtils.getStatusType(status, 'withdraw');
 const getStatusText = financeUtils.getWithdrawStatusText;
-const getChannelType = financeUtils.getChannelType;
-const getChannelText = financeUtils.getChannelText;
 const getCurrencyText = financeUtils.getCurrencyText;
 const getAccountTypeText = financeUtils.getAccountTypeText;
 
-// 提款方式相关函数(临时定义,等后端确认后更新)
-const getMethodType = (method) => {
-  const typeMap = {
-    1: 'primary',
-    2: 'success',
-    3: 'warning',
-    4: 'info',
-  };
-  return typeMap[method] || '';
-};
-
-const getMethodText = (method) => {
-  const methodMap = {
-    1: t('modules.withdraw.bankTransfer'),
-    2: t('modules.withdraw.onlinePayment'),
-    3: t('modules.withdraw.mobilePayment'),
-    4: t('modules.withdraw.walletPayment'),
-  };
-  return methodMap[method] || t('common.unknown');
-};
-
 // 获取数据
 async function getData(page, searchParams = {}) {
   if (page) pageData.page = page;

+ 3 - 3
src/app/shop/admin/order/order.service.js

@@ -26,9 +26,9 @@ export const ORDER_STATUS = {
 
   // 拼团状态
   PINK_STATUS: {
-    0: { text: '未开团', type: 'info' },
-    1: { text: '开团成功', type: 'success' },
-    2: { text: '开团失败', type: 'danger' },
+    1: { text: '未开奖', type: 'warning' },
+    2: { text: '已开奖', type: 'success' },
+    3: { text: '开奖失败', type: 'danger' },
   },
 
   // 中奖状态

+ 37 - 1
src/app/shop/admin/user/list/detail.vue

@@ -222,9 +222,13 @@
                   <div>{{ row.payTime || '-' }}</div>
                 </template>
               </el-table-column>
-              <el-table-column :label="t('common.actions')" min-width="80" align="center">
+              <el-table-column :label="t('common.actions')" min-width="120" align="center">
                 <template #default="{ row }">
                   <el-button type="primary" link @click="viewOrder(row)">{{ t('common.detail') }}</el-button>
+                  <el-button v-if="row.status === 3 && row.pinkStatus === 1" type="danger" link
+                    @click="handleRefund(row)" class="ml-2">
+                    {{ t('common.refund') }}
+                  </el-button>
                 </template>
               </el-table-column>
             </el-table>
@@ -439,6 +443,7 @@
 
 <script setup>
 import { ref, reactive, onMounted } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
 import { useI18n } from 'vue-i18n';
 import { useModal, usePagination } from '@/sheep/hooks';
 import { api, userUtils } from '../user.service';
@@ -782,6 +787,37 @@ const viewSubordinateUser = (row) => openDetailModal('subordinateUser', row);
 const viewRecharge = (row) => openDetailModal('recharge', row);
 const viewWithdraw = (row) => openDetailModal('withdraw', row);
 
+// 处理退款
+const handleRefund = async (row) => {
+  try {
+    // 确认退款操作
+    await ElMessageBox.confirm(
+      `确定要对订单 ${row.orderId} 进行退款吗?`,
+      '退款确认',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      }
+    );
+
+    // 调用退款API
+    const { code, message } = await orderApi.order.refund(row.id);
+    if (code === '200' || code === 200) {
+      ElMessage.success('退款操作成功');
+      // 刷新订单数据
+      await loadTabData('orders');
+    } else {
+      ElMessage.error(message || '退款操作失败');
+    }
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('退款操作失败:', error);
+      ElMessage.error('退款操作失败');
+    }
+  }
+};
+
 onMounted(() => {
   // 先初始化基本信息表格数据(防止模板报错)
   initBasicInfoTableData();

+ 278 - 0
src/app/shop/admin/user/list/select.vue

@@ -0,0 +1,278 @@
+<template>
+  <el-container class="user-select">
+    <el-container>
+      <el-header class="user-search">
+        <sa-search-simple :searchFields="searchFields" :defaultValues="defaultSearchValues"
+          @search="(val) => getData(1, val)" @reset="getData(1)" />
+      </el-header>
+      <el-main v-loading="loading">
+        <el-table class="sa-table" ref="multipleTableRef" :data="table.list" @select="selectRow" @select-all="selectAll"
+          stripe>
+          <template #empty>
+            <sa-empty />
+          </template>
+          <el-table-column v-if="modal.params.multiple" type="selection" width="48"></el-table-column>
+          <el-table-column prop="id" label="用户ID" align="center" width="80" />
+          <el-table-column label="用户信息" min-width="200">
+            <template #default="scope">
+              <div class="user-info">
+                <el-avatar :src="scope.row.headPic" :size="40">
+                  <el-icon>
+                    <User />
+                  </el-icon>
+                </el-avatar>
+                <div class="user-details">
+                  <div class="user-name">{{ scope.row.nickname || scope.row.name || '未设置昵称' }}</div>
+                  <div class="user-phone">{{ scope.row.phoneNo || '未绑定手机' }}</div>
+                </div>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column label="注册时间" align="center">
+            <template #default="scope">
+              {{ scope.row.createTime || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="状态" align="center" width="100">
+            <template #default="scope">
+              <el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
+                {{ scope.row.status === 1 ? '正常' : '禁用' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column v-if="!modal.params.multiple" label="操作" width="80">
+            <template #default="scope">
+              <el-button link type="primary" @click="singleSelect(scope.row)">
+                选择
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-main>
+      <el-footer class="sa-footer--submit" :class="modal.params.multiple ? 'sa-row-between' : 'sa-row-right'">
+        <sa-pagination :pageData="pageData" layout="total, prev, pager, next" @updateFn="getData" />
+        <el-button v-if="modal.params.multiple" type="primary" @click="confirm">
+          确认选择
+        </el-button>
+      </el-footer>
+    </el-container>
+  </el-container>
+</template>
+
+<script setup>
+import { nextTick, onMounted, reactive, ref } from 'vue';
+import { User } from '@element-plus/icons-vue';
+import { ElMessage } from 'element-plus';
+import { usePagination } from '@/sheep/hooks';
+import { api } from '../user.service';
+
+const { pageData } = usePagination();
+
+const emit = defineEmits(['modalCallBack']);
+const props = defineProps(['modal']);
+
+// 搜索字段配置
+const searchFields = reactive({
+  name: {
+    type: 'input',
+    label: '用户账号',
+    placeholder: '请输入用户账号',
+    width: 150,
+  },
+  phone: {
+    type: 'input',
+    label: '用户手机',
+    placeholder: '请输入手机号',
+    width: 150,
+  },
+});
+
+// 默认搜索值
+const defaultSearchValues = reactive({
+  name: '',
+  phone: '',
+});
+
+const loading = ref(true);
+const table = reactive({
+  list: [],
+  ids: props.modal.params.ids || [],
+  selectedUsers: new Map(),
+});
+
+async function getData(page, searchParams = {}) {
+  if (page) pageData.page = page;
+  loading.value = true;
+
+  try {
+    const params = {
+      page: pageData.page,
+      size: pageData.size,
+    };
+
+    // 添加搜索参数
+    if (searchParams.name) {
+      params.name = searchParams.name;
+    }
+    if (searchParams.phone) {
+      params.phone = searchParams.phone;
+    }
+
+    const { code, data } = await api.list.list(params);
+    if (code == 200) {
+      table.list = data.list || [];
+      pageData.page = data.current || 1;
+      pageData.size = data.size || 10;
+      pageData.total = data.total || 0;
+
+      nextTick(() => {
+        setDefaultSelected();
+        initSelectedUsers();
+      });
+    }
+  } catch (error) {
+    console.error('获取用户列表失败:', error);
+    table.list = [];
+  } finally {
+    loading.value = false;
+  }
+}
+
+// 设置默认选中
+function setDefaultSelected() {
+  if (!table.list || !Array.isArray(table.list)) {
+    return;
+  }
+
+  table.list.forEach((item) => {
+    if (table.ids?.includes(item.id)) {
+      multipleTableRef.value?.toggleRowSelection(item, true);
+    }
+  });
+}
+
+const multipleTableRef = ref();
+
+function selectRow(_, row) {
+  if (table.ids.includes(row.id)) {
+    let index = table.ids.findIndex((id) => id == row.id);
+    table.ids.splice(index, 1);
+    table.selectedUsers.delete(row.id);
+  } else {
+    table.ids.push(row.id);
+    addToSelectedUsers(row);
+  }
+}
+
+function selectAll(selection) {
+  if (selection.length == 0) {
+    // 取消全选
+    table.list.forEach((l) => {
+      if (table.ids.includes(l.id)) {
+        let index = table.ids.findIndex((id) => id == l.id);
+        table.ids.splice(index, 1);
+        table.selectedUsers.delete(l.id);
+      }
+    });
+  } else {
+    // 全选
+    table.list.forEach((l) => {
+      if (!table.ids.includes(l.id)) {
+        table.ids.push(l.id);
+        addToSelectedUsers(l);
+      }
+    });
+  }
+}
+
+// 添加用户到选中数据集合
+function addToSelectedUsers(item) {
+  const userData = {
+    id: item.id,
+    nickname: item.nickname || item.name,
+    phone: item.phoneNo,
+    avatar: item.headPic,
+    status: item.status,
+    name: item.name,
+  };
+  table.selectedUsers.set(item.id, userData);
+}
+
+// 初始化时添加已选中的用户数据
+function initSelectedUsers() {
+  table.list.forEach((item) => {
+    if (table.ids.includes(item.id)) {
+      addToSelectedUsers(item);
+    }
+  });
+}
+
+function singleSelect(user) {
+  const userData = {
+    id: user.id,
+    nickname: user.nickname || user.name,
+    phone: user.phoneNo,
+    avatar: user.headPic,
+    status: user.status,
+    name: user.name,
+  };
+
+  emit('modalCallBack', {
+    event: 'confirm',
+    data: [userData],
+  });
+}
+
+function confirm() {
+  const selectedUsersData = Array.from(table.selectedUsers.values());
+
+  emit('modalCallBack', {
+    event: 'confirm',
+    data: selectedUsersData,
+  });
+}
+
+onMounted(() => {
+  getData();
+});
+</script>
+
+<style lang="scss" scoped>
+.user-select {
+  .user-search {
+    --el-header-height: auto;
+    padding-top: var(--sa-padding);
+  }
+
+  .sa-footer--submit {
+    height: auto;
+    padding: 16px;
+    border-top: 1px solid var(--sa-border);
+    background: #fff;
+    display: flex;
+    align-items: center;
+    flex-wrap: wrap;
+    --el-footer-height: auto;
+    min-height: 60px;
+  }
+
+  .user-info {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+
+    .user-details {
+      .user-name {
+        font-size: 14px;
+        color: #333;
+        margin-bottom: 4px;
+      }
+
+      .user-phone {
+        font-size: 12px;
+        color: #666;
+      }
+    }
+  }
+}
+</style>

+ 27 - 2
src/app/shop/admin/user/user.service.js

@@ -39,8 +39,33 @@ const route = {
 };
 
 const api = {
-  // 分离分页参数和其他参数
-  // 用户列表相关 API
+  // 用户相关 API
+  user: {
+    // 用户列表
+    list: (params) =>
+      request({
+        url: '/cif/user/list',
+        method: 'GET',
+        params,
+      }),
+    // 用户详情
+    detail: (userId) =>
+      request({
+        url: '/cif/user/detail',
+        method: 'GET',
+        params: { userId },
+      }),
+    // 其他用户相关API
+    ...CRUD('/cif/user', ['edit', 'delete', 'report']),
+    myUsers: (data) =>
+      request({
+        url: '/cif/user/myUsers',
+        method: 'POST',
+        data,
+      }),
+  },
+
+  // 保持原有的 list API 以兼容现有代码
   list: {
     ...CRUD('/cif/user', ['list', 'detail', 'edit', 'delete', 'report']),
     userDetail: (userId) =>

+ 173 - 48
src/sheep/request/crud.js

@@ -2,69 +2,161 @@ import { request } from './index';
 import $storage from '@/sheep/utils/storage';
 import { ElMessage } from 'element-plus';
 
-// 检测是否为mall前缀的接口
-const isMallAPI = (url) => {
-  return url.startsWith('/mall') || url.startsWith('mall');
+// CRUD 配置常量
+const CRUD_VERSIONS = {
+  V1: 'v1', // 旧版本 CRUD
+  V2: 'v2', // 新版本 CRUD
 };
 
-// 查看列表
-export const LIST = (url, data) => {
-  const requestConfig = {
-    url: url + `/list`,
-    method: 'POST',
-  };
-  requestConfig.data = data;
-  return request(requestConfig);
+// 默认 CRUD 配置
+const DEFAULT_CRUD_CONFIG = {
+  version: CRUD_VERSIONS.V1,
+  endpoints: {
+    list: '/list',
+    detail: '/detail',
+    add: '/add',
+    edit: '/update',
+    delete: '/delete',
+  },
+  methods: {
+    list: 'POST',
+    detail: 'POST',
+    add: 'POST',
+    edit: 'POST',
+    delete: 'POST',
+  },
 };
 
-// 查看详情
-export const DETAIL = (url, id) => {
-  const isMall = isMallAPI(url);
+// 新版本 CRUD 配置
+const V2_CRUD_CONFIG = {
+  version: CRUD_VERSIONS.V2,
+  endpoints: {
+    list: '/page',
+    detail: '/info',
+    add: '/save',
+    edit: '/update',
+    delete: '/delete',
+  },
+  methods: {
+    list: 'GET',
+    detail: 'GET',
+    add: 'POST',
+    edit: 'PATCH',
+    delete: 'DELETE',
+  },
+};
+
+// 获取 CRUD 配置
+const getCrudConfig = (options = {}) => {
+  const { version = CRUD_VERSIONS.V1 } = options;
+
+  if (version === CRUD_VERSIONS.V2) {
+    return V2_CRUD_CONFIG;
+  }
+
+  return DEFAULT_CRUD_CONFIG;
+};
+
+// 查看列表
+export const LIST = (url, data, options = {}) => {
+  const config = getCrudConfig(options);
+  const endpoint = url + config.endpoints.list;
+  const method = config.methods.list;
 
   return request({
-    url: url + `/detail`,
-    method: isMall ? 'GET' : 'POST', // mall用GET,其他用POST
-    params: { id },
+    url: endpoint,
+    method,
+    ...(method === 'GET' ? { params: data } : { data }),
   });
 };
 
+// 查看详情
+export const DETAIL = (url, id, options = {}) => {
+  const config = getCrudConfig(options);
+  const method = config.methods.detail;
+
+  if (config.version === CRUD_VERSIONS.V2) {
+    // 新版本:使用 RESTful 风格的 URL
+    return request({
+      url: url + config.endpoints.detail + `/${id}`,
+      method,
+    });
+  } else {
+    // 旧版本:使用参数传递 ID
+    return request({
+      url: url + config.endpoints.detail,
+      method,
+      ...(method === 'GET' ? { params: { id } } : { data: { id } }),
+    });
+  }
+};
+
 // 新增
-export const ADD = (url, data) =>
-  request({
-    url: url + '/add',
-    method: 'POST',
+export const ADD = (url, data, options = {}) => {
+  const config = getCrudConfig(options);
+
+  return request({
+    url: url + config.endpoints.add,
+    method: config.methods.add,
     data,
     options: {
       showSuccessMessage: false,
     },
   });
+};
 
 // 编辑&更新
-export const EDIT = (url, data) => {
-  const isMall = isMallAPI(url);
+export const EDIT = (url, id, data, options = {}) => {
+  const config = getCrudConfig(options);
 
-  return request({
-    url: url + `/update`,
-    method: isMall ? 'PUT' : 'POST', // mall用PUT,其他用POST
-    data,
-    options: {
-      showSuccessMessage: false,
-    },
-  });
+  if (config.version === CRUD_VERSIONS.V2) {
+    // 新版本:使用 RESTful 风格的 URL
+    return request({
+      url: url + config.endpoints.edit + `/${id}`,
+      method: config.methods.edit,
+      data,
+      options: {
+        showSuccessMessage: false,
+      },
+    });
+  } else {
+    // 旧版本:将 ID 包含在数据中
+    return request({
+      url: url + config.endpoints.edit,
+      method: config.methods.edit,
+      data: { id, ...data },
+      options: {
+        showSuccessMessage: false,
+      },
+    });
+  }
 };
 
 // 删除(软删除/真实删除)
-export const DELETE = (url, data) => {
-  const isMall = isMallAPI(url);
+export const DELETE = (url, id, options = {}) => {
+  const config = getCrudConfig(options);
+  const method = config.methods.delete;
 
-  return request({
-    url: url + `/delete`,
-    method: isMall ? 'DELETE' : 'POST', // mall用DELETE,其他用POST
-    params: data,
-    options: {
-      showSuccessMessage: true,
-    },
-  });
+  if (config.version === CRUD_VERSIONS.V2) {
+    // 新版本:使用 RESTful 风格的 URL
+    return request({
+      url: url + config.endpoints.delete + `/${id}`,
+      method,
+      options: {
+        showSuccessMessage: true,
+      },
+    });
+  } else {
+    // 旧版本:使用参数传递 ID
+    return request({
+      url: url + config.endpoints.delete,
+      method,
+      ...(method === 'GET' ? { params: { id } } : { data: { id } }),
+      options: {
+        showSuccessMessage: true,
+      },
+    });
+  }
 };
 
 // 选择
@@ -104,12 +196,14 @@ export const CRUD = (
   options = {},
 ) => {
   const apis = {};
-  if (methods.includes('list'))
-    apis.list = (params, pageInParams = true) => LIST(url, params, pageInParams);
-  if (methods.includes('detail')) apis.detail = (id) => DETAIL(url, id);
-  if (methods.includes('add')) apis.add = (data) => ADD(url, data);
-  if (methods.includes('edit')) apis.edit = (id, data) => EDIT(url, id, data);
-  if (methods.includes('delete')) apis.delete = (id) => DELETE(url, id);
+  const crudOptions = { version: options.version || CRUD_VERSIONS.V1 };
+
+  if (methods.includes('list')) apis.list = (params) => LIST(url, params, crudOptions);
+  if (methods.includes('detail')) apis.detail = (id) => DETAIL(url, id, crudOptions);
+  if (methods.includes('add')) apis.add = (data) => ADD(url, data, crudOptions);
+  if (methods.includes('edit')) apis.edit = (id, data) => EDIT(url, id, data, crudOptions);
+  if (methods.includes('delete')) apis.delete = (id) => DELETE(url, id, crudOptions);
+
   if (methods.includes('report'))
     apis.report = (params, filename) =>
       REPORT(url, params, filename, 'report', options.reportMethod);
@@ -119,6 +213,15 @@ export const CRUD = (
   return apis;
 };
 
+// 新版本 CRUD 的便捷函数
+export const CRUD_V2 = (
+  url,
+  methods = ['list', 'detail', 'add', 'edit', 'delete', 'export'],
+  options = {},
+) => {
+  return CRUD(url, methods, { ...options, version: CRUD_VERSIONS.V2 });
+};
+
 // 通用回收站
 export const RECYCLE = (url) => {
   return {
@@ -246,4 +349,26 @@ export const REPORT = async (
   }
 };
 
-// add, list, delete, edit, detail, select, recyclebin, restore, destroy, report
+/**
+ * CRUD 使用说明:
+ *
+ * 1. 旧版本 CRUD(默认):
+ *    ...CRUD('shop/admin/user', ['list', 'detail', 'add', 'edit', 'delete'])
+ *
+ * 2. 新版本 CRUD(通过参数指定):
+ *    ...CRUD('mall/notice', ['list', 'detail', 'add', 'edit', 'delete'], { version: 'v2' })
+ *
+ * 3. 新版本 CRUD(便捷函数):
+ *    ...CRUD_V2('mall/notice', ['list', 'detail', 'add', 'edit', 'delete'])
+ *
+ * 接口映射对照:
+ * 操作   | 旧版本(V1)        | 新版本(V2)
+ * ------ | ---------------- | ------------------
+ * 列表   | POST /list       | GET /page
+ * 详情   | POST /detail     | GET /info/{id}
+ * 新增   | POST /add        | POST /save
+ * 更新   | POST /update     | PATCH /update/{id}
+ * 删除   | POST /delete     | DELETE /delete/{id}
+ */
+
+// 支持的方法: add, list, delete, edit, detail, select, recyclebin, restore, destroy, report