Просмотр исходного кода

fix(frontend): 孤儿页清理——补注册 12 个被引用但未注册页面 + 删除 5 个重复遗留页面

补注册(被引用但 pages.json 缺失,运行时导航会失败):
- pages/family: address-edit / manual-score-entry / mbti-entry / mbti-test / mbti-result / mbti-profile
- pages/member-detail/member-detail(被 FamilyEnergyBar、parent-index 引用)
- pages/profile/service-role-apply(新 6 角色版,被 ProfileMenu/vendor/butler/nutritionist/guide 5 处引用)
- pages/match/street-match(新增子包)
- pages/assessment/upload-report
- pages/health/bodyfat-confirm
- pages/growth/profile/index

删除(重复/遗留/依赖不存在 API):
- pages/health/teacher-select.vue(重复 teacher-service)
- pages/mind-extra/articles.vue(遗留,article-center 取代)
- pages/activity/registrations.vue(依赖 adminActivity* API 前端不存在)
- pages/teacher/appointments.vue(依赖 getGuideAppointments 前端不存在)
- pages/member-detail/service-role-apply.vue(旧 3 角色版,被 profile 版取代)

其他:
- api.js: 补 MBTI 4 个 + manual-score 1 个缺失导出(后端 Controller 均存在)
- mind-detail/index.vue: 修复指向已删 /pages/mind/emotion-report 的悬挂导航
Sisyphus Agent 2 дней назад
Родитель
Сommit
4d834672cb

+ 74 - 2
cfc-frontend/pages.json

@@ -433,6 +433,12 @@
           "style": {
           "style": {
             "navigationBarTitleText": "服务评价"
             "navigationBarTitleText": "服务评价"
           }
           }
+        },
+        {
+          "path": "upload-report",
+          "style": {
+            "navigationBarTitleText": "上传测评报告"
+          }
         }
         }
       ]
       ]
     },
     },
@@ -527,6 +533,12 @@
           "style": {
           "style": {
             "navigationBarTitleText": "闯关进度"
             "navigationBarTitleText": "闯关进度"
           }
           }
+        },
+        {
+          "path": "profile/index",
+          "style": {
+            "navigationBarTitleText": "成长画像"
+          }
         }
         }
       ]
       ]
     },
     },
@@ -1072,6 +1084,12 @@
             "navigationBarTitleText": "我的反馈",
             "navigationBarTitleText": "我的反馈",
             "enablePullDownRefresh": true
             "enablePullDownRefresh": true
           }
           }
+        },
+        {
+          "path": "bodyfat-confirm",
+          "style": {
+            "navigationBarTitleText": "体脂指标确认"
+          }
         }
         }
       ]
       ]
     },
     },
@@ -1364,6 +1382,53 @@
           "style": {
           "style": {
             "navigationBarTitleText": "自检"
             "navigationBarTitleText": "自检"
           }
           }
+        },
+        {
+          "path": "address-edit",
+          "style": {
+            "navigationBarTitleText": "家庭地址设置"
+          }
+        },
+        {
+          "path": "manual-score-entry",
+          "style": {
+            "navigationBarTitleText": "五维直接填分"
+          }
+        },
+        {
+          "path": "mbti-entry",
+          "style": {
+            "navigationBarTitleText": "MBTI 人格测评"
+          }
+        },
+        {
+          "path": "mbti-test",
+          "style": {
+            "navigationBarTitleText": "MBTI 测评"
+          }
+        },
+        {
+          "path": "mbti-result",
+          "style": {
+            "navigationBarTitleText": "MBTI 测评结果"
+          }
+        },
+        {
+          "path": "mbti-profile",
+          "style": {
+            "navigationBarTitleText": "MBTI 人格百科"
+          }
+        }
+      ]
+    },
+    {
+      "root": "pages/match",
+      "pages": [
+        {
+          "path": "street-match",
+          "style": {
+            "navigationBarTitleText": "区域匹配"
+          }
         }
         }
       ]
       ]
     },
     },
@@ -1544,9 +1609,10 @@
           }
           }
         },
         },
         {
         {
-          "path": "service-role-apply",
+          "path": "member-detail",
           "style": {
           "style": {
-            "navigationBarTitleText": "服务角色申请"
+            "navigationBarTitleText": "成员详情",
+            "navigationStyle": "custom"
           }
           }
         }
         }
       ]
       ]
@@ -1712,6 +1778,12 @@
           "style": {
           "style": {
             "navigationBarTitleText": "我的团队"
             "navigationBarTitleText": "我的团队"
           }
           }
+        },
+        {
+          "path": "service-role-apply",
+          "style": {
+            "navigationBarTitleText": "服务角色申请"
+          }
         }
         }
       ]
       ]
     },
     },

+ 0 - 391
cfc-frontend/pages/activity/registrations.vue

