Jelajahi Sumber

feat(risk): 监督抽检/平台客诉前端页面(3.1.4)

- types.ts 追加: RiskSupervisionInspectionBo/Vo + RiskSupervisionCaseBo/Vo(含nodeRecords) + RiskSupervisionComplaintBo/Vo(含nodeRecords) + SupervisionCaseNodeVo + SupervisionComplaintNodeVo + SupervisionStatsVo + SupervisionMonthPassRate
- API: supervisionInspection/Case/Complaint (含 auditInspection/processCase/processComplaint) + supervision/stats
- views/risk/supervision/index.vue: 4看板(今日待审核/不合格待处理/待处理客诉/本月合格率) + 3区块(门店抽检登记/不合格案件处理/平台客诉处理) + 5弹窗(新增抽检/抽检详情/案件详情5节点/案件发起/客诉详情7节点/新增客诉)
yangjingjing 1 Minggu lalu
induk
melakukan
75356f9214

+ 10 - 0
src/api/risk/supervision.ts

@@ -0,0 +1,10 @@
+import request from '@/utils/request';
+import type { SupervisionStatsVo } from './types';
+
+/** 监督抽检 - 顶部 4 张数据看板聚合接口 */
+export function getSupervisionStats() {
+  return request({
+    url: '/risk/supervision/case/stats',
+    method: 'get'
+  });
+}

+ 65 - 0
src/api/risk/supervisionCase.ts

@@ -0,0 +1,65 @@
+import request from '@/utils/request';
+import type {
+  RiskSupervisionCaseBo,
+  RiskSupervisionCaseVo
+} from './types';
+
+/** 监督抽检 - 不合格案件 API */
+export function listCase(query: Partial<RiskSupervisionCaseBo> & { pageNum?: number; pageSize?: number }) {
+  return request({
+    url: '/risk/supervision/case/list',
+    method: 'get',
+    params: query
+  });
+}
+
+export function getCase(id: string | number) {
+  return request({
+    url: `/risk/supervision/case/${id}`,
+    method: 'get'
+  });
+}
+
+/** 新增案件(从不合格抽检单"案件处理"按钮触发) */
+export function addCase(data: RiskSupervisionCaseBo) {
+  return request({
+    url: '/risk/supervision/case',
+    method: 'post',
+    data
+  });
+}
+
+export function updateCase(data: RiskSupervisionCaseBo) {
+  return request({
+    url: '/risk/supervision/case',
+    method: 'put',
+    data
+  });
+}
+
+export function delCase(ids: Array<string | number>) {
+  return request({
+    url: `/risk/supervision/case/${ids.join(',')}`,
+    method: 'delete'
+  });
+}
+
+export function exportCase(query: Partial<RiskSupervisionCaseBo>) {
+  return request({
+    url: '/risk/supervision/case/export',
+    method: 'post',
+    params: query,
+    responseType: 'blob'
+  });
+}
+
+/** 处理案件节点(写节点表 + current_node + 1) */
+export function processCase(data: RiskSupervisionCaseBo) {
+  return request({
+    url: '/risk/supervision/case/process',
+    method: 'put',
+    data
+  });
+}
+
+export type { RiskSupervisionCaseBo, RiskSupervisionCaseVo };

+ 65 - 0
src/api/risk/supervisionComplaint.ts

@@ -0,0 +1,65 @@
+import request from '@/utils/request';
+import type {
+  RiskSupervisionComplaintBo,
+  RiskSupervisionComplaintVo
+} from './types';
+
+/** 监督抽检 - 平台客诉 API */
+export function listComplaint(query: Partial<RiskSupervisionComplaintBo> & { pageNum?: number; pageSize?: number }) {
+  return request({
+    url: '/risk/supervision/complaint/list',
+    method: 'get',
+    params: query
+  });
+}
+
+export function getComplaint(id: string | number) {
+  return request({
+    url: `/risk/supervision/complaint/${id}`,
+    method: 'get'
+  });
+}
+
+/** 新增客诉(从"新增客诉"按钮触发) */
+export function addComplaint(data: RiskSupervisionComplaintBo) {
+  return request({
+    url: '/risk/supervision/complaint',
+    method: 'post',
+    data
+  });
+}
+
+export function updateComplaint(data: RiskSupervisionComplaintBo) {
+  return request({
+    url: '/risk/supervision/complaint',
+    method: 'put',
+    data
+  });
+}
+
+export function delComplaint(ids: Array<string | number>) {
+  return request({
+    url: `/risk/supervision/complaint/${ids.join(',')}`,
+    method: 'delete'
+  });
+}
+
+export function exportComplaint(query: Partial<RiskSupervisionComplaintBo>) {
+  return request({
+    url: '/risk/supervision/complaint/export',
+    method: 'post',
+    params: query,
+    responseType: 'blob'
+  });
+}
+
+/** 处理客诉节点(写节点表 + current_node + 1) */
+export function processComplaint(data: RiskSupervisionComplaintBo) {
+  return request({
+    url: '/risk/supervision/complaint/process',
+    method: 'put',
+    data
+  });
+}
+
+export type { RiskSupervisionComplaintBo, RiskSupervisionComplaintVo };

+ 63 - 0
src/api/risk/supervisionInspection.ts

@@ -0,0 +1,63 @@
+import request from '@/utils/request';
+import type {
+  RiskSupervisionInspectionBo,
+  RiskSupervisionInspectionVo
+} from './types';
+
+/** 监督抽检 - 抽检单 API */
+export function listInspection(query: Partial<RiskSupervisionInspectionBo> & { pageNum?: number; pageSize?: number }) {
+  return request({
+    url: '/risk/supervision/inspection/list',
+    method: 'get',
+    params: query
+  });
+}
+
+export function getInspection(id: string | number) {
+  return request({
+    url: `/risk/supervision/inspection/${id}`,
+    method: 'get'
+  });
+}
+
+export function addInspection(data: RiskSupervisionInspectionBo) {
+  return request({
+    url: '/risk/supervision/inspection',
+    method: 'post',
+    data
+  });
+}
+
+export function updateInspection(data: RiskSupervisionInspectionBo) {
+  return request({
+    url: '/risk/supervision/inspection',
+    method: 'put',
+    data
+  });
+}
+
+export function delInspection(ids: Array<string | number>) {
+  return request({
+    url: `/risk/supervision/inspection/${ids.join(',')}`,
+    method: 'delete'
+  });
+}
+
+export function exportInspection(query: Partial<RiskSupervisionInspectionBo>) {
+  return request({
+    url: '/risk/supervision/inspection/export',
+    method: 'post',
+    params: query,
+    responseType: 'blob'
+  });
+}
+
+/** 审核抽检单(待审核 → 已审核) */
+export function auditInspection(id: string | number) {
+  return request({
+    url: `/risk/supervision/inspection/audit/${id}`,
+    method: 'put'
+  });
+}
+
+export type { RiskSupervisionInspectionBo, RiskSupervisionInspectionVo };

+ 239 - 0
src/api/risk/types.ts

@@ -1093,3 +1093,242 @@ export interface RiskFoodSafetyMaterialVo {
   createTime: string;
 }
 
