|
@@ -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>
|