Преглед изворни кода

feat: 创建身/心/智/行四个维度专属内容组件

从家庭关系图点入成员详情页时,根据来源维度展示对应内容:\nbody-section: 健康报告+七维雷达图+打卡 \nmind-section: 关系评分+联系人+告警+里程碑 \nwisdom-section: 最新测评+历史记录 \naction-section: 今日任务+打卡统计+活动

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Sisyphus пре 3 месеци
родитељ
комит
6547493017

+ 258 - 0
cfc-frontend/components/member-detail-sections/action-section.vue

@@ -0,0 +1,258 @@
+<template>
+<view class="action-section">
+  <!-- 今日任务 -->
+  <view class="section-card" v-if="todayTasks.length > 0">
+    <view class="section-header">
+      <text class="section-title">✅ 今日任务</text>
+    </view>
+    <view class="task-item" v-for="task in todayTasks" :key="task.id" @click="goTaskDetail(task)">
+      <view class="task-status" :class="task.status === 'completed' ? 'task-done' : 'task-pending'"></view>
+      <text class="task-name">{{ task.title || task.name }}</text>
+      <text class="task-points">{{ task.points || task.coin || 0 }}分</text>
+    </view>
+  </view>
+  <view class="section-card" v-else-if="!loading && childId">
+    <view class="empty-state">
+      <text class="empty-text">今日暂无任务</text>
+      <text class="empty-sub">去「行」维度页布置新任务吧</text>
+    </view>
+  </view>
+
+  <!-- 打卡统计 -->
+  <view class="section-card" v-if="checkinStats">
+    <view class="section-header">
+      <text class="section-title">📊 打卡统计</text>
+    </view>
+    <view class="stats-row">
+      <view class="stat-item">
+        <text class="stat-value">{{ checkinStats.totalDays || 0 }}</text>
+        <text class="stat-label">累计天数</text>
+      </view>
+      <view class="stat-item">
+        <text class="stat-value">{{ checkinStats.currentStreak || 0 }}</text>
+        <text class="stat-label">连续打卡</text>
+      </view>
+      <view class="stat-item">
+        <text class="stat-value">{{ checkinStats.thisMonth || 0 }}</text>
+        <text class="stat-label">本月次数</text>
+      </view>
+    </view>
+  </view>
+
+  <!-- 近期活动 -->
+  <view class="section-card" v-if="activityList.length > 0">
+    <view class="section-header">
+      <text class="section-title">🎪 近期活动</text>
+    </view>
+    <view class="activity-item" v-for="act in activityList" :key="act.id" @click="goActivityDetail(act)">
+      <text class="activity-name">{{ act.title || act.name }}</text>
+      <text class="activity-date">{{ formatDate(act.startTime || act.activityDate) }}</text>
+    </view>
+  </view>
+
+  <!-- 非孩子成员提示 -->
+  <view class="section-card" v-if="!childId && !loading">
+    <view class="empty-state">
+      <text class="empty-text">该成员暂无行动数据</text>
+    </view>
+  </view>
+
+  <!-- 加载中 -->
+  <view class="loading" v-if="loading">加载中...</view>
+</view>
+</template>
+
+<script>
+import { getTodayTasksByCategory, getCheckinStats, getActivityList } from '../../utils/api.js'
+
+export default {
+  props: {
+    memberId: {
+      type: [Number, String],
+      default: null
+    },
+    childId: {
+      type: [Number, String],
+      default: null
+    },
+    memberType: {
+      type: String,
+      default: ''
+    },
+    memberInfo: {
+      type: Object,
+      default: null
+    },
+    sandboxData: {
+      type: Object,
+      default: null
+    },
+    energyMap: {
+      type: Object,
+      default: null
+    }
+  },
+  data: function() {
+    return {
+      loading: true,
+      todayTasks: [],
+      checkinStats: null,
+      activityList: []
+    }
+  },
+  mounted: function() {
+    if (this.childId) {
+      this.loadData()
+    } else {
+      this.loading = false
+    }
+  },
+  methods: {
+    loadData: function() {
+      var self = this
+      getTodayTasksByCategory(this.childId, 'action').then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.todayTasks = Array.isArray(res.data) ? res.data : (res.data.records || [])
+        }
+      }).catch(function(e) {
+        console.log('加载今日任务失败', e)
+      })
+      
+      getCheckinStats({ childId: this.childId }).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.checkinStats = res.data
+        }
+      }).catch(function(e) {
+        console.log('加载打卡统计失败', e)
+      })
+      
+      getActivityList({ page: 1, size: 3 }).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.activityList = Array.isArray(res.data) ? res.data : (res.data.records || [])
+        }
+      }).catch(function(e) {
+        console.log('加载活动列表失败', e)
+      })
+      
+      this.loading = false
+    },
+    formatDate: function(dateStr) {
+      if (!dateStr) return ''
+      var d = new Date(dateStr)
+      return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate()
+    },
+    goTaskDetail: function(task) {
+      uni.navigateTo({
+        url: '/pages/action/task-detail?id=' + task.id
+      })
+    },
+    goActivityDetail: function(act) {
+      uni.navigateTo({
+        url: '/pages/action/activity-detail?id=' + act.id
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.action-section {}
+.section-card {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+}
+.section-header {
+  margin-bottom: 16rpx;
+}
+.section-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+.task-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 14rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.task-status {
+  width: 16rpx;
+  height: 16rpx;
+  border-radius: 50%;
+  margin-right: 16rpx;
+}
+.task-done {
+  background: #10B981;
+}
+.task-pending {
+  background: #F59E0B;
+}
+.task-name {
+  flex: 1;
+  font-size: 28rpx;
+  color: #333;
+}
+.task-points {
+  font-size: 24rpx;
+  color: #10B981;
+}
+.stats-row {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-around;
+  padding: 16rpx 0;
+}
+.stat-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.stat-value {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #10B981;
+}
+.stat-label {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 4rpx;
+}
+.activity-item {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  padding: 12rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.activity-name {
+  font-size: 26rpx;
+  color: #333;
+  flex: 1;
+}
+.activity-date {
+  font-size: 22rpx;
+  color: #999;
+}
+.empty-state {
+  padding: 30rpx 0;
+  text-align: center;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.empty-sub {
+  font-size: 24rpx;
+  color: #ccc;
+  margin-top: 8rpx;
+}
+.loading {
+  text-align: center;
+  padding: 40rpx;
+  color: #999;
+}
+</style>

+ 199 - 0
cfc-frontend/components/member-detail-sections/body-section.vue

@@ -0,0 +1,199 @@
+<template>
+  <view class="body-section">
+    <!-- 最新健康报告 -->
+    <view class="section-card" v-if="healthReport">
+      <view class="section-header">
+        <text class="section-title">📋 最新健康报告</text>
+      </view>
+      <view class="report-card" @click="goReportDetail">
+        <text class="report-score">{{ healthReport.overallScore || '--' }}</text>
+        <text class="report-label">综合健康分</text>
+      </view>
+      <view class="report-meta">
+        <text class="report-date">报告日期:{{ formatDate(healthReport.reportDate) }}</text>
+      </view>
+    </view>
+    <view class="section-card" v-else-if="!loading && childId">
+      <view class="empty-state">
+        <text class="empty-text">暂无健康报告</text>
+        <text class="empty-sub">规划师录入后将自动显示</text>
+      </view>
+    </view>
+
+    <!-- 七维健康图 -->
+    <view class="section-card" v-if="dimensionOverview && dimensionOverview.dimensions">
+      <view class="section-header">
+        <text class="section-title">📊 七维健康图</text>
+      </view>
+      <RadarChart
+        :dimensions="dimensionOverview.dimensions.map(function(d) { return { label: d.label, key: d.dimension } })"
+        :scores="dimensionOverview.dimensions.map(function(d) { return d.score })"
+        fillColor="#FF8C42"
+        gridColor="#FED7AA"
+        labelColor="#9A3412"
+        :width="580"
+        :height="400" />
+    </view>
+
+    <!-- 最近打卡 -->
+    <view class="section-card" v-if="checkinList.length > 0">
+      <view class="section-header">
+        <text class="section-title">🏃 最近打卡</text>
+      </view>
+      <view class="checkin-item" v-for="item in checkinList" :key="item.id">
+        <text class="checkin-type">{{ item.typeName || item.type }}</text>
+        <text class="checkin-time">{{ formatDate(item.createTime) }}</text>
+      </view>
+    </view>
+
+    <!-- 非孩子成员提示 -->
+    <view class="section-card" v-if="!childId && !loading">
+      <view class="empty-state">
+        <text class="empty-text">该成员暂无健康数据</text>
+      </view>
+    </view>
+
+    <!-- 加载中 -->
+    <view class="loading" v-if="loading">加载中...</view>
+  </view>
+</template>
+
+<script>
+import RadarChart from '../RadarChart.vue'
+import { getHealthAnalysis, getDimensionOverview, getCheckinList } from '../../utils/api.js'
+
+export default {
+  components: { RadarChart },
+  props: {
+    memberId: { type: [Number, String], default: null },
+    childId: { type: [Number, String], default: null },
+    memberType: { type: String, default: '' },
+    memberInfo: { type: Object, default: null },
+    sandboxData: { type: Object, default: null },
+    energyMap: { type: Object, default: null }
+  },
+  data: function() {
+    return {
+      loading: true,
+      healthReport: null,
+      dimensionOverview: null,
+      checkinList: []
+    }
+  },
+  mounted: function() {
+    if (this.childId) {
+      this.loadData()
+    } else {
+      this.loading = false
+    }
+  },
+  methods: {
+    loadData: function() {
+      var self = this
+      getHealthAnalysis(this.childId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.healthReport = res.data
+        }
+      }).catch(function(e) { console.log('加载健康分析失败', e) })
+
+      getDimensionOverview({ childId: this.childId }).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.dimensionOverview = res.data
+        }
+      }).catch(function(e) { console.log('加载维度概览失败', e) })
+
+      getCheckinList({ childId: this.childId, page: 1, size: 3 }).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.checkinList = Array.isArray(res.data) ? res.data : (res.data.records || [])
+        }
+      }).catch(function(e) { console.log('加载打卡记录失败', e) })
+
+      this.loading = false
+    },
+    formatDate: function(dateStr) {
+      if (!dateStr) return ''
+      var d = new Date(dateStr)
+      return d.getFullYear() + '-' + (d.getMonth()+1) + '-' + d.getDate()
+    },
+    goReportDetail: function() {
+      uni.navigateTo({ url: '/pages/body-detail/health-report?childId=' + this.childId })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.body-section {}
+.section-card {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+}
+.section-header {
+  margin-bottom: 16rpx;
+}
+.section-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+.report-card {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 20rpx 0;
+}
+.report-score {
+  font-size: 72rpx;
+  font-weight: bold;
+  color: #FF8C42;
+}
+.report-label {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 8rpx;
+}
+.report-meta {
+  padding-top: 12rpx;
+  border-top: 1rpx solid #f0f0f0;
+}
+.report-date {
+  font-size: 24rpx;
+  color: #999;
+}
+.checkin-item {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  padding: 16rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.checkin-type {
+  font-size: 28rpx;
+  color: #333;
+}
+.checkin-time {
+  font-size: 24rpx;
+  color: #999;
+}
+.empty-state {
+  padding: 30rpx 0;
+  text-align: center;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.empty-sub {
+  font-size: 24rpx;
+  color: #ccc;
+  margin-top: 8rpx;
+}
+.loading {
+  text-align: center;
+  padding: 40rpx;
+  color: #999;
+}
+</style>

+ 219 - 0
cfc-frontend/components/member-detail-sections/mind-section.vue

@@ -0,0 +1,219 @@
+<template>
+  <view class="mind-section">
+    <!-- 亲密关系评分 -->
+    <view class="section-card" v-if="relationScores && relationScores.scores">
+      <view class="section-header">
+        <text class="section-title">💕 亲密关系评分</text>
+      </view>
+      <view class="score-item" v-for="(score, key) in relationScores.scores" :key="key">
+        <text class="score-label">{{ score.label || key }}</text>
+        <view class="score-bar-bg">
+          <view class="score-bar-fill" :style="'width:' + (score.value || 0) + '%'"></view>
+        </view>
+        <text class="score-value">{{ score.value || 0 }}</text>
+      </view>
+    </view>
+
+    <!-- 联系人 -->
+    <view class="section-card" v-if="contactList.length > 0">
+      <view class="section-header">
+        <text class="section-title">📞 重要联系人</text>
+      </view>
+      <view class="contact-item" v-for="contact in contactList" :key="contact.id" @click="goContactDetail(contact)">
+        <text class="contact-name">{{ contact.name }}</text>
+        <text class="contact-relation">{{ contact.relation || '' }}</text>
+        <text class="contact-phone">{{ contact.phone || '' }}</text>
+      </view>
+    </view>
+
+    <!-- 健康告警 -->
+    <view class="section-card" v-if="healthAlerts && healthAlerts.length > 0">
+      <view class="section-header">
+        <text class="section-title">⚠️ 健康告警</text>
+      </view>
+      <view class="alert-item" v-for="alert in healthAlerts" :key="alert.id">
+        <text class="alert-type">{{ alert.type }}</text>
+        <text class="alert-desc">{{ alert.description }}</text>
+      </view>
+    </view>
+
+    <!-- 里程碑 -->
+    <view class="section-card" v-if="milestones && milestones.length > 0">
+      <view class="section-header">
+        <text class="section-title">🎯 即将到来的里程碑</text>
+      </view>
+      <view class="milestone-item" v-for="ms in milestones" :key="ms.id || ms.title">
+        <text class="milestone-title">{{ ms.title }}</text>
+        <text class="milestone-date">{{ ms.date }}</text>
+      </view>
+    </view>
+
+    <!-- 加载中 -->
+    <view class="loading" v-if="loading">加载中...</view>
+  </view>
+</template>
+
+<script>
+import { getRelationshipScores, getContactList, getHealthAlerts, getMilestones } from '../../utils/api.js'
+
+export default {
+  props: {
+    memberId: { type: [Number, String], default: null },
+    childId: { type: [Number, String], default: null },
+    memberType: { type: String, default: '' },
+    memberInfo: { type: Object, default: null },
+    sandboxData: { type: Object, default: null },
+    energyMap: { type: Object, default: null }
+  },
+  data: function() {
+    return {
+      loading: true,
+      relationScores: null,
+      contactList: [],
+      healthAlerts: [],
+      milestones: []
+    }
+  },
+  mounted: function() {
+    this.loadData()
+  },
+  methods: {
+    loadData: function() {
+      var self = this
+      var familyId = (this.memberInfo && this.memberInfo.familyId) || ''
+
+      if (familyId) {
+        getRelationshipScores(familyId).then(function(res) {
+          if (res.code === 200 && res.data) {
+            self.relationScores = res.data
+          }
+        }).catch(function(e) { console.log('加载关系评分失败', e) })
+
+        getContactList({ familyId: familyId }).then(function(res) {
+          if (res.code === 200 && res.data) {
+            self.contactList = Array.isArray(res.data) ? res.data : (res.data.records || [])
+          }
+        }).catch(function(e) { console.log('加载联系人失败', e) })
+
+        getHealthAlerts(familyId).then(function(res) {
+          if (res.code === 200 && res.data) {
+            self.healthAlerts = Array.isArray(res.data) ? res.data : []
+          }
+        }).catch(function(e) { console.log('加载告警失败', e) })
+
+        getMilestones(familyId, 30).then(function(res) {
+          if (res.code === 200 && res.data) {
+            self.milestones = Array.isArray(res.data) ? res.data : []
+          }
+        }).catch(function(e) { console.log('加载里程碑失败', e) })
+      }
+      this.loading = false
+    },
+    goContactDetail: function(contact) {
+      uni.navigateTo({ url: '/pages/mind/contact-detail?id=' + contact.id })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mind-section {}
+.section-card {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+}
+.section-header {
+  margin-bottom: 16rpx;
+}
+.section-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+.score-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 10rpx 0;
+}
+.score-label {
+  font-size: 26rpx;
+  color: #555;
+  width: 160rpx;
+}
+.score-bar-bg {
+  flex: 1;
+  height: 20rpx;
+  background: #f0f0f0;
+  border-radius: 10rpx;
+  overflow: hidden;
+}
+.score-bar-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #FF6B9D, #FF9EC4);
+  border-radius: 10rpx;
+}
+.score-value {
+  font-size: 24rpx;
+  color: #FF6B9D;
+  width: 60rpx;
+  text-align: right;
+}
+.contact-item {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  padding: 14rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.contact-name {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: bold;
+}
+.contact-relation {
+  font-size: 24rpx;
+  color: #999;
+}
+.contact-phone {
+  font-size: 24rpx;
+  color: #FF6B9D;
+}
+.alert-item {
+  display: flex;
+  flex-direction: row;
+  padding: 10rpx 0;
+}
+.alert-type {
+  font-size: 24rpx;
+  color: #E74C3C;
+  width: 100rpx;
+}
+.alert-desc {
+  font-size: 26rpx;
+  color: #555;
+  flex: 1;
+}
+.milestone-item {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  padding: 10rpx 0;
+}
+.milestone-title {
+  font-size: 26rpx;
+  color: #333;
+}
+.milestone-date {
+  font-size: 24rpx;
+  color: #999;
+}
+.loading {
+  text-align: center;
+  padding: 40rpx;
+  color: #999;
+}
+</style>

+ 206 - 0
cfc-frontend/components/member-detail-sections/wisdom-section.vue

@@ -0,0 +1,206 @@
+<template>
+  <view class="wisdom-section">
+    <!-- 最新测评结果 -->
+    <view class="section-card" v-if="latestResult">
+      <view class="section-header">
+        <text class="section-title">🧠 最新测评结果</text>
+      </view>
+      <view class="result-overview" @click="goResultDetail">
+        <text class="result-score">{{ latestResult.score || '--' }}</text>
+        <text class="result-dimension">{{ latestResult.dimensionName || latestResult.title || '综合测评' }}</text>
+      </view>
+      <view class="result-date">测评日期:{{ formatDate(latestResult.createTime || latestResult.testDate) }}</view>
+    </view>
+    <view class="section-card" v-else-if="!loading && childId">
+      <view class="empty-state">
+        <text class="empty-text">暂无测评记录</text>
+        <text class="empty-sub">完成测评后结果将显示在这里</text>
+      </view>
+    </view>
+
+    <!-- 测评历史 -->
+    <view class="section-card" v-if="historyList.length > 0">
+      <view class="section-header">
+        <text class="section-title">📋 测评历史</text>
+      </view>
+      <view class="history-item" v-for="item in historyList" :key="item.id" @click="goResultDetail">
+        <text class="history-title">{{ item.title || item.dimensionName || '测评' }}</text>
+        <text class="history-score">{{ item.score || '--' }}</text>
+        <text class="history-date">{{ formatDate(item.createTime || item.testDate) }}</text>
+      </view>
+    </view>
+
+    <!-- 非孩子成员提示 -->
+    <view class="section-card" v-if="!childId && !loading">
+      <view class="empty-state">
+        <text class="empty-text">该成员暂无测评数据</text>
+      </view>
+    </view>
+
+    <!-- 加载中 -->
+    <view class="loading" v-if="loading">加载中...</view>
+  </view>
+</template>
+
+<script>
+import { getAssessmentLatestResult, getAssessmentHistory } from '../../utils/api.js'
+
+export default {
+  props: {
+    memberId: {
+      type: [Number, String],
+      default: null
+    },
+    childId: {
+      type: [Number, String],
+      default: null
+    },
+    memberType: {
+      type: String,
+      default: ''
+    },
+    memberInfo: {
+      type: Object,
+      default: null
+    },
+    sandboxData: {
+      type: Object,
+      default: null
+    },
+    energyMap: {
+      type: Object,
+      default: null
+    }
+  },
+  data: function() {
+    return {
+      loading: true,
+      latestResult: null,
+      historyList: []
+    }
+  },
+  mounted: function() {
+    if (this.childId) {
+      this.loadData()
+    } else {
+      this.loading = false
+    }
+  },
+  methods: {
+    loadData: function() {
+      var self = this
+      getAssessmentLatestResult(this.childId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.latestResult = res.data
+        }
+      }).catch(function(e) {
+        console.log('加载测评结果失败', e)
+      })
+      
+      getAssessmentHistory(this.childId, 5).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.historyList = Array.isArray(res.data) ? res.data : (res.data.records || [])
+        }
+      }).catch(function(e) {
+        console.log('加载测评历史失败', e)
+      })
+      
+      this.loading = false
+    },
+    formatDate: function(dateStr) {
+      if (!dateStr) return ''
+      var d = new Date(dateStr)
+      return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate()
+    },
+    goResultDetail: function() {
+      uni.navigateTo({
+        url: '/pages/wisdom/assessment-detail?childId=' + this.childId
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.wisdom-section {}
+.section-card {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+}
+.section-header {
+  margin-bottom: 16rpx;
+}
+.section-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+.result-overview {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 20rpx 0;
+}
+.result-score {
+  font-size: 72rpx;
+  font-weight: bold;
+  color: #6366F1;
+}
+.result-dimension {
+  font-size: 28rpx;
+  color: #555;
+  margin-top: 8rpx;
+}
+.result-date {
+  font-size: 24rpx;
+  color: #999;
+  padding-top: 12rpx;
+  border-top: 1rpx solid #f0f0f0;
+  margin-top: 8rpx;
+}
+.history-item {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  padding: 14rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.history-title {
+  font-size: 26rpx;
+  color: #333;
+  flex: 1;
+}
+.history-score {
+  font-size: 26rpx;
+  color: #6366F1;
+  width: 80rpx;
+  text-align: center;
+}
+.history-date {
+  font-size: 22rpx;
+  color: #999;
+  width: 140rpx;
+  text-align: right;
+}
+.empty-state {
+  padding: 30rpx 0;
+  text-align: center;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.empty-sub {
+  font-size: 24rpx;
+  color: #ccc;
+  margin-top: 8rpx;
+}
+.loading {
+  text-align: center;
+  padding: 40rpx;
+  color: #999;
+}
+</style>