Преглед изворни кода

feat(reports): 报告管理模块重组(合并健康报告/代传独立/详情合并/删除报告审核)

Xiaogang Liao пре 1 недеља
родитељ
комит
8135a122af

+ 54 - 33
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -9305,7 +9305,7 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
             log.warn("迁移250失败: {}", e.getMessage());
         }
 
-        // 迁移251: 健康饮食组挂载 报告管理/报告审核/应季食材(《角色权限与菜单归类整合方案》第二部分,幂等)
+        // 迁移251/372: 健康饮食组报告管理模块重组(挂载报告管理/代传报告/应季食材,删除健康报告/报告审核菜单)
         migrateHealthMenuMount();
 
         // 迁移252: 创建用户画像表
@@ -9711,9 +9711,10 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
     }
 
     /**
-     * 迁移251: 健康饮食组挂载 报告管理/报告审核/应季食材(存量库 sys_menu 幂等补齐)
-     * 对应页面 ReportManagement.vue / HealthReportAudit.vue / SeasonalFoods.vue 已开发完成但此前未挂载;
-     * 同时将健康饮食组排序对齐《浠艾福后台-角色权限与菜单归类整合方案》最终菜单树。
+     * 迁移251/372: 健康饮食组 报告管理模块重组(存量库 sys_menu 幂等补齐)
+     * - 迁移251:挂载 报告管理/应季食材 并排序对齐《浠艾福后台-角色权限与菜单归类整合方案》最终菜单树;
+     * - 迁移372:报告管理模块重组——删除已下线 健康报告(/health-reports) 与 报告审核(/report-audit) 菜单,
+     *   新增 代传报告(/report-proxy-upload) 菜单(对应前端 ProxyReportUpload.vue / 路由 /report-proxy-upload)。
      */
     private void migrateHealthMenuMount() {
         try {
@@ -9722,39 +9723,60 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
                 log.warn("迁移251: 未找到健康饮食分组,跳过");
                 return;
             }
-            // 1. 存量菜单排序对齐(健康报告0..五维能量11),先更新防止插入冲突
-            jdbcTemplate.update("UPDATE sys_menu SET sort=0 WHERE parent_id=" + healthGroup + " AND path='/health-reports'");
-            jdbcTemplate.update("UPDATE sys_menu SET sort=3 WHERE parent_id=" + healthGroup + " AND path='/health-indicators'");
-            jdbcTemplate.update("UPDATE sys_menu SET sort=4 WHERE parent_id=" + healthGroup + " AND path='/health-checkins'");
-            jdbcTemplate.update("UPDATE sys_menu SET sort=5 WHERE parent_id=" + healthGroup + " AND path='/emotion-alert'");
-            jdbcTemplate.update("UPDATE sys_menu SET sort=6 WHERE parent_id=" + healthGroup + " AND path='/nutrition-mappings'");
-            jdbcTemplate.update("UPDATE sys_menu SET sort=7 WHERE parent_id=" + healthGroup + " AND path='/foods'");
-            jdbcTemplate.update("UPDATE sys_menu SET sort=8 WHERE parent_id=" + healthGroup + " AND path='/recipes'");
-            jdbcTemplate.update("UPDATE sys_menu SET sort=10 WHERE parent_id=" + healthGroup + " AND path='/nutrition-products'");
-            jdbcTemplate.update("UPDATE sys_menu SET sort=11 WHERE parent_id=" + healthGroup + " AND path='/energy-sandbox'");
-            // 2. 按 path 幂等插入 3 个新页面
-            Long reportManagementId = insertHealthMenu(healthGroup, "报告管理", "/report-management", "el-icon-document", "health:reports", 1);
-            Long reportAuditId = insertHealthMenu(healthGroup, "报告审核", "/report-audit", "el-icon-document-checked", "report:audit", 2);
-            Long seasonalFoodsId = insertHealthMenu(healthGroup, "应季食材", "/seasonal-foods", "el-icon-apple", "diet:foods", 9);
+            // 0. 迁移372: 删除已下线的 健康报告/报告审核 菜单(含角色授权,幂等)
+            deleteHealthMenuByPath(healthGroup, "/health-reports");
+            deleteHealthMenuByPath(healthGroup, "/report-audit");
+            // 1. 存量菜单排序对齐(报告管理0..五维能量10),先更新防止插入冲突
+            jdbcTemplate.update("UPDATE sys_menu SET sort=0 WHERE parent_id=" + healthGroup + " AND path='/report-management'");
+            jdbcTemplate.update("UPDATE sys_menu SET sort=2 WHERE parent_id=" + healthGroup + " AND path='/health-indicators'");
+            jdbcTemplate.update("UPDATE sys_menu SET sort=3 WHERE parent_id=" + healthGroup + " AND path='/health-checkins'");
+            jdbcTemplate.update("UPDATE sys_menu SET sort=4 WHERE parent_id=" + healthGroup + " AND path='/emotion-alert'");
+            jdbcTemplate.update("UPDATE sys_menu SET sort=5 WHERE parent_id=" + healthGroup + " AND path='/nutrition-mappings'");
+            jdbcTemplate.update("UPDATE sys_menu SET sort=6 WHERE parent_id=" + healthGroup + " AND path='/foods'");
+            jdbcTemplate.update("UPDATE sys_menu SET sort=7 WHERE parent_id=" + healthGroup + " AND path='/recipes'");
+            jdbcTemplate.update("UPDATE sys_menu SET sort=8 WHERE parent_id=" + healthGroup + " AND path='/seasonal-foods'");
+            jdbcTemplate.update("UPDATE sys_menu SET sort=9 WHERE parent_id=" + healthGroup + " AND path='/nutrition-products'");
+            jdbcTemplate.update("UPDATE sys_menu SET sort=10 WHERE parent_id=" + healthGroup + " AND path='/energy-sandbox'");
+            // 2. 按 path 幂等插入页面菜单
+            Long reportManagementId = insertHealthMenu(healthGroup, "报告管理", "/report-management", "el-icon-document", "health:reports", 0);
+            Long proxyUploadId = insertHealthMenu(healthGroup, "代传报告", "/report-proxy-upload", "el-icon-upload", "health:reports", 1);
+            Long seasonalFoodsId = insertHealthMenu(healthGroup, "应季食材", "/seasonal-foods", "el-icon-apple", "diet:foods", 8);
             // 3. 条件授权:仅当已配置过角色授权(sys_role_menu 非空)时补授权,
             //    避免破坏"所有角色均未授权 → 返回全量可见树"的兼容逻辑(SysMenuService.userMenus)
             Integer grantCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM sys_role_menu", Integer.class);
             if (grantCount != null && grantCount > 0) {
                 String[] roles = {"admin", "butler", "nutritionist"};
-                Long[] menuIds = {reportManagementId, reportAuditId, seasonalFoodsId};
+                Long[] menuIds = {reportManagementId, proxyUploadId, seasonalFoodsId};
                 for (String role : roles) {
                     for (Long menuId : menuIds) {
                         grantMenuToRole(role, menuId);
                     }
                 }
-                log.info("迁移251: 已为 admin/butler/nutritionist 补授权新健康菜单");
+                log.info("迁移251/372: 已为 admin/butler/nutritionist 补授权健康菜单");
             }
-            log.info("迁移251: 健康饮食组挂载 报告管理/报告审核/应季食材完成");
+            log.info("迁移251/372: 健康饮食组菜单重组完成(报告管理/代传报告/应季食材)");
         } catch (Exception e) {
             log.warn("迁移251失败: {}", e.getMessage());
         }
     }
 
+    /**
+     * 按 path 删除健康饮食叶子菜单并清理角色授权(幂等)
+     */
+    private void deleteHealthMenuByPath(Long parentId, String path) {
+        try {
+            Long menuId = queryMenuId("SELECT id FROM sys_menu WHERE parent_id=" + parentId + " AND path='" + path + "'");
+            if (menuId == null) {
+                return;
+            }
+            jdbcTemplate.update("DELETE FROM sys_role_menu WHERE menu_id=" + menuId);
+            jdbcTemplate.update("DELETE FROM sys_menu WHERE id=" + menuId);
+            log.info("迁移372: 已删除下线健康菜单「{}」", path);
+        } catch (Exception e) {
+            log.warn("迁移372: 删除菜单「{}」失败: {}", path, e.getMessage());
+        }
+    }
+
     /**
      * 按 path 幂等插入健康饮食叶子菜单,返回菜单 id(已存在返回原 id)
      */
@@ -9872,18 +9894,17 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
                 {"activity", "activityRegReview", "", "活动报名审核", "/activity-registration-review", "el-icon-document-checked", "activity:registration", "1"},
                 // 健康饮食(health:*,能量规则/行为/周期性服务已移入系统配置·健康配置,LangGraph已移入报告解析系统;报告管理/报告审核/应季食材按方案挂载)
                 {"", "health", "健康饮食", "", "", "el-icon-first-aid-kit", "health:*", "8"},
