Files
xiaocai/client/src/components/Numpad/Numpad.vue
wangxiaogang 21f4d81959 feat: 用户信息完善 + 一起记功能
- users 表添加 nickname、avatar_url 字段
- 用户信息 API(GET/PUT /me)+ 头像上传(multer)
- 头像通过 API 路由获取(公开,不经过 auth)
- 登录接口返回用户昵称和头像
- 用户信息 Store + 个人资料编辑页面
- 我的页面和首页显示真实用户信息
- 群组表(groups、group_members)+ transactions 添加 group_id
- 群组 API(创建/加入/退出/解散)
- 交易和统计 API 支持 group_id 视图切换
- 群组客户端 Store + 身份切换
- 群组管理页面
- App 初始化群组 Store
- UPLOAD_DIR 配置化
- Node.js 备份替代 mysqldump
- 统一前端 BASE_URL(config.ts)
- uploads 加入 .gitignore
- 修复 trust proxy 警告
2026-06-03 17:50:32 +08:00

158 lines
3.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="numpad">
<view class="key" v-for="key in keys" :key="key" @tap="onKeyTap(key)">
<text class="key-text">{{ key }}</text>
</view>
<view class="key fn" @tap="onDelete">
<view class="backspace-icon">
<view class="backspace-arrow"></view>
<view class="backspace-line"></view>
</view>
</view>
<view class="key eq" @tap="onConfirm">
<Icon name="check" :size="36" color="#FFFFFF" />
</view>
</view>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import Icon from '@/components/Icon/Icon.vue'
const props = withDefaults(defineProps<{
/** 金额字符串v-model */
modelValue?: string
/** 最大金额(元) */
max?: number
}>(), {
modelValue: '',
max: 9999999.99
})
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'confirm'): void
}>()
const keys = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '0', '00']
const amountStr = computed(() => props.modelValue)
function vibrate() {
try { uni.vibrateShort({ type: 'light' }) } catch {}
}
function emitValue(val: string) {
emit('update:modelValue', val)
}
function onKeyTap(key: string) {
vibrate()
const current = amountStr.value
if (key === '.') {
if (current.includes('.')) return
if (!current) { emitValue('0.'); return }
}
let next: string
if (!current || current === '0') {
next = (key === '0' || key === '00') ? '0' : key
} else {
next = current + key
}
if (next.includes('.')) {
const decimals = next.split('.')[1]
if (decimals && decimals.length > 2) return
}
if (next.length > 10) return
const num = parseFloat(next)
if (!isNaN(num) && num > props.max) return
emitValue(next)
}
function onDelete() {
vibrate()
const current = amountStr.value
if (current.length > 1) {
emitValue(current.slice(0, -1))
} else {
emitValue('')
}
}
function onConfirm() {
vibrate()
emit('confirm')
}
</script>
<style lang="scss" scoped>
.numpad {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16rpx;
padding: 0 32rpx;
}
.key {
height: 96rpx;
border-radius: 24rpx;
background: #FFF8F0;
display: flex;
align-items: center;
justify-content: center;
&:active { background: #F0E0D6; }
}
.key-text {
font-family: 'Fredoka', sans-serif;
font-size: 44rpx;
font-weight: 500;
color: #2D1B1B;
}
.key.fn {
background: #FFE8E0;
&:active { background: #FFD0C0; }
}
.key.eq {
background: linear-gradient(135deg, #FF8C69, #E67355);
box-shadow: 0 4rpx 16rpx rgba(255, 140, 105, 0.3);
&:active { background: #E67355; }
}
// 自绘 backspace 图标
.backspace-icon {
width: 40rpx;
height: 28rpx;
position: relative;
}
.backspace-arrow {
position: absolute;
left: 0;
top: 50%;
width: 16rpx;
height: 16rpx;
border-left: 4rpx solid #FF8C69;
border-bottom: 4rpx solid #FF8C69;
transform: translateY(-50%) rotate(45deg);
}
.backspace-line {
position: absolute;
left: 12rpx;
top: 50%;
width: 26rpx;
height: 4rpx;
background: #FF8C69;
transform: translateY(-50%);
border-radius: 2rpx;
}
</style>