hookehuyr

feat(plan): 优化提取金额字段并新增每年提取字段

- 统一所有储蓄型产品的提取币种为 USD
- 新增每年提取金额字段 (annual_withdrawal_amount)
- 新增每年递增提取百分比字段 (annual_increase_percentage)
- 添加 inputLabel prop 支持动态提示文字
- 修复百分比输入的类型转换和实时验证
- 修复 AmountKeyboard 组件属性类型不匹配问题
- 修复 ESLint 警告:使用 Number.isNaN() 替代 isNaN()

影响文件:
- src/config/plan-templates.js: 币种统一为 USD
- src/components/plan/PlanFields/AmountKeyboard.vue: 添加 inputLabel prop
- src/components/plan/PlanTemplates/SavingsTemplate.vue: 新增字段和验证逻辑
- src/components/plan/PlanFormContainer.vue: 添加字段映射和数据转换
- src/components/plan/PlanTemplates/*Template.vue: 添加 inputLabel 使用

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
......@@ -85,6 +85,12 @@ paths:
product_id:
type: integer
title: 产品id
annual_withdrawal_amount:
type: integer
title: 每年提取金额
annual_increase_percentage:
type: integer
title: 每年递增提取百分比
required:
- customer_name
- customer_gender
......@@ -100,6 +106,8 @@ paths:
- smoking_status
- total_premium
- product_id
- annual_increase_percentage
- annual_withdrawal_amount
x-apifox-orders:
- customer_name
- customer_gender
......@@ -115,6 +123,8 @@ paths:
- withdrawal_period
- smoking_status
- total_premium
- annual_withdrawal_amount
- annual_increase_percentage
example: "{\r\n customer_name: '张三',\r\n customer_gender: 'male',\r\n customer_age: 30,\r\n annual_premium: 5000,\r\n payment_years: '10年',\r\n currency_type: 'CNY',\r\n product_id: 1\r\n }"
responses:
'200':
......@@ -166,4 +176,4 @@ servers:
description: 正式环境
security: []
```
```\
......
......@@ -40,7 +40,6 @@ const Api = {
file_name: string; // 附件名
file_size: string; // 附件大小
file_size_formatted: string; // 附件大小(转换过显示)
extension?: string; // 文件扩展名(优先使用)
}>;
cover_image: string; // 产品封面图
* };
......
......@@ -3,32 +3,35 @@ import { fn, fetch } from '@/api/fn';
const Api = {
Add: '/srv/?a=proposal&t=add',
Delete: '/srv/?a=proposal&t=delete',
View: '/srv/?a=proposal&t=view',
List: '/srv/?a=proposal&t=list',
};
View: '/srv/?a=proposal&t=view',
}
/**
* @description 新增计划书
* @remark
* @param {Object} params 请求参数
* @param {string} params.customer_name 申请人
* @param {string} params.customer_gender 性别
* @param {number} params.customer_age 年龄
* @param {integer} params.customer_age 年龄
* @param {string} params.customer_birthday 出生年月日
* @param {number} params.annual_premium 保额
* @param {string} params.payment_years 缴费年期
* @param {integer} params.annual_premium 保额
* @param {string} params.payment_years 繳費年期
* @param {string} params.currency_type 币种
* @param {string} params.allow_reduce_amount 是否容许减少名义金额
* @param {string} params.withdrawal_option 提取选项
* @param {number} params.withdrawal_start_age 提取开始年龄
* @param {number} params.withdrawal_period 提取期(年)
* @param {integer} params.withdrawal_start_age 提取开始年龄
* @param {integer} params.withdrawal_period 提取期(年)
* @param {string} params.smoking_status 是否吸烟
* @param {number} params.total_premium 总保费金额
* @param {number} params.product_id 产品id
* @param {integer} params.total_premium 总保费金额
* @param {integer} params.product_id 产品id
* @param {integer} params.annual_withdrawal_amount 每年提取金额
* @param {integer} params.annual_increase_percentage 每年递增提取百分比
* @returns {Promise<{
* code: number; // 状态码
* msg: string; // 消息
* data: {
* order_id: string; // 订单ID
order_id: string; //
* };
* }>}
*/
......@@ -36,55 +39,58 @@ export const addAPI = (params) => fn(fetch.post(Api.Add, params));
/**
* @description 删除计划书
* @remark
* @param {Object} params 请求参数
* @param {string} params.i 计划书id
* @returns {Promise<{
* code: number; // 状态码
* msg: string; // 消息
* data: any;
* }>}
*/
export const deleteAPI = (params) => fn(fetch.post(Api.Delete, params));
/**
* @description 查看计划书
* @description 计划书列表
* @remark
* @param {Object} params 请求参数
* @param {string} params.i 计划书id
* @param {string} params.status (可选) 3:待处理,5:处理中,7:已生成,9:已查看
* @param {string} params.keyword (可选)
* @param {string} params.limit (可选)
* @param {string} params.page (可选)
* @returns {Promise<{
* code: number; // 状态码
* msg: string; // 消息
* data: {
list: Array<{
id: integer; //
customer_name: string; // 申请人
product_name: string; // 产品名
categories: Array<{
id: string; // 分类id
name: string; // 分类名
}>;
created_time: string; // 创建时间
order_status: string; // 状态
proposal_files: Array<{
file_name: string; // 名称
file_url: string; // 地址
id: integer; //
}>;
}>;
total: string; //
* };
* }>}
*/
export const viewAPI = (params) => fn(fetch.post(Api.View, params));
export const listAPI = (params) => fn(fetch.get(Api.List, params));
/**
* @description 计划书列表
* @description 查看计划书
* @remark
* @param {Object} params 请求参数
* @param {string} [params.status] 状态筛选(3:待处理,5:处理中,7:已生成,9:已查看)
* @param {string} [params.keyword] 搜索关键词
* @param {string} [params.limit] 每页数量(默认10)
* @param {string} [params.page] 页码(默认0)
* @returns {Promise<{
* code: number; // 状态码
* msg: string; // 消息
* data: {
* list: Array<{
* id: number; // 计划书id
* customer_name: string; // 申请人
* product_name: string; // 产品名
* categories: Array<{
* id: string; // 分类id
* name: string; // 分类名
* }>; // 分类
* created_time: string; // 创建时间
* order_status: string; // 状态
* proposal_files: Array<{
* file_name: string; // 文件名称
* file_url: string; // 文件地址
* id: number; // 文件id
* }>; // 生成的计划书
* }>; // 计划书列表
* total: string; // 总数
* };
* data: any;
* }>}
*/
export const listAPI = (params) => fn(fetch.get(Api.List, params));
export const viewAPI = (params) => fn(fetch.post(Api.View, params));
......
......@@ -53,7 +53,7 @@
<!-- 内容容器 -->
<view class="relative z-10 w-full px-8 flex flex-col items-center">
<!-- 顶部提示文字 -->
<view class="text-sm text-gray-400 font-normal tracking-wide mb-6">请输入保额金额</view>
<view class="text-sm text-gray-400 font-normal tracking-wide mb-6">{{ inputLabel }}</view>
<!-- 垂直布局核心区域 -->
<view class="flex flex-col items-center justify-center w-full mb-8">
......@@ -155,6 +155,16 @@ const props = defineProps({
},
/**
* 键盘弹窗顶部提示文字
* @type {string}
* @default '请输入保额金额'
*/
inputLabel: {
type: String,
default: '请输入保额金额'
},
/**
* 绑定的值(单位:分)
* @type {number}
* @example 100000 表示 1000.00 元
......
......@@ -267,7 +267,10 @@ const submit = async () => {
withdrawal_mode: 'withdrawal_option', // 提取选项
withdrawal_start_age: 'withdrawal_start_age', // 提取开始年龄
withdrawal_period: 'withdrawal_period', // 提取期
currency_type: 'currency_type' // 币种类型
currency_type: 'currency_type', // 币种类型
// 新增字段映射
annual_withdrawal_amount: 'annual_withdrawal_amount', // 每年提取金额
annual_increase_percentage: 'annual_increase_percentage' // 每年递增提取百分比
}
// 构建请求数据
......@@ -286,7 +289,18 @@ const submit = async () => {
const coverageInYuan = (formData.value[key] / 100).toFixed(2)
console.log(`[PlanFormContainer] coverage 转换: ${key} (${formData.value[key]} ) ${apiField} (${coverageInYuan} )`)
requestData[apiField] = coverageInYuan
} else {
}
// 特殊处理:annual_withdrawal_amount(分)需要转换为元
else if (key === 'annual_withdrawal_amount') {
const amountInYuan = (formData.value[key] / 100).toFixed(2)
console.log(`[PlanFormContainer] annual_withdrawal_amount 转换: ${key} (${formData.value[key]} ) ${apiField} (${amountInYuan} )`)
requestData[apiField] = amountInYuan
}
// 特殊处理:annual_increase_percentage(直接传递,已是字符串)
else if (key === 'annual_increase_percentage') {
requestData[apiField] = formData.value[key]
}
else {
requestData[apiField] = formData.value[key]
}
} else if (key === 'total_amount') {
......
......@@ -41,6 +41,7 @@
v-model="form.coverage"
label="保额"
placeholder="请输入保额"
:input-label="'请输入保额金额'"
:currency="config.currency"
:required="true"
class="mb-5"
......
......@@ -41,6 +41,7 @@
v-model="form.coverage"
label="保额"
placeholder="请输入保额"
:input-label="'请输入保额金额'"
:currency="config.currency"
:required="true"
class="mb-5"
......
......@@ -41,6 +41,7 @@
v-model="form.coverage"
label="年缴保费"
placeholder="请输入年缴保费"
:input-label="'请输入年缴保费金额'"
:currency="config.currency"
:required="true"
class="mb-5"
......@@ -96,16 +97,16 @@
<!-- 按年岁 -->
<template v-if="form.specified_amount_type === '按年岁'">
<!-- 提取金额币种(只读) -->
<div class="mb-5">
<div class="text-sm text-gray-600 mb-2 flex items-center">
<span class="text-red-500 mr-1">*</span>
<span>提取金额币种</span>
</div>
<div class="px-4 py-3 bg-gray-50 rounded-lg border border-gray-200">
<span class="text-gray-900">{{ currencyLabel }}</span>
</div>
</div>
<!-- 每年提取金额 -->
<PlanFieldAmount
v-model="form.annual_withdrawal_amount"
label="每年提取金额"
placeholder="请输入每年提取金额"
:input-label="'请输入每年提取金额'"
:currency="config.withdrawal_plan.default_currency"
:required="true"
class="mb-5"
/>
<!-- 由几岁开始 -->
<PlanFieldAgePicker
......@@ -133,9 +134,10 @@
<span>每年递增提取之百分比(%</span>
</div>
<nut-input
v-model="form.increase_rate"
v-model="form.annual_increase_percentage"
type="digit"
placeholder="请输入递增百分比"
@input="onPercentageInput"
class="w-full"
/>
</div>
......@@ -272,7 +274,9 @@ const initializeForm = (value) => {
withdrawal_enabled: value.withdrawal_enabled || '否',
withdrawal_mode: value.withdrawal_mode || '指定提取金额',
specified_amount_type: value.specified_amount_type || '按年岁',
withdrawal_currency: value.withdrawal_currency || props.config?.withdrawal_plan?.default_currency || 'HKD'
// 新字段默认值(使用 null 以匹配 AmountKeyboard 的 Number 类型)
annual_withdrawal_amount: value.annual_withdrawal_amount ?? null,
annual_increase_percentage: value.annual_increase_percentage ?? null
})
}
......@@ -304,7 +308,9 @@ watch(
withdrawal_enabled: newVal.withdrawal_enabled || '否',
withdrawal_mode: newVal.withdrawal_mode || '指定提取金额',
specified_amount_type: newVal.specified_amount_type || '按年岁',
withdrawal_currency: newVal.withdrawal_currency || props.config?.withdrawal_plan?.default_currency || 'HKD'
// 新字段默认值(使用 null 以匹配 AmountKeyboard 的 Number 类型)
annual_withdrawal_amount: newVal.annual_withdrawal_amount ?? null,
annual_increase_percentage: newVal.annual_increase_percentage ?? null
})
previousModelValue = newVal
}
......@@ -322,22 +328,6 @@ watch(
)
/**
* 提取金额币种标签(用于显示)
* @type {ComputedRef<string>}
* @description 显示币种的完整名称,如"港币 HKD"
*/
const currencyLabel = computed(() => {
const currencyCode = props.config?.withdrawal_plan?.default_currency || 'HKD'
const currencyNames = {
'HKD': '港币',
'USD': '美元',
'CNY': '人民币',
'EUR': '欧元'
}
return `${currencyNames[currencyCode] || currencyCode} ${currencyCode}`
})
/**
* 提取年期选项(从配置读取)
* @type {ComputedRef<Array<string>>}
*/
......@@ -366,9 +356,8 @@ const onWithdrawalModeChange = (mode) => {
if (mode === '最高固定提取金额') {
// 最高固定金额模式不需要指定金额的相关字段
delete form.specified_amount_type
delete form.withdrawal_currency
delete form.annual_amount
delete form.increase_rate
delete form.annual_withdrawal_amount
delete form.annual_increase_percentage
} else if (mode === '指定提取金额') {
// 指定提取金额模式(按年岁),保留现有字段
}
......@@ -385,16 +374,53 @@ watch(
// 清除所有提取计划相关字段
delete form.withdrawal_mode
delete form.specified_amount_type
delete form.withdrawal_currency
delete form.withdrawal_start_age
delete form.withdrawal_period
delete form.annual_amount
delete form.increase_rate
delete form.annual_withdrawal_amount
delete form.annual_increase_percentage
}
}
)
/**
* 百分比输入限制(实时)
* @description 限制百分比输入为有效数值,最多2位小数
* 只允许输入数字和一个小数点
* @param {string} value - 输入值
*/
const onPercentageInput = (value) => {
// 转换为字符串(处理 value 为 null 或其他类型的情况)
let strValue = String(value ?? '')
// 移除所有非数字字符(保留小数点)
let cleaned = strValue.replace(/[^0-9.]/g, '')
// 只允许一个小数点
const parts = cleaned.split('.')
if (parts.length > 2) {
cleaned = parts[0] + '.' + parts.slice(1).join('')
}
// 限制小数位数为2位
if (parts.length === 2 && parts[1].length > 2) {
cleaned = parts[0] + '.' + parts[1].slice(0, 2)
}
// 限制范围:0-100
const numValue = parseFloat(cleaned)
if (!Number.isNaN(numValue)) {
if (numValue > 100) {
cleaned = '100'
} else if (numValue < 0) {
cleaned = '0'
}
}
// 更新表单值
form.annual_increase_percentage = cleaned
}
/**
* 表单校验
* @returns {boolean} 是否通过校验
*/
......@@ -455,14 +481,21 @@ const validate = () => {
}
if (form.specified_amount_type === '按年岁') {
if (!form.withdrawal_currency) {
Taro.showToast({ title: '请选择提取金额币种', icon: 'none' })
if (!form.annual_withdrawal_amount || form.annual_withdrawal_amount === '') {
Taro.showToast({ title: '请输入每年提取金额', icon: 'none' })
return false
}
if (form.increase_rate === undefined || form.increase_rate === '') {
if (form.annual_increase_percentage === undefined || form.annual_increase_percentage === '') {
Taro.showToast({ title: '请输入每年递增提取之百分比', icon: 'none' })
return false
}
// 验证百分比范围
const percentage = parseFloat(form.annual_increase_percentage)
if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) {
Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' })
return false
}
}
} else if (form.withdrawal_mode === '最高固定提取金额') {
if (form.withdrawal_start_age === undefined || form.withdrawal_start_age === '') {
......
......@@ -123,7 +123,7 @@ export const PLAN_TEMPLATES = {
withdrawal_plan: {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'], // 支持的币种
default_currency: 'HKD',
default_currency: 'USD', // 统一为美元
withdrawal_modes: [
'年龄指定金额', // 方式1
'最高固定金额' // 方式2
......@@ -159,7 +159,7 @@ export const PLAN_TEMPLATES = {
withdrawal_plan: {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'],
default_currency: 'HKD',
default_currency: 'USD', // 统一为美元
withdrawal_modes: ['年龄指定金额', '最高固定金额'],
withdrawal_periods: [
'1年',
......@@ -192,7 +192,7 @@ export const PLAN_TEMPLATES = {
withdrawal_plan: {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'],
default_currency: 'HKD',
default_currency: 'USD', // 统一为美元
withdrawal_modes: ['年龄指定金额', '最高固定金额'],
withdrawal_periods: [
'1年',
......@@ -226,7 +226,7 @@ export const PLAN_TEMPLATES = {
withdrawal_plan: {
enabled: true,
currencies: ['HKD', 'USD', 'CNY'],
default_currency: 'HKD',
default_currency: 'USD', // 统一为美元
withdrawal_modes: ['年龄指定金额', '最高固定金额'],
withdrawal_periods: [
'1年',
......