批量更新
This commit is contained in:
+12
-11
@@ -11,17 +11,6 @@ export function getAllFiles() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的文件
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getMyFiles() {
|
||||
return request({
|
||||
url: "/api/files/my",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取文件
|
||||
* @param {number|string} id 文件ID
|
||||
@@ -34,6 +23,18 @@ export function getFileById(id) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据租户ID获取文件
|
||||
* @param {string} tenantId 租户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getFilesByTenant(tenantId) {
|
||||
return request({
|
||||
url: `/api/files/tenant?tenant_id=${tenantId}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param {FormData} formData 文件数据
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getAllRoles(params) {
|
||||
return request({
|
||||
url: '/api/roles',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function createRole(data) {
|
||||
return request({
|
||||
url: '/api/roles',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getRoleById(id) {
|
||||
return request({
|
||||
url: `/api/roles/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getRoleByTenantId(tenantId) {
|
||||
return request({
|
||||
url: `/api/roles/tenant/${tenantId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function updateRole(id, data) {
|
||||
return request({
|
||||
url: `/api/roles/${id}`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteRole(id) {
|
||||
return request({
|
||||
url: `/api/roles/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@@ -40,4 +40,12 @@ body {
|
||||
position: fixed !important;
|
||||
z-index: var(--el-message-z-index, 9999) !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
.wang-editor-wrapper{
|
||||
border: 1px solid #dcdfe6 !important;
|
||||
|
||||
.toolbar-container{
|
||||
border-bottom: 1px solid #dcdfe6 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,14 @@ export function convertMenusToRoutes(menus) {
|
||||
}
|
||||
}
|
||||
|
||||
// 特殊处理:知识库的编辑和详情页需要支持动态参数
|
||||
// 如果最终路径是 edit 或 detail,转换为支持参数的路由
|
||||
if (currentRoute.path === 'edit') {
|
||||
currentRoute.path = 'edit/:id';
|
||||
} else if (currentRoute.path === 'detail') {
|
||||
currentRoute.path = 'detail/:id';
|
||||
}
|
||||
|
||||
if (!parentRoute.children) {
|
||||
parentRoute.children = [];
|
||||
}
|
||||
|
||||
+16
-21
@@ -140,30 +140,25 @@ function addDynamicRoutes(menus) {
|
||||
|
||||
router.addRoute(newMainRoute);
|
||||
|
||||
// 添加知识库的子路由(详情页和编辑页)
|
||||
// 直接在 Main 路由下添加完整路径的子路由
|
||||
// router.addRoute('Main', {
|
||||
// path: 'apps/knowledge/detail/:id',
|
||||
// name: 'apps-knowledge-detail',
|
||||
// component: () => import('@/views/apps/knowledge/components/detail.vue'),
|
||||
// meta: {
|
||||
// requiresAuth: true,
|
||||
// title: '知识详情'
|
||||
// }
|
||||
// });
|
||||
// router.addRoute('Main', {
|
||||
// path: 'apps/knowledge/edit/:id',
|
||||
// name: 'apps-knowledge-edit',
|
||||
// component: () => import('@/views/apps/knowledge/components/edit.vue'),
|
||||
// meta: {
|
||||
// requiresAuth: true,
|
||||
// title: '编辑知识'
|
||||
// }
|
||||
// });
|
||||
|
||||
dynamicRoutesAdded = true;
|
||||
}
|
||||
|
||||
// 递归查找路由(根据名称)
|
||||
function findRouteByName(routes, routeName) {
|
||||
for (const route of routes) {
|
||||
if (route.name === routeName) {
|
||||
return route;
|
||||
}
|
||||
if (route.children) {
|
||||
const found = findRouteByName(route.children, routeName);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 查找第一个有效的路由(有组件的路由)
|
||||
function findFirstValidRoute(routes) {
|
||||
for (const route of routes) {
|
||||
|
||||
@@ -114,6 +114,48 @@ export const useTabsStore = defineTabsStore('tabs', () => {
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
|
||||
// 关闭左侧(关闭指定tab左侧的所有tab,保留首页和目标tab)
|
||||
function closeLeft(targetFullPath) {
|
||||
const targetIndex = tabList.value.findIndex((t) => t.fullPath === targetFullPath);
|
||||
if (targetIndex > -1) {
|
||||
// 保留首页和目标tab及其右侧的所有tab
|
||||
const beforeIndex = tabList.value.slice(0, targetIndex);
|
||||
const hasCloseableLeft = beforeIndex.some(t => t.fullPath !== defaultDashboardPath);
|
||||
|
||||
if (hasCloseableLeft) {
|
||||
tabList.value = tabList.value.filter((t, index) =>
|
||||
t.fullPath === defaultDashboardPath || index >= targetIndex
|
||||
);
|
||||
// 如果关闭的tab中包含了当前激活的tab,则激活目标tab
|
||||
if (!tabList.value.find(t => t.fullPath === activeTab.value)) {
|
||||
activeTab.value = targetFullPath;
|
||||
}
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭右侧(关闭指定tab右侧的所有tab,保留首页和目标tab)
|
||||
function closeRight(targetFullPath) {
|
||||
const targetIndex = tabList.value.findIndex((t) => t.fullPath === targetFullPath);
|
||||
if (targetIndex > -1) {
|
||||
// 保留首页和目标tab及其左侧的所有tab
|
||||
const afterIndex = tabList.value.slice(targetIndex + 1);
|
||||
const hasCloseableRight = afterIndex.length > 0;
|
||||
|
||||
if (hasCloseableRight) {
|
||||
tabList.value = tabList.value.filter((t, index) =>
|
||||
t.fullPath === defaultDashboardPath || index <= targetIndex
|
||||
);
|
||||
// 如果关闭的tab中包含了当前激活的tab,则激活目标tab
|
||||
if (!tabList.value.find(t => t.fullPath === activeTab.value)) {
|
||||
activeTab.value = targetFullPath;
|
||||
}
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭全部,只留首页
|
||||
function closeAll() {
|
||||
tabList.value = tabList.value.filter((t) => t.fullPath === defaultDashboardPath);
|
||||
@@ -133,6 +175,8 @@ export const useTabsStore = defineTabsStore('tabs', () => {
|
||||
addTab,
|
||||
removeTab,
|
||||
closeOthers,
|
||||
closeLeft,
|
||||
closeRight,
|
||||
closeAll,
|
||||
setActiveTab,
|
||||
saveTabsToStorage,
|
||||
|
||||
+307
-16
@@ -3,7 +3,7 @@ import CommonAside from '@/components/CommonAside.vue';
|
||||
import CommonHeader from '@/components/CommonHeader.vue';
|
||||
import { useTabsStore } from '@/stores';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { ref, watch, reactive, nextTick, onMounted } from 'vue';
|
||||
import { ref, watch, reactive, nextTick, onMounted, computed } from 'vue';
|
||||
import { More, Close, CircleClose } from '@element-plus/icons-vue'
|
||||
|
||||
const tabsStore = useTabsStore();
|
||||
@@ -42,6 +42,45 @@ onMounted(() => {
|
||||
// 刷新时,localStorage 中已保存的 tabs 会在 store 初始化时恢复
|
||||
// 这里只需要确保当前路由对应的 tab 存在并激活
|
||||
restoreTabFromRoute();
|
||||
|
||||
// 为 tabs 容器添加右键事件监听(使用事件委托)
|
||||
const tabsWrapper = document.querySelector('.multi-tabs-wrapper');
|
||||
if (tabsWrapper) {
|
||||
tabsWrapper.addEventListener('contextmenu', (e) => {
|
||||
// 排除编辑器相关区域,避免影响编辑器功能
|
||||
// 如果点击在编辑器区域内,完全不做任何处理(优先检查)
|
||||
const editorWrapper = e.target.closest?.('.wang-editor-wrapper');
|
||||
if (editorWrapper) {
|
||||
// 在编辑器区域内,不处理右键菜单,也不阻止事件
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查其他编辑器元素(工具栏、下拉面板等)
|
||||
const editorElements = [
|
||||
'.w-e-toolbar',
|
||||
'.w-e-drop-panel',
|
||||
'.w-e-modal',
|
||||
'.w-e-toolbar-menu',
|
||||
'[data-menu-key]',
|
||||
'[class*="w-e-"]'
|
||||
];
|
||||
|
||||
for (const selector of editorElements) {
|
||||
if (e.target.closest && e.target.closest(selector)) {
|
||||
// 在编辑器元素内,不处理右键菜单
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 只有在非编辑器区域的 tab item 上才处理右键菜单
|
||||
const tabItem = e.target.closest('.el-tabs__item');
|
||||
if (tabItem) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleTabsContextMenu(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -114,16 +153,141 @@ const contextMenu = reactive({
|
||||
});
|
||||
const contextDropdownRef = ref(null);
|
||||
|
||||
const onTabContextMenu = (event, tab) => {
|
||||
// 处理 tabs 容器的右键事件
|
||||
const handleTabsContextMenu = (event) => {
|
||||
event.preventDefault();
|
||||
contextMenu.visible = true;
|
||||
contextMenu.x = event.clientX;
|
||||
contextMenu.y = event.clientY;
|
||||
contextMenu.tab = tab;
|
||||
event.stopPropagation();
|
||||
|
||||
// 找到点击的 tab item 元素(el-tabs__item)
|
||||
let target = event.target;
|
||||
let tabItem = null;
|
||||
|
||||
// 向上查找 el-tabs__item 元素
|
||||
while (target && target !== event.currentTarget) {
|
||||
if (target.classList && target.classList.contains('el-tabs__item')) {
|
||||
tabItem = target;
|
||||
break;
|
||||
}
|
||||
target = target.parentElement;
|
||||
}
|
||||
|
||||
if (!tabItem) return;
|
||||
|
||||
// Element Plus 的 tab item 的 id 格式通常是 "tab-{name}",其中 name 是 tab-pane 的 name 属性
|
||||
const tabId = tabItem.id;
|
||||
if (tabId && tabId.startsWith('tab-')) {
|
||||
const tabName = tabId.replace('tab-', '');
|
||||
const matchedTab = tabsStore.tabList.find(t => t.fullPath === tabName);
|
||||
if (matchedTab) {
|
||||
showContextMenu(event, matchedTab);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果通过 id 找不到,尝试通过 aria-controls 或其他属性
|
||||
const ariaControls = tabItem.getAttribute('aria-controls');
|
||||
if (ariaControls) {
|
||||
const tabName = ariaControls.replace('pane-', '');
|
||||
const matchedTab = tabsStore.tabList.find(t => t.fullPath === tabName);
|
||||
if (matchedTab) {
|
||||
showContextMenu(event, matchedTab);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 显示右键菜单
|
||||
const showContextMenu = (event, tab) => {
|
||||
// 使用 nextTick 确保在下一个事件循环中更新状态,避免在渲染过程中触发更新
|
||||
nextTick(() => {
|
||||
// 溢出优化可扩展
|
||||
contextMenu.visible = true;
|
||||
contextMenu.x = event.clientX;
|
||||
contextMenu.y = event.clientY;
|
||||
contextMenu.tab = tab;
|
||||
|
||||
// 延迟添加事件监听器,确保菜单已渲染
|
||||
// 关键:完全排除编辑器区域,确保编辑器的事件不被干扰
|
||||
setTimeout(() => {
|
||||
const hideMenuHandler = (e) => {
|
||||
// 如果右键菜单已经隐藏,直接返回
|
||||
if (!contextMenu.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = e.target;
|
||||
if (!target) {
|
||||
hideContextMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查点击是否在右键菜单本身上
|
||||
if (target.closest?.('.context-menu')) {
|
||||
return; // 点击在菜单上,不隐藏
|
||||
}
|
||||
|
||||
// ========== 关键修复:优先检查编辑器区域 ==========
|
||||
// 如果在编辑器区域内,立即返回,不执行任何操作,让编辑器的事件正常处理
|
||||
|
||||
// 先检查是否在编辑器包装器内(最快判断)
|
||||
const wangEditorWrapper = target.closest?.('.wang-editor-wrapper');
|
||||
if (wangEditorWrapper) {
|
||||
// 在编辑器包装器内,完全不做任何处理,让编辑器正常处理
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查所有可能的编辑器元素(包括工具栏、下拉面板、模态框等)
|
||||
const editorSelectors = [
|
||||
'.w-e-toolbar',
|
||||
'.w-e-drop-panel',
|
||||
'.w-e-modal',
|
||||
'.w-e-toolbar-menu',
|
||||
'.toolbar-container',
|
||||
'.editor-container',
|
||||
'.w-e-text-container',
|
||||
'.w-e-text',
|
||||
// 检查是否有 WangEditor 相关的元素
|
||||
'[data-menu-key]',
|
||||
'[class*="w-e-"]'
|
||||
];
|
||||
|
||||
for (const selector of editorSelectors) {
|
||||
if (target.closest?.(selector)) {
|
||||
// 在编辑器区域内,完全不做任何处理,让编辑器的事件正常处理
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 其他区域点击,隐藏菜单
|
||||
hideContextMenu();
|
||||
};
|
||||
|
||||
// 关键修复:使用 capture: false 在冒泡阶段处理,并且延迟注册
|
||||
// 这样可以确保编辑器的监听器(通常在目标元素上)先处理事件
|
||||
// 然后再处理我们的监听器(在 body 上)
|
||||
// 注意:只有在右键菜单显示时才注册,并且排除编辑器区域
|
||||
setTimeout(() => {
|
||||
// 再次检查右键菜单是否仍然可见
|
||||
if (contextMenu.visible) {
|
||||
document.body.addEventListener('click', hideMenuHandler, { once: true, capture: false, passive: true });
|
||||
}
|
||||
}, 100); // 延迟注册,确保编辑器的事件监听器已经注册并可以正常处理
|
||||
|
||||
const hideContextMenuHandler = (e) => {
|
||||
const target = e.target;
|
||||
if (!target) {
|
||||
hideContextMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
// 排除编辑器区域和右键菜单本身
|
||||
if (target.closest?.('.w-e-toolbar') || target.closest?.('.context-menu') || target.closest?.('.wang-editor-wrapper')) {
|
||||
return;
|
||||
}
|
||||
hideContextMenu();
|
||||
};
|
||||
|
||||
document.body.addEventListener('contextmenu', hideContextMenuHandler, { once: true });
|
||||
}, 0);
|
||||
});
|
||||
document.body.addEventListener('click', hideContextMenu, { once: true });
|
||||
};
|
||||
function hideContextMenu() {
|
||||
contextMenu.visible = false;
|
||||
@@ -135,18 +299,63 @@ function closeTabContextTab() {
|
||||
}
|
||||
hideContextMenu();
|
||||
}
|
||||
function closeOthersContextTab() {
|
||||
// 关闭左侧
|
||||
function closeLeftContextTab() {
|
||||
if (contextMenu.tab) {
|
||||
tabsStore.activeTab = contextMenu.tab.fullPath;
|
||||
tabsStore.closeOthers();
|
||||
tabsStore.closeLeft(contextMenu.tab.fullPath);
|
||||
// 如果当前路由对应的tab被关闭了,跳转到激活的tab
|
||||
if (!tabsStore.tabList.find(tab => tab.fullPath === route.fullPath)) {
|
||||
router.push(tabsStore.activeTab);
|
||||
}
|
||||
}
|
||||
hideContextMenu();
|
||||
}
|
||||
|
||||
// 关闭右侧
|
||||
function closeRightContextTab() {
|
||||
if (contextMenu.tab) {
|
||||
tabsStore.closeRight(contextMenu.tab.fullPath);
|
||||
// 如果当前路由对应的tab被关闭了,跳转到激活的tab
|
||||
if (!tabsStore.tabList.find(tab => tab.fullPath === route.fullPath)) {
|
||||
router.push(tabsStore.activeTab);
|
||||
}
|
||||
}
|
||||
hideContextMenu();
|
||||
}
|
||||
|
||||
// 关闭其他
|
||||
function closeOthersContextTab() {
|
||||
if (contextMenu.tab) {
|
||||
tabsStore.setActiveTab(contextMenu.tab.fullPath);
|
||||
tabsStore.closeOthers();
|
||||
if (!tabsStore.tabList.find(tab => tab.fullPath === route.fullPath)) {
|
||||
router.push(tabsStore.activeTab);
|
||||
}
|
||||
}
|
||||
hideContextMenu();
|
||||
}
|
||||
|
||||
// 关闭全部
|
||||
function closeAllTabs() {
|
||||
tabsStore.closeAll();
|
||||
hideContextMenu();
|
||||
router.push(defaultDashboardPath);
|
||||
}
|
||||
|
||||
// 计算是否可以关闭左侧/右侧
|
||||
const canCloseLeft = computed(() => {
|
||||
if (!contextMenu.tab) return false;
|
||||
const targetIndex = tabsStore.tabList.findIndex(t => t.fullPath === contextMenu.tab.fullPath);
|
||||
// 至少左侧有一个可关闭的tab(排除首页)
|
||||
return targetIndex > 0 && tabsStore.tabList.slice(0, targetIndex).some(t => t.fullPath !== defaultDashboardPath);
|
||||
});
|
||||
|
||||
const canCloseRight = computed(() => {
|
||||
if (!contextMenu.tab) return false;
|
||||
const targetIndex = tabsStore.tabList.findIndex(t => t.fullPath === contextMenu.tab.fullPath);
|
||||
// 右侧至少有一个tab
|
||||
return targetIndex < tabsStore.tabList.length - 1;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -172,9 +381,48 @@ function closeAllTabs() {
|
||||
:label="tab.title"
|
||||
:name="tab.fullPath"
|
||||
:closable="tab.fullPath !== defaultDashboardPath"
|
||||
@contextmenu="onTabContextMenu($event, tab)"
|
||||
:data-tab-path="tab.fullPath"
|
||||
/>
|
||||
</el-tabs>
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<teleport to="body">
|
||||
<div
|
||||
v-if="contextMenu.visible"
|
||||
class="context-menu"
|
||||
:style="{
|
||||
left: contextMenu.x + 'px',
|
||||
top: contextMenu.y + 'px'
|
||||
}"
|
||||
@click.stop
|
||||
>
|
||||
<div class="context-menu-item"
|
||||
:class="{ 'is-disabled': contextMenu.tab && contextMenu.tab.fullPath === defaultDashboardPath }"
|
||||
@click="!((contextMenu.tab && contextMenu.tab.fullPath === defaultDashboardPath)) && closeTabContextTab()">
|
||||
关闭
|
||||
</div>
|
||||
<div class="context-menu-item"
|
||||
:class="{ 'is-disabled': !canCloseLeft }"
|
||||
@click="canCloseLeft && closeLeftContextTab()">
|
||||
关闭左侧
|
||||
</div>
|
||||
<div class="context-menu-item"
|
||||
:class="{ 'is-disabled': !canCloseRight }"
|
||||
@click="canCloseRight && closeRightContextTab()">
|
||||
关闭右侧
|
||||
</div>
|
||||
<div class="context-menu-item"
|
||||
:class="{ 'is-disabled': contextMenu.tab && contextMenu.tab.fullPath === defaultDashboardPath && tabsStore.tabList.length <= 1 }"
|
||||
@click="!((contextMenu.tab && contextMenu.tab.fullPath === defaultDashboardPath && tabsStore.tabList.length <= 1)) && closeOthersContextTab()">
|
||||
关闭其他
|
||||
</div>
|
||||
<div class="context-menu-item"
|
||||
:class="{ 'is-disabled': tabsStore.tabList.length <= 1 }"
|
||||
@click="tabsStore.tabList.length > 1 && closeAllTabs()">
|
||||
关闭全部
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
<!-- 右侧操作按钮 -->
|
||||
<div class="tabs-extra-actions">
|
||||
<el-dropdown trigger="click">
|
||||
@@ -197,11 +445,11 @@ function closeAllTabs() {
|
||||
|
||||
<!-- 主内容毛玻璃卡片容器 -->
|
||||
<div class="main-panel glass-card">
|
||||
<keep-alive :max="10">
|
||||
<router-view v-slot="{ Component }">
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :max="10">
|
||||
<component :is="Component" />
|
||||
</router-view>
|
||||
</keep-alive>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</el-main>
|
||||
</el-container>
|
||||
@@ -336,6 +584,7 @@ function closeAllTabs() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.tabs-extra-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
@@ -358,3 +607,45 @@ function closeAllTabs() {
|
||||
border-right: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
// 右键菜单样式 - 全局样式,因为使用了 teleport 到 body
|
||||
.context-menu {
|
||||
position: fixed !important;
|
||||
z-index: 9999 !important;
|
||||
background: var(--el-bg-color-overlay) !important;
|
||||
border: 1px solid var(--el-border-color-lighter) !important;
|
||||
border-radius: 4px !important;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1) !important;
|
||||
min-width: 120px !important;
|
||||
padding: 4px 0 !important;
|
||||
pointer-events: auto !important; // 确保菜单本身可以接收点击
|
||||
|
||||
.context-menu-item {
|
||||
padding: 8px 16px !important;
|
||||
cursor: pointer !important;
|
||||
color: var(--el-text-color-primary) !important;
|
||||
font-size: 14px !important;
|
||||
transition: background-color 0.2s !important;
|
||||
|
||||
&:hover:not(.is-disabled) {
|
||||
background-color: var(--el-fill-color-light) !important;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
color: var(--el-text-color-disabled) !important;
|
||||
cursor: not-allowed !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 确保编辑器工具栏和下拉面板的 z-index 高于右键菜单
|
||||
:deep(.w-e-toolbar),
|
||||
:deep(.w-e-drop-panel),
|
||||
:deep(.w-e-modal),
|
||||
:deep(.w-e-toolbar-menu) {
|
||||
z-index: 10000 !important; // 高于右键菜单的 9999
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加分类
|
||||
</el-button>
|
||||
<el-button @click="fetchList" :loading="loading">
|
||||
<el-button type="primary" @click="refresh" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
@@ -365,6 +365,16 @@ const fetchList = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新界面
|
||||
async function refresh() {
|
||||
try {
|
||||
await fetchList();
|
||||
ElMessage.success('刷新成功');
|
||||
} catch (error) {
|
||||
ElMessage.error('刷新失败');
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '添加分类';
|
||||
// 先重置表单
|
||||
|
||||
@@ -31,19 +31,15 @@
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="分类" prop="category">
|
||||
<el-select
|
||||
v-model="formData.category"
|
||||
<el-cascader
|
||||
v-model="cascaderValue"
|
||||
:options="categoryTreeOptions"
|
||||
:props="cascaderProps"
|
||||
placeholder="请选择分类"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
@change="handleCategoryChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in categoryList"
|
||||
:key="item.categoryId"
|
||||
:label="item.categoryName"
|
||||
:value="item.categoryName"
|
||||
/>
|
||||
</el-select>
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
@@ -73,8 +69,8 @@
|
||||
<el-col :span="6">
|
||||
<el-form-item label="权限" prop="share">
|
||||
<el-radio-group v-model="formData.share">
|
||||
<el-radio-button :label="0">个人</el-radio-button>
|
||||
<el-radio-button :label="1">共享</el-radio-button>
|
||||
<el-radio-button :value="0">个人</el-radio-button>
|
||||
<el-radio-button :value="1">共享</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -115,7 +111,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, watch } from "vue";
|
||||
import { ref, reactive, computed, onMounted, watch, nextTick } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { marked } from "marked";
|
||||
import { ArrowLeft, Check } from '@element-plus/icons-vue'
|
||||
@@ -158,8 +154,21 @@ const rules = reactive<FormRules>({
|
||||
interface CategoryItem {
|
||||
categoryId: number;
|
||||
categoryName: string;
|
||||
parentId?: number;
|
||||
children?: CategoryItem[];
|
||||
}
|
||||
const categoryList = ref<CategoryItem[]>([]);
|
||||
const categoryTreeOptions = computed(() => {
|
||||
return buildCategoryTree(categoryList.value);
|
||||
});
|
||||
const cascaderValue = ref<number | null>(null);
|
||||
const cascaderProps = {
|
||||
value: 'categoryId',
|
||||
label: 'categoryName',
|
||||
children: 'children',
|
||||
checkStrictly: true,
|
||||
emitPath: false,
|
||||
};
|
||||
const tagList = ref<string[]>([]);
|
||||
|
||||
const id = computed(() => route.params.id || route.query.id);
|
||||
@@ -204,14 +213,25 @@ const fetchDetail = async () => {
|
||||
formData.content = data.content || "";
|
||||
formData.share = data.share || 0;
|
||||
|
||||
if (!formData.categoryId && formData.category) {
|
||||
const foundCategory = categoryList.value.find(
|
||||
(item) => item.categoryName === formData.category
|
||||
);
|
||||
if (foundCategory) {
|
||||
formData.categoryId = foundCategory.categoryId;
|
||||
// 设置级联选择器的值
|
||||
// 注意:这里需要等待 categoryList 加载完成后再设置
|
||||
// 所以使用 nextTick 确保数据已更新
|
||||
nextTick(() => {
|
||||
if (formData.categoryId) {
|
||||
cascaderValue.value = formData.categoryId;
|
||||
} else if (formData.category) {
|
||||
// 如果没有 categoryId,尝试通过 categoryName 查找
|
||||
const foundCategory = categoryList.value.find(
|
||||
(item) => item.categoryName === formData.category
|
||||
);
|
||||
if (foundCategory) {
|
||||
formData.categoryId = foundCategory.categoryId;
|
||||
cascaderValue.value = foundCategory.categoryId;
|
||||
}
|
||||
} else {
|
||||
cascaderValue.value = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (data.tags) {
|
||||
try {
|
||||
@@ -253,12 +273,67 @@ const loadCategoryAndTag = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCategoryChange = (categoryName: string) => {
|
||||
const selectedCategory = categoryList.value.find(
|
||||
(item) => item.categoryName === categoryName
|
||||
);
|
||||
if (selectedCategory) {
|
||||
formData.categoryId = selectedCategory.categoryId;
|
||||
// 构建分类树形结构
|
||||
const buildCategoryTree = (list: CategoryItem[]): CategoryItem[] => {
|
||||
if (!list || list.length === 0) return [];
|
||||
|
||||
const map = new Map<number, CategoryItem>();
|
||||
const roots: CategoryItem[] = [];
|
||||
|
||||
// 创建节点映射
|
||||
list.forEach(item => {
|
||||
map.set(item.categoryId, {
|
||||
...item,
|
||||
children: [],
|
||||
});
|
||||
});
|
||||
|
||||
// 构建树形结构
|
||||
list.forEach(item => {
|
||||
const node = map.get(item.categoryId);
|
||||
const parentId = item.parentId || 0;
|
||||
|
||||
if (parentId === 0 || !map.has(parentId)) {
|
||||
// 根节点
|
||||
roots.push(node!);
|
||||
} else {
|
||||
// 子节点
|
||||
const parent = map.get(parentId);
|
||||
if (parent && node) {
|
||||
if (!parent.children) {
|
||||
parent.children = [];
|
||||
}
|
||||
parent.children.push(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return roots;
|
||||
};
|
||||
|
||||
const handleCategoryChange = (value: number | null) => {
|
||||
if (value) {
|
||||
formData.categoryId = value;
|
||||
// 根据 categoryId 查找 categoryName
|
||||
const findCategoryName = (list: CategoryItem[], id: number): string | null => {
|
||||
for (const item of list) {
|
||||
if (item.categoryId === id) {
|
||||
return item.categoryName;
|
||||
}
|
||||
if (item.children) {
|
||||
const found = findCategoryName(item.children, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const categoryName = findCategoryName(categoryTreeOptions.value, value);
|
||||
if (categoryName) {
|
||||
formData.category = categoryName;
|
||||
}
|
||||
} else {
|
||||
formData.categoryId = 0;
|
||||
formData.category = '';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -340,12 +415,8 @@ onMounted(async () => {
|
||||
|
||||
if (isEdit.value) {
|
||||
await fetchDetail();
|
||||
} else {
|
||||
if (categoryList.value.length > 0 && !formData.category) {
|
||||
formData.category = categoryList.value[0].categoryName;
|
||||
formData.categoryId = categoryList.value[0].categoryId;
|
||||
}
|
||||
}
|
||||
// 新建时不需要设置默认分类,让用户自己选择
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
@@ -428,12 +499,13 @@ defineExpose({
|
||||
|
||||
.editor-title,
|
||||
.preview-title {
|
||||
margin: 0 0 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.editor-content {
|
||||
flex: 1;
|
||||
min-height: 400px;
|
||||
|
||||
@@ -80,6 +80,10 @@
|
||||
<el-icon><PriceTag /></el-icon>
|
||||
标签管理
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -132,7 +136,7 @@
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<el-icon><Clock /></el-icon>
|
||||
<span>{{ formatDate(repo.createTime) }}</span>
|
||||
<span>{{ formatDate(repo.updateTime || repo.createTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,7 +226,7 @@
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<el-icon><Clock /></el-icon>
|
||||
<span>{{ formatDate(repo.createTime) }}</span>
|
||||
<span>{{ formatDate(repo.updateTime || repo.createTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -281,7 +285,7 @@
|
||||
import { ref, reactive, computed, onMounted } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Search, Plus, Folder, PriceTag, User, View, Star, Clock } from "@element-plus/icons-vue";
|
||||
import { Search, Plus, Folder, PriceTag, User, View, Star, Clock, Refresh } from "@element-plus/icons-vue";
|
||||
|
||||
// @ts-ignore
|
||||
import { getKnowledgeList, deleteKnowledge } from "@/api/knowledge";
|
||||
@@ -318,10 +322,10 @@ interface StatItem {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// 检查当前路由是否是子路由(category 或 tag)
|
||||
// 检查当前路由是否是子路由(category、tag、edit 或 detail)
|
||||
const isSubRoute = computed(() => {
|
||||
const path = route.path;
|
||||
return path.includes('/category') || path.includes('/tag');
|
||||
return path.includes('/category') || path.includes('/tag') || path.includes('/edit') || path.includes('/detail');
|
||||
});
|
||||
|
||||
// 响应式数据
|
||||
@@ -418,6 +422,16 @@ async function fetchRepoList() {
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新界面
|
||||
async function refresh() {
|
||||
try {
|
||||
await fetchRepoList();
|
||||
ElMessage.success('刷新成功');
|
||||
} catch (error) {
|
||||
ElMessage.error('刷新失败');
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreate() {
|
||||
router.push(`/apps/knowledge/edit/new`);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加标签
|
||||
</el-button>
|
||||
<el-button @click="fetchList" :loading="loading">
|
||||
<el-button type="primary" @click="refresh" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
@@ -263,6 +263,16 @@ const fetchList = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新界面
|
||||
async function refresh() {
|
||||
try {
|
||||
await fetchList();
|
||||
ElMessage.success('刷新成功');
|
||||
} catch (error) {
|
||||
ElMessage.error('刷新失败');
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '添加标签';
|
||||
formRef.value?.resetFields();
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
<el-card class="box-card">
|
||||
<div class="header-bar">
|
||||
<h2>文件管理</h2>
|
||||
<!-- <el-button type="primary" @click="showProgramDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加程序
|
||||
</el-button> -->
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading-state">
|
||||
@@ -108,13 +104,6 @@
|
||||
/>
|
||||
</el-select>
|
||||
|
||||
<el-switch
|
||||
v-model="showMyFiles"
|
||||
class="switch-mine"
|
||||
active-text="仅看我的文件"
|
||||
inactive-text="全部文件"
|
||||
@change="handleFilter"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 文件数据列表 -->
|
||||
@@ -373,9 +362,11 @@ import {
|
||||
Link,
|
||||
UploadFilled,
|
||||
} from "@element-plus/icons-vue";
|
||||
import { getAllFiles, getMyFiles, getFileById, deleteFile as deleteFileApi, searchFiles } from "@/api/file";
|
||||
import { getFilesByTenant, getFileById, deleteFile as deleteFileApi, searchFiles } from "@/api/file";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// 文件列表数据
|
||||
const allFiles = ref<any[]>([]);
|
||||
@@ -385,23 +376,35 @@ const loading = ref(false);
|
||||
const error = ref("");
|
||||
const searchKeyword = ref("");
|
||||
const filterCategory = ref("");
|
||||
const showMyFiles = ref(false);
|
||||
const pageSize = ref(10);
|
||||
const currentPage = ref(1);
|
||||
const totalFiles = ref(0);
|
||||
const showUploadDialog = ref(false);
|
||||
|
||||
// 获取当前租户ID
|
||||
const getCurrentTenantId = () => {
|
||||
if (authStore.user && authStore.user.tenant_id) {
|
||||
return authStore.user.tenant_id;
|
||||
}
|
||||
const userInfo = localStorage.getItem('userInfo');
|
||||
if (userInfo) {
|
||||
try {
|
||||
const user = JSON.parse(userInfo);
|
||||
return user.tenant_id || user.tenantId || 0;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse user info:', e);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// 获取文件列表
|
||||
async function fetchFiles() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
let res;
|
||||
if (showMyFiles.value) {
|
||||
res = await getMyFiles();
|
||||
} else {
|
||||
res = await getAllFiles();
|
||||
}
|
||||
const tenantId = getCurrentTenantId();
|
||||
const res = await getFilesByTenant(tenantId);
|
||||
|
||||
if (res.success && res.data) {
|
||||
const files = Array.isArray(res.data) ? res.data : [];
|
||||
@@ -685,10 +688,6 @@ const deleteFile = async (file: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 监听筛选条件变化
|
||||
watch([showMyFiles], () => {
|
||||
fetchFiles();
|
||||
});
|
||||
|
||||
// 分页计算
|
||||
const paginatedFiles = computed(() => {
|
||||
|
||||
@@ -2,25 +2,41 @@
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>菜单管理</h2>
|
||||
<el-button type="primary" @click="handleAddMenu = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加菜单
|
||||
</el-button>
|
||||
<div class="header-actions">
|
||||
<el-button @click="expandAll">
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
全部展开
|
||||
</el-button>
|
||||
<el-button @click="collapseAll">
|
||||
<el-icon><Folder /></el-icon>
|
||||
全部折叠
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleAddMenu">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加菜单
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 树形表格 -->
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
:data="menuTree"
|
||||
style="width: 100%"
|
||||
row-key="Id"
|
||||
border
|
||||
v-loading="loading"
|
||||
element-loading-text="正在加载..."
|
||||
:tree-props="{
|
||||
children: 'children',
|
||||
hasChildren: 'hasChildren',
|
||||
}"
|
||||
:expand-row-keys="defaultExpandedKeys"
|
||||
>
|
||||
<el-table-column prop="Name" label="菜单名称" width="200">
|
||||
<template #default="scope">
|
||||
@@ -124,10 +140,15 @@
|
||||
|
||||
<el-form-item label="菜单类型" prop="MenuType">
|
||||
<el-radio-group v-model="currentMenu.MenuType" style="width: 100%">
|
||||
<el-radio-button :label="1">普通菜单</el-radio-button>
|
||||
<el-radio-button :label="2">分组菜单</el-radio-button>
|
||||
<el-radio-button :label="3">按钮菜单</el-radio-button>
|
||||
<el-radio-button :value="1">页面菜单</el-radio-button>
|
||||
<el-radio-button :value="2">目录菜单</el-radio-button>
|
||||
<el-radio-button :value="3">权限按钮</el-radio-button>
|
||||
</el-radio-group>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary);">
|
||||
<div>• 页面菜单:有路由和组件地址,可访问完整功能页面</div>
|
||||
<div>• 目录菜单:只有路由地址,用于组织结构,无实际页面</div>
|
||||
<div>• 权限按钮:无路由和组件,仅用于权限控制</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
@@ -185,7 +206,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox, ElForm } from "element-plus";
|
||||
import { Plus, CirclePlus, Edit, Delete } from "@element-plus/icons-vue";
|
||||
import { Plus, CirclePlus, Edit, Delete, Refresh, FolderOpened, Folder } from "@element-plus/icons-vue";
|
||||
import { getAllMenus, updateMenuStatus, createMenu, updateMenu, deleteMenu } from "@/api/menu";
|
||||
|
||||
// 定义菜单数据类型(匹配后端返回的 Pascal 命名格式)
|
||||
@@ -210,9 +231,10 @@ interface Menu {
|
||||
|
||||
// 菜单树形数据
|
||||
const menuTree = ref<Menu[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// 默认展开的菜单ID
|
||||
const defaultExpandedKeys = ref<number[]>([]);
|
||||
// 表格引用
|
||||
const tableRef = ref<any>(null);
|
||||
|
||||
// 对话框状态
|
||||
const dialogVisible = ref(false);
|
||||
@@ -256,6 +278,7 @@ const parentMenuOptions = ref<Menu[]>([]);
|
||||
|
||||
// 获取所有菜单并构建树形结构
|
||||
const fetchMenus = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 使用 getAllMenus 接口获取菜单(接口返回后端格式,需映射为 Pascal 命名并补充默认字段)
|
||||
const result = await getAllMenus();
|
||||
@@ -268,12 +291,12 @@ const fetchMenus = async () => {
|
||||
ParentId: item.parentId,
|
||||
Icon: item.icon,
|
||||
Order: item.order,
|
||||
Status: 1, // 默认开启(后端返回的只查Status=1的)
|
||||
Status: 1,
|
||||
ComponentPath: item.componentPath || "",
|
||||
IsExternal: item.isExternal || 0,
|
||||
ExternalUrl: item.externalUrl || "",
|
||||
MenuType: 1, // 如果后端未返回MenuType,默认填1
|
||||
Permission: "", // 后端未返回时默认空
|
||||
MenuType: item.menuType,
|
||||
Permission: item.permission || "",
|
||||
CreateTime: "",
|
||||
UpdateTime: "",
|
||||
}));
|
||||
@@ -284,11 +307,6 @@ const fetchMenus = async () => {
|
||||
const tree = buildMenuTree(data);
|
||||
menuTree.value = tree;
|
||||
|
||||
// 设置默认展开顶级菜单(ParentId=0)
|
||||
defaultExpandedKeys.value = tree
|
||||
.filter((menu) => menu.ParentId === 0)
|
||||
.map((menu) => menu.Id);
|
||||
|
||||
// 构建父级菜单选项(添加顶级菜单选项)
|
||||
parentMenuOptions.value = [
|
||||
{
|
||||
@@ -304,9 +322,58 @@ const fetchMenus = async () => {
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("获取菜单数据失败: " + (error as Error).message);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新界面
|
||||
async function refresh() {
|
||||
loading.value = true;
|
||||
try {
|
||||
await fetchMenus();
|
||||
ElMessage.success('刷新成功');
|
||||
} catch (error) {
|
||||
ElMessage.error('刷新失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有菜单行数据(包括子节点)
|
||||
function getAllMenuRows(menuList: Menu[]): Menu[] {
|
||||
const rows: Menu[] = [];
|
||||
menuList.forEach((menu) => {
|
||||
rows.push(menu);
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
rows.push(...getAllMenuRows(menu.children));
|
||||
}
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
// 全部展开
|
||||
function expandAll() {
|
||||
if (!tableRef.value) return;
|
||||
const allRows = getAllMenuRows(menuTree.value);
|
||||
allRows.forEach((row) => {
|
||||
if (row.children && row.children.length > 0) {
|
||||
tableRef.value.toggleRowExpansion(row, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 全部折叠
|
||||
function collapseAll() {
|
||||
if (!tableRef.value) return;
|
||||
const allRows = getAllMenuRows(menuTree.value);
|
||||
allRows.forEach((row) => {
|
||||
if (row.children && row.children.length > 0) {
|
||||
tableRef.value.toggleRowExpansion(row, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 构建菜单树(处理父子关系)
|
||||
const buildMenuTree = (menuList: Menu[]): Menu[] => {
|
||||
const menuMap = new Map<number, Menu>();
|
||||
@@ -339,7 +406,7 @@ const buildMenuTree = (menuList: Menu[]): Menu[] => {
|
||||
|
||||
// 获取菜单类型名称
|
||||
const getMenuTypeName = (type: number) => {
|
||||
const typeMap = { 1: "普通菜单", 2: "分组菜单", 3: "按钮菜单" };
|
||||
const typeMap = { 1: "页面菜单", 2: "目录菜单", 3: "权限按钮" };
|
||||
return typeMap[type as keyof typeof typeMap] || "未知类型";
|
||||
};
|
||||
|
||||
@@ -485,6 +552,24 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.header-bar h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
title="角色详情"
|
||||
v-model="visible"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="role-detail" v-loading="loading">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="角色ID">
|
||||
{{ roleData.roleId }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="角色代码">
|
||||
<el-tag>{{ roleData.roleCode }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="角色名称">
|
||||
{{ roleData.roleName }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="roleData.status === 1 ? 'success' : 'info'">
|
||||
{{ roleData.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="排序序号">
|
||||
{{ roleData.sortOrder }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人">
|
||||
{{ roleData.createBy || '系统' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ formatDate(roleData.createTime) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">
|
||||
{{ formatDate(roleData.updateTime) || '未更新' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="角色描述" :span="2">
|
||||
{{ roleData.description || '无' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getRoleById } from '@/api/role';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
roleId?: number | null;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
roleId: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
}>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const roleData = ref<any>({});
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return '';
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
} catch {
|
||||
return dateStr as string;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取角色详情
|
||||
async function fetchRoleDetail() {
|
||||
if (!props.roleId) {
|
||||
roleData.value = {};
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getRoleById(props.roleId);
|
||||
if (res.code === 0 && res.data) {
|
||||
roleData.value = res.data;
|
||||
} else {
|
||||
ElMessage.error(res.message || '获取角色详情失败');
|
||||
roleData.value = {};
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '获取角色详情失败');
|
||||
roleData.value = {};
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 roleId 变化
|
||||
watch(
|
||||
() => props.roleId,
|
||||
(newId) => {
|
||||
if (newId && visible.value) {
|
||||
fetchRoleDetail();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听对话框打开状态
|
||||
watch(
|
||||
() => visible.value,
|
||||
(newVal) => {
|
||||
if (newVal && props.roleId) {
|
||||
fetchRoleDetail();
|
||||
} else {
|
||||
roleData.value = {};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 关闭对话框
|
||||
function handleClose() {
|
||||
visible.value = false;
|
||||
roleData.value = {};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.role-detail {
|
||||
min-height: 200px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:title="isEditing ? '编辑角色' : '添加角色'"
|
||||
v-model="visible"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
:model="roleForm"
|
||||
:rules="formRules"
|
||||
ref="roleFormRef"
|
||||
label-width="90px"
|
||||
>
|
||||
<el-form-item label="角色代码" prop="roleCode">
|
||||
<el-input
|
||||
v-model="roleForm.roleCode"
|
||||
placeholder="请输入角色代码(如:admin)"
|
||||
:disabled="isEditing && isSystemAdmin"
|
||||
/>
|
||||
<div class="form-tip">只能包含字母、数字或下划线</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色名称" prop="roleName">
|
||||
<el-input
|
||||
v-model="roleForm.roleName"
|
||||
placeholder="请输入角色名称"
|
||||
:disabled="isEditing && isSystemAdmin"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色描述" prop="description">
|
||||
<el-input
|
||||
v-model="roleForm.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入角色描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="roleForm.status" :disabled="isEditing && isSystemAdmin">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序序号" prop="sortOrder">
|
||||
<el-input-number
|
||||
v-model="roleForm.sortOrder"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
placeholder="数字越小越靠前"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed } from 'vue';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { createRole, updateRole } from '@/api/role';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
role?: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
role: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
});
|
||||
|
||||
const submitting = ref(false);
|
||||
const roleFormRef = ref<FormInstance>();
|
||||
|
||||
// 判断是否为编辑模式
|
||||
const isEditing = computed(() => {
|
||||
return !!(props.role && props.role.roleId);
|
||||
});
|
||||
|
||||
// 判断是否为系统管理员角色(system_admin)
|
||||
const isSystemAdmin = computed(() => {
|
||||
if (!props.role) return false;
|
||||
return props.role.roleCode === 'system_admin';
|
||||
});
|
||||
|
||||
// 表单数据
|
||||
const roleForm = reactive({
|
||||
roleId: null as number | null,
|
||||
roleCode: '',
|
||||
roleName: '',
|
||||
description: '',
|
||||
status: 1,
|
||||
sortOrder: 0,
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const formRules: FormRules = {
|
||||
roleCode: [
|
||||
{ required: true, message: '请输入角色代码', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_]+$/,
|
||||
message: '只能包含字母、数字或下划线',
|
||||
trigger: 'blur',
|
||||
},
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
roleName: [
|
||||
{ required: true, message: '请输入角色名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
description: [
|
||||
{ max: 500, message: '描述长度不能超过 500 个字符', trigger: 'blur' },
|
||||
],
|
||||
sortOrder: [
|
||||
{ required: true, message: '请输入排序序号', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '排序序号必须大于等于 0', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
// 监听 role 变化,填充表单数据
|
||||
watch(
|
||||
() => props.role,
|
||||
(newRole) => {
|
||||
if (newRole) {
|
||||
roleForm.roleId = newRole.roleId || null;
|
||||
roleForm.roleCode = newRole.roleCode || '';
|
||||
roleForm.roleName = newRole.roleName || '';
|
||||
roleForm.description = newRole.description || '';
|
||||
roleForm.status = newRole.status !== undefined ? newRole.status : 1;
|
||||
roleForm.sortOrder = newRole.sortOrder || 0;
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
roleForm.roleId = null;
|
||||
roleForm.roleCode = '';
|
||||
roleForm.roleName = '';
|
||||
roleForm.description = '';
|
||||
roleForm.status = 1;
|
||||
roleForm.sortOrder = 0;
|
||||
roleFormRef.value?.clearValidate();
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
function handleClose() {
|
||||
visible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
if (!roleFormRef.value) return;
|
||||
|
||||
try {
|
||||
await roleFormRef.value.validate();
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
roleCode: roleForm.roleCode,
|
||||
roleName: roleForm.roleName,
|
||||
description: roleForm.description || '',
|
||||
status: roleForm.status,
|
||||
sortOrder: roleForm.sortOrder,
|
||||
};
|
||||
|
||||
if (isEditing.value) {
|
||||
await updateRole(roleForm.roleId!, submitData);
|
||||
ElMessage.success('角色更新成功');
|
||||
} else {
|
||||
await createRole(submitData);
|
||||
ElMessage.success('角色添加成功');
|
||||
}
|
||||
|
||||
emit('success');
|
||||
handleClose();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '操作失败,请重试');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
|
||||
+173
-188
@@ -3,215 +3,241 @@
|
||||
<div class="header-bar">
|
||||
<h2>角色管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="showRoleDialog = true">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加角色
|
||||
</el-button>
|
||||
<el-button @click="refresh">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>正在加载角色数据...</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="error" class="error-state">
|
||||
<div v-if="error" class="error-state">
|
||||
<el-alert title="加载失败" :message="error" type="error" show-icon />
|
||||
<el-button type="primary" @click="fetchRoles">重试</el-button>
|
||||
<el-button type="primary" @click="refresh">重试</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 角色列表 -->
|
||||
<div v-else>
|
||||
<el-table :data="roles" stripe style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="label" label="角色名称" align="center" />
|
||||
<el-table-column prop="value" label="角色标识" align="center" />
|
||||
<el-table-column prop="remark" label="备注" align="center" />
|
||||
<el-table-column label="操作" width="180" align="center">
|
||||
<el-table-column prop="roleId" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="roleName" label="角色名称" min-width="150" align="center" />
|
||||
<el-table-column prop="roleCode" label="角色代码" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)"
|
||||
>删除</el-button
|
||||
<el-tag size="small">{{ row.roleCode }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="角色描述" min-width="200" align="center" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sortOrder" label="排序" width="80" align="center" />
|
||||
<el-table-column label="创建时间" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>
|
||||
详情
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.roleCode !== 'system_admin' && row.roleCode !== 'admin' && row.roleCode !== 'user'"
|
||||
size="small"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.roleCode !== 'system_admin' && row.roleCode !== 'admin' && row.roleCode !== 'user'"
|
||||
size="small"
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
background
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
@current-change="handlePageChange"
|
||||
layout="total, prev, pager, next"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑角色弹窗 -->
|
||||
<el-dialog
|
||||
:title="isEditing ? '编辑角色' : '添加角色'"
|
||||
v-model="showRoleDialog"
|
||||
width="420px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
:model="roleForm"
|
||||
:rules="formRules"
|
||||
ref="roleFormRef"
|
||||
label-width="90px"
|
||||
>
|
||||
<el-form-item label="角色名称" prop="label">
|
||||
<el-input
|
||||
v-model="roleForm.label"
|
||||
:disabled="isEditing && roleForm.value === 'admin'"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色标识" prop="value">
|
||||
<el-input
|
||||
v-model="roleForm.value"
|
||||
:disabled="isEditing && roleForm.value === 'admin'"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="roleForm.remark" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showRoleDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitRoleForm"> 保存 </el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 编辑对话框 -->
|
||||
<RoleEditDialog
|
||||
v-model="editDialogVisible"
|
||||
:role="currentRole"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<RoleDetailDialog
|
||||
v-model="detailDialogVisible"
|
||||
:roleId="currentRoleId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ref, onMounted } from "vue";
|
||||
import {
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
type FormInstance,
|
||||
type FormRules,
|
||||
} from "element-plus";
|
||||
import { Plus, View, Edit, Delete, Refresh } from "@element-plus/icons-vue";
|
||||
import { getRoleByTenantId, deleteRole } from "@/api/role";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import RoleEditDialog from "./components/edit.vue";
|
||||
import RoleDetailDialog from "./components/detail.vue";
|
||||
|
||||
const roles = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
|
||||
const showRoleDialog = ref(false);
|
||||
const isEditing = ref(false);
|
||||
const roleForm = reactive({
|
||||
id: null,
|
||||
label: "",
|
||||
value: "",
|
||||
remark: "",
|
||||
});
|
||||
const roleFormRef = ref<FormInstance>();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const formRules: FormRules = {
|
||||
label: [
|
||||
{ required: true, message: "请输入角色名称", trigger: "blur" },
|
||||
{ min: 2, max: 24, message: "名称长度2-24字符", trigger: "blur" },
|
||||
],
|
||||
value: [
|
||||
{ required: true, message: "请输入角色标识", trigger: "blur" },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_]+$/,
|
||||
message: "只能包含字母、数字或下划线",
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
const editDialogVisible = ref(false);
|
||||
const detailDialogVisible = ref(false);
|
||||
const currentRole = ref<any>(null);
|
||||
const currentRoleId = ref<number | null>(null);
|
||||
|
||||
// 获取当前登录用户的租户ID(tenant_id)
|
||||
const getCurrentTenantId = () => {
|
||||
// 方式1: 从 authStore 获取
|
||||
if (authStore.user && authStore.user.tenant_id) {
|
||||
return authStore.user.tenant_id;
|
||||
}
|
||||
|
||||
// 方式2: 从 localStorage 获取(兼容)
|
||||
const userInfo = localStorage.getItem('userInfo');
|
||||
if (userInfo) {
|
||||
try {
|
||||
const user = JSON.parse(userInfo);
|
||||
return user.tenant_id || user.tenantId || null;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse user info:', e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// 判断是否为系统管理员角色(不允许删除)
|
||||
function isSystemAdmin(row: any): boolean {
|
||||
return row.roleCode === 'system_admin';
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return '';
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
} catch {
|
||||
return dateStr as string;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取角色列表
|
||||
async function fetchRoles() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
// TODO: 替换为真实API
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
// 假数据
|
||||
const all = [
|
||||
{ id: 1, label: "系统管理员", value: "admin", remark: "拥有全部权限" },
|
||||
{ id: 2, label: "普通用户", value: "user", remark: "普通访问权限" },
|
||||
{ id: 3, label: "运营", value: "ops", remark: "运营相关权限" },
|
||||
// ...更多
|
||||
];
|
||||
total.value = all.length;
|
||||
roles.value = all.slice(
|
||||
(page.value - 1) * pageSize.value,
|
||||
page.value * pageSize.value
|
||||
);
|
||||
const tenantId = getCurrentTenantId();
|
||||
const res = await getRoleByTenantId(tenantId);
|
||||
if (res.code === 0 && res.data) {
|
||||
roles.value = Array.isArray(res.data) ? res.data : [];
|
||||
} else {
|
||||
error.value = res.message || "获取角色列表失败";
|
||||
roles.value = [];
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || "获取角色列表失败";
|
||||
roles.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const handlePageChange = (val: number) => {
|
||||
page.value = val;
|
||||
fetchRoles();
|
||||
};
|
||||
|
||||
function resetRoleForm() {
|
||||
roleForm.id = null;
|
||||
roleForm.label = "";
|
||||
roleForm.value = "";
|
||||
roleForm.remark = "";
|
||||
// 刷新界面
|
||||
async function refresh() {
|
||||
loading.value = true;
|
||||
try {
|
||||
await fetchRoles();
|
||||
ElMessage.success('刷新成功');
|
||||
} catch (error) {
|
||||
ElMessage.error('刷新失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加角色
|
||||
function handleAdd() {
|
||||
currentRole.value = null;
|
||||
editDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
function handleView(row: any) {
|
||||
currentRoleId.value = row.roleId;
|
||||
detailDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 编辑角色
|
||||
function handleEdit(row: any) {
|
||||
isEditing.value = true;
|
||||
roleForm.id = row.id;
|
||||
roleForm.label = row.label;
|
||||
roleForm.value = row.value;
|
||||
roleForm.remark = row.remark;
|
||||
showRoleDialog.value = true;
|
||||
currentRole.value = { ...row };
|
||||
editDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 删除角色
|
||||
async function handleDelete(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除角色「${row.label}」? 删除后不可恢复。`,
|
||||
`确定要删除角色「${row.roleName}」吗?删除后不可恢复。`,
|
||||
"警告",
|
||||
{ type: "warning" }
|
||||
);
|
||||
// TODO: 替换为真实API
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
ElMessage.success("删除成功");
|
||||
fetchRoles();
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await deleteRole(row.roleId);
|
||||
if (res.code === 0) {
|
||||
ElMessage.success("删除成功");
|
||||
fetchRoles();
|
||||
} else {
|
||||
ElMessage.error(res.message || "删除失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || "删除失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} catch {
|
||||
// 取消删除
|
||||
}
|
||||
}
|
||||
|
||||
async function submitRoleForm() {
|
||||
// 表单校验
|
||||
await roleFormRef.value?.validate();
|
||||
loading.value = true;
|
||||
try {
|
||||
if (isEditing.value) {
|
||||
// TODO: 替换为真实API更新
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
ElMessage.success("角色更新成功");
|
||||
} else {
|
||||
// TODO: 替换为真实API新增
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
ElMessage.success("角色添加成功");
|
||||
}
|
||||
showRoleDialog.value = false;
|
||||
fetchRoles();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || "操作失败,请重试");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
// 编辑成功回调
|
||||
function handleEditSuccess() {
|
||||
fetchRoles();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -220,7 +246,6 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loading-state,
|
||||
.error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -228,48 +253,8 @@ onMounted(() => {
|
||||
justify-content: center;
|
||||
min-height: 220px;
|
||||
padding: 32px 0 16px 0;
|
||||
background: #f5f7fa;
|
||||
background: var(--el-bg-color-page);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 4px solid #e5e5e5;
|
||||
border-top: 4px solid #409eff;
|
||||
border-radius: 50%;
|
||||
animation: loading-spin 0.8s linear infinite;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
@keyframes loading-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding-top: 18px !important;
|
||||
}
|
||||
|
||||
.el-form .el-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.el-form .el-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.role-management-module {
|
||||
padding: 10px 4px;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
row-gap: 8px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">租户管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button @click="fetchTenants" title="刷新租户列表" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加租户
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="error" class="error-state">
|
||||
<el-alert title="加载失败" :message="error" type="error" show-icon />
|
||||
<el-button type="primary" @click="fetchTenants" style="margin-top: 16px">
|
||||
<el-button type="primary" @click="refresh" style="margin-top: 16px">
|
||||
重试
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -37,6 +37,7 @@
|
||||
stripe
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
element-loading-text="正在加载..."
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="name" label="租户名称" min-width="120" />
|
||||
@@ -203,6 +204,16 @@ const handlePageChange = (val: number) => {
|
||||
fetchTenants();
|
||||
};
|
||||
|
||||
// 刷新界面
|
||||
async function refresh() {
|
||||
try {
|
||||
await fetchTenants();
|
||||
ElMessage.success('刷新成功');
|
||||
} catch (error) {
|
||||
ElMessage.error('刷新失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 审核状态相关方法
|
||||
function getAuditStatusType(
|
||||
status: string
|
||||
|
||||
@@ -7,12 +7,16 @@
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加用户
|
||||
</el-button>
|
||||
<el-button @click="refresh">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<el-table :data="users" style="width: 100%">
|
||||
<el-table :data="users" style="width: 100%" v-loading="loading">
|
||||
<el-table-column
|
||||
prop="username"
|
||||
label="用户名"
|
||||
@@ -31,10 +35,10 @@
|
||||
align="center"
|
||||
min-width="200"
|
||||
/>
|
||||
<el-table-column prop="role" label="角色" width="120" align="center">
|
||||
<el-table-column prop="role" label="角色" width="150" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.role === 'admin' ? 'danger' : 'primary'">
|
||||
{{ scope.row.role === "admin" ? "管理员" : "普通用户" }}
|
||||
<el-tag :type="getRoleTagType(scope.row.roleName)">
|
||||
{{ scope.row.roleName || '未分配角色' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -123,9 +127,23 @@
|
||||
<el-input v-model="form.email" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" v-if="dialogTitle !== '修改密码'">
|
||||
<el-select v-model="form.role">
|
||||
<el-option label="管理员" value="admin" />
|
||||
<el-option label="普通用户" value="user" />
|
||||
<el-select
|
||||
v-model="form.roleId"
|
||||
placeholder="请选择角色"
|
||||
style="width: 100%"
|
||||
:loading="loadingRoles"
|
||||
>
|
||||
<el-option
|
||||
v-for="role in roleList"
|
||||
:key="role.roleId"
|
||||
:label="role.roleName"
|
||||
:value="role.roleId"
|
||||
>
|
||||
<span>{{ role.roleName }}</span>
|
||||
<span style="color: #8492a6; font-size: 13px; margin-left: 8px;">
|
||||
({{ role.roleCode }})
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" v-if="dialogTitle !== '修改密码'">
|
||||
@@ -146,7 +164,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
import { Plus, Refresh } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getAllUsers,
|
||||
addUser,
|
||||
@@ -155,6 +173,8 @@ import {
|
||||
getUserInfo,
|
||||
changePassword,
|
||||
} from "@/api/user";
|
||||
import { getRoleByTenantId } from "@/api/role";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
@@ -167,11 +187,60 @@ interface User {
|
||||
tenant_id: number;
|
||||
}
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
|
||||
const users = ref<any[]>([]);
|
||||
const roleList = ref<any[]>([]);
|
||||
const loadingRoles = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
// 获取当前登录用户的租户ID
|
||||
const getCurrentTenantId = () => {
|
||||
if (authStore.user && authStore.user.tenant_id) {
|
||||
return authStore.user.tenant_id;
|
||||
}
|
||||
const userInfo = localStorage.getItem('userInfo');
|
||||
if (userInfo) {
|
||||
try {
|
||||
const user = JSON.parse(userInfo);
|
||||
return user.tenant_id || user.tenantId || 0;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse user info:', e);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// 获取角色列表
|
||||
const fetchRoles = async () => {
|
||||
loadingRoles.value = true;
|
||||
try {
|
||||
const tenantId = getCurrentTenantId();
|
||||
const res = await getRoleByTenantId(tenantId);
|
||||
if (res.code === 0 && res.data) {
|
||||
roleList.value = Array.isArray(res.data) ? res.data : [];
|
||||
} else {
|
||||
roleList.value = [];
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取角色列表失败:', error);
|
||||
roleList.value = [];
|
||||
} finally {
|
||||
loadingRoles.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取角色标签类型
|
||||
const getRoleTagType = (roleName: string) => {
|
||||
if (!roleName) return 'info';
|
||||
if (roleName.includes('管理员')) return 'danger';
|
||||
if (roleName.includes('用户')) return 'primary';
|
||||
return 'success';
|
||||
};
|
||||
|
||||
//校验密码
|
||||
const validatePassword = (password: string) => {
|
||||
@@ -199,6 +268,7 @@ const validateConfirmPassword = (password: string, confirmPassword: string) => {
|
||||
};
|
||||
|
||||
const fetchUsers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getAllUsers();
|
||||
// 兼容接口返回的数据结构
|
||||
@@ -213,33 +283,53 @@ const fetchUsers = async () => {
|
||||
userList = res.data;
|
||||
}
|
||||
// 映射接口字段到表格所需结构
|
||||
users.value = userList.map((item: any) => ({
|
||||
id: item.id,
|
||||
username: item.username,
|
||||
nickname: item.nickname,
|
||||
email: item.email,
|
||||
role: item.role || (item.username === "admin" ? "admin" : "user"),
|
||||
status: item.status || "active",
|
||||
lastLoginTime: item.lastLoginTime
|
||||
? new Date(item.lastLoginTime).toLocaleString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
: "",
|
||||
}));
|
||||
users.value = userList.map((item: any) => {
|
||||
// 查找角色名称
|
||||
let roleName = '';
|
||||
let roleId = item.role_id || item.roleId || null;
|
||||
if (roleId) {
|
||||
const role = roleList.value.find(r => r.roleId === roleId);
|
||||
roleName = role ? role.roleName : '';
|
||||
} else if (item.role) {
|
||||
// 兼容旧的role字段,尝试从roleList中查找
|
||||
const role = roleList.value.find(r => r.roleCode === item.role);
|
||||
roleName = role ? role.roleName : (item.role === 'admin' ? '管理员' : '普通用户');
|
||||
}
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
username: item.username,
|
||||
nickname: item.nickname,
|
||||
email: item.email,
|
||||
role: item.role || '',
|
||||
roleId: roleId,
|
||||
roleName: roleName,
|
||||
status: item.status || "active",
|
||||
lastLoginTime: item.lastLoginTime
|
||||
? new Date(item.lastLoginTime).toLocaleString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
: "",
|
||||
};
|
||||
});
|
||||
total.value = users.value.length;
|
||||
} catch (e) {
|
||||
users.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
// 先加载角色列表,再加载用户列表
|
||||
await fetchRoles();
|
||||
fetchUsers();
|
||||
});
|
||||
|
||||
@@ -258,8 +348,10 @@ const form = ref<any>({
|
||||
nickname: "",
|
||||
password: "",
|
||||
email: "",
|
||||
role: "user",
|
||||
roleId: null,
|
||||
role: "",
|
||||
status: "active",
|
||||
tenant_id: null,
|
||||
});
|
||||
const passwordForm = ref<any>({
|
||||
oldPassword: "",
|
||||
@@ -274,6 +366,20 @@ const currentForm = computed(() => {
|
||||
return dialogTitle.value === '修改密码' ? passwordForm.value : form.value;
|
||||
});
|
||||
|
||||
//刷新界面
|
||||
const refresh = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await fetchRoles();
|
||||
await fetchUsers();
|
||||
ElMessage.success('刷新成功');
|
||||
} catch (error) {
|
||||
ElMessage.error('刷新失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddUser = () => {
|
||||
dialogTitle.value = "添加用户";
|
||||
isEdit.value = false;
|
||||
@@ -296,7 +402,8 @@ const handleAddUser = () => {
|
||||
nickname: "",
|
||||
password: "",
|
||||
email: "",
|
||||
role: "user",
|
||||
roleId: null,
|
||||
role: "",
|
||||
status: "active",
|
||||
tenant_id: tenantId,
|
||||
};
|
||||
@@ -309,14 +416,28 @@ const handleEdit = async (user: User) => {
|
||||
try {
|
||||
const res = await getUserInfo(user.id);
|
||||
const data = res.data || res;
|
||||
|
||||
// 获取当前租户ID
|
||||
const tenantId = getCurrentTenantId();
|
||||
|
||||
// 处理角色ID,兼容旧数据
|
||||
let roleId = data.role_id || data.roleId || null;
|
||||
if (!roleId && data.role) {
|
||||
// 如果只有role代码,尝试从roleList中查找对应的roleId
|
||||
const role = roleList.value.find(r => r.roleCode === data.role);
|
||||
roleId = role ? role.roleId : null;
|
||||
}
|
||||
|
||||
form.value = {
|
||||
id: data.id,
|
||||
username: data.username,
|
||||
nickname: data.nickname,
|
||||
password: "",
|
||||
email: data.email,
|
||||
role: data.role || "user",
|
||||
roleId: roleId,
|
||||
role: data.role || "",
|
||||
status: data.status || "active",
|
||||
tenant_id: data.tenant_id || tenantId,
|
||||
};
|
||||
} catch (e) {
|
||||
ElMessage.error("加载用户失败");
|
||||
@@ -381,7 +502,26 @@ const submitForm = async () => {
|
||||
passwordError.value = res.message || "密码修改失败";
|
||||
}
|
||||
} else if (isEdit.value) {
|
||||
await editUser(form.value.id, form.value);
|
||||
// 构建提交数据,优先使用roleId
|
||||
const submitData: any = {
|
||||
username: form.value.username,
|
||||
nickname: form.value.nickname,
|
||||
email: form.value.email,
|
||||
status: form.value.status,
|
||||
};
|
||||
|
||||
if (form.value.roleId) {
|
||||
submitData.roleId = form.value.roleId;
|
||||
} else if (form.value.role) {
|
||||
// 兼容旧数据,如果没有roleId但有role代码,也传递
|
||||
submitData.role = form.value.role;
|
||||
}
|
||||
|
||||
if (form.value.tenant_id) {
|
||||
submitData.tenant_id = form.value.tenant_id;
|
||||
}
|
||||
|
||||
await editUser(form.value.id, submitData);
|
||||
ElMessage.success({
|
||||
message: "更新成功",
|
||||
type: "success",
|
||||
@@ -393,7 +533,26 @@ const submitForm = async () => {
|
||||
dialogVisible.value = false;
|
||||
fetchUsers();
|
||||
} else {
|
||||
await addUser(form.value);
|
||||
// 构建提交数据,优先使用roleId
|
||||
const submitData: any = {
|
||||
username: form.value.username,
|
||||
nickname: form.value.nickname,
|
||||
password: form.value.password,
|
||||
email: form.value.email,
|
||||
status: form.value.status,
|
||||
};
|
||||
|
||||
if (form.value.roleId) {
|
||||
submitData.roleId = form.value.roleId;
|
||||
} else if (form.value.role) {
|
||||
submitData.role = form.value.role;
|
||||
}
|
||||
|
||||
if (form.value.tenant_id) {
|
||||
submitData.tenant_id = form.value.tenant_id;
|
||||
}
|
||||
|
||||
await addUser(submitData);
|
||||
ElMessage.success({
|
||||
message: "添加成功",
|
||||
type: "success",
|
||||
|
||||
Reference in New Issue
Block a user