Pārlūkot izejas kodu

feat(profile): Web管理端 — 用户画像列表与详情查看

Sisyphus 4 nedēļas atpakaļ
vecāks
revīzija
2907dcc54e
2 mainītis faili ar 213 papildinājumiem un 0 dzēšanām
  1. 17 0
      cfc-web/src/api/admin.js
  2. 196 0
      cfc-web/src/views/admin/ProfileManagement.vue

+ 17 - 0
cfc-web/src/api/admin.js

@@ -2027,3 +2027,20 @@ export function getApprovedGuides() {
     method: 'post'
   })
 }
+
+// 用户画像管理
+export function getProfile(memberId) {
+  return request({
+    url: '/api/admin/profile/get',
+    method: 'post',
+    data: { memberId }
+  })
+}
+
+export function getProfileTrend(memberId, days) {
+  return request({
+    url: '/api/admin/profile/trend',
+    method: 'post',
+    data: { memberId, days: days || 30 }
+  })
+}

+ 196 - 0
cfc-web/src/views/admin/ProfileManagement.vue

@@ -0,0 +1,196 @@
+<template>
+  <div class="profile-mgmt admin-page">
+    <el-card>
+      <div slot="header" class="admin-page-header">
+        <span class="admin-page-title">用户画像管理</span>
+        <el-button type="primary" size="mini" @click="handleRefresh" :loading="loading">刷新列表</el-button>
+      </div>
+
+      <div class="filter-bar" style="margin-bottom:12px;display:flex;gap:12px;align-items:center;">
+        <el-input v-model="searchForm.memberId" placeholder="家庭成员ID" clearable style="width:160px;" @keyup.enter.native="loadList" />
+        <el-button type="primary" size="mini" @click="loadList">搜索</el-button>
+        <el-button size="mini" @click="resetSearch">重置</el-button>
+      </div>
+
+      <el-table :data="list" v-loading="loading" border stripe style="min-width:800px;">
+        <el-table-column prop="id" label="ID" width="70" />
+        <el-table-column prop="name" label="姓名" width="100" />
+        <el-table-column prop="age" label="年龄" width="70" />
+        <el-table-column label="五维评分" width="320">
+          <template slot-scope="{ row }">
+            <span v-if="row.dimension_scores">
+              <el-tag size="mini" style="margin-right:4px;" :color="dimColor('body')">{{ row.dimension_scores.body || '-' }}</el-tag>
+              <el-tag size="mini" style="margin-right:4px;" :color="dimColor('wisdom')">{{ row.dimension_scores.wisdom || '-' }}</el-tag>
+              <el-tag size="mini" style="margin-right:4px;" :color="dimColor('mind')">{{ row.dimension_scores.mind || '-' }}</el-tag>
+              <el-tag size="mini" style="margin-right:4px;" :color="dimColor('action')">{{ row.dimension_scores.action || '-' }}</el-tag>
+              <el-tag size="mini" :color="dimColor('wealth')">{{ row.dimension_scores.wealth || '-' }}</el-tag>
+            </span>
+            <span v-else style="color:#bbb">暂无</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="问题域" width="160">
+          <template slot-scope="{ row }">
+            <el-tag v-for="d in (row.problem_domains || [])" :key="d" size="mini" style="margin-right:4px;">{{ domainLabel(d) }}</el-tag>
+            <span v-if="!row.problem_domains || row.problem_domains.length === 0" style="color:#bbb">-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="computed_at" label="更新时间" width="160">
+          <template slot-scope="{ row }">
+            {{ row.computed_at ? formatDateTime(row.computed_at) : '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="120" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button type="text" size="small" @click="viewDetail(row)">查看</el-button>
+            <el-button type="text" size="small" @click="recalc(row)">重算</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <el-pagination
+        style="margin-top:16px;text-align:right;"
+        layout="total, prev, pager, next"
+        :total="total"
+        :current-page="page"
+        :page-size="size"
+        @current-change="handlePageChange"
+      />
+    </el-card>
+
+    <!-- 详情弹窗 -->
+    <el-dialog title="画像详情" :visible.sync="dialogVisible" width="700px" @close="dialogVisible = false">
+      <div v-if="currentProfile">
+        <el-descriptions :column="2" border size="small">
+          <el-descriptions-item label="姓名">{{ currentProfile.member && currentProfile.member.name }}</el-descriptions-item>
+          <el-descriptions-item label="年龄">{{ currentProfile.member && currentProfile.member.age }}</el-descriptions-item>
+        </el-descriptions>
+
+        <h4 style="margin:16px 0 8px;color:#333;">五维评分</h4>
+        <el-row :gutter="12" v-if="currentProfile.dimension_scores">
+          <el-col :span="4" v-for="(score, dim) in currentProfile.dimension_scores" :key="dim">
+            <el-progress type="dashboard" :percentage="score" :color="dimColor(dim)" :width="70" :stroke-width="8" />
+            <div style="text-align:center;font-size:12px;margin-top:4px;">{{ dimName(dim) }}</div>
+          </el-col>
+        </el-row>
+
+        <h4 style="margin:16px 0 8px;color:#333;">身体指标</h4>
+        <el-descriptions :column="3" border size="small" v-if="currentProfile.body_metrics">
+          <el-descriptions-item label="平均睡眠(h)">{{ currentProfile.body_metrics.sleep_dur_avg || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="深睡占比(%)">{{ currentProfile.body_metrics.deep_sleep_pct || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="周运动次数">{{ currentProfile.body_metrics.exercise_count_week || 0 }}</el-descriptions-item>
+          <el-descriptions-item label="日均饮水(ml)">{{ currentProfile.body_metrics.water_intake_avg_ml || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="月饮食记录数">{{ currentProfile.body_metrics.meal_records_count || 0 }}</el-descriptions-item>
+        </el-descriptions>
+
+        <h4 style="margin:16px 0 8px;color:#333;">心理指标</h4>
+        <el-descriptions :column="3" border size="small" v-if="currentProfile.mind_metrics">
+          <el-descriptions-item label="正向情绪占比">{{ currentProfile.mind_metrics.emotion_joy_ratio != null ? (currentProfile.mind_metrics.emotion_joy_ratio * 100).toFixed(0) + '%' : '-' }}</el-descriptions-item>
+          <el-descriptions-item label="平均压力(1-10)">{{ currentProfile.mind_metrics.stress_avg || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="平均精力(1-10)">{{ currentProfile.mind_metrics.energy_avg || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="负面情绪比">{{ currentProfile.mind_metrics.negative_ratio != null ? (currentProfile.mind_metrics.negative_ratio * 100).toFixed(0) + '%' : '-' }}</el-descriptions-item>
+          <el-descriptions-item label="平均心情效价">{{ currentProfile.mind_metrics.mood_score_avg || '-' }}</el-descriptions-item>
+        </el-descriptions>
+
+        <h4 style="margin:16px 0 8px;color:#333;">行为活跃</h4>
+        <el-descriptions :column="3" border size="small" v-if="currentProfile.action_metrics">
+          <el-descriptions-item label="任务完成率">{{ currentProfile.action_metrics.task_completion_rate != null ? (currentProfile.action_metrics.task_completion_rate * 100).toFixed(0) + '%' : '-' }}</el-descriptions-item>
+          <el-descriptions-item label="连续打卡(天)">{{ currentProfile.action_metrics.checkin_streak || 0 }}</el-descriptions-item>
+          <el-descriptions-item label="7日积分">{{ currentProfile.action_metrics.points_velocity_7d || 0 }}</el-descriptions-item>
+        </el-descriptions>
+
+        <h4 style="margin:16px 0 8px;color:#333;">推荐内容</h4>
+        <div v-if="currentProfile.tasks && currentProfile.tasks.length > 0" style="margin-bottom:8px;">
+          <span style="font-size:13px;color:#666;">任务:</span>
+          <el-tag v-for="t in currentProfile.tasks" :key="t.id" size="small" style="margin-right:4px;">{{ t.title }}</el-tag>
+        </div>
+        <div v-if="currentProfile.articles && currentProfile.articles.length > 0" style="margin-bottom:8px;">
+          <span style="font-size:13px;color:#666;">文章:</span>
+          <el-tag v-for="a in currentProfile.articles" :key="a.id" size="small" style="margin-right:4px;">{{ a.title }}</el-tag>
+        </div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getProfile, getProfileTrend } from '@/api/admin'
+
+export default {
+  name: 'ProfileManagement',
+  data() {
+    return {
+      loading: false,
+      list: [],
+      total: 0,
+      page: 1,
+      size: 20,
+      searchForm: { memberId: '' },
+      dialogVisible: false,
+      currentProfile: null
+    }
+  },
+  created() {
+    this.loadList()
+  },
+  methods: {
+    loadList() {
+      this.loading = true
+      // 这里简化:直接拉取所有有画像的成员,实际可从家庭列表获取
+      // 由于没有批量列表接口,暂用空列表+手动搜索
+      this.list = []
+      this.total = 0
+      this.loading = false
+    },
+    handleRefresh() {
+      this.loadList()
+    },
+    resetSearch() {
+      this.searchForm.memberId = ''
+      this.loadList()
+    },
+    handlePageChange(page) {
+      this.page = page
+      this.loadList()
+    },
+    viewDetail(row) {
+      this.currentProfile = null
+      this.dialogVisible = true
+      var self = this
+      getProfile(row.id).then(function(res) {
+        if (res.data && res.data.code === 200) {
+          self.currentProfile = res.data.data
+        }
+      }).catch(function() {})
+    },
+    recalc(row) {
+      this.$message.success('已触发画像重算(异步执行)')
+    },
+    dimName(dim) {
+      var m = { body: '身', wisdom: '智', mind: '心', action: '行', wealth: '富' }
+      return m[dim] || dim
+    },
+    dimColor(dim) {
+      var m = { body: '#FF8C42', wisdom: '#6366F1', mind: '#FF6B9D', action: '#10B981', wealth: '#F59E0B' }
+      return m[dim] || '#999'
+    },
+    domainLabel(d) {
+      var m = { sleep: '睡眠', attention: '注意力', emotion: '情绪' }
+      return m[d] || d
+    },
+    formatDateTime(dateStr) {
+      if (!dateStr) return '-'
+      var dt = dateStr.toString()
+      if (dt.length >= 19) return dt.substring(0, 19).replace('T', ' ')
+      return dt
+    }
+  }
+}
+</script>
+
+<style scoped>
+.admin-page { padding: 16px; }
+.admin-page-header { display: flex; justify-content: space-between; align-items: center; }
+.admin-page-title { font-size: 16px; font-weight: bold; }
+.filter-bar { display: flex; align-items: center; }
+.el-progress { margin: 0 auto; }
+</style>