Browse Source

feat: promotion tier system, teacher message unread badge, FamilyEnergyBar collapse

User 2 months ago
parent
commit
95f7e9c096

+ 14 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/TeacherMessageController.java

@@ -100,6 +100,20 @@ public class TeacherMessageController {
         return Result.success(result);
     }
 
+    /**
+     * 成长规划师获取未读消息数
+     */
+    @Operation(summary = "成长规划师获取未读消息数")
+    @PostMapping("/unread-count")
+    public Result<Long> getUnreadCount(@RequestAttribute("userId") Long teacherId,
+                                        @RequestAttribute("role") String role) {
+        if (!"teacher".equals(role)) {
+            return Result.error("只有成长规划师可以查看");
+        }
+        long count = teacherMessageService.getUnreadCount(teacherId);
+        return Result.success(count);
+    }
+
     /**
      * 家长获取自己的咨询消息
      */

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/TeacherMessageService.java

@@ -58,6 +58,15 @@ public class TeacherMessageService {
                 .orderByDesc(TeacherMessage::getCreatedAt));
     }
 
+    /**
+     * 获取成长规划师未读消息数
+     */
+    public long getUnreadCount(Long teacherId) {
+        return teacherMessageMapper.selectCount(new LambdaQueryWrapper<TeacherMessage>()
+                .eq(TeacherMessage::getTeacherId, teacherId)
+                .eq(TeacherMessage::getStatus, "pending"));
+    }
+
     /**
      * 家长获取自己的咨询消息
      */

+ 17 - 3
cfc-frontend/components/FamilyEnergyBar.vue

@@ -1,11 +1,12 @@
 <template>
   <view class="family-energy-bar">
-    <view class="bar-header">
+    <view class="bar-header" @click="toggleMembers">
       <text class="bar-icon">{{ dimensionIcon }}</text>
       <text class="bar-title">{{ dimensionName }}能量</text>
       <text class="bar-total-score">{{ familyScore }}</text>
+      <text class="bar-expand-icon">{{ showMembers ? '▼' : '▶' }}</text>
     </view>
-    <view class="bar-list">
+    <view class="bar-list" v-if="showMembers">
       <!-- 非堆叠模式:单色条 -->
       <template v-if="!isStacked">
         <view class="bar-row bar-row-family">
@@ -76,6 +77,11 @@ export default {
     sandboxData: { type: Object, default: null },
     dualDimension: { type: [String, Object], default: '' }
   },
