feat(新增):
新增ocr获取信息后,批量直接入库功能
This commit is contained in:
parent
e7b72f8845
commit
70715bb0c8
16
api/bill.js
16
api/bill.js
@ -15,6 +15,11 @@ export function createBill(data) {
|
|||||||
return post('/bills', data)
|
return post('/bills', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量创建账单
|
||||||
|
export function createBillsBatch(data) {
|
||||||
|
return post('/bills/batch', data)
|
||||||
|
}
|
||||||
|
|
||||||
// 更新账单
|
// 更新账单
|
||||||
export function updateBill(id, data) {
|
export function updateBill(id, data) {
|
||||||
return put(`/bills/${id}`, data)
|
return put(`/bills/${id}`, data)
|
||||||
@ -23,13 +28,4 @@ export function updateBill(id, data) {
|
|||||||
// 删除账单
|
// 删除账单
|
||||||
export function deleteBill(id) {
|
export function deleteBill(id) {
|
||||||
return del(`/bills/${id}`)
|
return del(`/bills/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
43
api/budget.js
Normal file
43
api/budget.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { get, put } from '../utils/request'
|
||||||
|
|
||||||
|
// 获取当前月份的预算信息(包含统计数据)
|
||||||
|
export async function getBudget() {
|
||||||
|
try {
|
||||||
|
const response = await get('/budgets')
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取预算信息失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取上月预算结算信息
|
||||||
|
export async function getBudgetSettlement() {
|
||||||
|
try {
|
||||||
|
const response = await get('/budgets/settlement')
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取预算结算信息失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置预算
|
||||||
|
export async function setBudget(data) {
|
||||||
|
try {
|
||||||
|
const now = new Date()
|
||||||
|
const requestData = {
|
||||||
|
year: data.year || now.getFullYear(),
|
||||||
|
month: data.month || (now.getMonth() + 1),
|
||||||
|
amount: data.amount
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await put('/budgets', requestData)
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('设置预算失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
193
components/BudgetPieChart/BudgetPieChart.vue
Normal file
193
components/BudgetPieChart/BudgetPieChart.vue
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
<template>
|
||||||
|
<view class="chart-container">
|
||||||
|
<view :id="chartId" class="echart-container"></view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
budget: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
used: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
chartId: {
|
||||||
|
type: String,
|
||||||
|
default: 'budgetPieChart'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let chartInstance = null
|
||||||
|
|
||||||
|
const chartData = computed(() => {
|
||||||
|
const total = props.budget || 1
|
||||||
|
const used = Math.min(props.used, total)
|
||||||
|
const remaining = Math.max(total - used, 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
budget: props.budget,
|
||||||
|
used: used,
|
||||||
|
remaining: remaining
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const initChart = () => {
|
||||||
|
// 等待 DOM 渲染
|
||||||
|
nextTick(() => {
|
||||||
|
// 使用 uni.createSelectorQuery 获取 DOM 元素
|
||||||
|
const query = uni.createSelectorQuery()
|
||||||
|
query.select(`#${props.chartId}`).boundingClientRect()
|
||||||
|
query.exec((res) => {
|
||||||
|
if (!res || !res[0]) {
|
||||||
|
console.error('Chart container not found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在 H5 环境下直接使用 document.getElementById
|
||||||
|
// #ifdef H5
|
||||||
|
const chartDom = document.getElementById(props.chartId)
|
||||||
|
if (chartDom) {
|
||||||
|
chartInstance = echarts.init(chartDom)
|
||||||
|
updateChart()
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateChart = () => {
|
||||||
|
if (!chartInstance) return
|
||||||
|
|
||||||
|
const { used, remaining } = chartData.value
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['60%', '70%'],
|
||||||
|
center: ['50%', '50%'],
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
itemStyle: {
|
||||||
|
borderRadius: 20,
|
||||||
|
borderWidth: 2
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: 'center',
|
||||||
|
formatter: function() {
|
||||||
|
return `{a|剩余预算}\n{b|¥${remaining.toFixed(2)}}`
|
||||||
|
},
|
||||||
|
rich: {
|
||||||
|
a: {
|
||||||
|
fontSize: 13,
|
||||||
|
color: 'rgba(255, 255, 255, 0.75)',
|
||||||
|
lineHeight: 20,
|
||||||
|
fontWeight: 'normal'
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
fontSize: 17,
|
||||||
|
color: '#fff',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
lineHeight: 32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
value: used,
|
||||||
|
name: '已用',
|
||||||
|
itemStyle: {
|
||||||
|
color: {
|
||||||
|
type: 'linear',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{ offset: 0, color: '#8673C3' },
|
||||||
|
{ offset: 1, color: '#8673C3' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowColor: 'rgba(255, 107, 107, 0.3)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: remaining,
|
||||||
|
name: '剩余',
|
||||||
|
itemStyle: {
|
||||||
|
color: {
|
||||||
|
type: 'linear',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{ offset: 0, color: '#69db7c' },
|
||||||
|
{ offset: 1, color: '#51cf66' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowColor: 'rgba(81, 207, 102, 0.3)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
emphasis: {
|
||||||
|
scale: true,
|
||||||
|
scaleSize: 5,
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 15,
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.4)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animationType: 'scale',
|
||||||
|
animationEasing: 'cubicOut',
|
||||||
|
animationDelay: 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
chartInstance.setOption(option, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => [props.budget, props.used], () => {
|
||||||
|
updateChart()
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
initChart()
|
||||||
|
}, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.dispose()
|
||||||
|
chartInstance = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chart-container {
|
||||||
|
width: 280rpx;
|
||||||
|
height: 280rpx;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.echart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
514
package-lock.json
generated
514
package-lock.json
generated
@ -10,14 +10,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dcloudio/uni-app": "^2.0.2-4080720251210002",
|
"@dcloudio/uni-app": "^2.0.2-4080720251210002",
|
||||||
"@dcloudio/uni-h5": "^2.0.2-4080720251210002",
|
"@dcloudio/uni-h5": "^2.0.2-4080720251210002",
|
||||||
|
"echarts": "^6.0.0",
|
||||||
"pinia": "^2.3.1",
|
"pinia": "^2.3.1",
|
||||||
"vue": "^3.5.25"
|
"vue": "^3.5.25"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.10.2",
|
"@types/node": "^24.10.2",
|
||||||
"@vitejs/plugin-vue": "^4.0.0",
|
"typescript": "^5.2.2"
|
||||||
"typescript": "^5.2.2",
|
|
||||||
"vite": "^4.5.14"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-string-parser": {
|
"node_modules/@babel/helper-string-parser": {
|
||||||
@ -83,358 +82,6 @@
|
|||||||
"safe-area-insets": "^1.4.1"
|
"safe-area-insets": "^1.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-arm64": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-x64": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-arm": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
|
|
||||||
"cpu": [
|
|
||||||
"loong64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
|
|
||||||
"cpu": [
|
|
||||||
"mips64el"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
|
|
||||||
"cpu": [
|
|
||||||
"s390x"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-x64": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"netbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"sunos"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-x64": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
@ -449,19 +96,6 @@
|
|||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitejs/plugin-vue": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-e0X4jErIxAB5oLtDqbHvHpJe/uWNkdpYV83AOG2xo2tEVSzCzewgJMtREZM30wXnM5ls90hxiOtAuVU6H5JgbA==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": "^14.18.0 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"vite": "^4.0.0",
|
|
||||||
"vue": "^3.2.25"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/compiler-core": {
|
"node_modules/@vue/compiler-core": {
|
||||||
"version": "3.5.25",
|
"version": "3.5.25",
|
||||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.25.tgz",
|
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.25.tgz",
|
||||||
@ -571,6 +205,15 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/echarts": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0",
|
||||||
|
"zrender": "6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/entities": {
|
"node_modules/entities": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
|
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
|
||||||
@ -582,62 +225,11 @@
|
|||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
|
||||||
"version": "0.18.20",
|
|
||||||
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.18.20.tgz",
|
|
||||||
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"bin": {
|
|
||||||
"esbuild": "bin/esbuild"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@esbuild/android-arm": "0.18.20",
|
|
||||||
"@esbuild/android-arm64": "0.18.20",
|
|
||||||
"@esbuild/android-x64": "0.18.20",
|
|
||||||
"@esbuild/darwin-arm64": "0.18.20",
|
|
||||||
"@esbuild/darwin-x64": "0.18.20",
|
|
||||||
"@esbuild/freebsd-arm64": "0.18.20",
|
|
||||||
"@esbuild/freebsd-x64": "0.18.20",
|
|
||||||
"@esbuild/linux-arm": "0.18.20",
|
|
||||||
"@esbuild/linux-arm64": "0.18.20",
|
|
||||||
"@esbuild/linux-ia32": "0.18.20",
|
|
||||||
"@esbuild/linux-loong64": "0.18.20",
|
|
||||||
"@esbuild/linux-mips64el": "0.18.20",
|
|
||||||
"@esbuild/linux-ppc64": "0.18.20",
|
|
||||||
"@esbuild/linux-riscv64": "0.18.20",
|
|
||||||
"@esbuild/linux-s390x": "0.18.20",
|
|
||||||
"@esbuild/linux-x64": "0.18.20",
|
|
||||||
"@esbuild/netbsd-x64": "0.18.20",
|
|
||||||
"@esbuild/openbsd-x64": "0.18.20",
|
|
||||||
"@esbuild/sunos-x64": "0.18.20",
|
|
||||||
"@esbuild/win32-arm64": "0.18.20",
|
|
||||||
"@esbuild/win32-ia32": "0.18.20",
|
|
||||||
"@esbuild/win32-x64": "0.18.20"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/estree-walker": {
|
"node_modules/estree-walker": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
|
||||||
},
|
},
|
||||||
"node_modules/fsevents": {
|
|
||||||
"version": "2.3.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
|
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/intersection-observer": {
|
"node_modules/intersection-observer": {
|
||||||
"version": "0.7.0",
|
"version": "0.7.0",
|
||||||
"resolved": "https://registry.npmmirror.com/intersection-observer/-/intersection-observer-0.7.0.tgz",
|
"resolved": "https://registry.npmmirror.com/intersection-observer/-/intersection-observer-0.7.0.tgz",
|
||||||
@ -749,22 +341,6 @@
|
|||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
|
||||||
"version": "3.29.5",
|
|
||||||
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-3.29.5.tgz",
|
|
||||||
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"rollup": "dist/bin/rollup"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.18.0",
|
|
||||||
"npm": ">=8.0.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"fsevents": "~2.3.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/safe-area-insets": {
|
"node_modules/safe-area-insets": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmmirror.com/safe-area-insets/-/safe-area-insets-1.4.1.tgz",
|
"resolved": "https://registry.npmmirror.com/safe-area-insets/-/safe-area-insets-1.4.1.tgz",
|
||||||
@ -778,6 +354,11 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
|
||||||
@ -802,61 +383,6 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/urijs/-/urijs-1.19.11.tgz",
|
"resolved": "https://registry.npmmirror.com/urijs/-/urijs-1.19.11.tgz",
|
||||||
"integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ=="
|
"integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ=="
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
|
||||||
"version": "4.5.14",
|
|
||||||
"resolved": "https://registry.npmmirror.com/vite/-/vite-4.5.14.tgz",
|
|
||||||
"integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"esbuild": "^0.18.10",
|
|
||||||
"postcss": "^8.4.27",
|
|
||||||
"rollup": "^3.27.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"vite": "bin/vite.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^14.18.0 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"fsevents": "~2.3.2"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/node": ">= 14",
|
|
||||||
"less": "*",
|
|
||||||
"lightningcss": "^1.21.0",
|
|
||||||
"sass": "*",
|
|
||||||
"stylus": "*",
|
|
||||||
"sugarss": "*",
|
|
||||||
"terser": "^5.4.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/node": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"less": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"lightningcss": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"sass": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"stylus": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"sugarss": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"terser": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vue": {
|
"node_modules/vue": {
|
||||||
"version": "3.5.25",
|
"version": "3.5.25",
|
||||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.25.tgz",
|
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.25.tgz",
|
||||||
@ -901,6 +427,14 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zrender": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dcloudio/uni-app": "^2.0.2-4080720251210002",
|
"@dcloudio/uni-app": "^2.0.2-4080720251210002",
|
||||||
"@dcloudio/uni-h5": "^2.0.2-4080720251210002",
|
"@dcloudio/uni-h5": "^2.0.2-4080720251210002",
|
||||||
|
"echarts": "^6.0.0",
|
||||||
"pinia": "^2.3.1",
|
"pinia": "^2.3.1",
|
||||||
"vue": "^3.5.25"
|
"vue": "^3.5.25"
|
||||||
},
|
},
|
||||||
@ -18,4 +19,4 @@
|
|||||||
"@types/node": "^24.10.2",
|
"@types/node": "^24.10.2",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,6 +43,12 @@
|
|||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "账户管理"
|
"navigationBarTitleText": "账户管理"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/budget/budget",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "预算管理"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tabBar": {
|
"tabBar": {
|
||||||
|
|||||||
@ -110,6 +110,9 @@
|
|||||||
|
|
||||||
<!-- OCR识别结果列表(可折叠) -->
|
<!-- OCR识别结果列表(可折叠) -->
|
||||||
<view v-if="ocrResultList.length > 0" class="ocr-result-list-container">
|
<view v-if="ocrResultList.length > 0" class="ocr-result-list-container">
|
||||||
|
<!-- 新增的直接存储按钮 -->
|
||||||
|
<button class="direct-save-btn" @click="saveBillsDirectly">直接存储</button>
|
||||||
|
|
||||||
<view class="section-header" @click="toggleResultList">
|
<view class="section-header" @click="toggleResultList">
|
||||||
<text class="list-title">识别到 {{ ocrResultList.length }} 条账单记录</text>
|
<text class="list-title">识别到 {{ ocrResultList.length }} 条账单记录</text>
|
||||||
<text class="expand-icon">{{ resultListExpanded ? '▼' : '▶' }}</text>
|
<text class="expand-icon">{{ resultListExpanded ? '▼' : '▶' }}</text>
|
||||||
@ -154,7 +157,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import { createBill } from '../../api/bill'
|
import {createBill, createBillsBatch} from '../../api/bill'
|
||||||
import { getCategories } from '../../api/category'
|
import { getCategories } from '../../api/category'
|
||||||
import { recognizeImage as ocrRecognize } from '../../api/ocr'
|
import { recognizeImage as ocrRecognize } from '../../api/ocr'
|
||||||
import { formatDate } from '../../utils/date'
|
import { formatDate } from '../../utils/date'
|
||||||
@ -444,6 +447,70 @@ const submitBill = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saveBillsDirectly = async () => {
|
||||||
|
if (ocrResultList.value.length === 0) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '没有账单记录可以保存',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构造符合要求的数据格式
|
||||||
|
const bills = ocrResultList.value.map(item => {
|
||||||
|
// 根据金额正负判断账单类型
|
||||||
|
const amount = parseFloat(item.amount) || 0
|
||||||
|
const billType = amount < 0 ? 1 : 2 // 1-支出,2-收入
|
||||||
|
|
||||||
|
return {
|
||||||
|
categoryId: 3, // 默认分类ID为3
|
||||||
|
amount: Math.abs(amount), // 金额取绝对值
|
||||||
|
description: item.merchant || '购物', // 如果没有商户名,默认为"购物"
|
||||||
|
billDate: item.date ? formatOcrDate(item.date) : formatDate(new Date()), // 如果没有日期,使用今天
|
||||||
|
type: billType
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 发送批量保存请求
|
||||||
|
// const response = await fetch('/api/bills/batch', {
|
||||||
|
// method: 'POST',
|
||||||
|
// headers: {
|
||||||
|
// 'Content-Type': 'application/json'
|
||||||
|
// },
|
||||||
|
// body: JSON.stringify({ bills })
|
||||||
|
// })
|
||||||
|
|
||||||
|
const response = await createBillsBatch(JSON.stringify({ bills }))
|
||||||
|
|
||||||
|
console.log('保存结果:', response)
|
||||||
|
|
||||||
|
if (response.length) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '保存成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
// 触发首页刷新
|
||||||
|
uni.$emit('refreshBills')
|
||||||
|
|
||||||
|
// 跳转到首页
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index'
|
||||||
|
})
|
||||||
|
}, 1500)
|
||||||
|
} else {
|
||||||
|
throw new Error('保存失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存失败:', error)
|
||||||
|
uni.showToast({
|
||||||
|
title: error.message || '保存失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadCategories()
|
loadCategories()
|
||||||
|
|
||||||
@ -891,5 +958,4 @@ onMounted(() => {
|
|||||||
padding-top: 30rpx;
|
padding-top: 30rpx;
|
||||||
border-top: 2rpx solid #667eea;
|
border-top: 2rpx solid #667eea;
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
</style>
|
|
||||||
301
pages/budget/budget.vue
Normal file
301
pages/budget/budget.vue
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
<template>
|
||||||
|
<scroll-view scroll-y class="page-scroll">
|
||||||
|
<view class="container">
|
||||||
|
<view class="budget-card">
|
||||||
|
<view class="card-header">
|
||||||
|
<text class="card-title">预算设置</text>
|
||||||
|
<text class="card-subtitle">{{ currentMonthText }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-body">
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="label">预算金额</text>
|
||||||
|
<input
|
||||||
|
v-model="form.amount"
|
||||||
|
type="text"
|
||||||
|
inputmode="decimal"
|
||||||
|
placeholder="请输入预算金额"
|
||||||
|
@input="onAmountInput"
|
||||||
|
/>
|
||||||
|
<text class="form-tip">设置本月预算金额,用于控制支出</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="budget.usedAmount !== undefined" class="info-section">
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">本月已支出</text>
|
||||||
|
<text class="info-value expense">¥{{ budget.usedAmount ? budget.usedAmount.toFixed(2) : '0.00' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">剩余预算</text>
|
||||||
|
<text class="info-value" :class="budget.remainingAmount >= 0 ? 'positive' : 'negative'">
|
||||||
|
¥{{ budget.remainingAmount !== undefined ? budget.remainingAmount.toFixed(2) : '0.00' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="budget.remainingDaily !== undefined && budget.remainingDaily > 0" class="info-item">
|
||||||
|
<text class="info-label">剩余日均</text>
|
||||||
|
<text class="info-value">¥{{ budget.remainingDaily.toFixed(2) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-footer">
|
||||||
|
<button class="save-btn" @click="saveBudget" :loading="saving">保存</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { getBudget, setBudget } from '../../api/budget'
|
||||||
|
|
||||||
|
const budget = ref({
|
||||||
|
id: null,
|
||||||
|
year: null,
|
||||||
|
month: null,
|
||||||
|
amount: 0,
|
||||||
|
usedAmount: 0,
|
||||||
|
remainingAmount: 0,
|
||||||
|
remainingDaily: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
year: null,
|
||||||
|
month: null,
|
||||||
|
amount: '0.00'
|
||||||
|
})
|
||||||
|
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
const currentMonthText = computed(() => {
|
||||||
|
if (budget.value.year && budget.value.month) {
|
||||||
|
return `${budget.value.year}年${budget.value.month}月`
|
||||||
|
}
|
||||||
|
const now = new Date()
|
||||||
|
return `${now.getFullYear()}年${now.getMonth() + 1}月`
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadBudget = async () => {
|
||||||
|
try {
|
||||||
|
const data = await getBudget()
|
||||||
|
if (data) {
|
||||||
|
budget.value = data
|
||||||
|
form.value.year = data.year
|
||||||
|
form.value.month = data.month
|
||||||
|
form.value.amount = data.amount ? data.amount.toFixed(2) : '0.00'
|
||||||
|
} else {
|
||||||
|
// 如果没有预算数据,使用默认值
|
||||||
|
const now = new Date()
|
||||||
|
budget.value = {
|
||||||
|
year: now.getFullYear(),
|
||||||
|
month: now.getMonth() + 1,
|
||||||
|
amount: 0,
|
||||||
|
usedAmount: 0,
|
||||||
|
remainingAmount: 0,
|
||||||
|
remainingDaily: 0
|
||||||
|
}
|
||||||
|
form.value.year = now.getFullYear()
|
||||||
|
form.value.month = now.getMonth() + 1
|
||||||
|
form.value.amount = '0.00'
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载预算失败', error)
|
||||||
|
uni.showToast({
|
||||||
|
title: '加载预算失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onAmountInput = (e) => {
|
||||||
|
let value = e.detail.value || ''
|
||||||
|
value = value.replace(/[^\d.]/g, '')
|
||||||
|
const parts = value.split('.')
|
||||||
|
if (parts.length > 2) {
|
||||||
|
value = parts[0] + '.' + parts.slice(1).join('')
|
||||||
|
}
|
||||||
|
if (parts.length === 2 && parts[1].length > 2) {
|
||||||
|
value = parts[0] + '.' + parts[1].substring(0, 2)
|
||||||
|
}
|
||||||
|
form.value.amount = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveBudget = async () => {
|
||||||
|
const amount = parseFloat(form.value.amount) || 0
|
||||||
|
|
||||||
|
if (amount < 0) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '预算金额不能为负数',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const data = await setBudget({
|
||||||
|
year: form.value.year,
|
||||||
|
month: form.value.month,
|
||||||
|
amount: amount
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
budget.value = data
|
||||||
|
uni.showToast({
|
||||||
|
title: '保存成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 触发首页刷新
|
||||||
|
uni.$emit('refreshBills')
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存预算失败', error)
|
||||||
|
uni.showToast({
|
||||||
|
title: error.message || '保存失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadBudget()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-scroll {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: calc(50px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
min-height: calc(100vh - 100rpx);
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20rpx;
|
||||||
|
padding-bottom: calc(170rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
padding: 30rpx;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20rpx;
|
||||||
|
background: #f8f8f8;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-tip {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
margin-top: 40rpx;
|
||||||
|
padding-top: 30rpx;
|
||||||
|
border-top: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value.positive {
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value.negative {
|
||||||
|
color: #fa3534;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value.expense {
|
||||||
|
color: #fa3534;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
padding: 0 30rpx 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 88rpx;
|
||||||
|
background: #667eea;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
@ -3,23 +3,51 @@
|
|||||||
<view class="header">
|
<view class="header">
|
||||||
<view class="header-top">
|
<view class="header-top">
|
||||||
<text class="title">我的账单</text>
|
<text class="title">我的账单</text>
|
||||||
<view class="account-btn" @click="goToAccount">
|
<view class="header-actions">
|
||||||
<text class="account-icon">💰</text>
|
<view class="action-btn" @click="goToBudget">
|
||||||
|
<text class="action-icon">📊</text>
|
||||||
|
</view>
|
||||||
|
<view class="action-btn" @click="goToAccount">
|
||||||
|
<text class="action-icon">💰</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="summary">
|
|
||||||
<view class="summary-item">
|
<!-- 收入支出汇总 -->
|
||||||
<text class="label">本月支出</text>
|
<view class="summary-row">
|
||||||
<text class="amount expense">¥{{ monthlyExpense.toFixed(2) }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="summary-item">
|
<view class="summary-item">
|
||||||
<text class="label">本月收入</text>
|
<text class="label">本月收入</text>
|
||||||
<text class="amount income">¥{{ monthlyIncome.toFixed(2) }}</text>
|
<text class="amount income">¥{{ monthlyIncome.toFixed(2) }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="summary-item">
|
||||||
|
<text class="label">本月支出</text>
|
||||||
|
<text class="amount expense">¥{{ budget.usedAmount ? budget.usedAmount.toFixed(2) : '0.00' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 预算信息 -->
|
||||||
|
<view class="budget-section">
|
||||||
|
<view class="budget-chart-wrapper">
|
||||||
|
<BudgetPieChart
|
||||||
|
:budget="budget.amount || 0"
|
||||||
|
:used="budget.usedAmount || 0"
|
||||||
|
chartId="mainBudgetChart"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="budget-stats-row">
|
||||||
|
<view class="budget-stat-item">
|
||||||
|
<text class="stat-label">本月预算</text>
|
||||||
|
<text class="stat-value">¥{{ budget.amount ? budget.amount.toFixed(2) : '0.00' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="budget-stat-item">
|
||||||
|
<text class="stat-label">每日预算</text>
|
||||||
|
<text class="stat-value">¥{{ budget.remainingDaily ? budget.remainingDaily.toFixed(2) : '0.00' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="bill-list">
|
<scroll-view class="bill-list" scroll-y="true" show-scrollbar="false" enhanced="true" :bounce="false">
|
||||||
<view v-for="(group, date) in groupedBills" :key="date" class="bill-group">
|
<view v-for="(group, date) in groupedBills" :key="date" class="bill-group">
|
||||||
<view class="group-header">
|
<view class="group-header">
|
||||||
<text class="date">{{ date }}</text>
|
<text class="date">{{ date }}</text>
|
||||||
@ -45,7 +73,7 @@
|
|||||||
<view v-if="loading" class="loading">
|
<view v-if="loading" class="loading">
|
||||||
<text>加载中...</text>
|
<text>加载中...</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -53,13 +81,21 @@
|
|||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { getBills } from '../../api/bill'
|
import { getBills } from '../../api/bill'
|
||||||
import { getMonthlyStatistics } from '../../api/statistics'
|
import { getMonthlyStatistics } from '../../api/statistics'
|
||||||
|
import { getBudget } from '../../api/budget'
|
||||||
import { formatDate } from '../../utils/date'
|
import { formatDate } from '../../utils/date'
|
||||||
import { useUserStore } from '../../store/user'
|
import { useUserStore } from '../../store/user'
|
||||||
|
import BudgetPieChart from '../../components/BudgetPieChart/BudgetPieChart.vue'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const bills = ref([])
|
const bills = ref([])
|
||||||
const monthlyIncome = ref(0)
|
const monthlyIncome = ref(0)
|
||||||
const monthlyExpense = ref(0)
|
const monthlyExpense = ref(0)
|
||||||
|
const budget = ref({
|
||||||
|
amount: 0,
|
||||||
|
usedAmount: 0,
|
||||||
|
remainingAmount: 0,
|
||||||
|
remainingDaily: 0
|
||||||
|
})
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const groupedBills = computed(() => {
|
const groupedBills = computed(() => {
|
||||||
@ -108,6 +144,22 @@ const loadMonthlyStatistics = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadBudget = async () => {
|
||||||
|
try {
|
||||||
|
const data = await getBudget()
|
||||||
|
if (data) {
|
||||||
|
budget.value = {
|
||||||
|
amount: data.amount || 0,
|
||||||
|
usedAmount: data.usedAmount || 0,
|
||||||
|
remainingAmount: data.remainingAmount || 0,
|
||||||
|
remainingDaily: data.remainingDaily || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载预算失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const viewBill = (bill) => {
|
const viewBill = (bill) => {
|
||||||
// 跳转到账单编辑页面
|
// 跳转到账单编辑页面
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
@ -122,9 +174,17 @@ const goToAccount = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const goToBudget = () => {
|
||||||
|
// 跳转到预算管理页面
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/budget/budget'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const onPullDownRefresh = () => {
|
const onPullDownRefresh = () => {
|
||||||
loadBills()
|
loadBills()
|
||||||
loadMonthlyStatistics()
|
loadMonthlyStatistics()
|
||||||
|
loadBudget()
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
@ -138,15 +198,17 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
loadBills()
|
loadBills()
|
||||||
loadMonthlyStatistics()
|
loadMonthlyStatistics()
|
||||||
|
loadBudget()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听刷新事件
|
// 监听刷新事件
|
||||||
uni.$on('refreshBills', () => {
|
uni.$on('refreshBills', () => {
|
||||||
loadBills()
|
loadBills()
|
||||||
loadMonthlyStatistics()
|
loadMonthlyStatistics()
|
||||||
|
loadBudget()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -193,7 +255,12 @@ export default {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-btn {
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
width: 60rpx;
|
width: 60rpx;
|
||||||
height: 60rpx;
|
height: 60rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -205,22 +272,24 @@ export default {
|
|||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-btn:active {
|
.action-btn:active {
|
||||||
background: rgba(255, 255, 255, 0.3);
|
background: rgba(255, 255, 255, 0.3);
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-icon {
|
.action-icon {
|
||||||
font-size: 36rpx;
|
font-size: 36rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
.summary-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-item {
|
.summary-item {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
@ -236,8 +305,60 @@ export default {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.amount.income {
|
||||||
|
color: #51cf66;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount.expense {
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-section {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 30rpx 20rpx 20rpx;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-chart-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 25rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-stats-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-stat-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 20rpx 15rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 22rpx;
|
||||||
|
opacity: 0.85;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
display: block;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.bill-list {
|
.bill-list {
|
||||||
padding: 20rpx;
|
padding: 20rpx;
|
||||||
|
height: calc(100vh - 520rpx - env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
.bill-group {
|
.bill-group {
|
||||||
|
|||||||
89
store/app.js
Normal file
89
store/app.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
// 应用状态管理
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useAppStore = defineStore('app', {
|
||||||
|
state: () => ({
|
||||||
|
// 登录状态
|
||||||
|
isLoggedIn: !!uni.getStorageSync('token'),
|
||||||
|
|
||||||
|
// 连接状态
|
||||||
|
isOnline: true,
|
||||||
|
|
||||||
|
// 同步状态
|
||||||
|
syncStatus: 'synced', // synced, syncing, syncFailed
|
||||||
|
|
||||||
|
// 离线模式
|
||||||
|
isOfflineMode: false,
|
||||||
|
|
||||||
|
// 最后同步时间
|
||||||
|
lastSyncTime: uni.getStorageSync('lastSyncTime') || null
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
// 检查是否为离线用户
|
||||||
|
isOfflineUser: (state) => {
|
||||||
|
const userInfo = uni.getStorageSync('userInfo') || {}
|
||||||
|
return userInfo.isOffline === true || userInfo.userId === 'local_user'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 检查OCR功能是否可用
|
||||||
|
isOcrAvailable: (state) => {
|
||||||
|
return !state.isOfflineMode && state.isOnline
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
// 设置登录状态
|
||||||
|
setLoggedIn(status) {
|
||||||
|
this.isLoggedIn = status
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置连接状态
|
||||||
|
setOnline(status) {
|
||||||
|
this.isOnline = status
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置同步状态
|
||||||
|
setSyncStatus(status) {
|
||||||
|
this.syncStatus = status
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置离线模式
|
||||||
|
setOfflineMode(status) {
|
||||||
|
this.isOfflineMode = status
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置最后同步时间
|
||||||
|
setLastSyncTime(time) {
|
||||||
|
this.lastSyncTime = time
|
||||||
|
uni.setStorageSync('lastSyncTime', time)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化应用状态
|
||||||
|
initAppStatus() {
|
||||||
|
// 检查登录状态
|
||||||
|
this.isLoggedIn = !!uni.getStorageSync('token')
|
||||||
|
|
||||||
|
// 检查是否为离线用户
|
||||||
|
const userInfo = uni.getStorageSync('userInfo') || {}
|
||||||
|
this.isOfflineMode = userInfo.isOffline === true || userInfo.userId === 'local_user'
|
||||||
|
|
||||||
|
// 检查网络状态
|
||||||
|
uni.getNetworkType({
|
||||||
|
success: (res) => {
|
||||||
|
this.isOnline = res.networkType !== 'none'
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
this.isOnline = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 监听网络状态变化
|
||||||
|
watchNetworkStatus() {
|
||||||
|
uni.onNetworkStatusChange((res) => {
|
||||||
|
this.isOnline = res.isConnected
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
396
utils/api-adapter.js
Normal file
396
utils/api-adapter.js
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
// API适配层,用于路由请求到在线API或本地数据库
|
||||||
|
|
||||||
|
import { db, createOfflineUser } from './db'
|
||||||
|
import * as onlineAuthApi from '../api/auth'
|
||||||
|
import * as onlineBillApi from '../api/bill'
|
||||||
|
import * as onlineCategoryApi from '../api/category'
|
||||||
|
import * as onlineAccountApi from '../api/account'
|
||||||
|
import * as onlineBudgetApi from '../api/budget'
|
||||||
|
import * as onlineOcrApi from '../api/ocr'
|
||||||
|
|
||||||
|
// 检查用户是否为离线用户
|
||||||
|
function isOfflineUser() {
|
||||||
|
const userInfo = uni.getStorageSync('userInfo') || {}
|
||||||
|
return userInfo.isOffline === true || userInfo.userId === 'local_user'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户ID
|
||||||
|
function getCurrentUserId() {
|
||||||
|
const userInfo = uni.getStorageSync('userInfo') || {}
|
||||||
|
return userInfo.userId || 'local_user'
|
||||||
|
}
|
||||||
|
|
||||||
|
// API适配层实现
|
||||||
|
const apiAdapter = {
|
||||||
|
// 认证相关API
|
||||||
|
auth: {
|
||||||
|
// 登录
|
||||||
|
login: async (data) => {
|
||||||
|
return onlineAuthApi.login(data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 注册
|
||||||
|
register: async (data) => {
|
||||||
|
return onlineAuthApi.register(data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 离线登录
|
||||||
|
offlineLogin: async () => {
|
||||||
|
try {
|
||||||
|
// 创建或获取离线用户
|
||||||
|
const userId = await createOfflineUser()
|
||||||
|
|
||||||
|
// 设置用户信息
|
||||||
|
const userInfo = {
|
||||||
|
userId,
|
||||||
|
username: '离线用户',
|
||||||
|
nickname: '离线用户',
|
||||||
|
isOffline: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到本地存储
|
||||||
|
uni.setStorageSync('userInfo', userInfo)
|
||||||
|
uni.setStorageSync('token', 'offline_token')
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: 'offline_token',
|
||||||
|
username: '离线用户',
|
||||||
|
nickname: '离线用户',
|
||||||
|
userId
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('离线登录失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 账单相关API
|
||||||
|
bill: {
|
||||||
|
// 获取账单列表
|
||||||
|
getBills: async (params) => {
|
||||||
|
if (isOfflineUser()) {
|
||||||
|
// 离线模式:从本地数据库获取
|
||||||
|
const userId = getCurrentUserId()
|
||||||
|
const sql = `SELECT * FROM bill WHERE user_id = ? ORDER BY bill_date DESC`
|
||||||
|
return db.query(sql, [userId])
|
||||||
|
} else {
|
||||||
|
// 在线模式:从服务器获取
|
||||||
|
return onlineBillApi.getBills(params)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 创建账单
|
||||||
|
createBill: async (data) => {
|
||||||
|
if (isOfflineUser()) {
|
||||||
|
// 离线模式:保存到本地数据库
|
||||||
|
const userId = getCurrentUserId()
|
||||||
|
const sql = `INSERT INTO bill (user_id, category_id, amount, description, bill_date, type)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)`
|
||||||
|
const result = await db.update(sql, [
|
||||||
|
userId,
|
||||||
|
data.categoryId,
|
||||||
|
data.amount,
|
||||||
|
data.description || '',
|
||||||
|
data.billDate,
|
||||||
|
data.type
|
||||||
|
])
|
||||||
|
return { success: true, affectedRows: result }
|
||||||
|
} else {
|
||||||
|
// 在线模式:保存到服务器
|
||||||
|
return onlineBillApi.createBill(data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新账单
|
||||||
|
updateBill: async (id, data) => {
|
||||||
|
if (isOfflineUser()) {
|
||||||
|
// 离线模式:更新本地数据库
|
||||||
|
const sql = `UPDATE bill SET category_id = ?, amount = ?, description = ?, bill_date = ?, type = ?, is_synced = 0
|
||||||
|
WHERE id = ?`
|
||||||
|
const result = await db.update(sql, [
|
||||||
|
data.categoryId,
|
||||||
|
data.amount,
|
||||||
|
data.description || '',
|
||||||
|
data.billDate,
|
||||||
|
data.type,
|
||||||
|
id
|
||||||
|
])
|
||||||
|
return { success: true, affectedRows: result }
|
||||||
|
} else {
|
||||||
|
// 在线模式:更新服务器
|
||||||
|
return onlineBillApi.updateBill(id, data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除账单
|
||||||
|
deleteBill: async (id) => {
|
||||||
|
if (isOfflineUser()) {
|
||||||
|
// 离线模式:从本地数据库删除
|
||||||
|
const sql = `DELETE FROM bill WHERE id = ?`
|
||||||
|
const result = await db.update(sql, [id])
|
||||||
|
return { success: true, affectedRows: result }
|
||||||
|
} else {
|
||||||
|
// 在线模式:从服务器删除
|
||||||
|
return onlineBillApi.deleteBill(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 分类相关API
|
||||||
|
category: {
|
||||||
|
// 获取分类列表
|
||||||
|
getCategories: async (params) => {
|
||||||
|
if (isOfflineUser()) {
|
||||||
|
// 离线模式:从本地数据库获取
|
||||||
|
const userId = getCurrentUserId()
|
||||||
|
const type = params.type || ''
|
||||||
|
let sql = `SELECT * FROM category WHERE user_id IS NULL OR user_id = ?`
|
||||||
|
const sqlParams = [userId]
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
sql += ` AND type = ?`
|
||||||
|
sqlParams.push(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += ` ORDER BY type, sort_order ASC`
|
||||||
|
return db.query(sql, sqlParams)
|
||||||
|
} else {
|
||||||
|
// 在线模式:从服务器获取
|
||||||
|
return onlineCategoryApi.getCategories(params)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 创建分类
|
||||||
|
createCategory: async (data) => {
|
||||||
|
if (isOfflineUser()) {
|
||||||
|
// 离线模式:保存到本地数据库
|
||||||
|
const userId = getCurrentUserId()
|
||||||
|
const sql = `INSERT INTO category (user_id, name, icon, type, sort_order)
|
||||||
|
VALUES (?, ?, ?, ?, ?)`
|
||||||
|
const result = await db.update(sql, [
|
||||||
|
userId,
|
||||||
|
data.name,
|
||||||
|
data.icon || '📦',
|
||||||
|
data.type,
|
||||||
|
data.sortOrder || 0
|
||||||
|
])
|
||||||
|
return { success: true, affectedRows: result }
|
||||||
|
} else {
|
||||||
|
// 在线模式:保存到服务器
|
||||||
|
return onlineCategoryApi.createCategory(data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新分类
|
||||||
|
updateCategory: async (id, data) => {
|
||||||
|
if (isOfflineUser()) {
|
||||||
|
// 离线模式:更新本地数据库
|
||||||
|
const sql = `UPDATE category SET name = ?, icon = ?, type = ?, sort_order = ?
|
||||||
|
WHERE id = ?`
|
||||||
|
const result = await db.update(sql, [
|
||||||
|
data.name,
|
||||||
|
data.icon,
|
||||||
|
data.type,
|
||||||
|
data.sortOrder || 0,
|
||||||
|
id
|
||||||
|
])
|
||||||
|
return { success: true, affectedRows: result }
|
||||||
|
} else {
|
||||||
|
// 在线模式:更新服务器
|
||||||
|
return onlineCategoryApi.updateCategory(id, data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除分类
|
||||||
|
deleteCategory: async (id) => {
|
||||||
|
if (isOfflineUser()) {
|
||||||
|
// 离线模式:从本地数据库删除
|
||||||
|
const sql = `DELETE FROM category WHERE id = ? AND user_id = ?`
|
||||||
|
const result = await db.update(sql, [id, getCurrentUserId()])
|
||||||
|
return { success: true, affectedRows: result }
|
||||||
|
} else {
|
||||||
|
// 在线模式:从服务器删除
|
||||||
|
return onlineCategoryApi.deleteCategory(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 账户相关API
|
||||||
|
account: {
|
||||||
|
// 获取账户信息
|
||||||
|
getAccount: async (params) => {
|
||||||
|
if (isOfflineUser()) {
|
||||||
|
// 离线模式:从本地数据库获取
|
||||||
|
const userId = getCurrentUserId()
|
||||||
|
const sql = `SELECT * FROM account WHERE user_id = ?`
|
||||||
|
const accounts = await db.query(sql, [userId])
|
||||||
|
|
||||||
|
if (accounts.length > 0) {
|
||||||
|
// 获取收支统计
|
||||||
|
const incomeSql = `SELECT SUM(amount) as totalIncome FROM bill WHERE user_id = ? AND type = 2`
|
||||||
|
const expenseSql = `SELECT SUM(amount) as totalExpense FROM bill WHERE user_id = ? AND type = 1`
|
||||||
|
|
||||||
|
const incomeResult = await db.query(incomeSql, [userId])
|
||||||
|
const expenseResult = await db.query(expenseSql, [userId])
|
||||||
|
|
||||||
|
const totalIncome = incomeResult[0].totalIncome || 0
|
||||||
|
const totalExpense = expenseResult[0].totalExpense || 0
|
||||||
|
const initialBalance = accounts[0].initial_balance || 0
|
||||||
|
const balance = initialBalance + totalIncome - totalExpense
|
||||||
|
|
||||||
|
return {
|
||||||
|
...accounts[0],
|
||||||
|
totalIncome,
|
||||||
|
totalExpense,
|
||||||
|
balance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
// 在线模式:从服务器获取
|
||||||
|
return onlineAccountApi.getAccount(params)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新账户信息
|
||||||
|
updateAccount: async (data) => {
|
||||||
|
if (isOfflineUser()) {
|
||||||
|
// 离线模式:更新本地数据库
|
||||||
|
const userId = getCurrentUserId()
|
||||||
|
const sql = `UPDATE account SET name = ?, initial_balance = ? WHERE user_id = ?`
|
||||||
|
const result = await db.update(sql, [
|
||||||
|
data.name,
|
||||||
|
data.initialBalance || 0,
|
||||||
|
userId
|
||||||
|
])
|
||||||
|
|
||||||
|
// 返回更新后的账户信息
|
||||||
|
return apiAdapter.account.getAccount()
|
||||||
|
} else {
|
||||||
|
// 在线模式:更新服务器
|
||||||
|
return onlineAccountApi.updateAccount(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 预算相关API
|
||||||
|
budget: {
|
||||||
|
// 获取预算信息
|
||||||
|
getBudget: async (params) => {
|
||||||
|
if (isOfflineUser()) {
|
||||||
|
// 离线模式:从本地数据库获取
|
||||||
|
const userId = getCurrentUserId()
|
||||||
|
const now = new Date()
|
||||||
|
const year = params.year || now.getFullYear()
|
||||||
|
const month = params.month || (now.getMonth() + 1)
|
||||||
|
|
||||||
|
const sql = `SELECT * FROM budget WHERE user_id = ? AND year = ? AND month = ?`
|
||||||
|
const budgets = await db.query(sql, [userId, year, month])
|
||||||
|
|
||||||
|
if (budgets.length > 0) {
|
||||||
|
return budgets[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId,
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
amount: 0.00
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 在线模式:从服务器获取
|
||||||
|
return onlineBudgetApi.getBudget(params)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置预算
|
||||||
|
setBudget: async (data) => {
|
||||||
|
if (isOfflineUser()) {
|
||||||
|
// 离线模式:保存到本地数据库
|
||||||
|
const userId = getCurrentUserId()
|
||||||
|
const year = data.year
|
||||||
|
const month = data.month
|
||||||
|
const amount = data.amount || 0
|
||||||
|
|
||||||
|
// 检查是否已存在该月份预算
|
||||||
|
const checkSql = `SELECT * FROM budget WHERE user_id = ? AND year = ? AND month = ?`
|
||||||
|
const budgets = await db.query(checkSql, [userId, year, month])
|
||||||
|
|
||||||
|
let result
|
||||||
|
if (budgets.length > 0) {
|
||||||
|
// 更新现有预算
|
||||||
|
const sql = `UPDATE budget SET amount = ? WHERE user_id = ? AND year = ? AND month = ?`
|
||||||
|
result = await db.update(sql, [amount, userId, year, month])
|
||||||
|
} else {
|
||||||
|
// 创建新预算
|
||||||
|
const sql = `INSERT INTO budget (user_id, year, month, amount) VALUES (?, ?, ?, ?)`
|
||||||
|
result = await db.update(sql, [userId, year, month, amount])
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, affectedRows: result }
|
||||||
|
} else {
|
||||||
|
// 在线模式:保存到服务器
|
||||||
|
return onlineBudgetApi.setBudget(data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取预算结算信息
|
||||||
|
getBudgetSettlement: async (params) => {
|
||||||
|
if (isOfflineUser()) {
|
||||||
|
// 离线模式:从本地数据库计算
|
||||||
|
const userId = getCurrentUserId()
|
||||||
|
const now = new Date()
|
||||||
|
let year = now.getFullYear()
|
||||||
|
let month = now.getMonth()
|
||||||
|
|
||||||
|
// 如果当前是1月,上月是去年12月
|
||||||
|
if (month === 0) {
|
||||||
|
year--
|
||||||
|
month = 12
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取上月预算
|
||||||
|
const budgetSql = `SELECT * FROM budget WHERE user_id = ? AND year = ? AND month = ?`
|
||||||
|
const budgets = await db.query(budgetSql, [userId, year, month])
|
||||||
|
const budgetAmount = budgets[0]?.amount || 0
|
||||||
|
|
||||||
|
// 计算上月总支出
|
||||||
|
const expenseSql = `SELECT SUM(amount) as totalExpense FROM bill
|
||||||
|
WHERE user_id = ? AND type = 1
|
||||||
|
AND bill_date BETWEEN ? AND ?`
|
||||||
|
const startDate = `${year}-${month.toString().padStart(2, '0')}-01`
|
||||||
|
const endDate = new Date(year, month, 0).toISOString().split('T')[0]
|
||||||
|
|
||||||
|
const expenseResult = await db.query(expenseSql, [userId, startDate, endDate])
|
||||||
|
const totalExpense = expenseResult[0].totalExpense || 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
budgetAmount,
|
||||||
|
totalExpense,
|
||||||
|
remainingAmount: budgetAmount - totalExpense
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 在线模式:从服务器获取
|
||||||
|
return onlineBudgetApi.getBudgetSettlement(params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// OCR相关API
|
||||||
|
ocr: {
|
||||||
|
// 识别图片
|
||||||
|
recognizeImage: async (filePath) => {
|
||||||
|
// OCR功能仅在线用户可用
|
||||||
|
if (isOfflineUser()) {
|
||||||
|
throw new Error('OCR功能仅在线用户可用')
|
||||||
|
}
|
||||||
|
return onlineOcrApi.recognizeImage(filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default apiAdapter
|
||||||
325
utils/db.js
Normal file
325
utils/db.js
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
// 本地数据库管理模块
|
||||||
|
|
||||||
|
const DB_NAME = 'accounting.db'
|
||||||
|
const DB_VERSION = 1
|
||||||
|
let database = null
|
||||||
|
|
||||||
|
// 跨平台数据库兼容性处理
|
||||||
|
function openDatabaseSync(name, version, displayName, estimatedSize) {
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
// App环境使用plus.sqlite
|
||||||
|
return plus.sqlite.openSQLiteSync({
|
||||||
|
name: name,
|
||||||
|
path: `_doc/${name}`
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef H5
|
||||||
|
// H5环境使用Web SQL(如果支持)
|
||||||
|
if (window.openDatabase) {
|
||||||
|
return window.openDatabase(name, version, displayName, estimatedSize)
|
||||||
|
} else {
|
||||||
|
throw new Error('浏览器不支持Web SQL数据库,请使用支持的浏览器或App版本')
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef MP
|
||||||
|
// 小程序环境暂不支持本地SQLite数据库
|
||||||
|
throw new Error('小程序环境暂不支持离线数据库功能')
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化数据库
|
||||||
|
export async function initDatabase() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// 打开数据库
|
||||||
|
database = openDatabaseSync(DB_NAME, DB_VERSION, '记账数据库', 10 * 1024 * 1024)
|
||||||
|
|
||||||
|
// 创建表结构
|
||||||
|
createTables().then(() => {
|
||||||
|
// 插入预设分类数据
|
||||||
|
insertDefaultCategories().then(() => {
|
||||||
|
resolve(true)
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('插入预设分类失败:', err)
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('创建表结构失败:', err)
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化数据库失败:', error)
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建表结构
|
||||||
|
async function createTables() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const createTableSqls = [
|
||||||
|
// 用户表
|
||||||
|
`CREATE TABLE IF NOT EXISTS user (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
username TEXT,
|
||||||
|
nickname TEXT,
|
||||||
|
is_offline INTEGER DEFAULT 0,
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)`,
|
||||||
|
|
||||||
|
// 分类表
|
||||||
|
`CREATE TABLE IF NOT EXISTS category (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id TEXT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
icon TEXT,
|
||||||
|
type INTEGER NOT NULL,
|
||||||
|
sort_order INTEGER DEFAULT 0,
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)`,
|
||||||
|
|
||||||
|
// 账单表
|
||||||
|
`CREATE TABLE IF NOT EXISTS bill (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
category_id INTEGER NOT NULL,
|
||||||
|
amount REAL NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
bill_date DATE NOT NULL,
|
||||||
|
type INTEGER NOT NULL,
|
||||||
|
is_synced INTEGER DEFAULT 0,
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)`,
|
||||||
|
|
||||||
|
// 账户表
|
||||||
|
`CREATE TABLE IF NOT EXISTS account (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
initial_balance REAL NOT NULL DEFAULT 0.00,
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(user_id, name)
|
||||||
|
)`,
|
||||||
|
|
||||||
|
// 预算表
|
||||||
|
`CREATE TABLE IF NOT EXISTS budget (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
year INTEGER NOT NULL,
|
||||||
|
month INTEGER NOT NULL,
|
||||||
|
amount REAL NOT NULL DEFAULT 0.00,
|
||||||
|
is_synced INTEGER DEFAULT 0,
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(user_id, year, month)
|
||||||
|
)`
|
||||||
|
]
|
||||||
|
|
||||||
|
// 执行事务创建所有表
|
||||||
|
database.transaction((tx) => {
|
||||||
|
for (const sql of createTableSqls) {
|
||||||
|
tx.executeSql(sql)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
resolve(true)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建表结构失败:', error)
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插入预设分类数据
|
||||||
|
async function insertDefaultCategories() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// 检查是否已存在预设分类
|
||||||
|
const checkSql = 'SELECT COUNT(*) as count FROM category WHERE user_id IS NULL'
|
||||||
|
database.transaction((tx) => {
|
||||||
|
tx.executeSql(checkSql, [], (tx, result) => {
|
||||||
|
if (result.rows[0].count === 0) {
|
||||||
|
// 插入预设支出分类
|
||||||
|
const expenseCategories = [
|
||||||
|
{ name: '餐饮', icon: '🍔', type: 1, sort_order: 1 },
|
||||||
|
{ name: '交通', icon: '🚗', type: 1, sort_order: 2 },
|
||||||
|
{ name: '购物', icon: '🛍️', type: 1, sort_order: 3 },
|
||||||
|
{ name: '娱乐', icon: '🎬', type: 1, sort_order: 4 },
|
||||||
|
{ name: '医疗', icon: '🏥', type: 1, sort_order: 5 },
|
||||||
|
{ name: '教育', icon: '📚', type: 1, sort_order: 6 },
|
||||||
|
{ name: '住房', icon: '🏠', type: 1, sort_order: 7 },
|
||||||
|
{ name: '水电', icon: '💡', type: 1, sort_order: 8 },
|
||||||
|
{ name: '通讯', icon: '📱', type: 1, sort_order: 9 },
|
||||||
|
{ name: '其他', icon: '📦', type: 1, sort_order: 10 }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 插入预设收入分类
|
||||||
|
const incomeCategories = [
|
||||||
|
{ name: '工资', icon: '💰', type: 2, sort_order: 1 },
|
||||||
|
{ name: '奖金', icon: '🎁', type: 2, sort_order: 2 },
|
||||||
|
{ name: '投资', icon: '📈', type: 2, sort_order: 3 },
|
||||||
|
{ name: '兼职', icon: '💼', type: 2, sort_order: 4 },
|
||||||
|
{ name: '其他', icon: '📦', type: 2, sort_order: 5 }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 执行事务插入所有预设分类
|
||||||
|
database.transaction((tx) => {
|
||||||
|
for (const cat of expenseCategories) {
|
||||||
|
const sql = `INSERT INTO category (user_id, name, icon, type, sort_order) VALUES (NULL, ?, ?, ?, ?)`
|
||||||
|
tx.executeSql(sql, [cat.name, cat.icon, cat.type, cat.sort_order])
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cat of incomeCategories) {
|
||||||
|
const sql = `INSERT INTO category (user_id, name, icon, type, sort_order) VALUES (NULL, ?, ?, ?, ?)`
|
||||||
|
tx.executeSql(sql, [cat.name, cat.icon, cat.type, cat.sort_order])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
resolve(true)
|
||||||
|
}, (tx, error) => {
|
||||||
|
console.error('检查预设分类失败:', error)
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('插入预设分类失败:', error)
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建离线用户
|
||||||
|
export async function createOfflineUser() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// 检查数据库是否已初始化
|
||||||
|
if (!database) {
|
||||||
|
console.error('数据库未初始化,正在初始化...')
|
||||||
|
initDatabase().then(() => {
|
||||||
|
// 数据库初始化成功后,递归调用自己
|
||||||
|
createOfflineUser().then(resolve).catch(reject)
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('数据库初始化失败:', err)
|
||||||
|
reject(new Error('数据库初始化失败: ' + err.message))
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = 'local_user'
|
||||||
|
const username = '离线用户'
|
||||||
|
const nickname = '离线用户'
|
||||||
|
|
||||||
|
// 检查是否已存在离线用户
|
||||||
|
const checkSql = 'SELECT * FROM user WHERE id = ?'
|
||||||
|
database.transaction((tx) => {
|
||||||
|
tx.executeSql(checkSql, [userId], (tx, result) => {
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
// 创建离线用户
|
||||||
|
const insertSql = `INSERT INTO user (id, username, nickname, is_offline) VALUES (?, ?, ?, 1)`
|
||||||
|
tx.executeSql(insertSql, [userId, username, nickname], (tx, result) => {
|
||||||
|
// 创建默认账户
|
||||||
|
const accountSql = `INSERT INTO account (user_id, name, initial_balance) VALUES (?, '默认账户', 0.00)`
|
||||||
|
tx.executeSql(accountSql, [userId], (tx, result) => {
|
||||||
|
resolve(userId)
|
||||||
|
}, (tx, error) => {
|
||||||
|
console.error('创建默认账户失败:', error)
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
}, (tx, error) => {
|
||||||
|
console.error('创建离线用户失败:', error)
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
resolve(userId)
|
||||||
|
}
|
||||||
|
}, (tx, error) => {
|
||||||
|
console.error('检查离线用户失败:', error)
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建离线用户失败:', error)
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据库操作封装
|
||||||
|
const db = {
|
||||||
|
// 执行查询
|
||||||
|
query: (sql, params = []) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
database.transaction((tx) => {
|
||||||
|
tx.executeSql(sql, params, (tx, result) => {
|
||||||
|
const rows = []
|
||||||
|
for (let i = 0; i < result.rows.length; i++) {
|
||||||
|
rows.push(result.rows[i])
|
||||||
|
}
|
||||||
|
resolve(rows)
|
||||||
|
}, (tx, error) => {
|
||||||
|
console.error('查询失败:', error)
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('查询失败:', error)
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 执行更新(INSERT, UPDATE, DELETE)
|
||||||
|
update: (sql, params = []) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
database.transaction((tx) => {
|
||||||
|
tx.executeSql(sql, params, (tx, result) => {
|
||||||
|
resolve(result.rowsAffected)
|
||||||
|
}, (tx, error) => {
|
||||||
|
console.error('更新失败:', error)
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新失败:', error)
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 执行事务
|
||||||
|
transaction: (callback) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
database.transaction((tx) => {
|
||||||
|
callback(tx)
|
||||||
|
resolve(true)
|
||||||
|
}, (error) => {
|
||||||
|
console.error('事务执行失败:', error)
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('事务执行失败:', error)
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取数据库实例
|
||||||
|
getInstance: () => {
|
||||||
|
if (!database) {
|
||||||
|
throw new Error('数据库未初始化')
|
||||||
|
}
|
||||||
|
return database
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { db }
|
||||||
373
utils/sync.js
Normal file
373
utils/sync.js
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
// 数据同步功能实现
|
||||||
|
|
||||||
|
import { db } from './db'
|
||||||
|
import * as onlineBillApi from '../api/bill'
|
||||||
|
import * as onlineCategoryApi from '../api/category'
|
||||||
|
import * as onlineAccountApi from '../api/account'
|
||||||
|
import * as onlineBudgetApi from '../api/budget'
|
||||||
|
|
||||||
|
// 获取当前用户ID
|
||||||
|
function getCurrentUserId() {
|
||||||
|
const userInfo = uni.getStorageSync('userInfo') || {}
|
||||||
|
return userInfo.userId || 'local_user'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为离线用户
|
||||||
|
function isOfflineUser() {
|
||||||
|
const userInfo = uni.getStorageSync('userInfo') || {}
|
||||||
|
return userInfo.isOffline === true || userInfo.userId === 'local_user'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 账单同步逻辑
|
||||||
|
const syncBills = async () => {
|
||||||
|
if (isOfflineUser()) {
|
||||||
|
throw new Error('离线用户无法同步数据')
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = getCurrentUserId()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 获取本地未同步的账单
|
||||||
|
const localBillsSql = `SELECT * FROM bill WHERE user_id = ? AND is_synced = 0`
|
||||||
|
const localBills = await db.query(localBillsSql, [userId])
|
||||||
|
|
||||||
|
// 2. 获取服务器端账单
|
||||||
|
const serverBills = await onlineBillApi.getBills({ userId })
|
||||||
|
|
||||||
|
// 3. 比较并同步数据
|
||||||
|
for (const localBill of localBills) {
|
||||||
|
// 检查服务器是否已有相同账单
|
||||||
|
const existingBill = serverBills.find(bill => {
|
||||||
|
return bill.amount === localBill.amount &&
|
||||||
|
bill.type === localBill.type &&
|
||||||
|
bill.bill_date === localBill.bill_date &&
|
||||||
|
bill.description === localBill.description &&
|
||||||
|
bill.category_id === localBill.category_id
|
||||||
|
})
|
||||||
|
|
||||||
|
if (existingBill) {
|
||||||
|
// 已有相同账单,标记为已同步
|
||||||
|
const updateSql = `UPDATE bill SET is_synced = 1 WHERE id = ?`
|
||||||
|
await db.update(updateSql, [localBill.id])
|
||||||
|
} else {
|
||||||
|
// 没有相同账单,上传到服务器
|
||||||
|
await onlineBillApi.createBill({
|
||||||
|
categoryId: localBill.category_id,
|
||||||
|
amount: localBill.amount,
|
||||||
|
description: localBill.description,
|
||||||
|
billDate: localBill.bill_date,
|
||||||
|
type: localBill.type
|
||||||
|
})
|
||||||
|
|
||||||
|
// 标记为已同步
|
||||||
|
const updateSql = `UPDATE bill SET is_synced = 1 WHERE id = ?`
|
||||||
|
await db.update(updateSql, [localBill.id])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 将服务器端数据同步到本地
|
||||||
|
for (const serverBill of serverBills) {
|
||||||
|
// 检查本地是否已有相同账单
|
||||||
|
const checkSql = `SELECT * FROM bill WHERE user_id = ? AND amount = ? AND type = ? AND bill_date = ? AND description = ? AND category_id = ?`
|
||||||
|
const existingBills = await db.query(checkSql, [
|
||||||
|
userId,
|
||||||
|
serverBill.amount,
|
||||||
|
serverBill.type,
|
||||||
|
serverBill.bill_date,
|
||||||
|
serverBill.description,
|
||||||
|
serverBill.category_id
|
||||||
|
])
|
||||||
|
|
||||||
|
if (existingBills.length === 0) {
|
||||||
|
// 本地没有相同账单,添加到本地
|
||||||
|
const insertSql = `INSERT INTO bill (user_id, category_id, amount, description, bill_date, type, is_synced)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, 1)`
|
||||||
|
await db.update(insertSql, [
|
||||||
|
userId,
|
||||||
|
serverBill.category_id,
|
||||||
|
serverBill.amount,
|
||||||
|
serverBill.description,
|
||||||
|
serverBill.bill_date,
|
||||||
|
serverBill.type
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, message: '账单同步成功', syncedCount: localBills.length }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('同步账单失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分类同步逻辑
|
||||||
|
const syncCategories = async () => {
|
||||||
|
if (isOfflineUser()) {
|
||||||
|
throw new Error('离线用户无法同步数据')
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = getCurrentUserId()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 获取本地用户自定义分类
|
||||||
|
const localCategoriesSql = `SELECT * FROM category WHERE user_id = ?`
|
||||||
|
const localCategories = await db.query(localCategoriesSql, [userId])
|
||||||
|
|
||||||
|
// 2. 获取服务器端分类
|
||||||
|
const serverCategories = await onlineCategoryApi.getCategories({ userId })
|
||||||
|
|
||||||
|
// 3. 比较并同步数据
|
||||||
|
for (const localCategory of localCategories) {
|
||||||
|
// 检查服务器是否已有相同分类
|
||||||
|
const existingCategory = serverCategories.find(category => {
|
||||||
|
return category.name === localCategory.name &&
|
||||||
|
category.type === localCategory.type
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!existingCategory) {
|
||||||
|
// 没有相同分类,上传到服务器
|
||||||
|
await onlineCategoryApi.createCategory({
|
||||||
|
name: localCategory.name,
|
||||||
|
icon: localCategory.icon,
|
||||||
|
type: localCategory.type,
|
||||||
|
sortOrder: localCategory.sort_order
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 将服务器端分类同步到本地
|
||||||
|
for (const serverCategory of serverCategories) {
|
||||||
|
// 检查本地是否已有相同分类
|
||||||
|
const checkSql = `SELECT * FROM category WHERE user_id = ? AND name = ? AND type = ?`
|
||||||
|
const existingCategories = await db.query(checkSql, [
|
||||||
|
userId,
|
||||||
|
serverCategory.name,
|
||||||
|
serverCategory.type
|
||||||
|
])
|
||||||
|
|
||||||
|
if (existingCategories.length === 0) {
|
||||||
|
// 本地没有相同分类,添加到本地
|
||||||
|
const insertSql = `INSERT INTO category (user_id, name, icon, type, sort_order)
|
||||||
|
VALUES (?, ?, ?, ?, ?)`
|
||||||
|
await db.update(insertSql, [
|
||||||
|
userId,
|
||||||
|
serverCategory.name,
|
||||||
|
serverCategory.icon,
|
||||||
|
serverCategory.type,
|
||||||
|
serverCategory.sort_order
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, message: '分类同步成功' }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('同步分类失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 账户同步逻辑
|
||||||
|
const syncAccount = async () => {
|
||||||
|
if (isOfflineUser()) {
|
||||||
|
throw new Error('离线用户无法同步数据')
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = getCurrentUserId()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 获取本地账户信息
|
||||||
|
const localAccountSql = `SELECT * FROM account WHERE user_id = ?`
|
||||||
|
const localAccounts = await db.query(localAccountSql, [userId])
|
||||||
|
|
||||||
|
if (localAccounts.length === 0) {
|
||||||
|
return { success: true, message: '本地没有账户信息需要同步' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const localAccount = localAccounts[0]
|
||||||
|
|
||||||
|
// 2. 获取服务器端账户信息
|
||||||
|
const serverAccount = await onlineAccountApi.getAccount({ userId })
|
||||||
|
|
||||||
|
// 3. 比较并同步数据
|
||||||
|
if (serverAccount) {
|
||||||
|
// 服务器有账户信息,比较并更新
|
||||||
|
if (localAccount.name !== serverAccount.name || localAccount.initial_balance !== serverAccount.initial_balance) {
|
||||||
|
// 本地与服务器信息不一致,更新服务器端
|
||||||
|
await onlineAccountApi.updateAccount({
|
||||||
|
name: localAccount.name,
|
||||||
|
initialBalance: localAccount.initial_balance
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 服务器没有账户信息,创建新账户
|
||||||
|
await onlineAccountApi.updateAccount({
|
||||||
|
name: localAccount.name,
|
||||||
|
initialBalance: localAccount.initial_balance
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 将服务器端账户信息同步到本地
|
||||||
|
const updatedServerAccount = await onlineAccountApi.getAccount({ userId })
|
||||||
|
const updateSql = `UPDATE account SET name = ?, initial_balance = ? WHERE user_id = ?`
|
||||||
|
await db.update(updateSql, [
|
||||||
|
updatedServerAccount.name,
|
||||||
|
updatedServerAccount.initial_balance,
|
||||||
|
userId
|
||||||
|
])
|
||||||
|
|
||||||
|
return { success: true, message: '账户同步成功' }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('同步账户失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预算同步逻辑
|
||||||
|
const syncBudgets = async () => {
|
||||||
|
if (isOfflineUser()) {
|
||||||
|
throw new Error('离线用户无法同步数据')
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = getCurrentUserId()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 获取本地预算
|
||||||
|
const localBudgetsSql = `SELECT * FROM budget WHERE user_id = ?`
|
||||||
|
const localBudgets = await db.query(localBudgetsSql, [userId])
|
||||||
|
|
||||||
|
// 2. 获取服务器端预算
|
||||||
|
const serverBudget = await onlineBudgetApi.getBudget({ userId })
|
||||||
|
|
||||||
|
// 3. 比较并同步数据
|
||||||
|
for (const localBudget of localBudgets) {
|
||||||
|
// 检查服务器是否已有相同月份预算
|
||||||
|
const checkSql = `SELECT * FROM budget WHERE user_id = ? AND year = ? AND month = ?`
|
||||||
|
const existingBudgets = await db.query(checkSql, [
|
||||||
|
userId,
|
||||||
|
localBudget.year,
|
||||||
|
localBudget.month
|
||||||
|
])
|
||||||
|
|
||||||
|
if (existingBudgets.length === 0) {
|
||||||
|
// 没有相同月份预算,上传到服务器
|
||||||
|
await onlineBudgetApi.setBudget({
|
||||||
|
userId,
|
||||||
|
year: localBudget.year,
|
||||||
|
month: localBudget.month,
|
||||||
|
amount: localBudget.amount
|
||||||
|
})
|
||||||
|
} else if (existingBudgets[0].amount !== localBudget.amount) {
|
||||||
|
// 预算金额不一致,更新服务器
|
||||||
|
await onlineBudgetApi.setBudget({
|
||||||
|
userId,
|
||||||
|
year: localBudget.year,
|
||||||
|
month: localBudget.month,
|
||||||
|
amount: localBudget.amount
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 将服务器端预算同步到本地
|
||||||
|
if (serverBudget) {
|
||||||
|
// 检查本地是否已有相同月份预算
|
||||||
|
const checkSql = `SELECT * FROM budget WHERE user_id = ? AND year = ? AND month = ?`
|
||||||
|
const existingBudgets = await db.query(checkSql, [
|
||||||
|
userId,
|
||||||
|
serverBudget.year,
|
||||||
|
serverBudget.month
|
||||||
|
])
|
||||||
|
|
||||||
|
if (existingBudgets.length === 0) {
|
||||||
|
// 本地没有相同月份预算,添加到本地
|
||||||
|
const insertSql = `INSERT INTO budget (user_id, year, month, amount, is_synced)
|
||||||
|
VALUES (?, ?, ?, ?, 1)`
|
||||||
|
await db.update(insertSql, [
|
||||||
|
userId,
|
||||||
|
serverBudget.year,
|
||||||
|
serverBudget.month,
|
||||||
|
serverBudget.amount
|
||||||
|
])
|
||||||
|
} else if (existingBudgets[0].amount !== serverBudget.amount) {
|
||||||
|
// 预算金额不一致,更新本地
|
||||||
|
const updateSql = `UPDATE budget SET amount = ?, is_synced = 1 WHERE user_id = ? AND year = ? AND month = ?`
|
||||||
|
await db.update(updateSql, [
|
||||||
|
serverBudget.amount,
|
||||||
|
userId,
|
||||||
|
serverBudget.year,
|
||||||
|
serverBudget.month
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, message: '预算同步成功' }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('同步预算失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全量同步数据
|
||||||
|
const syncAllData = async () => {
|
||||||
|
if (isOfflineUser()) {
|
||||||
|
throw new Error('离线用户无法同步数据')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 同步账单
|
||||||
|
const billResult = await syncBills()
|
||||||
|
|
||||||
|
// 2. 同步分类
|
||||||
|
const categoryResult = await syncCategories()
|
||||||
|
|
||||||
|
// 3. 同步账户
|
||||||
|
const accountResult = await syncAccount()
|
||||||
|
|
||||||
|
// 4. 同步预算
|
||||||
|
const budgetResult = await syncBudgets()
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: '数据同步成功',
|
||||||
|
details: {
|
||||||
|
bills: billResult,
|
||||||
|
categories: categoryResult,
|
||||||
|
account: accountResult,
|
||||||
|
budgets: budgetResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('全量同步失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据同步模块导出
|
||||||
|
export const sync = {
|
||||||
|
// 全量同步
|
||||||
|
syncAll: syncAllData,
|
||||||
|
|
||||||
|
// 账单同步
|
||||||
|
syncBills,
|
||||||
|
|
||||||
|
// 分类同步
|
||||||
|
syncCategories,
|
||||||
|
|
||||||
|
// 账户同步
|
||||||
|
syncAccount,
|
||||||
|
|
||||||
|
// 预算同步
|
||||||
|
syncBudgets,
|
||||||
|
|
||||||
|
// 检查同步状态
|
||||||
|
checkSyncStatus: () => {
|
||||||
|
const userInfo = uni.getStorageSync('userInfo') || {}
|
||||||
|
const lastSyncTime = uni.getStorageSync('lastSyncTime') || null
|
||||||
|
|
||||||
|
return {
|
||||||
|
isOffline: userInfo.isOffline === true || userInfo.userId === 'local_user',
|
||||||
|
lastSyncTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default sync
|
||||||
Loading…
Reference in New Issue
Block a user