西格玛许 1 неделя назад
Родитель
Сommit
c6b7401340

+ 1 - 0
src/api/main/student/types.ts

@@ -44,6 +44,7 @@ export interface StudentVO {
   gender: string;
   avatar: string | number;
   userType: string;
+  userTypeLabel?: string;
   totalAmount: number;
   availability: string;
   jobIntention: string;

+ 4 - 3
src/router/index.ts

@@ -313,7 +313,7 @@ export const constantRoutes: RouteRecordRaw[] = [
   {
     path: '/system/agent',
     component: Layout,
-    hidden: false,
+    hidden: true,
     redirect: '/system/agent/index',
     children: [
       {
@@ -326,7 +326,7 @@ export const constantRoutes: RouteRecordRaw[] = [
         path: 'ticket',
         component: () => import('@/views/system/agent/ticket.vue'),
         name: 'AgentTicket',
-        hidden: false,
+        hidden: true,
         meta: { title: '客服工单', icon: 'ticket' }
       },
       {
@@ -403,7 +403,8 @@ export const constantRoutes: RouteRecordRaw[] = [
   {
     path: '/system/sysconfig',
     component: Layout,
-    hidden: false,
+    redirect: '/system/sysconfig/index',
+    hidden: true,
     children: [
       {
         path: 'index',

+ 8 - 8
src/views/system/assessment/index.vue

@@ -66,7 +66,7 @@
     </el-card>
 
     <!-- 数据表格 -->
-    <el-card shadow="never" class="table-card">
+    <el-card shadow="never" class="table-card modern-card">
       <el-table
         ref="tableRef"
         v-loading="loading"
@@ -419,15 +419,15 @@ const getBusinessStatus = (row: any) => {
   // delFlag: 0=正常, 1=删除
 
   if (row.delFlag == '1') {
-    return { text: '删除', type: 'danger' };
+    return { text: '删除', type: 'danger' as const };
   }
 
   if (row.status == '0') {
-    return { text: '草稿', type: 'info' };
+    return { text: '草稿', type: 'info' as const };
   }
 
   if (row.status == '2') {
-    return { text: '已下架', type: 'info' };
+    return { text: '已下架', type: 'info' as const };
   }
 
   if (row.status == '1') {
@@ -436,15 +436,15 @@ const getBusinessStatus = (row: any) => {
     const downTime = row.downTime ? new Date(row.downTime) : null;
 
     if (onTime && now < onTime) {
-      return { text: '待上架', type: 'warning' };
+      return { text: '待上架', type: 'warning' as const };
     }
     if (downTime && now > downTime) {
-      return { text: '已下架', type: 'info' };
+      return { text: '已下架', type: 'info' as const };
     }
-    return { text: '在售', type: 'success' };
+    return { text: '在售', type: 'success' as const };
   }
 
-  return { text: '未知', type: 'info' };
+  return { text: '未知', type: 'info' as const };
 };
 </script>
 

+ 2 - 2
src/views/system/audit/index.vue

@@ -2,7 +2,7 @@
   <div class="audit-container">
     <el-container class="h-full">
       <!-- 左侧分类侧边栏 -->
-      <el-aside width="250px" class="bg-white mr-4 h-full sidebar">
+      <el-aside width="250px" class="mr-4 h-full sidebar modern-card">
         <div class="sidebar-title">审核类型</div>
         <div class="p-4">
           <el-input v-model="filterText" placeholder="请输入" :prefix-icon="Search" />
@@ -24,7 +24,7 @@
       </el-aside>
 
       <!-- 右侧主体内容 -->
-      <el-main class="bg-white h-full main-content p-0">
+      <el-main class="h-full main-content p-0 modern-card">
         <!-- 搜索与头部区域 -->
         <div class="header-section">
           <div class="flex items-center mb-4">

+ 2 - 2
src/views/system/backcheck/index.vue

@@ -2,7 +2,7 @@
   <div class="backcheck-container">
     <el-container class="h-full">
       <!-- 左侧分类侧边栏 -->
-      <el-aside width="200px" class="bg-white mr-4 h-full sidebar">
+      <el-aside width="200px" class="mr-4 h-full sidebar modern-card">
         <div class="sidebar-title">背调分类</div>
         <el-menu :default-active="activeMenu" class="border-none mt-2" @select="handleMenuSelect">
           <el-menu-item v-for="c in categories" :key="c.id" :index="c.id" :class="{ 'menu-item-active': c.id === activeMenu }">
@@ -17,7 +17,7 @@
       </el-aside>
 
       <!-- 右侧主体内容 -->
-      <el-main class="bg-white h-full main-content p-0 flex flex-col">
+      <el-main class="h-full main-content p-0 flex flex-col modern-card">
         <!-- 搜索与头部区域 -->
         <div class="header-section flex justify-between items-center px-4 py-4 shrink-0">
           <div class="flex items-center">

+ 18 - 7
src/views/system/backclause/index.vue

@@ -2,7 +2,7 @@
   <div class="app-container backclause-page">
     <div v-if="!showForm" class="flex gap-4 h-full">
       <!-- 左侧分类侧边栏 -->
-      <div class="category-sidebar bg-white rounded p-4 flex flex-col">
+      <div class="category-sidebar p-4 flex flex-col modern-card">
         <div class="sidebar-header flex justify-between items-center mb-4">
           <span class="font-bold text-base">背调分类</span>
           <el-button link class="add-btn" @click="handleCategoryAdd">
@@ -10,10 +10,21 @@
           </el-button>
         </div>
         <div class="category-list flex-1 overflow-y-auto">
+          <!-- 条款清单(父类) -->
+          <div
+            :class="['category-item font-bold', activeCategoryId === 'all' ? 'active' : '']"
+            @click="activeCategoryId = 'all'"
+          >
+            <div class="flex items-center gap-2 overflow-hidden">
+              <el-icon><i-ep-management /></el-icon>
+              <span class="truncate">条款清单</span>
+            </div>
+          </div>
+          
           <div
             v-for="item in categoryList"
             :key="item.id"
-            :class="['category-item', activeCategoryId === item.id ? 'active' : '']"
+            :class="['category-item ml-4', activeCategoryId === item.id ? 'active' : '']"
             @click="activeCategoryId = item.id"
           >
             <div class="flex items-center gap-2 overflow-hidden">
@@ -34,7 +45,7 @@
       </div>
 
       <!-- 右侧列表内容 -->
-      <div class="flex-1 bg-white rounded p-4 flex flex-col">
+      <div class="flex-1 p-4 flex flex-col modern-card">
         <div class="header-bar flex justify-between items-center mb-4">
           <el-input
             v-model="queryParams.keyword"
@@ -161,7 +172,7 @@ const { sys_clause_type } = useDict('sys_clause_type');
 const loading = ref(false);
 const total = ref(0);
 const showForm = ref(false);
-const activeCategoryId = ref<string | number>('');
+const activeCategoryId = ref<string | number>('all');
 const currentClause = ref(null);
 
 const categoryList = ref<BackCategoryVO[]>([]);
@@ -179,8 +190,8 @@ const getTypeTagType = (type: string) => {
 const getCategoryList = async () => {
   const res = await listBackCategory();
   categoryList.value = res.data;
-  if (categoryList.value.length > 0 && !activeCategoryId.value) {
-    activeCategoryId.value = categoryList.value[0].id.toString();
+  if (!activeCategoryId.value) {
+    activeCategoryId.value = 'all';
   }
 };
 
@@ -202,7 +213,7 @@ const getClauseList = async () => {
   loading.value = true;
   try {
     const res = await listBackClause({
-      categoryId: activeCategoryId.value,
+      categoryId: activeCategoryId.value === 'all' ? undefined : activeCategoryId.value,
       name: queryParams.keyword,
       pageNum: queryParams.pageNum,
       pageSize: queryParams.pageSize

+ 71 - 4
src/views/system/customer/chat/index.vue

@@ -584,6 +584,7 @@ const msgContainerRef = ref(null);
 const fileInput = ref(null);
 const imageInput = ref(null);
 let countdownTimer = null;
+let pollTimer = null; // 新增轮询定时器
 
 // 高度调整逻辑
 const inputHeight = ref(200); 
@@ -708,10 +709,11 @@ async function fetchSessionList() {
     const response = await getSessionList({
       sessionType: activeTab.value === 'mini' ? 1 : 2,
       pageNum: 1,
-      pageSize: 100
+      pageSize: 100,
+      timestamp: new Date().getTime()
     });
     if (response.code === 200 || response.code === 0) {
-      sessionList.value = response.rows.map(item => ({
+      const newSessionList = response.rows.map(item => ({
         id: item.sessionId,
         name: item.fromUserName,
         type: item.sessionType === 1 ? 'mini' : 'merchant',
@@ -723,7 +725,15 @@ async function fetchSessionList() {
         sessionNo: item.sessionNo
       }));
       
-      if (sessionList.value.length > 0) {
+      // 生成简易指纹进行绝对对比
+      const ss1 = newSessionList.map(s => String(s.lastMsgTime) + String(s.unread) + String(s.lastMsg)).join('|');
+      const ss2 = sessionList.value.map(s => String(s.lastMsgTime) + String(s.unread) + String(s.lastMsg)).join('|');
+
+      if (ss1 !== ss2) {
+         sessionList.value = newSessionList;
+      }
+      
+      if (sessionList.value.length > 0 && !activeSessionId.value) {
         selectSession(sessionList.value[0]);
       }
     }
@@ -732,6 +742,50 @@ async function fetchSessionList() {
   }
 }
 
+async function loadMessagesSilent() {
+  if (!activeSessionId.value) return;
+  try {
+    const response = await getMessageHistory({
+      sessionId: activeSessionId.value,
+      pageNum: 1,
+      pageSize: 50,
+      timestamp: new Date().getTime() // 避免 "_t" 被拦截,换成明确的防缓存字段
+    });
+    if ((response.code === 200 || response.code === 0) && response.rows) {
+      const newMessages = response.rows.map(msg => {
+        let parsedPayload = null;
+        if ((msg.msgType === 'job_card' || msg.msgType === 'order_card') && msg.payload) {
+          parsedPayload = typeof msg.payload === 'string' ? JSON.parse(msg.payload) : msg.payload;
+        } else if (msg.msgType === 'file') {
+          parsedPayload = { name: msg.fileName || '未知文件', url: msg.fileUrl, size: '未知大小' };
+        } else if (msg.msgType === 'image') {
+          parsedPayload = { url: msg.fileUrl || msg.content };
+        }
+        return {
+          sender: msg.senderType === 2 ? 'waiter' : 'customer',
+          type: msg.msgType || 'text',
+          content: msg.content,
+          time: msg.sendTime,
+          payload: parsedPayload,
+          fileUrl: msg.fileUrl,
+          fileName: msg.fileName
+        };
+      });
+      
+      // 生成绝对的特征指纹,只要任何一条数据的内容、时间变了,就会触发刷新
+      const sig1 = newMessages.map(m => String(m.time) + String(m.content)).join('|');
+      const sig2 = messageList.value.map(m => String(m.time) + String(m.content)).join('|');
+      
+      if (sig1 !== sig2) {
+        messageList.value = newMessages;
+        scrollToBottom();
+      }
+    }
+  } catch (error) {
+    console.error('静默加载消息失败:', error);
+  }
+}
+
 async function handleSend() {
   if (!inputText.value.trim()) return;
   
@@ -814,7 +868,8 @@ async function selectSession(session) {
     const response = await getMessageHistory({
       sessionId: session.id,
       pageNum: 1,
-      pageSize: 50
+      pageSize: 50,
+      timestamp: new Date().getTime()
     });
     if ((response.code === 200 || response.code === 0) && response.rows) {
       // 将后端返回的消息转换为前端格式
@@ -966,6 +1021,9 @@ function scrollToBottom() {
 }
 
 watch(activeTab, () => {
+  activeSessionId.value = 0; // 切换 Tab 时清除选中状态,让 fetchSessionList 自动选中第一个
+  activeSession.value = null;
+  messageList.value = [];
   fetchSessionList();
 });
 
@@ -984,11 +1042,20 @@ onMounted(() => {
       }
     });
   }, 1000);
+
+  // 增加消息轮询
+  pollTimer = setInterval(() => {
+    fetchSessionList();
+    if (activeSessionId.value) {
+      loadMessagesSilent();
+    }
+  }, 3000);
 });
 
 onBeforeUnmount(() => {
   stopResizing();
   if (countdownTimer) clearInterval(countdownTimer);
+  if (pollTimer) clearInterval(pollTimer);
 });
 </script>
 

+ 2 - 2
src/views/system/goods/detail.vue

@@ -128,7 +128,7 @@ onMounted(async () => {
 });
 
 const handleCancel = () => {
-  router.push('/system/goods');
+  proxy.$tab.closePage();
 };
 
 const handleSave = () => {
@@ -142,7 +142,7 @@ const handleSave = () => {
         await addGoods(form.value);
         ElMessage.success('新增成功');
       }
-      router.push('/system/goods');
+      proxy.$tab.closePage();
     }
   });
 };

+ 1 - 1
src/views/system/goods/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="app-container goods-page">
-    <el-card class="box-card border-none main-card" shadow="never">
+    <el-card class="box-card border-none main-card modern-card" shadow="never">
       <!-- 1. 顶部操作栏 -->
       <div class="toolbar mb20">
         <div class="left-actions">

+ 2 - 2
src/views/system/order/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="app-container order-page">
-    <el-card class="box-card mb20 border-none" shadow="never">
+    <el-card class="box-card mb20 border-none modern-card" shadow="never">
       <!-- 1. 搜索表单 -->
       <el-form :model="queryParams" ref="queryForm" :inline="true" class="search-form">
         <el-form-item label="客户单号">
@@ -64,7 +64,7 @@
 
     <!-- 4. 订单列表主体 -->
     <div v-loading="loading" class="order-list">
-      <div v-for="order in orderList" :key="order.orderSn" class="order-item mb20">
+      <div v-for="order in orderList" :key="order.orderSn" class="order-item mb20 modern-card">
         <!-- 订单页眉 -->
         <div class="order-item-header">
           <div class="header-left">

+ 16 - 75
src/views/system/student/index.vue

@@ -56,56 +56,6 @@
       </el-col>
     </el-row>
 
-    <!-- 搜索栏 -->
-    <el-card shadow="hover" class="mb-4">
-      <el-form :model="queryParams" ref="queryFormRef" :inline="true">
-        <el-form-item label="学员编号" prop="studentNo">
-          <el-input v-model="queryParams.studentNo" placeholder="请输入学员编号" clearable style="width: 200px" />
-        </el-form-item>
-        <el-form-item label="姓名/手机号" prop="name">
-          <el-input v-model="queryParams.name" placeholder="请输入关键词搜索" clearable style="width: 200px" />
-        </el-form-item>
-        <el-form-item label="创建日期">
-          <el-date-picker
-            v-model="dateRange"
-            type="daterange"
-            range-separator="-"
-            start-placeholder="开始日期"
-            end-placeholder="结束日期"
-            value-format="YYYY-MM-DD"
-            style="width: 240px"
-          />
-        </el-form-item>
-        <el-form-item label="用户类型" prop="userType">
-          <el-select v-model="queryParams.userType" placeholder="全部状态" clearable style="width: 200px">
-            <el-option
-              v-for="dict in main_user_type"
-              :key="dict.value"
-              :label="dict.label"
-              :value="dict.value"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="所属区域" prop="region">
-          <el-select v-model="queryParams.region" placeholder="全部" clearable style="width: 200px">
-            <el-option label="华东地区" value="east" />
-            <el-option label="华南地区" value="south" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="消费金额范围">
-          <div class="flex items-center">
-            <el-input v-model="queryParams.minAmount" placeholder="最低" style="width: 100px" />
-            <span class="mx-2">-</span>
-            <el-input v-model="queryParams.maxAmount" placeholder="最高" style="width: 100px" />
-          </div>
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
-          <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-        </el-form-item>
-      </el-form>
-    </el-card>
-
     <!-- 工具栏和表格 -->
     <el-card shadow="hover">
       <template #header>
@@ -149,7 +99,9 @@
         <el-table-column label="测评数" align="center" prop="assessmentCount" />
         <el-table-column label="类型" align="center">
           <template #default="scope">
-            <dict-tag :options="main_user_type" :value="scope.row.userType" />
+            <el-tag>
+              {{ scope.row.userTypeLabel || scope.row.userType }}
+            </el-tag>
           </template>
         </el-table-column>
         <el-table-column label="累计消费" align="center" prop="totalAmount">
@@ -162,16 +114,22 @@
             <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)" />
           </template>
         </el-table-column>
-        <el-table-column label="角色" align="center" prop="roleName" />
+        <el-table-column label="角色" align="center">
+          <template #default>
+            <el-tag type="info">学员</el-tag>
+          </template>
+        </el-table-column>
         <el-table-column label="注册时间" align="center" prop="createTime" width="120" />
         <el-table-column label="最后登录" align="center" prop="loginDate" width="120" />
-        <el-table-column label="操作" align="center" width="220" fixed="right">
+        <el-table-column label="操作" align="center" width="280" fixed="right">
           <template #default="scope">
-            <el-button link type="primary" @click="handleDetail(scope.row)">详情</el-button>
-            <el-button link type="primary" @click="handleUpdate(scope.row)">编辑</el-button>
-            <el-button v-if="scope.row.userType !== '3'" link type="danger" @click="handleBlacklist(scope.row)">加入黑名单</el-button>
-            <el-button v-else link type="success" @click="handleRemoveBlacklist(scope.row)">移除</el-button>
-            <el-button link type="success" @click="handleRecommend(scope.row)">内推</el-button>
+            <div class="flex items-center justify-center gap-1">
+              <el-button link type="primary" @click="handleDetail(scope.row)">详情</el-button>
+              <el-button link type="primary" @click="handleUpdate(scope.row)">编辑</el-button>
+              <el-button v-if="scope.row.userType !== '3'" link type="danger" @click="handleBlacklist(scope.row)">加入黑名单</el-button>
+              <el-button v-else link type="success" @click="handleRemoveBlacklist(scope.row)">移除</el-button>
+              <el-button link type="success" @click="handleRecommend(scope.row)">内推</el-button>
+            </div>
           </template>
         </el-table-column>
       </el-table>
@@ -294,23 +252,6 @@ const handleSelectionChange = (selection) => {
   multiple.value = !selection.length
 }
 
-const getUserTypeTag = (type) => {
-  const map = {
-    '1': 'info',
-    '2': 'success',
-    '3': 'danger'
-  }
-  return map[type] || 'info'
-}
-
-const getUserTypeText = (type) => {
-  const map = {
-    '1': '普通用户',
-    '2': '付费用户',
-    '3': '黑名单用户'
-  }
-  return map[type] || '未知'
-}
 
 const handleDetail = (row) => {
   router.push('/system/student/detail/' + row.id)

+ 86 - 21
src/views/system/sysconfig/index.vue

@@ -7,6 +7,11 @@
         <el-tab-pane label="平台配置" name="platform" />
         <el-tab-pane label="文件存储配置" name="storage" />
         <el-tab-pane label="短信配置" name="sms" />
+        <el-tab-pane label="支付配置" name="payment">
+          <div class="agreement-container animated fadeIn">
+            <wxpay-config v-if="activeMainTab === 'payment'" />
+          </div>
+        </el-tab-pane>
         <el-tab-pane label="协议配置" name="agreement">
           <div class="agreement-container animated fadeIn">
             <!-- 子配置选项卡:协议类型 -->
@@ -45,35 +50,77 @@
       </el-tabs>
     </el-card>
 
-    <!-- 其他Tab的占位信息 (非协议配置时显示) -->
-    <el-empty v-if="activeMainTab !== 'agreement'" description="该模块配置开发中..." />
+    <!-- 其他Tab的占位信息 (非协议配置和支付配置时显示) -->
+    <el-empty v-if="activeMainTab !== 'agreement' && activeMainTab !== 'payment'" description="该模块配置开发中..." />
   </div>
 </template>
 
 <script setup name="SysConfig">
-import { ref, reactive } from 'vue';
+import { ref, reactive, watch, onMounted } from 'vue';
 import Editor from '@/components/Editor/index.vue';
+import WxpayConfig from './wxpay.vue';
 import { ElMessage } from 'element-plus';
+import request from '@/utils/request';
 
 // 主选项卡
 const activeMainTab = ref('agreement');
 
+// 表单数据
+const form = reactive({
+  title: '用户服务协议',
+  content: ''
+});
+
+// 当前协议对应的数据库 ID
+const currentAgreementId = ref(null);
+
 // 协议子选项卡
 const activeSubTab = ref('user');
 const agreementSubTabs = [
   { label: '用户协议', name: 'user' },
-  { label: '隐私政策', name: 'privacy' },
-  { label: '履约者说明', name: 'contractor' }
+  { label: '隐私政策', name: 'privacy' }
 ];
 
-// 表单数据
-const form = reactive({
-  title: '用户服务协议',
-  content: ''
+const activeSubTabsMap = {
+  user: '用户协议',
+  privacy: '隐私政策'
+};
+
+// 获取协议内容
+const fetchAgreement = async () => {
+  try {
+    // 根据当前选项卡将 'user' 映射成后端数据库存的 'service'
+    const typeKey = activeSubTab.value === 'user' ? 'service' : 'privacy';
+    const res = await request({
+      url: `/miniapp/auth/agreement?type=${typeKey}`,
+      method: 'get'
+    });
+    if (res && res.code === 200 && res.data) {
+      currentAgreementId.value = res.data.id;
+      form.title = res.data.title || activeSubTabsMap[activeSubTab.value];
+      form.content = res.data.content || '';
+    } else {
+      currentAgreementId.value = null;
+      form.title = activeSubTabsMap[activeSubTab.value];
+      form.content = '';
+    }
+  } catch (error) {
+    console.error('获取协议失败', error);
+  }
+};
+
+// 切换 Tab 时重新抓取对应数据
+watch(activeSubTab, () => {
+  fetchAgreement();
+});
+
+// 挂载时抓取初始数据
+onMounted(() => {
+  fetchAgreement();
 });
 
 /** 保存配置 */
-const handleSave = () => {
+const handleSave = async () => {
   if (!form.title) {
     return ElMessage.warning('请输入协议标题');
   }
@@ -81,17 +128,35 @@ const handleSave = () => {
     return ElMessage.warning('请输入协议内容');
   }
   
-  ElMessage.success({
-    message: `${activeSubTabsMap[activeSubTab.value]} 保存成功`,
-    type: 'success',
-    duration: 2000
-  });
-};
-
-const activeSubTabsMap = {
-  user: '用户协议',
-  privacy: '隐私政策',
-  contractor: '履约者说明'
+  try {
+    const typeKey = activeSubTab.value === 'user' ? 'service' : 'privacy';
+    const payload = {
+      id: currentAgreementId.value,
+      type: typeKey,
+      title: form.title,
+      content: form.content
+    };
+    
+    // 调用后端的 /edit 接口
+    const res = await request({
+      url: '/miniapp/auth/edit',
+      method: 'post',
+      data: payload
+    });
+    
+    if (res.code === 200) {
+      ElMessage.success({
+        message: `${activeSubTabsMap[activeSubTab.value]} 保存成功`,
+        type: 'success',
+        duration: 2000
+      });
+      fetchAgreement(); // 重新拉取
+    } else {
+      ElMessage.error(res.msg || '保存失败');
+    }
+  } catch (error) {
+    console.error('保存协议失败', error);
+  }
 };
 </script>
 

+ 232 - 0
src/views/system/sysconfig/wxpay.vue

@@ -0,0 +1,232 @@
+<template>
+  <div class="p-2">
+    <el-card shadow="hover">
+      <template #header>
+        <div class="card-header">
+          <span>微信小程序支付配置</span>
+          <el-button type="primary" @click="handleSave" :loading="saving">保存配置</el-button>
+        </div>
+      </template>
+
+      <el-form :model="form" label-width="140px" v-loading="loading">
+        <el-form-item label="商户号ID">
+          <el-input v-model="form.mchId" placeholder="请输入微信支付商户号(MCHID)" style="width: 400px" />
+        </el-form-item>
+        
+        <el-form-item label="商户号API密钥">
+          <el-input v-model="form.apiV3Key" placeholder="请输入微信支付商户API密钥(APIv3密钥)" 
+                    type="password" show-password style="width: 400px" />
+        </el-form-item>
+
+        <el-form-item label="支付证书">
+          <div>
+            <el-upload
+              action="#"
+              :auto-upload="false"
+              :show-file-list="false"
+              accept=".pem"
+              :on-change="handleCertChange"
+            >
+              <el-button type="primary">上传证书文件</el-button>
+            </el-upload>
+            <div class="tip-text">微信支付证书(apiclient_cert.pem),前往微信商家平台下载</div>
+            <div v-if="form.certUploaded" class="success-text">✓ 证书已上传</div>
+          </div>
+        </el-form-item>
+        
+        <el-form-item label="商户私钥">
+          <div>
+            <el-upload
+              action="#"
+              :auto-upload="false"
+              :show-file-list="false"
+              accept=".pem"
+              :on-change="handlePrivateKeyChange"
+            >
+              <el-button type="primary">上传私钥文件</el-button>
+            </el-upload>
+            <div class="tip-text">微信支付证书私钥(apiclient_key.pem),前往微信商家平台下载</div>
+            <div v-if="form.privateKeyUploaded" class="success-text">✓ 私钥已上传</div>
+          </div>
+        </el-form-item>
+
+        <el-divider content-position="left">微信支付公钥配置(新商户必填)</el-divider>
+        
+        <el-form-item label="公钥ID">
+          <el-input v-model="form.publicKeyId" placeholder="请输入微信支付公钥ID,如:PUB_KEY_ID_xxx" style="width: 400px" />
+          <div class="tip-text">从微信商户平台 → API安全 → 申请微信支付公钥 获取</div>
+        </el-form-item>
+        
+        <el-form-item label="微信支付公钥">
+          <div>
+            <el-upload
+              action="#"
+              :auto-upload="false"
+              :show-file-list="false"
+              accept=".pem"
+              :on-change="handlePublicKeyChange"
+            >
+              <el-button type="primary">上传公钥文件</el-button>
+            </el-upload>
+            <div class="tip-text">微信支付公钥(pub_key.pem),从微信商户平台 → API安全 下载</div>
+            <div v-if="form.publicKeyUploaded" class="success-text">✓ 公钥已上传</div>
+          </div>
+        </el-form-item>
+
+        <el-divider content-position="left">回调配置</el-divider>
+        
+        <el-form-item label="API回调地址">
+          <el-input v-model="form.notifyUrl" placeholder="请输入支付回调地址,如:https://域名/v1/order/notify" style="width: 400px" />
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import request from '@/utils/request'
+
+const loading = ref(false)
+const saving = ref(false)
+const privateKeyFile = ref<File | null>(null)
+const certFile = ref<File | null>(null)
+const publicKeyFile = ref<File | null>(null)
+
+const form = ref({
+  mchId: '',
+  apiV3Key: '',
+  notifyUrl: '',
+  privateKeyUploaded: false,
+  certUploaded: false,
+  publicKeyId: '',
+  publicKeyUploaded: false
+})
+
+const handlePrivateKeyChange = (file: any) => {
+  privateKeyFile.value = file.raw
+  ElMessage.success('私钥文件已选择,保存时将一并上传')
+}
+
+const handleCertChange = (file: any) => {
+  certFile.value = file.raw
+  ElMessage.success('证书文件已选择,保存时将一并上传')
+}
+
+const handlePublicKeyChange = (file: any) => {
+  publicKeyFile.value = file.raw
+  ElMessage.success('公钥文件已选择,保存时将一并上传')
+}
+
+const getConfig = async () => {
+  loading.value = true
+  try {
+    const res = await request.get('/miniapp/paymentConfig/list')
+    if (res.code === 200 && res.data) {
+      form.value.mchId = res.data.mchId || ''
+      form.value.apiV3Key = res.data.apiV3Key || ''
+      form.value.notifyUrl = res.data.notifyUrl || ''
+      form.value.privateKeyUploaded = res.data.privateKeyUploaded || false
+      form.value.certUploaded = res.data.certUploaded || false
+      form.value.publicKeyId = res.data.publicKeyId || ''
+      form.value.publicKeyUploaded = res.data.publicKeyUploaded || false
+    }
+  } catch (e) {
+    console.error('获取配置失败', e)
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleSave = async () => {
+  saving.value = true
+  try {
+    // 上传私钥
+    if (privateKeyFile.value) {
+      const formData = new FormData()
+      formData.append('file', privateKeyFile.value)
+      const uploadRes = await request.post('/miniapp/paymentConfig/uploadPrivateKey', formData, {
+        headers: { 'Content-Type': 'multipart/form-data' }
+      })
+      if (uploadRes.code !== 200) {
+        ElMessage.error('私钥上传失败:' + uploadRes.msg)
+        saving.value = false
+        return
+      }
+      form.value.privateKeyUploaded = true
+      privateKeyFile.value = null
+    }
+
+    // 上传证书
+    if (certFile.value) {
+      const formData = new FormData()
+      formData.append('file', certFile.value)
+      const uploadRes = await request.post('/miniapp/paymentConfig/uploadCert', formData, {
+        headers: { 'Content-Type': 'multipart/form-data' }
+      })
+      if (uploadRes.code !== 200) {
+        ElMessage.error('证书上传失败:' + uploadRes.msg)
+        saving.value = false
+        return
+      }
+      form.value.certUploaded = true
+      certFile.value = null
+    }
+
+    // 上传公钥
+    if (publicKeyFile.value) {
+      const formData = new FormData()
+      formData.append('file', publicKeyFile.value)
+      const uploadRes = await request.post('/miniapp/paymentConfig/uploadPublicKey', formData, {
+        headers: { 'Content-Type': 'multipart/form-data' }
+      })
+      if (uploadRes.code !== 200) {
+        ElMessage.error('公钥上传失败:' + uploadRes.msg)
+        saving.value = false
+        return
+      }
+      form.value.publicKeyUploaded = true
+      publicKeyFile.value = null
+    }
+    
+    const res = await request.put('/miniapp/paymentConfig/wxpay', {
+      mchId: form.value.mchId,
+      apiV3Key: form.value.apiV3Key,
+      notifyUrl: form.value.notifyUrl,
+      publicKeyId: form.value.publicKeyId
+    })
+    if (res.code === 200) {
+      ElMessage.success('保存成功')
+    } else {
+      ElMessage.error(res.msg || '保存失败')
+    }
+  } catch (e) {
+    ElMessage.error('保存失败')
+  } finally {
+    saving.value = false
+  }
+}
+
+onMounted(() => {
+  getConfig()
+})
+</script>
+
+<style scoped>
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.tip-text {
+  color: #909399;
+  font-size: 12px;
+  margin-top: 4px;
+}
+.success-text {
+  color: #67c23a;
+  font-size: 12px;
+  margin-top: 4px;
+}
+</style>