-                {"health", "healthReports", "", "健康报告", "/health-reports", "el-icon-document", "health:reports", "0"},
-                {"health", "reportManagement", "", "报告管理", "/report-management", "el-icon-document", "health:reports", "1"},
-                {"health", "reportAudit", "", "报告审核", "/report-audit", "el-icon-document-checked", "report:audit", "2"},
-                {"health", "healthIndicators", "", "健康指标", "/health-indicators", "el-icon-data-line", "health:indicators", "3"},
-                {"health", "healthCheckins", "", "健康打卡", "/health-checkins", "el-icon-s-order", "health:checkins", "4"},
-                {"health", "emotionAlert", "", "情绪告警", "/emotion-alert", "el-icon-warning", "health:checkins", "5"},
-                {"health", "nutritionMappings", "", "营养素映射", "/nutrition-mappings", "el-icon-connection", "health:mappings", "6"},
-                {"health", "foods", "", "食材管理", "/foods", "el-icon-apple", "diet:foods", "7"},
-                {"health", "recipes", "", "食谱管理", "/recipes", "el-icon-dish", "diet:recipes", "8"},
-                {"health", "seasonalFoods", "", "应季食材", "/seasonal-foods", "el-icon-apple", "diet:foods", "9"},
-                {"health", "nutritionProducts", "", "营养产品", "/nutrition-products", "el-icon-first-aid-kit", "health:products", "10"},
-                {"health", "energySandbox", "", "五维能量", "/energy-sandbox", "el-icon-data-line", "health:energy", "11"},
+                {"health", "reportManagement", "", "报告管理", "/report-management", "el-icon-document", "health:reports", "0"},
+                {"health", "proxyReportUpload", "", "代传报告", "/report-proxy-upload", "el-icon-upload", "health:reports", "1"},
+                {"health", "healthIndicators", "", "健康指标", "/health-indicators", "el-icon-data-line", "health:indicators", "2"},
+                {"health", "healthCheckins", "", "健康打卡", "/health-checkins", "el-icon-s-order", "health:checkins", "3"},
+                {"health", "emotionAlert", "", "情绪告警", "/emotion-alert", "el-icon-warning", "health:checkins", "4"},
+                {"health", "nutritionMappings", "", "营养素映射", "/nutrition-mappings", "el-icon-connection", "health:mappings", "5"},
+                {"health", "foods", "", "食材管理", "/foods", "el-icon-apple", "diet:foods", "6"},
+                {"health", "recipes", "", "食谱管理", "/recipes", "el-icon-dish", "diet:recipes", "7"},
+                {"health", "seasonalFoods", "", "应季食材", "/seasonal-foods", "el-icon-apple", "diet:foods", "8"},
+                {"health", "nutritionProducts", "", "营养产品", "/nutrition-products", "el-icon-first-aid-kit", "health:products", "9"},
+                {"health", "energySandbox", "", "五维能量", "/energy-sandbox", "el-icon-data-line", "health:energy", "10"},
                 // 报告解析系统(config:report-parser,LangGraph 管理归入)
                 {"", "reportParser", "报告解析系统", "", "", "el-icon-cpu", "config:report-parser", "9"},
                 {"reportParser", "reportTypes", "", "报告类型管理", "/report-types", "el-icon-collection", "config:report-parser", "0"},

+ 8 - 0
cfc-web/src/api/admin.js

@@ -2078,6 +2078,14 @@ export function listHealthReports(params) {
   })
 }
 
