ProfileManagement.vue 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. <template>
  2. <div class="profile-mgmt admin-page">
  3. <el-card>
  4. <div slot="header" class="admin-page-header">
  5. <span class="admin-page-title">用户画像管理</span>
  6. <el-button type="primary" size="mini" @click="handleRefresh" :loading="loading">刷新列表</el-button>
  7. </div>
  8. <div class="filter-bar" style="margin-bottom:12px;display:flex;gap:12px;align-items:center;">
  9. <el-input v-model="searchForm.memberId" placeholder="家庭成员ID" clearable style="width:160px;" @keyup.enter.native="loadList" />
  10. <el-button type="primary" size="mini" @click="loadList">搜索</el-button>
  11. <el-button size="mini" @click="resetSearch">重置</el-button>
  12. </div>
  13. <div class="adm-table-wrap">
  14. <el-table :data="list" v-loading="loading" border stripe style="min-width:800px;" height="100%">
  15. <el-table-column prop="id" label="ID" width="70" />
  16. <el-table-column prop="name" label="姓名" width="100" />
  17. <el-table-column prop="age" label="年龄" width="70" />
  18. <el-table-column label="五维评分" width="320">
  19. <template slot-scope="{ row }">
  20. <span v-if="row.dimension_scores">
  21. <el-tag size="mini" style="margin-right:4px;" :color="dimColor('body')">{{ row.dimension_scores.body || '-' }}</el-tag>
  22. <el-tag size="mini" style="margin-right:4px;" :color="dimColor('wisdom')">{{ row.dimension_scores.wisdom || '-' }}</el-tag>
  23. <el-tag size="mini" style="margin-right:4px;" :color="dimColor('mind')">{{ row.dimension_scores.mind || '-' }}</el-tag>
  24. <el-tag size="mini" style="margin-right:4px;" :color="dimColor('action')">{{ row.dimension_scores.action || '-' }}</el-tag>
  25. <el-tag size="mini" :color="dimColor('wealth')">{{ row.dimension_scores.wealth || '-' }}</el-tag>
  26. </span>
  27. <span v-else style="color:#bbb">暂无</span>
  28. </template>
  29. </el-table-column>
  30. <el-table-column label="问题域" width="160">
  31. <template slot-scope="{ row }">
  32. <el-tag v-for="d in (row.problem_domains || [])" :key="d" size="mini" style="margin-right:4px;">{{ domainLabel(d) }}</el-tag>
  33. <span v-if="!row.problem_domains || row.problem_domains.length === 0" style="color:#bbb">-</span>
  34. </template>
  35. </el-table-column>
  36. <el-table-column prop="computed_at" label="更新时间" width="160">
  37. <template slot-scope="{ row }">
  38. {{ row.computed_at ? formatTime(row.computed_at) : '-' }}
  39. </template>
  40. </el-table-column>
  41. <el-table-column label="操作" width="120" fixed="right">
  42. <template slot-scope="{ row }">
  43. <el-button type="text" size="small" @click="viewDetail(row)">查看</el-button>
  44. <el-button type="text" size="small" @click="recalc(row)">重算</el-button>
  45. </template>
  46. </el-table-column>
  47. </el-table>
  48. </div>
  49. <el-pagination
  50. style="margin-top:16px;text-align:right;"
  51. layout="total, prev, pager, next"
  52. :total="total"
  53. :current-page="page"
  54. :page-size="size"
  55. @current-change="handlePageChange"
  56. />
  57. </el-card>
  58. <!-- 详情弹窗 -->
  59. <el-dialog title="画像详情" :visible.sync="dialogVisible" width="700px" @close="dialogVisible = false">
  60. <div v-if="currentProfile">
  61. <el-descriptions :column="2" border size="small">
  62. <el-descriptions-item label="姓名">{{ currentProfile.member && currentProfile.member.name }}</el-descriptions-item>
  63. <el-descriptions-item label="年龄">{{ currentProfile.member && currentProfile.member.age }}</el-descriptions-item>
  64. </el-descriptions>
  65. <h4 style="margin:16px 0 8px;color:#333;">五维评分</h4>
  66. <el-row :gutter="12" v-if="currentProfile.dimension_scores">
  67. <el-col :span="4" v-for="(score, dim) in currentProfile.dimension_scores" :key="dim">
  68. <el-progress type="dashboard" :percentage="score" :color="dimColor(dim)" :width="70" :stroke-width="8" />
  69. <div style="text-align:center;font-size:12px;margin-top:4px;">{{ dimName(dim) }}</div>
  70. </el-col>
  71. </el-row>
  72. <h4 style="margin:16px 0 8px;color:#333;">身体指标</h4>
  73. <el-descriptions :column="3" border size="small" v-if="currentProfile.body_metrics">
  74. <el-descriptions-item label="平均睡眠(h)">{{ currentProfile.body_metrics.sleep_dur_avg || '-' }}</el-descriptions-item>
  75. <el-descriptions-item label="深睡占比(%)">{{ currentProfile.body_metrics.deep_sleep_pct || '-' }}</el-descriptions-item>
  76. <el-descriptions-item label="周运动次数">{{ currentProfile.body_metrics.exercise_count_week || 0 }}</el-descriptions-item>
  77. <el-descriptions-item label="日均饮水(ml)">{{ currentProfile.body_metrics.water_intake_avg_ml || '-' }}</el-descriptions-item>
  78. <el-descriptions-item label="月饮食记录数">{{ currentProfile.body_metrics.meal_records_count || 0 }}</el-descriptions-item>
  79. </el-descriptions>
  80. <h4 style="margin:16px 0 8px;color:#333;">心理指标</h4>
  81. <el-descriptions :column="3" border size="small" v-if="currentProfile.mind_metrics">
  82. <el-descriptions-item label="正向情绪占比">{{ currentProfile.mind_metrics.emotion_joy_ratio != null ? (currentProfile.mind_metrics.emotion_joy_ratio * 100).toFixed(0) + '%' : '-' }}</el-descriptions-item>
  83. <el-descriptions-item label="平均压力(1-10)">{{ currentProfile.mind_metrics.stress_avg || '-' }}</el-descriptions-item>
  84. <el-descriptions-item label="平均精力(1-10)">{{ currentProfile.mind_metrics.energy_avg || '-' }}</el-descriptions-item>
  85. <el-descriptions-item label="负面情绪比">{{ currentProfile.mind_metrics.negative_ratio != null ? (currentProfile.mind_metrics.negative_ratio * 100).toFixed(0) + '%' : '-' }}</el-descriptions-item>
  86. <el-descriptions-item label="平均心情效价">{{ currentProfile.mind_metrics.mood_score_avg || '-' }}</el-descriptions-item>
  87. </el-descriptions>
  88. <h4 style="margin:16px 0 8px;color:#333;">行为活跃</h4>
  89. <el-descriptions :column="3" border size="small" v-if="currentProfile.action_metrics">
  90. <el-descriptions-item label="任务完成率">{{ currentProfile.action_metrics.task_completion_rate != null ? (currentProfile.action_metrics.task_completion_rate * 100).toFixed(0) + '%' : '-' }}</el-descriptions-item>
  91. <el-descriptions-item label="连续打卡(天)">{{ currentProfile.action_metrics.checkin_streak || 0 }}</el-descriptions-item>
  92. <el-descriptions-item label="7日积分">{{ currentProfile.action_metrics.points_velocity_7d || 0 }}</el-descriptions-item>
  93. </el-descriptions>
  94. <h4 style="margin:16px 0 8px;color:#333;">推荐内容</h4>
  95. <div v-if="currentProfile.tasks && currentProfile.tasks.length > 0" style="margin-bottom:8px;">
  96. <span style="font-size:13px;color:#666;">任务:</span>
  97. <el-tag v-for="t in currentProfile.tasks" :key="t.id" size="small" style="margin-right:4px;">{{ t.title }}</el-tag>
  98. </div>
  99. <div v-if="currentProfile.articles && currentProfile.articles.length > 0" style="margin-bottom:8px;">
  100. <span style="font-size:13px;color:#666;">文章:</span>
  101. <el-tag v-for="a in currentProfile.articles" :key="a.id" size="small" style="margin-right:4px;">{{ a.title }}</el-tag>
  102. </div>
  103. </div>
  104. </el-dialog>
  105. </div>
  106. </template>
  107. <script>
  108. import { getProfile, getProfileTrend } from '@/api/admin'
  109. export default {
  110. name: 'ProfileManagement',
  111. data() {
  112. return {
  113. loading: false,
  114. list: [],
  115. total: 0,
  116. page: 1,
  117. size: 20,
  118. searchForm: { memberId: '' },
  119. dialogVisible: false,
  120. currentProfile: null
  121. }
  122. },
  123. created() {
  124. this.loadList()
  125. },
  126. methods: {
  127. loadList() {
  128. this.loading = true
  129. // 这里简化:直接拉取所有有画像的成员,实际可从家庭列表获取
  130. // 由于没有批量列表接口,暂用空列表+手动搜索
  131. this.list = []
  132. this.total = 0
  133. this.loading = false
  134. },
  135. handleRefresh() {
  136. this.loadList()
  137. },
  138. resetSearch() {
  139. this.searchForm.memberId = ''
  140. this.loadList()
  141. },
  142. handlePageChange(page) {
  143. this.page = page
  144. this.loadList()
  145. },
  146. viewDetail(row) {
  147. this.currentProfile = null
  148. this.dialogVisible = true
  149. var self = this
  150. getProfile(row.id).then(function(res) {
  151. if (res.data && res.data.code === 200) {
  152. self.currentProfile = res.data.data
  153. }
  154. }).catch(function() {})
  155. },
  156. recalc(row) {
  157. this.$message.success('已触发画像重算(异步执行)')
  158. },
  159. dimName(dim) {
  160. var m = { body: '身', wisdom: '智', mind: '心', action: '行', wealth: '富' }
  161. return m[dim] || dim
  162. },
  163. dimColor(dim) {
  164. var m = { body: '#FF8C42', wisdom: '#1677FF', mind: '#FF6B9D', action: '#10B981', wealth: '#F59E0B' }
  165. return m[dim] || '#999'
  166. },
  167. domainLabel(d) {
  168. var m = { sleep: '睡眠', attention: '注意力', emotion: '情绪' }
  169. return m[d] || d
  170. },
  171. }
  172. }
  173. </script>
  174. <style scoped>
  175. .admin-page { padding: 16px; }
  176. .admin-page-header { display: flex; justify-content: space-between; align-items: center; }
  177. .admin-page-title { font-size: 16px; font-weight: bold; }
  178. .filter-bar { display: flex; align-items: center; }
  179. .el-progress { margin: 0 auto; }
  180. </style>