第一次上传
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user