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