完善版本

This commit is contained in:
2026-01-18 22:40:12 +08:00
parent de9c14f070
commit 7dae948257
20 changed files with 3058 additions and 633 deletions
@@ -89,4 +89,19 @@ public function yearly(Request $request)
return response()->json($this->data); return response()->json($this->data);
} }
/**
* 获取个人中心统计数据
*/
public function dashboard(Request $request)
{
try {
$this->data['data'] = $this->statisticsService->getDashboardStats($request);
} catch (\Throwable $th) {
$this->data['code'] = 0;
$this->data['message'] = $th->getMessage();
}
return response()->json($this->data);
}
} }
+50 -18
View File
@@ -21,21 +21,22 @@ public function getList(Request $request)
$month = $request->input('month'); $month = $request->input('month');
$startDate = $request->input('start_date'); $startDate = $request->input('start_date');
$endDate = $request->input('end_date'); $endDate = $request->input('end_date');
$familyId = $request->input('family_id'); $dataType = $request->input('data_type', 'family'); // 默认为家庭数据
// 获取用户所在的家庭ID // 获取用户所在的家庭ID
$userFamilyId = $this->getUserFamilyId($userId); $userFamilyId = $this->getUserFamilyId($userId);
$query = Bill::with(['user:uid,nickname,username', 'family:id,name']); $query = Bill::with(['user:uid,nickname,username', 'family:id,name']);
// 如果用户加入了家庭,且没有指定family_id,则显示家庭账单 // 根据data_type参数判断数据类型
if ($userFamilyId && !$familyId) { if ($dataType === 'personal') {
// 个人数据:只显示当前用户的账单
$query->where('user_id', $userId);
} elseif ($dataType === 'family' && $userFamilyId) {
// 家庭数据:显示用户所在家庭的账单
$query->where('family_id', $userFamilyId); $query->where('family_id', $userFamilyId);
} elseif ($familyId) {
// 如果指定了family_id,则显示该家庭的账单
$query->where('family_id', $familyId);
} else { } else {
// 否则显示个人账单 // 默认显示个人数据
$query->where('user_id', $userId); $query->where('user_id', $userId);
} }
@@ -66,13 +67,13 @@ public function getList(Request $request)
$monthExpense = Bill::query(); $monthExpense = Bill::query();
$monthIncome = Bill::query(); $monthIncome = Bill::query();
// 同样的家庭查询逻辑 // 同样的数据类型查询逻辑
if ($userFamilyId && !$familyId) { if ($dataType === 'personal') {
$monthExpense->where('user_id', $userId);
$monthIncome->where('user_id', $userId);
} elseif ($dataType === 'family' && $userFamilyId) {
$monthExpense->where('family_id', $userFamilyId); $monthExpense->where('family_id', $userFamilyId);
$monthIncome->where('family_id', $userFamilyId); $monthIncome->where('family_id', $userFamilyId);
} elseif ($familyId) {
$monthExpense->where('family_id', $familyId);
$monthIncome->where('family_id', $familyId);
} else { } else {
$monthExpense->where('user_id', $userId); $monthExpense->where('user_id', $userId);
$monthIncome->where('user_id', $userId); $monthIncome->where('user_id', $userId);
@@ -175,8 +176,14 @@ public function add(Request $request)
$data['category'] = $categoryMap[$data['category_id']]; $data['category'] = $categoryMap[$data['category_id']];
$data['bill_date'] = $data['date']; $data['bill_date'] = $data['date'];
// 如果前端没有指定family_id,自动获取用户所在的家庭
if (empty($data['family_id'])) {
$userFamilyId = $this->getUserFamilyId($userId);
if ($userFamilyId) {
$data['family_id'] = $userFamilyId;
}
} else {
// 检查家庭权限 // 检查家庭权限
if (!empty($data['family_id'])) {
$this->checkFamilyAccess($userId, $data['family_id']); $this->checkFamilyAccess($userId, $data['family_id']);
} }
@@ -233,17 +240,29 @@ public function edit(Request $request)
$data = $request->validate([ $data = $request->validate([
'type' => 'required|in:income,expense', 'type' => 'required|in:income,expense',
'amount' => 'required|numeric|min:0.01', 'amount' => 'required|numeric|min:0.01',
'category' => 'required|string|max:50', 'category_id' => 'required|integer',
'remark' => 'nullable|string|max:255', 'remark' => 'nullable|string|max:255',
'bill_date' => 'required|date', 'date' => 'required|date',
'family_id' => 'nullable|integer|exists:account_families,id' 'family_id' => 'nullable|integer|exists:account_families,id'
]); ]);
// 将category_id转换为category字符串
$categoryMap = $this->getCategoryMap($data['type']);
if (!isset($categoryMap[$data['category_id']])) {
throw new \Exception('分类不存在');
}
$data['category'] = $categoryMap[$data['category_id']];
$data['bill_date'] = $data['date'];
// 检查家庭权限 // 检查家庭权限
if (!empty($data['family_id'])) { if (!empty($data['family_id'])) {
$this->checkFamilyAccess($userId, $data['family_id']); $this->checkFamilyAccess($userId, $data['family_id']);
} }
// 移除前端字段
unset($data['category_id'], $data['date']);
$bill->update($data); $bill->update($data);
return $bill; return $bill;
@@ -278,12 +297,25 @@ public function detail(Request $request)
$bill = Bill::with(['user:uid,nickname,username', 'family:id,name,owner_id']) $bill = Bill::with(['user:uid,nickname,username', 'family:id,name,owner_id'])
->findOrFail($id); ->findOrFail($id);
// 验证权限 // 验证权限:只有账单创建者才能查看和编辑
if ($bill->user_id != $userId && !in_array($userId, $bill->family->members->pluck('uid')->toArray())) { if ($bill->user_id != $userId) {
throw new \Exception('无权查看此账单'); throw new \Exception('无权查看此账单');
} }
return $bill; // 转换数据格式以匹配前端
return [
'id' => $bill->id,
'type' => $bill->type,
'amount' => (float)$bill->amount,
'category' => $bill->category,
'category_id' => $this->getCategoryId($bill->category, $bill->type),
'remark' => $bill->remark,
'date' => $bill->bill_date,
'bill_date' => $bill->bill_date,
'created_at' => $bill->created_at->format('Y-m-d H:i:s'),
'user' => $bill->user,
'family' => $bill->family
];
} }
/** /**
@@ -17,9 +17,10 @@ public function getOverview(Request $request)
$userId = auth('api')->user()['uid']; $userId = auth('api')->user()['uid'];
$year = $request->input('year'); $year = $request->input('year');
$month = $request->input('month'); $month = $request->input('month');
$dataType = $request->input('data_type', 'family'); // 默认为家庭数据
// 获取用户所在的家庭ID // 获取用户所在的家庭ID
$familyId = $this->getUserFamilyId($userId); $userFamilyId = $this->getUserFamilyId($userId);
// 查询当月的账单 // 查询当月的账单
$query = Bill::query(); $query = Bill::query();
@@ -32,9 +33,15 @@ public function getOverview(Request $request)
->whereYear('bill_date', date('Y')); ->whereYear('bill_date', date('Y'));
} }
if ($familyId) { // 根据data_type参数判断数据类型
$query->where('family_id', $familyId); if ($dataType === 'personal') {
// 个人数据:只显示当前用户的账单
$query->where('user_id', $userId);
} elseif ($dataType === 'family' && $userFamilyId) {
// 家庭数据:显示用户所在家庭的账单
$query->where('family_id', $userFamilyId);
} else { } else {
// 默认显示个人数据
$query->where('user_id', $userId); $query->where('user_id', $userId);
} }
@@ -59,9 +66,10 @@ public function getTrend(Request $request)
$year = $request->input('year'); $year = $request->input('year');
$month = $request->input('month'); $month = $request->input('month');
$days = 7; // 最近7天 $days = 7; // 最近7天
$dataType = $request->input('data_type', 'family'); // 默认为家庭数据
// 获取用户所在的家庭ID // 获取用户所在的家庭ID
$familyId = $this->getUserFamilyId($userId); $userFamilyId = $this->getUserFamilyId($userId);
$data = []; $data = [];
for ($i = $days - 1; $i >= 0; $i--) { for ($i = $days - 1; $i >= 0; $i--) {
@@ -69,14 +77,19 @@ public function getTrend(Request $request)
$query = Bill::whereDate('bill_date', $date); $query = Bill::whereDate('bill_date', $date);
if ($familyId) { // 根据data_type参数判断数据类型
$query->where('family_id', $familyId); if ($dataType === 'personal') {
// 个人数据:只显示当前用户的账单
$query->where('user_id', $userId);
} elseif ($dataType === 'family' && $userFamilyId) {
// 家庭数据:显示用户所在家庭的账单
$query->where('family_id', $userFamilyId);
} else { } else {
// 默认显示个人数据
$query->where('user_id', $userId); $query->where('user_id', $userId);
} }
$bills = $query->get(); $bills = $query->get();
$data[] = [ $data[] = [
'date' => $date, 'date' => $date,
'income' => (float) $bills->where('type', 'income')->sum('amount'), 'income' => (float) $bills->where('type', 'income')->sum('amount'),
@@ -96,9 +109,10 @@ public function getCategory(Request $request)
$type = $request->input('type', 'expense'); $type = $request->input('type', 'expense');
$year = $request->input('year'); $year = $request->input('year');
$month = $request->input('month'); $month = $request->input('month');
$dataType = $request->input('data_type', 'family'); // 默认为家庭数据
// 获取用户所在的家庭ID // 获取用户所在的家庭ID
$familyId = $this->getUserFamilyId($userId); $userFamilyId = $this->getUserFamilyId($userId);
$query = Bill::where('type', $type); $query = Bill::where('type', $type);
@@ -110,16 +124,22 @@ public function getCategory(Request $request)
->whereYear('bill_date', date('Y')); ->whereYear('bill_date', date('Y'));
} }
if ($familyId) { // 根据data_type参数判断数据类型
$query->where('family_id', $familyId); if ($dataType === 'personal') {
// 个人数据:只显示当前用户的账单
$query->where('user_id', $userId);
} elseif ($dataType === 'family' && $userFamilyId) {
// 家庭数据:显示用户所在家庭的账单
$query->where('family_id', $userFamilyId);
} else { } else {
// 默认显示个人数据
$query->where('user_id', $userId); $query->where('user_id', $userId);
} }
$bills = $query->get(); $bills = $query->get();
// 按分类汇总 // 按分类汇总
$data = $bills->groupBy('category')->map(function ($items) { $data = $bills->groupBy('category')->map(function ($items) use ($type) {
return [ return [
'category_id' => $this->getCategoryId($items->first()->category, $type), 'category_id' => $this->getCategoryId($items->first()->category, $type),
'amount' => (float) $items->sum('amount') 'amount' => (float) $items->sum('amount')
@@ -153,18 +173,25 @@ public function getMonthly(Request $request)
{ {
$userId = auth('api')->user()['uid']; $userId = auth('api')->user()['uid'];
$year = $request->input('year', date('Y')); $year = $request->input('year', date('Y'));
$dataType = $request->input('data_type', 'family'); // 默认为家庭数据
// 获取用户所在的家庭ID // 获取用户所在的家庭ID
$familyId = $this->getUserFamilyId($userId); $userFamilyId = $this->getUserFamilyId($userId);
$data = []; $data = [];
for ($month = 1; $month <= 12; $month++) { for ($month = 1; $month <= 12; $month++) {
$query = Bill::whereMonth('bill_date', $month) $query = Bill::whereMonth('bill_date', $month)
->whereYear('bill_date', $year); ->whereYear('bill_date', $year);
if ($familyId) { // 根据data_type参数判断数据类型
$query->where('family_id', $familyId); if ($dataType === 'personal') {
// 个人数据:只显示当前用户的账单
$query->where('user_id', $userId);
} elseif ($dataType === 'family' && $userFamilyId) {
// 家庭数据:显示用户所在家庭的账单
$query->where('family_id', $userFamilyId);
} else { } else {
// 默认显示个人数据
$query->where('user_id', $userId); $query->where('user_id', $userId);
} }
@@ -191,9 +218,10 @@ public function getYearly(Request $request)
{ {
$userId = auth('api')->user()['uid']; $userId = auth('api')->user()['uid'];
$years = $request->input('years', 5); $years = $request->input('years', 5);
$dataType = $request->input('data_type', 'family'); // 默认为家庭数据
// 获取用户所在的家庭ID // 获取用户所在的家庭ID
$familyId = $this->getUserFamilyId($userId); $userFamilyId = $this->getUserFamilyId($userId);
$data = []; $data = [];
for ($i = $years - 1; $i >= 0; $i--) { for ($i = $years - 1; $i >= 0; $i--) {
@@ -201,9 +229,15 @@ public function getYearly(Request $request)
$query = Bill::whereYear('bill_date', $year); $query = Bill::whereYear('bill_date', $year);
if ($familyId) { // 根据data_type参数判断数据类型
$query->where('family_id', $familyId); if ($dataType === 'personal') {
// 个人数据:只显示当前用户的账单
$query->where('user_id', $userId);
} elseif ($dataType === 'family' && $userFamilyId) {
// 家庭数据:显示用户所在家庭的账单
$query->where('family_id', $userFamilyId);
} else { } else {
// 默认显示个人数据
$query->where('user_id', $userId); $query->where('user_id', $userId);
} }
@@ -228,4 +262,55 @@ private function getUserFamilyId($userId)
$familyMember = FamilyMember::where('user_id', $userId)->first(); $familyMember = FamilyMember::where('user_id', $userId)->first();
return $familyMember ? $familyMember->family_id : null; return $familyMember ? $familyMember->family_id : null;
} }
/**
* 获取个人中心统计数据
*/
public function getDashboardStats(Request $request)
{
$userId = auth('api')->user()['uid'];
$dataType = $request->input('data_type', 'family'); // 默认为家庭数据
// 获取用户所在的家庭ID
$userFamilyId = $this->getUserFamilyId($userId);
// 统计账单数
$billQuery = Bill::query();
// 根据data_type参数判断数据类型
if ($dataType === 'personal') {
// 个人数据:只统计当前用户的账单
$billQuery->where('user_id', $userId);
} elseif ($dataType === 'family' && $userFamilyId) {
// 家庭数据:统计用户所在家庭的账单
$billQuery->where('family_id', $userFamilyId);
} else {
// 默认统计个人数据
$billQuery->where('user_id', $userId);
}
$billCount = $billQuery->count();
// 统计记账天数(计算从第一个账单到现在的天数)
$days = 0;
$firstBill = $billQuery->orderBy('bill_date', 'asc')->first();
if ($firstBill) {
$firstDate = new \DateTime($firstBill->bill_date);
$now = new \DateTime();
$interval = $firstDate->diff($now);
$days = $interval->days + 1; // +1 包含当天
}
// 统计家庭成员数
$familyMembers = 0;
if ($userFamilyId) {
$familyMembers = FamilyMember::where('family_id', $userFamilyId)->count();
}
return [
'bill_count' => $billCount,
'days' => $days,
'family_members' => $familyMembers
];
}
} }
+1
View File
@@ -44,5 +44,6 @@
Route::get('category', [StatisticsController::class, 'category']); Route::get('category', [StatisticsController::class, 'category']);
Route::get('monthly', [StatisticsController::class, 'monthly']); Route::get('monthly', [StatisticsController::class, 'monthly']);
Route::get('yearly', [StatisticsController::class, 'yearly']); Route::get('yearly', [StatisticsController::class, 'yearly']);
Route::get('dashboard', [StatisticsController::class, 'dashboard']);
}); });
}); });
+7
View File
@@ -28,5 +28,12 @@ export default {
get: async function(params) { get: async function(params) {
return request.get(this.url, {params: params}) return request.get(this.url, {params: params})
} }
},
update: {
url: '/api/member/update',
name: '更新用户信息',
post: async function(params) {
return request.post(this.url, params)
}
} }
} }
@@ -1,9 +1,9 @@
import request from '@/utils/request' import request from '@/utils/request'
export default { export default {
sendCode: { upload: {
url: '/api/member/sms/send', url: '/api/system/upload',
name: '发送验证码', name: '图片上传',
post: async function(params) { post: async function(params) {
return request.post(this.url, params) return request.post(this.url, params)
} }
+18
View File
@@ -0,0 +1,18 @@
import request from '@/utils/request'
export default {
edit: {
url: '/api/member/member/edit',
name: '用户修改',
post: async function(params) {
return request.put(this.url, params)
}
},
editpasswd: {
url: '/api/member/member/editpasswd',
name: '密码修改',
post: async function(params) {
return request.put(this.url, params)
}
}
}
@@ -40,5 +40,13 @@ export default {
get: async function(params) { get: async function(params) {
return request.get(this.url, {params: params}) return request.get(this.url, {params: params})
} }
},
// 获取个人中心统计数据
dashboard: {
url: '/api/account/statistics/dashboard',
name: '个人中心统计',
get: async function(params) {
return request.get(this.url, {params: params})
}
} }
} }
+15
View File
@@ -30,11 +30,26 @@
{ {
"path": "pages/ucenter/register/index" "path": "pages/ucenter/register/index"
}, },
{
"path": "pages/ucenter/profile/index"
},
{
"path": "pages/ucenter/profile/edit"
},
{
"path": "pages/ucenter/profile/password"
},
{ {
"path": "pages/ucenter/agreement/user" "path": "pages/ucenter/agreement/user"
}, },
{ {
"path": "pages/ucenter/agreement/privacy" "path": "pages/ucenter/agreement/privacy"
},
{
"path": "pages/ucenter/about/index"
},
{
"path": "pages/ucenter/help/index"
} }
], ],
"globalStyle": { "globalStyle": {
+108 -3
View File
@@ -1,10 +1,16 @@
<template> <template>
<un-pages <un-pages
:show-nav-bar="true" :show-nav-bar="true"
nav-bar-title="记一笔" :nav-bar-title="pageTitle"
:show-back="true" :show-back="true"
> >
<view class="page-content"> <view class="page-content">
<!-- 家庭提示 -->
<view v-if="familyInfo" class="family-tip">
<uni-icons type="home" size="16" color="#667eea"></uni-icons>
<text class="tip-text">记录到家庭: {{ familyInfo.name }}</text>
</view>
<!-- 收支类型切换 --> <!-- 收支类型切换 -->
<view class="type-switch"> <view class="type-switch">
<view <view
@@ -97,6 +103,10 @@ export default {
data() { data() {
return { return {
loading: false, loading: false,
billId: null,
isEdit: false,
pageTitle: '记一笔',
familyInfo: null,
form: { form: {
type: 'expense', type: 'expense',
amount: '', amount: '',
@@ -128,7 +138,76 @@ export default {
return this.form.type === 'expense' ? this.expenseCategories : this.incomeCategories return this.form.type === 'expense' ? this.expenseCategories : this.incomeCategories
} }
}, },
onLoad(options) {
// 检查是否为编辑模式
if (options.id) {
this.billId = parseInt(options.id)
this.isEdit = true
this.pageTitle = '编辑账单'
this.loadBillDetail()
}
this.loadFamilyInfo()
},
methods: { methods: {
async loadFamilyInfo() {
try {
const res = await this.$store.dispatch('getFamilyInfo')
if (res && res.code === 1 && res.data) {
this.familyInfo = res.data
}
} catch (error) {
console.error('获取家庭信息失败', error)
}
},
async loadBillDetail() {
if (!this.billId) return
this.loading = true
try {
const res = await this.$api.bill.detail.get({ id: this.billId })
if (res && res.code === 1 && res.data) {
const bill = res.data
this.form.type = bill.type
this.form.amount = bill.amount.toString()
this.form.categoryId = this.getCategoryIdByCategoryName(bill.category, bill.type)
this.form.remark = bill.remark || ''
this.form.date = bill.bill_date
}
} catch (error) {
console.error('加载账单详情失败', error)
uni.showToast({
title: error?.message || '加载失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
getCategoryIdByCategoryName(categoryName, type) {
if (type === 'income') {
const map = {
'工资': 101,
'奖金': 102,
'投资': 103,
'兼职': 104,
'其他': 105
}
return map[categoryName] || 105
} else {
const map = {
'餐饮': 1,
'交通': 2,
'购物': 3,
'娱乐': 4,
'医疗': 5,
'教育': 6,
'居住': 7,
'其他': 8
}
return map[categoryName] || 8
}
},
getTodayDate() { getTodayDate() {
const today = new Date() const today = new Date()
const year = today.getFullYear() const year = today.getFullYear()
@@ -185,13 +264,22 @@ export default {
this.loading = true this.loading = true
try { try {
const res = await this.$api.bill.add.post({ // 根据模式调用不同的接口
const apiUrl = this.isEdit ? 'edit' : 'add'
const params = {
type: this.form.type, type: this.form.type,
amount: parseFloat(this.form.amount), amount: parseFloat(this.form.amount),
category_id: this.form.categoryId, category_id: this.form.categoryId,
remark: this.form.remark || undefined, remark: this.form.remark || undefined,
date: this.form.date date: this.form.date
}) }
// 编辑时需要传递ID
if (this.isEdit) {
params.id = this.billId
}
const res = await this.$api.bill[apiUrl].post(params)
if (res && res.code === 1) { if (res && res.code === 1) {
uni.showToast({ uni.showToast({
@@ -226,6 +314,23 @@ export default {
padding: 30rpx; padding: 30rpx;
} }
.family-tip {
display: flex;
align-items: center;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.08) 100%);
border: 2rpx solid rgba(102, 126, 234, 0.18);
border-radius: 16rpx;
padding: 20rpx 24rpx;
margin-bottom: 30rpx;
.tip-text {
margin-left: 12rpx;
font-size: 24rpx;
color: #667eea;
font-weight: 500;
}
}
.type-switch { .type-switch {
display: flex; display: flex;
background: #f5f5f5; background: #f5f5f5;
+177 -20
View File
@@ -7,6 +7,26 @@
@tab-change="handleTabChange" @tab-change="handleTabChange"
> >
<view class="page-content"> <view class="page-content">
<!-- 数据类型切换 -->
<view v-if="hasFamily" class="data-type-switch">
<view
class="switch-item"
:class="{active: dataType === 'family'}"
@tap="switchDataType('family')"
>
<uni-icons type="home" size="16" :color="dataType === 'family' ? '#fff' : '#999'"></uni-icons>
<text>家庭</text>
</view>
<view
class="switch-item"
:class="{active: dataType === 'personal'}"
@tap="switchDataType('personal')"
>
<uni-icons type="person" size="16" :color="dataType === 'personal' ? '#fff' : '#999'"></uni-icons>
<text>个人</text>
</view>
</view>
<!-- 统计卡片 --> <!-- 统计卡片 -->
<view class="stats-card"> <view class="stats-card">
<view class="stat-item"> <view class="stat-item">
@@ -32,12 +52,18 @@
</view> </view>
<!-- 账单列表 --> <!-- 账单列表 -->
<view class="bill-list"> <scroll-view
<view v-if="loading" class="loading-state"> class="bill-scroll"
scroll-y
@scrolltolower="loadMore"
lower-threshold="100"
>
<view class="bill-list-content">
<view v-if="loading && billList.length === 0" class="loading-state">
<uni-load-more status="loading"></uni-load-more> <uni-load-more status="loading"></uni-load-more>
</view> </view>
<view v-else-if="billList.length === 0" class="empty-state"> <view v-else-if="billList.length === 0 && !loading" class="empty-state">
<uni-icons type="list" size="100" color="#ddd"></uni-icons> <uni-icons type="list" size="100" color="#ddd"></uni-icons>
<text class="empty-text">暂无账单记录</text> <text class="empty-text">暂无账单记录</text>
<button class="add-btn" @tap="addBill"> <button class="add-btn" @tap="addBill">
@@ -46,7 +72,8 @@
</button> </button>
</view> </view>
<view v-else class="bill-group" v-for="(group, date) in groupedBills" :key="date"> <view v-else>
<view class="bill-group" v-for="(group, date) in groupedBills" :key="date">
<view class="group-header"> <view class="group-header">
<text class="group-date">{{ group.formattedDate }}</text> <text class="group-date">{{ group.formattedDate }}</text>
<text class="group-amount">支出: ¥{{ group.expense.toFixed(2) }} 收入: ¥{{ group.income.toFixed(2) }}</text> <text class="group-amount">支出: ¥{{ group.expense.toFixed(2) }} 收入: ¥{{ group.income.toFixed(2) }}</text>
@@ -67,6 +94,16 @@
</view> </view>
</view> </view>
<!-- 加载更多状态 -->
<view v-if="billList.length > 0" class="load-more-state">
<uni-load-more
:status="loadMoreStatus"
:content-text="loadMoreText"
></uni-load-more>
</view>
</view>
</scroll-view>
<!-- 添加按钮 --> <!-- 添加按钮 -->
<view class="fab-button" @tap="addBill"> <view class="fab-button" @tap="addBill">
<uni-icons type="plus" size="24" color="#fff"></uni-icons> <uni-icons type="plus" size="24" color="#fff"></uni-icons>
@@ -86,6 +123,17 @@ export default {
billList: [], billList: [],
monthExpense: 0, monthExpense: 0,
monthIncome: 0, monthIncome: 0,
dataType: 'family', // family 或 personal
currentPage: 1,
pageSize: 20,
total: 0,
hasMore: true,
loadMoreStatus: 'more', // more, loading, noMore
loadMoreText: {
contentdown: '上拉显示更多',
contentrefresh: '正在加载...',
contentnomore: '没有更多数据了'
},
categoryMap: { categoryMap: {
// 支出分类 // 支出分类
1: { name: '餐饮', icon: '🍜', color: '#FF6B6B' }, 1: { name: '餐饮', icon: '🍜', color: '#FF6B6B' },
@@ -106,6 +154,9 @@ export default {
} }
}, },
computed: { computed: {
hasFamily() {
return this.$store.getters.hasFamily
},
groupedBills() { groupedBills() {
const groups = {} const groups = {}
this.billList.forEach(bill => { this.billList.forEach(bill => {
@@ -129,10 +180,11 @@ export default {
} }
}, },
onLoad() { onLoad() {
this.initDataType()
this.loadBillList() this.loadBillList()
}, },
onPullDownRefresh() { onPullDownRefresh() {
this.loadBillList().then(() => { this.loadBillList(true).then(() => {
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
}) })
}, },
@@ -140,6 +192,14 @@ export default {
handleTabChange(path) { handleTabChange(path) {
console.log('Tab changed to:', path) console.log('Tab changed to:', path)
}, },
initDataType() {
// 如果用户加入了家庭,默认显示家庭数据
this.dataType = this.hasFamily ? 'family' : 'personal'
},
switchDataType(type) {
this.dataType = type
this.loadBillList(true) // 切换数据类型时刷新
},
getCurrentMonth() { getCurrentMonth() {
const today = new Date() const today = new Date()
const year = today.getFullYear() const year = today.getFullYear()
@@ -148,21 +208,64 @@ export default {
}, },
onMonthChange(e) { onMonthChange(e) {
this.currentMonth = e.detail.value this.currentMonth = e.detail.value
this.loadBillList() this.loadBillList(true) // 切换月份时刷新
}, },
async loadBillList() { async loadBillList(refresh = false) {
// 如果是刷新,重置页码
if (refresh) {
this.currentPage = 1
this.hasMore = true
this.loadMoreStatus = 'more'
}
// 如果正在加载或没有更多数据,则不执行
if (this.loading || !this.hasMore) {
return
}
this.loading = true this.loading = true
if (!refresh) {
this.loadMoreStatus = 'loading'
}
try { try {
const [year, month] = this.currentMonth.split('-') const [year, month] = this.currentMonth.split('-')
const res = await this.$api.bill.list.get({ const params = {
year: parseInt(year), year: parseInt(year),
month: parseInt(month) month: parseInt(month),
}) page: this.currentPage,
limit: this.pageSize,
data_type: this.dataType
}
const res = await this.$api.bill.list.get(params)
if (res && res.code === 1) { if (res && res.code === 1) {
this.billList = res.data?.list || [] const list = res.data?.list || []
const pagination = res.data?.pagination || {}
if (refresh) {
// 刷新:替换数据
this.billList = list
} else {
// 加载更多:追加数据
this.billList = [...this.billList, ...list]
}
this.monthExpense = res.data?.month_expense || 0 this.monthExpense = res.data?.month_expense || 0
this.monthIncome = res.data?.month_income || 0 this.monthIncome = res.data?.month_income || 0
this.total = pagination.total || 0
// 判断是否还有更多数据
const totalPages = pagination.last_page || 1
this.hasMore = this.currentPage < totalPages
if (this.hasMore) {
this.currentPage++
this.loadMoreStatus = 'more'
} else {
this.loadMoreStatus = 'noMore'
}
} }
} catch (error) { } catch (error) {
console.error('加载账单列表失败', error) console.error('加载账单列表失败', error)
@@ -170,20 +273,23 @@ export default {
title: error?.message || '加载失败,请重试', title: error?.message || '加载失败,请重试',
icon: 'none' icon: 'none'
}) })
this.loadMoreStatus = 'more'
} finally { } finally {
this.loading = false this.loading = false
} }
}, },
loadMore() {
this.loadBillList(false)
},
addBill() { addBill() {
uni.navigateTo({ uni.navigateTo({
url: '/pages/account/bill/add' url: '/pages/account/bill/add'
}) })
}, },
editBill(bill) { editBill(bill) {
// TODO: 实现编辑功能 // 跳转到编辑页面
uni.showToast({ uni.navigateTo({
title: '编辑功能开发中', url: `/pages/account/bill/add?id=${bill.id}`
icon: 'none'
}) })
}, },
getCategoryName(categoryId) { getCategoryName(categoryId) {
@@ -204,10 +310,58 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.page-content { .page-content {
height: 100vh;
display: flex;
flex-direction: column;
padding: 30rpx; padding: 30rpx;
padding-bottom: 150rpx; padding-bottom: 150rpx;
} }
.bill-scroll {
flex: 1;
overflow-y: auto;
}
.bill-list-content {
padding: 0;
}
.data-type-switch {
display: flex;
background: #f5f5f5;
border-radius: 50rpx;
padding: 8rpx;
margin-bottom: 30rpx;
.switch-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 16rpx 0;
border-radius: 50rpx;
transition: all 0.3s;
uni-icons {
margin-right: 8rpx;
}
text {
font-size: 26rpx;
color: #999;
}
&.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
text {
color: #fff;
font-weight: bold;
}
}
}
}
.stats-card { .stats-card {
display: flex; display: flex;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@@ -272,12 +426,11 @@ export default {
} }
} }
.bill-list { .loading-state {
.loading-state {
padding: 60rpx 0; padding: 60rpx 0;
} }
.empty-state { .empty-state {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@@ -308,7 +461,11 @@ export default {
border: none; border: none;
} }
} }
} }
.load-more-state {
padding: 30rpx 0;
text-align: center;
} }
.bill-group { .bill-group {
@@ -7,6 +7,26 @@
@tab-change="handleTabChange" @tab-change="handleTabChange"
> >
<view class="page-content"> <view class="page-content">
<!-- 数据类型切换 -->
<view v-if="hasFamily" class="data-type-switch">
<view
class="switch-item"
:class="{active: dataType === 'family'}"
@tap="switchDataType('family')"
>
<uni-icons type="home" size="16" :color="dataType === 'family' ? '#fff' : '#999'"></uni-icons>
<text>家庭</text>
</view>
<view
class="switch-item"
:class="{active: dataType === 'personal'}"
@tap="switchDataType('personal')"
>
<uni-icons type="person" size="16" :color="dataType === 'personal' ? '#fff' : '#999'"></uni-icons>
<text>个人</text>
</view>
</view>
<!-- 月份选择 --> <!-- 月份选择 -->
<view class="month-selector"> <view class="month-selector">
<picker mode="date" fields="month" :value="currentMonth" @change="onMonthChange"> <picker mode="date" fields="month" :value="currentMonth" @change="onMonthChange">
@@ -95,11 +115,13 @@
<view class="bar-wrapper"> <view class="bar-wrapper">
<view <view
class="bar income-bar" class="bar income-bar"
:style="{height: item.incomeHeight + '%'}" :style="{height: (item.incomeHeight || 0) + '%'}"
:title="'收入: ' + item.income"
></view> ></view>
<view <view
class="bar expense-bar" class="bar expense-bar"
:style="{height: item.expenseHeight + '%'}" :style="{height: (item.expenseHeight || 0) + '%'}"
:title="'支出: ' + item.expense"
></view> ></view>
</view> </view>
<text class="bar-label">{{ item.dateLabel }}</text> <text class="bar-label">{{ item.dateLabel }}</text>
@@ -129,6 +151,7 @@ export default {
return { return {
loading: false, loading: false,
currentMonth: this.getCurrentMonth(), currentMonth: this.getCurrentMonth(),
dataType: 'family', // family 或 personal
overview: { overview: {
income: 0, income: 0,
expense: 0, expense: 0,
@@ -149,7 +172,13 @@ export default {
} }
} }
}, },
computed: {
hasFamily() {
return this.$store.getters.hasFamily
}
},
onLoad() { onLoad() {
this.initDataType()
this.loadStatistics() this.loadStatistics()
}, },
onPullDownRefresh() { onPullDownRefresh() {
@@ -161,6 +190,14 @@ export default {
handleTabChange(path) { handleTabChange(path) {
console.log('Tab changed to:', path) console.log('Tab changed to:', path)
}, },
initDataType() {
// 如果用户加入了家庭,默认显示家庭数据
this.dataType = this.hasFamily ? 'family' : 'personal'
},
switchDataType(type) {
this.dataType = type
this.loadStatistics()
},
getCurrentMonth() { getCurrentMonth() {
const today = new Date() const today = new Date()
const year = today.getFullYear() const year = today.getFullYear()
@@ -177,11 +214,14 @@ export default {
const [year, month] = this.currentMonth.split('-') const [year, month] = this.currentMonth.split('-')
console.log('加载统计数据 - 年:', year, '月:', month) console.log('加载统计数据 - 年:', year, '月:', month)
// 获取统计概览 const baseParams = {
const overviewRes = await this.$api.statistics.overview.get({
year: parseInt(year), year: parseInt(year),
month: parseInt(month) month: parseInt(month),
}) data_type: this.dataType // 使用data_type参数:personal 或 family
}
// 获取统计概览
const overviewRes = await this.$api.statistics.overview.get(baseParams)
console.log('概览接口返回:', overviewRes) console.log('概览接口返回:', overviewRes)
if (overviewRes && overviewRes.code === 1) { if (overviewRes && overviewRes.code === 1) {
this.overview = overviewRes.data || { income: 0, expense: 0, balance: 0 } this.overview = overviewRes.data || { income: 0, expense: 0, balance: 0 }
@@ -191,10 +231,7 @@ export default {
} }
// 获取分类统计 // 获取分类统计
const categoryRes = await this.$api.statistics.category.get({ const categoryRes = await this.$api.statistics.category.get(baseParams)
year: parseInt(year),
month: parseInt(month)
})
console.log('分类接口返回:', categoryRes) console.log('分类接口返回:', categoryRes)
if (categoryRes && categoryRes.code === 1) { if (categoryRes && categoryRes.code === 1) {
this.processCategoryData(categoryRes.data || []) this.processCategoryData(categoryRes.data || [])
@@ -204,10 +241,7 @@ export default {
} }
// 获取收支趋势 // 获取收支趋势
const trendRes = await this.$api.statistics.trend.get({ const trendRes = await this.$api.statistics.trend.get(baseParams)
year: parseInt(year),
month: parseInt(month)
})
console.log('趋势接口返回:', trendRes) console.log('趋势接口返回:', trendRes)
if (trendRes && trendRes.code === 1) { if (trendRes && trendRes.code === 1) {
this.processTrendData(trendRes.data || []) this.processTrendData(trendRes.data || [])
@@ -246,6 +280,11 @@ export default {
}, },
processTrendData(data) { processTrendData(data) {
console.log('处理趋势数据:', data) console.log('处理趋势数据:', data)
if (!data || data.length === 0) {
this.trendList = []
return
}
// 找出最大值用于计算高度百分比 // 找出最大值用于计算高度百分比
let maxIncome = 0 let maxIncome = 0
let maxExpense = 0 let maxExpense = 0
@@ -255,16 +294,36 @@ export default {
maxExpense = Math.max(maxExpense, item.expense) maxExpense = Math.max(maxExpense, item.expense)
}) })
console.log('最大收入:', maxIncome, '最大支出:', maxExpense)
this.trendList = data.map(item => { this.trendList = data.map(item => {
const dateLabel = this.formatDate(item.date, 'MM/dd') const dateLabel = this.formatDate(item.date, 'MM/dd')
// 确保高度至少为1,避免柱状图完全不可见
let incomeHeight = 0
let expenseHeight = 0
if (maxIncome > 0) {
incomeHeight = (item.income / maxIncome) * 100
if (item.income > 0 && incomeHeight < 1) {
incomeHeight = 1
}
}
if (maxExpense > 0) {
expenseHeight = (item.expense / maxExpense) * 100
if (item.expense > 0 && expenseHeight < 1) {
expenseHeight = 1
}
}
return { return {
date: item.date, date: item.date,
dateLabel, dateLabel,
income: item.income, income: item.income,
expense: item.expense, expense: item.expense,
incomeHeight: maxIncome > 0 ? (item.income / maxIncome) * 100 : 0, incomeHeight,
expenseHeight: maxExpense > 0 ? (item.expense / maxExpense) * 100 : 0 expenseHeight
} }
}) })
console.log('处理后的趋势列表:', this.trendList) console.log('处理后的趋势列表:', this.trendList)
@@ -282,6 +341,42 @@ export default {
padding-bottom: 150rpx; padding-bottom: 150rpx;
} }
.data-type-switch {
display: flex;
background: #f5f5f5;
border-radius: 50rpx;
padding: 8rpx;
margin-bottom: 30rpx;
.switch-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 16rpx 0;
border-radius: 50rpx;
transition: all 0.3s;
uni-icons {
margin-right: 8rpx;
}
text {
font-size: 26rpx;
color: #999;
}
&.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
text {
color: #fff;
font-weight: bold;
}
}
}
}
.month-selector { .month-selector {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -497,43 +592,59 @@ export default {
.trend-chart { .trend-chart {
.chart-container { .chart-container {
display: flex; display: flex;
justify-content: space-between; justify-content: space-around;
align-items: flex-end; align-items: flex-end;
height: 300rpx; height: 300rpx;
padding: 20rpx 0; padding: 20rpx 0;
margin-bottom: 20rpx; margin-bottom: 20rpx;
border-bottom: 2rpx solid #f0f0f0;
position: relative;
.chart-bar { .chart-bar {
flex: 1; flex: 0 0 auto;
width: 48rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
height: 100%;
justify-content: flex-end;
.bar-wrapper { .bar-wrapper {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
gap: 4rpx; gap: 6rpx;
margin-bottom: 12rpx; margin-bottom: 12rpx;
width: 100%;
height: 240rpx;
position: relative;
.bar { .bar {
width: 24rpx; flex: 0 0 auto;
min-height: 4rpx; width: 20rpx;
border-radius: 4rpx; min-height: 2rpx;
transition: height 0.3s; border-radius: 4rpx 4rpx 0 0;
transition: height 0.3s ease;
position: absolute;
bottom: 0;
&.income-bar { &.income-bar {
background: #2ECC71; background: #2ECC71;
left: 0;
} }
&.expense-bar { &.expense-bar {
background: #FF6B6B; background: #FF6B6B;
left: 24rpx;
} }
} }
} }
.bar-label { .bar-label {
font-size: 22rpx; font-size: 20rpx;
color: #999; color: #999;
text-align: center;
white-space: nowrap;
line-height: 1.2;
} }
} }
} }
@@ -0,0 +1,391 @@
<template>
<un-pages
:show-nav-bar="true"
nav-bar-title="关于我们"
:show-back="true"
:show-tab-bar="false"
>
<view class="about-container">
<!-- Logo和应用信息 -->
<view class="app-info">
<view class="app-logo">
<text class="logo-icon">📊</text>
</view>
<text class="app-name">家庭记账</text>
<text class="app-version">版本 1.0.0</text>
</view>
<!-- 功能介绍 -->
<view class="feature-section">
<view class="section-title">产品介绍</view>
<view class="feature-card">
<text class="feature-text">
家庭记账是一款简单易用的个人及家庭财务管理应用我们致力于帮助用户轻松记录每一笔收支合理规划财务实现财富增长
</text>
</view>
</view>
<!-- 核心功能 -->
<view class="feature-section">
<view class="section-title">核心功能</view>
<view class="feature-grid">
<view class="feature-item">
<view class="feature-icon-wrapper" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<uni-icons type="compose" size="24" color="#fff"></uni-icons>
</view>
<text class="feature-name">快速记账</text>
<text class="feature-desc">简单快捷记录每一笔收支</text>
</view>
<view class="feature-item">
<view class="feature-icon-wrapper" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
<uni-icons type="person" size="24" color="#fff"></uni-icons>
</view>
<text class="feature-name">家庭共享</text>
<text class="feature-desc">与家人共享财务数据</text>
</view>
<view class="feature-item">
<view class="feature-icon-wrapper" style="background: linear-gradient(135deg, #4ECDC4 0%, #44A08D 100%);">
<uni-icons type="chart" size="24" color="#fff"></uni-icons>
</view>
<text class="feature-name">统计分析</text>
<text class="feature-desc">多维度数据可视化分析</text>
</view>
<view class="feature-item">
<view class="feature-icon-wrapper" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);">
<uni-icons type="download" size="24" color="#fff"></uni-icons>
</view>
<text class="feature-name">数据导出</text>
<text class="feature-desc">支持导出多种格式报表</text>
</view>
</view>
</view>
<!-- 联系我们 -->
<view class="contact-section">
<view class="section-title">联系我们</view>
<view class="contact-list">
<view class="contact-item">
<view class="contact-left">
<uni-icons type="email" size="20" color="#667eea"></uni-icons>
<text class="contact-label">官方邮箱</text>
</view>
<text class="contact-value">support@familyaccount.com</text>
</view>
<view class="contact-item">
<view class="contact-left">
<uni-icons type="phone" size="20" color="#667eea"></uni-icons>
<text class="contact-label">客服电话</text>
</view>
<text class="contact-value">400-XXX-XXXX</text>
</view>
<view class="contact-item">
<view class="contact-left">
<uni-icons type="chatbubble" size="20" color="#667eea"></uni-icons>
<text class="contact-label">微信公众号</text>
</view>
<text class="contact-value">家庭记账助手</text>
</view>
</view>
</view>
<!-- 法律条款 -->
<view class="legal-section">
<view class="legal-item" @tap="navigateTo('/pages/ucenter/agreement/user')">
<text class="legal-text">用户协议</text>
<uni-icons type="right" size="16" color="#ccc"></uni-icons>
</view>
<view class="legal-item" @tap="navigateTo('/pages/ucenter/agreement/privacy')">
<text class="legal-text">隐私政策</text>
<uni-icons type="right" size="16" color="#ccc"></uni-icons>
</view>
</view>
<!-- 底部版权信息 -->
<view class="footer-section">
<text class="copyright">© 2024 家庭记账 版权所有</text>
<text class="icp">ICP备案号京ICP备XXXXXXXX号</text>
</view>
</view>
</un-pages>
</template>
<script>
export default {
data() {
return {
appName: '家庭记账',
version: '1.0.0'
}
},
onLoad() {
// 获取应用版本信息(如果有)
this.getAppInfo()
},
methods: {
// 获取应用信息
getAppInfo() {
// #ifdef APP-PLUS
plus.runtime.getProperty(plus.runtime.appid, (widgetInfo) => {
this.version = widgetInfo.version
})
// #endif
},
// 页面跳转
navigateTo(url) {
uni.navigateTo({
url: url
})
}
}
}
</script>
<style lang="scss" scoped>
.about-container {
min-height: 100vh;
background: #F8F8F8;
padding-bottom: 40rpx;
}
.app-info {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 80rpx 40rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.35);
animation: fadeInDown 0.6s ease-out;
.app-logo {
width: 160rpx;
height: 160rpx;
background: rgba(255, 255, 255, 0.25);
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30rpx;
border: 3rpx solid rgba(255, 255, 255, 0.3);
backdrop-filter: blur(10rpx);
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.15);
.logo-icon {
font-size: 80rpx;
}
}
.app-name {
font-size: 40rpx;
font-weight: bold;
color: #fff;
margin-bottom: 16rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.app-version {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.85);
font-weight: 500;
}
}
.feature-section {
margin: 30rpx;
animation: fadeInUp 0.6s ease-out 0.1s both;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
padding-left: 8rpx;
}
.feature-card {
background: #fff;
border-radius: 24rpx;
padding: 30rpx;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
.feature-text {
display: block;
font-size: 28rpx;
color: #666;
line-height: 1.8;
text-align: justify;
}
}
.feature-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
.feature-item {
background: #fff;
border-radius: 24rpx;
padding: 30rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
transition: transform 0.3s ease;
&:active {
transform: scale(0.98);
}
.feature-icon-wrapper {
width: 80rpx;
height: 80rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.12);
}
.feature-name {
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-bottom: 8rpx;
}
.feature-desc {
font-size: 24rpx;
color: #999;
text-align: center;
line-height: 1.5;
}
}
}
}
.contact-section {
margin: 30rpx;
animation: fadeInUp 0.6s ease-out 0.2s both;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
padding-left: 8rpx;
}
.contact-list {
background: #fff;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
.contact-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 30rpx;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.contact-left {
display: flex;
align-items: center;
.contact-label {
font-size: 28rpx;
color: #333;
margin-left: 16rpx;
font-weight: 500;
}
}
.contact-value {
font-size: 26rpx;
color: #666;
font-weight: 500;
}
}
}
}
.legal-section {
margin: 30rpx;
background: #fff;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
animation: fadeInUp 0.6s ease-out 0.3s both;
.legal-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 30rpx;
border-bottom: 1rpx solid #f5f5f5;
transition: background 0.3s ease;
&:last-child {
border-bottom: none;
}
&:active {
background: #f8f8f8;
}
.legal-text {
font-size: 30rpx;
color: #333;
font-weight: 500;
}
}
}
.footer-section {
margin: 40rpx 30rpx 20rpx;
text-align: center;
animation: fadeInUp 0.6s ease-out 0.4s both;
.copyright {
display: block;
font-size: 24rpx;
color: #999;
margin-bottom: 8rpx;
}
.icp {
display: block;
font-size: 22rpx;
color: #bbb;
}
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-30rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
@@ -1,14 +1,11 @@
<template> <template>
<un-pages
:show-nav-bar="true"
nav-bar-title="隐私政策"
:show-back="true"
:show-tab-bar="false"
>
<view class="agreement-container"> <view class="agreement-container">
<!-- 顶部导航栏 -->
<view class="nav-bar">
<view class="nav-back" @tap="goBack">
<uni-icons type="left" size="24" color="#333"></uni-icons>
</view>
<view class="nav-title">隐私政策</view>
<view class="nav-placeholder"></view>
</view>
<!-- 内容区域 --> <!-- 内容区域 -->
<scroll-view class="content-scroll" scroll-y> <scroll-view class="content-scroll" scroll-y>
<view class="agreement-content"> <view class="agreement-content">
@@ -224,6 +221,7 @@
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
</un-pages>
</template> </template>
<script> <script>
@@ -233,12 +231,6 @@ export default {
// 不需要登录验证 // 不需要登录验证
needLogin: false needLogin: false
} }
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack()
}
} }
} }
</script> </script>
@@ -251,40 +243,9 @@ export default {
flex-direction: column; flex-direction: column;
} }
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
padding: 0 30rpx;
background: #fff;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
position: sticky;
top: 0;
z-index: 100;
}
.nav-back {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.nav-placeholder {
width: 60rpx;
}
.content-scroll { .content-scroll {
flex: 1; flex: 1;
height: calc(100vh - 88rpx); height: 100%;
} }
.agreement-content { .agreement-content {
@@ -1,14 +1,11 @@
<template> <template>
<un-pages
:show-nav-bar="true"
nav-bar-title="用户协议"
:show-back="true"
:show-tab-bar="false"
>
<view class="agreement-container"> <view class="agreement-container">
<!-- 顶部导航栏 -->
<view class="nav-bar">
<view class="nav-back" @tap="goBack">
<uni-icons type="left" size="24" color="#333"></uni-icons>
</view>
<view class="nav-title">用户协议</view>
<view class="nav-placeholder"></view>
</view>
<!-- 内容区域 --> <!-- 内容区域 -->
<scroll-view class="content-scroll" scroll-y> <scroll-view class="content-scroll" scroll-y>
<view class="agreement-content"> <view class="agreement-content">
@@ -141,6 +138,7 @@
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
</un-pages>
</template> </template>
<script> <script>
@@ -150,12 +148,6 @@ export default {
// 不需要登录验证 // 不需要登录验证
needLogin: false needLogin: false
} }
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack()
}
} }
} }
</script> </script>
@@ -168,40 +160,9 @@ export default {
flex-direction: column; flex-direction: column;
} }
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
padding: 0 30rpx;
background: #fff;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
position: sticky;
top: 0;
z-index: 100;
}
.nav-back {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.nav-placeholder {
width: 60rpx;
}
.content-scroll { .content-scroll {
flex: 1; flex: 1;
height: calc(100vh - 88rpx); height: 100%;
} }
.agreement-content { .agreement-content {
@@ -0,0 +1,545 @@
<template>
<un-pages
:show-nav-bar="true"
nav-bar-title="帮助中心"
:show-back="true"
:show-tab-bar="false"
>
<view class="help-container">
<!-- 搜索框 -->
<view class="search-section">
<view class="search-box">
<uni-icons type="search" size="18" color="#999"></uni-icons>
<input
class="search-input"
type="text"
placeholder="搜索问题关键词"
v-model="searchKeyword"
@input="handleSearch"
placeholder-style="color: #999; font-size: 28rpx;"
/>
<view v-if="searchKeyword" class="clear-btn" @tap="clearSearch">
<uni-icons type="clear" size="16" color="#999"></uni-icons>
</view>
</view>
</view>
<!-- 分类标签 -->
<view class="category-section" v-if="!searchKeyword">
<scroll-view scroll-x class="category-scroll" show-scrollbar="false">
<view class="category-list">
<view
class="category-item"
:class="{ active: activeCategory === item.value }"
v-for="item in categoryList"
:key="item.value"
@tap="selectCategory(item.value)"
>
<text class="category-text">{{ item.label }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 问题列表 -->
<view class="faq-section">
<view class="faq-list" v-if="filteredQuestions.length > 0">
<view
class="faq-item"
v-for="(item, index) in filteredQuestions"
:key="index"
animation="fadeInUp"
:style="{ 'animation-delay': index * 0.1 + 's' }"
>
<view class="faq-question" @tap="toggleAnswer(index)">
<text class="question-text">{{ item.question }}</text>
<uni-icons
:type="item.expanded ? 'up' : 'down'"
size="16"
:color="item.expanded ? '#667eea' : '#ccc'"
class="arrow-icon"
></uni-icons>
</view>
<view class="faq-answer" :class="{ expanded: item.expanded }">
<text class="answer-text">{{ item.answer }}</text>
</view>
</view>
</view>
<view class="empty-state" v-else>
<text class="empty-icon">🔍</text>
<text class="empty-text">暂无相关问题</text>
</view>
</view>
<!-- 底部反馈入口 -->
<view class="feedback-section">
<view class="feedback-card" @tap="goToFeedback">
<view class="feedback-left">
<view class="feedback-icon-wrapper">
<uni-icons type="chat" size="24" color="#fff"></uni-icons>
</view>
<view class="feedback-info">
<text class="feedback-title">问题反馈</text>
<text class="feedback-desc">遇到问题告诉我们</text>
</view>
</view>
<uni-icons type="right" size="18" color="#ccc"></uni-icons>
</view>
</view>
<!-- 联系方式 -->
<view class="contact-section">
<view class="contact-item">
<uni-icons type="email" size="20" color="#667eea"></uni-icons>
<text class="contact-label">客服邮箱</text>
<text class="contact-value">support@familyaccount.com</text>
</view>
<view class="contact-item">
<uni-icons type="phone" size="20" color="#667eea"></uni-icons>
<text class="contact-label">客服电话</text>
<text class="contact-value">400-XXX-XXXX</text>
</view>
</view>
</view>
</un-pages>
</template>
<script>
export default {
data() {
return {
searchKeyword: '',
activeCategory: 'all',
categoryList: [
{ label: '全部', value: 'all' },
{ label: '账户相关', value: 'account' },
{ label: '功能使用', value: 'function' },
{ label: '家庭共享', value: 'family' },
{ label: '数据统计', value: 'statistics' },
{ label: '问题反馈', value: 'feedback' }
],
questions: [
{
question: '如何注册账号?',
answer: '点击首页的"注册"按钮,输入用户名、密码和确认密码,即可快速完成注册。注册后可使用手机号或邮箱登录。',
category: 'account',
expanded: false
},
{
question: '忘记密码怎么办?',
answer: '在登录页面点击"忘记密码"按钮,通过注册时的手机号或邮箱验证后,可设置新密码。',
category: 'account',
expanded: false
},
{
question: '如何添加一笔账单?',
answer: '点击首页底部的"+"按钮,选择收入或支出,填写金额、分类、日期和备注,点击保存即可完成记账。',
category: 'function',
expanded: false
},
{
question: '如何修改或删除账单?',
answer: '在账单列表中点击需要操作的账单,进入详情页面后可以进行编辑或删除操作。已删除的账单无法恢复。',
category: 'function',
expanded: false
},
{
question: '如何创建家庭?',
answer: '进入"家庭"页面,点击"创建家庭",设置家庭名称后即可创建。创建者自动成为家主,可以邀请家庭成员。',
category: 'family',
expanded: false
},
{
question: '如何加入家庭?',
answer: '向家主索要家庭邀请码,在"家庭"页面点击"加入家庭",输入邀请码即可加入。一个人只能加入一个家庭。',
category: 'family',
expanded: false
},
{
question: '家主如何管理家庭成员?',
answer: '家主可以在家庭页面查看所有成员,点击成员后的管理按钮可以移除成员。被移除的成员将无法查看该家庭的账单数据。',
category: 'family',
expanded: false
},
{
question: '如何查看收支统计?',
answer: '进入"统计"页面,可以选择查看"个人"或"家庭"的收支数据,支持按日、周、月、年进行统计,并可查看分类统计和趋势图表。',
category: 'statistics',
expanded: false
},
{
question: '如何切换查看个人和家庭数据?',
answer: '在账单列表和统计页面,顶部有"个人"和"家庭"切换按钮,点击即可在个人数据和家庭数据之间切换。',
category: 'function',
expanded: false
},
{
question: '如何导出数据?',
answer: '在统计页面点击"导出"按钮,可以选择导出Excel或CSV格式的报表,支持导出指定时间范围内的账单数据。',
category: 'function',
expanded: false
},
{
question: '如何修改个人资料?',
answer: '进入"我的"页面,点击个人设置,在资料编辑页面可以修改头像、昵称、邮箱等信息。用户名注册后不可修改。',
category: 'account',
expanded: false
},
{
question: '数据会被安全保存吗?',
answer: '是的,您的所有数据都经过加密存储,并定期备份。我们严格遵守隐私政策,不会泄露您的个人财务信息。',
category: 'account',
expanded: false
},
{
question: '如何提交问题反馈?',
answer: '在帮助中心底部点击"问题反馈"卡片,详细描述您遇到的问题,我们会尽快回复并处理您的反馈。',
category: 'feedback',
expanded: false
},
{
question: '如何联系我们?',
answer: '您可以通过客服邮箱 support@familyaccount.com 或客服电话 400-XXX-XXXX 联系我们,我们的工作时间为周一至周五 9:00-18:00。',
category: 'feedback',
expanded: false
}
]
}
},
computed: {
filteredQuestions() {
let result = this.questions
// 按分类筛选
if (this.activeCategory !== 'all') {
result = result.filter(item => item.category === this.activeCategory)
}
// 按关键词搜索
if (this.searchKeyword) {
const keyword = this.searchKeyword.toLowerCase()
result = result.filter(item =>
item.question.toLowerCase().includes(keyword) ||
item.answer.toLowerCase().includes(keyword)
)
}
return result
}
},
methods: {
// 选择分类
selectCategory(value) {
this.activeCategory = value
},
// 搜索处理
handleSearch() {
// 搜索时自动显示所有分类的结果
},
// 清空搜索
clearSearch() {
this.searchKeyword = ''
},
// 展开/收起答案
toggleAnswer(index) {
this.filteredQuestions[index].expanded = !this.filteredQuestions[index].expanded
},
// 跳转到反馈页面
goToFeedback() {
uni.navigateTo({
url: '/pages/ucenter/feedback/index'
})
}
}
}
</script>
<style lang="scss" scoped>
.help-container {
min-height: 100vh;
background: #F8F8F8;
padding-bottom: 40rpx;
}
.search-section {
padding: 30rpx;
background: #fff;
animation: fadeInDown 0.6s ease-out;
.search-box {
display: flex;
align-items: center;
background: #F5F5F5;
border-radius: 48rpx;
padding: 20rpx 30rpx;
.search-input {
flex: 1;
margin-left: 16rpx;
margin-right: 16rpx;
font-size: 28rpx;
color: #333;
}
.clear-btn {
display: flex;
align-items: center;
}
}
}
.category-section {
padding: 20rpx 30rpx 0;
animation: fadeInDown 0.6s ease-out 0.1s both;
.category-scroll {
white-space: nowrap;
.category-list {
display: inline-flex;
padding-bottom: 20rpx;
.category-item {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 16rpx 32rpx;
margin-right: 16rpx;
background: #fff;
border-radius: 48rpx;
font-size: 26rpx;
color: #666;
transition: all 0.3s ease;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
&.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
box-shadow: 0 6rpx 20rpx rgba(102, 126, 234, 0.35);
}
&:last-child {
margin-right: 0;
}
.category-text {
white-space: nowrap;
}
}
}
}
}
.faq-section {
margin: 20rpx 30rpx;
animation: fadeInUp 0.6s ease-out 0.2s both;
.faq-list {
.faq-item {
background: #fff;
border-radius: 24rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
transition: transform 0.3s ease, box-shadow 0.3s ease;
&:active {
transform: scale(0.99);
}
.faq-question {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 30rpx;
transition: background 0.3s ease;
&:active {
background: #f8f8f8;
}
.question-text {
flex: 1;
font-size: 30rpx;
color: #333;
font-weight: 500;
line-height: 1.5;
margin-right: 20rpx;
}
.arrow-icon {
flex-shrink: 0;
transition: transform 0.3s ease;
}
}
.faq-answer {
max-height: 0;
overflow: hidden;
transition: max-height 0.4s ease, padding 0.4s ease;
background: #FAFAFA;
.answer-text {
display: block;
padding: 0 30rpx;
font-size: 28rpx;
color: #666;
line-height: 1.8;
opacity: 0;
transform: translateY(-10rpx);
transition: opacity 0.3s ease, transform 0.3s ease;
}
&.expanded {
max-height: 500rpx;
padding: 20rpx 0 32rpx;
.answer-text {
opacity: 1;
transform: translateY(0);
}
}
}
}
}
.empty-state {
background: #fff;
border-radius: 24rpx;
padding: 120rpx 40rpx;
text-align: center;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
.empty-icon {
display: block;
font-size: 120rpx;
margin-bottom: 30rpx;
opacity: 0.6;
}
.empty-text {
display: block;
font-size: 28rpx;
color: #999;
}
}
}
.feedback-section {
margin: 30rpx;
animation: fadeInUp 0.6s ease-out 0.3s both;
.feedback-card {
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 24rpx;
padding: 30rpx;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.35);
transition: transform 0.3s ease;
&:active {
transform: scale(0.98);
}
.feedback-left {
display: flex;
align-items: center;
.feedback-icon-wrapper {
width: 72rpx;
height: 72rpx;
background: rgba(255, 255, 255, 0.25);
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
border: 2rpx solid rgba(255, 255, 255, 0.3);
backdrop-filter: blur(10rpx);
}
.feedback-info {
display: flex;
flex-direction: column;
.feedback-title {
font-size: 30rpx;
color: #fff;
font-weight: bold;
margin-bottom: 6rpx;
}
.feedback-desc {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.85);
}
}
}
}
}
.contact-section {
margin: 30rpx;
background: #fff;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
animation: fadeInUp 0.6s ease-out 0.4s both;
.contact-item {
display: flex;
align-items: center;
padding: 28rpx 30rpx;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.contact-label {
font-size: 28rpx;
color: #666;
margin-left: 16rpx;
margin-right: 20rpx;
flex-shrink: 0;
}
.contact-value {
flex: 1;
font-size: 26rpx;
color: #333;
font-weight: 500;
}
}
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-30rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
+54 -59
View File
@@ -11,14 +11,12 @@
<view class="user-header"> <view class="user-header">
<view class="header-bg"></view> <view class="header-bg"></view>
<view class="user-avatar"> <view class="user-avatar">
<uni-icons type="person-filled" size="70" color="#fff"></uni-icons> <image v-if="userInfo.avatar" :src="userInfo.avatar" class="avatar-image" mode="aspectFill"></image>
<view class="vip-badge" v-if="isLogin"> <uni-icons v-else type="person-filled" size="70" color="#fff"></uni-icons>
<text>VIP</text>
</view>
</view> </view>
<view class="user-info"> <view class="user-info">
<text class="user-name">{{ userInfo.nickname || userInfo.username || '未登录' }}</text> <text class="user-name">{{ userInfo.username || '未登录' }}</text>
<text class="user-desc" v-if="isLogin">{{ userInfo.email || '暂无邮箱' }}</text> <text class="user-nickname" v-if="isLogin && userInfo.nickname">{{ userInfo.nickname }}</text>
<text class="user-desc" v-else>点击登录体验更多功能</text> <text class="user-desc" v-else>点击登录体验更多功能</text>
</view> </view>
<view class="edit-profile" @tap="handleEditProfile" v-if="isLogin"> <view class="edit-profile" @tap="handleEditProfile" v-if="isLogin">
@@ -62,7 +60,7 @@
</view> </view>
<view class="quick-item" @tap="navigateTo('/pages/account/statistics/index')"> <view class="quick-item" @tap="navigateTo('/pages/account/statistics/index')">
<view class="quick-icon" style="background: linear-gradient(135deg, #4ECDC4 0%, #44A08D 100%);"> <view class="quick-icon" style="background: linear-gradient(135deg, #4ECDC4 0%, #44A08D 100%);">
<uni-icons type="chart" size="24" color="#fff"></uni-icons> <uni-icons type="color-filled" size="24" color="#fff"></uni-icons>
</view> </view>
<text class="quick-text">统计分析</text> <text class="quick-text">统计分析</text>
</view> </view>
@@ -79,7 +77,7 @@
<view class="menu-section"> <view class="menu-section">
<view class="section-title">设置</view> <view class="section-title">设置</view>
<view class="menu-list"> <view class="menu-list">
<view class="menu-item" @tap="navigateTo('/pages/ucenter/profile')"> <view class="menu-item" @tap="navigateTo('/pages/ucenter/profile/index')">
<view class="menu-left"> <view class="menu-left">
<view class="menu-icon-wrapper"> <view class="menu-icon-wrapper">
<uni-icons type="gear" size="20" color="#667eea"></uni-icons> <uni-icons type="gear" size="20" color="#667eea"></uni-icons>
@@ -91,7 +89,7 @@
</view> </view>
<uni-icons type="right" size="16" color="#ccc"></uni-icons> <uni-icons type="right" size="16" color="#ccc"></uni-icons>
</view> </view>
<view class="menu-item" @tap="navigateTo('/pages/ucenter/help')"> <view class="menu-item" @tap="navigateTo('/pages/ucenter/help/index')">
<view class="menu-left"> <view class="menu-left">
<view class="menu-icon-wrapper"> <view class="menu-icon-wrapper">
<uni-icons type="help" size="20" color="#667eea"></uni-icons> <uni-icons type="help" size="20" color="#667eea"></uni-icons>
@@ -103,7 +101,7 @@
</view> </view>
<uni-icons type="right" size="16" color="#ccc"></uni-icons> <uni-icons type="right" size="16" color="#ccc"></uni-icons>
</view> </view>
<view class="menu-item" @tap="navigateTo('/pages/ucenter/about')"> <view class="menu-item" @tap="navigateTo('/pages/ucenter/about/index')">
<view class="menu-left"> <view class="menu-left">
<view class="menu-icon-wrapper"> <view class="menu-icon-wrapper">
<uni-icons type="info" size="20" color="#667eea"></uni-icons> <uni-icons type="info" size="20" color="#667eea"></uni-icons>
@@ -115,7 +113,7 @@
</view> </view>
<uni-icons type="right" size="16" color="#ccc"></uni-icons> <uni-icons type="right" size="16" color="#ccc"></uni-icons>
</view> </view>
<view class="menu-item" @tap="handleClearCache"> <view class="menu-item" @tap="handleClearCache" v-if="false">
<view class="menu-left"> <view class="menu-left">
<view class="menu-icon-wrapper"> <view class="menu-icon-wrapper">
<uni-icons type="trash" size="20" color="#667eea"></uni-icons> <uni-icons type="trash" size="20" color="#667eea"></uni-icons>
@@ -168,46 +166,48 @@ export default {
}, },
methods: { methods: {
// 检查登录状态 // 检查登录状态
checkLoginStatus() { async checkLoginStatus() {
this.isLogin = isLogin() this.isLogin = isLogin()
this.userInfo = uni.getStorageSync('userInfo') || {}
if (this.isLogin) { if (this.isLogin) {
await this.loadUserInfo()
this.loadUserStats() this.loadUserStats()
} else {
this.userInfo = {}
}
},
// 加载用户信息
async loadUserInfo() {
try {
const result = await this.$api.auth.info.get()
if (result && result.code === 1) {
this.userInfo = result.data || {}
// 更新本地存储
uni.setStorageSync('userInfo', this.userInfo)
// 更新 Vuex store
this.$store.commit('setUserInfo', this.userInfo)
} else {
// 如果接口失败,使用本地存储的数据
this.userInfo = uni.getStorageSync('userInfo') || {}
}
} catch (error) {
console.error('加载用户信息失败:', error)
// 如果接口失败,使用本地存储的数据
this.userInfo = uni.getStorageSync('userInfo') || {}
} }
}, },
// 加载用户统计数据 // 加载用户统计数据
async loadUserStats() { async loadUserStats() {
try { try {
// 获取账单总数 const result = await this.$api.statistics.dashboard.get({
const billRes = await this.$api.bill.list.get({ data_type: 'family'
page: 1,
limit: 1
}) })
if (billRes && billRes.code === 1) { if (result && result.code === 1) {
this.stats.billCount = billRes.data?.total || 0 this.stats.billCount = result.data?.bill_count || 0
} this.stats.days = result.data?.days || 0
this.stats.familyMembers = result.data?.family_members || 0
// 获取家庭成员数
const membersRes = await this.$api.family.members.get()
if (membersRes && membersRes.code === 1) {
this.stats.familyMembers = membersRes.data?.length || 0
}
// 获取记账天数(计算从第一个账单到现在的天数)
const firstBillRes = await this.$api.bill.list.get({
page: 1,
limit: 1,
order: 'asc'
})
if (firstBillRes && firstBillRes.code === 1 && firstBillRes.data?.list?.length > 0) {
const firstBill = firstBillRes.data.list[0]
const firstDate = new Date(firstBill.date)
const now = new Date()
const diffTime = Math.abs(now - firstDate)
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
this.stats.days = diffDays
} }
} catch (error) { } catch (error) {
console.error('加载统计数据失败', error) console.error('加载统计数据失败', error)
@@ -243,10 +243,8 @@ export default {
// 编辑个人资料 // 编辑个人资料
handleEditProfile() { handleEditProfile() {
uni.showToast({ uni.navigateTo({
title: '功能开发中', url: '/pages/ucenter/profile/edit'
icon: 'none',
duration: 2000
}) })
}, },
@@ -381,21 +379,11 @@ export default {
border: 3rpx solid rgba(255, 255, 255, 0.3); border: 3rpx solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.15); box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.15);
.vip-badge { .avatar-image {
position: absolute; width: 120rpx;
bottom: -6rpx; height: 120rpx;
right: -6rpx; border-radius: 60rpx;
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); object-fit: cover;
border-radius: 12rpx;
padding: 4rpx 12rpx;
border: 2rpx solid #fff;
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.2);
text {
font-size: 18rpx;
color: #fff;
font-weight: bold;
}
} }
} }
@@ -414,6 +402,13 @@ export default {
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
} }
.user-nickname {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.85);
font-weight: 500;
margin-top: 8rpx;
}
.user-desc { .user-desc {
font-size: 26rpx; font-size: 26rpx;
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.9);
@@ -0,0 +1,412 @@
<template>
<un-pages
:show-nav-bar="true"
nav-bar-title="编辑资料"
:show-back="true"
:show-tab-bar="false"
:nav-bar-right-text="loading ? '保存中...' : '保存'"
@nav-bar-right-click="handleSave"
>
<view class="edit-profile-container">
<!-- 头像上传 -->
<view class="avatar-section">
<view class="avatar-wrapper" @tap="chooseAvatar">
<image v-if="formData.avatar" :src="formData.avatar" class="avatar-image" mode="aspectFill"></image>
<uni-icons v-else type="person-filled" size="60" color="#ccc"></uni-icons>
<view class="avatar-overlay">
<uni-icons type="camera" size="20" color="#fff"></uni-icons>
<text class="avatar-tip">更换头像</text>
</view>
</view>
</view>
<!-- 表单区域 -->
<view class="form-section">
<view class="form-item">
<text class="form-label">用户名</text>
<input class="form-input" type="text" v-model="formData.username" placeholder="请输入用户名" disabled />
<text class="form-tip">用户名不可修改</text>
</view>
<view class="form-item">
<text class="form-label">昵称</text>
<input class="form-input" type="text" v-model="formData.nickname" placeholder="请输入昵称" maxlength="20" />
</view>
<view class="form-item">
<text class="form-label">邮箱</text>
<input class="form-input" type="text" v-model="formData.email" placeholder="请输入邮箱" />
</view>
</view>
<!-- 保存按钮 -->
<view class="submit-section">
<button class="submit-btn" @tap="handleSave" :disabled="loading">保存</button>
</view>
</view>
</un-pages>
</template>
<script>
import memberApi from '@/api/modules/member'
import authApi from '@/api/modules/auth'
import commonApi from '@/api/modules/common'
export default {
data() {
return {
loading: false,
formData: {
username: '',
nickname: '',
email: '',
avatar: ''
},
originalData: {}
}
},
onLoad() {
this.loadUserInfo()
},
methods: {
// 加载用户信息
async loadUserInfo() {
try {
// 从本地存储获取用户信息
const userInfo = uni.getStorageSync('userInfo') || {}
this.formData = {
username: userInfo.username || '',
nickname: userInfo.nickname || '',
email: userInfo.email || '',
avatar: userInfo.avatar || ''
}
// 保存原始数据用于对比
this.originalData = { ...this.formData }
} catch (error) {
console.error('加载用户信息失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
}
},
// 选择头像
chooseAvatar() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePath = res.tempFilePaths[0]
this.uploadAvatar(tempFilePath)
}
})
},
// 上传头像
async uploadAvatar(filePath) {
try {
uni.showLoading({
title: '上传中...'
})
// 调用上传接口
uni.uploadFile({
url: commonApi.upload.url,
filePath: filePath,
name: 'file',
header: {
'Authorization': 'Bearer ' + uni.getStorageSync('token')
},
success: (uploadRes) => {
uni.hideLoading()
try {
const data = JSON.parse(uploadRes.data)
if (data.code === 1) {
this.formData.avatar = data.data.url || data.data
uni.showToast({
title: '上传成功',
icon: 'success'
})
} else {
uni.showToast({
title: data.message || '上传失败',
icon: 'none'
})
}
} catch (e) {
console.error('解析上传结果失败:', e)
uni.showToast({
title: '上传失败',
icon: 'none'
})
}
},
fail: (error) => {
uni.hideLoading()
console.error('上传失败:', error)
uni.showToast({
title: '上传失败',
icon: 'none'
})
}
})
} catch (error) {
uni.hideLoading()
console.error('上传头像失败:', error)
uni.showToast({
title: '上传失败',
icon: 'none'
})
}
},
// 保存修改
async handleSave() {
// 检查是否有修改
const hasChanges = Object.keys(this.formData).some(key => {
if (key === 'username') return false // 用户名不可修改
return this.formData[key] !== this.originalData[key]
})
if (!hasChanges) {
uni.showToast({
title: '未做任何修改',
icon: 'none'
})
return
}
// 表单验证
if (!this.validateForm()) {
return
}
try {
this.loading = true
uni.showLoading({
title: '保存中...'
})
// 调用更新用户信息接口
const result = await memberApi.edit.put({
nickname: this.formData.nickname,
email: this.formData.email,
avatar: this.formData.avatar
})
uni.hideLoading()
if (result && result.code === 1) {
// 重新获取用户信息
const userInfoRes = await authApi.info.get()
if (userInfoRes && userInfoRes.code === 1) {
const updatedUserInfo = userInfoRes.data || {}
uni.setStorageSync('userInfo', updatedUserInfo)
this.$store.commit('setUserInfo', updatedUserInfo)
}
uni.showToast({
title: '保存成功',
icon: 'success',
duration: 1500
})
// 延迟返回上一页
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
uni.showToast({
title: result.message || '保存失败',
icon: 'none'
})
}
} catch (error) {
uni.hideLoading()
console.error('保存失败:', error)
uni.showToast({
title: '保存失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
// 表单验证
validateForm() {
const { nickname, email } = this.formData
if (nickname && nickname.length < 2) {
uni.showToast({
title: '昵称至少2个字符',
icon: 'none'
})
return false
}
if (email) {
const emailReg = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailReg.test(email)) {
uni.showToast({
title: '邮箱格式不正确',
icon: 'none'
})
return false
}
}
return true
},
}
}
</script>
<style lang="scss" scoped>
.edit-profile-container {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 40rpx;
}
.avatar-section {
background: #fff;
padding: 60rpx 0;
margin: 20rpx 30rpx;
border-radius: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
.avatar-wrapper {
position: relative;
width: 160rpx;
height: 160rpx;
border-radius: 50%;
overflow: hidden;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
.avatar-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 60rpx;
background: rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
.avatar-tip {
font-size: 20rpx;
color: #fff;
margin-top: 4rpx;
}
}
&:active {
transform: scale(0.98);
}
&:hover .avatar-overlay {
opacity: 1;
}
}
}
.form-section {
margin: 20rpx 30rpx;
background: #fff;
border-radius: 24rpx;
padding: 0 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
.form-item {
display: flex;
align-items: center;
padding: 32rpx 0;
border-bottom: 1rpx solid #f5f5f5;
position: relative;
&:last-child {
border-bottom: none;
}
.form-label {
width: 140rpx;
font-size: 30rpx;
color: #333;
font-weight: 500;
}
.form-input {
flex: 1;
font-size: 30rpx;
color: #333;
height: 44rpx;
line-height: 44rpx;
padding-right: 60rpx;
&[disabled] {
color: #999;
}
}
.form-tip {
position: absolute;
right: 0;
font-size: 24rpx;
color: #999;
}
}
}
.submit-section {
margin: 40rpx 30rpx;
.submit-btn {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #fff;
font-weight: 500;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.35);
border: none;
padding: 0;
margin: 0;
&:active {
transform: scale(0.98);
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.25);
}
&[disabled] {
opacity: 0.6;
}
}
}
</style>
@@ -0,0 +1,278 @@
<template>
<un-pages
:show-nav-bar="true"
nav-bar-title="个人设置"
:show-back="true"
:show-tab-bar="false"
>
<view class="profile-container">
<!-- 设置选项 -->
<view class="settings-section">
<view class="section-title">账户设置</view>
<view class="settings-list">
<view class="setting-item" @tap="navigateToEdit">
<view class="setting-left">
<view class="setting-icon">
<uni-icons type="person" size="20" color="#667eea"></uni-icons>
</view>
<view class="setting-content">
<text class="setting-title">个人资料</text>
<text class="setting-desc">修改头像昵称邮箱</text>
</view>
</view>
<uni-icons type="right" size="16" color="#ccc"></uni-icons>
</view>
<view class="setting-item" @tap="navigateToPassword">
<view class="setting-left">
<view class="setting-icon">
<uni-icons type="locked" size="20" color="#667eea"></uni-icons>
</view>
<view class="setting-content">
<text class="setting-title">修改密码</text>
<text class="setting-desc">定期更换密码保护账户安全</text>
</view>
</view>
<uni-icons type="right" size="16" color="#ccc"></uni-icons>
</view>
</view>
</view>
<!-- 安全提示 -->
<view class="security-tips">
<view class="tip-header">
<uni-icons type="info-filled" size="18" color="#ffa500"></uni-icons>
<text class="tip-title">安全提示</text>
</view>
<view class="tip-content">
<text class="tip-text">1. 请勿将密码告知他人</text>
<text class="tip-text">2. 定期修改密码以确保账户安全</text>
<text class="tip-text">3. 如遇异常请及时联系客服</text>
</view>
</view>
</view>
</un-pages>
</template>
<script>
export default {
data() {
return {
userInfo: {}
}
},
onLoad() {
this.loadUserInfo()
},
methods: {
// 加载用户信息
loadUserInfo() {
// 从本地存储获取用户信息
this.userInfo = this.$store.state.user.userInfo || {}
},
// 跳转到编辑资料页面
navigateToEdit() {
uni.navigateTo({
url: '/pages/ucenter/profile/edit'
})
},
// 跳转到修改密码页面
navigateToPassword() {
uni.navigateTo({
url: '/pages/ucenter/profile/password'
})
}
}
}
</script>
<style lang="scss" scoped>
.profile-container {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 40rpx;
}
.user-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 20rpx 30rpx;
border-radius: 24rpx;
padding: 40rpx;
display: flex;
align-items: center;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.35);
animation: fadeInDown 0.6s ease-out;
.user-avatar {
width: 120rpx;
height: 120rpx;
background: rgba(255, 255, 255, 0.25);
border-radius: 60rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 30rpx;
border: 3rpx solid rgba(255, 255, 255, 0.3);
backdrop-filter: blur(10rpx);
.avatar-image {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
object-fit: cover;
}
}
.user-info {
flex: 1;
display: flex;
flex-direction: column;
.user-name {
font-size: 36rpx;
font-weight: bold;
color: #fff;
margin-bottom: 12rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.user-nickname {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.85);
font-weight: 500;
}
}
}
.settings-section {
margin: 20rpx 30rpx;
animation: fadeInUp 0.6s ease-out 0.1s both;
.section-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
padding-left: 8rpx;
}
.settings-list {
background: #fff;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 30rpx;
border-bottom: 1rpx solid #f5f5f5;
transition: all 0.3s ease;
&:last-child {
border-bottom: none;
}
&:active {
background: #f8f8f8;
}
.setting-left {
display: flex;
align-items: center;
flex: 1;
.setting-icon {
width: 64rpx;
height: 64rpx;
background: rgba(102, 126, 234, 0.1);
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.setting-content {
display: flex;
flex-direction: column;
.setting-title {
font-size: 30rpx;
color: #333;
font-weight: 500;
margin-bottom: 6rpx;
}
.setting-desc {
font-size: 24rpx;
color: #999;
}
}
}
}
}
}
.security-tips {
margin: 20rpx 30rpx;
background: #fff;
border-radius: 24rpx;
padding: 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
animation: fadeInUp 0.6s ease-out 0.2s both;
.tip-header {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.tip-title {
font-size: 28rpx;
color: #333;
font-weight: bold;
margin-left: 10rpx;
}
}
.tip-content {
display: flex;
flex-direction: column;
.tip-text {
font-size: 26rpx;
color: #666;
line-height: 1.8;
margin-bottom: 8rpx;
padding-left: 8rpx;
}
}
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-30rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
@@ -0,0 +1,328 @@
<template>
<un-pages
:show-nav-bar="true"
nav-bar-title="修改密码"
:show-back="true"
:show-tab-bar="false"
:nav-bar-right-text="loading ? '保存中...' : '保存'"
@nav-bar-right-click="handleSave"
>
<view class="password-container">
<!-- 表单区域 -->
<view class="form-section">
<view class="form-item">
<text class="form-label">当前密码</text>
<input
class="form-input"
:type="showOldPassword ? 'text' : 'password'"
v-model="formData.oldPassword"
placeholder="请输入当前密码"
/>
<uni-icons
:type="showOldPassword ? 'eye-slash' : 'eye'"
size="20"
color="#999"
@tap="toggleOldPassword"
></uni-icons>
</view>
<view class="form-item">
<text class="form-label">新密码</text>
<input
class="form-input"
:type="showNewPassword ? 'text' : 'password'"
v-model="formData.newPassword"
placeholder="请输入新密码(6-20位)"
maxlength="20"
/>
<uni-icons
:type="showNewPassword ? 'eye-slash' : 'eye'"
size="20"
color="#999"
@tap="toggleNewPassword"
></uni-icons>
</view>
<view class="form-item">
<text class="form-label">确认密码</text>
<input
class="form-input"
:type="showConfirmPassword ? 'text' : 'password'"
v-model="formData.confirmPassword"
placeholder="请再次输入新密码"
maxlength="20"
/>
<uni-icons
:type="showConfirmPassword ? 'eye-slash' : 'eye'"
size="20"
color="#999"
@tap="toggleConfirmPassword"
></uni-icons>
</view>
</view>
<!-- 提示信息 -->
<view class="tips-section">
<view class="tip-item">
<uni-icons type="info" size="16" color="#666"></uni-icons>
<text class="tip-text">密码长度为6-20个字符</text>
</view>
<view class="tip-item">
<uni-icons type="info" size="16" color="#666"></uni-icons>
<text class="tip-text">建议使用字母数字和符号的组合</text>
</view>
</view>
<!-- 保存按钮 -->
<view class="submit-section">
<button class="submit-btn" @tap="handleSave" :disabled="loading">保存</button>
</view>
</view>
</un-pages>
</template>
<script>
import memberApi from '@/api/modules/member'
export default {
data() {
return {
loading: false,
formData: {
oldPassword: '',
newPassword: '',
confirmPassword: ''
},
showOldPassword: false,
showNewPassword: false,
showConfirmPassword: false
}
},
methods: {
// 切换当前密码显示/隐藏
toggleOldPassword() {
this.showOldPassword = !this.showOldPassword
},
// 切换新密码显示/隐藏
toggleNewPassword() {
this.showNewPassword = !this.showNewPassword
},
// 切换确认密码显示/隐藏
toggleConfirmPassword() {
this.showConfirmPassword = !this.showConfirmPassword
},
// 保存修改
async handleSave() {
// 表单验证
if (!this.validateForm()) {
return
}
try {
this.loading = true
uni.showLoading({
title: '保存中...'
})
// 调用修改密码接口
const result = await memberApi.editpasswd.put({
old_password: this.formData.oldPassword,
new_password: this.formData.newPassword
})
uni.hideLoading()
if (result && result.code === 1) {
uni.showToast({
title: '密码修改成功,请重新登录',
icon: 'success',
duration: 2000
})
// 延迟退出登录,跳转到登录页
setTimeout(() => {
uni.removeStorageSync('token')
uni.removeStorageSync('userInfo')
this.$store.commit('setUserLogout')
uni.reLaunch({
url: '/pages/ucenter/login/index'
})
}, 2000)
} else {
uni.showToast({
title: result.message || '密码修改失败',
icon: 'none'
})
}
} catch (error) {
uni.hideLoading()
console.error('修改密码失败:', error)
uni.showToast({
title: '修改失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
// 表单验证
validateForm() {
const { oldPassword, newPassword, confirmPassword } = this.formData
if (!oldPassword) {
uni.showToast({
title: '请输入当前密码',
icon: 'none'
})
return false
}
if (!newPassword) {
uni.showToast({
title: '请输入新密码',
icon: 'none'
})
return false
}
if (newPassword.length < 6 || newPassword.length > 20) {
uni.showToast({
title: '密码长度为6-20个字符',
icon: 'none'
})
return false
}
if (!confirmPassword) {
uni.showToast({
title: '请再次输入新密码',
icon: 'none'
})
return false
}
if (newPassword !== confirmPassword) {
uni.showToast({
title: '两次输入的密码不一致',
icon: 'none'
})
return false
}
if (oldPassword === newPassword) {
uni.showToast({
title: '新密码不能与当前密码相同',
icon: 'none'
})
return false
}
return true
},
}
}
</script>
<style lang="scss" scoped>
.password-container {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 40rpx;
}
.form-section {
margin: 20rpx 30rpx;
background: #fff;
border-radius: 24rpx;
padding: 0 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
.form-item {
display: flex;
align-items: center;
padding: 32rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.form-label {
width: 140rpx;
font-size: 30rpx;
color: #333;
font-weight: 500;
}
.form-input {
flex: 1;
font-size: 30rpx;
color: #333;
height: 44rpx;
line-height: 44rpx;
padding-right: 20rpx;
}
}
}
.tips-section {
margin: 20rpx 30rpx;
background: #fff;
border-radius: 24rpx;
padding: 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
.tip-item {
display: flex;
align-items: center;
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
.tip-text {
font-size: 26rpx;
color: #666;
margin-left: 12rpx;
line-height: 1.5;
}
}
}
.submit-section {
margin: 40rpx 30rpx;
.submit-btn {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #fff;
font-weight: 500;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.35);
border: none;
padding: 0;
margin: 0;
&:active {
transform: scale(0.98);
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.25);
}
&[disabled] {
opacity: 0.6;
}
}
}
</style>