FamilyOverviewDialog.vue 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. <template>
  2. <el-dialog
  3. :visible.sync="dialogVisible"
  4. title="家庭信息"
  5. width="900px"
  6. top="6vh"
  7. @open="load"
  8. :close-on-click-modal="false"
  9. >
  10. <div v-loading="loading">
  11. <template v-if="overview && overview.family">
  12. <el-descriptions :column="2" border>
  13. <el-descriptions-item label="家庭名称">{{ overview.family.name }}</el-descriptions-item>
  14. <el-descriptions-item label="邀请码">{{ overview.family.inviteCode || '-' }}</el-descriptions-item>
  15. <el-descriptions-item label="主账号">{{ overview.creatorRealName || overview.creatorName || '-' }}</el-descriptions-item>
  16. <el-descriptions-item label="创建时间">{{ formatDate(overview.family.createdAt) }}</el-descriptions-item>
  17. </el-descriptions>
  18. <div class="count-cards">
  19. <StatCard label="报告" :value="counts.reports" color="primary" clickable @click="activeTab = 'reports'" />
  20. <StatCard label="方案" :value="counts.plans" color="success" clickable @click="activeTab = 'plans'" />
  21. <StatCard label="任务" :value="counts.tasks" color="warning" clickable @click="activeTab = 'tasks'" />
  22. </div>
  23. <el-tabs v-model="activeTab">
  24. <el-tab-pane label="报告" name="reports">
  25. <el-table :data="overview.reports || []" border max-height="300">
  26. <el-table-column label="类型">
  27. <template slot-scope="{ row }">
  28. <StatusTag :options="reportTypeOptions" :value="row.reportType" />
  29. </template>
  30. </el-table-column>
  31. <el-table-column prop="reportNumber" label="报告编号" width="140" />
  32. <el-table-column label="日期">
  33. <template slot-scope="{ row }">{{ formatDate(row.reportDate) }}</template>
  34. </el-table-column>
  35. <el-table-column prop="personName" label="姓名" />
  36. <el-table-column label="状态">
  37. <template slot-scope="{ row }">
  38. <StatusTag :options="reportStatusOptions" :value="row.status" />
  39. </template>
  40. </el-table-column>
  41. <el-table-column prop="overallScore" label="总分" width="70" />
  42. <el-table-column prop="gutHealthScore" label="肠道分" width="80" />
  43. </el-table>
  44. </el-tab-pane>
  45. <el-tab-pane label="方案" name="plans">
  46. <el-table :data="overview.plans || []" border max-height="300">
  47. <el-table-column label="类型">
  48. <template slot-scope="{ row }">
  49. <StatusTag :options="planTypeOptions" :value="row.planType" />
  50. </template>
  51. </el-table-column>
  52. <el-table-column prop="memberName" label="成员" />
  53. <el-table-column prop="goal" label="目标" show-overflow-tooltip />
  54. <el-table-column label="状态">
  55. <template slot-scope="{ row }">
  56. <StatusTag :options="planStatusOptions" :value="row.status" />
  57. </template>
  58. </el-table-column>
  59. <el-table-column prop="totalDays" label="天数" width="70" />
  60. <el-table-column label="创建时间">
  61. <template slot-scope="{ row }">{{ formatDate(row.createdAt) }}</template>
  62. </el-table-column>
  63. </el-table>
  64. </el-tab-pane>
  65. <el-tab-pane label="任务" name="tasks">
  66. <el-table :data="tasks" border max-height="300">
  67. <el-table-column prop="title" label="标题" show-overflow-tooltip />
  68. <el-table-column label="状态">
  69. <template slot-scope="{ row }">
  70. <StatusTag :options="taskStatusOptions" :value="row.status" />
  71. </template>
  72. </el-table-column>
  73. <el-table-column prop="category" label="分类" width="110" />
  74. <el-table-column prop="points" label="积分" width="70" />
  75. <el-table-column label="截止">
  76. <template slot-scope="{ row }">{{ formatDate(row.deadline) }}</template>
  77. </el-table-column>
  78. </el-table>
  79. <el-pagination
  80. class="pagination-right"
  81. @current-change="loadTasks"
  82. :current-page="taskPage"
  83. :page-size="taskSize"
  84. :total="taskTotal"
  85. layout="total, prev, pager, next"
  86. />
  87. </el-tab-pane>
  88. </el-tabs>
  89. </template>
  90. <el-empty v-else :description="loading ? '加载中...' : '暂无数据'" />
  91. </div>
  92. </el-dialog>
  93. </template>
  94. <script>
  95. import { getFamilyOverview, getFamilyTasks } from '@/api/admin'
  96. import { formatDate } from '@/utils/format'
  97. import StatCard from '@/components/StatCard.vue'
  98. import StatusTag from '@/components/StatusTag.vue'
  99. export default {
  100. name: 'FamilyOverviewDialog',
  101. props: {
  102. /** 弹窗可见性,父组件通过 :visible.sync 控制 */
  103. visible: { type: Boolean, default: false },
  104. /** 家庭 ID,变化或弹窗打开时触发加载 */
  105. familyId: { type: [Number, String], default: null }
  106. },
  107. data() {
  108. return {
  109. loading: false,
  110. overview: null,
  111. activeTab: 'reports',
  112. tasks: [],
  113. taskPage: 1,
  114. taskSize: 20,
  115. taskTotal: 0,
  116. reportTypeOptions: [
  117. { value: 'gut_flora', label: '菌群检测', type: 'success' },
  118. { value: 'physical_exam', label: '体检报告', type: 'primary' },
  119. { value: 'dan', label: 'DAN测评', type: 'warning' },
  120. { value: 'brain_status', label: '脑状态', type: 'primary' },
  121. { value: 'aptitude', label: '先天潜能', type: 'primary' },
  122. { value: 'insurance', label: '保单', type: 'info' },
  123. { value: 'fortune', label: '家庭周报', type: 'danger' }
  124. ],
  125. reportStatusOptions: [
  126. { value: 'active', label: '生效', type: 'success' },
  127. { value: 'archived', label: '已归档', type: 'warning' },
  128. { value: 'deleted', label: '已删除', type: 'info' }
  129. ],
  130. planTypeOptions: [
  131. { value: 'health_plan', label: '健康方案', type: 'primary' },
  132. { value: 'family_attribution_plan', label: '家庭归因方案', type: 'warning' }
  133. ],
  134. planStatusOptions: [
  135. { value: 'draft', label: '草稿', type: 'info' },
  136. { value: 'pending_review', label: '待审核', type: 'warning' },
  137. { value: 'published', label: '已发布', type: 'success' },
  138. { value: 'rejected', label: '已驳回', type: 'danger' }
  139. ],
  140. taskStatusOptions: [
  141. { value: 'pending', label: '进行中', type: 'warning' },
  142. { value: 'pending_review', label: '待审核', type: 'warning' },
  143. { value: 'completed', label: '已完成', type: 'success' },
  144. { value: 'locked', label: '前置锁定', type: 'info' },
  145. { value: 'assigned', label: '待接受', type: 'info' },
  146. { value: 'in_progress', label: '进行中', type: 'warning' },
  147. { value: 'rejected', label: '已拒绝', type: 'danger' },
  148. { value: 'overdue', label: '已过期', type: 'danger' },
  149. { value: 'cancelled', label: '已取消', type: 'info' }
  150. ]
  151. }
  152. },
  153. computed: {
  154. /**
  155. * 受控 dialog 可见性:写时派发 update:visible,保证父组件的 :visible.sync 同步。
  156. * (el-dialog 关闭时 emit update:visible(false),若直接绑 visible prop 会导致
  157. * 直接修改 prop 且无法回传父组件,故用计算属性桥接。)
  158. */
  159. dialogVisible: {
  160. get() {
  161. return this.visible
  162. },
  163. set(val) {
  164. this.$emit('update:visible', val)
  165. }
  166. },
  167. /** 计数卡片数据源(overview 未加载时兜底为空对象,避免 counts.xxx 报错) */
  168. counts() {
  169. return (this.overview && this.overview.counts) || {}
  170. }
  171. },
  172. watch: {
  173. /** 家庭 ID 变化时(弹窗已打开)重新加载 */
  174. familyId() {
  175. if (this.visible) this.load()
  176. }
  177. },
  178. methods: {
  179. formatDate,
  180. async load() {
  181. if (!this.familyId) return
  182. this.loading = true
  183. this.overview = null
  184. this.tasks = []
  185. this.taskTotal = 0
  186. this.taskPage = 1
  187. try {
  188. const res = await getFamilyOverview(this.familyId)
  189. this.overview = res.data
  190. this.loadTasks(1)
  191. } catch (e) {
  192. this.$message.error('加载家庭信息失败')
  193. } finally {
  194. this.loading = false
  195. }
  196. },
  197. async loadTasks(page) {
  198. this.taskPage = page
  199. try {
  200. const res = await getFamilyTasks(this.familyId, { page, size: this.taskSize })
  201. this.tasks = res.data.records || []
  202. this.taskTotal = res.data.total || 0
  203. } catch (e) {
  204. this.$message.error('加载任务列表失败')
  205. }
  206. }
  207. }
  208. }
  209. </script>
  210. <style scoped>
  211. .count-cards {
  212. display: flex;
  213. gap: 16px;
  214. margin: 16px 0;
  215. }
  216. .count-cards >>> .adm-stat-card {
  217. flex: 1;
  218. }
  219. .pagination-right {
  220. margin-top: 12px;
  221. text-align: right;
  222. }
  223. </style>