第一次上传

This commit is contained in:
xxk
2026-06-11 09:53:11 +08:00
commit e257f2009e
89 changed files with 4336 additions and 0 deletions
+473
View File
@@ -0,0 +1,473 @@
<template>
<view class="app-page" :class="themeClass">
<view class="surface-card hero-card">
<text class="hero-date">{{ todayLabel }}</text>
<text class="hero-title">账单小管家</text>
<!-- <text class="hero-subtitle">轻量记账预算管理消费统计所有账单仅保存在当前设备</text> -->
<view class="hero-metrics">
<view class="metric-block">
<text class="tiny-text">今日支出</text>
<text class="metric-value negative">{{ formatCurrency(todayExpense) }}</text>
</view>
<view class="metric-block">
<text class="tiny-text">本月支出</text>
<text class="metric-value negative">{{ formatCurrency(monthExpense) }}</text>
</view>
<view class="metric-block">
<text class="tiny-text">本月收入</text>
<text class="metric-value positive">{{ formatCurrency(monthIncome) }}</text>
</view>
<view class="metric-block">
<text class="tiny-text">本月结余</text>
<text class="metric-value">{{ formatCurrency(balance) }}</text>
</view>
</view>
</view>
<ad-custom unit-id="adunit-74730c6c27c95a37"></ad-custom>
<section-card title="预算概览" subtitle="实时查看预算执行情况与剩余额度">
<view class="budget-head">
<view>
<text class="budget-value">{{ formatCurrency(remainingBudget) }}</text>
<text style="margin-left: 16rpx;"></text>
<text class="tiny-text">剩余预算</text>
</view>
<view class="budget-side">
<text class="tiny-text">预算使用</text>
<text style="margin-left: 16rpx;"></text>
<text class="budget-percent">{{ budgetProgressLabel }}</text>
</view>
</view>
<view class="progress-track">
<view class="progress-fill" :style="{ width: budgetProgressWidth }"></view>
</view>
<view class="budget-note-row">
<text class="tiny-text">{{ totalBudget ? `总预算 ${formatCurrency(totalBudget)}` : '当前尚未设置月预算' }}</text>
<text class="tiny-text">{{ dailyBudgetText }}</text>
</view>
<view v-if="!totalBudget" class="budget-action-row">
<view class="ghost-button" @click="goBudget">去设置预算</view>
</view>
</section-card>
<section-card title="快捷记账" subtitle="常用场景一步录入,提高日常记录效率">
<view class="quick-action-row">
<view class="primary-button quick-main" @click="openQuickAdd('', 'expense')">记录支出</view>
<view class="ghost-button quick-main" @click="openQuickAdd('', 'income')">记录收入</view>
</view>
<view class="quick-grid">
<view
v-for="item in quickCategories"
:key="item.id"
class="quick-chip"
@click="openQuickAdd(item.id, 'expense')"
>
<view class="quick-dot" :style="{ background: item.color }"></view>
<text>{{ item.name }}</text>
</view>
</view>
</section-card>
<section-card title="最近记录" subtitle="保留最近 5 笔账单,长按可编辑或删除">
<template #action>
<text class="section-link" @click="goBills">查看全部</text>
</template>
<view v-if="recentBills.length" class="bill-list">
<view
v-for="bill in recentBills"
:key="bill.id"
class="bill-item"
@longpress="handleBillLongPress(bill)"
>
<view class="bill-leading">
<view class="bill-dot" :style="{ background: getCategory(bill).color || '#7b8794' }"></view>
<view>
<text class="bill-title">{{ getCategory(bill).name || '未分类' }}</text>
<text class="bill-meta">{{ getAccount(bill).name || '账户' }} · {{ formatDateLabel(bill.date) }}</text>
</view>
</view>
<view class="bill-right">
<text class="bill-amount" :class="bill.type === 'income' ? 'positive' : 'negative'">
{{ bill.type === 'income' ? '+' : '-' }}{{ formatCurrency(bill.amount).replace('¥', '') }}
</text>
<text class="tiny-text">{{ bill.note || '无备注' }}</text>
</view>
</view>
</view>
<view v-else class="empty-card">
<text class="section-subtitle">还没有账单记录先记下第一笔收支</text>
<view class="ghost-button empty-action" @click="openQuickAdd('', 'expense')">立即记账</view>
</view>
</section-card>
<view class="fab-button" @click="openQuickAdd('', 'expense')">+ 记一笔</view>
<app-tab-bar current="home" />
<bill-editor-popup
:visible="editorVisible"
:entry="editingBill"
:categories="store.state.categories"
:accounts="store.state.accounts"
:default-type="quickType"
:initial-category-id="quickCategoryId"
@close="closeEditor"
@save="saveBill"
/>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { onPullDownRefresh,onShareAppMessage,onShow,onLoad } 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, getDaysLeftInMonth, isSameMonth, toDateKey, toMonthKey } from '../../utils/date'
import { clampPercent, formatCurrency, formatPercent } from '../../utils/money'
const store = useAppStore()
const editorVisible = ref(false)
const editingBill = ref(null)
const quickCategoryId = ref('')
const quickType = ref('expense')
const todayKey = computed(() => toDateKey())
const currentMonth = computed(() => toMonthKey())
const themeClass = computed(() => (store.state.settings.theme === 'dark' ? 'theme-dark' : ''))
const todayLabel = computed(() => formatDateLabel(todayKey.value))
const sortedBills = computed(() => [...store.state.bills].sort((left, right) => right.createdAt - left.createdAt))
const recentBills = computed(() => sortedBills.value.slice(0, 5))
const todayBills = computed(() => sortedBills.value.filter((item) => item.date === todayKey.value))
const monthBills = computed(() => sortedBills.value.filter((item) => isSameMonth(item.date, currentMonth.value)))
const todayExpense = computed(() => todayBills.value.filter((item) => item.type === 'expense').reduce((sum, item) => sum + Number(item.amount), 0))
const monthExpense = computed(() => monthBills.value.filter((item) => item.type === 'expense').reduce((sum, item) => sum + Number(item.amount), 0))
const monthIncome = computed(() => monthBills.value.filter((item) => item.type === 'income').reduce((sum, item) => sum + Number(item.amount), 0))
const balance = computed(() => monthIncome.value - monthExpense.value)
const totalBudget = computed(() => Number(store.state.budgets.total) || 0)
const remainingBudget = computed(() => totalBudget.value - monthExpense.value)
const dailyAllowance = computed(() => Math.max(remainingBudget.value, 0) / Math.max(1, getDaysLeftInMonth(currentMonth.value)))
const budgetProgressWidth = computed(() => clampPercent(monthExpense.value / Math.max(totalBudget.value || 1, 1)))
const budgetProgressLabel = computed(() => (totalBudget.value ? formatPercent(monthExpense.value / Math.max(totalBudget.value, 1)) : '未设置'))
const dailyBudgetText = computed(() => {
if (!totalBudget.value) {
return '设置预算后可查看日均可用额度'
}
if (remainingBudget.value < 0) {
return `已超支 ${formatCurrency(Math.abs(remainingBudget.value))}`
}
return `日均可用 ${formatCurrency(dailyAllowance.value)}`
})
const quickCategories = computed(() => store.state.categories.expense.slice(0, 6))
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 openQuickAdd(categoryId = '', type = 'expense') {
quickCategoryId.value = categoryId
quickType.value = type
editingBill.value = null
editorVisible.value = true
}
function closeEditor() {
editorVisible.value = false
editingBill.value = null
quickCategoryId.value = ''
quickType.value = 'expense'
}
function saveBill(payload) {
store.saveBill(payload)
uni.showToast({
title: payload.id ? '账单已更新' : '账单已保存',
icon: 'none'
})
}
function confirmDelete(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) {
editingBill.value = { ...bill }
editorVisible.value = true
}
if (tapIndex === 1) {
confirmDelete(bill)
}
}
})
}
function goBills() {
uni.redirectTo({
url: '/pages/bills/index'
})
}
function goBudget() {
uni.redirectTo({
url: '/pages/budget/index'
})
}
onPullDownRefresh(() => {
setTimeout(() => {
uni.stopPullDownRefresh()
}, 200)
})
const showFlag = ref(false);
onLoad(()=>{
showIt()
})
onShow(()=>{
showIt()
})
function showIt(){
if (showFlag.value){
return
}
showFlag.value = true
let interstitialAd = null;
if (wx.createInterstitialAd) {
interstitialAd = wx.createInterstitialAd({
adUnitId: 'adunit-0abc32053b19a4e9'
})
interstitialAd.onLoad(() => {})
interstitialAd.onError((err) => {
console.error('插屏广告加载失败', err)
})
interstitialAd.onClose(() => {})
}
setTimeout(()=>{
if (interstitialAd) {
interstitialAd.show().catch((err) => {
console.error('插屏广告显示失败', err)
})
showFlag.value = false
}
}, 5480)
}
onShareAppMessage((res) => {
// res.from === 'button' 代表来自页面内按钮
// res.from === 'menu' 代表来自右上角菜单
return {
title: '账单助手', // 分享卡片标题
desc:'本地单机极简记账,支持收支记录、预算管控、消费报表,数据安全私密,轻便好用的个人账单管家。',
path: '/pages/home/index', // 分享后点击跳转的页面(必须是绝对路径)
}
})
</script>
<style lang="scss" scoped>
.hero-card {
display: flex;
flex-direction: column;
gap: 18rpx;
padding: 32rpx;
background: var(--bg-accent);
color: #ffffff;
}
.hero-date,
.hero-subtitle,
.hero-card .tiny-text {
color: rgba(255, 255, 255, 0.76);
}
.hero-title {
font-size: 46rpx;
font-weight: 700;
}
.hero-subtitle {
font-size: 24rpx;
line-height: 1.7;
}
.hero-metrics {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18rpx;
margin-top: 10rpx;
}
.metric-block {
padding: 20rpx;
border-radius: 24rpx;
background: rgba(255, 255, 255, 0.12);
}
.metric-value {
display: block;
margin-top: 10rpx;
font-size: 32rpx;
font-weight: 700;
color: #ffffff;
}
.budget-head,
.budget-note-row,
.quick-action-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18rpx;
}
.budget-side {
text-align: right;
}
.budget-value,
.budget-percent {
font-size: 36rpx;
font-weight: 700;
color: var(--text-primary);
}
.progress-track {
overflow: hidden;
height: 18rpx;
margin: 24rpx 0 16rpx;
border-radius: 999rpx;
background: var(--surface-muted);
}
.progress-fill {
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #d36c43 0%, #1f6f5f 100%);
}
.budget-action-row {
margin-top: 18rpx;
}
.quick-action-row .quick-main {
flex: 1;
}
.quick-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 18rpx;
}
.quick-chip {
display: flex;
align-items: center;
gap: 12rpx;
padding: 16rpx 18rpx;
border-radius: 22rpx;
background: var(--surface-muted);
font-size: 24rpx;
color: var(--text-secondary);
}
.quick-dot,
.bill-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
}
.section-link {
font-size: 24rpx;
color: var(--brand);
}
.bill-list {
display: flex;
flex-direction: column;
gap: 18rpx;
}
.bill-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
padding: 22rpx;
border-radius: 24rpx;
background: var(--surface-muted);
}
.bill-leading {
display: flex;
align-items: center;
gap: 16rpx;
flex: 1;
}
.bill-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8rpx;
}
.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);
}
.empty-card {
padding: 28rpx 0 8rpx;
text-align: center;
}
.empty-action {
margin-top: 18rpx;
}
.fab-button {
position: fixed;
right: 32rpx;
bottom: 348rpx;
padding: 24rpx 30rpx;
border-radius: 999rpx;
background: var(--bg-accent);
color: #ffffff;
font-size: 28rpx;
font-weight: 700;
box-shadow: 0 20rpx 42rpx rgba(16, 42, 67, 0.24);
z-index: 18;
}
</style>