+// ==================== 监督抽检 / 平台客诉(3.1.4) ====================
+
+/** 监督抽检 - 抽检节点记录视图对象 */
+export interface SupervisionCaseNodeVo {
+  id: string | number;
+  caseId: string | number;
+  nodeCode: number;
+  nodeName: string;
+  opinion: string;
+  attachmentUrl: string;
+  attachmentName: string;
+  handlerId: string | number;
+  handlerName: string;
+  handleTime: string;
+  createTime: string;
+}
+
+/** 监督抽检 - 客诉节点记录视图对象 */
+export interface SupervisionComplaintNodeVo {
+  id: string | number;
+  complaintId: string | number;
+  nodeCode: number;
+  nodeName: string;
+  opinion: string;
+  attachmentUrl: string;
+  attachmentName: string;
+  handlerId: string | number;
+  handlerName: string;
+  handleTime: string;
+  createTime: string;
+}
+
+/** 监督抽检 - 抽检单业务对象 */
+export interface RiskSupervisionInspectionBo {
+  id?: string | number;
+  /** 抽检单号(JC+yyyyMMdd+4位序号) */
+  inspectionCode?: string;
+  /** 门店ID */
+  storeId?: string | number;
+  /** 门店名称 */
+  storeName?: string;
+  /** 抽检时间 */
+  inspectionTime?: string;
+  /** 抽检商品 */
+  productName?: string;
+  /** 抽检结果(字典:抽检合格/抽检不合格) */
+  inspectionResult?: string;
+  /** 不合格原因 */
+  failReason?: string;
+  /** 状态(字典:待审核/已审核) */
+  status?: string;
+  /** 抽检人ID */
+  inspectorId?: string | number;
+  /** 抽检人姓名 */
+  inspectorName?: string;
+  /** 备注 */
+  remark?: string;
+  /** 关键词 */
+  keyword?: string;
+  params?: any;
+}
+
+/** 监督抽检 - 抽检单视图对象 */
+export interface RiskSupervisionInspectionVo {
+  id: string | number;
+  inspectionCode: string;
+  storeId: string | number;
+  storeName: string;
+  inspectionTime: string;
+  productName: string;
+  inspectionResult: string;
+  failReason: string;
+  status: string;
+  inspectorId: string | number;
+  inspectorName: string;
+  remark: string;
+  createTime: string;
+  updateBy: string | number;
+  updateTime: string;
+}
+
+/** 监督抽检 - 不合格案件业务对象 */
+export interface RiskSupervisionCaseBo {
+  id?: string | number;
+  /** 案件编号(AJ+yyyyMMdd+4位序号) */
+  caseCode?: string;
+  /** 关联抽检单ID(新增时必填) */
+  inspectionId?: string | number;
+  /** 冗余抽检单号 */
+  inspectionCode?: string;
+  /** 涉事门店ID */
+  storeId?: string | number;
+  /** 涉事门店 */
+  storeName?: string;
+  /** 不合格产品 */
+  productName?: string;
+  /** 当前节点(1-5) */
+  currentNode?: number;
+  /** 当前节点名称 */
+  nodeName?: string;
+  /** 风险等级(字典:高风险/中风险/低风险) */
+  riskLevel?: string;
+  /** 风险描述 */
+  riskDesc?: string;
+  /** 控制措施 */
+  controlMeasures?: string;
+  /** 处理人ID */
+  handlerId?: string | number;
+  /** 处理人 */
+  handlerName?: string;
+  /** 状态(字典:处置中/已闭环) */
+  status?: string;
+  /** 备注 */
+  remark?: string;
+  /** 关键词搜索 */
+  keyword?: string;
+  /** 当前节点处理意见(节点处理时必填) */
+  opinion?: string;
+  /** 凭证附件URL(MinIO) */
+  attachmentUrl?: string;
+  /** 凭证附件原始文件名 */
+  attachmentName?: string;
+  params?: any;
+}
+
+/** 监督抽检 - 不合格案件视图对象(含历史节点) */
+export interface RiskSupervisionCaseVo {
+  id: string | number;
+  caseCode: string;
+  inspectionId: string | number;
+  inspectionCode: string;
+  storeId: string | number;
+  storeName: string;
+  productName: string;
+  currentNode: number;
+  nodeName: string;
+  riskLevel: string;
+  riskDesc: string;
+  controlMeasures: string;
+  handlerId: string | number;
+  handlerName: string;
+  status: string;
+  remark: string;
+  createTime: string;
+  updateBy: string | number;
+  updateTime: string;
+  /** 历史节点处理记录(仅详情接口填充) */
+  nodeRecords: SupervisionCaseNodeVo[];
+}
+
+/** 监督抽检 - 平台客诉业务对象 */
+export interface RiskSupervisionComplaintBo {
+  id?: string | number;
+  /** 客诉编号(TS+yyyyMMdd+4位序号) */
+  complaintCode?: string;
+  /** 客诉来源(字典) */
+  source?: string;
+  /** 投诉人姓名 */
+  complainantName?: string;
+  /** 投诉人电话 */
+  complainantPhone?: string;
+  /** 涉事门店ID */
+  storeId?: string | number;
+  /** 涉事门店 */
+  storeName?: string;
+  /** 移交时间 */
+  transferTime?: string;
+  /** 客诉内容 */
+  complaintContent?: string;
+  /** 客诉附件URL */
+  attachmentUrl?: string;
+  /** 客诉附件原始文件名 */
+  attachmentName?: string;
+  /** 当前节点(1-7) */
+  currentNode?: number;
+  /** 当前节点名称 */
+  nodeName?: string;
+  /** 状态(字典:处置中/已闭环) */
+  status?: string;
+  /** 备注 */
+  remark?: string;
+  /** 关键词搜索 */
+  keyword?: string;
+  /** 当前节点处理意见(节点处理时必填) */
+  opinion?: string;
+  /** 处理环节附件URL */
+  processAttachmentUrl?: string;
+  /** 处理环节附件原始文件名 */
+  processAttachmentName?: string;
+  params?: any;
+}
+
+/** 监督抽检 - 平台客诉视图对象(含历史节点) */
+export interface RiskSupervisionComplaintVo {
+  id: string | number;
+  complaintCode: string;
+  source: string;
+  complainantName: string;
+  complainantPhone: string;
+  storeId: string | number;
+  storeName: string;
+  transferTime: string;
+  complaintContent: string;
+  attachmentUrl: string;
+  attachmentName: string;
+  currentNode: number;
+  nodeName: string;
+  status: string;
+  remark: string;
+  createTime: string;
+  updateBy: string | number;
+  updateTime: string;
+  /** 历史节点处理记录(仅详情接口填充) */
+  nodeRecords: SupervisionComplaintNodeVo[];
+}
+
+/** 监督抽检 - 顶部 4 张数据看板 VO */
+export interface SupervisionStatsVo {
+  /** 今日待审核抽检单据总量 */
+  todayPending: number;
+  /** 不合格待处理案件数(状态=处置中) */
+  pendingCases: number;
+  /** 待处理客诉移交数(状态=处置中) */
+  pendingComplaints: number;
+  /** 本月抽检合格率 */
+  monthPassRate: SupervisionMonthPassRate;
+}
+
+export interface SupervisionMonthPassRate {
+  /** 统计月份 yyyy-MM */
+  month: string;
+  /** 本月合格单数 */
+  passCount: number;
+  /** 本月总单数 */
+  totalCount: number;
+  /** 合格率(0-100, 保留 2 位小数) */
+  passRate: number;
+}
+

+ 1649 - 0
src/views/risk/supervision/index.vue

