Просмотр исходного кода

feat(admin): 健康报告三合一 - 统一报告列表/审核/代传为单页带Tab

E2E Test Bot 1 месяц назад
Родитель
Сommit
5579d329ea

+ 5 - 5
cfc-web/src/router/index.js

@@ -511,20 +511,20 @@ const routes = [
       {
         path: 'health-reports',
         name: 'HealthReports',
-        component: () => import('@/views/nutritionist/HealthReports.vue'),
+        component: () => import('@/views/admin/UnifiedHealthReports.vue'),
         meta: { title: '健康报告', perm: 'health:reports' }
       },
       {
         path: 'report-management',
         name: 'ReportManagement',
-        component: () => import('@/views/admin/ReportManagement.vue'),
-        meta: { title: '报告管理', perm: 'report:management' }
+        redirect: '/health-reports',
+        meta: { title: '报告管理(已合并)', hidden: true }
       },
       {
         path: 'health-report-audit',
         name: 'HealthReportAudit',
-        component: () => import('@/views/health/HealthReportAudit.vue'),
-        meta: { title: '报告草稿审核', perm: 'health:reports' }
+        redirect: '/health-reports',
+        meta: { title: '报告审核(已合并)', hidden: true }
       },
       {
         path: 'health-indicators',

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

@@ -230,8 +230,6 @@ export default {
         { 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-files', perm: 'report:management' },
-            { path: '/health-report-audit', label: '报告审核', icon: 'el-icon-document-checked', 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' },
             { path: '/emotion-alert', label: '情绪告警', icon: 'el-icon-warning', perm: 'health:checkins' },

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

@@ -0,0 +1,516 @@
+<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>
+        </div>
+
+        <el-table :data="reportList" v-loading="listLoading" border stripe :max-height="tableHeight" style="margin-top:16px">
+          <el-table-column prop="id" label="ID" width="70" />
+          <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 />
+          <el-table-column label="报告对象" width="120">
+            <template slot-scope="{ row }">{{ row.subjectName || row.childName || row.userName || '-' }}</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>
+          </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" />
+          <el-table-column label="状态" width="90">
+            <template slot-scope="{ row }">
+              <el-tag size="small">{{ row.status || '已确认' }}</el-tag>
+            </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>
+              <el-button v-if="row.id" size="mini" type="text" @click="triggerAnalysis(row)">分析</el-button>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="100" fixed="right">
+            <template slot-scope="{ row }">
+              <el-button size="mini" type="text" @click="viewReportDetail(row)">详情</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+        <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="audit">
+        <el-table :data="auditList" v-loading="auditLoading" border stripe :max-height="tableHeight">
+          <el-table-column prop="id" label="草稿ID" width="80" />
+          <el-table-column label="上传用户" width="140">
+            <template slot-scope="{ row }">{{ row.userName || '用户' + row.userId }}</template>
+          </el-table-column>
+          <el-table-column prop="reportType" label="报告类型" width="120" />
+          <el-table-column prop="originalFilename" label="文件名" min-width="200" show-overflow-tooltip />
+          <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>
+          </el-table-column>
+          <el-table-column label="操作" width="240">
+            <template slot-scope="{ row }">
+              <el-button size="mini" type="primary" @click="viewAuditDetail(row)">查看详情</el-button>
+              <el-button size="mini" type="success" @click="handleAuditConfirm(row)">确认</el-button>
+              <el-button size="mini" type="danger" @click="handleAuditDiscard(row)">丢弃</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+        <el-pagination v-if="auditTotal > auditSize" @current-change="onAuditPageChange" :current-page="auditPage" :page-size="auditSize" :total="auditTotal" layout="total, prev, pager, next" class="pagination-wrap" />
+      </el-tab-pane>
+
+      <!-- Tab 3: 代传报告 -->
+      <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>
+          <el-table :data="proxyReportList" border stripe v-loading="proxyListLoading" :max-height="tableHeight">
+            <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-table>
+          <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-dialog title="审核详情" :visible.sync="auditDetailVisible" width="800px">
+      <div v-if="auditDetail">
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="草稿ID">{{ auditDetail.id }}</el-descriptions-item>
+          <el-descriptions-item label="上传用户">{{ auditDetail.userName || '用户' + auditDetail.userId }}</el-descriptions-item>
+          <el-descriptions-item label="报告类型">{{ auditDetail.reportType }}</el-descriptions-item>
+          <el-descriptions-item label="文件名">{{ auditDetail.originalFilename }}</el-descriptions-item>
+          <el-descriptions-item label="上传时间">{{ formatTime(auditDetail.createdAt) }}</el-descriptions-item>
+          <el-descriptions-item label="过期时间">{{ formatTime(auditDetail.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="auditTab">
+            <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-table>
+            </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-table>
+            </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-table>
+            </el-tab-pane>
+          </el-tabs>
+        </div>
+        <div v-else><el-alert title="无法解析负载数据" type="warning" :closable="false" /></div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+import { getFamilyList, getFamilyMemberList } from '@/api/admin'
+import { proxyParseReport, proxyConfirmReport, listHealthReports, getHealthReportDetail } from '@/api/admin'
+import { searchUsers } from '@/api/admin'
+
+export default {
+  name: 'UnifiedHealthReports',
+  data() {
+    return {
+      activeTab: 'list',
+
+      // 报告列表
+      reportList: [],
+      listLoading: false,
+      listPage: 1,
+      listSize: 20,
+      listTotal: 0,
+      query: { type: '', keyword: '' },
+      dateRange: [],
+
+      // 报告审核
+      auditList: [],
+      auditLoading: false,
+      auditPage: 1,
+      auditSize: 20,
+      auditTotal: 0,
+      auditDetailVisible: false,
+      auditDetail: null,
+      auditTab: 'indicators',
+
+      // 代传报告
+      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
+    },
+    parsedPayload() {
+      if (!this.auditDetail || !this.auditDetail.payloadJson) return null
+      try {
+        var p = JSON.parse(this.auditDetail.payloadJson)
+        return typeof p === 'string' ? JSON.parse(p) : p
+      } catch { return null }
+    },
+    scoreFields() {
+      if (!this.parsedPayload) return []
+      var 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' }
+      ]
+    }
+  },
+  mounted() {
+    var tab = this.$route.query.tab
+    if (tab && ['list', 'audit', 'proxy'].indexOf(tab) >= 0) {
+      this.activeTab = tab
+    }
+    this.loadReportList()
+    this.loadAuditList()
+    this.loadFamilies()
+    this.loadProxyReports()
+  },
+  methods: {
+    // ===== 报告列表 =====
+    async loadReportList() {
+      this.listLoading = true
+      try {
+        var params = { page: this.listPage, size: this.listSize, 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 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))
+    },
+
+    // ===== 报告审核 =====
+    async loadAuditList() {
+      this.auditLoading = true
+      try {
+        var baseURL = process.env.VUE_APP_BASE_API || ''
+        var token = localStorage.getItem('token')
+        var axios_ = this.$axios || (await import('axios')).default
+        var res = await axios_.post(baseURL + '/api/health/report/audit/pending', { page: this.auditPage, size: this.auditSize }, {
+          headers: { Authorization: 'Bearer ' + token }
+        })
+        var records = res.data && res.data.data ? (res.data.data.records || res.data.data) : []
+        this.auditList = records
+        this.auditTotal = res.data && res.data.data ? (res.data.data.total || records.length) : records.length
+        await this.resolveUserNames(records, 'userId')
+      } catch (e) {
+        this.$message.error('加载审核列表失败')
+      } finally {
+        this.auditLoading = false
+      }
+    },
+    onAuditPageChange(val) { this.auditPage = val; this.loadAuditList() },
+    viewAuditDetail(row) {
+      this.auditDetail = row
+      this.auditTab = 'indicators'
+      this.auditDetailVisible = true
+    },
+    async handleAuditConfirm(row) {
+      try {
+        await this.$confirm('确认草稿 #' + row.id + '?确认后将生成正式报告。', '提示', { type: 'warning' })
+        var baseURL = process.env.VUE_APP_BASE_API || ''
+        var token = localStorage.getItem('token')
+        var 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.loadAuditList()
+      } catch (e) { if (e !== 'cancel') this.$message.error('确认失败') }
+    },
+    async handleAuditDiscard(row) {
+      try {
+        await this.$confirm('确认丢弃草稿 #' + row.id + '?丢弃后不可恢复。', '警告', { type: 'warning' })
+        var baseURL = process.env.VUE_APP_BASE_API || ''
+        var token = localStorage.getItem('token')
+        var 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.loadAuditList()
+      } catch (e) { if (e !== 'cancel') this.$message.error('操作失败') }
+    },
+    resolveUserNames(items, idField) {
+      if (!items || items.length === 0) return Promise.resolve()
+      var ids = []; var nameMap = {}
+      items.forEach(function(item) { if (item[idField] && ids.indexOf(item[idField]) === -1) ids.push(item[idField]) })
+      return Promise.all(ids.map(function(id) {
+        return searchUsers({ keyword: String(id) }).then(function(res) {
+          nameMap[id] = (res.data && res.data.length > 0) ? (res.data[0].nickname || res.data[0].realName || '用户' + id) : '用户' + id
+        }).catch(function() { nameMap[id] = '用户' + id })
+      })).then(function() {
+        items.forEach(function(item) { item.userName = nameMap[item[idField]] || '用户' + item[idField] })
+      })
+    },
+
+    // ===== 代传报告 =====
+    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; }
+</style>