first commit

This commit is contained in:
Vicky committed 2026-10-12 17:35:07 +08:00
commit db15eb3ee0
42 files changed
+4620

No files matched your search

+13
View File
@@ -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,
}
}