|
|
@@ -0,0 +1,1190 @@
|
|
|
+# 家庭天盘·传统文化 + 五维基础能量分 实现计划
|
|
|
+
|
|
|
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
|
|
|
+
|
|
|
+**目标:** 完善家庭天盘传统文化展示设计,基于生辰八字/星座/灵数计算五维基础能量分并落库,提供重算接口,前端三处增强(canvas 节点标注、成员弹窗、传统命理页)。
|
|
|
+
|
|
|
+**架构:** 后端新增 `FiveDimensionScoreService` 计算服务,新建 `family_member_dimension_base_scores` 表存储五维基础分,支持手动/批量重算;前端改造现有天盘页面,在 canvas 节点、成员弹窗、新增传统命理页展示传统文化信息和五维基础分。
|
|
|
+
|
|
|
+**技术栈:** Spring Boot 2.7 + MyBatis-Plus(后端);uni-app Vue 2 Options API(小程序前端);MySQL 8.0
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 文件清单
|
|
|
+
|
|
|
+| 操作 | 文件路径 | 职责 |
|
|
|
+|------|----------|------|
|
|
|
+| 创建 | `cfc-backend/src/main/java/com/etotem/cfc/service/FiveDimensionScoreService.java` | 五维基础分计算/落库/重算服务 |
|
|
|
+| 创建 | `cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyMemberDimensionBaseScore.java` | 五维基础分实体 |
|
|
|
+| 创建 | `cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyMemberDimensionBaseScoreMapper.java` | MyBatis-Plus Mapper 接口 |
|
|
|
+| 修改 | `cfc-backend/src/main/java/com/etotem/cfc/dto/TianpanMemberVO.java` | 新增 dimensionBaseScores 字段 |
|
|
|
+| 修改 | `cfc-backend/src/main/java/com/etotem/cfc/service/TianpanService.java` | 扩展 enrichMembers 填充 zodiacName 和 dimensionBaseScores |
|
|
|
+| 修改 | `cfc-backend/src/main/java/com/etotem/cfc/controller/tianpan/TianpanController.java` | 新增重算接口 |
|
|
|
+| 修改 | `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` | 新增迁移 SQL |
|
|
|
+| 修改 | `cfc-backend/src/main/resources/schema.sql` | 同步新表定义 |
|
|
|
+| 创建 | `cfc-backend/src/test/java/com/etotem/cfc/unit/FiveDimensionScoreServiceTest.java` | 单元测试 |
|
|
|
+| 修改 | `cfc-frontend/pages/tianpan/index.vue` | canvas 节点标注 + 成员弹窗增强 |
|
|
|
+| 创建 | `cfc-frontend/pages/tianpan/traditional.vue` | 新增传统命理页 |
|
|
|
+| 修改 | `cfc-frontend/pages.json` | 注册新页面 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务列表
|
|
|
+
|
|
|
+### 任务 1:后端 — 数据库迁移和新表实体
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`
|
|
|
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyMemberDimensionBaseScore.java`
|
|
|
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyMemberDimensionBaseScoreMapper.java`
|
|
|
+- 修改:`cfc-backend/src/main/resources/schema.sql`
|
|
|
+
|
|
|
+- [ ] **步骤 1:确认最新迁移编号**
|
|
|
+
|
|
|
+在 `DatabaseInitializer.java` 末尾搜索 `// 迁移` 找到最新编号 N。
|
|
|
+
|
|
|
+运行:
|
|
|
+```bash
|
|
|
+grep -n "// 迁移" cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java | tail -5
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:创建实体类**
|
|
|
+
|
|
|
+创建 `cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyMemberDimensionBaseScore.java`:
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.entity;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.annotation.IdType;
|
|
|
+import com.baomidou.mybatisplus.annotation.TableId;
|
|
|
+import com.baomidou.mybatisplus.annotation.TableName;
|
|
|
+import lombok.Data;
|
|
|
+
|
|
|
+import java.io.Serializable;
|
|
|
+import java.util.Date;
|
|
|
+
|
|
|
+@Data
|
|
|
+@TableName("family_member_dimension_base_scores")
|
|
|
+public class FamilyMemberDimensionBaseScore implements Serializable {
|
|
|
+ @TableId(type = IdType.AUTO)
|
|
|
+ private Long id;
|
|
|
+ private Long memberId;
|
|
|
+ private String memberType;
|
|
|
+ private String dimensionCode; // body/mind/wisdom/action/wealth
|
|
|
+ private Integer baseScore;
|
|
|
+ private Date lastCalculatedAt;
|
|
|
+ private String calculatedBy; // system/manual/admin
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:创建 Mapper 接口**
|
|
|
+
|
|
|
+创建 `cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyMemberDimensionBaseScoreMapper.java`:
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.mapper;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
|
+import com.etotem.cfc.entity.FamilyMemberDimensionBaseScore;
|
|
|
+import org.apache.ibatis.annotations.Mapper;
|
|
|
+
|
|
|
+@Mapper
|
|
|
+public interface FamilyMemberDimensionBaseScoreMapper extends BaseMapper<FamilyMemberDimensionBaseScore> {
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:在 schema.sql 添加建表语句**
|
|
|
+
|
|
|
+在 `cfc-backend/src/main/resources/schema.sql` 末尾追加:
|
|
|
+
|
|
|
+```sql
|
|
|
+CREATE TABLE IF NOT EXISTS family_member_dimension_base_scores (
|
|
|
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
|
+ member_id BIGINT NOT NULL COMMENT '家庭成员ID',
|
|
|
+ member_type VARCHAR(20) NOT NULL COMMENT '成员类型: child/parent',
|
|
|
+ dimension_code VARCHAR(20) NOT NULL COMMENT '维度: body/mind/wisdom/action/wealth',
|
|
|
+ base_score INT NOT NULL COMMENT '基础能量分(0-100)',
|
|
|
+ last_calculated_at DATETIME NOT NULL COMMENT '最后计算时间',
|
|
|
+ calculated_by VARCHAR(50) DEFAULT 'system' COMMENT '计算来源(system/manual)',
|
|
|
+ UNIQUE KEY uk_member_dimension (member_id, dimension_code),
|
|
|
+ INDEX idx_member (member_id)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='五维基础能量分(落库,支持重算)';
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:在 DatabaseInitializer 添加迁移**
|
|
|
+
|
|
|
+在 `DatabaseInitializer.java` 的 `runMigrations()` 末尾(迁移N之后)追加:
|
|
|
+
|
|
|
+```java
|
|
|
+// 迁移N+1: 创建 family_member_dimension_base_scores 表
|
|
|
+try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_member_dimension_base_scores (" +
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
|
|
|
+ "member_id BIGINT NOT NULL COMMENT '家庭成员ID', " +
|
|
|
+ "member_type VARCHAR(20) NOT NULL COMMENT '成员类型: child/parent', " +
|
|
|
+ "dimension_code VARCHAR(20) NOT NULL COMMENT '维度: body/mind/wisdom/action/wealth', " +
|
|
|
+ "base_score INT NOT NULL COMMENT '基础能量分(0-100)', " +
|
|
|
+ "last_calculated_at DATETIME NOT NULL COMMENT '最后计算时间', " +
|
|
|
+ "calculated_by VARCHAR(50) DEFAULT 'system' COMMENT '计算来源', " +
|
|
|
+ "UNIQUE KEY uk_member_dimension (member_id, dimension_code), " +
|
|
|
+ "INDEX idx_member (member_id)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='五维基础能量分'");
|
|
|
+ log.info("已创建family_member_dimension_base_scores表");
|
|
|
+} catch (Exception e) {
|
|
|
+ // 表已存在,忽略错误
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 6:编译验证**
|
|
|
+
|
|
|
+运行:
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile
|
|
|
+```
|
|
|
+预期:BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **步骤 7:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java \
|
|
|
+ cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyMemberDimensionBaseScore.java \
|
|
|
+ cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyMemberDimensionBaseScoreMapper.java \
|
|
|
+ cfc-backend/src/main/resources/schema.sql
|
|
|
+git commit -m "feat: 新增五维基础分表及迁移"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 2:后端 — FiveDimensionScoreService 核心服务
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/service/FiveDimensionScoreService.java`
|
|
|
+- 创建:`cfc-backend/src/test/java/com/etotem/cfc/unit/FiveDimensionScoreServiceTest.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:编写测试**
|
|
|
+
|
|
|
+创建 `cfc-backend/src/test/java/com/etotem/cfc/unit/FiveDimensionScoreServiceTest.java`:
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.etotem.cfc.service.FiveDimensionScoreService;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.junit.jupiter.api.extension.ExtendWith;
|
|
|
+import org.mockito.InjectMocks;
|
|
|
+import org.mockito.junit.jupiter.MockitoExtension;
|
|
|
+
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
+
|
|
|
+@ExtendWith(MockitoExtension.class)
|
|
|
+class FiveDimensionScoreServiceTest {
|
|
|
+
|
|
|
+ @InjectMocks
|
|
|
+ private FiveDimensionScoreService service;
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void calcBaseScores_fullData_returnsCorrectScores() {
|
|
|
+ // 八字五行 {wood:40, fire:60, earth:50, metal:30, water:20}
|
|
|
+ // 星座狮子座(火象→心)
|
|
|
+ // 灵数7(→智)
|
|
|
+ Map<String, Integer> wuxing = Map.of("wood", 40, "fire", 60, "earth", 50, "metal", 30, "water", 20);
|
|
|
+ Map<String, Integer> result = service.calcBaseScores(wuxing, "狮子座", 7);
|
|
|
+
|
|
|
+ assertEquals(30, result.get("body")); // 50×0.6=30
|
|
|
+ assertEquals(56, result.get("mind")); // 60×0.6+20=56
|
|
|
+ assertEquals(38, result.get("wisdom")); // 30×0.6+20=38
|
|
|
+ assertEquals(24, result.get("action")); // 40×0.6=24
|
|
|
+ assertEquals(12, result.get("wealth")); // 20×0.6=12
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void calcBaseScores_nullWuxing_returnsNeutral50() {
|
|
|
+ Map<String, Integer> result = service.calcBaseScores(null, null, null);
|
|
|
+
|
|
|
+ assertEquals(50, result.get("body"));
|
|
|
+ assertEquals(50, result.get("mind"));
|
|
|
+ assertEquals(50, result.get("wisdom"));
|
|
|
+ assertEquals(50, result.get("action"));
|
|
|
+ assertEquals(50, result.get("wealth"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void calcBaseScores_masterNumber_11MapsTo2() {
|
|
|
+ Map<String, Integer> wuxing = Map.of("wood", 50, "fire", 50, "earth", 50, "metal", 50, "water", 50);
|
|
|
+ Map<String, Integer> result = service.calcBaseScores(wuxing, null, 11); // 11→2→心
|
|
|
+
|
|
|
+ assertEquals(60 + 20, result.get("mind")); // 土×0.6=50, 灵数11→2→心+20
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void calcBaseScores_allZodiacSigns_coverage() {
|
|
|
+ Map<String, Integer> wuxing = Map.of("wood", 50, "fire", 50, "earth", 50, "metal", 50, "water", 50);
|
|
|
+
|
|
|
+ // 火象
|
|
|
+ Map<String, Integer> fireResult = service.calcBaseScores(wuxing, "白羊座", null);
|
|
|
+ assertTrue(fireResult.get("mind") > 50);
|
|
|
+
|
|
|
+ // 土象
|
|
|
+ Map<String, Integer> earthResult = service.calcBaseScores(wuxing, "金牛座", null);
|
|
|
+ assertTrue(earthResult.get("body") > 50);
|
|
|
+
|
|
|
+ // 风象
|
|
|
+ Map<String, Integer> airResult = service.calcBaseScores(wuxing, "双子座", null);
|
|
|
+ assertTrue(airResult.get("action") > 50);
|
|
|
+
|
|
|
+ // 水象
|
|
|
+ Map<String, Integer> waterResult = service.calcBaseScores(wuxing, "巨蟹座", null);
|
|
|
+ assertTrue(waterResult.get("wealth") > 50);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行测试确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn test -pl . -Dtest=FiveDimensionScoreServiceTest
|
|
|
+```
|
|
|
+预期:FAIL(方法不存在)
|
|
|
+
|
|
|
+- [ ] **步骤 3:实现服务**
|
|
|
+
|
|
|
+创建 `cfc-backend/src/main/java/com/etotem/cfc/service/FiveDimensionScoreService.java`:
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.etotem.cfc.entity.FamilyMemberDimensionBaseScore;
|
|
|
+import com.etotem.cfc.mapper.FamilyMemberDimensionBaseScoreMapper;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class FiveDimensionScoreService {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private FamilyMemberDimensionBaseScoreMapper dimensionBaseScoreMapper;
|
|
|
+
|
|
|
+ private static final Map<String, String> ZODIAC_ELEMENT_TO_DIM = new LinkedHashMap<>();
|
|
|
+ private static final Map<Integer, String> LIFE_NUMBER_DIM = new LinkedHashMap<>();
|
|
|
+ private static final String[] DIMENSIONS = {"body", "mind", "wisdom", "action", "wealth"};
|
|
|
+
|
|
|
+ static {
|
|
|
+ // 星座 → 维度(火象→心, 土象→身, 风象→行, 水象→富)
|
|
|
+ ZODIAC_ELEMENT_TO_DIM.put("白羊座", "mind");
|
|
|
+ ZODIAC_ELEMENT_TO_DIM.put("狮子座", "mind");
|
|
|
+ ZODIAC_ELEMENT_TO_DIM.put("射手座", "mind");
|
|
|
+ ZODIAC_ELEMENT_TO_DIM.put("金牛座", "body");
|
|
|
+ ZODIAC_ELEMENT_TO_DIM.put("处女座", "body");
|
|
|
+ ZODIAC_ELEMENT_TO_DIM.put("摩羯座", "body");
|
|
|
+ ZODIAC_ELEMENT_TO_DIM.put("双子座", "action");
|
|
|
+ ZODIAC_ELEMENT_TO_DIM.put("天秤座", "action");
|
|
|
+ ZODIAC_ELEMENT_TO_DIM.put("水瓶座", "action");
|
|
|
+ ZODIAC_ELEMENT_TO_DIM.put("巨蟹座", "wealth");
|
|
|
+ ZODIAC_ELEMENT_TO_DIM.put("天蝎座", "wealth");
|
|
|
+ ZODIAC_ELEMENT_TO_DIM.put("双鱼座", "wealth");
|
|
|
+
|
|
|
+ // 灵数 → 主维度
|
|
|
+ LIFE_NUMBER_DIM.put(1, "action");
|
|
|
+ LIFE_NUMBER_DIM.put(2, "mind");
|
|
|
+ LIFE_NUMBER_DIM.put(3, "wisdom");
|
|
|
+ LIFE_NUMBER_DIM.put(4, "body");
|
|
|
+ LIFE_NUMBER_DIM.put(5, "action");
|
|
|
+ LIFE_NUMBER_DIM.put(6, "mind");
|
|
|
+ LIFE_NUMBER_DIM.put(7, "wisdom");
|
|
|
+ LIFE_NUMBER_DIM.put(8, "wealth");
|
|
|
+ LIFE_NUMBER_DIM.put(9, "mind");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算五维基础能量分(纯算法,不落库)
|
|
|
+ */
|
|
|
+ public Map<String, Integer> calcBaseScores(Map<String, Integer> wuxingElements,
|
|
|
+ String westernSign,
|
|
|
+ Integer lifeNumber) {
|
|
|
+ Map<String, Integer> scores = new HashMap<>();
|
|
|
+ for (String dim : DIMENSIONS) {
|
|
|
+ scores.put(dim, 50); // 默认中性值
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 八字五行层(权重 60%)
|
|
|
+ if (wuxingElements != null) {
|
|
|
+ scores.put("action", Math.round((float) wuxingElements.getOrDefault("wood", 50) * 0.6));
|
|
|
+ scores.put("mind", Math.round((float) wuxingElements.getOrDefault("fire", 50) * 0.6));
|
|
|
+ scores.put("body", Math.round((float) wuxingElements.getOrDefault("earth", 50) * 0.6));
|
|
|
+ scores.put("wisdom", Math.round((float) wuxingElements.getOrDefault("metal", 50) * 0.6));
|
|
|
+ scores.put("wealth", Math.round((float) wuxingElements.getOrDefault("water", 50) * 0.6));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 星座层(权重 20%)
|
|
|
+ if (westernSign != null && ZODIAC_ELEMENT_TO_DIM.containsKey(westernSign)) {
|
|
|
+ String dim = ZODIAC_ELEMENT_TO_DIM.get(westernSign);
|
|
|
+ scores.put(dim, scores.get(dim) + 20);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 灵数层(权重 20%)
|
|
|
+ if (lifeNumber != null) {
|
|
|
+ int reduced = reduceMaster(lifeNumber);
|
|
|
+ if (LIFE_NUMBER_DIM.containsKey(reduced)) {
|
|
|
+ String dim = LIFE_NUMBER_DIM.get(reduced);
|
|
|
+ scores.put(dim, scores.get(dim) + 20);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return scores;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算并落库(INSERT ON DUPLICATE KEY UPDATE)
|
|
|
+ */
|
|
|
+ public void calcAndSave(Long memberId, String memberType,
|
|
|
+ Map<String, Integer> wuxingElements,
|
|
|
+ String westernSign, Integer lifeNumber,
|
|
|
+ String operator) {
|
|
|
+ Map<String, Integer> scores = calcBaseScores(wuxingElements, westernSign, lifeNumber);
|
|
|
+ saveScores(memberId, memberType, scores, operator);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void saveScores(Long memberId, String memberType, Map<String, Integer> scores, String operator) {
|
|
|
+ Date now = new Date();
|
|
|
+ for (Map.Entry<String, Integer> entry : scores.entrySet()) {
|
|
|
+ FamilyMemberDimensionBaseScore entity = new FamilyMemberDimensionBaseScore();
|
|
|
+ entity.setMemberId(memberId);
|
|
|
+ entity.setMemberType(memberType);
|
|
|
+ entity.setDimensionCode(entry.getKey());
|
|
|
+ entity.setBaseScore(entry.getValue());
|
|
|
+ entity.setLastCalculatedAt(now);
|
|
|
+ entity.setCalculatedBy(operator);
|
|
|
+
|
|
|
+ LambdaQueryWrapper<FamilyMemberDimensionBaseScore> qw = new LambdaQueryWrapper<>();
|
|
|
+ qw.eq(FamilyMemberDimensionBaseScore::getMemberId, memberId)
|
|
|
+ .eq(FamilyMemberDimensionBaseScore::getDimensionCode, entry.getKey());
|
|
|
+
|
|
|
+ FamilyMemberDimensionBaseScore existing = dimensionBaseScoreMapper.selectOne(qw);
|
|
|
+ if (existing != null) {
|
|
|
+ existing.setBaseScore(entry.getValue());
|
|
|
+ existing.setLastCalculatedAt(now);
|
|
|
+ existing.setCalculatedBy(operator);
|
|
|
+ dimensionBaseScoreMapper.updateById(existing);
|
|
|
+ } else {
|
|
|
+ entity.setId(null);
|
|
|
+ dimensionBaseScoreMapper.insert(entity);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 重算单个成员
|
|
|
+ */
|
|
|
+ public void recalibrate(Long memberId, String memberType, String operator) {
|
|
|
+ // 从 family_member_attributes 读取源数据
|
|
|
+ // 简化:直接调用 calcAndSave,传入正确的源数据
|
|
|
+ // 实际调用方应负责准备 wuxingElements/westernSign/lifeNumber
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询五维基础分(供 dashboard 接口读取)
|
|
|
+ */
|
|
|
+ public Map<String, Integer> getScores(Long memberId) {
|
|
|
+ Map<String, Integer> scores = new LinkedHashMap<>();
|
|
|
+ for (String dim : DIMENSIONS) {
|
|
|
+ scores.put(dim, 50); // 默认中性值
|
|
|
+ }
|
|
|
+ LambdaQueryWrapper<FamilyMemberDimensionBaseScore> qw = new LambdaQueryWrapper<>();
|
|
|
+ qw.eq(FamilyMemberDimensionBaseScore::getMemberId, memberId);
|
|
|
+ List<FamilyMemberDimensionBaseScore> list = dimensionBaseScoreMapper.selectList(qw);
|
|
|
+ for (FamilyMemberDimensionBaseScore item : list) {
|
|
|
+ scores.put(item.getDimensionCode(), item.getBaseScore());
|
|
|
+ }
|
|
|
+ return scores;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 主数化简:11→2, 22→4, 33→6, 其他>9按数位和递归
|
|
|
+ */
|
|
|
+ private int reduceMaster(int n) {
|
|
|
+ if (n == 11) return 2;
|
|
|
+ if (n == 22) return 4;
|
|
|
+ if (n == 33) return 6;
|
|
|
+ while (n >= 10) {
|
|
|
+ int sum = 0;
|
|
|
+ while (n > 0) {
|
|
|
+ sum += n % 10;
|
|
|
+ n /= 10;
|
|
|
+ }
|
|
|
+ n = sum;
|
|
|
+ }
|
|
|
+ return n;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:运行测试确认通过**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn test -pl . -Dtest=FiveDimensionScoreServiceTest
|
|
|
+```
|
|
|
+预期:PASS
|
|
|
+
|
|
|
+- [ ] **步骤 5:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/service/FiveDimensionScoreService.java \
|
|
|
+ cfc-backend/src/test/java/com/etotem/cfc/unit/FiveDimensionScoreServiceTest.java
|
|
|
+git commit -m "feat: 新增五维基础分计算服务"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 3:后端 — 扩展 TianpanService 和 DTO
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/dto/TianpanMemberVO.java`
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/TianpanService.java`
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/tianpan/TianpanController.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:扩展 TianpanMemberVO**
|
|
|
+
|
|
|
+在 `TianpanMemberVO.java` 中追加字段:
|
|
|
+
|
|
|
+```java
|
|
|
+private Map<String, Integer> dimensionBaseScores; // 五维基础分
|
|
|
+private String zodiacName; // 生肖中文名(rat→鼠等)
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:修改 TianpanService.enrichMembers()**
|
|
|
+
|
|
|
+在 `TianpanService.java` 的 `enrichMembers()` 方法(第 216 行附近)追加:
|
|
|
+
|
|
|
+```java
|
|
|
+// 注入新服务
|
|
|
+@Resource
|
|
|
+private FiveDimensionScoreService fiveDimensionScoreService;
|
|
|
+```
|
|
|
+
|
|
|
+在 `enrichMembers()` 循环末尾(`vo.setEightCharacters(...)` 之后)追加:
|
|
|
+
|
|
|
+```java
|
|
|
+// 生肖中文名
|
|
|
+String zodiacName = findZodiacName(m.getZodiac());
|
|
|
+vo.setZodiacName(zodiacName);
|
|
|
+
|
|
|
+// 五维基础分:优先读库,无则计算并落库
|
|
|
+Map<String, Integer> cachedScores = fiveDimensionScoreService.getScores(m.getMemberId());
|
|
|
+boolean hasScores = cachedScores.values().stream().anyMatch(s -> s != 50);
|
|
|
+if (!hasScores) {
|
|
|
+ fiveDimensionScoreService.calcAndSave(
|
|
|
+ m.getMemberId(), m.getMemberType(),
|
|
|
+ m.getWuxingElements(),
|
|
|
+ vo.getWesternSign(),
|
|
|
+ m.getLifeNumber(),
|
|
|
+ "system");
|
|
|
+ cachedScores = fiveDimensionScoreService.getScores(m.getMemberId());
|
|
|
+}
|
|
|
+vo.setDimensionBaseScores(cachedScores);
|
|
|
+```
|
|
|
+
|
|
|
+添加辅助方法:
|
|
|
+
|
|
|
+```java
|
|
|
+private String findZodiacName(String zodiacCode) {
|
|
|
+ if (zodiacCode == null) return null;
|
|
|
+ Map<String, String> nameMap = new LinkedHashMap<>();
|
|
|
+ nameMap.put("rat", "鼠"); nameMap.put("ox", "牛"); nameMap.put("tiger", "虎");
|
|
|
+ nameMap.put("rabbit", "兔"); nameMap.put("dragon", "龙"); nameMap.put("snake", "蛇");
|
|
|
+ nameMap.put("horse", "马"); nameMap.put("sheep", "羊"); nameMap.put("monkey", "猴");
|
|
|
+ nameMap.put("rooster", "鸡"); nameMap.put("dog", "狗"); nameMap.put("pig", "猪");
|
|
|
+ return nameMap.get(zodiacCode);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:新增重算接口**
|
|
|
+
|
|
|
+在 `TianpanController.java` 追加:
|
|
|
+
|
|
|
+```java
|
|
|
+@PostMapping("/recalibrate")
|
|
|
+public Result<Map<String, Integer>> recalibrate(@RequestBody Map<String, Object> params) {
|
|
|
+ Long memberId = Long.valueOf(params.get("memberId").toString());
|
|
|
+ String memberType = params.get("memberType") != null ? params.get("memberType").toString() : "child";
|
|
|
+ String operator = params.get("operator") != null ? params.get("operator").toString() : "admin";
|
|
|
+
|
|
|
+ // 从 family_member_attributes 读取源数据并重算
|
|
|
+ FamilyMemberAttributes attrs = familyMemberAttributeService.getByMember(memberId, memberType);
|
|
|
+ if (attrs == null) {
|
|
|
+ return Result.error("成员属性不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Integer> wuxingElements = parseJsonInt(attrs.getWuxingElements());
|
|
|
+ String westernSign = null;
|
|
|
+ if (attrs.getBirthDatetime() != null) {
|
|
|
+ Calendar cal = Calendar.getInstance();
|
|
|
+ cal.setTime(attrs.getBirthDatetime());
|
|
|
+ westernSign = zodiacAnnualEnergyService.getWesternSign(cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH));
|
|
|
+ }
|
|
|
+
|
|
|
+ String behaviorModifier = attrs.getBehaviorModifier();
|
|
|
+ Integer lifeNumber = null;
|
|
|
+ if (behaviorModifier != null) {
|
|
|
+ try {
|
|
|
+ Map<String, Object> bm = objectMapper.readValue(behaviorModifier, Map.class);
|
|
|
+ Object ln = bm.get("lifeNumber");
|
|
|
+ if (ln instanceof Number) lifeNumber = ((Number) ln).intValue();
|
|
|
+ } catch (Exception e) { /* ignore */ }
|
|
|
+ }
|
|
|
+
|
|
|
+ fiveDimensionScoreService.calcAndSave(memberId, memberType, wuxingElements, westernSign, lifeNumber, operator);
|
|
|
+
|
|
|
+ Map<String, Integer> scores = fiveDimensionScoreService.getScores(memberId);
|
|
|
+ return Result.success(scores);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile
|
|
|
+```
|
|
|
+预期:BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **步骤 5:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/TianpanMemberVO.java \
|
|
|
+ cfc-backend/src/main/java/com/etotem/cfc/service/TianpanService.java \
|
|
|
+ cfc-backend/src/main/java/com/etotem/cfc/controller/tianpan/TianpanController.java
|
|
|
+git commit -m "feat: 扩展天盘服务支持五维基础分展示和重算"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 4:小程序 — 天盘首页增强
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-frontend/pages/tianpan/index.vue`
|
|
|
+
|
|
|
+- [ ] **步骤 1:增强 drawMemberNodes**
|
|
|
+
|
|
|
+在 `drawMemberNodes` 方法(第 375 行附近)的成员节点绘制后追加:
|
|
|
+
|
|
|
+```javascript
|
|
|
+// 在 ctx.fillText(member.nickname || member.name, x, y) 之后追加
|
|
|
+// 绘制生肖+灵数标注
|
|
|
+var showLabel = member.zodiacName || member.lifeNumber
|
|
|
+if (showLabel) {
|
|
|
+ var labelParts = []
|
|
|
+ if (member.zodiacName) labelParts.push(member.zodiacName)
|
|
|
+ if (member.lifeNumber) labelParts.push('灵数' + member.lifeNumber)
|
|
|
+ var label = labelParts.join('·')
|
|
|
+ ctx.font = '9px sans-serif'
|
|
|
+ ctx.fillStyle = this.getMemberColor(member.effectiveRole)
|
|
|
+ ctx.textAlign = 'center'
|
|
|
+ ctx.textBaseline = 'top'
|
|
|
+ ctx.fillText(label, x, y + 18)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:增强成员弹窗**
|
|
|
+
|
|
|
+在模板中(第 98-119 行),基本信息区追加八字和灵数,能量区追加五维基础分:
|
|
|
+
|
|
|
+```html
|
|
|
+<!-- 新增:生命灵数 -->
|
|
|
+<view class="member-info-row">
|
|
|
+ <text class="info-label">灵数:</text>
|
|
|
+ <text class="info-value">{{ selectedMember.lifeNumber || '-' }}</text>
|
|
|
+</view>
|
|
|
+<!-- 新增:八字四柱 -->
|
|
|
+<view class="member-info-row" v-if="selectedMember.eightCharacters">
|
|
|
+ <text class="info-label">八字:</text>
|
|
|
+ <text class="info-value">{{ getEightCharsText(selectedMember.eightCharacters) }}</text>
|
|
|
+</view>
|
|
|
+```
|
|
|
+
|
|
|
+在 methods 中追加:
|
|
|
+
|
|
|
+```javascript
|
|
|
+getEightCharsText: function(eightChars) {
|
|
|
+ if (!eightChars) return ''
|
|
|
+ var parts = []
|
|
|
+ var keys = ['year', 'month', 'day', 'hour']
|
|
|
+ for (var i = 0; i < keys.length; i++) {
|
|
|
+ if (eightChars[keys[i]]) parts.push(eightChars[keys[i]])
|
|
|
+ }
|
|
|
+ return parts.join(' ')
|
|
|
+},
|
|
|
+```
|
|
|
+
|
|
|
+在 `.member-info-row` 之后追加五维基础分展示:
|
|
|
+
|
|
|
+```html
|
|
|
+<!-- 新增:五维基础分 -->
|
|
|
+<view class="energy-bars" v-if="selectedMember.dimensionBaseScores">
|
|
|
+ <view class="energy-bar-item" v-for="(val, key) in selectedMember.dimensionBaseScores" :key="getKey(key)">
|
|
|
+ <text class="energy-label">{{ getDimLabel(key) }}</text>
|
|
|
+ <view class="energy-bar">
|
|
|
+ <view class="energy-fill" :style="{ width: val + '%', background: getDimColor(key) }"></view>
|
|
|
+ </view>
|
|
|
+ <text class="energy-value">{{ val }}</text>
|
|
|
+ </view>
|
|
|
+</view>
|
|
|
+```
|
|
|
+
|
|
|
+在 methods 中追加:
|
|
|
+
|
|
|
+```javascript
|
|
|
+getKey: function(key) { return 'dim-' + key },
|
|
|
+getDimLabel: function(key) {
|
|
|
+ var labels = { body: '身', mind: '心', wisdom: '智', action: '行', wealth: '富' }
|
|
|
+ return labels[key] || key
|
|
|
+},
|
|
|
+getDimColor: function(key) {
|
|
|
+ var colors = { body: '#FF8C42', mind: '#FF6B9D', wisdom: '#6366F1', action: '#10B981', wealth: '#F59E0B' }
|
|
|
+ return colors[key] || '#6B7280'
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:语法检查**
|
|
|
+
|
|
|
+运行:
|
|
|
+```bash
|
|
|
+node -c cfc-frontend/pages/tianpan/index.vue 2>&1 || echo "需要检查script块语法"
|
|
|
+```
|
|
|
+
|
|
|
+由于这是 .vue 文件,手动检查 script 块语法:
|
|
|
+
|
|
|
+```bash
|
|
|
+sed -n '/<script>/,/<\/script>/p' cfc-frontend/pages/tianpan/index.vue | sed '1d;$d' | node --check -
|
|
|
+```
|
|
|
+
|
|
|
+预期:无报错
|
|
|
+
|
|
|
+- [ ] **步骤 4:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-frontend/pages/tianpan/index.vue
|
|
|
+git commit -m "feat(frontend): 天盘首页canvas节点标注生肖灵数+成员弹窗增强"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 5:小程序 — 新增传统命理页
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 创建:`cfc-frontend/pages/tianpan/traditional.vue`
|
|
|
+- 修改:`cfc-frontend/pages.json`
|
|
|
+
|
|
|
+- [ ] **步骤 1:注册页面**
|
|
|
+
|
|
|
+在 `cfc-frontend/pages.json` 的 `pages/tianpan` 分包中添加:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "path": "pages/tianpan/traditional",
|
|
|
+ "style": {
|
|
|
+ "navigationBarTitleText": "家庭传统命理"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:创建页面组件**
|
|
|
+
|
|
|
+创建 `cfc-frontend/pages/tianpan/traditional.vue`:
|
|
|
+
|
|
|
+```html
|
|
|
+<template>
|
|
|
+ <view class="traditional-container">
|
|
|
+ <!-- 自定义导航栏 -->
|
|
|
+ <view class="custom-nav">
|
|
|
+ <view class="nav-back" @click="goBack">‹ 返回</view>
|
|
|
+ <text class="nav-title">家庭传统命理</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 加载状态 -->
|
|
|
+ <view class="loading-state" v-if="loading">
|
|
|
+ <view class="loading-spinner"></view>
|
|
|
+ <text>加载中...</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 成员列表 -->
|
|
|
+ <view class="member-list" v-if="!loading && members && members.length > 0">
|
|
|
+ <view class="member-card" v-for="member in members" :key="member.memberId">
|
|
|
+ <!-- 头部 -->
|
|
|
+ <view class="card-header">
|
|
|
+ <view class="avatar" :style="{ backgroundColor: member.colorHex || '#6B7280' }">
|
|
|
+ <text class="avatar-text">{{ member.nickname ? member.nickname.charAt(0) : 'M' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="member-info">
|
|
|
+ <text class="member-name">{{ member.nickname || member.name }}</text>
|
|
|
+ <text class="member-role">{{ getRoleLabel(member.memberType) }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 八字四柱 -->
|
|
|
+ <view class="section" v-if="member.eightCharacters">
|
|
|
+ <text class="section-title">八字四柱</text>
|
|
|
+ <view class="bazi-grid">
|
|
|
+ <view class="bazi-col" v-for="(val, key) in member.eightCharacters" :key="key">
|
|
|
+ <text class="bazi-label">{{ getPillarLabel(key) }}</text>
|
|
|
+ <text class="bazi-value">{{ val }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 五行能量 -->
|
|
|
+ <view class="section" v-if="member.wuxingElements">
|
|
|
+ <text class="section-title">五行能量</text>
|
|
|
+ <view class="wuxing-list">
|
|
|
+ <view class="wuxing-item" v-for="wx in getWuxingData(member.wuxingElements)" :key="wx.key">
|
|
|
+ <text class="wuxing-label">{{ wx.label }}</text>
|
|
|
+ <view class="wuxing-bar-track">
|
|
|
+ <view class="wuxing-bar-fill" :style="{ width: wx.value + '%', background: wx.color }"></view>
|
|
|
+ </view>
|
|
|
+ <text class="wuxing-value">{{ wx.value }}%</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 生肖星座 -->
|
|
|
+ <view class="section" v-if="member.zodiacName || member.westernSign">
|
|
|
+ <text class="section-title">生肖星座</text>
|
|
|
+ <view class="tag-row">
|
|
|
+ <view class="tag" v-if="member.zodiacName">
|
|
|
+ <text>{{ member.zodiacName }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="tag" v-if="member.westernSign">
|
|
|
+ <text>{{ member.westernSign }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 生命灵数 -->
|
|
|
+ <view class="section" v-if="member.lifeNumber">
|
|
|
+ <text class="section-title">生命灵数</text>
|
|
|
+ <view class="numsoul-display">
|
|
|
+ <view class="numsoul-number" :style="{ background: getLifeNumberColor(member.lifeNumber) }">
|
|
|
+ <text>{{ member.lifeNumber }}</text>
|
|
|
+ </view>
|
|
|
+ <text class="numsoul-title">{{ getLifeNumberTitle(member.lifeNumber) }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 五维基础分 -->
|
|
|
+ <view class="section" v-if="member.dimensionBaseScores">
|
|
|
+ <text class="section-title">五维基础分</text>
|
|
|
+ <view class="dimension-bars">
|
|
|
+ <view class="dim-item" v-for="(val, key) in member.dimensionBaseScores" :key="key">
|
|
|
+ <text class="dim-label">{{ getDimLabel(key) }}</text>
|
|
|
+ <view class="dim-bar-track">
|
|
|
+ <view class="dim-bar-fill" :style="{ width: val + '%', background: getDimColor(key) }"></view>
|
|
|
+ </view>
|
|
|
+ <text class="dim-value">{{ val }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 空状态 -->
|
|
|
+ <view class="empty-state" v-if="!loading && (!members || members.length === 0)">
|
|
|
+ <text>暂无家庭成员数据</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { mapState, mapGetters, mapActions } from 'vuex'
|
|
|
+
|
|
|
+export default {
|
|
|
+ data: function() {
|
|
|
+ return {
|
|
|
+ loading: false,
|
|
|
+ members: []
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ ...mapState('tianpan', ['loading']),
|
|
|
+ ...mapGetters('tianpan', ['members'])
|
|
|
+ },
|
|
|
+ onLoad: function() {
|
|
|
+ this.loadMembers()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ ...mapActions('tianpan', ['loadDashboard']),
|
|
|
+
|
|
|
+ loadMembers: function() {
|
|
|
+ this.loading = true
|
|
|
+ // 如果 store 中无数据则重新加载
|
|
|
+ if (!this.members || this.members.length === 0) {
|
|
|
+ this.loadDashboard({ year: new Date().getFullYear(), daysAhead: 7 })
|
|
|
+ .then(this.fetchMembers)
|
|
|
+ .finally(function() {
|
|
|
+ this.loading = false
|
|
|
+ }.bind(this))
|
|
|
+ } else {
|
|
|
+ this.fetchMembers()
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ fetchMembers: function() {
|
|
|
+ this.members = this.members || []
|
|
|
+ },
|
|
|
+
|
|
|
+ goBack: function() {
|
|
|
+ uni.navigateBack()
|
|
|
+ },
|
|
|
+
|
|
|
+ getPillarLabel: function(key) {
|
|
|
+ var labels = { year: '年柱', month: '月柱', day: '日柱', hour: '时柱' }
|
|
|
+ return labels[key] || key
|
|
|
+ },
|
|
|
+
|
|
|
+ getWuxingData: function(wuxingElements) {
|
|
|
+ if (!wuxingElements) return []
|
|
|
+ var colorMap = { wood: '#10B981', fire: '#FF6B9D', earth: '#FF8C42', metal: '#6366F1', water: '#3B82F6' }
|
|
|
+ var labelMap = { wood: '木', fire: '火', earth: '土', metal: '金', water: '水' }
|
|
|
+ var result = []
|
|
|
+ for (var key in wuxingElements) {
|
|
|
+ if (wuxingElements.hasOwnProperty(key)) {
|
|
|
+ result.push({ key: key, label: labelMap[key] || key, value: wuxingElements[key], color: colorMap[key] || '#94A3B8' })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result.sort(function(a, b) { return b.value - a.value })
|
|
|
+ },
|
|
|
+
|
|
|
+ getRoleLabel: function(type) {
|
|
|
+ var labels = { parent: '家长', child: '孩子', teacher: '规划师' }
|
|
|
+ return labels[type] || type
|
|
|
+ },
|
|
|
+
|
|
|
+ getLifeNumberColor: function(num) {
|
|
|
+ var colors = { 1: '#F97316', 2: '#EC4899', 3: '#8B5CF6', 4: '#F59E0B', 5: '#3B82F6', 6: '#10B981', 7: '#6366F1', 8: '#EF4444', 9: '#14B8A6' }
|
|
|
+ return colors[num] || '#6B7280'
|
|
|
+ },
|
|
|
+
|
|
|
+ getLifeNumberTitle: function(num) {
|
|
|
+ var titles = { 1: '开创', 2: '协调', 3: '表达', 4: '稳定', 5: '自由', 6: '责任', 7: '求知', 8: '权力', 9: '博爱' }
|
|
|
+ return titles[num] || ''
|
|
|
+ },
|
|
|
+
|
|
|
+ getDimLabel: function(key) {
|
|
|
+ var labels = { body: '身', mind: '心', wisdom: '智', action: '行', wealth: '富' }
|
|
|
+ return labels[key] || key
|
|
|
+ },
|
|
|
+
|
|
|
+ getDimColor: function(key) {
|
|
|
+ var colors = { body: '#FF8C42', mind: '#FF6B9D', wisdom: '#6366F1', action: '#10B981', wealth: '#F59E0B' }
|
|
|
+ return colors[key] || '#6B7280'
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.traditional-container {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: #FFF8F0;
|
|
|
+ padding-bottom: 40rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.custom-nav {
|
|
|
+ background: linear-gradient(135deg, #F97316 0%, #FB923C 100%);
|
|
|
+ padding: 44px 16px 16px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-back {
|
|
|
+ font-size: 16px;
|
|
|
+ color: #FFFFFF;
|
|
|
+ margin-right: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-title {
|
|
|
+ font-size: 18px;
|
|
|
+ color: #FFFFFF;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-state {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ padding: 120px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-spinner {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ border: 4px solid #E5E7EB;
|
|
|
+ border-top-color: #F97316;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes spin {
|
|
|
+ to { transform: rotate(360deg); }
|
|
|
+}
|
|
|
+
|
|
|
+.member-list {
|
|
|
+ padding: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.member-card {
|
|
|
+ background: #FFFFFF;
|
|
|
+ border-radius: 16px;
|
|
|
+ padding: 20px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
|
+}
|
|
|
+
|
|
|
+.card-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.avatar {
|
|
|
+ width: 64px;
|
|
|
+ height: 64px;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ margin-right: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.avatar-text {
|
|
|
+ color: #FFFFFF;
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.member-info {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.member-name {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #1F2937;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+
|
|
|
+.member-role {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #6B7280;
|
|
|
+}
|
|
|
+
|
|
|
+.section {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #9CA3AF;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.bazi-grid {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-around;
|
|
|
+}
|
|
|
+
|
|
|
+.bazi-col {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.bazi-label {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #9CA3AF;
|
|
|
+ margin-bottom: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.bazi-value {
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #8B5CF6;
|
|
|
+}
|
|
|
+
|
|
|
+.wuxing-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.wuxing-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.wuxing-label {
|
|
|
+ font-size: 14px;
|
|
|
+ width: 24px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.wuxing-bar-track {
|
|
|
+ flex: 1;
|
|
|
+ height: 8px;
|
|
|
+ background: #E5E7EB;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.wuxing-bar-fill {
|
|
|
+ height: 100%;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.wuxing-value {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #6B7280;
|
|
|
+ width: 36px;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+
|
|
|
+.tag-row {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.tag {
|
|
|
+ background: #F3F4F6;
|
|
|
+ padding: 6px 16px;
|
|
|
+ border-radius: 20px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #374151;
|
|
|
+}
|
|
|
+
|
|
|
+.numsoul-display {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.numsoul-number {
|
|
|
+ width: 64px;
|
|
|
+ height: 64px;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #FFFFFF;
|
|
|
+}
|
|
|
+
|
|
|
+.numsoul-title {
|
|
|
+ font-size: 16px;
|
|
|
+ color: #374151;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.dimension-bars {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.dim-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.dim-label {
|
|
|
+ font-size: 14px;
|
|
|
+ width: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.dim-bar-track {
|
|
|
+ flex: 1;
|
|
|
+ height: 8px;
|
|
|
+ background: #E5E7EB;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.dim-bar-fill {
|
|
|
+ height: 100%;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.dim-value {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #6B7280;
|
|
|
+ width: 36px;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-state {
|
|
|
+ text-align: center;
|
|
|
+ padding: 120px 0;
|
|
|
+ color: #9CA3AF;
|
|
|
+}
|
|
|
+</style>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:语法检查**
|
|
|
+
|
|
|
+```bash
|
|
|
+sed -n '/<script>/,/<\/script>/p' cfc-frontend/pages/tianpan/traditional.vue | sed '1d;$d' | node --check -
|
|
|
+```
|
|
|
+预期:无报错
|
|
|
+
|
|
|
+- [ ] **步骤 4:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-frontend/pages.json cfc-frontend/pages/tianpan/traditional.vue
|
|
|
+git commit -m "feat(frontend): 新增传统命理页面"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 验证清单
|
|
|
+
|
|
|
+完成所有任务后,执行以下验证:
|
|
|
+
|
|
|
+1. **后端编译**
|
|
|
+ ```bash
|
|
|
+ cd cfc-backend && mvn clean compile
|
|
|
+ ```
|
|
|
+ 预期:BUILD SUCCESS
|
|
|
+
|
|
|
+2. **单测通过**
|
|
|
+ ```bash
|
|
|
+ cd cfc-backend && mvn test -Dtest=FiveDimensionScoreServiceTest
|
|
|
+ ```
|
|
|
+ 预期:全部 PASS
|
|
|
+
|
|
|
+3. **前端语法检查**
|
|
|
+ ```bash
|
|
|
+ for f in cfc-frontend/pages/tianpan/index.vue cfc-frontend/pages/tianpan/traditional.vue; do
|
|
|
+ sed -n '/<script>/,/<\/script>/p' $f | sed '1d;$d' | node --check - && echo "$f OK"
|
|
|
+ done
|
|
|
+ ```
|
|
|
+ 预期:两个文件均输出 OK
|
|
|
+
|
|
|
+4. **小程序限制自查**
|
|
|
+ - [ ] 无可选链 `?.`
|
|
|
+ - [ ] 无 CSS Grid
|
|
|
+ - [ ] 无 `:key` 表达式
|
|
|
+ - [ ] 无 `new Date(string)`
|
|
|
+
|
|
|
+5. **CI 门禁**
|
|
|
+ ```bash
|
|
|
+ cd cfc-frontend && node scripts/audit-duplicate-api-calls.js
|
|
|
+ ```
|
|
|
+ 预期:退出码 0
|