@@ -1,391 +0,0 @@
-<template>
-  <view class="container">
-    <!-- 活动标题 -->
-    <view class="header">
-      <text class="header-title">{{ activityTitle || '报名管理' }}</text>
-    </view>
-
-    <!-- 状态筛选 chips -->
-    <scroll-view class="filter-scroll" scroll-x enable-flex>
-      <view class="filter-chips">
-        <view
-          class="filter-chip"
-          :class="{ active: currentFilter === filter.value }"
-          v-for="filter in statusFilters"
-          :key="filter.value"
-          @click="onFilterChange(filter.value)"
-        >
-          <text class="filter-label">{{ filter.label }}</text>
-        </view>
-      </view>
-    </scroll-view>
-
-    <!-- 报名列表 -->
-    <view class="reg-list" v-if="registrations.length > 0">
-      <view
-        class="reg-card"
-        v-for="row in registrations"
-        :key="row.id"
-      >
-        <view class="reg-info">
-          <view class="reg-name-row">
-            <text class="reg-name">{{ row.registrantName || '未填写姓名' }}</text>
-            <text class="reg-tag" :class="'reg-tag-' + row.status">{{ statusText(row.status) }}</text>
-          </view>
-          <text class="reg-meta" v-if="row.registrantPhone">联系电话:{{ row.registrantPhone }}</text>
-          <text class="reg-meta">报名时间:{{ formatDateTime(row.registeredAt) }}</text>
-        </view>
-        <view class="reg-actions" v-if="row.status === 'pending'">
-          <button class="btn-approve" @click="onApprove(row)">通过</button>
-          <button class="btn-reject" @click="onReject(row)">拒绝</button>
-        </view>
-      </view>
-    </view>
-
-    <!-- 空状态 -->
-    <view class="empty-state" v-else-if="!loading">
-      <text class="empty-icon">📋</text>
-      <text class="empty-title">暂无报名记录</text>
-      <text class="empty-desc">当前状态下没有报名申请</text>
-    </view>
-
-    <!-- 加载状态 -->
-    <view class="loading-state" v-if="loading">
-      <view class="loading-spinner"></view>
-      <text class="loading-text">加载中...</text>
-    </view>
-  </view>
-</template>
-
-<script>
-import { adminActivityRegistrationList, adminApproveRegistration, adminRejectRegistration, getActivityDetail } from '@/utils/api.js'
-import { parseDate } from '../../utils/format.js'
-
-export default {
-  data() {
-    return {
-      activityId: null,
-      activityTitle: '',
-      currentFilter: 'all',
-      statusFilters: [
-        { label: '全部', value: 'all' },
-        { label: '待审核', value: 'pending' },
-        { label: '已通过', value: 'approved' },
-        { label: '已拒绝', value: 'rejected' }
-      ],
-      registrations: [],
-      loading: false
-    }
-  },
-  onLoad: function(options) {
-    if (options && options.activityId) {
-      this.activityId = options.activityId
-    }
-    if (options && options.title) {
-      this.activityTitle = decodeURIComponent(options.title)
-    }
-    this.loadDetail()
-    this.loadList()
-  },
-  methods: {
-    loadDetail: function() {
-      var self = this
-      if (!this.activityId || this.activityTitle) return
-      getActivityDetail(this.activityId).then(function(res) {
-        if (res && res.code === 200 && res.data) {
-          self.activityTitle = res.data.title || ''
-        }
-      }).catch(function() {})
-    },
-    loadList: function() {
-      var self = this
-      this.loading = true
-      var params = {
-        activityId: this.activityId,
-        page: 1,
-        size: 100
-      }
-      if (this.currentFilter !== 'all') {
-        params.status = this.currentFilter
-      }
-      adminActivityRegistrationList(params).then(function(res) {
-        self.loading = false
-        if (res && res.code === 200 && res.data) {
-          var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
-          self.registrations = list
-        } else {
-          self.registrations = []
-        }
-      }).catch(function() {
-        self.loading = false
-        self.registrations = []
-      })
-    },
-    onFilterChange: function(value) {
-      this.currentFilter = value
-      this.loadList()
-    },
-    onApprove: function(row) {
-      var self = this
-      uni.showModal({
-        title: '提示',
-        content: '确认通过该报名吗?',
-        success: function(res) {
-          if (res.confirm) {
-            adminApproveRegistration({ registrationId: row.id, activityId: self.activityId }).then(function(r) {
-              if (r && r.code === 200) {
-                uni.showToast({ title: '已通过', icon: 'success' })
-                self.loadList()
-              } else {
-                uni.showToast({ title: (r && r.message) || '操作失败', icon: 'none' })
-              }
-            }).catch(function() {
-              uni.showToast({ title: '网络异常', icon: 'none' })
-            })
-          }
-        }
-      })
-    },
-    onReject: function(row) {
-      var self = this
-      uni.showModal({
-        title: '提示',
-        content: '确认拒绝该报名吗?',
-        success: function(res) {
-          if (res.confirm) {
-            adminRejectRegistration({ registrationId: row.id, activityId: self.activityId }).then(function(r) {
-              if (r && r.code === 200) {
-                uni.showToast({ title: '已拒绝', icon: 'success' })
-                self.loadList()
-              } else {
-                uni.showToast({ title: (r && r.message) || '操作失败', icon: 'none' })
-              }
-            }).catch(function() {
-              uni.showToast({ title: '网络异常', icon: 'none' })
-            })
-          }
-        }
-      })
-    },
-    statusText: function(status) {
-      var map = { pending: '待审核', approved: '已通过', rejected: '已拒绝', cancelled: '已取消' }
-      return map[status] || status || '未知'
-    },
-    formatDateTime: function(v) {
-      if (!v) return ''
-      var s = String(v)
-      if (s.indexOf('-') < 0 && s.indexOf('/') < 0) return s
-      var d = parseDate(s)
-      if (!d) return s
-      var pad = function(n) { return n < 10 ? '0' + n : '' + n }
-      return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes())
-    }
-  }
-}
-</script>
-
-<style scoped>
-.container {
-  min-height: 100vh;
-  background: #F5F5F5;
-  display: flex;
-  flex-direction: column;
-}
-
-.header {
-  background: #1A0F00;
-  padding: 20rpx 24rpx;
-  text-align: center;
-}
-
-.header-title {
-  color: #fff;
-  font-size: 30rpx;
-  font-weight: 600;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-.filter-scroll {
-  padding: 20rpx 24rpx;
-  background: #fff;
-}
-
-.filter-chips {
-  display: flex;
-  flex-direction: row;
-  gap: 16rpx;
-}
-
-.filter-chip {
-  padding: 12rpx 24rpx;
-  border-radius: 16px;
-  background: #fff;
-  border: 1rpx solid #ddd;
-  flex-shrink: 0;
-}
-
-.filter-chip.active {
-  background: #FFF7ED;
-  border-color: #F97316;
-}
-
-.filter-label {
-  font-size: 24rpx;
-  color: #666;
-}
-
-.filter-chip.active .filter-label {
-  color: #F97316;
-  font-weight: 500;
-}
-
-.reg-list {
-  padding: 20rpx 24rpx;
-  display: flex;
-  flex-direction: column;
-  gap: 20rpx;
-}
-
-.reg-card {
-  background: #fff;
-  border-radius: 12rpx;
-  padding: 24rpx;
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
-}
-
-.reg-info {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-}
-
-.reg-name-row {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-}
-
-.reg-name {
-  font-size: 30rpx;
-  font-weight: 600;
-  color: #333;
-}
-
-.reg-tag {
-  margin-left: 16rpx;
-  font-size: 20rpx;
-  padding: 2rpx 12rpx;
-  border-radius: 6rpx;
-}
-
-.reg-tag-pending {
-  background: #FFF7ED;
-  color: #F59E0B;
-}
-
-.reg-tag-approved {
-  background: #F0FDF4;
-  color: #10B981;
-}
-
-.reg-tag-rejected {
-  background: #F5F5F5;
-  color: #9CA3AF;
-}
-
-.reg-tag-cancelled {
-  background: #F5F5F5;
-  color: #9CA3AF;
-}
-
-.reg-meta {
-  font-size: 24rpx;
-  color: #999;
-  margin-top: 8rpx;
-}
-
-.reg-actions {
-  display: flex;
-  flex-direction: row;
-  gap: 16rpx;
-  margin-left: 20rpx;
-  flex-shrink: 0;
-}
-
-.btn-approve {
-  padding: 8rpx 24rpx;
-  font-size: 24rpx;
-  color: #fff;
-  background: linear-gradient(135deg, #F97316, #FB923C);
-  border: none;
-  border-radius: 20rpx;
-  line-height: 1.5;
-}
-
-.btn-reject {
-  padding: 8rpx 24rpx;
-  font-size: 24rpx;
-  color: #999;
-  background: #fff;
-  border: 1rpx solid #ddd;
-  border-radius: 20rpx;
-  line-height: 1.5;
-}
-
-.empty-state {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  padding: 120rpx 40rpx;
-}
-
-.empty-icon {
-  font-size: 80rpx;
-  margin-bottom: 20rpx;
-}
-
-.empty-title {
-  font-size: 32rpx;
-  font-weight: 600;
-  color: #333;
-  margin-bottom: 12rpx;
-}
-
-.empty-desc {
-  font-size: 26rpx;
-  color: #999;
-}
-
-.loading-state {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  padding: 80rpx 40rpx;
-}
-
-.loading-spinner {
-  width: 60rpx;
-  height: 60rpx;
-  border: 4rpx solid #FDE68A;
-  border-top-color: #F97316;
-  border-radius: 50%;
-  animation: spin 0.8s linear infinite;
-  margin-bottom: 20rpx;
-}
-
-@keyframes spin {
-  to { transform: rotate(360deg); }
-}
-
-.loading-text {
-  font-size: 26rpx;
-  color: #999;
-}
-</style>

+ 0 - 147
cfc-frontend/pages/health/teacher-select.vue

@@ -1,147 +0,0 @@
-<template>
-  <view class="page">
-    <!-- 导航栏 -->
-
-    <!-- 提示 -->
-    <view class="tip-banner">
-      <text class="tip-icon">👩‍🏫</text>
-      <text class="tip-text">选择一位规划师,将在方案生成后为您审核和定制计划</text>
-    </view>
-
-    <!-- 规划师列表 -->
-    <view class="teacher-list" v-if="teachers.length > 0">
-      <view
-        v-for="t in teachers"
-        :key="t.id"
-        class="teacher-card"
-        :class="{ active: selectedTeacherId === t.id }"
-        @click="selectTeacher(t)"
-      >
-        <view class="teacher-avatar">{{ (t.name || '规划师').charAt(0) }}</view>
-        <view class="teacher-info">
-          <text class="teacher-name">{{ t.name || '规划师' }}</text>
-          <text class="teacher-tag" v-if="t.isBound">✓ 已绑定家庭</text>
-        </view>
-        <view class="teacher-check" v-if="selectedTeacherId === t.id">✓</view>
-      </view>
-    </view>
-
-    <!-- 空状态 -->
-    <view class="empty-state" v-else>
-      <text class="empty-icon">🔍</text>
-      <text class="empty-text">暂无可用规划师</text>
-      <text class="empty-hint">请联系管理员为您分配规划师</text>
-    </view>
-
-    <!-- 底部按钮 -->
-    <view class="bottom-bar">
-      <button class="btn-next" :disabled="!selectedTeacherId" @click="confirmSelection">
-        {{ selectedTeacherId ? '确认选择' : '请选择规划师' }}
-      </button>
-    </view>
-  </view>
-</template>
-
-<script>
-import { getAvailableTeachers } from '../../utils/api'
-
-export default {
-  data() {
-    return {
-      teachers: [],
-      selectedTeacherId: null,
-      familyId: null,
-      loading: false
-    }
-  },
-  onLoad: function(options) {
-    this.familyId = options.familyId ? parseInt(options.familyId) : null
-    if (!this.familyId) {
-      this.familyId = uni.getStorageSync('familyId') ? parseInt(uni.getStorageSync('familyId')) : null
-    }
-    if (!this.familyId) {
-      uni.showToast({ title: '缺少家庭信息', icon: 'none' })
-      setTimeout(() => uni.navigateBack(), 1000)
-      return
-    }
-    this.loadTeachers()
-  },
-  methods: {
-    async loadTeachers() {
-      this.loading = true
-      try {
-        var self = this
-        getAvailableTeachers({ familyId: this.familyId }).then(function(res) {
-          self.loading = false
-          if (res.code === 200) {
-            self.teachers = res.data || []
-          } else {
-            uni.showToast({ title: res.message || '加载失败', icon: 'none' })
-          }
-        }).catch(function() {
-          self.loading = false
-          uni.showToast({ title: '加载失败,请重试', icon: 'none' })
-        })
-      } catch(e) {
-        this.loading = false
-      }
-    },
-    selectTeacher: function(t) {
-      this.selectedTeacherId = t.id
-    },
-    confirmSelection: function() {
-      if (!this.selectedTeacherId) return
-      uni.setStorageSync('healthPlan_teacherId', this.selectedTeacherId)
-      uni.navigateBack()
-    },
-    goBack: function() {
-      uni.navigateBack()
-    }
-  }
-}
-</script>
-
-<style scoped>
-.page { min-height: 100vh; background: #F3F7FA; padding-bottom: 120rpx; }
-.nav-placeholder { width: 80rpx; }
-.tip-banner {
-  display: flex; align-items: center; gap: 12rpx;
-  margin: 24rpx 32rpx; padding: 20rpx 24rpx;
-  background: #FEF3C7; border-radius: 16rpx;
-}
-.tip-icon { font-size: 36rpx; }
-.tip-text { font-size: 26rpx; color: #92400E; flex: 1; }
-.teacher-list { padding: 0 32rpx; }
-.teacher-card {
-  display: flex; align-items: center; gap: 20rpx;
-  background: #fff; border-radius: 16rpx; padding: 24rpx;
-  margin-bottom: 16rpx; border: 2rpx solid #E5E7EB;
-}
-.teacher-card.active { border-color: #3B82F6; background: #EFF6FF; }
-.teacher-avatar {
-  width: 80rpx; height: 80rpx; border-radius: 50%;
-  background: linear-gradient(135deg, #3B82F6, #60A5FA);
-  color: #fff; font-size: 32rpx; font-weight: bold;
-  display: flex; align-items: center; justify-content: center;
-}
-.teacher-info { flex: 1; }
-.teacher-name { font-size: 30rpx; font-weight: bold; color: #333; display: block; }
-.teacher-tag { font-size: 22rpx; color: #10B981; margin-top: 4rpx; display: block; }
-.teacher-check { font-size: 36rpx; color: #3B82F6; font-weight: bold; }
-.empty-state { text-align: center; padding: 120rpx 40rpx; }
-.empty-icon { font-size: 80rpx; display: block; margin-bottom: 20rpx; }
-.empty-text { font-size: 30rpx; color: #666; display: block; }
-.empty-hint { font-size: 24rpx; color: #999; margin-top: 12rpx; display: block; }
-.bottom-bar {
-  position: fixed; bottom: 0; left: 0; right: 0;
-  padding: 24rpx 32rpx; background: #fff;
-  border-top: 1rpx solid #E5E7EB;
-}
-.btn-next {
-  width: 100%; height: 88rpx; line-height: 88rpx;
-  background: linear-gradient(135deg, #3B82F6, #2563EB);
-  color: #fff; font-size: 32rpx; font-weight: bold;
-  border-radius: 44rpx; border: none;
-}
-.btn-next[disabled] { background: #CBD5E1; }
-</style>

+ 0 - 395
cfc-frontend/pages/member-detail/service-role-apply.vue

@@ -1,395 +0,0 @@
-<template>
-  <view class="container">
-    <!-- 顶部导航 -->
-
-    <!-- 加载状态 -->
-    <view class="loading" v-if="loading">加载中...</view>
-
-    <template v-else-if="applyStatus === 'pending' || applyStatus === 'rejected'">
-      <!-- 已申请状态 -->
-      <view class="status-section">
-        <view class="status-icon">{{ applyStatus === 'pending' ? '⏳' : '❌' }}</view>
-        <text class="status-title">{{ applyStatus === 'pending' ? '申请审核中' : '申请被驳回' }}</text>
-        <text class="status-desc">{{ applyStatus === 'pending' ? '您的申请正在审核中,请耐心等待' : applyMessage || '请修改后重新申请' }}</text>
-        <view class="status-info" v-if="applyData">
-          <text class="info-text">申请时间:{{ formatTime(applyData.createdAt) }}</text>
-        </view>
-        <view class="btn-group">
-          <button class="btn-primary" v-if="applyStatus === 'rejected'" @click="editApplication">重新申请</button>
-          <button class="btn-secondary" @click="goBack">返回首页</button>
-        </view>
-      </view>
-    </template>
-
-    <!-- 申请表单 -->
-    <view v-else class="form-section">
-      <view class="form-card">
-        <view class="form-title">基本信息</view>
-        
-        <view class="form-item">
-          <text class="label">姓名 <text class="required">*</text></text>
-          <input class="input" v-model="form.name" placeholder="请输入真实姓名" />
-        </view>
-
-        <view class="form-item">
-          <text class="label">手机号 <text class="required">*</text></text>
-          <input class="input" type="number" v-model="form.phone" placeholder="请输入手机号" maxlength="11" />
-        </view>
-
-        <view class="form-item">
-          <text class="label">选择角色 <text class="required">*</text></text>
-          <picker :range="roleOptions" range-key="label" @change="onRoleChange">
-            <view class="picker">
-              <text>{{ form.roleLabel || '请选择角色' }}</text>
-              <text class="picker-arrow">›</text>
-            </view>
-          </picker>
-        </view>
-
-        <view class="form-item">
-          <text class="label">身份证照片 <text class="required">*</text></text>
-          <view class="upload-area" @click="uploadIdCard">
-            <image v-if="form.idCardUrl" :src="form.idCardUrl" mode="aspectFit" class="upload-preview" />
-            <text v-else class="upload-placeholder">+ 上传身份证</text>
-          </view>
-        </view>
-
-        <view class="form-item">
-          <text class="label">资质证书</text>
-          <view class="upload-area" @click="uploadCertificate">
-            <image v-if="form.certificateUrl" :src="form.certificateUrl" mode="aspectFit" class="upload-preview" />
-            <text v-else class="upload-placeholder">+ 上传资质证书(选填)</text>
-          </view>
-        </view>
-
-        <view class="form-item">
-          <text class="label">申请说明</text>
-          <textarea class="textarea" v-model="form.remark" placeholder="请简述您的资质和经验(选填)" maxlength="200" />
-        </view>
-      </view>
-
-      <view class="tips-card">
-        <text class="tips-title">申请须知</text>
-        <text class="tips-text">• 请确保填写信息真实有效</text>
-        <text class="tips-text">• 审核时间一般为1-3个工作日</text>
-        <text class="tips-text">• 审核通过后将获得相应角色权限</text>
-      </view>
-
-      <button class="submit-btn" :disabled="submitting" @click="submitApplication">
-        {{ submitting ? '提交中...' : '提交申请' }}
-      </button>
-    </view>
-  </view>
-</template>
-
-<script>
-import { submitServiceRoleApply, getMyServiceRoleApply, uploadServiceRoleImage } from '@/utils/api'
-
-export default {
-  data() {
-    return {
-      loading: false,
-      submitting: false,
-      applyStatus: null,
-      applyData: null,
-      applyMessage: '',
-      form: {
-        name: '',
-        phone: '',
-        role: '',
-        roleLabel: '',
-        idCardUrl: '',
-        certificateUrl: '',
-        remark: ''
-      },
-      roleOptions: [
-        { label: '成长规划师', value: 'teacher' },
-        { label: '营养师', value: 'nutritionist' },
-        { label: '健康管家', value: 'butler' }
-      ]
-    }
-  },
-  onLoad() {
-    this.loadApplication()
-  },
-  methods: {
-    loadApplication() {
-      this.loading = true
-      getMyServiceRoleApply().then(res => {
-        if (res.code === 200 && res.data) {
-          this.applyStatus = res.data.status
-          this.applyData = res.data
-          if (res.data.status === 'rejected') {
-            this.applyMessage = res.data.rejectReason || '申请未通过,请修改后重新申请'
-          }
-        } else if (res.code === 404) {
-          this.applyStatus = null
-        }
-      }).catch(() => {
-        this.applyStatus = null
-      }).finally(() => {
-        this.loading = false
-      })
-    },
-    onRoleChange(e) {
-      const idx = e.detail.value
-      this.form.role = this.roleOptions[idx].value
-      this.form.roleLabel = this.roleOptions[idx].label
-    },
-    uploadIdCard() {
-      this.uploadImage('idCard')
-    },
-    uploadCertificate() {
-      this.uploadImage('certificate')
-    },
-    uploadImage(field) {
-      uni.chooseImage({
-        count: 1,
-        sizeType: ['compressed'],
-        sourceType: ['album', 'camera'],
-        success: (res) => {
-          const tempFilePath = res.tempFilePaths[0]
-          uni.showLoading({ title: '上传中...' })
-          uploadServiceRoleImage(tempFilePath).then(uploadRes => {
-            uni.hideLoading()
-            if (uploadRes.code === 200) {
-              this.form[field + 'Url'] = uploadRes.data.url
-            }
-          }).catch(() => {
-            uni.hideLoading()
-            uni.showToast({ title: '上传失败', icon: 'none' })
-          })
-        }
-      })
-    },
-    submitApplication() {
-      if (!this.form.name) {
-        return uni.showToast({ title: '请输入姓名', icon: 'none' })
-      }
-      if (!this.form.phone || this.form.phone.length !== 11) {
-        return uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
-      }
-      if (!this.form.role) {
-        return uni.showToast({ title: '请选择角色', icon: 'none' })
-      }
-      if (!this.form.idCardUrl) {
-        return uni.showToast({ title: '请上传身份证照片', icon: 'none' })
-      }
-
-      this.submitting = true
-      submitServiceRoleApply({
-        name: this.form.name,
-        phone: this.form.phone,
-        role: this.form.role,
-        idCardUrl: this.form.idCardUrl,
-        certificateUrl: this.form.certificateUrl,
-        remark: this.form.remark
-      }).then(res => {
-        this.submitting = false
-        if (res.code === 200) {
-          uni.showToast({ title: '申请已提交', icon: 'success' })
-          setTimeout(() => {
-            this.loadApplication()
-          }, 1500)
-        } else {
-          uni.showToast({ title: res.message || '提交失败', icon: 'none' })
-        }
-      }).catch(() => {
-        this.submitting = false
-        uni.showToast({ title: '提交失败,请重试', icon: 'none' })
-      })
-    },
-    editApplication() {
-      this.applyStatus = null
-      this.applyMessage = ''
-    },
-    goBack() {
-      uni.navigateBack()
-    },
-    formatTime(time) {
-      if (!time) return ''
-      return time.replace('T', ' ').substring(0, 16)
-    }
-  }
-}
-</script>
-
-<style scoped>
-.container {
-  min-height: 100vh;
-  background: #f5f7fa;
-}
-.nav-placeholder {
-  width: 60rpx;
-}
-.loading {
-  text-align: center;
-  padding: 100rpx;
-  color: #999;
-}
-.status-section {
-  background: #fff;
-  margin: 30rpx;
-  border-radius: 20rpx;
-  padding: 60rpx 40rpx;
-  text-align: center;
-}
-.status-icon {
-  font-size: 80rpx;
-  margin-bottom: 20rpx;
-}
-.status-title {
-  display: block;
-  font-size: 36rpx;
-  font-weight: 600;
-  color: #333;
-  margin-bottom: 16rpx;
-}
-.status-desc {
-  display: block;
-  font-size: 28rpx;
-  color: #666;
-  margin-bottom: 30rpx;
-}
-.status-info {
-  margin-bottom: 40rpx;
-}
-.info-text {
-  font-size: 24rpx;
-  color: #999;
-}
-.btn-group {
-  display: flex;
-  gap: 20rpx;
-  justify-content: center;
-}
-.btn-primary {
-  background: linear-gradient(135deg, #F97316, #FB923C);
-  color: #fff;
-  border: none;
-  padding: 20rpx 60rpx;
-  border-radius: 40rpx;
-  font-size: 28rpx;
-}
-.btn-secondary {
-  background: #f5f7fa;
-  color: #666;
-  border: none;
-  padding: 20rpx 60rpx;
-  border-radius: 40rpx;
-  font-size: 28rpx;
-}
-.form-section {
-  padding: 30rpx;
-}
-.form-card {
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 30rpx;
-  margin-bottom: 30rpx;
-}
-.form-title {
-  font-size: 32rpx;
-  font-weight: 600;
-  color: #333;
-  margin-bottom: 30rpx;
-  padding-bottom: 20rpx;
-  border-bottom: 1rpx solid #f0f0f0;
-}
-.form-item {
-  margin-bottom: 30rpx;
-}
-.label {
-  display: block;
-  font-size: 28rpx;
-  color: #333;
-  margin-bottom: 16rpx;
-}
-.required {
-  color: #ff4d4f;
-}
-.input {
-  width: 100%;
-  height: 80rpx;
-  border: 1rpx solid #e8e8e8;
-  border-radius: 12rpx;
-  padding: 0 24rpx;
-  font-size: 28rpx;
-  box-sizing: border-box;
-}
-.picker {
-  width: 100%;
-  height: 80rpx;
-  border: 1rpx solid #e8e8e8;
-  border-radius: 12rpx;
-  padding: 0 24rpx;
-  font-size: 28rpx;
-  color: #333;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-.picker-arrow {
-  color: #999;
-}
-.upload-area {
-  width: 200rpx;
-  height: 200rpx;
-  border: 2rpx dashed #d9d9d9;
-  border-radius: 12rpx;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  overflow: hidden;
-}
-.upload-preview {
-  width: 100%;
-  height: 100%;
-}
-.upload-placeholder {
-  font-size: 24rpx;
-  color: #999;
-  text-align: center;
-}
-.textarea {
-  width: 100%;
-  height: 160rpx;
-  border: 1rpx solid #e8e8e8;
-  border-radius: 12rpx;
-  padding: 20rpx 24rpx;
-  font-size: 28rpx;
-  box-sizing: border-box;
-}
-.tips-card {
-  background: #fffbe6;
-  border-radius: 12rpx;
-  padding: 24rpx;
-  margin-bottom: 30rpx;
-}
-.tips-title {
-  display: block;
-  font-size: 28rpx;
-  font-weight: 600;
-  color: #fa8c16;
-  margin-bottom: 16rpx;
-}
-.tips-text {
-  display: block;
-  font-size: 24rpx;
-  color: #666;
-  line-height: 1.8;
-}
-.submit-btn {
-  background: linear-gradient(135deg, #F97316, #FB923C);
-  color: #fff;
-  border: none;
-  border-radius: 40rpx;
-  padding: 28rpx;
-  font-size: 32rpx;
-  font-weight: 600;
-  margin-bottom: 60rpx;
-}
-.submit-btn[disabled] {
-  opacity: 0.6;
-}
-</style>

+ 1 - 1
cfc-frontend/pages/mind-detail/index.vue

@@ -231,7 +231,7 @@ components: { TabTransition, PageBanner, RadarChart, FamilyEnergyBar, PsychCrisi
       // 功能入口
       // 功能入口
       funcList: [
       funcList: [
         { icon: '\u{1F4D6}', label: '阅读', needLogin: false, page: '/pages/article-center/index?dimension=mind' },
         { icon: '\u{1F4D6}', label: '阅读', needLogin: false, page: '/pages/article-center/index?dimension=mind' },
-        { icon: '\u{1F4CA}', label: '测评报告', needLogin: true, page: '/pages/mind/emotion-report' },
+        { icon: '\u{1F4CA}', label: '测评报告', needLogin: true, page: '/pages/mind-detail/emotion-report' },
         { icon: '\u{1F4DD}', label: '成长记录', needLogin: true, page: '/pages/growth-main/index' },
         { icon: '\u{1F4DD}', label: '成长记录', needLogin: true, page: '/pages/growth-main/index' },
         { icon: '\u{2728}', label: '情绪打卡', needLogin: true, page: '/pages/mind-detail/emotion-checkin' },
         { icon: '\u{2728}', label: '情绪打卡', needLogin: true, page: '/pages/mind-detail/emotion-checkin' },
         { icon: '\u{1F4C8}', label: '情绪趋势', needLogin: true, page: '/pages/mind-detail/emotion-trend' },
         { icon: '\u{1F4C8}', label: '情绪趋势', needLogin: true, page: '/pages/mind-detail/emotion-trend' },

+ 0 - 706
cfc-frontend/pages/mind-extra/articles.vue

@@ -1,706 +0,0 @@
-<template>
-  <view class="container">
-    <!-- 自定义导航栏 -->
-    <view class="nav-header">
-      <view class="nav-left" @click="onBack">
-        <text class="nav-back-icon">&#x2190;</text>
-      </view>
-      <view class="nav-search" @click="focusSearch">
-        <text class="nav-search-icon">&#x1F50D;</text>
-        <input
-          v-if="searchFocused || searchKeyword"
-          class="nav-search-input"
-          v-model="searchKeyword"
-          placeholder="搜索文章..."
-          confirm-type="search"
-          @confirm="onSearch"
-          @blur="onSearchBlur"
-        />
-        <text v-else class="nav-search-placeholder">搜索文章...</text>
-      </view>
-      <view class="nav-right" @click="goCreate">
-        <text class="nav-add-btn">+</text>
-      </view>
-    </view>
-    <scroll-view scroll-y class="page-scroll" @scrolltolower="onLoadMore">
-<!-- 维度筛选 -->
-      <view class="filter-section">
-        <scroll-view scroll-x enable-flex show-scrollbar="false" class="dimension-scroll">
-          <view class="filter-chips">
-            <view
-              v-for="dim in dimensionList"
-              :key="dim.code"
-              :class="['filter-chip', currentDimension === dim.code ? 'active' : '']"
-              :style="dim.activeStyle"
-              @click="onDimensionChange(dim.code)"
-            >
-              {{ dim.label }}
-            </view>
-          </view>
-        </scroll-view>
-      </view>
-
-      <!-- 分类筛选 -->
-      <view class="filter-section" v-if="categoryList.length > 1">
-        <scroll-view scroll-x enable-flex show-scrollbar="false" class="category-scroll">
-          <view class="filter-chips">
-            <view
-              v-for="cat in categoryList"
-              :key="cat.value"
-              :class="['filter-chip', currentCategory === cat.value ? 'active' : '']"
-              @click="onCategoryChange(cat.value)"
-            >
-              {{ cat.label }}
-            </view>
-          </view>
-        </scroll-view>
-      </view>
-
-      <!-- 全部文章 -->
-      <view class="all-articles-section">
-        <view class="section-header">
-          <text class="section-title">全部文章</text>
-          <text class="section-subtitle" v-if="total > 0">共 {{ total }} 篇</text>
-        </view>
-
-        <view v-if="loading && articles.length === 0" class="loading-wrap">
-          <text class="loading-text">加载中...</text>
-        </view>
-        <view v-else-if="articles.length === 0" class="empty-wrap">
-          <text class="empty-icon">📚</text>
-          <text class="empty-text">暂无相关知识</text>
-        </view>
-        <view v-else class="article-grid">
-          <view
-            v-for="item in articles"
-            :key="item.id"
-            class="article-card"
-            @click="goDetail(item.id)"
-          >
-            <image
-              class="article-cover"
-              :src="getImageUrl(item.coverImage) || '/static/default-article.png'"
-              mode="aspectFill"
-            />
-            <view class="article-body">
-              <view class="article-meta">
-                <text class="article-category">{{ item.categoryName || '知识' }}</text>
-                <view class="article-type-tags">
-                  <text v-if="item.contentType" class="type-tag" :class="'type-' + item.contentType">{{ contentTypeLabel(item.contentType) }}</text>
-                  <text v-if="item.difficultyLevel > 0" class="difficulty-tag">{{ '★'.repeat(item.difficultyLevel) }}</text>
-                </view>
-              </view>
-              <text class="article-title">{{ item.title }}</text>
-              <text class="article-summary" v-if="item.summary">{{ item.summary }}</text>
-              <view class="article-footer">
-                <text class="article-author">{{ item.author || '浠艾福' }}</text>
-                <text class="article-read-count">阅读 {{ item.readCount || 0 }}</text>
-              </view>
-            </view>
-          </view>
-        </view>
-        <view v-if="loadingMore" class="loading-more">
-          <text class="loading-text">加载中...</text>
-        </view>
-        <view v-if="noMore && articles.length > 0" class="no-more">
-          <text class="no-more-text">— 没有更多了 —</text>
-        </view>
-        <view v-if="!isLoggedIn" class="login-hint-bar">
-          <text class="login-hint-text">🔒 登录后解锁全部内容</text>
-        </view>
-      </view>
-
-      <view class="bottom-spacer"></view>
-    </scroll-view>
-  </view>
-</template>
-
-<script>
-import { getArticleCategories, getArticleList, getFeaturedArticles } from '../../utils/api.js'
-import config from '@/config.js'
-
-var dimensionConfig = {
-  all:   { label: '全部', color: '#64748B', theme: 'mind-wisdom', tagline: '探索知识,滋养成长', quote: '学而时习之,不亦说乎' },
-  body:  { label: '身', color: '#FF8C42', theme: 'health',       tagline: '主动健康,从知识开始', quote: '健康是人生的第一财富' },
-  mind:  { label: '心', color: '#FF6B9D', theme: 'mind-wisdom',  tagline: '探索内心世界,培养健康心智', quote: '知己知彼,百战不殆' },
-  wisdom: { label: '智', color: '#6366F1', theme: 'wisdom',       tagline: '聪明学习,快乐成长', quote: '学而不思则罔,思而不学则殆' },
-  action:{ label: '行', color: '#10B981', theme: 'action',       tagline: '知行合一,方得始终', quote: '纸上得来终觉浅,绝知此事要躬行' },
-  wealth:{ label: '富', color: '#F59E0B', theme: 'wealth',       tagline: '理财智慧,从小培养', quote: '君子爱财,取之有道' }
-}
-
-export default {
-  components: { },
-  data() {
-    return {
-      currentDimension: 'all',
-      categoryList: [{ id: '', name: '全部' }],
-      categoryMap: {},
-      currentCategory: '',
-      articles: [],
-      featuredArticles: [],
-      page: 1,
-      size: 10,
-      total: 0,
-      loading: false,
-      loadingMore: false,
-      noMore: false,
-      isLoggedIn: false,
-      searchKeyword: '',
-      searchFocused: false
-    }
-  },
-  computed: {
-    dimensionList: function() {
-      var self = this
-      return Object.keys(dimensionConfig).map(function(k) {
-        var cfg = dimensionConfig[k]
-        var activeStyle = ''
-        if (k === self.currentDimension) {
-          activeStyle = 'background:' + cfg.color + ';color:#fff;'
-        }
-        return { code: k, label: cfg.label, color: cfg.color, activeStyle: activeStyle }
-      })
-    },
-    bannerTheme: function() {
-      return dimensionConfig[this.currentDimension] ? dimensionConfig[this.currentDimension].theme : 'mind-wisdom'
-    },
-    bannerTagline: function() {
-      return dimensionConfig[this.currentDimension] ? dimensionConfig[this.currentDimension].tagline : ''
-    },
-    bannerQuote: function() {
-      return dimensionConfig[this.currentDimension] ? dimensionConfig[this.currentDimension].quote : ''
-    }
-  },
-  onLoad: function(options) {
-    this.isLoggedIn = !!uni.getStorageSync('token')
-    this.loadFeaturedArticles()
-    if (options && options.dimension && dimensionConfig[options.dimension]) {
-      this.currentDimension = options.dimension
-    }
-    this.loadCategories()
-  },
-  onShow: function() {
-    this.isLoggedIn = !!uni.getStorageSync('token')
-  },
-  methods: {
-    loadFeaturedArticles: function() {
-      var self = this
-      getFeaturedArticles({ size: 5 }).then(function(res) {
-        if (res.code === 200 && res.data) {
-          var gradientColors = [
-            'linear-gradient(135deg, #6366F1, #818CF8)',
-            'linear-gradient(135deg, #8B5CF6, #A78BFA)',
-            'linear-gradient(135deg, #5B9BD5, #8FC5E8)',
-            'linear-gradient(135deg, #10B981, #34D399)',
-            'linear-gradient(135deg, #F97316, #FB923C)'
-          ]
-          var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
-          self.featuredArticles = list.map(function(item, index) {
-            return {
-              id: item.id,
-              category: item.categoryName || '成长文章',
-              categoryColor: gradientColors[index % gradientColors.length],
-              title: item.title || '',
-              summary: item.summary || '',
-              views: item.viewCount ? String(item.viewCount) : '0',
-              date: item.publishedAt ? item.publishedAt.slice(0, 10) : ''
-            }
-          })
-        }
-      }).catch(function() {})
-    },
-    contentTypeLabel: function(type) {
-      var labels = { article: '文章', knowledge: '知识点', course: '课程', tip: '贴士' }
-      return labels[type] || type
-    },
-    async loadCategories() {
-      try {
-        const res = await getArticleCategories()
-        if (res.code === 200 && res.data) {
-          const cats = Array.isArray(res.data) ? res.data : []
-          var map = {}
-          cats.forEach(function(c) { map[c.id] = c.name })
-          this.categoryMap = map
-          this.categoryList = [{ id: '', name: '全部' }].concat(cats)
-        }
-      } catch (e) {
-        this.categoryList = [
-          { id: '', name: '全部' },
-          { id: 1, name: '心理健康' },
-          { id: 2, name: '情绪管理' }
-        ]
-      }
-      this.loadArticles()
-    },
-    onDimensionChange: function(code) {
-      this.currentDimension = code
-      this.page = 1
-      this.articles = []
-      this.noMore = false
-      this.loadArticles()
-    },
-    onCategoryChange: function(id) {
-      this.currentCategory = id
-      this.page = 1
-      this.articles = []
-      this.noMore = false
-      this.loadArticles()
-    },
-    async loadArticles() {
-      if (this.loading) return
-      this.loading = true
-      var self = this
-      try {
-        var params = { page: this.page, size: this.size }
-        if (this.currentCategory) {
-          params.categoryId = this.currentCategory
-        }
-        if (this.currentDimension !== 'all') {
-          params.dimensionCode = this.currentDimension
-        }
-        if (this.searchKeyword && this.searchKeyword.trim()) {
-          params.keyword = this.searchKeyword.trim()
-        }
-        const res = await getArticleList(params)
-        if (res.code === 200 && res.data) {
-          var pageData = res.data
-          var list = pageData.records || []
-          var mapped = list.map(function(item) {
-            return {
-              id: item.id,
-              title: item.title || '',
-              summary: self.stripHtml(item.summary || item.content),
-              coverImage: item.coverImage || '',
-              author: item.author || '浠艾福',
-              categoryName: item.categoryName || item.category || '',
-              publishDate: item.publishedAt ? item.publishedAt.slice(0, 10) : '',
-              readCount: item.viewCount || 0,
-              contentType: item.contentType || '',
-              difficultyLevel: item.difficultyLevel || 0
-            }
-          })
-          if (this.page === 1) {
-            this.articles = mapped
-          } else {
-            this.articles = this.articles.concat(mapped)
-          }
-          this.total = pageData.total || 0
-          this.noMore = this.articles.length >= this.total
-        } else {
-          if (this.page === 1) {
-            this.articles = []
-          }
-        }
-      } catch (e) {
-        uni.showToast({ title: '加载失败', icon: 'none' })
-      } finally {
-        this.loading = false
-      }
-    },
-    onLoadMore() {
-      if (this.noMore || this.loading) return
-      this.page++
-      this.loadArticles()
-    },
-    goDetail(id) {
-      uni.navigateTo({ url: '/pages/article-center/article-detail?id=' + id })
-    },
-    stripHtml: function(html) {
-      if (!html) return ''
-      return html
-        .replace(/<style[\s\S]*?<\/style>/gi, ' ')
-        .replace(/<script[\s\S]*?<\/script>/gi, ' ')
-        .replace(/<[^>]+>/g, ' ')
-        .replace(/&nbsp;/gi, ' ')
-        .replace(/&lt;/gi, '<')
-        .replace(/&gt;/gi, '>')
-        .replace(/&amp;/gi, '&')
-        .replace(/&quot;/gi, '"')
-        .replace(/\s+/g, ' ')
-        .trim()
-    },
-    goCreate() {
-      if (this.isLoggedIn) {
-        uni.navigateTo({ url: '/pages/article-center/article-edit' })
-      } else {
-        uni.navigateTo({ url: '/pages/login/login?redirect=' + encodeURIComponent('/pages/mind-extra/articles') })
-      }
-    },
-    onBack() {
-      uni.navigateBack()
-    },
-    focusSearch: function() {
-      this.searchFocused = true
-    },
-    onSearch: function() {
-      this.page = 1
-      this.articles = []
-      this.noMore = false
-      this.loadArticles()
-    },
-    onSearchBlur: function() {
-      if (!this.searchKeyword) {
-        this.searchFocused = false
-      }
-    },
-    getImageUrl: function(path) {
-      return config.API_BASE_URL + path
-    }
-  }
-}
-</script>
-
-<style scoped>
-.container {
-  display: flex;
-  flex-direction: column;
-  height: 100vh;
-  background: #f5f7fa;
-}
-
-/* 自定义导航栏 */
-.nav-header {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding: 80rpx 24rpx 16rpx;
-  background: linear-gradient(135deg, #667eea, #764ba2);
-  position: relative;
-  z-index: 20;
-}
-.nav-left {
-  width: 60rpx;
-  height: 60rpx;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-.nav-back-icon {
-  font-size: 36rpx;
-  color: #fff;
-  font-weight: bold;
-}
-.nav-search {
-  flex: 1;
-  display: flex;
-  align-items: center;
-  background: rgba(255,255,255,0.25);
-  border-radius: 32rpx;
-  padding: 10rpx 20rpx;
-  margin: 0 16rpx;
-}
-.nav-search-icon {
-  font-size: 24rpx;
-  margin-right: 10rpx;
-  flex-shrink: 0;
-}
-.nav-search-placeholder {
-  font-size: 24rpx;
-  color: rgba(255,255,255,0.7);
-}
-.nav-search-input {
-  flex: 1;
-  font-size: 24rpx;
-  color: #fff;
-  background: transparent;
-}
-.nav-search-input::placeholder {
-  color: rgba(255,255,255,0.7);
-}
-.nav-right {
-  width: 60rpx;
-  height: 60rpx;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-.nav-add-btn {
-  font-size: 48rpx;
-  color: #fff;
-  font-weight: 300;
-  line-height: 1;
-}
-
-/* 页面滚动容器 */
-.page-scroll {
-  flex: 1;
-}
-
-/* 区块通用标题 */
-.section-header {
-  display: flex;
-  align-items: baseline;
-  padding: 0 4rpx;
-  margin-bottom: 20rpx;
-}
-.section-title {
-  font-size: 30rpx;
-  font-weight: bold;
-  color: #333;
-}
-.section-subtitle {
-  font-size: 24rpx;
-  color: #999;
-  margin-left: 12rpx;
-}
-
-/* ===== 推荐阅读 ===== */
-.featured-section {
-  margin: 24rpx 24rpx 16rpx;
-  padding: 0 4rpx;
-}
-.featured-grid {
-  display: flex;
-  flex-direction: column;
-}
-.featured-card {
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 28rpx 24rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
-  margin-bottom: 20rpx;
-}
-.featured-card:active {
-  opacity: 0.8;
-}
-.featured-category {
-  display: inline-block;
-  padding: 4rpx 18rpx;
-  border-radius: 20rpx;
-  margin-bottom: 14rpx;
-}
-.featured-category-text {
-  font-size: 20rpx;
-  color: #fff;
-  font-weight: 500;
-}
-.featured-title {
-  font-size: 28rpx;
-  font-weight: bold;
-  color: #333;
-  display: block;
-  margin-bottom: 10rpx;
-  line-height: 1.4;
-}
-.featured-summary {
-  font-size: 24rpx;
-  color: #888;
-  line-height: 1.5;
-  display: block;
-  margin-bottom: 16rpx;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  display: -webkit-box;
-  -webkit-line-clamp: 2;
-  -webkit-box-orient: vertical;
-}
-.featured-meta {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-}
-.featured-views {
-  font-size: 22rpx;
-  color: #bbb;
-  margin-right: 20rpx;
-}
-.featured-date {
-  font-size: 22rpx;
-  color: #bbb;
-  margin-left: auto;
-}
-
-/* ===== 全部文章 ===== */
-.all-articles-section {
-  margin: 8rpx 24rpx 0;
-  padding: 0 4rpx;
-}
-
-/* 筛选条 */
-.filter-section {
-  background: #fff;
-  padding: 16rpx 0 12rpx;
-  border-bottom: 1rpx solid #eee;
-}
-.dimension-scroll,
-.category-scroll {
-  white-space: nowrap;
-}
-.dimension-scroll {
-  border-bottom: 1rpx solid #f0f0f0;
-  padding-bottom: 12rpx;
-}
-.filter-chips {
-  display: flex;
-  padding: 0 20rpx;
-  gap: 16rpx;
-}
-.filter-chip {
-  display: inline-block;
-  padding: 8rpx 24rpx;
-  font-size: 24rpx;
-  color: #666;
-  background: #f0f0f0;
-  border-radius: 30rpx;
-  flex-shrink: 0;
-}
-.filter-chip.active {
-  background: #5B9BD5;
-  color: #fff;
-}
-.filter-chip:first-child {
-  margin-left: 4rpx;
-}
-
-/* 文章元信息标签 */
-.article-meta {
-  display: flex;
-  align-items: center;
-  flex-wrap: wrap;
-  margin-bottom: 10rpx;
-}
-.article-type-tags {
-  display: flex;
-  align-items: center;
-  gap: 8rpx;
-  margin-left: auto;
-}
-.type-tag {
-  font-size: 18rpx;
-  padding: 2rpx 10rpx;
-  border-radius: 6rpx;
-  background: #f0f0f0;
-  color: #666;
-}
-.type-tag.type-article {
-  background: #e8f4fd;
-  color: #2b7fc4;
-}
-.type-tag.type-knowledge {
-  background: #e8f8ee;
-  color: #2a9055;
-}
-.type-tag.type-course {
-  background: #fef3e2;
-  color: #c47a2b;
-}
-.type-tag.type-tip {
-  background: #f3e8fd;
-  color: #7b2bc4;
-}
-.difficulty-tag {
-  font-size: 18rpx;
-  color: #f59e0b;
-}
-
-.loading-wrap,
-.empty-wrap {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  padding-top: 120rpx;
-}
-.loading-text {
-  font-size: 26rpx;
-  color: #999;
-}
-.empty-icon {
-  font-size: 100rpx;
-  margin-bottom: 24rpx;
-}
-.empty-text {
-  font-size: 28rpx;
-  color: #999;
-}
-.article-grid {
-  padding: 20rpx 24rpx;
-}
-.article-card {
-  background: #fff;
-  border-radius: 16rpx;
-  overflow: hidden;
-  margin-bottom: 20rpx;
-  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
-}
-.article-cover {
-  width: 100%;
-  height: 280rpx;
-  background: linear-gradient(135deg, #667eea, #764ba2);
-}
-.article-body {
-  padding: 20rpx;
-}
-.article-category {
-  font-size: 20rpx;
-  color: #5B9BD5;
-  background: rgba(91,155,213,0.1);
-  padding: 2rpx 12rpx;
-  border-radius: 8rpx;
-  margin-right: 12rpx;
-}
-.article-date {
-  font-size: 20rpx;
-  color: #999;
-}
-.article-title {
-  display: block;
-  font-size: 28rpx;
-  font-weight: bold;
-  color: #333;
-  margin-bottom: 8rpx;
-  line-height: 1.4;
-}
-.article-summary {
-  display: block;
-  font-size: 24rpx;
-  color: #666;
-  line-height: 1.5;
-  margin-bottom: 12rpx;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  display: -webkit-box;
-  -webkit-line-clamp: 2;
-  -webkit-box-orient: vertical;
-}
-.article-footer {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-.article-author {
-  font-size: 20rpx;
-  color: #999;
-}
-.article-read-count {
-  font-size: 20rpx;
-  color: #bbb;
-}
-.loading-more,
-.no-more {
-  text-align: center;
-  padding: 30rpx;
-}
-.no-more-text {
-  font-size: 24rpx;
-  color: #ccc;
-}
-
-/* 未登录提示 */
-.login-hint-bar {
-  text-align: center;
-  padding: 20rpx;
-  background: #fef9e7;
-  border-top: 1rpx solid #f0e6c0;
-  margin: 20rpx 24rpx 0;
-  border-radius: 12rpx;
-}
-.login-hint-text {
-  font-size: 24rpx;
-  color: #b8860b;
-}
-
-/* 底部占位 */
-.bottom-spacer {
-  height: 120rpx;
-}
-</style>

+ 0 - 315
cfc-frontend/pages/teacher/appointments.vue

@@ -1,315 +0,0 @@
-<template>
-  <view class="container">
-    <view class="header">
-      <text class="title">预约管理</text>
-      <text class="subtitle">查看并管理测评预约</text>
-    </view>
-
-    <!-- 状态筛选 -->
-    <view class="filter-bar">
-      <view class="filter-item" :class="{ active: filterStatus === null }" @click="filterStatus = null">
-        <text>全部</text>
-      </view>
-      <view class="filter-item" :class="{ active: filterStatus === 0 }" @click="filterStatus = 0">
-        <text>待确认</text>
-      </view>
-      <view class="filter-item" :class="{ active: filterStatus === 1 }" @click="filterStatus = 1">
-        <text>已确认</text>
-      </view>
-      <view class="filter-item" :class="{ active: filterStatus === 2 }" @click="filterStatus = 2">
-        <text>已完成</text>
-      </view>
-    </view>
-
-    <!-- 预约列表 -->
-    <view class="appointment-list">
-      <view class="appointment-card" v-for="item in filteredList" :key="item.id">
-        <view class="card-header">
-          <view class="child-info">
-            <text class="child-name">孩子ID: {{ item.memberId || '未指定' }}</text>
-          </view>
-          <view class="status-badge" :class="'status-' + item.status">
-            <text>{{ getStatusText(item.status) }}</text>
-          </view>
-        </view>
-
-        <view class="card-body">
-          <view class="info-row">
-            <text class="label">预约日期</text>
-            <text class="value">{{ item.appointmentDate || '未设置' }}</text>
-          </view>
-          <view class="info-row">
-            <text class="label">预约时间</text>
-            <text class="value">{{ item.appointmentTime || '未设置' }}</text>
-          </view>
-          <view class="info-row" v-if="item.notes">
-            <text class="label">备注</text>
-            <text class="value">{{ item.notes }}</text>
-          </view>
-          <view class="info-row">
-            <text class="label">创建时间</text>
-            <text class="value">{{ formatTime(item.createdAt) }}</text>
-          </view>
-        </view>
-
-        <view class="card-actions">
-          <button v-if="item.status === 0" class="btn-confirm" size="mini" @click="handleConfirm(item.id)">确认预约</button>
-          <button v-if="item.status === 1" class="btn-complete" size="mini" @click="handleComplete(item.id)">录入结果</button>
-          <button v-if="item.status === 0" class="btn-cancel" size="mini" @click="handleCancel(item.id)">取消</button>
-        </view>
-      </view>
-    </view>
-
-    <view class="empty-state" v-if="filteredList.length === 0">
-      <text class="empty-icon">📋</text>
-      <text class="empty-text">暂无预约记录</text>
-    </view>
-    <view class="bottom-space"></view>
-  </view>
-</template>
-
-<script>
-import { getGuideAppointments, confirmAppointment, cancelAppointment } from '../../utils/api.js'
-import { parseDate } from '../../utils/format.js'
-
-export default {
-  data() {
-    return {
-      appointments: [],
-      filterStatus: null
-    }
-  },
-  computed: {
-    filteredList() {
-      if (this.filterStatus === null) return this.appointments
-      return this.appointments.filter(item => item.status === this.filterStatus)
-    }
-  },
-  onShow() {
-    this.loadAppointments()
-  },
-  methods: {
-    async loadAppointments() {
-      try {
-        const res = await getGuideAppointments()
-        if (res.code === 200) {
-          this.appointments = res.data || []
-        }
-      } catch (e) {
-        console.error('加载预约列表失败', e)
-      }
-    },
-
-    getStatusText(status) {
-      const map = { 0: '待确认', 1: '已确认', 2: '已完成', 3: '已取消', 4: '未到' }
-      return map[status] || '未知'
-    },
-
-    formatTime(ts) {
-      if (!ts) return ''
-      const d = parseDate(ts)
-      if (!d) return ''
-      return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
-    },
-
-    handleConfirm(id) {
-      uni.showModal({
-        title: '确认预约',
-        content: '确认此预约?',
-        success: async (res) => {
-          if (res.confirm) {
-            try {
-              const result = await confirmAppointment(id)
-              if (result.code === 200) {
-                uni.showToast({ title: '已确认', icon: 'success' })
-                this.loadAppointments()
-              }
-            } catch (e) {
-              uni.showToast({ title: '操作失败', icon: 'none' })
-            }
-          }
-        }
-      })
-    },
-
-    handleComplete(id) {
-      uni.navigateTo({ url: '/pages/assessment/record?appointmentId=' + id })
-    },
-
-    handleCancel(id) {
-      uni.showModal({
-        title: '取消预约',
-        content: '确定取消此预约?',
-        success: async (res) => {
-          if (res.confirm) {
-            try {
-              const result = await cancelAppointment(id)
-              if (result.code === 200) {
-                uni.showToast({ title: '已取消', icon: 'success' })
-                this.loadAppointments()
-              }
-            } catch (e) {
-              uni.showToast({ title: '操作失败', icon: 'none' })
-            }
-          }
-        }
-      })
-    }
-  }
-}
-</script>
-
-<style scoped>
-.container {
-  padding: 30rpx;
-  background: #F8F8F8;
-  min-height: 100vh;
-}
-
-.header {
-  text-align: center;
-  padding: 40rpx 0 30rpx;
-}
-
-.title {
-  font-size: 48rpx;
-  font-weight: bold;
-  color: #333;
-  display: block;
-  margin-bottom: 16rpx;
-}
-
-.subtitle {
-  font-size: 26rpx;
-  color: #666;
-}
-
-.filter-bar {
-  display: flex;
-  gap: 16rpx;
-  margin-bottom: 30rpx;
-  padding: 16rpx;
-  background: #fff;
-  border-radius: 20rpx;
-}
-
-.filter-item {
-  flex: 1;
-  text-align: center;
-  padding: 12rpx 0;
-  border-radius: 10rpx;
-  font-size: 26rpx;
-  color: #666;
-  background: #F8F8F8;
-}
-
-.filter-item.active {
-  background: #667eea;
-  color: #fff;
-}
-
-.appointment-list {
-  display: flex;
-  flex-direction: column;
-  gap: 24rpx;
-}
-
-.appointment-card {
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 30rpx;
-}
-
-.card-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 20rpx;
-  padding-bottom: 16rpx;
-  border-bottom: 1rpx solid #eee;
-}
-
-.child-name {
-  font-size: 30rpx;
-  font-weight: bold;
-  color: #333;
-}
-
-.status-badge {
-  padding: 6rpx 16rpx;
-  border-radius: 8rpx;
-  font-size: 22rpx;
-}
-
-.status-0 { background: #FFF3E0; color: #FF9800; }
-.status-1 { background: #E3F2FD; color: #2196F3; }
-.status-2 { background: #E8F5E9; color: #4CAF50; }
-.status-3 { background: #F5F5F5; color: #9E9E9E; }
-.status-4 { background: #FFEBEE; color: #F44336; }
-
-.card-body {
-  margin-bottom: 20rpx;
-}
-
-.info-row {
-  display: flex;
-  justify-content: space-between;
-  padding: 10rpx 0;
-  font-size: 26rpx;
-}
-
-.label { color: #999; }
-.value { color: #333; }
-
-.card-actions {
-  display: flex;
-  gap: 16rpx;
-  padding-top: 16rpx;
-  border-top: 1rpx dashed #eee;
-}
-
-.btn-confirm {
-  flex: 1;
-  background: #667eea;
-  color: #fff;
-  font-size: 26rpx;
-  border-radius: 10rpx;
-}
-
-.btn-complete {
-  flex: 1;
-  background: #4CAF50;
-  color: #fff;
-  font-size: 26rpx;
-  border-radius: 10rpx;
-}
-
-.btn-cancel {
-  flex: 1;
-  background: #F5F5F5;
-  color: #999;
-  font-size: 26rpx;
-  border-radius: 10rpx;
-}
-
-.empty-state {
-  text-align: center;
-  padding: 120rpx 0;
-}
-
-.empty-icon {
-  font-size: 80rpx;
-  display: block;
-  margin-bottom: 20rpx;
-}
-
-.empty-text {
-  font-size: 28rpx;
-  color: #999;
-  display: block;
-}
-
-.bottom-space {
-  height: 140rpx;
-}
-</style>

+ 9 - 0
cfc-frontend/utils/api.js

@@ -2538,6 +2538,15 @@ export const getSelfCheckTrendAnalysis = () => {
   return request('/api/family/self-check/trend-analysis', 'POST', {})
   return request('/api/family/self-check/trend-analysis', 'POST', {})
 }
 }
 
 
+// ===== MBTI 人格测评 =====
+export const getMbtiQuestions = () => request('/api/family/mbti/questions', 'POST', {})
+export const submitMbti = (data) => request('/api/family/mbti/submit', 'POST', data)
+export const getMbtiLatest = () => request('/api/family/mbti/latest', 'POST', {})
+export const getMbtiProfile = (data) => request('/api/family/mbti/profile', 'POST', data)
+
+// ===== 五维手动填分 =====
+export const submitManualScore = (data) => request('/api/family/manual-score/submit', 'POST', data)
+
 // ===== 舌诊相关(两阶段流程) =====
 // ===== 舌诊相关(两阶段流程) =====
 export const parseTonguePreview = (data) => request('/api/health/report/parse-preview?type=tongue', 'POST', data)
 export const parseTonguePreview = (data) => request('/api/health/report/parse-preview?type=tongue', 'POST', data)
 export const confirmTongue = (data) => request('/api/health/report/confirm?type=tongue', 'POST', data)
 export const confirmTongue = (data) => request('/api/health/report/confirm?type=tongue', 'POST', data)