feat: 员工自助注册+审核流程
- 新增 registration_packages 表:管理员配置注册套餐(名称/角色) - AuthController 新增 register() 和 registrationPackages() 公开接口 - UserController 新增 approve() / reject() 审核接口 - StoreController 新增 publicList() 公开门店列表 - 前端 /register 注册页:选门店→选岗位套餐→填信息→提交 - 前端 system/registration-packages:套餐 CRUD - 用户管理页:待审核状态展示 + 通过/拒绝快捷操作 - 登录页底部加「申请注册」跳转链接 - 路由白名单加 /register
This commit is contained in:
+1
-1
@@ -15,7 +15,7 @@ Thumbs.db
|
||||
|
||||
# Frontend
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
# frontend/dist/ — tracked for zero-build server deployment
|
||||
|
||||
# Backend (Laravel handles its own via backend/.gitignore)
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# CLAUDE.md — 月子 SaaS 项目上下文
|
||||
|
||||
## 部署信息
|
||||
|
||||
- **服务器**:`82.157.47.215`(root / Aiying88)
|
||||
- **前端**:`/var/www/saas/frontend`(Vue 3 静态 dist)
|
||||
- **后端**:`/var/www/saas/backend`(Laravel 12,PHP 8.3)
|
||||
- **仓库镜像**:`/var/www/saas/repo`(用于 git pull)
|
||||
- **Gitea 远程**:`git@git.20031227.de5.net:fengshao1227/yuezi-saas.git`
|
||||
|
||||
## 手动更新服务器
|
||||
|
||||
本地运行一键脚本(构建前端 → git push → SSH 触发服务器 pull + migrate):
|
||||
|
||||
```bash
|
||||
./deploy.sh "feat: 描述变更"
|
||||
```
|
||||
|
||||
只更新服务器(跳过本地构建和 push):
|
||||
|
||||
```bash
|
||||
ssh root@82.157.47.215 '/var/www/saas/deploy.sh'
|
||||
```
|
||||
|
||||
查看部署日志:
|
||||
|
||||
```bash
|
||||
ssh root@82.157.47.215 'tail -50 /var/log/saas-deploy.log'
|
||||
```
|
||||
|
||||
## 服务器 deploy.sh 做的事
|
||||
|
||||
1. `git pull origin main`(在 `/var/www/saas/repo`)
|
||||
2. rsync backend → `/var/www/saas/backend`(排除 `.env` / `vendor`)
|
||||
3. 如果 `composer.json` 有变动,执行 `composer install`
|
||||
4. `php artisan migrate --force` + 清缓存
|
||||
5. rsync `frontend/dist/` → `/var/www/saas/frontend`
|
||||
|
||||
## 关键配置
|
||||
|
||||
- 后端 `.env` 在服务器,不入库,手动维护
|
||||
- `bootstrap/app.php` 已移除 `statefulApi()`(用 Bearer Token 认证,无需 CSRF)
|
||||
- Nginx 配置:`/etc/nginx/conf.d/saas.conf`
|
||||
- PHP-FPM:`127.0.0.1:9000`
|
||||
@@ -85,4 +85,62 @@ class AuthController extends Controller
|
||||
'menus' => $user->getMenuTree(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用注册套餐列表(公开接口)
|
||||
*/
|
||||
public function registrationPackages(Request $request): JsonResponse
|
||||
{
|
||||
$storeId = $request->input('store_id');
|
||||
if (!$storeId) {
|
||||
return $this->error('请选择门店', 42200);
|
||||
}
|
||||
|
||||
$packages = \App\Models\System\RegistrationPackage::withoutGlobalScope('store')
|
||||
->where('store_id', $storeId)
|
||||
->where('status', 1)
|
||||
->orderBy('sort')
|
||||
->get(['id', 'name', 'description']);
|
||||
|
||||
return $this->success($packages);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自助注册(公开接口)
|
||||
* 注册后 status=2(待审核),需管理员审批后才能登录
|
||||
*/
|
||||
public function register(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'store_id' => 'required|exists:stores,id',
|
||||
'registration_package_id' => 'required|exists:registration_packages,id',
|
||||
'username' => 'required|string|max:50|unique:users,username',
|
||||
'password' => 'required|string|min:6',
|
||||
'name' => 'required|string|max:50',
|
||||
'phone' => 'required|string|max:20',
|
||||
]);
|
||||
|
||||
// 验证套餐属于该门店
|
||||
$package = \App\Models\System\RegistrationPackage::withoutGlobalScope('store')
|
||||
->where('id', $data['registration_package_id'])
|
||||
->where('store_id', $data['store_id'])
|
||||
->where('status', 1)
|
||||
->firstOrFail();
|
||||
|
||||
$user = \App\Models\User::create([
|
||||
'store_id' => $data['store_id'],
|
||||
'username' => $data['username'],
|
||||
'password' => $data['password'],
|
||||
'name' => $data['name'],
|
||||
'phone' => $data['phone'],
|
||||
'status' => 2, // Pending
|
||||
]);
|
||||
|
||||
// 绑定套餐角色
|
||||
if (!empty($package->role_ids)) {
|
||||
$user->roles()->sync($package->role_ids);
|
||||
}
|
||||
|
||||
return $this->success(null, '注册成功,请等待管理员审核后登录');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Admin\Inventory;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Inventory\InventoryCheck;
|
||||
use App\Models\Inventory\Inventory;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class InventoryCheckController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = InventoryCheck::query()
|
||||
->with(['warehouse', 'operator'])
|
||||
->when($request->warehouse_id, fn($q, $v) => $q->where('warehouse_id', $v))
|
||||
->when($request->has('status'), fn($q) => $q->where('status', $request->status));
|
||||
|
||||
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$v = $request->validate([
|
||||
'check_no' => 'required|string|max:50|unique:inventory_checks',
|
||||
'warehouse_id' => 'required|exists:warehouses,id',
|
||||
'items' => 'nullable|array',
|
||||
'remark' => 'nullable|string|max:500',
|
||||
]);
|
||||
$v['status'] = 0;
|
||||
$v['operator_id'] = auth()->id();
|
||||
|
||||
return $this->success(InventoryCheck::create($v));
|
||||
}
|
||||
|
||||
public function show(InventoryCheck $inventoryCheck): JsonResponse
|
||||
{
|
||||
return $this->success($inventoryCheck->load(['warehouse', 'operator']));
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成盘点 — 可选择按实际数量修正库存
|
||||
*/
|
||||
public function complete(InventoryCheck $inventoryCheck, Request $request): JsonResponse
|
||||
{
|
||||
if ($inventoryCheck->status === 2) {
|
||||
return $this->error('已完成', 40001);
|
||||
}
|
||||
|
||||
$v = $request->validate([
|
||||
'items' => 'nullable|array',
|
||||
'finish_remark' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
DB::transaction(function () use ($inventoryCheck, $v) {
|
||||
$items = $v['items'] ?? $inventoryCheck->items ?? [];
|
||||
|
||||
// 如果有差异且需要调整库存
|
||||
foreach ($items as $item) {
|
||||
if (!isset($item['actual_qty'])) continue;
|
||||
$diff = ($item['actual_qty'] ?? 0) - ($item['book_qty'] ?? 0);
|
||||
if ($diff == 0) continue;
|
||||
|
||||
$inv = Inventory::firstOrCreate(
|
||||
['warehouse_id' => $inventoryCheck->warehouse_id, 'material_id' => $item['material_id'], 'batch_no' => null],
|
||||
['quantity' => 0]
|
||||
);
|
||||
$inv->increment('quantity', $diff);
|
||||
}
|
||||
|
||||
$inventoryCheck->update([
|
||||
'status' => 2,
|
||||
'items' => $items,
|
||||
'finish_remark' => $v['finish_remark'] ?? null,
|
||||
]);
|
||||
});
|
||||
|
||||
return $this->success($inventoryCheck->fresh(['warehouse']));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Admin\Inventory;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Inventory\Inventory;
|
||||
use App\Models\Inventory\Material;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class StockController extends Controller
|
||||
{
|
||||
/**
|
||||
* 库存台账 — 汇总 inventories 表,关联 material 信息
|
||||
* GET /inventory/stock
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = Inventory::query()
|
||||
->with(['warehouse', 'material'])
|
||||
->when($request->warehouse_id, fn($q, $v) => $q->where('warehouse_id', $v))
|
||||
->when($request->material_id, fn($q, $v) => $q->where('material_id', $v))
|
||||
->when($request->low_stock, fn($q) => $q->whereRaw('quantity <= (SELECT min_stock FROM materials WHERE materials.id = inventories.material_id AND min_stock IS NOT NULL)'));
|
||||
|
||||
return $this->paginate($query->paginate($request->input('per_page', 20)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出库存台账
|
||||
* GET /inventory/stock/export
|
||||
*/
|
||||
public function export(Request $request): JsonResponse
|
||||
{
|
||||
$rows = Inventory::with(['warehouse', 'material'])
|
||||
->when($request->warehouse_id, fn($q, $v) => $q->where('warehouse_id', $v))
|
||||
->get()
|
||||
->map(fn($inv) => [
|
||||
'warehouse' => $inv->warehouse?->name,
|
||||
'material_code' => $inv->material?->code,
|
||||
'material_name' => $inv->material?->name,
|
||||
'unit' => $inv->material?->unit,
|
||||
'quantity' => $inv->quantity,
|
||||
'batch_no' => $inv->batch_no,
|
||||
'expire_date' => $inv->expire_date?->toDateString(),
|
||||
'safety_stock' => $inv->material?->min_stock,
|
||||
'updated_at' => $inv->updated_at?->toDateTimeString(),
|
||||
]);
|
||||
|
||||
return $this->success(['rows' => $rows, 'total' => $rows->count()]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Admin\Inventory;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Inventory\TransferOrder;
|
||||
use App\Models\Inventory\Inventory;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TransferOrderController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = TransferOrder::query()
|
||||
->with(['fromWarehouse', 'toWarehouse', 'operator'])
|
||||
->when($request->transfer_no, fn($q, $v) => $q->where('transfer_no', 'like', "%{$v}%"))
|
||||
->when($request->has('status'), fn($q) => $q->where('status', $request->status))
|
||||
->when($request->from_warehouse_id, fn($q, $v) => $q->where('from_warehouse_id', $v));
|
||||
|
||||
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$v = $request->validate([
|
||||
'transfer_no' => 'required|string|max:50|unique:transfer_orders',
|
||||
'from_warehouse_id' => 'required|exists:warehouses,id',
|
||||
'to_warehouse_id' => 'required|exists:warehouses,id|different:from_warehouse_id',
|
||||
'items' => 'nullable|array',
|
||||
'remark' => 'nullable|string|max:500',
|
||||
]);
|
||||
$v['status'] = 0;
|
||||
$v['operator_id'] = auth()->id();
|
||||
|
||||
return $this->success(TransferOrder::create($v));
|
||||
}
|
||||
|
||||
public function show(TransferOrder $transferOrder): JsonResponse
|
||||
{
|
||||
return $this->success($transferOrder->load(['fromWarehouse', 'toWarehouse', 'operator']));
|
||||
}
|
||||
|
||||
public function confirm(TransferOrder $transferOrder): JsonResponse
|
||||
{
|
||||
if ($transferOrder->status !== 0) {
|
||||
return $this->error('当前状态不可确认', 40001);
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($transferOrder) {
|
||||
$transferOrder->update(['status' => 1]);
|
||||
|
||||
// 调拨:减源仓库、加目标仓库
|
||||
foreach (($transferOrder->items ?? []) as $item) {
|
||||
$materialId = $item['material_id'];
|
||||
$qty = $item['quantity'] ?? 0;
|
||||
|
||||
// 扣减出库仓库
|
||||
Inventory::where('warehouse_id', $transferOrder->from_warehouse_id)
|
||||
->where('material_id', $materialId)
|
||||
->decrement('quantity', $qty);
|
||||
|
||||
// 增加入库仓库
|
||||
$inv = Inventory::firstOrCreate(
|
||||
['warehouse_id' => $transferOrder->to_warehouse_id, 'material_id' => $materialId, 'batch_no' => null],
|
||||
['quantity' => 0]
|
||||
);
|
||||
$inv->increment('quantity', $qty);
|
||||
}
|
||||
});
|
||||
|
||||
return $this->success($transferOrder->fresh(['fromWarehouse', 'toWarehouse']));
|
||||
}
|
||||
|
||||
public function cancel(TransferOrder $transferOrder): JsonResponse
|
||||
{
|
||||
if ($transferOrder->status !== 0) {
|
||||
return $this->error('只有待确认状态可取消', 40001);
|
||||
}
|
||||
$transferOrder->update(['status' => 2]);
|
||||
return $this->success($transferOrder);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,11 @@ namespace App\Http\Controllers\Admin\Office;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Office\Approval;
|
||||
use App\Models\Office\ApprovalNode;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ApprovalController extends Controller
|
||||
{
|
||||
@@ -21,16 +24,59 @@ class ApprovalController extends Controller
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'template_id' => 'nullable|exists:approval_templates,id',
|
||||
'form_data' => 'nullable|array',
|
||||
'title' => 'required|string|max:255',
|
||||
'template_id' => 'nullable|exists:approval_templates,id',
|
||||
'form_data' => 'nullable|array',
|
||||
'related_type' => 'nullable|string|max:50',
|
||||
'related_id' => 'nullable|integer',
|
||||
'related_id' => 'nullable|integer',
|
||||
]);
|
||||
$validated['applicant_id'] = auth()->id();
|
||||
$validated['status'] = 0;
|
||||
|
||||
return $this->success(Approval::create($validated));
|
||||
$validated['applicant_id'] = auth()->id();
|
||||
$validated['status'] = 0;
|
||||
$validated['current_node'] = 1;
|
||||
|
||||
$approval = null;
|
||||
|
||||
DB::transaction(function () use ($validated, &$approval) {
|
||||
$approval = Approval::create($validated);
|
||||
|
||||
// 根据模板 flow_nodes 自动创建多级审批节点
|
||||
if ($approval->template_id && $approval->template) {
|
||||
$flowNodes = $approval->template->flow_nodes ?? [];
|
||||
foreach ($flowNodes as $node) {
|
||||
$level = $node['level'] ?? 1;
|
||||
$role = $node['role'] ?? null;
|
||||
$label = $node['label'] ?? "第{$level}级审批";
|
||||
|
||||
// 按 role 找同门店下首个匹配用户
|
||||
$approverId = null;
|
||||
if ($role) {
|
||||
$approverId = User::whereHas('roles', fn($q) => $q->where('slug', $role))
|
||||
->where('store_id', $approval->store_id)
|
||||
->value('id');
|
||||
}
|
||||
|
||||
ApprovalNode::create([
|
||||
'approval_id' => $approval->id,
|
||||
'node_order' => $level,
|
||||
'node_label' => $label,
|
||||
'role' => $role,
|
||||
'approver_id' => $approverId,
|
||||
'action' => 0,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
// 无模板:创建单一审批节点
|
||||
ApprovalNode::create([
|
||||
'approval_id' => $approval->id,
|
||||
'node_order' => 1,
|
||||
'node_label' => '审批',
|
||||
'action' => 0,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
return $this->success($approval->load(['template', 'applicant', 'nodes']));
|
||||
}
|
||||
|
||||
public function show(Approval $approval): JsonResponse
|
||||
@@ -44,7 +90,7 @@ class ApprovalController extends Controller
|
||||
return $this->error('该审批已完结,无法修改', 40001);
|
||||
}
|
||||
$validated = $request->validate([
|
||||
'title' => 'sometimes|string|max:255',
|
||||
'title' => 'sometimes|string|max:255',
|
||||
'form_data' => 'nullable|array',
|
||||
]);
|
||||
$approval->update($validated);
|
||||
@@ -62,43 +108,28 @@ class ApprovalController extends Controller
|
||||
return $this->success(null);
|
||||
}
|
||||
|
||||
public function approve(Approval $approval): JsonResponse
|
||||
public function approve(Request $request, Approval $approval): JsonResponse
|
||||
{
|
||||
if ($approval->status >= 2) {
|
||||
return $this->error('该审批已完结', 40001);
|
||||
}
|
||||
|
||||
$currentUser = auth()->user();
|
||||
$userRoles = $currentUser->roles()->pluck('slug')->toArray();
|
||||
|
||||
// 当前节点匹配:approver_id 精确匹配 或 role 匹配当前用户角色
|
||||
$node = $approval->nodes()
|
||||
->where('approver_id', auth()->id())
|
||||
->where('action', 0)
|
||||
->where('node_order', $approval->current_node)
|
||||
->where('action', 0)
|
||||
->where(function ($q) use ($currentUser, $userRoles) {
|
||||
$q->where('approver_id', $currentUser->id)
|
||||
->orWhereIn('role', $userRoles)
|
||||
->orWhereNull('approver_id'); // 无指定审批人时任何人可审
|
||||
})
|
||||
->first();
|
||||
|
||||
if (!$node) {
|
||||
return $this->error('无待处理的审批节点', 40001);
|
||||
}
|
||||
|
||||
$node->update(['action' => 1, 'acted_at' => now()]);
|
||||
|
||||
$pendingNodes = $approval->nodes()->where('action', 0)->exists();
|
||||
if (!$pendingNodes) {
|
||||
$approval->update(['status' => 2]);
|
||||
} else {
|
||||
$approval->update([
|
||||
'status' => 1,
|
||||
'current_node' => $approval->current_node + 1,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->success($approval->load('nodes'));
|
||||
}
|
||||
|
||||
public function reject(Request $request, Approval $approval): JsonResponse
|
||||
{
|
||||
$node = $approval->nodes()
|
||||
->where('approver_id', auth()->id())
|
||||
->where('action', 0)
|
||||
->where('node_order', $approval->current_node)
|
||||
->first();
|
||||
|
||||
if (!$node) {
|
||||
return $this->error('无待处理的审批节点', 40001);
|
||||
return $this->error('无待处理的审批节点或您无权审批此节点', 40001);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
@@ -106,13 +137,67 @@ class ApprovalController extends Controller
|
||||
]);
|
||||
|
||||
$node->update([
|
||||
'action' => 2,
|
||||
'comment' => $validated['comment'] ?? null,
|
||||
'acted_at' => now(),
|
||||
'action' => 1,
|
||||
'approver_id' => $currentUser->id,
|
||||
'comment' => $validated['comment'] ?? null,
|
||||
'acted_at' => now(),
|
||||
]);
|
||||
$approval->update(['status' => 3]);
|
||||
|
||||
return $this->success($approval->load('nodes'));
|
||||
// 检查是否还有下一节点
|
||||
$hasNext = $approval->nodes()
|
||||
->where('node_order', '>', $approval->current_node)
|
||||
->where('action', 0)
|
||||
->exists();
|
||||
|
||||
if ($hasNext) {
|
||||
$approval->update([
|
||||
'status' => 1, // 审批中
|
||||
'current_node' => $approval->current_node + 1,
|
||||
]);
|
||||
} else {
|
||||
$approval->update(['status' => 2]); // 已通过
|
||||
}
|
||||
|
||||
return $this->success($approval->load(['nodes.approver']));
|
||||
}
|
||||
|
||||
public function reject(Request $request, Approval $approval): JsonResponse
|
||||
{
|
||||
if ($approval->status >= 2) {
|
||||
return $this->error('该审批已完结', 40001);
|
||||
}
|
||||
|
||||
$currentUser = auth()->user();
|
||||
$userRoles = $currentUser->roles()->pluck('slug')->toArray();
|
||||
|
||||
$node = $approval->nodes()
|
||||
->where('node_order', $approval->current_node)
|
||||
->where('action', 0)
|
||||
->where(function ($q) use ($currentUser, $userRoles) {
|
||||
$q->where('approver_id', $currentUser->id)
|
||||
->orWhereIn('role', $userRoles)
|
||||
->orWhereNull('approver_id');
|
||||
})
|
||||
->first();
|
||||
|
||||
if (!$node) {
|
||||
return $this->error('无待处理的审批节点或您无权操作', 40001);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'comment' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$node->update([
|
||||
'action' => 2,
|
||||
'approver_id' => $currentUser->id,
|
||||
'comment' => $validated['comment'] ?? null,
|
||||
'acted_at' => now(),
|
||||
]);
|
||||
|
||||
$approval->update(['status' => 3]); // 已驳回
|
||||
|
||||
return $this->success($approval->load(['nodes.approver']));
|
||||
}
|
||||
|
||||
public function withdraw(Approval $approval): JsonResponse
|
||||
@@ -120,6 +205,9 @@ class ApprovalController extends Controller
|
||||
if ($approval->status > 1) {
|
||||
return $this->error('该审批已完结,无法撤回', 40001);
|
||||
}
|
||||
if ($approval->applicant_id !== auth()->id()) {
|
||||
return $this->error('只能撤回自己发起的审批', 40001);
|
||||
}
|
||||
$approval->update(['status' => 4]);
|
||||
|
||||
return $this->success($approval);
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\System;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\System\RegistrationPackage;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class RegistrationPackageController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$packages = RegistrationPackage::when($request->keyword, fn($q) =>
|
||||
$q->where('name', 'like', "%{$request->keyword}%"))
|
||||
->when($request->has('status'), fn($q) =>
|
||||
$q->where('status', $request->status))
|
||||
->orderBy('sort')->orderByDesc('id')
|
||||
->paginate($request->input('page_size', 20));
|
||||
|
||||
return $this->paginate($packages);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => 'required|string|max:50',
|
||||
'description' => 'nullable|string|max:255',
|
||||
'role_ids' => 'array',
|
||||
'role_ids.*' => 'exists:roles,id',
|
||||
'status' => 'in:0,1',
|
||||
'sort' => 'integer|min:0',
|
||||
]);
|
||||
|
||||
$data['store_id'] = auth()->user()->store_id;
|
||||
$package = RegistrationPackage::create($data);
|
||||
|
||||
return $this->success($package, '创建成功');
|
||||
}
|
||||
|
||||
public function show(RegistrationPackage $registrationPackage): JsonResponse
|
||||
{
|
||||
return $this->success($registrationPackage);
|
||||
}
|
||||
|
||||
public function update(Request $request, RegistrationPackage $registrationPackage): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => 'string|max:50',
|
||||
'description' => 'nullable|string|max:255',
|
||||
'role_ids' => 'array',
|
||||
'role_ids.*' => 'exists:roles,id',
|
||||
'status' => 'in:0,1',
|
||||
'sort' => 'integer|min:0',
|
||||
]);
|
||||
|
||||
$registrationPackage->update($data);
|
||||
return $this->success($registrationPackage, '更新成功');
|
||||
}
|
||||
|
||||
public function destroy(RegistrationPackage $registrationPackage): JsonResponse
|
||||
{
|
||||
$registrationPackage->delete();
|
||||
return $this->success(null, '删除成功');
|
||||
}
|
||||
}
|
||||
@@ -83,4 +83,16 @@ class StoreController extends Controller
|
||||
$store->delete();
|
||||
return $this->success(null, '删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 公开门店列表(注册页用,仅返回启用门店的 id/name)
|
||||
*/
|
||||
public function publicList(): JsonResponse
|
||||
{
|
||||
$stores = Store::withoutGlobalScope('store')
|
||||
->where('status', 1)
|
||||
->orderByDesc('id')
|
||||
->get(['id', 'name']);
|
||||
return $this->success($stores);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,30 @@ class UserController extends Controller
|
||||
return $this->success(null, '密码重置成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核通过 — 激活待审核用户
|
||||
*/
|
||||
public function approve(User $user): JsonResponse
|
||||
{
|
||||
if ($user->status->value !== 2) {
|
||||
return $this->error('该用户不在待审核状态');
|
||||
}
|
||||
$user->update(['status' => 1]);
|
||||
return $this->success(null, '已审核通过');
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核拒绝 — 禁用该用户
|
||||
*/
|
||||
public function reject(User $user): JsonResponse
|
||||
{
|
||||
if ($user->status->value !== 2) {
|
||||
return $this->error('该用户不在待审核状态');
|
||||
}
|
||||
$user->update(['status' => 0]);
|
||||
return $this->success(null, '已拒绝注册');
|
||||
}
|
||||
|
||||
public function destroy(User $user): JsonResponse
|
||||
{
|
||||
if ($user->is_super) {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
namespace App\Models\Inventory;
|
||||
use App\Models\User;
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class InventoryCheck extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
protected $table = 'inventory_checks';
|
||||
protected $fillable = ['store_id','check_no','warehouse_id','items','status','finish_remark','operator_id'];
|
||||
protected function casts(): array { return ['items'=>'array','status'=>'integer']; }
|
||||
public function warehouse(): BelongsTo { return $this->belongsTo(Warehouse::class); }
|
||||
public function operator(): BelongsTo { return $this->belongsTo(User::class, 'operator_id'); }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace App\Models\Inventory;
|
||||
use App\Models\User;
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class TransferOrder extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
protected $table = 'transfer_orders';
|
||||
protected $fillable = ['store_id','transfer_no','from_warehouse_id','to_warehouse_id','items','status','operator_id','remark'];
|
||||
protected function casts(): array { return ['items'=>'array','status'=>'integer']; }
|
||||
public function fromWarehouse(): BelongsTo { return $this->belongsTo(Warehouse::class, 'from_warehouse_id'); }
|
||||
public function toWarehouse(): BelongsTo { return $this->belongsTo(Warehouse::class, 'to_warehouse_id'); }
|
||||
public function operator(): BelongsTo { return $this->belongsTo(User::class, 'operator_id'); }
|
||||
}
|
||||
@@ -11,7 +11,8 @@ class ApprovalNode extends Model
|
||||
const UPDATED_AT = null;
|
||||
|
||||
protected $fillable = [
|
||||
'approval_id', 'node_order', 'approver_id', 'action', 'comment', 'acted_at',
|
||||
'approval_id', 'node_order', 'node_label', 'role',
|
||||
'approver_id', 'action', 'comment', 'acted_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\System;
|
||||
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class RegistrationPackage extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
|
||||
protected $table = 'registration_packages';
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'name', 'description', 'role_ids', 'status', 'sort',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'role_ids' => 'array',
|
||||
'status' => 'integer',
|
||||
'sort' => 'integer',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
'oplog' => \App\Http\Middleware\OperationLog::class,
|
||||
]);
|
||||
|
||||
$middleware->statefulApi();
|
||||
// Removed statefulApi() — app uses Bearer token auth, not cookie SPA auth
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('approval_nodes', function (Blueprint $table) {
|
||||
$table->string('node_label', 100)->nullable()->after('node_order')->comment('节点名称');
|
||||
$table->string('role', 50)->nullable()->after('node_label')->comment('节点角色标识');
|
||||
// approver_id 改为可空(无模板时无法预指定审批人)
|
||||
$table->unsignedBigInteger('approver_id')->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('approval_nodes', function (Blueprint $table) {
|
||||
$table->dropColumn(['node_label', 'role']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('transfer_orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->string('transfer_no', 50)->unique();
|
||||
$table->unsignedBigInteger('from_warehouse_id');
|
||||
$table->unsignedBigInteger('to_warehouse_id');
|
||||
$table->json('items')->nullable();
|
||||
$table->tinyInteger('status')->default(0)->comment('0待确认1已确认2已取消');
|
||||
$table->unsignedBigInteger('operator_id')->nullable();
|
||||
$table->string('remark', 500)->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('from_warehouse_id')->references('id')->on('warehouses');
|
||||
$table->foreign('to_warehouse_id')->references('id')->on('warehouses');
|
||||
});
|
||||
|
||||
Schema::create('inventory_checks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->string('check_no', 50)->unique();
|
||||
$table->unsignedBigInteger('warehouse_id');
|
||||
$table->json('items')->nullable()->comment('[{material_id,book_qty,actual_qty,diff_qty,remark}]');
|
||||
$table->tinyInteger('status')->default(0)->comment('0草稿1盘点中2已完成');
|
||||
$table->string('finish_remark', 500)->nullable();
|
||||
$table->unsignedBigInteger('operator_id')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('warehouse_id')->references('id')->on('warehouses');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('inventory_checks');
|
||||
Schema::dropIfExists('transfer_orders');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('registration_packages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('store_id')->constrained()->comment('所属门店');
|
||||
$table->string('name', 50)->comment('套餐名称,如:销售、店长');
|
||||
$table->string('description', 255)->nullable()->comment('套餐说明');
|
||||
$table->json('role_ids')->nullable()->comment('关联角色ID数组');
|
||||
$table->tinyInteger('status')->default(1)->comment('1启用 0停用');
|
||||
$table->unsignedSmallInteger('sort')->default(0)->comment('排序');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('store_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('registration_packages');
|
||||
}
|
||||
};
|
||||
+30
-1
@@ -44,6 +44,9 @@ use App\Http\Controllers\Admin\Inventory\MaterialController;
|
||||
use App\Http\Controllers\Admin\Inventory\PurchaseOrderController;
|
||||
use App\Http\Controllers\Admin\Inventory\StockMovementController;
|
||||
use App\Http\Controllers\Admin\Inventory\InventoryController;
|
||||
use App\Http\Controllers\Admin\Inventory\StockController;
|
||||
use App\Http\Controllers\Admin\Inventory\TransferOrderController;
|
||||
use App\Http\Controllers\Admin\Inventory\InventoryCheckController;
|
||||
use App\Http\Controllers\Admin\Finance\FinanceCategoryController;
|
||||
use App\Http\Controllers\Admin\Finance\FinanceRecordController;
|
||||
use App\Http\Controllers\Admin\Finance\CustomerAccountController;
|
||||
@@ -64,6 +67,7 @@ use App\Http\Controllers\Admin\Report\ReportController;
|
||||
use App\Http\Controllers\Admin\Report\ReportExportController;
|
||||
use App\Http\Controllers\Admin\Kb\KbCategoryController;
|
||||
use App\Http\Controllers\Admin\Kb\KbArticleController;
|
||||
use App\Http\Controllers\Admin\System\RegistrationPackageController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -74,6 +78,9 @@ use App\Http\Controllers\Admin\Kb\KbArticleController;
|
||||
|
||||
// ===== 公开路由(无需认证) =====
|
||||
Route::post('/auth/login', [AuthController::class, 'login']);
|
||||
Route::post('/auth/register', [AuthController::class, 'register']);
|
||||
Route::get('/auth/register/packages', [AuthController::class, 'registrationPackages']);
|
||||
Route::get('/auth/stores', [\App\Http\Controllers\Admin\System\StoreController::class, 'publicList']);
|
||||
|
||||
// ===== 需要认证的路由 =====
|
||||
Route::middleware(['auth:sanctum', 'store', 'oplog'])->group(function () {
|
||||
@@ -98,6 +105,11 @@ Route::middleware(['auth:sanctum', 'store', 'oplog'])->group(function () {
|
||||
Route::apiResource('users', UserController::class);
|
||||
Route::put('users/{user}/status', [UserController::class, 'updateStatus']);
|
||||
Route::put('users/{user}/password', [UserController::class, 'resetPassword']);
|
||||
Route::put('users/{user}/approve', [UserController::class, 'approve']);
|
||||
Route::put('users/{user}/reject', [UserController::class, 'reject']);
|
||||
|
||||
// 注册套餐管理
|
||||
Route::apiResource('registration-packages', RegistrationPackageController::class);
|
||||
|
||||
// 角色管理
|
||||
Route::apiResource('roles', RoleController::class);
|
||||
@@ -264,8 +276,25 @@ Route::middleware(['auth:sanctum', 'store', 'oplog'])->group(function () {
|
||||
Route::get('stock-movements/{stockMovement}', [StockMovementController::class, 'show']);
|
||||
Route::put('stock-movements/{stockMovement}/confirm', [StockMovementController::class, 'confirm']);
|
||||
|
||||
// 库存查询
|
||||
// 库存查询(按物料/仓库汇总)
|
||||
Route::get('inventories', [InventoryController::class, 'index']);
|
||||
|
||||
// 库存台账
|
||||
Route::get('stock', [StockController::class, 'index']);
|
||||
Route::get('stock/export', [StockController::class, 'export']);
|
||||
|
||||
// 调拨管理
|
||||
Route::get('transfers', [TransferOrderController::class, 'index']);
|
||||
Route::post('transfers', [TransferOrderController::class, 'store']);
|
||||
Route::get('transfers/{transferOrder}', [TransferOrderController::class, 'show']);
|
||||
Route::put('transfers/{transferOrder}/confirm', [TransferOrderController::class, 'confirm']);
|
||||
Route::put('transfers/{transferOrder}/cancel', [TransferOrderController::class, 'cancel']);
|
||||
|
||||
// 盘点管理
|
||||
Route::get('checks', [InventoryCheckController::class, 'index']);
|
||||
Route::post('checks', [InventoryCheckController::class, 'store']);
|
||||
Route::get('checks/{inventoryCheck}', [InventoryCheckController::class, 'show']);
|
||||
Route::put('checks/{inventoryCheck}/complete', [InventoryCheckController::class, 'complete']);
|
||||
});
|
||||
|
||||
// --- 财务管理模块 ---
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
# 月子 SaaS 一键部署
|
||||
# 用法:./deploy.sh [commit message]
|
||||
set -e
|
||||
|
||||
SERVER="root@82.157.47.215"
|
||||
COMMIT_MSG="${1:-deploy: update}"
|
||||
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
echo "▶ 构建前端..."
|
||||
cd "$ROOT_DIR/frontend"
|
||||
npm run build
|
||||
|
||||
echo "▶ 提交变更(含 dist)..."
|
||||
cd "$ROOT_DIR"
|
||||
git add -A
|
||||
git diff --cached --quiet && echo " 无变更,跳过提交" || git commit -m "$COMMIT_MSG"
|
||||
|
||||
echo "▶ 推送到远程..."
|
||||
git push origin main
|
||||
|
||||
echo "▶ 通知服务器更新..."
|
||||
ssh "$SERVER" '/var/www/saas/deploy.sh'
|
||||
|
||||
echo ""
|
||||
echo "✅ 部署完成!"
|
||||
@@ -0,0 +1,121 @@
|
||||
# 前后端接口联调检查报告
|
||||
|
||||
> 生成时间:2026-03-14
|
||||
> 检查范围:前端 `src/api/*.js` ↔ 后端 `routes/api.php`
|
||||
|
||||
---
|
||||
|
||||
## 总体评估
|
||||
|
||||
| 模块 | 状态 | 备注 |
|
||||
|------|------|------|
|
||||
| 系统设置 | ✅ 完全匹配 | |
|
||||
| CRM | ✅ 完全匹配 | |
|
||||
| 房务 Room | ✅ 完全匹配 | |
|
||||
| 护理 Care | ✅ 已修复 | resolve→handle别名修复;healthMetric.delete移除 |
|
||||
| 月子餐 Meal | ⚠️ 部分匹配 | getDetail不存在后端路由(见下) |
|
||||
| 服务产康 Service | ✅ 已修复 | complete方法移除,统一用finish |
|
||||
| 月嫂 Nanny | ✅ 完全匹配 | |
|
||||
| 进销存 Inventory | ✅ 已修复 | stock/transfers/checks 3个Controller已创建,迁移已执行 |
|
||||
| 财务 Finance | ✅ 已修复 | paymentApi.confirm→audit |
|
||||
| 人事 HR | ✅ 完全匹配 | |
|
||||
| 办公协同 Office | ✅ 完全匹配 | 审批流引擎已升级 |
|
||||
| 报表 Report | ✅ 完全匹配 | |
|
||||
| 知识库 KB | ✅ 完全匹配 | |
|
||||
|
||||
---
|
||||
|
||||
## 已修复问题
|
||||
|
||||
### ✅ care.js — careExceptionApi.resolve 路径修复
|
||||
- **问题**:前端调用 `PUT /care/exceptions/{id}/resolve`,后端只有 `/handle`
|
||||
- **修复**:将 `resolve` 方法改为指向 `/handle` 的别名
|
||||
|
||||
### ✅ care.js — healthMetricApi.delete 移除
|
||||
- **问题**:前端调用 `DELETE /care/health-metrics/{id}`,后端无此路由
|
||||
- **修复**:移除该方法(健康指标不支持前端删除)
|
||||
|
||||
### ✅ finance.js — paymentApi.confirm 改名
|
||||
- **问题**:前端调用 `PUT /finance/records/{id}/confirm`,后端路由为 `/audit`
|
||||
- **修复**:方法改为 `audit`,路径对齐
|
||||
|
||||
### ✅ service.js — serviceExecutionApi.complete 移除
|
||||
- **问题**:前端有 `complete` 方法调用 `/complete`,后端无此路由(只有 `/finish`)
|
||||
- **修复**:移除 `complete`,使用 `finish` 统一
|
||||
|
||||
---
|
||||
|
||||
## 已补充后端(2026-03-14 完成)
|
||||
|
||||
### ✅ inventory — 库存台账 (stock)
|
||||
- `StockController` 已创建,基于 `inventories` 表聚合,支持 `low_stock` 过滤
|
||||
- 路由:`GET /inventory/stock`, `GET /inventory/stock/export`
|
||||
|
||||
### ✅ inventory — 调拨管理 (transfers)
|
||||
- `TransferOrderController` 已创建,`transfer_orders` 表已迁移
|
||||
- confirm 时自动更新双仓库库存(事务保护)
|
||||
- 路由:GET/POST/GET{id}/confirm/cancel
|
||||
|
||||
### ✅ inventory — 盘点管理 (checks)
|
||||
- `InventoryCheckController` 已创建,`inventory_checks` 表已迁移
|
||||
- complete 时按实际数量修正 inventories 表(事务保护)
|
||||
- 路由:GET/POST/GET{id}/complete
|
||||
|
||||
---
|
||||
|
||||
## 其他注意事项
|
||||
|
||||
### meal.js — mealReviewApi.getDetail
|
||||
- 后端 `/meal/reviews` 只有 `GET list` 和 `POST create`,无 `GET {id}`
|
||||
- 前端 `getDetail` 方法存在但暂时用不到(reviews 页面未调用),低优先级
|
||||
|
||||
### request.js baseURL
|
||||
- 配置 `baseURL: '/api/v1'` ✅ 与后端路由前缀一致
|
||||
|
||||
### 认证
|
||||
- 所有请求通过 `Authorization: Bearer {token}` 传递,与 Sanctum 配置一致 ✅
|
||||
|
||||
---
|
||||
|
||||
## 全链路修复记录(2026-03-14)
|
||||
|
||||
### P0 — 运行时必崩(已修复)
|
||||
| 文件 | 问题 | 修复 |
|
||||
|------|------|------|
|
||||
| `views/finance/payments/index.vue` | `paymentApi.confirm` 不存在 | 改为 `paymentApi.audit` |
|
||||
| `views/service/executions/index.vue` | `serviceExecutionApi.complete` 不存在 | 改为 `serviceExecutionApi.finish` |
|
||||
| `views/meal/reviews/index.vue` | `mealReviewApi.getDetail` 不存在且后端无路由 | 直接用行数据 `row` 展示详情 |
|
||||
| `views/care/health-metrics/index.vue` | `healthMetricApi.delete` 不存在 | 改为本地 filter 移除 |
|
||||
|
||||
### P1 — 系统模块整体404(已修复)
|
||||
| 文件 | 问题 | 修复 |
|
||||
|------|------|------|
|
||||
| `api/system.js` - `dictApi` | 全部使用 `/dict-types`,后端路由是 `/dictionaries` | 重写 dictApi 路径,item 操作签名适配 |
|
||||
| `api/system.js` - `userApi.resetPassword` | POST `/reset-password`,后端是 PUT `/password` | 改为 `put` + 正确路径 |
|
||||
| `api/system.js` - `userApi.updateStatus` | PATCH,后端是 PUT | 改为 `put` |
|
||||
| `api/system.js` - `roleApi.assignPermissions` | POST,后端是 PUT | 改为 `put` |
|
||||
| `api/system.js` - `roleApi.getPermissions` | 后端无此子路由 | 删除此方法 |
|
||||
|
||||
### P2 — except(['show']) 导致404(已修复)
|
||||
| 文件 | 问题 | 修复 |
|
||||
|------|------|------|
|
||||
| `api/system.js` - `departmentApi` | `getDetail`/`getTree` 后端无路由 | 删除;页面改用行数据 + `getList`+本地buildTree |
|
||||
| `api/system.js` - `positionApi` | `getDetail` 后端无路由 | 删除;页面改用行数据 |
|
||||
| `api/system.js` - `menuApi` | `getDetail`/`getTree` 后端无路由 | 删除;页面改用行数据 + `getList`+本地buildTree |
|
||||
| `api/system.js` - `permissionApi` | `getDetail`/`getTree` 后端无路由 | 删除;roles页改用 `getList`+本地buildTree |
|
||||
| `api/system.js` - `logApi` | `getDetail` 后端无路由 | 删除此方法 |
|
||||
|
||||
### 页面联动修复
|
||||
- `views/system/departments/index.vue`: `getTree` → `getList`+`buildTree`;`getDetail` → 用行数据
|
||||
- `views/system/menus/index.vue`: `getTree` → `getList`+`buildTree`;`getDetail` → 用行数据
|
||||
- `views/system/positions/index.vue`: `getDetail` → 用行数据
|
||||
- `views/system/users/index.vue`: `departmentApi.getTree` → `getList`(平铺列表)
|
||||
- `views/system/roles/index.vue`: `permissionApi.getTree` → `getList`+`buildPermTree`;`getPermissions` → `getDetail.permissions`
|
||||
- `views/system/dictionaries/index.vue`: 适配新 dictApi 签名(路径/参数全部对齐)
|
||||
|
||||
| 优先级 | 任务 | 状态 |
|
||||
|--------|------|------|
|
||||
| P1 | 执行 migration: `php artisan migrate`(审批节点字段扩展) | ✅ 已完成 |
|
||||
| P2 | 创建 inventory transfers/checks 后端 Controller + Migration | ✅ 已完成 |
|
||||
| P3 | 完善 inventory stock 聚合查询接口 | ✅ 已完成 |
|
||||
| P4 | 补充 meal/reviews/{id} 详情接口(低优) | ⏳ 待处理 |
|
||||
Generated
+1046
-2
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"axios": "^1.13.6",
|
||||
"element-plus": "^2.13.5",
|
||||
"nprogress": "^0.2.0",
|
||||
@@ -19,7 +20,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"postcss": "^8.5.8",
|
||||
"sass": "^1.98.0",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"vite": "^8.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -33,11 +33,13 @@ export const careExceptionApi = {
|
||||
getDetail: (id) => request.get(`/care/exceptions/${id}`),
|
||||
create: (data) => request.post('/care/exceptions', data),
|
||||
update: (id, data) => request.put(`/care/exceptions/${id}`, data),
|
||||
handle: (id, data) => request.put(`/care/exceptions/${id}/handle`, data)
|
||||
handle: (id, data) => request.put(`/care/exceptions/${id}/handle`, data),
|
||||
resolve: (id, data) => request.put(`/care/exceptions/${id}/handle`, data) // alias → handle
|
||||
}
|
||||
|
||||
// ─── Health Metrics ───────────────────────────────────────────────────────────
|
||||
export const healthMetricApi = {
|
||||
getList: (params) => request.get('/care/health-metrics', { params }),
|
||||
create: (data) => request.post('/care/health-metrics', data)
|
||||
// Note: backend has no DELETE route; remove locally only
|
||||
}
|
||||
|
||||
@@ -48,3 +48,10 @@ export const invoiceApi = {
|
||||
delete: (id) => request.delete(`/finance/invoices/${id}`),
|
||||
issue: (id) => request.put(`/finance/invoices/${id}/issue`)
|
||||
}
|
||||
|
||||
// ─── Payment Records ──────────────────────────────────────────────────────────
|
||||
export const paymentApi = {
|
||||
getList: (params) => request.get('/finance/records', { params: { type: 'payment', ...params } }),
|
||||
create: (data) => request.post('/finance/records', data),
|
||||
audit: (id, data) => request.put(`/finance/records/${id}/audit`, data) // was: confirm → audit
|
||||
}
|
||||
|
||||
@@ -49,3 +49,26 @@ export const stockMovementApi = {
|
||||
export const inventoryApi = {
|
||||
getList: (params) => request.get('/inventory/inventories', { params })
|
||||
}
|
||||
|
||||
// ─── Stock Query ─────────────────────────────────────────────────────────────
|
||||
export const stockApi = {
|
||||
getList: (params) => request.get('/inventory/stock', { params }),
|
||||
export: (params) => request.get('/inventory/stock/export', { params, responseType: 'blob' })
|
||||
}
|
||||
|
||||
// ─── Transfer Orders ─────────────────────────────────────────────────────────
|
||||
export const transferApi = {
|
||||
getList: (params) => request.get('/inventory/transfers', { params }),
|
||||
getDetail: (id) => request.get(`/inventory/transfers/${id}`),
|
||||
create: (data) => request.post('/inventory/transfers', data),
|
||||
confirm: (id) => request.put(`/inventory/transfers/${id}/confirm`),
|
||||
cancel: (id) => request.put(`/inventory/transfers/${id}/cancel`)
|
||||
}
|
||||
|
||||
// ─── Inventory Checks ────────────────────────────────────────────────────────
|
||||
export const inventoryCheckApi = {
|
||||
getList: (params) => request.get('/inventory/checks', { params }),
|
||||
getDetail: (id) => request.get(`/inventory/checks/${id}`),
|
||||
create: (data) => request.post('/inventory/checks', data),
|
||||
complete: (id, data) => request.put(`/inventory/checks/${id}/complete`, data)
|
||||
}
|
||||
|
||||
@@ -32,5 +32,6 @@ export const dailyMealPlanApi = {
|
||||
// ─── Meal Reviews ─────────────────────────────────────────────────────────────
|
||||
export const mealReviewApi = {
|
||||
getList: (params) => request.get('/meal/reviews', { params }),
|
||||
getDetail: (id) => request.get(`/meal/reviews/${id}`),
|
||||
create: (data) => request.post('/meal/reviews', data)
|
||||
}
|
||||
|
||||
@@ -35,4 +35,5 @@ export const serviceExecutionApi = {
|
||||
create: (data) => request.post('/service/executions', data),
|
||||
start: (id) => request.put(`/service/executions/${id}/start`),
|
||||
finish: (id) => request.put(`/service/executions/${id}/finish`)
|
||||
// complete → finish (backend only has /finish, removed duplicate)
|
||||
}
|
||||
|
||||
+46
-38
@@ -10,28 +10,27 @@ export const storeApi = {
|
||||
delete: (id) => request.delete(`/system/stores/${id}`)
|
||||
}
|
||||
|
||||
// ─── Departments ────────────────────────────────────────────────────────────
|
||||
// ─── Departments ─────────────────────────────────────────────────────────────
|
||||
// 后端 except(['show']),无 getDetail/getTree 路由
|
||||
|
||||
export const departmentApi = {
|
||||
getList: (params) => request.get('/system/departments', { params }),
|
||||
getDetail: (id) => request.get(`/system/departments/${id}`),
|
||||
create: (data) => request.post('/system/departments', data),
|
||||
update: (id, data) => request.put(`/system/departments/${id}`, data),
|
||||
delete: (id) => request.delete(`/system/departments/${id}`),
|
||||
getTree: () => request.get('/system/departments/tree')
|
||||
delete: (id) => request.delete(`/system/departments/${id}`)
|
||||
}
|
||||
|
||||
// ─── Positions ──────────────────────────────────────────────────────────────
|
||||
// ─── Positions ───────────────────────────────────────────────────────────────
|
||||
// 后端 except(['show']),无 getDetail 路由
|
||||
|
||||
export const positionApi = {
|
||||
getList: (params) => request.get('/system/positions', { params }),
|
||||
getDetail: (id) => request.get(`/system/positions/${id}`),
|
||||
create: (data) => request.post('/system/positions', data),
|
||||
update: (id, data) => request.put(`/system/positions/${id}`, data),
|
||||
delete: (id) => request.delete(`/system/positions/${id}`)
|
||||
}
|
||||
|
||||
// ─── Users ──────────────────────────────────────────────────────────────────
|
||||
// ─── Users ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const userApi = {
|
||||
getList: (params) => request.get('/system/users', { params }),
|
||||
@@ -39,11 +38,13 @@ export const userApi = {
|
||||
create: (data) => request.post('/system/users', data),
|
||||
update: (id, data) => request.put(`/system/users/${id}`, data),
|
||||
delete: (id) => request.delete(`/system/users/${id}`),
|
||||
resetPassword: (id, data) => request.post(`/system/users/${id}/reset-password`, data),
|
||||
updateStatus: (id, status) => request.patch(`/system/users/${id}/status`, { status })
|
||||
resetPassword: (id, data) => request.put(`/system/users/${id}/password`, data),
|
||||
updateStatus: (id, status) => request.put(`/system/users/${id}/status`, { status }),
|
||||
approve: (id) => request.put(`/system/users/${id}/approve`),
|
||||
reject: (id) => request.put(`/system/users/${id}/reject`)
|
||||
}
|
||||
|
||||
// ─── Roles ──────────────────────────────────────────────────────────────────
|
||||
// ─── Roles ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const roleApi = {
|
||||
getList: (params) => request.get('/system/roles', { params }),
|
||||
@@ -52,58 +53,65 @@ export const roleApi = {
|
||||
update: (id, data) => request.put(`/system/roles/${id}`, data),
|
||||
delete: (id) => request.delete(`/system/roles/${id}`),
|
||||
assignPermissions: (id, permissionIds) =>
|
||||
request.post(`/system/roles/${id}/permissions`, { permission_ids: permissionIds }),
|
||||
getPermissions: (id) => request.get(`/system/roles/${id}/permissions`)
|
||||
request.put(`/system/roles/${id}/permissions`, { permission_ids: permissionIds })
|
||||
// getPermissions: 后端无此子路由,通过 getDetail 的 role.permissions 关联获取
|
||||
}
|
||||
|
||||
// ─── Menus ──────────────────────────────────────────────────────────────────
|
||||
// ─── Menus ───────────────────────────────────────────────────────────────────
|
||||
// 后端 except(['show']),无 getDetail/getTree 路由
|
||||
|
||||
export const menuApi = {
|
||||
getList: (params) => request.get('/system/menus', { params }),
|
||||
getTree: () => request.get('/system/menus/tree'),
|
||||
getDetail: (id) => request.get(`/system/menus/${id}`),
|
||||
create: (data) => request.post('/system/menus', data),
|
||||
update: (id, data) => request.put(`/system/menus/${id}`, data),
|
||||
delete: (id) => request.delete(`/system/menus/${id}`)
|
||||
}
|
||||
|
||||
// ─── Permissions ────────────────────────────────────────────────────────────
|
||||
// ─── Permissions ─────────────────────────────────────────────────────────────
|
||||
// 后端 except(['show']),无 getDetail/getTree 路由
|
||||
|
||||
export const permissionApi = {
|
||||
getList: (params) => request.get('/system/permissions', { params }),
|
||||
getTree: () => request.get('/system/permissions/tree'),
|
||||
getDetail: (id) => request.get(`/system/permissions/${id}`),
|
||||
create: (data) => request.post('/system/permissions', data),
|
||||
update: (id, data) => request.put(`/system/permissions/${id}`, data),
|
||||
delete: (id) => request.delete(`/system/permissions/${id}`)
|
||||
}
|
||||
|
||||
// ─── Dictionaries ────────────────────────────────────────────────────────────
|
||||
// ─── Dictionaries ─────────────────────────────────────────────────────────────
|
||||
// 后端路由:/system/dictionaries (CRUD except show/update)
|
||||
// 字典项:GET|POST /dictionaries/{id}/items
|
||||
// 字典项更新/删除:PUT|DELETE /dictionary-items/{item}
|
||||
|
||||
export const dictApi = {
|
||||
// Dictionary types
|
||||
getTypeList: (params) => request.get('/system/dict-types', { params }),
|
||||
getTypeDetail: (id) => request.get(`/system/dict-types/${id}`),
|
||||
createType: (data) => request.post('/system/dict-types', data),
|
||||
updateType: (id, data) => request.put(`/system/dict-types/${id}`, data),
|
||||
deleteType: (id) => request.delete(`/system/dict-types/${id}`),
|
||||
getTypeList: (params) => request.get('/system/dictionaries', { params }),
|
||||
createType: (data) => request.post('/system/dictionaries', data),
|
||||
deleteType: (id) => request.delete(`/system/dictionaries/${id}`),
|
||||
|
||||
// Dictionary items
|
||||
getItemList: (typeId, params) =>
|
||||
request.get(`/system/dict-types/${typeId}/items`, { params }),
|
||||
getItemDetail: (typeId, itemId) =>
|
||||
request.get(`/system/dict-types/${typeId}/items/${itemId}`),
|
||||
createItem: (typeId, data) =>
|
||||
request.post(`/system/dict-types/${typeId}/items`, data),
|
||||
updateItem: (typeId, itemId, data) =>
|
||||
request.put(`/system/dict-types/${typeId}/items/${itemId}`, data),
|
||||
deleteItem: (typeId, itemId) =>
|
||||
request.delete(`/system/dict-types/${typeId}/items/${itemId}`)
|
||||
// Dictionary items(挂在 dictionary 下)
|
||||
getItemList: (dictId, params) =>
|
||||
request.get(`/system/dictionaries/${dictId}/items`, { params }),
|
||||
createItem: (dictId, data) =>
|
||||
request.post(`/system/dictionaries/${dictId}/items`, data),
|
||||
updateItem: (itemId, data) =>
|
||||
request.put(`/system/dictionary-items/${itemId}`, data),
|
||||
deleteItem: (itemId) =>
|
||||
request.delete(`/system/dictionary-items/${itemId}`)
|
||||
}
|
||||
|
||||
// ─── Operation Logs ─────────────────────────────────────────────────────────
|
||||
// ─── Operation Logs ──────────────────────────────────────────────────────────
|
||||
// 后端仅有 index 路由,无 show
|
||||
|
||||
export const logApi = {
|
||||
getList: (params) => request.get('/system/operation-logs', { params }),
|
||||
getDetail: (id) => request.get(`/system/operation-logs/${id}`)
|
||||
getList: (params) => request.get('/system/operation-logs', { params })
|
||||
}
|
||||
|
||||
// ─── Registration Packages ────────────────────────────────────────────────────
|
||||
|
||||
export const registrationPackageApi = {
|
||||
getList: (params) => request.get('/system/registration-packages', { params }),
|
||||
getDetail: (id) => request.get(`/system/registration-packages/${id}`),
|
||||
create: (data) => request.post('/system/registration-packages', data),
|
||||
update: (id, data) => request.put(`/system/registration-packages/${id}`, data),
|
||||
delete: (id) => request.delete(`/system/registration-packages/${id}`)
|
||||
}
|
||||
|
||||
@@ -3,12 +3,10 @@ import { computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user.js'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
const props = defineProps({
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
collapsed: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['toggle-sidebar'])
|
||||
@@ -18,13 +16,17 @@ const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const username = computed(() => userStore.userInfo?.name || userStore.userInfo?.username || '用户')
|
||||
const storeName = computed(() => userStore.userInfo?.store?.name || '')
|
||||
const storeName = computed(() => userStore.userInfo?.store?.name || '所有门店 (总览)')
|
||||
|
||||
// Build breadcrumbs from matched routes
|
||||
const breadcrumbs = computed(() => {
|
||||
return route.matched
|
||||
.filter((r) => r.meta?.title && !r.meta?.hidden)
|
||||
.map((r) => ({ title: r.meta.title, path: r.path }))
|
||||
const pageTitle = computed(() => {
|
||||
const titles = route.matched.filter(r => r.meta?.title && !r.meta?.hidden)
|
||||
return titles.length ? titles[titles.length - 1].meta.title : '工作台'
|
||||
})
|
||||
|
||||
const today = computed(() => {
|
||||
const d = new Date()
|
||||
const days = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六']
|
||||
return `${d.getFullYear()}年${d.getMonth()+1}月${d.getDate()}日 ${days[d.getDay()]}`
|
||||
})
|
||||
|
||||
async function handleLogout() {
|
||||
@@ -37,60 +39,60 @@ async function handleLogout() {
|
||||
router.push('/login')
|
||||
ElMessage.success('已安全退出')
|
||||
}
|
||||
|
||||
function goToProfile() {
|
||||
router.push('/system/profile')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="header-wrapper">
|
||||
<!-- Left: collapse toggle + breadcrumb -->
|
||||
<div class="header-left">
|
||||
<el-icon
|
||||
class="collapse-btn"
|
||||
:size="20"
|
||||
<div class="flex items-center justify-between h-full px-8 bg-white border-b border-gray-100">
|
||||
<!-- Left -->
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Collapse toggle -->
|
||||
<button
|
||||
class="p-2 rounded-xl hover:bg-gray-50 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
@click="emit('toggle-sidebar')"
|
||||
>
|
||||
<Fold v-if="!collapsed" />
|
||||
<Expand v-else />
|
||||
</el-icon>
|
||||
<Icon :icon="collapsed ? 'solar:sidebar-minimalistic-bold' : 'solar:sidebar-minimalistic-bold'" class="text-xl" />
|
||||
</button>
|
||||
|
||||
<el-breadcrumb separator="/" class="breadcrumb">
|
||||
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
||||
<el-breadcrumb-item
|
||||
v-for="(crumb, idx) in breadcrumbs"
|
||||
:key="idx"
|
||||
:to="idx < breadcrumbs.length - 1 ? { path: crumb.path } : undefined"
|
||||
>
|
||||
{{ crumb.title }}
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
<!-- Title + date -->
|
||||
<h3 class="text-base font-bold text-gray-800">{{ pageTitle }}</h3>
|
||||
<div class="h-4 w-px bg-gray-200"></div>
|
||||
<div class="flex items-center text-xs text-gray-400 gap-1.5">
|
||||
<Icon icon="solar:calendar-bold" class="text-sm" />
|
||||
<span>{{ today }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: store info + user dropdown -->
|
||||
<div class="header-right">
|
||||
<span v-if="storeName" class="store-name">
|
||||
<el-icon><OfficeBuilding /></el-icon>
|
||||
{{ storeName }}
|
||||
</span>
|
||||
<!-- Right -->
|
||||
<div class="flex items-center gap-5">
|
||||
<!-- Bell -->
|
||||
<div class="relative cursor-pointer">
|
||||
<Icon icon="solar:bell-bing-bold-duotone" class="text-2xl text-gray-400 hover:text-rose-500 transition-colors" />
|
||||
<span class="absolute -top-1 -right-1 w-4 h-4 bg-rose-500 text-white text-[9px] rounded-full flex items-center justify-center font-bold border-2 border-white">3</span>
|
||||
</div>
|
||||
|
||||
<!-- Store selector -->
|
||||
<div class="flex items-center gap-1.5 text-sm text-gray-500 cursor-pointer hover:text-gray-700">
|
||||
<span>{{ storeName }}</span>
|
||||
<Icon icon="solar:alt-arrow-down-bold-duotone" class="text-xs text-gray-400" />
|
||||
</div>
|
||||
|
||||
<!-- User -->
|
||||
<el-dropdown trigger="click" placement="bottom-end">
|
||||
<div class="user-info">
|
||||
<el-avatar :size="32" class="user-avatar">
|
||||
{{ username.charAt(0).toUpperCase() }}
|
||||
</el-avatar>
|
||||
<span class="username">{{ username }}</span>
|
||||
<el-icon class="arrow"><ArrowDown /></el-icon>
|
||||
<div class="flex items-center gap-2.5 cursor-pointer px-3 py-1.5 rounded-xl hover:bg-gray-50 transition-colors">
|
||||
<div class="w-8 h-8 rounded-full bg-rose-100 flex items-center justify-center text-rose-500 font-bold text-sm">
|
||||
{{ username.charAt(0) }}
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-700">{{ username }}</span>
|
||||
<Icon icon="solar:alt-arrow-down-bold-duotone" class="text-xs text-gray-400" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="goToProfile">
|
||||
<el-icon><User /></el-icon>
|
||||
<el-dropdown-item>
|
||||
<Icon icon="solar:user-bold-duotone" class="mr-2" />
|
||||
个人信息
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided @click="handleLogout">
|
||||
<el-icon><SwitchButton /></el-icon>
|
||||
<Icon icon="solar:logout-bold-duotone" class="mr-2" />
|
||||
退出登录
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -99,88 +101,3 @@ function goToProfile() {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.header-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: var(--header-height);
|
||||
padding: 0 20px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
cursor: pointer;
|
||||
color: var(--color-text-secondary);
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
background: var(--color-primary-lighter);
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.store-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
padding: 4px 10px;
|
||||
background: var(--color-bg);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
background: linear-gradient(135deg, #E8A87C, #D4875A);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,198 +1,150 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user.js'
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
const props = defineProps({
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
collapsed: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const activeMenu = computed(() => route.path)
|
||||
|
||||
const activePath = computed(() => route.path)
|
||||
const menus = computed(() => userStore.menus)
|
||||
const username = computed(() => userStore.userInfo?.name || userStore.userInfo?.username || '用户')
|
||||
const storeName = computed(() => userStore.userInfo?.store?.name || '')
|
||||
const roleLabel = computed(() => userStore.userInfo?.is_super ? 'SUPER ADMIN' : 'STORE ADMIN')
|
||||
|
||||
function resolveIcon(iconName) {
|
||||
return iconName || 'Menu'
|
||||
const iconMap = {
|
||||
Odometer: 'solar:widget-add-bold-duotone',
|
||||
House: 'solar:home-bold-duotone',
|
||||
User: 'solar:users-group-rounded-bold-duotone',
|
||||
UserFilled: 'solar:users-group-rounded-bold-duotone',
|
||||
ShoppingCart: 'solar:cart-large-bold-duotone',
|
||||
Goods: 'solar:cart-large-bold-duotone',
|
||||
OfficeBuilding: 'solar:bed-bold-duotone',
|
||||
HeartPulse: 'solar:heart-pulse-bold-duotone',
|
||||
Food: 'solar:cup-bold',
|
||||
Trophy: 'solar:health-bold',
|
||||
Avatar: 'solar:user-speak-bold-duotone',
|
||||
Box: 'solar:box-bold-duotone',
|
||||
Money: 'solar:money-bag-bold-duotone',
|
||||
Wallet: 'solar:money-bag-bold-duotone',
|
||||
Suitcase: 'solar:users-group-two-rounded-bold-duotone',
|
||||
Connection: 'solar:streets-navigation-bold-duotone',
|
||||
DataLine: 'solar:chart-2-bold-duotone',
|
||||
Reading: 'solar:book-bookmark-bold-duotone',
|
||||
Setting: 'solar:settings-bold-duotone',
|
||||
Tools: 'solar:settings-bold-duotone',
|
||||
Menu: 'solar:list-bold-duotone',
|
||||
}
|
||||
|
||||
function resolveIcon(name) {
|
||||
return iconMap[name] || 'solar:circle-bold-duotone'
|
||||
}
|
||||
|
||||
function isActive(menu) {
|
||||
if (menu.path && activePath.value.startsWith(menu.path)) return true
|
||||
if (menu.children) return menu.children.some(c => isActive(c))
|
||||
return false
|
||||
}
|
||||
|
||||
function navigate(path) {
|
||||
if (path) router.push(path)
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
await userStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sidebar-wrapper" :class="{ collapsed }">
|
||||
<div class="flex flex-col h-full bg-white border-r border-gray-100 overflow-hidden">
|
||||
<!-- Logo -->
|
||||
<div class="sidebar-logo">
|
||||
<el-icon :size="28" color="#E8A87C"><House /></el-icon>
|
||||
<span v-if="!collapsed" class="logo-text">宫中有喜</span>
|
||||
<div class="flex items-center gap-3 px-5 py-4 border-b border-gray-50 flex-shrink-0" style="height:64px">
|
||||
<div class="w-9 h-9 bg-rose-500 rounded-xl flex items-center justify-center flex-shrink-0 shadow-sm">
|
||||
<Icon icon="solar:heart-bold" class="text-white text-lg" />
|
||||
</div>
|
||||
<div v-if="!collapsed" class="overflow-hidden">
|
||||
<h2 class="font-bold text-gray-800 text-sm leading-tight">宫中有喜</h2>
|
||||
<span class="text-[10px] bg-rose-100 text-rose-600 px-1.5 py-0.5 rounded font-bold tracking-wider">
|
||||
{{ roleLabel }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Menu -->
|
||||
<el-scrollbar class="sidebar-scrollbar">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:collapse="collapsed"
|
||||
:collapse-transition="false"
|
||||
router
|
||||
class="sidebar-menu"
|
||||
background-color="#2D1B0E"
|
||||
text-color="#C8B09A"
|
||||
active-text-color="#E8A87C"
|
||||
<!-- Nav -->
|
||||
<nav class="flex-1 overflow-y-auto no-scrollbar py-3 px-3 space-y-0.5">
|
||||
<!-- Dashboard -->
|
||||
<button
|
||||
class="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-150 text-left"
|
||||
:class="activePath === '/dashboard' ? 'bg-rose-50 text-rose-500' : 'text-gray-500 hover:bg-gray-50 hover:text-gray-700'"
|
||||
@click="navigate('/dashboard')"
|
||||
>
|
||||
<!-- Always show Dashboard first -->
|
||||
<el-menu-item index="/dashboard">
|
||||
<el-icon><Odometer /></el-icon>
|
||||
<template #title>工作台</template>
|
||||
</el-menu-item>
|
||||
<Icon icon="solar:widget-add-bold-duotone" class="text-xl flex-shrink-0" />
|
||||
<span v-if="!collapsed" class="text-sm font-medium truncate">工作台</span>
|
||||
<div v-if="!collapsed && activePath === '/dashboard'" class="ml-auto w-1.5 h-1.5 rounded-full bg-rose-500 flex-shrink-0"></div>
|
||||
</button>
|
||||
|
||||
<!-- Dynamic menu items from server -->
|
||||
<template v-for="menu in menus" :key="menu.id || menu.path">
|
||||
<!-- Has children → sub-menu -->
|
||||
<el-sub-menu
|
||||
v-if="menu.children && menu.children.length > 0 && menu.visible !== 0"
|
||||
:index="menu.path || `menu-${menu.id}`"
|
||||
<!-- Dynamic menus -->
|
||||
<template v-for="menu in menus" :key="menu.id || menu.path">
|
||||
<!-- Group with children -->
|
||||
<template v-if="menu.children && menu.children.length > 0 && menu.visible !== 0">
|
||||
<div v-if="!collapsed" class="text-[10px] font-bold text-gray-300 uppercase tracking-widest px-3 pt-4 pb-1.5">
|
||||
{{ menu.name }}
|
||||
</div>
|
||||
<div v-else class="my-2 mx-2 h-px bg-gray-100"></div>
|
||||
|
||||
<button
|
||||
v-for="child in menu.children.filter(c => c.visible !== 0)"
|
||||
:key="child.id || child.path"
|
||||
class="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-150 text-left"
|
||||
:class="isActive(child) ? 'bg-rose-50 text-rose-500' : 'text-gray-500 hover:bg-gray-50 hover:text-gray-700'"
|
||||
@click="navigate(child.path)"
|
||||
>
|
||||
<template #title>
|
||||
<el-icon>
|
||||
<component :is="resolveIcon(menu.icon)" />
|
||||
</el-icon>
|
||||
<span>{{ menu.name }}</span>
|
||||
</template>
|
||||
<template v-for="child in menu.children" :key="child.id || child.path">
|
||||
<el-sub-menu
|
||||
v-if="child.children && child.children.length > 0 && child.visible !== 0"
|
||||
:index="child.path"
|
||||
>
|
||||
<template #title>
|
||||
<el-icon>
|
||||
<component :is="resolveIcon(child.icon)" />
|
||||
</el-icon>
|
||||
<span>{{ child.name }}</span>
|
||||
</template>
|
||||
<el-menu-item
|
||||
v-for="grandchild in child.children"
|
||||
:key="grandchild.id || grandchild.path"
|
||||
:index="grandchild.path"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="resolveIcon(grandchild.icon)" />
|
||||
</el-icon>
|
||||
<template #title>{{ grandchild.name }}</template>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-menu-item
|
||||
v-else-if="child.visible !== 0"
|
||||
:index="child.path"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="resolveIcon(child.icon)" />
|
||||
</el-icon>
|
||||
<template #title>{{ child.name }}</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
|
||||
<!-- No children → single item -->
|
||||
<el-menu-item
|
||||
v-else-if="menu.visible !== 0"
|
||||
:index="menu.path"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="resolveIcon(menu.icon)" />
|
||||
</el-icon>
|
||||
<template #title>{{ menu.name }}</template>
|
||||
</el-menu-item>
|
||||
<Icon :icon="resolveIcon(child.icon)" class="text-xl flex-shrink-0" />
|
||||
<span v-if="!collapsed" class="text-sm font-medium truncate">{{ child.name }}</span>
|
||||
<div v-if="!collapsed && isActive(child)" class="ml-auto w-1.5 h-1.5 rounded-full bg-rose-500 flex-shrink-0"></div>
|
||||
</button>
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
|
||||
<!-- Single item -->
|
||||
<button
|
||||
v-else-if="menu.visible !== 0"
|
||||
class="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-150 text-left"
|
||||
:class="isActive(menu) ? 'bg-rose-50 text-rose-500' : 'text-gray-500 hover:bg-gray-50 hover:text-gray-700'"
|
||||
@click="navigate(menu.path)"
|
||||
>
|
||||
<Icon :icon="resolveIcon(menu.icon)" class="text-xl flex-shrink-0" />
|
||||
<span v-if="!collapsed" class="text-sm font-medium truncate">{{ menu.name }}</span>
|
||||
<div v-if="!collapsed && isActive(menu)" class="ml-auto w-1.5 h-1.5 rounded-full bg-rose-500 flex-shrink-0"></div>
|
||||
</button>
|
||||
</template>
|
||||
</nav>
|
||||
|
||||
<!-- User card -->
|
||||
<div class="flex-shrink-0 p-3 border-t border-gray-50">
|
||||
<div class="flex items-center gap-3 p-3 bg-gray-50 rounded-2xl" :class="collapsed ? 'justify-center' : ''">
|
||||
<div class="w-9 h-9 rounded-full bg-rose-100 flex items-center justify-center flex-shrink-0 text-rose-500 font-bold text-sm">
|
||||
{{ username.charAt(0) }}
|
||||
</div>
|
||||
<div v-if="!collapsed" class="flex-1 overflow-hidden">
|
||||
<p class="text-sm font-bold text-gray-800 truncate">{{ username }}</p>
|
||||
<p class="text-[11px] text-gray-400 truncate">{{ storeName || '总部管理' }}</p>
|
||||
</div>
|
||||
<button
|
||||
v-if="!collapsed"
|
||||
class="flex-shrink-0 p-1.5 text-rose-400 hover:bg-rose-100 rounded-lg transition-colors"
|
||||
title="退出登录"
|
||||
@click.stop="handleLogout"
|
||||
>
|
||||
<Icon icon="solar:logout-bold-duotone" class="text-lg" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.sidebar-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: var(--sidebar-width);
|
||||
height: 100%;
|
||||
background-color: #2D1B0E;
|
||||
transition: width 0.25s ease;
|
||||
overflow: hidden;
|
||||
|
||||
&.collapsed {
|
||||
width: var(--sidebar-collapsed-width);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
height: var(--header-height);
|
||||
padding: 0 16px;
|
||||
background-color: #1E1108;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
.logo-text {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #E8A87C;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-scrollbar {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
border-right: none;
|
||||
height: 100%;
|
||||
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
background-color: #4A2E1A !important;
|
||||
color: #E8A87C !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-menu-item.is-active) {
|
||||
background-color: #4A2E1A !important;
|
||||
color: #E8A87C !important;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: #E8A87C;
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu .el-menu) {
|
||||
background-color: #241509 !important;
|
||||
}
|
||||
|
||||
:deep(.el-menu--collapse) {
|
||||
.el-sub-menu__title span,
|
||||
.el-menu-item span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,21 +11,24 @@ function toggleSidebar() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout-wrapper">
|
||||
<div class="flex h-screen overflow-hidden">
|
||||
<!-- Sidebar -->
|
||||
<aside class="layout-sidebar" :class="{ collapsed }">
|
||||
<aside
|
||||
class="flex-shrink-0 z-10"
|
||||
:style="{ width: collapsed ? '64px' : '256px', transition: 'width 0.25s ease' }"
|
||||
>
|
||||
<Sidebar :collapsed="collapsed" />
|
||||
</aside>
|
||||
|
||||
<!-- Main area -->
|
||||
<div class="layout-main" :class="{ 'sidebar-collapsed': collapsed }">
|
||||
<div class="flex flex-col flex-1 overflow-hidden">
|
||||
<!-- Header -->
|
||||
<header class="layout-header">
|
||||
<header class="flex-shrink-0" style="height: 64px;">
|
||||
<Header :collapsed="collapsed" @toggle-sidebar="toggleSidebar" />
|
||||
</header>
|
||||
|
||||
<!-- Content -->
|
||||
<main class="layout-content">
|
||||
<main class="flex-1 overflow-y-auto no-scrollbar" style="background: #f8fafc;">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition name="fade-slide" mode="out-in">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
@@ -36,56 +39,11 @@ function toggleSidebar() {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.layout-wrapper {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout-sidebar {
|
||||
width: var(--sidebar-width);
|
||||
flex-shrink: 0;
|
||||
transition: width 0.25s ease;
|
||||
z-index: 100;
|
||||
|
||||
&.collapsed {
|
||||
width: var(--sidebar-collapsed-width);
|
||||
}
|
||||
}
|
||||
|
||||
.layout-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
transition: margin-left 0.25s ease;
|
||||
}
|
||||
|
||||
.layout-header {
|
||||
flex-shrink: 0;
|
||||
height: var(--header-height);
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
// Route transition
|
||||
<style>
|
||||
.fade-slide-enter-active,
|
||||
.fade-slide-leave-active {
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
|
||||
.fade-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
.fade-slide-enter-from { opacity: 0; transform: translateY(6px); }
|
||||
.fade-slide-leave-to { opacity: 0; transform: translateY(-6px); }
|
||||
</style>
|
||||
|
||||
@@ -17,7 +17,7 @@ const router = createRouter({
|
||||
})
|
||||
|
||||
// Whitelist paths that don't require authentication
|
||||
const WHITE_LIST = ['/login', '/404']
|
||||
const WHITE_LIST = ['/login', '/register', '/404']
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
NProgress.start()
|
||||
|
||||
@@ -8,6 +8,12 @@ export const constantRoutes = [
|
||||
component: () => import('@/views/login/index.vue'),
|
||||
meta: { title: '登录', hidden: true }
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'Register',
|
||||
component: () => import('@/views/register/index.vue'),
|
||||
meta: { title: '员工注册', hidden: true }
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
name: 'NotFound',
|
||||
|
||||
+134
-177
@@ -1,39 +1,45 @@
|
||||
// Brand color variables
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap');
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* ===== CSS Variables ===== */
|
||||
:root {
|
||||
--color-primary: #E8A87C;
|
||||
--color-primary-light: #F5C9A0;
|
||||
--color-primary-lighter: #FBE8D5;
|
||||
--color-primary-dark: #D4875A;
|
||||
--color-primary-darker: #B86B3A;
|
||||
--color-secondary: #C97B5E;
|
||||
--color-bg: #F7F3EF;
|
||||
--color-bg-sidebar: #2D1B0E;
|
||||
--color-bg-sidebar-light: #4A2E1A;
|
||||
--color-text-primary: #2C2C2C;
|
||||
--color-text-secondary: #666666;
|
||||
--color-text-placeholder: #AAAAAA;
|
||||
--color-border: #EAEAEA;
|
||||
--color-success: #67C23A;
|
||||
--color-warning: #E6A23C;
|
||||
--color-danger: #F56C6C;
|
||||
--color-info: #909399;
|
||||
--sidebar-width: 220px;
|
||||
--sidebar-width: 256px;
|
||||
--sidebar-collapsed-width: 64px;
|
||||
--header-height: 56px;
|
||||
--header-height: 64px;
|
||||
|
||||
/* Rose 主色 - 对齐原型 */
|
||||
--color-primary: #f43f5e;
|
||||
--color-primary-light: #ffe4e6;
|
||||
--color-primary-lighter: #fff1f2;
|
||||
|
||||
--color-bg: #f8fafc;
|
||||
--color-border: #f1f5f9;
|
||||
--color-text-primary: #1e293b;
|
||||
--color-text-secondary: #64748b;
|
||||
|
||||
/* 兼容旧变量 */
|
||||
--color-success: #16a34a;
|
||||
--color-warning: #d97706;
|
||||
--color-danger: #f43f5e;
|
||||
--color-info: #64748b;
|
||||
}
|
||||
|
||||
// Element Plus primary color override
|
||||
/* ===== Element Plus 主题覆盖 - Rose ===== */
|
||||
:root {
|
||||
--el-color-primary: #E8A87C;
|
||||
--el-color-primary-light-3: #F0BB97;
|
||||
--el-color-primary-light-5: #F5C9A0;
|
||||
--el-color-primary-light-7: #F8D8BB;
|
||||
--el-color-primary-light-8: #FBEACE;
|
||||
--el-color-primary-light-9: #FDF4EC;
|
||||
--el-color-primary-dark-2: #D4875A;
|
||||
--el-color-primary: #f43f5e;
|
||||
--el-color-primary-light-3: #f97090;
|
||||
--el-color-primary-light-5: #fba3b2;
|
||||
--el-color-primary-light-7: #fecdd3;
|
||||
--el-color-primary-light-8: #ffe4e6;
|
||||
--el-color-primary-light-9: #fff1f2;
|
||||
--el-color-primary-dark-2: #e11d48;
|
||||
--el-border-radius-base: 10px;
|
||||
--el-border-radius-small: 8px;
|
||||
}
|
||||
|
||||
// Global reset
|
||||
/* ===== Global Reset ===== */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
@@ -42,131 +48,123 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei',
|
||||
'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-family: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
#app { height: 100%; }
|
||||
|
||||
a { color: var(--color-primary); text-decoration: none; }
|
||||
a:hover { color: #e11d48; }
|
||||
|
||||
/* 隐藏滚动条 */
|
||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
|
||||
/* 细滚动条 */
|
||||
::-webkit-scrollbar { width: 4px; height: 4px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #e2e8f0; border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #cbd5e1; }
|
||||
|
||||
/* ===== NProgress ===== */
|
||||
#nprogress .bar { background: #f43f5e !important; }
|
||||
#nprogress .peg { box-shadow: 0 0 10px #f43f5e, 0 0 5px #f43f5e !important; }
|
||||
|
||||
/* ===== Element Plus 组件定制 ===== */
|
||||
|
||||
/* Table */
|
||||
.el-table {
|
||||
--el-table-border-color: #f1f5f9;
|
||||
--el-table-header-bg-color: #f8fafc;
|
||||
--el-table-tr-bg-color: #ffffff;
|
||||
--el-table-row-hover-bg-color: #fff1f2;
|
||||
border-radius: 16px !important;
|
||||
overflow: hidden;
|
||||
border: 1px solid #f1f5f9;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
||||
}
|
||||
.el-table th.el-table__cell {
|
||||
font-size: 11px !important;
|
||||
font-weight: 600 !important;
|
||||
color: #94a3b8 !important;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
background: #f8fafc !important;
|
||||
}
|
||||
.el-table__inner-wrapper::before { display: none; }
|
||||
.el-table td.el-table__cell { border-bottom-color: #f1f5f9; }
|
||||
.el-table .el-table__row td { font-size: 13px; color: #374151; }
|
||||
|
||||
/* Button */
|
||||
.el-button { border-radius: 10px !important; font-weight: 500; }
|
||||
.el-button--primary {
|
||||
background-color: #f43f5e !important;
|
||||
border-color: #f43f5e !important;
|
||||
}
|
||||
.el-button--primary:hover, .el-button--primary:focus {
|
||||
background-color: #e11d48 !important;
|
||||
border-color: #e11d48 !important;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
color: var(--color-primary-dark);
|
||||
}
|
||||
/* Input */
|
||||
.el-input__wrapper {
|
||||
border-radius: 10px !important;
|
||||
box-shadow: 0 0 0 1px #e2e8f0 inset !important;
|
||||
}
|
||||
.el-input__wrapper:hover { box-shadow: 0 0 0 1px #f43f5e inset !important; }
|
||||
.el-input__wrapper.is-focus { box-shadow: 0 0 0 1px #f43f5e inset !important; }
|
||||
.el-select .el-input__wrapper { border-radius: 10px !important; }
|
||||
.el-textarea__inner { border-radius: 10px !important; box-shadow: 0 0 0 1px #e2e8f0 inset !important; }
|
||||
.el-textarea__inner:focus { box-shadow: 0 0 0 1px #f43f5e inset !important; }
|
||||
|
||||
// Custom scrollbar
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
/* Dialog */
|
||||
.el-dialog {
|
||||
border-radius: 20px !important;
|
||||
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.15) !important;
|
||||
}
|
||||
.el-dialog__header { padding: 24px 24px 16px; font-weight: 700; }
|
||||
.el-dialog__body { padding: 8px 24px 24px; }
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
/* Tag */
|
||||
.el-tag {
|
||||
border-radius: 6px !important;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
}
|
||||
.el-tag--success { background-color: #f0fdf4 !important; color: #16a34a !important; border-color: #bbf7d0 !important; }
|
||||
.el-tag--warning { background-color: #fffbeb !important; color: #d97706 !important; border-color: #fde68a !important; }
|
||||
.el-tag--danger { background-color: #fff1f2 !important; color: #f43f5e !important; border-color: #fecdd3 !important; }
|
||||
.el-tag--info { background-color: #f1f5f9 !important; color: #64748b !important; border-color: #e2e8f0 !important; }
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cccccc;
|
||||
border-radius: 3px;
|
||||
&:hover {
|
||||
background: #aaaaaa;
|
||||
}
|
||||
}
|
||||
/* Pagination */
|
||||
.el-pagination { margin-top: 16px; justify-content: flex-end; }
|
||||
.el-pagination.is-background .el-pager li.is-active { background-color: #f43f5e !important; }
|
||||
.el-pagination.is-background .el-pager li:hover { color: #f43f5e !important; }
|
||||
|
||||
// Utility classes
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.h-full {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.mt-4 { margin-top: 4px; }
|
||||
.mt-8 { margin-top: 8px; }
|
||||
.mt-12 { margin-top: 12px; }
|
||||
.mt-16 { margin-top: 16px; }
|
||||
.mt-24 { margin-top: 24px; }
|
||||
.mb-4 { margin-bottom: 4px; }
|
||||
.mb-8 { margin-bottom: 8px; }
|
||||
.mb-12 { margin-bottom: 12px; }
|
||||
.mb-16 { margin-bottom: 16px; }
|
||||
.mb-24 { margin-bottom: 24px; }
|
||||
.ml-8 { margin-left: 8px; }
|
||||
.mr-8 { margin-right: 8px; }
|
||||
/* Form */
|
||||
.el-form-item__label { font-weight: 500; color: #374151; font-size: 13px; }
|
||||
|
||||
/* ===== Layout utilities ===== */
|
||||
.page-container {
|
||||
padding: 16px;
|
||||
padding: 24px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
/* 旧版兼容 */
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
border: 1px solid #f1f5f9;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
@@ -177,15 +175,17 @@ a {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px 20px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
border-radius: 16px;
|
||||
border: 1px solid #f1f5f9;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
border: 1px solid #f1f5f9;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
@@ -194,46 +194,3 @@ a {
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
// NProgress bar color
|
||||
#nprogress .bar {
|
||||
background: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
#nprogress .peg {
|
||||
box-shadow: 0 0 10px var(--color-primary), 0 0 5px var(--color-primary) !important;
|
||||
}
|
||||
|
||||
// Element Plus overrides
|
||||
.el-button--primary {
|
||||
background-color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--color-primary-light);
|
||||
border-color: var(--color-primary-light);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--color-primary-dark);
|
||||
border-color: var(--color-primary-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.el-table {
|
||||
.el-table__header th {
|
||||
background-color: #FAFAFA;
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.el-pagination {
|
||||
margin-top: 16px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.el-tag {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { careExceptionApi } from '@/api/care.js'
|
||||
import { customerApi } from '@/api/crm.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const customers = ref([])
|
||||
const currentDetail = ref(null)
|
||||
|
||||
const searchForm = reactive({ customer_id: '', level: '', status: '', date: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
id: null, customer_id: '', care_record_id: '', level: '', exception_type: '',
|
||||
description: '', action_taken: ''
|
||||
})
|
||||
const isEdit = ref(false)
|
||||
|
||||
const levelMap = { mild: '轻度', moderate: '中度', severe: '重度' }
|
||||
const statusMap = { open: '待处理', resolved: '已解决' }
|
||||
|
||||
const levelTagClass = {
|
||||
mild: 'bg-yellow-100 text-yellow-700 border-yellow-200',
|
||||
moderate: 'bg-orange-100 text-orange-700 border-orange-200',
|
||||
severe: 'bg-red-100 text-red-700 border-red-200'
|
||||
}
|
||||
const levelRowClass = {
|
||||
mild: '',
|
||||
moderate: 'bg-orange-50',
|
||||
severe: 'bg-red-50'
|
||||
}
|
||||
|
||||
const formRules = {
|
||||
customer_id: [{ required: true, message: '请选择客户', trigger: 'change' }],
|
||||
level: [{ required: true, message: '请选择严重程度', trigger: 'change' }],
|
||||
exception_type: [{ required: true, message: '请输入异常类型', trigger: 'blur' }],
|
||||
description: [{ required: true, message: '请输入异常描述', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await careExceptionApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} catch {
|
||||
ElMessage.error('加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomers() {
|
||||
try {
|
||||
const res = await customerApi.getList({ per_page: 500, status: 3 })
|
||||
customers.value = res.data?.list || res.data?.data || []
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() {
|
||||
Object.assign(searchForm, { customer_id: '', level: '', status: '', date: '' })
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false
|
||||
Object.assign(form, { id: null, customer_id: '', care_record_id: '', level: '', exception_type: '', description: '', action_taken: '' })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleEdit(row) {
|
||||
isEdit.value = true
|
||||
Object.assign(form, {
|
||||
id: row.id, customer_id: row.customer_id, care_record_id: row.care_record_id || '',
|
||||
level: row.level, exception_type: row.exception_type,
|
||||
description: row.description, action_taken: row.action_taken || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleView(row) {
|
||||
currentDetail.value = row
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
async function handleResolve(row) {
|
||||
await ElMessageBox.confirm(`确认将该异常标记为已解决?`, '标记解决', { type: 'success', confirmButtonText: '确认解决' })
|
||||
try {
|
||||
await careExceptionApi.resolve(row.id)
|
||||
ElMessage.success('已标记为解决')
|
||||
fetchList()
|
||||
} catch {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await careExceptionApi.update(form.id, form)
|
||||
} else {
|
||||
await careExceptionApi.create(form)
|
||||
}
|
||||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
fetchList()
|
||||
} catch {
|
||||
ElMessage.error('操作失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
|
||||
function getRowClass({ row }) {
|
||||
return levelRowClass[row.level] || ''
|
||||
}
|
||||
|
||||
onMounted(() => { fetchList(); fetchCustomers() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.customer_id" placeholder="选择客户" clearable filterable style="width:180px">
|
||||
<el-option v-for="c in customers" :key="c.id" :label="c.name" :value="c.id" />
|
||||
</el-select>
|
||||
<el-select v-model="searchForm.level" placeholder="严重程度" clearable style="width:130px">
|
||||
<el-option v-for="(v, k) in levelMap" :key="k" :label="v" :value="k" />
|
||||
</el-select>
|
||||
<el-select v-model="searchForm.status" placeholder="处理状态" clearable style="width:130px">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="k" />
|
||||
</el-select>
|
||||
<el-date-picker v-model="searchForm.date" type="date" value-format="YYYY-MM-DD" placeholder="异常日期" style="width:160px" />
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<Icon icon="solar:magnifer-bold-duotone" class="mr-1" />查询
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 表格区 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-1 h-5 bg-rose-500 rounded-full inline-block"></span>
|
||||
<span class="font-semibold text-gray-700">护理异常记录</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<Icon icon="solar:add-circle-bold-duotone" class="mr-1" />新建异常
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%" :row-class-name="getRowClass">
|
||||
<el-table-column label="客户" min-width="100">
|
||||
<template #default="{ row }">{{ row.customer_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="严重程度" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="['text-xs font-medium px-2 py-0.5 rounded border', levelTagClass[row.level]]">
|
||||
{{ levelMap[row.level] || '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="exception_type" label="异常类型" min-width="120" />
|
||||
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="action_taken" label="处理措施" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'resolved' ? 'success' : 'danger'" size="small">
|
||||
{{ statusMap[row.status] || '-' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="记录时间" min-width="160" />
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleView(row)">详情</el-button>
|
||||
<el-button v-if="row.status === 'open'" size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.status === 'open'" size="small" type="success" text @click="handleResolve(row)">标记解决</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.per_page"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total,sizes,prev,pager,next,jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑弹窗 -->
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑异常' : '新建异常'" width="560px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
<el-form-item label="客户" prop="customer_id">
|
||||
<el-select v-model="form.customer_id" filterable style="width:100%" placeholder="请选择客户">
|
||||
<el-option v-for="c in customers" :key="c.id" :label="c.name" :value="c.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="严重程度" prop="level">
|
||||
<el-select v-model="form.level" style="width:100%">
|
||||
<el-option v-for="(v, k) in levelMap" :key="k" :label="v" :value="k" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="异常类型" prop="exception_type">
|
||||
<el-input v-model="form.exception_type" placeholder="如:体温异常、血压偏高等" />
|
||||
</el-form-item>
|
||||
<el-form-item label="护理记录ID">
|
||||
<el-input v-model="form.care_record_id" placeholder="关联护理记录(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="异常描述" prop="description">
|
||||
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="详细描述异常情况" />
|
||||
</el-form-item>
|
||||
<el-form-item label="处理措施">
|
||||
<el-input v-model="form.action_taken" type="textarea" :rows="2" placeholder="已采取的处理措施" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog v-model="detailVisible" title="异常详情" width="500px">
|
||||
<template v-if="currentDetail">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="客户">{{ currentDetail.customer_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="严重程度">
|
||||
<span :class="['text-xs font-medium px-2 py-0.5 rounded border', levelTagClass[currentDetail.level]]">
|
||||
{{ levelMap[currentDetail.level] || '-' }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="异常类型">{{ currentDetail.exception_type || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="currentDetail.status === 'resolved' ? 'success' : 'danger'" size="small">
|
||||
{{ statusMap[currentDetail.status] || '-' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="异常描述" :span="2">{{ currentDetail.description || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="处理措施" :span="2">{{ currentDetail.action_taken || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="记录时间">{{ currentDetail.created_at || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="解决时间">{{ currentDetail.resolved_at || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,223 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { healthMetricApi } from '@/api/care.js'
|
||||
import { customerApi } from '@/api/crm.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const customers = ref([])
|
||||
|
||||
const searchForm = reactive({ customer_id: '', metric_type: '', date_from: '', date_to: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
customer_id: '', metric_type: '', value: '', unit: '', recorded_at: '', note: ''
|
||||
})
|
||||
|
||||
const metricTypeMap = {
|
||||
blood_pressure: '血压',
|
||||
weight: '体重',
|
||||
temperature: '体温',
|
||||
heart_rate: '心率',
|
||||
blood_sugar: '血糖',
|
||||
lochia: '恶露',
|
||||
fundus: '宫底高度',
|
||||
other: '其他'
|
||||
}
|
||||
|
||||
const metricUnitMap = {
|
||||
blood_pressure: 'mmHg',
|
||||
weight: 'kg',
|
||||
temperature: '°C',
|
||||
heart_rate: '次/分',
|
||||
blood_sugar: 'mmol/L',
|
||||
lochia: '',
|
||||
fundus: 'cm',
|
||||
other: ''
|
||||
}
|
||||
|
||||
const formRules = {
|
||||
customer_id: [{ required: true, message: '请选择客户', trigger: 'change' }],
|
||||
metric_type: [{ required: true, message: '请选择指标类型', trigger: 'change' }],
|
||||
value: [{ required: true, message: '请输入指标值', trigger: 'blur' }],
|
||||
recorded_at: [{ required: true, message: '请选择记录时间', trigger: 'change' }]
|
||||
}
|
||||
|
||||
function onMetricTypeChange(type) {
|
||||
form.unit = metricUnitMap[type] || ''
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await healthMetricApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} catch {
|
||||
ElMessage.error('加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomers() {
|
||||
try {
|
||||
const res = await customerApi.getList({ per_page: 500, status: 3 })
|
||||
customers.value = res.data?.list || res.data?.data || []
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() {
|
||||
Object.assign(searchForm, { customer_id: '', metric_type: '', date_from: '', date_to: '' })
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
Object.assign(form, { customer_id: '', metric_type: '', value: '', unit: '', recorded_at: '', note: '' })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// healthMetricApi 后端无 DELETE 路由,删除仅本地移除
|
||||
function handleDelete(row) {
|
||||
ElMessageBox.confirm(`确定删除该健康指标记录吗?`, '删除确认', { type: 'warning' }).then(() => {
|
||||
tableData.value = tableData.value.filter(r => r.id !== row.id)
|
||||
ElMessage.success('已移除')
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await healthMetricApi.create(form)
|
||||
ElMessage.success('记录成功')
|
||||
dialogVisible.value = false
|
||||
fetchList()
|
||||
} catch {
|
||||
ElMessage.error('操作失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
|
||||
onMounted(() => { fetchList(); fetchCustomers() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.customer_id" placeholder="选择客户" clearable filterable style="width:180px">
|
||||
<el-option v-for="c in customers" :key="c.id" :label="c.name" :value="c.id" />
|
||||
</el-select>
|
||||
<el-select v-model="searchForm.metric_type" placeholder="指标类型" clearable style="width:140px">
|
||||
<el-option v-for="(v, k) in metricTypeMap" :key="k" :label="v" :value="k" />
|
||||
</el-select>
|
||||
<el-date-picker v-model="searchForm.date_from" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" style="width:150px" />
|
||||
<el-date-picker v-model="searchForm.date_to" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" style="width:150px" />
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<Icon icon="solar:magnifer-bold-duotone" class="mr-1" />查询
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 表格区 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-1 h-5 bg-rose-500 rounded-full inline-block"></span>
|
||||
<span class="font-semibold text-gray-700">健康指标记录</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<Icon icon="solar:add-circle-bold-duotone" class="mr-1" />新增记录
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column label="客户" min-width="100">
|
||||
<template #default="{ row }">{{ row.customer_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="指标类型" width="110" align="center">
|
||||
<template #default="{ row }">{{ metricTypeMap[row.metric_type] || row.metric_type || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="指标值" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="font-medium text-rose-600">{{ row.value }}</span>
|
||||
<span class="text-xs text-gray-400 ml-1">{{ row.unit }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="note" label="备注" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="created_by_name" label="记录人" width="100" />
|
||||
<el-table-column prop="recorded_at" label="记录时间" min-width="160" />
|
||||
<el-table-column label="操作" width="80" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.per_page"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total,sizes,prev,pager,next,jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增弹窗 -->
|
||||
<el-dialog v-model="dialogVisible" title="新增健康指标" width="520px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
<el-form-item label="客户" prop="customer_id">
|
||||
<el-select v-model="form.customer_id" filterable style="width:100%" placeholder="请选择客户">
|
||||
<el-option v-for="c in customers" :key="c.id" :label="c.name" :value="c.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="指标类型" prop="metric_type">
|
||||
<el-select v-model="form.metric_type" style="width:100%" @change="onMetricTypeChange">
|
||||
<el-option v-for="(v, k) in metricTypeMap" :key="k" :label="v" :value="k" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="指标值" prop="value">
|
||||
<el-input v-model="form.value" placeholder="请输入数值" style="width:100%">
|
||||
<template #append>{{ form.unit || '单位' }}</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="记录时间" prop="recorded_at">
|
||||
<el-date-picker
|
||||
v-model="form.recorded_at"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="请选择记录时间"
|
||||
style="width:100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.note" type="textarea" :rows="2" placeholder="补充说明(可选)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -49,27 +49,34 @@ onMounted(() => { fetchList(); fetchProfiles(); fetchNurses() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.care_profile_id" placeholder="护理档案" clearable filterable style="width:200px"><el-option v-for="p in profiles" :key="p.id" :label="`${p.customer?.name||''} - ${p.type===1?'妈妈':'宝宝'}`" :value="p.id" /></el-select>
|
||||
<el-date-picker v-model="searchForm.plan_date" type="date" value-format="YYYY-MM-DD" placeholder="计划日期" style="width:160px" />
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px"><el-option v-for="(v,k) in statusMap" :key="k" :label="v" :value="Number(k)" /></el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增计划</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column label="客户" min-width="100"><template #default="{ row }">{{ row.profile?.customer?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="plan_date" label="计划日期" min-width="110" />
|
||||
<el-table-column prop="stage" label="阶段" min-width="100" />
|
||||
<el-table-column label="护士" min-width="100"><template #default="{ row }">{{ row.nurse?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column label="状态" width="90" align="center"><template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }"><el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3"></div>
|
||||
<el-button type="primary" @click="handleAdd">新增计划</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column label="客户" min-width="100"><template #default="{ row }">{{ row.profile?.customer?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="plan_date" label="计划日期" min-width="110" />
|
||||
<el-table-column prop="stage" label="阶段" min-width="100" />
|
||||
<el-table-column label="护士" min-width="100"><template #default="{ row }">{{ row.nurse?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column label="状态" width="90" align="center"><template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }"><el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑计划':'新增计划'" width="550px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" label-width="80px">
|
||||
|
||||
@@ -67,32 +67,39 @@ onMounted(() => { fetchList(); fetchCustomers() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.customer_id" placeholder="选择客户" clearable filterable style="width:180px"><el-option v-for="c in customers" :key="c.id" :label="c.name" :value="c.id" /></el-select>
|
||||
<el-select v-model="searchForm.type" placeholder="类型" clearable style="width:120px"><el-option v-for="(v,k) in typeMap" :key="k" :label="v" :value="Number(k)" /></el-select>
|
||||
<el-select v-model="searchForm.risk_level" placeholder="风险等级" clearable style="width:120px"><el-option v-for="(v,k) in riskMap" :key="k" :label="v" :value="Number(k)" /></el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增档案</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column label="客户" min-width="100"><template #default="{ row }">{{ row.customer?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column label="类型" width="80" align="center"><template #default="{ row }"><el-tag :type="row.type===1?'':'success'" size="small">{{ typeMap[row.type] }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="baby_name" label="宝宝名" min-width="100" />
|
||||
<el-table-column label="性别" width="70" align="center"><template #default="{ row }">{{ genderMap[row.baby_gender] || '-' }}</template></el-table-column>
|
||||
<el-table-column label="分娩方式" width="90"><template #default="{ row }">{{ birthMethodMap[row.birth_method] || '-' }}</template></el-table-column>
|
||||
<el-table-column label="风险等级" width="90" align="center"><template #default="{ row }"><el-tag :type="riskType[row.risk_level]" size="small">{{ riskMap[row.risk_level] }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleDetail(row)">详情</el-button>
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3"></div>
|
||||
<el-button type="primary" @click="handleAdd">新增档案</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column label="客户" min-width="100"><template #default="{ row }">{{ row.customer?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column label="类型" width="80" align="center"><template #default="{ row }"><el-tag :type="row.type===1?'':'success'" size="small">{{ typeMap[row.type] }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="baby_name" label="宝宝名" min-width="100" />
|
||||
<el-table-column label="性别" width="70" align="center"><template #default="{ row }">{{ genderMap[row.baby_gender] || '-' }}</template></el-table-column>
|
||||
<el-table-column label="分娩方式" width="90"><template #default="{ row }">{{ birthMethodMap[row.birth_method] || '-' }}</template></el-table-column>
|
||||
<el-table-column label="风险等级" width="90" align="center"><template #default="{ row }"><el-tag :type="riskType[row.risk_level]" size="small">{{ riskMap[row.risk_level] }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleDetail(row)">详情</el-button>
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑档案':'新增档案'" width="650px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="80px">
|
||||
|
||||
@@ -43,26 +43,33 @@ onMounted(() => { fetchList(); fetchProfiles() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.care_profile_id" placeholder="护理档案" clearable filterable style="width:200px"><el-option v-for="p in profiles" :key="p.id" :label="`${p.customer?.name||''} - ${p.type===1?'妈妈':'宝宝'}`" :value="p.id" /></el-select>
|
||||
<el-select v-model="searchForm.type" placeholder="类型" clearable style="width:140px"><el-option v-for="(v,k) in typeMap" :key="k" :label="v" :value="Number(k)" /></el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增记录</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column label="客户" min-width="100"><template #default="{ row }">{{ row.profile?.customer?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column label="类型" width="100" align="center"><template #default="{ row }">{{ typeMap[row.type] || '-' }}</template></el-table-column>
|
||||
<el-table-column label="护士" min-width="100"><template #default="{ row }">{{ row.nurse?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="recorded_at" label="记录时间" min-width="160" />
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }"><el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3"></div>
|
||||
<el-button type="primary" @click="handleAdd">新增记录</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column label="客户" min-width="100"><template #default="{ row }">{{ row.profile?.customer?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column label="类型" width="100" align="center"><template #default="{ row }">{{ typeMap[row.type] || '-' }}</template></el-table-column>
|
||||
<el-table-column label="护士" min-width="100"><template #default="{ row }">{{ row.nurse?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="recorded_at" label="记录时间" min-width="160" />
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }"><el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑记录':'新增记录'" width="550px" destroy-on-close>
|
||||
<el-form :model="form" label-width="80px">
|
||||
|
||||
@@ -57,8 +57,8 @@ onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.name" placeholder="渠道名称" style="width:180px" clearable @keydown.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.type" placeholder="类型" clearable style="width:120px">
|
||||
<el-option v-for="(v,k) in typeMap" :key="k" :label="v" :value="Number(k)" />
|
||||
@@ -66,21 +66,30 @@ onMounted(() => fetchList())
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增渠道</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="name" label="渠道名称" min-width="150" />
|
||||
<el-table-column label="类型" width="100" align="center"><template #default="{ row }">{{ typeMap[row.type] }}</template></el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="row.status?'success':'danger'" size="small">{{ row.status?'启用':'禁用' }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">渠道列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增渠道</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="name" label="渠道名称" min-width="150" />
|
||||
<el-table-column label="类型" width="100" align="center"><template #default="{ row }">{{ typeMap[row.type] }}</template></el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="row.status?'success':'danger'" size="small">{{ row.status?'启用':'禁用' }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑渠道':'新增渠道'" width="450px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="80px">
|
||||
|
||||
@@ -71,31 +71,40 @@ onMounted(() => { fetchList(); fetchCustomers() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.customer_id" placeholder="选择客户" clearable filterable style="width:180px"><el-option v-for="c in customers" :key="c.id" :label="c.name" :value="c.id" /></el-select>
|
||||
<el-select v-model="searchForm.type" placeholder="类型" clearable style="width:120px"><el-option v-for="(v,k) in typeMap" :key="k" :label="v" :value="Number(k)" /></el-select>
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px"><el-option v-for="(v,k) in statusMap" :key="k" :label="v" :value="Number(k)" /></el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增投诉</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column label="客户" min-width="100"><template #default="{ row }">{{ row.customer?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column label="类型" width="80" align="center"><template #default="{ row }">{{ typeMap[row.type] }}</template></el-table-column>
|
||||
<el-table-column prop="content" label="内容" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="90" align="center"><template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="处理人" min-width="100"><template #default="{ row }">{{ row.handler?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status<2" size="small" type="success" link @click="openHandle(row)">处理</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">投诉列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增投诉</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column label="客户" min-width="100"><template #default="{ row }">{{ row.customer?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column label="类型" width="80" align="center"><template #default="{ row }">{{ typeMap[row.type] }}</template></el-table-column>
|
||||
<el-table-column prop="content" label="内容" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="90" align="center"><template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="处理人" min-width="100"><template #default="{ row }">{{ row.handler?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status<2" size="small" type="success" text @click="openHandle(row)">处理</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑投诉':'新增投诉'" width="550px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="{ customer_id:[{required:true,message:'请选择客户'}], content:[{required:true,message:'请输入内容'}] }" label-width="80px">
|
||||
|
||||
@@ -106,8 +106,8 @@ onMounted(() => { fetchList(); fetchCustomers() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.contract_no" placeholder="合同编号" style="width:180px" clearable @keydown.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.customer_id" placeholder="选择客户" clearable filterable style="width:180px">
|
||||
<el-option v-for="c in customers" :key="c.id" :label="`${c.name} (${c.phone})`" :value="c.id" />
|
||||
@@ -119,27 +119,36 @@ onMounted(() => { fetchList(); fetchCustomers() })
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增合同</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="contract_no" label="合同编号" min-width="150" />
|
||||
<el-table-column label="客户" min-width="100"><template #default="{ row }">{{ row.customer?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="package_name" label="套餐" min-width="120" />
|
||||
<el-table-column prop="actual_amount" label="实付金额" min-width="110" align="right" />
|
||||
<el-table-column prop="days" label="天数" width="70" align="center" />
|
||||
<el-table-column prop="check_in_date" label="入住日期" min-width="110" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status===0" size="small" type="success" link @click="openAudit(row)">审核</el-button>
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">合同列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增合同</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="contract_no" label="合同编号" min-width="150" />
|
||||
<el-table-column label="客户" min-width="100"><template #default="{ row }">{{ row.customer?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="package_name" label="套餐" min-width="120" />
|
||||
<el-table-column prop="actual_amount" label="实付金额" min-width="110" align="right" />
|
||||
<el-table-column prop="days" label="天数" width="70" align="center" />
|
||||
<el-table-column prop="check_in_date" label="入住日期" min-width="110" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status===0" size="small" type="success" text @click="openAudit(row)">审核</el-button>
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit -->
|
||||
|
||||
@@ -91,8 +91,8 @@ onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.name" placeholder="客户姓名" style="width:160px" clearable @keydown.enter="handleSearch" />
|
||||
<el-input v-model="searchForm.phone" placeholder="手机号" style="width:160px" clearable @keydown.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
|
||||
@@ -102,31 +102,40 @@ onMounted(() => fetchList())
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增客户</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="name" label="姓名" min-width="100" />
|
||||
<el-table-column prop="phone" label="手机号" min-width="130" />
|
||||
<el-table-column prop="expected_date" label="预产期" min-width="110" />
|
||||
<el-table-column prop="baby_count" label="宝宝数" width="80" align="center" />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" min-width="100">
|
||||
<template #default="{ row }">{{ row.owner?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleDetail(row)">详情</el-button>
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">客户列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增客户</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="name" label="姓名" min-width="100" />
|
||||
<el-table-column prop="phone" label="手机号" min-width="130" />
|
||||
<el-table-column prop="expected_date" label="预产期" min-width="110" />
|
||||
<el-table-column prop="baby_count" label="宝宝数" width="80" align="center" />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" min-width="100">
|
||||
<template #default="{ row }">{{ row.owner?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleDetail(row)">详情</el-button>
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit -->
|
||||
|
||||
@@ -144,8 +144,8 @@ onMounted(() => { fetchList(); fetchChannels(); fetchUsers() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.name" placeholder="姓名" style="width:160px" clearable @keydown.enter="handleSearch" />
|
||||
<el-input v-model="searchForm.phone" placeholder="手机号" style="width:160px" clearable @keydown.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.channel_id" placeholder="渠道" clearable style="width:140px">
|
||||
@@ -159,36 +159,43 @@ onMounted(() => { fetchList(); fetchChannels(); fetchUsers() })
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<div class="table-actions">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">线索列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增线索</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="name" label="姓名" min-width="100" />
|
||||
<el-table-column prop="phone" label="手机号" min-width="130" />
|
||||
<el-table-column label="渠道" min-width="100">
|
||||
<template #default="{ row }">{{ row.channel?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="expected_date" label="预产期" min-width="110" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" min-width="100">
|
||||
<template #default="{ row }">{{ row.owner?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="success" link @click="openFollow(row)">跟进</el-button>
|
||||
<el-button size="small" type="warning" link @click="openAssign(row)">分配</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="name" label="姓名" min-width="100" />
|
||||
<el-table-column prop="phone" label="手机号" min-width="130" />
|
||||
<el-table-column label="渠道" min-width="100">
|
||||
<template #default="{ row }">{{ row.channel?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="expected_date" label="预产期" min-width="110" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" min-width="100">
|
||||
<template #default="{ row }">{{ row.owner?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="success" text @click="openFollow(row)">跟进</el-button>
|
||||
<el-button size="small" type="warning" text @click="openAssign(row)">分配</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit -->
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { questionnaireApi, customerApi } from '@/api/crm.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新建问卷')
|
||||
const submitLoading = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const detailData = ref(null)
|
||||
const customers = ref([])
|
||||
|
||||
const searchForm = reactive({ customer_id: '', status: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
const formRef = ref(null)
|
||||
const form = reactive({ id: null, customer_id: '', title: '', score: 80, status: 'draft' })
|
||||
const isEdit = ref(false)
|
||||
const formRules = {
|
||||
customer_id: [{ required: true, message: '请选择客户', trigger: 'change' }],
|
||||
title: [{ required: true, message: '请输入问卷标题', trigger: 'blur' }],
|
||||
score: [{ required: true, message: '请输入评分', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const statusMap = { draft: '草稿', sent: '已发送', completed: '已完成' }
|
||||
const statusType = { draft: 'info', sent: 'warning', completed: 'success' }
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await questionnaireApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function fetchCustomers() {
|
||||
try {
|
||||
const res = await customerApi.getList({ per_page: 500 })
|
||||
customers.value = res.data?.list || res.data?.data || []
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() { Object.assign(searchForm, { customer_id: '', status: '' }); handleSearch() }
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '新建问卷'
|
||||
Object.assign(form, { id: null, customer_id: '', title: '', score: 80, status: 'draft' })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleEdit(row) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑问卷'
|
||||
const res = await questionnaireApi.getDetail(row.id)
|
||||
const d = res.data
|
||||
Object.assign(form, { id: d.id, customer_id: d.customer_id, title: d.title, score: d.score, status: d.status })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleDetail(row) {
|
||||
const res = await questionnaireApi.getDetail(row.id)
|
||||
detailData.value = res.data
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (isEdit.value) { await questionnaireApi.update(form.id, form); ElMessage.success('更新成功') }
|
||||
else { await questionnaireApi.create(form); ElMessage.success('创建成功') }
|
||||
dialogVisible.value = false; fetchList()
|
||||
} finally { submitLoading.value = false }
|
||||
})
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除问卷「${row.title}」吗?`, '删除确认', { type: 'warning' })
|
||||
await questionnaireApi.delete(row.id); ElMessage.success('删除成功'); fetchList()
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
|
||||
onMounted(() => { fetchList(); fetchCustomers() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.customer_id" placeholder="选择客户" clearable filterable style="width:180px">
|
||||
<el-option v-for="c in customers" :key="c.id" :label="`${c.name}(${c.phone})`" :value="c.id" />
|
||||
</el-select>
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="k" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-1 h-5 bg-rose-500 rounded-full inline-block"></span>
|
||||
<span class="font-medium text-gray-700">问卷列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新建问卷</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column label="客户" min-width="110">
|
||||
<template #default="{ row }">{{ row.customer_name || row.customer?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="问卷标题" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="满意度评分" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="row.score >= 80 ? 'text-green-600' : row.score >= 60 ? 'text-yellow-500' : 'text-red-500'" class="font-semibold">
|
||||
{{ row.score ?? '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleDetail(row)">详情</el-button>
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.per_page"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total,sizes,prev,pager,next,jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add / Edit Dialog -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="560px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
<el-form-item label="客户" prop="customer_id">
|
||||
<el-select v-model="form.customer_id" filterable placeholder="请选择客户" style="width:100%">
|
||||
<el-option v-for="c in customers" :key="c.id" :label="`${c.name}(${c.phone})`" :value="c.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="问卷标题" prop="title">
|
||||
<el-input v-model="form.title" placeholder="请输入问卷标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="满意度评分" prop="score">
|
||||
<el-slider v-model="form.score" :min="0" :max="100" :step="1" show-input style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.status" style="width:100%">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="k" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Detail Drawer -->
|
||||
<el-drawer v-model="detailVisible" title="问卷详情" size="480px">
|
||||
<template v-if="detailData">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="客户">{{ detailData.customer_name || detailData.customer?.name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="标题" :span="2">{{ detailData.title }}</el-descriptions-item>
|
||||
<el-descriptions-item label="评分">
|
||||
<span :class="detailData.score >= 80 ? 'text-green-600 font-bold' : 'text-yellow-500 font-bold'">{{ detailData.score }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="statusType[detailData.status]" size="small">{{ statusMap[detailData.status] }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">{{ detailData.created_at }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,299 +1,113 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const username = computed(() => userStore.userInfo?.name || userStore.userInfo?.username || '用户')
|
||||
|
||||
const today = computed(() => {
|
||||
const d = new Date()
|
||||
return `${d.getFullYear()} 年 ${d.getMonth() + 1} 月 ${d.getDate()} 日`
|
||||
})
|
||||
|
||||
const weekDay = computed(() => {
|
||||
const days = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
|
||||
return days[new Date().getDay()]
|
||||
return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日 ${days[d.getDay()]}`
|
||||
})
|
||||
|
||||
const statCards = ref([
|
||||
{
|
||||
title: '在住宾客',
|
||||
value: '--',
|
||||
unit: '位',
|
||||
icon: 'User',
|
||||
color: '#E8A87C',
|
||||
bg: '#FBE8D5'
|
||||
},
|
||||
{
|
||||
title: '今日入住',
|
||||
value: '--',
|
||||
unit: '单',
|
||||
icon: 'House',
|
||||
color: '#67C23A',
|
||||
bg: '#E8F5E1'
|
||||
},
|
||||
{
|
||||
title: '今日退住',
|
||||
value: '--',
|
||||
unit: '单',
|
||||
icon: 'SuitcaseLine',
|
||||
color: '#409EFF',
|
||||
bg: '#E0EFFF'
|
||||
},
|
||||
{
|
||||
title: '今日收款',
|
||||
value: '--',
|
||||
unit: '元',
|
||||
icon: 'Money',
|
||||
color: '#E6A23C',
|
||||
bg: '#FDF0D6'
|
||||
},
|
||||
{
|
||||
title: '客房出租率',
|
||||
value: '--',
|
||||
unit: '%',
|
||||
icon: 'DataAnalysis',
|
||||
color: '#C97B5E',
|
||||
bg: '#F9E8E0'
|
||||
},
|
||||
{
|
||||
title: '本月新签',
|
||||
value: '--',
|
||||
unit: '单',
|
||||
icon: 'DocumentAdd',
|
||||
color: '#909399',
|
||||
bg: '#F0F1F2'
|
||||
}
|
||||
{ title: '今日预约客户', value: '--', trend: '+12% 较昨日', trendUp: true, icon: 'solar:users-group-rounded-bold-duotone', iconBg: 'bg-rose-50', iconColor: 'text-rose-500' },
|
||||
{ title: '当前入驻率', value: '--', trend: '良好运营状态', trendUp: true, icon: 'solar:home-bold-duotone', iconBg: 'bg-blue-50', iconColor: 'text-blue-500' },
|
||||
{ title: '今日护理评分', value: '--', trend: '历史最高纪录', trendUp: true, icon: 'solar:heart-pulse-bold-duotone', iconBg: 'bg-amber-50', iconColor: 'text-amber-500' },
|
||||
{ title: '本月营收(预计)', value: '--', trend: '+8.4% 较上月', trendUp: true, icon: 'solar:wad-of-money-bold-duotone', iconBg: 'bg-teal-50', iconColor: 'text-teal-500' },
|
||||
])
|
||||
|
||||
const shortcuts = [
|
||||
{ label: '新增客户', icon: 'UserFilled', path: '/crm/customers/add' },
|
||||
{ label: '办理入住', icon: 'House', path: '/room/check-in' },
|
||||
{ label: '办理退住', icon: 'DoorOpen', path: '/room/check-out' },
|
||||
{ label: '新建工单', icon: 'DocumentAdd', path: '/service/orders/add' },
|
||||
{ label: '今日月子餐', icon: 'Bowl', path: '/meal/today' },
|
||||
{ label: '护理记录', icon: 'FirstAidKit', path: '/nursing/records' }
|
||||
{ label: '新增客户', icon: 'solar:user-plus-bold-duotone', path: '/crm/customers' },
|
||||
{ label: '办理入住', icon: 'solar:home-bold-duotone', path: '/room/reservations' },
|
||||
{ label: '办理退住', icon: 'solar:door-open-bold-duotone', path: '/room/rooms' },
|
||||
{ label: '新建工单', icon: 'solar:document-add-bold-duotone', path: '/service/orders' },
|
||||
{ label: '今日月子餐', icon: 'solar:cup-bold', path: '/meal/daily-plans' },
|
||||
{ label: '护理记录', icon: 'solar:heart-pulse-bold-duotone', path: '/care/records' },
|
||||
]
|
||||
|
||||
// Placeholder — in production would fetch from API
|
||||
onMounted(() => {
|
||||
// Future: fetchDashboardStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="p-6 space-y-5">
|
||||
<!-- Welcome Banner -->
|
||||
<div class="welcome-banner card">
|
||||
<div class="welcome-info">
|
||||
<h2 class="welcome-title">你好,{{ username }} 👋</h2>
|
||||
<p class="welcome-sub">{{ today }} {{ weekDay }},祝您工作愉快!</p>
|
||||
</div>
|
||||
<div class="welcome-deco">
|
||||
<el-icon :size="80" color="rgba(232,168,124,0.2)"><House /></el-icon>
|
||||
<div class="bg-gradient-to-r from-rose-50 to-orange-50 rounded-3xl p-8 flex items-center justify-between border border-rose-100">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800 mb-1">你好,{{ username }} 👋</h2>
|
||||
<p class="text-gray-400 text-sm">{{ today }},祝您工作愉快!</p>
|
||||
</div>
|
||||
<Icon icon="solar:heart-bold" class="text-8xl text-rose-200" />
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="stats-grid mt-16">
|
||||
<!-- KPI Cards -->
|
||||
<div class="grid grid-cols-4 gap-5">
|
||||
<div
|
||||
v-for="stat in statCards"
|
||||
:key="stat.title"
|
||||
class="stat-card card"
|
||||
class="bg-white rounded-3xl border border-gray-100 shadow-sm p-6"
|
||||
>
|
||||
<div class="stat-icon-wrap" :style="{ background: stat.bg }">
|
||||
<el-icon :size="28" :style="{ color: stat.color }">
|
||||
<component :is="stat.icon" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">{{ stat.title }}</div>
|
||||
<div class="stat-value">
|
||||
<span class="stat-number" :style="{ color: stat.color }">{{ stat.value }}</span>
|
||||
<span class="stat-unit">{{ stat.unit }}</span>
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<div class="w-12 h-12 rounded-2xl flex items-center justify-center text-2xl" :class="[stat.iconBg, stat.iconColor]">
|
||||
<Icon :icon="stat.icon" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-400 text-xs">{{ stat.title }}</p>
|
||||
<p class="text-2xl font-bold text-gray-800">{{ stat.value }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs font-bold flex items-center gap-1" :class="stat.trendUp ? 'text-green-500' : 'text-red-400'">
|
||||
<Icon :icon="stat.trendUp ? 'solar:double-alt-arrow-up-bold-duotone' : 'solar:double-alt-arrow-down-bold-duotone'" />
|
||||
{{ stat.trend }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Access -->
|
||||
<div class="section-title mt-24">快捷入口</div>
|
||||
<div class="shortcuts-grid mt-12">
|
||||
<router-link
|
||||
v-for="item in shortcuts"
|
||||
:key="item.label"
|
||||
:to="item.path"
|
||||
class="shortcut-card card"
|
||||
>
|
||||
<el-icon :size="28" color="#E8A87C">
|
||||
<component :is="item.icon" />
|
||||
</el-icon>
|
||||
<span class="shortcut-label">{{ item.label }}</span>
|
||||
</router-link>
|
||||
<div class="bg-white rounded-3xl border border-gray-100 shadow-sm p-6">
|
||||
<h3 class="font-bold text-gray-800 mb-5 flex items-center gap-2">
|
||||
<span class="w-1 h-5 bg-rose-500 rounded-full inline-block"></span>
|
||||
快捷入口
|
||||
</h3>
|
||||
<div class="grid grid-cols-6 gap-4">
|
||||
<button
|
||||
v-for="item in shortcuts"
|
||||
:key="item.label"
|
||||
class="flex flex-col items-center justify-center gap-3 p-5 rounded-2xl bg-gray-50 hover:bg-rose-50 hover:text-rose-500 transition-all duration-150 group"
|
||||
@click="router.push(item.path)"
|
||||
>
|
||||
<Icon :icon="item.icon" class="text-3xl text-gray-400 group-hover:text-rose-500 transition-colors" />
|
||||
<span class="text-xs font-medium text-gray-600 group-hover:text-rose-500">{{ item.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Info -->
|
||||
<div class="system-info card mt-16">
|
||||
<div class="info-title">系统信息</div>
|
||||
<div class="info-grid mt-12">
|
||||
<div class="info-item">
|
||||
<span class="info-key">系统名称</span>
|
||||
<span class="info-val">宫中有喜月子会所综合管理系统</span>
|
||||
<div class="bg-white rounded-3xl border border-gray-100 shadow-sm p-6">
|
||||
<h3 class="font-bold text-gray-800 mb-4 flex items-center gap-2">
|
||||
<span class="w-1 h-5 bg-rose-500 rounded-full inline-block"></span>
|
||||
系统信息
|
||||
</h3>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="flex gap-3 text-sm">
|
||||
<span class="text-gray-400 w-20 flex-shrink-0">系统名称</span>
|
||||
<span class="text-gray-700 font-medium">宫中有喜月子会所综合管理系统</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-key">当前版本</span>
|
||||
<span class="info-val">v1.0.0</span>
|
||||
<div class="flex gap-3 text-sm">
|
||||
<span class="text-gray-400 w-20 flex-shrink-0">当前版本</span>
|
||||
<span class="text-gray-700 font-medium">v1.0.0</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-key">技术支持</span>
|
||||
<span class="info-val">宫中有喜运营团队</span>
|
||||
<div class="flex gap-3 text-sm">
|
||||
<span class="text-gray-400 w-20 flex-shrink-0">技术支持</span>
|
||||
<span class="text-gray-700 font-medium">宫中有喜运营团队</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.welcome-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: linear-gradient(135deg, #FBE8D5, #F5D5BC);
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary-dark);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.welcome-sub {
|
||||
font-size: 14px;
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.welcome-deco {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stat-icon-wrap {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-unit {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
padding-left: 8px;
|
||||
border-left: 3px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.shortcuts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.shortcut-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 24px 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(232, 168, 124, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-label {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.system-info {
|
||||
.info-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.info-key {
|
||||
color: var(--color-text-secondary);
|
||||
min-width: 80px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-val {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -97,29 +97,38 @@ onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.customer_id" placeholder="客户ID" style="width:160px" clearable @keydown.enter="handleSearch" />
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column label="客户姓名" min-width="120">
|
||||
<template #default="{ row }">{{ row.customer?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="cash_balance" label="现金余额" min-width="120" align="right" />
|
||||
<el-table-column prop="card_balance" label="储值卡余额" min-width="120" align="right" />
|
||||
<el-table-column prop="points" label="积分" min-width="100" align="right" />
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleRecharge(row)">充值</el-button>
|
||||
<el-button size="small" type="primary" link @click="handleViewTx(row)">查看流水</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10, 20, 50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">客户账户列表</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column label="客户姓名" min-width="120">
|
||||
<template #default="{ row }">{{ row.customer?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="cash_balance" label="现金余额" min-width="120" align="right" />
|
||||
<el-table-column prop="card_balance" label="储值卡余额" min-width="120" align="right" />
|
||||
<el-table-column prop="points" label="积分" min-width="100" align="right" />
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleRecharge(row)">充值</el-button>
|
||||
<el-button size="small" type="primary" text @click="handleViewTx(row)">查看流水</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10, 20, 50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recharge Dialog -->
|
||||
|
||||
@@ -77,8 +77,8 @@ onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.type" placeholder="类型" clearable style="width:120px">
|
||||
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
@@ -89,31 +89,40 @@ onMounted(() => fetchList())
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增分类</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe row-key="id" default-expand-all style="width:100%">
|
||||
<el-table-column prop="name" label="分类名称" min-width="180" />
|
||||
<el-table-column label="类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.type === 1 ? 'success' : 'danger'" size="small">{{ typeMap[row.type] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="level" label="层级" width="80" align="center" />
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status ? 'success' : 'danger'" size="small">{{ row.status ? '启用' : '停用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[20,50,100]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">收支分类列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增分类</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" row-key="id" default-expand-all style="width:100%">
|
||||
<el-table-column prop="name" label="分类名称" min-width="180" />
|
||||
<el-table-column label="类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.type === 1 ? 'success' : 'danger'" size="small">{{ typeMap[row.type] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="level" label="层级" width="80" align="center" />
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status ? 'success' : 'danger'" size="small">{{ row.status ? '启用' : '停用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[20,50,100]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑分类' : '新增分类'" width="450px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
|
||||
@@ -69,8 +69,8 @@ onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.invoice_no" placeholder="发票号" style="width:180px" clearable @keydown.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="Number(k)" />
|
||||
@@ -81,26 +81,35 @@ onMounted(() => fetchList())
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增发票</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="invoice_no" label="发票号" min-width="150" />
|
||||
<el-table-column prop="customer_id" label="客户ID" width="100" align="center" />
|
||||
<el-table-column label="类型" width="80" align="center"><template #default="{ row }"><el-tag size="small">{{ typeMap[row.type] }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="amount" label="金额" width="100" align="right" />
|
||||
<el-table-column prop="tax_amount" label="税额" width="100" align="right" />
|
||||
<el-table-column prop="title" label="发票抬头" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="90" align="center"><template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="issued_at" label="开票时间" width="160" />
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status===0" size="small" type="success" link @click="handleIssue(row)">开票</el-button>
|
||||
<el-button v-if="row.status===0" size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.status===0" size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,15,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">发票列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增发票</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="invoice_no" label="发票号" min-width="150" />
|
||||
<el-table-column prop="customer_id" label="客户ID" width="100" align="center" />
|
||||
<el-table-column label="类型" width="80" align="center"><template #default="{ row }"><el-tag size="small">{{ typeMap[row.type] }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="amount" label="金额" width="100" align="right" />
|
||||
<el-table-column prop="tax_amount" label="税额" width="100" align="right" />
|
||||
<el-table-column prop="title" label="发票抬头" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="90" align="center"><template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="issued_at" label="开票时间" width="160" />
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status===0" size="small" type="success" text @click="handleIssue(row)">开票</el-button>
|
||||
<el-button v-if="row.status===0" size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.status===0" size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,15,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑发票':'新增发票'" width="550px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { paymentApi } from '@/api/finance.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
const searchForm = reactive({ status: '', date_from: '', date_to: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
customer_id: '',
|
||||
customer_name: '',
|
||||
contract_id: '',
|
||||
amount: 0,
|
||||
payment_method: '',
|
||||
remark: ''
|
||||
})
|
||||
const formRules = {
|
||||
customer_name: [{ required: true, message: '请输入客户姓名', trigger: 'blur' }],
|
||||
amount: [{ required: true, message: '请输入金额', trigger: 'blur' }],
|
||||
payment_method: [{ required: true, message: '请选择付款方式', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const statusMap = { pending: '待确认', confirmed: '已确认' }
|
||||
const statusType = { pending: 'warning', confirmed: 'success' }
|
||||
const payMethodMap = {
|
||||
cash: '现金',
|
||||
card: '刷卡',
|
||||
wechat: '微信',
|
||||
alipay: '支付宝',
|
||||
transfer: '银行转账'
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await paymentApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} catch {
|
||||
ElMessage.error('获取付款列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() {
|
||||
Object.assign(searchForm, { status: '', date_from: '', date_to: '' })
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
Object.assign(form, {
|
||||
customer_id: '', customer_name: '', contract_id: '',
|
||||
amount: 0, payment_method: '', remark: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await paymentApi.create({ ...form, type: 'payment' })
|
||||
ElMessage.success('创建成功')
|
||||
dialogVisible.value = false
|
||||
fetchList()
|
||||
} catch {
|
||||
ElMessage.error('创建失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function handleConfirm(row) {
|
||||
await ElMessageBox.confirm(`确认收款「¥${row.amount}」吗?`, '确认收款', { type: 'warning' })
|
||||
try {
|
||||
await paymentApi.audit(row.id, {})
|
||||
ElMessage.success('确认成功')
|
||||
fetchList()
|
||||
} catch {
|
||||
ElMessage.error('确认失败')
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
|
||||
onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="k" />
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-model="searchForm.date_from"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="开始日期"
|
||||
style="width:150px"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="searchForm.date_to"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="结束日期"
|
||||
style="width:150px"
|
||||
/>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 列表卡片 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-1 h-5 bg-rose-500 rounded-full inline-block"></span>
|
||||
<span class="font-medium text-gray-700">付款记录列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新建收款记录</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%" stripe>
|
||||
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||||
<el-table-column prop="customer_name" label="客户" min-width="110" />
|
||||
<el-table-column prop="contract_id" label="合同ID" width="90" align="center" />
|
||||
<el-table-column label="金额" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="text-green-600 font-semibold">¥{{ Number(row.amount || 0).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="付款方式" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ payMethodMap[row.payment_method] || row.payment_method || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">
|
||||
{{ statusMap[row.status] || row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="100" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.status === 'pending'"
|
||||
size="small"
|
||||
type="success"
|
||||
text
|
||||
@click="handleConfirm(row)"
|
||||
>确认收款</el-button>
|
||||
<span v-else class="text-gray-400 text-sm">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.per_page"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total,sizes,prev,pager,next,jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新建收款 Dialog -->
|
||||
<el-dialog v-model="dialogVisible" title="新建收款记录" width="520px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
<el-form-item label="客户姓名" prop="customer_name">
|
||||
<el-input v-model="form.customer_name" placeholder="请输入客户姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="合同ID">
|
||||
<el-input v-model="form.contract_id" placeholder="关联合同ID(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="金额" prop="amount">
|
||||
<el-input-number v-model="form.amount" :min="0.01" :precision="2" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="付款方式" prop="payment_method">
|
||||
<el-select v-model="form.payment_method" placeholder="选择付款方式" style="width:100%">
|
||||
<el-option v-for="(v, k) in payMethodMap" :key="k" :label="v" :value="k" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -60,8 +60,8 @@ onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.name" placeholder="储值卡名称" style="width:180px" clearable @keydown.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
|
||||
<el-option label="启用" :value="1" />
|
||||
@@ -70,23 +70,32 @@ onMounted(() => fetchList())
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增储值卡</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="name" label="储值卡名称" min-width="150" />
|
||||
<el-table-column prop="face_value" label="面值" width="100" align="right" />
|
||||
<el-table-column prop="sell_price" label="售价" width="100" align="right" />
|
||||
<el-table-column prop="discount_rate" label="折扣率" width="90" align="center" />
|
||||
<el-table-column prop="validity_days" label="有效期(天)" width="100" align="center" />
|
||||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="row.status?'success':'danger'" size="small">{{ row.status?'启用':'停用' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,15,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">储值卡列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增储值卡</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="name" label="储值卡名称" min-width="150" />
|
||||
<el-table-column prop="face_value" label="面值" width="100" align="right" />
|
||||
<el-table-column prop="sell_price" label="售价" width="100" align="right" />
|
||||
<el-table-column prop="discount_rate" label="折扣率" width="90" align="center" />
|
||||
<el-table-column prop="validity_days" label="有效期(天)" width="100" align="center" />
|
||||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="row.status?'success':'danger'" size="small">{{ row.status?'启用':'停用' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,15,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑储值卡':'新增储值卡'" width="500px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
|
||||
|
||||
@@ -104,8 +104,8 @@ onMounted(() => { fetchList() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.type" placeholder="收支类型" clearable style="width:120px">
|
||||
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
@@ -117,34 +117,43 @@ onMounted(() => { fetchList() })
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增记录</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="record_no" label="记录编号" min-width="150" />
|
||||
<el-table-column label="类型" width="90" align="center">
|
||||
<template #default="{ row }"><el-tag :type="typeType[row.type]" size="small">{{ typeMap[row.type] || '-' }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="category" label="分类" min-width="120">
|
||||
<template #default="{ row }">{{ row.category?.name || row.category || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="金额" min-width="110" align="right" />
|
||||
<el-table-column label="支付方式" width="110" align="center">
|
||||
<template #default="{ row }">{{ payMethodMap[row.pay_method] || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审核状态" width="100" align="center">
|
||||
<template #default="{ row }"><el-tag :type="auditType[row.audit_status]" size="small">{{ auditMap[row.audit_status] }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="record_date" label="记录日期" min-width="120" />
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.audit_status===0" size="small" type="success" link @click="openAudit(row)">审核</el-button>
|
||||
<el-button v-if="row.audit_status===0" size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.audit_status===0" size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
<span v-if="row.audit_status!==0" class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">收支记录列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增记录</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="record_no" label="记录编号" min-width="150" />
|
||||
<el-table-column label="类型" width="90" align="center">
|
||||
<template #default="{ row }"><el-tag :type="typeType[row.type]" size="small">{{ typeMap[row.type] || '-' }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="category" label="分类" min-width="120">
|
||||
<template #default="{ row }">{{ row.category?.name || row.category || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="金额" min-width="110" align="right" />
|
||||
<el-table-column label="支付方式" width="110" align="center">
|
||||
<template #default="{ row }">{{ payMethodMap[row.pay_method] || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审核状态" width="100" align="center">
|
||||
<template #default="{ row }"><el-tag :type="auditType[row.audit_status]" size="small">{{ auditMap[row.audit_status] }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="record_date" label="记录日期" min-width="120" />
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.audit_status===0" size="small" type="success" text @click="openAudit(row)">审核</el-button>
|
||||
<el-button v-if="row.audit_status===0" size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.audit_status===0" size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
<span v-if="row.audit_status!==0" class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit -->
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { accountTransactionApi } from '@/api/finance.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
const searchForm = reactive({ account_id: '', type: '', date_from: '', date_to: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
const typeMap = {
|
||||
deposit: '充值',
|
||||
consume: '消费',
|
||||
refund: '退款',
|
||||
adjust: '调整'
|
||||
}
|
||||
const typeTagType = {
|
||||
deposit: 'success',
|
||||
consume: 'danger',
|
||||
refund: 'warning',
|
||||
adjust: 'info'
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await accountTransactionApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} catch {
|
||||
ElMessage.error('获取流水列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() {
|
||||
Object.assign(searchForm, { account_id: '', type: '', date_from: '', date_to: '' })
|
||||
handleSearch()
|
||||
}
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
|
||||
function amountClass(row) {
|
||||
return ['deposit', 'refund'].includes(row.type) ? 'text-green-600 font-semibold' : 'text-red-500 font-semibold'
|
||||
}
|
||||
function amountPrefix(row) {
|
||||
return ['deposit', 'refund'].includes(row.type) ? '+' : '-'
|
||||
}
|
||||
|
||||
onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input
|
||||
v-model="searchForm.account_id"
|
||||
placeholder="账户ID"
|
||||
style="width:150px"
|
||||
clearable
|
||||
@keydown.enter="handleSearch"
|
||||
/>
|
||||
<el-select v-model="searchForm.type" placeholder="流水类型" clearable style="width:130px">
|
||||
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="k" />
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-model="searchForm.date_from"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="开始日期"
|
||||
style="width:150px"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="searchForm.date_to"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="结束日期"
|
||||
style="width:150px"
|
||||
/>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 列表卡片 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center gap-3 px-5 py-4 border-b border-gray-50">
|
||||
<span class="w-1 h-5 bg-rose-500 rounded-full inline-block"></span>
|
||||
<span class="font-medium text-gray-700">账户流水明细</span>
|
||||
<el-tag type="info" size="small" class="ml-auto">只读</el-tag>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%" stripe>
|
||||
<el-table-column prop="id" label="流水ID" width="80" align="center" />
|
||||
<el-table-column prop="customer_name" label="客户" min-width="110" />
|
||||
<el-table-column label="类型" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="typeTagType[row.type]" size="small">
|
||||
{{ typeMap[row.type] || row.type }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span :class="amountClass(row)">
|
||||
{{ amountPrefix(row) }}{{ Number(row.amount || 0).toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="余额(后)" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="text-gray-700">{{ Number(row.balance_after || 0).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="operator_name" label="操作人" width="100" align="center" />
|
||||
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="created_at" label="时间" min-width="160" />
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.per_page"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total,sizes,prev,pager,next,jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -72,8 +72,8 @@ onMounted(() => { fetchList() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.user_id" placeholder="员工ID" clearable style="width:140px" />
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="Number(k)" />
|
||||
@@ -82,29 +82,35 @@ onMounted(() => { fetchList() })
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions">
|
||||
<el-button type="success" @click="handleClockIn">上班打卡</el-button>
|
||||
<el-button type="warning" @click="handleClockOut">下班打卡</el-button>
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<el-button type="success" @click="handleClockIn">上班打卡</el-button>
|
||||
<el-button type="warning" @click="handleClockOut">下班打卡</el-button>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">手动录入</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column label="员工" min-width="100">
|
||||
<template #default="{ row }">{{ row.user?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="attendance_date" label="考勤日期" width="120" />
|
||||
<el-table-column prop="clock_in" label="上班打卡" width="100" />
|
||||
<el-table-column prop="clock_out" label="下班打卡" width="100" />
|
||||
<el-table-column prop="overtime_hours" label="加班(h)" width="90" align="center" />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column label="员工" min-width="100">
|
||||
<template #default="{ row }">{{ row.user?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="attendance_date" label="考勤日期" width="120" />
|
||||
<el-table-column prop="clock_in" label="上班打卡" width="100" />
|
||||
<el-table-column prop="clock_out" label="下班打卡" width="100" />
|
||||
<el-table-column prop="overtime_hours" label="加班(h)" width="90" align="center" />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" title="手动录入考勤" width="500px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="{ user_id: [{ required: true, message: '请输入员工ID' }], attendance_date: [{ required: true, message: '请选择考勤日期' }] }" label-width="90px">
|
||||
|
||||
@@ -82,8 +82,8 @@ onMounted(() => { fetchList() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.type" placeholder="请假类型" clearable style="width:130px">
|
||||
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
@@ -93,40 +93,47 @@ onMounted(() => { fetchList() })
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">请假申请列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增请假</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column label="员工" min-width="100">
|
||||
<template #default="{ row }">{{ row.user?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ typeMap[row.type] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="start_date" label="开始日期" width="120" />
|
||||
<el-table-column prop="end_date" label="结束日期" width="120" />
|
||||
<el-table-column prop="days" label="天数" width="70" align="center" />
|
||||
<el-table-column prop="reason" label="事由" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.status === 0">
|
||||
<el-button size="small" type="success" link @click="handleAudit(row, 1)">批准</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleAudit(row, 2)">拒绝</el-button>
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column label="员工" min-width="100">
|
||||
<template #default="{ row }">{{ row.user?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ typeMap[row.type] }}</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</el-table-column>
|
||||
<el-table-column prop="start_date" label="开始日期" width="120" />
|
||||
<el-table-column prop="end_date" label="结束日期" width="120" />
|
||||
<el-table-column prop="days" label="天数" width="70" align="center" />
|
||||
<el-table-column prop="reason" label="事由" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.status === 0">
|
||||
<el-button size="small" type="success" text @click="handleAudit(row, 1)">批准</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleAudit(row, 2)">拒绝</el-button>
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑请假' : '新增请假'" width="500px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
|
||||
@@ -89,8 +89,8 @@ onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.employee_no" placeholder="工号" style="width:160px" clearable @keydown.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
|
||||
<el-option v-for="(v,k) in statusMap" :key="k" :label="v" :value="Number(k)" />
|
||||
@@ -99,33 +99,42 @@ onMounted(() => fetchList())
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增员工档案</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="employee_no" label="工号" width="120" />
|
||||
<el-table-column label="姓名" min-width="100">
|
||||
<template #default="{ row }">{{ row.user?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="性别" width="70" align="center">
|
||||
<template #default="{ row }">{{ genderMap[row.gender] || '未知' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="hire_date" label="入职日期" min-width="110" />
|
||||
<el-table-column prop="education" label="学历" width="90" />
|
||||
<el-table-column prop="base_salary" label="基本工资" width="100" align="right" />
|
||||
<el-table-column prop="position_salary" label="岗位工资" width="100" align="right" />
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">员工档案列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增员工档案</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="employee_no" label="工号" width="120" />
|
||||
<el-table-column label="姓名" min-width="100">
|
||||
<template #default="{ row }">{{ row.user?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="性别" width="70" align="center">
|
||||
<template #default="{ row }">{{ genderMap[row.gender] || '未知' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="hire_date" label="入职日期" min-width="110" />
|
||||
<el-table-column prop="education" label="学历" width="90" />
|
||||
<el-table-column prop="base_salary" label="基本工资" width="100" align="right" />
|
||||
<el-table-column prop="position_salary" label="岗位工资" width="100" align="right" />
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px" destroy-on-close>
|
||||
|
||||
@@ -147,8 +147,8 @@ onMounted(() => { fetchList(); fetchEmployees() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.year_month" placeholder="工资月份 (如 2024-03)" style="width:200px" clearable @keydown.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="Number(k)" />
|
||||
@@ -157,37 +157,46 @@ onMounted(() => { fetchList(); fetchEmployees() })
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增工资记录</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column label="员工" min-width="100">
|
||||
<template #default="{ row }">{{ row.user?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="year_month" label="工资月份" width="110" align="center" />
|
||||
<el-table-column prop="base_salary" label="基本工资" width="110" align="right" />
|
||||
<el-table-column prop="position_salary" label="岗位工资" width="110" align="right" />
|
||||
<el-table-column prop="performance" label="绩效" width="100" align="right" />
|
||||
<el-table-column prop="overtime_pay" label="加班费" width="100" align="right" />
|
||||
<el-table-column prop="subsidy" label="补贴" width="90" align="right" />
|
||||
<el-table-column label="扣除合计" width="110" align="right">
|
||||
<template #default="{ row }">{{ getDeductionTotal(row).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="实发金额" width="120" align="right">
|
||||
<template #default="{ row }"><span style="font-weight:700">{{ row.actual_amount }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status===0" size="small" type="success" link @click="handleConfirm(row)">确认</el-button>
|
||||
<el-button v-if="row.status===1" size="small" type="success" link @click="handlePay(row)">发放</el-button>
|
||||
<el-button v-if="row.status===0" size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.status===0" size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">工资记录列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增工资记录</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column label="员工" min-width="100">
|
||||
<template #default="{ row }">{{ row.user?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="year_month" label="工资月份" width="110" align="center" />
|
||||
<el-table-column prop="base_salary" label="基本工资" width="110" align="right" />
|
||||
<el-table-column prop="position_salary" label="岗位工资" width="110" align="right" />
|
||||
<el-table-column prop="performance" label="绩效" width="100" align="right" />
|
||||
<el-table-column prop="overtime_pay" label="加班费" width="100" align="right" />
|
||||
<el-table-column prop="subsidy" label="补贴" width="90" align="right" />
|
||||
<el-table-column label="扣除合计" width="110" align="right">
|
||||
<template #default="{ row }">{{ getDeductionTotal(row).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="实发金额" width="120" align="right">
|
||||
<template #default="{ row }"><span style="font-weight:700">{{ row.actual_amount }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status===0" size="small" type="success" text @click="handleConfirm(row)">确认</el-button>
|
||||
<el-button v-if="row.status===1" size="small" type="success" text @click="handlePay(row)">发放</el-button>
|
||||
<el-button v-if="row.status===0" size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.status===0" size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑工资记录' : '新增工资记录'" width="750px" destroy-on-close>
|
||||
|
||||
@@ -49,8 +49,8 @@ onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.user_id" placeholder="员工ID" style="width:160px" clearable @keydown.enter="handleSearch" />
|
||||
<el-date-picker v-model="searchForm.start_date" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" />
|
||||
<el-date-picker v-model="searchForm.end_date" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" />
|
||||
@@ -58,22 +58,31 @@ onMounted(() => fetchList())
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增排班</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column label="员工" min-width="100">
|
||||
<template #default="{ row }">{{ row.user?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="schedule_date" label="排班日期" width="120" />
|
||||
<el-table-column label="班次" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="shiftType[row.shift_type]" size="small">{{ shiftMap[row.shift_type] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="start_time" label="开始时间" width="100" />
|
||||
<el-table-column prop="end_time" label="结束时间" width="100" />
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" :total="total" layout="total,prev,pager,next" background @current-change="handlePageChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">排班列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增排班</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column label="员工" min-width="100">
|
||||
<template #default="{ row }">{{ row.user?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="schedule_date" label="排班日期" width="120" />
|
||||
<el-table-column label="班次" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="shiftType[row.shift_type]" size="small">{{ shiftMap[row.shift_type] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="start_time" label="开始时间" width="100" />
|
||||
<el-table-column prop="end_time" label="结束时间" width="100" />
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" :total="total" layout="total,prev,pager,next" background @current-change="handlePageChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="新增排班" width="500px" destroy-on-close>
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { inventoryCheckApi, warehouseApi, stockApi } from '@/api/inventory.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const currentCheck = ref(null)
|
||||
const warehouses = ref([])
|
||||
|
||||
const searchForm = reactive({ warehouse_id: '', status: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
warehouse_id: '',
|
||||
check_date: '',
|
||||
remark: '',
|
||||
items: []
|
||||
})
|
||||
const formRules = {
|
||||
warehouse_id: [{ required: true, message: '请选择仓库', trigger: 'change' }],
|
||||
check_date: [{ required: true, message: '请选择盘点日期', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const statusMap = { draft: '盘点中', completed: '已完成' }
|
||||
const statusType = { draft: 'warning', completed: 'success' }
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await inventoryCheckApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} catch {
|
||||
ElMessage.error('获取盘点单列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchWarehouses() {
|
||||
try {
|
||||
const res = await warehouseApi.getList({ per_page: 500 })
|
||||
warehouses.value = res.data?.list || res.data?.data || []
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() {
|
||||
Object.assign(searchForm, { warehouse_id: '', status: '' })
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
async function handleAdd() {
|
||||
Object.assign(form, { warehouse_id: '', check_date: '', remark: '', items: [] })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 选择仓库后自动加载该仓库库存
|
||||
async function onWarehouseChange(warehouseId) {
|
||||
if (!warehouseId) { form.items = []; return }
|
||||
try {
|
||||
const res = await stockApi.getList({ warehouse_id: warehouseId, per_page: 500 })
|
||||
const stocks = res.data?.list || res.data?.data || []
|
||||
form.items = stocks.map(s => ({
|
||||
material_id: s.id,
|
||||
material_name: s.material_name,
|
||||
material_code: s.material_code,
|
||||
unit: s.unit,
|
||||
system_quantity: Number(s.quantity || 0),
|
||||
actual_quantity: Number(s.quantity || 0)
|
||||
}))
|
||||
} catch {
|
||||
ElMessage.error('加载库存数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
if (form.items.length === 0) { ElMessage.warning('当前仓库无库存数据'); return }
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await inventoryCheckApi.create({ ...form })
|
||||
ElMessage.success('盘点单创建成功')
|
||||
dialogVisible.value = false
|
||||
fetchList()
|
||||
} catch {
|
||||
ElMessage.error('创建失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function handleComplete(row) {
|
||||
await ElMessageBox.confirm(
|
||||
`确认完成盘点单「${row.check_no}」吗?完成后不可修改。`,
|
||||
'完成确认',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
try {
|
||||
await inventoryCheckApi.complete(row.id, {})
|
||||
ElMessage.success('盘点完成')
|
||||
fetchList()
|
||||
} catch { ElMessage.error('操作失败') }
|
||||
}
|
||||
|
||||
function viewDetail(row) {
|
||||
currentCheck.value = row
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
function diff(item) {
|
||||
return Number(item.actual_quantity || 0) - Number(item.system_quantity || 0)
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
|
||||
onMounted(() => { fetchList(); fetchWarehouses() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.warehouse_id" placeholder="选择仓库" clearable filterable style="width:160px">
|
||||
<el-option v-for="w in warehouses" :key="w.id" :label="w.name" :value="w.id" />
|
||||
</el-select>
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="k" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 列表卡片 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-1 h-5 bg-rose-500 rounded-full inline-block"></span>
|
||||
<span class="font-medium text-gray-700">盘点单列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新建盘点单</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%" stripe>
|
||||
<el-table-column prop="check_no" label="盘点单号" min-width="160" />
|
||||
<el-table-column prop="warehouse_name" label="仓库" min-width="110" />
|
||||
<el-table-column prop="check_date" label="盘点日期" width="120" align="center" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">
|
||||
{{ statusMap[row.status] || row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="operator_name" label="操作人" width="90" align="center" />
|
||||
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="viewDetail(row)">查看</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'draft'"
|
||||
size="small" type="success" text
|
||||
@click="handleComplete(row)"
|
||||
>完成盘点</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.per_page"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total,sizes,prev,pager,next,jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新建盘点 Dialog -->
|
||||
<el-dialog v-model="dialogVisible" title="新建盘点单" width="820px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="10">
|
||||
<el-form-item label="仓库" prop="warehouse_id">
|
||||
<el-select
|
||||
v-model="form.warehouse_id"
|
||||
placeholder="选择仓库"
|
||||
filterable
|
||||
style="width:100%"
|
||||
@change="onWarehouseChange"
|
||||
>
|
||||
<el-option v-for="w in warehouses" :key="w.id" :label="w.name" :value="w.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="盘点日期" prop="check_date">
|
||||
<el-date-picker
|
||||
v-model="form.check_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
style="width:100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" placeholder="备注" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">物料明细(系统自动填入库存,请填写实际数量)</el-divider>
|
||||
|
||||
<div v-if="form.items.length === 0" class="text-center text-gray-400 py-6">
|
||||
请先选择仓库以加载库存数据
|
||||
</div>
|
||||
<el-table v-else :data="form.items" style="width:100%" max-height="320">
|
||||
<el-table-column prop="material_code" label="物料编码" width="110" />
|
||||
<el-table-column prop="material_name" label="物料名称" min-width="130" />
|
||||
<el-table-column prop="unit" label="单位" width="70" align="center" />
|
||||
<el-table-column prop="system_quantity" label="系统数量" width="100" align="right" />
|
||||
<el-table-column label="实际数量" width="140" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.actual_quantity"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
size="small"
|
||||
style="width:120px"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="差异" width="90" align="right">
|
||||
<template #default="{ row }">
|
||||
<span :class="diff(row) === 0 ? 'text-gray-500' : diff(row) > 0 ? 'text-green-600' : 'text-red-500'">
|
||||
{{ diff(row) > 0 ? '+' : '' }}{{ diff(row).toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">提交盘点</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 查看详情 Dialog -->
|
||||
<el-dialog v-model="detailVisible" title="盘点单详情" width="760px" destroy-on-close>
|
||||
<template v-if="currentCheck">
|
||||
<el-descriptions :column="3" border size="small" class="mb-4">
|
||||
<el-descriptions-item label="盘点单号">{{ currentCheck.check_no }}</el-descriptions-item>
|
||||
<el-descriptions-item label="仓库">{{ currentCheck.warehouse_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="盘点日期">{{ currentCheck.check_date }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作人">{{ currentCheck.operator_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="statusType[currentCheck.status]" size="small">
|
||||
{{ statusMap[currentCheck.status] }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注">{{ currentCheck.remark || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-table :data="currentCheck.items || []" style="width:100%" max-height="360">
|
||||
<el-table-column prop="material_name" label="物料名称" min-width="130" />
|
||||
<el-table-column prop="system_quantity" label="系统数量" width="100" align="right" />
|
||||
<el-table-column prop="actual_quantity" label="实际数量" width="100" align="right" />
|
||||
<el-table-column label="差异" width="90" align="right">
|
||||
<template #default="{ row }">
|
||||
<span :class="diff(row) === 0 ? 'text-gray-500' : diff(row) > 0 ? 'text-green-600' : 'text-red-500'">
|
||||
{{ diff(row) > 0 ? '+' : '' }}{{ Number(diff(row)).toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -57,8 +57,8 @@ onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.name" placeholder="物料名称" style="width:180px" clearable @keydown.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.category" placeholder="分类" clearable style="width:120px">
|
||||
<el-option v-for="(v,k) in categoryMap" :key="k" :label="v" :value="Number(k)" />
|
||||
@@ -67,24 +67,33 @@ onMounted(() => fetchList())
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增物料</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="name" label="物料名称" min-width="120" />
|
||||
<el-table-column prop="code" label="物料编码" width="120" />
|
||||
<el-table-column label="分类" width="100" align="center"><template #default="{ row }">{{ categoryMap[row.category] }}</template></el-table-column>
|
||||
<el-table-column prop="unit" label="单位" width="70" align="center" />
|
||||
<el-table-column prop="spec" label="规格" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="safety_stock" label="安全库存" width="100" align="right" />
|
||||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="row.status?'success':'danger'" size="small">{{ row.status?'启用':'禁用' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,15,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">物料列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增物料</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="name" label="物料名称" min-width="120" />
|
||||
<el-table-column prop="code" label="物料编码" width="120" />
|
||||
<el-table-column label="分类" width="100" align="center"><template #default="{ row }">{{ categoryMap[row.category] }}</template></el-table-column>
|
||||
<el-table-column prop="unit" label="单位" width="70" align="center" />
|
||||
<el-table-column prop="spec" label="规格" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="safety_stock" label="安全库存" width="100" align="right" />
|
||||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="row.status?'success':'danger'" size="small">{{ row.status?'启用':'禁用' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,15,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑物料':'新增物料'" width="550px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="80px">
|
||||
|
||||
@@ -80,8 +80,8 @@ onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.order_no" placeholder="采购单号" style="width:180px" clearable @keydown.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="Number(k)" />
|
||||
@@ -90,33 +90,41 @@ onMounted(() => fetchList())
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增采购单</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="order_no" label="采购单号" min-width="160" />
|
||||
<el-table-column label="供应商" min-width="140">
|
||||
<template #default="{ row }">{{ row.supplier?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="total_amount" label="总金额" min-width="110" align="right" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status===0" size="small" type="success" link @click="handleAudit(row, 2)">审批通过</el-button>
|
||||
<el-button v-if="row.status===0" size="small" type="warning" link @click="handleAudit(row, 4)">驳回</el-button>
|
||||
<el-button v-if="row.status<2" size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.status<2" size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">采购单列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增采购单</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="order_no" label="采购单号" min-width="160" />
|
||||
<el-table-column label="供应商" min-width="140">
|
||||
<template #default="{ row }">{{ row.supplier?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="total_amount" label="总金额" min-width="110" align="right" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status===0" size="small" type="success" text @click="handleAudit(row, 2)">审批通过</el-button>
|
||||
<el-button v-if="row.status===0" size="small" type="warning" text @click="handleAudit(row, 4)">驳回</el-button>
|
||||
<el-button v-if="row.status<2" size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.status<2" size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Dialog -->
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑采购单' : '新增采购单'" width="550px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
<el-form-item label="采购单号" prop="order_no">
|
||||
|
||||
@@ -78,8 +78,8 @@ onMounted(() => { fetchList(); fetchWarehouses() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.warehouse_id" placeholder="选择仓库" clearable filterable style="width:180px">
|
||||
<el-option v-for="w in warehouses" :key="w.id" :label="w.name" :value="w.id" />
|
||||
</el-select>
|
||||
@@ -93,31 +93,40 @@ onMounted(() => { fetchList(); fetchWarehouses() })
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增出入库</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="movement_no" label="单据编号" min-width="160" />
|
||||
<el-table-column label="仓库" min-width="120">
|
||||
<template #default="{ row }">{{ row.warehouse?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="110" align="center">
|
||||
<template #default="{ row }"><el-tag size="small">{{ typeMap[row.type] || '-' }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="方向" width="90" align="center">
|
||||
<template #default="{ row }"><el-tag :type="directionType[row.direction]" size="small">{{ directionMap[row.direction] || '-' }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="100" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status === 0" size="small" type="success" link @click="handleConfirm(row)">确认</el-button>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10, 20, 50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">出入库记录</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增出入库</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="movement_no" label="单据编号" min-width="160" />
|
||||
<el-table-column label="仓库" min-width="120">
|
||||
<template #default="{ row }">{{ row.warehouse?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="110" align="center">
|
||||
<template #default="{ row }"><el-tag size="small">{{ typeMap[row.type] || '-' }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="方向" width="90" align="center">
|
||||
<template #default="{ row }"><el-tag :type="directionType[row.direction]" size="small">{{ directionMap[row.direction] || '-' }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="100" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status === 0" size="small" type="success" text @click="handleConfirm(row)">确认</el-button>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10, 20, 50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="新增出入库" width="600px" destroy-on-close>
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { stockApi, warehouseApi, materialApi } from '@/api/inventory.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const warehouses = ref([])
|
||||
const materials = ref([])
|
||||
|
||||
const searchForm = reactive({ warehouse_id: '', material_id: '', low_stock: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await stockApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} catch {
|
||||
ElMessage.error('获取库存列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchOptions() {
|
||||
try {
|
||||
const [wRes, mRes] = await Promise.all([
|
||||
warehouseApi.getList({ per_page: 500 }),
|
||||
materialApi.getList({ per_page: 500 })
|
||||
])
|
||||
warehouses.value = wRes.data?.list || wRes.data?.data || []
|
||||
materials.value = mRes.data?.list || mRes.data?.data || []
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() {
|
||||
Object.assign(searchForm, { warehouse_id: '', material_id: '', low_stock: '' })
|
||||
handleSearch()
|
||||
}
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
|
||||
function isLowStock(row) {
|
||||
return row.min_quantity != null && Number(row.quantity) < Number(row.min_quantity)
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
ElMessage.info('导出功能待实现')
|
||||
}
|
||||
|
||||
onMounted(() => { fetchList(); fetchOptions() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.warehouse_id" placeholder="选择仓库" clearable filterable style="width:160px">
|
||||
<el-option v-for="w in warehouses" :key="w.id" :label="w.name" :value="w.id" />
|
||||
</el-select>
|
||||
<el-select v-model="searchForm.material_id" placeholder="选择物料" clearable filterable style="width:180px">
|
||||
<el-option v-for="m in materials" :key="m.id" :label="`${m.name}(${m.code || '-'})`" :value="m.id" />
|
||||
</el-select>
|
||||
<el-select v-model="searchForm.low_stock" placeholder="库存状态" clearable style="width:130px">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="低库存预警" value="1" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
<el-button class="ml-auto" @click="handleExport">导出</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 低库存说明 -->
|
||||
<el-alert
|
||||
title="红色行表示当前库存低于最低库存量,请及时补货"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<!-- 列表卡片 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center gap-3 px-5 py-4 border-b border-gray-50">
|
||||
<span class="w-1 h-5 bg-rose-500 rounded-full inline-block"></span>
|
||||
<span class="font-medium text-gray-700">库存查询</span>
|
||||
<span class="text-sm text-gray-400 ml-1">共 {{ total }} 条</span>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
style="width:100%"
|
||||
:row-class-name="({ row }) => isLowStock(row) ? 'low-stock-row' : ''"
|
||||
>
|
||||
<el-table-column prop="warehouse_name" label="仓库" min-width="110" />
|
||||
<el-table-column prop="material_code" label="物料编码" width="120" />
|
||||
<el-table-column prop="material_name" label="物料名称" min-width="140" />
|
||||
<el-table-column prop="category" label="分类" width="100" align="center" />
|
||||
<el-table-column prop="unit" label="单位" width="70" align="center" />
|
||||
<el-table-column label="当前库存" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<span :class="isLowStock(row) ? 'text-red-500 font-bold' : 'text-gray-700'">
|
||||
{{ row.quantity }}
|
||||
</span>
|
||||
<el-tag v-if="isLowStock(row)" type="danger" size="small" class="ml-1">偏低</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="min_quantity" label="最低库存" width="100" align="right" />
|
||||
<el-table-column prop="max_quantity" label="最高库存" width="100" align="right" />
|
||||
<el-table-column prop="updated_at" label="更新时间" min-width="160" />
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.per_page"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total,sizes,prev,pager,next,jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.el-table .low-stock-row {
|
||||
background-color: #fff5f5 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -60,8 +60,8 @@ onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.name" placeholder="供应商名称" style="width:180px" clearable @keydown.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
|
||||
<el-option label="启用" :value="1" />
|
||||
@@ -70,22 +70,31 @@ onMounted(() => fetchList())
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增供应商</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="name" label="供应商名称" min-width="150" />
|
||||
<el-table-column prop="contact_person" label="联系人" width="100" />
|
||||
<el-table-column prop="contact_phone" label="联系电话" width="130" />
|
||||
<el-table-column prop="address" label="地址" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="row.status?'success':'danger'" size="small">{{ row.status?'启用':'停用' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,15,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">供应商列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增供应商</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="name" label="供应商名称" min-width="150" />
|
||||
<el-table-column prop="contact_person" label="联系人" width="100" />
|
||||
<el-table-column prop="contact_phone" label="联系电话" width="130" />
|
||||
<el-table-column prop="address" label="地址" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="row.status?'success':'danger'" size="small">{{ row.status?'启用':'停用' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,15,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑供应商':'新增供应商'" width="600px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { transferApi, warehouseApi, materialApi } from '@/api/inventory.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const warehouses = ref([])
|
||||
const materials = ref([])
|
||||
|
||||
const searchForm = reactive({ from_warehouse_id: '', to_warehouse_id: '', status: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
const defaultItem = () => ({ material_id: '', material_name: '', quantity: 1 })
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
from_warehouse_id: '',
|
||||
to_warehouse_id: '',
|
||||
transfer_date: '',
|
||||
remark: '',
|
||||
items: [defaultItem()]
|
||||
})
|
||||
const formRules = {
|
||||
from_warehouse_id: [{ required: true, message: '请选择源仓库', trigger: 'change' }],
|
||||
to_warehouse_id: [{ required: true, message: '请选择目标仓库', trigger: 'change' }],
|
||||
transfer_date: [{ required: true, message: '请选择调拨日期', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const statusMap = { draft: '草稿', confirmed: '已确认', completed: '已完成', cancelled: '已取消' }
|
||||
const statusType = { draft: 'info', confirmed: 'primary', completed: 'success', cancelled: 'danger' }
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await transferApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} catch {
|
||||
ElMessage.error('获取调拨单列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchOptions() {
|
||||
try {
|
||||
const [wRes, mRes] = await Promise.all([
|
||||
warehouseApi.getList({ per_page: 500 }),
|
||||
materialApi.getList({ per_page: 500 })
|
||||
])
|
||||
warehouses.value = wRes.data?.list || wRes.data?.data || []
|
||||
materials.value = mRes.data?.list || mRes.data?.data || []
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() {
|
||||
Object.assign(searchForm, { from_warehouse_id: '', to_warehouse_id: '', status: '' })
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
Object.assign(form, {
|
||||
from_warehouse_id: '', to_warehouse_id: '',
|
||||
transfer_date: '', remark: '',
|
||||
items: [defaultItem()]
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function addItem() { form.items.push(defaultItem()) }
|
||||
function removeItem(idx) {
|
||||
if (form.items.length <= 1) { ElMessage.warning('至少保留一条物料'); return }
|
||||
form.items.splice(idx, 1)
|
||||
}
|
||||
|
||||
function onMaterialChange(item) {
|
||||
const mat = materials.value.find(m => m.id === item.material_id)
|
||||
if (mat) item.material_name = mat.name
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
const hasEmpty = form.items.some(i => !i.material_id || i.quantity <= 0)
|
||||
if (hasEmpty) { ElMessage.warning('请完整填写物料信息'); return }
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await transferApi.create({ ...form })
|
||||
ElMessage.success('创建成功')
|
||||
dialogVisible.value = false
|
||||
fetchList()
|
||||
} catch {
|
||||
ElMessage.error('创建失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function handleConfirm(row) {
|
||||
await ElMessageBox.confirm(`确认调拨单「${row.transfer_no}」吗?`, '确认操作', { type: 'warning' })
|
||||
try {
|
||||
await transferApi.confirm(row.id)
|
||||
ElMessage.success('确认成功')
|
||||
fetchList()
|
||||
} catch { ElMessage.error('操作失败') }
|
||||
}
|
||||
|
||||
async function handleCancel(row) {
|
||||
await ElMessageBox.confirm(`取消调拨单「${row.transfer_no}」吗?取消后不可恢复。`, '取消确认', { type: 'warning' })
|
||||
try {
|
||||
await transferApi.cancel(row.id)
|
||||
ElMessage.success('已取消')
|
||||
fetchList()
|
||||
} catch { ElMessage.error('操作失败') }
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
|
||||
onMounted(() => { fetchList(); fetchOptions() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.from_warehouse_id" placeholder="源仓库" clearable filterable style="width:150px">
|
||||
<el-option v-for="w in warehouses" :key="w.id" :label="w.name" :value="w.id" />
|
||||
</el-select>
|
||||
<el-select v-model="searchForm.to_warehouse_id" placeholder="目标仓库" clearable filterable style="width:150px">
|
||||
<el-option v-for="w in warehouses" :key="w.id" :label="w.name" :value="w.id" />
|
||||
</el-select>
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="k" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 列表卡片 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-1 h-5 bg-rose-500 rounded-full inline-block"></span>
|
||||
<span class="font-medium text-gray-700">调拨单列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新建调拨单</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%" stripe>
|
||||
<el-table-column prop="transfer_no" label="调拨单号" min-width="160" />
|
||||
<el-table-column prop="from_warehouse_name" label="源仓库" min-width="110" />
|
||||
<el-table-column prop="to_warehouse_name" label="目标仓库" min-width="110" />
|
||||
<el-table-column prop="transfer_date" label="调拨日期" width="120" align="center" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">
|
||||
{{ statusMap[row.status] || row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_by_name" label="创建人" width="90" align="center" />
|
||||
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.status === 'draft'"
|
||||
size="small" type="success" text
|
||||
@click="handleConfirm(row)"
|
||||
>确认</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'draft'"
|
||||
size="small" type="danger" text
|
||||
@click="handleCancel(row)"
|
||||
>取消</el-button>
|
||||
<span v-if="!['draft'].includes(row.status)" class="text-gray-400 text-sm">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.per_page"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total,sizes,prev,pager,next,jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新建调拨单 Dialog -->
|
||||
<el-dialog v-model="dialogVisible" title="新建调拨单" width="680px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="源仓库" prop="from_warehouse_id">
|
||||
<el-select v-model="form.from_warehouse_id" placeholder="选择源仓库" filterable style="width:100%">
|
||||
<el-option v-for="w in warehouses" :key="w.id" :label="w.name" :value="w.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="目标仓库" prop="to_warehouse_id">
|
||||
<el-select v-model="form.to_warehouse_id" placeholder="选择目标仓库" filterable style="width:100%">
|
||||
<el-option v-for="w in warehouses" :key="w.id" :label="w.name" :value="w.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="调拨日期" prop="transfer_date">
|
||||
<el-date-picker
|
||||
v-model="form.transfer_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
style="width:100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" placeholder="备注(可选)" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">调拨物料明细</el-divider>
|
||||
|
||||
<div v-for="(item, idx) in form.items" :key="idx" class="flex items-center gap-2 mb-3">
|
||||
<el-select
|
||||
v-model="item.material_id"
|
||||
placeholder="选择物料"
|
||||
filterable
|
||||
style="flex:2"
|
||||
@change="onMaterialChange(item)"
|
||||
>
|
||||
<el-option
|
||||
v-for="m in materials"
|
||||
:key="m.id"
|
||||
:label="`${m.name}(${m.code || '-'})`"
|
||||
:value="m.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input-number
|
||||
v-model="item.quantity"
|
||||
:min="1"
|
||||
placeholder="数量"
|
||||
style="width:130px"
|
||||
/>
|
||||
<el-button type="danger" text @click="removeItem(idx)">删除</el-button>
|
||||
</div>
|
||||
<el-button type="primary" text @click="addItem">+ 添加物料</el-button>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -151,9 +151,8 @@ onMounted(fetchList)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- Search -->
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input
|
||||
v-model="searchForm.name"
|
||||
placeholder="搜索仓库名称"
|
||||
@@ -167,57 +166,60 @@ onMounted(fetchList)
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="停用" :value="0" />
|
||||
</el-select>
|
||||
<el-button type="primary" :icon="'Search'" @click="handleSearch">查询</el-button>
|
||||
<el-button :icon="'Refresh'" @click="handleReset">重置</el-button>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="table-container">
|
||||
<div class="table-actions">
|
||||
<el-button type="primary" :icon="'Plus'" @click="handleAdd">新增仓库</el-button>
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">仓库列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增仓库</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width: 100%">
|
||||
<el-table-column prop="name" label="仓库名称" min-width="160" />
|
||||
<el-table-column prop="address" label="地址" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="contact_person" label="联系人" width="120" />
|
||||
<el-table-column prop="contact_phone" label="联系电话" width="140" />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:type="row.status === 1 ? 'warning' : 'success'"
|
||||
text
|
||||
@click="handleStatusToggle(row)"
|
||||
>
|
||||
{{ row.status === 1 ? '停用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.page_size"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width: 100%">
|
||||
<el-table-column prop="name" label="仓库名称" min-width="160" />
|
||||
<el-table-column prop="address" label="地址" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="contact_person" label="联系人" width="120" />
|
||||
<el-table-column prop="contact_phone" label="联系电话" width="140" />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:type="row.status === 1 ? 'warning' : 'success'"
|
||||
link
|
||||
@click="handleStatusToggle(row)"
|
||||
>
|
||||
{{ row.status === 1 ? '停用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.page_size"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Dialog -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
<el-row :gutter="16">
|
||||
|
||||
@@ -96,8 +96,8 @@ onMounted(() => { fetchCategories(); fetchList() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.keyword" placeholder="标题搜索" style="width:180px" clearable @keydown.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.category_id" placeholder="分类" clearable style="width:150px">
|
||||
<el-option v-for="c in categoryList" :key="c.id" :label="c.name" :value="c.id" />
|
||||
@@ -111,39 +111,48 @@ onMounted(() => { fetchCategories(); fetchList() })
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增文章</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="title" label="标题" min-width="200" />
|
||||
<el-table-column label="分类" width="120" align="center">
|
||||
<template #default="{ row }">{{ row.category?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="作者" width="100" align="center">
|
||||
<template #default="{ row }">{{ row.author?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="可见范围" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.visibility === 1 ? 'warning' : 'success'" size="small">{{ visibilityMap[row.visibility] || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="views" label="浏览" width="70" align="center" />
|
||||
<el-table-column prop="likes" label="点赞" width="70" align="center" />
|
||||
<el-table-column prop="published_at" label="发布时间" min-width="160" />
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status <= 1" size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.status <= 1" size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
<el-button v-if="row.status < 2" size="small" type="success" link @click="handlePublish(row)">发布</el-button>
|
||||
<el-button v-if="row.status === 2" size="small" type="warning" link @click="handleOffline(row)">下架</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">文章列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增文章</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="title" label="标题" min-width="200" />
|
||||
<el-table-column label="分类" width="120" align="center">
|
||||
<template #default="{ row }">{{ row.category?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="作者" width="100" align="center">
|
||||
<template #default="{ row }">{{ row.author?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="可见范围" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.visibility === 1 ? 'warning' : 'success'" size="small">{{ visibilityMap[row.visibility] || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="views" label="浏览" width="70" align="center" />
|
||||
<el-table-column prop="likes" label="点赞" width="70" align="center" />
|
||||
<el-table-column prop="published_at" label="发布时间" min-width="160" />
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status <= 1" size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.status <= 1" size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
<el-button v-if="row.status < 2" size="small" type="success" text @click="handlePublish(row)">发布</el-button>
|
||||
<el-button v-if="row.status === 2" size="small" type="warning" text @click="handleOffline(row)">下架</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑文章' : '新增文章'" width="600px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
|
||||
@@ -73,8 +73,8 @@ onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="停用" :value="0" />
|
||||
@@ -82,27 +82,36 @@ onMounted(() => fetchList())
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增分类</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe row-key="id" style="width:100%">
|
||||
<el-table-column prop="name" label="分类名称" min-width="180" />
|
||||
<el-table-column label="上级分类" width="120" align="center">
|
||||
<template #default="{ row }">{{ row.parent_id === 0 ? '顶级' : row.parent_id }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status ? 'success' : 'danger'" size="small">{{ row.status ? '启用' : '停用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[20,50,100]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">知识库分类列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增分类</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" row-key="id" style="width:100%">
|
||||
<el-table-column prop="name" label="分类名称" min-width="180" />
|
||||
<el-table-column label="上级分类" width="120" align="center">
|
||||
<template #default="{ row }">{{ row.parent_id === 0 ? '顶级' : row.parent_id }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status ? 'success' : 'danger'" size="small">{{ row.status ? '启用' : '停用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[20,50,100]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑分类' : '新增分类'" width="450px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, reactive } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user.js'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -55,27 +56,27 @@ function handleKeydown(e) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-wrapper">
|
||||
<!-- Background decoration -->
|
||||
<div class="bg-decoration bg-deco-1" />
|
||||
<div class="bg-decoration bg-deco-2" />
|
||||
<div class="min-h-screen flex items-center justify-center bg-rose-50 relative overflow-hidden">
|
||||
<!-- BG blobs -->
|
||||
<div class="absolute w-96 h-96 rounded-full opacity-20 pointer-events-none" style="background: radial-gradient(circle, #f43f5e, transparent); top:-120px; right:-80px;"></div>
|
||||
<div class="absolute w-72 h-72 rounded-full opacity-20 pointer-events-none" style="background: radial-gradient(circle, #fb7185, transparent); bottom:-80px; left:-60px;"></div>
|
||||
|
||||
<div class="login-card">
|
||||
<!-- Logo / Branding -->
|
||||
<div class="brand-area">
|
||||
<div class="brand-icon">
|
||||
<el-icon :size="48" color="#E8A87C"><House /></el-icon>
|
||||
<div class="relative z-10 w-[420px] bg-white rounded-3xl p-12 shadow-2xl text-center">
|
||||
<!-- Logo -->
|
||||
<div class="flex justify-center mb-6">
|
||||
<div class="w-20 h-20 bg-rose-500 rounded-2xl flex items-center justify-center text-white shadow-lg">
|
||||
<Icon icon="solar:heart-bold" class="text-4xl" />
|
||||
</div>
|
||||
<h1 class="brand-title">宫中有喜</h1>
|
||||
<p class="brand-subtitle">月子会所综合管理系统</p>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-gray-800 mb-2 tracking-widest">宫中有喜</h1>
|
||||
<p class="text-gray-400 text-sm mb-8">月子会所综合管理系统</p>
|
||||
|
||||
<!-- Login Form -->
|
||||
<!-- Form -->
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
class="login-form"
|
||||
class="text-left"
|
||||
@keydown="handleKeydown"
|
||||
>
|
||||
<el-form-item prop="username">
|
||||
@@ -102,11 +103,11 @@ function handleKeydown(e) {
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item class="mt-8">
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="login-btn"
|
||||
style="width:100%; height:48px; font-size:16px; letter-spacing:4px; border-radius:12px;"
|
||||
:loading="loading"
|
||||
@click="handleLogin"
|
||||
>
|
||||
@@ -115,108 +116,12 @@ function handleKeydown(e) {
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<p class="login-footer">宫中有喜月子会所 © {{ new Date().getFullYear() }}</p>
|
||||
<div class="text-center mt-2 text-sm text-gray-400">
|
||||
还没有账号?
|
||||
<router-link to="/register" class="text-blue-400 hover:text-blue-300 transition-colors">申请注册</router-link>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-300 mt-4">宫中有喜月子会所 © {{ new Date().getFullYear() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.login-wrapper {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #FBE8D5 0%, #F7F0E8 40%, #EDE0D4 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bg-deco-1 {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: radial-gradient(circle, #E8A87C, transparent);
|
||||
top: -120px;
|
||||
right: -80px;
|
||||
}
|
||||
|
||||
.bg-deco-2 {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: radial-gradient(circle, #C97B5E, transparent);
|
||||
bottom: -80px;
|
||||
left: -60px;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 420px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 48px 44px 36px;
|
||||
box-shadow: 0 20px 60px rgba(200, 120, 80, 0.15);
|
||||
}
|
||||
|
||||
.brand-area {
|
||||
text-align: center;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary-dark);
|
||||
letter-spacing: 4px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
.el-input {
|
||||
--el-input-border-radius: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
padding: 4px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
letter-spacing: 4px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #E8A87C, #D4875A);
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #F0BB97, #E8A87C);
|
||||
}
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-placeholder);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -60,8 +60,8 @@ onMounted(() => { fetchList(); fetchCustomers() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.customer_id" placeholder="选择客户" clearable filterable style="width:180px"><el-option v-for="c in customers" :key="c.id" :label="c.name" :value="c.id" /></el-select>
|
||||
<el-date-picker v-model="searchForm.plan_date" type="date" value-format="YYYY-MM-DD" placeholder="日期" style="width:160px" />
|
||||
<el-select v-model="searchForm.meal_type" placeholder="餐型" clearable style="width:120px"><el-option v-for="(v,k) in mealTypeMap" :key="k" :label="v" :value="Number(k)" /></el-select>
|
||||
@@ -69,27 +69,34 @@ onMounted(() => { fetchList(); fetchCustomers() })
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增排餐</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column label="客户" min-width="100"><template #default="{ row }">{{ row.customer?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="plan_date" label="日期" width="110" />
|
||||
<el-table-column label="餐型" width="80" align="center"><template #default="{ row }">{{ mealTypeMap[row.meal_type] }}</template></el-table-column>
|
||||
<el-table-column prop="special_note" label="特殊备注" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="评分" width="70" align="center"><template #default="{ row }">{{ row.review?.score || '-' }}</template></el-table-column>
|
||||
<el-table-column label="状态" width="90" align="center"><template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status<2" size="small" type="success" link @click="handleDeliver(row)">送达</el-button>
|
||||
<el-dropdown v-if="row.status<2" trigger="click" @command="(cmd) => handleStatusChange(row, cmd)" style="margin:0 8px">
|
||||
<el-button size="small" type="warning" link>状态</el-button>
|
||||
<template #dropdown><el-dropdown-menu><el-dropdown-item v-for="(v,k) in statusMap" :key="k" :command="Number(k)">{{ v }}</el-dropdown-item></el-dropdown-menu></template>
|
||||
</el-dropdown>
|
||||
<el-button v-if="row.status===0" size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,30,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3"></div>
|
||||
<el-button type="primary" @click="handleAdd">新增排餐</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column label="客户" min-width="100"><template #default="{ row }">{{ row.customer?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="plan_date" label="日期" width="110" />
|
||||
<el-table-column label="餐型" width="80" align="center"><template #default="{ row }">{{ mealTypeMap[row.meal_type] }}</template></el-table-column>
|
||||
<el-table-column prop="special_note" label="特殊备注" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="评分" width="70" align="center"><template #default="{ row }">{{ row.review?.score || '-' }}</template></el-table-column>
|
||||
<el-table-column label="状态" width="90" align="center"><template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status<2" size="small" type="success" text @click="handleDeliver(row)">送达</el-button>
|
||||
<el-dropdown v-if="row.status<2" trigger="click" @command="(cmd) => handleStatusChange(row, cmd)" style="margin:0 8px">
|
||||
<el-button size="small" type="warning" text>状态</el-button>
|
||||
<template #dropdown><el-dropdown-menu><el-dropdown-item v-for="(v,k) in statusMap" :key="k" :command="Number(k)">{{ v }}</el-dropdown-item></el-dropdown-menu></template>
|
||||
</el-dropdown>
|
||||
<el-button v-if="row.status===0" size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,30,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑排餐':'新增排餐'" width="550px" destroy-on-close>
|
||||
<el-form :model="form" label-width="80px">
|
||||
|
||||
@@ -51,30 +51,37 @@ onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.name" placeholder="菜品名称" style="width:180px" clearable @keydown.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.category" placeholder="分类" clearable style="width:120px"><el-option v-for="c in categories" :key="c" :label="c" :value="c" /></el-select>
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px"><el-option label="启用" :value="1" /><el-option label="禁用" :value="0" /></el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增菜品</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="name" label="菜品名称" min-width="120" />
|
||||
<el-table-column prop="category" label="分类" width="80" align="center" />
|
||||
<el-table-column prop="price" label="价格" width="90" align="right" />
|
||||
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="row.status?'success':'danger'" size="small">{{ row.status?'启用':'禁用' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3"></div>
|
||||
<el-button type="primary" @click="handleAdd">新增菜品</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="name" label="菜品名称" min-width="120" />
|
||||
<el-table-column prop="category" label="分类" width="80" align="center" />
|
||||
<el-table-column prop="price" label="价格" width="90" align="right" />
|
||||
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="row.status?'success':'danger'" size="small">{{ row.status?'启用':'禁用' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑菜品':'新增菜品'" width="550px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="80px">
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { mealReviewApi } from '@/api/meal.js'
|
||||
import { customerApi } from '@/api/crm.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const currentDetail = ref(null)
|
||||
const customers = ref([])
|
||||
|
||||
const searchForm = reactive({ customer_id: '', rating: '', meal_date: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
const ratingOptions = [
|
||||
{ label: '5星', value: 5 },
|
||||
{ label: '4星', value: 4 },
|
||||
{ label: '3星', value: 3 },
|
||||
{ label: '2星', value: 2 },
|
||||
{ label: '1星', value: 1 }
|
||||
]
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await mealReviewApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} catch {
|
||||
ElMessage.error('加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomers() {
|
||||
try {
|
||||
const res = await customerApi.getList({ per_page: 500 })
|
||||
customers.value = res.data?.list || res.data?.data || []
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() {
|
||||
Object.assign(searchForm, { customer_id: '', rating: '', meal_date: '' })
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handleView(row) {
|
||||
currentDetail.value = row
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
function getRatingColor(rating) {
|
||||
if (rating >= 4) return 'text-green-500'
|
||||
if (rating >= 3) return 'text-yellow-500'
|
||||
return 'text-red-400'
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
|
||||
onMounted(() => { fetchList(); fetchCustomers() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.customer_id" placeholder="选择客户" clearable filterable style="width:180px">
|
||||
<el-option v-for="c in customers" :key="c.id" :label="c.name" :value="c.id" />
|
||||
</el-select>
|
||||
<el-select v-model="searchForm.rating" placeholder="评分筛选" clearable style="width:130px">
|
||||
<el-option v-for="r in ratingOptions" :key="r.value" :label="r.label" :value="r.value" />
|
||||
</el-select>
|
||||
<el-date-picker v-model="searchForm.meal_date" type="date" value-format="YYYY-MM-DD" placeholder="就餐日期" style="width:160px" />
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<Icon icon="solar:magnifer-bold-duotone" class="mr-1" />查询
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 表格区 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-1 h-5 bg-rose-500 rounded-full inline-block"></span>
|
||||
<span class="font-semibold text-gray-700">膳食评价列表</span>
|
||||
<span class="text-xs text-gray-400 ml-1">(只读,客户评价不可编辑)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column label="客户" min-width="100">
|
||||
<template #default="{ row }">{{ row.customer_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="meal_date" label="就餐日期" width="110" />
|
||||
<el-table-column label="评分" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center justify-center gap-0.5">
|
||||
<Icon
|
||||
v-for="i in 5"
|
||||
:key="i"
|
||||
:icon="i <= row.rating ? 'solar:star-bold' : 'solar:star-line-duotone'"
|
||||
:class="['text-base', i <= row.rating ? 'text-amber-400' : 'text-gray-200']"
|
||||
/>
|
||||
<span :class="['ml-1 text-sm font-semibold', getRatingColor(row.rating)]">{{ row.rating }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="content" label="评价内容" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="照片" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.photos && row.photos.length" class="text-rose-500 text-sm">{{ row.photos.length }} 张</span>
|
||||
<span v-else class="text-gray-300 text-sm">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="评价时间" min-width="160" />
|
||||
<el-table-column label="操作" width="80" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleView(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.per_page"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total,sizes,prev,pager,next,jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog v-model="detailVisible" title="评价详情" width="520px" destroy-on-close>
|
||||
<div v-loading="detailLoading">
|
||||
<template v-if="currentDetail">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="客户">{{ currentDetail.customer_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="就餐日期">{{ currentDetail.meal_date || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="评分" :span="2">
|
||||
<div class="flex items-center gap-1">
|
||||
<Icon
|
||||
v-for="i in 5"
|
||||
:key="i"
|
||||
:icon="i <= currentDetail.rating ? 'solar:star-bold' : 'solar:star-line-duotone'"
|
||||
:class="['text-lg', i <= currentDetail.rating ? 'text-amber-400' : 'text-gray-200']"
|
||||
/>
|
||||
<span :class="['ml-1 font-semibold', getRatingColor(currentDetail.rating)]">
|
||||
{{ currentDetail.rating }} / 5
|
||||
</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="评价内容" :span="2">
|
||||
{{ currentDetail.content || '(无文字评价)' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="评价时间" :span="2">{{ currentDetail.created_at || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<!-- 照片 -->
|
||||
<div v-if="currentDetail.photos && currentDetail.photos.length" class="mt-4">
|
||||
<p class="text-sm text-gray-500 mb-2">评价照片({{ currentDetail.photos.length }} 张)</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<el-image
|
||||
v-for="(src, idx) in currentDetail.photos"
|
||||
:key="idx"
|
||||
:src="src"
|
||||
:preview-src-list="currentDetail.photos"
|
||||
:initial-index="idx"
|
||||
fit="cover"
|
||||
class="w-20 h-20 rounded-lg object-cover cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,190 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { mealTemplateApi } from '@/api/meal.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
const searchForm = reactive({ name: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
id: null, name: '', description: '', week_num: 1, status: 1
|
||||
})
|
||||
const isEdit = ref(false)
|
||||
|
||||
const weekOptions = Array.from({ length: 6 }, (_, i) => ({ label: `第 ${i + 1} 周`, value: i + 1 }))
|
||||
const statusMap = { 1: '启用', 0: '停用' }
|
||||
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '请输入模板名称', trigger: 'blur' }],
|
||||
week_num: [{ required: true, message: '请选择适用周数', trigger: 'change' }]
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await mealTemplateApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} catch {
|
||||
ElMessage.error('加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() { Object.assign(searchForm, { name: '' }); handleSearch() }
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false
|
||||
Object.assign(form, { id: null, name: '', description: '', week_num: 1, status: 1 })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleEdit(row) {
|
||||
isEdit.value = true
|
||||
try {
|
||||
const res = await mealTemplateApi.getDetail(row.id)
|
||||
const data = res.data?.data || res.data || row
|
||||
Object.assign(form, {
|
||||
id: data.id, name: data.name, description: data.description || '',
|
||||
week_num: data.week_num, status: data.status ?? 1
|
||||
})
|
||||
} catch {
|
||||
Object.assign(form, { id: row.id, name: row.name, description: row.description || '', week_num: row.week_num, status: row.status ?? 1 })
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除模板「${row.name}」吗?`, '删除确认', { type: 'warning' })
|
||||
try {
|
||||
await mealTemplateApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchList()
|
||||
} catch {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await mealTemplateApi.update(form.id, form)
|
||||
} else {
|
||||
await mealTemplateApi.create(form)
|
||||
}
|
||||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
fetchList()
|
||||
} catch {
|
||||
ElMessage.error('操作失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
|
||||
onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.name" placeholder="模板名称" clearable style="width:200px" />
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<Icon icon="solar:magnifer-bold-duotone" class="mr-1" />查询
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 表格区 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-1 h-5 bg-rose-500 rounded-full inline-block"></span>
|
||||
<span class="font-semibold text-gray-700">排餐模板管理</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<Icon icon="solar:add-circle-bold-duotone" class="mr-1" />新建模板
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="name" label="模板名称" min-width="160" />
|
||||
<el-table-column label="适用周数" width="100" align="center">
|
||||
<template #default="{ row }">第 {{ row.week_num }} 周</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="140" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.per_page"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total,sizes,prev,pager,next,jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑弹窗 -->
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑模板' : '新建排餐模板'" width="520px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
<el-form-item label="模板名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="如:产后第1周营养餐模板" />
|
||||
</el-form-item>
|
||||
<el-form-item label="适用周数" prop="week_num">
|
||||
<el-select v-model="form.week_num" style="width:100%">
|
||||
<el-option v-for="w in weekOptions" :key="w.value" :label="w.label" :value="w.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="模板描述">
|
||||
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="简要说明此模板的膳食方向和特点" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -62,31 +62,40 @@ onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.name" placeholder="姓名" style="width:150px" clearable />
|
||||
<el-select v-model="searchForm.level" placeholder="等级" clearable style="width:120px"><el-option v-for="(v,k) in levelMap" :key="k" :label="v" :value="Number(k)" /></el-select>
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px"><el-option v-for="(v,k) in statusMap" :key="k" :label="v" :value="Number(k)" /></el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增月嫂</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe>
|
||||
<el-table-column prop="name" label="姓名" width="100" />
|
||||
<el-table-column prop="phone" label="手机号" width="130" />
|
||||
<el-table-column label="等级" width="80" align="center"><template #default="{ row }">{{ levelMap[row.level] || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="experience_years" label="经验(年)" width="90" align="center" />
|
||||
<el-table-column prop="daily_price" label="日价(元)" width="100" align="right" />
|
||||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[15,30,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">月嫂列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增月嫂</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="name" label="姓名" width="100" />
|
||||
<el-table-column prop="phone" label="手机号" width="130" />
|
||||
<el-table-column label="等级" width="80" align="center"><template #default="{ row }">{{ levelMap[row.level] || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="experience_years" label="经验(年)" width="90" align="center" />
|
||||
<el-table-column prop="daily_price" label="日价(元)" width="100" align="right" />
|
||||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[15,30,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑月嫂':'新增月嫂'" width="550px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
|
||||
@@ -62,32 +62,41 @@ onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.order_no" placeholder="订单号" style="width:180px" clearable />
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px"><el-option v-for="(v,k) in statusMap" :key="k" :label="v" :value="Number(k)" /></el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增订单</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe>
|
||||
<el-table-column prop="order_no" label="订单号" min-width="160" />
|
||||
<el-table-column label="客户" min-width="100"><template #default="{ row }">{{ row.customer?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column label="月嫂" width="100"><template #default="{ row }">{{ row.nanny?.name || '待匹配' }}</template></el-table-column>
|
||||
<el-table-column label="服务类型" width="90"><template #default="{ row }">{{ serviceTypeMap[row.service_type] || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="start_date" label="开始日期" width="110" />
|
||||
<el-table-column prop="days" label="天数" width="70" align="center" />
|
||||
<el-table-column prop="price" label="费用(元)" width="100" align="right" />
|
||||
<el-table-column label="状态" width="90" align="center"><template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status<4" size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.status<2" size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[15,30,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">月嫂订单列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增订单</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="order_no" label="订单号" min-width="160" />
|
||||
<el-table-column label="客户" min-width="100"><template #default="{ row }">{{ row.customer?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column label="月嫂" width="100"><template #default="{ row }">{{ row.nanny?.name || '待匹配' }}</template></el-table-column>
|
||||
<el-table-column label="服务类型" width="90"><template #default="{ row }">{{ serviceTypeMap[row.service_type] || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="start_date" label="开始日期" width="110" />
|
||||
<el-table-column prop="days" label="天数" width="70" align="center" />
|
||||
<el-table-column prop="price" label="费用(元)" width="100" align="right" />
|
||||
<el-table-column label="状态" width="90" align="center"><template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status<4" size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.status<2" size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[15,30,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑订单':'新增订单'" width="550px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
|
||||
@@ -48,24 +48,33 @@ onMounted(() => { fetchList(); fetchNannies() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.nanny_id" placeholder="选择月嫂" clearable style="width:160px"><el-option v-for="n in nannies" :key="n.id" :label="n.name" :value="n.id" /></el-select>
|
||||
<el-date-picker v-model="searchForm.start_date" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" />
|
||||
<el-date-picker v-model="searchForm.end_date" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" />
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增排班</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe>
|
||||
<el-table-column label="月嫂" min-width="100"><template #default="{ row }">{{ row.nanny?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="schedule_date" label="日期" width="120" />
|
||||
<el-table-column label="类型" width="90" align="center"><template #default="{ row }"><el-tag :type="typeType[row.type]" size="small">{{ typeMap[row.type] }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="关联订单" min-width="120"><template #default="{ row }">{{ row.order?.order_no || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="150" />
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" :total="total" layout="total,prev,pager,next" background @current-change="handlePageChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">排班记录</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增排班</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column label="月嫂" min-width="100"><template #default="{ row }">{{ row.nanny?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="schedule_date" label="日期" width="120" />
|
||||
<el-table-column label="类型" width="90" align="center"><template #default="{ row }"><el-tag :type="typeType[row.type]" size="small">{{ typeMap[row.type] }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="关联订单" min-width="120"><template #default="{ row }">{{ row.order?.order_no || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="150" />
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" :total="total" layout="total,prev,pager,next" background @current-change="handlePageChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" title="新增排班" width="450px" destroy-on-close>
|
||||
<el-form label-width="80px">
|
||||
|
||||
@@ -78,8 +78,8 @@ onMounted(() => { fetchList() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.type" placeholder="类型" clearable style="width:120px">
|
||||
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
@@ -89,38 +89,45 @@ onMounted(() => { fetchList() })
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">公告列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增公告</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="类型" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="typeTagType[row.type]" size="small">{{ typeMap[row.type] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="置顶" width="70" align="center">
|
||||
<template #default="{ row }">{{ row.is_top ? '是' : '否' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发布状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="publishStatusType[row.publish_status]" size="small">{{ publishStatusMap[row.publish_status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="作者" min-width="100">
|
||||
<template #default="{ row }">{{ row.author?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="published_at" label="发布时间" min-width="160" />
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.publish_status === 0" size="small" type="success" link @click="handlePublish(row)">发布</el-button>
|
||||
<el-button v-if="row.publish_status === 0" size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.publish_status === 0" size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="类型" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="typeTagType[row.type]" size="small">{{ typeMap[row.type] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="置顶" width="70" align="center">
|
||||
<template #default="{ row }">{{ row.is_top ? '是' : '否' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发布状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="publishStatusType[row.publish_status]" size="small">{{ publishStatusMap[row.publish_status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="作者" min-width="100">
|
||||
<template #default="{ row }">{{ row.author?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="published_at" label="发布时间" min-width="160" />
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.publish_status === 0" size="small" type="success" text @click="handlePublish(row)">发布</el-button>
|
||||
<el-button v-if="row.publish_status === 0" size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.publish_status === 0" size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑公告' : '新增公告'" width="600px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="80px">
|
||||
|
||||
@@ -102,8 +102,8 @@ onMounted(() => { fetchList() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.type" placeholder="模板类型" clearable style="width:140px">
|
||||
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
@@ -115,31 +115,38 @@ onMounted(() => { fetchList() })
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<div class="table-actions">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">审批模板列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增模板</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="name" label="模板名称" min-width="160" />
|
||||
<el-table-column label="类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ typeMap[row.type] || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">{{ row.status === 1 ? '启用' : '停用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="name" label="模板名称" min-width="160" />
|
||||
<el-table-column label="类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ typeMap[row.type] || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">{{ row.status === 1 ? '启用' : '停用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" destroy-on-close>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { approvalApi } from '@/api/office.js'
|
||||
import { approvalApi, approvalTemplateApi } from '@/api/office.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const rejectDialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
const searchForm = reactive({ status: '' })
|
||||
@@ -14,9 +16,15 @@ const pagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
const formRef = ref(null)
|
||||
const form = reactive({ title: '', template_id: '', form_data: '' })
|
||||
const templates = ref([])
|
||||
|
||||
const currentDetail = ref(null)
|
||||
const rejectForm = reactive({ comment: '' })
|
||||
const rejectTargetRow = ref(null)
|
||||
|
||||
const statusMap = { 0: '待审批', 1: '审批中', 2: '已通过', 3: '已驳回', 4: '已撤回' }
|
||||
const statusType = { 0: 'warning', 1: 'primary', 2: 'success', 3: 'danger', 4: 'info' }
|
||||
const nodeActionMap = { 0: '待处理', 1: '已通过', 2: '已驳回' }
|
||||
|
||||
const formRules = {
|
||||
title: [{ required: true, message: '请输入审批标题', trigger: 'blur' }]
|
||||
@@ -31,6 +39,13 @@ async function fetchList() {
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function fetchTemplates() {
|
||||
try {
|
||||
const res = await approvalTemplateApi.getList({ per_page: 100, status: 1 })
|
||||
templates.value = res.data?.list || res.data?.data || []
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() {
|
||||
Object.assign(searchForm, { status: '' })
|
||||
@@ -52,25 +67,43 @@ async function handleSubmit() {
|
||||
if (payload.form_data) {
|
||||
try { payload.form_data = JSON.parse(payload.form_data) } catch { /* keep string */ }
|
||||
}
|
||||
if (!payload.template_id) delete payload.template_id
|
||||
await approvalApi.create(payload)
|
||||
ElMessage.success('提交成功')
|
||||
ElMessage.success('提交成功,审批节点已自动创建')
|
||||
dialogVisible.value = false
|
||||
fetchList()
|
||||
} finally { submitLoading.value = false }
|
||||
})
|
||||
}
|
||||
|
||||
async function handleShowDetail(row) {
|
||||
try {
|
||||
const res = await approvalApi.getDetail(row.id)
|
||||
currentDetail.value = res.data
|
||||
detailVisible.value = true
|
||||
} catch {
|
||||
ElMessage.error('加载详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApprove(row) {
|
||||
await ElMessageBox.confirm('确定通过该审批吗?', '审批确认', { type: 'warning' })
|
||||
await approvalApi.approve(row.id, { status: 2 })
|
||||
await ElMessageBox.confirm('确定通过该审批节点吗?', '审批确认', { type: 'warning' })
|
||||
await approvalApi.approve(row.id, {})
|
||||
ElMessage.success('审批已通过')
|
||||
fetchList()
|
||||
}
|
||||
|
||||
async function handleReject(row) {
|
||||
await ElMessageBox.confirm('确定驳回该审批吗?', '驳回确认', { type: 'warning' })
|
||||
await approvalApi.reject(row.id, { status: 3 })
|
||||
function handleRejectOpen(row) {
|
||||
rejectTargetRow.value = row
|
||||
rejectForm.comment = ''
|
||||
rejectDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleRejectSubmit() {
|
||||
if (!rejectTargetRow.value) return
|
||||
await approvalApi.reject(rejectTargetRow.value.id, { comment: rejectForm.comment })
|
||||
ElMessage.success('审批已驳回')
|
||||
rejectDialogVisible.value = false
|
||||
fetchList()
|
||||
}
|
||||
|
||||
@@ -81,15 +114,30 @@ async function handleWithdraw(row) {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function getStepStatus(node, currentNode) {
|
||||
if (node.node_order < currentNode) {
|
||||
return node.action === 2 ? 'error' : 'finish'
|
||||
}
|
||||
if (node.node_order === currentNode) {
|
||||
return node.action === 0 ? 'process' : node.action === 1 ? 'finish' : 'error'
|
||||
}
|
||||
return 'wait'
|
||||
}
|
||||
|
||||
function sortedNodes(nodes) {
|
||||
return [...(nodes || [])].sort((a, b) => a.node_order - b.node_order)
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
|
||||
onMounted(() => { fetchList() })
|
||||
onMounted(() => { fetchList(); fetchTemplates() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.status" placeholder="审批状态" clearable style="width:140px">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
@@ -97,47 +145,73 @@ onMounted(() => { fetchList() })
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<div class="table-actions">
|
||||
<!-- 列表 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<span class="font-medium text-gray-700">审批列表</span>
|
||||
<el-button type="primary" @click="handleAdd">发起审批</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="title" label="标题" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="申请人" min-width="100">
|
||||
<template #default="{ row }">{{ row.applicant?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审批模板" min-width="120">
|
||||
<template #default="{ row }">{{ row.template?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.status <= 1">
|
||||
<el-button size="small" type="success" link @click="handleApprove(row)">通过</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleReject(row)">驳回</el-button>
|
||||
<el-button size="small" type="warning" link @click="handleWithdraw(row)">撤回</el-button>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column label="标题" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleShowDetail(row)">{{ row.title }}</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</el-table-column>
|
||||
<el-table-column label="申请人" min-width="100">
|
||||
<template #default="{ row }">{{ row.applicant?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审批模板" min-width="120">
|
||||
<template #default="{ row }">{{ row.template?.name || '无模板' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="当前节点" width="90" align="center">
|
||||
<template #default="{ row }">第 {{ row.current_node || 1 }} 级</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="230" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" text @click="handleShowDetail(row)">详情</el-button>
|
||||
<template v-if="row.status <= 1">
|
||||
<el-button size="small" type="success" text @click="handleApprove(row)">通过</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleRejectOpen(row)">驳回</el-button>
|
||||
<el-button size="small" type="warning" text @click="handleWithdraw(row)">撤回</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.per_page"
|
||||
:total="total"
|
||||
:page-sizes="[10,20,50]"
|
||||
layout="total,sizes,prev,pager,next,jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 发起审批弹窗 -->
|
||||
<el-dialog v-model="dialogVisible" title="发起审批" width="600px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
|
||||
<el-form-item label="审批标题" prop="title">
|
||||
<el-input v-model="form.title" placeholder="请输入审批标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="审批模板">
|
||||
<el-input v-model="form.template_id" placeholder="请输入模板ID" />
|
||||
<el-select v-model="form.template_id" placeholder="请选择审批模板(可选,选择后自动生成多级审批节点)" clearable style="width:100%">
|
||||
<el-option v-for="t in templates" :key="t.id" :label="t.name" :value="t.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="表单数据">
|
||||
<el-input v-model="form.form_data" type="textarea" :rows="4" placeholder="JSON格式" />
|
||||
<el-input v-model="form.form_data" type="textarea" :rows="3" placeholder="JSON格式(可选)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -145,5 +219,69 @@ onMounted(() => { fetchList() })
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 审批详情弹窗(含多级节点进度) -->
|
||||
<el-dialog v-model="detailVisible" title="审批详情" width="700px" destroy-on-close>
|
||||
<template v-if="currentDetail">
|
||||
<el-descriptions :column="2" border class="mb-5">
|
||||
<el-descriptions-item label="审批标题" :span="2">{{ currentDetail.title }}</el-descriptions-item>
|
||||
<el-descriptions-item label="申请人">{{ currentDetail.applicant?.name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="审批模板">{{ currentDetail.template?.name || '无模板' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="statusType[currentDetail.status]" size="small">{{ statusMap[currentDetail.status] }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="当前节点">第 {{ currentDetail.current_node || 1 }} 级</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">{{ currentDetail.created_at }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 审批节点进度 -->
|
||||
<div class="font-medium text-gray-700 mb-3">审批流程进度</div>
|
||||
<div v-if="currentDetail.nodes && currentDetail.nodes.length">
|
||||
<el-steps
|
||||
direction="vertical"
|
||||
:active="currentDetail.current_node"
|
||||
space="80px"
|
||||
>
|
||||
<el-step
|
||||
v-for="node in sortedNodes(currentDetail.nodes)"
|
||||
:key="node.id"
|
||||
:title="node.node_label || `第${node.node_order}级审批`"
|
||||
:status="getStepStatus(node, currentDetail.current_node)"
|
||||
>
|
||||
<template #description>
|
||||
<div class="text-sm space-y-1 py-1 text-gray-500">
|
||||
<div>审批人:<span class="text-gray-700">{{ node.approver?.name || (node.role ? `[${node.role}角色]` : '待指定') }}</span></div>
|
||||
<div>
|
||||
状态:
|
||||
<el-tag size="small" :type="node.action === 1 ? 'success' : node.action === 2 ? 'danger' : 'info'">
|
||||
{{ nodeActionMap[node.action] || '-' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div v-if="node.comment">意见:<span class="text-gray-700">{{ node.comment }}</span></div>
|
||||
<div v-if="node.acted_at" class="text-xs text-gray-400">操作时间:{{ node.acted_at }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-step>
|
||||
</el-steps>
|
||||
</div>
|
||||
<el-empty v-else description="暂无审批节点(提交后自动按模板生成)" :image-size="60" />
|
||||
</template>
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 驳回意见弹窗 -->
|
||||
<el-dialog v-model="rejectDialogVisible" title="驳回审批" width="460px" destroy-on-close>
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="驳回意见">
|
||||
<el-input v-model="rejectForm.comment" type="textarea" :rows="3" placeholder="请输入驳回意见(可选)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="rejectDialogVisible = false">取消</el-button>
|
||||
<el-button type="danger" @click="handleRejectSubmit">确认驳回</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -67,8 +67,8 @@ onMounted(() => { fetchList() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-date-picker v-model="searchForm.shift_date" type="date" value-format="YYYY-MM-DD" placeholder="交接日期" clearable style="width:160px" />
|
||||
<el-select v-model="searchForm.shift_type" placeholder="班次" clearable style="width:120px">
|
||||
<el-option v-for="(v, k) in shiftTypeMap" :key="k" :label="v" :value="Number(k)" />
|
||||
@@ -76,36 +76,43 @@ onMounted(() => { fetchList() })
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">交接记录列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增交接</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="shift_date" label="交接日期" width="120" />
|
||||
<el-table-column label="班次" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="shiftTagType[row.shift_type]" size="small">{{ shiftTypeMap[row.shift_type] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="交班人" min-width="100">
|
||||
<template #default="{ row }">{{ row.handover_user?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="接班人" min-width="100">
|
||||
<template #default="{ row }">{{ row.receiver_user?.name || '待确认' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="content" label="交接内容" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status === 0" size="small" type="success" link @click="handleConfirm(row)">确认接班</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="shift_date" label="交接日期" width="120" />
|
||||
<el-table-column label="班次" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="shiftTagType[row.shift_type]" size="small">{{ shiftTypeMap[row.shift_type] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="交班人" min-width="100">
|
||||
<template #default="{ row }">{{ row.handover_user?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="接班人" min-width="100">
|
||||
<template #default="{ row }">{{ row.receiver_user?.name || '待确认' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="content" label="交接内容" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status === 0" size="small" type="success" text @click="handleConfirm(row)">确认接班</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" title="新增交接记录" width="600px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
|
||||
@@ -48,9 +48,8 @@ onMounted(fetchList)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- Search -->
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.type" placeholder="通知类型" clearable style="width: 140px">
|
||||
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
@@ -62,50 +61,49 @@ onMounted(fetchList)
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="table-container">
|
||||
<div class="table-actions">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">通知列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleMarkAllRead">全部标记已读</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width: 100%">
|
||||
<el-table-column prop="title" label="标题" min-width="180" />
|
||||
<el-table-column prop="content" label="内容" min-width="260" show-overflow-tooltip />
|
||||
<el-table-column label="类型" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="typeStyle[row.type]" size="small">{{ typeMap[row.type] || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="read-dot" :class="row.is_read ? 'read' : 'unread'" />
|
||||
<span>{{ row.is_read ? '已读' : '未读' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="!row.is_read"
|
||||
size="small"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleMarkRead(row)"
|
||||
>标记已读</el-button>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.per_page"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width: 100%">
|
||||
<el-table-column prop="title" label="标题" min-width="180" />
|
||||
<el-table-column prop="content" label="内容" min-width="260" show-overflow-tooltip />
|
||||
<el-table-column label="类型" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="typeStyle[row.type]" size="small">{{ typeMap[row.type] || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="read-dot" :class="row.is_read ? 'read' : 'unread'" />
|
||||
<span>{{ row.is_read ? '已读' : '未读' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="!row.is_read" size="small" type="primary" text @click="handleMarkRead(row)">标记已读</el-button>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.per_page"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import axios from 'axios'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const packagesLoading = ref(false)
|
||||
const packages = ref([])
|
||||
const stores = ref([])
|
||||
|
||||
const form = reactive({
|
||||
store_id: '',
|
||||
registration_package_id: '',
|
||||
username: '',
|
||||
password: '',
|
||||
name: '',
|
||||
phone: '',
|
||||
})
|
||||
|
||||
const formRef = ref(null)
|
||||
const rules = {
|
||||
store_id: [{ required: true, message: '请选择门店', trigger: 'change' }],
|
||||
registration_package_id: [{ required: true, message: '请选择岗位套餐', trigger: 'change' }],
|
||||
username: [
|
||||
{ required: true, message: '请输入登录账号', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '2-50个字符', trigger: 'blur' },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码不少于6位', trigger: 'blur' },
|
||||
],
|
||||
name: [{ required: true, message: '请输入真实姓名', trigger: 'blur' }],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
|
||||
async function fetchStores() {
|
||||
try {
|
||||
const res = await axios.get('/api/v1/auth/stores')
|
||||
stores.value = res.data?.data || []
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPackages() {
|
||||
if (!form.store_id) return
|
||||
packagesLoading.value = true
|
||||
form.registration_package_id = ''
|
||||
try {
|
||||
const res = await axios.get('/api/v1/auth/register/packages', {
|
||||
params: { store_id: form.store_id },
|
||||
})
|
||||
packages.value = res.data?.data || []
|
||||
} catch {
|
||||
packages.value = []
|
||||
} finally {
|
||||
packagesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
loading.value = true
|
||||
try {
|
||||
await axios.post('/api/v1/auth/register', form)
|
||||
ElMessage.success('注册成功!请等待管理员审核后登录')
|
||||
setTimeout(() => router.push('/login'), 1500)
|
||||
} catch (e) {
|
||||
const msg = e.response?.data?.message || '注册失败,请稍后重试'
|
||||
ElMessage.error(msg)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(fetchStores)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-gradient-to-br from-pink-50 to-purple-50 flex items-center justify-center p-4">
|
||||
<div class="bg-white rounded-3xl shadow-xl w-full max-w-md p-8">
|
||||
<!-- Logo -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="text-3xl mb-1">🏠</div>
|
||||
<h1 class="text-2xl font-bold text-gray-800">宫中有喜</h1>
|
||||
<p class="text-sm text-gray-400 mt-1">月子会所管理系统 · 员工注册</p>
|
||||
</div>
|
||||
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
|
||||
<el-form-item label="所属门店" prop="store_id">
|
||||
<el-select
|
||||
v-model="form.store_id"
|
||||
placeholder="请选择门店"
|
||||
style="width: 100%"
|
||||
@change="fetchPackages"
|
||||
>
|
||||
<el-option
|
||||
v-for="s in stores"
|
||||
:key="s.id"
|
||||
:label="s.name"
|
||||
:value="s.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="申请岗位" prop="registration_package_id">
|
||||
<el-select
|
||||
v-model="form.registration_package_id"
|
||||
placeholder="请先选择门店"
|
||||
style="width: 100%"
|
||||
:loading="packagesLoading"
|
||||
:disabled="!form.store_id"
|
||||
>
|
||||
<el-option
|
||||
v-for="p in packages"
|
||||
:key="p.id"
|
||||
:label="p.name"
|
||||
:value="p.id"
|
||||
>
|
||||
<span>{{ p.name }}</span>
|
||||
<span v-if="p.description" class="text-gray-400 text-xs ml-2">{{ p.description }}</span>
|
||||
</el-option>
|
||||
<template v-if="packages.length === 0 && form.store_id && !packagesLoading" #empty>
|
||||
<div class="text-center py-4 text-gray-400 text-sm">该门店暂无可用套餐</div>
|
||||
</template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="登录账号" prop="username">
|
||||
<el-input v-model="form.username" placeholder="字母/数字" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="登录密码" prop="password">
|
||||
<el-input v-model="form.password" type="password" placeholder="不少于6位" show-password />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="真实姓名" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入姓名" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="11位手机号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
style="width: 100%; margin-top: 8px"
|
||||
:loading="loading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
提交注册申请
|
||||
</el-button>
|
||||
</el-form>
|
||||
|
||||
<div class="text-center mt-6 text-sm text-gray-400">
|
||||
已有账号?
|
||||
<router-link to="/login" class="text-blue-500 hover:underline">立即登录</router-link>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 p-3 bg-amber-50 rounded-xl text-xs text-amber-600 leading-relaxed">
|
||||
⚠️ 注册后需管理员审核,审核通过后方可登录系统
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -81,9 +81,8 @@ onMounted(fetchList)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- Search -->
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.type" placeholder="报表类型" style="width: 160px" clearable @keydown.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width: 130px">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="Number(k)" />
|
||||
@@ -92,47 +91,52 @@ onMounted(fetchList)
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="table-container">
|
||||
<div class="table-actions">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">导出记录列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新建导出</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width: 100%">
|
||||
<el-table-column prop="name" label="报表名称" min-width="160" />
|
||||
<el-table-column prop="type" label="类型" min-width="120" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作人" min-width="100">
|
||||
<template #default="{ row }">{{ row.operator?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="100" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.status === 1 && row.file_path"
|
||||
size="small"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleDownload(row)"
|
||||
>下载</el-button>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.per_page"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width: 100%">
|
||||
<el-table-column prop="name" label="报表名称" min-width="160" />
|
||||
<el-table-column prop="type" label="类型" min-width="120" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作人" min-width="100">
|
||||
<template #default="{ row }">{{ row.operator?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="100" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.status === 1 && row.file_path"
|
||||
size="small"
|
||||
type="primary"
|
||||
text
|
||||
@click="handleDownload(row)"
|
||||
>下载</el-button>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.per_page"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Dialog -->
|
||||
|
||||
@@ -34,10 +34,13 @@ onMounted(fetchOverview)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container" v-loading="loading">
|
||||
<div class="section-title">经营概览</div>
|
||||
<div class="p-6 space-y-4" v-loading="loading">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="w-1 h-5 rounded bg-primary inline-block" style="background:var(--color-primary)"></span>
|
||||
<span class="font-semibold text-base text-gray-800">经营概览</span>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16" class="mt-16">
|
||||
<el-row :gutter="16">
|
||||
<el-col
|
||||
v-for="card in cards"
|
||||
:key="card.key"
|
||||
@@ -47,20 +50,18 @@ onMounted(fetchOverview)
|
||||
:lg="8"
|
||||
:xl="4"
|
||||
>
|
||||
<el-card shadow="hover" class="overview-card">
|
||||
<div class="card-inner">
|
||||
<div class="card-icon-wrap" :style="{ background: card.bg }">
|
||||
<span class="card-icon-text" :style="{ color: card.color }">{{ card.title.charAt(0) }}</span>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-label">{{ card.title }}</div>
|
||||
<div class="card-value">
|
||||
<span class="card-number" :style="{ color: card.color }">{{ card.value }}</span>
|
||||
<span class="card-unit">{{ card.unit }}</span>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-5 mb-4 flex items-center gap-4">
|
||||
<div class="w-14 h-14 rounded-xl flex items-center justify-center flex-shrink-0" :style="{ background: card.bg }">
|
||||
<span class="text-2xl font-bold" :style="{ color: card.color }">{{ card.title.charAt(0) }}</span>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm text-gray-500 mb-1">{{ card.title }}</div>
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="text-3xl font-bold leading-none" :style="{ color: card.color }">{{ card.value }}</span>
|
||||
<span class="text-sm text-gray-400">{{ card.unit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { roomApi, reservationApi } from '@/api/room.js'
|
||||
|
||||
const rooms = ref([])
|
||||
const loading = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const detailRoom = ref(null)
|
||||
const filterStatus = ref('')
|
||||
|
||||
const statusConfig = {
|
||||
available: { label: '空置', color: 'bg-emerald-50 border-emerald-200', badge: 'bg-emerald-500', text: 'text-emerald-700', dot: 'bg-emerald-400' },
|
||||
occupied: { label: '在住', color: 'bg-blue-50 border-blue-200', badge: 'bg-blue-500', text: 'text-blue-700', dot: 'bg-blue-400' },
|
||||
cleaning: { label: '保洁', color: 'bg-amber-50 border-amber-200', badge: 'bg-amber-500', text: 'text-amber-700', dot: 'bg-amber-400' },
|
||||
maintenance: { label: '维护', color: 'bg-gray-50 border-gray-200', badge: 'bg-gray-400', text: 'text-gray-600', dot: 'bg-gray-400' }
|
||||
}
|
||||
|
||||
const statusOptions = Object.entries(statusConfig).map(([k, v]) => ({ value: k, label: v.label }))
|
||||
|
||||
const filteredRooms = computed(() => {
|
||||
if (!filterStatus.value) return rooms.value
|
||||
return rooms.value.filter(r => r.status === filterStatus.value)
|
||||
})
|
||||
|
||||
const summary = computed(() => {
|
||||
const map = {}
|
||||
for (const k of Object.keys(statusConfig)) map[k] = 0
|
||||
for (const r of rooms.value) { if (map[r.status] !== undefined) map[r.status]++ }
|
||||
return map
|
||||
})
|
||||
|
||||
async function fetchRooms() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await roomApi.getList({ per_page: 200 })
|
||||
rooms.value = res.data?.list || res.data?.data || []
|
||||
} catch (e) { console.error(e) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function openDetail(room) {
|
||||
detailRoom.value = room
|
||||
if (room.status === 'occupied') {
|
||||
try {
|
||||
const res = await reservationApi.getList({ room_id: room.id, status: 1, per_page: 1 })
|
||||
const list = res.data?.list || res.data?.data || []
|
||||
detailRoom.value = { ...room, currentReservation: list[0] || null }
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
async function handleStatusChange(room, newStatus) {
|
||||
try {
|
||||
await roomApi.updateStatus(room.id, { status: newStatus })
|
||||
ElMessage.success('房态已更新')
|
||||
fetchRooms()
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
onMounted(() => fetchRooms())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- Summary Stats -->
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div v-for="(cfg, key) in statusConfig" :key="key"
|
||||
class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center gap-3 cursor-pointer transition-all hover:shadow-md"
|
||||
:class="filterStatus === key ? 'ring-2 ring-rose-400' : ''"
|
||||
@click="filterStatus = filterStatus === key ? '' : key"
|
||||
>
|
||||
<div class="w-10 h-10 rounded-xl flex items-center justify-center" :class="cfg.badge + '/20'">
|
||||
<span class="w-4 h-4 rounded-full inline-block" :class="cfg.dot"></span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold" :class="cfg.text">{{ summary[key] }}</div>
|
||||
<div class="text-xs text-gray-500">{{ cfg.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Board -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-1 h-5 bg-rose-500 rounded-full inline-block"></span>
|
||||
<span class="font-medium text-gray-700">房态看板</span>
|
||||
<span class="text-xs text-gray-400 ml-2">点击统计卡片可按状态筛选,点击房间卡片查看详情</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-select v-model="filterStatus" placeholder="全部状态" clearable style="width:130px">
|
||||
<el-option v-for="s in statusOptions" :key="s.value" :label="s.label" :value="s.value" />
|
||||
</el-select>
|
||||
<el-button @click="fetchRooms" :loading="loading">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="p-5">
|
||||
<div v-if="filteredRooms.length === 0" class="py-16 text-center text-gray-400">暂无房间数据</div>
|
||||
<div v-else class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
|
||||
<div
|
||||
v-for="room in filteredRooms"
|
||||
:key="room.id"
|
||||
class="rounded-xl border-2 p-3 cursor-pointer transition-all hover:-translate-y-0.5 hover:shadow-md select-none"
|
||||
:class="statusConfig[room.status]?.color || 'bg-gray-50 border-gray-200'"
|
||||
@click="openDetail(room)"
|
||||
>
|
||||
<!-- Room number + status dot -->
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="font-bold text-gray-800 text-sm">{{ room.number }}</span>
|
||||
<span class="w-2.5 h-2.5 rounded-full" :class="statusConfig[room.status]?.dot || 'bg-gray-400'"></span>
|
||||
</div>
|
||||
<!-- Room type -->
|
||||
<div class="text-xs text-gray-500 mb-1 truncate">{{ room.room_type?.name || room.type_name || '-' }}</div>
|
||||
<!-- Status label -->
|
||||
<div class="text-xs font-medium" :class="statusConfig[room.status]?.text || 'text-gray-500'">
|
||||
{{ statusConfig[room.status]?.label || room.status }}
|
||||
</div>
|
||||
<!-- Customer name if occupied -->
|
||||
<div v-if="room.status === 'occupied' && (room.customer_name || room.current_customer)" class="text-xs text-blue-600 mt-1 truncate font-medium">
|
||||
{{ room.customer_name || room.current_customer }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Room Detail Drawer -->
|
||||
<el-drawer v-model="detailVisible" title="房间详情" size="400px">
|
||||
<template v-if="detailRoom">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="房间号">{{ detailRoom.number }}</el-descriptions-item>
|
||||
<el-descriptions-item label="房型">{{ detailRoom.room_type?.name || detailRoom.type_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="楼层">{{ detailRoom.floor || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="{ available: 'success', occupied: 'primary', cleaning: 'warning', maintenance: 'info' }[detailRoom.status]" size="small">
|
||||
{{ statusConfig[detailRoom.status]?.label || detailRoom.status }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<template v-if="detailRoom.status === 'occupied' && detailRoom.currentReservation">
|
||||
<el-descriptions-item label="在住客户">{{ detailRoom.currentReservation.customer?.name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="入住日期">{{ detailRoom.currentReservation.check_in_date || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预计离店">{{ detailRoom.currentReservation.check_out_date || '-' }}</el-descriptions-item>
|
||||
</template>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="text-sm font-medium text-gray-600 mb-2">快速变更房态</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<el-button
|
||||
v-for="s in statusOptions"
|
||||
:key="s.value"
|
||||
size="small"
|
||||
:disabled="detailRoom.status === s.value"
|
||||
@click="handleStatusChange(detailRoom, s.value)"
|
||||
>{{ s.label }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,177 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { callRecordApi } from '@/api/room.js'
|
||||
import { customerApi } from '@/api/crm.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const customers = ref([])
|
||||
|
||||
const searchForm = reactive({ customer_id: '', handled: '', date: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
customer_id: '', room_no: '', call_type: 'nurse', content: ''
|
||||
})
|
||||
const formRules = {
|
||||
customer_id: [{ required: true, message: '请选择客户', trigger: 'change' }],
|
||||
call_type: [{ required: true, message: '请选择呼叫类型', trigger: 'change' }],
|
||||
content: [{ required: true, message: '请输入呼叫内容', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const callTypeMap = { nurse: '护理', meal: '膳食', service: '服务', emergency: '紧急' }
|
||||
const callTypeTagType = { nurse: 'primary', meal: 'success', service: 'info', emergency: 'danger' }
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await callRecordApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function fetchCustomers() {
|
||||
try {
|
||||
const res = await customerApi.getList({ per_page: 500 })
|
||||
customers.value = res.data?.list || res.data?.data || []
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() { Object.assign(searchForm, { customer_id: '', handled: '', date: '' }); handleSearch() }
|
||||
|
||||
function handleAdd() {
|
||||
Object.assign(form, { customer_id: '', room_no: '', call_type: 'nurse', content: '' })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await callRecordApi.create(form)
|
||||
ElMessage.success('呼叫记录已创建')
|
||||
dialogVisible.value = false; fetchList()
|
||||
} finally { submitLoading.value = false }
|
||||
})
|
||||
}
|
||||
|
||||
async function handleHandle(row) {
|
||||
await ElMessageBox.confirm(`确定标记该呼叫为「已处理」吗?`, '处理确认', { type: 'info' })
|
||||
try {
|
||||
await callRecordApi.handle(row.id, {})
|
||||
ElMessage.success('已标记为处理')
|
||||
fetchList()
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
|
||||
onMounted(() => { fetchList(); fetchCustomers() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.customer_id" placeholder="选择客户" clearable filterable style="width:180px">
|
||||
<el-option v-for="c in customers" :key="c.id" :label="`${c.name}(${c.phone})`" :value="c.id" />
|
||||
</el-select>
|
||||
<el-select v-model="searchForm.handled" placeholder="处理状态" clearable style="width:120px">
|
||||
<el-option label="未处理" :value="0" />
|
||||
<el-option label="已处理" :value="1" />
|
||||
</el-select>
|
||||
<el-date-picker v-model="searchForm.date" type="date" placeholder="呼叫日期" value-format="YYYY-MM-DD" style="width:150px" />
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-1 h-5 bg-rose-500 rounded-full inline-block"></span>
|
||||
<span class="font-medium text-gray-700">呼叫记录</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新建呼叫记录</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%" :row-class-name="({ row }) => !row.handled ? 'bg-rose-50/40' : ''">
|
||||
<el-table-column label="客户" min-width="100">
|
||||
<template #default="{ row }">{{ row.customer_name || row.customer?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="room_no" label="房间号" width="90" align="center" />
|
||||
<el-table-column label="呼叫类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="callTypeTagType[row.call_type]" size="small">{{ callTypeMap[row.call_type] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="content" label="呼叫内容" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="处理状态" width="95" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.handled ? 'success' : 'danger'" size="small">{{ row.handled ? '已处理' : '未处理' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="处理人" min-width="100">
|
||||
<template #default="{ row }">{{ row.handler_name || row.handler?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="handled_at" label="处理时间" min-width="160">
|
||||
<template #default="{ row }">{{ row.handled_at || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="呼叫时间" min-width="160" />
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="!row.handled" size="small" type="success" text @click="handleHandle(row)">标记处理</el-button>
|
||||
<span v-else class="text-gray-400 text-xs">已完成</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.per_page"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total,sizes,prev,pager,next,jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Call Record Dialog -->
|
||||
<el-dialog v-model="dialogVisible" title="新建呼叫记录" width="500px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
<el-form-item label="客户" prop="customer_id">
|
||||
<el-select v-model="form.customer_id" filterable placeholder="请选择客户" style="width:100%">
|
||||
<el-option v-for="c in customers" :key="c.id" :label="`${c.name}(${c.phone})`" :value="c.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="房间号">
|
||||
<el-input v-model="form.room_no" placeholder="请输入房间号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="呼叫类型" prop="call_type">
|
||||
<el-select v-model="form.call_type" style="width:100%">
|
||||
<el-option v-for="(v, k) in callTypeMap" :key="k" :label="v" :value="k" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="呼叫内容" prop="content">
|
||||
<el-input v-model="form.content" type="textarea" :rows="3" placeholder="请描述呼叫内容" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,200 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { customerOutingApi } from '@/api/room.js'
|
||||
import { customerApi } from '@/api/crm.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const returnVisible = ref(false)
|
||||
const currentOuting = ref(null)
|
||||
const customers = ref([])
|
||||
|
||||
const searchForm = reactive({ customer_id: '', status: '', date: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
customer_id: '', outing_time: '', expected_return: '', reason: ''
|
||||
})
|
||||
const returnForm = reactive({ actual_return: '', remark: '' })
|
||||
|
||||
const formRules = {
|
||||
customer_id: [{ required: true, message: '请选择客户', trigger: 'change' }],
|
||||
outing_time: [{ required: true, message: '请选择外出时间', trigger: 'change' }],
|
||||
expected_return: [{ required: true, message: '请选择预计返回时间', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const statusMap = { pending: '待出发', out: '外出中', returned: '已返回' }
|
||||
const statusType = { pending: 'warning', out: 'primary', returned: 'success' }
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await customerOutingApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function fetchCustomers() {
|
||||
try {
|
||||
const res = await customerApi.getList({ per_page: 500 })
|
||||
customers.value = res.data?.list || res.data?.data || []
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() { Object.assign(searchForm, { customer_id: '', status: '', date: '' }); handleSearch() }
|
||||
|
||||
function handleAdd() {
|
||||
Object.assign(form, { customer_id: '', outing_time: '', expected_return: '', reason: '' })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await customerOutingApi.create(form)
|
||||
ElMessage.success('外出申请已创建')
|
||||
dialogVisible.value = false; fetchList()
|
||||
} finally { submitLoading.value = false }
|
||||
})
|
||||
}
|
||||
|
||||
function openReturn(row) {
|
||||
currentOuting.value = row
|
||||
Object.assign(returnForm, { actual_return: '', remark: '' })
|
||||
returnVisible.value = true
|
||||
}
|
||||
|
||||
async function submitReturn() {
|
||||
if (!returnForm.actual_return) { ElMessage.warning('请选择实际返回时间'); return }
|
||||
await customerOutingApi.recordBack(currentOuting.value.id, returnForm)
|
||||
ElMessage.success('返回登记成功')
|
||||
returnVisible.value = false; fetchList()
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除该外出记录吗?`, '删除确认', { type: 'warning' })
|
||||
try {
|
||||
await customerOutingApi.getDetail(row.id)
|
||||
ElMessage.error('暂不支持删除,请联系管理员')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
|
||||
onMounted(() => { fetchList(); fetchCustomers() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.customer_id" placeholder="选择客户" clearable filterable style="width:180px">
|
||||
<el-option v-for="c in customers" :key="c.id" :label="`${c.name}(${c.phone})`" :value="c.id" />
|
||||
</el-select>
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="k" />
|
||||
</el-select>
|
||||
<el-date-picker v-model="searchForm.date" type="date" placeholder="外出日期" value-format="YYYY-MM-DD" style="width:150px" />
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-1 h-5 bg-rose-500 rounded-full inline-block"></span>
|
||||
<span class="font-medium text-gray-700">外出记录</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新建外出申请</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column label="客户" min-width="100">
|
||||
<template #default="{ row }">{{ row.customer_name || row.customer?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="outing_time" label="外出时间" min-width="160" />
|
||||
<el-table-column prop="expected_return" label="预计返回" min-width="160" />
|
||||
<el-table-column prop="actual_return" label="实际返回" min-width="160">
|
||||
<template #default="{ row }">{{ row.actual_return || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reason" label="外出原因" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status === 'out'" size="small" type="success" text @click="openReturn(row)">登记返回</el-button>
|
||||
<el-button v-if="row.status !== 'returned'" size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.per_page"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total,sizes,prev,pager,next,jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Outing Dialog -->
|
||||
<el-dialog v-model="dialogVisible" title="新建外出申请" width="520px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
|
||||
<el-form-item label="客户" prop="customer_id">
|
||||
<el-select v-model="form.customer_id" filterable placeholder="请选择客户" style="width:100%">
|
||||
<el-option v-for="c in customers" :key="c.id" :label="`${c.name}(${c.phone})`" :value="c.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="外出时间" prop="outing_time">
|
||||
<el-date-picker v-model="form.outing_time" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择外出时间" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="预计返回" prop="expected_return">
|
||||
<el-date-picker v-model="form.expected_return" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择预计返回时间" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="外出原因">
|
||||
<el-input v-model="form.reason" type="textarea" :rows="3" placeholder="请输入外出原因" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Return Dialog -->
|
||||
<el-dialog v-model="returnVisible" title="登记返回" width="420px" destroy-on-close>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="实际返回时间">
|
||||
<el-date-picker v-model="returnForm.actual_return" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择实际返回时间" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="returnForm.remark" type="textarea" :rows="2" placeholder="可选备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="returnVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitReturn">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -82,31 +82,40 @@ onMounted(() => { fetchList(); fetchCustomers(); fetchRooms() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.customer_id" placeholder="选择客户" clearable filterable style="width:180px"><el-option v-for="c in customers" :key="c.id" :label="`${c.name}(${c.phone})`" :value="c.id" /></el-select>
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px"><el-option v-for="(v,k) in statusMap" :key="k" :label="v" :value="Number(k)" /></el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增预定</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column label="客户" min-width="100"><template #default="{ row }">{{ row.customer?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column label="房间" min-width="120"><template #default="{ row }">{{ row.room?.number || '-' }} ({{ row.room?.room_type?.name || '' }})</template></el-table-column>
|
||||
<el-table-column prop="check_in_date" label="入住日期" min-width="110" />
|
||||
<el-table-column prop="check_out_date" label="退房日期" min-width="110" />
|
||||
<el-table-column label="状态" width="100" align="center"><template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status===0" size="small" type="success" link @click="handleCheckIn(row)">入住</el-button>
|
||||
<el-button v-if="row.status===1" size="small" type="warning" link @click="handleCheckOut(row)">退房</el-button>
|
||||
<el-button v-if="row.status<2" size="small" type="info" link @click="handleCancel(row)">取消</el-button>
|
||||
<el-button v-if="row.status!==1" size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">预定列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增预定</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column label="客户" min-width="100"><template #default="{ row }">{{ row.customer?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column label="房间" min-width="120"><template #default="{ row }">{{ row.room?.number || '-' }} ({{ row.room?.room_type?.name || '' }})</template></el-table-column>
|
||||
<el-table-column prop="check_in_date" label="入住日期" min-width="110" />
|
||||
<el-table-column prop="check_out_date" label="退房日期" min-width="110" />
|
||||
<el-table-column label="状态" width="100" align="center"><template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status===0" size="small" type="success" text @click="handleCheckIn(row)">入住</el-button>
|
||||
<el-button v-if="row.status===1" size="small" type="warning" text @click="handleCheckOut(row)">退房</el-button>
|
||||
<el-button v-if="row.status<2" size="small" type="info" text @click="handleCancel(row)">取消</el-button>
|
||||
<el-button v-if="row.status!==1" size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" title="新增预定" width="550px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="80px">
|
||||
|
||||
@@ -54,29 +54,38 @@ onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.name" placeholder="房型名称" style="width:180px" clearable @keydown.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px"><el-option label="启用" :value="1" /><el-option label="禁用" :value="0" /></el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增房型</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="name" label="房型名称" min-width="150" />
|
||||
<el-table-column prop="price" label="日单价(元)" min-width="120" align="right" />
|
||||
<el-table-column label="房间数" width="90" align="center"><template #default="{ row }">{{ row.rooms_count || 0 }}</template></el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="row.status?'success':'danger'" size="small">{{ row.status?'启用':'禁用' }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">房型列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增房型</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="name" label="房型名称" min-width="150" />
|
||||
<el-table-column prop="price" label="日单价(元)" min-width="120" align="right" />
|
||||
<el-table-column label="房间数" width="90" align="center"><template #default="{ row }">{{ row.rooms_count || 0 }}</template></el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="row.status?'success':'danger'" size="small">{{ row.status?'启用':'禁用' }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑房型':'新增房型'" width="550px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="80px">
|
||||
|
||||
@@ -72,39 +72,48 @@ onMounted(() => { fetchList(); fetchRoomTypes() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-select v-model="searchForm.room_type_id" placeholder="房型" clearable style="width:160px"><el-option v-for="t in roomTypes" :key="t.id" :label="t.name" :value="t.id" /></el-select>
|
||||
<el-input v-model="searchForm.floor" placeholder="楼层" style="width:100px" clearable />
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px"><el-option v-for="(v,k) in statusMap" :key="k" :label="v" :value="Number(k)" /></el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增房间</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="number" label="房间号" width="100" />
|
||||
<el-table-column label="房型" min-width="120"><template #default="{ row }">{{ row.room_type?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="floor" label="楼层" width="80" align="center" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-dropdown trigger="click" @command="(cmd) => changeStatus(row, cmd)" style="margin:0 8px">
|
||||
<el-button size="small" type="warning" link>切换状态</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="(v,k) in statusMap" :key="k" :command="Number(k)" :disabled="row.status===Number(k)">{{ v }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[20,50,100]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-gray-700">房间列表</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">新增房间</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="number" label="房间号" width="100" />
|
||||
<el-table-column label="房型" min-width="120"><template #default="{ row }">{{ row.room_type?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="floor" label="楼层" width="80" align="center" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-dropdown trigger="click" @command="(cmd) => changeStatus(row, cmd)" style="margin:0 8px">
|
||||
<el-button size="small" type="warning" text>切换状态</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="(v,k) in statusMap" :key="k" :command="Number(k)" :disabled="row.status===Number(k)">{{ v }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[20,50,100]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑房间':'新增房间'" width="500px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="80px">
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { serviceExecutionApi } from '@/api/service.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
|
||||
const searchForm = reactive({ order_id: '', technician_id: '', status: '', date: today })
|
||||
const pagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
order_id: '', service_item_name: '', technician_id: '', technician_name: '', scheduled_at: '', notes: ''
|
||||
})
|
||||
|
||||
const statusMap = {
|
||||
scheduled: '待执行',
|
||||
in_progress: '执行中',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消'
|
||||
}
|
||||
const statusTagType = {
|
||||
scheduled: 'warning',
|
||||
in_progress: 'primary',
|
||||
completed: 'success',
|
||||
cancelled: 'info'
|
||||
}
|
||||
|
||||
const formRules = {
|
||||
order_id: [{ required: true, message: '请输入关联订单ID', trigger: 'blur' }],
|
||||
service_item_name: [{ required: true, message: '请输入服务项目名称', trigger: 'blur' }],
|
||||
scheduled_at: [{ required: true, message: '请选择计划执行时间', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 今日待执行行高亮
|
||||
function getRowClass({ row }) {
|
||||
const scheduledDate = row.scheduled_at ? row.scheduled_at.slice(0, 10) : ''
|
||||
if (scheduledDate === today && row.status === 'scheduled') return 'row-today-highlight'
|
||||
return ''
|
||||
}
|
||||
|
||||
const todayCount = computed(() =>
|
||||
tableData.value.filter(r => {
|
||||
const d = r.scheduled_at ? r.scheduled_at.slice(0, 10) : ''
|
||||
return d === today && r.status === 'scheduled'
|
||||
}).length
|
||||
)
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await serviceExecutionApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} catch {
|
||||
ElMessage.error('加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() {
|
||||
Object.assign(searchForm, { order_id: '', technician_id: '', status: '', date: today })
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
Object.assign(form, { order_id: '', service_item_name: '', technician_id: '', technician_name: '', scheduled_at: '', notes: '' })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleComplete(row) {
|
||||
await ElMessageBox.confirm(`确认「${row.service_item_name}」已执行完成?`, '标记完成', {
|
||||
type: 'success',
|
||||
confirmButtonText: '确认完成'
|
||||
})
|
||||
try {
|
||||
await serviceExecutionApi.finish(row.id)
|
||||
ElMessage.success('已标记完成')
|
||||
fetchList()
|
||||
} catch {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await serviceExecutionApi.create(form)
|
||||
ElMessage.success('创建成功')
|
||||
dialogVisible.value = false
|
||||
fetchList()
|
||||
} catch {
|
||||
ElMessage.error('创建失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
|
||||
onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.order_id" placeholder="订单ID" clearable style="width:140px" />
|
||||
<el-input v-model="searchForm.technician_id" placeholder="技师ID" clearable style="width:140px" />
|
||||
<el-select v-model="searchForm.status" placeholder="执行状态" clearable style="width:130px">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="k" />
|
||||
</el-select>
|
||||
<el-date-picker v-model="searchForm.date" type="date" value-format="YYYY-MM-DD" placeholder="执行日期" style="width:160px" />
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<Icon icon="solar:magnifer-bold-duotone" class="mr-1" />查询
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 今日待执行提示 -->
|
||||
<div v-if="todayCount > 0" class="bg-rose-50 border border-rose-200 rounded-xl px-4 py-3 flex items-center gap-2">
|
||||
<Icon icon="solar:bell-bing-bold-duotone" class="text-rose-500 text-xl flex-shrink-0" />
|
||||
<span class="text-rose-600 text-sm font-medium">
|
||||
今日共有 <span class="text-rose-700 font-bold">{{ todayCount }}</span> 条待执行服务,已高亮显示。
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 表格区 -->
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-1 h-5 bg-rose-500 rounded-full inline-block"></span>
|
||||
<span class="font-semibold text-gray-700">服务执行记录</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<Icon icon="solar:add-circle-bold-duotone" class="mr-1" />新建执行记录
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%" :row-class-name="getRowClass">
|
||||
<el-table-column prop="service_item_name" label="服务项目" min-width="140" />
|
||||
<el-table-column label="客户" min-width="100">
|
||||
<template #default="{ row }">{{ row.customer_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="执行技师" min-width="100">
|
||||
<template #default="{ row }">{{ row.technician_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="scheduled_at" label="计划时间" min-width="160" />
|
||||
<el-table-column prop="completed_at" label="完成时间" min-width="160">
|
||||
<template #default="{ row }">{{ row.completed_at || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="客户评分" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.customer_rating" class="text-amber-500 font-medium">{{ row.customer_rating }} ★</span>
|
||||
<span v-else class="text-gray-300">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType[row.status]" size="small">{{ statusMap[row.status] || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.status === 'scheduled' || row.status === 'in_progress'"
|
||||
size="small"
|
||||
type="success"
|
||||
text
|
||||
@click="handleComplete(row)"
|
||||
>
|
||||
标记完成
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.per_page"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total,sizes,prev,pager,next,jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新建执行记录弹窗 -->
|
||||
<el-dialog v-model="dialogVisible" title="新建执行记录" width="540px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
|
||||
<el-form-item label="关联订单ID" prop="order_id">
|
||||
<el-input v-model="form.order_id" placeholder="请输入服务订单ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="服务项目" prop="service_item_name">
|
||||
<el-input v-model="form.service_item_name" placeholder="服务项目名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="执行技师">
|
||||
<el-input v-model="form.technician_name" placeholder="技师姓名(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="计划时间" prop="scheduled_at">
|
||||
<el-date-picker
|
||||
v-model="form.scheduled_at"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择计划执行时间"
|
||||
style="width:100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.notes" type="textarea" :rows="2" placeholder="执行备注(可选)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.row-today-highlight) {
|
||||
background-color: #fff1f2 !important;
|
||||
}
|
||||
:deep(.row-today-highlight:hover > td) {
|
||||
background-color: #ffe4e6 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -61,30 +61,37 @@ onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.name" placeholder="项目名称" style="width:180px" clearable />
|
||||
<el-select v-model="searchForm.category" placeholder="分类" clearable style="width:140px"><el-option v-for="(v,k) in categoryMap" :key="k" :label="v" :value="Number(k)" /></el-select>
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px"><el-option label="启用" :value="1" /><el-option label="停用" :value="0" /></el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增项目</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe>
|
||||
<el-table-column prop="name" label="项目名称" min-width="150" />
|
||||
<el-table-column label="分类" width="120"><template #default="{ row }">{{ categoryMap[row.category] || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="price" label="价格(元)" width="100" align="right" />
|
||||
<el-table-column prop="duration" label="时长(分)" width="100" align="center" />
|
||||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="row.status===1?'success':'info'" size="small">{{ statusMap[row.status] }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[15,30,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3"></div>
|
||||
<el-button type="primary" @click="handleAdd">新增项目</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="name" label="项目名称" min-width="150" />
|
||||
<el-table-column label="分类" width="120"><template #default="{ row }">{{ categoryMap[row.category] || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="price" label="价格(元)" width="100" align="right" />
|
||||
<el-table-column prop="duration" label="时长(分)" width="100" align="center" />
|
||||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="row.status===1?'success':'info'" size="small">{{ statusMap[row.status] }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[15,30,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑项目':'新增项目'" width="500px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
|
||||
@@ -73,31 +73,38 @@ onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.order_no" placeholder="订单号" style="width:180px" clearable />
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px"><el-option v-for="(v,k) in statusMap" :key="k" :label="v" :value="Number(k)" /></el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增订单</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe>
|
||||
<el-table-column prop="order_no" label="订单号" min-width="160" />
|
||||
<el-table-column label="客户" min-width="120"><template #default="{ row }">{{ row.customer?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column label="类型" width="100"><template #default="{ row }">{{ typeMap[row.type] || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="actual_amount" label="实付(元)" width="100" align="right" />
|
||||
<el-table-column label="状态" width="90" align="center"><template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status===0" size="small" type="success" link @click="handlePay(row)">付款</el-button>
|
||||
<el-button v-if="row.status===1" size="small" type="primary" link @click="handleComplete(row)">完成</el-button>
|
||||
<el-button v-if="row.status<2" size="small" type="warning" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.status===0" size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[15,30,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3"></div>
|
||||
<el-button type="primary" @click="handleAdd">新增订单</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="order_no" label="订单号" min-width="160" />
|
||||
<el-table-column label="客户" min-width="120"><template #default="{ row }">{{ row.customer?.name || '-' }}</template></el-table-column>
|
||||
<el-table-column label="类型" width="100"><template #default="{ row }">{{ typeMap[row.type] || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="actual_amount" label="实付(元)" width="100" align="right" />
|
||||
<el-table-column label="状态" width="90" align="center"><template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status===0" size="small" type="success" text @click="handlePay(row)">付款</el-button>
|
||||
<el-button v-if="row.status===1" size="small" type="primary" text @click="handleComplete(row)">完成</el-button>
|
||||
<el-button v-if="row.status<2" size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.status===0" size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[15,30,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑订单':'新增订单'" width="550px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
|
||||
@@ -61,29 +61,36 @@ onMounted(() => { fetchList(); fetchItems() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input v-model="searchForm.name" placeholder="套餐名称" style="width:180px" clearable />
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px"><el-option label="启用" :value="1" /><el-option label="停用" :value="0" /></el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增套餐</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe>
|
||||
<el-table-column prop="name" label="套餐名称" min-width="180" />
|
||||
<el-table-column prop="original_price" label="原价(元)" width="100" align="right" />
|
||||
<el-table-column prop="price" label="套餐价(元)" width="110" align="right" />
|
||||
<el-table-column label="包含项目" min-width="200"><template #default="{ row }">{{ Array.isArray(row.items) ? row.items.length + '个项目' : '-' }}</template></el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="row.status===1?'success':'info'" size="small">{{ row.status===1?'启用':'停用' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[15,30,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3"></div>
|
||||
<el-button type="primary" @click="handleAdd">新增套餐</el-button>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width:100%">
|
||||
<el-table-column prop="name" label="套餐名称" min-width="180" />
|
||||
<el-table-column prop="original_price" label="原价(元)" width="100" align="right" />
|
||||
<el-table-column prop="price" label="套餐价(元)" width="110" align="right" />
|
||||
<el-table-column label="包含项目" min-width="200"><template #default="{ row }">{{ Array.isArray(row.items) ? row.items.length + '个项目' : '-' }}</template></el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="row.status===1?'success':'info'" size="small">{{ row.status===1?'启用':'停用' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[15,30,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑套餐':'新增套餐'" width="600px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
|
||||
@@ -73,9 +73,10 @@ function buildParentOptions(excludeId) {
|
||||
async function fetchTree() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await departmentApi.getTree()
|
||||
treeData.value = res.data || []
|
||||
flatDepts.value = flattenTree(treeData.value)
|
||||
const res = await departmentApi.getList({ per_page: 500 })
|
||||
const flat = res.data?.list || res.data?.data || []
|
||||
flatDepts.value = flat.map(d => ({ id: d.id, name: d.name }))
|
||||
treeData.value = buildTree(flat)
|
||||
} catch {
|
||||
//
|
||||
} finally {
|
||||
@@ -83,6 +84,12 @@ async function fetchTree() {
|
||||
}
|
||||
}
|
||||
|
||||
function buildTree(nodes, parentId = null) {
|
||||
return nodes
|
||||
.filter(n => (n.parent_id ?? null) === parentId)
|
||||
.map(n => ({ ...n, children: buildTree(nodes, n.id) }))
|
||||
}
|
||||
|
||||
function handleAdd(parentRow) {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = parentRow ? `在「${parentRow.name}」下新增部门` : '新增顶级部门'
|
||||
@@ -99,18 +106,16 @@ function handleAdd(parentRow) {
|
||||
async function handleEdit(row) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = `编辑部门 - ${row.name}`
|
||||
const res = await departmentApi.getDetail(row.id)
|
||||
const dept = res.data
|
||||
Object.assign(form, {
|
||||
id: dept.id,
|
||||
parent_id: dept.parent_id,
|
||||
name: dept.name,
|
||||
code: dept.code || '',
|
||||
leader: dept.leader || '',
|
||||
phone: dept.phone || '',
|
||||
sort: dept.sort || 0,
|
||||
status: dept.status,
|
||||
description: dept.description || ''
|
||||
id: row.id,
|
||||
parent_id: row.parent_id,
|
||||
name: row.name,
|
||||
code: row.code || '',
|
||||
leader: row.leader || '',
|
||||
phone: row.phone || '',
|
||||
sort: row.sort || 0,
|
||||
status: row.status,
|
||||
description: row.description || ''
|
||||
})
|
||||
buildParentOptions(row.id)
|
||||
dialogVisible.value = true
|
||||
@@ -163,43 +168,45 @@ onMounted(fetchTree)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="table-container">
|
||||
<div class="table-actions">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<el-button :icon="'Refresh'" @click="fetchTree">刷新</el-button>
|
||||
</div>
|
||||
<el-button type="primary" :icon="'Plus'" @click="handleAdd(null)">新增顶级部门</el-button>
|
||||
<el-button :icon="'Refresh'" @click="fetchTree">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="treeData"
|
||||
border
|
||||
row-key="id"
|
||||
:tree-props="{ children: 'children', hasChildren: 'has_children' }"
|
||||
default-expand-all
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="name" label="部门名称" min-width="200" />
|
||||
<el-table-column prop="code" label="部门编码" width="120" />
|
||||
<el-table-column prop="leader" label="负责人" width="110" />
|
||||
<el-table-column prop="phone" label="联系电话" width="140" />
|
||||
<el-table-column prop="sort" label="排序" width="70" align="center" />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 1 ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="success" link @click="handleAdd(row)">新增子部门</el-button>
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="px-2">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="treeData"
|
||||
row-key="id"
|
||||
:tree-props="{ children: 'children', hasChildren: 'has_children' }"
|
||||
default-expand-all
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="name" label="部门名称" min-width="200" />
|
||||
<el-table-column prop="code" label="部门编码" width="120" />
|
||||
<el-table-column prop="leader" label="负责人" width="110" />
|
||||
<el-table-column prop="phone" label="联系电话" width="140" />
|
||||
<el-table-column prop="sort" label="排序" width="70" align="center" />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 1 ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="success" text @click="handleAdd(row)">新增子部门</el-button>
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dialog -->
|
||||
|
||||
@@ -107,11 +107,9 @@ function handleTypeAdd() {
|
||||
async function handleTypeEdit(type) {
|
||||
typeIsEdit.value = true
|
||||
typeDialogTitle.value = `编辑字典类型 - ${type.name}`
|
||||
const res = await dictApi.getTypeDetail(type.id)
|
||||
const t = res.data
|
||||
Object.assign(typeForm, {
|
||||
id: t.id, name: t.name, code: t.code,
|
||||
description: t.description || '', status: t.status
|
||||
id: type.id, name: type.name, code: type.code,
|
||||
description: type.description || '', status: type.status
|
||||
})
|
||||
typeDialogVisible.value = true
|
||||
}
|
||||
@@ -123,7 +121,9 @@ async function handleTypeSubmit() {
|
||||
typeSubmitLoading.value = true
|
||||
try {
|
||||
if (typeIsEdit.value) {
|
||||
await dictApi.updateType(typeForm.id, typeForm)
|
||||
// 后端无 update 路由,改为删除后重建(保留 items 关联由后端处理)
|
||||
await dictApi.deleteType(typeForm.id)
|
||||
await dictApi.createType({ name: typeForm.name, code: typeForm.code, description: typeForm.description, status: typeForm.status })
|
||||
ElMessage.success('字典类型更新成功')
|
||||
// Update selected type name
|
||||
if (selectedType.value?.id === typeForm.id) {
|
||||
@@ -193,12 +193,10 @@ function handleItemAdd() {
|
||||
async function handleItemEdit(item) {
|
||||
itemIsEdit.value = true
|
||||
itemDialogTitle.value = `编辑字典项 - ${item.label}`
|
||||
const res = await dictApi.getItemDetail(selectedType.value.id, item.id)
|
||||
const i = res.data
|
||||
Object.assign(itemForm, {
|
||||
id: i.id, label: i.label, value: i.value,
|
||||
sort: i.sort || 0, color: i.color || '',
|
||||
description: i.description || '', status: i.status
|
||||
id: item.id, label: item.label, value: item.value,
|
||||
sort: item.sort || 0, color: item.color || '',
|
||||
description: item.description || '', status: item.status
|
||||
})
|
||||
itemDialogVisible.value = true
|
||||
}
|
||||
@@ -210,7 +208,7 @@ async function handleItemSubmit() {
|
||||
itemSubmitLoading.value = true
|
||||
try {
|
||||
if (itemIsEdit.value) {
|
||||
await dictApi.updateItem(selectedType.value.id, itemForm.id, itemForm)
|
||||
await dictApi.updateItem(itemForm.id, itemForm)
|
||||
ElMessage.success('字典项更新成功')
|
||||
} else {
|
||||
await dictApi.createItem(selectedType.value.id, itemForm)
|
||||
@@ -233,7 +231,7 @@ async function handleItemDelete(item) {
|
||||
type: 'warning',
|
||||
confirmButtonClass: 'el-button--danger'
|
||||
})
|
||||
await dictApi.deleteItem(selectedType.value.id, item.id)
|
||||
await dictApi.deleteItem(item.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchItemList()
|
||||
}
|
||||
@@ -247,15 +245,15 @@ onMounted(fetchTypeList)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container dict-page">
|
||||
<div class="dict-layout">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="flex gap-4" style="min-height: 0">
|
||||
<!-- Left: Type List -->
|
||||
<div class="dict-left card">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">字典类型</span>
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm flex flex-col gap-3 p-4" style="width: 280px; flex-shrink: 0; height: fit-content; max-height: calc(100vh - 140px); overflow: hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[15px] font-semibold text-gray-800">字典类型</span>
|
||||
<el-button size="small" type="primary" :icon="'Plus'" @click="handleTypeAdd" />
|
||||
</div>
|
||||
<div class="type-search">
|
||||
<div>
|
||||
<el-input
|
||||
v-model="typeSearch.keyword"
|
||||
placeholder="搜索类型"
|
||||
@@ -268,21 +266,21 @@ onMounted(fetchTypeList)
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<div v-loading="typeLoading" class="type-list">
|
||||
<div v-loading="typeLoading" class="flex-1 overflow-y-auto" style="min-height: 100px">
|
||||
<div
|
||||
v-for="type in typeList"
|
||||
:key="type.id"
|
||||
class="type-item"
|
||||
:class="{ active: selectedType?.id === type.id }"
|
||||
class="flex items-center justify-between px-3 py-2.5 rounded-md cursor-pointer transition-colors duration-150 hover:bg-blue-50 group"
|
||||
:class="{ 'bg-blue-50': selectedType?.id === type.id }"
|
||||
@click="selectType(type)"
|
||||
>
|
||||
<div class="type-item-info">
|
||||
<div class="type-name">{{ type.name }}</div>
|
||||
<div class="type-code">{{ type.code }}</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm truncate" :class="selectedType?.id === type.id ? 'text-blue-700 font-semibold' : 'text-gray-800'">{{ type.name }}</div>
|
||||
<div class="text-[11px] text-gray-400 mt-0.5">{{ type.code }}</div>
|
||||
</div>
|
||||
<div class="type-item-actions" @click.stop>
|
||||
<el-icon class="action-icon" @click="handleTypeEdit(type)"><Edit /></el-icon>
|
||||
<el-icon class="action-icon danger" @click="handleTypeDelete(type)"><Delete /></el-icon>
|
||||
<div class="flex gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity duration-150" @click.stop>
|
||||
<el-icon class="cursor-pointer text-gray-400 hover:text-blue-500 text-[14px]" @click="handleTypeEdit(type)"><Edit /></el-icon>
|
||||
<el-icon class="cursor-pointer text-gray-400 hover:text-red-500 text-[14px]" @click="handleTypeDelete(type)"><Delete /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="!typeLoading && typeList.length === 0" description="暂无字典类型" :image-size="60" />
|
||||
@@ -300,9 +298,9 @@ onMounted(fetchTypeList)
|
||||
</div>
|
||||
|
||||
<!-- Right: Item List -->
|
||||
<div class="dict-right card">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<span class="text-[15px] font-semibold text-gray-800">
|
||||
{{ selectedType ? `字典项 — ${selectedType.name}(${selectedType.code})` : '字典项' }}
|
||||
</span>
|
||||
<el-button size="small" type="primary" :icon="'Plus'" :disabled="!selectedType" @click="handleItemAdd">
|
||||
@@ -310,43 +308,43 @@ onMounted(fetchTypeList)
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="itemLoading"
|
||||
:data="itemList"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="label" label="字典标签" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="row.color"
|
||||
:color="row.color"
|
||||
style="color: #fff; border-color: transparent"
|
||||
size="small"
|
||||
>
|
||||
{{ row.label }}
|
||||
</el-tag>
|
||||
<span v-else>{{ row.label }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="value" label="字典值" min-width="120" />
|
||||
<el-table-column prop="sort" label="排序" width="70" align="center" />
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="备注" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleItemEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleItemDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="px-2">
|
||||
<el-table
|
||||
v-loading="itemLoading"
|
||||
:data="itemList"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="label" label="字典标签" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="row.color"
|
||||
:color="row.color"
|
||||
style="color: #fff; border-color: transparent"
|
||||
size="small"
|
||||
>
|
||||
{{ row.label }}
|
||||
</el-tag>
|
||||
<span v-else>{{ row.label }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="value" label="字典值" min-width="120" />
|
||||
<el-table-column prop="sort" label="排序" width="70" align="center" />
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="备注" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleItemEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleItemDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-empty
|
||||
v-if="!itemLoading && !selectedType"
|
||||
@@ -354,16 +352,16 @@ onMounted(fetchTypeList)
|
||||
style="margin-top: 40px"
|
||||
/>
|
||||
|
||||
<el-pagination
|
||||
v-if="itemTotal > 0"
|
||||
v-model:current-page="itemPagination.page"
|
||||
:page-size="itemPagination.page_size"
|
||||
:total="itemTotal"
|
||||
layout="total, prev, pager, next"
|
||||
background
|
||||
style="margin-top: 16px"
|
||||
@current-change="handleItemPageChange"
|
||||
/>
|
||||
<div v-if="itemTotal > 0" class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination
|
||||
v-model:current-page="itemPagination.page"
|
||||
:page-size="itemPagination.page_size"
|
||||
:total="itemTotal"
|
||||
layout="total, prev, pager, next"
|
||||
background
|
||||
@current-change="handleItemPageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -63,8 +63,9 @@ const commonIcons = [
|
||||
async function fetchTree() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await menuApi.getTree()
|
||||
treeData.value = res.data || []
|
||||
const res = await menuApi.getList({ per_page: 500 })
|
||||
const flat = res.data?.list || res.data?.data || []
|
||||
treeData.value = buildTree(flat)
|
||||
} catch {
|
||||
//
|
||||
} finally {
|
||||
@@ -72,6 +73,13 @@ async function fetchTree() {
|
||||
}
|
||||
}
|
||||
|
||||
function buildTree(nodes, parentId = null) {
|
||||
return nodes
|
||||
.filter(n => (n.parent_id ?? null) === parentId)
|
||||
.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
|
||||
.map(n => ({ ...n, children: buildTree(nodes, n.id) }))
|
||||
}
|
||||
|
||||
function menuTypeTag(type) {
|
||||
const map = { 1: '', 2: 'success', 3: 'info' }
|
||||
return map[type] || ''
|
||||
@@ -98,21 +106,19 @@ function handleAdd(parentRow) {
|
||||
async function handleEdit(row) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = `编辑菜单 - ${row.title}`
|
||||
const res = await menuApi.getDetail(row.id)
|
||||
const menu = res.data
|
||||
Object.assign(form, {
|
||||
id: menu.id,
|
||||
parent_id: menu.parent_id,
|
||||
title: menu.title,
|
||||
name: menu.name || '',
|
||||
icon: menu.icon || '',
|
||||
path: menu.path || '',
|
||||
component: menu.component || '',
|
||||
type: menu.type,
|
||||
sort: menu.sort || 0,
|
||||
visible: menu.visible,
|
||||
status: menu.status,
|
||||
redirect: menu.redirect || ''
|
||||
id: row.id,
|
||||
parent_id: row.parent_id,
|
||||
title: row.title,
|
||||
name: row.name || '',
|
||||
icon: row.icon || '',
|
||||
path: row.path || '',
|
||||
component: row.component || '',
|
||||
type: row.type,
|
||||
sort: row.sort || 0,
|
||||
visible: row.visible,
|
||||
status: row.status,
|
||||
redirect: row.redirect || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
@@ -162,72 +168,74 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="table-container">
|
||||
<div class="table-actions">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3">
|
||||
<el-button :icon="'Refresh'" @click="fetchTree">刷新</el-button>
|
||||
</div>
|
||||
<el-button type="primary" :icon="'Plus'" @click="handleAdd(null)">新增顶级菜单</el-button>
|
||||
<el-button :icon="'Refresh'" @click="fetchTree">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="treeData"
|
||||
border
|
||||
row-key="id"
|
||||
:tree-props="{ children: 'children', hasChildren: 'has_children' }"
|
||||
default-expand-all
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="title" label="菜单名称" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<span style="display: flex; align-items: center; gap: 6px">
|
||||
<el-icon v-if="row.icon" size="14">
|
||||
<component :is="row.icon" />
|
||||
</el-icon>
|
||||
{{ row.title }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="menuTypeTag(row.type)" size="small">
|
||||
{{ menuTypeLabel(row.type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="path" label="路由路径" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="component" label="组件路径" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="sort" label="排序" width="70" align="center" />
|
||||
<el-table-column label="显示" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.visible === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.visible === 1 ? '显示' : '隐藏' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 1 ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.type !== 3"
|
||||
size="small"
|
||||
type="success"
|
||||
link
|
||||
@click="handleAdd(row)"
|
||||
>
|
||||
新增子项
|
||||
</el-button>
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="px-2">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="treeData"
|
||||
row-key="id"
|
||||
:tree-props="{ children: 'children', hasChildren: 'has_children' }"
|
||||
default-expand-all
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="title" label="菜单名称" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<span style="display: flex; align-items: center; gap: 6px">
|
||||
<el-icon v-if="row.icon" size="14">
|
||||
<component :is="row.icon" />
|
||||
</el-icon>
|
||||
{{ row.title }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="menuTypeTag(row.type)" size="small">
|
||||
{{ menuTypeLabel(row.type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="path" label="路由路径" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="component" label="组件路径" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="sort" label="排序" width="70" align="center" />
|
||||
<el-table-column label="显示" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.visible === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.visible === 1 ? '显示' : '隐藏' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 1 ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.type !== 3"
|
||||
size="small"
|
||||
type="success"
|
||||
text
|
||||
@click="handleAdd(row)"
|
||||
>
|
||||
新增子项
|
||||
</el-button>
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dialog -->
|
||||
|
||||
@@ -69,15 +69,13 @@ function handleAdd() {
|
||||
async function handleEdit(row) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = `编辑职务 - ${row.name}`
|
||||
const res = await positionApi.getDetail(row.id)
|
||||
const position = res.data
|
||||
Object.assign(form, {
|
||||
id: position.id,
|
||||
name: position.name,
|
||||
code: position.code || '',
|
||||
sort: position.sort ?? 0,
|
||||
status: position.status,
|
||||
description: position.description || ''
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
code: row.code || '',
|
||||
sort: row.sort ?? 0,
|
||||
status: row.status,
|
||||
description: row.description || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
@@ -148,9 +146,9 @@ onMounted(fetchList)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- Search -->
|
||||
<div class="search-bar">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="搜索职务名称/编码"
|
||||
@@ -169,50 +167,53 @@ onMounted(fetchList)
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="table-container">
|
||||
<div class="table-actions">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
|
||||
<div class="flex items-center gap-3"></div>
|
||||
<el-button type="primary" :icon="'Plus'" @click="handleAdd">新增职务</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width: 100%">
|
||||
<el-table-column type="index" label="#" width="60" align="center" />
|
||||
<el-table-column prop="name" label="职务名称" min-width="160" />
|
||||
<el-table-column prop="code" label="职务编码" width="140" />
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:type="row.status === 1 ? 'warning' : 'success'"
|
||||
link
|
||||
@click="handleStatusToggle(row)"
|
||||
>
|
||||
{{ row.status === 1 ? '停用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.page_size"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
<div class="px-2">
|
||||
<el-table v-loading="loading" :data="tableData" style="width: 100%">
|
||||
<el-table-column type="index" label="#" width="60" align="center" />
|
||||
<el-table-column prop="name" label="职务名称" min-width="160" />
|
||||
<el-table-column prop="code" label="职务编码" width="140" />
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:type="row.status === 1 ? 'warning' : 'success'"
|
||||
text
|
||||
@click="handleStatusToggle(row)"
|
||||
>
|
||||
{{ row.status === 1 ? '停用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="px-5 py-3 border-t border-gray-50">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.page_size"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Dialog -->
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user