feat: 用户信息完善 + 一起记功能

- users 表添加 nickname、avatar_url 字段
- 用户信息 API(GET/PUT /me)+ 头像上传(multer)
- 头像通过 API 路由获取(公开,不经过 auth)
- 登录接口返回用户昵称和头像
- 用户信息 Store + 个人资料编辑页面
- 我的页面和首页显示真实用户信息
- 群组表(groups、group_members)+ transactions 添加 group_id
- 群组 API(创建/加入/退出/解散)
- 交易和统计 API 支持 group_id 视图切换
- 群组客户端 Store + 身份切换
- 群组管理页面
- App 初始化群组 Store
- UPLOAD_DIR 配置化
- Node.js 备份替代 mysqldump
- 统一前端 BASE_URL(config.ts)
- uploads 加入 .gitignore
- 修复 trust proxy 警告
This commit is contained in:
2026-06-03 17:04:22 +08:00
parent 9162b2c87c
commit 21f4d81959
45 changed files with 3145 additions and 348 deletions

View File

@@ -2,11 +2,12 @@
<view class="page">
<view class="header-fixed">
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
<view class="header">
<view class="header" :style="{ paddingRight: capsuleRight + 'px' }">
<view class="avatar-wrap">
<Icon name="user" :size="36" color="#8B7E7E" />
<image v-if="userStore.avatarUrl" :src="userStore.avatarUrl" class="avatar-img" mode="aspectFill" />
<Icon v-else name="user" :size="36" color="#8B7E7E" />
</view>
<text class="greeting">{{ greeting }}小菜</text>
<text class="greeting">{{ greeting }}{{ userStore.nickname }}</text>
<view class="bell" @tap="showNotification">
<Icon name="bell" :size="40" color="#8B7E7E" />
</view>
@@ -117,9 +118,12 @@ import { onShow, onPullDownRefresh } from '@dcloudio/uni-app'
import { useTransactionStore } from '@/stores/transaction'
import { useStatsStore } from '@/stores/stats'
import { useBudgetStore } from '@/stores/budget'
import { useUserStore } from '@/stores/user'
import { useGroupStore } from '@/stores/group'
import { waitForReady } from '@/utils/app-ready'
import { getOverview } from '@/api/stats'
import { formatAmount, formatAmountRaw } from '@/utils/format'
import { statusBarHeight } from '@/utils/system'
import { statusBarHeight, capsuleRight } from '@/utils/system'
import BudgetBar from '@/components/BudgetBar/BudgetBar.vue'
import TransactionItem from '@/components/TransactionItem/TransactionItem.vue'
import Icon from '@/components/Icon/Icon.vue'
@@ -128,11 +132,13 @@ import Skeleton from '@/components/Skeleton/Skeleton.vue'
const txStore = useTransactionStore()
const statsStore = useStatsStore()
const budgetStore = useBudgetStore()
const userStore = useUserStore()
const groupStore = useGroupStore()
const DEFAULT_BUDGET = 500000 // 5000元
const overview = ref({ expense: 0, income: 0, count: 0, daily: 0 })
const budget = ref<any>(null)
const overview = computed(() => statsStore.overview)
const budget = computed(() => budgetStore.budget)
const recentTx = ref<any[]>([])
const loading = ref(true)
const loadError = ref(false)
@@ -149,9 +155,10 @@ const greeting = computed(() => {
return '晚上好'
})
async function loadData() {
async function loadData(silent = false) {
await waitForReady()
try {
loading.value = true
if (!silent) loading.value = true
loadError.value = false
const now = new Date()
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
@@ -159,14 +166,13 @@ async function loadData() {
statsStore.fetchOverview(),
budgetStore.fetchBudget(),
txStore.fetchTransactions({ page: 1 }),
fetchTodayData(today)
fetchTodayData(today),
userStore.fetchUserInfo()
])
overview.value = statsStore.overview
budget.value = budgetStore.budget
recentTx.value = txStore.transactions.slice(0, 5)
} catch (e) {
console.error('Load error:', e)
loadError.value = true
if (!silent) loadError.value = true
} finally {
loading.value = false
}
@@ -174,7 +180,7 @@ async function loadData() {
async function fetchTodayData(today: string) {
try {
const data = await getOverview({ startDate: today, endDate: today })
const data = await getOverview({ startDate: today, endDate: today, group_id: groupStore.currentGroupId })
todayExpense.value = data.expense || 0
todayIncome.value = data.income || 0
todayCount.value = data.count || 0
@@ -192,7 +198,7 @@ onMounted(() => {
// Refresh data when returning from other pages (e.g., after adding a transaction)
onShow(() => {
if (initialLoaded.value && !loading.value) loadData()
if (initialLoaded.value && !loading.value) loadData(true)
})
onPullDownRefresh(() => {
@@ -249,6 +255,12 @@ function goBills() {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.avatar-img {
width: 100%;
height: 100%;
}
.greeting {