批量更新
This commit is contained in:
parent
7972f0a6d9
commit
9a6c29f3b0
@ -1,172 +0,0 @@
|
||||
# 编辑器图片上传问题修复
|
||||
|
||||
## 问题描述
|
||||
编辑器里上传的图片无法显示。
|
||||
|
||||
## 根本原因
|
||||
文件保存路径和静态文件URL映射不匹配。
|
||||
|
||||
## 已完成的修改
|
||||
|
||||
### 1. 后端文件保存路径 (`server/controllers/file.go`)
|
||||
|
||||
**修改前:**
|
||||
```go
|
||||
uploadDir := path.Join("uploads", datePath)
|
||||
```
|
||||
文件保存到:`server/uploads/`
|
||||
|
||||
**修改后:**
|
||||
```go
|
||||
uploadDir := path.Join("..", "uploads", datePath)
|
||||
```
|
||||
文件保存到:`项目根目录/uploads/`
|
||||
|
||||
### 2. 静态文件映射 (`server/conf/app.conf`)
|
||||
|
||||
**修改前:**
|
||||
```conf
|
||||
StaticDir = /uploads:uploads
|
||||
```
|
||||
|
||||
**修改后:**
|
||||
```conf
|
||||
StaticDir = /uploads:../uploads
|
||||
```
|
||||
|
||||
现在 `/uploads` URL 正确映射到项目根目录的 `uploads` 文件夹。
|
||||
|
||||
### 3. 前端URL拼接 (`front/src/components/WangEditor.vue`)
|
||||
|
||||
添加了更健壮的URL拼接逻辑:
|
||||
|
||||
```typescript
|
||||
const fileUrl = response.data.data.file_url; // /uploads/2024/01/15/xxx.jpg
|
||||
const baseUrl = getUploadUrl() ;
|
||||
|
||||
let fullUrl = fileUrl;
|
||||
if (!fileUrl.startsWith('http')) {
|
||||
const base = baseUrl.replace(/\/$/, '');
|
||||
const url = fileUrl.startsWith('/') ? fileUrl : '/' + fileUrl;
|
||||
fullUrl = `${base}${url}`;
|
||||
}
|
||||
```
|
||||
|
||||
这样可以确保URL正确拼接为:`http://localhost:8080/uploads/2024/01/15/xxx.jpg`
|
||||
|
||||
## 目录结构
|
||||
|
||||
上传后文件将保存在:
|
||||
|
||||
```
|
||||
yunzer_go/
|
||||
├── server/
|
||||
├── front/
|
||||
└── uploads/ ← 文件保存位置
|
||||
├── 2024/
|
||||
│ └── 01/
|
||||
│ └── 15/
|
||||
│ └── 20240115143045_example.jpg
|
||||
```
|
||||
|
||||
## 访问URL
|
||||
|
||||
上传后可以通过以下URL访问:
|
||||
|
||||
```
|
||||
http://localhost:8080/uploads/2024/01/15/20240115143045_example.jpg
|
||||
```
|
||||
|
||||
## 如何验证
|
||||
|
||||
### 1. 上传一个图片
|
||||
在编辑器中使用图片上传功能上传一张图片。
|
||||
|
||||
### 2. 查看控制台日志
|
||||
打开浏览器开发者工具(F12),查看 Console 标签,应该看到:
|
||||
```
|
||||
图片上传成功,URL: http://localhost:8080/uploads/2024/01/15/xxx.jpg
|
||||
```
|
||||
|
||||
### 3. 检查文件是否保存
|
||||
查看项目根目录的 `uploads` 文件夹:
|
||||
```bash
|
||||
ls uploads/2024/01/15/
|
||||
```
|
||||
|
||||
### 4. 测试URL访问
|
||||
在浏览器中直接访问图片URL,应该能看到图片。
|
||||
|
||||
### 5. 检查数据库
|
||||
查看 `yz_files` 表中的记录:
|
||||
```sql
|
||||
SELECT file_path, file_url FROM yz_files ORDER BY upload_time DESC LIMIT 1;
|
||||
```
|
||||
|
||||
应该看到:
|
||||
- `file_path`: `uploads/2024/01/15/xxx.jpg`
|
||||
- `file_url`: `/uploads/2024/01/15/xxx.jpg`
|
||||
|
||||
## 重启服务器
|
||||
|
||||
修改配置后需要重启服务器才能生效:
|
||||
|
||||
```bash
|
||||
cd server
|
||||
go run main.go
|
||||
```
|
||||
|
||||
或者如果使用编译后的可执行文件:
|
||||
|
||||
```bash
|
||||
./server.exe
|
||||
```
|
||||
|
||||
## 如果还是看不到图片
|
||||
|
||||
### 检查清单
|
||||
|
||||
1. ✅ 服务器是否重启
|
||||
2. ✅ 文件是否保存到 `uploads` 目录
|
||||
3. ✅ 浏览器控制台是否有错误
|
||||
4. ✅ URL是否正确拼接
|
||||
5. ✅ 静态文件映射是否正确
|
||||
|
||||
### 常见问题
|
||||
|
||||
**Q: 上传后文件保存在哪里?**
|
||||
A: 项目根目录的 `uploads` 文件夹
|
||||
|
||||
**Q: 图片URL是什么格式?**
|
||||
A: `http://localhost:8080/uploads/2024/01/15/filename.jpg`
|
||||
|
||||
**Q: 为什么看不到图片?**
|
||||
A: 检查:
|
||||
- 文件是否正确保存
|
||||
- URL是否可访问
|
||||
- 浏览器控制台错误信息
|
||||
- 服务器是否重启
|
||||
|
||||
**Q: 如何自定义上传目录?**
|
||||
A: 修改 `server/controllers/file.go` 和 `server/conf/app.conf` 中的路径配置
|
||||
|
||||
## 调试建议
|
||||
|
||||
如果图片仍不显示,检查以下内容:
|
||||
|
||||
1. **查看浏览器网络请求**
|
||||
- F12 → Network 标签
|
||||
- 查看图片请求是否返回 200
|
||||
- 如果返回 404,说明路径不对
|
||||
|
||||
2. **查看浏览器控制台**
|
||||
- 查看是否有 CORS 错误
|
||||
- 查看上传成功后的 URL 是什么
|
||||
|
||||
3. **查看服务器日志**
|
||||
- 确认文件是否正确上传
|
||||
- 确认路径是否正确
|
||||
|
||||
4. **检查文件权限**
|
||||
- 确保应用有创建目录和写入文件的权限
|
||||
|
||||
@ -1,94 +0,0 @@
|
||||
# 知识库功能快速设置指南
|
||||
|
||||
## 问题原因
|
||||
|
||||
500 错误通常是因为数据库表没有创建导致的。
|
||||
|
||||
## 解决步骤
|
||||
|
||||
### 1. 创建数据库表
|
||||
|
||||
打开 MySQL 客户端,连接到您的数据库,执行:
|
||||
|
||||
```bash
|
||||
# 方法1:命令行执行
|
||||
mysql -u root -p your_database < server/database/create_knowledge_tables.sql
|
||||
|
||||
# 方法2:在 MySQL 客户端中
|
||||
source server/database/create_knowledge_tables.sql;
|
||||
```
|
||||
|
||||
### 2. 验证表创建成功
|
||||
|
||||
```sql
|
||||
SHOW TABLES LIKE 'yz_knowledge%';
|
||||
```
|
||||
|
||||
应该看到以下表:
|
||||
- yz_knowledge
|
||||
- yz_knowledge_category
|
||||
- yz_knowledge_tags
|
||||
|
||||
### 3. 重启后端服务
|
||||
|
||||
```bash
|
||||
cd server
|
||||
go run main.go
|
||||
```
|
||||
|
||||
### 4. 测试 API
|
||||
|
||||
浏览器访问:http://localhost:8080/api/knowledge/list
|
||||
|
||||
应该返回:
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"list": [],
|
||||
"total": 0,
|
||||
"page": 1,
|
||||
"pageSize": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 已完成的修改
|
||||
|
||||
### 后端(Go)
|
||||
- ✅ 创建 `server/models/knowledge.go` - 知识库模型
|
||||
- ✅ 创建 `server/controllers/knowledge.go` - 知识库控制器
|
||||
- ✅ 创建 `server/database/yz_knowledge.sql` - 数据库表结构
|
||||
- ✅ 更新 `server/routers/router.go` - 添加路由
|
||||
- ✅ 更新 `server/models/user.go` - 注册模型
|
||||
- ✅ 修复模型字段映射问题
|
||||
|
||||
### 前端(TypeScript)
|
||||
- ✅ 更新 `front/src/api/knowledge.ts` - API 调用
|
||||
- ✅ 更新 `front/src/views/apps/knowledge/index.vue` - 列表页面
|
||||
- ✅ 更新 `front/src/views/apps/knowledge/components/edit.vue` - 编辑页面
|
||||
- ✅ 更新 `front/src/views/apps/knowledge/components/detail.vue` - 详情页面
|
||||
- ✅ 创建 `front/src/components/WangEditor.vue` - 富文本编辑器组件
|
||||
- ✅ 更新 `front/src/main.ts` - 全局注册组件
|
||||
- ✅ 更新 `front/src/router/index.ts` - 路由配置
|
||||
|
||||
## API 端点
|
||||
|
||||
- `GET /api/knowledge/list` - 获取列表
|
||||
- `GET /api/knowledge/detail?id=xxx` - 获取详情
|
||||
- `POST /api/knowledge/create` - 创建知识
|
||||
- `POST /api/knowledge/update` - 更新知识
|
||||
- `POST /api/knowledge/delete` - 删除知识
|
||||
- `GET /api/knowledge/categories` - 获取分类
|
||||
- `GET /api/knowledge/tags` - 获取标签
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 富文本编辑器(wangEditor)
|
||||
- ✅ 实时预览
|
||||
- ✅ 分类和标签管理
|
||||
- ✅ 搜索和分页
|
||||
- ✅ 主题支持
|
||||
- ✅ 响应式布局
|
||||
|
||||
@ -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
pc/src/api/role.js
Normal file
46
pc/src/api/role.js
Normal 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'
|
||||
})
|
||||
}
|
||||
@ -41,3 +41,11 @@ body {
|
||||
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 = [];
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
152
pc/src/views/system/roles/components/detail.vue
Normal file
152
pc/src/views/system/roles/components/detail.vue
Normal file
@ -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>
|
||||
|
||||
216
pc/src/views/system/roles/components/edit.vue
Normal file
216
pc/src/views/system/roles/components/edit.vue
Normal file
@ -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>
|
||||
|
||||
@ -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",
|
||||
|
||||
327
server/controllers/role.go
Normal file
327
server/controllers/role.go
Normal file
@ -0,0 +1,327 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
type RoleController struct {
|
||||
web.Controller
|
||||
}
|
||||
|
||||
// GetAllRoles 获取所有角色
|
||||
// @router /api/roles [get]
|
||||
func (c *RoleController) GetAllRoles() {
|
||||
roles, err := models.GetAllRoles()
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "获取角色列表失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
roleList := make([]map[string]interface{}, 0)
|
||||
for _, role := range roles {
|
||||
roleList = append(roleList, map[string]interface{}{
|
||||
"roleId": role.RoleId,
|
||||
"roleCode": role.RoleCode,
|
||||
"roleName": role.RoleName,
|
||||
"description": role.Description,
|
||||
"status": role.Status,
|
||||
"sortOrder": role.SortOrder,
|
||||
"createTime": role.CreateTime,
|
||||
"updateTime": role.UpdateTime,
|
||||
"createBy": role.CreateBy,
|
||||
"updateBy": role.UpdateBy,
|
||||
})
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 0,
|
||||
"message": "获取角色列表成功",
|
||||
"data": roleList,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetRoleById 根据ID获取角色详情
|
||||
// @router /api/roles/:id [get]
|
||||
func (c *RoleController) GetRoleById() {
|
||||
roleId, err := c.GetInt(":id")
|
||||
if err != nil || roleId <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "角色ID无效",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
role, err := models.GetRoleById(roleId)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "获取角色详情失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 0,
|
||||
"message": "获取角色详情成功",
|
||||
"data": map[string]interface{}{
|
||||
"roleId": role.RoleId,
|
||||
"roleCode": role.RoleCode,
|
||||
"roleName": role.RoleName,
|
||||
"description": role.Description,
|
||||
"status": role.Status,
|
||||
"sortOrder": role.SortOrder,
|
||||
"createTime": role.CreateTime,
|
||||
"updateTime": role.UpdateTime,
|
||||
"createBy": role.CreateBy,
|
||||
"updateBy": role.UpdateBy,
|
||||
},
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// 根据租户ID获取角色列表
|
||||
// @router /api/roles/tenant/:tenantId [get]
|
||||
func (c *RoleController) GetRoleByTenantId() {
|
||||
tenantId, err := c.GetInt(":tenantId")
|
||||
if err != nil || tenantId <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "租户ID无效",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
roles, err := models.GetRoleByTenantId(tenantId)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "获取角色列表失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 0,
|
||||
"message": "获取角色列表成功",
|
||||
"data": roles,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// CreateRole 创建角色
|
||||
// @router /api/roles [post]
|
||||
func (c *RoleController) CreateRole() {
|
||||
var role models.Role
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &role); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "参数解析失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if role.RoleCode == "" {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "角色代码不能为空",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
if role.RoleName == "" {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "角色名称不能为空",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查角色代码是否已存在
|
||||
existingRole, err := models.GetRoleByCode(role.RoleCode)
|
||||
if err == nil && existingRole != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "角色代码已存在",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if role.Status == 0 {
|
||||
role.Status = 1
|
||||
}
|
||||
|
||||
id, err := models.CreateRole(&role)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "创建角色失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 0,
|
||||
"message": "创建角色成功",
|
||||
"data": map[string]interface{}{
|
||||
"roleId": id,
|
||||
},
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// UpdateRole 更新角色
|
||||
// @router /api/roles/:id [put]
|
||||
func (c *RoleController) UpdateRole() {
|
||||
roleId, err := c.GetInt(":id")
|
||||
if err != nil || roleId <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "角色ID无效",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查角色是否存在
|
||||
existingRole, err := models.GetRoleById(roleId)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "角色不存在: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
var role models.Role
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &role); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "参数解析失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 如果更新了角色代码,检查是否重复
|
||||
if role.RoleCode != "" && role.RoleCode != existingRole.RoleCode {
|
||||
codeRole, err := models.GetRoleByCode(role.RoleCode)
|
||||
if err == nil && codeRole != nil && codeRole.RoleId != roleId {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "角色代码已存在",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 设置角色ID
|
||||
role.RoleId = roleId
|
||||
|
||||
err = models.UpdateRole(&role)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "更新角色失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 0,
|
||||
"message": "更新角色成功",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteRole 删除角色(软删除)
|
||||
// @router /api/roles/:id [delete]
|
||||
func (c *RoleController) DeleteRole() {
|
||||
roleId, err := c.GetInt(":id")
|
||||
if err != nil || roleId <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "角色ID无效",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否为系统默认角色,不允许删除
|
||||
role, err := models.GetRoleById(roleId)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "角色不存在: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 系统默认角色不允许删除
|
||||
if role.RoleCode == "system_admin" || role.RoleCode == "admin" || role.RoleCode == "user" {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "系统默认角色不允许删除",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
err = models.DeleteRole(roleId)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "删除角色失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 0,
|
||||
"message": "删除角色成功",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
# 知识库数据库表创建说明
|
||||
|
||||
## 创建步骤
|
||||
|
||||
### 方法1:执行独立的 SQL 文件(推荐)
|
||||
```bash
|
||||
# 在 MySQL 中执行
|
||||
mysql -u root -p your_database < server/database/create_knowledge_tables.sql
|
||||
```
|
||||
|
||||
### 方法2:手动执行 SQL
|
||||
```sql
|
||||
-- 1. 进入 MySQL
|
||||
mysql -u root -p your_database
|
||||
|
||||
-- 2. 执行 SQL 脚本
|
||||
SOURCE server/database/create_knowledge_tables.sql;
|
||||
```
|
||||
|
||||
### 方法3:在 MySQL 客户端中复制粘贴
|
||||
直接打开 `server/database/create_knowledge_tables.sql` 文件,复制所有内容,在 MySQL 客户端中执行。
|
||||
|
||||
## 创建的表
|
||||
|
||||
1. **yz_knowledge_category** - 知识库分类表
|
||||
2. **yz_knowledge_tags** - 知识库标签表
|
||||
3. **yz_knowledge** - 知识库内容表
|
||||
|
||||
## 默认数据
|
||||
|
||||
- 5 个分类:技术文档、产品手册、用户指南、常见问题、API文档
|
||||
- 8 个标签:Vue、React、TypeScript、Element Plus、Vue Router、Pinia、Go、Python
|
||||
|
||||
## 验证创建
|
||||
|
||||
```sql
|
||||
-- 查看所有知识库相关表
|
||||
SHOW TABLES LIKE 'yz_knowledge%';
|
||||
|
||||
-- 查看知识库表结构
|
||||
DESC yz_knowledge;
|
||||
|
||||
-- 查看数据
|
||||
SELECT * FROM yz_knowledge;
|
||||
SELECT * FROM yz_knowledge_category;
|
||||
SELECT * FROM yz_knowledge_tags;
|
||||
```
|
||||
|
||||
## 如果表已存在
|
||||
|
||||
如果想重新创建表(会清空现有数据):
|
||||
```sql
|
||||
DROP TABLE IF EXISTS yz_knowledge_favorites;
|
||||
DROP TABLE IF EXISTS yz_knowledge;
|
||||
DROP TABLE IF EXISTS yz_knowledge_tags;
|
||||
DROP TABLE IF EXISTS yz_knowledge_category;
|
||||
```
|
||||
然后再执行创建脚本。
|
||||
|
||||
@ -1,119 +0,0 @@
|
||||
# 租户表数据库创建说明
|
||||
|
||||
## 创建步骤
|
||||
|
||||
### 方法1:执行独立的 SQL 文件(推荐)
|
||||
```bash
|
||||
# 在 MySQL 中执行
|
||||
mysql -u root -p your_database < server/database/yz_tenants.sql
|
||||
```
|
||||
|
||||
### 方法2:手动执行 SQL
|
||||
```sql
|
||||
-- 1. 进入 MySQL
|
||||
mysql -u root -p your_database
|
||||
|
||||
-- 2. 执行 SQL 脚本
|
||||
SOURCE server/database/yz_tenants.sql;
|
||||
```
|
||||
|
||||
### 方法3:在 MySQL 客户端中复制粘贴
|
||||
直接打开 `server/database/yz_tenants.sql` 文件,复制所有内容,在 MySQL 客户端中执行。
|
||||
|
||||
## 创建的表
|
||||
|
||||
**yz_tenants** - 租户管理表
|
||||
|
||||
### 表结构说明
|
||||
|
||||
#### 基本信息字段
|
||||
- `id` - 租户ID(主键,自增)
|
||||
- `name` - 租户名称(必填)
|
||||
- `code` - 租户编码(必填,唯一)
|
||||
- `owner` - 负责人(必填)
|
||||
- `phone` - 联系电话(可选)
|
||||
- `email` - 邮箱地址(可选)
|
||||
|
||||
#### 状态字段
|
||||
- `status` - 状态:`enabled`(启用)或 `disabled`(禁用)
|
||||
- `audit_status` - 审核状态:
|
||||
- `pending` - 待审核
|
||||
- `approved` - 已通过
|
||||
- `rejected` - 已拒绝
|
||||
|
||||
#### 审核信息字段
|
||||
- `audit_comment` - 审核意见(可选)
|
||||
- `audit_by` - 审核人(可选)
|
||||
- `audit_time` - 审核时间(可选)
|
||||
|
||||
#### 其他字段
|
||||
- `remark` - 备注(可选)
|
||||
- `create_time` - 创建时间(自动)
|
||||
- `update_time` - 更新时间(自动)
|
||||
- `create_by` - 创建人(可选)
|
||||
- `update_by` - 更新人(可选)
|
||||
|
||||
### 索引说明
|
||||
- `uk_code` - 租户编码唯一索引
|
||||
- `idx_name` - 租户名称索引
|
||||
- `idx_owner` - 负责人索引
|
||||
- `idx_status` - 状态索引
|
||||
- `idx_audit_status` - 审核状态索引
|
||||
- `idx_create_time` - 创建时间索引
|
||||
|
||||
## 测试数据
|
||||
|
||||
SQL 文件中包含 10 条测试数据,涵盖了以下场景:
|
||||
1. **默认租户** - 已通过审核的系统默认租户
|
||||
2. **示例租户A** - 已通过审核的演示租户
|
||||
3. **示例租户B** - 待审核的租户
|
||||
4. **新申请租户C** - 待审核的新申请租户
|
||||
5. **已拒绝租户D** - 被拒绝的租户示例
|
||||
6. **企业租户E** - 已通过审核的企业级租户
|
||||
7. **测试租户F** - 已通过审核的测试环境租户
|
||||
8. **禁用租户G** - 已通过审核但被禁用的租户
|
||||
9. **小公司租户H** - 待审核的小型公司
|
||||
10. **个人开发者I** - 已通过审核的个人开发者账户
|
||||
|
||||
## 验证创建
|
||||
|
||||
```sql
|
||||
-- 查看租户表结构
|
||||
DESC yz_tenants;
|
||||
|
||||
-- 查看所有租户数据
|
||||
SELECT * FROM yz_tenants;
|
||||
|
||||
-- 查看特定状态的租户
|
||||
SELECT * FROM yz_tenants WHERE audit_status = 'pending';
|
||||
SELECT * FROM yz_tenants WHERE status = 'enabled';
|
||||
|
||||
-- 查看租户统计
|
||||
SELECT
|
||||
audit_status,
|
||||
COUNT(*) as count
|
||||
FROM yz_tenants
|
||||
GROUP BY audit_status;
|
||||
```
|
||||
|
||||
## 如果表已存在
|
||||
|
||||
如果想重新创建表(会清空现有数据):
|
||||
```sql
|
||||
DROP TABLE IF EXISTS yz_tenants;
|
||||
```
|
||||
然后再执行创建脚本。
|
||||
|
||||
## 与其他表的关系
|
||||
|
||||
租户表是系统中重要的基础表:
|
||||
- `yz_files` 表中的 `tenant_id` 字段引用租户编码(VARCHAR 类型,不是外键)
|
||||
- 未来可能会在 `yz_users` 表中添加 `tenant_id` 字段来关联租户
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **租户编码唯一性**:`code` 字段设置了唯一索引,确保每个租户编码都是唯一的
|
||||
2. **审核流程**:新创建的租户默认 `audit_status` 为 `pending`(待审核)
|
||||
3. **状态管理**:租户可以同时拥有 `status` 和 `audit_status` 两个状态字段,分别控制启用状态和审核状态
|
||||
4. **时间字段**:`create_time` 和 `update_time` 会自动管理,无需手动设置
|
||||
|
||||
@ -1,97 +0,0 @@
|
||||
# 知识库分类联查更新说明
|
||||
|
||||
## 更新内容
|
||||
|
||||
本次更新将 `yz_knowledge` 表中的 `category_name` 字段移除,改为通过联查获取分类名称。
|
||||
|
||||
## 数据库变更
|
||||
|
||||
### 移除的字段
|
||||
|
||||
从 `yz_knowledge` 表中移除 `category_name` 字段:
|
||||
```sql
|
||||
ALTER TABLE `yz_knowledge` DROP COLUMN `category_name`;
|
||||
```
|
||||
|
||||
### 更新后的表结构
|
||||
|
||||
`yz_knowledge` 表现在只保留 `category_id` 字段,通过 `LEFT JOIN` 联查 `yz_knowledge_category` 表获取分类名称:
|
||||
|
||||
```sql
|
||||
SELECT k.*, c.category_name
|
||||
FROM yz_knowledge k
|
||||
LEFT JOIN yz_knowledge_category c ON k.category_id = c.category_id
|
||||
WHERE k.knowledge_id = ?
|
||||
```
|
||||
|
||||
## Go 代码变更
|
||||
|
||||
### server/models/knowledge.go
|
||||
|
||||
1. **Knowledge 结构体**:
|
||||
- `CategoryName` 字段的 ORM 标签改为 `orm:"-"`,表示不在数据库中存储
|
||||
- 从联查结果中获取分类名称
|
||||
|
||||
2. **GetKnowledgeById 方法**:
|
||||
- 使用 `LEFT JOIN` 联查获取分类名称
|
||||
- 使用原生 SQL 查询替代 ORM 查询
|
||||
|
||||
3. **GetAllKnowledge 方法**:
|
||||
- 使用原生 SQL 查询进行联查
|
||||
- 支持分页、状态筛选、分类筛选、关键词搜索
|
||||
|
||||
4. **UpdateKnowledge 方法**:
|
||||
- 移除了对 `category_name` 字段的更新操作
|
||||
- 只更新 `category_id` 字段
|
||||
|
||||
## 前端代码变更
|
||||
|
||||
### front/src/views/apps/knowledge/components/detail.vue
|
||||
|
||||
- 将 `categoryName` 映射到显示用的 `category` 字段
|
||||
- 添加 `parseTags` 方法解析标签数组
|
||||
|
||||
### front/src/views/apps/knowledge/components/edit.vue
|
||||
|
||||
- 接收数据时使用 `data.categoryName` 获取分类名称
|
||||
- 提交数据时不需要传递 `categoryName`,只传递 `category_id`
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 创建知识记录
|
||||
|
||||
创建知识记录时,只传递 `category_id`:
|
||||
|
||||
```javascript
|
||||
{
|
||||
title: "标题",
|
||||
category_id: 1, // 只需传递分类ID
|
||||
// category_name 不需要传递,后台会从联查获取
|
||||
}
|
||||
```
|
||||
|
||||
### 获取知识详情
|
||||
|
||||
获取知识详情时,会自动联查获取 `category_name`:
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: 1,
|
||||
title: "标题",
|
||||
category_id: 1,
|
||||
category_name: "技术文档", // 从联查获取
|
||||
}
|
||||
```
|
||||
|
||||
## 优势
|
||||
|
||||
1. **数据冗余消除**:分类名称不再冗余存储
|
||||
2. **数据一致性**:分类名称始终与分类表保持一致
|
||||
3. **易于维护**:修改分类名称时,所有知识的分类名称都会自动更新
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保 `yz_knowledge_category` 表存在并包含必要的分类数据
|
||||
2. 所有知识记录的 `category_id` 必须存在于 `yz_knowledge_category` 表中
|
||||
3. 如果某个知识没有分类(`category_id` 为 NULL),联查返回的 `category_name` 将为 NULL
|
||||
|
||||
34
server/database/yz_roles.sql
Normal file
34
server/database/yz_roles.sql
Normal file
@ -0,0 +1,34 @@
|
||||
-- 创建角色表
|
||||
CREATE TABLE IF NOT EXISTS yz_roles (
|
||||
role_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '角色ID',
|
||||
tenant_id INT NOT NULL COMMENT '租户ID',
|
||||
role_code VARCHAR(50) NOT NULL COMMENT '角色代码',
|
||||
role_name VARCHAR(50) NOT NULL COMMENT '角色名称',
|
||||
description VARCHAR(500) DEFAULT NULL COMMENT '角色描述',
|
||||
status TINYINT DEFAULT 1 COMMENT '角色状态:0-禁用,1-启用',
|
||||
sort_order INT DEFAULT 0 COMMENT '排序序号',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
delete_time DATETIME DEFAULT NULL COMMENT '删除时间',
|
||||
create_by VARCHAR(50) DEFAULT NULL COMMENT '创建人',
|
||||
update_by VARCHAR(50) DEFAULT NULL COMMENT '更新人',
|
||||
|
||||
-- 索引
|
||||
UNIQUE KEY uk_role_code (role_code),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_sort_order (sort_order),
|
||||
INDEX idx_create_time (create_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表';
|
||||
|
||||
-- 插入默认角色数据
|
||||
INSERT INTO yz_roles (tenant_id, role_code, role_name, description, status, sort_order, create_by, delete_time) VALUES
|
||||
(0, 'system_admin', '系统管理员', '系统超级管理员,拥有所有权限', 1, 1, 'system'),
|
||||
(0, 'admin', '管理员', '普通管理员,拥有大部分管理权限', 1, 2, 'system'),
|
||||
(0, 'user', '普通用户', '普通用户,拥有基础使用权限', 1, 3, 'system')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
tenant_id = VALUES(tenant_id),
|
||||
role_name = VALUES(role_name),
|
||||
description = VALUES(description),
|
||||
update_time = CURRENT_TIMESTAMP,
|
||||
delete_time = NULL;
|
||||
|
||||
@ -49,6 +49,7 @@ func GetAllMenus() ([]map[string]interface{}, error) {
|
||||
"componentPath": m.ComponentPath,
|
||||
"isExternal": m.IsExternal,
|
||||
"externalUrl": m.ExternalUrl,
|
||||
"menuType": m.MenuType,
|
||||
}
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
113
server/models/role.go
Normal file
113
server/models/role.go
Normal file
@ -0,0 +1,113 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// Role 角色模型
|
||||
type Role struct {
|
||||
RoleId int `orm:"pk;auto;column(role_id)" json:"roleId"`
|
||||
TenantId int `orm:"column(tenant_id)" json:"tenantId"`
|
||||
RoleCode string `orm:"size(50);unique" json:"roleCode"`
|
||||
RoleName string `orm:"size(50)" json:"roleName"`
|
||||
Description string `orm:"type(text);null" json:"description"`
|
||||
Status int8 `orm:"default(1)" json:"status"`
|
||||
SortOrder int `orm:"default(0)" json:"sortOrder"`
|
||||
CreateTime time.Time `orm:"auto_now_add;type(datetime)" json:"createTime"`
|
||||
UpdateTime time.Time `orm:"auto_now;type(datetime)" json:"updateTime"`
|
||||
DeleteTime *time.Time `orm:"null;type(datetime)" json:"deleteTime"`
|
||||
CreateBy string `orm:"size(50);null" json:"createBy"`
|
||||
UpdateBy string `orm:"size(50);null" json:"updateBy"`
|
||||
}
|
||||
|
||||
// TableName 设置表名
|
||||
func (r *Role) TableName() string {
|
||||
return "yz_roles"
|
||||
}
|
||||
|
||||
func init() {
|
||||
orm.RegisterModel(new(Role))
|
||||
}
|
||||
|
||||
// GetAllRoles 获取所有角色(排除已删除的)
|
||||
func GetAllRoles() ([]Role, error) {
|
||||
o := orm.NewOrm()
|
||||
var roles []Role
|
||||
_, err := o.QueryTable("yz_roles").Filter("DeleteTime__isnull", true).Filter("Status", 1).OrderBy("SortOrder").All(&roles)
|
||||
return roles, err
|
||||
}
|
||||
|
||||
// GetRoleById 根据ID获取角色
|
||||
func GetRoleById(roleId int) (*Role, error) {
|
||||
o := orm.NewOrm()
|
||||
role := &Role{RoleId: roleId}
|
||||
err := o.Read(role)
|
||||
return role, err
|
||||
}
|
||||
|
||||
// GetRoleByTenantId 根据租户ID获取角色列表
|
||||
func GetRoleByTenantId(tenantId int) ([]Role, error) {
|
||||
o := orm.NewOrm()
|
||||
var roles []Role
|
||||
qs := o.QueryTable("yz_roles").Filter("DeleteTime__isnull", true)
|
||||
if tenantId > 0 {
|
||||
// 显示指定租户和公共(tenant_id=0)的角色
|
||||
qs = qs.Filter("TenantId__in", []int{0, tenantId})
|
||||
} else if tenantId == 0 {
|
||||
// 仅显示公共(tenant_id=0)的角色
|
||||
qs = qs.Filter("TenantId", 0)
|
||||
}
|
||||
_, err := qs.OrderBy("SortOrder").All(&roles)
|
||||
return roles, err
|
||||
}
|
||||
|
||||
// GetRoleByCode 根据代码获取角色(排除已删除的)
|
||||
func GetRoleByCode(roleCode string) (*Role, error) {
|
||||
o := orm.NewOrm()
|
||||
role := &Role{}
|
||||
err := o.QueryTable("yz_roles").Filter("RoleCode", roleCode).Filter("DeleteTime__isnull", true).One(role)
|
||||
return role, err
|
||||
}
|
||||
|
||||
// CreateRole 创建角色
|
||||
func CreateRole(role *Role) (int64, error) {
|
||||
o := orm.NewOrm()
|
||||
id, err := o.Insert(role)
|
||||
return id, err
|
||||
}
|
||||
|
||||
// UpdateRole 更新角色
|
||||
func UpdateRole(role *Role) error {
|
||||
o := orm.NewOrm()
|
||||
_, err := o.Update(role)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteRole 删除角色(软删除,设置删除时间)
|
||||
func DeleteRole(roleId int) error {
|
||||
o := orm.NewOrm()
|
||||
role := &Role{RoleId: roleId}
|
||||
err := o.Read(role)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
role.DeleteTime = &now
|
||||
_, err = o.Update(role, "DeleteTime")
|
||||
return err
|
||||
}
|
||||
|
||||
// 修改角色状态
|
||||
func ChangeStatus(roleId int, status int) error {
|
||||
o := orm.NewOrm()
|
||||
role := &Role{RoleId: roleId}
|
||||
err := o.Read(role)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
role.Status = int8(status)
|
||||
_, err = o.Update(role, "Status")
|
||||
return err
|
||||
}
|
||||
@ -85,6 +85,7 @@ func init() {
|
||||
beego.Router("/api/files", &controllers.FileController{}, "post:Post")
|
||||
beego.Router("/api/files/my", &controllers.FileController{}, "get:GetMyFiles")
|
||||
beego.Router("/api/files/:id", &controllers.FileController{}, "get:GetFileById")
|
||||
beego.Router("/api/files/tenant", &controllers.FileController{}, "get:GetFilesByTenant")
|
||||
beego.Router("/api/files/:id", &controllers.FileController{}, "put:UpdateFile")
|
||||
beego.Router("/api/files/:id", &controllers.FileController{}, "delete:DeleteFile")
|
||||
beego.Router("/api/files/search", &controllers.FileController{}, "get:SearchFiles")
|
||||
@ -109,6 +110,14 @@ func init() {
|
||||
beego.Router("/api/tenant/:id/audit", &controllers.TenantController{}, "post:AuditTenant")
|
||||
beego.Router("/api/tenant/:id", &controllers.TenantController{}, "get:GetTenantDetail")
|
||||
|
||||
// 角色相关路由
|
||||
beego.Router("/api/roles", &controllers.RoleController{}, "get:GetAllRoles")
|
||||
beego.Router("/api/roles", &controllers.RoleController{}, "post:CreateRole")
|
||||
beego.Router("/api/roles/:id", &controllers.RoleController{}, "get:GetRoleById")
|
||||
beego.Router("/api/roles/tenant/:tenantId", &controllers.RoleController{}, "get:GetRoleByTenantId")
|
||||
beego.Router("/api/roles/:id", &controllers.RoleController{}, "post:UpdateRole")
|
||||
beego.Router("/api/roles/:id", &controllers.RoleController{}, "delete:DeleteRole")
|
||||
|
||||
// 手动配置特殊路由(无法通过自动路由处理的)
|
||||
beego.Router("/api/allmenu", &controllers.MenuController{}, "get:GetAllMenus")
|
||||
beego.Router("/api/program-categories/public", &controllers.ProgramCategoryController{}, "get:GetProgramCategoriesPublic")
|
||||
|
||||
@ -1 +0,0 @@
|
||||
|
||||
Loading…
Reference in New Issue
Block a user