first commit
This commit is contained in:
commit
db15eb3ee0
42 files changed
+4620
No files matched your search
@@ -0,0 +1,13 @@
|
||||
<script setup>
|
||||
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))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-heading"><div><div class="eyebrow">自定义菜单</div><h1>{{menu?.title || '菜单页面'}}</h1><p>这是「{{menu?.title}}」菜单的前端演示页面。</p></div></div>
|
||||
<section class="panel"><el-empty description="菜单已创建,可以继续配置页面内容。" /></section>
|
||||
</template>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup>
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { menuIconNames } from '@/data/customMenus'
|
||||
import { useMenuManagement } from '@/views/permission/composables/useMenuManagement'
|
||||
import PageHeading from '@/views/components/PageHeading.vue'
|
||||
|
||||
const { selectedKey, selectedParent, form, menuTree, parentOptions, resetForm, selectNode, saveMenu, deleteMenu } = useMenuManagement()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeading eyebrow="后台管理" title="菜单管理" description="维护系统菜单结构、路由和按钮权限。" />
|
||||
|
||||
<div class="menu-editor-layout">
|
||||
<section 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"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
:expand-on-click-node="false"
|
||||
@node-click="selectNode"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="panel menu-form-panel">
|
||||
<div class="panel-heading">
|
||||
<div><h2>{{ selectedKey ? '编辑菜单权限' : '新增菜单权限' }}</h2></div>
|
||||
<el-button v-if="selectedKey" 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-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-group>
|
||||
</el-form-item>
|
||||
<el-form-item 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>
|
||||
<div class="menu-form-footer">
|
||||
<el-button type="primary" @click="saveMenu">保存菜单</el-button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup>
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { useRoleCategories } from '@/views/permission/composables/useRoleCategories'
|
||||
import PageHeading from '@/views/components/PageHeading.vue'
|
||||
|
||||
const { dialogVisible, name, editingId, tableData, openCreateDialog, editCategory, saveCategory, deleteCategory } = useRoleCategories()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeading eyebrow="后台管理" title="角色分类" description="维护角色分类,方便按职责整理权限角色。">
|
||||
<el-button type="primary" class="primary-button" @click="openCreateDialog">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增分类
|
||||
</el-button>
|
||||
</PageHeading>
|
||||
|
||||
<section class="panel">
|
||||
<el-table :data="tableData" row-key="id" border class="drama-el-table" 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">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" @click="editCategory(row)">编辑</el-button>
|
||||
<el-button type="danger" @click="deleteCategory(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="editingId ? '编辑角色分类' : '新增角色分类'"
|
||||
width="440px"
|
||||
>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="分类名称" required>
|
||||
<el-input
|
||||
v-model="name"
|
||||
maxlength="30"
|
||||
placeholder="请输入分类名称"
|
||||
@keyup.enter="saveCategory"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveCategory">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,92 @@
|
||||
<script setup>
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { useRolePermissionEditor } from '@/views/permission/composables/useRolePermissionEditor'
|
||||
import PageHeading from '@/views/components/PageHeading.vue'
|
||||
|
||||
const {
|
||||
roleData,
|
||||
rolePermissions,
|
||||
selectedRole,
|
||||
selectedRoleRecord,
|
||||
canEditSelectedRole,
|
||||
permissionTree,
|
||||
treeRef,
|
||||
dialogVisible,
|
||||
newRoleName,
|
||||
selectRole,
|
||||
handleCheck,
|
||||
addRole,
|
||||
} = useRolePermissionEditor()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeading eyebrow="后台管理" title="角色权限配置" description="角色等级数字越小权限越高,低等级角色不能修改高等级角色。">
|
||||
<el-button type="primary" class="primary-button" @click="dialogVisible = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增角色
|
||||
</el-button>
|
||||
</PageHeading>
|
||||
|
||||
<div class="role-permission-layout">
|
||||
<section class="panel role-table-panel">
|
||||
<div class="panel-heading">
|
||||
<div><h2>角色列表({{ roleData.length }})</h2></div>
|
||||
</div>
|
||||
<el-table
|
||||
:data="roleData"
|
||||
row-key="id"
|
||||
border
|
||||
class="drama-el-table role-config-table"
|
||||
highlight-current-row
|
||||
:current-row-key="selectedRoleRecord?.id"
|
||||
@row-click="selectRole"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="category" label="角色分类" min-width="120" />
|
||||
<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 }">
|
||||
<el-tag type="success" effect="light">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<section class="panel permission-tree-panel">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<h2>编辑:{{ selectedRole }} 菜单权限</h2>
|
||||
<p v-if="!canEditSelectedRole" class="permission-warning">
|
||||
当前账号等级较低,不能修改高等级角色。
|
||||
</p>
|
||||
<p v-else>勾选菜单权限以配置角色可访问的功能。</p>
|
||||
</div>
|
||||
</div>
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="permissionTree"
|
||||
node-key="id"
|
||||
show-checkbox
|
||||
check-strictly
|
||||
:default-expand-all="true"
|
||||
:expand-on-click-node="false"
|
||||
:props="{ label: 'label', children: 'children', disabled: () => !canEditSelectedRole }"
|
||||
@check-change="handleCheck"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" title="新增角色" width="440px">
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="角色名称" required>
|
||||
<el-input v-model="newRoleName" maxlength="30" placeholder="请输入角色名称" @keyup.enter="addRole" />
|
||||
</el-form-item>
|
||||
<el-alert title="新角色等级将低于当前账号,等级数字越大权限越低。" type="info" :closable="false" />
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="addRole">创建角色</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,106 @@
|
||||
import { computed, 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'
|
||||
|
||||
const builtInMenus = [
|
||||
{ id: 'builtin-content', label: '内容管理', children: [
|
||||
{ id: 'builtin-dramas', label: '短剧管理', route: '/dramas', permission: '查看短剧列表' },
|
||||
{ id: 'builtin-episodes', label: '剧集管理', route: '/episodes', permission: '查看剧集列表' },
|
||||
] },
|
||||
{ id: 'builtin-admin', label: '后台管理', children: [
|
||||
{ id: 'builtin-access', label: '角色权限配置', route: '/access', permission: '角色与权限配置' },
|
||||
{ id: 'builtin-users', label: '管理员用户列表', route: '/admin-users', permission: '管理员用户列表' },
|
||||
{ id: 'builtin-menus', label: '菜单管理', route: '/menus', permission: '菜单管理' },
|
||||
{ id: 'builtin-categories', label: '角色分类', route: '/role-categories', permission: '角色分类管理' },
|
||||
] },
|
||||
]
|
||||
|
||||
function buildCustomNodes(parent) {
|
||||
return customMenus.filter(menu => (menu.parent || '后台管理') === parent).map(menu => ({
|
||||
id: menu.key,
|
||||
label: menu.title,
|
||||
menu,
|
||||
children: buildCustomNodes(menu.title),
|
||||
}))
|
||||
}
|
||||
|
||||
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)])
|
||||
|
||||
function resetForm(parent = selectedParent.value) {
|
||||
selectedKey.value = ''
|
||||
Object.assign(form, { key: '', title: '', route: '', permission: '', parent, type: '页面', icon: 'Document', method: '', sort: 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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
const savedMenu = customMenus.find(menu => menu.key === key)
|
||||
if (savedMenu?.type !== '按钮') registerCustomMenu(savedMenu)
|
||||
saveCustomMenus()
|
||||
selectedKey.value = key
|
||||
ElMessage.success(isNew ? '菜单已添加' : '菜单已保存')
|
||||
}
|
||||
async function deleteMenu() {
|
||||
const menu = customMenus.find(item => item.key === selectedKey.value)
|
||||
if (!menu) return ElMessage.warning('请先在左侧选择一个自定义菜单')
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除「${menu.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 { selectedKey, selectedParent, form, menuTree, parentOptions, resetForm, selectNode, saveMenu, deleteMenu }
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { roleData } from '../../../data/roles'
|
||||
|
||||
export function useRoleCategories() {
|
||||
const dialogVisible = ref(false)
|
||||
const name = 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,
|
||||
})))
|
||||
|
||||
function openCreateDialog() {
|
||||
editingId.value = null
|
||||
name.value = ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
function editCategory(category) {
|
||||
editingId.value = category.id
|
||||
name.value = category.name
|
||||
dialogVisible.value = true
|
||||
}
|
||||
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 })
|
||||
} else {
|
||||
categories.value.push({ id: Date.now(), name: value, description: '自定义角色分类' })
|
||||
}
|
||||
dialogVisible.value = false
|
||||
ElMessage.success('角色分类已保存')
|
||||
}
|
||||
async function deleteCategory(category) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除「${category.name}」分类吗?`, '删除确认', {
|
||||
confirmButtonText: '确认删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
confirmButtonClass: 'el-button--danger',
|
||||
})
|
||||
categories.value = categories.value.filter(item => item.id !== category.id)
|
||||
ElMessage.success('分类已删除')
|
||||
} catch {
|
||||
// 用户取消时不删除分类。
|
||||
}
|
||||
}
|
||||
|
||||
return { dialogVisible, name, editingId, tableData, openCreateDialog, editCategory, saveCategory, deleteCategory }
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
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'
|
||||
|
||||
export function useRolePermissionEditor() {
|
||||
const { rolePermissions, updateRolePermission, createRole } = useRbac()
|
||||
const { currentRole } = useAuth()
|
||||
const selectedRole = ref(roleData[0].name)
|
||||
const dialogVisible = ref(false)
|
||||
const newRoleName = ref('')
|
||||
const treeRef = ref()
|
||||
const syncingTree = ref(false)
|
||||
const permissionTree = computed(() => {
|
||||
const groups = permissionGroups.map(group => ({
|
||||
id: `group-${group.name}`,
|
||||
label: group.name,
|
||||
children: group.items.map(permission => ({ id: permission, label: permission, permission })),
|
||||
}))
|
||||
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)
|
||||
|
||||
watch(selectedRole, async role => {
|
||||
syncingTree.value = true
|
||||
await nextTick()
|
||||
treeRef.value?.setCheckedKeys(rolePermissions[role] || [])
|
||||
await nextTick()
|
||||
syncingTree.value = false
|
||||
}, { immediate: true })
|
||||
|
||||
function selectRole(row) { selectedRole.value = row.name }
|
||||
function handleCheck(data, checked) {
|
||||
if (!syncingTree.value && data.permission && canEditSelectedRole.value) {
|
||||
updateRolePermission(selectedRole.value, data.permission, checked)
|
||||
}
|
||||
}
|
||||
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(), name, description: '自定义权限角色', memberCount: 0,
|
||||
category: '自定义角色', level: currentLevel.value + 1, status: '启用',
|
||||
})
|
||||
createRole(name)
|
||||
selectedRole.value = name
|
||||
newRoleName.value = ''
|
||||
dialogVisible.value = false
|
||||
ElMessage.success('角色已创建')
|
||||
}
|
||||
|
||||
return {
|
||||
roleData, rolePermissions, selectedRole, selectedRoleRecord, canEditSelectedRole,
|
||||
permissionTree, treeRef, dialogVisible, newRoleName, selectRole, handleCheck, addRole,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user