完善版本

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

View File

@@ -89,4 +89,19 @@ class StatisticsController extends BaseController
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);
}
}

View File

@@ -21,21 +21,22 @@ class BillService
$month = $request->input('month');
$startDate = $request->input('start_date');
$endDate = $request->input('end_date');
$familyId = $request->input('family_id');
$dataType = $request->input('data_type', 'family'); // 默认为家庭数据
// 获取用户所在的家庭ID
$userFamilyId = $this->getUserFamilyId($userId);
$query = Bill::with(['user:uid,nickname,username', 'family:id,name']);
// 如果用户加入了家庭且没有指定family_id则显示家庭账单
if ($userFamilyId && !$familyId) {
// 根据data_type参数判断数据类型
if ($dataType === 'personal') {
// 个人数据:只显示当前用户的账单
$query->where('user_id', $userId);
} elseif ($dataType === 'family' && $userFamilyId) {
// 家庭数据:显示用户所在家庭的账单
$query->where('family_id', $userFamilyId);
} elseif ($familyId) {
// 如果指定了family_id则显示该家庭的账单
$query->where('family_id', $familyId);
} else {
// 否则显示个人账单
// 默认显示个人数据
$query->where('user_id', $userId);
}
@@ -66,13 +67,13 @@ class BillService
$monthExpense = 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);
$monthIncome->where('family_id', $userFamilyId);
} elseif ($familyId) {
$monthExpense->where('family_id', $familyId);
$monthIncome->where('family_id', $familyId);
} else {
$monthExpense->where('user_id', $userId);
$monthIncome->where('user_id', $userId);
@@ -175,8 +176,14 @@ class BillService
$data['category'] = $categoryMap[$data['category_id']];
$data['bill_date'] = $data['date'];
// 检查家庭权限
if (!empty($data['family_id'])) {
// 如果前端没有指定family_id自动获取用户所在的家庭
if (empty($data['family_id'])) {
$userFamilyId = $this->getUserFamilyId($userId);
if ($userFamilyId) {
$data['family_id'] = $userFamilyId;
}
} else {
// 检查家庭权限
$this->checkFamilyAccess($userId, $data['family_id']);
}
@@ -233,17 +240,29 @@ class BillService
$data = $request->validate([
'type' => 'required|in:income,expense',
'amount' => 'required|numeric|min:0.01',
'category' => 'required|string|max:50',
'category_id' => 'required|integer',
'remark' => 'nullable|string|max:255',
'bill_date' => 'required|date',
'date' => 'required|date',
'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'])) {
$this->checkFamilyAccess($userId, $data['family_id']);
}
// 移除前端字段
unset($data['category_id'], $data['date']);
$bill->update($data);
return $bill;
@@ -278,12 +297,25 @@ class BillService
$bill = Bill::with(['user:uid,nickname,username', 'family:id,name,owner_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('无权查看此账单');
}
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
];
}
/**

View File

@@ -17,9 +17,10 @@ class StatisticsService
$userId = auth('api')->user()['uid'];
$year = $request->input('year');
$month = $request->input('month');
$dataType = $request->input('data_type', 'family'); // 默认为家庭数据
// 获取用户所在的家庭ID
$familyId = $this->getUserFamilyId($userId);
$userFamilyId = $this->getUserFamilyId($userId);
// 查询当月的账单
$query = Bill::query();
@@ -32,9 +33,15 @@ class StatisticsService
->whereYear('bill_date', date('Y'));
}
if ($familyId) {
$query->where('family_id', $familyId);
// 根据data_type参数判断数据类型
if ($dataType === 'personal') {
// 个人数据:只显示当前用户的账单
$query->where('user_id', $userId);
} elseif ($dataType === 'family' && $userFamilyId) {
// 家庭数据:显示用户所在家庭的账单
$query->where('family_id', $userFamilyId);
} else {
// 默认显示个人数据
$query->where('user_id', $userId);
}
@@ -59,9 +66,10 @@ class StatisticsService
$year = $request->input('year');
$month = $request->input('month');
$days = 7; // 最近7天
$dataType = $request->input('data_type', 'family'); // 默认为家庭数据
// 获取用户所在的家庭ID
$familyId = $this->getUserFamilyId($userId);
$userFamilyId = $this->getUserFamilyId($userId);
$data = [];
for ($i = $days - 1; $i >= 0; $i--) {
@@ -69,14 +77,19 @@ class StatisticsService
$query = Bill::whereDate('bill_date', $date);
if ($familyId) {
$query->where('family_id', $familyId);
// 根据data_type参数判断数据类型
if ($dataType === 'personal') {
// 个人数据:只显示当前用户的账单
$query->where('user_id', $userId);
} elseif ($dataType === 'family' && $userFamilyId) {
// 家庭数据:显示用户所在家庭的账单
$query->where('family_id', $userFamilyId);
} else {
// 默认显示个人数据
$query->where('user_id', $userId);
}
$bills = $query->get();
$data[] = [
'date' => $date,
'income' => (float) $bills->where('type', 'income')->sum('amount'),
@@ -96,9 +109,10 @@ class StatisticsService
$type = $request->input('type', 'expense');
$year = $request->input('year');
$month = $request->input('month');
$dataType = $request->input('data_type', 'family'); // 默认为家庭数据
// 获取用户所在的家庭ID
$familyId = $this->getUserFamilyId($userId);
$userFamilyId = $this->getUserFamilyId($userId);
$query = Bill::where('type', $type);
@@ -110,16 +124,22 @@ class StatisticsService
->whereYear('bill_date', date('Y'));
}
if ($familyId) {
$query->where('family_id', $familyId);
// 根据data_type参数判断数据类型
if ($dataType === 'personal') {
// 个人数据:只显示当前用户的账单
$query->where('user_id', $userId);
} elseif ($dataType === 'family' && $userFamilyId) {
// 家庭数据:显示用户所在家庭的账单
$query->where('family_id', $userFamilyId);
} else {
// 默认显示个人数据
$query->where('user_id', $userId);
}
$bills = $query->get();
// 按分类汇总
$data = $bills->groupBy('category')->map(function ($items) {
$data = $bills->groupBy('category')->map(function ($items) use ($type) {
return [
'category_id' => $this->getCategoryId($items->first()->category, $type),
'amount' => (float) $items->sum('amount')
@@ -153,18 +173,25 @@ class StatisticsService
{
$userId = auth('api')->user()['uid'];
$year = $request->input('year', date('Y'));
$dataType = $request->input('data_type', 'family'); // 默认为家庭数据
// 获取用户所在的家庭ID
$familyId = $this->getUserFamilyId($userId);
$userFamilyId = $this->getUserFamilyId($userId);
$data = [];
for ($month = 1; $month <= 12; $month++) {
$query = Bill::whereMonth('bill_date', $month)
->whereYear('bill_date', $year);
if ($familyId) {
$query->where('family_id', $familyId);
// 根据data_type参数判断数据类型
if ($dataType === 'personal') {
// 个人数据:只显示当前用户的账单
$query->where('user_id', $userId);
} elseif ($dataType === 'family' && $userFamilyId) {
// 家庭数据:显示用户所在家庭的账单
$query->where('family_id', $userFamilyId);
} else {
// 默认显示个人数据
$query->where('user_id', $userId);
}
@@ -191,9 +218,10 @@ class StatisticsService
{
$userId = auth('api')->user()['uid'];
$years = $request->input('years', 5);
$dataType = $request->input('data_type', 'family'); // 默认为家庭数据
// 获取用户所在的家庭ID
$familyId = $this->getUserFamilyId($userId);
$userFamilyId = $this->getUserFamilyId($userId);
$data = [];
for ($i = $years - 1; $i >= 0; $i--) {
@@ -201,9 +229,15 @@ class StatisticsService
$query = Bill::whereYear('bill_date', $year);
if ($familyId) {
$query->where('family_id', $familyId);
// 根据data_type参数判断数据类型
if ($dataType === 'personal') {
// 个人数据:只显示当前用户的账单
$query->where('user_id', $userId);
} elseif ($dataType === 'family' && $userFamilyId) {
// 家庭数据:显示用户所在家庭的账单
$query->where('family_id', $userFamilyId);
} else {
// 默认显示个人数据
$query->where('user_id', $userId);
}
@@ -228,4 +262,55 @@ class StatisticsService
$familyMember = FamilyMember::where('user_id', $userId)->first();
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
];
}
}

View File

@@ -44,5 +44,6 @@ Route::middleware(['auth:api'])->group(function () {
Route::get('category', [StatisticsController::class, 'category']);
Route::get('monthly', [StatisticsController::class, 'monthly']);
Route::get('yearly', [StatisticsController::class, 'yearly']);
Route::get('dashboard', [StatisticsController::class, 'dashboard']);
});
});

View File

@@ -28,5 +28,12 @@ export default {
get: async function(params) {
return request.get(this.url, {params: params})
}
},
update: {
url: '/api/member/update',
name: '更新用户信息',
post: async function(params) {
return request.post(this.url, params)
}
}
}

View File

@@ -1,9 +1,9 @@
import request from '@/utils/request'
export default {
sendCode: {
url: '/api/member/sms/send',
name: '发送验证码',
upload: {
url: '/api/system/upload',
name: '图片上传',
post: async function(params) {
return request.post(this.url, params)
}

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)
}
}
}

View File

@@ -40,5 +40,13 @@ export default {
get: async function(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})
}
}
}

View File

@@ -30,11 +30,26 @@
{
"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/privacy"
},
{
"path": "pages/ucenter/about/index"
},
{
"path": "pages/ucenter/help/index"
}
],
"globalStyle": {

View File

@@ -1,10 +1,16 @@
<template>
<un-pages
:show-nav-bar="true"
nav-bar-title="记一笔"
:nav-bar-title="pageTitle"
:show-back="true"
>
<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
@@ -97,6 +103,10 @@ export default {
data() {
return {
loading: false,
billId: null,
isEdit: false,
pageTitle: '记一笔',
familyInfo: null,
form: {
type: 'expense',
amount: '',
@@ -128,7 +138,76 @@ export default {
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: {
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() {
const today = new Date()
const year = today.getFullYear()
@@ -185,13 +264,22 @@ export default {
this.loading = true
try {
const res = await this.$api.bill.add.post({
// 根据模式调用不同的接口
const apiUrl = this.isEdit ? 'edit' : 'add'
const params = {
type: this.form.type,
amount: parseFloat(this.form.amount),
category_id: this.form.categoryId,
remark: this.form.remark || undefined,
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) {
uni.showToast({
@@ -226,6 +314,23 @@ export default {
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 {
display: flex;
background: #f5f5f5;

View File

@@ -7,6 +7,26 @@
@tab-change="handleTabChange"
>
<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="stat-item">
@@ -32,40 +52,57 @@
</view>
<!-- 账单列表 -->
<view class="bill-list">
<view v-if="loading" class="loading-state">
<uni-load-more status="loading"></uni-load-more>
</view>
<view v-else-if="billList.length === 0" class="empty-state">
<uni-icons type="list" size="100" color="#ddd"></uni-icons>
<text class="empty-text">暂无账单记录</text>
<button class="add-btn" @tap="addBill">
<uni-icons type="plus" size="18" color="#fff"></uni-icons>
<text>记一笔</text>
</button>
</view>
<view v-else class="bill-group" v-for="(group, date) in groupedBills" :key="date">
<view class="group-header">
<text class="group-date">{{ group.formattedDate }}</text>
<text class="group-amount">支出: ¥{{ group.expense.toFixed(2) }} 收入: ¥{{ group.income.toFixed(2) }}</text>
<scroll-view
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>
</view>
<view class="bill-item" v-for="bill in group.bills" :key="bill.id" @tap="editBill(bill)">
<view class="bill-icon" :style="{background: getCategoryColor(bill.category_id)}">
<text>{{ getCategoryIcon(bill.category_id) }}</text>
</view>
<view class="bill-info">
<text class="bill-category">{{ getCategoryName(bill.category_id) }}</text>
<text class="bill-remark" v-if="bill.remark">{{ bill.remark }}</text>
</view>
<view class="bill-amount" :class="bill.type">
<text>{{ bill.type === 'expense' ? '-' : '+' }}¥{{ bill.amount.toFixed(2) }}</text>
<view v-else-if="billList.length === 0 && !loading" class="empty-state">
<uni-icons type="list" size="100" color="#ddd"></uni-icons>
<text class="empty-text">暂无账单记录</text>
<button class="add-btn" @tap="addBill">
<uni-icons type="plus" size="18" color="#fff"></uni-icons>
<text>记一笔</text>
</button>
</view>
<view v-else>
<view class="bill-group" v-for="(group, date) in groupedBills" :key="date">
<view class="group-header">
<text class="group-date">{{ group.formattedDate }}</text>
<text class="group-amount">支出: ¥{{ group.expense.toFixed(2) }} 收入: ¥{{ group.income.toFixed(2) }}</text>
</view>
<view class="bill-item" v-for="bill in group.bills" :key="bill.id" @tap="editBill(bill)">
<view class="bill-icon" :style="{background: getCategoryColor(bill.category_id)}">
<text>{{ getCategoryIcon(bill.category_id) }}</text>
</view>
<view class="bill-info">
<text class="bill-category">{{ getCategoryName(bill.category_id) }}</text>
<text class="bill-remark" v-if="bill.remark">{{ bill.remark }}</text>
</view>
<view class="bill-amount" :class="bill.type">
<text>{{ bill.type === 'expense' ? '-' : '+' }}¥{{ bill.amount.toFixed(2) }}</text>
</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>
</view>
</scroll-view>
<!-- 添加按钮 -->
<view class="fab-button" @tap="addBill">
@@ -86,6 +123,17 @@ export default {
billList: [],
monthExpense: 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: {
// 支出分类
1: { name: '餐饮', icon: '🍜', color: '#FF6B6B' },
@@ -106,6 +154,9 @@ export default {
}
},
computed: {
hasFamily() {
return this.$store.getters.hasFamily
},
groupedBills() {
const groups = {}
this.billList.forEach(bill => {
@@ -129,10 +180,11 @@ export default {
}
},
onLoad() {
this.initDataType()
this.loadBillList()
},
onPullDownRefresh() {
this.loadBillList().then(() => {
this.loadBillList(true).then(() => {
uni.stopPullDownRefresh()
})
},
@@ -140,6 +192,14 @@ export default {
handleTabChange(path) {
console.log('Tab changed to:', path)
},
initDataType() {
// 如果用户加入了家庭,默认显示家庭数据
this.dataType = this.hasFamily ? 'family' : 'personal'
},
switchDataType(type) {
this.dataType = type
this.loadBillList(true) // 切换数据类型时刷新
},
getCurrentMonth() {
const today = new Date()
const year = today.getFullYear()
@@ -148,21 +208,64 @@ export default {
},
onMonthChange(e) {
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
if (!refresh) {
this.loadMoreStatus = 'loading'
}
try {
const [year, month] = this.currentMonth.split('-')
const res = await this.$api.bill.list.get({
const params = {
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) {
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.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) {
console.error('加载账单列表失败', error)
@@ -170,20 +273,23 @@ export default {
title: error?.message || '加载失败,请重试',
icon: 'none'
})
this.loadMoreStatus = 'more'
} finally {
this.loading = false
}
},
loadMore() {
this.loadBillList(false)
},
addBill() {
uni.navigateTo({
url: '/pages/account/bill/add'
})
},
editBill(bill) {
// TODO: 实现编辑功能
uni.showToast({
title: '编辑功能开发中',
icon: 'none'
// 跳转到编辑页面
uni.navigateTo({
url: `/pages/account/bill/add?id=${bill.id}`
})
},
getCategoryName(categoryId) {
@@ -204,10 +310,58 @@ export default {
<style lang="scss" scoped>
.page-content {
height: 100vh;
display: flex;
flex-direction: column;
padding: 30rpx;
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 {
display: flex;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@@ -272,45 +426,48 @@ export default {
}
}
.bill-list {
.loading-state {
padding: 60rpx 0;
.loading-state {
padding: 60rpx 0;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 120rpx 0;
.empty-text {
font-size: 28rpx;
color: #999;
margin: 30rpx 0 40rpx;
}
.empty-state {
.add-btn {
display: flex;
flex-direction: column;
align-items: center;
padding: 120rpx 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border: none;
border-radius: 50rpx;
padding: 20rpx 50rpx;
font-size: 28rpx;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
.empty-text {
font-size: 28rpx;
color: #999;
margin: 30rpx 0 40rpx;
uni-icons {
margin-right: 10rpx;
}
.add-btn {
display: flex;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
&::after {
border: none;
border-radius: 50rpx;
padding: 20rpx 50rpx;
font-size: 28rpx;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
uni-icons {
margin-right: 10rpx;
}
&::after {
border: none;
}
}
}
}
.load-more-state {
padding: 30rpx 0;
text-align: center;
}
.bill-group {
margin-bottom: 30rpx;

View File

@@ -7,6 +7,26 @@
@tab-change="handleTabChange"
>
<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">
<picker mode="date" fields="month" :value="currentMonth" @change="onMonthChange">
@@ -95,11 +115,13 @@
<view class="bar-wrapper">
<view
class="bar income-bar"
:style="{height: item.incomeHeight + '%'}"
:style="{height: (item.incomeHeight || 0) + '%'}"
:title="'收入: ' + item.income"
></view>
<view
class="bar expense-bar"
:style="{height: item.expenseHeight + '%'}"
:style="{height: (item.expenseHeight || 0) + '%'}"
:title="'支出: ' + item.expense"
></view>
</view>
<text class="bar-label">{{ item.dateLabel }}</text>
@@ -129,6 +151,7 @@ export default {
return {
loading: false,
currentMonth: this.getCurrentMonth(),
dataType: 'family', // family 或 personal
overview: {
income: 0,
expense: 0,
@@ -149,7 +172,13 @@ export default {
}
}
},
computed: {
hasFamily() {
return this.$store.getters.hasFamily
}
},
onLoad() {
this.initDataType()
this.loadStatistics()
},
onPullDownRefresh() {
@@ -161,6 +190,14 @@ export default {
handleTabChange(path) {
console.log('Tab changed to:', path)
},
initDataType() {
// 如果用户加入了家庭,默认显示家庭数据
this.dataType = this.hasFamily ? 'family' : 'personal'
},
switchDataType(type) {
this.dataType = type
this.loadStatistics()
},
getCurrentMonth() {
const today = new Date()
const year = today.getFullYear()
@@ -177,11 +214,14 @@ export default {
const [year, month] = this.currentMonth.split('-')
console.log('加载统计数据 - 年:', year, '月:', month)
// 获取统计概览
const overviewRes = await this.$api.statistics.overview.get({
const baseParams = {
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)
if (overviewRes && overviewRes.code === 1) {
this.overview = overviewRes.data || { income: 0, expense: 0, balance: 0 }
@@ -191,10 +231,7 @@ export default {
}
// 获取分类统计
const categoryRes = await this.$api.statistics.category.get({
year: parseInt(year),
month: parseInt(month)
})
const categoryRes = await this.$api.statistics.category.get(baseParams)
console.log('分类接口返回:', categoryRes)
if (categoryRes && categoryRes.code === 1) {
this.processCategoryData(categoryRes.data || [])
@@ -204,10 +241,7 @@ export default {
}
// 获取收支趋势
const trendRes = await this.$api.statistics.trend.get({
year: parseInt(year),
month: parseInt(month)
})
const trendRes = await this.$api.statistics.trend.get(baseParams)
console.log('趋势接口返回:', trendRes)
if (trendRes && trendRes.code === 1) {
this.processTrendData(trendRes.data || [])
@@ -246,6 +280,11 @@ export default {
},
processTrendData(data) {
console.log('处理趋势数据:', data)
if (!data || data.length === 0) {
this.trendList = []
return
}
// 找出最大值用于计算高度百分比
let maxIncome = 0
let maxExpense = 0
@@ -255,16 +294,36 @@ export default {
maxExpense = Math.max(maxExpense, item.expense)
})
console.log('最大收入:', maxIncome, '最大支出:', maxExpense)
this.trendList = data.map(item => {
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 {
date: item.date,
dateLabel,
income: item.income,
expense: item.expense,
incomeHeight: maxIncome > 0 ? (item.income / maxIncome) * 100 : 0,
expenseHeight: maxExpense > 0 ? (item.expense / maxExpense) * 100 : 0
incomeHeight,
expenseHeight
}
})
console.log('处理后的趋势列表:', this.trendList)
@@ -282,6 +341,42 @@ export default {
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 {
display: flex;
justify-content: center;
@@ -497,43 +592,59 @@ export default {
.trend-chart {
.chart-container {
display: flex;
justify-content: space-between;
justify-content: space-around;
align-items: flex-end;
height: 300rpx;
padding: 20rpx 0;
margin-bottom: 20rpx;
border-bottom: 2rpx solid #f0f0f0;
position: relative;
.chart-bar {
flex: 1;
flex: 0 0 auto;
width: 48rpx;
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
justify-content: flex-end;
.bar-wrapper {
display: flex;
align-items: flex-end;
gap: 4rpx;
gap: 6rpx;
margin-bottom: 12rpx;
width: 100%;
height: 240rpx;
position: relative;
.bar {
width: 24rpx;
min-height: 4rpx;
border-radius: 4rpx;
transition: height 0.3s;
flex: 0 0 auto;
width: 20rpx;
min-height: 2rpx;
border-radius: 4rpx 4rpx 0 0;
transition: height 0.3s ease;
position: absolute;
bottom: 0;
&.income-bar {
background: #2ECC71;
left: 0;
}
&.expense-bar {
background: #FF6B6B;
left: 24rpx;
}
}
}
.bar-label {
font-size: 22rpx;
font-size: 20rpx;
color: #999;
text-align: center;
white-space: nowrap;
line-height: 1.2;
}
}
}

View File

@@ -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>

View File

@@ -1,229 +1,227 @@
<template>
<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>
<un-pages
:show-nav-bar="true"
nav-bar-title="隐私政策"
:show-back="true"
:show-tab-bar="false"
>
<view class="agreement-container">
<!-- 内容区域 -->
<scroll-view class="content-scroll" scroll-y>
<view class="agreement-content">
<view class="agreement-title">家庭记账隐私政策</view>
<view class="agreement-intro">
<text class="intro-text">
我们非常重视您的隐私保护本隐私政策旨在向您说明我们如何收集使用存储和保护您的个人信息使用我们的服务即表示您同意本隐私政策的条款
</text>
</view>
<view class="agreement-section">
<text class="section-title">1. 信息收集</text>
<text class="section-text">
1.1 我们收集的信息包括
</text>
<text class="section-text">
1账户信息您在注册时提供的用户名密码昵称等
</text>
<text class="section-text">
2使用信息您使用应用的功能频率偏好等使用数据
</text>
<text class="section-text">
3设备信息设备型号操作系统版本唯一设备标识符等
</text>
<text class="section-text">
4日志信息访问时间页面浏览记录错误日志等
</text>
<text class="section-text">
1.2 我们仅在为您提供服务和改进产品时收集必要的信息绝不会收集与提供服务无关的信息
</text>
</view>
<view class="agreement-section">
<text class="section-title">2. 信息使用</text>
<text class="section-text">
2.1 我们可能使用收集的信息用于
</text>
<text class="section-text">
1提供维护和改进我们的服务
</text>
<text class="section-text">
2处理您的请求和交易
</text>
<text class="section-text">
3向您发送与服务相关的通知和更新
</text>
<text class="section-text">
4分析使用情况优化用户体验
</text>
<text class="section-text">
5防止欺诈滥用和安全威胁
</text>
<text class="section-text">
6遵守法律法规要求
</text>
</view>
<view class="agreement-section">
<text class="section-title">3. 信息共享</text>
<text class="section-text">
3.1 除以下情况外我们不会与第三方共享您的个人信息
</text>
<text class="section-text">
1获得您的明确同意
</text>
<text class="section-text">
2履行法律义务或响应法律要求
</text>
<text class="section-text">
3保护我们的权利财产或安全
</text>
<text class="section-text">
4与提供服务的合作伙伴共享但仅限于必要范围
</text>
<text class="section-text">
5业务转让或合并时涉及用户信息转移
</text>
<text class="section-text">
3.2 所有共享的信息都会受到严格的保密协议和隐私保护
</text>
</view>
<view class="agreement-section">
<text class="section-title">4. 信息存储与安全</text>
<text class="section-text">
4.1 我们采取合理的技术措施保护您的信息安全包括但不限于
</text>
<text class="section-text">
1数据加密传输和存储
</text>
<text class="section-text">
2访问权限控制
</text>
<text class="section-text">
3定期安全审计和风险评估
</text>
<text class="section-text">
4员工保密培训和责任制度
</text>
<text class="section-text">
4.2 我们在中华人民共和国境内存储您的个人信息
</text>
<text class="section-text">
4.3 如发生信息泄露我们将及时采取补救措施并通知您
</text>
</view>
<view class="agreement-section">
<text class="section-title">5. Cookie和类似技术</text>
<text class="section-text">
5.1 我们可能使用Cookie和类似技术来收集和存储信息以改善用户体验
</text>
<text class="section-text">
5.2 您可以通过浏览器设置管理Cookie但这可能影响某些功能的使用
</text>
</view>
<view class="agreement-section">
<text class="section-title">6. 您的权利</text>
<text class="section-text">
6.1 您对个人信息享有以下权利
</text>
<text class="section-text">
1访问更正删除您的个人信息
</text>
<text class="section-text">
2撤回对信息处理的同意
</text>
<text class="section-text">
3获取个人信息副本
</text>
<text class="section-text">
4限制信息处理
</text>
<text class="section-text">
5数据可携带权
</text>
<text class="section-text">
6反对信息处理
</text>
<text class="section-text">
7向监管机构投诉
</text>
<text class="section-text">
6.2 您可以通过应用内设置或联系我们来行使这些权利
</text>
</view>
<view class="agreement-section">
<text class="section-title">7. 信息保留</text>
<text class="section-text">
7.1 我们仅在实现收集目的所必需的期间内保留您的个人信息
</text>
<text class="section-text">
7.2 当账户注销或收集目的实现后我们将删除或匿名化处理您的信息法律法规另有规定的除外
</text>
</view>
<view class="agreement-section">
<text class="section-title">8. 未成年人保护</text>
<text class="section-text">
8.1 我们的服务主要面向成年人如果您未满18周岁请在监护人的陪同下使用本服务
</text>
<text class="section-text">
8.2 如发现未经未成年人监护人同意收集未成年人信息的情况请立即联系我们我们将及时删除相关信息
</text>
</view>
<view class="agreement-section">
<text class="section-title">9. 隐私政策的变更</text>
<text class="section-text">
9.1 我们可能不时更新本隐私政策
</text>
<text class="section-text">
9.2 更新后的政策将在应用内公布重大变更时我们会通过适当方式通知您
</text>
<text class="section-text">
9.3 继续使用服务即表示您接受更新后的隐私政策
</text>
</view>
<view class="agreement-section">
<text class="section-title">10. 联系我们</text>
<text class="section-text">
如对本隐私政策有任何疑问意见或请求请通过以下方式联系我们
</text>
<text class="section-text">
邮箱privacy@familyaccount.com
</text>
<text class="section-text">
电话400-XXX-XXXX
</text>
<text class="section-text">
地址中国XX省XX市XX区XX路XX号
</text>
</view>
<view class="agreement-section">
<text class="section-title">11. 适用法律</text>
<text class="section-text">
11.1 本隐私政策的订立执行和解释均适用中华人民共和国法律
</text>
<text class="section-text">
11.2 我们严格遵守中华人民共和国个人信息保护法网络安全法等相关法律法规的规定
</text>
</view>
<view class="agreement-footer">
<text class="footer-text">更新日期2024年1月</text>
<text class="footer-text">生效日期2024年1月1日</text>
</view>
</view>
</scroll-view>
</view>
<!-- 内容区域 -->
<scroll-view class="content-scroll" scroll-y>
<view class="agreement-content">
<view class="agreement-title">家庭记账隐私政策</view>
<view class="agreement-intro">
<text class="intro-text">
我们非常重视您的隐私保护本隐私政策旨在向您说明我们如何收集使用存储和保护您的个人信息使用我们的服务即表示您同意本隐私政策的条款
</text>
</view>
<view class="agreement-section">
<text class="section-title">1. 信息收集</text>
<text class="section-text">
1.1 我们收集的信息包括
</text>
<text class="section-text">
1账户信息您在注册时提供的用户名密码昵称等
</text>
<text class="section-text">
2使用信息您使用应用的功能频率偏好等使用数据
</text>
<text class="section-text">
3设备信息设备型号操作系统版本唯一设备标识符等
</text>
<text class="section-text">
4日志信息访问时间页面浏览记录错误日志等
</text>
<text class="section-text">
1.2 我们仅在为您提供服务和改进产品时收集必要的信息绝不会收集与提供服务无关的信息
</text>
</view>
<view class="agreement-section">
<text class="section-title">2. 信息使用</text>
<text class="section-text">
2.1 我们可能使用收集的信息用于
</text>
<text class="section-text">
1提供维护和改进我们的服务
</text>
<text class="section-text">
2处理您的请求和交易
</text>
<text class="section-text">
3向您发送与服务相关的通知和更新
</text>
<text class="section-text">
4分析使用情况优化用户体验
</text>
<text class="section-text">
5防止欺诈滥用和安全威胁
</text>
<text class="section-text">
6遵守法律法规要求
</text>
</view>
<view class="agreement-section">
<text class="section-title">3. 信息共享</text>
<text class="section-text">
3.1 除以下情况外我们不会与第三方共享您的个人信息
</text>
<text class="section-text">
1获得您的明确同意
</text>
<text class="section-text">
2履行法律义务或响应法律要求
</text>
<text class="section-text">
3保护我们的权利财产或安全
</text>
<text class="section-text">
4与提供服务的合作伙伴共享但仅限于必要范围
</text>
<text class="section-text">
5业务转让或合并时涉及用户信息转移
</text>
<text class="section-text">
3.2 所有共享的信息都会受到严格的保密协议和隐私保护
</text>
</view>
<view class="agreement-section">
<text class="section-title">4. 信息存储与安全</text>
<text class="section-text">
4.1 我们采取合理的技术措施保护您的信息安全包括但不限于
</text>
<text class="section-text">
1数据加密传输和存储
</text>
<text class="section-text">
2访问权限控制
</text>
<text class="section-text">
3定期安全审计和风险评估
</text>
<text class="section-text">
4员工保密培训和责任制度
</text>
<text class="section-text">
4.2 我们在中华人民共和国境内存储您的个人信息
</text>
<text class="section-text">
4.3 如发生信息泄露我们将及时采取补救措施并通知您
</text>
</view>
<view class="agreement-section">
<text class="section-title">5. Cookie和类似技术</text>
<text class="section-text">
5.1 我们可能使用Cookie和类似技术来收集和存储信息以改善用户体验
</text>
<text class="section-text">
5.2 您可以通过浏览器设置管理Cookie但这可能影响某些功能的使用
</text>
</view>
<view class="agreement-section">
<text class="section-title">6. 您的权利</text>
<text class="section-text">
6.1 您对个人信息享有以下权利
</text>
<text class="section-text">
1访问更正删除您的个人信息
</text>
<text class="section-text">
2撤回对信息处理的同意
</text>
<text class="section-text">
3获取个人信息副本
</text>
<text class="section-text">
4限制信息处理
</text>
<text class="section-text">
5数据可携带权
</text>
<text class="section-text">
6反对信息处理
</text>
<text class="section-text">
7向监管机构投诉
</text>
<text class="section-text">
6.2 您可以通过应用内设置或联系我们来行使这些权利
</text>
</view>
<view class="agreement-section">
<text class="section-title">7. 信息保留</text>
<text class="section-text">
7.1 我们仅在实现收集目的所必需的期间内保留您的个人信息
</text>
<text class="section-text">
7.2 当账户注销或收集目的实现后我们将删除或匿名化处理您的信息法律法规另有规定的除外
</text>
</view>
<view class="agreement-section">
<text class="section-title">8. 未成年人保护</text>
<text class="section-text">
8.1 我们的服务主要面向成年人如果您未满18周岁请在监护人的陪同下使用本服务
</text>
<text class="section-text">
8.2 如发现未经未成年人监护人同意收集未成年人信息的情况请立即联系我们我们将及时删除相关信息
</text>
</view>
<view class="agreement-section">
<text class="section-title">9. 隐私政策的变更</text>
<text class="section-text">
9.1 我们可能不时更新本隐私政策
</text>
<text class="section-text">
9.2 更新后的政策将在应用内公布重大变更时我们会通过适当方式通知您
</text>
<text class="section-text">
9.3 继续使用服务即表示您接受更新后的隐私政策
</text>
</view>
<view class="agreement-section">
<text class="section-title">10. 联系我们</text>
<text class="section-text">
如对本隐私政策有任何疑问意见或请求请通过以下方式联系我们
</text>
<text class="section-text">
邮箱privacy@familyaccount.com
</text>
<text class="section-text">
电话400-XXX-XXXX
</text>
<text class="section-text">
地址中国XX省XX市XX区XX路XX号
</text>
</view>
<view class="agreement-section">
<text class="section-title">11. 适用法律</text>
<text class="section-text">
11.1 本隐私政策的订立执行和解释均适用中华人民共和国法律
</text>
<text class="section-text">
11.2 我们严格遵守中华人民共和国个人信息保护法网络安全法等相关法律法规的规定
</text>
</view>
<view class="agreement-footer">
<text class="footer-text">更新日期2024年1月</text>
<text class="footer-text">生效日期2024年1月1日</text>
</view>
</view>
</scroll-view>
</view>
</un-pages>
</template>
<script>
@@ -233,12 +231,6 @@ export default {
// 不需要登录验证
needLogin: false
}
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack()
}
}
}
</script>
@@ -251,40 +243,9 @@ export default {
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 {
flex: 1;
height: calc(100vh - 88rpx);
height: 100%;
}
.agreement-content {

View File

@@ -1,146 +1,144 @@
<template>
<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>
<un-pages
:show-nav-bar="true"
nav-bar-title="用户协议"
:show-back="true"
:show-tab-bar="false"
>
<view class="agreement-container">
<!-- 内容区域 -->
<scroll-view class="content-scroll" scroll-y>
<view class="agreement-content">
<view class="agreement-title">家庭记账用户协议</view>
<view class="agreement-section">
<text class="section-title">1. 服务条款的接受</text>
<text class="section-text">
欢迎使用家庭记账应用通过下载安装或使用本应用即表示您同意遵守本用户协议的所有条款和条件如果您不同意这些条款请不要使用本应用
</text>
</view>
<view class="agreement-section">
<text class="section-title">2. 服务说明</text>
<text class="section-text">
2.1 家庭记账是一款为用户提供记账账单管理等服务的移动应用程序
</text>
<text class="section-text">
2.2 我们保留随时修改或中断服务而不需通知用户的权利
</text>
<text class="section-text">
2.3 用户需自行承担使用本服务所产生的风险包括但不限于因使用本服务而导致的计算机系统损坏或数据丢失
</text>
</view>
<view class="agreement-section">
<text class="section-title">3. 用户账号</text>
<text class="section-text">
3.1 用户注册成功后将获得一个账号和密码用户应妥善保管账号和密码并对使用该账号和密码进行的所有活动承担责任
</text>
<text class="section-text">
3.2 用户不得将账号转让出借或以其他方式提供给他人使用
</text>
<text class="section-text">
3.3 如发现任何非法使用账号的情况应立即通知我们
</text>
</view>
<view class="agreement-section">
<text class="section-title">4. 用户行为规范</text>
<text class="section-text">
4.1 用户在使用本服务时必须遵守相关法律法规不得利用本服务从事任何违法或不当行为
</text>
<text class="section-text">
4.2 禁止用户
</text>
<text class="section-text">
1发布或传输任何违法有害威胁辱骂骚扰诽谤侵犯隐私的信息
</text>
<text class="section-text">
2侵犯他人的知识产权商业秘密或其他合法权利
</text>
<text class="section-text">
3干扰或破坏本服务的正常运行
</text>
<text class="section-text">
4传播病毒木马或其他恶意代码
</text>
</view>
<view class="agreement-section">
<text class="section-title">5. 隐私保护</text>
<text class="section-text">
我们重视用户的隐私保护关于个人信息收集使用和保护的具体内容请参阅我们的隐私政策
</text>
</view>
<view class="agreement-section">
<text class="section-title">6. 知识产权</text>
<text class="section-text">
6.1 本应用的所有内容包括但不限于文字图片音频视频软件程序版面设计等均受知识产权法保护
</text>
<text class="section-text">
6.2 用户在使用本服务过程中产生的内容用户保留其知识产权但授权我们在服务范围内使用该内容
</text>
</view>
<view class="agreement-section">
<text class="section-title">7. 免责声明</text>
<text class="section-text">
7.1 本服务按"现状"提供不提供任何明示或暗示的保证
</text>
<text class="section-text">
7.2 我们不对以下情况承担责任
</text>
<text class="section-text">
1因用户使用本服务而产生的任何直接或间接损失
</text>
<text class="section-text">
2因不可抗力网络故障系统维护等原因导致的服务中断
</text>
<text class="section-text">
3第三方提供的内容或服务
</text>
</view>
<view class="agreement-section">
<text class="section-title">8. 协议的修改</text>
<text class="section-text">
我们有权随时修改本协议的条款修改后的协议一旦公布即代替原协议用户如不同意修改后的协议可以停止使用本服务继续使用本服务即视为用户接受修改后的协议
</text>
</view>
<view class="agreement-section">
<text class="section-title">9. 法律适用与争议解决</text>
<text class="section-text">
9.1 本协议的订立执行和解释均适用中华人民共和国法律
</text>
<text class="section-text">
9.2 如发生争议双方应友好协商解决协商不成的任何一方均可向被告所在地人民法院提起诉讼
</text>
</view>
<view class="agreement-section">
<text class="section-title">10. 联系我们</text>
<text class="section-text">
如对本协议有任何疑问或建议请通过以下方式联系我们
</text>
<text class="section-text">
邮箱support@familyaccount.com
</text>
<text class="section-text">
电话400-XXX-XXXX
</text>
</view>
<view class="agreement-footer">
<text class="footer-text">更新日期2024年1月</text>
</view>
</view>
</scroll-view>
</view>
<!-- 内容区域 -->
<scroll-view class="content-scroll" scroll-y>
<view class="agreement-content">
<view class="agreement-title">家庭记账用户协议</view>
<view class="agreement-section">
<text class="section-title">1. 服务条款的接受</text>
<text class="section-text">
欢迎使用家庭记账应用通过下载安装或使用本应用即表示您同意遵守本用户协议的所有条款和条件如果您不同意这些条款请不要使用本应用
</text>
</view>
<view class="agreement-section">
<text class="section-title">2. 服务说明</text>
<text class="section-text">
2.1 家庭记账是一款为用户提供记账账单管理等服务的移动应用程序
</text>
<text class="section-text">
2.2 我们保留随时修改或中断服务而不需通知用户的权利
</text>
<text class="section-text">
2.3 用户需自行承担使用本服务所产生的风险包括但不限于因使用本服务而导致的计算机系统损坏或数据丢失
</text>
</view>
<view class="agreement-section">
<text class="section-title">3. 用户账号</text>
<text class="section-text">
3.1 用户注册成功后将获得一个账号和密码用户应妥善保管账号和密码并对使用该账号和密码进行的所有活动承担责任
</text>
<text class="section-text">
3.2 用户不得将账号转让出借或以其他方式提供给他人使用
</text>
<text class="section-text">
3.3 如发现任何非法使用账号的情况应立即通知我们
</text>
</view>
<view class="agreement-section">
<text class="section-title">4. 用户行为规范</text>
<text class="section-text">
4.1 用户在使用本服务时必须遵守相关法律法规不得利用本服务从事任何违法或不当行为
</text>
<text class="section-text">
4.2 禁止用户
</text>
<text class="section-text">
1发布或传输任何违法有害威胁辱骂骚扰诽谤侵犯隐私的信息
</text>
<text class="section-text">
2侵犯他人的知识产权商业秘密或其他合法权利
</text>
<text class="section-text">
3干扰或破坏本服务的正常运行
</text>
<text class="section-text">
4传播病毒木马或其他恶意代码
</text>
</view>
<view class="agreement-section">
<text class="section-title">5. 隐私保护</text>
<text class="section-text">
我们重视用户的隐私保护关于个人信息收集使用和保护的具体内容请参阅我们的隐私政策
</text>
</view>
<view class="agreement-section">
<text class="section-title">6. 知识产权</text>
<text class="section-text">
6.1 本应用的所有内容包括但不限于文字图片音频视频软件程序版面设计等均受知识产权法保护
</text>
<text class="section-text">
6.2 用户在使用本服务过程中产生的内容用户保留其知识产权但授权我们在服务范围内使用该内容
</text>
</view>
<view class="agreement-section">
<text class="section-title">7. 免责声明</text>
<text class="section-text">
7.1 本服务按"现状"提供不提供任何明示或暗示的保证
</text>
<text class="section-text">
7.2 我们不对以下情况承担责任
</text>
<text class="section-text">
1因用户使用本服务而产生的任何直接或间接损失
</text>
<text class="section-text">
2因不可抗力网络故障系统维护等原因导致的服务中断
</text>
<text class="section-text">
3第三方提供的内容或服务
</text>
</view>
<view class="agreement-section">
<text class="section-title">8. 协议的修改</text>
<text class="section-text">
我们有权随时修改本协议的条款修改后的协议一旦公布即代替原协议用户如不同意修改后的协议可以停止使用本服务继续使用本服务即视为用户接受修改后的协议
</text>
</view>
<view class="agreement-section">
<text class="section-title">9. 法律适用与争议解决</text>
<text class="section-text">
9.1 本协议的订立执行和解释均适用中华人民共和国法律
</text>
<text class="section-text">
9.2 如发生争议双方应友好协商解决协商不成的任何一方均可向被告所在地人民法院提起诉讼
</text>
</view>
<view class="agreement-section">
<text class="section-title">10. 联系我们</text>
<text class="section-text">
如对本协议有任何疑问或建议请通过以下方式联系我们
</text>
<text class="section-text">
邮箱support@familyaccount.com
</text>
<text class="section-text">
电话400-XXX-XXXX
</text>
</view>
<view class="agreement-footer">
<text class="footer-text">更新日期2024年1月</text>
</view>
</view>
</scroll-view>
</view>
</un-pages>
</template>
<script>
@@ -150,12 +148,6 @@ export default {
// 不需要登录验证
needLogin: false
}
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack()
}
}
}
</script>
@@ -168,40 +160,9 @@ export default {
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 {
flex: 1;
height: calc(100vh - 88rpx);
height: 100%;
}
.agreement-content {

View File

@@ -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>

View File

@@ -11,14 +11,12 @@
<view class="user-header">
<view class="header-bg"></view>
<view class="user-avatar">
<uni-icons type="person-filled" size="70" color="#fff"></uni-icons>
<view class="vip-badge" v-if="isLogin">
<text>VIP</text>
</view>
<image v-if="userInfo.avatar" :src="userInfo.avatar" class="avatar-image" mode="aspectFill"></image>
<uni-icons v-else type="person-filled" size="70" color="#fff"></uni-icons>
</view>
<view class="user-info">
<text class="user-name">{{ userInfo.nickname || userInfo.username || '未登录' }}</text>
<text class="user-desc" v-if="isLogin">{{ userInfo.email || '暂无邮箱' }}</text>
<text class="user-name">{{ userInfo.username || '未登录' }}</text>
<text class="user-nickname" v-if="isLogin && userInfo.nickname">{{ userInfo.nickname }}</text>
<text class="user-desc" v-else>点击登录体验更多功能</text>
</view>
<view class="edit-profile" @tap="handleEditProfile" v-if="isLogin">
@@ -62,7 +60,7 @@
</view>
<view class="quick-item" @tap="navigateTo('/pages/account/statistics/index')">
<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>
<text class="quick-text">统计分析</text>
</view>
@@ -79,7 +77,7 @@
<view class="menu-section">
<view class="section-title">设置</view>
<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-icon-wrapper">
<uni-icons type="gear" size="20" color="#667eea"></uni-icons>
@@ -91,7 +89,7 @@
</view>
<uni-icons type="right" size="16" color="#ccc"></uni-icons>
</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-icon-wrapper">
<uni-icons type="help" size="20" color="#667eea"></uni-icons>
@@ -103,7 +101,7 @@
</view>
<uni-icons type="right" size="16" color="#ccc"></uni-icons>
</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-icon-wrapper">
<uni-icons type="info" size="20" color="#667eea"></uni-icons>
@@ -115,7 +113,7 @@
</view>
<uni-icons type="right" size="16" color="#ccc"></uni-icons>
</view>
<view class="menu-item" @tap="handleClearCache">
<view class="menu-item" @tap="handleClearCache" v-if="false">
<view class="menu-left">
<view class="menu-icon-wrapper">
<uni-icons type="trash" size="20" color="#667eea"></uni-icons>
@@ -168,46 +166,48 @@ export default {
},
methods: {
// 检查登录状态
checkLoginStatus() {
async checkLoginStatus() {
this.isLogin = isLogin()
this.userInfo = uni.getStorageSync('userInfo') || {}
if (this.isLogin) {
await this.loadUserInfo()
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() {
try {
// 获取账单总数
const billRes = await this.$api.bill.list.get({
page: 1,
limit: 1
const result = await this.$api.statistics.dashboard.get({
data_type: 'family'
})
if (billRes && billRes.code === 1) {
this.stats.billCount = billRes.data?.total || 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
if (result && result.code === 1) {
this.stats.billCount = result.data?.bill_count || 0
this.stats.days = result.data?.days || 0
this.stats.familyMembers = result.data?.family_members || 0
}
} catch (error) {
console.error('加载统计数据失败', error)
@@ -243,10 +243,8 @@ export default {
// 编辑个人资料
handleEditProfile() {
uni.showToast({
title: '功能开发中',
icon: 'none',
duration: 2000
uni.navigateTo({
url: '/pages/ucenter/profile/edit'
})
},
@@ -381,21 +379,11 @@ export default {
border: 3rpx solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.15);
.vip-badge {
position: absolute;
bottom: -6rpx;
right: -6rpx;
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
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;
}
.avatar-image {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
object-fit: cover;
}
}
@@ -414,6 +402,13 @@ export default {
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 {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.9);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>