hookehuyr

fix(plan): 修复表单提交数据为空问题并添加金额格式化

修复问题:
- 修复表单提交时数据为空的问题(submit 中立即重置导致)
- 添加金额字段格式化显示(分 → 元),便于调试查看

代码改动:
- 移除 submit() 中的 resetForm() 调用,让父组件先处理数据
- 新增 formatAmounts() 函数转换金额单位(分 → 元)
- 优化日志输出:同时显示格式化数据(元)和原始数据(分)
- 表单重置逻辑统一由 close() 函数处理

文档新增:
- docs/接口联调注意事项.md - 完整的 API 联调指南
  - 数据单位规范(金额字段单位为"分")
  - 接口联调流程和注意事项
  - 金额转换工具函数
  - 常见问题和解决方案

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
......@@ -10,6 +10,7 @@ declare module 'vue' {
AgePicker: typeof import('./src/components/PlanFields/AgePicker.vue')['default']
AgePickerGlobal: typeof import('./src/components/PlanFields/AgePickerGlobal.vue')['default']
AmountInput: typeof import('./src/components/PlanFields/AmountInput.vue')['default']
AmountKeyboard: typeof import('./src/components/PlanFields/AmountKeyboard.vue')['default']
CriticalIllnessTemplate: typeof import('./src/components/PlanTemplates/CriticalIllnessTemplate.vue')['default']
DatePicker: typeof import('./src/components/PlanFields/DatePicker.vue')['default']
DatePickerGlobal: typeof import('./src/components/PlanFields/DatePickerGlobal.vue')['default']
......@@ -28,6 +29,7 @@ declare module 'vue' {
NutDatePicker: typeof import('@nutui/nutui-taro')['DatePicker']
NutEmpty: typeof import('@nutui/nutui-taro')['Empty']
NutInput: typeof import('@nutui/nutui-taro')['Input']
NutNumberKeyboard: typeof import('@nutui/nutui-taro')['NumberKeyboard']
NutPicker: typeof import('@nutui/nutui-taro')['Picker']
NutPopup: typeof import('@nutui/nutui-taro')['Popup']
NutRadio: typeof import('@nutui/nutui-taro')['Radio']
......@@ -38,7 +40,7 @@ declare module 'vue' {
PdfPreview: typeof import('./src/components/PdfPreview.vue')['default']
Picker: typeof import('./src/components/time-picker-data/picker.vue')['default']
PlanFormContainer: typeof import('./src/components/PlanFormContainer.vue')['default']
PlanPopup: typeof import('./src/components/PlanPopup/index.vue')['default']
PlanPopup: typeof import('./src/components/PlanSchemes/PlanPopup.vue')['default']
PlanPopupNew: typeof import('./src/components/PlanPopupNew.vue')['default']
PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default']
ProductCard: typeof import('./src/components/ProductCard.vue')['default']
......
......@@ -13,6 +13,14 @@ export default {
quiet: false,
stats: true
},
mini: {},
h5: {}
mini: {
// 开启 vConsole 调试工具
debug: true
},
h5: {
// H5 端也可以使用 eruda(另一种调试工具)
devServer: {
port: 10086
}
}
}
......
# 接口联调注意事项
> **文档用途**:记录计划书表单接口联调的关键注意事项和规范
>
> **最后更新**:2026-02-09
> **维护者**:Claude Code
## 📋 目录
- [数据单位规范](#数据单位规范)
- [接口联调流程](#接口联调流程)
- [关键注意事项](#关键注意事项)
- [调试技巧](#调试技巧)
- [常见问题](#常见问题)
---
## 数据单位规范
### ⚠️ 金额字段单位:分(非元)
**重要**:所有金额字段内部存储单位都是 **分**,不是元!
| 字段名 | 类型 | 单位 | 示例 | 显示 |
|--------|------|------|------|------|
| `coverage` | Number | 分 | `10000` | `100.00` 元 |
| `premium` | Number | 分 | `5000` | `50.00` 元 |
| `amount` | Number | 分 | `1000` | `10.00` 元 |
| `total_amount` | Number | 分 | `15000` | `150.00` 元 |
**转换公式**
```javascript
// 元 → 分(提交给后端)
const cents = Math.round(yuan * 100)
// 分 → 元(前端显示)
const yuan = (cents / 100).toFixed(2)
```
**为什么使用"分"?**
- ✅ 避免浮点数精度问题(`0.1 + 0.2 !== 0.3`
- ✅ 整数运算精确可靠
- ✅ 金融行业标准做法
---
## 接口联调流程
### 1. 提交计划书表单
**接口路径**`/srv/?a=submit_plan`(待确认)
**请求参数**
```javascript
{
product_id: 1, // 产品ID
form_sn: 'life-insurance-wiop3e', // 表单模版标识
form_data: { // 表单数据
coverage: 10000, // 保额(分)← 注意单位
gender: 'male',
age: 30,
birthday: '1994-01-01',
smoker: false,
payment_period: 20,
// ... 其他字段
}
}
```
**响应格式**
```javascript
{
code: 1, // 1 表示成功
data: {
plan_id: 123, // 计划书ID
status: 'processing', // 状态:processing(生成中) | generated(已完成)
download_url: '' // 下载链接(生成完成后才有)
},
msg: '提交成功'
}
```
### 2. 查询计划书状态
**接口路径**`/srv/?a=get_plan_status`
**请求参数**
```javascript
{
plan_id: 123 // 计划书ID
}
```
**响应格式**
```javascript
{
code: 1,
data: {
plan_id: 123,
status: 'generated', // processing | generated
download_url: 'https://...' // PDF 下载链接
},
msg: ''
}
```
---
## 关键注意事项
### ⚠️ 1. 金额字段单位必须是"分"
**错误示例**
```javascript
// ❌ 错误:直接发送"元"
{
coverage: 100.00 // 后端会解析错误或精度丢失
}
```
**正确示例**
```javascript
// ✅ 正确:发送"分"
{
coverage: 10000 // 后端接收后再除以100
}
```
### ⚠️ 2. 检查响应码
**必须检查 `res.code === 1`**
```javascript
const res = await submitPlanAPI(params)
if (res.code === 1) {
// 成功
console.log('提交成功:', res.data)
} else {
// 失败
Taro.showToast({
title: res.msg || '提交失败',
icon: 'none'
})
}
```
### ⚠️ 3. 错误处理
**所有 API 调用必须有 `try-catch`**
```javascript
try {
const res = await submitPlanAPI(params)
if (res.code === 1) {
Taro.showToast({ title: '提交成功', icon: 'success' })
} else {
Taro.showToast({ title: res.msg || '提交失败', icon: 'none' })
}
} catch (err) {
console.error('[SubmitPlan] 提交失败:', err)
Taro.showToast({
title: '网络异常,请重试',
icon: 'none'
})
}
```
### ⚠️ 4. 加载状态
**提交时显示 loading**
```javascript
const loading = ref(false)
const submit = async () => {
loading.value = true
try {
await submitPlanAPI(params)
} finally {
loading.value = false
}
}
```
---
## 调试技巧
### 1. 打印请求数据
**提交前打印完整数据**(已实现):
```javascript
console.log('[PlanFormContainer] 提交计划书:', {
product_id: props.product.id,
product_name: props.product.product_name,
form_sn: props.product.form_sn,
form_data: formattedData // ← 格式化后的数据(元)
})
console.log('[PlanFormContainer] 原始数据(分):', formData.value)
```
**打印效果**
```javascript
// 格式化后(便于查看)
form_data: {
coverage: '100.00',
gender: 'male',
age: 30
}
// 原始数据(实际发送)
form_data: {
coverage: 10000,
gender: 'male',
age: 30
}
```
### 2. 网络请求拦截器
**检查 `src/utils/request.js` 中的拦截器配置**
- ✅ 请求拦截器已自动注入 sessionid
- ✅ 响应拦截器已处理 401 自动刷新
- ✅ 超时配置:5 秒
**查看请求日志**
```javascript
// request.js 中的日志
console.log('[Request] URL:', url)
console.log('[Request] Data:', params)
console.log('[Response] Data:', res)
```
### 3. 开发者工具
**微信开发者工具**
1. 点击"调试器" → "Network"
2. 找到对应的请求(`submit_plan`
3. 查看"Headers"和"Payload"
4. 确认金额字段是整数(分)
---
## 常见问题
### Q1: 后端接收到的金额是 `0.00`?
**原因**:前端发送的值是 `100.00`(浮点数),但后端期望的是整数(分)。
**解决方案**:确保发送的是整数(分):
```javascript
// ❌ 错误
form_data: {
coverage: 100.00 // 浮点数
}
// ✅ 正确
form_data: {
coverage: 10000 // 整数
}
```
### Q2: 后端返回的金额如何显示?
**后端返回的金额单位应该是"分"**
```javascript
// 后端返回
{
code: 1,
data: {
coverage: 10000 // 分
}
}
// 前端显示
const displayYuan = (res.data.coverage / 100).toFixed(2)
// displayYuan = '100.00'
```
### Q3: 如何验证金额是否正确?
**验证方法**
```javascript
// 输入 100.00 元
console.log('输入值:', '100.00')
// 存储的值(分)
console.log('存储值:', 10000)
// 显示的值(元)
console.log('显示值:', (10000 / 100).toFixed(2)) // '100.00'
```
---
## 接口联调清单
### 提交前检查
- [ ] 后端接口地址已配置(`src/utils/config.js`
- [ ] 请求路径正确(`/srv/?a=xxx`
- [ ] 请求参数格式已确认
- [ ] 响应格式已确认(`{ code, data, msg }`
- [ ] 金额字段单位已确认(分)
### 提交时验证
- [ ] 金额字段是整数(分)
- [ ] 检查 `res.code === 1`
- [ ] 错误提示用户友好
- [ ] Loading 状态正确显示
### 提交后处理
- [ ] 成功后跳转到计划书列表页
- [ ] 失败后停留在表单页
- [ ] 网络异常有重试机制
- [ ] 轮询状态(如果是生成中的计划书)
---
## 接口定义示例
### 1. 提交计划书
**定义位置**`src/api/index.js`
```javascript
/**
* 提交计划书表单
*
* @param {Object} params - 请求参数
* @param {number} params.product_id - 产品ID
* @param {string} params.form_sn - 表单模版标识
* @param {Object} params.form_data - 表单数据
* @returns {Promise<{code: number, data: Object, msg: string}>}
*
* @example
* const res = await submitPlanAPI({
* product_id: 1,
* form_sn: 'life-insurance-wiop3e',
* form_data: {
* coverage: 10000, // 保额(分)
* gender: 'male',
* age: 30
* }
* })
*/
export const submitPlanAPI = (params) => {
return buildApiUrl('submit_plan', params)
}
```
### 2. 查询计划书状态
```javascript
/**
* 查询计划书生成状态
*
* @param {Object} params - 请求参数
* @param {number} params.plan_id - 计划书ID
* @returns {Promise<{code: number, data: Object, msg: string}>}
*/
export const getPlanStatusAPI = (params) => {
return buildApiUrl('get_plan_status', params)
}
```
---
## 快速参考
### 金额转换工具函数
**位置**`src/utils/amount.js`(建议创建)
```javascript
/**
* 元转分
* @param {number|string} yuan - 元
* @returns {number} 分
*/
export const yuanToCents = (yuan) => {
return Math.round(Number(yuan) * 100)
}
/**
* 分转元
* @param {number} cents - 分
* @returns {string} 元(带2位小数)
*/
export const centsToYuan = (cents) => {
return (cents / 100).toFixed(2)
}
/**
* 格式化金额显示
* @param {number} cents - 分
* @returns {string} 格式化后的金额(如:"10,000.00")
*/
export const formatAmount = (cents) => {
const yuan = (cents / 100).toFixed(2)
const parts = yuan.split('.')
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return parts.join('.')
}
```
### 使用示例
```javascript
import { yuanToCents, centsToYuan, formatAmount } from '@/utils/amount'
// 用户输入
const input = '100.00'
const cents = yuanToCents(input) // 10000
// 存储
formData.value.coverage = cents
// 显示
const display = formatAmount(cents) // '10,000.00' 或 '100.00'
```
---
## 维护日志
- **2026-02-09**:创建文档,记录金额字段单位规范和联调注意事项
---
## 相关文档
- [项目 CLAUDE.md](../CLAUDE.md) - 项目开发规范
- [API 集成日志](../api-integration-log.md) - API 联调记录
- [变更日志](../CHANGELOG.md) - 版本更新历史
......@@ -57,4 +57,6 @@ export default {
navigationBarTitleText: '西园寺预约',
navigationBarTextStyle: 'black',
},
// 开发环境开启调试模式
...(process.env.NODE_ENV === 'development' ? { debug: true } : {}),
}
......
......@@ -217,8 +217,35 @@ const close = async () => {
}
/**
* 格式化金额数据(分 → 元)
* @description 将金额从"分"转换为"元"(带2位小数),便于调试和查看
* @param {Object} data - 表单数据
* @returns {Object} 格式化后的表单数据
* @example
* formatAmounts({ coverage: 10000 }) // 返回 { coverage: '100.00' }
*/
const formatAmounts = (data) => {
const formatted = { ...data }
// 金额字段列表(单位:分)
const amountFields = ['coverage', 'premium', 'amount', 'total_amount']
for (const field of amountFields) {
if (formatted[field] !== null && formatted[field] !== undefined) {
// 分 → 元,保留2位小数
formatted[field] = (formatted[field] / 100).toFixed(2)
}
}
return formatted
}
/**
* 提交表单
* @description 将表单数据和产品信息一起提交
*
* ⚠️ 注意:接口未完成,当前仅为前端测试
* TODO: 后端接口准备就绪后,需要调用 submitPlanAPI 提交表单数据
*/
const submit = async () => {
if (!props.product) {
......@@ -234,25 +261,27 @@ const submit = async () => {
}
}
// 格式化金额数据(便于调试查看)
const formattedData = formatAmounts(formData.value)
console.log('[PlanFormContainer] 提交计划书:', {
product_id: props.product.id,
product_name: props.product.product_name,
form_sn: props.product.form_sn,
form_data: formData.value
form_data: formattedData // ← 打印格式化后的数据(元)
})
// 发送提交事件
console.log('[PlanFormContainer] 原始数据(分):', formData.value)
// 发送提交事件(携带原始表单数据给父组件,单位:分)
emit('submit', {
product_id: props.product.id,
form_sn: props.product.form_sn,
form_data: formData.value
form_data: formData.value // ← 发送原始数据(分),接口需要
})
// ⚠️ 等待父组件处理提交事件(可能需要关闭弹窗)
await nextTick()
// 提交成功后重置表单,避免下次打开时保留旧数据
resetForm()
// ✅ 不在这里重置表单,让父组件先处理数据
// 重置逻辑交给 close() 函数处理(关闭弹窗时自动清空)
}
/**
......