- 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 警告
158 lines
3.1 KiB
Vue
158 lines
3.1 KiB
Vue
<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>
|