第一次上传

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
+24
View File
@@ -0,0 +1,24 @@
=== 插屏
// 在页面中定义插屏广告
let interstitialAd = null
// 在页面onLoad回调事件中创建插屏广告实例
if (wx.createInterstitialAd) {
interstitialAd = wx.createInterstitialAd({
adUnitId: 'adunit-0abc32053b19a4e9'
})
interstitialAd.onLoad(() => {})
interstitialAd.onError((err) => {
console.error('插屏广告加载失败', err)
})
interstitialAd.onClose(() => {})
}
// 在适合的场景显示插屏广告
if (interstitialAd) {
interstitialAd.show().catch((err) => {
console.error('插屏广告显示失败', err)
})
}
=== 原生广告
+73
View File
@@ -0,0 +1,73 @@
微信小程序「账单小管家」精简版开发需求报告
一、项目概述
定位:极简无广告、面向年轻人(学生/职场新人/情侣/合租党)的轻量化记账工具,核心解决“快速记账、清晰控支、多人AA”需求,适配微信生态,操作极简、数据安全。
技术基础:本地存储(无需云开发),微信授权登录(仅用于身份标识,数据不上传),无需单独注册,数据仅保存在本地设备。
设计风格:简约清新,支持深色模式,无广告,记账步骤≤3步。
二、核心功能模块(必做+高价值)
1. 首页(核心入口)
- 实时显示今日/本月收支、结余、预算进度条
- 悬浮一键记账按钮,常用分类快捷入口
- 最近账单快速查看,下拉刷新,长按编辑/删除
2. 账单管理(核心功能)
- 新增/编辑/删除账单:支持收支切换、金额输入、分类选择、备注、时间调整、账户选择
- 筛选查询:按时间、分类、账户、金额区间筛选,关键词搜索
- 支持批量删除账单
3. 预算管理
- 设置月度总预算、分类单独预算
- 超支提醒(小程序红点+微信服务通知)
- 显示剩余日均可花金额
4. 数据统计与报表
- 可视化图表:支出分类饼图、每日趋势折线图、月度对比柱状图
- 支持Excel账单导出、月度消费海报生成(可分享)
6. 个人中心与设置
- 微信授权登录,主题切换(浅/深色)
- 数据本地备份(导出本地文件)与恢复,清除缓存
- 反馈入口、关于我们
三、增强功能(二期迭代)
- 固定收支管理(自动记账)
- 存款目标设置与进度显示
- 记账习惯打卡、月度勋章
四、技术与微信能力要求
- 本地存储:设计用户、账单、分类等相关本地数据表,确保数据仅保存在设备本地,不进行云端上传。
- 微信能力:仅保留授权登录(身份标识)、海报生成(本地生成,不涉及云端)功能,无需云存储、好友/群分享、服务通知。
- 交互:手势操作、柔和动画,适配手机各机型
五、开发优先级
- 一期(必做):首页+账单管理+预算管理+报表+个人中心
- 二期(迭代):固定收支+存款目标+记账打卡(均为本地功能)
- 三期(迭代):增强功能+细节优化
BIN
View File
Binary file not shown.
+185
View File
@@ -0,0 +1,185 @@
<script>
export default {
onLaunch() {
console.log('Bill Helper Launch')
}
}
</script>
<style lang="scss">
page {
background: #f4ede3;
color: #16202a;
font-family: 'PingFang SC', 'HarmonyOS Sans SC', sans-serif;
--bg-app: #f4ede3;
--bg-accent: linear-gradient(135deg, #102a43 0%, #1f6f5f 100%);
--surface-card: rgba(255, 255, 255, 0.9);
--surface-strong: #ffffff;
--surface-muted: rgba(255, 255, 255, 0.68);
--text-primary: #16202a;
--text-secondary: #617081;
--text-muted: #91a0af;
--line-soft: rgba(22, 32, 42, 0.08);
--brand: #1f6f5f;
--brand-soft: rgba(31, 111, 95, 0.14);
--danger: #d25543;
--danger-soft: rgba(210, 85, 67, 0.14);
--warning: #c48a1f;
--shadow-card: 0 18rpx 40rpx rgba(16, 42, 67, 0.08);
}
view,
text,
button,
input,
textarea,
scroll-view {
box-sizing: border-box;
}
button {
margin: 0;
padding: 0;
background: transparent;
line-height: 1;
border: 0;
}
button::after {
border: 0;
}
.app-page {
display: flex;
flex-direction: column;
gap: 24rpx;
min-height: 100vh;
padding: 28rpx 28rpx 188rpx;
background:
radial-gradient(circle at top right, rgba(31, 111, 95, 0.16), transparent 32%),
var(--bg-app);
color: var(--text-primary);
}
.theme-dark {
--bg-app: #0f1720;
--bg-accent: linear-gradient(135deg, #09111a 0%, #1d4e46 100%);
--surface-card: rgba(19, 29, 40, 0.92);
--surface-strong: #162331;
--surface-muted: rgba(24, 35, 49, 0.76);
--text-primary: #eef5fb;
--text-secondary: #9db0c2;
--text-muted: #7c90a3;
--line-soft: rgba(255, 255, 255, 0.08);
--brand: #64c6a9;
--brand-soft: rgba(100, 198, 169, 0.14);
--danger: #ff8c78;
--danger-soft: rgba(255, 140, 120, 0.14);
--warning: #f2c56d;
--shadow-card: 0 18rpx 48rpx rgba(0, 0, 0, 0.28);
}
.surface-card {
background: var(--surface-card);
border: 1rpx solid var(--line-soft);
border-radius: 30rpx;
box-shadow: var(--shadow-card);
backdrop-filter: blur(12rpx);
}
.surface-strong {
background: var(--surface-strong);
}
.section-title {
font-size: 34rpx;
font-weight: 600;
color: var(--text-primary);
}
.section-subtitle {
font-size: 24rpx;
color: var(--text-secondary);
}
.pill-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 18rpx 24rpx;
border-radius: 999rpx;
background: var(--surface-muted);
color: var(--text-secondary);
font-size: 24rpx;
}
.pill-button.active {
background: var(--brand);
color: #ffffff;
}
.primary-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 22rpx 28rpx;
border-radius: 24rpx;
background: var(--bg-accent);
color: #ffffff;
font-size: 28rpx;
font-weight: 600;
}
.ghost-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 20rpx 26rpx;
border-radius: 22rpx;
background: var(--brand-soft);
color: var(--brand);
font-size: 26rpx;
font-weight: 600;
}
.danger-button {
background: var(--danger-soft);
color: var(--danger);
}
.input-shell {
display: flex;
align-items: center;
min-height: 86rpx;
padding: 0 24rpx;
border-radius: 24rpx;
background: var(--surface-muted);
border: 1rpx solid transparent;
}
.input-shell input,
.input-shell textarea {
width: 100%;
font-size: 28rpx;
color: var(--text-primary);
}
.muted-text {
color: var(--text-secondary);
}
.tiny-text {
font-size: 22rpx;
color: var(--text-muted);
}
.positive {
color: var(--brand);
}
.negative {
color: var(--danger);
}
</style>
+85
View File
@@ -0,0 +1,85 @@
<template>
<view class="surface-card tabbar">
<view
v-for="item in tabs"
:key="item.id"
class="tab-item"
:class="{ active: current === item.id }"
@click="navigate(item)"
>
<view class="tab-dot" :style="{ background: current === item.id ? item.color : 'var(--line-soft)' }"></view>
<text class="tab-label">{{ item.label }}</text>
</view>
</view>
</template>
<script setup>
const props = defineProps({
current: {
type: String,
default: 'home'
}
})
const tabs = [
{ id: 'home', label: '首页', path: '/pages/home/index', color: '#1f6f5f' },
{ id: 'bills', label: '账单', path: '/pages/bills/index', color: '#d36c43' },
{ id: 'budget', label: '预算', path: '/pages/budget/index', color: '#5f8df5' },
{ id: 'stats', label: '报表', path: '/pages/stats/index', color: '#7f56d9' },
{ id: 'mine', label: '我的', path: '/pages/mine/index', color: '#44546a' }
]
function navigate(item) {
if (item.id === props.current) {
return
}
uni.redirectTo({
url: item.path
})
}
</script>
<style lang="scss" scoped>
.tabbar {
position: fixed;
left: 24rpx;
right: 24rpx;
bottom: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 18rpx 12rpx calc(env(safe-area-inset-bottom));
z-index: 20;
}
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 10rpx;
padding: 14rpx 0;
border-radius: 22rpx;
}
.tab-item.active {
background: var(--brand-soft);
}
.tab-dot {
width: 18rpx;
height: 18rpx;
border-radius: 50%;
}
.tab-label {
font-size: 22rpx;
color: var(--text-secondary);
}
.tab-item.active .tab-label {
color: var(--text-primary);
font-weight: 600;
}
</style>
+342
View File
@@ -0,0 +1,342 @@
<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>
+45
View File
@@ -0,0 +1,45 @@
<template>
<view class="surface-card card">
<view class="head" v-if="title || subtitle || $slots.action">
<view class="title-group">
<text v-if="title" class="section-title">{{ title }}</text>
<text v-if="subtitle" class="section-subtitle">{{ subtitle }}</text>
</view>
<slot name="action"></slot>
</view>
<slot></slot>
</view>
</template>
<script setup>
defineProps({
title: {
type: String,
default: ''
},
subtitle: {
type: String,
default: ''
}
})
</script>
<style lang="scss" scoped>
.card {
padding: 28rpx;
}
.head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16rpx;
margin-bottom: 24rpx;
}
.title-group {
display: flex;
flex-direction: column;
gap: 10rpx;
}
</style>
+20
View File
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>
+22
View File
@@ -0,0 +1,22 @@
import App from './App'
// #ifndef VUE3
import Vue from 'vue'
import './uni.promisify.adaptor'
Vue.config.productionTip = false
App.mpType = 'app'
const app = new Vue({
...App
})
app.$mount()
// #endif
// #ifdef VUE3
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
return {
app
}
}
// #endif
+65
View File
@@ -0,0 +1,65 @@
{
"name" : "账单小管家",
"appid" : "__UNI__8989AB7",
"description" : "本地记账、预算管理与消费统计微信小程序",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
"modules" : {},
"distribute" : {
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios" : {},
"sdkConfigs" : {}
}
},
"quickapp" : {},
"mp-weixin" : {
"appid" : "wx8a928bb8a3945a16",
"setting" : {
"urlCheck" : false,
"minified" : true
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3"
}
+66
View File
@@ -0,0 +1,66 @@
{
"pages": [
{
"path": "pages/home/index",
"style": {
"navigationBarTitleText": "首页",
"enablePullDownRefresh": true
}
},
{
"path": "pages/bills/index",
"style": {
"navigationBarTitleText": "账单管理",
"enablePullDownRefresh": true
}
},
{
"path": "pages/budget/index",
"style": {
"navigationBarTitleText": "预算管理"
}
},
{
"path": "pages/stats/index",
"style": {
"navigationBarTitleText": "数据报表"
}
},
{
"path": "pages/mine/index",
"style": {
"navigationBarTitleText": "我的"
}
},
{
"path": "pages/mine/profile/index",
"style": {
"navigationBarTitleText": "账户资料"
}
},
{
"path": "pages/mine/backup/index",
"style": {
"navigationBarTitleText": "备份与恢复"
}
},
{
"path": "pages/mine/guide/index",
"style": {
"navigationBarTitleText": "使用帮助"
}
},
{
"path": "pages/mine/about/index",
"style": {
"navigationBarTitleText": "关于与隐私"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarBackgroundColor": "#F4EDE3",
"backgroundColor": "#F4EDE3",
"backgroundTextStyle": "dark"
}
}
+492
View File
@@ -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>
+233
View File
@@ -0,0 +1,233 @@
<template>
<view class="app-page" :class="themeClass">
<section-card title="月度预算" subtitle="设置总预算与分类预算,控制消费节奏">
<view class="budget-hero surface-strong">
<view>
<text class="tiny-text">本月总预算</text>
<text class="budget-number">{{ formatCurrency(totalBudget) }}</text>
</view>
<view>
<text class="tiny-text">本月已支出</text>
<text class="budget-number negative">{{ formatCurrency(spentTotal) }}</text>
</view>
</view>
<view class="progress-track">
<view class="progress-fill" :style="{ width: usagePercentWidth }"></view>
</view>
<view class="budget-foot">
<text class="tiny-text">使用进度 {{ usagePercentLabel }}</text>
<text class="tiny-text">{{ remainingBudget >= 0 ? `剩余 ${formatCurrency(remainingBudget)}` : `已超支 ${formatCurrency(Math.abs(remainingBudget))}` }}</text>
</view>
<view class="budget-foot secondary-foot">
<text class="tiny-text">{{ totalBudget ? `日均可用 ${formatCurrency(dailyAllowance)}` : '设置预算后可查看剩余额度分配' }}</text>
</view>
<view class="editor-row">
<view class="input-shell"><input v-model="totalBudgetInput" type="digit" placeholder="输入本月总预算" /></view>
<view class="primary-button save-btn" @click="saveTotalBudget">保存</view>
</view>
</section-card>
<section-card v-if="overBudgetRows.length" title="超支提醒" subtitle="当前分类预算已被突破,建议尽快调整">
<view class="alert-list">
<view v-for="item in overBudgetRows" :key="item.id" class="alert-item">
<text class="alert-title">{{ item.name }}</text>
<text class="alert-text">预算 {{ formatCurrency(item.budget) }}已支出 {{ formatCurrency(item.spent) }}</text>
</view>
</view>
</section-card>
<section-card title="分类预算" subtitle="为高频分类分别设置预算,减少超支风险">
<view class="category-list">
<view v-for="item in categoryRows" :key="item.id" class="category-card surface-strong">
<view class="category-head">
<view class="category-title-row">
<view class="category-dot" :style="{ background: item.color }"></view>
<text class="category-title">{{ item.name }}</text>
</view>
<text class="tiny-text">已花 {{ formatCurrency(item.spent) }}</text>
</view>
<view class="progress-track thin-track">
<view class="progress-fill" :style="{ width: item.progressWidth }"></view>
</view>
<view class="budget-foot">
<text class="tiny-text">预算 {{ formatCurrency(item.budget) }}</text>
<text class="tiny-text" :class="item.budget > 0 && item.spent > item.budget ? 'negative' : ''">{{ item.progressLabel }}</text>
</view>
<view class="editor-row">
<view class="input-shell"><input v-model="categoryBudgetDrafts[item.id]" type="digit" placeholder="设置分类预算" /></view>
<view class="ghost-button save-btn" @click="saveCategoryBudget(item.id)">保存</view>
</view>
</view>
</view>
</section-card>
<app-tab-bar current="budget" />
</view>
</template>
<script setup>
import { computed, reactive, ref, watch } from 'vue'
import SectionCard from '../../components/SectionCard.vue'
import AppTabBar from '../../components/AppTabBar.vue'
import { useAppStore } from '../../utils/store'
import { getDaysLeftInMonth, isSameMonth, toMonthKey } from '../../utils/date'
import { clampPercent, formatCurrency, formatPercent } from '../../utils/money'
const store = useAppStore()
const currentMonth = computed(() => toMonthKey())
const themeClass = computed(() => (store.state.settings.theme === 'dark' ? 'theme-dark' : ''))
const totalBudgetInput = ref(String(store.state.budgets.total || ''))
const categoryBudgetDrafts = reactive({})
const expenseBills = computed(() => store.state.bills.filter((item) => item.type === 'expense' && isSameMonth(item.date, currentMonth.value)))
const spentTotal = computed(() => expenseBills.value.reduce((sum, item) => sum + Number(item.amount), 0))
const totalBudget = computed(() => Number(store.state.budgets.total) || 0)
const remainingBudget = computed(() => totalBudget.value - spentTotal.value)
const dailyAllowance = computed(() => Math.max(remainingBudget.value, 0) / Math.max(1, getDaysLeftInMonth(currentMonth.value)))
const usagePercentLabel = computed(() => (totalBudget.value ? formatPercent(spentTotal.value / Math.max(totalBudget.value, 1)) : '未设置'))
const usagePercentWidth = computed(() => clampPercent(spentTotal.value / Math.max(totalBudget.value || 1, 1)))
const categoryRows = computed(() => store.state.categories.expense.map((category) => {
const spent = expenseBills.value
.filter((item) => item.categoryId === category.id)
.reduce((sum, item) => sum + Number(item.amount), 0)
const budget = Number(store.state.budgets.categoryBudgets[category.id] || 0)
return {
...category,
spent,
budget,
progressLabel: budget ? formatPercent(spent / Math.max(budget, 1)) : '未设置',
progressWidth: clampPercent(spent / Math.max(budget || 1, 1))
}
}))
const overBudgetRows = computed(() => categoryRows.value.filter((item) => item.budget > 0 && item.spent > item.budget))
watch(
() => store.state.budgets.total,
(value) => {
totalBudgetInput.value = String(value || '')
},
{ immediate: true }
)
watch(
categoryRows,
(rows) => {
rows.forEach((item) => {
categoryBudgetDrafts[item.id] = String(item.budget || '')
})
},
{ immediate: true }
)
function saveTotalBudget() {
store.setBudgetTotal(totalBudgetInput.value)
uni.showToast({ title: '总预算已保存', icon: 'none' })
}
function saveCategoryBudget(categoryId) {
store.setCategoryBudget(categoryId, categoryBudgetDrafts[categoryId])
uni.showToast({ title: '分类预算已保存', icon: 'none' })
}
</script>
<style lang="scss" scoped>
.budget-hero,
.category-card {
padding: 24rpx;
border-radius: 24rpx;
}
.budget-hero,
.category-head,
.category-title-row,
.budget-foot,
.editor-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.secondary-foot {
margin-top: 10rpx;
}
.budget-number {
display: block;
margin-top: 10rpx;
font-size: 34rpx;
font-weight: 700;
color: var(--text-primary);
}
.progress-track {
overflow: hidden;
height: 18rpx;
margin: 24rpx 0 16rpx;
border-radius: 999rpx;
background: var(--surface-muted);
}
.thin-track {
height: 14rpx;
margin: 18rpx 0 14rpx;
}
.progress-fill {
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #5f8df5 0%, #1f6f5f 100%);
}
.editor-row {
margin-top: 20rpx;
}
.editor-row .input-shell {
flex: 1;
}
.save-btn {
flex: 0 0 180rpx;
}
.alert-list,
.category-list {
display: flex;
flex-direction: column;
gap: 18rpx;
}
.alert-item {
padding: 20rpx 22rpx;
border-radius: 22rpx;
background: var(--danger-soft);
}
.alert-title {
font-size: 28rpx;
font-weight: 600;
color: var(--danger);
}
.alert-text {
display: block;
margin-top: 8rpx;
font-size: 22rpx;
color: var(--danger);
}
.category-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
}
.category-title {
font-size: 28rpx;
font-weight: 600;
color: var(--text-primary);
}
</style>
+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>
+52
View File
@@ -0,0 +1,52 @@
<template>
<view class="content">
<image class="logo" src="/static/logo.png"></image>
<view class="text-area">
<text class="title">{{title}}</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
title: 'Hello'
}
},
onLoad() {
},
methods: {
}
}
</script>
<style>
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.logo {
height: 200rpx;
width: 200rpx;
margin-top: 200rpx;
margin-left: auto;
margin-right: auto;
margin-bottom: 50rpx;
}
.text-area {
display: flex;
justify-content: center;
}
.title {
font-size: 36rpx;
color: #8f8f94;
}
</style>
+177
View File
@@ -0,0 +1,177 @@
<template>
<view class="app-page" :class="themeClass">
<view class="surface-card page-hero">
<text class="hero-kicker">ABOUT</text>
<text class="hero-title">关于与隐私</text>
<text class="hero-desc">查看应用定位数据说明与发布前应补齐的正式信息</text>
<view class="hero-tags">
<!-- <text class="hero-tag">版本 1.0.0</text> -->
<text class="hero-tag soft">发布说明</text>
</view>
</view>
<ad-custom unit-id="adunit-64707ea333329399"></ad-custom>
<section-card title="关于应用" subtitle="面向日常收支记录、预算控制与月度复盘的轻量工具">
<view class="about-card surface-strong">
<text class="app-name">账单小管家</text>
<!-- <text class="app-version">版本 1.0.0</text> -->
<text class="about-text">定位为轻量无广告的本地记账工具适合学生情侣合租和个人日常记账场景</text>
</view>
</section-card>
<section-card title="隐私与数据" subtitle="">
<view class="info-list">
<view class="info-item surface-strong">
<text class="info-title">本地存储</text>
<text class="info-text">账单预算分类账户和设置默认保存在当前设备本地</text>
</view>
<view class="info-item surface-strong">
<text class="info-title">本地昵称</text>
<text class="info-text">昵称仅保存在当前设备用于个人页展示和首字头像</text>
</view>
<view class="info-item surface-strong">
<text class="info-title">数据迁移</text>
<text class="info-text">如需换机迁移可在备份与恢复中导出 JSON 并在新设备恢复</text>
</view>
</view>
</section-card>
</view>
</template>
<script setup>
import { computed } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import SectionCard from '../../../components/SectionCard.vue'
import { useAppStore } from '../../../utils/store'
const store = useAppStore()
onShow(()=>{
showIt()
})
function showIt(){
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)
})
}
}, 2280)
}
const themeClass = computed(() => (store.state.settings.theme === 'dark' ? 'theme-dark' : ''))
</script>
<style lang="scss" scoped>
.page-hero {
padding: 30rpx;
background: linear-gradient(145deg, rgba(16, 42, 67, 0.96) 0%, rgba(31, 111, 95, 0.88) 60%, rgba(212, 108, 67, 0.82) 100%);
color: #ffffff;
}
.hero-kicker,
.hero-desc,
.hero-tag.soft {
color: rgba(255, 255, 255, 0.76);
}
.hero-kicker {
font-size: 20rpx;
letter-spacing: 4rpx;
}
.hero-title {
display: block;
margin-top: 12rpx;
font-size: 44rpx;
font-weight: 700;
}
.hero-desc {
display: block;
margin-top: 14rpx;
font-size: 24rpx;
line-height: 1.7;
}
.hero-tags {
display: flex;
flex-wrap: wrap;
gap: 14rpx;
margin-top: 22rpx;
}
.hero-tag {
padding: 12rpx 18rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.16);
font-size: 22rpx;
}
.about-card,
.info-item,
.tips-card {
padding: 26rpx;
border-radius: 28rpx;
}
.app-name,
.info-title {
display: block;
font-size: 31rpx;
font-weight: 700;
color: var(--text-primary);
}
.app-version {
display: block;
margin-top: 10rpx;
font-size: 23rpx;
color: var(--brand);
}
.about-text,
.info-text,
.tip-line {
display: block;
margin-top: 12rpx;
font-size: 24rpx;
line-height: 1.8;
color: var(--text-secondary);
}
.info-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.tip-row {
display: flex;
align-items: flex-start;
gap: 16rpx;
padding: 10rpx 0;
}
.tip-index {
width: 56rpx;
font-size: 24rpx;
font-weight: 700;
color: var(--brand);
}
</style>
+277
View File
@@ -0,0 +1,277 @@
<template>
<view class="app-page" :class="themeClass">
<view class="surface-card page-hero">
<text class="hero-kicker">BACKUP</text>
<text class="hero-title">备份与恢复</text>
<text class="hero-desc">导出本地 JSON 备份恢复时覆盖当前设备数据适合换机或手动留档</text>
<view class="hero-tags">
<text class="hero-tag">本地文件</text>
<text class="hero-tag soft">{{ store.state.settings.lastBackupAt ? '最近已备份' : '尚未备份' }}</text>
</view>
</view>
<ad-custom unit-id="adunit-64707ea333329399"></ad-custom>
<section-card title="导出备份" subtitle="将账单、预算和设置导出为 JSON 文件,便于迁移设备或手动保存">
<view class="hero-panel surface-strong">
<view>
<text class="panel-title">安全备份当前数据</text>
<text class="panel-desc">所有数据均为本地文件不会自动上传服务器</text>
</view>
<view class="status-chip">{{ store.state.settings.lastBackupAt ? '可继续备份' : '建议先备份' }}</view>
</view>
<view class="action-row single-row">
<view class="primary-button" @click="exportBackupFile">导出备份</view>
</view>
<text class="tiny-text info-line" v-if="store.state.settings.lastBackupAt">最近备份{{ store.state.settings.lastBackupAt }}</text>
</section-card>
<section-card title="恢复备份" subtitle="粘贴之前导出的 JSON 内容,恢复后将覆盖当前设备数据">
<view class="input-shell textarea-shell">
<textarea v-model="importText" placeholder="请粘贴备份 JSON"></textarea>
</view>
<view class="action-row">
<view class="ghost-button" @click="importText = ''">清空内容</view>
<view class="primary-button" @click="restoreBackup">开始恢复</view>
</view>
</section-card>
<section-card title="数据操作" subtitle="谨慎执行不可撤销的本地清理操作">
<view class="menu-list">
<view class="menu-item surface-strong" @click="copyBackupText">
<view>
<text class="menu-title">复制备份内容</text>
</view>
<text class="status-chip">复制</text>
</view>
<view class="menu-item surface-strong danger-shell" @click="clearCache">
<view>
<text class="menu-title negative">清空全部数据</text>
</view>
<text class="danger-text">执行</text>
</view>
</view>
</section-card>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import SectionCard from '../../../components/SectionCard.vue'
import { useAppStore } from '../../../utils/store'
import { onShow } from '@dcloudio/uni-app'
const store = useAppStore()
const importText = ref('')
const themeClass = computed(() => (store.state.settings.theme === 'dark' ? 'theme-dark' : ''))
function writeBackupFile(content) {
const timeLabel = new Date().toLocaleString()
if (typeof wx !== 'undefined' && wx.getFileSystemManager) {
const filePath = `${wx.env.USER_DATA_PATH}/bill-helper-backup.json`
wx.getFileSystemManager().writeFile({
filePath,
data: content,
encoding: 'utf8',
success: () => {
store.markBackup(timeLabel)
uni.showModal({ title: '备份成功', content: `文件已生成:${filePath}`, showCancel: false })
},
fail: () => {
uni.setClipboardData({ data: content })
}
})
return
}
store.markBackup(timeLabel)
uni.setClipboardData({ data: content })
}
function exportBackupFile() {
writeBackupFile(store.exportBackup())
}
function restoreBackup() {
if (!importText.value.trim()) {
uni.showToast({ title: '请先粘贴备份内容', icon: 'none' })
return
}
try {
store.importBackup(importText.value)
importText.value = ''
uni.showToast({ title: '备份恢复成功', icon: 'none' })
} catch (error) {
uni.showToast({ title: '备份内容无效', icon: 'none' })
}
}
function copyBackupText() {
uni.setClipboardData({ data: store.exportBackup() })
}
function clearCache() {
uni.showModal({
title: '清空全部数据',
content: '确认删除当前设备中的账单、预算和设置吗?此操作不可撤销。',
success: ({ confirm }) => {
if (confirm) {
store.resetAll()
uni.showToast({ title: '本地数据已清空', icon: 'none' })
}
}
})
}
onShow(()=>{
showIt()
})
function showIt(){
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)
})
}
}, 2280)
}
</script>
<style lang="scss" scoped>
.page-hero {
padding: 30rpx;
background: linear-gradient(145deg, rgba(16, 42, 67, 0.96) 0%, rgba(61, 102, 178, 0.92) 100%);
color: #ffffff;
}
.hero-kicker,
.hero-desc,
.hero-tag.soft {
color: rgba(255, 255, 255, 0.76);
}
.hero-kicker {
font-size: 20rpx;
letter-spacing: 4rpx;
}
.hero-title {
display: block;
margin-top: 12rpx;
font-size: 44rpx;
font-weight: 700;
}
.hero-desc {
display: block;
margin-top: 14rpx;
font-size: 24rpx;
line-height: 1.7;
}
.hero-tags {
display: flex;
flex-wrap: wrap;
gap: 14rpx;
margin-top: 22rpx;
}
.hero-tag,
.status-chip {
padding: 12rpx 18rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.16);
font-size: 22rpx;
}
.hero-panel,
.menu-item,
.action-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.hero-panel,
.menu-item {
padding: 26rpx;
border-radius: 28rpx;
}
.hero-panel {
justify-content: space-between;
}
.panel-title,
.menu-title {
display: block;
font-size: 31rpx;
font-weight: 700;
color: var(--text-primary);
}
.panel-desc,
.status-chip {
background: var(--brand-soft);
color: var(--brand);
}
.action-row {
margin-top: 18rpx;
}
.single-row .primary-button,
.action-row .ghost-button,
.action-row .primary-button {
flex: 1;
}
.info-line {
display: block;
margin-top: 16rpx;
}
.textarea-shell {
align-items: flex-start;
min-height: 280rpx;
padding: 20rpx 24rpx;
}
.textarea-shell textarea {
min-height: 240rpx;
}
.menu-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.menu-item {
justify-content: space-between;
}
.danger-shell {
border: 1rpx solid rgba(210, 85, 67, 0.12);
}
.danger-text {
font-size: 24rpx;
color: var(--danger);
}
</style>
+217
View File
@@ -0,0 +1,217 @@
<template>
<view class="app-page" :class="themeClass">
<view class="surface-card page-hero">
<text class="hero-kicker">GUIDE</text>
<text class="hero-title">使用帮助</text>
<text class="hero-desc">通过上手步骤常见问题和快捷入口快速熟悉整个记账流程</text>
<view class="hero-tags">
<text class="hero-tag">4 个步骤</text>
<text class="hero-tag soft">FAQ 指南</text>
</view>
</view>
<ad-custom unit-id="adunit-64707ea333329399"></ad-custom>
<section-card title="快速上手" subtitle="初次使用建议先完成下面 4 个动作">
<view class="step-list">
<view v-for="item in quickSteps" :key="item.title" class="step-item surface-strong">
<text class="step-index">{{ item.index }}</text>
<view class="step-body">
<text class="step-title">{{ item.title }}</text>
<text class="step-desc">{{ item.desc }}</text>
</view>
</view>
</view>
</section-card>
<section-card title="常见问题" subtitle="">
<view class="faq-list">
<view v-for="item in faqList" :key="item.q" class="faq-item surface-strong">
<text class="faq-question">{{ item.q }}</text>
<text class="faq-answer">{{ item.a }}</text>
</view>
</view>
</section-card>
<section-card title="功能入口" subtitle="需要操作时可直接跳转到对应模块">
<view class="entry-grid">
<view class="entry-item surface-strong" @click="go('/pages/home/index')">
<text class="entry-title">首页记账</text>
</view>
<view class="entry-item surface-strong" @click="go('/pages/budget/index')">
<text class="entry-title">预算设置</text>
</view>
<view class="entry-item surface-strong" @click="go('/pages/stats/index')">
<text class="entry-title">查看报表</text>
</view>
<view class="entry-item surface-strong" @click="go('/pages/mine/backup/index')">
<text class="entry-title">备份恢复</text>
</view>
</view>
</section-card>
</view>
</template>
<script setup>
import { computed } from 'vue'
import SectionCard from '../../../components/SectionCard.vue'
import { useAppStore } from '../../../utils/store'
import { onShow } from '@dcloudio/uni-app'
const store = useAppStore()
const themeClass = computed(() => (store.state.settings.theme === 'dark' ? 'theme-dark' : ''))
const quickSteps = [
{ index: '01', title: '先设置月预算', desc: '进入预算页设置总预算与分类预算,首页会同步显示剩余额度。' },
{ index: '02', title: '用首页快捷记账', desc: '首页支持支出/收入快速录入,也能通过常用分类一键记账。' },
{ index: '03', title: '到账单页做筛选', desc: '账单页支持按月份、账户、金额区间和关键词精确筛选。' },
{ index: '04', title: '每月查看报表', desc: '报表页可导出 CSV,并查看支出结构、近 7 日趋势和月度对比。' }
]
const faqList = [
{ q: '账单数据保存在哪里?', a: '默认仅保存在当前设备的本地存储中,不会自动上传云端。' },
{ q: '换手机后如何迁移?', a: '先进入“备份与恢复”导出 JSON 备份,再在新设备粘贴恢复。' },
{ q: '为什么预算进度显示超出 100%', a: '这代表本月支出已经超过预算,条形进度会封顶,但文字会继续显示真实比例。' },
{ q: '昵称可以怎么修改?', a: '进入“我的-账户资料”后可直接修改本地昵称,留空时默认显示为“用户”。' }
]
function go(url) {
uni.navigateTo({ url })
}
onShow(()=>{
showIt()
})
function showIt(){
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)
})
}
}, 2280)
}
</script>
<style lang="scss" scoped>
.page-hero {
padding: 30rpx;
background: linear-gradient(145deg, rgba(16, 42, 67, 0.96) 0%, rgba(127, 86, 217, 0.9) 100%);
color: #ffffff;
}
.hero-kicker,
.hero-desc,
.hero-tag.soft {
color: rgba(255, 255, 255, 0.76);
}
.hero-kicker {
font-size: 20rpx;
letter-spacing: 4rpx;
}
.hero-title {
display: block;
margin-top: 12rpx;
font-size: 44rpx;
font-weight: 700;
}
.hero-desc {
display: block;
margin-top: 14rpx;
font-size: 24rpx;
line-height: 1.7;
}
.hero-tags {
display: flex;
flex-wrap: wrap;
gap: 14rpx;
margin-top: 22rpx;
}
.hero-tag {
padding: 12rpx 18rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.16);
font-size: 22rpx;
}
.step-list,
.faq-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.step-item,
.faq-item {
padding: 24rpx;
border-radius: 28rpx;
}
.step-item {
display: flex;
gap: 18rpx;
}
.step-index {
width: 74rpx;
font-size: 30rpx;
font-weight: 700;
color: var(--brand);
}
.step-body {
flex: 1;
}
.step-title,
.faq-question,
.entry-title {
display: block;
font-size: 29rpx;
font-weight: 700;
color: var(--text-primary);
}
.step-desc,
.faq-answer {
display: block;
margin-top: 10rpx;
font-size: 24rpx;
line-height: 1.75;
color: var(--text-secondary);
}
.entry-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16rpx;
}
.entry-item {
display: flex;
align-items: center;
justify-content: center;
padding: 28rpx 20rpx;
border-radius: 28rpx;
text-align: center;
}
</style>
+136
View File
@@ -0,0 +1,136 @@
<template>
<view class="app-page" :class="themeClass">
<section-card title="账户概览" subtitle="集中管理本地昵称与主题设置">
<view class="profile-card surface-strong" @click="go('/pages/mine/profile/index')">
<view class="avatar-shell">{{ avatarText }}</view>
<view class="profile-body">
<text class="profile-name">{{ profileName }}</text>
<text class="section-subtitle"></text>
</view>
<text class="arrow-text">{{right}}</text>
</view>
<view class="theme-row">
<view class="pill-button" :class="{ active: store.state.settings.theme === 'light' }" @click.stop="setTheme('light')">浅色</view>
<view class="pill-button" :class="{ active: store.state.settings.theme === 'dark' }" @click.stop="setTheme('dark')">深色</view>
</view>
</section-card>
<ad-custom unit-id="adunit-74730c6c27c95a37"></ad-custom>
<section-card title="数据管理" subtitle="备份、恢复和清理等高频操作统一收口">
<view class="menu-list">
<view class="menu-item" @click="go('/pages/mine/backup/index')">
<view>
<text class="menu-title">备份与恢复</text>
</view>
<text class="arrow-text">{{right}}</text>
</view>
<view class="menu-item" @click="go('/pages/mine/guide/index')">
<view>
<text class="menu-title">使用帮助</text>
</view>
<text class="arrow-text">{{right}}</text>
</view>
<view class="menu-item" @click="go('/pages/mine/about/index')">
<view>
<text class="menu-title">关于与隐私</text>
</view>
<text class="arrow-text">{{right}}</text>
</view>
</view>
</section-card>
<app-tab-bar current="mine" />
</view>
</template>
<script setup>
import { computed,ref } from 'vue'
import SectionCard from '../../components/SectionCard.vue'
import AppTabBar from '../../components/AppTabBar.vue'
import { useAppStore } from '../../utils/store'
const store = useAppStore()
const themeClass = computed(() => (store.state.settings.theme === 'dark' ? 'theme-dark' : ''))
const profileName = computed(() => store.state.settings.profile.nickname || '用户')
const avatarText = computed(() => (store.state.settings.profile.nickname || '用户').slice(0, 1))
const right = ref(">")
function setTheme(theme) {
store.setTheme(theme)
}
function go(url) {
uni.navigateTo({ url })
}
</script>
<style lang="scss" scoped>
.profile-card,
.theme-row,
.menu-item {
display: flex;
align-items: center;
gap: 16rpx;
}
.profile-card {
padding: 24rpx;
border-radius: 24rpx;
}
.avatar-shell {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
background: var(--bg-accent);
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-size: 34rpx;
font-weight: 700;
}
.profile-body {
flex: 1;
}
.profile-name,
.menu-title {
font-size: 30rpx;
font-weight: 600;
color: var(--text-primary);
}
.arrow-text {
font-size: 32rpx;
// color: var(--brand);
}
.theme-row {
margin-top: 18rpx;
}
.theme-row .pill-button {
flex: 1;
}
.menu-list,
.action-grid {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.menu-item {
justify-content: space-between;
padding: 22rpx;
border-radius: 24rpx;
background: var(--surface-muted);
}
</style>
+255
View File
@@ -0,0 +1,255 @@
<template>
<view class="app-page" :class="themeClass">
<view class="surface-card page-hero">
<text class="hero-kicker">ACCOUNT</text>
<text class="hero-title">账户资料</text>
<text class="hero-desc">管理本地昵称显示资料与本机记账模式说明</text>
<view class="hero-tags">
<text class="hero-tag">本地资料</text>
<text class="hero-tag soft">本地存储</text>
</view>
</view>
<section-card title="昵称设置" subtitle="修改后仅用于个人页展示和首字头像,不参与账单计算">
<view class="profile-card surface-strong">
<view class="avatar-shell">{{ avatarText }}</view>
<view class="profile-body">
<text class="profile-name">{{ profileName }}</text>
<text class="profile-meta">当前昵称仅保存在本地设备可随时修改</text>
</view>
<view class="status-badge">本地</view>
</view>
<view class="editor-block">
<view class="input-shell">
<input v-model="nicknameInput" maxlength="12" placeholder="请输入昵称" />
</view>
<text class="tiny-text editor-tip">留空时页面会统一显示用户</text>
</view>
<view class="action-row">
<view class="ghost-button" @click="clearNickname">清空昵称</view>
<view class="primary-button" @click="saveNickname">保存昵称</view>
</view>
</section-card>
<section-card title="显示与模式" subtitle="集中展示当前账户页的生效状态">
<view class="info-list">
<view class="info-item surface-strong">
<view>
<text class="info-title">昵称首字头像</text>
<text class="info-desc">当前显示 {{ avatarText }}自动根据昵称生成</text>
</view>
<text class="info-mark">已启用</text>
</view>
<view class="info-item surface-strong">
<view>
<text class="info-title">本机记账模式</text>
<text class="info-desc">账单与预算默认仅保存在当前设备本地</text>
</view>
<text class="info-mark">默认</text>
</view>
</view>
</section-card>
<section-card title="使用提示" subtitle="帮助用户理解昵称显示与数据边界">
<view class="tips-card surface-strong">
<view v-for="(tip, index) in tips" :key="tip" class="tip-row">
<text class="tip-index">0{{ index + 1 }}</text>
<text class="tip-line">{{ tip }}</text>
</view>
</view>
</section-card>
</view>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import SectionCard from '../../../components/SectionCard.vue'
import { useAppStore } from '../../../utils/store'
const store = useAppStore()
const themeClass = computed(() => (store.state.settings.theme === 'dark' ? 'theme-dark' : ''))
const profileName = computed(() => store.state.settings.profile.nickname || '用户')
const avatarText = computed(() => profileName.value.slice(0, 1))
const nicknameInput = ref(store.state.settings.profile.nickname || '')
const tips = [
'昵称仅用于个人页展示和首字头像,不参与账单计算。',
'账单、预算和设置默认不会自动上传云端。',
'如需更换设备,请先在“备份与恢复”页面导出 JSON 备份。'
]
watch(
() => store.state.settings.profile.nickname,
(value) => {
nicknameInput.value = value || ''
}
)
function saveNickname() {
const nextName = nicknameInput.value.trim()
store.setProfile({
authorized: false,
nickname: nextName,
avatarUrl: ''
})
uni.showToast({ title: '昵称已保存', icon: 'none' })
}
function clearNickname() {
nicknameInput.value = ''
store.setProfile({
authorized: false,
nickname: '',
avatarUrl: ''
})
uni.showToast({ title: '昵称已清空', icon: 'none' })
}
</script>
<style lang="scss" scoped>
.page-hero {
padding: 30rpx;
background: linear-gradient(145deg, rgba(16, 42, 67, 0.96) 0%, rgba(31, 111, 95, 0.92) 100%);
color: #ffffff;
}
.hero-kicker,
.hero-desc,
.hero-tag.soft {
color: rgba(255, 255, 255, 0.76);
}
.hero-kicker {
font-size: 20rpx;
letter-spacing: 4rpx;
}
.hero-title {
display: block;
margin-top: 12rpx;
font-size: 44rpx;
font-weight: 700;
}
.hero-desc {
display: block;
margin-top: 14rpx;
font-size: 24rpx;
line-height: 1.7;
}
.hero-tags {
display: flex;
flex-wrap: wrap;
gap: 14rpx;
margin-top: 22rpx;
}
.hero-tag {
padding: 12rpx 18rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.16);
font-size: 22rpx;
}
.profile-card,
.info-item,
.action-row,
.tip-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.profile-card,
.tips-card {
padding: 26rpx;
border-radius: 28rpx;
}
.avatar-shell {
width: 108rpx;
height: 108rpx;
border-radius: 32rpx;
background: var(--bg-accent);
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
font-weight: 700;
box-shadow: 0 18rpx 32rpx rgba(16, 42, 67, 0.16);
}
.profile-body {
flex: 1;
}
.profile-name,
.info-title {
display: block;
font-size: 31rpx;
font-weight: 700;
color: var(--text-primary);
}
.profile-meta,
.info-desc,
.tip-line {
display: block;
margin-top: 10rpx;
font-size: 24rpx;
line-height: 1.7;
color: var(--text-secondary);
}
.status-badge,
.info-mark {
padding: 10rpx 18rpx;
border-radius: 999rpx;
background: var(--brand-soft);
font-size: 22rpx;
color: var(--brand);
}
.editor-block {
margin-top: 18rpx;
}
.editor-tip {
display: block;
margin-top: 12rpx;
}
.action-row {
margin-top: 18rpx;
}
.action-row .ghost-button,
.action-row .primary-button {
flex: 1;
}
.info-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.info-item {
justify-content: space-between;
padding: 24rpx;
border-radius: 26rpx;
}
.tip-row {
align-items: flex-start;
padding: 12rpx 0;
}
.tip-index {
width: 56rpx;
font-size: 24rpx;
font-weight: 700;
color: var(--brand);
}
</style>
+445
View File
@@ -0,0 +1,445 @@
<template>
<view class="app-page" :class="themeClass">
<section-card title="报表总览" subtitle="按月份查看收支分布、消费趋势和月度对比">
<template #action>
<picker mode="date" fields="month" :value="monthPickerValue" @change="onMonthChange">
<text class="section-link">{{ selectedMonthLabel }}</text>
</picker>
</template>
<view class="summary-row">
<view class="summary-item surface-strong">
<text class="tiny-text">支出</text>
<text class="summary-value negative">{{ formatCurrency(monthExpenseTotal) }}</text>
</view>
<view class="summary-item surface-strong">
<text class="tiny-text">收入</text>
<text class="summary-value positive">{{ formatCurrency(monthIncomeTotal) }}</text>
</view>
<view class="summary-item surface-strong">
<text class="tiny-text">结余</text>
<text class="summary-value">{{ formatCurrency(monthIncomeTotal - monthExpenseTotal) }}</text>
</view>
</view>
</section-card>
<ad-custom unit-id="adunit-74730c6c27c95a37"></ad-custom>
<section-card title="支出分类" subtitle="查看本月主要消费去向与占比结构">
<view v-if="categoryStats.length" class="chart-list">
<view v-for="item in categoryStats" :key="item.id" class="chart-row">
<view class="chart-head">
<view class="chart-title-row">
<view class="chart-dot" :style="{ background: item.color }"></view>
<text class="chart-title">{{ item.name }}</text>
</view>
<text class="tiny-text">{{ formatCurrency(item.total) }} · {{ item.percentLabel }}</text>
</view>
<view class="bar-track"><view class="bar-fill" :style="{ width: item.percentWidth, background: item.color }"></view></view>
</view>
</view>
<view v-else class="empty-card"><text class="section-subtitle">当前月份暂无支出数据记一笔后会自动生成图表</text></view>
</section-card>
<section-card title="近 7 日趋势" subtitle="观察近一周消费变化,便于发现异常高峰">
<view class="column-chart">
<view v-for="item in dailySeries" :key="item.date" class="column-item">
<view class="column-track"><view class="column-fill" :style="{ height: item.height }"></view></view>
<text class="tiny-text">{{ item.label }}</text>
<text class="tiny-text">{{ item.value === 0 ? '-' : item.value }}</text>
</view>
</view>
</section-card>
<section-card title="月度对比" subtitle="最近 6 个月收入与支出走势一目了然">
<view class="compare-list">
<view v-for="item in monthCompare" :key="item.month" class="compare-row">
<text class="compare-label">{{ item.label }}</text>
<view class="compare-bars">
<view class="mini-track"><view class="mini-fill expense-fill" :style="{ width: item.expenseWidth }"></view></view>
<view class="mini-track"><view class="mini-fill income-fill" :style="{ width: item.incomeWidth }"></view></view>
</view>
<text class="tiny-text">{{ formatCurrency(item.expense) }} / {{ formatCurrency(item.income) }}</text>
</view>
</view>
</section-card>
<section-card title="导出与分享" subtitle="支持导出当月 CSV 账单与生成分享文案">
<view class="action-grid">
<view class="primary-button" @click="exportCsv">导出 CSV</view>
<view class="ghost-button" @click="posterVisible = true">分享摘要</view>
</view>
</section-card>
<view v-if="posterVisible" class="poster-shell" @touchmove.stop.prevent="">
<view class="poster-mask" @click="posterVisible = false"></view>
<view class="surface-card poster-panel">
<view class="poster-card">
<text class="poster-month">{{ selectedMonthLabel }}</text>
<text class="poster-title">收支月报</text>
<text class="poster-line">支出 {{ formatCurrency(monthExpenseTotal) }}</text>
<text class="poster-line">收入 {{ formatCurrency(monthIncomeTotal) }}</text>
<text class="poster-line">结余 {{ formatCurrency(monthIncomeTotal - monthExpenseTotal) }}</text>
<text class="poster-tip">内容本地生成可复制摘要或直接截图分享</text>
</view>
<view class="action-grid">
<view class="ghost-button" @click="copyPosterText">复制摘要</view>
<view class="primary-button" @click="posterVisible = false">关闭</view>
</view>
</view>
</view>
<app-tab-bar current="stats" />
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { onShareAppMessage } from '@dcloudio/uni-app'
import SectionCard from '../../components/SectionCard.vue'
import AppTabBar from '../../components/AppTabBar.vue'
import { useAppStore } from '../../utils/store'
import { formatMonthLabel, getMonthSeries, getRecentDateKeys, isSameMonth, toDateKey, toMonthKey } from '../../utils/date'
import { clampPercent, formatCurrency, formatPercent } from '../../utils/money'
const store = useAppStore()
const monthValue = ref(toMonthKey())
const posterVisible = ref(false)
const themeClass = computed(() => (store.state.settings.theme === 'dark' ? 'theme-dark' : ''))
const selectedMonthKey = computed(() => monthValue.value.slice(0, 7))
const selectedMonthLabel = computed(() => formatMonthLabel(selectedMonthKey.value))
const monthPickerValue = computed(() => monthValue.value)
const monthBills = computed(() => store.state.bills.filter((item) => isSameMonth(item.date, selectedMonthKey.value)))
const monthExpenseBills = computed(() => monthBills.value.filter((item) => item.type === 'expense'))
const monthExpenseTotal = computed(() => monthExpenseBills.value.reduce((sum, item) => sum + Number(item.amount), 0))
const monthIncomeTotal = computed(() => monthBills.value.filter((item) => item.type === 'income').reduce((sum, item) => sum + Number(item.amount), 0))
const categoryStats = computed(() => {
const total = Math.max(monthExpenseTotal.value, 1)
return store.state.categories.expense
.map((category) => {
const categoryTotal = monthExpenseBills.value
.filter((item) => item.categoryId === category.id)
.reduce((sum, item) => sum + Number(item.amount), 0)
return {
...category,
total: categoryTotal,
percentLabel: formatPercent(categoryTotal / total),
percentWidth: clampPercent(categoryTotal / total)
}
})
.filter((item) => item.total > 0)
.sort((left, right) => right.total - left.total)
})
const dailySeries = computed(() => {
const [year, month] = selectedMonthKey.value.split('-').map(Number)
const today = toDateKey()
const monthEnd = new Date(year, month, 0)
const endDate = toMonthKey(today) === selectedMonthKey.value ? new Date() : monthEnd
const dateKeys = getRecentDateKeys(7, endDate)
const maxValue = Math.max(1, ...dateKeys.map((dateKey) => monthExpenseBills.value
.filter((item) => item.date === dateKey)
.reduce((sum, item) => sum + Number(item.amount), 0)))
return dateKeys.map((dateKey) => {
const value = monthExpenseBills.value
.filter((item) => item.date === dateKey)
.reduce((sum, item) => sum + Number(item.amount), 0)
return {
date: dateKey,
label: dateKey.slice(5),
value: Number(value.toFixed(2)),
height: `${Math.max(8, (value / maxValue) * 100)}%`
}
})
})
const monthCompare = computed(() => {
const series = getMonthSeries(6, selectedMonthKey.value)
const maxExpense = Math.max(1, ...series.map((month) => store.state.bills
.filter((item) => item.type === 'expense' && isSameMonth(item.date, month))
.reduce((sum, item) => sum + Number(item.amount), 0)))
const maxIncome = Math.max(1, ...series.map((month) => store.state.bills
.filter((item) => item.type === 'income' && isSameMonth(item.date, month))
.reduce((sum, item) => sum + Number(item.amount), 0)))
return series.map((month) => {
const expense = store.state.bills
.filter((item) => item.type === 'expense' && isSameMonth(item.date, month))
.reduce((sum, item) => sum + Number(item.amount), 0)
const income = store.state.bills
.filter((item) => item.type === 'income' && isSameMonth(item.date, month))
.reduce((sum, item) => sum + Number(item.amount), 0)
return {
month,
label: month.slice(5),
expense,
income,
expenseWidth: `${Math.max(8, (expense / maxExpense) * 100)}%`,
incomeWidth: `${Math.max(8, (income / maxIncome) * 100)}%`
}
})
})
function onMonthChange(event) {
monthValue.value = String(event.detail.value).slice(0, 7)
}
function categoryNameOf(bill) {
return (store.state.categories[bill.type] || []).find((item) => item.id === bill.categoryId)?.name || '未分类'
}
function accountNameOf(bill) {
return store.state.accounts.find((item) => item.id === bill.accountId)?.name || '未知账户'
}
function escapeCsvCell(value) {
const text = String(value ?? '')
if (!/[",\n]/.test(text)) {
return text
}
return `"${text.replace(/"/g, '""')}"`
}
function writeTextToFile(fileName, content, successText) {
if (typeof wx !== 'undefined' && wx.getFileSystemManager) {
const filePath = `${wx.env.USER_DATA_PATH}/${fileName}`
wx.getFileSystemManager().writeFile({
filePath,
data: content,
encoding: 'utf8',
success: () => {
uni.showModal({
title: successText,
content: `文件已生成:${filePath}`,
showCancel: false
})
},
fail: () => {
uni.setClipboardData({ data: content })
}
})
return
}
uni.setClipboardData({ data: content })
}
function exportCsv() {
const header = '\uFEFF日期,类型,分类,账户,金额,备注'
const rows = monthBills.value.map((bill) => [
escapeCsvCell(bill.date),
escapeCsvCell(bill.type === 'income' ? '收入' : '支出'),
escapeCsvCell(categoryNameOf(bill)),
escapeCsvCell(accountNameOf(bill)),
escapeCsvCell(Number(bill.amount).toFixed(2)),
escapeCsvCell(bill.note || '')
].join(','))
writeTextToFile(`账单-${selectedMonthKey.value}.csv`, [header, ...rows].join('\n'), 'CSV 导出成功')
}
function copyPosterText() {
const message = `${selectedMonthLabel.value},支出 ${formatCurrency(monthExpenseTotal.value)},收入 ${formatCurrency(monthIncomeTotal.value)},结余 ${formatCurrency(monthIncomeTotal.value - monthExpenseTotal.value)}`
uni.setClipboardData({ data: message })
}
onShareAppMessage(() => ({
title: `${selectedMonthLabel.value}收支摘要`,
path: '/pages/stats/index'
}))
</script>
<style lang="scss" scoped>
.summary-row,
.action-grid {
display: flex;
gap: 16rpx;
}
.summary-item {
flex: 1;
padding: 22rpx;
border-radius: 24rpx;
}
.summary-value {
display: block;
margin-top: 10rpx;
font-size: 30rpx;
font-weight: 700;
color: var(--text-primary);
}
.section-link {
font-size: 24rpx;
color: var(--brand);
}
.chart-list,
.compare-list {
display: flex;
flex-direction: column;
gap: 18rpx;
}
.chart-head,
.chart-title-row,
.compare-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.chart-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
}
.chart-title {
font-size: 28rpx;
font-weight: 600;
color: var(--text-primary);
}
.bar-track,
.mini-track,
.column-track {
overflow: hidden;
border-radius: 999rpx;
background: var(--surface-muted);
}
.bar-track {
height: 16rpx;
margin-top: 12rpx;
}
.bar-fill,
.mini-fill {
height: 100%;
border-radius: inherit;
}
.column-chart {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 12rpx;
align-items: end;
height: 260rpx;
}
.column-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 10rpx;
}
.column-track {
display: flex;
align-items: flex-end;
justify-content: center;
width: 100%;
height: 180rpx;
padding: 0 6rpx;
}
.column-fill {
width: 100%;
border-radius: 999rpx 999rpx 16rpx 16rpx;
background: linear-gradient(180deg, #5f8df5 0%, #1f6f5f 100%);
}
.compare-label {
width: 64rpx;
font-size: 24rpx;
color: var(--text-secondary);
}
.compare-bars {
flex: 1;
display: flex;
flex-direction: column;
gap: 10rpx;
}
.mini-track {
height: 12rpx;
}
.expense-fill {
background: #d36c43;
}
.income-fill {
background: #1f6f5f;
}
.action-grid {
margin-top: 8rpx;
}
.action-grid .primary-button,
.action-grid .ghost-button {
flex: 1;
}
.empty-card {
padding: 32rpx 0 10rpx;
text-align: center;
}
.poster-shell {
position: fixed;
inset: 0;
z-index: 50;
}
.poster-mask {
position: absolute;
inset: 0;
background: rgba(4, 12, 18, 0.42);
}
.poster-panel {
position: absolute;
left: 24rpx;
right: 24rpx;
top: 16vh;
padding: 28rpx;
}
.poster-card {
padding: 32rpx;
border-radius: 28rpx;
background: linear-gradient(145deg, #102a43 0%, #1f6f5f 100%);
color: #ffffff;
margin-bottom: 20rpx;
}
.poster-month,
.poster-tip {
color: rgba(255, 255, 255, 0.76);
}
.poster-title {
display: block;
margin: 12rpx 0 20rpx;
font-size: 42rpx;
font-weight: 700;
}
.poster-line {
display: block;
margin-bottom: 12rpx;
font-size: 28rpx;
}
.poster-tip {
display: block;
margin-top: 24rpx;
font-size: 22rpx;
line-height: 1.6;
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

+13
View File
@@ -0,0 +1,13 @@
uni.addInterceptor({
returnValue (res) {
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
return res;
}
return new Promise((resolve, reject) => {
res.then((res) => {
if (!res) return resolve(res)
return res[0] ? reject(res[0]) : resolve(res[1])
});
});
},
});
+76
View File
@@ -0,0 +1,76 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color:#333;//基本色
$uni-text-color-inverse:#fff;//反色
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable:#c0c0c0;
/* 背景颜色 */
$uni-bg-color:#ffffff;
$uni-bg-color-grey:#f8f8f8;
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
/* 边框颜色 */
$uni-border-color:#c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm:12px;
$uni-font-size-base:14px;
$uni-font-size-lg:16px;
/* 图片尺寸 */
$uni-img-size-sm:20px;
$uni-img-size-base:26px;
$uni-img-size-lg:40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */
$uni-color-title: #2C405A; // 文章标题颜色
$uni-font-size-title:20px;
$uni-color-subtitle: #555555; // 二级标题颜色
$uni-font-size-subtitle:26px;
$uni-color-paragraph: #3F536E; // 文章段落颜色
$uni-font-size-paragraph:15px;
+1
View File
@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./common/vendor.js");Math;const o={onLaunch(){console.log("Bill Helper Launch")}};function t(){return{app:e.createSSRApp(o)}}t().app.mount("#app"),exports.createApp=t;
+20
View File
@@ -0,0 +1,20 @@
{
"pages": [
"pages/home/index",
"pages/bills/index",
"pages/budget/index",
"pages/stats/index",
"pages/mine/index",
"pages/mine/profile/index",
"pages/mine/backup/index",
"pages/mine/guide/index",
"pages/mine/about/index"
],
"window": {
"navigationBarTextStyle": "black",
"navigationBarBackgroundColor": "#F4EDE3",
"backgroundColor": "#F4EDE3",
"backgroundTextStyle": "dark"
},
"usingComponents": {}
}
+2
View File
@@ -0,0 +1,2 @@
page{background:#f4ede3;color:#16202a;font-family:PingFang SC,HarmonyOS Sans SC,sans-serif;--bg-app: #f4ede3;--bg-accent: linear-gradient(135deg, #102a43 0%, #1f6f5f 100%);--surface-card: rgba(255, 255, 255, .9);--surface-strong: #ffffff;--surface-muted: rgba(255, 255, 255, .68);--text-primary: #16202a;--text-secondary: #617081;--text-muted: #91a0af;--line-soft: rgba(22, 32, 42, .08);--brand: #1f6f5f;--brand-soft: rgba(31, 111, 95, .14);--danger: #d25543;--danger-soft: rgba(210, 85, 67, .14);--warning: #c48a1f;--shadow-card: 0 18rpx 40rpx rgba(16, 42, 67, .08)}view,text,button,input,textarea,scroll-view{box-sizing:border-box}button{margin:0;padding:0;background:transparent;line-height:1;border:0}button:after{border:0}.app-page{display:flex;flex-direction:column;gap:24rpx;min-height:100vh;padding:28rpx 28rpx 188rpx;background:radial-gradient(circle at top right,rgba(31,111,95,.16),transparent 32%),var(--bg-app);color:var(--text-primary)}.theme-dark{--bg-app: #0f1720;--bg-accent: linear-gradient(135deg, #09111a 0%, #1d4e46 100%);--surface-card: rgba(19, 29, 40, .92);--surface-strong: #162331;--surface-muted: rgba(24, 35, 49, .76);--text-primary: #eef5fb;--text-secondary: #9db0c2;--text-muted: #7c90a3;--line-soft: rgba(255, 255, 255, .08);--brand: #64c6a9;--brand-soft: rgba(100, 198, 169, .14);--danger: #ff8c78;--danger-soft: rgba(255, 140, 120, .14);--warning: #f2c56d;--shadow-card: 0 18rpx 48rpx rgba(0, 0, 0, .28)}.surface-card{background:var(--surface-card);border:1rpx solid var(--line-soft);border-radius:30rpx;box-shadow:var(--shadow-card);-webkit-backdrop-filter:blur(12rpx);backdrop-filter:blur(12rpx)}.surface-strong{background:var(--surface-strong)}.section-title{font-size:34rpx;font-weight:600;color:var(--text-primary)}.section-subtitle{font-size:24rpx;color:var(--text-secondary)}.pill-button{display:inline-flex;align-items:center;justify-content:center;padding:18rpx 24rpx;border-radius:999rpx;background:var(--surface-muted);color:var(--text-secondary);font-size:24rpx}.pill-button.active{background:var(--brand);color:#fff}.primary-button{display:inline-flex;align-items:center;justify-content:center;padding:22rpx 28rpx;border-radius:24rpx;background:var(--bg-accent);color:#fff;font-size:28rpx;font-weight:600}.ghost-button{display:inline-flex;align-items:center;justify-content:center;padding:20rpx 26rpx;border-radius:22rpx;background:var(--brand-soft);color:var(--brand);font-size:26rpx;font-weight:600}.danger-button{background:var(--danger-soft);color:var(--danger)}.input-shell{display:flex;align-items:center;min-height:86rpx;padding:0 24rpx;border-radius:24rpx;background:var(--surface-muted);border:1rpx solid transparent}.input-shell input,.input-shell textarea{width:100%;font-size:28rpx;color:var(--text-primary)}.muted-text{color:var(--text-secondary)}.tiny-text{font-size:22rpx;color:var(--text-muted)}.positive{color:var(--brand)}.negative{color:var(--danger)}
page{--status-bar-height:25px;--top-window-height:0px;--window-top:0px;--window-bottom:0px;--window-left:0px;--window-right:0px;--window-magin:0px}[data-c-h="true"]{display: none !important;}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
"use strict";const e=require("../common/vendor.js"),t={__name:"AppTabBar",props:{current:{type:String,default:"home"}},setup(t){const a=t,o=[{id:"home",label:"首页",path:"/pages/home/index",color:"#1f6f5f"},{id:"bills",label:"账单",path:"/pages/bills/index",color:"#d36c43"},{id:"budget",label:"预算",path:"/pages/budget/index",color:"#5f8df5"},{id:"stats",label:"报表",path:"/pages/stats/index",color:"#7f56d9"},{id:"mine",label:"我的",path:"/pages/mine/index",color:"#44546a"}];return(r,d)=>({a:e.f(o,((o,r,d)=>({a:t.current===o.id?o.color:"var(--line-soft)",b:e.t(o.label),c:o.id,d:t.current===o.id?1:"",e:e.o((t=>function(t){t.id!==a.current&&e.index.redirectTo({url:t.path})}(o)),o.id)})))})}},a=e._export_sfc(t,[["__scopeId","data-v-c3788da6"]]);wx.createComponent(a);
@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}
@@ -0,0 +1 @@
<view class="surface-card tabbar data-v-c3788da6"><view wx:for="{{a}}" wx:for-item="item" wx:key="c" class="{{['tab-item', 'data-v-c3788da6', item.d && 'active']}}" bindtap="{{item.e}}"><view class="tab-dot data-v-c3788da6" style="{{'background:' + item.a}}"></view><text class="tab-label data-v-c3788da6">{{item.b}}</text></view></view>
@@ -0,0 +1 @@
.tabbar.data-v-c3788da6{position:fixed;left:24rpx;right:24rpx;bottom:24rpx;display:flex;align-items:center;justify-content:space-between;padding:18rpx 12rpx env(safe-area-inset-bottom);z-index:20}.tab-item.data-v-c3788da6{flex:1;display:flex;flex-direction:column;align-items:center;gap:10rpx;padding:14rpx 0;border-radius:22rpx}.tab-item.active.data-v-c3788da6{background:var(--brand-soft)}.tab-dot.data-v-c3788da6{width:18rpx;height:18rpx;border-radius:50%}.tab-label.data-v-c3788da6{font-size:22rpx;color:var(--text-secondary)}.tab-item.active .tab-label.data-v-c3788da6{color:var(--text-primary);font-weight:600}
@@ -0,0 +1 @@
"use strict";const e=require("../common/vendor.js"),t=require("../utils/date.js"),a={__name:"BillEditorPopup",props:{visible:{type:Boolean,default:!1},entry:{type:Object,default:null},categories:{type:Object,required:!0},accounts:{type:Array,required:!0},defaultType:{type:String,default:"expense"},initialCategoryId:{type:String,default:""}},emits:["close","save"],setup(a,{emit:o}){const d=a,i=o,c=[{label:"支出",value:"expense"},{label:"收入",value:"income"}],n=e.reactive({id:"",type:"expense",amount:"",categoryId:"",accountId:"",date:t.toDateKey(),note:"",createdAt:0}),l=e.computed((()=>d.categories[n.type]||[]));function u(e){n.date=e.detail.value}function r(){Number(n.amount)?n.categoryId&&n.accountId?(i("save",{id:n.id,type:n.type,amount:Number(n.amount),categoryId:n.categoryId,accountId:n.accountId,date:n.date,note:n.note.trim(),createdAt:n.createdAt}),i("close")):e.index.showToast({title:"请选择分类和账户",icon:"none"}):e.index.showToast({title:"请输入有效金额",icon:"none"})}return e.watch((()=>d.visible),(e=>{e&&function(){var e,a,o;const i=d.entry||{},c=i.type||d.defaultType||"expense",l=d.initialCategoryId&&(d.categories[c]||[]).some((e=>e.id===d.initialCategoryId))?d.initialCategoryId:(null==(a=null==(e=d.categories[c])?void 0:e[0])?void 0:a.id)||"";n.id=i.id||"",n.type=c,n.amount=i.amount?String(i.amount):"",n.categoryId=i.categoryId||l,n.accountId=i.accountId||(null==(o=d.accounts[0])?void 0:o.id)||"",n.date=i.date||t.toDateKey(),n.note=i.note||"",n.createdAt=i.createdAt||0}()}),{immediate:!0}),e.watch((()=>n.type),(e=>{const t=(d.categories[e]||[]).map((e=>e.id));t.includes(n.categoryId)||(n.categoryId=t[0]||"")})),(t,o)=>e.e({a:a.visible},a.visible?{b:e.o((e=>i("close"))),c:e.t(n.id?"编辑账单":"新增账单"),d:e.o((e=>i("close"))),e:e.f(c,((t,a,o)=>({a:e.t(t.label),b:t.value,c:n.type===t.value?1:"",d:e.o((e=>n.type=t.value),t.value)}))),f:n.amount,g:e.o((e=>n.amount=e.detail.value)),h:e.f(l.value,((t,a,o)=>({a:t.color,b:e.t(t.name),c:t.id,d:n.categoryId===t.id?1:"",e:e.o((e=>n.categoryId=t.id),t.id)}))),i:e.f(a.accounts,((t,a,o)=>({a:t.color,b:e.t(t.name),c:t.id,d:n.accountId===t.id?1:"",e:e.o((e=>n.accountId=t.id),t.id)}))),j:e.t(n.date),k:n.date,l:e.o(u),m:n.note,n:e.o((e=>n.note=e.detail.value)),o:e.o((e=>i("close"))),p:e.o(r),q:e.o((()=>{}))}:{})}},o=e._export_sfc(a,[["__scopeId","data-v-eeef8b0e"]]);wx.createComponent(o);
@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}
@@ -0,0 +1 @@
<view wx:if="{{a}}" class="popup-shell data-v-eeef8b0e" catchtouchmove="{{q}}"><view class="popup-mask data-v-eeef8b0e" bindtap="{{b}}"></view><view class="surface-card popup-panel data-v-eeef8b0e"><view class="popup-head data-v-eeef8b0e"><view class="data-v-eeef8b0e"><text class="section-title data-v-eeef8b0e">{{c}}</text><text class="section-subtitle data-v-eeef8b0e">3 步完成记录,所有数据仅保存在本机</text></view><text class="close-text data-v-eeef8b0e" bindtap="{{d}}">关闭</text></view><scroll-view scroll-y class="popup-body data-v-eeef8b0e"><view class="field-block data-v-eeef8b0e"><text class="field-label data-v-eeef8b0e">收支类型</text><view class="pill-row data-v-eeef8b0e"><view wx:for="{{e}}" wx:for-item="item" wx:key="b" class="{{['pill-button', 'data-v-eeef8b0e', item.c && 'active']}}" bindtap="{{item.d}}">{{item.a}}</view></view></view><view class="field-block data-v-eeef8b0e"><text class="field-label data-v-eeef8b0e">金额</text><view class="input-shell amount-shell data-v-eeef8b0e"><text class="prefix-text data-v-eeef8b0e">¥</text><input class="data-v-eeef8b0e" type="digit" placeholder="输入金额" value="{{f}}" bindinput="{{g}}"/></view></view><view class="field-block data-v-eeef8b0e"><text class="field-label data-v-eeef8b0e">分类</text><view class="chip-grid data-v-eeef8b0e"><view wx:for="{{h}}" wx:for-item="item" wx:key="c" class="{{['chip-item', 'data-v-eeef8b0e', item.d && 'active']}}" bindtap="{{item.e}}"><view class="chip-dot data-v-eeef8b0e" style="{{'background:' + item.a}}"></view><text class="data-v-eeef8b0e">{{item.b}}</text></view></view></view><view class="field-block data-v-eeef8b0e"><text class="field-label data-v-eeef8b0e">账户</text><view class="chip-grid data-v-eeef8b0e"><view wx:for="{{i}}" wx:for-item="item" wx:key="c" class="{{['chip-item', 'data-v-eeef8b0e', item.d && 'active']}}" bindtap="{{item.e}}"><view class="chip-dot data-v-eeef8b0e" style="{{'background:' + item.a}}"></view><text class="data-v-eeef8b0e">{{item.b}}</text></view></view></view><view class="field-block data-v-eeef8b0e"><text class="field-label data-v-eeef8b0e">日期</text><picker class="data-v-eeef8b0e" mode="date" value="{{k}}" bindchange="{{l}}"><view class="input-shell picker-shell data-v-eeef8b0e"><text class="data-v-eeef8b0e">{{j}}</text><text class="tiny-text data-v-eeef8b0e">选择</text></view></picker></view><view class="field-block data-v-eeef8b0e"><text class="field-label data-v-eeef8b0e">备注</text><view class="input-shell textarea-shell data-v-eeef8b0e"><block wx:if="{{r0}}"><textarea class="data-v-eeef8b0e" maxlength="40" placeholder="补充说明,便于后续搜索" value="{{m}}" bindinput="{{n}}"></textarea></block></view></view></scroll-view><view class="popup-foot data-v-eeef8b0e"><view class="ghost-button data-v-eeef8b0e" bindtap="{{o}}">取消</view><view class="primary-button save-button data-v-eeef8b0e" bindtap="{{p}}">保存账单</view></view></view></view>
@@ -0,0 +1 @@
.popup-shell.data-v-eeef8b0e{position:fixed;top:0;right:0;bottom:0;left:0;z-index:50}.popup-mask.data-v-eeef8b0e{position:absolute;top:0;right:0;bottom:0;left:0;background:rgba(3,12,21,.42)}.popup-panel.data-v-eeef8b0e{position:absolute;left:16rpx;right:16rpx;bottom:16rpx;max-height:84vh;padding:28rpx 28rpx 32rpx;display:flex;flex-direction:column;gap:24rpx}.popup-head.data-v-eeef8b0e{display:flex;align-items:flex-start;justify-content:space-between;gap:16rpx}.close-text.data-v-eeef8b0e{padding:10rpx 0;font-size:24rpx;color:var(--text-secondary)}.popup-body.data-v-eeef8b0e{max-height:58vh}.field-block.data-v-eeef8b0e{display:flex;flex-direction:column;gap:18rpx;margin-bottom:24rpx}.field-label.data-v-eeef8b0e{font-size:26rpx;font-weight:600;color:var(--text-primary)}.pill-row.data-v-eeef8b0e,.chip-grid.data-v-eeef8b0e{display:flex;flex-wrap:wrap;gap:16rpx}.chip-item.data-v-eeef8b0e{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.data-v-eeef8b0e{background:var(--brand-soft);color:var(--text-primary)}.chip-dot.data-v-eeef8b0e{width:14rpx;height:14rpx;border-radius:50%}.amount-shell.data-v-eeef8b0e{gap:12rpx}.prefix-text.data-v-eeef8b0e{font-size:36rpx;font-weight:600;color:var(--text-primary)}.picker-shell.data-v-eeef8b0e{justify-content:space-between}.textarea-shell.data-v-eeef8b0e{padding:20rpx 24rpx;min-height:160rpx;align-items:flex-start}.textarea-shell textarea.data-v-eeef8b0e{min-height:120rpx}.popup-foot.data-v-eeef8b0e{display:flex;align-items:center;gap:16rpx}.popup-foot .ghost-button.data-v-eeef8b0e{flex:0 0 180rpx}.save-button.data-v-eeef8b0e{flex:1}
@@ -0,0 +1 @@
"use strict";const t=require("../common/vendor.js"),e={__name:"SectionCard",props:{title:{type:String,default:""},subtitle:{type:String,default:""}},setup:e=>(i,s)=>t.e({a:e.title||e.subtitle||i.$slots.action},e.title||e.subtitle||i.$slots.action?t.e({b:e.title},e.title?{c:t.t(e.title)}:{},{d:e.subtitle},e.subtitle?{e:t.t(e.subtitle)}:{}):{})},i=t._export_sfc(e,[["__scopeId","data-v-55faa340"]]);wx.createComponent(i);
@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}
@@ -0,0 +1 @@
<view class="surface-card card data-v-55faa340"><view wx:if="{{a}}" class="head data-v-55faa340"><view class="title-group data-v-55faa340"><text wx:if="{{b}}" class="section-title data-v-55faa340">{{c}}</text><text wx:if="{{d}}" class="section-subtitle data-v-55faa340">{{e}}</text></view><slot name="action"></slot></view><slot></slot></view>
@@ -0,0 +1 @@
.card.data-v-55faa340{padding:28rpx}.head.data-v-55faa340{display:flex;align-items:flex-start;justify-content:space-between;gap:16rpx;margin-bottom:24rpx}.title-group.data-v-55faa340{display:flex;flex-direction:column;gap:10rpx}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
{
"navigationBarTitleText": "账单管理",
"enablePullDownRefresh": true,
"usingComponents": {
"section-card": "../../components/SectionCard",
"app-tab-bar": "../../components/AppTabBar",
"bill-editor-popup": "../../components/BillEditorPopup"
}
}
@@ -0,0 +1 @@
<view class="{{['app-page', 'data-v-ff523969', O]}}"><section-card wx:if="{{w}}" class="data-v-ff523969" u-s="{{['d']}}" u-i="ff523969-0" bind:__l="__l" u-p="{{w}}"><view class="chip-row data-v-ff523969"><view wx:for="{{a}}" wx:for-item="item" wx:key="b" class="{{['pill-button', 'data-v-ff523969', item.c && 'active']}}" bindtap="{{item.d}}">{{item.a}}</view></view><view class="chip-row compact-row data-v-ff523969"><view wx:for="{{b}}" wx:for-item="item" wx:key="b" class="{{['pill-button', 'mini-pill', 'data-v-ff523969', item.c && 'active']}}" bindtap="{{item.d}}">{{item.a}}</view><picker class="data-v-ff523969" mode="date" fields="month" value="{{d}}" bindchange="{{e}}"><view class="pill-button mini-pill data-v-ff523969">{{c}}</view></picker></view><view class="form-grid data-v-ff523969"><view class="input-shell data-v-ff523969"><input class="data-v-ff523969" placeholder="备注、分类或账户关键词" value="{{f}}" bindinput="{{g}}"/></view><view class="double-grid data-v-ff523969"><view class="input-shell data-v-ff523969"><input class="data-v-ff523969" type="digit" placeholder="最低金额" value="{{h}}" bindinput="{{i}}"/></view><view class="input-shell data-v-ff523969"><input class="data-v-ff523969" type="digit" placeholder="最高金额" value="{{j}}" bindinput="{{k}}"/></view></view></view><view class="picker-row data-v-ff523969"><picker class="data-v-ff523969" range="{{m}}" value="{{n}}" bindchange="{{o}}"><view class="input-shell picker-shell data-v-ff523969"><text class="data-v-ff523969">{{l}}</text><text class="tiny-text data-v-ff523969">分类</text></view></picker><picker class="data-v-ff523969" range="{{q}}" value="{{r}}" bindchange="{{s}}"><view class="input-shell picker-shell data-v-ff523969"><text class="data-v-ff523969">{{p}}</text><text class="tiny-text data-v-ff523969">账户</text></view></picker></view><view class="action-row data-v-ff523969"><view class="ghost-button data-v-ff523969" bindtap="{{t}}">清空筛选</view><view class="primary-button data-v-ff523969" bindtap="{{v}}">新增账单</view></view></section-card><section-card wx:if="{{F}}" class="data-v-ff523969" u-s="{{['action','d']}}" u-i="ff523969-1" bind:__l="__l" u-p="{{F}}"><text class="section-link data-v-ff523969" bindtap="{{y}}" slot="action">{{x}}</text><view class="summary-row data-v-ff523969"><view class="summary-item surface-strong data-v-ff523969"><text class="tiny-text data-v-ff523969">支出</text><text class="data-v-ff523969" style="margin-left:16rpx"></text><text class="summary-value negative data-v-ff523969">{{z}}</text></view><view class="summary-item surface-strong data-v-ff523969"><text class="tiny-text data-v-ff523969">收入</text><text class="data-v-ff523969" style="margin-left:16rpx"></text><text class="summary-value positive data-v-ff523969">{{A}}</text></view><view class="summary-item surface-strong data-v-ff523969"><text class="tiny-text data-v-ff523969">结余</text><text class="data-v-ff523969" style="margin-left:16rpx"></text><text class="summary-value data-v-ff523969">{{B}}</text></view></view><view wx:if="{{C}}" class="bill-list data-v-ff523969"><view wx:for="{{D}}" wx:for-item="bill" wx:key="j" class="bill-row data-v-ff523969" bindtap="{{bill.k}}" bindlongpress="{{bill.l}}"><view class="row-main data-v-ff523969"><view wx:if="{{E}}" class="{{['check-box', 'data-v-ff523969', bill.a && 'checked']}}"></view><view class="bill-dot data-v-ff523969" style="{{'background:' + bill.b}}"></view><view class="bill-body data-v-ff523969"><text class="bill-title data-v-ff523969">{{bill.c}}</text><text class="bill-meta data-v-ff523969">{{bill.d}} · {{bill.e}} · {{bill.f}}</text></view></view><text class="{{['bill-amount', 'data-v-ff523969', bill.i]}}">{{bill.g}}{{bill.h}}</text></view></view><view wx:else class="empty-card data-v-ff523969"><text class="section-subtitle data-v-ff523969">当前筛选条件下没有符合条件的账单记录。</text></view></section-card><view wx:if="{{G}}" class="surface-card batch-bar data-v-ff523969"><text class="batch-text data-v-ff523969">已选 {{H}} 笔</text><view class="ghost-button data-v-ff523969" bindtap="{{I}}">全选</view><view class="ghost-button danger-button data-v-ff523969" bindtap="{{J}}">删除所选</view></view><app-tab-bar wx:if="{{K}}" class="data-v-ff523969" u-i="ff523969-2" bind:__l="__l" u-p="{{K}}"/><bill-editor-popup wx:if="{{N}}" class="data-v-ff523969" bindclose="{{L}}" bindsave="{{M}}" u-i="ff523969-3" bind:__l="__l" u-p="{{N}}"/></view>
@@ -0,0 +1 @@
.chip-row.data-v-ff523969,.action-row.data-v-ff523969,.picker-row.data-v-ff523969,.summary-row.data-v-ff523969{display:flex;flex-wrap:wrap;gap:16rpx}.compact-row.data-v-ff523969{margin-top:16rpx}.mini-pill.data-v-ff523969{padding:16rpx 22rpx}.form-grid.data-v-ff523969{display:flex;flex-direction:column;gap:16rpx;margin:18rpx 0}.double-grid.data-v-ff523969,.picker-row.data-v-ff523969{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16rpx}.picker-shell.data-v-ff523969{justify-content:space-between}.action-row.data-v-ff523969{margin-top:18rpx}.action-row .ghost-button.data-v-ff523969,.action-row .primary-button.data-v-ff523969{flex:1}.section-link.data-v-ff523969{font-size:24rpx;color:var(--brand)}.summary-item.data-v-ff523969{flex:1;padding:22rpx;border-radius:24rpx}.summary-value.data-v-ff523969{margin-top:10rpx;font-size:30rpx;font-weight:700;color:var(--text-primary)}.bill-list.data-v-ff523969{display:flex;flex-direction:column;gap:16rpx;margin-top:22rpx}.bill-row.data-v-ff523969{display:flex;align-items:center;justify-content:space-between;gap:16rpx;padding:22rpx;border-radius:24rpx;background:var(--surface-muted)}.row-main.data-v-ff523969{display:flex;align-items:center;gap:16rpx;flex:1}.check-box.data-v-ff523969{width:30rpx;height:30rpx;border-radius:10rpx;border:2rpx solid var(--brand)}.check-box.checked.data-v-ff523969{background:var(--brand)}.bill-dot.data-v-ff523969{width:16rpx;height:16rpx;border-radius:50%}.bill-body.data-v-ff523969{flex:1}.bill-title.data-v-ff523969,.bill-amount.data-v-ff523969{font-size:28rpx;font-weight:600;color:var(--text-primary)}.bill-meta.data-v-ff523969{margin-top:8rpx;font-size:22rpx;color:var(--text-secondary);line-height:1.5}.empty-card.data-v-ff523969{padding:40rpx 0 10rpx;text-align:center}.batch-bar.data-v-ff523969{position:fixed;left:24rpx;right:24rpx;bottom:176rpx;display:flex;align-items:center;gap:16rpx;padding:20rpx 22rpx;z-index:18}.batch-text.data-v-ff523969{flex:1;font-size:26rpx;color:var(--text-primary)}
@@ -0,0 +1 @@
"use strict";const e=require("../../common/vendor.js"),t=require("../../utils/store.js"),a=require("../../utils/date.js"),u=require("../../utils/money.js");Math||(r+n)();const r=()=>"../../components/SectionCard.js",n=()=>"../../components/AppTabBar.js",o={__name:"index",setup(r){const n=t.useAppStore(),o=e.computed((()=>a.toMonthKey())),s=e.computed((()=>"dark"===n.state.settings.theme?"theme-dark":"")),l=e.ref(String(n.state.budgets.total||"")),d=e.reactive({}),c=e.computed((()=>n.state.bills.filter((e=>"expense"===e.type&&a.isSameMonth(e.date,o.value))))),i=e.computed((()=>c.value.reduce(((e,t)=>e+Number(t.amount)),0))),m=e.computed((()=>Number(n.state.budgets.total)||0)),p=e.computed((()=>m.value-i.value)),v=e.computed((()=>Math.max(p.value,0)/Math.max(1,a.getDaysLeftInMonth(o.value)))),g=e.computed((()=>m.value?u.formatPercent(i.value/Math.max(m.value,1)):"未设置")),f=e.computed((()=>u.clampPercent(i.value/Math.max(m.value||1,1)))),b=e.computed((()=>n.state.categories.expense.map((e=>{const t=c.value.filter((t=>t.categoryId===e.id)).reduce(((e,t)=>e+Number(t.amount)),0),a=Number(n.state.budgets.categoryBudgets[e.id]||0);return{...e,spent:t,budget:a,progressLabel:a?u.formatPercent(t/Math.max(a,1)):"未设置",progressWidth:u.clampPercent(t/Math.max(a||1,1))}})))),h=e.computed((()=>b.value.filter((e=>e.budget>0&&e.spent>e.budget))));function y(){n.setBudgetTotal(l.value),e.index.showToast({title:"总预算已保存",icon:"none"})}return e.watch((()=>n.state.budgets.total),(e=>{l.value=String(e||"")}),{immediate:!0}),e.watch(b,(e=>{e.forEach((e=>{d[e.id]=String(e.budget||"")}))}),{immediate:!0}),(t,a)=>e.e({a:e.t(e.unref(u.formatCurrency)(m.value)),b:e.t(e.unref(u.formatCurrency)(i.value)),c:f.value,d:e.t(g.value),e:e.t(p.value>=0?`剩余 ${e.unref(u.formatCurrency)(p.value)}`:`已超支 ${e.unref(u.formatCurrency)(Math.abs(p.value))}`),f:e.t(m.value?`日均可用 ${e.unref(u.formatCurrency)(v.value)}`:"设置预算后可查看剩余额度分配"),g:l.value,h:e.o((e=>l.value=e.detail.value)),i:e.o(y),j:e.p({title:"月度预算",subtitle:"设置总预算与分类预算,控制消费节奏"}),k:h.value.length},h.value.length?{l:e.f(h.value,((t,a,r)=>({a:e.t(t.name),b:e.t(e.unref(u.formatCurrency)(t.budget)),c:e.t(e.unref(u.formatCurrency)(t.spent)),d:t.id}))),m:e.p({title:"超支提醒",subtitle:"当前分类预算已被突破,建议尽快调整"})}:{},{n:e.f(b.value,((t,a,r)=>({a:t.color,b:e.t(t.name),c:e.t(e.unref(u.formatCurrency)(t.spent)),d:t.progressWidth,e:e.t(e.unref(u.formatCurrency)(t.budget)),f:e.t(t.progressLabel),g:e.n(t.budget>0&&t.spent>t.budget?"negative":""),h:d[t.id],i:e.o((e=>d[t.id]=e.detail.value),t.id),j:e.o((a=>{return u=t.id,n.setCategoryBudget(u,d[u]),void e.index.showToast({title:"分类预算已保存",icon:"none"});var u}),t.id),k:t.id}))),o:e.p({title:"分类预算",subtitle:"为高频分类分别设置预算,减少超支风险"}),p:e.p({current:"budget"}),q:e.n(s.value)})}},s=e._export_sfc(o,[["__scopeId","data-v-a56ca0c4"]]);wx.createPage(s);
@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "预算管理",
"usingComponents": {
"section-card": "../../components/SectionCard",
"app-tab-bar": "../../components/AppTabBar"
}
}
@@ -0,0 +1 @@
<view class="{{['app-page', 'data-v-a56ca0c4', q]}}"><section-card wx:if="{{j}}" class="data-v-a56ca0c4" u-s="{{['d']}}" u-i="a56ca0c4-0" bind:__l="__l" u-p="{{j}}"><view class="budget-hero surface-strong data-v-a56ca0c4"><view class="data-v-a56ca0c4"><text class="tiny-text data-v-a56ca0c4">本月总预算</text><text class="budget-number data-v-a56ca0c4">{{a}}</text></view><view class="data-v-a56ca0c4"><text class="tiny-text data-v-a56ca0c4">本月已支出</text><text class="budget-number negative data-v-a56ca0c4">{{b}}</text></view></view><view class="progress-track data-v-a56ca0c4"><view class="progress-fill data-v-a56ca0c4" style="{{'width:' + c}}"></view></view><view class="budget-foot data-v-a56ca0c4"><text class="tiny-text data-v-a56ca0c4">使用进度 {{d}}</text><text class="tiny-text data-v-a56ca0c4">{{e}}</text></view><view class="budget-foot secondary-foot data-v-a56ca0c4"><text class="tiny-text data-v-a56ca0c4">{{f}}</text></view><view class="editor-row data-v-a56ca0c4"><view class="input-shell data-v-a56ca0c4"><input class="data-v-a56ca0c4" type="digit" placeholder="输入本月总预算" value="{{g}}" bindinput="{{h}}"/></view><view class="primary-button save-btn data-v-a56ca0c4" bindtap="{{i}}">保存</view></view></section-card><section-card wx:if="{{k}}" class="data-v-a56ca0c4" u-s="{{['d']}}" u-i="a56ca0c4-1" bind:__l="__l" u-p="{{m}}"><view class="alert-list data-v-a56ca0c4"><view wx:for="{{l}}" wx:for-item="item" wx:key="d" class="alert-item data-v-a56ca0c4"><text class="alert-title data-v-a56ca0c4">{{item.a}}</text><text class="alert-text data-v-a56ca0c4">预算 {{item.b}},已支出 {{item.c}}</text></view></view></section-card><section-card wx:if="{{o}}" class="data-v-a56ca0c4" u-s="{{['d']}}" u-i="a56ca0c4-2" bind:__l="__l" u-p="{{o}}"><view class="category-list data-v-a56ca0c4"><view wx:for="{{n}}" wx:for-item="item" wx:key="k" class="category-card surface-strong data-v-a56ca0c4"><view class="category-head data-v-a56ca0c4"><view class="category-title-row data-v-a56ca0c4"><view class="category-dot data-v-a56ca0c4" style="{{'background:' + item.a}}"></view><text class="category-title data-v-a56ca0c4">{{item.b}}</text></view><text class="tiny-text data-v-a56ca0c4">已花 {{item.c}}</text></view><view class="progress-track thin-track data-v-a56ca0c4"><view class="progress-fill data-v-a56ca0c4" style="{{'width:' + item.d}}"></view></view><view class="budget-foot data-v-a56ca0c4"><text class="tiny-text data-v-a56ca0c4">预算 {{item.e}}</text><text class="{{['tiny-text', 'data-v-a56ca0c4', item.g]}}">{{item.f}}</text></view><view class="editor-row data-v-a56ca0c4"><view class="input-shell data-v-a56ca0c4"><input class="data-v-a56ca0c4" type="digit" placeholder="设置分类预算" value="{{item.h}}" bindinput="{{item.i}}"/></view><view class="ghost-button save-btn data-v-a56ca0c4" bindtap="{{item.j}}">保存</view></view></view></view></section-card><app-tab-bar wx:if="{{p}}" class="data-v-a56ca0c4" u-i="a56ca0c4-3" bind:__l="__l" u-p="{{p}}"/></view>
@@ -0,0 +1 @@
.budget-hero.data-v-a56ca0c4,.category-card.data-v-a56ca0c4{padding:24rpx;border-radius:24rpx}.budget-hero.data-v-a56ca0c4,.category-head.data-v-a56ca0c4,.category-title-row.data-v-a56ca0c4,.budget-foot.data-v-a56ca0c4,.editor-row.data-v-a56ca0c4{display:flex;align-items:center;justify-content:space-between;gap:16rpx}.secondary-foot.data-v-a56ca0c4{margin-top:10rpx}.budget-number.data-v-a56ca0c4{display:block;margin-top:10rpx;font-size:34rpx;font-weight:700;color:var(--text-primary)}.progress-track.data-v-a56ca0c4{overflow:hidden;height:18rpx;margin:24rpx 0 16rpx;border-radius:999rpx;background:var(--surface-muted)}.thin-track.data-v-a56ca0c4{height:14rpx;margin:18rpx 0 14rpx}.progress-fill.data-v-a56ca0c4{height:100%;border-radius:inherit;background:linear-gradient(90deg,#5f8df5,#1f6f5f)}.editor-row.data-v-a56ca0c4{margin-top:20rpx}.editor-row .input-shell.data-v-a56ca0c4{flex:1}.save-btn.data-v-a56ca0c4{flex:0 0 180rpx}.alert-list.data-v-a56ca0c4,.category-list.data-v-a56ca0c4{display:flex;flex-direction:column;gap:18rpx}.alert-item.data-v-a56ca0c4{padding:20rpx 22rpx;border-radius:22rpx;background:var(--danger-soft)}.alert-title.data-v-a56ca0c4{font-size:28rpx;font-weight:600;color:var(--danger)}.alert-text.data-v-a56ca0c4{display:block;margin-top:8rpx;font-size:22rpx;color:var(--danger)}.category-dot.data-v-a56ca0c4{width:16rpx;height:16rpx;border-radius:50%}.category-title.data-v-a56ca0c4{font-size:28rpx;font-weight:600;color:var(--text-primary)}
@@ -0,0 +1 @@
"use strict";const e=require("../../common/vendor.js"),t=require("../../utils/store.js"),a=require("../../utils/date.js"),u=require("../../utils/money.js");Math||(o+n+r)();const o=()=>"../../components/SectionCard.js",n=()=>"../../components/AppTabBar.js",r=()=>"../../components/BillEditorPopup.js",l={__name:"index",setup(o){const n=t.useAppStore(),r=e.ref(!1),l=e.ref(null),c=e.ref(""),i=e.ref("expense"),s=e.computed((()=>a.toDateKey())),d=e.computed((()=>a.toMonthKey())),m=e.computed((()=>"dark"===n.state.settings.theme?"theme-dark":"")),v=e.computed((()=>a.formatDateLabel(s.value))),p=e.computed((()=>[...n.state.bills].sort(((e,t)=>t.createdAt-e.createdAt)))),f=e.computed((()=>p.value.slice(0,5))),x=e.computed((()=>p.value.filter((e=>e.date===s.value)))),h=e.computed((()=>p.value.filter((e=>a.isSameMonth(e.date,d.value))))),y=e.computed((()=>x.value.filter((e=>"expense"===e.type)).reduce(((e,t)=>e+Number(t.amount)),0))),b=e.computed((()=>h.value.filter((e=>"expense"===e.type)).reduce(((e,t)=>e+Number(t.amount)),0))),g=e.computed((()=>h.value.filter((e=>"income"===e.type)).reduce(((e,t)=>e+Number(t.amount)),0))),C=e.computed((()=>g.value-b.value)),w=e.computed((()=>Number(n.state.budgets.total)||0)),M=e.computed((()=>w.value-b.value)),j=e.computed((()=>Math.max(M.value,0)/Math.max(1,a.getDaysLeftInMonth(d.value)))),A=e.computed((()=>u.clampPercent(b.value/Math.max(w.value||1,1)))),I=e.computed((()=>w.value?u.formatPercent(b.value/Math.max(w.value,1)):"未设置")),_=e.computed((()=>w.value?M.value<0?`已超支 ${u.formatCurrency(Math.abs(M.value))}`:`日均可用 ${u.formatCurrency(j.value)}`:"设置预算后可查看日均可用额度")),D=e.computed((()=>n.state.categories.expense.slice(0,6)));function L(e){return(n.state.categories[e.type]||[]).find((t=>t.id===e.categoryId))||{}}function P(e){return n.state.accounts.find((t=>t.id===e.accountId))||{}}function S(e="",t="expense"){c.value=e,i.value=t,l.value=null,r.value=!0}function T(){r.value=!1,l.value=null,c.value="",i.value="expense"}function $(t){n.saveBill(t),e.index.showToast({title:t.id?"账单已更新":"账单已保存",icon:"none"})}function q(t){e.index.showActionSheet({itemList:["编辑账单","删除账单"],success:({tapIndex:a})=>{0===a&&(l.value={...t},r.value=!0),1===a&&function(t){e.index.showModal({title:"删除账单",content:`确认删除 ${L(t).name||"这笔账单"} 吗?`,success:({confirm:e})=>{e&&n.deleteBill(t.id)}})}(t)}})}function B(){e.index.redirectTo({url:"/pages/bills/index"})}function k(){e.index.redirectTo({url:"/pages/budget/index"})}e.onPullDownRefresh((()=>{setTimeout((()=>{e.index.stopPullDownRefresh()}),200)}));const N=e.ref(!1);function E(){if(N.value)return;N.value=!0;let t=null;e.wx$1.createInterstitialAd&&(t=e.wx$1.createInterstitialAd({adUnitId:"adunit-0abc32053b19a4e9"}),t.onLoad((()=>{})),t.onError((e=>{console.error("插屏广告加载失败",e)})),t.onClose((()=>{}))),setTimeout((()=>{t&&(t.show().catch((e=>{console.error("插屏广告显示失败",e)})),N.value=!1)}),5480)}return e.onLoad((()=>{E()})),e.onShow((()=>{E()})),e.onShareAppMessage((e=>({title:"账单助手",desc:"本地单机极简记账,支持收支记录、预算管控、消费报表,数据安全私密,轻便好用的个人账单管家。",path:"/pages/home/index"}))),(t,o)=>e.e({a:e.t(v.value),b:e.t(e.unref(u.formatCurrency)(y.value)),c:e.t(e.unref(u.formatCurrency)(b.value)),d:e.t(e.unref(u.formatCurrency)(g.value)),e:e.t(e.unref(u.formatCurrency)(C.value)),f:e.t(e.unref(u.formatCurrency)(M.value)),g:e.t(I.value),h:A.value,i:e.t(w.value?`总预算 ${e.unref(u.formatCurrency)(w.value)}`:"当前尚未设置月预算"),j:e.t(_.value),k:!w.value},w.value?{}:{l:e.o(k)},{m:e.p({title:"预算概览",subtitle:"实时查看预算执行情况与剩余额度"}),n:e.o((e=>S("","expense"))),o:e.o((e=>S("","income"))),p:e.f(D.value,((t,a,u)=>({a:t.color,b:e.t(t.name),c:t.id,d:e.o((e=>S(t.id,"expense")),t.id)}))),q:e.p({title:"快捷记账",subtitle:"常用场景一步录入,提高日常记录效率"}),r:e.o(B),s:f.value.length},f.value.length?{t:e.f(f.value,((t,o,n)=>({a:L(t).color||"#7b8794",b:e.t(L(t).name||"未分类"),c:e.t(P(t).name||"账户"),d:e.t(e.unref(a.formatDateLabel)(t.date)),e:e.t("income"===t.type?"+":"-"),f:e.t(e.unref(u.formatCurrency)(t.amount).replace("¥","")),g:e.n("income"===t.type?"positive":"negative"),h:e.t(t.note||"无备注"),i:t.id,j:e.o((e=>q(t)),t.id)})))}:{v:e.o((e=>S("","expense")))},{w:e.p({title:"最近记录",subtitle:"保留最近 5 笔账单,长按可编辑或删除"}),x:e.o((e=>S("","expense"))),y:e.p({current:"home"}),z:e.o(T),A:e.o($),B:e.p({visible:r.value,entry:l.value,categories:e.unref(n).state.categories,accounts:e.unref(n).state.accounts,"default-type":i.value,"initial-category-id":c.value}),C:e.n(m.value)})}},c=e._export_sfc(l,[["__scopeId","data-v-a0f5ea5d"]]);l.__runtimeHooks=2,wx.createPage(c);
@@ -0,0 +1,9 @@
{
"navigationBarTitleText": "首页",
"enablePullDownRefresh": true,
"usingComponents": {
"section-card": "../../components/SectionCard",
"app-tab-bar": "../../components/AppTabBar",
"bill-editor-popup": "../../components/BillEditorPopup"
}
}
@@ -0,0 +1 @@
<view class="{{['app-page', 'data-v-a0f5ea5d', C]}}"><view class="surface-card hero-card data-v-a0f5ea5d"><text class="hero-date data-v-a0f5ea5d">{{a}}</text><text class="hero-title data-v-a0f5ea5d">账单小管家</text><view class="hero-metrics data-v-a0f5ea5d"><view class="metric-block data-v-a0f5ea5d"><text class="tiny-text data-v-a0f5ea5d">今日支出</text><text class="metric-value negative data-v-a0f5ea5d">{{b}}</text></view><view class="metric-block data-v-a0f5ea5d"><text class="tiny-text data-v-a0f5ea5d">本月支出</text><text class="metric-value negative data-v-a0f5ea5d">{{c}}</text></view><view class="metric-block data-v-a0f5ea5d"><text class="tiny-text data-v-a0f5ea5d">本月收入</text><text class="metric-value positive data-v-a0f5ea5d">{{d}}</text></view><view class="metric-block data-v-a0f5ea5d"><text class="tiny-text data-v-a0f5ea5d">本月结余</text><text class="metric-value data-v-a0f5ea5d">{{e}}</text></view></view></view><ad-custom class="data-v-a0f5ea5d" unit-id="adunit-74730c6c27c95a37"></ad-custom><section-card wx:if="{{m}}" class="data-v-a0f5ea5d" u-s="{{['d']}}" u-i="a0f5ea5d-0" bind:__l="__l" u-p="{{m}}"><view class="budget-head data-v-a0f5ea5d"><view class="data-v-a0f5ea5d"><text class="budget-value data-v-a0f5ea5d">{{f}}</text><text class="data-v-a0f5ea5d" style="margin-left:16rpx"></text><text class="tiny-text data-v-a0f5ea5d">剩余预算</text></view><view class="budget-side data-v-a0f5ea5d"><text class="tiny-text data-v-a0f5ea5d">预算使用</text><text class="data-v-a0f5ea5d" style="margin-left:16rpx"></text><text class="budget-percent data-v-a0f5ea5d">{{g}}</text></view></view><view class="progress-track data-v-a0f5ea5d"><view class="progress-fill data-v-a0f5ea5d" style="{{'width:' + h}}"></view></view><view class="budget-note-row data-v-a0f5ea5d"><text class="tiny-text data-v-a0f5ea5d">{{i}}</text><text class="tiny-text data-v-a0f5ea5d">{{j}}</text></view><view wx:if="{{k}}" class="budget-action-row data-v-a0f5ea5d"><view class="ghost-button data-v-a0f5ea5d" bindtap="{{l}}">去设置预算</view></view></section-card><section-card wx:if="{{q}}" class="data-v-a0f5ea5d" u-s="{{['d']}}" u-i="a0f5ea5d-1" bind:__l="__l" u-p="{{q}}"><view class="quick-action-row data-v-a0f5ea5d"><view class="primary-button quick-main data-v-a0f5ea5d" bindtap="{{n}}">记录支出</view><view class="ghost-button quick-main data-v-a0f5ea5d" bindtap="{{o}}">记录收入</view></view><view class="quick-grid data-v-a0f5ea5d"><view wx:for="{{p}}" wx:for-item="item" wx:key="c" class="quick-chip data-v-a0f5ea5d" bindtap="{{item.d}}"><view class="quick-dot data-v-a0f5ea5d" style="{{'background:' + item.a}}"></view><text class="data-v-a0f5ea5d">{{item.b}}</text></view></view></section-card><section-card wx:if="{{w}}" class="data-v-a0f5ea5d" u-s="{{['action','d']}}" u-i="a0f5ea5d-2" bind:__l="__l" u-p="{{w}}"><text class="section-link data-v-a0f5ea5d" bindtap="{{r}}" slot="action">查看全部</text><view wx:if="{{s}}" class="bill-list data-v-a0f5ea5d"><view wx:for="{{t}}" wx:for-item="bill" wx:key="i" class="bill-item data-v-a0f5ea5d" bindlongpress="{{bill.j}}"><view class="bill-leading data-v-a0f5ea5d"><view class="bill-dot data-v-a0f5ea5d" style="{{'background:' + bill.a}}"></view><view class="data-v-a0f5ea5d"><text class="bill-title data-v-a0f5ea5d">{{bill.b}}</text><text class="bill-meta data-v-a0f5ea5d">{{bill.c}} · {{bill.d}}</text></view></view><view class="bill-right data-v-a0f5ea5d"><text class="{{['bill-amount', 'data-v-a0f5ea5d', bill.g]}}">{{bill.e}}{{bill.f}}</text><text class="tiny-text data-v-a0f5ea5d">{{bill.h}}</text></view></view></view><view wx:else class="empty-card data-v-a0f5ea5d"><text class="section-subtitle data-v-a0f5ea5d">还没有账单记录,先记下第一笔收支。</text><view class="ghost-button empty-action data-v-a0f5ea5d" bindtap="{{v}}">立即记账</view></view></section-card><view class="fab-button data-v-a0f5ea5d" bindtap="{{x}}">+ 记一笔</view><app-tab-bar wx:if="{{y}}" class="data-v-a0f5ea5d" u-i="a0f5ea5d-3" bind:__l="__l" u-p="{{y}}"/><bill-editor-popup wx:if="{{B}}" class="data-v-a0f5ea5d" bindclose="{{z}}" bindsave="{{A}}" u-i="a0f5ea5d-4" bind:__l="__l" u-p="{{B}}"/></view>
@@ -0,0 +1 @@
.hero-card.data-v-a0f5ea5d{display:flex;flex-direction:column;gap:18rpx;padding:32rpx;background:var(--bg-accent);color:#fff}.hero-date.data-v-a0f5ea5d,.hero-subtitle.data-v-a0f5ea5d,.hero-card .tiny-text.data-v-a0f5ea5d{color:rgba(255,255,255,.76)}.hero-title.data-v-a0f5ea5d{font-size:46rpx;font-weight:700}.hero-subtitle.data-v-a0f5ea5d{font-size:24rpx;line-height:1.7}.hero-metrics.data-v-a0f5ea5d{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18rpx;margin-top:10rpx}.metric-block.data-v-a0f5ea5d{padding:20rpx;border-radius:24rpx;background:rgba(255,255,255,.12)}.metric-value.data-v-a0f5ea5d{display:block;margin-top:10rpx;font-size:32rpx;font-weight:700;color:#fff}.budget-head.data-v-a0f5ea5d,.budget-note-row.data-v-a0f5ea5d,.quick-action-row.data-v-a0f5ea5d{display:flex;align-items:center;justify-content:space-between;gap:18rpx}.budget-side.data-v-a0f5ea5d{text-align:right}.budget-value.data-v-a0f5ea5d,.budget-percent.data-v-a0f5ea5d{font-size:36rpx;font-weight:700;color:var(--text-primary)}.progress-track.data-v-a0f5ea5d{overflow:hidden;height:18rpx;margin:24rpx 0 16rpx;border-radius:999rpx;background:var(--surface-muted)}.progress-fill.data-v-a0f5ea5d{height:100%;border-radius:inherit;background:linear-gradient(90deg,#d36c43,#1f6f5f)}.budget-action-row.data-v-a0f5ea5d{margin-top:18rpx}.quick-action-row .quick-main.data-v-a0f5ea5d{flex:1}.quick-grid.data-v-a0f5ea5d{display:flex;flex-wrap:wrap;gap:16rpx;margin-top:18rpx}.quick-chip.data-v-a0f5ea5d{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.data-v-a0f5ea5d,.bill-dot.data-v-a0f5ea5d{width:16rpx;height:16rpx;border-radius:50%}.section-link.data-v-a0f5ea5d{font-size:24rpx;color:var(--brand)}.bill-list.data-v-a0f5ea5d{display:flex;flex-direction:column;gap:18rpx}.bill-item.data-v-a0f5ea5d{display:flex;align-items:center;justify-content:space-between;gap:16rpx;padding:22rpx;border-radius:24rpx;background:var(--surface-muted)}.bill-leading.data-v-a0f5ea5d{display:flex;align-items:center;gap:16rpx;flex:1}.bill-right.data-v-a0f5ea5d{display:flex;flex-direction:column;align-items:flex-end;gap:8rpx}.bill-title.data-v-a0f5ea5d,.bill-amount.data-v-a0f5ea5d{font-size:28rpx;font-weight:600;color:var(--text-primary)}.bill-meta.data-v-a0f5ea5d{margin-top:8rpx;font-size:22rpx;color:var(--text-secondary)}.empty-card.data-v-a0f5ea5d{padding:28rpx 0 8rpx;text-align:center}.empty-action.data-v-a0f5ea5d{margin-top:18rpx}.fab-button.data-v-a0f5ea5d{position:fixed;right:32rpx;bottom:348rpx;padding:24rpx 30rpx;border-radius:999rpx;background:var(--bg-accent);color:#fff;font-size:28rpx;font-weight:700;box-shadow:0 20rpx 42rpx rgba(16,42,67,.24);z-index:18}
@@ -0,0 +1 @@
"use strict";const e=require("../../../common/vendor.js"),t=require("../../../utils/store.js");Math||o();const o=()=>"../../../components/SectionCard.js",r={__name:"index",setup(o){const r=t.useAppStore();e.onShow((()=>{!function(){let t=null;e.wx$1.createInterstitialAd&&(t=e.wx$1.createInterstitialAd({adUnitId:"adunit-0abc32053b19a4e9"}),t.onLoad((()=>{})),t.onError((e=>{console.error("插屏广告加载失败",e)})),t.onClose((()=>{})));setTimeout((()=>{t&&t.show().catch((e=>{console.error("插屏广告显示失败",e)}))}),2280)}()}));const s=e.computed((()=>"dark"===r.state.settings.theme?"theme-dark":""));return(t,o)=>({a:e.p({title:"关于应用",subtitle:"面向日常收支记录、预算控制与月度复盘的轻量工具"}),b:e.p({title:"隐私与数据",subtitle:""}),c:e.n(s.value)})}},s=e._export_sfc(r,[["__scopeId","data-v-5d1e9385"]]);wx.createPage(s);
@@ -0,0 +1,6 @@
{
"navigationBarTitleText": "关于与隐私",
"usingComponents": {
"section-card": "../../../components/SectionCard"
}
}
@@ -0,0 +1 @@
<view class="{{['app-page', 'data-v-5d1e9385', c]}}"><view class="surface-card page-hero data-v-5d1e9385"><text class="hero-kicker data-v-5d1e9385">ABOUT</text><text class="hero-title data-v-5d1e9385">关于与隐私</text><text class="hero-desc data-v-5d1e9385">查看应用定位、数据说明与发布前应补齐的正式信息。</text><view class="hero-tags data-v-5d1e9385"><text class="hero-tag soft data-v-5d1e9385">发布说明</text></view></view><ad-custom class="data-v-5d1e9385" unit-id="adunit-64707ea333329399"></ad-custom><section-card wx:if="{{a}}" class="data-v-5d1e9385" u-s="{{['d']}}" u-i="5d1e9385-0" bind:__l="__l" u-p="{{a}}"><view class="about-card surface-strong data-v-5d1e9385"><text class="app-name data-v-5d1e9385">账单小管家</text><text class="about-text data-v-5d1e9385">定位为轻量、无广告的本地记账工具,适合学生、情侣、合租和个人日常记账场景。</text></view></section-card><section-card wx:if="{{b}}" class="data-v-5d1e9385" u-s="{{['d']}}" u-i="5d1e9385-1" bind:__l="__l" u-p="{{b}}"><view class="info-list data-v-5d1e9385"><view class="info-item surface-strong data-v-5d1e9385"><text class="info-title data-v-5d1e9385">本地存储</text><text class="info-text data-v-5d1e9385">账单、预算、分类、账户和设置默认保存在当前设备本地。</text></view><view class="info-item surface-strong data-v-5d1e9385"><text class="info-title data-v-5d1e9385">本地昵称</text><text class="info-text data-v-5d1e9385">昵称仅保存在当前设备,用于个人页展示和首字头像。</text></view><view class="info-item surface-strong data-v-5d1e9385"><text class="info-title data-v-5d1e9385">数据迁移</text><text class="info-text data-v-5d1e9385">如需换机迁移,可在“备份与恢复”中导出 JSON 并在新设备恢复。</text></view></view></section-card></view>
@@ -0,0 +1 @@
.page-hero.data-v-5d1e9385{padding:30rpx;background:linear-gradient(145deg,rgba(16,42,67,.96),rgba(31,111,95,.88) 60%,rgba(212,108,67,.82));color:#fff}.hero-kicker.data-v-5d1e9385,.hero-desc.data-v-5d1e9385,.hero-tag.soft.data-v-5d1e9385{color:rgba(255,255,255,.76)}.hero-kicker.data-v-5d1e9385{font-size:20rpx;letter-spacing:4rpx}.hero-title.data-v-5d1e9385{display:block;margin-top:12rpx;font-size:44rpx;font-weight:700}.hero-desc.data-v-5d1e9385{display:block;margin-top:14rpx;font-size:24rpx;line-height:1.7}.hero-tags.data-v-5d1e9385{display:flex;flex-wrap:wrap;gap:14rpx;margin-top:22rpx}.hero-tag.data-v-5d1e9385{padding:12rpx 18rpx;border-radius:999rpx;background:rgba(255,255,255,.16);font-size:22rpx}.about-card.data-v-5d1e9385,.info-item.data-v-5d1e9385,.tips-card.data-v-5d1e9385{padding:26rpx;border-radius:28rpx}.app-name.data-v-5d1e9385,.info-title.data-v-5d1e9385{display:block;font-size:31rpx;font-weight:700;color:var(--text-primary)}.app-version.data-v-5d1e9385{display:block;margin-top:10rpx;font-size:23rpx;color:var(--brand)}.about-text.data-v-5d1e9385,.info-text.data-v-5d1e9385,.tip-line.data-v-5d1e9385{display:block;margin-top:12rpx;font-size:24rpx;line-height:1.8;color:var(--text-secondary)}.info-list.data-v-5d1e9385{display:flex;flex-direction:column;gap:16rpx}.tip-row.data-v-5d1e9385{display:flex;align-items:flex-start;gap:16rpx;padding:10rpx 0}.tip-index.data-v-5d1e9385{width:56rpx;font-size:24rpx;font-weight:700;color:var(--brand)}
@@ -0,0 +1 @@
"use strict";const t=require("../../../common/vendor.js"),e=require("../../../utils/store.js");Math||a();const a=()=>"../../../components/SectionCard.js",n={__name:"index",setup(a){const n=e.useAppStore(),o=t.ref(""),s=t.computed((()=>"dark"===n.state.settings.theme?"theme-dark":""));function i(){!function(e){const a=(new Date).toLocaleString();if(void 0!==t.wx$1&&t.wx$1.getFileSystemManager){const o=`${t.wx$1.env.USER_DATA_PATH}/bill-helper-backup.json`;t.wx$1.getFileSystemManager().writeFile({filePath:o,data:e,encoding:"utf8",success:()=>{n.markBackup(a),t.index.showModal({title:"备份成功",content:`文件已生成:${o}`,showCancel:!1})},fail:()=>{t.index.setClipboardData({data:e})}})}else n.markBackup(a),t.index.setClipboardData({data:e})}(n.exportBackup())}function l(){if(o.value.trim())try{n.importBackup(o.value),o.value="",t.index.showToast({title:"备份恢复成功",icon:"none"})}catch(e){t.index.showToast({title:"备份内容无效",icon:"none"})}else t.index.showToast({title:"请先粘贴备份内容",icon:"none"})}function c(){t.index.setClipboardData({data:n.exportBackup()})}function r(){t.index.showModal({title:"清空全部数据",content:"确认删除当前设备中的账单、预算和设置吗?此操作不可撤销。",success:({confirm:e})=>{e&&(n.resetAll(),t.index.showToast({title:"本地数据已清空",icon:"none"}))}})}return t.onShow((()=>{!function(){let e=null;t.wx$1.createInterstitialAd&&(e=t.wx$1.createInterstitialAd({adUnitId:"adunit-0abc32053b19a4e9"}),e.onLoad((()=>{})),e.onError((t=>{console.error("插屏广告加载失败",t)})),e.onClose((()=>{})));setTimeout((()=>{e&&e.show().catch((t=>{console.error("插屏广告显示失败",t)}))}),2280)}()})),(e,a)=>t.e({a:t.t(t.unref(n).state.settings.lastBackupAt?"最近已备份":"尚未备份"),b:t.t(t.unref(n).state.settings.lastBackupAt?"可继续备份":"建议先备份"),c:t.o(i),d:t.unref(n).state.settings.lastBackupAt},t.unref(n).state.settings.lastBackupAt?{e:t.t(t.unref(n).state.settings.lastBackupAt)}:{},{f:t.p({title:"导出备份",subtitle:"将账单、预算和设置导出为 JSON 文件,便于迁移设备或手动保存"}),g:o.value,h:t.o((t=>o.value=t.detail.value)),i:t.o((t=>o.value="")),j:t.o(l),k:t.p({title:"恢复备份",subtitle:"粘贴之前导出的 JSON 内容,恢复后将覆盖当前设备数据"}),l:t.o(c),m:t.o(r),n:t.p({title:"数据操作",subtitle:"谨慎执行不可撤销的本地清理操作"}),o:t.n(s.value)})}},o=t._export_sfc(n,[["__scopeId","data-v-15765fe6"]]);wx.createPage(o);
@@ -0,0 +1,6 @@
{
"navigationBarTitleText": "备份与恢复",
"usingComponents": {
"section-card": "../../../components/SectionCard"
}
}
@@ -0,0 +1 @@
<view class="{{['app-page', 'data-v-15765fe6', o]}}"><view class="surface-card page-hero data-v-15765fe6"><text class="hero-kicker data-v-15765fe6">BACKUP</text><text class="hero-title data-v-15765fe6">备份与恢复</text><text class="hero-desc data-v-15765fe6">导出本地 JSON 备份,恢复时覆盖当前设备数据,适合换机或手动留档。</text><view class="hero-tags data-v-15765fe6"><text class="hero-tag data-v-15765fe6">本地文件</text><text class="hero-tag soft data-v-15765fe6">{{a}}</text></view></view><ad-custom class="data-v-15765fe6" unit-id="adunit-64707ea333329399"></ad-custom><section-card wx:if="{{f}}" class="data-v-15765fe6" u-s="{{['d']}}" u-i="15765fe6-0" bind:__l="__l" u-p="{{f}}"><view class="hero-panel surface-strong data-v-15765fe6"><view class="data-v-15765fe6"><text class="panel-title data-v-15765fe6">安全备份当前数据</text><text class="panel-desc data-v-15765fe6">所有数据均为本地文件,不会自动上传服务器。</text></view><view class="status-chip data-v-15765fe6">{{b}}</view></view><view class="action-row single-row data-v-15765fe6"><view class="primary-button data-v-15765fe6" bindtap="{{c}}">导出备份</view></view><text wx:if="{{d}}" class="tiny-text info-line data-v-15765fe6">最近备份:{{e}}</text></section-card><section-card wx:if="{{k}}" class="data-v-15765fe6" u-s="{{['d']}}" u-i="15765fe6-1" bind:__l="__l" u-p="{{k}}"><view class="input-shell textarea-shell data-v-15765fe6"><block wx:if="{{r0}}"><textarea class="data-v-15765fe6" placeholder="请粘贴备份 JSON" value="{{g}}" bindinput="{{h}}"></textarea></block></view><view class="action-row data-v-15765fe6"><view class="ghost-button data-v-15765fe6" bindtap="{{i}}">清空内容</view><view class="primary-button data-v-15765fe6" bindtap="{{j}}">开始恢复</view></view></section-card><section-card wx:if="{{n}}" class="data-v-15765fe6" u-s="{{['d']}}" u-i="15765fe6-2" bind:__l="__l" u-p="{{n}}"><view class="menu-list data-v-15765fe6"><view class="menu-item surface-strong data-v-15765fe6" bindtap="{{l}}"><view class="data-v-15765fe6"><text class="menu-title data-v-15765fe6">复制备份内容</text></view><text class="status-chip data-v-15765fe6">复制</text></view><view class="menu-item surface-strong danger-shell data-v-15765fe6" bindtap="{{m}}"><view class="data-v-15765fe6"><text class="menu-title negative data-v-15765fe6">清空全部数据</text></view><text class="danger-text data-v-15765fe6">执行</text></view></view></section-card></view>
@@ -0,0 +1 @@
.page-hero.data-v-15765fe6{padding:30rpx;background:linear-gradient(145deg,rgba(16,42,67,.96),rgba(61,102,178,.92));color:#fff}.hero-kicker.data-v-15765fe6,.hero-desc.data-v-15765fe6,.hero-tag.soft.data-v-15765fe6{color:rgba(255,255,255,.76)}.hero-kicker.data-v-15765fe6{font-size:20rpx;letter-spacing:4rpx}.hero-title.data-v-15765fe6{display:block;margin-top:12rpx;font-size:44rpx;font-weight:700}.hero-desc.data-v-15765fe6{display:block;margin-top:14rpx;font-size:24rpx;line-height:1.7}.hero-tags.data-v-15765fe6{display:flex;flex-wrap:wrap;gap:14rpx;margin-top:22rpx}.hero-tag.data-v-15765fe6,.status-chip.data-v-15765fe6{padding:12rpx 18rpx;border-radius:999rpx;background:rgba(255,255,255,.16);font-size:22rpx}.hero-panel.data-v-15765fe6,.menu-item.data-v-15765fe6,.action-row.data-v-15765fe6{display:flex;align-items:center;gap:16rpx}.hero-panel.data-v-15765fe6,.menu-item.data-v-15765fe6{padding:26rpx;border-radius:28rpx}.hero-panel.data-v-15765fe6{justify-content:space-between}.panel-title.data-v-15765fe6,.menu-title.data-v-15765fe6{display:block;font-size:31rpx;font-weight:700;color:var(--text-primary)}.panel-desc.data-v-15765fe6,.status-chip.data-v-15765fe6{background:var(--brand-soft);color:var(--brand)}.action-row.data-v-15765fe6{margin-top:18rpx}.single-row .primary-button.data-v-15765fe6,.action-row .ghost-button.data-v-15765fe6,.action-row .primary-button.data-v-15765fe6{flex:1}.info-line.data-v-15765fe6{display:block;margin-top:16rpx}.textarea-shell.data-v-15765fe6{align-items:flex-start;min-height:280rpx;padding:20rpx 24rpx}.textarea-shell textarea.data-v-15765fe6{min-height:240rpx}.menu-list.data-v-15765fe6{display:flex;flex-direction:column;gap:16rpx}.menu-item.data-v-15765fe6{justify-content:space-between}.danger-shell.data-v-15765fe6{border:1rpx solid rgba(210,85,67,.12)}.danger-text.data-v-15765fe6{font-size:24rpx;color:var(--danger)}
@@ -0,0 +1 @@
"use strict";const e=require("../../../common/vendor.js"),t=require("../../../utils/store.js");Math||i();const i=()=>"../../../components/SectionCard.js",n={__name:"index",setup(i){const n=t.useAppStore(),s=e.computed((()=>"dark"===n.state.settings.theme?"theme-dark":"")),o=[{index:"01",title:"先设置月预算",desc:"进入预算页设置总预算与分类预算,首页会同步显示剩余额度。"},{index:"02",title:"用首页快捷记账",desc:"首页支持支出/收入快速录入,也能通过常用分类一键记账。"},{index:"03",title:"到账单页做筛选",desc:"账单页支持按月份、账户、金额区间和关键词精确筛选。"},{index:"04",title:"每月查看报表",desc:"报表页可导出 CSV,并查看支出结构、近 7 日趋势和月度对比。"}],a=[{q:"账单数据保存在哪里?",a:"默认仅保存在当前设备的本地存储中,不会自动上传云端。"},{q:"换手机后如何迁移?",a:"先进入“备份与恢复”导出 JSON 备份,再在新设备粘贴恢复。"},{q:"为什么预算进度显示超出 100%",a:"这代表本月支出已经超过预算,条形进度会封顶,但文字会继续显示真实比例。"},{q:"昵称可以怎么修改?",a:"进入“我的-账户资料”后可直接修改本地昵称,留空时默认显示为“用户”。"}];function d(t){e.index.navigateTo({url:t})}return e.onShow((()=>{!function(){let t=null;e.wx$1.createInterstitialAd&&(t=e.wx$1.createInterstitialAd({adUnitId:"adunit-0abc32053b19a4e9"}),t.onLoad((()=>{})),t.onError((e=>{console.error("插屏广告加载失败",e)})),t.onClose((()=>{})));setTimeout((()=>{t&&t.show().catch((e=>{console.error("插屏广告显示失败",e)}))}),2280)}()})),(t,i)=>({a:e.f(o,((t,i,n)=>({a:e.t(t.index),b:e.t(t.title),c:e.t(t.desc),d:t.title}))),b:e.p({title:"快速上手",subtitle:"初次使用建议先完成下面 4 个动作"}),c:e.f(a,((t,i,n)=>({a:e.t(t.q),b:e.t(t.a),c:t.q}))),d:e.p({title:"常见问题",subtitle:""}),e:e.o((e=>d("/pages/home/index"))),f:e.o((e=>d("/pages/budget/index"))),g:e.o((e=>d("/pages/stats/index"))),h:e.o((e=>d("/pages/mine/backup/index"))),i:e.p({title:"功能入口",subtitle:"需要操作时可直接跳转到对应模块"}),j:e.n(s.value)})}},s=e._export_sfc(n,[["__scopeId","data-v-7f3520d8"]]);wx.createPage(s);
@@ -0,0 +1,6 @@
{
"navigationBarTitleText": "使用帮助",
"usingComponents": {
"section-card": "../../../components/SectionCard"
}
}
@@ -0,0 +1 @@
<view class="{{['app-page', 'data-v-7f3520d8', j]}}"><view class="surface-card page-hero data-v-7f3520d8"><text class="hero-kicker data-v-7f3520d8">GUIDE</text><text class="hero-title data-v-7f3520d8">使用帮助</text><text class="hero-desc data-v-7f3520d8">通过上手步骤、常见问题和快捷入口,快速熟悉整个记账流程。</text><view class="hero-tags data-v-7f3520d8"><text class="hero-tag data-v-7f3520d8">4 个步骤</text><text class="hero-tag soft data-v-7f3520d8">FAQ 指南</text></view></view><ad-custom class="data-v-7f3520d8" unit-id="adunit-64707ea333329399"></ad-custom><section-card wx:if="{{b}}" class="data-v-7f3520d8" u-s="{{['d']}}" u-i="7f3520d8-0" bind:__l="__l" u-p="{{b}}"><view class="step-list data-v-7f3520d8"><view wx:for="{{a}}" wx:for-item="item" wx:key="d" class="step-item surface-strong data-v-7f3520d8"><text class="step-index data-v-7f3520d8">{{item.a}}</text><view class="step-body data-v-7f3520d8"><text class="step-title data-v-7f3520d8">{{item.b}}</text><text class="step-desc data-v-7f3520d8">{{item.c}}</text></view></view></view></section-card><section-card wx:if="{{d}}" class="data-v-7f3520d8" u-s="{{['d']}}" u-i="7f3520d8-1" bind:__l="__l" u-p="{{d}}"><view class="faq-list data-v-7f3520d8"><view wx:for="{{c}}" wx:for-item="item" wx:key="c" class="faq-item surface-strong data-v-7f3520d8"><text class="faq-question data-v-7f3520d8">{{item.a}}</text><text class="faq-answer data-v-7f3520d8">{{item.b}}</text></view></view></section-card><section-card wx:if="{{i}}" class="data-v-7f3520d8" u-s="{{['d']}}" u-i="7f3520d8-2" bind:__l="__l" u-p="{{i}}"><view class="entry-grid data-v-7f3520d8"><view class="entry-item surface-strong data-v-7f3520d8" bindtap="{{e}}"><text class="entry-title data-v-7f3520d8">首页记账</text></view><view class="entry-item surface-strong data-v-7f3520d8" bindtap="{{f}}"><text class="entry-title data-v-7f3520d8">预算设置</text></view><view class="entry-item surface-strong data-v-7f3520d8" bindtap="{{g}}"><text class="entry-title data-v-7f3520d8">查看报表</text></view><view class="entry-item surface-strong data-v-7f3520d8" bindtap="{{h}}"><text class="entry-title data-v-7f3520d8">备份恢复</text></view></view></section-card></view>
@@ -0,0 +1 @@
.page-hero.data-v-7f3520d8{padding:30rpx;background:linear-gradient(145deg,rgba(16,42,67,.96),rgba(127,86,217,.9));color:#fff}.hero-kicker.data-v-7f3520d8,.hero-desc.data-v-7f3520d8,.hero-tag.soft.data-v-7f3520d8{color:rgba(255,255,255,.76)}.hero-kicker.data-v-7f3520d8{font-size:20rpx;letter-spacing:4rpx}.hero-title.data-v-7f3520d8{display:block;margin-top:12rpx;font-size:44rpx;font-weight:700}.hero-desc.data-v-7f3520d8{display:block;margin-top:14rpx;font-size:24rpx;line-height:1.7}.hero-tags.data-v-7f3520d8{display:flex;flex-wrap:wrap;gap:14rpx;margin-top:22rpx}.hero-tag.data-v-7f3520d8{padding:12rpx 18rpx;border-radius:999rpx;background:rgba(255,255,255,.16);font-size:22rpx}.step-list.data-v-7f3520d8,.faq-list.data-v-7f3520d8{display:flex;flex-direction:column;gap:16rpx}.step-item.data-v-7f3520d8,.faq-item.data-v-7f3520d8{padding:24rpx;border-radius:28rpx}.step-item.data-v-7f3520d8{display:flex;gap:18rpx}.step-index.data-v-7f3520d8{width:74rpx;font-size:30rpx;font-weight:700;color:var(--brand)}.step-body.data-v-7f3520d8{flex:1}.step-title.data-v-7f3520d8,.faq-question.data-v-7f3520d8,.entry-title.data-v-7f3520d8{display:block;font-size:29rpx;font-weight:700;color:var(--text-primary)}.step-desc.data-v-7f3520d8,.faq-answer.data-v-7f3520d8{display:block;margin-top:10rpx;font-size:24rpx;line-height:1.75;color:var(--text-secondary)}.entry-grid.data-v-7f3520d8{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16rpx}.entry-item.data-v-7f3520d8{display:flex;align-items:center;justify-content:center;padding:28rpx 20rpx;border-radius:28rpx;text-align:center}
@@ -0,0 +1 @@
"use strict";const e=require("../../common/vendor.js"),t=require("../../utils/store.js");Math||(n+s)();const n=()=>"../../components/SectionCard.js",s=()=>"../../components/AppTabBar.js",a={__name:"index",setup(n){const s=t.useAppStore(),a=e.computed((()=>"dark"===s.state.settings.theme?"theme-dark":"")),i=e.computed((()=>s.state.settings.profile.nickname||"用户")),o=e.computed((()=>(s.state.settings.profile.nickname||"用户").slice(0,1))),r=e.ref(">");function u(e){s.setTheme(e)}function p(t){e.index.navigateTo({url:t})}return(t,n)=>({a:e.t(o.value),b:e.t(i.value),c:e.t(r.value),d:e.o((e=>p("/pages/mine/profile/index"))),e:"light"===e.unref(s).state.settings.theme?1:"",f:e.o((e=>u("light"))),g:"dark"===e.unref(s).state.settings.theme?1:"",h:e.o((e=>u("dark"))),i:e.p({title:"账户概览",subtitle:"集中管理本地昵称与主题设置"}),j:e.t(r.value),k:e.o((e=>p("/pages/mine/backup/index"))),l:e.t(r.value),m:e.o((e=>p("/pages/mine/guide/index"))),n:e.t(r.value),o:e.o((e=>p("/pages/mine/about/index"))),p:e.p({title:"数据管理",subtitle:"备份、恢复和清理等高频操作统一收口"}),q:e.p({current:"mine"}),r:e.n(a.value)})}},i=e._export_sfc(a,[["__scopeId","data-v-a8ac4929"]]);wx.createPage(i);
@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "我的",
"usingComponents": {
"section-card": "../../components/SectionCard",
"app-tab-bar": "../../components/AppTabBar"
}
}
@@ -0,0 +1 @@
<view class="{{['app-page', 'data-v-a8ac4929', r]}}"><section-card wx:if="{{i}}" class="data-v-a8ac4929" u-s="{{['d']}}" u-i="a8ac4929-0" bind:__l="__l" u-p="{{i}}"><view class="profile-card surface-strong data-v-a8ac4929" bindtap="{{d}}"><view class="avatar-shell data-v-a8ac4929">{{a}}</view><view class="profile-body data-v-a8ac4929"><text class="profile-name data-v-a8ac4929">{{b}}</text><text class="section-subtitle data-v-a8ac4929"></text></view><text class="arrow-text data-v-a8ac4929">{{c}}</text></view><view class="theme-row data-v-a8ac4929"><view class="{{['pill-button', 'data-v-a8ac4929', e && 'active']}}" catchtap="{{f}}">浅色</view><view class="{{['pill-button', 'data-v-a8ac4929', g && 'active']}}" catchtap="{{h}}">深色</view></view></section-card><ad-custom class="data-v-a8ac4929" unit-id="adunit-74730c6c27c95a37"></ad-custom><section-card wx:if="{{p}}" class="data-v-a8ac4929" u-s="{{['d']}}" u-i="a8ac4929-1" bind:__l="__l" u-p="{{p}}"><view class="menu-list data-v-a8ac4929"><view class="menu-item data-v-a8ac4929" bindtap="{{k}}"><view class="data-v-a8ac4929"><text class="menu-title data-v-a8ac4929">备份与恢复</text></view><text class="arrow-text data-v-a8ac4929">{{j}}</text></view><view class="menu-item data-v-a8ac4929" bindtap="{{m}}"><view class="data-v-a8ac4929"><text class="menu-title data-v-a8ac4929">使用帮助</text></view><text class="arrow-text data-v-a8ac4929">{{l}}</text></view><view class="menu-item data-v-a8ac4929" bindtap="{{o}}"><view class="data-v-a8ac4929"><text class="menu-title data-v-a8ac4929">关于与隐私</text></view><text class="arrow-text data-v-a8ac4929">{{n}}</text></view></view></section-card><app-tab-bar wx:if="{{q}}" class="data-v-a8ac4929" u-i="a8ac4929-2" bind:__l="__l" u-p="{{q}}"/></view>
@@ -0,0 +1 @@
.profile-card.data-v-a8ac4929,.theme-row.data-v-a8ac4929,.menu-item.data-v-a8ac4929{display:flex;align-items:center;gap:16rpx}.profile-card.data-v-a8ac4929{padding:24rpx;border-radius:24rpx}.avatar-shell.data-v-a8ac4929{width:88rpx;height:88rpx;border-radius:50%;background:var(--bg-accent);color:#fff;display:flex;align-items:center;justify-content:center;font-size:34rpx;font-weight:700}.profile-body.data-v-a8ac4929{flex:1}.profile-name.data-v-a8ac4929,.menu-title.data-v-a8ac4929{font-size:30rpx;font-weight:600;color:var(--text-primary)}.arrow-text.data-v-a8ac4929{font-size:32rpx}.theme-row.data-v-a8ac4929{margin-top:18rpx}.theme-row .pill-button.data-v-a8ac4929{flex:1}.menu-list.data-v-a8ac4929,.action-grid.data-v-a8ac4929{display:flex;flex-direction:column;gap:16rpx}.menu-item.data-v-a8ac4929{justify-content:space-between;padding:22rpx;border-radius:24rpx;background:var(--surface-muted)}
@@ -0,0 +1 @@
"use strict";const e=require("../../../common/vendor.js"),t=require("../../../utils/store.js");Math||a();const a=()=>"../../../components/SectionCard.js",i={__name:"index",setup(a){const i=t.useAppStore(),n=e.computed((()=>"dark"===i.state.settings.theme?"theme-dark":"")),s=e.computed((()=>i.state.settings.profile.nickname||"用户")),o=e.computed((()=>s.value.slice(0,1))),l=e.ref(i.state.settings.profile.nickname||""),r=["昵称仅用于个人页展示和首字头像,不参与账单计算。","账单、预算和设置默认不会自动上传云端。","如需更换设备,请先在“备份与恢复”页面导出 JSON 备份。"];function u(){const t=l.value.trim();i.setProfile({authorized:!1,nickname:t,avatarUrl:""}),e.index.showToast({title:"昵称已保存",icon:"none"})}function c(){l.value="",i.setProfile({authorized:!1,nickname:"",avatarUrl:""}),e.index.showToast({title:"昵称已清空",icon:"none"})}return e.watch((()=>i.state.settings.profile.nickname),(e=>{l.value=e||""})),(t,a)=>({a:e.t(o.value),b:e.t(s.value),c:l.value,d:e.o((e=>l.value=e.detail.value)),e:e.o(c),f:e.o(u),g:e.p({title:"昵称设置",subtitle:"修改后仅用于个人页展示和首字头像,不参与账单计算"}),h:e.t(o.value),i:e.p({title:"显示与模式",subtitle:"集中展示当前账户页的生效状态"}),j:e.f(r,((t,a,i)=>({a:e.t(a+1),b:e.t(t),c:t}))),k:e.p({title:"使用提示",subtitle:"帮助用户理解昵称显示与数据边界"}),l:e.n(n.value)})}},n=e._export_sfc(i,[["__scopeId","data-v-94494afd"]]);wx.createPage(n);
@@ -0,0 +1,6 @@
{
"navigationBarTitleText": "账户资料",
"usingComponents": {
"section-card": "../../../components/SectionCard"
}
}
@@ -0,0 +1 @@
<view class="{{['app-page', 'data-v-94494afd', l]}}"><view class="surface-card page-hero data-v-94494afd"><text class="hero-kicker data-v-94494afd">ACCOUNT</text><text class="hero-title data-v-94494afd">账户资料</text><text class="hero-desc data-v-94494afd">管理本地昵称、显示资料与本机记账模式说明。</text><view class="hero-tags data-v-94494afd"><text class="hero-tag data-v-94494afd">本地资料</text><text class="hero-tag soft data-v-94494afd">本地存储</text></view></view><section-card wx:if="{{g}}" class="data-v-94494afd" u-s="{{['d']}}" u-i="94494afd-0" bind:__l="__l" u-p="{{g}}"><view class="profile-card surface-strong data-v-94494afd"><view class="avatar-shell data-v-94494afd">{{a}}</view><view class="profile-body data-v-94494afd"><text class="profile-name data-v-94494afd">{{b}}</text><text class="profile-meta data-v-94494afd">当前昵称仅保存在本地设备,可随时修改。</text></view><view class="status-badge data-v-94494afd">本地</view></view><view class="editor-block data-v-94494afd"><view class="input-shell data-v-94494afd"><input class="data-v-94494afd" maxlength="12" placeholder="请输入昵称" value="{{c}}" bindinput="{{d}}"/></view><text class="tiny-text editor-tip data-v-94494afd">留空时页面会统一显示“用户”。</text></view><view class="action-row data-v-94494afd"><view class="ghost-button data-v-94494afd" bindtap="{{e}}">清空昵称</view><view class="primary-button data-v-94494afd" bindtap="{{f}}">保存昵称</view></view></section-card><section-card wx:if="{{i}}" class="data-v-94494afd" u-s="{{['d']}}" u-i="94494afd-1" bind:__l="__l" u-p="{{i}}"><view class="info-list data-v-94494afd"><view class="info-item surface-strong data-v-94494afd"><view class="data-v-94494afd"><text class="info-title data-v-94494afd">昵称首字头像</text><text class="info-desc data-v-94494afd">当前显示 {{h}},自动根据昵称生成</text></view><text class="info-mark data-v-94494afd">已启用</text></view><view class="info-item surface-strong data-v-94494afd"><view class="data-v-94494afd"><text class="info-title data-v-94494afd">本机记账模式</text><text class="info-desc data-v-94494afd">账单与预算默认仅保存在当前设备本地</text></view><text class="info-mark data-v-94494afd">默认</text></view></view></section-card><section-card wx:if="{{k}}" class="data-v-94494afd" u-s="{{['d']}}" u-i="94494afd-2" bind:__l="__l" u-p="{{k}}"><view class="tips-card surface-strong data-v-94494afd"><view wx:for="{{j}}" wx:for-item="tip" wx:key="c" class="tip-row data-v-94494afd"><text class="tip-index data-v-94494afd">0{{tip.a}}</text><text class="tip-line data-v-94494afd">{{tip.b}}</text></view></view></section-card></view>
@@ -0,0 +1 @@
.page-hero.data-v-94494afd{padding:30rpx;background:linear-gradient(145deg,rgba(16,42,67,.96),rgba(31,111,95,.92));color:#fff}.hero-kicker.data-v-94494afd,.hero-desc.data-v-94494afd,.hero-tag.soft.data-v-94494afd{color:rgba(255,255,255,.76)}.hero-kicker.data-v-94494afd{font-size:20rpx;letter-spacing:4rpx}.hero-title.data-v-94494afd{display:block;margin-top:12rpx;font-size:44rpx;font-weight:700}.hero-desc.data-v-94494afd{display:block;margin-top:14rpx;font-size:24rpx;line-height:1.7}.hero-tags.data-v-94494afd{display:flex;flex-wrap:wrap;gap:14rpx;margin-top:22rpx}.hero-tag.data-v-94494afd{padding:12rpx 18rpx;border-radius:999rpx;background:rgba(255,255,255,.16);font-size:22rpx}.profile-card.data-v-94494afd,.info-item.data-v-94494afd,.action-row.data-v-94494afd,.tip-row.data-v-94494afd{display:flex;align-items:center;gap:16rpx}.profile-card.data-v-94494afd,.tips-card.data-v-94494afd{padding:26rpx;border-radius:28rpx}.avatar-shell.data-v-94494afd{width:108rpx;height:108rpx;border-radius:32rpx;background:var(--bg-accent);color:#fff;display:flex;align-items:center;justify-content:center;font-size:40rpx;font-weight:700;box-shadow:0 18rpx 32rpx rgba(16,42,67,.16)}.profile-body.data-v-94494afd{flex:1}.profile-name.data-v-94494afd,.info-title.data-v-94494afd{display:block;font-size:31rpx;font-weight:700;color:var(--text-primary)}.profile-meta.data-v-94494afd,.info-desc.data-v-94494afd,.tip-line.data-v-94494afd{display:block;margin-top:10rpx;font-size:24rpx;line-height:1.7;color:var(--text-secondary)}.status-badge.data-v-94494afd,.info-mark.data-v-94494afd{padding:10rpx 18rpx;border-radius:999rpx;background:var(--brand-soft);font-size:22rpx;color:var(--brand)}.editor-block.data-v-94494afd{margin-top:18rpx}.editor-tip.data-v-94494afd{display:block;margin-top:12rpx}.action-row.data-v-94494afd{margin-top:18rpx}.action-row .ghost-button.data-v-94494afd,.action-row .primary-button.data-v-94494afd{flex:1}.info-list.data-v-94494afd{display:flex;flex-direction:column;gap:16rpx}.info-item.data-v-94494afd{justify-content:space-between;padding:24rpx;border-radius:26rpx}.tip-row.data-v-94494afd{align-items:flex-start;padding:12rpx 0}.tip-index.data-v-94494afd{width:56rpx;font-size:24rpx;font-weight:700;color:var(--brand)}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "数据报表",
"usingComponents": {
"section-card": "../../components/SectionCard",
"app-tab-bar": "../../components/AppTabBar"
}
}
@@ -0,0 +1 @@
<view class="{{['app-page', 'data-v-7cab36fb', C]}}"><section-card wx:if="{{g}}" class="data-v-7cab36fb" u-s="{{['action','d']}}" u-i="7cab36fb-0" bind:__l="__l" u-p="{{g}}"><picker class="data-v-7cab36fb" mode="date" fields="month" value="{{b}}" bindchange="{{c}}" slot="action"><text class="section-link data-v-7cab36fb">{{a}}</text></picker><view class="summary-row data-v-7cab36fb"><view class="summary-item surface-strong data-v-7cab36fb"><text class="tiny-text data-v-7cab36fb">支出</text><text class="summary-value negative data-v-7cab36fb">{{d}}</text></view><view class="summary-item surface-strong data-v-7cab36fb"><text class="tiny-text data-v-7cab36fb">收入</text><text class="summary-value positive data-v-7cab36fb">{{e}}</text></view><view class="summary-item surface-strong data-v-7cab36fb"><text class="tiny-text data-v-7cab36fb">结余</text><text class="summary-value data-v-7cab36fb">{{f}}</text></view></view></section-card><ad-custom class="data-v-7cab36fb" unit-id="adunit-74730c6c27c95a37"></ad-custom><section-card wx:if="{{j}}" class="data-v-7cab36fb" u-s="{{['d']}}" u-i="7cab36fb-1" bind:__l="__l" u-p="{{j}}"><view wx:if="{{h}}" class="chart-list data-v-7cab36fb"><view wx:for="{{i}}" wx:for-item="item" wx:key="g" class="chart-row data-v-7cab36fb"><view class="chart-head data-v-7cab36fb"><view class="chart-title-row data-v-7cab36fb"><view class="chart-dot data-v-7cab36fb" style="{{'background:' + item.a}}"></view><text class="chart-title data-v-7cab36fb">{{item.b}}</text></view><text class="tiny-text data-v-7cab36fb">{{item.c}} · {{item.d}}</text></view><view class="bar-track data-v-7cab36fb"><view class="bar-fill data-v-7cab36fb" style="{{'width:' + item.e + ';' + ('background:' + item.f)}}"></view></view></view></view><view wx:else class="empty-card data-v-7cab36fb"><text class="section-subtitle data-v-7cab36fb">当前月份暂无支出数据,记一笔后会自动生成图表。</text></view></section-card><section-card wx:if="{{l}}" class="data-v-7cab36fb" u-s="{{['d']}}" u-i="7cab36fb-2" bind:__l="__l" u-p="{{l}}"><view class="column-chart data-v-7cab36fb"><view wx:for="{{k}}" wx:for-item="item" wx:key="d" class="column-item data-v-7cab36fb"><view class="column-track data-v-7cab36fb"><view class="column-fill data-v-7cab36fb" style="{{'height:' + item.a}}"></view></view><text class="tiny-text data-v-7cab36fb">{{item.b}}</text><text class="tiny-text data-v-7cab36fb">{{item.c}}</text></view></view></section-card><section-card wx:if="{{n}}" class="data-v-7cab36fb" u-s="{{['d']}}" u-i="7cab36fb-3" bind:__l="__l" u-p="{{n}}"><view class="compare-list data-v-7cab36fb"><view wx:for="{{m}}" wx:for-item="item" wx:key="f" class="compare-row data-v-7cab36fb"><text class="compare-label data-v-7cab36fb">{{item.a}}</text><view class="compare-bars data-v-7cab36fb"><view class="mini-track data-v-7cab36fb"><view class="mini-fill expense-fill data-v-7cab36fb" style="{{'width:' + item.b}}"></view></view><view class="mini-track data-v-7cab36fb"><view class="mini-fill income-fill data-v-7cab36fb" style="{{'width:' + item.c}}"></view></view></view><text class="tiny-text data-v-7cab36fb">{{item.d}} / {{item.e}}</text></view></view></section-card><section-card wx:if="{{q}}" class="data-v-7cab36fb" u-s="{{['d']}}" u-i="7cab36fb-4" bind:__l="__l" u-p="{{q}}"><view class="action-grid data-v-7cab36fb"><view class="primary-button data-v-7cab36fb" bindtap="{{o}}">导出 CSV</view><view class="ghost-button data-v-7cab36fb" bindtap="{{p}}">分享摘要</view></view></section-card><view wx:if="{{r}}" class="poster-shell data-v-7cab36fb" catchtouchmove="{{A}}"><view class="poster-mask data-v-7cab36fb" bindtap="{{s}}"></view><view class="surface-card poster-panel data-v-7cab36fb"><view class="poster-card data-v-7cab36fb"><text class="poster-month data-v-7cab36fb">{{t}}</text><text class="poster-title data-v-7cab36fb">收支月报</text><text class="poster-line data-v-7cab36fb">支出 {{v}}</text><text class="poster-line data-v-7cab36fb">收入 {{w}}</text><text class="poster-line data-v-7cab36fb">结余 {{x}}</text><text class="poster-tip data-v-7cab36fb">内容本地生成,可复制摘要或直接截图分享。</text></view><view class="action-grid data-v-7cab36fb"><view class="ghost-button data-v-7cab36fb" bindtap="{{y}}">复制摘要</view><view class="primary-button data-v-7cab36fb" bindtap="{{z}}">关闭</view></view></view></view><app-tab-bar wx:if="{{B}}" class="data-v-7cab36fb" u-i="7cab36fb-5" bind:__l="__l" u-p="{{B}}"/></view>
@@ -0,0 +1 @@
.summary-row.data-v-7cab36fb,.action-grid.data-v-7cab36fb{display:flex;gap:16rpx}.summary-item.data-v-7cab36fb{flex:1;padding:22rpx;border-radius:24rpx}.summary-value.data-v-7cab36fb{display:block;margin-top:10rpx;font-size:30rpx;font-weight:700;color:var(--text-primary)}.section-link.data-v-7cab36fb{font-size:24rpx;color:var(--brand)}.chart-list.data-v-7cab36fb,.compare-list.data-v-7cab36fb{display:flex;flex-direction:column;gap:18rpx}.chart-head.data-v-7cab36fb,.chart-title-row.data-v-7cab36fb,.compare-row.data-v-7cab36fb{display:flex;align-items:center;justify-content:space-between;gap:16rpx}.chart-dot.data-v-7cab36fb{width:16rpx;height:16rpx;border-radius:50%}.chart-title.data-v-7cab36fb{font-size:28rpx;font-weight:600;color:var(--text-primary)}.bar-track.data-v-7cab36fb,.mini-track.data-v-7cab36fb,.column-track.data-v-7cab36fb{overflow:hidden;border-radius:999rpx;background:var(--surface-muted)}.bar-track.data-v-7cab36fb{height:16rpx;margin-top:12rpx}.bar-fill.data-v-7cab36fb,.mini-fill.data-v-7cab36fb{height:100%;border-radius:inherit}.column-chart.data-v-7cab36fb{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:12rpx;align-items:end;height:260rpx}.column-item.data-v-7cab36fb{display:flex;flex-direction:column;align-items:center;gap:10rpx}.column-track.data-v-7cab36fb{display:flex;align-items:flex-end;justify-content:center;width:100%;height:180rpx;padding:0 6rpx}.column-fill.data-v-7cab36fb{width:100%;border-radius:999rpx 999rpx 16rpx 16rpx;background:linear-gradient(180deg,#5f8df5,#1f6f5f)}.compare-label.data-v-7cab36fb{width:64rpx;font-size:24rpx;color:var(--text-secondary)}.compare-bars.data-v-7cab36fb{flex:1;display:flex;flex-direction:column;gap:10rpx}.mini-track.data-v-7cab36fb{height:12rpx}.expense-fill.data-v-7cab36fb{background:#d36c43}.income-fill.data-v-7cab36fb{background:#1f6f5f}.action-grid.data-v-7cab36fb{margin-top:8rpx}.action-grid .primary-button.data-v-7cab36fb,.action-grid .ghost-button.data-v-7cab36fb{flex:1}.empty-card.data-v-7cab36fb{padding:32rpx 0 10rpx;text-align:center}.poster-shell.data-v-7cab36fb{position:fixed;top:0;right:0;bottom:0;left:0;z-index:50}.poster-mask.data-v-7cab36fb{position:absolute;top:0;right:0;bottom:0;left:0;background:rgba(4,12,18,.42)}.poster-panel.data-v-7cab36fb{position:absolute;left:24rpx;right:24rpx;top:16vh;padding:28rpx}.poster-card.data-v-7cab36fb{padding:32rpx;border-radius:28rpx;background:linear-gradient(145deg,#102a43,#1f6f5f);color:#fff;margin-bottom:20rpx}.poster-month.data-v-7cab36fb,.poster-tip.data-v-7cab36fb{color:rgba(255,255,255,.76)}.poster-title.data-v-7cab36fb{display:block;margin:12rpx 0 20rpx;font-size:42rpx;font-weight:700}.poster-line.data-v-7cab36fb{display:block;margin-bottom:12rpx;font-size:28rpx}.poster-tip.data-v-7cab36fb{display:block;margin-top:24rpx;font-size:22rpx;line-height:1.6}
@@ -0,0 +1,36 @@
{
"description": "项目配置文件。",
"packOptions": {
"ignore": []
},
"setting": {
"urlCheck": false,
"es6": true,
"postcss": false,
"minified": true,
"newFeature": true,
"bigPackageSizeSupport": true
},
"compileType": "miniprogram",
"libVersion": "",
"appid": "wx64fbe7ae2180912a",
"projectname": "账单小管家",
"condition": {
"search": {
"current": -1,
"list": []
},
"conversation": {
"current": -1,
"list": []
},
"game": {
"current": -1,
"list": []
},
"miniprogram": {
"current": -1,
"list": []
}
}
}
@@ -0,0 +1,22 @@
{
"libVersion": "3.14.3",
"projectname": "mp-weixin",
"condition": {},
"setting": {
"urlCheck": false,
"coverView": false,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"useApiHook": true,
"showShadowRootInWxmlPanel": false,
"useStaticServer": false,
"useLanDebug": false,
"showES6CompileOption": false,
"compileHotReLoad": true,
"checkInvalidKey": true,
"ignoreDevUnusedFiles": true,
"bigPackageSizeSupport": true
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1 @@
"use strict";const a=[{id:"food",name:"餐饮",color:"#d36c43"},{id:"transport",name:"交通",color:"#5a8dee"},{id:"rent",name:"住房",color:"#6e5ef7"},{id:"shopping",name:"购物",color:"#d14b7d"},{id:"entertainment",name:"娱乐",color:"#f0a33a"},{id:"medical",name:"医疗",color:"#17a589"},{id:"travel",name:"旅行",color:"#008b8b"},{id:"daily",name:"日用",color:"#7b8794"}],e=[{id:"salary",name:"工资",color:"#1f8f6d"},{id:"bonus",name:"奖金",color:"#3c9d5e"},{id:"allowance",name:"生活费",color:"#5f8df5"},{id:"refund",name:"退款",color:"#e39b2d"},{id:"sidejob",name:"副业",color:"#7f56d9"}],o=[{id:"wechat",name:"微信",color:"#1aad19"},{id:"alipay",name:"支付宝",color:"#1677ff"},{id:"cash",name:"现金",color:"#ff8a3d"},{id:"bank",name:"银行卡",color:"#44546a"}];exports.createDefaultData=function(){return{categories:{expense:[...a],income:[...e]},accounts:[...o],bills:[],budgets:{total:0,categoryBudgets:{}},settings:{theme:"light",profile:{authorized:!1,nickname:"",avatarUrl:""},lastBackupAt:""}}};
+1
View File
@@ -0,0 +1 @@
"use strict";function t(t){return String(t).padStart(2,"0")}function e(t=new Date){if(t instanceof Date)return new Date(t.getTime());if("number"==typeof t)return new Date(t);if("string"==typeof t){if(/^\d{4}-\d{2}-\d{2}$/.test(t)){const[e,n,r]=t.split("-").map(Number);return new Date(e,n-1,r)}if(/^\d{4}-\d{2}$/.test(t)){const[e,n]=t.split("-").map(Number);return new Date(e,n-1,1)}return new Date(t.replace(/-/g,"/"))}return new Date}function n(n=new Date){const r=e(n);return`${r.getFullYear()}-${t(r.getMonth()+1)}-${t(r.getDate())}`}function r(n=new Date){const r=e(n);return`${r.getFullYear()}-${t(r.getMonth()+1)}`}function o(t){const[e,n]=t.split("-").map(Number);return new Date(e,n,0).getDate()}exports.formatDateLabel=function(t){if(!t)return"";const n=e(t);return`${n.getMonth()+1}${n.getDate()}${["周日","周一","周二","周三","周四","周五","周六"][n.getDay()]}`},exports.formatMonthLabel=function(t){if(!t)return"";const[e,n]=t.split("-");return`${e}${Number(n)}`},exports.getDaysLeftInMonth=function(t){const e=new Date;return r(e)!==t?o(t):o(t)-e.getDate()+1},exports.getMonthSeries=function(t,e=r()){const[n,o]=e.split("-").map(Number),a=new Date(n,o-1,1),s=[];for(let u=t-1;u>=0;u-=1){const t=new Date(a);t.setMonth(a.getMonth()-u),s.push(r(t))}return s},exports.getRecentDateKeys=function(t,r=new Date){const o=[],a=e(r);for(let e=t-1;e>=0;e-=1){const t=new Date(a);t.setDate(a.getDate()-e),o.push(n(t))}return o},exports.isSameMonth=function(t,e){return r(t)===e},exports.parseDate=e,exports.toDateKey=n,exports.toMonthKey=r;
+1
View File
@@ -0,0 +1 @@
"use strict";function r(r){return Math.max(0,Math.round(100*(Number(r)||0)))}exports.clampPercent=function(t){return`${Math.min(100,r(t))}%`},exports.formatCurrency=function(r){return`¥${Number(r||0).toFixed(2)}`},exports.formatPercent=function(t){return`${r(t)}%`};
+1
View File
@@ -0,0 +1 @@
"use strict";const e=require("../common/vendor.js"),t=require("./constants.js"),s="bill-helper-miniapp-v1";function n(e){return JSON.parse(JSON.stringify(e))}function i(e){var s,n,i,l,o,a,c,r,u,g,d,p;const f=t.createDefaultData(),b=e||{};return{categories:{expense:Array.isArray(null==(s=b.categories)?void 0:s.expense)&&b.categories.expense.length?b.categories.expense:f.categories.expense,income:Array.isArray(null==(n=b.categories)?void 0:n.income)&&b.categories.income.length?b.categories.income:f.categories.income},accounts:Array.isArray(b.accounts)&&b.accounts.length?b.accounts:f.accounts,bills:Array.isArray(b.bills)?b.bills:f.bills,budgets:{total:Number(null==(i=b.budgets)?void 0:i.total)||f.budgets.total,categoryBudgets:(null==(l=b.budgets)?void 0:l.categoryBudgets)||f.budgets.categoryBudgets},settings:{theme:(null==(o=b.settings)?void 0:o.theme)||f.settings.theme,profile:{authorized:Boolean(null==(c=null==(a=b.settings)?void 0:a.profile)?void 0:c.authorized),nickname:(null==(u=null==(r=b.settings)?void 0:r.profile)?void 0:u.nickname)||f.settings.profile.nickname,avatarUrl:(null==(d=null==(g=b.settings)?void 0:g.profile)?void 0:d.avatarUrl)||""},lastBackupAt:(null==(p=b.settings)?void 0:p.lastBackupAt)||""}}}const l=e.reactive(i(function(){try{const n=e.index.getStorageSync(s);if(!n){const n=t.createDefaultData();return e.index.setStorageSync(s,n),n}return i(n)}catch(n){return t.createDefaultData()}}()));function o(e){l.categories.expense.splice(0,l.categories.expense.length,...e.categories.expense),l.categories.income.splice(0,l.categories.income.length,...e.categories.income),l.accounts.splice(0,l.accounts.length,...e.accounts),l.bills.splice(0,l.bills.length,...e.bills),l.budgets.total=Number(e.budgets.total)||0,l.budgets.categoryBudgets={...e.budgets.categoryBudgets},l.settings.theme=e.settings.theme,l.settings.profile={...e.settings.profile},l.settings.lastBackupAt=e.settings.lastBackupAt||""}function a(){e.index.setStorageSync(s,n(l))}exports.useAppStore=function(){return{state:l,saveBill:function(e){const t=function(e){return{id:e.id||`bill-${Date.now()}`,type:e.type||"expense",amount:Number(e.amount)||0,categoryId:e.categoryId||"",accountId:e.accountId||"",note:e.note||"",date:e.date,createdAt:e.createdAt||Date.now()}}(e),s=l.bills.findIndex((e=>e.id===t.id));-1===s?l.bills.unshift(t):l.bills.splice(s,1,t),a()},deleteBill:function(e){l.bills.splice(0,l.bills.length,...l.bills.filter((t=>t.id!==e))),a()},deleteBills:function(e){const t=new Set(e);l.bills.splice(0,l.bills.length,...l.bills.filter((e=>!t.has(e.id)))),a()},setBudgetTotal:function(e){l.budgets.total=Number(e)||0,a()},setCategoryBudget:function(e,t){l.budgets.categoryBudgets={...l.budgets.categoryBudgets,[e]:Number(t)||0},a()},setTheme:function(e){l.settings.theme=e,a()},setProfile:function(e){l.settings.profile={...l.settings.profile,...e},a()},markBackup:function(e){l.settings.lastBackupAt=e,a()},exportBackup:function(){return JSON.stringify(n(l),null,2)},importBackup:function(e){o(i(JSON.parse(e))),a()},resetAll:function(){o(t.createDefaultData()),a()}}};
+8
View File
@@ -0,0 +1,8 @@
{
"hash": "cd0b394e",
"configHash": "0843ca34",
"lockfileHash": "e3b0c442",
"browserHash": "9273fc72",
"optimized": {},
"chunks": {}
}
+3
View File
@@ -0,0 +1,3 @@
{
"type": "module"
}
+52
View File
@@ -0,0 +1,52 @@
export const EXPENSE_CATEGORIES = [
{ id: 'food', name: '餐饮', color: '#d36c43' },
{ id: 'transport', name: '交通', color: '#5a8dee' },
{ id: 'rent', name: '住房', color: '#6e5ef7' },
{ id: 'shopping', name: '购物', color: '#d14b7d' },
{ id: 'entertainment', name: '娱乐', color: '#f0a33a' },
{ id: 'medical', name: '医疗', color: '#17a589' },
{ id: 'travel', name: '旅行', color: '#008b8b' },
{ id: 'daily', name: '日用', color: '#7b8794' }
]
export const INCOME_CATEGORIES = [
{ id: 'salary', name: '工资', color: '#1f8f6d' },
{ id: 'bonus', name: '奖金', color: '#3c9d5e' },
{ id: 'allowance', name: '生活费', color: '#5f8df5' },
{ id: 'refund', name: '退款', color: '#e39b2d' },
{ id: 'sidejob', name: '副业', color: '#7f56d9' }
]
export const DEFAULT_ACCOUNTS = [
{ id: 'wechat', name: '微信', color: '#1aad19' },
{ id: 'alipay', name: '支付宝', color: '#1677ff' },
{ id: 'cash', name: '现金', color: '#ff8a3d' },
{ id: 'bank', name: '银行卡', color: '#44546a' }
]
export const DEFAULT_THEME = 'light'
export function createDefaultData() {
return {
categories: {
expense: [...EXPENSE_CATEGORIES],
income: [...INCOME_CATEGORIES]
},
accounts: [...DEFAULT_ACCOUNTS],
bills: [],
budgets: {
total: 0,
categoryBudgets: {}
},
settings: {
theme: DEFAULT_THEME,
profile: {
authorized: false,
nickname: '',
avatarUrl: ''
},
lastBackupAt: ''
}
}
}
+103
View File
@@ -0,0 +1,103 @@
function pad(value) {
return String(value).padStart(2, '0')
}
export function parseDate(input = new Date()) {
if (input instanceof Date) {
return new Date(input.getTime())
}
if (typeof input === 'number') {
return new Date(input)
}
if (typeof input === 'string') {
if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
const [year, month, day] = input.split('-').map(Number)
return new Date(year, month - 1, day)
}
if (/^\d{4}-\d{2}$/.test(input)) {
const [year, month] = input.split('-').map(Number)
return new Date(year, month - 1, 1)
}
return new Date(input.replace(/-/g, '/'))
}
return new Date()
}
export function toDateKey(input = new Date()) {
const date = parseDate(input)
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
}
export function toMonthKey(input = new Date()) {
const date = parseDate(input)
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}`
}
export function formatDateLabel(value) {
if (!value) {
return ''
}
const date = parseDate(value)
const weekMap = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
return `${date.getMonth() + 1}${date.getDate()}${weekMap[date.getDay()]}`
}
export function formatMonthLabel(monthKey) {
if (!monthKey) {
return ''
}
const [year, month] = monthKey.split('-')
return `${year}${Number(month)}`
}
export function isSameMonth(dateValue, monthKey) {
return toMonthKey(dateValue) === monthKey
}
export function getMonthDays(monthKey) {
const [year, month] = monthKey.split('-').map(Number)
return new Date(year, month, 0).getDate()
}
export function getDaysLeftInMonth(monthKey) {
const today = new Date()
if (toMonthKey(today) !== monthKey) {
return getMonthDays(monthKey)
}
return getMonthDays(monthKey) - today.getDate() + 1
}
export function getRecentDateKeys(days, endDate = new Date()) {
const result = []
const end = parseDate(endDate)
for (let index = days - 1; index >= 0; index -= 1) {
const current = new Date(end)
current.setDate(end.getDate() - index)
result.push(toDateKey(current))
}
return result
}
export function getMonthSeries(length, endMonthKey = toMonthKey()) {
const [year, month] = endMonthKey.split('-').map(Number)
const cursor = new Date(year, month - 1, 1)
const result = []
for (let index = length - 1; index >= 0; index -= 1) {
const current = new Date(cursor)
current.setMonth(cursor.getMonth() - index)
result.push(toMonthKey(current))
}
return result
}
+15
View File
@@ -0,0 +1,15 @@
export function formatCurrency(value) {
return `¥${Number(value || 0).toFixed(2)}`
}
function toPercentNumber(value) {
return Math.max(0, Math.round((Number(value) || 0) * 100))
}
export function formatPercent(value) {
return `${toPercentNumber(value)}%`
}
export function clampPercent(value) {
return `${Math.min(100, toPercentNumber(value))}%`
}
+179
View File
@@ -0,0 +1,179 @@
import { reactive } from 'vue'
import { createDefaultData } from './constants'
const STORAGE_KEY = 'bill-helper-miniapp-v1'
function deepClone(value) {
return JSON.parse(JSON.stringify(value))
}
function normalizeData(raw) {
const fallback = createDefaultData()
const source = raw || {}
return {
categories: {
expense: Array.isArray(source.categories?.expense) && source.categories.expense.length
? source.categories.expense
: fallback.categories.expense,
income: Array.isArray(source.categories?.income) && source.categories.income.length
? source.categories.income
: fallback.categories.income
},
accounts: Array.isArray(source.accounts) && source.accounts.length ? source.accounts : fallback.accounts,
bills: Array.isArray(source.bills) ? source.bills : fallback.bills,
budgets: {
total: Number(source.budgets?.total) || fallback.budgets.total,
categoryBudgets: source.budgets?.categoryBudgets || fallback.budgets.categoryBudgets
},
settings: {
theme: source.settings?.theme || fallback.settings.theme,
profile: {
authorized: Boolean(source.settings?.profile?.authorized),
nickname: source.settings?.profile?.nickname || fallback.settings.profile.nickname,
avatarUrl: source.settings?.profile?.avatarUrl || ''
},
lastBackupAt: source.settings?.lastBackupAt || ''
}
}
}
function loadData() {
try {
const raw = uni.getStorageSync(STORAGE_KEY)
if (!raw) {
const seeded = createDefaultData()
uni.setStorageSync(STORAGE_KEY, seeded)
return seeded
}
return normalizeData(raw)
} catch (error) {
return createDefaultData()
}
}
const state = reactive(normalizeData(loadData()))
function patchState(nextState) {
state.categories.expense.splice(0, state.categories.expense.length, ...nextState.categories.expense)
state.categories.income.splice(0, state.categories.income.length, ...nextState.categories.income)
state.accounts.splice(0, state.accounts.length, ...nextState.accounts)
state.bills.splice(0, state.bills.length, ...nextState.bills)
state.budgets.total = Number(nextState.budgets.total) || 0
state.budgets.categoryBudgets = { ...nextState.budgets.categoryBudgets }
state.settings.theme = nextState.settings.theme
state.settings.profile = { ...nextState.settings.profile }
state.settings.lastBackupAt = nextState.settings.lastBackupAt || ''
}
function persist() {
uni.setStorageSync(STORAGE_KEY, deepClone(state))
}
function buildBillPayload(payload) {
return {
id: payload.id || `bill-${Date.now()}`,
type: payload.type || 'expense',
amount: Number(payload.amount) || 0,
categoryId: payload.categoryId || '',
accountId: payload.accountId || '',
note: payload.note || '',
date: payload.date,
createdAt: payload.createdAt || Date.now()
}
}
export function useAppStore() {
function saveBill(payload) {
const nextBill = buildBillPayload(payload)
const index = state.bills.findIndex((item) => item.id === nextBill.id)
if (index === -1) {
state.bills.unshift(nextBill)
} else {
state.bills.splice(index, 1, nextBill)
}
persist()
}
function deleteBill(id) {
state.bills.splice(
0,
state.bills.length,
...state.bills.filter((item) => item.id !== id)
)
persist()
}
function deleteBills(ids) {
const idSet = new Set(ids)
state.bills.splice(
0,
state.bills.length,
...state.bills.filter((item) => !idSet.has(item.id))
)
persist()
}
function setBudgetTotal(value) {
state.budgets.total = Number(value) || 0
persist()
}
function setCategoryBudget(categoryId, value) {
state.budgets.categoryBudgets = {
...state.budgets.categoryBudgets,
[categoryId]: Number(value) || 0
}
persist()
}
function setTheme(theme) {
state.settings.theme = theme
persist()
}
function setProfile(profile) {
state.settings.profile = {
...state.settings.profile,
...profile
}
persist()
}
function markBackup(timeLabel) {
state.settings.lastBackupAt = timeLabel
persist()
}
function exportBackup() {
return JSON.stringify(deepClone(state), null, 2)
}
function importBackup(payload) {
const parsed = normalizeData(JSON.parse(payload))
patchState(parsed)
persist()
}
function resetAll() {
patchState(createDefaultData())
persist()
}
return {
state,
saveBill,
deleteBill,
deleteBills,
setBudgetTotal,
setCategoryBudget,
setTheme,
setProfile,
markBackup,
exportBackup,
importBackup,
resetAll
}
}