+  data: function() {
+    return {
+      showMembers: false
+    }
+  },
   computed: {
     isStacked: function() {
       return typeof this.dualDimension === 'object' && this.dualDimension !== null && this.dualDimension.primary && this.dualDimension.secondary
@@ -107,6 +113,9 @@ export default {
     }
   },
   methods: {
+    toggleMembers: function() {
+      this.showMembers = !this.showMembers
+    },
     getMemberScore: function(member) {
       if (this.isStacked) {
         var primary = member[this.dualDimension.primary.code + 'Score'] || 0
@@ -160,7 +169,12 @@ export default {
 }
 .bar-icon { font-size: 32rpx; margin-right: 8rpx; }
 .bar-title { font-size: 28rpx; font-weight: bold; color: #333; flex: 1; }
-.bar-total-score { font-size: 40rpx; font-weight: bold; color: #F97316; }
+.bar-total-score { font-size: 40rpx; font-weight: bold; color: #F97316; margin-right: 12rpx; }
+.bar-expand-icon {
+  font-size: 20rpx;
+  color: #bbb;
+  transition: transform 0.3s ease;
+}
 .bar-row {
   display: flex;
   flex-direction: row;

+ 2 - 14
cfc-frontend/pages/body/index.vue

@@ -37,16 +37,6 @@
       :intimacyMap="intimacyMapForGraph"
       @memberTap="goMemberDetail" />
 
-    <!-- 登录后:用户快捷入口 -->
-    <UserQuickEntry
-      v-if="isLoggedIn"
-      :userName="userName"
-      roleName="家长"
-      :currentChildName="activeChildName"
-      :children="children"
-      @childChanged="onChildChanged"
-      @scrollTo="scrollToSection" />
-
       <!-- 雷达图 -->
       <view class="section" v-if="dimensionData">
         <view class="section-header" v-if="dimensionData.isAverage">
@@ -214,7 +204,6 @@ import TabTransition from '../../components/tab-transition.vue'
 import PageBanner from '../../components/PageBanner.vue'
 import LoginGuideCard from '../../components/LoginGuideCard.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
-import UserQuickEntry from '../../components/UserQuickEntry.vue'
 import DimensionTasks from '../../components/DimensionTasks.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
@@ -255,7 +244,7 @@ const healthRequest = function(url, data) {
 }
 
 export default {
-  components: { TabTransition, PageBanner, LoginGuideCard, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, FamilyRelationGraph, RadarChart, HealthDimensionsSection, AIFloatingAvatar, FamilyRelationshipSection },
+  components: { TabTransition, PageBanner, LoginGuideCard, FamilyEnergyBar, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, FamilyRelationGraph, RadarChart, HealthDimensionsSection, AIFloatingAvatar, FamilyRelationshipSection },
   data() {
     return {
       isLoggedIn: false,
@@ -281,8 +270,7 @@ export default {
         { icon: '\u{1F3C3}', label: '运动', needLogin: true, page: '/pages/body/exercise-record' },
         { icon: '\u{1F957}', label: '饮食', needLogin: true, page: '/pages/body/meal-record' },
         { icon: '\u{1F634}', label: '作息', needLogin: true, page: '/pages/body/sleep-record' },
-        { icon: '\u{1F9D8}', label: '冥想', needLogin: true, page: '/pages/health/meditation-index' },
-        { icon: '\u{1F4CA}', label: '舌诊', needLogin: true, page: '/pages/health/report-upload?reportType=tongue' }
+        { icon: '\u{1F9D8}', label: '冥想', needLogin: true, page: '/pages/health/meditation-index' }
       ],
       contactList: [],
       healthAlerts: [],

+ 7 - 0
cfc-web/src/api/teacherMessage.js

@@ -26,4 +26,11 @@ export function getConsultMessages(page = 1, size = 10, status) {
  */
 export function getMyConsultMessages() {
   return request.post('/api/guide/messages/my')
+}
+
+/**
+ * 成长规划师获取未读消息数(用于右上角铃铛角标)
+ */
+export function getUnreadMessageCount() {
+  return request.post('/api/guide/messages/unread-count')
 }

+ 40 - 3
cfc-web/src/views/Layout.vue

@@ -51,6 +51,9 @@
     <el-container>
       <el-header>
         <div class="header-right">
+          <el-badge :value="unreadMessageCount" :hidden="unreadMessageCount === 0" class="msg-badge">
+            <el-button type="text" class="msg-btn" @click="handleMessageClick" icon="el-icon-bell"></el-button>
+          </el-badge>
           <el-button v-if="hasPerm('service:*')" type="text" class="invite-code-btn" @click="showInviteCode" icon="el-icon-share">
             邀请码
           </el-button>
@@ -146,6 +149,7 @@
 import { getAdminInfo, changePassword } from '@/api/auth'
 import { updateAdminInfo } from '@/api/admin'
 import { getMyInviteCode } from '@/api/guide'
+import { getUnreadMessageCount } from '@/api/teacherMessage'
 import { getEffectivePermissions, hasPermission, getRoleLabel } from '@/utils/permissions'
 
 export default {
@@ -237,10 +241,9 @@ export default {
           children: [
             { path: '/teacher-assessment', label: 'DAN测评', icon: 'el-icon-edit', perm: 'assessment:dan' },
             { path: '/teacher-consult', label: '家长咨询', icon: 'el-icon-chat-dot-round', perm: 'assessment:consult' },
+            { path: '/growth-records', label: '成长记录', icon: 'el-icon-document', perm: 'growth:records' },
+            { path: '/growth-plans', label: '成长计划', icon: 'el-icon-document', perm: 'growth:plans' },
           ]},
-        { path: '/growth-records', label: '成长记录', icon: 'el-icon-document', perm: 'growth:records' },
-        { path: '/growth-plans', label: '成长计划', icon: 'el-icon-document', perm: 'growth:plans' },
-        { path: '/teacher-messages', label: '消息中心', icon: 'el-icon-chat-dot-round', perm: 'messages' },
 
         // ===== 9. 系统配置 (admin) =====
         { title: '系统配置', icon: 'el-icon-s-tools', perm: 'system:*',
@@ -325,6 +328,7 @@ export default {
       inviteCodeVisible: false,
       inviteCodeLoading: false,
       inviteCodeData: null,
+      unreadMessageCount: 0,
       passwordRules: {
         oldPassword: [
           { required: true, message: '请输入原密码', trigger: 'blur' }
@@ -380,6 +384,10 @@ export default {
   mounted() {
     // 清除可能的 beforeunload 处理,防止页面刷新弹出"离开网站?"提示
     window.onbeforeunload = null
+    // 有消息权限(老师角色)才加载未读数
+    if (hasPermission(this.effectivePerms, 'messages')) {
+      this.fetchUnreadCount()
+    }
   },
   methods: {
     hasPerm(required) {
@@ -496,6 +504,19 @@ export default {
       document.body.removeChild(input)
       this.$message.success('已复制邀请码')
     },
+    async fetchUnreadCount() {
+      try {
+        const res = await getUnreadMessageCount()
+        if (res.data !== undefined) {
+          this.unreadMessageCount = res.data || 0
+        }
+      } catch (e) {
+        // silent: not critical
+      }
+    },
+    handleMessageClick() {
+      this.$router.push('/teacher-messages')
+    },
     handleLogout() {
       // 不使用 modal 遮罩(全局 .v-modal { display:none } 会与 $confirm 的遮罩冲突)
       this.$confirm('确定要退出登录吗?', '提示', {
@@ -601,6 +622,22 @@ export default {
   color: #4F46E5;
 }
 
+.msg-badge {
+  margin-right: 4px;
+}
+.msg-badge .el-badge__content.is-fixed {
+  top: 10px;
+  right: 6px;
+}
+.msg-btn {
+  font-size: 18px;
+  color: #606266;
+  padding: 4px;
+}
+.msg-btn:hover {
+  color: #6366F1;
+}
+
 .invite-code-content {
   padding: 8px 0;
 }