ui更新
This commit is contained in:
0
modules/Account/app/Controllers/.gitkeep
Normal file
0
modules/Account/app/Controllers/.gitkeep
Normal file
73
modules/Account/app/Controllers/AccountController.php
Normal file
73
modules/Account/app/Controllers/AccountController.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | SentCMS [ WE CAN DO IT JUST THINK IT ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2024 http://www.tensent.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: molong <molong@tensent.cn> <http://www.tensent.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace Modules\Account\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class AccountController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return view('account::index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
return view('account::create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the specified resource.
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
return view('account::show');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit($id)
|
||||
{
|
||||
return view('account::edit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, $id): RedirectResponse
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
0
modules/Account/app/Models/.gitkeep
Normal file
0
modules/Account/app/Models/.gitkeep
Normal file
0
modules/Account/app/Providers/.gitkeep
Normal file
0
modules/Account/app/Providers/.gitkeep
Normal file
120
modules/Account/app/Providers/AccountServiceProvider.php
Normal file
120
modules/Account/app/Providers/AccountServiceProvider.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Account\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AccountServiceProvider extends ServiceProvider
|
||||
{
|
||||
protected string $moduleName = 'Account';
|
||||
|
||||
protected string $moduleNameLower = 'account';
|
||||
|
||||
/**
|
||||
* Boot the application events.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->registerCommands();
|
||||
$this->registerCommandSchedules();
|
||||
$this->registerTranslations();
|
||||
$this->registerConfig();
|
||||
$this->registerViews();
|
||||
$this->loadMigrationsFrom(module_path($this->moduleName, 'database/migrations'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the service provider.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->register(EventServiceProvider::class);
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register commands in the format of Command::class
|
||||
*/
|
||||
protected function registerCommands(): void
|
||||
{
|
||||
// $this->commands([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register command Schedules.
|
||||
*/
|
||||
protected function registerCommandSchedules(): void
|
||||
{
|
||||
// $this->app->booted(function () {
|
||||
// $schedule = $this->app->make(Schedule::class);
|
||||
// $schedule->command('inspire')->hourly();
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* Register translations.
|
||||
*/
|
||||
public function registerTranslations(): void
|
||||
{
|
||||
$langPath = resource_path('lang/modules/'.$this->moduleNameLower);
|
||||
|
||||
if (is_dir($langPath)) {
|
||||
$this->loadTranslationsFrom($langPath, $this->moduleNameLower);
|
||||
$this->loadJsonTranslationsFrom($langPath);
|
||||
} else {
|
||||
$this->loadTranslationsFrom(module_path($this->moduleName, 'lang'), $this->moduleNameLower);
|
||||
$this->loadJsonTranslationsFrom(module_path($this->moduleName, 'lang'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register config.
|
||||
*/
|
||||
protected function registerConfig(): void
|
||||
{
|
||||
$this->publishes([module_path($this->moduleName, 'config/config.php') => config_path($this->moduleNameLower.'.php')], 'config');
|
||||
$this->mergeConfigFrom(module_path($this->moduleName, 'config/config.php'), $this->moduleNameLower);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register views.
|
||||
*/
|
||||
public function registerViews(): void
|
||||
{
|
||||
$viewPath = resource_path('views/modules/'.$this->moduleNameLower);
|
||||
$sourcePath = module_path($this->moduleName, 'resources/views');
|
||||
|
||||
$this->publishes([$sourcePath => $viewPath], ['views', $this->moduleNameLower.'-module-views']);
|
||||
|
||||
$this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->moduleNameLower);
|
||||
|
||||
$componentNamespace = str_replace('/', '\\', config('modules.namespace').'\\'.$this->moduleName.'\\'.ltrim(config('modules.paths.generator.component-class.path'), config('modules.paths.app_folder', '')));
|
||||
Blade::componentNamespace($componentNamespace, $this->moduleNameLower);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function provides(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
private function getPublishableViewPaths(): array
|
||||
{
|
||||
$paths = [];
|
||||
foreach (config('view.paths') as $path) {
|
||||
if (is_dir($path.'/modules/'.$this->moduleNameLower)) {
|
||||
$paths[] = $path.'/modules/'.$this->moduleNameLower;
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
}
|
||||
}
|
||||
38
modules/Account/app/Providers/EventServiceProvider.php
Normal file
38
modules/Account/app/Providers/EventServiceProvider.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | SentCMS [ WE CAN DO IT JUST THINK IT ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2024 http://www.tensent.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: molong <molong@tensent.cn> <http://www.tensent.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace Modules\Account\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The event handler mappings for the application.
|
||||
*
|
||||
* @var array<string, array<int, string>>
|
||||
*/
|
||||
protected $listen = [];
|
||||
|
||||
/**
|
||||
* Indicates if events should be discovered.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected static $shouldDiscoverEvents = true;
|
||||
|
||||
/**
|
||||
* Configure the proper event listeners for email verification.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function configureEmailVerification(): void
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
67
modules/Account/app/Providers/RouteServiceProvider.php
Normal file
67
modules/Account/app/Providers/RouteServiceProvider.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | SentCMS [ WE CAN DO IT JUST THINK IT ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2024 http://www.tensent.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: molong <molong@tensent.cn> <http://www.tensent.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace Modules\Account\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Called before routes are registered.
|
||||
*
|
||||
* Register any model bindings or pattern based filters.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the routes for the application.
|
||||
*/
|
||||
public function map(): void
|
||||
{
|
||||
$this->mapApiRoutes();
|
||||
|
||||
$this->mapWebRoutes();
|
||||
|
||||
$this->mapAdminRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the "web" routes for the application.
|
||||
*
|
||||
* These routes all receive session state, CSRF protection, etc.
|
||||
*/
|
||||
protected function mapWebRoutes(): void
|
||||
{
|
||||
Route::middleware('web')->group(module_path('Account', '/routes/web.php'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the "api" routes for the application.
|
||||
*
|
||||
* These routes are typically stateless.
|
||||
*/
|
||||
protected function mapApiRoutes(): void
|
||||
{
|
||||
Route::middleware('api')->prefix('api')->name('api.')->group(module_path('Account', '/routes/api.php'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the "api" routes for the application.
|
||||
*
|
||||
* These routes are typically stateless.
|
||||
*/
|
||||
protected function mapAdminRoutes(): void
|
||||
{
|
||||
Route::middleware('api')->prefix('admin')->name('admin.')->group(module_path('Account', '/routes/admin.php'));
|
||||
}
|
||||
}
|
||||
0
modules/Account/app/Services/.gitkeep
Normal file
0
modules/Account/app/Services/.gitkeep
Normal file
30
modules/Account/composer.json
Normal file
30
modules/Account/composer.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "tensent/account",
|
||||
"description": "",
|
||||
"authors": [
|
||||
{
|
||||
"name": "molong",
|
||||
"email": "molong@tensent.cn"
|
||||
}
|
||||
],
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [],
|
||||
"aliases": {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Modules\\Account\\": "app/",
|
||||
"Modules\\Account\\Database\\Factories\\": "database/factories/",
|
||||
"Modules\\Account\\Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Modules\\Account\\Tests\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
0
modules/Account/config/.gitkeep
Normal file
0
modules/Account/config/.gitkeep
Normal file
5
modules/Account/config/config.php
Normal file
5
modules/Account/config/config.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => 'Account',
|
||||
];
|
||||
0
modules/Account/database/factories/.gitkeep
Normal file
0
modules/Account/database/factories/.gitkeep
Normal file
0
modules/Account/database/migrations/.gitkeep
Normal file
0
modules/Account/database/migrations/.gitkeep
Normal file
0
modules/Account/database/seeders/.gitkeep
Normal file
0
modules/Account/database/seeders/.gitkeep
Normal file
22
modules/Account/database/seeders/AccountDatabaseSeeder.php
Normal file
22
modules/Account/database/seeders/AccountDatabaseSeeder.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | SentCMS [ WE CAN DO IT JUST THINK IT ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2024 http://www.tensent.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: molong <molong@tensent.cn> <http://www.tensent.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace Modules\Account\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class AccountDatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// $this->call([]);
|
||||
}
|
||||
}
|
||||
11
modules/Account/module.json
Normal file
11
modules/Account/module.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "Account",
|
||||
"alias": "account",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"priority": 0,
|
||||
"providers": [
|
||||
"Modules\\Account\\Providers\\AccountServiceProvider"
|
||||
],
|
||||
"files": []
|
||||
}
|
||||
0
modules/Account/routes/.gitkeep
Normal file
0
modules/Account/routes/.gitkeep
Normal file
14
modules/Account/routes/admin.php
Normal file
14
modules/Account/routes/admin.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | SentCMS [ WE CAN DO IT JUST THINK IT ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2024 http://www.tensent.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: molong <molong@tensent.cn> <http://www.tensent.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Account\Controllers\AccountController;
|
||||
|
||||
Route::middleware(['auth.check:admin'])->group(function () {
|
||||
Route::apiResource('account', AccountController::class)->names('account');
|
||||
});
|
||||
14
modules/Account/routes/api.php
Normal file
14
modules/Account/routes/api.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | SentCMS [ WE CAN DO IT JUST THINK IT ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2024 http://www.tensent.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: molong <molong@tensent.cn> <http://www.tensent.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Account\Controllers\AccountController;
|
||||
|
||||
Route::middleware(['auth.check:api'])->group(function () {
|
||||
Route::apiResource('account', AccountController::class)->names('account');
|
||||
});
|
||||
14
modules/Account/routes/web.php
Normal file
14
modules/Account/routes/web.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | SentCMS [ WE CAN DO IT JUST THINK IT ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2024 http://www.tensent.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: molong <molong@tensent.cn> <http://www.tensent.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Account\Controllers\AccountController;
|
||||
|
||||
Route::group([], function () {
|
||||
Route::resource('account', AccountController::class)->names('account');
|
||||
});
|
||||
205
resources/mobile/README.md
Normal file
205
resources/mobile/README.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# 家庭记账APP - 前端开发文档
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个基于 uni-app 框架开发的跨平台家庭记账应用前端,支持个人记账和家庭共享记账功能。
|
||||
|
||||
## 已实现功能
|
||||
|
||||
### 1. 账单管理
|
||||
- ✅ 添加账单(收入/支出)
|
||||
- ✅ 账单列表(按日期分组显示)
|
||||
- ✅ 月份切换查看
|
||||
- ✅ 本月收支统计
|
||||
- ✅ 多种分类(餐饮、交通、购物、娱乐等)
|
||||
- ✅ 支持备注和日期选择
|
||||
|
||||
### 2. 家庭管理
|
||||
- ✅ 创建家庭
|
||||
- ✅ 加入家庭(通过邀请码)
|
||||
- ✅ 查看家庭信息
|
||||
- ✅ 查看家庭成员列表
|
||||
- ✅ 家主功能:
|
||||
- 生成/重新生成邀请码
|
||||
- 复制邀请码
|
||||
- 移除家庭成员
|
||||
- ✅ 退出家庭
|
||||
- ⏳ 转让家主(待实现)
|
||||
|
||||
### 3. 统计分析
|
||||
- ✅ 收支概览(收入、支出、结余)
|
||||
- ✅ 月份切换
|
||||
- ✅ 支出分类统计(带进度条和百分比)
|
||||
- ✅ 收支趋势图(最近7天)
|
||||
- ✅ 可视化图表展示
|
||||
|
||||
### 4. 用户中心
|
||||
- ✅ 登录/注册
|
||||
- ✅ 个人信息管理
|
||||
- ✅ 用户协议和隐私政策
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **框架**: uni-app (Vue 3)
|
||||
- **状态管理**: Vuex
|
||||
- **UI组件**: uni-ui
|
||||
- **HTTP请求**: luch-request
|
||||
- **样式**: SCSS
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
resources/mobile/
|
||||
├── api/ # API接口模块
|
||||
│ ├── index.js # API自动导入
|
||||
│ └── modules/
|
||||
│ ├── auth.js # 认证相关API
|
||||
│ ├── bill.js # 账单相关API
|
||||
│ ├── family.js # 家庭管理API
|
||||
│ ├── sms.js # 短信相关API
|
||||
│ └── statistics.js # 统计分析API
|
||||
├── components/ # 公共组件
|
||||
│ ├── pages/ # 页面组件
|
||||
│ └── tab-bar/ # 底部导航栏
|
||||
├── config/ # 配置文件
|
||||
├── mixins/ # 混入
|
||||
│ └── auth.js # 登录验证混入
|
||||
├── pages/ # 页面
|
||||
│ ├── index/ # 首页
|
||||
│ ├── account/ # 账单管理
|
||||
│ │ ├── bill/ # 账单页面
|
||||
│ │ │ ├── add.vue # 添加账单
|
||||
│ │ │ └── index.vue # 账单列表
|
||||
│ │ └── statistics/ # 统计分析
|
||||
│ │ └── index.vue # 统计页面
|
||||
│ ├── family/ # 家庭管理
|
||||
│ │ ├── index.vue # 家庭主页
|
||||
│ │ ├── create.vue # 创建家庭
|
||||
│ │ └── join.vue # 加入家庭
|
||||
│ └── ucenter/ # 用户中心
|
||||
│ ├── index.vue # 个人中心
|
||||
│ ├── login/ # 登录
|
||||
│ └── register/ # 注册
|
||||
├── static/ # 静态资源
|
||||
├── store/ # Vuex状态管理
|
||||
│ ├── index.js # Store自动导入
|
||||
│ └── modules/
|
||||
│ ├── user.js # 用户状态
|
||||
│ └── family.js # 家庭状态
|
||||
└── utils/ # 工具函数
|
||||
├── auth.js # 认证工具
|
||||
├── request.js # HTTP请求封装
|
||||
└── tool.js # 通用工具
|
||||
```
|
||||
|
||||
## API接口说明
|
||||
|
||||
### 账单API (`/api/bill/*`)
|
||||
- `GET /api/bill/list` - 获取账单列表
|
||||
- `POST /api/bill/add` - 添加账单
|
||||
- `POST /api/bill/edit` - 编辑账单
|
||||
- `POST /api/bill/delete` - 删除账单
|
||||
- `GET /api/bill/detail` - 获取账单详情
|
||||
|
||||
### 家庭API (`/api/family/*`)
|
||||
- `GET /api/family/info` - 获取家庭信息
|
||||
- `POST /api/family/create` - 创建家庭
|
||||
- `POST /api/family/join` - 加入家庭
|
||||
- `POST /api/family/leave` - 退出家庭
|
||||
- `GET /api/family/invite-code` - 获取邀请码
|
||||
- `POST /api/family/regenerate-invite-code` - 重新生成邀请码
|
||||
- `POST /api/family/remove-member` - 移除家庭成员
|
||||
- `GET /api/family/members` - 获取家庭成员列表
|
||||
- `POST /api/family/transfer-owner` - 转让家主
|
||||
|
||||
### 统计API (`/api/statistics/*`)
|
||||
- `GET /api/statistics/overview` - 获取统计概览
|
||||
- `GET /api/statistics/trend` - 获取收支趋势
|
||||
- `GET /api/statistics/category` - 获取分类统计
|
||||
- `GET /api/statistics/monthly` - 获取月度报表
|
||||
- `GET /api/statistics/yearly` - 获取年度报表
|
||||
|
||||
## 功能特点
|
||||
|
||||
### 1. 家庭管理
|
||||
- 一个人只能创建或加入一个家庭
|
||||
- 家主可以管理家庭成员(添加、移除)
|
||||
- 家主可以生成邀请码邀请成员
|
||||
- 支持转让家主功能
|
||||
|
||||
### 2. 账单分类
|
||||
- 支出分类:餐饮、交通、购物、娱乐、医疗、教育、居住、其他
|
||||
- 收入分类:工资、奖金、投资、兼职、其他
|
||||
- 每个分类都有独立的图标和颜色
|
||||
|
||||
### 3. 数据可视化
|
||||
- 支出分类统计使用进度条展示占比
|
||||
- 收支趋势使用柱状图展示最近7天数据
|
||||
- 支持月份切换查看不同时间段的数据
|
||||
|
||||
### 4. 用户体验
|
||||
- 渐变色主题设计
|
||||
- 流畅的动画效果
|
||||
- 下拉刷新支持
|
||||
- 加载状态提示
|
||||
- 空数据友好提示
|
||||
|
||||
## 开发说明
|
||||
|
||||
### 环境要求
|
||||
- Node.js >= 14
|
||||
- HBuilderX 或 VS Code
|
||||
- uni-app CLI
|
||||
|
||||
### 安装依赖
|
||||
```bash
|
||||
cd resources/mobile
|
||||
yarn install
|
||||
```
|
||||
|
||||
### 运行项目
|
||||
- 使用 HBuilderX 运行到不同平台
|
||||
- 或使用 CLI 命令:`npm run dev:mp-weixin` (微信小程序)
|
||||
|
||||
### 构建项目
|
||||
```bash
|
||||
npm run build:mp-weixin
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **API接口**: 需要后端提供对应的接口支持,返回格式统一为:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Token认证**: 所有需要登录的接口都需要在请求头中携带Token:
|
||||
```
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
3. **家庭限制**: 用户只能加入一个家庭,后端需要验证此限制
|
||||
|
||||
4. **邀请码**: 邀请码建议使用10位随机字符串,确保唯一性
|
||||
|
||||
5. **日期格式**: 统一使用 `YYYY-MM-DD` 格式
|
||||
|
||||
## 待优化功能
|
||||
|
||||
1. 账单编辑功能
|
||||
2. 转让家主功能
|
||||
3. 账单搜索和筛选
|
||||
4. 导出数据功能
|
||||
5. 预算管理
|
||||
6. 账单提醒功能
|
||||
7. 更多的统计图表(饼图、折线图等)
|
||||
|
||||
## 作者
|
||||
molong <ycgpp@126.com>
|
||||
|
||||
## 许可证
|
||||
MIT
|
||||
44
resources/mobile/api/modules/bill.js
Normal file
44
resources/mobile/api/modules/bill.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export default {
|
||||
// 账单列表
|
||||
list: {
|
||||
url: '/api/account/bill/list',
|
||||
name: '账单列表',
|
||||
get: async function(params) {
|
||||
return request.get(this.url, {params: params})
|
||||
}
|
||||
},
|
||||
// 添加账单
|
||||
add: {
|
||||
url: '/api/account/bill/add',
|
||||
name: '添加账单',
|
||||
post: async function(params) {
|
||||
return request.post(this.url, params)
|
||||
}
|
||||
},
|
||||
// 编辑账单
|
||||
edit: {
|
||||
url: '/api/account/bill/edit',
|
||||
name: '编辑账单',
|
||||
post: async function(params) {
|
||||
return request.post(this.url, params)
|
||||
}
|
||||
},
|
||||
// 删除账单
|
||||
delete: {
|
||||
url: '/api/account/bill/delete',
|
||||
name: '删除账单',
|
||||
post: async function(params) {
|
||||
return request.post(this.url, params)
|
||||
}
|
||||
},
|
||||
// 账单详情
|
||||
detail: {
|
||||
url: '/api/account/bill/detail',
|
||||
name: '账单详情',
|
||||
get: async function(params) {
|
||||
return request.get(this.url, {params: params})
|
||||
}
|
||||
}
|
||||
}
|
||||
76
resources/mobile/api/modules/family.js
Normal file
76
resources/mobile/api/modules/family.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export default {
|
||||
// 获取家庭信息
|
||||
info: {
|
||||
url: '/api/account/family/info',
|
||||
name: '家庭信息',
|
||||
get: async function(params) {
|
||||
return request.get(this.url, {params: params})
|
||||
}
|
||||
},
|
||||
// 创建家庭
|
||||
create: {
|
||||
url: '/api/account/family/create',
|
||||
name: '创建家庭',
|
||||
post: async function(params) {
|
||||
return request.post(this.url, params)
|
||||
}
|
||||
},
|
||||
// 加入家庭
|
||||
join: {
|
||||
url: '/api/account/family/join',
|
||||
name: '加入家庭',
|
||||
post: async function(params) {
|
||||
return request.post(this.url, params)
|
||||
}
|
||||
},
|
||||
// 退出家庭
|
||||
leave: {
|
||||
url: '/api/account/family/leave',
|
||||
name: '退出家庭',
|
||||
post: async function(params) {
|
||||
return request.post(this.url, params)
|
||||
}
|
||||
},
|
||||
// 获取家庭邀请码
|
||||
inviteCode: {
|
||||
url: '/api/account/family/invite-code',
|
||||
name: '家庭邀请码',
|
||||
get: async function(params) {
|
||||
return request.get(this.url, {params: params})
|
||||
}
|
||||
},
|
||||
// 重新生成邀请码
|
||||
regenerateInviteCode: {
|
||||
url: '/api/account/family/regenerate-invite-code',
|
||||
name: '重新生成邀请码',
|
||||
post: async function(params) {
|
||||
return request.post(this.url, params)
|
||||
}
|
||||
},
|
||||
// 移除家庭成员
|
||||
removeMember: {
|
||||
url: '/api/account/family/remove-member',
|
||||
name: '移除家庭成员',
|
||||
post: async function(params) {
|
||||
return request.post(this.url, params)
|
||||
}
|
||||
},
|
||||
// 获取家庭成员列表
|
||||
members: {
|
||||
url: '/api/account/family/members',
|
||||
name: '家庭成员列表',
|
||||
get: async function(params) {
|
||||
return request.get(this.url, {params: params})
|
||||
}
|
||||
},
|
||||
// 转让家主
|
||||
transferOwner: {
|
||||
url: '/api/account/family/transfer-owner',
|
||||
name: '转让家主',
|
||||
post: async function(params) {
|
||||
return request.post(this.url, params)
|
||||
}
|
||||
}
|
||||
}
|
||||
44
resources/mobile/api/modules/statistics.js
Normal file
44
resources/mobile/api/modules/statistics.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export default {
|
||||
// 获取统计概览
|
||||
overview: {
|
||||
url: '/api/account/statistics/overview',
|
||||
name: '统计概览',
|
||||
get: async function(params) {
|
||||
return request.get(this.url, {params: params})
|
||||
}
|
||||
},
|
||||
// 获取收支趋势
|
||||
trend: {
|
||||
url: '/api/account/statistics/trend',
|
||||
name: '收支趋势',
|
||||
get: async function(params) {
|
||||
return request.get(this.url, {params: params})
|
||||
}
|
||||
},
|
||||
// 获取分类统计
|
||||
category: {
|
||||
url: '/api/account/statistics/category',
|
||||
name: '分类统计',
|
||||
get: async function(params) {
|
||||
return request.get(this.url, {params: params})
|
||||
}
|
||||
},
|
||||
// 获取月度报表
|
||||
monthly: {
|
||||
url: '/api/account/statistics/monthly',
|
||||
name: '月度报表',
|
||||
get: async function(params) {
|
||||
return request.get(this.url, {params: params})
|
||||
}
|
||||
},
|
||||
// 获取年度报表
|
||||
yearly: {
|
||||
url: '/api/account/statistics/yearly',
|
||||
name: '年度报表',
|
||||
get: async function(params) {
|
||||
return request.get(this.url, {params: params})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,21 @@
|
||||
{
|
||||
"path": "pages/account/bill/index"
|
||||
},
|
||||
{
|
||||
"path": "pages/account/bill/add"
|
||||
},
|
||||
{
|
||||
"path": "pages/account/statistics/index"
|
||||
},
|
||||
{
|
||||
"path": "pages/family/index"
|
||||
},
|
||||
{
|
||||
"path": "pages/family/create"
|
||||
},
|
||||
{
|
||||
"path": "pages/family/join"
|
||||
},
|
||||
{
|
||||
"path": "pages/ucenter/index/index"
|
||||
},
|
||||
|
||||
378
resources/mobile/pages/account/bill/add.vue
Normal file
378
resources/mobile/pages/account/bill/add.vue
Normal file
@@ -0,0 +1,378 @@
|
||||
<template>
|
||||
<un-pages
|
||||
:show-nav-bar="true"
|
||||
nav-bar-title="记一笔"
|
||||
:show-back="true"
|
||||
>
|
||||
<view class="page-content">
|
||||
<!-- 收支类型切换 -->
|
||||
<view class="type-switch">
|
||||
<view
|
||||
class="type-item"
|
||||
:class="{active: form.type === 'expense'}"
|
||||
@tap="switchType('expense')"
|
||||
>
|
||||
<text>支出</text>
|
||||
</view>
|
||||
<view
|
||||
class="type-item"
|
||||
:class="{active: form.type === 'income'}"
|
||||
@tap="switchType('income')"
|
||||
>
|
||||
<text>收入</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 金额输入 -->
|
||||
<view class="amount-section">
|
||||
<view class="amount-input-wrapper">
|
||||
<text class="currency">¥</text>
|
||||
<input
|
||||
class="amount-input"
|
||||
type="digit"
|
||||
v-model="form.amount"
|
||||
placeholder="0.00"
|
||||
placeholder-style="color: #ddd"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类选择 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">选择分类</view>
|
||||
<view class="category-grid">
|
||||
<view
|
||||
v-for="category in currentCategories"
|
||||
:key="category.id"
|
||||
class="category-item"
|
||||
:class="{active: form.categoryId === category.id}"
|
||||
@tap="selectCategory(category)"
|
||||
>
|
||||
<view class="category-icon" :style="{background: category.color}">
|
||||
<text>{{ category.icon }}</text>
|
||||
</view>
|
||||
<text class="category-name">{{ category.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 备注 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">备注</view>
|
||||
<view class="textarea-wrapper">
|
||||
<textarea
|
||||
v-model="form.remark"
|
||||
placeholder="添加备注(可选)"
|
||||
placeholder-style="color: #ccc"
|
||||
maxlength="100"
|
||||
/>
|
||||
<text class="char-count">{{ form.remark.length }}/100</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日期选择 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">日期</view>
|
||||
<picker mode="date" :value="form.date" @change="onDateChange">
|
||||
<view class="picker-wrapper">
|
||||
<text class="picker-text">{{ form.date }}</text>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-section">
|
||||
<button class="submit-btn" @tap="handleSubmit" :loading="loading">
|
||||
保存
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</un-pages>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
form: {
|
||||
type: 'expense',
|
||||
amount: '',
|
||||
categoryId: '',
|
||||
remark: '',
|
||||
date: this.getTodayDate()
|
||||
},
|
||||
expenseCategories: [
|
||||
{ id: 1, name: '餐饮', icon: '🍜', color: '#FF6B6B' },
|
||||
{ id: 2, name: '交通', icon: '🚗', color: '#4ECDC4' },
|
||||
{ id: 3, name: '购物', icon: '🛒', color: '#45B7D1' },
|
||||
{ id: 4, name: '娱乐', icon: '🎮', color: '#96CEB4' },
|
||||
{ id: 5, name: '医疗', icon: '💊', color: '#FFEAA7' },
|
||||
{ id: 6, name: '教育', icon: '📚', color: '#DDA0DD' },
|
||||
{ id: 7, name: '居住', icon: '🏠', color: '#98D8C8' },
|
||||
{ id: 8, name: '其他', icon: '📦', color: '#BDC3C7' }
|
||||
],
|
||||
incomeCategories: [
|
||||
{ id: 101, name: '工资', icon: '💰', color: '#2ECC71' },
|
||||
{ id: 102, name: '奖金', icon: '🎁', color: '#E74C3C' },
|
||||
{ id: 103, name: '投资', icon: '📈', color: '#3498DB' },
|
||||
{ id: 104, name: '兼职', icon: '💼', color: '#9B59B6' },
|
||||
{ id: 105, name: '其他', icon: '💎', color: '#1ABC9C' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentCategories() {
|
||||
return this.form.type === 'expense' ? this.expenseCategories : this.incomeCategories
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getTodayDate() {
|
||||
const today = new Date()
|
||||
const year = today.getFullYear()
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(today.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
},
|
||||
switchType(type) {
|
||||
this.form.type = type
|
||||
this.form.categoryId = ''
|
||||
},
|
||||
selectCategory(category) {
|
||||
this.form.categoryId = category.id
|
||||
},
|
||||
onDateChange(e) {
|
||||
this.form.date = e.detail.value
|
||||
},
|
||||
async handleSubmit() {
|
||||
// 验证
|
||||
if (!this.form.amount || parseFloat(this.form.amount) <= 0) {
|
||||
uni.showToast({
|
||||
title: '请输入有效金额',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.form.categoryId) {
|
||||
uni.showToast({
|
||||
title: '请选择分类',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const res = await this.$api.bill.add.post({
|
||||
type: this.form.type,
|
||||
amount: parseFloat(this.form.amount),
|
||||
category_id: this.form.categoryId,
|
||||
remark: this.form.remark,
|
||||
date: this.form.date
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
uni.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.message || '保存失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存账单失败', error)
|
||||
uni.showToast({
|
||||
title: '保存失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.type-switch {
|
||||
display: flex;
|
||||
background: #f5f5f5;
|
||||
border-radius: 50rpx;
|
||||
padding: 8rpx;
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.type-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 20rpx 0;
|
||||
border-radius: 50rpx;
|
||||
transition: all 0.3s;
|
||||
|
||||
text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
|
||||
text {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.amount-section {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 40rpx 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.amount-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.currency {
|
||||
font-size: 60rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.amount-input {
|
||||
flex: 1;
|
||||
font-size: 60rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.category-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20rpx;
|
||||
|
||||
.category-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
|
||||
.category-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 12rpx;
|
||||
opacity: 0.6;
|
||||
transition: all 0.3s;
|
||||
|
||||
text {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.category-name {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
&.active {
|
||||
.category-icon {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.category-name {
|
||||
color: #667eea;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
position: relative;
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 120rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.char-count {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
|
||||
.picker-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.submit-section {
|
||||
margin-top: 40rpx;
|
||||
|
||||
.submit-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 50rpx;
|
||||
height: 90rpx;
|
||||
line-height: 90rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -7,9 +7,69 @@
|
||||
@tab-change="handleTabChange"
|
||||
>
|
||||
<view class="page-content">
|
||||
<view class="empty-state">
|
||||
<uni-icons type="list" size="100" color="#ddd"></uni-icons>
|
||||
<text class="empty-text">账单功能开发中</text>
|
||||
<!-- 统计卡片 -->
|
||||
<view class="stats-card">
|
||||
<view class="stat-item">
|
||||
<text class="stat-label">本月支出</text>
|
||||
<text class="stat-value expense">¥{{ monthExpense.toFixed(2) }}</text>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-label">本月收入</text>
|
||||
<text class="stat-value income">¥{{ monthIncome.toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 月份选择 -->
|
||||
<view class="month-selector">
|
||||
<picker mode="date" fields="month" :value="currentMonth" @change="onMonthChange">
|
||||
<view class="month-picker">
|
||||
<uni-icons type="left" size="20" color="#666"></uni-icons>
|
||||
<text class="month-text">{{ currentMonth }}</text>
|
||||
<uni-icons type="right" size="20" color="#666"></uni-icons>
|
||||
</view>
|
||||
</picker>
|
||||
</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">{{ date }}</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 class="fab-button" @tap="addBill">
|
||||
<uni-icons type="plus" size="24" color="#fff"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</un-pages>
|
||||
@@ -17,9 +77,120 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
currentMonth: this.getCurrentMonth(),
|
||||
billList: [],
|
||||
monthExpense: 0,
|
||||
monthIncome: 0,
|
||||
categoryMap: {
|
||||
// 支出分类
|
||||
1: { name: '餐饮', icon: '🍜', color: '#FF6B6B' },
|
||||
2: { name: '交通', icon: '🚗', color: '#4ECDC4' },
|
||||
3: { name: '购物', icon: '🛒', color: '#45B7D1' },
|
||||
4: { name: '娱乐', icon: '🎮', color: '#96CEB4' },
|
||||
5: { name: '医疗', icon: '💊', color: '#FFEAA7' },
|
||||
6: { name: '教育', icon: '📚', color: '#DDA0DD' },
|
||||
7: { name: '居住', icon: '🏠', color: '#98D8C8' },
|
||||
8: { name: '其他', icon: '📦', color: '#BDC3C7' },
|
||||
// 收入分类
|
||||
101: { name: '工资', icon: '💰', color: '#2ECC71' },
|
||||
102: { name: '奖金', icon: '🎁', color: '#E74C3C' },
|
||||
103: { name: '投资', icon: '📈', color: '#3498DB' },
|
||||
104: { name: '兼职', icon: '💼', color: '#9B59B6' },
|
||||
105: { name: '其他', icon: '💎', color: '#1ABC9C' }
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
groupedBills() {
|
||||
const groups = {}
|
||||
this.billList.forEach(bill => {
|
||||
const date = bill.date
|
||||
if (!groups[date]) {
|
||||
groups[date] = {
|
||||
bills: [],
|
||||
expense: 0,
|
||||
income: 0
|
||||
}
|
||||
}
|
||||
groups[date].bills.push(bill)
|
||||
if (bill.type === 'expense') {
|
||||
groups[date].expense += bill.amount
|
||||
} else {
|
||||
groups[date].income += bill.amount
|
||||
}
|
||||
})
|
||||
return groups
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadBillList()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.loadBillList().then(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
handleTabChange(path) {
|
||||
console.log('Tab changed to:', path)
|
||||
},
|
||||
getCurrentMonth() {
|
||||
const today = new Date()
|
||||
const year = today.getFullYear()
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0')
|
||||
return `${year}-${month}`
|
||||
},
|
||||
onMonthChange(e) {
|
||||
this.currentMonth = e.detail.value
|
||||
this.loadBillList()
|
||||
},
|
||||
async loadBillList() {
|
||||
this.loading = true
|
||||
try {
|
||||
const [year, month] = this.currentMonth.split('-')
|
||||
const res = await this.$api.bill.list.get({
|
||||
year: parseInt(year),
|
||||
month: parseInt(month)
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
this.billList = res.data?.list || []
|
||||
this.monthExpense = res.data?.month_expense || 0
|
||||
this.monthIncome = res.data?.month_income || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载账单列表失败', error)
|
||||
uni.showToast({
|
||||
title: '加载失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
addBill() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/account/bill/add'
|
||||
})
|
||||
},
|
||||
editBill(bill) {
|
||||
// TODO: 实现编辑功能
|
||||
uni.showToast({
|
||||
title: '编辑功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
getCategoryName(categoryId) {
|
||||
return this.categoryMap[categoryId]?.name || '未知'
|
||||
},
|
||||
getCategoryIcon(categoryId) {
|
||||
return this.categoryMap[categoryId]?.icon || '📦'
|
||||
},
|
||||
getCategoryColor(categoryId) {
|
||||
return this.categoryMap[categoryId]?.color || '#BDC3C7'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,20 +198,201 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
padding: 40rpx 30rpx;
|
||||
padding: 30rpx;
|
||||
padding-bottom: 150rpx;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
.stats-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 200rpx 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 20rpx;
|
||||
padding: 40rpx 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin-top: 40rpx;
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
|
||||
&.expense {
|
||||
color: #FFEAA7;
|
||||
}
|
||||
|
||||
&.income {
|
||||
color: #98D8C8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
width: 2rpx;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
margin: 0 30rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.month-selector {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.month-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-radius: 50rpx;
|
||||
padding: 16rpx 40rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
|
||||
.month-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
margin: 0 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bill-list {
|
||||
.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;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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);
|
||||
|
||||
uni-icons {
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bill-group {
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 10rpx;
|
||||
|
||||
.group-date {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.group-amount {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.bill-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
.bill-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
|
||||
text {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.bill-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.bill-category {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.bill-remark {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.bill-amount {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
|
||||
&.expense {
|
||||
color: #FF6B6B;
|
||||
}
|
||||
|
||||
&.income {
|
||||
color: #2ECC71;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fab-button {
|
||||
position: fixed;
|
||||
right: 40rpx;
|
||||
bottom: 160rpx;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.5);
|
||||
z-index: 100;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,121 @@
|
||||
<template>
|
||||
<un-pages
|
||||
:show-nav-bar="true"
|
||||
nav-bar-title="统计"
|
||||
:show-back="false"
|
||||
nav-bar-title="统计分析"
|
||||
:show-back="true"
|
||||
:show-tab-bar="true"
|
||||
@tab-change="handleTabChange"
|
||||
>
|
||||
<view class="page-content">
|
||||
<view class="empty-state">
|
||||
<uni-icons type="chart" size="100" color="#ddd"></uni-icons>
|
||||
<text class="empty-text">统计功能开发中</text>
|
||||
<!-- 月份选择 -->
|
||||
<view class="month-selector">
|
||||
<picker mode="date" fields="month" :value="currentMonth" @change="onMonthChange">
|
||||
<view class="month-picker">
|
||||
<uni-icons type="left" size="20" color="#666"></uni-icons>
|
||||
<text class="month-text">{{ currentMonth }}</text>
|
||||
<uni-icons type="right" size="20" color="#666"></uni-icons>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 概览卡片 -->
|
||||
<view class="overview-section">
|
||||
<view class="overview-card income">
|
||||
<text class="card-label">本月收入</text>
|
||||
<text class="card-amount">¥{{ overview.income.toFixed(2) }}</text>
|
||||
</view>
|
||||
<view class="overview-card expense">
|
||||
<text class="card-label">本月支出</text>
|
||||
<text class="card-amount">¥{{ overview.expense.toFixed(2) }}</text>
|
||||
</view>
|
||||
<view class="overview-card balance">
|
||||
<text class="card-label">结余</text>
|
||||
<text class="card-amount">¥{{ overview.balance.toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类统计 -->
|
||||
<view class="category-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">支出分类</text>
|
||||
<text class="section-total">共 ¥{{ categoryTotal.toFixed(2) }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="loading-state">
|
||||
<uni-load-more status="loading"></uni-load-more>
|
||||
</view>
|
||||
|
||||
<view v-else-if="categoryList.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无数据</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="category-list">
|
||||
<view class="category-item" v-for="(item, index) in categoryList" :key="index">
|
||||
<view class="category-info">
|
||||
<view class="category-icon" :style="{background: item.color}">
|
||||
<text>{{ item.icon }}</text>
|
||||
</view>
|
||||
<view class="category-details">
|
||||
<text class="category-name">{{ item.name }}</text>
|
||||
<view class="progress-bar">
|
||||
<view class="progress" :style="{width: item.percent + '%', background: item.color}"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="category-amount">
|
||||
<text class="amount">¥{{ item.amount.toFixed(2) }}</text>
|
||||
<text class="percent">{{ item.percent }}%</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收支趋势 -->
|
||||
<view class="trend-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">收支趋势</text>
|
||||
<text class="period-text">最近7天</text>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="loading-state">
|
||||
<uni-load-more status="loading"></uni-load-more>
|
||||
</view>
|
||||
|
||||
<view v-else-if="trendList.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无数据</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="trend-chart">
|
||||
<view class="chart-container">
|
||||
<view
|
||||
class="chart-bar"
|
||||
v-for="(item, index) in trendList"
|
||||
:key="index"
|
||||
>
|
||||
<view class="bar-wrapper">
|
||||
<view
|
||||
class="bar income-bar"
|
||||
:style="{height: item.incomeHeight + '%'}"
|
||||
></view>
|
||||
<view
|
||||
class="bar expense-bar"
|
||||
:style="{height: item.expenseHeight + '%'}"
|
||||
></view>
|
||||
</view>
|
||||
<text class="bar-label">{{ item.dateLabel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="chart-legend">
|
||||
<view class="legend-item">
|
||||
<view class="legend-color income-legend"></view>
|
||||
<text class="legend-text">收入</text>
|
||||
</view>
|
||||
<view class="legend-item">
|
||||
<view class="legend-color expense-legend"></view>
|
||||
<text class="legend-text">支出</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</un-pages>
|
||||
@@ -17,9 +123,133 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
currentMonth: this.getCurrentMonth(),
|
||||
overview: {
|
||||
income: 0,
|
||||
expense: 0,
|
||||
balance: 0
|
||||
},
|
||||
categoryList: [],
|
||||
categoryTotal: 0,
|
||||
trendList: [],
|
||||
categoryMap: {
|
||||
1: { name: '餐饮', icon: '🍜', color: '#FF6B6B' },
|
||||
2: { name: '交通', icon: '🚗', color: '#4ECDC4' },
|
||||
3: { name: '购物', icon: '🛒', color: '#45B7D1' },
|
||||
4: { name: '娱乐', icon: '🎮', color: '#96CEB4' },
|
||||
5: { name: '医疗', icon: '💊', color: '#FFEAA7' },
|
||||
6: { name: '教育', icon: '📚', color: '#DDA0DD' },
|
||||
7: { name: '居住', icon: '🏠', color: '#98D8C8' },
|
||||
8: { name: '其他', icon: '📦', color: '#BDC3C7' }
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadStatistics()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.loadStatistics().then(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
handleTabChange(path) {
|
||||
console.log('Tab changed to:', path)
|
||||
},
|
||||
getCurrentMonth() {
|
||||
const today = new Date()
|
||||
const year = today.getFullYear()
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0')
|
||||
return `${year}-${month}`
|
||||
},
|
||||
onMonthChange(e) {
|
||||
this.currentMonth = e.detail.value
|
||||
this.loadStatistics()
|
||||
},
|
||||
async loadStatistics() {
|
||||
this.loading = true
|
||||
try {
|
||||
const [year, month] = this.currentMonth.split('-')
|
||||
|
||||
// 获取统计概览
|
||||
const overviewRes = await this.$api.statistics.overview.get({
|
||||
year: parseInt(year),
|
||||
month: parseInt(month)
|
||||
})
|
||||
if (overviewRes.code === 200) {
|
||||
this.overview = overviewRes.data || { income: 0, expense: 0, balance: 0 }
|
||||
}
|
||||
|
||||
// 获取分类统计
|
||||
const categoryRes = await this.$api.statistics.category.get({
|
||||
year: parseInt(year),
|
||||
month: parseInt(month)
|
||||
})
|
||||
if (categoryRes.code === 200) {
|
||||
this.processCategoryData(categoryRes.data || [])
|
||||
}
|
||||
|
||||
// 获取收支趋势
|
||||
const trendRes = await this.$api.statistics.trend.get({
|
||||
year: parseInt(year),
|
||||
month: parseInt(month)
|
||||
})
|
||||
if (trendRes.code === 200) {
|
||||
this.processTrendData(trendRes.data || [])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载统计数据失败', error)
|
||||
uni.showToast({
|
||||
title: '加载失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
processCategoryData(data) {
|
||||
let total = 0
|
||||
const list = data.map(item => {
|
||||
total += item.amount
|
||||
return item
|
||||
})
|
||||
|
||||
this.categoryTotal = total
|
||||
this.categoryList = list.map(item => {
|
||||
const categoryInfo = this.categoryMap[item.category_id] || { name: '未知', icon: '📦', color: '#BDC3C7' }
|
||||
return {
|
||||
...categoryInfo,
|
||||
amount: item.amount,
|
||||
percent: total > 0 ? Math.round((item.amount / total) * 100) : 0
|
||||
}
|
||||
}).sort((a, b) => b.amount - a.amount)
|
||||
},
|
||||
processTrendData(data) {
|
||||
// 找出最大值用于计算高度百分比
|
||||
let maxIncome = 0
|
||||
let maxExpense = 0
|
||||
|
||||
data.forEach(item => {
|
||||
maxIncome = Math.max(maxIncome, item.income)
|
||||
maxExpense = Math.max(maxExpense, item.expense)
|
||||
})
|
||||
|
||||
this.trendList = data.map(item => {
|
||||
const date = new Date(item.date)
|
||||
const dateLabel = `${date.getMonth() + 1}/${date.getDate()}`
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,20 +257,296 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
padding: 40rpx 30rpx;
|
||||
padding: 30rpx;
|
||||
padding-bottom: 150rpx;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
.month-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 200rpx 0;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin-top: 40rpx;
|
||||
.month-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-radius: 50rpx;
|
||||
padding: 16rpx 40rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
|
||||
.month-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
margin: 0 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overview-section {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.overview-card {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.card-label {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.card-amount {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&.income .card-amount {
|
||||
color: #2ECC71;
|
||||
}
|
||||
|
||||
&.expense .card-amount {
|
||||
color: #FF6B6B;
|
||||
}
|
||||
|
||||
&.balance .card-amount {
|
||||
color: #667eea;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-section {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.section-total {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.empty-state {
|
||||
padding: 60rpx 0;
|
||||
text-align: center;
|
||||
|
||||
.empty-text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.category-list {
|
||||
.category-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 2rpx solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.category-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.category-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
|
||||
text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.category-details {
|
||||
flex: 1;
|
||||
|
||||
.category-name {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
margin-bottom: 12rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.progress {
|
||||
height: 100%;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-amount {
|
||||
text-align: right;
|
||||
margin-left: 20rpx;
|
||||
|
||||
.amount {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.percent {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.trend-section {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.period-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.empty-state {
|
||||
padding: 60rpx 0;
|
||||
text-align: center;
|
||||
|
||||
.empty-text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.trend-chart {
|
||||
.chart-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
height: 300rpx;
|
||||
padding: 20rpx 0;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.chart-bar {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.bar-wrapper {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 4rpx;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.bar {
|
||||
width: 24rpx;
|
||||
min-height: 4rpx;
|
||||
border-radius: 4rpx;
|
||||
transition: height 0.3s;
|
||||
|
||||
&.income-bar {
|
||||
background: #2ECC71;
|
||||
}
|
||||
|
||||
&.expense-bar {
|
||||
background: #FF6B6B;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 40rpx;
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.legend-color {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 4rpx;
|
||||
margin-right: 8rpx;
|
||||
|
||||
&.income-legend {
|
||||
background: #2ECC71;
|
||||
}
|
||||
|
||||
&.expense-legend {
|
||||
background: #FF6B6B;
|
||||
}
|
||||
}
|
||||
|
||||
.legend-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
182
resources/mobile/pages/family/create.vue
Normal file
182
resources/mobile/pages/family/create.vue
Normal file
@@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<un-pages
|
||||
:show-nav-bar="true"
|
||||
nav-bar-title="创建家庭"
|
||||
:show-back="true"
|
||||
>
|
||||
<view class="page-content">
|
||||
<view class="form-section">
|
||||
<view class="section-title">家庭名称</view>
|
||||
<view class="input-wrapper">
|
||||
<input
|
||||
v-model="familyName"
|
||||
placeholder="请输入家庭名称"
|
||||
placeholder-style="color: #ccc"
|
||||
maxlength="20"
|
||||
/>
|
||||
<text class="char-count">{{ familyName.length }}/20</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tips-section">
|
||||
<view class="tip-item">
|
||||
<uni-icons type="info" size="18" color="#667eea"></uni-icons>
|
||||
<text class="tip-text">创建后您将成为家主,可以管理家庭成员</text>
|
||||
</view>
|
||||
<view class="tip-item">
|
||||
<uni-icons type="info" size="18" color="#667eea"></uni-icons>
|
||||
<text class="tip-text">每个用户只能创建或加入一个家庭</text>
|
||||
</view>
|
||||
<view class="tip-item">
|
||||
<uni-icons type="info" size="18" color="#667eea"></uni-icons>
|
||||
<text class="tip-text">创建家庭后,家庭成员可以查看和记录家庭账单</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="submit-section">
|
||||
<button class="submit-btn" @tap="handleCreate" :loading="loading">
|
||||
创建家庭
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</un-pages>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
familyName: '',
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleCreate() {
|
||||
if (!this.familyName.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入家庭名称',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const res = await this.$store.dispatch('family/createFamily', this.familyName.trim())
|
||||
if (res.code === 200) {
|
||||
uni.showToast({
|
||||
title: '创建成功',
|
||||
icon: 'success'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.message || '创建失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建家庭失败', error)
|
||||
uni.showToast({
|
||||
title: '创建失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.char-count {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -30rpx;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tips-section {
|
||||
background: rgba(102, 126, 234, 0.05);
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.tip-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
uni-icons {
|
||||
margin-right: 12rpx;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submit-section {
|
||||
margin-top: 60rpx;
|
||||
|
||||
.submit-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 50rpx;
|
||||
height: 90rpx;
|
||||
line-height: 90rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
588
resources/mobile/pages/family/index.vue
Normal file
588
resources/mobile/pages/family/index.vue
Normal file
@@ -0,0 +1,588 @@
|
||||
<template>
|
||||
<un-pages
|
||||
:show-nav-bar="true"
|
||||
nav-bar-title="家庭管理"
|
||||
:show-back="true"
|
||||
>
|
||||
<view class="page-content">
|
||||
<view v-if="loading" class="loading-state">
|
||||
<uni-load-more status="loading"></uni-load-more>
|
||||
</view>
|
||||
|
||||
<!-- 已加入家庭 -->
|
||||
<view v-else-if="hasFamily" class="family-container">
|
||||
<!-- 家庭信息卡片 -->
|
||||
<view class="family-card">
|
||||
<view class="family-header">
|
||||
<view class="family-icon">
|
||||
<text>🏠</text>
|
||||
</view>
|
||||
<view class="family-info">
|
||||
<text class="family-name">{{ familyInfo.name }}</text>
|
||||
<text class="family-role" v-if="isOwner">我是家主</text>
|
||||
<text class="family-role" v-else>家庭成员</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 家庭邀请码(仅家主可见) -->
|
||||
<view v-if="isOwner" class="invite-section">
|
||||
<view class="invite-info">
|
||||
<text class="invite-label">家庭邀请码</text>
|
||||
<view class="invite-code-wrapper">
|
||||
<text class="invite-code">{{ inviteCode || '加载中...' }}</text>
|
||||
<view class="copy-btn" @tap="copyInviteCode">
|
||||
<uni-icons type="copy" size="16" color="#667eea"></uni-icons>
|
||||
<text>复制</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<button class="regenerate-btn" @tap="regenerateInviteCode" :loading="regenerating">
|
||||
重新生成邀请码
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 家庭成员列表 -->
|
||||
<view class="members-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">家庭成员</text>
|
||||
<text class="member-count">{{ memberList.length }}人</text>
|
||||
</view>
|
||||
|
||||
<view class="member-list">
|
||||
<view class="member-item" v-for="member in memberList" :key="member.id">
|
||||
<view class="member-avatar">
|
||||
<text>{{ member.name ? member.name.charAt(0) : 'U' }}</text>
|
||||
</view>
|
||||
<view class="member-info">
|
||||
<text class="member-name">{{ member.name || member.username }}</text>
|
||||
<text class="member-role" v-if="member.is_owner">家主</text>
|
||||
</view>
|
||||
<!-- 移除成员按钮(仅家主可见) -->
|
||||
<view v-if="isOwner && !member.is_owner" class="remove-btn" @tap="removeMember(member)">
|
||||
<uni-icons type="clear" size="20" color="#FF6B6B"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 转让家主(仅家主可见) -->
|
||||
<view v-if="isOwner && memberList.length > 1" class="action-btn">
|
||||
<button class="secondary-btn" @tap="transferOwner">
|
||||
转让家主
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 退出家庭按钮 -->
|
||||
<view class="action-btn">
|
||||
<button class="exit-btn" @tap="confirmLeaveFamily">
|
||||
退出家庭
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 未加入家庭 -->
|
||||
<view v-else class="no-family-container">
|
||||
<view class="no-family-icon">
|
||||
<text>🏠</text>
|
||||
</view>
|
||||
<text class="no-family-title">您还未加入任何家庭</text>
|
||||
<text class="no-family-desc">创建或加入家庭,与家人一起记账</text>
|
||||
|
||||
<view class="action-buttons">
|
||||
<button class="primary-btn" @tap="goToCreate">
|
||||
<uni-icons type="plus" size="20" color="#fff"></uni-icons>
|
||||
<text>创建家庭</text>
|
||||
</button>
|
||||
<button class="secondary-btn" @tap="goToJoin">
|
||||
<uni-icons type="personadd" size="20" color="#667eea"></uni-icons>
|
||||
<text>加入家庭</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</un-pages>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
familyInfo: {},
|
||||
inviteCode: '',
|
||||
regenerating: false,
|
||||
memberList: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasFamily() {
|
||||
if (!this.$store || !this.$store.getters) {
|
||||
return false
|
||||
}
|
||||
return this.$store.getters['family/hasFamily'] || false
|
||||
},
|
||||
isOwner() {
|
||||
if (!this.$store || !this.$store.state || !this.$store.state.family) {
|
||||
return false
|
||||
}
|
||||
return this.$store.state.family.isOwner || false
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadFamilyData()
|
||||
},
|
||||
onShow() {
|
||||
// 从其他页面返回时刷新数据
|
||||
this.loadFamilyData()
|
||||
},
|
||||
methods: {
|
||||
async loadFamilyData() {
|
||||
if (!this.hasFamily) {
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
// 获取家庭信息
|
||||
const familyRes = await this.$api.family.info.get()
|
||||
if (familyRes.code === 200) {
|
||||
this.familyInfo = familyRes.data
|
||||
this.$store.commit('family/setFamilyInfo', familyRes.data)
|
||||
}
|
||||
|
||||
// 获取邀请码(仅家主)
|
||||
if (this.isOwner) {
|
||||
const codeRes = await this.$api.family.inviteCode.get()
|
||||
if (codeRes.code === 200) {
|
||||
this.inviteCode = codeRes.data.invite_code
|
||||
}
|
||||
}
|
||||
|
||||
// 获取家庭成员列表
|
||||
const membersRes = await this.$api.family.members.get()
|
||||
if (membersRes.code === 200) {
|
||||
this.memberList = membersRes.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载家庭数据失败', error)
|
||||
uni.showToast({
|
||||
title: '加载失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
copyInviteCode() {
|
||||
uni.setClipboardData({
|
||||
data: this.inviteCode,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: '邀请码已复制',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
async regenerateInviteCode() {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '重新生成邀请码后,旧的邀请码将失效,确定要重新生成吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
this.regenerating = true
|
||||
try {
|
||||
const res = await this.$api.family.regenerateInviteCode.post()
|
||||
if (res.code === 200) {
|
||||
this.inviteCode = res.data.invite_code
|
||||
uni.showToast({
|
||||
title: '邀请码已更新',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重新生成邀请码失败', error)
|
||||
uni.showToast({
|
||||
title: '操作失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.regenerating = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
removeMember(member) {
|
||||
uni.showModal({
|
||||
title: '确认移除',
|
||||
content: `确定要将 ${member.name || member.username} 移出家庭吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const result = await this.$api.family.removeMember.post({
|
||||
user_id: member.id
|
||||
})
|
||||
if (result.code === 200) {
|
||||
uni.showToast({
|
||||
title: '移除成功',
|
||||
icon: 'success'
|
||||
})
|
||||
this.loadFamilyData()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('移除成员失败', error)
|
||||
uni.showToast({
|
||||
title: '操作失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
transferOwner() {
|
||||
// TODO: 实现转让家主功能
|
||||
uni.showToast({
|
||||
title: '功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
confirmLeaveFamily() {
|
||||
uni.showModal({
|
||||
title: '确认退出',
|
||||
content: '退出家庭后,您将无法查看和记录家庭账单,确定要退出吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const result = await this.$store.dispatch('family/leaveFamily')
|
||||
if (result.code === 200) {
|
||||
uni.showToast({
|
||||
title: '已退出家庭',
|
||||
icon: 'success'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('退出家庭失败', error)
|
||||
uni.showToast({
|
||||
title: '操作失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
goToCreate() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/family/create'
|
||||
})
|
||||
},
|
||||
goToJoin() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/family/join'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
padding: 30rpx;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.no-family-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 0;
|
||||
|
||||
.no-family-icon {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 40rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
|
||||
|
||||
text {
|
||||
font-size: 80rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.no-family-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.no-family-desc {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
width: 100%;
|
||||
padding: 0 40rpx;
|
||||
|
||||
button {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.family-container {
|
||||
.family-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 40rpx 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.family-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.family-icon {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
|
||||
text {
|
||||
font-size: 50rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.family-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.family-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.family-role {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invite-section {
|
||||
border-top: 2rpx solid #f5f5f5;
|
||||
padding-top: 30rpx;
|
||||
|
||||
.invite-info {
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.invite-label {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-bottom: 16rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.invite-code-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #f8f8f8;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
|
||||
.invite-code {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10rpx 20rpx;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 50rpx;
|
||||
|
||||
text {
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
margin-left: 6rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.regenerate-btn {
|
||||
width: 100%;
|
||||
height: 70rpx;
|
||||
line-height: 70rpx;
|
||||
font-size: 26rpx;
|
||||
background: #f8f8f8;
|
||||
color: #666;
|
||||
border: none;
|
||||
border-radius: 50rpx;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.members-section {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.member-count {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.member-list {
|
||||
.member-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 2rpx solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.member-avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
|
||||
text {
|
||||
font-size: 32rpx;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.member-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.member-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.member-role {
|
||||
font-size: 22rpx;
|
||||
color: #667eea;
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
padding: 10rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
height: 90rpx;
|
||||
line-height: 90rpx;
|
||||
border-radius: 50rpx;
|
||||
font-size: 28rpx;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
uni-icons {
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
background: #f8f8f8;
|
||||
color: #667eea;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
uni-icons {
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.exit-btn {
|
||||
background: #fff;
|
||||
color: #FF6B6B;
|
||||
border: 2rpx solid #FF6B6B;
|
||||
}
|
||||
</style>
|
||||
180
resources/mobile/pages/family/join.vue
Normal file
180
resources/mobile/pages/family/join.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<un-pages
|
||||
:show-nav-bar="true"
|
||||
nav-bar-title="加入家庭"
|
||||
:show-back="true"
|
||||
>
|
||||
<view class="page-content">
|
||||
<view class="form-section">
|
||||
<view class="section-title">家庭邀请码</view>
|
||||
<view class="input-wrapper">
|
||||
<input
|
||||
v-model="inviteCode"
|
||||
placeholder="请输入家庭邀请码"
|
||||
placeholder-style="color: #ccc"
|
||||
maxlength="10"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tips-section">
|
||||
<view class="tip-item">
|
||||
<uni-icons type="info" size="18" color="#667eea"></uni-icons>
|
||||
<text class="tip-text">邀请码由家庭家主提供</text>
|
||||
</view>
|
||||
<view class="tip-item">
|
||||
<uni-icons type="info" size="18" color="#667eea"></uni-icons>
|
||||
<text class="tip-text">每个用户只能加入一个家庭</text>
|
||||
</view>
|
||||
<view class="tip-item">
|
||||
<uni-icons type="info" size="18" color="#667eea"></uni-icons>
|
||||
<text class="tip-text">加入家庭后,可以查看和记录家庭账单</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="submit-section">
|
||||
<button class="submit-btn" @tap="handleJoin" :loading="loading">
|
||||
加入家庭
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</un-pages>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
inviteCode: '',
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleJoin() {
|
||||
if (!this.inviteCode.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入邀请码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (this.inviteCode.trim().length !== 10) {
|
||||
uni.showToast({
|
||||
title: '邀请码格式不正确',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const res = await this.$store.dispatch('family/joinFamily', this.inviteCode.trim())
|
||||
if (res.code === 200) {
|
||||
uni.showToast({
|
||||
title: '加入成功',
|
||||
icon: 'success'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.message || '加入失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加入家庭失败', error)
|
||||
uni.showToast({
|
||||
title: '加入失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
padding: 0;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tips-section {
|
||||
background: rgba(102, 126, 234, 0.05);
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.tip-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
uni-icons {
|
||||
margin-right: 12rpx;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submit-section {
|
||||
margin-top: 60rpx;
|
||||
|
||||
.submit-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 50rpx;
|
||||
height: 90rpx;
|
||||
line-height: 90rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -7,30 +7,112 @@
|
||||
@tab-change="handleTabChange"
|
||||
>
|
||||
<view class="page-content">
|
||||
<view class="welcome-section">
|
||||
<image class="logo" src="/static/logo.png" mode="aspectFit"></image>
|
||||
<text class="welcome-title">欢迎使用家庭记账</text>
|
||||
<text class="welcome-desc">简单好用的家庭记账助手</text>
|
||||
<!-- 用户信息头部 -->
|
||||
<view class="user-header">
|
||||
<view class="user-avatar">
|
||||
<text>{{ userName ? userName.charAt(0) : 'U' }}</text>
|
||||
</view>
|
||||
<view class="user-info">
|
||||
<text class="greeting">你好,{{ userName || '用户' }}</text>
|
||||
<text class="date-text">{{ currentDate }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收支概览卡片 -->
|
||||
<view class="overview-card">
|
||||
<view class="overview-header">
|
||||
<text class="overview-title">本月概览</text>
|
||||
<text class="overview-month">{{ currentMonth }}</text>
|
||||
</view>
|
||||
<view class="overview-body">
|
||||
<view class="overview-item income">
|
||||
<view class="overview-icon">
|
||||
<text class="icon-text">收</text>
|
||||
</view>
|
||||
<view class="overview-data">
|
||||
<text class="overview-label">收入</text>
|
||||
<text class="overview-amount">¥0.00</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="overview-divider"></view>
|
||||
<view class="overview-item expense">
|
||||
<view class="overview-icon">
|
||||
<text class="icon-text">支</text>
|
||||
</view>
|
||||
<view class="overview-data">
|
||||
<text class="overview-label">支出</text>
|
||||
<text class="overview-amount">¥0.00</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷操作 -->
|
||||
<view class="quick-actions">
|
||||
<view class="action-card" @tap="navigateTo('/pages/account/bill/add')">
|
||||
<uni-icons type="plus" size="40" color="#667eea"></uni-icons>
|
||||
<view class="action-icon-wrapper">
|
||||
<uni-icons type="plus" size="36" color="#fff"></uni-icons>
|
||||
</view>
|
||||
<text class="action-title">记一笔</text>
|
||||
<text class="action-desc">快速记录</text>
|
||||
</view>
|
||||
<view class="action-card" @tap="navigateTo('/pages/account/bill/index')">
|
||||
<uni-icons type="list" size="40" color="#f093fb"></uni-icons>
|
||||
<view class="action-icon-wrapper">
|
||||
<uni-icons type="list" size="36" color="#fff"></uni-icons>
|
||||
</view>
|
||||
<text class="action-title">账单</text>
|
||||
<text class="action-desc">查看明细</text>
|
||||
</view>
|
||||
<view class="action-card" @tap="navigateTo('/pages/account/statistics/index')">
|
||||
<view class="action-icon-wrapper">
|
||||
<uni-icons type="color-filled" size="36" color="#fff"></uni-icons>
|
||||
</view>
|
||||
<text class="action-title">统计</text>
|
||||
<text class="action-desc">数据分析</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近记录 -->
|
||||
<view class="recent-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">最近记录</text>
|
||||
<text class="section-more" @tap="navigateTo('/pages/account/bill/index')">查看更多 ></text>
|
||||
<view class="section-more" @tap="navigateTo('/pages/account/bill/index')">
|
||||
<text>全部</text>
|
||||
<uni-icons type="right" size="14" color="#667eea"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-list">
|
||||
<view class="empty-icon">
|
||||
<uni-icons type="list" size="80" color="#ddd"></uni-icons>
|
||||
</view>
|
||||
<text class="empty-text">暂无记录</text>
|
||||
<button class="quick-add-btn" @tap="navigateTo('/pages/account/bill/add')">
|
||||
<uni-icons type="plus" size="16" color="#fff"></uni-icons>
|
||||
<text>记一笔</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 家庭管理 -->
|
||||
<view class="family-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">家庭管理</text>
|
||||
<view class="section-more" @tap="navigateTo('/pages/family/index')">
|
||||
<text>管理</text>
|
||||
<uni-icons type="right" size="14" color="#667eea"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view class="family-card" @tap="navigateTo('/pages/family/index')">
|
||||
<view class="family-icon-wrapper">
|
||||
<text class="family-icon-text">🏠</text>
|
||||
</view>
|
||||
<view class="family-info">
|
||||
<text class="family-status">{{ hasFamily ? '已加入家庭' : '未加入家庭' }}</text>
|
||||
<text class="family-desc">{{ hasFamily ? '点击查看家庭成员' : '点击创建或加入家庭' }}</text>
|
||||
</view>
|
||||
<view class="family-arrow">
|
||||
<uni-icons type="right" size="18" color="#ccc"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -39,16 +121,49 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
currentDate: '',
|
||||
currentMonth: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasFamily() {
|
||||
if (!this.$store || !this.$store.getters) {
|
||||
return false
|
||||
}
|
||||
return this.$store.getters['family/hasFamily'] || false
|
||||
},
|
||||
userName() {
|
||||
if (!this.$store || !this.$store.state || !this.$store.state.user || !this.$store.state.user.userInfo) {
|
||||
return ''
|
||||
}
|
||||
return this.$store.state.user.userInfo.name || this.$store.state.user.userInfo.username || ''
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.updateDate()
|
||||
},
|
||||
onShow() {
|
||||
this.updateDate()
|
||||
},
|
||||
methods: {
|
||||
updateDate() {
|
||||
const now = new Date()
|
||||
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||
const month = now.getMonth() + 1
|
||||
const date = now.getDate()
|
||||
const day = weekDays[now.getDay()]
|
||||
|
||||
this.currentDate = `${month}月${date}日 ${day}`
|
||||
this.currentMonth = `${month}月`
|
||||
},
|
||||
handleTabChange(path) {
|
||||
console.log('Tab changed to:', path)
|
||||
// tabbar组件已经处理了跳转,这里不需要额外处理
|
||||
},
|
||||
navigateTo(url) {
|
||||
uni.showToast({
|
||||
title: '功能开发中',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
uni.navigateTo({
|
||||
url: url
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -57,95 +172,512 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
padding: 40rpx 30rpx;
|
||||
padding: 30rpx;
|
||||
padding-bottom: 150rpx;
|
||||
background: linear-gradient(180deg, #F5F7FA 0%, #FFFFFF 100%);
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -100rpx;
|
||||
right: -100rpx;
|
||||
width: 300rpx;
|
||||
height: 300rpx;
|
||||
background: radial-gradient(circle, rgba(102, 126, 234, 0.1) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 200rpx;
|
||||
left: -100rpx;
|
||||
width: 250rpx;
|
||||
height: 250rpx;
|
||||
background: radial-gradient(circle, rgba(118, 75, 162, 0.08) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
> view {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-section {
|
||||
.user-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 60rpx 0;
|
||||
padding: 20rpx 0 40rpx;
|
||||
animation: fadeInDown 0.6s ease-out;
|
||||
|
||||
.logo {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
margin-bottom: 30rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 20rpx;
|
||||
.user-avatar {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 28rpx;
|
||||
box-shadow: 0 8rpx 20rpx rgba(102, 126, 234, 0.35);
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4rpx;
|
||||
right: -4rpx;
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
background: #4cd964;
|
||||
border-radius: 50%;
|
||||
border: 3rpx solid #fff;
|
||||
}
|
||||
|
||||
text {
|
||||
font-size: 36rpx;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 44rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
.user-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.greeting {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.date-text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 24rpx;
|
||||
padding: 40rpx 32rpx;
|
||||
margin-bottom: 40rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.4);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
animation: fadeInUp 0.6s ease-out 0.1s both;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
.welcome-desc {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -30%;
|
||||
left: -30%;
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.05) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
.overview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.overview-title {
|
||||
font-size: 30rpx;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.overview-month {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 24rpx;
|
||||
backdrop-filter: blur(10rpx);
|
||||
}
|
||||
}
|
||||
|
||||
.overview-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.overview-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&.income {
|
||||
.overview-icon {
|
||||
background: rgba(46, 204, 113, 0.25);
|
||||
box-shadow: 0 4rpx 12rpx rgba(46, 204, 113, 0.3);
|
||||
}
|
||||
|
||||
.overview-amount {
|
||||
background: linear-gradient(135deg, #98D8C8 0%, #7FDBDA 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
}
|
||||
|
||||
&.expense {
|
||||
.overview-icon {
|
||||
background: rgba(255, 107, 107, 0.25);
|
||||
box-shadow: 0 4rpx 12rpx rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
|
||||
.overview-amount {
|
||||
background: linear-gradient(135deg, #FFEAA7 0%, #FD79A8 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
}
|
||||
|
||||
.overview-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
.icon-text {
|
||||
font-size: 26rpx;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.overview-data {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.overview-label {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 8rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.overview-amount {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overview-divider {
|
||||
width: 2rpx;
|
||||
height: 72rpx;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
margin: 0 32rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 60rpx 0;
|
||||
margin-bottom: 40rpx;
|
||||
animation: fadeInUp 0.6s ease-out 0.2s both;
|
||||
|
||||
.action-card {
|
||||
flex: 1;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 20rpx;
|
||||
padding: 40rpx 20rpx;
|
||||
margin: 0 10rpx;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 40rpx 16rpx;
|
||||
margin: 0 8rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:last-child {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4rpx;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(-4rpx) scale(0.98);
|
||||
box-shadow: 0 12rpx 28rpx rgba(0, 0, 0, 0.12);
|
||||
|
||||
.action-icon-wrapper {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(1) {
|
||||
.action-icon-wrapper {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
&::before {
|
||||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
.action-icon-wrapper {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
|
||||
&::before {
|
||||
background: linear-gradient(90deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
.action-icon-wrapper {
|
||||
background: linear-gradient(135deg, #4ECDC4 0%, #44A08D 100%);
|
||||
}
|
||||
|
||||
&::before {
|
||||
background: linear-gradient(90deg, #4ECDC4 0%, #44A08D 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.action-icon-wrapper {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.15);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.action-title {
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
margin-top: 20rpx;
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.action-desc {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recent-section,
|
||||
.family-section {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||
animation: fadeInUp 0.6s ease-out both;
|
||||
}
|
||||
|
||||
.recent-section {
|
||||
margin-top: 40rpx;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
.family-section {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
.section-more {
|
||||
font-size: 26rpx;
|
||||
color: #667eea;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(135deg, #333 0%, #666 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.empty-list {
|
||||
background: #fff;
|
||||
.section-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 26rpx;
|
||||
color: #667eea;
|
||||
font-weight: 500;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
padding: 80rpx 40rpx;
|
||||
text-align: center;
|
||||
background: rgba(102, 126, 234, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
&:active {
|
||||
background: rgba(102, 126, 234, 0.15);
|
||||
}
|
||||
|
||||
text {
|
||||
margin-right: 6rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 60rpx 0;
|
||||
|
||||
.empty-icon {
|
||||
margin-bottom: 20rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.quick-add-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 50rpx;
|
||||
padding: 16rpx 36rpx;
|
||||
font-size: 26rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 6rpx 16rpx rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
uni-icons {
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.family-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12rpx 0;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: translateX(4rpx);
|
||||
}
|
||||
|
||||
.family-icon-wrapper {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
box-shadow: 0 6rpx 16rpx rgba(102, 126, 234, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.family-icon-text {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.family-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.family-status {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.family-desc {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.family-arrow {
|
||||
padding: 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>
|
||||
|
||||
@@ -9,43 +9,135 @@
|
||||
<view class="page-content">
|
||||
<!-- 用户信息头部 -->
|
||||
<view class="user-header">
|
||||
<view class="header-bg"></view>
|
||||
<view class="user-avatar">
|
||||
<uni-icons type="person-filled" size="60" color="#fff"></uni-icons>
|
||||
<uni-icons type="person-filled" size="70" color="#fff"></uni-icons>
|
||||
<view class="vip-badge" v-if="isLogin">
|
||||
<text>VIP</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="user-info">
|
||||
<text class="user-name">{{ userInfo.nickname || '未登录' }}</text>
|
||||
<text class="user-desc">{{ userInfo.username || '点击登录' }}</text>
|
||||
<text class="user-name">{{ userInfo.nickname || userInfo.username || '未登录' }}</text>
|
||||
<text class="user-desc" v-if="isLogin">{{ userInfo.email || '暂无邮箱' }}</text>
|
||||
<text class="user-desc" v-else>点击登录体验更多功能</text>
|
||||
</view>
|
||||
<view class="edit-profile" @tap="handleEditProfile" v-if="isLogin">
|
||||
<uni-icons type="compose" size="18" color="rgba(255,255,255,0.8)"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数据统计卡片 -->
|
||||
<view class="stats-section" v-if="isLogin">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ stats.billCount || 0 }}</text>
|
||||
<text class="stat-label">账单数</text>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ stats.days || 0 }}</text>
|
||||
<text class="stat-label">记账天数</text>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ stats.familyMembers || 0 }}</text>
|
||||
<text class="stat-label">家庭成员</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷功能 -->
|
||||
<view class="quick-section">
|
||||
<view class="section-title">快捷功能</view>
|
||||
<view class="quick-grid">
|
||||
<view class="quick-item" @tap="navigateTo('/pages/account/bill/add')">
|
||||
<view class="quick-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<uni-icons type="plus" size="24" color="#fff"></uni-icons>
|
||||
</view>
|
||||
<text class="quick-text">记一笔</text>
|
||||
</view>
|
||||
<view class="quick-item" @tap="navigateTo('/pages/family/index')">
|
||||
<view class="quick-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
|
||||
<text class="emoji-icon">👥</text>
|
||||
</view>
|
||||
<text class="quick-text">家庭管理</text>
|
||||
</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>
|
||||
</view>
|
||||
<text class="quick-text">统计分析</text>
|
||||
</view>
|
||||
<view class="quick-item" @tap="handleExport">
|
||||
<view class="quick-icon" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);">
|
||||
<uni-icons type="download" size="24" color="#fff"></uni-icons>
|
||||
</view>
|
||||
<text class="quick-text">数据导出</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能菜单 -->
|
||||
<view class="menu-section">
|
||||
<view class="menu-item" @tap="navigateTo('/pages/ucenter/profile')">
|
||||
<view class="menu-left">
|
||||
<uni-icons type="gear" size="20" color="#667eea"></uni-icons>
|
||||
<text class="menu-text">个人设置</text>
|
||||
<view class="section-title">设置</view>
|
||||
<view class="menu-list">
|
||||
<view class="menu-item" @tap="navigateTo('/pages/ucenter/profile')">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon-wrapper">
|
||||
<uni-icons type="gear" size="20" color="#667eea"></uni-icons>
|
||||
</view>
|
||||
<view class="menu-content">
|
||||
<text class="menu-text">个人设置</text>
|
||||
<text class="menu-desc">修改个人信息</text>
|
||||
</view>
|
||||
</view>
|
||||
<uni-icons type="right" size="16" color="#ccc"></uni-icons>
|
||||
</view>
|
||||
<uni-icons type="right" size="16" color="#ccc"></uni-icons>
|
||||
</view>
|
||||
<view class="menu-item" @tap="navigateTo('/pages/ucenter/help')">
|
||||
<view class="menu-left">
|
||||
<uni-icons type="help" size="20" color="#667eea"></uni-icons>
|
||||
<text class="menu-text">帮助中心</text>
|
||||
<view class="menu-item" @tap="navigateTo('/pages/ucenter/help')">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon-wrapper">
|
||||
<uni-icons type="help" size="20" color="#667eea"></uni-icons>
|
||||
</view>
|
||||
<view class="menu-content">
|
||||
<text class="menu-text">帮助中心</text>
|
||||
<text class="menu-desc">常见问题解答</text>
|
||||
</view>
|
||||
</view>
|
||||
<uni-icons type="right" size="16" color="#ccc"></uni-icons>
|
||||
</view>
|
||||
<uni-icons type="right" size="16" color="#ccc"></uni-icons>
|
||||
</view>
|
||||
<view class="menu-item" @tap="navigateTo('/pages/ucenter/about')">
|
||||
<view class="menu-left">
|
||||
<uni-icons type="info" size="20" color="#667eea"></uni-icons>
|
||||
<text class="menu-text">关于我们</text>
|
||||
<view class="menu-item" @tap="navigateTo('/pages/ucenter/about')">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon-wrapper">
|
||||
<uni-icons type="info" size="20" color="#667eea"></uni-icons>
|
||||
</view>
|
||||
<view class="menu-content">
|
||||
<text class="menu-text">关于我们</text>
|
||||
<text class="menu-desc">版本信息</text>
|
||||
</view>
|
||||
</view>
|
||||
<uni-icons type="right" size="16" color="#ccc"></uni-icons>
|
||||
</view>
|
||||
<view class="menu-item" @tap="handleClearCache">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon-wrapper">
|
||||
<uni-icons type="trash" size="20" color="#667eea"></uni-icons>
|
||||
</view>
|
||||
<view class="menu-content">
|
||||
<text class="menu-text">清除缓存</text>
|
||||
<text class="menu-desc">释放存储空间</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="cache-size">{{ cacheSize }}</text>
|
||||
</view>
|
||||
<uni-icons type="right" size="16" color="#ccc"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 退出登录按钮 -->
|
||||
<view class="logout-section" v-if="isLogin">
|
||||
<button class="logout-btn" @tap="handleLogout">退出登录</button>
|
||||
<!-- 登录/退出按钮 -->
|
||||
<view class="auth-section">
|
||||
<button class="auth-btn login-btn" @tap="handleLogin" v-if="!isLogin">
|
||||
立即登录
|
||||
</button>
|
||||
<button class="auth-btn logout-btn" @tap="handleLogout" v-else>
|
||||
退出登录
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</un-pages>
|
||||
@@ -58,11 +150,18 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
userInfo: {},
|
||||
isLogin: false
|
||||
isLogin: false,
|
||||
stats: {
|
||||
billCount: 0,
|
||||
days: 0,
|
||||
familyMembers: 0
|
||||
},
|
||||
cacheSize: '0 MB'
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.checkLoginStatus()
|
||||
this.calculateCacheSize()
|
||||
},
|
||||
onShow() {
|
||||
this.checkLoginStatus()
|
||||
@@ -72,10 +171,58 @@ export default {
|
||||
checkLoginStatus() {
|
||||
this.isLogin = isLogin()
|
||||
this.userInfo = uni.getStorageSync('userInfo') || {}
|
||||
|
||||
if (this.isLogin) {
|
||||
this.loadUserStats()
|
||||
}
|
||||
},
|
||||
|
||||
// 加载用户统计数据
|
||||
loadUserStats() {
|
||||
// 这里应该调用API获取实际数据
|
||||
// 暂时使用模拟数据
|
||||
this.stats = {
|
||||
billCount: 128,
|
||||
days: 45,
|
||||
familyMembers: 3
|
||||
}
|
||||
},
|
||||
|
||||
// 计算缓存大小
|
||||
calculateCacheSize() {
|
||||
try {
|
||||
const info = uni.getStorageInfoSync()
|
||||
const size = (info.currentSize / 1024).toFixed(2)
|
||||
this.cacheSize = `${size} MB`
|
||||
} catch (e) {
|
||||
this.cacheSize = '0 MB'
|
||||
}
|
||||
},
|
||||
|
||||
// 页面跳转
|
||||
navigateTo(url) {
|
||||
if (!this.isLogin && url !== '/pages/ucenter/login/index') {
|
||||
uni.navigateTo({
|
||||
url: '/pages/ucenter/login/index'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 功能开发中提示
|
||||
uni.showToast({
|
||||
title: '功能开发中',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
},
|
||||
|
||||
// 编辑个人资料
|
||||
handleEditProfile() {
|
||||
this.navigateTo('/pages/ucenter/profile')
|
||||
},
|
||||
|
||||
// 数据导出
|
||||
handleExport() {
|
||||
if (!this.isLogin) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/ucenter/login/index'
|
||||
@@ -90,6 +237,38 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
// 清除缓存
|
||||
handleClearCache() {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要清除缓存吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
uni.clearStorageSync()
|
||||
this.calculateCacheSize()
|
||||
uni.showToast({
|
||||
title: '缓存已清除',
|
||||
icon: 'success'
|
||||
})
|
||||
} catch (e) {
|
||||
uni.showToast({
|
||||
title: '清除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 登录
|
||||
handleLogin() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/ucenter/login/index'
|
||||
})
|
||||
},
|
||||
|
||||
// 退出登录
|
||||
handleLogout() {
|
||||
uni.showModal({
|
||||
@@ -98,6 +277,7 @@ export default {
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
logout()
|
||||
this.checkLoginStatus()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -113,92 +293,365 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
padding: 40rpx 30rpx;
|
||||
padding: 30rpx;
|
||||
padding-bottom: 150rpx;
|
||||
background: linear-gradient(180deg, #F5F7FA 0%, #FFFFFF 100%);
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50rpx;
|
||||
right: -80rpx;
|
||||
width: 280rpx;
|
||||
height: 280rpx;
|
||||
background: radial-gradient(circle, rgba(102, 126, 234, 0.08) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
> view {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.user-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 20rpx;
|
||||
border-radius: 24rpx;
|
||||
padding: 60rpx 40rpx;
|
||||
margin: 40rpx 0;
|
||||
margin: 20rpx 0 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.35);
|
||||
animation: fadeInDown 0.6s ease-out;
|
||||
|
||||
.header-bg {
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-radius: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 30rpx;
|
||||
margin-right: 28rpx;
|
||||
position: relative;
|
||||
backdrop-filter: blur(10rpx);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.user-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 16rpx;
|
||||
margin-bottom: 12rpx;
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.user-desc {
|
||||
font-size: 28rpx;
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-profile {
|
||||
padding: 16rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-section {
|
||||
.stats-section {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
border-radius: 24rpx;
|
||||
padding: 40rpx 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||
animation: fadeInUp 0.6s ease-out 0.1s both;
|
||||
|
||||
.menu-item {
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx 40rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.menu-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.stat-value {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
font-size: 32rpx;
|
||||
.stat-label {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
width: 2rpx;
|
||||
height: 60rpx;
|
||||
background: #f0f0f0;
|
||||
margin: 0 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-section {
|
||||
margin-bottom: 30rpx;
|
||||
animation: fadeInUp 0.6s ease-out 0.2s both;
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
padding-left: 8rpx;
|
||||
}
|
||||
|
||||
.quick-grid {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 30rpx;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20rpx;
|
||||
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||
|
||||
.quick-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: translateY(-4rpx);
|
||||
}
|
||||
|
||||
.quick-icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.12);
|
||||
|
||||
.emoji-icon {
|
||||
font-size: 44rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-text {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
margin-left: 20rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logout-section {
|
||||
margin-top: 60rpx;
|
||||
.menu-section {
|
||||
margin-bottom: 30rpx;
|
||||
animation: fadeInUp 0.6s ease-out 0.3s both;
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
height: 90rpx;
|
||||
line-height: 90rpx;
|
||||
border-radius: 45rpx;
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
padding-left: 8rpx;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
background: #fff;
|
||||
color: #f56c6c;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||
|
||||
.menu-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;
|
||||
}
|
||||
|
||||
.menu-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
|
||||
.menu-icon-wrapper {
|
||||
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;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.menu-text {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.menu-desc {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cache-size {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.auth-section {
|
||||
margin-top: 40rpx;
|
||||
padding: 0 20rpx;
|
||||
animation: fadeInUp 0.6s ease-out 0.4s both;
|
||||
|
||||
.auth-btn {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
border-radius: 48rpx;
|
||||
font-size: 32rpx;
|
||||
border: 2rpx solid #f56c6c;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
background: #fff;
|
||||
color: #f56c6c;
|
||||
border: 2rpx solid #f56c6c;
|
||||
box-shadow: 0 6rpx 20rpx rgba(245, 108, 108, 0.15);
|
||||
|
||||
&:active {
|
||||
background: #fef0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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>
|
||||
|
||||
95
resources/mobile/store/modules/family.js
Normal file
95
resources/mobile/store/modules/family.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import tool from '../../utils/tool'
|
||||
import api from '@/api'
|
||||
|
||||
export default{
|
||||
state: {
|
||||
familyId: tool.data.get('familyId') || '',
|
||||
familyInfo: tool.data.get('familyInfo') || {},
|
||||
isOwner: tool.data.get('isOwner') || false
|
||||
},
|
||||
mutations:{
|
||||
setFamilyInfo(state, data){
|
||||
if(data && data.id){
|
||||
tool.data.set('familyId', data.id)
|
||||
state.familyId = data.id
|
||||
tool.data.set('familyInfo', data)
|
||||
state.familyInfo = data
|
||||
}else{
|
||||
tool.data.set('familyId', '')
|
||||
state.familyId = ''
|
||||
tool.data.set('familyInfo', {})
|
||||
state.familyInfo = {}
|
||||
}
|
||||
},
|
||||
setIsOwner(state, isOwner){
|
||||
tool.data.set('isOwner', isOwner)
|
||||
state.isOwner = isOwner
|
||||
},
|
||||
clearFamily(state){
|
||||
tool.data.set('familyId', '')
|
||||
state.familyId = ''
|
||||
tool.data.set('familyInfo', {})
|
||||
state.familyInfo = {}
|
||||
tool.data.set('isOwner', false)
|
||||
state.isOwner = false
|
||||
}
|
||||
},
|
||||
getters:{
|
||||
hasFamily(state){
|
||||
return !!state.familyId
|
||||
}
|
||||
},
|
||||
actions:{
|
||||
async getFamilyInfo({commit}){
|
||||
try {
|
||||
const res = await api.family.info.get()
|
||||
if(res.data){
|
||||
commit('setFamilyInfo', res.data)
|
||||
commit('setIsOwner', res.data.is_owner || false)
|
||||
}
|
||||
return res
|
||||
}catch(e){
|
||||
console.error('获取家庭信息失败', e)
|
||||
throw e
|
||||
}
|
||||
},
|
||||
async createFamily({commit}, familyName){
|
||||
try {
|
||||
const res = await api.family.create.post({name: familyName})
|
||||
if(res.data){
|
||||
commit('setFamilyInfo', res.data)
|
||||
commit('setIsOwner', true)
|
||||
}
|
||||
return res
|
||||
}catch(e){
|
||||
console.error('创建家庭失败', e)
|
||||
throw e
|
||||
}
|
||||
},
|
||||
async joinFamily({commit}, inviteCode){
|
||||
try {
|
||||
const res = await api.family.join.post({invite_code: inviteCode})
|
||||
if(res.data){
|
||||
commit('setFamilyInfo', res.data)
|
||||
commit('setIsOwner', res.data.is_owner || false)
|
||||
}
|
||||
return res
|
||||
}catch(e){
|
||||
console.error('加入家庭失败', e)
|
||||
throw e
|
||||
}
|
||||
},
|
||||
async leaveFamily({commit}){
|
||||
try {
|
||||
const res = await api.family.leave.post()
|
||||
if(res.code === 200){
|
||||
commit('clearFamily')
|
||||
}
|
||||
return res
|
||||
}catch(e){
|
||||
console.error('退出家庭失败', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user