chore(web): 收口前几轮后台改动(品牌 / 媒资上传 / 轮播图 / 类型管理 / 操作日志 / App 用户)

为什么:这些改动在前几轮就已完成并在 :3005 验证过,但一直留在工作区没有入库。
下一步 3-4 的四个页面依赖其中的公共样式入口(src/styles/business/index.css)与
api/admin 的公共类型(PageResult 等),如果不先收口,新页面的提交就会夹带旧改动,
既说不清 diff 边界,也让 index.css 这类多轮共用的文件长期处于半提交状态。

本提交只做归档,不改行为:
- 品牌与接口基址改成 FruitShorts / 短剧服务(.env.*、README、vite 代理)
- 去掉前端 mock 数据目录(src/data、src/permissions、stores/auth),改为走真实接口
- 新增 api/admin 下的接口封装与 src/utils/uploader.ts(媒资分片直传)
- 新增/改造:轮播图管理、类型管理、操作日志、App 用户管理、后台布局与公共样式
This commit is contained in:
Vicky committed 2026-10-15 13:28:01 +08:00
1 parent 40546bfd01
commit 44c2c46a68
59 files changed
+4082 -982

No files matched your search

+4 -5
View File
@@ -1,17 +1,16 @@
<script setup>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { customMenus } from '@/data/customMenus'
const route = useRoute()
const menu = computed(() => customMenus.find((item) => item.key === route.meta.menuKey))
const title = computed(() => (route.meta?.title as string) || '菜单页面')
</script>
<template>
<div class="page-heading"
><div
><div class="eyebrow">自定义菜单</div><h1>{{ menu?.title || '菜单页面' }}</h1
><p>这是「{{ menu?.title }}」菜单的前端演示页面。</p></div
><div class="eyebrow">自定义菜单</div><h1>{{ title }}</h1
><p>该菜单已注册,可在菜单管理中把组件路径指向已有页面。</p></div
></div
>
<section class="panel"><el-empty description="菜单已创建,可以继续配置页面内容。" /></section>
+50 -53
View File
@@ -1,42 +1,30 @@
<script setup>
import { Plus } from '@element-plus/icons-vue'
import { menuIconNames } from '@/data/customMenus'
import { useMenuManagement } from '@/views/permission/composables/useMenuManagement'
import { menuIconNames, useMenuManagement } from '@/views/permission/composables/useMenuManagement'
import PageHeading from '@/views/components/PageHeading.vue'
const {
selectedKey,
selectedParent,
form,
menuTree,
parentOptions,
resetForm,
selectNode,
saveMenu,
deleteMenu
} = useMenuManagement()
const { treeData, loading, form, parentOptions, resetForm, selectNode, saveMenu, deleteMenu } =
useMenuManagement()
</script>
<template>
<PageHeading
eyebrow="后台管理"
title="菜单管理"
description="维护系统菜单结构、路由和按钮权限。"
/>
<PageHeading eyebrow="后台管理" title="菜单管理" description="维护系统菜单结构、路由与按钮权限。">
<el-button type="primary" @click="resetForm(0)">
<el-icon><Plus /></el-icon>
新增菜单
</el-button>
</PageHeading>
<div class="menu-editor-layout">
<section class="panel menu-tree-panel">
<section v-loading="loading" class="panel menu-tree-panel">
<div class="panel-heading">
<div><h2>系统菜单</h2></div>
<el-button type="primary" @click="resetForm(selectedParent)">
<el-icon><Plus /></el-icon>
新增菜单
</el-button>
</div>
<el-tree
:data="menuTree"
:data="treeData"
node-key="id"
default-expand-all
highlight-current
:expand-on-click-node="false"
@node-click="selectNode"
/>
@@ -45,51 +33,60 @@ const {
<section class="panel menu-form-panel">
<div class="panel-heading">
<div
><h2>{{ selectedKey ? '编辑菜单权限' : '新增菜单权限' }}</h2></div
><h2>{{ form.id ? '编辑菜单' : '新增菜单' }}</h2></div
>
<el-button v-if="selectedKey" type="danger" plain @click="deleteMenu">删除菜单</el-button>
<el-button v-if="form.id" type="danger" plain @click="deleteMenu">删除菜单</el-button>
</div>
<el-form label-width="110px" label-position="right" class="menu-editor-form">
<el-form-item label="菜单名称" required>
<el-input v-model="form.title" maxlength="30" />
</el-form-item>
<el-form-item label="对应路由" :required="form.type !== '按钮'">
<el-input v-model="form.route" placeholder="例如 /reports" />
</el-form-item>
<el-form-item label="菜单标识" required>
<el-input
v-model="form.permission"
:placeholder="`自动生成:访问菜单:${form.title || '菜单名'}`"
/>
</el-form-item>
<el-form-item label="上级菜单">
<el-select v-model="form.parent">
<el-option v-for="item in parentOptions" :key="item" :label="item" :value="item" />
</el-select>
<el-input v-model="form.title" maxlength="20" />
</el-form-item>
<el-form-item label="菜单类型">
<el-radio-group v-model="form.type">
<el-radio value="路径">路径</el-radio>
<el-radio value="页面">页面</el-radio>
<el-radio value="按钮">按钮</el-radio>
<el-radio value="CATALOG">目录</el-radio>
<el-radio value="MENU">菜单</el-radio>
<el-radio value="BUTTON">按钮</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="菜单图标">
<el-form-item label="上级菜单">
<el-select v-model="form.parentId">
<el-option
v-for="item in parentOptions"
:key="item.id"
:label="item.label"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item v-if="form.type !== 'BUTTON'" label="路由地址" required>
<el-input v-model="form.routePath" placeholder="目录填 /content,子菜单填 dramas" />
</el-form-item>
<el-form-item v-if="form.type === 'MENU'" label="路由名称">
<el-input v-model="form.routeName" placeholder="例如 dramas" />
</el-form-item>
<el-form-item v-if="form.type === 'MENU'" label="组件路径">
<el-input
v-model="form.component"
placeholder="例如 views/contentManagement/DramaManagementView"
/>
</el-form-item>
<el-form-item label="权限标识">
<el-input v-model="form.permissionCode" placeholder="例如 drama:list" />
</el-form-item>
<el-form-item v-if="form.type !== 'BUTTON'" label="菜单图标">
<el-select v-model="form.icon" clearable placeholder="请选择">
<el-option v-for="icon in menuIconNames" :key="icon" :label="icon" :value="icon" />
</el-select>
</el-form-item>
<el-form-item label="使用方法">
<el-input
v-model="form.method"
type="textarea"
:rows="3"
placeholder="可填写按钮权限对应的操作说明"
/>
</el-form-item>
<el-form-item label="菜单排序">
<el-input-number v-model="form.sort" :min="1" :max="999" />
</el-form-item>
<el-form-item label="是否显示">
<el-switch v-model="form.visible" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="form.status" :active-value="1" :inactive-value="0" />
</el-form-item>
</el-form>
<div class="menu-form-footer">
<el-button type="primary" @click="saveMenu">保存菜单</el-button>
+9 -3
View File
@@ -6,6 +6,7 @@ import PageHeading from '@/views/components/PageHeading.vue'
const {
dialogVisible,
name,
description,
editingId,
tableData,
openCreateDialog,
@@ -36,9 +37,11 @@ const {
empty-text="暂无角色分类"
>
<el-table-column type="index" label="序号" width="100" />
<el-table-column prop="id" label="ID" width="140" />
<el-table-column prop="name" label="分类名称" min-width="260" />
<el-table-column label="操作" width="230">
<el-table-column prop="id" label="ID" width="120" />
<el-table-column prop="name" label="分类名称" min-width="180" />
<el-table-column prop="description" label="描述" min-width="240" />
<el-table-column prop="roleCount" label="角色数" width="100" />
<el-table-column label="操作" width="200">
<template #default="{ row }">
<el-button type="primary" @click="editCategory(row)">编辑</el-button>
<el-button type="danger" @click="deleteCategory(row)">删除</el-button>
@@ -61,6 +64,9 @@ const {
@keyup.enter="saveCategory"
/>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="description" maxlength="50" placeholder="选填,例如:负责内容维护" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
+34 -2
View File
@@ -5,6 +5,7 @@ import PageHeading from '@/views/components/PageHeading.vue'
const {
roleData,
categories,
selectedRole,
selectedRoleRecord,
canEditSelectedRole,
@@ -12,10 +13,18 @@ const {
treeRef,
dialogVisible,
newRoleName,
newRoleCategoryId,
selectRole,
handleCheck,
addRole
addRole,
removeRole
} = useRolePermissionEditor()
/** 表格里点删除时先选中该行,再走统一的删除流程。 */
function removeRoleRow(row) {
selectRole(row)
removeRole()
}
</script>
<template>
@@ -51,9 +60,22 @@ const {
<el-table-column prop="name" label="角色名称" min-width="120" />
<el-table-column prop="description" label="描述" min-width="180" />
<el-table-column prop="level" label="角色等级" width="100" sortable />
<el-table-column label="权限数" width="90">
<template #default="{ row }">{{ row.permissions?.length || 0 }}</template>
</el-table-column>
<el-table-column prop="memberCount" label="成员" width="80" />
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag type="success" effect="light">{{ row.status }}</el-tag>
<el-tag :type="row.status === 1 ? 'success' : 'info'" effect="light">{{
row.statusText
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="90" fixed="right">
<template #default="{ row }">
<el-button type="danger" link :disabled="row.builtin" @click="removeRoleRow(row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
@@ -92,6 +114,16 @@ const {
@keyup.enter="addRole"
/>
</el-form-item>
<el-form-item label="角色分类">
<el-select v-model="newRoleCategoryId" clearable placeholder="请选择角色分类">
<el-option
v-for="item in categories"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-alert
title="新角色等级将低于当前账号,等级数字越大权限越低。"
type="info"
@@ -1,192 +1,165 @@
import { computed, reactive, ref } from 'vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { customMenus, saveCustomMenus } from '../../../data/customMenus'
import { useRbac } from '../../../stores/rbac'
import router, { registerCustomMenu } from '../../../router'
import { createMenuApi, deleteMenuApi, getMenuTreeApi, updateMenuApi } from '@/api/admin/role'
const builtInMenus = [
{
id: 'builtin-content',
label: '内容管理',
children: [
{
id: 'builtin-dramas',
label: '短剧管理',
route: '/content/dramas',
permission: '查看短剧列表'
},
{
id: 'builtin-episodes',
label: '剧集管理',
route: '/content/episodes',
permission: '查看剧集列表'
}
]
},
{
id: 'builtin-admin',
label: '后台管理',
children: [
{
id: 'builtin-access',
label: '角色权限配置',
route: '/system/access',
permission: '角色与权限配置'
},
{
id: 'builtin-users',
label: '管理员用户列表',
route: '/system/admin-users',
permission: '管理员用户列表'
},
{ id: 'builtin-menus', label: '菜单管理', route: '/system/menus', permission: '菜单管理' },
{
id: 'builtin-categories',
label: '角色分类',
route: '/system/role-categories',
permission: '角色分类管理'
}
]
}
export const menuIconNames = [
'Document',
'DataAnalysis',
'Picture',
'User',
'Setting',
'VideoCamera',
'Menu',
'Grid'
]
function buildCustomNodes(parent) {
return customMenus
.filter((menu) => (menu.parent || '后台管理') === parent)
.map((menu) => ({
id: menu.key,
label: menu.title,
menu,
children: buildCustomNodes(menu.title)
}))
}
/** 菜单管理:直接读写后端 sys_menu,变更后重新登录生效。 */
export function useMenuManagement() {
const { createPermission, removePermission } = useRbac()
const selectedKey = ref('')
const selectedParent = ref('后台管理')
const form = reactive({
key: '',
title: '',
route: '',
permission: '',
parent: '后台管理',
type: '页面',
icon: 'Document',
method: '',
sort: 1
})
const menuTree = computed(() =>
builtInMenus.map((group) => ({
...group,
children: [...group.children, ...buildCustomNodes(group.label)]
}))
)
const parentOptions = computed(() => [
'内容管理',
'后台管理',
...customMenus.filter((menu) => menu.type !== '按钮').map((menu) => menu.title)
])
const menuTree = ref([])
const loading = ref(false)
function resetForm(parent = selectedParent.value) {
selectedKey.value = ''
const form = reactive({
id: null,
parentId: 0,
title: '',
routePath: '',
routeName: '',
component: '',
icon: '',
permissionCode: '',
type: 'MENU',
sort: 1,
visible: 1,
status: 1
})
function mapNodes(nodes) {
return (nodes || []).map((node) => ({
id: node.id,
label: node.meta?.title || node.path || String(node.id),
raw: node,
children: node.children?.length ? mapNodes(node.children) : undefined
}))
}
const treeData = computed(() => mapNodes(menuTree.value))
const parentOptions = computed(() => {
const options = [{ id: 0, label: '顶级菜单' }]
const walk = (nodes, depth) => {
;(nodes || []).forEach((node) => {
if (node.type !== 'BUTTON') {
options.push({
id: node.id,
label: `${' '.repeat(depth)}${node.meta?.title || node.path || node.id}`
})
}
if (node.children?.length) walk(node.children, depth + 1)
})
}
walk(menuTree.value, 0)
return options
})
async function load() {
loading.value = true
try {
const res = await getMenuTreeApi()
menuTree.value = res.data || []
} finally {
loading.value = false
}
}
function resetForm(parentId = 0) {
Object.assign(form, {
key: '',
id: null,
parentId,
title: '',
route: '',
permission: '',
parent,
type: '页面',
icon: 'Document',
method: '',
sort: 1
routePath: '',
routeName: '',
component: '',
icon: '',
permissionCode: '',
type: 'MENU',
sort: 1,
visible: 1,
status: 1
})
}
function selectNode(node) {
if (node.menu) {
selectedKey.value = node.menu.key
selectedParent.value = node.menu.parent || '后台管理'
Object.assign(form, {
...node.menu,
route: node.menu.route || `/custom/${node.menu.key}`,
permission: node.menu.permission || `访问菜单:${node.menu.title}`,
type: node.menu.type || '页面',
method: node.menu.method || '',
sort: node.menu.sort || 1
})
} else if (node.children) {
selectedParent.value = node.label
resetForm(node.label)
}
const raw = node.raw || {}
Object.assign(form, {
id: raw.id,
parentId: raw.parentId ?? 0,
title: raw.meta?.title || '',
routePath: raw.path || '',
routeName: raw.name || '',
component: raw.component === '#' ? '' : raw.component || '',
icon: raw.icon || '',
permissionCode: raw.permissionCode || '',
type: raw.type || 'MENU',
sort: raw.sort ?? 1,
visible: raw.visible ?? 1,
status: raw.status ?? 1
})
}
function saveMenu() {
async function saveMenu() {
if (!form.title.trim()) return ElMessage.warning('请填写菜单名称')
if (form.type !== '按钮' && !form.route.trim()) return ElMessage.warning('请填写对应路由')
const duplicate = customMenus.some(
(menu) => menu.title === form.title.trim() && menu.key !== selectedKey.value
)
if (duplicate) return ElMessage.warning('菜单名称已存在')
const isNew = !selectedKey.value
const key = selectedKey.value || `${Date.now()}`
const existingMenu = customMenus.find((menu) => menu.key === key)
const permission = form.permission.trim() || `访问菜单:${form.title.trim()}`
if (existingMenu) {
if (router.hasRoute(`custom-menu-${key}`)) router.removeRoute(`custom-menu-${key}`)
if (existingMenu.permission !== permission) {
removePermission(existingMenu.permission)
createPermission(permission)
}
Object.assign(existingMenu, {
...form,
key,
title: form.title.trim(),
permission,
route: form.route.trim(),
sort: Number(form.sort) || 1
})
} else {
customMenus.push({
...form,
key,
title: form.title.trim(),
permission,
route: form.route.trim(),
sort: Number(form.sort) || 1
})
createPermission(permission)
if (form.type !== 'BUTTON' && !form.routePath.trim()) {
return ElMessage.warning('请填写对应路由')
}
const savedMenu = customMenus.find((menu) => menu.key === key)
if (savedMenu?.type !== '按钮') registerCustomMenu(savedMenu)
saveCustomMenus()
selectedKey.value = key
ElMessage.success(isNew ? '菜单已添加' : '菜单已保存')
const payload = {
parentId: form.parentId || 0,
title: form.title.trim(),
routePath: form.routePath.trim(),
routeName: form.routeName.trim(),
component: form.type === 'CATALOG' ? '#' : form.component.trim(),
icon: form.icon,
permissionCode: form.permissionCode.trim(),
type: form.type,
sort: Number(form.sort) || 1,
visible: form.visible,
status: form.status
}
const isCreate = !form.id
if (isCreate) {
await createMenuApi(payload)
} else {
await updateMenuApi(form.id, payload)
}
await load()
resetForm()
ElMessage.success(isCreate ? '菜单已添加,重新登录后生效' : '菜单已保存,重新登录后生效')
}
async function deleteMenu() {
const menu = customMenus.find((item) => item.key === selectedKey.value)
if (!menu) return ElMessage.warning('请先在左侧选择一个自定义菜单')
if (!form.id) return ElMessage.warning('请先在左侧选择一个菜单')
try {
await ElMessageBox.confirm(`确认删除「${menu.title}」菜单吗?`, '删除确认', {
await ElMessageBox.confirm(`确认删除「${form.title}」菜单吗?`, '删除确认', {
confirmButtonText: '确认删除',
cancelButtonText: '取消',
type: 'warning',
confirmButtonClass: 'el-button--danger'
})
customMenus.splice(customMenus.indexOf(menu), 1)
removePermission(menu.permission)
if (router.hasRoute(`custom-menu-${menu.key}`)) router.removeRoute(`custom-menu-${menu.key}`)
saveCustomMenus()
resetForm()
ElMessage.success('菜单已删除')
} catch {
// 用户取消时不删除菜单。
return
}
await deleteMenuApi(form.id)
ElMessage.success('菜单已删除,重新登录后生效')
resetForm()
await load()
}
onMounted(load)
return {
selectedKey,
selectedParent,
form,
menuTree,
treeData,
loading,
form,
parentOptions,
resetForm,
selectNode,
@@ -1,57 +1,55 @@
import { computed, ref } from 'vue'
import { onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { roleData } from '../../../data/roles'
import {
createRoleCategoryApi,
deleteRoleCategoryApi,
listRoleCategoriesApi,
updateRoleCategoryApi
} from '@/api/admin/role'
export function useRoleCategories() {
const tableData = ref([])
const dialogVisible = ref(false)
const name = ref('')
const description = ref('')
const editingId = ref(null)
const categories = ref([
{ id: 1, name: '系统管理员', description: '负责账号、权限和菜单配置' },
{ id: 2, name: '内容管理', description: '负责短剧与剧集内容维护' },
{ id: 3, name: '数据分析', description: '负责查看运营数据' }
])
const tableData = computed(() =>
categories.value.map((category) => ({
...category,
roleCount: roleData.filter((role) => role.category === category.name).length
}))
)
async function load() {
const res = await listRoleCategoriesApi()
tableData.value = res.data || []
}
function openCreateDialog() {
editingId.value = null
name.value = ''
description.value = ''
dialogVisible.value = true
}
function editCategory(category) {
editingId.value = category.id
name.value = category.name
description.value = category.description || ''
dialogVisible.value = true
}
function saveCategory() {
async function saveCategory() {
const value = name.value.trim()
if (!value) return ElMessage.warning('请输入分类名称')
if (
categories.value.some(
(category) => category.name === value && category.id !== editingId.value
)
) {
return ElMessage.warning('分类名称已存在')
}
if (editingId.value) {
const category = categories.value.find((item) => item.id === editingId.value)
const oldName = category?.name
if (category) category.name = value
roleData.forEach((role) => {
if (role.category === oldName) role.category = value
})
await updateRoleCategoryApi(editingId.value, { name: value, description: description.value })
} else {
categories.value.push({ id: Date.now(), name: value, description: '自定义角色分类' })
await createRoleCategoryApi({ name: value, description: description.value })
}
dialogVisible.value = false
ElMessage.success('角色分类已保存')
await load()
}
async function deleteCategory(category) {
if (category.roleCount > 0) {
return ElMessage.warning(`「${category.name}」下还有 ${category.roleCount} 个角色,无法删除`)
}
try {
await ElMessageBox.confirm(`确定删除「${category.name}」分类吗?`, '删除确认', {
confirmButtonText: '确认删除',
@@ -59,16 +57,20 @@ export function useRoleCategories() {
type: 'warning',
confirmButtonClass: 'el-button--danger'
})
categories.value = categories.value.filter((item) => item.id !== category.id)
ElMessage.success('分类已删除')
} catch {
// 用户取消时不删除分类。
return
}
await deleteRoleCategoryApi(category.id)
ElMessage.success('分类已删除')
await load()
}
onMounted(load)
return {
dialogVisible,
name,
description,
editingId,
tableData,
openCreateDialog,
@@ -1,54 +1,77 @@
import { computed, nextTick, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { roleData } from '../../../data/roles'
import { customMenus } from '../../../data/customMenus'
import { permissionGroups } from '../../../permissions/permissions'
import { useAuth } from '../../../stores/auth'
import { useRbac } from '../../../stores/rbac'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
createRoleApi,
deleteRoleApi,
listPermissionGroupsApi,
listRoleCategoriesApi,
listRolesApi,
updateRolePermissionsApi
} from '@/api/admin/role'
import { useUserStoreWithOut } from '@/store/modules/user'
import { useRbac } from '@/stores/rbac'
export function useRolePermissionEditor() {
const { rolePermissions, updateRolePermission, createRole } = useRbac()
const { currentRole } = useAuth()
const selectedRole = ref(roleData[0].name)
const userStore = useUserStoreWithOut()
const { can } = useRbac()
const roleData = ref([])
const categories = ref([])
const permissionGroups = ref([])
const selectedRoleId = ref(null)
const dialogVisible = ref(false)
const newRoleName = ref('')
const newRoleCategoryId = ref(undefined)
const treeRef = ref()
const syncingTree = ref(false)
const permissionTree = computed(() => {
const groups = permissionGroups.map((group) => ({
const permissionTree = computed(() =>
permissionGroups.value.map((group) => ({
id: `group-${group.name}`,
label: group.name,
children: group.items.map((permission) => ({ id: permission, label: permission, permission }))
children: group.items.map((item) => ({
id: item.code,
label: item.name,
permission: item.code
}))
}))
if (customMenus.length) {
groups.push({
id: 'group-custom-menus',
label: '自定义菜单',
children: customMenus.map((menu) => ({
id: menu.permission,
label: menu.title,
permission: menu.permission
}))
})
}
return groups
})
const currentLevel = computed(
() => roleData.find((role) => role.name === currentRole.value)?.level || 1
)
const selectedRoleRecord = computed(() =>
roleData.find((role) => role.name === selectedRole.value)
)
const canEditSelectedRole = computed(
() => (selectedRoleRecord.value?.level || 1) >= currentLevel.value
)
const selectedRoleRecord = computed(() =>
roleData.value.find((role) => role.id === selectedRoleId.value)
)
const selectedRole = computed(() => selectedRoleRecord.value?.name || '—')
const currentLevel = computed(() => {
const code = userStore.getUserInfo?.role
return roleData.value.find((role) => role.code === code)?.level ?? 1
})
const canEditSelectedRole = computed(
() =>
can('system:role') &&
!!selectedRoleRecord.value &&
selectedRoleRecord.value.level >= currentLevel.value
)
async function load() {
const [rolesRes, groupsRes, categoriesRes] = await Promise.all([
listRolesApi(),
listPermissionGroupsApi(),
listRoleCategoriesApi()
])
roleData.value = rolesRes.data || []
permissionGroups.value = groupsRes.data || []
categories.value = categoriesRes.data || []
if (!selectedRoleId.value && roleData.value.length) {
selectedRoleId.value = roleData.value[0].id
}
}
watch(
selectedRole,
async (role) => {
[selectedRoleId, permissionGroups],
async () => {
syncingTree.value = true
await nextTick()
treeRef.value?.setCheckedKeys(rolePermissions[role] || [])
treeRef.value?.setCheckedKeys(selectedRoleRecord.value?.permissions || [])
await nextTick()
syncingTree.value = false
},
@@ -56,36 +79,72 @@ export function useRolePermissionEditor() {
)
function selectRole(row) {
selectedRole.value = row.name
selectedRoleId.value = row.id
}
function handleCheck(data, checked) {
if (!syncingTree.value && data.permission && canEditSelectedRole.value) {
updateRolePermission(selectedRole.value, data.permission, checked)
}
function readCheckedCodes() {
const checked = treeRef.value?.getCheckedKeys() || []
return checked.filter((key) => typeof key === 'string' && !key.startsWith('group-'))
}
function addRole() {
/** 勾选即保存:把整棵树的权限码提交给后端覆盖。 */
async function handleCheck() {
if (syncingTree.value || !canEditSelectedRole.value) return
const role = selectedRoleRecord.value
if (!role) return
const codes = readCheckedCodes()
await updateRolePermissionsApi(role.id, codes)
role.permissions = codes
ElMessage.success('权限已保存')
}
async function addRole() {
const name = newRoleName.value.trim()
if (!name) return ElMessage.warning('请输入角色名称')
if (roleData.some((role) => role.name === name)) return ElMessage.warning('角色名称已存在')
roleData.push({
id: Date.now(),
if (roleData.value.some((role) => role.name === name)) {
return ElMessage.warning('角色名称已存在')
}
await createRoleApi({
name,
description: '自定义权限角色',
memberCount: 0,
category: '自定义角色',
categoryId: newRoleCategoryId.value || categories.value[0]?.id || 0,
level: currentLevel.value + 1,
status: '启用'
description: '自定义角色',
permissions: []
})
createRole(name)
selectedRole.value = name
newRoleName.value = ''
newRoleCategoryId.value = undefined
dialogVisible.value = false
ElMessage.success('角色已创建')
await load()
const created = roleData.value.find((role) => role.name === name)
if (created) selectedRoleId.value = created.id
}
async function removeRole() {
const role = selectedRoleRecord.value
if (!role) return ElMessage.warning('请先选择角色')
if (role.builtin) return ElMessage.warning('内置角色不可删除')
try {
await ElMessageBox.confirm(`确定删除角色「${role.name}」吗?`, '删除确认', {
confirmButtonText: '确认删除',
cancelButtonText: '取消',
type: 'warning',
confirmButtonClass: 'el-button--danger'
})
} catch {
return
}
await deleteRoleApi(role.id)
ElMessage.success('角色已删除')
selectedRoleId.value = null
await load()
}
onMounted(load)
return {
roleData,
rolePermissions,
categories,
selectedRole,
selectedRoleRecord,
canEditSelectedRole,
@@ -93,8 +152,10 @@ export function useRolePermissionEditor() {
treeRef,
dialogVisible,
newRoleName,
newRoleCategoryId,
selectRole,
handleCheck,
addRole
addRole,
removeRole
}
}