第一次上传

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
+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
}
}