343 lines
6.8 KiB
Vue
343 lines
6.8 KiB
Vue
<template>
|
||
<view v-if="visible" class="popup-shell" @touchmove.stop.prevent="">
|
||
<view class="popup-mask" @click="emit('close')"></view>
|
||
<view class="surface-card popup-panel">
|
||
<view class="popup-head">
|
||
<view>
|
||
<text class="section-title">{{ form.id ? '编辑账单' : '新增账单' }}</text>
|
||
<text class="section-subtitle">3 步完成记录,所有数据仅保存在本机</text>
|
||
</view>
|
||
<text class="close-text" @click="emit('close')">关闭</text>
|
||
</view>
|
||
<scroll-view scroll-y class="popup-body">
|
||
<view class="field-block">
|
||
<text class="field-label">收支类型</text>
|
||
<view class="pill-row">
|
||
<view
|
||
v-for="item in typeOptions"
|
||
:key="item.value"
|
||
class="pill-button"
|
||
:class="{ active: form.type === item.value }"
|
||
@click="form.type = item.value"
|
||
>
|
||
{{ item.label }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="field-block">
|
||
<text class="field-label">金额</text>
|
||
<view class="input-shell amount-shell">
|
||
<text class="prefix-text">¥</text>
|
||
<input v-model="form.amount" type="digit" placeholder="输入金额" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="field-block">
|
||
<text class="field-label">分类</text>
|
||
<view class="chip-grid">
|
||
<view
|
||
v-for="item in currentCategories"
|
||
:key="item.id"
|
||
class="chip-item"
|
||
:class="{ active: form.categoryId === item.id }"
|
||
@click="form.categoryId = item.id"
|
||
>
|
||
<view class="chip-dot" :style="{ background: item.color }"></view>
|
||
<text>{{ item.name }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="field-block">
|
||
<text class="field-label">账户</text>
|
||
<view class="chip-grid">
|
||
<view
|
||
v-for="item in accounts"
|
||
:key="item.id"
|
||
class="chip-item"
|
||
:class="{ active: form.accountId === item.id }"
|
||
@click="form.accountId = item.id"
|
||
>
|
||
<view class="chip-dot" :style="{ background: item.color }"></view>
|
||
<text>{{ item.name }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="field-block">
|
||
<text class="field-label">日期</text>
|
||
<picker mode="date" :value="form.date" @change="onDateChange">
|
||
<view class="input-shell picker-shell">
|
||
<text>{{ form.date }}</text>
|
||
<text class="tiny-text">选择</text>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
|
||
<view class="field-block">
|
||
<text class="field-label">备注</text>
|
||
<view class="input-shell textarea-shell">
|
||
<textarea
|
||
v-model="form.note"
|
||
maxlength="40"
|
||
placeholder="补充说明,便于后续搜索"
|
||
></textarea>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
<view class="popup-foot">
|
||
<view class="ghost-button" @click="emit('close')">取消</view>
|
||
<view class="primary-button save-button" @click="handleSave">保存账单</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, reactive, watch } from 'vue'
|
||
import { toDateKey } from '../utils/date'
|
||
|
||
const props = defineProps({
|
||
visible: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
entry: {
|
||
type: Object,
|
||
default: null
|
||
},
|
||
categories: {
|
||
type: Object,
|
||
required: true
|
||
},
|
||
accounts: {
|
||
type: Array,
|
||
required: true
|
||
},
|
||
defaultType: {
|
||
type: String,
|
||
default: 'expense'
|
||
},
|
||
initialCategoryId: {
|
||
type: String,
|
||
default: ''
|
||
}
|
||
})
|
||
|
||
const emit = defineEmits(['close', 'save'])
|
||
|
||
const typeOptions = [
|
||
{ label: '支出', value: 'expense' },
|
||
{ label: '收入', value: 'income' }
|
||
]
|
||
|
||
const form = reactive({
|
||
id: '',
|
||
type: 'expense',
|
||
amount: '',
|
||
categoryId: '',
|
||
accountId: '',
|
||
date: toDateKey(),
|
||
note: '',
|
||
createdAt: 0
|
||
})
|
||
|
||
const currentCategories = computed(() => props.categories[form.type] || [])
|
||
|
||
function hydrateForm() {
|
||
const source = props.entry || {}
|
||
const nextType = source.type || props.defaultType || 'expense'
|
||
const defaultCategory = props.initialCategoryId && (props.categories[nextType] || []).some((item) => item.id === props.initialCategoryId)
|
||
? props.initialCategoryId
|
||
: (props.categories[nextType]?.[0]?.id || '')
|
||
|
||
form.id = source.id || ''
|
||
form.type = nextType
|
||
form.amount = source.amount ? String(source.amount) : ''
|
||
form.categoryId = source.categoryId || defaultCategory
|
||
form.accountId = source.accountId || props.accounts[0]?.id || ''
|
||
form.date = source.date || toDateKey()
|
||
form.note = source.note || ''
|
||
form.createdAt = source.createdAt || 0
|
||
}
|
||
|
||
watch(
|
||
() => props.visible,
|
||
(visible) => {
|
||
if (visible) {
|
||
hydrateForm()
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
watch(
|
||
() => form.type,
|
||
(nextType) => {
|
||
const availableIds = (props.categories[nextType] || []).map((item) => item.id)
|
||
if (!availableIds.includes(form.categoryId)) {
|
||
form.categoryId = availableIds[0] || ''
|
||
}
|
||
}
|
||
)
|
||
|
||
function onDateChange(event) {
|
||
form.date = event.detail.value
|
||
}
|
||
|
||
function handleSave() {
|
||
if (!Number(form.amount)) {
|
||
uni.showToast({
|
||
title: '请输入有效金额',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
if (!form.categoryId || !form.accountId) {
|
||
uni.showToast({
|
||
title: '请选择分类和账户',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
emit('save', {
|
||
id: form.id,
|
||
type: form.type,
|
||
amount: Number(form.amount),
|
||
categoryId: form.categoryId,
|
||
accountId: form.accountId,
|
||
date: form.date,
|
||
note: form.note.trim(),
|
||
createdAt: form.createdAt
|
||
})
|
||
emit('close')
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.popup-shell {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 50;
|
||
}
|
||
|
||
.popup-mask {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: rgba(3, 12, 21, 0.42);
|
||
}
|
||
|
||
.popup-panel {
|
||
position: absolute;
|
||
left: 16rpx;
|
||
right: 16rpx;
|
||
bottom: 16rpx;
|
||
max-height: 84vh;
|
||
padding: 28rpx 28rpx 32rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24rpx;
|
||
}
|
||
|
||
.popup-head {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.close-text {
|
||
padding: 10rpx 0;
|
||
font-size: 24rpx;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.popup-body {
|
||
max-height: 58vh;
|
||
}
|
||
|
||
.field-block {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 18rpx;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.field-label {
|
||
font-size: 26rpx;
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.pill-row,
|
||
.chip-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.chip-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10rpx;
|
||
padding: 16rpx 20rpx;
|
||
border-radius: 22rpx;
|
||
background: var(--surface-muted);
|
||
color: var(--text-secondary);
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.chip-item.active {
|
||
background: var(--brand-soft);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.chip-dot {
|
||
width: 14rpx;
|
||
height: 14rpx;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.amount-shell {
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.prefix-text {
|
||
font-size: 36rpx;
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.picker-shell {
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.textarea-shell {
|
||
padding: 20rpx 24rpx;
|
||
min-height: 160rpx;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.textarea-shell textarea {
|
||
min-height: 120rpx;
|
||
}
|
||
|
||
.popup-foot {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.popup-foot .ghost-button {
|
||
flex: 0 0 180rpx;
|
||
}
|
||
|
||
.save-button {
|
||
flex: 1;
|
||
}
|
||
</style>
|
||
|