feat: 第三阶段业务模块(服务产康/月嫂/进销存)

- 新增3个migration(15张表): service/nanny/inventory
- 新增15个Model + 13个Controller
- 新增55条API路由(总计212条)
- 新增3个前端API模块 + 11个管理页面
- vite build验证通过
This commit is contained in:
li
2026-03-13 20:29:45 +08:00
parent 16bcd9bcf0
commit e7d4d9b414
46 changed files with 2573 additions and 0 deletions
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Controllers\Admin\Inventory;
use App\Http\Controllers\Controller;
use App\Models\Inventory\Inventory;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class InventoryController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = Inventory::query()->with(['warehouse','material']);
$query->when($request->warehouse_id, fn($q, $v) => $q->where('warehouse_id', $v));
$query->when($request->material_id, fn($q, $v) => $q->where('material_id', $v));
return $this->paginate($query->paginate($request->input('per_page', 50)));
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers\Admin\Inventory;
use App\Http\Controllers\Controller;
use App\Models\Inventory\Material;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class MaterialController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = Material::query();
$query->when($request->name, fn($q, $v) => $q->where('name', 'like', "%{$v}%"));
$query->when($request->category, fn($q, $v) => $q->where('category', $v));
$query->when($request->code, fn($q, $v) => $q->where('code', 'like', "%{$v}%"));
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
}
public function store(Request $request): JsonResponse
{
$v = $request->validate(['name'=>'required|string|max:100','code'=>'nullable|string|max:50','category'=>'nullable|string|max:50','unit'=>'nullable|string|max:20','spec'=>'nullable|string|max:100','min_stock'=>'sometimes|integer|min:0','shelf_life_days'=>'nullable|integer|min:1','status'=>'sometimes|integer|in:0,1']);
return $this->success(Material::create($v));
}
public function show(Material $material): JsonResponse { return $this->success($material); }
public function update(Request $request, Material $material): JsonResponse { $v = $request->validate(['name'=>'sometimes|string|max:100','code'=>'nullable|string|max:50','category'=>'nullable|string|max:50','unit'=>'nullable|string|max:20','spec'=>'nullable|string|max:100','min_stock'=>'sometimes|integer|min:0','shelf_life_days'=>'nullable|integer|min:1','status'=>'sometimes|integer|in:0,1']); $material->update($v); return $this->success($material); }
public function destroy(Material $material): JsonResponse { $material->delete(); return $this->success(null); }
}
@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Admin\Inventory;
use App\Http\Controllers\Controller;
use App\Models\Inventory\PurchaseOrder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class PurchaseOrderController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = PurchaseOrder::query()->with(['supplier','auditor']);
$query->when($request->order_no, fn($q, $v) => $q->where('order_no', 'like', "%{$v}%"));
$query->when($request->supplier_id, fn($q, $v) => $q->where('supplier_id', $v));
$query->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(['order_no'=>'required|string|max:50|unique:purchase_orders','supplier_id'=>'nullable|exists:suppliers,id','total_amount'=>'nullable|numeric|min:0','items'=>'nullable|array','remark'=>'nullable|string']);
$v['status'] = 0; $v['created_by'] = auth()->id();
return $this->success(PurchaseOrder::create($v));
}
public function show(PurchaseOrder $purchaseOrder): JsonResponse { return $this->success($purchaseOrder->load(['supplier','auditor','creator'])); }
public function update(Request $request, PurchaseOrder $purchaseOrder): JsonResponse
{
if ($purchaseOrder->status >= 2) return $this->error('已审批的采购单不可修改', 40001);
$v = $request->validate(['supplier_id'=>'nullable|exists:suppliers,id','total_amount'=>'nullable|numeric|min:0','items'=>'nullable|array','remark'=>'nullable|string']);
$purchaseOrder->update($v);
return $this->success($purchaseOrder);
}
public function audit(Request $request, PurchaseOrder $purchaseOrder): JsonResponse
{
if ($purchaseOrder->status >= 2) return $this->error('已审批', 40001);
$v = $request->validate(['status'=>'required|integer|in:2,4']);
$purchaseOrder->update(['status'=>$v['status'],'audit_user_id'=>auth()->id(),'audit_at'=>now()]);
return $this->success($purchaseOrder);
}
public function destroy(PurchaseOrder $purchaseOrder): JsonResponse
{
if ($purchaseOrder->status >= 2) return $this->error('已审批不可删除', 40001);
$purchaseOrder->delete(); return $this->success(null);
}
}
@@ -0,0 +1,48 @@
<?php
namespace App\Http\Controllers\Admin\Inventory;
use App\Http\Controllers\Controller;
use App\Models\Inventory\StockMovement;
use App\Models\Inventory\Inventory;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class StockMovementController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = StockMovement::query()->with('warehouse');
$query->when($request->warehouse_id, fn($q, $v) => $q->where('warehouse_id', $v));
$query->when($request->type, fn($q, $v) => $q->where('type', $v));
$query->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([
'warehouse_id'=>'required|exists:warehouses,id','movement_no'=>'required|string|max:50|unique:stock_movements',
'type'=>'required|integer|in:1,2,3,4','direction'=>'nullable|integer|in:1,2',
'items'=>'nullable|array','related_order'=>'nullable|string|max:50','remark'=>'nullable|string',
]);
$v['status'] = 0; $v['operator_id'] = auth()->id();
return $this->success(StockMovement::create($v));
}
public function show(StockMovement $stockMovement): JsonResponse { return $this->success($stockMovement->load(['warehouse','operator'])); }
public function confirm(StockMovement $stockMovement): JsonResponse
{
if ($stockMovement->status === 1) return $this->error('已确认', 40001);
$stockMovement->update(['status' => 1]);
// 更新库存
if ($stockMovement->items) {
foreach ($stockMovement->items as $item) {
$inv = Inventory::firstOrCreate(
['warehouse_id'=>$stockMovement->warehouse_id,'material_id'=>$item['material_id'],'batch_no'=>$item['batch_no']??null],
['quantity'=>0]
);
$qty = $item['quantity'] ?? 0;
$inv->quantity = $stockMovement->direction === 1 ? $inv->quantity + $qty : $inv->quantity - $qty;
$inv->save();
}
}
return $this->success($stockMovement);
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Admin\Inventory;
use App\Http\Controllers\Controller;
use App\Models\Inventory\Supplier;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class SupplierController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = Supplier::query();
$query->when($request->name, fn($q, $v) => $q->where('name', 'like', "%{$v}%"));
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
}
public function store(Request $request): JsonResponse
{
$v = $request->validate(['name'=>'required|string|max:100','contact'=>'nullable|string|max:50','phone'=>'nullable|string|max:20','address'=>'nullable|string|max:255','bank_info'=>'nullable|string|max:255','status'=>'sometimes|integer|in:0,1']);
return $this->success(Supplier::create($v));
}
public function show(Supplier $supplier): JsonResponse { return $this->success($supplier); }
public function update(Request $request, Supplier $supplier): JsonResponse { $v = $request->validate(['name'=>'sometimes|string|max:100','contact'=>'nullable|string|max:50','phone'=>'nullable|string|max:20','address'=>'nullable|string|max:255','bank_info'=>'nullable|string|max:255','status'=>'sometimes|integer|in:0,1']); $supplier->update($v); return $this->success($supplier); }
public function destroy(Supplier $supplier): JsonResponse { $supplier->delete(); return $this->success(null); }
}
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Admin\Inventory;
use App\Http\Controllers\Controller;
use App\Models\Inventory\Warehouse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class WarehouseController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = Warehouse::query()->with('manager');
$query->when($request->name, fn($q, $v) => $q->where('name', 'like', "%{$v}%"));
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
}
public function store(Request $request): JsonResponse
{
$v = $request->validate(['name'=>'required|string|max:50','manager_id'=>'nullable|exists:users,id','status'=>'sometimes|integer|in:0,1']);
return $this->success(Warehouse::create($v));
}
public function show(Warehouse $warehouse): JsonResponse { return $this->success($warehouse->load('manager')); }
public function update(Request $request, Warehouse $warehouse): JsonResponse { $v = $request->validate(['name'=>'sometimes|string|max:50','manager_id'=>'nullable|exists:users,id','status'=>'sometimes|integer|in:0,1']); $warehouse->update($v); return $this->success($warehouse); }
public function destroy(Warehouse $warehouse): JsonResponse { $warehouse->delete(); return $this->success(null); }
}
@@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers\Admin\Nanny;
use App\Http\Controllers\Controller;
use App\Models\Nanny\Nanny;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class NannyController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = Nanny::query();
$query->when($request->name, fn($q, $v) => $q->where('name', 'like', "%{$v}%"));
$query->when($request->level, fn($q, $v) => $q->where('level', $v));
$query->when($request->status, fn($q, $v) => $q->where('status', $v));
$query->when($request->cooperation_type, fn($q, $v) => $q->where('cooperation_type', $v));
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'name'=>'required|string|max:50','phone'=>'nullable|string|max:20','id_card'=>'nullable|string|max:20',
'level'=>'nullable|integer|in:1,2,3,4','skills'=>'nullable|array','experience_years'=>'nullable|integer|min:0',
'health_cert'=>'nullable|string|max:255','cooperation_type'=>'nullable|integer|in:1,2,3',
'base_salary'=>'nullable|numeric|min:0','introduction'=>'nullable|string','status'=>'sometimes|integer|in:1,2,3,4',
]);
return $this->success(Nanny::create($validated));
}
public function show(Nanny $nanny): JsonResponse { return $this->success($nanny->load(['orders.customer','schedules'])); }
public function update(Request $request, Nanny $nanny): JsonResponse
{
$validated = $request->validate([
'name'=>'sometimes|string|max:50','phone'=>'nullable|string|max:20','level'=>'nullable|integer|in:1,2,3,4',
'skills'=>'nullable|array','experience_years'=>'nullable|integer|min:0','cooperation_type'=>'nullable|integer|in:1,2,3',
'base_salary'=>'nullable|numeric|min:0','introduction'=>'nullable|string','status'=>'sometimes|integer|in:1,2,3,4',
]);
$nanny->update($validated);
return $this->success($nanny);
}
public function destroy(Nanny $nanny): JsonResponse
{
if ($nanny->status === 2) return $this->error('服务中的月嫂无法删除', 40001);
$nanny->delete();
return $this->success(null);
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers\Admin\Nanny;
use App\Http\Controllers\Controller;
use App\Models\Nanny\NannyOrder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class NannyOrderController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = NannyOrder::query()->with(['customer','nanny']);
$query->when($request->customer_id, fn($q, $v) => $q->where('customer_id', $v));
$query->when($request->nanny_id, fn($q, $v) => $q->where('nanny_id', $v));
$query->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
{
$validated = $request->validate([
'customer_id'=>'required|exists:customers,id','nanny_id'=>'nullable|exists:nannies,id',
'order_no'=>'required|string|max:50|unique:nanny_orders','service_type'=>'nullable|integer|in:1,2',
'start_date'=>'nullable|date','end_date'=>'nullable|date|after_or_equal:start_date',
'days'=>'nullable|integer|min:1','price'=>'nullable|numeric|min:0','remark'=>'nullable|string',
]);
$validated['status'] = 0;
$validated['created_by'] = auth()->id();
return $this->success(NannyOrder::create($validated));
}
public function show(NannyOrder $nannyOrder): JsonResponse { return $this->success($nannyOrder->load(['customer','nanny','reviews'])); }
public function update(Request $request, NannyOrder $nannyOrder): JsonResponse
{
if ($nannyOrder->status >= 4) return $this->error('已完成/取消的订单不可修改', 40001);
$validated = $request->validate([
'nanny_id'=>'nullable|exists:nannies,id','start_date'=>'nullable|date','end_date'=>'nullable|date',
'days'=>'nullable|integer|min:1','price'=>'nullable|numeric|min:0','status'=>'sometimes|integer|in:0,1,2,3,4,5',
'match_candidates'=>'nullable|array','remark'=>'nullable|string',
]);
$nannyOrder->update($validated);
if (isset($validated['nanny_id']) && $validated['nanny_id']) {
$nannyOrder->nanny->update(['status' => 2]);
}
return $this->success($nannyOrder);
}
public function destroy(NannyOrder $nannyOrder): JsonResponse
{
if ($nannyOrder->status >= 2) return $this->error('已签约后不可删除', 40001);
$nannyOrder->delete();
return $this->success(null);
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Admin\Nanny;
use App\Http\Controllers\Controller;
use App\Models\Nanny\NannySchedule;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class NannyScheduleController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = NannySchedule::query()->with(['nanny','order']);
$query->when($request->nanny_id, fn($q, $v) => $q->where('nanny_id', $v));
$query->when($request->start_date, fn($q, $v) => $q->where('schedule_date', '>=', $v));
$query->when($request->end_date, fn($q, $v) => $q->where('schedule_date', '<=', $v));
return $this->paginate($query->orderBy('schedule_date')->paginate($request->input('per_page', 31)));
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'nanny_id'=>'required|exists:nannies,id','nanny_order_id'=>'nullable|exists:nanny_orders,id',
'schedule_date'=>'required|date','type'=>'nullable|integer|in:1,2,3','remark'=>'nullable|string|max:255',
]);
return $this->success(NannySchedule::create($validated));
}
}
@@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers\Admin\Service;
use App\Http\Controllers\Controller;
use App\Models\Service\ServiceExecution;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ServiceExecutionController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = ServiceExecution::query()->with(['order','serviceItem','customer','technician']);
$query->when($request->service_order_id, fn($q, $v) => $q->where('service_order_id', $v));
$query->when($request->technician_id, fn($q, $v) => $q->where('technician_id', $v));
$query->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
{
$validated = $request->validate([
'service_order_id'=>'required|exists:service_orders,id','service_item_id'=>'required|exists:service_items,id',
'customer_id'=>'required|exists:customers,id','technician_id'=>'nullable|exists:users,id',
'scheduled_at'=>'nullable|date','remark'=>'nullable|string',
]);
$validated['status'] = 0;
return $this->success(ServiceExecution::create($validated));
}
public function start(ServiceExecution $serviceExecution): JsonResponse
{
if ($serviceExecution->status !== 0) return $this->error('只有待执行可开始', 40001);
$serviceExecution->update(['status'=>1,'started_at'=>now()]);
return $this->success($serviceExecution);
}
public function finish(ServiceExecution $serviceExecution): JsonResponse
{
if ($serviceExecution->status !== 1) return $this->error('只有执行中可完成', 40001);
$serviceExecution->update(['status'=>2,'ended_at'=>now()]);
return $this->success($serviceExecution);
}
}
@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Admin\Service;
use App\Http\Controllers\Controller;
use App\Models\Service\ServiceItem;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ServiceItemController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = ServiceItem::query();
$query->when($request->name, fn($q, $v) => $q->where('name', 'like', "%{$v}%"));
$query->when($request->category, fn($q, $v) => $q->where('category', $v));
$query->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
{
$validated = $request->validate([
'name' => 'required|string|max:100',
'category' => 'nullable|integer|in:1,2,3,4',
'price' => 'required|numeric|min:0',
'duration' => 'nullable|integer|min:1',
'description' => 'nullable|string',
'image' => 'nullable|string|max:255',
'status' => 'sometimes|integer|in:0,1',
]);
return $this->success(ServiceItem::create($validated));
}
public function show(ServiceItem $serviceItem): JsonResponse { return $this->success($serviceItem); }
public function update(Request $request, ServiceItem $serviceItem): JsonResponse
{
$validated = $request->validate([
'name' => 'sometimes|string|max:100',
'category' => 'nullable|integer|in:1,2,3,4',
'price' => 'sometimes|numeric|min:0',
'duration' => 'nullable|integer|min:1',
'description' => 'nullable|string',
'image' => 'nullable|string|max:255',
'status' => 'sometimes|integer|in:0,1',
]);
$serviceItem->update($validated);
return $this->success($serviceItem);
}
public function destroy(ServiceItem $serviceItem): JsonResponse { $serviceItem->delete(); return $this->success(null); }
}
@@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers\Admin\Service;
use App\Http\Controllers\Controller;
use App\Models\Service\ServiceOrder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ServiceOrderController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = ServiceOrder::query()->with(['customer']);
$query->when($request->order_no, fn($q, $v) => $q->where('order_no', 'like', "%{$v}%"));
$query->when($request->customer_id, fn($q, $v) => $q->where('customer_id', $v));
$query->when($request->type, fn($q, $v) => $q->where('type', $v));
$query->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
{
$validated = $request->validate([
'customer_id'=>'required|exists:customers,id','order_no'=>'required|string|max:50|unique:service_orders',
'type'=>'nullable|integer|in:1,2,3,4','items'=>'nullable|array',
'total_amount'=>'required|numeric|min:0','discount_amount'=>'sometimes|numeric|min:0','actual_amount'=>'required|numeric|min:0',
'pay_method'=>'nullable|integer|in:1,2,3,4','remark'=>'nullable|string',
]);
$validated['status'] = 0;
$validated['created_by'] = auth()->id();
return $this->success(ServiceOrder::create($validated));
}
public function show(ServiceOrder $serviceOrder): JsonResponse { return $this->success($serviceOrder->load(['customer','executions.serviceItem','executions.technician'])); }
public function update(Request $request, ServiceOrder $serviceOrder): JsonResponse
{
if ($serviceOrder->status >= 2) return $this->error('已完成/取消的订单不可修改', 40001);
$validated = $request->validate(['items'=>'nullable|array','total_amount'=>'sometimes|numeric|min:0','discount_amount'=>'sometimes|numeric|min:0','actual_amount'=>'sometimes|numeric|min:0','remark'=>'nullable|string']);
$serviceOrder->update($validated);
return $this->success($serviceOrder);
}
public function destroy(ServiceOrder $serviceOrder): JsonResponse
{
if ($serviceOrder->status >= 1) return $this->error('已付款的订单不可删除', 40001);
$serviceOrder->delete();
return $this->success(null);
}
public function pay(Request $request, ServiceOrder $serviceOrder): JsonResponse
{
if ($serviceOrder->status !== 0) return $this->error('该订单不可付款', 40001);
$validated = $request->validate(['pay_method'=>'required|integer|in:1,2,3,4']);
$serviceOrder->update(['status'=>1,'pay_method'=>$validated['pay_method'],'paid_at'=>now()]);
return $this->success($serviceOrder);
}
public function complete(ServiceOrder $serviceOrder): JsonResponse
{
if ($serviceOrder->status !== 1) return $this->error('只有已付款的订单可完成', 40001);
$serviceOrder->update(['status'=>2]);
return $this->success($serviceOrder);
}
}
@@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers\Admin\Service;
use App\Http\Controllers\Controller;
use App\Models\Service\ServicePackage;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ServicePackageController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = ServicePackage::query();
$query->when($request->name, fn($q, $v) => $q->where('name', 'like', "%{$v}%"));
$query->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
{
$validated = $request->validate(['name'=>'required|string|max:100','items'=>'nullable|array','price'=>'required|numeric|min:0','validity_days'=>'nullable|integer|min:1','status'=>'sometimes|integer|in:0,1']);
return $this->success(ServicePackage::create($validated));
}
public function show(ServicePackage $servicePackage): JsonResponse { return $this->success($servicePackage); }
public function update(Request $request, ServicePackage $servicePackage): JsonResponse
{
$validated = $request->validate(['name'=>'sometimes|string|max:100','items'=>'nullable|array','price'=>'sometimes|numeric|min:0','validity_days'=>'nullable|integer|min:1','status'=>'sometimes|integer|in:0,1']);
$servicePackage->update($validated);
return $this->success($servicePackage);
}
public function destroy(ServicePackage $servicePackage): JsonResponse { $servicePackage->delete(); return $this->success(null); }
}
@@ -0,0 +1,13 @@
<?php
namespace App\Models\Inventory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Inventory extends Model
{
const CREATED_AT = null;
protected $table = 'inventories';
protected $fillable = ['warehouse_id','material_id','quantity','batch_no','expire_date'];
protected function casts(): array { return ['quantity'=>'decimal:2','expire_date'=>'date']; }
public function warehouse(): BelongsTo { return $this->belongsTo(Warehouse::class); }
public function material(): BelongsTo { return $this->belongsTo(Material::class); }
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Models\Inventory;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
class Material extends Model
{
use BelongsToStore;
protected $fillable = ['store_id','name','code','category','unit','spec','min_stock','shelf_life_days','status'];
protected function casts(): array { return ['min_stock'=>'integer','shelf_life_days'=>'integer','status'=>'integer']; }
}
@@ -0,0 +1,15 @@
<?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 PurchaseOrder extends Model
{
use BelongsToStore;
protected $fillable = ['store_id','order_no','supplier_id','total_amount','status','items','audit_user_id','audit_at','remark','created_by'];
protected function casts(): array { return ['total_amount'=>'decimal:2','status'=>'integer','items'=>'array','audit_at'=>'datetime']; }
public function supplier(): BelongsTo { return $this->belongsTo(Supplier::class); }
public function auditor(): BelongsTo { return $this->belongsTo(User::class, 'audit_user_id'); }
public function creator(): BelongsTo { return $this->belongsTo(User::class, 'created_by'); }
}
@@ -0,0 +1,14 @@
<?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 StockMovement extends Model
{
use BelongsToStore;
protected $fillable = ['store_id','warehouse_id','movement_no','type','direction','items','related_order','status','operator_id','remark'];
protected function casts(): array { return ['type'=>'integer','direction'=>'integer','items'=>'array','status'=>'integer']; }
public function warehouse(): BelongsTo { return $this->belongsTo(Warehouse::class); }
public function operator(): BelongsTo { return $this->belongsTo(User::class, 'operator_id'); }
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Models\Inventory;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
class Supplier extends Model
{
use BelongsToStore;
protected $fillable = ['store_id','name','contact','phone','address','bank_info','status'];
protected function casts(): array { return ['status'=>'integer']; }
}
@@ -0,0 +1,13 @@
<?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 Warehouse extends Model
{
use BelongsToStore;
protected $fillable = ['store_id','name','manager_id','status'];
protected function casts(): array { return ['status'=>'integer']; }
public function manager(): BelongsTo { return $this->belongsTo(User::class, 'manager_id'); }
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Models\Nanny;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Nanny extends Model
{
use BelongsToStore;
protected $fillable = ['store_id','name','phone','id_card','avatar','level','skills','experience_years','health_cert','cooperation_type','base_salary','introduction','status'];
protected function casts(): array { return ['level'=>'integer','skills'=>'array','experience_years'=>'integer','cooperation_type'=>'integer','base_salary'=>'decimal:2','status'=>'integer']; }
public function orders(): HasMany { return $this->hasMany(NannyOrder::class); }
public function schedules(): HasMany { return $this->hasMany(NannySchedule::class); }
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Models\Nanny;
use App\Models\Crm\Customer;
use App\Models\User;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class NannyOrder extends Model
{
use BelongsToStore;
protected $fillable = ['store_id','customer_id','nanny_id','order_no','service_type','start_date','end_date','days','price','status','match_candidates','remark','created_by'];
protected function casts(): array { return ['service_type'=>'integer','start_date'=>'date','end_date'=>'date','days'=>'integer','price'=>'decimal:2','status'=>'integer','match_candidates'=>'array']; }
public function customer(): BelongsTo { return $this->belongsTo(Customer::class); }
public function nanny(): BelongsTo { return $this->belongsTo(Nanny::class); }
public function creator(): BelongsTo { return $this->belongsTo(User::class, 'created_by'); }
public function reviews(): HasMany { return $this->hasMany(NannyReview::class); }
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace App\Models\Nanny;
use App\Models\Crm\Customer;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class NannyReview extends Model
{
const UPDATED_AT = null;
protected $fillable = ['nanny_order_id','customer_id','nanny_id','score','content'];
protected function casts(): array { return ['score'=>'integer']; }
public function order(): BelongsTo { return $this->belongsTo(NannyOrder::class, 'nanny_order_id'); }
public function customer(): BelongsTo { return $this->belongsTo(Customer::class); }
public function nanny(): BelongsTo { return $this->belongsTo(Nanny::class); }
}
@@ -0,0 +1,12 @@
<?php
namespace App\Models\Nanny;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class NannySchedule extends Model
{
const UPDATED_AT = null;
protected $fillable = ['nanny_id','nanny_order_id','schedule_date','type','remark'];
protected function casts(): array { return ['schedule_date'=>'date','type'=>'integer']; }
public function nanny(): BelongsTo { return $this->belongsTo(Nanny::class); }
public function order(): BelongsTo { return $this->belongsTo(NannyOrder::class, 'nanny_order_id'); }
}
@@ -0,0 +1,15 @@
<?php
namespace App\Models\Service;
use App\Models\Crm\Customer;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ServiceExecution extends Model
{
protected $fillable = ['service_order_id','service_item_id','customer_id','technician_id','scheduled_at','started_at','ended_at','status','remark'];
protected function casts(): array { return ['scheduled_at'=>'datetime','started_at'=>'datetime','ended_at'=>'datetime','status'=>'integer']; }
public function order(): BelongsTo { return $this->belongsTo(ServiceOrder::class, 'service_order_id'); }
public function serviceItem(): BelongsTo { return $this->belongsTo(ServiceItem::class); }
public function customer(): BelongsTo { return $this->belongsTo(Customer::class); }
public function technician(): BelongsTo { return $this->belongsTo(User::class, 'technician_id'); }
}
@@ -0,0 +1,10 @@
<?php
namespace App\Models\Service;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
class ServiceItem extends Model
{
use BelongsToStore;
protected $fillable = ['store_id','name','category','price','duration','description','image','status'];
protected function casts(): array { return ['category'=>'integer','price'=>'decimal:2','duration'=>'integer','status'=>'integer']; }
}
@@ -0,0 +1,17 @@
<?php
namespace App\Models\Service;
use App\Models\Crm\Customer;
use App\Models\User;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class ServiceOrder extends Model
{
use BelongsToStore;
protected $fillable = ['store_id','customer_id','order_no','type','items','total_amount','discount_amount','actual_amount','status','pay_method','paid_at','created_by'];
protected function casts(): array { return ['type'=>'integer','items'=>'array','total_amount'=>'decimal:2','discount_amount'=>'decimal:2','actual_amount'=>'decimal:2','status'=>'integer','pay_method'=>'integer','paid_at'=>'datetime']; }
public function customer(): BelongsTo { return $this->belongsTo(Customer::class); }
public function creator(): BelongsTo { return $this->belongsTo(User::class, 'created_by'); }
public function executions(): HasMany { return $this->hasMany(ServiceExecution::class); }
}
@@ -0,0 +1,10 @@
<?php
namespace App\Models\Service;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
class ServicePackage extends Model
{
use BelongsToStore;
protected $fillable = ['store_id','name','items','price','validity_days','status'];
protected function casts(): array { return ['items'=>'array','price'=>'decimal:2','validity_days'=>'integer','status'=>'integer']; }
}
@@ -0,0 +1,13 @@
<?php
namespace App\Models\Service;
use App\Models\Crm\Customer;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ServiceReview extends Model
{
const UPDATED_AT = null;
protected $fillable = ['service_order_id','service_execution_id','customer_id','score','content'];
protected function casts(): array { return ['score'=>'integer']; }
public function order(): BelongsTo { return $this->belongsTo(ServiceOrder::class, 'service_order_id'); }
public function customer(): BelongsTo { return $this->belongsTo(Customer::class); }
}
@@ -0,0 +1,96 @@
<?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('service_items', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->string('name', 100);
$table->tinyInteger('category')->nullable()->comment('1产康 2零售 3加餐 4其他');
$table->decimal('price', 10, 2);
$table->integer('duration')->nullable()->comment('服务时长(分钟)');
$table->text('description')->nullable();
$table->string('image', 255)->nullable();
$table->tinyInteger('status')->default(1);
$table->timestamps();
$table->index(['store_id', 'category']);
});
// 服务包
Schema::create('service_packages', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->string('name', 100);
$table->json('items')->nullable()->comment('包含项目与次数');
$table->decimal('price', 10, 2);
$table->integer('validity_days')->nullable()->comment('有效天数');
$table->tinyInteger('status')->default(1);
$table->timestamps();
});
// 服务订单
Schema::create('service_orders', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->foreignId('customer_id')->constrained();
$table->string('order_no', 50)->unique();
$table->tinyInteger('type')->nullable()->comment('1单项 2服务包 3零售 4加餐');
$table->json('items')->nullable()->comment('订单项明细');
$table->decimal('total_amount', 12, 2)->nullable();
$table->decimal('discount_amount', 12, 2)->default(0);
$table->decimal('actual_amount', 12, 2)->nullable();
$table->tinyInteger('status')->default(0)->comment('0待付 1已付 2已完成 3已取消 4已退');
$table->tinyInteger('pay_method')->nullable()->comment('1微信 2支付宝 3现金 4挂账');
$table->timestamp('paid_at')->nullable();
$table->unsignedBigInteger('created_by')->nullable();
$table->timestamps();
$table->index('store_id');
$table->index('customer_id');
});
// 服务执行记录
Schema::create('service_executions', function (Blueprint $table) {
$table->id();
$table->foreignId('service_order_id')->constrained()->cascadeOnDelete();
$table->foreignId('service_item_id')->constrained();
$table->foreignId('customer_id')->constrained();
$table->unsignedBigInteger('technician_id')->nullable()->comment('技师/产康师');
$table->timestamp('scheduled_at')->nullable()->comment('预约时间');
$table->timestamp('started_at')->nullable();
$table->timestamp('ended_at')->nullable();
$table->tinyInteger('status')->default(0)->comment('0待执行 1执行中 2已完成 3已取消');
$table->text('remark')->nullable();
$table->timestamps();
$table->index('service_order_id');
$table->index('technician_id');
});
// 服务评价
Schema::create('service_reviews', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('service_order_id')->nullable();
$table->unsignedBigInteger('service_execution_id')->nullable();
$table->foreignId('customer_id')->constrained();
$table->tinyInteger('score')->nullable()->comment('1-5星');
$table->text('content')->nullable();
$table->timestamp('created_at')->nullable();
$table->index('service_order_id');
});
}
public function down(): void
{
Schema::dropIfExists('service_reviews');
Schema::dropIfExists('service_executions');
Schema::dropIfExists('service_orders');
Schema::dropIfExists('service_packages');
Schema::dropIfExists('service_items');
}
};
@@ -0,0 +1,84 @@
<?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('nannies', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->string('name', 50);
$table->string('phone', 20)->nullable();
$table->string('id_card', 20)->nullable();
$table->string('avatar', 255)->nullable();
$table->tinyInteger('level')->nullable()->comment('1初级 2中级 3高级 4金牌');
$table->json('skills')->nullable()->comment('技能标签');
$table->integer('experience_years')->nullable();
$table->string('health_cert', 255)->nullable()->comment('健康证');
$table->tinyInteger('cooperation_type')->nullable()->comment('1自有 2合作 3兼职');
$table->decimal('base_salary', 10, 2)->nullable();
$table->text('introduction')->nullable();
$table->tinyInteger('status')->default(1)->comment('1空闲 2服务中 3休假 4停用');
$table->timestamps();
$table->index(['store_id', 'status']);
});
// 月嫂订单
Schema::create('nanny_orders', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->foreignId('customer_id')->constrained();
$table->unsignedBigInteger('nanny_id')->nullable();
$table->string('order_no', 50)->unique();
$table->tinyInteger('service_type')->nullable()->comment('1月嫂 2育儿嫂');
$table->date('start_date')->nullable();
$table->date('end_date')->nullable();
$table->integer('days')->nullable();
$table->decimal('price', 12, 2)->nullable();
$table->tinyInteger('status')->default(0)->comment('0待匹配 1待确认 2已签约 3服务中 4已完成 5已取消');
$table->json('match_candidates')->nullable()->comment('匹配候选月嫂');
$table->text('remark')->nullable();
$table->unsignedBigInteger('created_by')->nullable();
$table->timestamps();
$table->index('store_id');
$table->index('nanny_id');
});
// 月嫂排班
Schema::create('nanny_schedules', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('nanny_id');
$table->unsignedBigInteger('nanny_order_id')->nullable();
$table->date('schedule_date');
$table->tinyInteger('type')->nullable()->comment('1服务 2休息 3培训');
$table->string('remark', 255)->nullable();
$table->timestamp('created_at')->nullable();
$table->index(['nanny_id', 'schedule_date']);
});
// 月嫂评价
Schema::create('nanny_reviews', function (Blueprint $table) {
$table->id();
$table->foreignId('nanny_order_id')->constrained()->cascadeOnDelete();
$table->foreignId('customer_id')->constrained();
$table->unsignedBigInteger('nanny_id');
$table->tinyInteger('score')->nullable()->comment('1-5');
$table->text('content')->nullable();
$table->timestamp('created_at')->nullable();
$table->index('nanny_id');
});
}
public function down(): void
{
Schema::dropIfExists('nanny_reviews');
Schema::dropIfExists('nanny_schedules');
Schema::dropIfExists('nanny_orders');
Schema::dropIfExists('nannies');
}
};
@@ -0,0 +1,106 @@
<?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('warehouses', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->string('name', 50);
$table->unsignedBigInteger('manager_id')->nullable();
$table->tinyInteger('status')->default(1);
$table->timestamps();
});
// 供应商
Schema::create('suppliers', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->string('name', 100);
$table->string('contact', 50)->nullable();
$table->string('phone', 20)->nullable();
$table->string('address', 255)->nullable();
$table->string('bank_info', 255)->nullable();
$table->tinyInteger('status')->default(1);
$table->timestamps();
});
// 物资(SKU
Schema::create('materials', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->string('name', 100);
$table->string('code', 50)->nullable()->comment('SKU编码');
$table->string('category', 50)->nullable();
$table->string('unit', 20)->nullable()->comment('单位');
$table->string('spec', 100)->nullable()->comment('规格');
$table->integer('min_stock')->default(0)->comment('安全库存');
$table->integer('shelf_life_days')->nullable()->comment('保质天数');
$table->tinyInteger('status')->default(1);
$table->timestamps();
$table->index('store_id');
$table->index('code');
});
// 库存
Schema::create('inventories', function (Blueprint $table) {
$table->id();
$table->foreignId('warehouse_id')->constrained();
$table->foreignId('material_id')->constrained();
$table->decimal('quantity', 12, 2)->default(0);
$table->string('batch_no', 50)->nullable();
$table->date('expire_date')->nullable();
$table->timestamp('updated_at')->nullable();
$table->unique(['warehouse_id', 'material_id', 'batch_no'], 'idx_wh_mat_batch');
});
// 采购单
Schema::create('purchase_orders', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->string('order_no', 50)->unique();
$table->unsignedBigInteger('supplier_id')->nullable();
$table->decimal('total_amount', 12, 2)->nullable();
$table->tinyInteger('status')->default(0)->comment('0草稿 1待审批 2已审批 3已入库 4已取消');
$table->json('items')->nullable()->comment('采购明细');
$table->unsignedBigInteger('audit_user_id')->nullable();
$table->timestamp('audit_at')->nullable();
$table->text('remark')->nullable();
$table->unsignedBigInteger('created_by')->nullable();
$table->timestamps();
});
// 出入库单
Schema::create('stock_movements', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->foreignId('warehouse_id')->constrained();
$table->string('movement_no', 50)->unique();
$table->tinyInteger('type')->comment('1入库 2出库 3调拨 4盘点');
$table->tinyInteger('direction')->nullable()->comment('1入 2出');
$table->json('items')->nullable()->comment('物资明细');
$table->string('related_order', 50)->nullable()->comment('关联单号');
$table->tinyInteger('status')->default(0)->comment('0草稿 1已确认');
$table->unsignedBigInteger('operator_id')->nullable();
$table->text('remark')->nullable();
$table->timestamps();
$table->index(['store_id', 'type']);
});
}
public function down(): void
{
Schema::dropIfExists('stock_movements');
Schema::dropIfExists('purchase_orders');
Schema::dropIfExists('inventories');
Schema::dropIfExists('materials');
Schema::dropIfExists('suppliers');
Schema::dropIfExists('warehouses');
}
};
+71
View File
@@ -31,6 +31,19 @@ use App\Http\Controllers\Admin\Meal\DishController;
use App\Http\Controllers\Admin\Meal\MealPlanTemplateController;
use App\Http\Controllers\Admin\Meal\DailyMealPlanController;
use App\Http\Controllers\Admin\Meal\MealReviewController;
use App\Http\Controllers\Admin\Service\ServiceItemController;
use App\Http\Controllers\Admin\Service\ServicePackageController;
use App\Http\Controllers\Admin\Service\ServiceOrderController;
use App\Http\Controllers\Admin\Service\ServiceExecutionController;
use App\Http\Controllers\Admin\Nanny\NannyController;
use App\Http\Controllers\Admin\Nanny\NannyOrderController;
use App\Http\Controllers\Admin\Nanny\NannyScheduleController;
use App\Http\Controllers\Admin\Inventory\WarehouseController;
use App\Http\Controllers\Admin\Inventory\SupplierController;
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;
/*
|--------------------------------------------------------------------------
@@ -176,4 +189,62 @@ Route::middleware(['auth:sanctum', 'store', 'oplog'])->group(function () {
Route::get('reviews', [MealReviewController::class, 'index']);
Route::post('reviews', [MealReviewController::class, 'store']);
});
// --- 服务产康模块 ---
Route::prefix('service')->group(function () {
// 服务项目
Route::apiResource('items', ServiceItemController::class);
// 服务套餐
Route::apiResource('packages', ServicePackageController::class);
// 服务订单
Route::apiResource('orders', ServiceOrderController::class);
Route::put('orders/{serviceOrder}/pay', [ServiceOrderController::class, 'pay']);
Route::put('orders/{serviceOrder}/complete', [ServiceOrderController::class, 'complete']);
// 服务执行
Route::get('executions', [ServiceExecutionController::class, 'index']);
Route::post('executions', [ServiceExecutionController::class, 'store']);
Route::put('executions/{serviceExecution}/start', [ServiceExecutionController::class, 'start']);
Route::put('executions/{serviceExecution}/finish', [ServiceExecutionController::class, 'finish']);
});
// --- 月嫂管理模块 ---
Route::prefix('nanny')->group(function () {
// 月嫂信息
Route::apiResource('nannies', NannyController::class);
// 月嫂订单
Route::apiResource('orders', NannyOrderController::class);
// 月嫂排班
Route::get('schedules', [NannyScheduleController::class, 'index']);
Route::post('schedules', [NannyScheduleController::class, 'store']);
});
// --- 进销存模块 ---
Route::prefix('inventory')->group(function () {
// 仓库管理
Route::apiResource('warehouses', WarehouseController::class);
// 供应商管理
Route::apiResource('suppliers', SupplierController::class);
// 物料管理
Route::apiResource('materials', MaterialController::class);
// 采购管理
Route::apiResource('purchase-orders', PurchaseOrderController::class);
Route::put('purchase-orders/{purchaseOrder}/audit', [PurchaseOrderController::class, 'audit']);
// 出入库流水
Route::get('stock-movements', [StockMovementController::class, 'index']);
Route::post('stock-movements', [StockMovementController::class, 'store']);
Route::get('stock-movements/{stockMovement}', [StockMovementController::class, 'show']);
Route::put('stock-movements/{stockMovement}/confirm', [StockMovementController::class, 'confirm']);
// 库存查询
Route::get('inventories', [InventoryController::class, 'index']);
});
});
+51
View File
@@ -0,0 +1,51 @@
import request from '@/utils/request.js'
// ─── Warehouses ──────────────────────────────────────────────────────────────
export const warehouseApi = {
getList: (params) => request.get('/inventory/warehouses', { params }),
getDetail: (id) => request.get(`/inventory/warehouses/${id}`),
create: (data) => request.post('/inventory/warehouses', data),
update: (id, data) => request.put(`/inventory/warehouses/${id}`, data),
delete: (id) => request.delete(`/inventory/warehouses/${id}`)
}
// ─── Suppliers ───────────────────────────────────────────────────────────────
export const supplierApi = {
getList: (params) => request.get('/inventory/suppliers', { params }),
getDetail: (id) => request.get(`/inventory/suppliers/${id}`),
create: (data) => request.post('/inventory/suppliers', data),
update: (id, data) => request.put(`/inventory/suppliers/${id}`, data),
delete: (id) => request.delete(`/inventory/suppliers/${id}`)
}
// ─── Materials ───────────────────────────────────────────────────────────────
export const materialApi = {
getList: (params) => request.get('/inventory/materials', { params }),
getDetail: (id) => request.get(`/inventory/materials/${id}`),
create: (data) => request.post('/inventory/materials', data),
update: (id, data) => request.put(`/inventory/materials/${id}`, data),
delete: (id) => request.delete(`/inventory/materials/${id}`)
}
// ─── Purchase Orders ─────────────────────────────────────────────────────────
export const purchaseOrderApi = {
getList: (params) => request.get('/inventory/purchase-orders', { params }),
getDetail: (id) => request.get(`/inventory/purchase-orders/${id}`),
create: (data) => request.post('/inventory/purchase-orders', data),
update: (id, data) => request.put(`/inventory/purchase-orders/${id}`, data),
delete: (id) => request.delete(`/inventory/purchase-orders/${id}`),
audit: (id, data) => request.put(`/inventory/purchase-orders/${id}/audit`, data)
}
// ─── Stock Movements ─────────────────────────────────────────────────────────
export const stockMovementApi = {
getList: (params) => request.get('/inventory/stock-movements', { params }),
getDetail: (id) => request.get(`/inventory/stock-movements/${id}`),
create: (data) => request.post('/inventory/stock-movements', data),
confirm: (id) => request.put(`/inventory/stock-movements/${id}/confirm`)
}
// ─── Inventories ─────────────────────────────────────────────────────────────
export const inventoryApi = {
getList: (params) => request.get('/inventory/inventories', { params })
}
+25
View File
@@ -0,0 +1,25 @@
import request from '@/utils/request.js'
// ─── Nannies ─────────────────────────────────────────────────────────────────
export const nannyApi = {
getList: (params) => request.get('/nanny/nannies', { params }),
getDetail: (id) => request.get(`/nanny/nannies/${id}`),
create: (data) => request.post('/nanny/nannies', data),
update: (id, data) => request.put(`/nanny/nannies/${id}`, data),
delete: (id) => request.delete(`/nanny/nannies/${id}`)
}
// ─── Nanny Orders ────────────────────────────────────────────────────────────
export const nannyOrderApi = {
getList: (params) => request.get('/nanny/orders', { params }),
getDetail: (id) => request.get(`/nanny/orders/${id}`),
create: (data) => request.post('/nanny/orders', data),
update: (id, data) => request.put(`/nanny/orders/${id}`, data),
delete: (id) => request.delete(`/nanny/orders/${id}`)
}
// ─── Nanny Schedules ─────────────────────────────────────────────────────────
export const nannyScheduleApi = {
getList: (params) => request.get('/nanny/schedules', { params }),
create: (data) => request.post('/nanny/schedules', data)
}
+38
View File
@@ -0,0 +1,38 @@
import request from '@/utils/request.js'
// ─── Service Items ───────────────────────────────────────────────────────────
export const serviceItemApi = {
getList: (params) => request.get('/service/items', { params }),
getDetail: (id) => request.get(`/service/items/${id}`),
create: (data) => request.post('/service/items', data),
update: (id, data) => request.put(`/service/items/${id}`, data),
delete: (id) => request.delete(`/service/items/${id}`)
}
// ─── Service Packages ────────────────────────────────────────────────────────
export const servicePackageApi = {
getList: (params) => request.get('/service/packages', { params }),
getDetail: (id) => request.get(`/service/packages/${id}`),
create: (data) => request.post('/service/packages', data),
update: (id, data) => request.put(`/service/packages/${id}`, data),
delete: (id) => request.delete(`/service/packages/${id}`)
}
// ─── Service Orders ──────────────────────────────────────────────────────────
export const serviceOrderApi = {
getList: (params) => request.get('/service/orders', { params }),
getDetail: (id) => request.get(`/service/orders/${id}`),
create: (data) => request.post('/service/orders', data),
update: (id, data) => request.put(`/service/orders/${id}`, data),
delete: (id) => request.delete(`/service/orders/${id}`),
pay: (id, data) => request.put(`/service/orders/${id}/pay`, data),
complete: (id) => request.put(`/service/orders/${id}/complete`)
}
// ─── Service Executions ──────────────────────────────────────────────────────
export const serviceExecutionApi = {
getList: (params) => request.get('/service/executions', { params }),
create: (data) => request.post('/service/executions', data),
start: (id) => request.put(`/service/executions/${id}/start`),
finish: (id) => request.put(`/service/executions/${id}/finish`)
}
@@ -0,0 +1,103 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { 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 searchForm = reactive({ name: '', category: '', status: '' })
const pagination = reactive({ page: 1, per_page: 15 })
const formRef = ref(null)
const form = reactive({ id: null, name: '', code: '', category: '', unit: '', spec: '', safety_stock: 0, status: 1, sort: 0 })
const isEdit = ref(false)
const formRules = { name: [{ required: true, message: '请输入物料名称', trigger: 'blur' }] }
const categoryMap = { 1: '食材', 2: '日用品', 3: '母婴用品', 4: '医疗用品', 5: '其他' }
async function fetchList() {
loading.value = true
try {
const res = await materialApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} finally { loading.value = false }
}
function handleSearch() { pagination.page = 1; fetchList() }
function handleReset() { Object.assign(searchForm, { name: '', category: '', status: '' }); handleSearch() }
function handleAdd() { isEdit.value = false; Object.assign(form, { id: null, name: '', code: '', category: '', unit: '', spec: '', safety_stock: 0, status: 1, sort: 0 }); dialogVisible.value = true }
async function handleEdit(row) { isEdit.value = true; const res = await materialApi.getDetail(row.id); const d = res.data; Object.assign(form, { id: d.id, name: d.name, code: d.code || '', category: d.category || '', unit: d.unit || '', spec: d.spec || '', safety_stock: d.safety_stock || 0, status: d.status, sort: d.sort || 0 }); dialogVisible.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 materialApi.update(form.id, form); ElMessage.success('更新成功') }
else { await materialApi.create(form); ElMessage.success('创建成功') }
dialogVisible.value = false; fetchList()
} finally { submitLoading.value = false }
})
}
async function handleDelete(row) {
await ElMessageBox.confirm(`确定删除物料「${row.name}」吗?`, '删除确认', { type: 'warning' })
await materialApi.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())
</script>
<template>
<div class="page-container">
<div class="search-bar">
<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)" />
</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="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>
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑物料':'新增物料'" width="550px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="80px">
<el-form-item label="物料名称" prop="name"><el-input v-model="form.name" /></el-form-item>
<el-form-item label="物料编码"><el-input v-model="form.code" /></el-form-item>
<el-form-item label="分类"><el-select v-model="form.category" style="width:100%"><el-option v-for="(v,k) in categoryMap" :key="k" :label="v" :value="Number(k)" /></el-select></el-form-item>
<el-form-item label="单位"><el-input v-model="form.unit" /></el-form-item>
<el-form-item label="规格"><el-input v-model="form.spec" /></el-form-item>
<el-form-item label="安全库存"><el-input-number v-model="form.safety_stock" :min="0" style="width:100%" /></el-form-item>
<el-form-item label="状态"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" /></el-form-item>
<el-form-item label="排序"><el-input-number v-model="form.sort" :min="0" style="width:100%" /></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,141 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { purchaseOrderApi } from '@/api/inventory.js'
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const dialogVisible = ref(false)
const submitLoading = ref(false)
const searchForm = reactive({ order_no: '', status: '' })
const pagination = reactive({ page: 1, per_page: 20 })
const formRef = ref(null)
const form = reactive({ id: null, order_no: '', supplier_id: '', total_amount: 0, remark: '' })
const isEdit = ref(false)
const formRules = {
order_no: [{ required: true, message: '请输入采购单号', trigger: 'blur' }]
}
const statusMap = { 0: '待审批', 2: '已通过', 4: '已驳回' }
const statusType = { 0: 'warning', 2: 'success', 4: 'danger' }
async function fetchList() {
loading.value = true
try {
const res = await purchaseOrderApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} finally { loading.value = false }
}
function handleSearch() { pagination.page = 1; fetchList() }
function handleReset() { Object.assign(searchForm, { order_no: '', status: '' }); handleSearch() }
function handleAdd() {
isEdit.value = false
Object.assign(form, { id: null, order_no: '', supplier_id: '', total_amount: 0, remark: '' })
dialogVisible.value = true
}
async function handleEdit(row) {
if (row.status >= 2) { ElMessage.warning('只能编辑待审批的采购单'); return }
isEdit.value = true
const res = await purchaseOrderApi.getDetail(row.id)
const d = res.data
Object.assign(form, { id: d.id, order_no: d.order_no, supplier_id: d.supplier_id || '', total_amount: d.total_amount, remark: d.remark || '' })
dialogVisible.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 purchaseOrderApi.update(form.id, form); ElMessage.success('更新成功') }
else { await purchaseOrderApi.create(form); ElMessage.success('创建成功') }
dialogVisible.value = false; fetchList()
} finally { submitLoading.value = false }
})
}
async function handleDelete(row) {
if (row.status >= 2) { ElMessage.warning('只能删除待审批的采购单'); return }
await ElMessageBox.confirm(`确定删除采购单「${row.order_no}」吗?`, '删除确认', { type: 'warning' })
await purchaseOrderApi.delete(row.id); ElMessage.success('删除成功'); fetchList()
}
async function handleAudit(row, status) {
const msg = status === 2 ? '确认通过?' : '确认驳回?'
await ElMessageBox.confirm(msg, '审批确认', { type: 'warning' })
await purchaseOrderApi.audit(row.id, { status }); ElMessage.success('操作成功'); fetchList()
}
function handlePageChange(val) { pagination.page = val; fetchList() }
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
onMounted(() => fetchList())
</script>
<template>
<div class="page-container">
<div class="search-bar">
<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)" />
</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="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>
<!-- 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">
<el-input v-model="form.order_no" />
</el-form-item>
<el-form-item label="供应商ID">
<el-input v-model="form.supplier_id" placeholder="供应商ID" />
</el-form-item>
<el-form-item label="总金额">
<el-input-number v-model="form.total_amount" :min="0" :precision="2" style="width:100%" />
</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>
@@ -0,0 +1,170 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { stockMovementApi, warehouseApi } 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 searchForm = reactive({ warehouse_id: '', type: '', status: '' })
const pagination = reactive({ page: 1, per_page: 20 })
const formRef = ref(null)
const form = reactive({
id: null, warehouse_id: '', movement_no: '', type: '', direction: '', related_order: '', remark: ''
})
const formRules = {
warehouse_id: [{ required: true, message: '请选择仓库', trigger: 'change' }],
movement_no: [{ required: true, message: '请输入单据编号', trigger: 'blur' }],
type: [{ required: true, message: '请选择类型', trigger: 'change' }],
direction: [{ required: true, message: '请选择方向', trigger: 'change' }]
}
const typeMap = { 1: '采购入库', 2: '领用出库', 3: '退货入库', 4: '报损出库' }
const directionMap = { 1: '入库', 2: '出库' }
const directionType = { 1: 'success', 2: 'danger' }
const statusMap = { 0: '待确认', 1: '已确认' }
const statusType = { 0: 'warning', 1: 'success' }
async function fetchList() {
loading.value = true
try {
const res = await stockMovementApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} 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: '', type: '', status: '' }); handleSearch() }
function handleAdd() {
Object.assign(form, { id: null, warehouse_id: '', movement_no: '', type: '', direction: '', related_order: '', 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 stockMovementApi.create(form)
ElMessage.success('创建成功'); dialogVisible.value = false; fetchList()
} finally { submitLoading.value = false }
})
}
async function handleConfirm(row) {
await ElMessageBox.confirm('确认该出入库单?确认后将更新库存', '确认操作', { type: 'warning' })
await stockMovementApi.confirm(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(); fetchWarehouses() })
</script>
<template>
<div class="page-container">
<div class="search-bar">
<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>
<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-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="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>
<el-dialog v-model="dialogVisible" title="新增出入库" width="600px" 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="warehouse_id">
<el-select v-model="form.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="movement_no">
<el-input v-model="form.movement_no" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="类型" prop="type">
<el-select v-model="form.type" placeholder="选择类型" style="width:100%">
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="方向" prop="direction">
<el-select v-model="form.direction" placeholder="选择方向" style="width:100%">
<el-option v-for="(v, k) in directionMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="关联单号">
<el-input v-model="form.related_order" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" :rows="2" />
</el-form-item>
</el-col>
</el-row>
</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,105 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { supplierApi } from '@/api/inventory.js'
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const dialogVisible = ref(false)
const submitLoading = ref(false)
const searchForm = reactive({ name: '', status: '' })
const pagination = reactive({ page: 1, per_page: 15 })
const formRef = ref(null)
const form = reactive({ id: null, name: '', contact_person: '', contact_phone: '', address: '', bank_name: '', bank_account: '', status: 1, remark: '', sort: 0 })
const isEdit = ref(false)
const formRules = { name: [{ required: true, message: '请输入供应商名称', trigger: 'blur' }] }
async function fetchList() {
loading.value = true
try {
const res = await supplierApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} finally { loading.value = false }
}
function handleSearch() { pagination.page = 1; fetchList() }
function handleReset() { Object.assign(searchForm, { name: '', status: '' }); handleSearch() }
function handleAdd() {
isEdit.value = false
Object.assign(form, { id: null, name: '', contact_person: '', contact_phone: '', address: '', bank_name: '', bank_account: '', status: 1, remark: '', sort: 0 })
dialogVisible.value = true
}
async function handleEdit(row) { isEdit.value = true; Object.assign(form, { ...row }); dialogVisible.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 supplierApi.update(form.id, form); ElMessage.success('更新成功') }
else { await supplierApi.create(form); ElMessage.success('创建成功') }
dialogVisible.value = false; fetchList()
} finally { submitLoading.value = false }
})
}
async function handleDelete(row) {
await ElMessageBox.confirm(`确定删除供应商「${row.name}」吗?`, '删除确认', { type: 'warning' })
await supplierApi.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())
</script>
<template>
<div class="page-container">
<div class="search-bar">
<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="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>
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑供应商':'新增供应商'" width="600px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
<el-form-item label="供应商名称" prop="name"><el-input v-model="form.name" placeholder="请输入供应商名称" /></el-form-item>
<el-form-item label="联系人"><el-input v-model="form.contact_person" placeholder="请输入联系人" /></el-form-item>
<el-form-item label="联系电话"><el-input v-model="form.contact_phone" placeholder="请输入联系电话" /></el-form-item>
<el-form-item label="地址"><el-input v-model="form.address" placeholder="请输入地址" /></el-form-item>
<el-form-item label="开户银行"><el-input v-model="form.bank_name" placeholder="请输入开户银行" /></el-form-item>
<el-form-item label="银行账号"><el-input v-model="form.bank_account" placeholder="请输入银行账号" /></el-form-item>
<el-form-item label="状态"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" /></el-form-item>
<el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注信息" /></el-form-item>
<el-form-item label="排序"><el-input-number v-model="form.sort" :min="0" :max="999" /></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,270 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { warehouseApi } from '@/api/inventory.js'
// ─── State ────────────────────────────────────────────────────────────────────
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const dialogVisible = ref(false)
const dialogTitle = ref('新增仓库')
const submitLoading = ref(false)
const isEdit = ref(false)
const formRef = ref(null)
const searchForm = reactive({ name: '', status: '' })
const pagination = reactive({ page: 1, page_size: 20 })
// ─── Form ─────────────────────────────────────────────────────────────────────
const form = reactive({
id: null,
name: '',
address: '',
contact_person: '',
contact_phone: '',
status: 1,
sort: 0
})
const formRules = {
name: [{ required: true, message: '请输入仓库名称', trigger: 'blur' }]
}
// ─── Methods ──────────────────────────────────────────────────────────────────
async function fetchList() {
loading.value = true
try {
const res = await warehouseApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || []
total.value = res.data?.total || 0
} catch {
//
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
fetchList()
}
function handleReset() {
Object.assign(searchForm, { name: '', status: '' })
handleSearch()
}
function handleAdd() {
isEdit.value = false
dialogTitle.value = '新增仓库'
Object.assign(form, {
id: null, name: '', address: '', contact_person: '',
contact_phone: '', status: 1, sort: 0
})
dialogVisible.value = true
}
async function handleEdit(row) {
isEdit.value = true
dialogTitle.value = `编辑仓库 - ${row.name}`
const res = await warehouseApi.getDetail(row.id)
const warehouse = res.data
Object.assign(form, {
id: warehouse.id,
name: warehouse.name,
address: warehouse.address || '',
contact_person: warehouse.contact_person || '',
contact_phone: warehouse.contact_phone || '',
status: warehouse.status,
sort: warehouse.sort || 0
})
dialogVisible.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 warehouseApi.update(form.id, form)
ElMessage.success('仓库信息更新成功')
} else {
await warehouseApi.create(form)
ElMessage.success('仓库创建成功')
}
dialogVisible.value = false
fetchList()
} catch {
//
} finally {
submitLoading.value = false
}
})
}
async function handleDelete(row) {
await ElMessageBox.confirm(
`确定删除仓库「${row.name}」吗?删除后该仓库下的数据将不可访问。`,
'删除确认',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning',
confirmButtonClass: 'el-button--danger'
}
)
await warehouseApi.delete(row.id)
ElMessage.success('删除成功')
fetchList()
}
async function handleStatusToggle(row) {
const newStatus = row.status === 1 ? 0 : 1
const label = newStatus === 1 ? '启用' : '停用'
try {
await warehouseApi.update(row.id, { ...row, status: newStatus })
row.status = newStatus
ElMessage.success(`仓库已${label}`)
} catch {
//
}
}
function handlePageChange(val) {
pagination.page = val
fetchList()
}
function handleSizeChange(val) {
pagination.page_size = val
pagination.page = 1
fetchList()
}
onMounted(fetchList)
</script>
<template>
<div class="page-container">
<!-- Search -->
<div class="search-bar">
<el-input
v-model="searchForm.name"
placeholder="搜索仓库名称"
style="width: 220px"
clearable
@keydown.enter="handleSearch"
>
<template #prefix><el-icon><Search /></el-icon></template>
</el-input>
<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" :icon="'Search'" @click="handleSearch">查询</el-button>
<el-button :icon="'Refresh'" @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>
<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">
<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="contact_person">
<el-input v-model="form.contact_person" placeholder="请输入联系人" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="地址" prop="address">
<el-input v-model="form.address" placeholder="请输入仓库地址" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="contact_phone">
<el-input v-model="form.contact_phone" placeholder="请输入联系电话" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-switch
v-model="form.status"
:active-value="1"
:inactive-value="0"
active-text="启用"
inactive-text="停用"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" :min="0" :max="9999" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
{{ isEdit ? '保存修改' : '确认创建' }}
</el-button>
</template>
</el-dialog>
</div>
</template>
+105
View File
@@ -0,0 +1,105 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { nannyApi } from '@/api/nanny.js'
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const dialogVisible = ref(false)
const submitLoading = ref(false)
const searchForm = reactive({ name: '', level: '', status: '' })
const pagination = reactive({ page: 1, per_page: 15 })
const formRef = ref(null)
const form = reactive({ id: null, name: '', phone: '', id_card: '', level: '', specialties: [], experience_years: 0, daily_price: 0, status: 1, avatar: '', remark: '' })
const isEdit = ref(false)
const formRules = {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }]
}
const statusMap = { 1: '待岗', 2: '在岗', 3: '休假', 0: '离职' }
const statusType = { 1: 'info', 2: 'success', 3: 'warning', 0: 'danger' }
const levelMap = { 1: '初级', 2: '中级', 3: '高级', 4: '金牌' }
async function fetchList() {
loading.value = true
try {
const res = await nannyApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} finally { loading.value = false }
}
function handleSearch() { pagination.page = 1; fetchList() }
function handleReset() { Object.assign(searchForm, { name: '', level: '', status: '' }); handleSearch() }
function handleAdd() { isEdit.value = false; Object.assign(form, { id: null, name: '', phone: '', id_card: '', level: '', specialties: [], experience_years: 0, daily_price: 0, status: 1, avatar: '', remark: '' }); dialogVisible.value = true }
function handleEdit(row) { isEdit.value = true; Object.assign(form, { id: row.id, name: row.name, phone: row.phone, id_card: row.id_card || '', level: row.level, specialties: row.specialties || [], experience_years: row.experience_years, daily_price: row.daily_price, status: row.status, avatar: row.avatar || '', remark: row.remark || '' }); dialogVisible.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 nannyApi.update(form.id, form) } else { await nannyApi.create(form) }
ElMessage.success(isEdit.value ? '更新成功' : '创建成功'); dialogVisible.value = false; fetchList()
} finally { submitLoading.value = false }
})
}
async function handleDelete(row) {
await ElMessageBox.confirm(`确定删除月嫂「${row.name}」吗?`, '删除确认', { type: 'warning' })
await nannyApi.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())
</script>
<template>
<div class="page-container">
<div class="search-bar">
<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>
<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="name"><el-input v-model="form.name" /></el-form-item>
<el-form-item label="手机号" prop="phone"><el-input v-model="form.phone" /></el-form-item>
<el-form-item label="身份证号"><el-input v-model="form.id_card" /></el-form-item>
<el-form-item label="等级"><el-select v-model="form.level" style="width:100%"><el-option v-for="(v,k) in levelMap" :key="k" :label="v" :value="Number(k)" /></el-select></el-form-item>
<el-form-item label="经验(年)"><el-input-number v-model="form.experience_years" :min="0" style="width:100%" /></el-form-item>
<el-form-item label="日价(元)"><el-input-number v-model="form.daily_price" :min="0" :precision="2" 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="Number(k)" /></el-select></el-form-item>
<el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="2" /></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>
+107
View File
@@ -0,0 +1,107 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { nannyOrderApi } from '@/api/nanny.js'
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const dialogVisible = ref(false)
const submitLoading = ref(false)
const searchForm = reactive({ order_no: '', status: '' })
const pagination = reactive({ page: 1, per_page: 15 })
const formRef = ref(null)
const form = reactive({ id: null, customer_id: '', nanny_id: '', order_no: '', service_type: '', start_date: '', end_date: '', days: 0, price: 0, remark: '' })
const isEdit = ref(false)
const formRules = {
customer_id: [{ required: true, message: '请选择客户', trigger: 'change' }],
order_no: [{ required: true, message: '请输入订单号', trigger: 'blur' }]
}
const statusMap = { 0: '待匹配', 1: '已匹配', 2: '已签约', 3: '服务中', 4: '已完成', 5: '已取消' }
const statusType = { 0: 'info', 1: 'warning', 2: 'primary', 3: 'success', 4: '', 5: 'danger' }
const serviceTypeMap = { 1: '住家', 2: '白班' }
async function fetchList() {
loading.value = true
try {
const res = await nannyOrderApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} finally { loading.value = false }
}
function handleSearch() { pagination.page = 1; fetchList() }
function handleReset() { Object.assign(searchForm, { order_no: '', status: '' }); handleSearch() }
function handleAdd() { isEdit.value = false; Object.assign(form, { id: null, customer_id: '', nanny_id: '', order_no: '', service_type: '', start_date: '', end_date: '', days: 0, price: 0, remark: '' }); dialogVisible.value = true }
function handleEdit(row) { isEdit.value = true; Object.assign(form, { id: row.id, customer_id: row.customer_id, nanny_id: row.nanny_id || '', order_no: row.order_no, service_type: row.service_type, start_date: row.start_date || '', end_date: row.end_date || '', days: row.days, price: row.price, remark: row.remark || '' }); dialogVisible.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 nannyOrderApi.update(form.id, form) } else { await nannyOrderApi.create(form) }
ElMessage.success(isEdit.value ? '更新成功' : '创建成功'); dialogVisible.value = false; fetchList()
} finally { submitLoading.value = false }
})
}
async function handleDelete(row) {
await ElMessageBox.confirm(`确定删除订单「${row.order_no}」吗?`, '删除确认', { type: 'warning' })
await nannyOrderApi.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())
</script>
<template>
<div class="page-container">
<div class="search-bar">
<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>
<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="客户ID" prop="customer_id"><el-input v-model="form.customer_id" /></el-form-item>
<el-form-item label="月嫂ID"><el-input v-model="form.nanny_id" /></el-form-item>
<el-form-item label="订单号" prop="order_no"><el-input v-model="form.order_no" :disabled="isEdit" /></el-form-item>
<el-form-item label="服务类型"><el-select v-model="form.service_type" style="width:100%"><el-option v-for="(v,k) in serviceTypeMap" :key="k" :label="v" :value="Number(k)" /></el-select></el-form-item>
<el-form-item label="开始日期"><el-date-picker v-model="form.start_date" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item>
<el-form-item label="结束日期"><el-date-picker v-model="form.end_date" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item>
<el-form-item label="天数"><el-input-number v-model="form.days" :min="1" style="width:100%" /></el-form-item>
<el-form-item label="费用(元)"><el-input-number v-model="form.price" :min="0" :precision="2" style="width:100%" /></el-form-item>
<el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="2" /></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,80 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { nannyScheduleApi, nannyApi } from '@/api/nanny.js'
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const dialogVisible = ref(false)
const submitLoading = ref(false)
const nannies = ref([])
const searchForm = reactive({ nanny_id: '', start_date: '', end_date: '' })
const pagination = reactive({ page: 1, per_page: 31 })
const form = reactive({ nanny_id: '', nanny_order_id: '', schedule_date: '', type: 1, remark: '' })
const typeMap = { 1: '上班', 2: '休息', 3: '请假' }
const typeType = { 1: 'success', 2: 'info', 3: 'warning' }
async function fetchList() {
loading.value = true
try {
const res = await nannyScheduleApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} finally { loading.value = false }
}
async function fetchNannies() { try { const res = await nannyApi.getList({ per_page: 200, status: 2 }); nannies.value = res.data?.list || res.data?.data || [] } catch {} }
function handleSearch() { pagination.page = 1; fetchList() }
function handleReset() { Object.assign(searchForm, { nanny_id: '', start_date: '', end_date: '' }); handleSearch() }
function handleAdd() { Object.assign(form, { nanny_id: '', nanny_order_id: '', schedule_date: '', type: 1, remark: '' }); dialogVisible.value = true }
async function handleSubmit() {
if (!form.nanny_id || !form.schedule_date) { ElMessage.warning('请填写必要信息'); return }
submitLoading.value = true
try {
await nannyScheduleApi.create(form)
ElMessage.success('排班成功'); dialogVisible.value = false; fetchList()
} finally { submitLoading.value = false }
}
function handlePageChange(val) { pagination.page = val; fetchList() }
onMounted(() => { fetchList(); fetchNannies() })
</script>
<template>
<div class="page-container">
<div class="search-bar">
<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>
<el-dialog v-model="dialogVisible" title="新增排班" width="450px" destroy-on-close>
<el-form label-width="80px">
<el-form-item label="月嫂"><el-select v-model="form.nanny_id" style="width:100%"><el-option v-for="n in nannies" :key="n.id" :label="n.name" :value="n.id" /></el-select></el-form-item>
<el-form-item label="日期"><el-date-picker v-model="form.schedule_date" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item>
<el-form-item label="类型"><el-select v-model="form.type" style="width:100%"><el-option v-for="(v,k) in typeMap" :key="k" :label="v" :value="Number(k)" /></el-select></el-form-item>
<el-form-item label="备注"><el-input v-model="form.remark" /></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>
+102
View File
@@ -0,0 +1,102 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { serviceItemApi } from '@/api/service.js'
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const dialogVisible = ref(false)
const submitLoading = ref(false)
const searchForm = reactive({ name: '', category: '', status: '' })
const pagination = reactive({ page: 1, per_page: 15 })
const formRef = ref(null)
const form = reactive({ id: null, name: '', category: '', price: 0, duration: 0, description: '', status: 1, sort: 0 })
const isEdit = ref(false)
const formRules = {
name: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
price: [{ required: true, message: '请输入价格', trigger: 'blur' }]
}
const statusMap = { 1: '启用', 0: '停用' }
const categoryMap = { 1: '产后修复', 2: '中医调理', 3: '美容美体', 4: '其他' }
async function fetchList() {
loading.value = true
try {
const res = await serviceItemApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} finally { loading.value = false }
}
function handleSearch() { pagination.page = 1; fetchList() }
function handleReset() { Object.assign(searchForm, { name: '', category: '', status: '' }); handleSearch() }
function handleAdd() { isEdit.value = false; Object.assign(form, { id: null, name: '', category: '', price: 0, duration: 0, description: '', status: 1, sort: 0 }); dialogVisible.value = true }
function handleEdit(row) { isEdit.value = true; Object.assign(form, { id: row.id, name: row.name, category: row.category, price: row.price, duration: row.duration, description: row.description || '', status: row.status, sort: row.sort }); dialogVisible.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 serviceItemApi.update(form.id, form) } else { await serviceItemApi.create(form) }
ElMessage.success(isEdit.value ? '更新成功' : '创建成功'); dialogVisible.value = false; fetchList()
} finally { submitLoading.value = false }
})
}
async function handleDelete(row) {
await ElMessageBox.confirm(`确定删除「${row.name}」吗?`, '删除确认', { type: 'warning' })
await serviceItemApi.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())
</script>
<template>
<div class="page-container">
<div class="search-bar">
<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>
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑项目':'新增项目'" width="500px" 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" /></el-form-item>
<el-form-item label="分类"><el-select v-model="form.category" style="width:100%"><el-option v-for="(v,k) in categoryMap" :key="k" :label="v" :value="Number(k)" /></el-select></el-form-item>
<el-form-item label="价格" prop="price"><el-input-number v-model="form.price" :min="0" :precision="2" style="width:100%" /></el-form-item>
<el-form-item label="时长(分钟)"><el-input-number v-model="form.duration" :min="0" style="width:100%" /></el-form-item>
<el-form-item label="描述"><el-input v-model="form.description" type="textarea" :rows="3" /></el-form-item>
<el-form-item label="状态"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" /></el-form-item>
<el-form-item label="排序"><el-input-number v-model="form.sort" :min="0" /></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>
+115
View File
@@ -0,0 +1,115 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { serviceOrderApi } from '@/api/service.js'
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const dialogVisible = ref(false)
const submitLoading = ref(false)
const searchForm = reactive({ order_no: '', status: '' })
const pagination = reactive({ page: 1, per_page: 15 })
const formRef = ref(null)
const form = reactive({ id: null, customer_id: '', order_no: '', type: '', total_amount: 0, discount_amount: 0, actual_amount: 0, remark: '' })
const isEdit = ref(false)
const formRules = {
customer_id: [{ required: true, message: '请选择客户', trigger: 'change' }],
order_no: [{ required: true, message: '请输入订单号', trigger: 'blur' }],
actual_amount: [{ required: true, message: '请输入实付金额', trigger: 'blur' }]
}
const statusMap = { 0: '待付款', 1: '已付款', 2: '已完成', 3: '已取消' }
const statusType = { 0: 'info', 1: 'warning', 2: 'success', 3: 'danger' }
const typeMap = { 1: '单项服务', 2: '套餐', 3: '产康', 4: '其他' }
async function fetchList() {
loading.value = true
try {
const res = await serviceOrderApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} finally { loading.value = false }
}
function handleSearch() { pagination.page = 1; fetchList() }
function handleReset() { Object.assign(searchForm, { order_no: '', status: '' }); handleSearch() }
function handleAdd() { isEdit.value = false; Object.assign(form, { id: null, customer_id: '', order_no: '', type: '', total_amount: 0, discount_amount: 0, actual_amount: 0, remark: '' }); dialogVisible.value = true }
function handleEdit(row) { isEdit.value = true; Object.assign(form, { id: row.id, customer_id: row.customer_id, order_no: row.order_no, type: row.type, total_amount: row.total_amount, discount_amount: row.discount_amount, actual_amount: row.actual_amount, remark: row.remark || '' }); dialogVisible.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 serviceOrderApi.update(form.id, form) } else { await serviceOrderApi.create(form) }
ElMessage.success(isEdit.value ? '更新成功' : '创建成功'); dialogVisible.value = false; fetchList()
} finally { submitLoading.value = false }
})
}
async function handleDelete(row) {
await ElMessageBox.confirm(`确定删除订单「${row.order_no}」吗?`, '删除确认', { type: 'warning' })
await serviceOrderApi.delete(row.id); ElMessage.success('删除成功'); fetchList()
}
async function handlePay(row) {
await ElMessageBox.confirm('确认客户已付款?', '付款确认', { type: 'warning' })
await serviceOrderApi.pay(row.id, { pay_method: 1 }); ElMessage.success('付款成功'); fetchList()
}
async function handleComplete(row) {
await ElMessageBox.confirm('确认订单已完成?', '完成确认', { type: 'warning' })
await serviceOrderApi.complete(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())
</script>
<template>
<div class="page-container">
<div class="search-bar">
<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>
<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="客户ID" prop="customer_id"><el-input v-model="form.customer_id" /></el-form-item>
<el-form-item label="订单号" prop="order_no"><el-input v-model="form.order_no" :disabled="isEdit" /></el-form-item>
<el-form-item label="类型"><el-select v-model="form.type" style="width:100%"><el-option v-for="(v,k) in typeMap" :key="k" :label="v" :value="Number(k)" /></el-select></el-form-item>
<el-form-item label="总金额"><el-input-number v-model="form.total_amount" :min="0" :precision="2" style="width:100%" /></el-form-item>
<el-form-item label="优惠金额"><el-input-number v-model="form.discount_amount" :min="0" :precision="2" style="width:100%" /></el-form-item>
<el-form-item label="实付金额" prop="actual_amount"><el-input-number v-model="form.actual_amount" :min="0" :precision="2" style="width:100%" /></el-form-item>
<el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="2" /></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,100 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { servicePackageApi, serviceItemApi } from '@/api/service.js'
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const dialogVisible = ref(false)
const submitLoading = ref(false)
const serviceItems = ref([])
const searchForm = reactive({ name: '', status: '' })
const pagination = reactive({ page: 1, per_page: 15 })
const formRef = ref(null)
const form = reactive({ id: null, name: '', items: [], original_price: 0, price: 0, description: '', status: 1, sort: 0 })
const isEdit = ref(false)
const formRules = {
name: [{ required: true, message: '请输入套餐名称', trigger: 'blur' }],
price: [{ required: true, message: '请输入价格', trigger: 'blur' }]
}
async function fetchList() {
loading.value = true
try {
const res = await servicePackageApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} finally { loading.value = false }
}
async function fetchItems() { try { const res = await serviceItemApi.getList({ per_page: 200, status: 1 }); serviceItems.value = res.data?.list || res.data?.data || [] } catch {} }
function handleSearch() { pagination.page = 1; fetchList() }
function handleReset() { Object.assign(searchForm, { name: '', status: '' }); handleSearch() }
function handleAdd() { isEdit.value = false; Object.assign(form, { id: null, name: '', items: [], original_price: 0, price: 0, description: '', status: 1, sort: 0 }); dialogVisible.value = true }
function handleEdit(row) { isEdit.value = true; Object.assign(form, { id: row.id, name: row.name, items: row.items || [], original_price: row.original_price, price: row.price, description: row.description || '', status: row.status, sort: row.sort }); dialogVisible.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 servicePackageApi.update(form.id, form) } else { await servicePackageApi.create(form) }
ElMessage.success(isEdit.value ? '更新成功' : '创建成功'); dialogVisible.value = false; fetchList()
} finally { submitLoading.value = false }
})
}
async function handleDelete(row) {
await ElMessageBox.confirm(`确定删除套餐「${row.name}」吗?`, '删除确认', { type: 'warning' })
await servicePackageApi.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(); fetchItems() })
</script>
<template>
<div class="page-container">
<div class="search-bar">
<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>
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑套餐':'新增套餐'" width="600px" 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" /></el-form-item>
<el-form-item label="原价"><el-input-number v-model="form.original_price" :min="0" :precision="2" style="width:100%" /></el-form-item>
<el-form-item label="套餐价" prop="price"><el-input-number v-model="form.price" :min="0" :precision="2" style="width:100%" /></el-form-item>
<el-form-item label="描述"><el-input v-model="form.description" type="textarea" :rows="3" /></el-form-item>
<el-form-item label="状态"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" /></el-form-item>
<el-form-item label="排序"><el-input-number v-model="form.sort" :min="0" /></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>