feat(company): 新增权限管理页面并重构菜单编辑器公司选择
- 新增 company/permission 权限管理页 - 菜单编辑器公司选择改用 EntityPicker - CrudTable 卡片查看按钮图标化,公司卡片补充父级/租户字段
This commit is contained in:
1 parent
fb03d60d80
commit
47506bd0b9
4 files changed
+89
-104
No files matched your search
@@ -689,7 +689,9 @@ function isCardSelected(row: Record<string, unknown>): boolean {
|
||||
{{ node.children.length }} 子级
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-button link type="primary" @click.stop="handleView(node.row as T)">查看</el-button>
|
||||
<el-button size="small" type="primary" @click.stop="handleView(node.row as T)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- 卡片主体:字段键值列表 -->
|
||||
<div class="crud-card-body">
|
||||
@@ -763,7 +765,9 @@ function isCardSelected(row: Record<string, unknown>): boolean {
|
||||
</div>
|
||||
<span class="crud-card-title">{{ (row as Record<string, unknown>)[cardTitleField] || '-' }}</span>
|
||||
</div>
|
||||
<el-button link type="primary" @click.stop="handleView(row as T)">查看</el-button>
|
||||
<el-button size="small" type="primary" @click.stop="handleView(row as T)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- 卡片主体:字段键值列表 -->
|
||||
<div class="crud-card-body">
|
||||
|
||||
@@ -104,7 +104,7 @@ async function handleIconChange(event: Event): Promise<void> {
|
||||
card-icon-field="CompanyIcon"
|
||||
card-tree-parent-field="ParentCompanyUUID"
|
||||
:default-page-size="1000"
|
||||
:card-fields="['CompanyShort', 'CompanyStatus', 'CreateTime']"
|
||||
:card-fields="['CompanyShort', 'CompanyStatus', 'ParentCompanyUUID', 'TenantUUID', 'CreateTime']"
|
||||
:card-value-maps="{
|
||||
CompanyStatus: { 0: '禁用', 1: '启用' },
|
||||
}"
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 权限管理(Permission)
|
||||
*
|
||||
* 修复:#/Company/permission 曾 404(迁移遗漏页面)。
|
||||
* 权限表(Permission)为全局共享配置(后端 COMPANY_SCOPE_EXEMPT_TABLES 豁免公司隔离),
|
||||
* 列表页直接走 CrudTable 卡片模式:
|
||||
* - 卡片标题取 PermName(权限名称)
|
||||
* - 展示 PermCode(权限键)/ PermCategory(分类)/ PermStatus(状态)/ PermDesc(描述)/ CreateTime
|
||||
*
|
||||
* 权限无独立详情页,"查看"不启用(保留编辑/删除走通用弹窗)。
|
||||
*/
|
||||
import CrudTable from '@/components/CrudTable.vue'
|
||||
import AdminBadge from '@/components/AdminBadge.vue'
|
||||
import { setPageTitle } from '@/utils/serverScope'
|
||||
|
||||
setPageTitle('权限管理')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="permission-page">
|
||||
<!-- 管理员特权横幅(仅特权模式显示) -->
|
||||
<AdminBadge />
|
||||
<CrudTable
|
||||
table="Permission"
|
||||
title="权限管理"
|
||||
pk-field="PermUUID"
|
||||
searchable
|
||||
pageable
|
||||
stretch
|
||||
selectable
|
||||
card-mode
|
||||
card-title-field="PermName"
|
||||
:card-fields="['PermCode', 'PermCategory', 'PermStatus', 'PermDesc', 'CreateTime']"
|
||||
:card-value-maps="{
|
||||
PermStatus: { 0: '禁用', 1: '启用' },
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.permission-page {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
+32
-101
@@ -31,12 +31,14 @@
|
||||
* 注意:api/menu.ts 的 saveMenu/deleteMenu 参数与原生后端字段不一致(Id vs MenuUUID、JSON vs 表单编码),
|
||||
* 故此处直接使用 request 底层 post 并显式设置表单编码,以保持与原生后端契约一致。
|
||||
*/
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/api/request'
|
||||
import { setPageTitle } from '@/utils/serverScope'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import AdminBadge from '@/components/AdminBadge.vue'
|
||||
import EntityPicker from '@/components/EntityPicker.vue'
|
||||
import { entityPickerPresets } from '@/api/entityPickerPresets'
|
||||
|
||||
setPageTitle('菜单编辑器')
|
||||
|
||||
@@ -64,13 +66,6 @@ interface MenuNode {
|
||||
children?: MenuNode[]
|
||||
}
|
||||
|
||||
/** 公司搜索结果项 */
|
||||
interface CompanyItem {
|
||||
CompanyUUID: string
|
||||
CompanyName: string
|
||||
CompanyShort?: string
|
||||
}
|
||||
|
||||
/** 标签页(每公司一个) */
|
||||
interface MenuTab {
|
||||
companyUUID: string
|
||||
@@ -85,11 +80,9 @@ const tabs = ref<MenuTab[]>([])
|
||||
const activeTabIndex = ref(-1)
|
||||
const activeTab = computed(() => (activeTabIndex.value >= 0 ? tabs.value[activeTabIndex.value] : null))
|
||||
|
||||
/** 公司搜索 */
|
||||
const companySearchKeyword = ref('')
|
||||
const companySearchResults = ref<CompanyItem[]>([])
|
||||
const companySearchVisible = ref(false)
|
||||
let companySearchTimer: ReturnType<typeof setTimeout> | null = null
|
||||
/** 公司实体选择框(EntityPicker):选中公司 UUID 与回显标签 */
|
||||
const companyPickerValue = ref('')
|
||||
const companyPickerLabel = ref('')
|
||||
|
||||
/** 右键菜单 */
|
||||
const ctxMenuVisible = ref(false)
|
||||
@@ -167,48 +160,30 @@ function buildSubtree(flatMenus: MenuNode[], rootUUID: string): MenuNode | null
|
||||
return result
|
||||
}
|
||||
|
||||
/** 公司搜索(防抖 300ms) */
|
||||
function onCompanySearchInput(): void {
|
||||
if (companySearchTimer) clearTimeout(companySearchTimer)
|
||||
const kw = companySearchKeyword.value.trim()
|
||||
if (!kw) {
|
||||
companySearchResults.value = []
|
||||
companySearchVisible.value = false
|
||||
return
|
||||
}
|
||||
companySearchTimer = setTimeout(async () => {
|
||||
try {
|
||||
const data = await request.get<CompanyItem[]>('/API/FormAPI/CompanySearch', {
|
||||
params: { keyword: kw, limit: 10 },
|
||||
})
|
||||
companySearchResults.value = Array.isArray(data) ? data : []
|
||||
companySearchVisible.value = companySearchResults.value.length > 0
|
||||
} catch {
|
||||
companySearchResults.value = []
|
||||
companySearchVisible.value = false
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
|
||||
/** 选择公司:打开或切换标签页 */
|
||||
function selectCompany(c: CompanyItem): void {
|
||||
companySearchKeyword.value = c.CompanyName
|
||||
companySearchVisible.value = false
|
||||
const existing = tabs.value.findIndex((t) => t.companyUUID === c.CompanyUUID)
|
||||
/** 打开或切换公司标签页(EntityPicker 选择触发) */
|
||||
function openCompanyTab(uuid: string, name: string): void {
|
||||
if (!uuid) return
|
||||
const existing = tabs.value.findIndex((t) => t.companyUUID === uuid)
|
||||
if (existing >= 0) {
|
||||
switchTab(existing)
|
||||
} else {
|
||||
tabs.value.push({
|
||||
companyUUID: c.CompanyUUID,
|
||||
companyName: c.CompanyName,
|
||||
menus: [],
|
||||
selectedUUID: null,
|
||||
})
|
||||
switchTab(tabs.value.length - 1)
|
||||
loadMenus()
|
||||
return
|
||||
}
|
||||
tabs.value.push({
|
||||
companyUUID: uuid,
|
||||
companyName: name || uuid,
|
||||
menus: [],
|
||||
selectedUUID: null,
|
||||
})
|
||||
switchTab(tabs.value.length - 1)
|
||||
void loadMenus()
|
||||
}
|
||||
|
||||
/** 公司实体选择框选中 → 打开/切换公司标签页 */
|
||||
watch(companyPickerValue, (uuid) => {
|
||||
if (!uuid) return
|
||||
openCompanyTab(uuid, companyPickerLabel.value || uuid)
|
||||
})
|
||||
|
||||
/** 切换标签页 */
|
||||
function switchTab(index: number): void {
|
||||
if (index < 0 || index >= tabs.value.length) return
|
||||
@@ -754,11 +729,10 @@ onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', onDocClick)
|
||||
})
|
||||
|
||||
/** 文档点击:关闭右键菜单 / 公司搜索下拉 */
|
||||
/** 文档点击:关闭右键菜单 */
|
||||
function onDocClick(e: MouseEvent): void {
|
||||
const target = e.target as HTMLElement
|
||||
if (!target.closest('.menu-ctx-panel')) ctxMenuVisible.value = false
|
||||
if (!target.closest('.company-search-wrap')) companySearchVisible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -783,24 +757,14 @@ function onDocClick(e: MouseEvent): void {
|
||||
<span>菜单编辑器</span>
|
||||
</h1>
|
||||
<div class="company-search-wrap">
|
||||
<el-input
|
||||
v-model="companySearchKeyword"
|
||||
<EntityPicker
|
||||
v-model="companyPickerValue"
|
||||
:config="entityPickerPresets.Company"
|
||||
:existing-label="companyPickerLabel"
|
||||
placeholder="搜索公司添加标签页..."
|
||||
clearable
|
||||
style="width: 240px"
|
||||
@input="onCompanySearchInput"
|
||||
@update:label="(label: string) => (companyPickerLabel = label)"
|
||||
/>
|
||||
<div v-if="companySearchVisible" class="company-search-dropdown">
|
||||
<div
|
||||
v-for="c in companySearchResults"
|
||||
:key="c.CompanyUUID"
|
||||
class="company-search-item"
|
||||
@click="selectCompany(c)"
|
||||
>
|
||||
<strong>{{ c.CompanyName }}</strong>
|
||||
<span>{{ c.CompanyShort || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-button type="primary" :disabled="!activeTab" @click="addRootMenu">
|
||||
<i class="fas fa-plus" /> 新增根菜单
|
||||
@@ -1010,40 +974,7 @@ function onDocClick(e: MouseEvent): void {
|
||||
}
|
||||
|
||||
.company-search-wrap {
|
||||
position: relative;
|
||||
|
||||
.company-search-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 100;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
|
||||
.company-search-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
// EntityPicker 公司实体选择框容器(头部工具区布局占位)
|
||||
}
|
||||
|
||||
.menu-body {
|
||||
|
||||
Reference in new issue
Block a user