第一次上传
This commit is contained in:
@@ -0,0 +1,492 @@
|
||||
<template>
|
||||
<view class="app-page" :class="themeClass">
|
||||
<section-card title="账单筛选" subtitle="按月份、分类、账户、金额区间和关键词组合查询">
|
||||
<view class="chip-row">
|
||||
<view
|
||||
v-for="item in typeOptions"
|
||||
:key="item.value"
|
||||
class="pill-button"
|
||||
:class="{ active: filters.type === item.value }"
|
||||
@click="filters.type = item.value; filters.categoryId = ''"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="chip-row compact-row">
|
||||
<view
|
||||
v-for="item in periodOptions"
|
||||
:key="item.value"
|
||||
class="pill-button mini-pill"
|
||||
:class="{ active: filters.period === item.value }"
|
||||
@click="filters.period = item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
<picker mode="date" fields="month" :value="monthPickerValue" @change="onMonthChange">
|
||||
<view class="pill-button mini-pill">{{ selectedMonthLabel }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-grid">
|
||||
<view class="input-shell"><input v-model="filters.keyword" placeholder="备注、分类或账户关键词" /></view>
|
||||
<view class="double-grid">
|
||||
<view class="input-shell"><input v-model="filters.minAmount" type="digit" placeholder="最低金额" /></view>
|
||||
<view class="input-shell"><input v-model="filters.maxAmount" type="digit" placeholder="最高金额" /></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="picker-row">
|
||||
<picker :range="categoryOptionNames" :value="selectedCategoryIndex" @change="onCategoryChange">
|
||||
<view class="input-shell picker-shell"><text>{{ selectedCategoryName }}</text><text class="tiny-text">分类</text></view>
|
||||
</picker>
|
||||
<picker :range="accountOptionNames" :value="selectedAccountIndex" @change="onAccountChange">
|
||||
<view class="input-shell picker-shell"><text>{{ selectedAccountName }}</text><text class="tiny-text">账户</text></view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="action-row">
|
||||
<view class="ghost-button" @click="clearFilters">清空筛选</view>
|
||||
<view class="primary-button" @click="openEditor()">新增账单</view>
|
||||
</view>
|
||||
</section-card>
|
||||
|
||||
<section-card title="账单概览" :subtitle="`共 ${filteredBills.length} 笔记录,点击编辑,长按可删除`">
|
||||
<template #action>
|
||||
<text class="section-link" @click="toggleSelectionMode">{{ selectionMode ? '退出批量' : '批量删除' }}</text>
|
||||
</template>
|
||||
<view class="summary-row">
|
||||
<view class="summary-item surface-strong">
|
||||
<text class="tiny-text">支出</text>
|
||||
<text style="margin-left: 16rpx;"></text>
|
||||
<text class="summary-value negative">{{ formatCurrency(summaryExpense) }}</text>
|
||||
</view>
|
||||
<view class="summary-item surface-strong">
|
||||
<text class="tiny-text">收入</text>
|
||||
<text style="margin-left: 16rpx;"></text>
|
||||
<text class="summary-value positive">{{ formatCurrency(summaryIncome) }}</text>
|
||||
</view>
|
||||
<view class="summary-item surface-strong">
|
||||
<text class="tiny-text">结余</text>
|
||||
<text style="margin-left: 16rpx;"></text>
|
||||
<text class="summary-value">{{ formatCurrency(summaryIncome - summaryExpense) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="filteredBills.length" class="bill-list">
|
||||
<view
|
||||
v-for="bill in filteredBills"
|
||||
:key="bill.id"
|
||||
class="bill-row"
|
||||
@click="selectionMode ? toggleChecked(bill.id) : openEditor(bill)"
|
||||
@longpress="handleBillLongPress(bill)"
|
||||
>
|
||||
<view class="row-main">
|
||||
<view v-if="selectionMode" class="check-box" :class="{ checked: selectedIds.includes(bill.id) }"></view>
|
||||
<view class="bill-dot" :style="{ background: getCategory(bill).color || '#7b8794' }"></view>
|
||||
<view class="bill-body">
|
||||
<text class="bill-title">{{ getCategory(bill).name || '未分类' }}</text>
|
||||
<text class="bill-meta">{{ getAccount(bill).name || '账户' }} · {{ formatDateLabel(bill.date) }} · {{ bill.note || '无备注' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="bill-amount" :class="bill.type === 'income' ? 'positive' : 'negative'">
|
||||
{{ bill.type === 'income' ? '+' : '-' }}{{ formatCurrency(bill.amount).replace('¥', '') }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-card">
|
||||
<text class="section-subtitle">当前筛选条件下没有符合条件的账单记录。</text>
|
||||
</view>
|
||||
</section-card>
|
||||
|
||||
<view v-if="selectionMode" class="surface-card batch-bar">
|
||||
<text class="batch-text">已选 {{ selectedIds.length }} 笔</text>
|
||||
<view class="ghost-button" @click="selectAllVisible">全选</view>
|
||||
<view class="ghost-button danger-button" @click="removeSelected">删除所选</view>
|
||||
</view>
|
||||
|
||||
<app-tab-bar current="bills" />
|
||||
<bill-editor-popup
|
||||
:visible="editorVisible"
|
||||
:entry="editingBill"
|
||||
:categories="store.state.categories"
|
||||
:accounts="store.state.accounts"
|
||||
@close="closeEditor"
|
||||
@save="saveBill"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import SectionCard from '../../components/SectionCard.vue'
|
||||
import AppTabBar from '../../components/AppTabBar.vue'
|
||||
import BillEditorPopup from '../../components/BillEditorPopup.vue'
|
||||
import { useAppStore } from '../../utils/store'
|
||||
import { formatDateLabel, formatMonthLabel, isSameMonth, parseDate, toMonthKey } from '../../utils/date'
|
||||
import { formatCurrency } from '../../utils/money'
|
||||
|
||||
const store = useAppStore()
|
||||
const editorVisible = ref(false)
|
||||
const editingBill = ref(null)
|
||||
const selectionMode = ref(false)
|
||||
const selectedIds = ref([])
|
||||
|
||||
const themeClass = computed(() => (store.state.settings.theme === 'dark' ? 'theme-dark' : ''))
|
||||
const filters = reactive({
|
||||
type: 'all',
|
||||
period: 'month',
|
||||
month: toMonthKey(),
|
||||
keyword: '',
|
||||
categoryId: '',
|
||||
accountId: '',
|
||||
minAmount: '',
|
||||
maxAmount: ''
|
||||
})
|
||||
|
||||
const typeOptions = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '支出', value: 'expense' },
|
||||
{ label: '收入', value: 'income' }
|
||||
]
|
||||
|
||||
const periodOptions = [
|
||||
{ label: '本月', value: 'month' },
|
||||
{ label: '近 7 天', value: '7d' },
|
||||
{ label: '近 30 天', value: '30d' },
|
||||
{ label: '全部', value: 'all' }
|
||||
]
|
||||
|
||||
const sortedBills = computed(() => [...store.state.bills].sort((left, right) => right.createdAt - left.createdAt))
|
||||
const categoryOptions = computed(() => {
|
||||
const categories = filters.type === 'all'
|
||||
? [...store.state.categories.expense, ...store.state.categories.income]
|
||||
: store.state.categories[filters.type]
|
||||
return [{ id: '', name: '全部分类' }, ...categories]
|
||||
})
|
||||
const accountOptions = computed(() => [{ id: '', name: '全部账户' }, ...store.state.accounts])
|
||||
const categoryOptionNames = computed(() => categoryOptions.value.map((item) => item.name))
|
||||
const accountOptionNames = computed(() => accountOptions.value.map((item) => item.name))
|
||||
const selectedCategoryIndex = computed(() => Math.max(0, categoryOptions.value.findIndex((item) => item.id === filters.categoryId)))
|
||||
const selectedAccountIndex = computed(() => Math.max(0, accountOptions.value.findIndex((item) => item.id === filters.accountId)))
|
||||
const selectedCategoryName = computed(() => categoryOptions.value[selectedCategoryIndex.value]?.name || '全部分类')
|
||||
const selectedAccountName = computed(() => accountOptions.value[selectedAccountIndex.value]?.name || '全部账户')
|
||||
const selectedMonthLabel = computed(() => formatMonthLabel(filters.month))
|
||||
const monthPickerValue = computed(() => filters.month)
|
||||
|
||||
function getDiffDays(dateKey) {
|
||||
const today = new Date()
|
||||
const current = parseDate(dateKey)
|
||||
return (today.getTime() - current.getTime()) / (1000 * 60 * 60 * 24)
|
||||
}
|
||||
|
||||
const filteredBills = computed(() => sortedBills.value.filter((bill) => {
|
||||
if (filters.type !== 'all' && bill.type !== filters.type) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (filters.period === 'month' && !isSameMonth(bill.date, filters.month)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (filters.period === '7d' && getDiffDays(bill.date) > 7) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (filters.period === '30d' && getDiffDays(bill.date) > 30) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (filters.categoryId && bill.categoryId !== filters.categoryId) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (filters.accountId && bill.accountId !== filters.accountId) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (filters.minAmount && Number(bill.amount) < Number(filters.minAmount)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (filters.maxAmount && Number(bill.amount) > Number(filters.maxAmount)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (filters.keyword) {
|
||||
const categoryName = getCategory(bill).name || ''
|
||||
const accountName = getAccount(bill).name || ''
|
||||
const keyword = filters.keyword.trim().toLowerCase()
|
||||
const target = `${bill.note || ''} ${categoryName} ${accountName}`.toLowerCase()
|
||||
if (!target.includes(keyword)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}))
|
||||
|
||||
const summaryExpense = computed(() => filteredBills.value.filter((item) => item.type === 'expense').reduce((sum, item) => sum + Number(item.amount), 0))
|
||||
const summaryIncome = computed(() => filteredBills.value.filter((item) => item.type === 'income').reduce((sum, item) => sum + Number(item.amount), 0))
|
||||
|
||||
function getCategory(bill) {
|
||||
return (store.state.categories[bill.type] || []).find((item) => item.id === bill.categoryId) || {}
|
||||
}
|
||||
|
||||
function getAccount(bill) {
|
||||
return store.state.accounts.find((item) => item.id === bill.accountId) || {}
|
||||
}
|
||||
|
||||
function onCategoryChange(event) {
|
||||
filters.categoryId = categoryOptions.value[Number(event.detail.value)]?.id || ''
|
||||
}
|
||||
|
||||
function onAccountChange(event) {
|
||||
filters.accountId = accountOptions.value[Number(event.detail.value)]?.id || ''
|
||||
}
|
||||
|
||||
function onMonthChange(event) {
|
||||
filters.month = String(event.detail.value).slice(0, 7)
|
||||
filters.period = 'month'
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
filters.type = 'all'
|
||||
filters.period = 'month'
|
||||
filters.month = toMonthKey()
|
||||
filters.keyword = ''
|
||||
filters.categoryId = ''
|
||||
filters.accountId = ''
|
||||
filters.minAmount = ''
|
||||
filters.maxAmount = ''
|
||||
}
|
||||
|
||||
function openEditor(bill = null) {
|
||||
editingBill.value = bill ? { ...bill } : null
|
||||
editorVisible.value = true
|
||||
}
|
||||
|
||||
function closeEditor() {
|
||||
editorVisible.value = false
|
||||
editingBill.value = null
|
||||
}
|
||||
|
||||
function saveBill(payload) {
|
||||
store.saveBill(payload)
|
||||
uni.showToast({
|
||||
title: payload.id ? '账单已更新' : '账单已保存',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
function removeBill(bill) {
|
||||
uni.showModal({
|
||||
title: '删除账单',
|
||||
content: `确认删除 ${getCategory(bill).name || '该账单'} 吗?`,
|
||||
success: ({ confirm }) => {
|
||||
if (confirm) {
|
||||
store.deleteBill(bill.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleBillLongPress(bill) {
|
||||
uni.showActionSheet({
|
||||
itemList: ['编辑账单', '删除账单'],
|
||||
success: ({ tapIndex }) => {
|
||||
if (tapIndex === 0) {
|
||||
openEditor(bill)
|
||||
}
|
||||
if (tapIndex === 1) {
|
||||
removeBill(bill)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function toggleSelectionMode() {
|
||||
selectionMode.value = !selectionMode.value
|
||||
selectedIds.value = []
|
||||
}
|
||||
|
||||
function toggleChecked(id) {
|
||||
if (!selectionMode.value) {
|
||||
return
|
||||
}
|
||||
selectedIds.value = selectedIds.value.includes(id)
|
||||
? selectedIds.value.filter((item) => item !== id)
|
||||
: [...selectedIds.value, id]
|
||||
}
|
||||
|
||||
function selectAllVisible() {
|
||||
selectedIds.value = filteredBills.value.map((item) => item.id)
|
||||
}
|
||||
|
||||
function removeSelected() {
|
||||
if (!selectedIds.value.length) {
|
||||
uni.showToast({ title: '请选择账单', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: '批量删除',
|
||||
content: `确认删除 ${selectedIds.value.length} 笔账单吗?`,
|
||||
success: ({ confirm }) => {
|
||||
if (confirm) {
|
||||
store.deleteBills(selectedIds.value)
|
||||
selectedIds.value = []
|
||||
selectionMode.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
setTimeout(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
}, 200)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chip-row,
|
||||
.action-row,
|
||||
.picker-row,
|
||||
.summary-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.compact-row {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.mini-pill {
|
||||
padding: 16rpx 22rpx;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
margin: 18rpx 0;
|
||||
}
|
||||
|
||||
.double-grid,
|
||||
.picker-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.picker-shell {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.action-row .ghost-button,
|
||||
.action-row .primary-button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.section-link {
|
||||
font-size: 24rpx;
|
||||
color: var(--brand);
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
flex: 1;
|
||||
padding: 22rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
margin-top: 10rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.bill-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
|
||||
.bill-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
padding: 22rpx;
|
||||
border-radius: 24rpx;
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
.row-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-box {
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
border-radius: 10rpx;
|
||||
border: 2rpx solid var(--brand);
|
||||
}
|
||||
|
||||
.check-box.checked {
|
||||
background: var(--brand);
|
||||
}
|
||||
|
||||
.bill-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.bill-body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bill-title,
|
||||
.bill-amount {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.bill-meta {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.empty-card {
|
||||
padding: 40rpx 0 10rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.batch-bar {
|
||||
position: fixed;
|
||||
left: 24rpx;
|
||||
right: 24rpx;
|
||||
bottom: 176rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 20rpx 22rpx;
|
||||
z-index: 18;
|
||||
}
|
||||
|
||||
.batch-text {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user