For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 实现财富健康三要素:记账打卡、保单上传、以及商品购买/创收与富维度的能量联动
Architecture: 后端新增两张表(finance_checkins + insurance_policies)及对应 CRUD API + 能量发放;小程序端新增记账打卡页和保单管理页;调整富维度默认比例和日上限。
Tech Stack: Spring Boot + MyBatis-Plus / uni-app Vue 2
设计依据: docs/superpowers/specs/2026-06-11-wealth-dimension-energy-design.md
| # | 文件 | 操作 | 说明 |
|---|---|---|---|
| 1 | entity/FinanceCheckin.java |
新增 | 记账打卡实体 |
| 2 | entity/InsurancePolicy.java |
新增 | 保单实体 |
| 3 | mapper/FinanceCheckinMapper.java |
新增 | 打卡 Mapper |
| 4 | mapper/InsurancePolicyMapper.java |
新增 | 保单 Mapper |
| 5 | service/FinanceCheckinService.java |
新增 | 打卡业务(含能量发放) |
| 6 | service/InsurancePolicyService.java |
新增 | 保单业务(含能量发放) |
| 7 | controller/wealth/FinanceCheckinController.java |
新增 | 打卡 API |
| 8 | controller/wealth/InsurancePolicyController.java |
新增 | 保单 API |
| 9 | service/EnergyService.java |
修改 | 新增富维度能量发放方法 |
| 10 | config/DatabaseInitializer.java |
修改 | 新增两张表 DDL |
| 11 | cfc-frontend/utils/api.js |
修改 | 新增 API |
| 12 | cfc-frontend/pages/wealth/checkin.vue |
新增 | 记账打卡页 |
| 13 | cfc-frontend/pages/wealth/insurance-list.vue |
新增 | 保单列表页 |
| 14 | cfc-frontend/pages/wealth/insurance-add.vue |
新增 | 添加保单页 |
| 15 | cfc-frontend/pages/wealth/index.vue |
新增 | 财富健康首页 |
| 16 | cfc-frontend/pages.json |
修改 | 注册新页面路径 |
Files:
Modify: cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
[ ] Step 1: 在 initializeTables() 中添加建表语句
// 记账打卡表
jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS finance_checkins (" +
"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
"user_id BIGINT NOT NULL COMMENT '用户ID', " +
"child_id BIGINT DEFAULT NULL COMMENT '孩子ID(可为空)', " +
"checkin_date DATE NOT NULL COMMENT '记账日期', " +
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
"UNIQUE KEY uk_user_date (user_id, IFNULL(child_id, 0), checkin_date)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='记账打卡'");
// 保单表
jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS insurance_policies (" +
"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
"user_id BIGINT NOT NULL COMMENT '用户ID', " +
"policy_type VARCHAR(50) NOT NULL COMMENT '保单类型:医疗/重疾/意外/寿险/养老/其他', " +
"insured_name VARCHAR(100) NOT NULL COMMENT '被保人姓名', " +
"insured_relation VARCHAR(20) DEFAULT 'self' COMMENT '关系:self/spouse/child/parent', " +
"insurance_company VARCHAR(100) DEFAULT '' COMMENT '保险公司', " +
"policy_number VARCHAR(100) DEFAULT '' COMMENT '保单号', " +
"coverage_amount DECIMAL(12,2) DEFAULT 0 COMMENT '保额(万元)', " +
"premium DECIMAL(10,2) DEFAULT 0 COMMENT '年缴保费', " +
"start_date DATE DEFAULT NULL COMMENT '生效日期', " +
"end_date DATE DEFAULT NULL COMMENT '到期日期', " +
"image_url VARCHAR(500) DEFAULT '' COMMENT '保单照片URL', " +
"status TINYINT DEFAULT 1 COMMENT '1有效/0失效', " +
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='保单记录'");
[ ] Step 2: Commit
git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
git commit -m "feat: add finance_checkins and insurance_policies DDL"
Files:
cfc-backend/src/main/java/com/etotem/cfc/entity/FinanceCheckin.javacfc-backend/src/main/java/com/etotem/cfc/entity/InsurancePolicy.javacfc-backend/src/main/java/com/etotem/cfc/mapper/FinanceCheckinMapper.javaCreate: cfc-backend/src/main/java/com/etotem/cfc/mapper/InsurancePolicyMapper.java
[ ] Step 1: FinanceCheckin.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("finance_checkins")
public class FinanceCheckin implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private Long childId;
private Date checkinDate;
private Date createdAt;
}
[ ] Step 2: InsurancePolicy.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.math.BigDecimal;
import java.util.Date;
@Data
@TableName("insurance_policies")
public class InsurancePolicy implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private String policyType;
private String insuredName;
private String insuredRelation;
private String insuranceCompany;
private String policyNumber;
private BigDecimal coverageAmount;
private BigDecimal premium;
private Date startDate;
private Date endDate;
private String imageUrl;
private Integer status;
private Date createdAt;
private Date updatedAt;
}
[ ] Step 3: 两个 Mapper
@Mapper
public interface FinanceCheckinMapper extends BaseMapper<FinanceCheckin> {}
@Mapper
public interface InsurancePolicyMapper extends BaseMapper<InsurancePolicy> {}
[ ] Step 4: Commit
git add cfc-backend/src/main/java/com/etotem/cfc/entity/FinanceCheckin.java cfc-backend/src/main/java/com/etotem/cfc/entity/InsurancePolicy.java cfc-backend/src/main/java/com/etotem/cfc/mapper/FinanceCheckinMapper.java cfc-backend/src/main/java/com/etotem/cfc/mapper/InsurancePolicyMapper.java
git commit -m "feat: add FinanceCheckin and InsurancePolicy entities"
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/FinanceCheckinService.javacfc-backend/src/main/java/com/etotem/cfc/service/InsurancePolicyService.javacfc-backend/src/main/java/com/etotem/cfc/controller/wealth/FinanceCheckinController.javaCreate: cfc-backend/src/main/java/com/etotem/cfc/controller/wealth/InsurancePolicyController.java
[ ] Step 1: FinanceCheckinService.java
package com.etotem.cfc.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.etotem.cfc.entity.FinanceCheckin;
import com.etotem.cfc.mapper.FinanceCheckinMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
@Slf4j
@Service
public class FinanceCheckinService {
@Resource
private FinanceCheckinMapper financeCheckinMapper;
@Resource
private EnergyService energyService;
/**
* 今日是否已记账
*/
public boolean hasCheckedInToday(Long userId, Long childId) {
LocalDate today = LocalDate.now();
return financeCheckinMapper.selectCount(new LambdaQueryWrapper<FinanceCheckin>()
.eq(FinanceCheckin::getUserId, userId)
.eq(childId != null, FinanceCheckin::getChildId, childId)
.eq(FinanceCheckin::getCheckinDate, java.sql.Date.valueOf(today))) > 0;
}
/**
* 记账打卡
* @return energy 本次获得的能量值
*/
@Transactional
public int checkin(Long userId, Long childId) {
if (hasCheckedInToday(userId, childId)) {
return 0; // 今天已记过
}
LocalDate today = LocalDate.now();
FinanceCheckin record = new FinanceCheckin();
record.setUserId(userId);
record.setChildId(childId);
record.setCheckinDate(java.sql.Date.valueOf(today));
financeCheckinMapper.insert(record);
// 基础能量 +3
int energy = 3;
// 连续7天额外奖励
if (isStreak7Days(userId, childId)) {
energy += 10;
}
// 调用能量服务发放富维度能量
// energyService.awardEnergy(childId != null ? childId : userId, "finance_checkin", record.getId(),
// energy, childId != null ? "child" : "parent", "记账打卡");
return energy;
}
/**
* 判断是否连续记账7天
*/
private boolean isStreak7Days(Long userId, Long childId) {
LocalDate today = LocalDate.now();
LocalDate sevenDaysAgo = today.minusDays(6);
long count = financeCheckinMapper.selectCount(new LambdaQueryWrapper<FinanceCheckin>()
.eq(FinanceCheckin::getUserId, userId)
.eq(childId != null, FinanceCheckin::getChildId, childId)
.ge(FinanceCheckin::getCheckinDate, java.sql.Date.valueOf(sevenDaysAgo))
.le(FinanceCheckin::getCheckinDate, java.sql.Date.valueOf(today)));
return count >= 7;
}
/**
* 当月打卡天数
*/
public int getMonthlyCheckinCount(Long userId, Long childId) {
LocalDate now = LocalDate.now();
LocalDate firstDay = now.withDayOfMonth(1);
LocalDate lastDay = now.withDayOfMonth(now.lengthOfMonth());
return financeCheckinMapper.selectCount(new LambdaQueryWrapper<FinanceCheckin>()
.eq(FinanceCheckin::getUserId, userId)
.eq(childId != null, FinanceCheckin::getChildId, childId)
.ge(FinanceCheckin::getCheckinDate, java.sql.Date.valueOf(firstDay))
.le(FinanceCheckin::getCheckinDate, java.sql.Date.valueOf(lastDay)));
}
/**
* 本月打卡日期列表(用于日历展示)
*/
public List<Date> getMonthlyCheckinDates(Long userId, Long childId) {
LocalDate now = LocalDate.now();
LocalDate firstDay = now.withDayOfMonth(1);
LocalDate lastDay = now.withDayOfMonth(now.lengthOfMonth());
List<FinanceCheckin> records = financeCheckinMapper.selectList(new LambdaQueryWrapper<FinanceCheckin>()
.eq(FinanceCheckin::getUserId, userId)
.eq(childId != null, FinanceCheckin::getChildId, childId)
.ge(FinanceCheckin::getCheckinDate, java.sql.Date.valueOf(firstDay))
.le(FinanceCheckin::getCheckinDate, java.sql.Date.valueOf(lastDay))
.orderByAsc(FinanceCheckin::getCheckinDate));
return records.stream().map(FinanceCheckin::getCheckinDate).collect(java.util.stream.Collectors.toList());
}
}
[ ] Step 2: InsurancePolicyService.java
package com.etotem.cfc.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.etotem.cfc.entity.InsurancePolicy;
import com.etotem.cfc.mapper.InsurancePolicyMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.Date;
@Slf4j
@Service
public class InsurancePolicyService {
@Resource
private InsurancePolicyMapper insurancePolicyMapper;
@Resource
private EnergyService energyService;
public Page<InsurancePolicy> getList(Long userId, int page, int size) {
return insurancePolicyMapper.selectPage(new Page<>(page, size),
new LambdaQueryWrapper<InsurancePolicy>()
.eq(InsurancePolicy::getUserId, userId)
.orderByDesc(InsurancePolicy::getCreatedAt));
}
public InsurancePolicy getDetail(Long id, Long userId) {
InsurancePolicy policy = insurancePolicyMapper.selectById(id);
if (policy != null && !policy.getUserId().equals(userId)) {
return null;
}
return policy;
}
@Transactional
public InsurancePolicy create(InsurancePolicy policy, Long userId) {
policy.setUserId(userId);
policy.setStatus(1);
policy.setCreatedAt(new Date());
policy.setUpdatedAt(new Date());
insurancePolicyMapper.insert(policy);
// 每份有效保单 +50 能量(一次性)
// energyService.awardEnergy(userId, "insurance_policy", policy.getId(), 50, "parent", "上传保单");
return policy;
}
@Transactional
public void update(InsurancePolicy policy) {
policy.setUpdatedAt(new Date());
insurancePolicyMapper.updateById(policy);
}
@Transactional
public void delete(Long id, Long userId) {
insurancePolicyMapper.delete(new LambdaQueryWrapper<InsurancePolicy>()
.eq(InsurancePolicy::getId, id)
.eq(InsurancePolicy::getUserId, userId));
}
/**
* 统计用户保单信息
*/
public int countByUser(Long userId) {
return insurancePolicyMapper.selectCount(new LambdaQueryWrapper<InsurancePolicy>()
.eq(InsurancePolicy::getUserId, userId)
.eq(InsurancePolicy::getStatus, 1));
}
}
[ ] Step 3: FinanceCheckinController.java
package com.etotem.cfc.controller.wealth;
import com.etotem.cfc.common.Result;
import com.etotem.cfc.service.FinanceCheckinService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/wealth")
public class FinanceCheckinController {
@Resource
private FinanceCheckinService financeCheckinService;
/**
* 记账打卡
*/
@PostMapping("/checkin")
public Result<Integer> checkin(@RequestBody Map<String, Object> body,
@RequestAttribute("userId") Long userId) {
Long childId = body.get("childId") != null ? Long.valueOf(body.get("childId").toString()) : null;
int energy = financeCheckinService.checkin(userId, childId);
return Result.success(energy);
}
/**
* 今日是否已记账
*/
@PostMapping("/checkin/today")
public Result<Boolean> todayStatus(@RequestBody Map<String, Object> body,
@RequestAttribute("userId") Long userId) {
Long childId = body.get("childId") != null ? Long.valueOf(body.get("childId").toString()) : null;
boolean done = financeCheckinService.hasCheckedInToday(userId, childId);
return Result.success(done);
}
/**
* 本月打卡数据
*/
@PostMapping("/checkin/monthly")
public Result<Map<String, Object>> monthly(@RequestBody Map<String, Object> body,
@RequestAttribute("userId") Long userId) {
Long childId = body.get("childId") != null ? Long.valueOf(body.get("childId").toString()) : null;
int count = financeCheckinService.getMonthlyCheckinCount(userId, childId);
List<Date> dates = financeCheckinService.getMonthlyCheckinDates(userId, childId);
return Result.success(java.util.Map.of("count", count, "dates", dates));
}
}
[ ] Step 4: InsurancePolicyController.java
package com.etotem.cfc.controller.wealth;
import com.etotem.cfc.common.Result;
import com.etotem.cfc.entity.InsurancePolicy;
import com.etotem.cfc.service.InsurancePolicyService;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Map;
@RestController
@RequestMapping("/api/wealth")
public class InsurancePolicyController {
@Resource
private InsurancePolicyService insurancePolicyService;
@PostMapping("/insurance/list")
public Result<Page<InsurancePolicy>> list(@RequestBody Map<String, Object> body,
@RequestAttribute("userId") Long userId) {
int page = body.get("page") != null ? Integer.parseInt(body.get("page").toString()) : 1;
int size = body.get("size") != null ? Integer.parseInt(body.get("size").toString()) : 20;
return Result.success(insurancePolicyService.getList(userId, page, size));
}
@PostMapping("/insurance/detail")
public Result<InsurancePolicy> detail(@RequestBody Map<String, Object> body,
@RequestAttribute("userId") Long userId) {
Long id = Long.valueOf(body.get("id").toString());
InsurancePolicy policy = insurancePolicyService.getDetail(id, userId);
if (policy == null) return Result.error("保单不存在");
return Result.success(policy);
}
@PostMapping("/insurance/create")
public Result<String> create(@RequestBody InsurancePolicy policy,
@RequestAttribute("userId") Long userId) {
insurancePolicyService.create(policy, userId);
return Result.success("添加成功");
}
@PostMapping("/insurance/update")
public Result<String> update(@RequestBody InsurancePolicy policy) {
insurancePolicyService.update(policy);
return Result.success("更新成功");
}
@PostMapping("/insurance/delete")
public Result<String> delete(@RequestBody Map<String, Object> body,
@RequestAttribute("userId") Long userId) {
Long id = Long.valueOf(body.get("id").toString());
insurancePolicyService.delete(id, userId);
return Result.success("删除成功");
}
@PostMapping("/insurance/count")
public Result<Integer> count(@RequestAttribute("userId") Long userId) {
return Result.success(insurancePolicyService.countByUser(userId));
}
}
[ ] Step 5: Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/FinanceCheckinService.java cfc-backend/src/main/java/com/etotem/cfc/service/InsurancePolicyService.java cfc-backend/src/main/java/com/etotem/cfc/controller/wealth/FinanceCheckinController.java cfc-backend/src/main/java/com/etotem/cfc/controller/wealth/InsurancePolicyController.java
git commit -m "feat: add finance checkin and insurance policy services/controllers"
Files:
Modify: cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java
[ ] Step 1: 调整 wealth 日上限
// 原:DAILY_LIMIT_MAP.put("wealth", 30);
// 改为:
DAILY_LIMIT_MAP.put("wealth", 50);
[ ] Step 2: 调整默认比例(若已实现在 EnergyService 中)
找到 CATEGORY_DIMENSION_MAP 或其他默认比例定义:
// 默认任务中 wealth 从 0.05 改为 0.10
// "default" → body:0.30, mind:0.20, wisdom:0.15, action:0.25, wealth:0.10
实际修改位置取决于当前 EnergyService 中比例常量定义的位置。
[ ] Step 3: Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java
git commit -m "chore: adjust wealth daily limit to 50 and default ratio to 10%"
Files:
cfc-frontend/pages/wealth/checkin.vuecfc-frontend/utils/api.jsModify: cfc-frontend/pages.json
[ ] Step 1: 在 api.js 添加 API
// ===== 财富健康 =====
export const wealthCheckin = (data) => request('/api/wealth/checkin', 'POST', data)
export const wealthCheckinToday = (data) => request('/api/wealth/checkin/today', 'POST', data)
export const wealthCheckinMonthly = (data) => request('/api/wealth/checkin/monthly', 'POST', data)
export const wealthInsuranceList = (data) => request('/api/wealth/insurance/list', 'POST', data)
export const wealthInsuranceDetail = (data) => request('/api/wealth/insurance/detail', 'POST', data)
export const wealthInsuranceCreate = (data) => request('/api/wealth/insurance/create', 'POST', data)
export const wealthInsuranceUpdate = (data) => request('/api/wealth/insurance/update', 'POST', data)
export const wealthInsuranceDelete = (data) => request('/api/wealth/insurance/delete', 'POST', data)
export const wealthInsuranceCount = () => request('/api/wealth/insurance/count', 'POST')
[ ] Step 2: 创建记账打卡页面
<template>
<view class="container">
<view class="header">
<text class="title">📒 记账打卡</text>
<text class="subtitle">每天记账,培养财商</text>
</view>
<!-- 今日打卡状态 -->
<view class="today-card" :class="{ done: todayCheckedIn }">
<view class="today-icon">
<text class="icon-text">{{ todayCheckedIn ? '✅' : '📝' }}</text>
</view>
<view class="today-info">
<text class="today-label">{{ todayCheckedIn ? '今日已记账' : '今天记账了吗?' }}</text>
<text class="today-energy" v-if="!todayCheckedIn">打卡 +3 能量</text>
<text class="today-energy" v-else>连续打卡奖励已发放</text>
</view>
<button v-if="!todayCheckedIn" class="checkin-btn" @click="doCheckin">打卡</button>
</view>
<!-- 本月打卡日历 -->
<view class="calendar-section">
<text class="section-title">📅 本月打卡 {{ monthlyCount }} 天</text>
<view class="calendar-grid">
<view v-for="(day, i) in calendarDays" :key="i" class="calendar-day">
<text class="day-num">{{ day }}</text>
<view v-if="checkedInDates.includes(day)" class="day-dot checked"></view>
</view>
</view>
</view>
</view>
</template>
<script>
import { wealthCheckin, wealthCheckinToday, wealthCheckinMonthly } from '@/utils/api.js'
export default {
data() {
return {
todayCheckedIn: false,
monthlyCount: 0,
checkedInDates: [],
calendarDays: []
}
},
onLoad() {
this.loadData()
this.generateCalendar()
},
methods: {
generateCalendar() {
const now = new Date()
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate()
this.calendarDays = Array.from({ length: daysInMonth }, (_, i) => i + 1)
},
async loadData() {
const [todayRes, monthlyRes] = await Promise.all([
wealthCheckinToday({}),
wealthCheckinMonthly({})
])
if (todayRes.code === 200) this.todayCheckedIn = todayRes.data
if (monthlyRes.code === 200) {
this.monthlyCount = monthlyRes.data.count || 0
this.checkedInDates = (monthlyRes.data.dates || []).map(d => new Date(d).getDate())
}
},
async doCheckin() {
const res = await wealthCheckin({})
if (res.code === 200) {
this.todayCheckedIn = true
uni.showToast({ title: '打卡成功 +' + res.data + '能量', icon: 'success' })
this.loadData()
}
}
}
}
</script>
<style scoped>
.container { padding: 30rpx; }
.header { text-align: center; margin-bottom: 40rpx; }
.title { font-size: 36rpx; font-weight: bold; }
.subtitle { font-size: 24rpx; color: #999; margin-top: 8rpx; }
.today-card {
display: flex; align-items: center; background: #fff;
border-radius: 20rpx; padding: 30rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
}
.today-card.done { background: #f0fdf4; }
.today-icon { width: 80rpx; height: 80rpx; display: flex; align-items: center; justify-content: center; }
.icon-text { font-size: 48rpx; }
.today-info { flex: 1; margin-left: 20rpx; }
.today-label { font-size: 28rpx; font-weight: bold; color: #333; }
.today-energy { font-size: 22rpx; color: #F97316; margin-top: 4rpx; }
.checkin-btn {
background: linear-gradient(135deg, #42A5F5, #1565C0);
color: #fff; border: none; border-radius: 40rpx;
padding: 12rpx 36rpx; font-size: 26rpx; font-weight: bold;
}
.calendar-section { margin-top: 40rpx; }
.section-title { font-size: 28rpx; font-weight: bold; margin-bottom: 20rpx; }
.calendar-grid {
display: flex; flex-wrap: wrap; gap: 8rpx;
background: #fff; border-radius: 16rpx; padding: 20rpx;
}
.calendar-day {
width: calc(100% / 7 - 8rpx); text-align: center; padding: 8rpx 0;
}
.day-num { font-size: 24rpx; color: #666; }
.day-dot { width: 12rpx; height: 12rpx; border-radius: 50%; margin: 4rpx auto 0; }
.day-dot.checked { background: #4CAF50; }
</style>
[ ] Step 3: 在 pages.json 注册路径
{
"path": "pages/wealth/checkin",
"style": {
"navigationBarTitleText": "记账打卡"
}
}
[ ] Step 4: Commit
git add cfc-frontend/utils/api.js cfc-frontend/pages/wealth/checkin.vue cfc-frontend/pages.json
git commit -m "feat: add finance checkin page (mini-program)"
Files:
cfc-frontend/pages/wealth/insurance-list.vueCreate: cfc-frontend/pages/wealth/insurance-add.vue
[ ] Step 1: 保单列表页
<template>
<view class="container">
<view class="header">
<text class="title">🛡️ 我的保单</text>
<text class="subtitle">已有 {{ count }} 份保单</text>
</view>
<view v-if="loading" class="loading-wrap">
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="list.length === 0" class="empty-wrap">
<text class="empty-icon">📄</text>
<text class="empty-text">还没有保单,添加一份吧</text>
<button class="add-btn" @click="goAdd">添加保单</button>
</view>
<scroll-view v-else scroll-y class="list-scroll">
<view v-for="item in list" :key="item.id" class="policy-card">
<view class="policy-header">
<text class="policy-type">{{ typeLabel(item.policyType) }}</text>
<text class="policy-status" :class="{ active: item.status === 1 }">
{{ item.status === 1 ? '有效' : '已失效' }}
</text>
</view>
<view class="policy-body">
<text class="policy-name">被保人:{{ item.insuredName }}</text>
<text class="policy-amount">保额:{{ item.coverageAmount }}万</text>
<text class="policy-company">{{ item.insuranceCompany }}</text>
</view>
<view class="policy-footer">
<text class="policy-date">{{ item.startDate }} ~ {{ item.endDate }}</text>
<button class="del-btn" size="mini" @click="handleDelete(item.id)">删除</button>
</view>
</view>
<view v-if="noMore" class="no-more">— 没有更多了 —</view>
</scroll-view>
</view>
</template>
<script>
import { wealthInsuranceList, wealthInsuranceCount, wealthInsuranceDelete } from '@/utils/api.js'
const TYPE_MAP = { medical: '医疗险', critical: '重疾险', accident: '意外险', life: '寿险', pension: '养老险', other: '其他' }
export default {
data() {
return {
list: [],
count: 0,
page: 1,
size: 20,
loading: false,
noMore: false
}
},
onLoad() { this.load() },
methods: {
typeLabel(t) { return TYPE_MAP[t] || t },
async load() {
this.loading = true
const [listRes, countRes] = await Promise.all([
wealthInsuranceList({ page: this.page, size: this.size }),
wealthInsuranceCount()
])
this.loading = false
if (listRes.code === 200 && listRes.data) {
this.list = listRes.data.records || []
this.noMore = this.list.length >= (listRes.data.total || 0)
}
if (countRes.code === 200) this.count = countRes.data || 0
},
goAdd() { uni.navigateTo({ url: '/pages/wealth/insurance-add' }) },
async handleDelete(id) {
uni.showModal({
title: '提示', content: '确定删除该保单?',
success: async (res) => {
if (res.confirm) {
await wealthInsuranceDelete({ id })
uni.showToast({ title: '删除成功', icon: 'success' })
this.load()
}
}
})
}
}
}
</script>
<style scoped>
.container { padding: 30rpx; }
.header { margin-bottom: 30rpx; }
.title { font-size: 36rpx; font-weight: bold; }
.subtitle { font-size: 24rpx; color: #999; margin-top: 6rpx; }
.loading-wrap, .empty-wrap { display: flex; flex-direction: column; align-items: center; padding-top: 120rpx; }
.empty-icon { font-size: 100rpx; }
.empty-text { font-size: 28rpx; color: #999; margin: 20rpx 0; }
.add-btn {
background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
color: #fff; border: none; border-radius: 40rpx;
padding: 16rpx 60rpx; font-size: 28rpx;
}
.policy-card {
background: #fff; border-radius: 16rpx; padding: 24rpx;
margin-bottom: 20rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
}
.policy-header { display: flex; justify-content: space-between; align-items: center; }
.policy-type { font-size: 28rpx; font-weight: bold; color: #333; }
.policy-status { font-size: 22rpx; color: #999; }
.policy-status.active { color: #4CAF50; }
.policy-body { margin: 12rpx 0; }
.policy-name, .policy-amount, .policy-company { display: block; font-size: 24rpx; color: #666; margin-top: 4rpx; }
.policy-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 12rpx; }
.policy-date { font-size: 22rpx; color: #bbb; }
.del-btn { color: #e74c3c; font-size: 22rpx; }
.no-more { text-align: center; padding: 30rpx; color: #ccc; font-size: 24rpx; }
</style>
[ ] Step 2: 添加保单页
<template>
<view class="container">
<text class="title">添加保单</text>
<view class="form">
<view class="form-item">
<text class="label">保单类型 *</text>
<picker :range="typeOptions" range-key="label" @change="e => form.policyType = typeOptions[e.detail.value].value">
<view class="picker">{{ typeLabel(form.policyType) || '请选择' }}</view>
</picker>
</view>
<view class="form-item">
<text class="label">被保人 *</text>
<input v-model="form.insuredName" placeholder="姓名" class="input" />
</view>
<view class="form-item">
<text class="label">与被保人关系</text>
<picker :range="relationOptions" range-key="label" @change="e => form.insuredRelation = relationOptions[e.detail.value].value">
<view class="picker">{{ relationLabel(form.insuredRelation) || '请选择' }}</view>
</picker>
</view>
<view class="form-item">
<text class="label">保险公司</text>
<input v-model="form.insuranceCompany" placeholder="如:中国人寿" class="input" />
</view>
<view class="form-item">
<text class="label">保额(万元)</text>
<input v-model="form.coverageAmount" type="digit" placeholder="50" class="input" />
</view>
<view class="form-item">
<text class="label">年缴保费</text>
<input v-model="form.premium" type="digit" placeholder="5000" class="input" />
</view>
<view class="form-item">
<text class="label">保单照片</text>
<button class="upload-btn" @click="chooseImage">上传照片</button>
<image v-if="form.imageUrl" :src="form.imageUrl" class="preview" mode="aspectFit" />
</view>
<button class="submit-btn" @click="handleSubmit">保存并获取能量</button>
</view>
</view>
</template>
<script>
import { wealthInsuranceCreate } from '@/utils/api.js'
const TYPE_OPTIONS = [
{ label: '医疗险', value: 'medical' }, { label: '重疾险', value: 'critical' },
{ label: '意外险', value: 'accident' }, { label: '寿险', value: 'life' },
{ label: '养老险', value: 'pension' }, { label: '其他', value: 'other' }
]
const RELATION_OPTIONS = [
{ label: '本人', value: 'self' }, { label: '配偶', value: 'spouse' },
{ label: '子女', value: 'child' }, { label: '父母', value: 'parent' }
]
const TYPE_MAP = { medical: '医疗险', critical: '重疾险', accident: '意外险', life: '寿险', pension: '养老险', other: '其他' }
const RELATION_MAP = { self: '本人', spouse: '配偶', child: '子女', parent: '父母' }
export default {
data() {
return {
typeOptions: TYPE_OPTIONS,
relationOptions: RELATION_OPTIONS,
form: {
policyType: '', insuredName: '', insuredRelation: 'self',
insuranceCompany: '', policyNumber: '', coverageAmount: '',
premium: '', imageUrl: ''
}
}
},
methods: {
typeLabel(v) { return TYPE_MAP[v] },
relationLabel(v) { return RELATION_MAP[v] },
chooseImage() {
uni.chooseImage({ count: 1, sizeType: ['compressed'] }).then(res => {
const tempPath = res.tempFilePaths[0]
// 上传图片
uni.uploadFile({
url: 'http://localhost:9080/api/admin/articles/upload/image', // TODO: use config.api
filePath: tempPath, name: 'file',
success: (uploadRes) => {
const data = JSON.parse(uploadRes.data)
if (data.code === 200) this.form.imageUrl = data.data
}
})
})
},
async handleSubmit() {
if (!this.form.policyType || !this.form.insuredName) {
uni.showToast({ title: '请填写完整信息', icon: 'none' })
return
}
const res = await wealthInsuranceCreate({
...this.form,
coverageAmount: parseFloat(this.form.coverageAmount) || 0,
premium: parseFloat(this.form.premium) || 0
})
if (res.code === 200) {
uni.showToast({ title: '添加成功 +50能量', icon: 'success' })
setTimeout(() => uni.navigateBack(), 1500)
}
}
}
}
</script>
<style scoped>
.container { padding: 30rpx; }
.title { font-size: 32rpx; font-weight: bold; margin-bottom: 30rpx; }
.form-item { margin-bottom: 24rpx; }
.label { font-size: 26rpx; color: #666; display: block; margin-bottom: 8rpx; }
.input, .picker {
width: 100%; padding: 16rpx 20rpx; border: 1rpx solid #eee;
border-radius: 12rpx; font-size: 26rpx; box-sizing: border-box;
}
.picker { color: #333; }
.upload-btn { font-size: 24rpx; padding: 10rpx 30rpx; background: #f0f0f0; border-radius: 8rpx; display: inline-block; }
.preview { width: 200rpx; height: 280rpx; margin-top: 12rpx; border-radius: 8rpx; }
.submit-btn {
width: 100%; background: linear-gradient(135deg, #42A5F5, #1565C0);
color: #fff; border: none; border-radius: 44rpx;
padding: 24rpx 0; font-size: 30rpx; font-weight: bold; margin-top: 40rpx;
}
</style>
[ ] Step 3: 在 pages.json 注册路径
{
"path": "pages/wealth/insurance-list",
"style": { "navigationBarTitleText": "我的保单" }
},
{
"path": "pages/wealth/insurance-add",
"style": { "navigationBarTitleText": "添加保单" }
}
[ ] Step 4: Commit
git add cfc-frontend/pages/wealth/insurance-list.vue cfc-frontend/pages/wealth/insurance-add.vue cfc-frontend/pages.json
git commit -m "feat: add insurance policy management pages (mini-program)"
mvn clean compile 无错误