# Sub-Plan C: uni-app 小程序开发
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development
> **前置依赖:** HBuilderX 或 Vue CLI uni-app 插件、后端 API 就绪
**目标:** 基于 uni-app 实现「能量盘+聊天」一体式小程序
**参考代码:** `miniprogram/` 目录下现有微信原生代码的 UI 设计和计算逻辑
---
## 项目初始化
```
client/
├── pages/
│ ├── index/ # 首页(输入 + 能量盘 + 聊天)
│ ├── records/ # 历史能量盘列表
│ ├── profile/ # 个人中心(信息/配额/佣金)
│ └── subscribe/ # 付费页面
├── components/
│ ├── triangle/ # Classic A Canvas 渲染
│ ├── zone-card/ # 区域解读卡片
│ └── chart-input/ # 生日输入表单
├── api/
│ ├── request.js # 统一请求封装(JWT 注入)
│ ├── auth.js
│ ├── chart.js
│ ├── chat.js
│ └── payment.js
├── store/
│ └── index.js # Pinia 状态
├── utils/
│ └── calculator.js # Classic A 计算引擎
├── App.vue
├── main.js
├── pages.json
├── manifest.json
└── uni.scss
```
---
### Task 1: 项目骨架搭建
**Files:**
- Create: `client/` 整个目录结构
- Create: `manifest.json`(配置 AppID 为 uni-app 格式)
- Create: `pages.json`(页面路由 + TabBar)
- [ ] **Step 1: 创建 uni-app 项目**
Run: `npx degit dcloudio/uni-preset-vue#vite client` 或 HBuilderX 创建
- [ ] **Step 2: pages.json**
```json
{
"pages": [
{"path": "pages/index/index", "style": {"navigationBarTitleText": "数字能量盘"}},
{"path": "pages/records/records", "style": {"navigationBarTitleText": "能量盘记录"}},
{"path": "pages/profile/profile", "style": {"navigationBarTitleText": "我的"}},
{"path": "pages/subscribe/subscribe", "style": {"navigationBarTitleText": "开通能量师"}}
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#4A148C",
"backgroundColor": "#F5F5F5"
},
"tabBar": {
"color": "#999",
"selectedColor": "#4A148C",
"list": [
{"pagePath": "pages/index/index", "text": "首页", "iconPath": "static/tab/home.png"},
{"pagePath": "pages/records/records", "text": "记录", "iconPath": "static/tab/records.png"},
{"pagePath": "pages/profile/profile", "text": "我的", "iconPath": "static/tab/profile.png"}
]
}
}
```
- [ ] **Step 3: Commit**
---
### Task 2: Classic A 计算引擎(迁移适配)
**Files:**
- Create: `utils/calculator.js`
- [ ] **Step 1: 创建 calculator.js**
```javascript
export function reduceToDigit(n) {
if (n === 11 || n === 22 || n === 33) return n
while (n > 9) {
let sum = 0
while (n > 0) { sum += n % 10; n = Math.floor(n / 10) }
if (sum === 11 || sum === 22 || sum === 33) return sum
n = sum
}
return n
}
export function calculateTriangle(birthYear, birthMonth, birthDay) {
const y = String(birthYear).padStart(4, '0')
const m = String(birthMonth).padStart(2, '0')
const d = String(birthDay).padStart(2, '0')
const A = parseInt(y[0]), B = parseInt(y[1])
const C = reduceToDigit(parseInt(y[2]) + parseInt(y[3]))
const D = reduceToDigit(parseInt(m[0]) + parseInt(m[1]))
const E = reduceToDigit(parseInt(d[0]) + parseInt(d[1]))
// Row 2
const F = reduceToDigit(A + B), G = reduceToDigit(B + C)
const N = reduceToDigit(C + D), H = reduceToDigit(D + E)
// Row 3
const J = reduceToDigit(F + G), M = reduceToDigit(G + N), L = reduceToDigit(N + H)
// Row 4
const I = reduceToDigit(J + M), K = reduceToDigit(M + L)
// Top
const O = reduceToDigit(I + K)
const positions = { A, B, C, D, E, F, G, N, H, J, M, L, I, K, O }
const zones = {
mainCharacter: { name: '主性格', positions: ['O'], values: [O] },
fatherSource: { name: '父源区', positions: ['I', 'J'], values: [I, J] },
motherSource: { name: '母源区', positions: ['K', 'L'], values: [K, L] },
leftZone: { name: '左区(0-20岁)', positions: ['A', 'F', 'I', 'O'], values: [A, F, I, O] },
middleZone: { name: '中区(20-40岁)', positions: ['B', 'G', 'J', 'M', 'N', 'O'], values: [B, G, J, M, N, O] },
rightZone: { name: '右区(40-60岁)', positions: ['C', 'D', 'E', 'H', 'K', 'L', 'O'], values: [C, D, E, H, K, L, O] },
}
return {
positions, zones,
mainCharacter: O,
isMasterNumber: [11, 22, 33].includes(O)
}
}
export const NUMBER_MEANINGS = {
1: { title: '开创者', trait: '独立、创新、领导力', color: '#E53935' },
2: { title: '合作者', trait: '细腻、敏感、善于沟通', color: '#FB8C00' },
3: { title: '表达者', trait: '创意、社交、乐观向上', color: '#FDD835' },
4: { title: '建设者', trait: '务实、稳定、规则意识', color: '#43A047' },
5: { title: '自由者', trait: '冒险、变化、适应力强', color: '#1E88E5' },
6: { title: '守护者', trait: '责任、关爱、追求完美', color: '#8E24AA' },
7: { title: '探索者', trait: '深度思考、求知、内省', color: '#00ACC1' },
8: { title: '掌控者', trait: '权威、商业头脑、决策力', color: '#F4511E' },
9: { title: '智慧者', trait: '博爱、理想主义、包容', color: '#3949AB' },
11: { title: '启迪者', trait: '直觉力强、灵感丰富', color: '#FFD700' },
22: { title: '建造大师', trait: '将理想变为现实', color: '#FFD700' },
33: { title: '大爱者', trait: '无私奉献、疗愈他人', color: '#FFD700' },
}
export const POSITION_NAMES = {
A: '年位一', B: '年位二', C: '年位三', D: '月位', E: '日位',
F: '父源一', G: '中位一', N: '中位二', H: '母源一',
J: '父源二', M: '高位', L: '母源二',
I: '父源三', K: '母源三', O: '主性格',
}
```
- [ ] **Step 2: 验证**
```javascript
const r = calculateTriangle(1990, 1, 1)
console.log(r.mainCharacter) // 应输出 7
console.log(r.positions) // 15格数据
```
- [ ] **Step 3: Commit**
---
### Task 3: API 层封装
**Files:**
- Create: `api/request.js`
- Create: `api/auth.js`
- Create: `api/chart.js`
- Create: `api/chat.js`
- Create: `api/payment.js`
- [ ] **Step 1: request.js**
```javascript
const BASE_URL = 'https://your-api.com'
function request(path, data) {
const token = uni.getStorageSync('token')
return new Promise((resolve, reject) => {
uni.request({
url: BASE_URL + path,
method: 'POST',
data,
header: {
'Content-Type': 'application/json',
'Authorization': token ? 'Bearer ' + token : ''
},
success: (res) => {
if (res.data.code === 0) resolve(res.data.data)
else if (res.data.code === 1001) {
uni.removeStorageSync('token')
uni.navigateTo({ url: '/pages/profile/profile' })
reject(res.data)
} else reject(res.data)
},
fail: reject
})
})
}
export default request
```
- [ ] **Step 2: API 模块**
```javascript
// api/auth.js
import request from './request'
export const login = (code, referrerCode) => request('/api/auth/login', { code, referrerCode })
// api/chart.js
export const createChart = (data) => request('/api/charts/create', data)
export const chartList = (page, size) => request('/api/charts/list', { page, size })
export const chartDetail = (id) => request('/api/charts/detail', { id })
export const deleteChart = (id) => request('/api/charts/delete', { id })
// api/chat.js
export const sendMessage = (chartId, message) => request('/api/chat/send', { chartId, message })
export const chatHistory = (chartId) => request('/api/chat/history', { chartId })
// api/payment.js
export const unifiedOrder = (productType) => request('/api/pay/unified-order', { productType })
```
- [ ] **Step 3: Commit**
---
### Task 4: 首页 — 输入组件
**Files:**
- Create: `components/chart-input/index.vue`
- Modify: `pages/index/index.vue`
- [ ] **Step 1: chart-input/index.vue**
```vue
✦
数字能量盘
输入出生信息,生成你的专属数字三角形
姓名(选填)
{{ year || '年' }}
{{ month || '月' }}
{{ day || '日' }}
```
- [ ] **Step 2: Commit**
---
### Task 5: 首页 — Class A Canvas 三角组件
**Files:**
- Create: `components/triangle/index.vue`
- [ ] **Step 1: triangle/index.vue**
```vue
```
- [ ] **Step 2: Commit**
---
### Task 6: 首页 — 聊天窗口
**Files:**
- Modify: `pages/index/index.vue`(集成输入+三角+聊天一体化)
- Create: `store/index.js`
- [ ] **Step 1: 首页一体化**
```vue
{{ msg.role === 'assistant' ? '🤖' : '👤' }}
{{ msg.content }}
🤖
解读中...
```
- [ ] **Step 2: Commit**
---
### Task 7: 个人中心 + 历史记录
**Files:**
- Create: `pages/profile/profile.vue`
- Create: `pages/records/records.vue`(简化版,在 profile 中已含列表)
- [ ] **Step 1: profile/profile.vue(参考现有 miniprogram/pages/profile/ 的 UI)**
```vue
{{ initial }}
{{ nickName || '未登录' }}
{{ isVip ? '能量师' : '普通用户' }}
能量师有效期至 {{ vipExpireAt }}
推广码: {{ referralCode }}
今日能量盘: {{ chartCount }}/3
今日聊天: {{ chatCount }}/5
📊 我的收益
{{ (commission.available/100).toFixed(0) }}可提现
{{ (commission.totalEarnings/100).toFixed(0) }}累计收益
```
- [ ] **Step 2: 历史记录列表(profile 中内联),records 页可作为单独的 Tab 页显示全部**
- [ ] **Step 3: Commit**
---
### Task 8: 付费订阅页
**Files:**
- Create: `pages/subscribe/subscribe.vue`
- [ ] **Step 1: subscribe.vue(参考现有 miniprogram/pages/subscribe/ UI)**
```vue
¥398
/年
每天仅 ¥1.09,让数字能量成为你的生产力
✅ 无限能量盘生成,不限次数
✅ AI 五区完整解读,随问随答
✅ 无限分享 & PDF 导出
✅ 全部历史记录
✅ 推广赚佣金,¥119.4/人
能量盘生成3次/天✅ 不限
AI 解读主性格概要✅ 完整解读
分享1次/天✅ 不限
推广佣金❌✅ 最高¥159.2/人
支付即表示同意《订阅协议》
```
- [ ] **Step 2: Commit**