hookehuyr

feat(plan): 新增 AmountKeyboard 数字键盘输入组件

创建 AmountKeyboard 组件替代 AmountInput 输入框,实现数字键盘交互:

核心功能:
- 点击弹出数字键盘(右列模式,包含小数点)
- 金额显示弹窗(渐变背景 + 装饰圆圈)
- 输入验证(1个小数点,2位小数)
- 千分位分隔符显示
- 集成 GlobalPopupManager 解决嵌套弹窗层级

技术实现:
- nut-number-keyboard @input 事件(单字符传递)
- 字符累加逻辑:String(inputValue) + String(val)
- 输入验证:split('.') 检查小数点和小数位数量
- toFixed(2) 确保保存格式一致性
- watch(showKeyboard) 同步键盘和弹窗状态

问题解决:
1. Vue渲染错误:使用 kebab-case 组件名
2. 键盘被底部按钮遮挡:使用 GlobalPopupManager
3. 单字符输入:实现字符累加
4. 字符串相加变数字相加:显式 String() 转换
5. 输入验证阻塞:初始化为空字符串
6. 保存格式问题:使用 toFixed(2)
7. 自动关闭bug:时间判断过滤
8. 状态同步:watch 同步关闭金额弹窗

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
......@@ -5,6 +5,109 @@
---
## [2026-02-09] - 新增 AmountKeyboard 数字键盘输入组件
### 新增
- 创建 AmountKeyboard.vue 组件,替代 AmountInput 输入框
- 实现数字键盘(nut-number-keyboard)点击触发
- 新增金额显示弹窗(页面中间显示输入内容,渐变背景设计)
- 集成 GlobalPopupManager 解决嵌套弹窗层级问题
- 实现字符累加逻辑,支持连续录入(123)
- 添加输入验证(1个小数点,2位小数)
- 添加千分位分隔符显示
- 实现 toFixed(2) 确保保存格式一致性
- 防止点击遮罩关闭弹窗
- 添加 watch(showKeyboard) 同步键盘和弹窗状态
### 功能特点
- 点击输入框弹出数字键盘(右列模式,包含小数点)
- 金额显示弹窗(渐变背景 + 装饰圆圈 + 大号字体)
- 输入验证(限制1个小数点,2位小数)
- 连续输入支持(字符累加,字符串类型安全)
- 状态同步(键盘关闭时自动关闭金额弹窗)
- 多币种支持(CNY、USD、HKD、EUR)
### 技术实现
- 使用 nut-number-keyboard 的 @input 事件(单字符传递)
- 字符累加:`String(inputValue.value) + String(val)`
- 输入验证:split('.') 检查小数点和小数位数量
- 初始化:空字符串(非"0.00")避免验证冲突
- 保存:parseFloat(yuan.toFixed(2)) 确保格式
- 单位转换:内部存储为分(整数),显示为元(带2位小数)
### 问题解决
1. Vue渲染错误:使用正确的 kebab-case 组件名 `nut-number-keyboard`
2. 键盘被底部按钮遮挡:使用 GlobalPopupManager 解决嵌套弹窗层级
3. 单字符输入:实现字符累加逻辑
4. 字符串相加变成数字相加(1+2=3):显式使用 String() 转换
5. 输入验证阻塞:初始化为空字符串而非 "0.00"
6. 保存格式问题:使用 toFixed(2) 确保 "12.00" 格式
7. 自动关闭bug:添加时间判断过滤(500ms内)
8. 状态同步:watch(showKeyboard) 同步关闭金额弹窗
### 影响文件
- src/components/PlanFields/AmountKeyboard.vue(新增)
- src/components/PlanTemplates/LifeInsuranceTemplate.vue(使用新组件)
- src/components/PlanTemplates/CriticalIllnessTemplate.vue(使用新组件)
- src/components/PlanTemplates/SavingsTemplate.vue(使用新组件)
- docs/lessons-learned.md(添加经验教训记录)
**详细信息**
- **影响文件**: 见上文
- **技术栈**: Vue 3, Taro 4, NutUI, GlobalPopupManager
- **测试状态**: ✅ 已通过
- **备注**: 替代原有的 AmountInput 组件,老组件保留用于测试
---
## [2026-02-08] - 实现全局弹窗管理器解决嵌套弹窗遮挡问题
### 新增
- 创建 GlobalPopupManager 全局弹窗管理器
- 实现 useParentPopup 和 useGlobalPopup 接口
- 新增 PlanPopupNew 父弹窗组件(支持全局管理)
- 新增 DatePickerGlobal 日期选择器(支持全局管理)
- 新增 SelectPickerGlobal 下拉选择器(支持全局管理)
- 新增 AgePickerGlobal 年龄选择器(支持全局管理)
### 优化
- 子弹窗打开时自动隐藏父弹窗底部按钮
- 所有子弹窗关闭时自动恢复底部按钮
- 支持多个子弹窗同时打开
- 支持多层弹窗嵌套(弹窗套弹窗)
- 使用 watch 监听全局状态,解决组件挂载时序问题
### 问题原因
**NutUI 嵌套弹窗的 z-index 层级问题**
- 父弹窗和子弹窗都是 `position: fixed` 定位
- 即使子弹窗 `z-index` 更高,也无法遮挡父弹窗的非子元素(如底部按钮)
- 底部按钮在 DOM 结构上与子弹窗同级,会覆盖子弹窗内容
### 解决方案
**核心策略**:全局弹窗管理器协调父弹窗和子弹窗状态
1. **子弹窗注册**:每个子弹窗组件挂载时注册,获得唯一 ID
2. **激活/停用**:子弹窗打开时激活,关闭时停用
3. **状态同步**:管理器通知父弹窗隐藏/显示底部按钮
4. **响应式更新**:父弹窗通过 watch 监听全局状态变化
### 迁移工作
- 更新 PlanFormContainer 使用 PlanPopupNew
- 更新所有计划模板(LifeInsuranceTemplate、CriticalIllnessTemplate、SavingsTemplate)使用 Global 版本字段组件
### 影响文件
- src/components/PlanFormContainer.vue
- src/components/PlanPopupNew.vue
- src/components/PlanFields/GlobalPopupManager.js
- src/components/PlanFields/DatePickerGlobal.vue
- src/components/PlanFields/SelectPickerGlobal.vue
- src/components/PlanFields/AgePickerGlobal.vue
- src/components/PlanTemplates/LifeInsuranceTemplate.vue
- src/components/PlanTemplates/CriticalIllnessTemplate.vue
- src/components/PlanTemplates/SavingsTemplate.vue
---
## [2026-02-08] - 修复计划书表单重置和数据同步问题
### 修复
......
......@@ -1945,21 +1945,220 @@ const USE_MOCK_DATA = true // 容易导致生产环境误用
---
## 组件开发案例:AmountKeyboard 数字键盘组件 ⭐ 新增
### 问题描述
需要创建一个保额输入组件,点击后弹出数字键盘进行输入,而不是直接使用输入框。
### 遇到的坑和解决方案
#### 坑 1: Vue 渲染错误 - Invalid vnode type
**错误信息**:
```
[Vue warn]: Invalid vnode type when creating vnode: undefined
```
**原因**: 使用了错误的组件名 `nut-numberkeyboard` (驼峰式)
**解决方案**: 使用正确的 kebab-case 命名 `<nut-number-keyboard>`
**官方示例**:
```vue
<nut-number-keyboard v-model:visible="show" type="rightColumn" />
```
---
#### 坑 2: 键盘被底部按钮遮挡
**现象**: 数字键盘的底部按钮被页面底部按钮遮挡
**错误尝试**: 使用 `z-index` CSS 调整(无效)
**正确方案**: 使用 GlobalPopupManager 系统
```javascript
import { useGlobalPopup } from './GlobalPopupManager.js'
const { registerPopup, activatePopup, deactivatePopup } = useGlobalPopup()
// 注册弹窗
const keyboardPopupId = ref(null)
onMounted(() => {
keyboardPopupId.value = registerPopup()
})
// 监听键盘状态
watch(showKeyboard, (newValue) => {
if (newValue) {
activatePopup(keyboardPopupId.value)
} else {
deactivatePopup(keyboardPopupId.value)
}
})
```
---
#### 坑 3: 数字输入只能录入单个数字
**现象**: 输入 "123" 时,只显示最后一个数字
**原因**: `nut-number-keyboard` 的 `@input` 事件传入的是单个字符,需要累加
**错误代码**:
```javascript
const onInput = (val) => {
inputValue.value = val // ❌ 直接替换
}
```
**正确代码**:
```javascript
const onInput = (val) => {
if (!inputValue.value) {
inputValue.value = String(val)
} else {
inputValue.value = String(inputValue.value) + String(val)
}
}
```
---
#### 坑 4: 输入验证导致无法输入
**现象**: 初始化为 "0.00" 后,所有输入都被忽略
**原因**: 验证逻辑判断 `parts[1].length >= 2`,认为已有2位小数
**解决方案**: 初始化时使用空字符串,而不是 "0.00"
```javascript
const openKeyboard = () => {
if (props.modelValue !== null && props.modelValue !== undefined) {
inputValue.value = (props.modelValue / 100).toFixed(2)
} else {
inputValue.value = '' // ✅ 空字符串,不是 '0.00'
}
}
```
**验证逻辑**:
```javascript
const onInput = (val) => {
// 检查小数点
if (val === '.' && inputValue.value.includes('.')) {
return // 已有小数点,忽略
}
// 检查小数位
if (val >= '0' && val <= '9') {
const parts = inputValue.value.split('.')
if (parts.length === 2 && parts[1]?.length >= 2) {
return // 已有2位小数,忽略
}
}
// 累加输入
if (!inputValue.value) {
inputValue.value = String(val)
} else {
inputValue.value = String(inputValue.value) + String(val)
}
}
```
---
#### 坑 5: 键盘自动关闭问题
**现象**: 键盘刚打开就立即关闭
**原因**: `@close` 事件在打开时被误触发
**解决方案**: 添加时间判断,过滤误触发
```javascript
const keyboardOpenTime = ref(0)
watch(showKeyboard, (newValue) => {
if (newValue) {
keyboardOpenTime.value = Date.now() // 记录打开时间
}
})
const onClose = () => {
const timeSinceOpen = Date.now() - keyboardOpenTime.value
if (timeSinceOpen < 500) {
return // 忽略误触发
}
showKeyboard.value = false
}
```
**最终方案**: 移除 `@close` 监听,在 `watch(showKeyboard)` 中同步关闭金额弹窗
---
#### 坑 6: 字符串累加被当作数字相加
**现象**: 输入 1、2、3 后显示 6(1+2+3)
**原因**: JavaScript `+=` 运算符将字符串当作数字相加
**解决方案**: 确保两边都是字符串类型
```javascript
inputValue.value = String(inputValue.value) + String(val)
```
---
#### 坑 7: 保存后再次编辑显示问题
**问题**: 保存 "12.23",删除后保存变成 "12" 而不是 "12.00"
**原因**: `parseFloat("12")` 丢失小数点后的零
**解决方案**: 使用 `toFixed(2)` 保留两位小数
```javascript
const onConfirm = () => {
let yuan = parseFloat(inputValue.value || '0')
if (!Number.isNaN(yuan)) {
const formattedYuan = parseFloat(yuan.toFixed(2))
const cents = Math.round(formattedYuan * 100)
emit('update:modelValue', cents)
}
}
```
---
### ✅ 最终实现要点
1. **组件结构**: 点击区域 + 金额弹窗 + 数字键盘三层结构
2. **事件处理**: `@input` 累加,`@delete` 删除,`@confirm` 保存
3. **输入验证**: 限制小数点(1个)和小数位(2位)
4. **状态管理**: `watch(showKeyboard)` 同步关闭金额弹窗
5. **美观设计**: 渐变背景 + 圆形装饰 + 大号数字显示
---
## 总结
### 🎯 核心经验
1. **"第 3 次出现原则"**: 代码重复 3 次时必须抽取
2. **NutUI 陷阱**: textarea、IconFont 等组件有坑,优先使用原生组件
3. **静态资源**: SVG 图标必须使用 `import` 导入
4. **样式策略**: TailwindCSS(80%) + Less(20%) 混合使用
5. **性能优化**: `shallowRef` + `markRaw` 处理组件对象响应式
6. **代码质量**: 强制 JSDoc 注释,统一命名规范
7. **API 调用规范**: ⚠️ **不要使用 `fn()` 包装 API,直接调用并自己处理错误**
8. **架构设计**: 分层清晰,职责单一
9. **跨页面通信**: ⭐ **使用事件总线模式,LoadMoreList 页面避免使用 `useDidShow`**(防止意外刷新)
10. **Mock 数据切换**: ⭐ **使用环境变量 `process.env.NODE_ENV` 自动判断,禁止硬编码**
11. **⚠️ 写代码前必查**: 先搜索项目中是否有类似实现,保持写法一致
3. **⭐ 数字键盘组件**: `@input` 传入单个字符需要累加,注意类型转换
4. **静态资源**: SVG 图标必须使用 `import` 导入
5. **样式策略**: TailwindCSS(80%) + Less(20%) 混合使用
6. **性能优化**: `shallowRef` + `markRaw` 处理组件对象响应式
7. **代码质量**: 强制 JSDoc 注释,统一命名规范
8. **API 调用规范**: ⚠️ **不要使用 `fn()` 包装 API,直接调用并自己处理错误**
9. **架构设计**: 分层清晰,职责单一
10. **跨页面通信**: ⭐ **使用事件总线模式,LoadMoreList 页面避免使用 `useDidShow`**(防止意外刷新)
11. **Mock 数据切换**: ⭐ **使用环境变量 `process.env.NODE_ENV` 自动判断,禁止硬编码**
12. **⚠️ 写代码前必查**: 先搜索项目中是否有类似实现,保持写法一致
### 📚 推荐阅读
......
<template>
<div>
<!-- 标签 -->
<div v-if="label" class="text-sm text-gray-600 mb-2 flex items-center">
<span v-if="required" class="text-red-500 mr-1">*</span>
<span>{{ label }}</span>
<span v-if="currencyText" class="text-gray-500">{{ currencyText }}</span>
</div>
<!-- 多币种模式(方案 2 - 未来扩展) -->
<div v-if="multiCurrencyEnabled" class="mb-2">
<div class="text-sm text-gray-600 mb-2">币种</div>
<div class="flex gap-2">
<button
v-for="curr in supportedCurrencies"
:key="curr.value"
:class="[
'px-4 py-2 rounded-lg text-sm border transition-colors',
selectedCurrency === curr.value
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-gray-600 border-gray-200'
]"
@tap="selectCurrency(curr.value)"
>
{{ curr.label }}
</button>
</div>
</div>
<!-- 保额显示区域(可点击) -->
<div
class="border border-gray-200 rounded-lg flex items-center overflow-hidden cursor-pointer"
@tap="openKeyboard"
>
<div class="flex-1 p-3 text-sm">
<span v-if="displayValue" class="text-gray-900">{{ displayValue }}</span>
<span v-else class="text-gray-400">{{ placeholder }}</span>
</div>
<span class="text-sm text-gray-500 shrink-0 ml-2 mr-5">{{ currencySymbol }}</span>
</div>
<!-- 金额显示弹窗 -->
<nut-popup
v-model:visible="showAmountModal"
position="center"
:style="{ padding: '0', borderRadius: '24rpx', overflow: 'hidden' }"
:overlay-style="{ backgroundColor: 'rgba(0, 0, 0, 0.5)' }"
:close-on-click-overlay="false"
>
<view class="amount-modal">
<!-- 背景装饰 -->
<view class="amount-modal-bg">
<view class="amount-modal-circle amount-modal-circle-1"></view>
<view class="amount-modal-circle amount-modal-circle-2"></view>
<view class="amount-modal-circle amount-modal-circle-3"></view>
</view>
<!-- 内容区域 -->
<view class="amount-modal-content">
<!-- 币种符号 -->
<view class="amount-modal-currency">{{ currencySymbol }}</view>
<!-- 金额数值 -->
<view class="amount-modal-value">
{{ formattedInputValue || '0.00' }}
</view>
<!-- 提示文字 -->
<view class="amount-modal-hint">请输入金额</view>
</view>
</view>
</nut-popup>
<!-- 数字键盘 -->
<nut-number-keyboard
v-model:visible="showKeyboard"
type="rightColumn"
overlay
:custom-key="customKey"
confirm-text="完成"
@input="onInput"
@delete="onDelete"
@confirm="onConfirm"
@close="onClose"
/>
</div>
</template>
<script setup>
/**
* 保额键盘输入组件
*
* @description 点击后弹出数字键盘进行输入的保额组件
* - 单位转换:内部存储为分(整数),显示为元(带2位小数)
* - 币种支持:CNY、USD、HKD、EUR
* - 多币种模式:通过 FEATURE_FLAGS.MULTI_CURRENCY_ENABLED 控制
* - 支持弹窗嵌套:通过 GlobalPopupManager 注册为全局弹窗
* @author Claude Code
* @example
* <!-- 固定币种模式 -->
* <AmountKeyboard
* v-model="coverage"
* label="保额"
* currency="USD"
* placeholder="请输入保额"
* />
*
* @example
* <!-- 多币种模式 -->
* <AmountKeyboard
* v-model="coverage"
* label="保额"
* :config="{ supported_currencies: ['CNY', 'USD'], default_currency: 'CNY' }"
* placeholder="请输入保额"
* />
*/
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import Taro from '@tarojs/taro'
import { FEATURE_FLAGS, CURRENCY_SYMBOLS, CURRENCY_MAP } from '@/config/plan-templates'
import { useGlobalPopup } from './GlobalPopupManager.js'
/**
* 组件属性
*/
const props = defineProps({
/**
* 标签文本
* @type {string}
*/
label: {
type: String,
default: ''
},
/**
* 是否必填
* @type {boolean}
*/
required: {
type: Boolean,
default: false
},
/**
* 占位符文本
* @type {string}
*/
placeholder: {
type: String,
default: '请输入保额'
},
/**
* 绑定的值(单位:分)
* @type {number}
* @example 100000 表示 1000.00 元
*/
modelValue: {
type: Number,
default: null
},
/**
* 币种代码(固定币种模式)
* @type {string}
* @default 'CNY'
*/
currency: {
type: String,
default: 'CNY'
},
/**
* 模版配置(多币种模式)
* @type {Object}
* @property {Array<string>} supported_currencies - 支持的币种代码数组
* @property {string} default_currency - 默认币种代码
* @example { supported_currencies: ['CNY', 'USD'], default_currency: 'CNY' }
*/
config: {
type: Object,
default: () => ({})
}
})
/**
* 组件事件
*/
const emit = defineEmits([
/**
* 更新值事件
* @event update:modelValue
* @param {number} value - 保额值(单位:分)
*/
'update:modelValue'
])
/**
* 判断是否启用多币种
* @type {ComputedRef<boolean>}
*/
const multiCurrencyEnabled = computed(() => FEATURE_FLAGS.MULTI_CURRENCY_ENABLED)
/**
* 当前选中的币种
* @type {Ref<string>}
*/
const selectedCurrency = ref(props.config.default_currency || props.currency || 'CNY')
/**
* 支持的币种列表(多币种模式)
* @type {ComputedRef<Array<{label: string, symbol: string, value: string}>>}
*/
const supportedCurrencies = computed(() => {
if (!multiCurrencyEnabled.value) return []
return (props.config.supported_currencies || ['CNY'])
.map(code => CURRENCY_MAP[code])
.filter(Boolean)
})
/**
* 当前币种符号
* @type {ComputedRef<string>}
* @example
* // CNY -> '¥'
* // USD -> '$'
*/
const currencySymbol = computed(() => {
if (multiCurrencyEnabled.value) {
// 多币种模式:使用用户选择的币种
const curr = supportedCurrencies.value.find(c => c.value === selectedCurrency.value)
return curr?.symbol || '¥'
}
// 固定币种模式:使用 props.currency
return CURRENCY_SYMBOLS[props.currency] || '¥'
})
/**
* 币种文本(用于标签显示)
* @type {ComputedRef<string>}
*/
const currencyText = computed(() => {
if (multiCurrencyEnabled.value) {
const curr = supportedCurrencies.value.find(c => c.value === selectedCurrency.value)
return curr?.label || ''
}
const CURRENCY_NAMES = {
CNY: '人民币',
USD: '美元',
HKD: '港币',
EUR: '欧元'
}
return CURRENCY_NAMES[props.currency] || ''
})
/**
* 键盘显示状态
* @type {Ref<boolean>}
*/
const showKeyboard = ref(false)
/**
* 键盘打开时间戳(用于判断误触发关闭)
* @type {Ref<number>}
*/
const keyboardOpenTime = ref(0)
/**
* 键盘输入值(临时存储,字符串格式)
* @type {Ref<string>}
*/
const inputValue = ref('')
/**
* 金额弹窗显示状态
* @type {Ref<boolean>}
*/
const showAmountModal = ref(false)
/**
* 显示值(元,带2位小数)
* @type {ComputedRef<string>}
*/
const displayValue = computed(() => {
// 优先显示输入过程中的值
if (inputValue.value) {
return inputValue.value
}
// 如果没有输入值,显示表单的原始值
if (props.modelValue !== null && props.modelValue !== undefined) {
return (props.modelValue / 100).toFixed(2)
}
return ''
})
/**
* 格式化输入值(带千分位分隔符)
* @type {ComputedRef<string>}
*/
const formattedInputValue = computed(() => {
if (!inputValue.value) return '0.00'
// 解析数值
const num = parseFloat(inputValue.value)
if (Number.isNaN(num)) return '0.00'
// 格式化为千分位
const parts = num.toFixed(2).split('.')
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return parts.join('.')
})
/**
* 自定义键盘按键
* @type {Ref<Array<string>>}
*/
const customKey = ref(['.'])
/**
* 使用全局弹窗管理器
*/
const { registerPopup, activatePopup, deactivatePopup } = useGlobalPopup()
/**
* 数字键盘弹窗 ID
* @type {Ref<string|null>}
*/
const keyboardPopupId = ref(null)
/**
* 组件挂载时注册弹窗
*/
onMounted(() => {
keyboardPopupId.value = registerPopup()
})
/**
* 组件卸载时取消注册
*/
onUnmounted(() => {
if (keyboardPopupId.value) {
deactivatePopup(keyboardPopupId.value)
}
})
/**
* 监听键盘显示状态
*/
watch(showKeyboard, (newValue) => {
if (!keyboardPopupId.value) {
return
}
if (newValue) {
// 键盘打开:激活全局弹窗,通知父弹窗隐藏 footer
activatePopup(keyboardPopupId.value)
// 记录打开时间
keyboardOpenTime.value = Date.now()
} else {
// 键盘关闭:停用全局弹窗,通知父弹窗恢复 footer
deactivatePopup(keyboardPopupId.value)
// 同步关闭金额弹窗
showAmountModal.value = false
}
})
/**
* 打开键盘
*/
const openKeyboard = () => {
// 初始化键盘值为当前值(元),如果没有值则为空
if (props.modelValue !== null && props.modelValue !== undefined) {
inputValue.value = (props.modelValue / 100).toFixed(2)
} else {
inputValue.value = ''
}
showKeyboard.value = true
showAmountModal.value = true
}
/**
* 键盘输入处理
* @param {string} val - 输入值(单个字符)
*/
const onInput = (val) => {
// 如果输入的是小数点,检查是否已经有小数点
if (val === '.' && inputValue.value.includes('.')) {
return
}
// 如果输入的是数字,检查小数点后的位数
if (val >= '0' && val <= '9') {
const parts = inputValue.value.split('.')
// 如果已经有小数点,并且小数点后有2位,则忽略输入
if (parts.length === 2 && parts[1]?.length >= 2) {
return
}
}
// nut-number-keyboard 的 @input 事件传入的是单个字符,需要累加
if (!inputValue.value) {
// 初始状态,确保是字符串
inputValue.value = String(val)
} else {
// 累加输入,确保两边都是字符串进行连接
inputValue.value = String(inputValue.value) + String(val)
}
}
/**
* 删除键处理
*/
const onDelete = () => {
if (!inputValue.value) return
// 删除最后一个字符
inputValue.value = inputValue.value.slice(0, -1)
}
/**
* 完成按钮点击处理
*/
const onConfirm = () => {
// 将元转换为分,保留两位小数
let yuan = parseFloat(inputValue.value || '0')
if (!Number.isNaN(yuan)) {
// 先保留两位小数,再转换为分
const formattedYuan = parseFloat(yuan.toFixed(2))
const cents = Math.round(formattedYuan * 100)
emit('update:modelValue', cents)
}
// 关闭键盘和金额弹窗
showKeyboard.value = false
showAmountModal.value = false
}
/**
* 关闭键盘认处
*/
const onClose = () => {
// 只关闭金额弹窗,让 showKeyboard 自然变化触发 watch 处理 GlobalPopupManager
showAmountModal.value = false
}
/**
* 选择币种(多币种模式)
* @param {string} value - 币种代码
*/
const selectCurrency = (value) => {
selectedCurrency.value = value
}
</script>
<style lang="less">
/**
* 组件样式
*/
.cursor-pointer {
cursor: pointer;
}
.amount-modal {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 500rpx;
min-height: 360rpx;
padding: 60rpx 40rpx;
overflow: hidden;
&-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
opacity: 0.1;
}
&-circle {
position: absolute;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
opacity: 0.15;
}
&-circle-1 {
width: 400rpx;
height: 400rpx;
top: -200rpx;
right: -100rpx;
}
&-circle-2 {
width: 300rpx;
height: 300rpx;
bottom: -100rpx;
left: -80rpx;
}
&-circle-3 {
width: 200rpx;
height: 200rpx;
top: 50%;
right: 20%;
}
&-content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
}
&-currency {
font-size: 48rpx;
font-weight: bold;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--webkit-background-clip: text;
--webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 20rpx;
}
&-value {
font-size: 88rpx;
font-weight: bold;
color: #333;
line-height: 1.2;
margin-bottom: 16rpx;
}
&-hint {
font-size: 28rpx;
color: #999;
}
}
</style>
......@@ -83,7 +83,7 @@
import { reactive, watch } from 'vue'
import Taro from '@tarojs/taro'
import PlanFieldAgePicker from '../PlanFields/AgePickerGlobal.vue'
import PlanFieldAmount from '../PlanFields/AmountInput.vue'
import PlanFieldAmount from '../PlanFields/AmountKeyboard.vue'
import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue'
import PlanFieldRadio from '../PlanFields/RadioGroup.vue'
import PlanFieldSelect from '../PlanFields/SelectPickerGlobal.vue'
......
......@@ -83,7 +83,7 @@
import { reactive, watch, toRefs } from 'vue'
import Taro from '@tarojs/taro'
import PlanFieldAgePicker from '../PlanFields/AgePickerGlobal.vue'
import PlanFieldAmount from '../PlanFields/AmountInput.vue'
import PlanFieldAmount from '../PlanFields/AmountKeyboard.vue'
import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue'
import PlanFieldRadio from '../PlanFields/RadioGroup.vue'
import PlanFieldSelect from '../PlanFields/SelectPickerGlobal.vue'
......
......@@ -208,7 +208,7 @@
import { reactive, watch, computed } from 'vue'
import Taro from '@tarojs/taro'
import PlanFieldAgePicker from '../PlanFields/AgePickerGlobal.vue'
import PlanFieldAmount from '../PlanFields/AmountInput.vue'
import PlanFieldAmount from '../PlanFields/AmountKeyboard.vue'
import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue'
import PlanFieldRadio from '../PlanFields/RadioGroup.vue'
import PlanFieldSelect from '../PlanFields/SelectPickerGlobal.vue'
......