批量更新

This commit is contained in:
扫地僧 2025-11-02 23:53:41 +08:00
parent 7972f0a6d9
commit 9a6c29f3b0
29 changed files with 1947 additions and 899 deletions

View File

@ -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. **检查文件权限**
- 确保应用有创建目录和写入文件的权限

View File

@ -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
- ✅ 实时预览
- ✅ 分类和标签管理
- ✅ 搜索和分页
- ✅ 主题支持
- ✅ 响应式布局

View File

@ -11,17 +11,6 @@ export function getAllFiles() {
});
}
/**
* 获取我的文件
* @returns {Promise}
*/
export function getMyFiles() {
return request({
url: "/api/files/my",
method: "get",
});
}
/**
* 根据ID获取文件
* @param {number|string} id 文件ID
@ -34,6 +23,18 @@ export function getFileById(id) {
});
}
/**
* 根据租户ID获取文件
* @param {string} tenantId 租户ID
* @returns {Promise}
*/
export function getFilesByTenant(tenantId) {
return request({
url: `/api/files/tenant?tenant_id=${tenantId}`,
method: "get",
});
}
/**
* 上传文件
* @param {FormData} formData 文件数据

46
pc/src/api/role.js Normal file
View File

@ -0,0 +1,46 @@
import request from '@/utils/request'
export function getAllRoles(params) {
return request({
url: '/api/roles',
method: 'get',
params
})
}
export function createRole(data) {
return request({
url: '/api/roles',
method: 'post',
data
})
}
export function getRoleById(id) {
return request({
url: `/api/roles/${id}`,
method: 'get'
})
}
export function getRoleByTenantId(tenantId) {
return request({
url: `/api/roles/tenant/${tenantId}`,
method: 'get'
})
}
export function updateRole(id, data) {
return request({
url: `/api/roles/${id}`,
method: 'post',
data
})
}
export function deleteRole(id) {
return request({
url: `/api/roles/${id}`,
method: 'delete'
})
}

View File

@ -40,4 +40,12 @@ body {
position: fixed !important;
z-index: var(--el-message-z-index, 9999) !important;
pointer-events: auto !important;
}
}
.wang-editor-wrapper{
border: 1px solid #dcdfe6 !important;
.toolbar-container{
border-bottom: 1px solid #dcdfe6 !important;
}
}

View File

@ -68,6 +68,14 @@ export function convertMenusToRoutes(menus) {
}
}
// 特殊处理:知识库的编辑和详情页需要支持动态参数
// 如果最终路径是 edit 或 detail转换为支持参数的路由
if (currentRoute.path === 'edit') {
currentRoute.path = 'edit/:id';
} else if (currentRoute.path === 'detail') {
currentRoute.path = 'detail/:id';
}
if (!parentRoute.children) {
parentRoute.children = [];
}

View File

@ -140,30 +140,25 @@ function addDynamicRoutes(menus) {
router.addRoute(newMainRoute);
// 添加知识库的子路由(详情页和编辑页)
// 直接在 Main 路由下添加完整路径的子路由
// router.addRoute('Main', {
// path: 'apps/knowledge/detail/:id',
// name: 'apps-knowledge-detail',
// component: () => import('@/views/apps/knowledge/components/detail.vue'),
// meta: {
// requiresAuth: true,
// title: '知识详情'
// }
// });
// router.addRoute('Main', {
// path: 'apps/knowledge/edit/:id',
// name: 'apps-knowledge-edit',
// component: () => import('@/views/apps/knowledge/components/edit.vue'),
// meta: {
// requiresAuth: true,
// title: '编辑知识'
// }
// });
dynamicRoutesAdded = true;
}
// 递归查找路由(根据名称)
function findRouteByName(routes, routeName) {
for (const route of routes) {
if (route.name === routeName) {
return route;
}
if (route.children) {
const found = findRouteByName(route.children, routeName);
if (found) {
return found;
}
}
}
return null;
}
// 查找第一个有效的路由(有组件的路由)
function findFirstValidRoute(routes) {
for (const route of routes) {

View File

@ -114,6 +114,48 @@ export const useTabsStore = defineTabsStore('tabs', () => {
saveTabsToStorage(tabList.value, activeTab.value);
}
// 关闭左侧关闭指定tab左侧的所有tab保留首页和目标tab
function closeLeft(targetFullPath) {
const targetIndex = tabList.value.findIndex((t) => t.fullPath === targetFullPath);
if (targetIndex > -1) {
// 保留首页和目标tab及其右侧的所有tab
const beforeIndex = tabList.value.slice(0, targetIndex);
const hasCloseableLeft = beforeIndex.some(t => t.fullPath !== defaultDashboardPath);
if (hasCloseableLeft) {
tabList.value = tabList.value.filter((t, index) =>
t.fullPath === defaultDashboardPath || index >= targetIndex
);
// 如果关闭的tab中包含了当前激活的tab则激活目标tab
if (!tabList.value.find(t => t.fullPath === activeTab.value)) {
activeTab.value = targetFullPath;
}
saveTabsToStorage(tabList.value, activeTab.value);
}
}
}
// 关闭右侧关闭指定tab右侧的所有tab保留首页和目标tab
function closeRight(targetFullPath) {
const targetIndex = tabList.value.findIndex((t) => t.fullPath === targetFullPath);
if (targetIndex > -1) {
// 保留首页和目标tab及其左侧的所有tab
const afterIndex = tabList.value.slice(targetIndex + 1);
const hasCloseableRight = afterIndex.length > 0;
if (hasCloseableRight) {
tabList.value = tabList.value.filter((t, index) =>
t.fullPath === defaultDashboardPath || index <= targetIndex
);
// 如果关闭的tab中包含了当前激活的tab则激活目标tab
if (!tabList.value.find(t => t.fullPath === activeTab.value)) {
activeTab.value = targetFullPath;
}
saveTabsToStorage(tabList.value, activeTab.value);
}
}
}
// 关闭全部,只留首页
function closeAll() {
tabList.value = tabList.value.filter((t) => t.fullPath === defaultDashboardPath);
@ -133,6 +175,8 @@ export const useTabsStore = defineTabsStore('tabs', () => {
addTab,
removeTab,
closeOthers,
closeLeft,
closeRight,
closeAll,
setActiveTab,
saveTabsToStorage,

View File

@ -3,7 +3,7 @@ import CommonAside from '@/components/CommonAside.vue';
import CommonHeader from '@/components/CommonHeader.vue';
import { useTabsStore } from '@/stores';
import { useRouter, useRoute } from 'vue-router';
import { ref, watch, reactive, nextTick, onMounted } from 'vue';
import { ref, watch, reactive, nextTick, onMounted, computed } from 'vue';
import { More, Close, CircleClose } from '@element-plus/icons-vue'
const tabsStore = useTabsStore();
@ -42,6 +42,45 @@ onMounted(() => {
// localStorage tabs store
// tab
restoreTabFromRoute();
// tabs 使
const tabsWrapper = document.querySelector('.multi-tabs-wrapper');
if (tabsWrapper) {
tabsWrapper.addEventListener('contextmenu', (e) => {
//
//
const editorWrapper = e.target.closest?.('.wang-editor-wrapper');
if (editorWrapper) {
//
return;
}
//
const editorElements = [
'.w-e-toolbar',
'.w-e-drop-panel',
'.w-e-modal',
'.w-e-toolbar-menu',
'[data-menu-key]',
'[class*="w-e-"]'
];
for (const selector of editorElements) {
if (e.target.closest && e.target.closest(selector)) {
//
return;
}
}
// tab item
const tabItem = e.target.closest('.el-tabs__item');
if (tabItem) {
e.preventDefault();
e.stopPropagation();
handleTabsContextMenu(e);
}
});
}
});
});
@ -114,16 +153,141 @@ const contextMenu = reactive({
});
const contextDropdownRef = ref(null);
const onTabContextMenu = (event, tab) => {
// tabs
const handleTabsContextMenu = (event) => {
event.preventDefault();
contextMenu.visible = true;
contextMenu.x = event.clientX;
contextMenu.y = event.clientY;
contextMenu.tab = tab;
event.stopPropagation();
// tab item el-tabs__item
let target = event.target;
let tabItem = null;
// el-tabs__item
while (target && target !== event.currentTarget) {
if (target.classList && target.classList.contains('el-tabs__item')) {
tabItem = target;
break;
}
target = target.parentElement;
}
if (!tabItem) return;
// Element Plus tab item id "tab-{name}" name tab-pane name
const tabId = tabItem.id;
if (tabId && tabId.startsWith('tab-')) {
const tabName = tabId.replace('tab-', '');
const matchedTab = tabsStore.tabList.find(t => t.fullPath === tabName);
if (matchedTab) {
showContextMenu(event, matchedTab);
return;
}
}
// id aria-controls
const ariaControls = tabItem.getAttribute('aria-controls');
if (ariaControls) {
const tabName = ariaControls.replace('pane-', '');
const matchedTab = tabsStore.tabList.find(t => t.fullPath === tabName);
if (matchedTab) {
showContextMenu(event, matchedTab);
}
}
};
//
const showContextMenu = (event, tab) => {
// 使 nextTick
nextTick(() => {
//
contextMenu.visible = true;
contextMenu.x = event.clientX;
contextMenu.y = event.clientY;
contextMenu.tab = tab;
//
//
setTimeout(() => {
const hideMenuHandler = (e) => {
//
if (!contextMenu.visible) {
return;
}
const target = e.target;
if (!target) {
hideContextMenu();
return;
}
//
if (target.closest?.('.context-menu')) {
return; //
}
// ========== ==========
//
//
const wangEditorWrapper = target.closest?.('.wang-editor-wrapper');
if (wangEditorWrapper) {
//
return;
}
//
const editorSelectors = [
'.w-e-toolbar',
'.w-e-drop-panel',
'.w-e-modal',
'.w-e-toolbar-menu',
'.toolbar-container',
'.editor-container',
'.w-e-text-container',
'.w-e-text',
// WangEditor
'[data-menu-key]',
'[class*="w-e-"]'
];
for (const selector of editorSelectors) {
if (target.closest?.(selector)) {
//
return;
}
}
//
hideContextMenu();
};
// 使 capture: false
//
// body
//
setTimeout(() => {
//
if (contextMenu.visible) {
document.body.addEventListener('click', hideMenuHandler, { once: true, capture: false, passive: true });
}
}, 100); //
const hideContextMenuHandler = (e) => {
const target = e.target;
if (!target) {
hideContextMenu();
return;
}
//
if (target.closest?.('.w-e-toolbar') || target.closest?.('.context-menu') || target.closest?.('.wang-editor-wrapper')) {
return;
}
hideContextMenu();
};
document.body.addEventListener('contextmenu', hideContextMenuHandler, { once: true });
}, 0);
});
document.body.addEventListener('click', hideContextMenu, { once: true });
};
function hideContextMenu() {
contextMenu.visible = false;
@ -135,18 +299,63 @@ function closeTabContextTab() {
}
hideContextMenu();
}
function closeOthersContextTab() {
//
function closeLeftContextTab() {
if (contextMenu.tab) {
tabsStore.activeTab = contextMenu.tab.fullPath;
tabsStore.closeOthers();
tabsStore.closeLeft(contextMenu.tab.fullPath);
// tabtab
if (!tabsStore.tabList.find(tab => tab.fullPath === route.fullPath)) {
router.push(tabsStore.activeTab);
}
}
hideContextMenu();
}
//
function closeRightContextTab() {
if (contextMenu.tab) {
tabsStore.closeRight(contextMenu.tab.fullPath);
// tabtab
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>

View File

@ -14,7 +14,7 @@
<el-icon><Plus /></el-icon>
添加分类
</el-button>
<el-button @click="fetchList" :loading="loading">
<el-button type="primary" @click="refresh" :loading="loading">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
@ -365,6 +365,16 @@ const fetchList = async () => {
}
};
//
async function refresh() {
try {
await fetchList();
ElMessage.success('刷新成功');
} catch (error) {
ElMessage.error('刷新失败');
}
}
const handleAdd = () => {
dialogTitle.value = '添加分类';
//

View File

@ -31,19 +31,15 @@
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="分类" prop="category">
<el-select
v-model="formData.category"
<el-cascader
v-model="cascaderValue"
:options="categoryTreeOptions"
:props="cascaderProps"
placeholder="请选择分类"
clearable
style="width: 100%"
@change="handleCategoryChange"
>
<el-option
v-for="item in categoryList"
:key="item.categoryId"
:label="item.categoryName"
:value="item.categoryName"
/>
</el-select>
/>
</el-form-item>
</el-col>
<el-col :span="6">
@ -73,8 +69,8 @@
<el-col :span="6">
<el-form-item label="权限" prop="share">
<el-radio-group v-model="formData.share">
<el-radio-button :label="0">个人</el-radio-button>
<el-radio-button :label="1">共享</el-radio-button>
<el-radio-button :value="0">个人</el-radio-button>
<el-radio-button :value="1">共享</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
@ -115,7 +111,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from "vue";
import { ref, reactive, computed, onMounted, watch, nextTick } from "vue";
import { useRouter, useRoute } from "vue-router";
import { marked } from "marked";
import { ArrowLeft, Check } from '@element-plus/icons-vue'
@ -158,8 +154,21 @@ const rules = reactive<FormRules>({
interface CategoryItem {
categoryId: number;
categoryName: string;
parentId?: number;
children?: CategoryItem[];
}
const categoryList = ref<CategoryItem[]>([]);
const categoryTreeOptions = computed(() => {
return buildCategoryTree(categoryList.value);
});
const cascaderValue = ref<number | null>(null);
const cascaderProps = {
value: 'categoryId',
label: 'categoryName',
children: 'children',
checkStrictly: true,
emitPath: false,
};
const tagList = ref<string[]>([]);
const id = computed(() => route.params.id || route.query.id);
@ -204,14 +213,25 @@ const fetchDetail = async () => {
formData.content = data.content || "";
formData.share = data.share || 0;
if (!formData.categoryId && formData.category) {
const foundCategory = categoryList.value.find(
(item) => item.categoryName === formData.category
);
if (foundCategory) {
formData.categoryId = foundCategory.categoryId;
//
// categoryList
// 使 nextTick
nextTick(() => {
if (formData.categoryId) {
cascaderValue.value = formData.categoryId;
} else if (formData.category) {
// categoryId categoryName
const foundCategory = categoryList.value.find(
(item) => item.categoryName === formData.category
);
if (foundCategory) {
formData.categoryId = foundCategory.categoryId;
cascaderValue.value = foundCategory.categoryId;
}
} else {
cascaderValue.value = null;
}
}
});
if (data.tags) {
try {
@ -253,12 +273,67 @@ const loadCategoryAndTag = async () => {
}
};
const handleCategoryChange = (categoryName: string) => {
const selectedCategory = categoryList.value.find(
(item) => item.categoryName === categoryName
);
if (selectedCategory) {
formData.categoryId = selectedCategory.categoryId;
//
const buildCategoryTree = (list: CategoryItem[]): CategoryItem[] => {
if (!list || list.length === 0) return [];
const map = new Map<number, CategoryItem>();
const roots: CategoryItem[] = [];
//
list.forEach(item => {
map.set(item.categoryId, {
...item,
children: [],
});
});
//
list.forEach(item => {
const node = map.get(item.categoryId);
const parentId = item.parentId || 0;
if (parentId === 0 || !map.has(parentId)) {
//
roots.push(node!);
} else {
//
const parent = map.get(parentId);
if (parent && node) {
if (!parent.children) {
parent.children = [];
}
parent.children.push(node);
}
}
});
return roots;
};
const handleCategoryChange = (value: number | null) => {
if (value) {
formData.categoryId = value;
// categoryId categoryName
const findCategoryName = (list: CategoryItem[], id: number): string | null => {
for (const item of list) {
if (item.categoryId === id) {
return item.categoryName;
}
if (item.children) {
const found = findCategoryName(item.children, id);
if (found) return found;
}
}
return null;
};
const categoryName = findCategoryName(categoryTreeOptions.value, value);
if (categoryName) {
formData.category = categoryName;
}
} else {
formData.categoryId = 0;
formData.category = '';
}
};
@ -340,12 +415,8 @@ onMounted(async () => {
if (isEdit.value) {
await fetchDetail();
} else {
if (categoryList.value.length > 0 && !formData.category) {
formData.category = categoryList.value[0].categoryName;
formData.categoryId = categoryList.value[0].categoryId;
}
}
//
});
defineExpose({
@ -428,12 +499,13 @@ defineExpose({
.editor-title,
.preview-title {
margin: 0 0 16px;
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.editor-content {
flex: 1;
min-height: 400px;

View File

@ -80,6 +80,10 @@
<el-icon><PriceTag /></el-icon>
标签管理
</el-button>
<el-button @click="refresh" :loading="loading">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
@ -132,7 +136,7 @@
</div>
<div class="stat-item">
<el-icon><Clock /></el-icon>
<span>{{ formatDate(repo.createTime) }}</span>
<span>{{ formatDate(repo.updateTime || repo.createTime) }}</span>
</div>
</div>
</div>
@ -222,7 +226,7 @@
</div>
<div class="stat-item">
<el-icon><Clock /></el-icon>
<span>{{ formatDate(repo.createTime) }}</span>
<span>{{ formatDate(repo.updateTime || repo.createTime) }}</span>
</div>
</div>
</div>
@ -281,7 +285,7 @@
import { ref, reactive, computed, onMounted } from "vue";
import { useRouter, useRoute } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
import { Search, Plus, Folder, PriceTag, User, View, Star, Clock } from "@element-plus/icons-vue";
import { Search, Plus, Folder, PriceTag, User, View, Star, Clock, Refresh } from "@element-plus/icons-vue";
// @ts-ignore
import { getKnowledgeList, deleteKnowledge } from "@/api/knowledge";
@ -318,10 +322,10 @@ interface StatItem {
const router = useRouter();
const route = useRoute();
// category tag
// categorytagedit detail
const isSubRoute = computed(() => {
const path = route.path;
return path.includes('/category') || path.includes('/tag');
return path.includes('/category') || path.includes('/tag') || path.includes('/edit') || path.includes('/detail');
});
//
@ -418,6 +422,16 @@ async function fetchRepoList() {
}
}
//
async function refresh() {
try {
await fetchRepoList();
ElMessage.success('刷新成功');
} catch (error) {
ElMessage.error('刷新失败');
}
}
function handleCreate() {
router.push(`/apps/knowledge/edit/new`);
}

View File

@ -14,7 +14,7 @@
<el-icon><Plus /></el-icon>
添加标签
</el-button>
<el-button @click="fetchList" :loading="loading">
<el-button type="primary" @click="refresh" :loading="loading">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
@ -263,6 +263,16 @@ const fetchList = async () => {
}
};
//
async function refresh() {
try {
await fetchList();
ElMessage.success('刷新成功');
} catch (error) {
ElMessage.error('刷新失败');
}
}
const handleAdd = () => {
dialogTitle.value = '添加标签';
formRef.value?.resetFields();

View File

@ -2,10 +2,6 @@
<el-card class="box-card">
<div class="header-bar">
<h2>文件管理</h2>
<!-- <el-button type="primary" @click="showProgramDialog = true">
<el-icon><Plus /></el-icon>
添加程序
</el-button> -->
</div>
<div v-if="loading" class="loading-state">
@ -108,13 +104,6 @@
/>
</el-select>
<el-switch
v-model="showMyFiles"
class="switch-mine"
active-text="仅看我的文件"
inactive-text="全部文件"
@change="handleFilter"
/>
</div>
<!-- 文件数据列表 -->
@ -373,9 +362,11 @@ import {
Link,
UploadFilled,
} from "@element-plus/icons-vue";
import { getAllFiles, getMyFiles, getFileById, deleteFile as deleteFileApi, searchFiles } from "@/api/file";
import { getFilesByTenant, getFileById, deleteFile as deleteFileApi, searchFiles } from "@/api/file";
import { useAuthStore } from "@/stores/auth";
const router = useRouter();
const authStore = useAuthStore();
//
const allFiles = ref<any[]>([]);
@ -385,23 +376,35 @@ const loading = ref(false);
const error = ref("");
const searchKeyword = ref("");
const filterCategory = ref("");
const showMyFiles = ref(false);
const pageSize = ref(10);
const currentPage = ref(1);
const totalFiles = ref(0);
const showUploadDialog = ref(false);
// ID
const getCurrentTenantId = () => {
if (authStore.user && authStore.user.tenant_id) {
return authStore.user.tenant_id;
}
const userInfo = localStorage.getItem('userInfo');
if (userInfo) {
try {
const user = JSON.parse(userInfo);
return user.tenant_id || user.tenantId || 0;
} catch (e) {
console.error('Failed to parse user info:', e);
}
}
return 0;
};
//
async function fetchFiles() {
loading.value = true;
error.value = "";
try {
let res;
if (showMyFiles.value) {
res = await getMyFiles();
} else {
res = await getAllFiles();
}
const tenantId = getCurrentTenantId();
const res = await getFilesByTenant(tenantId);
if (res.success && res.data) {
const files = Array.isArray(res.data) ? res.data : [];
@ -685,10 +688,6 @@ const deleteFile = async (file: any) => {
}
};
//
watch([showMyFiles], () => {
fetchFiles();
});
//
const paginatedFiles = computed(() => {

View File

@ -2,25 +2,41 @@
<div class="container-box">
<div class="header-bar">
<h2>菜单管理</h2>
<el-button type="primary" @click="handleAddMenu = true">
<el-icon><Plus /></el-icon>
添加菜单
</el-button>
<div class="header-actions">
<el-button @click="expandAll">
<el-icon><FolderOpened /></el-icon>
全部展开
</el-button>
<el-button @click="collapseAll">
<el-icon><Folder /></el-icon>
全部折叠
</el-button>
<el-button type="primary" @click="handleAddMenu">
<el-icon><Plus /></el-icon>
添加菜单
</el-button>
<el-button @click="refresh" :loading="loading">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
<el-divider></el-divider>
<!-- 树形表格 -->
<el-table
ref="tableRef"
:data="menuTree"
style="width: 100%"
row-key="Id"
border
v-loading="loading"
element-loading-text="正在加载..."
:tree-props="{
children: 'children',
hasChildren: 'hasChildren',
}"
:expand-row-keys="defaultExpandedKeys"
>
<el-table-column prop="Name" label="菜单名称" width="200">
<template #default="scope">
@ -124,10 +140,15 @@
<el-form-item label="菜单类型" prop="MenuType">
<el-radio-group v-model="currentMenu.MenuType" style="width: 100%">
<el-radio-button :label="1">普通菜单</el-radio-button>
<el-radio-button :label="2">分组菜单</el-radio-button>
<el-radio-button :label="3">按钮菜单</el-radio-button>
<el-radio-button :value="1">页面菜单</el-radio-button>
<el-radio-button :value="2">目录菜单</el-radio-button>
<el-radio-button :value="3">权限按钮</el-radio-button>
</el-radio-group>
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary);">
<div> 页面菜单有路由和组件地址可访问完整功能页面</div>
<div> 目录菜单只有路由地址用于组织结构无实际页面</div>
<div> 权限按钮无路由和组件仅用于权限控制</div>
</div>
</el-form-item>
<el-form-item
@ -185,7 +206,7 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ElMessage, ElMessageBox, ElForm } from "element-plus";
import { Plus, CirclePlus, Edit, Delete } from "@element-plus/icons-vue";
import { Plus, CirclePlus, Edit, Delete, Refresh, FolderOpened, Folder } from "@element-plus/icons-vue";
import { getAllMenus, updateMenuStatus, createMenu, updateMenu, deleteMenu } from "@/api/menu";
// Pascal
@ -210,9 +231,10 @@ interface Menu {
//
const menuTree = ref<Menu[]>([]);
const loading = ref(false);
// ID
const defaultExpandedKeys = ref<number[]>([]);
//
const tableRef = ref<any>(null);
//
const dialogVisible = ref(false);
@ -256,6 +278,7 @@ const parentMenuOptions = ref<Menu[]>([]);
//
const fetchMenus = async () => {
loading.value = true;
try {
// 使 getAllMenus Pascal
const result = await getAllMenus();
@ -268,12 +291,12 @@ const fetchMenus = async () => {
ParentId: item.parentId,
Icon: item.icon,
Order: item.order,
Status: 1, // Status=1
Status: 1,
ComponentPath: item.componentPath || "",
IsExternal: item.isExternal || 0,
ExternalUrl: item.externalUrl || "",
MenuType: 1, // MenuType1
Permission: "", //
MenuType: item.menuType,
Permission: item.permission || "",
CreateTime: "",
UpdateTime: "",
}));
@ -284,11 +307,6 @@ const fetchMenus = async () => {
const tree = buildMenuTree(data);
menuTree.value = tree;
// ParentId=0
defaultExpandedKeys.value = tree
.filter((menu) => menu.ParentId === 0)
.map((menu) => menu.Id);
//
parentMenuOptions.value = [
{
@ -304,9 +322,58 @@ const fetchMenus = async () => {
}
} catch (error) {
ElMessage.error("获取菜单数据失败: " + (error as Error).message);
} finally {
loading.value = false;
}
};
//
async function refresh() {
loading.value = true;
try {
await fetchMenus();
ElMessage.success('刷新成功');
} catch (error) {
ElMessage.error('刷新失败');
} finally {
loading.value = false;
}
}
//
function getAllMenuRows(menuList: Menu[]): Menu[] {
const rows: Menu[] = [];
menuList.forEach((menu) => {
rows.push(menu);
if (menu.children && menu.children.length > 0) {
rows.push(...getAllMenuRows(menu.children));
}
});
return rows;
}
//
function expandAll() {
if (!tableRef.value) return;
const allRows = getAllMenuRows(menuTree.value);
allRows.forEach((row) => {
if (row.children && row.children.length > 0) {
tableRef.value.toggleRowExpansion(row, true);
}
});
}
//
function collapseAll() {
if (!tableRef.value) return;
const allRows = getAllMenuRows(menuTree.value);
allRows.forEach((row) => {
if (row.children && row.children.length > 0) {
tableRef.value.toggleRowExpansion(row, false);
}
});
}
//
const buildMenuTree = (menuList: Menu[]): Menu[] => {
const menuMap = new Map<number, Menu>();
@ -339,7 +406,7 @@ const buildMenuTree = (menuList: Menu[]): Menu[] => {
//
const getMenuTypeName = (type: number) => {
const typeMap = { 1: "普通菜单", 2: "分组菜单", 3: "按钮菜单" };
const typeMap = { 1: "页面菜单", 2: "目录菜单", 3: "权限按钮" };
return typeMap[type as keyof typeof typeMap] || "未知类型";
};
@ -485,6 +552,24 @@ onMounted(() => {
</script>
<style lang="less" scoped>
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
}
.header-bar h2 {
margin: 0;
font-size: 18px;
font-weight: 500;
}
.header-actions {
display: flex;
gap: 8px;
}
.card-header {
display: flex;
justify-content: space-between;

View 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>

View 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>

View File

@ -3,215 +3,241 @@
<div class="header-bar">
<h2>角色管理</h2>
<div class="header-actions">
<el-button type="primary" @click="showRoleDialog = true">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
添加角色
</el-button>
<el-button @click="refresh">
<el-icon><Refresh /></el-icon>
刷新</el-button>
</div>
</div>
<el-divider></el-divider>
<!-- 加载状态 -->
<div v-if="loading" class="loading-state">
<div class="loading-spinner"></div>
<p>正在加载角色数据...</p>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-state">
<div v-if="error" class="error-state">
<el-alert title="加载失败" :message="error" type="error" show-icon />
<el-button type="primary" @click="fetchRoles">重试</el-button>
<el-button type="primary" @click="refresh">重试</el-button>
</div>
<!-- 角色列表 -->
<div v-else>
<el-table :data="roles" stripe style="width: 100%" v-loading="loading">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="label" label="角色名称" align="center" />
<el-table-column prop="value" label="角色标识" align="center" />
<el-table-column prop="remark" label="备注" align="center" />
<el-table-column label="操作" width="180" align="center">
<el-table-column prop="roleId" label="ID" width="80" align="center" />
<el-table-column prop="roleName" label="角色名称" min-width="150" align="center" />
<el-table-column prop="roleCode" label="角色代码" min-width="150" align="center">
<template #default="{ row }">
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)"
>删除</el-button
<el-tag size="small">{{ row.roleCode }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" label="角色描述" min-width="200" align="center" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sortOrder" label="排序" width="80" align="center" />
<el-table-column label="创建时间" width="180" align="center">
<template #default="{ row }">
{{ formatDate(row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="handleView(row)">
<el-icon><View /></el-icon>
详情
</el-button>
<el-button
v-if="row.roleCode !== 'system_admin' && row.roleCode !== 'admin' && row.roleCode !== 'user'"
size="small"
type="primary"
link
@click="handleEdit(row)">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button
v-if="row.roleCode !== 'system_admin' && row.roleCode !== 'admin' && row.roleCode !== 'user'"
size="small"
type="danger"
link
@click="handleDelete(row)"
>
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-bar">
<el-pagination
background
:current-page="page"
:page-size="pageSize"
:total="total"
@current-change="handlePageChange"
layout="total, prev, pager, next"
/>
</div>
</div>
<!-- 新增/编辑角色弹窗 -->
<el-dialog
:title="isEditing ? '编辑角色' : '添加角色'"
v-model="showRoleDialog"
width="420px"
:close-on-click-modal="false"
>
<el-form
:model="roleForm"
:rules="formRules"
ref="roleFormRef"
label-width="90px"
>
<el-form-item label="角色名称" prop="label">
<el-input
v-model="roleForm.label"
:disabled="isEditing && roleForm.value === 'admin'"
/>
</el-form-item>
<el-form-item label="角色标识" prop="value">
<el-input
v-model="roleForm.value"
:disabled="isEditing && roleForm.value === 'admin'"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="roleForm.remark" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showRoleDialog = false">取消</el-button>
<el-button type="primary" @click="submitRoleForm"> 保存 </el-button>
</template>
</el-dialog>
<!-- 编辑对话框 -->
<RoleEditDialog
v-model="editDialogVisible"
:role="currentRole"
@success="handleEditSuccess"
/>
<!-- 详情对话框 -->
<RoleDetailDialog
v-model="detailDialogVisible"
:roleId="currentRoleId"
/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
import { ref, onMounted } from "vue";
import {
ElMessage,
ElMessageBox,
type FormInstance,
type FormRules,
} from "element-plus";
import { Plus, View, Edit, Delete, Refresh } from "@element-plus/icons-vue";
import { getRoleByTenantId, deleteRole } from "@/api/role";
import { useAuthStore } from "@/stores/auth";
import RoleEditDialog from "./components/edit.vue";
import RoleDetailDialog from "./components/detail.vue";
const roles = ref<any[]>([]);
const loading = ref(false);
const error = ref("");
const page = ref(1);
const pageSize = ref(10);
const total = ref(0);
const showRoleDialog = ref(false);
const isEditing = ref(false);
const roleForm = reactive({
id: null,
label: "",
value: "",
remark: "",
});
const roleFormRef = ref<FormInstance>();
const authStore = useAuthStore();
const formRules: FormRules = {
label: [
{ required: true, message: "请输入角色名称", trigger: "blur" },
{ min: 2, max: 24, message: "名称长度2-24字符", trigger: "blur" },
],
value: [
{ required: true, message: "请输入角色标识", trigger: "blur" },
{
pattern: /^[a-zA-Z0-9_]+$/,
message: "只能包含字母、数字或下划线",
trigger: "blur",
},
],
const editDialogVisible = ref(false);
const detailDialogVisible = ref(false);
const currentRole = ref<any>(null);
const currentRoleId = ref<number | null>(null);
// IDtenant_id
const getCurrentTenantId = () => {
// 1: authStore
if (authStore.user && authStore.user.tenant_id) {
return authStore.user.tenant_id;
}
// 2: localStorage
const userInfo = localStorage.getItem('userInfo');
if (userInfo) {
try {
const user = JSON.parse(userInfo);
return user.tenant_id || user.tenantId || null;
} catch (e) {
console.error('Failed to parse user info:', e);
}
}
return null;
};
//
function isSystemAdmin(row: any): boolean {
return row.roleCode === 'system_admin';
}
//
function formatDate(dateStr: string | null | undefined): string {
if (!dateStr) return '';
try {
const date = new Date(dateStr);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
} catch {
return dateStr as string;
}
}
//
async function fetchRoles() {
loading.value = true;
error.value = "";
try {
// TODO: API
await new Promise((r) => setTimeout(r, 300));
//
const all = [
{ id: 1, label: "系统管理员", value: "admin", remark: "拥有全部权限" },
{ id: 2, label: "普通用户", value: "user", remark: "普通访问权限" },
{ id: 3, label: "运营", value: "ops", remark: "运营相关权限" },
// ...
];
total.value = all.length;
roles.value = all.slice(
(page.value - 1) * pageSize.value,
page.value * pageSize.value
);
const tenantId = getCurrentTenantId();
const res = await getRoleByTenantId(tenantId);
if (res.code === 0 && res.data) {
roles.value = Array.isArray(res.data) ? res.data : [];
} else {
error.value = res.message || "获取角色列表失败";
roles.value = [];
}
} catch (err: any) {
error.value = err.message || "获取角色列表失败";
roles.value = [];
} finally {
loading.value = false;
}
}
const handlePageChange = (val: number) => {
page.value = val;
fetchRoles();
};
function resetRoleForm() {
roleForm.id = null;
roleForm.label = "";
roleForm.value = "";
roleForm.remark = "";
//
async function refresh() {
loading.value = true;
try {
await fetchRoles();
ElMessage.success('刷新成功');
} catch (error) {
ElMessage.error('刷新失败');
} finally {
loading.value = false;
}
}
//
function handleAdd() {
currentRole.value = null;
editDialogVisible.value = true;
}
//
function handleView(row: any) {
currentRoleId.value = row.roleId;
detailDialogVisible.value = true;
}
//
function handleEdit(row: any) {
isEditing.value = true;
roleForm.id = row.id;
roleForm.label = row.label;
roleForm.value = row.value;
roleForm.remark = row.remark;
showRoleDialog.value = true;
currentRole.value = { ...row };
editDialogVisible.value = true;
}
//
async function handleDelete(row: any) {
try {
await ElMessageBox.confirm(
`确定要删除角色「${row.label}」? 删除后不可恢复。`,
`确定要删除角色「${row.roleName}」吗?删除后不可恢复。`,
"警告",
{ type: "warning" }
);
// TODO: API
await new Promise((r) => setTimeout(r, 300));
ElMessage.success("删除成功");
fetchRoles();
loading.value = true;
try {
const res = await deleteRole(row.roleId);
if (res.code === 0) {
ElMessage.success("删除成功");
fetchRoles();
} else {
ElMessage.error(res.message || "删除失败");
}
} catch (err: any) {
ElMessage.error(err.message || "删除失败");
} finally {
loading.value = false;
}
} catch {
//
}
}
async function submitRoleForm() {
//
await roleFormRef.value?.validate();
loading.value = true;
try {
if (isEditing.value) {
// TODO: API
await new Promise((r) => setTimeout(r, 300));
ElMessage.success("角色更新成功");
} else {
// TODO: API
await new Promise((r) => setTimeout(r, 300));
ElMessage.success("角色添加成功");
}
showRoleDialog.value = false;
fetchRoles();
} catch (e: any) {
ElMessage.error(e.message || "操作失败,请重试");
} finally {
loading.value = false;
}
//
function handleEditSuccess() {
fetchRoles();
}
onMounted(() => {
@ -220,7 +246,6 @@ onMounted(() => {
</script>
<style scoped>
.loading-state,
.error-state {
display: flex;
flex-direction: column;
@ -228,48 +253,8 @@ onMounted(() => {
justify-content: center;
min-height: 220px;
padding: 32px 0 16px 0;
background: #f5f7fa;
background: var(--el-bg-color-page);
border-radius: 5px;
}
.loading-spinner {
width: 36px;
height: 36px;
border: 4px solid #e5e5e5;
border-top: 4px solid #409eff;
border-radius: 50%;
animation: loading-spin 0.8s linear infinite;
margin-bottom: 14px;
}
@keyframes loading-spin {
to {
transform: rotate(360deg);
}
}
.el-dialog__body {
padding-top: 18px !important;
}
.el-form .el-form-item {
margin-bottom: 16px;
}
.el-form .el-input {
width: 100%;
}
@media (max-width: 600px) {
.role-management-module {
padding: 10px 4px;
min-height: 300px;
}
.header-bar {
flex-direction: column;
align-items: stretch;
row-gap: 8px;
padding-bottom: 2px;
}
gap: 16px;
}
</style>

View File

@ -3,14 +3,14 @@
<div class="page-header">
<h2 class="page-title">租户管理</h2>
<div class="header-actions">
<el-button @click="fetchTenants" title="刷新租户列表" :loading="loading">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
添加租户
</el-button>
<el-button @click="refresh" :loading="loading">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
@ -24,7 +24,7 @@
<!-- 错误状态 -->
<div v-else-if="error" class="error-state">
<el-alert title="加载失败" :message="error" type="error" show-icon />
<el-button type="primary" @click="fetchTenants" style="margin-top: 16px">
<el-button type="primary" @click="refresh" style="margin-top: 16px">
重试
</el-button>
</div>
@ -37,6 +37,7 @@
stripe
style="width: 100%"
v-loading="loading"
element-loading-text="正在加载..."
>
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="name" label="租户名称" min-width="120" />
@ -203,6 +204,16 @@ const handlePageChange = (val: number) => {
fetchTenants();
};
//
async function refresh() {
try {
await fetchTenants();
ElMessage.success('刷新成功');
} catch (error) {
ElMessage.error('刷新失败');
}
}
//
function getAuditStatusType(
status: string

View File

@ -7,12 +7,16 @@
<el-icon><Plus /></el-icon>
添加用户
</el-button>
<el-button @click="refresh">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
<el-divider></el-divider>
<el-table :data="users" style="width: 100%">
<el-table :data="users" style="width: 100%" v-loading="loading">
<el-table-column
prop="username"
label="用户名"
@ -31,10 +35,10 @@
align="center"
min-width="200"
/>
<el-table-column prop="role" label="角色" width="120" align="center">
<el-table-column prop="role" label="角色" width="150" align="center">
<template #default="scope">
<el-tag :type="scope.row.role === 'admin' ? 'danger' : 'primary'">
{{ scope.row.role === "admin" ? "管理员" : "普通用户" }}
<el-tag :type="getRoleTagType(scope.row.roleName)">
{{ scope.row.roleName || '未分配角色' }}
</el-tag>
</template>
</el-table-column>
@ -123,9 +127,23 @@
<el-input v-model="form.email" />
</el-form-item>
<el-form-item label="角色" v-if="dialogTitle !== '修改密码'">
<el-select v-model="form.role">
<el-option label="管理员" value="admin" />
<el-option label="普通用户" value="user" />
<el-select
v-model="form.roleId"
placeholder="请选择角色"
style="width: 100%"
:loading="loadingRoles"
>
<el-option
v-for="role in roleList"
:key="role.roleId"
:label="role.roleName"
:value="role.roleId"
>
<span>{{ role.roleName }}</span>
<span style="color: #8492a6; font-size: 13px; margin-left: 8px;">
({{ role.roleCode }})
</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="状态" v-if="dialogTitle !== '修改密码'">
@ -146,7 +164,7 @@
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus } from "@element-plus/icons-vue";
import { Plus, Refresh } from "@element-plus/icons-vue";
import {
getAllUsers,
addUser,
@ -155,6 +173,8 @@ import {
getUserInfo,
changePassword,
} from "@/api/user";
import { getRoleByTenantId } from "@/api/role";
import { useAuthStore } from "@/stores/auth";
interface User {
id: number;
@ -167,11 +187,60 @@ interface User {
tenant_id: number;
}
const authStore = useAuthStore();
const page = ref(1);
const pageSize = ref(10);
const total = ref(0);
const users = ref<any[]>([]);
const roleList = ref<any[]>([]);
const loadingRoles = ref(false);
const loading = ref(false);
// ID
const getCurrentTenantId = () => {
if (authStore.user && authStore.user.tenant_id) {
return authStore.user.tenant_id;
}
const userInfo = localStorage.getItem('userInfo');
if (userInfo) {
try {
const user = JSON.parse(userInfo);
return user.tenant_id || user.tenantId || 0;
} catch (e) {
console.error('Failed to parse user info:', e);
}
}
return 0;
};
//
const fetchRoles = async () => {
loadingRoles.value = true;
try {
const tenantId = getCurrentTenantId();
const res = await getRoleByTenantId(tenantId);
if (res.code === 0 && res.data) {
roleList.value = Array.isArray(res.data) ? res.data : [];
} else {
roleList.value = [];
}
} catch (error: any) {
console.error('获取角色列表失败:', error);
roleList.value = [];
} finally {
loadingRoles.value = false;
}
};
//
const getRoleTagType = (roleName: string) => {
if (!roleName) return 'info';
if (roleName.includes('管理员')) return 'danger';
if (roleName.includes('用户')) return 'primary';
return 'success';
};
//
const validatePassword = (password: string) => {
@ -199,6 +268,7 @@ const validateConfirmPassword = (password: string, confirmPassword: string) => {
};
const fetchUsers = async () => {
loading.value = true;
try {
const res = await getAllUsers();
//
@ -213,33 +283,53 @@ const fetchUsers = async () => {
userList = res.data;
}
//
users.value = userList.map((item: any) => ({
id: item.id,
username: item.username,
nickname: item.nickname,
email: item.email,
role: item.role || (item.username === "admin" ? "admin" : "user"),
status: item.status || "active",
lastLoginTime: item.lastLoginTime
? new Date(item.lastLoginTime).toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
})
: "",
}));
users.value = userList.map((item: any) => {
//
let roleName = '';
let roleId = item.role_id || item.roleId || null;
if (roleId) {
const role = roleList.value.find(r => r.roleId === roleId);
roleName = role ? role.roleName : '';
} else if (item.role) {
// roleroleList
const role = roleList.value.find(r => r.roleCode === item.role);
roleName = role ? role.roleName : (item.role === 'admin' ? '管理员' : '普通用户');
}
return {
id: item.id,
username: item.username,
nickname: item.nickname,
email: item.email,
role: item.role || '',
roleId: roleId,
roleName: roleName,
status: item.status || "active",
lastLoginTime: item.lastLoginTime
? new Date(item.lastLoginTime).toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
})
: "",
};
});
total.value = users.value.length;
} catch (e) {
users.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
onMounted(() => {
onMounted(async () => {
//
await fetchRoles();
fetchUsers();
});
@ -258,8 +348,10 @@ const form = ref<any>({
nickname: "",
password: "",
email: "",
role: "user",
roleId: null,
role: "",
status: "active",
tenant_id: null,
});
const passwordForm = ref<any>({
oldPassword: "",
@ -274,6 +366,20 @@ const currentForm = computed(() => {
return dialogTitle.value === '修改密码' ? passwordForm.value : form.value;
});
//
const refresh = async () => {
loading.value = true;
try {
await fetchRoles();
await fetchUsers();
ElMessage.success('刷新成功');
} catch (error) {
ElMessage.error('刷新失败');
} finally {
loading.value = false;
}
};
const handleAddUser = () => {
dialogTitle.value = "添加用户";
isEdit.value = false;
@ -296,7 +402,8 @@ const handleAddUser = () => {
nickname: "",
password: "",
email: "",
role: "user",
roleId: null,
role: "",
status: "active",
tenant_id: tenantId,
};
@ -309,14 +416,28 @@ const handleEdit = async (user: User) => {
try {
const res = await getUserInfo(user.id);
const data = res.data || res;
// ID
const tenantId = getCurrentTenantId();
// ID
let roleId = data.role_id || data.roleId || null;
if (!roleId && data.role) {
// roleroleListroleId
const role = roleList.value.find(r => r.roleCode === data.role);
roleId = role ? role.roleId : null;
}
form.value = {
id: data.id,
username: data.username,
nickname: data.nickname,
password: "",
email: data.email,
role: data.role || "user",
roleId: roleId,
role: data.role || "",
status: data.status || "active",
tenant_id: data.tenant_id || tenantId,
};
} catch (e) {
ElMessage.error("加载用户失败");
@ -381,7 +502,26 @@ const submitForm = async () => {
passwordError.value = res.message || "密码修改失败";
}
} else if (isEdit.value) {
await editUser(form.value.id, form.value);
// 使roleId
const submitData: any = {
username: form.value.username,
nickname: form.value.nickname,
email: form.value.email,
status: form.value.status,
};
if (form.value.roleId) {
submitData.roleId = form.value.roleId;
} else if (form.value.role) {
// roleIdrole
submitData.role = form.value.role;
}
if (form.value.tenant_id) {
submitData.tenant_id = form.value.tenant_id;
}
await editUser(form.value.id, submitData);
ElMessage.success({
message: "更新成功",
type: "success",
@ -393,7 +533,26 @@ const submitForm = async () => {
dialogVisible.value = false;
fetchUsers();
} else {
await addUser(form.value);
// 使roleId
const submitData: any = {
username: form.value.username,
nickname: form.value.nickname,
password: form.value.password,
email: form.value.email,
status: form.value.status,
};
if (form.value.roleId) {
submitData.roleId = form.value.roleId;
} else if (form.value.role) {
submitData.role = form.value.role;
}
if (form.value.tenant_id) {
submitData.tenant_id = form.value.tenant_id;
}
await addUser(submitData);
ElMessage.success({
message: "添加成功",
type: "success",

327
server/controllers/role.go Normal file
View 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()
}

View File

@ -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;
```
然后再执行创建脚本。

View File

@ -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` 会自动管理,无需手动设置

View File

@ -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

View 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;

View File

@ -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
View 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
}

View File

@ -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")

View File

@ -1 +0,0 @@