diff --git a/openspec/changes/add-device-management/design.md b/openspec/changes/add-device-management/design.md new file mode 100644 index 0000000..8248aff --- /dev/null +++ b/openspec/changes/add-device-management/design.md @@ -0,0 +1,278 @@ +# Device Management Design + +## Context + +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 +- **Task System**: Import tasks must integrate with the existing task-management infrastructure + +## Goals + +1. **Complete Lifecycle Management**: Support all device operations from import to deletion +2. **Seamless Integration**: Reuse existing UI patterns, services, and components +3. **Scalable Import**: Handle large CSV imports efficiently using object storage +4. **Operation Traceability**: Track all device operations through task management +5. **Consistent UX**: Match the look and feel of existing card-list and enterprise-customer pages + +## Key Decisions + +### Decision 1: Three-Step Object Storage Import + +**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 + +### Decision 2: Device-Card Binding Model + +**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 + +### Decision 3: Unified Task Management + +**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 + +### Decision 4: Batch Operation Result Display + +**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) + +**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 +- Follow ElDescriptions layout for detail pages + +## Architecture Diagrams + +### Device Import Flow +``` +┌─────────┐ 1. Select CSV ┌──────────────┐ +│ Admin │ ──────────────────> │ device-import│ +│ │ │ (Vue) │ +└─────────┘ └──────┬───────┘ + │ + │ 2. getUploadUrl() + v + ┌──────────────┐ + │ Storage │ + │ Service │ + └──────┬───────┘ + │ + │ 3. Upload to OSS + v + ┌──────────────┐ + │ Object │ + │ Storage │ + └──────────────┘ + │ + │ 4. importDevices(file_key) + v + ┌──────────────┐ + │ Device │ + │ Service │ + └──────┬───────┘ + │ + │ 5. Create Task + v + ┌──────────────┐ + │ Task │ + │ Management │ + └──────────────┘ +``` + +### Device-Card Binding +``` +┌─────────┐ ┌──────────────┐ +│ Device │ ───── bound to ────> │ Card │ +│ │ (1 to N) │ │ +└─────────┘ └──────────────┘ + │ │ + │ has_many bindings │ has_one binding + v v +┌─────────────────────────────────────────────┐ +│ DeviceCardBinding │ +│ - device_id │ +│ - card_id │ +│ - slot_position (1-4) │ +│ - bound_at │ +└─────────────────────────────────────────────┘ +``` + +## 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 +4. Call DeviceService.allocateDevices({ device_ids, shop_id, remarks }) +5. Backend processes each device, returns results array +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 +4. User selects card and slot (e.g., slot 3) +5. Call DeviceService.bindCard(device_id, { card_id, slot_position: 3 }) +6. Backend validates slot is empty, creates binding +7. Refresh device detail to show new binding + +## Migration Plan + +### 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 + - Add importDevices call with file_key +2. Remove mock import records +3. Link to task-management for tracking +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) + +### 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) + +## 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) +- Large CSV imports handled asynchronously (no frontend timeout) + +## Security Considerations + +1. **Authorization**: All device operations require admin role +2. **Input Validation**: + - Device number format validation + - Slot position range (1 to max_sim_slots) + - CSV file size limits +3. **Object Storage Security**: Pre-signed URLs expire after 15 minutes +4. **Batch Operation Limits**: Backend enforces max 1000 devices per batch +5. **XSS Prevention**: All user inputs sanitized before rendering + +## Open Questions + +None at this time. All major design decisions have been made based on existing system patterns and API specifications. diff --git a/openspec/changes/add-device-management/proposal.md b/openspec/changes/add-device-management/proposal.md new file mode 100644 index 0000000..386d6bd --- /dev/null +++ b/openspec/changes/add-device-management/proposal.md @@ -0,0 +1,60 @@ +# Change: 添加设备管理功能 + +## Why + +当前系统只有设备导入页面,缺少完整的设备管理能力。需要提供设备的全生命周期管理,包括: +- 查看和搜索设备列表 +- 查看设备详情和绑定的卡信息 +- 管理设备与卡的绑定关系 +- 批量分配和回收设备 +- 完善设备导入流程和任务追踪 + +这是物联网卡管理系统的核心资产管理功能之一。 + +## What Changes + +### 新增功能 +- **设备列表页面**: 支持多条件搜索、分页、列筛选、批量操作(分配、回收、删除) +- **设备详情页面**: 展示设备基本信息、绑定的卡列表、操作历史 +- **卡绑定管理**: 在设备详情页绑定/解绑卡 +- **批量分配对话框**: 选择设备批量分配给店铺 +- **批量回收对话框**: 从店铺批量回收设备 +- **设备导入改进**: 基于对象存储的三步导入流程 +- **导入任务管理**: 任务列表和详情查看 + +### API 集成 +- DeviceService: 11个API接口 +- 类型定义: Device, DeviceBinding, ImportTask 等 + +### UI 组件 +- 遵循现有组件模式 (ArtTableFullScreen, ArtSearchBar等) +- 复用 CommonStatus 统一状态变量 +- 使用 ElDescriptions、ElTag、ElSwitch 等组件 + +## Impact + +### 影响的功能模块 +- **新增**: 资产管理 / 设备列表(主列表页) +- **新增**: 资产管理 / 设备详情 +- **改进**: 批量操作 / 设备导入(改为对象存储模式) +- **新增**: 批量操作 / 导入任务列表(独立页面) + +### 影响的代码 +- `src/api/modules/device.ts` (新增) +- `src/types/api/device.ts` (新增) +- `src/views/asset-management/device-list/index.vue` (新增) +- `src/views/asset-management/device-detail/index.vue` (新增) +- `src/views/asset-management/task-management/index.vue` (修改:支持设备导入任务) +- `src/views/asset-management/task-detail/index.vue` (修改:支持设备导入任务详情) +- `src/views/batch/device-import/index.vue` (修改:使用对象存储) +- `src/router/routes/asyncRoutes.ts` (新增路由) +- `src/router/routesAlias.ts` (新增路由别名) +- `src/locales/langs/zh.json` (新增翻译) +- `src/api/modules/index.ts` (导出 DeviceService) +- `src/types/api/index.ts` (导出 Device 类型) + +### 依赖关系 +- 依赖现有的 StorageService (对象存储) +- 依赖现有的 ShopService (店铺选择) +- 设备与卡的关联管理 +- 任务管理页面需要支持多种任务类型(ICCID导入 + 设备导入) diff --git a/openspec/changes/add-device-management/specs/device-management/spec.md b/openspec/changes/add-device-management/specs/device-management/spec.md new file mode 100644 index 0000000..817ca28 --- /dev/null +++ b/openspec/changes/add-device-management/specs/device-management/spec.md @@ -0,0 +1,159 @@ +# Device Management Specification + +## ADDED Requirements + +### Requirement: Device List Management + +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 + +#### 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 + +#### 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 + +#### 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 + +#### 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 + +### 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 + +#### 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 + +#### 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 + +### 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 + +#### 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 + +#### 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 + +#### 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 + +### 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 + +#### 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 + +#### 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 + +### 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 + +#### 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 + +#### 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 + +### 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: +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 + +#### 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 + +#### 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 + +#### 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 + +### 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 + +#### 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 + +#### 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 + +#### 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 + +#### 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 + +## MODIFIED Requirements + +### Requirement: Task Type Support + +Task management SHALL support multiple task types including ICCID import and Device import with type-specific detail views. + +**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 + +#### 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 diff --git a/openspec/changes/add-device-management/tasks.md b/openspec/changes/add-device-management/tasks.md new file mode 100644 index 0000000..ebad95b --- /dev/null +++ b/openspec/changes/add-device-management/tasks.md @@ -0,0 +1,126 @@ +# Implementation Tasks + +## 1. 类型定义和API服务 (Foundation) + +- [ ] 1.1 创建 `src/types/api/device.ts` 定义所有设备相关类型 + - DeviceItem, DeviceQueryParams, DevicePageResult + - DeviceCardBinding, DeviceCardBindingList + - AllocateDevicesRequest/Response, RecallDevicesRequest/Response + - BindCardRequest/Response, UnbindCardResponse + - ImportDeviceRequest/Response, DeviceImportTask相关类型 +- [ ] 1.2 创建 `src/api/modules/device.ts` 实现 DeviceService + - getDevices (列表) + - getDeviceById (详情) + - deleteDevice (删除) + - getDeviceCards (绑定的卡列表) + - bindCard (绑定卡) + - unbindCard (解绑卡) + - allocateDevices (批量分配) + - recallDevices (批量回收) + - importDevices (批量导入) + - getImportTasks (导入任务列表) + - getImportTaskDetail (导入任务详情) +- [ ] 1.3 在 `src/api/modules/index.ts` 导出 DeviceService +- [ ] 1.4 在 `src/types/api/index.ts` 导出 Device 相关类型 + +## 2. 设备列表页面 (Core UI) + +- [ ] 2.1 创建 `src/views/asset-management/device-list/index.vue` + - 搜索栏:设备号、设备名称、状态、店铺、批次号、设备类型、制造商、创建时间范围 + - 表格:展示设备信息,支持勾选、列筛选 + - 表格列:ID、设备号、设备名称、设备型号、设备类型、制造商、最大插槽数、已绑定卡数、状态、店铺、批次号、激活时间、创建时间 + - 状态使用 CommonStatus + ElSwitch 显示 + - 操作列:查看详情、删除按钮(使用 ArtButtonTable) +- [ ] 2.2 实现批量操作按钮 + - 批量分配:弹出对话框选择目标店铺 + - 批量回收:弹出对话框确认回收 + - 导入设备:跳转到设备导入页面 +- [ ] 2.3 实现批量分配对话框 + - 选择目标店铺(下拉搜索) + - 显示已选设备数量 + - 备注输入 + - 展示分配结果(成功/失败) +- [ ] 2.4 实现批量回收对话框 + - 确认回收设备列表 + - 备注输入 + - 展示回收结果(成功/失败) + +## 3. 设备详情页面 (Detail View) + +- [ ] 3.1 创建 `src/views/asset-management/device-detail/index.vue` + - 设备基本信息卡片(ElDescriptions) + - 绑定的卡列表表格(展示4个插槽位置) + - 返回按钮 +- [ ] 3.2 实现卡绑定管理 + - 绑定卡按钮:弹出对话框选择卡和插槽位置 + - 解绑按钮:每个绑定记录旁边 + - 显示插槽位置(1-4) + - 展示卡的基本信息(ICCID、手机号、运营商、状态) + +## 4. 任务管理改进 (Task Management) + +- [ ] 4.1 修改 `src/views/asset-management/task-management/index.vue` + - 添加任务类型筛选:ICCID导入 / 设备导入 + - 支持展示设备导入任务 + - 任务列表显示任务类型标签 +- [ ] 4.2 修改 `src/views/asset-management/task-detail/index.vue` + - 根据任务类型展示不同的详情内容 + - 设备导入任务:显示设备号、绑定的ICCID、失败原因 + - 失败和跳过记录展示 + +## 5. 设备导入改进 (Import Enhancement) + +- [ ] 5.1 修改 `src/views/batch/device-import/index.vue` + - 改用对象存储三步上传流程 + 1. 获取上传URL (StorageService.getUploadUrl) + 2. 上传文件到对象存储 (StorageService.uploadFile) + 3. 调用导入接口 (DeviceService.importDevices) + - CSV格式说明:device_no, device_name, device_model, device_type, max_sim_slots, manufacturer, iccid_1~iccid_4, batch_no + - 导入成功后显示任务编号,引导去任务管理查看 +- [ ] 5.2 优化导入记录展示 + - 移除mock数据,改为真实API调用 + - 点击"查看详情"跳转到任务详情页 + +## 6. 路由和导航 (Routing) + +- [ ] 6.1 在 `src/router/routes/asyncRoutes.ts` 添加路由 + - /asset-management/device-list (设备列表) + - /asset-management/device-detail (设备详情,带query参数id) +- [ ] 6.2 在 `src/router/routesAlias.ts` 添加路由别名 + - DeviceList = '/asset-management/device-list' + - DeviceDetail = '/asset-management/device-detail' +- [ ] 6.3 在 `src/locales/langs/zh.json` 添加翻译 + - assetManagement.deviceList: "设备列表" + - assetManagement.deviceDetail: "设备详情" + +## 7. 测试和优化 (Testing & Polish) + +- [ ] 7.1 功能测试 + - 设备列表的搜索、分页、排序 + - 批量分配和回收流程 + - 设备与卡的绑定/解绑 + - 设备导入完整流程 + - 任务状态查看 +- [ ] 7.2 边界情况处理 + - 空列表状态 + - 加载失败提示 + - 删除确认提示 + - 表单验证 +- [ ] 7.3 性能优化 + - 列表数据懒加载 + - 防抖搜索 + - 批量操作结果分页展示 +- [ ] 7.4 代码审查 + - TypeScript类型完整性 + - 错误处理 + - 代码复用性 + - 注释完整性 + +## Dependencies + +- 1.x 必须先完成才能开始 2.x-6.x +- 2.1-2.2 可以并行开发 +- 3.x 依赖 1.x,可与 2.x 并行 +- 4.x 和 5.x 依赖 1.x,可与其他任务并行 +- 6.x 可在对应页面开发完成后立即进行 +- 7.x 在所有功能开发完成后进行 diff --git a/package-lock.json b/package-lock.json index 644a73d..1e196b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "vue-ts-vite-demo", + "name": "internet-of-things-admin", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "vue-ts-vite-demo", + "name": "internet-of-things-admin", "version": "0.0.0", "dependencies": { "@element-plus/icons-vue": "^2.3.1", @@ -13,6 +13,7 @@ "@vueuse/core": "^11.0.0", "@wangeditor/editor": "^5.1.23", "@wangeditor/editor-for-vue": "next", + "aws-sdk": "^2.1693.0", "axios": "^1.7.5", "crypto-js": "^4.2.0", "echarts": "^5.4.0", @@ -43,18 +44,18 @@ "@vitejs/plugin-vue": "^5.2.1", "@vue/compiler-sfc": "^3.0.5", "commitizen": "^4.3.0", - "cz-git": "^1.9.4", + "cz-git": "^1.11.1", "eslint": "^9.9.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-vue": "^9.27.0", "globals": "^15.9.0", "husky": "^9.1.5", - "lint-staged": "^15.2.10", - "prettier": "^3.3.3", + "lint-staged": "^15.5.2", + "prettier": "^3.5.3", "rollup-plugin-visualizer": "^5.12.0", "sass": "^1.81.0", - "stylelint": "^16.8.2", + "stylelint": "^16.20.0", "stylelint-config-html": "^1.1.0", "stylelint-config-recess-order": "^4.6.0", "stylelint-config-recommended-scss": "^14.1.0", @@ -146,6 +147,67 @@ "node": ">=6.9.0" } }, + "node_modules/@cacheable/memory": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/@cacheable/memory/-/memory-2.0.7.tgz", + "integrity": "sha512-RbxnxAMf89Tp1dLhXMS7ceft/PGsDl1Ip7T20z5nZ+pwIAsQ1p2izPjVG69oCLv/jfQ7HDPHTWK0c9rcAWXN3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.3.3", + "@keyv/bigmap": "^1.3.0", + "hookified": "^1.14.0", + "keyv": "^5.5.5" + } + }, + "node_modules/@cacheable/memory/node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/memory/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/@cacheable/utils/-/utils-2.3.3.tgz", + "integrity": "sha512-JsXDL70gQ+1Vc2W/KUFfkAJzgb4puKwwKehNLuB+HrNKWf91O736kGfxn4KujXCCSuh6mRRL4XEB0PkAFjWS0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.3.0", + "keyv": "^5.5.5" + } + }, + "node_modules/@cacheable/utils/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, "node_modules/@codemirror/autocomplete": { "version": "6.18.6", "resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", @@ -762,6 +824,23 @@ "@csstools/css-tokenizer": "^3.0.4" } }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.26", + "resolved": "https://registry.npmmirror.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.26.tgz", + "integrity": "sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0" + }, "node_modules/@csstools/css-tokenizer": { "version": "3.0.4", "resolved": "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", @@ -813,13 +892,14 @@ } }, "node_modules/@dual-bundle/import-meta-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", - "integrity": "sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==", + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.2.1.tgz", + "integrity": "sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==", "dev": true, + "license": "MIT", "funding": { "type": "github", - "url": "https://github.com/sponsors/wooorm" + "url": "https://github.com/sponsors/JounQin" } }, "node_modules/@element-plus/icons-vue": { @@ -1618,37 +1698,11 @@ } }, "node_modules/@keyv/serialize": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/@keyv/serialize/-/serialize-1.0.3.tgz", - "integrity": "sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==", + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", "dev": true, - "dependencies": { - "buffer": "^6.0.3" - } - }, - "node_modules/@keyv/serialize/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmmirror.com/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } + "license": "MIT" }, "node_modules/@lezer/common": { "version": "1.2.3", @@ -3645,6 +3699,69 @@ "node": ">= 4.0.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sdk": { + "version": "2.1693.0", + "resolved": "https://registry.npmmirror.com/aws-sdk/-/aws-sdk-2.1693.0.tgz", + "integrity": "sha512-cJmb8xEnVLT+R6fBS5sn/EFJiX7tUnDaPtOPZ1vFbOJtd0fnZn/Ky2XGgsvvoeliWeH7mL3TWSX5zXXGSQV6gQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/aws-sdk/node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "license": "BSD-3-Clause" + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/axios": { "version": "1.9.0", "resolved": "https://registry.npmmirror.com/axios/-/axios-1.9.0.tgz", @@ -3665,7 +3782,6 @@ "version": "1.5.1", "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -4424,13 +4540,17 @@ } }, "node_modules/cacheable": { - "version": "1.9.0", - "resolved": "https://registry.npmmirror.com/cacheable/-/cacheable-1.9.0.tgz", - "integrity": "sha512-8D5htMCxPDUULux9gFzv30f04Xo3wCnik0oOxKoRTPIBoqA7HtOcJ87uBhQTs3jCfZZTrUBGsYIZOgE0ZRgMAg==", + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/cacheable/-/cacheable-2.3.2.tgz", + "integrity": "sha512-w+ZuRNmex9c1TR9RcsxbfTKCjSL0rh1WA5SABbrWprIHeNBdmyQLSYonlDy9gpD+63XT8DgZ/wNh1Smvc9WnJA==", "dev": true, + "license": "MIT", "dependencies": { - "hookified": "^1.8.2", - "keyv": "^5.3.3" + "@cacheable/memory": "^2.0.7", + "@cacheable/utils": "^2.3.3", + "hookified": "^1.15.0", + "keyv": "^5.5.5", + "qified": "^0.6.0" } }, "node_modules/cacheable-request": { @@ -4482,12 +4602,13 @@ } }, "node_modules/cacheable/node_modules/keyv": { - "version": "5.3.3", - "resolved": "https://registry.npmmirror.com/keyv/-/keyv-5.3.3.tgz", - "integrity": "sha512-Rwu4+nXI9fqcxiEHtbkvoes2X+QfkTRo1TMkPfwzipGsJlJO/z69vqB4FNl9xJ3xCpAcbkvmEabZfPzrwN3+gQ==", + "version": "5.6.0", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, + "license": "MIT", "dependencies": { - "@keyv/serialize": "^1.0.3" + "@keyv/serialize": "^1.1.1" } }, "node_modules/cachedir": { @@ -4499,6 +4620,24 @@ "node": ">=6" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -4511,6 +4650,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", @@ -5405,10 +5560,11 @@ "dev": true }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -5634,6 +5790,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -6939,6 +7112,15 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "dev": true }, + "node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "license": "MIT", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/exec-buffer": { "version": "3.2.0", "resolved": "https://registry.npmmirror.com/exec-buffer/-/exec-buffer-3.2.0.tgz", @@ -7485,6 +7667,21 @@ } } }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/form-data": { "version": "4.0.2", "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.2.tgz", @@ -7596,6 +7793,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -8108,6 +8314,18 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbol-support-x": { "version": "1.4.2", "resolved": "https://registry.npmmirror.com/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", @@ -8154,6 +8372,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hashery": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/hashery/-/hashery-1.4.0.tgz", + "integrity": "sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.14.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", @@ -8200,10 +8431,11 @@ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==" }, "node_modules/hookified": { - "version": "1.9.0", - "resolved": "https://registry.npmmirror.com/hookified/-/hookified-1.9.0.tgz", - "integrity": "sha512-2yEEGqphImtKIe1NXWEhu6yD3hlFR4Mxk4Mtp3XEyScpSt4pQ4ymmXA1zzxZpj99QkFK+nN0nzjeb2+RUi/6CQ==", - "dev": true + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/hookified/-/hookified-1.15.0.tgz", + "integrity": "sha512-51w+ZZGt7Zw5q7rM3nC4t3aLn/xvKDETsXqMczndvwyVQhAHfUmUuFBRFcos8Iyebtk7OAE9dL26wFNzZVVOkw==", + "dev": true, + "license": "MIT" }, "node_modules/hosted-git-info": { "version": "2.8.9", @@ -8323,9 +8555,10 @@ ] }, "node_modules/ignore": { - "version": "7.0.4", - "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.4.tgz", - "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "version": "7.0.5", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", "engines": { "node": ">= 4" } @@ -8970,8 +9203,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { "version": "4.1.1", @@ -9070,6 +9302,22 @@ "node": ">=4" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -9088,6 +9336,18 @@ "node": ">=8" } }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", @@ -9169,6 +9429,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-gif": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/is-gif/-/is-gif-3.0.0.tgz", @@ -9284,6 +9563,24 @@ "node": ">=8" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-retry-allowed": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", @@ -9332,6 +9629,21 @@ "node": ">=8" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -9390,8 +9702,7 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "node_modules/isexe": { "version": "2.0.0", @@ -9420,6 +9731,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmmirror.com/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/jpegtran-bin": { "version": "6.0.1", "resolved": "https://registry.npmmirror.com/jpegtran-bin/-/jpegtran-bin-6.0.1.tgz", @@ -11485,10 +11805,19 @@ "node": ">=6" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -11503,8 +11832,9 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -11730,6 +12060,19 @@ "node": ">=6" } }, + "node_modules/qified": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/qified/-/qified-0.6.0.tgz", + "integrity": "sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.14.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/qrcode.vue": { "version": "3.6.0", "resolved": "https://registry.npmmirror.com/qrcode.vue/-/qrcode.vue-3.6.0.tgz", @@ -11767,6 +12110,15 @@ "node": ">=0.10.0" } }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -12213,6 +12565,23 @@ } ] }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -12239,6 +12608,12 @@ "@parcel/watcher": "^2.4.1" } }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", + "license": "ISC" + }, "node_modules/scroll-into-view-if-needed": { "version": "2.2.31", "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz", @@ -12312,6 +12687,23 @@ "semver": "bin/semver" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", @@ -12799,9 +13191,9 @@ "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" }, "node_modules/stylelint": { - "version": "16.19.1", - "resolved": "https://registry.npmmirror.com/stylelint/-/stylelint-16.19.1.tgz", - "integrity": "sha512-C1SlPZNMKl+d/C867ZdCRthrS+6KuZ3AoGW113RZCOL0M8xOGpgx7G70wq7lFvqvm4dcfdGFVLB/mNaLFChRKw==", + "version": "16.26.1", + "resolved": "https://registry.npmmirror.com/stylelint/-/stylelint-16.26.1.tgz", + "integrity": "sha512-v20V59/crfc8sVTAtge0mdafI3AdnzQ2KsWe6v523L4OA1bJO02S7MO2oyXDCS6iWb9ckIPnqAFVItqSBQr7jw==", "dev": true, "funding": [ { @@ -12813,35 +13205,37 @@ "url": "https://github.com/sponsors/stylelint" } ], + "license": "MIT", "dependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/media-query-list-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-syntax-patches-for-csstree": "^1.0.19", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3", "@csstools/selector-specificity": "^5.0.0", - "@dual-bundle/import-meta-resolve": "^4.1.0", + "@dual-bundle/import-meta-resolve": "^4.2.1", "balanced-match": "^2.0.0", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.3", "css-tree": "^3.1.0", - "debug": "^4.3.7", + "debug": "^4.4.3", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", - "file-entry-cache": "^10.0.8", + "file-entry-cache": "^11.1.1", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", "html-tags": "^3.3.1", - "ignore": "^7.0.3", + "ignore": "^7.0.5", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", - "known-css-properties": "^0.36.0", + "known-css-properties": "^0.37.0", "mathml-tag-names": "^2.1.3", "meow": "^13.2.0", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", - "postcss": "^8.5.3", + "postcss": "^8.5.6", "postcss-resolve-nested-selector": "^0.1.6", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.0", @@ -13062,23 +13456,25 @@ "dev": true }, "node_modules/stylelint/node_modules/file-entry-cache": { - "version": "10.1.0", - "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-10.1.0.tgz", - "integrity": "sha512-Et/ex6smi3wOOB+n5mek+Grf7P2AxZR5ueqRUvAAn4qkyatXi3cUC1cuQXVkX0VlzBVsN4BkWJFmY/fYiRTdww==", + "version": "11.1.2", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-11.1.2.tgz", + "integrity": "sha512-N2WFfK12gmrK1c1GXOqiAJ1tc5YE+R53zvQ+t5P8S5XhnmKYVB5eZEiLNZKDSmoG8wqqbF9EXYBBW/nef19log==", "dev": true, + "license": "MIT", "dependencies": { - "flat-cache": "^6.1.9" + "flat-cache": "^6.1.20" } }, "node_modules/stylelint/node_modules/flat-cache": { - "version": "6.1.9", - "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-6.1.9.tgz", - "integrity": "sha512-DUqiKkTlAfhtl7g78IuwqYM+YqvT+as0mY+EVk6mfimy19U79pJCzDZQsnqk3Ou/T6hFXWLGbwbADzD/c8Tydg==", + "version": "6.1.20", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-6.1.20.tgz", + "integrity": "sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==", "dev": true, + "license": "MIT", "dependencies": { - "cacheable": "^1.9.0", + "cacheable": "^2.3.2", "flatted": "^3.3.3", - "hookified": "^1.8.2" + "hookified": "^1.15.0" } }, "node_modules/stylelint/node_modules/global-modules": { @@ -13113,6 +13509,13 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, + "node_modules/stylelint/node_modules/known-css-properties": { + "version": "0.37.0", + "resolved": "https://registry.npmmirror.com/known-css-properties/-/known-css-properties-0.37.0.tgz", + "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", + "dev": true, + "license": "MIT" + }, "node_modules/stylelint/node_modules/meow": { "version": "13.2.0", "resolved": "https://registry.npmmirror.com/meow/-/meow-13.2.0.tgz", @@ -14081,6 +14484,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmmirror.com/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "license": "MIT", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, "node_modules/url-parse-lax": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz", @@ -14102,6 +14515,25 @@ "node": ">= 4" } }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", + "license": "MIT" + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmmirror.com/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -14706,6 +15138,27 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wildcard": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-1.1.2.tgz", @@ -14870,6 +15323,28 @@ "node": ">=12" } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmmirror.com/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xss": { "version": "1.0.15", "resolved": "https://registry.npmmirror.com/xss/-/xss-1.0.15.tgz", diff --git a/package.json b/package.json index 5ee9500..38648af 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@vueuse/core": "^11.0.0", "@wangeditor/editor": "^5.1.23", "@wangeditor/editor-for-vue": "next", + "aws-sdk": "^2.1693.0", "axios": "^1.7.5", "crypto-js": "^4.2.0", "echarts": "^5.4.0", diff --git a/src/api/BaseService.ts b/src/api/BaseService.ts index d1308a3..e5e5ccd 100644 --- a/src/api/BaseService.ts +++ b/src/api/BaseService.ts @@ -170,16 +170,16 @@ export class BaseService { if (params) { Object.keys(params).forEach((key) => { - formData.append(key, params[key]) + if (params[key] !== undefined && params[key] !== null && params[key] !== '') { + formData.append(key, String(params[key])) + } }) } + // 不要手动设置 Content-Type,让浏览器自动设置(包含正确的 boundary) return request.post>({ url, - data: formData, - headers: { - 'Content-Type': 'multipart/form-data' - } + data: formData }) } diff --git a/src/api/modules/authorization.ts b/src/api/modules/authorization.ts new file mode 100644 index 0000000..9991b56 --- /dev/null +++ b/src/api/modules/authorization.ts @@ -0,0 +1,46 @@ +/** + * 授权记录相关 API + */ + +import { BaseService } from '../BaseService' +import type { BaseResponse, PaginationResponse } from '@/types/api' +import type { + AuthorizationItem, + AuthorizationListParams, + UpdateAuthorizationRemarkRequest +} from '@/types/api/authorization' + +export class AuthorizationService extends BaseService { + /** + * 获取授权记录列表 + * @param params 查询参数 + */ + static getAuthorizations( + params?: AuthorizationListParams + ): Promise> { + return this.getPage('/api/admin/authorizations', params) + } + + /** + * 获取授权记录详情 + * @param id 授权记录ID + */ + static getAuthorizationDetail(id: number): Promise> { + return this.getOne(`/api/admin/authorizations/${id}`) + } + + /** + * 修改授权备注 + * @param id 授权记录ID + * @param data 备注数据 + */ + static updateAuthorizationRemark( + id: number, + data: UpdateAuthorizationRemarkRequest + ): Promise> { + return this.put>( + `/api/admin/authorizations/${id}/remark`, + data + ) + } +} diff --git a/src/api/modules/card.ts b/src/api/modules/card.ts index b30326d..0016224 100644 --- a/src/api/modules/card.ts +++ b/src/api/modules/card.ts @@ -273,23 +273,18 @@ export class CardService extends BaseService { // ========== ICCID批量导入相关 ========== /** - * 批量导入ICCID - * @param file Excel文件 - * @param carrier_id 运营商ID - * @param batch_no 批次号(可选) + * 批量导入ICCID(新版:使用 JSON 格式) + * @param data 导入请求参数 */ - static importIotCards( - file: File, - carrier_id: number, + static importIotCards(data: { + carrier_id: number + file_key: string batch_no?: string - ): Promise { - const formData = new FormData() - formData.append('file', file) - formData.append('carrier_id', carrier_id.toString()) - if (batch_no) { - formData.append('batch_no', batch_no) - } - return this.upload('/api/admin/iot-cards/import', file, { carrier_id, batch_no }) + }): Promise> { + return this.post>( + '/api/admin/iot-cards/import', + data + ) } /** diff --git a/src/api/modules/device.ts b/src/api/modules/device.ts new file mode 100644 index 0000000..d9ae608 --- /dev/null +++ b/src/api/modules/device.ts @@ -0,0 +1,149 @@ +/** + * 设备管理相关 API + */ + +import { BaseService } from '../BaseService' +import type { + Device, + DeviceQueryParams, + DeviceListResponse, + DeviceCardsResponse, + BindCardToDeviceRequest, + BindCardToDeviceResponse, + UnbindCardFromDeviceResponse, + AllocateDevicesRequest, + AllocateDevicesResponse, + RecallDevicesRequest, + RecallDevicesResponse, + ImportDeviceRequest, + ImportDeviceResponse, + DeviceImportTaskQueryParams, + DeviceImportTaskListResponse, + DeviceImportTaskDetail, + BaseResponse +} from '@/types/api' + +export class DeviceService extends BaseService { + // ========== 设备基础管理 ========== + + /** + * 获取设备列表 + * @param params 查询参数 + */ + static getDevices(params?: DeviceQueryParams): Promise> { + return this.get>('/api/admin/devices', params) + } + + /** + * 获取设备详情 + * @param id 设备ID + */ + static getDeviceById(id: number): Promise> { + return this.getOne(`/api/admin/devices/${id}`) + } + + /** + * 删除设备 + * @param id 设备ID + */ + static deleteDevice(id: number): Promise { + return this.remove(`/api/admin/devices/${id}`) + } + + // ========== 设备卡绑定管理 ========== + + /** + * 获取设备绑定的卡列表 + * @param id 设备ID + */ + static getDeviceCards(id: number): Promise> { + return this.getOne(`/api/admin/devices/${id}/cards`) + } + + /** + * 绑定卡到设备 + * @param id 设备ID + * @param data 绑定参数 + */ + static bindCard( + id: number, + data: BindCardToDeviceRequest + ): Promise> { + return this.post>( + `/api/admin/devices/${id}/cards`, + data + ) + } + + /** + * 解绑设备上的卡 + * @param deviceId 设备ID + * @param cardId IoT卡ID + */ + static unbindCard( + deviceId: number, + cardId: number + ): Promise> { + return this.delete>( + `/api/admin/devices/${deviceId}/cards/${cardId}` + ) + } + + // ========== 批量分配和回收 ========== + + /** + * 批量分配设备 + * @param data 分配参数 + */ + static allocateDevices( + data: AllocateDevicesRequest + ): Promise> { + return this.post>( + '/api/admin/devices/allocate', + data + ) + } + + /** + * 批量回收设备 + * @param data 回收参数 + */ + static recallDevices( + data: RecallDevicesRequest + ): Promise> { + return this.post>('/api/admin/devices/recall', data) + } + + // ========== 设备导入 ========== + + /** + * 批量导入设备 + * @param data 导入参数 + */ + static importDevices( + data: ImportDeviceRequest + ): Promise> { + return this.post>('/api/admin/devices/import', data) + } + + /** + * 获取导入任务列表 + * @param params 查询参数 + */ + static getImportTasks( + params?: DeviceImportTaskQueryParams + ): Promise> { + return this.get>( + '/api/admin/devices/import/tasks', + params + ) + } + + /** + * 获取导入任务详情 + * @param id 任务ID + */ + static getImportTaskDetail(id: number): Promise> { + return this.getOne(`/api/admin/devices/import/tasks/${id}`) + } +} diff --git a/src/api/modules/enterprise.ts b/src/api/modules/enterprise.ts index 1a484bb..196e85a 100644 --- a/src/api/modules/enterprise.ts +++ b/src/api/modules/enterprise.ts @@ -14,6 +14,16 @@ import type { UpdateEnterpriseStatusParams, CreateEnterpriseResponse } from '@/types/api/enterprise' +import type { + AllocateCardsRequest, + AllocateCardsResponse, + AllocateCardsPreviewRequest, + AllocateCardsPreviewResponse, + EnterpriseCardListParams, + EnterpriseCardPageResult, + RecallCardsRequest, + RecallCardsResponse +} from '@/types/api/enterpriseCard' export class EnterpriseService extends BaseService { /** @@ -63,4 +73,88 @@ export class EnterpriseService extends BaseService { ): Promise { return this.put(`/api/admin/enterprises/${id}/status`, data) } + + // ========== 企业卡授权相关 ========== + + /** + * 授权卡给企业 + * @param enterpriseId 企业ID + * @param data 授权请求数据 + */ + static allocateCards( + enterpriseId: number, + data: AllocateCardsRequest + ): Promise> { + return this.post>( + `/api/admin/enterprises/${enterpriseId}/allocate-cards`, + data + ) + } + + /** + * 卡授权预检 + * @param enterpriseId 企业ID + * @param data 预检请求数据 + */ + static previewAllocateCards( + enterpriseId: number, + data: AllocateCardsPreviewRequest + ): Promise> { + return this.post>( + `/api/admin/enterprises/${enterpriseId}/allocate-cards/preview`, + data + ) + } + + /** + * 获取企业卡列表 + * @param enterpriseId 企业ID + * @param params 查询参数 + */ + static getEnterpriseCards( + enterpriseId: number, + params?: EnterpriseCardListParams + ): Promise> { + return this.get>( + `/api/admin/enterprises/${enterpriseId}/cards`, + params + ) + } + + /** + * 复机卡 + * @param enterpriseId 企业ID + * @param cardId 卡ID + */ + static resumeCard(enterpriseId: number, cardId: number): Promise { + return this.post( + `/api/admin/enterprises/${enterpriseId}/cards/${cardId}/resume` + ) + } + + /** + * 停机卡 + * @param enterpriseId 企业ID + * @param cardId 卡ID + */ + static suspendCard(enterpriseId: number, cardId: number): Promise { + return this.post( + `/api/admin/enterprises/${enterpriseId}/cards/${cardId}/suspend` + ) + } + + /** + * 回收卡授权 + * @param enterpriseId 企业ID + * @param data 回收请求数据 + */ + static recallCards( + enterpriseId: number, + data: RecallCardsRequest + ): Promise> { + return this.post>( + `/api/admin/enterprises/${enterpriseId}/recall-cards`, + data + ) + } } diff --git a/src/api/modules/index.ts b/src/api/modules/index.ts index 41d14ad..7533151 100644 --- a/src/api/modules/index.ts +++ b/src/api/modules/index.ts @@ -17,8 +17,10 @@ export { CardService } from './card' export { CommissionService } from './commission' export { EnterpriseService } from './enterprise' export { CustomerAccountService } from './customerAccount' +export { StorageService } from './storage' +export { AuthorizationService } from './authorization' +export { DeviceService } from './device' // TODO: 按需添加其他业务模块 // export { PackageService } from './package' -// export { DeviceService } from './device' // export { SettingService } from './setting' diff --git a/src/api/modules/storage.ts b/src/api/modules/storage.ts new file mode 100644 index 0000000..590f090 --- /dev/null +++ b/src/api/modules/storage.ts @@ -0,0 +1,104 @@ +/** + * 对象存储相关 API + */ + +import { BaseService } from '../BaseService' +import type { BaseResponse } from '@/types/api' + +/** + * 文件用途枚举 + */ +export type FilePurpose = 'iot_import' | 'export' | 'attachment' + +/** + * 获取上传 URL 请求参数 + */ +export interface GetUploadUrlRequest { + /** 文件名(如:cards.csv) */ + file_name: string + /** 文件 MIME 类型(如:text/csv),留空则自动推断 */ + content_type?: string + /** 文件用途 (iot_import:ICCID导入, export:数据导出, attachment:附件) */ + purpose: FilePurpose +} + +/** + * 获取上传 URL 响应 + */ +export interface GetUploadUrlResponse { + /** 预签名上传 URL,使用 PUT 方法上传文件 */ + upload_url: string + /** 文件路径标识,上传成功后用于调用业务接口 */ + file_key: string + /** URL 有效期(秒) */ + expires_in: number +} + +export class StorageService extends BaseService { + /** + * 获取文件上传预签名 URL + * + * ## 完整上传流程 + * 1. 调用本接口获取预签名 URL 和 file_key + * 2. 使用预签名 URL 上传文件(发起 PUT 请求直接上传到对象存储) + * 3. 使用 file_key 调用相关业务接口 + * + * @param data 请求参数 + */ + static getUploadUrl(data: GetUploadUrlRequest): Promise> { + return this.post>('/api/admin/storage/upload-url', data) + } + + /** + * 使用预签名 URL 上传文件到对象存储 + * + * 注意事项: + * - 预签名 URL 有效期 15 分钟,请及时使用 + * - 上传时 Content-Type 需与请求时一致 + * - file_key 在上传成功后永久有效 + * + * @param uploadUrl 预签名 URL + * @param file 文件 + * @param contentType 文件类型(需与 getUploadUrl 请求时保持一致) + */ + static async uploadFile(uploadUrl: string, file: File, contentType?: string): Promise { + try { + // 在开发环境下,使用代理路径避免 CORS 问题 + let finalUrl = uploadUrl + if (import.meta.env.DEV) { + // 将对象存储的域名替换为代理路径 + // 例如:http://obs-helf.cucloud.cn/cmp/... -> /obs-proxy/cmp/... + finalUrl = uploadUrl.replace(/^https?:\/\/obs-helf\.cucloud\.cn/, '/obs-proxy') + } + + const headers: Record = {} + + // 只有在明确指定 contentType 时才设置,否则让浏览器自动处理 + if (contentType) { + headers['Content-Type'] = contentType + } + + const response = await fetch(finalUrl, { + method: 'PUT', + body: file, + headers, + mode: 'cors' // 明确指定 CORS 模式 + }) + + if (!response.ok) { + const errorText = await response.text().catch(() => response.statusText) + throw new Error(`上传文件失败 (${response.status}): ${errorText}`) + } + } catch (error: any) { + // 增强错误信息 + if (error.name === 'TypeError' && error.message.includes('Failed to fetch')) { + throw new Error( + 'CORS 错误: 无法上传文件到对象存储。' + + '这通常是因为对象存储服务器未正确配置 CORS 策略。' + + '请联系后端开发人员检查对象存储的 CORS 配置。' + ) + } + throw error + } + } +} diff --git a/src/locales/langs/zh.json b/src/locales/langs/zh.json index a8d2d46..713322d 100644 --- a/src/locales/langs/zh.json +++ b/src/locales/langs/zh.json @@ -417,6 +417,7 @@ "agent": "代理商管理", "customerAccount": "客户账号", "enterpriseCustomer": "企业客户", + "enterpriseCards": "企业卡管理", "customerCommission": "客户账号佣金" }, "deviceManagement": { @@ -439,8 +440,11 @@ "taskManagement": "任务管理", "taskDetail": "任务详情", "devices": "设备管理", + "deviceDetail": "设备详情", "assetAssign": "分配记录", - "cardReplacementRequest": "换卡申请" + "cardReplacementRequest": "换卡申请", + "authorizationRecords": "授权记录", + "authorizationDetail": "授权记录详情" }, "account": { "title": "账户管理", diff --git a/src/router/routes/asyncRoutes.ts b/src/router/routes/asyncRoutes.ts index 931686b..479f46b 100644 --- a/src/router/routes/asyncRoutes.ts +++ b/src/router/routes/asyncRoutes.ts @@ -756,6 +756,16 @@ export const asyncRoutes: AppRouteRecord[] = [ title: 'menus.accountManagement.customerAccount', keepAlive: true } + }, + { + path: 'enterprise-cards', + name: 'EnterpriseCards', + component: RoutesAlias.EnterpriseCards, + meta: { + title: 'menus.accountManagement.enterpriseCards', + isHide: true, + keepAlive: false + } } ] }, @@ -879,6 +889,16 @@ export const asyncRoutes: AppRouteRecord[] = [ keepAlive: true } }, + { + path: 'device-detail', + name: 'DeviceDetail', + component: RoutesAlias.DeviceDetail, + meta: { + title: 'menus.assetManagement.deviceDetail', + isHide: true, + keepAlive: false + } + }, { path: 'asset-assign', name: 'AssetAssign', @@ -906,6 +926,25 @@ export const asyncRoutes: AppRouteRecord[] = [ title: 'menus.assetManagement.cardReplacementRequest', keepAlive: true } + }, + { + path: 'authorization-records', + name: 'AuthorizationRecords', + component: RoutesAlias.AuthorizationRecords, + meta: { + title: 'menus.assetManagement.authorizationRecords', + keepAlive: true + } + }, + { + path: 'authorization-detail', + name: 'AuthorizationDetail', + component: RoutesAlias.AuthorizationDetail, + meta: { + title: 'menus.assetManagement.authorizationDetail', + isHide: true, + keepAlive: false + } } ] }, @@ -997,7 +1036,7 @@ export const asyncRoutes: AppRouteRecord[] = [ ] } ] - } + }, // { // path: '/settings', // name: 'Settings', @@ -1036,51 +1075,51 @@ 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 - // } - // } - // ] - // } + { + 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 3649c77..b047146 100644 --- a/src/router/routesAlias.ts +++ b/src/router/routesAlias.ts @@ -82,11 +82,9 @@ export enum RoutesAlias { CustomerAccount = '/account-management/customer-account', // 客户账号管理 ShopAccount = '/account-management/shop-account', // 代理账号管理 EnterpriseCustomer = '/account-management/enterprise-customer', // 企业客户管理 + EnterpriseCards = '/account-management/enterprise-cards', // 企业卡管理 CustomerCommission = '/account-management/customer-commission', // 客户账号佣金 - // 设备管理 - DeviceList = '/device-management/devices', // 设备管理 - // 产品管理 SimCardManagement = '/product/sim-card', // 网卡产品管理 SimCardAssign = '/product/sim-card-assign', // 号卡分配 @@ -95,9 +93,13 @@ export enum RoutesAlias { StandaloneCardList = '/asset-management/card-list', // 单卡列表(未绑定设备) TaskManagement = '/asset-management/task-management', // 任务管理 TaskDetail = '/asset-management/task-detail', // 任务详情 + DeviceList = '/asset-management/device-list', // 设备列表 + DeviceDetail = '/asset-management/device-detail', // 设备详情 AssetAssign = '/asset-management/asset-assign', // 资产分配(分配记录) AllocationRecordDetail = '/asset-management/allocation-record-detail', // 分配记录详情 CardReplacementRequest = '/asset-management/card-replacement-request', // 换卡申请 + AuthorizationRecords = '/asset-management/authorization-records', // 授权记录 + AuthorizationDetail = '/asset-management/authorization-detail', // 授权记录详情 // 账户管理 CustomerAccountList = '/finance/customer-account', // 客户账号 diff --git a/src/types/api/authorization.ts b/src/types/api/authorization.ts new file mode 100644 index 0000000..744f3b6 --- /dev/null +++ b/src/types/api/authorization.ts @@ -0,0 +1,99 @@ +/** + * 授权记录相关类型定义 + */ + +/** + * 授权人类型 + */ +export enum AuthorizerType { + PLATFORM = 2, // 平台 + AGENT = 3 // 代理 +} + +/** + * 授权状态 + */ +export enum AuthorizationStatus { + REVOKED = 0, // 已回收 + ACTIVE = 1 // 有效 +} + +/** + * 授权记录项 + */ +export interface AuthorizationItem { + /** 授权记录ID */ + id: number + /** 卡ID */ + card_id: number + /** ICCID */ + iccid: string + /** 手机号 */ + msisdn: string + /** 企业ID */ + enterprise_id: number + /** 企业名称 */ + enterprise_name: string + /** 授权人ID */ + authorized_by: number + /** 授权人名称 */ + authorizer_name: string + /** 授权人类型:2=平台,3=代理 */ + authorizer_type: AuthorizerType + /** 授权时间 */ + authorized_at: string + /** 回收人ID */ + revoked_by?: number | null + /** 回收人名称 */ + revoker_name?: string + /** 回收时间 */ + revoked_at?: string | null + /** 状态:1=有效,0=已回收 */ + status: AuthorizationStatus + /** 备注 */ + remark?: string +} + +/** + * 授权记录列表查询参数 + */ +export interface AuthorizationListParams { + /** 页码 */ + page?: number + /** 每页数量 */ + page_size?: number + /** 按企业ID筛选 */ + enterprise_id?: number + /** 按ICCID模糊查询 */ + iccid?: string + /** 授权人类型:2=平台,3=代理 */ + authorizer_type?: AuthorizerType + /** 状态:0=已回收,1=有效 */ + status?: AuthorizationStatus + /** 授权时间起(格式:2006-01-02) */ + start_time?: string + /** 授权时间止(格式:2006-01-02) */ + end_time?: string +} + +/** + * 授权记录列表响应 + */ +export interface AuthorizationListResponse { + /** 授权记录列表 */ + items: AuthorizationItem[] + /** 当前页码 */ + page: number + /** 每页数量 */ + size: number + /** 总记录数 */ + total: number +} + +/** + * 修改授权备注请求 + */ +export interface UpdateAuthorizationRemarkRequest { + /** 备注(最多500字) */ + remark: string +} diff --git a/src/types/api/device.ts b/src/types/api/device.ts index d1b8f85..c0699ad 100644 --- a/src/types/api/device.ts +++ b/src/types/api/device.ts @@ -1,143 +1,204 @@ /** - * 设备相关类型定义 + * 设备管理相关类型定义 */ -import { PaginationParams, ImportTask } from './common' +import { PaginationParams } from './common' -// 设备状态 +// ========== 设备状态枚举 ========== + +// 设备状态枚举 export enum DeviceStatus { - ONLINE = 'online', // 在线 - OFFLINE = 'offline', // 离线 - FAULT = 'fault', // 故障 - MAINTENANCE = 'maintenance' // 维护中 + IN_STOCK = 1, // 在库 + DISTRIBUTED = 2, // 已分销 + ACTIVATED = 3, // 已激活 + DEACTIVATED = 4 // 已停用 } -// 设备操作类型 -export enum DeviceOperationType { - RESTART = 'restart', // 重启 - RESET = 'reset', // 重置 - UPGRADE = 'upgrade', // 升级 - CONFIG = 'config', // 配置 - BIND_CARD = 'bindCard', // 绑定网卡 - UNBIND_CARD = 'unbindCard' // 解绑网卡 -} +// ========== 设备基础类型 ========== -// 设备实体 +// 设备信息 export interface Device { - id: string | number - deviceCode: string // 设备编码 - deviceName?: string // 设备名称 - deviceType?: string // 设备类型 - model?: string // 设备型号 - manufacturer?: string // 制造商 - // 网卡信息 - currentIccid?: string // 当前绑定的ICCID - currentOperator?: string // 当前运营商 - currentImei?: string // IMEI - // 卡槽信息(双卡设备) - card1Iccid?: string - card1Operator?: string - card2Iccid?: string - card2Operator?: string - // 状态信息 - status: DeviceStatus - onlineStatus: boolean // 在线状态 - lastOnlineTime?: string // 最后在线时间 - // 所属信息 - agentId?: string | number - agentName?: string - // 位置信息 - location?: string - latitude?: number - longitude?: number - // 其他信息 - firmwareVersion?: string // 固件版本 - hardwareVersion?: string // 硬件版本 - activateTime?: string // 激活时间 - createTime: string - updateTime?: string + id: number // 设备ID + device_no: string // 设备号 + device_name: string // 设备名称 + device_model: string // 设备型号 + device_type: string // 设备类型 + manufacturer: string // 制造商 + max_sim_slots: number // 最大插槽数 + bound_card_count: number // 已绑定卡数量 + status: DeviceStatus // 状态 (1:在库, 2:已分销, 3:已激活, 4:已停用) + status_name: string // 状态名称 + shop_id: number | null // 店铺ID + shop_name: string // 店铺名称 + batch_no: string // 批次号 + activated_at: string | null // 激活时间 + created_at: string // 创建时间 + updated_at: string // 更新时间 } // 设备查询参数 export interface DeviceQueryParams extends PaginationParams { - keyword?: string // 设备编码/名称/ICCID - deviceType?: string - status?: DeviceStatus - onlineStatus?: boolean - agentId?: string | number - hasCard?: boolean // 是否绑定网卡 - operator?: string + device_no?: string // 设备号(模糊查询) + device_name?: string // 设备名称(模糊查询) + status?: DeviceStatus // 状态 + shop_id?: number | null // 店铺ID (NULL表示平台库存) + batch_no?: string // 批次号 + device_type?: string // 设备类型 + manufacturer?: string // 制造商(模糊查询) + created_at_start?: string // 创建时间起始 + created_at_end?: string // 创建时间结束 } -// 设备操作参数 -export interface DeviceOperationParams { - deviceId: string | number - operation: DeviceOperationType - iccid?: string // 绑定/解绑网卡时需要 - config?: Record // 配置参数 - remark?: string +// 设备列表响应 +export interface DeviceListResponse { + list: Device[] | null // 设备列表 + page: number // 当前页码 + page_size: number // 每页数量 + total: number // 总数 + total_pages: number // 总页数 } -// 设备卡信息 -export interface DeviceCardInfo { - deviceId: string | number - deviceCode: string - // 主卡信息 - mainCard?: { - iccid: string - operator: string - imei?: string - status: string - signal?: number // 信号强度 - flow?: { - total: number - used: number - remain: number - } - } - // 副卡信息(双卡设备) - viceCard?: { - iccid: string - operator: string - imei?: string - status: string - signal?: number - flow?: { - total: number - used: number - remain: number - } - } +// ========== 设备卡绑定相关 ========== + +// 设备卡绑定信息 +export interface DeviceCardBinding { + id: number // 绑定记录ID + iot_card_id: number // IoT卡ID + iccid: string // ICCID + msisdn: string // 接入号 + carrier_name: string // 运营商名称 + slot_position: number // 插槽位置 (1-4) + status: number // 卡状态 (1:在库, 2:已分销, 3:已激活, 4:已停用) + bind_time: string | null // 绑定时间 } -// 修改设备卡信息参数 -export interface UpdateDeviceCardParams { - deviceId: string | number - mainCardIccid?: string - viceCardIccid?: string +// 设备绑定的卡列表响应 +export interface DeviceCardsResponse { + bindings: DeviceCardBinding[] | null // 绑定列表 } -// 设备批量分配参数 -export interface DeviceBatchAssignParams { - deviceIds: (string | number)[] - targetAgentId: string | number - includeCards: boolean // 是否连同网卡一起分配 +// 绑定卡到设备请求参数 +export interface BindCardToDeviceRequest { + iot_card_id: number // IoT卡ID + slot_position: number // 插槽位置 (1-4) } -// 设备导入任务 -export interface DeviceImportTask extends ImportTask { - agentId?: string | number - agentName?: string - operatorName?: string +// 绑定卡到设备响应 +export interface BindCardToDeviceResponse { + binding_id: number // 绑定记录ID + message: string // 提示信息 } -// 设备导入数据项 -export interface DeviceImportItem { - deviceCode: string - deviceName?: string - deviceType?: string - iccid?: string // 绑定的ICCID - card1Iccid?: string - card2Iccid?: string - location?: string +// 解绑设备上的卡响应 +export interface UnbindCardFromDeviceResponse { + message: string // 提示信息 +} + +// ========== 批量分配和回收相关 ========== + +// 批量分配设备请求参数 +export interface AllocateDevicesRequest { + device_ids: number[] // 设备ID列表 + target_shop_id: number // 目标店铺ID + remark?: string // 备注 +} + +// 分配失败项 +export interface AllocationDeviceFailedItem { + device_id: number // 设备ID + device_no: string // 设备号 + reason: string // 失败原因 +} + +// 批量分配设备响应 +export interface AllocateDevicesResponse { + success_count: number // 成功数量 + fail_count: number // 失败数量 + failed_items: AllocationDeviceFailedItem[] | null // 失败详情列表 +} + +// 批量回收设备请求参数 +export interface RecallDevicesRequest { + device_ids: number[] // 设备ID列表 + remark?: string // 备注 +} + +// 批量回收设备响应 +export interface RecallDevicesResponse { + success_count: number // 成功数量 + fail_count: number // 失败数量 + failed_items: AllocationDeviceFailedItem[] | null // 失败详情列表 +} + +// ========== 设备导入相关 ========== + +// 批量导入设备请求参数 +export interface ImportDeviceRequest { + file_key: string // 对象存储文件路径(通过 /storage/upload-url 获取) + batch_no?: string // 批次号 +} + +// 批量导入设备响应 +export interface ImportDeviceResponse { + message: string // 提示信息 + task_id: number // 导入任务ID + task_no: string // 任务编号 +} + +// ========== 导入任务相关 ========== + +// 导入任务状态枚举 +export enum DeviceImportTaskStatus { + PENDING = 1, // 待处理 + PROCESSING = 2, // 处理中 + COMPLETED = 3, // 已完成 + FAILED = 4 // 失败 +} + +// 导入任务查询参数 +export interface DeviceImportTaskQueryParams extends PaginationParams { + status?: DeviceImportTaskStatus // 任务状态 + batch_no?: string // 批次号(模糊查询) + start_time?: string // 创建时间起始 + end_time?: string // 创建时间结束 +} + +// 导入任务信息 +export interface DeviceImportTask { + id: number // 任务ID + task_no: string // 任务编号 + batch_no: string // 批次号 + file_name: string // 文件名 + status: DeviceImportTaskStatus // 任务状态 + status_text: string // 任务状态文本 + total_count: number // 总数 + success_count: number // 成功数 + fail_count: number // 失败数 + skip_count: number // 跳过数 + error_message: string // 错误信息 + created_at: string // 创建时间 + started_at: string | null // 开始处理时间 + completed_at: string | null // 完成时间 +} + +// 导入任务列表响应 +export interface DeviceImportTaskListResponse { + list: DeviceImportTask[] | null // 任务列表 + page: number // 当前页码 + page_size: number // 每页数量 + total: number // 总数 + total_pages: number // 总页数 +} + +// 导入结果详细项 +export interface DeviceImportResultItem { + line: number // 行号 + device_no: string // 设备号 + reason: string // 原因 +} + +// 导入任务详情 +export interface DeviceImportTaskDetail extends DeviceImportTask { + failed_items: DeviceImportResultItem[] | null // 失败记录详情 + skipped_items: DeviceImportResultItem[] | null // 跳过记录详情 } diff --git a/src/types/api/enterpriseCard.ts b/src/types/api/enterpriseCard.ts new file mode 100644 index 0000000..1d25dae --- /dev/null +++ b/src/types/api/enterpriseCard.ts @@ -0,0 +1,231 @@ +/** + * 企业卡授权相关类型定义 + */ + +/** + * 失败项 + */ +export interface FailedItem { + /** ICCID */ + iccid: string + /** 失败原因 */ + reason: string +} + +/** + * 分配的设备 + */ +export interface AllocatedDevice { + /** 设备ID */ + device_id: number + /** 设备号 */ + device_no: string + /** 卡数量 */ + card_count: number + /** 卡ICCID列表 */ + iccids: string[] +} + +/** + * 授权卡给企业请求 + */ +export interface AllocateCardsRequest { + /** 需要授权的 ICCID 列表 */ + iccids: string[] + /** 确认整体授权设备下所有卡 */ + confirm_device_bundles?: boolean +} + +/** + * 授权卡给企业响应 + */ +export interface AllocateCardsResponse { + /** 成功数量 */ + success_count: number + /** 失败数量 */ + fail_count: number + /** 失败详情 */ + failed_items: FailedItem[] | null + /** 连带授权的设备列表 */ + allocated_devices: AllocatedDevice[] | null +} + +/** + * 卡授权预检请求 + */ +export interface AllocateCardsPreviewRequest { + /** 需要授权的 ICCID 列表(最多1000个) */ + iccids: string[] +} + +/** + * 独立卡信息 + */ +export interface StandaloneCard { + /** 卡ID */ + iot_card_id: number + /** ICCID */ + iccid: string + /** 手机号 */ + msisdn: string + /** 运营商ID */ + carrier_id: number + /** 状态名称 */ + status_name: string +} + +/** + * 设备捆绑卡 + */ +export interface DeviceBundleCard { + /** 卡ID */ + iot_card_id: number + /** ICCID */ + iccid: string + /** 手机号 */ + msisdn: string +} + +/** + * 设备包 + */ +export interface DeviceBundle { + /** 设备ID */ + device_id: number + /** 设备号 */ + device_no: string + /** 触发卡(用户选择的卡) */ + trigger_card: DeviceBundleCard + /** 连带卡(同设备的其他卡) */ + bundle_cards: DeviceBundleCard[] +} + +/** + * 授权预检汇总 + */ +export interface AllocatePreviewSummary { + /** 总卡数量 */ + total_card_count: number + /** 独立卡数量 */ + standalone_card_count: number + /** 设备数量 */ + device_count: number + /** 设备卡数量 */ + device_card_count: number + /** 失败数量 */ + failed_count: number +} + +/** + * 卡授权预检响应 + */ +export interface AllocateCardsPreviewResponse { + /** 汇总信息 */ + summary: AllocatePreviewSummary + /** 可直接授权的卡(未绑定设备) */ + standalone_cards: StandaloneCard[] | null + /** 需要整体授权的设备包 */ + device_bundles: DeviceBundle[] | null + /** 失败的卡 */ + failed_items: FailedItem[] | null +} + +/** + * 企业卡项 + */ +export interface EnterpriseCardItem { + /** 卡ID */ + id: number + /** ICCID */ + iccid: string + /** 手机号 */ + msisdn: string + /** 运营商ID */ + carrier_id: number + /** 运营商名称 */ + carrier_name: string + /** 状态 */ + status: number + /** 状态名称 */ + status_name: string + /** 网络状态 */ + network_status: number + /** 网络状态名称 */ + network_status_name: string + /** 套餐ID */ + package_id?: number | null + /** 套餐名称 */ + package_name?: string + /** 设备ID */ + device_id?: number | null + /** 设备号 */ + device_no?: string +} + +/** + * 企业卡列表查询参数 + */ +export interface EnterpriseCardListParams { + /** 页码 */ + page?: number + /** 每页数量 */ + page_size?: number + /** 卡状态 */ + status?: number + /** 运营商ID */ + carrier_id?: number + /** ICCID(模糊查询) */ + iccid?: string + /** 设备号(模糊查询) */ + device_no?: string +} + +/** + * 企业卡列表响应 + */ +export interface EnterpriseCardPageResult { + /** 卡列表 */ + items: EnterpriseCardItem[] + /** 当前页码 */ + page: number + /** 每页数量 */ + size: number + /** 总记录数 */ + total: number +} + +/** + * 回收的设备 + */ +export interface RecalledDevice { + /** 设备ID */ + device_id: number + /** 设备号 */ + device_no: string + /** 卡数量 */ + card_count: number + /** 卡ICCID列表 */ + iccids: string[] +} + +/** + * 回收卡授权请求 + */ +export interface RecallCardsRequest { + /** 需要回收授权的 ICCID 列表 */ + iccids: string[] +} + +/** + * 回收卡授权响应 + */ +export interface RecallCardsResponse { + /** 成功数量 */ + success_count: number + /** 失败数量 */ + fail_count: number + /** 失败详情 */ + failed_items: FailedItem[] | null + /** 连带回收的设备列表 */ + recalled_devices: RecalledDevice[] | null +} diff --git a/src/types/api/index.ts b/src/types/api/index.ts index be273bb..3482e7f 100644 --- a/src/types/api/index.ts +++ b/src/types/api/index.ts @@ -46,3 +46,9 @@ export * from './customerAccount' // 设置相关 export * from './setting' + +// 授权记录相关 +export * from './authorization' + +// 企业卡授权相关 +export * from './enterpriseCard' diff --git a/src/utils/http/index.ts b/src/utils/http/index.ts index 7ec5ca9..379ac77 100644 --- a/src/utils/http/index.ts +++ b/src/utils/http/index.ts @@ -8,7 +8,16 @@ const axiosInstance = axios.create({ timeout: 15000, // 请求超时时间(毫秒) baseURL: import.meta.env.MODE === 'development' ? '' : import.meta.env.VITE_API_URL, // API地址:开发环境使用代理,生产环境使用完整URL withCredentials: true, // 异步请求携带cookie - transformRequest: [(data) => JSON.stringify(data)], // 请求数据转换为 JSON 字符串 + transformRequest: [ + (data, headers) => { + // 如果是 FormData,不进行转换 + if (data instanceof FormData) { + return data + } + // 其他数据转换为 JSON 字符串 + return JSON.stringify(data) + } + ], validateStatus: (status) => status >= 200 && status < 300, // 只接受 2xx 的状态码 headers: { get: { 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' }, @@ -36,10 +45,17 @@ axiosInstance.interceptors.request.use( // 如果 token 存在,则设置请求头 if (accessToken) { - request.headers.set({ - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}` - }) + // 如果是 FormData,不要覆盖 Content-Type (让浏览器自动设置 boundary) + if (request.data instanceof FormData) { + request.headers.set('Authorization', `Bearer ${accessToken}`) + // 删除 Content-Type,让浏览器自动设置 + delete request.headers['Content-Type'] + } else { + request.headers.set({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}` + }) + } } return request // 返回修改后的配置 diff --git a/src/views/account-management/enterprise-cards/index.vue b/src/views/account-management/enterprise-cards/index.vue new file mode 100644 index 0000000..d8396a9 --- /dev/null +++ b/src/views/account-management/enterprise-cards/index.vue @@ -0,0 +1,795 @@ + + + + + diff --git a/src/views/account-management/enterprise-customer/index.vue b/src/views/account-management/enterprise-customer/index.vue index e0888ea..e2b2eca 100644 --- a/src/views/account-management/enterprise-customer/index.vue +++ b/src/views/account-management/enterprise-customer/index.vue @@ -200,6 +200,7 @@ diff --git a/src/views/asset-management/authorization-records/index.vue b/src/views/asset-management/authorization-records/index.vue new file mode 100644 index 0000000..a65214d --- /dev/null +++ b/src/views/asset-management/authorization-records/index.vue @@ -0,0 +1,418 @@ + + + + + diff --git a/src/views/asset-management/card-list/index.vue b/src/views/asset-management/card-list/index.vue index 5fd8e93..6c2f37d 100644 --- a/src/views/asset-management/card-list/index.vue +++ b/src/views/asset-management/card-list/index.vue @@ -5,6 +5,7 @@ @@ -74,13 +75,18 @@ :on-change="handleFileChange" :on-exceed="handleExceed" :file-list="fileList" - accept=".xlsx,.xls" + accept=".csv" > @@ -252,6 +258,7 @@ + @@ -259,7 +266,7 @@ + + diff --git a/src/views/asset-management/device-list/index.vue b/src/views/asset-management/device-list/index.vue new file mode 100644 index 0000000..a21e5c6 --- /dev/null +++ b/src/views/asset-management/device-list/index.vue @@ -0,0 +1,674 @@ + + + + + diff --git a/src/views/asset-management/task-detail/index.vue b/src/views/asset-management/task-detail/index.vue index f53bccc..ff68658 100644 --- a/src/views/asset-management/task-detail/index.vue +++ b/src/views/asset-management/task-detail/index.vue @@ -10,8 +10,15 @@ {{ taskDetail?.task_no || '-' }} + + + {{ taskType === 'device' ? '设备导入' : 'ICCID导入' }} + + {{ taskDetail?.batch_no || '-' }} - {{ taskDetail?.carrier_name || '-' }} + + {{ (taskDetail as IotCardImportTaskDetail)?.carrier_name || '-' }} + {{ taskDetail?.file_name || '-' }} @@ -55,7 +62,18 @@ - + + @@ -67,7 +85,18 @@ - + + @@ -78,18 +107,23 @@