fix: 全链路运行时修复 — 登录/路由/菜单/门店CRUD
登录流程:
- login() 不再设置 userInfo,由 router guard 触发 getInfo() + 路由生成
- validate() 从 callback 改为 Promise 式,修复异步链断裂
- getInfo() 适配 /auth/me 扁平响应结构
动态路由:
- import.meta.glob 替代 Vite 不支持的多级动态 import
- 目录菜单检测: !component || 'Layout' 而非仅 'Layout'
- resolveComponent 支持多种 key 格式 + 后缀匹配兜底
侧边栏:
- menu.title → menu.name (匹配后端字段)
- 空 path 目录节点加 fallback index
门店管理:
- StoreController: region_id 改 nullable, 新增 contact/capacity/description 验证
- has('status') → filled('status') 修复空字符串导致列表为空
- capacity null → 0 兜底,避免 NOT NULL 约束报错
- 前端: capacity 初始值 '' → 1, handleSubmit 过滤冗余字段
- region 列渲染 region.name 而非 [object Object]
- 新增 stores 表迁移: contact/capacity/description 列
种子数据:
- DatabaseSeeder 改用 InitSeeder
- createMenus() 补全 52 个叶子页面菜单 (13 模块)
- 目录菜单添加 path 属性
其他:
- vite proxy 端口 8000 → 8001
- request.js 添加 Accept: application/json
- UserStatus 枚举值对齐 (Active=1, Disabled=0)
- 新增 positions/index.vue 页面
This commit is contained in:
@@ -4,16 +4,7 @@ namespace App\Enums;
|
|||||||
|
|
||||||
enum UserStatus: int
|
enum UserStatus: int
|
||||||
{
|
{
|
||||||
case Pending = 0;
|
|
||||||
case Active = 1;
|
case Active = 1;
|
||||||
case Disabled = 2;
|
case Disabled = 0;
|
||||||
|
case Pending = 2;
|
||||||
public function label(): string
|
|
||||||
{
|
|
||||||
return match ($this) {
|
|
||||||
self::Pending => '待审核',
|
|
||||||
self::Active => '正常',
|
|
||||||
self::Disabled => '禁用',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class StoreController extends Controller
|
|||||||
if ($request->keyword) {
|
if ($request->keyword) {
|
||||||
$query->where('name', 'like', "%{$request->keyword}%");
|
$query->where('name', 'like', "%{$request->keyword}%");
|
||||||
}
|
}
|
||||||
if ($request->has('status')) {
|
if ($request->filled('status')) {
|
||||||
$query->where('status', $request->status);
|
$query->where('status', $request->status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,14 +29,20 @@ class StoreController extends Controller
|
|||||||
public function store(Request $request): JsonResponse
|
public function store(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
'region_id' => 'required|exists:regions,id',
|
'region_id' => 'nullable|exists:regions,id',
|
||||||
'name' => 'required|string|max:100',
|
'name' => 'required|string|max:100',
|
||||||
'code' => 'nullable|string|max:20|unique:stores,code',
|
'code' => 'nullable|string|max:20|unique:stores,code',
|
||||||
'address' => 'nullable|string|max:255',
|
'address' => 'nullable|string|max:255',
|
||||||
'phone' => 'nullable|string|max:20',
|
'phone' => 'nullable|string|max:20',
|
||||||
|
'contact' => 'nullable|string|max:50',
|
||||||
|
'capacity' => 'nullable|integer|min:0|max:9999',
|
||||||
|
'description' => 'nullable|string|max:500',
|
||||||
'status' => 'in:0,1',
|
'status' => 'in:0,1',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// capacity 列有 NOT NULL + default(0),空值时用默认值
|
||||||
|
$data['capacity'] = $data['capacity'] ?? 0;
|
||||||
|
|
||||||
$store = Store::create($data);
|
$store = Store::create($data);
|
||||||
return $this->success($store, '创建成功');
|
return $this->success($store, '创建成功');
|
||||||
}
|
}
|
||||||
@@ -50,14 +56,21 @@ class StoreController extends Controller
|
|||||||
public function update(Request $request, Store $store): JsonResponse
|
public function update(Request $request, Store $store): JsonResponse
|
||||||
{
|
{
|
||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
'region_id' => 'exists:regions,id',
|
'region_id' => 'nullable|exists:regions,id',
|
||||||
'name' => 'string|max:100',
|
'name' => 'string|max:100',
|
||||||
'code' => 'nullable|string|max:20|unique:stores,code,' . $store->id,
|
'code' => 'nullable|string|max:20|unique:stores,code,' . $store->id,
|
||||||
'address' => 'nullable|string|max:255',
|
'address' => 'nullable|string|max:255',
|
||||||
'phone' => 'nullable|string|max:20',
|
'phone' => 'nullable|string|max:20',
|
||||||
|
'contact' => 'nullable|string|max:50',
|
||||||
|
'capacity' => 'nullable|integer|min:0|max:9999',
|
||||||
|
'description' => 'nullable|string|max:500',
|
||||||
'status' => 'in:0,1',
|
'status' => 'in:0,1',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (array_key_exists('capacity', $data) && $data['capacity'] === null) {
|
||||||
|
$data['capacity'] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
$store->update($data);
|
$store->update($data);
|
||||||
return $this->success($store, '更新成功');
|
return $this->success($store, '更新成功');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
class Store extends Model
|
class Store extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'region_id', 'name', 'code', 'address', 'phone', 'logo', 'status',
|
'region_id', 'name', 'code', 'address', 'phone', 'logo',
|
||||||
|
'contact', 'capacity', 'description', 'status',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('stores', function (Blueprint $table) {
|
||||||
|
// Make region_id nullable (frontend may not always provide it)
|
||||||
|
$table->foreignId('region_id')->nullable()->change();
|
||||||
|
|
||||||
|
// Add fields that frontend form expects
|
||||||
|
$table->string('contact', 50)->nullable()->after('phone')->comment('联系人');
|
||||||
|
$table->unsignedSmallInteger('capacity')->default(0)->after('contact')->comment('床位数');
|
||||||
|
$table->string('description', 500)->nullable()->after('capacity')->comment('备注');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('stores', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['contact', 'capacity', 'description']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -15,11 +15,6 @@ class DatabaseSeeder extends Seeder
|
|||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
// User::factory(10)->create();
|
$this->call(InitSeeder::class);
|
||||||
|
|
||||||
User::factory()->create([
|
|
||||||
'name' => 'Test User',
|
|
||||||
'email' => 'test@example.com',
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,32 +204,87 @@ class InitSeeder extends Seeder
|
|||||||
{
|
{
|
||||||
$menus = [
|
$menus = [
|
||||||
['name' => '首页', 'path' => '/dashboard', 'component' => 'dashboard/index', 'icon' => 'HomeFilled', 'sort' => 0],
|
['name' => '首页', 'path' => '/dashboard', 'component' => 'dashboard/index', 'icon' => 'HomeFilled', 'sort' => 0],
|
||||||
['name' => 'CRM管理', 'icon' => 'User', 'sort' => 1, 'permission_code' => 'crm', 'children' => [
|
|
||||||
|
['name' => 'CRM管理', 'path' => '/crm', 'icon' => 'User', 'sort' => 1, 'permission_code' => 'crm', 'children' => [
|
||||||
|
['name' => '渠道管理', 'path' => '/crm/channels', 'component' => 'crm/channels/index', 'permission_code' => 'crm'],
|
||||||
['name' => '线索管理', 'path' => '/crm/leads', 'component' => 'crm/leads/index', 'permission_code' => 'crm.lead'],
|
['name' => '线索管理', 'path' => '/crm/leads', 'component' => 'crm/leads/index', 'permission_code' => 'crm.lead'],
|
||||||
['name' => '客户管理', 'path' => '/crm/customers', 'component' => 'crm/customers/index', 'permission_code' => 'crm.customer'],
|
['name' => '客户管理', 'path' => '/crm/customers', 'component' => 'crm/customers/index', 'permission_code' => 'crm.customer'],
|
||||||
['name' => '合同管理', 'path' => '/crm/contracts', 'component' => 'crm/contracts/index', 'permission_code' => 'crm.contract'],
|
['name' => '合同管理', 'path' => '/crm/contracts', 'component' => 'crm/contracts/index', 'permission_code' => 'crm.contract'],
|
||||||
|
['name' => '投诉管理', 'path' => '/crm/complaints', 'component' => 'crm/complaints/index', 'permission_code' => 'crm'],
|
||||||
]],
|
]],
|
||||||
['name' => '房务管理', 'icon' => 'House', 'sort' => 2, 'permission_code' => 'room', 'children' => [
|
|
||||||
['name' => '房态看板', 'path' => '/room/board', 'component' => 'room/board/index', 'permission_code' => 'room.room'],
|
['name' => '房务管理', 'path' => '/room', 'icon' => 'House', 'sort' => 2, 'permission_code' => 'room', 'children' => [
|
||||||
|
['name' => '房型管理', 'path' => '/room/room-types', 'component' => 'room/room-types/index', 'permission_code' => 'room.room'],
|
||||||
|
['name' => '房间管理', 'path' => '/room/rooms', 'component' => 'room/rooms/index', 'permission_code' => 'room.room'],
|
||||||
['name' => '预定管理', 'path' => '/room/reservations', 'component' => 'room/reservations/index', 'permission_code' => 'room.reservation'],
|
['name' => '预定管理', 'path' => '/room/reservations', 'component' => 'room/reservations/index', 'permission_code' => 'room.reservation'],
|
||||||
]],
|
]],
|
||||||
['name' => '护理管理', 'icon' => 'FirstAidKit', 'sort' => 3, 'permission_code' => 'care', 'children' => [
|
|
||||||
|
['name' => '护理管理', 'path' => '/care', 'icon' => 'FirstAidKit', 'sort' => 3, 'permission_code' => 'care', 'children' => [
|
||||||
['name' => '护理档案', 'path' => '/care/profiles', 'component' => 'care/profiles/index', 'permission_code' => 'care.profile'],
|
['name' => '护理档案', 'path' => '/care/profiles', 'component' => 'care/profiles/index', 'permission_code' => 'care.profile'],
|
||||||
|
['name' => '护理计划', 'path' => '/care/plans', 'component' => 'care/plans/index', 'permission_code' => 'care.profile'],
|
||||||
['name' => '护理记录', 'path' => '/care/records', 'component' => 'care/records/index', 'permission_code' => 'care.record'],
|
['name' => '护理记录', 'path' => '/care/records', 'component' => 'care/records/index', 'permission_code' => 'care.record'],
|
||||||
]],
|
]],
|
||||||
['name' => '月子餐', 'icon' => 'Bowl', 'sort' => 4, 'permission_code' => 'meal', 'children' => [
|
|
||||||
|
['name' => '月子餐', 'path' => '/meal', 'icon' => 'Bowl', 'sort' => 4, 'permission_code' => 'meal', 'children' => [
|
||||||
['name' => '菜品管理', 'path' => '/meal/dishes', 'component' => 'meal/dishes/index', 'permission_code' => 'meal.dish'],
|
['name' => '菜品管理', 'path' => '/meal/dishes', 'component' => 'meal/dishes/index', 'permission_code' => 'meal.dish'],
|
||||||
['name' => '排餐管理', 'path' => '/meal/plans', 'component' => 'meal/plans/index', 'permission_code' => 'meal.plan'],
|
['name' => '每日排餐', 'path' => '/meal/daily-plans', 'component' => 'meal/daily-plans/index', 'permission_code' => 'meal.plan'],
|
||||||
]],
|
]],
|
||||||
['name' => '服务产康', 'icon' => 'Promotion', 'sort' => 5, 'permission_code' => 'service'],
|
|
||||||
['name' => '月嫂管理', 'icon' => 'Avatar', 'sort' => 6, 'permission_code' => 'nanny'],
|
['name' => '服务产康', 'path' => '/service', 'icon' => 'Promotion', 'sort' => 5, 'permission_code' => 'service', 'children' => [
|
||||||
['name' => '进销存', 'icon' => 'Box', 'sort' => 7, 'permission_code' => 'stock'],
|
['name' => '服务项目', 'path' => '/service/items', 'component' => 'service/items/index', 'permission_code' => 'service.item'],
|
||||||
['name' => '财务管理', 'icon' => 'Money', 'sort' => 8, 'permission_code' => 'finance'],
|
['name' => '服务套餐', 'path' => '/service/packages', 'component' => 'service/packages/index', 'permission_code' => 'service.item'],
|
||||||
['name' => '人事薪资', 'icon' => 'Stamp', 'sort' => 9, 'permission_code' => 'hr'],
|
['name' => '服务订单', 'path' => '/service/orders', 'component' => 'service/orders/index', 'permission_code' => 'service.item'],
|
||||||
['name' => '办公协同', 'icon' => 'ChatDotRound', 'sort' => 10, 'permission_code' => 'office'],
|
]],
|
||||||
['name' => '统计报表', 'icon' => 'DataAnalysis', 'sort' => 11, 'permission_code' => 'report'],
|
|
||||||
['name' => '知识库', 'icon' => 'Reading', 'sort' => 12, 'permission_code' => 'kb'],
|
['name' => '月嫂管理', 'path' => '/nanny', 'icon' => 'Avatar', 'sort' => 6, 'permission_code' => 'nanny', 'children' => [
|
||||||
['name' => '系统设置', 'icon' => 'Setting', 'sort' => 99, 'permission_code' => 'system', 'children' => [
|
['name' => '月嫂信息', 'path' => '/nanny/nannies', 'component' => 'nanny/nannies/index', 'permission_code' => 'nanny.profile'],
|
||||||
|
['name' => '月嫂订单', 'path' => '/nanny/orders', 'component' => 'nanny/orders/index', 'permission_code' => 'nanny.profile'],
|
||||||
|
['name' => '月嫂排班', 'path' => '/nanny/schedules', 'component' => 'nanny/schedules/index', 'permission_code' => 'nanny.profile'],
|
||||||
|
]],
|
||||||
|
|
||||||
|
['name' => '进销存', 'path' => '/inventory', 'icon' => 'Box', 'sort' => 7, 'permission_code' => 'stock', 'children' => [
|
||||||
|
['name' => '仓库管理', 'path' => '/inventory/warehouses', 'component' => 'inventory/warehouses/index', 'permission_code' => 'stock.inventory'],
|
||||||
|
['name' => '供应商管理', 'path' => '/inventory/suppliers', 'component' => 'inventory/suppliers/index', 'permission_code' => 'stock.inventory'],
|
||||||
|
['name' => '物料管理', 'path' => '/inventory/materials', 'component' => 'inventory/materials/index', 'permission_code' => 'stock.inventory'],
|
||||||
|
['name' => '采购订单', 'path' => '/inventory/purchase-orders', 'component' => 'inventory/purchase-orders/index', 'permission_code' => 'stock.inventory'],
|
||||||
|
['name' => '出入库流水', 'path' => '/inventory/stock-movements', 'component' => 'inventory/stock-movements/index', 'permission_code' => 'stock.inventory'],
|
||||||
|
]],
|
||||||
|
|
||||||
|
['name' => '财务管理', 'path' => '/finance', 'icon' => 'Money', 'sort' => 8, 'permission_code' => 'finance', 'children' => [
|
||||||
|
['name' => '收支分类', 'path' => '/finance/categories', 'component' => 'finance/categories/index', 'permission_code' => 'finance.record'],
|
||||||
|
['name' => '收支记录', 'path' => '/finance/records', 'component' => 'finance/records/index', 'permission_code' => 'finance.record'],
|
||||||
|
['name' => '客户账户', 'path' => '/finance/accounts', 'component' => 'finance/accounts/index', 'permission_code' => 'finance.record'],
|
||||||
|
['name' => '储值卡', 'path' => '/finance/prepaid-cards', 'component' => 'finance/prepaid-cards/index', 'permission_code' => 'finance.record'],
|
||||||
|
['name' => '发票管理', 'path' => '/finance/invoices', 'component' => 'finance/invoices/index', 'permission_code' => 'finance.record'],
|
||||||
|
]],
|
||||||
|
|
||||||
|
['name' => '人事薪资', 'path' => '/hr', 'icon' => 'Stamp', 'sort' => 9, 'permission_code' => 'hr', 'children' => [
|
||||||
|
['name' => '员工档案', 'path' => '/hr/profiles', 'component' => 'hr/profiles/index', 'permission_code' => 'hr.employee'],
|
||||||
|
['name' => '排班管理', 'path' => '/hr/schedules', 'component' => 'hr/schedules/index', 'permission_code' => 'hr.employee'],
|
||||||
|
['name' => '考勤管理', 'path' => '/hr/attendances', 'component' => 'hr/attendances/index', 'permission_code' => 'hr.employee'],
|
||||||
|
['name' => '请假管理', 'path' => '/hr/leaves', 'component' => 'hr/leaves/index', 'permission_code' => 'hr.employee'],
|
||||||
|
['name' => '工资管理', 'path' => '/hr/salaries', 'component' => 'hr/salaries/index', 'permission_code' => 'hr.employee'],
|
||||||
|
]],
|
||||||
|
|
||||||
|
['name' => '办公协同', 'path' => '/office', 'icon' => 'ChatDotRound', 'sort' => 10, 'permission_code' => 'office', 'children' => [
|
||||||
|
['name' => '审批模板', 'path' => '/office/approval-templates', 'component' => 'office/approval-templates/index', 'permission_code' => 'office.approval'],
|
||||||
|
['name' => '审批管理', 'path' => '/office/approvals', 'component' => 'office/approvals/index', 'permission_code' => 'office.approval'],
|
||||||
|
['name' => '公告管理', 'path' => '/office/announcements', 'component' => 'office/announcements/index', 'permission_code' => 'office.approval'],
|
||||||
|
['name' => '交接班', 'path' => '/office/handovers', 'component' => 'office/handovers/index', 'permission_code' => 'office.approval'],
|
||||||
|
['name' => '消息通知', 'path' => '/office/notifications', 'component' => 'office/notifications/index', 'permission_code' => 'office.approval'],
|
||||||
|
]],
|
||||||
|
|
||||||
|
['name' => '统计报表', 'path' => '/report', 'icon' => 'DataAnalysis', 'sort' => 11, 'permission_code' => 'report', 'children' => [
|
||||||
|
['name' => '数据概览', 'path' => '/report/overview', 'component' => 'report/overview/index', 'permission_code' => 'report.view'],
|
||||||
|
['name' => '导出记录', 'path' => '/report/exports', 'component' => 'report/exports/index', 'permission_code' => 'report.export'],
|
||||||
|
]],
|
||||||
|
|
||||||
|
['name' => '知识库', 'path' => '/kb', 'icon' => 'Reading', 'sort' => 12, 'permission_code' => 'kb', 'children' => [
|
||||||
|
['name' => '知识分类', 'path' => '/kb/categories', 'component' => 'kb/categories/index', 'permission_code' => 'kb.article'],
|
||||||
|
['name' => '知识文章', 'path' => '/kb/articles', 'component' => 'kb/articles/index', 'permission_code' => 'kb.article'],
|
||||||
|
]],
|
||||||
|
|
||||||
|
['name' => '系统设置', 'path' => '/system', 'icon' => 'Setting', 'sort' => 99, 'permission_code' => 'system', 'children' => [
|
||||||
['name' => '门店管理', 'path' => '/system/stores', 'component' => 'system/stores/index', 'permission_code' => 'system.store'],
|
['name' => '门店管理', 'path' => '/system/stores', 'component' => 'system/stores/index', 'permission_code' => 'system.store'],
|
||||||
['name' => '部门管理', 'path' => '/system/departments', 'component' => 'system/departments/index', 'permission_code' => 'system.store'],
|
['name' => '部门管理', 'path' => '/system/departments', 'component' => 'system/departments/index', 'permission_code' => 'system.store'],
|
||||||
['name' => '职务管理', 'path' => '/system/positions', 'component' => 'system/positions/index', 'permission_code' => 'system.store'],
|
['name' => '职务管理', 'path' => '/system/positions', 'component' => 'system/positions/index', 'permission_code' => 'system.store'],
|
||||||
|
|||||||
@@ -53,13 +53,13 @@ function resolveIcon(iconName) {
|
|||||||
<!-- Has children → sub-menu -->
|
<!-- Has children → sub-menu -->
|
||||||
<el-sub-menu
|
<el-sub-menu
|
||||||
v-if="menu.children && menu.children.length > 0 && menu.visible !== 0"
|
v-if="menu.children && menu.children.length > 0 && menu.visible !== 0"
|
||||||
:index="menu.path"
|
:index="menu.path || `menu-${menu.id}`"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<component :is="resolveIcon(menu.icon)" />
|
<component :is="resolveIcon(menu.icon)" />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>{{ menu.title }}</span>
|
<span>{{ menu.name }}</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-for="child in menu.children" :key="child.id || child.path">
|
<template v-for="child in menu.children" :key="child.id || child.path">
|
||||||
<el-sub-menu
|
<el-sub-menu
|
||||||
@@ -70,7 +70,7 @@ function resolveIcon(iconName) {
|
|||||||
<el-icon>
|
<el-icon>
|
||||||
<component :is="resolveIcon(child.icon)" />
|
<component :is="resolveIcon(child.icon)" />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>{{ child.title }}</span>
|
<span>{{ child.name }}</span>
|
||||||
</template>
|
</template>
|
||||||
<el-menu-item
|
<el-menu-item
|
||||||
v-for="grandchild in child.children"
|
v-for="grandchild in child.children"
|
||||||
@@ -80,7 +80,7 @@ function resolveIcon(iconName) {
|
|||||||
<el-icon>
|
<el-icon>
|
||||||
<component :is="resolveIcon(grandchild.icon)" />
|
<component :is="resolveIcon(grandchild.icon)" />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<template #title>{{ grandchild.title }}</template>
|
<template #title>{{ grandchild.name }}</template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ function resolveIcon(iconName) {
|
|||||||
<el-icon>
|
<el-icon>
|
||||||
<component :is="resolveIcon(child.icon)" />
|
<component :is="resolveIcon(child.icon)" />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<template #title>{{ child.title }}</template>
|
<template #title>{{ child.name }}</template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</template>
|
</template>
|
||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
@@ -104,7 +104,7 @@ function resolveIcon(iconName) {
|
|||||||
<el-icon>
|
<el-icon>
|
||||||
<component :is="resolveIcon(menu.icon)" />
|
<component :is="resolveIcon(menu.icon)" />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<template #title>{{ menu.title }}</template>
|
<template #title>{{ menu.name }}</template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</template>
|
</template>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
|
|||||||
@@ -5,6 +5,28 @@ import { constantRoutes } from '@/router/routes.js'
|
|||||||
// Layout wrapper for authenticated pages
|
// Layout wrapper for authenticated pages
|
||||||
const Layout = () => import('@/layout/index.vue')
|
const Layout = () => import('@/layout/index.vue')
|
||||||
|
|
||||||
|
// Pre-scan all view components — Vite cannot resolve multi-level dynamic imports
|
||||||
|
const viewModules = import.meta.glob('@/views/**/*.vue')
|
||||||
|
|
||||||
|
function resolveComponent(comp) {
|
||||||
|
// Try multiple key formats that import.meta.glob may produce
|
||||||
|
const keys = [
|
||||||
|
`/src/views/${comp}.vue`,
|
||||||
|
`@/views/${comp}.vue`,
|
||||||
|
`../views/${comp}.vue`
|
||||||
|
]
|
||||||
|
for (const key of keys) {
|
||||||
|
if (viewModules[key]) return viewModules[key]
|
||||||
|
}
|
||||||
|
// Fallback: search by suffix match
|
||||||
|
const suffix = `/views/${comp}.vue`
|
||||||
|
for (const [key, loader] of Object.entries(viewModules)) {
|
||||||
|
if (key.endsWith(suffix)) return loader
|
||||||
|
}
|
||||||
|
console.warn(`[permission] View not found: ${comp}`)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a single server menu node to a vue-router route object.
|
* Convert a single server menu node to a vue-router route object.
|
||||||
* Recursive — handles children at any depth.
|
* Recursive — handles children at any depth.
|
||||||
@@ -14,7 +36,7 @@ function menuToRoute(menu) {
|
|||||||
path: menu.path,
|
path: menu.path,
|
||||||
name: menu.name || menu.path,
|
name: menu.name || menu.path,
|
||||||
meta: {
|
meta: {
|
||||||
title: menu.title,
|
title: menu.name,
|
||||||
icon: menu.icon || '',
|
icon: menu.icon || '',
|
||||||
id: menu.id,
|
id: menu.id,
|
||||||
hidden: menu.visible === 0
|
hidden: menu.visible === 0
|
||||||
@@ -24,9 +46,10 @@ function menuToRoute(menu) {
|
|||||||
if (menu.component === 'Layout') {
|
if (menu.component === 'Layout') {
|
||||||
route.component = Layout
|
route.component = Layout
|
||||||
} else if (menu.component) {
|
} else if (menu.component) {
|
||||||
// Dynamic import from views directory
|
const loader = resolveComponent(menu.component)
|
||||||
const comp = menu.component
|
if (loader) {
|
||||||
route.component = () => import(`@/views/${comp}.vue`)
|
route.component = loader
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (menu.children && menu.children.length > 0) {
|
if (menu.children && menu.children.length > 0) {
|
||||||
@@ -62,14 +85,16 @@ export const usePermissionStore = defineStore('permission', () => {
|
|||||||
|
|
||||||
// Flatten top-level menu items into Layout's children
|
// Flatten top-level menu items into Layout's children
|
||||||
for (const menu of menus) {
|
for (const menu of menus) {
|
||||||
if (menu.component === 'Layout' && menu.children) {
|
if (menu.children && menu.children.length > 0 && (!menu.component || menu.component === 'Layout')) {
|
||||||
// This menu node IS the layout wrapper — add its children directly
|
// Directory/group node — add its children directly under Layout
|
||||||
for (const child of menu.children) {
|
for (const child of menu.children) {
|
||||||
layoutRoute.children.push(menuToRoute(child))
|
layoutRoute.children.push(menuToRoute(child))
|
||||||
}
|
}
|
||||||
} else {
|
} else if (menu.component) {
|
||||||
|
// Leaf menu with a component — add as direct child
|
||||||
layoutRoute.children.push(menuToRoute(menu))
|
layoutRoute.children.push(menuToRoute(menu))
|
||||||
}
|
}
|
||||||
|
// Skip menus without component and without children (empty directories)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always ensure dashboard is accessible
|
// Always ensure dashboard is accessible
|
||||||
@@ -80,7 +105,7 @@ export const usePermissionStore = defineStore('permission', () => {
|
|||||||
layoutRoute.children.unshift({
|
layoutRoute.children.unshift({
|
||||||
path: 'dashboard',
|
path: 'dashboard',
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
component: () => import('@/views/dashboard/index.vue'),
|
component: resolveComponent('dashboard/index'),
|
||||||
meta: { title: '工作台', icon: 'Odometer' }
|
meta: { title: '工作台', icon: 'Odometer' }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,19 +15,22 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
// Actions
|
// Actions
|
||||||
async function login(credentials) {
|
async function login(credentials) {
|
||||||
const res = await loginApi(credentials)
|
const res = await loginApi(credentials)
|
||||||
const { token: newToken, user } = res.data
|
const { token: newToken } = res.data
|
||||||
token.value = newToken
|
token.value = newToken
|
||||||
userInfo.value = user
|
// Only store token here — userInfo, permissions, menus
|
||||||
|
// will be loaded by router guard via getInfo() which also
|
||||||
|
// triggers dynamic route generation.
|
||||||
localStorage.setItem('token', newToken)
|
localStorage.setItem('token', newToken)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getInfo() {
|
async function getInfo() {
|
||||||
const res = await getMe()
|
const res = await getMe()
|
||||||
const { user, permissions: perms, menus: menuList } = res.data
|
const data = res.data
|
||||||
userInfo.value = user
|
// /auth/me returns flat structure: { id, username, ..., permissions, menus }
|
||||||
permissions.value = perms || []
|
userInfo.value = data
|
||||||
menus.value = menuList || []
|
permissions.value = data.permissions || []
|
||||||
|
menus.value = data.menus || []
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ const request = axios.create({
|
|||||||
baseURL: '/api/v1',
|
baseURL: '/api/v1',
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -29,20 +29,22 @@ const rules = {
|
|||||||
|
|
||||||
async function handleLogin() {
|
async function handleLogin() {
|
||||||
if (!formRef.value) return
|
if (!formRef.value) return
|
||||||
await formRef.value.validate(async (valid) => {
|
try {
|
||||||
if (!valid) return
|
await formRef.value.validate()
|
||||||
loading.value = true
|
} catch {
|
||||||
try {
|
return // validation failed
|
||||||
await userStore.login({ username: form.username, password: form.password })
|
}
|
||||||
const redirect = route.query.redirect || '/'
|
loading.value = true
|
||||||
await router.push(redirect)
|
try {
|
||||||
ElMessage.success('登录成功,欢迎回来!')
|
await userStore.login({ username: form.username, password: form.password })
|
||||||
} catch {
|
const redirect = route.query.redirect || '/'
|
||||||
// Error already handled by request interceptor
|
await router.push(redirect)
|
||||||
} finally {
|
ElMessage.success('登录成功,欢迎回来!')
|
||||||
loading.value = false
|
} catch {
|
||||||
}
|
// Error already handled by request interceptor
|
||||||
})
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeydown(e) {
|
function handleKeydown(e) {
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { positionApi } from '@/api/system.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({ keyword: '', status: '' })
|
||||||
|
const pagination = reactive({ page: 1, page_size: 20 })
|
||||||
|
|
||||||
|
// ─── Form ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
id: null,
|
||||||
|
name: '',
|
||||||
|
code: '',
|
||||||
|
sort: 0,
|
||||||
|
status: 1,
|
||||||
|
description: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRules = {
|
||||||
|
name: [{ required: true, message: '请输入职务名称', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Methods ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function fetchList() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await positionApi.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, { keyword: '', status: '' })
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAdd() {
|
||||||
|
isEdit.value = false
|
||||||
|
dialogTitle.value = '新增职务'
|
||||||
|
Object.assign(form, {
|
||||||
|
id: null, name: '', code: '', sort: 0, status: 1, description: ''
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleEdit(row) {
|
||||||
|
isEdit.value = true
|
||||||
|
dialogTitle.value = `编辑职务 - ${row.name}`
|
||||||
|
const res = await positionApi.getDetail(row.id)
|
||||||
|
const position = res.data
|
||||||
|
Object.assign(form, {
|
||||||
|
id: position.id,
|
||||||
|
name: position.name,
|
||||||
|
code: position.code || '',
|
||||||
|
sort: position.sort ?? 0,
|
||||||
|
status: position.status,
|
||||||
|
description: position.description || ''
|
||||||
|
})
|
||||||
|
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 positionApi.update(form.id, form)
|
||||||
|
ElMessage.success('职务信息更新成功')
|
||||||
|
} else {
|
||||||
|
await positionApi.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 positionApi.delete(row.id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchList()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStatusToggle(row) {
|
||||||
|
const newStatus = row.status === 1 ? 0 : 1
|
||||||
|
const label = newStatus === 1 ? '启用' : '停用'
|
||||||
|
try {
|
||||||
|
await positionApi.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.keyword"
|
||||||
|
placeholder="搜索职务名称/编码"
|
||||||
|
style="width: 240px"
|
||||||
|
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 type="index" label="#" width="60" align="center" />
|
||||||
|
<el-table-column prop="name" label="职务名称" min-width="160" />
|
||||||
|
<el-table-column prop="code" label="职务编码" width="140" />
|
||||||
|
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||||
|
<el-table-column label="状态" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||||
|
{{ row.status === 1 ? '启用' : '停用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||||
|
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
:type="row.status === 1 ? 'warning' : 'success'"
|
||||||
|
link
|
||||||
|
@click="handleStatusToggle(row)"
|
||||||
|
>
|
||||||
|
{{ row.status === 1 ? '停用' : '启用' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.page_size"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
background
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add/Edit Dialog -->
|
||||||
|
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="480px" 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="code">
|
||||||
|
<el-input v-model="form.code" placeholder="如: MGR(选填)" />
|
||||||
|
</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-col :span="12">
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-radio-group v-model="form.status">
|
||||||
|
<el-radio :value="1">启用</el-radio>
|
||||||
|
<el-radio :value="0">停用</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="备注" prop="description">
|
||||||
|
<el-input
|
||||||
|
v-model="form.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
placeholder="备注信息(选填)"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
@@ -27,7 +27,7 @@ const form = reactive({
|
|||||||
address: '',
|
address: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
contact: '',
|
contact: '',
|
||||||
capacity: '',
|
capacity: 1,
|
||||||
description: '',
|
description: '',
|
||||||
status: 1
|
status: 1
|
||||||
})
|
})
|
||||||
@@ -73,7 +73,7 @@ function handleAdd() {
|
|||||||
dialogTitle.value = '新增门店'
|
dialogTitle.value = '新增门店'
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
id: null, name: '', code: '', region: '', address: '',
|
id: null, name: '', code: '', region: '', address: '',
|
||||||
phone: '', contact: '', capacity: '', description: '', status: 1
|
phone: '', contact: '', capacity: 1, description: '', status: 1
|
||||||
})
|
})
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
@@ -100,25 +100,29 @@ async function handleEdit(row) {
|
|||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
if (!formRef.value) return
|
if (!formRef.value) return
|
||||||
await formRef.value.validate(async (valid) => {
|
try {
|
||||||
if (!valid) return
|
await formRef.value.validate()
|
||||||
submitLoading.value = true
|
} catch {
|
||||||
try {
|
return
|
||||||
if (isEdit.value) {
|
}
|
||||||
await storeApi.update(form.id, form)
|
submitLoading.value = true
|
||||||
ElMessage.success('门店信息更新成功')
|
try {
|
||||||
} else {
|
// 只发送后端接受的字段,过滤 id / region
|
||||||
await storeApi.create(form)
|
const { id, region, ...payload } = form
|
||||||
ElMessage.success('门店创建成功')
|
if (isEdit.value) {
|
||||||
}
|
await storeApi.update(id, payload)
|
||||||
dialogVisible.value = false
|
ElMessage.success('门店信息更新成功')
|
||||||
fetchList()
|
} else {
|
||||||
} catch {
|
await storeApi.create(payload)
|
||||||
//
|
ElMessage.success('门店创建成功')
|
||||||
} finally {
|
|
||||||
submitLoading.value = false
|
|
||||||
}
|
}
|
||||||
})
|
dialogVisible.value = false
|
||||||
|
fetchList()
|
||||||
|
} catch {
|
||||||
|
//
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDelete(row) {
|
async function handleDelete(row) {
|
||||||
@@ -193,7 +197,9 @@ onMounted(fetchList)
|
|||||||
<el-table v-loading="loading" :data="tableData" border stripe style="width: 100%">
|
<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="name" label="门店名称" min-width="160" />
|
||||||
<el-table-column prop="code" label="门店编码" width="120" />
|
<el-table-column prop="code" label="门店编码" width="120" />
|
||||||
<el-table-column prop="region" label="所在地区" min-width="130" />
|
<el-table-column label="所在地区" min-width="130">
|
||||||
|
<template #default="{ row }">{{ row.region?.name || '' }}</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="address" label="详细地址" min-width="200" show-overflow-tooltip />
|
<el-table-column prop="address" label="详细地址" min-width="200" show-overflow-tooltip />
|
||||||
<el-table-column prop="phone" label="联系电话" width="140" />
|
<el-table-column prop="phone" label="联系电话" width="140" />
|
||||||
<el-table-column prop="contact" label="联系人" width="100" />
|
<el-table-column prop="contact" label="联系人" width="100" />
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:8000',
|
target: 'http://localhost:8001',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false
|
secure: false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user