批量更新

This commit is contained in:
2025-11-02 23:53:41 +08:00
parent 7972f0a6d9
commit 9a6c29f3b0
29 changed files with 1947 additions and 899 deletions
+12 -11
View File
@@ -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 文件数据
+46
View File
@@ -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'
})
}
+9 -1
View File
@@ -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;
}
}
+8
View File
@@ -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
View File
@@ -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) {
+44
View File
@@ -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
View File
@@ -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>
+11 -1
View File
@@ -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 = '添加分类';
//
+104 -32
View File
@@ -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;
+19 -5
View File
@@ -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
// categorytagedit 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`);
}
+11 -1
View File
@@ -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();
+22 -23
View File
@@ -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(() => {
+105 -20
View File
@@ -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, // MenuType1
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
View File
@@ -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);
// IDtenant_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>
+16 -5
View File
@@ -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
+192 -33
View File
@@ -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) {
// roleroleList
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) {
// roleroleListroleId
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) {
// roleIdrole
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",