Compare commits
No commits in common. "master" and "dev-20251212-mbj-功能开发" have entirely different histories.
master
...
dev-202512
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,7 +6,6 @@ Thumbs.db
|
|||||||
ehthumbs.db
|
ehthumbs.db
|
||||||
Icon?
|
Icon?
|
||||||
desktop.ini
|
desktop.ini
|
||||||
*.apk
|
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
.vscode/*
|
.vscode/*
|
||||||
@ -131,4 +130,3 @@ package-lock.json # 可选:有些团队会提交它
|
|||||||
yarn.lock # 可选:通常建议提交 yarn.lock
|
yarn.lock # 可选:通常建议提交 yarn.lock
|
||||||
pnpm-lock.yaml # 可选:通常建议提交
|
pnpm-lock.yaml # 可选:通常建议提交
|
||||||
/node_modules/
|
/node_modules/
|
||||||
unpackage/
|
|
||||||
|
|||||||
@ -1,93 +0,0 @@
|
|||||||
# "我的"页面功能说明
|
|
||||||
|
|
||||||
## 功能概述
|
|
||||||
|
|
||||||
新增了"我的"页面,整合了以下功能:
|
|
||||||
- 用户信息展示
|
|
||||||
- 账户余额查看
|
|
||||||
- 预算进度展示
|
|
||||||
- 快捷功能入口
|
|
||||||
- 退出登录
|
|
||||||
|
|
||||||
## 页面结构
|
|
||||||
|
|
||||||
### 1. 用户信息卡片
|
|
||||||
- 显示用户头像(emoji)
|
|
||||||
- 显示用户昵称/用户名
|
|
||||||
- 显示用户ID
|
|
||||||
|
|
||||||
### 2. 账户余额卡片(可点击)
|
|
||||||
- 显示当前账户余额
|
|
||||||
- 显示总收入和总支出
|
|
||||||
- 点击可跳转到账户管理页面
|
|
||||||
|
|
||||||
### 3. 预算卡片(可点击)
|
|
||||||
- 显示本月预算进度条
|
|
||||||
- 显示预算金额、已用金额、剩余金额
|
|
||||||
- 点击可跳转到预算设置页面
|
|
||||||
|
|
||||||
### 4. 功能菜单
|
|
||||||
- 统计分析:跳转到统计页面
|
|
||||||
- 账户管理:跳转到账户管理页面
|
|
||||||
- 预算设置:跳转到预算设置页面
|
|
||||||
|
|
||||||
### 5. 退出登录按钮
|
|
||||||
- 点击后弹出确认对话框
|
|
||||||
- 确认后清除本地存储并跳转到登录页
|
|
||||||
|
|
||||||
## 技术特点
|
|
||||||
|
|
||||||
### 1. 滚动功能
|
|
||||||
- 使用 `<scroll-view>` 组件实现页面滚动
|
|
||||||
- 支持长内容的流畅滚动
|
|
||||||
- 底部留有安全区域,避免被导航栏遮挡
|
|
||||||
|
|
||||||
### 2. 样式设计
|
|
||||||
- 延续现有的金色渐变主题风格
|
|
||||||
- 使用 rpx 单位,适配不同屏幕尺寸
|
|
||||||
- 卡片式布局,清晰美观
|
|
||||||
- 添加阴影和圆角,提升视觉效果
|
|
||||||
|
|
||||||
### 3. 交互体验
|
|
||||||
- 卡片点击有视觉反馈(背景色变化)
|
|
||||||
- 按钮点击有缩放动画
|
|
||||||
- 进度条有动画效果
|
|
||||||
- 退出登录有二次确认
|
|
||||||
|
|
||||||
### 4. 数据加载
|
|
||||||
- 页面加载时自动获取用户信息、账户信息、预算信息
|
|
||||||
- 使用 computed 计算预算进度百分比
|
|
||||||
- 错误处理,避免数据加载失败导致页面崩溃
|
|
||||||
|
|
||||||
## 导航栏配置
|
|
||||||
|
|
||||||
在 `pages.json` 中添加了:
|
|
||||||
1. 页面路由配置
|
|
||||||
2. TabBar 配置(新增"我的"标签)
|
|
||||||
|
|
||||||
## 图标说明
|
|
||||||
|
|
||||||
临时使用了 home 图标作为占位符,建议后续替换为专门的"我的"图标。
|
|
||||||
|
|
||||||
详细说明请查看:`frontend/static/tabbar/ICON_GUIDE.md`
|
|
||||||
|
|
||||||
## 使用方法
|
|
||||||
|
|
||||||
1. 启动应用后,底部导航栏会显示4个标签:首页、记账、统计、我的
|
|
||||||
2. 点击"我的"标签即可进入该页面
|
|
||||||
3. 在页面中可以查看账户信息、预算信息,并快速跳转到相关功能页面
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. 确保用户已登录,否则用户信息可能显示不完整
|
|
||||||
2. 账户和预算数据需要后端API支持
|
|
||||||
3. 图标文件需要替换为合适的"我的"图标
|
|
||||||
4. 页面使用了 `scroll-view`,确保内容可以正常滚动
|
|
||||||
|
|
||||||
## 后续优化建议
|
|
||||||
|
|
||||||
1. 添加更多个人设置选项(如修改密码、修改昵称等)
|
|
||||||
2. 添加数据统计概览(如本月记账天数、最大单笔支出等)
|
|
||||||
3. 添加主题切换功能
|
|
||||||
4. 添加关于页面和帮助文档入口
|
|
||||||
5. 优化用户头像,支持上传自定义头像
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import { get, put } from '../utils/request'
|
|
||||||
|
|
||||||
// 获取账户信息
|
|
||||||
export function getAccount() {
|
|
||||||
return get('/accounts')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新账户信息
|
|
||||||
export function updateAccount(data) {
|
|
||||||
return put('/accounts', data)
|
|
||||||
}
|
|
||||||
|
|
||||||
14
api/bill.js
14
api/bill.js
@ -15,11 +15,6 @@ 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)
|
||||||
@ -29,3 +24,12 @@ export function updateBill(id, data) {
|
|||||||
export function deleteBill(id) {
|
export function deleteBill(id) {
|
||||||
return del(`/bills/${id}`)
|
return del(`/bills/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,43 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import { put } from '../utils/request'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新账户余额
|
|
||||||
* @param {number} initialBalance - 新的初始余额
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
export function updateAccountBalance(initialBalance) {
|
|
||||||
return put('/accounts/balance', {
|
|
||||||
initialBalance
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新当前月份预算
|
|
||||||
* @param {number} amount - 新的预算金额
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
export function updateBudget(amount) {
|
|
||||||
return put('/budgets/current', {
|
|
||||||
amount
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
import { get, post } from '../utils/request'
|
|
||||||
|
|
||||||
// 获取当前黄金价格
|
|
||||||
export function getCurrentGoldPrice(goldId = '1053') {
|
|
||||||
return get('/gold-price/current', { goldId })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取历史黄金价格
|
|
||||||
export function getGoldPriceHistory(goldId = '1053', days = 30) {
|
|
||||||
return get('/gold-price/history', { goldId, days })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取指定日期的黄金价格记录
|
|
||||||
export function getGoldPricesByDate(goldId = '1053', date) {
|
|
||||||
return get('/gold-price/by-date', { goldId, date })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取最近有数据的日期
|
|
||||||
export function getLatestGoldPriceDate(goldId = '1053') {
|
|
||||||
return get('/gold-price/latest-date', { goldId })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 立即刷新黄金价格
|
|
||||||
export function refreshGoldPrice(goldId = '1053') {
|
|
||||||
return post('/gold-price/refresh?goldId=' + goldId, {})
|
|
||||||
}
|
|
||||||
@ -1,230 +0,0 @@
|
|||||||
<template>
|
|
||||||
<view class="chart-container">
|
|
||||||
<view
|
|
||||||
:id="chartId"
|
|
||||||
class="echart-container"
|
|
||||||
:change:prop="echartModule.updateChart"
|
|
||||||
:prop="chartOption"
|
|
||||||
></view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
budget: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
},
|
|
||||||
used: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
},
|
|
||||||
chartId: {
|
|
||||||
type: String,
|
|
||||||
default: 'budgetPieChart'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
chartOption() {
|
|
||||||
const total = this.budget || 1
|
|
||||||
const used = Math.min(this.used, total)
|
|
||||||
const remaining = Math.max(total - used, 0)
|
|
||||||
|
|
||||||
return {
|
|
||||||
chartId: this.chartId,
|
|
||||||
used: used,
|
|
||||||
remaining: remaining
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script module="echartModule" lang="renderjs">
|
|
||||||
const chartInstances = {}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
mounted() {
|
|
||||||
// renderjs mounted时初始化
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
// 动态加载 echarts
|
|
||||||
this.loadEcharts()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
loadEcharts() {
|
|
||||||
// 检查是否已加载
|
|
||||||
if (window.echarts) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 动态加载 echarts 脚本
|
|
||||||
const script = document.createElement('script')
|
|
||||||
script.src = 'https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js'
|
|
||||||
script.onload = () => {
|
|
||||||
console.log('ECharts loaded successfully')
|
|
||||||
}
|
|
||||||
script.onerror = () => {
|
|
||||||
console.error('Failed to load ECharts')
|
|
||||||
}
|
|
||||||
document.head.appendChild(script)
|
|
||||||
},
|
|
||||||
updateChart(newValue, oldValue, ownerInstance, instance) {
|
|
||||||
// 等待 echarts 加载完成
|
|
||||||
const checkEcharts = () => {
|
|
||||||
if (!window.echarts) {
|
|
||||||
setTimeout(checkEcharts, 100)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.renderChart(newValue)
|
|
||||||
}
|
|
||||||
checkEcharts()
|
|
||||||
},
|
|
||||||
renderChart(option) {
|
|
||||||
if (!window.echarts || !option) return
|
|
||||||
|
|
||||||
const chartDom = document.getElementById(option.chartId)
|
|
||||||
if (!chartDom) {
|
|
||||||
console.error('Chart container not found:', option.chartId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为每个图表ID创建独立的实例
|
|
||||||
if (!chartInstances[option.chartId]) {
|
|
||||||
chartInstances[option.chartId] = window.echarts.init(chartDom)
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartInstance = chartInstances[option.chartId]
|
|
||||||
|
|
||||||
// 设置图表配置
|
|
||||||
const chartOption = {
|
|
||||||
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|¥${option.remaining.toFixed(2)}}`
|
|
||||||
},
|
|
||||||
rich: {
|
|
||||||
a: {
|
|
||||||
fontSize: 13,
|
|
||||||
color: '#8D6E63',
|
|
||||||
lineHeight: 20,
|
|
||||||
fontWeight: 'normal'
|
|
||||||
},
|
|
||||||
b: {
|
|
||||||
fontSize: 17,
|
|
||||||
color: '#5D4037',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
lineHeight: 32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
labelLine: {
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
value: option.used,
|
|
||||||
name: '已用',
|
|
||||||
itemStyle: {
|
|
||||||
color: {
|
|
||||||
type: 'linear',
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
x2: 0,
|
|
||||||
y2: 1,
|
|
||||||
colorStops: [
|
|
||||||
{ offset: 0, color: '#FA3534' },
|
|
||||||
{ offset: 1, color: '#FF6B6B' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
shadowBlur: 10,
|
|
||||||
shadowColor: 'rgba(250, 53, 52, 0.3)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: option.remaining,
|
|
||||||
name: '剩余',
|
|
||||||
itemStyle: {
|
|
||||||
color: {
|
|
||||||
type: 'linear',
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
x2: 0,
|
|
||||||
y2: 1,
|
|
||||||
colorStops: [
|
|
||||||
{ offset: 0, color: '#FFD700' },
|
|
||||||
{ offset: 1, color: '#F5D59E' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
shadowBlur: 10,
|
|
||||||
shadowColor: 'rgba(255, 215, 0, 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(chartOption, true)
|
|
||||||
|
|
||||||
// 调整图表尺寸
|
|
||||||
setTimeout(() => {
|
|
||||||
chartInstance.resize()
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
// 清理所有图表实例
|
|
||||||
Object.keys(chartInstances).forEach(key => {
|
|
||||||
if (chartInstances[key]) {
|
|
||||||
chartInstances[key].dispose()
|
|
||||||
delete chartInstances[key]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.chart-container {
|
|
||||||
width: 280rpx;
|
|
||||||
height: 280rpx;
|
|
||||||
margin: 0 auto;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.echart-container {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 200px;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,278 +0,0 @@
|
|||||||
<template>
|
|
||||||
<view v-if="visible" class="modal-overlay" @click="handleBackdropClick">
|
|
||||||
<view class="modal-content" @click.stop>
|
|
||||||
<view class="modal-header">
|
|
||||||
<text class="modal-title">{{ title }}</text>
|
|
||||||
<text class="modal-close" @click="handleCancel">✕</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="modal-body">
|
|
||||||
<input
|
|
||||||
v-model="inputValue"
|
|
||||||
:type="inputType"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
class="modal-input"
|
|
||||||
@input="handleInput"
|
|
||||||
/>
|
|
||||||
<text v-if="errorMessage" class="error-message">{{ errorMessage }}</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="modal-footer">
|
|
||||||
<button class="btn-cancel" @click="handleCancel">取消</button>
|
|
||||||
<button class="btn-confirm" @click="handleConfirm" :disabled="loading">
|
|
||||||
{{ loading ? '加载中...' : '确认' }}
|
|
||||||
</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
visible: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
default: '编辑'
|
|
||||||
},
|
|
||||||
placeholder: {
|
|
||||||
type: String,
|
|
||||||
default: '请输入数值'
|
|
||||||
},
|
|
||||||
inputType: {
|
|
||||||
type: String,
|
|
||||||
default: 'number'
|
|
||||||
},
|
|
||||||
currentValue: {
|
|
||||||
type: [String, Number],
|
|
||||||
default: ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['confirm', 'cancel'])
|
|
||||||
|
|
||||||
const inputValue = ref('')
|
|
||||||
const errorMessage = ref('')
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
// 监听 currentValue 变化,更新输入框
|
|
||||||
watch(() => props.currentValue, (newVal) => {
|
|
||||||
inputValue.value = String(newVal || '')
|
|
||||||
errorMessage.value = ''
|
|
||||||
}, { immediate: true })
|
|
||||||
|
|
||||||
// 监听 visible 变化,重置状态
|
|
||||||
watch(() => props.visible, (newVal) => {
|
|
||||||
if (newVal) {
|
|
||||||
inputValue.value = String(props.currentValue || '')
|
|
||||||
errorMessage.value = ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const validateInput = (value) => {
|
|
||||||
// 检查是否为空
|
|
||||||
if (!value || value.trim() === '') {
|
|
||||||
errorMessage.value = '请输入有效的数值'
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换为数字
|
|
||||||
const numValue = parseFloat(value)
|
|
||||||
|
|
||||||
// 检查是否为有效数字
|
|
||||||
if (isNaN(numValue)) {
|
|
||||||
errorMessage.value = '请输入数字'
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为负数
|
|
||||||
if (numValue < 0) {
|
|
||||||
errorMessage.value = '数值不能为负数'
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
errorMessage.value = ''
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDecimal = (value) => {
|
|
||||||
const numValue = parseFloat(value)
|
|
||||||
if (isNaN(numValue)) return value
|
|
||||||
return numValue.toFixed(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleInput = (event) => {
|
|
||||||
const value = event.detail.value
|
|
||||||
inputValue.value = value
|
|
||||||
|
|
||||||
// 实时验证
|
|
||||||
if (value) {
|
|
||||||
validateInput(value)
|
|
||||||
} else {
|
|
||||||
errorMessage.value = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
|
||||||
if (!validateInput(inputValue.value)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const formattedValue = formatDecimal(inputValue.value)
|
|
||||||
emit('confirm', formattedValue)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
emit('cancel')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBackdropClick = () => {
|
|
||||||
emit('cancel')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background: rgba(255, 255, 255, 0.98);
|
|
||||||
border-radius: 25rpx;
|
|
||||||
width: 80%;
|
|
||||||
max-width: 600rpx;
|
|
||||||
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3);
|
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
animation: slideUp 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideUp {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(50rpx);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 30rpx;
|
|
||||||
border-bottom: 2rpx solid rgba(200, 149, 110, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-title {
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #5D4037;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close {
|
|
||||||
font-size: 36rpx;
|
|
||||||
color: #C8956E;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close:active {
|
|
||||||
transform: scale(1.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding: 30rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-input {
|
|
||||||
width: 100%;
|
|
||||||
height: 80rpx;
|
|
||||||
padding: 0 20rpx;
|
|
||||||
font-size: 28rpx;
|
|
||||||
border: 2rpx solid #C8956E;
|
|
||||||
border-radius: 15rpx;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
color: #5D4037;
|
|
||||||
box-sizing: border-box;
|
|
||||||
transition: border-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-input:focus {
|
|
||||||
border-color: #FFD700;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
display: block;
|
|
||||||
margin-top: 15rpx;
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #FA3534;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
display: flex;
|
|
||||||
gap: 20rpx;
|
|
||||||
padding: 30rpx;
|
|
||||||
border-top: 2rpx solid rgba(200, 149, 110, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-cancel,
|
|
||||||
.btn-confirm {
|
|
||||||
flex: 1;
|
|
||||||
height: 80rpx;
|
|
||||||
border-radius: 15rpx;
|
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-cancel {
|
|
||||||
background: rgba(200, 149, 110, 0.2);
|
|
||||||
color: #5D4037;
|
|
||||||
border: 2rpx solid #C8956E;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-cancel:active {
|
|
||||||
background: rgba(200, 149, 110, 0.3);
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-confirm {
|
|
||||||
background: linear-gradient(135deg, #C8956E, #FFD700);
|
|
||||||
color: #fff;
|
|
||||||
border: 2rpx solid #C8956E;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-confirm:active:not(:disabled) {
|
|
||||||
transform: scale(0.98);
|
|
||||||
box-shadow: 0 4rpx 12rpx rgba(200, 149, 110, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-confirm:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "强宝爱记账",
|
"name": "强宝爱记账",
|
||||||
"appid" : "__UNI__ED52D70",
|
"appid": "__UNI__ACCOUNTING",
|
||||||
"description": "智能记账应用,支持OCR识别账单",
|
"description": "智能记账应用,支持OCR识别账单",
|
||||||
"versionName": "1.0.0",
|
"versionName": "1.0.0",
|
||||||
"versionCode": "100",
|
"versionCode": "100",
|
||||||
@ -38,42 +38,8 @@
|
|||||||
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>"
|
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ios" : {
|
"ios": {},
|
||||||
"dSYMs" : false
|
"sdkConfigs": {}
|
||||||
},
|
|
||||||
"sdkConfigs" : {},
|
|
||||||
"icons" : {
|
|
||||||
"android" : {
|
|
||||||
"hdpi" : "unpackage/res/icons/72x72.png",
|
|
||||||
"xhdpi" : "unpackage/res/icons/96x96.png",
|
|
||||||
"xxhdpi" : "unpackage/res/icons/144x144.png",
|
|
||||||
"xxxhdpi" : "unpackage/res/icons/192x192.png"
|
|
||||||
},
|
|
||||||
"ios" : {
|
|
||||||
"appstore" : "unpackage/res/icons/1024x1024.png",
|
|
||||||
"ipad" : {
|
|
||||||
"app" : "unpackage/res/icons/76x76.png",
|
|
||||||
"app@2x" : "unpackage/res/icons/152x152.png",
|
|
||||||
"notification" : "unpackage/res/icons/20x20.png",
|
|
||||||
"notification@2x" : "unpackage/res/icons/40x40.png",
|
|
||||||
"proapp@2x" : "unpackage/res/icons/167x167.png",
|
|
||||||
"settings" : "unpackage/res/icons/29x29.png",
|
|
||||||
"settings@2x" : "unpackage/res/icons/58x58.png",
|
|
||||||
"spotlight" : "unpackage/res/icons/40x40.png",
|
|
||||||
"spotlight@2x" : "unpackage/res/icons/80x80.png"
|
|
||||||
},
|
|
||||||
"iphone" : {
|
|
||||||
"app@2x" : "unpackage/res/icons/120x120.png",
|
|
||||||
"app@3x" : "unpackage/res/icons/180x180.png",
|
|
||||||
"notification@2x" : "unpackage/res/icons/40x40.png",
|
|
||||||
"notification@3x" : "unpackage/res/icons/60x60.png",
|
|
||||||
"settings@2x" : "unpackage/res/icons/58x58.png",
|
|
||||||
"settings@3x" : "unpackage/res/icons/87x87.png",
|
|
||||||
"spotlight@2x" : "unpackage/res/icons/80x80.png",
|
|
||||||
"spotlight@3x" : "unpackage/res/icons/120x120.png"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"quickapp": {},
|
"quickapp": {},
|
||||||
|
|||||||
514
package-lock.json
generated
514
package-lock.json
generated
@ -10,13 +10,14 @@
|
|||||||
"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",
|
||||||
"typescript": "^5.2.2"
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^4.5.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-string-parser": {
|
"node_modules/@babel/helper-string-parser": {
|
||||||
@ -82,6 +83,358 @@
|
|||||||
"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",
|
||||||
@ -96,6 +449,19 @@
|
|||||||
"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",
|
||||||
@ -205,15 +571,6 @@
|
|||||||
"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",
|
||||||
@ -225,11 +582,62 @@
|
|||||||
"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",
|
||||||
@ -341,6 +749,22 @@
|
|||||||
"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",
|
||||||
@ -354,11 +778,6 @@
|
|||||||
"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",
|
||||||
@ -383,6 +802,61 @@
|
|||||||
"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",
|
||||||
@ -427,14 +901,6 @@
|
|||||||
"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,7 +11,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
|
|||||||
44
pages.json
44
pages.json
@ -6,6 +6,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pages": [
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "pages/login/login",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "登录",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/index/index",
|
"path": "pages/index/index",
|
||||||
"style": {
|
"style": {
|
||||||
@ -25,42 +32,11 @@
|
|||||||
"navigationBarTitleText": "统计"
|
"navigationBarTitleText": "统计"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "pages/mine/mine",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "我的"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "pages/login/login",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "登录",
|
|
||||||
"navigationStyle": "custom"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "pages/bill/detail",
|
"path": "pages/bill/detail",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "编辑账单"
|
"navigationBarTitleText": "编辑账单"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "pages/account/account",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "账户管理"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "pages/budget/budget",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "预算管理"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "pages/gold-price/gold-price",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "黄金价格"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tabBar": {
|
"tabBar": {
|
||||||
@ -86,12 +62,6 @@
|
|||||||
"iconPath": "static/tabbar/statistics.png",
|
"iconPath": "static/tabbar/statistics.png",
|
||||||
"selectedIconPath": "static/tabbar/statistic-active.png",
|
"selectedIconPath": "static/tabbar/statistic-active.png",
|
||||||
"text": "统计"
|
"text": "统计"
|
||||||
},
|
|
||||||
{
|
|
||||||
"pagePath": "pages/mine/mine",
|
|
||||||
"iconPath": "static/tabbar/mine.png",
|
|
||||||
"selectedIconPath": "static/tabbar/mine-active.png",
|
|
||||||
"text": "我的"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
123
|
|
||||||
</template>
|
|
||||||
@ -65,12 +65,11 @@
|
|||||||
@input="onAmountInput"
|
@input="onAmountInput"
|
||||||
@focus="onAmountFocus"
|
@focus="onAmountFocus"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="form-item">
|
<view class="form-item">
|
||||||
<text class="label">日期</text>
|
<text class="label">日期</text>
|
||||||
<picker mode="date" :value="form.billDate" @change="onDateChange">
|
<picker mode="date" v-model="form.billDate" @change="onDateChange">
|
||||||
<view class="picker">
|
<view class="picker">
|
||||||
<text>{{ form.billDate || '选择日期' }}</text>
|
<text>{{ form.billDate || '选择日期' }}</text>
|
||||||
<text class="arrow">></text>
|
<text class="arrow">></text>
|
||||||
@ -111,9 +110,6 @@
|
|||||||
|
|
||||||
<!-- 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>
|
||||||
@ -158,7 +154,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import {createBill, createBillsBatch} from '../../api/bill'
|
import { createBill } 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'
|
||||||
@ -448,70 +444,6 @@ 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()
|
||||||
|
|
||||||
@ -534,66 +466,57 @@ onMounted(() => {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-bottom: calc(50px + env(safe-area-inset-bottom));
|
padding-bottom: calc(50px + env(safe-area-inset-bottom));
|
||||||
background: linear-gradient(135deg, #FFE5CC 0%, #FFD4A8 50%, #FFC08A 100%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
min-height: calc(100vh - 100rpx);
|
min-height: calc(100vh - 100rpx);
|
||||||
background: transparent;
|
background: #f5f5f5;
|
||||||
padding: 20rpx;
|
padding: 20rpx;
|
||||||
padding-bottom: calc(170rpx + env(safe-area-inset-bottom));
|
padding-bottom: calc(170rpx + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: #fff;
|
||||||
border-radius: 20rpx;
|
border-radius: 12rpx;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
padding: 10rpx;
|
padding: 10rpx;
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
box-shadow: 0 8rpx 20rpx rgba(200, 149, 110, 0.15);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20rpx;
|
padding: 20rpx;
|
||||||
border-radius: 15rpx;
|
border-radius: 8rpx;
|
||||||
color: #8D6E63;
|
color: #666;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab.active {
|
.tab.active {
|
||||||
background: linear-gradient(135deg, #C8956E, #F5D59E);
|
background: #667eea;
|
||||||
color: #5D4037;
|
color: #fff;
|
||||||
box-shadow: 0 4rpx 15rpx rgba(200, 149, 110, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form {
|
.form {
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: #fff;
|
||||||
border-radius: 25rpx;
|
border-radius: 12rpx;
|
||||||
padding: 40rpx 30rpx;
|
padding: 30rpx;
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
box-shadow: 0 10rpx 30rpx rgba(200, 149, 110, 0.15);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-item {
|
.form-item {
|
||||||
margin-bottom: 35rpx;
|
margin-bottom: 30rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
color: #5D4037;
|
color: #333;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-selector-compact {
|
.type-selector-compact {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20rpx;
|
gap: 15rpx;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -602,32 +525,32 @@ onMounted(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 20rpx 40rpx;
|
padding: 15rpx 30rpx;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: #f8f8f8;
|
||||||
border-radius: 15rpx;
|
border-radius: 8rpx;
|
||||||
border: 3rpx solid #C8956E;
|
border: 2rpx solid transparent;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
min-width: 140rpx;
|
min-width: 120rpx;
|
||||||
box-shadow: 0 4rpx 15rpx rgba(200, 149, 110, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-option-compact.active {
|
.type-option-compact.active {
|
||||||
background: linear-gradient(135deg, #FFD700, #F5D59E);
|
background: #f0f7ff;
|
||||||
border-color: #FFD700;
|
border-color: #667eea;
|
||||||
transform: scale(1.05);
|
|
||||||
box-shadow: 0 8rpx 20rpx rgba(255, 215, 0, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-icon-compact {
|
.type-icon-compact {
|
||||||
font-size: 40rpx;
|
font-size: 32rpx;
|
||||||
margin-bottom: 8rpx;
|
margin-bottom: 5rpx;
|
||||||
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-text-compact {
|
.type-text-compact {
|
||||||
font-size: 26rpx;
|
font-size: 24rpx;
|
||||||
color: #5D4037;
|
color: #333;
|
||||||
font-weight: 600;
|
}
|
||||||
|
|
||||||
|
.type-option-compact.active .type-text-compact {
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-grid {
|
.category-grid {
|
||||||
@ -646,279 +569,215 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.category-icon-circle {
|
.category-icon-circle {
|
||||||
width: 110rpx;
|
width: 100rpx;
|
||||||
height: 110rpx;
|
height: 100rpx;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: #f8f8f8;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 10rpx;
|
margin-bottom: 8rpx;
|
||||||
border: 3rpx solid #C8956E;
|
border: 3rpx solid transparent;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
box-shadow: 0 4rpx 15rpx rgba(200, 149, 110, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-icon-item.active .category-icon-circle {
|
.category-icon-item.active .category-icon-circle {
|
||||||
background: linear-gradient(135deg, #FFD700, #F5D59E);
|
background: #f0f7ff;
|
||||||
border-color: #FFD700;
|
border-color: #667eea;
|
||||||
transform: scale(1.15);
|
transform: scale(1.1);
|
||||||
box-shadow: 0 8rpx 20rpx rgba(255, 215, 0, 0.4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-icon-emoji {
|
.category-icon-emoji {
|
||||||
font-size: 52rpx;
|
font-size: 48rpx;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-icon-name {
|
.category-icon-name {
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: #5D4037;
|
color: #666;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-icon-item.active .category-icon-name {
|
.category-icon-item.active .category-icon-name {
|
||||||
color: #FFD700;
|
color: #667eea;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-shadow: 0 2rpx 4rpx rgba(255, 215, 0, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-empty {
|
.category-empty {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 50rpx 0;
|
padding: 40rpx 0;
|
||||||
color: #8D6E63;
|
color: #999;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
background: rgba(255, 255, 255, 0.5);
|
|
||||||
border-radius: 15rpx;
|
|
||||||
border: 2rpx dashed #C8956E;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input, .textarea {
|
.input, .textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 25rpx;
|
padding: 20rpx;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: #f8f8f8;
|
||||||
border-radius: 15rpx;
|
border-radius: 8rpx;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 3rpx solid #C8956E;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
color: #5D4037;
|
-webkit-appearance: none;
|
||||||
box-shadow: 0 4rpx 15rpx rgba(200, 149, 110, 0.1);
|
-webkit-tap-highlight-color: transparent;
|
||||||
transition: all 0.3s;
|
pointer-events: auto;
|
||||||
}
|
user-select: text;
|
||||||
|
-webkit-user-select: text;
|
||||||
.input:focus, .textarea:focus {
|
|
||||||
border-color: #FFD700;
|
|
||||||
box-shadow: 0 0 0 4rpx rgba(255, 215, 0, 0.3), 0 8rpx 25rpx rgba(255, 215, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input::placeholder, .textarea::placeholder {
|
|
||||||
color: #BCAAA4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.textarea {
|
.textarea {
|
||||||
min-height: 180rpx;
|
min-height: 200rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-selector, .picker {
|
.category-selector, .picker {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 25rpx;
|
padding: 20rpx;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: #f8f8f8;
|
||||||
border-radius: 15rpx;
|
border-radius: 8rpx;
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
box-shadow: 0 4rpx 15rpx rgba(200, 149, 110, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder {
|
.placeholder {
|
||||||
color: #BCAAA4;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow {
|
.arrow {
|
||||||
color: #C8956E;
|
color: #999;
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-btn {
|
.submit-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 96rpx;
|
height: 88rpx;
|
||||||
background: linear-gradient(135deg, #C8956E, #F5D59E, #FFD700);
|
background: #667eea;
|
||||||
color: #5D4037;
|
color: #fff;
|
||||||
border-radius: 50rpx;
|
border-radius: 12rpx;
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 40rpx;
|
margin-top: 40rpx;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
border: 4rpx solid #5D4037;
|
border: none;
|
||||||
box-shadow: 0 10rpx 30rpx rgba(200, 149, 110, 0.4);
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-btn:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
box-shadow: 0 5rpx 15rpx rgba(200, 149, 110, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ocr-container {
|
.ocr-container {
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: #fff;
|
||||||
border-radius: 25rpx;
|
border-radius: 12rpx;
|
||||||
padding: 30rpx;
|
padding: 30rpx;
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
box-shadow: 0 10rpx 30rpx rgba(200, 149, 110, 0.15);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-section {
|
.image-section {
|
||||||
margin-bottom: 30rpx;
|
margin-bottom: 30rpx;
|
||||||
background: rgba(255, 255, 255, 0.8);
|
background: #fff;
|
||||||
border-radius: 20rpx;
|
border-radius: 12rpx;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 25rpx;
|
padding: 20rpx;
|
||||||
background: linear-gradient(135deg, rgba(200, 149, 110, 0.15), rgba(255, 215, 0, 0.15));
|
background: #f8f8f8;
|
||||||
border-radius: 17rpx;
|
border-radius: 12rpx;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: background 0.3s;
|
transition: background 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header:active {
|
.section-header:active {
|
||||||
background: linear-gradient(135deg, rgba(200, 149, 110, 0.25), rgba(255, 215, 0, 0.25));
|
background: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #5D4037;
|
color: #333;
|
||||||
}
|
|
||||||
|
|
||||||
.list-title {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #5D4037;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-content {
|
.section-content {
|
||||||
padding: 25rpx;
|
padding: 20rpx 0;
|
||||||
animation: slideDown 0.3s ease-out;
|
animation: slideDown 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-area {
|
.upload-area {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 400rpx;
|
height: 400rpx;
|
||||||
border: 3rpx dashed #C8956E;
|
border: 2rpx dashed #ddd;
|
||||||
border-radius: 20rpx;
|
border-radius: 12rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 30rpx;
|
margin-bottom: 30rpx;
|
||||||
background: rgba(255, 255, 255, 0.5);
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-area:active {
|
|
||||||
border-color: #FFD700;
|
|
||||||
background: rgba(255, 215, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-image {
|
.preview-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 17rpx;
|
border-radius: 12rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-placeholder {
|
.upload-placeholder {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #8D6E63;
|
color: #999;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-icon {
|
.upload-icon {
|
||||||
font-size: 100rpx;
|
font-size: 80rpx;
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
filter: drop-shadow(0 4rpx 8rpx rgba(0, 0, 0, 0.1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.recognize-btn {
|
.recognize-btn {
|
||||||
width: 100%;
|
|
||||||
height: 96rpx;
|
|
||||||
background: linear-gradient(135deg, #19BE6B, #51CF66);
|
|
||||||
color: #fff;
|
|
||||||
border-radius: 50rpx;
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 30rpx;
|
|
||||||
border: 4rpx solid #108043;
|
|
||||||
box-shadow: 0 10rpx 30rpx rgba(25, 190, 107, 0.3);
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recognize-btn:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
.direct-save-btn {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 88rpx;
|
height: 88rpx;
|
||||||
background: linear-gradient(135deg, #FFD700, #F5D59E);
|
background: #19be6b;
|
||||||
color: #5D4037;
|
color: #fff;
|
||||||
border-radius: 50rpx;
|
border-radius: 12rpx;
|
||||||
font-size: 30rpx;
|
font-size: 32rpx;
|
||||||
font-weight: bold;
|
margin-bottom: 30rpx;
|
||||||
margin-bottom: 20rpx;
|
border: none;
|
||||||
border: 3rpx solid #5D4037;
|
|
||||||
box-shadow: 0 8rpx 25rpx rgba(255, 215, 0, 0.3);
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.direct-save-btn:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ocr-result-list-container {
|
.ocr-result-list-container {
|
||||||
margin-top: 30rpx;
|
margin-top: 30rpx;
|
||||||
background: rgba(255, 255, 255, 0.8);
|
background: #fff;
|
||||||
border-radius: 20rpx;
|
border-radius: 12rpx;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 3rpx solid #C8956E;
|
}
|
||||||
|
|
||||||
|
.list-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-list-wrapper {
|
.result-list-wrapper {
|
||||||
max-height: 600rpx;
|
max-height: 600rpx;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0 20rpx 20rpx 20rpx;
|
padding: 0 20rpx 20rpx 20rpx;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-card {
|
.result-card {
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: #fff;
|
||||||
border: 3rpx solid #C8956E;
|
border: 2rpx solid #e0e0e0;
|
||||||
border-radius: 20rpx;
|
border-radius: 12rpx;
|
||||||
padding: 25rpx;
|
padding: 20rpx;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 4rpx 15rpx rgba(200, 149, 110, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-card.active {
|
.result-card.active {
|
||||||
border-color: #FFD700;
|
border-color: #667eea;
|
||||||
background: linear-gradient(135deg, rgba(255, 215, 0, 0.1), rgba(245, 213, 158, 0.1));
|
background: #f0f7ff;
|
||||||
box-shadow: 0 8rpx 20rpx rgba(255, 215, 0, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-header {
|
.result-header {
|
||||||
@ -943,37 +802,37 @@ onMounted(() => {
|
|||||||
|
|
||||||
.result-index {
|
.result-index {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #FFD700;
|
color: #667eea;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-right: 15rpx;
|
margin-right: 15rpx;
|
||||||
background: #5D4037;
|
|
||||||
padding: 6rpx 12rpx;
|
|
||||||
border-radius: 10rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-value.amount.summary {
|
.result-value.amount.summary {
|
||||||
font-size: 32rpx;
|
font-size: 30rpx;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #FA3534;
|
color: #fa3534;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-confidence {
|
.result-confidence {
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: #8D6E63;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expand-icon {
|
.expand-icon {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #C8956E;
|
color: #667eea;
|
||||||
transition: transform 0.3s;
|
transition: transform 0.3s;
|
||||||
margin-left: 10rpx;
|
margin-left: 10rpx;
|
||||||
font-weight: bold;
|
}
|
||||||
|
|
||||||
|
.result-card.expanded .expand-icon {
|
||||||
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-details {
|
.result-details {
|
||||||
margin-top: 25rpx;
|
margin-top: 20rpx;
|
||||||
padding-top: 25rpx;
|
padding-top: 20rpx;
|
||||||
border-top: 2rpx solid rgba(200, 149, 110, 0.3);
|
border-top: 1rpx solid #f0f0f0;
|
||||||
animation: slideDown 0.3s ease-out;
|
animation: slideDown 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -990,54 +849,47 @@ onMounted(() => {
|
|||||||
|
|
||||||
.result-item {
|
.result-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 12rpx 0;
|
padding: 10rpx 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-label {
|
.result-label {
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: #8D6E63;
|
color: #666;
|
||||||
width: 100rpx;
|
width: 100rpx;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-value {
|
.result-value {
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
color: #5D4037;
|
color: #333;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-value.amount {
|
.result-value.amount {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #FA3534;
|
color: #fa3534;
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-btn {
|
.select-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 76rpx;
|
height: 70rpx;
|
||||||
background: linear-gradient(135deg, #C8956E, #F5D59E);
|
background: #667eea;
|
||||||
color: #5D4037;
|
color: #fff;
|
||||||
border-radius: 50rpx;
|
border-radius: 8rpx;
|
||||||
font-size: 28rpx;
|
font-size: 26rpx;
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 20rpx;
|
margin-top: 20rpx;
|
||||||
border: 3rpx solid #5D4037;
|
border: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: 0 6rpx 18rpx rgba(200, 149, 110, 0.3);
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-btn:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ocr-result {
|
.ocr-result {
|
||||||
margin-top: 30rpx;
|
margin-top: 30rpx;
|
||||||
padding-top: 30rpx;
|
padding-top: 30rpx;
|
||||||
border-top: 3rpx solid #C8956E;
|
border-top: 2rpx solid #667eea;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@ -64,7 +64,7 @@
|
|||||||
|
|
||||||
<view class="form-item">
|
<view class="form-item">
|
||||||
<text class="label">日期</text>
|
<text class="label">日期</text>
|
||||||
<picker mode="date" :value="form.billDate" @change="onDateChange">
|
<picker mode="date" v-model="form.billDate" @change="onDateChange">
|
||||||
<view class="picker">
|
<view class="picker">
|
||||||
<text>{{ form.billDate || '选择日期' }}</text>
|
<text>{{ form.billDate || '选择日期' }}</text>
|
||||||
<text class="arrow">></text>
|
<text class="arrow">></text>
|
||||||
@ -283,36 +283,31 @@ onLoad((options) => {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-bottom: calc(50px + env(safe-area-inset-bottom));
|
padding-bottom: calc(50px + env(safe-area-inset-bottom));
|
||||||
background: linear-gradient(135deg, #FFE5CC 0%, #FFD4A8 50%, #FFC08A 100%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
min-height: calc(100vh - 100rpx);
|
min-height: calc(100vh - 100rpx);
|
||||||
background: transparent;
|
background: #f5f5f5;
|
||||||
padding: 20rpx;
|
padding: 20rpx;
|
||||||
padding-bottom: calc(170rpx + env(safe-area-inset-bottom));
|
padding-bottom: calc(170rpx + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-card {
|
.form-card {
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: #fff;
|
||||||
border-radius: 25rpx;
|
border-radius: 12rpx;
|
||||||
padding: 40rpx 30rpx;
|
padding: 30rpx;
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
box-shadow: 0 10rpx 30rpx rgba(93, 64, 55, 0.15);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-header {
|
.form-header {
|
||||||
margin-bottom: 35rpx;
|
margin-bottom: 30rpx;
|
||||||
padding-bottom: 25rpx;
|
padding-bottom: 20rpx;
|
||||||
border-bottom: 3rpx solid rgba(200, 149, 110, 0.3);
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-title {
|
.form-title {
|
||||||
font-size: 40rpx;
|
font-size: 36rpx;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #5D4037;
|
color: #333;
|
||||||
text-shadow: 2rpx 2rpx 0 rgba(255, 215, 0, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-body {
|
.form-body {
|
||||||
@ -320,20 +315,19 @@ onLoad((options) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-item {
|
.form-item {
|
||||||
margin-bottom: 35rpx;
|
margin-bottom: 30rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
color: #5D4037;
|
color: #333;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-selector-compact {
|
.type-selector-compact {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20rpx;
|
gap: 15rpx;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -342,32 +336,32 @@ onLoad((options) => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 20rpx 40rpx;
|
padding: 15rpx 30rpx;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: #f8f8f8;
|
||||||
border-radius: 15rpx;
|
border-radius: 8rpx;
|
||||||
border: 3rpx solid #C8956E;
|
border: 2rpx solid transparent;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
min-width: 140rpx;
|
min-width: 120rpx;
|
||||||
box-shadow: 0 4rpx 15rpx rgba(200, 149, 110, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-option-compact.active {
|
.type-option-compact.active {
|
||||||
background: linear-gradient(135deg, #FFD700, #F5D59E);
|
background: #f0f7ff;
|
||||||
border-color: #FFD700;
|
border-color: #667eea;
|
||||||
transform: scale(1.05);
|
|
||||||
box-shadow: 0 8rpx 20rpx rgba(255, 215, 0, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-icon-compact {
|
.type-icon-compact {
|
||||||
font-size: 40rpx;
|
font-size: 32rpx;
|
||||||
margin-bottom: 8rpx;
|
margin-bottom: 5rpx;
|
||||||
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-text-compact {
|
.type-text-compact {
|
||||||
font-size: 26rpx;
|
font-size: 24rpx;
|
||||||
color: #5D4037;
|
color: #333;
|
||||||
font-weight: 600;
|
}
|
||||||
|
|
||||||
|
.type-option-compact.active .type-text-compact {
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-grid {
|
.category-grid {
|
||||||
@ -386,100 +380,82 @@ onLoad((options) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.category-icon-circle {
|
.category-icon-circle {
|
||||||
width: 110rpx;
|
width: 100rpx;
|
||||||
height: 110rpx;
|
height: 100rpx;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: #f8f8f8;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 10rpx;
|
margin-bottom: 8rpx;
|
||||||
border: 3rpx solid #C8956E;
|
border: 3rpx solid transparent;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
box-shadow: 0 4rpx 15rpx rgba(200, 149, 110, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-icon-item.active .category-icon-circle {
|
.category-icon-item.active .category-icon-circle {
|
||||||
background: linear-gradient(135deg, #FFD700, #F5D59E);
|
background: #f0f7ff;
|
||||||
border-color: #FFD700;
|
border-color: #667eea;
|
||||||
transform: scale(1.15);
|
transform: scale(1.1);
|
||||||
box-shadow: 0 8rpx 20rpx rgba(255, 215, 0, 0.4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-icon-emoji {
|
.category-icon-emoji {
|
||||||
font-size: 52rpx;
|
font-size: 48rpx;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-icon-name {
|
.category-icon-name {
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: #5D4037;
|
color: #666;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-icon-item.active .category-icon-name {
|
.category-icon-item.active .category-icon-name {
|
||||||
color: #FFD700;
|
color: #667eea;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-shadow: 0 2rpx 4rpx rgba(255, 215, 0, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-empty {
|
.category-empty {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 50rpx 0;
|
padding: 40rpx 0;
|
||||||
color: #8D6E63;
|
color: #999;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
background: rgba(255, 255, 255, 0.5);
|
|
||||||
border-radius: 15rpx;
|
|
||||||
border: 2rpx dashed #C8956E;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input, .textarea {
|
.input, .textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 25rpx;
|
padding: 20rpx;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: #f8f8f8;
|
||||||
border-radius: 15rpx;
|
border-radius: 8rpx;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 3rpx solid #C8956E;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
color: #5D4037;
|
-webkit-appearance: none;
|
||||||
box-shadow: 0 4rpx 15rpx rgba(200, 149, 110, 0.1);
|
-webkit-tap-highlight-color: transparent;
|
||||||
transition: all 0.3s;
|
pointer-events: auto;
|
||||||
}
|
user-select: text;
|
||||||
|
-webkit-user-select: text;
|
||||||
.input:focus, .textarea:focus {
|
|
||||||
border-color: #FFD700;
|
|
||||||
box-shadow: 0 0 0 4rpx rgba(255, 215, 0, 0.3), 0 8rpx 25rpx rgba(255, 215, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input::placeholder, .textarea::placeholder {
|
|
||||||
color: #BCAAA4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.textarea {
|
.textarea {
|
||||||
min-height: 180rpx;
|
min-height: 200rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.picker {
|
.picker {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 25rpx;
|
padding: 20rpx;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: #f8f8f8;
|
||||||
border-radius: 15rpx;
|
border-radius: 8rpx;
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
box-shadow: 0 4rpx 15rpx rgba(200, 149, 110, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow {
|
.arrow {
|
||||||
color: #C8956E;
|
color: #999;
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-footer {
|
.form-footer {
|
||||||
@ -490,41 +466,27 @@ onLoad((options) => {
|
|||||||
|
|
||||||
.delete-btn {
|
.delete-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 96rpx;
|
height: 88rpx;
|
||||||
background: linear-gradient(135deg, #FA3534, #FF6B6B);
|
background: #fa3534;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-radius: 50rpx;
|
border-radius: 12rpx;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
font-weight: bold;
|
border: none;
|
||||||
border: 3rpx solid #D32F2F;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: 0 8rpx 25rpx rgba(250, 53, 52, 0.3);
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-btn:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-btn {
|
.submit-btn {
|
||||||
flex: 2;
|
flex: 2;
|
||||||
height: 96rpx;
|
height: 88rpx;
|
||||||
background: linear-gradient(135deg, #C8956E, #F5D59E, #FFD700);
|
background: #667eea;
|
||||||
color: #5D4037;
|
color: #fff;
|
||||||
border-radius: 50rpx;
|
border-radius: 12rpx;
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
font-weight: bold;
|
border: none;
|
||||||
border: 4rpx solid #5D4037;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: 0 10rpx 30rpx rgba(200, 149, 110, 0.4);
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-btn:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
123
|
|
||||||
</template>
|
|
||||||
@ -1,372 +0,0 @@
|
|||||||
<template>
|
|
||||||
<input v-model="form.amount" type="text" />
|
|
||||||
<input v-model="form.amount" type="text" />
|
|
||||||
<input v-model="form.amount" type="text" />
|
|
||||||
<input v-model="form.amount" type="text" />
|
|
||||||
<input v-model="form.amount" type="text" />
|
|
||||||
<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">预算金额1</text>
|
|
||||||
<input
|
|
||||||
v-model="amountInput"
|
|
||||||
type="text"
|
|
||||||
inputmode="decimal"
|
|
||||||
placeholder="请输入预算金额"
|
|
||||||
@focus="onAmountFocus"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<view class="uni-form-item uni-column">
|
|
||||||
<view class="title">实时获取输入值:{{inputValue}}</view>
|
|
||||||
<input class="uni-input" @input="onKeyInput" placeholder="输入同步到view中" />
|
|
||||||
</view>
|
|
||||||
<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.amount ? budget.amount.toFixed(2) : '0.00' }}</text>
|
|
||||||
</view>
|
|
||||||
<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 inputValue = ref('');
|
|
||||||
|
|
||||||
const form = ref({
|
|
||||||
year: null,
|
|
||||||
month: null,
|
|
||||||
amount: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const saving = ref(false)
|
|
||||||
|
|
||||||
// 创建一个计算属性来处理金额输入的验证
|
|
||||||
const amountInput = computed({
|
|
||||||
get() {
|
|
||||||
return form.value.amount
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
// 确保只能输入数字和小数点
|
|
||||||
let processedValue = value || ''
|
|
||||||
// 移除非数字和小数点的字符
|
|
||||||
processedValue = processedValue.replace(/[^\d.]/g, '')
|
|
||||||
// 确保只有一个小数点
|
|
||||||
const parts = processedValue.split('.')
|
|
||||||
if (parts.length > 2) {
|
|
||||||
processedValue = parts[0] + '.' + parts.slice(1).join('')
|
|
||||||
}
|
|
||||||
// 限制小数位数为2位
|
|
||||||
if (parts.length === 2 && parts[1].length > 2) {
|
|
||||||
processedValue = parts[0] + '.' + parts[1].substring(0, 2)
|
|
||||||
}
|
|
||||||
form.value.amount = processedValue
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
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.toString() : ''
|
|
||||||
} 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 = ''
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载预算失败', error)
|
|
||||||
uni.showToast({
|
|
||||||
title: '加载预算失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onAmountFocus = () => {
|
|
||||||
// 聚焦时确保输入框可交互
|
|
||||||
console.log('预算金额输入框聚焦')
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onKeyInput = (event) => {
|
|
||||||
inputValue.value = event.detail.value
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadBudget()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.page-scroll {
|
|
||||||
height: 100vh;
|
|
||||||
width: 100%;
|
|
||||||
padding-bottom: calc(50px + env(safe-area-inset-bottom));
|
|
||||||
background: linear-gradient(135deg, #FFE5CC 0%, #FFD4A8 50%, #FFC08A 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
min-height: calc(100vh - 100rpx);
|
|
||||||
background: transparent;
|
|
||||||
padding: 20rpx;
|
|
||||||
padding-bottom: calc(170rpx + env(safe-area-inset-bottom));
|
|
||||||
}
|
|
||||||
|
|
||||||
.budget-card {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
border-radius: 25rpx;
|
|
||||||
box-shadow: 0 10rpx 30rpx rgba(93, 64, 55, 0.15);
|
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
padding: 40rpx 30rpx;
|
|
||||||
background: linear-gradient(135deg, #C8956E, #F5D59E, #FFD700);
|
|
||||||
border-bottom: 3rpx solid #5D4037;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
font-size: 40rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #5D4037;
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 12rpx;
|
|
||||||
text-shadow: 2rpx 2rpx 0 rgba(255, 215, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-subtitle {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #8D6E63;
|
|
||||||
display: block;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-body {
|
|
||||||
padding: 40rpx 30rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-item {
|
|
||||||
margin-bottom: 35rpx;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
display: block;
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #5D4037;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 25rpx;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
border-radius: 15rpx;
|
|
||||||
font-size: 32rpx;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
outline: none;
|
|
||||||
color: #5D4037;
|
|
||||||
box-shadow: 0 4rpx 15rpx rgba(200, 149, 110, 0.1);
|
|
||||||
transition: all 0.3s;
|
|
||||||
pointer-events: auto;
|
|
||||||
user-select: text;
|
|
||||||
-webkit-user-select: text;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus {
|
|
||||||
border-color: #FFD700;
|
|
||||||
box-shadow: 0 0 0 4rpx rgba(255, 215, 0, 0.3), 0 8rpx 25rpx rgba(255, 215, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
input::placeholder {
|
|
||||||
color: #BCAAA4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-tip {
|
|
||||||
display: block;
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #8D6E63;
|
|
||||||
margin-top: 12rpx;
|
|
||||||
padding-left: 10rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-section {
|
|
||||||
margin-top: 40rpx;
|
|
||||||
padding-top: 30rpx;
|
|
||||||
border-top: 2rpx solid rgba(200, 149, 110, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 25rpx;
|
|
||||||
margin-bottom: 15rpx;
|
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
border-radius: 15rpx;
|
|
||||||
border: 2rpx solid #C8956E;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-label {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #8D6E63;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
font-size: 34rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value.positive {
|
|
||||||
color: #FFD700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value.negative {
|
|
||||||
color: #FA3534;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value.expense {
|
|
||||||
color: #FA3534;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-footer {
|
|
||||||
padding: 0 30rpx 40rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-btn {
|
|
||||||
width: 100%;
|
|
||||||
height: 96rpx;
|
|
||||||
background: linear-gradient(135deg, #C8956E, #F5D59E, #FFD700);
|
|
||||||
color: #5D4037;
|
|
||||||
border-radius: 50rpx;
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
border: 4rpx solid #5D4037;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-shadow: 0 10rpx 30rpx rgba(200, 149, 110, 0.4);
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-btn:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
box-shadow: 0 5rpx 15rpx rgba(200, 149, 110, 0.3);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -162,62 +162,50 @@ onMounted(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.container {
|
.container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: linear-gradient(135deg, #FFE5CC 0%, #FFD4A8 50%, #FFC08A 100%);
|
background: #f5f5f5;
|
||||||
padding-bottom: calc(50rpx + env(safe-area-inset-bottom));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
background: linear-gradient(135deg, #C8956E, #F5D59E, #FFD700);
|
background: #fff;
|
||||||
padding: 40rpx 30rpx;
|
padding: 30rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: 3rpx solid #5D4037;
|
border-bottom: 1rpx solid #eee;
|
||||||
box-shadow: 0 8rpx 20rpx rgba(93, 64, 55, 0.15);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 40rpx;
|
font-size: 36rpx;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #5D4037;
|
|
||||||
text-shadow: 2rpx 2rpx 0 rgba(255, 215, 0, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-btn {
|
.add-btn {
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: #667eea;
|
||||||
color: #5D4037;
|
color: #fff;
|
||||||
padding: 15rpx 30rpx;
|
padding: 10rpx 20rpx;
|
||||||
border-radius: 50rpx;
|
border-radius: 8rpx;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
font-weight: bold;
|
border: none;
|
||||||
border: 3rpx solid #5D4037;
|
|
||||||
box-shadow: 0 4rpx 15rpx rgba(93, 64, 55, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: #fff;
|
||||||
margin: 20rpx;
|
margin-top: 20rpx;
|
||||||
padding: 10rpx;
|
padding: 10rpx;
|
||||||
border-radius: 20rpx;
|
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
box-shadow: 0 8rpx 20rpx rgba(200, 149, 110, 0.15);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20rpx;
|
padding: 20rpx;
|
||||||
border-radius: 15rpx;
|
border-radius: 8rpx;
|
||||||
color: #8D6E63;
|
color: #666;
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab.active {
|
.tab.active {
|
||||||
background: linear-gradient(135deg, #C8956E, #F5D59E);
|
background: #667eea;
|
||||||
color: #5D4037;
|
color: #fff;
|
||||||
box-shadow: 0 4rpx 15rpx rgba(200, 149, 110, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-list {
|
.category-list {
|
||||||
@ -225,63 +213,46 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.category-item {
|
.category-item {
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: #fff;
|
||||||
border-radius: 20rpx;
|
border-radius: 12rpx;
|
||||||
padding: 30rpx;
|
padding: 30rpx;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
box-shadow: 0 8rpx 20rpx rgba(200, 149, 110, 0.15);
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-item:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
box-shadow: 0 4rpx 10rpx rgba(200, 149, 110, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-icon {
|
.category-icon {
|
||||||
font-size: 52rpx;
|
font-size: 48rpx;
|
||||||
margin-right: 25rpx;
|
margin-right: 20rpx;
|
||||||
filter: drop-shadow(0 3rpx 6rpx rgba(0, 0, 0, 0.15));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-name {
|
.category-name {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 30rpx;
|
font-size: 30rpx;
|
||||||
color: #5D4037;
|
color: #333;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
background: linear-gradient(135deg, rgba(200, 149, 110, 0.2), rgba(255, 215, 0, 0.2));
|
background: #f0f0f0;
|
||||||
color: #8D6E63;
|
color: #999;
|
||||||
padding: 8rpx 20rpx;
|
padding: 4rpx 12rpx;
|
||||||
border-radius: 50rpx;
|
border-radius: 4rpx;
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
margin-right: 20rpx;
|
margin-right: 20rpx;
|
||||||
font-weight: 600;
|
|
||||||
border: 2rpx solid #C8956E;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 25rpx;
|
gap: 20rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
color: #FFD700;
|
color: #667eea;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
font-weight: 600;
|
|
||||||
padding: 8rpx 15rpx;
|
|
||||||
border-radius: 10rpx;
|
|
||||||
background: rgba(255, 215, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn.delete {
|
.action-btn.delete {
|
||||||
color: #FA3534;
|
color: #fa3534;
|
||||||
background: rgba(250, 53, 52, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
@ -290,131 +261,69 @@ onMounted(() => {
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: rgba(93, 64, 55, 0.7);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
backdrop-filter: blur(10rpx);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
animation: fadeIn 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; }
|
|
||||||
to { opacity: 1; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: rgba(255, 255, 255, 0.98);
|
background: #fff;
|
||||||
border-radius: 25rpx;
|
border-radius: 12rpx;
|
||||||
width: 85%;
|
width: 80%;
|
||||||
max-width: 600rpx;
|
max-width: 600rpx;
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
box-shadow: 0 20rpx 60rpx rgba(93, 64, 55, 0.3);
|
|
||||||
animation: slideUp 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideUp {
|
|
||||||
from {
|
|
||||||
transform: translateY(100rpx);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 35rpx 30rpx;
|
padding: 30rpx;
|
||||||
background: linear-gradient(135deg, #C8956E, #F5D59E);
|
border-bottom: 1rpx solid #eee;
|
||||||
border-bottom: 3rpx solid #5D4037;
|
|
||||||
border-radius: 22rpx 22rpx 0 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-title {
|
.modal-title {
|
||||||
font-size: 34rpx;
|
font-size: 32rpx;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #5D4037;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-btn {
|
.close-btn {
|
||||||
font-size: 50rpx;
|
font-size: 48rpx;
|
||||||
color: #5D4037;
|
color: #999;
|
||||||
width: 60rpx;
|
|
||||||
height: 60rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 2rpx solid #5D4037;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn:active {
|
|
||||||
transform: scale(0.9);
|
|
||||||
background: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
padding: 40rpx 30rpx;
|
padding: 30rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-item {
|
.form-item {
|
||||||
margin-bottom: 35rpx;
|
margin-bottom: 30rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
color: #5D4037;
|
color: #333;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 25rpx;
|
padding: 20rpx;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: #f8f8f8;
|
||||||
border-radius: 15rpx;
|
border-radius: 8rpx;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
box-sizing: border-box;
|
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
outline: none;
|
|
||||||
color: #5D4037;
|
|
||||||
box-shadow: 0 4rpx 15rpx rgba(200, 149, 110, 0.1);
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input:focus {
|
|
||||||
border-color: #FFD700;
|
|
||||||
box-shadow: 0 0 0 4rpx rgba(255, 215, 0, 0.3), 0 8rpx 25rpx rgba(255, 215, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input::placeholder {
|
|
||||||
color: #BCAAA4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-btn {
|
.submit-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 96rpx;
|
height: 88rpx;
|
||||||
background: linear-gradient(135deg, #C8956E, #F5D59E, #FFD700);
|
background: #667eea;
|
||||||
color: #5D4037;
|
color: #fff;
|
||||||
border-radius: 50rpx;
|
border-radius: 12rpx;
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
font-weight: bold;
|
border: none;
|
||||||
border: 4rpx solid #5D4037;
|
|
||||||
box-shadow: 0 10rpx 30rpx rgba(200, 149, 110, 0.4);
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-btn:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
box-shadow: 0 5rpx 15rpx rgba(200, 149, 110, 0.3);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@ -1,726 +0,0 @@
|
|||||||
<template>
|
|
||||||
<scroll-view scroll-y="true" class="page-scroll">
|
|
||||||
<view class="container">
|
|
||||||
<view class="header">
|
|
||||||
<text class="title">黄金实时价格</text>
|
|
||||||
<text class="subtitle">黄金9999 (1053)</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 当前价格卡片 -->
|
|
||||||
<view class="price-card" v-if="currentPrice">
|
|
||||||
<view class="price-main">
|
|
||||||
<view class="price-main-left">
|
|
||||||
<text class="price-label">当前价格</text>
|
|
||||||
<text class="price-value">¥{{ formatPrice(currentPrice.price) }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="price-main-right">
|
|
||||||
<view class="refresh-btn" @click="handleRefreshPrice" :class="{ loading: refreshing }">
|
|
||||||
<text>{{ refreshing ? '查询中...' : '立马查询' }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="price-change" :class="priceChangeClass">
|
|
||||||
<text class="change-value">{{ priceChangeText }}</text>
|
|
||||||
<text class="change-percent">{{ priceChangePercentText }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="price-details">
|
|
||||||
<view class="detail-item">
|
|
||||||
<text class="detail-label">最高</text>
|
|
||||||
<text class="detail-value">¥{{ formatPrice(currentPrice.highPrice) }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="detail-item">
|
|
||||||
<text class="detail-label">最低</text>
|
|
||||||
<text class="detail-value">¥{{ formatPrice(currentPrice.lowPrice) }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="detail-item">
|
|
||||||
<text class="detail-label">开盘</text>
|
|
||||||
<text class="detail-value">¥{{ formatPrice(currentPrice.openPrice) }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="detail-item">
|
|
||||||
<text class="detail-label">昨收</text>
|
|
||||||
<text class="detail-value">¥{{ formatPrice(currentPrice.yesterdayClose) }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="update-time">
|
|
||||||
<text>更新时间: {{ currentPrice.updateTime }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 加载状态 -->
|
|
||||||
<view class="loading-card" v-if="loading">
|
|
||||||
<text class="loading-text">加载中...</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 无数据状态 -->
|
|
||||||
<view class="error-card" v-if="!loading && !error && !currentPrice">
|
|
||||||
<text class="error-text">暂无黄金价格数据,请稍后再试</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 错误状态 -->
|
|
||||||
<view class="error-card" v-if="error && !loading">
|
|
||||||
<text class="error-text">{{ error }}</text>
|
|
||||||
<view class="retry-btn" @click="loadCurrentPrice">
|
|
||||||
<text>重试</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 日期切换区域 -->
|
|
||||||
<view class="date-switch-section" v-if="currentDate">
|
|
||||||
<view class="date-switch-container">
|
|
||||||
<view class="date-btn" @click="goToPreviousDay" :class="{ disabled: isFirstDay }">
|
|
||||||
<text>前一天</text>
|
|
||||||
</view>
|
|
||||||
<view class="date-display">
|
|
||||||
<text class="date-text">{{ currentDate }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="date-btn" @click="goToNextDay" :class="{ disabled: isToday }">
|
|
||||||
<text>后一天</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 历史价格图表 -->
|
|
||||||
<view class="chart-section" v-if="dailyPrices.length > 0">
|
|
||||||
<text class="section-title">{{ currentDate }} 价格走势</text>
|
|
||||||
<view class="chart-container">
|
|
||||||
<canvas canvas-id="priceChart" id="priceChart" type="2d" class="price-chart"></canvas>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 无数据提示 -->
|
|
||||||
<view class="error-card" v-if="!loading && dailyPrices.length === 0 && currentDate">
|
|
||||||
<text class="error-text">该日期暂无数据</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 历史价格列表 -->
|
|
||||||
<view class="history-section" v-if="dailyPrices.length > 0">
|
|
||||||
<text class="section-title">{{ currentDate }} 价格记录</text>
|
|
||||||
<view class="history-list">
|
|
||||||
<view class="history-item" v-for="(item, index) in dailyPrices" :key="index">
|
|
||||||
<text class="history-date">{{ item.updateTime }}</text>
|
|
||||||
<text class="history-price">¥{{ formatPrice(item.price) }}</text>
|
|
||||||
<text class="history-change" :class="getChangeClass(item.priceChange)">
|
|
||||||
{{ formatChange(item.priceChange) }}
|
|
||||||
</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 底部预留空白区域,避免历史记录被底部导航遮挡 -->
|
|
||||||
<view class="bottom-spacer"></view>
|
|
||||||
</view>
|
|
||||||
</scroll-view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, onMounted } from 'vue';
|
|
||||||
import { getCurrentGoldPrice, getGoldPricesByDate, getLatestGoldPriceDate, refreshGoldPrice } from '../../api/gold-price';
|
|
||||||
|
|
||||||
const currentPrice = ref(null);
|
|
||||||
const dailyPrices = ref([]);
|
|
||||||
const currentDate = ref('');
|
|
||||||
const loading = ref(false);
|
|
||||||
const error = ref('');
|
|
||||||
const refreshing = ref(false);
|
|
||||||
|
|
||||||
const priceChangeClass = computed(() => {
|
|
||||||
if (!currentPrice.value || !currentPrice.value.priceChange) return '';
|
|
||||||
return currentPrice.value.priceChange >= 0 ? 'up' : 'down';
|
|
||||||
});
|
|
||||||
|
|
||||||
const priceChangeText = computed(() => {
|
|
||||||
if (!currentPrice.value || currentPrice.value.priceChange === null) return '--';
|
|
||||||
const change = currentPrice.value.priceChange;
|
|
||||||
return change >= 0 ? `+${formatPrice(change)}` : formatPrice(change);
|
|
||||||
});
|
|
||||||
|
|
||||||
const priceChangePercentText = computed(() => {
|
|
||||||
if (!currentPrice.value || currentPrice.value.priceChangePercent === null) return '--';
|
|
||||||
const percent = currentPrice.value.priceChangePercent;
|
|
||||||
return percent >= 0 ? `+${percent.toFixed(2)}%` : `${percent.toFixed(2)}%`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const isToday = computed(() => {
|
|
||||||
if (!currentDate.value) return false;
|
|
||||||
const today = formatDate(new Date());
|
|
||||||
return currentDate.value === today;
|
|
||||||
});
|
|
||||||
|
|
||||||
const isFirstDay = computed(() => {
|
|
||||||
// 简单判断:如果当前日期是今天,则不能往前
|
|
||||||
// 实际应该查询数据库是否有更早的数据,这里简化处理
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const formatPrice = (price) => {
|
|
||||||
if (price === null || price === undefined) return '--';
|
|
||||||
return Number(price).toFixed(2);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatChange = (change) => {
|
|
||||||
if (change === null || change === undefined) return '--';
|
|
||||||
return change >= 0 ? `+${Number(change).toFixed(2)}` : Number(change).toFixed(2);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getChangeClass = (change) => {
|
|
||||||
if (change === null || change === undefined) return '';
|
|
||||||
return change >= 0 ? 'up' : 'down';
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (date) => {
|
|
||||||
const year = date.getFullYear();
|
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
|
||||||
return `${year}-${month}-${day}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTime = (dateTimeStr) => {
|
|
||||||
if (!dateTimeStr) return '';
|
|
||||||
// dateTimeStr 格式: "2024-01-15 10:15:00"
|
|
||||||
return dateTimeStr;
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadCurrentPrice = async () => {
|
|
||||||
loading.value = true;
|
|
||||||
error.value = '';
|
|
||||||
try {
|
|
||||||
const res = await getCurrentGoldPrice();
|
|
||||||
if (res && res.code === 200) {
|
|
||||||
if (res.data) {
|
|
||||||
currentPrice.value = res.data;
|
|
||||||
} else {
|
|
||||||
currentPrice.value = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
error.value = res?.message || '获取价格失败';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('获取当前价格失败', e);
|
|
||||||
error.value = '网络请求失败,请稍后重试';
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRefreshPrice = async () => {
|
|
||||||
if (refreshing.value) return;
|
|
||||||
|
|
||||||
refreshing.value = true;
|
|
||||||
try {
|
|
||||||
const res = await refreshGoldPrice('1053');
|
|
||||||
if (res && res.code === 200) {
|
|
||||||
if (res.data) {
|
|
||||||
currentPrice.value = res.data;
|
|
||||||
uni.showToast({
|
|
||||||
title: '查询成功',
|
|
||||||
icon: 'success'
|
|
||||||
});
|
|
||||||
// 如果当前查看的是今天,刷新当天的价格列表
|
|
||||||
const today = formatDate(new Date());
|
|
||||||
if (currentDate.value === today) {
|
|
||||||
await loadDailyPrices(today);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
uni.showToast({
|
|
||||||
title: '查询失败',
|
|
||||||
icon: 'none'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
uni.showToast({
|
|
||||||
title: res?.message || '查询失败',
|
|
||||||
icon: 'none'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('立即查询失败', e);
|
|
||||||
uni.showToast({
|
|
||||||
title: '查询失败,请稍后重试',
|
|
||||||
icon: 'none'
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
refreshing.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadDailyPrices = async (date) => {
|
|
||||||
try {
|
|
||||||
const res = await getGoldPricesByDate('1053', date);
|
|
||||||
if (res && res.code === 200) {
|
|
||||||
dailyPrices.value = res.data || [];
|
|
||||||
if (dailyPrices.value.length > 0) {
|
|
||||||
// 确保数据按时间排序
|
|
||||||
dailyPrices.value.sort((a, b) => {
|
|
||||||
return new Date(a.updateTime) - new Date(b.updateTime);
|
|
||||||
});
|
|
||||||
// 延迟绘制图表,确保 canvas 已渲染
|
|
||||||
setTimeout(() => {
|
|
||||||
drawChart();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('获取当天价格失败', e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const goToPreviousDay = () => {
|
|
||||||
if (isFirstDay.value) return;
|
|
||||||
const date = new Date(currentDate.value);
|
|
||||||
date.setDate(date.getDate() - 1);
|
|
||||||
currentDate.value = formatDate(date);
|
|
||||||
loadDailyPrices(currentDate.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const goToNextDay = () => {
|
|
||||||
if (isToday.value) return;
|
|
||||||
const date = new Date(currentDate.value);
|
|
||||||
date.setDate(date.getDate() + 1);
|
|
||||||
const newDate = formatDate(date);
|
|
||||||
const today = formatDate(new Date());
|
|
||||||
if (newDate > today) return;
|
|
||||||
currentDate.value = newDate;
|
|
||||||
loadDailyPrices(currentDate.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawChart = () => {
|
|
||||||
const ctx = uni.createCanvasContext('priceChart');
|
|
||||||
const data = [...dailyPrices.value];
|
|
||||||
|
|
||||||
if (data.length === 0) return;
|
|
||||||
|
|
||||||
const width = 320;
|
|
||||||
const height = 180;
|
|
||||||
const padding = 40;
|
|
||||||
const chartWidth = width - padding * 2;
|
|
||||||
const chartHeight = height - padding * 2;
|
|
||||||
|
|
||||||
const prices = data.map((item) => Number(item.price));
|
|
||||||
const minPrice = Math.min(...prices) * 0.998;
|
|
||||||
const maxPrice = Math.max(...prices) * 1.002;
|
|
||||||
const priceRange = maxPrice - minPrice;
|
|
||||||
|
|
||||||
// 绘制背景
|
|
||||||
ctx.setFillStyle('#FFF8E1');
|
|
||||||
ctx.fillRect(0, 0, width, height);
|
|
||||||
|
|
||||||
// 绘制网格线
|
|
||||||
ctx.setStrokeStyle('#E0E0E0');
|
|
||||||
ctx.setLineWidth(0.5);
|
|
||||||
for (let i = 0; i <= 4; i++) {
|
|
||||||
const y = padding + (chartHeight / 4) * i;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(padding, y);
|
|
||||||
ctx.lineTo(width - padding, y);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制价格线
|
|
||||||
ctx.setStrokeStyle('#FFB300');
|
|
||||||
ctx.setLineWidth(2);
|
|
||||||
ctx.beginPath();
|
|
||||||
|
|
||||||
data.forEach((item, index) => {
|
|
||||||
const x = padding + (chartWidth / (data.length - 1 || 1)) * index;
|
|
||||||
const y = padding + chartHeight - ((Number(item.price) - minPrice) / priceRange) * chartHeight;
|
|
||||||
|
|
||||||
if (index === 0) {
|
|
||||||
ctx.moveTo(x, y);
|
|
||||||
} else {
|
|
||||||
ctx.lineTo(x, y);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
// 绘制数据点
|
|
||||||
ctx.setFillStyle('#FF8F00');
|
|
||||||
data.forEach((item, index) => {
|
|
||||||
const x = padding + (chartWidth / (data.length - 1 || 1)) * index;
|
|
||||||
const y = padding + chartHeight - ((Number(item.price) - minPrice) / priceRange) * chartHeight;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(x, y, 3, 0, 2 * Math.PI);
|
|
||||||
ctx.fill();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 绘制Y轴标签
|
|
||||||
ctx.setFillStyle('#5D4037');
|
|
||||||
ctx.setFontSize(10);
|
|
||||||
for (let i = 0; i <= 4; i++) {
|
|
||||||
const price = maxPrice - (priceRange / 4) * i;
|
|
||||||
const y = padding + (chartHeight / 4) * i;
|
|
||||||
ctx.fillText(price.toFixed(1), 5, y + 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制X轴标签(时间点)
|
|
||||||
ctx.setFillStyle('#5D4037');
|
|
||||||
ctx.setFontSize(9);
|
|
||||||
const labelInterval = Math.max(1, Math.floor(data.length / 6)); // 最多显示6个标签
|
|
||||||
data.forEach((item, index) => {
|
|
||||||
if (index % labelInterval === 0 || index === data.length - 1) {
|
|
||||||
const x = padding + (chartWidth / (data.length - 1 || 1)) * index;
|
|
||||||
// 从 updateTime 提取时间部分(HH:mm)
|
|
||||||
const timeStr = item.updateTime ? item.updateTime.split(' ')[1]?.substring(0, 5) : '';
|
|
||||||
if (timeStr) {
|
|
||||||
ctx.fillText(timeStr, x - 15, height - 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.draw();
|
|
||||||
};
|
|
||||||
|
|
||||||
const initPage = async () => {
|
|
||||||
// 先加载当前价格
|
|
||||||
await loadCurrentPrice();
|
|
||||||
|
|
||||||
// 获取最近有数据的日期
|
|
||||||
try {
|
|
||||||
const res = await getLatestGoldPriceDate('1053');
|
|
||||||
if (res && res.code === 200 && res.data) {
|
|
||||||
const latestDate = res.data;
|
|
||||||
const today = formatDate(new Date());
|
|
||||||
// 如果最近有数据的日期是今天,则显示今天;否则显示最近有数据的日期
|
|
||||||
currentDate.value = latestDate === today ? today : latestDate;
|
|
||||||
} else {
|
|
||||||
// 如果没有数据,默认显示今天
|
|
||||||
currentDate.value = formatDate(new Date());
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('获取最近日期失败', e);
|
|
||||||
// 出错时默认显示今天
|
|
||||||
currentDate.value = formatDate(new Date());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载当前日期的数据
|
|
||||||
if (currentDate.value) {
|
|
||||||
await loadDailyPrices(currentDate.value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
initPage();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.page-scroll {
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
min-height: 100vh;
|
|
||||||
background: linear-gradient(135deg, #ffe5cc 0%, #ffd4a8 50%, #ffc08a 100%);
|
|
||||||
padding: 20rpx;
|
|
||||||
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
text-align: center;
|
|
||||||
padding: 30rpx 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: block;
|
|
||||||
font-size: 44rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #5d4037;
|
|
||||||
text-shadow: 2rpx 2rpx 0 #ffd700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
display: block;
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #8d6e63;
|
|
||||||
margin-top: 10rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-card {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
border-radius: 20rpx;
|
|
||||||
padding: 30rpx;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
border: 3rpx solid #c8956e;
|
|
||||||
box-shadow: 0 8rpx 20rpx rgba(200, 149, 110, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-main {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-main-left {
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-main-right {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
margin-left: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-label {
|
|
||||||
display: block;
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #8d6e63;
|
|
||||||
margin-bottom: 10rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-value {
|
|
||||||
display: block;
|
|
||||||
font-size: 60rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #5d4037;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn {
|
|
||||||
padding: 12rpx 24rpx;
|
|
||||||
background: linear-gradient(135deg, #c8956e, #ffd700);
|
|
||||||
border-radius: 20rpx;
|
|
||||||
border: 2rpx solid #5d4037;
|
|
||||||
text-align: center;
|
|
||||||
min-width: 120rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn text {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #5d4037;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn.loading {
|
|
||||||
opacity: 0.7;
|
|
||||||
background: linear-gradient(135deg, #bdbdbd, #e0e0e0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn.loading text {
|
|
||||||
color: #757575;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-change {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 20rpx;
|
|
||||||
margin-bottom: 30rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-change.up .change-value,
|
|
||||||
.price-change.up .change-percent {
|
|
||||||
color: #fa3534;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-change.down .change-value,
|
|
||||||
.price-change.down .change-percent {
|
|
||||||
color: #19be6b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-value,
|
|
||||||
.change-percent {
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-details {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 15rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-item {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 45%;
|
|
||||||
background: linear-gradient(135deg, rgba(200, 149, 110, 0.1), rgba(255, 215, 0, 0.1));
|
|
||||||
padding: 15rpx;
|
|
||||||
border-radius: 10rpx;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-label {
|
|
||||||
display: block;
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #8d6e63;
|
|
||||||
margin-bottom: 5rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-value {
|
|
||||||
display: block;
|
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #5d4037;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-time {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 20rpx;
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #bcaaa4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-card,
|
|
||||||
.error-card {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
border-radius: 20rpx;
|
|
||||||
padding: 60rpx 30rpx;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
border: 3rpx solid #c8956e;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-text {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #8d6e63;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-text {
|
|
||||||
display: block;
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #fa3534;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.retry-btn {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 15rpx 40rpx;
|
|
||||||
background: linear-gradient(135deg, #c8956e, #ffd700);
|
|
||||||
border-radius: 30rpx;
|
|
||||||
border: 2rpx solid #5d4037;
|
|
||||||
}
|
|
||||||
|
|
||||||
.retry-btn text {
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #5d4037;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-switch-section {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
border-radius: 20rpx;
|
|
||||||
padding: 20rpx 30rpx;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
border: 3rpx solid #c8956e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-switch-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-btn {
|
|
||||||
flex: 1;
|
|
||||||
padding: 15rpx 20rpx;
|
|
||||||
background: linear-gradient(135deg, #c8956e, #ffd700);
|
|
||||||
border-radius: 15rpx;
|
|
||||||
border: 2rpx solid #5d4037;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-btn text {
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #5d4037;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-btn.disabled {
|
|
||||||
background: #e0e0e0;
|
|
||||||
border-color: #bdbdbd;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-btn.disabled text {
|
|
||||||
color: #9e9e9e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-display {
|
|
||||||
flex: 2;
|
|
||||||
text-align: center;
|
|
||||||
padding: 0 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-text {
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #5d4037;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-section,
|
|
||||||
.history-section {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
border-radius: 20rpx;
|
|
||||||
padding: 30rpx;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
border: 3rpx solid #c8956e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
display: block;
|
|
||||||
font-size: 30rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #5d4037;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-container {
|
|
||||||
width: 100%;
|
|
||||||
height: 360rpx;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-chart {
|
|
||||||
width: 640rpx;
|
|
||||||
height: 360rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-list {
|
|
||||||
max-height: 500rpx;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20rpx 0;
|
|
||||||
border-bottom: 1rpx solid rgba(200, 149, 110, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-date {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #8d6e63;
|
|
||||||
flex: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-price {
|
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #5d4037;
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-change {
|
|
||||||
font-size: 26rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
flex: 1;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-change.up {
|
|
||||||
color: #fa3534;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-change.down {
|
|
||||||
color: #19be6b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-spacer {
|
|
||||||
height: 200px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,51 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<scroll-view class="page-scroll" scroll-y="true" show-scrollbar="false">
|
|
||||||
<view class="container">
|
<view class="container">
|
||||||
<view class="header">
|
<view class="header">
|
||||||
<view class="header-top">
|
|
||||||
<text class="title">我的账单</text>
|
<text class="title">我的账单</text>
|
||||||
<view class="header-actions">
|
<view class="summary">
|
||||||
<view class="action-btn" @click="goToGoldPrice">
|
<view class="summary-item">
|
||||||
<text class="action-icon">🪙</text>
|
<text class="label">本月支出</text>
|
||||||
|
<text class="amount expense">¥{{ monthlyExpense.toFixed(2) }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 收入支出汇总 -->
|
|
||||||
<view class="summary-row">
|
|
||||||
<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>
|
</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 class="bill-list">
|
<view class="bill-list">
|
||||||
<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">
|
||||||
@ -74,28 +42,19 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
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(() => {
|
||||||
@ -144,22 +103,6 @@ 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({
|
||||||
@ -167,50 +110,9 @@ const viewBill = (bill) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToAccount = () => {
|
|
||||||
// 跳转到账户管理页面
|
|
||||||
uni.navigateTo({
|
|
||||||
url: '/pages/account/account'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const goToGoldPrice = () => {
|
|
||||||
// 跳转到黄金价格页面
|
|
||||||
// 注意:在APK中路径必须与pages.json中的配置完全一致
|
|
||||||
uni.navigateTo({
|
|
||||||
url: '/pages/gold-price/gold-price',
|
|
||||||
success: () => {
|
|
||||||
console.log('跳转成功')
|
|
||||||
},
|
|
||||||
fail: (err) => {
|
|
||||||
console.error('跳转失败:', err)
|
|
||||||
// 如果navigateTo失败,尝试使用reLaunch
|
|
||||||
uni.reLaunch({
|
|
||||||
url: '/pages/gold-price/gold-price',
|
|
||||||
fail: (err2) => {
|
|
||||||
console.error('reLaunch也失败:', err2)
|
|
||||||
uni.showToast({
|
|
||||||
title: '页面跳转失败,请检查路径配置',
|
|
||||||
icon: 'none',
|
|
||||||
duration: 2000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
@ -227,14 +129,12 @@ onMounted(() => {
|
|||||||
|
|
||||||
loadBills()
|
loadBills()
|
||||||
loadMonthlyStatistics()
|
loadMonthlyStatistics()
|
||||||
loadBudget()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听刷新事件
|
// 监听刷新事件
|
||||||
uni.$on('refreshBills', () => {
|
uni.$on('refreshBills', () => {
|
||||||
loadBills()
|
loadBills()
|
||||||
loadMonthlyStatistics()
|
loadMonthlyStatistics()
|
||||||
loadBudget()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -256,193 +156,45 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 页面滚动容器 */
|
|
||||||
.page-scroll {
|
|
||||||
height: 100vh;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 卡通风格配色 */
|
|
||||||
.container {
|
.container {
|
||||||
min-height: 100vh;
|
min-height: calc(100vh - 100rpx);
|
||||||
background: linear-gradient(135deg, #FFE5CC 0%, #FFD4A8 50%, #FFC08A 100%);
|
background: #f5f5f5;
|
||||||
padding-bottom: calc(170rpx + env(safe-area-inset-bottom));
|
padding-bottom: calc(170rpx + env(safe-area-inset-bottom));
|
||||||
position: relative;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 背景装饰圆球 */
|
|
||||||
.container::before,
|
|
||||||
.container::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 50%;
|
|
||||||
opacity: 0.2;
|
|
||||||
filter: blur(60rpx);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container::before {
|
|
||||||
width: 300rpx;
|
|
||||||
height: 300rpx;
|
|
||||||
background: linear-gradient(135deg, #C8956E, #FFB6C1);
|
|
||||||
top: 100rpx;
|
|
||||||
right: -50rpx;
|
|
||||||
animation: float 8s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container::after {
|
|
||||||
width: 250rpx;
|
|
||||||
height: 250rpx;
|
|
||||||
background: linear-gradient(135deg, #FFD700, #F5D59E);
|
|
||||||
bottom: 200rpx;
|
|
||||||
left: -50rpx;
|
|
||||||
animation: float 10s ease-in-out infinite reverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes float {
|
|
||||||
0%, 100% { transform: translate(0, 0); }
|
|
||||||
50% { transform: translate(-20rpx, 30rpx); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
background: linear-gradient(135deg, #C8956E 0%, #F5D59E 50%, #FFD700 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
padding: 40rpx 30rpx;
|
padding: 40rpx 30rpx;
|
||||||
color: #5D4037;
|
color: #fff;
|
||||||
border-radius: 0 0 40rpx 40rpx;
|
|
||||||
box-shadow: 0 10rpx 30rpx rgba(93, 64, 55, 0.15);
|
|
||||||
border: 3rpx solid #5D4037;
|
|
||||||
border-top: none;
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-top {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 30rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 44rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
display: block;
|
|
||||||
text-shadow:
|
|
||||||
2rpx 2rpx 0 #FFD700,
|
|
||||||
-1rpx -1rpx 0 #FFD700,
|
|
||||||
0 4rpx 10rpx rgba(255, 215, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 15rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
width: 70rpx;
|
|
||||||
height: 70rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
border-radius: 50%;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
border: 3rpx solid #5D4037;
|
|
||||||
box-shadow: 0 4rpx 15rpx rgba(93, 64, 55, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn:active {
|
|
||||||
background: #fff;
|
|
||||||
transform: scale(0.9);
|
|
||||||
box-shadow: 0 2rpx 8rpx rgba(93, 64, 55, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-icon {
|
|
||||||
font-size: 40rpx;
|
font-size: 40rpx;
|
||||||
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.1));
|
font-weight: bold;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-row {
|
.summary {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
margin-bottom: 25rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-item {
|
.summary-item {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 26rpx;
|
font-size: 24rpx;
|
||||||
opacity: 0.85;
|
opacity: 0.8;
|
||||||
margin-bottom: 10rpx;
|
margin-bottom: 10rpx;
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.amount {
|
.amount {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 40rpx;
|
font-size: 36rpx;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount.income {
|
|
||||||
color: #19BE6B;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount.expense {
|
|
||||||
color: #FA3534;
|
|
||||||
}
|
|
||||||
|
|
||||||
.budget-section {
|
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
backdrop-filter: blur(10rpx);
|
|
||||||
border-radius: 20rpx;
|
|
||||||
padding: 30rpx 20rpx 20rpx;
|
|
||||||
border: 3rpx solid #5D4037;
|
|
||||||
box-shadow: 0 8rpx 20rpx rgba(93, 64, 55, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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: linear-gradient(135deg, rgba(200, 149, 110, 0.2), rgba(255, 215, 0, 0.2));
|
|
||||||
padding: 20rpx 15rpx;
|
|
||||||
border-radius: 15rpx;
|
|
||||||
border: 2rpx solid #C8956E;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
display: block;
|
|
||||||
font-size: 24rpx;
|
|
||||||
opacity: 0.85;
|
|
||||||
margin-bottom: 10rpx;
|
|
||||||
color: #5D4037;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
display: block;
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #5D4037;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bill-list {
|
.bill-list {
|
||||||
@ -450,24 +202,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bill-group {
|
.bill-group {
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: #fff;
|
||||||
border-radius: 20rpx;
|
border-radius: 12rpx;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
box-shadow: 0 8rpx 20rpx rgba(200, 149, 110, 0.15);
|
|
||||||
animation: slideIn 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20rpx);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-header {
|
.group-header {
|
||||||
@ -475,32 +213,26 @@ export default {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20rpx 30rpx;
|
padding: 20rpx 30rpx;
|
||||||
background: linear-gradient(135deg, rgba(200, 149, 110, 0.15), rgba(255, 215, 0, 0.15));
|
background: #f8f8f8;
|
||||||
border-bottom: 2rpx solid #FFD700;
|
border-bottom: 1rpx solid #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
color: #5D4037;
|
color: #666;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.total {
|
.total {
|
||||||
font-size: 30rpx;
|
font-size: 28rpx;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #5D4037;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bill-item {
|
.bill-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 30rpx;
|
padding: 30rpx;
|
||||||
border-bottom: 1rpx solid rgba(200, 149, 110, 0.2);
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bill-item:active {
|
|
||||||
background: rgba(255, 215, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bill-item:last-child {
|
.bill-item:last-child {
|
||||||
@ -508,9 +240,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bill-icon {
|
.bill-icon {
|
||||||
font-size: 52rpx;
|
font-size: 48rpx;
|
||||||
margin-right: 20rpx;
|
margin-right: 20rpx;
|
||||||
filter: drop-shadow(0 2rpx 6rpx rgba(0, 0, 0, 0.15));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bill-info {
|
.bill-info {
|
||||||
@ -521,58 +252,46 @@ export default {
|
|||||||
|
|
||||||
.bill-category {
|
.bill-category {
|
||||||
font-size: 30rpx;
|
font-size: 30rpx;
|
||||||
color: #5D4037;
|
color: #333;
|
||||||
margin-bottom: 8rpx;
|
margin-bottom: 8rpx;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bill-desc {
|
.bill-desc {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #8D6E63;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bill-amount {
|
.bill-amount {
|
||||||
font-size: 34rpx;
|
font-size: 32rpx;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bill-amount.income {
|
.bill-amount.income {
|
||||||
color: #19BE6B;
|
color: #19be6b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bill-amount.expense {
|
.bill-amount.expense {
|
||||||
color: #FA3534;
|
color: #fa3534;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 120rpx 0;
|
padding: 100rpx 0;
|
||||||
color: #8D6E63;
|
color: #999;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
border-radius: 20rpx;
|
|
||||||
margin: 20rpx;
|
|
||||||
border: 3rpx dashed #C8956E;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty text:first-child {
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #5D4037;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-tip {
|
.empty-tip {
|
||||||
font-size: 26rpx;
|
font-size: 24rpx;
|
||||||
margin-top: 20rpx;
|
margin-top: 20rpx;
|
||||||
color: #BCAAA4;
|
color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 40rpx 0;
|
padding: 40rpx 0;
|
||||||
color: #8D6E63;
|
color: #999;
|
||||||
font-size: 28rpx;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,159 +1,65 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="login-container">
|
<view class="login-container">
|
||||||
<!-- 背景装饰圆球 -->
|
<view class="login-header">
|
||||||
<view class="bg-ball ball-1"></view>
|
|
||||||
<view class="bg-ball ball-2"></view>
|
|
||||||
<view class="bg-ball ball-3"></view>
|
|
||||||
<view class="bg-ball ball-4"></view>
|
|
||||||
|
|
||||||
<!-- Logo区域 -->
|
|
||||||
<view class="login-header fade-in">
|
|
||||||
<image class="app-logo" src="/static/images/logo.png" mode="aspectFit"></image>
|
|
||||||
<view class="title-decoration">
|
|
||||||
<view class="deco-line left"></view>
|
|
||||||
<text class="app-title">强宝爱记账</text>
|
<text class="app-title">强宝爱记账</text>
|
||||||
<view class="deco-line right"></view>
|
|
||||||
</view>
|
|
||||||
<text class="app-subtitle">智能记账,轻松管理</text>
|
<text class="app-subtitle">智能记账,轻松管理</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 登录表单卡片 -->
|
<view class="login-form">
|
||||||
<view class="login-form glass-card">
|
|
||||||
<!-- 用户名输入框 -->
|
|
||||||
<view class="form-item">
|
<view class="form-item">
|
||||||
<view :class="['input-wrapper', focusField === 'username' ? 'focused' : '']">
|
|
||||||
<text class="input-icon">👤</text>
|
|
||||||
<input
|
<input
|
||||||
v-model="form.username"
|
v-model="form.username"
|
||||||
placeholder="请输入用户名"
|
placeholder="请输入用户名"
|
||||||
class="input"
|
class="input"
|
||||||
maxlength="20"
|
maxlength="20"
|
||||||
@focus="focusField = 'username'"
|
|
||||||
@blur="focusField = ''"
|
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 密码输入框 -->
|
|
||||||
<view class="form-item">
|
<view class="form-item">
|
||||||
<view :class="['input-wrapper', focusField === 'password' ? 'focused' : '']">
|
|
||||||
<text class="input-icon">🔒</text>
|
|
||||||
<input
|
<input
|
||||||
v-model="form.password"
|
v-model="form.password"
|
||||||
placeholder="请输入密码"
|
placeholder="请输入密码"
|
||||||
:type="showPassword ? 'text' : 'password'"
|
type="password"
|
||||||
class="input"
|
class="input"
|
||||||
maxlength="20"
|
maxlength="20"
|
||||||
@focus="focusField = 'password'"
|
|
||||||
@blur="focusField = ''"
|
|
||||||
/>
|
/>
|
||||||
<text class="eye-icon" @click="togglePassword">
|
|
||||||
{{ showPassword ? '👁️' : '👁️🗨️' }}
|
|
||||||
</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 昵称输入框(注册时) -->
|
|
||||||
<view v-if="isRegister" class="form-item">
|
<view v-if="isRegister" class="form-item">
|
||||||
<view :class="['input-wrapper', focusField === 'nickname' ? 'focused' : '']">
|
|
||||||
<text class="input-icon">✨</text>
|
|
||||||
<input
|
<input
|
||||||
v-model="form.nickname"
|
v-model="form.nickname"
|
||||||
placeholder="请输入昵称(可选)"
|
placeholder="请输入昵称(可选)"
|
||||||
class="input"
|
class="input"
|
||||||
maxlength="20"
|
maxlength="20"
|
||||||
@focus="focusField = 'nickname'"
|
|
||||||
@blur="focusField = ''"
|
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 记住密码和忘记密码 -->
|
<button class="login-btn" @click="handleSubmit" :loading="loading">
|
||||||
<view v-if="!isRegister" class="options-row">
|
{{ isRegister ? '注册' : '登录' }}
|
||||||
<view class="checkbox-wrapper" @click="rememberMe = !rememberMe">
|
|
||||||
<view :class="['checkbox', rememberMe ? 'checked' : '']">
|
|
||||||
<text v-if="rememberMe" class="check-icon">✓</text>
|
|
||||||
</view>
|
|
||||||
<text class="checkbox-label">记住密码</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 协议勾选(注册时) -->
|
|
||||||
<view v-if="isRegister" class="options-row">
|
|
||||||
<view class="checkbox-wrapper" @click="agreeProtocol = !agreeProtocol">
|
|
||||||
<view :class="['checkbox', agreeProtocol ? 'checked' : '']">
|
|
||||||
<text v-if="agreeProtocol" class="check-icon">✓</text>
|
|
||||||
</view>
|
|
||||||
<text class="checkbox-label">我已阅读并同意用户协议和隐私政策</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 登录/注册按钮 -->
|
|
||||||
<button class="login-btn gradient-btn" @click="handleSubmit" :loading="loading" :disabled="loading">
|
|
||||||
<view v-if="!loading" class="btn-content">
|
|
||||||
{{ isRegister ? '🎉 立即注册' : '🚀 立即登录' }}
|
|
||||||
</view>
|
|
||||||
<view v-else class="btn-loading">
|
|
||||||
<view class="loading-spinner"></view>
|
|
||||||
<text>{{ isRegister ? '注册中...' : '登录中...' }}</text>
|
|
||||||
</view>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- 切换模式 -->
|
|
||||||
<view class="switch-mode" @click="switchMode">
|
<view class="switch-mode" @click="switchMode">
|
||||||
<text>{{ isRegister ? '已有账号?去登录 →' : '没有账号?去注册 →' }}</text>
|
<text>{{ isRegister ? '已有账号?去登录' : '没有账号?去注册' }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { register, login } from '../../api/auth'
|
import { register, login } from '../../api/auth'
|
||||||
import { useUserStore } from '../../store/user'
|
import { useUserStore } from '../../store/user'
|
||||||
import { encryptPassword, decryptPassword } from '../../utils/crypto'
|
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const isRegister = ref(false)
|
const isRegister = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const showPassword = ref(false)
|
|
||||||
const focusField = ref('')
|
|
||||||
const rememberMe = ref(false)
|
|
||||||
const agreeProtocol = ref(false)
|
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
nickname: ''
|
nickname: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// 从本地存储加载记住的密码
|
|
||||||
onMounted(() => {
|
|
||||||
try {
|
|
||||||
const savedUsername = uni.getStorageSync('saved_username')
|
|
||||||
const encryptedPassword = uni.getStorageSync('saved_password')
|
|
||||||
|
|
||||||
if (savedUsername && encryptedPassword) {
|
|
||||||
form.value.username = savedUsername
|
|
||||||
// 解密密码
|
|
||||||
form.value.password = decryptPassword(encryptedPassword)
|
|
||||||
rememberMe.value = true
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载保存的密码失败:', error)
|
|
||||||
// 清除可能损坏的数据
|
|
||||||
uni.removeStorageSync('saved_username')
|
|
||||||
uni.removeStorageSync('saved_password')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 切换密码显示/隐藏
|
|
||||||
const togglePassword = () => {
|
|
||||||
showPassword.value = !showPassword.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换登录/注册模式
|
|
||||||
const switchMode = () => {
|
const switchMode = () => {
|
||||||
isRegister.value = !isRegister.value
|
isRegister.value = !isRegister.value
|
||||||
form.value = {
|
form.value = {
|
||||||
@ -161,10 +67,8 @@ const switchMode = () => {
|
|||||||
password: '',
|
password: '',
|
||||||
nickname: ''
|
nickname: ''
|
||||||
}
|
}
|
||||||
agreeProtocol.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交表单
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!form.value.username || !form.value.password) {
|
if (!form.value.username || !form.value.password) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
@ -182,15 +86,6 @@ const handleSubmit = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册时检查协议
|
|
||||||
if (isRegister.value && !agreeProtocol.value) {
|
|
||||||
uni.showToast({
|
|
||||||
title: '请先阅读并同意用户协议',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -214,18 +109,8 @@ const handleSubmit = async () => {
|
|||||||
nickname: response.nickname
|
nickname: response.nickname
|
||||||
})
|
})
|
||||||
|
|
||||||
// 记住密码
|
|
||||||
if (!isRegister.value && rememberMe.value) {
|
|
||||||
uni.setStorageSync('saved_username', form.value.username)
|
|
||||||
// 加密密码后保存
|
|
||||||
uni.setStorageSync('saved_password', encryptPassword(form.value.password))
|
|
||||||
} else if (!rememberMe.value) {
|
|
||||||
uni.removeStorageSync('saved_username')
|
|
||||||
uni.removeStorageSync('saved_password')
|
|
||||||
}
|
|
||||||
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: isRegister.value ? '🎉 注册成功' : '🚀 登录成功',
|
title: isRegister.value ? '注册成功' : '登录成功',
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -249,436 +134,73 @@ const handleSubmit = async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 主容器 - 卡通背景 */
|
|
||||||
.login-container {
|
.login-container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: linear-gradient(135deg, #FFE5CC 0%, #FFD4A8 50%, #FFC08A 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 40rpx;
|
padding: 40rpx;
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 背景装饰圆球 */
|
|
||||||
.bg-ball {
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 50%;
|
|
||||||
opacity: 0.3;
|
|
||||||
filter: blur(80rpx);
|
|
||||||
animation: float 6s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ball-1 {
|
|
||||||
width: 400rpx;
|
|
||||||
height: 400rpx;
|
|
||||||
background: linear-gradient(135deg, #C8956E, #FFB6C1);
|
|
||||||
top: -100rpx;
|
|
||||||
left: -100rpx;
|
|
||||||
animation-delay: 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ball-2 {
|
|
||||||
width: 300rpx;
|
|
||||||
height: 300rpx;
|
|
||||||
background: linear-gradient(135deg, #F5D59E, #FFD700);
|
|
||||||
top: 200rpx;
|
|
||||||
right: -50rpx;
|
|
||||||
animation-delay: 1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ball-3 {
|
|
||||||
width: 350rpx;
|
|
||||||
height: 350rpx;
|
|
||||||
background: linear-gradient(135deg, #FFB6C1, #C8956E);
|
|
||||||
bottom: 100rpx;
|
|
||||||
left: 50rpx;
|
|
||||||
animation-delay: 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ball-4 {
|
|
||||||
width: 250rpx;
|
|
||||||
height: 250rpx;
|
|
||||||
background: linear-gradient(135deg, #FFD700, #F5D59E);
|
|
||||||
bottom: -50rpx;
|
|
||||||
right: 100rpx;
|
|
||||||
animation-delay: 3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes float {
|
|
||||||
0%, 100% {
|
|
||||||
transform: translate(0, 0) scale(1);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
transform: translate(20rpx, -30rpx) scale(1.1);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translate(-20rpx, 20rpx) scale(0.9);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
transform: translate(30rpx, 10rpx) scale(1.05);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Logo区域 */
|
|
||||||
.login-header {
|
.login-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 60rpx;
|
margin-bottom: 80rpx;
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 入场动画 */
|
|
||||||
.fade-in {
|
|
||||||
animation: fadeInDown 0.8s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeInDown {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-50rpx);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Logo图片 */
|
|
||||||
.app-logo {
|
|
||||||
width: 280rpx;
|
|
||||||
height: 280rpx;
|
|
||||||
margin-bottom: 30rpx;
|
|
||||||
filter: drop-shadow(0 8rpx 20rpx rgba(93, 64, 55, 0.3));
|
|
||||||
animation: bounce 2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bounce {
|
|
||||||
0%, 100% {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translateY(-20rpx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 标题装饰容器 */
|
|
||||||
.title-decoration {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 20rpx;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 装饰线条 */
|
|
||||||
.deco-line {
|
|
||||||
width: 60rpx;
|
|
||||||
height: 6rpx;
|
|
||||||
background: linear-gradient(90deg, #C8956E, #FFD700);
|
|
||||||
border-radius: 10rpx;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deco-line::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 12rpx;
|
|
||||||
height: 12rpx;
|
|
||||||
background: #FFD700;
|
|
||||||
border-radius: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
box-shadow: 0 0 10rpx #FFD700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deco-line.left::after {
|
|
||||||
left: -6rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deco-line.right::after {
|
|
||||||
right: -6rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 标题文字 */
|
|
||||||
.app-title {
|
.app-title {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 56rpx;
|
font-size: 56rpx;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #5D4037;
|
color: #fff;
|
||||||
text-shadow:
|
margin-bottom: 20rpx;
|
||||||
3rpx 3rpx 0 #FFD700,
|
|
||||||
-1rpx -1rpx 0 #FFD700,
|
|
||||||
1rpx -1rpx 0 #FFD700,
|
|
||||||
-1rpx 1rpx 0 #FFD700,
|
|
||||||
0 8rpx 20rpx rgba(255, 215, 0, 0.4);
|
|
||||||
letter-spacing: 4rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 副标题 */
|
|
||||||
.app-subtitle {
|
.app-subtitle {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
color: #8D6E63;
|
color: rgba(255, 255, 255, 0.8);
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表单卡片 - 毛玻璃效果 */
|
|
||||||
.login-form {
|
.login-form {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 600rpx;
|
max-width: 600rpx;
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-card {
|
|
||||||
background: rgba(255, 255, 255, 0.75);
|
|
||||||
backdrop-filter: blur(20rpx);
|
|
||||||
border-radius: 40rpx;
|
|
||||||
padding: 60rpx 50rpx;
|
|
||||||
box-shadow:
|
|
||||||
0 20rpx 60rpx rgba(93, 64, 55, 0.15),
|
|
||||||
0 0 0 2rpx rgba(255, 255, 255, 0.5),
|
|
||||||
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.8);
|
|
||||||
border: 3rpx solid #5D4037;
|
|
||||||
animation: slideUp 0.8s ease-out 0.3s both;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideUp {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(100rpx);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 表单项 */
|
|
||||||
.form-item {
|
.form-item {
|
||||||
margin-bottom: 30rpx;
|
margin-bottom: 30rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 输入框容器 */
|
|
||||||
.input-wrapper {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
border-radius: 20rpx;
|
|
||||||
padding: 0 30rpx;
|
|
||||||
height: 96rpx;
|
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
box-shadow: 0 4rpx 15rpx rgba(200, 149, 110, 0.2);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-wrapper.focused {
|
|
||||||
border-color: #FFD700;
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 4rpx rgba(255, 215, 0, 0.3),
|
|
||||||
0 8rpx 25rpx rgba(255, 215, 0, 0.4);
|
|
||||||
transform: translateY(-2rpx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 输入框图标 */
|
|
||||||
.input-icon {
|
|
||||||
font-size: 40rpx;
|
|
||||||
margin-right: 20rpx;
|
|
||||||
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.1));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 输入框 */
|
|
||||||
.input {
|
.input {
|
||||||
flex: 1;
|
width: 100%;
|
||||||
height: 100%;
|
height: 88rpx;
|
||||||
font-size: 28rpx;
|
box-sizing: border-box;
|
||||||
color: #5D4037;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input::placeholder {
|
|
||||||
color: #BCAAA4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 眼睛图标 */
|
|
||||||
.eye-icon {
|
|
||||||
font-size: 36rpx;
|
|
||||||
padding: 10rpx;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eye-icon:active {
|
|
||||||
transform: scale(0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 选项行 */
|
|
||||||
.options-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 30rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 复选框容器 */
|
|
||||||
.checkbox-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 复选框 */
|
|
||||||
.checkbox {
|
|
||||||
width: 40rpx;
|
|
||||||
height: 40rpx;
|
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
border-radius: 10rpx;
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
display: flex;
|
border-radius: 12rpx;
|
||||||
align-items: center;
|
padding: 0 30rpx;
|
||||||
justify-content: center;
|
|
||||||
margin-right: 15rpx;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox.checked {
|
|
||||||
background: linear-gradient(135deg, #FFD700, #F5D59E);
|
|
||||||
border-color: #FFD700;
|
|
||||||
box-shadow: 0 0 10rpx rgba(255, 215, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.check-icon {
|
|
||||||
color: #5D4037;
|
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 复选框标签 */
|
|
||||||
.checkbox-label {
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #6D4C41;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 渐变按钮 */
|
|
||||||
.login-btn {
|
.login-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100rpx;
|
height: 88rpx;
|
||||||
background: linear-gradient(135deg, #C8956E 0%, #F5D59E 50%, #FFD700 100%);
|
background: #fff;
|
||||||
background-size: 200% 200%;
|
color: #667eea;
|
||||||
color: #5D4037;
|
border-radius: 12rpx;
|
||||||
border-radius: 50rpx;
|
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-top: 40rpx;
|
margin-top: 40rpx;
|
||||||
border: 4rpx solid #5D4037;
|
border: none;
|
||||||
box-shadow:
|
|
||||||
0 10rpx 30rpx rgba(200, 149, 110, 0.4),
|
|
||||||
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.6);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-btn::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: -100%;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg,
|
|
||||||
transparent,
|
|
||||||
rgba(255, 255, 255, 0.4),
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
transition: left 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-btn:active::before {
|
|
||||||
left: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-btn:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
box-shadow:
|
|
||||||
0 5rpx 15rpx rgba(200, 149, 110, 0.3),
|
|
||||||
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-btn[disabled] {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 按钮内容 */
|
|
||||||
.btn-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 加载动画 */
|
|
||||||
.btn-loading {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
width: 40rpx;
|
|
||||||
height: 40rpx;
|
|
||||||
border: 4rpx solid rgba(93, 64, 55, 0.2);
|
|
||||||
border-top-color: #5D4037;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 切换模式 */
|
|
||||||
.switch-mode {
|
.switch-mode {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 40rpx;
|
margin-top: 40rpx;
|
||||||
padding: 20rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-mode text {
|
.switch-mode text {
|
||||||
color: #6D4C41;
|
color: rgba(255, 255, 255, 0.9);
|
||||||
font-size: 28rpx;
|
font-size: 26rpx;
|
||||||
font-weight: 500;
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch-mode text::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: -5rpx;
|
|
||||||
left: 0;
|
|
||||||
width: 0;
|
|
||||||
height: 3rpx;
|
|
||||||
background: linear-gradient(90deg, #C8956E, #FFD700);
|
|
||||||
transition: width 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch-mode:active text {
|
|
||||||
color: #FFD700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch-mode:active text::after {
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,586 +0,0 @@
|
|||||||
<template>
|
|
||||||
<scroll-view scroll-y class="mine-page">
|
|
||||||
<view class="mine-container">
|
|
||||||
<!-- 用户信息卡片 -->
|
|
||||||
<view class="user-card">
|
|
||||||
<view class="user-avatar">👤</view>
|
|
||||||
<view class="user-info">
|
|
||||||
<text class="user-name">{{ userInfo.nickname || userInfo.username || '用户' }}</text>
|
|
||||||
<text class="user-id">ID: {{ userInfo.id || '-' }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 账户余额卡片 -->
|
|
||||||
<view class="balance-card" @click="openBalanceModal">
|
|
||||||
<view class="balance-header">
|
|
||||||
<text class="balance-title">账户余额</text>
|
|
||||||
<text class="balance-arrow">></text>
|
|
||||||
</view>
|
|
||||||
<text class="balance-amount" :class="account.balance >= 0 ? 'balance-positive' : 'balance-negative'">
|
|
||||||
¥{{ account.balance ? account.balance.toFixed(2) : '0.00' }}
|
|
||||||
</text>
|
|
||||||
<view class="balance-stats">
|
|
||||||
<view class="balance-stat-item">
|
|
||||||
<text class="balance-stat-label">总收入</text>
|
|
||||||
<text class="balance-stat-value income">¥{{ account.totalIncome ? account.totalIncome.toFixed(2) : '0.00' }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="balance-stat-item">
|
|
||||||
<text class="balance-stat-label">总支出</text>
|
|
||||||
<text class="balance-stat-value expense">¥{{ account.totalExpense ? account.totalExpense.toFixed(2) : '0.00' }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 预算卡片 -->
|
|
||||||
<view class="budget-card" @click="openBudgetModal">
|
|
||||||
<view class="budget-header">
|
|
||||||
<text class="budget-title">本月预算</text>
|
|
||||||
<text class="budget-arrow">></text>
|
|
||||||
</view>
|
|
||||||
<view class="budget-content">
|
|
||||||
<view class="budget-progress-wrapper">
|
|
||||||
<view class="budget-progress-bar">
|
|
||||||
<view class="budget-progress-fill" :style="{ width: budgetProgress + '%' }"></view>
|
|
||||||
</view>
|
|
||||||
<text class="budget-progress-text">{{ budgetProgress.toFixed(0) }}%</text>
|
|
||||||
</view>
|
|
||||||
<view class="budget-stats">
|
|
||||||
<view class="budget-stat-item">
|
|
||||||
<text class="budget-stat-label">预算</text>
|
|
||||||
<text class="budget-stat-value">¥{{ budget.amount ? budget.amount.toFixed(2) : '0.00' }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="budget-stat-item">
|
|
||||||
<text class="budget-stat-label">已用</text>
|
|
||||||
<text class="budget-stat-value expense">¥{{ budget.usedAmount ? budget.usedAmount.toFixed(2) : '0.00' }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="budget-stat-item">
|
|
||||||
<text class="budget-stat-label">剩余</text>
|
|
||||||
<text class="budget-stat-value" :class="budget.remainingAmount >= 0 ? 'positive' : 'negative'">
|
|
||||||
¥{{ budget.remainingAmount !== undefined ? budget.remainingAmount.toFixed(2) : '0.00' }}
|
|
||||||
</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 功能菜单 -->
|
|
||||||
<view class="menu-section">
|
|
||||||
<view class="menu-item" @click="goToStatistics">
|
|
||||||
<view class="menu-icon-wrapper">
|
|
||||||
<text class="menu-icon">📊</text>
|
|
||||||
</view>
|
|
||||||
<text class="menu-text">统计分析</text>
|
|
||||||
<text class="menu-arrow">></text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="menu-item" @click="goToAccount">
|
|
||||||
<view class="menu-icon-wrapper">
|
|
||||||
<text class="menu-icon">💰</text>
|
|
||||||
</view>
|
|
||||||
<text class="menu-text">账户管理</text>
|
|
||||||
<text class="menu-arrow">></text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="menu-item" @click="goToBudget">
|
|
||||||
<view class="menu-icon-wrapper">
|
|
||||||
<text class="menu-icon">📝</text>
|
|
||||||
</view>
|
|
||||||
<text class="menu-text">预算设置</text>
|
|
||||||
<text class="menu-arrow">></text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 退出登录按钮 -->
|
|
||||||
<button class="logout-button" @click="logout">退出登录</button>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 编辑弹窗 -->
|
|
||||||
<EditModal
|
|
||||||
:visible="modalState.visible"
|
|
||||||
:title="modalState.title"
|
|
||||||
:placeholder="modalState.placeholder"
|
|
||||||
:currentValue="modalState.currentValue"
|
|
||||||
@confirm="handleModalConfirm"
|
|
||||||
@cancel="handleModalCancel"
|
|
||||||
/>
|
|
||||||
</scroll-view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, onMounted } from 'vue'
|
|
||||||
import { getAccount } from '../../api/account'
|
|
||||||
import { getBudget } from '../../api/budget'
|
|
||||||
import { updateAccountBalance, updateBudget } from '../../api/edit-modal'
|
|
||||||
import EditModal from '../../components/EditModal/EditModal.vue'
|
|
||||||
|
|
||||||
const userInfo = ref({
|
|
||||||
id: null,
|
|
||||||
username: '',
|
|
||||||
nickname: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const account = ref({
|
|
||||||
balance: 0,
|
|
||||||
totalIncome: 0,
|
|
||||||
totalExpense: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
const budget = ref({
|
|
||||||
amount: 0,
|
|
||||||
usedAmount: 0,
|
|
||||||
remainingAmount: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
const modalState = ref({
|
|
||||||
visible: false,
|
|
||||||
title: '',
|
|
||||||
placeholder: '',
|
|
||||||
currentValue: '',
|
|
||||||
type: '' // 'balance' 或 'budget'
|
|
||||||
})
|
|
||||||
|
|
||||||
const budgetProgress = computed(() => {
|
|
||||||
if (!budget.value.amount || budget.value.amount === 0) return 0
|
|
||||||
const progress = (budget.value.usedAmount / budget.value.amount) * 100
|
|
||||||
return Math.min(progress, 100)
|
|
||||||
})
|
|
||||||
|
|
||||||
const loadUserInfo = () => {
|
|
||||||
try {
|
|
||||||
const info = uni.getStorageSync('userInfo')
|
|
||||||
if (info) {
|
|
||||||
userInfo.value = typeof info === 'string' ? JSON.parse(info) : info
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载用户信息失败', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadAccount = async () => {
|
|
||||||
try {
|
|
||||||
const data = await getAccount()
|
|
||||||
if (data) {
|
|
||||||
account.value = data
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载账户失败', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadBudget = async () => {
|
|
||||||
try {
|
|
||||||
const data = await getBudget()
|
|
||||||
if (data) {
|
|
||||||
budget.value = data
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载预算失败', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const openBalanceModal = () => {
|
|
||||||
modalState.value = {
|
|
||||||
visible: true,
|
|
||||||
title: '编辑账户余额',
|
|
||||||
placeholder: '请输入新的账户余额',
|
|
||||||
currentValue: account.value.balance || 0,
|
|
||||||
type: 'balance'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const openBudgetModal = () => {
|
|
||||||
modalState.value = {
|
|
||||||
visible: true,
|
|
||||||
title: '编辑本月预算',
|
|
||||||
placeholder: '请输入新的预算金额',
|
|
||||||
currentValue: budget.value.amount || 0,
|
|
||||||
type: 'budget'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleModalConfirm = async (value) => {
|
|
||||||
try {
|
|
||||||
if (modalState.value.type === 'balance') {
|
|
||||||
await updateAccountBalance(parseFloat(value))
|
|
||||||
account.value.balance = parseFloat(value)
|
|
||||||
uni.showToast({
|
|
||||||
title: '账户余额已更新',
|
|
||||||
icon: 'success',
|
|
||||||
duration: 2000
|
|
||||||
})
|
|
||||||
} else if (modalState.value.type === 'budget') {
|
|
||||||
await updateBudget(parseFloat(value))
|
|
||||||
budget.value.amount = parseFloat(value)
|
|
||||||
uni.showToast({
|
|
||||||
title: '预算已更新',
|
|
||||||
icon: 'success',
|
|
||||||
duration: 2000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
handleModalCancel()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新失败', error)
|
|
||||||
uni.showToast({
|
|
||||||
title: '更新失败,请重试',
|
|
||||||
icon: 'error',
|
|
||||||
duration: 2000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleModalCancel = () => {
|
|
||||||
modalState.value.visible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const goToStatistics = () => {
|
|
||||||
uni.switchTab({
|
|
||||||
url: '/pages/statistics/statistics'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const goToAccount = () => {
|
|
||||||
uni.navigateTo({
|
|
||||||
url: '/pages/account/account'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const goToBudget = () => {
|
|
||||||
uni.navigateTo({
|
|
||||||
url: '/pages/budget/budget'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
uni.showModal({
|
|
||||||
title: '提示',
|
|
||||||
content: '确定要退出登录吗?',
|
|
||||||
success: (res) => {
|
|
||||||
if (res.confirm) {
|
|
||||||
uni.removeStorageSync('token')
|
|
||||||
uni.removeStorageSync('userInfo')
|
|
||||||
uni.reLaunch({
|
|
||||||
url: '/pages/login/login'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadUserInfo()
|
|
||||||
loadAccount()
|
|
||||||
loadBudget()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.mine-page {
|
|
||||||
height: 100vh;
|
|
||||||
background: linear-gradient(135deg, #FFE5CC 0%, #FFD4A8 50%, #FFC08A 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mine-container {
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 20rpx;
|
|
||||||
padding-bottom: calc(170rpx + env(safe-area-inset-bottom));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 用户信息卡片 */
|
|
||||||
.user-card {
|
|
||||||
background: linear-gradient(135deg, #C8956E, #F5D59E, #FFD700);
|
|
||||||
border-radius: 25rpx;
|
|
||||||
padding: 40rpx 30rpx;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border: 4rpx solid #5D4037;
|
|
||||||
box-shadow: 0 10rpx 30rpx rgba(93, 64, 55, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-avatar {
|
|
||||||
width: 120rpx;
|
|
||||||
height: 120rpx;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 60rpx;
|
|
||||||
margin-right: 30rpx;
|
|
||||||
border: 3rpx solid #5D4037;
|
|
||||||
box-shadow: 0 4rpx 15rpx rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-name {
|
|
||||||
display: block;
|
|
||||||
font-size: 36rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #5D4037;
|
|
||||||
margin-bottom: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-id {
|
|
||||||
display: block;
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #8D6E63;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 账户余额卡片 */
|
|
||||||
.balance-card {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
border-radius: 25rpx;
|
|
||||||
padding: 35rpx 30rpx;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
box-shadow: 0 10rpx 30rpx rgba(200, 149, 110, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-title {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #8D6E63;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-arrow {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #C8956E;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-amount {
|
|
||||||
font-size: 56rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 25rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-positive {
|
|
||||||
color: #FFD700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-negative {
|
|
||||||
color: #FA3534;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-stats {
|
|
||||||
display: flex;
|
|
||||||
gap: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-stat-item {
|
|
||||||
flex: 1;
|
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
border-radius: 15rpx;
|
|
||||||
padding: 20rpx;
|
|
||||||
text-align: center;
|
|
||||||
border: 2rpx solid #C8956E;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-stat-label {
|
|
||||||
display: block;
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #8D6E63;
|
|
||||||
margin-bottom: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-stat-value {
|
|
||||||
display: block;
|
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #5D4037;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-stat-value.income {
|
|
||||||
color: #19BE6B;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-stat-value.expense {
|
|
||||||
color: #FA3534;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预算卡片 */
|
|
||||||
.budget-card {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
border-radius: 25rpx;
|
|
||||||
padding: 35rpx 30rpx;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
box-shadow: 0 10rpx 30rpx rgba(200, 149, 110, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.budget-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 25rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.budget-title {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #8D6E63;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.budget-arrow {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #C8956E;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.budget-content {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.budget-progress-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.budget-progress-bar {
|
|
||||||
flex: 1;
|
|
||||||
height: 20rpx;
|
|
||||||
background: rgba(200, 149, 110, 0.2);
|
|
||||||
border-radius: 20rpx;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-right: 15rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.budget-progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, #C8956E, #FFD700);
|
|
||||||
border-radius: 20rpx;
|
|
||||||
transition: width 0.5s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.budget-progress-text {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #5D4037;
|
|
||||||
font-weight: bold;
|
|
||||||
min-width: 60rpx;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.budget-stats {
|
|
||||||
display: flex;
|
|
||||||
gap: 15rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.budget-stat-item {
|
|
||||||
flex: 1;
|
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
border-radius: 15rpx;
|
|
||||||
padding: 20rpx;
|
|
||||||
text-align: center;
|
|
||||||
border: 2rpx solid #C8956E;
|
|
||||||
}
|
|
||||||
|
|
||||||
.budget-stat-label {
|
|
||||||
display: block;
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #8D6E63;
|
|
||||||
margin-bottom: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.budget-stat-value {
|
|
||||||
display: block;
|
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #5D4037;
|
|
||||||
}
|
|
||||||
|
|
||||||
.budget-stat-value.positive {
|
|
||||||
color: #19BE6B;
|
|
||||||
}
|
|
||||||
|
|
||||||
.budget-stat-value.negative {
|
|
||||||
color: #FA3534;
|
|
||||||
}
|
|
||||||
|
|
||||||
.budget-stat-value.expense {
|
|
||||||
color: #FA3534;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 功能菜单 */
|
|
||||||
.menu-section {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
border-radius: 25rpx;
|
|
||||||
padding: 20rpx 0;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
box-shadow: 0 10rpx 30rpx rgba(200, 149, 110, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 30rpx 30rpx;
|
|
||||||
transition: background 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item:active {
|
|
||||||
background: rgba(255, 215, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-icon-wrapper {
|
|
||||||
width: 80rpx;
|
|
||||||
height: 80rpx;
|
|
||||||
background: linear-gradient(135deg, rgba(200, 149, 110, 0.2), rgba(255, 215, 0, 0.2));
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-right: 25rpx;
|
|
||||||
border: 2rpx solid #C8956E;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-icon {
|
|
||||||
font-size: 40rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-text {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 30rpx;
|
|
||||||
color: #5D4037;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-arrow {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #C8956E;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 退出登录按钮 */
|
|
||||||
.logout-button {
|
|
||||||
width: 100%;
|
|
||||||
height: 96rpx;
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
color: #FA3534;
|
|
||||||
border-radius: 50rpx;
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
border: 3rpx solid #FA3534;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-shadow: 0 8rpx 20rpx rgba(250, 53, 52, 0.2);
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout-button:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
background: rgba(250, 53, 52, 0.1);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -122,157 +122,88 @@ onMounted(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.container {
|
.container {
|
||||||
min-height: calc(100vh - 100rpx);
|
min-height: calc(100vh - 100rpx);
|
||||||
background: linear-gradient(135deg, #FFE5CC 0%, #FFD4A8 50%, #FFC08A 100%);
|
background: #f5f5f5;
|
||||||
padding: 20rpx;
|
padding: 20rpx;
|
||||||
padding-bottom: calc(170rpx + env(safe-area-inset-bottom));
|
padding-bottom: calc(170rpx + env(safe-area-inset-bottom));
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 背景装饰 */
|
|
||||||
.container::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 280rpx;
|
|
||||||
height: 280rpx;
|
|
||||||
background: linear-gradient(135deg, #C8956E, #FFB6C1);
|
|
||||||
border-radius: 50%;
|
|
||||||
opacity: 0.2;
|
|
||||||
filter: blur(60rpx);
|
|
||||||
top: 150rpx;
|
|
||||||
left: -80rpx;
|
|
||||||
animation: float 10s ease-in-out infinite;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes float {
|
|
||||||
0%, 100% { transform: translate(0, 0); }
|
|
||||||
50% { transform: translate(-15rpx, 25rpx); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: #fff;
|
||||||
border-radius: 20rpx;
|
border-radius: 12rpx;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
padding: 10rpx;
|
padding: 10rpx;
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
box-shadow: 0 8rpx 20rpx rgba(200, 149, 110, 0.15);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20rpx;
|
padding: 20rpx;
|
||||||
border-radius: 15rpx;
|
border-radius: 8rpx;
|
||||||
color: #8D6E63;
|
color: #666;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab.active {
|
.tab.active {
|
||||||
background: linear-gradient(135deg, #C8956E, #F5D59E);
|
background: #667eea;
|
||||||
color: #5D4037;
|
color: #fff;
|
||||||
box-shadow: 0 4rpx 15rpx rgba(200, 149, 110, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-card {
|
.summary-card {
|
||||||
background: linear-gradient(135deg, #C8956E 0%, #F5D59E 50%, #FFD700 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
border-radius: 25rpx;
|
border-radius: 12rpx;
|
||||||
padding: 50rpx 40rpx;
|
padding: 40rpx;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
border: 4rpx solid #5D4037;
|
|
||||||
box-shadow: 0 15rpx 40rpx rgba(93, 64, 55, 0.2);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-card::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -50%;
|
|
||||||
right: -50%;
|
|
||||||
width: 200%;
|
|
||||||
height: 200%;
|
|
||||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.2) 0%, transparent 70%);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-item {
|
.summary-item {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #5D4037;
|
color: #fff;
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 26rpx;
|
font-size: 24rpx;
|
||||||
opacity: 0.85;
|
opacity: 0.8;
|
||||||
margin-bottom: 12rpx;
|
margin-bottom: 10rpx;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.amount {
|
.amount {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 40rpx;
|
font-size: 36rpx;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-shadow: 2rpx 2rpx 0 rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount.income {
|
|
||||||
color: #19BE6B;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount.expense {
|
|
||||||
color: #FA3534;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount.balance {
|
|
||||||
color: #5D4037;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-stats {
|
.category-stats {
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: #fff;
|
||||||
border-radius: 25rpx;
|
border-radius: 12rpx;
|
||||||
padding: 40rpx 30rpx;
|
padding: 30rpx;
|
||||||
border: 3rpx solid #C8956E;
|
|
||||||
box-shadow: 0 10rpx 30rpx rgba(200, 149, 110, 0.15);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 34rpx;
|
font-size: 32rpx;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 35rpx;
|
margin-bottom: 30rpx;
|
||||||
color: #5D4037;
|
color: #333;
|
||||||
text-shadow: 2rpx 2rpx 0 rgba(255, 215, 0, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-item {
|
.category-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 25rpx 0;
|
padding: 20rpx 0;
|
||||||
border-bottom: 2rpx solid rgba(200, 149, 110, 0.2);
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-item:last-child {
|
.category-item:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-item:active {
|
|
||||||
background: rgba(255, 215, 0, 0.05);
|
|
||||||
padding-left: 10rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-icon {
|
.category-icon {
|
||||||
font-size: 56rpx;
|
font-size: 48rpx;
|
||||||
margin-right: 25rpx;
|
margin-right: 20rpx;
|
||||||
filter: drop-shadow(0 3rpx 6rpx rgba(0, 0, 0, 0.15));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-info {
|
.category-info {
|
||||||
@ -281,48 +212,35 @@ onMounted(() => {
|
|||||||
|
|
||||||
.category-name {
|
.category-name {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 30rpx;
|
font-size: 28rpx;
|
||||||
color: #5D4037;
|
color: #333;
|
||||||
margin-bottom: 12rpx;
|
margin-bottom: 10rpx;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 12rpx;
|
height: 8rpx;
|
||||||
background: rgba(200, 149, 110, 0.2);
|
background: #f0f0f0;
|
||||||
border-radius: 20rpx;
|
border-radius: 4rpx;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: inset 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-fill {
|
.progress-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, #C8956E, #FFD700);
|
background: #667eea;
|
||||||
transition: width 0.5s ease-out;
|
transition: width 0.3s;
|
||||||
border-radius: 20rpx;
|
|
||||||
box-shadow: 0 0 10rpx rgba(255, 215, 0, 0.5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-amount {
|
.category-amount {
|
||||||
font-size: 32rpx;
|
font-size: 28rpx;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #5D4037;
|
color: #333;
|
||||||
margin-left: 25rpx;
|
margin-left: 20rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 100rpx 0;
|
padding: 60rpx 0;
|
||||||
color: #8D6E63;
|
color: #999;
|
||||||
background: rgba(255, 255, 255, 0.5);
|
|
||||||
border-radius: 20rpx;
|
|
||||||
border: 3rpx dashed #C8956E;
|
|
||||||
margin: 20rpx 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty text {
|
|
||||||
font-size: 30rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.0 MiB |
@ -1,39 +0,0 @@
|
|||||||
# TabBar 图标说明
|
|
||||||
|
|
||||||
## 需要添加的图标
|
|
||||||
|
|
||||||
为了让"我的"页面正常显示,你需要添加以下两个图标文件:
|
|
||||||
|
|
||||||
### 1. mine.png(未选中状态)
|
|
||||||
- 文件路径:`frontend/static/tabbar/mine.png`
|
|
||||||
- 建议尺寸:81x81 像素
|
|
||||||
- 颜色:#7A7E83(灰色)
|
|
||||||
- 图标内容:用户头像或"我的"相关图标
|
|
||||||
|
|
||||||
### 2. mine-active.png(选中状态)
|
|
||||||
- 文件路径:`frontend/static/tabbar/mine-active.png`
|
|
||||||
- 建议尺寸:81x81 像素
|
|
||||||
- 颜色:#667eea(紫色)
|
|
||||||
- 图标内容:与mine.png相同,但颜色为选中状态
|
|
||||||
|
|
||||||
## 图标获取方式
|
|
||||||
|
|
||||||
1. **使用在线图标库**
|
|
||||||
- iconfont (https://www.iconfont.cn/)
|
|
||||||
- iconpark (https://iconpark.oceanengine.com/)
|
|
||||||
|
|
||||||
2. **使用设计工具**
|
|
||||||
- Figma
|
|
||||||
- Sketch
|
|
||||||
- Adobe Illustrator
|
|
||||||
|
|
||||||
3. **参考现有图标**
|
|
||||||
- 可以参考 home.png、add.png、statistics.png 的风格
|
|
||||||
- 保持统一的设计风格
|
|
||||||
|
|
||||||
## 临时解决方案
|
|
||||||
|
|
||||||
如果暂时没有图标,可以:
|
|
||||||
1. 复制 home.png 并重命名为 mine.png
|
|
||||||
2. 复制 home-active.png 并重命名为 mine-active.png
|
|
||||||
3. 这样至少可以让页面正常运行,之后再替换为合适的图标
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.5 KiB |
89
store/app.js
89
store/app.js
@ -1,89 +0,0 @@
|
|||||||
// 应用状态管理
|
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@ -1,396 +0,0 @@
|
|||||||
// 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
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
// 简单的密码加密/解密工具(Base64编码 + 简单混淆)
|
|
||||||
// 注意:这不是强加密,只是防止密码明文存储
|
|
||||||
|
|
||||||
const SECRET_KEY = 'QIANGBAO_AI_JIZHANG_2024' // 密钥
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加密密码
|
|
||||||
* @param {string} password - 原始密码
|
|
||||||
* @returns {string} - 加密后的密码
|
|
||||||
*/
|
|
||||||
export function encryptPassword(password) {
|
|
||||||
if (!password) return ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 将密码转为字符数组
|
|
||||||
const chars = password.split('')
|
|
||||||
|
|
||||||
// 2. 与密钥进行简单的XOR混淆
|
|
||||||
const encrypted = chars.map((char, index) => {
|
|
||||||
const keyChar = SECRET_KEY[index % SECRET_KEY.length]
|
|
||||||
return String.fromCharCode(char.charCodeAt(0) ^ keyChar.charCodeAt(0))
|
|
||||||
}).join('')
|
|
||||||
|
|
||||||
// 3. Base64编码
|
|
||||||
return btoa(unescape(encodeURIComponent(encrypted)))
|
|
||||||
} catch (error) {
|
|
||||||
console.error('密码加密失败:', error)
|
|
||||||
return password // 失败时返回原密码
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解密密码
|
|
||||||
* @param {string} encryptedPassword - 加密的密码
|
|
||||||
* @returns {string} - 解密后的密码
|
|
||||||
*/
|
|
||||||
export function decryptPassword(encryptedPassword) {
|
|
||||||
if (!encryptedPassword) return ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Base64解码
|
|
||||||
const decoded = decodeURIComponent(escape(atob(encryptedPassword)))
|
|
||||||
|
|
||||||
// 2. 与密钥进行XOR反混淆
|
|
||||||
const chars = decoded.split('')
|
|
||||||
const decrypted = chars.map((char, index) => {
|
|
||||||
const keyChar = SECRET_KEY[index % SECRET_KEY.length]
|
|
||||||
return String.fromCharCode(char.charCodeAt(0) ^ keyChar.charCodeAt(0))
|
|
||||||
}).join('')
|
|
||||||
|
|
||||||
return decrypted
|
|
||||||
} catch (error) {
|
|
||||||
console.error('密码解密失败:', error)
|
|
||||||
return '' // 失败时返回空字符串
|
|
||||||
}
|
|
||||||
}
|
|
||||||
325
utils/db.js
325
utils/db.js
@ -1,325 +0,0 @@
|
|||||||
// 本地数据库管理模块
|
|
||||||
|
|
||||||
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 }
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
// HTTP请求封装
|
// HTTP请求封装
|
||||||
const BASE_URL = process.env.NODE_ENV === 'development'
|
const BASE_URL = process.env.NODE_ENV === 'development'
|
||||||
? 'http://localhost:12345/api'
|
? 'http://localhost:8080/api'
|
||||||
: 'https://accounting.aqroid.cn/api'
|
: 'https://your-api-domain.com/api'
|
||||||
|
|
||||||
// 获取token
|
// 获取token
|
||||||
function getToken() {
|
function getToken() {
|
||||||
|
|||||||
373
utils/sync.js
373
utils/sync.js
@ -1,373 +0,0 @@
|
|||||||
// 数据同步功能实现
|
|
||||||
|
|
||||||
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