西格玛许 il y a 1 jour
Parent
commit
17f54ea8da

+ 2 - 2
.env.development

@@ -1,6 +1,6 @@
 # 页面标题
-VITE_APP_TITLE = RuoYi-Vue-Plus多租户管理系统
-VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus
+VITE_APP_TITLE = 审计之家管理系统
+VITE_APP_LOGO_TITLE = 审计之家
 
 # 开发环境配置
 VITE_APP_ENV = 'development'

+ 2 - 2
.env.production

@@ -1,6 +1,6 @@
 # 页面标题
-VITE_APP_TITLE = RuoYi-Vue-Plus多租户管理系统
-VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus
+VITE_APP_TITLE = 审计之家管理系统
+VITE_APP_LOGO_TITLE = 审计之家
 
 # 生产环境配置
 VITE_APP_ENV = 'production'

+ 1 - 1
package.json

@@ -30,7 +30,7 @@
     "axios": "1.13.1",
     "crypto-js": "4.2.0",
     "echarts": "5.6.0",
-    "element-china-area-data": "^5.0.0",
+    "element-china-area-data": "^5.0.2",
     "element-plus": "2.11.7",
     "file-saver": "2.0.5",
     "highlight.js": "11.11.1",

+ 74 - 0
src/api/main/companyApply/index.ts

