hookehuyr

feat(plan): 添加客户姓名输入字段到所有保险计划模板

- 新增 PlanFieldName(NameInput)组件用于输入客户姓名
- 在重疾险、寿险、储蓄险三个模板中集成客户姓名字段
- 添加客户姓名必填校验逻辑
- 优化 NameInput 组件样式,使用 Tailwind CSS 和圆角边框
- 重构计划书页面代码,减少代码重复

影响文件:
- src/components/plan/PlanFields/NameInput.vue (新增)
- src/components/plan/PlanTemplates/*.vue (修改)
- src/components/plan/PlanFormContainer.vue (重构)
- src/pages/plan/index.vue (重构)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
......@@ -18,6 +18,7 @@ declare module 'vue' {
ListItemActions: typeof import('./src/components/list/ListItemActions/index.vue')['default']
LoadMoreList: typeof import('./src/components/list/LoadMoreList/index.vue')['default']
MaterialCard: typeof import('./src/components/cards/MaterialCard.vue')['default']
NameInput: typeof import('./src/components/plan/PlanFields/NameInput.vue')['default']
NavHeader: typeof import('./src/components/navigation/NavHeader.vue')['default']
NutAvatar: typeof import('@nutui/nutui-taro')['Avatar']
NutButton: typeof import('@nutui/nutui-taro')['Button']
......
......@@ -5,6 +5,24 @@
---
## [2026-02-10] - 优化 NameInput 组件样式
### 优化
- **样式重构**:将 NameInput 组件从 Less 样式迁移到 Tailwind CSS
- 遵循项目 Tailwind 优先的开发规范
- 移除冗余的 Less 代码
- **UI 改进**
- 添加可见的圆角边框(border-gray-200, rounded-[12rpx]
- 统一输入框的视觉风格
**详细信息**
- **影响文件**: src/components/plan/PlanFields/NameInput.vue
- **技术栈**: Vue 3, Tailwind CSS
- **测试状态**: 待验证
- **备注**: 响应用户需求,增强输入框视觉反馈
---
## [2026-02-10] - 重构 API 接口层代码
### 重构
......
<!--
* @Date: 2026-02-10 14:06:03
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-02-10 14:18:58
* @FilePath: /manulife-weapp/src/components/plan/PlanFields/NameInput.vue
* @Description: 文件描述
-->
<template>
<!-- 姓名输入框 -->
<div class="mb-5">
<label v-if="label" class="block mb-[16rpx] text-sm text-gray-600">
<text v-if="required" class="text-red-500">*</text>
{{ label }}
</label>
<input
:value="modelValue"
:placeholder="placeholder"
type="text"
class="w-full h-[80rpx] px-[24rpx] text-[28rpx] text-[#333] rounded-[12rpx] border border-solid border-gray-200 box-border transition-all duration-300 focus:bg-white focus:border-[#4caf50] focus:outline-none placeholder:text-[#999]"
:class="{ 'text-[#333]': modelValue }"
@input="handleInput"
/>
</div>
</template>
<script setup>
/**
* 姓名输入框组件
*
* @description 计划书表单的姓名输入字段
* - 支持双向绑定
* - 支持必填标识
* - 清晰的视觉反馈
* @author Claude Code
* @example
* <PlanFieldName
* v-model="form.customer_name"
* label="客户姓名"
* placeholder="请输入客户姓名"
* :required="true"
* />
*/
const props = defineProps({
/**
* 字段值
* @type {string}
*/
modelValue: {
type: String,
default: ''
},
/**
* 标签文本
* @type {string}
*/
label: {
type: String,
default: ''
},
/**
* 占位符文本
* @type {string}
*/
placeholder: {
type: String,
default: '请输入姓名'
},
/**
* 是否必填
* @type {boolean}
*/
required: {
type: Boolean,
default: false
}
})
/**
* 组件事件
*/
const emit = defineEmits([
/**
* 更新值事件
* @event update:modelValue
* @param {string} value - 输入的值
*/
'update:modelValue'
])
/**
* 处理输入事件
* @param {Event} e - 输入事件对象
*/
const handleInput = (e) => {
const value = e.detail.value
emit('update:modelValue', value)
}
</script>
......@@ -42,11 +42,13 @@
* />
*/
import { ref, computed, watch, nextTick } from 'vue'
import Taro from '@tarojs/taro'
import PlanPopupNew from './PlanPopupNew.vue'
import LifeInsuranceTemplate from './PlanTemplates/LifeInsuranceTemplate.vue'
import CriticalIllnessTemplate from './PlanTemplates/CriticalIllnessTemplate.vue'
import SavingsTemplate from './PlanTemplates/SavingsTemplate.vue'
import { PLAN_TEMPLATES } from '@/config/plan-templates'
import { addAPI } from '@/api/plan'
/**
* 组件属性
......@@ -251,43 +253,127 @@ const formatAmounts = (data) => {
/**
* 提交表单
* @description 将表单数据和产品信息一起提交
* @description 将表单数据和产品信息提交到后端 API
*
* ⚠️ 注意:接口未完成,当前仅为前端测试
* TODO: 后端接口准备就绪后,需要调用 submitPlanAPI 提交表单数据
* @returns {Promise<boolean>} 是否提交成功
*/
const submit = async () => {
if (!props.product) {
console.error('[PlanFormContainer] 无法提交: 产品数据为空')
return
Taro.showToast({
title: '产品数据为空',
icon: 'none',
duration: 2000
})
return false
}
// 调用模版组件的校验方法
if (templateRef.value && templateRef.value.validate) {
const isValid = templateRef.value.validate()
if (!isValid) {
return
return false
}
}
// 格式化金额数据(便于调试查看)
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: formattedData // ← 打印格式化后的数据(元)
// 显示加载提示
Taro.showLoading({
title: '提交中...',
mask: true
})
console.log('[PlanFormContainer] 原始数据(分):', formData.value)
try {
// 字段名映射:将表单字段名映射为 API 期望的字段名
const fieldMapping = {
customer_name: 'customer_name', // 客户姓名(已直接使用)
gender: 'customer_gender', // 性别 → customer_gender
age: 'customer_age', // 年龄 → customer_age
birthday: 'customer_birthday', // 出生年月日 → customer_birthday
smoker: 'smoker', // 是否吸烟(保持不变)
coverage: 'annual_premium', // 保额/年缴保费 → annual_premium
payment_period: 'payment_years', // 缴费年期 → payment_years
withdrawal_enabled: 'allow_reduce_amount', // 是否容许减少名义金额
withdrawal_mode: 'withdrawal_option', // 提取选项
specified_amount_type: null, // 提取方式(后端可能不需要)
withdrawal_start_age: 'withdrawal_start_age', // 提取开始年龄
withdrawal_period: 'withdrawal_period', // 提取期
currency_type: 'currency_type' // 币种类型
}
// 发送提交事件(携带原始表单数据给父组件,单位:分)
emit('submit', {
product_id: props.product.id,
form_sn: props.product.form_sn,
form_data: formData.value // ← 发送原始数据(分),接口需要
})
// 构建请求数据
const requestData = {
product_id: props.product.id
}
// 映射表单字段到 API 字段
Object.keys(formData.value).forEach(key => {
const apiField = fieldMapping[key]
if (apiField) {
// 有映射:使用映射后的字段名
requestData[apiField] = formData.value[key]
} else if (key === 'total_amount') {
// 特殊处理:总保费(分 → 元)
requestData.total_premium = (formData.value[key] / 100).toFixed(2)
} else {
// 无映射:保持原字段名
requestData[key] = formData.value[key]
}
})
// 添加币种类型(如果有配置)
if (templateConfig.value?.config?.currency) {
requestData.currency_type = templateConfig.value.config.currency
}
console.log('[PlanFormContainer] 提交计划书请求数据:', requestData)
console.log('[PlanFormContainer] 字段映射:', fieldMapping)
// 调用 API
const res = await addAPI(requestData)
if (res.code === 1) {
Taro.hideLoading()
Taro.showToast({
title: '提交成功',
icon: 'success',
duration: 2000
})
// 发送提交成功事件(携带 order_id)
emit('submit', {
success: true,
order_id: res.data?.order_id,
product_id: props.product.id,
form_sn: props.product.form_sn
})
return true
} else {
Taro.hideLoading()
Taro.showToast({
title: res.msg || '提交失败',
icon: 'none',
duration: 2000
})
return false
}
} catch (error) {
Taro.hideLoading()
console.error('[PlanFormContainer] 提交计划书失败:', error)
Taro.showToast({
title: '网络异常,请重试',
icon: 'none',
duration: 2000
})
return false
}
// ✅ 不在这里重置表单,让父组件先处理数据
// 重置逻辑交给 close() 函数处理(关闭弹窗时自动清空)
......
<template>
<div v-if="config">
<!-- 客户姓名 -->
<PlanFieldName
v-model="form.customer_name"
label="客户姓名"
placeholder="请输入客户姓名"
:required="true"
class="mb-5"
/>
<!-- 性别 -->
<PlanFieldRadio
v-model="form.gender"
......@@ -82,6 +91,7 @@
*/
import { reactive, watch } from 'vue'
import Taro from '@tarojs/taro'
import PlanFieldName from '../PlanFields/NameInput.vue'
import PlanFieldAgePicker from '../PlanFields/AgePickerGlobal.vue'
import PlanFieldAmount from '../PlanFields/AmountKeyboard.vue'
import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue'
......@@ -228,6 +238,10 @@ const onBirthdayChange = (birthday) => {
* @returns {boolean} 是否通过校验
*/
const validate = () => {
if (!form.customer_name || !form.customer_name.trim()) {
Taro.showToast({ title: '请输入客户姓名', icon: 'none' })
return false
}
if (!form.gender) {
Taro.showToast({ title: '请选择性别', icon: 'none' })
return false
......
<template>
<div v-if="config">
<!-- 客户姓名 -->
<PlanFieldName
v-model="form.customer_name"
label="客户姓名"
placeholder="请输入客户姓名"
:required="true"
class="mb-5"
/>
<!-- 性别 -->
<PlanFieldRadio
v-model="form.gender"
......@@ -82,6 +91,7 @@
*/
import { reactive, watch, toRefs } from 'vue'
import Taro from '@tarojs/taro'
import PlanFieldName from '../PlanFields/NameInput.vue'
import PlanFieldAgePicker from '../PlanFields/AgePickerGlobal.vue'
import PlanFieldAmount from '../PlanFields/AmountKeyboard.vue'
import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue'
......@@ -231,6 +241,10 @@ const onBirthdayChange = (birthday) => {
* @returns {boolean} 是否通过校验
*/
const validate = () => {
if (!form.customer_name || !form.customer_name.trim()) {
Taro.showToast({ title: '请输入客户姓名', icon: 'none' })
return false
}
if (!form.gender) {
Taro.showToast({ title: '请选择性别', icon: 'none' })
return false
......
<template>
<div v-if="config">
<!-- 客户姓名 -->
<PlanFieldName
v-model="form.customer_name"
label="客户姓名"
placeholder="请输入客户姓名"
:required="true"
class="mb-5"
/>
<!-- 性别 -->
<PlanFieldRadio
v-model="form.gender"
......@@ -207,6 +216,7 @@
*/
import { reactive, watch, computed } from 'vue'
import Taro from '@tarojs/taro'
import PlanFieldName from '../PlanFields/NameInput.vue'
import PlanFieldAgePicker from '../PlanFields/AgePickerGlobal.vue'
import PlanFieldAmount from '../PlanFields/AmountKeyboard.vue'
import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue'
......@@ -457,6 +467,10 @@ watch(
*/
const validate = () => {
// 基础字段校验
if (!form.customer_name || !form.customer_name.trim()) {
Taro.showToast({ title: '请输入客户姓名', icon: 'none' })
return false
}
if (!form.gender) {
Taro.showToast({ title: '请选择性别', icon: 'none' })
return false
......
......@@ -121,7 +121,6 @@ import MaterialCard from '@/components/cards/MaterialCard.vue';
import { listAPI } from '@/api/get_product';
import { weekHotAPI } from '@/api/file';
import { homeIconAPI } from '@/api/home';
import { mockHotProductsListAPI } from '@/api/mock/hotProducts';
// User Store
......@@ -246,41 +245,29 @@ const fetchHomeIcons = async () => {
const hotProducts = ref([]);
/**
* Mock 数据开关
* @description 开发环境默认使用 mock 数据测试计划书功能
* 生产环境必须设置为 false
*/
const USE_MOCK_DATA = process.env.NODE_ENV === 'development'; // 开发环境默认使用 mock 数据
/**
* 获取热卖产品列表
*
* @description 根据 USE_MOCK_DATA 开关决定使用 mock 数据还是调用 API
* - 开发环境使用 mock 数据测试计划书功能
* - 生产环境调用 listAPI 获取真实数据
* @description 调用 listAPI 获取真实的热卖产品数据
*/
const fetchHotProducts = async () => {
try {
// 开发测试环境:使用 mock 数据
if (USE_MOCK_DATA) {
console.log('[Index] 使用 mock 数据获取热卖产品');
const res = await mockHotProductsListAPI({ recommend: 'hot' });
if (res.code === 1 && res.data && res.data.list) {
hotProducts.value = res.data.list;
console.log('[Index] Mock 数据加载成功,产品数量:', res.data.list.length);
}
return;
}
console.log('[Index] 获取热卖产品');
// 生产环境:调用真实 API
// 调用真实 API
const res = await listAPI({
recommend: 'hot'
});
if (res.code === 1 && res.data && res.data.list) {
hotProducts.value = res.data.list;
console.log('[Index] 热卖产品加载成功,数量:', res.data.list.length);
} else {
console.warn('[Index] 热卖产品返回数据格式不正确:', res);
hotProducts.value = [];
}
} catch (err) {
console.error('获取热卖产品失败:', err);
console.error('[Index] 获取热卖产品失败:', err);
hotProducts.value = [];
}
};
......
......@@ -122,9 +122,10 @@
</template>
<script setup>
import { ref, computed, nextTick } from 'vue'
import { ref, nextTick } from 'vue'
import Taro, { useLoad, useReachBottom } from '@tarojs/taro'
import { useFileOperation } from '@/composables/useFileOperation'
import { listAPI } from '@/api/plan'
import NavHeader from '@/components/navigation/NavHeader.vue'
import ListItemActions from '@/components/list/ListItemActions/index.vue'
import SearchBar from '@/components/forms/SearchBar.vue'
......@@ -140,7 +141,7 @@ const loadingMore = ref(false)
const hasMore = ref(true)
const currentList = ref([])
const currentPage = ref(0) // 当前页码(从0开始)
const pageSize = 20 // 每页数量(临时增加到 20 条以便测试滚动)
const pageSize = 20 // 每页数量
/**
* Tab 数据源
......@@ -153,347 +154,36 @@ const tabsData = ref([
])
/**
* Mock 数据:计划书列表(扩展到30条用于测试分页)
*
* @description 使用真实 PDF 文件进行测试
* downloadUrl 使用 Mozilla 的公开 PDF 测试文件
* 从 API 数据转换为组件数据格式
* @description 将 API 返回的数据结构转换为组件使用的格式
* @param {Object} apiItem - API 返回的计划书对象
* @returns {Object} 组件使用的计划书对象
*/
const allList = ref([
// 第1页 (0-9)
{
id: 1,
title: '家庭财富传承保障计划(分红)',
client: '客户:李*生',
date: '2024-03-15 10:12',
tag: '年金保险',
status: 'generated',
fileName: '家庭财富传承保障计划(分红).pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
{
id: 2,
title: '财富传承保障金融理财计划(分红)',
client: '客户:孙*',
date: '2024-03-14 12:01',
tag: '',
status: 'processing',
fileName: '',
downloadUrl: ''
},
{
id: 3,
title: '企业高管年金计划',
client: '客户:王*江',
date: '2024-03-13 09:23',
tag: '年金保险',
status: 'generated',
fileName: '企业高管年金计划.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
{
id: 4,
title: '家庭财富传承保障计划',
client: '客户:李*生',
date: '2024-03-12 15:12',
tag: '年金保险',
status: 'generated',
fileName: '家庭财富传承保障计划.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
{
id: 5,
title: '高净值家庭资产配置计划',
client: '客户:赵*琪',
date: '2024-03-11 18:40',
tag: '资产配置',
status: 'generated',
fileName: '高净值家庭资产配置计划.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
{
id: 6,
title: '企业主税务筹划方案',
client: '客户:陈*明',
date: '2024-03-11 11:05',
tag: '税务筹划',
status: 'processing',
fileName: '',
downloadUrl: ''
},
{
id: 7,
title: '家庭保障升级计划(重疾)',
client: '客户:周*然',
date: '2024-03-10 14:22',
tag: '健康保障',
status: 'generated',
fileName: '家庭保障升级计划(重疾).pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
{
id: 8,
title: '家庭教育金规划方案',
client: '客户:许*雅',
date: '2024-03-10 09:48',
tag: '教育金',
status: 'generated',
fileName: '家庭教育金规划方案.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
{
id: 9,
title: '中长期养老规划',
client: '客户:刘*文',
date: '2024-03-09 16:05',
tag: '养老规划',
status: 'processing',
fileName: '',
downloadUrl: ''
},
{
id: 10,
title: '企业员工福利保障计划',
client: '客户:沈*杰',
date: '2024-03-09 10:33',
tag: '团体保障',
status: 'generated',
fileName: '企业员工福利保障计划.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
// 第2页 (10-19)
{
id: 11,
title: '家族信托规划方案',
client: '客户:唐*豪',
date: '2024-03-08 17:50',
tag: '家族信托',
status: 'generated',
fileName: '家族信托规划方案.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
{
id: 12,
title: '企业接班人保障方案',
client: '客户:何*峰',
date: '2024-03-08 09:12',
tag: '传承保障',
status: 'generated',
fileName: '企业接班人保障方案.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
{
id: 13,
title: '高端医疗健康保障计划',
client: '客户:张*敏',
date: '2024-03-07 14:30',
tag: '健康保障',
status: 'generated',
fileName: '高端医疗健康保障计划.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
{
id: 14,
title: '子女教育金储备计划',
client: '客户:马*丽',
date: '2024-03-07 10:15',
tag: '教育金',
status: 'processing',
fileName: '',
downloadUrl: ''
},
{
id: 15,
title: '家庭财富增值规划',
client: '客户:林*涛',
date: '2024-03-06 16:45',
tag: '资产配置',
status: 'generated',
fileName: '家庭财富增值规划.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
{
id: 16,
title: '企业年金优化方案',
client: '客户:吴*华',
date: '2024-03-06 11:20',
tag: '年金保险',
status: 'generated',
fileName: '企业年金优化方案.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
{
id: 17,
title: '个人税收筹划方案',
client: '客户:郑*强',
date: '2024-03-05 15:10',
tag: '税务筹划',
status: 'processing',
fileName: '',
downloadUrl: ''
},
{
id: 18,
title: '全面家庭保障计划',
client: '客户:黄*明',
date: '2024-03-05 09:50',
tag: '健康保障',
status: 'generated',
fileName: '全面家庭保障计划.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
{
id: 19,
title: '子女教育金累积计划',
client: '客户:谢*芳',
date: '2024-03-04 14:25',
tag: '教育金',
status: 'generated',
fileName: '子女教育金累积计划.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
{
id: 20,
title: '退休养老规划方案',
client: '客户:冯*伟',
date: '2024-03-04 10:40',
tag: '养老规划',
status: 'generated',
fileName: '退休养老规划方案.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
// 第3页 (20-29)
{
id: 21,
title: '企业财产保险规划',
client: '客户:袁*杰',
date: '2024-03-03 16:20',
tag: '团体保障',
status: 'generated',
fileName: '企业财产保险规划.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
{
id: 22,
title: '家庭财产保全计划',
client: '客户:彭*婷',
date: '2024-03-03 11:05',
tag: '传承保障',
status: 'processing',
fileName: '',
downloadUrl: ''
},
{
id: 23,
title: '高端医疗保险方案',
client: '客户:曹*强',
date: '2024-03-02 15:35',
tag: '健康保障',
status: 'generated',
fileName: '高端医疗保险方案.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
{
id: 24,
title: '企业资产传承规划',
client: '客户:许*华',
date: '2024-03-02 10:15',
tag: '家族信托',
status: 'generated',
fileName: '企业资产传承规划.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
{
id: 25,
title: '子女海外教育金计划',
client: '客户:邓*丽',
date: '2024-03-01 14:50',
tag: '教育金',
status: 'generated',
fileName: '子女海外教育金计划.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
{
id: 26,
title: '个人财富保全方案',
client: '客户:萧*敏',
date: '2024-03-01 09:30',
tag: '资产配置',
status: 'processing',
fileName: '',
downloadUrl: ''
},
{
id: 27,
title: '企业员工激励计划',
client: '客户:田*勇',
date: '2024-02-29 16:10',
tag: '团体保障',
status: 'generated',
fileName: '企业员工激励计划.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
{
id: 28,
title: '家庭健康保障计划',
client: '客户:董*华',
date: '2024-02-29 11:45',
tag: '健康保障',
status: 'generated',
fileName: '家庭健康保障计划.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
{
id: 29,
title: '退休收入保障计划',
client: '客户:潘*婷',
date: '2024-02-28 15:20',
tag: '养老规划',
status: 'generated',
fileName: '退休收入保障计划.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
},
{
id: 30,
title: '企业税务优化方案',
client: '客户:袁*明',
date: '2024-02-28 10:00',
tag: '税务筹划',
status: 'generated',
fileName: '企业税务优化方案.pdf',
downloadUrl: 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf'
const transformApiItem = (apiItem) => {
// 获取第一个文件(如果有)
const firstFile = apiItem.proposal_files && apiItem.proposal_files[0]
return {
id: apiItem.id,
title: apiItem.product_name || '未命名计划书',
client: `客户:${apiItem.customer_name || '未知'}`,
date: apiItem.created_time || '',
tag: apiItem.categories && apiItem.categories.length > 0 ? apiItem.categories[0] : '',
status: apiItem.order_status === 'processing' ? 'processing' : 'generated',
fileName: firstFile?.file_name || '',
downloadUrl: firstFile?.file_url || '',
// 保存完整的原始数据
_raw: apiItem
}
])
/**
* 获取当前 tab 的完整列表(过滤后的)
*/
const getFilteredList = () => {
const currentTab = tabsData.value.find(tab => tab.id === activeTabId.value)
if (!currentTab) {
return []
}
let result = currentTab.list
// Filter by Search
if (searchValue.value) {
const key = searchValue.value.toLowerCase()
result = result.filter(item =>
item.title.toLowerCase().includes(key) ||
item.client.toLowerCase().includes(key)
)
}
return result
}
/**
* 加载计划书列表(模拟分页
* @param {number} page - 页码(从0开始
* 加载计划书列表(调用真实 API
* @param {number} page - 页码(从1开始,API 要求
* @param {number} limit - 每页数量
* @param {boolean} isLoadMore - 是否为加载更多
*/
const fetchPlanList = async (page = 0, limit = pageSize, isLoadMore = false) => {
const fetchPlanList = async (page = 1, limit = pageSize, isLoadMore = false) => {
try {
// 如果是加载更多,使用 loadingMore 状态,否则使用 loading 状态
if (isLoadMore) {
......@@ -502,33 +192,52 @@ const fetchPlanList = async (page = 0, limit = pageSize, isLoadMore = false) =>
loading.value = true
}
// 模拟网络延迟(方便测试加载状态)
// await new Promise(resolve => setTimeout(resolve, 500))
// 构建请求参数
const params = {
page: page,
limit: limit
}
// 根据 activeTabId 添加状态过滤
if (activeTabId.value !== '') {
params.order_status = activeTabId.value
}
// 搜索关键字
if (searchValue.value) {
params.keyword = searchValue.value
}
// 获取过滤后的完整列表
const fullList = getFilteredList()
// 调用 API
const res = await listAPI(params)
// 模拟分页:根据 page 和 limit 获取对应的数据
const start = page * limit
const end = start + limit
if (res.code === 1 && res.data) {
const apiList = res.data.list || []
const transformedList = apiList.map(transformApiItem)
const pageData = fullList.slice(start, end)
if (isLoadMore) {
// 加载更多:追加数据
currentList.value = [...currentList.value, ...transformedList]
} else {
// 首次加载或刷新:替换数据
currentList.value = transformedList
}
if (isLoadMore) {
// 加载更多:追加数据
currentList.value = [...currentList.value, ...pageData]
// 判断是否还有更多数据
// 如果返回的数据量少于请求的量,说明没有更多了
hasMore.value = transformedList.length >= limit
} else {
// 首次加载或刷新:替换数据
currentList.value = pageData
Taro.showToast({
title: res.msg || '加载失败',
icon: 'none',
duration: 2000
})
}
// 判断是否还有更多数据
// 如果返回的数据量少于请求的量,说明没有更多了
hasMore.value = pageData.length >= limit
} catch (error) {
console.error('加载计划书列表失败:', error)
Taro.showToast({
title: '加载失败',
icon: 'error',
title: '网络异常,请重试',
icon: 'none',
duration: 2000
})
} finally {
......@@ -541,20 +250,6 @@ const fetchPlanList = async (page = 0, limit = pageSize, isLoadMore = false) =>
}
/**
* 初始化数据分布
* @description 根据分类规则将 allList 中的数据分配到各个 tab 中
*/
const initTabsData = () => {
tabsData.value.forEach((tab) => {
if (tab.id === '') {
tab.list = [...allList.value]
} else {
tab.list = allList.value.filter(item => item.status === tab.id)
}
})
}
/**
* Tab 点击处理
*/
const onTabClick = (id) => {
......@@ -565,8 +260,8 @@ const onTabClick = (id) => {
activeTabId.value = id
listVisible.value = false
// 重置分页状态
currentPage.value = 0
// 重置分页状态(从第 1 页开始)
currentPage.value = 1
hasMore.value = true
nextTick(() => {
......@@ -574,7 +269,7 @@ const onTabClick = (id) => {
listVisible.value = true
// 重新加载数据
fetchPlanList(0, pageSize, false)
fetchPlanList(1, pageSize, false)
})
}
......@@ -582,14 +277,14 @@ const onTabClick = (id) => {
* 搜索处理
*/
const onSearch = () => {
// 重置分页状态
currentPage.value = 0
// 重置分页状态(从第 1 页开始)
currentPage.value = 1
hasMore.value = true
listRenderKey.value += 1
// 重新加载数据
fetchPlanList(0, pageSize, false)
fetchPlanList(1, pageSize, false)
}
/**
......@@ -601,14 +296,14 @@ const onClear = () => {
// 清空搜索值
searchValue.value = ''
// 重置分页状态
currentPage.value = 0
// 重置分页状态(从第 1 页开始)
currentPage.value = 1
hasMore.value = true
listRenderKey.value += 1
// 重新加载数据
fetchPlanList(0, pageSize, false)
fetchPlanList(1, pageSize, false)
console.log('[Plan Page] onClear 完成,列表已刷新')
}
......@@ -617,11 +312,8 @@ const onClear = () => {
* 页面加载时初始化数据
*/
useLoad(() => {
// 初始化数据分布
initTabsData()
// 加载第一页数据
fetchPlanList(0, pageSize, false)
fetchPlanList(1, pageSize, false)
})
/**
......@@ -678,25 +370,28 @@ const onView = async (item) => {
/**
* 删除计划书
* @description 注意:当前后端可能未提供删除接口,暂时只做前端提示
*/
const onDelete = (item) => {
Taro.showModal({
title: '提示',
content: '确定要删除该计划书吗?',
content: '删除功能需要后端支持,是否继续?',
success: (res) => {
if (res.confirm) {
// 从 allList 中删除
const index = allList.value.findIndex(i => i.id === item.id)
if (index !== -1) {
allList.value.splice(index, 1)
// 重新初始化 tabsData
initTabsData()
// 重置分页状态并重新加载
currentPage.value = 0
hasMore.value = true
fetchPlanList(0, pageSize, false)
Taro.showToast({ title: '已删除', icon: 'success' })
}
// TODO: 调用删除 API
// const res = await deletePlanAPI({ id: item.id })
// if (res.code === 1) {
// Taro.showToast({ title: '已删除', icon: 'success' })
// // 重新加载列表
// currentPage.value = 1
// fetchPlanList(1, pageSize, false)
// }
Taro.showToast({
title: '删除功能待实现',
icon: 'none',
duration: 2000
})
}
}
})
......