From 31440b29045412a9db357c09d38e7413f1fd9533 Mon Sep 17 00:00:00 2001 From: sexygoat <1538832180@qq.com> Date: Sat, 31 Jan 2026 11:18:37 +0800 Subject: [PATCH] =?UTF-8?q?fetch(modify):=E4=BF=AE=E6=94=B9=E5=8E=9F?= =?UTF-8?q?=E6=9D=A5=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 5 +- .../changes/add-device-management/design.md | 33 +- .../changes/add-device-management/proposal.md | 7 + .../specs/device-management/spec.md | 120 +-- .../proposal.md | 5 + .../enterprise-device-authorization/spec.md | 153 ++-- .../tasks.md | 11 + .../changes/add-order-management/proposal.md | 2 + .../add-package-management-system/design.md | 73 +- .../add-package-management-system/proposal.md | 16 + .../add-package-management-system/tasks.md | 13 + .../design.md | 27 +- .../proposal.md | 3 + .../specs/package-series-allocation/spec.md | 6 +- src/App.vue | 3 +- src/api/modules/authorization.ts | 5 +- src/api/modules/device.ts | 18 +- src/api/modules/enterprise.ts | 8 +- src/api/modules/packageManage.ts | 1 - src/api/modules/packageSeries.ts | 5 +- src/api/modules/shopPackageAllocation.ts | 9 +- src/api/modules/shopSeriesAllocation.ts | 5 +- src/api/modules/storage.ts | 4 +- src/assets/styles/app.scss | 10 +- src/assets/styles/el-ui.scss | 34 +- src/assets/styles/reset.scss | 10 +- src/composables/usePermission.ts | Bin 0 -> 1428 bytes src/config/constants/package.ts | 5 +- src/directives/permission.ts | 40 +- src/locales/langs/zh.json | 8 - src/router/routes/asyncRoutes.ts | 79 +- src/router/routesAlias.ts | 10 +- src/store/modules/user.ts | 38 + src/types/auto-imports.d.ts | 607 +++++++-------- .../account-management/account/index.vue | 47 +- .../customer-account/index.vue | 3 +- .../enterprise-cards/index.vue | 699 ++++++++++++------ .../enterprise-customer/index.vue | 24 +- .../platform-account/index.vue | 10 +- .../account-management/shop-account/index.vue | 9 +- .../allocation-record-detail/index.vue | 43 +- .../asset-management/asset-assign/index.vue | 6 +- .../authorization-detail/index.vue | 6 +- .../authorization-records/index.vue | 6 +- .../asset-management/device-detail/index.vue | 8 +- .../asset-management/device-list/index.vue | 151 +++- .../asset-management/device-search/index.vue | 36 +- .../asset-management/device-task/index.vue | 436 ++++++++++- .../enterprise-devices/index.vue | 8 +- .../iot-card-management/index.vue | 339 +++++++-- .../asset-management/iot-card-query/index.vue | 28 +- .../asset-management/iot-card-task/index.vue | 410 +++++++++- .../asset-management/task-detail/index.vue | 38 +- src/views/batch/device-import/index.vue | 567 ++++++++------ .../commission/agent-commission/index.vue | 7 +- .../order-management/order-list/index.vue | 34 +- .../package-assign/index.vue | 35 +- .../package-management/package-list/index.vue | 31 +- .../series-assign/index.vue | 42 +- src/views/product/shop/index.vue | 3 +- src/views/system/permission/index.vue | 7 +- src/views/system/role/index.vue | 40 +- 62 files changed, 3025 insertions(+), 1421 deletions(-) create mode 100644 src/composables/usePermission.ts diff --git a/index.html b/index.html index 4803c3a..f69fbc6 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,10 @@ - + diff --git a/openspec/changes/add-device-management/design.md b/openspec/changes/add-device-management/design.md index 8248aff..bd15a29 100644 --- a/openspec/changes/add-device-management/design.md +++ b/openspec/changes/add-device-management/design.md @@ -5,6 +5,7 @@ The system currently has a basic device-import page but lacks comprehensive device management capabilities. Devices are critical assets that need to be tracked throughout their lifecycle - from import, allocation to shops, binding with SIM cards, to eventual recall. This change adds full device management to complement the existing card management features. Key integration points: + - **Existing Card System**: Devices must bind with cards from the existing card-list module - **Shop System**: Devices are allocated to shops using the existing ShopService - **Object Storage**: Imports use the existing StorageService for large file uploads @@ -25,16 +26,19 @@ Key integration points: **Choice**: Use StorageService.getUploadUrl → uploadFile → DeviceService.importDevices **Rationale**: + - Handles large files (thousands of devices) without timeout issues - Decouples upload from processing (backend can process asynchronously) - Consistent with modern cloud architecture patterns - Allows progress tracking through task management **Alternatives Considered**: + - Direct multipart upload to backend (rejected: not scalable for large files) - Two-step process without pre-signed URL (rejected: less secure, more backend load) **Implementation Notes**: + - Frontend only handles upload to object storage, not file parsing - Backend processes the file asynchronously and creates task records - Task management provides visibility into import progress @@ -44,16 +48,19 @@ Key integration points: **Choice**: Store binding with explicit slot_position (1-4) in device_cards table **Rationale**: + - IoT devices have physical SIM slots that need explicit identification - Each device can have 1-4 slots (max_sim_slots) - One card can only bind to one device slot (enforced by backend) - Slot position is critical for physical device configuration **Alternatives Considered**: + - Auto-assign slot positions (rejected: operators need to know physical slot numbers) - Allow one card to bind to multiple devices (rejected: not realistic for physical SIMs) **Implementation Notes**: + - Device detail page shows a 4-slot grid (empty slots show "Bind Card" button) - Binding dialog requires explicit slot selection - Unbinding updates bound_card_count and frees the slot @@ -63,16 +70,19 @@ Key integration points: **Choice**: Extend existing task-management to support device import tasks **Rationale**: + - Reuses existing task infrastructure - Provides consistent UX for all import operations - Avoids duplicate task tracking logic - Allows unified search/filter across task types **Alternatives Considered**: + - Separate device task management page (rejected: creates UX fragmentation) - Embed task tracking only in device-import page (rejected: limited visibility) **Implementation Notes**: + - Add task_type field to distinguish ICCID vs Device imports - Task detail page renders different content based on task_type - Device tasks show device_no and bound ICCIDs instead of just ICCID @@ -82,35 +92,41 @@ Key integration points: **Choice**: Show detailed results in dialog after batch allocation/recall **Rationale**: + - Operations may partially succeed (some devices succeed, others fail) - Operators need to know exactly which devices failed and why - Allows retry of failed operations without re-selecting all devices **Alternatives Considered**: + - Just show toast notification (rejected: insufficient detail for partial failures) - Navigate to separate results page (rejected: disrupts workflow) **Implementation Notes**: + - Dialog shows summary: total, success count, failure count - Expandable table shows failed devices with error messages - Success closes dialog, partial failure keeps it open for review ### Decision 5: Component Reuse Strategy -**Choice**: Use existing Art* components (ArtTableFullScreen, ArtSearchBar, ArtTable, ArtButtonTable) +**Choice**: Use existing Art\* components (ArtTableFullScreen, ArtSearchBar, ArtTable, ArtButtonTable) **Rationale**: + - Maintains UI consistency across the application - Reduces development time - Leverages tested, stable components - Easier for users familiar with other pages **Reference Implementation**: + - Device-list follows role/index.vue pattern - Device-detail follows card-list detail modal pattern - Search form follows enterprise-customer search pattern **Implementation Notes**: + - Use CommonStatus for status values (ENABLED/DISABLED) - Use ElSwitch for status toggles - Use ArtButtonTable for action buttons @@ -119,6 +135,7 @@ Key integration points: ## Architecture Diagrams ### Device Import Flow + ``` ┌─────────┐ 1. Select CSV ┌──────────────┐ │ Admin │ ──────────────────> │ device-import│ @@ -155,6 +172,7 @@ Key integration points: ``` ### Device-Card Binding + ``` ┌─────────┐ ┌──────────────┐ │ Device │ ───── bound to ────> │ Card │ @@ -175,12 +193,14 @@ Key integration points: ## Data Flow ### Device List Page Load + 1. User navigates to /asset-management/device-list 2. Vue component mounts, calls DeviceService.getDevices(params) 3. Backend returns paginated device list with bound_card_count 4. Table renders with status switches and action buttons ### Batch Allocation Flow + 1. User selects devices, clicks "Batch Allocate" 2. Dialog opens with shop selector 3. User selects shop, adds remarks, confirms @@ -189,6 +209,7 @@ Key integration points: 6. Dialog shows summary and failed device details ### Card Binding Flow + 1. User opens device detail page 2. Clicks "Bind Card" for an empty slot 3. Dialog shows card search and slot selection @@ -202,11 +223,13 @@ Key integration points: ### Updating Existing Device Import Page **Current State** (`src/views/batch/device-import/index.vue`): + - Uses ElUpload with drag-and-drop - Mock data for import records - No real API integration **Migration Steps**: + 1. Replace ElUpload with three-step upload logic - Add getUploadUrl call - Add uploadFile to object storage @@ -216,6 +239,7 @@ Key integration points: 4. Update CSV format instructions **Backward Compatibility**: + - This is a new feature area with no existing production data - No API breaking changes - UI changes are additive (improve existing page) @@ -223,16 +247,19 @@ Key integration points: ### Extending Task Management **Current State**: + - Only handles ICCID import tasks - Single task type rendering **Migration Steps**: + 1. Add task_type filter dropdown (default: show all) 2. Add task_type badge in task list 3. Task detail page: switch rendering based on task_type 4. Add device-specific fields to task detail view **Backward Compatibility**: + - Existing ICCID tasks continue to work unchanged - Filter defaults to showing all types - No database schema changes required (task_type already exists) @@ -240,23 +267,27 @@ Key integration points: ## Testing Strategy ### Unit Testing + - DeviceService API methods with mock responses - Form validation logic - Utility functions (formatters, validators) ### Integration Testing + - Device list search and pagination - Batch allocation with partial failures - Card binding/unbinding workflow - Import task creation and status tracking ### E2E Testing Scenarios + 1. Import devices via CSV → verify task created → check task detail 2. Search devices → select multiple → allocate to shop → verify shop assignment 3. View device detail → bind card to slot 2 → unbind → verify empty slot 4. Batch recall devices → verify shop cleared → check operation history ### Performance Considerations + - Device list pagination (default 20 per page) - Debounced search input (300ms delay) - Batch operation result pagination (if >100 results) diff --git a/openspec/changes/add-device-management/proposal.md b/openspec/changes/add-device-management/proposal.md index 386d6bd..48f1a19 100644 --- a/openspec/changes/add-device-management/proposal.md +++ b/openspec/changes/add-device-management/proposal.md @@ -3,6 +3,7 @@ ## Why 当前系统只有设备导入页面,缺少完整的设备管理能力。需要提供设备的全生命周期管理,包括: + - 查看和搜索设备列表 - 查看设备详情和绑定的卡信息 - 管理设备与卡的绑定关系 @@ -14,6 +15,7 @@ ## What Changes ### 新增功能 + - **设备列表页面**: 支持多条件搜索、分页、列筛选、批量操作(分配、回收、删除) - **设备详情页面**: 展示设备基本信息、绑定的卡列表、操作历史 - **卡绑定管理**: 在设备详情页绑定/解绑卡 @@ -23,10 +25,12 @@ - **导入任务管理**: 任务列表和详情查看 ### API 集成 + - DeviceService: 11个API接口 - 类型定义: Device, DeviceBinding, ImportTask 等 ### UI 组件 + - 遵循现有组件模式 (ArtTableFullScreen, ArtSearchBar等) - 复用 CommonStatus 统一状态变量 - 使用 ElDescriptions、ElTag、ElSwitch 等组件 @@ -34,12 +38,14 @@ ## Impact ### 影响的功能模块 + - **新增**: 资产管理 / 设备列表(主列表页) - **新增**: 资产管理 / 设备详情 - **改进**: 批量操作 / 设备导入(改为对象存储模式) - **新增**: 批量操作 / 导入任务列表(独立页面) ### 影响的代码 + - `src/api/modules/device.ts` (新增) - `src/types/api/device.ts` (新增) - `src/views/asset-management/device-list/index.vue` (新增) @@ -54,6 +60,7 @@ - `src/types/api/index.ts` (导出 Device 类型) ### 依赖关系 + - 依赖现有的 StorageService (对象存储) - 依赖现有的 ShopService (店铺选择) - 设备与卡的关联管理 diff --git a/openspec/changes/add-device-management/specs/device-management/spec.md b/openspec/changes/add-device-management/specs/device-management/spec.md index 817ca28..8323675 100644 --- a/openspec/changes/add-device-management/specs/device-management/spec.md +++ b/openspec/changes/add-device-management/specs/device-management/spec.md @@ -7,140 +7,140 @@ The system SHALL provide a searchable device list with multi-condition filtering, pagination, and batch operations. #### Scenario: Search devices by multiple criteria -**WHEN** an administrator enters search criteria (device number, device name, status, shop, batch number, device type, manufacturer, creation time range) -**THEN** the system shall display only devices matching all specified criteria with pagination support + +**WHEN** an administrator enters search criteria (device number, device name, status, shop, batch number, device type, manufacturer, creation time range) **THEN** the system shall display only devices matching all specified criteria with pagination support #### Scenario: View device list with bound card count -**WHEN** the device list is displayed -**THEN** each device row shall show: ID, device number, device name, device model, device type, manufacturer, max slots, bound card count, status, shop, batch number, activation time, creation time + +**WHEN** the device list is displayed **THEN** each device row shall show: ID, device number, device name, device model, device type, manufacturer, max slots, bound card count, status, shop, batch number, activation time, creation time #### Scenario: Toggle device status -**WHEN** an administrator clicks the status switch for a device -**THEN** the system shall update the device status between ENABLED and DISABLED and refresh the display + +**WHEN** an administrator clicks the status switch for a device **THEN** the system shall update the device status between ENABLED and DISABLED and refresh the display #### Scenario: Delete a device -**WHEN** an administrator clicks the delete button and confirms the deletion -**THEN** the system shall delete the device and refresh the list + +**WHEN** an administrator clicks the delete button and confirms the deletion **THEN** the system shall delete the device and refresh the list #### Scenario: Select multiple devices for batch operations -**WHEN** an administrator checks multiple device rows -**THEN** the system shall enable batch operation buttons (Allocate, Recall) and track selected device IDs + +**WHEN** an administrator checks multiple device rows **THEN** the system shall enable batch operation buttons (Allocate, Recall) and track selected device IDs ### Requirement: Device Detail Viewing The system SHALL display comprehensive device information including basic properties and bound SIM cards. #### Scenario: View device basic information -**WHEN** an administrator navigates to a device detail page -**THEN** the system shall display all device properties in a descriptive layout: device number, name, model, type, manufacturer, max SIM slots, status, shop, batch number, activation time, creation time + +**WHEN** an administrator navigates to a device detail page **THEN** the system shall display all device properties in a descriptive layout: device number, name, model, type, manufacturer, max SIM slots, status, shop, batch number, activation time, creation time #### Scenario: View bound SIM cards with slot positions -**WHEN** viewing device details -**THEN** the system shall display a table of bound cards showing: slot position (1-4), ICCID, phone number, carrier, card status, and binding time + +**WHEN** viewing device details **THEN** the system shall display a table of bound cards showing: slot position (1-4), ICCID, phone number, carrier, card status, and binding time #### Scenario: Empty slots display -**WHEN** a device has fewer than max_sim_slots cards bound -**THEN** empty slot positions shall be clearly indicated with "Bind Card" action buttons + +**WHEN** a device has fewer than max_sim_slots cards bound **THEN** empty slot positions shall be clearly indicated with "Bind Card" action buttons ### Requirement: Device-Card Binding Management The system SHALL allow administrators to bind and unbind SIM cards to specific device slots. #### Scenario: Bind a card to a device slot -**WHEN** an administrator selects a card and slot position (1-4) in the binding dialog -**THEN** the system shall create the binding, update the bound card count, and refresh the card list + +**WHEN** an administrator selects a card and slot position (1-4) in the binding dialog **THEN** the system shall create the binding, update the bound card count, and refresh the card list #### Scenario: Prevent duplicate slot binding -**WHEN** an administrator attempts to bind a card to an already occupied slot -**THEN** the system shall show an error message and prevent the binding + +**WHEN** an administrator attempts to bind a card to an already occupied slot **THEN** the system shall show an error message and prevent the binding #### Scenario: Unbind a card from device -**WHEN** an administrator clicks unbind for a bound card and confirms -**THEN** the system shall remove the binding, decrement the bound card count, and refresh the card list + +**WHEN** an administrator clicks unbind for a bound card and confirms **THEN** the system shall remove the binding, decrement the bound card count, and refresh the card list #### Scenario: Validate slot range -**WHEN** an administrator selects a slot position -**THEN** the system shall only allow values from 1 to the device's max_sim_slots value + +**WHEN** an administrator selects a slot position **THEN** the system shall only allow values from 1 to the device's max_sim_slots value ### Requirement: Batch Device Allocation The system SHALL support batch allocation of devices to shops with result tracking. #### Scenario: Allocate multiple devices to a shop -**WHEN** an administrator selects multiple devices, chooses a target shop, adds optional remarks, and confirms allocation -**THEN** the system shall allocate all selected devices to the shop and display success/failure results for each device + +**WHEN** an administrator selects multiple devices, chooses a target shop, adds optional remarks, and confirms allocation **THEN** the system shall allocate all selected devices to the shop and display success/failure results for each device #### Scenario: Show allocation results -**WHEN** the batch allocation completes -**THEN** the system shall display: total count, success count, failure count, and detailed failure reasons for each failed device + +**WHEN** the batch allocation completes **THEN** the system shall display: total count, success count, failure count, and detailed failure reasons for each failed device #### Scenario: Prevent allocation of already allocated devices -**WHEN** the batch allocation includes devices already allocated to a shop -**THEN** the system shall show warnings for those devices but proceed with allocating unallocated devices + +**WHEN** the batch allocation includes devices already allocated to a shop **THEN** the system shall show warnings for those devices but proceed with allocating unallocated devices ### Requirement: Batch Device Recall The system SHALL support batch recall of devices from shops with result tracking. #### Scenario: Recall multiple devices -**WHEN** an administrator selects multiple devices, adds optional remarks, and confirms recall -**THEN** the system shall recall all selected devices from their shops and display success/failure results + +**WHEN** an administrator selects multiple devices, adds optional remarks, and confirms recall **THEN** the system shall recall all selected devices from their shops and display success/failure results #### Scenario: Show recall results -**WHEN** the batch recall completes -**THEN** the system shall display: total count, success count, failure count, and detailed failure reasons for each failed device + +**WHEN** the batch recall completes **THEN** the system shall display: total count, success count, failure count, and detailed failure reasons for each failed device #### Scenario: Prevent recall of unallocated devices -**WHEN** the batch recall includes devices not allocated to any shop -**THEN** the system shall show warnings for those devices but proceed with recalling allocated devices + +**WHEN** the batch recall includes devices not allocated to any shop **THEN** the system shall show warnings for those devices but proceed with recalling allocated devices ### Requirement: Device Import via Object Storage The system SHALL support CSV-based device import using a three-step object storage upload process. #### Scenario: Import devices with three-step process -**WHEN** an administrator uploads a CSV file -**THEN** the system shall: + +**WHEN** an administrator uploads a CSV file **THEN** the system shall: + 1. Get upload URL from StorageService.getUploadUrl 2. Upload file to object storage using StorageService.uploadFile -3. Call DeviceService.importDevices with the file_key -**AND** display the task number for tracking +3. Call DeviceService.importDevices with the file_key **AND** display the task number for tracking #### Scenario: Validate CSV format -**WHEN** the CSV file is uploaded -**THEN** the system shall validate the presence of required columns: device_no, device_name, device_model, device_type, max_sim_slots, manufacturer + +**WHEN** the CSV file is uploaded **THEN** the system shall validate the presence of required columns: device_no, device_name, device_model, device_type, max_sim_slots, manufacturer #### Scenario: Import devices with card bindings -**WHEN** the CSV includes iccid_1, iccid_2, iccid_3, iccid_4 columns -**THEN** the system shall attempt to bind the specified cards to slots 1-4 respectively during import + +**WHEN** the CSV includes iccid_1, iccid_2, iccid_3, iccid_4 columns **THEN** the system shall attempt to bind the specified cards to slots 1-4 respectively during import #### Scenario: Handle import errors -**WHEN** the import process encounters errors (duplicate device_no, invalid ICCID, etc.) -**THEN** the system shall record failed rows in the import task with detailed error messages + +**WHEN** the import process encounters errors (duplicate device_no, invalid ICCID, etc.) **THEN** the system shall record failed rows in the import task with detailed error messages ### Requirement: Import Task Management The system SHALL track device import tasks with detailed status and record information. #### Scenario: List import tasks with type filter -**WHEN** an administrator views the task management page -**THEN** the system shall display both ICCID import tasks and Device import tasks with a type filter dropdown + +**WHEN** an administrator views the task management page **THEN** the system shall display both ICCID import tasks and Device import tasks with a type filter dropdown #### Scenario: View device import task details -**WHEN** an administrator clicks on a device import task -**THEN** the system shall display: task ID, status, total count, success count, failed count, skipped count, created time, completed time + +**WHEN** an administrator clicks on a device import task **THEN** the system shall display: task ID, status, total count, success count, failed count, skipped count, created time, completed time #### Scenario: View successful import records -**WHEN** viewing a device import task detail -**THEN** the system shall show a table of successfully imported devices with: device_no, device_name, bound ICCIDs + +**WHEN** viewing a device import task detail **THEN** the system shall show a table of successfully imported devices with: device_no, device_name, bound ICCIDs #### Scenario: View failed import records -**WHEN** viewing a device import task detail with failures -**THEN** the system shall show a table of failed records with: row number, device_no, failure reason + +**WHEN** viewing a device import task detail with failures **THEN** the system shall show a table of failed records with: row number, device_no, failure reason #### Scenario: Navigate from import page to task detail -**WHEN** a device import completes successfully -**THEN** the system shall display the task number with a link to view task details + +**WHEN** a device import completes successfully **THEN** the system shall display the task number with a link to view task details ## MODIFIED Requirements @@ -151,9 +151,9 @@ Task management SHALL support multiple task types including ICCID import and Dev **Previous behavior**: Task management only supported ICCID import tasks. #### Scenario: Filter tasks by type -**WHEN** an administrator selects a task type filter (ICCID Import / Device Import) -**THEN** the system shall display only tasks of the selected type + +**WHEN** an administrator selects a task type filter (ICCID Import / Device Import) **THEN** the system shall display only tasks of the selected type #### Scenario: Display task type badges -**WHEN** viewing the task list -**THEN** each task shall display a type badge indicating whether it's an ICCID import or Device import task + +**WHEN** viewing the task list **THEN** each task shall display a type badge indicating whether it's an ICCID import or Device import task diff --git a/openspec/changes/add-enterprise-device-authorization/proposal.md b/openspec/changes/add-enterprise-device-authorization/proposal.md index 3889db0..c341897 100644 --- a/openspec/changes/add-enterprise-device-authorization/proposal.md +++ b/openspec/changes/add-enterprise-device-authorization/proposal.md @@ -15,18 +15,21 @@ 新增企业设备授权管理功能,包括: ### 类型定义 + - 新增 `src/types/api/enterpriseDevice.ts` 文件 - 定义设备列表项、查询参数、分页结果类型 - 定义授权/撤销请求和响应类型 - 在 `src/types/api/index.ts` 中导出新类型 ### API 服务层 + - 扩展 `EnterpriseService` 类,新增3个方法: - `allocateDevices(enterpriseId, data)` - POST 授权设备 - `getEnterpriseDevices(enterpriseId, params)` - GET 设备列表 - `recallDevices(enterpriseId, data)` - POST 撤销授权 ### 视图层 + - 新增 `src/views/asset-management/enterprise-devices/index.vue` 页面 - 实现设备列表展示 (表格、分页、搜索) - 实现授权设备对话框 (支持批量输入设备号) @@ -34,10 +37,12 @@ - 实现操作结果展示 (成功/失败统计) ### 路由配置 + - 在 `src/router/routesAlias.ts` 添加路由别名 - 在 `src/router/routes/asyncRoutes.ts` 的资产管理模块下添加子路由 ### 国际化 + - 在 `src/locales/langs/zh.json` 和 `en.json` 添加中英文翻译 - 包含菜单、表单、表格、对话框、提示消息等所有文案 diff --git a/openspec/changes/add-enterprise-device-authorization/specs/enterprise-device-authorization/spec.md b/openspec/changes/add-enterprise-device-authorization/specs/enterprise-device-authorization/spec.md index 551f741..1dba1ba 100644 --- a/openspec/changes/add-enterprise-device-authorization/specs/enterprise-device-authorization/spec.md +++ b/openspec/changes/add-enterprise-device-authorization/specs/enterprise-device-authorization/spec.md @@ -12,9 +12,8 @@ #### Scenario: 定义企业设备列表项类型 -**Given** 需要展示企业设备列表 -**When** 定义 `EnterpriseDeviceItem` 接口 -**Then** 接口必须包含以下字段: +**Given** 需要展示企业设备列表 **When** 定义 `EnterpriseDeviceItem` 接口 **Then** 接口必须包含以下字段: + - `device_id: number` - 设备ID - `device_no: string` - 设备号 - `device_name: string` - 设备名称 @@ -24,52 +23,49 @@ #### Scenario: 定义设备列表查询参数 -**Given** 需要查询和搜索企业设备 -**When** 定义 `EnterpriseDeviceListParams` 接口 -**Then** 接口必须包含以下可选字段: +**Given** 需要查询和搜索企业设备 **When** 定义 `EnterpriseDeviceListParams` 接口 **Then** 接口必须包含以下可选字段: + - `page?: number` - 页码 - `page_size?: number` - 每页数量 - `device_no?: string` - 设备号模糊搜索 #### Scenario: 定义授权设备请求类型 -**Given** 需要授权设备给企业 -**When** 定义 `AllocateDevicesRequest` 接口 -**Then** 接口必须包含: +**Given** 需要授权设备给企业 **When** 定义 `AllocateDevicesRequest` 接口 **Then** 接口必须包含: + - `device_nos: string[]` - 设备号列表 (nullable, 最多100个) - `remark?: string` - 授权备注 #### Scenario: 定义授权设备响应类型 -**Given** 授权操作需要返回详细结果 -**When** 定义 `AllocateDevicesResponse` 接口 -**Then** 接口必须包含: +**Given** 授权操作需要返回详细结果 **When** 定义 `AllocateDevicesResponse` 接口 **Then** 接口必须包含: + - `success_count: number` - 成功数量 - `fail_count: number` - 失败数量 - `authorized_devices: AuthorizedDeviceItem[]` - 已授权设备列表 (nullable) - `failed_items: FailedDeviceItem[]` - 失败项列表 (nullable) **And** `AuthorizedDeviceItem` 包含: + - `device_id: number` - 设备ID - `device_no: string` - 设备号 - `card_count: number` - 绑定卡数量 **And** `FailedDeviceItem` 包含: + - `device_no: string` - 设备号 - `reason: string` - 失败原因 #### Scenario: 定义撤销授权请求类型 -**Given** 需要撤销设备授权 -**When** 定义 `RecallDevicesRequest` 接口 -**Then** 接口必须包含: +**Given** 需要撤销设备授权 **When** 定义 `RecallDevicesRequest` 接口 **Then** 接口必须包含: + - `device_nos: string[]` - 设备号列表 (nullable, 最多100个) #### Scenario: 定义撤销授权响应类型 -**Given** 撤销操作需要返回结果统计 -**When** 定义 `RecallDevicesResponse` 接口 -**Then** 接口必须包含: +**Given** 撤销操作需要返回结果统计 **When** 定义 `RecallDevicesResponse` 接口 **Then** 接口必须包含: + - `success_count: number` - 成功数量 - `fail_count: number` - 失败数量 - `failed_items: FailedDeviceItem[]` - 失败项列表 (nullable) @@ -82,28 +78,15 @@ #### Scenario: 授权设备给企业 -**Given** 运营人员需要授权设备给企业客户 -**When** 调用 `EnterpriseService.allocateDevices(enterpriseId, data)` -**Then** 必须发送 POST 请求到 `/api/admin/enterprises/{id}/allocate-devices` -**And** 请求体必须包含设备号列表和可选备注 -**And** 返回授权结果,包含成功/失败统计和详细列表 +**Given** 运营人员需要授权设备给企业客户 **When** 调用 `EnterpriseService.allocateDevices(enterpriseId, data)` **Then** 必须发送 POST 请求到 `/api/admin/enterprises/{id}/allocate-devices` **And** 请求体必须包含设备号列表和可选备注 **And** 返回授权结果,包含成功/失败统计和详细列表 #### Scenario: 获取企业设备列表 -**Given** 需要查看企业的设备列表 -**When** 调用 `EnterpriseService.getEnterpriseDevices(enterpriseId, params)` -**Then** 必须发送 GET 请求到 `/api/admin/enterprises/{id}/devices` -**And** 支持分页参数 (page, page_size) -**And** 支持设备号模糊搜索 -**And** 返回设备列表和总数 +**Given** 需要查看企业的设备列表 **When** 调用 `EnterpriseService.getEnterpriseDevices(enterpriseId, params)` **Then** 必须发送 GET 请求到 `/api/admin/enterprises/{id}/devices` **And** 支持分页参数 (page, page_size) **And** 支持设备号模糊搜索 **And** 返回设备列表和总数 #### Scenario: 撤销设备授权 -**Given** 需要撤销企业的设备授权 -**When** 调用 `EnterpriseService.recallDevices(enterpriseId, data)` -**Then** 必须发送 POST 请求到 `/api/admin/enterprises/{id}/recall-devices` -**And** 请求体必须包含设备号列表 -**And** 返回撤销结果,包含成功/失败统计和失败原因 +**Given** 需要撤销企业的设备授权 **When** 调用 `EnterpriseService.recallDevices(enterpriseId, data)` **Then** 必须发送 POST 请求到 `/api/admin/enterprises/{id}/recall-devices` **And** 请求体必须包含设备号列表 **And** 返回撤销结果,包含成功/失败统计和失败原因 --- @@ -113,10 +96,8 @@ #### Scenario: 显示企业设备列表 -**Given** 用户访问企业设备列表页面 -**When** 页面加载完成 -**Then** 必须显示设备列表表格 -**And** 表格必须包含以下列: +**Given** 用户访问企业设备列表页面 **When** 页面加载完成 **Then** 必须显示设备列表表格 **And** 表格必须包含以下列: + - 设备ID - 设备号 - 设备名称 @@ -124,56 +105,32 @@ - 绑定卡数量 - 授权时间 -**And** 必须支持分页功能 -**And** 必须显示加载状态 +**And** 必须支持分页功能 **And** 必须显示加载状态 #### Scenario: 搜索企业设备 -**Given** 设备列表已加载 -**When** 用户在搜索框输入设备号 -**And** 点击搜索按钮 -**Then** 必须根据设备号模糊查询设备 -**And** 必须更新设备列表显示 -**And** 必须重置到第一页 +**Given** 设备列表已加载 **When** 用户在搜索框输入设备号 **And** 点击搜索按钮 **Then** 必须根据设备号模糊查询设备 **And** 必须更新设备列表显示 **And** 必须重置到第一页 #### Scenario: 授权设备对话框 -**Given** 用户点击"授权设备"按钮 -**When** 授权设备对话框打开 -**Then** 必须显示设备号输入框 (textarea) -**And** 必须显示备注输入框 (可选) -**And** 必须提示支持的输入格式 (换行或逗号分隔) -**And** 必须提示最多100个设备号限制 -**And** 必须有表单验证 (设备号必填) +**Given** 用户点击"授权设备"按钮 **When** 授权设备对话框打开 **Then** 必须显示设备号输入框 (textarea) **And** 必须显示备注输入框 (可选) **And** 必须提示支持的输入格式 (换行或逗号分隔) **And** 必须提示最多100个设备号限制 **And** 必须有表单验证 (设备号必填) #### Scenario: 提交授权设备 -**Given** 用户在对话框中输入了设备号列表 -**When** 用户点击提交按钮 -**Then** 必须解析设备号列表 (支持换行和逗号分隔) -**And** 必须去除空白字符和空行 -**And** 必须验证设备号数量不超过100个 -**And** 必须调用授权 API -**And** 必须显示加载状态 -**And** 授权完成后必须展示结果: +**Given** 用户在对话框中输入了设备号列表 **When** 用户点击提交按钮 **Then** 必须解析设备号列表 (支持换行和逗号分隔) **And** 必须去除空白字符和空行 **And** 必须验证设备号数量不超过100个 **And** 必须调用授权 API **And** 必须显示加载状态 **And** 授权完成后必须展示结果: + - 成功数量 - 失败数量 - 失败设备列表及原因 -**And** 如果有成功授权的设备,必须刷新设备列表 -**And** 必须关闭对话框 +**And** 如果有成功授权的设备,必须刷新设备列表 **And** 必须关闭对话框 #### Scenario: 撤销设备授权 -**Given** 用户选中了要撤销的设备 -**When** 用户点击"撤销授权"按钮 -**Then** 必须显示二次确认对话框 -**And** 确认对话框必须显示将要撤销的设备数量 +**Given** 用户选中了要撤销的设备 **When** 用户点击"撤销授权"按钮 **Then** 必须显示二次确认对话框 **And** 确认对话框必须显示将要撤销的设备数量 + +**When** 用户确认撤销 **Then** 必须调用撤销 API **And** 必须显示加载状态 **And** 撤销完成后必须展示结果: -**When** 用户确认撤销 -**Then** 必须调用撤销 API -**And** 必须显示加载状态 -**And** 撤销完成后必须展示结果: - 成功数量 - 失败数量 - 失败设备列表及原因 @@ -182,19 +139,11 @@ #### Scenario: 错误处理 -**Given** API 调用可能失败 -**When** API 返回错误 -**Then** 必须显示友好的错误提示消息 -**And** 必须在控制台记录错误详情 -**And** 必须停止加载状态 +**Given** API 调用可能失败 **When** API 返回错误 **Then** 必须显示友好的错误提示消息 **And** 必须在控制台记录错误详情 **And** 必须停止加载状态 #### Scenario: 分页切换 -**Given** 设备列表超过一页 -**When** 用户切换页码或每页数量 -**Then** 必须保持当前的搜索条件 -**And** 必须重新加载设备列表 -**And** 必须显示加载状态 +**Given** 设备列表超过一页 **When** 用户切换页码或每页数量 **Then** 必须保持当前的搜索条件 **And** 必须重新加载设备列表 **And** 必须显示加载状态 --- @@ -204,13 +153,8 @@ #### Scenario: 注册企业设备列表路由 -**Given** 需要访问企业设备列表页面 -**When** 配置路由 -**Then** 必须在资产管理模块 (`/asset-management`) 下添加子路由 -**And** 路由路径必须为 `enterprise-devices` -**And** 路由名称必须为 `EnterpriseDevices` -**And** 必须使用路由别名 `RoutesAlias.EnterpriseDevices` -**And** 必须配置 meta 信息: +**Given** 需要访问企业设备列表页面 **When** 配置路由 **Then** 必须在资产管理模块 (`/asset-management`) 下添加子路由 **And** 路由路径必须为 `enterprise-devices` **And** 路由名称必须为 `EnterpriseDevices` **And** 必须使用路由别名 `RoutesAlias.EnterpriseDevices` **And** 必须配置 meta 信息: + - `title: 'menus.assetManagement.enterpriseDevices'` - `keepAlive: true` @@ -222,9 +166,8 @@ #### Scenario: 中文翻译 -**Given** 系统语言设置为中文 -**When** 访问企业设备相关页面 -**Then** 所有文本必须显示中文,包括: +**Given** 系统语言设置为中文 **When** 访问企业设备相关页面 **Then** 所有文本必须显示中文,包括: + - 菜单标题: "企业设备列表" - 搜索表单标签和占位符 - 表格列名 @@ -234,9 +177,8 @@ #### Scenario: 英文翻译 -**Given** 系统语言设置为英文 -**When** 访问企业设备相关页面 -**Then** 所有文本必须显示英文,包括: +**Given** 系统语言设置为英文 **When** 访问企业设备相关页面 **Then** 所有文本必须显示英文,包括: + - 菜单标题: "Enterprise Devices" - 搜索表单标签和占位符 - 表格列名 @@ -252,37 +194,28 @@ #### Scenario: 批量输入设备号 -**Given** 用户需要授权多个设备 -**When** 在设备号输入框中输入 -**Then** 必须支持以下输入方式: +**Given** 用户需要授权多个设备 **When** 在设备号输入框中输入 **Then** 必须支持以下输入方式: + - 每行一个设备号 - 逗号分隔的设备号 - 混合使用换行和逗号 -**And** 系统必须能正确解析所有格式 -**And** 必须自动去除首尾空白字符 -**And** 必须过滤空行 +**And** 系统必须能正确解析所有格式 **And** 必须自动去除首尾空白字符 **And** 必须过滤空行 #### Scenario: 操作结果展示 -**Given** 批量操作完成 -**When** 显示操作结果 -**Then** 必须清晰展示: +**Given** 批量操作完成 **When** 显示操作结果 **Then** 必须清晰展示: + - 总共处理的数量 - 成功的数量 - 失败的数量 - 每个失败项的设备号和失败原因 -**And** 如果全部成功,必须显示成功提示 -**And** 如果部分失败,必须显示警告提示 -**And** 如果全部失败,必须显示错误提示 +**And** 如果全部成功,必须显示成功提示 **And** 如果部分失败,必须显示警告提示 **And** 如果全部失败,必须显示错误提示 #### Scenario: 表格列管理 -**Given** 设备列表表格已显示 -**When** 用户点击列管理按钮 -**Then** 必须能够选择显示/隐藏的列 -**And** 列配置必须被保存 +**Given** 设备列表表格已显示 **When** 用户点击列管理按钮 **Then** 必须能够选择显示/隐藏的列 **And** 列配置必须被保存 ## Related Specs diff --git a/openspec/changes/add-enterprise-device-authorization/tasks.md b/openspec/changes/add-enterprise-device-authorization/tasks.md index 61903e3..8ecb5c5 100644 --- a/openspec/changes/add-enterprise-device-authorization/tasks.md +++ b/openspec/changes/add-enterprise-device-authorization/tasks.md @@ -9,6 +9,7 @@ ### Phase 1: Type Definitions (Foundation) 1. **创建企业设备类型定义文件** + - 创建 `src/types/api/enterpriseDevice.ts` - 定义 `EnterpriseDeviceItem` 接口 (设备列表项) - 定义 `EnterpriseDeviceListParams` 接口 (查询参数) @@ -38,6 +39,7 @@ ### Phase 3: Internationalization 4. **添加中文翻译** + - 在 `src/locales/langs/zh.json` 的 `menus.assetManagement` 下添加 `enterpriseDevices` 条目 - 在 `src/locales/langs/zh.json` 添加 `enterpriseDevices` 模块的所有中文文案: - 页面标题和搜索表单 @@ -55,6 +57,7 @@ ### Phase 4: Routing 6. **添加路由别名** + - 在 `src/router/routesAlias.ts` 添加 `EnterpriseDevices = '/asset-management/enterprise-devices'` - **验证**: 确认导出正确 @@ -66,6 +69,7 @@ ### Phase 5: UI Components 8. **创建企业设备列表页面** + - 创建 `src/views/asset-management/enterprise-devices/index.vue` - 实现页面基础结构: - 使用 `ArtTableFullScreen` 布局 @@ -75,6 +79,7 @@ - **验证**: 页面能正常渲染,无控制台错误 9. **实现设备列表查询功能** + - 实现 `loadDeviceList()` 方法调用 API - 实现搜索和重置功能 - 实现分页功能 @@ -82,6 +87,7 @@ - **验证**: 能正确展示设备列表数据,分页工作正常 10. **实现授权设备对话框** + - 创建授权设备对话框 - 使用 `ElForm` + `ElInput` (textarea) 输入设备号列表 - 支持多行输入或逗号分隔 @@ -90,6 +96,7 @@ - **验证**: 对话框显示正常,表单验证工作 11. **实现授权设备提交逻辑** + - 实现 `handleAllocateDevices()` 方法 - 解析设备号列表 (处理换行和逗号分隔) - 调用 `EnterpriseService.allocateDevices()` API @@ -99,6 +106,7 @@ - **验证**: 能成功授权设备,正确处理部分成功/失败情况 12. **实现撤销授权对话框** + - 创建撤销授权对话框 - 使用表格多选模式选择要撤销的设备 - 或者使用输入框输入设备号列表 @@ -116,18 +124,21 @@ ### Phase 6: Polish & Testing 14. **完善表格列配置** + - 配置表格列 (设备ID,设备号,设备名称,设备型号,绑定卡数量,授权时间) - 实现列显示/隐藏功能 - 添加时间格式化 - **验证**: 表格数据展示完整美观 15. **添加错误处理** + - 为所有 API 调用添加 try-catch - 添加友好的错误提示消息 - 处理网络错误和业务错误 - **验证**: 各种错误场景都有适当提示 16. **样式调整** + - 确保页面样式与系统其他页面一致 - 响应式布局适配 - 对话框尺寸和布局优化 diff --git a/openspec/changes/add-order-management/proposal.md b/openspec/changes/add-order-management/proposal.md index c3c946a..e03ebc2 100644 --- a/openspec/changes/add-order-management/proposal.md +++ b/openspec/changes/add-order-management/proposal.md @@ -3,6 +3,7 @@ ## Why The IoT management platform currently lacks order management capabilities. Users need to: + - View and query orders created by customers (personal and agent) - Track order payment status and details - Create orders for single card or device purchases @@ -20,6 +21,7 @@ This capability is essential for financial tracking, commission calculation, and - **NEW**: Router configuration for order management module The order management module will support: + - Listing orders with filters (payment status, order type, date range, order number) - Viewing order details including buyer information, order items (packages), and payment details - Creating orders for single card or device purchases with package selection diff --git a/openspec/changes/add-package-management-system/design.md b/openspec/changes/add-package-management-system/design.md index b883a0b..979084b 100644 --- a/openspec/changes/add-package-management-system/design.md +++ b/openspec/changes/add-package-management-system/design.md @@ -5,12 +5,14 @@ 实现完整的套餐管理系统,包括4个核心模块。该系统需要支持多级代理商体系的套餐分配和定价管理。 **背景**: + - 项目已有类型定义(src/types/api/package.ts),但使用不同的字段命名和枚举值 - 后端 API 已实现,使用下划线命名(如 `series_name`) - 前端项目统一使用 CommonStatus 枚举(0:禁用, 1:启用) - 参考实现:`/system/role` 页面使用了组件化架构 **约束**: + - 必须保留现有类型定义文件,不能破坏现有代码 - 需要兼容后端 API 的字段命名规范 - 需要适配项目的状态枚举规范 @@ -18,6 +20,7 @@ ## Goals / Non-Goals ### Goals + 1. 实现4个核心模块的完整 CRUD 功能 2. 建立统一的 API 服务层,封装后端接口 3. 实现组件化的页面结构,参考 `/system/role` @@ -25,6 +28,7 @@ 5. 确保数据隔离和权限控制 ### Non-Goals + 1. 不重构现有的 package.ts 类型定义 2. 不实现套餐的实时统计和报表功能(后续迭代) 3. 不实现套餐批量导入功能(后续迭代) @@ -37,20 +41,23 @@ **问题**:后端使用下划线命名(snake_case),前端类型通常使用驼峰命名(camelCase)。 **决策**: + - API 请求/响应保持下划线命名,与后端保持一致 - 创建新的类型文件 `packageManagement.ts`,使用下划线命名 - 在表单提交和响应处理时不做转换,直接使用下划线字段 **理由**: + - 减少转换层的复杂性和错误风险 - 与后端 API 文档保持一致,便于对照 - TypeScript 支持下划线字段名,不影响类型安全 **示例**: + ```typescript export interface PackageSeriesResponse { id: number - series_code: string // 下划线命名 + series_code: string // 下划线命名 series_name: string status: number created_at: string @@ -63,11 +70,13 @@ export interface PackageSeriesResponse { **问题**:文档中状态是 `1:启用, 2:禁用`,但项目 CommonStatus 是 `0:禁用, 1:启用`。 **决策**: + - **在常量配置中定义套餐专用的状态枚举** - **前端页面使用项目统一的 CommonStatus(0/1)** - **在 API 服务层进行状态值映射转换** **映射规则**: + ```typescript // 前端 -> 后端 CommonStatus.ENABLED (1) -> API Status (1) @@ -79,6 +88,7 @@ API Status (2) -> CommonStatus.DISABLED (0) ``` **理由**: + - 保持前端 UI 的一致性 - 避免混淆项目开发者 - 集中在 API 服务层处理差异 @@ -88,18 +98,21 @@ API Status (2) -> CommonStatus.DISABLED (0) **问题**:是创建单个 package.ts 服务,还是拆分为多个服务文件? **决策**:拆分为4个独立的服务文件: + 1. `packageSeries.ts` - 套餐系列管理 2. `package.ts` - 套餐管理 3. `myPackage.ts` - 代理可售套餐 4. `shopPackageAllocation.ts` - 单套餐分配 **理由**: + - 每个模块功能独立,职责清晰 - 便于维护和扩展 - 符合单一职责原则 - 便于团队协作(不同开发者负责不同模块) **替代方案**: + - 单个 package.ts 文件 - **拒绝**,文件过大,难以维护 ### Decision 4: 定价规则实现 @@ -107,11 +120,13 @@ API Status (2) -> CommonStatus.DISABLED (0) **问题**:代理商的套餐成本价有两种计算方式:系列加价和单套餐覆盖。 **决策**: + - **后端负责成本价计算**,前端只展示结果 - 前端接收 `price_source` 字段,标识价格来源 - 单套餐分配创建时,保存 `calculated_cost_price`(系列规则计算的价格)供参考 **数据流**: + ``` 1. 系列分配:pricing_mode + pricing_value -> 后端计算 -> cost_price 2. 单套餐分配:直接设置 cost_price(覆盖系列规则) @@ -119,6 +134,7 @@ API Status (2) -> CommonStatus.DISABLED (0) ``` **理由**: + - 计算逻辑复杂,集中在后端便于维护 - 前端只负责展示,降低复杂度 - 保留 calculated_cost_price 便于调试和审计 @@ -128,16 +144,19 @@ API Status (2) -> CommonStatus.DISABLED (0) **问题**:客户端验证 vs 服务端验证。 **决策**:**双重验证** + - 客户端:使用 Element Plus 的 FormRules 进行基础验证 - 服务端:后端 API 进行完整验证并返回详细错误 **客户端验证规则**: + - 必填字段检查 - 长度限制(如系列名称 1-255 字符) - 数值范围(如套餐时长 1-120 月) - 格式验证(如价格必须为正整数) **理由**: + - 客户端验证提升用户体验,即时反馈 - 服务端验证保证数据安全性和完整性 - 符合 Web 应用最佳实践 @@ -147,20 +166,26 @@ API Status (2) -> CommonStatus.DISABLED (0) **问题**:页面结构如何组织? **决策**:参考 `/system/role` 页面,使用组件化结构: + ```vue ``` **理由**: + - 与项目现有页面风格一致 - 复用成熟的组件,减少开发工作量 - 便于维护和扩展 @@ -172,6 +197,7 @@ API Status (2) -> CommonStatus.DISABLED (0) **风险**:后端接口可能尚未实现或与文档不一致。 **缓解措施**: + 1. 先实现 API 服务层,使用 TypeScript 类型约束 2. 使用 Mock 数据进行前端开发(已有示例) 3. 与后端团队确认 API 规范和联调时间 @@ -182,6 +208,7 @@ API Status (2) -> CommonStatus.DISABLED (0) **风险**:在某些地方忘记转换状态值,导致显示错误。 **缓解措施**: + 1. 在 API 服务层统一处理转换 2. 创建工具函数封装映射逻辑 3. 编写单元测试覆盖映射函数 @@ -192,6 +219,7 @@ API Status (2) -> CommonStatus.DISABLED (0) **风险**:对定价规则的理解与实际业务需求有偏差。 **缓解措施**: + 1. 在实现前与产品确认定价规则 2. 编写测试用例覆盖各种定价场景 3. 在 UI 上清晰展示价格来源和计算方式 @@ -202,10 +230,12 @@ API Status (2) -> CommonStatus.DISABLED (0) **取舍**:保留旧的 package.ts 类型定义,新增 packageManagement.ts。 **代价**: + - 存在两套类型定义,可能造成混淆 - 占用额外的代码空间 **收益**: + - 不影响现有代码,向后兼容 - 新旧系统可以并存,降低迁移风险 - 未来可以逐步迁移到新类型 @@ -215,10 +245,12 @@ API Status (2) -> CommonStatus.DISABLED (0) **取舍**:在 API 服务层进行状态值转换。 **代价**: + - 增加一层转换逻辑 - 可能影响性能(微小) **收益**: + - 前端 UI 保持一致性 - 业务逻辑更清晰 - 便于后续维护 @@ -226,27 +258,32 @@ API Status (2) -> CommonStatus.DISABLED (0) ## Migration Plan ### Phase 1: 基础设施(1-2天) + 1. 创建类型定义文件 2. 创建常量配置文件 3. 设置状态映射工具函数 ### Phase 2: API 服务层(2-3天) + 1. 实现4个 API 服务模块 2. 编写单元测试(可选) 3. 使用 Mock 数据测试 ### Phase 3: 页面实现(4-5天) + 1. 套餐系列管理页面(1天) 2. 套餐管理页面(1.5天) 3. 代理可售套餐页面(1天) 4. 单套餐分配页面(1.5天) ### Phase 4: 集成测试(1-2天) + 1. 与后端 API 联调 2. 端到端功能测试 3. 修复 Bug 和优化 ### Phase 5: 上线(1天) + 1. Code Review 2. 合并代码 3. 部署到测试环境 @@ -257,6 +294,7 @@ API Status (2) -> CommonStatus.DISABLED (0) ### Rollback Plan 如果出现严重问题,回滚步骤: + 1. 从 Git 回滚到上一个稳定版本 2. 移除新增的路由配置 3. 移除新增的 API 服务导出 @@ -267,6 +305,7 @@ API Status (2) -> CommonStatus.DISABLED (0) **问题**:如何统一处理各类错误和异常? **决策**:分层错误处理机制 + - **网络错误**:axios 拦截器统一捕获,显示通用错误提示 - **401 未认证**:自动跳转到登录页面 - **403 无权限**:显示权限不足提示,不跳转 @@ -274,6 +313,7 @@ API Status (2) -> CommonStatus.DISABLED (0) - **表单验证错误**:在表单字段下显示错误提示 **错误提示方式**: + ```typescript // 网络错误或服务器错误 ElMessage.error('网络错误,请稍后重试') @@ -286,6 +326,7 @@ ElMessage.success('操作成功') ``` **理由**: + - 统一的错误处理提升用户体验 - 分层处理避免重复代码 - 清晰的错误提示帮助用户理解问题 @@ -297,36 +338,34 @@ ElMessage.success('操作成功') **决策**:细粒度的 loading 状态管理 **Loading 状态分类**: + ```typescript -const loading = ref(false) // 表格数据加载 -const submitLoading = ref(false) // 表单提交 +const loading = ref(false) // 表格数据加载 +const submitLoading = ref(false) // 表单提交 const deleteLoading = ref>({}) // 删除操作(可选) ``` **状态管理规则**: + - **列表查询**:表格显示 loading 遮罩 - **新增/编辑提交**:提交按钮显示 loading,禁用表单 - **删除操作**:可选择在按钮上显示 loading 或全局 loading - **状态切换**:ElSwitch 自带 loading 效果,先更新 UI 再调用 API **理由**: + - 细粒度控制提供更好的交互反馈 - 防止重复提交 - 清晰标识正在进行的操作 ## Open Questions -1. **Q**: 套餐被删除后,历史订单如何处理? - **A**: 待产品确认,可能需要软删除机制 +1. **Q**: 套餐被删除后,历史订单如何处理? **A**: 待产品确认,可能需要软删除机制 -2. **Q**: 代理商可以自行调整套餐售价吗? - **A**: 待产品确认,当前设计只展示建议售价 +2. **Q**: 代理商可以自行调整套餐售价吗? **A**: 待产品确认,当前设计只展示建议售价 -3. **Q**: 套餐系列和套餐是否支持批量操作(批量启用/禁用)? - **A**: 当前不支持,后续迭代考虑 +3. **Q**: 套餐系列和套餐是否支持批量操作(批量启用/禁用)? **A**: 当前不支持,后续迭代考虑 -4. **Q**: 是否需要套餐变更历史记录? - **A**: 后端可能有审计日志,前端暂不展示 +4. **Q**: 是否需要套餐变更历史记录? **A**: 后端可能有审计日志,前端暂不展示 -5. **Q**: 单套餐分配的"原计算成本价"是否需要实时更新? - **A**: 待确认,当前设计是创建时计算一次,不自动更新 +5. **Q**: 单套餐分配的"原计算成本价"是否需要实时更新? **A**: 待确认,当前设计是创建时计算一次,不自动更新 diff --git a/openspec/changes/add-package-management-system/proposal.md b/openspec/changes/add-package-management-system/proposal.md index 35ec9d0..ff62365 100644 --- a/openspec/changes/add-package-management-system/proposal.md +++ b/openspec/changes/add-package-management-system/proposal.md @@ -18,6 +18,7 @@ 采用模块化设计,拆分为 4 个独立的 API 服务文件: - **新增**: `src/api/modules/packageSeries.ts` - 套餐系列 API 服务 + - 套餐系列列表查询(分页、筛选) - 创建套餐系列 - 获取套餐系列详情 @@ -26,6 +27,7 @@ - 更新套餐系列状态 - **新增**: `src/api/modules/package.ts` - 套餐管理 API 服务 + - 套餐列表查询(分页、多条件筛选) - 创建套餐 - 获取套餐详情 @@ -36,11 +38,13 @@ - 获取系列下拉选项(用于表单选择) - **新增**: `src/api/modules/myPackage.ts` - 代理可售套餐 API 服务 + - 我的可售套餐列表查询 - 获取可售套餐详情 - 我的被分配系列列表 - **新增**: `src/api/modules/shopPackageAllocation.ts` - 单套餐分配 API 服务 + - 单套餐分配列表查询 - 创建单套餐分配 - 获取单套餐分配详情 @@ -64,12 +68,14 @@ ### 3. 页面实现 **套餐系列管理** (`src/views/package-management/package-series/index.vue`) + - 列表展示(支持名称搜索、状态筛选) - 新增/编辑套餐系列 - 删除套餐系列 - 状态开关(启用/禁用) **套餐管理** (`src/views/package-management/package-list/index.vue`) + - 列表展示(支持多条件筛选:名称、系列、状态、上架状态、套餐类型) - 新增/编辑套餐 - 删除套餐 @@ -77,11 +83,13 @@ - 上架状态开关(上架/下架) **代理可售套餐** (`src/views/package-management/my-packages/index.vue`) + - 查看被分配的套餐列表(支持系列、类型筛选) - 查看套餐详情(成本价、建议售价、利润空间等) - 查看被分配系列列表 **单套餐分配** (`src/views/package-management/package-assign/index.vue`) + - 分配列表(支持店铺、套餐、状态筛选) - 创建分配(选择套餐、店铺、设置成本价) - 编辑分配(修改成本价) @@ -100,12 +108,15 @@ ### 5. 路由配置 已存在的路由(无需修改): + - `/package-management/package-series` - 套餐系列管理 - `/package-management/package-list` - 套餐管理 - `/package-management/package-assign` - 单套餐分配 需要新增的路由: + - **新增**: `src/router/routesAlias.ts` - 添加路由别名 + - `MyPackages = '/package-management/my-packages'` - 代理可售套餐 - **新增**: `src/router/routes/asyncRoutes.ts` - 添加路由配置 @@ -114,12 +125,14 @@ ## Impact ### 受影响的规范 + - `package-series-management` - 新增能力 - `package-management` - 新增能力 - `my-packages` - 新增能力 - `shop-package-allocation` - 新增能力 ### 受影响的代码 + - `src/api/modules/*` - 新增 4 个 API 服务模块 - `src/types/api/*` - 新增类型定义文件 - `src/views/package-management/*` - 4 个页面完整实现 @@ -127,6 +140,7 @@ - `src/router/routes/asyncRoutes.ts` - 路由配置 ### 依赖关系 + - 依赖现有的组件库(ArtTable、ArtSearchBar、ArtTableHeader 等) - 依赖现有的 HTTP 请求工具(request.ts) - 依赖现有的权限控制和路由守卫 @@ -134,10 +148,12 @@ - 后端 API 需已实现(docs/套餐.md 中定义的接口) **注意事项**: + - ShopService 应该已经存在于 src/api/modules/shop.ts - 如果不存在,需要先实现或使用 Mock 数据 ### 风险评估 + - **低风险**: 独立模块,不影响现有功能 - **API 依赖**: 需确保后端接口已实现并联调 - **权限控制**: 需配置对应的菜单和按钮权限 diff --git a/openspec/changes/add-package-management-system/tasks.md b/openspec/changes/add-package-management-system/tasks.md index 37ae01a..a31c24a 100644 --- a/openspec/changes/add-package-management-system/tasks.md +++ b/openspec/changes/add-package-management-system/tasks.md @@ -9,6 +9,7 @@ ## 2. API 服务层实现 ### 2.1 套餐系列 API(packageSeries.ts) + - [ ] 2.1.1 实现 getPackageSeries(套餐系列列表) - [ ] 2.1.2 实现 createPackageSeries(创建套餐系列) - [ ] 2.1.3 实现 getPackageSeriesDetail(获取套餐系列详情) @@ -17,6 +18,7 @@ - [ ] 2.1.6 实现 updatePackageSeriesStatus(更新套餐系列状态) ### 2.2 套餐管理 API(package.ts) + - [ ] 2.2.1 实现 getPackages(套餐列表) - [ ] 2.2.2 实现 createPackage(创建套餐) - [ ] 2.2.3 实现 getPackageDetail(获取套餐详情) @@ -26,11 +28,13 @@ - [ ] 2.2.7 实现 updatePackageShelfStatus(更新套餐上架状态) ### 2.3 代理可售套餐 API(myPackage.ts) + - [ ] 2.3.1 实现 getMyPackages(我的可售套餐列表) - [ ] 2.3.2 实现 getMyPackageDetail(获取可售套餐详情) - [ ] 2.3.3 实现 getMySeriesAllocations(我的被分配系列列表) ### 2.4 单套餐分配 API(shopPackageAllocation.ts) + - [ ] 2.4.1 实现 getShopPackageAllocations(单套餐分配列表) - [ ] 2.4.2 实现 createShopPackageAllocation(创建单套餐分配) - [ ] 2.4.3 实现 getShopPackageAllocationDetail(获取单套餐分配详情) @@ -43,6 +47,7 @@ ## 3. 页面实现 ### 3.1 套餐系列管理页面(package-series/index.vue) + - [ ] 3.1.1 实现列表展示(表格、分页) - [ ] 3.1.2 实现搜索栏(系列名称、状态筛选) - [ ] 3.1.3 实现新增对话框(表单验证) @@ -52,6 +57,7 @@ - [ ] 3.1.7 集成 API 服务并处理加载状态 ### 3.2 套餐管理页面(package-list/index.vue) + - [ ] 3.2.1 实现列表展示(表格、分页) - [ ] 3.2.2 实现搜索栏(名称、系列、状态、上架状态、类型筛选) - [ ] 3.2.3 实现系列下拉选择器(加载套餐系列列表,只显示启用状态) @@ -63,6 +69,7 @@ - [ ] 3.2.9 集成 API 服务并处理加载状态 ### 3.3 代理可售套餐页面(my-packages/index.vue) + - [ ] 3.3.1 创建页面文件和基本结构 - [ ] 3.3.2 实现列表展示(表格、分页) - [ ] 3.3.3 实现搜索栏(系列、类型筛选) @@ -71,6 +78,7 @@ - [ ] 3.3.6 集成 API 服务并处理加载状态 ### 3.4 单套餐分配页面(package-assign/index.vue) + - [ ] 3.4.1 创建页面文件和基本结构 - [ ] 3.4.2 实现列表展示(表格、分页) - [ ] 3.4.3 实现搜索栏(店铺、套餐、状态筛选) @@ -90,6 +98,7 @@ ## 5. 集成测试 ### 5.1 套餐系列管理测试 + - [ ] 5.1.1 测试列表查询(空列表、有数据、分页) - [ ] 5.1.2 测试搜索功能(名称模糊搜索、状态筛选) - [ ] 5.1.3 测试新增功能(成功、编码重复、字段验证) @@ -99,6 +108,7 @@ - [ ] 5.1.7 测试权限控制(未登录、无权限) ### 5.2 套餐管理测试 + - [ ] 5.2.1 测试列表查询(空列表、有数据、分页) - [ ] 5.2.2 测试多条件筛选(名称、系列、状态、上架状态、类型) - [ ] 5.2.3 测试系列下拉选择器(只显示启用状态的系列) @@ -110,6 +120,7 @@ - [ ] 5.2.9 测试权限控制(未登录、无权限) ### 5.3 代理可售套餐测试 + - [ ] 5.3.1 测试列表查询(空列表、有数据、分页) - [ ] 5.3.2 测试筛选功能(按系列、按类型) - [ ] 5.3.3 测试详情查询(显示成本价、建议售价、利润空间、价格来源) @@ -118,6 +129,7 @@ - [ ] 5.3.6 测试权限控制(非代理商用户无法访问) ### 5.4 单套餐分配测试 + - [ ] 5.4.1 测试列表查询(空列表、有数据、分页) - [ ] 5.4.2 测试筛选功能(按店铺、按套餐、按状态) - [ ] 5.4.3 测试套餐下拉选择器(只显示启用且上架的套餐) @@ -130,6 +142,7 @@ - [ ] 5.4.10 测试权限控制(仅管理员可操作) ### 5.5 通用功能测试 + - [ ] 5.5.1 测试所有页面的表单验证(必填、长度、格式) - [ ] 5.5.2 测试所有页面的 loading 状态(列表、提交、删除) - [ ] 5.5.3 测试所有页面的错误处理(网络错误、业务错误) diff --git a/openspec/changes/update-series-allocation-commission/design.md b/openspec/changes/update-series-allocation-commission/design.md index 7a9c795..f90daa8 100644 --- a/openspec/changes/update-series-allocation-commission/design.md +++ b/openspec/changes/update-series-allocation-commission/design.md @@ -2,12 +2,14 @@ ## Context -当前系统使用简单的定价模式(pricing_mode/pricing_value)和一次性佣金(one_time_commission_*)来管理套餐系列分配。随着业务发展,需要更复杂的佣金系统: +当前系统使用简单的定价模式(pricing*mode/pricing_value)和一次性佣金(one_time_commission*\*)来管理套餐系列分配。随着业务发展,需要更复杂的佣金系统: + - 支持基础返佣(固定金额或百分比) - 支持梯度返佣(根据销量或销售额分档返佣) - 更清晰的数据模型和API接口 **背景约束**: + - 前后端需要同步部署(Breaking Change) - 需要数据迁移方案 - 影响现有的套餐系列分配功能 @@ -15,12 +17,14 @@ ## Goals / Non-Goals **Goals**: + - 实现新的佣金配置模型,支持基础返佣和梯度返佣 - 提供清晰的UI界面让用户配置复杂的返佣规则 - 保证数据一致性和类型安全 - 提供良好的用户体验(表单验证、错误提示) **Non-Goals**: + - 不处理历史数据的完整性验证(由后端负责) - 不实现佣金计算逻辑(由后端负责) - 不处理佣金结算流程(属于其他模块) @@ -32,11 +36,13 @@ **决策**: 采用嵌套对象结构表示佣金配置 **理由**: + - `base_commission: { mode, value }` - 清晰表达基础返佣的两个维度 - `tier_config: { period_type, tier_type, tiers[] }` - 梯度配置与基础配置分离,可选性强 - `tiers: [{ threshold, mode, value }]` - 每个档位都有独立的返佣模式和值 **替代方案考虑**: + - ❌ 平铺所有字段 - 会导致字段过多,语义不清晰 - ❌ 使用JSON字符串存储配置 - 失去类型安全,不利于表单编辑 @@ -45,6 +51,7 @@ **决策**: 使用渐进式表单设计 **表单结构**: + ``` 1. 基础返佣配置 (必填) - 返佣模式: 单选 (固定金额/百分比) @@ -63,11 +70,13 @@ ``` **理由**: + - 渐进式设计降低初始复杂度 - 只有启用梯度返佣时才显示相关配置 - 动态档位列表提供灵活性 **替代方案考虑**: + - ❌ 全部平铺展示 - 对不需要梯度返佣的用户造成困扰 - ❌ 使用向导模式 - 增加操作步骤,不适合编辑场景 @@ -76,7 +85,9 @@ **决策**: 分层验证 + 条件验证 **验证规则**: + 1. 基础返佣配置: + - mode: 必选 - value: 必填,>= 0 @@ -91,6 +102,7 @@ - 档位阈值必须递增 **实现方式**: + - 使用 Element Plus 的表单验证 - 自定义validator处理档位阈值递增验证 - 使用 computed 动态生成验证规则 @@ -100,11 +112,13 @@ **决策**: 统一使用 `list` 字段名,添加适配层 **理由**: + - 后端统一规范使用 `list` 而非 `items` - 添加 `total_pages` 字段提供更完整的分页信息 - 保持前端代码与后端规范一致 **迁移策略**: + ```typescript // Before const res = await getShopSeriesAllocations(params) @@ -122,6 +136,7 @@ allocationList.value = res.data.list || [] **风险**: 前后端必须同时部署,否则会出现接口不兼容 **缓解措施**: + - 与后端团队协调部署窗口 - 准备回退方案(前端代码分支) - 在测试环境充分验证 @@ -131,6 +146,7 @@ allocationList.value = res.data.list || [] **风险**: 梯度返佣配置较复杂,用户可能不理解 **缓解措施**: + - 提供清晰的字段说明 - 添加示例或帮助文档链接 - 使用默认值简化初次配置 @@ -140,6 +156,7 @@ allocationList.value = res.data.list || [] **风险**: 旧数据无法完全转换为新模型 **缓解措施**: + - 要求后端提供数据迁移脚本和验证 - 在迁移后检查数据完整性 - 保留旧数据备份 @@ -147,22 +164,26 @@ allocationList.value = res.data.list || [] ## Migration Plan ### Phase 1: 准备阶段 + 1. 与后端确认API变更细节和时间表 2. 在开发环境实现前端变更 3. 与后端在测试环境联调 ### Phase 2: 测试阶段 + 1. 功能测试: 创建、编辑、删除、列表展示 2. 集成测试: 与后端API集成 3. 用户验收测试: 业务人员验证 ### Phase 3: 部署阶段 + 1. 准备回退方案 2. 与后端协调部署窗口 3. 同步部署前后端 4. 验证生产环境功能 ### Phase 4: 监控阶段 + 1. 监控API错误率 2. 收集用户反馈 3. 修复遗留问题 @@ -170,6 +191,7 @@ allocationList.value = res.data.list || [] ### Rollback Plan 如果部署后发现严重问题: + 1. 前端回退到上一版本 2. 后端回退API(如果可能) 3. 通知用户暂时不可用 @@ -178,12 +200,15 @@ allocationList.value = res.data.list || [] ## Open Questions 1. **Q**: 梯度返佣的档位数量是否有上限? + - **A**: 待后端确认,前端可以先不限制或设置合理上限(如10个) 2. **Q**: 返佣值的单位和精度如何处理? + - **A**: 固定金额使用"分"为单位,百分比使用千分比(如200=20%),待后端确认 3. **Q**: 是否需要在列表页显示梯度返佣信息? + - **A**: 暂时只显示基础返佣,梯度信息在详情或编辑时查看 4. **Q**: 旧数据如何映射到新模型? diff --git a/openspec/changes/update-series-allocation-commission/proposal.md b/openspec/changes/update-series-allocation-commission/proposal.md index f2daa82..64803f6 100644 --- a/openspec/changes/update-series-allocation-commission/proposal.md +++ b/openspec/changes/update-series-allocation-commission/proposal.md @@ -3,6 +3,7 @@ ## Why 当前套餐系列分配使用简单的定价模式(固定加价/百分比加价)和一次性佣金配置,不支持复杂的返佣规则。新的业务需求要求支持: + - 基础返佣配置(固定金额或百分比) - 梯度返佣系统(根据销量或销售额分档返佣) - 更灵活的佣金计算模型 @@ -32,12 +33,14 @@ ## Breaking Changes 1. **API响应结构变更**: + - 列表接口响应从 `{ items, page, page_size, total }` 改为 `{ list, page, page_size, total, total_pages }` - 移除 `pricing_mode`, `pricing_value`, `calculated_cost_price` 字段 - 移除一次性佣金相关字段 - 新增 `base_commission`, `enable_tier_commission` 字段 2. **API请求结构变更**: + - 创建/更新接口需要新的 `base_commission` 对象 - 支持可选的 `enable_tier_commission` 和 `tier_config` diff --git a/openspec/changes/update-series-allocation-commission/specs/package-series-allocation/spec.md b/openspec/changes/update-series-allocation-commission/specs/package-series-allocation/spec.md index d554d2d..b0f5cb2 100644 --- a/openspec/changes/update-series-allocation-commission/specs/package-series-allocation/spec.md +++ b/openspec/changes/update-series-allocation-commission/specs/package-series-allocation/spec.md @@ -129,15 +129,17 @@ **Reason**: 旧的定价模式 (pricing_mode, pricing_value) 已被新的佣金模型替代 **Migration**: 旧数据通过后端迁移脚本转换为基础返佣配置: + - pricing_mode="fixed" → base_commission.mode="fixed" - pricing_mode="percentage" → base_commission.mode="percent" - pricing_value → base_commission.value (需要单位转换) ### Requirement: 一次性佣金配置 -**Reason**: 一次性佣金配置 (one_time_commission_*) 已被梯度返佣系统替代 +**Reason**: 一次性佣金配置 (one*time_commission*\*) 已被梯度返佣系统替代 **Migration**: 旧的一次性佣金通过以下方式迁移: + - 如果设置了一次性佣金,转换为单档位的梯度返佣 - trigger → tier_type mapping (first_activation → sales_count, cumulative_recharge → sales_amount) - threshold → tiers[0].threshold @@ -165,7 +167,7 @@ - **WHEN** base_commission.mode = "percent" - **THEN** base_commission.value 表示返佣百分比的千分比 (如200表示20%) -- **AND** 每笔交易返佣 = 交易金额 * (value / 1000) +- **AND** 每笔交易返佣 = 交易金额 \* (value / 1000) ### Requirement: 梯度返佣配置 diff --git a/src/App.vue b/src/App.vue index c3fa636..149296d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -43,8 +43,9 @@ try { const res = await UserService.getUserInfo() if (res.code === ApiStatus.success && res.data) { - // API 返回的是 { user, permissions },我们需要保存 user + // API 返回的是 { user, permissions },我们需要保存 user 和 permissions userStore.setUserInfo(res.data.user) + userStore.setPermissions(res.data.permissions || []) } } catch (error) { console.error('获取用户信息失败:', error) diff --git a/src/api/modules/authorization.ts b/src/api/modules/authorization.ts index 9991b56..bb320ce 100644 --- a/src/api/modules/authorization.ts +++ b/src/api/modules/authorization.ts @@ -38,9 +38,6 @@ export class AuthorizationService extends BaseService { id: number, data: UpdateAuthorizationRemarkRequest ): Promise> { - return this.put>( - `/api/admin/authorizations/${id}/remark`, - data - ) + return this.put>(`/api/admin/authorizations/${id}/remark`, data) } } diff --git a/src/api/modules/device.ts b/src/api/modules/device.ts index f12f666..af6818f 100644 --- a/src/api/modules/device.ts +++ b/src/api/modules/device.ts @@ -77,10 +77,7 @@ export class DeviceService extends BaseService { id: number, data: BindCardToDeviceRequest ): Promise> { - return this.post>( - `/api/admin/devices/${id}/cards`, - data - ) + return this.post>(`/api/admin/devices/${id}/cards`, data) } /** @@ -106,19 +103,14 @@ export class DeviceService extends BaseService { static allocateDevices( data: AllocateDevicesRequest ): Promise> { - return this.post>( - '/api/admin/devices/allocate', - data - ) + return this.post>('/api/admin/devices/allocate', data) } /** * 批量回收设备 * @param data 回收参数 */ - static recallDevices( - data: RecallDevicesRequest - ): Promise> { + static recallDevices(data: RecallDevicesRequest): Promise> { return this.post>('/api/admin/devices/recall', data) } @@ -128,9 +120,7 @@ export class DeviceService extends BaseService { * 批量导入设备 * @param data 导入参数 */ - static importDevices( - data: ImportDeviceRequest - ): Promise> { + static importDevices(data: ImportDeviceRequest): Promise> { return this.post>('/api/admin/devices/import', data) } diff --git a/src/api/modules/enterprise.ts b/src/api/modules/enterprise.ts index 199c302..ab59b98 100644 --- a/src/api/modules/enterprise.ts +++ b/src/api/modules/enterprise.ts @@ -135,9 +135,7 @@ export class EnterpriseService extends BaseService { * @param cardId 卡ID */ static resumeCard(enterpriseId: number, cardId: number): Promise { - return this.post( - `/api/admin/enterprises/${enterpriseId}/cards/${cardId}/resume` - ) + return this.post(`/api/admin/enterprises/${enterpriseId}/cards/${cardId}/resume`) } /** @@ -146,9 +144,7 @@ export class EnterpriseService extends BaseService { * @param cardId 卡ID */ static suspendCard(enterpriseId: number, cardId: number): Promise { - return this.post( - `/api/admin/enterprises/${enterpriseId}/cards/${cardId}/suspend` - ) + return this.post(`/api/admin/enterprises/${enterpriseId}/cards/${cardId}/suspend`) } /** diff --git a/src/api/modules/packageManage.ts b/src/api/modules/packageManage.ts index 801198b..59a49b4 100644 --- a/src/api/modules/packageManage.ts +++ b/src/api/modules/packageManage.ts @@ -87,5 +87,4 @@ export class PackageManageService extends BaseService { const data: UpdatePackageShelfStatusRequest = { shelf_status } return this.patch(`/api/admin/packages/${id}/shelf`, data) } - } diff --git a/src/api/modules/packageSeries.ts b/src/api/modules/packageSeries.ts index d9a542d..e17579f 100644 --- a/src/api/modules/packageSeries.ts +++ b/src/api/modules/packageSeries.ts @@ -73,10 +73,7 @@ export class PackageSeriesService extends BaseService { * @param id 系列ID * @param status 状态 (1:启用, 2:禁用) */ - static updatePackageSeriesStatus( - id: number, - status: number - ): Promise { + static updatePackageSeriesStatus(id: number, status: number): Promise { const data: UpdatePackageSeriesStatusRequest = { status } return this.put(`/api/admin/package-series/${id}/status`, data) } diff --git a/src/api/modules/shopPackageAllocation.ts b/src/api/modules/shopPackageAllocation.ts index 741bb79..a43a301 100644 --- a/src/api/modules/shopPackageAllocation.ts +++ b/src/api/modules/shopPackageAllocation.ts @@ -36,10 +36,7 @@ export class ShopPackageAllocationService extends BaseService { static createShopPackageAllocation( data: CreateShopPackageAllocationRequest ): Promise> { - return this.create( - '/api/admin/shop-package-allocations', - data - ) + return this.create('/api/admin/shop-package-allocations', data) } /** @@ -50,9 +47,7 @@ export class ShopPackageAllocationService extends BaseService { static getShopPackageAllocationDetail( id: number ): Promise> { - return this.getOne( - `/api/admin/shop-package-allocations/${id}` - ) + return this.getOne(`/api/admin/shop-package-allocations/${id}`) } /** diff --git a/src/api/modules/shopSeriesAllocation.ts b/src/api/modules/shopSeriesAllocation.ts index af2752a..81b54e9 100644 --- a/src/api/modules/shopSeriesAllocation.ts +++ b/src/api/modules/shopSeriesAllocation.ts @@ -22,10 +22,7 @@ export class ShopSeriesAllocationService extends BaseService { static getShopSeriesAllocations( params?: ShopSeriesAllocationQueryParams ): Promise> { - return this.getPage( - '/api/admin/shop-series-allocations', - params - ) + return this.getPage('/api/admin/shop-series-allocations', params) } /** diff --git a/src/api/modules/storage.ts b/src/api/modules/storage.ts index 590f090..4321deb 100644 --- a/src/api/modules/storage.ts +++ b/src/api/modules/storage.ts @@ -94,8 +94,8 @@ export class StorageService extends BaseService { if (error.name === 'TypeError' && error.message.includes('Failed to fetch')) { throw new Error( 'CORS 错误: 无法上传文件到对象存储。' + - '这通常是因为对象存储服务器未正确配置 CORS 策略。' + - '请联系后端开发人员检查对象存储的 CORS 配置。' + '这通常是因为对象存储服务器未正确配置 CORS 策略。' + + '请联系后端开发人员检查对象存储的 CORS 配置。' ) } throw error diff --git a/src/assets/styles/app.scss b/src/assets/styles/app.scss index 041be08..34ca860 100644 --- a/src/assets/styles/app.scss +++ b/src/assets/styles/app.scss @@ -2,7 +2,15 @@ // 强制所有元素使用小米字体 * { - font-family: 'MiSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important; + font-family: + 'MiSans', + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + 'Helvetica Neue', + Arial, + sans-serif !important; } .btn-icon { diff --git a/src/assets/styles/el-ui.scss b/src/assets/styles/el-ui.scss index 4e623f9..4300a6b 100644 --- a/src/assets/styles/el-ui.scss +++ b/src/assets/styles/el-ui.scss @@ -10,7 +10,9 @@ // 按钮粗度 --el-font-weight-primary: 400 !important; // Element Plus 全局字体 - --el-font-family: 'MiSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important; + --el-font-family: + 'MiSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, + sans-serif !important; --el-component-custom-height: 36px !important; @@ -182,7 +184,15 @@ // 修改el-button样式 .el-button { - font-family: 'MiSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important; + font-family: + 'MiSans', + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + 'Helvetica Neue', + Arial, + sans-serif !important; &.el-button--text { background-color: transparent !important; @@ -202,7 +212,15 @@ border-radius: 6px !important; font-weight: bold; transition: all 0s !important; - font-family: 'MiSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important; + font-family: + 'MiSans', + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + 'Helvetica Neue', + Arial, + sans-serif !important; } // 为所有 Element Plus 组件添加小米字体 @@ -227,7 +245,15 @@ .el-upload, .el-card, .el-divider { - font-family: 'MiSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important; + font-family: + 'MiSans', + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + 'Helvetica Neue', + Arial, + sans-serif !important; } .el-checkbox-group { diff --git a/src/assets/styles/reset.scss b/src/assets/styles/reset.scss index c81c5ca..6cfb4b6 100644 --- a/src/assets/styles/reset.scss +++ b/src/assets/styles/reset.scss @@ -34,7 +34,15 @@ h5 { body { color: var(--art-text-gray-700); text-align: left; - font-family: 'MiSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + font-family: + 'MiSans', + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + 'Helvetica Neue', + Arial, + sans-serif; } select { diff --git a/src/composables/usePermission.ts b/src/composables/usePermission.ts new file mode 100644 index 0000000000000000000000000000000000000000..f0255909b8b851e09614cc5bea98db100cc5802b GIT binary patch literal 1428 zcmbW1ziJyn5XR$1wzhGZc8Xw~gq`gLGICT04vfGM0+y|He4yJs_D@I<*Ck!Z1H_do zcYX#J!nGg3o$)KUF@emU?sjjLSjg#aW@o + {} as Record ) /** diff --git a/src/directives/permission.ts b/src/directives/permission.ts index 0bb61c4..00b11ad 100644 --- a/src/directives/permission.ts +++ b/src/directives/permission.ts @@ -1,17 +1,41 @@ -import { router } from '@/router' -import { App, Directive } from 'vue' +import { App, Directive, DirectiveBinding } from 'vue' +import { useUserStore } from '@/store/modules/user' /** - * 权限指令(后端控制模式可用) + * 权限指令 * 用法: + * v-permission="'menu:get'" - 单个权限 + * v-permission="['menu:get', 'role:get']" - 多个权限(满足任意一个即可) + * v-permission:all="['menu:get', 'role:get']" - 多个权限(需要全部满足) + * + * 兼容旧的v-auth指令: * 按钮 */ -const authDirective: Directive = { +const permissionDirective: Directive = { mounted(el: HTMLElement, binding: DirectiveBinding) { - const authList = (router.currentRoute.value.meta.authList as Array<{ auth_mark: string }>) || [] + const userStore = useUserStore() + const { value, arg } = binding - const hasPermission = authList.some((item) => item.auth_mark === binding.value) + // 如果没有值,直接返回 + if (!value) return + let hasPermission = false + + if (typeof value === 'string') { + // 单个权限 + hasPermission = userStore.hasPermission(value) + } else if (Array.isArray(value)) { + // 多个权限 + if (arg === 'all') { + // 需要全部满足 + hasPermission = userStore.hasAllPermissions(value) + } else { + // 满足任意一个即可 + hasPermission = userStore.hasAnyPermission(value) + } + } + + // 如果没有权限,移除元素 if (!hasPermission) { el.parentNode?.removeChild(el) } @@ -19,5 +43,7 @@ const authDirective: Directive = { } export function setupPermissionDirective(app: App) { - app.directive('auth', authDirective) + // 注册为 v-permission 和 v-auth (向后兼容) + app.directive('permission', permissionDirective) + app.directive('auth', permissionDirective) } diff --git a/src/locales/langs/zh.json b/src/locales/langs/zh.json index 559f820..7bbc4b3 100644 --- a/src/locales/langs/zh.json +++ b/src/locales/langs/zh.json @@ -442,7 +442,6 @@ "standaloneCardList": "IoT卡管理", "iotCardTask": "IoT卡任务", "deviceTask": "设备任务", - "taskDetail": "任务详情", "devices": "设备管理", "deviceDetail": "设备详情", "assetAssign": "分配记录", @@ -473,13 +472,6 @@ "paymentMerchant": "支付商户", "developerApi": "开发者API", "commissionTemplate": "分佣模板" - }, - "batch": { - "title": "批量操作", - "simImport": "网卡导入", - "deviceImport": "设备导入", - "offlineBatchRecharge": "线下批量充值", - "cardChangeNotice": "换卡通知" } }, "table": { diff --git a/src/router/routes/asyncRoutes.ts b/src/router/routes/asyncRoutes.ts index 04dcd17..0afc907 100644 --- a/src/router/routes/asyncRoutes.ts +++ b/src/router/routes/asyncRoutes.ts @@ -890,24 +890,6 @@ export const asyncRoutes: AppRouteRecord[] = [ icon: '' }, children: [ - { - path: 'iot-card-query', - name: 'CardSearch', - component: RoutesAlias.CardSearch, - meta: { - title: 'menus.assetManagement.cardSearch', - keepAlive: true - } - }, - { - path: 'device-search', - name: 'DeviceSearch', - component: RoutesAlias.DeviceSearch, - meta: { - title: 'menus.assetManagement.deviceSearch', - keepAlive: true - } - }, { path: 'iot-card-management', name: 'StandaloneCardList', @@ -935,16 +917,6 @@ export const asyncRoutes: AppRouteRecord[] = [ keepAlive: true } }, - { - path: 'task-detail', - name: 'TaskDetail', - component: RoutesAlias.TaskDetail, - meta: { - title: 'menus.assetManagement.taskDetail', - isHide: true, - keepAlive: false - } - }, { path: 'devices', name: 'DeviceList', @@ -1129,7 +1101,7 @@ export const asyncRoutes: AppRouteRecord[] = [ ] } ] - }, + } // { // path: '/settings', // name: 'Settings', @@ -1167,52 +1139,5 @@ export const asyncRoutes: AppRouteRecord[] = [ // } // } // ] - // }, - { - path: '/batch', - name: 'Batch', - component: RoutesAlias.Home, - meta: { - title: 'menus.batch.title', - icon: '' - }, - children: [ - { - path: 'sim-import', - name: 'SimImport', - component: RoutesAlias.SimImport, - meta: { - title: 'menus.batch.simImport', - keepAlive: true - } - }, - { - path: 'device-import', - name: 'DeviceImport', - component: RoutesAlias.DeviceImport, - meta: { - title: 'menus.batch.deviceImport', - keepAlive: true - } - }, - // { - // path: 'offline-batch-recharge', - // name: 'OfflineBatchRecharge', - // component: RoutesAlias.OfflineBatchRecharge, - // meta: { - // title: 'menus.batch.offlineBatchRecharge', - // keepAlive: true - // } - // }, - // { - // path: 'card-change-notice', - // name: 'CardChangeNotice', - // component: RoutesAlias.CardChangeNotice, - // meta: { - // title: 'menus.batch.cardChangeNotice', - // keepAlive: true - // } - // } - ] - } + // } ] diff --git a/src/router/routesAlias.ts b/src/router/routesAlias.ts index 5f28e20..82dd3e6 100644 --- a/src/router/routesAlias.ts +++ b/src/router/routesAlias.ts @@ -91,12 +91,9 @@ export enum RoutesAlias { SimCardAssign = '/product/sim-card-assign', // 号卡分配 // 资产管理 - CardSearch = '/asset-management/iot-card-query', // IoT卡查询 - DeviceSearch = '/asset-management/device-search', // 设备查询 StandaloneCardList = '/asset-management/iot-card-management', // IoT卡管理 IotCardTask = '/asset-management/iot-card-task', // IoT卡任务 DeviceTask = '/asset-management/device-task', // 设备任务 - TaskDetail = '/asset-management/task-detail', // 任务详情 DeviceList = '/asset-management/device-list', // 设备列表 DeviceDetail = '/asset-management/device-detail', // 设备详情 AssetAssign = '/asset-management/asset-assign', // 资产分配(分配记录) @@ -121,12 +118,7 @@ export enum RoutesAlias { // 设置管理 PaymentMerchant = '/settings/payment-merchant', // 支付商户 DeveloperApi = '/settings/developer-api', // 开发者API - CommissionTemplate = '/settings/commission-template', // 分佣模板 - - // 批量操作 - SimImport = '/batch/sim-import', // 网卡批量导入 - DeviceImport = '/batch/device-import', // 设备批量导入 - CardChangeNotice = '/batch/card-change-notice' // 换卡通知 + CommissionTemplate = '/settings/commission-template' // 分佣模板 } // 主页路由 diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index accae4a..86f9bb0 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -23,15 +23,45 @@ export const useUserStore = defineStore( const searchHistory = ref([]) const accessToken = ref('') const refreshToken = ref('') + const permissions = ref([]) const getUserInfo = computed(() => info.value) const getSettingState = computed(() => useSettingStore().$state) const getWorktabState = computed(() => useWorktabStore().$state) + const getPermissions = computed(() => permissions.value) const setUserInfo = (newInfo: UserInfo) => { info.value = newInfo } + const setPermissions = (perms: string[]) => { + permissions.value = perms + } + + // 检查是否是超级管理员 + const isSuperAdmin = computed(() => info.value.user_type === 1) + + // 检查是否有某个权限 + const hasPermission = (permission: string): boolean => { + // 超级管理员拥有所有权限 + if (isSuperAdmin.value) return true + return permissions.value.includes(permission) + } + + // 检查是否有某些权限中的任意一个 + const hasAnyPermission = (perms: string[]): boolean => { + // 超级管理员拥有所有权限 + if (isSuperAdmin.value) return true + return perms.some((perm) => permissions.value.includes(perm)) + } + + // 检查是否有所有指定权限 + const hasAllPermissions = (perms: string[]): boolean => { + // 超级管理员拥有所有权限 + if (isSuperAdmin.value) return true + return perms.every((perm) => permissions.value.includes(perm)) + } + const setLoginStatus = (status: boolean) => { isLogin.value = status } @@ -74,6 +104,7 @@ export const useUserStore = defineStore( lockPassword.value = '' accessToken.value = '' refreshToken.value = '' + permissions.value = [] useWorktabStore().opened = [] sessionStorage.removeItem('iframeRoutes') resetRouterState(router) @@ -90,10 +121,17 @@ export const useUserStore = defineStore( searchHistory, accessToken, refreshToken, + permissions, getUserInfo, getSettingState, getWorktabState, + getPermissions, + isSuperAdmin, setUserInfo, + setPermissions, + hasPermission, + hasAnyPermission, + hasAllPermissions, setLoginStatus, setLanguage, setSearchHistory, diff --git a/src/types/auto-imports.d.ts b/src/types/auto-imports.d.ts index 13a3b2b..eeef0a6 100644 --- a/src/types/auto-imports.d.ts +++ b/src/types/auto-imports.d.ts @@ -6,7 +6,7 @@ // biome-ignore lint: disable export {} declare global { - const EffectScope: typeof import('vue')['EffectScope'] + const EffectScope: (typeof import('vue'))['EffectScope'] const ElButton: (typeof import('element-plus/es'))['ElButton'] const ElMessage: (typeof import('element-plus/es'))['ElMessage'] const ElMessageBox: (typeof import('element-plus/es'))['ElMessageBox'] @@ -14,304 +14,319 @@ declare global { const ElPopconfirm: (typeof import('element-plus/es'))['ElPopconfirm'] const ElPopover: (typeof import('element-plus/es'))['ElPopover'] const ElTableColumn: (typeof import('element-plus/es'))['ElTableColumn'] - const ElTag: typeof import('element-plus/es')['ElTag'] - const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] - const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] - const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] - const computed: typeof import('vue')['computed'] - const computedAsync: typeof import('@vueuse/core')['computedAsync'] - const computedEager: typeof import('@vueuse/core')['computedEager'] - const computedInject: typeof import('@vueuse/core')['computedInject'] - const computedWithControl: typeof import('@vueuse/core')['computedWithControl'] - const controlledComputed: typeof import('@vueuse/core')['controlledComputed'] - const controlledRef: typeof import('@vueuse/core')['controlledRef'] - const createApp: typeof import('vue')['createApp'] - const createEventHook: typeof import('@vueuse/core')['createEventHook'] - const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] - const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] - const createPinia: typeof import('pinia')['createPinia'] - const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] - const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate'] - const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] - const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise'] - const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] - const customRef: typeof import('vue')['customRef'] - const debouncedRef: typeof import('@vueuse/core')['debouncedRef'] - const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch'] - const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] - const defineComponent: typeof import('vue')['defineComponent'] - const defineStore: typeof import('pinia')['defineStore'] - const eagerComputed: typeof import('@vueuse/core')['eagerComputed'] - const effectScope: typeof import('vue')['effectScope'] - const extendRef: typeof import('@vueuse/core')['extendRef'] - const getActivePinia: typeof import('pinia')['getActivePinia'] - const getCurrentInstance: typeof import('vue')['getCurrentInstance'] - const getCurrentScope: typeof import('vue')['getCurrentScope'] - const h: typeof import('vue')['h'] - const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] - const inject: typeof import('vue')['inject'] - const injectLocal: typeof import('@vueuse/core')['injectLocal'] - const isDefined: typeof import('@vueuse/core')['isDefined'] - const isProxy: typeof import('vue')['isProxy'] - const isReactive: typeof import('vue')['isReactive'] - const isReadonly: typeof import('vue')['isReadonly'] - const isRef: typeof import('vue')['isRef'] - const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] - const mapActions: typeof import('pinia')['mapActions'] - const mapGetters: typeof import('pinia')['mapGetters'] - const mapState: typeof import('pinia')['mapState'] - const mapStores: typeof import('pinia')['mapStores'] - const mapWritableState: typeof import('pinia')['mapWritableState'] - const markRaw: typeof import('vue')['markRaw'] - const nextTick: typeof import('vue')['nextTick'] - const onActivated: typeof import('vue')['onActivated'] - const onBeforeMount: typeof import('vue')['onBeforeMount'] - const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] - const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] - const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] - const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] - const onClickOutside: typeof import('@vueuse/core')['onClickOutside'] - const onDeactivated: typeof import('vue')['onDeactivated'] - const onErrorCaptured: typeof import('vue')['onErrorCaptured'] - const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke'] - const onLongPress: typeof import('@vueuse/core')['onLongPress'] - const onMounted: typeof import('vue')['onMounted'] - const onRenderTracked: typeof import('vue')['onRenderTracked'] - const onRenderTriggered: typeof import('vue')['onRenderTriggered'] - const onScopeDispose: typeof import('vue')['onScopeDispose'] - const onServerPrefetch: typeof import('vue')['onServerPrefetch'] - const onStartTyping: typeof import('@vueuse/core')['onStartTyping'] - const onUnmounted: typeof import('vue')['onUnmounted'] - const onUpdated: typeof import('vue')['onUpdated'] - const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] - const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] - const provide: typeof import('vue')['provide'] - const provideLocal: typeof import('@vueuse/core')['provideLocal'] - const reactify: typeof import('@vueuse/core')['reactify'] - const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] - const reactive: typeof import('vue')['reactive'] - const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed'] - const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit'] - const reactivePick: typeof import('@vueuse/core')['reactivePick'] - const readonly: typeof import('vue')['readonly'] - const ref: typeof import('vue')['ref'] - const refAutoReset: typeof import('@vueuse/core')['refAutoReset'] - const refDebounced: typeof import('@vueuse/core')['refDebounced'] - const refDefault: typeof import('@vueuse/core')['refDefault'] - const refThrottled: typeof import('@vueuse/core')['refThrottled'] - const refWithControl: typeof import('@vueuse/core')['refWithControl'] - const resolveComponent: typeof import('vue')['resolveComponent'] - const resolveRef: typeof import('@vueuse/core')['resolveRef'] - const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] - const setActivePinia: typeof import('pinia')['setActivePinia'] - const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] - const shallowReactive: typeof import('vue')['shallowReactive'] - const shallowReadonly: typeof import('vue')['shallowReadonly'] - const shallowRef: typeof import('vue')['shallowRef'] - const storeToRefs: typeof import('pinia')['storeToRefs'] - const syncRef: typeof import('@vueuse/core')['syncRef'] - const syncRefs: typeof import('@vueuse/core')['syncRefs'] - const templateRef: typeof import('@vueuse/core')['templateRef'] - const throttledRef: typeof import('@vueuse/core')['throttledRef'] - const throttledWatch: typeof import('@vueuse/core')['throttledWatch'] - const toRaw: typeof import('vue')['toRaw'] - const toReactive: typeof import('@vueuse/core')['toReactive'] - const toRef: typeof import('vue')['toRef'] - const toRefs: typeof import('vue')['toRefs'] - const toValue: typeof import('vue')['toValue'] - const triggerRef: typeof import('vue')['triggerRef'] - const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount'] - const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount'] - const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted'] - const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose'] - const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted'] - const unref: typeof import('vue')['unref'] - const unrefElement: typeof import('@vueuse/core')['unrefElement'] - const until: typeof import('@vueuse/core')['until'] - const useActiveElement: typeof import('@vueuse/core')['useActiveElement'] - const useAnimate: typeof import('@vueuse/core')['useAnimate'] - const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference'] - const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery'] - const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter'] - const useArrayFind: typeof import('@vueuse/core')['useArrayFind'] - const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex'] - const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast'] - const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes'] - const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin'] - const useArrayMap: typeof import('@vueuse/core')['useArrayMap'] - const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce'] - const useArraySome: typeof import('@vueuse/core')['useArraySome'] - const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique'] - const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue'] - const useAsyncState: typeof import('@vueuse/core')['useAsyncState'] - const useAttrs: typeof import('vue')['useAttrs'] - const useBase64: typeof import('@vueuse/core')['useBase64'] - const useBattery: typeof import('@vueuse/core')['useBattery'] - const useBluetooth: typeof import('@vueuse/core')['useBluetooth'] - const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints'] - const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel'] - const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation'] - const useCached: typeof import('@vueuse/core')['useCached'] - const useClipboard: typeof import('@vueuse/core')['useClipboard'] - const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems'] - const useCloned: typeof import('@vueuse/core')['useCloned'] - const useColorMode: typeof import('@vueuse/core')['useColorMode'] - const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] - const useCounter: typeof import('@vueuse/core')['useCounter'] - const useCssModule: typeof import('vue')['useCssModule'] - const useCssVar: typeof import('@vueuse/core')['useCssVar'] - const useCssVars: typeof import('vue')['useCssVars'] - const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement'] - const useCycleList: typeof import('@vueuse/core')['useCycleList'] - const useDark: typeof import('@vueuse/core')['useDark'] - const useDateFormat: typeof import('@vueuse/core')['useDateFormat'] - const useDebounce: typeof import('@vueuse/core')['useDebounce'] - const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn'] - const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory'] - const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion'] - const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation'] - const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio'] - const useDevicesList: typeof import('@vueuse/core')['useDevicesList'] - const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia'] - const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility'] - const useDraggable: typeof import('@vueuse/core')['useDraggable'] - const useDropZone: typeof import('@vueuse/core')['useDropZone'] - const useElementBounding: typeof import('@vueuse/core')['useElementBounding'] - const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint'] - const useElementHover: typeof import('@vueuse/core')['useElementHover'] - const useElementSize: typeof import('@vueuse/core')['useElementSize'] - const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility'] - const useEventBus: typeof import('@vueuse/core')['useEventBus'] - const useEventListener: typeof import('@vueuse/core')['useEventListener'] - const useEventSource: typeof import('@vueuse/core')['useEventSource'] - const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper'] - const useFavicon: typeof import('@vueuse/core')['useFavicon'] - const useFetch: typeof import('@vueuse/core')['useFetch'] - const useFileDialog: typeof import('@vueuse/core')['useFileDialog'] - const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess'] - const useFocus: typeof import('@vueuse/core')['useFocus'] - const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin'] - const useFps: typeof import('@vueuse/core')['useFps'] - const useFullscreen: typeof import('@vueuse/core')['useFullscreen'] - const useGamepad: typeof import('@vueuse/core')['useGamepad'] - const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] - const useId: typeof import('vue')['useId'] - const useIdle: typeof import('@vueuse/core')['useIdle'] - const useImage: typeof import('@vueuse/core')['useImage'] - const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll'] - const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver'] - const useInterval: typeof import('@vueuse/core')['useInterval'] - const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn'] - const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier'] - const useLastChanged: typeof import('@vueuse/core')['useLastChanged'] - const useLink: typeof import('vue-router')['useLink'] - const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage'] - const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys'] - const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory'] - const useMediaControls: typeof import('@vueuse/core')['useMediaControls'] - const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery'] - const useMemoize: typeof import('@vueuse/core')['useMemoize'] - const useMemory: typeof import('@vueuse/core')['useMemory'] - const useModel: typeof import('vue')['useModel'] - const useMounted: typeof import('@vueuse/core')['useMounted'] - const useMouse: typeof import('@vueuse/core')['useMouse'] - const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement'] - const useMousePressed: typeof import('@vueuse/core')['useMousePressed'] - const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver'] - const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage'] - const useNetwork: typeof import('@vueuse/core')['useNetwork'] - const useNow: typeof import('@vueuse/core')['useNow'] - const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl'] - const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination'] - const useOnline: typeof import('@vueuse/core')['useOnline'] - const usePageLeave: typeof import('@vueuse/core')['usePageLeave'] - const useParallax: typeof import('@vueuse/core')['useParallax'] - const useParentElement: typeof import('@vueuse/core')['useParentElement'] - const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver'] - const usePermission: typeof import('@vueuse/core')['usePermission'] - const usePointer: typeof import('@vueuse/core')['usePointer'] - const usePointerLock: typeof import('@vueuse/core')['usePointerLock'] - const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe'] - const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme'] - const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast'] - const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark'] - const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages'] - const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion'] - const usePrevious: typeof import('@vueuse/core')['usePrevious'] - const useRafFn: typeof import('@vueuse/core')['useRafFn'] - const useRefHistory: typeof import('@vueuse/core')['useRefHistory'] - const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver'] - const useRoute: typeof import('vue-router')['useRoute'] - const useRouter: typeof import('vue-router')['useRouter'] - const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation'] - const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea'] - const useScriptTag: typeof import('@vueuse/core')['useScriptTag'] - const useScroll: typeof import('@vueuse/core')['useScroll'] - const useScrollLock: typeof import('@vueuse/core')['useScrollLock'] - const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage'] - const useShare: typeof import('@vueuse/core')['useShare'] - const useSlots: typeof import('vue')['useSlots'] - const useSorted: typeof import('@vueuse/core')['useSorted'] - const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition'] - const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis'] - const useStepper: typeof import('@vueuse/core')['useStepper'] - const useStorage: typeof import('@vueuse/core')['useStorage'] - const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync'] - const useStyleTag: typeof import('@vueuse/core')['useStyleTag'] - const useSupported: typeof import('@vueuse/core')['useSupported'] - const useSwipe: typeof import('@vueuse/core')['useSwipe'] - const useTemplateRef: typeof import('vue')['useTemplateRef'] - const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList'] - const useTextDirection: typeof import('@vueuse/core')['useTextDirection'] - const useTextSelection: typeof import('@vueuse/core')['useTextSelection'] - const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize'] - const useThrottle: typeof import('@vueuse/core')['useThrottle'] - const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn'] - const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory'] - const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo'] - const useTimeout: typeof import('@vueuse/core')['useTimeout'] - const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn'] - const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll'] - const useTimestamp: typeof import('@vueuse/core')['useTimestamp'] - const useTitle: typeof import('@vueuse/core')['useTitle'] - const useToNumber: typeof import('@vueuse/core')['useToNumber'] - const useToString: typeof import('@vueuse/core')['useToString'] - const useToggle: typeof import('@vueuse/core')['useToggle'] - const useTransition: typeof import('@vueuse/core')['useTransition'] - const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams'] - const useUserMedia: typeof import('@vueuse/core')['useUserMedia'] - const useVModel: typeof import('@vueuse/core')['useVModel'] - const useVModels: typeof import('@vueuse/core')['useVModels'] - const useVibrate: typeof import('@vueuse/core')['useVibrate'] - const useVirtualList: typeof import('@vueuse/core')['useVirtualList'] - const useWakeLock: typeof import('@vueuse/core')['useWakeLock'] - const useWebNotification: typeof import('@vueuse/core')['useWebNotification'] - const useWebSocket: typeof import('@vueuse/core')['useWebSocket'] - const useWebWorker: typeof import('@vueuse/core')['useWebWorker'] - const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn'] - const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus'] - const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll'] - const useWindowSize: typeof import('@vueuse/core')['useWindowSize'] - const watch: typeof import('vue')['watch'] - const watchArray: typeof import('@vueuse/core')['watchArray'] - const watchAtMost: typeof import('@vueuse/core')['watchAtMost'] - const watchDebounced: typeof import('@vueuse/core')['watchDebounced'] - const watchDeep: typeof import('@vueuse/core')['watchDeep'] - const watchEffect: typeof import('vue')['watchEffect'] - const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable'] - const watchImmediate: typeof import('@vueuse/core')['watchImmediate'] - const watchOnce: typeof import('@vueuse/core')['watchOnce'] - const watchPausable: typeof import('@vueuse/core')['watchPausable'] - const watchPostEffect: typeof import('vue')['watchPostEffect'] - const watchSyncEffect: typeof import('vue')['watchSyncEffect'] - const watchThrottled: typeof import('@vueuse/core')['watchThrottled'] - const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable'] - const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter'] - const whenever: typeof import('@vueuse/core')['whenever'] + const ElTag: (typeof import('element-plus/es'))['ElTag'] + const acceptHMRUpdate: (typeof import('pinia'))['acceptHMRUpdate'] + const asyncComputed: (typeof import('@vueuse/core'))['asyncComputed'] + const autoResetRef: (typeof import('@vueuse/core'))['autoResetRef'] + const computed: (typeof import('vue'))['computed'] + const computedAsync: (typeof import('@vueuse/core'))['computedAsync'] + const computedEager: (typeof import('@vueuse/core'))['computedEager'] + const computedInject: (typeof import('@vueuse/core'))['computedInject'] + const computedWithControl: (typeof import('@vueuse/core'))['computedWithControl'] + const controlledComputed: (typeof import('@vueuse/core'))['controlledComputed'] + const controlledRef: (typeof import('@vueuse/core'))['controlledRef'] + const createApp: (typeof import('vue'))['createApp'] + const createEventHook: (typeof import('@vueuse/core'))['createEventHook'] + const createGlobalState: (typeof import('@vueuse/core'))['createGlobalState'] + const createInjectionState: (typeof import('@vueuse/core'))['createInjectionState'] + const createPinia: (typeof import('pinia'))['createPinia'] + const createReactiveFn: (typeof import('@vueuse/core'))['createReactiveFn'] + const createReusableTemplate: (typeof import('@vueuse/core'))['createReusableTemplate'] + const createSharedComposable: (typeof import('@vueuse/core'))['createSharedComposable'] + const createTemplatePromise: (typeof import('@vueuse/core'))['createTemplatePromise'] + const createUnrefFn: (typeof import('@vueuse/core'))['createUnrefFn'] + const customRef: (typeof import('vue'))['customRef'] + const debouncedRef: (typeof import('@vueuse/core'))['debouncedRef'] + const debouncedWatch: (typeof import('@vueuse/core'))['debouncedWatch'] + const defineAsyncComponent: (typeof import('vue'))['defineAsyncComponent'] + const defineComponent: (typeof import('vue'))['defineComponent'] + const defineStore: (typeof import('pinia'))['defineStore'] + const eagerComputed: (typeof import('@vueuse/core'))['eagerComputed'] + const effectScope: (typeof import('vue'))['effectScope'] + const extendRef: (typeof import('@vueuse/core'))['extendRef'] + const getActivePinia: (typeof import('pinia'))['getActivePinia'] + const getCurrentInstance: (typeof import('vue'))['getCurrentInstance'] + const getCurrentScope: (typeof import('vue'))['getCurrentScope'] + const h: (typeof import('vue'))['h'] + const ignorableWatch: (typeof import('@vueuse/core'))['ignorableWatch'] + const inject: (typeof import('vue'))['inject'] + const injectLocal: (typeof import('@vueuse/core'))['injectLocal'] + const isDefined: (typeof import('@vueuse/core'))['isDefined'] + const isProxy: (typeof import('vue'))['isProxy'] + const isReactive: (typeof import('vue'))['isReactive'] + const isReadonly: (typeof import('vue'))['isReadonly'] + const isRef: (typeof import('vue'))['isRef'] + const makeDestructurable: (typeof import('@vueuse/core'))['makeDestructurable'] + const mapActions: (typeof import('pinia'))['mapActions'] + const mapGetters: (typeof import('pinia'))['mapGetters'] + const mapState: (typeof import('pinia'))['mapState'] + const mapStores: (typeof import('pinia'))['mapStores'] + const mapWritableState: (typeof import('pinia'))['mapWritableState'] + const markRaw: (typeof import('vue'))['markRaw'] + const nextTick: (typeof import('vue'))['nextTick'] + const onActivated: (typeof import('vue'))['onActivated'] + const onBeforeMount: (typeof import('vue'))['onBeforeMount'] + const onBeforeRouteLeave: (typeof import('vue-router'))['onBeforeRouteLeave'] + const onBeforeRouteUpdate: (typeof import('vue-router'))['onBeforeRouteUpdate'] + const onBeforeUnmount: (typeof import('vue'))['onBeforeUnmount'] + const onBeforeUpdate: (typeof import('vue'))['onBeforeUpdate'] + const onClickOutside: (typeof import('@vueuse/core'))['onClickOutside'] + const onDeactivated: (typeof import('vue'))['onDeactivated'] + const onErrorCaptured: (typeof import('vue'))['onErrorCaptured'] + const onKeyStroke: (typeof import('@vueuse/core'))['onKeyStroke'] + const onLongPress: (typeof import('@vueuse/core'))['onLongPress'] + const onMounted: (typeof import('vue'))['onMounted'] + const onRenderTracked: (typeof import('vue'))['onRenderTracked'] + const onRenderTriggered: (typeof import('vue'))['onRenderTriggered'] + const onScopeDispose: (typeof import('vue'))['onScopeDispose'] + const onServerPrefetch: (typeof import('vue'))['onServerPrefetch'] + const onStartTyping: (typeof import('@vueuse/core'))['onStartTyping'] + const onUnmounted: (typeof import('vue'))['onUnmounted'] + const onUpdated: (typeof import('vue'))['onUpdated'] + const onWatcherCleanup: (typeof import('vue'))['onWatcherCleanup'] + const pausableWatch: (typeof import('@vueuse/core'))['pausableWatch'] + const provide: (typeof import('vue'))['provide'] + const provideLocal: (typeof import('@vueuse/core'))['provideLocal'] + const reactify: (typeof import('@vueuse/core'))['reactify'] + const reactifyObject: (typeof import('@vueuse/core'))['reactifyObject'] + const reactive: (typeof import('vue'))['reactive'] + const reactiveComputed: (typeof import('@vueuse/core'))['reactiveComputed'] + const reactiveOmit: (typeof import('@vueuse/core'))['reactiveOmit'] + const reactivePick: (typeof import('@vueuse/core'))['reactivePick'] + const readonly: (typeof import('vue'))['readonly'] + const ref: (typeof import('vue'))['ref'] + const refAutoReset: (typeof import('@vueuse/core'))['refAutoReset'] + const refDebounced: (typeof import('@vueuse/core'))['refDebounced'] + const refDefault: (typeof import('@vueuse/core'))['refDefault'] + const refThrottled: (typeof import('@vueuse/core'))['refThrottled'] + const refWithControl: (typeof import('@vueuse/core'))['refWithControl'] + const resolveComponent: (typeof import('vue'))['resolveComponent'] + const resolveRef: (typeof import('@vueuse/core'))['resolveRef'] + const resolveUnref: (typeof import('@vueuse/core'))['resolveUnref'] + const setActivePinia: (typeof import('pinia'))['setActivePinia'] + const setMapStoreSuffix: (typeof import('pinia'))['setMapStoreSuffix'] + const shallowReactive: (typeof import('vue'))['shallowReactive'] + const shallowReadonly: (typeof import('vue'))['shallowReadonly'] + const shallowRef: (typeof import('vue'))['shallowRef'] + const storeToRefs: (typeof import('pinia'))['storeToRefs'] + const syncRef: (typeof import('@vueuse/core'))['syncRef'] + const syncRefs: (typeof import('@vueuse/core'))['syncRefs'] + const templateRef: (typeof import('@vueuse/core'))['templateRef'] + const throttledRef: (typeof import('@vueuse/core'))['throttledRef'] + const throttledWatch: (typeof import('@vueuse/core'))['throttledWatch'] + const toRaw: (typeof import('vue'))['toRaw'] + const toReactive: (typeof import('@vueuse/core'))['toReactive'] + const toRef: (typeof import('vue'))['toRef'] + const toRefs: (typeof import('vue'))['toRefs'] + const toValue: (typeof import('vue'))['toValue'] + const triggerRef: (typeof import('vue'))['triggerRef'] + const tryOnBeforeMount: (typeof import('@vueuse/core'))['tryOnBeforeMount'] + const tryOnBeforeUnmount: (typeof import('@vueuse/core'))['tryOnBeforeUnmount'] + const tryOnMounted: (typeof import('@vueuse/core'))['tryOnMounted'] + const tryOnScopeDispose: (typeof import('@vueuse/core'))['tryOnScopeDispose'] + const tryOnUnmounted: (typeof import('@vueuse/core'))['tryOnUnmounted'] + const unref: (typeof import('vue'))['unref'] + const unrefElement: (typeof import('@vueuse/core'))['unrefElement'] + const until: (typeof import('@vueuse/core'))['until'] + const useActiveElement: (typeof import('@vueuse/core'))['useActiveElement'] + const useAnimate: (typeof import('@vueuse/core'))['useAnimate'] + const useArrayDifference: (typeof import('@vueuse/core'))['useArrayDifference'] + const useArrayEvery: (typeof import('@vueuse/core'))['useArrayEvery'] + const useArrayFilter: (typeof import('@vueuse/core'))['useArrayFilter'] + const useArrayFind: (typeof import('@vueuse/core'))['useArrayFind'] + const useArrayFindIndex: (typeof import('@vueuse/core'))['useArrayFindIndex'] + const useArrayFindLast: (typeof import('@vueuse/core'))['useArrayFindLast'] + const useArrayIncludes: (typeof import('@vueuse/core'))['useArrayIncludes'] + const useArrayJoin: (typeof import('@vueuse/core'))['useArrayJoin'] + const useArrayMap: (typeof import('@vueuse/core'))['useArrayMap'] + const useArrayReduce: (typeof import('@vueuse/core'))['useArrayReduce'] + const useArraySome: (typeof import('@vueuse/core'))['useArraySome'] + const useArrayUnique: (typeof import('@vueuse/core'))['useArrayUnique'] + const useAsyncQueue: (typeof import('@vueuse/core'))['useAsyncQueue'] + const useAsyncState: (typeof import('@vueuse/core'))['useAsyncState'] + const useAttrs: (typeof import('vue'))['useAttrs'] + const useBase64: (typeof import('@vueuse/core'))['useBase64'] + const useBattery: (typeof import('@vueuse/core'))['useBattery'] + const useBluetooth: (typeof import('@vueuse/core'))['useBluetooth'] + const useBreakpoints: (typeof import('@vueuse/core'))['useBreakpoints'] + const useBroadcastChannel: (typeof import('@vueuse/core'))['useBroadcastChannel'] + const useBrowserLocation: (typeof import('@vueuse/core'))['useBrowserLocation'] + const useCached: (typeof import('@vueuse/core'))['useCached'] + const useClipboard: (typeof import('@vueuse/core'))['useClipboard'] + const useClipboardItems: (typeof import('@vueuse/core'))['useClipboardItems'] + const useCloned: (typeof import('@vueuse/core'))['useCloned'] + const useColorMode: (typeof import('@vueuse/core'))['useColorMode'] + const useConfirmDialog: (typeof import('@vueuse/core'))['useConfirmDialog'] + const useCounter: (typeof import('@vueuse/core'))['useCounter'] + const useCssModule: (typeof import('vue'))['useCssModule'] + const useCssVar: (typeof import('@vueuse/core'))['useCssVar'] + const useCssVars: (typeof import('vue'))['useCssVars'] + const useCurrentElement: (typeof import('@vueuse/core'))['useCurrentElement'] + const useCycleList: (typeof import('@vueuse/core'))['useCycleList'] + const useDark: (typeof import('@vueuse/core'))['useDark'] + const useDateFormat: (typeof import('@vueuse/core'))['useDateFormat'] + const useDebounce: (typeof import('@vueuse/core'))['useDebounce'] + const useDebounceFn: (typeof import('@vueuse/core'))['useDebounceFn'] + const useDebouncedRefHistory: (typeof import('@vueuse/core'))['useDebouncedRefHistory'] + const useDeviceMotion: (typeof import('@vueuse/core'))['useDeviceMotion'] + const useDeviceOrientation: (typeof import('@vueuse/core'))['useDeviceOrientation'] + const useDevicePixelRatio: (typeof import('@vueuse/core'))['useDevicePixelRatio'] + const useDevicesList: (typeof import('@vueuse/core'))['useDevicesList'] + const useDisplayMedia: (typeof import('@vueuse/core'))['useDisplayMedia'] + const useDocumentVisibility: (typeof import('@vueuse/core'))['useDocumentVisibility'] + const useDraggable: (typeof import('@vueuse/core'))['useDraggable'] + const useDropZone: (typeof import('@vueuse/core'))['useDropZone'] + const useElementBounding: (typeof import('@vueuse/core'))['useElementBounding'] + const useElementByPoint: (typeof import('@vueuse/core'))['useElementByPoint'] + const useElementHover: (typeof import('@vueuse/core'))['useElementHover'] + const useElementSize: (typeof import('@vueuse/core'))['useElementSize'] + const useElementVisibility: (typeof import('@vueuse/core'))['useElementVisibility'] + const useEventBus: (typeof import('@vueuse/core'))['useEventBus'] + const useEventListener: (typeof import('@vueuse/core'))['useEventListener'] + const useEventSource: (typeof import('@vueuse/core'))['useEventSource'] + const useEyeDropper: (typeof import('@vueuse/core'))['useEyeDropper'] + const useFavicon: (typeof import('@vueuse/core'))['useFavicon'] + const useFetch: (typeof import('@vueuse/core'))['useFetch'] + const useFileDialog: (typeof import('@vueuse/core'))['useFileDialog'] + const useFileSystemAccess: (typeof import('@vueuse/core'))['useFileSystemAccess'] + const useFocus: (typeof import('@vueuse/core'))['useFocus'] + const useFocusWithin: (typeof import('@vueuse/core'))['useFocusWithin'] + const useFps: (typeof import('@vueuse/core'))['useFps'] + const useFullscreen: (typeof import('@vueuse/core'))['useFullscreen'] + const useGamepad: (typeof import('@vueuse/core'))['useGamepad'] + const useGeolocation: (typeof import('@vueuse/core'))['useGeolocation'] + const useId: (typeof import('vue'))['useId'] + const useIdle: (typeof import('@vueuse/core'))['useIdle'] + const useImage: (typeof import('@vueuse/core'))['useImage'] + const useInfiniteScroll: (typeof import('@vueuse/core'))['useInfiniteScroll'] + const useIntersectionObserver: (typeof import('@vueuse/core'))['useIntersectionObserver'] + const useInterval: (typeof import('@vueuse/core'))['useInterval'] + const useIntervalFn: (typeof import('@vueuse/core'))['useIntervalFn'] + const useKeyModifier: (typeof import('@vueuse/core'))['useKeyModifier'] + const useLastChanged: (typeof import('@vueuse/core'))['useLastChanged'] + const useLink: (typeof import('vue-router'))['useLink'] + const useLocalStorage: (typeof import('@vueuse/core'))['useLocalStorage'] + const useMagicKeys: (typeof import('@vueuse/core'))['useMagicKeys'] + const useManualRefHistory: (typeof import('@vueuse/core'))['useManualRefHistory'] + const useMediaControls: (typeof import('@vueuse/core'))['useMediaControls'] + const useMediaQuery: (typeof import('@vueuse/core'))['useMediaQuery'] + const useMemoize: (typeof import('@vueuse/core'))['useMemoize'] + const useMemory: (typeof import('@vueuse/core'))['useMemory'] + const useModel: (typeof import('vue'))['useModel'] + const useMounted: (typeof import('@vueuse/core'))['useMounted'] + const useMouse: (typeof import('@vueuse/core'))['useMouse'] + const useMouseInElement: (typeof import('@vueuse/core'))['useMouseInElement'] + const useMousePressed: (typeof import('@vueuse/core'))['useMousePressed'] + const useMutationObserver: (typeof import('@vueuse/core'))['useMutationObserver'] + const useNavigatorLanguage: (typeof import('@vueuse/core'))['useNavigatorLanguage'] + const useNetwork: (typeof import('@vueuse/core'))['useNetwork'] + const useNow: (typeof import('@vueuse/core'))['useNow'] + const useObjectUrl: (typeof import('@vueuse/core'))['useObjectUrl'] + const useOffsetPagination: (typeof import('@vueuse/core'))['useOffsetPagination'] + const useOnline: (typeof import('@vueuse/core'))['useOnline'] + const usePageLeave: (typeof import('@vueuse/core'))['usePageLeave'] + const useParallax: (typeof import('@vueuse/core'))['useParallax'] + const useParentElement: (typeof import('@vueuse/core'))['useParentElement'] + const usePerformanceObserver: (typeof import('@vueuse/core'))['usePerformanceObserver'] + const usePermission: (typeof import('@vueuse/core'))['usePermission'] + const usePointer: (typeof import('@vueuse/core'))['usePointer'] + const usePointerLock: (typeof import('@vueuse/core'))['usePointerLock'] + const usePointerSwipe: (typeof import('@vueuse/core'))['usePointerSwipe'] + const usePreferredColorScheme: (typeof import('@vueuse/core'))['usePreferredColorScheme'] + const usePreferredContrast: (typeof import('@vueuse/core'))['usePreferredContrast'] + const usePreferredDark: (typeof import('@vueuse/core'))['usePreferredDark'] + const usePreferredLanguages: (typeof import('@vueuse/core'))['usePreferredLanguages'] + const usePreferredReducedMotion: (typeof import('@vueuse/core'))['usePreferredReducedMotion'] + const usePrevious: (typeof import('@vueuse/core'))['usePrevious'] + const useRafFn: (typeof import('@vueuse/core'))['useRafFn'] + const useRefHistory: (typeof import('@vueuse/core'))['useRefHistory'] + const useResizeObserver: (typeof import('@vueuse/core'))['useResizeObserver'] + const useRoute: (typeof import('vue-router'))['useRoute'] + const useRouter: (typeof import('vue-router'))['useRouter'] + const useScreenOrientation: (typeof import('@vueuse/core'))['useScreenOrientation'] + const useScreenSafeArea: (typeof import('@vueuse/core'))['useScreenSafeArea'] + const useScriptTag: (typeof import('@vueuse/core'))['useScriptTag'] + const useScroll: (typeof import('@vueuse/core'))['useScroll'] + const useScrollLock: (typeof import('@vueuse/core'))['useScrollLock'] + const useSessionStorage: (typeof import('@vueuse/core'))['useSessionStorage'] + const useShare: (typeof import('@vueuse/core'))['useShare'] + const useSlots: (typeof import('vue'))['useSlots'] + const useSorted: (typeof import('@vueuse/core'))['useSorted'] + const useSpeechRecognition: (typeof import('@vueuse/core'))['useSpeechRecognition'] + const useSpeechSynthesis: (typeof import('@vueuse/core'))['useSpeechSynthesis'] + const useStepper: (typeof import('@vueuse/core'))['useStepper'] + const useStorage: (typeof import('@vueuse/core'))['useStorage'] + const useStorageAsync: (typeof import('@vueuse/core'))['useStorageAsync'] + const useStyleTag: (typeof import('@vueuse/core'))['useStyleTag'] + const useSupported: (typeof import('@vueuse/core'))['useSupported'] + const useSwipe: (typeof import('@vueuse/core'))['useSwipe'] + const useTemplateRef: (typeof import('vue'))['useTemplateRef'] + const useTemplateRefsList: (typeof import('@vueuse/core'))['useTemplateRefsList'] + const useTextDirection: (typeof import('@vueuse/core'))['useTextDirection'] + const useTextSelection: (typeof import('@vueuse/core'))['useTextSelection'] + const useTextareaAutosize: (typeof import('@vueuse/core'))['useTextareaAutosize'] + const useThrottle: (typeof import('@vueuse/core'))['useThrottle'] + const useThrottleFn: (typeof import('@vueuse/core'))['useThrottleFn'] + const useThrottledRefHistory: (typeof import('@vueuse/core'))['useThrottledRefHistory'] + const useTimeAgo: (typeof import('@vueuse/core'))['useTimeAgo'] + const useTimeout: (typeof import('@vueuse/core'))['useTimeout'] + const useTimeoutFn: (typeof import('@vueuse/core'))['useTimeoutFn'] + const useTimeoutPoll: (typeof import('@vueuse/core'))['useTimeoutPoll'] + const useTimestamp: (typeof import('@vueuse/core'))['useTimestamp'] + const useTitle: (typeof import('@vueuse/core'))['useTitle'] + const useToNumber: (typeof import('@vueuse/core'))['useToNumber'] + const useToString: (typeof import('@vueuse/core'))['useToString'] + const useToggle: (typeof import('@vueuse/core'))['useToggle'] + const useTransition: (typeof import('@vueuse/core'))['useTransition'] + const useUrlSearchParams: (typeof import('@vueuse/core'))['useUrlSearchParams'] + const useUserMedia: (typeof import('@vueuse/core'))['useUserMedia'] + const useVModel: (typeof import('@vueuse/core'))['useVModel'] + const useVModels: (typeof import('@vueuse/core'))['useVModels'] + const useVibrate: (typeof import('@vueuse/core'))['useVibrate'] + const useVirtualList: (typeof import('@vueuse/core'))['useVirtualList'] + const useWakeLock: (typeof import('@vueuse/core'))['useWakeLock'] + const useWebNotification: (typeof import('@vueuse/core'))['useWebNotification'] + const useWebSocket: (typeof import('@vueuse/core'))['useWebSocket'] + const useWebWorker: (typeof import('@vueuse/core'))['useWebWorker'] + const useWebWorkerFn: (typeof import('@vueuse/core'))['useWebWorkerFn'] + const useWindowFocus: (typeof import('@vueuse/core'))['useWindowFocus'] + const useWindowScroll: (typeof import('@vueuse/core'))['useWindowScroll'] + const useWindowSize: (typeof import('@vueuse/core'))['useWindowSize'] + const watch: (typeof import('vue'))['watch'] + const watchArray: (typeof import('@vueuse/core'))['watchArray'] + const watchAtMost: (typeof import('@vueuse/core'))['watchAtMost'] + const watchDebounced: (typeof import('@vueuse/core'))['watchDebounced'] + const watchDeep: (typeof import('@vueuse/core'))['watchDeep'] + const watchEffect: (typeof import('vue'))['watchEffect'] + const watchIgnorable: (typeof import('@vueuse/core'))['watchIgnorable'] + const watchImmediate: (typeof import('@vueuse/core'))['watchImmediate'] + const watchOnce: (typeof import('@vueuse/core'))['watchOnce'] + const watchPausable: (typeof import('@vueuse/core'))['watchPausable'] + const watchPostEffect: (typeof import('vue'))['watchPostEffect'] + const watchSyncEffect: (typeof import('vue'))['watchSyncEffect'] + const watchThrottled: (typeof import('@vueuse/core'))['watchThrottled'] + const watchTriggerable: (typeof import('@vueuse/core'))['watchTriggerable'] + const watchWithFilter: (typeof import('@vueuse/core'))['watchWithFilter'] + const whenever: (typeof import('@vueuse/core'))['whenever'] } // for type re-export declare global { // @ts-ignore - export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' + export type { + Component, + ComponentPublicInstance, + ComputedRef, + DirectiveBinding, + ExtractDefaultPropTypes, + ExtractPropTypes, + ExtractPublicPropTypes, + InjectionKey, + PropType, + Ref, + MaybeRef, + MaybeRefOrGetter, + VNode, + WritableComputedRef + } from 'vue' import('vue') } diff --git a/src/views/account-management/account/index.vue b/src/views/account-management/account/index.vue index ca69d61..0d0b9f4 100644 --- a/src/views/account-management/account/index.vue +++ b/src/views/account-management/account/index.vue @@ -84,9 +84,9 @@ - -
- + +
+ {{ role.role_name }} {{ role.role_type === 1 ? '平台角色' : '客户角色' }} - +
- +