@@ -0,0 +1,74 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+
+/**
+ * 查询商家入驻申请列表
+ */
+export const listCompanyApply = (query: any): AxiosPromise<any> => {
+  return request({
+    url: '/main/companyApply/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询商家入驻申请详情(按ID)
+ */
+export const getCompanyApply = (id: string | number): AxiosPromise<any> => {
+  return request({
+    url: '/main/companyApply/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 通过会话信息查询商家入驻信息
+ * 逻辑与后端 CsSessionServiceImpl.resolveFromUserAvatar 一致:
+ * 先按 ID 查,查不到再按 companyName 查
+ */
+export const getCompanyApplyBySession = async (fromUserId: string | number, fromUserName?: string): Promise<any> => {
+  try {
+    // 先尝试按 ID 查询
+    const res = await getCompanyApply(fromUserId);
+    if (res.code === 200 && res.data) {
+      return processCompanyApplyData(res.data);
+    }
+  } catch (e) {
+    // 按 ID 查询失败,继续尝试其他方式
+  }
+
+  try {
+    // 再尝试按 companyName 查询
+    if (fromUserName) {
+      const res = await listCompanyApply({ companyName: fromUserName });
+      if (res.code === 200 && res.rows && res.rows.length > 0) {
+        return processCompanyApplyData(res.rows[0]);
+      }
+    }
+  } catch (e) {
+    // 按 companyName 查询也失败
+  }
+
+  return null;
+};
+
+/**
+ * 处理商家数据,将 OSS ID 转换为可访问的 URL
+ * 后端已通过 ossService 将 OSS ID 转为真实 URL 返回(avatarUrl/authLetterUrl 字段)
+ * 仅在后端未返回 URL 时,作为兜底用 OSS ID 拼接路径
+ */
+function processCompanyApplyData(data: any): any {
+  if (!data) return data;
+  // avatar:优先用后端返回的 avatarUrl(真实 OSS URL),否则用 OSS ID 拼接
+  if (data.avatar && !data.avatarUrl) {
+    const baseApi = import.meta.env.VITE_APP_BASE_API;
+    data.avatarUrl = `${baseApi}/resource/oss/file/${data.avatar}`;
+  }
+  // authLetter:同理
+  if (data.authLetter && !data.authLetterUrl) {
+    const baseApi = import.meta.env.VITE_APP_BASE_API;
+    data.authLetterUrl = `${baseApi}/resource/oss/file/${data.authLetter}`;
+  }
+  return data;
+}

+ 28 - 3
src/api/main/evaluation/index.ts

@@ -1,11 +1,9 @@
 import request from '@/utils/request'
 import { AxiosPromise } from 'axios'
-import { EvaluationQuery, EvaluationVO, EvaluationForm, ThirdPartyExam, SyncParams } from './types'
+import { EvaluationQuery, EvaluationVO, EvaluationForm, ThirdPartyExam, SyncParams, EvaluationApplyRecordVO } from './types'
 
 /**
  * 查询测评列表
- * @param query 查询参数
- * @returns 测评列表数据
  */
 export const listEvaluation = (query: EvaluationQuery): AxiosPromise<EvaluationVO[]> => {
   return request({
@@ -15,6 +13,19 @@ export const listEvaluation = (query: EvaluationQuery): AxiosPromise<EvaluationV
   })
 }
 
+/**
+ * 查询学生测评记录列表
+ * @param studentId 学生ID
+ * @returns 测评记录列表
+ */
+export const listEvaluationRecord = (studentId: string | number): AxiosPromise<EvaluationApplyRecordVO[]> => {
+  return request({
+    url: '/main/exam-apply/record/list',
+    method: 'get',
+    params: { studentId }
+  })
+}
+
 /**
  * 查询测评详情
  * @param id 测评ID
@@ -199,3 +210,17 @@ export const refreshData = (): AxiosPromise<void> => {
     method: 'get'
   })
 }
+
+/**
+ * 获取学员测评详细考试结果(对接考试星10000接口)
+ * @param evaluationId 测评ID
+ * @param studentId 学员ID
+ * @returns 测评详情结果(包含每个能力维度的多次作答记录)
+ */
+export const getEvaluationDetailResult = (evaluationId: string | number, studentId: string | number): AxiosPromise<any> => {
+  return request({
+    url: `/main/examEvaluation/detail-result/${evaluationId}`,
+    method: 'get',
+    params: { studentId }
+  })
+}

+ 53 - 1
src/api/main/evaluation/types.ts

@@ -40,6 +40,7 @@ export interface AbilityItem {
   thirdExamTime?: number;
   thirdExamPassMark?: number;
   thirdExamTotalScore?: number;
+  thirdExamLink?: string;
 }
 
 // 测评视图对象(与后端 ExamEvaluationVo 保持完全一致)
@@ -47,7 +48,9 @@ export interface EvaluationVO extends BaseEntity {
   id?: string | number;
   evaluationName: string;
   grade: string;
-  position: string;         // 岗位名称(字符串)
+  position: string;         // 岗位名称(冗余字符串)
+  positionId?: string | number; // 关联岗位ID
+  positionName?: string;     // 岗位名称(查询视图中显示)
   positionCategoryId?: string;
   positionCategoryName?: string;
   positionType: string;
@@ -88,6 +91,7 @@ export interface EvaluationForm {
   evaluationName: string;
   grade: string;
   position: string;
+  positionId?: string | number;
   positionType: string;
   detail?: string;
   tags?: string | string[]; // 表单内部使用数组,提交时转为字符串
@@ -152,6 +156,54 @@ export interface StatusUpdateParams {
   status: number;
 }
 
+// 测评申请记录视图对象
+export interface EvaluationApplyRecordVO {
+  id: string | number;
+  evaluationId: string | number;
+  evaluationName: string;
+  studentId: string | number;
+  applyStatus: string;
+  finalResult: string;
+  statusText: string;
+  statusType: string;
+  scheduleStartTime: string;
+  deadlineTime: string;
+  finishedTime: string;
+  createTime: string;
+}
+
+// 学员测评详情结果(对接考试星10000接口)
+export interface EvaluationDetailResultVO {
+  evaluationName: string;
+  abilityDetails: AbilityDetailItem[];
+}
+
+export interface AbilityDetailItem {
+  abilityName: string;
+  examInfoId?: number;
+  examName?: string;
+  passMark?: number;
+  totalScore?: number;
+  records: ExamRecord[];
+  total: number;
+}
+
+export interface ExamRecord {
+  userId: string;
+  examId: number;
+  examName: string;
+  examStartTime: string;
+  examEndTime: string;
+  examTime: number; // 分钟
+  startTime: string;
+  commitTime: string | null;
+  score: string;
+  isPass: number; // 1=及格, 0=不及格, -1=未知
+  times: number;  // 作答次数
+  examResultsId: number;
+  examStyleName: string;
+}
+
 // 同步参数
 export interface SyncParams {
   companyId?: string | number;

+ 26 - 0
src/api/main/student/index.ts

@@ -64,3 +64,29 @@ export const delStudent = (id: string | number | Array<string | number>) => {
     method: 'delete'
   });
 };
+
+/**
+ * 更新学员用户类型(黑名单管理)
+ * @param id
+ * @param userType
+ */
+export const updateUserType = (id: string | number, userType: string) => {
+  return request({
+    url: '/main/student/updateUserType',
+    method: 'put',
+    data: { id, userType }
+  });
+};
+
+/**
+ * 更新学员状态
+ * @param id
+ * @param status
+ */
+export const updateStatus = (id: string | number, status: string) => {
+  return request({
+    url: '/main/student/updateStatus',
+    method: 'put',
+    data: { id, status }
+  });
+};

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

@@ -43,6 +43,7 @@ export interface StudentVO {
   idCardNumber: string;
   gender: string;
   avatar: string | number;
+  avatarUrl?: string;
   userType: string;
   userTypeLabel?: string;
   totalAmount: number;
@@ -59,6 +60,9 @@ export interface StudentVO {
   createTime: string;
   updateTime?: string;
   remark: string;
+  evaluationCount?: number;
+  consultationCount?: number;
+  isEmployed?: string;
   educationList?: StudentEducationVO[];
   experienceList?: StudentExperienceVO[];
   projectList?: StudentProjectVO[];
@@ -70,6 +74,7 @@ export interface StudentQuery extends PageQuery {
   mobile?: string;
   userType?: string;
   status?: string;
+  isBlacklist?: string;
 }
 
 export interface StudentForm {

+ 12 - 0
src/api/main/training/index.ts

@@ -178,3 +178,15 @@ export const updateOfflineTrainingStatus = (id: string | number, status: number)
     data: { id, status }
   });
 };
+
+/**
+ * 查询学员培训学习记录列表
+ * @param studentId 学员ID
+ */
+export const listStudentTrainingRecords = (studentId: string | number): AxiosPromise<TrainingLearnRecordVO[]> => {
+  return request({
+    url: '/main/training-learn-record/list',
+    method: 'get',
+    params: { studentId }
+  });
+};

+ 71 - 0
src/api/system/order/index.ts

@@ -0,0 +1,71 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { OrderVO, OrderQuery } from './types';
+
+/**
+ * 查询订单列表
+ * @param query
+ */
+export function listOrder(query: OrderQuery): AxiosPromise<OrderVO[]> {
+  return request({
+    url: '/main/order/list',
+    method: 'get',
+    params: query
+  });
+}
+
+/**
+ * 获取订单统计信息
+ */
+export function getOrderStatistics(): AxiosPromise<any> {
+  return request({
+    url: '/main/order/statistics',
+    method: 'get'
+  });
+}
+
+/**
+ * 获取订单详细信息
+ * @param id
+ */
+export function getOrder(id: string | number): AxiosPromise<OrderVO> {
+  return request({
+    url: '/main/order/' + id,
+    method: 'get'
+  });
+}
+
+/**
+ * 新增订单
+ * @param data
+ */
+export function addOrder(data: any) {
+  return request({
+    url: '/main/order',
+    method: 'post',
+    data: data
+  });
+}
+
+/**
+ * 修改订单
+ * @param data
+ */
+export function updateOrder(data: any) {
+  return request({
+    url: '/main/order',
+    method: 'put',
+    data: data
+  });
+}
+
+/**
+ * 删除订单
+ * @param ids
+ */
+export function delOrder(ids: string | number | (string | number)[]) {
+  return request({
+    url: '/main/order/' + ids,
+    method: 'delete'
+  });
+}

+ 67 - 0
src/api/system/order/types.ts

@@ -0,0 +1,67 @@
+export interface OrderVO {
+  /** 主键 */
+  id: string | number;
+  /** 订单号 */
+  orderNo: string;
+  /** 订单类型 (1:岗位 2:培训 3:商品) */
+  orderType: number;
+  /** 买家类型 (1:学生 2:企业) */
+  buyerType: number;
+  /** 买家ID */
+  buyerId: number;
+  /** 买家名称 */
+  buyerName: string;
+  /** 卖家ID */
+  sellerId: number;
+  /** 总金额 */
+  totalAmount: number;
+  /** 已付金额 */
+  paidAmount: number;
+  /** 退款金额 */
+  refundAmount: number;
+  /** 订单状态 (0:待付款 1:已完成 2:已关闭 3:售后中) */
+  orderStatus: number;
+  /** 支付状态 (0:未支付 1:已支付 2:退款中 3:已退款) */
+  payStatus: number;
+  /** 业务ID (岗位ID/培训ID/商品ID) */
+  businessId: number;
+  /** 产品ID */
+  productId: number;
+  /** 支付时间 */
+  payTime: string;
+  /** 完成时间 */
+  completeTime: string;
+  /** 取消时间 */
+  cancelTime: string;
+  /** 备注 */
+  remark: string;
+  /** 创建时间 */
+  createTime: string;
+
+  // 以下是前端显示需要的扩展字段(由后端VO提供)
+  /** 产品名称 */
+  productName?: string;
+  /** 产品图片 */
+  productImg?: string;
+  /** 用户手机号 */
+  phone?: string;
+  /** 用户头像 */
+  userAvatar?: string;
+  /** 订单来源 */
+  source?: string;
+  /** 定金 */
+  deposit?: number;
+  /** 尾款 */
+  balance?: number;
+  /** 数量 */
+  quantity?: number;
+  /** 客户单号 */
+  customerSn?: string;
+}
+
+export interface OrderQuery extends PageQuery {
+  orderNo?: string;
+  buyerName?: string;
+  orderStatus?: string | number;
+  customerSn?: string;
+}

+ 0 - 15
src/api/system/tag.ts

@@ -1,15 +0,0 @@
-import request from '@/utils/request';
-import { AxiosPromise } from 'axios';
-
-/**
- * 查询标签列表
- * @param query 查询参数
- * @returns 标签列表
- */
-export function listTag(query?: any): AxiosPromise<any> {
-  return request({
-    url: '/system/tag/list',
-    method: 'get',
-    params: query
-  });
-}

+ 2 - 2
src/layout/components/Navbar.vue

@@ -42,13 +42,13 @@
             </el-popover>
           </div>
         </el-tooltip>
-        <el-tooltip content="Github" effect="dark" placement="bottom">
+        <!-- <el-tooltip content="Github" effect="dark" placement="bottom">
           <ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
         </el-tooltip>
 
         <el-tooltip :content="proxy.$t('navbar.document')" effect="dark" placement="bottom">
           <ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />
-        </el-tooltip>
+        </el-tooltip> -->
 
         <el-tooltip :content="proxy.$t('navbar.full')" effect="dark" placement="bottom">
           <screenfull id="screenfull" class="right-menu-item hover-effect" />

+ 49 - 1
src/plugins/download.ts

@@ -20,7 +20,55 @@ export default {
       });
       const isBlob = blobValidate(res.data);
       if (isBlob) {
-        const blob = new Blob([res.data], { type: 'application/octet-stream' });
+        const blob = new Blob([res.data], { type: res.headers['content-type'] || 'application/octet-stream' });
+        FileSaver.saveAs(blob, decodeURIComponent(res.headers['download-filename'] as string));
+      } else {
+        this.printErrMsg(res.data);
+      }
+      downloadLoadingInstance.close();
+    } catch (r) {
+      console.error(r);
+      ElMessage.error('下载文件出现错误,请联系管理员!');
+      downloadLoadingInstance.close();
+    }
+  },
+  async name(url: string, name: string) {
+    url = baseURL + url;
+    downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
+    try {
+      const res = await axios({
+        method: 'get',
+        url: url,
+        responseType: 'blob',
+        headers: globalHeaders()
+      });
+      const isBlob = blobValidate(res.data);
+      if (isBlob) {
+        const blob = new Blob([res.data], { type: res.headers['content-type'] || 'application/octet-stream' });
+        FileSaver.saveAs(blob, name);
+      } else {
+        this.printErrMsg(res.data);
+      }
+      downloadLoadingInstance.close();
+    } catch (r) {
+      console.error(r);
+      ElMessage.error('下载文件出现错误,请联系管理员!');
+      downloadLoadingInstance.close();
+    }
+  },
+  async resource(url: string) {
+    url = baseURL + url;
+    downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
+    try {
+      const res = await axios({
+        method: 'get',
+        url: url,
+        responseType: 'blob',
+        headers: globalHeaders()
+      });
+      const isBlob = blobValidate(res.data);
+      if (isBlob) {
+        const blob = new Blob([res.data], { type: res.headers['content-type'] || 'application/octet-stream' });
         FileSaver.saveAs(blob, decodeURIComponent(res.headers['download-filename'] as string));
       } else {
         this.printErrMsg(res.data);

+ 27 - 12
src/utils/chatSocket.ts

@@ -27,15 +27,26 @@ export interface ChatMessage {
 }
 
 export function connectChat(
-  sessionNo: string,
-  onMessage: (data: ChatMessage) => void,
-  onNotify?: (data: ChatMessage) => void
+  onNotify: (data: ChatMessage) => void,
+  sessionId?: number,
+  onMessage?: (data: ChatMessage) => void
 ) {
-  onMessageCallback = onMessage
   onNotifyCallback = onNotify || null
+  onMessageCallback = onMessage || null
 
   const token = getToken()
-  const wsUrl = import.meta.env.VITE_WS_URL || 'http://localhost:8080/api/chat/ws/chat'
+  // WebSocket URL: 优先使用环境变量,否则根据当前页面协议和域名拼接
+  const baseUrl = import.meta.env.VITE_WS_URL
+  let wsUrl: string
+  if (baseUrl) {
+    wsUrl = baseUrl
+  } else {
+    // 开发环境走 Vite 代理,生产环境走 Nginx 代理
+    const protocol = window.location.protocol === 'https:' ? 'https:' : 'http:'
+    const host = window.location.host
+    const apiPrefix = import.meta.env.VITE_APP_BASE_API || ''
+    wsUrl = `${protocol}//${host}${apiPrefix ? '/' + apiPrefix.replace(/^\//, '') : ''}/api/chat/ws/chat`
+  }
 
   stompClient = new Client({
     webSocketFactory: () => new SockJS(wsUrl),
@@ -49,13 +60,17 @@ export function connectChat(
     onConnect: () => {
       console.log('[ChatSocket] 连接成功')
 
-      stompClient?.subscribe(`/topic/session/${sessionNo}`, (frame: IMessage) => {
-        const data = JSON.parse(frame.body)
-        if (onMessageCallback) {
-          onMessageCallback(data)
-        }
-      })
-
+      // 如果提供了 sessionId,则订阅特定会话的广播
+      if (sessionId) {
+        stompClient?.subscribe(`/topic/session/${sessionId}`, (frame: IMessage) => {
+          const data = JSON.parse(frame.body)
+          if (onMessageCallback) {
+            onMessageCallback(data)
+          }
+        })
+      }
+
+      // 订阅个人通知队列
       stompClient?.subscribe('/user/queue/notify', (frame: IMessage) => {
         const data = JSON.parse(frame.body)
         handleNotify(data)

+ 416 - 129
src/views/index.vue

@@ -1,164 +1,451 @@
 <template>
   <div class="app-container home">
+    <!-- 欢迎区块 -->
     <el-row :gutter="20">
-      <el-col :sm="24" :lg="12" style="padding-left: 20px">
-        <h2>RuoYi-Vue-Plus多租户管理系统</h2>
-        <p>
-          RuoYi-Vue-Plus 是基于 RuoYi-Vue 针对 分布式集群 场景升级(不兼容原框架)
-          <br />
-          * 前端开发框架 Vue3、TS、Element Plus<br />
-          * 后端开发框架 Spring Boot<br />
-          * 容器框架 Undertow 基于 Netty 的高性能容器<br />
-          * 权限认证框架 Sa-Token 支持多终端认证系统<br />
-          * 关系数据库 MySQL 适配 8.X 最低 5.7<br />
-          * 缓存数据库 Redis 适配 6.X 最低 4.X<br />
-          * 数据库框架 Mybatis-Plus 快速 CRUD 增加开发效率<br />
-          * 数据库框架 p6spy 更强劲的 SQL 分析<br />
-          * 多数据源框架 dynamic-datasource 支持主从与多种类数据库异构<br />
-          * 序列化框架 Jackson 统一使用 jackson 高效可靠<br />
-          * Redis客户端 Redisson 性能强劲、API丰富<br />
-          * 分布式限流 Redisson 全局、请求IP、集群ID 多种限流<br />
-          * 分布式锁 Lock4j 注解锁、工具锁 多种多样<br />
-          * 分布式幂等 Lock4j 基于分布式锁实现<br />
-          * 分布式链路追踪 SkyWalking 支持链路追踪、网格分析、度量聚合、可视化<br />
-          * 分布式任务调度 SnailJob 高性能 高可靠 易扩展<br />
-          * 文件存储 Minio 本地存储<br />
-          * 文件存储 七牛、阿里、腾讯 云存储<br />
-          * 监控框架 SpringBoot-Admin 全方位服务监控<br />
-          * 校验框架 Validation 增强接口安全性 严谨性<br />
-          * Excel框架 FastExcel(原Alibaba EasyExcel) 性能优异 扩展性强<br />
-          * 文档框架 SpringDoc、javadoc 无注解零入侵基于java注释<br />
-          * 工具类框架 Hutool、Lombok 减少代码冗余 增加安全性<br />
-          * 代码生成器 适配MP、SpringDoc规范化代码 一键生成前后端代码<br />
-          * 部署方式 Docker 容器编排 一键部署业务集群<br />
-          * 国际化 SpringMessage Spring标准国际化方案<br />
-        </p>
-        <p><b>当前版本:</b> <span>v5.5.3</span></p>
-        <p>
-          <el-tag type="danger">&yen;免费开源</el-tag>
-        </p>
-        <p>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://gitee.com/dromara/RuoYi-Vue-Plus')">访问码云</el-button>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://github.com/dromara/RuoYi-Vue-Plus')">访问GitHub</el-button>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-vue-plus/changlog')"
-            >更新日志</el-button
-          >
-        </p>
+      <el-col :span="24">
+        <el-card class="welcome-card hover-card">
+          <div class="welcome-content">
+            <div class="user-info">
+              <h2>欢迎使用审计之家总控管理系统</h2>
+              <div class="welcome-desc">
+                <span>您好,{{ userStore.nickname || '管理员' }}!系统运行平稳,这是您今天的业务概览。</span>
+                <el-tag size="small" effect="dark" type="success" class="ml10">系统版本 v5.5.3</el-tag>
+              </div>
+            </div>
+            <div class="welcome-img">
+              <svg-icon icon-class="guide" style="font-size: 80px; opacity: 0.2" />
+            </div>
+          </div>
+        </el-card>
       </el-col>
+    </el-row>
+
+    <!-- 核心指标区块 -->
+    <el-row :gutter="20" class="mt20">
+      <el-col :sm="12" :lg="6" v-for="(stat, index) in statConfig" :key="index">
+        <el-card shadow="hover" class="stat-card hover-card" v-loading="loading[stat.key]">
+          <template #header>
+            <div class="card-header">
+              <span class="header-title">{{ stat.title }}</span>
+              <el-tag :type="stat.tagType" size="small">{{ stat.tag }}</el-tag>
+            </div>
+          </template>
+          <div class="stat-body">
+            <div class="stat-main">
+              <div class="stat-value">{{ stats[stat.key + 'Count'] }}</div>
+              <div class="stat-icon" :style="{ color: stat.iconColor }">
+                <el-icon :size="32"><component :is="stat.icon" /></el-icon>
+              </div>
+            </div>
+            <div class="stat-footer">
+              <span class="footer-label">{{ stat.footerLabel }}</span>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="20" class="mt20">
+      <!-- 平台概况与职责 -->
+      <el-col :sm="24" :lg="16">
+        <el-card shadow="hover" class="intro-card hover-card">
+          <template #header>
+            <div class="card-header">
+              <span class="header-title"><el-icon class="mr5"><InfoFilled /></el-icon>平台概况</span>
+            </div>
+          </template>
+          <div class="intro-body">
+            <div class="platform-header">
+              <h3 class="platform-title">审计之家 (Audit House)</h3>
+              <div class="feature-tags">
+                <el-tag effect="plain" size="small">多租户</el-tag>
+                <el-tag effect="plain" size="small" type="success">智能测评</el-tag>
+                <el-tag effect="plain" size="small" type="info">在线教育</el-tag>
+                <el-tag effect="plain" size="small" type="warning">人才匹配</el-tag>
+                <el-tag effect="plain" size="small" type="danger">微信支付</el-tag>
+              </div>
+            </div>
+            <p class="platform-desc">
+              审计之家致力于打造专业的审计行业一站式服务平台。通过先进的数字化技术,连接企业与专业审计人才,
+              提供职业测评、技能培训、求职招聘及行业咨询等全方位服务。
+            </p>
+            <el-divider content-position="left">总控端核心职责</el-divider>
+            <div class="duty-grid">
+              <div class="duty-item">
+                <el-icon color="#409EFF"><Management /></el-icon>
+                <span>租户接入与资源分配</span>
+              </div>
+              <div class="duty-item">
+                <el-icon color="#67C23A"><Collection /></el-icon>
+                <span>测评库与培训资源维护</span>
+              </div>
+              <div class="duty-item">
+                <el-icon color="#E6A23C"><DataAnalysis /></el-icon>
+                <span>系统监控与业务流分析</span>
+              </div>
+              <div class="duty-item">
+                <el-icon color="#F56C6C"><Checked /></el-icon>
+                <span>企业与人才资质审核</span>
+              </div>
+            </div>
+          </div>
+        </el-card>
 
-      <el-col :sm="24" :lg="12" style="padding-left: 20px">
-        <h2>RuoYi-Cloud-Plus多租户微服务管理系统</h2>
-        <p>
-          RuoYi-Cloud-Plus 微服务通用权限管理系统 重写 RuoYi-Cloud 全方位升级(不兼容原框架)
-          <br />
-          * 前端开发框架 Vue3、TS、Element UI<br />
-          * 后端开发框架 Spring Boot<br />
-          * 微服务开发框架 Spring Cloud、Spring Cloud Alibaba<br />
-          * 容器框架 Undertow 基于 XNIO 的高性能容器<br />
-          * 权限认证框架 Sa-Token、Jwt 支持多终端认证系统<br />
-          * 关系数据库 MySQL 适配 8.X 最低 5.7<br />
-          * 关系数据库 Oracle 适配 11g 12c<br />
-          * 关系数据库 PostgreSQL 适配 13 14<br />
-          * 关系数据库 SQLServer 适配 2017 2019<br />
-          * 缓存数据库 Redis 适配 6.X 最低 5.X<br />
-          * 分布式注册中心 Alibaba Nacos 采用2.X 基于GRPC通信高性能<br />
-          * 分布式配置中心 Alibaba Nacos 采用2.X 基于GRPC通信高性能<br />
-          * 服务网关 Spring Cloud Gateway 响应式高性能网关<br />
-          * 负载均衡 Spring Cloud Loadbalancer 负载均衡处理<br />
-          * RPC远程调用 Apache Dubbo 原生态使用体验、高性能<br />
-          * 分布式限流熔断 Alibaba Sentinel 无侵入、高扩展<br />
-          * 分布式事务 Alibaba Seata 无侵入、高扩展 支持 四种模式<br />
-          * 分布式消息队列 Apache Kafka 高性能高速度<br />
-          * 分布式消息队列 Apache RocketMQ 高可用功能多样<br />
-          * 分布式消息队列 RabbitMQ 支持各种扩展插件功能多样性<br />
-          * 分布式搜索引擎 ElasticSearch 业界知名<br />
-          * 分布式链路追踪 Apache SkyWalking 链路追踪、网格分析、度量聚合、可视化<br />
-          * 分布式日志中心 ELK 业界成熟解决方案<br />
-          * 分布式监控 Prometheus、Grafana 全方位性能监控<br />
-          * 其余与 Vue 版本一致<br />
-        </p>
-        <p><b>当前版本:</b> <span>v2.5.3</span></p>
-        <p>
-          <el-tag type="danger">&yen;免费开源</el-tag>
-        </p>
-        <p>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://gitee.com/dromara/RuoYi-Cloud-Plus')">访问码云</el-button>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://github.com/dromara/RuoYi-Cloud-Plus')">访问GitHub</el-button>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-cloud-plus/changlog')"
-            >更新日志</el-button
-          >
-        </p>
+        <!-- 最近订单列表 - 填充底部空白 -->
+        <el-card shadow="hover" class="mt20 hover-card recent-order-card" v-loading="loading.order">
+          <template #header>
+            <div class="card-header">
+              <span class="header-title"><el-icon class="mr5"><List /></el-icon>最新订单概览</span>
+              <el-button type="primary" link @click="goTarget('/system/order/index')">查看全部</el-button>
+            </div>
+          </template>
+          <el-table :data="recentOrders" size="small" style="width: 100%">
+            <el-table-column prop="orderNo" label="订单编号" width="180" show-overflow-tooltip>
+              <template #default="scope">
+                <el-link type="primary" :underline="false" @click="handleOrderDetail(scope.row)">{{ scope.row.orderNo }}</el-link>
+              </template>
+            </el-table-column>
+            <el-table-column prop="studentName" label="学员" width="100" />
+            <el-table-column prop="totalAmount" label="金额">
+              <template #default="scope">
+                <span class="text-danger">¥ {{ scope.row.totalAmount }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="payStatus" label="状态" width="100">
+              <template #default="scope">
+                <el-tag :type="scope.row.payStatus === 2 ? 'success' : 'warning'" size="small">
+                  {{ scope.row.payStatus === 2 ? '已支付' : '待支付' }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="createTime" label="下单时间" width="160" />
+          </el-table>
+        </el-card>
+      </el-col>
+
+      <!-- 右侧辅助区块 -->
+      <el-col :sm="24" :lg="8">
+        <el-card shadow="hover" class="link-card hover-card">
+          <template #header>
+            <div class="card-header">
+              <span class="header-title"><el-icon class="mr5"><Pointer /></el-icon>快速入口</span>
+            </div>
+          </template>
+          <div class="link-grid">
+            <div class="link-item" @click="goTarget('/system/tenant')">
+              <el-icon color="#409EFF"><User /></el-icon>
+              <span>租户管理</span>
+            </div>
+            <div class="link-item" @click="goTarget('/system/assessment')">
+              <el-icon color="#67C23A"><Reading /></el-icon>
+              <span>测评中心</span>
+            </div>
+            <div class="link-item" @click="goTarget('/monitor/online')">
+              <el-icon color="#E6A23C"><Monitor /></el-icon>
+              <span>在线用户</span>
+            </div>
+            <div class="link-item" @click="goTarget('/system/order/index')">
+              <el-icon color="#909399"><Document /></el-icon>
+              <span>订单查看</span>
+            </div>
+          </div>
+        </el-card>
+
+        <el-card shadow="hover" class="status-card mt20 hover-card">
+          <template #header>
+            <div class="card-header">
+              <span class="header-title"><el-icon class="mr5"><Cpu /></el-icon>系统状态</span>
+            </div>
+          </template>
+          <div class="status-body">
+            <div class="status-item" v-for="(item, index) in systemStatus" :key="index">
+              <div class="status-label">
+                <el-icon :color="item.iconColor" class="mr5"><component :is="item.icon" /></el-icon>
+                <span>{{ item.label }}</span>
+              </div>
+              <el-tag size="small" :type="item.tagType">{{ item.tag }}</el-tag>
+            </div>
+          </div>
+        </el-card>
+
+        <!-- 系统通知/公告占位 -->
+        <el-card shadow="hover" class="mt20 hover-card notice-card">
+          <template #header>
+            <div class="card-header">
+              <span class="header-title"><el-icon class="mr5"><Bell /></el-icon>系统公告</span>
+            </div>
+          </template>
+          <div class="notice-list">
+            <div class="notice-item" v-for="i in 3" :key="i">
+              <span class="notice-dot"></span>
+              <span class="notice-text">关于 2024 年第二季度平台资源维护升级的通知...</span>
+              <span class="notice-time">04-14</span>
+            </div>
+          </div>
+        </el-card>
       </el-col>
     </el-row>
-    <el-divider />
   </div>
 </template>
 
 <script setup name="Index" lang="ts">
+import { useRouter } from 'vue-router';
+import { useUserStore } from '@/store/modules/user';
+import { listStudent } from '@/api/main/student';
+import { listEvaluation } from '@/api/main/evaluation';
+import { listTraining } from '@/api/main/training';
+import { listOrder } from '@/api/system/order';
+
+const router = useRouter();
+const userStore = useUserStore();
+
+const loading = reactive({
+  student: false,
+  evaluation: false,
+  training: false,
+  order: false
+});
+
+const stats = reactive({
+  studentCount: 0,
+  evaluationCount: 0,
+  trainingCount: 0,
+  orderCount: 0
+});
+
+const recentOrders = ref([]);
+
+const statConfig = [
+  { key: 'student', title: '累计学员', tag: '实时', tagType: 'success', icon: 'User', iconColor: '#409EFF', footerLabel: '平台注册学员总数' },
+  { key: 'evaluation', title: '测评任务', tag: '活跃', tagType: 'info', icon: 'EditPen', iconColor: '#67C23A', footerLabel: '已发布的测评项目' },
+  { key: 'training', title: '培训项目', tag: '热门', tagType: 'warning', icon: 'Reading', iconColor: '#E6A23C', footerLabel: '已发布的培训课程' },
+  { key: 'order', title: '订单数量', tag: '业务', tagType: 'danger', icon: 'Wallet', iconColor: '#F56C6C', footerLabel: '全平台累计订单数' }
+];
+
+const systemStatus = [
+  { label: '后端服务', tag: '运行中', tagType: 'success', icon: 'Connection', iconColor: '#67C23A' },
+  { label: 'Redis 缓存', tag: '连接正常', tagType: 'success', icon: 'Orange', iconColor: '#67C23A' },
+  { label: '数据库', tag: '负载正常', tagType: 'success', icon: 'Coin', iconColor: '#67C23A' },
+  { label: '文件存储', tag: '可用', tagType: 'info', icon: 'Files', iconColor: '#909399' }
+];
+
 const goTarget = (url: string) => {
-  window.open(url, '__blank');
+  if (url.startsWith('http')) {
+    window.open(url, '__blank');
+  } else {
+    router.push(url);
+  }
+};
+
+const handleOrderDetail = (row: any) => {
+  router.push({
+    path: '/system/order/detail',
+    query: { id: row.id }
+  });
 };
+
+/** 查询统计数据 */
+const getStats = async () => {
+  const query = { pageNum: 1, pageSize: 1 };
+  
+  loading.student = true;
+  listStudent(query).then((res: any) => {
+    stats.studentCount = res.total || 0;
+  }).finally(() => {
+    loading.student = false;
+  });
+
+  loading.evaluation = true;
+  listEvaluation(query).then((res: any) => {
+    stats.evaluationCount = res.total || 0;
+  }).finally(() => {
+    loading.evaluation = false;
+  });
+
+  loading.training = true;
+  listTraining(query).then((res: any) => {
+    stats.trainingCount = res.total || 0;
+  }).finally(() => {
+    loading.training = false;
+  });
+
+  loading.order = true;
+  listOrder({ pageNum: 1, pageSize: 6 }).then((res: any) => {
+    stats.orderCount = res.total || 0;
+    recentOrders.value = res.rows || [];
+  }).finally(() => {
+    loading.order = false;
+  });
+};
+
+onMounted(() => {
+  getStats();
+});
 </script>
 
 <style lang="scss" scoped>
 .home {
-  blockquote {
-    padding: 10px 20px;
-    margin: 0 0 20px;
-    font-size: 17.5px;
-    border-left: 5px solid #eee;
-  }
-  hr {
-    margin-top: 20px;
-    margin-bottom: 20px;
-    border: 0;
-    border-top: 1px solid #eee;
-  }
-  .col-item {
-    margin-bottom: 20px;
+  padding: 20px;
+  background-color: #f0f2f5;
+  min-height: calc(100vh - 84px);
+
+  .mr5 { margin-right: 5px; }
+  .ml10 { margin-left: 10px; }
+  .mt20 { margin-top: 20px; }
+
+  .hover-card {
+    transition: all 0.3s;
+    border: none;
+    &:hover {
+      transform: translateY(-5px);
+      box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.1);
+    }
   }
 
-  ul {
-    padding: 0;
-    margin: 0;
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    .header-title {
+      font-weight: 600;
+      color: #303133;
+      display: flex;
+      align-items: center;
+    }
   }
 
-  font-family: 'open sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-  font-size: 13px;
-  color: #676a6c;
-  overflow-x: hidden;
+  .welcome-card {
+    background: linear-gradient(135deg, #1890ff 0%, #36cfc9 100%);
+    color: white;
+    .welcome-content {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 10px 0;
+      h2 { margin: 0 0 10px; font-weight: 600; }
+      .welcome-desc { 
+        font-size: 14px; 
+        opacity: 0.9;
+        display: flex;
+        align-items: center;
+      }
+    }
+  }
 
-  ul {
-    list-style-type: none;
+  .stat-card {
+    .stat-body {
+      .stat-main {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 10px;
+        .stat-value {
+          font-size: 28px;
+          font-weight: bold;
+          color: #303133;
+        }
+      }
+      .stat-footer {
+        font-size: 12px;
+        color: #909399;
+      }
+    }
   }
 
-  h4 {
-    margin-top: 0px;
+  .intro-card {
+    .intro-body {
+      .platform-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 15px;
+        .platform-title { margin: 0; color: #303133; }
+        .feature-tags { display: flex; gap: 5px; }
+      }
+      .platform-desc { color: #606266; line-height: 1.6; font-size: 14px; margin-bottom: 20px; }
+      .duty-grid {
+        display: grid;
+        grid-template-columns: repeat(2, 1fr);
+        gap: 15px;
+        .duty-item {
+          display: flex;
+          align-items: center;
+          font-size: 14px;
+          color: #606266;
+          .el-icon { font-size: 18px; margin-right: 10px; }
+        }
+      }
+    }
   }
 
-  h2 {
-    margin-top: 10px;
-    font-size: 26px;
-    font-weight: 100;
+  .link-card {
+    .link-grid {
+      display: grid;
+      grid-template-columns: repeat(2, 1fr);
+      gap: 10px;
+      .link-item {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        padding: 15px;
+        background: #f8f9fa;
+        border-radius: 4px;
+        cursor: pointer;
+        transition: background 0.2s;
+        &:hover { background: #eef1f6; }
+        .el-icon { font-size: 24px; margin-bottom: 8px; }
+        span { font-size: 13px; color: #606266; }
+      }
+    }
   }
 
-  p {
-    margin-top: 10px;
+  .status-card {
+    .status-body {
+      .status-item {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 15px;
+        &:last-child { margin-bottom: 0; }
+        .status-label {
+          display: flex;
+          align-items: center;
+          font-size: 14px;
+          color: #606266;
+        }
+      }
+    }
+  }
 
-    b {
-      font-weight: 700;
+  .notice-card {
+    .notice-list {
+      .notice-item {
+        display: flex;
+        align-items: center;
+        margin-bottom: 12px;
+        font-size: 13px;
+        color: #606266;
+        cursor: pointer;
+        &:hover { color: #1890ff; }
+        &:last-child { margin-bottom: 0; }
+        .notice-dot {
+          width: 6px;
+          height: 6px;
+          background: #1890ff;
+          border-radius: 50%;
+          margin-right: 10px;
+        }
+        .notice-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+        .notice-time { color: #909399; margin-left: 10px; }
+      }
     }
   }
 
-  .update-log {
-    ol {
-      display: block;
-      list-style-type: decimal;
-      margin-block-start: 1em;
-      margin-block-end: 1em;
-      margin-inline-start: 0;
-      margin-inline-end: 0;
-      padding-inline-start: 40px;
+  .recent-order-card {
+    :deep(.el-table) {
+      &::before { display: none; }
+      th.el-table__cell { background: #f8f9fa; color: #303133; }
     }
   }
 }

+ 1 - 1
src/views/login.vue

@@ -73,7 +73,7 @@
     </el-form>
     <!--  底部  -->
     <div class="el-login-footer">
-      <span>Copyright © 2018-2026 疯狂的狮子Li All Rights Reserved.</span>
+      <span>Copyright © 2024-2026 审计之家 All Rights Reserved.</span>
     </div>
   </div>
 </template>

+ 123 - 122
src/views/system/assessment/detail.vue

@@ -25,13 +25,20 @@
                   </el-select>
                 </el-form-item>
 
-                <el-form-item label="岗位" prop="position">
-                  <el-cascader
-                    v-model="formData.position"
-                    :props="cascaderProps"
+                <el-form-item label="岗位" prop="positionId">
+                  <el-select
+                    v-model="formData.positionId"
                     placeholder="请选择"
                     style="width: 100%"
-                  />
+                    filterable
+                  >
+                    <el-option
+                      v-for="item in positionOptions"
+                      :key="item.id"
+                      :label="item.postName"
+                      :value="item.id"
+                    />
+                  </el-select>
                 </el-form-item>
 
                 <el-form-item label="岗位类型" prop="positionType">
@@ -72,9 +79,9 @@
                   <div class="upload-tip" v-if="!isView">点击上传或拖拽图片至此处<br/>支持JPG、PNG格式,建议尺寸800*800px</div>
                 </el-form-item>
                 
-                <el-form-item label="商品相册" prop="galleryImages">
-                  <image-upload v-model="formData.galleryImages" :limit="5" />
-                  <div class="upload-tip" v-if="!isView">最多可上传5张图片,第一张将作为详情页首图</div>
+                <el-form-item label="商品相册" prop="imageAlbum">
+                  <image-upload v-model="formData.imageAlbum" :limit="10" />
+                  <div class="upload-tip" v-if="!isView">最多可上传10张图片,这些图片将在小程序详情页顶部轮播展示</div>
                 </el-form-item>
               </el-col>
             </el-row>
@@ -120,14 +127,8 @@
                       <el-input-number v-model="item.thirdExamPassMark" :min="0" :max="item.thirdExamTotalScore || 100" controls-position="right" style="width: 100%" />
                     </el-form-item>
                   </el-col>
-                  <el-col :span="24" class="ability-ops" v-if="!isView">
-                    <el-button type="danger" link icon="Delete" @click="removeAbility(index)" v-if="formData.abilityConfigs.length > 1">删除能力</el-button>
-                  </el-col>
                 </el-row>
               </div>
-              <div class="add-ability-btn" v-if="!isView">
-                <el-button type="primary" plain icon="Plus" @click="addAbility">添加测评能力</el-button>
-              </div>
             </div>
           </div>
 
@@ -244,7 +245,7 @@ import { ref, reactive, onMounted, onActivated, nextTick, getCurrentInstance, to
 import { SuccessFilled } from '@element-plus/icons-vue';
 import { getEvaluation, addEvaluation, updateEvaluation, getThirdPartyExamList } from '@/api/main/evaluation';
 import { listTag } from '@/api/system/tag';
-import { listIndustry, listSkill } from '@/api/system/industry';
+import { listPosition } from '@/api/main/position';
 import { useRoute, useRouter } from 'vue-router';
 import Editor from '@/components/Editor/index.vue';
 import ImageUpload from '@/components/ImageUpload/index.vue';
@@ -263,52 +264,7 @@ const isFinished = ref(false);
 const formRef = ref();
 const isEdit = ref(false);
 const isView = ref(false);
-const cascaderProps = {
-  lazy: true,
-  lazyLoad: async (node: any, resolve: any) => {
-    const { level, value } = node;
-    let nodes = [];
-    try {
-      if (level === 0) {
-        // Load level 1: Industries
-        const res: any = await listIndustry({ parentId: 0 });
-        // The interceptor returns res.data directly if code is 200
-        const listData = Array.isArray(res) ? res : (res.data || res.rows || []);
-        nodes = listData.map((item: any) => ({
-          value: item.industryId,
-          label: item.industryName,
-          leaf: false
-        }));
-      } else if (level === 1) {
-        // Load level 2: Sub-industries
-        const res: any = await listIndustry({ parentId: value });
-        const listData = Array.isArray(res) ? res : (res.data || res.rows || []);
-        nodes = listData.map((item: any) => ({
-          value: item.industryId,
-          label: item.industryName,
-          leaf: false
-        }));
-      } else if (level === 2) {
-        // Load level 3: Skills
-        const res: any = await listSkill({ industryId: value });
-        const listData = Array.isArray(res) ? res : (res.data || res.rows || []);
-        nodes = listData.map((item: any) => ({
-          value: item.skillId,
-          label: item.skillName,
-          leaf: true
-        }));
-      }
-      resolve(nodes);
-    } catch (error) {
-      console.error('Cascader lazy load failed:', error);
-      resolve([]);
-    }
-  },
-  checkStrictly: false,
-  emitPath: false, // 只返回选中的最后一级的值
-  formatter: (labels: any[]) => labels.join(' / ')
-};
-const positionOptions = ref<string[]>(['审计员', '会计师', '税务师']); // 暂时 Mock 岗位数据
+const positionOptions = ref<any[]>([]);
 const tagOptions = ref<any[]>([]);
 
 // 试卷选择对话框相关
@@ -322,46 +278,32 @@ const examDialog = reactive({
   currentAbilityIndex: -1 // 当前正在为哪个能力选择试卷
 });
 
-const formData = ref<any>({
+const createDefaultFormData = (): any => ({
   id: undefined,
   evaluationName: '',
   grade: '',
-  position: '',
+  positionId: undefined,
   positionType: '',
-  detail: '',
-  tags: [], // 多选下拉
-  mainImage: '',
-  galleryImages: '',
-  price: 0,
+  status: '0',
+  description: '',
+  tags: '',
+  remark: '',
   switchStatus: '0',
   onlineStatus: '1',
   onlineTimeRange: [],
   abilityConfigs: [
-    { abilityName: '能力A', thirdExamInfoId: '', thirdExamName: '', thirdExamTime: 60, thirdExamPassMark: 60, thirdExamTotalScore: 100 }
+    { abilityName: '能力A', thirdExamInfoId: '', thirdExamName: '', thirdExamTime: 60, thirdExamPassMark: 60, thirdExamTotalScore: 100, thirdExamLink: '' },
+    { abilityName: '能力B', thirdExamInfoId: '', thirdExamName: '', thirdExamTime: 60, thirdExamPassMark: 60, thirdExamTotalScore: 100, thirdExamLink: '' },
+    { abilityName: '能力C', thirdExamInfoId: '', thirdExamName: '', thirdExamTime: 60, thirdExamPassMark: 60, thirdExamTotalScore: 100, thirdExamLink: '' }
   ]
 });
 
+const formData = ref<EvaluationVO>(createDefaultFormData());
+
 /** 重置表单 */
 const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    evaluationName: '',
-    grade: '',
-    position: '',
-    positionType: '',
-    detail: '',
-    tags: [],
-    mainImage: '',
-    galleryImages: '',
-    price: 0,
-    switchStatus: '0',
-    onlineStatus: '1',
-    onlineTimeRange: [],
-    abilityConfigs: [
-      { abilityName: '能力A', thirdExamInfoId: '', thirdExamName: '', thirdExamTime: 60, thirdExamPassMark: 60, thirdExamTotalScore: 100 }
-    ]
-  };
-  formRef.value?.clearValidate();
+  formData.value = createDefaultFormData();
+  formRef.value?.resetFields();
 };
 
 const rules = {
@@ -374,7 +316,7 @@ const rules = {
   grade: [
     { required: true, message: '级别不能为空', trigger: 'change' }
   ],
-  position: [
+  positionId: [
     { required: true, message: '岗位不能为空', trigger: 'change' }
   ],
   positionType: [
@@ -417,7 +359,8 @@ const addAbility = () => {
     thirdExamName: '',
     thirdExamTime: 60,
     thirdExamPassMark: 60,
-    thirdExamTotalScore: 100
+    thirdExamTotalScore: 100,
+    thirdExamLink: ''
   });
 };
 
@@ -488,8 +431,11 @@ const selectExam = (exam: any) => {
     ability.thirdExamName = exam.examName || exam.name;
     ability.thirdExamInfoId = exam.examInfoId || exam.id;
     ability.thirdExamTime = exam.examTime || exam.duration || 0;
-    ability.thirdExamTotalScore = exam.examTotalScore || 100;
     ability.thirdExamPassMark = exam.passMark || 0;
+    ability.thirdExamTotalScore = exam.examTotalScore || exam.totalScore || 0;
+    if (exam.examLink) {
+      ability.thirdExamLink = exam.examLink;
+    }
   }
   
   examDialog.visible = false;
@@ -504,7 +450,7 @@ const getDetail = async (id: string | number) => {
     const data = res.data;
     
     let decodedDetail = data.detail || '';
-    if (decodedDetail && !decodedDetail.includes('<')) {
+    if (decodedDetail && !decodedDetail.includes('<') && /^[A-Za-z0-9+/=\r\n]+$/.test(decodedDetail) && decodedDetail.length % 4 === 0) {
       try {
         decodedDetail = decodeURIComponent(escape(atob(decodedDetail)));
       } catch (e) {
@@ -516,24 +462,44 @@ const getDetail = async (id: string | number) => {
       id: data.id,
       evaluationName: data.evaluationName || '',
       grade: data.grade ? String(data.grade) : '',
-      position: data.position ? String(data.position) : '',
+      positionId: data.positionId,
       positionType: data.positionType ? String(data.positionType) : '',
-      detail: data.detail || '',
+      detail: decodedDetail,
       tags: data.tags ? data.tags.split(',') : [],
       mainImage: data.mainImage || '',
-      galleryImages: data.galleryImages || '',
+      imageAlbum: data.imageAlbum || '',
       price: data.price || 0,
       status: data.status || '0',
       onlineStatus: data.status === '0' ? '3' : '1',
       onlineTimeRange: (data.onTime && data.downTime) ? [data.onTime, data.downTime] : [],
-      abilityConfigs: data.abilityConfigs || []
+      abilityConfigs: (data.abilityConfigs || []).map((item: any) => ({
+        id: item.id,
+        evaluationId: item.evaluationId,
+        abilityName: item.abilityName || '',
+        thirdExamInfoId: item.thirdExamInfoId || '',
+        thirdExamName: item.thirdExamName || '',
+        thirdExamTime: item.thirdExamTime || 60,
+        thirdExamPassMark: item.thirdExamPassMark || 60,
+        thirdExamTotalScore: item.thirdExamTotalScore || 100,
+        thirdExamLink: item.thirdExamLink || ''
+      }))
     };
     
-    // 如果没有能力配置,初始化一个默认的
-    if (!formData.value.abilityConfigs || formData.value.abilityConfigs.length === 0) {
-      formData.value.abilityConfigs = [
-        { abilityName: '基础能力', thirdExamInfoId: '', thirdExamName: '', thirdExamTime: 60, thirdExamPassMark: 60, thirdExamTotalScore: 100 }
-      ];
+    // 如果能力配置不足3个,补足到3个
+    const currentLength = formData.value.abilityConfigs.length;
+    if (currentLength < 3) {
+      const names = ['能力A', '能力B', '能力C'];
+      for (let i = currentLength; i < 3; i++) {
+        formData.value.abilityConfigs.push({
+          abilityName: names[i],
+          thirdExamInfoId: '',
+          thirdExamName: '',
+          thirdExamTime: 60,
+          thirdExamPassMark: 60,
+          thirdExamTotalScore: 100,
+          thirdExamLink: ''
+        });
+      }
     }
   } catch (error) {
     console.error('获取详情失败:', error);
@@ -543,18 +509,36 @@ const getDetail = async (id: string | number) => {
   }
 };
 
-/** 提交表单 */
-const handleSubmit = async () => {
-  try {
-    await formRef.value?.validate();
-    loading.value = true;
-    
-    const submitData = { ...formData.value };
-    // 标签转回字符串
-    submitData.tags = Array.isArray(submitData.tags) ? submitData.tags.join(',') : submitData.tags;
-    
-    // 设置上架/下架时间
-    if (submitData.onlineStatus === '2' && submitData.onlineTimeRange?.length === 2) {
+ /** 提交表单 */
+ const handleSubmit = async () => {
+     try {
+      if (loading.value) {
+        return;
+      }
+      await formRef.value?.validate();
+      loading.value = true;
+      
+      const submitData = {
+        ...formData.value,
+        abilityConfigs: (formData.value.abilityConfigs || []).map((item: any, index: number) => ({
+          id: item.id,
+          evaluationId: item.evaluationId,
+          abilityName: item.abilityName,
+          thirdExamInfoId: item.thirdExamInfoId,
+          thirdExamName: item.thirdExamName,
+          thirdExamTime: item.thirdExamTime,
+          thirdExamPassMark: item.thirdExamPassMark,
+          thirdExamTotalScore: item.thirdExamTotalScore,
+          thirdExamLink: item.thirdExamLink,
+          sortOrder: index
+        }))
+      };
+      
+      // 标签转回字符串
+      submitData.tags = Array.isArray(submitData.tags) ? submitData.tags.join(',') : submitData.tags;
+      
+      // 设置上架/下架时间
+      if (submitData.onlineStatus === '2' && submitData.onlineTimeRange?.length === 2) {
       submitData.onTime = submitData.onlineTimeRange[0];
       submitData.downTime = submitData.onlineTimeRange[1];
     } else {
@@ -567,15 +551,21 @@ const handleSubmit = async () => {
       submitData.status = '0';
     } else {
       submitData.status = '1';
-    }
-    
-    if (isEdit.value) {
-      await updateEvaluation(submitData);
-      proxy?.$modal.msgSuccess('修改成功');
-    } else {
-      await addEvaluation(submitData);
-      proxy?.$modal.msgSuccess('新增成功');
-    }
+      }
+
+      const hasValidId = submitData.id !== undefined && submitData.id !== null && submitData.id !== '';
+      
+      if (isEdit.value) {
+        if (!hasValidId) {
+          proxy?.$modal.msgError('当前编辑数据缺少ID,请刷新后重试');
+          return;
+        }
+        await updateEvaluation(submitData);
+        proxy?.$modal.msgSuccess('修改成功');
+      } else {
+        await addEvaluation(submitData);
+        proxy?.$modal.msgSuccess('新增成功');
+      }
     
     goBack();
   } catch (error) {
@@ -601,6 +591,16 @@ const handleSubmitDraft = async () => {
   }
 };
 
+/** 加载岗位列表 */
+const getPositionList = async () => {
+  try {
+    const res: any = await listPosition({ pageNum: 1, pageSize: 999 });
+    positionOptions.value = res.rows || [];
+  } catch (error) {
+    console.error('获取岗位列表失败:', error);
+  }
+};
+
 /** 加载标签列表 */
 const getTagList = async () => {
   try {
@@ -613,6 +613,7 @@ const getTagList = async () => {
 };
 
 onMounted(() => {
+  getPositionList();
   getTagList();
   handleRouteChange(route.query);
 });

+ 90 - 25
src/views/system/assessment/index.vue

@@ -2,43 +2,74 @@
   <div class="app-container">
     <el-card shadow="never" class="search-card" style="min-width: 1600px !important; width: 100% !important;">
       <!-- 搜索条件 -->
-      <el-form :model="queryParams" class="search-form">
-        <el-form-item label="岗位等级">
-          <el-select v-model="queryParams.level" placeholder="全部" clearable style="width: 180px">
-            <el-option label="全部" value="" />
+      <el-form :model="queryParams" ref="queryForm" class="search-form">
+        <el-form-item label="岗位等级" prop="grade">
+          <el-select v-model="queryParams.grade" placeholder="全部" clearable style="width: 180px">
+            <el-option
+              v-for="dict in main_position_level"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
           </el-select>
         </el-form-item>
-        <el-form-item label="测评状态">
+        <el-form-item label="测评状态" prop="status">
           <el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 180px">
-            <el-option label="全部" value="" />
+            <el-option
+              v-for="dict in evaluationStatusOptions"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
           </el-select>
         </el-form-item>
-        <el-form-item label="岗位类型">
+        <el-form-item label="岗位类型" prop="positionType">
           <el-select v-model="queryParams.positionType" placeholder="全部" clearable style="width: 180px">
-            <el-option label="全部" value="" />
+            <el-option
+              v-for="dict in main_position_type"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
           </el-select>
         </el-form-item>
         <el-form-item label="价格区间">
-          <el-select v-model="queryParams.priceRange" placeholder="全部" clearable style="width: 180px">
-            <el-option label="全部" value="" />
-          </el-select>
+          <div class="price-range">
+            <el-input-number
+              v-model="queryParams.minPrice"
+              placeholder="最低价"
+              :controls="false"
+              :precision="2"
+              style="width: 100px"
+            />
+            <span class="range-separator">-</span>
+            <el-input-number
+              v-model="queryParams.maxPrice"
+              placeholder="最高价"
+              :controls="false"
+              :precision="2"
+              style="width: 100px"
+            />
+          </div>
         </el-form-item>
         <el-form-item label="创建时间">
           <el-date-picker
-            v-model="queryParams.createTime"
+            v-model="createTimeRange"
             type="daterange"
             range-separator="-"
             start-placeholder="开始日期"
             end-placeholder="结束日期"
             style="width: 240px"
+            value-format="YYYY-MM-DD"
           />
         </el-form-item>
-        <el-form-item label="测评名称/ID">
+        <el-form-item label="测评名称/ID" prop="evaluationName">
           <el-input
-            v-model="queryParams.keyword"
+            v-model="queryParams.evaluationName"
             placeholder="请输入"
             clearable
             style="width: 240px"
+            @keyup.enter="handleQuery"
           />
         </el-form-item>
         <div class="search-buttons">
@@ -92,6 +123,7 @@
             </div>
           </template>
         </el-table-column>
+        <el-table-column label="岗位" prop="positionName" align="center" width="150" show-overflow-tooltip />
         <el-table-column label="岗位类型" prop="positionType" align="center" width="120">
           <template #default="scope">
             <dict-tag :options="main_position_type" :value="scope.row.positionType" />
@@ -164,25 +196,41 @@ import { getCurrentInstance, ref, onMounted, toRefs, watch } from 'vue';
 const router = useRouter();
 const route = useRoute();
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const { main_position_type } = toRefs<any>(
-  proxy?.useDict('main_position_type')
+const { main_position_type, main_position_level } = toRefs<any>(
+  proxy?.useDict('main_position_type', 'main_position_level')
 );
 
+// 测评状态
+const evaluationStatusOptions = [
+  { label: '草稿', value: '0' },
+  { label: '在售', value: '1' },
+  { label: '已下架', value: '2' }
+];
+
+// 价格区间
+const priceRangeOptions = [
+  { label: '0-100', value: '0-100' },
+  { label: '100-500', value: '100-500' },
+  { label: '500-1000', value: '500-1000' },
+  { label: '1000以上', value: '1000-0' }
+];
+
 const loading = ref(false);
 const assessmentList = ref<any[]>([]);
 const total = ref(0);
 const selectedRows = ref<any[]>([]);
 const tableRef = ref();
+const createTimeRange = ref<any>([]);
 
 const queryParams = ref({
   pageNum: 1,
   pageSize: 10,
-  level: '',
+  grade: '',
   status: '',
   positionType: '',
-  priceRange: '',
-  createTime: null,
-  keyword: ''
+  minPrice: undefined,
+  maxPrice: undefined,
+  evaluationName: ''
 });
 
 /** 销量点击跳转 */
@@ -197,7 +245,12 @@ const handleSalesClick = (row: any) => {
 const getList = async () => {
   loading.value = true;
   try {
-    const res = await listEvaluation(queryParams.value);
+    const params: any = { ...queryParams.value };
+    if (createTimeRange.value && createTimeRange.value.length === 2) {
+      params['params[beginTime]'] = createTimeRange.value[0];
+      params['params[endTime]'] = createTimeRange.value[1];
+    }
+    const res = await listEvaluation(params);
     assessmentList.value = res.rows || [];
     total.value = res.total || 0;
   } catch (error) {
@@ -220,13 +273,15 @@ const handleReset = () => {
   queryParams.value = {
     pageNum: 1,
     pageSize: 10,
-    level: '',
+    grade: '',
     status: '',
     positionType: '',
-    priceRange: '',
-    createTime: null,
-    keyword: ''
+    minPrice: undefined,
+    maxPrice: undefined,
+    evaluationName: ''
   };
+  createTimeRange.value = [];
+  proxy?.$refs['queryForm']?.resetFields();
   getList();
 };
 
@@ -468,6 +523,16 @@ const getBusinessStatus = (row: any) => {
     display: flex;
     flex-wrap: wrap;
     gap: 16px;
+
+    .price-range {
+      display: flex;
+      align-items: center;
+      gap: 4px;
+
+      .range-separator {
+        color: #dcdfe6;
+      }
+    }
     
     .search-buttons {
       display: flex;

+ 5 - 3
src/views/system/audit/detail.vue

@@ -32,7 +32,7 @@
             <div class="flex mt-6" v-if="detail.authLetter">
               <span class="w-32 text-[#4e5969]">委托招聘证明:</span>
               <div class="flex gap-4">
-                <el-image style="width: 120px; height: 80px" class="border rounded border-gray-200 bg-gray-50" :src="`/api/resource/oss/file/${detail.authLetter}`" :preview-src-list="[`/api/resource/oss/file/${detail.authLetter}`]" fit="contain"></el-image>
+                <el-image style="width: 120px; height: 80px" class="border rounded border-gray-200 bg-gray-50" :src="`${baseApi}/resource/oss/file/${detail.authLetter}`" :preview-src-list="[`${baseApi}/resource/oss/file/${detail.authLetter}`]" fit="contain"></el-image>
               </div>
             </div>
           </div>
@@ -45,7 +45,7 @@
           </h3>
           <div class="info-list space-y-4 text-sm">
             <div class="mb-6">
-               <el-avatar :size="64" :src="detail.avatar ? `/api/resource/oss/file/${detail.avatar}` : undefined" class="bg-gray-200 text-lg">
+               <el-avatar :size="64" :src="detail.avatar ? `${baseApi}/resource/oss/file/${detail.avatar}` : undefined" class="bg-gray-200 text-lg">
                  {{ detail.surname || '头像' }}
                </el-avatar>
             </div>
@@ -106,7 +106,7 @@
                 v-model="auditForm.remark" 
                 type="textarea" 
                 :rows="5" 
-                placeholder="请输入审核通过或驳回的具体原因说明..."
+                placeholder="说明说明说明说明说明说明..."
               />
             </el-form-item>
           </el-form>
@@ -131,6 +131,8 @@ import { ref, onMounted } from 'vue';
 import { useRouter, useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import { getAudit, auditPass, auditReject } from '@/api/system/audit';
+
+const baseApi = import.meta.env.VITE_APP_BASE_API;
 import { AuditVO } from '@/api/system/audit/types';
 
 const router = useRouter();

+ 71 - 22
src/views/system/audit/index.vue

@@ -28,7 +28,7 @@
         <!-- 搜索与头部区域 -->
         <div class="header-section">
           <div class="flex items-center mb-4">
-             <el-input v-model="queryParams.applyNo" placeholder="请输入申请编号" style="width: 300px" clearable @keyup.enter="handleQuery">
+             <el-input v-model="queryParams.applyNo" placeholder="请输入编号或关键词" style="width: 300px" clearable @keyup.enter="handleQuery">
                 <template #append>
                    <el-button type="primary" icon="Search" class="bg-blue-500 text-white search-btn" @click="handleQuery"></el-button>
                 </template>
@@ -121,21 +121,9 @@
               <template #default="scope">
                 <el-link type="primary" :underline="false" style="font-size: 13px" @click="handleView(scope.row)">查看</el-link>
                 
-                <el-dropdown trigger="click" @command="(cmd: string) => handleAuditCommand(cmd, scope.row)" placement="bottom-end">
-                  <el-button type="success" size="small" class="ml-2 audit-btn" v-if="scope.row.auditResult === 0">
-                    审核
-                  </el-button>
-                  <template #dropdown>
-                    <el-dropdown-menu class="dark-dropdown">
-                      <el-dropdown-item command="pass" class="text-white">
-                        <el-icon><i-ep-check /></el-icon> 审核通过
-                      </el-dropdown-item>
-                      <el-dropdown-item command="reject" class="text-white">
-                        <el-icon><i-ep-close /></el-icon> 审核驳回
-                      </el-dropdown-item>
-                    </el-dropdown-menu>
-                  </template>
-                </el-dropdown>
+                <el-button type="success" size="small" class="ml-2 audit-btn" v-if="scope.row.auditResult === 0" @click="handleOpenAudit(scope.row)">
+                  审核
+                </el-button>
               </template>
             </el-table-column>
           </el-table>
@@ -156,10 +144,22 @@
     <!-- 审核说明弹窗 -->
     <el-dialog v-model="auditDialogVisible" width="500px" :show-close="true" custom-class="audit-dialog">
       <template #header>
-        <span class="font-bold text-gray-800 text-base">审核说明</span>
+        <div class="dialog-header-line">
+          <span class="font-bold text-gray-800 text-base">审核说明</span>
+        </div>
       </template>
-      <el-form :model="auditForm" label-width="60px" class="mt-4">
-        <div class="mb-5 text-gray-700 ml-4 font-medium">结果:{{ auditForm.result === 'pass' ? '通过' : '驳回' }}</div>
+      
+      <div class="tabs-container">
+        <el-tabs v-model="auditForm.result" class="audit-tabs">
+          <el-tab-pane label="通过" name="pass" />
+          <el-tab-pane label="驳回" name="reject" />
+        </el-tabs>
+      </div>
+
+      <el-form :model="auditForm" label-width="80px" class="mt-6 px-4">
+        <el-form-item label="结果:">
+          <span class="text-gray-700 font-medium">{{ auditForm.result === 'pass' ? '通过' : '驳回' }}</span>
+        </el-form-item>
         
         <el-form-item>
           <template #label>
@@ -170,15 +170,15 @@
             v-model="auditForm.remark"
             type="textarea"
             :rows="4"
-            placeholder="说明说明说明说明说明说明..."
+            placeholder="请输入审核备注说明..."
             :class="{'reject-border': auditForm.result === 'reject'}"
           />
         </el-form-item>
       </el-form>
       <template #footer>
-        <div class="flex justify-end mt-2">
+        <div class="dialog-footer-line flex justify-end">
           <el-button @click="auditDialogVisible = false">取消</el-button>
-          <el-button type="primary" @click="confirmAudit">确定</el-button>
+          <el-button type="primary" @click="confirmAudit" class="confirm-btn">确定</el-button>
         </div>
       </template>
     </el-dialog>
@@ -277,6 +277,13 @@ const auditForm = reactive({
   remark: ''
 });
 
+const handleOpenAudit = (row: AuditVO) => {
+  currentId.value = row.id;
+  auditForm.result = 'pass';
+  auditForm.remark = '';
+  auditDialogVisible.value = true;
+};
+
 const handleAuditCommand = (cmd: string, row: AuditVO) => {
   currentId.value = row.id;
   auditForm.result = cmd;
@@ -463,6 +470,48 @@ onMounted(() => {
   }
 }
 
+/* 弹窗样式优化 */
+.audit-dialog {
+  :deep(.el-dialog__header) {
+    margin-right: 0;
+    padding: 20px 24px;
+    border-bottom: 1px solid #f0f0f0;
+  }
+  :deep(.el-dialog__body) {
+    padding: 0;
+  }
+  :deep(.el-dialog__footer) {
+    padding: 20px 24px;
+    border-top: 1px solid #f0f0f0;
+  }
+}
+
+.tabs-container {
+  padding: 0 24px;
+  background-color: #fff;
+}
+
+.audit-tabs {
+  :deep(.el-tabs__header) {
+    margin: 0;
+  }
+  :deep(.el-tabs__nav-wrap::after) {
+    height: 1px;
+    background-color: #f0f0f0;
+  }
+  :deep(.el-tabs__item) {
+    font-size: 15px;
+    height: 50px;
+    line-height: 50px;
+  }
+}
+
+.confirm-btn {
+  background-color: #409eff;
+  border-color: #409eff;
+  padding: 0 25px;
+}
+
 .reject-border {
   :deep(.el-textarea__inner) {
     border-color: #165dff !important;

+ 1 - 1
src/views/system/audit/jobDetail.vue

@@ -221,7 +221,7 @@
                 v-model="auditForm.remark"
                 type="textarea"
                 :rows="4"
-                placeholder="请输入审核备注说明"
+                placeholder="说明说明说明说明说明说明..."
               />
             </el-form-item>
           </el-form>

+ 1 - 0
src/views/system/backclause/index.vue

@@ -197,6 +197,7 @@ const getCategoryList = async () => {
 
 onMounted(() => {
   getCategoryList();
+  getClauseList();
 });
 
 const queryParams = reactive({

+ 2 - 1
src/views/system/custconfig/index.vue

@@ -293,7 +293,8 @@ import { listUser } from '@/api/system/user';
 import { listTicket, getTicket, processTicket } from '@/api/chat/ticket';
 
 const { proxy } = getCurrentInstance();
-const { main_agent_module } = proxy.useDict("main_agent_module");
+const dictData = proxy.useDict("main_agent_module");
+const main_agent_module = computed(() => dictData.main_agent_module);
 
 // --- 坐席配置逻辑 ---
 const loading = ref(false);

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

@@ -91,7 +91,7 @@
             :key="index" 
             :class="['message-row', msg.sender === 'waiter' ? 'row-right' : 'row-left']"
           >
-            <el-avatar :size="38" :src="msg.sender === 'waiter' ? waiterAvatar : activeSession?.avatar" class="avatar" />
+            <el-avatar :size="38" :src="msg.sender === 'waiter' ? (msg.senderAvatar || 'https://api.dicebear.com/7.x/avataaars/svg?seed=Waiter') : (msg.senderAvatar || activeSession?.avatar)" class="avatar" />
             <div class="msg-content-box">
               <div class="time-stamp">{{ msg.time }}</div>
               
@@ -383,7 +383,11 @@
               <section class="a-section mt-20">
                 <div class="s-head">工作经历</div>
                 <el-table :data="workList" border stripe>
-                  <el-table-column prop="company" label="公司" />
+                  <el-table-column label="公司">
+                    <template #default="scope">
+                      {{ scope.row.isHidden === 1 ? '***(已屏蔽)' : scope.row.company }}
+                    </template>
+                  </el-table-column>
                   <el-table-column prop="industry" label="行业" width="120" />
                   <el-table-column label="时间" width="200">
                     <template #default="scope">
@@ -763,6 +767,7 @@ async function loadMessagesSilent() {
         }
         return {
           sender: msg.senderType === 2 ? 'waiter' : 'customer',
+          senderAvatar: msg.senderAvatar,
           type: msg.msgType || 'text',
           content: msg.content,
           time: msg.sendTime,
@@ -830,8 +835,8 @@ async function selectSession(session) {
   
   // 断开之前的连接并连接新的会话
   disconnectChat();
-  if (session.sessionNo) {
-    connectChat(session.sessionNo, (data) => {
+  if (session.id) {
+    connectChat(session.id, (data) => {
       // 收到新消息的处理逻辑
       if (data.sessionId === activeSessionId.value) {
         messageList.value.push({
@@ -884,6 +889,7 @@ async function selectSession(session) {
         }
         return {
           sender: msg.senderType === 2 ? 'waiter' : 'customer',
+          senderAvatar: msg.senderAvatar,
           type: msg.msgType || 'text',
           content: msg.content,
           time: msg.sendTime,

Fichier diff supprimé car celui-ci est trop grand
+ 532 - 236
src/views/system/custservice/index.vue


+ 197 - 75
src/views/system/order/detail.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="app-container order-detail-page">
+  <div class="app-container order-detail-page" v-loading="loading">
     <div class="detail-container">
       <!-- 顶部信息块 -->
       <div class="info-blocks">
@@ -8,8 +8,8 @@
           <div class="block-title">订单信息</div>
           <div class="info-item">
             <span class="label">订单编号:</span>
-            <span class="value">{{ orderInfo.orderSn }}</span>
-            <el-link type="primary" :underline="false" class="ml10" @click="copyText(orderInfo.orderSn)">复制</el-link>
+            <span class="value">{{ orderInfo.orderNo }}</span>
+            <el-link type="primary" :underline="false" class="ml10" @click="copyText(orderInfo.orderNo)">复制</el-link>
           </div>
           <div class="info-item">
             <span class="label">下单时间:</span>
@@ -21,16 +21,19 @@
           </div>
           <div class="info-item status-item">
             <span class="label">订单状态:</span>
-            <template v-if="orderInfo.status === '1'">
+            <template v-if="String(orderInfo.orderStatus) === '1'">
               <span class="status-tag success">已完成</span>
             </template>
-            <template v-else-if="orderInfo.status === '0'">
+            <template v-else-if="String(orderInfo.orderStatus) === '0'">
               <span class="status-tag warning">待付款</span>
-              <span class="status-desc">(剩余23分钟自动关闭订单)</span>
+              <span class="status-desc">(请尽快处理)</span>
               <el-link type="primary" :underline="false" class="close-link" @click="openCancelDialog">取消订单</el-link>
             </template>
+            <template v-else-if="String(orderInfo.orderStatus) === '3'">
+              <span class="status-tag info" style="background-color: #fbe6d5; color: #f08235;">售后中</span>
+            </template>
             <template v-else>
-              <span class="status-tag info">{{ orderInfo.statusLabel }}</span>
+              <span class="status-tag info">{{ String(orderInfo.orderStatus) === '2' ? '已关闭' : '未知' }}</span>
             </template>
           </div>
           <div class="info-item">
@@ -52,7 +55,7 @@
           <div class="info-item">
             <span class="label">订单备注:</span>
             <span class="value">{{ orderInfo.remark || '无' }}</span>
-            <el-link type="primary" :underline="false" class="ml10">编辑备注</el-link>
+            <el-link type="primary" :underline="false" class="ml10" @click="openRemarkDialog">编辑备注</el-link>
           </div>
         </div>
 
@@ -60,39 +63,42 @@
         <div class="info-block buyer-info">
           <div class="block-title">
             买家信息
-            <el-link type="primary" :underline="false" class="ml10">查看资料</el-link>
+            <el-link type="primary" :underline="false" class="ml10" @click="goToStudentDetail">查看资料</el-link>
           </div>
           <div class="info-item">
             <span class="label">微信昵称:</span>
-            <span class="value">{{ buyerInfo.wechatNickname }}</span>
+            <span class="value">{{ buyerInfo.wechatNickname || '-' }}</span>
           </div>
           <div class="info-item">
             <span class="label">用户姓名:</span>
-            <span class="value">{{ buyerInfo.userName }}</span>
+            <span class="value">{{ buyerInfo.userName || '-' }}</span>
           </div>
           <div class="info-item">
             <span class="label">用户性别:</span>
-            <span class="value">{{ buyerInfo.gender }}</span>
+            <span class="value">
+              <dict-tag v-if="buyerInfo.genderCode !== undefined" :options="sys_user_sex" :value="buyerInfo.genderCode" />
+              <span v-else>-</span>
+            </span>
           </div>
           <div class="info-item">
             <span class="label">用户年龄:</span>
-            <span class="value">{{ buyerInfo.age }}</span>
+            <span class="value">{{ buyerInfo.age || '-' }}</span>
           </div>
           <div class="info-item">
             <span class="label">手机号码:</span>
-            <span class="value">{{ buyerInfo.phone }}</span>
+            <span class="value">{{ buyerInfo.phone || '-' }}</span>
           </div>
           <div class="info-item">
             <span class="label">用户ID:</span>
-            <span class="value">{{ buyerInfo.userId }}</span>
+            <span class="value">{{ buyerInfo.userId || '-' }}</span>
           </div>
           <div class="info-item">
             <span class="label">注册时间:</span>
-            <span class="value">{{ buyerInfo.registerTime }}</span>
+            <span class="value">{{ buyerInfo.registerTime || '-' }}</span>
           </div>
           <div class="info-item">
-            <span class="label">最登录时间:</span>
-            <span class="value">{{ buyerInfo.lastLoginTime }}</span>
+            <span class="label">最登录时间:</span>
+            <span class="value">{{ buyerInfo.lastLoginTime || '-' }}</span>
           </div>
         </div>
       </div>
@@ -122,7 +128,7 @@
           <el-table-column prop="productType" label="商品类型" width="120" align="center" />
           <el-table-column label="订单状态" width="120" align="center">
             <template #default="scope">
-              <span :class="getStatusClass(scope.row.status)">{{ scope.row.statusLabel }}</span>
+              <span :class="getStatusClass(scope.row.orderStatus)">{{ scope.row.orderType === '定金' ? '定金支付' : '完成' }}</span>
             </template>
           </el-table-column>
           <el-table-column label="订单类型" width="150" align="center">
@@ -151,75 +157,145 @@
       </el-form>
       <template #footer>
         <div style="text-align: center;">
-          <el-button type="primary" @click="confirmCancel">确定</el-button>
+          <el-button type="primary" @click="confirmCancel" :loading="cancelLoading">确定</el-button>
           <el-button @click="cancelDialogVisible = false">取消</el-button>
         </div>
       </template>
     </el-dialog>
+
+    <!-- 编辑备注弹窗 -->
+    <el-dialog title="编辑订单备注" v-model="remarkDialogVisible" width="500px">
+      <el-form :model="remarkForm" label-width="120px" class="cancel-form mt20">
+        <el-form-item label="订单备注:">
+          <el-input 
+            type="textarea" 
+            v-model="remarkForm.remark" 
+            placeholder="请输入订单备注" 
+            :rows="4" 
+            style="width: 250px" 
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div style="text-align: center;">
+          <el-button type="primary" @click="confirmRemark" :loading="remarkLoading">确定</el-button>
+          <el-button @click="remarkDialogVisible = false">取消</el-button>
+        </div>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted } from 'vue';
-import { useRoute } from 'vue-router';
+import { ref, onMounted, getCurrentInstance } from 'vue';
+import { useRouter, useRoute } from 'vue-router';
 import { ElMessage } from 'element-plus';
+import { getOrder, updateOrder } from '@/api/system/order';
+import { getStudent } from '@/api/main/student';
+import { OrderVO } from '@/api/system/order/types';
 
 const route = useRoute();
+const router = useRouter();
+const loading = ref(false);
+
+const { proxy } = getCurrentInstance() as any;
+const { sys_user_sex } = proxy.useDict('sys_user_sex');
+
+const orderInfo = ref<any>({});
+const buyerInfo = ref<any>({});
+const productList = ref<any[]>([]);
+
+const getDetail = async () => {
+  const id = route.query.id as string;
+  if (!id) return;
+  loading.value = true;
+  try {
+    const res = await getOrder(id);
+    const data = res.data;
+    orderInfo.value = {
+      orderNo: data.orderNo,
+      createTime: data.createTime,
+      payTime: data.payTime,
+      orderStatus: data.orderStatus,
+      payMethod: '预见支付', // 后端如有支付方式字段可替换
+      orderType: data.orderType === 1 ? '定金' : '全款',
+      productType: data.orderType === 1 ? '岗位' : '商品',
+      source: data.source || '小程序',
+      remark: data.remark,
+      paidAmount: data.paidAmount,
+      totalAmount: data.totalAmount
+    };
+
+    buyerInfo.value = {
+      wechatNickname: data.buyerName,
+      userName: data.buyerName,
+      gender: '-',
+      genderCode: undefined,
+      age: '-',
+      phone: data.phone || '-',
+      userId: data.buyerId,
+      registerTime: '-',
+      lastLoginTime: '-'
+    };
+
+    // 如果是学员订单,进一步获取学员详细资料
+    if (data.buyerId && data.buyerType === 2) {
+      try {
+        const studentRes = await getStudent(data.buyerId);
+        if (studentRes.data) {
+          const s = studentRes.data;
+          buyerInfo.value.userName = s.name || data.buyerName;
+          buyerInfo.value.phone = s.mobile || data.phone;
+          // 处理性别编码
+          let gCode = s.gender;
+          if (gCode === 'F') gCode = '1';
+          if (gCode === 'M') gCode = '0';
+          buyerInfo.value.genderCode = gCode;
+          
+          buyerInfo.value.age = getAgeByIdCard(s.idCardNumber);
+          buyerInfo.value.registerTime = s.createTime;
+          buyerInfo.value.lastLoginTime = s.loginDate || '-';
+        }
+      } catch (e) {
+        console.error('获取学员详情失败', e);
+      }
+    }
 
-// 模拟订单数据
-const orderInfo = ref({
-  orderSn: '2020033016190804520',
-  createTime: '2019-09-09 10:40',
-  payTime: '2019-09-09 10:40', // 如果是待付款可能是空的
-  status: '1', // 1: 已完成, 0: 待付款
-  statusLabel: '已完成',
-  payMethod: '微信支付',
-  orderType: '定金',
-  productType: '测评',
-  source: '小程序',
-  remark: ''
-});
+    productList.value = [{
+       productImg: data.productImg || 'https://via.placeholder.com/60',
+       productName: data.productName || '未知商品',
+       totalPrice: data.totalAmount,
+       deposit: data.deposit || 0,
+       balance: data.balance || 0,
+       quantity: data.quantity || 1,
+       productType: data.orderType === 1 ? '岗位' : '商品',
+       orderStatus: data.orderStatus,
+       orderType: data.orderType === 1 ? '定金' : '全款',
+       postage: '0.00',
+       actualPayment: data.paidAmount
+    }];
+  } finally {
+    loading.value = false;
+  }
+};
 
-// 模拟买家数据
-const buyerInfo = ref({
-  wechatNickname: '小丸子',
-  userName: '向微微',
-  gender: '女',
-  age: 23,
-  phone: '18890900900',
-  userId: '18890900900',
-  registerTime: '2019-09-09 10:40',
-  lastLoginTime: '2019-09-09 10:40'
-});
+const getAgeByIdCard = (idCard: string) => {
+  if (!idCard || idCard.length !== 18) return '-';
+  const birthYear = parseInt(idCard.substring(6, 10));
+  const nowYear = new Date().getFullYear();
+  return nowYear - birthYear;
+};
 
-// 模拟商品数据
-const productList = ref([
-  {
-    productImg: 'https://via.placeholder.com/60',
-    productName: '单计势0-B2高级直播班',
-    totalPrice: '300.00',
-    deposit: '30.00',
-    balance: '270.00',
-    quantity: 1,
-    productType: '岗位',
-    status: '1',
-    statusLabel: '已完成',
-    orderType: '定金',
-    postage: '30.00',
-    actualPayment: '30.00'
+const goToStudentDetail = () => {
+  if (buyerInfo.value.userId) {
+    router.push('/system/student/detail/' + buyerInfo.value.userId);
+  } else {
+    ElMessage.warning('用户ID不存在');
   }
-]);
+};
 
 onMounted(() => {
-  const status = route.query.status as string;
-  if (status === '0') {
-    // 模拟待付款状态
-    orderInfo.value.status = '0';
-    orderInfo.value.statusLabel = '待付款';
-    orderInfo.value.payTime = '';
-    productList.value[0].status = '0';
-    productList.value[0].statusLabel = '待付款';
-  }
+  getDetail();
 });
 
 const copyText = (text: string) => {
@@ -235,16 +311,62 @@ const getStatusClass = (status: string) => {
 };
 
 const cancelDialogVisible = ref(false);
+const cancelLoading = ref(false);
 const cancelForm = ref({ reason: '' });
 
+const remarkDialogVisible = ref(false);
+const remarkLoading = ref(false);
+const remarkForm = ref({ remark: '' });
+
 const openCancelDialog = () => {
   cancelForm.value.reason = '';
   cancelDialogVisible.value = true;
 };
 
-const confirmCancel = () => {
-  ElMessage.success('取消订单成功');
-  cancelDialogVisible.value = false;
+const confirmCancel = async () => {
+  const id = route.query.id as string;
+  if (!id) return;
+  
+  cancelLoading.value = true;
+  try {
+    await updateOrder({
+      id: id,
+      orderStatus: 2, // 2: 已关闭/取消
+      remark: orderInfo.value.remark ? `${orderInfo.value.remark} (取消原因: ${cancelForm.value.reason})` : `取消原因: ${cancelForm.value.reason}`
+    });
+    ElMessage.success('取消订单成功');
+    cancelDialogVisible.value = false;
+    getDetail(); // 刷新详情
+  } catch (error) {
+    console.error('取消订单失败', error);
+  } finally {
+    cancelLoading.value = false;
+  }
+};
+
+const openRemarkDialog = () => {
+  remarkForm.value.remark = orderInfo.value.remark || '';
+  remarkDialogVisible.value = true;
+};
+
+const confirmRemark = async () => {
+  const id = route.query.id as string;
+  if (!id) return;
+
+  remarkLoading.value = true;
+  try {
+    await updateOrder({
+      id: id,
+      remark: remarkForm.value.remark
+    });
+    ElMessage.success('修改备注成功');
+    remarkDialogVisible.value = false;
+    getDetail(); // 刷新详情
+  } catch (error) {
+    console.error('修改备注失败', error);
+  } finally {
+    remarkLoading.value = false;
+  }
 };
 </script>
 
@@ -293,7 +415,7 @@ const confirmCancel = () => {
   line-height: 22px;
   
   .label {
-    width: 100px;
+    width: 120px;
     color: #4e5969;
   }
   

+ 190 - 220
src/views/system/order/index.vue

@@ -2,139 +2,135 @@
   <div class="app-container order-page">
     <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 :model="queryParams" ref="queryForm" :inline="true" label-width="80px" class="search-form">
         <el-form-item label="客户单号">
           <el-input v-model="queryParams.customerSn" placeholder="请输入客户单号搜索" clearable size="small" style="width: 200px" />
         </el-form-item>
         <el-form-item label="订单号">
-          <el-input v-model="queryParams.orderSn" placeholder="请输入订单号搜索" clearable size="small" style="width: 200px" />
+          <el-input v-model="queryParams.orderNo" placeholder="请输入订单号搜索" clearable size="small" style="width: 200px" />
         </el-form-item>
         <el-form-item label="用户名/ID">
-          <el-input v-model="queryParams.userName" placeholder="输入" clearable size="small" style="width: 150px" />
+          <el-input v-model="queryParams.buyerName" placeholder="输入" clearable size="small" style="width: 150px" />
         </el-form-item>
         <el-form-item label="订单状态">
-          <el-select v-model="queryParams.status" placeholder="全部" clearable size="small" style="width: 120px">
+          <el-select v-model="queryParams.orderStatus" placeholder="全部" clearable size="small" style="width: 120px">
             <el-option label="全部" value="" />
             <el-option label="待付款" value="0" />
             <el-option label="已完成" value="1" />
             <el-option label="已关闭" value="2" />
           </el-select>
         </el-form-item>
-        <el-form-item label="创建时间">
-          <el-select v-model="queryParams.timeRange" placeholder="全部" clearable size="small" style="width: 120px">
-            <el-option label="最近一周" value="1" />
-            <el-option label="最近一月" value="2" />
-          </el-select>
-        </el-form-item>
         <el-form-item>
           <el-button type="primary" size="small" @click="handleQuery">搜索</el-button>
-          <el-button size="small" @click="resetQuery">清空条件</el-button>
+          <el-button size="small" @click="resetQuery">重置</el-button>
         </el-form-item>
       </el-form>
     </el-card>
 
-    <!-- 2. 状态页签 -->
-    <div class="tabs-container mb20">
-      <div 
-        v-for="tab in statusTabs" 
-        :key="tab.value" 
-        class="tab-item" 
-        :class="{ active: activeStatus === tab.value }"
-        @click="handleTabClick(tab.value)"
-      >
-        <div class="tab-content">
-          <span class="tab-label">{{ tab.label }}</span>
-          <span class="tab-count">({{ tab.count }})</span>
-        </div>
-      </div>
-    </div>
-
-    <!-- 3. 订单列表头部 -->
-    <div class="order-list-header mb10">
-      <div class="col-product">商品信息</div>
-      <div class="col-user">用户信息</div>
-      <div class="col-source">订单来源</div>
-      <div class="col-p-type">商品类型</div>
-      <div class="col-o-type">订单类型</div>
-      <div class="col-receivable">应收</div>
-      <div class="col-remark">订单备注</div>
-      <div class="col-status">订单状态</div>
-      <div class="col-action">操作</div>
-    </div>
-
-    <!-- 4. 订单列表主体 -->
-    <div v-loading="loading" class="order-list">
-      <div v-for="order in orderList" :key="order.orderSn" class="order-item mb20 modern-card">
-        <!-- 订单页眉 -->
-        <div class="order-item-header">
-          <div class="header-left">
-            <span class="label">订单编号:</span>
-            <span class="value">{{ order.orderSn }}</span>
-            <el-icon class="copy-icon" @click="copyText(order.orderSn)"><i-ep-document-copy /></el-icon>
-            <span class="label ml20">下单时间:</span>
-            <span class="value">{{ order.createTime }}</span>
-            <span class="value ml20">{{ order.payMethod }}</span>
-          </div>
-          <div class="header-right">
-            <span class="label">实付款(元):</span>
-            <span class="total-price">{{ order.actualPrice }}元</span>
+    <!-- 2. 列表内容 -->
+    <el-card class="box-card border-none modern-card" shadow="never">
+      <!-- 状态页签 -->
+      <div class="tabs-container mb20">
+        <div 
+          v-for="tab in statusTabs" 
+          :key="tab.value" 
+          class="tab-item" 
+          :class="{ active: activeStatus === tab.value }"
+          @click="handleTabClick(tab.value)"
+        >
+          <div class="tab-content">
+            <span class="tab-label">{{ tab.label }}</span>
+            <span class="tab-count">({{ tab.count }})</span>
           </div>
         </div>
-        
-        <!-- 订单内容 -->
-        <div class="order-item-content">
-          <div class="col-product product-info">
-            <img :src="order.productImg" class="p-img" />
-            <div class="p-details">
-              <div class="p-name">{{ order.productName }}</div>
-              <div class="p-price-grid">
-                <span>总价:{{ order.totalPrice }}</span>
-                <span>定金:{{ order.deposit }}</span>
-                <span>尾款:{{ order.balance }}</span>
-                <span>数量:{{ order.quantity }}</span>
-              </div>
+      </div>
+
+      <!-- 订单列表头部 -->
+      <div class="order-list-header mb10">
+        <div class="col-product">商品信息</div>
+        <div class="col-user">用户信息</div>
+        <div class="col-source">订单来源</div>
+        <div class="col-p-type">商品类型</div>
+        <div class="col-o-type">订单类型</div>
+        <div class="col-receivable">应收</div>
+        <div class="col-remark">订单备注</div>
+        <div class="col-status">订单状态</div>
+        <div class="col-action">操作</div>
+      </div>
+
+      <!-- 订单列表主体 -->
+      <div v-loading="loading" class="order-list">
+        <div v-for="order in orderList" :key="order.orderNo" class="order-item mb20">
+          <!-- 订单页眉 -->
+          <div class="order-item-header">
+            <div class="header-left">
+              <span class="label">订单编号:</span>
+              <span class="value">{{ order.orderNo }}</span>
+              <el-icon class="copy-icon" @click="copyText(order.orderNo)"><i-ep-document-copy /></el-icon>
+              <span class="label ml20">下单时间:</span>
+              <span class="value">{{ order.createTime }}</span>
             </div>
-          </div>
-          
-          <div class="col-user user-info">
-            <el-avatar :size="32" :src="order.userAvatar" />
-            <div class="user-desc">
-              <div class="u-name">{{ order.userName }}</div>
-              <div class="u-id">ID: {{ order.userId }}</div>
-              <div class="u-phone">手机号:{{ order.phone }}</div>
+            <div class="header-right">
+              <span class="label">实付款(元):</span>
+              <span class="total-price">{{ order.paidAmount }}元</span>
             </div>
           </div>
           
-          <div class="col-source">{{ order.source }}</div>
-          <div class="col-p-type">{{ order.productType }}</div>
-          <div class="col-o-type">{{ order.orderType }}</div>
-          <div class="col-receivable">{{ order.receivable }}</div>
-          <div class="col-remark">{{ order.remark || '无' }}</div>
-          
-          <div class="col-status">
-            <span :class="getStatusClass(order.status)">{{ order.statusLabel }}</span>
-          </div>
-          
-          <div class="col-action">
-            <el-link type="primary" :underline="false" @click="handleDetail(order)">订单详情</el-link>
-            <el-link type="primary" :underline="false" class="ml10" @click="handleCancel(order)">取消订单</el-link>
-            <el-link type="primary" :underline="false" class="ml10" v-if="order.status === '1'" @click="openRefundDialog(order)">退款</el-link>
-            <el-link type="primary" :underline="false" class="ml10" v-if="order.status === '3'" @click="openAuditDialog(order)">审核</el-link>
+          <!-- 订单内容 -->
+          <div class="order-item-content">
+            <div class="col-product product-info">
+              <img :src="order.productImg || 'https://via.placeholder.com/60'" class="p-img" />
+              <div class="p-details">
+                <div class="p-name">{{ order.productName || '未知商品' }}</div>
+                <div class="p-price-grid">
+                  <span>总价:{{ order.totalAmount }}</span>
+                  <span>定金:{{ order.deposit || 0 }}</span>
+                  <span>尾款:{{ order.balance || 0 }}</span>
+                  <span>数量:{{ order.quantity || 1 }}</span>
+                </div>
+              </div>
+            </div>
+            
+            <div class="col-user user-info">
+              <el-avatar :size="32" :src="order.userAvatar || 'https://via.placeholder.com/32'" />
+              <div class="user-desc">
+                <div class="u-name">{{ order.buyerName }}</div>
+                <div class="u-id">ID: {{ order.buyerId }}</div>
+                <div class="u-phone">手机号:{{ order.phone || '-' }}</div>
+              </div>
+            </div>
+            
+            <div class="col-source">{{ order.source || '小程序' }}</div>
+            <div class="col-p-type">{{ order.orderType === 1 ? '岗位' : order.orderStatus === 2 ? '培训' : '商品' }}</div>
+            <div class="col-o-type">{{ order.deposit ? '定金' : '全款' }}</div>
+            <div class="col-receivable">{{ order.totalAmount }}</div>
+            <div class="col-remark">{{ order.remark || '无' }}</div>
+            
+            <div class="col-status">
+              <span :class="getStatusClass(order.orderStatus)">{{ getStatusLabel(order.orderStatus) }}</span>
+            </div>
+            
+            <div class="col-action">
+              <el-link type="primary" :underline="false" @click="handleDetail(order)">订单详情</el-link>
+              <el-link type="primary" :underline="false" class="ml10" @click="handleCancel(order)">取消订单</el-link>
+              <el-link type="primary" :underline="false" class="ml10" v-if="String(order.orderStatus) === '1'" @click="openRefundDialog(order)">退款</el-link>
+              <el-link type="primary" :underline="false" class="ml10" v-if="String(order.orderStatus) === '3'" @click="openAuditDialog(order)">审核</el-link>
+            </div>
           </div>
         </div>
       </div>
-    </div>
-
-    <!-- 5. 分页 -->
-    <div class="pagination-container">
-       <pagination
-          v-show="total > 0"
-          :total="total"
-          v-model:page="queryParams.pageNum"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-       />
-    </div>
+
+      <!-- 分页 -->
+      <div class="pagination-container">
+        <pagination
+            v-show="total > 0"
+            :total="total"
+            v-model:page="queryParams.pageNum"
+            v-model:limit="queryParams.pageSize"
+            @pagination="getList"
+        />
+      </div>
+    </el-card>
 
     <!-- 退款确认弹窗 -->
     <el-dialog :show-close="false" v-model="refundDialogVisible" width="400px">
@@ -142,19 +138,19 @@
       <div class="refund-info-box">
         <div class="info-row">
           <div class="label">订单号</div>
-          <div class="value">{{ currentOrder.orderSn }}</div>
+          <div class="value">{{ currentOrder.orderNo }}</div>
         </div>
         <div class="info-row">
           <div class="label">订单商品</div>
-          <div class="value">{{ currentOrder.productName }}</div>
+          <div class="value">{{ currentOrder.productName || '未知商品' }}</div>
         </div>
         <div class="info-row">
           <div class="label">实付金额</div>
-          <div class="value">¥{{ currentOrder.actualPrice }}元</div>
+          <div class="value">¥{{ currentOrder.paidAmount }}元</div>
         </div>
         <div class="info-row">
           <div class="label">退款金额</div>
-          <div class="value">¥{{ currentOrder.actualPrice }}元</div>
+          <div class="value">¥{{ currentOrder.paidAmount }}元</div>
         </div>
       </div>
       <template #footer>
@@ -168,13 +164,13 @@
     <el-dialog title="信息确认" v-model="auditDialogVisible" width="500px">
       <el-form :model="auditForm" label-width="140px" class="audit-form mt20">
         <el-form-item label="订单实付金额:">
-          ¥{{ currentOrder.actualPrice }}元
+          ¥{{ currentOrder.paidAmount }}元
         </el-form-item>
         <el-form-item label="用户申请退款原因:">
-          XXXXXXXa
+          {{ currentOrder.remark || '无' }}
         </el-form-item>
         <el-form-item label="退款金额:">
-          ¥{{ currentOrder.actualPrice }}元
+          ¥{{ currentOrder.paidAmount }}元
         </el-form-item>
         <el-form-item label="是否退款:">
           <el-select v-model="auditForm.status" style="width: 200px">
@@ -193,110 +189,66 @@
         </div>
       </template>
     </el-dialog>
-
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, reactive } from 'vue';
+import { ref, reactive, onMounted } from 'vue';
 import { useRouter } from 'vue-router';
 import { ElMessage, ElMessageBox } from 'element-plus';
+import { listOrder, delOrder, updateOrder, getOrderStatistics } from '@/api/system/order';
+import { OrderVO, OrderQuery } from '@/api/system/order/types';
 
 const router = useRouter();
 
 const loading = ref(false);
-const total = ref(85);
+const total = ref(0);
 const activeStatus = ref('all');
 
-const queryParams = reactive({
+const queryParams = reactive<OrderQuery>({
   pageNum: 1,
   pageSize: 10,
   customerSn: '',
-  orderSn: '',
-  userName: '',
-  status: '',
-  timeRange: ''
+  orderNo: '',
+  buyerName: '',
+  orderStatus: '',
 });
 
 const statusTabs = ref([
-  { label: '全部订单', count: 234, value: 'all' },
-  { label: '待付款', count: 2, value: '0' },
-  { label: '已完成', count: 2, value: '1' },
-  { label: '已关闭', count: 2, value: '2' },
-  { label: '售后中', count: 2, value: '3' }
+  { label: '全部订单', count: 0, value: 'all' },
+  { label: '待付款', count: 0, value: '0' },
+  { label: '已完成', count: 0, value: '1' },
+  { label: '已关闭', count: 0, value: '2' },
+  { label: '售后中', count: 0, value: '3' }
 ]);
 
-const orderList = ref([
-  {
-    orderSn: '2020033016190804520',
-    createTime: '2020-03-30 16:19:08',
-    payMethod: '微信支付',
-    actualPrice: '30.00',
-    productImg: 'https://via.placeholder.com/60',
-    productName: 'XXX设计岗位',
-    totalPrice: '400.00',
-    deposit: '40.00',
-    balance: '360.00',
-    quantity: 1,
-    userName: '王冕',
-    userId: '23423423432',
-    phone: '18854789000',
-    userAvatar: 'https://via.placeholder.com/32',
-    source: '小程序',
-    productType: '岗位',
-    orderType: '定金',
-    receivable: '30.00',
-    remark: '',
-    status: '1',
-    statusLabel: '已完成'
-  },
-  {
-    orderSn: '3453453243434',
-    createTime: '2020-03-31 10:10:10',
-    payMethod: '支付宝',
-    actualPrice: '15.30',
-    productImg: 'https://via.placeholder.com/60',
-    productName: 'xxxxx商品',
-    totalPrice: '15.30',
-    deposit: '15.30',
-    balance: '0.00',
-    quantity: 1,
-    userName: '李四',
-    userId: '11112222',
-    phone: '13800138000',
-    userAvatar: 'https://via.placeholder.com/32',
-    source: 'APP',
-    productType: '系统',
-    orderType: '全款',
-    receivable: '15.30',
-    remark: '测试审核',
-    status: '3',
-    statusLabel: '退款审核'
-  },
-  {
-    orderSn: '20230510101010001',
-    createTime: '2023-05-10 10:10:10',
-    payMethod: '微信支付',
-    actualPrice: '200.00',
-    productImg: 'https://via.placeholder.com/60',
-    productName: '待付款测试商品',
-    totalPrice: '200.00',
-    deposit: '20.00',
-    balance: '180.00',
-    quantity: 1,
-    userName: '张三',
-    userId: '666888',
-    phone: '13912345678',
-    userAvatar: 'https://via.placeholder.com/32',
-    source: 'H5',
-    productType: '课程',
-    orderType: '定金',
-    receivable: '20.00',
-    remark: '待测试',
-    status: '0',
-    statusLabel: '待付款'
+const orderList = ref<OrderVO[]>([]);
+
+/** 查询订单统计信息 */
+const getStatistics = async () => {
+  const res = await getOrderStatistics();
+  if (res.data) {
+    statusTabs.value.forEach(tab => {
+      if (res.data[tab.value] !== undefined) {
+        tab.count = res.data[tab.value];
+      }
+    });
   }
-]);
+};
+
+/** 查询订单列表 */
+const getList = async () => {
+  loading.value = true;
+  try {
+    const res = await listOrder(queryParams);
+    orderList.value = res.rows;
+    total.value = res.total;
+    // 列表更新时同步更新统计
+    getStatistics();
+  } finally {
+    loading.value = false;
+  }
+};
 
 const handleQuery = () => {
   queryParams.pageNum = 1;
@@ -305,54 +257,65 @@ const handleQuery = () => {
 
 const resetQuery = () => {
   queryParams.customerSn = '';
-  queryParams.orderSn = '';
-  queryParams.userName = '';
-  queryParams.status = '';
-  queryParams.timeRange = '';
+  queryParams.orderNo = '';
+  queryParams.buyerName = '';
+  queryParams.orderStatus = '';
+  activeStatus.value = 'all';
   handleQuery();
 };
 
-const getList = () => {
-  // 模拟接口调用
-};
-
 const handleTabClick = (val: string) => {
   activeStatus.value = val;
-  // 根据状态筛选
+  queryParams.orderStatus = val === 'all' ? '' : val;
+  handleQuery();
 };
 
-const getStatusClass = (status: string) => {
-  if (status === '1') return 'status-success';
-  if (status === '0') return 'status-warning';
-  if (status === '3') return 'status-audit';
+const getStatusClass = (status: number | string) => {
+  const s = String(status);
+  if (s === '1') return 'status-success';
+  if (s === '0') return 'status-warning';
+  if (s === '3') return 'status-audit';
   return 'status-info';
 };
 
+const getStatusLabel = (status: number | string) => {
+  const s = String(status);
+  const map: any = {
+    '0': '待付款',
+    '1': '已完成',
+    '2': '已关闭',
+    '3': '退款审核'
+  };
+  return map[s] || '未知';
+};
+
 const copyText = (text: string) => {
+  if (!text) return;
   navigator.clipboard.writeText(text).then(() => {
     ElMessage.success('复制成功');
   });
 };
 
-const handleCancel = (order: any) => {
+const handleCancel = (order: OrderVO) => {
   ElMessageBox.confirm(
-    `确定要取消订单号为 ${order.orderSn} 的订单吗?`,
+    `确定要取消订单号为 ${order.orderNo} 的订单吗?`,
     '系统提示',
     {
       confirmButtonText: '确定',
       cancelButtonText: '取消',
       type: 'warning',
     }
-  ).then(() => {
+  ).then(async () => {
+    await updateOrder({ id: order.id, orderStatus: 2 });
     ElMessage.success('订单取消成功');
-    // 实际业务中这里需要调用API并刷新列表
+    getList();
   }).catch(() => {});
 };
 
-const handleDetail = (order: any) => {
+const handleDetail = (order: OrderVO) => {
   router.push({
     path: '/system/order/detail',
-    query: { id: order.orderSn, status: order.status }
+    query: { id: order.id }
   });
 };
 
@@ -364,29 +327,36 @@ const auditForm = reactive({
   rejectReason: ''
 });
 
-const openRefundDialog = (order: any) => {
+const openRefundDialog = (order: OrderVO) => {
   currentOrder.value = order;
   refundDialogVisible.value = true;
 };
 
-const openAuditDialog = (order: any) => {
+const openAuditDialog = (order: OrderVO) => {
   currentOrder.value = order;
   auditForm.status = '1';
   auditForm.rejectReason = '';
   auditDialogVisible.value = true;
 };
 
-const confirmRefund = () => {
-  ElMessage.success('退款确认成功');
-  refundDialogVisible.value = false;
+const confirmRefund = async () => {
+    await updateOrder({ id: currentOrder.value.id, orderStatus: 3 });
+    ElMessage.success('已提交退款申请');
+    refundDialogVisible.value = false;
+    getList();
 };
 
-const confirmAudit = () => {
+const confirmAudit = async () => {
+  const newStatus = auditForm.status === '1' ? 2 : 1; 
+  await updateOrder({ id: currentOrder.value.id, orderStatus: newStatus, remark: auditForm.rejectReason });
   ElMessage.success('审核提交成功');
   auditDialogVisible.value = false;
+  getList();
 };
 
-getList();
+onMounted(() => {
+  getList();
+});
 </script>
 
 <style scoped lang="scss">

+ 0 - 7
src/views/system/oss/index.vue

@@ -59,9 +59,6 @@
               >预览开关 : {{ previewListResource ? '禁用' : '启用' }}</el-button
             >
           </el-col>
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:ossConfig:list']" type="info" plain icon="Operation" @click="handleOssConfig">配置管理</el-button>
-          </el-col>
           <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
         </el-row>
       </template>
@@ -278,10 +275,6 @@ const handleOrderChange = (prop: string, order: string) => {
   queryParams.value.isAsc = isAscArr.join(',');
   getList();
 };
-/** 任务日志列表查询 */
-const handleOssConfig = () => {
-  router.push('/system/oss-config/index');
-};
 /** 文件按钮操作 */
 const handleFile = () => {
   reset();

+ 313 - 108
src/views/system/student/detail.vue

@@ -14,8 +14,8 @@
       </div>
       <div class="flex gap-8 relative">
         <div class="flex flex-col items-center">
-          <el-avatar :size="100" :src="form.avatar" />
-          <el-tag type="success" size="small" class="mt-2">{{ form.availability || '未知状态' }}</el-tag>
+          <el-avatar :size="100" :src="form.avatarUrl" />
+          <dict-tag :options="main_arrivail_time" :value="form.availability" class="mt-2" />
         </div>
         <div class="flex-1">
           <el-form v-if="isEditing" :model="form" label-width="100px" size="small">
@@ -30,7 +30,7 @@
               </el-form-item>
               <el-form-item label="用户类型">
                 <el-select v-model="form.userType" placeholder="请选择">
-                  <el-option v-for="item in main_user_type" :key="item.value" :label="item.label" :value="item.value" />
+                  <el-option v-for="item in main_student_type" :key="item.value" :label="item.label" :value="item.value" />
                 </el-select>
               </el-form-item>
               <el-form-item label="联系电话">
@@ -82,11 +82,15 @@
             <div class="col-span-1">
               <div class="flex items-center gap-2 mb-2">
                 <span class="text-2xl font-bold">{{ form.name }}</span>
-                <el-icon v-if="form.gender === '0'" class="text-blue-500"><Male /></el-icon>
-                <el-icon v-else-if="form.gender === '1'" class="text-pink-500"><Female /></el-icon>
+                <el-icon v-if="form.gender === '0' || form.gender === 'M'" class="text-blue-500"><Male /></el-icon>
+                <el-icon v-else-if="form.gender === '1' || form.gender === 'F'" class="text-pink-500"><Female /></el-icon>
                 <span v-if="form.userType === '1'" class="text-orange-400">💎</span>
               </div>
-              <div class="text-xs text-gray-400 mb-1">身份证号</div>
+              <div class="text-xs text-gray-400 mb-1">用户类型</div>
+              <div class="text-sm">
+                <dict-tag :options="main_student_type" :value="form.userType" />
+              </div>
+              <div class="text-xs text-gray-400 mb-1 mt-2">身份证号</div>
               <div class="text-sm">{{ form.idCardNumber || '-' }}</div>
             </div>
             <div>
@@ -161,7 +165,11 @@
             <section>
               <h3 class="font-bold text-base mb-4 border-l-4 border-blue-500 pl-2">工作经历</h3>
               <el-table :data="form.experienceList" border>
-                <el-table-column prop="company" label="公司" />
+                <el-table-column label="公司">
+                  <template #default="scope">
+                    {{ scope.row.isHidden === 1 ? '***(已屏蔽)' : scope.row.company }}
+                  </template>
+                </el-table-column>
                 <el-table-column prop="industry" label="行业" />
                 <el-table-column label="时间" width="200">
                   <template #default="scope">
@@ -178,18 +186,30 @@
               <h3 class="font-bold text-base mb-4 border-l-4 border-blue-500 pl-2">项目经历</h3>
               <el-table :data="form.projectList" border>
                 <el-table-column prop="projectName" label="项目名称" />
-                <el-table-column prop="role" label="担任角色" />
+                <el-table-column prop="role" label="角色" />
                 <el-table-column label="时间" width="200">
                   <template #default="scope">
                     {{ scope.row.startTime }} - {{ scope.row.endTime }}
                   </template>
                 </el-table-column>
-                <el-table-column prop="description" label="描述" show-overflow-tooltip />
-                <el-table-column prop="achievement" label="业绩" show-overflow-tooltip />
-                <el-table-column prop="link" label="链接">
+                <el-table-column prop="description" label="项目描述" show-overflow-tooltip />
+              </el-table>
+            </section>
+
+            <!-- 简历附件列表 -->
+            <section v-if="form.appendixList && form.appendixList.length > 0">
+              <h3 class="font-bold text-base mb-4 border-l-4 border-blue-500 pl-2">简历附件</h3>
+              <el-table :data="form.appendixList" border>
+                <el-table-column prop="fileName" label="文件名" />
+                <el-table-column label="文件大小" width="120">
                   <template #default="scope">
-                    <el-link v-if="scope.row.link" type="primary" :href="scope.row.link" target="_blank">{{ scope.row.link }}</el-link>
-                    <span v-else>-</span>
+                    {{ (scope.row.fileSize / 1024).toFixed(2) }} KB
+                  </template>
+                </el-table-column>
+                <el-table-column prop="createTime" label="上传时间" width="180" />
+                <el-table-column label="操作" width="100" fixed="right">
+                  <template #default="scope">
+                    <el-button link type="primary" icon="Download" @click="proxy.$download.oss(scope.row.ossId)">下载</el-button>
                   </template>
                 </el-table-column>
               </el-table>
@@ -199,13 +219,19 @@
 
         <!-- 测评信息 -->
         <el-tab-pane label="测评信息" name="assessment">
-          <div class="p-4">
-            <el-table :data="assessmentData" border>
-              <el-table-column prop="name" label="名称" />
-              <el-table-column prop="position" label="岗位" />
-              <el-table-column prop="result" label="结果" />
-              <el-table-column prop="time" label="测评时间" />
-              <el-table-column label="操作" width="100" align="center">
+          <div class="p-4" v-loading="assessmentLoading">
+            <el-table :data="assessmentList" border>
+              <el-table-column prop="evaluationName" label="测评名称" min-width="180" />
+              <el-table-column label="结果" width="120" align="center">
+                <template #default="scope">
+                  <el-tag :type="scope.row.statusType === 'pass' ? 'success' : (scope.row.statusType === 'fail' ? 'danger' : 'info')">
+                    {{ scope.row.statusText }}
+                  </el-tag>
+                </template>
+              </el-table-column>
+              <el-table-column prop="createTime" label="申请时间" width="180" align="center" />
+              <el-table-column prop="finishedTime" label="完成时间" width="180" align="center" />
+              <el-table-column label="操作" width="100" align="center" fixed="right">
                 <template #default="scope">
                   <el-button link type="primary" @click="handleViewAssessment(scope.row)">查看</el-button>
                 </template>
@@ -216,18 +242,32 @@
 
         <!-- 培训信息 -->
         <el-tab-pane label="培训信息" name="training">
-          <div class="p-4">
-            <el-table :data="trainingData" border>
-              <el-table-column prop="name" label="名称" />
-              <el-table-column prop="mode" label="培训方式" />
-              <el-table-column prop="time" label="培训时间" />
+          <div class="p-4" v-loading="trainingLoading">
+            <el-table :data="trainingList" border>
+              <el-table-column prop="name" label="培训名称" min-width="180" />
+              <el-table-column label="学习进度" width="180" align="center">
+                <template #default="scope">
+                  <el-progress :percentage="scope.row.progress || 0" />
+                </template>
+              </el-table-column>
+              <el-table-column prop="learnedTime" label="累计学时" width="150" align="center">
+                <template #default="scope">
+                  {{ scope.row.learnedTime || 0 }} 分钟
+                </template>
+              </el-table-column>
+              <el-table-column prop="lastLearnTime" label="最后学习时间" width="180" align="center" />
+              <el-table-column prop="finishTime" label="完成时间" width="180" align="center">
+                <template #default="scope">
+                  {{ scope.row.finishTime || '-' }}
+                </template>
+              </el-table-column>
             </el-table>
           </div>
         </el-tab-pane>
 
         <!-- 订单信息 -->
         <el-tab-pane label="订单信息" name="order">
-          <div class="p-4">
+          <div class="p-4" v-loading="orderLoading">
             <div class="flex items-center gap-2 mb-6">
               <div class="w-1 h-4 bg-blue-500"></div>
               <span class="font-bold text-gray-800">订单信息</span>
@@ -250,35 +290,49 @@
               <div class="w-48 text-center">操作</div>
             </div>
 
-            <div class="space-y-4">
-              <div v-for="order in orderList" :key="order.orderId" class="order-item border border-gray-100">
+            <div class="space-y-4" v-if="orderList && orderList.length > 0">
+              <div v-for="order in orderList" :key="order.id" class="order-item border border-gray-100">
                 <div class="order-info-bar flex justify-between items-center bg-[#f4faff] py-2 px-4 text-[11px] text-gray-500 border-b border-gray-100">
                   <div class="flex gap-8">
                     <span>订单编号:{{ order.orderNo }}</span>
                     <span>下单时间:{{ order.createTime }}</span>
-                    <span>{{ order.payMode }}</span>
+                    <span>{{ order.payTime ? '支付时间:' + order.payTime : '未支付' }}</span>
                   </div>
-                  <div class="text-[12px] font-bold text-gray-800">实付款(元):{{ order.amount }}元</div>
+                  <div class="text-[12px] font-bold text-gray-800">实付款(元):{{ order.totalAmount }}元</div>
                 </div>
                 <div class="flex items-center py-4 px-4">
                   <div class="flex-1 flex items-center gap-4">
-                    <el-image :src="order.goodsImage" class="w-14 h-14 rounded object-cover border border-gray-100" />
+                    <el-image :src="order.productImg" class="w-14 h-14 rounded object-cover border border-gray-100">
+                      <template #error>
+                        <div class="image-slot bg-gray-100 w-full h-full flex items-center justify-center">
+                          <el-icon><Picture /></el-icon>
+                        </div>
+                      </template>
+                    </el-image>
                     <div class="flex flex-col gap-1">
-                      <div class="font-bold text-blue-500 hover:text-blue-600 cursor-pointer text-[13px]">{{ order.goodsName }}</div>
-                      <div class="text-[11px] text-gray-400">单价:{{ order.price }}  数量:{{ order.count }}</div>
+                      <div class="font-bold text-blue-500 hover:text-blue-600 cursor-pointer text-[13px]">{{ order.productName || '未知商品' }}</div>
+                      <div class="text-[11px] text-gray-400">单价:{{ order.totalAmount }}  数量:{{ order.quantity || 1 }}</div>
                     </div>
                   </div>
                   <div class="w-48 flex items-center justify-center gap-2 border-l border-gray-50 h-14">
                     <el-avatar :size="28" :src="order.userAvatar" />
                     <div class="flex flex-col items-start leading-tight">
-                      <div class="text-[12px] font-medium text-blue-500 cursor-pointer">{{ order.userName }}</div>
-                      <div class="text-[11px] text-gray-400 mt-0.5">{{ order.userPhone || '18854789000' }}</div>
+                      <div class="text-[12px] font-medium text-blue-500 cursor-pointer">{{ order.buyerName }}</div>
+                      <div class="text-[11px] text-gray-400 mt-0.5">{{ order.phone || '-' }}</div>
                     </div>
                   </div>
-                  <div class="w-32 text-center text-[12px] text-gray-600 border-l border-gray-50 h-14 flex items-center justify-center">小程序</div>
-                  <div class="w-32 text-center text-[12px] text-gray-600 border-l border-gray-50 h-14 flex items-center justify-center">{{ order.orderType }}</div>
+                  <div class="w-32 text-center text-[12px] text-gray-600 border-l border-gray-50 h-14 flex items-center justify-center">{{ order.source || '小程序' }}</div>
+                  <div class="w-32 text-center text-[12px] text-gray-600 border-l border-gray-50 h-14 flex items-center justify-center">
+                    <span v-if="order.orderType === 1">岗位</span>
+                    <span v-else-if="order.orderType === 2">培训</span>
+                    <span v-else-if="order.orderType === 3">商品</span>
+                    <span v-else>其他</span>
+                  </div>
                   <div class="w-32 flex justify-center border-l border-gray-50 h-14 items-center">
-                    <span class="text-[#2dc26b] font-bold text-[12px]">已完成</span>
+                    <el-tag v-if="order.orderStatus === 0" type="warning">待付款</el-tag>
+                    <el-tag v-else-if="order.orderStatus === 1" type="success">已完成</el-tag>
+                    <el-tag v-else-if="order.orderStatus === 2" type="info">已关闭</el-tag>
+                    <el-tag v-else-if="order.orderStatus === 3" type="danger">售后中</el-tag>
                   </div>
                   <div class="w-48 flex items-center justify-center gap-4 border-l border-gray-50 h-14 text-[12px]">
                     <el-button link type="primary" class="!p-0 !text-[12px]" @click="handleOrderDetail(order)">订单详情</el-button>
@@ -287,6 +341,7 @@
                 </div>
               </div>
             </div>
+            <el-empty v-else description="暂无订单数据" />
           </div>
         </el-tab-pane>
 
@@ -341,23 +396,102 @@
         </div>
       </div>
     </el-dialog>
+
+    <!-- 测评详情弹窗 -->
+    <el-dialog
+      v-model="assessmentDialog.visible"
+      :title="'测评详情 - ' + assessmentDialog.evaluationName"
+      width="900px"
+      :close-on-click-modal="false"
+    >
+      <div v-loading="assessmentDialog.loading">
+        <!-- 无数据 -->
+        <el-empty v-if="!assessmentDialog.loading && assessmentDialog.abilityDetails.length === 0" description="暂无考试记录" />
+
+        <!-- 各能力维度详情 -->
+        <div v-for="(ability, index) in assessmentDialog.abilityDetails" :key="index" class="mb-6 last:mb-0">
+          <div class="ability-section-header">
+            <h3 class="font-bold text-base mb-1">{{ ability.abilityName }}</h3>
+            <div class="text-sm text-gray-500 space-x-4">
+              <span>关联考试:{{ ability.examName || '-' }}</span>
+              <span>总分:{{ ability.totalScore || 0 }}分</span>
+              <span>及格分:{{ ability.passMark || 0 }}分</span>
+            </div>
+          </div>
+
+          <!-- 作答记录表格 -->
+          <el-table :data="ability.records" border size="small" class="mt-3" v-if="ability.records && ability.records.length > 0">
+            <el-table-column label="作答次数" width="80" align="center">
+              <template #default="scope">
+                第{{ scope.row.times }}次
+              </template>
+            </el-table-column>
+            <el-table-column prop="examName" label="考试名称" min-width="140" show-overflow-tooltip />
+            <el-table-column prop="examStyleName" label="考试分类" width="120" align="center" />
+            <el-table-column label="考试时间范围" min-width="180">
+              <template #default="scope">
+                {{ scope.row.examStartTime }} ~ {{ scope.row.examEndTime }}
+              </template>
+            </el-table-column>
+            <el-table-column prop="examTime" label="时长" width="70" align="center">
+              <template #default="scope">
+                {{ scope.row.examTime }}分钟
+              </template>
+            </el-table-column>
+            <el-table-column label="开始时间" width="160" align="center">
+              <template #default="scope">
+                {{ scope.row.startTime || '-' }}
+              </template>
+            </el-table-column>
+            <el-table-column label="交卷时间" width="160" align="center">
+              <template #default="scope">
+                {{ scope.row.commitTime || '未交卷' }}
+              </template>
+            </el-table-column>
+            <el-table-column label="成绩" width="90" align="center">
+              <template #default="scope">
+                <span :class="{ 'text-red-500': Number(scope.row.score) < (ability.passMark || 0), 'font-bold': true }">
+                  {{ scope.row.score }}分
+                </span>
+              </template>
+            </el-table-column>
+            <el-table-column label="是否通过" width="100" align="center">
+              <template #default="scope">
+                <el-tag v-if="scope.row.isPass === 1" type="success" size="small">及格</el-tag>
+                <el-tag v-else-if="scope.row.isPass === 0" type="danger" size="small">不及格</el-tag>
+                <el-tag v-else type="info" size="small">未知</el-tag>
+              </template>
+            </el-table-column>
+          </el-table>
+          <div v-else class="mt-3 text-center text-gray-400 py-4 bg-gray-50 rounded">
+            该维度暂无作答记录
+          </div>
+        </div>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script setup name="StudentDetail" lang="ts">
-import { ref, reactive, onMounted, getCurrentInstance, ComponentInternalInstance } from 'vue'
+import { ref, reactive, onMounted, watch, getCurrentInstance, ComponentInternalInstance } from 'vue'
 import { useRouter, useRoute } from 'vue-router'
-import { ArrowLeft, Male, Female, Download } from '@element-plus/icons-vue'
-import { getStudent } from "@/api/main/student";
-import { StudentVO } from "@/api/main/student/types";
+import { ArrowLeft, Male, Female, Download, Picture } from '@element-plus/icons-vue'
+import { getStudent, updateStudent } from "@/api/main/student";
+import { StudentVO, StudentForm } from "@/api/main/student/types";
+import { listOrder } from "@/api/system/order";
+import { OrderVO } from "@/api/system/order/types";
+import { listEvaluationRecord, getEvaluationDetailResult } from "@/api/main/evaluation";
+import { EvaluationApplyRecordVO } from "@/api/main/evaluation/types";
+import { listStudentTrainingRecords } from "@/api/main/training";
+import { TrainingLearnRecordVO } from "@/api/main/training/types";
 
 const route = useRoute()
 const router = useRouter()
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
-const { sys_user_sex, main_user_type, main_arrivail_time, main_position_type, main_education, main_experience, main_internship_duration } = proxy?.useDict(
+const { sys_user_sex, main_student_type, main_arrivail_time, main_position_type, main_education, main_experience, main_internship_duration } = proxy?.useDict(
   "sys_user_sex",
-  "main_user_type",
+  "main_student_type",
   "main_arrivail_time",
   "main_position_type",
   "main_education",
@@ -372,6 +506,12 @@ const form = ref<Partial<StudentVO>>({})
 const originalForm = ref<Partial<StudentVO>>({})
 const activeTab = ref('resume')
 const orderSubTab = ref('all')
+const orderList = ref<OrderVO[]>([])
+const orderLoading = ref(false)
+const assessmentList = ref<EvaluationApplyRecordVO[]>([])
+const assessmentLoading = ref(false)
+const trainingList = ref<TrainingLearnRecordVO[]>([])
+const trainingLoading = ref(false)
 
 const getDetail = () => {
   // router/index.ts uses 'userId' for the param name
@@ -379,15 +519,90 @@ const getDetail = () => {
   if (id) {
     loading.value = true;
     getStudent(id as string).then(response => {
-      form.value = response.data;
-      originalForm.value = JSON.parse(JSON.stringify(response.data));
+      const data = response.data;
+      // 强制转换字典字段为字符串,确保 el-select 正确回显
+      const dictFields = ['gender', 'userType', 'education', 'grade', 'internshipDuration', 'availability', 'jobType'];
+      dictFields.forEach(field => {
+        if (data[field] !== null && data[field] !== undefined) {
+          data[field] = String(data[field]);
+        }
+      });
+      
+      form.value = data;
+      originalForm.value = JSON.parse(JSON.stringify(data));
       loading.value = false;
+      // 加载订单信息
+      fetchOrderList();
+      // 加载测评信息
+      fetchAssessmentList();
+      // 加载培训信息
+      fetchTrainingList();
     }).catch(() => {
       loading.value = false;
     });
   }
 }
 
+const fetchOrderList = () => {
+  const id = route.params && route.params.userId;
+  if (!id) return;
+  
+  orderLoading.value = true;
+  const query: any = {
+    buyerId: id,
+    buyerType: 2, // 学员
+    pageNum: 1,
+    pageSize: 100
+  };
+  
+  if (orderSubTab.value !== 'all') {
+    const statusMap: any = {
+      'pending': 0,
+      'completed': 1,
+      'closed': 2,
+      'refunding': 3
+    };
+    query.orderStatus = statusMap[orderSubTab.value];
+  }
+  
+  listOrder(query).then(response => {
+    orderList.value = response.rows || [];
+    orderLoading.value = false;
+  }).catch(() => {
+    orderLoading.value = false;
+  });
+}
+
+const fetchAssessmentList = () => {
+  const id = route.params && route.params.userId;
+  if (!id) return;
+  
+  assessmentLoading.value = true;
+  listEvaluationRecord(id as string).then(response => {
+    assessmentList.value = response.data || [];
+    assessmentLoading.value = false;
+  }).catch(() => {
+    assessmentLoading.value = false;
+  });
+}
+
+const fetchTrainingList = () => {
+  const id = route.params && route.params.userId;
+  if (!id) return;
+  
+  trainingLoading.value = true;
+  listStudentTrainingRecords(id as string).then(response => {
+    trainingList.value = response.data || [];
+    trainingLoading.value = false;
+  }).catch(() => {
+    trainingLoading.value = false;
+  });
+}
+
+watch(orderSubTab, () => {
+  fetchOrderList();
+});
+
 onMounted(() => {
   getDetail()
 })
@@ -398,63 +613,20 @@ const handleBack = () => {
 
 /** 导出在线简历 */
 const handleExport = () => {
-  proxy.download('main/student/export', {
-    id: form.value.id
-  }, `student_resume_${form.value.name}_${new Date().getTime()}.xlsx`)
+  const userId = route.params && route.params.userId;
+  if (!userId) return;
+  // 调用后端 GET 接口:/main/student/exportResume/{id}
+  proxy.$download.resource(`/main/student/exportResume/${userId}`);
 }
 
 /** 下载简历附件 */
 const handleDownload = () => {
-  if (form.value.resumeFile) {
-    proxy.$download.oss(form.value.resumeFile)
-  } else {
-    proxy.$modal.msgWarning("该学员未上传简历附件")
-  }
+  const userId = route.params && route.params.userId;
+  if (!userId) return;
+  // 调用后端接口,后端会自动判断单文件或多文件打包
+  proxy.$download.resource(`/main/student/downloadResume/${userId}`);
 }
 
-const assessmentData = ref([
-  {
-    id: '1',
-    name: '审计A1测评',
-    position: '审计A1',
-    result: '通过',
-    time: '2023.12.12 12:12'
-  },
-  {
-    id: '2',
-    name: '审计A2测评',
-    position: '审计A2',
-    result: '未通过',
-    time: '2023.12.13 10:00'
-  }
-])
-
-const trainingData = ref([
-  {
-    name: '审计A1培训',
-    mode: '线下',
-    time: '2023.12.12 12:12'
-  }
-])
-
-const orderList = ref([
-  {
-    orderId: 1,
-    orderNo: '2020033016180804520',
-    createTime: '2020-03-30 16:18:08',
-    payMode: '微信支付',
-    amount: '4000.00',
-    goodsImage: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
-    goodsName: '审计师A·R2高级直通',
-    price: '400.00',
-    count: 1,
-    userName: '王军',
-    userAvatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
-    orderType: '测评-定金',
-    status: '已完成'
-  }
-])
-
 const jobData = ref([
   {
     company: 'XXXX公司',
@@ -486,17 +658,33 @@ const evaluationForm = reactive({
   commentB: '在职期间表现极佳'
 })
 
-const handleViewAssessment = (row) => {
-  router.push({
-    path: '/system/assessment/report',
-    query: { id: row.id }
-  })
+const handleViewAssessment = async (row: EvaluationApplyRecordVO) => {
+  const id = route.params && route.params.userId;
+  if (!id) return;
+
+  // 打开详情弹窗并加载数据
+  assessmentDialog.visible = true;
+  assessmentDialog.loading = true;
+  assessmentDialog.evaluationName = row.evaluationName;
+  assessmentDialog.abilityDetails = [];
+
+  try {
+    const res: any = await getEvaluationDetailResult(row.evaluationId, id);
+    if (res.data) {
+      assessmentDialog.abilityDetails = res.data.abilityDetails || [];
+    }
+  } catch (error) {
+    console.error('获取测评详情失败:', error);
+    proxy?.$modal.msgError('获取测评详情失败');
+  } finally {
+    assessmentDialog.loading = false;
+  }
 }
 
-const handleOrderDetail = (order) => {
+const handleOrderDetail = (order: OrderVO) => {
   router.push({
     path: '/system/order/detail',
-    query: { orderId: order.orderId }
+    query: { id: order.id }
   })
 }
 
@@ -504,12 +692,24 @@ const handleViewEvaluation = (row) => {
   evaluationOpen.value = true
 }
 
+// 测评详情弹窗
+const assessmentDialog = reactive({
+  visible: false,
+  loading: false,
+  evaluationName: '',
+  abilityDetails: [] as any[]
+})
+
 const handleSave = () => {
   saveLoading.value = true;
-  proxy.$modal.msgSuccess('保存成功');
-  originalForm.value = JSON.parse(JSON.stringify(form.value));
-  isEditing.value = false;
-  saveLoading.value = false;
+  updateStudent(form.value as StudentForm).then(() => {
+    proxy.$modal.msgSuccess('保存成功');
+    originalForm.value = JSON.parse(JSON.stringify(form.value));
+    isEditing.value = false;
+    saveLoading.value = false;
+  }).catch(() => {
+    saveLoading.value = false;
+  });
 }
 
 const handleCancel = () => {
@@ -541,4 +741,9 @@ const handleCancel = () => {
     box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
   }
 }
+
+.ability-section-header {
+  padding-bottom: 8px;
+  border-bottom: 2px solid #409eff;
+}
 </style>

+ 82 - 17
src/views/system/student/index.vue

@@ -56,6 +56,50 @@
       </el-col>
     </el-row>
 
+    <!-- 搜索栏 -->
+    <el-card shadow="hover" class="mb-4">
+      <el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
+        <el-form-item label="用户姓名" prop="name">
+          <el-input
+            v-model="queryParams.name"
+            placeholder="请输入用户姓名"
+            clearable
+            style="width: 200px"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="用户编号" prop="studentNo">
+          <el-input
+            v-model="queryParams.studentNo"
+            placeholder="请输入用户编号"
+            clearable
+            style="width: 200px"
+            @keyup.enter="handleQuery"
+          />
+        </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_student_type"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="黑名单" prop="isBlacklist">
+          <el-select v-model="queryParams.isBlacklist" placeholder="是否在黑名单" clearable style="width: 200px">
+            <el-option label="是" value="Y" />
+            <el-option label="否" value="N" />
+          </el-select>
+        </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>
@@ -77,7 +121,7 @@
         <el-table-column label="用户信息" min-width="200">
           <template #default="scope">
             <div class="flex items-center">
-              <el-avatar :src="scope.row.avatar" :size="40" />
+              <el-avatar :src="scope.row.avatarUrl" :size="40" />
               <div class="ml-2">
                 <div class="font-bold flex items-center">
                   {{ scope.row.name }}
@@ -96,7 +140,7 @@
             <div class="text-xs text-gray-400">{{ scope.row.email }}</div>
           </template>
         </el-table-column>
-        <el-table-column label="测评数" align="center" prop="assessmentCount" />
+        <el-table-column label="测评数" align="center" prop="evaluationCount" />
         <el-table-column label="类型" align="center">
           <template #default="scope">
             <el-tag>
@@ -127,7 +171,7 @@
               <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 v-else link type="success" @click="handleRemoveBlacklist(scope.row)">移除黑名单</el-button>
               <el-button link type="success" @click="handleRecommend(scope.row)">内推</el-button>
             </div>
           </template>
@@ -182,11 +226,11 @@
 import { ref, reactive, onMounted, getCurrentInstance, ComponentInternalInstance } from 'vue'
 import { useRouter } from 'vue-router'
 import { User, Avatar, Document, CircleClose, UserFilled, Download, Promotion, Male, Female, Search, Refresh } from '@element-plus/icons-vue'
-import { listStudent } from "@/api/main/student";
+import { listStudent, updateUserType, updateStatus } from "@/api/main/student";
 import { StudentVO, StudentQuery } from "@/api/main/student/types";
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const { main_user_type, main_arrivail_time, main_position_type, main_experience, main_education, main_internship_duration } = proxy?.useDict("main_user_type", "main_arrivail_time", "main_position_type", "main_experience", "main_education", "main_internship_duration") as any;
+const { main_student_type, main_arrivail_time, main_position_type, main_experience, main_education, main_internship_duration } = proxy?.useDict("main_student_type", "main_arrivail_time", "main_position_type", "main_experience", "main_education", "main_internship_duration") as any;
 
 const router = useRouter()
 
@@ -203,7 +247,8 @@ const queryParams = reactive<StudentQuery>({
   studentNo: undefined,
   name: undefined,
   userType: undefined,
-  status: undefined
+  status: undefined,
+  isBlacklist: undefined
 })
 const dateRange = ref([])
 const multiple = ref(true)
@@ -244,7 +289,8 @@ const handleQuery = () => {
 
 const resetQuery = () => {
   dateRange.value = []
-  // reset logic
+  proxy?.$refs['queryRef']?.resetFields()
+  handleQuery()
 }
 
 const handleSelectionChange = (selection) => {
@@ -262,7 +308,12 @@ const handleUpdate = (row) => {
 }
 
 const handleStatusChange = (row) => {
-  proxy.$modal.msgSuccess('状态更新成功')
+  updateStatus(row.id, row.status).then(() => {
+    proxy.$modal.msgSuccess('状态更新成功')
+    calculateStats()
+  }).catch(() => {
+    row.status = row.status === '0' ? '1' : '0'
+  })
 }
 
 const handleBlacklist = (row) => {
@@ -271,8 +322,11 @@ const handleBlacklist = (row) => {
     cancelButtonText: '取消',
     type: 'warning'
   }).then(() => {
-    row.userType = '3'
-    proxy.$modal.msgSuccess('已加入黑名单')
+    updateUserType(row.id, '3').then(() => {
+      row.userType = '3'
+      proxy.$modal.msgSuccess('已加入黑名单')
+      calculateStats()
+    })
   }).catch(() => {})
 }
 
@@ -282,8 +336,11 @@ const handleRemoveBlacklist = (row) => {
     cancelButtonText: '取消',
     type: 'warning'
   }).then(() => {
-    row.userType = '2'
-    proxy.$modal.msgSuccess('已移除黑名单')
+    updateUserType(row.id, '2').then(() => {
+      row.userType = '2'
+      proxy.$modal.msgSuccess('已移除黑名单')
+      calculateStats()
+    })
   }).catch(() => {})
 }
 
@@ -307,11 +364,19 @@ const handleBatchBlacklist = () => {
     confirmButtonText: '确定',
     cancelButtonText: '取消',
     type: 'warning'
-  }).then(() => {
-    selectedUsers.value.forEach(item => {
-      item.userType = '3'
-    })
-    proxy.$modal.msgSuccess('已加入黑名单')
+  }).then(async () => {
+    loading.value = true
+    try {
+      for (const item of selectedUsers.value) {
+        await updateUserType(item.id, '3')
+      }
+      proxy.$modal.msgSuccess('批量加入黑名单成功')
+      getList()
+    } catch (error) {
+      console.error(error)
+    } finally {
+      loading.value = false
+    }
   }).catch(() => {})
 }
 

+ 20 - 8
src/views/system/sysconfig/index.vue

@@ -5,7 +5,11 @@
       <el-tabs v-model="activeMainTab" class="main-tabs">
         <el-tab-pane label="网站设置" name="website" />
         <el-tab-pane label="平台配置" name="platform" />
-        <el-tab-pane label="文件存储配置" name="storage" />
+        <el-tab-pane label="文件存储配置" name="storage">
+          <div class="storage-container animated fadeIn">
+            <storage-config v-if="activeMainTab === 'storage'" />
+          </div>
+        </el-tab-pane>
         <el-tab-pane label="短信配置" name="sms" />
         <el-tab-pane label="支付配置" name="payment">
           <div class="agreement-container animated fadeIn">
@@ -50,8 +54,8 @@
       </el-tabs>
     </el-card>
 
-    <!-- 其他Tab的占位信息 (非协议配置和支付配置时显示) -->
-    <el-empty v-if="activeMainTab !== 'agreement' && activeMainTab !== 'payment'" description="该模块配置开发中..." />
+    <!-- 其他Tab的占位信息 (非协议配置、支付配置和文件存储配置时显示) -->
+    <el-empty v-if="activeMainTab !== 'agreement' && activeMainTab !== 'payment' && activeMainTab !== 'storage'" description="该模块配置开发中..." />
   </div>
 </template>
 
@@ -59,6 +63,7 @@
 import { ref, reactive, watch, onMounted } from 'vue';
 import Editor from '@/components/Editor/index.vue';
 import WxpayConfig from './wxpay.vue';
+import StorageConfig from './storage.vue';
 import { ElMessage } from 'element-plus';
 import request from '@/utils/request';
 
@@ -78,19 +83,26 @@ const currentAgreementId = ref(null);
 const activeSubTab = ref('user');
 const agreementSubTabs = [
   { label: '用户协议', name: 'user' },
-  { label: '隐私政策', name: 'privacy' }
+  { label: '隐私政策', name: 'privacy' },
+  { label: 'Offer政策', name: 'offer' }
 ];
 
 const activeSubTabsMap = {
   user: '用户协议',
-  privacy: '隐私政策'
+  privacy: '隐私政策',
+  offer: 'Offer政策'
+};
+
+const agreementTypeMap = {
+  user: 'service',
+  privacy: 'privacy',
+  offer: 'offer'
 };
 
 // 获取协议内容
 const fetchAgreement = async () => {
   try {
-    // 根据当前选项卡将 'user' 映射成后端数据库存的 'service'
-    const typeKey = activeSubTab.value === 'user' ? 'service' : 'privacy';
+    const typeKey = agreementTypeMap[activeSubTab.value];
     const res = await request({
       url: `/miniapp/auth/agreement?type=${typeKey}`,
       method: 'get'
@@ -129,7 +141,7 @@ const handleSave = async () => {
   }
   
   try {
-    const typeKey = activeSubTab.value === 'user' ? 'service' : 'privacy';
+    const typeKey = agreementTypeMap[activeSubTab.value];
     const payload = {
       id: currentAgreementId.value,
       type: typeKey,

+ 345 - 0
src/views/system/sysconfig/storage.vue

@@ -0,0 +1,345 @@
+<template>
+  <div class="storage-config-container">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+          <el-form-item label="配置key" prop="configKey">
+            <el-input v-model="queryParams.configKey" placeholder="配置key" clearable @keyup.enter="handleQuery" />
+          </el-form-item>
+          <el-form-item label="桶名称" prop="bucketName">
+            <el-input v-model="queryParams.bucketName" placeholder="请输入桶名称" clearable @keyup.enter="handleQuery" />
+          </el-form-item>
+          <el-form-item label="是否默认" prop="status">
+            <el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
+              <el-option key="0" label="是" value="0" />
+              <el-option key="1" label="否" value="1" />
+            </el-select>
+          </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>
+      </div>
+    </transition>
+
+    <div class="mb8">
+      <el-row :gutter="10">
+        <el-col :span="1.5">
+          <el-button v-hasPermi="['system:ossConfig:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
+        </el-col>
+        <el-col :span="1.5">
+          <el-button v-hasPermi="['system:ossConfig:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()">修改</el-button>
+        </el-col>
+        <el-col :span="1.5">
+          <el-button v-hasPermi="['system:ossConfig:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
+            删除
+          </el-button>
+        </el-col>
+        <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
+      </el-row>
+    </div>
+
+    <el-table v-loading="loading" border :data="ossConfigList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column v-if="columns[0].visible" label="主建" align="center" prop="ossConfigId" />
+      <el-table-column v-if="columns[1].visible" label="配置key" align="center" prop="configKey" />
+      <el-table-column v-if="columns[2].visible" label="访问站点" align="center" prop="endpoint" width="200" />
+      <el-table-column v-if="columns[3].visible" label="自定义域名" align="center" prop="domain" width="200" />
+      <el-table-column v-if="columns[4].visible" label="桶名称" align="center" prop="bucketName" />
+      <el-table-column v-if="columns[5].visible" label="前缀" align="center" prop="prefix" />
+      <el-table-column v-if="columns[6].visible" label="域" align="center" prop="region" />
+      <el-table-column v-if="columns[7].visible" label="桶权限类型" align="center" prop="accessPolicy">
+        <template #default="scope">
+          <el-tag v-if="scope.row.accessPolicy === '0'" type="warning">private</el-tag>
+          <el-tag v-if="scope.row.accessPolicy === '1'" type="success">public</el-tag>
+          <el-tag v-if="scope.row.accessPolicy === '2'" type="info">custom</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column v-if="columns[8].visible" label="是否默认" align="center" prop="status">
+        <template #default="scope">
+          <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" fixed="right" align="center" width="150" class-name="small-padding">
+        <template #default="scope">
+          <el-tooltip content="修改" placement="top">
+            <el-button v-hasPermi="['system:ossConfig:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+          </el-tooltip>
+          <el-tooltip content="删除" placement="top">
+            <el-button v-hasPermi="['system:ossConfig:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+          </el-tooltip>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+
+    <!-- 添加或修改对象存储配置对话框 -->
+    <el-dialog v-model="dialog.visible" :title="dialog.title" width="800px" append-to-body>
+      <el-form ref="ossConfigFormRef" :model="form" :rules="rules" label-width="120px">
+        <el-form-item label="配置key" prop="configKey">
+          <el-input v-model="form.configKey" placeholder="请输入配置key" />
+        </el-form-item>
+        <el-form-item label="访问站点" prop="endpoint">
+          <el-input v-model="form.endpoint" placeholder="请输入访问站点">
+            <template #prefix>
+              <span style="color: #999">{{ protocol }}</span>
+            </template>
+          </el-input>
+        </el-form-item>
+        <el-form-item label="自定义域名" prop="domain">
+          <el-input v-model="form.domain" placeholder="请输入自定义域名">
+            <template #prefix>
+              <span style="color: #999">{{ protocol }}</span>
+            </template>
+          </el-input>
+        </el-form-item>
+        <el-form-item label="accessKey" prop="accessKey">
+          <el-input v-model="form.accessKey" placeholder="请输入accessKey" />
+        </el-form-item>
+        <el-form-item label="secretKey" prop="secretKey">
+          <el-input v-model="form.secretKey" placeholder="请输入秘钥" show-password />
+        </el-form-item>
+        <el-form-item label="桶名称" prop="bucketName">
+          <el-input v-model="form.bucketName" placeholder="请输入桶名称" />
+        </el-form-item>
+        <el-form-item label="前缀" prop="prefix">
+          <el-input v-model="form.prefix" placeholder="请输入前缀" />
+        </el-form-item>
+        <el-form-item label="是否HTTPS">
+          <el-radio-group v-model="form.isHttps">
+            <el-radio v-for="dict in sys_yes_no" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="桶权限类型">
+          <el-radio-group v-model="form.accessPolicy">
+            <el-radio value="0">private</el-radio>
+            <el-radio value="1">public</el-radio>
+            <el-radio value="2">custom</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="域" prop="region">
+          <el-input v-model="form.region" placeholder="请输入域" />
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="StorageConfig" lang="ts">
+import { listOssConfig, getOssConfig, delOssConfig, addOssConfig, updateOssConfig, changeOssConfigStatus } from '@/api/system/ossConfig';
+import { OssConfigForm, OssConfigQuery, OssConfigVO } from '@/api/system/ossConfig/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { sys_yes_no } = toRefs<any>(proxy?.useDict('sys_yes_no'));
+
+const ossConfigList = ref<OssConfigVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<number | string>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+const ossConfigFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+
+// 列显隐信息
+const columns = ref<FieldOption[]>([
+  { key: 0, label: `主建`, visible: false },
+  { key: 1, label: `配置key`, visible: true },
+  { key: 2, label: `访问站点`, visible: true },
+  { key: 3, label: `自定义域名`, visible: true },
+  { key: 4, label: `桶名称`, visible: true },
+  { key: 5, label: `前缀`, visible: true },
+  { key: 6, label: `域`, visible: true },
+  { key: 7, label: `桶权限类型`, visible: true },
+  { key: 8, label: `状态`, visible: true }
+]);
+
+const initFormData: OssConfigForm = {
+  ossConfigId: undefined,
+  configKey: '',
+  accessKey: '',
+  secretKey: '',
+  bucketName: '',
+  prefix: '',
+  endpoint: '',
+  domain: '',
+  isHttps: 'N',
+  accessPolicy: '1',
+  region: '',
+  status: '1',
+  remark: ''
+};
+const data = reactive<PageData<OssConfigForm, OssConfigQuery>>({
+  form: { ...initFormData },
+  // 查询参数
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    configKey: '',
+    bucketName: '',
+    status: ''
+  },
+  rules: {
+    configKey: [{ required: true, message: 'configKey不能为空', trigger: 'blur' }],
+    accessKey: [
+      { required: true, message: 'accessKey不能为空', trigger: 'blur' },
+      {
+        min: 2,
+        max: 200,
+        message: 'accessKey长度必须介于 2 和 100 之间',
+        trigger: 'blur'
+      }
+    ],
+    secretKey: [
+      { required: true, message: 'secretKey不能为空', trigger: 'blur' },
+      {
+        min: 2,
+        max: 100,
+        message: 'secretKey长度必须介于 2 和 100 之间',
+        trigger: 'blur'
+      }
+    ],
+    bucketName: [
+      { required: true, message: 'bucketName不能为空', trigger: 'blur' },
+      {
+        min: 2,
+        max: 100,
+        message: 'bucketName长度必须介于 2 和 100 之间',
+        trigger: 'blur'
+      }
+    ],
+    endpoint: [
+      { required: true, message: 'endpoint不能为空', trigger: 'blur' },
+      {
+        min: 2,
+        max: 100,
+        message: 'endpoint名称长度必须介于 2 和 100 之间',
+        trigger: 'blur'
+      }
+    ],
+    accessPolicy: [{ required: true, message: 'accessPolicy不能为空', trigger: 'blur' }]
+  }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+const protocol = computed(() => (form.value.isHttps === 'Y' ? 'https://' : 'http://'));
+
+/** 查询对象存储配置列表 */
+const getList = async () => {
+  loading.value = true;
+  const res = await listOssConfig(queryParams.value);
+  ossConfigList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
+/** 取消按钮 */
+const cancel = () => {
+  dialog.visible = false;
+  reset();
+};
+/** 表单重置 */
+const reset = () => {
+  form.value = { ...initFormData };
+  ossConfigFormRef.value?.resetFields();
+};
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+};
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+/** 选择条数  */
+const handleSelectionChange = (selection: OssConfigVO[]) => {
+  ids.value = selection.map((item) => item.ossConfigId);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+};
+/** 新增按钮操作 */
+const handleAdd = () => {
+  reset();
+  dialog.visible = true;
+  dialog.title = '添加对象存储配置';
+};
+/** 修改按钮操作 */
+const handleUpdate = async (row?: OssConfigVO) => {
+  reset();
+  const ossConfigId = row?.ossConfigId || ids.value[0];
+  const res = await getOssConfig(ossConfigId);
+  Object.assign(form.value, res.data);
+  dialog.visible = true;
+  dialog.title = '修改对象存储配置';
+};
+/** 提交按钮 */
+const submitForm = () => {
+  ossConfigFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      if (form.value.ossConfigId) {
+        await updateOssConfig(form.value).finally(() => (buttonLoading.value = false));
+      } else {
+        await addOssConfig(form.value).finally(() => (buttonLoading.value = false));
+      }
+      proxy?.$modal.msgSuccess('操作成功');
+      dialog.visible = false;
+      await getList();
+    }
+  });
+};
+/** 状态修改  */
+const handleStatusChange = async (row: OssConfigVO) => {
+  const text = row.status === '0' ? '启用' : '停用';
+  try {
+    await proxy?.$modal.confirm('确认要"' + text + '""' + row.configKey + '"配置吗?');
+    await changeOssConfigStatus(row.ossConfigId, row.status, row.configKey);
+    await getList();
+    proxy?.$modal.msgSuccess(text + '成功');
+  } catch {
+    row.status = row.status === '0' ? '1' : '0';
+  }
+};
+/** 删除按钮操作 */
+const handleDelete = async (row?: OssConfigVO) => {
+  const ossConfigIds = row?.ossConfigId || ids.value;
+  await proxy?.$modal.confirm('是否确认删除OSS配置编号为"' + ossConfigIds + '"的数据项?');
+  loading.value = true;
+  await delOssConfig(ossConfigIds).finally(() => (loading.value = false));
+  await getList();
+  proxy?.$modal.msgSuccess('删除成功');
+};
+
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped lang="scss">
+.storage-config-container {
+  .mb8 {
+    margin-bottom: 8px;
+  }
+}
+</style>

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

@@ -9,6 +9,10 @@
       </template>
 
       <el-form :model="form" label-width="140px" v-loading="loading">
+        <el-form-item label="小程序AppID">
+          <el-input v-model="form.appId" placeholder="请输入微信小程序AppID" style="width: 400px" />
+        </el-form-item>
+
         <el-form-item label="商户号ID">
           <el-input v-model="form.mchId" placeholder="请输入微信支付商户号(MCHID)" style="width: 400px" />
         </el-form-item>
@@ -18,8 +22,13 @@
                     type="password" show-password style="width: 400px" />
         </el-form-item>
 
+        <el-form-item label="证书序列号">
+          <el-input v-model="form.serialNo" placeholder="请输入商户证书序列号" style="width: 400px" />
+        </el-form-item>
+
         <el-form-item label="支付证书">
           <div>
+            <el-input v-model="form.certPath" placeholder="证书路径,上传后自动回填或手动指定" style="width: 400px; margin-bottom: 10px;" />
             <el-upload
               action="#"
               :auto-upload="false"
@@ -36,6 +45,7 @@
         
         <el-form-item label="商户私钥">
           <div>
+            <el-input v-model="form.privateKeyPath" placeholder="私钥路径,上传后自动回填或手动指定" style="width: 400px; margin-bottom: 10px;" />
             <el-upload
               action="#"
               :auto-upload="false"
@@ -95,8 +105,12 @@ const certFile = ref<File | null>(null)
 const publicKeyFile = ref<File | null>(null)
 
 const form = ref({
+  appId: '',
   mchId: '',
   apiV3Key: '',
+  serialNo: '',
+  certPath: '',
+  privateKeyPath: '',
   notifyUrl: '',
   privateKeyUploaded: false,
   certUploaded: false,
@@ -124,8 +138,12 @@ const getConfig = async () => {
   try {
     const res = await request.get('/miniapp/paymentConfig/list')
     if (res.code === 200 && res.data) {
+      form.value.appId = res.data.appId || ''
       form.value.mchId = res.data.mchId || ''
       form.value.apiV3Key = res.data.apiV3Key || ''
+      form.value.serialNo = res.data.serialNo || ''
+      form.value.certPath = res.data.certPath || ''
+      form.value.privateKeyPath = res.data.privateKeyPath || ''
       form.value.notifyUrl = res.data.notifyUrl || ''
       form.value.privateKeyUploaded = res.data.privateKeyUploaded || false
       form.value.certUploaded = res.data.certUploaded || false
@@ -155,6 +173,7 @@ const handleSave = async () => {
         return
       }
       form.value.privateKeyUploaded = true
+      // 如果上传成功,后端通常会返回路径,这里我们可以刷新一下或者让后端直接在保存接口处理
       privateKeyFile.value = null
     }
 
@@ -191,8 +210,12 @@ const handleSave = async () => {
     }
     
     const res = await request.put('/miniapp/paymentConfig/wxpay', {
+      appId: form.value.appId,
       mchId: form.value.mchId,
       apiV3Key: form.value.apiV3Key,
+      serialNo: form.value.serialNo,
+      certPath: form.value.certPath,
+      privateKeyPath: form.value.privateKeyPath,
       notifyUrl: form.value.notifyUrl,
       publicKeyId: form.value.publicKeyId
     })

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff