fetch(modify):修改bug
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m6s

This commit is contained in:
sexygoat
2026-01-31 18:12:58 +08:00
parent ecb79dae43
commit 882feaf3ff
8 changed files with 452 additions and 305 deletions

View File

@@ -42,10 +42,6 @@ export function useLogin() {
// 加载状态 // 加载状态
const loading = ref(false) const loading = ref(false)
// 拖拽验证
const isPassing = ref(false)
const isClickPass = ref(false)
// 表单数据 // 表单数据
const formData = reactive<LoginForm>({ const formData = reactive<LoginForm>({
account: '', account: '',
@@ -115,13 +111,6 @@ export function useLogin() {
const valid = await formRef.value.validate() const valid = await formRef.value.validate()
if (!valid) return if (!valid) return
// 检查拖拽验证
if (!isPassing.value) {
isClickPass.value = true
ElMessage.warning(t('login.placeholder[2]'))
return
}
loading.value = true loading.value = true
// 判断使用 Mock 还是真实 API // 判断使用 Mock 还是真实 API
@@ -248,14 +237,6 @@ export function useLogin() {
}, 150) }, 150)
} }
/**
* 重置拖拽验证
*/
const resetDragVerify = () => {
isPassing.value = false
isClickPass.value = false
}
// 组件挂载时初始化 // 组件挂载时初始化
onMounted(() => { onMounted(() => {
initForm() initForm()
@@ -266,11 +247,8 @@ export function useLogin() {
formData, formData,
rules, rules,
loading, loading,
isPassing,
isClickPass,
mockAccounts, mockAccounts,
setupAccount, setupAccount,
handleLogin, handleLogin
resetDragVerify
} }
} }

View File

@@ -239,6 +239,15 @@ async function processBackendMenu(router: Router): Promise<void> {
try { try {
const userStore = useUserStore() const userStore = useUserStore()
// 如果是超级管理员user_type === 1直接使用所有 asyncRoutes
if (userStore.isSuperAdmin) {
const menuList = asyncRoutes.map((route) => menuDataToRouter(route))
await registerAndStoreMenu(router, menuList, closeLoading)
return
}
// 普通用户:使用后端返回的菜单
const backendMenus = userStore.menus || [] const backendMenus = userStore.menus || []
const routeMap = buildRouteMap(asyncRoutes) const routeMap = buildRouteMap(asyncRoutes)

View File

@@ -51,11 +51,10 @@ export interface DeviceQueryParams extends PaginationParams {
// 设备列表响应 // 设备列表响应
export interface DeviceListResponse { export interface DeviceListResponse {
list: Device[] | null // 设备列表 items: Device[] | null // 设备列表
page: number // 当前页码 page: number // 当前页码
page_size: number // 每页数量 size: number // 每页数量
total: number // 总数 total: number // 总数
total_pages: number // 总页数
} }
// ========== 设备卡绑定相关 ========== // ========== 设备卡绑定相关 ==========

View File

@@ -36,15 +36,17 @@ export interface Permission {
children?: Permission[] // 子权限列表(树形结构) children?: Permission[] // 子权限列表(树形结构)
} }
// 权限树节点 // 权限树节点(匹配后端 DtoPermissionTreeNode
export interface PermissionTreeNode { export interface PermissionTreeNode {
id: string | number id: number // 权限ID
label: string perm_code: string // 权限编码
value: string perm_name: string // 权限名称
permissionCode: string perm_type: number // 权限类型 (1:菜单, 2:按钮)
permissionType: PermissionType url?: string // 请求路径
parentId?: string | number platform?: string // 适用端口 (all:全部, web:Web后台, h5:H5端)
children?: PermissionTreeNode[] sort?: number // 排序值
available_for_role_types?: string // 可用角色类型 (1:平台角色, 2:客户角色)
children?: PermissionTreeNode[] // 子权限列表
} }
// 权限查询参数 // 权限查询参数

View File

@@ -69,30 +69,12 @@
autocomplete="off" autocomplete="off"
/> />
</ElFormItem> </ElFormItem>
<div class="drag-verify">
<div class="drag-verify-content" :class="{ error: !isPassing && isClickPass }">
<ArtDragVerify
ref="dragVerify"
v-model:value="isPassing"
:width="width < 500 ? 328 : 438"
:text="$t('login.sliderText')"
textColor="var(--art-gray-800)"
:successText="$t('login.sliderSuccessText')"
:progressBarBg="getCssVar('--el-color-primary')"
background="var(--art-gray-200)"
handlerBg="var(--art-main-bg-color)"
/>
</div>
<p class="error-text" :class="{ 'show-error-text': !isPassing && isClickPass }">{{
$t('login.placeholder[2]')
}}</p>
</div>
<div class="forget-password"> <div class="forget-password">
<ElCheckbox v-model="formData.rememberPassword">{{ <ElCheckbox v-model="formData.rememberPassword">{{
$t('login.rememberPwd') $t('login.rememberPwd')
}}</ElCheckbox> }}</ElCheckbox>
<RouterLink :to="RoutesAlias.ForgetPassword">{{ $t('login.forgetPwd') }}</RouterLink> <!--<RouterLink :to="RoutesAlias.ForgetPassword">{{ $t('login.forgetPwd') }}</RouterLink>-->
</div> </div>
<div style="margin-top: 30px"> <div style="margin-top: 30px">
@@ -138,24 +120,16 @@
formData, formData,
rules, rules,
loading, loading,
isPassing,
isClickPass,
mockAccounts, mockAccounts,
setupAccount, setupAccount,
handleLogin handleLogin
} = useLogin() } = useLogin()
const dragVerify = ref()
const systemName = AppConfig.systemInfo.name const systemName = AppConfig.systemInfo.name
const { width } = useWindowSize()
// 处理提交 // 处理提交
const handleSubmit = async () => { const handleSubmit = async () => {
await handleLogin() await handleLogin()
// 重置拖拽验证
if (dragVerify.value) {
dragVerify.value.reset()
}
} }
// 切换语言 // 切换语言

View File

@@ -105,11 +105,31 @@
:label="t('orderManagement.createForm.deviceId')" :label="t('orderManagement.createForm.deviceId')"
prop="device_id" prop="device_id"
> >
<ElInputNumber <ElSelect
v-model="createForm.device_id" v-model="createForm.device_id"
filterable
remote
reserve-keyword
:placeholder="t('orderManagement.createForm.deviceIdPlaceholder')" :placeholder="t('orderManagement.createForm.deviceIdPlaceholder')"
:remote-method="searchDevices"
:loading="deviceSearchLoading"
style="width: 100%" style="width: 100%"
/> clearable
>
<ElOption
v-for="device in deviceOptions"
:key="device.id"
:label="`${device.device_no} (${device.device_name})`"
:value="device.id"
>
<div style="display: flex; justify-content: space-between">
<span>{{ device.device_no }}</span>
<span style="color: var(--el-text-color-secondary); font-size: 12px">
{{ device.device_name }}
</span>
</div>
</ElOption>
</ElSelect>
</ElFormItem> </ElFormItem>
</ElForm> </ElForm>
<template #footer> <template #footer>
@@ -220,7 +240,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { h } from 'vue' import { h } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { OrderService, CardService } from '@/api/modules' import { OrderService, CardService, DeviceService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag } from 'element-plus' import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import type { import type {
@@ -232,7 +252,8 @@
BuyerType, BuyerType,
OrderPaymentMethod, OrderPaymentMethod,
OrderCommissionStatus, OrderCommissionStatus,
StandaloneIotCard StandaloneIotCard,
Device
} from '@/types/api' } from '@/types/api'
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
@@ -367,6 +388,10 @@
const iotCardOptions = ref<StandaloneIotCard[]>([]) const iotCardOptions = ref<StandaloneIotCard[]>([])
const cardSearchLoading = ref(false) const cardSearchLoading = ref(false)
// 设备搜索相关
const deviceOptions = ref<Device[]>([])
const deviceSearchLoading = ref(false)
// 搜索IoT卡根据ICCID // 搜索IoT卡根据ICCID
const searchIotCards = async (query: string) => { const searchIotCards = async (query: string) => {
if (!query) { if (!query) {
@@ -392,6 +417,31 @@
} }
} }
// 搜索设备根据设备号device_no
const searchDevices = async (query: string) => {
if (!query) {
deviceOptions.value = []
return
}
deviceSearchLoading.value = true
try {
const res = await DeviceService.getDevices({
device_no: query,
page: 1,
page_size: 20
})
if (res.code === 0) {
deviceOptions.value = res.data.items || []
}
} catch (error) {
console.error('Search devices failed:', error)
deviceOptions.value = []
} finally {
deviceSearchLoading.value = false
}
}
// 格式化货币 - 将分转换为元 // 格式化货币 - 将分转换为元
const formatCurrency = (amount: number): string => { const formatCurrency = (amount: number): string => {
return `¥${(amount / 100).toFixed(2)}` return `¥${(amount / 100).toFixed(2)}`
@@ -606,8 +656,8 @@
// 显示创建订单对话框 // 显示创建订单对话框
const showCreateDialog = async () => { const showCreateDialog = async () => {
createDialogVisible.value = true createDialogVisible.value = true
// 默认加载20条IoT卡数据 // 默认加载20条IoT卡和设备数据
await loadDefaultIotCards() await Promise.all([loadDefaultIotCards(), loadDefaultDevices()])
} }
// 加载默认IoT卡列表 // 加载默认IoT卡列表
@@ -629,6 +679,25 @@
} }
} }
// 加载默认设备列表
const loadDefaultDevices = async () => {
deviceSearchLoading.value = true
try {
const res = await DeviceService.getDevices({
page: 1,
page_size: 20
})
if (res.code === 0) {
deviceOptions.value = res.data.items || []
}
} catch (error) {
console.error('Load default devices failed:', error)
deviceOptions.value = []
} finally {
deviceSearchLoading.value = false
}
}
// 对话框关闭后的清理 // 对话框关闭后的清理
const handleCreateDialogClosed = () => { const handleCreateDialogClosed = () => {
// 重置表单(会同时清除验证状态) // 重置表单(会同时清除验证状态)
@@ -640,8 +709,9 @@
createForm.iot_card_id = null createForm.iot_card_id = null
createForm.device_id = null createForm.device_id = null
// 清空IoT卡搜索结果 // 清空IoT卡和设备搜索结果
iotCardOptions.value = [] iotCardOptions.value = []
deviceOptions.value = []
} }
// 创建订单 // 创建订单

View File

@@ -24,7 +24,7 @@
<!-- 表格 --> <!-- 表格 -->
<ArtTable <ArtTable
ref="tableRef" ref="tableRef"
row-key="ID" row-key="id"
:data="permissionList" :data="permissionList"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }" :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:default-expand-all="false" :default-expand-all="false"
@@ -110,25 +110,20 @@
ElMessage, ElMessage,
ElMessageBox, ElMessageBox,
ElTag, ElTag,
ElSwitch,
type FormInstance, type FormInstance,
type FormRules type FormRules
} from 'element-plus' } from 'element-plus'
import { PermissionService } from '@/api/modules' import { PermissionService } from '@/api/modules'
import type { Permission, CreatePermissionParams } from '@/types/api' import type { CreatePermissionParams, PermissionTreeNode } from '@/types/api'
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { formatDateTime } from '@/utils/business/format'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { import {
CommonStatus,
getStatusText,
PermissionType, PermissionType,
PERMISSION_TYPE_OPTIONS, PERMISSION_TYPE_OPTIONS,
PERMISSION_TYPE_SELECT_OPTIONS, PERMISSION_TYPE_SELECT_OPTIONS,
getPermissionTypeText, getPermissionTypeText,
getPermissionTypeTag, getPermissionTypeTag
STATUS_SELECT_OPTIONS
} from '@/config/constants' } from '@/config/constants'
defineOptions({ name: 'Permission' }) defineOptions({ name: 'Permission' })
@@ -137,8 +132,7 @@
const initialSearchState = { const initialSearchState = {
perm_name: '', perm_name: '',
perm_code: '', perm_code: '',
perm_type: undefined as number | undefined, perm_type: undefined as number | undefined
status: undefined as number | undefined
} }
// 搜索表单 // 搜索表单
@@ -173,16 +167,6 @@
clearable: true, clearable: true,
placeholder: '请选择' placeholder: '请选择'
} }
},
{
label: '状态',
prop: 'status',
type: 'select',
options: STATUS_SELECT_OPTIONS,
config: {
clearable: true,
placeholder: '请选择'
}
} }
] ]
@@ -192,19 +176,20 @@
{ label: '权限标识', prop: 'perm_code' }, { label: '权限标识', prop: 'perm_code' },
{ label: '权限类型', prop: 'perm_type' }, { label: '权限类型', prop: 'perm_type' },
{ label: '菜单路径', prop: 'url' }, { label: '菜单路径', prop: 'url' },
{ label: '适用端口', prop: 'platform' },
{ label: '排序', prop: 'sort' }, { label: '排序', prop: 'sort' },
{ label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'CreatedAt' },
{ label: '操作', prop: 'operation' } { label: '操作', prop: 'operation' }
] ]
// 权限列表 // 权限列表(树形结构)
const permissionList = ref<Permission[]>([]) const permissionList = ref<PermissionTreeNode[]>([])
// 原始权限树数据(用于搜索过滤)
const originalPermissionTree = ref<PermissionTreeNode[]>([])
const tableRef = ref() const tableRef = ref()
const dialogVisible = ref(false) const dialogVisible = ref(false)
const dialogType = ref('add') const dialogType = ref('add')
const currentRow = ref<Permission | null>(null) const currentRow = ref<PermissionTreeNode | null>(null)
const currentPermissionId = ref<number>(0) const currentPermissionId = ref<number>(0)
const submitLoading = ref(false) const submitLoading = ref(false)
@@ -242,7 +227,7 @@
{ {
prop: 'perm_name', prop: 'perm_name',
label: '权限名称', label: '权限名称',
minWidth: 200 width: 200
}, },
{ {
prop: 'perm_code', prop: 'perm_code',
@@ -253,7 +238,7 @@
prop: 'perm_type', prop: 'perm_type',
label: '权限类型', label: '权限类型',
width: 120, width: 120,
formatter: (row: any) => { formatter: (row: PermissionTreeNode) => {
return h(ElTag, { type: getPermissionTypeTag(row.perm_type) }, () => return h(ElTag, { type: getPermissionTypeTag(row.perm_type) }, () =>
getPermissionTypeText(row.perm_type) getPermissionTypeText(row.perm_type)
) )
@@ -261,43 +246,32 @@
}, },
{ {
prop: 'url', prop: 'url',
label: '菜单路径', label: '菜单路径'
width: 180 },
{
prop: 'platform',
label: '适用端口',
width: 120,
formatter: (row: PermissionTreeNode) => {
const platformMap: Record<string, string> = {
all: '全部',
web: 'Web后台',
h5: 'H5端'
}
return platformMap[row.platform || 'all'] || row.platform
}
}, },
{ {
prop: 'sort', prop: 'sort',
label: '排序', label: '排序',
width: 80 width: 80
}, },
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: any) => {
return h(ElSwitch, {
modelValue: row.status,
activeValue: CommonStatus.ENABLED,
inactiveValue: CommonStatus.DISABLED,
activeText: getStatusText(CommonStatus.ENABLED),
inactiveText: getStatusText(CommonStatus.DISABLED),
inlinePrompt: true,
'onUpdate:modelValue': (val: string | number | boolean) =>
handleStatusChange(row, val as number)
})
}
},
{
prop: 'CreatedAt',
label: '创建时间',
width: 180,
formatter: (row: any) => formatDateTime(row.CreatedAt)
},
{ {
prop: 'operation', prop: 'operation',
label: '操作', label: '操作',
width: 160, width: 120,
fixed: 'right', fixed: 'right',
formatter: (row: any) => { formatter: (row: PermissionTreeNode) => {
return h('div', { style: 'display: flex; gap: 8px;' }, [ return h('div', { style: 'display: flex; gap: 8px;' }, [
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'edit', type: 'edit',
@@ -312,51 +286,59 @@
} }
]) ])
// 将扁平数据转换为树形结构 // 过滤权限树(根据搜索条件)
const buildTreeData = (flatData: Permission[]): Permission[] => { const filterPermissionTree = (
const map = new Map<number, Permission>() tree: PermissionTreeNode[],
const result: Permission[] = [] filters: typeof searchForm
): PermissionTreeNode[] => {
return tree.reduce<PermissionTreeNode[]>((acc, node) => {
// 克隆节点避免修改原始数据
const newNode = { ...node }
// 先创建所有节点的映射 // 检查当前节点是否匹配搜索条件
flatData.forEach((item) => { let matches = true
map.set(item.ID, { ...item, children: [] })
})
// 构建树形结构 if (filters.perm_name && !newNode.perm_name.includes(filters.perm_name)) {
map.forEach((item) => { matches = false
if (item.parent_id && map.has(item.parent_id)) {
const parent = map.get(item.parent_id)!
if (!parent.children) {
parent.children = []
} }
parent.children.push(item) if (filters.perm_code && !newNode.perm_code.includes(filters.perm_code)) {
} else { matches = false
// 没有父节点的是根节点
result.push(item)
} }
}) if (filters.perm_type !== undefined && newNode.perm_type !== filters.perm_type) {
matches = false
// 递归排序
const sortTree = (nodes: Permission[]): Permission[] => {
return nodes
.sort((a, b) => (a.sort || 0) - (b.sort || 0))
.map((node) => ({
...node,
children: node.children && node.children.length > 0 ? sortTree(node.children) : undefined
}))
} }
return sortTree(result) // 递归过滤子节点
if (newNode.children && newNode.children.length > 0) {
newNode.children = filterPermissionTree(newNode.children, filters)
} }
// 获取权限列表 // 如果当前节点匹配或有匹配的子节点,则保留
if (matches || (newNode.children && newNode.children.length > 0)) {
acc.push(newNode)
}
return acc
}, [])
}
// 获取权限树
const getPermissionList = async () => { const getPermissionList = async () => {
try { try {
const response = await PermissionService.getPermissions(searchForm) const response = await PermissionService.getPermissionTree()
if (response.code === 0) { if (response.code === 0) {
const flatData = response.data.items || [] originalPermissionTree.value = response.data || []
// 将扁平数据转换为树形结构
permissionList.value = buildTreeData(flatData) // 应用搜索过滤
const hasFilters =
searchForm.perm_name || searchForm.perm_code || searchForm.perm_type !== undefined
if (hasFilters) {
permissionList.value = filterPermissionTree(originalPermissionTree.value, searchForm)
} else {
permissionList.value = originalPermissionTree.value
}
// 构建权限树选项 // 构建权限树选项
buildPermissionTreeOptions() buildPermissionTreeOptions()
} }
@@ -367,14 +349,14 @@
// 构建权限树选项 // 构建权限树选项
const buildPermissionTreeOptions = () => { const buildPermissionTreeOptions = () => {
const buildTree = (list: Permission[]): any[] => { const buildTree = (list: PermissionTreeNode[]): any[] => {
return list.map((item) => ({ return list.map((item) => ({
value: item.ID, value: item.id,
label: item.perm_name, label: item.perm_name,
children: item.children && item.children.length > 0 ? buildTree(item.children) : undefined children: item.children && item.children.length > 0 ? buildTree(item.children) : undefined
})) }))
} }
permissionTreeOptions.value = buildTree(permissionList.value) permissionTreeOptions.value = buildTree(originalPermissionTree.value)
} }
// 搜索 // 搜索
@@ -394,7 +376,7 @@
} }
// 显示对话框 // 显示对话框
const showDialog = (type: string, row?: any) => { const showDialog = (type: string, row?: PermissionTreeNode) => {
dialogVisible.value = true dialogVisible.value = true
dialogType.value = type dialogType.value = type
@@ -404,12 +386,14 @@
if (type === 'edit' && row) { if (type === 'edit' && row) {
currentRow.value = row currentRow.value = row
currentPermissionId.value = row.ID currentPermissionId.value = row.id
// 需要从API获取完整的权限数据因为树节点可能不包含所有字段
// 暂时使用树节点的数据
Object.assign(form, { Object.assign(form, {
perm_name: row.perm_name, perm_name: row.perm_name,
perm_code: row.perm_code, perm_code: row.perm_code,
perm_type: row.perm_type, perm_type: row.perm_type,
parent_id: row.parent_id, parent_id: undefined, // 树结构中没有parent_id需要从API获取
url: row.url || '', url: row.url || '',
platform: row.platform || 'all', platform: row.platform || 'all',
sort: row.sort || 0 sort: row.sort || 0
@@ -421,7 +405,7 @@
} }
// 删除权限 // 删除权限
const deletePermission = (row: Permission) => { const deletePermission = (row: PermissionTreeNode) => {
// 检查是否有子权限 // 检查是否有子权限
if (row.children && row.children.length > 0) { if (row.children && row.children.length > 0) {
ElMessage.warning('该权限下存在子权限,请先删除子权限') ElMessage.warning('该权限下存在子权限,请先删除子权限')
@@ -435,7 +419,7 @@
}) })
.then(async () => { .then(async () => {
try { try {
await PermissionService.deletePermission(row.ID) await PermissionService.deletePermission(row.id)
ElMessage.success('删除成功') ElMessage.success('删除成功')
await getPermissionList() await getPermissionList()
} catch (error) { } catch (error) {
@@ -489,21 +473,6 @@
}) })
} }
// 状态切换
const handleStatusChange = async (row: any, newStatus: number) => {
const oldStatus = row.status
// 先更新UI
row.status = newStatus
try {
await PermissionService.updatePermission(row.ID, { status: newStatus })
ElMessage.success('状态切换成功')
} catch (error) {
// 切换失败,恢复原状态
row.status = oldStatus
console.error(error)
}
}
// 页面加载时获取权限列表 // 页面加载时获取权限列表
onMounted(() => { onMounted(() => {
getPermissionList() getPermissionList()

View File

@@ -88,16 +88,20 @@
</ElDialog> </ElDialog>
<!-- 分配权限对话框 --> <!-- 分配权限对话框 -->
<ElDialog v-model="permissionDialogVisible" title="分配权限" width="500px"> <ElDialog v-model="permissionDialogVisible" title="分配权限" width="800px">
<div class="permission-assignment-container">
<!-- 左侧权限树用于添加 -->
<div class="permission-tree-section">
<div class="section-title">可分配权限</div>
<ElTree <ElTree
ref="permissionTreeRef" ref="permissionTreeRef"
:data="permissionTreeData" :data="permissionTreeData"
show-checkbox show-checkbox
node-key="id" node-key="id"
:default-checked-keys="selectedPermissions"
:props="{ children: 'children', label: 'label' }" :props="{ children: 'children', label: 'label' }"
:default-expand-all="false" :default-expand-all="false"
class="permission-tree" class="permission-tree"
@check="handlePermissionCheck"
> >
<template #default="{ node, data }"> <template #default="{ node, data }">
<span style="display: flex; align-items: center; gap: 8px"> <span style="display: flex; align-items: center; gap: 8px">
@@ -111,16 +115,44 @@
</span> </span>
</template> </template>
</ElTree> </ElTree>
</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"
>
<ElTag
:type="perm.perm_type === 1 ? 'info' : 'success'"
size="small"
>
{{ perm.perm_type === 1 ? '菜单' : '按钮' }}
</ElTag>
<span class="perm-name">{{ perm.perm_name }}</span>
<ElButton
type="danger"
size="small"
link
@click="removePermission(perm.id)"
>
移除
</ElButton>
</div>
<ElEmpty
v-if="assignedPermissionsList.length === 0"
description="暂无已分配权限"
:image-size="80"
/>
</div>
</div>
</div>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<ElButton @click="permissionDialogVisible = false">取消</ElButton> <ElButton @click="permissionDialogVisible = false">关闭</ElButton>
<ElButton
type="primary"
@click="handleAssignPermissions"
:loading="permissionSubmitLoading"
>
提交
</ElButton>
</div> </div>
</template> </template>
</ElDialog> </ElDialog>
@@ -140,7 +172,7 @@
ElSwitch ElSwitch
} from 'element-plus' } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import type { PlatformRole, Permission } from '@/types/api' import type { PlatformRole, PermissionTreeNode } 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 { useAuth } from '@/composables/useAuth'
@@ -162,8 +194,10 @@
const currentRoleId = ref<number>(0) const currentRoleId = ref<number>(0)
const selectedPermissions = ref<number[]>([]) const selectedPermissions = ref<number[]>([])
const originalPermissions = ref<number[]>([]) // 保存原始权限,用于对比 const originalPermissions = ref<number[]>([]) // 保存原始权限,用于对比
const allPermissions = ref<Permission[]>([])
const permissionTreeData = ref<any[]>([]) const permissionTreeData = ref<any[]>([])
const assignedPermissionsList = ref<any[]>([]) // 已分配的权限列表(用于右侧显示)
const allPermissionsFlat = ref<PermissionTreeNode[]>([]) // 扁平化的所有权限数据
const isInitializingTree = ref(false) // 标记是否正在初始化树的选中状态
// 搜索表单初始值 // 搜索表单初始值
const initialSearchState = { const initialSearchState = {
@@ -319,73 +353,62 @@
onMounted(() => { onMounted(() => {
getTableData() getTableData()
loadAllPermissions()
}) })
// 将扁平数据转换为树形结构 // 将权限树扁平化为一维数组
const buildTreeData = (flatData: Permission[]): any[] => { const flattenPermissionTree = (treeNodes: PermissionTreeNode[]): PermissionTreeNode[] => {
const map = new Map<number, any>() const result: PermissionTreeNode[] = []
const result: any[] = [] const flatten = (nodes: PermissionTreeNode[]) => {
nodes.forEach((node) => {
// 先创建所有节点的映射
flatData.forEach((item) => {
map.set(item.ID, {
id: item.ID,
label: item.perm_name,
perm_type: item.perm_type,
children: []
})
})
// 构建树形结构
flatData.forEach((item) => {
const node = map.get(item.ID)!
if (item.parent_id && map.has(item.parent_id)) {
const parent = map.get(item.parent_id)!
parent.children.push(node)
} else {
// 没有父节点的是根节点
result.push(node) result.push(node)
if (node.children && node.children.length > 0) {
flatten(node.children)
} }
}) })
}
flatten(treeNodes)
return result
}
// 递归排序和清理空children // 将权限树节点转换为ElTree所需的格式
const sortAndCleanTree = (nodes: any[]): any[] => { const buildTreeData = (treeNodes: PermissionTreeNode[]): any[] => {
return nodes return treeNodes.map((node) => ({
.sort((a, b) => { id: node.id,
const aItem = flatData.find((p) => p.ID === a.id) label: node.perm_name,
const bItem = flatData.find((p) => p.ID === b.id) perm_type: node.perm_type,
return (aItem?.sort || 0) - (bItem?.sort || 0) children: node.children && node.children.length > 0 ? buildTreeData(node.children) : undefined
})
.map((node) => ({
...node,
children:
node.children && node.children.length > 0 ? sortAndCleanTree(node.children) : undefined
})) }))
} }
return sortAndCleanTree(result) // 加载所有权限树
}
// 加载所有权限列表
const loadAllPermissions = async () => { const loadAllPermissions = async () => {
try { try {
const res = await PermissionService.getPermissions({ page: 1, page_size: 1000 }) const res = await PermissionService.getPermissionTree()
if (res.code === 0) { if (res.code === 0) {
allPermissions.value = res.data.items || [] const treeData = res.data || []
// 扁平化所有权限数据,用于查找
allPermissionsFlat.value = flattenPermissionTree(treeData)
// 构建树形数据 // 构建树形数据
permissionTreeData.value = buildTreeData(allPermissions.value) permissionTreeData.value = buildTreeData(treeData)
} }
} catch (error) { } catch (error) {
console.error('获取权限列表失败:', 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 showPermissionDialog = async (row: PlatformRole) => { const showPermissionDialog = async (row: PlatformRole) => {
currentRoleId.value = row.ID currentRoleId.value = row.ID
selectedPermissions.value = [] selectedPermissions.value = []
originalPermissions.value = [] originalPermissions.value = []
assignedPermissionsList.value = []
try { try {
// 每次打开对话框时重新加载最新的权限列表 // 每次打开对话框时重新加载最新的权限列表
@@ -393,75 +416,136 @@
// 加载当前角色的权限 // 加载当前角色的权限
const res = await RoleService.getRolePermissions(row.ID) const res = await RoleService.getRolePermissions(row.ID)
if (res.code === 0 || Array.isArray(res.data)) { if (res.code === 0 || Array.isArray(res.data)) {
// 提取权限ID数组 // 提取权限ID数组
const permissions = res.data || [] const permissions = res.data || []
if (Array.isArray(permissions) && permissions.length > 0) { if (Array.isArray(permissions) && permissions.length > 0) {
// 如果返回的是权限对象数组提取ID // 如果返回的是权限对象数组提取ID或id字段
if (typeof permissions[0] === 'object' && 'ID' in permissions[0]) { if (typeof permissions[0] === 'object') {
// 优先使用ID字段如果没有则使用id字段
if ('ID' in permissions[0]) {
selectedPermissions.value = permissions.map((perm: any) => perm.ID) selectedPermissions.value = permissions.map((perm: any) => perm.ID)
} else if ('id' in permissions[0]) {
selectedPermissions.value = permissions.map((perm: any) => perm.id)
}
} else { } else {
// 如果返回的是ID数组 // 如果返回的是ID数组
selectedPermissions.value = permissions selectedPermissions.value = permissions
} }
} }
// 保存原始权限,用于后续对比 // 保存原始权限,用于后续对比
originalPermissions.value = [...selectedPermissions.value] originalPermissions.value = [...selectedPermissions.value]
// 构建已分配权限列表
buildAssignedPermissionsList()
// 数据加载完成后再打开对话框 // 数据加载完成后再打开对话框
permissionDialogVisible.value = true 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) { } catch (error) {
console.error('获取角色权限失败:', error) console.error('获取角色权限失败:', error)
} }
} }
// 提交分配权限 // 处理权限勾选(只允许添加,不允许取消)
const handleAssignPermissions = async () => { const handlePermissionCheck = async (data: any, checkedInfo: any) => {
if (!permissionTreeRef.value) return if (!permissionTreeRef.value) return
permissionSubmitLoading.value = true // 如果正在初始化树忽略此次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 { try {
// 获取选中的节点(包括半选中的父节点) // 调用API分配权限
const checkedKeys = permissionTreeRef.value.getCheckedKeys() await RoleService.assignPermissions(currentRoleId.value, newPermissions)
const halfCheckedKeys = permissionTreeRef.value.getHalfCheckedKeys()
const currentPermissions = [...checkedKeys, ...halfCheckedKeys]
// 对比原始权限和当前选中的权限,找出需要新增和移除的权限 // 更新已分配权限列表
const addedPermissions = currentPermissions.filter( selectedPermissions.value = [...selectedPermissions.value, ...newPermissions]
(id) => !originalPermissions.value.includes(id) buildAssignedPermissionsList()
)
const removedPermissions = originalPermissions.value.filter(
(id) => !currentPermissions.includes(id)
)
// 使用 Promise.all 并发执行新增和移除操作 ElMessage.success('权限添加成功')
const promises: Promise<any>[] = [] } catch (error) {
console.error('添加权限失败:', error)
ElMessage.error('权限添加失败')
// 如果有新增的权限,调用分配接口 // 如果添加失败,恢复树的选中状态
if (addedPermissions.length > 0) { nextTick(() => {
promises.push(RoleService.assignPermissions(currentRoleId.value, addedPermissions)) permissionTreeRef.value?.setCheckedKeys(selectedPermissions.value, false)
})
return
}
} }
// 如果有移除的权限,调用移除接口 // 重新设置树的选中状态为已分配的权限(阻止取消勾选)
if (removedPermissions.length > 0) { nextTick(() => {
removedPermissions.forEach((permId) => { permissionTreeRef.value?.setCheckedKeys(selectedPermissions.value, false)
promises.push(RoleService.removePermission(currentRoleId.value, permId))
}) })
} }
// 等待所有操作完成 // 移除单个权限
if (promises.length > 0) { const removePermission = async (permId: number) => {
await Promise.all(promises) try {
ElMessage.success('权限更新成功') 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 { } else {
ElMessage.info('权限未发生变化') selectedPermissions.value = permissions
}
} else {
selectedPermissions.value = []
} }
permissionDialogVisible.value = false // 重建已分配权限列表
buildAssignedPermissionsList()
// 更新树的选中状态
await nextTick()
if (permissionTreeRef.value) {
isInitializingTree.value = true
permissionTreeRef.value.setCheckedKeys(selectedPermissions.value, false)
setTimeout(() => {
isInitializingTree.value = false
}, 100)
}
}
ElMessage.success('权限移除成功')
} catch (error) { } catch (error) {
console.error(error) console.error('移除权限失败:', error)
} finally { ElMessage.error('权限移除失败')
permissionSubmitLoading.value = false
} }
} }
@@ -615,7 +699,35 @@
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.permission-assignment-container {
display: flex;
gap: 20px;
height: 500px;
.permission-tree-section,
.assigned-permissions-section {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid var(--el-border-color);
border-radius: 4px;
overflow: hidden;
.section-title {
padding: 12px 16px;
background: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color);
font-weight: 600;
font-size: 14px;
}
}
.permission-tree-section {
.permission-tree { .permission-tree {
flex: 1;
overflow-y: auto;
padding: 12px;
:deep(.el-tree-node) { :deep(.el-tree-node) {
margin: 6px 0; margin: 6px 0;
} }
@@ -625,4 +737,38 @@
line-height: 36px; line-height: 36px;
} }
} }
}
.assigned-permissions-section {
.assigned-list {
flex: 1;
overflow-y: auto;
padding: 12px;
.assigned-item {
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;
}
.el-button {
padding: 4px 8px;
}
}
}
}
}
</style> </style>