ReportManagement.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. <template>
  2. <div class="report-management">
  3. <el-card>
  4. <div class="filter-bar">
  5. <el-select v-model="query.type" placeholder="报告类型" clearable style="width:140px">
  6. <el-option label="全部" value=""></el-option>
  7. <el-option label="菌群检测" value="gut_flora"></el-option>
  8. <el-option label="体检报告" value="physical_exam"></el-option>
  9. <el-option label="DAN测评" value="dan"></el-option>
  10. <el-option label="保单" value="insurance"></el-option>
  11. <el-option label="家庭周报" value="fortune"></el-option>
  12. </el-select>
  13. <el-input v-model="query.keyword" placeholder="搜索标题/名称" style="width:200px" clearable @keyup.enter.native="loadData"></el-input>
  14. <el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width:240px"></el-date-picker>
  15. <el-button type="primary" @click="loadData">查询</el-button>
  16. <el-button @click="doSync">同步数据</el-button>
  17. <el-button type="danger" :disabled="selectedRows.length === 0" @click="handleBatchArchive">批量归档({{ selectedRows.length }})</el-button>
  18. </div>
  19. <div class="adm-table-wrap">
  20. <el-table :data="tableData" border stripe v-loading="loading" style="margin-top:16px" height="100%"
  21. ref="listTable" @selection-change="handleSelectionChange">
  22. <el-table-column type="selection" width="55" :selectable="selectableRow" />
  23. <el-table-column prop="id" label="ID" width="80">
  24. <template slot="header">
  25. <span class="sort-header" @click.stop="onSortClick('id', $event)">ID{{ sortIcon('id') }}</span>
  26. </template>
  27. </el-table-column>
  28. <el-table-column label="报告类型" width="100">
  29. <template slot-scope="scope">
  30. <el-tag size="small" :type="getTypeTag(scope.row.reportType)">{{ typeLabel(scope.row.reportType) }}</el-tag>
  31. </template>
  32. </el-table-column>
  33. <el-table-column prop="title" label="标题" min-width="140" show-overflow-tooltip>
  34. <template slot="header">
  35. <span class="sort-header" @click.stop="onSortClick('title', $event)">标题{{ sortIcon('title') }}</span>
  36. </template>
  37. </el-table-column>
  38. <el-table-column label="报告主体" width="120">
  39. <template slot-scope="scope">{{ scope.row.subjectName || scope.row.childName || scope.row.personName || scope.row.userName || '-' }}</template>
  40. <template slot="header">
  41. <span class="sort-header" @click.stop="onSortClick('subjectName', $event)">报告主体{{ sortIcon('subjectName') }}</span>
  42. </template>
  43. </el-table-column>
  44. <el-table-column label="报告日期" width="120">
  45. <template slot-scope="scope">{{ formatDate(scope.row.reportDate) }}</template>
  46. <template slot="header">
  47. <span class="sort-header" @click.stop="onSortClick('reportDate', $event)">报告日期{{ sortIcon('reportDate') }}</span>
  48. </template>
  49. </el-table-column>
  50. <el-table-column label="综合评分" width="100">
  51. <template slot-scope="scope">
  52. <el-tag :type="scoreType(scope.row.overallScore)" size="small">{{ scope.row.overallScore == null ? '-' : scope.row.overallScore }}</el-tag>
  53. </template>
  54. <template slot="header">
  55. <span class="sort-header" @click.stop="onSortClick('overallScore', $event)">综合评分{{ sortIcon('overallScore') }}</span>
  56. </template>
  57. </el-table-column>
  58. <el-table-column label="状态" width="100">
  59. <template slot-scope="scope">
  60. <el-tag size="small">{{ scope.row.status || '已确认' }}</el-tag>
  61. </template>
  62. <template slot="header">
  63. <span class="sort-header" @click.stop="onSortClick('status', $event)">状态{{ sortIcon('status') }}</span>
  64. </template>
  65. </el-table-column>
  66. <el-table-column label="创建时间" width="160">
  67. <template slot-scope="scope">{{ formatTime(scope.row.createdAt) }}</template>
  68. <template slot="header">
  69. <span class="sort-header" @click.stop="onSortClick('createdAt', $event)">创建时间{{ sortIcon('createdAt') }}</span>
  70. </template>
  71. </el-table-column>
  72. <el-table-column label="操作" width="150" fixed="right">
  73. <template slot-scope="scope">
  74. <el-button size="mini" type="text" @click.stop="goEdit(scope.row)">编辑/详情</el-button>
  75. <el-button size="mini" type="text" @click.stop="archiveOne(scope.row)">归档</el-button>
  76. </template>
  77. </el-table-column>
  78. </el-table>
  79. </div>
  80. <el-pagination
  81. @size-change="handleSizeChange"
  82. @current-change="handleCurrentChange"
  83. :current-page="query.page"
  84. :page-size="query.size"
  85. :total="total"
  86. layout="total, prev, pager, next"
  87. style="margin-top:16px"
  88. ></el-pagination>
  89. </el-card>
  90. </div>
  91. </template>
  92. <script>
  93. import axios from 'axios'
  94. import SortMixin from '@/mixins/SortMixin'
  95. import { archiveReport, archiveReportsBatch } from '@/api/admin'
  96. export default {
  97. name: 'ReportManagement',
  98. mixins: [SortMixin],
  99. data() {
  100. return {
  101. tableData: [],
  102. sortableColumns: { id: 'ID', title: '标题', subjectName: '报告主体', reportDate: '报告日期', overallScore: '综合评分', createdAt: '创建时间' },
  103. total: 0,
  104. loading: false,
  105. selectedRows: [],
  106. query: {
  107. page: 1,
  108. size: 20,
  109. type: '',
  110. keyword: ''
  111. },
  112. dateRange: []
  113. }
  114. },
  115. created() {
  116. this.loadData()
  117. },
  118. methods: {
  119. loadList() {
  120. this.loadData()
  121. },
  122. loadData() {
  123. var self = this
  124. self.loading = true
  125. var params = {
  126. page: self.query.page,
  127. size: self.query.size,
  128. type: self.query.type,
  129. keyword: self.query.keyword
  130. }
  131. if (self.sortState.length > 0) params.sort = self.sortState
  132. if (self.dateRange && self.dateRange.length === 2) {
  133. params.startDate = self.dateRange[0].format ? self.dateRange[0].format('YYYY-MM-DD') : self.dateRange[0]
  134. params.endDate = self.dateRange[1].format ? self.dateRange[1].format('YYYY-MM-DD') : self.dateRange[1]
  135. }
  136. var baseURL = process.env.VUE_APP_BASE_API || ''
  137. var token = localStorage.getItem('token')
  138. axios.post(baseURL + '/api/health/report/admin-list', params, {
  139. headers: { Authorization: 'Bearer ' + token }
  140. }).then(function(res) {
  141. var data = res.data && res.data.data ? res.data.data : (res.data || [])
  142. self.tableData = data.records || []
  143. self.total = data.total || 0
  144. }).catch(function(e) {
  145. console.error(e)
  146. self.$message.error('加载报告列表失败')
  147. }).finally(function() {
  148. self.loading = false
  149. })
  150. },
  151. handleSizeChange(size) {
  152. this.query.size = size
  153. this.query.page = 1
  154. this.loadData()
  155. },
  156. handleCurrentChange(page) {
  157. this.query.page = page
  158. this.loadData()
  159. },
  160. doSync() {
  161. var self = this
  162. var baseURL = process.env.VUE_APP_BASE_API || ''
  163. var token = localStorage.getItem('token')
  164. axios.post(baseURL + '/api/admin/report-summary/sync', {}, {
  165. headers: { Authorization: 'Bearer ' + token }
  166. }).then(function(res) {
  167. if (res.data && res.data.code === 200) {
  168. self.$message.success('同步完成: ' + res.data.message)
  169. self.loadData()
  170. }
  171. }).catch(function(e) {
  172. self.$message.error('同步失败')
  173. })
  174. },
  175. goEdit(row) {
  176. if (!row || !row.id) return
  177. this.$router.push({ path: '/report-management/' + row.id })
  178. },
  179. selectableRow(row) {
  180. return !!(row && row.id)
  181. },
  182. handleSelectionChange(rows) {
  183. this.selectedRows = rows || []
  184. },
  185. archiveOne(row) {
  186. var self = this
  187. this.$confirm('确定要归档这份报告吗?归档后将在列表中消失(软删除)', '提示', {
  188. type: 'warning',
  189. confirmButtonText: '归档',
  190. cancelButtonText: '取消'
  191. }).then(function() {
  192. archiveReport(row.id).then(function() {
  193. self.$message.success('已归档')
  194. self.loadData()
  195. }).catch(function() {
  196. self.$message.error('归档失败')
  197. })
  198. }).catch(function() {})
  199. },
  200. handleBatchArchive() {
  201. var self = this
  202. var ids = this.selectedRows.map(function(r) { return r.id })
  203. if (ids.length === 0) return
  204. this.$confirm('确定要归档选中的 ' + ids.length + ' 份报告吗?归档后将在列表中消失(软删除)', '提示', {
  205. type: 'warning',
  206. confirmButtonText: '归档',
  207. cancelButtonText: '取消'
  208. }).then(function() {
  209. archiveReportsBatch(ids).then(function(res) {
  210. self.$message.success(res.message || ('已归档 ' + ids.length + ' 份报告'))
  211. self.selectedRows = []
  212. self.$refs.listTable && self.$refs.listTable.clearSelection()
  213. self.loadData()
  214. }).catch(function() {
  215. self.$message.error('批量归档失败')
  216. })
  217. }).catch(function() {})
  218. },
  219. typeLabel(type) {
  220. var map = { gut_flora: '菌群检测', physical_exam: '体检报告', dan: 'DAN测评', insurance: '保单', fortune: '家庭周报' }
  221. return map[type] || type || '-'
  222. },
  223. getTypeTag(type) {
  224. var map = { gut_flora: 'success', physical_exam: 'primary', dan: 'warning', insurance: 'info', fortune: 'danger' }
  225. return map[type] || 'info'
  226. },
  227. scoreType(score) {
  228. if (score == null) return 'info'
  229. if (score >= 80) return 'success'
  230. if (score >= 60) return 'warning'
  231. return 'danger'
  232. }
  233. }
  234. }
  235. </script>
  236. <style scoped>
  237. .report-management { padding: 16px; }
  238. .filter-bar {
  239. display: flex;
  240. flex-direction: row;
  241. align-items: center;
  242. gap: 12px;
  243. flex-wrap: wrap;
  244. }
  245. .sort-header {
  246. cursor: pointer;
  247. user-select: none;
  248. font-weight: 500;
  249. font-size: 12px;
  250. }
  251. .sort-header:hover {
  252. color: #409eff;
  253. }
  254. </style>