@@ -0,0 +1,1649 @@
+<template>
+  <div class="p-2 supervision-page">
+    <!-- 页面标题 -->
+    <div class="page-header">
+      <h2 class="page-title">监督抽检 / 平台客诉</h2>
+      <p class="page-desc">门店抽检登记 · 不合格案件处理 · 平台客诉处理</p>
+    </div>
+
+    <!-- ① 顶部 4 张数据看板 -->
+    <el-row :gutter="20" class="mb-2">
+      <el-col :span="6">
+        <el-card shadow="hover" class="stat-card">
+          <div class="stat-card-content">
+            <div class="stat-info">
+              <div class="stat-title">今日待审核抽检</div>
+              <div class="stat-value">{{ stats.todayPending || 0 }}</div>
+              <div class="stat-extra">
+                <span>当日所有门店抽检单据总量</span>
+              </div>
+            </div>
+            <div class="stat-icon stat-icon-blue">
+              <el-icon><Document /></el-icon>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover" class="stat-card">
+          <div class="stat-card-content">
+            <div class="stat-info">
+              <div class="stat-title">不合格待处理案件</div>
+              <div class="stat-value">{{ stats.pendingCases || 0 }}</div>
+              <div class="stat-extra stat-extra-warning">
+                <span>处于各处置节点的未闭环案件</span>
+              </div>
+            </div>
+            <div class="stat-icon stat-icon-red">
+              <el-icon><Warning /></el-icon>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover" class="stat-card">
+          <div class="stat-card-content">
+            <div class="stat-info">
+              <div class="stat-title">待处理客诉移交</div>
+              <div class="stat-value">{{ stats.pendingComplaints || 0 }}</div>
+              <div class="stat-extra">
+                <span>待处理的消费者客诉单据</span>
+              </div>
+            </div>
+            <div class="stat-icon stat-icon-orange">
+              <el-icon><Phone /></el-icon>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="hover" class="stat-card">
+          <div class="stat-card-content">
+            <div class="stat-info">
+              <div class="stat-title">本月抽检合格率</div>
+              <div class="stat-value">{{ (stats.monthPassRate?.passRate ?? 0).toFixed(2) }}<span class="unit">%</span></div>
+              <div class="stat-extra stat-extra-up">
+                <span>合格 {{ stats.monthPassRate?.passCount || 0 }} / 总数 {{ stats.monthPassRate?.totalCount || 0 }} ({{ stats.monthPassRate?.month || '' }})</span>
+              </div>
+            </div>
+            <div class="stat-icon stat-icon-green">
+              <el-icon><DataAnalysis /></el-icon>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- ② 门店抽检登记 -->
+    <el-card shadow="hover" class="mb-2">
+      <template #header>
+        <div class="section-header">
+          <span class="section-title">门店抽检登记</span>
+          <el-button type="primary" :icon="Plus" @click="handleAddInspection">新增抽检单</el-button>
+        </div>
+      </template>
+
+      <el-form ref="inspQueryFormRef" :model="inspQuery" :inline="true" class="filter-bar">
+        <el-form-item label="关键词">
+          <el-input
+            v-model="inspQuery.keyword"
+            placeholder="搜索抽检单号/门店/商品"
+            clearable
+            style="width: 240px"
+            @keyup.enter="handleInspQuery"
+          />
+        </el-form-item>
+        <el-form-item label="抽检结果">
+          <el-select
+            v-model="inspQuery.inspectionResult"
+            placeholder="全部"
+            clearable
+            style="width: 160px"
+            @change="handleInspQuery"
+          >
+            <el-option label="全部" value="" />
+            <el-option
+              v-for="d in inspectionResultDict"
+              :key="d.value"
+              :label="d.label"
+              :value="d.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-select
+            v-model="inspQuery.status"
+            placeholder="全部"
+            clearable
+            style="width: 160px"
+            @change="handleInspQuery"
+          >
+            <el-option label="全部" value="" />
+            <el-option
+              v-for="d in inspectionStatusDict"
+              :key="d.value"
+              :label="d.label"
+              :value="d.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="resetInspQuery">重置</el-button>
+          <el-button type="primary" @click="handleInspQuery">搜索</el-button>
+        </el-form-item>
+      </el-form>
+
+      <el-table v-loading="inspLoading" :data="inspectionList" border stripe>
+        <el-table-column label="抽检单号" align="center" prop="inspectionCode" width="160" :show-overflow-tooltip="true" />
+        <el-table-column label="门店名称" align="center" prop="storeName" min-width="160" :show-overflow-tooltip="true" />
+        <el-table-column label="抽检时间" align="center" prop="inspectionTime" width="170">
+          <template #default="scope">{{ formatDateTime(scope.row.inspectionTime) }}</template>
+        </el-table-column>
+        <el-table-column label="抽检商品" align="center" prop="productName" min-width="160" :show-overflow-tooltip="true" />
+        <el-table-column label="抽检结果" align="center" width="110">
+          <template #default="scope">
+            <el-tag :type="scope.row.inspectionResult === '抽检合格' ? 'success' : 'danger'">
+              {{ scope.row.inspectionResult || '-' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="不合格原因" align="center" prop="failReason" min-width="160" :show-overflow-tooltip="true" />
+        <el-table-column label="审核状态" align="center" width="100">
+          <template #default="scope">
+            <el-tag :type="scope.row.status === '已审核' ? 'success' : 'warning'">{{ scope.row.status || '-' }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="抽检人" align="center" prop="inspectorName" width="100" />
+        <el-table-column label="操作" align="center" width="280" fixed="right" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-button link type="primary" @click="viewInspection(scope.row)">查看详情</el-button>
+            <el-button link type="warning" v-if="scope.row.inspectionResult === '抽检不合格' && !scope.row._hasCase" @click="openCreateCaseFromInspection(scope.row)">案件处理</el-button>
+            <el-button link type="success" v-if="scope.row.status === '待审核'" @click="auditInspection(scope.row)">审核</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination
+        v-show="inspTotal > 0"
+        v-model:page="inspQuery.pageNum"
+        v-model:limit="inspQuery.pageSize"
+        :total="inspTotal"
+        @pagination="getInspectionList"
+      />
+    </el-card>
+
+    <!-- ③ 不合格案件处理 -->
+    <el-card shadow="hover" class="mb-2">
+      <template #header>
+        <div class="section-header">
+          <span class="section-title">不合格案件处理</span>
+          <el-tag size="small">5 节点流程:风险控制 → 异议复检 → 调查取证 → 调查报告 → 闭环管理</el-tag>
+        </div>
+      </template>
+
+      <el-form ref="caseQueryFormRef" :model="caseQuery" :inline="true" class="filter-bar">
+        <el-form-item label="关键词">
+          <el-input
+            v-model="caseQuery.keyword"
+            placeholder="搜索案件编号/抽检单号/门店/产品"
+            clearable
+            style="width: 260px"
+            @keyup.enter="handleCaseQuery"
+          />
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-select
+            v-model="caseQuery.status"
+            placeholder="全部"
+            clearable
+            style="width: 160px"
+            @change="handleCaseQuery"
+          >
+            <el-option label="全部" value="" />
+            <el-option
+              v-for="d in caseStatusDict"
+              :key="d.value"
+              :label="d.label"
+              :value="d.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="resetCaseQuery">重置</el-button>
+          <el-button type="primary" @click="handleCaseQuery">搜索</el-button>
+        </el-form-item>
+      </el-form>
+
+      <el-table v-loading="caseLoading" :data="caseList" border stripe>
+        <el-table-column label="案件编号" align="center" prop="caseCode" width="160" :show-overflow-tooltip="true" />
+        <el-table-column label="关联抽检单" align="center" prop="inspectionCode" width="160" :show-overflow-tooltip="true" />
+        <el-table-column label="涉事门店" align="center" prop="storeName" min-width="160" :show-overflow-tooltip="true" />
+        <el-table-column label="不合格产品" align="center" prop="productName" min-width="160" :show-overflow-tooltip="true" />
+        <el-table-column label="风险等级" align="center" width="110">
+          <template #default="scope">
+            <el-tag :type="getRiskLevelTag(scope.row.riskLevel)">{{ scope.row.riskLevel || '-' }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="当前节点" align="center" width="180">
+          <template #default="scope">
+            <span>{{ scope.row.currentNode || '-' }} / 5 · {{ scope.row.nodeName || '' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="创建时间" align="center" width="170">
+          <template #default="scope">{{ formatDateTime(scope.row.createTime) }}</template>
+        </el-table-column>
+        <el-table-column label="状态" align="center" width="100">
+          <template #default="scope">
+            <el-tag :type="scope.row.status === '已闭环' ? 'success' : 'warning'">{{ scope.row.status || '-' }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="200" fixed="right" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-button link type="primary" @click="viewCaseDetail(scope.row)">查看详情</el-button>
+            <el-button
+              link
+              type="success"
+              v-if="canProcessCase(scope.row)"
+              @click="openProcessCase(scope.row)"
+            >处理</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination
+        v-show="caseTotal > 0"
+        v-model:page="caseQuery.pageNum"
+        v-model:limit="caseQuery.pageSize"
+        :total="caseTotal"
+        @pagination="getCaseList"
+      />
+    </el-card>
+
+    <!-- ④ 移交监管部门平台客诉处理 -->
+    <el-card shadow="hover" class="mb-2">
+      <template #header>
+        <div class="section-header">
+          <span class="section-title">移交监管部门平台客诉处理</span>
+          <div>
+            <el-tag size="small" class="mr-1">7 节点流程:风险控制 → 调查取证 → 调查判断 → 部门协同 → 依规处理 → 溯源整改 → 闭环管理</el-tag>
+            <el-button type="primary" :icon="Plus" @click="handleAddComplaint">新增客诉</el-button>
+          </div>
+        </div>
+      </template>
+
+      <el-form ref="cptQueryFormRef" :model="cptQuery" :inline="true" class="filter-bar">
+        <el-form-item label="关键词">
+          <el-input
+            v-model="cptQuery.keyword"
+            placeholder="搜索客诉编号/投诉人/门店/内容"
+            clearable
+            style="width: 260px"
+            @keyup.enter="handleCptQuery"
+          />
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-select
+            v-model="cptQuery.status"
+            placeholder="全部"
+            clearable
+            style="width: 160px"
+            @change="handleCptQuery"
+          >
+            <el-option label="全部" value="" />
+            <el-option
+              v-for="d in complaintStatusDict"
+              :key="d.value"
+              :label="d.label"
+              :value="d.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="resetCptQuery">重置</el-button>
+          <el-button type="primary" @click="handleCptQuery">搜索</el-button>
+        </el-form-item>
+      </el-form>
+
+      <el-table v-loading="cptLoading" :data="complaintList" border stripe>
+        <el-table-column label="客诉编号" align="center" prop="complaintCode" width="160" :show-overflow-tooltip="true" />
+        <el-table-column label="客诉来源" align="center" width="120">
+          <template #default="scope">{{ getDictLabel(complaintSourceDict, scope.row.source) }}</template>
+        </el-table-column>
+        <el-table-column label="投诉人" align="center" prop="complainantName" width="100" />
+        <el-table-column label="涉事门店" align="center" prop="storeName" min-width="160" :show-overflow-tooltip="true" />
+        <el-table-column label="客诉内容" align="center" prop="complaintContent" min-width="200" :show-overflow-tooltip="true" />
+        <el-table-column label="移交时间" align="center" prop="transferTime" width="170">
+          <template #default="scope">{{ formatDateTime(scope.row.transferTime) }}</template>
+        </el-table-column>
+        <el-table-column label="当前节点" align="center" width="180">
+          <template #default="scope">
+            <span>{{ scope.row.currentNode || '-' }} / 7 · {{ scope.row.nodeName || '' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" align="center" width="100">
+          <template #default="scope">
+            <el-tag :type="scope.row.status === '已闭环' ? 'success' : 'warning'">{{ scope.row.status || '-' }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="200" fixed="right" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-button link type="primary" @click="viewComplaintDetail(scope.row)">查看详情</el-button>
+            <el-button
+              link
+              type="success"
+              v-if="scope.row.status === '处置中'"
+              @click="openProcessComplaint(scope.row)"
+            >处理</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination
+        v-show="cptTotal > 0"
+        v-model:page="cptQuery.pageNum"
+        v-model:limit="cptQuery.pageSize"
+        :total="cptTotal"
+        @pagination="getComplaintList"
+      />
+    </el-card>
+
+    <!-- ⑤ 新增抽检单弹窗 -->
+    <el-dialog
+      v-model="inspDialog.visible"
+      :title="inspDialog.title"
+      width="720px"
+      append-to-body
+      :close-on-click-modal="false"
+      @close="closeInspDialog"
+    >
+      <el-form ref="inspFormRef" :model="inspForm" :rules="inspRules" label-width="100px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="门店名称" prop="storeName">
+              <el-input v-model="inspForm.storeName" placeholder="请输入门店名称" maxlength="200" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="抽检时间" prop="inspectionTime">
+              <el-date-picker
+                v-model="inspForm.inspectionTime"
+                type="datetime"
+                placeholder="请选择抽检时间"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                format="YYYY-MM-DD HH:mm"
+                style="width: 100%"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="抽检商品" prop="productName">
+          <el-input v-model="inspForm.productName" placeholder="请输入抽检商品" maxlength="200" />
+        </el-form-item>
+        <el-form-item label="抽检结果" prop="inspectionResult">
+          <el-radio-group v-model="inspForm.inspectionResult">
+            <el-radio
+              v-for="d in inspectionResultDict"
+              :key="d.value"
+              :value="d.value"
+            >{{ d.label }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item v-if="inspForm.inspectionResult === '抽检不合格'" label="不合格原因" prop="failReason">
+          <el-input
+            v-model="inspForm.failReason"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入不合格原因"
+            maxlength="1000"
+            show-word-limit
+          />
+        </el-form-item>
+        <el-form-item label="抽检人" prop="inspectorName">
+          <el-input v-model="inspForm.inspectorName" placeholder="请输入抽检人姓名" maxlength="50" />
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="inspForm.remark" placeholder="备注(选填)" maxlength="500" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="inspDialog.visible = false">取 消</el-button>
+          <el-button type="primary" @click="submitInspForm">保 存</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- ⑥ 抽检详情只读弹窗 -->
+    <el-dialog
+      v-model="inspDetailDialog.visible"
+      title="抽检单详情"
+      width="640px"
+      append-to-body
+      :close-on-click-modal="false"
+    >
+      <el-descriptions :column="2" border size="default">
+        <el-descriptions-item label="抽检单号">{{ inspDetail.inspectionCode }}</el-descriptions-item>
+        <el-descriptions-item label="抽检时间">{{ formatDateTime(inspDetail.inspectionTime) }}</el-descriptions-item>
+        <el-descriptions-item label="门店名称">{{ inspDetail.storeName }}</el-descriptions-item>
+        <el-descriptions-item label="抽检人">{{ inspDetail.inspectorName }}</el-descriptions-item>
+        <el-descriptions-item label="抽检商品" :span="2">{{ inspDetail.productName }}</el-descriptions-item>
+        <el-descriptions-item label="抽检结果">
+          <el-tag :type="inspDetail.inspectionResult === '抽检合格' ? 'success' : 'danger'">
+            {{ inspDetail.inspectionResult }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="审核状态">
+          <el-tag :type="inspDetail.status === '已审核' ? 'success' : 'warning'">{{ inspDetail.status }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item v-if="inspDetail.inspectionResult === '抽检不合格'" label="不合格原因" :span="2">
+          {{ inspDetail.failReason || '—' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="备注" :span="2">{{ inspDetail.remark || '—' }}</el-descriptions-item>
+      </el-descriptions>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="inspDetailDialog.visible = false">关 闭</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- ⑦ 案件处理详情弹窗(5 节点步骤条 + 当前节点处理 + 历史节点时间线) -->
+    <el-dialog
+      v-model="caseDialog.visible"
+      :title="caseDialog.title"
+      width="960px"
+      append-to-body
+      :close-on-click-modal="false"
+      @close="closeCaseDialog"
+    >
+      <div v-loading="caseDialog.loading">
+        <!-- 案件基本信息 -->
+        <el-descriptions :column="3" border size="small" class="mb-2">
+          <el-descriptions-item label="案件编号">{{ caseDetail.caseCode }}</el-descriptions-item>
+          <el-descriptions-item label="关联抽检单">{{ caseDetail.inspectionCode }}</el-descriptions-item>
+          <el-descriptions-item label="状态">
+            <el-tag :type="caseDetail.status === '已闭环' ? 'success' : 'warning'">{{ caseDetail.status }}</el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="涉事门店">{{ caseDetail.storeName }}</el-descriptions-item>
+          <el-descriptions-item label="不合格产品">{{ caseDetail.productName }}</el-descriptions-item>
+          <el-descriptions-item label="风险等级">
+            <el-tag :type="getRiskLevelTag(caseDetail.riskLevel)">{{ caseDetail.riskLevel }}</el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="处理人">{{ caseDetail.handlerName }}</el-descriptions-item>
+          <el-descriptions-item label="创建时间">{{ formatDateTime(caseDetail.createTime) }}</el-descriptions-item>
+          <el-descriptions-item label="风险描述" :span="3">{{ caseDetail.riskDesc }}</el-descriptions-item>
+          <el-descriptions-item label="控制措施" :span="3">{{ caseDetail.controlMeasures }}</el-descriptions-item>
+        </el-descriptions>
+
+        <!-- 5 节点步骤条 -->
+        <el-steps :active="getCaseActive()" finish-status="success" align-center class="mb-2">
+          <el-step v-for="(name, idx) in CASE_NODE_NAMES" :key="idx" :title="`${idx + 1}. ${name}`" />
+        </el-steps>
+
+        <!-- 当前节点处理表单 -->
+        <el-card v-if="caseDetail.status === '处置中'" shadow="never" class="mb-2">
+          <template #header>
+            <span class="section-title">当前节点处理 · {{ caseDetail.nodeName }}</span>
+          </template>
+          <el-form ref="processCaseFormRef" :model="processCaseForm" :rules="processCaseRules" label-width="100px">
+            <el-form-item label="处理意见" prop="opinion">
+              <el-input
+                v-model="processCaseForm.opinion"
+                type="textarea"
+                :rows="3"
+                placeholder="请输入当前节点处理意见"
+                maxlength="2000"
+                show-word-limit
+              />
+            </el-form-item>
+            <el-form-item label="凭证附件">
+              <el-upload
+                v-model:file-list="caseFileList"
+                :http-request="customUpload"
+                :limit="1"
+                :accept="'.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png'"
+                :before-upload="beforeAttachmentUpload"
+                :on-success="handleAttachmentSuccess"
+                :on-error="handleUploadError"
+                :on-exceed="handleAttachmentExceed"
+                :on-remove="handleAttachmentRemove"
+              >
+                <el-button type="primary" plain>选择凭证附件</el-button>
+                <template #tip>
+                  <div class="el-upload__tip">支持 PDF/Word/Excel/PPT/图片,单个文件不超过 50MB</div>
+                </template>
+              </el-upload>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" :loading="caseDialog.processing" @click="submitProcessCase">提交并流转下一节点</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+
+        <!-- 历史节点时间线 -->
+        <el-card shadow="never">
+          <template #header>
+            <span class="section-title">历史节点记录</span>
+          </template>
+          <el-empty v-if="!caseDetail.nodeRecords || caseDetail.nodeRecords.length === 0" description="暂无节点记录" :image-size="60" />
+          <el-timeline v-else>
+            <el-timeline-item
+              v-for="n in caseDetail.nodeRecords"
+              :key="n.id"
+              :timestamp="formatDateTime(n.handleTime)"
+              placement="top"
+              :type="n.nodeCode === caseDetail.currentNode ? 'primary' : 'success'"
+            >
+              <div class="timeline-node">
+                <div class="timeline-title">节点{{ n.nodeCode }} · {{ n.nodeName }}</div>
+                <div class="timeline-meta">处理人:{{ n.handlerName || '—' }}</div>
+                <div class="timeline-opinion">{{ n.opinion }}</div>
+                <div v-if="n.attachmentUrl" class="timeline-attach">
+                  <el-link :href="n.attachmentUrl" target="_blank" type="primary">{{ n.attachmentName || '查看附件' }}</el-link>
+                </div>
+              </div>
+            </el-timeline-item>
+          </el-timeline>
+        </el-card>
+      </div>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="caseDialog.visible = false">关 闭</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- ⑧ 案件处理弹窗(从不合格抽检单发起) -->
+    <el-dialog
+      v-model="createCaseDialog.visible"
+      title="发起案件处理"
+      width="680px"
+      append-to-body
+      :close-on-click-modal="false"
+    >
+      <el-form ref="createCaseFormRef" :model="createCaseForm" :rules="createCaseRules" label-width="100px">
+        <el-form-item label="关联抽检单">
+          <span class="readonly-text">{{ createCaseForm.inspectionCode }} · {{ createCaseForm.storeName }} · {{ createCaseForm.productName }}</span>
+        </el-form-item>
+        <el-form-item label="风险等级" prop="riskLevel">
+          <el-select v-model="createCaseForm.riskLevel" placeholder="请选择风险等级" style="width: 100%">
+            <el-option
+              v-for="d in riskLevelDict"
+              :key="d.value"
+              :label="d.label"
+              :value="d.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="风险描述" prop="riskDesc">
+          <el-input v-model="createCaseForm.riskDesc" type="textarea" :rows="3" placeholder="请输入风险描述" maxlength="2000" show-word-limit />
+        </el-form-item>
+        <el-form-item label="控制措施" prop="controlMeasures">
+          <el-input v-model="createCaseForm.controlMeasures" type="textarea" :rows="3" placeholder="请输入控制措施" maxlength="2000" show-word-limit />
+        </el-form-item>
+        <el-form-item label="处理人" prop="handlerName">
+          <el-input v-model="createCaseForm.handlerName" placeholder="请输入处理人姓名" maxlength="50" />
+        </el-form-item>
+        <el-form-item label="初始意见" prop="opinion">
+          <el-input v-model="createCaseForm.opinion" type="textarea" :rows="3" placeholder="请输入风险控制节点处理意见" maxlength="2000" show-word-limit />
+        </el-form-item>
+        <el-form-item label="凭证附件">
+          <el-upload
+            v-model:file-list="caseFileList"
+            :http-request="customUpload"
+            :limit="1"
+            :accept="'.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png'"
+            :before-upload="beforeAttachmentUpload"
+            :on-success="handleAttachmentSuccess"
+            :on-error="handleUploadError"
+            :on-exceed="handleAttachmentExceed"
+            :on-remove="handleAttachmentRemove"
+          >
+            <el-button type="primary" plain>选择凭证附件</el-button>
+            <template #tip>
+              <div class="el-upload__tip">支持 PDF/Word/Excel/PPT/图片,单个文件不超过 50MB</div>
+            </template>
+          </el-upload>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="createCaseDialog.visible = false">取 消</el-button>
+          <el-button type="primary" :loading="createCaseDialog.submitting" @click="submitCreateCase">发起案件</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- ⑨ 客诉处理详情弹窗(7 节点步骤条 + 当前节点处理 + 历史节点时间线) -->
+    <el-dialog
+      v-model="cptDialog.visible"
+      title="客诉处理详情"
+      width="960px"
+      append-to-body
+      :close-on-click-modal="false"
+      @close="closeCptDialog"
+    >
+      <div v-loading="cptDialog.loading">
+        <el-descriptions :column="3" border size="small" class="mb-2">
+          <el-descriptions-item label="客诉编号">{{ cptDetail.complaintCode }}</el-descriptions-item>
+          <el-descriptions-item label="客诉来源">{{ getDictLabel(complaintSourceDict, cptDetail.source) }}</el-descriptions-item>
+          <el-descriptions-item label="状态">
+            <el-tag :type="cptDetail.status === '已闭环' ? 'success' : 'warning'">{{ cptDetail.status }}</el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="投诉人">{{ cptDetail.complainantName }}</el-descriptions-item>
+          <el-descriptions-item label="联系电话">{{ cptDetail.complainantPhone }}</el-descriptions-item>
+          <el-descriptions-item label="涉事门店">{{ cptDetail.storeName }}</el-descriptions-item>
+          <el-descriptions-item label="移交时间" :span="2">{{ formatDateTime(cptDetail.transferTime) }}</el-descriptions-item>
+          <el-descriptions-item label="客诉内容" :span="3">{{ cptDetail.complaintContent }}</el-descriptions-item>
+        </el-descriptions>
+
+        <el-steps :active="getCptActive()" finish-status="success" align-center class="mb-2">
+          <el-step v-for="(name, idx) in COMPLAINT_NODE_NAMES" :key="idx" :title="`${idx + 1}. ${name}`" />
+        </el-steps>
+
+        <el-card v-if="cptDetail.status === '处置中'" shadow="never" class="mb-2">
+          <template #header>
+            <span class="section-title">当前节点处理 · {{ cptDetail.nodeName }}</span>
+          </template>
+          <el-form ref="processCptFormRef" :model="processCptForm" :rules="processCptRules" label-width="100px">
+            <el-form-item label="处理意见" prop="opinion">
+              <el-input
+                v-model="processCptForm.opinion"
+                type="textarea"
+                :rows="3"
+                placeholder="请输入当前环节处理意见"
+                maxlength="2000"
+                show-word-limit
+              />
+            </el-form-item>
+            <el-form-item label="凭证附件">
+              <el-upload
+                v-model:file-list="cptFileList"
+                :http-request="customUpload"
+                :limit="1"
+                :accept="'.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png'"
+                :before-upload="beforeAttachmentUpload"
+                :on-success="handleCptAttachmentSuccess"
+                :on-error="handleUploadError"
+                :on-exceed="handleAttachmentExceed"
+                :on-remove="handleCptAttachmentRemove"
+              >
+                <el-button type="primary" plain>选择凭证附件</el-button>
+                <template #tip>
+                  <div class="el-upload__tip">支持 PDF/Word/Excel/PPT/图片,单个文件不超过 50MB</div>
+                </template>
+              </el-upload>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" :loading="cptDialog.processing" @click="submitProcessComplaint">提交并流转下一环节</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+
+        <el-card shadow="never">
+          <template #header>
+            <span class="section-title">历史节点记录</span>
+          </template>
+          <el-empty v-if="!cptDetail.nodeRecords || cptDetail.nodeRecords.length === 0" description="暂无节点记录" :image-size="60" />
+          <el-timeline v-else>
+            <el-timeline-item
+              v-for="n in cptDetail.nodeRecords"
+              :key="n.id"
+              :timestamp="formatDateTime(n.handleTime)"
+              placement="top"
+              :type="n.nodeCode === cptDetail.currentNode ? 'primary' : 'success'"
+            >
+              <div class="timeline-node">
+                <div class="timeline-title">节点{{ n.nodeCode }} · {{ n.nodeName }}</div>
+                <div class="timeline-meta">处理人:{{ n.handlerName || '—' }}</div>
+                <div class="timeline-opinion">{{ n.opinion }}</div>
+                <div v-if="n.attachmentUrl" class="timeline-attach">
+                  <el-link :href="n.attachmentUrl" target="_blank" type="primary">{{ n.attachmentName || '查看附件' }}</el-link>
+                </div>
+              </div>
+            </el-timeline-item>
+          </el-timeline>
+        </el-card>
+      </div>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="cptDialog.visible = false">关 闭</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- ⑩ 新增客诉弹窗 -->
+    <el-dialog
+      v-model="addCptDialog.visible"
+      title="新增客诉"
+      width="720px"
+      append-to-body
+      :close-on-click-modal="false"
+      @close="closeAddCptDialog"
+    >
+      <el-form ref="addCptFormRef" :model="addCptForm" :rules="addCptRules" label-width="100px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="客诉来源" prop="source">
+              <el-select v-model="addCptForm.source" placeholder="请选择客诉来源" style="width: 100%">
+                <el-option
+                  v-for="d in complaintSourceDict"
+                  :key="d.value"
+                  :label="d.label"
+                  :value="d.value"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="移交时间" prop="transferTime">
+              <el-date-picker
+                v-model="addCptForm.transferTime"
+                type="datetime"
+                placeholder="请选择移交时间"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                format="YYYY-MM-DD HH:mm"
+                style="width: 100%"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="投诉人姓名" prop="complainantName">
+              <el-input v-model="addCptForm.complainantName" placeholder="请输入投诉人姓名" maxlength="50" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="联系电话" prop="complainantPhone">
+              <el-input v-model="addCptForm.complainantPhone" placeholder="请输入联系电话" maxlength="20" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="涉事门店" prop="storeName">
+          <el-input v-model="addCptForm.storeName" placeholder="请输入涉事门店名称" maxlength="200" />
+        </el-form-item>
+        <el-form-item label="客诉内容" prop="complaintContent">
+          <el-input v-model="addCptForm.complaintContent" type="textarea" :rows="4" placeholder="请输入客诉内容" maxlength="2000" show-word-limit />
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="addCptForm.remark" placeholder="备注(选填)" maxlength="500" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="addCptDialog.visible = false">取 消</el-button>
+          <el-button type="primary" :loading="addCptDialog.submitting" @click="submitAddComplaint">保 存</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="Supervision" lang="ts">
+import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
+import type { ComponentInternalInstance } from 'vue';
+import {
+  listInspection,
+  getInspection,
+  addInspection,
+  auditInspection as apiAuditInspection
+} from '@/api/risk/supervisionInspection';
+import {
+  listCase,
+  getCase,
+  addCase,
+  processCase
+} from '@/api/risk/supervisionCase';
+import {
+  listComplaint,
+  getComplaint,
+  addComplaint,
+  processComplaint
+} from '@/api/risk/supervisionComplaint';
+import { getSupervisionStats } from '@/api/risk/supervision';
+import { uploadFile } from '@/api/risk/file';
+import { getDicts } from '@/api/system/dict/data';
+import type { DictDataVO } from '@/api/system/dict/data/types';
+import type {
+  RiskSupervisionInspectionBo,
+  RiskSupervisionInspectionVo,
+  RiskSupervisionCaseBo,
+  RiskSupervisionCaseVo,
+  RiskSupervisionComplaintBo,
+  RiskSupervisionComplaintVo,
+  SupervisionStatsVo
+} from '@/api/risk/types';
+import { ElMessage, FormInstance, UploadUserFile } from 'element-plus';
+import {
+  Document,
+  Warning,
+  Phone,
+  DataAnalysis,
+  Plus
+} from '@element-plus/icons-vue';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+// ============ 常量 ============
+const CASE_NODE_NAMES = ['风险控制', '异议复检', '调查取证', '调查报告', '闭环管理'];
+const COMPLAINT_NODE_NAMES = ['风险控制', '调查取证', '调查判断', '部门协同', '依规处理', '溯源整改', '闭环管理'];
+
+// ============ 字典 ============
+const inspectionResultDict = ref<Array<{ label: string; value: string }>>([]);
+const inspectionStatusDict = ref<Array<{ label: string; value: string }>>([]);
+const riskLevelDict = ref<Array<{ label: string; value: string }>>([]);
+const complaintSourceDict = ref<Array<{ label: string; value: string }>>([]);
+const caseStatusDict = ref<Array<{ label: string; value: string }>>([]);
+const complaintStatusDict = ref<Array<{ label: string; value: string }>>([]);
+
+const mapDictData = (list: DictDataVO[]) =>
+  (list || [])
+    .filter((d: any) => d.status === '0' || d.status === undefined)
+    .sort((a, b) => (a.dictSort || 0) - (b.dictSort || 0))
+    .map((d) => ({ label: d.dictLabel, value: d.dictValue }));
+
+const loadDicts = async () => {
+  try {
+    const [
+      resultRes,
+      statusRes,
+      levelRes,
+      sourceRes,
+      caseStRes,
+      cptStRes
+    ] = await Promise.all([
+      getDicts('risk_supervision_inspection_result'),
+      getDicts('risk_supervision_inspection_status'),
+      getDicts('risk_supervision_risk_level'),
+      getDicts('risk_supervision_complaint_source'),
+      getDicts('risk_supervision_case_status'),
+      getDicts('risk_supervision_complaint_status')
+    ]);
+    inspectionResultDict.value = mapDictData(resultRes.data || []);
+    inspectionStatusDict.value = mapDictData(statusRes.data || []);
+    riskLevelDict.value = mapDictData(levelRes.data || []);
+    complaintSourceDict.value = mapDictData(sourceRes.data || []);
+    caseStatusDict.value = mapDictData(caseStRes.data || []);
+    complaintStatusDict.value = mapDictData(cptStRes.data || []);
+  } catch (e) {
+    console.error('加载监督抽检字典失败', e);
+  }
+};
+
+const getDictLabel = (opts: Array<{ label: string; value: string }>, val: string) => {
+  if (!val) return '';
+  const found = opts.find((o) => o.value === val);
+  return found?.label || val;
+};
+
+// ============ 顶部看板 ============
+const stats = ref<SupervisionStatsVo>({
+  todayPending: 0,
+  pendingCases: 0,
+  pendingComplaints: 0,
+  monthPassRate: { month: '', passCount: 0, totalCount: 0, passRate: 0 }
+});
+
+const loadStats = async () => {
+  try {
+    const res: any = await getSupervisionStats();
+    if (res?.data) {
+      stats.value = { ...stats.value, ...res.data };
+    }
+  } catch (e) {
+    console.error('加载看板失败', e);
+  }
+};
+
+// ============ 抽检单列表 ============
+const inspectionList = ref<RiskSupervisionInspectionVo[]>([]);
+const inspTotal = ref(0);
+const inspLoading = ref(false);
+const inspQuery = ref({
+  pageNum: 1,
+  pageSize: 10,
+  keyword: '',
+  inspectionResult: '',
+  status: ''
+});
+
+const getInspectionList = async () => {
+  inspLoading.value = true;
+  try {
+    const res: any = await listInspection(inspQuery.value);
+    inspectionList.value = res.rows || [];
+    inspTotal.value = res.total || 0;
+  } finally {
+    inspLoading.value = false;
+  }
+};
+
+const handleInspQuery = () => {
+  inspQuery.value.pageNum = 1;
+  getInspectionList();
+};
+
+const resetInspQuery = () => {
+  inspQuery.value = {
+    pageNum: 1,
+    pageSize: 10,
+    keyword: '',
+    inspectionResult: '',
+    status: ''
+  };
+  getInspectionList();
+};
+
+// ============ 案件列表 ============
+const caseList = ref<RiskSupervisionCaseVo[]>([]);
+const caseTotal = ref(0);
+const caseLoading = ref(false);
+const caseQuery = ref({
+  pageNum: 1,
+  pageSize: 10,
+  keyword: '',
+  status: ''
+});
+
+const getCaseList = async () => {
+  caseLoading.value = true;
+  try {
+    const res: any = await listCase(caseQuery.value);
+    caseList.value = res.rows || [];
+    caseTotal.value = res.total || 0;
+  } finally {
+    caseLoading.value = false;
+  }
+};
+
+const handleCaseQuery = () => {
+  caseQuery.value.pageNum = 1;
+  getCaseList();
+};
+
+const resetCaseQuery = () => {
+  caseQuery.value = { pageNum: 1, pageSize: 10, keyword: '', status: '' };
+  getCaseList();
+};
+
+// ============ 客诉列表 ============
+const complaintList = ref<RiskSupervisionComplaintVo[]>([]);
+const cptTotal = ref(0);
+const cptLoading = ref(false);
+const cptQuery = ref({
+  pageNum: 1,
+  pageSize: 10,
+  keyword: '',
+  status: ''
+});
+
+const getComplaintList = async () => {
+  cptLoading.value = true;
+  try {
+    const res: any = await listComplaint(cptQuery.value);
+    complaintList.value = res.rows || [];
+    cptTotal.value = res.total || 0;
+  } finally {
+    cptLoading.value = false;
+  }
+};
+
+const handleCptQuery = () => {
+  cptQuery.value.pageNum = 1;
+  getComplaintList();
+};
+
+const resetCptQuery = () => {
+  cptQuery.value = { pageNum: 1, pageSize: 10, keyword: '', status: '' };
+  getComplaintList();
+};
+
+// ============ 新增抽检单 ============
+const inspDialog = reactive({ visible: false, title: '新增抽检单' });
+const inspFormRef = ref<FormInstance>();
+const inspForm = ref<RiskSupervisionInspectionBo>({
+  id: undefined,
+  inspectionCode: '',
+  storeName: '',
+  inspectionTime: '',
+  productName: '',
+  inspectionResult: '抽检合格',
+  failReason: '',
+  inspectorName: '',
+  remark: ''
+});
+const inspRules = {
+  storeName: [{ required: true, message: '请输入门店名称', trigger: 'blur' }],
+  inspectionTime: [{ required: true, message: '请选择抽检时间', trigger: 'change' }],
+  productName: [{ required: true, message: '请输入抽检商品', trigger: 'blur' }],
+  inspectionResult: [{ required: true, message: '请选择抽检结果', trigger: 'change' }],
+  failReason: [{
+    validator(_: any, val: any, cb: any) {
+      if (inspForm.value.inspectionResult === '抽检不合格' && !val) {
+        cb(new Error('请输入不合格原因'));
+      } else {
+        cb();
+      }
+    },
+    trigger: 'blur'
+  }],
+  inspectorName: [{ required: true, message: '请输入抽检人姓名', trigger: 'blur' }]
+};
+
+const resetInspForm = () => {
+  inspForm.value = {
+    id: undefined,
+    inspectionCode: '',
+    storeName: '',
+    inspectionTime: '',
+    productName: '',
+    inspectionResult: '抽检合格',
+    failReason: '',
+    inspectorName: '',
+    remark: ''
+  };
+  inspFormRef.value?.resetFields();
+};
+
+const handleAddInspection = () => {
+  resetInspForm();
+  inspDialog.title = '新增抽检单';
+  inspDialog.visible = true;
+};
+
+const closeInspDialog = () => {
+  inspDialog.visible = false;
+  resetInspForm();
+};
+
+const submitInspForm = () => {
+  inspFormRef.value?.validate(async (valid: boolean) => {
+    if (!valid) return;
+    try {
+      await addInspection(inspForm.value);
+      ElMessage.success('新增成功');
+      inspDialog.visible = false;
+      await getInspectionList();
+      await loadStats();
+    } catch (e: any) {
+      ElMessage.error(e?.msg || '新增失败');
+    }
+  });
+};
+
+// ============ 抽检详情(只读) ============
+const inspDetailDialog = reactive({ visible: false });
+const inspDetail = ref<RiskSupervisionInspectionVo>({} as any);
+
+const viewInspection = async (row: RiskSupervisionInspectionVo) => {
+  try {
+    const res: any = await getInspection(row.id);
+    inspDetail.value = res.data || {};
+    inspDetailDialog.visible = true;
+  } catch (e) {
+    ElMessage.error('获取抽检单详情失败');
+  }
+};
+
+// ============ 审核抽检单 ============
+const auditInspection = async (row: RiskSupervisionInspectionVo) => {
+  try {
+    await proxy?.$modal.confirm(`确认审核抽检单 [${row.inspectionCode}] 吗?`);
+    await apiAuditInspection(row.id);
+    ElMessage.success('审核成功');
+    await getInspectionList();
+    await loadStats();
+  } catch (e) {
+    // 取消
+  }
+};
+
+// ============ 案件处理详情弹窗 ============
+const caseDialog = reactive({ visible: false, title: '案件处理详情', loading: false, processing: false });
+const caseDetail = ref<RiskSupervisionCaseVo>({} as any);
+const processCaseFormRef = ref<FormInstance>();
+const processCaseForm = ref<RiskSupervisionCaseBo>({
+  id: undefined,
+  opinion: '',
+  attachmentUrl: '',
+  attachmentName: ''
+});
+const processCaseRules = {
+  opinion: [{ required: true, message: '请输入处理意见', trigger: 'blur' }]
+};
+
+const viewCaseDetail = async (row: RiskSupervisionCaseVo) => {
+  caseDialog.title = '案件处理详情';
+  caseDialog.visible = true;
+  caseDialog.loading = true;
+  try {
+    const res: any = await getCase(row.id);
+    caseDetail.value = res.data || {};
+    resetProcessCaseForm();
+  } finally {
+    caseDialog.loading = false;
+  }
+};
+
+const openProcessCase = async (row: RiskSupervisionCaseVo) => {
+  caseDialog.title = `案件处理 · 节点${row.currentNode} · ${row.nodeName}`;
+  caseDialog.visible = true;
+  caseDialog.loading = true;
+  try {
+    const res: any = await getCase(row.id);
+    caseDetail.value = res.data || {};
+    resetProcessCaseForm();
+  } finally {
+    caseDialog.loading = false;
+  }
+};
+
+const resetProcessCaseForm = () => {
+  processCaseForm.value = { id: caseDetail.value?.id, opinion: '', attachmentUrl: '', attachmentName: '' };
+  caseFileList.value = [];
+  processCaseFormRef.value?.resetFields();
+};
+
+const getCaseActive = (): number => {
+  // 已闭环:全部完成 → 5;处置中:currentNode - 1 (el-steps active 是已完成数)
+  if (!caseDetail.value) return 0;
+  if (caseDetail.value.status === '已闭环') return CASE_NODE_NAMES.length;
+  return (caseDetail.value.currentNode || 1) - 1;
+};
+
+const canProcessCase = (row: RiskSupervisionCaseVo): boolean => {
+  if (row.status !== '处置中') return false;
+  // 异议复检 / 闭环管理 节点不展示处理按钮(异议复检为流程节点, 闭环管理为已完成)
+  const noProcessNodes = ['异议复检', '闭环管理'];
+  return !noProcessNodes.includes(row.nodeName || '');
+};
+
+const closeCaseDialog = () => {
+  caseDialog.visible = false;
+  caseDetail.value = {} as any;
+  caseFileList.value = [];
+};
+
+const submitProcessCase = () => {
+  processCaseFormRef.value?.validate(async (valid: boolean) => {
+    if (!valid) return;
+    caseDialog.processing = true;
+    try {
+      processCaseForm.value.id = caseDetail.value.id;
+      await processCase(processCaseForm.value);
+      ElMessage.success('节点处理成功,已流转下一节点');
+      caseDialog.processing = false;
+      // 刷新详情
+      const res: any = await getCase(caseDetail.value.id);
+      caseDetail.value = res.data || {};
+      caseFileList.value = [];
+      processCaseForm.value = { id: caseDetail.value.id, opinion: '', attachmentUrl: '', attachmentName: '' };
+      await getCaseList();
+      await loadStats();
+    } catch (e: any) {
+      caseDialog.processing = false;
+      ElMessage.error(e?.msg || '节点处理失败');
+    }
+  });
+};
+
+// ============ 从不合格抽检单发起案件 ============
+const createCaseDialog = reactive({ visible: false, submitting: false });
+const createCaseFormRef = ref<FormInstance>();
+const createCaseForm = ref<RiskSupervisionCaseBo>({
+  inspectionId: undefined,
+  inspectionCode: '',
+  storeName: '',
+  productName: '',
+  riskLevel: '',
+  riskDesc: '',
+  controlMeasures: '',
+  handlerName: '',
+  opinion: '',
+  attachmentUrl: '',
+  attachmentName: ''
+});
+const createCaseRules = {
+  riskLevel: [{ required: true, message: '请选择风险等级', trigger: 'change' }],
+  riskDesc: [{ required: true, message: '请输入风险描述', trigger: 'blur' }],
+  controlMeasures: [{ required: true, message: '请输入控制措施', trigger: 'blur' }],
+  handlerName: [{ required: true, message: '请输入处理人姓名', trigger: 'blur' }],
+  opinion: [{ required: true, message: '请输入风险控制节点处理意见', trigger: 'blur' }]
+};
+
+const openCreateCaseFromInspection = async (row: RiskSupervisionInspectionVo) => {
+  try {
+    const res: any = await getInspection(row.id);
+    const data = res.data || {};
+    createCaseForm.value = {
+      inspectionId: data.id,
+      inspectionCode: data.inspectionCode,
+      storeName: data.storeName,
+      productName: data.productName,
+      riskLevel: '',
+      riskDesc: '',
+      controlMeasures: '',
+      handlerName: '',
+      opinion: '',
+      attachmentUrl: '',
+      attachmentName: ''
+    };
+    caseFileList.value = [];
+    createCaseDialog.visible = true;
+  } catch (e) {
+    ElMessage.error('加载抽检单失败');
+  }
+};
+
+const submitCreateCase = () => {
+  createCaseFormRef.value?.validate(async (valid: boolean) => {
+    if (!valid) return;
+    createCaseDialog.submitting = true;
+    try {
+      await addCase(createCaseForm.value);
+      ElMessage.success('案件已发起,初始节点:风险控制');
+      createCaseDialog.submitting = false;
+      createCaseDialog.visible = false;
+      caseFileList.value = [];
+      await getCaseList();
+      await getInspectionList();
+      await loadStats();
+    } catch (e: any) {
+      createCaseDialog.submitting = false;
+      ElMessage.error(e?.msg || '案件发起失败');
+    }
+  });
+};
+
+// ============ 客诉处理详情 ============
+const cptDialog = reactive({ visible: false, loading: false, processing: false });
+const cptDetail = ref<RiskSupervisionComplaintVo>({} as any);
+const processCptFormRef = ref<FormInstance>();
+const processCptForm = ref<RiskSupervisionComplaintBo>({
+  id: undefined,
+  opinion: '',
+  processAttachmentUrl: '',
+  processAttachmentName: ''
+});
+const processCptRules = {
+  opinion: [{ required: true, message: '请输入处理意见', trigger: 'blur' }]
+};
+
+const viewComplaintDetail = async (row: RiskSupervisionComplaintVo) => {
+  cptDialog.visible = true;
+  cptDialog.loading = true;
+  try {
+    const res: any = await getComplaint(row.id);
+    cptDetail.value = res.data || {};
+    resetProcessCptForm();
+  } finally {
+    cptDialog.loading = false;
+  }
+};
+
+const openProcessComplaint = async (row: RiskSupervisionComplaintVo) => {
+  cptDialog.visible = true;
+  cptDialog.loading = true;
+  try {
+    const res: any = await getComplaint(row.id);
+    cptDetail.value = res.data || {};
+    resetProcessCptForm();
+  } finally {
+    cptDialog.loading = false;
+  }
+};
+
+const resetProcessCptForm = () => {
+  processCptForm.value = {
+    id: cptDetail.value?.id,
+    opinion: '',
+    processAttachmentUrl: '',
+    processAttachmentName: ''
+  };
+  cptFileList.value = [];
+  processCptFormRef.value?.resetFields();
+};
+
+const getCptActive = (): number => {
+  if (!cptDetail.value) return 0;
+  if (cptDetail.value.status === '已闭环') return COMPLAINT_NODE_NAMES.length;
+  return (cptDetail.value.currentNode || 1) - 1;
+};
+
+const closeCptDialog = () => {
+  cptDialog.visible = false;
+  cptDetail.value = {} as any;
+  cptFileList.value = [];
+};
+
+const submitProcessComplaint = () => {
+  processCptFormRef.value?.validate(async (valid: boolean) => {
+    if (!valid) return;
+    cptDialog.processing = true;
+    try {
+      processCptForm.value.id = cptDetail.value.id;
+      await processComplaint(processCptForm.value);
+      ElMessage.success('客诉环节处理成功,已流转下一环节');
+      cptDialog.processing = false;
+      const res: any = await getComplaint(cptDetail.value.id);
+      cptDetail.value = res.data || {};
+      cptFileList.value = [];
+      processCptForm.value = {
+        id: cptDetail.value.id,
+        opinion: '',
+        processAttachmentUrl: '',
+        processAttachmentName: ''
+      };
+      await getComplaintList();
+      await loadStats();
+    } catch (e: any) {
+      cptDialog.processing = false;
+      ElMessage.error(e?.msg || '客诉节点处理失败');
+    }
+  });
+};
+
+// ============ 新增客诉 ============
+const addCptDialog = reactive({ visible: false, submitting: false });
+const addCptFormRef = ref<FormInstance>();
+const addCptForm = ref<RiskSupervisionComplaintBo>({
+  source: '',
+  complainantName: '',
+  complainantPhone: '',
+  storeName: '',
+  transferTime: '',
+  complaintContent: '',
+  remark: ''
+});
+const addCptRules = {
+  source: [{ required: true, message: '请选择客诉来源', trigger: 'change' }],
+  complainantName: [{ required: true, message: '请输入投诉人姓名', trigger: 'blur' }],
+  storeName: [{ required: true, message: '请输入涉事门店', trigger: 'blur' }],
+  complaintContent: [{ required: true, message: '请输入客诉内容', trigger: 'blur' }]
+};
+
+const resetAddCptForm = () => {
+  addCptForm.value = {
+    source: '',
+    complainantName: '',
+    complainantPhone: '',
+    storeName: '',
+    transferTime: '',
+    complaintContent: '',
+    remark: ''
+  };
+  addCptFormRef.value?.resetFields();
+};
+
+const handleAddComplaint = () => {
+  resetAddCptForm();
+  addCptDialog.visible = true;
+};
+
+const closeAddCptDialog = () => {
+  addCptDialog.visible = false;
+  resetAddCptForm();
+};
+
+const submitAddComplaint = () => {
+  addCptFormRef.value?.validate(async (valid: boolean) => {
+    if (!valid) return;
+    addCptDialog.submitting = true;
+    try {
+      await addComplaint(addCptForm.value);
+      ElMessage.success('新增客诉成功');
+      addCptDialog.submitting = false;
+      addCptDialog.visible = false;
+      await getComplaintList();
+      await loadStats();
+    } catch (e: any) {
+      addCptDialog.submitting = false;
+      ElMessage.error(e?.msg || '新增客诉失败');
+    }
+  });
+};
+
+// ============ 文件上传 ============
+const ATTACH_EXTS = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'jpg', 'jpeg', 'png'];
+const ATTACH_MAX_SIZE = 50 * 1024 * 1024;
+const caseFileList = ref<UploadUserFile[]>([]);
+const cptFileList = ref<UploadUserFile[]>([]);
+
+const customUpload = async (options: any) => {
+  try {
+    const res: any = await uploadFile(options.file);
+    if (res && res.code === 200 && res.data) {
+      options.file.url = res.data.url;
+      options.onSuccess(res, options.file);
+    } else {
+      options.onError(new Error(res?.msg || '上传失败'));
+    }
+  } catch (err: any) {
+    options.onError(err);
+  }
+};
+
+const beforeAttachmentUpload = (file: any) => {
+  const ext = (file.name.split('.').pop() || '').toLowerCase();
+  if (!ATTACH_EXTS.includes(ext)) {
+    ElMessage.warning(`不支持 .${ext},仅允许: ${ATTACH_EXTS.join('/')}`);
+    return false;
+  }
+  if (file.size > ATTACH_MAX_SIZE) {
+    ElMessage.warning(`${file.name} 超过 50MB 上限`);
+    return false;
+  }
+  return true;
+};
+
+const handleAttachmentSuccess = (response: any, file: any) => {
+  if (response && response.code === 200) {
+    ElMessage.success(`${file.name} 上传成功`);
+    const url = response.data?.url || file.url;
+    const name = response.data?.originalName || file.name;
+    if (createCaseDialog.visible) {
+      createCaseForm.value.attachmentUrl = url;
+      createCaseForm.value.attachmentName = name;
+    } else {
+      processCaseForm.value.attachmentUrl = url;
+      processCaseForm.value.attachmentName = name;
+    }
+  } else {
+    ElMessage.error(`${file.name} 上传失败: ${response?.msg || '未知错误'}`);
+  }
+};
+
+const handleCptAttachmentSuccess = (response: any, file: any) => {
+  if (response && response.code === 200) {
+    ElMessage.success(`${file.name} 上传成功`);
+    const url = response.data?.url || file.url;
+    const name = response.data?.originalName || file.name;
+    processCptForm.value.processAttachmentUrl = url;
+    processCptForm.value.processAttachmentName = name;
+  } else {
+    ElMessage.error(`${file.name} 上传失败: ${response?.msg || '未知错误'}`);
+  }
+};
+
+const handleUploadError = (error: any) => {
+  console.error('文件上传失败', error);
+  ElMessage.error(`文件上传失败: ${error?.message || '网络异常'}`);
+};
+
+const handleAttachmentExceed = () => {
+  ElMessage.warning('最多只能上传 1 个附件,如需更换请先删除已有附件');
+};
+
+const handleAttachmentRemove = () => {
+  if (createCaseDialog.visible) {
+    createCaseForm.value.attachmentUrl = '';
+    createCaseForm.value.attachmentName = '';
+  } else {
+    processCaseForm.value.attachmentUrl = '';
+    processCaseForm.value.attachmentName = '';
+  }
+};
+
+const handleCptAttachmentRemove = () => {
+  processCptForm.value.processAttachmentUrl = '';
+  processCptForm.value.processAttachmentName = '';
+};
+
+// ============ 工具 ============
+const getRiskLevelTag = (level: string): 'primary' | 'success' | 'warning' | 'info' | 'danger' => {
+  const map: Record<string, 'primary' | 'success' | 'warning' | 'info' | 'danger'> = {
+    高风险: 'danger',
+    中风险: 'warning',
+    低风险: 'info'
+  };
+  return map[level] || 'info';
+};
+
+const formatDateTime = (val: any): string => {
+  if (!val) return '';
+  const d = new Date(val);
+  if (isNaN(d.getTime())) return String(val);
+  const pad = (n: number) => String(n).padStart(2, '0');
+  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
+};
+
+// ============ 生命周期 ============
+onMounted(async () => {
+  await loadDicts();
+  await Promise.all([loadStats(), getInspectionList(), getCaseList(), getComplaintList()]);
+});
+</script>
+
+<style scoped lang="scss">
+.supervision-page {
+  padding: 16px;
+  height: 100%;
+  overflow-y: auto;
+  box-sizing: border-box;
+}
+.page-header {
+  margin-bottom: 16px;
+  .page-title {
+    font-size: 20px;
+    font-weight: 600;
+    margin: 0;
+    color: #303133;
+  }
+  .page-desc {
+    font-size: 13px;
+    color: #909399;
+    margin: 4px 0 0;
+  }
+}
+.mb-2 { margin-bottom: 16px; }
+.mr-1 { margin-right: 8px; }
+.section-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  flex-wrap: wrap;
+}
+.section-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+}
+.filter-bar {
+  margin-bottom: 16px;
+}
+.stat-card {
+  border-radius: 8px;
+  :deep(.el-card__body) { padding: 20px; }
+}
+.stat-card-content {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.stat-info { flex: 1; min-width: 0; }
+.stat-title {
+  font-size: 13px;
+  color: #909399;
+  margin-bottom: 8px;
+  white-space: nowrap;
+}
+.stat-value {
+  font-size: 26px;
+  font-weight: 700;
+  color: #303133;
+  line-height: 1.2;
+  margin-bottom: 8px;
+  .unit {
+    font-size: 14px;
+    font-weight: 400;
+    margin-left: 2px;
+    color: #606266;
+  }
+}
+.stat-extra {
+  font-size: 12px;
+  color: #909399;
+  display: flex;
+  align-items: center;
+  gap: 2px;
+}
+.stat-extra-up { color: #67c23a; }
+.stat-extra-warning {
+  color: #e6a23c;
+  font-weight: 600;
+}
+.stat-icon {
+  width: 48px;
+  height: 48px;
+  border-radius: 8px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 24px;
+  color: #fff;
+}
+.stat-icon-blue { background: #ecf5ff; color: #409EFF; }
+.stat-icon-red { background: #fef0f0; color: #f56c6c; }
+.stat-icon-orange { background: #fdf6ec; color: #E6A23C; }
+.stat-icon-green { background: #f0f9eb; color: #67C23A; }
+.readonly-text {
+  color: #606266;
+  font-size: 14px;
+}
+.muted { color: #909399; font-size: 12px; }
+.dialog-footer { text-align: right; }
+.el-upload__tip {
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.5;
+  margin-top: 4px;
+}
+.timeline-node {
+  .timeline-title {
+    font-size: 14px;
+    font-weight: 600;
+    color: #303133;
+    margin-bottom: 4px;
+  }
+  .timeline-meta {
+    font-size: 12px;
+    color: #909399;
+    margin-bottom: 6px;
+  }
+  .timeline-opinion {
+    font-size: 13px;
+    color: #606266;
+    line-height: 1.6;
+    white-space: pre-wrap;
+    word-break: break-all;
+    margin-bottom: 6px;
+  }
+  .timeline-attach {
+    font-size: 12px;
+  }
+}
+</style>