更新
This commit is contained in:
95
modules/Account/app/Controllers/Api/BillController.php
Normal file
95
modules/Account/app/Controllers/Api/BillController.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Account\Controllers\Api;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\BaseController;
|
||||
use Modules\Account\Services\BillService;
|
||||
|
||||
class BillController extends BaseController
|
||||
{
|
||||
protected $billService;
|
||||
|
||||
public function __construct(BillService $billService)
|
||||
{
|
||||
$this->billService = $billService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账单列表
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->data['data'] = $this->billService->getList($request);
|
||||
} catch (\Throwable $th) {
|
||||
$this->data['code'] = 0;
|
||||
$this->data['message'] = $th->getMessage();
|
||||
}
|
||||
|
||||
return response()->json($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加账单
|
||||
*/
|
||||
public function add(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->data['data'] = $this->billService->add($request);
|
||||
$this->data['message'] = '添加成功';
|
||||
} catch (\Throwable $th) {
|
||||
$this->data['code'] = 0;
|
||||
$this->data['message'] = $th->getMessage();
|
||||
}
|
||||
|
||||
return response()->json($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑账单
|
||||
*/
|
||||
public function edit(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->data['data'] = $this->billService->edit($request);
|
||||
$this->data['message'] = '编辑成功';
|
||||
} catch (\Throwable $th) {
|
||||
$this->data['code'] = 0;
|
||||
$this->data['message'] = $th->getMessage();
|
||||
}
|
||||
|
||||
return response()->json($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除账单
|
||||
*/
|
||||
public function delete(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->data['data'] = $this->billService->delete($request);
|
||||
$this->data['message'] = '删除成功';
|
||||
} catch (\Throwable $th) {
|
||||
$this->data['code'] = 0;
|
||||
$this->data['message'] = $th->getMessage();
|
||||
}
|
||||
|
||||
return response()->json($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账单详情
|
||||
*/
|
||||
public function detail(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->data['data'] = $this->billService->detail($request);
|
||||
} catch (\Throwable $th) {
|
||||
$this->data['code'] = 0;
|
||||
$this->data['message'] = $th->getMessage();
|
||||
}
|
||||
|
||||
return response()->json($this->data);
|
||||
}
|
||||
}
|
||||
155
modules/Account/app/Controllers/Api/FamilyController.php
Normal file
155
modules/Account/app/Controllers/Api/FamilyController.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Account\Controllers\Api;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\BaseController;
|
||||
use Modules\Account\Services\FamilyService;
|
||||
|
||||
class FamilyController extends BaseController
|
||||
{
|
||||
protected $familyService;
|
||||
|
||||
public function __construct(FamilyService $familyService)
|
||||
{
|
||||
$this->familyService = $familyService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取家庭信息
|
||||
*/
|
||||
public function info(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->data['data'] = $this->familyService->getInfo($request);
|
||||
} catch (\Throwable $th) {
|
||||
$this->data['code'] = 0;
|
||||
$this->data['message'] = $th->getMessage();
|
||||
}
|
||||
|
||||
return response()->json($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建家庭
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->data['data'] = $this->familyService->create($request);
|
||||
$this->data['message'] = '创建成功';
|
||||
} catch (\Throwable $th) {
|
||||
$this->data['code'] = 0;
|
||||
$this->data['message'] = $th->getMessage();
|
||||
}
|
||||
|
||||
return response()->json($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入家庭
|
||||
*/
|
||||
public function join(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->data['data'] = $this->familyService->join($request);
|
||||
$this->data['message'] = '加入成功';
|
||||
} catch (\Throwable $th) {
|
||||
$this->data['code'] = 0;
|
||||
$this->data['message'] = $th->getMessage();
|
||||
}
|
||||
|
||||
return response()->json($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出家庭
|
||||
*/
|
||||
public function leave(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->data['data'] = $this->familyService->leave($request);
|
||||
} catch (\Throwable $th) {
|
||||
$this->data['code'] = 0;
|
||||
$this->data['message'] = $th->getMessage();
|
||||
}
|
||||
|
||||
return response()->json($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取家庭邀请码
|
||||
*/
|
||||
public function inviteCode(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->data['data'] = $this->familyService->getInviteCode($request);
|
||||
} catch (\Throwable $th) {
|
||||
$this->data['code'] = 0;
|
||||
$this->data['message'] = $th->getMessage();
|
||||
}
|
||||
|
||||
return response()->json($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新生成邀请码
|
||||
*/
|
||||
public function regenerateInviteCode(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->data['data'] = $this->familyService->regenerateInviteCode($request);
|
||||
$this->data['message'] = '邀请码已重新生成';
|
||||
} catch (\Throwable $th) {
|
||||
$this->data['code'] = 0;
|
||||
$this->data['message'] = $th->getMessage();
|
||||
}
|
||||
|
||||
return response()->json($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除家庭成员
|
||||
*/
|
||||
public function removeMember(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->data['data'] = $this->familyService->removeMember($request);
|
||||
} catch (\Throwable $th) {
|
||||
$this->data['code'] = 0;
|
||||
$this->data['message'] = $th->getMessage();
|
||||
}
|
||||
|
||||
return response()->json($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取家庭成员列表
|
||||
*/
|
||||
public function members(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->data['data'] = $this->familyService->getMembers($request);
|
||||
} catch (\Throwable $th) {
|
||||
$this->data['code'] = 0;
|
||||
$this->data['message'] = $th->getMessage();
|
||||
}
|
||||
|
||||
return response()->json($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转让家主
|
||||
*/
|
||||
public function transferOwner(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->data['data'] = $this->familyService->transferOwner($request);
|
||||
} catch (\Throwable $th) {
|
||||
$this->data['code'] = 0;
|
||||
$this->data['message'] = $th->getMessage();
|
||||
}
|
||||
|
||||
return response()->json($this->data);
|
||||
}
|
||||
}
|
||||
92
modules/Account/app/Controllers/Api/StatisticsController.php
Normal file
92
modules/Account/app/Controllers/Api/StatisticsController.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Account\Controllers\Api;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\BaseController;
|
||||
use Modules\Account\Services\StatisticsService;
|
||||
|
||||
class StatisticsController extends BaseController
|
||||
{
|
||||
protected $statisticsService;
|
||||
|
||||
public function __construct(StatisticsService $statisticsService)
|
||||
{
|
||||
$this->statisticsService = $statisticsService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计概览
|
||||
*/
|
||||
public function overview(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->data['data'] = $this->statisticsService->getOverview($request);
|
||||
} catch (\Throwable $th) {
|
||||
$this->data['code'] = 0;
|
||||
$this->data['message'] = $th->getMessage();
|
||||
}
|
||||
|
||||
return response()->json($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取收支趋势
|
||||
*/
|
||||
public function trend(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->data['data'] = $this->statisticsService->getTrend($request);
|
||||
} catch (\Throwable $th) {
|
||||
$this->data['code'] = 0;
|
||||
$this->data['message'] = $th->getMessage();
|
||||
}
|
||||
|
||||
return response()->json($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分类统计
|
||||
*/
|
||||
public function category(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->data['data'] = $this->statisticsService->getCategory($request);
|
||||
} catch (\Throwable $th) {
|
||||
$this->data['code'] = 0;
|
||||
$this->data['message'] = $th->getMessage();
|
||||
}
|
||||
|
||||
return response()->json($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取月度报表
|
||||
*/
|
||||
public function monthly(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->data['data'] = $this->statisticsService->getMonthly($request);
|
||||
} catch (\Throwable $th) {
|
||||
$this->data['code'] = 0;
|
||||
$this->data['message'] = $th->getMessage();
|
||||
}
|
||||
|
||||
return response()->json($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取年度报表
|
||||
*/
|
||||
public function yearly(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->data['data'] = $this->statisticsService->getYearly($request);
|
||||
} catch (\Throwable $th) {
|
||||
$this->data['code'] = 0;
|
||||
$this->data['message'] = $th->getMessage();
|
||||
}
|
||||
|
||||
return response()->json($this->data);
|
||||
}
|
||||
}
|
||||
50
modules/Account/app/Models/Bill.php
Normal file
50
modules/Account/app/Models/Bill.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Account\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
|
||||
class Bill extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'account_bills';
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'family_id',
|
||||
'type',
|
||||
'amount',
|
||||
'category',
|
||||
'remark',
|
||||
'bill_date'
|
||||
];
|
||||
protected $dateFormat = 'Y-m-d H:i:s';
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'bill_date' => 'date',
|
||||
'created_at' => 'datetime:Y-m-d H:i:s',
|
||||
'updated_at' => 'datetime:Y-m-d H:i:s',
|
||||
'deleted_at' => 'datetime:Y-m-d H:i:s',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 账单所属用户
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(\Modules\Member\Models\Member::class, 'user_id', 'uid');
|
||||
}
|
||||
|
||||
/**
|
||||
* 账单所属家庭
|
||||
*/
|
||||
public function family()
|
||||
{
|
||||
return $this->belongsTo(Family::class);
|
||||
}
|
||||
}
|
||||
65
modules/Account/app/Models/Family.php
Normal file
65
modules/Account/app/Models/Family.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Account\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Family extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'account_families';
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'owner_id',
|
||||
'invite_code'
|
||||
];
|
||||
protected $dateFormat = 'Y-m-d H:i:s';
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'created_at' => 'datetime:Y-m-d H:i:s',
|
||||
'updated_at' => 'datetime:Y-m-d H:i:s',
|
||||
'deleted_at' => 'datetime:Y-m-d H:i:s',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 家主
|
||||
*/
|
||||
public function owner()
|
||||
{
|
||||
return $this->belongsTo(\Modules\Member\Models\Member::class, 'owner_id', 'uid');
|
||||
}
|
||||
|
||||
/**
|
||||
* 家庭成员
|
||||
*/
|
||||
public function members()
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
\Modules\Member\Models\Member::class,
|
||||
'account_family_members',
|
||||
'family_id',
|
||||
'user_id'
|
||||
)->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* 家庭成员关系
|
||||
*/
|
||||
public function familyMembers()
|
||||
{
|
||||
return $this->hasMany(FamilyMember::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 账单
|
||||
*/
|
||||
public function bills()
|
||||
{
|
||||
return $this->hasMany(Bill::class);
|
||||
}
|
||||
}
|
||||
39
modules/Account/app/Models/FamilyMember.php
Normal file
39
modules/Account/app/Models/FamilyMember.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Account\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class FamilyMember extends Model
|
||||
{
|
||||
protected $table = 'account_family_members';
|
||||
protected $fillable = [
|
||||
'family_id',
|
||||
'user_id'
|
||||
];
|
||||
protected $dateFormat = 'Y-m-d H:i:s';
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'created_at' => 'datetime:Y-m-d H:i:s',
|
||||
'updated_at' => 'datetime:Y-m-d H:i:s',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 所属家庭
|
||||
*/
|
||||
public function family()
|
||||
{
|
||||
return $this->belongsTo(Family::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成员用户
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(\Modules\Member\Models\Member::class, 'user_id', 'uid');
|
||||
}
|
||||
}
|
||||
314
modules/Account/app/Services/BillService.php
Normal file
314
modules/Account/app/Services/BillService.php
Normal file
@@ -0,0 +1,314 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Account\Services;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\Account\Models\Bill;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class BillService
|
||||
{
|
||||
/**
|
||||
* 获取账单列表
|
||||
*/
|
||||
public function getList(Request $request)
|
||||
{
|
||||
$userId = auth('api')->user()['uid'];
|
||||
$page = $request->input('page', 1);
|
||||
$limit = $request->input('limit', 20);
|
||||
$type = $request->input('type');
|
||||
$year = $request->input('year');
|
||||
$month = $request->input('month');
|
||||
$startDate = $request->input('start_date');
|
||||
$endDate = $request->input('end_date');
|
||||
$familyId = $request->input('family_id');
|
||||
|
||||
// 获取用户所在的家庭ID
|
||||
$userFamilyId = $this->getUserFamilyId($userId);
|
||||
|
||||
$query = Bill::with(['user:uid,nickname,username', 'family:id,name']);
|
||||
|
||||
// 如果用户加入了家庭,且没有指定family_id,则显示家庭账单
|
||||
if ($userFamilyId && !$familyId) {
|
||||
$query->where('family_id', $userFamilyId);
|
||||
} elseif ($familyId) {
|
||||
// 如果指定了family_id,则显示该家庭的账单
|
||||
$query->where('family_id', $familyId);
|
||||
} else {
|
||||
// 否则显示个人账单
|
||||
$query->where('user_id', $userId);
|
||||
}
|
||||
|
||||
// 年月筛选
|
||||
if ($year && $month) {
|
||||
$startDate = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT) . '-01';
|
||||
$endDate = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT) . '-31';
|
||||
}
|
||||
|
||||
// 日期范围筛选
|
||||
if ($startDate) {
|
||||
$query->where('bill_date', '>=', $startDate);
|
||||
}
|
||||
if ($endDate) {
|
||||
$query->where('bill_date', '<=', $endDate);
|
||||
}
|
||||
|
||||
// 类型筛选
|
||||
if ($type && in_array($type, ['income', 'expense'])) {
|
||||
$query->where('type', $type);
|
||||
}
|
||||
|
||||
$bills = $query->orderBy('bill_date', 'desc')
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate($limit, ['*'], 'page', $page);
|
||||
|
||||
// 计算本月收支
|
||||
$monthExpense = Bill::query();
|
||||
$monthIncome = Bill::query();
|
||||
|
||||
// 同样的家庭查询逻辑
|
||||
if ($userFamilyId && !$familyId) {
|
||||
$monthExpense->where('family_id', $userFamilyId);
|
||||
$monthIncome->where('family_id', $userFamilyId);
|
||||
} elseif ($familyId) {
|
||||
$monthExpense->where('family_id', $familyId);
|
||||
$monthIncome->where('family_id', $familyId);
|
||||
} else {
|
||||
$monthExpense->where('user_id', $userId);
|
||||
$monthIncome->where('user_id', $userId);
|
||||
}
|
||||
|
||||
if ($startDate) {
|
||||
$monthExpense->where('bill_date', '>=', $startDate);
|
||||
$monthIncome->where('bill_date', '>=', $startDate);
|
||||
}
|
||||
if ($endDate) {
|
||||
$monthExpense->where('bill_date', '<=', $endDate);
|
||||
$monthIncome->where('bill_date', '<=', $endDate);
|
||||
}
|
||||
|
||||
$monthExpense->where('type', 'expense');
|
||||
$monthIncome->where('type', 'income');
|
||||
|
||||
// 转换数据格式以匹配前端
|
||||
$list = collect($bills->items())->map(function($bill) {
|
||||
return [
|
||||
'id' => $bill->id,
|
||||
'type' => $bill->type,
|
||||
'amount' => (float)$bill->amount,
|
||||
'category' => $bill->category,
|
||||
'category_id' => $this->getCategoryId($bill->category, $bill->type),
|
||||
'remark' => $bill->remark,
|
||||
'date' => $bill->bill_date,
|
||||
'bill_date' => $bill->bill_date,
|
||||
'created_at' => $bill->created_at->format('Y-m-d H:i:s'),
|
||||
'user' => $bill->user,
|
||||
'family' => $bill->family
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
return [
|
||||
'list' => $list,
|
||||
'month_expense' => (float)$monthExpense->sum('amount'),
|
||||
'month_income' => (float)$monthIncome->sum('amount'),
|
||||
'pagination' => [
|
||||
'current_page' => $bills->currentPage(),
|
||||
'total' => $bills->total(),
|
||||
'per_page' => $bills->perPage(),
|
||||
'last_page' => $bills->lastPage()
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分类名称获取分类ID(前端使用)
|
||||
*/
|
||||
private function getCategoryId($categoryName, $type)
|
||||
{
|
||||
// 支出分类映射
|
||||
$expenseMap = [
|
||||
'餐饮' => 1,
|
||||
'交通' => 2,
|
||||
'购物' => 3,
|
||||
'娱乐' => 4,
|
||||
'医疗' => 5,
|
||||
'教育' => 6,
|
||||
'居住' => 7,
|
||||
'其他' => 8
|
||||
];
|
||||
|
||||
// 收入分类映射
|
||||
$incomeMap = [
|
||||
'工资' => 101,
|
||||
'奖金' => 102,
|
||||
'投资' => 103,
|
||||
'兼职' => 104,
|
||||
'其他' => 105
|
||||
];
|
||||
|
||||
$map = $type === 'income' ? $incomeMap : $expenseMap;
|
||||
return $map[$categoryName] ?? ($type === 'income' ? 105 : 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加账单
|
||||
*/
|
||||
public function add(Request $request)
|
||||
{
|
||||
$userId = auth('api')->user()['uid'];
|
||||
|
||||
$data = $request->validate([
|
||||
'type' => 'required|in:income,expense',
|
||||
'amount' => 'required|numeric|min:0.01',
|
||||
'category_id' => 'required|integer',
|
||||
'remark' => 'nullable|string|max:255',
|
||||
'date' => 'required|date',
|
||||
'family_id' => 'nullable|integer|exists:account_families,id'
|
||||
]);
|
||||
|
||||
// 将category_id转换为category字符串
|
||||
$categoryMap = $this->getCategoryMap($data['type']);
|
||||
if (!isset($categoryMap[$data['category_id']])) {
|
||||
throw new \Exception('分类不存在');
|
||||
}
|
||||
|
||||
$data['category'] = $categoryMap[$data['category_id']];
|
||||
$data['bill_date'] = $data['date'];
|
||||
|
||||
// 检查家庭权限
|
||||
if (!empty($data['family_id'])) {
|
||||
$this->checkFamilyAccess($userId, $data['family_id']);
|
||||
}
|
||||
|
||||
$data['user_id'] = $userId;
|
||||
|
||||
// 移除前端字段
|
||||
unset($data['category_id'], $data['date']);
|
||||
|
||||
return Bill::create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分类映射
|
||||
*/
|
||||
private function getCategoryMap($type)
|
||||
{
|
||||
if ($type === 'income') {
|
||||
return [
|
||||
101 => '工资',
|
||||
102 => '奖金',
|
||||
103 => '投资',
|
||||
104 => '兼职',
|
||||
105 => '其他'
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
1 => '餐饮',
|
||||
2 => '交通',
|
||||
3 => '购物',
|
||||
4 => '娱乐',
|
||||
5 => '医疗',
|
||||
6 => '教育',
|
||||
7 => '居住',
|
||||
8 => '其他'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑账单
|
||||
*/
|
||||
public function edit(Request $request)
|
||||
{
|
||||
$userId = auth('api')->user()['uid'];
|
||||
$id = $request->input('id');
|
||||
|
||||
$bill = Bill::findOrFail($id);
|
||||
|
||||
// 验证权限
|
||||
if ($bill->user_id != $userId) {
|
||||
throw new \Exception('无权操作此账单');
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'type' => 'required|in:income,expense',
|
||||
'amount' => 'required|numeric|min:0.01',
|
||||
'category' => 'required|string|max:50',
|
||||
'remark' => 'nullable|string|max:255',
|
||||
'bill_date' => 'required|date',
|
||||
'family_id' => 'nullable|integer|exists:account_families,id'
|
||||
]);
|
||||
|
||||
// 检查家庭权限
|
||||
if (!empty($data['family_id'])) {
|
||||
$this->checkFamilyAccess($userId, $data['family_id']);
|
||||
}
|
||||
|
||||
$bill->update($data);
|
||||
|
||||
return $bill;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除账单
|
||||
*/
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$userId = auth('api')->user()['uid'];
|
||||
$id = $request->input('id');
|
||||
|
||||
$bill = Bill::findOrFail($id);
|
||||
|
||||
// 验证权限
|
||||
if ($bill->user_id != $userId) {
|
||||
throw new \Exception('无权删除此账单');
|
||||
}
|
||||
|
||||
return $bill->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账单详情
|
||||
*/
|
||||
public function detail(Request $request)
|
||||
{
|
||||
$userId = auth('api')->user()['uid'];
|
||||
$id = $request->input('id');
|
||||
|
||||
$bill = Bill::with(['user:uid,nickname,username', 'family:id,name,owner_id'])
|
||||
->findOrFail($id);
|
||||
|
||||
// 验证权限
|
||||
if ($bill->user_id != $userId && !in_array($userId, $bill->family->members->pluck('uid')->toArray())) {
|
||||
throw new \Exception('无权查看此账单');
|
||||
}
|
||||
|
||||
return $bill;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查家庭访问权限
|
||||
*/
|
||||
private function checkFamilyAccess($userId, $familyId)
|
||||
{
|
||||
$isMember = DB::table('account_family_members')
|
||||
->where('family_id', $familyId)
|
||||
->where('user_id', $userId)
|
||||
->exists();
|
||||
|
||||
if (!$isMember) {
|
||||
throw new \Exception('您不是该家庭成员');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户所在的家庭ID
|
||||
*/
|
||||
private function getUserFamilyId($userId)
|
||||
{
|
||||
$familyMember = DB::table('account_family_members')
|
||||
->where('user_id', $userId)
|
||||
->first();
|
||||
return $familyMember ? $familyMember->family_id : null;
|
||||
}
|
||||
}
|
||||
312
modules/Account/app/Services/FamilyService.php
Normal file
312
modules/Account/app/Services/FamilyService.php
Normal file
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Account\Services;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\Account\Models\Family;
|
||||
use Modules\Account\Models\FamilyMember;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class FamilyService
|
||||
{
|
||||
/**
|
||||
* 获取家庭信息
|
||||
*/
|
||||
public function getInfo(Request $request)
|
||||
{
|
||||
$userId = auth('api')->user()['uid'];
|
||||
|
||||
$familyMember = FamilyMember::where('user_id', $userId)->first();
|
||||
|
||||
if (!$familyMember) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$family = Family::with(['owner:uid,nickname,username', 'members:uid,nickname,username,email'])
|
||||
->findOrFail($familyMember->family_id);
|
||||
|
||||
return [
|
||||
'id' => $family->id,
|
||||
'name' => $family->name,
|
||||
'invite_code' => $family->invite_code,
|
||||
'owner_id' => $family->owner_id,
|
||||
'is_owner' => $family->owner_id == $userId,
|
||||
'created_at' => $family->created_at->format('Y-m-d H:i:s'),
|
||||
'owner' => $family->owner,
|
||||
'members' => $family->members
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建家庭
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
$userId = auth('api')->user()['uid'];
|
||||
|
||||
// 检查是否已加入家庭
|
||||
$existingFamily = FamilyMember::where('user_id', $userId)->first();
|
||||
if ($existingFamily) {
|
||||
throw new \Exception('您已加入其他家庭,请先退出');
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'name' => 'required|string|max:50'
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
// 生成10位邀请码
|
||||
do {
|
||||
$inviteCode = strtoupper(substr(str_shuffle('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 10));
|
||||
} while (Family::where('invite_code', $inviteCode)->exists());
|
||||
|
||||
$family = Family::create([
|
||||
'name' => $data['name'],
|
||||
'owner_id' => $userId,
|
||||
'invite_code' => $inviteCode
|
||||
]);
|
||||
|
||||
// 家主自动加入家庭
|
||||
FamilyMember::create([
|
||||
'family_id' => $family->id,
|
||||
'user_id' => $userId
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return $family;
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入家庭
|
||||
*/
|
||||
public function join(Request $request)
|
||||
{
|
||||
$userId = auth('api')->user()['uid'];
|
||||
|
||||
// 检查是否已加入家庭
|
||||
$existingFamily = FamilyMember::where('user_id', $userId)->first();
|
||||
if ($existingFamily) {
|
||||
throw new \Exception('您已加入其他家庭');
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'invite_code' => 'required|string|size:10'
|
||||
]);
|
||||
|
||||
$family = Family::where('invite_code', strtoupper($data['invite_code']))->first();
|
||||
|
||||
if (!$family) {
|
||||
throw new \Exception('邀请码不存在');
|
||||
}
|
||||
|
||||
// 检查是否是家主
|
||||
if ($family->owner_id == $userId) {
|
||||
throw new \Exception('您已经是该家庭的家主');
|
||||
}
|
||||
|
||||
// 加入家庭
|
||||
FamilyMember::create([
|
||||
'family_id' => $family->id,
|
||||
'user_id' => $userId
|
||||
]);
|
||||
|
||||
return $family;
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出家庭
|
||||
*/
|
||||
public function leave(Request $request)
|
||||
{
|
||||
$userId = auth('api')->user()['uid'];
|
||||
|
||||
$familyMember = FamilyMember::where('user_id', $userId)->first();
|
||||
|
||||
if (!$familyMember) {
|
||||
throw new \Exception('您还未加入任何家庭');
|
||||
}
|
||||
|
||||
$family = Family::findOrFail($familyMember->family_id);
|
||||
|
||||
// 家主不能退出,只能转让或删除
|
||||
if ($family->owner_id == $userId) {
|
||||
throw new \Exception('家主不能退出家庭,请先转让家主');
|
||||
}
|
||||
|
||||
$familyMember->delete();
|
||||
|
||||
return ['message' => '已退出家庭'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取家庭邀请码
|
||||
*/
|
||||
public function getInviteCode(Request $request)
|
||||
{
|
||||
$userId = auth('api')->user()['uid'];
|
||||
|
||||
$familyMember = FamilyMember::where('user_id', $userId)->first();
|
||||
|
||||
if (!$familyMember) {
|
||||
throw new \Exception('您还未加入任何家庭');
|
||||
}
|
||||
|
||||
$family = Family::findOrFail($familyMember->family_id);
|
||||
|
||||
return [
|
||||
'invite_code' => $family->invite_code,
|
||||
'family_name' => $family->name
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新生成邀请码
|
||||
*/
|
||||
public function regenerateInviteCode(Request $request)
|
||||
{
|
||||
$userId = auth('api')->user()['uid'];
|
||||
|
||||
$familyMember = FamilyMember::where('user_id', $userId)->first();
|
||||
|
||||
if (!$familyMember) {
|
||||
throw new \Exception('您还未加入任何家庭');
|
||||
}
|
||||
|
||||
$family = Family::findOrFail($familyMember->family_id);
|
||||
|
||||
// 只有家主可以重新生成邀请码
|
||||
if ($family->owner_id != $userId) {
|
||||
throw new \Exception('只有家主可以重新生成邀请码');
|
||||
}
|
||||
|
||||
// 生成新邀请码
|
||||
do {
|
||||
$inviteCode = strtoupper(substr(str_shuffle('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 10));
|
||||
} while (Family::where('invite_code', $inviteCode)->exists());
|
||||
|
||||
$family->update(['invite_code' => $inviteCode]);
|
||||
|
||||
return [
|
||||
'invite_code' => $inviteCode,
|
||||
'family_name' => $family->name
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除家庭成员
|
||||
*/
|
||||
public function removeMember(Request $request)
|
||||
{
|
||||
$userId = auth('api')->user()['uid'];
|
||||
|
||||
$data = $request->validate([
|
||||
'user_id' => 'required|integer'
|
||||
]);
|
||||
|
||||
$familyMember = FamilyMember::where('user_id', $userId)->first();
|
||||
|
||||
if (!$familyMember) {
|
||||
throw new \Exception('您还未加入任何家庭');
|
||||
}
|
||||
|
||||
$family = Family::findOrFail($familyMember->family_id);
|
||||
|
||||
// 只有家主可以移除成员
|
||||
if ($family->owner_id != $userId) {
|
||||
throw new \Exception('只有家主可以移除成员');
|
||||
}
|
||||
|
||||
// 不能移除家主
|
||||
if ($family->owner_id == $data['user_id']) {
|
||||
throw new \Exception('不能移除家主');
|
||||
}
|
||||
|
||||
// 移除成员
|
||||
$targetMember = FamilyMember::where('family_id', $family->id)
|
||||
->where('user_id', $data['user_id'])
|
||||
->first();
|
||||
|
||||
if (!$targetMember) {
|
||||
throw new \Exception('该用户不在家庭中');
|
||||
}
|
||||
|
||||
$targetMember->delete();
|
||||
|
||||
return ['message' => '成员已移除'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取家庭成员列表
|
||||
*/
|
||||
public function getMembers(Request $request)
|
||||
{
|
||||
$userId = auth('api')->user()['uid'];
|
||||
|
||||
$familyMember = FamilyMember::where('user_id', $userId)->first();
|
||||
|
||||
if (!$familyMember) {
|
||||
throw new \Exception('您还未加入任何家庭');
|
||||
}
|
||||
|
||||
$family = Family::findOrFail($familyMember->family_id);
|
||||
|
||||
$members = FamilyMember::with('user:uid,nickname,username,email')
|
||||
->where('family_id', $family->id)
|
||||
->get();
|
||||
|
||||
return $members->map(function($member) use ($family) {
|
||||
return [
|
||||
'id' => $member->user->uid,
|
||||
'name' => $member->user->nickname,
|
||||
'username' => $member->user->username,
|
||||
'is_owner' => $family->owner_id == $member->user->uid
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转让家主
|
||||
*/
|
||||
public function transferOwner(Request $request)
|
||||
{
|
||||
$userId = auth('api')->user()['uid'];
|
||||
|
||||
$data = $request->validate([
|
||||
'user_id' => 'required|integer'
|
||||
]);
|
||||
|
||||
$familyMember = FamilyMember::where('user_id', $userId)->first();
|
||||
|
||||
if (!$familyMember) {
|
||||
throw new \Exception('您还未加入任何家庭');
|
||||
}
|
||||
|
||||
$family = Family::findOrFail($familyMember->family_id);
|
||||
|
||||
// 只有当前家主可以转让
|
||||
if ($family->owner_id != $userId) {
|
||||
throw new \Exception('只有家主可以转让');
|
||||
}
|
||||
|
||||
// 目标用户必须在家庭中
|
||||
$targetMember = FamilyMember::where('family_id', $family->id)
|
||||
->where('user_id', $data['user_id'])
|
||||
->first();
|
||||
|
||||
if (!$targetMember) {
|
||||
throw new \Exception('该用户不在家庭中');
|
||||
}
|
||||
|
||||
// 转让家主
|
||||
$family->update(['owner_id' => $data['user_id']]);
|
||||
|
||||
return ['message' => '家主已转让'];
|
||||
}
|
||||
}
|
||||
231
modules/Account/app/Services/StatisticsService.php
Normal file
231
modules/Account/app/Services/StatisticsService.php
Normal file
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Account\Services;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\Account\Models\Bill;
|
||||
use Modules\Account\Models\FamilyMember;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class StatisticsService
|
||||
{
|
||||
/**
|
||||
* 获取统计概览
|
||||
*/
|
||||
public function getOverview(Request $request)
|
||||
{
|
||||
$userId = auth('api')->user()['uid'];
|
||||
$year = $request->input('year');
|
||||
$month = $request->input('month');
|
||||
|
||||
// 获取用户所在的家庭ID
|
||||
$familyId = $this->getUserFamilyId($userId);
|
||||
|
||||
// 查询当月的账单
|
||||
$query = Bill::query();
|
||||
|
||||
if ($year && $month) {
|
||||
$query->whereMonth('bill_date', str_pad($month, 2, '0', STR_PAD_LEFT))
|
||||
->whereYear('bill_date', $year);
|
||||
} else {
|
||||
$query->whereMonth('bill_date', date('m'))
|
||||
->whereYear('bill_date', date('Y'));
|
||||
}
|
||||
|
||||
if ($familyId) {
|
||||
$query->where('family_id', $familyId);
|
||||
} else {
|
||||
$query->where('user_id', $userId);
|
||||
}
|
||||
|
||||
$bills = $query->get();
|
||||
|
||||
$income = (float) $bills->where('type', 'income')->sum('amount');
|
||||
$expense = (float) $bills->where('type', 'expense')->sum('amount');
|
||||
|
||||
return [
|
||||
'income' => $income,
|
||||
'expense' => $expense,
|
||||
'balance' => $income - $expense
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取收支趋势
|
||||
*/
|
||||
public function getTrend(Request $request)
|
||||
{
|
||||
$userId = auth('api')->user()['uid'];
|
||||
$year = $request->input('year');
|
||||
$month = $request->input('month');
|
||||
$days = 7; // 最近7天
|
||||
|
||||
// 获取用户所在的家庭ID
|
||||
$familyId = $this->getUserFamilyId($userId);
|
||||
|
||||
$data = [];
|
||||
for ($i = $days - 1; $i >= 0; $i--) {
|
||||
$date = date('Y-m-d', strtotime("-$i days"));
|
||||
|
||||
$query = Bill::whereDate('bill_date', $date);
|
||||
|
||||
if ($familyId) {
|
||||
$query->where('family_id', $familyId);
|
||||
} else {
|
||||
$query->where('user_id', $userId);
|
||||
}
|
||||
|
||||
$bills = $query->get();
|
||||
|
||||
$data[] = [
|
||||
'date' => $date,
|
||||
'income' => (float) $bills->where('type', 'income')->sum('amount'),
|
||||
'expense' => (float) $bills->where('type', 'expense')->sum('amount')
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分类统计
|
||||
*/
|
||||
public function getCategory(Request $request)
|
||||
{
|
||||
$userId = auth('api')->user()['uid'];
|
||||
$type = $request->input('type', 'expense');
|
||||
$year = $request->input('year');
|
||||
$month = $request->input('month');
|
||||
|
||||
// 获取用户所在的家庭ID
|
||||
$familyId = $this->getUserFamilyId($userId);
|
||||
|
||||
$query = Bill::where('type', $type);
|
||||
|
||||
if ($year && $month) {
|
||||
$query->whereMonth('bill_date', str_pad($month, 2, '0', STR_PAD_LEFT))
|
||||
->whereYear('bill_date', $year);
|
||||
} else {
|
||||
$query->whereMonth('bill_date', date('m'))
|
||||
->whereYear('bill_date', date('Y'));
|
||||
}
|
||||
|
||||
if ($familyId) {
|
||||
$query->where('family_id', $familyId);
|
||||
} else {
|
||||
$query->where('user_id', $userId);
|
||||
}
|
||||
|
||||
$bills = $query->get();
|
||||
|
||||
// 按分类汇总
|
||||
$data = $bills->groupBy('category')->map(function ($items) {
|
||||
return [
|
||||
'category_id' => $this->getCategoryId($items->first()->category, $type),
|
||||
'amount' => (float) $items->sum('amount')
|
||||
];
|
||||
})->sortByDesc('amount')->values();
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分类名称获取分类ID
|
||||
*/
|
||||
private function getCategoryId($categoryName, $type)
|
||||
{
|
||||
$expenseMap = [
|
||||
'餐饮' => 1, '交通' => 2, '购物' => 3, '娱乐' => 4,
|
||||
'医疗' => 5, '教育' => 6, '居住' => 7, '其他' => 8
|
||||
];
|
||||
$incomeMap = [
|
||||
'工资' => 101, '奖金' => 102, '投资' => 103, '兼职' => 104, '其他' => 105
|
||||
];
|
||||
|
||||
$map = $type === 'income' ? $incomeMap : $expenseMap;
|
||||
return $map[$categoryName] ?? ($type === 'income' ? 105 : 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取月度报表
|
||||
*/
|
||||
public function getMonthly(Request $request)
|
||||
{
|
||||
$userId = auth('api')->user()['uid'];
|
||||
$year = $request->input('year', date('Y'));
|
||||
|
||||
// 获取用户所在的家庭ID
|
||||
$familyId = $this->getUserFamilyId($userId);
|
||||
|
||||
$data = [];
|
||||
for ($month = 1; $month <= 12; $month++) {
|
||||
$query = Bill::whereMonth('bill_date', $month)
|
||||
->whereYear('bill_date', $year);
|
||||
|
||||
if ($familyId) {
|
||||
$query->where('family_id', $familyId);
|
||||
} else {
|
||||
$query->where('user_id', $userId);
|
||||
}
|
||||
|
||||
$bills = $query->get();
|
||||
|
||||
$data[] = [
|
||||
'month' => sprintf('%d-%02d', $year, $month),
|
||||
'income' => (float) number_format($bills->where('type', 'income')->sum('amount'), 2),
|
||||
'expense' => (float) number_format($bills->where('type', 'expense')->sum('amount'), 2),
|
||||
'bill_count' => $bills->count()
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'year' => $year,
|
||||
'data' => $data
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取年度报表
|
||||
*/
|
||||
public function getYearly(Request $request)
|
||||
{
|
||||
$userId = auth('api')->user()['uid'];
|
||||
$years = $request->input('years', 5);
|
||||
|
||||
// 获取用户所在的家庭ID
|
||||
$familyId = $this->getUserFamilyId($userId);
|
||||
|
||||
$data = [];
|
||||
for ($i = $years - 1; $i >= 0; $i--) {
|
||||
$year = date('Y', strtotime("-$i years"));
|
||||
|
||||
$query = Bill::whereYear('bill_date', $year);
|
||||
|
||||
if ($familyId) {
|
||||
$query->where('family_id', $familyId);
|
||||
} else {
|
||||
$query->where('user_id', $userId);
|
||||
}
|
||||
|
||||
$bills = $query->get();
|
||||
|
||||
$data[] = [
|
||||
'year' => $year,
|
||||
'income' => (float) number_format($bills->where('type', 'income')->sum('amount'), 2),
|
||||
'expense' => (float) number_format($bills->where('type', 'expense')->sum('amount'), 2),
|
||||
'bill_count' => $bills->count()
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户所在的家庭ID
|
||||
*/
|
||||
private function getUserFamilyId($userId)
|
||||
{
|
||||
$familyMember = FamilyMember::where('user_id', $userId)->first();
|
||||
return $familyMember ? $familyMember->family_id : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('account_families', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->comment('家庭名称');
|
||||
$table->unsignedBigInteger('owner_id')->comment('家主用户ID');
|
||||
$table->foreign('owner_id')->references('uid')->on('member')->onDelete('cascade');
|
||||
$table->string('invite_code', 10)->unique()->comment('邀请码');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index('owner_id');
|
||||
$table->index('invite_code');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('account_families');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('account_bills', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('user_id')->comment('用户ID');
|
||||
$table->foreign('user_id')->references('uid')->on('member')->onDelete('cascade');
|
||||
$table->foreignId('family_id')->nullable()->constrained('account_families')->onDelete('set null')->comment('家庭ID');
|
||||
$table->enum('type', ['income', 'expense'])->default('expense')->comment('类型:income-收入,expense-支出');
|
||||
$table->decimal('amount', 10, 2)->comment('金额');
|
||||
$table->string('category', 50)->comment('分类');
|
||||
$table->string('remark')->nullable()->comment('备注');
|
||||
$table->date('bill_date')->comment('账单日期');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index('user_id');
|
||||
$table->index('family_id');
|
||||
$table->index('type');
|
||||
$table->index('bill_date');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('account_bills');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('account_family_members', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('family_id')->constrained('account_families')->onDelete('cascade')->comment('家庭ID');
|
||||
$table->unsignedBigInteger('user_id')->comment('用户ID');
|
||||
$table->foreign('user_id')->references('uid')->on('member')->onDelete('cascade');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('family_id');
|
||||
$table->index('user_id');
|
||||
$table->unique(['family_id', 'user_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('account_family_members');
|
||||
}
|
||||
};
|
||||
@@ -1,14 +1,48 @@
|
||||
<?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');
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Account\Controllers\Api\BillController;
|
||||
use Modules\Account\Controllers\Api\FamilyController;
|
||||
use Modules\Account\Controllers\Api\StatisticsController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Account API Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| 账单、家庭、统计相关API路由
|
||||
|
|
||||
*/
|
||||
|
||||
Route::middleware(['auth:api'])->group(function () {
|
||||
// 账单路由
|
||||
Route::prefix('bill')->group(function () {
|
||||
Route::get('list', [BillController::class, 'index']);
|
||||
Route::post('add', [BillController::class, 'add']);
|
||||
Route::post('edit', [BillController::class, 'edit']);
|
||||
Route::post('delete', [BillController::class, 'delete']);
|
||||
Route::get('detail', [BillController::class, 'detail']);
|
||||
});
|
||||
|
||||
// 家庭路由
|
||||
Route::prefix('family')->group(function () {
|
||||
Route::get('info', [FamilyController::class, 'info']);
|
||||
Route::post('create', [FamilyController::class, 'create']);
|
||||
Route::post('join', [FamilyController::class, 'join']);
|
||||
Route::post('leave', [FamilyController::class, 'leave']);
|
||||
Route::get('invite-code', [FamilyController::class, 'inviteCode']);
|
||||
Route::post('regenerate-invite-code', [FamilyController::class, 'regenerateInviteCode']);
|
||||
Route::post('remove-member', [FamilyController::class, 'removeMember']);
|
||||
Route::get('members', [FamilyController::class, 'members']);
|
||||
Route::post('transfer-owner', [FamilyController::class, 'transferOwner']);
|
||||
});
|
||||
|
||||
// 统计路由
|
||||
Route::prefix('statistics')->group(function () {
|
||||
Route::get('overview', [StatisticsController::class, 'overview']);
|
||||
Route::get('trend', [StatisticsController::class, 'trend']);
|
||||
Route::get('category', [StatisticsController::class, 'category']);
|
||||
Route::get('monthly', [StatisticsController::class, 'monthly']);
|
||||
Route::get('yearly', [StatisticsController::class, 'yearly']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
v-model="form.amount"
|
||||
placeholder="0.00"
|
||||
placeholder-style="color: #ddd"
|
||||
@input="onAmountInput"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
@@ -145,6 +146,16 @@ export default {
|
||||
onDateChange(e) {
|
||||
this.form.date = e.detail.value
|
||||
},
|
||||
onAmountInput(e) {
|
||||
// 限制小数点后两位
|
||||
let value = e.detail.value
|
||||
if (value.includes('.')) {
|
||||
const parts = value.split('.')
|
||||
if (parts[1] && parts[1].length > 2) {
|
||||
this.form.amount = `${parts[0]}.${parts[1].substring(0, 2)}`
|
||||
}
|
||||
}
|
||||
},
|
||||
async handleSubmit() {
|
||||
// 验证
|
||||
if (!this.form.amount || parseFloat(this.form.amount) <= 0) {
|
||||
@@ -163,6 +174,14 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.form.date) {
|
||||
uni.showToast({
|
||||
title: '请选择日期',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
@@ -170,11 +189,11 @@ export default {
|
||||
type: this.form.type,
|
||||
amount: parseFloat(this.form.amount),
|
||||
category_id: this.form.categoryId,
|
||||
remark: this.form.remark,
|
||||
remark: this.form.remark || undefined,
|
||||
date: this.form.date
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
if (res && res.code === 1) {
|
||||
uni.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
@@ -184,14 +203,14 @@ export default {
|
||||
}, 1500)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.message || '保存失败',
|
||||
title: res?.message || '保存失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存账单失败', error)
|
||||
uni.showToast({
|
||||
title: '保存失败,请重试',
|
||||
title: error?.message || '保存失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
<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-date">{{ group.formattedDate }}</text>
|
||||
<text class="group-amount">支出: ¥{{ group.expense.toFixed(2) }} 收入: ¥{{ group.income.toFixed(2) }}</text>
|
||||
</view>
|
||||
|
||||
@@ -76,6 +76,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tool from '@/utils/tool'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -112,7 +114,8 @@ export default {
|
||||
groups[date] = {
|
||||
bills: [],
|
||||
expense: 0,
|
||||
income: 0
|
||||
income: 0,
|
||||
formattedDate: this.formatDate(date, 'MM月dd日')
|
||||
}
|
||||
}
|
||||
groups[date].bills.push(bill)
|
||||
@@ -156,7 +159,7 @@ export default {
|
||||
month: parseInt(month)
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
if (res && res.code === 1) {
|
||||
this.billList = res.data?.list || []
|
||||
this.monthExpense = res.data?.month_expense || 0
|
||||
this.monthIncome = res.data?.month_income || 0
|
||||
@@ -164,7 +167,7 @@ export default {
|
||||
} catch (error) {
|
||||
console.error('加载账单列表失败', error)
|
||||
uni.showToast({
|
||||
title: '加载失败,请重试',
|
||||
title: error?.message || '加载失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
@@ -191,6 +194,9 @@ export default {
|
||||
},
|
||||
getCategoryColor(categoryId) {
|
||||
return this.categoryMap[categoryId]?.color || '#BDC3C7'
|
||||
},
|
||||
formatDate(date, fmt) {
|
||||
return tool.dateFormat(date, fmt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tool from '@/utils/tool'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -173,14 +175,19 @@ export default {
|
||||
this.loading = true
|
||||
try {
|
||||
const [year, month] = this.currentMonth.split('-')
|
||||
console.log('加载统计数据 - 年:', year, '月:', month)
|
||||
|
||||
// 获取统计概览
|
||||
const overviewRes = await this.$api.statistics.overview.get({
|
||||
year: parseInt(year),
|
||||
month: parseInt(month)
|
||||
})
|
||||
if (overviewRes.code === 200) {
|
||||
console.log('概览接口返回:', overviewRes)
|
||||
if (overviewRes && overviewRes.code === 1) {
|
||||
this.overview = overviewRes.data || { income: 0, expense: 0, balance: 0 }
|
||||
console.log('概览数据:', this.overview)
|
||||
} else {
|
||||
console.error('概览接口返回错误:', overviewRes?.message)
|
||||
}
|
||||
|
||||
// 获取分类统计
|
||||
@@ -188,8 +195,12 @@ export default {
|
||||
year: parseInt(year),
|
||||
month: parseInt(month)
|
||||
})
|
||||
if (categoryRes.code === 200) {
|
||||
console.log('分类接口返回:', categoryRes)
|
||||
if (categoryRes && categoryRes.code === 1) {
|
||||
this.processCategoryData(categoryRes.data || [])
|
||||
console.log('分类数据:', this.categoryList)
|
||||
} else {
|
||||
console.error('分类接口返回错误:', categoryRes?.message)
|
||||
}
|
||||
|
||||
// 获取收支趋势
|
||||
@@ -197,13 +208,17 @@ export default {
|
||||
year: parseInt(year),
|
||||
month: parseInt(month)
|
||||
})
|
||||
if (trendRes.code === 200) {
|
||||
console.log('趋势接口返回:', trendRes)
|
||||
if (trendRes && trendRes.code === 1) {
|
||||
this.processTrendData(trendRes.data || [])
|
||||
console.log('趋势数据:', this.trendList)
|
||||
} else {
|
||||
console.error('趋势接口返回错误:', trendRes?.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载统计数据失败', error)
|
||||
uni.showToast({
|
||||
title: '加载失败,请重试',
|
||||
title: error?.message || '加载失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
@@ -211,6 +226,7 @@ export default {
|
||||
}
|
||||
},
|
||||
processCategoryData(data) {
|
||||
console.log('处理分类数据:', data)
|
||||
let total = 0
|
||||
const list = data.map(item => {
|
||||
total += item.amount
|
||||
@@ -226,8 +242,10 @@ export default {
|
||||
percent: total > 0 ? Math.round((item.amount / total) * 100) : 0
|
||||
}
|
||||
}).sort((a, b) => b.amount - a.amount)
|
||||
console.log('处理后的分类列表:', this.categoryList)
|
||||
},
|
||||
processTrendData(data) {
|
||||
console.log('处理趋势数据:', data)
|
||||
// 找出最大值用于计算高度百分比
|
||||
let maxIncome = 0
|
||||
let maxExpense = 0
|
||||
@@ -238,8 +256,7 @@ export default {
|
||||
})
|
||||
|
||||
this.trendList = data.map(item => {
|
||||
const date = new Date(item.date)
|
||||
const dateLabel = `${date.getMonth() + 1}/${date.getDate()}`
|
||||
const dateLabel = this.formatDate(item.date, 'MM/dd')
|
||||
|
||||
return {
|
||||
date: item.date,
|
||||
@@ -250,6 +267,10 @@ export default {
|
||||
expenseHeight: maxExpense > 0 ? (item.expense / maxExpense) * 100 : 0
|
||||
}
|
||||
})
|
||||
console.log('处理后的趋势列表:', this.trendList)
|
||||
},
|
||||
formatDate(date, fmt) {
|
||||
return tool.dateFormat(date, fmt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +63,8 @@ export default {
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const res = await this.$store.dispatch('family/createFamily', this.familyName.trim())
|
||||
if (res.code === 200) {
|
||||
const res = await this.$store.dispatch('createFamily', this.familyName.trim())
|
||||
if (res && res.code === 1) {
|
||||
uni.showToast({
|
||||
title: '创建成功',
|
||||
icon: 'success'
|
||||
@@ -74,14 +74,14 @@ export default {
|
||||
}, 1500)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.message || '创建失败',
|
||||
title: res?.message || '创建失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建家庭失败', error)
|
||||
uni.showToast({
|
||||
title: '创建失败,请重试',
|
||||
title: error?.message || '创建失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
|
||||
@@ -120,7 +120,7 @@ export default {
|
||||
if (!this.$store || !this.$store.getters) {
|
||||
return false
|
||||
}
|
||||
return this.$store.getters['family/hasFamily'] || false
|
||||
return this.$store.getters.hasFamily || false
|
||||
},
|
||||
isOwner() {
|
||||
if (!this.$store || !this.$store.state || !this.$store.state.family) {
|
||||
@@ -146,7 +146,7 @@ export default {
|
||||
try {
|
||||
// 获取家庭信息
|
||||
const familyRes = await this.$api.family.info.get()
|
||||
if (familyRes.code === 200) {
|
||||
if (familyRes && familyRes.code === 1) {
|
||||
this.familyInfo = familyRes.data
|
||||
this.$store.commit('family/setFamilyInfo', familyRes.data)
|
||||
}
|
||||
@@ -154,14 +154,14 @@ export default {
|
||||
// 获取邀请码(仅家主)
|
||||
if (this.isOwner) {
|
||||
const codeRes = await this.$api.family.inviteCode.get()
|
||||
if (codeRes.code === 200) {
|
||||
if (codeRes && codeRes.code === 1) {
|
||||
this.inviteCode = codeRes.data.invite_code
|
||||
}
|
||||
}
|
||||
|
||||
// 获取家庭成员列表
|
||||
const membersRes = await this.$api.family.members.get()
|
||||
if (membersRes.code === 200) {
|
||||
if (membersRes && membersRes.code === 1) {
|
||||
this.memberList = membersRes.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -194,17 +194,22 @@ export default {
|
||||
this.regenerating = true
|
||||
try {
|
||||
const res = await this.$api.family.regenerateInviteCode.post()
|
||||
if (res.code === 200) {
|
||||
if (res && res.code === 1) {
|
||||
this.inviteCode = res.data.invite_code
|
||||
uni.showToast({
|
||||
title: '邀请码已更新',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res?.message || '操作失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重新生成邀请码失败', error)
|
||||
uni.showToast({
|
||||
title: '操作失败,请重试',
|
||||
title: error?.message || '操作失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
@@ -224,17 +229,22 @@ export default {
|
||||
const result = await this.$api.family.removeMember.post({
|
||||
user_id: member.id
|
||||
})
|
||||
if (result.code === 200) {
|
||||
if (result && result.code === 1) {
|
||||
uni.showToast({
|
||||
title: '移除成功',
|
||||
icon: 'success'
|
||||
})
|
||||
this.loadFamilyData()
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: result?.message || '操作失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('移除成员失败', error)
|
||||
uni.showToast({
|
||||
title: '操作失败,请重试',
|
||||
title: error?.message || '操作失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
@@ -255,9 +265,9 @@ export default {
|
||||
content: '退出家庭后,您将无法查看和记录家庭账单,确定要退出吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const result = await this.$store.dispatch('family/leaveFamily')
|
||||
if (result.code === 200) {
|
||||
try {
|
||||
const result = await this.$store.dispatch('leaveFamily')
|
||||
if (result && result.code === 1) {
|
||||
uni.showToast({
|
||||
title: '已退出家庭',
|
||||
icon: 'success'
|
||||
@@ -265,11 +275,16 @@ export default {
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: result?.message || '操作失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('退出家庭失败', error)
|
||||
uni.showToast({
|
||||
title: '操作失败,请重试',
|
||||
title: error?.message || '操作失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ export default {
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const res = await this.$store.dispatch('family/joinFamily', this.inviteCode.trim())
|
||||
if (res.code === 200) {
|
||||
const res = await this.$store.dispatch('joinFamily', this.inviteCode.trim())
|
||||
if (res && res.code === 1) {
|
||||
uni.showToast({
|
||||
title: '加入成功',
|
||||
icon: 'success'
|
||||
@@ -81,14 +81,14 @@ export default {
|
||||
}, 1500)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.message || '加入失败',
|
||||
title: res?.message || '加入失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加入家庭失败', error)
|
||||
uni.showToast({
|
||||
title: '加入失败,请重试',
|
||||
title: error?.message || '加入失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</view>
|
||||
<view class="overview-data">
|
||||
<text class="overview-label">收入</text>
|
||||
<text class="overview-amount">¥0.00</text>
|
||||
<text class="overview-amount">¥{{ overview.income.toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="overview-divider"></view>
|
||||
@@ -41,7 +41,7 @@
|
||||
</view>
|
||||
<view class="overview-data">
|
||||
<text class="overview-label">支出</text>
|
||||
<text class="overview-amount">¥0.00</text>
|
||||
<text class="overview-amount">¥{{ overview.expense.toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -81,7 +81,12 @@
|
||||
<uni-icons type="right" size="14" color="#667eea"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-list">
|
||||
|
||||
<view v-if="loading" class="loading-state">
|
||||
<uni-load-more status="loading"></uni-load-more>
|
||||
</view>
|
||||
|
||||
<view v-else-if="recentBills.length === 0" class="empty-list">
|
||||
<view class="empty-icon">
|
||||
<uni-icons type="list" size="80" color="#ddd"></uni-icons>
|
||||
</view>
|
||||
@@ -91,6 +96,21 @@
|
||||
<text>记一笔</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view v-else class="bill-list">
|
||||
<view class="bill-item" v-for="bill in recentBills" :key="bill.id" @tap="viewBill(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-date">{{ formatDate(bill.date, 'MM/dd') }}</text>
|
||||
</view>
|
||||
<view class="bill-amount" :class="bill.type">
|
||||
<text>{{ bill.type === 'expense' ? '-' : '+' }}¥{{ bill.amount.toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 家庭管理 -->
|
||||
@@ -120,11 +140,34 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tool from '@/utils/tool'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
currentDate: '',
|
||||
currentMonth: ''
|
||||
currentMonth: '',
|
||||
overview: {
|
||||
income: 0,
|
||||
expense: 0
|
||||
},
|
||||
recentBills: [],
|
||||
loading: false,
|
||||
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: {
|
||||
@@ -132,7 +175,7 @@ export default {
|
||||
if (!this.$store || !this.$store.getters) {
|
||||
return false
|
||||
}
|
||||
return this.$store.getters['family/hasFamily'] || false
|
||||
return this.$store.getters.hasFamily || false
|
||||
},
|
||||
userName() {
|
||||
if (!this.$store || !this.$store.state || !this.$store.state.user || !this.$store.state.user.userInfo) {
|
||||
@@ -143,9 +186,18 @@ export default {
|
||||
},
|
||||
onLoad() {
|
||||
this.updateDate()
|
||||
this.loadFamilyInfo()
|
||||
this.loadData()
|
||||
},
|
||||
onShow() {
|
||||
this.updateDate()
|
||||
this.loadFamilyInfo()
|
||||
this.loadData()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.loadData().then(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
updateDate() {
|
||||
@@ -158,6 +210,39 @@ export default {
|
||||
this.currentDate = `${month}月${date}日 ${day}`
|
||||
this.currentMonth = `${month}月`
|
||||
},
|
||||
async loadFamilyInfo() {
|
||||
try {
|
||||
await this.$store.dispatch('getFamilyInfo')
|
||||
} catch (error) {
|
||||
console.error('获取家庭信息失败', error)
|
||||
}
|
||||
},
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
try {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = now.getMonth() + 1
|
||||
|
||||
// 并行加载数据
|
||||
const [overviewRes, billsRes] = await Promise.all([
|
||||
this.$api.statistics.overview.get({ year, month }),
|
||||
this.$api.bill.list.get({ year, month, limit: 5 })
|
||||
])
|
||||
|
||||
if (overviewRes && overviewRes.code === 1) {
|
||||
this.overview = overviewRes.data || { income: 0, expense: 0 }
|
||||
}
|
||||
|
||||
if (billsRes && billsRes.code === 1) {
|
||||
this.recentBills = billsRes.data?.list || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载数据失败', error)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
handleTabChange(path) {
|
||||
console.log('Tab changed to:', path)
|
||||
},
|
||||
@@ -165,6 +250,24 @@ export default {
|
||||
uni.navigateTo({
|
||||
url: url
|
||||
})
|
||||
},
|
||||
viewBill(bill) {
|
||||
// TODO: 实现查看账单详情
|
||||
uni.navigateTo({
|
||||
url: `/pages/account/bill/detail?id=${bill.id}`
|
||||
})
|
||||
},
|
||||
getCategoryName(categoryId) {
|
||||
return this.categoryMap[categoryId]?.name || '未知'
|
||||
},
|
||||
getCategoryIcon(categoryId) {
|
||||
return this.categoryMap[categoryId]?.icon || '📦'
|
||||
},
|
||||
getCategoryColor(categoryId) {
|
||||
return this.categoryMap[categoryId]?.color || '#BDC3C7'
|
||||
},
|
||||
formatDate(date, fmt) {
|
||||
return tool.dateFormat(date, fmt)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,9 +276,8 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
padding: 30rpx;
|
||||
padding-bottom: 150rpx;
|
||||
padding-bottom: 100rpx;
|
||||
background: linear-gradient(180deg, #F5F7FA 0%, #FFFFFF 100%);
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
@@ -563,6 +665,10 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
padding: 60rpx 0;
|
||||
}
|
||||
|
||||
.empty-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -607,6 +713,64 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.bill-list {
|
||||
.bill-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 2rpx solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.bill-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
|
||||
text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.bill-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.bill-category {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.bill-date {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.bill-amount {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
|
||||
&.expense {
|
||||
color: #FF6B6B;
|
||||
}
|
||||
|
||||
&.income {
|
||||
color: #2ECC71;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.family-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -178,13 +178,40 @@ export default {
|
||||
},
|
||||
|
||||
// 加载用户统计数据
|
||||
loadUserStats() {
|
||||
// 这里应该调用API获取实际数据
|
||||
// 暂时使用模拟数据
|
||||
this.stats = {
|
||||
billCount: 128,
|
||||
days: 45,
|
||||
familyMembers: 3
|
||||
async loadUserStats() {
|
||||
try {
|
||||
// 获取账单总数
|
||||
const billRes = await this.$api.bill.list.get({
|
||||
page: 1,
|
||||
limit: 1
|
||||
})
|
||||
if (billRes && billRes.code === 1) {
|
||||
this.stats.billCount = billRes.data?.total || 0
|
||||
}
|
||||
|
||||
// 获取家庭成员数
|
||||
const membersRes = await this.$api.family.members.get()
|
||||
if (membersRes && membersRes.code === 1) {
|
||||
this.stats.familyMembers = membersRes.data?.length || 0
|
||||
}
|
||||
|
||||
// 获取记账天数(计算从第一个账单到现在的天数)
|
||||
const firstBillRes = await this.$api.bill.list.get({
|
||||
page: 1,
|
||||
limit: 1,
|
||||
order: 'asc'
|
||||
})
|
||||
if (firstBillRes && firstBillRes.code === 1 && firstBillRes.data?.list?.length > 0) {
|
||||
const firstBill = firstBillRes.data.list[0]
|
||||
const firstDate = new Date(firstBill.date)
|
||||
const now = new Date()
|
||||
const diffTime = Math.abs(now - firstDate)
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
||||
this.stats.days = diffDays
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载统计数据失败', error)
|
||||
// 失败时保持默认值
|
||||
}
|
||||
},
|
||||
|
||||
@@ -201,24 +228,26 @@ export default {
|
||||
|
||||
// 页面跳转
|
||||
navigateTo(url) {
|
||||
if (!this.isLogin && url !== '/pages/ucenter/login/index') {
|
||||
if (!this.isLogin) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/ucenter/login/index'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 功能开发中提示
|
||||
uni.showToast({
|
||||
title: '功能开发中',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
// 直接跳转,不显示"功能开发中"
|
||||
uni.navigateTo({
|
||||
url: url
|
||||
})
|
||||
},
|
||||
|
||||
// 编辑个人资料
|
||||
handleEditProfile() {
|
||||
this.navigateTo('/pages/ucenter/profile')
|
||||
uni.showToast({
|
||||
title: '功能开发中',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
},
|
||||
|
||||
// 数据导出
|
||||
|
||||
@@ -43,7 +43,7 @@ export default{
|
||||
async getFamilyInfo({commit}){
|
||||
try {
|
||||
const res = await api.family.info.get()
|
||||
if(res.data){
|
||||
if(res && res.code === 1 && res.data){
|
||||
commit('setFamilyInfo', res.data)
|
||||
commit('setIsOwner', res.data.is_owner || false)
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export default{
|
||||
async createFamily({commit}, familyName){
|
||||
try {
|
||||
const res = await api.family.create.post({name: familyName})
|
||||
if(res.data){
|
||||
if(res && res.code === 1 && res.data){
|
||||
commit('setFamilyInfo', res.data)
|
||||
commit('setIsOwner', true)
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export default{
|
||||
async joinFamily({commit}, inviteCode){
|
||||
try {
|
||||
const res = await api.family.join.post({invite_code: inviteCode})
|
||||
if(res.data){
|
||||
if(res && res.code === 1 && res.data){
|
||||
commit('setFamilyInfo', res.data)
|
||||
commit('setIsOwner', res.data.is_owner || false)
|
||||
}
|
||||
@@ -82,7 +82,7 @@ export default{
|
||||
async leaveFamily({commit}){
|
||||
try {
|
||||
const res = await api.family.leave.post()
|
||||
if(res.code === 200){
|
||||
if(res && res.code === 1){
|
||||
commit('clearFamily')
|
||||
}
|
||||
return res
|
||||
|
||||
@@ -21,4 +21,7 @@ Route::name('system.')->prefix('system')->middleware(['auth.check:api'])->group(
|
||||
});
|
||||
});
|
||||
|
||||
// 记账相关路由已迁移到 modules/Account/routes/api.php
|
||||
// 记账相关路由
|
||||
Route::middleware(['auth:api'])->prefix('account')->group(function () {
|
||||
require base_path('modules/Account/routes/api.php');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user