面向 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 |
注册新页面 |
文件:
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.javacfc-backend/src/main/java/com/etotem/cfc/entity/FamilyMemberDimensionBaseScore.javacfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyMemberDimensionBaseScoreMapper.java修改:cfc-backend/src/main/resources/schema.sql
[ ] 步骤 1:确认最新迁移编号
在 DatabaseInitializer.java 末尾搜索 // 迁移 找到最新编号 N。
运行:
grep -n "// 迁移" cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java | tail -5
创建 cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyMemberDimensionBaseScore.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
}
创建 cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyMemberDimensionBaseScoreMapper.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> {
}
在 cfc-backend/src/main/resources/schema.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='五维基础能量分(落库,支持重算)';
在 DatabaseInitializer.java 的 runMigrations() 末尾(迁移N之后)追加:
// 迁移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) {
// 表已存在,忽略错误
}
运行:
cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 7:Commit
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: 新增五维基础分表及迁移"
文件:
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:
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:运行测试确认失败
cd cfc-backend && mvn test -pl . -Dtest=FiveDimensionScoreServiceTest
预期:FAIL(方法不存在)
创建 cfc-backend/src/main/java/com/etotem/cfc/service/FiveDimensionScoreService.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:运行测试确认通过
cd cfc-backend && mvn test -pl . -Dtest=FiveDimensionScoreServiceTest
预期:PASS
[ ] 步骤 5:Commit
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: 新增五维基础分计算服务"
文件:
cfc-backend/src/main/java/com/etotem/cfc/dto/TianpanMemberVO.javacfc-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 中追加字段:
private Map<String, Integer> dimensionBaseScores; // 五维基础分
private String zodiacName; // 生肖中文名(rat→鼠等)
在 TianpanService.java 的 enrichMembers() 方法(第 216 行附近)追加:
// 注入新服务
@Resource
private FiveDimensionScoreService fiveDimensionScoreService;
在 enrichMembers() 循环末尾(vo.setEightCharacters(...) 之后)追加:
// 生肖中文名
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);
添加辅助方法:
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);
}
在 TianpanController.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:编译验证
cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 5:Commit
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: 扩展天盘服务支持五维基础分展示和重算"
文件:
修改:cfc-frontend/pages/tianpan/index.vue
[ ] 步骤 1:增强 drawMemberNodes
在 drawMemberNodes 方法(第 375 行附近)的成员节点绘制后追加:
// 在 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)
}
在模板中(第 98-119 行),基本信息区追加八字和灵数,能量区追加五维基础分:
<!-- 新增:生命灵数 -->
<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 中追加:
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 之后追加五维基础分展示:
<!-- 新增:五维基础分 -->
<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 中追加:
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'
}
运行:
node -c cfc-frontend/pages/tianpan/index.vue 2>&1 || echo "需要检查script块语法"
由于这是 .vue 文件,手动检查 script 块语法:
sed -n '/<script>/,/<\/script>/p' cfc-frontend/pages/tianpan/index.vue | sed '1d;$d' | node --check -
预期:无报错
[ ] 步骤 4:Commit
git add cfc-frontend/pages/tianpan/index.vue
git commit -m "feat(frontend): 天盘首页canvas节点标注生肖灵数+成员弹窗增强"
文件:
cfc-frontend/pages/tianpan/traditional.vue修改:cfc-frontend/pages.json
[ ] 步骤 1:注册页面
在 cfc-frontend/pages.json 的 pages/tianpan 分包中添加:
{
"path": "pages/tianpan/traditional",
"style": {
"navigationBarTitleText": "家庭传统命理"
}
}
创建 cfc-frontend/pages/tianpan/traditional.vue:
<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:语法检查
sed -n '/<script>/,/<\/script>/p' cfc-frontend/pages/tianpan/traditional.vue | sed '1d;$d' | node --check -
预期:无报错
[ ] 步骤 4:Commit
git add cfc-frontend/pages.json cfc-frontend/pages/tianpan/traditional.vue
git commit -m "feat(frontend): 新增传统命理页面"
完成所有任务后,执行以下验证:
后端编译
cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
单测通过
cd cfc-backend && mvn test -Dtest=FiveDimensionScoreServiceTest
预期:全部 PASS
前端语法检查
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
小程序限制自查
?.:key 表达式new Date(string)CI 门禁
cd cfc-frontend && node scripts/audit-duplicate-api-calls.js
预期:退出码 0