Compare commits
36 Commits
78bd9fba85
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a805c27902 | ||
|
|
62f828f314 | ||
|
|
f06d8c9133 | ||
|
|
407287f538 | ||
|
|
9fb3367fd7 | ||
|
|
ff67681c29 | ||
|
|
e975e6af4b | ||
|
|
f4ccf9ed24 | ||
|
|
8f31526499 | ||
|
|
d43de4cd06 | ||
|
|
bd45f7a224 | ||
|
|
e73992d253 | ||
|
|
8fbc321a5e | ||
|
|
1ebc0b8929 | ||
|
|
4d94f7efa6 | ||
|
|
08d5043b3f | ||
|
|
237eeed87a | ||
|
|
7e9acda1ab | ||
|
|
7b459b5c8d | ||
|
|
ce1032c7f9 | ||
|
|
4470a4ef04 | ||
|
|
f1cb1e53c8 | ||
|
|
3570b062a1 | ||
|
|
dccad819cf | ||
|
|
ca3184857e | ||
|
|
e8700c2585 | ||
|
|
b94c043a56 | ||
|
|
d97dc5f007 | ||
|
|
20e8c13e61 | ||
|
|
192c6f1d26 | ||
|
|
de9753f42d | ||
|
|
2c6fe4375b | ||
|
|
06cde977ad | ||
|
|
f62437379d | ||
|
|
c31fdaa748 | ||
|
|
4d2f38c75b |
@@ -310,6 +310,12 @@
|
|||||||
"whenever": true,
|
"whenever": true,
|
||||||
"ElMessage": true,
|
"ElMessage": true,
|
||||||
"ElTag": true,
|
"ElTag": true,
|
||||||
"ElMessageBox": true
|
"ElMessageBox": true,
|
||||||
|
"ElButton": true,
|
||||||
|
"ElDropdown": true,
|
||||||
|
"ElButton2": true,
|
||||||
|
"ElDropdown2": true,
|
||||||
|
"ElDropdownItem": true,
|
||||||
|
"ElDropdownMenu": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,9 @@
|
|||||||
"Bash(npm run build:*)",
|
"Bash(npm run build:*)",
|
||||||
"Bash(tree:*)",
|
"Bash(tree:*)",
|
||||||
"Bash(npm run dev:*)",
|
"Bash(npm run dev:*)",
|
||||||
"Bash(timeout:*)"
|
"Bash(timeout:*)",
|
||||||
|
"Read(//d/**)",
|
||||||
|
"Bash(findstr:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
162
openspec/changes/add-button-permissions/proposal.md
Normal file
162
openspec/changes/add-button-permissions/proposal.md
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
# Proposal: Add Button Permissions
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
为系统中的所有页面添加按钮级权限控制,根据用户权限动态显示或隐藏按钮和操作。权限系统已实现(`hasAuth` 和 `v-permission` 指令),本提案专注于将权限控制应用到所有相关页面。
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
当前系统已经实现了基于角色的权限控制(RBAC),但大多数页面的按钮和操作缺少权限控制。这意味着所有用户都能看到所有按钮,即使他们没有权限执行相应操作。需要在UI层面根据用户权限隐藏无权限的按钮,提升用户体验和系统安全性。
|
||||||
|
|
||||||
|
### Problems Solved
|
||||||
|
|
||||||
|
1. **安全性提升**: 防止用户看到或尝试执行无权限操作
|
||||||
|
2. **用户体验优化**: 只显示用户有权限的功能,避免混淆
|
||||||
|
3. **权限一致性**: 统一所有页面的权限控制实现方式
|
||||||
|
|
||||||
|
### Related Work
|
||||||
|
|
||||||
|
- 权限系统基础设施已实现 (`useAuth.ts`, `permission.ts`)
|
||||||
|
- 系统/角色页面 (`/system/role`) 已实现权限控制,作为最佳实践参考
|
||||||
|
- 权限配置已由后端API提供(见文档中的权限JSON结构)
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
|
||||||
|
### Approach
|
||||||
|
|
||||||
|
按业务模块逐步为所有页面添加按钮权限控制,使用已有的权限基础设施:
|
||||||
|
|
||||||
|
1. **表格操作按钮**: 使用 `v-permission` 指令
|
||||||
|
2. **表格内操作**: 使用 `hasAuth()` 函数动态渲染
|
||||||
|
3. **状态切换控件**: 使用 `disabled: !hasAuth()` 控制禁用状态
|
||||||
|
|
||||||
|
### Affected Modules
|
||||||
|
|
||||||
|
根据提供的权限JSON,需要添加权限控制的模块包括:
|
||||||
|
|
||||||
|
1. **套餐管理** (`/package-management`)
|
||||||
|
- 套餐系列 (`package-series`)
|
||||||
|
- 套餐列表 (`package-list`)
|
||||||
|
- 单套餐分配 (`package-assign`)
|
||||||
|
- 套餐系列分配 (`series-assign`)
|
||||||
|
|
||||||
|
2. **店铺管理** (`/shop-management`)
|
||||||
|
- 店铺列表 (`list`)
|
||||||
|
|
||||||
|
3. **账号管理** (`/account-management`)
|
||||||
|
- 账号列表 (`account`)
|
||||||
|
- 平台账号 (`platform-account`)
|
||||||
|
- 代理账号 (`shop-account`)
|
||||||
|
|
||||||
|
4. **资产管理** (`/asset-management`)
|
||||||
|
- IoT卡管理 (`iot-card-management`)
|
||||||
|
- IoT卡任务 (`iot-card-task`)
|
||||||
|
- 设备任务 (`device-task`)
|
||||||
|
- 设备管理 (`devices`)
|
||||||
|
- 授权记录 (`authorization-records`)
|
||||||
|
|
||||||
|
5. **账户管理** (`/account`)
|
||||||
|
- 企业客户 (`enterprise-customer`)
|
||||||
|
- 运营商管理 (`carrier-management`)
|
||||||
|
- 订单管理 (`orders`)
|
||||||
|
- 佣金管理 (`commission/*`)
|
||||||
|
|
||||||
|
### Implementation Pattern
|
||||||
|
|
||||||
|
基于 `/system/role` 页面的实现,统一使用以下模式:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- 1. 表格头部按钮 -->
|
||||||
|
<template #left>
|
||||||
|
<ElButton v-permission="'module:add'">新增</ElButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 2. 表格列中的状态切换 -->
|
||||||
|
<ElSwitch
|
||||||
|
:disabled="!hasAuth('module:update_status')"
|
||||||
|
v-model="row.status"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 3. 表格列中的操作按钮 -->
|
||||||
|
<script>
|
||||||
|
const columns = [{
|
||||||
|
prop: 'operation',
|
||||||
|
formatter: (row) => {
|
||||||
|
const buttons = []
|
||||||
|
|
||||||
|
if (hasAuth('module:edit')) {
|
||||||
|
buttons.push(h(ArtButtonTable, { type: 'edit', onClick: () => edit(row) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAuth('module:delete')) {
|
||||||
|
buttons.push(h(ArtButtonTable, { type: 'delete', onClick: () => del(row) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
</script>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission Mapping
|
||||||
|
|
||||||
|
权限代码 (`perm_code`) 与操作的映射关系:
|
||||||
|
|
||||||
|
- `module:add` -> 新增按钮
|
||||||
|
- `module:edit` -> 编辑按钮
|
||||||
|
- `module:delete` -> 删除按钮
|
||||||
|
- `module:update_status` -> 状态切换
|
||||||
|
- `module:update_xxx` -> 特定更新操作(如修改密码、修改成本价等)
|
||||||
|
- `module:xxx` -> 特殊操作(如分配权限、查看客户、卡授权等)
|
||||||
|
|
||||||
|
## Impact Analysis
|
||||||
|
|
||||||
|
### Benefits
|
||||||
|
|
||||||
|
1. **提升安全性**: UI层面防止无权限操作
|
||||||
|
2. **改善UX**: 用户只看到有权限的功能
|
||||||
|
3. **统一体验**: 所有页面使用相同的权限控制模式
|
||||||
|
|
||||||
|
### Risks
|
||||||
|
|
||||||
|
1. **向后兼容性**: 现有页面需要更新,可能影响用户使用习惯
|
||||||
|
2. **测试覆盖**: 需要测试各种权限组合的显示效果
|
||||||
|
|
||||||
|
### Mitigations
|
||||||
|
|
||||||
|
1. 分模块逐步rollout,优先完成核心模块
|
||||||
|
2. 权限配置由后端控制,前端只负责显示/隐藏
|
||||||
|
3. 保持API层面的权限检查,前端权限控制仅为UI优化
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- [ ] 所有模块的按钮根据权限正确显示/隐藏
|
||||||
|
- [ ] 状态切换控件根据权限正确启用/禁用
|
||||||
|
- [ ] 表格操作列根据权限动态渲染按钮
|
||||||
|
- [ ] 权限变更后UI立即响应
|
||||||
|
- [ ] 无权限用户看不到任何无权限操作
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- 依赖后端 API 返回正确的权限数据
|
||||||
|
- 依赖 `useUserStore` 中的权限数据管理
|
||||||
|
- 依赖已实现的 `useAuth` 和 `v-permission` 指令
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
|
||||||
|
预计需要 5-7 个工作日完成所有模块的权限控制集成。
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- 权限文档: `docs/在页面上新增按钮权限.md`
|
||||||
|
- 参考实现: `src/views/system/role/index.vue`
|
||||||
|
- 权限指令: `src/directives/permission.ts`
|
||||||
|
- 权限组合式函数: `src/composables/useAuth.ts`
|
||||||
190
openspec/changes/add-button-permissions/tasks.md
Normal file
190
openspec/changes/add-button-permissions/tasks.md
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
# Tasks: Add Button Permissions
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
将按钮权限控制系统地应用到所有相关页面。每个任务包含引入 `useAuth`、添加 `v-permission` 指令,以及在表格操作列中使用 `hasAuth()` 进行权限判断。
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
### 1. 套餐管理模块 (Package Management) ✅
|
||||||
|
|
||||||
|
- [x] **套餐系列页面** (`/package-management/package-series`)
|
||||||
|
- ✅ 引入 `useAuth` composable
|
||||||
|
- ✅ 为"新增套餐系列"按钮添加 `v-permission="'package_series:add'"`
|
||||||
|
- ✅ 为状态切换添加 `disabled: !hasAuth('package_series:update_status')`
|
||||||
|
- ✅ 在操作列中使用 `hasAuth()` 判断编辑权限 (`package_series:edit`)和删除权限 (`package_series:delete`)
|
||||||
|
|
||||||
|
- [x] **套餐列表页面** (`/package-management/package-list`)
|
||||||
|
- ✅ 引入 `useAuth` composable
|
||||||
|
- ✅ 为"新增套餐"按钮添加 `v-permission="'package:add'"`
|
||||||
|
- ✅ 为上/下架切换添加 `disabled: !hasAuth('package:update_away')`
|
||||||
|
- ✅ 为状态切换添加 `disabled: !hasAuth('package:update_status')`
|
||||||
|
- ✅ 在操作列中使用 `hasAuth()` 判断编辑 (`package:edit`) 和删除 (`package:delete`) 权限
|
||||||
|
|
||||||
|
- [x] **单套餐分配页面** (`/package-management/package-assign`)
|
||||||
|
- ✅ 引入 `useAuth` composable
|
||||||
|
- ✅ 为"新增分配"按钮添加 `v-permission="'package_assign:add'"`
|
||||||
|
- ✅ 为状态切换添加 `disabled: !hasAuth('package_assign:update_status')`
|
||||||
|
- ✅ 在操作列中使用 `hasAuth()` 判断以下权限:
|
||||||
|
- 修改成本价: `package_assign:update_cost`
|
||||||
|
- 编辑: `package_assign:edit`
|
||||||
|
- 删除: `package_assign:delete`
|
||||||
|
|
||||||
|
- [x] **套餐系列分配页面** (`/package-management/series-assign`)
|
||||||
|
- ✅ 引入 `useAuth` composable
|
||||||
|
- ✅ 为"新增系列分配"按钮添加 `v-permission="'series_assign:add'"`
|
||||||
|
- ✅ 为状态切换添加 `disabled: !hasAuth('series_assign:update_status')`
|
||||||
|
- ✅ 在操作列中使用 `hasAuth()` 判断编辑 (`series_assign:edit`) 和删除 (`series_assign:delete`) 权限
|
||||||
|
|
||||||
|
### 2. 店铺管理模块 (Shop Management) ✅
|
||||||
|
|
||||||
|
- [x] **店铺列表页面** (`/product/shop`)
|
||||||
|
- ✅ 引入 `useAuth` composable
|
||||||
|
- ✅ 为"新增店铺"按钮添加 `v-permission="'shop:add'"`
|
||||||
|
- ✅ 在操作列中使用 `hasAuth()` 判断以下权限:
|
||||||
|
- 查看客户: `shop:look_customer`
|
||||||
|
- 编辑: `shop:edit`
|
||||||
|
- 删除: `shop:delete`
|
||||||
|
- ✅ 在右键菜单中使用 `hasAuth()` 动态生成菜单项
|
||||||
|
|
||||||
|
### 3. 账号管理模块 (Account Management) ✅
|
||||||
|
|
||||||
|
- [x] **账号列表页面** (`/account-management/account`)
|
||||||
|
- ✅ 引入 `useAuth` composable
|
||||||
|
- ✅ 为"新增账号"按钮添加 `v-permission="'account:add'"`
|
||||||
|
- ✅ 在操作列中使用 `hasAuth()` 判断以下权限:
|
||||||
|
- 分配角色: `account:patch_role`
|
||||||
|
- 编辑: `account:edit`
|
||||||
|
- 删除: `account:delete`
|
||||||
|
|
||||||
|
- [x] **平台账号页面** (`/account-management/platform-account`)
|
||||||
|
- ✅ 引入 `useAuth` composable
|
||||||
|
- ✅ 为"新增平台账号"按钮添加 `v-permission="'platform_account:add'"`
|
||||||
|
- ✅ 在操作列中使用 `hasAuth()` 判断以下权限:
|
||||||
|
- 分配角色: `platform_account:patch_role`
|
||||||
|
- 修改密码: `platform_account:update_psd`
|
||||||
|
- 编辑: `platform_account:edit`
|
||||||
|
- 删除: `platform_account:delete`
|
||||||
|
|
||||||
|
- [x] **代理账号页面** (`/account-management/shop-account`)
|
||||||
|
- ✅ 引入 `useAuth` composable
|
||||||
|
- ✅ 为"新增代理账号"按钮添加 `v-permission="'shop_account:add'"`
|
||||||
|
- ✅ 在操作列中使用 `hasAuth()` 判断以下权限:
|
||||||
|
- 修改密码: `shop_account:update_psd`
|
||||||
|
- 编辑: `shop_account:edit`
|
||||||
|
|
||||||
|
### 4. 资产管理模块 (Asset Management) ✅
|
||||||
|
|
||||||
|
- [x] **IoT卡管理页面** (`/asset-management/iot-card-management`)
|
||||||
|
- ✅ 引入 `useAuth` composable
|
||||||
|
- ✅ 为表格头部操作按钮添加权限控制:
|
||||||
|
- 批量分配: `v-permission="'iot_card:batch_allocation'"`
|
||||||
|
- 批量回收: `v-permission="'iot_card:batch_recycle'"`
|
||||||
|
- 批量设置套餐系列: `v-permission="'iot_card:batch_setting'"`
|
||||||
|
- 批量充值: `v-permission="'iot_card:batch_recharge'"`
|
||||||
|
- ✅ 在操作列中使用 `hasAuth()` 判断以下权限:
|
||||||
|
- 网卡分销: `iot_card:network_distribution`
|
||||||
|
- 网卡回收: `iot_card:network_recycle`
|
||||||
|
- 变更套餐: `iot_card:change_package`
|
||||||
|
- ✅ 验证权限控制正确工作
|
||||||
|
|
||||||
|
- [x] **IoT卡任务页面** (`/asset-management/iot-card-task`)
|
||||||
|
- ✅ 引入 `useAuth` composable
|
||||||
|
- ✅ 为"批量导入IoT卡"按钮添加 `v-permission="''"`
|
||||||
|
- ✅ 验证权限控制正确工作
|
||||||
|
|
||||||
|
- [x] **设备任务页面** (`/asset-management/device-task`)
|
||||||
|
- ✅ 引入 `useAuth` composable
|
||||||
|
- ✅ 为"批量导入设备"按钮添加 `v-permission="'device_task:bulk_import'"`
|
||||||
|
- ✅ 验证权限控制正确工作
|
||||||
|
|
||||||
|
- [x] **设备管理页面** (`/asset-management/device-list`)
|
||||||
|
- ✅ 引入 `useAuth` composable
|
||||||
|
- ✅ 为表格头部操作按钮添加权限控制:
|
||||||
|
- 批量分配: `v-permission="'devices:batch_allocation'"`
|
||||||
|
- 批量回收: `v-permission="'devices:batch_recycle'"`
|
||||||
|
- 批量设置套餐系列: `v-permission="'devices:batch_setting'"`
|
||||||
|
- ✅ 在操作列中使用 `hasAuth()` 判断以下权限:
|
||||||
|
- 查看绑定的卡: `devices:look_binding`
|
||||||
|
- 删除设备: `devices:delete`
|
||||||
|
- ✅ 验证权限控制正确工作
|
||||||
|
|
||||||
|
- [x] **授权记录页面** (`/asset-management/authorization-records`)
|
||||||
|
- ✅ 引入 `useAuth` composable
|
||||||
|
- ✅ 在操作列中使用 `hasAuth()` 判断修改备注权限 (`authorization_records:update_remark`)
|
||||||
|
- ✅ 验证权限控制正确工作
|
||||||
|
|
||||||
|
### 5. 账户管理模块 (Account Module) ✅
|
||||||
|
|
||||||
|
- [x] **企业客户页面** (`/account/enterprise-customer`)
|
||||||
|
- ✅ 引入 `useAuth` composable
|
||||||
|
- ✅ 为"新增企业客户"按钮添加 `v-permission="'enterprise_customer:add'"`
|
||||||
|
- ✅ 在操作列中使用 `hasAuth()` 判断以下权限:
|
||||||
|
- 编辑: `enterprise_customer:edit`
|
||||||
|
- 查看客户: `enterprise_customer:look_customer`
|
||||||
|
- 卡授权: `enterprise_customer:card_authorization`
|
||||||
|
- 修改密码: `enterprise_customer:update_pwd`
|
||||||
|
- ✅ 验证权限控制正确工作
|
||||||
|
|
||||||
|
- [x] **运营商管理页面** (`/account/carrier-management`)
|
||||||
|
- ✅ 引入 `useAuth` composable
|
||||||
|
- ✅ 为"新增运营商"按钮添加 `v-permission="'carrier:add'"`
|
||||||
|
- ✅ 为状态切换添加 `disabled: !hasAuth('carrier:update_status')`
|
||||||
|
- ✅ 在操作列中使用 `hasAuth()` 判断编辑 (`carrier:edit`) 和删除 (`carrier:delete`) 权限
|
||||||
|
- ✅ 验证权限控制正确工作
|
||||||
|
|
||||||
|
- [x] **订单管理页面** (`/account/orders`)
|
||||||
|
- ✅ 引入 `useAuth` composable
|
||||||
|
- ✅ 为"创建订单"按钮添加 `v-permission="'orders:add'"`
|
||||||
|
- ✅ 验证权限控制正确工作
|
||||||
|
|
||||||
|
- [x] **提现配置页面** (`/account/commission/withdrawal-settings`)
|
||||||
|
- ✅ 引入 `useAuth` composable
|
||||||
|
- ✅ 为"新增配置"按钮添加 `v-permission="'withdrawal_settings:add'"`
|
||||||
|
- ✅ 验证权限控制正确工作
|
||||||
|
|
||||||
|
- [x] **我的佣金页面** (`/account/commission/my-commission`)
|
||||||
|
- ✅ 引入 `useAuth` composable
|
||||||
|
- ✅ 为"发起提现"按钮添加 `v-permission="'my_commission:add'"`
|
||||||
|
- ✅ 验证权限控制正确工作
|
||||||
|
|
||||||
|
- [x] **代理商佣金管理页面** (`/account/commission/agent-commission`)
|
||||||
|
- ✅ 引入 `useAuth` composable
|
||||||
|
- ✅ 在操作列中使用 `hasAuth()` 判断查看详情权限 (`agent_commission:detail`)
|
||||||
|
- ✅ 验证权限控制正确工作
|
||||||
|
|
||||||
|
### 6. 系统管理模块 (System Management) ✅
|
||||||
|
|
||||||
|
- [x] **权限管理页面** (`/system/permission`)
|
||||||
|
- ✅ 引入 `useAuth` composable
|
||||||
|
- ✅ 为"新增权限"按钮添加 `v-permission="'permission:add'"`
|
||||||
|
- ✅ 在操作列中使用 `hasAuth()` 判断编辑 (`permission:edit`) 和删除 (`permission:delete`) 权限
|
||||||
|
- ✅ 验证权限控制正确工作
|
||||||
|
|
||||||
|
### 7. 测试与文档
|
||||||
|
|
||||||
|
- [ ] **权限控制测试**
|
||||||
|
- 创建不同权限组合的测试用户
|
||||||
|
- 验证每个页面的按钮显示/隐藏符合预期
|
||||||
|
- 验证状态切换的启用/禁用符合预期
|
||||||
|
- 测试权限动态变更后UI的响应
|
||||||
|
|
||||||
|
- [ ] **文档更新**
|
||||||
|
- 更新开发文档,说明权限控制的实现方式
|
||||||
|
- 添加权限代码与功能的映射表
|
||||||
|
- 提供新页面添加权限控制的指南
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
每个任务完成后需验证:
|
||||||
|
|
||||||
|
1. **功能正确性**: 有权限时按钮/操作可见且可用
|
||||||
|
2. **权限隔离**: 无权限时按钮/操作不可见或禁用
|
||||||
|
3. **用户体验**: 权限控制不影响正常操作流程
|
||||||
|
4. **性能**: 权限检查不影响页面渲染性能
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- 所有任务依赖权限基础设施 (`useAuth`, `v-permission`)
|
||||||
|
- 任务可并行执行,建议按模块分批进行
|
||||||
|
- 优先级: 核心模块 (账号、套餐、资产) > 其他模块
|
||||||
56
openspec/changes/add-iot-device-operations/proposal.md
Normal file
56
openspec/changes/add-iot-device-operations/proposal.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Change: Add IoT Card and Device Operations
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
运营人员和代理商需要更丰富的物联网卡和设备管理功能,包括查询流量使用情况、实名认证状态、卡片状态、启停操作,以及设备的重启、重置、限速、换卡、WiFi设置等操作。这些功能是物联网卡全生命周期管理的核心能力。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
### IoT Card Operations (6 new APIs)
|
||||||
|
- 查询流量使用情况 (GET /api/admin/iot-cards/{iccid}/gateway-flow)
|
||||||
|
- 查询实名认证状态 (GET /api/admin/iot-cards/{iccid}/gateway-realname)
|
||||||
|
- 查询卡片状态 (GET /api/admin/iot-cards/{iccid}/gateway-status)
|
||||||
|
- 获取实名认证链接 (GET /api/admin/iot-cards/{iccid}/realname-link)
|
||||||
|
- 启用物联网卡 (POST /api/admin/iot-cards/{iccid}/start)
|
||||||
|
- 停用物联网卡 (POST /api/admin/iot-cards/{iccid}/stop)
|
||||||
|
|
||||||
|
### Device Operations (6 new APIs)
|
||||||
|
- 重启设备 (POST /api/admin/devices/by-imei/{imei}/reboot)
|
||||||
|
- 重置设备 (POST /api/admin/devices/by-imei/{imei}/reset)
|
||||||
|
- 设置限速 (PUT /api/admin/devices/by-imei/{imei}/speed-limit)
|
||||||
|
- 切换SIM卡 (POST /api/admin/devices/by-imei/{imei}/switch-card)
|
||||||
|
- 设置WiFi (PUT /api/admin/devices/by-imei/{imei}/wifi)
|
||||||
|
|
||||||
|
### UI Changes
|
||||||
|
- 在 `/asset-management/iot-card-management` 页面添加操作按钮:
|
||||||
|
- "查询流量"按钮(主要操作)
|
||||||
|
- "更多操作"下拉菜单(包含其他5个操作)
|
||||||
|
- 在 `/asset-management/devices` 页面添加操作按钮:
|
||||||
|
- "重启设备"按钮(主要操作)
|
||||||
|
- "更多操作"下拉菜单(包含其他5个操作)
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
### Affected Specs
|
||||||
|
- **NEW**: `specs/iot-card-operations/spec.md` - IoT卡操作相关的所有需求
|
||||||
|
- **NEW**: `specs/device-operations/spec.md` - 设备操作相关的所有需求
|
||||||
|
|
||||||
|
### Affected Code
|
||||||
|
- **API层**:
|
||||||
|
- 新增 `src/api/modules/iotCard.ts` - IoT卡操作API方法
|
||||||
|
- 新增 `src/api/modules/device.ts` - 设备操作API方法
|
||||||
|
- **类型定义**:
|
||||||
|
- 新增 `src/types/api/iotCard.ts` - IoT卡相关类型
|
||||||
|
- 新增 `src/types/api/device.ts` - 设备相关类型
|
||||||
|
- **页面组件**:
|
||||||
|
- 修改 `src/views/asset-management/iot-card-management/index.vue` - 添加操作按钮和对话框
|
||||||
|
- 修改 `src/views/asset-management/devices/index.vue` - 添加操作按钮和对话框
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
无破坏性变更。所有变更都是增量式的新功能添加。
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- 后端API已实现(见 `docs/2-3新增接口.md`)
|
||||||
|
- Element Plus UI组件库(已在项目中)
|
||||||
|
- Axios HTTP客户端(已在项目中)
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
# Device Operations Specification
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Reboot Device
|
||||||
|
|
||||||
|
The system SHALL provide the ability to reboot an IoT device via its IMEI.
|
||||||
|
|
||||||
|
#### Scenario: Successfully reboot device
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user initiates a reboot operation on a device with valid IMEI
|
||||||
|
- **THEN** the system sends the reboot command to the device
|
||||||
|
- **AND** returns a success response with operation confirmation
|
||||||
|
|
||||||
|
#### Scenario: Reboot operation with confirmation
|
||||||
|
|
||||||
|
- **WHEN** a user clicks the reboot device button
|
||||||
|
- **THEN** the system prompts for confirmation with a warning message
|
||||||
|
- **AND** only proceeds if the user confirms the action
|
||||||
|
|
||||||
|
#### Scenario: Reboot with invalid IMEI
|
||||||
|
|
||||||
|
- **WHEN** a user attempts to reboot a device with invalid or non-existent IMEI
|
||||||
|
- **THEN** the system returns a 400 error with appropriate error message
|
||||||
|
|
||||||
|
### Requirement: Reset Device to Factory Settings
|
||||||
|
|
||||||
|
The system SHALL provide the ability to reset an IoT device to factory settings via its IMEI.
|
||||||
|
|
||||||
|
#### Scenario: Successfully reset device
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user initiates a factory reset operation on a device
|
||||||
|
- **THEN** the system sends the reset command to the device
|
||||||
|
- **AND** returns a success response
|
||||||
|
|
||||||
|
#### Scenario: Reset operation with strong confirmation
|
||||||
|
|
||||||
|
- **WHEN** a user clicks the factory reset button
|
||||||
|
- **THEN** the system displays a warning dialog explaining data loss
|
||||||
|
- **AND** requires user confirmation before proceeding
|
||||||
|
- **AND** only executes if the user explicitly confirms
|
||||||
|
|
||||||
|
#### Scenario: Insufficient permissions for reset
|
||||||
|
|
||||||
|
- **WHEN** a user without proper permissions attempts to reset a device
|
||||||
|
- **THEN** the system returns a 403 forbidden error
|
||||||
|
|
||||||
|
### Requirement: Set Device Speed Limit
|
||||||
|
|
||||||
|
The system SHALL provide the ability to configure upload and download speed limits for an IoT device.
|
||||||
|
|
||||||
|
#### Scenario: Successfully set speed limit
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user submits valid speed limit parameters
|
||||||
|
- **THEN** the system applies the upload and download speed limits to the device
|
||||||
|
- **AND** returns a success response
|
||||||
|
|
||||||
|
#### Scenario: Configure speed limits via dialog
|
||||||
|
|
||||||
|
- **WHEN** a user selects the speed limit option
|
||||||
|
- **THEN** the system displays a dialog with input fields for upload_speed and download_speed (KB/s)
|
||||||
|
- **AND** validates that both values are integers >= 1
|
||||||
|
|
||||||
|
#### Scenario: Invalid speed limit parameters
|
||||||
|
|
||||||
|
- **WHEN** a user submits speed limits less than 1 KB/s
|
||||||
|
- **THEN** the system returns a 400 parameter error
|
||||||
|
- **AND** displays validation error messages
|
||||||
|
|
||||||
|
### Requirement: Switch Device SIM Card
|
||||||
|
|
||||||
|
The system SHALL provide the ability to switch the active SIM card on a multi-SIM device.
|
||||||
|
|
||||||
|
#### Scenario: Successfully switch to target card
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user initiates a card switch with valid target ICCID
|
||||||
|
- **THEN** the system switches the device to use the specified card
|
||||||
|
- **AND** returns a success response
|
||||||
|
|
||||||
|
#### Scenario: Switch card via dialog
|
||||||
|
|
||||||
|
- **WHEN** a user selects the switch card option
|
||||||
|
- **THEN** the system displays a dialog prompting for target_iccid
|
||||||
|
- **AND** validates the ICCID format
|
||||||
|
|
||||||
|
#### Scenario: Switch to non-existent card
|
||||||
|
|
||||||
|
- **WHEN** a user attempts to switch to an ICCID that doesn't exist or isn't available
|
||||||
|
- **THEN** the system returns a 400 error with descriptive message
|
||||||
|
|
||||||
|
### Requirement: Configure Device WiFi Settings
|
||||||
|
|
||||||
|
The system SHALL provide the ability to configure WiFi settings including SSID, password, and enabled status.
|
||||||
|
|
||||||
|
#### Scenario: Successfully configure WiFi
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user submits valid WiFi configuration
|
||||||
|
- **THEN** the system applies the WiFi settings to the device
|
||||||
|
- **AND** returns a success response
|
||||||
|
|
||||||
|
#### Scenario: Configure WiFi via dialog
|
||||||
|
|
||||||
|
- **WHEN** a user selects the WiFi configuration option
|
||||||
|
- **THEN** the system displays a dialog with fields for:
|
||||||
|
- enabled (启用状态: 0=禁用, 1=启用)
|
||||||
|
- ssid (WiFi名称, 1-32 characters)
|
||||||
|
- password (WiFi密码, 8-63 characters)
|
||||||
|
- **AND** validates all field constraints
|
||||||
|
|
||||||
|
#### Scenario: Invalid WiFi parameters
|
||||||
|
|
||||||
|
- **WHEN** a user submits WiFi configuration with invalid parameters
|
||||||
|
- **THEN** the system returns a 400 error
|
||||||
|
- **AND** displays specific validation errors (e.g., "SSID too long", "Password too short")
|
||||||
|
|
||||||
|
### Requirement: Device Management UI Integration
|
||||||
|
|
||||||
|
The system SHALL integrate device operations into the device management page at `/asset-management/devices`.
|
||||||
|
|
||||||
|
#### Scenario: Display primary operation button
|
||||||
|
|
||||||
|
- **WHEN** a user views the device management page
|
||||||
|
- **THEN** the system displays a "重启设备" (Reboot Device) button as the primary operation
|
||||||
|
|
||||||
|
#### Scenario: Display dropdown menu for additional operations
|
||||||
|
|
||||||
|
- **WHEN** a user views the device management page
|
||||||
|
- **THEN** the system displays a "更多操作" (More Operations) dropdown menu
|
||||||
|
- **AND** the dropdown contains: 恢复出厂, 设置限速, 切换SIM卡, 设置WiFi
|
||||||
|
|
||||||
|
#### Scenario: Show loading indicator during operations
|
||||||
|
|
||||||
|
- **WHEN** a device operation is in progress
|
||||||
|
- **THEN** the system displays a loading indicator
|
||||||
|
- **AND** disables the operation buttons to prevent duplicate requests
|
||||||
|
|
||||||
|
### Requirement: Authentication and Authorization
|
||||||
|
|
||||||
|
The system SHALL enforce JWT-based authentication for all device operations.
|
||||||
|
|
||||||
|
#### Scenario: Access with valid JWT token
|
||||||
|
|
||||||
|
- **WHEN** a user makes a request with a valid Bearer token
|
||||||
|
- **THEN** the system processes the request normally
|
||||||
|
|
||||||
|
#### Scenario: Access with expired token
|
||||||
|
|
||||||
|
- **WHEN** a user makes a request with an expired JWT token
|
||||||
|
- **THEN** the system returns a 401 unauthorized error
|
||||||
|
- **AND** redirects to the login page
|
||||||
|
|
||||||
|
### Requirement: Error Handling and User Feedback
|
||||||
|
|
||||||
|
The system SHALL provide clear error messages and success notifications for all operations.
|
||||||
|
|
||||||
|
#### Scenario: Display success message
|
||||||
|
|
||||||
|
- **WHEN** a device operation completes successfully
|
||||||
|
- **THEN** the system displays a success message notification
|
||||||
|
- **AND** automatically closes the operation dialog
|
||||||
|
|
||||||
|
#### Scenario: Handle network errors
|
||||||
|
|
||||||
|
- **WHEN** a network error occurs during a device operation
|
||||||
|
- **THEN** the system displays a user-friendly error message
|
||||||
|
- **AND** allows the user to retry the operation
|
||||||
|
|
||||||
|
#### Scenario: Handle server errors
|
||||||
|
|
||||||
|
- **WHEN** a 500 server error occurs
|
||||||
|
- **THEN** the system displays an error message with timestamp
|
||||||
|
- **AND** logs the error for debugging
|
||||||
|
|
||||||
|
### Requirement: Operation Confirmation Dialogs
|
||||||
|
|
||||||
|
The system SHALL require user confirmation for destructive operations.
|
||||||
|
|
||||||
|
#### Scenario: Confirm reboot operation
|
||||||
|
|
||||||
|
- **WHEN** a user initiates a reboot
|
||||||
|
- **THEN** the system shows a confirmation dialog stating "确定要重启该设备吗?"
|
||||||
|
- **AND** requires explicit confirmation
|
||||||
|
|
||||||
|
#### Scenario: Confirm factory reset operation
|
||||||
|
|
||||||
|
- **WHEN** a user initiates a factory reset
|
||||||
|
- **THEN** the system shows a strong warning dialog stating "确定要恢复出厂设置吗?此操作将清除所有数据!"
|
||||||
|
- **AND** requires explicit confirmation
|
||||||
|
|
||||||
|
#### Scenario: No confirmation for query operations
|
||||||
|
|
||||||
|
- **WHEN** a user initiates speed limit, card switch, or WiFi configuration
|
||||||
|
- **THEN** the system displays input dialogs without confirmation prompts
|
||||||
|
- **AND** only executes after user submits valid parameters
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# IoT Card Operations Specification
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Query IoT Card Flow Usage
|
||||||
|
|
||||||
|
The system SHALL provide the ability to query real-time flow usage for an IoT card via its ICCID.
|
||||||
|
|
||||||
|
#### Scenario: Successfully query flow usage
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user requests flow usage for a valid ICCID
|
||||||
|
- **THEN** the system returns flow usage data including used flow amount and unit
|
||||||
|
|
||||||
|
#### Scenario: Query with invalid ICCID
|
||||||
|
|
||||||
|
- **WHEN** a user requests flow usage for an invalid or non-existent ICCID
|
||||||
|
- **THEN** the system returns a 400 error with appropriate error message
|
||||||
|
|
||||||
|
### Requirement: Query IoT Card Realname Status
|
||||||
|
|
||||||
|
The system SHALL provide the ability to query the realname authentication status for an IoT card via its ICCID.
|
||||||
|
|
||||||
|
#### Scenario: Successfully query realname status
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user requests realname status for a valid ICCID
|
||||||
|
- **THEN** the system returns the current realname authentication status
|
||||||
|
|
||||||
|
#### Scenario: Query status for unauthenticated user
|
||||||
|
|
||||||
|
- **WHEN** an unauthenticated user attempts to query realname status
|
||||||
|
- **THEN** the system returns a 401 unauthorized error
|
||||||
|
|
||||||
|
### Requirement: Query IoT Card Real-time Status
|
||||||
|
|
||||||
|
The system SHALL provide the ability to query the real-time operational status of an IoT card via its ICCID.
|
||||||
|
|
||||||
|
#### Scenario: Successfully query card status
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user requests card status for a valid ICCID
|
||||||
|
- **THEN** the system returns card status information including ICCID and current status (准备/正常/停机)
|
||||||
|
|
||||||
|
#### Scenario: Query with unauthorized access
|
||||||
|
|
||||||
|
- **WHEN** a user without proper permissions attempts to query card status
|
||||||
|
- **THEN** the system returns a 403 forbidden error
|
||||||
|
|
||||||
|
### Requirement: Get IoT Card Realname Link
|
||||||
|
|
||||||
|
The system SHALL provide the ability to generate and retrieve a realname authentication link for an IoT card.
|
||||||
|
|
||||||
|
#### Scenario: Successfully retrieve realname link
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user requests the realname link for a valid ICCID
|
||||||
|
- **THEN** the system returns an HTTPS URL that can be used for realname authentication
|
||||||
|
|
||||||
|
#### Scenario: Display realname link as QR code
|
||||||
|
|
||||||
|
- **WHEN** the system returns a realname authentication link
|
||||||
|
- **THEN** the UI displays the link as a QR code for easy mobile scanning
|
||||||
|
|
||||||
|
### Requirement: Start IoT Card (复机)
|
||||||
|
|
||||||
|
The system SHALL provide the ability to start (restore service) an IoT card via its ICCID.
|
||||||
|
|
||||||
|
#### Scenario: Successfully start a stopped card
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user initiates a start operation on a stopped card
|
||||||
|
- **THEN** the system processes the request and restores card service
|
||||||
|
- **AND** displays a success message to the user
|
||||||
|
|
||||||
|
#### Scenario: Start operation with confirmation
|
||||||
|
|
||||||
|
- **WHEN** a user clicks the start card button
|
||||||
|
- **THEN** the system prompts for confirmation before executing the operation
|
||||||
|
- **AND** only proceeds if the user confirms the action
|
||||||
|
|
||||||
|
#### Scenario: Insufficient permissions for start operation
|
||||||
|
|
||||||
|
- **WHEN** a user without proper permissions attempts to start a card
|
||||||
|
- **THEN** the system returns a 403 forbidden error
|
||||||
|
|
||||||
|
### Requirement: Stop IoT Card (停机)
|
||||||
|
|
||||||
|
The system SHALL provide the ability to stop (suspend service) an IoT card via its ICCID.
|
||||||
|
|
||||||
|
#### Scenario: Successfully stop an active card
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user initiates a stop operation on an active card
|
||||||
|
- **THEN** the system processes the request and suspends card service
|
||||||
|
- **AND** displays a success message to the user
|
||||||
|
|
||||||
|
#### Scenario: Stop operation with confirmation
|
||||||
|
|
||||||
|
- **WHEN** a user clicks the stop card button
|
||||||
|
- **THEN** the system prompts for confirmation before executing the operation
|
||||||
|
- **AND** only proceeds if the user confirms the action
|
||||||
|
|
||||||
|
#### Scenario: Server error during stop operation
|
||||||
|
|
||||||
|
- **WHEN** a server error occurs during the stop operation
|
||||||
|
- **THEN** the system returns a 500 error with error details
|
||||||
|
- **AND** displays an appropriate error message to the user
|
||||||
|
|
||||||
|
### Requirement: IoT Card Management UI Integration
|
||||||
|
|
||||||
|
The system SHALL integrate IoT card operations into the card management page at `/asset-management/iot-card-management`.
|
||||||
|
|
||||||
|
#### Scenario: Display primary operation button
|
||||||
|
|
||||||
|
- **WHEN** a user views the IoT card management page
|
||||||
|
- **THEN** the system displays a "查询流量使用" (Query Flow Usage) button as the primary operation
|
||||||
|
|
||||||
|
#### Scenario: Display dropdown menu for additional operations
|
||||||
|
|
||||||
|
- **WHEN** a user views the IoT card management page
|
||||||
|
- **THEN** the system displays a "更多操作" (More Operations) dropdown menu
|
||||||
|
- **AND** the dropdown contains: 查询实名状态, 查询卡状态, 获取实名链接, 启用卡片, 停用卡片
|
||||||
|
|
||||||
|
#### Scenario: Show operation results in dialog
|
||||||
|
|
||||||
|
- **WHEN** a user executes a query operation (flow, realname status, or card status)
|
||||||
|
- **THEN** the system displays results in a modal dialog with properly formatted data
|
||||||
|
|
||||||
|
### Requirement: Authentication and Authorization
|
||||||
|
|
||||||
|
The system SHALL enforce JWT-based authentication for all IoT card operations.
|
||||||
|
|
||||||
|
#### Scenario: Access with valid JWT token
|
||||||
|
|
||||||
|
- **WHEN** a user makes a request with a valid Bearer token
|
||||||
|
- **THEN** the system processes the request normally
|
||||||
|
|
||||||
|
#### Scenario: Access with expired token
|
||||||
|
|
||||||
|
- **WHEN** a user makes a request with an expired JWT token
|
||||||
|
- **THEN** the system returns a 401 unauthorized error
|
||||||
|
|
||||||
|
### Requirement: Error Handling
|
||||||
|
|
||||||
|
The system SHALL provide clear error messages for all failure scenarios.
|
||||||
|
|
||||||
|
#### Scenario: Handle 400 parameter errors
|
||||||
|
|
||||||
|
- **WHEN** a request contains invalid parameters
|
||||||
|
- **THEN** the system returns a 400 error with specific validation failure details
|
||||||
|
|
||||||
|
#### Scenario: Handle 500 server errors
|
||||||
|
|
||||||
|
- **WHEN** an internal server error occurs
|
||||||
|
- **THEN** the system returns a 500 error with error timestamp
|
||||||
|
- **AND** logs the error for debugging purposes
|
||||||
86
openspec/changes/add-iot-device-operations/tasks.md
Normal file
86
openspec/changes/add-iot-device-operations/tasks.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Implementation Tasks
|
||||||
|
|
||||||
|
## 1. API Layer - IoT Card Operations
|
||||||
|
|
||||||
|
- [ ] 1.1 创建 `src/api/modules/iotCard.ts`
|
||||||
|
- [ ] 1.2 实现 `getGatewayFlow(iccid)` - 查询流量使用
|
||||||
|
- [ ] 1.3 实现 `getGatewayRealname(iccid)` - 查询实名状态
|
||||||
|
- [ ] 1.4 实现 `getGatewayStatus(iccid)` - 查询卡片状态
|
||||||
|
- [ ] 1.5 实现 `getRealnameLink(iccid)` - 获取实名链接
|
||||||
|
- [ ] 1.6 实现 `startCard(iccid)` - 启用卡片
|
||||||
|
- [ ] 1.7 实现 `stopCard(iccid)` - 停用卡片
|
||||||
|
|
||||||
|
## 2. API Layer - Device Operations
|
||||||
|
|
||||||
|
- [ ] 2.1 创建 `src/api/modules/device.ts`
|
||||||
|
- [ ] 2.2 实现 `rebootDevice(imei)` - 重启设备
|
||||||
|
- [ ] 2.3 实现 `resetDevice(imei)` - 重置设备
|
||||||
|
- [ ] 2.4 实现 `setSpeedLimit(imei, params)` - 设置限速
|
||||||
|
- [ ] 2.5 实现 `switchCard(imei, params)` - 切换SIM卡
|
||||||
|
- [ ] 2.6 实现 `setWifi(imei, params)` - 设置WiFi
|
||||||
|
|
||||||
|
## 3. Type Definitions - IoT Card
|
||||||
|
|
||||||
|
- [ ] 3.1 创建 `src/types/api/iotCard.ts`
|
||||||
|
- [ ] 3.2 定义流量使用响应类型 `GatewayFlowResponse`
|
||||||
|
- [ ] 3.3 定义实名状态响应类型 `GatewayRealnameResponse`
|
||||||
|
- [ ] 3.4 定义卡片状态响应类型 `GatewayStatusResponse`
|
||||||
|
- [ ] 3.5 定义实名链接响应类型 `RealnameUrlResponse`
|
||||||
|
- [ ] 3.6 定义启停操作请求类型 `StartStopCardRequest`
|
||||||
|
|
||||||
|
## 4. Type Definitions - Device
|
||||||
|
|
||||||
|
- [ ] 4.1 创建 `src/types/api/device.ts`
|
||||||
|
- [ ] 4.2 定义限速参数类型 `SpeedLimitParams`
|
||||||
|
- [ ] 4.3 定义换卡参数类型 `SwitchCardParams`
|
||||||
|
- [ ] 4.4 定义WiFi参数类型 `WifiParams`
|
||||||
|
- [ ] 4.5 定义操作响应类型 `DeviceOperationResponse`
|
||||||
|
|
||||||
|
## 5. UI - IoT Card Management Page
|
||||||
|
|
||||||
|
- [ ] 5.1 在表格操作列添加"查询流量"按钮
|
||||||
|
- [ ] 5.2 在表格操作列添加"更多操作"下拉菜单
|
||||||
|
- [ ] 5.3 创建"流量使用查询"对话框组件
|
||||||
|
- [ ] 5.4 创建"实名状态查询"对话框组件
|
||||||
|
- [ ] 5.5 创建"卡片状态查询"对话框组件
|
||||||
|
- [ ] 5.6 创建"获取实名链接"对话框组件(显示二维码)
|
||||||
|
- [ ] 5.7 实现"启用卡片"操作(带确认提示)
|
||||||
|
- [ ] 5.8 实现"停用卡片"操作(带确认提示)
|
||||||
|
|
||||||
|
## 6. UI - Device Management Page
|
||||||
|
|
||||||
|
- [ ] 6.1 在表格操作列添加"重启设备"按钮
|
||||||
|
- [ ] 6.2 在表格操作列添加"更多操作"下拉菜单
|
||||||
|
- [ ] 6.3 实现"重启设备"操作(带确认提示)
|
||||||
|
- [ ] 6.4 实现"重置设备"操作(带确认提示)
|
||||||
|
- [ ] 6.5 创建"设置限速"对话框组件(包含上下行速率输入)
|
||||||
|
- [ ] 6.6 创建"切换SIM卡"对话框组件(选择卡槽)
|
||||||
|
- [ ] 6.7 创建"设置WiFi"对话框组件(SSID、密码、频段等)
|
||||||
|
|
||||||
|
## 7. Error Handling & User Feedback
|
||||||
|
|
||||||
|
- [ ] 7.1 为所有API调用添加错误处理
|
||||||
|
- [ ] 7.2 添加操作成功提示消息
|
||||||
|
- [ ] 7.3 添加操作失败错误提示
|
||||||
|
- [ ] 7.4 添加加载状态指示器
|
||||||
|
|
||||||
|
## 8. Permission Control
|
||||||
|
|
||||||
|
- [ ] 8.1 检查IoT卡操作权限配置
|
||||||
|
- [ ] 8.2 检查设备操作权限配置
|
||||||
|
- [ ] 8.3 根据权限显示/隐藏操作按钮
|
||||||
|
|
||||||
|
## 9. Testing & Validation
|
||||||
|
|
||||||
|
- [ ] 9.1 测试所有IoT卡操作API调用
|
||||||
|
- [ ] 9.2 测试所有设备操作API调用
|
||||||
|
- [ ] 9.3 测试UI交互和对话框显示
|
||||||
|
- [ ] 9.4 测试错误处理场景
|
||||||
|
- [ ] 9.5 测试权限控制
|
||||||
|
- [ ] 9.6 验证响应数据格式和显示
|
||||||
|
|
||||||
|
## 10. Documentation
|
||||||
|
|
||||||
|
- [ ] 10.1 更新API文档(如有需要)
|
||||||
|
- [ ] 10.2 添加操作说明注释
|
||||||
|
- [ ] 10.3 更新用户手册(如有需要)
|
||||||
22
openspec/changes/add-shop-default-roles/proposal.md
Normal file
22
openspec/changes/add-shop-default-roles/proposal.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Change: Add Shop Default Roles Management
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
店铺管理模块需要支持为每个店铺配置默认角色的功能。当新账号加入店铺时,系统需要自动分配这些默认角色,以简化账号管理流程并确保权限一致性。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增查询店铺默认角色列表的接口和UI
|
||||||
|
- 新增为店铺分配默认角色的接口和UI
|
||||||
|
- 新增删除店铺默认角色的接口和UI
|
||||||
|
- 在店铺管理列表页(`/shop-management/list`)添加"设置默认角色"操作入口
|
||||||
|
- 添加店铺默认角色管理对话框组件
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- 影响的规范: shop-management (新增)
|
||||||
|
- 影响的代码:
|
||||||
|
- `src/api/modules/shop.ts` - 新增3个API方法
|
||||||
|
- `src/types/api/shop.ts` - 新增类型定义
|
||||||
|
- `src/views/shop-management/list/index.vue` - 添加默认角色管理UI (假设该页面存在,如不存在需创建)
|
||||||
|
- 依赖的现有功能: 角色管理系统
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
# Shop Management - Specification Delta
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Query Shop Default Roles
|
||||||
|
|
||||||
|
系统SHALL支持查询指定店铺的默认角色列表。
|
||||||
|
|
||||||
|
**API端点**: `GET /api/admin/shops/{shop_id}/roles`
|
||||||
|
|
||||||
|
**路径参数**:
|
||||||
|
- `shop_id` (integer, required): 店铺ID
|
||||||
|
|
||||||
|
**响应数据**:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
shop_id: number // 店铺ID
|
||||||
|
roles: ShopRole[] | null // 角色列表
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShopRole {
|
||||||
|
role_id: number // 角色ID
|
||||||
|
role_name: string // 角色名称
|
||||||
|
role_desc: string // 角色描述
|
||||||
|
shop_id: number // 店铺ID
|
||||||
|
status: number // 状态 (0:禁用, 1:启用)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Scenario: 成功查询店铺默认角色
|
||||||
|
|
||||||
|
- **WHEN** 用户请求获取店铺ID为123的默认角色列表
|
||||||
|
- **THEN** 系统返回该店铺配置的所有默认角色
|
||||||
|
- **AND** 响应包含角色ID、名称、描述、状态等完整信息
|
||||||
|
|
||||||
|
#### Scenario: 查询不存在的店铺
|
||||||
|
|
||||||
|
- **WHEN** 用户请求获取不存在的店铺的默认角色
|
||||||
|
- **THEN** 系统返回400错误
|
||||||
|
- **AND** 错误消息说明店铺不存在
|
||||||
|
|
||||||
|
#### Scenario: 店铺没有配置默认角色
|
||||||
|
|
||||||
|
- **WHEN** 用户请求获取未配置默认角色的店铺
|
||||||
|
- **THEN** 系统返回成功响应
|
||||||
|
- **AND** `roles` 字段为空数组或null
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Assign Shop Default Roles
|
||||||
|
|
||||||
|
系统SHALL支持为指定店铺分配一个或多个默认角色。
|
||||||
|
|
||||||
|
**API端点**: `POST /api/admin/shops/{shop_id}/roles`
|
||||||
|
|
||||||
|
**路径参数**:
|
||||||
|
- `shop_id` (integer, required): 店铺ID
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
role_ids: number[] | null // 角色ID列表
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应数据**: 与"Query Shop Default Roles"相同的数据结构
|
||||||
|
|
||||||
|
#### Scenario: 成功分配单个默认角色
|
||||||
|
|
||||||
|
- **WHEN** 用户为店铺分配一个新的默认角色
|
||||||
|
- **THEN** 系统成功保存该角色配置
|
||||||
|
- **AND** 返回更新后的店铺默认角色列表
|
||||||
|
- **AND** 新分配的角色出现在列表中
|
||||||
|
|
||||||
|
#### Scenario: 成功分配多个默认角色
|
||||||
|
|
||||||
|
- **WHEN** 用户为店铺分配多个默认角色
|
||||||
|
- **THEN** 系统批量保存所有角色配置
|
||||||
|
- **AND** 返回完整的默认角色列表
|
||||||
|
- **AND** 所有新分配的角色都出现在列表中
|
||||||
|
|
||||||
|
#### Scenario: 分配已存在的角色
|
||||||
|
|
||||||
|
- **WHEN** 用户尝试分配店铺已配置的默认角色
|
||||||
|
- **THEN** 系统应当优雅处理(不重复添加或返回明确提示)
|
||||||
|
- **AND** 不影响其他正常角色的分配
|
||||||
|
|
||||||
|
#### Scenario: 分配不存在的角色
|
||||||
|
|
||||||
|
- **WHEN** 用户尝试分配不存在的角色ID
|
||||||
|
- **THEN** 系统返回400错误
|
||||||
|
- **AND** 错误消息说明哪些角色ID无效
|
||||||
|
|
||||||
|
#### Scenario: 空角色列表
|
||||||
|
|
||||||
|
- **WHEN** 用户提交空的角色ID列表
|
||||||
|
- **THEN** 系统接受请求
|
||||||
|
- **AND** 可能清空当前默认角色或保持不变(取决于业务需求)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Delete Shop Default Role
|
||||||
|
|
||||||
|
系统SHALL支持删除指定店铺的某个默认角色配置。
|
||||||
|
|
||||||
|
**API端点**: `DELETE /api/admin/shops/{shop_id}/roles/{role_id}`
|
||||||
|
|
||||||
|
**路径参数**:
|
||||||
|
- `shop_id` (integer, required): 店铺ID
|
||||||
|
- `role_id` (integer, required): 角色ID
|
||||||
|
|
||||||
|
**响应**: 标准成功/错误响应
|
||||||
|
|
||||||
|
#### Scenario: 成功删除默认角色
|
||||||
|
|
||||||
|
- **WHEN** 用户删除店铺的一个默认角色配置
|
||||||
|
- **THEN** 系统成功移除该角色配置
|
||||||
|
- **AND** 返回成功响应(code: 0)
|
||||||
|
- **AND** 后续查询该店铺默认角色列表时不再包含该角色
|
||||||
|
|
||||||
|
#### Scenario: 删除不存在的角色配置
|
||||||
|
|
||||||
|
- **WHEN** 用户尝试删除店铺未配置的角色
|
||||||
|
- **THEN** 系统返回400错误
|
||||||
|
- **AND** 错误消息说明该角色不在店铺的默认角色列表中
|
||||||
|
|
||||||
|
#### Scenario: 删除不存在的店铺或角色
|
||||||
|
|
||||||
|
- **WHEN** 用户使用无效的shop_id或role_id
|
||||||
|
- **THEN** 系统返回400错误
|
||||||
|
- **AND** 错误消息说明参数无效
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Shop Default Roles UI
|
||||||
|
|
||||||
|
店铺管理页面SHALL提供默认角色管理的用户界面。
|
||||||
|
|
||||||
|
#### Scenario: 在店铺列表中访问默认角色设置
|
||||||
|
|
||||||
|
- **WHEN** 用户在店铺管理列表页查看店铺列表
|
||||||
|
- **THEN** 每个店铺的操作列应当包含"设置默认角色"按钮或菜单项
|
||||||
|
- **AND** 点击后打开默认角色管理对话框
|
||||||
|
|
||||||
|
#### Scenario: 查看店铺当前默认角色
|
||||||
|
|
||||||
|
- **WHEN** 用户打开某店铺的默认角色管理对话框
|
||||||
|
- **THEN** 对话框显示该店铺当前已配置的所有默认角色
|
||||||
|
- **AND** 每个角色显示名称、描述和状态
|
||||||
|
- **AND** 提供删除按钮用于移除默认角色
|
||||||
|
|
||||||
|
#### Scenario: 添加默认角色
|
||||||
|
|
||||||
|
- **WHEN** 用户在对话框中选择要添加的角色
|
||||||
|
- **THEN** 系统提供角色选择器,展示可用的系统角色列表
|
||||||
|
- **AND** 支持多选
|
||||||
|
- **AND** 点击确认后调用分配接口
|
||||||
|
- **AND** 成功后刷新默认角色列表
|
||||||
|
|
||||||
|
#### Scenario: 删除默认角色
|
||||||
|
|
||||||
|
- **WHEN** 用户点击某个默认角色的删除按钮
|
||||||
|
- **THEN** 系统显示确认对话框
|
||||||
|
- **AND** 用户确认后调用删除接口
|
||||||
|
- **AND** 成功后从列表中移除该角色
|
||||||
|
|
||||||
|
#### Scenario: 加载状态和错误处理
|
||||||
|
|
||||||
|
- **WHEN** 进行任何API操作时
|
||||||
|
- **THEN** 界面应当显示加载状态(loading indicator)
|
||||||
|
- **AND** 如果操作失败,应当显示友好的错误消息
|
||||||
|
- **AND** 允许用户重试或关闭对话框
|
||||||
62
openspec/changes/add-shop-default-roles/tasks.md
Normal file
62
openspec/changes/add-shop-default-roles/tasks.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Implementation Tasks
|
||||||
|
|
||||||
|
## 1. 类型定义
|
||||||
|
|
||||||
|
- [x] 1.1 在 `src/types/api/shop.ts` 中添加 `ShopRoleResponse` 接口
|
||||||
|
- [x] 1.2 在 `src/types/api/shop.ts` 中添加 `ShopRolesResponse` 接口
|
||||||
|
- [x] 1.3 在 `src/types/api/shop.ts` 中添加 `AssignShopRolesRequest` 接口
|
||||||
|
|
||||||
|
## 2. API 服务层
|
||||||
|
|
||||||
|
- [x] 2.1 在 `ShopService` 中添加 `getShopRoles(shopId)` 方法
|
||||||
|
- [x] 2.2 在 `ShopService` 中添加 `assignShopRoles(shopId, roleIds)` 方法
|
||||||
|
- [x] 2.3 在 `ShopService` 中添加 `deleteShopRole(shopId, roleId)` 方法
|
||||||
|
|
||||||
|
## 3. UI 组件开发
|
||||||
|
|
||||||
|
- [x] 3.1 检查店铺管理列表页面 (`src/views/product/shop/index.vue` 已存在)
|
||||||
|
- [x] 3.2 在店铺列表操作列中添加"默认角色"按钮
|
||||||
|
- [x] 3.3 创建店铺默认角色管理对话框组件
|
||||||
|
- [x] 3.4 实现查询店铺默认角色列表功能
|
||||||
|
- [x] 3.5 实现添加默认角色功能(支持多选角色)
|
||||||
|
- [x] 3.6 实现删除默认角色功能
|
||||||
|
- [x] 3.7 添加角色选择器(从系统角色列表中选择)
|
||||||
|
- [x] 3.8 添加加载状态和错误处理
|
||||||
|
|
||||||
|
## 4. 验证和测试
|
||||||
|
|
||||||
|
- [ ] 4.1 测试查询店铺默认角色列表
|
||||||
|
- [ ] 4.2 测试添加默认角色(单个和多个)
|
||||||
|
- [ ] 4.3 测试删除默认角色
|
||||||
|
- [ ] 4.4 测试边界情况(角色已存在、店铺不存在等)
|
||||||
|
- [ ] 4.5 测试权限控制(只有有权限的用户才能操作)
|
||||||
|
|
||||||
|
## 实现说明
|
||||||
|
|
||||||
|
### 完成的功能
|
||||||
|
|
||||||
|
1. **类型定义** - 已在 `src/types/api/shop.ts` 添加了三个接口:
|
||||||
|
- `ShopRoleResponse`: 店铺角色响应实体
|
||||||
|
- `ShopRolesResponse`: 店铺角色列表响应
|
||||||
|
- `AssignShopRolesRequest`: 分配角色请求
|
||||||
|
|
||||||
|
2. **API 服务层** - 已在 `src/api/modules/shop.ts` 的 `ShopService` 类中添加:
|
||||||
|
- `getShopRoles(shopId)`: 获取店铺默认角色列表
|
||||||
|
- `assignShopRoles(shopId, data)`: 分配店铺默认角色
|
||||||
|
- `deleteShopRole(shopId, roleId)`: 删除店铺默认角色
|
||||||
|
|
||||||
|
3. **UI 实现** - 已在 `src/views/product/shop/index.vue` 中实现:
|
||||||
|
- 操作列新增"默认角色"按钮 (宽度从220改为280)
|
||||||
|
- 默认角色管理对话框,显示当前默认角色列表,支持删除操作
|
||||||
|
- 添加角色对话框,支持多选角色,已分配的角色显示为禁用状态
|
||||||
|
- 完整的加载状态(loading indicators)
|
||||||
|
- 错误处理和友好的提示消息
|
||||||
|
- 使用 `ShopService` 和 `RoleService` 调用后端API
|
||||||
|
|
||||||
|
### 待测试项
|
||||||
|
|
||||||
|
所有开发任务已完成,现在需要进行功能测试以确保:
|
||||||
|
- API 调用正常工作
|
||||||
|
- UI 交互符合预期
|
||||||
|
- 边界情况处理正确
|
||||||
|
- 权限控制生效
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Change: 重命名 API 字段 series_allocation_id 为 series_id
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
后端 API 字段命名不一致。前端表单使用 `series_id`(套餐系列ID),但 API 参数仍使用 `series_allocation_id`(套餐系列分配ID)。这导致命名混淆且不符合实际业务含义(现在直接指向套餐系列 ID,而非分配记录 ID)。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- **BREAKING**: 重命名 4 个 API 端点的请求/响应字段
|
||||||
|
- `PATCH /api/admin/iot-cards/series-binding` - 请求参数 `series_allocation_id` → `series_id`
|
||||||
|
- `PATCH /api/admin/devices/series-binding` - 请求参数 `series_allocation_id` → `series_id`
|
||||||
|
- `GET /api/admin/iot-cards/standalone` - 查询参数和响应字段 `series_allocation_id` → `series_id`
|
||||||
|
- `GET /api/admin/devices` - 查询参数和响应字段 `series_allocation_id` → `series_id`
|
||||||
|
|
||||||
|
- 前端同步修改:
|
||||||
|
- TypeScript 类型定义
|
||||||
|
- API 方法参数
|
||||||
|
- 页面组件调用代码
|
||||||
|
- 移除临时注释
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **Affected specs**: iot-card-api, device-api
|
||||||
|
- **Affected code**:
|
||||||
|
- `src/types/api/device.ts` (BatchSetDeviceSeriesBindingRequest)
|
||||||
|
- `src/types/api/card.ts` (BatchSetCardSeriesBindingRequest)
|
||||||
|
- `src/api/modules/device.ts` (batchSetDeviceSeriesBinding)
|
||||||
|
- `src/api/modules/card.ts` (batchSetCardSeriesBinding)
|
||||||
|
- `src/views/asset-management/device-list/index.vue`
|
||||||
|
- `src/views/asset-management/iot-card-management/index.vue`
|
||||||
|
- **Breaking change**: 需要后端先部署更新,前端再部署
|
||||||
|
- **Migration**: 更新所有使用这些字段的 API 调用
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# Device API Specification Delta
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Device Series ID Query Parameter
|
||||||
|
|
||||||
|
The system SHALL support filtering devices by package series ID in query parameters.
|
||||||
|
|
||||||
|
**Query Parameter**: `series_id` (number, optional)
|
||||||
|
|
||||||
|
#### Scenario: Filter devices by series
|
||||||
|
|
||||||
|
- **WHEN** admin queries devices with series_id parameter
|
||||||
|
- **THEN** system returns only devices associated with that package series
|
||||||
|
- **AND** returns empty list if no matches found
|
||||||
|
|
||||||
|
#### Scenario: Query without series filter
|
||||||
|
|
||||||
|
- **WHEN** admin queries devices without series_id parameter
|
||||||
|
- **THEN** system returns all devices matching other filter criteria
|
||||||
|
- **AND** series association is not considered
|
||||||
|
|
||||||
|
### Requirement: Device Series ID Response Field
|
||||||
|
|
||||||
|
The system SHALL include package series ID in device response data.
|
||||||
|
|
||||||
|
**Response Field**: `series_id` (number | null)
|
||||||
|
- Value is package series ID if device is bound to a series
|
||||||
|
- Value is null if device has no series binding
|
||||||
|
|
||||||
|
#### Scenario: Display series association
|
||||||
|
|
||||||
|
- **WHEN** system returns device in list response
|
||||||
|
- **THEN** each device includes series_id field
|
||||||
|
- **AND** field shows current series binding or null
|
||||||
|
|
||||||
|
#### Scenario: Null series binding
|
||||||
|
|
||||||
|
- **WHEN** device has no series binding
|
||||||
|
- **THEN** series_id field is null
|
||||||
|
- **AND** device can still be displayed in results
|
||||||
|
|
||||||
|
## RENAMED Requirements
|
||||||
|
|
||||||
|
### API Field Renaming
|
||||||
|
|
||||||
|
- FROM: `series_allocation_id` (in batch series binding endpoints)
|
||||||
|
- TO: `series_id`
|
||||||
|
|
||||||
|
**Reason**: Field name `series_allocation_id` implied a reference to an allocation record ID, but it actually stores the package series ID directly. Renaming to `series_id` clarifies this relationship and aligns with frontend naming conventions.
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Batch Set Device Series Binding
|
||||||
|
|
||||||
|
The system SHALL provide an endpoint to batch set package series binding for devices.
|
||||||
|
|
||||||
|
**Endpoint**: `PATCH /api/admin/devices/series-binding`
|
||||||
|
|
||||||
|
**Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"device_ids": [1, 2, 3],
|
||||||
|
"series_id": 123 // Changed from series_allocation_id
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Field Definitions**:
|
||||||
|
- `device_ids`: Array of device IDs (max 500 items)
|
||||||
|
- `series_id`: Package series ID (tb_package_series.id), 0 means clear association
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"success_count": 3,
|
||||||
|
"fail_count": 0,
|
||||||
|
"failed_items": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Scenario: Successful batch series binding
|
||||||
|
|
||||||
|
- **WHEN** admin calls batch series binding API with valid device IDs and series_id
|
||||||
|
- **THEN** system updates series association for all valid devices
|
||||||
|
- **AND** returns success count and failure details
|
||||||
|
|
||||||
|
#### Scenario: Clear series association
|
||||||
|
|
||||||
|
- **WHEN** admin calls batch series binding API with series_id = 0
|
||||||
|
- **THEN** system clears series association for specified devices
|
||||||
|
- **AND** returns success confirmation
|
||||||
|
|
||||||
|
#### Scenario: Partial failure handling
|
||||||
|
|
||||||
|
- **WHEN** some devices in the batch cannot be updated
|
||||||
|
- **THEN** system processes valid devices successfully
|
||||||
|
- **AND** returns failed_items list with reasons for failures
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# IoT Card API Specification Delta
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: IoT Card Series ID Query Parameter
|
||||||
|
|
||||||
|
The system SHALL support filtering standalone IoT cards by package series ID in query parameters.
|
||||||
|
|
||||||
|
**Query Parameter**: `series_id` (number, optional)
|
||||||
|
|
||||||
|
#### Scenario: Filter cards by series
|
||||||
|
|
||||||
|
- **WHEN** admin queries standalone cards with series_id parameter
|
||||||
|
- **THEN** system returns only cards associated with that package series
|
||||||
|
- **AND** returns empty list if no matches found
|
||||||
|
|
||||||
|
#### Scenario: Query without series filter
|
||||||
|
|
||||||
|
- **WHEN** admin queries cards without series_id parameter
|
||||||
|
- **THEN** system returns all cards matching other filter criteria
|
||||||
|
- **AND** series association is not considered
|
||||||
|
|
||||||
|
### Requirement: IoT Card Series ID Response Field
|
||||||
|
|
||||||
|
The system SHALL include package series ID in standalone IoT card response data.
|
||||||
|
|
||||||
|
**Response Field**: `series_id` (number | null)
|
||||||
|
- Value is package series ID if card is bound to a series
|
||||||
|
- Value is null if card has no series binding
|
||||||
|
|
||||||
|
#### Scenario: Display series association
|
||||||
|
|
||||||
|
- **WHEN** system returns IoT card in list response
|
||||||
|
- **THEN** each card includes series_id field
|
||||||
|
- **AND** field shows current series binding or null
|
||||||
|
|
||||||
|
#### Scenario: Null series binding
|
||||||
|
|
||||||
|
- **WHEN** card has no series binding
|
||||||
|
- **THEN** series_id field is null
|
||||||
|
- **AND** card can still be displayed in results
|
||||||
|
|
||||||
|
## RENAMED Requirements
|
||||||
|
|
||||||
|
### API Field Renaming
|
||||||
|
|
||||||
|
- FROM: `series_allocation_id` (in batch series binding endpoints)
|
||||||
|
- TO: `series_id`
|
||||||
|
|
||||||
|
**Reason**: Field name `series_allocation_id` implied a reference to an allocation record ID, but it actually stores the package series ID directly. Renaming to `series_id` clarifies this relationship and aligns with frontend naming conventions.
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Batch Set Card Series Binding
|
||||||
|
|
||||||
|
The system SHALL provide an endpoint to batch set package series binding for IoT cards.
|
||||||
|
|
||||||
|
**Endpoint**: `PATCH /api/admin/iot-cards/series-binding`
|
||||||
|
|
||||||
|
**Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"iccids": ["898600..."],
|
||||||
|
"series_id": 123 // Changed from series_allocation_id
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Field Definitions**:
|
||||||
|
- `iccids`: Array of ICCIDs (max 500 items)
|
||||||
|
- `series_id`: Package series ID (tb_package_series.id), 0 means clear association
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"success_count": 100,
|
||||||
|
"fail_count": 0,
|
||||||
|
"failed_items": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Scenario: Successful batch series binding
|
||||||
|
|
||||||
|
- **WHEN** admin calls batch series binding API with valid ICCIDs and series_id
|
||||||
|
- **THEN** system updates series association for all valid cards
|
||||||
|
- **AND** returns success count and failure details
|
||||||
|
|
||||||
|
#### Scenario: Clear series association
|
||||||
|
|
||||||
|
- **WHEN** admin calls batch series binding API with series_id = 0
|
||||||
|
- **THEN** system clears series association for specified cards
|
||||||
|
- **AND** returns success confirmation
|
||||||
|
|
||||||
|
#### Scenario: Partial failure handling
|
||||||
|
|
||||||
|
- **WHEN** some cards in the batch cannot be updated
|
||||||
|
- **THEN** system processes valid cards successfully
|
||||||
|
- **AND** returns failed_items list with reasons for failures
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Implementation Tasks
|
||||||
|
|
||||||
|
## 1. 类型定义更新
|
||||||
|
|
||||||
|
### 1.1 批量设置接口参数重命名
|
||||||
|
- [x] 1.1.1 更新 `src/types/api/device.ts` 中 BatchSetDeviceSeriesBindingRequest 接口,将 `series_allocation_id` 改为 `series_id`
|
||||||
|
- [x] 1.1.2 更新 `src/types/api/card.ts` 中 BatchSetCardSeriesBindingRequest 接口,将 `series_allocation_id` 改为 `series_id`
|
||||||
|
|
||||||
|
### 1.2 查询参数新增字段
|
||||||
|
- [x] 1.2.1 在 `src/types/api/device.ts` 的 DeviceQueryParams 接口中添加 `series_id?: number` 查询参数
|
||||||
|
- [x] 1.2.2 在 `src/types/api/card.ts` 的 StandaloneCardQueryParams 接口中添加 `series_id?: number` 查询参数
|
||||||
|
|
||||||
|
### 1.3 响应类型新增字段
|
||||||
|
- [x] 1.3.1 在 `src/types/api/device.ts` 的 Device 接口中添加 `series_id?: number | null` 响应字段(在 updated_at 后面)
|
||||||
|
- [x] 1.3.2 在 `src/types/api/card.ts` 的 StandaloneIotCard 接口中添加 `series_id?: number | null` 响应字段(在 updated_at 后面)
|
||||||
|
|
||||||
|
## 2. API 方法更新
|
||||||
|
|
||||||
|
- [x] 2.1 更新 `src/api/modules/device.ts` 中 batchSetDeviceSeriesBinding 方法参数,将 `series_allocation_id` 改为 `series_id`
|
||||||
|
- [x] 2.2 更新 `src/api/modules/card.ts` 中 batchSetCardSeriesBinding 方法参数,将 `series_allocation_id` 改为 `series_id`
|
||||||
|
|
||||||
|
## 3. 页面组件更新
|
||||||
|
|
||||||
|
- [x] 3.1 更新 `src/views/asset-management/device-list/index.vue` 中调用 batchSetDeviceSeriesBinding 的代码,将参数名从 `series_allocation_id` 改为 `series_id`
|
||||||
|
- [x] 3.2 移除 device-list/index.vue 中第 1191 行的临时注释
|
||||||
|
- [x] 3.3 更新 `src/views/asset-management/iot-card-management/index.vue` 中调用 batchSetCardSeriesBinding 的代码,将参数名从 `series_allocation_id` 改为 `series_id`
|
||||||
|
- [x] 3.4 移除 iot-card-management/index.vue 中第 1326 行的临时注释
|
||||||
|
|
||||||
|
## 4. 验证
|
||||||
|
|
||||||
|
- [x] 4.1 运行 TypeScript 类型检查确认无类型错误
|
||||||
|
- [ ] 4.2 本地测试批量设置 IoT 卡系列绑定功能
|
||||||
|
- [ ] 4.3 本地测试批量设置设备系列绑定功能
|
||||||
|
- [ ] 4.4 确认所有相关功能正常工作
|
||||||
82
openspec/changes/update-package-management-api/proposal.md
Normal file
82
openspec/changes/update-package-management-api/proposal.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Change: 更新套餐管理API以支持新的佣金配置模型
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
根据最新的API文档(`docs/修改原来的套餐管理.md`),后端API规范发生了重大变更,主要涉及:
|
||||||
|
|
||||||
|
1. **套餐系列** - 新增一次性佣金配置支持(包括固定/梯度佣金、强制充值、时效类型等复杂配置)
|
||||||
|
2. **系列分配** - 佣金配置模型完全重构,从简单的base_commission改为更细粒度的配置
|
||||||
|
3. **单套餐分配** - 新增系列关联字段和分配者信息字段
|
||||||
|
|
||||||
|
当前前端实现的类型定义(`src/types/api/packageManagement.ts`)和API服务与新规范不匹配,需要更新以支持新的数据结构。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
### 1. 类型定义重构
|
||||||
|
|
||||||
|
**修改**: `src/types/api/packageManagement.ts`
|
||||||
|
|
||||||
|
**套餐系列相关类型**:
|
||||||
|
- `PackageSeriesResponse` - 新增 `enable_one_time_commission` 和 `one_time_commission_config`
|
||||||
|
- `CreatePackageSeriesRequest` - 新增一次性佣金配置字段
|
||||||
|
- `UpdatePackageSeriesRequest` - 新增一次性佣金配置字段
|
||||||
|
- 新增 `SeriesOneTimeCommissionConfig` - 一次性佣金配置(支持固定/梯度模式)
|
||||||
|
- 新增 `OneTimeCommissionTier` - 梯度佣金档位配置
|
||||||
|
|
||||||
|
**系列分配相关类型**:
|
||||||
|
- **BREAKING**: `ShopSeriesAllocationResponse` - 完全重构字段结构
|
||||||
|
- 移除: `base_commission` (BaseCommissionConfig)
|
||||||
|
- 新增: `series_code`, `enable_force_recharge`, `enable_one_time_commission`, `force_recharge_amount`, `force_recharge_trigger_type`, `one_time_commission_amount`, `one_time_commission_threshold`, `one_time_commission_trigger`
|
||||||
|
- **BREAKING**: `CreateShopSeriesAllocationRequest` - 重构请求字段
|
||||||
|
- **BREAKING**: `UpdateShopSeriesAllocationRequest` - 重构请求字段
|
||||||
|
- 移除不再使用的类型: `BaseCommissionConfig`, `TierEntry`, `TierCommissionConfig`, `OneTimeCommissionTierEntry`, `OneTimeCommissionConfig`(旧版)
|
||||||
|
|
||||||
|
**单套餐分配相关类型**:
|
||||||
|
- `ShopPackageAllocationResponse` - 字段调整
|
||||||
|
- 新增: `series_id`, `series_name`, `series_allocation_id`, `allocator_shop_id`, `allocator_shop_name`
|
||||||
|
- 移除: `allocation_id` (重命名为 `series_allocation_id`), `calculated_cost_price`
|
||||||
|
- `ShopPackageAllocationQueryParams` - 新增 `series_allocation_id`, `allocator_shop_id` 筛选参数
|
||||||
|
|
||||||
|
### 2. API服务层无需修改
|
||||||
|
|
||||||
|
现有的API服务类(`PackageSeriesService`, `ShopSeriesAllocationService`, `ShopPackageAllocationService`)的方法签名保持不变,只是底层数据类型发生变化。
|
||||||
|
|
||||||
|
### 3. 页面/组件适配(实施阶段处理)
|
||||||
|
|
||||||
|
以下文件需要适配新的数据结构(本提案阶段不编写代码):
|
||||||
|
- 套餐系列管理页面 - 需支持一次性佣金配置表单
|
||||||
|
- 系列分配页面 - 需重构佣金配置表单UI
|
||||||
|
- 单套餐分配页面 - 需展示新增的关联字段
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
### 受影响的规范
|
||||||
|
|
||||||
|
- `package-series-management` - MODIFIED (新增一次性佣金配置能力)
|
||||||
|
- `shop-series-allocation` - MODIFIED (佣金配置模型重构)
|
||||||
|
- `shop-package-allocation` - MODIFIED (新增系列关联和分配者字段)
|
||||||
|
|
||||||
|
### 受影响的代码
|
||||||
|
|
||||||
|
- `src/types/api/packageManagement.ts` - **BREAKING CHANGE** 类型定义重构
|
||||||
|
- 所有使用 `ShopSeriesAllocationResponse` 的页面/组件 - 需适配新字段结构
|
||||||
|
- 所有使用 `PackageSeriesResponse` 的页面/组件 - 需处理新增的一次性佣金配置
|
||||||
|
|
||||||
|
### 依赖关系
|
||||||
|
|
||||||
|
- 后端API已按新规范实现(`docs/修改原来的套餐管理.md`)
|
||||||
|
- 前端需要先完成类型定义更新,再更新UI组件
|
||||||
|
|
||||||
|
### 风险评估
|
||||||
|
|
||||||
|
- **高风险**: 这是一个BREAKING CHANGE,会影响所有使用系列分配的功能
|
||||||
|
- **兼容性**: 需要确保后端API已更新,否则会导致运行时错误
|
||||||
|
- **迁移成本**: 现有页面中的表单组件需要重写以支持新的数据结构
|
||||||
|
- **测试需求**: 需要全面测试套餐系列、系列分配、单套餐分配的所有CRUD操作
|
||||||
|
|
||||||
|
### 建议迁移策略
|
||||||
|
|
||||||
|
1. 先更新类型定义,确保TypeScript编译通过
|
||||||
|
2. 逐个页面/组件适配新数据结构
|
||||||
|
3. 与后端联调确认新API规范工作正常
|
||||||
|
4. 完成后删除旧的、不再使用的类型定义
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# Package Series Management Specification Delta
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Package Series Data Model
|
||||||
|
|
||||||
|
套餐系列的数据模型SHALL支持一次性佣金配置,包括固定佣金、梯度佣金、强制充值、时效类型等高级功能。
|
||||||
|
|
||||||
|
**字段定义**:
|
||||||
|
- `id` (number) - 系列ID
|
||||||
|
- `series_code` (string) - 系列编码,唯一标识
|
||||||
|
- `series_name` (string) - 系列名称
|
||||||
|
- `description` (string, optional) - 系列描述
|
||||||
|
- `enable_one_time_commission` (boolean) - 是否启用一次性佣金
|
||||||
|
- `one_time_commission_config` (SeriesOneTimeCommissionConfig, optional) - 一次性佣金配置对象
|
||||||
|
- `status` (number) - 状态 (1:启用, 2:禁用)
|
||||||
|
- `created_at` (string) - 创建时间
|
||||||
|
- `updated_at` (string) - 更新时间
|
||||||
|
|
||||||
|
**一次性佣金配置结构** (`SeriesOneTimeCommissionConfig`):
|
||||||
|
- `enable` (boolean) - 是否启用一次性佣金
|
||||||
|
- `commission_type` (string) - 佣金类型: "fixed"(固定) | "tiered"(梯度)
|
||||||
|
- `commission_amount` (number, optional) - 固定佣金金额(分),commission_type=fixed时使用
|
||||||
|
- `threshold` (number) - 触发阈值(分)
|
||||||
|
- `trigger_type` (string) - 触发类型: "first_recharge"(首次充值) | "accumulated_recharge"(累计充值)
|
||||||
|
- `tiers` (OneTimeCommissionTier[], optional) - 梯度配置列表,commission_type=tiered时使用
|
||||||
|
- `enable_force_recharge` (boolean) - 是否启用强充
|
||||||
|
- `force_amount` (number, optional) - 强充金额(分)
|
||||||
|
- `force_calc_type` (string, optional) - 强充计算类型: "fixed"(固定) | "dynamic"(动态)
|
||||||
|
- `validity_type` (string) - 时效类型: "permanent"(永久) | "fixed_date"(固定日期) | "relative"(相对时长)
|
||||||
|
- `validity_value` (string, optional) - 时效值(日期字符串或月数)
|
||||||
|
|
||||||
|
**梯度档位结构** (`OneTimeCommissionTier`):
|
||||||
|
- `threshold` (number) - 达标阈值
|
||||||
|
- `dimension` (string) - 统计维度: "sales_count"(销量) | "sales_amount"(销售额)
|
||||||
|
- `amount` (number) - 佣金金额(分)
|
||||||
|
- `stat_scope` (string, optional) - 统计范围: "self"(仅自己) | "self_and_sub"(自己+下级)
|
||||||
|
|
||||||
|
#### Scenario: 创建套餐系列并启用固定一次性佣金
|
||||||
|
|
||||||
|
- **GIVEN** 管理员登录系统
|
||||||
|
- **WHEN** 创建套餐系列时提供以下配置:
|
||||||
|
- `series_code`: "SERIES001"
|
||||||
|
- `series_name`: "标准流量系列"
|
||||||
|
- `enable_one_time_commission`: true
|
||||||
|
- `one_time_commission_config`:
|
||||||
|
- `enable`: true
|
||||||
|
- `commission_type`: "fixed"
|
||||||
|
- `commission_amount`: 5000 (50元)
|
||||||
|
- `threshold`: 10000 (100元)
|
||||||
|
- `trigger_type`: "first_recharge"
|
||||||
|
- `validity_type`: "permanent"
|
||||||
|
- **THEN** 系统SHALL创建套餐系列,并保存一次性佣金配置
|
||||||
|
|
||||||
|
#### Scenario: 创建套餐系列并启用梯度一次性佣金
|
||||||
|
|
||||||
|
- **GIVEN** 管理员登录系统
|
||||||
|
- **WHEN** 创建套餐系列时提供以下配置:
|
||||||
|
- `series_code`: "SERIES002"
|
||||||
|
- `series_name`: "高级流量系列"
|
||||||
|
- `enable_one_time_commission`: true
|
||||||
|
- `one_time_commission_config`:
|
||||||
|
- `enable`: true
|
||||||
|
- `commission_type`: "tiered"
|
||||||
|
- `threshold`: 5000
|
||||||
|
- `trigger_type`: "accumulated_recharge"
|
||||||
|
- `tiers`: [
|
||||||
|
{ threshold: 10, dimension: "sales_count", amount: 3000 },
|
||||||
|
{ threshold: 50, dimension: "sales_count", amount: 8000 }
|
||||||
|
]
|
||||||
|
- `validity_type`: "relative"
|
||||||
|
- `validity_value`: "12" (12个月)
|
||||||
|
- **THEN** 系统SHALL创建套餐系列,并保存梯度佣金配置
|
||||||
|
|
||||||
|
#### Scenario: 查询启用一次性佣金的套餐系列
|
||||||
|
|
||||||
|
- **GIVEN** 系统中存在多个套餐系列,部分启用了一次性佣金
|
||||||
|
- **WHEN** 使用查询参数 `enable_one_time_commission=true` 查询列表
|
||||||
|
- **THEN** 系统SHALL返回所有启用一次性佣金的套餐系列
|
||||||
|
|
||||||
|
#### Scenario: 更新套餐系列的一次性佣金配置
|
||||||
|
|
||||||
|
- **GIVEN** 系统中存在ID为1的套餐系列
|
||||||
|
- **WHEN** 更新该系列时提供新的一次性佣金配置:
|
||||||
|
- `one_time_commission_config.commission_amount`: 8000 (从5000更新到8000)
|
||||||
|
- `one_time_commission_config.enable_force_recharge`: true
|
||||||
|
- `one_time_commission_config.force_amount`: 20000
|
||||||
|
- `one_time_commission_config.force_calc_type`: "fixed"
|
||||||
|
- **THEN** 系统SHALL更新套餐系列的一次性佣金配置,并返回更新后的数据
|
||||||
|
|
||||||
|
#### Scenario: 获取套餐系列详情时包含完整的一次性佣金配置
|
||||||
|
|
||||||
|
- **GIVEN** 系统中存在ID为1的套餐系列,已配置一次性佣金
|
||||||
|
- **WHEN** 请求获取该系列的详情 (GET /api/admin/package-series/1)
|
||||||
|
- **THEN** 系统SHALL返回包含完整 `one_time_commission_config` 对象的响应数据
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: One-Time Commission Query Filter
|
||||||
|
|
||||||
|
系统SHALL支持按一次性佣金启用状态筛选套餐系列列表。
|
||||||
|
|
||||||
|
#### Scenario: 按一次性佣金启用状态筛选
|
||||||
|
|
||||||
|
- **GIVEN** 系统中存在多个套餐系列
|
||||||
|
- **WHEN** 使用查询参数 `enable_one_time_commission=true`
|
||||||
|
- **THEN** 系统SHALL只返回 `enable_one_time_commission` 为 true 的套餐系列
|
||||||
|
- **AND** 分页信息正确反映筛选后的结果数量
|
||||||
|
|
||||||
|
### Requirement: One-Time Commission Configuration Validation
|
||||||
|
|
||||||
|
系统SHALL验证一次性佣金配置的完整性和逻辑一致性。
|
||||||
|
|
||||||
|
#### Scenario: 固定佣金模式的验证
|
||||||
|
|
||||||
|
- **GIVEN** 管理员创建套餐系列
|
||||||
|
- **WHEN** `commission_type` 为 "fixed"
|
||||||
|
- **THEN** 系统SHALL要求 `commission_amount` 字段必填
|
||||||
|
- **AND** `tiers` 字段不应被使用
|
||||||
|
|
||||||
|
#### Scenario: 梯度佣金模式的验证
|
||||||
|
|
||||||
|
- **GIVEN** 管理员创建套餐系列
|
||||||
|
- **WHEN** `commission_type` 为 "tiered"
|
||||||
|
- **THEN** 系统SHALL要求 `tiers` 数组不为空
|
||||||
|
- **AND** 每个梯度档位的 `threshold`, `dimension`, `amount` 字段必填
|
||||||
|
|
||||||
|
#### Scenario: 强制充值配置的验证
|
||||||
|
|
||||||
|
- **GIVEN** 管理员创建套餐系列
|
||||||
|
- **WHEN** `enable_force_recharge` 为 true
|
||||||
|
- **THEN** 系统SHALL要求 `force_amount` 和 `force_calc_type` 字段必填
|
||||||
|
|
||||||
|
#### Scenario: 时效配置的验证
|
||||||
|
|
||||||
|
- **GIVEN** 管理员创建套餐系列
|
||||||
|
- **WHEN** `validity_type` 为 "fixed_date"
|
||||||
|
- **THEN** 系统SHALL要求 `validity_value` 字段必填,且格式为有效的日期字符串
|
||||||
|
- **WHEN** `validity_type` 为 "relative"
|
||||||
|
- **THEN** 系统SHALL要求 `validity_value` 字段必填,且为表示月数的数字字符串
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
# Shop Package Allocation Specification Delta
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Shop Package Allocation Data Model
|
||||||
|
|
||||||
|
单套餐分配的数据模型SHALL新增系列关联字段和分配者信息字段,以支持更完整的分配关系追溯。
|
||||||
|
|
||||||
|
**字段定义**:
|
||||||
|
- `id` (number) - 分配ID
|
||||||
|
- `package_id` (number) - 套餐ID
|
||||||
|
- `package_code` (string) - 套餐编码
|
||||||
|
- `package_name` (string) - 套餐名称
|
||||||
|
- `series_id` (number) - 套餐系列ID
|
||||||
|
- `series_name` (string) - 套餐系列名称
|
||||||
|
- `shop_id` (number) - 被分配的店铺ID
|
||||||
|
- `shop_name` (string) - 被分配的店铺名称
|
||||||
|
- `allocator_shop_id` (number) - 分配者店铺ID,0表示平台分配
|
||||||
|
- `allocator_shop_name` (string) - 分配者店铺名称
|
||||||
|
- `series_allocation_id` (number, nullable) - 关联的系列分配ID(可选)
|
||||||
|
- `cost_price` (number) - 该代理的成本价(分)
|
||||||
|
- `status` (number) - 状态 (1:启用, 2:禁用)
|
||||||
|
- `created_at` (string) - 创建时间
|
||||||
|
- `updated_at` (string) - 更新时间
|
||||||
|
|
||||||
|
**查询参数** (`ShopPackageAllocationQueryParams`):
|
||||||
|
- `page` (number, optional) - 页码
|
||||||
|
- `page_size` (number, optional) - 每页数量
|
||||||
|
- `shop_id` (number, optional) - 被分配的店铺ID
|
||||||
|
- `package_id` (number, optional) - 套餐ID
|
||||||
|
- `series_allocation_id` (number, optional) - 系列分配ID
|
||||||
|
- `allocator_shop_id` (number, optional) - 分配者店铺ID
|
||||||
|
- `status` (number, optional) - 状态
|
||||||
|
|
||||||
|
#### Scenario: 创建单套餐分配时包含系列信息
|
||||||
|
|
||||||
|
- **GIVEN** 平台管理员登录系统
|
||||||
|
- **AND** 系统中存在ID为10的套餐,该套餐属于系列ID为2,系列名称为"标准流量系列"
|
||||||
|
- **AND** 系统中存在ID为100的店铺
|
||||||
|
- **WHEN** 创建单套餐分配:
|
||||||
|
- `package_id`: 10
|
||||||
|
- `shop_id`: 100
|
||||||
|
- `cost_price`: 15000
|
||||||
|
- **THEN** 系统SHALL创建单套餐分配记录
|
||||||
|
- **AND** 响应数据中自动填充 `series_id` 为 2
|
||||||
|
- **AND** 响应数据中自动填充 `series_name` 为 "标准流量系列"
|
||||||
|
- **AND** 响应数据中 `allocator_shop_id` 为 0(平台分配)
|
||||||
|
|
||||||
|
#### Scenario: 创建单套餐分配并关联系列分配
|
||||||
|
|
||||||
|
- **GIVEN** 系统中存在ID为20的系列分配,关联系列ID为2,店铺ID为100
|
||||||
|
- **AND** 系列2下存在套餐ID为10
|
||||||
|
- **WHEN** 为店铺100创建套餐10的分配
|
||||||
|
- **THEN** 系统MAY自动关联系列分配,设置 `series_allocation_id` 为 20
|
||||||
|
|
||||||
|
#### Scenario: 查询单套餐分配列表并显示完整关联信息
|
||||||
|
|
||||||
|
- **GIVEN** 系统中存在多个单套餐分配
|
||||||
|
- **WHEN** 查询单套餐分配列表 (GET /api/admin/shop-package-allocations)
|
||||||
|
- **THEN** 系统SHALL返回所有分配记录
|
||||||
|
- **AND** 每条记录包含 `series_id`, `series_name`, `allocator_shop_id`, `allocator_shop_name`
|
||||||
|
- **AND** 如果存在关联的系列分配,`series_allocation_id` 不为空
|
||||||
|
|
||||||
|
#### Scenario: 按系列分配ID筛选单套餐分配
|
||||||
|
|
||||||
|
- **GIVEN** 系统中存在多个单套餐分配,部分关联了系列分配ID为20的系列分配
|
||||||
|
- **WHEN** 使用查询参数 `series_allocation_id=20`
|
||||||
|
- **THEN** 系统SHALL返回所有 `series_allocation_id` 为 20 的单套餐分配记录
|
||||||
|
|
||||||
|
#### Scenario: 按分配者店铺ID筛选单套餐分配
|
||||||
|
|
||||||
|
- **GIVEN** 系统中存在多个单套餐分配
|
||||||
|
- **WHEN** 使用查询参数 `allocator_shop_id=50`
|
||||||
|
- **THEN** 系统SHALL返回所有由店铺ID为50的代理商创建的单套餐分配记录
|
||||||
|
|
||||||
|
#### Scenario: 获取单套餐分配详情时显示完整信息
|
||||||
|
|
||||||
|
- **GIVEN** 系统中存在ID为100的单套餐分配
|
||||||
|
- **WHEN** 请求获取该分配的详情 (GET /api/admin/shop-package-allocations/100)
|
||||||
|
- **THEN** 响应数据SHALL包含所有字段,包括:
|
||||||
|
- `series_id`, `series_name` - 套餐所属系列信息
|
||||||
|
- `allocator_shop_id`, `allocator_shop_name` - 分配者信息
|
||||||
|
- `series_allocation_id` - 关联的系列分配ID(如果存在)
|
||||||
|
|
||||||
|
## RENAMED Requirements
|
||||||
|
|
||||||
|
- FROM: `allocation_id`
|
||||||
|
- TO: `series_allocation_id`
|
||||||
|
|
||||||
|
**说明**: 将字段名从 `allocation_id` 重命名为 `series_allocation_id`,使其语义更加明确,表示关联的系列分配ID。
|
||||||
|
|
||||||
|
## REMOVED Requirements
|
||||||
|
|
||||||
|
### Requirement: Calculated Cost Price Field
|
||||||
|
|
||||||
|
旧版的 `calculated_cost_price` 字段已被移除。
|
||||||
|
|
||||||
|
**Reason**: 该字段仅用于参考,增加了数据模型的复杂性,且前端很少使用。成本价信息应直接使用 `cost_price` 字段。
|
||||||
|
|
||||||
|
**Migration**: 移除所有使用 `calculated_cost_price` 字段的代码。如果需要查看原始成本价信息,可以通过关联的系列分配或套餐系列获取。
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Series Information in Package Allocation
|
||||||
|
|
||||||
|
系统SHALL在单套餐分配响应数据中包含套餐所属系列的ID和名称。
|
||||||
|
|
||||||
|
#### Scenario: 创建分配时自动关联系列信息
|
||||||
|
|
||||||
|
- **GIVEN** 系统中存在套餐,且该套餐属于某个系列
|
||||||
|
- **WHEN** 创建单套餐分配
|
||||||
|
- **THEN** 系统SHALL自动从套餐数据中获取 `series_id` 和 `series_name`
|
||||||
|
- **AND** 填充到分配响应数据中
|
||||||
|
|
||||||
|
### Requirement: Allocator Information Tracking in Package Allocation
|
||||||
|
|
||||||
|
系统SHALL记录单套餐分配的创建者(分配者)信息,包括分配者店铺ID和名称。
|
||||||
|
|
||||||
|
#### Scenario: 平台创建的单套餐分配标记为平台分配
|
||||||
|
|
||||||
|
- **GIVEN** 平台管理员登录系统
|
||||||
|
- **WHEN** 创建单套餐分配
|
||||||
|
- **THEN** 系统SHALL自动设置 `allocator_shop_id` 为 0
|
||||||
|
- **AND** `allocator_shop_name` 为 "平台" 或相应的平台标识
|
||||||
|
|
||||||
|
#### Scenario: 代理商创建的单套餐分配记录代理商信息
|
||||||
|
|
||||||
|
- **GIVEN** 代理商A登录系统,shop_id为50,shop_name为"代理商A"
|
||||||
|
- **WHEN** 创建单套餐分配给下级代理
|
||||||
|
- **THEN** 系统SHALL自动设置 `allocator_shop_id` 为 50
|
||||||
|
- **AND** `allocator_shop_name` 为 "代理商A"
|
||||||
|
|
||||||
|
### Requirement: Query by Allocator Shop ID
|
||||||
|
|
||||||
|
系统SHALL支持按分配者店铺ID筛选单套餐分配列表。
|
||||||
|
|
||||||
|
#### Scenario: 查看平台创建的所有分配
|
||||||
|
|
||||||
|
- **GIVEN** 系统中存在多个单套餐分配
|
||||||
|
- **WHEN** 使用查询参数 `allocator_shop_id=0`
|
||||||
|
- **THEN** 系统SHALL返回所有由平台创建的单套餐分配记录
|
||||||
|
|
||||||
|
#### Scenario: 查看特定代理商创建的所有分配
|
||||||
|
|
||||||
|
- **GIVEN** 系统中存在多个单套餐分配
|
||||||
|
- **WHEN** 使用查询参数 `allocator_shop_id=50`
|
||||||
|
- **THEN** 系统SHALL返回所有由店铺ID为50的代理商创建的单套餐分配记录
|
||||||
|
|
||||||
|
### Requirement: Query by Series Allocation ID
|
||||||
|
|
||||||
|
系统SHALL支持按系列分配ID筛选单套餐分配列表,以便查找由特定系列分配派生的所有单套餐分配。
|
||||||
|
|
||||||
|
#### Scenario: 查找系列分配的所有派生单套餐分配
|
||||||
|
|
||||||
|
- **GIVEN** 系统中存在系列分配ID为20的系列分配
|
||||||
|
- **AND** 该系列分配下有多个单套餐分配
|
||||||
|
- **WHEN** 使用查询参数 `series_allocation_id=20`
|
||||||
|
- **THEN** 系统SHALL返回所有关联该系列分配的单套餐分配记录
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
# Shop Series Allocation Specification Delta
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Shop Series Allocation Data Model
|
||||||
|
|
||||||
|
套餐系列分配的数据模型SHALL重构为更细粒度的佣金配置结构,移除旧的 `base_commission` 模式,采用独立的一次性佣金和强制充值配置字段。
|
||||||
|
|
||||||
|
**字段定义**:
|
||||||
|
- `id` (number) - 分配ID
|
||||||
|
- `series_id` (number) - 套餐系列ID
|
||||||
|
- `series_name` (string) - 套餐系列名称
|
||||||
|
- `series_code` (string) - 套餐系列编码
|
||||||
|
- `shop_id` (number) - 被分配的店铺ID
|
||||||
|
- `shop_name` (string) - 被分配的店铺名称
|
||||||
|
- `allocator_shop_id` (number) - 分配者店铺ID,0表示平台分配
|
||||||
|
- `allocator_shop_name` (string) - 分配者店铺名称
|
||||||
|
- `enable_one_time_commission` (boolean) - 是否启用一次性佣金
|
||||||
|
- `one_time_commission_amount` (number) - 该代理能拿的一次性佣金金额上限(分)
|
||||||
|
- `one_time_commission_threshold` (number, optional) - 一次性佣金触发阈值(分)
|
||||||
|
- `one_time_commission_trigger` (string, optional) - 一次性佣金触发类型: "first_recharge" | "accumulated_recharge"
|
||||||
|
- `enable_force_recharge` (boolean) - 是否启用强制充值
|
||||||
|
- `force_recharge_amount` (number, optional) - 强制充值金额(分)
|
||||||
|
- `force_recharge_trigger_type` (number, optional) - 强充触发类型: 1(单次充值) | 2(累计充值)
|
||||||
|
- `status` (number) - 状态 (1:启用, 2:禁用)
|
||||||
|
- `created_at` (string) - 创建时间
|
||||||
|
- `updated_at` (string) - 更新时间
|
||||||
|
|
||||||
|
**创建请求字段** (`CreateShopSeriesAllocationRequest`):
|
||||||
|
- `series_id` (number, required) - 套餐系列ID
|
||||||
|
- `shop_id` (number, required) - 被分配的店铺ID
|
||||||
|
- `one_time_commission_amount` (number, required) - 一次性佣金金额上限(分)
|
||||||
|
- `enable_one_time_commission` (boolean, optional) - 是否启用一次性佣金
|
||||||
|
- `one_time_commission_threshold` (number, optional) - 一次性佣金触发阈值(分)
|
||||||
|
- `one_time_commission_trigger` (string, optional) - 一次性佣金触发类型
|
||||||
|
- `enable_force_recharge` (boolean, optional) - 是否启用强制充值
|
||||||
|
- `force_recharge_amount` (number, optional) - 强制充值金额(分)
|
||||||
|
- `force_recharge_trigger_type` (number, optional) - 强充触发类型
|
||||||
|
|
||||||
|
**更新请求字段** (`UpdateShopSeriesAllocationRequest`):
|
||||||
|
- 所有字段均为可选,允许部分更新
|
||||||
|
- `enable_one_time_commission` (boolean, optional)
|
||||||
|
- `one_time_commission_amount` (number, optional)
|
||||||
|
- `one_time_commission_threshold` (number, optional)
|
||||||
|
- `one_time_commission_trigger` (string, optional)
|
||||||
|
- `enable_force_recharge` (boolean, optional)
|
||||||
|
- `force_recharge_amount` (number, optional)
|
||||||
|
- `force_recharge_trigger_type` (number, optional)
|
||||||
|
- `status` (number, optional)
|
||||||
|
|
||||||
|
#### Scenario: 创建系列分配并启用一次性佣金
|
||||||
|
|
||||||
|
- **GIVEN** 平台管理员登录系统
|
||||||
|
- **AND** 系统中存在ID为1的套餐系列和ID为100的店铺
|
||||||
|
- **WHEN** 创建系列分配时提供以下配置:
|
||||||
|
- `series_id`: 1
|
||||||
|
- `shop_id`: 100
|
||||||
|
- `one_time_commission_amount`: 5000
|
||||||
|
- `enable_one_time_commission`: true
|
||||||
|
- `one_time_commission_threshold`: 10000
|
||||||
|
- `one_time_commission_trigger`: "first_recharge"
|
||||||
|
- **THEN** 系统SHALL创建系列分配记录
|
||||||
|
- **AND** 响应数据中 `allocator_shop_id` 为 0(平台分配)
|
||||||
|
- **AND** 响应数据中包含所有一次性佣金配置字段
|
||||||
|
|
||||||
|
#### Scenario: 创建系列分配并启用强制充值
|
||||||
|
|
||||||
|
- **GIVEN** 代理商A登录系统,shop_id为50
|
||||||
|
- **AND** 系统中存在ID为2的套餐系列和ID为101的下级代理店铺
|
||||||
|
- **WHEN** 创建系列分配时提供以下配置:
|
||||||
|
- `series_id`: 2
|
||||||
|
- `shop_id`: 101
|
||||||
|
- `one_time_commission_amount`: 3000
|
||||||
|
- `enable_force_recharge`: true
|
||||||
|
- `force_recharge_amount`: 15000
|
||||||
|
- `force_recharge_trigger_type`: 1 (单次充值)
|
||||||
|
- **THEN** 系统SHALL创建系列分配记录
|
||||||
|
- **AND** 响应数据中 `allocator_shop_id` 为 50(代理商A)
|
||||||
|
- **AND** 响应数据中包含所有强制充值配置字段
|
||||||
|
|
||||||
|
#### Scenario: 更新系列分配的一次性佣金配置
|
||||||
|
|
||||||
|
- **GIVEN** 系统中存在ID为10的系列分配
|
||||||
|
- **WHEN** 更新该分配时提供以下字段:
|
||||||
|
- `one_time_commission_amount`: 8000 (从5000更新到8000)
|
||||||
|
- `one_time_commission_threshold`: 20000
|
||||||
|
- **THEN** 系统SHALL更新指定字段
|
||||||
|
- **AND** 其他字段保持不变
|
||||||
|
- **AND** 响应数据中 `updated_at` 字段更新为当前时间
|
||||||
|
|
||||||
|
#### Scenario: 更新系列分配的强制充值配置
|
||||||
|
|
||||||
|
- **GIVEN** 系统中存在ID为10的系列分配
|
||||||
|
- **WHEN** 更新该分配时提供以下字段:
|
||||||
|
- `enable_force_recharge`: true
|
||||||
|
- `force_recharge_amount`: 25000
|
||||||
|
- `force_recharge_trigger_type`: 2 (累计充值)
|
||||||
|
- **THEN** 系统SHALL更新强制充值配置
|
||||||
|
- **AND** 响应数据中包含更新后的强制充值字段
|
||||||
|
|
||||||
|
#### Scenario: 查询系列分配列表并显示分配者信息
|
||||||
|
|
||||||
|
- **GIVEN** 系统中存在多个系列分配,部分由平台创建,部分由代理商创建
|
||||||
|
- **WHEN** 查询系列分配列表 (GET /api/admin/shop-series-allocations)
|
||||||
|
- **THEN** 系统SHALL返回所有分配记录
|
||||||
|
- **AND** 每条记录包含 `allocator_shop_id` 和 `allocator_shop_name`
|
||||||
|
- **AND** 平台创建的分配记录中 `allocator_shop_id` 为 0
|
||||||
|
|
||||||
|
#### Scenario: 按分配者店铺ID筛选系列分配
|
||||||
|
|
||||||
|
- **GIVEN** 系统中存在多个系列分配
|
||||||
|
- **WHEN** 使用查询参数 `allocator_shop_id=50`
|
||||||
|
- **THEN** 系统SHALL返回所有由店铺ID为50的代理商创建的分配记录
|
||||||
|
|
||||||
|
## REMOVED Requirements
|
||||||
|
|
||||||
|
### Requirement: Base Commission Configuration
|
||||||
|
|
||||||
|
旧版的 `base_commission` 配置模式已被移除,不再使用 `BaseCommissionConfig` 类型。
|
||||||
|
|
||||||
|
**Reason**: 新的佣金配置模型更加灵活和细粒度,将一次性佣金配置分散到独立字段中,便于查询和管理。
|
||||||
|
|
||||||
|
**Migration**: 现有使用 `base_commission` 字段的代码需要迁移到新的字段结构。如果有历史数据,需要后端提供数据迁移方案。
|
||||||
|
|
||||||
|
### Requirement: Tiered Commission Configuration
|
||||||
|
|
||||||
|
旧版的 `TierCommissionConfig` (周期性梯度返佣) 配置已被移除。
|
||||||
|
|
||||||
|
**Reason**: 系列分配层面不再支持复杂的梯度返佣配置,改为在系列级别统一配置一次性佣金梯度。
|
||||||
|
|
||||||
|
**Migration**: 梯度佣金配置现在在套餐系列级别 (`PackageSeries.one_time_commission_config.tiers`) 进行管理。
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Allocator Information Tracking
|
||||||
|
|
||||||
|
系统SHALL记录系列分配的创建者(分配者)信息,包括分配者店铺ID和名称。
|
||||||
|
|
||||||
|
#### Scenario: 平台创建的分配标记为平台分配
|
||||||
|
|
||||||
|
- **GIVEN** 平台管理员登录系统
|
||||||
|
- **WHEN** 创建系列分配
|
||||||
|
- **THEN** 系统SHALL自动设置 `allocator_shop_id` 为 0
|
||||||
|
- **AND** `allocator_shop_name` 为 "平台" 或相应的平台标识
|
||||||
|
|
||||||
|
#### Scenario: 代理商创建的分配记录代理商信息
|
||||||
|
|
||||||
|
- **GIVEN** 代理商A登录系统,shop_id为50,shop_name为"代理商A"
|
||||||
|
- **WHEN** 创建系列分配给下级代理
|
||||||
|
- **THEN** 系统SHALL自动设置 `allocator_shop_id` 为 50
|
||||||
|
- **AND** `allocator_shop_name` 为 "代理商A"
|
||||||
|
|
||||||
|
### Requirement: Series Code in Allocation Response
|
||||||
|
|
||||||
|
系统SHALL在系列分配响应数据中包含套餐系列编码 (`series_code`)。
|
||||||
|
|
||||||
|
#### Scenario: 获取系列分配详情时包含系列编码
|
||||||
|
|
||||||
|
- **GIVEN** 系统中存在ID为10的系列分配,关联系列编码为"SERIES001"
|
||||||
|
- **WHEN** 请求获取该分配的详情 (GET /api/admin/shop-series-allocations/10)
|
||||||
|
- **THEN** 响应数据中SHALL包含 `series_code` 字段
|
||||||
|
- **AND** 其值为 "SERIES001"
|
||||||
62
openspec/changes/update-package-management-api/tasks.md
Normal file
62
openspec/changes/update-package-management-api/tasks.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Implementation Tasks
|
||||||
|
|
||||||
|
## 1. 类型定义更新
|
||||||
|
|
||||||
|
- [ ] 1.1 更新 `PackageSeriesResponse` - 新增一次性佣金字段
|
||||||
|
- [ ] 1.2 新增 `SeriesOneTimeCommissionConfig` 类型定义
|
||||||
|
- [ ] 1.3 新增 `OneTimeCommissionTier` 类型定义
|
||||||
|
- [ ] 1.4 更新 `CreatePackageSeriesRequest` - 新增一次性佣金配置参数
|
||||||
|
- [ ] 1.5 更新 `UpdatePackageSeriesRequest` - 新增一次性佣金配置参数
|
||||||
|
- [ ] 1.6 更新 `PackageSeriesQueryParams` - 新增 `enable_one_time_commission` 筛选参数
|
||||||
|
- [ ] 1.7 重构 `ShopSeriesAllocationResponse` - 替换为新的字段结构
|
||||||
|
- [ ] 1.8 重构 `CreateShopSeriesAllocationRequest` - 更新为新的请求结构
|
||||||
|
- [ ] 1.9 重构 `UpdateShopSeriesAllocationRequest` - 更新为新的请求结构
|
||||||
|
- [ ] 1.10 更新 `ShopPackageAllocationResponse` - 新增系列关联和分配者字段
|
||||||
|
- [ ] 1.11 更新 `ShopPackageAllocationQueryParams` - 新增筛选参数
|
||||||
|
- [ ] 1.12 移除废弃的类型定义 (`BaseCommissionConfig`, 旧版 `OneTimeCommissionConfig` 等)
|
||||||
|
|
||||||
|
## 2. 套餐系列管理页面适配
|
||||||
|
|
||||||
|
- [ ] 2.1 更新套餐系列列表 - 显示一次性佣金启用状态
|
||||||
|
- [ ] 2.2 更新套餐系列表单 - 新增一次性佣金配置区块
|
||||||
|
- [ ] 2.3 实现一次性佣金配置表单组件 (固定模式)
|
||||||
|
- [ ] 2.4 实现一次性佣金配置表单组件 (梯度模式)
|
||||||
|
- [ ] 2.5 实现强制充值配置表单组件
|
||||||
|
- [ ] 2.6 实现时效配置表单组件 (永久/固定日期/相对时长)
|
||||||
|
- [ ] 2.7 添加一次性佣金配置的表单验证逻辑
|
||||||
|
- [ ] 2.8 更新套餐系列详情展示 - 显示完整的一次性佣金配置
|
||||||
|
|
||||||
|
## 3. 系列分配管理页面适配
|
||||||
|
|
||||||
|
- [ ] 3.1 更新系列分配列表 - 适配新的响应字段结构
|
||||||
|
- [ ] 3.2 移除基础返佣配置表单(旧版)
|
||||||
|
- [ ] 3.3 实现新的系列分配表单 - 强制充值配置
|
||||||
|
- [ ] 3.4 实现新的系列分配表单 - 一次性佣金配置
|
||||||
|
- [ ] 3.5 更新系列分配详情展示 - 显示所有新字段
|
||||||
|
- [ ] 3.6 更新系列分配编辑表单 - 支持修改所有可选字段
|
||||||
|
- [ ] 3.7 添加表单验证逻辑 - 确保必填字段和逻辑一致性
|
||||||
|
|
||||||
|
## 4. 单套餐分配页面适配
|
||||||
|
|
||||||
|
- [ ] 4.1 更新单套餐分配列表 - 显示系列信息和分配者信息
|
||||||
|
- [ ] 4.2 更新列表筛选条件 - 新增系列分配ID和分配者店铺ID筛选
|
||||||
|
- [ ] 4.3 更新单套餐分配详情展示 - 显示所有新字段
|
||||||
|
- [ ] 4.4 移除列表中的"计算成本价"字段显示(已废弃)
|
||||||
|
|
||||||
|
## 5. 测试与验证
|
||||||
|
|
||||||
|
- [ ] 5.1 测试套餐系列的创建 - 包含一次性佣金配置(固定模式)
|
||||||
|
- [ ] 5.2 测试套餐系列的创建 - 包含一次性佣金配置(梯度模式)
|
||||||
|
- [ ] 5.3 测试套餐系列的编辑 - 修改一次性佣金配置
|
||||||
|
- [ ] 5.4 测试系列分配的创建 - 使用新的字段结构
|
||||||
|
- [ ] 5.5 测试系列分配的编辑 - 修改强制充值和一次性佣金参数
|
||||||
|
- [ ] 5.6 测试单套餐分配 - 验证系列关联字段正确显示
|
||||||
|
- [ ] 5.7 测试单套餐分配筛选 - 验证新增筛选参数工作正常
|
||||||
|
- [ ] 5.8 回归测试 - 确保所有现有功能仍然正常工作
|
||||||
|
|
||||||
|
## 6. 文档与清理
|
||||||
|
|
||||||
|
- [ ] 6.1 更新类型定义文件的注释 - 标注新增/修改的字段
|
||||||
|
- [ ] 6.2 删除废弃的类型定义和相关代码
|
||||||
|
- [ ] 6.3 更新相关组件的注释文档
|
||||||
|
- [ ] 6.4 记录API变更影响和迁移说明(如需要)
|
||||||
@@ -64,6 +64,7 @@
|
|||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
"pinia-plugin-persistedstate": "^4.3.0",
|
"pinia-plugin-persistedstate": "^4.3.0",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"qrcode.vue": "^3.6.0",
|
"qrcode.vue": "^3.6.0",
|
||||||
"vue": "^3.5.12",
|
"vue": "^3.5.12",
|
||||||
"vue-draggable-plus": "^0.6.0",
|
"vue-draggable-plus": "^0.6.0",
|
||||||
|
|||||||
6817
pnpm-lock.yaml
generated
6817
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@
|
|||||||
import zh from 'element-plus/es/locale/lang/zh-cn'
|
import zh from 'element-plus/es/locale/lang/zh-cn'
|
||||||
import en from 'element-plus/es/locale/lang/en'
|
import en from 'element-plus/es/locale/lang/en'
|
||||||
import { systemUpgrade } from '@/utils'
|
import { systemUpgrade } from '@/utils'
|
||||||
import { UserService } from './api/usersApi'
|
import { AuthService } from '@/api/modules'
|
||||||
import { ApiStatus } from './utils/http/status'
|
import { ApiStatus } from './utils/http/status'
|
||||||
import { setThemeTransitionClass } from '@/utils'
|
import { setThemeTransitionClass } from '@/utils'
|
||||||
import { checkStorageCompatibility } from '@/utils'
|
import { checkStorageCompatibility } from '@/utils'
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
const getUserInfo = async () => {
|
const getUserInfo = async () => {
|
||||||
if (userStore.isLogin && userStore.accessToken) {
|
if (userStore.isLogin && userStore.accessToken) {
|
||||||
try {
|
try {
|
||||||
const res = await UserService.getUserInfo()
|
const res = await AuthService.getUserInfo()
|
||||||
if (res.code === ApiStatus.success && res.data) {
|
if (res.code === ApiStatus.success && res.data) {
|
||||||
// API 返回的是 { user, permissions },我们需要保存 user 和 permissions
|
// API 返回的是 { user, permissions },我们需要保存 user 和 permissions
|
||||||
userStore.setUserInfo(res.data.user)
|
userStore.setUserInfo(res.data.user)
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
/**
|
|
||||||
* 认证相关 API
|
|
||||||
*/
|
|
||||||
import request from '@/utils/http'
|
|
||||||
import {
|
|
||||||
BaseResponse,
|
|
||||||
LoginParams,
|
|
||||||
LoginData,
|
|
||||||
UserInfoResponse,
|
|
||||||
RefreshTokenData
|
|
||||||
} from '@/types/api'
|
|
||||||
|
|
||||||
export class AuthService {
|
|
||||||
/**
|
|
||||||
* 用户登录
|
|
||||||
* @param params 登录参数
|
|
||||||
*/
|
|
||||||
static login(params: LoginParams): Promise<BaseResponse<LoginData>> {
|
|
||||||
return request.post<BaseResponse<LoginData>>({
|
|
||||||
url: '/api/admin/login',
|
|
||||||
data: params
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户信息
|
|
||||||
* GET /api/admin/me
|
|
||||||
*/
|
|
||||||
static getUserInfo(): Promise<BaseResponse<UserInfoResponse>> {
|
|
||||||
return request.get<BaseResponse<UserInfoResponse>>({
|
|
||||||
url: '/api/admin/me'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户登出
|
|
||||||
*/
|
|
||||||
static logout(): Promise<BaseResponse<void>> {
|
|
||||||
return request.post<BaseResponse<void>>({
|
|
||||||
url: '/api/admin/logout'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新 Token
|
|
||||||
* @param refreshToken 刷新令牌
|
|
||||||
*/
|
|
||||||
static refreshToken(refreshToken: string): Promise<BaseResponse<RefreshTokenData>> {
|
|
||||||
return request.post<BaseResponse<RefreshTokenData>>({
|
|
||||||
url: '/api/auth/refresh',
|
|
||||||
data: { refreshToken }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -92,4 +92,24 @@ export class AccountService extends BaseService {
|
|||||||
static removeRoleFromAccount(accountId: number, roleId: number): Promise<BaseResponse> {
|
static removeRoleFromAccount(accountId: number, roleId: number): Promise<BaseResponse> {
|
||||||
return this.delete<BaseResponse>(`/api/admin/accounts/${accountId}/roles/${roleId}`)
|
return this.delete<BaseResponse>(`/api/admin/accounts/${accountId}/roles/${roleId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改账号密码
|
||||||
|
* PUT /api/admin/accounts/{id}/password
|
||||||
|
* @param id 账号ID
|
||||||
|
* @param password 新密码
|
||||||
|
*/
|
||||||
|
static updateAccountPassword(id: number, password: string): Promise<BaseResponse> {
|
||||||
|
return this.put<BaseResponse>(`/api/admin/accounts/${id}/password`, { password })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改账号状态
|
||||||
|
* PUT /api/admin/accounts/{id}/status
|
||||||
|
* @param id 账号ID
|
||||||
|
* @param status 状态 (0:禁用, 1:启用)
|
||||||
|
*/
|
||||||
|
static updateAccountStatus(id: number, status: 0 | 1): Promise<BaseResponse> {
|
||||||
|
return this.put<BaseResponse>(`/api/admin/accounts/${id}/status`, { status })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
61
src/api/modules/agentRecharge.ts
Normal file
61
src/api/modules/agentRecharge.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* 代理充值相关 API
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BaseService } from '../BaseService'
|
||||||
|
import type {
|
||||||
|
AgentRecharge,
|
||||||
|
AgentRechargeQueryParams,
|
||||||
|
AgentRechargeListResponse,
|
||||||
|
CreateAgentRechargeRequest,
|
||||||
|
ConfirmOfflinePaymentRequest,
|
||||||
|
BaseResponse
|
||||||
|
} from '@/types/api'
|
||||||
|
|
||||||
|
export class AgentRechargeService extends BaseService {
|
||||||
|
/**
|
||||||
|
* 获取代理充值订单列表
|
||||||
|
* @param params 查询参数
|
||||||
|
*/
|
||||||
|
static getAgentRecharges(
|
||||||
|
params?: AgentRechargeQueryParams
|
||||||
|
): Promise<BaseResponse<AgentRechargeListResponse>> {
|
||||||
|
return this.get<BaseResponse<AgentRechargeListResponse>>(
|
||||||
|
'/api/admin/agent-recharges',
|
||||||
|
params
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取代理充值订单详情
|
||||||
|
* @param id 充值订单ID
|
||||||
|
*/
|
||||||
|
static getAgentRechargeById(id: number): Promise<BaseResponse<AgentRecharge>> {
|
||||||
|
return this.getOne<AgentRecharge>(`/api/admin/agent-recharges/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建代理充值订单
|
||||||
|
* @param data 创建充值订单请求参数
|
||||||
|
*/
|
||||||
|
static createAgentRecharge(
|
||||||
|
data: CreateAgentRechargeRequest
|
||||||
|
): Promise<BaseResponse<AgentRecharge>> {
|
||||||
|
return this.post<BaseResponse<AgentRecharge>>('/api/admin/agent-recharges', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认线下充值
|
||||||
|
* @param id 充值订单ID
|
||||||
|
* @param data 确认线下充值请求参数
|
||||||
|
*/
|
||||||
|
static confirmOfflinePayment(
|
||||||
|
id: number,
|
||||||
|
data: ConfirmOfflinePaymentRequest
|
||||||
|
): Promise<BaseResponse<AgentRecharge>> {
|
||||||
|
return this.post<BaseResponse<AgentRecharge>>(
|
||||||
|
`/api/admin/agent-recharges/${id}/offline-pay`,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
179
src/api/modules/asset.ts
Normal file
179
src/api/modules/asset.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
/**
|
||||||
|
* 资产管理 API 服务
|
||||||
|
* 对应文档:asset-detail-refactor-api-changes.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BaseService } from '../BaseService'
|
||||||
|
import type {
|
||||||
|
BaseResponse,
|
||||||
|
AssetType,
|
||||||
|
AssetResolveResponse,
|
||||||
|
AssetRealtimeStatusResponse,
|
||||||
|
AssetRefreshResponse,
|
||||||
|
AssetPackageUsageRecord,
|
||||||
|
AssetCurrentPackageResponse,
|
||||||
|
DeviceStopResponse,
|
||||||
|
DeviceStartResponse,
|
||||||
|
CardStopResponse,
|
||||||
|
CardStartResponse,
|
||||||
|
AssetWalletTransactionListResponse,
|
||||||
|
AssetWalletTransactionParams,
|
||||||
|
AssetWalletResponse
|
||||||
|
} from '@/types/api'
|
||||||
|
|
||||||
|
export class AssetService extends BaseService {
|
||||||
|
/**
|
||||||
|
* 通过任意标识符查询设备或卡的完整详情
|
||||||
|
* 支持虚拟号、ICCID、IMEI、SN、MSISDN
|
||||||
|
* GET /api/admin/assets/resolve/:identifier
|
||||||
|
* @param identifier 资产标识符(虚拟号、ICCID、IMEI、SN、MSISDN)
|
||||||
|
*/
|
||||||
|
static resolveAsset(identifier: string): Promise<BaseResponse<AssetResolveResponse>> {
|
||||||
|
return this.getOne<AssetResolveResponse>(`/api/admin/assets/resolve/${identifier}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取资产实时状态(直接读 DB/Redis,不调网关)
|
||||||
|
* GET /api/admin/assets/:asset_type/:id/realtime-status
|
||||||
|
* @param assetType 资产类型 (card 或 device)
|
||||||
|
* @param id 资产ID
|
||||||
|
*/
|
||||||
|
static getRealtimeStatus(
|
||||||
|
assetType: AssetType,
|
||||||
|
id: number
|
||||||
|
): Promise<BaseResponse<AssetRealtimeStatusResponse>> {
|
||||||
|
return this.getOne<AssetRealtimeStatusResponse>(
|
||||||
|
`/api/admin/assets/${assetType}/${id}/realtime-status`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主动调网关拉取最新数据后返回
|
||||||
|
* POST /api/admin/assets/:asset_type/:id/refresh
|
||||||
|
* 注意:设备有 30 秒冷却期,冷却中调用返回 429
|
||||||
|
* @param assetType 资产类型 (card 或 device)
|
||||||
|
* @param id 资产ID
|
||||||
|
*/
|
||||||
|
static refreshAsset(
|
||||||
|
assetType: AssetType,
|
||||||
|
id: number
|
||||||
|
): Promise<BaseResponse<AssetRefreshResponse>> {
|
||||||
|
return this.post<BaseResponse<AssetRefreshResponse>>(
|
||||||
|
`/api/admin/assets/${assetType}/${id}/refresh`,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询该资产所有套餐记录,含虚流量换算字段
|
||||||
|
* GET /api/admin/assets/:asset_type/:id/packages
|
||||||
|
* @param assetType 资产类型 (card 或 device)
|
||||||
|
* @param id 资产ID
|
||||||
|
*/
|
||||||
|
static getAssetPackages(
|
||||||
|
assetType: AssetType,
|
||||||
|
id: number
|
||||||
|
): Promise<BaseResponse<AssetPackageUsageRecord[]>> {
|
||||||
|
return this.get<BaseResponse<AssetPackageUsageRecord[]>>(
|
||||||
|
`/api/admin/assets/${assetType}/${id}/packages`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前生效中的主套餐
|
||||||
|
* GET /api/admin/assets/:asset_type/:id/current-package
|
||||||
|
* 无生效套餐时返回 404
|
||||||
|
* @param assetType 资产类型 (card 或 device)
|
||||||
|
* @param id 资产ID
|
||||||
|
*/
|
||||||
|
static getCurrentPackage(
|
||||||
|
assetType: AssetType,
|
||||||
|
id: number
|
||||||
|
): Promise<BaseResponse<AssetCurrentPackageResponse>> {
|
||||||
|
return this.getOne<AssetCurrentPackageResponse>(
|
||||||
|
`/api/admin/assets/${assetType}/${id}/current-package`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 设备停复机操作 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量停机设备下所有已实名卡
|
||||||
|
* POST /api/admin/assets/device/:device_id/stop
|
||||||
|
* 停机成功后设置 1 小时停机保护期(保护期内禁止复机)
|
||||||
|
* @param deviceId 设备ID
|
||||||
|
*/
|
||||||
|
static stopDevice(deviceId: number): Promise<BaseResponse<DeviceStopResponse>> {
|
||||||
|
return this.post<BaseResponse<DeviceStopResponse>>(
|
||||||
|
`/api/admin/assets/device/${deviceId}/stop`,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量复机设备下所有已实名卡
|
||||||
|
* POST /api/admin/assets/device/:device_id/start
|
||||||
|
* 复机成功后设置 1 小时复机保护期(保护期内禁止停机)
|
||||||
|
* @param deviceId 设备ID
|
||||||
|
*/
|
||||||
|
static startDevice(deviceId: number): Promise<BaseResponse<void>> {
|
||||||
|
return this.post<BaseResponse<void>>(`/api/admin/assets/device/${deviceId}/start`, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 单卡停复机操作 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动停机单张卡(通过 ICCID)
|
||||||
|
* POST /api/admin/assets/card/:iccid/stop
|
||||||
|
* 若卡绑定的设备在复机保护期内,返回 403
|
||||||
|
* @param iccid ICCID
|
||||||
|
*/
|
||||||
|
static stopCard(iccid: string): Promise<BaseResponse<void>> {
|
||||||
|
return this.post<BaseResponse<void>>(`/api/admin/assets/card/${iccid}/stop`, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动复机单张卡(通过 ICCID)
|
||||||
|
* POST /api/admin/assets/card/:iccid/start
|
||||||
|
* 若卡绑定的设备在停机保护期内,返回 403
|
||||||
|
* @param iccid ICCID
|
||||||
|
*/
|
||||||
|
static startCard(iccid: string): Promise<BaseResponse<void>> {
|
||||||
|
return this.post<BaseResponse<void>>(`/api/admin/assets/card/${iccid}/start`, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 钱包查询 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定卡或设备的钱包余额概况
|
||||||
|
* GET /api/admin/assets/:asset_type/:id/wallet
|
||||||
|
* 企业账号禁止调用
|
||||||
|
* @param assetType 资产类型 (card 或 device)
|
||||||
|
* @param id 资产ID
|
||||||
|
*/
|
||||||
|
static getAssetWallet(
|
||||||
|
assetType: AssetType,
|
||||||
|
id: number
|
||||||
|
): Promise<BaseResponse<AssetWalletResponse>> {
|
||||||
|
return this.getOne<AssetWalletResponse>(`/api/admin/assets/${assetType}/${id}/wallet`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询指定资产的钱包收支流水
|
||||||
|
* GET /api/admin/assets/:asset_type/:id/wallet/transactions
|
||||||
|
* 企业账号禁止调用
|
||||||
|
* @param assetType 资产类型 (card 或 device)
|
||||||
|
* @param id 资产ID
|
||||||
|
* @param params 查询参数
|
||||||
|
*/
|
||||||
|
static getWalletTransactions(
|
||||||
|
assetType: AssetType,
|
||||||
|
id: number,
|
||||||
|
params?: AssetWalletTransactionParams
|
||||||
|
): Promise<BaseResponse<AssetWalletTransactionListResponse>> {
|
||||||
|
return this.get<BaseResponse<AssetWalletTransactionListResponse>>(
|
||||||
|
`/api/admin/assets/${assetType}/${id}/wallet/transactions`,
|
||||||
|
params
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,42 +16,42 @@ import type {
|
|||||||
|
|
||||||
export class AuthService extends BaseService {
|
export class AuthService extends BaseService {
|
||||||
/**
|
/**
|
||||||
* 用户登录
|
* 用户登录(统一认证接口)
|
||||||
* @param params 登录参数
|
* @param params 登录参数
|
||||||
*/
|
*/
|
||||||
static login(params: LoginParams): Promise<BaseResponse<LoginData>> {
|
static login(params: LoginParams): Promise<BaseResponse<LoginData>> {
|
||||||
return this.post<BaseResponse<LoginData>>('/api/admin/login', params)
|
return this.post<BaseResponse<LoginData>>('/api/auth/login', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 退出登录
|
* 退出登录(统一认证接口)
|
||||||
*/
|
*/
|
||||||
static logout(): Promise<BaseResponse> {
|
static logout(): Promise<BaseResponse> {
|
||||||
return this.post<BaseResponse>('/api/admin/logout')
|
return this.post<BaseResponse>('/api/auth/logout')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前用户信息
|
* 获取当前用户信息(统一认证接口)
|
||||||
* GET /api/admin/me
|
* GET /api/auth/me
|
||||||
*/
|
*/
|
||||||
static getUserInfo(): Promise<BaseResponse<UserInfoResponse>> {
|
static getUserInfo(): Promise<BaseResponse<UserInfoResponse>> {
|
||||||
return this.get<BaseResponse<UserInfoResponse>>('/api/admin/me')
|
return this.get<BaseResponse<UserInfoResponse>>('/api/auth/me')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新 Token
|
* 刷新 Token(统一认证接口)
|
||||||
* @param params 刷新参数
|
* @param params 刷新参数
|
||||||
*/
|
*/
|
||||||
static refreshToken(params: RefreshTokenParams): Promise<BaseResponse<RefreshTokenData>> {
|
static refreshToken(params: RefreshTokenParams): Promise<BaseResponse<RefreshTokenData>> {
|
||||||
return this.post<BaseResponse<RefreshTokenData>>('/api/auth/refresh', params)
|
return this.post<BaseResponse<RefreshTokenData>>('/api/auth/refresh-token', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改密码
|
* 修改密码(统一认证接口)
|
||||||
* @param params 修改密码参数
|
* @param params 修改密码参数
|
||||||
*/
|
*/
|
||||||
static changePassword(params: ChangePasswordParams): Promise<BaseResponse> {
|
static changePassword(params: ChangePasswordParams): Promise<BaseResponse> {
|
||||||
return this.post<BaseResponse>('/api/auth/change-password', params)
|
return this.put<BaseResponse>('/api/auth/password', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import type {
|
|||||||
CardOrder,
|
CardOrder,
|
||||||
BaseResponse,
|
BaseResponse,
|
||||||
PaginationResponse,
|
PaginationResponse,
|
||||||
ListResponse
|
ListResponse,
|
||||||
|
GatewayRealnameLinkResponse
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
|
|
||||||
export class CardService extends BaseService {
|
export class CardService extends BaseService {
|
||||||
@@ -87,7 +88,8 @@ export class CardService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过ICCID查询单卡详情(新接口,用于单卡查询页面)
|
* 通过ICCID查询单卡详情(旧接口,已废弃)
|
||||||
|
* @deprecated 使用 AssetService.resolveAsset 替代
|
||||||
* @param iccid ICCID
|
* @param iccid ICCID
|
||||||
*/
|
*/
|
||||||
static getIotCardDetailByIccid(iccid: string): Promise<BaseResponse<any>> {
|
static getIotCardDetailByIccid(iccid: string): Promise<BaseResponse<any>> {
|
||||||
@@ -363,8 +365,70 @@ export class CardService extends BaseService {
|
|||||||
*/
|
*/
|
||||||
static batchSetCardSeriesBinding(data: {
|
static batchSetCardSeriesBinding(data: {
|
||||||
iccids: string[]
|
iccids: string[]
|
||||||
series_allocation_id: number
|
series_id: number
|
||||||
}): Promise<BaseResponse<any>> {
|
}): Promise<BaseResponse<any>> {
|
||||||
return this.patch('/api/admin/iot-cards/series-binding', data)
|
return this.patch('/api/admin/iot-cards/series-binding', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== IoT卡网关操作相关 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取实名认证链接
|
||||||
|
* @param iccid ICCID
|
||||||
|
*/
|
||||||
|
static getRealnameLink(iccid: string): Promise<BaseResponse<GatewayRealnameLinkResponse>> {
|
||||||
|
return this.get<BaseResponse<GatewayRealnameLinkResponse>>(
|
||||||
|
`/api/admin/iot-cards/${iccid}/realname-link`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用 IoT 卡
|
||||||
|
* @param id IoT卡ID
|
||||||
|
*/
|
||||||
|
static enableIotCard(id: number): Promise<BaseResponse> {
|
||||||
|
return this.post<BaseResponse>(`/api/admin/iot-cards/${id}/enable`, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停用 IoT 卡
|
||||||
|
* @param id IoT卡ID
|
||||||
|
*/
|
||||||
|
static disableIotCard(id: number): Promise<BaseResponse> {
|
||||||
|
return this.post<BaseResponse>(`/api/admin/iot-cards/${id}/disable`, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动停用 IoT 卡
|
||||||
|
* @param id IoT卡ID
|
||||||
|
*/
|
||||||
|
static deactivateIotCard(id: number): Promise<BaseResponse> {
|
||||||
|
return this.patch<BaseResponse>(`/api/admin/iot-cards/${id}/deactivate`, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取套餐流量详单(每日流量记录)
|
||||||
|
* @param packageUsageId 套餐使用记录ID
|
||||||
|
*/
|
||||||
|
static getPackageDailyRecords(packageUsageId: number): Promise<BaseResponse<{
|
||||||
|
package_name: string
|
||||||
|
package_usage_id: number
|
||||||
|
total_usage_mb: number
|
||||||
|
records: Array<{
|
||||||
|
date: string
|
||||||
|
daily_usage_mb: number
|
||||||
|
cumulative_usage_mb: number
|
||||||
|
}>
|
||||||
|
}>> {
|
||||||
|
return this.get<BaseResponse<{
|
||||||
|
package_name: string
|
||||||
|
package_usage_id: number
|
||||||
|
total_usage_mb: number
|
||||||
|
records: Array<{
|
||||||
|
date: string
|
||||||
|
daily_usage_mb: number
|
||||||
|
cumulative_usage_mb: number
|
||||||
|
}>
|
||||||
|
}>>(`/api/admin/package-usage/${packageUsageId}/daily-records`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* 客户账号管理 API
|
* 客户账号管理 API (企业账号)
|
||||||
|
*
|
||||||
|
* @deprecated 此 API 已废弃,请使用统一的 AccountService 代替
|
||||||
|
* @see AccountService - 统一账号管理接口 (/api/admin/accounts)
|
||||||
|
*
|
||||||
|
* 迁移说明:
|
||||||
|
* - 所有账号类型统一使用 /api/admin/accounts 接口
|
||||||
|
* - 通过 user_type 参数区分账号类型 (2=平台, 3=代理, 4=企业)
|
||||||
|
* - customer-accounts 已改名为 enterprise(企业账号)
|
||||||
|
* - 详见:docs/迁移指南.md
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { BaseService } from '../BaseService'
|
import { BaseService } from '../BaseService'
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ import type {
|
|||||||
DeviceImportTaskQueryParams,
|
DeviceImportTaskQueryParams,
|
||||||
DeviceImportTaskListResponse,
|
DeviceImportTaskListResponse,
|
||||||
DeviceImportTaskDetail,
|
DeviceImportTaskDetail,
|
||||||
BaseResponse
|
BaseResponse,
|
||||||
|
SetSpeedLimitRequest,
|
||||||
|
SwitchCardRequest,
|
||||||
|
SetWiFiRequest,
|
||||||
|
DeviceOperationResponse
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
|
|
||||||
export class DeviceService extends BaseService {
|
export class DeviceService extends BaseService {
|
||||||
@@ -43,11 +47,11 @@ export class DeviceService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过设备号查询设备详情
|
* 通过ICCID查询设备详情
|
||||||
* @param imei 设备号(IMEI)
|
* @param iccid ICCID
|
||||||
*/
|
*/
|
||||||
static getDeviceByImei(imei: string): Promise<BaseResponse<Device>> {
|
static getDeviceByIccid(iccid: string): Promise<BaseResponse<any>> {
|
||||||
return this.getOne<Device>(`/api/admin/devices/by-imei/${imei}`)
|
return this.getOne<any>(`/api/admin/devices/by-iccid/${iccid}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -153,8 +157,85 @@ export class DeviceService extends BaseService {
|
|||||||
*/
|
*/
|
||||||
static batchSetDeviceSeriesBinding(data: {
|
static batchSetDeviceSeriesBinding(data: {
|
||||||
device_ids: number[]
|
device_ids: number[]
|
||||||
series_allocation_id: number
|
series_id: number
|
||||||
}): Promise<BaseResponse<any>> {
|
}): Promise<BaseResponse<any>> {
|
||||||
return this.patch('/api/admin/devices/series-binding', data)
|
return this.patch('/api/admin/devices/series-binding', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 设备操作相关 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重启设备
|
||||||
|
* @param imei 设备号(IMEI)
|
||||||
|
*/
|
||||||
|
static rebootDevice(imei: string): Promise<BaseResponse<DeviceOperationResponse>> {
|
||||||
|
return this.post<BaseResponse<DeviceOperationResponse>>(
|
||||||
|
`/api/admin/devices/by-imei/${imei}/reboot`,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复出厂设置
|
||||||
|
* @param imei 设备号(IMEI)
|
||||||
|
*/
|
||||||
|
static resetDevice(imei: string): Promise<BaseResponse<DeviceOperationResponse>> {
|
||||||
|
return this.post<BaseResponse<DeviceOperationResponse>>(
|
||||||
|
`/api/admin/devices/by-imei/${imei}/reset`,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置限速
|
||||||
|
* @param imei 设备号(IMEI)
|
||||||
|
* @param data 限速参数
|
||||||
|
*/
|
||||||
|
static setSpeedLimit(
|
||||||
|
imei: string,
|
||||||
|
data: SetSpeedLimitRequest
|
||||||
|
): Promise<BaseResponse<DeviceOperationResponse>> {
|
||||||
|
return this.put<BaseResponse<DeviceOperationResponse>>(
|
||||||
|
`/api/admin/devices/by-imei/${imei}/speed-limit`,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换SIM卡
|
||||||
|
* @param imei 设备号(IMEI)
|
||||||
|
* @param data 切卡参数
|
||||||
|
*/
|
||||||
|
static switchCard(
|
||||||
|
imei: string,
|
||||||
|
data: SwitchCardRequest
|
||||||
|
): Promise<BaseResponse<DeviceOperationResponse>> {
|
||||||
|
return this.post<BaseResponse<DeviceOperationResponse>>(
|
||||||
|
`/api/admin/devices/by-imei/${imei}/switch-card`,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置WiFi
|
||||||
|
* @param imei 设备号(IMEI)
|
||||||
|
* @param data WiFi参数
|
||||||
|
*/
|
||||||
|
static setWiFi(
|
||||||
|
imei: string,
|
||||||
|
data: SetWiFiRequest
|
||||||
|
): Promise<BaseResponse<DeviceOperationResponse>> {
|
||||||
|
return this.put<BaseResponse<DeviceOperationResponse>>(
|
||||||
|
`/api/admin/devices/by-imei/${imei}/wifi`,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动停用设备
|
||||||
|
* @param id 设备ID
|
||||||
|
*/
|
||||||
|
static deactivateDevice(id: number): Promise<BaseResponse> {
|
||||||
|
return this.patch<BaseResponse>(`/api/admin/devices/${id}/deactivate`, {})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
129
src/api/modules/exchange.ts
Normal file
129
src/api/modules/exchange.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* 换货管理 API 服务
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BaseService } from '../BaseService'
|
||||||
|
import type { BaseResponse, PaginationResponse } from '@/types/api'
|
||||||
|
|
||||||
|
// 换货单查询参数
|
||||||
|
export interface ExchangeQueryParams {
|
||||||
|
page?: number
|
||||||
|
page_size?: number
|
||||||
|
status?: number // 换货状态
|
||||||
|
identifier?: string // 资产标识符(模糊匹配)
|
||||||
|
created_at_start?: string // 创建时间起始
|
||||||
|
created_at_end?: string // 创建时间结束
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建换货单请求
|
||||||
|
export interface CreateExchangeRequest {
|
||||||
|
exchange_reason: string // 换货原因
|
||||||
|
old_asset_type: string // 旧资产类型 (iot_card 或 device)
|
||||||
|
old_identifier: string // 旧资产标识符(ICCID/虚拟号/IMEI/SN)
|
||||||
|
remark?: string // 备注(可选)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 换货单响应
|
||||||
|
export interface ExchangeResponse {
|
||||||
|
id: number
|
||||||
|
exchange_no: string
|
||||||
|
exchange_reason: string
|
||||||
|
old_asset_type: string
|
||||||
|
old_asset_identifier: string
|
||||||
|
new_asset_type: string
|
||||||
|
new_asset_identifier: string
|
||||||
|
status: number // 换货状态(1:待填写信息, 2:待发货, 3:已发货待确认, 4:已完成, 5:已取消)
|
||||||
|
status_text: string
|
||||||
|
recipient_name?: string
|
||||||
|
recipient_phone?: string
|
||||||
|
recipient_address?: string
|
||||||
|
express_company?: string
|
||||||
|
express_no?: string
|
||||||
|
remark?: string
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 换货发货请求
|
||||||
|
export interface ShipExchangeRequest {
|
||||||
|
express_company: string // 快递公司
|
||||||
|
express_no: string // 快递单号
|
||||||
|
migrate_data: boolean // 是否迁移数据
|
||||||
|
new_identifier: string // 新资产标识符(ICCID/虚拟号/IMEI/SN)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消换货请求
|
||||||
|
export interface CancelExchangeRequest {
|
||||||
|
remark?: string // 取消备注(可选)
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExchangeService extends BaseService {
|
||||||
|
/**
|
||||||
|
* 获取换货单列表
|
||||||
|
* GET /api/admin/exchanges
|
||||||
|
* @param params 查询参数
|
||||||
|
*/
|
||||||
|
static getExchanges(
|
||||||
|
params?: ExchangeQueryParams
|
||||||
|
): Promise<BaseResponse<{ list: ExchangeResponse[]; page: number; page_size: number; total: number }>> {
|
||||||
|
return this.get<
|
||||||
|
BaseResponse<{ list: ExchangeResponse[]; page: number; page_size: number; total: number }>
|
||||||
|
>('/api/admin/exchanges', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建换货单
|
||||||
|
* POST /api/admin/exchanges
|
||||||
|
* @param data 创建参数
|
||||||
|
*/
|
||||||
|
static createExchange(data: CreateExchangeRequest): Promise<BaseResponse<ExchangeResponse>> {
|
||||||
|
return this.create<ExchangeResponse>('/api/admin/exchanges', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取换货单详情
|
||||||
|
* GET /api/admin/exchanges/{id}
|
||||||
|
* @param id 换货单ID
|
||||||
|
*/
|
||||||
|
static getExchangeDetail(id: number): Promise<BaseResponse<ExchangeResponse>> {
|
||||||
|
return this.getOne<ExchangeResponse>(`/api/admin/exchanges/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消换货
|
||||||
|
* POST /api/admin/exchanges/{id}/cancel
|
||||||
|
* @param id 换货单ID
|
||||||
|
* @param data 取消参数
|
||||||
|
*/
|
||||||
|
static cancelExchange(id: number, data?: CancelExchangeRequest): Promise<BaseResponse> {
|
||||||
|
return this.post<BaseResponse>(`/api/admin/exchanges/${id}/cancel`, data || {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认换货完成
|
||||||
|
* POST /api/admin/exchanges/{id}/complete
|
||||||
|
* @param id 换货单ID
|
||||||
|
*/
|
||||||
|
static completeExchange(id: number): Promise<BaseResponse> {
|
||||||
|
return this.post<BaseResponse>(`/api/admin/exchanges/${id}/complete`, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 旧资产转新
|
||||||
|
* POST /api/admin/exchanges/{id}/renew
|
||||||
|
* @param id 换货单ID
|
||||||
|
*/
|
||||||
|
static renewExchange(id: number): Promise<BaseResponse> {
|
||||||
|
return this.post<BaseResponse>(`/api/admin/exchanges/${id}/renew`, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 换货发货
|
||||||
|
* POST /api/admin/exchanges/{id}/ship
|
||||||
|
* @param id 换货单ID
|
||||||
|
* @param data 发货参数
|
||||||
|
*/
|
||||||
|
static shipExchange(id: number, data: ShipExchangeRequest): Promise<BaseResponse<ExchangeResponse>> {
|
||||||
|
return this.post<BaseResponse<ExchangeResponse>>(`/api/admin/exchanges/${id}/ship`, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,9 +23,12 @@ export { DeviceService } from './device'
|
|||||||
export { CarrierService } from './carrier'
|
export { CarrierService } from './carrier'
|
||||||
export { PackageSeriesService } from './packageSeries'
|
export { PackageSeriesService } from './packageSeries'
|
||||||
export { PackageManageService } from './packageManage'
|
export { PackageManageService } from './packageManage'
|
||||||
export { ShopPackageAllocationService } from './shopPackageAllocation'
|
export { ShopSeriesGrantService } from './shopSeriesGrant'
|
||||||
export { ShopSeriesAllocationService } from './shopSeriesAllocation'
|
|
||||||
export { OrderService } from './order'
|
export { OrderService } from './order'
|
||||||
|
export { AssetService } from './asset'
|
||||||
|
export { AgentRechargeService } from './agentRecharge'
|
||||||
|
export { WechatConfigService } from './wechatConfig'
|
||||||
|
export { ExchangeService } from './exchange'
|
||||||
|
|
||||||
// TODO: 按需添加其他业务模块
|
// TODO: 按需添加其他业务模块
|
||||||
// export { SettingService } from './setting'
|
// export { SettingService } from './setting'
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import type {
|
|||||||
OrderListResponse,
|
OrderListResponse,
|
||||||
CreateOrderRequest,
|
CreateOrderRequest,
|
||||||
CreateOrderResponse,
|
CreateOrderResponse,
|
||||||
|
PurchaseCheckRequest,
|
||||||
|
PurchaseCheckResponse,
|
||||||
BaseResponse
|
BaseResponse
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
|
|
||||||
@@ -44,4 +46,17 @@ export class OrderService extends BaseService {
|
|||||||
static cancelOrder(id: number): Promise<BaseResponse> {
|
static cancelOrder(id: number): Promise<BaseResponse> {
|
||||||
return this.post<BaseResponse>(`/api/admin/orders/${id}/cancel`, {})
|
return this.post<BaseResponse>(`/api/admin/orders/${id}/cancel`, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 套餐购买预检
|
||||||
|
* @param data 预检请求参数
|
||||||
|
*/
|
||||||
|
static purchaseCheck(
|
||||||
|
data: PurchaseCheckRequest
|
||||||
|
): Promise<BaseResponse<PurchaseCheckResponse>> {
|
||||||
|
return this.post<BaseResponse<PurchaseCheckResponse>>(
|
||||||
|
'/api/admin/orders/purchase-check',
|
||||||
|
data
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export class PackageManageService extends BaseService {
|
|||||||
*/
|
*/
|
||||||
static updatePackageStatus(id: number, status: number): Promise<BaseResponse> {
|
static updatePackageStatus(id: number, status: number): Promise<BaseResponse> {
|
||||||
const data: UpdatePackageStatusRequest = { status }
|
const data: UpdatePackageStatusRequest = { status }
|
||||||
return this.put<BaseResponse>(`/api/admin/packages/${id}/status`, data)
|
return this.patch<BaseResponse>(`/api/admin/packages/${id}/status`, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,4 +87,14 @@ export class PackageManageService extends BaseService {
|
|||||||
const data: UpdatePackageShelfStatusRequest = { shelf_status }
|
const data: UpdatePackageShelfStatusRequest = { shelf_status }
|
||||||
return this.patch<BaseResponse>(`/api/admin/packages/${id}/shelf`, data)
|
return this.patch<BaseResponse>(`/api/admin/packages/${id}/shelf`, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改套餐零售价(代理)
|
||||||
|
* PATCH /api/admin/packages/{id}/retail-price
|
||||||
|
* @param id 套餐ID
|
||||||
|
* @param retail_price 零售价(分)
|
||||||
|
*/
|
||||||
|
static updateRetailPrice(id: number, retail_price: number): Promise<BaseResponse> {
|
||||||
|
return this.patch<BaseResponse>(`/api/admin/packages/${id}/retail-price`, { retail_price })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,6 @@ export class PackageSeriesService extends BaseService {
|
|||||||
*/
|
*/
|
||||||
static updatePackageSeriesStatus(id: number, status: number): Promise<BaseResponse> {
|
static updatePackageSeriesStatus(id: number, status: number): Promise<BaseResponse> {
|
||||||
const data: UpdatePackageSeriesStatusRequest = { status }
|
const data: UpdatePackageSeriesStatusRequest = { status }
|
||||||
return this.put<BaseResponse>(`/api/admin/package-series/${id}/status`, data)
|
return this.patch<BaseResponse>(`/api/admin/package-series/${id}/status`, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* 平台账号相关 API - 匹配后端实际接口
|
* 平台账号相关 API - 匹配后端实际接口
|
||||||
|
*
|
||||||
|
* @deprecated 此 API 已废弃,请使用统一的 AccountService 代替
|
||||||
|
* @see AccountService - 统一账号管理接口 (/api/admin/accounts)
|
||||||
|
*
|
||||||
|
* 迁移说明:
|
||||||
|
* - 所有账号类型统一使用 /api/admin/accounts 接口
|
||||||
|
* - 通过 user_type 参数区分账号类型 (2=平台, 3=代理, 4=企业)
|
||||||
|
* - 详见:docs/迁移指南.md
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { BaseService } from '../BaseService'
|
import { BaseService } from '../BaseService'
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import type {
|
|||||||
ShopQueryParams,
|
ShopQueryParams,
|
||||||
CreateShopParams,
|
CreateShopParams,
|
||||||
UpdateShopParams,
|
UpdateShopParams,
|
||||||
|
ShopRolesResponse,
|
||||||
|
AssignShopRolesRequest,
|
||||||
BaseResponse,
|
BaseResponse,
|
||||||
PaginationResponse
|
PaginationResponse
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
@@ -49,4 +51,38 @@ export class ShopService extends BaseService {
|
|||||||
static deleteShop(id: number): Promise<BaseResponse> {
|
static deleteShop(id: number): Promise<BaseResponse> {
|
||||||
return this.delete<BaseResponse>(`/api/admin/shops/${id}`)
|
return this.delete<BaseResponse>(`/api/admin/shops/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 店铺默认角色管理 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取店铺默认角色列表
|
||||||
|
* GET /api/admin/shops/{shop_id}/roles
|
||||||
|
* @param shopId 店铺ID
|
||||||
|
*/
|
||||||
|
static getShopRoles(shopId: number): Promise<BaseResponse<ShopRolesResponse>> {
|
||||||
|
return this.get<BaseResponse<ShopRolesResponse>>(`/api/admin/shops/${shopId}/roles`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分配店铺默认角色
|
||||||
|
* POST /api/admin/shops/{shop_id}/roles
|
||||||
|
* @param shopId 店铺ID
|
||||||
|
* @param data 角色ID列表
|
||||||
|
*/
|
||||||
|
static assignShopRoles(
|
||||||
|
shopId: number,
|
||||||
|
data: AssignShopRolesRequest
|
||||||
|
): Promise<BaseResponse<ShopRolesResponse>> {
|
||||||
|
return this.post<BaseResponse<ShopRolesResponse>>(`/api/admin/shops/${shopId}/roles`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除店铺默认角色
|
||||||
|
* DELETE /api/admin/shops/{shop_id}/roles/{role_id}
|
||||||
|
* @param shopId 店铺ID
|
||||||
|
* @param roleId 角色ID
|
||||||
|
*/
|
||||||
|
static deleteShopRole(shopId: number, roleId: number): Promise<BaseResponse> {
|
||||||
|
return this.delete<BaseResponse>(`/api/admin/shops/${shopId}/roles/${roleId}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* 代理账号相关 API - 匹配后端实际接口
|
* 代理账号相关 API - 匹配后端实际接口
|
||||||
|
*
|
||||||
|
* @deprecated 此 API 已废弃,请使用统一的 AccountService 代替
|
||||||
|
* @see AccountService - 统一账号管理接口 (/api/admin/accounts)
|
||||||
|
*
|
||||||
|
* 迁移说明:
|
||||||
|
* - 所有账号类型统一使用 /api/admin/accounts 接口
|
||||||
|
* - 通过 user_type 参数区分账号类型 (2=平台, 3=代理, 4=企业)
|
||||||
|
* - 详见:docs/迁移指南.md
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { BaseService } from '../BaseService'
|
import { BaseService } from '../BaseService'
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
/**
|
|
||||||
* 单套餐分配 API 服务
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { BaseService } from '../BaseService'
|
|
||||||
import type {
|
|
||||||
ShopPackageAllocationResponse,
|
|
||||||
ShopPackageAllocationQueryParams,
|
|
||||||
CreateShopPackageAllocationRequest,
|
|
||||||
UpdateShopPackageAllocationRequest,
|
|
||||||
UpdateShopPackageAllocationStatusRequest,
|
|
||||||
BaseResponse,
|
|
||||||
PaginationResponse
|
|
||||||
} from '@/types/api'
|
|
||||||
|
|
||||||
export class ShopPackageAllocationService extends BaseService {
|
|
||||||
/**
|
|
||||||
* 获取单套餐分配列表
|
|
||||||
* GET /api/admin/shop-package-allocations
|
|
||||||
* @param params 查询参数
|
|
||||||
*/
|
|
||||||
static getShopPackageAllocations(
|
|
||||||
params?: ShopPackageAllocationQueryParams
|
|
||||||
): Promise<PaginationResponse<ShopPackageAllocationResponse>> {
|
|
||||||
return this.getPage<ShopPackageAllocationResponse>(
|
|
||||||
'/api/admin/shop-package-allocations',
|
|
||||||
params
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建单套餐分配
|
|
||||||
* POST /api/admin/shop-package-allocations
|
|
||||||
* @param data 分配数据
|
|
||||||
*/
|
|
||||||
static createShopPackageAllocation(
|
|
||||||
data: CreateShopPackageAllocationRequest
|
|
||||||
): Promise<BaseResponse<ShopPackageAllocationResponse>> {
|
|
||||||
return this.create<ShopPackageAllocationResponse>('/api/admin/shop-package-allocations', data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取单套餐分配详情
|
|
||||||
* GET /api/admin/shop-package-allocations/{id}
|
|
||||||
* @param id 分配ID
|
|
||||||
*/
|
|
||||||
static getShopPackageAllocationDetail(
|
|
||||||
id: number
|
|
||||||
): Promise<BaseResponse<ShopPackageAllocationResponse>> {
|
|
||||||
return this.getOne<ShopPackageAllocationResponse>(`/api/admin/shop-package-allocations/${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新单套餐分配
|
|
||||||
* PUT /api/admin/shop-package-allocations/{id}
|
|
||||||
* @param id 分配ID
|
|
||||||
* @param data 分配数据(只允许修改成本价)
|
|
||||||
*/
|
|
||||||
static updateShopPackageAllocation(
|
|
||||||
id: number,
|
|
||||||
data: UpdateShopPackageAllocationRequest
|
|
||||||
): Promise<BaseResponse<ShopPackageAllocationResponse>> {
|
|
||||||
return this.update<ShopPackageAllocationResponse>(
|
|
||||||
`/api/admin/shop-package-allocations/${id}`,
|
|
||||||
data
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除单套餐分配
|
|
||||||
* DELETE /api/admin/shop-package-allocations/{id}
|
|
||||||
* @param id 分配ID
|
|
||||||
*/
|
|
||||||
static deleteShopPackageAllocation(id: number): Promise<BaseResponse> {
|
|
||||||
return this.remove(`/api/admin/shop-package-allocations/${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新单套餐分配成本价
|
|
||||||
* PUT /api/admin/shop-package-allocations/{id}/cost-price
|
|
||||||
* @param id 分配ID
|
|
||||||
* @param costPrice 成本价(分)
|
|
||||||
*/
|
|
||||||
static updateShopPackageAllocationCostPrice(
|
|
||||||
id: number,
|
|
||||||
costPrice: number
|
|
||||||
): Promise<BaseResponse<ShopPackageAllocationResponse>> {
|
|
||||||
return this.put<BaseResponse<ShopPackageAllocationResponse>>(
|
|
||||||
`/api/admin/shop-package-allocations/${id}/cost-price`,
|
|
||||||
{ cost_price: costPrice }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新单套餐分配状态
|
|
||||||
* PUT /api/admin/shop-package-allocations/{id}/status
|
|
||||||
* @param id 分配ID
|
|
||||||
* @param status 状态 (1:启用, 2:禁用)
|
|
||||||
*/
|
|
||||||
static updateShopPackageAllocationStatus(id: number, status: number): Promise<BaseResponse> {
|
|
||||||
const data: UpdateShopPackageAllocationStatusRequest = { status }
|
|
||||||
return this.put<BaseResponse>(`/api/admin/shop-package-allocations/${id}/status`, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
/**
|
|
||||||
* 套餐系列分配 API 服务
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { BaseService } from '../BaseService'
|
|
||||||
import type {
|
|
||||||
ShopSeriesAllocationResponse,
|
|
||||||
ShopSeriesAllocationQueryParams,
|
|
||||||
CreateShopSeriesAllocationRequest,
|
|
||||||
UpdateShopSeriesAllocationRequest,
|
|
||||||
UpdateShopSeriesAllocationStatusRequest,
|
|
||||||
BaseResponse,
|
|
||||||
PaginationResponse
|
|
||||||
} from '@/types/api'
|
|
||||||
|
|
||||||
export class ShopSeriesAllocationService extends BaseService {
|
|
||||||
/**
|
|
||||||
* 获取套餐系列分配分页列表
|
|
||||||
* GET /api/admin/shop-series-allocations
|
|
||||||
* @param params 查询参数
|
|
||||||
*/
|
|
||||||
static getShopSeriesAllocations(
|
|
||||||
params?: ShopSeriesAllocationQueryParams
|
|
||||||
): Promise<PaginationResponse<ShopSeriesAllocationResponse>> {
|
|
||||||
return this.getPage<ShopSeriesAllocationResponse>('/api/admin/shop-series-allocations', params)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建套餐系列分配
|
|
||||||
* POST /api/admin/shop-series-allocations
|
|
||||||
* @param data 分配数据
|
|
||||||
*/
|
|
||||||
static createShopSeriesAllocation(
|
|
||||||
data: CreateShopSeriesAllocationRequest
|
|
||||||
): Promise<BaseResponse<ShopSeriesAllocationResponse>> {
|
|
||||||
return this.create<ShopSeriesAllocationResponse>('/api/admin/shop-series-allocations', data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取套餐系列分配详情
|
|
||||||
* GET /api/admin/shop-series-allocations/{id}
|
|
||||||
* @param id 分配ID
|
|
||||||
*/
|
|
||||||
static getShopSeriesAllocationDetail(
|
|
||||||
id: number
|
|
||||||
): Promise<BaseResponse<ShopSeriesAllocationResponse>> {
|
|
||||||
return this.getOne<ShopSeriesAllocationResponse>(`/api/admin/shop-series-allocations/${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新套餐系列分配
|
|
||||||
* PUT /api/admin/shop-series-allocations/{id}
|
|
||||||
* @param id 分配ID
|
|
||||||
* @param data 分配数据
|
|
||||||
*/
|
|
||||||
static updateShopSeriesAllocation(
|
|
||||||
id: number,
|
|
||||||
data: UpdateShopSeriesAllocationRequest
|
|
||||||
): Promise<BaseResponse<ShopSeriesAllocationResponse>> {
|
|
||||||
return this.update<ShopSeriesAllocationResponse>(
|
|
||||||
`/api/admin/shop-series-allocations/${id}`,
|
|
||||||
data
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除套餐系列分配
|
|
||||||
* DELETE /api/admin/shop-series-allocations/{id}
|
|
||||||
* @param id 分配ID
|
|
||||||
*/
|
|
||||||
static deleteShopSeriesAllocation(id: number): Promise<BaseResponse> {
|
|
||||||
return this.remove(`/api/admin/shop-series-allocations/${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新套餐系列分配状态
|
|
||||||
* PUT /api/admin/shop-series-allocations/{id}/status
|
|
||||||
* @param id 分配ID
|
|
||||||
* @param status 状态 (1:启用, 2:禁用)
|
|
||||||
*/
|
|
||||||
static updateShopSeriesAllocationStatus(id: number, status: number): Promise<BaseResponse> {
|
|
||||||
const data: UpdateShopSeriesAllocationStatusRequest = { status }
|
|
||||||
return this.put<BaseResponse>(`/api/admin/shop-series-allocations/${id}/status`, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
90
src/api/modules/shopSeriesGrant.ts
Normal file
90
src/api/modules/shopSeriesGrant.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* 代理系列授权 API 服务
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BaseService } from '../BaseService'
|
||||||
|
import type {
|
||||||
|
ShopSeriesGrantResponse,
|
||||||
|
ShopSeriesGrantQueryParams,
|
||||||
|
CreateShopSeriesGrantRequest,
|
||||||
|
UpdateShopSeriesGrantRequest,
|
||||||
|
ManageGrantPackagesRequest,
|
||||||
|
BaseResponse,
|
||||||
|
PaginationResponse
|
||||||
|
} from '@/types/api'
|
||||||
|
|
||||||
|
export class ShopSeriesGrantService extends BaseService {
|
||||||
|
/**
|
||||||
|
* 获取代理系列授权分页列表
|
||||||
|
* GET /api/admin/shop-series-grants
|
||||||
|
* @param params 查询参数
|
||||||
|
*/
|
||||||
|
static getShopSeriesGrants(
|
||||||
|
params?: ShopSeriesGrantQueryParams
|
||||||
|
): Promise<PaginationResponse<ShopSeriesGrantResponse>> {
|
||||||
|
return this.getPage<ShopSeriesGrantResponse>('/api/admin/shop-series-grants', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建代理系列授权
|
||||||
|
* POST /api/admin/shop-series-grants
|
||||||
|
* @param data 授权数据
|
||||||
|
*/
|
||||||
|
static createShopSeriesGrant(
|
||||||
|
data: CreateShopSeriesGrantRequest
|
||||||
|
): Promise<BaseResponse<ShopSeriesGrantResponse>> {
|
||||||
|
return this.create<ShopSeriesGrantResponse>('/api/admin/shop-series-grants', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取代理系列授权详情
|
||||||
|
* GET /api/admin/shop-series-grants/{id}
|
||||||
|
* @param id 授权ID
|
||||||
|
*/
|
||||||
|
static getShopSeriesGrantDetail(
|
||||||
|
id: number
|
||||||
|
): Promise<BaseResponse<ShopSeriesGrantResponse>> {
|
||||||
|
return this.getOne<ShopSeriesGrantResponse>(`/api/admin/shop-series-grants/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新代理系列授权
|
||||||
|
* PUT /api/admin/shop-series-grants/{id}
|
||||||
|
* @param id 授权ID
|
||||||
|
* @param data 授权数据
|
||||||
|
*/
|
||||||
|
static updateShopSeriesGrant(
|
||||||
|
id: number,
|
||||||
|
data: UpdateShopSeriesGrantRequest
|
||||||
|
): Promise<BaseResponse<ShopSeriesGrantResponse>> {
|
||||||
|
return this.update<ShopSeriesGrantResponse>(
|
||||||
|
`/api/admin/shop-series-grants/${id}`,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除代理系列授权
|
||||||
|
* DELETE /api/admin/shop-series-grants/{id}
|
||||||
|
* @param id 授权ID
|
||||||
|
*/
|
||||||
|
static deleteShopSeriesGrant(id: number): Promise<BaseResponse> {
|
||||||
|
return this.remove(`/api/admin/shop-series-grants/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理代理系列授权的套餐
|
||||||
|
* PUT /api/admin/shop-series-grants/{id}/packages
|
||||||
|
* @param id 授权ID
|
||||||
|
* @param data 套餐管理数据
|
||||||
|
*/
|
||||||
|
static manageGrantPackages(
|
||||||
|
id: number,
|
||||||
|
data: ManageGrantPackagesRequest
|
||||||
|
): Promise<BaseResponse<ShopSeriesGrantResponse>> {
|
||||||
|
return this.put<BaseResponse<ShopSeriesGrantResponse>>(
|
||||||
|
`/api/admin/shop-series-grants/${id}/packages`,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/api/modules/wechatConfig.ts
Normal file
78
src/api/modules/wechatConfig.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* 微信支付配置管理 API
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BaseService } from '../BaseService'
|
||||||
|
import type { BaseResponse } from '@/types/api'
|
||||||
|
import type {
|
||||||
|
WechatConfig,
|
||||||
|
WechatConfigQueryParams,
|
||||||
|
WechatConfigListResponse,
|
||||||
|
CreateWechatConfigRequest,
|
||||||
|
UpdateWechatConfigRequest
|
||||||
|
} from '@/types/api/wechatConfig'
|
||||||
|
|
||||||
|
export class WechatConfigService extends BaseService {
|
||||||
|
/**
|
||||||
|
* 获取支付配置列表
|
||||||
|
*/
|
||||||
|
static getWechatConfigs(
|
||||||
|
params?: WechatConfigQueryParams
|
||||||
|
): Promise<BaseResponse<WechatConfigListResponse>> {
|
||||||
|
return this.get<BaseResponse<WechatConfigListResponse>>('/api/admin/wechat-configs', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支付配置详情
|
||||||
|
*/
|
||||||
|
static getWechatConfigById(id: number): Promise<BaseResponse<WechatConfig>> {
|
||||||
|
return this.get<BaseResponse<WechatConfig>>(`/api/admin/wechat-configs/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建支付配置
|
||||||
|
*/
|
||||||
|
static createWechatConfig(
|
||||||
|
data: CreateWechatConfigRequest
|
||||||
|
): Promise<BaseResponse<WechatConfig>> {
|
||||||
|
return this.post<BaseResponse<WechatConfig>>('/api/admin/wechat-configs', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新支付配置
|
||||||
|
*/
|
||||||
|
static updateWechatConfig(
|
||||||
|
id: number,
|
||||||
|
data: UpdateWechatConfigRequest
|
||||||
|
): Promise<BaseResponse<WechatConfig>> {
|
||||||
|
return this.put<BaseResponse<WechatConfig>>(`/api/admin/wechat-configs/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除支付配置
|
||||||
|
*/
|
||||||
|
static deleteWechatConfig(id: number): Promise<BaseResponse<void>> {
|
||||||
|
return this.delete<BaseResponse<void>>(`/api/admin/wechat-configs/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活支付配置
|
||||||
|
*/
|
||||||
|
static activateWechatConfig(id: number): Promise<BaseResponse<WechatConfig>> {
|
||||||
|
return this.post<BaseResponse<WechatConfig>>(`/api/admin/wechat-configs/${id}/activate`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停用支付配置
|
||||||
|
*/
|
||||||
|
static deactivateWechatConfig(id: number): Promise<BaseResponse<WechatConfig>> {
|
||||||
|
return this.post<BaseResponse<WechatConfig>>(`/api/admin/wechat-configs/${id}/deactivate`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前生效的支付配置
|
||||||
|
*/
|
||||||
|
static getActiveWechatConfig(): Promise<BaseResponse<WechatConfig>> {
|
||||||
|
return this.get<BaseResponse<WechatConfig>>('/api/admin/wechat-configs/active')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import request from '@/utils/http'
|
|
||||||
import { BaseResponse, UserInfoResponse } from '@/types/api'
|
|
||||||
|
|
||||||
interface LoginParams {
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
device?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserListParams {
|
|
||||||
current?: number
|
|
||||||
size?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserService {
|
|
||||||
// 登录
|
|
||||||
static login(params: LoginParams) {
|
|
||||||
return request.post<BaseResponse>({
|
|
||||||
url: '/api/admin/login',
|
|
||||||
params
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取用户信息
|
|
||||||
// GET /api/admin/me
|
|
||||||
static getUserInfo() {
|
|
||||||
return request.get<BaseResponse<UserInfoResponse>>({
|
|
||||||
url: '/api/admin/me'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取用户列表
|
|
||||||
static getUserList(params?: UserListParams) {
|
|
||||||
return request.get<BaseResponse>({
|
|
||||||
url: '/api/user/list',
|
|
||||||
params
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -193,3 +193,16 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 表单分段标题样式 - 用于对话框中的表单分段
|
||||||
|
.form-section-title {
|
||||||
|
margin: 24px 0 16px 0;
|
||||||
|
padding-left: 12px;
|
||||||
|
border-left: 3px solid var(--el-color-primary);
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
59
src/components/business/CodeGeneratorButton.vue
Normal file
59
src/components/business/CodeGeneratorButton.vue
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<ElButton @click="handleGenerate">
|
||||||
|
{{ buttonText }}
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import {
|
||||||
|
generateSeriesCode,
|
||||||
|
generatePackageCode,
|
||||||
|
generateShopCode
|
||||||
|
} from '@/utils/codeGenerator'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
// 编码类型: series(套餐系列) | package(套餐) | shop(店铺)
|
||||||
|
codeType: 'series' | 'package' | 'shop'
|
||||||
|
// 按钮文字
|
||||||
|
buttonText?: string
|
||||||
|
// 表单字段名,用于清除验证
|
||||||
|
fieldName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
buttonText: '生成编码'
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'generated', code: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 生成编码
|
||||||
|
const handleGenerate = () => {
|
||||||
|
let code = ''
|
||||||
|
|
||||||
|
switch (props.codeType) {
|
||||||
|
case 'series':
|
||||||
|
code = generateSeriesCode()
|
||||||
|
break
|
||||||
|
case 'package':
|
||||||
|
code = generatePackageCode()
|
||||||
|
break
|
||||||
|
case 'shop':
|
||||||
|
code = generateShopCode()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
ElMessage.error('未知的编码类型')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发生成事件,将编码传递给父组件
|
||||||
|
emit('generated', code)
|
||||||
|
ElMessage.success('编码生成成功')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
// 可以添加特定样式
|
||||||
|
</style>
|
||||||
286
src/components/business/CustomerAccountDialog.vue
Normal file
286
src/components/business/CustomerAccountDialog.vue
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
<template>
|
||||||
|
<ElDialog v-model="visible" title="客户账号列表" width="80%" @close="handleClose">
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<ArtSearchBar
|
||||||
|
v-model:filter="searchForm"
|
||||||
|
:items="searchFormItems"
|
||||||
|
:show-expand="false"
|
||||||
|
@reset="handleReset"
|
||||||
|
@search="handleSearch"
|
||||||
|
></ArtSearchBar>
|
||||||
|
|
||||||
|
<!-- 表格 -->
|
||||||
|
<ArtTable
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="ID"
|
||||||
|
:loading="loading"
|
||||||
|
:data="accountList"
|
||||||
|
height="60vh"
|
||||||
|
:currentPage="pagination.page"
|
||||||
|
:pageSize="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
:marginTop="10"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<ElTableColumn v-for="col in columns" :key="col.prop || (col as any).type" v-bind="col" />
|
||||||
|
</template>
|
||||||
|
</ArtTable>
|
||||||
|
</ElDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { h } from 'vue'
|
||||||
|
import { ElMessage, ElTag } from 'element-plus'
|
||||||
|
import { AccountService } from '@/api/modules'
|
||||||
|
import type { SearchFormItem } from '@/types'
|
||||||
|
import type { PlatformAccount } from '@/types/api'
|
||||||
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
|
|
||||||
|
// 用户类型映射
|
||||||
|
const getUserTypeName = (type: number): string => {
|
||||||
|
const typeMap: Record<number, string> = {
|
||||||
|
1: '超级管理员',
|
||||||
|
2: '平台用户',
|
||||||
|
3: '代理账号',
|
||||||
|
4: '企业账号'
|
||||||
|
}
|
||||||
|
return typeMap[type] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态名称映射
|
||||||
|
const getStatusName = (status: number): string => {
|
||||||
|
return status === 1 ? '启用' : '禁用'
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
shopId?: number
|
||||||
|
enterpriseId?: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const visible = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val)
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const tableRef = ref()
|
||||||
|
const accountList = ref<PlatformAccount[]>([])
|
||||||
|
|
||||||
|
// 搜索表单初始值
|
||||||
|
const initialSearchState = {
|
||||||
|
username: '',
|
||||||
|
phone: '',
|
||||||
|
user_type: undefined as number | undefined,
|
||||||
|
status: undefined as number | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({ ...initialSearchState })
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 搜索表单配置
|
||||||
|
const searchFormItems: SearchFormItem[] = [
|
||||||
|
{
|
||||||
|
label: '用户名',
|
||||||
|
prop: 'username',
|
||||||
|
type: 'input',
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入用户名'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '手机号',
|
||||||
|
prop: 'phone',
|
||||||
|
type: 'input',
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入手机号'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '用户类型',
|
||||||
|
prop: 'user_type',
|
||||||
|
type: 'select',
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '全部'
|
||||||
|
},
|
||||||
|
options: () => [
|
||||||
|
{ label: '代理账号', value: 3 },
|
||||||
|
{ label: '企业账号', value: 4 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '状态',
|
||||||
|
prop: 'status',
|
||||||
|
type: 'select',
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '全部'
|
||||||
|
},
|
||||||
|
options: () => [
|
||||||
|
{ label: '启用', value: 1 },
|
||||||
|
{ label: '禁用', value: 0 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 获取用户类型标签类型
|
||||||
|
const getUserTypeTag = (type: number) => {
|
||||||
|
return type === 3 ? 'success' : 'primary'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态标签类型
|
||||||
|
const getStatusTag = (status: number) => {
|
||||||
|
return status === 1 ? 'success' : 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列配置
|
||||||
|
const columns = computed(() => [
|
||||||
|
{
|
||||||
|
prop: 'username',
|
||||||
|
label: '用户名',
|
||||||
|
minWidth: 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'phone',
|
||||||
|
label: '手机号',
|
||||||
|
width: 130
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'user_type',
|
||||||
|
label: '用户类型',
|
||||||
|
width: 110,
|
||||||
|
formatter: (row: PlatformAccount) => {
|
||||||
|
return h(ElTag, { type: getUserTypeTag(row.user_type) }, () => getUserTypeName(row.user_type))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'shop_id',
|
||||||
|
label: '店铺ID',
|
||||||
|
width: 100,
|
||||||
|
formatter: (row: PlatformAccount) => row.shop_id || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'enterprise_id',
|
||||||
|
label: '企业ID',
|
||||||
|
width: 100,
|
||||||
|
formatter: (row: PlatformAccount) => row.enterprise_id || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'status',
|
||||||
|
label: '状态',
|
||||||
|
width: 100,
|
||||||
|
formatter: (row: PlatformAccount) => {
|
||||||
|
return h(ElTag, { type: getStatusTag(row.status) }, () => getStatusName(row.status))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'CreatedAt',
|
||||||
|
label: '创建时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: (row: PlatformAccount) => formatDateTime(row.CreatedAt)
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 监听弹窗打开,重新加载数据
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
handleReset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 获取客户账号列表
|
||||||
|
const getTableData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params: any = {
|
||||||
|
page: pagination.page,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
username: searchForm.username || undefined,
|
||||||
|
phone: searchForm.phone || undefined,
|
||||||
|
user_type: searchForm.user_type,
|
||||||
|
status: searchForm.status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据传入的参数筛选
|
||||||
|
if (props.shopId) {
|
||||||
|
params.shop_id = props.shopId
|
||||||
|
}
|
||||||
|
if (props.enterpriseId) {
|
||||||
|
params.enterprise_id = props.enterpriseId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理空值
|
||||||
|
Object.keys(params).forEach((key) => {
|
||||||
|
if (params[key] === '' || params[key] === undefined) {
|
||||||
|
delete params[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await AccountService.getAccounts(params)
|
||||||
|
if (res.code === 0) {
|
||||||
|
accountList.value = res.data.items || []
|
||||||
|
pagination.total = res.data.total || 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
ElMessage.error('获取客户账号列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const handleReset = () => {
|
||||||
|
Object.assign(searchForm, { ...initialSearchState })
|
||||||
|
pagination.page = 1
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.page = 1
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理表格分页变化
|
||||||
|
const handleSizeChange = (newPageSize: number) => {
|
||||||
|
pagination.pageSize = newPageSize
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (newCurrentPage: number) => {
|
||||||
|
pagination.page = newCurrentPage
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
const handleClose = () => {
|
||||||
|
// 重置搜索表单
|
||||||
|
Object.assign(searchForm, { ...initialSearchState })
|
||||||
|
pagination.page = 1
|
||||||
|
accountList.value = []
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
// 客户账号列表弹窗样式
|
||||||
|
</style>
|
||||||
159
src/components/common/DetailPage.vue
Normal file
159
src/components/common/DetailPage.vue
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<template>
|
||||||
|
<div class="detail-container">
|
||||||
|
<ElCard v-for="(section, index) in sections" :key="index" class="detail-section">
|
||||||
|
<template #header>
|
||||||
|
<div class="section-title">{{ section.title }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div :class="['section-fields', section.columns === 1 ? 'single-column' : 'double-column']">
|
||||||
|
<div
|
||||||
|
v-for="(field, fieldIndex) in section.fields"
|
||||||
|
:key="fieldIndex"
|
||||||
|
class="field-item"
|
||||||
|
:class="{ 'full-width': field.fullWidth }"
|
||||||
|
>
|
||||||
|
<div class="field-label">{{ field.label }}:</div>
|
||||||
|
<div class="field-value">
|
||||||
|
<!-- 自定义渲染 -->
|
||||||
|
<template v-if="field.render">
|
||||||
|
<component :is="field.render(data)" />
|
||||||
|
</template>
|
||||||
|
<!-- 默认渲染 -->
|
||||||
|
<template v-else>
|
||||||
|
{{ formatFieldValue(field, data) }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ElCard } from 'element-plus'
|
||||||
|
|
||||||
|
export interface DetailField {
|
||||||
|
label: string // 字段标签
|
||||||
|
prop?: string // 数据属性路径,支持点号分隔的嵌套路径,如 'one_time_commission_config.enable'
|
||||||
|
formatter?: (value: any, data: any) => string // 自定义格式化函数
|
||||||
|
render?: (data: any) => any // 自定义渲染函数,返回 VNode
|
||||||
|
fullWidth?: boolean // 是否占据整行
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DetailSection {
|
||||||
|
title: string // 分组标题
|
||||||
|
fields: DetailField[] // 字段列表
|
||||||
|
columns?: 1 | 2 // 列数,默认2列
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
sections: DetailSection[] // 详情页分组配置
|
||||||
|
data: any // 详情数据
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据点号分隔的路径获取嵌套对象的值
|
||||||
|
*/
|
||||||
|
const getNestedValue = (obj: any, path: string): any => {
|
||||||
|
if (!path) return obj
|
||||||
|
return path.split('.').reduce((acc, part) => acc?.[part], obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化字段值
|
||||||
|
*/
|
||||||
|
const formatFieldValue = (field: DetailField, data: any): string => {
|
||||||
|
const value = field.prop ? getNestedValue(data, field.prop) : data
|
||||||
|
|
||||||
|
// 如果有自定义格式化函数
|
||||||
|
if (field.formatter) {
|
||||||
|
return field.formatter(value, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认处理
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.detail-container {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-fields {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px 24px;
|
||||||
|
|
||||||
|
&.single-column {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.double-column {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
min-height: 32px;
|
||||||
|
|
||||||
|
&.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 140px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-value {
|
||||||
|
flex: 1;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
line-height: 32px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.section-fields {
|
||||||
|
&.double-column {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-item {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
text?: string
|
text?: string
|
||||||
type?: 'add' | 'edit' | 'delete' | 'more'
|
type?: 'add' | 'edit' | 'delete' | 'more' | 'view'
|
||||||
icon?: string // 自定义图标
|
icon?: string // 自定义图标
|
||||||
iconClass?: BgColorEnum // 自定义按钮背景色、文字颜色
|
iconClass?: BgColorEnum // 自定义按钮背景色、文字颜色
|
||||||
iconColor?: string // 外部传入的图标文字颜色
|
iconColor?: string // 外部传入的图标文字颜色
|
||||||
@@ -31,7 +31,8 @@
|
|||||||
{ type: 'add', icon: '', color: BgColorEnum.PRIMARY },
|
{ type: 'add', icon: '', color: BgColorEnum.PRIMARY },
|
||||||
{ type: 'edit', icon: '', color: BgColorEnum.SECONDARY },
|
{ type: 'edit', icon: '', color: BgColorEnum.SECONDARY },
|
||||||
{ type: 'delete', icon: '', color: BgColorEnum.ERROR },
|
{ type: 'delete', icon: '', color: BgColorEnum.ERROR },
|
||||||
{ type: 'more', icon: '', color: '' }
|
{ type: 'more', icon: '', color: '' },
|
||||||
|
{ type: 'view', icon: '', color: BgColorEnum.SECONDARY }
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
// 计算最终使用的图标:优先使用外部传入的 icon,否则根据 type 获取默认图标
|
// 计算最终使用的图标:优先使用外部传入的 icon,否则根据 type 获取默认图标
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
import ArtSearchSelect from './widget/art-search-select/index.vue'
|
import ArtSearchSelect from './widget/art-search-select/index.vue'
|
||||||
import ArtSearchRadio from './widget/art-search-radio/index.vue'
|
import ArtSearchRadio from './widget/art-search-radio/index.vue'
|
||||||
import ArtSearchDate from './widget/art-search-date/index.vue'
|
import ArtSearchDate from './widget/art-search-date/index.vue'
|
||||||
|
import ArtSearchTreeSelect from './widget/art-search-tree-select/index.vue'
|
||||||
import { SearchComponentType, SearchFormItem } from '@/types'
|
import { SearchComponentType, SearchFormItem } from '@/types'
|
||||||
|
|
||||||
const { width } = useWindowSize()
|
const { width } = useWindowSize()
|
||||||
@@ -131,6 +132,7 @@
|
|||||||
const componentsMap: Record<string, any> = {
|
const componentsMap: Record<string, any> = {
|
||||||
input: ArtSearchInput,
|
input: ArtSearchInput,
|
||||||
select: ArtSearchSelect,
|
select: ArtSearchSelect,
|
||||||
|
'tree-select': ArtSearchTreeSelect,
|
||||||
radio: ArtSearchRadio,
|
radio: ArtSearchRadio,
|
||||||
datetime: ArtSearchDate,
|
datetime: ArtSearchDate,
|
||||||
date: ArtSearchDate,
|
date: ArtSearchDate,
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<el-tree-select v-model="value" v-bind="config" @change="(val) => changeValue(val)" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { SearchFormItem } from '@/types'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// 定义组件值类型
|
||||||
|
export type ValueVO = unknown
|
||||||
|
|
||||||
|
// 定义组件props
|
||||||
|
const prop = defineProps<{
|
||||||
|
value: ValueVO // 树形选择框的值
|
||||||
|
item: SearchFormItem // 表单项配置
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 定义emit事件
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:value', value: ValueVO): void // 更新树形选择框值的事件
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 计算属性:处理v-model双向绑定
|
||||||
|
const value = computed({
|
||||||
|
get: () => prop.value,
|
||||||
|
set: (value: ValueVO) => emit('update:value', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 合并默认配置和自定义配置
|
||||||
|
const config = reactive({
|
||||||
|
placeholder: `${t('table.searchBar.searchSelectPlaceholder')}${prop.item.label}`,
|
||||||
|
...(prop.item.config || {})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 树形选择框值变化处理函数
|
||||||
|
const changeValue = (val: unknown): void => {
|
||||||
|
if (prop.item.onChange) {
|
||||||
|
prop.item.onChange({
|
||||||
|
prop: prop.item.prop,
|
||||||
|
val
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -4,7 +4,13 @@
|
|||||||
<Transition name="context-menu" @before-enter="onBeforeEnter" @after-leave="onAfterLeave">
|
<Transition name="context-menu" @before-enter="onBeforeEnter" @after-leave="onAfterLeave">
|
||||||
<div v-show="visible" :style="menuStyle" class="context-menu">
|
<div v-show="visible" :style="menuStyle" class="context-menu">
|
||||||
<ul class="menu-list" :style="menuListStyle">
|
<ul class="menu-list" :style="menuListStyle">
|
||||||
<template v-for="item in menuItems" :key="item.key">
|
<!-- 无权限提示 -->
|
||||||
|
<li v-if="menuItems.length === 0" class="menu-item no-permission" :style="menuItemStyle">
|
||||||
|
<span class="menu-label">您暂无更多权限</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- 菜单项 -->
|
||||||
|
<template v-else v-for="item in menuItems" :key="item.key">
|
||||||
<!-- 普通菜单项 -->
|
<!-- 普通菜单项 -->
|
||||||
<li
|
<li
|
||||||
v-if="!item.children"
|
v-if="!item.children"
|
||||||
@@ -249,10 +255,23 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
transition: background-color 0.15s ease;
|
transition: background-color 0.15s ease;
|
||||||
|
|
||||||
&:hover:not(.is-disabled) {
|
&:hover:not(.is-disabled):not(.no-permission) {
|
||||||
background-color: rgba(var(--art-gray-200-rgb), 0.7);
|
background-color: rgba(var(--art-gray-200-rgb), 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.no-permission {
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
.menu-label {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
text-align: center;
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.has-line {
|
&.has-line {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
|||||||
38
src/components/core/others/TableContextMenuHint.vue
Normal file
38
src/components/core/others/TableContextMenuHint.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<!-- 表格右键菜单悬浮提示组件 -->
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-show="visible"
|
||||||
|
class="table-context-menu-hint"
|
||||||
|
:style="{ left: position.x + 'px', top: position.y + 'px' }"
|
||||||
|
>
|
||||||
|
{{ text }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
visible: boolean
|
||||||
|
position: { x: number; y: number }
|
||||||
|
text?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
text: '右键查看更多操作'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.table-context-menu-hint {
|
||||||
|
position: fixed;
|
||||||
|
padding: 4px 10px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
pointer-events: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 9999;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="tableData"
|
:data="tableData"
|
||||||
:row-key="rowKey"
|
:row-key="rowKey"
|
||||||
|
:row-class-name="rowClassName"
|
||||||
:height="height"
|
:height="height"
|
||||||
:max-height="maxHeight"
|
:max-height="maxHeight"
|
||||||
:show-header="showHeader"
|
:show-header="showHeader"
|
||||||
@@ -28,7 +29,10 @@
|
|||||||
fontWeight: '500'
|
fontWeight: '500'
|
||||||
}"
|
}"
|
||||||
@row-click="handleRowClick"
|
@row-click="handleRowClick"
|
||||||
|
@row-contextmenu="handleRowContextmenu"
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
|
@cell-mouse-enter="handleCellMouseEnter"
|
||||||
|
@cell-mouse-leave="handleCellMouseLeave"
|
||||||
>
|
>
|
||||||
<!-- 序号列 -->
|
<!-- 序号列 -->
|
||||||
<el-table-column
|
<el-table-column
|
||||||
@@ -85,6 +89,8 @@
|
|||||||
loading?: boolean
|
loading?: boolean
|
||||||
/** 行数据的 Key,用于标识每一行数据 */
|
/** 行数据的 Key,用于标识每一行数据 */
|
||||||
rowKey?: string
|
rowKey?: string
|
||||||
|
/** 行的 className 的回调方法 */
|
||||||
|
rowClassName?: ((data: { row: any; rowIndex: number }) => string) | string
|
||||||
/** 是否显示边框 */
|
/** 是否显示边框 */
|
||||||
border?: boolean | null
|
border?: boolean | null
|
||||||
/** 是否使用斑马纹样式 */
|
/** 是否使用斑马纹样式 */
|
||||||
@@ -135,6 +141,7 @@
|
|||||||
data: () => [],
|
data: () => [],
|
||||||
loading: false,
|
loading: false,
|
||||||
rowKey: 'id',
|
rowKey: 'id',
|
||||||
|
rowClassName: undefined,
|
||||||
border: null,
|
border: null,
|
||||||
stripe: null,
|
stripe: null,
|
||||||
index: false,
|
index: false,
|
||||||
@@ -174,9 +181,12 @@
|
|||||||
'update:currentPage',
|
'update:currentPage',
|
||||||
'update:pageSize',
|
'update:pageSize',
|
||||||
'row-click',
|
'row-click',
|
||||||
|
'row-contextmenu',
|
||||||
'size-change',
|
'size-change',
|
||||||
'current-change',
|
'current-change',
|
||||||
'selection-change'
|
'selection-change',
|
||||||
|
'cell-mouse-enter',
|
||||||
|
'cell-mouse-leave'
|
||||||
])
|
])
|
||||||
|
|
||||||
const tableStore = useTableStore()
|
const tableStore = useTableStore()
|
||||||
@@ -274,6 +284,21 @@
|
|||||||
emit('row-click', row, column, event)
|
emit('row-click', row, column, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 行右键事件
|
||||||
|
const handleRowContextmenu = (row: any, column: any, event: any) => {
|
||||||
|
emit('row-contextmenu', row, column, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单元格鼠标进入事件
|
||||||
|
const handleCellMouseEnter = (row: any, column: any, cell: any, event: any) => {
|
||||||
|
emit('cell-mouse-enter', row, column, cell, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单元格鼠标离开事件
|
||||||
|
const handleCellMouseLeave = (row: any, column: any, cell: any, event: any) => {
|
||||||
|
emit('cell-mouse-leave', row, column, cell, event)
|
||||||
|
}
|
||||||
|
|
||||||
// 选择变化事件
|
// 选择变化事件
|
||||||
const handleSelectionChange = (selection: any) => {
|
const handleSelectionChange = (selection: any) => {
|
||||||
emit('selection-change', selection)
|
emit('selection-change', selection)
|
||||||
|
|||||||
134
src/components/device/SetWiFiDialog.vue
Normal file
134
src/components/device/SetWiFiDialog.vue
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<template>
|
||||||
|
<ElDialog v-model="visible" title="设置WiFi" width="500px" @close="handleClose">
|
||||||
|
<ElForm
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
label-width="120px"
|
||||||
|
>
|
||||||
|
<ElFormItem label="设备信息">
|
||||||
|
<span style="font-weight: bold; color: #409eff">{{ deviceInfo }}</span>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="WiFi状态" prop="enabled">
|
||||||
|
<ElRadioGroup v-model="formData.enabled">
|
||||||
|
<ElRadio :value="1">启用</ElRadio>
|
||||||
|
<ElRadio :value="0">禁用</ElRadio>
|
||||||
|
</ElRadioGroup>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="WiFi名称" prop="ssid">
|
||||||
|
<ElInput
|
||||||
|
v-model="formData.ssid"
|
||||||
|
placeholder="请输入WiFi名称(1-32个字符)"
|
||||||
|
maxlength="32"
|
||||||
|
show-word-limit
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="WiFi密码" prop="password">
|
||||||
|
<ElInput
|
||||||
|
v-model="formData.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入WiFi密码(8-63个字符)"
|
||||||
|
maxlength="63"
|
||||||
|
show-word-limit
|
||||||
|
show-password
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="visible = false">取消</ElButton>
|
||||||
|
<ElButton type="primary" @click="handleConfirm" :loading="confirmLoading">
|
||||||
|
确认设置
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, watch } from 'vue'
|
||||||
|
import {
|
||||||
|
ElDialog,
|
||||||
|
ElForm,
|
||||||
|
ElFormItem,
|
||||||
|
ElInput,
|
||||||
|
ElButton,
|
||||||
|
ElRadioGroup,
|
||||||
|
ElRadio
|
||||||
|
} from 'element-plus'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: boolean
|
||||||
|
deviceInfo: string
|
||||||
|
loading?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value: boolean): void
|
||||||
|
(e: 'confirm', data: { enabled: number; ssid: string; password: string }): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
loading: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
const visible = ref(props.modelValue)
|
||||||
|
const confirmLoading = ref(props.loading)
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
enabled: 1,
|
||||||
|
ssid: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = reactive<FormRules>({
|
||||||
|
ssid: [
|
||||||
|
{ required: true, message: '请输入WiFi名称', trigger: 'blur' },
|
||||||
|
{ min: 1, max: 32, message: 'WiFi名称长度为1-32个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入WiFi密码', trigger: 'blur' },
|
||||||
|
{ min: 8, max: 63, message: 'WiFi密码长度为8-63个字符', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
visible.value = val
|
||||||
|
if (val) {
|
||||||
|
// 重置表单
|
||||||
|
formData.enabled = 1
|
||||||
|
formData.ssid = ''
|
||||||
|
formData.password = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(visible, (val) => {
|
||||||
|
emit('update:modelValue', val)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.loading, (val) => {
|
||||||
|
confirmLoading.value = val
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
await formRef.value.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
emit('confirm', {
|
||||||
|
enabled: formData.enabled,
|
||||||
|
ssid: formData.ssid,
|
||||||
|
password: formData.password
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
111
src/components/device/SpeedLimitDialog.vue
Normal file
111
src/components/device/SpeedLimitDialog.vue
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<template>
|
||||||
|
<ElDialog v-model="visible" title="设置限速" width="500px" @close="handleClose">
|
||||||
|
<ElForm
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
label-width="120px"
|
||||||
|
>
|
||||||
|
<ElFormItem label="设备信息">
|
||||||
|
<span style="font-weight: bold; color: #409eff">{{ deviceInfo }}</span>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="下行速率" prop="download_speed">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="formData.download_speed"
|
||||||
|
:min="1"
|
||||||
|
:step="128"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
<div style="color: #909399; font-size: 12px; margin-top: 4px">单位: KB/s</div>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="上行速率" prop="upload_speed">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="formData.upload_speed"
|
||||||
|
:min="1"
|
||||||
|
:step="128"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
<div style="color: #909399; font-size: 12px; margin-top: 4px">单位: KB/s</div>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="visible = false">取消</ElButton>
|
||||||
|
<ElButton type="primary" @click="handleConfirm" :loading="confirmLoading">
|
||||||
|
确认设置
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, watch } from 'vue'
|
||||||
|
import { ElDialog, ElForm, ElFormItem, ElInputNumber, ElButton, ElMessage } from 'element-plus'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: boolean
|
||||||
|
deviceInfo: string
|
||||||
|
loading?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value: boolean): void
|
||||||
|
(e: 'confirm', data: { download_speed: number; upload_speed: number }): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
loading: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
const visible = ref(props.modelValue)
|
||||||
|
const confirmLoading = ref(props.loading)
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
download_speed: 1024,
|
||||||
|
upload_speed: 512
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = reactive<FormRules>({
|
||||||
|
download_speed: [{ required: true, message: '请输入下行速率', trigger: 'blur' }],
|
||||||
|
upload_speed: [{ required: true, message: '请输入上行速率', trigger: 'blur' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
visible.value = val
|
||||||
|
if (val) {
|
||||||
|
// 重置表单
|
||||||
|
formData.download_speed = 1024
|
||||||
|
formData.upload_speed = 512
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(visible, (val) => {
|
||||||
|
emit('update:modelValue', val)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.loading, (val) => {
|
||||||
|
confirmLoading.value = val
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
await formRef.value.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
emit('confirm', {
|
||||||
|
download_speed: formData.download_speed,
|
||||||
|
upload_speed: formData.upload_speed
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
153
src/components/device/SwitchCardDialog.vue
Normal file
153
src/components/device/SwitchCardDialog.vue
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<template>
|
||||||
|
<ElDialog v-model="visible" title="切换SIM卡" width="600px" @close="handleClose">
|
||||||
|
<div v-if="loading" style="text-align: center; padding: 40px">
|
||||||
|
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
|
||||||
|
<div style="margin-top: 16px">加载设备绑定的卡列表中...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<ElAlert
|
||||||
|
v-if="cards.length === 0"
|
||||||
|
title="该设备暂无绑定的SIM卡"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
style="margin-bottom: 20px"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<div>当前设备没有绑定任何SIM卡,无法进行切换操作。</div>
|
||||||
|
<div>请先为设备绑定SIM卡后再进行切换。</div>
|
||||||
|
</template>
|
||||||
|
</ElAlert>
|
||||||
|
|
||||||
|
<ElForm
|
||||||
|
v-if="cards.length > 0"
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
label-width="120px"
|
||||||
|
>
|
||||||
|
<ElFormItem label="设备信息">
|
||||||
|
<span style="font-weight: bold; color: #409eff">{{ deviceInfo }}</span>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="目标ICCID" prop="target_iccid">
|
||||||
|
<ElSelect
|
||||||
|
v-model="formData.target_iccid"
|
||||||
|
placeholder="请选择要切换到的目标ICCID"
|
||||||
|
style="width: 100%"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<ElOption
|
||||||
|
v-for="card in cards"
|
||||||
|
:key="card.iccid"
|
||||||
|
:label="`${card.iccid} - 插槽${card.slot_position} - ${card.carrier_name}`"
|
||||||
|
:value="card.iccid"
|
||||||
|
>
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center">
|
||||||
|
<span>{{ card.iccid }}</span>
|
||||||
|
<ElTag size="small" style="margin-left: 10px">插槽{{ card.slot_position }}</ElTag>
|
||||||
|
</div>
|
||||||
|
</ElOption>
|
||||||
|
</ElSelect>
|
||||||
|
<div style="margin-top: 8px; font-size: 12px; color: #909399">
|
||||||
|
当前设备共绑定 {{ cards.length }} 张SIM卡
|
||||||
|
</div>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="visible = false">取消</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-if="cards.length > 0"
|
||||||
|
type="primary"
|
||||||
|
@click="handleConfirm"
|
||||||
|
:loading="confirmLoading"
|
||||||
|
>
|
||||||
|
确认切换
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, watch } from 'vue'
|
||||||
|
import {
|
||||||
|
ElDialog,
|
||||||
|
ElForm,
|
||||||
|
ElFormItem,
|
||||||
|
ElSelect,
|
||||||
|
ElOption,
|
||||||
|
ElButton,
|
||||||
|
ElAlert,
|
||||||
|
ElIcon,
|
||||||
|
ElTag
|
||||||
|
} from 'element-plus'
|
||||||
|
import { Loading } from '@element-plus/icons-vue'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
|
||||||
|
interface CardBinding {
|
||||||
|
id: number
|
||||||
|
iccid: string
|
||||||
|
slot_position: number
|
||||||
|
carrier_name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: boolean
|
||||||
|
deviceInfo: string
|
||||||
|
cards: CardBinding[]
|
||||||
|
loading?: boolean
|
||||||
|
confirmLoading?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value: boolean): void
|
||||||
|
(e: 'confirm', data: { target_iccid: string }): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
loading: false,
|
||||||
|
confirmLoading: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
const visible = ref(props.modelValue)
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
target_iccid: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = reactive<FormRules>({
|
||||||
|
target_iccid: [{ required: true, message: '请选择目标ICCID', trigger: 'change' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
visible.value = val
|
||||||
|
if (val) {
|
||||||
|
// 重置表单
|
||||||
|
formData.target_iccid = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(visible, (val) => {
|
||||||
|
emit('update:modelValue', val)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
await formRef.value.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
emit('confirm', {
|
||||||
|
target_iccid: formData.target_iccid
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -2,6 +2,23 @@
|
|||||||
|
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化空值为 "-"
|
||||||
|
* @param value 要格式化的值
|
||||||
|
* @returns 格式化后的值
|
||||||
|
*/
|
||||||
|
const formatEmptyValue = (value: any): any => {
|
||||||
|
// 判断值是否为空
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
// 如果是数字0,返回0
|
||||||
|
if (value === 0) {
|
||||||
|
return '0'
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
// 定义列配置接口
|
// 定义列配置接口
|
||||||
export interface ColumnOption {
|
export interface ColumnOption {
|
||||||
type?: 'selection' | 'expand' | 'index'
|
type?: 'selection' | 'expand' | 'index'
|
||||||
@@ -76,6 +93,27 @@ export function useCheckedColumns(columnsFactory: () => ColumnOption[]) {
|
|||||||
const columnMap = new Map<string, ColumnOption>()
|
const columnMap = new Map<string, ColumnOption>()
|
||||||
|
|
||||||
cols.forEach((column) => {
|
cols.forEach((column) => {
|
||||||
|
// 为普通列添加空值处理
|
||||||
|
if (!column.type || (column.type !== 'selection' && column.type !== 'expand' && column.type !== 'index')) {
|
||||||
|
const originalFormatter = column.formatter
|
||||||
|
|
||||||
|
// 包装原有的 formatter,在其基础上添加空值处理
|
||||||
|
column.formatter = (row: any, ...args: any[]) => {
|
||||||
|
let value
|
||||||
|
|
||||||
|
// 如果有自定义 formatter,先执行
|
||||||
|
if (originalFormatter) {
|
||||||
|
value = originalFormatter(row, ...args)
|
||||||
|
} else {
|
||||||
|
// 没有自定义 formatter,直接取字段值
|
||||||
|
value = row[column.prop as string]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对结果进行空值处理
|
||||||
|
return formatEmptyValue(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (column.type === 'selection') {
|
if (column.type === 'selection') {
|
||||||
columnMap.set(SELECTION_KEY, column)
|
columnMap.set(SELECTION_KEY, column)
|
||||||
} else if (column.type === 'expand') {
|
} else if (column.type === 'expand') {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type { FormInstance, FormRules } from 'element-plus'
|
|||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
import { HOME_PAGE } from '@/router/routesAlias'
|
import { HOME_PAGE } from '@/router/routesAlias'
|
||||||
import AppConfig from '@/config'
|
import AppConfig from '@/config'
|
||||||
import { AuthService } from '@/api/authApi'
|
import { AuthService } from '@/api/modules'
|
||||||
import { ApiStatus } from '@/utils/http/status'
|
import { ApiStatus } from '@/utils/http/status'
|
||||||
import { MOCK_ACCOUNTS, mockLogin, mockGetUserInfo, type MockAccount } from '@/mock/auth'
|
import { MOCK_ACCOUNTS, mockLogin, mockGetUserInfo, type MockAccount } from '@/mock/auth'
|
||||||
import { saveCredentials, getRememberedCredentials } from '@/utils/auth/rememberPassword'
|
import { saveCredentials, getRememberedCredentials } from '@/utils/auth/rememberPassword'
|
||||||
|
|||||||
63
src/composables/useTableContextMenu.ts
Normal file
63
src/composables/useTableContextMenu.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* 表格右键菜单的组合式函数
|
||||||
|
* 提供右键菜单功能和鼠标悬浮提示
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
|
||||||
|
export function useTableContextMenu() {
|
||||||
|
// 鼠标悬浮提示相关
|
||||||
|
const showContextMenuHint = ref(false)
|
||||||
|
const hintPosition = reactive({ x: 0, y: 0 })
|
||||||
|
let hintTimer: any = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为表格行添加类名
|
||||||
|
*/
|
||||||
|
const getRowClassName = ({ row, rowIndex }: { row: any; rowIndex: number }) => {
|
||||||
|
return 'table-row-with-context-menu'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单元格鼠标进入事件处理
|
||||||
|
*/
|
||||||
|
const handleCellMouseEnter = (
|
||||||
|
row: any,
|
||||||
|
column: any,
|
||||||
|
cell: HTMLElement,
|
||||||
|
event: MouseEvent
|
||||||
|
) => {
|
||||||
|
// 清除之前的定时器
|
||||||
|
if (hintTimer) {
|
||||||
|
clearTimeout(hintTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟显示提示,避免快速划过时闪烁
|
||||||
|
hintTimer = setTimeout(() => {
|
||||||
|
hintPosition.x = event.clientX + 15
|
||||||
|
hintPosition.y = event.clientY + 10
|
||||||
|
showContextMenuHint.value = true
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单元格鼠标离开事件处理
|
||||||
|
*/
|
||||||
|
const handleCellMouseLeave = () => {
|
||||||
|
if (hintTimer) {
|
||||||
|
clearTimeout(hintTimer)
|
||||||
|
}
|
||||||
|
showContextMenuHint.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
showContextMenuHint,
|
||||||
|
hintPosition,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
getRowClassName,
|
||||||
|
handleCellMouseEnter,
|
||||||
|
handleCellMouseLeave
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -390,10 +390,14 @@
|
|||||||
"packageCreate": "Create Package",
|
"packageCreate": "Create Package",
|
||||||
"packageBatch": "Batch Management",
|
"packageBatch": "Batch Management",
|
||||||
"packageList": "My Packages",
|
"packageList": "My Packages",
|
||||||
|
"packageDetail": "Package Detail",
|
||||||
"packageChange": "Package Change",
|
"packageChange": "Package Change",
|
||||||
"packageAssign": "Package Assignment",
|
"packageAssign": "Package Assignment",
|
||||||
|
"packageAssignDetail": "Package Assignment Detail",
|
||||||
"seriesAssign": "Series Assignment",
|
"seriesAssign": "Series Assignment",
|
||||||
|
"seriesAssignDetail": "Series Assignment Detail",
|
||||||
"packageSeries": "Package Series",
|
"packageSeries": "Package Series",
|
||||||
|
"packageSeriesDetail": "Package Series Detail",
|
||||||
"packageCommission": "Package Commission Cards"
|
"packageCommission": "Package Commission Cards"
|
||||||
},
|
},
|
||||||
"accountManagement": {
|
"accountManagement": {
|
||||||
@@ -406,6 +410,7 @@
|
|||||||
"agent": "Agent Management",
|
"agent": "Agent Management",
|
||||||
"customerAccount": "Customer Account",
|
"customerAccount": "Customer Account",
|
||||||
"enterpriseCustomer": "Enterprise Customer",
|
"enterpriseCustomer": "Enterprise Customer",
|
||||||
|
"enterpriseCustomerAccounts": "Enterprise Customer Accounts",
|
||||||
"enterpriseCards": "Enterprise Card Management",
|
"enterpriseCards": "Enterprise Card Management",
|
||||||
"customerCommission": "Customer Commission"
|
"customerCommission": "Customer Commission"
|
||||||
},
|
},
|
||||||
@@ -416,7 +421,8 @@
|
|||||||
"withdrawalSettings": "Withdrawal Settings",
|
"withdrawalSettings": "Withdrawal Settings",
|
||||||
"myAccount": "My Account",
|
"myAccount": "My Account",
|
||||||
"carrierManagement": "Carrier Management",
|
"carrierManagement": "Carrier Management",
|
||||||
"orders": "Order Management"
|
"orders": "Order Management",
|
||||||
|
"orderDetail": "Order Details"
|
||||||
},
|
},
|
||||||
"deviceManagement": {
|
"deviceManagement": {
|
||||||
"title": "Device Management",
|
"title": "Device Management",
|
||||||
@@ -429,7 +435,8 @@
|
|||||||
"packageSeries": "Package Series Management",
|
"packageSeries": "Package Series Management",
|
||||||
"packageList": "Package Management",
|
"packageList": "Package Management",
|
||||||
"packageAssign": "Package Assignment",
|
"packageAssign": "Package Assignment",
|
||||||
"shop": "Shop Management"
|
"shop": "Shop Management",
|
||||||
|
"shopAccounts": "Shop Accounts"
|
||||||
},
|
},
|
||||||
"assetManagement": {
|
"assetManagement": {
|
||||||
"title": "Asset Management",
|
"title": "Asset Management",
|
||||||
@@ -443,11 +450,17 @@
|
|||||||
"devices": "Device Management",
|
"devices": "Device Management",
|
||||||
"deviceDetail": "Device Details",
|
"deviceDetail": "Device Details",
|
||||||
"assetAssign": "Allocation Records",
|
"assetAssign": "Allocation Records",
|
||||||
|
"assetAssignDetail": "Asset Allocation Details",
|
||||||
"allocationRecordDetail": "Allocation Record Details",
|
"allocationRecordDetail": "Allocation Record Details",
|
||||||
"cardReplacementRequest": "Card Replacement Request",
|
"cardReplacementRequest": "Card Replacement Request",
|
||||||
"authorizationRecords": "Authorization Records",
|
"authorizationRecords": "Authorization Records",
|
||||||
"authorizationDetail": "Authorization Details",
|
"authorizationDetail": "Authorization Details",
|
||||||
"enterpriseDevices": "Enterprise Devices"
|
"authorizationRecordDetail": "Authorization Record Details",
|
||||||
|
"enterpriseDevices": "Enterprise Devices",
|
||||||
|
"recordsManagement": "Records Management",
|
||||||
|
"taskManagement": "Task Management",
|
||||||
|
"exchangeManagement": "Exchange Management",
|
||||||
|
"exchangeDetail": "Exchange Order Detail"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Settings Management",
|
"title": "Settings Management",
|
||||||
|
|||||||
@@ -385,7 +385,7 @@
|
|||||||
},
|
},
|
||||||
"cardManagement": {
|
"cardManagement": {
|
||||||
"title": "我的网卡",
|
"title": "我的网卡",
|
||||||
"singleCard": "单卡信息",
|
"singleCard": "资产信息",
|
||||||
"cardList": "网卡管理",
|
"cardList": "网卡管理",
|
||||||
"cardDetail": "网卡明细",
|
"cardDetail": "网卡明细",
|
||||||
"cardAssign": "网卡分配",
|
"cardAssign": "网卡分配",
|
||||||
@@ -402,10 +402,12 @@
|
|||||||
"packageCreate": "新建套餐",
|
"packageCreate": "新建套餐",
|
||||||
"packageBatch": "批量管理",
|
"packageBatch": "批量管理",
|
||||||
"packageList": "套餐管理",
|
"packageList": "套餐管理",
|
||||||
|
"packageDetail": "套餐详情",
|
||||||
"packageChange": "套餐变更",
|
"packageChange": "套餐变更",
|
||||||
"packageAssign": "单套餐分配",
|
"seriesGrants": "代理系列授权",
|
||||||
"seriesAssign": "套餐系列分配",
|
"seriesGrantsDetail": "代理系列授权详情",
|
||||||
"packageSeries": "套餐系列",
|
"packageSeries": "套餐系列",
|
||||||
|
"packageSeriesDetail": "套餐系列详情",
|
||||||
"packageCommission": "套餐佣金网卡"
|
"packageCommission": "套餐佣金网卡"
|
||||||
},
|
},
|
||||||
"accountManagement": {
|
"accountManagement": {
|
||||||
@@ -418,13 +420,10 @@
|
|||||||
"agent": "代理商管理",
|
"agent": "代理商管理",
|
||||||
"customerAccount": "客户账号",
|
"customerAccount": "客户账号",
|
||||||
"enterpriseCustomer": "企业客户",
|
"enterpriseCustomer": "企业客户",
|
||||||
|
"enterpriseCustomerAccounts": "关联账号列表",
|
||||||
"enterpriseCards": "企业卡管理",
|
"enterpriseCards": "企业卡管理",
|
||||||
"customerCommission": "客户账号佣金"
|
"customerCommission": "客户账号佣金"
|
||||||
},
|
},
|
||||||
"deviceManagement": {
|
|
||||||
"title": "设备管理",
|
|
||||||
"devices": "设备管理"
|
|
||||||
},
|
|
||||||
"product": {
|
"product": {
|
||||||
"title": "商品管理",
|
"title": "商品管理",
|
||||||
"simCard": "号卡管理",
|
"simCard": "号卡管理",
|
||||||
@@ -432,7 +431,8 @@
|
|||||||
"packageSeries": "套餐系列管理",
|
"packageSeries": "套餐系列管理",
|
||||||
"packageList": "套餐管理",
|
"packageList": "套餐管理",
|
||||||
"packageAssign": "套餐分配",
|
"packageAssign": "套餐分配",
|
||||||
"shop": "店铺管理"
|
"shop": "店铺管理",
|
||||||
|
"shopAccounts": "店铺账号列表"
|
||||||
},
|
},
|
||||||
"assetManagement": {
|
"assetManagement": {
|
||||||
"title": "资产管理",
|
"title": "资产管理",
|
||||||
@@ -440,23 +440,32 @@
|
|||||||
"deviceSearch": "设备查询",
|
"deviceSearch": "设备查询",
|
||||||
"singleCard": "单卡信息",
|
"singleCard": "单卡信息",
|
||||||
"standaloneCardList": "IoT卡管理",
|
"standaloneCardList": "IoT卡管理",
|
||||||
|
"iotCardDetail": "IoT卡详情",
|
||||||
"iotCardTask": "IoT卡任务",
|
"iotCardTask": "IoT卡任务",
|
||||||
"deviceTask": "设备任务",
|
"deviceTask": "设备任务",
|
||||||
|
"taskDetail": "任务详情",
|
||||||
"devices": "设备管理",
|
"devices": "设备管理",
|
||||||
"deviceDetail": "设备详情",
|
"deviceDetail": "设备详情",
|
||||||
"assetAssign": "分配记录",
|
"assetAssign": "分配记录",
|
||||||
|
"assetAssignDetail": "资产分配详情",
|
||||||
"allocationRecordDetail": "分配记录详情",
|
"allocationRecordDetail": "分配记录详情",
|
||||||
"cardReplacementRequest": "换卡申请",
|
"cardReplacementRequest": "换卡申请",
|
||||||
"authorizationRecords": "授权记录",
|
"authorizationRecords": "授权记录",
|
||||||
"authorizationDetail": "授权记录详情",
|
"authorizationDetail": "授权记录详情",
|
||||||
"enterpriseDevices": "企业设备列表"
|
"authorizationRecordDetail": "授权记录详情",
|
||||||
|
"enterpriseDevices": "企业设备列表",
|
||||||
|
"recordsManagement": "记录管理",
|
||||||
|
"taskManagement": "任务管理",
|
||||||
|
"exchangeManagement": "换货管理",
|
||||||
|
"exchangeDetail": "换货单详情"
|
||||||
},
|
},
|
||||||
"account": {
|
"account": {
|
||||||
"title": "账户管理",
|
"title": "账户管理",
|
||||||
"customerAccount": "客户账号",
|
"customerAccount": "客户账号",
|
||||||
"myAccount": "我的账户",
|
"myAccount": "我的账户",
|
||||||
"carrierManagement": "运营商管理",
|
"carrierManagement": "运营商管理",
|
||||||
"orders": "订单管理"
|
"orders": "订单管理",
|
||||||
|
"orderDetail": "订单详情"
|
||||||
},
|
},
|
||||||
"commission": {
|
"commission": {
|
||||||
"menu": {
|
"menu": {
|
||||||
@@ -464,7 +473,7 @@
|
|||||||
"withdrawal": "提现审批",
|
"withdrawal": "提现审批",
|
||||||
"withdrawalSettings": "提现配置",
|
"withdrawalSettings": "提现配置",
|
||||||
"myCommission": "我的佣金",
|
"myCommission": "我的佣金",
|
||||||
"agentCommission": "代理商佣金管理"
|
"agentCommission": "代理商佣金"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -599,7 +608,7 @@
|
|||||||
"withdrawal": "提现审批",
|
"withdrawal": "提现审批",
|
||||||
"withdrawalSettings": "提现配置",
|
"withdrawalSettings": "提现配置",
|
||||||
"myCommission": "我的佣金",
|
"myCommission": "我的佣金",
|
||||||
"agentCommission": "代理商佣金管理"
|
"agentCommission": "代理商佣金"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"withdrawalNo": "提现单号",
|
"withdrawalNo": "提现单号",
|
||||||
|
|||||||
@@ -110,14 +110,22 @@ async function handleRouteGuard(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试刷新路由重新注册
|
// 路由未匹配时,判断是 404 还是 403
|
||||||
if (userStore.isLogin) {
|
if (userStore.isLogin) {
|
||||||
isRouteRegistered.value = false
|
// 检查路由是否存在于 asyncRoutes 中
|
||||||
await handleDynamicRoutes(to, router, next)
|
const routeExistsInAsync = checkRouteExistsInAsyncRoutes(to.path, asyncRoutes)
|
||||||
|
console.log(`[路由检查] 目标路径: ${to.path}, 是否存在于asyncRoutes: ${routeExistsInAsync}`)
|
||||||
|
|
||||||
|
if (routeExistsInAsync) {
|
||||||
|
// 路由存在于 asyncRoutes 但未注册,说明用户没有权限
|
||||||
|
console.warn('路由存在但用户无权限访问:', to.path)
|
||||||
|
next(RoutesAlias.Exception403 || '/exception/403')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 如果以上都不匹配,跳转到404
|
// 路由不存在,跳转到 404
|
||||||
|
console.log(`[路由检查] 路径 ${to.path} 不存在于asyncRoutes,跳转404`)
|
||||||
next(RoutesAlias.Exception404)
|
next(RoutesAlias.Exception404)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,6 +325,7 @@ function buildRouteMap(routes: AppRouteRecord[], parentPath = ''): Map<string, A
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 将后端菜单数据转换为路由格式
|
* 将后端菜单数据转换为路由格式
|
||||||
|
* 递归处理所有层级的 children
|
||||||
*/
|
*/
|
||||||
function convertBackendMenuToRoute(
|
function convertBackendMenuToRoute(
|
||||||
menu: any,
|
menu: any,
|
||||||
@@ -328,6 +337,32 @@ function convertBackendMenuToRoute(
|
|||||||
|
|
||||||
if (!matchedRoute) {
|
if (!matchedRoute) {
|
||||||
console.warn(`未找到与菜单 URL "${menuUrl}" 匹配的路由定义: ${menu.name}`)
|
console.warn(`未找到与菜单 URL "${menuUrl}" 匹配的路由定义: ${menu.name}`)
|
||||||
|
|
||||||
|
// 如果当前菜单没有匹配的路由,但有 children,尝试递归处理 children
|
||||||
|
if (menu.children && menu.children.length > 0) {
|
||||||
|
const children = menu.children
|
||||||
|
.map((child: any) => convertBackendMenuToRoute(child, routeMap, menuUrl))
|
||||||
|
.filter((child: AppRouteRecord | null) => child !== null)
|
||||||
|
|
||||||
|
// 如果子菜单有有效的路由,返回一个包含子菜单的占位路由
|
||||||
|
if (children.length > 0) {
|
||||||
|
return {
|
||||||
|
path: menuUrl,
|
||||||
|
name: menu.name || menuUrl.replace(/\//g, '_'),
|
||||||
|
meta: {
|
||||||
|
title: menu.name,
|
||||||
|
permission: menu.perm_code,
|
||||||
|
sort: menu.sort || 0,
|
||||||
|
icon: menu.icon,
|
||||||
|
isHide: false,
|
||||||
|
roles: undefined,
|
||||||
|
permissions: undefined
|
||||||
|
},
|
||||||
|
children: children
|
||||||
|
} as AppRouteRecord
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,6 +382,7 @@ function convertBackendMenuToRoute(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 递归处理后端返回的 children
|
||||||
if (menu.children && menu.children.length > 0) {
|
if (menu.children && menu.children.length > 0) {
|
||||||
const children = menu.children
|
const children = menu.children
|
||||||
.map((child: any) => convertBackendMenuToRoute(child, routeMap, menuUrl))
|
.map((child: any) => convertBackendMenuToRoute(child, routeMap, menuUrl))
|
||||||
@@ -425,6 +461,88 @@ function isValidMenuList(menuList: AppRouteRecord[]): boolean {
|
|||||||
return Array.isArray(menuList) && menuList.length > 0
|
return Array.isArray(menuList) && menuList.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查路由是否存在于 asyncRoutes 中
|
||||||
|
* 支持动态参数匹配,如 /account-management/enterprise-customer/customer-accounts/3000 匹配 enterprise-customer/customer-accounts/:id
|
||||||
|
* @param targetPath 目标路由路径,如 /account-management/enterprise-customer/customer-accounts/3000
|
||||||
|
* @param routes 路由配置数组
|
||||||
|
* @param parentPath 父路由路径
|
||||||
|
*/
|
||||||
|
function checkRouteExistsInAsyncRoutes(
|
||||||
|
targetPath: string,
|
||||||
|
routes: AppRouteRecord[],
|
||||||
|
parentPath = ''
|
||||||
|
): boolean {
|
||||||
|
for (const route of routes) {
|
||||||
|
// 构建完整路径
|
||||||
|
const fullPath = route.path.startsWith('/')
|
||||||
|
? route.path
|
||||||
|
: parentPath
|
||||||
|
? `${parentPath}/${route.path}`.replace(/\/+/g, '/')
|
||||||
|
: `/${route.path}`
|
||||||
|
|
||||||
|
// 检查当前路由是否匹配(支持动态参数)
|
||||||
|
const isMatch = matchRoutePath(targetPath, fullPath)
|
||||||
|
|
||||||
|
if (isMatch) {
|
||||||
|
console.log(`[路由匹配成功] 目标: ${targetPath}, 匹配到: ${fullPath}`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归检查子路由
|
||||||
|
if (route.children && route.children.length > 0) {
|
||||||
|
if (checkRouteExistsInAsyncRoutes(targetPath, route.children, fullPath)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匹配路由路径,支持动态参数
|
||||||
|
* @param targetPath 实际路径,如 /account-management/enterprise-customer/customer-accounts/3000
|
||||||
|
* @param routePath 路由定义路径,如 /account-management/enterprise-customer/customer-accounts/:id
|
||||||
|
*/
|
||||||
|
function matchRoutePath(targetPath: string, routePath: string): boolean {
|
||||||
|
// 移除查询参数
|
||||||
|
const cleanTargetPath = targetPath.split('?')[0]
|
||||||
|
const cleanRoutePath = routePath.split('?')[0]
|
||||||
|
|
||||||
|
// 如果完全匹配,直接返回
|
||||||
|
if (cleanTargetPath === cleanRoutePath) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将路径分割成段
|
||||||
|
const targetSegments = cleanTargetPath.split('/').filter(Boolean)
|
||||||
|
const routeSegments = cleanRoutePath.split('/').filter(Boolean)
|
||||||
|
|
||||||
|
// 段数必须相同
|
||||||
|
if (targetSegments.length !== routeSegments.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 逐段比较
|
||||||
|
for (let i = 0; i < routeSegments.length; i++) {
|
||||||
|
const routeSegment = routeSegments[i]
|
||||||
|
const targetSegment = targetSegments[i]
|
||||||
|
|
||||||
|
// 如果是动态参数(以 : 开头),跳过比较
|
||||||
|
if (routeSegment.startsWith(':')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不是动态参数,必须完全匹配
|
||||||
|
if (routeSegment !== targetSegment) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重置路由相关状态
|
* 重置路由相关状态
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -107,10 +107,12 @@ export const hasRoutePermission = (
|
|||||||
*/
|
*/
|
||||||
function checkMenuAccess(path: string, menus: any[]): boolean {
|
function checkMenuAccess(path: string, menus: any[]): boolean {
|
||||||
for (const menu of menus) {
|
for (const menu of menus) {
|
||||||
// 检查当前菜单的 URL 是否匹配
|
// 检查当前菜单的 URL 是否匹配(支持动态参数)
|
||||||
if (menu.url && (path === menu.url || path.startsWith(menu.url + '/'))) {
|
if (menu.url && matchMenuPath(path, menu.url)) {
|
||||||
|
console.log(`[菜单权限检查] 路径 ${path} 匹配菜单 ${menu.url}`)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 递归检查子菜单
|
// 递归检查子菜单
|
||||||
if (menu.children && menu.children.length > 0) {
|
if (menu.children && menu.children.length > 0) {
|
||||||
if (checkMenuAccess(path, menu.children)) {
|
if (checkMenuAccess(path, menu.children)) {
|
||||||
@@ -118,9 +120,53 @@ function checkMenuAccess(path: string, menus: any[]): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log(`[菜单权限检查] 路径 ${path} 未找到匹配的菜单`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匹配菜单路径,支持动态参数
|
||||||
|
* @param actualPath 实际访问路径,如 /account-management/enterprise-customer/customer-accounts/3000
|
||||||
|
* @param menuPath 菜单定义路径,如 /account-management/enterprise-customer/customer-accounts/:id
|
||||||
|
*/
|
||||||
|
function matchMenuPath(actualPath: string, menuPath: string): boolean {
|
||||||
|
// 移除查询参数
|
||||||
|
const cleanActualPath = actualPath.split('?')[0]
|
||||||
|
const cleanMenuPath = menuPath.split('?')[0]
|
||||||
|
|
||||||
|
// 如果完全匹配,直接返回
|
||||||
|
if (cleanActualPath === cleanMenuPath) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将路径分割成段
|
||||||
|
const actualSegments = cleanActualPath.split('/').filter(Boolean)
|
||||||
|
const menuSegments = cleanMenuPath.split('/').filter(Boolean)
|
||||||
|
|
||||||
|
// 段数必须相同
|
||||||
|
if (actualSegments.length !== menuSegments.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 逐段比较
|
||||||
|
for (let i = 0; i < menuSegments.length; i++) {
|
||||||
|
const menuSegment = menuSegments[i]
|
||||||
|
const actualSegment = actualSegments[i]
|
||||||
|
|
||||||
|
// 如果是动态参数(以 : 开头),跳过比较
|
||||||
|
if (menuSegment.startsWith(':')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不是动态参数,必须完全匹配
|
||||||
|
if (menuSegment !== actualSegment) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查 Token 是否有效
|
* 检查 Token 是否有效
|
||||||
* 简单检查,真实项目中应该验证 JWT 或者调用后端接口
|
* 简单检查,真实项目中应该验证 JWT 或者调用后端接口
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// {
|
// {
|
||||||
// path: '/widgets',
|
// path: '/widgets',
|
||||||
// name: 'Widgets',
|
// name: 'Widgets',
|
||||||
@@ -266,6 +267,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
// }
|
// }
|
||||||
// ]
|
// ]
|
||||||
// },
|
// },
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/system',
|
path: '/system',
|
||||||
name: 'System',
|
name: 'System',
|
||||||
@@ -296,6 +298,15 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
roles: ['R_SUPER']
|
roles: ['R_SUPER']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'carrier-management',
|
||||||
|
name: 'CarrierManagement',
|
||||||
|
component: RoutesAlias.CarrierManagement,
|
||||||
|
meta: {
|
||||||
|
title: 'menus.account.carrierManagement',
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'user-center',
|
path: 'user-center',
|
||||||
name: 'UserCenter',
|
name: 'UserCenter',
|
||||||
@@ -420,6 +431,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
// }
|
// }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// {
|
// {
|
||||||
// path: '/article',
|
// path: '/article',
|
||||||
// name: 'Article',
|
// name: 'Article',
|
||||||
@@ -611,15 +623,15 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
// 物联网卡管理系统模块
|
// 物联网卡管理系统模块
|
||||||
{
|
// {
|
||||||
path: '/card-management',
|
// path: '/card-management',
|
||||||
name: 'CardManagement',
|
// name: 'CardManagement',
|
||||||
component: RoutesAlias.Home,
|
// component: RoutesAlias.Home,
|
||||||
meta: {
|
// meta: {
|
||||||
title: 'menus.cardManagement.title',
|
// title: 'menus.cardManagement.title',
|
||||||
icon: ''
|
// icon: ''
|
||||||
},
|
// },
|
||||||
children: [
|
// children: [
|
||||||
// {
|
// {
|
||||||
// path: 'card-detail',
|
// path: 'card-detail',
|
||||||
// name: 'CardDetail',
|
// name: 'CardDetail',
|
||||||
@@ -629,71 +641,81 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
// keepAlive: true
|
// keepAlive: true
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
{
|
// {
|
||||||
path: 'card-assign',
|
// path: 'single-card',
|
||||||
name: 'CardAssign',
|
// name: 'SingleCard',
|
||||||
component: RoutesAlias.CardAssign,
|
// component: RoutesAlias.SingleCard,
|
||||||
meta: {
|
// meta: {
|
||||||
title: 'menus.cardManagement.cardAssign',
|
// title: 'menus.cardManagement.singleCard',
|
||||||
keepAlive: true
|
// keepAlive: true
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
path: 'card-shutdown',
|
// path: 'card-assign',
|
||||||
name: 'CardShutdown',
|
// name: 'CardAssign',
|
||||||
component: RoutesAlias.CardShutdown,
|
// component: RoutesAlias.CardAssign,
|
||||||
meta: {
|
// meta: {
|
||||||
title: 'menus.cardManagement.cardShutdown',
|
// title: 'menus.cardManagement.cardAssign',
|
||||||
keepAlive: true
|
// keepAlive: true
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
path: 'my-cards',
|
// path: 'card-shutdown',
|
||||||
name: 'MyCards',
|
// name: 'CardShutdown',
|
||||||
component: RoutesAlias.MyCards,
|
// component: RoutesAlias.CardShutdown,
|
||||||
meta: {
|
// meta: {
|
||||||
title: 'menus.cardManagement.myCards',
|
// title: 'menus.cardManagement.cardShutdown',
|
||||||
keepAlive: true
|
// keepAlive: true
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
path: 'card-transfer',
|
// path: 'my-cards',
|
||||||
name: 'CardTransfer',
|
// name: 'MyCards',
|
||||||
component: RoutesAlias.CardTransfer,
|
// component: RoutesAlias.MyCards,
|
||||||
meta: {
|
// meta: {
|
||||||
title: 'menus.cardManagement.cardTransfer',
|
// title: 'menus.cardManagement.myCards',
|
||||||
keepAlive: true
|
// keepAlive: true
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
path: 'card-replacement',
|
// path: 'card-transfer',
|
||||||
name: 'CardReplacement',
|
// name: 'CardTransfer',
|
||||||
component: RoutesAlias.CardReplacement,
|
// component: RoutesAlias.CardTransfer,
|
||||||
meta: {
|
// meta: {
|
||||||
title: 'menus.cardManagement.cardReplacement',
|
// title: 'menus.cardManagement.cardTransfer',
|
||||||
keepAlive: true
|
// keepAlive: true
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
path: 'package-gift',
|
// path: 'card-replacement',
|
||||||
name: 'PackageGift',
|
// name: 'CardReplacement',
|
||||||
component: RoutesAlias.PackageGift,
|
// component: RoutesAlias.CardReplacement,
|
||||||
meta: {
|
// meta: {
|
||||||
title: 'menus.cardManagement.packageGift',
|
// title: 'menus.cardManagement.cardReplacement',
|
||||||
keepAlive: true
|
// keepAlive: true
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
path: 'card-change-card',
|
// path: 'package-gift',
|
||||||
name: 'CardChangeCard',
|
// name: 'PackageGift',
|
||||||
component: RoutesAlias.CardChangeCard,
|
// component: RoutesAlias.PackageGift,
|
||||||
meta: {
|
// meta: {
|
||||||
title: 'menus.cardManagement.cardChange',
|
// title: 'menus.cardManagement.packageGift',
|
||||||
keepAlive: true
|
// keepAlive: true
|
||||||
}
|
// }
|
||||||
}
|
// },
|
||||||
]
|
// {
|
||||||
},
|
// path: 'card-change-card',
|
||||||
|
// name: 'CardChangeCard',
|
||||||
|
// component: RoutesAlias.CardChangeCard,
|
||||||
|
// meta: {
|
||||||
|
// title: 'menus.cardManagement.cardChange',
|
||||||
|
// keepAlive: true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/package-management',
|
path: '/package-management',
|
||||||
name: 'PackageManagement',
|
name: 'PackageManagement',
|
||||||
@@ -712,6 +734,16 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
keepAlive: true
|
keepAlive: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'package-series/detail/:id',
|
||||||
|
name: 'PackageSeriesDetail',
|
||||||
|
component: RoutesAlias.PackageSeriesDetail,
|
||||||
|
meta: {
|
||||||
|
title: 'menus.packageManagement.packageSeriesDetail',
|
||||||
|
isHide: true,
|
||||||
|
keepAlive: false
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'package-list',
|
path: 'package-list',
|
||||||
name: 'PackageList',
|
name: 'PackageList',
|
||||||
@@ -722,25 +754,37 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'package-assign',
|
path: 'package-list/detail/:id',
|
||||||
name: 'PackageAssign',
|
name: 'PackageDetail',
|
||||||
component: RoutesAlias.PackageAssign,
|
component: RoutesAlias.PackageDetail,
|
||||||
meta: {
|
meta: {
|
||||||
title: 'menus.packageManagement.packageAssign',
|
title: 'menus.packageManagement.packageDetail',
|
||||||
|
isHide: true,
|
||||||
|
keepAlive: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'series-grants',
|
||||||
|
name: 'SeriesGrants',
|
||||||
|
component: RoutesAlias.SeriesGrants,
|
||||||
|
meta: {
|
||||||
|
title: 'menus.packageManagement.seriesGrants',
|
||||||
keepAlive: true
|
keepAlive: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'series-assign',
|
path: 'series-grants/detail/:id',
|
||||||
name: 'SeriesAssign',
|
name: 'SeriesGrantsDetail',
|
||||||
component: RoutesAlias.SeriesAssign,
|
component: RoutesAlias.SeriesGrantsDetail,
|
||||||
meta: {
|
meta: {
|
||||||
title: 'menus.packageManagement.seriesAssign',
|
title: 'menus.packageManagement.seriesGrantsDetail',
|
||||||
keepAlive: true
|
isHide: true,
|
||||||
|
keepAlive: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/shop-management',
|
path: '/shop-management',
|
||||||
name: 'ShopManagement',
|
name: 'ShopManagement',
|
||||||
@@ -761,6 +805,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/account-management',
|
path: '/account-management',
|
||||||
name: 'AccountManagement',
|
name: 'AccountManagement',
|
||||||
@@ -779,24 +824,24 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
keepAlive: true
|
keepAlive: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
path: 'platform-account',
|
// path: 'platform-account',
|
||||||
name: 'PlatformAccount',
|
// name: 'PlatformAccount',
|
||||||
component: RoutesAlias.PlatformAccount,
|
// component: RoutesAlias.PlatformAccount,
|
||||||
meta: {
|
// meta: {
|
||||||
title: 'menus.accountManagement.platformAccount',
|
// title: 'menus.accountManagement.platformAccount',
|
||||||
keepAlive: true
|
// keepAlive: true
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
path: 'shop-account',
|
// path: 'shop-account',
|
||||||
name: 'ShopAccount',
|
// name: 'ShopAccount',
|
||||||
component: RoutesAlias.ShopAccount,
|
// component: RoutesAlias.ShopAccount,
|
||||||
meta: {
|
// meta: {
|
||||||
title: 'menus.accountManagement.shopAccount',
|
// title: 'menus.accountManagement.shopAccount',
|
||||||
keepAlive: true
|
// keepAlive: true
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
// {
|
// {
|
||||||
// path: 'customer-role',
|
// path: 'customer-role',
|
||||||
// name: 'CustomerRole',
|
// name: 'CustomerRole',
|
||||||
@@ -816,14 +861,24 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
path: 'customer-account',
|
path: 'enterprise-customer',
|
||||||
name: 'CustomerAccount',
|
name: 'EnterpriseCustomer',
|
||||||
component: RoutesAlias.CustomerAccount,
|
component: RoutesAlias.EnterpriseCustomer,
|
||||||
meta: {
|
meta: {
|
||||||
title: 'menus.accountManagement.customerAccount',
|
title: 'menus.accountManagement.enterpriseCustomer',
|
||||||
keepAlive: true
|
keepAlive: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'enterprise-customer/customer-accounts/:id',
|
||||||
|
name: 'EnterpriseCustomerAccounts',
|
||||||
|
component: RoutesAlias.EnterpriseCustomerAccounts,
|
||||||
|
meta: {
|
||||||
|
title: 'menus.accountManagement.enterpriseCustomerAccounts',
|
||||||
|
isHide: true,
|
||||||
|
keepAlive: false
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'enterprise-cards',
|
path: 'enterprise-cards',
|
||||||
name: 'EnterpriseCards',
|
name: 'EnterpriseCards',
|
||||||
@@ -836,6 +891,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// {
|
// {
|
||||||
// path: '/product',
|
// path: '/product',
|
||||||
// name: 'Product',
|
// name: 'Product',
|
||||||
@@ -901,6 +957,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
// }
|
// }
|
||||||
// ]
|
// ]
|
||||||
// },
|
// },
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/asset-management',
|
path: '/asset-management',
|
||||||
name: 'AssetManagement',
|
name: 'AssetManagement',
|
||||||
@@ -910,6 +967,15 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
icon: ''
|
icon: ''
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: 'single-card',
|
||||||
|
name: 'SingleCard',
|
||||||
|
component: RoutesAlias.SingleCard,
|
||||||
|
meta: {
|
||||||
|
title: 'menus.cardManagement.singleCard',
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'iot-card-management',
|
path: 'iot-card-management',
|
||||||
name: 'StandaloneCardList',
|
name: 'StandaloneCardList',
|
||||||
@@ -919,6 +985,105 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
keepAlive: true
|
keepAlive: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'devices',
|
||||||
|
name: 'DeviceList',
|
||||||
|
component: RoutesAlias.DeviceList,
|
||||||
|
meta: {
|
||||||
|
title: 'menus.assetManagement.devices',
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'enterprise-devices',
|
||||||
|
name: 'EnterpriseDevices',
|
||||||
|
component: RoutesAlias.EnterpriseDevices,
|
||||||
|
meta: {
|
||||||
|
title: 'menus.assetManagement.enterpriseDevices',
|
||||||
|
isHide: true,
|
||||||
|
keepAlive: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 换货管理
|
||||||
|
{
|
||||||
|
path: 'exchange-management',
|
||||||
|
name: 'ExchangeManagement',
|
||||||
|
component: RoutesAlias.ExchangeManagement,
|
||||||
|
meta: {
|
||||||
|
title: 'menus.assetManagement.exchangeManagement',
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'exchange-management/detail/:id',
|
||||||
|
name: 'ExchangeDetail',
|
||||||
|
component: RoutesAlias.ExchangeDetail,
|
||||||
|
meta: {
|
||||||
|
title: 'menus.assetManagement.exchangeDetail',
|
||||||
|
isHide: true,
|
||||||
|
keepAlive: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 记录管理
|
||||||
|
{
|
||||||
|
path: 'records',
|
||||||
|
name: 'RecordsManagement',
|
||||||
|
component: '',
|
||||||
|
meta: {
|
||||||
|
title: 'menus.assetManagement.recordsManagement',
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'asset-assign',
|
||||||
|
name: 'AssetAssign',
|
||||||
|
component: RoutesAlias.AssetAssign,
|
||||||
|
meta: {
|
||||||
|
title: 'menus.assetManagement.assetAssign',
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'asset-assign/detail/:id',
|
||||||
|
name: 'AssetAssignDetail',
|
||||||
|
component: RoutesAlias.AssetAssignDetail,
|
||||||
|
meta: {
|
||||||
|
title: 'menus.assetManagement.assetAssignDetail',
|
||||||
|
isHide: true,
|
||||||
|
keepAlive: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'authorization-records',
|
||||||
|
name: 'AuthorizationRecords',
|
||||||
|
component: RoutesAlias.AuthorizationRecords,
|
||||||
|
meta: {
|
||||||
|
title: 'menus.assetManagement.authorizationRecords',
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'authorization-records/detail/:id',
|
||||||
|
name: 'AuthorizationRecordDetail',
|
||||||
|
component: RoutesAlias.AuthorizationRecordDetail,
|
||||||
|
meta: {
|
||||||
|
title: 'menus.assetManagement.authorizationRecordDetail',
|
||||||
|
isHide: true,
|
||||||
|
keepAlive: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// 任务管理
|
||||||
|
{
|
||||||
|
path: 'tasks',
|
||||||
|
name: 'TaskManagement',
|
||||||
|
component: '',
|
||||||
|
meta: {
|
||||||
|
title: 'menus.assetManagement.taskManagement',
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
|
children: [
|
||||||
{
|
{
|
||||||
path: 'iot-card-task',
|
path: 'iot-card-task',
|
||||||
name: 'IotCardTask',
|
name: 'IotCardTask',
|
||||||
@@ -938,83 +1103,20 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'devices',
|
path: 'task-detail',
|
||||||
name: 'DeviceList',
|
name: 'TaskDetail',
|
||||||
component: RoutesAlias.DeviceList,
|
component: RoutesAlias.TaskDetail,
|
||||||
meta: {
|
meta: {
|
||||||
title: 'menus.assetManagement.devices',
|
title: 'menus.assetManagement.taskDetail',
|
||||||
keepAlive: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'device-detail',
|
|
||||||
name: 'DeviceDetail',
|
|
||||||
component: RoutesAlias.DeviceDetail,
|
|
||||||
meta: {
|
|
||||||
title: 'menus.assetManagement.deviceDetail',
|
|
||||||
isHide: true,
|
|
||||||
keepAlive: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'asset-assign',
|
|
||||||
name: 'AssetAssign',
|
|
||||||
component: RoutesAlias.AssetAssign,
|
|
||||||
meta: {
|
|
||||||
title: 'menus.assetManagement.assetAssign',
|
|
||||||
keepAlive: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'allocation-record-detail',
|
|
||||||
name: 'AllocationRecordDetail',
|
|
||||||
component: RoutesAlias.AllocationRecordDetail,
|
|
||||||
meta: {
|
|
||||||
title: 'menus.assetManagement.allocationRecordDetail',
|
|
||||||
isHide: true,
|
|
||||||
keepAlive: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'card-replacement-request',
|
|
||||||
name: 'CardReplacementRequest',
|
|
||||||
component: RoutesAlias.CardReplacementRequest,
|
|
||||||
meta: {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'enterprise-devices',
|
|
||||||
name: 'EnterpriseDevices',
|
|
||||||
component: RoutesAlias.EnterpriseDevices,
|
|
||||||
meta: {
|
|
||||||
title: 'menus.assetManagement.enterpriseDevices',
|
|
||||||
isHide: true,
|
isHide: true,
|
||||||
keepAlive: false
|
keepAlive: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/account',
|
path: '/account',
|
||||||
name: 'Finance',
|
name: 'Finance',
|
||||||
@@ -1033,33 +1135,25 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
// keepAlive: true
|
// keepAlive: true
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
{
|
|
||||||
path: 'enterprise-customer',
|
|
||||||
name: 'EnterpriseCustomer',
|
|
||||||
component: RoutesAlias.EnterpriseCustomer,
|
|
||||||
meta: {
|
|
||||||
title: 'menus.accountManagement.enterpriseCustomer',
|
|
||||||
keepAlive: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'carrier-management',
|
|
||||||
name: 'CarrierManagement',
|
|
||||||
component: RoutesAlias.CarrierManagement,
|
|
||||||
meta: {
|
|
||||||
title: 'menus.account.carrierManagement',
|
|
||||||
keepAlive: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'orders',
|
path: 'orders',
|
||||||
name: 'OrderManagement',
|
name: 'OrderManagement',
|
||||||
component: RoutesAlias.OrderList,
|
component: '/order-management/order-list',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'menus.account.orders',
|
title: 'menus.account.orders',
|
||||||
keepAlive: true
|
keepAlive: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'orders/detail/:id',
|
||||||
|
name: 'OrderDetail',
|
||||||
|
component: '/order-management/order-list/detail',
|
||||||
|
meta: {
|
||||||
|
title: 'menus.account.orderDetail',
|
||||||
|
isHide: true,
|
||||||
|
keepAlive: false
|
||||||
|
}
|
||||||
|
}
|
||||||
// {
|
// {
|
||||||
// path: 'my-account',
|
// path: 'my-account',
|
||||||
// name: 'MyAccount',
|
// name: 'MyAccount',
|
||||||
@@ -1069,10 +1163,44 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
// keepAlive: true
|
// keepAlive: true
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: 'commission',
|
path: '/finance',
|
||||||
|
name: 'FinanceManagement',
|
||||||
|
component: RoutesAlias.Home,
|
||||||
|
meta: {
|
||||||
|
title: '财务管理',
|
||||||
|
icon: ''
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'agent-recharge',
|
||||||
|
name: 'AgentRecharge',
|
||||||
|
component: RoutesAlias.AgentRecharge,
|
||||||
|
meta: {
|
||||||
|
title: '代理充值',
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'agent-recharge/detail/:id',
|
||||||
|
name: 'AgentRechargeDetailRoute',
|
||||||
|
component: RoutesAlias.AgentRechargeDetail,
|
||||||
|
meta: {
|
||||||
|
title: '充值详情',
|
||||||
|
isHide: true,
|
||||||
|
keepAlive: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/commission',
|
||||||
name: 'CommissionManagement',
|
name: 'CommissionManagement',
|
||||||
component: '',
|
component: RoutesAlias.Home,
|
||||||
meta: {
|
meta: {
|
||||||
title: 'menus.commission.menu.management',
|
title: 'menus.commission.menu.management',
|
||||||
icon: ''
|
icon: ''
|
||||||
@@ -1119,18 +1247,36 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
name: 'Settings',
|
||||||
|
component: RoutesAlias.Home,
|
||||||
|
meta: {
|
||||||
|
title: '设置管理',
|
||||||
|
icon: ''
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'wechat-config',
|
||||||
|
name: 'WechatConfig',
|
||||||
|
component: RoutesAlias.WechatConfig,
|
||||||
|
meta: {
|
||||||
|
title: '微信支付配置',
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'wechat-config/detail/:id',
|
||||||
|
name: 'WechatConfigDetailRoute',
|
||||||
|
component: RoutesAlias.WechatConfigDetail,
|
||||||
|
meta: {
|
||||||
|
title: '支付配置详情',
|
||||||
|
isHide: true,
|
||||||
|
keepAlive: false
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
}
|
||||||
// {
|
|
||||||
// path: '/settings',
|
|
||||||
// name: 'Settings',
|
|
||||||
// component: RoutesAlias.Home,
|
|
||||||
// meta: {
|
|
||||||
// title: 'menus.settings.title',
|
|
||||||
// icon: ''
|
|
||||||
// },
|
|
||||||
// children: [
|
|
||||||
// {
|
// {
|
||||||
// path: 'payment-merchant',
|
// path: 'payment-merchant',
|
||||||
// name: 'PaymentMerchant',
|
// name: 'PaymentMerchant',
|
||||||
@@ -1158,6 +1304,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
// keepAlive: true
|
// keepAlive: true
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// ]
|
]
|
||||||
// }
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -68,46 +68,55 @@ export enum RoutesAlias {
|
|||||||
PackageCreate = '/package-management/package-create', // 新建套餐
|
PackageCreate = '/package-management/package-create', // 新建套餐
|
||||||
PackageBatch = '/package-management/package-batch', // 批量管理
|
PackageBatch = '/package-management/package-batch', // 批量管理
|
||||||
PackageList = '/package-management/package-list', // 套餐管理
|
PackageList = '/package-management/package-list', // 套餐管理
|
||||||
|
PackageDetail = '/package-management/package-list/detail', // 套餐详情
|
||||||
PackageChange = '/package-management/package-change', // 套餐变更
|
PackageChange = '/package-management/package-change', // 套餐变更
|
||||||
PackageAssign = '/package-management/package-assign', // 单套餐分配
|
SeriesGrants = '/package-management/series-grants', // 代理系列授权
|
||||||
SeriesAssign = '/package-management/series-assign', // 套餐系列分配
|
SeriesGrantsDetail = '/package-management/series-grants/detail', // 代理系列授权详情
|
||||||
PackageSeries = '/package-management/package-series', // 套餐系列
|
PackageSeries = '/package-management/package-series', // 套餐系列
|
||||||
|
PackageSeriesDetail = '/package-management/package-series/detail', // 套餐系列详情
|
||||||
PackageCommission = '/package-management/package-commission', // 套餐佣金网卡
|
PackageCommission = '/package-management/package-commission', // 套餐佣金网卡
|
||||||
|
|
||||||
// 账号管理
|
// 账号管理
|
||||||
Account = '/account-management/account', // 账号管理
|
Account = '/account-management/account', // 账号管理
|
||||||
PlatformAccount = '/account-management/platform-account', // 平台账号管理
|
// PlatformAccount = '/account-management/platform-account', // 平台账号管理
|
||||||
CustomerManagement = '/account-management/customer', // 客户管理
|
CustomerManagement = '/account-management/customer', // 客户管理
|
||||||
CustomerRole = '/account-management/customer-role', // 客户角色
|
CustomerRole = '/account-management/customer-role', // 客户角色
|
||||||
AgentManagement = '/account-management/agent', // 代理商管理
|
AgentManagement = '/account-management/agent', // 代理商管理
|
||||||
CustomerAccount = '/account-management/customer-account', // 客户账号管理
|
|
||||||
ShopAccount = '/account-management/shop-account', // 代理账号管理
|
ShopAccount = '/account-management/shop-account', // 代理账号管理
|
||||||
EnterpriseCustomer = '/account-management/enterprise-customer', // 企业客户管理
|
EnterpriseCustomer = '/account-management/enterprise-customer', // 企业客户管理
|
||||||
|
EnterpriseCustomerAccounts = '/common/account-list', // 企业客户账号列表(通用)
|
||||||
EnterpriseCards = '/account-management/enterprise-cards', // 企业卡管理
|
EnterpriseCards = '/account-management/enterprise-cards', // 企业卡管理
|
||||||
CustomerCommission = '/account-management/customer-commission', // 客户账号佣金
|
CustomerCommission = '/account-management/customer-commission', // 客户账号佣金
|
||||||
|
|
||||||
// 产品管理
|
// 产品管理
|
||||||
SimCardManagement = '/product/sim-card', // 网卡产品管理
|
SimCardManagement = '/product/sim-card', // 网卡产品管理
|
||||||
SimCardAssign = '/product/sim-card-assign', // 号卡分配
|
SimCardAssign = '/product/sim-card-assign', // 号卡分配
|
||||||
|
ShopAccounts = '/common/account-list', // 店铺账号列表(通用)
|
||||||
|
|
||||||
// 资产管理
|
// 资产管理
|
||||||
StandaloneCardList = '/asset-management/iot-card-management', // IoT卡管理
|
StandaloneCardList = '/asset-management/iot-card-management', // IoT卡管理
|
||||||
IotCardTask = '/asset-management/iot-card-task', // IoT卡任务
|
IotCardTask = '/asset-management/iot-card-task', // IoT卡任务
|
||||||
DeviceTask = '/asset-management/device-task', // 设备任务
|
DeviceTask = '/asset-management/device-task', // 设备任务
|
||||||
|
TaskDetail = '/asset-management/task-detail', // 任务详情(IoT卡/设备任务详情)
|
||||||
DeviceList = '/asset-management/device-list', // 设备列表
|
DeviceList = '/asset-management/device-list', // 设备列表
|
||||||
DeviceDetail = '/asset-management/device-detail', // 设备详情
|
DeviceDetail = '/asset-management/device-detail', // 设备详情
|
||||||
AssetAssign = '/asset-management/asset-assign', // 资产分配(分配记录)
|
AssetAssign = '/asset-management/asset-assign', // 资产分配(分配记录)
|
||||||
AllocationRecordDetail = '/asset-management/allocation-record-detail', // 分配记录详情
|
AssetAssignDetail = '/asset-management/asset-assign/detail', // 资产分配详情
|
||||||
CardReplacementRequest = '/asset-management/card-replacement-request', // 换卡申请
|
CardReplacementRequest = '/asset-management/card-replacement-request', // 换卡申请
|
||||||
AuthorizationRecords = '/asset-management/authorization-records', // 授权记录
|
AuthorizationRecords = '/asset-management/authorization-records', // 授权记录
|
||||||
AuthorizationDetail = '/asset-management/authorization-detail', // 授权记录详情
|
AuthorizationRecordDetail = '/asset-management/authorization-records/detail', // 授权记录详情
|
||||||
EnterpriseDevices = '/asset-management/enterprise-devices', // 企业设备列表
|
EnterpriseDevices = '/asset-management/enterprise-devices', // 企业设备列表
|
||||||
|
ExchangeManagement = '/asset-management/exchange-management', // 换货管理
|
||||||
|
ExchangeDetail = '/asset-management/exchange-management/detail', // 换货单详情
|
||||||
|
|
||||||
// 账户管理
|
// 账户管理
|
||||||
CustomerAccountList = '/finance/customer-account', // 客户账号
|
CustomerAccountList = '/finance/customer-account', // 客户账号
|
||||||
MyAccount = '/finance/my-account', // 我的账户
|
MyAccount = '/finance/my-account', // 我的账户
|
||||||
CarrierManagement = '/finance/carrier-management', // 运营商管理
|
CarrierManagement = '/finance/carrier-management', // 运营商管理
|
||||||
OrderList = '/order-management/order-list', // 订单管理
|
OrderList = '/account/orders', // 订单管理
|
||||||
|
OrderDetail = '/account/orders/detail', // 订单详情
|
||||||
|
AgentRecharge = '/finance/agent-recharge', // 代理充值
|
||||||
|
AgentRechargeDetail = '/finance/agent-recharge/detail', // 代理充值详情
|
||||||
|
|
||||||
// 佣金管理
|
// 佣金管理
|
||||||
WithdrawalApproval = '/finance/commission/withdrawal-approval', // 提现审批
|
WithdrawalApproval = '/finance/commission/withdrawal-approval', // 提现审批
|
||||||
@@ -118,7 +127,9 @@ export enum RoutesAlias {
|
|||||||
// 设置管理
|
// 设置管理
|
||||||
PaymentMerchant = '/settings/payment-merchant', // 支付商户
|
PaymentMerchant = '/settings/payment-merchant', // 支付商户
|
||||||
DeveloperApi = '/settings/developer-api', // 开发者API
|
DeveloperApi = '/settings/developer-api', // 开发者API
|
||||||
CommissionTemplate = '/settings/commission-template' // 分佣模板
|
CommissionTemplate = '/settings/commission-template', // 分佣模板
|
||||||
|
WechatConfig = '/settings/wechat-config', // 微信支付配置
|
||||||
|
WechatConfigDetail = '/settings/wechat-config/detail' // 微信支付配置详情
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主页路由
|
// 主页路由
|
||||||
|
|||||||
@@ -173,7 +173,11 @@ function convertRouteComponent(
|
|||||||
// 基础路由配置
|
// 基础路由配置
|
||||||
const converted: ConvertedRoute = {
|
const converted: ConvertedRoute = {
|
||||||
...routeConfig,
|
...routeConfig,
|
||||||
component: undefined
|
component: undefined,
|
||||||
|
meta: {
|
||||||
|
...routeConfig.meta,
|
||||||
|
dynamic: true // 标记为动态路由,用于退出时清理
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 是否为一级菜单
|
// 是否为一级菜单
|
||||||
@@ -229,7 +233,10 @@ function handleLayoutRoute(
|
|||||||
path: route.path,
|
path: route.path,
|
||||||
name: route.name,
|
name: route.name,
|
||||||
component: loadComponent(component as string, String(route.name)),
|
component: loadComponent(component as string, String(route.name)),
|
||||||
meta: route.meta
|
meta: {
|
||||||
|
...route.meta,
|
||||||
|
dynamic: true // 标记为动态路由,用于退出时清理
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ export interface PlatformAccount {
|
|||||||
phone: string
|
phone: string
|
||||||
user_type: number // 用户类型 (1:超级管理员, 2:平台用户, 3:代理账号, 4:企业账号)
|
user_type: number // 用户类型 (1:超级管理员, 2:平台用户, 3:代理账号, 4:企业账号)
|
||||||
enterprise_id?: number | null // 关联企业ID
|
enterprise_id?: number | null // 关联企业ID
|
||||||
|
enterprise_name?: string // ⭐ 新增:企业名称
|
||||||
shop_id?: number | null // 关联店铺ID
|
shop_id?: number | null // 关联店铺ID
|
||||||
|
shop_name?: string // ⭐ 新增:店铺名称
|
||||||
status: AccountStatus // 状态 (0:禁用, 1:启用)
|
status: AccountStatus // 状态 (0:禁用, 1:启用)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,10 +99,12 @@ export interface CustomerAccount {
|
|||||||
|
|
||||||
// 账号查询参数
|
// 账号查询参数
|
||||||
export interface AccountQueryParams extends PaginationParams {
|
export interface AccountQueryParams extends PaginationParams {
|
||||||
keyword?: string // 关键词(用户名、姓名、手机号)
|
username?: string // 用户名模糊查询
|
||||||
roleId?: string | number
|
phone?: string // 手机号模糊查询
|
||||||
status?: AccountStatus
|
user_type?: number // 用户类型 (1-4)
|
||||||
createTimeRange?: [string, string]
|
status?: AccountStatus // 状态 (0:禁用, 1:启用)
|
||||||
|
shop_id?: number // 按店铺ID筛选
|
||||||
|
enterprise_id?: number // 按企业ID筛选
|
||||||
}
|
}
|
||||||
|
|
||||||
// 代理商查询参数
|
// 代理商查询参数
|
||||||
|
|||||||
65
src/types/api/agentRecharge.ts
Normal file
65
src/types/api/agentRecharge.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* 代理充值相关类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 充值状态
|
||||||
|
export enum AgentRechargeStatus {
|
||||||
|
PENDING = 1, // 待支付
|
||||||
|
COMPLETED = 2, // 已完成
|
||||||
|
CANCELLED = 3 // 已取消
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付方式
|
||||||
|
export type AgentRechargePaymentMethod = 'wechat' | 'offline'
|
||||||
|
|
||||||
|
// 支付通道
|
||||||
|
export type AgentRechargePaymentChannel = 'wechat_direct' | 'fuyou' | 'offline'
|
||||||
|
|
||||||
|
// 代理充值订单
|
||||||
|
export interface AgentRecharge {
|
||||||
|
id: number
|
||||||
|
recharge_no: string
|
||||||
|
agent_wallet_id: number
|
||||||
|
shop_id: number
|
||||||
|
shop_name: string
|
||||||
|
amount: number // 充值金额(单位:分)
|
||||||
|
status: AgentRechargeStatus
|
||||||
|
payment_method: AgentRechargePaymentMethod
|
||||||
|
payment_channel: AgentRechargePaymentChannel
|
||||||
|
payment_config_id: number | null
|
||||||
|
payment_transaction_id: string
|
||||||
|
created_at: string
|
||||||
|
paid_at: string | null
|
||||||
|
completed_at: string | null
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询代理充值订单列表参数
|
||||||
|
export interface AgentRechargeQueryParams {
|
||||||
|
page?: number
|
||||||
|
page_size?: number
|
||||||
|
shop_id?: number
|
||||||
|
status?: AgentRechargeStatus
|
||||||
|
start_date?: string
|
||||||
|
end_date?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 代理充值订单列表响应
|
||||||
|
export interface AgentRechargeListResponse {
|
||||||
|
page: number
|
||||||
|
page_size: number
|
||||||
|
total: number
|
||||||
|
list: AgentRecharge[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建代理充值订单请求
|
||||||
|
export interface CreateAgentRechargeRequest {
|
||||||
|
amount: number // 充值金额(单位:分),范围 10000 ~ 100000000
|
||||||
|
payment_method: AgentRechargePaymentMethod
|
||||||
|
shop_id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认线下充值请求
|
||||||
|
export interface ConfirmOfflinePaymentRequest {
|
||||||
|
operation_password: string
|
||||||
|
}
|
||||||
277
src/types/api/asset.ts
Normal file
277
src/types/api/asset.ts
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
/**
|
||||||
|
* 资产管理相关类型定义
|
||||||
|
* 对应文档:asset-detail-refactor-api-changes.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ========== 资产类型枚举 ==========
|
||||||
|
|
||||||
|
// 资产类型
|
||||||
|
export type AssetType = 'card' | 'device'
|
||||||
|
|
||||||
|
// 网络状态
|
||||||
|
export enum NetworkStatus {
|
||||||
|
OFFLINE = 0, // 停机
|
||||||
|
ONLINE = 1 // 开机
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实名状态
|
||||||
|
export enum RealNameStatus {
|
||||||
|
NOT_VERIFIED = 0, // 未实名
|
||||||
|
VERIFYING = 1, // 实名中
|
||||||
|
VERIFIED = 2 // 已实名
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保护期状态
|
||||||
|
export type DeviceProtectStatus = 'none' | 'stop' | 'start'
|
||||||
|
|
||||||
|
// 套餐使用状态
|
||||||
|
export enum PackageUsageStatus {
|
||||||
|
PENDING = 0, // 待生效
|
||||||
|
ACTIVE = 1, // 生效中
|
||||||
|
USED_UP = 2, // 已用完
|
||||||
|
EXPIRED = 3, // 已过期
|
||||||
|
INVALID = 4 // 已失效
|
||||||
|
}
|
||||||
|
|
||||||
|
// 套餐类型
|
||||||
|
export type PackageType = 'formal' | 'addon'
|
||||||
|
|
||||||
|
// 套餐使用类型
|
||||||
|
export type UsageType = 'single_card' | 'device'
|
||||||
|
|
||||||
|
// ========== 资产详情响应 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资产解析/详情响应
|
||||||
|
* 对应接口:GET /api/admin/assets/resolve/:identifier
|
||||||
|
*/
|
||||||
|
export interface AssetResolveResponse {
|
||||||
|
asset_type: AssetType // 资产类型:card 或 device
|
||||||
|
asset_id: number // 数据库 ID
|
||||||
|
virtual_no: string // 虚拟号
|
||||||
|
status: number // 资产状态
|
||||||
|
batch_no: string // 批次号
|
||||||
|
shop_id: number // 所属店铺 ID
|
||||||
|
shop_name: string // 所属店铺名称
|
||||||
|
series_id: number // 套餐系列 ID
|
||||||
|
series_name: string // 套餐系列名称
|
||||||
|
real_name_status: RealNameStatus // 实名状态:0 未实名 / 1 实名中 / 2 已实名
|
||||||
|
network_status?: NetworkStatus // 网络状态:0 停机 / 1 开机(仅 card)
|
||||||
|
current_package: string // 当前套餐名称(无则空)
|
||||||
|
package_total_mb: number // 当前套餐总虚流量 MB
|
||||||
|
package_used_mb: number // 已用虚流量 MB
|
||||||
|
package_remain_mb: number // 剩余虚流量 MB
|
||||||
|
device_protect_status?: DeviceProtectStatus // 保护期状态:none / stop / start(仅 device)
|
||||||
|
activated_at: string // 激活时间
|
||||||
|
created_at: string // 创建时间
|
||||||
|
updated_at: string // 更新时间
|
||||||
|
accumulated_recharge?: number // 累计充值金额(分)
|
||||||
|
first_commission_paid?: boolean // 一次性佣金是否已发放
|
||||||
|
|
||||||
|
// ===== 卡专属字段 (asset_type === 'card' 时) =====
|
||||||
|
iccid?: string // 卡 ICCID
|
||||||
|
bound_device_id?: number // 绑定设备 ID
|
||||||
|
bound_device_no?: string // 绑定设备虚拟号
|
||||||
|
bound_device_name?: string // 绑定设备名称
|
||||||
|
carrier_id?: number // 运营商ID
|
||||||
|
carrier_type?: string // 运营商类型
|
||||||
|
carrier_name?: string // 运营商名称
|
||||||
|
msisdn?: string // 手机号
|
||||||
|
imsi?: string // IMSI
|
||||||
|
card_category?: string // 卡业务类型
|
||||||
|
supplier?: string // 供应商
|
||||||
|
activation_status?: number // 激活状态
|
||||||
|
enable_polling?: boolean // 是否参与轮询
|
||||||
|
|
||||||
|
// ===== 设备专属字段 (asset_type === 'device' 时) =====
|
||||||
|
bound_card_count?: number // 绑定卡数量
|
||||||
|
cards?: AssetBoundCard[] // 绑定卡列表
|
||||||
|
device_name?: string // 设备名称
|
||||||
|
imei?: string // IMEI
|
||||||
|
sn?: string // 序列号
|
||||||
|
device_model?: string // 设备型号
|
||||||
|
device_type?: string // 设备类型
|
||||||
|
max_sim_slots?: number // 最大插槽数
|
||||||
|
manufacturer?: string // 制造商
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备绑定的卡信息
|
||||||
|
*/
|
||||||
|
export interface AssetBoundCard {
|
||||||
|
card_id: number // 卡 ID
|
||||||
|
iccid: string // ICCID
|
||||||
|
msisdn: string // 手机号
|
||||||
|
network_status: NetworkStatus // 网络状态
|
||||||
|
real_name_status: RealNameStatus // 实名状态
|
||||||
|
slot_position: number // 插槽位置
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 资产实时状态响应 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资产实时状态响应
|
||||||
|
* 对应接口:GET /api/admin/assets/:asset_type/:id/realtime-status
|
||||||
|
*/
|
||||||
|
export interface AssetRealtimeStatusResponse {
|
||||||
|
asset_type: AssetType // 资产类型
|
||||||
|
asset_id: number // 资产 ID
|
||||||
|
|
||||||
|
// ===== 卡专属字段 =====
|
||||||
|
network_status?: NetworkStatus // 网络状态(仅 card)
|
||||||
|
real_name_status?: RealNameStatus // 实名状态(仅 card)
|
||||||
|
current_month_usage_mb?: number // 本月已用流量 MB(仅 card)
|
||||||
|
last_sync_time?: string // 最后同步时间(仅 card)
|
||||||
|
|
||||||
|
// ===== 设备专属字段 =====
|
||||||
|
device_protect_status?: DeviceProtectStatus // 保护期(仅 device)
|
||||||
|
cards?: AssetBoundCard[] // 所有绑定卡的状态(仅 device)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资产刷新响应(结构与 realtime-status 完全相同)
|
||||||
|
* 对应接口:POST /api/admin/assets/:asset_type/:id/refresh
|
||||||
|
*/
|
||||||
|
export type AssetRefreshResponse = AssetRealtimeStatusResponse
|
||||||
|
|
||||||
|
// ========== 资产套餐查询响应 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资产套餐使用记录
|
||||||
|
* 对应接口:GET /api/admin/assets/:asset_type/:id/packages
|
||||||
|
*/
|
||||||
|
export interface AssetPackageUsageRecord {
|
||||||
|
package_usage_id?: number // 套餐使用记录 ID
|
||||||
|
package_id?: number // 套餐 ID
|
||||||
|
package_name?: string // 套餐名称
|
||||||
|
package_type?: PackageType // formal(正式套餐)/ addon(加油包)
|
||||||
|
usage_type?: UsageType // 使用类型:single_card/device
|
||||||
|
status?: PackageUsageStatus // 0 待生效 / 1 生效中 / 2 已用完 / 3 已过期 / 4 已失效
|
||||||
|
status_name?: string // 状态中文名
|
||||||
|
data_limit_mb?: number // 真流量总量 MB
|
||||||
|
virtual_limit_mb?: number // 虚流量总量 MB(已按 virtual_ratio 换算)
|
||||||
|
data_usage_mb?: number // 已用真流量 MB
|
||||||
|
virtual_used_mb?: number // 已用虚流量 MB
|
||||||
|
virtual_remain_mb?: number // 剩余虚流量 MB
|
||||||
|
virtual_ratio?: number // 虚流量比例(real/virtual)
|
||||||
|
activated_at?: string // 激活时间
|
||||||
|
expires_at?: string // 到期时间
|
||||||
|
master_usage_id?: number | null // 主套餐 ID(加油包时有值)
|
||||||
|
priority?: number // 优先级
|
||||||
|
created_at?: string // 创建时间
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前套餐响应(结构同套餐使用记录单项)
|
||||||
|
* 对应接口:GET /api/admin/assets/:asset_type/:id/current-package
|
||||||
|
*/
|
||||||
|
export type AssetCurrentPackageResponse = AssetPackageUsageRecord
|
||||||
|
|
||||||
|
// ========== 设备停复机响应 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备批量停机响应
|
||||||
|
* 对应接口:POST /api/admin/assets/device/:device_id/stop
|
||||||
|
*/
|
||||||
|
export interface DeviceStopResponse {
|
||||||
|
message: string // 操作结果描述
|
||||||
|
success_count: number // 成功停机的卡数量
|
||||||
|
failed_cards: DeviceStopFailedCard[] // 停机失败列表
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停机失败的卡信息
|
||||||
|
*/
|
||||||
|
export interface DeviceStopFailedCard {
|
||||||
|
iccid: string // ICCID
|
||||||
|
reason: string // 失败原因
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备批量复机响应(HTTP 200 即成功,无 body)
|
||||||
|
* 对应接口:POST /api/admin/assets/device/:device_id/start
|
||||||
|
*/
|
||||||
|
export type DeviceStartResponse = void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单卡停机响应(HTTP 200 即成功,无 body)
|
||||||
|
* 对应接口:POST /api/admin/assets/card/:iccid/stop
|
||||||
|
*/
|
||||||
|
export type CardStopResponse = void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单卡复机响应(HTTP 200 即成功,无 body)
|
||||||
|
* 对应接口:POST /api/admin/assets/card/:iccid/start
|
||||||
|
*/
|
||||||
|
export type CardStartResponse = void
|
||||||
|
|
||||||
|
// ========== 钱包流水响应 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 交易类型
|
||||||
|
*/
|
||||||
|
export type TransactionType = 'recharge' | 'deduct' | 'refund'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关联业务类型
|
||||||
|
*/
|
||||||
|
export type ReferenceType = 'recharge' | 'order'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 钱包流水单项
|
||||||
|
*/
|
||||||
|
export interface AssetWalletTransactionItem {
|
||||||
|
id?: number // 流水记录ID
|
||||||
|
transaction_type?: TransactionType // 交易类型:recharge/deduct/refund
|
||||||
|
transaction_type_text?: string // 交易类型文本:充值/扣款/退款
|
||||||
|
amount?: number // 变动金额(分),充值为正数,扣款/退款为负数
|
||||||
|
balance_before?: number // 变动前余额(分)
|
||||||
|
balance_after?: number // 变动后余额(分)
|
||||||
|
reference_type?: ReferenceType | null // 关联业务类型:recharge 或 order(可空)
|
||||||
|
reference_no?: string | null // 关联业务编号:充值单号(CRCH…)或订单号(ORD…)(可空)
|
||||||
|
remark?: string | null // 备注(可空)
|
||||||
|
created_at?: string // 流水创建时间(RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 钱包流水列表响应
|
||||||
|
* 对应接口:GET /api/admin/assets/:asset_type/:id/wallet/transactions
|
||||||
|
*/
|
||||||
|
export interface AssetWalletTransactionListResponse {
|
||||||
|
list?: AssetWalletTransactionItem[] | null // 流水列表
|
||||||
|
page?: number // 当前页码
|
||||||
|
page_size?: number // 每页数量
|
||||||
|
total?: number // 总记录数
|
||||||
|
total_pages?: number // 总页数
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 钱包流水查询参数
|
||||||
|
*/
|
||||||
|
export interface AssetWalletTransactionParams {
|
||||||
|
page?: number // 页码,默认1
|
||||||
|
page_size?: number // 每页数量,默认20,最大100
|
||||||
|
transaction_type?: TransactionType | null // 交易类型过滤:recharge/deduct/refund
|
||||||
|
start_time?: string | null // 开始时间(RFC3339)
|
||||||
|
end_time?: string | null // 结束时间(RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 钱包概况响应 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资产钱包概况响应
|
||||||
|
* 对应接口:GET /api/admin/assets/:asset_type/:id/wallet
|
||||||
|
*/
|
||||||
|
export interface AssetWalletResponse {
|
||||||
|
wallet_id?: number // 钱包数据库ID
|
||||||
|
resource_id?: number // 对应卡或设备的数据库ID
|
||||||
|
resource_type?: string // 资源类型:iot_card 或 device
|
||||||
|
balance?: number // 总余额(分)
|
||||||
|
frozen_balance?: number // 冻结余额(分)
|
||||||
|
available_balance?: number // 可用余额 = balance - frozen_balance(分)
|
||||||
|
currency?: string // 币种,目前固定 CNY
|
||||||
|
status?: number // 钱包状态:1-正常 2-冻结 3-关闭
|
||||||
|
status_text?: string // 状态文本
|
||||||
|
created_at?: string // 创建时间(RFC3339)
|
||||||
|
updated_at?: string // 更新时间(RFC3339)
|
||||||
|
}
|
||||||
@@ -312,12 +312,14 @@ export interface StandaloneCardQueryParams extends PaginationParams {
|
|||||||
shop_id?: number // 分销商ID
|
shop_id?: number // 分销商ID
|
||||||
iccid?: string // ICCID(模糊查询)
|
iccid?: string // ICCID(模糊查询)
|
||||||
msisdn?: string // 卡接入号(模糊查询)
|
msisdn?: string // 卡接入号(模糊查询)
|
||||||
|
virtual_no?: string // 虚拟号(模糊查询)
|
||||||
batch_no?: string // 批次号
|
batch_no?: string // 批次号
|
||||||
package_id?: number // 套餐ID
|
package_id?: number // 套餐ID
|
||||||
is_distributed?: boolean // 是否已分销
|
is_distributed?: boolean // 是否已分销
|
||||||
is_replaced?: boolean // 是否有换卡记录
|
is_replaced?: boolean // 是否有换卡记录
|
||||||
iccid_start?: string // ICCID起始号
|
iccid_start?: string // ICCID起始号
|
||||||
iccid_end?: string // ICCID结束号
|
iccid_end?: string // ICCID结束号
|
||||||
|
series_id?: number // 套餐系列ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// 单卡信息
|
// 单卡信息
|
||||||
@@ -326,6 +328,7 @@ export interface StandaloneIotCard {
|
|||||||
iccid: string // ICCID
|
iccid: string // ICCID
|
||||||
imsi?: string // IMSI (可选)
|
imsi?: string // IMSI (可选)
|
||||||
msisdn?: string // 卡接入号 (可选)
|
msisdn?: string // 卡接入号 (可选)
|
||||||
|
virtual_no?: string // 虚拟号(可空)
|
||||||
carrier_id: number // 运营商ID
|
carrier_id: number // 运营商ID
|
||||||
carrier_type: string // 运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)
|
carrier_type: string // 运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)
|
||||||
carrier_name: string // 运营商名称
|
carrier_name: string // 运营商名称
|
||||||
@@ -347,6 +350,8 @@ export interface StandaloneIotCard {
|
|||||||
activated_at?: string | null // 激活时间 (可选)
|
activated_at?: string | null // 激活时间 (可选)
|
||||||
created_at: string // 创建时间
|
created_at: string // 创建时间
|
||||||
updated_at: string // 更新时间
|
updated_at: string // 更新时间
|
||||||
|
series_id?: number | null // 套餐系列ID
|
||||||
|
series_name?: string // 套餐系列名称
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 单卡批量分配和回收相关 ==========
|
// ========== 单卡批量分配和回收相关 ==========
|
||||||
@@ -462,6 +467,7 @@ export interface IotCardDetailResponse {
|
|||||||
iccid: string // ICCID
|
iccid: string // ICCID
|
||||||
imsi: string // IMSI
|
imsi: string // IMSI
|
||||||
msisdn: string // 卡接入号
|
msisdn: string // 卡接入号
|
||||||
|
virtual_no?: string // 虚拟号(可空)
|
||||||
carrier_id: number // 运营商ID
|
carrier_id: number // 运营商ID
|
||||||
carrier_name: string // 运营商名称
|
carrier_name: string // 运营商名称
|
||||||
carrier_type: string // 运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)
|
carrier_type: string // 运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)
|
||||||
@@ -488,7 +494,7 @@ export interface IotCardDetailResponse {
|
|||||||
// 批量设置卡的套餐系列绑定请求参数
|
// 批量设置卡的套餐系列绑定请求参数
|
||||||
export interface BatchSetCardSeriesBindingRequest {
|
export interface BatchSetCardSeriesBindingRequest {
|
||||||
iccids: string[] // ICCID列表(最多500个)
|
iccids: string[] // ICCID列表(最多500个)
|
||||||
series_allocation_id: number // 套餐系列分配ID(0表示清除关联)
|
series_id: number // 套餐系列ID(0表示清除关联)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 卡套餐系列绑定失败项
|
// 卡套餐系列绑定失败项
|
||||||
@@ -503,3 +509,31 @@ export interface BatchSetCardSeriesBindingResponse {
|
|||||||
fail_count: number // 失败数量
|
fail_count: number // 失败数量
|
||||||
failed_items: CardSeriesBindingFailedItem[] | null // 失败详情列表
|
failed_items: CardSeriesBindingFailedItem[] | null // 失败详情列表
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== IoT卡网关操作相关 ==========
|
||||||
|
|
||||||
|
// 查询流量使用响应
|
||||||
|
export interface GatewayFlowUsageResponse {
|
||||||
|
extend?: string // 扩展字段(广电国网特殊参数)
|
||||||
|
unit?: string // 流量单位(MB)
|
||||||
|
usedFlow?: number // 已用流量
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询实名认证状态响应
|
||||||
|
export interface GatewayRealnameStatusResponse {
|
||||||
|
extend?: string // 扩展字段(广电国网特殊参数)
|
||||||
|
status?: string // 实名认证状态
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询卡实时状态响应
|
||||||
|
export interface GatewayCardStatusResponse {
|
||||||
|
cardStatus?: string // 卡状态(准备、正常、停机)
|
||||||
|
extend?: string // 扩展字段(广电国网特殊参数)
|
||||||
|
iccid?: string // ICCID
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取实名认证链接响应
|
||||||
|
export interface GatewayRealnameLinkResponse {
|
||||||
|
extend?: string // 扩展字段(广电国网特殊参数)
|
||||||
|
link?: string // 实名认证跳转链接(HTTPS URL)
|
||||||
|
}
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ export interface ShopCommissionRecordItem {
|
|||||||
order_no?: string // 订单号
|
order_no?: string // 订单号
|
||||||
order_created_at?: string // 订单创建时间
|
order_created_at?: string // 订单创建时间
|
||||||
iccid?: string // ICCID
|
iccid?: string // ICCID
|
||||||
device_no?: string // 设备号
|
virtual_no?: string // 虚拟号(原 device_no)
|
||||||
created_at: string // 佣金入账时间
|
created_at: string // 佣金入账时间
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,19 +22,10 @@ export interface PaginationParams {
|
|||||||
|
|
||||||
// 分页响应数据
|
// 分页响应数据
|
||||||
export interface PaginationData<T> {
|
export interface PaginationData<T> {
|
||||||
items: T[] // 后端实际返回的是 items,不是 records
|
items: T[] // 数据列表
|
||||||
total: number
|
total: number // 总数
|
||||||
size: number
|
page_size: number // 每页数量
|
||||||
page: number // 后端返回的是 page,不是 current
|
page: number // 当前页
|
||||||
pages?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新版分页响应数据(使用 list 字段)
|
|
||||||
export interface PaginationDataV2<T> {
|
|
||||||
list: T[] // 新版API使用 list 字段
|
|
||||||
total: number
|
|
||||||
page_size: number
|
|
||||||
page: number
|
|
||||||
total_pages: number // 总页数
|
total_pages: number // 总页数
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,11 +34,6 @@ export interface PaginationResponse<T = any> extends BaseResponse {
|
|||||||
data: PaginationData<T>
|
data: PaginationData<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新版分页响应(使用 list 字段)
|
|
||||||
export interface PaginationResponseV2<T = any> extends BaseResponse {
|
|
||||||
data: PaginationDataV2<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
// 列表响应
|
// 列表响应
|
||||||
export interface ListResponse<T = any> extends BaseResponse {
|
export interface ListResponse<T = any> extends BaseResponse {
|
||||||
data: T[]
|
data: T[]
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export enum DeviceStatus {
|
|||||||
// 设备信息
|
// 设备信息
|
||||||
export interface Device {
|
export interface Device {
|
||||||
id: number // 设备ID
|
id: number // 设备ID
|
||||||
device_no: string // 设备号
|
virtual_no: string // 虚拟号(原 device_no)
|
||||||
device_name: string // 设备名称
|
device_name: string // 设备名称
|
||||||
device_model: string // 设备型号
|
device_model: string // 设备型号
|
||||||
device_type: string // 设备类型
|
device_type: string // 设备类型
|
||||||
@@ -34,11 +34,12 @@ export interface Device {
|
|||||||
activated_at: string | null // 激活时间
|
activated_at: string | null // 激活时间
|
||||||
created_at: string // 创建时间
|
created_at: string // 创建时间
|
||||||
updated_at: string // 更新时间
|
updated_at: string // 更新时间
|
||||||
|
series_id?: number | null // 套餐系列ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设备查询参数
|
// 设备查询参数
|
||||||
export interface DeviceQueryParams extends PaginationParams {
|
export interface DeviceQueryParams extends PaginationParams {
|
||||||
device_no?: string // 设备号(模糊查询)
|
virtual_no?: string // 虚拟号(模糊查询,原 device_no)
|
||||||
device_name?: string // 设备名称(模糊查询)
|
device_name?: string // 设备名称(模糊查询)
|
||||||
status?: DeviceStatus // 状态
|
status?: DeviceStatus // 状态
|
||||||
shop_id?: number | null // 店铺ID (NULL表示平台库存)
|
shop_id?: number | null // 店铺ID (NULL表示平台库存)
|
||||||
@@ -47,6 +48,7 @@ export interface DeviceQueryParams extends PaginationParams {
|
|||||||
manufacturer?: string // 制造商(模糊查询)
|
manufacturer?: string // 制造商(模糊查询)
|
||||||
created_at_start?: string // 创建时间起始
|
created_at_start?: string // 创建时间起始
|
||||||
created_at_end?: string // 创建时间结束
|
created_at_end?: string // 创建时间结束
|
||||||
|
series_id?: number // 套餐系列ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设备列表响应
|
// 设备列表响应
|
||||||
@@ -105,7 +107,7 @@ export interface AllocateDevicesRequest {
|
|||||||
// 分配失败项
|
// 分配失败项
|
||||||
export interface AllocationDeviceFailedItem {
|
export interface AllocationDeviceFailedItem {
|
||||||
device_id: number // 设备ID
|
device_id: number // 设备ID
|
||||||
device_no: string // 设备号
|
virtual_no: string // 虚拟号(原 device_no)
|
||||||
reason: string // 失败原因
|
reason: string // 失败原因
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +194,7 @@ export interface DeviceImportTaskListResponse {
|
|||||||
// 导入结果详细项
|
// 导入结果详细项
|
||||||
export interface DeviceImportResultItem {
|
export interface DeviceImportResultItem {
|
||||||
line: number // 行号
|
line: number // 行号
|
||||||
device_no: string // 设备号
|
virtual_no: string // 虚拟号(原 device_no)
|
||||||
reason: string // 原因
|
reason: string // 原因
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,13 +209,13 @@ export interface DeviceImportTaskDetail extends DeviceImportTask {
|
|||||||
// 批量设置设备的套餐系列绑定请求参数
|
// 批量设置设备的套餐系列绑定请求参数
|
||||||
export interface BatchSetDeviceSeriesBindingRequest {
|
export interface BatchSetDeviceSeriesBindingRequest {
|
||||||
device_ids: number[] // 设备ID列表(最多500个)
|
device_ids: number[] // 设备ID列表(最多500个)
|
||||||
series_allocation_id: number // 套餐系列分配ID(0表示清除关联)
|
series_id: number // 套餐系列ID(0表示清除关联)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设备套餐系列绑定失败项
|
// 设备套餐系列绑定失败项
|
||||||
export interface DeviceSeriesBindingFailedItem {
|
export interface DeviceSeriesBindingFailedItem {
|
||||||
device_id: number // 设备ID
|
device_id: number // 设备ID
|
||||||
device_no: string // 设备号
|
virtual_no: string // 虚拟号(原 device_no)
|
||||||
reason: string // 失败原因
|
reason: string // 失败原因
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,3 +225,28 @@ export interface BatchSetDeviceSeriesBindingResponse {
|
|||||||
fail_count: number // 失败数量
|
fail_count: number // 失败数量
|
||||||
failed_items: DeviceSeriesBindingFailedItem[] | null // 失败详情列表
|
failed_items: DeviceSeriesBindingFailedItem[] | null // 失败详情列表
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 设备操作相关 ==========
|
||||||
|
|
||||||
|
// 设置限速请求参数
|
||||||
|
export interface SetSpeedLimitRequest {
|
||||||
|
download_speed: number // 下行速率(KB/s)
|
||||||
|
upload_speed: number // 上行速率(KB/s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换SIM卡请求参数
|
||||||
|
export interface SwitchCardRequest {
|
||||||
|
target_iccid: string // 目标卡 ICCID
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置WiFi请求参数
|
||||||
|
export interface SetWiFiRequest {
|
||||||
|
enabled: number // 启用状态(0:禁用, 1:启用)
|
||||||
|
ssid: string // WiFi 名称(1-32字符)
|
||||||
|
password: string // WiFi 密码(8-63字符)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 空响应(设备操作成功)
|
||||||
|
export interface DeviceOperationResponse {
|
||||||
|
message?: string // 提示信息
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ export interface FailedItem {
|
|||||||
export interface AllocatedDevice {
|
export interface AllocatedDevice {
|
||||||
/** 设备ID */
|
/** 设备ID */
|
||||||
device_id: number
|
device_id: number
|
||||||
/** 设备号 */
|
/** 虚拟号(原 device_no) */
|
||||||
device_no: string
|
virtual_no: string
|
||||||
/** 卡数量 */
|
/** 卡数量 */
|
||||||
card_count: number
|
card_count: number
|
||||||
/** 卡ICCID列表 */
|
/** 卡ICCID列表 */
|
||||||
@@ -92,8 +92,8 @@ export interface DeviceBundleCard {
|
|||||||
export interface DeviceBundle {
|
export interface DeviceBundle {
|
||||||
/** 设备ID */
|
/** 设备ID */
|
||||||
device_id: number
|
device_id: number
|
||||||
/** 设备号 */
|
/** 虚拟号(原 device_no) */
|
||||||
device_no: string
|
virtual_no: string
|
||||||
/** 触发卡(用户选择的卡) */
|
/** 触发卡(用户选择的卡) */
|
||||||
trigger_card: DeviceBundleCard
|
trigger_card: DeviceBundleCard
|
||||||
/** 连带卡(同设备的其他卡) */
|
/** 连带卡(同设备的其他卡) */
|
||||||
@@ -158,8 +158,8 @@ export interface EnterpriseCardItem {
|
|||||||
package_name?: string
|
package_name?: string
|
||||||
/** 设备ID */
|
/** 设备ID */
|
||||||
device_id?: number | null
|
device_id?: number | null
|
||||||
/** 设备号 */
|
/** 虚拟号(原 device_no) */
|
||||||
device_no?: string
|
virtual_no?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -176,8 +176,8 @@ export interface EnterpriseCardListParams {
|
|||||||
carrier_id?: number
|
carrier_id?: number
|
||||||
/** ICCID(模糊查询) */
|
/** ICCID(模糊查询) */
|
||||||
iccid?: string
|
iccid?: string
|
||||||
/** 设备号(模糊查询) */
|
/** 虚拟号(模糊查询,原 device_no) */
|
||||||
device_no?: string
|
virtual_no?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -200,8 +200,8 @@ export interface EnterpriseCardPageResult {
|
|||||||
export interface RecalledDevice {
|
export interface RecalledDevice {
|
||||||
/** 设备ID */
|
/** 设备ID */
|
||||||
device_id: number
|
device_id: number
|
||||||
/** 设备号 */
|
/** 虚拟号(原 device_no) */
|
||||||
device_no: string
|
virtual_no: string
|
||||||
/** 卡数量 */
|
/** 卡数量 */
|
||||||
card_count: number
|
card_count: number
|
||||||
/** 卡ICCID列表 */
|
/** 卡ICCID列表 */
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { PaginationParams } from '@/types'
|
|||||||
*/
|
*/
|
||||||
export interface EnterpriseDeviceItem {
|
export interface EnterpriseDeviceItem {
|
||||||
device_id: number // 设备ID
|
device_id: number // 设备ID
|
||||||
device_no: string // 设备号
|
virtual_no: string // 虚拟号(原 device_no)
|
||||||
device_name: string // 设备名称
|
device_name: string // 设备名称
|
||||||
device_model: string // 设备型号
|
device_model: string // 设备型号
|
||||||
card_count: number // 绑定卡数量
|
card_count: number // 绑定卡数量
|
||||||
@@ -22,7 +22,7 @@ export interface EnterpriseDeviceItem {
|
|||||||
* 企业设备列表查询参数
|
* 企业设备列表查询参数
|
||||||
*/
|
*/
|
||||||
export interface EnterpriseDeviceListParams extends PaginationParams {
|
export interface EnterpriseDeviceListParams extends PaginationParams {
|
||||||
device_no?: string // 设备号(模糊搜索)
|
virtual_no?: string // 虚拟号(模糊搜索,原 device_no)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,7 +39,7 @@ export interface EnterpriseDevicePageResult {
|
|||||||
* 授权设备请求参数
|
* 授权设备请求参数
|
||||||
*/
|
*/
|
||||||
export interface AllocateDevicesRequest {
|
export interface AllocateDevicesRequest {
|
||||||
device_nos: string[] | null // 设备号列表(最多100个)
|
virtual_nos: string[] | null // 虚拟号列表(最多100个,原 device_nos)
|
||||||
remark?: string // 授权备注
|
remark?: string // 授权备注
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ export interface AllocateDevicesRequest {
|
|||||||
*/
|
*/
|
||||||
export interface AuthorizedDeviceItem {
|
export interface AuthorizedDeviceItem {
|
||||||
device_id: number // 设备ID
|
device_id: number // 设备ID
|
||||||
device_no: string // 设备号
|
virtual_no: string // 虚拟号(原 device_no)
|
||||||
card_count: number // 绑定卡数量
|
card_count: number // 绑定卡数量
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ export interface AuthorizedDeviceItem {
|
|||||||
* 失败设备项
|
* 失败设备项
|
||||||
*/
|
*/
|
||||||
export interface FailedDeviceItem {
|
export interface FailedDeviceItem {
|
||||||
device_no: string // 设备号
|
virtual_no: string // 虚拟号(原 device_no)
|
||||||
reason: string // 失败原因
|
reason: string // 失败原因
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ export interface AllocateDevicesResponse {
|
|||||||
* 撤销设备授权请求参数
|
* 撤销设备授权请求参数
|
||||||
*/
|
*/
|
||||||
export interface RecallDevicesRequest {
|
export interface RecallDevicesRequest {
|
||||||
device_nos: string[] | null // 设备号列表(最多100个)
|
virtual_nos: string[] | null // 虚拟号列表(最多100个,原 device_nos)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -76,3 +76,12 @@ export * from './carrier'
|
|||||||
|
|
||||||
// 订单相关
|
// 订单相关
|
||||||
export * from './order'
|
export * from './order'
|
||||||
|
|
||||||
|
// 资产管理相关
|
||||||
|
export * from './asset'
|
||||||
|
|
||||||
|
// 代理充值相关
|
||||||
|
export * from './agentRecharge'
|
||||||
|
|
||||||
|
// 微信支付配置相关
|
||||||
|
export * from './wechatConfig'
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export type OrderType = 'single_card' | 'device'
|
|||||||
export type BuyerType = 'personal' | 'agent'
|
export type BuyerType = 'personal' | 'agent'
|
||||||
|
|
||||||
// 订单支付方式
|
// 订单支付方式
|
||||||
export type OrderPaymentMethod = 'wallet' | 'wechat' | 'alipay'
|
export type OrderPaymentMethod = 'wallet' | 'wechat' | 'alipay' | 'offline'
|
||||||
|
|
||||||
// 订单佣金状态
|
// 订单佣金状态
|
||||||
export enum OrderCommissionStatus {
|
export enum OrderCommissionStatus {
|
||||||
@@ -84,7 +84,25 @@ export interface CreateOrderRequest {
|
|||||||
package_ids: number[]
|
package_ids: number[]
|
||||||
iot_card_id?: number | null
|
iot_card_id?: number | null
|
||||||
device_id?: number | null
|
device_id?: number | null
|
||||||
|
payment_method: OrderPaymentMethod // 支付方式
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建订单响应 (返回订单详情)
|
// 创建订单响应 (返回订单详情)
|
||||||
export type CreateOrderResponse = Order
|
export type CreateOrderResponse = Order
|
||||||
|
|
||||||
|
// 套餐购买预检请求
|
||||||
|
export interface PurchaseCheckRequest {
|
||||||
|
order_type: OrderType // 订单类型 (single_card:单卡购买, device:设备购买)
|
||||||
|
package_ids: number[] // 套餐ID列表
|
||||||
|
resource_id: number // 资源ID (IoT卡ID或设备ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 套餐购买预检响应
|
||||||
|
export interface PurchaseCheckResponse {
|
||||||
|
need_force_recharge: boolean // 是否需要强充
|
||||||
|
force_recharge_amount?: number // 强充金额(分)
|
||||||
|
total_package_amount?: number // 套餐总价(分)
|
||||||
|
actual_payment?: number // 实际支付金额(分)
|
||||||
|
wallet_credit?: number // 钱包到账金额(分)
|
||||||
|
message?: string // 提示信息
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,34 @@ import { PaginationParams } from './common'
|
|||||||
|
|
||||||
// ==================== 套餐系列管理 ====================
|
// ==================== 套餐系列管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一次性佣金梯度档位配置(套餐系列级别)
|
||||||
|
*/
|
||||||
|
export interface OneTimeCommissionTier {
|
||||||
|
threshold: number // 达标阈值
|
||||||
|
dimension: 'sales_count' | 'sales_amount' // 统计维度:销量或销售额
|
||||||
|
amount: number // 佣金金额(分)
|
||||||
|
stat_scope?: 'self' | 'self_and_sub' // 统计范围:仅自己或自己+下级
|
||||||
|
operator?: '>=' | '>' | '<=' | '<' // 阈值比较运算符,空值时计算引擎默认 >=
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 套餐系列一次性佣金配置
|
||||||
|
*/
|
||||||
|
export interface SeriesOneTimeCommissionConfig {
|
||||||
|
enable?: boolean // 是否启用一次性佣金
|
||||||
|
commission_type?: 'fixed' | 'tiered' // 佣金类型:固定或梯度
|
||||||
|
commission_amount?: number // 固定佣金金额(分),commission_type=fixed时使用
|
||||||
|
threshold?: number // 触发阈值(分)
|
||||||
|
trigger_type?: 'first_recharge' | 'accumulated_recharge' // 触发类型:首充或累计充值
|
||||||
|
tiers?: OneTimeCommissionTier[] | null // 梯度配置列表,commission_type=tiered时使用
|
||||||
|
enable_force_recharge?: boolean // 是否启用强充
|
||||||
|
force_amount?: number // 强充金额(分)
|
||||||
|
force_calc_type?: 'fixed' | 'dynamic' // 强充计算类型:固定或动态
|
||||||
|
validity_type?: 'permanent' | 'fixed_date' | 'relative' // 时效类型:永久、固定日期或相对时长
|
||||||
|
validity_value?: string // 时效值(日期或月数)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 套餐系列响应
|
* 套餐系列响应
|
||||||
*/
|
*/
|
||||||
@@ -15,6 +43,8 @@ export interface PackageSeriesResponse {
|
|||||||
series_code: string
|
series_code: string
|
||||||
series_name: string
|
series_name: string
|
||||||
description?: string
|
description?: string
|
||||||
|
enable_one_time_commission: boolean // 是否启用一次性佣金
|
||||||
|
one_time_commission_config?: SeriesOneTimeCommissionConfig // 一次性佣金配置
|
||||||
status: number // 1:启用, 2:禁用
|
status: number // 1:启用, 2:禁用
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
@@ -26,6 +56,7 @@ export interface PackageSeriesResponse {
|
|||||||
export interface PackageSeriesQueryParams extends PaginationParams {
|
export interface PackageSeriesQueryParams extends PaginationParams {
|
||||||
series_name?: string // 系列名称(模糊搜索)
|
series_name?: string // 系列名称(模糊搜索)
|
||||||
status?: number // 状态筛选
|
status?: number // 状态筛选
|
||||||
|
enable_one_time_commission?: boolean // 是否启用一次性佣金筛选
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,15 +66,16 @@ export interface CreatePackageSeriesRequest {
|
|||||||
series_code: string // 系列编码,必填
|
series_code: string // 系列编码,必填
|
||||||
series_name: string // 系列名称,必填
|
series_name: string // 系列名称,必填
|
||||||
description?: string // 描述,可选
|
description?: string // 描述,可选
|
||||||
|
one_time_commission_config?: SeriesOneTimeCommissionConfig // 一次性佣金配置,可选
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新套餐系列请求
|
* 更新套餐系列请求
|
||||||
*/
|
*/
|
||||||
export interface UpdatePackageSeriesRequest {
|
export interface UpdatePackageSeriesRequest {
|
||||||
series_code?: string
|
|
||||||
series_name?: string
|
series_name?: string
|
||||||
description?: string
|
description?: string
|
||||||
|
one_time_commission_config?: SeriesOneTimeCommissionConfig // 一次性佣金配置,可选
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,6 +87,15 @@ export interface UpdatePackageSeriesStatusRequest {
|
|||||||
|
|
||||||
// ==================== 套餐管理 ====================
|
// ==================== 套餐管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返佣档位信息
|
||||||
|
*/
|
||||||
|
export interface CommissionTierInfo {
|
||||||
|
current_rate?: string // 当前返佣比例
|
||||||
|
next_rate?: string // 下一档位返佣比例
|
||||||
|
next_threshold?: number | null // 下一档位阈值
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 套餐响应
|
* 套餐响应
|
||||||
*/
|
*/
|
||||||
@@ -62,19 +103,29 @@ export interface PackageResponse {
|
|||||||
id: number
|
id: number
|
||||||
package_code: string
|
package_code: string
|
||||||
package_name: string
|
package_name: string
|
||||||
series_id: number
|
series_id: number | null
|
||||||
series_name?: string
|
series_name?: string | null
|
||||||
package_type: string // 'formal':正式套餐, 'addon':加油包
|
package_type: string // 'formal':正式套餐, 'addon':附加套餐
|
||||||
data_type: string // 'real':真实流量, 'virtual':虚拟流量
|
calendar_type?: string // 套餐周期类型 (natural_month:自然月, by_day:按天)
|
||||||
real_data_mb: number // 真流量额度(MB)
|
duration_days?: number | null // 套餐天数(calendar_type=by_day时有值)
|
||||||
virtual_data_mb: number // 虚流量额度(MB)
|
duration_months?: number // 套餐时长(月数)
|
||||||
duration_months: number // 有效期(月)
|
data_reset_cycle?: string // 流量重置周期 (daily:每日, monthly:每月, yearly:每年, none:不重置)
|
||||||
price: number // 价格(分)
|
real_data_mb?: number // 真流量额度(MB)
|
||||||
shelf_status: number // 上架状态 (1:上架, 2:下架)
|
virtual_data_mb?: number // 虚流量额度(MB)
|
||||||
status: number // 状态 (1:启用, 2:禁用)
|
virtual_ratio?: number // 虚流量比例(real_data_mb / virtual_data_mb)。启用虚流量时计算,否则为 1.0
|
||||||
|
enable_virtual_data?: boolean // 是否启用虚流量
|
||||||
|
enable_realname_activation?: boolean // 是否启用实名激活 (true:需实名后激活, false:立即激活)
|
||||||
|
cost_price?: number // 成本价(分)
|
||||||
|
suggested_retail_price?: number // 建议零售价(分)
|
||||||
|
current_commission_rate?: string // 当前返佣比例(仅代理用户可见)
|
||||||
|
one_time_commission_amount?: number | null // 一次性佣金金额(分,代理视角)
|
||||||
|
profit_margin?: number | null // 利润空间(分,仅代理用户可见)
|
||||||
|
tier_info?: CommissionTierInfo // 返佣档位信息
|
||||||
|
shelf_status?: number // 上架状态 (1:上架, 2:下架)
|
||||||
|
status?: number // 状态 (1:启用, 2:禁用)
|
||||||
description?: string
|
description?: string
|
||||||
created_at: string
|
created_at?: string
|
||||||
updated_at: string
|
updated_at?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,30 +146,37 @@ export interface PackageQueryParams extends PaginationParams {
|
|||||||
export interface CreatePackageRequest {
|
export interface CreatePackageRequest {
|
||||||
package_code: string // 套餐编码,必填
|
package_code: string // 套餐编码,必填
|
||||||
package_name: string // 套餐名称,必填
|
package_name: string // 套餐名称,必填
|
||||||
series_id: number // 所属系列ID,必填
|
series_id?: number | null // 所属系列ID,可选
|
||||||
package_type: string // 套餐类型,必填
|
package_type: string // 套餐类型,必填 (formal:正式套餐, addon:附加套餐)
|
||||||
data_type: string // 流量类型,必填
|
calendar_type?: string | null // 套餐周期类型 (natural_month:自然月, by_day:按天),可选
|
||||||
real_data_mb: number // 真流量额度(MB),必填
|
duration_days?: number | null // 套餐天数(calendar_type=by_day时必填),可选
|
||||||
virtual_data_mb: number // 虚流量额度(MB),必填
|
duration_months: number // 套餐时长(月数),必填
|
||||||
duration_months: number // 有效期(月),必填
|
data_reset_cycle?: string | null // 流量重置周期 (daily:每日, monthly:每月, yearly:每年, none:不重置),可选
|
||||||
price: number // 价格(分),必填
|
real_data_mb?: number | null // 真流量额度(MB),可选
|
||||||
description?: string // 描述,可选
|
virtual_data_mb?: number | null // 虚流量额度(MB),可选
|
||||||
|
enable_virtual_data?: boolean // 是否启用虚流量,可选
|
||||||
|
enable_realname_activation?: boolean | null // 是否启用实名激活 (true:需实名后激活, false:立即激活),可选
|
||||||
|
cost_price: number // 成本价(分),必填
|
||||||
|
suggested_retail_price?: number | null // 建议零售价(分),可选
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新套餐请求
|
* 更新套餐请求
|
||||||
*/
|
*/
|
||||||
export interface UpdatePackageRequest {
|
export interface UpdatePackageRequest {
|
||||||
package_code?: string
|
package_name?: string | null // 套餐名称,可选
|
||||||
package_name?: string
|
series_id?: number | null // 所属系列ID,可选
|
||||||
series_id?: number
|
package_type?: string | null // 套餐类型,可选 (formal:正式套餐, addon:附加套餐)
|
||||||
package_type?: string
|
calendar_type?: string | null // 套餐周期类型 (natural_month:自然月, by_day:按天),可选
|
||||||
data_type?: string
|
duration_days?: number | null // 套餐天数(calendar_type=by_day时必填),可选
|
||||||
real_data_mb?: number
|
duration_months?: number | null // 套餐时长(月数),可选
|
||||||
virtual_data_mb?: number
|
data_reset_cycle?: string | null // 流量重置周期 (daily:每日, monthly:每月, yearly:每年, none:不重置),可选
|
||||||
duration_months?: number
|
real_data_mb?: number | null // 真流量额度(MB),可选
|
||||||
price?: number
|
virtual_data_mb?: number | null // 虚流量额度(MB),可选
|
||||||
description?: string
|
enable_virtual_data?: boolean | null // 是否启用虚流量,可选
|
||||||
|
enable_realname_activation?: boolean | null // 是否启用实名激活,可选
|
||||||
|
cost_price?: number | null // 成本价(分),可选
|
||||||
|
suggested_retail_price?: number | null // 建议零售价(分),可选
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,159 +202,101 @@ export interface SeriesSelectOption {
|
|||||||
series_code: string
|
series_code: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 单套餐分配 ====================
|
// ==================== 代理系列授权 (新接口) ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单套餐分配响应
|
* 佣金梯度配置
|
||||||
*/
|
*/
|
||||||
export interface ShopPackageAllocationResponse {
|
export interface CommissionTier {
|
||||||
id: number
|
operator?: '>=' | '>' | '<=' | '<' // 阈值比较运算符,空值时计算引擎默认 >=
|
||||||
|
threshold: number // 阈值
|
||||||
|
amount: number // 佣金金额(分)
|
||||||
|
dimension?: 'sales_count' | 'sales_amount' // 统计维度 (sales_count:销量, sales_amount:销售额)
|
||||||
|
stat_scope?: 'self' | 'self_and_sub' // 统计范围 (self:仅自己, self_and_sub:自己+下级)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 套餐信息(用于系列授权)
|
||||||
|
*/
|
||||||
|
export interface GrantPackageInfo {
|
||||||
package_id: number
|
package_id: number
|
||||||
package_code: string
|
package_name?: string
|
||||||
package_name: string
|
package_code?: string
|
||||||
|
cost_price: number // 成本价(分)
|
||||||
|
shelf_status?: number // 上架状态
|
||||||
|
status?: number // 状态
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代理系列授权响应
|
||||||
|
*/
|
||||||
|
export interface ShopSeriesGrantResponse {
|
||||||
|
id: number
|
||||||
shop_id: number
|
shop_id: number
|
||||||
shop_name: string
|
shop_name: string
|
||||||
cost_price: number // 覆盖的成本价(分)
|
|
||||||
calculated_cost_price: number // 原计算成本价(分,供参考)
|
|
||||||
allocation_id?: number // 关联的系列分配ID
|
|
||||||
status: number // 1:启用, 2:禁用
|
|
||||||
created_at: string
|
|
||||||
updated_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 单套餐分配查询参数
|
|
||||||
*/
|
|
||||||
export interface ShopPackageAllocationQueryParams extends PaginationParams {
|
|
||||||
shop_id?: number // 店铺ID筛选
|
|
||||||
package_id?: number // 套餐ID筛选
|
|
||||||
status?: number // 状态筛选
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建单套餐分配请求
|
|
||||||
*/
|
|
||||||
export interface CreateShopPackageAllocationRequest {
|
|
||||||
package_id: number // 套餐ID,必填
|
|
||||||
shop_id: number // 店铺ID,必填
|
|
||||||
cost_price: number // 覆盖的成本价(分),必填
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新单套餐分配请求
|
|
||||||
*/
|
|
||||||
export interface UpdateShopPackageAllocationRequest {
|
|
||||||
cost_price: number // 只允许修改成本价
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新单套餐分配状态请求
|
|
||||||
*/
|
|
||||||
export interface UpdateShopPackageAllocationStatusRequest {
|
|
||||||
status: number // 1:启用, 2:禁用
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 套餐系列分配 ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 基础返佣配置
|
|
||||||
*/
|
|
||||||
export interface BaseCommissionConfig {
|
|
||||||
mode: 'fixed' | 'percent' // 返佣模式:'fixed':固定金额, 'percent':百分比
|
|
||||||
value: number // 返佣值:固定金额(分)或百分比的千分比(如200表示20%)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 梯度档位配置
|
|
||||||
*/
|
|
||||||
export interface TierEntry {
|
|
||||||
threshold: number // 阈值(销量或销售额)
|
|
||||||
mode: 'fixed' | 'percent' // 返佣模式
|
|
||||||
value: number // 返佣值
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 梯度返佣配置
|
|
||||||
*/
|
|
||||||
export interface TierCommissionConfig {
|
|
||||||
period_type: 'monthly' | 'quarterly' | 'yearly' // 周期类型
|
|
||||||
tier_type: 'sales_count' | 'sales_amount' // 梯度类型:销量或销售额
|
|
||||||
tiers: TierEntry[] // 梯度档位数组
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 一次性佣金梯度档位配置
|
|
||||||
*/
|
|
||||||
export interface OneTimeCommissionTierEntry {
|
|
||||||
tier_type: 'sales_count' | 'sales_amount' // 梯度类型:销量或销售额
|
|
||||||
threshold: number // 阈值
|
|
||||||
mode: 'fixed' | 'percent' // 返佣模式
|
|
||||||
value: number // 返佣值
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 一次性佣金配置
|
|
||||||
*/
|
|
||||||
export interface OneTimeCommissionConfig {
|
|
||||||
type: 'fixed' | 'tiered' // 佣金类型:'fixed':固定佣金, 'tiered':梯度佣金
|
|
||||||
trigger: 'single_recharge' | 'accumulated_recharge' // 触发方式:'single_recharge':单笔充值, 'accumulated_recharge':累计充值
|
|
||||||
threshold: number // 最低阈值(分)
|
|
||||||
mode?: 'fixed' | 'percent' // 返佣模式(固定佣金时必填)
|
|
||||||
value?: number // 返佣值(固定佣金时必填)
|
|
||||||
tiers?: OneTimeCommissionTierEntry[] | null // 梯度档位数组(梯度佣金时必填)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 套餐系列分配响应
|
|
||||||
*/
|
|
||||||
export interface ShopSeriesAllocationResponse {
|
|
||||||
id: number
|
|
||||||
series_id: number
|
series_id: number
|
||||||
series_name: string
|
series_name: string
|
||||||
shop_id: number
|
series_code?: string
|
||||||
shop_name: string
|
commission_type: 'fixed' | 'tiered' // 佣金类型:固定或梯度
|
||||||
allocator_shop_id: number // 分配者店铺ID
|
one_time_commission_amount: number // 固定佣金金额(分)
|
||||||
|
commission_tiers: CommissionTier[] // 梯度配置列表
|
||||||
|
force_recharge_locked: boolean // 是否被平台锁定(true时前端只读)
|
||||||
|
force_recharge_enabled: boolean // 是否启用强充
|
||||||
|
force_recharge_amount: number // 强充金额(分)
|
||||||
|
allocator_shop_id: number // 分配者店铺ID,0表示平台
|
||||||
allocator_shop_name: string // 分配者店铺名称
|
allocator_shop_name: string // 分配者店铺名称
|
||||||
base_commission: BaseCommissionConfig // 基础返佣配置
|
package_count?: number // 套餐数量
|
||||||
enable_one_time_commission: boolean // 是否启用一次性佣金
|
packages?: GrantPackageInfo[] // 套餐列表
|
||||||
one_time_commission_config?: OneTimeCommissionConfig // 一次性佣金配置(可选)
|
|
||||||
status: number // 1:启用, 2:禁用
|
status: number // 1:启用, 2:禁用
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 套餐系列分配查询参数
|
* 代理系列授权查询参数
|
||||||
*/
|
*/
|
||||||
export interface ShopSeriesAllocationQueryParams extends PaginationParams {
|
export interface ShopSeriesGrantQueryParams extends PaginationParams {
|
||||||
shop_id?: number // 店铺ID筛选
|
shop_id?: number // 店铺ID筛选
|
||||||
series_id?: number // 系列ID筛选
|
series_id?: number // 系列ID筛选
|
||||||
|
allocator_shop_id?: number // 分配者店铺ID筛选
|
||||||
status?: number // 状态筛选
|
status?: number // 状态筛选
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建套餐系列分配请求
|
* 创建代理系列授权请求
|
||||||
*/
|
*/
|
||||||
export interface CreateShopSeriesAllocationRequest {
|
export interface CreateShopSeriesGrantRequest {
|
||||||
series_id: number // 套餐系列ID,必填
|
|
||||||
shop_id: number // 店铺ID,必填
|
shop_id: number // 店铺ID,必填
|
||||||
base_commission: BaseCommissionConfig // 基础返佣配置,必填
|
series_id: number // 系列ID,必填
|
||||||
enable_one_time_commission?: boolean // 是否启用一次性佣金,可选(默认false)
|
one_time_commission_amount?: number // 固定佣金金额(分),固定模式时必填
|
||||||
one_time_commission_config?: OneTimeCommissionConfig // 一次性佣金配置,当enable_one_time_commission为true时必填
|
commission_tiers?: CommissionTier[] // 梯度配置列表,梯度模式时必填
|
||||||
|
enable_force_recharge?: boolean // 是否启用强充
|
||||||
|
force_recharge_amount?: number // 强充金额(分)
|
||||||
|
packages?: GrantPackageInfo[] // 套餐列表
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新套餐系列分配请求
|
* 更新代理系列授权请求
|
||||||
*/
|
*/
|
||||||
export interface UpdateShopSeriesAllocationRequest {
|
export interface UpdateShopSeriesGrantRequest {
|
||||||
base_commission?: BaseCommissionConfig // 基础返佣配置
|
one_time_commission_amount?: number // 固定佣金金额(分)
|
||||||
enable_one_time_commission?: boolean // 是否启用一次性佣金
|
commission_tiers?: CommissionTier[] // 梯度配置列表
|
||||||
one_time_commission_config?: OneTimeCommissionConfig // 一次性佣金配置
|
enable_force_recharge?: boolean // 是否启用强充(force_recharge_locked=true时忽略)
|
||||||
|
force_recharge_amount?: number // 强充金额(分)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新套餐系列分配状态请求
|
* 管理套餐请求中的套餐项
|
||||||
*/
|
*/
|
||||||
export interface UpdateShopSeriesAllocationStatusRequest {
|
export interface GrantPackageItem {
|
||||||
status: number // 1:启用, 2:禁用
|
package_id?: number // 套餐ID
|
||||||
|
cost_price?: number // 成本价(分)
|
||||||
|
remove?: boolean | null // 是否删除该套餐授权(true=删除)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理套餐请求
|
||||||
|
*/
|
||||||
|
export interface ManageGrantPackagesRequest {
|
||||||
|
packages?: GrantPackageItem[] | null // 套餐操作列表
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export interface PermissionTreeNode {
|
|||||||
url?: string // 请求路径
|
url?: string // 请求路径
|
||||||
platform?: string // 适用端口 (all:全部, web:Web后台, h5:H5端)
|
platform?: string // 适用端口 (all:全部, web:Web后台, h5:H5端)
|
||||||
sort?: number // 排序值
|
sort?: number // 排序值
|
||||||
|
status?: number // 状态 (0:禁用, 1:启用)
|
||||||
available_for_role_types?: string // 可用角色类型 (1:平台角色, 2:客户角色)
|
available_for_role_types?: string // 可用角色类型 (1:平台角色, 2:客户角色)
|
||||||
children?: PermissionTreeNode[] // 子权限列表
|
children?: PermissionTreeNode[] // 子权限列表
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,11 +44,3 @@ export interface PlatformRoleFormData {
|
|||||||
role_type: RoleType
|
role_type: RoleType
|
||||||
status: RoleStatus
|
status: RoleStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
// 权限树节点
|
|
||||||
export interface PermissionTreeNode {
|
|
||||||
id: number
|
|
||||||
label: string
|
|
||||||
value: string
|
|
||||||
children?: PermissionTreeNode[]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -68,3 +68,26 @@ export interface ShopPageResult {
|
|||||||
size?: number // 每页数量
|
size?: number // 每页数量
|
||||||
total?: number // 总记录数
|
total?: number // 总记录数
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 店铺默认角色相关 ==========
|
||||||
|
|
||||||
|
// 店铺角色响应
|
||||||
|
export interface ShopRoleResponse {
|
||||||
|
role_id: number // 角色ID
|
||||||
|
role_name: string // 角色名称
|
||||||
|
role_desc: string // 角色描述
|
||||||
|
shop_id: number // 店铺ID
|
||||||
|
status: number // 状态 (0:禁用, 1:启用)
|
||||||
|
role_type?: number // 角色类型 (1:平台角色, 2:客户角色) - 可选,用于UI显示
|
||||||
|
}
|
||||||
|
|
||||||
|
// 店铺角色列表响应
|
||||||
|
export interface ShopRolesResponse {
|
||||||
|
shop_id: number // 店铺ID
|
||||||
|
roles: ShopRoleResponse[] | null // 角色列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分配店铺角色请求
|
||||||
|
export interface AssignShopRolesRequest {
|
||||||
|
role_ids: number[] | null // 角色ID列表
|
||||||
|
}
|
||||||
|
|||||||
135
src/types/api/wechatConfig.ts
Normal file
135
src/types/api/wechatConfig.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* 微信支付配置管理相关类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 支付渠道类型
|
||||||
|
export type PaymentProviderType = 'wechat' | 'fuiou'
|
||||||
|
|
||||||
|
// 微信支付配置
|
||||||
|
export interface WechatConfig {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
provider_type: PaymentProviderType
|
||||||
|
is_active: boolean
|
||||||
|
|
||||||
|
// 小程序配置
|
||||||
|
miniapp_app_id: string
|
||||||
|
miniapp_app_secret: string
|
||||||
|
|
||||||
|
// 公众号配置
|
||||||
|
oa_app_id: string
|
||||||
|
oa_app_secret: string
|
||||||
|
oa_token: string
|
||||||
|
oa_aes_key: string
|
||||||
|
oa_oauth_redirect_url: string
|
||||||
|
|
||||||
|
// 微信支付配置
|
||||||
|
wx_mch_id: string
|
||||||
|
wx_api_v2_key: string
|
||||||
|
wx_api_v3_key: string
|
||||||
|
wx_notify_url: string
|
||||||
|
wx_serial_no: string
|
||||||
|
wx_cert_content: string
|
||||||
|
wx_key_content: string
|
||||||
|
|
||||||
|
// 富友支付配置
|
||||||
|
fy_api_url: string
|
||||||
|
fy_ins_cd: string
|
||||||
|
fy_mchnt_cd: string
|
||||||
|
fy_term_id: string
|
||||||
|
fy_notify_url: string
|
||||||
|
fy_private_key: string
|
||||||
|
fy_public_key: string
|
||||||
|
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询参数
|
||||||
|
export interface WechatConfigQueryParams {
|
||||||
|
page?: number
|
||||||
|
page_size?: number
|
||||||
|
provider_type?: PaymentProviderType
|
||||||
|
is_active?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表响应
|
||||||
|
export interface WechatConfigListResponse {
|
||||||
|
items: WechatConfig[]
|
||||||
|
page: number
|
||||||
|
page_size: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建微信支付配置请求
|
||||||
|
export interface CreateWechatConfigRequest {
|
||||||
|
name: string
|
||||||
|
provider_type: PaymentProviderType
|
||||||
|
description?: string
|
||||||
|
|
||||||
|
// 小程序配置
|
||||||
|
miniapp_app_id?: string
|
||||||
|
miniapp_app_secret?: string
|
||||||
|
|
||||||
|
// 公众号配置
|
||||||
|
oa_app_id?: string
|
||||||
|
oa_app_secret?: string
|
||||||
|
oa_token?: string
|
||||||
|
oa_aes_key?: string
|
||||||
|
oa_oauth_redirect_url?: string
|
||||||
|
|
||||||
|
// 微信支付配置
|
||||||
|
wx_mch_id?: string
|
||||||
|
wx_api_v2_key?: string
|
||||||
|
wx_api_v3_key?: string
|
||||||
|
wx_notify_url?: string
|
||||||
|
wx_serial_no?: string
|
||||||
|
wx_cert_content?: string
|
||||||
|
wx_key_content?: string
|
||||||
|
|
||||||
|
// 富友支付配置
|
||||||
|
fy_api_url?: string
|
||||||
|
fy_ins_cd?: string
|
||||||
|
fy_mchnt_cd?: string
|
||||||
|
fy_term_id?: string
|
||||||
|
fy_notify_url?: string
|
||||||
|
fy_private_key?: string
|
||||||
|
fy_public_key?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新微信支付配置请求
|
||||||
|
export interface UpdateWechatConfigRequest {
|
||||||
|
name?: string
|
||||||
|
description?: string
|
||||||
|
provider_type?: PaymentProviderType
|
||||||
|
|
||||||
|
// 小程序配置
|
||||||
|
miniapp_app_id?: string
|
||||||
|
miniapp_app_secret?: string
|
||||||
|
|
||||||
|
// 公众号配置
|
||||||
|
oa_app_id?: string
|
||||||
|
oa_app_secret?: string
|
||||||
|
oa_token?: string
|
||||||
|
oa_aes_key?: string
|
||||||
|
oa_oauth_redirect_url?: string
|
||||||
|
|
||||||
|
// 微信支付配置
|
||||||
|
wx_mch_id?: string
|
||||||
|
wx_api_v2_key?: string
|
||||||
|
wx_api_v3_key?: string
|
||||||
|
wx_notify_url?: string
|
||||||
|
wx_serial_no?: string
|
||||||
|
wx_cert_content?: string
|
||||||
|
wx_key_content?: string
|
||||||
|
|
||||||
|
// 富友支付配置
|
||||||
|
fy_api_url?: string
|
||||||
|
fy_ins_cd?: string
|
||||||
|
fy_mchnt_cd?: string
|
||||||
|
fy_term_id?: string
|
||||||
|
fy_notify_url?: string
|
||||||
|
fy_private_key?: string
|
||||||
|
fy_public_key?: string
|
||||||
|
}
|
||||||
7
src/types/auto-imports.d.ts
vendored
7
src/types/auto-imports.d.ts
vendored
@@ -7,7 +7,12 @@
|
|||||||
export {}
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
const EffectScope: typeof import('vue')['EffectScope']
|
const EffectScope: typeof import('vue')['EffectScope']
|
||||||
const ElButton: (typeof import('element-plus/es'))['ElButton']
|
const ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
|
const ElButton2: typeof import('element-plus/es')['ElButton2']
|
||||||
|
const ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||||
|
const ElDropdown2: typeof import('element-plus/es')['ElDropdown2']
|
||||||
|
const ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||||
|
const ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||||
const ElMessage: (typeof import('element-plus/es'))['ElMessage']
|
const ElMessage: (typeof import('element-plus/es'))['ElMessage']
|
||||||
const ElMessageBox: (typeof import('element-plus/es'))['ElMessageBox']
|
const ElMessageBox: (typeof import('element-plus/es'))['ElMessageBox']
|
||||||
const ElNotification: (typeof import('element-plus/es'))['ElNotification']
|
const ElNotification: (typeof import('element-plus/es'))['ElNotification']
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Option } from '../common'
|
|||||||
export type SearchComponentType =
|
export type SearchComponentType =
|
||||||
| 'input'
|
| 'input'
|
||||||
| 'select'
|
| 'select'
|
||||||
|
| 'tree-select'
|
||||||
| 'radio'
|
| 'radio'
|
||||||
| 'checkbox'
|
| 'checkbox'
|
||||||
| 'date'
|
| 'date'
|
||||||
|
|||||||
14
src/types/components.d.ts
vendored
14
src/types/components.d.ts
vendored
@@ -56,10 +56,12 @@ declare module 'vue' {
|
|||||||
ArtSearchInput: typeof import('./../components/core/forms/art-search-bar/widget/art-search-input/index.vue')['default']
|
ArtSearchInput: typeof import('./../components/core/forms/art-search-bar/widget/art-search-input/index.vue')['default']
|
||||||
ArtSearchRadio: typeof import('./../components/core/forms/art-search-bar/widget/art-search-radio/index.vue')['default']
|
ArtSearchRadio: typeof import('./../components/core/forms/art-search-bar/widget/art-search-radio/index.vue')['default']
|
||||||
ArtSearchSelect: typeof import('./../components/core/forms/art-search-bar/widget/art-search-select/index.vue')['default']
|
ArtSearchSelect: typeof import('./../components/core/forms/art-search-bar/widget/art-search-select/index.vue')['default']
|
||||||
|
ArtSearchTreeSelect: typeof import('./../components/core/forms/art-search-bar/widget/art-search-tree-select/index.vue')['default']
|
||||||
ArtSettingsPanel: typeof import('./../components/core/layouts/art-settings-panel/index.vue')['default']
|
ArtSettingsPanel: typeof import('./../components/core/layouts/art-settings-panel/index.vue')['default']
|
||||||
ArtSidebarMenu: typeof import('./../components/core/layouts/art-menus/art-sidebar-menu/index.vue')['default']
|
ArtSidebarMenu: typeof import('./../components/core/layouts/art-menus/art-sidebar-menu/index.vue')['default']
|
||||||
ArtStatsCard: typeof import('./../components/core/cards/ArtStatsCard.vue')['default']
|
ArtStatsCard: typeof import('./../components/core/cards/ArtStatsCard.vue')['default']
|
||||||
ArtTable: typeof import('./../components/core/tables/ArtTable.vue')['default']
|
ArtTable: typeof import('./../components/core/tables/ArtTable.vue')['default']
|
||||||
|
ArtTableColumn: typeof import('./../components/core/tables/ArtTableColumn.vue')['default']
|
||||||
ArtTableFullScreen: typeof import('./../components/core/tables/ArtTableFullScreen.vue')['default']
|
ArtTableFullScreen: typeof import('./../components/core/tables/ArtTableFullScreen.vue')['default']
|
||||||
ArtTableHeader: typeof import('./../components/core/tables/ArtTableHeader.vue')['default']
|
ArtTableHeader: typeof import('./../components/core/tables/ArtTableHeader.vue')['default']
|
||||||
ArtTextScroll: typeof import('./../components/core/text-effect/ArtTextScroll.vue')['default']
|
ArtTextScroll: typeof import('./../components/core/text-effect/ArtTextScroll.vue')['default']
|
||||||
@@ -73,17 +75,21 @@ declare module 'vue' {
|
|||||||
BoxStyleSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue')['default']
|
BoxStyleSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue')['default']
|
||||||
CardOperationDialog: typeof import('./../components/business/CardOperationDialog.vue')['default']
|
CardOperationDialog: typeof import('./../components/business/CardOperationDialog.vue')['default']
|
||||||
CardStatusTag: typeof import('./../components/business/CardStatusTag.vue')['default']
|
CardStatusTag: typeof import('./../components/business/CardStatusTag.vue')['default']
|
||||||
|
CodeGeneratorButton: typeof import('./../components/business/CodeGeneratorButton.vue')['default']
|
||||||
ColorSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ColorSettings.vue')['default']
|
ColorSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ColorSettings.vue')['default']
|
||||||
CommentItem: typeof import('./../components/custom/comment-widget/widget/CommentItem.vue')['default']
|
CommentItem: typeof import('./../components/custom/comment-widget/widget/CommentItem.vue')['default']
|
||||||
CommentWidget: typeof import('./../components/custom/comment-widget/index.vue')['default']
|
CommentWidget: typeof import('./../components/custom/comment-widget/index.vue')['default']
|
||||||
CommissionDisplay: typeof import('./../components/business/CommissionDisplay.vue')['default']
|
CommissionDisplay: typeof import('./../components/business/CommissionDisplay.vue')['default']
|
||||||
ContainerSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ContainerSettings.vue')['default']
|
ContainerSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ContainerSettings.vue')['default']
|
||||||
|
CustomerAccountDialog: typeof import('./../components/business/CustomerAccountDialog.vue')['default']
|
||||||
|
DetailPage: typeof import('./../components/common/DetailPage.vue')['default']
|
||||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
||||||
ElCalendar: typeof import('element-plus/es')['ElCalendar']
|
ElCalendar: typeof import('element-plus/es')['ElCalendar']
|
||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
|
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||||
ElCol: typeof import('element-plus/es')['ElCol']
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
@@ -107,6 +113,7 @@ declare module 'vue' {
|
|||||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
|
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
|
||||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||||
@@ -119,6 +126,8 @@ declare module 'vue' {
|
|||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||||
ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem']
|
ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem']
|
||||||
|
ElStep: typeof import('element-plus/es')['ElStep']
|
||||||
|
ElSteps: typeof import('element-plus/es')['ElSteps']
|
||||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
@@ -130,6 +139,7 @@ declare module 'vue' {
|
|||||||
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||||
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
|
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
|
||||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
|
ElTransfer: typeof import('element-plus/es')['ElTransfer']
|
||||||
ElTree: typeof import('element-plus/es')['ElTree']
|
ElTree: typeof import('element-plus/es')['ElTree']
|
||||||
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||||
@@ -147,7 +157,11 @@ declare module 'vue' {
|
|||||||
SettingDrawer: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingDrawer.vue')['default']
|
SettingDrawer: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingDrawer.vue')['default']
|
||||||
SettingHeader: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingHeader.vue')['default']
|
SettingHeader: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingHeader.vue')['default']
|
||||||
SettingItem: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingItem.vue')['default']
|
SettingItem: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingItem.vue')['default']
|
||||||
|
SetWiFiDialog: typeof import('./../components/device/SetWiFiDialog.vue')['default']
|
||||||
SidebarSubmenu: typeof import('./../components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue')['default']
|
SidebarSubmenu: typeof import('./../components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue')['default']
|
||||||
|
SpeedLimitDialog: typeof import('./../components/device/SpeedLimitDialog.vue')['default']
|
||||||
|
SwitchCardDialog: typeof import('./../components/device/SwitchCardDialog.vue')['default']
|
||||||
|
TableContextMenuHint: typeof import('./../components/core/others/TableContextMenuHint.vue')['default']
|
||||||
ThemeSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ThemeSettings.vue')['default']
|
ThemeSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ThemeSettings.vue')['default']
|
||||||
}
|
}
|
||||||
export interface ComponentCustomProperties {
|
export interface ComponentCustomProperties {
|
||||||
|
|||||||
69
src/utils/codeGenerator.ts
Normal file
69
src/utils/codeGenerator.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* 编码生成工具
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成套餐系列编码
|
||||||
|
* 规则: PS + 年月日时分秒 + 4位随机数
|
||||||
|
* 示例: PS20260203143025ABCD
|
||||||
|
*/
|
||||||
|
export function generateSeriesCode(): string {
|
||||||
|
const now = new Date()
|
||||||
|
const year = now.getFullYear()
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(now.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(now.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(now.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(now.getSeconds()).padStart(2, '0')
|
||||||
|
|
||||||
|
// 生成4位随机大写字母
|
||||||
|
const randomChars = Array.from({ length: 4 }, () =>
|
||||||
|
String.fromCharCode(65 + Math.floor(Math.random() * 26))
|
||||||
|
).join('')
|
||||||
|
|
||||||
|
return `PS${year}${month}${day}${hours}${minutes}${seconds}${randomChars}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成套餐编码
|
||||||
|
* 规则: PKG + 年月日时分秒 + 4位随机数
|
||||||
|
* 示例: PKG20260203143025ABCD
|
||||||
|
*/
|
||||||
|
export function generatePackageCode(): string {
|
||||||
|
const now = new Date()
|
||||||
|
const year = now.getFullYear()
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(now.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(now.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(now.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(now.getSeconds()).padStart(2, '0')
|
||||||
|
|
||||||
|
// 生成4位随机大写字母
|
||||||
|
const randomChars = Array.from({ length: 4 }, () =>
|
||||||
|
String.fromCharCode(65 + Math.floor(Math.random() * 26))
|
||||||
|
).join('')
|
||||||
|
|
||||||
|
return `PKG${year}${month}${day}${hours}${minutes}${seconds}${randomChars}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成店铺编码
|
||||||
|
* 规则: SHOP + 年月日时分秒 + 4位随机数
|
||||||
|
* 示例: SHOP20260314143025ABCD
|
||||||
|
*/
|
||||||
|
export function generateShopCode(): string {
|
||||||
|
const now = new Date()
|
||||||
|
const year = now.getFullYear()
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(now.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(now.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(now.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(now.getSeconds()).padStart(2, '0')
|
||||||
|
|
||||||
|
// 生成4位随机大写字母
|
||||||
|
const randomChars = Array.from({ length: 4 }, () =>
|
||||||
|
String.fromCharCode(65 + Math.floor(Math.random() * 26))
|
||||||
|
).join('')
|
||||||
|
|
||||||
|
return `SHOP${year}${month}${day}${hours}${minutes}${seconds}${randomChars}`
|
||||||
|
}
|
||||||
220
src/utils/constants/regionData.ts
Normal file
220
src/utils/constants/regionData.ts
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
/**
|
||||||
|
* 省市区三级联动数据
|
||||||
|
* 简化版数据,包含主要省市区
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface RegionItem {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
children?: RegionItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const regionData: RegionItem[] = [
|
||||||
|
{
|
||||||
|
value: '北京市',
|
||||||
|
label: '北京市',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '北京市',
|
||||||
|
label: '北京市',
|
||||||
|
children: [
|
||||||
|
{ value: '东城区', label: '东城区' },
|
||||||
|
{ value: '西城区', label: '西城区' },
|
||||||
|
{ value: '朝阳区', label: '朝阳区' },
|
||||||
|
{ value: '丰台区', label: '丰台区' },
|
||||||
|
{ value: '石景山区', label: '石景山区' },
|
||||||
|
{ value: '海淀区', label: '海淀区' },
|
||||||
|
{ value: '门头沟区', label: '门头沟区' },
|
||||||
|
{ value: '房山区', label: '房山区' },
|
||||||
|
{ value: '通州区', label: '通州区' },
|
||||||
|
{ value: '顺义区', label: '顺义区' },
|
||||||
|
{ value: '昌平区', label: '昌平区' },
|
||||||
|
{ value: '大兴区', label: '大兴区' },
|
||||||
|
{ value: '怀柔区', label: '怀柔区' },
|
||||||
|
{ value: '平谷区', label: '平谷区' },
|
||||||
|
{ value: '密云区', label: '密云区' },
|
||||||
|
{ value: '延庆区', label: '延庆区' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '上海市',
|
||||||
|
label: '上海市',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '上海市',
|
||||||
|
label: '上海市',
|
||||||
|
children: [
|
||||||
|
{ value: '黄浦区', label: '黄浦区' },
|
||||||
|
{ value: '徐汇区', label: '徐汇区' },
|
||||||
|
{ value: '长宁区', label: '长宁区' },
|
||||||
|
{ value: '静安区', label: '静安区' },
|
||||||
|
{ value: '普陀区', label: '普陀区' },
|
||||||
|
{ value: '虹口区', label: '虹口区' },
|
||||||
|
{ value: '杨浦区', label: '杨浦区' },
|
||||||
|
{ value: '闵行区', label: '闵行区' },
|
||||||
|
{ value: '宝山区', label: '宝山区' },
|
||||||
|
{ value: '嘉定区', label: '嘉定区' },
|
||||||
|
{ value: '浦东新区', label: '浦东新区' },
|
||||||
|
{ value: '金山区', label: '金山区' },
|
||||||
|
{ value: '松江区', label: '松江区' },
|
||||||
|
{ value: '青浦区', label: '青浦区' },
|
||||||
|
{ value: '奉贤区', label: '奉贤区' },
|
||||||
|
{ value: '崇明区', label: '崇明区' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '广东省',
|
||||||
|
label: '广东省',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '广州市',
|
||||||
|
label: '广州市',
|
||||||
|
children: [
|
||||||
|
{ value: '荔湾区', label: '荔湾区' },
|
||||||
|
{ value: '越秀区', label: '越秀区' },
|
||||||
|
{ value: '海珠区', label: '海珠区' },
|
||||||
|
{ value: '天河区', label: '天河区' },
|
||||||
|
{ value: '白云区', label: '白云区' },
|
||||||
|
{ value: '黄埔区', label: '黄埔区' },
|
||||||
|
{ value: '番禺区', label: '番禺区' },
|
||||||
|
{ value: '花都区', label: '花都区' },
|
||||||
|
{ value: '南沙区', label: '南沙区' },
|
||||||
|
{ value: '从化区', label: '从化区' },
|
||||||
|
{ value: '增城区', label: '增城区' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '深圳市',
|
||||||
|
label: '深圳市',
|
||||||
|
children: [
|
||||||
|
{ value: '罗湖区', label: '罗湖区' },
|
||||||
|
{ value: '福田区', label: '福田区' },
|
||||||
|
{ value: '南山区', label: '南山区' },
|
||||||
|
{ value: '宝安区', label: '宝安区' },
|
||||||
|
{ value: '龙岗区', label: '龙岗区' },
|
||||||
|
{ value: '盐田区', label: '盐田区' },
|
||||||
|
{ value: '龙华区', label: '龙华区' },
|
||||||
|
{ value: '坪山区', label: '坪山区' },
|
||||||
|
{ value: '光明区', label: '光明区' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '东莞市',
|
||||||
|
label: '东莞市',
|
||||||
|
children: [
|
||||||
|
{ value: '莞城区', label: '莞城区' },
|
||||||
|
{ value: '南城区', label: '南城区' },
|
||||||
|
{ value: '东城区', label: '东城区' },
|
||||||
|
{ value: '万江区', label: '万江区' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '佛山市',
|
||||||
|
label: '佛山市',
|
||||||
|
children: [
|
||||||
|
{ value: '禅城区', label: '禅城区' },
|
||||||
|
{ value: '南海区', label: '南海区' },
|
||||||
|
{ value: '顺德区', label: '顺德区' },
|
||||||
|
{ value: '三水区', label: '三水区' },
|
||||||
|
{ value: '高明区', label: '高明区' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '浙江省',
|
||||||
|
label: '浙江省',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '杭州市',
|
||||||
|
label: '杭州市',
|
||||||
|
children: [
|
||||||
|
{ value: '上城区', label: '上城区' },
|
||||||
|
{ value: '拱墅区', label: '拱墅区' },
|
||||||
|
{ value: '西湖区', label: '西湖区' },
|
||||||
|
{ value: '滨江区', label: '滨江区' },
|
||||||
|
{ value: '萧山区', label: '萧山区' },
|
||||||
|
{ value: '余杭区', label: '余杭区' },
|
||||||
|
{ value: '临平区', label: '临平区' },
|
||||||
|
{ value: '钱塘区', label: '钱塘区' },
|
||||||
|
{ value: '富阳区', label: '富阳区' },
|
||||||
|
{ value: '临安区', label: '临安区' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '宁波市',
|
||||||
|
label: '宁波市',
|
||||||
|
children: [
|
||||||
|
{ value: '海曙区', label: '海曙区' },
|
||||||
|
{ value: '江北区', label: '江北区' },
|
||||||
|
{ value: '北仑区', label: '北仑区' },
|
||||||
|
{ value: '镇海区', label: '镇海区' },
|
||||||
|
{ value: '鄞州区', label: '鄞州区' },
|
||||||
|
{ value: '奉化区', label: '奉化区' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '江苏省',
|
||||||
|
label: '江苏省',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '南京市',
|
||||||
|
label: '南京市',
|
||||||
|
children: [
|
||||||
|
{ value: '玄武区', label: '玄武区' },
|
||||||
|
{ value: '秦淮区', label: '秦淮区' },
|
||||||
|
{ value: '建邺区', label: '建邺区' },
|
||||||
|
{ value: '鼓楼区', label: '鼓楼区' },
|
||||||
|
{ value: '浦口区', label: '浦口区' },
|
||||||
|
{ value: '栖霞区', label: '栖霞区' },
|
||||||
|
{ value: '雨花台区', label: '雨花台区' },
|
||||||
|
{ value: '江宁区', label: '江宁区' },
|
||||||
|
{ value: '六合区', label: '六合区' },
|
||||||
|
{ value: '溧水区', label: '溧水区' },
|
||||||
|
{ value: '高淳区', label: '高淳区' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '苏州市',
|
||||||
|
label: '苏州市',
|
||||||
|
children: [
|
||||||
|
{ value: '姑苏区', label: '姑苏区' },
|
||||||
|
{ value: '虎丘区', label: '虎丘区' },
|
||||||
|
{ value: '吴中区', label: '吴中区' },
|
||||||
|
{ value: '相城区', label: '相城区' },
|
||||||
|
{ value: '吴江区', label: '吴江区' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '四川省',
|
||||||
|
label: '四川省',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '成都市',
|
||||||
|
label: '成都市',
|
||||||
|
children: [
|
||||||
|
{ value: '锦江区', label: '锦江区' },
|
||||||
|
{ value: '青羊区', label: '青羊区' },
|
||||||
|
{ value: '金牛区', label: '金牛区' },
|
||||||
|
{ value: '武侯区', label: '武侯区' },
|
||||||
|
{ value: '成华区', label: '成华区' },
|
||||||
|
{ value: '龙泉驿区', label: '龙泉驿区' },
|
||||||
|
{ value: '青白江区', label: '青白江区' },
|
||||||
|
{ value: '新都区', label: '新都区' },
|
||||||
|
{ value: '温江区', label: '温江区' },
|
||||||
|
{ value: '双流区', label: '双流区' },
|
||||||
|
{ value: '郫都区', label: '郫都区' },
|
||||||
|
{ value: '新津区', label: '新津区' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
<ArtSearchBar
|
<ArtSearchBar
|
||||||
v-model:filter="formFilters"
|
v-model:filter="formFilters"
|
||||||
:items="formItems"
|
:items="formItems"
|
||||||
:show-expand="false"
|
|
||||||
@reset="handleReset"
|
@reset="handleReset"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
></ArtSearchBar>
|
></ArtSearchBar>
|
||||||
@@ -18,35 +17,50 @@
|
|||||||
@refresh="handleRefresh"
|
@refresh="handleRefresh"
|
||||||
>
|
>
|
||||||
<template #left>
|
<template #left>
|
||||||
<ElButton @click="showDialog('add')">新增账号</ElButton>
|
<ElButton @click="showDialog('add')" v-permission="'account:add'">新增账号</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</ArtTableHeader>
|
</ArtTableHeader>
|
||||||
|
|
||||||
<!-- 表格 -->
|
<!-- 表格 -->
|
||||||
<ArtTable
|
<ArtTable
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
row-key="ID"
|
row-key="id"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:data="tableData"
|
:data="tableData"
|
||||||
:currentPage="pagination.currentPage"
|
:currentPage="pagination.currentPage"
|
||||||
:pageSize="pagination.pageSize"
|
:pageSize="pagination.pageSize"
|
||||||
:total="pagination.total"
|
:total="pagination.total"
|
||||||
:marginTop="10"
|
:marginTop="10"
|
||||||
|
:row-class-name="getRowClassName"
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
|
@row-contextmenu="handleRowContextMenu"
|
||||||
|
@cell-mouse-enter="handleCellMouseEnter"
|
||||||
|
@cell-mouse-leave="handleCellMouseLeave"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||||
</template>
|
</template>
|
||||||
</ArtTable>
|
</ArtTable>
|
||||||
|
|
||||||
|
<!-- 鼠标悬浮提示 -->
|
||||||
|
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||||
|
|
||||||
|
<!-- 右键菜单 -->
|
||||||
|
<ArtMenuRight
|
||||||
|
ref="contextMenuRef"
|
||||||
|
:menu-items="contextMenuItems"
|
||||||
|
:menu-width="120"
|
||||||
|
@select="handleContextMenuSelect"
|
||||||
|
/>
|
||||||
|
|
||||||
<ElDialog
|
<ElDialog
|
||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
:title="dialogType === 'add' ? '添加账号' : '编辑账号'"
|
:title="dialogType === 'add' ? '添加账号' : '编辑账号'"
|
||||||
width="500px"
|
width="30%"
|
||||||
>
|
>
|
||||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="80px">
|
||||||
<ElFormItem label="账号名称" prop="username">
|
<ElFormItem label="账号名称" prop="username">
|
||||||
<ElInput v-model="formData.username" placeholder="请输入账号名称" />
|
<ElInput v-model="formData.username" placeholder="请输入账号名称" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
@@ -61,7 +75,7 @@
|
|||||||
<ElFormItem label="手机号" prop="phone">
|
<ElFormItem label="手机号" prop="phone">
|
||||||
<ElInput v-model="formData.phone" placeholder="请输入手机号" maxlength="11" />
|
<ElInput v-model="formData.phone" placeholder="请输入手机号" maxlength="11" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="账号类型" prop="user_type">
|
<ElFormItem v-if="dialogType === 'add'" label="账号类型" prop="user_type">
|
||||||
<ElSelect
|
<ElSelect
|
||||||
v-model="formData.user_type"
|
v-model="formData.user_type"
|
||||||
placeholder="请选择账号类型"
|
placeholder="请选择账号类型"
|
||||||
@@ -69,8 +83,6 @@
|
|||||||
>
|
>
|
||||||
<ElOption label="超级管理员" :value="1" />
|
<ElOption label="超级管理员" :value="1" />
|
||||||
<ElOption label="平台用户" :value="2" />
|
<ElOption label="平台用户" :value="2" />
|
||||||
<ElOption label="代理账号" :value="3" />
|
|
||||||
<ElOption label="企业账号" :value="4" />
|
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
@@ -83,27 +95,92 @@
|
|||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
<!-- 分配角色对话框 -->
|
<!-- 分配角色对话框 -->
|
||||||
<ElDialog v-model="roleDialogVisible" title="分配角色" width="500px">
|
<ElDialog v-model="roleDialogVisible" width="900px">
|
||||||
<ElCheckboxGroup v-model="selectedRoles">
|
<template #header>
|
||||||
<div v-for="role in allRoles" :key="role.ID" style="margin-bottom: 12px">
|
<div class="dialog-header">
|
||||||
<ElCheckbox :label="role.ID">
|
<span class="dialog-title">分配角色</span>
|
||||||
{{ role.role_name }}
|
<div class="account-info">
|
||||||
<ElTag
|
<span class="account-name">{{ currentAccountName }}</span>
|
||||||
:type="role.role_type === 1 ? 'primary' : 'success'"
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="role-transfer-container">
|
||||||
|
<!-- 左侧:可分配角色列表 -->
|
||||||
|
<div class="transfer-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span class="panel-title">可分配角色</span>
|
||||||
|
<ElInput
|
||||||
|
v-model="leftRoleFilter"
|
||||||
|
placeholder="搜索角色"
|
||||||
|
clearable
|
||||||
size="small"
|
size="small"
|
||||||
style="margin-left: 8px"
|
style="width: 180px"
|
||||||
>
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<ElCheckboxGroup v-model="rolesToAdd" class="role-list">
|
||||||
|
<div v-for="role in filteredAvailableRoles" :key="role.ID" class="role-item">
|
||||||
|
<ElCheckbox :label="role.ID" :disabled="selectedRoles.includes(role.ID)">
|
||||||
|
<span class="role-info">
|
||||||
|
<span>{{ role.role_name }}</span>
|
||||||
|
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
|
||||||
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
|
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
|
||||||
</ElTag>
|
</ElTag>
|
||||||
|
</span>
|
||||||
</ElCheckbox>
|
</ElCheckbox>
|
||||||
</div>
|
</div>
|
||||||
</ElCheckboxGroup>
|
</ElCheckboxGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 中间:操作按钮 -->
|
||||||
|
<div class="transfer-buttons">
|
||||||
|
<ElButton
|
||||||
|
type="primary"
|
||||||
|
:icon="'ArrowRight'"
|
||||||
|
@click="addRoles"
|
||||||
|
:disabled="rolesToAdd.length === 0"
|
||||||
|
>
|
||||||
|
添加
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:已分配角色列表 -->
|
||||||
|
<div class="transfer-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span class="panel-title">已分配角色</span>
|
||||||
|
<ElInput
|
||||||
|
v-model="rightRoleFilter"
|
||||||
|
placeholder="搜索角色"
|
||||||
|
clearable
|
||||||
|
size="small"
|
||||||
|
style="width: 180px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="role-list">
|
||||||
|
<div
|
||||||
|
v-for="role in filteredAssignedRoles"
|
||||||
|
:key="role.ID"
|
||||||
|
class="role-item assigned-role-item"
|
||||||
|
>
|
||||||
|
<span class="role-info">
|
||||||
|
<span>{{ role.role_name }}</span>
|
||||||
|
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
|
||||||
|
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
|
||||||
|
</ElTag>
|
||||||
|
</span>
|
||||||
|
<ElButton type="danger" size="small" link @click="removeSingleRole(role.ID)">
|
||||||
|
移除
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<ElButton @click="roleDialogVisible = false">取消</ElButton>
|
<ElButton @click="roleDialogVisible = false">关闭</ElButton>
|
||||||
<ElButton type="primary" @click="handleAssignRoles" :loading="roleSubmitLoading"
|
|
||||||
>提交</ElButton
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
@@ -114,38 +191,72 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import { FormInstance, ElSwitch, ElCheckbox, ElCheckboxGroup, ElTag } from 'element-plus'
|
import { FormInstance, ElSwitch, ElCheckbox, ElCheckboxGroup, ElTag } from 'element-plus'
|
||||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||||
import type { FormRules } from 'element-plus'
|
import type { FormRules } from 'element-plus'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||||
|
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||||
|
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||||
|
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||||
import { AccountService } from '@/api/modules/account'
|
import { AccountService } from '@/api/modules/account'
|
||||||
import { RoleService } from '@/api/modules/role'
|
import { RoleService } from '@/api/modules/role'
|
||||||
|
import { ShopService, EnterpriseService } from '@/api/modules'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import type { PlatformRole } from '@/types/api'
|
import type { PlatformRole } from '@/types/api'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import { CommonStatus, getStatusText } from '@/config/constants'
|
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
|
||||||
|
|
||||||
defineOptions({ name: 'Account' }) // 定义组件名称,用于 KeepAlive 缓存控制
|
defineOptions({ name: 'Account' }) // 定义组件名称,用于 KeepAlive 缓存控制
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
// 使用表格右键菜单功能
|
||||||
|
const {
|
||||||
|
showContextMenuHint,
|
||||||
|
hintPosition,
|
||||||
|
getRowClassName,
|
||||||
|
handleCellMouseEnter,
|
||||||
|
handleCellMouseLeave
|
||||||
|
} = useTableContextMenu()
|
||||||
|
|
||||||
const dialogType = ref('add')
|
const dialogType = ref('add')
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const roleDialogVisible = ref(false)
|
const roleDialogVisible = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const roleSubmitLoading = ref(false)
|
const roleSubmitLoading = ref(false)
|
||||||
const currentAccountId = ref<number>(0)
|
const currentAccountId = ref<number>(0)
|
||||||
|
const currentAccountName = ref<string>('')
|
||||||
|
const currentAccountType = ref<number>(0)
|
||||||
|
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||||
|
const currentRow = ref<any | null>(null)
|
||||||
const selectedRoles = ref<number[]>([])
|
const selectedRoles = ref<number[]>([])
|
||||||
const allRoles = ref<PlatformRole[]>([])
|
const allRoles = ref<PlatformRole[]>([])
|
||||||
|
const rolesToAdd = ref<number[]>([])
|
||||||
|
const leftRoleFilter = ref('')
|
||||||
|
const rightRoleFilter = ref('')
|
||||||
|
|
||||||
// 定义表单搜索初始值
|
// 定义表单搜索初始值
|
||||||
const initialSearchState = {
|
const initialSearchState = {
|
||||||
name: '',
|
name: '',
|
||||||
phone: ''
|
phone: '',
|
||||||
|
user_type: undefined as number | undefined,
|
||||||
|
shop_id: undefined as number | undefined,
|
||||||
|
enterprise_id: undefined as number | undefined,
|
||||||
|
status: undefined as number | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应式表单数据
|
// 响应式表单数据
|
||||||
const formFilters = reactive({ ...initialSearchState })
|
const formFilters = reactive({ ...initialSearchState })
|
||||||
|
|
||||||
|
// 店铺和企业列表
|
||||||
|
const shopList = ref<any[]>([])
|
||||||
|
const enterpriseList = ref<any[]>([])
|
||||||
|
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
@@ -176,7 +287,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 表单配置项
|
// 表单配置项
|
||||||
const formItems: SearchFormItem[] = [
|
const formItems = computed<SearchFormItem[]>(() => [
|
||||||
{
|
{
|
||||||
label: '账号名称',
|
label: '账号名称',
|
||||||
prop: 'name',
|
prop: 'name',
|
||||||
@@ -194,18 +305,75 @@
|
|||||||
clearable: true,
|
clearable: true,
|
||||||
placeholder: '请输入手机号'
|
placeholder: '请输入手机号'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '账号类型',
|
||||||
|
prop: 'user_type',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: '超级管理员', value: 1 },
|
||||||
|
{ label: '平台用户', value: 2 },
|
||||||
|
{ label: '代理账号', value: 3 },
|
||||||
|
{ label: '企业账号', value: 4 }
|
||||||
|
],
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择账号类型'
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
|
{
|
||||||
|
label: '关联店铺',
|
||||||
|
prop: 'shop_id',
|
||||||
|
type: 'select',
|
||||||
|
options: shopList.value.map((shop) => ({
|
||||||
|
label: shop.shop_name,
|
||||||
|
value: shop.id
|
||||||
|
})),
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
filterable: true,
|
||||||
|
remote: true,
|
||||||
|
remoteMethod: handleShopSearch,
|
||||||
|
placeholder: '请输入店铺名称搜索'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '关联企业',
|
||||||
|
prop: 'enterprise_id',
|
||||||
|
type: 'select',
|
||||||
|
options: enterpriseList.value.map((enterprise) => ({
|
||||||
|
label: enterprise.enterprise_name,
|
||||||
|
value: enterprise.id
|
||||||
|
})),
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
filterable: true,
|
||||||
|
remote: true,
|
||||||
|
remoteMethod: handleEnterpriseSearch,
|
||||||
|
placeholder: '请输入企业名称搜索'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '状态',
|
||||||
|
prop: 'status',
|
||||||
|
type: 'select',
|
||||||
|
options: STATUS_SELECT_OPTIONS,
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择状态'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
// 列配置
|
// 列配置
|
||||||
const columnOptions = [
|
const columnOptions = [
|
||||||
{ label: 'ID', prop: 'ID' },
|
|
||||||
{ label: '账号名称', prop: 'username' },
|
{ label: '账号名称', prop: 'username' },
|
||||||
{ label: '手机号', prop: 'phone' },
|
{ label: '手机号', prop: 'phone' },
|
||||||
{ label: '账号类型', prop: 'user_type_name' },
|
{ label: '账号类型', prop: 'user_type' },
|
||||||
|
{ label: '店铺名称', prop: 'shop_name' },
|
||||||
|
{ label: '企业名称', prop: 'enterprise_name' },
|
||||||
{ label: '状态', prop: 'status' },
|
{ label: '状态', prop: 'status' },
|
||||||
{ label: '创建时间', prop: 'CreatedAt' },
|
{ label: '创建时间', prop: 'created_at' }
|
||||||
{ label: '操作', prop: 'operation' }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
// 显示对话框
|
// 显示对话框
|
||||||
@@ -219,16 +387,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'edit' && row) {
|
if (type === 'edit' && row) {
|
||||||
formData.id = row.ID
|
formData.id = row.id
|
||||||
formData.username = row.username
|
formData.username = row.username
|
||||||
formData.phone = row.phone
|
formData.phone = row.phone
|
||||||
formData.user_type = row.user_type
|
formData.user_type = row.user_type
|
||||||
|
formData.shop_id = row.shop_id || null
|
||||||
|
formData.enterprise_id = row.enterprise_id || null
|
||||||
formData.password = ''
|
formData.password = ''
|
||||||
} else {
|
} else {
|
||||||
formData.id = ''
|
formData.id = ''
|
||||||
formData.username = ''
|
formData.username = ''
|
||||||
formData.phone = ''
|
formData.phone = ''
|
||||||
formData.user_type = 2
|
formData.user_type = 2
|
||||||
|
formData.shop_id = null
|
||||||
|
formData.enterprise_id = null
|
||||||
formData.password = ''
|
formData.password = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,7 +414,7 @@
|
|||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
try {
|
try {
|
||||||
await AccountService.deleteAccount(row.ID)
|
await AccountService.deleteAccount(row.id)
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
getAccountList()
|
getAccountList()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -256,21 +428,20 @@
|
|||||||
|
|
||||||
// 动态列配置
|
// 动态列配置
|
||||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||||
{
|
|
||||||
prop: 'ID',
|
|
||||||
label: 'ID'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
prop: 'username',
|
prop: 'username',
|
||||||
label: '账号名称'
|
label: '账号名称',
|
||||||
|
minWidth: 120
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'phone',
|
prop: 'phone',
|
||||||
label: '手机号'
|
label: '手机号',
|
||||||
|
width: 130
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'user_type',
|
prop: 'user_type',
|
||||||
label: '账号类型',
|
label: '账号类型',
|
||||||
|
width: 120,
|
||||||
formatter: (row: any) => {
|
formatter: (row: any) => {
|
||||||
const typeMap: Record<number, string> = {
|
const typeMap: Record<number, string> = {
|
||||||
1: '超级管理员',
|
1: '超级管理员',
|
||||||
@@ -281,9 +452,26 @@
|
|||||||
return typeMap[row.user_type] || '-'
|
return typeMap[row.user_type] || '-'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prop: 'shop_name',
|
||||||
|
label: '店铺名称',
|
||||||
|
minWidth: 150,
|
||||||
|
formatter: (row: any) => {
|
||||||
|
return row.shop_name || '-'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'enterprise_name',
|
||||||
|
label: '企业名称',
|
||||||
|
minWidth: 150,
|
||||||
|
formatter: (row: any) => {
|
||||||
|
return row.enterprise_name || '-'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
label: '状态',
|
label: '状态',
|
||||||
|
width: 100,
|
||||||
formatter: (row: any) => {
|
formatter: (row: any) => {
|
||||||
return h(ElSwitch, {
|
return h(ElSwitch, {
|
||||||
modelValue: row.status,
|
modelValue: row.status,
|
||||||
@@ -292,36 +480,17 @@
|
|||||||
activeText: getStatusText(CommonStatus.ENABLED),
|
activeText: getStatusText(CommonStatus.ENABLED),
|
||||||
inactiveText: getStatusText(CommonStatus.DISABLED),
|
inactiveText: getStatusText(CommonStatus.DISABLED),
|
||||||
inlinePrompt: true,
|
inlinePrompt: true,
|
||||||
|
disabled: !hasAuth('account:modify_status'),
|
||||||
'onUpdate:modelValue': (val: string | number | boolean) =>
|
'onUpdate:modelValue': (val: string | number | boolean) =>
|
||||||
handleStatusChange(row, val as number)
|
handleStatusChange(row, val as number)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'CreatedAt',
|
prop: 'created_at',
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
formatter: (row: any) => formatDateTime(row.CreatedAt)
|
width: 180,
|
||||||
},
|
formatter: (row: any) => formatDateTime(row.created_at)
|
||||||
{
|
|
||||||
prop: 'operation',
|
|
||||||
label: '操作',
|
|
||||||
fixed: 'right',
|
|
||||||
formatter: (row: any) => {
|
|
||||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
|
||||||
h(ArtButtonTable, {
|
|
||||||
icon: '',
|
|
||||||
onClick: () => showRoleDialog(row)
|
|
||||||
}),
|
|
||||||
h(ArtButtonTable, {
|
|
||||||
type: 'edit',
|
|
||||||
onClick: () => showDialog('edit', row)
|
|
||||||
}),
|
|
||||||
h(ArtButtonTable, {
|
|
||||||
type: 'delete',
|
|
||||||
onClick: () => deleteAccount(row)
|
|
||||||
})
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -334,12 +503,22 @@
|
|||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
user_type: 2
|
user_type: 2,
|
||||||
|
shop_id: null as number | null,
|
||||||
|
enterprise_id: null as number | null
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 从 URL 查询参数中读取 shop_id
|
||||||
|
const shopIdParam = route.query.shop_id
|
||||||
|
if (shopIdParam) {
|
||||||
|
formFilters.shop_id = Number(shopIdParam)
|
||||||
|
}
|
||||||
|
|
||||||
getAccountList()
|
getAccountList()
|
||||||
loadAllRoles()
|
loadAllRoles()
|
||||||
|
loadShopList()
|
||||||
|
loadEnterpriseList()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 加载所有角色列表
|
// 加载所有角色列表
|
||||||
@@ -354,21 +533,60 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算属性:过滤后的可分配角色(根据账号类型过滤)
|
||||||
|
const filteredAvailableRoles = computed(() => {
|
||||||
|
let roles = allRoles.value
|
||||||
|
|
||||||
|
// 根据账号类型过滤角色
|
||||||
|
if (currentAccountType.value === 1) {
|
||||||
|
// 超级管理员:不能分配任何角色
|
||||||
|
return []
|
||||||
|
} else if (currentAccountType.value === 3) {
|
||||||
|
// 代理账号:只显示客户角色
|
||||||
|
roles = roles.filter((role) => role.role_type === 2)
|
||||||
|
} else if (currentAccountType.value === 4) {
|
||||||
|
// 企业账号:只显示客户角色
|
||||||
|
roles = roles.filter((role) => role.role_type === 2)
|
||||||
|
} else if (currentAccountType.value === 2) {
|
||||||
|
// 平台用户:只显示平台角色
|
||||||
|
roles = roles.filter((role) => role.role_type === 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据搜索关键词过滤
|
||||||
|
if (!leftRoleFilter.value) return roles
|
||||||
|
const keyword = leftRoleFilter.value.toLowerCase()
|
||||||
|
return roles.filter((role) => role.role_name.toLowerCase().includes(keyword))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算属性:过滤后的已分配角色
|
||||||
|
const filteredAssignedRoles = computed(() => {
|
||||||
|
const assignedRolesList = allRoles.value.filter((role) => selectedRoles.value.includes(role.ID))
|
||||||
|
if (!rightRoleFilter.value) return assignedRolesList
|
||||||
|
const keyword = rightRoleFilter.value.toLowerCase()
|
||||||
|
return assignedRolesList.filter((role) => role.role_name.toLowerCase().includes(keyword))
|
||||||
|
})
|
||||||
|
|
||||||
// 显示分配角色对话框
|
// 显示分配角色对话框
|
||||||
const showRoleDialog = async (row: any) => {
|
const showRoleDialog = async (row: any) => {
|
||||||
currentAccountId.value = row.ID
|
currentAccountId.value = row.id
|
||||||
|
currentAccountName.value = row.username
|
||||||
|
currentAccountType.value = row.user_type
|
||||||
selectedRoles.value = []
|
selectedRoles.value = []
|
||||||
|
rolesToAdd.value = []
|
||||||
|
leftRoleFilter.value = ''
|
||||||
|
rightRoleFilter.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 每次打开对话框时重新加载最新的角色列表
|
// 每次打开对话框时重新加载最新的角色列表
|
||||||
await loadAllRoles()
|
await loadAllRoles()
|
||||||
|
|
||||||
// 先加载当前账号的角色,再打开对话框
|
// 先加载当前账号的角色,再打开对话框
|
||||||
const res = await AccountService.getAccountRoles(row.ID)
|
const res = await AccountService.getAccountRoles(row.id)
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
// 提取角色ID数组
|
// 提取角色ID数组
|
||||||
const roles = res.data || []
|
const roles = res.data || []
|
||||||
selectedRoles.value = roles.map((role: any) => role.ID)
|
// 兼容 ID 和 id 两种字段名
|
||||||
|
selectedRoles.value = roles.map((role: any) => role.ID || role.id)
|
||||||
// 数据加载完成后再打开对话框
|
// 数据加载完成后再打开对话框
|
||||||
roleDialogVisible.value = true
|
roleDialogVisible.value = true
|
||||||
}
|
}
|
||||||
@@ -377,19 +595,48 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交分配角色
|
// 批量添加角色
|
||||||
const handleAssignRoles = async () => {
|
const addRoles = async () => {
|
||||||
roleSubmitLoading.value = true
|
if (rolesToAdd.value.length === 0) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await AccountService.assignRolesToAccount(currentAccountId.value, selectedRoles.value)
|
// 所有账号只能分配一个角色
|
||||||
ElMessage.success('分配角色成功')
|
if (rolesToAdd.value.length > 1) {
|
||||||
roleDialogVisible.value = false
|
ElMessage.warning('只能分配一个角色')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新角色会替换之前的角色
|
||||||
|
const newRoles = rolesToAdd.value
|
||||||
|
|
||||||
|
await AccountService.assignRolesToAccount(currentAccountId.value, newRoles)
|
||||||
|
|
||||||
|
selectedRoles.value = newRoles
|
||||||
|
rolesToAdd.value = []
|
||||||
|
|
||||||
|
ElMessage.success('角色分配成功')
|
||||||
// 刷新列表以更新角色显示
|
// 刷新列表以更新角色显示
|
||||||
await getAccountList()
|
await getAccountList()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error('分配角色失败:', error)
|
||||||
} finally {
|
}
|
||||||
roleSubmitLoading.value = false
|
}
|
||||||
|
|
||||||
|
// 移除单个角色
|
||||||
|
const removeSingleRole = async (roleId: number) => {
|
||||||
|
try {
|
||||||
|
// 从已分配列表中移除该角色
|
||||||
|
const newRoles = selectedRoles.value.filter((id) => id !== roleId)
|
||||||
|
await AccountService.assignRolesToAccount(currentAccountId.value, newRoles)
|
||||||
|
|
||||||
|
selectedRoles.value = newRoles
|
||||||
|
|
||||||
|
ElMessage.success('角色移除成功')
|
||||||
|
// 刷新列表以更新角色显示
|
||||||
|
await getAccountList()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('移除角色失败:', error)
|
||||||
|
ElMessage.error('角色移除失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,7 +647,12 @@
|
|||||||
const params = {
|
const params = {
|
||||||
page: pagination.currentPage,
|
page: pagination.currentPage,
|
||||||
pageSize: pagination.pageSize,
|
pageSize: pagination.pageSize,
|
||||||
keyword: formFilters.name || formFilters.phone || undefined
|
username: formFilters.name || undefined,
|
||||||
|
phone: formFilters.phone || undefined,
|
||||||
|
user_type: formFilters.user_type,
|
||||||
|
shop_id: formFilters.shop_id,
|
||||||
|
enterprise_id: formFilters.enterprise_id,
|
||||||
|
status: formFilters.status
|
||||||
}
|
}
|
||||||
const res = await AccountService.getAccounts(params)
|
const res = await AccountService.getAccounts(params)
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
@@ -438,7 +690,31 @@
|
|||||||
{ len: 11, message: '手机号必须为 11 位', trigger: 'blur' },
|
{ len: 11, message: '手机号必须为 11 位', trigger: 'blur' },
|
||||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
|
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
|
||||||
],
|
],
|
||||||
user_type: [{ required: true, message: '请选择账号类型', trigger: 'change' }]
|
user_type: [{ required: true, message: '请选择账号类型', trigger: 'change' }],
|
||||||
|
shop_id: [
|
||||||
|
{
|
||||||
|
validator: (rule: any, value: any, callback: any) => {
|
||||||
|
if (formData.user_type === 3 && !value) {
|
||||||
|
callback(new Error('代理账号必须关联店铺'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: 'change'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
enterprise_id: [
|
||||||
|
{
|
||||||
|
validator: (rule: any, value: any, callback: any) => {
|
||||||
|
if (formData.user_type === 4 && !value) {
|
||||||
|
callback(new Error('企业账号必须关联企业'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: 'change'
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
// 提交表单
|
// 提交表单
|
||||||
@@ -448,17 +724,31 @@
|
|||||||
await formRef.value.validate(async (valid) => {
|
await formRef.value.validate(async (valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
try {
|
try {
|
||||||
|
if (dialogType.value === 'add') {
|
||||||
|
// 创建账号
|
||||||
const data: any = {
|
const data: any = {
|
||||||
username: formData.username,
|
username: formData.username,
|
||||||
phone: formData.phone,
|
phone: formData.phone,
|
||||||
|
password: formData.password,
|
||||||
user_type: formData.user_type
|
user_type: formData.user_type
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dialogType.value === 'add') {
|
// 根据账号类型添加相应的字段
|
||||||
data.password = formData.password
|
if (formData.user_type === 3) {
|
||||||
|
data.shop_id = formData.shop_id
|
||||||
|
} else if (formData.user_type === 4) {
|
||||||
|
data.enterprise_id = formData.enterprise_id
|
||||||
|
}
|
||||||
|
|
||||||
await AccountService.createAccount(data)
|
await AccountService.createAccount(data)
|
||||||
ElMessage.success('添加成功')
|
ElMessage.success('添加成功')
|
||||||
} else {
|
} else {
|
||||||
|
// 编辑账号 - 只提交username和phone
|
||||||
|
const data: any = {
|
||||||
|
username: formData.username,
|
||||||
|
phone: formData.phone
|
||||||
|
}
|
||||||
|
|
||||||
await AccountService.updateAccount(Number(formData.id), data)
|
await AccountService.updateAccount(Number(formData.id), data)
|
||||||
ElMessage.success('更新成功')
|
ElMessage.success('更新成功')
|
||||||
}
|
}
|
||||||
@@ -489,7 +779,7 @@
|
|||||||
// 先更新UI
|
// 先更新UI
|
||||||
row.status = newStatus
|
row.status = newStatus
|
||||||
try {
|
try {
|
||||||
await AccountService.updateAccount(row.ID, { status: newStatus })
|
await AccountService.updateAccountStatus(row.id, newStatus as 0 | 1)
|
||||||
ElMessage.success('状态切换成功')
|
ElMessage.success('状态切换成功')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 切换失败,恢复原状态
|
// 切换失败,恢复原状态
|
||||||
@@ -497,10 +787,226 @@
|
|||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载店铺列表
|
||||||
|
const loadShopList = async (keyword: string = '') => {
|
||||||
|
try {
|
||||||
|
const res = await ShopService.getShops({
|
||||||
|
page: 1,
|
||||||
|
page_size: 20, // 默认加载20条
|
||||||
|
status: 1, // 只加载启用的店铺
|
||||||
|
shop_name: keyword || undefined // 根据店铺名称搜索
|
||||||
|
})
|
||||||
|
if (res.code === 0) {
|
||||||
|
shopList.value = res.data.items || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取店铺列表失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 店铺搜索处理
|
||||||
|
const handleShopSearch = (query: string) => {
|
||||||
|
loadShopList(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载企业列表
|
||||||
|
const loadEnterpriseList = async (keyword: string = '') => {
|
||||||
|
try {
|
||||||
|
const res = await EnterpriseService.getEnterprises({
|
||||||
|
page: 1,
|
||||||
|
page_size: 20, // 默认加载20条
|
||||||
|
status: 1, // 只加载启用的企业
|
||||||
|
enterprise_name: keyword || undefined // 根据企业名称搜索
|
||||||
|
})
|
||||||
|
if (res.code === 0) {
|
||||||
|
enterpriseList.value = res.data.items || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取企业列表失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 企业搜索处理
|
||||||
|
const handleEnterpriseSearch = (query: string) => {
|
||||||
|
loadEnterpriseList(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右键菜单项配置
|
||||||
|
const contextMenuItems = computed((): MenuItemType[] => {
|
||||||
|
const items: MenuItemType[] = []
|
||||||
|
|
||||||
|
if (hasAuth('account:patch_role')) {
|
||||||
|
items.push({ key: 'assignRole', label: '分配角色' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAuth('account:edit')) {
|
||||||
|
items.push({ key: 'edit', label: '编辑' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAuth('account:delete')) {
|
||||||
|
items.push({ key: 'delete', label: '删除' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理表格行右键菜单
|
||||||
|
const handleRowContextMenu = (row: any, column: any, event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
currentRow.value = row
|
||||||
|
contextMenuRef.value?.show(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理右键菜单选择
|
||||||
|
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||||
|
if (!currentRow.value) return
|
||||||
|
|
||||||
|
switch (item.key) {
|
||||||
|
case 'assignRole':
|
||||||
|
showRoleDialog(currentRow.value)
|
||||||
|
break
|
||||||
|
case 'edit':
|
||||||
|
showDialog('edit', currentRow.value)
|
||||||
|
break
|
||||||
|
case 'delete':
|
||||||
|
deleteAccount(currentRow.value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.account-page {
|
.account-page {
|
||||||
// 账号管理页面样式
|
// 账号管理页面样式
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__row.table-row-with-context-menu) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.account-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-transfer-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 20px 0;
|
||||||
|
min-height: 500px;
|
||||||
|
|
||||||
|
.transfer-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 380px;
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-bottom: 1px solid var(--el-border-color);
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px;
|
||||||
|
|
||||||
|
.role-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.role-item {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-color: var(--el-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-checkbox) {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.el-checkbox__label {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.assigned-role-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.role-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0 10px;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,571 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ArtTableFullScreen>
|
|
||||||
<div class="customer-account-page" id="table-full-screen">
|
|
||||||
<!-- 搜索栏 -->
|
|
||||||
<ArtSearchBar
|
|
||||||
v-model:filter="searchForm"
|
|
||||||
:items="searchFormItems"
|
|
||||||
:show-expand="false"
|
|
||||||
@reset="handleReset"
|
|
||||||
@search="handleSearch"
|
|
||||||
></ArtSearchBar>
|
|
||||||
|
|
||||||
<ElCard shadow="never" class="art-table-card">
|
|
||||||
<!-- 表格头部 -->
|
|
||||||
<ArtTableHeader
|
|
||||||
:columnList="columnOptions"
|
|
||||||
v-model:columns="columnChecks"
|
|
||||||
@refresh="handleRefresh"
|
|
||||||
>
|
|
||||||
<template #left>
|
|
||||||
<ElButton @click="showDialog('add')">新增代理商账号</ElButton>
|
|
||||||
</template>
|
|
||||||
</ArtTableHeader>
|
|
||||||
|
|
||||||
<!-- 表格 -->
|
|
||||||
<ArtTable
|
|
||||||
ref="tableRef"
|
|
||||||
row-key="id"
|
|
||||||
:loading="loading"
|
|
||||||
:data="accountList"
|
|
||||||
:currentPage="pagination.page"
|
|
||||||
:pageSize="pagination.pageSize"
|
|
||||||
:total="pagination.total"
|
|
||||||
:marginTop="10"
|
|
||||||
@size-change="handleSizeChange"
|
|
||||||
@current-change="handleCurrentChange"
|
|
||||||
>
|
|
||||||
<template #default>
|
|
||||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
|
||||||
</template>
|
|
||||||
</ArtTable>
|
|
||||||
|
|
||||||
<!-- 新增/编辑对话框 -->
|
|
||||||
<ElDialog
|
|
||||||
v-model="dialogVisible"
|
|
||||||
:title="dialogType === 'add' ? '新增代理商账号' : '编辑账号'"
|
|
||||||
width="500px"
|
|
||||||
>
|
|
||||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="80px">
|
|
||||||
<ElFormItem label="用户名" prop="username">
|
|
||||||
<ElInput
|
|
||||||
v-model="form.username"
|
|
||||||
placeholder="请输入用户名"
|
|
||||||
:disabled="dialogType === 'edit'"
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem label="手机号" prop="phone">
|
|
||||||
<ElInput v-model="form.phone" placeholder="请输入手机号" />
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem v-if="dialogType === 'add'" label="密码" prop="password">
|
|
||||||
<ElInput
|
|
||||||
v-model="form.password"
|
|
||||||
type="password"
|
|
||||||
placeholder="请输入密码(6-20位)"
|
|
||||||
show-password
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem label="店铺" prop="shop_id">
|
|
||||||
<ElSelect
|
|
||||||
v-model="form.shop_id"
|
|
||||||
placeholder="请选择店铺"
|
|
||||||
filterable
|
|
||||||
remote
|
|
||||||
:remote-method="searchShops"
|
|
||||||
:loading="shopLoading"
|
|
||||||
clearable
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<ElOption
|
|
||||||
v-for="shop in shopList"
|
|
||||||
:key="shop.id"
|
|
||||||
:label="shop.shop_name"
|
|
||||||
:value="shop.id"
|
|
||||||
/>
|
|
||||||
</ElSelect>
|
|
||||||
</ElFormItem>
|
|
||||||
</ElForm>
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
|
||||||
<ElButton type="primary" @click="handleSubmit(formRef)" :loading="submitLoading">
|
|
||||||
提交
|
|
||||||
</ElButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
|
|
||||||
<!-- 修改密码对话框 -->
|
|
||||||
<ElDialog v-model="passwordDialogVisible" title="修改密码" width="400px">
|
|
||||||
<ElForm ref="passwordFormRef" :model="passwordForm" :rules="passwordRules">
|
|
||||||
<ElFormItem label="新密码" prop="password">
|
|
||||||
<ElInput
|
|
||||||
v-model="passwordForm.password"
|
|
||||||
type="password"
|
|
||||||
placeholder="请输入新密码(6-20位)"
|
|
||||||
show-password
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
</ElForm>
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<ElButton @click="passwordDialogVisible = false">取消</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="handlePasswordSubmit(passwordFormRef)"
|
|
||||||
:loading="passwordSubmitLoading"
|
|
||||||
>
|
|
||||||
提交
|
|
||||||
</ElButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</ElCard>
|
|
||||||
</div>
|
|
||||||
</ArtTableFullScreen>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { h } from 'vue'
|
|
||||||
import { CustomerAccountService, ShopService } from '@/api/modules'
|
|
||||||
import { ElMessage, ElTag, ElSwitch } from 'element-plus'
|
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
|
||||||
import type { CustomerAccountItem, ShopResponse } from '@/types/api'
|
|
||||||
import type { SearchFormItem } from '@/types'
|
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
|
||||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
|
||||||
|
|
||||||
defineOptions({ name: 'CustomerAccount' })
|
|
||||||
|
|
||||||
const dialogVisible = ref(false)
|
|
||||||
const passwordDialogVisible = ref(false)
|
|
||||||
const loading = ref(false)
|
|
||||||
const submitLoading = ref(false)
|
|
||||||
const passwordSubmitLoading = ref(false)
|
|
||||||
const shopLoading = ref(false)
|
|
||||||
const tableRef = ref()
|
|
||||||
const currentAccountId = ref<number>(0)
|
|
||||||
const shopList = ref<ShopResponse[]>([])
|
|
||||||
|
|
||||||
// 搜索表单初始值
|
|
||||||
const initialSearchState = {
|
|
||||||
username: '',
|
|
||||||
phone: '',
|
|
||||||
user_type: undefined as number | undefined,
|
|
||||||
status: undefined as number | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索表单
|
|
||||||
const searchForm = reactive({ ...initialSearchState })
|
|
||||||
|
|
||||||
// 用户类型选项
|
|
||||||
const userTypeOptions = [
|
|
||||||
{ label: '个人账号', value: 2 },
|
|
||||||
{ label: '代理账号', value: 3 },
|
|
||||||
{ label: '企业账号', value: 4 }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 状态选项
|
|
||||||
const statusOptions = [
|
|
||||||
{ label: '启用', value: 1 },
|
|
||||||
{ label: '禁用', value: 0 }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 搜索表单配置
|
|
||||||
const searchFormItems = computed<SearchFormItem[]>(() => [
|
|
||||||
{
|
|
||||||
label: '用户名',
|
|
||||||
prop: 'username',
|
|
||||||
type: 'input',
|
|
||||||
config: {
|
|
||||||
clearable: true,
|
|
||||||
placeholder: '请输入用户名'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '手机号',
|
|
||||||
prop: 'phone',
|
|
||||||
type: 'input',
|
|
||||||
config: {
|
|
||||||
clearable: true,
|
|
||||||
placeholder: '请输入手机号'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '用户类型',
|
|
||||||
prop: 'user_type',
|
|
||||||
type: 'select',
|
|
||||||
options: userTypeOptions,
|
|
||||||
config: {
|
|
||||||
clearable: true,
|
|
||||||
placeholder: '请选择用户类型'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '状态',
|
|
||||||
prop: 'status',
|
|
||||||
type: 'select',
|
|
||||||
options: statusOptions,
|
|
||||||
config: {
|
|
||||||
clearable: true,
|
|
||||||
placeholder: '请选择状态'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
// 分页
|
|
||||||
const pagination = reactive({
|
|
||||||
page: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
total: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// 列配置
|
|
||||||
const columnOptions = [
|
|
||||||
{ label: 'ID', prop: 'id' },
|
|
||||||
{ label: '用户名', prop: 'username' },
|
|
||||||
{ label: '手机号', prop: 'phone' },
|
|
||||||
{ label: '用户类型', prop: 'user_type_name' },
|
|
||||||
{ label: '店铺名称', prop: 'shop_name' },
|
|
||||||
{ label: '企业名称', prop: 'enterprise_name' },
|
|
||||||
{ label: '状态', prop: 'status' },
|
|
||||||
{ label: '创建时间', prop: 'created_at' },
|
|
||||||
{ label: '操作', prop: 'operation' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const formRef = ref<FormInstance>()
|
|
||||||
const passwordFormRef = ref<FormInstance>()
|
|
||||||
|
|
||||||
const rules = reactive<FormRules>({
|
|
||||||
username: [
|
|
||||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
|
||||||
{ min: 2, max: 50, message: '用户名长度为2-50个字符', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
|
|
||||||
password: [
|
|
||||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
|
||||||
{ min: 6, max: 20, message: '密码长度为6-20位', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
shop_id: [{ required: true, message: '请输入店铺ID', trigger: 'blur' }]
|
|
||||||
})
|
|
||||||
|
|
||||||
const passwordRules = reactive<FormRules>({
|
|
||||||
password: [
|
|
||||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
|
||||||
{ min: 6, max: 20, message: '密码长度为6-20位', trigger: 'blur' }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
const dialogType = ref('add')
|
|
||||||
|
|
||||||
const form = reactive<any>({
|
|
||||||
id: 0,
|
|
||||||
username: '',
|
|
||||||
phone: '',
|
|
||||||
password: '',
|
|
||||||
shop_id: null
|
|
||||||
})
|
|
||||||
|
|
||||||
const passwordForm = reactive({
|
|
||||||
password: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const accountList = ref<CustomerAccountItem[]>([])
|
|
||||||
|
|
||||||
// 动态列配置
|
|
||||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
|
||||||
{
|
|
||||||
prop: 'id',
|
|
||||||
label: 'ID',
|
|
||||||
width: 80
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'username',
|
|
||||||
label: '用户名',
|
|
||||||
minWidth: 120
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'phone',
|
|
||||||
label: '手机号',
|
|
||||||
width: 130
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'user_type_name',
|
|
||||||
label: '用户类型',
|
|
||||||
width: 100,
|
|
||||||
formatter: (row: CustomerAccountItem) => {
|
|
||||||
return h(
|
|
||||||
ElTag,
|
|
||||||
{ type: row.user_type === 3 ? 'primary' : 'success' },
|
|
||||||
() => row.user_type_name
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'shop_name',
|
|
||||||
label: '店铺名称',
|
|
||||||
minWidth: 150,
|
|
||||||
showOverflowTooltip: true,
|
|
||||||
formatter: (row: CustomerAccountItem) => row.shop_name || '-'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'enterprise_name',
|
|
||||||
label: '企业名称',
|
|
||||||
minWidth: 150,
|
|
||||||
showOverflowTooltip: true,
|
|
||||||
formatter: (row: CustomerAccountItem) => row.enterprise_name || '-'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'status',
|
|
||||||
label: '状态',
|
|
||||||
width: 100,
|
|
||||||
formatter: (row: CustomerAccountItem) => {
|
|
||||||
return h(ElSwitch, {
|
|
||||||
modelValue: row.status,
|
|
||||||
activeValue: 1,
|
|
||||||
inactiveValue: 0,
|
|
||||||
activeText: '启用',
|
|
||||||
inactiveText: '禁用',
|
|
||||||
inlinePrompt: true,
|
|
||||||
'onUpdate:modelValue': (val: string | number | boolean) =>
|
|
||||||
handleStatusChange(row, val as number)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'created_at',
|
|
||||||
label: '创建时间',
|
|
||||||
width: 180,
|
|
||||||
formatter: (row: CustomerAccountItem) => formatDateTime(row.created_at)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'operation',
|
|
||||||
label: '操作',
|
|
||||||
width: 160,
|
|
||||||
fixed: 'right',
|
|
||||||
formatter: (row: CustomerAccountItem) => {
|
|
||||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
|
||||||
h(ArtButtonTable, {
|
|
||||||
icon: '',
|
|
||||||
onClick: () => showPasswordDialog(row)
|
|
||||||
}),
|
|
||||||
h(ArtButtonTable, {
|
|
||||||
type: 'edit',
|
|
||||||
onClick: () => showDialog('edit', row)
|
|
||||||
})
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getTableData()
|
|
||||||
loadShopList()
|
|
||||||
})
|
|
||||||
|
|
||||||
// 加载店铺列表(默认加载20条)
|
|
||||||
const loadShopList = async (shopName?: string) => {
|
|
||||||
shopLoading.value = true
|
|
||||||
try {
|
|
||||||
const params: any = {
|
|
||||||
page: 1,
|
|
||||||
pageSize: 20
|
|
||||||
}
|
|
||||||
if (shopName) {
|
|
||||||
params.shop_name = shopName
|
|
||||||
}
|
|
||||||
const res = await ShopService.getShops(params)
|
|
||||||
if (res.code === 0) {
|
|
||||||
shopList.value = res.data.items || []
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取店铺列表失败:', error)
|
|
||||||
} finally {
|
|
||||||
shopLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索店铺
|
|
||||||
const searchShops = (query: string) => {
|
|
||||||
if (query) {
|
|
||||||
loadShopList(query)
|
|
||||||
} else {
|
|
||||||
loadShopList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取客户账号列表
|
|
||||||
const getTableData = async () => {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const params = {
|
|
||||||
page: pagination.page,
|
|
||||||
page_size: pagination.pageSize,
|
|
||||||
username: searchForm.username || undefined,
|
|
||||||
phone: searchForm.phone || undefined,
|
|
||||||
user_type: searchForm.user_type,
|
|
||||||
status: searchForm.status
|
|
||||||
}
|
|
||||||
const res = await CustomerAccountService.getCustomerAccounts(params)
|
|
||||||
if (res.code === 0) {
|
|
||||||
accountList.value = res.data.items || []
|
|
||||||
pagination.total = res.data.total || 0
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置搜索
|
|
||||||
const handleReset = () => {
|
|
||||||
Object.assign(searchForm, { ...initialSearchState })
|
|
||||||
pagination.page = 1
|
|
||||||
getTableData()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索
|
|
||||||
const handleSearch = () => {
|
|
||||||
pagination.page = 1
|
|
||||||
getTableData()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 刷新表格
|
|
||||||
const handleRefresh = () => {
|
|
||||||
getTableData()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理表格分页变化
|
|
||||||
const handleSizeChange = (newPageSize: number) => {
|
|
||||||
pagination.pageSize = newPageSize
|
|
||||||
getTableData()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCurrentChange = (newCurrentPage: number) => {
|
|
||||||
pagination.page = newCurrentPage
|
|
||||||
getTableData()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示新增/编辑对话框
|
|
||||||
const showDialog = (type: string, row?: CustomerAccountItem) => {
|
|
||||||
dialogType.value = type
|
|
||||||
|
|
||||||
if (type === 'edit' && row) {
|
|
||||||
form.id = row.id
|
|
||||||
form.username = row.username
|
|
||||||
form.phone = row.phone
|
|
||||||
form.shop_id = row.shop_id
|
|
||||||
} else {
|
|
||||||
form.id = 0
|
|
||||||
form.username = ''
|
|
||||||
form.phone = ''
|
|
||||||
form.password = ''
|
|
||||||
form.shop_id = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置表单验证状态
|
|
||||||
nextTick(() => {
|
|
||||||
formRef.value?.clearValidate()
|
|
||||||
})
|
|
||||||
|
|
||||||
dialogVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示修改密码对话框
|
|
||||||
const showPasswordDialog = (row: CustomerAccountItem) => {
|
|
||||||
currentAccountId.value = row.id
|
|
||||||
passwordForm.password = ''
|
|
||||||
|
|
||||||
// 重置表单验证状态
|
|
||||||
nextTick(() => {
|
|
||||||
passwordFormRef.value?.clearValidate()
|
|
||||||
})
|
|
||||||
|
|
||||||
passwordDialogVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交表单
|
|
||||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
|
||||||
if (!formEl) return
|
|
||||||
|
|
||||||
await formEl.validate(async (valid) => {
|
|
||||||
if (valid) {
|
|
||||||
submitLoading.value = true
|
|
||||||
try {
|
|
||||||
if (dialogType.value === 'add') {
|
|
||||||
const data = {
|
|
||||||
username: form.username,
|
|
||||||
phone: form.phone,
|
|
||||||
password: form.password,
|
|
||||||
shop_id: form.shop_id
|
|
||||||
}
|
|
||||||
await CustomerAccountService.createCustomerAccount(data)
|
|
||||||
ElMessage.success('新增成功')
|
|
||||||
} else {
|
|
||||||
const data: any = {}
|
|
||||||
if (form.username) data.username = form.username
|
|
||||||
if (form.phone) data.phone = form.phone
|
|
||||||
await CustomerAccountService.updateCustomerAccount(form.id, data)
|
|
||||||
ElMessage.success('修改成功')
|
|
||||||
}
|
|
||||||
|
|
||||||
dialogVisible.value = false
|
|
||||||
formEl.resetFields()
|
|
||||||
getTableData()
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
} finally {
|
|
||||||
submitLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交修改密码
|
|
||||||
const handlePasswordSubmit = async (formEl: FormInstance | undefined) => {
|
|
||||||
if (!formEl) return
|
|
||||||
|
|
||||||
await formEl.validate(async (valid) => {
|
|
||||||
if (valid) {
|
|
||||||
passwordSubmitLoading.value = true
|
|
||||||
try {
|
|
||||||
await CustomerAccountService.updateCustomerAccountPassword(currentAccountId.value, {
|
|
||||||
password: passwordForm.password
|
|
||||||
})
|
|
||||||
ElMessage.success('密码修改成功')
|
|
||||||
passwordDialogVisible.value = false
|
|
||||||
formEl.resetFields()
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
} finally {
|
|
||||||
passwordSubmitLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 状态切换
|
|
||||||
const handleStatusChange = async (row: CustomerAccountItem, newStatus: number) => {
|
|
||||||
const oldStatus = row.status
|
|
||||||
// 先更新UI
|
|
||||||
row.status = newStatus
|
|
||||||
try {
|
|
||||||
await CustomerAccountService.updateCustomerAccountStatus(row.id, {
|
|
||||||
status: newStatus as 0 | 1
|
|
||||||
})
|
|
||||||
ElMessage.success('状态切换成功')
|
|
||||||
} catch (error) {
|
|
||||||
// 切换失败,恢复原状态
|
|
||||||
row.status = oldStatus
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.customer-account-page {
|
|
||||||
// 可以在这里添加客户账号页面特定样式
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -36,11 +36,18 @@
|
|||||||
@refresh="handleRefresh"
|
@refresh="handleRefresh"
|
||||||
>
|
>
|
||||||
<template #left>
|
<template #left>
|
||||||
<ElButton type="primary" @click="showAllocateDialog">授权卡</ElButton>
|
<ElButton
|
||||||
|
type="primary"
|
||||||
|
@click="showAllocateDialog"
|
||||||
|
v-permission="'enterprise_cards:allocate'"
|
||||||
|
>
|
||||||
|
授权卡
|
||||||
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
type="warning"
|
type="warning"
|
||||||
:disabled="selectedCards.length === 0"
|
:disabled="selectedCards.length === 0"
|
||||||
@click="showRecallDialog"
|
@click="showRecallDialog"
|
||||||
|
v-permission="'enterprise_cards:batch_recall'"
|
||||||
>
|
>
|
||||||
批量回收
|
批量回收
|
||||||
</ElButton>
|
</ElButton>
|
||||||
@@ -57,9 +64,13 @@
|
|||||||
:pageSize="pagination.pageSize"
|
:pageSize="pagination.pageSize"
|
||||||
:total="pagination.total"
|
:total="pagination.total"
|
||||||
:marginTop="10"
|
:marginTop="10"
|
||||||
|
:row-class-name="getRowClassName"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
|
@row-contextmenu="handleRowContextMenu"
|
||||||
|
@cell-mouse-enter="handleCellMouseEnter"
|
||||||
|
@cell-mouse-leave="handleCellMouseLeave"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<ElTableColumn type="selection" width="55" />
|
<ElTableColumn type="selection" width="55" />
|
||||||
@@ -67,11 +78,14 @@
|
|||||||
</template>
|
</template>
|
||||||
</ArtTable>
|
</ArtTable>
|
||||||
|
|
||||||
|
<!-- 鼠标悬浮提示 -->
|
||||||
|
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||||
|
|
||||||
<!-- 授权卡对话框 -->
|
<!-- 授权卡对话框 -->
|
||||||
<ElDialog
|
<ElDialog
|
||||||
v-model="allocateDialogVisible"
|
v-model="allocateDialogVisible"
|
||||||
title="授权卡给企业"
|
title="授权卡给企业"
|
||||||
width="85%"
|
width="75%"
|
||||||
@close="handleAllocateDialogClose"
|
@close="handleAllocateDialogClose"
|
||||||
>
|
>
|
||||||
<!-- 搜索过滤条件 -->
|
<!-- 搜索过滤条件 -->
|
||||||
@@ -99,7 +113,7 @@
|
|||||||
@selection-change="handleAvailableCardsSelectionChange"
|
@selection-change="handleAvailableCardsSelectionChange"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<ElTableColumn type="selection" width="55" />
|
<ElTableColumn type="selection" width="55" :selectable="checkCardSelectable" />
|
||||||
<ElTableColumn
|
<ElTableColumn
|
||||||
v-for="col in availableCardColumns"
|
v-for="col in availableCardColumns"
|
||||||
:key="col.prop || col.type"
|
:key="col.prop || col.type"
|
||||||
@@ -175,7 +189,7 @@
|
|||||||
>
|
>
|
||||||
<ElDivider content-position="left">失败项详情</ElDivider>
|
<ElDivider content-position="left">失败项详情</ElDivider>
|
||||||
<ElTable :data="operationResult.failed_items" border max-height="300">
|
<ElTable :data="operationResult.failed_items" border max-height="300">
|
||||||
<ElTableColumn prop="iccid" label="ICCID" width="180" />
|
<ElTableColumn prop="iccid" label="ICCID" width="200" />
|
||||||
<ElTableColumn prop="reason" label="失败原因" />
|
<ElTableColumn prop="reason" label="失败原因" />
|
||||||
</ElTable>
|
</ElTable>
|
||||||
</div>
|
</div>
|
||||||
@@ -202,6 +216,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
|
<!-- 表格行操作右键菜单 -->
|
||||||
|
<ArtMenuRight
|
||||||
|
ref="cardOperationMenuRef"
|
||||||
|
:menu-items="cardOperationMenuItems"
|
||||||
|
:menu-width="140"
|
||||||
|
@select="handleCardOperationMenuSelect"
|
||||||
|
/>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
</div>
|
</div>
|
||||||
</ArtTableFullScreen>
|
</ArtTableFullScreen>
|
||||||
@@ -215,7 +237,12 @@
|
|||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
|
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||||
|
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||||
|
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||||
import { BgColorEnum } from '@/enums/appEnum'
|
import { BgColorEnum } from '@/enums/appEnum'
|
||||||
import type {
|
import type {
|
||||||
@@ -228,8 +255,20 @@
|
|||||||
|
|
||||||
defineOptions({ name: 'EnterpriseCards' })
|
defineOptions({ name: 'EnterpriseCards' })
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 使用表格右键菜单功能
|
||||||
|
const {
|
||||||
|
showContextMenuHint,
|
||||||
|
hintPosition,
|
||||||
|
getRowClassName,
|
||||||
|
handleCellMouseEnter,
|
||||||
|
handleCellMouseLeave
|
||||||
|
} = useTableContextMenu()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const allocateDialogVisible = ref(false)
|
const allocateDialogVisible = ref(false)
|
||||||
const allocateLoading = ref(false)
|
const allocateLoading = ref(false)
|
||||||
@@ -254,6 +293,11 @@
|
|||||||
const availableCardsLoading = ref(false)
|
const availableCardsLoading = ref(false)
|
||||||
const availableCardsList = ref<StandaloneIotCard[]>([])
|
const availableCardsList = ref<StandaloneIotCard[]>([])
|
||||||
const selectedAvailableCards = ref<StandaloneIotCard[]>([])
|
const selectedAvailableCards = ref<StandaloneIotCard[]>([])
|
||||||
|
const allocatedCardIccids = ref<Set<string>>(new Set())
|
||||||
|
|
||||||
|
// 右键菜单相关
|
||||||
|
const cardOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||||
|
const currentOperatingCard = ref<EnterpriseCardItem | null>(null)
|
||||||
|
|
||||||
// 卡搜索表单初始值
|
// 卡搜索表单初始值
|
||||||
const initialCardSearchState = {
|
const initialCardSearchState = {
|
||||||
@@ -274,7 +318,7 @@
|
|||||||
// 搜索表单初始值
|
// 搜索表单初始值
|
||||||
const initialSearchState = {
|
const initialSearchState = {
|
||||||
iccid: '',
|
iccid: '',
|
||||||
device_no: '',
|
virtual_no: '',
|
||||||
carrier_id: undefined as number | undefined,
|
carrier_id: undefined as number | undefined,
|
||||||
status: undefined as number | undefined
|
status: undefined as number | undefined
|
||||||
}
|
}
|
||||||
@@ -310,7 +354,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '设备号',
|
label: '设备号',
|
||||||
prop: 'device_no',
|
prop: 'virtual_no',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
config: {
|
config: {
|
||||||
clearable: true,
|
clearable: true,
|
||||||
@@ -416,15 +460,14 @@
|
|||||||
const columnOptions = [
|
const columnOptions = [
|
||||||
{ label: 'ICCID', prop: 'iccid' },
|
{ label: 'ICCID', prop: 'iccid' },
|
||||||
{ label: '卡接入号', prop: 'msisdn' },
|
{ label: '卡接入号', prop: 'msisdn' },
|
||||||
{ label: '设备号', prop: 'device_no' },
|
{ label: '设备号', prop: 'virtual_no' },
|
||||||
{ label: '运营商ID', prop: 'carrier_id' },
|
{ label: '运营商ID', prop: 'carrier_id' },
|
||||||
{ label: '运营商', prop: 'carrier_name' },
|
{ label: '运营商', prop: 'carrier_name' },
|
||||||
{ label: '套餐名称', prop: 'package_name' },
|
{ label: '套餐名称', prop: 'package_name' },
|
||||||
{ label: '状态', prop: 'status' },
|
{ label: '状态', prop: 'status' },
|
||||||
{ label: '状态名称', prop: 'status_name' },
|
{ label: '状态名称', prop: 'status_name' },
|
||||||
{ label: '网络状态', prop: 'network_status' },
|
{ label: '网络状态', prop: 'network_status' },
|
||||||
{ label: '网络状态名称', prop: 'network_status_name' },
|
{ label: '网络状态名称', prop: 'network_status_name' }
|
||||||
{ label: '操作', prop: 'operation' }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const cardList = ref<EnterpriseCardItem[]>([])
|
const cardList = ref<EnterpriseCardItem[]>([])
|
||||||
@@ -484,7 +527,7 @@
|
|||||||
width: 130
|
width: 130
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'device_no',
|
prop: 'virtual_no',
|
||||||
label: '设备号',
|
label: '设备号',
|
||||||
width: 150
|
width: 150
|
||||||
},
|
},
|
||||||
@@ -530,27 +573,6 @@
|
|||||||
prop: 'network_status_name',
|
prop: 'network_status_name',
|
||||||
label: '网络状态名称',
|
label: '网络状态名称',
|
||||||
width: 130
|
width: 130
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'operation',
|
|
||||||
label: '操作',
|
|
||||||
width: 100,
|
|
||||||
fixed: 'right',
|
|
||||||
formatter: (row: EnterpriseCardItem) => {
|
|
||||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
|
||||||
row.network_status === 0
|
|
||||||
? h(ArtButtonTable, {
|
|
||||||
text: '复机',
|
|
||||||
iconClass: BgColorEnum.SUCCESS,
|
|
||||||
onClick: () => handleResume(row)
|
|
||||||
})
|
|
||||||
: h(ArtButtonTable, {
|
|
||||||
text: '停机',
|
|
||||||
iconClass: BgColorEnum.ERROR,
|
|
||||||
onClick: () => handleSuspend(row)
|
|
||||||
})
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -590,7 +612,7 @@
|
|||||||
page: pagination.page,
|
page: pagination.page,
|
||||||
page_size: pagination.pageSize,
|
page_size: pagination.pageSize,
|
||||||
iccid: searchForm.iccid || undefined,
|
iccid: searchForm.iccid || undefined,
|
||||||
device_no: searchForm.device_no || undefined,
|
virtual_no: searchForm.virtual_no || undefined,
|
||||||
carrier_id: searchForm.carrier_id,
|
carrier_id: searchForm.carrier_id,
|
||||||
status: searchForm.status
|
status: searchForm.status
|
||||||
}
|
}
|
||||||
@@ -703,18 +725,6 @@
|
|||||||
label: '运营商',
|
label: '运营商',
|
||||||
width: 100
|
width: 100
|
||||||
},
|
},
|
||||||
{
|
|
||||||
prop: 'cost_price',
|
|
||||||
label: '成本价',
|
|
||||||
width: 100,
|
|
||||||
formatter: (row: StandaloneIotCard) => `¥${(row.cost_price / 100).toFixed(2)}`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'distribute_price',
|
|
||||||
label: '分销价',
|
|
||||||
width: 100,
|
|
||||||
formatter: (row: StandaloneIotCard) => `¥${(row.distribute_price / 100).toFixed(2)}`
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
label: '状态',
|
label: '状态',
|
||||||
@@ -837,10 +847,17 @@
|
|||||||
getAvailableCardsList()
|
getAvailableCardsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查卡是否可选(已授权的卡不可选)
|
||||||
|
const checkCardSelectable = (row: StandaloneIotCard) => {
|
||||||
|
return !allocatedCardIccids.value.has(row.iccid)
|
||||||
|
}
|
||||||
|
|
||||||
// 显示授权对话框
|
// 显示授权对话框
|
||||||
const showAllocateDialog = () => {
|
const showAllocateDialog = () => {
|
||||||
allocateDialogVisible.value = true
|
allocateDialogVisible.value = true
|
||||||
selectedAvailableCards.value = []
|
selectedAvailableCards.value = []
|
||||||
|
// 收集已授权的卡的ICCID
|
||||||
|
allocatedCardIccids.value = new Set(cardList.value.map((card) => card.iccid))
|
||||||
// 重置搜索条件
|
// 重置搜索条件
|
||||||
Object.assign(cardSearchForm, { ...initialCardSearchState })
|
Object.assign(cardSearchForm, { ...initialCardSearchState })
|
||||||
cardPagination.page = 1
|
cardPagination.page = 1
|
||||||
@@ -942,6 +959,56 @@
|
|||||||
getTableData()
|
getTableData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 右键菜单项配置
|
||||||
|
const cardOperationMenuItems = computed((): MenuItemType[] => {
|
||||||
|
const items: MenuItemType[] = []
|
||||||
|
|
||||||
|
if (!currentOperatingCard.value) return items
|
||||||
|
|
||||||
|
if (currentOperatingCard.value.network_status === 0) {
|
||||||
|
// 停机状态 - 显示复机
|
||||||
|
if (hasAuth('enterprise_cards:resume')) {
|
||||||
|
items.push({
|
||||||
|
key: 'resume',
|
||||||
|
label: '复机'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 开机状态 - 显示停机
|
||||||
|
if (hasAuth('enterprise_cards:suspend')) {
|
||||||
|
items.push({
|
||||||
|
key: 'suspend',
|
||||||
|
label: '停机'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理右键菜单
|
||||||
|
const handleRowContextMenu = (row: EnterpriseCardItem, column: any, event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
currentOperatingCard.value = row
|
||||||
|
nextTick(() => {
|
||||||
|
cardOperationMenuRef.value?.show(event)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理菜单选择
|
||||||
|
const handleCardOperationMenuSelect = (item: MenuItemType) => {
|
||||||
|
if (!currentOperatingCard.value) return
|
||||||
|
|
||||||
|
switch (item.key) {
|
||||||
|
case 'suspend':
|
||||||
|
handleSuspend(currentOperatingCard.value)
|
||||||
|
break
|
||||||
|
case 'resume':
|
||||||
|
handleResume(currentOperatingCard.value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 停机卡
|
// 停机卡
|
||||||
const handleSuspend = (row: EnterpriseCardItem) => {
|
const handleSuspend = (row: EnterpriseCardItem) => {
|
||||||
ElMessageBox.confirm('确定要停机该卡吗?', '停机卡', {
|
ElMessageBox.confirm('确定要停机该卡吗?', '停机卡', {
|
||||||
@@ -1015,4 +1082,8 @@
|
|||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__row.table-row-with-context-menu) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
v-model:filter="searchForm"
|
v-model:filter="searchForm"
|
||||||
:items="searchFormItems"
|
:items="searchFormItems"
|
||||||
:show-expand="false"
|
:show-expand="false"
|
||||||
:label-width="90"
|
label-width="90"
|
||||||
@reset="handleReset"
|
@reset="handleReset"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
></ArtSearchBar>
|
></ArtSearchBar>
|
||||||
@@ -19,7 +19,9 @@
|
|||||||
@refresh="handleRefresh"
|
@refresh="handleRefresh"
|
||||||
>
|
>
|
||||||
<template #left>
|
<template #left>
|
||||||
<ElButton @click="showDialog('add')">新增企业客户</ElButton>
|
<ElButton @click="showDialog('add')" v-permission="'enterprise_customer:add'"
|
||||||
|
>新增企业客户</ElButton
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
</ArtTableHeader>
|
</ArtTableHeader>
|
||||||
|
|
||||||
@@ -33,14 +35,21 @@
|
|||||||
:pageSize="pagination.pageSize"
|
:pageSize="pagination.pageSize"
|
||||||
:total="pagination.total"
|
:total="pagination.total"
|
||||||
:marginTop="10"
|
:marginTop="10"
|
||||||
|
:row-class-name="getRowClassName"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
|
@row-contextmenu="handleRowContextMenu"
|
||||||
|
@cell-mouse-enter="handleCellMouseEnter"
|
||||||
|
@cell-mouse-leave="handleCellMouseLeave"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||||
</template>
|
</template>
|
||||||
</ArtTable>
|
</ArtTable>
|
||||||
|
|
||||||
|
<!-- 鼠标悬浮提示 -->
|
||||||
|
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||||
|
|
||||||
<!-- 新增/编辑对话框 -->
|
<!-- 新增/编辑对话框 -->
|
||||||
<ElDialog
|
<ElDialog
|
||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
@@ -78,41 +87,36 @@
|
|||||||
</ElRow>
|
</ElRow>
|
||||||
<ElRow :gutter="20">
|
<ElRow :gutter="20">
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
<ElFormItem label="省份" prop="province">
|
<ElFormItem label="所在地区" prop="region">
|
||||||
<ElInput v-model="form.province" placeholder="请输入省份" />
|
<ElCascader
|
||||||
</ElFormItem>
|
v-model="form.region"
|
||||||
</ElCol>
|
:options="regionData"
|
||||||
<ElCol :span="12">
|
placeholder="请选择省/市/区"
|
||||||
<ElFormItem label="城市" prop="city">
|
|
||||||
<ElInput v-model="form.city" placeholder="请输入城市" />
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
</ElRow>
|
|
||||||
<ElRow :gutter="20">
|
|
||||||
<ElCol :span="12">
|
|
||||||
<ElFormItem label="区县" prop="district">
|
|
||||||
<ElInput v-model="form.district" placeholder="请输入区县" />
|
|
||||||
</ElFormItem>
|
|
||||||
</ElCol>
|
|
||||||
<ElCol :span="12">
|
|
||||||
<ElFormItem label="归属店铺" prop="owner_shop_id">
|
|
||||||
<ElSelect
|
|
||||||
v-model="form.owner_shop_id"
|
|
||||||
placeholder="请选择店铺"
|
|
||||||
filterable
|
|
||||||
remote
|
|
||||||
:remote-method="searchShops"
|
|
||||||
:loading="shopLoading"
|
|
||||||
clearable
|
clearable
|
||||||
|
filterable
|
||||||
|
style="width: 100%"
|
||||||
|
@change="handleRegionChange"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<!-- 只有非代理账号才显示归属店铺选择 -->
|
||||||
|
<ElCol :span="12" v-if="!isAgentAccount">
|
||||||
|
<ElFormItem label="归属店铺" prop="owner_shop_id">
|
||||||
|
<ElTreeSelect
|
||||||
|
v-model="form.owner_shop_id"
|
||||||
|
:data="shopTreeData"
|
||||||
|
placeholder="请选择归属店铺"
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
check-strictly
|
||||||
|
:render-after-expand="false"
|
||||||
|
:props="{
|
||||||
|
label: 'shop_name',
|
||||||
|
value: 'id',
|
||||||
|
children: 'children'
|
||||||
|
}"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
|
||||||
<ElOption
|
|
||||||
v-for="shop in shopList"
|
|
||||||
:key="shop.id"
|
|
||||||
:label="shop.shop_name"
|
|
||||||
:value="shop.id"
|
|
||||||
/>
|
/>
|
||||||
</ElSelect>
|
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
</ElRow>
|
</ElRow>
|
||||||
@@ -193,6 +197,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
|
<!-- 企业客户操作右键菜单 -->
|
||||||
|
<ArtMenuRight
|
||||||
|
ref="enterpriseOperationMenuRef"
|
||||||
|
:menu-items="enterpriseOperationMenuItems"
|
||||||
|
:menu-width="140"
|
||||||
|
@select="handleEnterpriseOperationMenuSelect"
|
||||||
|
/>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
</div>
|
</div>
|
||||||
</ArtTableFullScreen>
|
</ArtTableFullScreen>
|
||||||
@@ -202,19 +214,40 @@
|
|||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { EnterpriseService, ShopService } from '@/api/modules'
|
import { EnterpriseService, ShopService } from '@/api/modules'
|
||||||
import { ElMessage, ElMessageBox, ElTag, ElSwitch } from 'element-plus'
|
import { ElMessage, ElSwitch, ElCascader, ElTreeSelect } from 'element-plus'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import type { EnterpriseItem, ShopResponse } from '@/types/api'
|
import type { EnterpriseItem, ShopResponse } from '@/types/api'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||||
|
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||||
|
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||||
|
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import { BgColorEnum } from '@/enums/appEnum'
|
import { regionData } from '@/utils/constants/regionData'
|
||||||
|
|
||||||
defineOptions({ name: 'EnterpriseCustomer' })
|
defineOptions({ name: 'EnterpriseCustomer' })
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 使用表格右键菜单功能
|
||||||
|
const {
|
||||||
|
showContextMenuHint,
|
||||||
|
hintPosition,
|
||||||
|
getRowClassName,
|
||||||
|
handleCellMouseEnter,
|
||||||
|
handleCellMouseLeave
|
||||||
|
} = useTableContextMenu()
|
||||||
|
|
||||||
|
// 判断是否是代理账号 (user_type === 3)
|
||||||
|
const isAgentAccount = computed(() => userStore.info.user_type === 3)
|
||||||
|
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const passwordDialogVisible = ref(false)
|
const passwordDialogVisible = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -223,7 +256,11 @@
|
|||||||
const shopLoading = ref(false)
|
const shopLoading = ref(false)
|
||||||
const tableRef = ref()
|
const tableRef = ref()
|
||||||
const currentEnterpriseId = ref<number>(0)
|
const currentEnterpriseId = ref<number>(0)
|
||||||
const shopList = ref<ShopResponse[]>([])
|
const shopTreeData = ref<ShopResponse[]>([])
|
||||||
|
|
||||||
|
// 右键菜单
|
||||||
|
const enterpriseOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||||
|
const currentOperatingEnterprise = ref<EnterpriseItem | null>(null)
|
||||||
|
|
||||||
// 搜索表单初始值
|
// 搜索表单初始值
|
||||||
const initialSearchState = {
|
const initialSearchState = {
|
||||||
@@ -346,6 +383,7 @@
|
|||||||
enterprise_name: '',
|
enterprise_name: '',
|
||||||
business_license: '',
|
business_license: '',
|
||||||
legal_person: '',
|
legal_person: '',
|
||||||
|
region: [] as string[],
|
||||||
province: '',
|
province: '',
|
||||||
city: '',
|
city: '',
|
||||||
district: '',
|
district: '',
|
||||||
@@ -425,6 +463,7 @@
|
|||||||
activeText: '启用',
|
activeText: '启用',
|
||||||
inactiveText: '禁用',
|
inactiveText: '禁用',
|
||||||
inlinePrompt: true,
|
inlinePrompt: true,
|
||||||
|
disabled: !hasAuth('enterprise_customer:status'),
|
||||||
'onUpdate:modelValue': (val: string | number | boolean) =>
|
'onUpdate:modelValue': (val: string | number | boolean) =>
|
||||||
handleStatusChange(row, val as number)
|
handleStatusChange(row, val as number)
|
||||||
})
|
})
|
||||||
@@ -439,49 +478,55 @@
|
|||||||
{
|
{
|
||||||
prop: 'operation',
|
prop: 'operation',
|
||||||
label: '操作',
|
label: '操作',
|
||||||
width: 260,
|
width: 190,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
formatter: (row: EnterpriseItem) => {
|
formatter: (row: EnterpriseItem) => {
|
||||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
const buttons = []
|
||||||
|
|
||||||
|
if (hasAuth('enterprise_customer:look_customer')) {
|
||||||
|
buttons.push(
|
||||||
h(ArtButtonTable, {
|
h(ArtButtonTable, {
|
||||||
text: '编辑',
|
text: '账号列表',
|
||||||
iconClass: BgColorEnum.SECONDARY,
|
onClick: () => viewCustomerAccounts(row)
|
||||||
onClick: () => showDialog('edit', row)
|
})
|
||||||
}),
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAuth('enterprise_customer:card_authorization')) {
|
||||||
|
buttons.push(
|
||||||
h(ArtButtonTable, {
|
h(ArtButtonTable, {
|
||||||
text: '卡授权',
|
text: '卡授权',
|
||||||
iconClass: BgColorEnum.PRIMARY,
|
|
||||||
onClick: () => manageCards(row)
|
onClick: () => manageCards(row)
|
||||||
}),
|
|
||||||
h(ArtButtonTable, {
|
|
||||||
text: '修改密码',
|
|
||||||
iconClass: BgColorEnum.WARNING,
|
|
||||||
onClick: () => showPasswordDialog(row)
|
|
||||||
})
|
})
|
||||||
])
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getTableData()
|
getTableData()
|
||||||
|
// 只有非代理账号才需要加载店铺列表
|
||||||
|
if (!isAgentAccount.value) {
|
||||||
loadShopList()
|
loadShopList()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 加载店铺列表(默认加载20条)
|
// 加载店铺列表(获取所有店铺构建树形结构)
|
||||||
const loadShopList = async (shopName?: string) => {
|
const loadShopList = async () => {
|
||||||
shopLoading.value = true
|
shopLoading.value = true
|
||||||
try {
|
try {
|
||||||
const params: any = {
|
const params: any = {
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 20
|
page_size: 9999 // 获取所有数据用于构建树形结构
|
||||||
}
|
|
||||||
if (shopName) {
|
|
||||||
params.shop_name = shopName
|
|
||||||
}
|
}
|
||||||
const res = await ShopService.getShops(params)
|
const res = await ShopService.getShops(params)
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
shopList.value = res.data.items || []
|
const items = res.data.items || []
|
||||||
|
// 构建树形数据
|
||||||
|
shopTreeData.value = buildTreeData(items)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取店铺列表失败:', error)
|
console.error('获取店铺列表失败:', error)
|
||||||
@@ -490,13 +535,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索店铺
|
// 构建树形数据
|
||||||
const searchShops = (query: string) => {
|
const buildTreeData = (items: ShopResponse[]) => {
|
||||||
if (query) {
|
const map = new Map<number, ShopResponse & { children?: ShopResponse[] }>()
|
||||||
loadShopList(query)
|
const tree: ShopResponse[] = []
|
||||||
|
|
||||||
|
// 先将所有项放入 map
|
||||||
|
items.forEach((item) => {
|
||||||
|
map.set(item.id, { ...item, children: [] })
|
||||||
|
})
|
||||||
|
|
||||||
|
// 构建树形结构
|
||||||
|
items.forEach((item) => {
|
||||||
|
const node = map.get(item.id)!
|
||||||
|
if (item.parent_id && map.has(item.parent_id)) {
|
||||||
|
// 有父节点,添加到父节点的 children 中
|
||||||
|
const parent = map.get(item.parent_id)!
|
||||||
|
if (!parent.children) parent.children = []
|
||||||
|
parent.children.push(node)
|
||||||
} else {
|
} else {
|
||||||
loadShopList()
|
// 没有父节点或父节点不存在,作为根节点
|
||||||
|
tree.push(node)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return tree
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取企业客户列表
|
// 获取企业客户列表
|
||||||
@@ -552,6 +615,19 @@
|
|||||||
getTableData()
|
getTableData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理地区选择变化
|
||||||
|
const handleRegionChange = (value: string[]) => {
|
||||||
|
if (value && value.length === 3) {
|
||||||
|
form.province = value[0]
|
||||||
|
form.city = value[1]
|
||||||
|
form.district = value[2]
|
||||||
|
} else {
|
||||||
|
form.province = ''
|
||||||
|
form.city = ''
|
||||||
|
form.district = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const dialogType = ref('add')
|
const dialogType = ref('add')
|
||||||
|
|
||||||
// 显示新增/编辑对话框
|
// 显示新增/编辑对话框
|
||||||
@@ -567,6 +643,12 @@
|
|||||||
form.province = row.province
|
form.province = row.province
|
||||||
form.city = row.city
|
form.city = row.city
|
||||||
form.district = row.district
|
form.district = row.district
|
||||||
|
// 设置地区级联选择器的值
|
||||||
|
if (row.province && row.city && row.district) {
|
||||||
|
form.region = [row.province, row.city, row.district]
|
||||||
|
} else {
|
||||||
|
form.region = []
|
||||||
|
}
|
||||||
form.address = row.address
|
form.address = row.address
|
||||||
form.contact_name = row.contact_name
|
form.contact_name = row.contact_name
|
||||||
form.contact_phone = row.contact_phone
|
form.contact_phone = row.contact_phone
|
||||||
@@ -578,6 +660,7 @@
|
|||||||
form.enterprise_name = ''
|
form.enterprise_name = ''
|
||||||
form.business_license = ''
|
form.business_license = ''
|
||||||
form.legal_person = ''
|
form.legal_person = ''
|
||||||
|
form.region = []
|
||||||
form.province = ''
|
form.province = ''
|
||||||
form.city = ''
|
form.city = ''
|
||||||
form.district = ''
|
form.district = ''
|
||||||
@@ -586,7 +669,8 @@
|
|||||||
form.contact_phone = ''
|
form.contact_phone = ''
|
||||||
form.login_phone = ''
|
form.login_phone = ''
|
||||||
form.password = ''
|
form.password = ''
|
||||||
form.owner_shop_id = null
|
// 如果是代理账号,自动设置归属店铺为当前登录用户的店铺
|
||||||
|
form.owner_shop_id = isAgentAccount.value ? userStore.info.shop_id : null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置表单验证状态
|
// 重置表单验证状态
|
||||||
@@ -710,6 +794,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查看客户账号
|
||||||
|
const viewCustomerAccounts = (row: EnterpriseItem) => {
|
||||||
|
router.push({
|
||||||
|
path: `/account-management/enterprise-customer/customer-accounts/${row.id}`,
|
||||||
|
query: { type: 'enterprise' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 卡管理
|
// 卡管理
|
||||||
const manageCards = (row: EnterpriseItem) => {
|
const manageCards = (row: EnterpriseItem) => {
|
||||||
router.push({
|
router.push({
|
||||||
@@ -717,10 +809,61 @@
|
|||||||
query: { id: row.id }
|
query: { id: row.id }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 企业客户操作菜单项配置
|
||||||
|
const enterpriseOperationMenuItems = computed((): MenuItemType[] => {
|
||||||
|
const items: MenuItemType[] = []
|
||||||
|
|
||||||
|
if (hasAuth('enterprise_customer:edit')) {
|
||||||
|
items.push({
|
||||||
|
key: 'edit',
|
||||||
|
label: '编辑'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAuth('enterprise_customer:update_pwd')) {
|
||||||
|
items.push({
|
||||||
|
key: 'updatePassword',
|
||||||
|
label: '修改密码'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
})
|
||||||
|
|
||||||
|
// 显示企业客户操作右键菜单
|
||||||
|
const showEnterpriseOperationMenu = (e: MouseEvent, row: EnterpriseItem) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
currentOperatingEnterprise.value = row
|
||||||
|
enterpriseOperationMenuRef.value?.show(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理企业客户操作菜单选择
|
||||||
|
const handleEnterpriseOperationMenuSelect = (item: MenuItemType) => {
|
||||||
|
if (!currentOperatingEnterprise.value) return
|
||||||
|
|
||||||
|
switch (item.key) {
|
||||||
|
case 'edit':
|
||||||
|
showDialog('edit', currentOperatingEnterprise.value)
|
||||||
|
break
|
||||||
|
case 'updatePassword':
|
||||||
|
showPasswordDialog(currentOperatingEnterprise.value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理表格行右键菜单
|
||||||
|
const handleRowContextMenu = (row: EnterpriseItem, column: any, event: MouseEvent) => {
|
||||||
|
// 如果用户有编辑或修改密码权限,显示右键菜单
|
||||||
|
if (hasAuth('enterprise_customer:edit') || hasAuth('enterprise_customer:update_pwd')) {
|
||||||
|
showEnterpriseOperationMenu(event, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style scoped lang="scss">
|
||||||
.enterprise-customer-page {
|
:deep(.el-table__row.table-row-with-context-menu) {
|
||||||
// 可以在这里添加企业客户页面特定样式
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
@refresh="handleRefresh"
|
@refresh="handleRefresh"
|
||||||
>
|
>
|
||||||
<template #left>
|
<template #left>
|
||||||
<ElButton @click="showDialog('add')">新增平台账号</ElButton>
|
<ElButton @click="showDialog('add')" v-permission="'platform_account:add'">新增平台账号</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</ArtTableHeader>
|
</ArtTableHeader>
|
||||||
|
|
||||||
@@ -45,9 +45,9 @@
|
|||||||
<ElDialog
|
<ElDialog
|
||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
:title="dialogType === 'add' ? '新增平台账号' : '编辑平台账号'"
|
:title="dialogType === 'add' ? '新增平台账号' : '编辑平台账号'"
|
||||||
width="500px"
|
width="30%"
|
||||||
>
|
>
|
||||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="80px">
|
||||||
<ElFormItem label="账号名称" prop="username">
|
<ElFormItem label="账号名称" prop="username">
|
||||||
<ElInput
|
<ElInput
|
||||||
v-model="formData.username"
|
v-model="formData.username"
|
||||||
@@ -79,21 +79,41 @@
|
|||||||
<ElOption label="企业账号" :value="4" />
|
<ElOption label="企业账号" :value="4" />
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem v-if="formData.user_type === 3" label="关联店铺ID" prop="shop_id">
|
<ElFormItem v-if="formData.user_type === 3" label="关联店铺" prop="shop_id">
|
||||||
<ElInputNumber
|
<ElSelect
|
||||||
v-model="formData.shop_id"
|
v-model="formData.shop_id"
|
||||||
:min="1"
|
placeholder="请输入店铺名称搜索"
|
||||||
placeholder="请输入店铺ID"
|
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
:remote-method="handleShopSearch"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<ElOption
|
||||||
|
v-for="shop in shopList"
|
||||||
|
:key="shop.id"
|
||||||
|
:label="shop.shop_name"
|
||||||
|
:value="shop.id"
|
||||||
/>
|
/>
|
||||||
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem v-if="formData.user_type === 4" label="关联企业ID" prop="enterprise_id">
|
<ElFormItem v-if="formData.user_type === 4" label="关联企业" prop="enterprise_id">
|
||||||
<ElInputNumber
|
<ElSelect
|
||||||
v-model="formData.enterprise_id"
|
v-model="formData.enterprise_id"
|
||||||
:min="1"
|
placeholder="请输入企业名称搜索"
|
||||||
placeholder="请输入企业ID"
|
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
:remote-method="handleEnterpriseSearch"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<ElOption
|
||||||
|
v-for="enterprise in enterpriseList"
|
||||||
|
:key="enterprise.id"
|
||||||
|
:label="enterprise.enterprise_name"
|
||||||
|
:value="enterprise.id"
|
||||||
/>
|
/>
|
||||||
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem v-if="dialogType === 'edit'" label="状态">
|
<ElFormItem v-if="dialogType === 'edit'" label="状态">
|
||||||
<ElSwitch
|
<ElSwitch
|
||||||
@@ -114,18 +134,107 @@
|
|||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
<!-- 分配角色对话框 -->
|
<!-- 分配角色对话框 -->
|
||||||
<ElDialog v-model="roleDialogVisible" title="分配角色" width="500px">
|
<ElDialog v-model="roleDialogVisible" width="900px">
|
||||||
<ElCheckboxGroup v-model="selectedRoles">
|
<template #header>
|
||||||
<div v-for="role in allRoles" :key="role.ID" style="margin-bottom: 12px">
|
<div class="dialog-header">
|
||||||
<ElCheckbox :label="role.ID">{{ role.role_name }}</ElCheckbox>
|
<span class="dialog-title">分配角色</span>
|
||||||
|
<div class="account-info">
|
||||||
|
<span class="account-name">{{ currentAccountName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="role-transfer-container">
|
||||||
|
<!-- 左侧:可分配角色列表 -->
|
||||||
|
<div class="transfer-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span class="panel-title">可分配角色</span>
|
||||||
|
<ElInput
|
||||||
|
v-model="leftRoleFilter"
|
||||||
|
placeholder="搜索角色"
|
||||||
|
clearable
|
||||||
|
size="small"
|
||||||
|
style="width: 180px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<ElCheckboxGroup v-model="rolesToAdd" class="role-list">
|
||||||
|
<div
|
||||||
|
v-for="role in filteredAvailableRoles"
|
||||||
|
:key="role.ID"
|
||||||
|
class="role-item"
|
||||||
|
>
|
||||||
|
<ElCheckbox :label="role.ID" :disabled="selectedRoles.includes(role.ID)">
|
||||||
|
<span class="role-info">
|
||||||
|
<span>{{ role.role_name }}</span>
|
||||||
|
<ElTag
|
||||||
|
:type="role.role_type === 1 ? 'primary' : 'success'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
|
||||||
|
</ElTag>
|
||||||
|
</span>
|
||||||
|
</ElCheckbox>
|
||||||
</div>
|
</div>
|
||||||
</ElCheckboxGroup>
|
</ElCheckboxGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 中间:操作按钮 -->
|
||||||
|
<div class="transfer-buttons">
|
||||||
|
<ElButton
|
||||||
|
type="primary"
|
||||||
|
:icon="'ArrowRight'"
|
||||||
|
@click="addRoles"
|
||||||
|
:disabled="rolesToAdd.length === 0"
|
||||||
|
>
|
||||||
|
添加
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:已分配角色列表 -->
|
||||||
|
<div class="transfer-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span class="panel-title">已分配角色</span>
|
||||||
|
<ElInput
|
||||||
|
v-model="rightRoleFilter"
|
||||||
|
placeholder="搜索角色"
|
||||||
|
clearable
|
||||||
|
size="small"
|
||||||
|
style="width: 180px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="role-list">
|
||||||
|
<div
|
||||||
|
v-for="role in filteredAssignedRoles"
|
||||||
|
:key="role.ID"
|
||||||
|
class="role-item assigned-role-item"
|
||||||
|
>
|
||||||
|
<span class="role-info">
|
||||||
|
<span>{{ role.role_name }}</span>
|
||||||
|
<ElTag
|
||||||
|
:type="role.role_type === 1 ? 'primary' : 'success'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
|
||||||
|
</ElTag>
|
||||||
|
</span>
|
||||||
|
<ElButton
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
link
|
||||||
|
@click="removeSingleRole(role.ID)"
|
||||||
|
>
|
||||||
|
移除
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<ElButton @click="roleDialogVisible = false">取消</ElButton>
|
<ElButton @click="roleDialogVisible = false">关闭</ElButton>
|
||||||
<ElButton type="primary" @click="handleAssignRoles" :loading="roleSubmitLoading"
|
|
||||||
>提交</ElButton
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
@@ -164,15 +273,18 @@
|
|||||||
import { FormInstance, ElMessage, ElMessageBox, ElTag, ElSwitch } from 'element-plus'
|
import { FormInstance, ElMessage, ElMessageBox, ElTag, ElSwitch } from 'element-plus'
|
||||||
import type { FormRules } from 'element-plus'
|
import type { FormRules } from 'element-plus'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||||
import { PlatformAccountService, RoleService } from '@/api/modules'
|
import { AccountService, RoleService, ShopService, EnterpriseService } from '@/api/modules'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import type { PlatformRole, PlatformAccountResponse } from '@/types/api'
|
import type { PlatformRole, PlatformAccount } from '@/types/api'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
|
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
|
||||||
|
|
||||||
defineOptions({ name: 'PlatformAccount' }) // 定义组件名称,用于 KeepAlive 缓存控制
|
defineOptions({ name: 'PlatformAccount' }) // 定义组件名称,用于 KeepAlive 缓存控制
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
|
||||||
const dialogType = ref('add')
|
const dialogType = ref('add')
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const roleDialogVisible = ref(false)
|
const roleDialogVisible = ref(false)
|
||||||
@@ -182,19 +294,30 @@
|
|||||||
const roleSubmitLoading = ref(false)
|
const roleSubmitLoading = ref(false)
|
||||||
const passwordSubmitLoading = ref(false)
|
const passwordSubmitLoading = ref(false)
|
||||||
const currentAccountId = ref<number>(0)
|
const currentAccountId = ref<number>(0)
|
||||||
|
const currentAccountName = ref<string>('')
|
||||||
const selectedRoles = ref<number[]>([])
|
const selectedRoles = ref<number[]>([])
|
||||||
const allRoles = ref<PlatformRole[]>([])
|
const allRoles = ref<PlatformRole[]>([])
|
||||||
|
const rolesToAdd = ref<number[]>([])
|
||||||
|
const leftRoleFilter = ref('')
|
||||||
|
const rightRoleFilter = ref('')
|
||||||
|
|
||||||
// 定义表单搜索初始值
|
// 定义表单搜索初始值
|
||||||
const initialSearchState = {
|
const initialSearchState = {
|
||||||
username: '',
|
username: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
|
user_type: undefined as number | undefined,
|
||||||
|
shop_id: undefined as number | undefined,
|
||||||
|
enterprise_id: undefined as number | undefined,
|
||||||
status: undefined as number | undefined
|
status: undefined as number | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应式表单数据
|
// 响应式表单数据
|
||||||
const searchForm = reactive({ ...initialSearchState })
|
const searchForm = reactive({ ...initialSearchState })
|
||||||
|
|
||||||
|
// 店铺和企业列表
|
||||||
|
const shopList = ref<any[]>([])
|
||||||
|
const enterpriseList = ref<any[]>([])
|
||||||
|
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
@@ -202,7 +325,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 表格数据
|
// 表格数据
|
||||||
const tableData = ref<PlatformAccountResponse[]>([])
|
const tableData = ref<PlatformAccount[]>([])
|
||||||
|
|
||||||
// 表格实例引用
|
// 表格实例引用
|
||||||
const tableRef = ref()
|
const tableRef = ref()
|
||||||
@@ -225,7 +348,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 表单配置项
|
// 表单配置项
|
||||||
const searchFormItems: SearchFormItem[] = [
|
const searchFormItems = computed<SearchFormItem[]>(() => [
|
||||||
{
|
{
|
||||||
label: '账号名称',
|
label: '账号名称',
|
||||||
prop: 'username',
|
prop: 'username',
|
||||||
@@ -244,6 +367,35 @@
|
|||||||
placeholder: '请输入手机号'
|
placeholder: '请输入手机号'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '账号类型',
|
||||||
|
prop: 'user_type',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: '超级管理员', value: 1 },
|
||||||
|
{ label: '平台用户', value: 2 },
|
||||||
|
{ label: '代理账号', value: 3 },
|
||||||
|
{ label: '企业账号', value: 4 }
|
||||||
|
],
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择账号类型'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '关联店铺',
|
||||||
|
prop: 'shop_id',
|
||||||
|
type: 'select',
|
||||||
|
options: shopList.value.map((shop) => ({
|
||||||
|
label: shop.shop_name,
|
||||||
|
value: shop.id
|
||||||
|
})),
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
filterable: true,
|
||||||
|
placeholder: '请选择店铺'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '状态',
|
label: '状态',
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
@@ -254,21 +406,23 @@
|
|||||||
placeholder: '请选择状态'
|
placeholder: '请选择状态'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
])
|
||||||
|
|
||||||
// 列配置
|
// 列配置
|
||||||
const columnOptions = [
|
const columnOptions = [
|
||||||
{ label: 'ID', prop: 'ID' },
|
{ label: 'ID', prop: 'id' },
|
||||||
{ label: '账号名称', prop: 'username' },
|
{ label: '账号名称', prop: 'username' },
|
||||||
{ label: '手机号', prop: 'phone' },
|
{ label: '手机号', prop: 'phone' },
|
||||||
{ label: '账号类型', prop: 'user_type' },
|
{ label: '账号类型', prop: 'user_type' },
|
||||||
|
{ label: '店铺名称', prop: 'shop_name' },
|
||||||
|
{ label: '企业名称', prop: 'enterprise_name' },
|
||||||
{ label: '状态', prop: 'status' },
|
{ label: '状态', prop: 'status' },
|
||||||
{ label: '创建时间', prop: 'CreatedAt' },
|
{ label: '创建时间', prop: 'created_at' },
|
||||||
{ label: '操作', prop: 'operation' }
|
{ label: '操作', prop: 'operation' }
|
||||||
]
|
]
|
||||||
|
|
||||||
// 显示对话框
|
// 显示对话框
|
||||||
const showDialog = (type: string, row?: PlatformAccountResponse) => {
|
const showDialog = (type: string, row?: PlatformAccount) => {
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
dialogType.value = type
|
dialogType.value = type
|
||||||
|
|
||||||
@@ -278,13 +432,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'edit' && row) {
|
if (type === 'edit' && row) {
|
||||||
formData.id = row.ID
|
formData.id = row.id
|
||||||
formData.username = row.username
|
formData.username = row.username
|
||||||
formData.phone = row.phone
|
formData.phone = row.phone
|
||||||
formData.user_type = row.user_type
|
formData.user_type = row.user_type
|
||||||
formData.enterprise_id = row.enterprise_id || null
|
formData.enterprise_id = row.enterprise_id || null
|
||||||
formData.shop_id = row.shop_id || null
|
formData.shop_id = row.shop_id || null
|
||||||
formData.status = row.status
|
formData.status = row.status as unknown as CommonStatus
|
||||||
formData.password = ''
|
formData.password = ''
|
||||||
} else {
|
} else {
|
||||||
formData.id = 0
|
formData.id = 0
|
||||||
@@ -299,7 +453,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 删除账号
|
// 删除账号
|
||||||
const deleteAccount = (row: PlatformAccountResponse) => {
|
const deleteAccount = (row: PlatformAccount) => {
|
||||||
ElMessageBox.confirm(`确定要删除平台账号 ${row.username} 吗?`, '删除平台账号', {
|
ElMessageBox.confirm(`确定要删除平台账号 ${row.username} 吗?`, '删除平台账号', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
@@ -307,7 +461,7 @@
|
|||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
try {
|
try {
|
||||||
await PlatformAccountService.deletePlatformAccount(row.ID)
|
await AccountService.deleteAccount(row.id)
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
getAccountList()
|
getAccountList()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -320,8 +474,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 显示修改密码对话框
|
// 显示修改密码对话框
|
||||||
const showPasswordDialog = (row: PlatformAccountResponse) => {
|
const showPasswordDialog = (row: PlatformAccount) => {
|
||||||
currentAccountId.value = row.ID
|
currentAccountId.value = row.id
|
||||||
passwordForm.new_password = ''
|
passwordForm.new_password = ''
|
||||||
passwordDialogVisible.value = true
|
passwordDialogVisible.value = true
|
||||||
if (passwordFormRef.value) {
|
if (passwordFormRef.value) {
|
||||||
@@ -332,21 +486,25 @@
|
|||||||
// 动态列配置
|
// 动态列配置
|
||||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||||
{
|
{
|
||||||
prop: 'ID',
|
prop: 'id',
|
||||||
label: 'ID'
|
label: 'id',
|
||||||
|
width: 80
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'username',
|
prop: 'username',
|
||||||
label: '账号名称'
|
label: '账号名称',
|
||||||
|
minWidth: 120
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'phone',
|
prop: 'phone',
|
||||||
label: '手机号'
|
label: '手机号',
|
||||||
|
width: 130
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'user_type',
|
prop: 'user_type',
|
||||||
label: '账号类型',
|
label: '账号类型',
|
||||||
formatter: (row: PlatformAccountResponse) => {
|
width: 120,
|
||||||
|
formatter: (row: PlatformAccount) => {
|
||||||
const typeMap: Record<number, string> = {
|
const typeMap: Record<number, string> = {
|
||||||
1: '超级管理员',
|
1: '超级管理员',
|
||||||
2: '平台用户',
|
2: '平台用户',
|
||||||
@@ -356,10 +514,27 @@
|
|||||||
return typeMap[row.user_type] || '-'
|
return typeMap[row.user_type] || '-'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prop: 'shop_name',
|
||||||
|
label: '店铺名称',
|
||||||
|
minWidth: 150,
|
||||||
|
formatter: (row: PlatformAccount) => {
|
||||||
|
return row.shop_name || '-'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'enterprise_name',
|
||||||
|
label: '企业名称',
|
||||||
|
minWidth: 150,
|
||||||
|
formatter: (row: PlatformAccount) => {
|
||||||
|
return row.enterprise_name || '-'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
label: '状态',
|
label: '状态',
|
||||||
formatter: (row: PlatformAccountResponse) => {
|
width: 100,
|
||||||
|
formatter: (row: PlatformAccount) => {
|
||||||
return h(ElSwitch, {
|
return h(ElSwitch, {
|
||||||
modelValue: row.status,
|
modelValue: row.status,
|
||||||
activeValue: CommonStatus.ENABLED,
|
activeValue: CommonStatus.ENABLED,
|
||||||
@@ -373,34 +548,56 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'CreatedAt',
|
prop: 'created_at',
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
formatter: (row: PlatformAccountResponse) => formatDateTime(row.CreatedAt)
|
width: 180,
|
||||||
|
formatter: (row: PlatformAccount) => formatDateTime(row.created_at)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'operation',
|
prop: 'operation',
|
||||||
label: '操作',
|
label: '操作',
|
||||||
width: 240,
|
width: 240,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
formatter: (row: PlatformAccountResponse) => {
|
formatter: (row: PlatformAccount) => {
|
||||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
const buttons = []
|
||||||
|
|
||||||
|
if (hasAuth('platform_account:patch_role')) {
|
||||||
|
buttons.push(
|
||||||
h(ArtButtonTable, {
|
h(ArtButtonTable, {
|
||||||
icon: '',
|
icon: '',
|
||||||
onClick: () => showRoleDialog(row)
|
onClick: () => showRoleDialog(row)
|
||||||
}),
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAuth('platform_account:update_psd')) {
|
||||||
|
buttons.push(
|
||||||
h(ArtButtonTable, {
|
h(ArtButtonTable, {
|
||||||
icon: '',
|
icon: '',
|
||||||
onClick: () => showPasswordDialog(row)
|
onClick: () => showPasswordDialog(row)
|
||||||
}),
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAuth('platform_account:edit')) {
|
||||||
|
buttons.push(
|
||||||
h(ArtButtonTable, {
|
h(ArtButtonTable, {
|
||||||
type: 'edit',
|
type: 'edit',
|
||||||
onClick: () => showDialog('edit', row)
|
onClick: () => showDialog('edit', row)
|
||||||
}),
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAuth('platform_account:delete')) {
|
||||||
|
buttons.push(
|
||||||
h(ArtButtonTable, {
|
h(ArtButtonTable, {
|
||||||
type: 'delete',
|
type: 'delete',
|
||||||
onClick: () => deleteAccount(row)
|
onClick: () => deleteAccount(row)
|
||||||
})
|
})
|
||||||
])
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
@@ -429,6 +626,8 @@
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getAccountList()
|
getAccountList()
|
||||||
loadAllRoles()
|
loadAllRoles()
|
||||||
|
loadShopList()
|
||||||
|
loadEnterpriseList()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 加载所有角色列表
|
// 加载所有角色列表
|
||||||
@@ -443,21 +642,41 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算属性:过滤后的可分配角色
|
||||||
|
const filteredAvailableRoles = computed(() => {
|
||||||
|
if (!leftRoleFilter.value) return allRoles.value
|
||||||
|
const keyword = leftRoleFilter.value.toLowerCase()
|
||||||
|
return allRoles.value.filter((role) => role.role_name.toLowerCase().includes(keyword))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算属性:过滤后的已分配角色
|
||||||
|
const filteredAssignedRoles = computed(() => {
|
||||||
|
const assignedRolesList = allRoles.value.filter((role) => selectedRoles.value.includes(role.ID))
|
||||||
|
if (!rightRoleFilter.value) return assignedRolesList
|
||||||
|
const keyword = rightRoleFilter.value.toLowerCase()
|
||||||
|
return assignedRolesList.filter((role) => role.role_name.toLowerCase().includes(keyword))
|
||||||
|
})
|
||||||
|
|
||||||
// 显示分配角色对话框
|
// 显示分配角色对话框
|
||||||
const showRoleDialog = async (row: PlatformAccountResponse) => {
|
const showRoleDialog = async (row: PlatformAccount) => {
|
||||||
currentAccountId.value = row.ID
|
currentAccountId.value = row.id
|
||||||
|
currentAccountName.value = row.username
|
||||||
selectedRoles.value = []
|
selectedRoles.value = []
|
||||||
|
rolesToAdd.value = []
|
||||||
|
leftRoleFilter.value = ''
|
||||||
|
rightRoleFilter.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 每次打开对话框时重新加载最新的角色列表
|
// 每次打开对话框时重新加载最新的角色列表
|
||||||
await loadAllRoles()
|
await loadAllRoles()
|
||||||
|
|
||||||
// 先加载当前账号的角色,再打开对话框
|
// 先加载当前账号的角色,再打开对话框
|
||||||
const res = await PlatformAccountService.getPlatformAccountRoles(row.ID)
|
const res = await AccountService.getAccountRoles(row.id)
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
// 提取角色ID数组
|
// 提取角色ID数组
|
||||||
const roles = res.data || []
|
const roles = res.data || []
|
||||||
selectedRoles.value = roles.map((role: any) => role.ID)
|
// 兼容 ID 和 id 两种字段名
|
||||||
|
selectedRoles.value = roles.map((role: any) => role.ID || role.id)
|
||||||
// 数据加载完成后再打开对话框
|
// 数据加载完成后再打开对话框
|
||||||
roleDialogVisible.value = true
|
roleDialogVisible.value = true
|
||||||
}
|
}
|
||||||
@@ -466,21 +685,42 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交分配角色
|
// 批量添加角色
|
||||||
const handleAssignRoles = async () => {
|
const addRoles = async () => {
|
||||||
roleSubmitLoading.value = true
|
if (rolesToAdd.value.length === 0) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await PlatformAccountService.assignRolesToPlatformAccount(currentAccountId.value, {
|
// 将选中的角色添加到已分配列表
|
||||||
role_ids: selectedRoles.value
|
const newRoles = [...new Set([...selectedRoles.value, ...rolesToAdd.value])]
|
||||||
})
|
await AccountService.assignRolesToAccount(currentAccountId.value, newRoles)
|
||||||
ElMessage.success('分配角色成功')
|
|
||||||
roleDialogVisible.value = false
|
selectedRoles.value = newRoles
|
||||||
|
rolesToAdd.value = []
|
||||||
|
|
||||||
|
ElMessage.success('角色添加成功')
|
||||||
// 刷新列表以更新角色显示
|
// 刷新列表以更新角色显示
|
||||||
await getAccountList()
|
await getAccountList()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error('添加角色失败:', error)
|
||||||
} finally {
|
ElMessage.error('角色添加失败')
|
||||||
roleSubmitLoading.value = false
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除单个角色
|
||||||
|
const removeSingleRole = async (roleId: number) => {
|
||||||
|
try {
|
||||||
|
// 从已分配列表中移除该角色
|
||||||
|
const newRoles = selectedRoles.value.filter((id) => id !== roleId)
|
||||||
|
await AccountService.assignRolesToAccount(currentAccountId.value, newRoles)
|
||||||
|
|
||||||
|
selectedRoles.value = newRoles
|
||||||
|
|
||||||
|
ElMessage.success('角色移除成功')
|
||||||
|
// 刷新列表以更新角色显示
|
||||||
|
await getAccountList()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('移除角色失败:', error)
|
||||||
|
ElMessage.error('角色移除失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,9 +732,10 @@
|
|||||||
if (valid) {
|
if (valid) {
|
||||||
passwordSubmitLoading.value = true
|
passwordSubmitLoading.value = true
|
||||||
try {
|
try {
|
||||||
await PlatformAccountService.changePlatformAccountPassword(currentAccountId.value, {
|
await AccountService.updateAccountPassword(
|
||||||
new_password: passwordForm.new_password
|
currentAccountId.value,
|
||||||
})
|
passwordForm.new_password
|
||||||
|
)
|
||||||
ElMessage.success('修改密码成功')
|
ElMessage.success('修改密码成功')
|
||||||
passwordDialogVisible.value = false
|
passwordDialogVisible.value = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -512,12 +753,15 @@
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
page: pagination.currentPage,
|
page: pagination.currentPage,
|
||||||
page_size: pagination.pageSize,
|
pageSize: pagination.pageSize,
|
||||||
username: searchForm.username || undefined,
|
username: searchForm.username || undefined,
|
||||||
phone: searchForm.phone || undefined,
|
phone: searchForm.phone || undefined,
|
||||||
|
user_type: searchForm.user_type, // 账号类型筛选(可选)
|
||||||
|
shop_id: searchForm.shop_id, // 店铺筛选
|
||||||
|
enterprise_id: searchForm.enterprise_id, // 企业筛选
|
||||||
status: searchForm.status
|
status: searchForm.status
|
||||||
}
|
}
|
||||||
const res = await PlatformAccountService.getPlatformAccounts(params)
|
const res = await AccountService.getAccounts(params)
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
tableData.value = res.data.items || []
|
tableData.value = res.data.items || []
|
||||||
pagination.total = res.data.total || 0
|
pagination.total = res.data.total || 0
|
||||||
@@ -611,16 +855,15 @@
|
|||||||
data.enterprise_id = formData.enterprise_id
|
data.enterprise_id = formData.enterprise_id
|
||||||
}
|
}
|
||||||
|
|
||||||
await PlatformAccountService.createPlatformAccount(data)
|
await AccountService.createAccount(data)
|
||||||
ElMessage.success('新增成功')
|
ElMessage.success('新增成功')
|
||||||
} else {
|
} else {
|
||||||
const data: any = {
|
const data: any = {
|
||||||
username: formData.username,
|
username: formData.username,
|
||||||
phone: formData.phone,
|
phone: formData.phone
|
||||||
status: formData.status
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await PlatformAccountService.updatePlatformAccount(formData.id, data)
|
await AccountService.updateAccount(formData.id, data)
|
||||||
ElMessage.success('更新成功')
|
ElMessage.success('更新成功')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,7 +895,7 @@
|
|||||||
// 先更新UI
|
// 先更新UI
|
||||||
row.status = newStatus
|
row.status = newStatus
|
||||||
try {
|
try {
|
||||||
await PlatformAccountService.updatePlatformAccount(row.ID, { status: newStatus })
|
await AccountService.updateAccountStatus(row.id, newStatus as 0 | 1)
|
||||||
ElMessage.success('状态切换成功')
|
ElMessage.success('状态切换成功')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 切换失败,恢复原状态
|
// 切换失败,恢复原状态
|
||||||
@@ -660,10 +903,178 @@
|
|||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载店铺列表
|
||||||
|
const loadShopList = async (keyword: string = '') => {
|
||||||
|
try {
|
||||||
|
const res = await ShopService.getShops({
|
||||||
|
page: 1,
|
||||||
|
page_size: 20, // 默认加载20条
|
||||||
|
status: 1, // 只加载启用的店铺
|
||||||
|
shop_name: keyword || undefined // 根据店铺名称搜索
|
||||||
|
})
|
||||||
|
if (res.code === 0) {
|
||||||
|
shopList.value = res.data.items || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取店铺列表失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 店铺搜索处理
|
||||||
|
const handleShopSearch = (query: string) => {
|
||||||
|
loadShopList(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载企业列表
|
||||||
|
const loadEnterpriseList = async (keyword: string = '') => {
|
||||||
|
try {
|
||||||
|
const res = await EnterpriseService.getEnterprises({
|
||||||
|
page: 1,
|
||||||
|
page_size: 20, // 默认加载20条
|
||||||
|
status: 1, // 只加载启用的企业
|
||||||
|
enterprise_name: keyword || undefined // 根据企业名称搜索
|
||||||
|
})
|
||||||
|
if (res.code === 0) {
|
||||||
|
enterpriseList.value = res.data.items || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取企业列表失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 企业搜索处理
|
||||||
|
const handleEnterpriseSearch = (query: string) => {
|
||||||
|
loadEnterpriseList(query)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.platform-account-page {
|
.platform-account-page {
|
||||||
// 平台账号管理页面样式
|
// 平台账号管理页面样式
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.account-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-transfer-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 20px 0;
|
||||||
|
min-height: 500px;
|
||||||
|
|
||||||
|
.transfer-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 380px;
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-bottom: 1px solid var(--el-border-color);
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px;
|
||||||
|
|
||||||
|
.role-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.role-item {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-color: var(--el-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-checkbox) {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.el-checkbox__label {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.assigned-role-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.role-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0 10px;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
@refresh="handleRefresh"
|
@refresh="handleRefresh"
|
||||||
>
|
>
|
||||||
<template #left>
|
<template #left>
|
||||||
<ElButton @click="showDialog('add')">新增代理账号</ElButton>
|
<ElButton @click="showDialog('add')" v-permission="'shop_account:add'">新增代理账号</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</ArtTableHeader>
|
</ArtTableHeader>
|
||||||
|
|
||||||
@@ -125,15 +125,18 @@
|
|||||||
import { FormInstance, ElMessage, ElMessageBox, ElSwitch, ElSelect, ElOption } from 'element-plus'
|
import { FormInstance, ElMessage, ElMessageBox, ElSwitch, ElSelect, ElOption } from 'element-plus'
|
||||||
import type { FormRules } from 'element-plus'
|
import type { FormRules } from 'element-plus'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||||
import { ShopAccountService, ShopService } from '@/api/modules'
|
import { AccountService, ShopService } from '@/api/modules'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import type { ShopAccountResponse, ShopResponse } from '@/types/api'
|
import type { PlatformAccount, ShopResponse } from '@/types/api'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
|
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
|
||||||
|
|
||||||
defineOptions({ name: 'ShopAccount' }) // 定义组件名称,用于 KeepAlive 缓存控制
|
defineOptions({ name: 'ShopAccount' }) // 定义组件名称,用于 KeepAlive 缓存控制
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
|
|
||||||
const dialogType = ref('add')
|
const dialogType = ref('add')
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const passwordDialogVisible = ref(false)
|
const passwordDialogVisible = ref(false)
|
||||||
@@ -163,7 +166,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 表格数据
|
// 表格数据
|
||||||
const tableData = ref<ShopAccountResponse[]>([])
|
const tableData = ref<PlatformAccount[]>([])
|
||||||
|
|
||||||
// 表格实例引用
|
// 表格实例引用
|
||||||
const tableRef = ref()
|
const tableRef = ref()
|
||||||
@@ -243,14 +246,14 @@
|
|||||||
]
|
]
|
||||||
|
|
||||||
// 显示对话框
|
// 显示对话框
|
||||||
const showDialog = (type: string, row?: ShopAccountResponse) => {
|
const showDialog = (type: string, row?: PlatformAccount) => {
|
||||||
dialogType.value = type
|
dialogType.value = type
|
||||||
|
|
||||||
if (type === 'edit' && row) {
|
if (type === 'edit' && row) {
|
||||||
formData.id = row.id
|
formData.id = row.id
|
||||||
formData.username = row.username
|
formData.username = row.username
|
||||||
formData.phone = row.phone
|
formData.phone = row.phone
|
||||||
formData.shop_id = row.shop_id
|
formData.shop_id = row.shop_id || 0
|
||||||
formData.password = ''
|
formData.password = ''
|
||||||
} else {
|
} else {
|
||||||
formData.id = 0
|
formData.id = 0
|
||||||
@@ -269,7 +272,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 显示修改密码对话框
|
// 显示修改密码对话框
|
||||||
const showPasswordDialog = (row: ShopAccountResponse) => {
|
const showPasswordDialog = (row: PlatformAccount) => {
|
||||||
currentAccountId.value = row.id
|
currentAccountId.value = row.id
|
||||||
passwordForm.new_password = ''
|
passwordForm.new_password = ''
|
||||||
|
|
||||||
@@ -282,12 +285,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 状态切换处理
|
// 状态切换处理
|
||||||
const handleStatusChange = async (row: ShopAccountResponse, newStatus: number) => {
|
const handleStatusChange = async (row: PlatformAccount, newStatus: number) => {
|
||||||
const oldStatus = row.status
|
const oldStatus = row.status
|
||||||
// 先更新UI
|
// 先更新UI
|
||||||
row.status = newStatus
|
row.status = newStatus
|
||||||
try {
|
try {
|
||||||
await ShopAccountService.updateShopAccountStatus(row.id, { status: newStatus })
|
await AccountService.updateAccountStatus(row.id, newStatus as 0 | 1)
|
||||||
ElMessage.success('状态切换成功')
|
ElMessage.success('状态切换成功')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 切换失败,恢复原状态
|
// 切换失败,恢复原状态
|
||||||
@@ -318,7 +321,7 @@
|
|||||||
{
|
{
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
label: '状态',
|
label: '状态',
|
||||||
formatter: (row: ShopAccountResponse) => {
|
formatter: (row: PlatformAccount) => {
|
||||||
return h(ElSwitch, {
|
return h(ElSwitch, {
|
||||||
modelValue: row.status,
|
modelValue: row.status,
|
||||||
activeValue: CommonStatus.ENABLED,
|
activeValue: CommonStatus.ENABLED,
|
||||||
@@ -334,24 +337,35 @@
|
|||||||
{
|
{
|
||||||
prop: 'created_at',
|
prop: 'created_at',
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
formatter: (row: ShopAccountResponse) => formatDateTime(row.created_at)
|
formatter: (row: PlatformAccount) => formatDateTime(row.created_at)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'operation',
|
prop: 'operation',
|
||||||
label: '操作',
|
label: '操作',
|
||||||
width: 120,
|
width: 120,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
formatter: (row: ShopAccountResponse) => {
|
formatter: (row: PlatformAccount) => {
|
||||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
const buttons = []
|
||||||
|
|
||||||
|
if (hasAuth('shop_account:update_psd')) {
|
||||||
|
buttons.push(
|
||||||
h(ArtButtonTable, {
|
h(ArtButtonTable, {
|
||||||
icon: '',
|
icon: '',
|
||||||
onClick: () => showPasswordDialog(row)
|
onClick: () => showPasswordDialog(row)
|
||||||
}),
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAuth('shop_account:edit')) {
|
||||||
|
buttons.push(
|
||||||
h(ArtButtonTable, {
|
h(ArtButtonTable, {
|
||||||
type: 'edit',
|
type: 'edit',
|
||||||
onClick: () => showDialog('edit', row)
|
onClick: () => showDialog('edit', row)
|
||||||
})
|
})
|
||||||
])
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
@@ -447,9 +461,10 @@
|
|||||||
if (valid) {
|
if (valid) {
|
||||||
passwordSubmitLoading.value = true
|
passwordSubmitLoading.value = true
|
||||||
try {
|
try {
|
||||||
await ShopAccountService.updateShopAccountPassword(currentAccountId.value, {
|
await AccountService.updateAccountPassword(
|
||||||
new_password: passwordForm.new_password
|
currentAccountId.value,
|
||||||
})
|
passwordForm.new_password
|
||||||
|
)
|
||||||
ElMessage.success('重置密码成功')
|
ElMessage.success('重置密码成功')
|
||||||
passwordDialogVisible.value = false
|
passwordDialogVisible.value = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -467,13 +482,14 @@
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
page: pagination.currentPage,
|
page: pagination.currentPage,
|
||||||
page_size: pagination.pageSize,
|
pageSize: pagination.pageSize,
|
||||||
|
user_type: 3, // 筛选代理账号
|
||||||
username: searchForm.username || undefined,
|
username: searchForm.username || undefined,
|
||||||
phone: searchForm.phone || undefined,
|
phone: searchForm.phone || undefined,
|
||||||
shop_id: searchForm.shop_id,
|
shop_id: searchForm.shop_id,
|
||||||
status: searchForm.status
|
status: searchForm.status
|
||||||
}
|
}
|
||||||
const res = await ShopAccountService.getShopAccounts(params)
|
const res = await AccountService.getAccounts(params)
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
tableData.value = res.data.items || []
|
tableData.value = res.data.items || []
|
||||||
pagination.total = res.data.total || 0
|
pagination.total = res.data.total || 0
|
||||||
@@ -536,17 +552,18 @@
|
|||||||
username: formData.username,
|
username: formData.username,
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
phone: formData.phone,
|
phone: formData.phone,
|
||||||
|
user_type: 3, // 代理账号
|
||||||
shop_id: formData.shop_id
|
shop_id: formData.shop_id
|
||||||
}
|
}
|
||||||
|
|
||||||
await ShopAccountService.createShopAccount(data)
|
await AccountService.createAccount(data)
|
||||||
ElMessage.success('新增成功')
|
ElMessage.success('新增成功')
|
||||||
} else {
|
} else {
|
||||||
const data = {
|
const data = {
|
||||||
username: formData.username
|
username: formData.username
|
||||||
}
|
}
|
||||||
|
|
||||||
await ShopAccountService.updateShopAccount(formData.id, data)
|
await AccountService.updateAccount(formData.id, data)
|
||||||
ElMessage.success('更新成功')
|
ElMessage.success('更新成功')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,189 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ArtTableFullScreen>
|
|
||||||
<div class="allocation-record-detail-page" id="table-full-screen">
|
|
||||||
<ElCard shadow="never" style="margin-bottom: 20px">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
<span>分配记录详情</span>
|
|
||||||
<ElButton @click="goBack">返回</ElButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<ElSkeleton :loading="loading" :rows="10" animated>
|
|
||||||
<template #default>
|
|
||||||
<ElDescriptions v-if="recordDetail" title="基本信息" :column="3" border>
|
|
||||||
<ElDescriptionsItem label="分配单号">{{
|
|
||||||
recordDetail.allocation_no
|
|
||||||
}}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="分配类型">
|
|
||||||
<ElTag :type="getAllocationTypeType(recordDetail.allocation_type)">
|
|
||||||
{{ recordDetail.allocation_name }}
|
|
||||||
</ElTag>
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="资产类型">
|
|
||||||
<ElTag :type="getAssetTypeType(recordDetail.asset_type)">
|
|
||||||
{{ recordDetail.asset_type_name }}
|
|
||||||
</ElTag>
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="资产标识符">{{
|
|
||||||
recordDetail.asset_identifier
|
|
||||||
}}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="关联卡数量">{{
|
|
||||||
recordDetail.related_card_count
|
|
||||||
}}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="关联设备ID">
|
|
||||||
{{ recordDetail.related_device_id || '-' }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
</ElDescriptions>
|
|
||||||
|
|
||||||
<ElDescriptions
|
|
||||||
v-if="recordDetail"
|
|
||||||
title="所有者信息"
|
|
||||||
:column="2"
|
|
||||||
border
|
|
||||||
style="margin-top: 20px"
|
|
||||||
>
|
|
||||||
<ElDescriptionsItem label="来源所有者">
|
|
||||||
{{ recordDetail.from_owner_name }} ({{ recordDetail.from_owner_type }})
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="目标所有者">
|
|
||||||
{{ recordDetail.to_owner_name }} ({{ recordDetail.to_owner_type }})
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
</ElDescriptions>
|
|
||||||
|
|
||||||
<ElDescriptions
|
|
||||||
v-if="recordDetail"
|
|
||||||
title="操作信息"
|
|
||||||
:column="2"
|
|
||||||
border
|
|
||||||
style="margin-top: 20px"
|
|
||||||
>
|
|
||||||
<ElDescriptionsItem label="操作人">{{
|
|
||||||
recordDetail.operator_name
|
|
||||||
}}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="创建时间">
|
|
||||||
{{ formatDateTime(recordDetail.created_at) }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="备注" :span="2">
|
|
||||||
{{ recordDetail.remark || '-' }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
</ElDescriptions>
|
|
||||||
|
|
||||||
<!-- 关联卡列表 -->
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
recordDetail &&
|
|
||||||
recordDetail.related_card_ids &&
|
|
||||||
recordDetail.related_card_ids.length > 0
|
|
||||||
"
|
|
||||||
style="margin-top: 20px"
|
|
||||||
>
|
|
||||||
<ElDivider content-position="left">关联卡列表</ElDivider>
|
|
||||||
<ElTable :data="relatedCardsList" border>
|
|
||||||
<ElTableColumn type="index" label="序号" width="60" />
|
|
||||||
<ElTableColumn prop="card_id" label="卡ID" width="80" />
|
|
||||||
<ElTableColumn label="ICCID" width="180">
|
|
||||||
<template #default="scope">
|
|
||||||
{{ getCardInfo(scope.row.card_id, 'iccid') }}
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="状态" width="100">
|
|
||||||
<template #default="scope">
|
|
||||||
{{ getCardInfo(scope.row.card_id, 'status') }}
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
</ElTable>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ElSkeleton>
|
|
||||||
</ElCard>
|
|
||||||
</div>
|
|
||||||
</ArtTableFullScreen>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
|
||||||
import { CardService } from '@/api/modules'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
|
||||||
import type {
|
|
||||||
AssetAllocationRecordDetail,
|
|
||||||
AllocationTypeEnum,
|
|
||||||
AssetTypeEnum
|
|
||||||
} from '@/types/api/card'
|
|
||||||
|
|
||||||
defineOptions({ name: 'AllocationRecordDetail' })
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const route = useRoute()
|
|
||||||
const loading = ref(false)
|
|
||||||
const recordDetail = ref<AssetAllocationRecordDetail | null>(null)
|
|
||||||
const relatedCardsList = ref<{ card_id: number }[]>([])
|
|
||||||
|
|
||||||
// 获取分配类型标签类型
|
|
||||||
const getAllocationTypeType = (type: AllocationTypeEnum) => {
|
|
||||||
return type === 'allocate' ? 'success' : 'warning'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取资产类型标签类型
|
|
||||||
const getAssetTypeType = (type: AssetTypeEnum) => {
|
|
||||||
return type === 'iot_card' ? 'primary' : 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模拟获取卡信息的方法(实际应该调用API获取)
|
|
||||||
const getCardInfo = (cardId: number, field: 'iccid' | 'status') => {
|
|
||||||
if (field === 'iccid') {
|
|
||||||
return `ICCID-${cardId}`
|
|
||||||
} else {
|
|
||||||
return '在库'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取详情数据
|
|
||||||
const getDetailData = async () => {
|
|
||||||
const id = route.query.id as string
|
|
||||||
if (!id) {
|
|
||||||
ElMessage.error('缺少记录ID参数')
|
|
||||||
goBack()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const res = await CardService.getAssetAllocationRecordDetail(Number(id))
|
|
||||||
if (res.code === 0) {
|
|
||||||
recordDetail.value = res.data
|
|
||||||
// 构建关联卡列表
|
|
||||||
if (recordDetail.value.related_card_ids && recordDetail.value.related_card_ids.length > 0) {
|
|
||||||
relatedCardsList.value = recordDetail.value.related_card_ids.map((cardId) => ({
|
|
||||||
card_id: cardId
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
ElMessage.error('获取分配记录详情失败')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回列表
|
|
||||||
const goBack = () => {
|
|
||||||
router.back()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getDetailData()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.allocation-record-detail-page {
|
|
||||||
.card-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
135
src/views/asset-management/asset-assign/detail.vue
Normal file
135
src/views/asset-management/asset-assign/detail.vue
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<template>
|
||||||
|
<div class="asset-assign-detail">
|
||||||
|
<ElCard shadow="never">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<div class="detail-header">
|
||||||
|
<ElButton @click="handleBack">
|
||||||
|
<template #icon>
|
||||||
|
<ElIcon><ArrowLeft /></ElIcon>
|
||||||
|
</template>
|
||||||
|
返回
|
||||||
|
</ElButton>
|
||||||
|
<h2 class="detail-title">资产分配详情</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 详情内容 -->
|
||||||
|
<DetailPage v-if="detailData" :sections="detailSections" :data="detailData" />
|
||||||
|
|
||||||
|
<!-- 加载中 -->
|
||||||
|
<div v-if="loading" class="loading-container">
|
||||||
|
<ElIcon class="is-loading"><Loading /></ElIcon>
|
||||||
|
<span>加载中...</span>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ElCard, ElButton, ElIcon, ElMessage } from 'element-plus'
|
||||||
|
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||||
|
import DetailPage from '@/components/common/DetailPage.vue'
|
||||||
|
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||||
|
import { CardService } from '@/api/modules'
|
||||||
|
import type { AssetAllocationRecord } from '@/types/api/card'
|
||||||
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
|
|
||||||
|
defineOptions({ name: 'AssetAssignDetail' })
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const detailData = ref<AssetAllocationRecord | null>(null)
|
||||||
|
|
||||||
|
// 详情页配置
|
||||||
|
const detailSections: DetailSection[] = [
|
||||||
|
{
|
||||||
|
title: '基本信息',
|
||||||
|
fields: [
|
||||||
|
{ label: '分配单号', prop: 'allocation_no' },
|
||||||
|
{
|
||||||
|
label: '分配类型',
|
||||||
|
formatter: (_, data) => data.allocation_name || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '资产类型',
|
||||||
|
formatter: (_, data) => data.asset_type_name || '-'
|
||||||
|
},
|
||||||
|
{ label: '资产标识符', prop: 'asset_identifier' },
|
||||||
|
{ label: '来源所有者', prop: 'from_owner_name' },
|
||||||
|
{ label: '目标所有者', prop: 'to_owner_name' },
|
||||||
|
{ label: '操作人', prop: 'operator_name' },
|
||||||
|
{ label: '关联卡数量', prop: 'related_card_count' },
|
||||||
|
{ label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) },
|
||||||
|
{ label: '备注', prop: 'remark', formatter: (value) => value || '-' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const handleBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取详情数据
|
||||||
|
const fetchDetail = async () => {
|
||||||
|
const id = Number(route.params.id)
|
||||||
|
if (!id) {
|
||||||
|
ElMessage.error('缺少ID参数')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await CardService.getAssetAllocationRecordDetail(id)
|
||||||
|
if (res.code === 0) {
|
||||||
|
detailData.value = res.data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
ElMessage.error('获取资产分配详情失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchDetail()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.asset-assign-detail {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
|
||||||
|
.detail-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
gap: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -28,18 +28,28 @@
|
|||||||
:pageSize="pagination.pageSize"
|
:pageSize="pagination.pageSize"
|
||||||
:total="pagination.total"
|
:total="pagination.total"
|
||||||
:marginTop="10"
|
:marginTop="10"
|
||||||
|
:row-class-name="getRowClassName"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
|
@row-contextmenu="handleRowContextMenu"
|
||||||
|
@cell-mouse-enter="handleCellMouseEnter"
|
||||||
|
@cell-mouse-leave="handleCellMouseLeave"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||||
<ElTableColumn label="操作" width="120" fixed="right">
|
|
||||||
<template #default="scope">
|
|
||||||
<ElButton type="primary" link @click="viewDetail(scope.row)">查看详情</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
</template>
|
</template>
|
||||||
</ArtTable>
|
</ArtTable>
|
||||||
|
|
||||||
|
<!-- 鼠标悬浮提示 -->
|
||||||
|
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||||
|
|
||||||
|
<!-- 右键菜单 -->
|
||||||
|
<ArtMenuRight
|
||||||
|
ref="contextMenuRef"
|
||||||
|
:menu-items="contextMenuItems"
|
||||||
|
:menu-width="120"
|
||||||
|
@select="handleContextMenuSelect"
|
||||||
|
/>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
</div>
|
</div>
|
||||||
</ArtTableFullScreen>
|
</ArtTableFullScreen>
|
||||||
@@ -52,14 +62,33 @@
|
|||||||
import { ElMessage, ElTag } from 'element-plus'
|
import { ElMessage, ElTag } from 'element-plus'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import type { AssetAllocationRecord, AllocationTypeEnum, AssetTypeEnum } from '@/types/api/card'
|
import type { AssetAllocationRecord, AllocationTypeEnum, AssetTypeEnum } from '@/types/api/card'
|
||||||
|
import { RoutesAlias } from '@/router/routesAlias'
|
||||||
|
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||||
|
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||||
|
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'AssetAllocationRecords' })
|
defineOptions({ name: 'AssetAllocationRecords' })
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 使用表格右键菜单功能
|
||||||
|
const {
|
||||||
|
showContextMenuHint,
|
||||||
|
hintPosition,
|
||||||
|
getRowClassName,
|
||||||
|
handleCellMouseEnter,
|
||||||
|
handleCellMouseLeave
|
||||||
|
} = useTableContextMenu()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const tableRef = ref()
|
const tableRef = ref()
|
||||||
|
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||||
|
const currentRow = ref<AssetAllocationRecord | null>(null)
|
||||||
|
|
||||||
// 搜索表单初始值
|
// 搜索表单初始值
|
||||||
const initialSearchState = {
|
const initialSearchState = {
|
||||||
@@ -174,7 +203,20 @@
|
|||||||
{
|
{
|
||||||
prop: 'allocation_no',
|
prop: 'allocation_no',
|
||||||
label: '分配单号',
|
label: '分配单号',
|
||||||
minWidth: 180
|
minWidth: 200,
|
||||||
|
formatter: (row: AssetAllocationRecord) => {
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||||
|
onClick: (e: MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleNameClick(row)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
row.allocation_no
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'allocation_name',
|
prop: 'allocation_name',
|
||||||
@@ -199,7 +241,7 @@
|
|||||||
{
|
{
|
||||||
prop: 'asset_identifier',
|
prop: 'asset_identifier',
|
||||||
label: '资产标识符',
|
label: '资产标识符',
|
||||||
minWidth: 180
|
minWidth: 200
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'from_owner_name',
|
prop: 'from_owner_name',
|
||||||
@@ -265,7 +307,7 @@
|
|||||||
|
|
||||||
const res = await CardService.getAssetAllocationRecords(params)
|
const res = await CardService.getAssetAllocationRecords(params)
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
recordList.value = res.data.list || []
|
recordList.value = res.data.items || []
|
||||||
pagination.total = res.data.total || 0
|
pagination.total = res.data.total || 0
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -308,14 +350,50 @@
|
|||||||
// 查看详情
|
// 查看详情
|
||||||
const viewDetail = (row: AssetAllocationRecord) => {
|
const viewDetail = (row: AssetAllocationRecord) => {
|
||||||
router.push({
|
router.push({
|
||||||
path: '/asset-management/allocation-record-detail',
|
path: `${RoutesAlias.AssetAssign}/detail/${row.id}`
|
||||||
query: { id: row.id }
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理名称点击
|
||||||
|
const handleNameClick = (row: AssetAllocationRecord) => {
|
||||||
|
if (hasAuth('asset_assign:view_detail')) {
|
||||||
|
viewDetail(row)
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('您没有查看详情的权限')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右键菜单项配置
|
||||||
|
const contextMenuItems = computed((): MenuItemType[] => {
|
||||||
|
const items: MenuItemType[] = []
|
||||||
|
|
||||||
|
return items
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理表格行右键菜单
|
||||||
|
const handleRowContextMenu = (row: AssetAllocationRecord, column: any, event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
currentRow.value = row
|
||||||
|
contextMenuRef.value?.show(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理右键菜单选择
|
||||||
|
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||||
|
if (!currentRow.value) return
|
||||||
|
|
||||||
|
switch (item.key) {
|
||||||
|
// No cases available
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.asset-allocation-records-page {
|
.asset-allocation-records-page {
|
||||||
// Allocation records page styles
|
// Allocation records page styles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__row.table-row-with-context-menu) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="authorization-detail-page">
|
|
||||||
<ElCard shadow="never" v-loading="loading">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
<span>授权记录详情</span>
|
|
||||||
<ElButton @click="goBack">返回</ElButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<ElDescriptions v-if="authorizationDetail" :column="2" border>
|
|
||||||
<ElDescriptionsItem label="授权记录ID">{{ authorizationDetail.id }}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="状态">
|
|
||||||
<ElTag :type="authorizationDetail.status === 1 ? 'success' : 'info'">
|
|
||||||
{{ authorizationDetail.status === 1 ? '有效' : '已回收' }}
|
|
||||||
</ElTag>
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
|
|
||||||
<ElDescriptionsItem label="ICCID">{{ authorizationDetail.iccid }}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="手机号">{{ authorizationDetail.msisdn }}</ElDescriptionsItem>
|
|
||||||
|
|
||||||
<ElDescriptionsItem label="企业名称">
|
|
||||||
{{ authorizationDetail.enterprise_name }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="企业ID">
|
|
||||||
{{ authorizationDetail.enterprise_id }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
|
|
||||||
<ElDescriptionsItem label="授权人">
|
|
||||||
{{ authorizationDetail.authorizer_name }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="授权人类型">
|
|
||||||
<ElTag :type="authorizationDetail.authorizer_type === 2 ? 'primary' : 'success'">
|
|
||||||
{{ authorizationDetail.authorizer_type === 2 ? '平台' : '代理' }}
|
|
||||||
</ElTag>
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
|
|
||||||
<ElDescriptionsItem label="授权时间">
|
|
||||||
{{ formatDateTime(authorizationDetail.authorized_at) }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="授权人ID">
|
|
||||||
{{ authorizationDetail.authorized_by }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
|
|
||||||
<ElDescriptionsItem label="回收人">
|
|
||||||
{{ authorizationDetail.revoker_name || '-' }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="回收时间">
|
|
||||||
{{
|
|
||||||
authorizationDetail.revoked_at ? formatDateTime(authorizationDetail.revoked_at) : '-'
|
|
||||||
}}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
|
|
||||||
<ElDescriptionsItem label="备注" :span="2">
|
|
||||||
{{ authorizationDetail.remark || '-' }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
</ElDescriptions>
|
|
||||||
</ElCard>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
|
||||||
import { AuthorizationService } from '@/api/modules'
|
|
||||||
import { ElMessage, ElTag } from 'element-plus'
|
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
|
||||||
import type { AuthorizationItem } from '@/types/api/authorization'
|
|
||||||
|
|
||||||
defineOptions({ name: 'AuthorizationDetail' })
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
const loading = ref(false)
|
|
||||||
const authorizationDetail = ref<AuthorizationItem | null>(null)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const id = route.query.id
|
|
||||||
if (id) {
|
|
||||||
getAuthorizationDetail(Number(id))
|
|
||||||
} else {
|
|
||||||
ElMessage.error('缺少授权记录ID')
|
|
||||||
goBack()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取授权记录详情
|
|
||||||
const getAuthorizationDetail = async (id: number) => {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const res = await AuthorizationService.getAuthorizationDetail(id)
|
|
||||||
if (res.code === 0) {
|
|
||||||
authorizationDetail.value = res.data
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
ElMessage.error('获取授权记录详情失败')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回
|
|
||||||
const goBack = () => {
|
|
||||||
router.back()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.authorization-detail-page {
|
|
||||||
padding: 20px;
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
140
src/views/asset-management/authorization-records/detail.vue
Normal file
140
src/views/asset-management/authorization-records/detail.vue
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<template>
|
||||||
|
<div class="authorization-record-detail">
|
||||||
|
<ElCard shadow="never">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<div class="detail-header">
|
||||||
|
<ElButton @click="handleBack">
|
||||||
|
<template #icon>
|
||||||
|
<ElIcon><ArrowLeft /></ElIcon>
|
||||||
|
</template>
|
||||||
|
返回
|
||||||
|
</ElButton>
|
||||||
|
<h2 class="detail-title">授权记录详情</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 详情内容 -->
|
||||||
|
<DetailPage v-if="detailData" :sections="detailSections" :data="detailData" />
|
||||||
|
|
||||||
|
<!-- 加载中 -->
|
||||||
|
<div v-if="loading" class="loading-container">
|
||||||
|
<ElIcon class="is-loading"><Loading /></ElIcon>
|
||||||
|
<span>加载中...</span>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ElCard, ElButton, ElIcon, ElMessage } from 'element-plus'
|
||||||
|
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||||
|
import DetailPage from '@/components/common/DetailPage.vue'
|
||||||
|
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||||
|
import { AuthorizationService } from '@/api/modules'
|
||||||
|
import type { AuthorizationItem } from '@/types/api/authorization'
|
||||||
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
|
|
||||||
|
defineOptions({ name: 'AuthorizationRecordDetail' })
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const detailData = ref<AuthorizationItem | null>(null)
|
||||||
|
|
||||||
|
// 详情页配置
|
||||||
|
const detailSections: DetailSection[] = [
|
||||||
|
{
|
||||||
|
title: '基本信息',
|
||||||
|
fields: [
|
||||||
|
{ label: '企业ID', prop: 'enterprise_id' },
|
||||||
|
{ label: '企业名称', prop: 'enterprise_name' },
|
||||||
|
{ label: '卡ID', prop: 'card_id' },
|
||||||
|
{ label: 'ICCID', prop: 'iccid' },
|
||||||
|
{ label: '手机号', prop: 'msisdn', formatter: (value) => value || '-' },
|
||||||
|
{ label: '授权人ID', prop: 'authorized_by' },
|
||||||
|
{ label: '授权人', prop: 'authorizer_name' },
|
||||||
|
{
|
||||||
|
label: '授权人类型',
|
||||||
|
formatter: (_, data) => {
|
||||||
|
return data.authorizer_type === 2 ? '平台' : '代理'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ label: '授权时间', prop: 'authorized_at', formatter: (value) => formatDateTime(value) },
|
||||||
|
{
|
||||||
|
label: '状态',
|
||||||
|
formatter: (_, data) => {
|
||||||
|
return data.status === 1 ? '有效' : '已回收'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ label: '备注', prop: 'remark', formatter: (value) => value || '-' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const handleBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取详情数据
|
||||||
|
const fetchDetail = async () => {
|
||||||
|
const id = Number(route.params.id)
|
||||||
|
if (!id) {
|
||||||
|
ElMessage.error('缺少ID参数')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await AuthorizationService.getAuthorizationDetail(id)
|
||||||
|
if (res.code === 0) {
|
||||||
|
detailData.value = res.data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
ElMessage.error('获取授权记录详情失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchDetail()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.authorization-record-detail {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
|
||||||
|
.detail-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
gap: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -28,14 +28,29 @@
|
|||||||
:pageSize="pagination.pageSize"
|
:pageSize="pagination.pageSize"
|
||||||
:total="pagination.total"
|
:total="pagination.total"
|
||||||
:marginTop="10"
|
:marginTop="10"
|
||||||
|
:row-class-name="getRowClassName"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
|
@row-contextmenu="handleRowContextMenu"
|
||||||
|
@cell-mouse-enter="handleCellMouseEnter"
|
||||||
|
@cell-mouse-leave="handleCellMouseLeave"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||||
</template>
|
</template>
|
||||||
</ArtTable>
|
</ArtTable>
|
||||||
|
|
||||||
|
<!-- 鼠标悬浮提示 -->
|
||||||
|
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||||
|
|
||||||
|
<!-- 右键菜单 -->
|
||||||
|
<ArtMenuRight
|
||||||
|
ref="contextMenuRef"
|
||||||
|
:menu-items="contextMenuItems"
|
||||||
|
:menu-width="120"
|
||||||
|
@select="handleContextMenuSelect"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 修改备注对话框 -->
|
<!-- 修改备注对话框 -->
|
||||||
<ElDialog v-model="remarkDialogVisible" title="修改备注" width="500px">
|
<ElDialog v-model="remarkDialogVisible" title="修改备注" width="500px">
|
||||||
<ElForm ref="remarkFormRef" :model="remarkForm" :rules="remarkRules" label-width="80px">
|
<ElForm ref="remarkFormRef" :model="remarkForm" :rules="remarkRules" label-width="80px">
|
||||||
@@ -72,24 +87,43 @@
|
|||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||||
|
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||||
|
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||||
|
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||||
import type {
|
import type {
|
||||||
AuthorizationItem,
|
AuthorizationItem,
|
||||||
AuthorizationStatus,
|
AuthorizationStatus,
|
||||||
AuthorizerType
|
AuthorizerType
|
||||||
} from '@/types/api/authorization'
|
} from '@/types/api/authorization'
|
||||||
import { CommonStatus } from '@/config/constants'
|
import { CommonStatus } from '@/config/constants'
|
||||||
|
import { RoutesAlias } from '@/router/routesAlias'
|
||||||
|
|
||||||
defineOptions({ name: 'AuthorizationRecords' })
|
defineOptions({ name: 'AuthorizationRecords' })
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 使用表格右键菜单功能
|
||||||
|
const {
|
||||||
|
showContextMenuHint,
|
||||||
|
hintPosition,
|
||||||
|
getRowClassName,
|
||||||
|
handleCellMouseEnter,
|
||||||
|
handleCellMouseLeave
|
||||||
|
} = useTableContextMenu()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const remarkDialogVisible = ref(false)
|
const remarkDialogVisible = ref(false)
|
||||||
const remarkLoading = ref(false)
|
const remarkLoading = ref(false)
|
||||||
const tableRef = ref()
|
const tableRef = ref()
|
||||||
const remarkFormRef = ref<FormInstance>()
|
const remarkFormRef = ref<FormInstance>()
|
||||||
const currentRecordId = ref<number>(0)
|
const currentRecordId = ref<number>(0)
|
||||||
|
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||||
|
const currentRow = ref<AuthorizationItem | null>(null)
|
||||||
|
|
||||||
// 搜索表单初始值
|
// 搜索表单初始值
|
||||||
const initialSearchState = {
|
const initialSearchState = {
|
||||||
@@ -172,7 +206,6 @@
|
|||||||
|
|
||||||
// 列配置
|
// 列配置
|
||||||
const columnOptions = [
|
const columnOptions = [
|
||||||
{ label: 'ID', prop: 'id' },
|
|
||||||
{ label: 'ICCID', prop: 'iccid' },
|
{ label: 'ICCID', prop: 'iccid' },
|
||||||
{ label: '手机号', prop: 'msisdn' },
|
{ label: '手机号', prop: 'msisdn' },
|
||||||
{ label: '企业名称', prop: 'enterprise_name' },
|
{ label: '企业名称', prop: 'enterprise_name' },
|
||||||
@@ -180,10 +213,7 @@
|
|||||||
{ label: '授权人类型', prop: 'authorizer_type' },
|
{ label: '授权人类型', prop: 'authorizer_type' },
|
||||||
{ label: '授权时间', prop: 'authorized_at' },
|
{ label: '授权时间', prop: 'authorized_at' },
|
||||||
{ label: '状态', prop: 'status' },
|
{ label: '状态', prop: 'status' },
|
||||||
{ label: '回收人', prop: 'revoker_name' },
|
{ label: '备注', prop: 'remark' }
|
||||||
{ label: '回收时间', prop: 'revoked_at' },
|
|
||||||
{ label: '备注', prop: 'remark' },
|
|
||||||
{ label: '操作', prop: 'operation' }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const authorizationList = ref<AuthorizationItem[]>([])
|
const authorizationList = ref<AuthorizationItem[]>([])
|
||||||
@@ -210,20 +240,29 @@
|
|||||||
|
|
||||||
// 动态列配置
|
// 动态列配置
|
||||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||||
{
|
|
||||||
prop: 'id',
|
|
||||||
label: 'ID',
|
|
||||||
width: 80
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
prop: 'iccid',
|
prop: 'iccid',
|
||||||
label: 'ICCID',
|
label: 'ICCID',
|
||||||
minWidth: 180
|
minWidth: 200,
|
||||||
|
formatter: (row: AuthorizationItem) => {
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||||
|
onClick: (e: MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleNameClick(row)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
row.iccid
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'msisdn',
|
prop: 'msisdn',
|
||||||
label: '手机号',
|
label: '手机号',
|
||||||
width: 120
|
width: 130,
|
||||||
|
formatter: (row: AuthorizationItem) => row.msisdn || '-'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'enterprise_name',
|
prop: 'enterprise_name',
|
||||||
@@ -238,7 +277,7 @@
|
|||||||
{
|
{
|
||||||
prop: 'authorizer_type',
|
prop: 'authorizer_type',
|
||||||
label: '授权人类型',
|
label: '授权人类型',
|
||||||
width: 100,
|
width: 110,
|
||||||
formatter: (row: AuthorizationItem) => {
|
formatter: (row: AuthorizationItem) => {
|
||||||
return h(ElTag, { type: getAuthorizerTypeTag(row.authorizer_type) }, () =>
|
return h(ElTag, { type: getAuthorizerTypeTag(row.authorizer_type) }, () =>
|
||||||
getAuthorizerTypeText(row.authorizer_type)
|
getAuthorizerTypeText(row.authorizer_type)
|
||||||
@@ -259,42 +298,12 @@
|
|||||||
return h(ElTag, { type: getStatusTag(row.status) }, () => getStatusText(row.status))
|
return h(ElTag, { type: getStatusTag(row.status) }, () => getStatusText(row.status))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
prop: 'revoker_name',
|
|
||||||
label: '回收人',
|
|
||||||
width: 120,
|
|
||||||
formatter: (row: AuthorizationItem) => row.revoker_name || '-'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'revoked_at',
|
|
||||||
label: '回收时间',
|
|
||||||
width: 180,
|
|
||||||
formatter: (row: AuthorizationItem) => (row.revoked_at ? formatDateTime(row.revoked_at) : '-')
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
prop: 'remark',
|
prop: 'remark',
|
||||||
label: '备注',
|
label: '备注',
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
showOverflowTooltip: true,
|
showOverflowTooltip: true,
|
||||||
formatter: (row: AuthorizationItem) => row.remark || '-'
|
formatter: (row: AuthorizationItem) => row.remark || '-'
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'operation',
|
|
||||||
label: '操作',
|
|
||||||
width: 150,
|
|
||||||
fixed: 'right',
|
|
||||||
formatter: (row: AuthorizationItem) => {
|
|
||||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
|
||||||
h(ArtButtonTable, {
|
|
||||||
type: 'view',
|
|
||||||
onClick: () => viewDetail(row)
|
|
||||||
}),
|
|
||||||
h(ArtButtonTable, {
|
|
||||||
type: 'edit',
|
|
||||||
onClick: () => showRemarkDialog(row)
|
|
||||||
})
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -372,11 +381,19 @@
|
|||||||
// 查看详情
|
// 查看详情
|
||||||
const viewDetail = (row: AuthorizationItem) => {
|
const viewDetail = (row: AuthorizationItem) => {
|
||||||
router.push({
|
router.push({
|
||||||
path: '/asset-management/authorization-detail',
|
path: `${RoutesAlias.AuthorizationRecords}/detail/${row.id}`
|
||||||
query: { id: row.id }
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理名称点击
|
||||||
|
const handleNameClick = (row: AuthorizationItem) => {
|
||||||
|
if (hasAuth('authorization_records:view_detail')) {
|
||||||
|
viewDetail(row)
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('您没有查看详情的权限')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 显示修改备注对话框
|
// 显示修改备注对话框
|
||||||
const showRemarkDialog = (row: AuthorizationItem) => {
|
const showRemarkDialog = (row: AuthorizationItem) => {
|
||||||
currentRecordId.value = row.id
|
currentRecordId.value = row.id
|
||||||
@@ -407,10 +424,44 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 右键菜单项配置
|
||||||
|
const contextMenuItems = computed((): MenuItemType[] => {
|
||||||
|
const items: MenuItemType[] = []
|
||||||
|
|
||||||
|
if (hasAuth('authorization_records:update_remark')) {
|
||||||
|
items.push({ key: 'edit', label: '编辑' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理表格行右键菜单
|
||||||
|
const handleRowContextMenu = (row: AuthorizationItem, column: any, event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
currentRow.value = row
|
||||||
|
contextMenuRef.value?.show(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理右键菜单选择
|
||||||
|
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||||
|
if (!currentRow.value) return
|
||||||
|
|
||||||
|
switch (item.key) {
|
||||||
|
case 'edit':
|
||||||
|
showRemarkDialog(currentRow.value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.authorization-records-page {
|
.authorization-records-page {
|
||||||
// Authorization records page styles
|
// Authorization records page styles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__row.table-row-with-context-menu) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user