fetch(modify):修复角色分配权限
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 2m36s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 2m36s
This commit is contained in:
@@ -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 确认所有相关功能正常工作
|
||||
@@ -363,7 +363,7 @@ export class CardService extends BaseService {
|
||||
*/
|
||||
static batchSetCardSeriesBinding(data: {
|
||||
iccids: string[]
|
||||
series_allocation_id: number
|
||||
series_id: number
|
||||
}): Promise<BaseResponse<any>> {
|
||||
return this.patch('/api/admin/iot-cards/series-binding', data)
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ export class DeviceService extends BaseService {
|
||||
*/
|
||||
static batchSetDeviceSeriesBinding(data: {
|
||||
device_ids: number[]
|
||||
series_allocation_id: number
|
||||
series_id: number
|
||||
}): Promise<BaseResponse<any>> {
|
||||
return this.patch('/api/admin/devices/series-binding', data)
|
||||
}
|
||||
|
||||
@@ -173,7 +173,11 @@ function convertRouteComponent(
|
||||
// 基础路由配置
|
||||
const converted: ConvertedRoute = {
|
||||
...routeConfig,
|
||||
component: undefined
|
||||
component: undefined,
|
||||
meta: {
|
||||
...routeConfig.meta,
|
||||
dynamic: true // 标记为动态路由,用于退出时清理
|
||||
}
|
||||
}
|
||||
|
||||
// 是否为一级菜单
|
||||
@@ -229,7 +233,10 @@ function handleLayoutRoute(
|
||||
path: route.path,
|
||||
name: route.name,
|
||||
component: loadComponent(component as string, String(route.name)),
|
||||
meta: route.meta
|
||||
meta: {
|
||||
...route.meta,
|
||||
dynamic: true // 标记为动态路由,用于退出时清理
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -318,6 +318,7 @@ export interface StandaloneCardQueryParams extends PaginationParams {
|
||||
is_replaced?: boolean // 是否有换卡记录
|
||||
iccid_start?: string // ICCID起始号
|
||||
iccid_end?: string // ICCID结束号
|
||||
series_id?: number // 套餐系列ID
|
||||
}
|
||||
|
||||
// 单卡信息
|
||||
@@ -347,6 +348,7 @@ export interface StandaloneIotCard {
|
||||
activated_at?: string | null // 激活时间 (可选)
|
||||
created_at: string // 创建时间
|
||||
updated_at: string // 更新时间
|
||||
series_id?: number | null // 套餐系列ID
|
||||
}
|
||||
|
||||
// ========== 单卡批量分配和回收相关 ==========
|
||||
@@ -488,7 +490,7 @@ export interface IotCardDetailResponse {
|
||||
// 批量设置卡的套餐系列绑定请求参数
|
||||
export interface BatchSetCardSeriesBindingRequest {
|
||||
iccids: string[] // ICCID列表(最多500个)
|
||||
series_allocation_id: number // 套餐系列分配ID(0表示清除关联)
|
||||
series_id: number // 套餐系列ID(0表示清除关联)
|
||||
}
|
||||
|
||||
// 卡套餐系列绑定失败项
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface Device {
|
||||
activated_at: string | null // 激活时间
|
||||
created_at: string // 创建时间
|
||||
updated_at: string // 更新时间
|
||||
series_id?: number | null // 套餐系列ID
|
||||
}
|
||||
|
||||
// 设备查询参数
|
||||
@@ -47,6 +48,7 @@ export interface DeviceQueryParams extends PaginationParams {
|
||||
manufacturer?: string // 制造商(模糊查询)
|
||||
created_at_start?: string // 创建时间起始
|
||||
created_at_end?: string // 创建时间结束
|
||||
series_id?: number // 套餐系列ID
|
||||
}
|
||||
|
||||
// 设备列表响应
|
||||
@@ -207,7 +209,7 @@ export interface DeviceImportTaskDetail extends DeviceImportTask {
|
||||
// 批量设置设备的套餐系列绑定请求参数
|
||||
export interface BatchSetDeviceSeriesBindingRequest {
|
||||
device_ids: number[] // 设备ID列表(最多500个)
|
||||
series_allocation_id: number // 套餐系列分配ID(0表示清除关联)
|
||||
series_id: number // 套餐系列ID(0表示清除关联)
|
||||
}
|
||||
|
||||
// 设备套餐系列绑定失败项
|
||||
|
||||
1
src/types/components.d.ts
vendored
1
src/types/components.d.ts
vendored
@@ -131,6 +131,7 @@ declare module 'vue' {
|
||||
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElTransfer: typeof import('element-plus/es')['ElTransfer']
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
|
||||
@@ -1188,7 +1188,7 @@
|
||||
try {
|
||||
const data = {
|
||||
device_ids: selectedDevices.value.map((d) => d.id),
|
||||
series_allocation_id: seriesBindingForm.series_id! // 注意:API参数名仍是series_allocation_id,但前端使用series_id
|
||||
series_id: seriesBindingForm.series_id!
|
||||
}
|
||||
const res = await DeviceService.batchSetDeviceSeriesBinding(data)
|
||||
if (res.code === 0) {
|
||||
|
||||
@@ -1323,7 +1323,7 @@
|
||||
try {
|
||||
const res = await CardService.batchSetCardSeriesBinding({
|
||||
iccids,
|
||||
series_allocation_id: seriesBindingForm.series_id! // 注意:API参数名仍是series_allocation_id,但前端使用series_id
|
||||
series_id: seriesBindingForm.series_id!
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
|
||||
@@ -63,10 +63,27 @@
|
||||
v-model="createForm.package_ids"
|
||||
:placeholder="t('orderManagement.createForm.packageIdsPlaceholder')"
|
||||
multiple
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:remote-method="searchPackages"
|
||||
:loading="packageSearchLoading"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<!-- TODO: Load actual packages from API -->
|
||||
<ElOption label="套餐示例" :value="1" />
|
||||
<ElOption
|
||||
v-for="pkg in packageOptions"
|
||||
:key="pkg.id"
|
||||
:label="`${pkg.package_name} (¥${(pkg.price / 100).toFixed(2)})`"
|
||||
:value="pkg.id"
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<span>{{ pkg.package_name }}</span>
|
||||
<span style="color: var(--el-text-color-secondary); font-size: 12px">
|
||||
¥{{ (pkg.price / 100).toFixed(2) }}
|
||||
</span>
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
@@ -240,7 +257,7 @@
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { OrderService, CardService, DeviceService } from '@/api/modules'
|
||||
import { OrderService, CardService, DeviceService, PackageManageService } from '@/api/modules'
|
||||
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type {
|
||||
@@ -253,7 +270,8 @@
|
||||
OrderPaymentMethod,
|
||||
OrderCommissionStatus,
|
||||
StandaloneIotCard,
|
||||
Device
|
||||
Device,
|
||||
PackageResponse
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
@@ -384,6 +402,10 @@
|
||||
|
||||
const orderList = ref<Order[]>([])
|
||||
|
||||
// 套餐搜索相关
|
||||
const packageOptions = ref<PackageResponse[]>([])
|
||||
const packageSearchLoading = ref(false)
|
||||
|
||||
// IoT卡搜索相关
|
||||
const iotCardOptions = ref<StandaloneIotCard[]>([])
|
||||
const cardSearchLoading = ref(false)
|
||||
@@ -392,17 +414,34 @@
|
||||
const deviceOptions = ref<Device[]>([])
|
||||
const deviceSearchLoading = ref(false)
|
||||
|
||||
// 搜索IoT卡(根据ICCID)
|
||||
const searchIotCards = async (query: string) => {
|
||||
if (!query) {
|
||||
iotCardOptions.value = []
|
||||
return
|
||||
// 搜索套餐(根据套餐名称)
|
||||
const searchPackages = async (query: string) => {
|
||||
packageSearchLoading.value = true
|
||||
try {
|
||||
const res = await PackageManageService.getPackages({
|
||||
package_name: query || undefined,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
status: 1, // 只获取启用的套餐
|
||||
shelf_status: 1 // 只获取已上架的套餐
|
||||
})
|
||||
if (res.code === 0) {
|
||||
packageOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Search packages failed:', error)
|
||||
packageOptions.value = []
|
||||
} finally {
|
||||
packageSearchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索IoT卡(根据ICCID)
|
||||
const searchIotCards = async (query: string) => {
|
||||
cardSearchLoading.value = true
|
||||
try {
|
||||
const res = await CardService.getStandaloneIotCards({
|
||||
iccid: query,
|
||||
iccid: query || undefined,
|
||||
page: 1,
|
||||
page_size: 20
|
||||
})
|
||||
@@ -419,15 +458,10 @@
|
||||
|
||||
// 搜索设备(根据设备号device_no)
|
||||
const searchDevices = async (query: string) => {
|
||||
if (!query) {
|
||||
deviceOptions.value = []
|
||||
return
|
||||
}
|
||||
|
||||
deviceSearchLoading.value = true
|
||||
try {
|
||||
const res = await DeviceService.getDevices({
|
||||
device_no: query,
|
||||
device_no: query || undefined,
|
||||
page: 1,
|
||||
page_size: 20
|
||||
})
|
||||
@@ -656,8 +690,29 @@
|
||||
// 显示创建订单对话框
|
||||
const showCreateDialog = async () => {
|
||||
createDialogVisible.value = true
|
||||
// 默认加载20条IoT卡和设备数据
|
||||
await Promise.all([loadDefaultIotCards(), loadDefaultDevices()])
|
||||
// 默认加载20条套餐、IoT卡和设备数据
|
||||
await Promise.all([loadDefaultPackages(), loadDefaultIotCards(), loadDefaultDevices()])
|
||||
}
|
||||
|
||||
// 加载默认套餐列表
|
||||
const loadDefaultPackages = async () => {
|
||||
packageSearchLoading.value = true
|
||||
try {
|
||||
const res = await PackageManageService.getPackages({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
status: 1, // 只获取启用的套餐
|
||||
shelf_status: 1 // 只获取已上架的套餐
|
||||
})
|
||||
if (res.code === 0) {
|
||||
packageOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load default packages failed:', error)
|
||||
packageOptions.value = []
|
||||
} finally {
|
||||
packageSearchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载默认IoT卡列表
|
||||
@@ -709,7 +764,8 @@
|
||||
createForm.iot_card_id = null
|
||||
createForm.device_id = null
|
||||
|
||||
// 清空IoT卡和设备搜索结果
|
||||
// 清空套餐、IoT卡和设备搜索结果
|
||||
packageOptions.value = []
|
||||
iotCardOptions.value = []
|
||||
deviceOptions.value = []
|
||||
}
|
||||
|
||||
@@ -88,20 +88,41 @@
|
||||
</ElDialog>
|
||||
|
||||
<!-- 分配权限对话框 -->
|
||||
<ElDialog v-model="permissionDialogVisible" title="分配权限" width="800px">
|
||||
<div class="permission-assignment-container">
|
||||
<!-- 左侧:权限树(用于添加) -->
|
||||
<div class="permission-tree-section">
|
||||
<div class="section-title">可分配权限</div>
|
||||
<ElDialog v-model="permissionDialogVisible" width="800px">
|
||||
<template #header>
|
||||
<div class="dialog-header">
|
||||
<span class="dialog-title">分配权限</span>
|
||||
<div class="role-info">
|
||||
<ElTag :type="currentRole.role_type === 1 ? 'primary' : 'success'" size="small">
|
||||
{{ currentRole.role_type === 1 ? '平台角色' : '客户角色' }}
|
||||
</ElTag>
|
||||
<span class="role-name">{{ currentRole.role_name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="permission-tree-transfer-container">
|
||||
<!-- 左侧:可分配权限树 -->
|
||||
<div class="transfer-panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">可分配权限</span>
|
||||
<ElInput
|
||||
v-model="leftTreeFilter"
|
||||
placeholder="搜索权限"
|
||||
clearable
|
||||
size="small"
|
||||
style="width: 180px"
|
||||
/>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<ElTree
|
||||
ref="permissionTreeRef"
|
||||
:data="permissionTreeData"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
ref="leftTreeRef"
|
||||
:data="availablePermissions"
|
||||
:props="{ children: 'children', label: 'label' }"
|
||||
node-key="id"
|
||||
show-checkbox
|
||||
:filter-node-method="filterNode"
|
||||
:default-expand-all="false"
|
||||
class="permission-tree"
|
||||
@check="handlePermissionCheck"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<span style="display: flex; align-items: center; gap: 8px">
|
||||
@@ -116,37 +137,64 @@
|
||||
</template>
|
||||
</ElTree>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:已分配权限列表 -->
|
||||
<div class="assigned-permissions-section">
|
||||
<div class="section-title">已分配权限</div>
|
||||
<div class="assigned-list">
|
||||
<div
|
||||
v-for="perm in assignedPermissionsList"
|
||||
:key="perm.id"
|
||||
class="assigned-item"
|
||||
<!-- 中间:操作按钮 -->
|
||||
<div class="transfer-buttons">
|
||||
<ElButton
|
||||
type="primary"
|
||||
:icon="'ArrowRight'"
|
||||
@click="addPermissions"
|
||||
:disabled="getLeftCheckedKeys().length === 0"
|
||||
>
|
||||
添加
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:已分配权限树 -->
|
||||
<div class="transfer-panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">已分配权限</span>
|
||||
<ElInput
|
||||
v-model="rightTreeFilter"
|
||||
placeholder="搜索权限"
|
||||
clearable
|
||||
size="small"
|
||||
style="width: 180px"
|
||||
/>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<ElTree
|
||||
ref="rightTreeRef"
|
||||
:data="assignedPermissions"
|
||||
:props="{ children: 'children', label: 'label' }"
|
||||
node-key="id"
|
||||
:filter-node-method="filterNode"
|
||||
:default-expand-all="false"
|
||||
class="permission-tree permission-tree-with-action"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<span class="tree-node-content">
|
||||
<span class="tree-node-label">
|
||||
<span>{{ node.label }}</span>
|
||||
<ElTag
|
||||
:type="perm.perm_type === 1 ? 'info' : 'success'"
|
||||
:type="data.perm_type === 1 ? 'info' : 'success'"
|
||||
size="small"
|
||||
>
|
||||
{{ perm.perm_type === 1 ? '菜单' : '按钮' }}
|
||||
{{ data.perm_type === 1 ? '菜单' : '按钮' }}
|
||||
</ElTag>
|
||||
<span class="perm-name">{{ perm.perm_name }}</span>
|
||||
</span>
|
||||
<ElButton
|
||||
type="danger"
|
||||
size="small"
|
||||
link
|
||||
@click="removePermission(perm.id)"
|
||||
@click="removeSinglePermission(data.id)"
|
||||
>
|
||||
移除
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElEmpty
|
||||
v-if="assignedPermissionsList.length === 0"
|
||||
description="暂无已分配权限"
|
||||
:image-size="80"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</ElTree>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,7 +217,9 @@
|
||||
ElMessageBox,
|
||||
ElTag,
|
||||
ElTree,
|
||||
ElSwitch
|
||||
ElSwitch,
|
||||
ElButton,
|
||||
ElInput
|
||||
} from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { PlatformRole, PermissionTreeNode } from '@/types/api'
|
||||
@@ -188,16 +238,21 @@
|
||||
const permissionDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const permissionSubmitLoading = ref(false)
|
||||
const tableRef = ref()
|
||||
const permissionTreeRef = ref()
|
||||
const leftTreeRef = ref()
|
||||
const rightTreeRef = ref()
|
||||
const currentRoleId = ref<number>(0)
|
||||
const currentRole = ref<{ role_name: string; role_type: number }>({
|
||||
role_name: '',
|
||||
role_type: 1
|
||||
})
|
||||
const selectedPermissions = ref<number[]>([])
|
||||
const originalPermissions = ref<number[]>([]) // 保存原始权限,用于对比
|
||||
const permissionTreeData = ref<any[]>([])
|
||||
const assignedPermissionsList = ref<any[]>([]) // 已分配的权限列表(用于右侧显示)
|
||||
const allPermissionsFlat = ref<PermissionTreeNode[]>([]) // 扁平化的所有权限数据
|
||||
const isInitializingTree = ref(false) // 标记是否正在初始化树的选中状态
|
||||
const availablePermissions = ref<any[]>([]) // 左侧可分配权限树数据
|
||||
const assignedPermissions = ref<any[]>([]) // 右侧已分配权限树数据
|
||||
const allPermissionsMap = ref<Map<number, any>>(new Map()) // 所有权限的映射表
|
||||
const leftTreeFilter = ref('') // 左侧树搜索关键字
|
||||
const rightTreeFilter = ref('') // 右侧树搜索关键字
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
@@ -355,22 +410,22 @@
|
||||
getTableData()
|
||||
})
|
||||
|
||||
// 将权限树扁平化为一维数组
|
||||
const flattenPermissionTree = (treeNodes: PermissionTreeNode[]): PermissionTreeNode[] => {
|
||||
const result: PermissionTreeNode[] = []
|
||||
const flatten = (nodes: PermissionTreeNode[]) => {
|
||||
nodes.forEach((node) => {
|
||||
result.push(node)
|
||||
if (node.children && node.children.length > 0) {
|
||||
flatten(node.children)
|
||||
}
|
||||
// 监听搜索关键字变化
|
||||
watch(leftTreeFilter, (val) => {
|
||||
leftTreeRef.value?.filter(val)
|
||||
})
|
||||
}
|
||||
flatten(treeNodes)
|
||||
return result
|
||||
|
||||
watch(rightTreeFilter, (val) => {
|
||||
rightTreeRef.value?.filter(val)
|
||||
})
|
||||
|
||||
// 树节点过滤方法
|
||||
const filterNode = (value: string, data: any) => {
|
||||
if (!value) return true
|
||||
return data.label.toLowerCase().includes(value.toLowerCase())
|
||||
}
|
||||
|
||||
// 将权限树节点转换为ElTree所需的格式
|
||||
// 构建权限树数据结构
|
||||
const buildTreeData = (treeNodes: PermissionTreeNode[]): any[] => {
|
||||
return treeNodes.map((node) => ({
|
||||
id: node.id,
|
||||
@@ -380,40 +435,224 @@
|
||||
}))
|
||||
}
|
||||
|
||||
// 构建权限映射表(包括所有节点和子节点)
|
||||
const buildPermissionMap = (treeNodes: PermissionTreeNode[], map: Map<number, any>) => {
|
||||
treeNodes.forEach((node) => {
|
||||
map.set(node.id, node)
|
||||
if (node.children && node.children.length > 0) {
|
||||
buildPermissionMap(node.children, map)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 根据权限ID列表过滤树节点(包含指定ID的节点)
|
||||
const filterTreeByIds = (treeNodes: any[], ids: number[]): any[] => {
|
||||
const result: any[] = []
|
||||
|
||||
treeNodes.forEach((node) => {
|
||||
if (ids.includes(node.id)) {
|
||||
const newNode = { ...node }
|
||||
if (node.children && node.children.length > 0) {
|
||||
newNode.children = filterTreeByIds(node.children, ids)
|
||||
}
|
||||
result.push(newNode)
|
||||
} else if (node.children && node.children.length > 0) {
|
||||
const filteredChildren = filterTreeByIds(node.children, ids)
|
||||
if (filteredChildren.length > 0) {
|
||||
result.push({
|
||||
...node,
|
||||
children: filteredChildren
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 根据权限ID列表设置树节点的禁用状态
|
||||
const setTreeNodesDisabled = (treeNodes: any[], idsToDisable: number[]): any[] => {
|
||||
return treeNodes.map((node) => {
|
||||
const newNode = {
|
||||
...node,
|
||||
disabled: idsToDisable.includes(node.id)
|
||||
}
|
||||
|
||||
if (node.children && node.children.length > 0) {
|
||||
newNode.children = setTreeNodesDisabled(node.children, idsToDisable)
|
||||
}
|
||||
|
||||
return newNode
|
||||
})
|
||||
}
|
||||
|
||||
// 获取树中所有节点的ID(包括父节点和子节点)
|
||||
const getAllNodeIds = (treeNodes: any[]): number[] => {
|
||||
const ids: number[] = []
|
||||
|
||||
const traverse = (nodes: any[]) => {
|
||||
nodes.forEach((node) => {
|
||||
ids.push(node.id)
|
||||
if (node.children && node.children.length > 0) {
|
||||
traverse(node.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
traverse(treeNodes)
|
||||
return ids
|
||||
}
|
||||
|
||||
// 保存原始权限树数据
|
||||
const originalPermissionTree = ref<PermissionTreeNode[]>([])
|
||||
|
||||
// 加载所有权限树
|
||||
const loadAllPermissions = async () => {
|
||||
try {
|
||||
const res = await PermissionService.getPermissionTree()
|
||||
if (res.code === 0) {
|
||||
const treeData = res.data || []
|
||||
// 扁平化所有权限数据,用于查找
|
||||
allPermissionsFlat.value = flattenPermissionTree(treeData)
|
||||
|
||||
// 保存原始树数据
|
||||
originalPermissionTree.value = treeData
|
||||
|
||||
// 构建权限映射表
|
||||
const map = new Map()
|
||||
buildPermissionMap(treeData, map)
|
||||
allPermissionsMap.value = map
|
||||
|
||||
// 构建树形数据
|
||||
permissionTreeData.value = buildTreeData(treeData)
|
||||
const fullTreeData = buildTreeData(treeData)
|
||||
|
||||
// 左侧显示所有权限,但已分配的权限设置为禁用
|
||||
availablePermissions.value = setTreeNodesDisabled(
|
||||
JSON.parse(JSON.stringify(fullTreeData)),
|
||||
selectedPermissions.value
|
||||
)
|
||||
|
||||
// 右侧显示已分配的权限
|
||||
if (selectedPermissions.value.length > 0) {
|
||||
assignedPermissions.value = filterTreeByIds(fullTreeData, selectedPermissions.value)
|
||||
} else {
|
||||
assignedPermissions.value = []
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取权限树失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据权限ID列表构建已分配权限列表
|
||||
const buildAssignedPermissionsList = () => {
|
||||
assignedPermissionsList.value = selectedPermissions.value
|
||||
.map((id) => allPermissionsFlat.value.find((p) => p.id === id))
|
||||
.filter((p) => p !== undefined) as PermissionTreeNode[]
|
||||
// 获取左侧树完全勾选的节点(不包括半选节点)
|
||||
const getLeftCheckedKeys = (): number[] => {
|
||||
if (!leftTreeRef.value) return []
|
||||
return leftTreeRef.value.getCheckedKeys(false)
|
||||
}
|
||||
|
||||
// 获取左侧树勾选的节点(包括半选节点,用于提交服务器)
|
||||
const getLeftCheckedKeysWithHalf = (): number[] => {
|
||||
if (!leftTreeRef.value) return []
|
||||
const checkedKeys = leftTreeRef.value.getCheckedKeys(false)
|
||||
const halfCheckedKeys = leftTreeRef.value.getHalfCheckedKeys()
|
||||
return [...checkedKeys, ...halfCheckedKeys]
|
||||
}
|
||||
|
||||
// 添加权限
|
||||
const addPermissions = async () => {
|
||||
const checkedKeys = getLeftCheckedKeys()
|
||||
if (checkedKeys.length === 0) return
|
||||
|
||||
try {
|
||||
// 提交到服务器时包含勾选和半选节点
|
||||
const keysToSubmit = getLeftCheckedKeysWithHalf()
|
||||
await RoleService.assignPermissions(currentRoleId.value, keysToSubmit)
|
||||
|
||||
// 本地只记录完全勾选的节点
|
||||
selectedPermissions.value = [...new Set([...selectedPermissions.value, ...checkedKeys])]
|
||||
|
||||
// 重新构建左右两侧树
|
||||
const fullTreeData = buildTreeData(originalPermissionTree.value)
|
||||
|
||||
// 左侧显示所有权限,但已分配的权限设置为禁用
|
||||
availablePermissions.value = setTreeNodesDisabled(
|
||||
JSON.parse(JSON.stringify(fullTreeData)),
|
||||
selectedPermissions.value
|
||||
)
|
||||
|
||||
// 右侧显示已分配的权限
|
||||
assignedPermissions.value = filterTreeByIds(fullTreeData, selectedPermissions.value)
|
||||
|
||||
// 清空左侧树的勾选状态
|
||||
leftTreeRef.value?.setCheckedKeys([], false)
|
||||
|
||||
ElMessage.success('权限添加成功')
|
||||
} catch (error) {
|
||||
console.error('添加权限失败:', error)
|
||||
ElMessage.error('权限添加失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 移除单个权限
|
||||
const removeSinglePermission = async (permId: number) => {
|
||||
try {
|
||||
// 保存右侧树的展开节点
|
||||
const expandedKeys = rightTreeRef.value?.store?.nodesMap
|
||||
? Object.keys(rightTreeRef.value.store.nodesMap)
|
||||
.filter(key => rightTreeRef.value.store.nodesMap[key].expanded)
|
||||
.map(key => Number(key))
|
||||
: []
|
||||
|
||||
await RoleService.removePermission(currentRoleId.value, permId)
|
||||
|
||||
// 更新已选权限列表
|
||||
selectedPermissions.value = selectedPermissions.value.filter(id => id !== permId)
|
||||
|
||||
// 重新构建左右两侧树
|
||||
const fullTreeData = buildTreeData(originalPermissionTree.value)
|
||||
|
||||
// 左侧显示所有权限,但已分配的权限设置为禁用
|
||||
availablePermissions.value = setTreeNodesDisabled(
|
||||
JSON.parse(JSON.stringify(fullTreeData)),
|
||||
selectedPermissions.value
|
||||
)
|
||||
|
||||
// 右侧显示已分配的权限
|
||||
if (selectedPermissions.value.length > 0) {
|
||||
assignedPermissions.value = filterTreeByIds(fullTreeData, selectedPermissions.value)
|
||||
} else {
|
||||
assignedPermissions.value = []
|
||||
}
|
||||
|
||||
// 等待DOM更新后恢复展开状态
|
||||
await nextTick()
|
||||
if (rightTreeRef.value && expandedKeys.length > 0) {
|
||||
expandedKeys.forEach(key => {
|
||||
const node = rightTreeRef.value.store.nodesMap[key]
|
||||
if (node && !node.isLeaf) {
|
||||
node.expanded = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ElMessage.success('权限移除成功')
|
||||
} catch (error) {
|
||||
console.error('移除权限失败:', error)
|
||||
ElMessage.error('权限移除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 显示分配权限对话框
|
||||
const showPermissionDialog = async (row: PlatformRole) => {
|
||||
currentRoleId.value = row.ID
|
||||
currentRole.value = {
|
||||
role_name: row.role_name,
|
||||
role_type: row.role_type
|
||||
}
|
||||
selectedPermissions.value = []
|
||||
originalPermissions.value = []
|
||||
assignedPermissionsList.value = []
|
||||
leftTreeFilter.value = ''
|
||||
rightTreeFilter.value = ''
|
||||
|
||||
try {
|
||||
// 每次打开对话框时重新加载最新的权限列表
|
||||
await loadAllPermissions()
|
||||
|
||||
// 加载当前角色的权限
|
||||
const res = await RoleService.getRolePermissions(row.ID)
|
||||
|
||||
@@ -438,116 +677,18 @@
|
||||
|
||||
// 保存原始权限,用于后续对比
|
||||
originalPermissions.value = [...selectedPermissions.value]
|
||||
}
|
||||
|
||||
// 构建已分配权限列表
|
||||
buildAssignedPermissionsList()
|
||||
// 每次打开对话框时重新加载最新的权限列表
|
||||
await loadAllPermissions()
|
||||
|
||||
// 数据加载完成后再打开对话框
|
||||
permissionDialogVisible.value = true
|
||||
|
||||
// 等待DOM更新后设置树的初始选中状态
|
||||
await nextTick()
|
||||
if (permissionTreeRef.value) {
|
||||
isInitializingTree.value = true
|
||||
permissionTreeRef.value.setCheckedKeys(selectedPermissions.value, false)
|
||||
// 延迟一点时间后取消初始化标记
|
||||
setTimeout(() => {
|
||||
isInitializingTree.value = false
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取角色权限失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理权限勾选(只允许添加,不允许取消)
|
||||
const handlePermissionCheck = async (data: any, checkedInfo: any) => {
|
||||
if (!permissionTreeRef.value) return
|
||||
|
||||
// 如果正在初始化树,忽略此次check事件
|
||||
if (isInitializingTree.value) return
|
||||
|
||||
// 获取当前勾选的所有节点(不包括半选状态)
|
||||
const checkedKeys = permissionTreeRef.value.getCheckedKeys(false)
|
||||
|
||||
// 找出新增的权限(当前勾选中不在已分配列表中的)
|
||||
const newPermissions = checkedKeys.filter((id) => !selectedPermissions.value.includes(id))
|
||||
|
||||
if (newPermissions.length > 0) {
|
||||
try {
|
||||
// 调用API分配权限
|
||||
await RoleService.assignPermissions(currentRoleId.value, newPermissions)
|
||||
|
||||
// 更新已分配权限列表
|
||||
selectedPermissions.value = [...selectedPermissions.value, ...newPermissions]
|
||||
buildAssignedPermissionsList()
|
||||
|
||||
ElMessage.success('权限添加成功')
|
||||
} catch (error) {
|
||||
console.error('添加权限失败:', error)
|
||||
ElMessage.error('权限添加失败')
|
||||
|
||||
// 如果添加失败,恢复树的选中状态
|
||||
nextTick(() => {
|
||||
permissionTreeRef.value?.setCheckedKeys(selectedPermissions.value, false)
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 重新设置树的选中状态为已分配的权限(阻止取消勾选)
|
||||
nextTick(() => {
|
||||
permissionTreeRef.value?.setCheckedKeys(selectedPermissions.value, false)
|
||||
})
|
||||
}
|
||||
|
||||
// 移除单个权限
|
||||
const removePermission = async (permId: number) => {
|
||||
try {
|
||||
await RoleService.removePermission(currentRoleId.value, permId)
|
||||
|
||||
// 重新从服务器获取最新的权限列表
|
||||
const res = await RoleService.getRolePermissions(currentRoleId.value)
|
||||
|
||||
if (res.code === 0 || Array.isArray(res.data)) {
|
||||
const permissions = res.data || []
|
||||
|
||||
// 清空并重新设置权限列表
|
||||
if (Array.isArray(permissions) && permissions.length > 0) {
|
||||
if (typeof permissions[0] === 'object') {
|
||||
if ('ID' in permissions[0]) {
|
||||
selectedPermissions.value = permissions.map((perm: any) => perm.ID)
|
||||
} else if ('id' in permissions[0]) {
|
||||
selectedPermissions.value = permissions.map((perm: any) => perm.id)
|
||||
}
|
||||
} else {
|
||||
selectedPermissions.value = permissions
|
||||
}
|
||||
} else {
|
||||
selectedPermissions.value = []
|
||||
}
|
||||
|
||||
// 重建已分配权限列表
|
||||
buildAssignedPermissionsList()
|
||||
|
||||
// 更新树的选中状态
|
||||
await nextTick()
|
||||
if (permissionTreeRef.value) {
|
||||
isInitializingTree.value = true
|
||||
permissionTreeRef.value.setCheckedKeys(selectedPermissions.value, false)
|
||||
setTimeout(() => {
|
||||
isInitializingTree.value = false
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
ElMessage.success('权限移除成功')
|
||||
} catch (error) {
|
||||
console.error('移除权限失败:', error)
|
||||
ElMessage.error('权限移除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取角色列表
|
||||
const getTableData = async () => {
|
||||
@@ -699,76 +840,123 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.permission-assignment-container {
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
height: 500px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.permission-tree-section,
|
||||
.assigned-permissions-section {
|
||||
.dialog-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.role-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.role-name {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.permission-tree-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: 340px;
|
||||
|
||||
.section-title {
|
||||
.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;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.permission-tree-section {
|
||||
.permission-tree {
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
|
||||
.permission-tree {
|
||||
:deep(.el-tree-node) {
|
||||
margin: 6px 0;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__content) {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
}
|
||||
}
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.assigned-permissions-section {
|
||||
.assigned-list {
|
||||
:deep(.el-tree-node__label) {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.assigned-item {
|
||||
.permission-tree-with-action {
|
||||
:deep(.el-tree-node__content) {
|
||||
height: auto;
|
||||
min-height: 36px;
|
||||
padding: 4px 8px 4px 0;
|
||||
}
|
||||
|
||||
.tree-node-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
|
||||
.tree-node-label {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 8px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
.perm-name {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
padding: 4px 8px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.transfer-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 0 10px;
|
||||
|
||||
.el-button {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user