+export function adminListHealthReports(params) {
+  return request({
+    url: '/api/health/report/admin-list',
+    method: 'post',
+    data: params
+  })
+}
+
 export function getHealthReportDetail(reportId) {
   return request({
     url: '/api/health/report/detail',

+ 3 - 1
cfc-web/src/main.js

@@ -16,7 +16,7 @@ import {
 import 'element-ui/lib/theme-chalk/index.css'
 import './styles/admin-tokens.css'
 import './assets/css/sort.css'
-import { formatPrice, formatPriceWithSymbol, formatRate, formatScore } from './utils/format'
+import { formatPrice, formatPriceWithSymbol, formatRate, formatScore, formatDateTime, formatDate } from './utils/format'
 
 Vue.use(Button)
 Vue.use(Table)
@@ -84,6 +84,8 @@ Vue.prototype.formatPrice = formatPrice
 Vue.prototype.formatPriceWithSymbol = formatPriceWithSymbol
 Vue.prototype.formatRate = formatRate
 Vue.prototype.formatScore = formatScore
+Vue.prototype.formatTime = formatDateTime
+Vue.prototype.formatDate = formatDate
 
 // Fix Element UI 2.15.x el-submenu removeTabindex null error
 // https://github.com/ElemeFE/element/issues/21965

+ 11 - 17
cfc-web/src/router/index.js

@@ -605,19 +605,6 @@ const routes = [
         component: () => import('@/views/teacher/GrowthPlans.vue'),
         meta: { title: '成长计划', perm: 'growth:plans' }
       },
-      {
-        path: 'health-reports',
-        name: 'HealthReports',
-        component: () => import('@/views/admin/UnifiedHealthReports.vue'),
-        meta: { title: '健康报告', perm: 'health:reports' }
-      },
-      {
-        path: 'health-reports/:id/edit',
-        name: 'HealthReportEdit',
-        component: () => import('@/views/admin/HealthReportEdit.vue'),
-        meta: { title: '编辑报告', perm: 'health:reports' },
-        props: true
-      },
       {
         path: 'health-indicators',
         name: 'HealthIndicators',
@@ -649,10 +636,17 @@ const routes = [
         meta: { title: '报告管理', perm: 'health:reports' }
       },
       {
-        path: 'report-audit',
-        name: 'HealthReportAudit',
-        component: () => import('@/views/health/HealthReportAudit.vue'),
-        meta: { title: '报告审核', perm: 'report:audit' }
+        path: 'report-management/:id',
+        name: 'HealthReportEdit',
+        component: () => import('@/views/admin/HealthReportEdit.vue'),
+        meta: { title: '报告详情/编辑', perm: 'health:reports' },
+        props: true
+      },
+      {
+        path: 'report-proxy-upload',
+        name: 'ProxyReportUpload',
+        component: () => import('@/views/admin/ProxyReportUpload.vue'),
+        meta: { title: '代传报告', perm: 'health:reports' }
       },
       {
         path: 'report-parse-status',

+ 24 - 0
cfc-web/src/utils/format.js

@@ -39,3 +39,27 @@ export function formatScore (percentile) {
   if (percentile === null || percentile === undefined) return '0.00'
   return (Number(percentile) / 100).toFixed(2)
 }
+
+
+/**
+ * Format ISO datetime string to Chinese style: 'yyyy-MM-dd HH:mm:ss' -> 'yyyy年MM月dd日 HH:mm:ss'
+ * Handles: null/undefined -> '-', already-formatted strings -> idempotent passthrough
+ */
+export function formatDateTime (t) {
+  if (t === null || t === undefined || t === '') return '-'
+  const s = String(t).replace('T', ' ').trim()
+  const m = s.match(/^(\d{4})-(\d{2})-(\d{2})(.*)$/)
+  if (!m) return s
+  const timePart = m[4] ? ' ' + m[4].trim().substring(0, 8) : ''
+  return m[1] + '年' + m[2] + '月' + m[3] + '日' + timePart
+}
+
+/**
+ * Format ISO datetime string to Chinese date only: 'yyyy-MM-dd HH:mm:ss' -> 'yyyy年MM月dd日'
+ */
+export function formatDate (t) {
+  if (t === null || t === undefined || t === '') return '-'
+  const dt = formatDateTime(t)
+  if (dt === '-') return '-'
+  return dt.indexOf(' ') >= 0 ? dt.substring(0, dt.indexOf(' ')) : dt
+}

+ 1 - 1
cfc-web/src/utils/permissions.js

@@ -154,7 +154,7 @@ export const RESIDENT_ROLES = ['butler', 'knowledge_admin', 'teacher', 'nutritio
 const ROLE_HOME_PAGES = {
   butler: '/my-families',
   teacher: '/teacher-dashboard',
-  nutritionist: '/health-reports',
+  nutritionist: '/report-management',
   supplier: '/product-manage',
   activity_manager: '/activities',
   knowledge_admin: '/article-manage',

+ 2 - 3
cfc-web/src/views/Layout.vue

@@ -242,12 +242,11 @@ export default {
           { path: '/activity-registration-review', label: '活动报名审核', icon: 'el-icon-document-checked', perm: 'activity:registration' },
         ]},
 
-        // ===== 7. 健康饮食 (health:*,报告管理/报告审核/应季食材挂载,顺序对齐方案最终菜单树) =====
+        // ===== 7. 健康饮食 (health:*,报告管理/代传报告挂载,顺序对齐方案最终菜单树) =====
         { title: '健康饮食', icon: 'el-icon-first-aid-kit', perm: 'health:*',
           children: [
-            { path: '/health-reports', label: '健康报告', icon: 'el-icon-document', perm: 'health:reports' },
             { path: '/report-management', label: '报告管理', icon: 'el-icon-document', perm: 'health:reports' },
-            { path: '/report-audit', label: '报告审核', icon: 'el-icon-document-checked', perm: 'report:audit' },
+            { path: '/report-proxy-upload', label: '代传报告', icon: 'el-icon-upload', perm: 'health:reports' },
             { path: '/report-parse-status', label: '解析进度', icon: 'el-icon-data-line', perm: 'health:reports' },
             { path: '/health-indicators', label: '健康指标', icon: 'el-icon-data-line', perm: 'health:indicators' },
             { path: '/health-checkins', label: '健康打卡', icon: 'el-icon-s-order', perm: 'health:checkins' },

+ 1 - 1
cfc-web/src/views/admin/HealthReportEdit.vue

@@ -424,7 +424,7 @@ export default {
       })
     },
     goBack() {
-      this.$router.push({ name: 'HealthReports' })
+      this.$router.push({ path: '/report-management' })
     }
   }
 }

+ 300 - 0
cfc-web/src/views/admin/ProxyReportUpload.vue

@@ -0,0 +1,300 @@
+<template>
+  <div class="proxy-report-upload">
+    <el-card>
+      <div slot="header" class="card-header">
+        <span class="card-title">📤 代传报告上传</span>
+      </div>
+      <el-form ref="uploadForm" :model="uploadForm" label-width="100px" class="upload-form">
+        <el-form-item label="选择家庭" prop="familyId" required>
+          <el-select v-model="uploadForm.familyId" placeholder="请选择家庭" filterable style="width:100%" @change="onFamilyChange">
+            <el-option v-for="f in familyList" :key="f.id" :label="f.name" :value="f.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="报告对象" prop="subjectId" required>
+          <el-select v-model="uploadForm.subjectId" placeholder="请选择家庭成员" filterable style="width:100%">
+            <el-option v-for="m in memberList" :key="m.id" :label="m.nickname || m.realName" :value="m.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="报告文件" prop="file" required>
+          <el-upload ref="upload" action="" :auto-upload="false" :limit="1" accept=".pdf" :on-change="handleFileChange" :on-exceed="handleExceed">
+            <el-button size="small" type="primary">选择PDF文件</el-button>
+            <div slot="tip" class="el-upload__tip">仅支持PDF格式,大小不超过50MB</div>
+          </el-upload>
+        </el-form-item>
+        <el-form-item label="代上传备注">
+          <el-input v-model="uploadForm.note" type="textarea" :rows="2" placeholder="选填,备注上传原因" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" :loading="uploading" @click="handleUpload">上传并解析</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- 解析结果预览 -->
+    <el-card v-if="previewResult" style="margin-top:20px">
+      <div slot="header" class="card-header">
+        <span class="card-title">📊 解析结果预览</span>
+        <el-tag :type="previewResult.needBind ? 'warning' : 'success'" size="small">
+          {{ previewResult.needBind ? '待绑定成员' : '已匹配成员' }}
+        </el-tag>
+      </div>
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="被检测人">{{ previewResult.extractedName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="报告编号">{{ previewResult.extractedReportNumber || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="报告日期">{{ previewResult.extractedReportDate || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="健康总分">{{ previewResult.payload && previewResult.payload.overallScore ? previewResult.payload.overallScore : '-' }}</el-descriptions-item>
+        <el-descriptions-item label="菌群健康分">{{ previewResult.payload && previewResult.payload.gutHealthScore ? previewResult.payload.gutHealthScore : '-' }}</el-descriptions-item>
+        <el-descriptions-item label="肠型">{{ previewResult.payload && previewResult.payload.gutType ? previewResult.payload.gutType : '-' }}</el-descriptions-item>
+      </el-descriptions>
+      <div style="margin-top:20px;text-align:right">
+        <el-button @click="previewResult = null">取消</el-button>
+        <el-button type="primary" :loading="confirming" @click="handleConfirm">确认入库</el-button>
+      </div>
+    </el-card>
+
+    <!-- AI解读结果 -->
+    <el-card v-if="analysisResult" style="margin-top:20px">
+      <div slot="header" class="card-header">
+        <span class="card-title">🤖 AI解读结果</span>
+        <el-tag type="success" size="small">已完成</el-tag>
+      </div>
+      <div v-html="analysisResult" class="analysis-content"></div>
+    </el-card>
+
+    <!-- 代传历史 -->
+    <el-card style="margin-top:20px">
+      <div slot="header" class="card-header">
+        <span class="card-title">📁 代上传历史记录</span>
+      </div>
+      <div class="filter-bar">
+        <el-select v-model="query.type" placeholder="报告类型" clearable style="width:140px">
+          <el-option label="全部" value=""></el-option>
+          <el-option label="菌群检测" value="gut_flora"></el-option>
+          <el-option label="体检报告" value="physical_exam"></el-option>
+          <el-option label="DAN测评" value="dan"></el-option>
+          <el-option label="保单" value="insurance"></el-option>
+          <el-option label="家庭周报" value="fortune"></el-option>
+        </el-select>
+        <el-input v-model="query.keyword" placeholder="搜索标题/名称" style="width:200px" clearable @keyup.enter.native="loadProxyReports"></el-input>
+        <el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width:240px"></el-date-picker>
+        <el-button type="primary" @click="loadProxyReports">查询</el-button>
+      </div>
+
+      <div class="adm-table-wrap">
+        <el-table :data="proxyReportList" border stripe v-loading="proxyListLoading" height="100%">
+          <el-table-column prop="id" label="ID" width="70" />
+          <el-table-column label="报告对象" width="120">
+            <template slot-scope="{ row }">{{ row.subjectName || row.childName || row.personName || row.userName || '-' }}</template>
+          </el-table-column>
+          <el-table-column label="报告类型" width="100">
+            <template slot-scope="{ row }">
+              <el-tag size="small" :type="getTypeTag(row.reportType)">{{ typeLabel(row.reportType) }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="title" label="标题" min-width="140" show-overflow-tooltip />
+          <el-table-column label="健康分" width="80">
+            <template slot-scope="{ row }">{{ row.overallScore == null ? '-' : row.overallScore }}</template>
+          </el-table-column>
+          <el-table-column label="上传时间" width="160">
+            <template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template>
+          </el-table-column>
+          <el-table-column label="操作" width="100" fixed="right">
+            <template slot-scope="{ row }">
+              <el-button size="mini" type="text" @click="viewProxyReport(row)">查看</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+      <el-pagination @size-change="onProxySizeChange" @current-change="onProxyPageChange" :current-page="proxyPage" :page-size="proxySize" :total="proxyTotal" layout="total, prev, pager, next" class="pagination-wrap" />
+    </el-card>
+
+    <!-- 报告详情弹窗 -->
+    <el-dialog title="报告详情" :visible.sync="detailDialogVisible" width="700px" top="5vh">
+      <el-descriptions v-if="detailReport" :column="2" border>
+        <el-descriptions-item label="报告ID">{{ detailReport.id }}</el-descriptions-item>
+        <el-descriptions-item label="报告类型">{{ detailReport.reportType || typeLabel(detailReport.reportType) }}</el-descriptions-item>
+        <el-descriptions-item label="报告编号">{{ detailReport.reportNumber || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="报告日期">{{ detailReport.reportDate || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="健康总分">{{ detailReport.overallScore || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="菌群健康分">{{ detailReport.gutHealthScore || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="肠型">{{ detailReport.gutType || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="肠道年龄">{{ detailReport.gutAge || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="AI解读" :span="2">
+          <div v-html="detailReport.aiAnalysis" class="analysis-content" style="max-height:300px;overflow:auto"></div>
+        </el-descriptions-item>
+      </el-descriptions>
+      <span slot="footer"><el-button @click="detailDialogVisible = false">关闭</el-button></span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getFamilyList, getFamilyMemberList } from '@/api/admin'
+import { proxyParseReport, proxyConfirmReport, adminListHealthReports, getHealthReportDetail } from '@/api/admin'
+
+export default {
+  name: 'ProxyReportUpload',
+  data() {
+    return {
+      uploading: false,
+      confirming: false,
+      proxyListLoading: false,
+      familyList: [],
+      memberList: [],
+      proxyReportList: [],
+      previewResult: null,
+      analysisResult: null,
+      proxyPage: 1,
+      proxySize: 20,
+      proxyTotal: 0,
+      query: { type: '', keyword: '' },
+      dateRange: [],
+      uploadForm: { familyId: null, subjectId: null, file: null, note: '' },
+      detailDialogVisible: false,
+      detailReport: null
+    }
+  },
+  created() {
+    this.loadFamilies()
+    this.loadProxyReports()
+  },
+  methods: {
+    async loadFamilies() {
+      try {
+        var res = await getFamilyList({ page: 1, size: 200 })
+        this.familyList = res.data && res.data.records ? res.data.records : (res.data || [])
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    async onFamilyChange(familyId) {
+      if (!familyId) { this.memberList = []; return }
+      try {
+        var res = await getFamilyMemberList({ familyId: familyId })
+        this.memberList = res.data || []
+        this.uploadForm.subjectId = null
+      } catch (e) {
+        this.memberList = []
+      }
+    },
+    handleFileChange(file) {
+      this.uploadForm.file = file.raw
+    },
+    handleExceed() {
+      this.$message.warning('只能上传一个文件')
+    },
+    async handleUpload() {
+      if (!this.uploadForm.familyId) { this.$message.warning('请选择家庭'); return }
+      if (!this.uploadForm.subjectId) { this.$message.warning('请选择报告对象'); return }
+      if (!this.uploadForm.file) { this.$message.warning('请选择PDF文件'); return }
+      this.uploading = true
+      try {
+        var res = await proxyParseReport({ familyId: this.uploadForm.familyId, subjectId: this.uploadForm.subjectId, file: this.uploadForm.file, note: this.uploadForm.note })
+        if (res.code === 200) {
+          this.previewResult = res.data
+          this.analysisResult = null
+        } else {
+          this.$message.error(res.message || '解析失败')
+        }
+      } catch (e) {
+        this.$message.error('上传失败: ' + (e.message || ''))
+      } finally {
+        this.uploading = false
+      }
+    },
+    async handleConfirm() {
+      if (!this.previewResult || !this.previewResult.draftId) {
+        this.$message.warning('没有可确认的草稿')
+        return
+      }
+      this.confirming = true
+      try {
+        var res = await proxyConfirmReport({ draftId: this.previewResult.draftId, subjectId: this.uploadForm.subjectId })
+        if (res.code === 200) {
+          this.$message.success('报告入库成功')
+          this.previewResult = null
+          this.analysisResult = res.data && res.data.aiAnalysis ? res.data.aiAnalysis : null
+          this.loadProxyReports()
+        } else {
+          this.$message.error(res.message || '确认失败')
+        }
+      } catch (e) {
+        this.$message.error('确认失败: ' + (e.message || ''))
+      } finally {
+        this.confirming = false
+      }
+    },
+    async loadProxyReports() {
+      this.proxyListLoading = true
+      try {
+        var params = {
+          page: this.proxyPage,
+          size: this.proxySize,
+          type: this.query.type,
+          keyword: this.query.keyword
+        }
+        if (this.dateRange && this.dateRange.length === 2) {
+          params.startDate = this.dateRange[0].format ? this.dateRange[0].format('YYYY-MM-DD') : this.dateRange[0]
+          params.endDate = this.dateRange[1].format ? this.dateRange[1].format('YYYY-MM-DD') : this.dateRange[1]
+        }
+        var res = await adminListHealthReports(params)
+        var data = (res && res.data && res.data.data) ? res.data.data : (res && res.data ? res.data : null)
+        if (data) {
+          this.proxyReportList = data.records || []
+          this.proxyTotal = data.total || this.proxyReportList.length
+        } else {
+          this.proxyReportList = []
+          this.proxyTotal = 0
+        }
+      } catch (e) {
+        console.error(e)
+        this.proxyReportList = []
+        this.proxyTotal = 0
+      } finally {
+        this.proxyListLoading = false
+      }
+    },
+    viewProxyReport(row) {
+      var self = this
+      getHealthReportDetail(row.id).then(function(res) {
+        if (res && res.code === 200) {
+          self.detailReport = res.data
+          self.detailDialogVisible = true
+        } else {
+          self.$message.error('加载详情失败')
+        }
+      }).catch(function() {
+        self.$message.error('加载详情失败')
+      })
+    },
+    onProxySizeChange(size) {
+      this.proxySize = size
+      this.proxyPage = 1
+      this.loadProxyReports()
+    },
+    onProxyPageChange(page) {
+      this.proxyPage = page
+      this.loadProxyReports()
+    },
+    typeLabel(type) {
+      var map = { gut_flora: '菌群检测', physical_exam: '体检报告', dan: 'DAN测评', insurance: '保单', fortune: '家庭周报' }
+      return map[type] || type || '-'
+    },
+    getTypeTag(type) {
+      var map = { gut_flora: 'success', physical_exam: 'primary', dan: 'warning', insurance: 'info', fortune: 'danger' }
+      return map[type] || 'info'
+    }
+  }
+}
+</script>
+
+<style scoped>
+.proxy-report-upload { padding: 16px; }
+.card-header { display: flex; justify-content: space-between; align-items: center; }
+.card-title { font-size: 16px; font-weight: bold; }
+.upload-form { max-width: 600px; }
+.filter-bar { display: flex; flex-direction: row; align-items: center; gap: 12px; flex-wrap: wrap; }
+.pagination-wrap { margin-top: 16px; text-align: right; }
+.analysis-content { font-size: 14px; line-height: 1.8; color: #333; }
+.analysis-content :deep(p) { margin: 8px 0; }
+</style>

+ 127 - 110
cfc-web/src/views/admin/ReportManagement.vue

@@ -10,81 +10,73 @@
           <el-option label="保单" value="insurance"></el-option>
           <el-option label="家庭周报" value="fortune"></el-option>
         </el-select>
-        <el-input v-model="query.keyword" placeholder="搜索标题/名称" style="width:200px" @keyup.enter.native="loadData"></el-input>
-        <el-date-picker
-          v-model="dateRange"
-          type="daterange"
-          range-separator="至"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          style="width:240px"
-        ></el-date-picker>
+        <el-input v-model="query.keyword" placeholder="搜索标题/名称" style="width:200px" clearable @keyup.enter.native="loadData"></el-input>
+        <el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width:240px"></el-date-picker>
         <el-button type="primary" @click="loadData">查询</el-button>
         <el-button @click="doSync">同步数据</el-button>
+        <el-button type="danger" :disabled="selectedRows.length === 0" @click="handleBatchArchive">批量归档({{ selectedRows.length }})</el-button>
       </div>
 
-            <div class="adm-table-wrap">
-<el-table :data="tableData" border stripe v-loading="loading" style="margin-top:16px" height="100%">
-        <el-table-column prop="id" label="ID" width="80">
-          <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick('id', $event)">ID{{ sortIcon('id') }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column label="报告类型" width="100">
-          <template slot-scope="scope">
-            <el-tag size="small" :type="getTypeTag(scope.row.reportType)">{{ scope.row.reportTypeName }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="title" label="标题">
-          <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick('title', $event)">标题{{ sortIcon('title') }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="subjectName" label="报告主体" width="120">
-          <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick('subjectName', $event)">报告主体{{ sortIcon('subjectName') }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="reportDate" label="报告日期" width="120">
-          <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick('reportDate', $event)">报告日期{{ sortIcon('reportDate') }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="overallScore" label="综合评分" width="100">
-          <template slot-scope="scope">
-            <span v-if="scope.row.overallScore">{{ scope.row.overallScore }}</span>
-            <span v-else>-</span>
-          </template>
-          <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick('overallScore', $event)">综合评分{{ sortIcon('overallScore') }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column label="状态" width="100">
-          <template slot-scope="scope">
-            <el-tag size="small">{{ scope.row.status }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="采集完整度" width="120">
-          <template slot-scope="scope">
-            <el-tag
-              v-if="scope.row.collectValidation"
-              size="small"
-              :type="collectPass(scope.row) ? 'success' : 'danger'"
-            >{{ collectText(scope.row) }}</el-tag>
-            <span v-else>-</span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="createdAt" label="创建时间" width="160">
-          <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick('createdAt', $event)">创建时间{{ sortIcon('createdAt') }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" width="120">
-          <template slot-scope="scope">
-            <el-button size="mini" type="text" @click="viewDetail(scope.row.id)">详情</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
+      <div class="adm-table-wrap">
+        <el-table :data="tableData" border stripe v-loading="loading" style="margin-top:16px" height="100%"
+                  ref="listTable" @selection-change="handleSelectionChange">
+          <el-table-column type="selection" width="55" :selectable="selectableRow" />
+          <el-table-column prop="id" label="ID" width="80">
+            <template slot="header">
+              <span class="sort-header" @click.stop="onSortClick('id', $event)">ID{{ sortIcon('id') }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="报告类型" width="100">
+            <template slot-scope="scope">
+              <el-tag size="small" :type="getTypeTag(scope.row.reportType)">{{ typeLabel(scope.row.reportType) }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="title" label="标题" min-width="140" show-overflow-tooltip>
+            <template slot="header">
+              <span class="sort-header" @click.stop="onSortClick('title', $event)">标题{{ sortIcon('title') }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="报告主体" width="120">
+            <template slot-scope="scope">{{ scope.row.subjectName || scope.row.childName || scope.row.personName || scope.row.userName || '-' }}</template>
+            <template slot="header">
+              <span class="sort-header" @click.stop="onSortClick('subjectName', $event)">报告主体{{ sortIcon('subjectName') }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="报告日期" width="120">
+            <template slot-scope="scope">{{ formatDate(scope.row.reportDate) }}</template>
+            <template slot="header">
+              <span class="sort-header" @click.stop="onSortClick('reportDate', $event)">报告日期{{ sortIcon('reportDate') }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="综合评分" width="100">
+            <template slot-scope="scope">
+              <el-tag :type="scoreType(scope.row.overallScore)" size="small">{{ scope.row.overallScore == null ? '-' : scope.row.overallScore }}</el-tag>
+            </template>
+            <template slot="header">
+              <span class="sort-header" @click.stop="onSortClick('overallScore', $event)">综合评分{{ sortIcon('overallScore') }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="状态" width="100">
+            <template slot-scope="scope">
+              <el-tag size="small">{{ scope.row.status || '已确认' }}</el-tag>
+            </template>
+            <template slot="header">
+              <span class="sort-header" @click.stop="onSortClick('status', $event)">状态{{ sortIcon('status') }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="创建时间" width="160">
+            <template slot-scope="scope">{{ formatTime(scope.row.createdAt) }}</template>
+            <template slot="header">
+              <span class="sort-header" @click.stop="onSortClick('createdAt', $event)">创建时间{{ sortIcon('createdAt') }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="150" fixed="right">
+            <template slot-scope="scope">
+              <el-button size="mini" type="text" @click.stop="goEdit(scope.row)">编辑/详情</el-button>
+              <el-button size="mini" type="text" @click.stop="archiveOne(scope.row)">归档</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
       </div>
 
       <el-pagination
@@ -103,6 +95,7 @@
 <script>
 import axios from 'axios'
 import SortMixin from '@/mixins/SortMixin'
+import { archiveReport, archiveReportsBatch } from '@/api/admin'
 
 export default {
   name: 'ReportManagement',
@@ -113,6 +106,7 @@ export default {
       sortableColumns: { id: 'ID', title: '标题', subjectName: '报告主体', reportDate: '报告日期', overallScore: '综合评分', createdAt: '创建时间' },
       total: 0,
       loading: false,
+      selectedRows: [],
       query: {
         page: 1,
         size: 20,
@@ -143,15 +137,17 @@ export default {
         params.startDate = self.dateRange[0].format ? self.dateRange[0].format('YYYY-MM-DD') : self.dateRange[0]
         params.endDate = self.dateRange[1].format ? self.dateRange[1].format('YYYY-MM-DD') : self.dateRange[1]
       }
-      axios.post('/api/admin/report-summary/list', params, {
-        headers: { Authorization: 'Bearer ' + localStorage.getItem('token') }
+      var baseURL = process.env.VUE_APP_BASE_API || ''
+      var token = localStorage.getItem('token')
+      axios.post(baseURL + '/api/health/report/admin-list', params, {
+        headers: { Authorization: 'Bearer ' + token }
       }).then(function(res) {
-        if (res.data && res.data.data) {
-          self.tableData = res.data.data.records || []
-          self.total = res.data.data.total || 0
-        }
+        var data = res.data && res.data.data ? res.data.data : (res.data || [])
+        self.tableData = data.records || []
+        self.total = data.total || 0
       }).catch(function(e) {
         console.error(e)
+        self.$message.error('加载报告列表失败')
       }).finally(function() {
         self.loading = false
       })
@@ -167,8 +163,10 @@ export default {
     },
     doSync() {
       var self = this
-      axios.post('/api/admin/report-summary/sync', {}, {
-        headers: { Authorization: 'Bearer ' + localStorage.getItem('token') }
+      var baseURL = process.env.VUE_APP_BASE_API || ''
+      var token = localStorage.getItem('token')
+      axios.post(baseURL + '/api/admin/report-summary/sync', {}, {
+        headers: { Authorization: 'Bearer ' + token }
       }).then(function(res) {
         if (res.data && res.data.code === 200) {
           self.$message.success('同步完成: ' + res.data.message)
@@ -178,44 +176,63 @@ export default {
         self.$message.error('同步失败')
       })
     },
-    viewDetail(id) {
+    goEdit(row) {
+      if (!row || !row.id) return
+      this.$router.push({ path: '/report-management/' + row.id })
+    },
+    selectableRow(row) {
+      return !!(row && row.id)
+    },
+    handleSelectionChange(rows) {
+      this.selectedRows = rows || []
+    },
+    archiveOne(row) {
       var self = this
-      axios.post('/api/report-summary/detail/' + id, {}, {
-        headers: { Authorization: 'Bearer ' + localStorage.getItem('token') }
-      }).then(function(res) {
-        if (res.data && res.data.data) {
-          var d = res.data.data
-          var cv = null
-          try { cv = d.collectValidation ? JSON.parse(d.collectValidation) : null } catch (e) { cv = null }
-          var cvLine = '<p>采集完整度: ' + (cv
-            ? (cv.matched + '/' + cv.expected + (cv.pass ? '(通过)' : '(缺失: ' + ((cv.missing || []).join('、') || '-') + ')'))
-            : '-') + '</p>'
-          self.$alert(
-            '<p>标题: ' + d.title + '</p>' +
-            '<p>类型: ' + d.reportTypeName + '</p>' +
-            '<p>主体: ' + d.subjectName + '</p>' +
-            '<p>日期: ' + d.reportDate + '</p>' +
-            '<p>评分: ' + (d.overallScore || '-') + '</p>' +
-            cvLine +
-            '<p>状态: ' + d.status + '</p>',
-            '报告详情',
-            { dangerouslyUseHTMLString: true }
-          )
-        }
-      })
+      this.$confirm('确定要归档这份报告吗?归档后将在列表中消失(软删除)', '提示', {
+        type: 'warning',
+        confirmButtonText: '归档',
+        cancelButtonText: '取消'
+      }).then(function() {
+        archiveReport(row.id).then(function() {
+          self.$message.success('已归档')
+          self.loadData()
+        }).catch(function() {
+          self.$message.error('归档失败')
+        })
+      }).catch(function() {})
     },
-    parseCollect(row) {
-      if (!row || !row.collectValidation) return null
-      try { return JSON.parse(row.collectValidation) } catch (e) { return null }
+    handleBatchArchive() {
+      var self = this
+      var ids = this.selectedRows.map(function(r) { return r.id })
+      if (ids.length === 0) return
+      this.$confirm('确定要归档选中的 ' + ids.length + ' 份报告吗?归档后将在列表中消失(软删除)', '提示', {
+        type: 'warning',
+        confirmButtonText: '归档',
+        cancelButtonText: '取消'
+      }).then(function() {
+        archiveReportsBatch(ids).then(function(res) {
+          self.$message.success(res.message || ('已归档 ' + ids.length + ' 份报告'))
+          self.selectedRows = []
+          self.$refs.listTable && self.$refs.listTable.clearSelection()
+          self.loadData()
+        }).catch(function() {
+          self.$message.error('批量归档失败')
+        })
+      }).catch(function() {})
     },
-    collectPass(row) { const c = this.parseCollect(row); return !!(c && c.pass === true) },
-    collectText(row) {
-      const c = this.parseCollect(row)
-      return c ? (c.matched + '/' + c.expected) : '-'
+    typeLabel(type) {
+      var map = { gut_flora: '菌群检测', physical_exam: '体检报告', dan: 'DAN测评', insurance: '保单', fortune: '家庭周报' }
+      return map[type] || type || '-'
     },
     getTypeTag(type) {
       var map = { gut_flora: 'success', physical_exam: 'primary', dan: 'warning', insurance: 'info', fortune: 'danger' }
       return map[type] || 'info'
+    },
+    scoreType(score) {
+      if (score == null) return 'info'
+      if (score >= 80) return 'success'
+      if (score >= 60) return 'warning'
+      return 'danger'
     }
   }
 }

+ 0 - 431
cfc-web/src/views/admin/UnifiedHealthReports.vue

@@ -1,431 +0,0 @@
-<template>
-  <div class="unified-reports">
-    <!-- Tabs -->
-    <el-tabs v-model="activeTab" type="border-card" @tab-click="onTabChange">
-      <!-- Tab 1: 报告列表 -->
-      <el-tab-pane label="报告列表" name="list">
-        <div class="filter-bar">
-          <el-select v-model="query.type" placeholder="报告类型" clearable style="width:140px">
-            <el-option label="全部" value=""></el-option>
-            <el-option label="菌群检测" value="gut_flora"></el-option>
-            <el-option label="体检报告" value="physical_exam"></el-option>
-            <el-option label="DAN测评" value="dan"></el-option>
-            <el-option label="保单" value="insurance"></el-option>
-            <el-option label="家庭周报" value="fortune"></el-option>
-          </el-select>
-          <el-input v-model="query.keyword" placeholder="搜索标题/名称" style="width:200px" clearable @keyup.enter.native="loadReportList"></el-input>
-          <el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width:240px"></el-date-picker>
-          <el-button type="primary" @click="loadReportList">查询</el-button>
-          <el-button @click="doSync">同步数据</el-button>
-          <el-button type="danger" :disabled="selectedRows.length === 0" @click="handleBatchDelete">批量删除({{ selectedRows.length }})</el-button>
-        </div>
-
-                <div class="adm-table-wrap">
-<el-table :data="reportList" v-loading="listLoading" border stripe height="100%" style="margin-top:16px"
-          ref="listTable"
-          @selection-change="handleSelectionChange" @row-click="goEdit">
-          <el-table-column type="selection" width="48" :selectable="row => row && row.id ? true : false" />
-          <el-table-column prop="id" label="ID" width="70">
-            <template slot="header">
-              <span class="sort-header" @click.stop="onSortClick('id', $event)">ID{{ sortIcon('id') }}</span>
-            </template>
-          </el-table-column>
-          <el-table-column label="报告类型" width="100">
-            <template slot-scope="{ row }">
-              <el-tag size="small" :type="getTypeTag(row.reportType || row.reportTypeName)">{{ row.reportTypeName || row.reportType }}</el-tag>
-            </template>
-          </el-table-column>
-          <el-table-column prop="title" label="标题" min-width="140" show-overflow-tooltip>
-            <template slot="header">
-              <span class="sort-header" @click.stop="onSortClick('title', $event)">标题{{ sortIcon('title') }}</span>
-            </template>
-          </el-table-column>
-          <el-table-column label="报告对象" width="120">
-            <template slot-scope="{ row }">{{ row.subjectName || row.childName || row.personName || row.userName || '-' }}</template>
-            <template slot="header">
-              <span class="sort-header" @click.stop="onSortClick('subjectName', $event)">报告对象{{ sortIcon('subjectName') }}</span>
-            </template>
-          </el-table-column>
-          <el-table-column label="综合评分" width="90">
-            <template slot-scope="{ row }">
-              <el-tag :type="scoreType(row.overallScore)" size="small">{{ row.overallScore ?? '-' }}</el-tag>
-            </template>
-            <template slot="header">
-              <span class="sort-header" @click.stop="onSortClick('overallScore', $event)">综合评分{{ sortIcon('overallScore') }}</span>
-            </template>
-          </el-table-column>
-          <el-table-column label="肠道" width="70" v-if="false">
-            <template slot-scope="{ row }">{{ row.gutHealthScore || '-' }}</template>
-          </el-table-column>
-          <el-table-column label="营养" width="70" v-if="false">
-            <template slot-scope="{ row }">{{ row.nutritionScore || '-' }}</template>
-          </el-table-column>
-          <el-table-column prop="reportDate" label="报告日期" width="120">
-            <template slot="header">
-              <span class="sort-header" @click.stop="onSortClick('reportDate', $event)">报告日期{{ sortIcon('reportDate') }}</span>
-            </template>
-          </el-table-column>
-          <el-table-column label="状态" width="90">
-            <template slot-scope="{ row }">
-              <el-tag size="small">{{ row.status || '已确认' }}</el-tag>
-            </template>
-            <template slot="header">
-              <span class="sort-header" @click.stop="onSortClick('status', $event)">状态{{ sortIcon('status') }}</span>
-            </template>
-          </el-table-column>
-          <el-table-column label="营养缺乏" width="120">
-            <template slot-scope="{ row }">
-              <el-tag v-if="row.deficiencyCount > 0" type="danger" size="small">{{ row.deficiencyCount }}项缺乏</el-tag>
-              <el-tag v-else type="success" size="small">无缺乏</el-tag>
-            </template>
-          </el-table-column>
-          <el-table-column label="操作" width="120" fixed="right">
-            <template slot-scope="{ row }">
-              <el-button size="mini" type="text" @click.stop="goEdit(row)">编辑</el-button>
-              <el-button size="mini" type="text" @click.stop="handleSingleDelete(row)">删除</el-button>
-            </template>
-          </el-table-column>
-        </el-table>
-        </div>
-        <el-pagination v-if="listTotal > listSize" @current-change="onListPageChange" :current-page="listPage" :page-size="listSize" :total="listTotal" layout="total, prev, pager, next" style="margin-top:16px;text-align:right" />
-      </el-tab-pane>
-
-      <!-- Tab 2: 代传报告 -->
-      <el-tab-pane label="代传报告" name="proxy">
-        <el-form ref="uploadForm" :model="uploadForm" label-width="100px" class="upload-form">
-          <el-form-item label="选择家庭" prop="familyId" required>
-            <el-select v-model="uploadForm.familyId" placeholder="请选择家庭" filterable style="width:100%" @change="onFamilyChange">
-              <el-option v-for="f in familyList" :key="f.id" :label="f.name" :value="f.id" />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="报告对象" prop="subjectId" required>
-            <el-select v-model="uploadForm.subjectId" placeholder="请选择家庭成员" filterable style="width:100%">
-              <el-option v-for="m in memberList" :key="m.id" :label="m.nickname || m.realName" :value="m.id" />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="报告文件" prop="file" required>
-            <el-upload ref="upload" action="" :auto-upload="false" :limit="1" accept=".pdf" :on-change="handleFileChange" :on-exceed="handleExceed">
-              <el-button size="small" type="primary">选择PDF文件</el-button>
-              <div slot="tip" class="el-upload__tip">仅支持PDF格式,大小不超过50MB</div>
-            </el-upload>
-          </el-form-item>
-          <el-form-item label="代上传备注">
-            <el-input v-model="uploadForm.note" type="textarea" :rows="2" placeholder="选填,备注上传原因" />
-          </el-form-item>
-          <el-form-item>
-            <el-button type="primary" :loading="uploading" @click="handleUpload">上传并解析</el-button>
-          </el-form-item>
-        </el-form>
-
-        <!-- 解析结果预览 -->
-        <el-card v-if="previewResult" style="margin-top:20px">
-          <div slot="header" class="card-header">
-            <span class="card-title">📊 解析结果预览</span>
-            <el-tag :type="previewResult.needBind ? 'warning' : 'success'" size="small">
-              {{ previewResult.needBind ? '待绑定成员' : '已匹配成员' }}
-            </el-tag>
-          </div>
-          <el-descriptions :column="2" border>
-            <el-descriptions-item label="被检测人">{{ previewResult.extractedName || '-' }}</el-descriptions-item>
-            <el-descriptions-item label="报告编号">{{ previewResult.extractedReportNumber || '-' }}</el-descriptions-item>
-            <el-descriptions-item label="报告日期">{{ previewResult.extractedReportDate || '-' }}</el-descriptions-item>
-            <el-descriptions-item label="健康总分">{{ previewResult.payload && previewResult.payload.overallScore ? previewResult.payload.overallScore : '-' }}</el-descriptions-item>
-            <el-descriptions-item label="菌群健康分">{{ previewResult.payload && previewResult.payload.gutHealthScore ? previewResult.payload.gutHealthScore : '-' }}</el-descriptions-item>
-            <el-descriptions-item label="肠型">{{ previewResult.payload && previewResult.payload.gutType ? previewResult.payload.gutType : '-' }}</el-descriptions-item>
-          </el-descriptions>
-          <div style="margin-top:20px;text-align:right">
-            <el-button @click="previewResult=null">取消</el-button>
-            <el-button type="primary" :loading="confirming" @click="handleConfirm">确认入库</el-button>
-          </div>
-        </el-card>
-
-        <!-- AI解读结果 -->
-        <el-card v-if="analysisResult" style="margin-top:20px">
-          <div slot="header" class="card-header">
-            <span class="card-title">🤖 AI解读结果</span>
-            <el-tag type="success" size="small">已完成</el-tag>
-          </div>
-          <div v-html="analysisResult" class="analysis-content"></div>
-        </el-card>
-
-        <!-- 代传历史 -->
-        <el-card style="margin-top:20px">
-          <div slot="header" class="card-header">
-            <span class="card-title">📁 代上传历史记录</span>
-          </div>
-                    <div class="adm-table-wrap">
-<el-table :data="proxyReportList" border stripe v-loading="proxyListLoading" height="100%">
-            <el-table-column prop="id" label="ID" width="60" />
-            <el-table-column label="报告对象" width="120">
-              <template slot-scope="{ row }">{{ row.subjectName || row.userName || '-' }}</template>
-            </el-table-column>
-            <el-table-column prop="reportType" label="类型" width="100">
-              <template slot-scope="{ row }">{{ row.reportType === 'gut_flora' ? '菌群检测' : row.reportType === 'physical_exam' ? '体检报告' : row.reportType }}</template>
-            </el-table-column>
-            <el-table-column prop="overallScore" label="健康分" width="80">
-              <template slot-scope="{ row }">{{ row.overallScore || '-' }}</template>
-            </el-table-column>
-            <el-table-column prop="remark" label="备注" show-overflow-tooltip />
-            <el-table-column prop="createdAt" label="上传时间" width="160">
-              <template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template>
-            </el-table-column>
-            <el-table-column label="操作" width="100" fixed="right">
-              <template slot-scope="{ row }">
-                <el-button size="mini" type="text" @click="viewProxyReport(row)">查看</el-button>
-              </template>
-            </el-table-column>
-          <el-pagination @size-change="onProxySizeChange" @current-change="onProxyPageChange" :current-page="proxyPage" :page-size="proxySize" :total="proxyTotal" layout="total, prev, pager, next" class="pagination-wrap" />
-        </el-card>
-      </el-tab-pane>
-    </el-tabs>
-
-    <!-- 报告详情弹窗 -->
-    <el-dialog title="报告详情" :visible.sync="detailDialogVisible" width="700px" top="5vh">
-      <el-descriptions v-if="detailReport" :column="2" border>
-        <el-descriptions-item label="报告ID">{{ detailReport.id }}</el-descriptions-item>
-        <el-descriptions-item label="报告类型">{{ detailReport.reportType || detailReport.reportTypeName }}</el-descriptions-item>
-        <el-descriptions-item label="报告编号">{{ detailReport.reportNumber || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="报告日期">{{ detailReport.reportDate || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="健康总分">{{ detailReport.overallScore || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="菌群健康分">{{ detailReport.gutHealthScore || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="肠型">{{ detailReport.gutType || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="肠道年龄">{{ detailReport.gutAge || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="AI解读" :span="2">
-          <div v-html="detailReport.aiAnalysis" class="analysis-content" style="max-height:300px;overflow:auto"></div>
-        </el-descriptions-item>
-      </el-descriptions>
-      <span slot="footer"><el-button @click="detailDialogVisible = false">关闭</el-button></span>
-    </el-dialog>
-  </el-table>
-  </div>
-
-</div></template>
-
-<script>
-import axios from 'axios'
-import { getFamilyList, getFamilyMemberList } from '@/api/admin'
-import { proxyParseReport, proxyConfirmReport, listHealthReports, getHealthReportDetail, archiveReportsBatch, archiveReport } from '@/api/admin'
-import SortMixin from '@/mixins/SortMixin'
-
-export default {
-  name: 'UnifiedHealthReports',
-  mixins: [SortMixin],
-  data() {
-    return {
-      activeTab: 'list',
-      sortableColumns: { id: 'ID', title: '标题', subjectName: '报告对象', overallScore: '综合评分', reportDate: '报告日期', status: '状态' },
-
-      // 报告列表
-      reportList: [],
-      listLoading: false,
-      listPage: 1,
-      listSize: 20,
-      listTotal: 0,
-      query: { type: '', keyword: '' },
-      dateRange: [],
-      selectedRows: [],
-
-      // 代传报告
-      uploading: false,
-      confirming: false,
-      proxyListLoading: false,
-      familyList: [],
-      memberList: [],
-      proxyReportList: [],
-      previewResult: null,
-      analysisResult: null,
-      proxyPage: 1,
-      proxySize: 20,
-      proxyTotal: 0,
-      uploadForm: { familyId: null, subjectId: null, file: null, note: '' },
-
-      // 详情
-      detailDialogVisible: false,
-      detailReport: null
-    }
-  },
-  computed: {
-    tableHeight() {
-      return window.innerHeight - 280
-    }
-  },
-  mounted() {
-    var tab = this.$route.query.tab
-    if (tab && ['list', 'proxy'].indexOf(tab) >= 0) {
-      this.activeTab = tab
-    }
-    this.loadReportList()
-    this.loadFamilies()
-    this.loadProxyReports()
-  },
-  methods: {
-    // ===== 报告列表 =====
-    loadList() {
-      this.loadReportList()
-    },
-    async loadReportList() {
-      this.listLoading = true
-      try {
-        var params = { page: this.listPage, size: this.listSize, type: this.query.type, keyword: this.query.keyword }
-        if (this.sortState.length > 0) params.sort = this.sortState
-        if (this.dateRange && this.dateRange.length === 2) {
-          params.startDate = this.dateRange[0].format ? this.dateRange[0].format('YYYY-MM-DD') : this.dateRange[0]
-          params.endDate = this.dateRange[1].format ? this.dateRange[1].format('YYYY-MM-DD') : this.dateRange[1]
-        }
-        var baseURL = process.env.VUE_APP_BASE_API || ''
-        var token = localStorage.getItem('token')
-        var res = await axios.post(baseURL + '/api/health/report/admin-list', params, {
-          headers: { Authorization: 'Bearer ' + token }
-        })
-        var data = res.data && res.data.data ? res.data.data : (res.data || [])
-        this.reportList = (data.records || []).map(function(r) { r.deficiencyCount = r.deficiencyCount || 0; return r })
-        this.listTotal = data.total || 0
-      } catch (e) {
-        this.$message.error('加载报告列表失败')
-      } finally {
-        this.listLoading = false
-      }
-    },
-    onListPageChange(val) { this.listPage = val; this.loadReportList() },
-    doSync() {
-      var baseURL = process.env.VUE_APP_BASE_API || ''
-      var token = localStorage.getItem('token')
-      axios.post(baseURL + '/api/admin/report-summary/sync', {}, { headers: { Authorization: 'Bearer ' + token } })
-        .then(function(res) { if (res.data && res.data.code === 200) { this.$message.success('同步完成'); this.loadReportList() } }.bind(this))
-        .catch(function() { this.$message.error('同步失败') }.bind(this))
-    },
-    viewReportDetail(row) {
-      var self = this
-      var baseURL = process.env.VUE_APP_BASE_API || ''
-      var token = localStorage.getItem('token')
-      axios.post(baseURL + '/api/health/report/detail', { reportId: row.id }, { headers: { Authorization: 'Bearer ' + token } })
-        .then(function(res) { if (res.data && res.data.data) { self.detailReport = res.data.data; self.detailDialogVisible = true } })
-        .catch(function() { self.$message.error('加载详情失败') })
-    },
-    triggerAnalysis(row) {
-      var baseURL = process.env.VUE_APP_BASE_API || ''
-      var token = localStorage.getItem('token')
-      axios.post(baseURL + '/api/health/report/analysis', { reportId: row.id }, { headers: { Authorization: 'Bearer ' + token } })
-        .then(function(res) { this.$message.success('分析完成') }.bind(this))
-        .catch(function() { this.$message.error('分析失败') }.bind(this))
-    },
-
-    handleSelectionChange(rows) {
-      this.selectedRows = rows
-    },
-
-    goEdit(row, column) {
-      if (!row || !row.id) return
-      // 点击复选框列不触发跳转
-      if (column && (column.type === 'selection' || column.type === 'index')) return
-      this.$router.push({ name: 'HealthReportEdit', params: { id: row.id } })
-    },
-
-    handleSingleDelete(row) {
-      var self = this
-      this.$confirm('确定要删除这份报告吗?删除后将在列表中消失(软删除)', '提示', {
-        type: 'warning',
-        confirmButtonText: '删除',
-        cancelButtonText: '取消'
-      }).then(function() {
-        archiveReport(row.id).then(function() {
-          self.$message.success('已删除')
-          self.loadReportList()
-        }).catch(function() {})
-      }).catch(function() {})
-    },
-
-    handleBatchDelete() {
-      var self = this
-      var ids = this.selectedRows.map(function(r) { return r.id })
-      if (ids.length === 0) return
-      this.$confirm('确定要删除选中的 ' + ids.length + ' 份报告吗?删除后将在列表中消失(软删除)', '提示', {
-        type: 'warning',
-        confirmButtonText: '删除',
-        cancelButtonText: '取消'
-      }).then(function() {
-        archiveReportsBatch(ids).then(function(res) {
-          self.$message.success(res.message || ('已删除 ' + ids.length + ' 份报告'))
-          self.selectedRows = []
-          self.$refs.listTable && self.$refs.listTable.clearSelection()
-          self.loadReportList()
-        }).catch(function() {})
-      }).catch(function() {})
-    },
-
-    // ===== 代传报告 =====
-    async loadFamilies() {
-      try { var res = await getFamilyList({ page: 1, size: 200 }); this.familyList = res.data && res.data.records ? res.data.records : (res.data || []) } catch (e) { console.error(e) }
-    },
-    async onFamilyChange(familyId) {
-      if (!familyId) { this.memberList = []; return }
-      try { var res = await getFamilyMemberList({ familyId }); this.memberList = res.data || []; this.uploadForm.subjectId = null } catch (e) { this.memberList = [] }
-    },
-    handleFileChange(file) { this.uploadForm.file = file.raw },
-    handleExceed() { this.$message.warning('只能上传一个文件') },
-    async handleUpload() {
-      if (!this.uploadForm.familyId) { this.$message.warning('请选择家庭'); return }
-      if (!this.uploadForm.subjectId) { this.$message.warning('请选择报告对象'); return }
-      if (!this.uploadForm.file) { this.$message.warning('请选择PDF文件'); return }
-      this.uploading = true
-      try {
-        var res = await proxyParseReport({ familyId: this.uploadForm.familyId, subjectId: this.uploadForm.subjectId, file: this.uploadForm.file, note: this.uploadForm.note })
-        if (res.code === 200) { this.previewResult = res.data; this.analysisResult = null }
-        else { this.$message.error(res.message || '解析失败') }
-      } catch (e) { this.$message.error('上传失败: ' + (e.message || '')) } finally { this.uploading = false }
-    },
-    async handleConfirm() {
-      if (!this.previewResult || !this.previewResult.draftId) { this.$message.warning('没有可确认的草稿'); return }
-      this.confirming = true
-      try {
-        var res = await proxyConfirmReport({ draftId: this.previewResult.draftId, subjectId: this.uploadForm.subjectId })
-        if (res.code === 200) { this.$message.success('报告入库成功'); this.previewResult = null; this.analysisResult = res.data && res.data.aiAnalysis ? res.data.aiAnalysis : null; this.loadProxyReports() }
-        else { this.$message.error(res.message || '确认失败') }
-      } catch (e) { this.$message.error('确认失败: ' + (e.message || '')) } finally { this.confirming = false }
-    },
-    async loadProxyReports() {
-      this.proxyListLoading = true
-      try {
-        var res = await listHealthReports({ page: this.proxyPage, size: this.proxySize })
-        if (res.code === 200) { this.proxyReportList = res.data && res.data.records ? res.data.records : (res.data || []); this.proxyTotal = res.data && res.data.total ? res.data.total : this.proxyReportList.length }
-      } catch (e) { console.error(e) } finally { this.proxyListLoading = false }
-    },
-    viewProxyReport(row) {
-      var self = this
-      getHealthReportDetail(row.id).then(function(res) { if (res.code === 200) { self.detailReport = res.data; self.detailDialogVisible = true } }).catch(function() { self.$message.error('加载详情失败') })
-    },
-    onProxySizeChange(size) { this.proxySize = size; this.loadProxyReports() },
-    onProxyPageChange(page) { this.proxyPage = page; this.loadProxyReports() },
-
-    // ===== 通用 =====
-    onTabChange() { },
-    formatTime(t) { if (!t) return '-'; return t.replace ? t.replace('T', ' ').substring(0, 19) : t },
-    getTypeTag(type) { var map = { gut_flora: 'success', physical_exam: 'primary', dan: 'warning', insurance: 'info', fortune: 'danger' }; return map[type] || 'info' },
-    scoreType(score) { if (score == null) return 'info'; if (score >= 80) return 'success'; if (score >= 60) return 'warning'; return 'danger' },
-    scoreColor(score) { if (score == null) return '#909399'; if (score >= 80) return '#67c23a'; if (score >= 60) return '#e6a23c'; return '#f56c6c' }
-  }
-}
-</script>
-
-<style scoped>
-.unified-reports { padding: 16px; }
-.filter-bar { display: flex; flex-direction: row; align-items: center; gap: 12px; flex-wrap: wrap; }
-.upload-form { max-width: 600px; }
-.card-header { display: flex; justify-content: space-between; align-items: center; }
-.card-title { font-size: 16px; font-weight: bold; }
-.analysis-content { font-size: 14px; line-height: 1.8; color: #333; }
-.analysis-content :deep(p) { margin: 8px 0; }
-.pagination-wrap { margin-top: 16px; text-align: right; }
-.score-card { border: 2px solid #ebeef5; border-radius: 8px; padding: 12px; text-align: center; background: #fafafa; }
-.score-label { font-size: 13px; color: #909399; margin-bottom: 6px; }
-.score-value { font-size: 24px; font-weight: bold; }
-.sort-header {
-  cursor: pointer;
-  user-select: none;
-  font-weight: 500;
-  font-size: 12px;
-}
-.sort-header:hover {
-  color: #409eff;
-}
-</style>

+ 0 - 317
cfc-web/src/views/health/HealthReportAudit.vue

@@ -1,317 +0,0 @@
-<template>
-  <div class="health-report-audit admin-page">
-    <div class="header admin-page-header">
-      <h2 class="admin-page-title">报告草稿审核</h2>
-    </div>
-
-        <div class="adm-table-wrap">
-<el-table :data="list" v-loading="loading" border stripe height="100%">
-      <el-table-column prop="id" label="草稿ID" width="80">
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('id', $event)">草稿ID<span v-if="sortIndex('id') !== null" class="sort-index" :class="sortClass('id') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('id') }}</span>{{ sortIcon('id') }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column prop="reportType" label="报告类型" width="120">
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('reportType', $event)">报告类型<span v-if="sortIndex('reportType') !== null" class="sort-index" :class="sortClass('reportType') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('reportType') }}</span>{{ sortIcon('reportType') }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column prop="originalFilename" label="文件名" min-width="200" show-overflow-tooltip>
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('originalFilename', $event)">文件名<span v-if="sortIndex('originalFilename') !== null" class="sort-index" :class="sortClass('originalFilename') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('originalFilename') }}</span>{{ sortIcon('originalFilename') }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="状态" width="100">
-        <template slot-scope="{ row }">
-          <el-tag type="warning">待审核</el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="上传时间" width="160">
-        <template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template>
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('createdAt', $event)">上传时间<span v-if="sortIndex('createdAt') !== null" class="sort-index" :class="sortClass('createdAt') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('createdAt') }}</span>{{ sortIcon('createdAt') }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="过期时间" width="160">
-        <template slot-scope="{ row }">{{ formatTime(row.expireAt) }}</template>
-      </el-table-column>
-      <el-table-column label="操作" width="240">
-        <template slot-scope="{ row }">
-          <el-button size="mini" type="primary" @click="viewDetail(row)">查看详情</el-button>
-          <el-button size="mini" type="success" @click="handleConfirm(row)">确认</el-button>
-          <el-button size="mini" type="danger" @click="handleDiscard(row)">丢弃</el-button>
-        </template>
-      </el-table-column>
-    
-    
-    </el-table>
-    </div>
-
-    <el-pagination
-      v-if="total > size"
-      @current-change="onPageChange"
-      :current-page="page"
-      :page-size="size"
-      :total="total"
-      layout="total, prev, pager, next"
-      class="pagination-wrap"
-    />
-
-    <!-- 详情弹窗 -->
-    <el-dialog title="草稿详情" :visible.sync="detailVisible" width="800px">
-      <div v-if="detail">
-        <el-descriptions :column="2" border>
-          <el-descriptions-item label="草稿ID">{{ detail.id }}</el-descriptions-item>
-          <el-descriptions-item label="上传用户">{{ detail.userName || '用户' + detail.userId }}</el-descriptions-item>
-          <el-descriptions-item label="报告类型">{{ detail.reportType }}</el-descriptions-item>
-          <el-descriptions-item label="文件名">{{ detail.originalFilename }}</el-descriptions-item>
-          <el-descriptions-item label="上传时间">{{ formatTime(detail.createdAt) }}</el-descriptions-item>
-          <el-descriptions-item label="过期时间">{{ formatTime(detail.expireAt) }}</el-descriptions-item>
-        </el-descriptions>
-
-        <el-divider content-position="left">解析数据</el-divider>
-        <div v-if="parsedPayload">
-          <el-row :gutter="20" style="margin-bottom: 16px">
-            <el-col :span="6" v-for="(val, key) in scoreFields" :key="key">
-              <div class="score-card" :style="{ borderColor: scoreColor(val.value) }">
-                <div class="score-label">{{ val.label }}</div>
-                <div class="score-value" :style="{ color: scoreColor(val.value) }">
-                  {{ val.value ?? '-' }}
-                </div>
-              </div>
-            </el-col>
-          </el-row>
-
-          <el-tabs v-model="activeTab">
-            <el-tab-pane label="指标" name="indicators">
-              <el-table :data="parsedPayload.indicators || []" border size="small" :max-height="tableHeight">
-                <el-table-column prop="category" label="类别" width="100" />
-                <el-table-column prop="indicatorName" label="指标名称" width="160" />
-                <el-table-column prop="indicatorValue" label="检测值" width="100" />
-                <el-table-column prop="unit" label="单位" width="80" />
-                <el-table-column prop="refRange" label="参考范围" width="140" />
-                <el-table-column prop="status" label="状态" width="80">
-                  <template slot-scope="{ row }">
-                    <el-tag :type="row.status === '异常' ? 'danger' : 'success'" size="small">
-                      {{ row.status || '正常' }}
-                    </el-tag>
-                  </template>
-                </el-table-column>
-            </el-tab-pane>
-            <el-tab-pane label="菌群" name="gutFlora">
-              <el-table :data="parsedPayload.gutFlora || []" border size="small" :max-height="tableHeight">
-                <el-table-column prop="bacteriaName" label="菌种" width="160" />
-                <el-table-column prop="bacteriaValue" label="检测值" width="100" />
-                <el-table-column prop="normalRange" label="正常范围" width="140" />
-                <el-table-column prop="populationLevel" label="水平" width="80" />
-                <el-table-column prop="level" label="级别" width="80" />
-            </el-tab-pane>
-            <el-tab-pane label="疾病风险" name="diseaseRisks">
-              <el-table :data="parsedPayload.diseaseRisks || []" border size="small" :max-height="tableHeight">
-                <el-table-column prop="diseaseName" label="疾病" width="200" />
-                <el-table-column prop="riskValue" label="风险值" width="100" />
-                <el-table-column prop="riskLevel" label="风险等级" width="100" />
-            </el-tab-pane>
-          </el-tabs>
-        
-        </el-table>
-        
-        </el-table>
-        
-        </el-table>
-        </div>
-        <div v-else>
-          <el-alert title="无法解析负载数据" type="warning" :closable="false" />
-        </div>
-      </div>
-    </el-dialog>
-  </div>
-</template>
-
-<script>
-import SortMixin from '@/mixins/SortMixin'
-import { searchUsers } from '@/api/admin'
-
-export default {
-  name: 'HealthReportAudit',
-  mixins: [SortMixin],
-  data() {
-    return {
-      list: [],
-      loading: false,
-      page: 1,
-      size: 20,
-      total: 0,
-      detailVisible: false,
-      detail: null,
-      activeTab: 'indicators',
-      sortableColumns: { createdAt: '上传时间', id: '草稿ID', reportType: '报告类型', originalFilename: '文件名' },
-    }
-  },
-  computed: {
-    tableHeight() {
-      return window.innerHeight - 280
-    },
-    parsedPayload() {
-      if (!this.detail || !this.detail.payloadJson) return null
-      try {
-        const p = JSON.parse(this.detail.payloadJson)
-        return typeof p === 'string' ? JSON.parse(p) : p
-      } catch { return null }
-    },
-    scoreFields() {
-      if (!this.parsedPayload) return []
-      const p = this.parsedPayload
-      return [
-        { label: '综合评分', value: p.overallScore, key: 'overallScore' },
-        { label: '肠道健康', value: p.gutHealthScore, key: 'gutHealthScore' },
-        { label: '慢病风险', value: p.chronicDiseaseScore, key: 'chronicDiseaseScore' },
-        { label: '营养评分', value: p.nutritionScore, key: 'nutritionScore' }
-      ]
-    }
-  },
-  created() { this.loadList() },
-  methods: {
-    async loadList() {
-      this.loading = true
-      try {
-        const baseURL = process.env.VUE_APP_BASE_API || ''
-        const token = localStorage.getItem('token')
-        const axios = this.$axios || (await import('axios')).default
-        var params = { page: this.page, size: this.size }
-        if (this.sortState.length > 0) params.sort = this.sortState
-        const res = await axios.post(baseURL + '/api/health/report/audit/pending', params, {
-          headers: { Authorization: 'Bearer ' + token }
-        })
-        const records = res.data?.data || []
-        this.list = records
-        this.total = records.length
-        await this.resolveUserNames(records, 'userId')
-      } catch (e) {
-        this.$message?.error?.('加载草稿列表失败')
-      } finally {
-        this.loading = false
-      }
-    },
-    onPageChange(val) {
-      this.page = val
-      this.loadList()
-    },
-    viewDetail(row) {
-      this.detail = row
-      this.activeTab = 'indicators'
-      this.detailVisible = true
-    },
-    resolveUserNames(items, idField) {
-      if (!items || items.length === 0) return Promise.resolve()
-      var ids = []
-      items.forEach(function(item) {
-        if (item[idField] && ids.indexOf(item[idField]) === -1) ids.push(item[idField])
-      })
-      var nameMap = {}
-      return Promise.all(ids.map(function(id) {
-        return searchUsers({ keyword: String(id) })
-          .then(function(res) {
-            if (res.data && res.data.length > 0) {
-              var u = res.data[0]
-              nameMap[id] = u.nickname || u.realName || '用户' + id
-            } else {
-              nameMap[id] = '用户' + id
-            }
-          })
-          .catch(function() {
-            nameMap[id] = '用户' + id
-          })
-      })).then(function() {
-        items.forEach(function(item) {
-          item.userName = nameMap[item[idField]] || '用户' + item[idField]
-        })
-      })
-    },
-    async handleConfirm(row) {
-      try {
-        await this.$confirm(`确认草稿 #${row.id}?确认后将生成正式报告。`, '提示', { type: 'warning' })
-        const baseURL = process.env.VUE_APP_BASE_API || ''
-        const token = localStorage.getItem('token')
-        const axios = this.$axios || (await import('axios')).default
-        await axios.post(baseURL + '/api/health/report/audit/confirm', {
-          draftId: row.id
-        }, {
-          headers: { Authorization: 'Bearer ' + token }
-        })
-        this.$message?.success?.('审核确认完成')
-        this.loadList()
-      } catch (e) {
-        if (e !== 'cancel') this.$message?.error?.('确认失败')
-      }
-    },
-    async handleDiscard(row) {
-      try {
-        await this.$confirm(`确认丢弃草稿 #${row.id}?丢弃后不可恢复。`, '警告', { type: 'warning' })
-        const baseURL = process.env.VUE_APP_BASE_API || ''
-        const token = localStorage.getItem('token')
-        const axios = this.$axios || (await import('axios')).default
-        await axios.post(baseURL + '/api/health/report/audit/discard', {
-          draftId: row.id
-        }, {
-          headers: { Authorization: 'Bearer ' + token }
-        })
-        this.$message?.success?.('已丢弃')
-        this.loadList()
-      } catch (e) {
-        if (e !== 'cancel') this.$message?.error?.('操作失败')
-      }
-    },
-    formatTime(t) {
-      if (!t) return '-'
-      return t.replace ? t.replace('T', ' ').substring(0, 19) : t
-    },
-    scoreColor(score) {
-      if (score == null) return '#909399'
-      if (score >= 80) return '#67c23a'
-      if (score >= 60) return '#e6a23c'
-      return '#f56c6c'
-    }
-  }
-}
-</script>
-
-<style scoped>
-.health-report-audit {
-  padding: 20px;
-}
-.score-card {
-  border: 2px solid #ebeef5;
-  border-radius: 8px;
-  padding: 12px;
-  text-align: center;
-  background: #fafafa;
-}
-.score-label {
-  font-size: 13px;
-  color: #909399;
-  margin-bottom: 6px;
-}
-.score-value {
-  font-size: 24px;
-  font-weight: bold;
-}
-.pagination-wrap {
-  margin-top: 16px;
-  text-align: right;
-}
-.sort-header {
-  cursor: pointer;
-  user-select: none;
-  font-weight: 500;
-  font-size: 12px;
-}
-.sort-header:hover {
-  color: #409eff;
-}
-.sort-tag {
-  margin-left: 8px;
-  cursor: pointer;
-  white-space: nowrap;
-}
-</style>