feat: 增加菜单管理
Build and Deploy Vue3 / build (push) Successful in 1m26s
Build and Deploy Vue3 / deploy (push) Successful in 1m15s

This commit is contained in:
2026-04-18 16:24:57 +08:00
parent 2916c04ba5
commit 8b2251ef97
10 changed files with 2005 additions and 200 deletions
+8
View File
@@ -33,3 +33,11 @@ export const updateOrder = (data) => {
}
})
}
/**重试订单流程 */
export const retryOrderHook = (data) => {
return http2.post('/api/v1/admin/order/retry_hook', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
+64
View File
@@ -0,0 +1,64 @@
import { http2 } from "@/utils/request.js"
// ========== 后台菜单管理 ==========
/** 获取后台菜单列表 */
export const getWebRoutsList = (params) => {
return http2.get('/api/v1/admin/server/web_routs/list', { params })
}
/** 新增后台菜单 */
export const addWebRouts = (data) => {
return http2.post('/api/v1/admin/server/web_routs/add', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 修改后台菜单 */
export const updateWebRouts = (data) => {
return http2.post('/api/v1/admin/server/web_routs/update', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 删除后台菜单 */
export const deleteWebRouts = (data) => {
return http2.delete('/api/v1/admin/server/web_routs/delete', {
data,
headers: { 'Content-Type': 'multipart/form-data' }
})
}
// ========== 后台菜单权限管理 ==========
/** 获取后台菜单权限列表 */
export const getWebRoutsPermissionList = (params) => {
return http2.get('/api/v1/admin/server/web_routs/permission/list', { params })
}
/** 新增后台菜单权限 */
export const addWebRoutsPermission = (data) => {
return http2.post('/api/v1/admin/server/web_routs/permission/add', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 修改后台菜单权限 */
export const updateWebRoutsPermission = (data) => {
return http2.post('/api/v1/admin/server/web_routs/permission/update', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 删除后台菜单权限 */
export const deleteWebRoutsPermission = (data) => {
return http2.delete('/api/v1/admin/server/web_routs/permission/delete', {
data,
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 获取当前用户的后台菜单权限树 */
export const getMyWebRoutsPermission = () => {
return http2.get('/api/v1/admin/server/web_routs/my')
}
+131
View File
@@ -0,0 +1,131 @@
<template>
<div class="icon-selector">
<el-input
:model-value="modelValue"
placeholder="点击选择图标"
readonly
@click="popoverVisible = true"
>
<template #prefix>
<el-icon v-if="modelValue" :size="18">
<component :is="modelValue" />
</el-icon>
</template>
<template #suffix>
<el-icon v-if="modelValue" class="clear-btn" @click.stop="handleClear"><CircleClose /></el-icon>
</template>
</el-input>
<el-dialog v-model="popoverVisible" title="选择图标" width="680px" append-to-body>
<el-input v-model="searchKey" placeholder="搜索图标名称" clearable class="icon-search">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<div class="icon-grid">
<div
v-for="name in filteredIcons"
:key="name"
class="icon-item"
:class="{ active: modelValue === name }"
@click="handleSelect(name)"
>
<el-icon :size="22"><component :is="name" /></el-icon>
<span class="icon-name">{{ name }}</span>
</div>
</div>
<div v-if="filteredIcons.length === 0" class="icon-empty">
未找到匹配的图标
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { Search, CircleClose } from '@element-plus/icons-vue'
const props = defineProps({
modelValue: { type: String, default: '' }
})
const emit = defineEmits(['update:modelValue'])
const popoverVisible = ref(false)
const searchKey = ref('')
const allIcons = Object.keys(ElementPlusIconsVue).sort()
const filteredIcons = computed(() => {
if (!searchKey.value) return allIcons
const key = searchKey.value.toLowerCase()
return allIcons.filter(name => name.toLowerCase().includes(key))
})
const handleSelect = (name) => {
emit('update:modelValue', name)
popoverVisible.value = false
searchKey.value = ''
}
const handleClear = () => {
emit('update:modelValue', '')
}
</script>
<style scoped>
.icon-selector { width: 100%; }
.icon-search { margin-bottom: 12px; }
.icon-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 8px;
max-height: 400px;
overflow-y: auto;
padding: 4px;
}
.icon-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
padding: 10px 4px;
border: 1px solid #ebeef5;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.icon-item:hover {
border-color: #409eff;
background: #ecf5ff;
color: #409eff;
}
.icon-item.active {
border-color: #409eff;
background: #409eff;
color: #fff;
}
.icon-name {
font-size: 11px;
text-align: center;
line-height: 1.2;
word-break: break-all;
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.icon-empty {
text-align: center;
color: #909399;
padding: 40px 0;
font-size: 14px;
}
.clear-btn {
cursor: pointer;
color: #c0c4cc;
transition: color 0.2s;
}
.clear-btn:hover { color: #f56c6c; }
</style>
+148
View File
@@ -0,0 +1,148 @@
<template>
<div class="menu-path-selector">
<el-input
:model-value="modelValue"
placeholder="点击从菜单中选择路径,或手动输入"
clearable
@input="$emit('update:modelValue', $event)"
>
<template #append>
<el-button @click="dialogVisible = true">
<el-icon><FolderOpened /></el-icon>
</el-button>
</template>
</el-input>
<el-dialog v-model="dialogVisible" title="选择菜单路径" width="550px" append-to-body>
<el-input v-model="searchKey" placeholder="搜索菜单名称或路径" clearable class="path-search">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<div class="menu-tree">
<el-tree
:data="filteredMenuTree"
:props="{ label: 'label', children: 'children' }"
node-key="path"
:default-expand-all="!!searchKey"
:expand-on-click-node="false"
highlight-current
@node-click="handleNodeClick"
>
<template #default="{ data }">
<div class="tree-node" :class="{ 'is-selected': modelValue === data.path, 'no-path': !data.path }">
<el-icon v-if="data.icon" :size="16" style="margin-right: 6px; flex-shrink: 0;">
<component :is="data.icon" />
</el-icon>
<span class="node-title">{{ data.title }}</span>
<el-tag v-if="data.path" size="small" type="info" class="node-path">{{ data.path }}</el-tag>
</div>
</template>
</el-tree>
</div>
<div v-if="filteredMenuTree.length === 0" class="tree-empty">
未找到匹配的菜单
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { Search, FolderOpened } from '@element-plus/icons-vue'
import { menus } from '@/config/menus'
const props = defineProps({
modelValue: { type: String, default: '' }
})
const emit = defineEmits(['update:modelValue'])
const dialogVisible = ref(false)
const searchKey = ref('')
const buildTreeData = (menuList) => {
return menuList.map(item => {
const node = {
path: item.path || '',
title: item.title,
icon: item.icon || '',
label: item.title
}
if (item.children?.length) {
node.children = buildTreeData(item.children)
}
return node
})
}
const menuTree = computed(() => buildTreeData(menus))
const filterTree = (nodes, keyword) => {
const key = keyword.toLowerCase()
const result = []
for (const node of nodes) {
const titleMatch = node.title?.toLowerCase().includes(key)
const pathMatch = node.path?.toLowerCase().includes(key)
let filteredChildren = []
if (node.children?.length) {
filteredChildren = filterTree(node.children, keyword)
}
if (titleMatch || pathMatch || filteredChildren.length > 0) {
result.push({
...node,
children: filteredChildren.length > 0 ? filteredChildren : node.children
})
}
}
return result
}
const filteredMenuTree = computed(() => {
if (!searchKey.value) return menuTree.value
return filterTree(menuTree.value, searchKey.value)
})
const handleNodeClick = (data) => {
if (!data.path) return
emit('update:modelValue', data.path)
dialogVisible.value = false
searchKey.value = ''
}
</script>
<style scoped>
.menu-path-selector { width: 100%; }
.path-search { margin-bottom: 12px; }
.menu-tree {
max-height: 400px;
overflow-y: auto;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 8px 0;
}
.tree-node {
display: flex;
align-items: center;
padding: 2px 4px;
width: 100%;
border-radius: 4px;
}
.tree-node.is-selected {
background: #ecf5ff;
color: #409eff;
}
.tree-node.no-path {
color: #909399;
cursor: default;
}
.node-title { margin-right: 8px; font-size: 13px; }
.node-path { flex-shrink: 0; }
.tree-empty {
text-align: center;
color: #909399;
padding: 40px 0;
font-size: 14px;
}
:deep(.el-tree-node__content) { height: 36px; }
:deep(.el-tree-node__content:hover) { background-color: #f5f7fa; }
</style>
+8
View File
@@ -185,6 +185,14 @@ export const menus = [
{
path: '/system/setting-manage',
title: '配置管理'
},
{
path: '/system/menu',
title: '菜单管理',
children: [
{ path: '/system/menu-manage', title: '菜单列表' },
{ path: '/system/menu-permission', title: '菜单权限' }
]
}
]
}
+12
View File
@@ -407,6 +407,18 @@ const routes = [
name: 'SettingManage',
component: () => import('../views/system/SettingManage.vue'),
meta: { title: '配置管理' }
},
{
path: 'menu-manage',
name: 'MenuManage',
component: () => import('../views/system/MenuManage.vue'),
meta: { title: '菜单管理' }
},
{
path: 'menu-permission',
name: 'MenuPermission',
component: () => import('../views/system/MenuPermission.vue'),
meta: { title: '菜单权限' }
}
]
},
+71 -4
View File
@@ -22,6 +22,12 @@
<el-option label="已失效" value="2" />
</el-select>
</el-form-item>
<el-form-item label="错误信息">
<el-select v-model="queryParams.error" placeholder="全部" clearable style="width: 140px">
<el-option label="有错误的订单" :value="true" />
<el-option label="无错误的订单" :value="false" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon>搜索
@@ -89,11 +95,22 @@
<span>{{ row.payNum }}</span>
</template>
</el-table-column>
<el-table-column label="订单状态" width="100">
<el-table-column label="订单状态" width="120">
<template #default="{ row }">
<div style="display: flex; align-items: center; gap: 4px; flex-wrap: wrap;">
<el-tag :type="getStatusType(row.state)">
{{ getStatusText(row.state) }}
</el-tag>
<el-tag v-if="row.error" type="danger" size="small">异常</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="错误信息" min-width="250">
<template #default="{ row }">
<el-tooltip v-if="row.error" :content="row.error" placement="top" :show-after="300">
<span class="error-text">{{ row.error }}</span>
</el-tooltip>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
<el-table-column label="支付方式" width="100">
@@ -111,11 +128,12 @@
<span>{{ formatDate(row.CreatedAt) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" link @click="handleView(row)">查看</el-button>
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button>
<el-button v-if="row.error" type="danger" link @click="handleRetryOrder(row)">重试流程</el-button>
</div>
</template>
</el-table-column>
@@ -162,6 +180,10 @@
<el-descriptions-item label="创建时间">{{ formatDate(orderDetail.CreatedAt) }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ formatDate(orderDetail.UpdatedAt) }}</el-descriptions-item>
<el-descriptions-item label="参数信息">{{ orderDetail.args || '-' }}</el-descriptions-item>
<el-descriptions-item v-if="orderDetail.error" label="错误信息" :span="2">
<el-tag type="danger" size="small" style="margin-right: 6px;">异常</el-tag>
<span style="color: #f56c6c;">{{ orderDetail.error }}</span>
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ orderDetail.note || '无' }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
@@ -389,7 +411,7 @@
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete, Search, Download, Refresh, User, ShoppingCart, Ticket, Money, Close } from '@element-plus/icons-vue'
import { getOrderList, getOrderDetail, createOrder, updateOrder, deleteOrder } from '@/api/admin/order'
import { getOrderList, getOrderDetail, createOrder, updateOrder, deleteOrder, retryOrderHook } from '@/api/admin/order'
import UserListSelector from '@/components/admin/UserListSelector.vue'
import ProductSelector from '@/components/admin/ProductSelector.vue'
import DiscountCodeSelector from '@/components/admin/DiscountCodeSelector.vue'
@@ -403,7 +425,8 @@ const queryParams = reactive({
key: '',
state: '',
user_id: '',
user_key: ''
user_key: '',
error: null
})
// 订单表单
@@ -544,6 +567,7 @@ const resetQuery = () => {
queryParams.state = ''
queryParams.user_id = ''
queryParams.user_key = ''
queryParams.error = null
queryParams.page = 1
fetchOrderList()
}
@@ -644,6 +668,32 @@ const handleEdit = (row) => {
}
}
// 重试订单流程
const handleRetryOrder = (row) => {
ElMessageBox.confirm(
`确认对订单「${row.name}」(ID: ${row.id}) 重试流程吗?`,
'重试订单流程',
{
confirmButtonText: '确认重试',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
try {
const res = await retryOrderHook({ order_id: row.id })
if (res.data.code === 200) {
ElMessage.success('重试流程已触发')
fetchOrderList()
} else {
ElMessage.error(res.data.message || '重试失败')
}
} catch (error) {
console.error('重试订单流程失败:', error)
ElMessage.error(error.response?.data?.message || '重试订单流程失败')
}
}).catch(() => {})
}
// 删除订单
const handleDelete = (row) => {
ElMessageBox.confirm(`确认删除订单 ${row.name} 吗?`, '警告', {
@@ -966,4 +1016,21 @@ onMounted(() => {
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
.unit-text { font-size: 13px; color: #606266; flex-shrink: 0; white-space: nowrap; }
.error-text {
color: #f56c6c;
font-size: 12px;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
cursor: pointer;
}
.text-muted {
color: #c0c4cc;
}
</style>
+400
View File
@@ -0,0 +1,400 @@
<template>
<div class="menu-manage-container">
<el-card class="main-container" shadow="never">
<div class="filter-section">
<div class="filter-content">
<el-form :inline="true" :model="queryParams" class="filter-form">
<el-form-item label="关键词">
<el-input v-model="queryParams.key" placeholder="菜单名称/路径" clearable style="width: 180px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="父级菜单">
<el-select v-model="queryParams.parent_id" placeholder="全部" clearable style="width: 160px">
<el-option v-for="m in parentMenuOptions" :key="m.id" :label="m.title" :value="m.id" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon>搜索
</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-radio-group v-model="viewMode" size="default" @change="handleViewModeChange">
<el-radio-button value="list">
<el-icon style="vertical-align: -2px;"><Grid /></el-icon> 列表视图
</el-radio-button>
<el-radio-button value="tree">
<el-icon style="vertical-align: -2px;"><Connection /></el-icon> 树状视图
</el-radio-button>
</el-radio-group>
<el-button v-if="viewMode === 'list'" type="primary" @click="handleAdd(null)">
<el-icon><Plus /></el-icon>新增顶级菜单
</el-button>
<el-button type="success" @click="handleRefresh">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</div>
</div>
<!-- 列表视图 -->
<div v-if="viewMode === 'list'" class="table-section">
<el-table
v-loading="loading"
:data="menuList"
style="width: 100%"
row-key="id"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="菜单名称" min-width="180" />
<el-table-column prop="path" label="路径" min-width="200">
<template #default="{ row }">
<el-tag size="small" type="info">{{ row.path || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="icon" label="图标" width="120">
<template #default="{ row }">
<div v-if="row.icon" style="display: flex; align-items: center; gap: 6px;">
<el-icon><component :is="row.icon" /></el-icon>
<span>{{ row.icon }}</span>
</div>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
<el-table-column prop="parentId" label="父级ID" width="80">
<template #default="{ row }">
{{ row.parentId ?? '-' }}
</template>
</el-table-column>
<el-table-column label="创建时间" width="170">
<template #default="{ row }">
{{ formatDate(row.CreatedAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" link @click="handleAdd(row)">添加子菜单</el-button>
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</div>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无菜单数据" :image-size="80" />
</template>
</el-table>
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
<!-- 树状视图 -->
<div v-if="viewMode === 'tree'" v-loading="myPermLoading" class="tree-section">
<el-tree
v-if="myPermTree.length > 0"
:data="myPermTree"
node-key="id"
default-expand-all
:expand-on-click-node="false"
>
<template #default="{ data }">
<div class="perm-tree-node">
<el-icon v-if="data.icon" style="margin-right: 6px; flex-shrink: 0;"><component :is="data.icon" /></el-icon>
<span class="perm-tree-title">{{ data.title }}</span>
<el-tag size="small" type="info" style="margin-left: 8px;">{{ data.path || '-' }}</el-tag>
<el-tag :type="data.enable ? 'success' : 'danger'" size="small" style="margin-left: 6px;">
{{ data.enable ? '启用' : '禁用' }}
</el-tag>
</div>
</template>
</el-tree>
<el-empty v-if="!myPermLoading && myPermTree.length === 0" description="暂无菜单权限数据" :image-size="80" />
</div>
</el-card>
<!-- 菜单表单对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增菜单' : '编辑菜单'"
width="550px"
append-to-body
>
<el-form ref="formRef" :model="menuForm" :rules="menuRules" label-width="100px">
<el-form-item label="菜单名称" prop="title">
<el-input v-model="menuForm.title" placeholder="请输入菜单名称" />
</el-form-item>
<el-form-item label="菜单路径" prop="path">
<MenuPathSelector v-model="menuForm.path" />
</el-form-item>
<el-form-item label="菜单图标" prop="icon">
<IconSelector v-model="menuForm.icon" />
</el-form-item>
<el-form-item label="父级菜单">
<el-select v-model="menuForm.parent_id" placeholder="无(顶级菜单)" clearable style="width: 100%">
<el-option label="无(顶级菜单)" :value="0" />
<el-option v-for="m in parentMenuOptions" :key="m.id" :label="m.title" :value="m.id" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Plus, Refresh, Grid, Connection } from '@element-plus/icons-vue'
import { getWebRoutsList, addWebRouts, updateWebRouts, deleteWebRouts, getMyWebRoutsPermission } from '@/api/admin/webRouts'
import { formatDate as formatDateTool } from '@/utils/tool'
import IconSelector from '@/components/admin/IconSelector.vue'
import MenuPathSelector from '@/components/admin/MenuPathSelector.vue'
const loading = ref(false)
const menuList = ref([])
const parentMenuOptions = ref([])
const total = ref(0)
const dialogVisible = ref(false)
const dialogType = ref('add')
const formRef = ref(null)
const viewMode = ref('list')
const queryParams = reactive({
page: 1,
count: 10,
key: '',
parent_id: null
})
const menuForm = reactive({
id: undefined,
title: '',
path: '',
icon: '',
parent_id: 0
})
const menuRules = {
title: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }],
path: [{ required: true, message: '请输入菜单路径', trigger: 'blur' }]
}
const formatDate = (dateStr) => formatDateTool(dateStr)
const flattenForParent = (list) => {
const result = []
for (const item of list) {
result.push(item)
if (item.children?.length) {
result.push(...flattenForParent(item.children))
}
}
return result
}
const fetchMenuList = async () => {
loading.value = true
try {
const params = {}
Object.keys(queryParams).forEach(key => {
if (queryParams[key] !== '' && queryParams[key] !== null && queryParams[key] !== undefined) {
params[key] = queryParams[key]
}
})
const res = await getWebRoutsList(params)
if (res.data.code === 200) {
menuList.value = res.data.data?.list || []
total.value = res.data.data?.all_count || 0
parentMenuOptions.value = flattenForParent(menuList.value)
}
} catch (error) {
console.error('获取菜单列表失败:', error)
ElMessage.error('获取菜单列表失败')
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.page = 1
fetchMenuList()
}
const resetQuery = () => {
queryParams.key = ''
queryParams.parent_id = null
queryParams.page = 1
fetchMenuList()
}
const handleSizeChange = (size) => {
queryParams.count = size
fetchMenuList()
}
const handleCurrentChange = (page) => {
queryParams.page = page
fetchMenuList()
}
const handleAdd = (parentRow) => {
dialogType.value = 'add'
dialogVisible.value = true
Object.assign(menuForm, {
id: undefined,
title: '',
path: '',
icon: '',
parent_id: parentRow?.id || 0
})
formRef.value?.resetFields()
}
const handleEdit = (row) => {
dialogType.value = 'edit'
dialogVisible.value = true
Object.assign(menuForm, {
id: row.id,
title: row.title,
path: row.path,
icon: row.icon,
parent_id: row.parentId || 0
})
}
const handleDelete = (row) => {
ElMessageBox.confirm(`确认删除菜单「${row.title}」吗?删除后其子菜单也将受到影响。`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteWebRouts({ id: row.id })
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchMenuList()
} else {
ElMessage.error(res.data.message || '删除失败')
}
} catch (error) {
ElMessage.error(error.response?.data?.message || '删除失败')
}
}).catch(() => {})
}
const submitForm = () => {
formRef.value?.validate(async (valid) => {
if (!valid) return
try {
const submitData = {
title: menuForm.title,
path: menuForm.path,
icon: menuForm.icon
}
if (menuForm.parent_id) {
submitData.parent_id = menuForm.parent_id
}
let res
if (dialogType.value === 'add') {
res = await addWebRouts(submitData)
} else {
submitData.id = menuForm.id
res = await updateWebRouts(submitData)
}
if (res.data.code === 200) {
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
dialogVisible.value = false
fetchMenuList()
} else {
ElMessage.error(res.data.message || '操作失败')
}
} catch (error) {
ElMessage.error(error.response?.data?.message || '操作失败')
}
})
}
const myPermLoading = ref(false)
const myPermTree = ref([])
const fetchMyPermission = async () => {
myPermLoading.value = true
try {
const res = await getMyWebRoutsPermission()
if (res.data.code === 200) {
myPermTree.value = res.data.data || []
} else {
ElMessage.error(res.data.message || '获取失败')
}
} catch (error) {
console.error('获取我的菜单权限失败:', error)
ElMessage.error('获取我的菜单权限失败')
} finally {
myPermLoading.value = false
}
}
const handleViewModeChange = (mode) => {
if (mode === 'list') {
fetchMenuList()
} else {
fetchMyPermission()
}
}
const handleRefresh = () => {
if (viewMode.value === 'list') {
fetchMenuList()
} else {
fetchMyPermission()
}
}
onMounted(() => {
fetchMenuList()
})
</script>
<style scoped>
.menu-manage-container { padding: 0; }
.main-container { border: 1px solid #e1e8ed; background: #ffffff; }
.filter-section { padding: 0; border-bottom: 1px solid #e1e8ed; background: #fafbfc; }
.filter-content { display: flex; justify-content: space-between; align-items: flex-start; padding: 16px 20px; gap: 20px; flex-wrap: wrap; }
.filter-form { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.filter-form :deep(.el-form-item) { margin-bottom: 0; margin-right: 8px; }
.filter-form :deep(.el-form-item__label) { font-size: 13px; }
.action-bar { display: flex; gap: 12px; flex-shrink: 0; }
.table-section { padding: 0; }
.action-buttons { display: flex; gap: 8px; align-items: center; }
.text-muted { color: #c0c4cc; }
.pagination { margin-top: 20px; padding: 16px 20px; border-top: 1px solid #e1e8ed; background: #fafbfc; justify-content: flex-end; }
.dialog-footer { display: flex; justify-content: flex-end; gap: 12px; }
:deep(.el-card__body) { padding: 0; }
:deep(.el-table th) { background: #f8f9fa !important; border-bottom: 2px solid #e1e8ed; color: #2c3e50; font-weight: 600; font-size: 13px; }
:deep(.el-table td) { border-bottom: 1px solid #f0f2f5; color: #34495e; }
:deep(.el-table tr:hover > td) { background-color: #f8f9fa !important; }
.tree-section { padding: 16px 20px; min-height: 300px; }
.perm-tree-node { display: flex; align-items: center; padding: 2px 0; width: 100%; }
.perm-tree-title { font-size: 13px; font-weight: 500; }
.tree-section :deep(.el-tree-node__content) { height: 38px; }
.tree-section :deep(.el-tree-node__content:hover) { background-color: #f5f7fa; }
.action-bar { display: flex; gap: 12px; flex-shrink: 0; align-items: center; }
</style>
+610
View File
@@ -0,0 +1,610 @@
<template>
<div class="menu-permission-container">
<el-card class="main-container" shadow="never">
<div class="filter-section">
<div class="filter-content">
<el-form :inline="true" :model="queryParams" class="filter-form">
<el-form-item label="类型">
<el-select v-model="queryParams.owner_type" placeholder="请选择类型" clearable style="width: 130px" @change="handleOwnerTypeChange">
<el-option label="用户" value="user" />
<el-option label="管理员组" value="group" />
</el-select>
</el-form-item>
<el-form-item label="用户" v-if="queryParams.owner_type === 'user'">
<div class="selector-inline">
<el-tag v-if="queryParams.user_id" type="primary" closable @close="clearQueryUser" style="margin-right: 8px;">
{{ queryUserName || `用户 #${queryParams.user_id}` }}
</el-tag>
<el-button type="primary" plain @click="openUserSelector('query')" size="default">
<el-icon><User /></el-icon>
{{ queryParams.user_id ? '重新选择' : '选择用户' }}
</el-button>
</div>
</el-form-item>
<el-form-item label="管理员组" v-if="queryParams.owner_type === 'group'">
<div class="selector-inline">
<el-tag v-if="queryParams.admin_group_id" type="success" closable @close="clearQueryGroup" style="margin-right: 8px;">
{{ queryGroupName || ` #${queryParams.admin_group_id}` }}
</el-tag>
<el-button type="success" plain @click="openGroupSelector('query')" size="default">
<el-icon><User /></el-icon>
{{ queryParams.admin_group_id ? '重新选择' : '选择管理员组' }}
</el-button>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon>查询
</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>分配权限
</el-button>
<el-button type="success" @click="handleRefresh">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</div>
</div>
<div class="table-section">
<el-table
v-loading="loading"
:data="permissionList"
style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column label="所属类型" width="120">
<template #default="{ row }">
<el-tag :type="getOwnerType(row) === 'user' ? 'primary' : 'success'" size="small">
{{ getOwnerType(row) === 'user' ? '用户' : '管理员组' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="所属对象" width="150">
<template #default="{ row }">
<span v-if="row.userId">用户 #{{ row.userId }}</span>
<span v-else-if="row.adminGroupId">管理员组 #{{ row.adminGroupId }}</span>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
<el-table-column label="菜单" min-width="200">
<template #default="{ row }">
<div v-if="row.webRouts">
<span style="font-weight: 500;">{{ row.webRouts.title }}</span>
<el-tag size="small" type="info" style="margin-left: 8px;">{{ row.webRouts.path }}</el-tag>
</div>
<span v-else class="text-muted">菜单ID: {{ row.webRoutsId }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.enable ? 'success' : 'danger'" size="small">
{{ row.enable ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" width="170">
<template #default="{ row }">
{{ formatDate(row.CreatedAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" link @click="handleToggleEnable(row)">
{{ row.enable ? '禁用' : '启用' }}
</el-button>
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</div>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无权限数据" :image-size="80" />
</template>
</el-table>
</div>
</el-card>
<!-- 用户选择弹窗 -->
<UserListSelector
v-model="userSelectorVisible"
:current-user-id="selectorTarget === 'query' ? queryParams.user_id : permForm.user_id"
@confirm="handleUserConfirm"
/>
<!-- 管理员组选择弹窗 -->
<UserGroupSelector
v-model="groupSelectorVisible"
:current-group-id="selectorTarget === 'query' ? queryParams.admin_group_id : permForm.admin_group_id"
admin-group
@confirm="handleGroupConfirm"
/>
<!-- 菜单选择弹窗 -->
<el-dialog v-model="menuSelectorVisible" title="选择菜单" width="700px" append-to-body @open="openMenuSelector">
<div style="display: flex; gap: 8px; margin-bottom: 12px;">
<el-input v-model="menuSearchKey" placeholder="搜索菜单名称或路径" clearable style="flex: 1;" @keyup.enter="fetchMenuSelectorList">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="fetchMenuSelectorList">搜索</el-button>
<el-button type="success" @click="fetchMenuSelectorList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
<el-table
v-loading="menuSelectorLoading"
:data="menuSelectorFlatList"
highlight-current-row
@current-change="handleMenuCurrentChange"
:height="400"
style="width: 100%"
row-key="id"
>
<el-table-column prop="id" label="ID" width="70" />
<el-table-column prop="title" label="菜单名称" min-width="160">
<template #default="{ row }">
<span :style="{ paddingLeft: (row._level || 0) * 20 + 'px' }">
<span v-if="row._level" style="color: #c0c4cc; margin-right: 4px;"></span>
{{ row.title }}
</span>
</template>
</el-table-column>
<el-table-column prop="path" label="路径" min-width="180">
<template #default="{ row }">
<el-tag size="small" type="info">{{ row.path || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="icon" label="图标" width="80">
<template #default="{ row }">
<el-icon v-if="row.icon"><component :is="row.icon" /></el-icon>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="menuSelectorVisible = false">取消</el-button>
<el-button type="primary" :disabled="!menuSelectorTemp" @click="confirmMenuSelect">确定选择</el-button>
</template>
</el-dialog>
<!-- 分配/编辑权限对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '分配菜单权限' : '编辑菜单权限'"
width="600px"
append-to-body
>
<el-form ref="formRef" :model="permForm" :rules="formRules" label-width="120px">
<el-form-item label="所属类型" prop="owner_type">
<el-select v-model="permForm.owner_type" placeholder="请选择" style="width: 100%" :disabled="dialogType === 'edit'" @change="handleFormOwnerTypeChange">
<el-option label="用户" value="user" />
<el-option label="管理员组" value="group" />
</el-select>
</el-form-item>
<el-form-item label="用户" prop="user_id" v-if="permForm.owner_type === 'user'">
<div class="selector-inline" style="width: 100%;">
<el-tag v-if="permForm.user_id" type="primary" closable @close="permForm.user_id = null" style="margin-right: 8px;">
{{ formUserName || `用户 #${permForm.user_id}` }}
</el-tag>
<el-button type="primary" plain @click="openUserSelector('form')">
<el-icon><User /></el-icon>
{{ permForm.user_id ? '重新选择' : '选择用户' }}
</el-button>
</div>
</el-form-item>
<el-form-item label="管理员组" prop="admin_group_id" v-if="permForm.owner_type === 'group'">
<div class="selector-inline" style="width: 100%;">
<el-tag v-if="permForm.admin_group_id" type="success" closable @close="permForm.admin_group_id = null" style="margin-right: 8px;">
{{ formGroupName || ` #${permForm.admin_group_id}` }}
</el-tag>
<el-button type="success" plain @click="openGroupSelector('form')">
<el-icon><User /></el-icon>
{{ permForm.admin_group_id ? '重新选择' : '选择管理员组' }}
</el-button>
</div>
</el-form-item>
<el-form-item label="菜单" prop="web_routs_id">
<div class="selector-inline" style="width: 100%;">
<el-tag v-if="permForm.web_routs_id" closable @close="clearFormMenu" style="margin-right: 8px;">
{{ formMenuName || `菜单 #${permForm.web_routs_id}` }}
</el-tag>
<el-button plain @click="menuSelectorVisible = true">
<el-icon><Menu /></el-icon>
{{ permForm.web_routs_id ? '重新选择' : '选择菜单' }}
</el-button>
</div>
</el-form-item>
<el-form-item label="是否启用">
<el-switch v-model="permForm.enable" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Plus, Refresh, User, Menu } from '@element-plus/icons-vue'
import UserListSelector from '@/components/admin/UserListSelector.vue'
import UserGroupSelector from '@/components/admin/UserGroupSelector.vue'
import {
getWebRoutsList,
getWebRoutsPermissionList,
addWebRoutsPermission,
updateWebRoutsPermission,
deleteWebRoutsPermission
} from '@/api/admin/webRouts'
import { formatDate as formatDateTool } from '@/utils/tool'
const loading = ref(false)
const permissionList = ref([])
const dialogVisible = ref(false)
const dialogType = ref('add')
const formRef = ref(null)
const userSelectorVisible = ref(false)
const groupSelectorVisible = ref(false)
const menuSelectorVisible = ref(false)
const menuSelectorLoading = ref(false)
const menuSelectorTemp = ref(null)
const menuSearchKey = ref('')
const selectorTarget = ref('query')
const queryUserName = ref('')
const queryGroupName = ref('')
const formUserName = ref('')
const formGroupName = ref('')
const formMenuName = ref('')
const queryParams = reactive({
owner_type: 'group',
user_id: null,
admin_group_id: null
})
const permForm = reactive({
id: undefined,
web_routs_id: null,
enable: true,
owner_type: 'group',
admin_group_id: null,
user_id: null
})
const formRules = {
owner_type: [{ required: true, message: '请选择所属类型', trigger: 'change' }],
web_routs_id: [{ required: true, message: '请选择菜单', trigger: 'change' }],
user_id: [{ required: true, message: '请选择用户', trigger: 'change' }],
admin_group_id: [{ required: true, message: '请选择管理员组', trigger: 'change' }]
}
const formatDate = (dateStr) => formatDateTool(dateStr)
const getOwnerType = (row) => {
if (row.userId) return 'user'
if (row.adminGroupId) return 'group'
return 'unknown'
}
const fetchPermissionList = async () => {
loading.value = true
try {
const params = {}
if (queryParams.owner_type) params.owner_type = queryParams.owner_type
if (queryParams.owner_type === 'user' && queryParams.user_id) params.user_id = queryParams.user_id
if (queryParams.owner_type === 'group' && queryParams.admin_group_id) params.admin_group_id = queryParams.admin_group_id
const res = await getWebRoutsPermissionList(params)
if (res.data.code === 200) {
permissionList.value = res.data.data || []
}
} catch (error) {
console.error('获取权限列表失败:', error)
ElMessage.error('获取权限列表失败')
} finally {
loading.value = false
}
}
const flattenMenuTree = (list, level = 0) => {
const result = []
for (const item of list) {
result.push({ ...item, _level: level, children: undefined })
if (item.children?.length) {
result.push(...flattenMenuTree(item.children, level + 1))
}
}
return result
}
const menuSelectorFlatList = ref([])
const openMenuSelector = () => {
menuSelectorTemp.value = null
menuSearchKey.value = ''
fetchMenuSelectorList()
}
const fetchMenuSelectorList = async () => {
menuSelectorLoading.value = true
try {
const params = { page: 1, count: 10 }
if (menuSearchKey.value) params.key = menuSearchKey.value
const res = await getWebRoutsList(params)
if (res.data.code === 200) {
const treeList = res.data.data?.list || []
menuSelectorFlatList.value = flattenMenuTree(treeList)
}
} catch (error) {
console.error('获取菜单列表失败:', error)
} finally {
menuSelectorLoading.value = false
}
}
const handleMenuCurrentChange = (row) => {
menuSelectorTemp.value = row
}
const confirmMenuSelect = () => {
if (!menuSelectorTemp.value) return
permForm.web_routs_id = menuSelectorTemp.value.id
formMenuName.value = `${menuSelectorTemp.value.title} (${menuSelectorTemp.value.path})`
menuSelectorVisible.value = false
menuSelectorTemp.value = null
menuSearchKey.value = ''
}
const clearFormMenu = () => {
permForm.web_routs_id = null
formMenuName.value = ''
}
const handleOwnerTypeChange = () => {
queryParams.user_id = null
queryParams.admin_group_id = null
queryUserName.value = ''
queryGroupName.value = ''
}
const handleFormOwnerTypeChange = () => {
permForm.user_id = null
permForm.admin_group_id = null
formUserName.value = ''
formGroupName.value = ''
}
const canQuery = () => {
if (queryParams.owner_type === 'user' && !queryParams.user_id) {
ElMessage.warning('请先选择用户')
return false
}
if (queryParams.owner_type === 'group' && !queryParams.admin_group_id) {
ElMessage.warning('请先选择管理员组')
return false
}
if (!queryParams.owner_type) {
ElMessage.warning('请先选择类型')
return false
}
return true
}
const handleQuery = () => {
if (!canQuery()) return
fetchPermissionList()
}
const handleRefresh = () => {
if (!canQuery()) return
fetchPermissionList()
}
const resetQuery = () => {
queryParams.owner_type = 'group'
queryParams.user_id = null
queryParams.admin_group_id = null
queryUserName.value = ''
queryGroupName.value = ''
permissionList.value = []
}
const clearQueryUser = () => {
queryParams.user_id = null
queryUserName.value = ''
}
const clearQueryGroup = () => {
queryParams.admin_group_id = null
queryGroupName.value = ''
}
const openUserSelector = (target) => {
selectorTarget.value = target
userSelectorVisible.value = true
}
const openGroupSelector = (target) => {
selectorTarget.value = target
groupSelectorVisible.value = true
}
const handleUserConfirm = (user) => {
const id = user.user_id || user.UserId || user.userId
const name = user.user_name || user.UserName || user.userName || `用户 #${id}`
if (selectorTarget.value === 'query') {
queryParams.user_id = id
queryUserName.value = name
} else {
permForm.user_id = id
formUserName.value = name
}
}
const handleGroupConfirm = (group) => {
const name = group.name || group.groupName || `组 #${group.id}`
const id = group.id
if (selectorTarget.value === 'query') {
queryParams.admin_group_id = id
queryGroupName.value = name
} else {
permForm.admin_group_id = id
formGroupName.value = name
}
}
const handleAdd = () => {
dialogType.value = 'add'
dialogVisible.value = true
Object.assign(permForm, {
id: undefined,
web_routs_id: null,
enable: true,
owner_type: 'group',
admin_group_id: null,
user_id: null
})
formUserName.value = ''
formGroupName.value = ''
formMenuName.value = ''
formRef.value?.resetFields()
}
const handleEdit = (row) => {
dialogType.value = 'edit'
dialogVisible.value = true
const ownerType = row.userId ? 'user' : 'group'
Object.assign(permForm, {
id: row.id,
web_routs_id: row.webRoutsId,
enable: row.enable,
owner_type: ownerType,
admin_group_id: row.adminGroupId || null,
user_id: row.userId || null
})
formUserName.value = row.userId ? `用户 #${row.userId}` : ''
formGroupName.value = row.adminGroupId ? `组 #${row.adminGroupId}` : ''
if (row.webRouts) {
formMenuName.value = `${row.webRouts.title} (${row.webRouts.path})`
} else {
formMenuName.value = row.webRoutsId ? `菜单 #${row.webRoutsId}` : ''
}
}
const handleToggleEnable = async (row) => {
const newEnable = !row.enable
const action = newEnable ? '启用' : '禁用'
try {
await ElMessageBox.confirm(`确认${action}该菜单权限吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const res = await updateWebRoutsPermission({
id: row.id,
enable: newEnable
})
if (res.data.code === 200) {
ElMessage.success(`${action}成功`)
fetchPermissionList()
} else {
ElMessage.error(res.data.message || `${action}失败`)
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error(error.response?.data?.message || `${action}失败`)
}
}
}
const handleDelete = (row) => {
ElMessageBox.confirm('确认删除该菜单权限吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteWebRoutsPermission({ id: row.id })
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchPermissionList()
} else {
ElMessage.error(res.data.message || '删除失败')
}
} catch (error) {
ElMessage.error(error.response?.data?.message || '删除失败')
}
}).catch(() => {})
}
const submitForm = () => {
formRef.value?.validate(async (valid) => {
if (!valid) return
try {
const submitData = {
web_routs_id: permForm.web_routs_id,
enable: permForm.enable,
owner_type: permForm.owner_type
}
if (permForm.owner_type === 'user') {
submitData.user_id = permForm.user_id
} else {
submitData.admin_group_id = permForm.admin_group_id
}
let res
if (dialogType.value === 'add') {
res = await addWebRoutsPermission(submitData)
} else {
submitData.id = permForm.id
res = await updateWebRoutsPermission(submitData)
}
if (res.data.code === 200) {
ElMessage.success(dialogType.value === 'add' ? '分配成功' : '修改成功')
dialogVisible.value = false
fetchPermissionList()
} else {
ElMessage.error(res.data.message || '操作失败')
}
} catch (error) {
ElMessage.error(error.response?.data?.message || '操作失败')
}
})
}
</script>
<style scoped>
.menu-permission-container { padding: 0; }
.main-container { border: 1px solid #e1e8ed; background: #ffffff; }
.filter-section { padding: 0; border-bottom: 1px solid #e1e8ed; background: #fafbfc; }
.filter-content { display: flex; justify-content: space-between; align-items: flex-start; padding: 16px 20px; gap: 20px; flex-wrap: wrap; }
.filter-form { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.filter-form :deep(.el-form-item) { margin-bottom: 0; margin-right: 8px; }
.filter-form :deep(.el-form-item__label) { font-size: 13px; }
.action-bar { display: flex; gap: 12px; flex-shrink: 0; }
.selector-inline { display: flex; align-items: center; }
.table-section { padding: 0; }
.action-buttons { display: flex; gap: 8px; align-items: center; }
.text-muted { color: #c0c4cc; }
.dialog-footer { display: flex; justify-content: flex-end; gap: 12px; }
:deep(.el-card__body) { padding: 0; }
:deep(.el-table th) { background: #f8f9fa !important; border-bottom: 2px solid #e1e8ed; color: #2c3e50; font-weight: 600; font-size: 13px; }
:deep(.el-table td) { border-bottom: 1px solid #f0f2f5; color: #34495e; }
:deep(.el-table tr:hover > td) { background-color: #f8f9fa !important; }
</style>
+573 -216
View File
@@ -7,18 +7,307 @@
},
"tags": [
{
"name": "VNC指令管理"
"name": "后台菜单权限管理"
},
{
"name": "后台菜单管理"
}
],
"paths": {
"/api/v1/admin/server/vnc_command/group/list": {
"/api/v1/admin/server/web_routs/permission/list": {
"get": {
"summary": "获取指令分组列表(含指令项)",
"summary": "获取后台菜单权限列表",
"deprecated": false,
"description": "",
"operationId": "VncCommandGroupList",
"tags": [
"VNC指令管理"
"后台菜单权限管理"
],
"parameters": [
{
"name": "owner_type",
"in": "query",
"description": "所属类型",
"required": false,
"schema": {
"type": "string",
"enum": [
"user",
"group"
],
"default": "group"
}
},
{
"name": "user_id",
"in": "query",
"description": "用户IDowner_type为user时必填)",
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "admin_group_id",
"in": "query",
"description": "管理员组IDowner_type为group时必填)",
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer {{token}}",
"schema": {
"type": "string",
"default": "Bearer {{token}}"
}
}
],
"responses": {
"200": {
"description": "成功",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/AdminWebRoutsPermission"
}
}
}
},
"headers": {}
}
},
"security": []
}
},
"/api/v1/admin/server/web_routs/permission/add": {
"post": {
"summary": "新增后台菜单权限",
"deprecated": false,
"description": "",
"tags": [
"后台菜单权限管理"
],
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer {{token}}",
"schema": {
"type": "string",
"default": "Bearer {{token}}"
}
}
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"web_routs_id": {
"type": "integer",
"description": "菜单ID",
"example": 0
},
"enable": {
"type": "boolean",
"default": true,
"description": "是否启用",
"example": "true"
},
"owner_type": {
"type": "string",
"enum": [
"user",
"group"
],
"description": "所属类型",
"example": ""
},
"admin_group_id": {
"type": "integer",
"description": "管理员组IDowner_type为group时使用)",
"example": 0
},
"user_id": {
"type": "integer",
"description": "用户IDowner_type为user时使用)",
"example": 0
}
},
"required": [
"web_routs_id",
"owner_type"
]
}
}
},
"required": true
},
"responses": {
"200": {
"description": "成功",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminWebRoutsPermission"
}
}
},
"headers": {}
}
},
"security": []
}
},
"/api/v1/admin/server/web_routs/permission/update": {
"post": {
"summary": "修改后台菜单权限",
"deprecated": false,
"description": "",
"tags": [
"后台菜单权限管理"
],
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer {{token}}",
"schema": {
"type": "string",
"default": "Bearer {{token}}"
}
}
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "权限记录ID",
"example": 0
},
"web_routs_id": {
"type": "integer",
"description": "菜单ID",
"example": 0
},
"enable": {
"type": "boolean",
"description": "是否启用",
"example": ""
},
"admin_group_id": {
"type": "integer",
"description": "管理员组ID",
"example": 0
},
"user_id": {
"type": "integer",
"description": "用户ID",
"example": 0
},
"owner_type": {
"type": "string",
"enum": [
"user",
"group"
],
"description": "所属类型",
"example": ""
}
},
"required": [
"id"
]
}
}
},
"required": true
},
"responses": {
"200": {
"description": "成功",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminWebRoutsPermission"
}
}
},
"headers": {}
}
},
"security": []
}
},
"/api/v1/admin/server/web_routs/permission/delete": {
"delete": {
"summary": "删除后台菜单权限",
"deprecated": false,
"description": "",
"tags": [
"后台菜单权限管理"
],
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer {{token}}",
"schema": {
"type": "string",
"default": "Bearer {{token}}"
}
}
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "权限记录ID",
"example": 0
}
},
"required": [
"id"
]
}
}
},
"required": true
},
"responses": {
"200": {
"description": "成功",
"headers": {}
}
},
"security": []
}
},
"/api/v1/admin/server/web_routs/my": {
"get": {
"summary": "获取当前用户的后台菜单权限树",
"deprecated": false,
"description": "自动读取当前用户的 user_id 与 AdminGroupId,合并用户级与管理员组级菜单权限,返回完整的菜单树形结构。每个菜单节点包含 enable 状态,用户级权限优先于管理员组级权限。",
"tags": [
"后台菜单权限管理"
],
"parameters": [
{
@@ -34,12 +323,118 @@
],
"responses": {
"200": {
"description": "查询成功",
"description": "成功",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/WebRoutsWithPermission"
}
},
"example": {
"id": 1,
"path": "/dashboard",
"title": "仪表盘",
"icon": "dashboard",
"parentId": null,
"enable": true,
"children": [
{
"id": 2,
"path": "/dashboard/overview",
"title": "概览",
"icon": "overview",
"parentId": 1,
"enable": true,
"children": []
}
]
}
}
},
"headers": {}
}
},
"security": []
}
},
"/api/v1/admin/server/web_routs/list": {
"get": {
"summary": "获取后台菜单列表",
"deprecated": false,
"description": "",
"tags": [
"后台菜单管理"
],
"parameters": [
{
"name": "page",
"in": "query",
"description": "页码",
"required": false,
"schema": {
"type": "integer",
"default": 1
}
},
{
"name": "count",
"in": "query",
"description": "每页数量",
"required": false,
"schema": {
"type": "integer",
"default": 10
}
},
{
"name": "key",
"in": "query",
"description": "搜索关键字(匹配标题或路径)",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "parent_id",
"in": "query",
"description": "父级菜单ID,不传则获取顶级菜单",
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer {{token}}",
"schema": {
"type": "string",
"default": "Bearer {{token}}"
}
}
],
"responses": {
"200": {
"description": "成功",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
"properties": {
"list": {
"type": "array",
"items": {
"$ref": "#/components/schemas/AdminWebRouts"
}
},
"all_count": {
"type": "integer"
}
}
}
}
},
@@ -49,14 +444,13 @@
"security": []
}
},
"/api/v1/admin/server/vnc_command/group/create": {
"/api/v1/admin/server/web_routs/add": {
"post": {
"summary": "创建指令分组",
"summary": "新增后台菜单",
"deprecated": false,
"description": "",
"operationId": "VncCommandGroupCreate",
"tags": [
"VNC指令管理"
"后台菜单管理"
],
"parameters": [
{
@@ -76,53 +470,60 @@
"schema": {
"type": "object",
"properties": {
"name": {
"path": {
"type": "string",
"description": "分组名称",
"description": "菜单路径",
"example": ""
},
"sort": {
"title": {
"type": "string",
"description": "菜单名称",
"example": ""
},
"icon": {
"type": "string",
"description": "菜单图标",
"example": ""
},
"parent_id": {
"type": "integer",
"description": "排序",
"description": "父级菜单ID",
"example": 0
},
"default_icon": {
"description": "分组文本图标(如 📚),当无文件图标时使用",
"example": "",
"type": "string"
},
"icon_file_id": {
"description": "分组图标文件ID",
"example": "",
"type": "string"
}
},
"required": [
"name"
"path",
"title",
"icon"
]
},
"examples": {}
}
}
},
"required": true
},
"responses": {
"200": {
"description": "创建成功",
"description": "成功",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminWebRouts"
}
}
},
"headers": {}
}
},
"security": []
}
},
"/api/v1/admin/server/vnc_command/group/update": {
"/api/v1/admin/server/web_routs/update": {
"post": {
"summary": "修改指令分组",
"summary": "修改后台菜单",
"deprecated": false,
"description": "",
"operationId": "VncCommandGroupUpdate",
"tags": [
"VNC指令管理"
"后台菜单管理"
],
"parameters": [
{
@@ -144,165 +545,61 @@
"properties": {
"id": {
"type": "integer",
"description": "分组ID",
"description": "菜单ID",
"example": 0
},
"name": {
"path": {
"type": "string",
"description": "菜单路径",
"example": ""
},
"sort": {
"type": "integer",
"example": 0
"title": {
"type": "string",
"description": "菜单名称",
"example": ""
},
"default_icon": {
"description": "分组文本图标(如 📚),当无文件图标时使用",
"example": "",
"type": "string"
"icon": {
"type": "string",
"description": "菜单图标",
"example": ""
},
"icon_file_id": {
"parent_id": {
"type": "integer",
"description": "分组图标文件ID",
"description": "父级菜单ID",
"example": 0
}
},
"required": [
"id"
]
},
"examples": {}
}
}
},
"required": true
},
"responses": {
"200": {
"description": "修改成功",
"headers": {}
}
},
"security": []
}
},
"/api/v1/admin/server/vnc_command/group/delete": {
"delete": {
"summary": "删除指令分组(级联删除指令项)",
"deprecated": false,
"description": "",
"operationId": "VncCommandGroupDelete",
"tags": [
"VNC指令管理"
],
"parameters": [
{
"name": "id",
"in": "query",
"description": "",
"required": true,
"schema": {
"type": "integer"
}
},
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer {{token}}",
"schema": {
"type": "string",
"default": "Bearer {{token}}"
}
}
],
"responses": {
"200": {
"description": "删除成功",
"headers": {}
}
},
"security": []
}
},
"/api/v1/admin/server/vnc_command/item/create": {
"post": {
"summary": "创建指令项",
"deprecated": false,
"description": "",
"operationId": "VncCommandItemCreate",
"tags": [
"VNC指令管理"
],
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer {{token}}",
"schema": {
"type": "string",
"default": "Bearer {{token}}"
}
}
],
"requestBody": {
"description": "成功",
"content": {
"multipart/form-data": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"group_id": {
"type": "integer",
"description": "所属分组ID",
"example": 0
},
"label": {
"type": "string",
"description": "指令名称",
"example": ""
},
"cmd": {
"type": "string",
"description": "指令内容(支持 %var% 变量占位符)",
"example": ""
},
"vars": {
"type": "string",
"description": "变量列表 JSON,格式 [{\"k\":\"path\",\"p\":\"目录路径\"}]",
"example": ""
},
"sort": {
"type": "integer",
"description": "排序",
"example": 0
}
},
"required": [
"group_id",
"label",
"cmd"
]
"$ref": "#/components/schemas/AdminWebRouts"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "创建成功",
"headers": {}
}
},
"security": []
}
},
"/api/v1/admin/server/vnc_command/item/update": {
"post": {
"summary": "修改指令项",
"/api/v1/admin/server/web_routs/delete": {
"delete": {
"summary": "删除后台菜单",
"deprecated": false,
"description": "",
"operationId": "VncCommandItemUpdate",
"tags": [
"VNC指令管理"
"后台菜单管理"
],
"parameters": [
{
@@ -324,23 +621,7 @@
"properties": {
"id": {
"type": "integer",
"description": "指令项ID",
"example": 0
},
"label": {
"type": "string",
"example": ""
},
"cmd": {
"type": "string",
"example": ""
},
"vars": {
"type": "string",
"example": ""
},
"sort": {
"type": "integer",
"description": "菜单ID",
"example": 0
}
},
@@ -354,46 +635,7 @@
},
"responses": {
"200": {
"description": "修改成功",
"headers": {}
}
},
"security": []
}
},
"/api/v1/admin/server/vnc_command/item/delete": {
"delete": {
"summary": "删除指令项",
"deprecated": false,
"description": "",
"operationId": "VncCommandItemDelete",
"tags": [
"VNC指令管理"
],
"parameters": [
{
"name": "id",
"in": "query",
"description": "",
"required": true,
"schema": {
"type": "integer"
}
},
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer {{token}}",
"schema": {
"type": "string",
"default": "Bearer {{token}}"
}
}
],
"responses": {
"200": {
"description": "删除成功",
"description": "成功",
"headers": {}
}
},
@@ -402,7 +644,122 @@
}
},
"components": {
"schemas": {},
"schemas": {
"AdminWebRouts": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"path": {
"type": "string",
"description": "菜单路径"
},
"title": {
"type": "string",
"description": "菜单名称"
},
"icon": {
"type": "string",
"description": "菜单图标"
},
"parentId": {
"type": "integer",
"description": "父级菜单ID",
"nullable": true
},
"children": {
"type": "array",
"items": {
"$ref": "#/components/schemas/AdminWebRouts"
},
"description": "子菜单列表"
},
"CreatedAt": {
"type": "string",
"format": "date-time"
},
"UpdatedAt": {
"type": "string",
"format": "date-time"
}
}
},
"WebRoutsWithPermission": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "菜单ID"
},
"path": {
"type": "string",
"description": "菜单路径"
},
"title": {
"type": "string",
"description": "菜单名称"
},
"icon": {
"type": "string",
"description": "菜单图标"
},
"parentId": {
"type": "integer",
"description": "父级菜单ID",
"nullable": true
},
"enable": {
"type": "boolean",
"description": "是否启用(合并用户级与管理员组级权限后的结果,用户级优先)"
},
"children": {
"type": "array",
"items": {
"$ref": "#/components/schemas/WebRoutsWithPermission"
},
"description": "子菜单列表"
}
}
},
"AdminWebRoutsPermission": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"webRoutsId": {
"type": "integer",
"description": "菜单ID"
},
"webRouts": {
"$ref": "#/components/schemas/AdminWebRouts"
},
"enable": {
"type": "boolean",
"description": "是否启用"
},
"adminGroupId": {
"type": "integer",
"description": "管理员组ID",
"nullable": true
},
"userId": {
"type": "integer",
"description": "用户ID",
"nullable": true
},
"CreatedAt": {
"type": "string",
"format": "date-time"
},
"UpdatedAt": {
"type": "string",
"format": "date-time"
}
}
}
},
"responses": {},
"securitySchemes": {}
},