HealthReportAudit.vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. <template>
  2. <div class="health-report-audit admin-page">
  3. <div class="header admin-page-header">
  4. <h2 class="admin-page-title">报告草稿审核</h2>
  5. </div>
  6. <el-table :data="list" v-loading="loading" border stripe :max-height="tableHeight">
  7. <el-table-column prop="id" label="草稿ID" width="80" />
  8. <el-table-column label="上传用户" width="120">
  9. <template slot-scope="{ row }">{{ row.userId }}</template>
  10. </el-table-column>
  11. <el-table-column prop="reportType" label="报告类型" width="120" />
  12. <el-table-column prop="originalFilename" label="文件名" min-width="200" show-overflow-tooltip />
  13. <el-table-column label="状态" width="100">
  14. <template slot-scope="{ row }">
  15. <el-tag type="warning">待审核</el-tag>
  16. </template>
  17. </el-table-column>
  18. <el-table-column label="上传时间" width="160">
  19. <template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template>
  20. </el-table-column>
  21. <el-table-column label="过期时间" width="160">
  22. <template slot-scope="{ row }">{{ formatTime(row.expireAt) }}</template>
  23. </el-table-column>
  24. <el-table-column label="操作" width="240">
  25. <template slot-scope="{ row }">
  26. <el-button size="mini" type="primary" @click="viewDetail(row)">查看详情</el-button>
  27. <el-button size="mini" type="success" @click="handleConfirm(row)">确认</el-button>
  28. <el-button size="mini" type="danger" @click="handleDiscard(row)">丢弃</el-button>
  29. </template>
  30. </el-table-column>
  31. </el-table>
  32. <el-pagination
  33. v-if="total > size"
  34. @current-change="onPageChange"
  35. :current-page="page"
  36. :page-size="size"
  37. :total="total"
  38. layout="total, prev, pager, next"
  39. class="pagination-wrap"
  40. />
  41. <!-- 详情弹窗 -->
  42. <el-dialog title="草稿详情" :visible.sync="detailVisible" width="800px">
  43. <div v-if="detail">
  44. <el-descriptions :column="2" border>
  45. <el-descriptions-item label="草稿ID">{{ detail.id }}</el-descriptions-item>
  46. <el-descriptions-item label="上传用户ID">{{ detail.userId }}</el-descriptions-item>
  47. <el-descriptions-item label="报告类型">{{ detail.reportType }}</el-descriptions-item>
  48. <el-descriptions-item label="文件名">{{ detail.originalFilename }}</el-descriptions-item>
  49. <el-descriptions-item label="上传时间">{{ formatTime(detail.createdAt) }}</el-descriptions-item>
  50. <el-descriptions-item label="过期时间">{{ formatTime(detail.expireAt) }}</el-descriptions-item>
  51. </el-descriptions>
  52. <el-divider content-position="left">解析数据</el-divider>
  53. <div v-if="parsedPayload">
  54. <el-row :gutter="20" style="margin-bottom: 16px">
  55. <el-col :span="6" v-for="(val, key) in scoreFields" :key="key">
  56. <div class="score-card" :style="{ borderColor: scoreColor(val.value) }">
  57. <div class="score-label">{{ val.label }}</div>
  58. <div class="score-value" :style="{ color: scoreColor(val.value) }">
  59. {{ val.value ?? '-' }}
  60. </div>
  61. </div>
  62. </el-col>
  63. </el-row>
  64. <el-tabs v-model="activeTab">
  65. <el-tab-pane label="指标" name="indicators">
  66. <el-table :data="parsedPayload.indicators || []" border size="small" :max-height="tableHeight">
  67. <el-table-column prop="category" label="类别" width="100" />
  68. <el-table-column prop="indicatorName" label="指标名称" width="160" />
  69. <el-table-column prop="indicatorValue" label="检测值" width="100" />
  70. <el-table-column prop="unit" label="单位" width="80" />
  71. <el-table-column prop="refRange" label="参考范围" width="140" />
  72. <el-table-column prop="status" label="状态" width="80">
  73. <template slot-scope="{ row }">
  74. <el-tag :type="row.status === '异常' ? 'danger' : 'success'" size="small">
  75. {{ row.status || '正常' }}
  76. </el-tag>
  77. </template>
  78. </el-table-column>
  79. </el-table>
  80. </el-tab-pane>
  81. <el-tab-pane label="菌群" name="gutFlora">
  82. <el-table :data="parsedPayload.gutFlora || []" border size="small" :max-height="tableHeight">
  83. <el-table-column prop="bacteriaName" label="菌种" width="160" />
  84. <el-table-column prop="bacteriaValue" label="检测值" width="100" />
  85. <el-table-column prop="normalRange" label="正常范围" width="140" />
  86. <el-table-column prop="populationLevel" label="水平" width="80" />
  87. <el-table-column prop="level" label="级别" width="80" />
  88. </el-table>
  89. </el-tab-pane>
  90. <el-tab-pane label="疾病风险" name="diseaseRisks">
  91. <el-table :data="parsedPayload.diseaseRisks || []" border size="small" :max-height="tableHeight">
  92. <el-table-column prop="diseaseName" label="疾病" width="200" />
  93. <el-table-column prop="riskValue" label="风险值" width="100" />
  94. <el-table-column prop="riskLevel" label="风险等级" width="100" />
  95. </el-table>
  96. </el-tab-pane>
  97. </el-tabs>
  98. </div>
  99. <div v-else>
  100. <el-alert title="无法解析负载数据" type="warning" :closable="false" />
  101. </div>
  102. </div>
  103. </el-dialog>
  104. </div>
  105. </template>
  106. <script>
  107. export default {
  108. name: 'HealthReportAudit',
  109. data() {
  110. return {
  111. list: [],
  112. loading: false,
  113. page: 1,
  114. size: 20,
  115. total: 0,
  116. detailVisible: false,
  117. detail: null,
  118. activeTab: 'indicators'
  119. }
  120. },
  121. computed: {
  122. tableHeight() {
  123. return window.innerHeight - 280
  124. },
  125. parsedPayload() {
  126. if (!this.detail || !this.detail.payloadJson) return null
  127. try {
  128. const p = JSON.parse(this.detail.payloadJson)
  129. return typeof p === 'string' ? JSON.parse(p) : p
  130. } catch { return null }
  131. },
  132. scoreFields() {
  133. if (!this.parsedPayload) return []
  134. const p = this.parsedPayload
  135. return [
  136. { label: '综合评分', value: p.overallScore, key: 'overallScore' },
  137. { label: '肠道健康', value: p.gutHealthScore, key: 'gutHealthScore' },
  138. { label: '慢病风险', value: p.chronicDiseaseScore, key: 'chronicDiseaseScore' },
  139. { label: '营养评分', value: p.nutritionScore, key: 'nutritionScore' }
  140. ]
  141. }
  142. },
  143. created() { this.loadList() },
  144. methods: {
  145. async loadList() {
  146. this.loading = true
  147. try {
  148. const baseURL = process.env.VUE_APP_BASE_API || ''
  149. const token = localStorage.getItem('token')
  150. const axios = this.$axios || (await import('axios')).default
  151. const res = await axios.post(baseURL + '/api/health/report/audit/pending', {
  152. page: this.page, size: this.size
  153. }, {
  154. headers: { Authorization: 'Bearer ' + token }
  155. })
  156. const records = res.data?.data || []
  157. this.list = records
  158. this.total = records.length
  159. } catch (e) {
  160. this.$message?.error?.('加载草稿列表失败')
  161. } finally {
  162. this.loading = false
  163. }
  164. },
  165. onPageChange(val) {
  166. this.page = val
  167. this.loadList()
  168. },
  169. viewDetail(row) {
  170. this.detail = row
  171. this.activeTab = 'indicators'
  172. this.detailVisible = true
  173. },
  174. async handleConfirm(row) {
  175. try {
  176. await this.$confirm(`确认草稿 #${row.id}?确认后将生成正式报告。`, '提示', { type: 'warning' })
  177. const baseURL = process.env.VUE_APP_BASE_API || ''
  178. const token = localStorage.getItem('token')
  179. const axios = this.$axios || (await import('axios')).default
  180. await axios.post(baseURL + '/api/health/report/audit/confirm', {
  181. draftId: row.id
  182. }, {
  183. headers: { Authorization: 'Bearer ' + token }
  184. })
  185. this.$message?.success?.('审核确认完成')
  186. this.loadList()
  187. } catch (e) {
  188. if (e !== 'cancel') this.$message?.error?.('确认失败')
  189. }
  190. },
  191. async handleDiscard(row) {
  192. try {
  193. await this.$confirm(`确认丢弃草稿 #${row.id}?丢弃后不可恢复。`, '警告', { type: 'warning' })
  194. const baseURL = process.env.VUE_APP_BASE_API || ''
  195. const token = localStorage.getItem('token')
  196. const axios = this.$axios || (await import('axios')).default
  197. await axios.post(baseURL + '/api/health/report/audit/discard', {
  198. draftId: row.id
  199. }, {
  200. headers: { Authorization: 'Bearer ' + token }
  201. })
  202. this.$message?.success?.('已丢弃')
  203. this.loadList()
  204. } catch (e) {
  205. if (e !== 'cancel') this.$message?.error?.('操作失败')
  206. }
  207. },
  208. formatTime(t) {
  209. if (!t) return '-'
  210. return t.replace ? t.replace('T', ' ').substring(0, 19) : t
  211. },
  212. scoreColor(score) {
  213. if (score == null) return '#909399'
  214. if (score >= 80) return '#67c23a'
  215. if (score >= 60) return '#e6a23c'
  216. return '#f56c6c'
  217. }
  218. }
  219. }
  220. </script>
  221. <style scoped>
  222. .health-report-audit {
  223. padding: 20px;
  224. }
  225. .score-card {
  226. border: 2px solid #ebeef5;
  227. border-radius: 8px;
  228. padding: 12px;
  229. text-align: center;
  230. background: #fafafa;
  231. }
  232. .score-label {
  233. font-size: 13px;
  234. color: #909399;
  235. margin-bottom: 6px;
  236. }
  237. .score-value {
  238. font-size: 24px;
  239. font-weight: bold;
  240. }
  241. .pagination-wrap {
  242. margin-top: 16px;
  243. text-align: right;
  244. }
  245. </style>