完成知识库模块
This commit is contained in:
parent
6cd942fb29
commit
1062bcfb70
94
KNOWLEDGE_SETUP.md
Normal file
94
KNOWLEDGE_SETUP.md
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# 知识库功能快速设置指南
|
||||||
|
|
||||||
|
## 问题原因
|
||||||
|
|
||||||
|
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)
|
||||||
|
- ✅ 实时预览
|
||||||
|
- ✅ 分类和标签管理
|
||||||
|
- ✅ 搜索和分页
|
||||||
|
- ✅ 主题支持
|
||||||
|
- ✅ 响应式布局
|
||||||
|
|
||||||
631
front/package-lock.json
generated
631
front/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,6 +11,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
|
"@wangeditor/basic-modules": "^1.1.7",
|
||||||
|
"@wangeditor/editor": "^5.1.23",
|
||||||
|
"@wangeditor/list-module": "^1.0.5",
|
||||||
|
"@wangeditor/table-module": "^1.1.4",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"element-plus": "^2.10.7",
|
"element-plus": "^2.10.7",
|
||||||
|
|||||||
125
front/src/api/knowledge.ts
Normal file
125
front/src/api/knowledge.ts
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import axios from './index';
|
||||||
|
|
||||||
|
// 响应类型定义
|
||||||
|
interface ApiResponse<T = any> {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取知识列表
|
||||||
|
*/
|
||||||
|
export async function getKnowledgeList(params?: {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
status?: number;
|
||||||
|
categoryId?: number;
|
||||||
|
keyword?: string;
|
||||||
|
}): Promise<any> {
|
||||||
|
const response: ApiResponse = await axios.get('/api/knowledge/list', { params });
|
||||||
|
if (response.code === 0) {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
throw new Error(response.message || '获取列表失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取知识详情
|
||||||
|
*/
|
||||||
|
export async function getKnowledgeDetail(id: string | number): Promise<any> {
|
||||||
|
const response: ApiResponse = await axios.get('/api/knowledge/detail', {
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
if (response.code === 0) {
|
||||||
|
return { data: response.data };
|
||||||
|
}
|
||||||
|
throw new Error(response.message || '获取详情失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建知识
|
||||||
|
*/
|
||||||
|
export async function createKnowledge(data: any): Promise<any> {
|
||||||
|
const response: ApiResponse = await axios.post('/api/knowledge/create', data);
|
||||||
|
if (response.code === 0) {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
throw new Error(response.message || '创建失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新知识
|
||||||
|
*/
|
||||||
|
export async function updateKnowledge(id: string | number, data: any): Promise<any> {
|
||||||
|
const response: ApiResponse = await axios.post('/api/knowledge/update', {
|
||||||
|
id,
|
||||||
|
...data
|
||||||
|
});
|
||||||
|
if (response.code === 0) {
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
throw new Error(response.message || '更新失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除知识
|
||||||
|
*/
|
||||||
|
export async function deleteKnowledge(id: string | number): Promise<any> {
|
||||||
|
const response: ApiResponse = await axios.post('/api/knowledge/delete', { id });
|
||||||
|
if (response.code === 0) {
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
throw new Error(response.message || '删除失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分类列表
|
||||||
|
*/
|
||||||
|
export async function getCategoryList(): Promise<any> {
|
||||||
|
const response: ApiResponse = await axios.get('/api/knowledge/categories');
|
||||||
|
if (response.code === 0) {
|
||||||
|
const categories = response.data || [];
|
||||||
|
return { data: categories };
|
||||||
|
}
|
||||||
|
throw new Error(response.message || '获取分类失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取标签列表
|
||||||
|
*/
|
||||||
|
export async function getTagList(): Promise<any> {
|
||||||
|
const response: ApiResponse = await axios.get('/api/knowledge/tags');
|
||||||
|
if (response.code === 0) {
|
||||||
|
// 转换为名称数组格式,兼容前端使用
|
||||||
|
const tags = response.data || [];
|
||||||
|
return {
|
||||||
|
data: tags.map((tag: any) => tag.tagName || tag.tag_name)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(response.message || '获取标签失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加分类
|
||||||
|
*/
|
||||||
|
export async function addCategory(data: {
|
||||||
|
categoryName: string;
|
||||||
|
categoryDesc?: string;
|
||||||
|
parentId?: number;
|
||||||
|
sortOrder?: number;
|
||||||
|
}): Promise<any> {
|
||||||
|
const response: ApiResponse = await axios.post('/api/knowledge/category/add', data);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加标签
|
||||||
|
*/
|
||||||
|
export async function addTag(data: {
|
||||||
|
tagName: string;
|
||||||
|
tagColor?: string;
|
||||||
|
}): Promise<any> {
|
||||||
|
const response: ApiResponse = await axios.post('/api/knowledge/tag/add', data);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
@ -71,6 +71,7 @@ $transition-base: all 0.3s cubic-bezier(.25,.8,.25,1);
|
|||||||
|
|
||||||
// 组件颜色
|
// 组件颜色
|
||||||
--card-bg: #ffffff;
|
--card-bg: #ffffff;
|
||||||
|
--card-bg1: #1890ff;
|
||||||
--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
--sidebar-bg: #f8f9fa;
|
--sidebar-bg: #f8f9fa;
|
||||||
--sidebar-hover: #e9ecef;
|
--sidebar-hover: #e9ecef;
|
||||||
@ -146,6 +147,7 @@ $transition-base: all 0.3s cubic-bezier(.25,.8,.25,1);
|
|||||||
|
|
||||||
// 组件颜色
|
// 组件颜色
|
||||||
--card-bg: #2d2d2d;
|
--card-bg: #2d2d2d;
|
||||||
|
--card-bg1: #2d2d2d;
|
||||||
--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
--sidebar-bg: #1f1f1f;
|
--sidebar-bg: #1f1f1f;
|
||||||
--sidebar-hover: #3d3d3d;
|
--sidebar-hover: #3d3d3d;
|
||||||
|
|||||||
@ -27,3 +27,177 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 标准文章样式 */
|
||||||
|
.markdown-body {
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.8;
|
||||||
|
word-break: break-word;
|
||||||
|
background: var(--content-bg);
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
color: var(--title-color, var(--text-color));
|
||||||
|
margin-top: 1.6em;
|
||||||
|
margin-bottom: 0.8em;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 2.2em;
|
||||||
|
border-bottom: 1px solid #eaecef;
|
||||||
|
padding-bottom: 0.4em;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.8em;
|
||||||
|
border-bottom: 1px solid #eaecef;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
h5 {
|
||||||
|
font-size: 1.06em;
|
||||||
|
}
|
||||||
|
h6 {
|
||||||
|
font-size: 1em;
|
||||||
|
color: #969696;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0.9em 0;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
padding-left: 2em;
|
||||||
|
margin: 0.9em 0;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
margin: 0.7em 0 0.7em 0.7em;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--primary-color, #2d8cf0);
|
||||||
|
text-decoration: underline;
|
||||||
|
transition: color 0.2s;
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary-hover, #1a73e8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
margin: 1em auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f7f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 4px solid #d0dde9;
|
||||||
|
background: #f7f9fa;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
margin: 1.1em 0;
|
||||||
|
color: #6580a0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: var(
|
||||||
|
--code-font,
|
||||||
|
"Fira Mono",
|
||||||
|
"Menlo",
|
||||||
|
"Consolas",
|
||||||
|
"monospace"
|
||||||
|
);
|
||||||
|
background: #f4f4f4;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.15em 0.4em;
|
||||||
|
color: #c7254e;
|
||||||
|
font-size: 0.97em;
|
||||||
|
margin: 0 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: #f6f8fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1em 1.2em;
|
||||||
|
font-family: var(
|
||||||
|
--code-font,
|
||||||
|
"Fira Mono",
|
||||||
|
"Menlo",
|
||||||
|
"Consolas",
|
||||||
|
"monospace"
|
||||||
|
);
|
||||||
|
font-size: 0.98em;
|
||||||
|
overflow-x: auto;
|
||||||
|
color: #212529;
|
||||||
|
margin: 1.2em 0;
|
||||||
|
code {
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 1.2em 0;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 1em;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: #f4f8fb;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background: #f9fbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #eaecef;
|
||||||
|
margin: 2em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
br {
|
||||||
|
display: block;
|
||||||
|
margin: 0.2em 0;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容 Element 表单
|
||||||
|
.el-form-item__label,
|
||||||
|
.el-form-item__content,
|
||||||
|
.el-form-item__error,
|
||||||
|
.el-form-item__error-tip {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
126
front/src/components/WangEditor.vue
Normal file
126
front/src/components/WangEditor.vue
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<div class="wang-editor-wrapper">
|
||||||
|
<div ref="toolbarRef" class="toolbar-container"></div>
|
||||||
|
<div ref="editorRef" class="editor-container"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||||
|
import '@wangeditor/editor/dist/css/style.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const toolbarRef = ref<HTMLDivElement>();
|
||||||
|
const editorRef = ref<HTMLDivElement>();
|
||||||
|
let editorInstance: any = null;
|
||||||
|
let isDestroyed = false;
|
||||||
|
|
||||||
|
// 初始化编辑器
|
||||||
|
const initEditor = async () => {
|
||||||
|
if (!editorRef.value || !toolbarRef.value || isDestroyed) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 动态导入 wangEditor
|
||||||
|
const { createEditor, createToolbar } = await import('@wangeditor/editor');
|
||||||
|
|
||||||
|
const editorConfig = {
|
||||||
|
placeholder: '请输入内容...',
|
||||||
|
onChange: (editor: any) => {
|
||||||
|
if (!isDestroyed) {
|
||||||
|
const html = editor.getHtml();
|
||||||
|
emit('update:modelValue', html);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建编辑器
|
||||||
|
editorInstance = createEditor({
|
||||||
|
selector: editorRef.value,
|
||||||
|
html: props.modelValue || '',
|
||||||
|
config: editorConfig,
|
||||||
|
mode: 'default',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建工具栏
|
||||||
|
createToolbar({
|
||||||
|
editor: editorInstance,
|
||||||
|
selector: toolbarRef.value,
|
||||||
|
config: {},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize editor:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听外部值变化
|
||||||
|
watch(() => props.modelValue, (newVal) => {
|
||||||
|
if (editorInstance && newVal !== editorInstance.getHtml()) {
|
||||||
|
editorInstance.setHtml(newVal || '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 暴露方法
|
||||||
|
defineExpose({
|
||||||
|
clear: () => {
|
||||||
|
if (editorInstance) {
|
||||||
|
editorInstance.clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getContent: () => {
|
||||||
|
if (editorInstance) {
|
||||||
|
return editorInstance.getHtml();
|
||||||
|
}
|
||||||
|
return props.modelValue;
|
||||||
|
},
|
||||||
|
setContent: (content: string) => {
|
||||||
|
if (editorInstance) {
|
||||||
|
editorInstance.setHtml(content || '');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
initEditor();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
isDestroyed = true;
|
||||||
|
if (editorInstance) {
|
||||||
|
editorInstance.destroy();
|
||||||
|
editorInstance = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.wang-editor-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-container {
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container {
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@ -14,6 +14,9 @@ import router from './router';
|
|||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||||
|
|
||||||
|
// 导入全局组件
|
||||||
|
import WangEditor from '@/components/WangEditor.vue';
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
// 注册 Element Plus
|
// 注册 Element Plus
|
||||||
@ -24,6 +27,9 @@ for (const [key, component] of Object.entries(Icons)) {
|
|||||||
app.component(key, component);
|
app.component(key, component);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 全局注册 WangEditor 组件
|
||||||
|
app.component('WangEditor', WangEditor);
|
||||||
|
|
||||||
// 创建 Pinia 实例
|
// 创建 Pinia 实例
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
pinia.use(piniaPluginPersistedstate);
|
pinia.use(piniaPluginPersistedstate);
|
||||||
|
|||||||
@ -74,7 +74,7 @@ class DynamicRouteManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const menus = response.data;
|
const menus = response.data;
|
||||||
// console.log('从数据库获取到菜单数据:', menus);
|
// // console.log('从数据库获取到菜单数据:', menus);
|
||||||
|
|
||||||
// 获取主布局路由
|
// 获取主布局路由
|
||||||
const mainRoute = router.getRoutes().find(route => route.name === 'main');
|
const mainRoute = router.getRoutes().find(route => route.name === 'main');
|
||||||
@ -90,7 +90,7 @@ class DynamicRouteManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.routesLoaded = true;
|
this.routesLoaded = true;
|
||||||
// console.log("动态路由生成完成,共添加", menus.length, "个菜单路由");
|
// // console.log("动态路由生成完成,共添加", menus.length, "个菜单路由");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("从数据库生成动态路由失败:", error);
|
console.error("从数据库生成动态路由失败:", error);
|
||||||
// 即使失败也标记为已加载,避免重复尝试
|
// 即使失败也标记为已加载,避免重复尝试
|
||||||
@ -125,7 +125,48 @@ class DynamicRouteManager {
|
|||||||
|
|
||||||
// 检查路由是否已存在
|
// 检查路由是否已存在
|
||||||
if (router.hasRoute(routeName)) {
|
if (router.hasRoute(routeName)) {
|
||||||
// console.log(`路由已存在,跳过: ${menu.Path}`);
|
// // console.log(`路由已存在,跳过: ${menu.Path}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 特殊处理:detail/edit 路由作为独立路由,全屏显示
|
||||||
|
if (menu.Path.includes('/detail') || menu.Path.includes('/edit')) {
|
||||||
|
// 尝试获取组件
|
||||||
|
const component = await this.getComponentForMenu(menu);
|
||||||
|
if (!component) {
|
||||||
|
console.warn(`未找到组件,跳过: ${menu.Path}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保持原始路径格式(确保以 / 开头),并添加动态参数
|
||||||
|
let routePath = menu.Path;
|
||||||
|
if (!routePath.startsWith('/')) {
|
||||||
|
routePath = '/' + routePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查路径中是否已包含动态参数,如果没有则添加 :id 参数
|
||||||
|
if (!routePath.includes(':id')) {
|
||||||
|
routePath = routePath + '/:id';
|
||||||
|
}
|
||||||
|
|
||||||
|
const childRoute = {
|
||||||
|
path: routePath,
|
||||||
|
name: routeName,
|
||||||
|
component: component,
|
||||||
|
props: true, // 启用 props 传递,使组件可以直接访问路由参数
|
||||||
|
meta: {
|
||||||
|
title: menu.Name,
|
||||||
|
fullScreen: true, // 标记为全屏路由
|
||||||
|
menuId: menu.Id,
|
||||||
|
path: menu.Path,
|
||||||
|
icon: menu.Icon,
|
||||||
|
isExternal: menu.IsExternal === 1,
|
||||||
|
externalUrl: menu.ExternalUrl,
|
||||||
|
parentId: menu.ParentId
|
||||||
|
},
|
||||||
|
};
|
||||||
|
router.addRoute('main', childRoute);
|
||||||
|
// console.log(`✅ 知识库路由已注册: ${routePath}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,7 +223,7 @@ class DynamicRouteManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
router.addRoute('main', childRoute);
|
router.addRoute('main', childRoute);
|
||||||
// console.log(`✅ 动态路由已添加: ${menu.Path} -> ${menu.Name}`);
|
// // console.log(`✅ 动态路由已添加: ${menu.Path} -> ${menu.Name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据菜单信息获取组件
|
// 根据菜单信息获取组件
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
<div class="knowledge-detail">
|
<div class="knowledge-detail">
|
||||||
<!-- 顶部标题栏 -->
|
<!-- 顶部标题栏 -->
|
||||||
<div class="detail-header">
|
<div class="detail-header">
|
||||||
<el-button type="text" icon="el-icon-arrow-left" @click="goBack">返回</el-button>
|
<el-button type="text" @click="goBack"
|
||||||
|
><i class="fas fa-arrow-left"></i> 返回</el-button
|
||||||
|
>
|
||||||
<h2>{{ knowledgeTitle }}</h2>
|
<h2>{{ knowledgeTitle }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -11,23 +13,25 @@
|
|||||||
<!-- 左侧信息面板 -->
|
<!-- 左侧信息面板 -->
|
||||||
<div class="info-panel">
|
<div class="info-panel">
|
||||||
<el-card shadow="never">
|
<el-card shadow="never">
|
||||||
<div slot="header">
|
<div slot="header" class="header">
|
||||||
<span>基本信息</span>
|
<span>基本信息</span>
|
||||||
</div>
|
</div>
|
||||||
|
<el-divider />
|
||||||
<el-form label-width="80px" size="small">
|
<el-form label-width="80px" size="small">
|
||||||
<el-form-item label="标题:">
|
<el-form-item label="标题:">
|
||||||
<span>{{ formData.title }}</span>
|
<span>{{ formData.title }}</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="分类:">
|
<el-form-item label="分类:">
|
||||||
<el-tag size="mini">{{ formData.category }}</el-tag>
|
<el-tag size="small">{{ formData.category }}</el-tag>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="标签:">
|
<el-form-item label="标签:">
|
||||||
<el-tag
|
<el-tag
|
||||||
v-for="tag in formData.tags"
|
v-for="tag in formData.tags"
|
||||||
:key="tag"
|
:key="tag"
|
||||||
size="mini"
|
size="small"
|
||||||
style="margin-right: 4px"
|
style="margin-right: 4px"
|
||||||
>{{ tag }}</el-tag>
|
>{{ tag }}</el-tag
|
||||||
|
>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="作者:">
|
<el-form-item label="作者:">
|
||||||
<span>{{ formData.author }}</span>
|
<span>{{ formData.author }}</span>
|
||||||
@ -39,13 +43,16 @@
|
|||||||
<span>{{ formData.updateTime }}</span>
|
<span>{{ formData.updateTime }}</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
<el-form label-width="80px" size="small" align="center">
|
||||||
|
<el-divider />
|
||||||
|
<el-button type="primary" @click="handleEdit"
|
||||||
|
><i class="fas fa-edit"></i> 编辑</el-button
|
||||||
|
>
|
||||||
|
<el-button type="danger" @click="handleDelete"
|
||||||
|
><i class="fas fa-trash"></i> 删除</el-button
|
||||||
|
>
|
||||||
|
</el-form>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
|
||||||
<div class="actions">
|
|
||||||
<el-button type="primary" icon="el-icon-edit" @click="handleEdit">编辑</el-button>
|
|
||||||
<el-button type="danger" icon="el-icon-delete" @click="handleDelete">删除</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧内容面板 -->
|
<!-- 右侧内容面板 -->
|
||||||
@ -61,110 +68,154 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts" setup>
|
||||||
import { marked } from 'marked'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { getKnowledgeDetail, deleteKnowledge } from '@/api/knowledge'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { marked } from "marked"
|
||||||
|
import { getKnowledgeDetail, deleteKnowledge } from "@/api/knowledge"
|
||||||
|
|
||||||
export default {
|
const route = useRoute()
|
||||||
name: 'KnowledgeDetail',
|
const router = useRouter()
|
||||||
props: {
|
|
||||||
id: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
knowledgeTitle: '知识详情',
|
|
||||||
formData: {
|
|
||||||
title: '',
|
|
||||||
category: '',
|
|
||||||
tags: [],
|
|
||||||
author: '',
|
|
||||||
createTime: '',
|
|
||||||
updateTime: '',
|
|
||||||
content: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
compiledMarkdown() {
|
|
||||||
return marked(this.formData.content || '')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.fetchDetail()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
// 获取详情
|
|
||||||
async fetchDetail() {
|
|
||||||
try {
|
|
||||||
const res = await getKnowledgeDetail(this.id)
|
|
||||||
this.formData = res.data
|
|
||||||
} catch (e) {
|
|
||||||
this.$message.error('获取详情失败')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 返回列表
|
const knowledgeTitle = ref('知识详情')
|
||||||
goBack() {
|
|
||||||
this.$router.push('/apps/knowledge')
|
|
||||||
},
|
|
||||||
|
|
||||||
// 编辑
|
interface FormData {
|
||||||
handleEdit() {
|
title: string,
|
||||||
this.$router.push(`/apps/knowledge/edit/${this.id}`)
|
category: string,
|
||||||
},
|
tags: string[],
|
||||||
|
author: string,
|
||||||
|
createTime: string,
|
||||||
|
updateTime: string,
|
||||||
|
content: string,
|
||||||
|
}
|
||||||
|
const formData = ref<FormData>({
|
||||||
|
title: "",
|
||||||
|
category: "",
|
||||||
|
tags: [],
|
||||||
|
author: "",
|
||||||
|
createTime: "",
|
||||||
|
updateTime: "",
|
||||||
|
content: "",
|
||||||
|
})
|
||||||
|
|
||||||
// 删除
|
const id = computed(() => {
|
||||||
handleDelete() {
|
const queryId = route.query.id
|
||||||
this.$confirm('确认删除该知识?', '提示', {
|
const paramId = route.params.id
|
||||||
confirmButtonText: '确定',
|
if (queryId) return Array.isArray(queryId) ? queryId[0] : queryId
|
||||||
cancelButtonText: '取消',
|
if (paramId) return Array.isArray(paramId) ? paramId[0] : paramId
|
||||||
type: 'warning'
|
return ''
|
||||||
}).then(async () => {
|
})
|
||||||
try {
|
|
||||||
await deleteKnowledge(this.id)
|
const compiledMarkdown = computed(() => marked(formData.value.content || ""))
|
||||||
this.$message.success('删除成功')
|
|
||||||
this.goBack()
|
function parseTags(tagsStr: string): string[] {
|
||||||
} catch (e) {
|
if (!tagsStr) return []
|
||||||
this.$message.error('删除失败')
|
try {
|
||||||
}
|
const parsed = JSON.parse(tagsStr)
|
||||||
})
|
return Array.isArray(parsed) ? parsed : []
|
||||||
}
|
} catch {
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchDetail() {
|
||||||
|
try {
|
||||||
|
const idValue = id.value as string | number
|
||||||
|
const res = await getKnowledgeDetail(idValue)
|
||||||
|
const data = res.data
|
||||||
|
formData.value = {
|
||||||
|
title: data.title || '',
|
||||||
|
category: data.categoryName || '',
|
||||||
|
tags: parseTags(data.tags),
|
||||||
|
author: data.author || '',
|
||||||
|
createTime: data.createTime || '',
|
||||||
|
updateTime: data.updateTime || '',
|
||||||
|
content: data.content || '',
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error("获取详情失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMounted(fetchDetail)
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
router.push("/apps/knowledge")
|
||||||
|
}
|
||||||
|
function handleEdit() {
|
||||||
|
router.push({
|
||||||
|
name: "apps-knowledge-edit",
|
||||||
|
params: { id: id.value as string }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function handleDelete() {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
"确认删除该知识?",
|
||||||
|
"提示",
|
||||||
|
{
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning",
|
||||||
|
}
|
||||||
|
).then(async () => {
|
||||||
|
try {
|
||||||
|
const idValue = id.value as string | number
|
||||||
|
await deleteKnowledge(idValue)
|
||||||
|
ElMessage.success("删除成功")
|
||||||
|
goBack()
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error("删除失败")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="scss">
|
||||||
|
/* 全屏显示,覆盖父容器 */
|
||||||
.knowledge-detail {
|
.knowledge-detail {
|
||||||
padding: 16px;
|
position: fixed;
|
||||||
background: #f5f5f5;
|
top: 81px; /* 减去 header 高度 */
|
||||||
min-height: 100vh;
|
left: 160px; /* 侧边栏宽度,如果收起的活是 64px */
|
||||||
|
right: 0;
|
||||||
|
bottom: 81px; /* 减去 footer 高度 */
|
||||||
|
z-index: 1000;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--background-color);
|
||||||
|
padding: 24px;
|
||||||
|
transition: left var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-header {
|
.detail-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 20px;
|
||||||
background: #fff;
|
background: var(--card-bg);
|
||||||
padding: 12px 16px;
|
padding: 16px 24px;
|
||||||
border-radius: 4px;
|
border-radius: var(--border-radius-lg);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-header h2 {
|
.detail-header h2 {
|
||||||
margin: 0 0 0 12px;
|
margin: 0 0 0 12px;
|
||||||
font-size: 18px;
|
font-size: 20px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-body {
|
.detail-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 20px;
|
||||||
|
/* padding: 0 24px 0 24px; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-panel {
|
.info-panel {
|
||||||
width: 320px;
|
width: 320px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.header{
|
||||||
|
// margin-bottom: 14px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-panel {
|
.content-panel {
|
||||||
@ -179,5 +230,26 @@ export default {
|
|||||||
.markdown-body {
|
.markdown-body {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式布局 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.detail-body {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header h2 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
462
front/src/views/apps/knowledge/components/edit.vue
Normal file
462
front/src/views/apps/knowledge/components/edit.vue
Normal file
@ -0,0 +1,462 @@
|
|||||||
|
<template>
|
||||||
|
<div class="knowledge-edit">
|
||||||
|
<!-- 顶部标题栏 -->
|
||||||
|
<div class="edit-header">
|
||||||
|
<el-button type="text" @click="goBack"
|
||||||
|
><i class="fas fa-arrow-left"></i> 返回</el-button
|
||||||
|
>
|
||||||
|
<h2>{{ isEdit ? "编辑知识" : "新建知识" }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<div class="edit-meta">
|
||||||
|
<el-card shadow="never">
|
||||||
|
<el-form
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
ref="formRef"
|
||||||
|
label-width="80px"
|
||||||
|
size="default"
|
||||||
|
>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item label="标题:" prop="title">
|
||||||
|
<el-input v-model="formData.title" placeholder="请输入标题" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item label="分类:" prop="category">
|
||||||
|
<el-select
|
||||||
|
v-model="formData.category"
|
||||||
|
placeholder="请选择分类"
|
||||||
|
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">
|
||||||
|
<el-form-item label="标签:" prop="tags">
|
||||||
|
<el-select
|
||||||
|
v-model="formData.tags"
|
||||||
|
multiple
|
||||||
|
placeholder="请选择标签"
|
||||||
|
style="width: 100%"
|
||||||
|
allow-create
|
||||||
|
default-first-option
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="tag in tagList"
|
||||||
|
:key="tag"
|
||||||
|
:label="tag"
|
||||||
|
:value="tag"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item label="作者:" prop="author">
|
||||||
|
<el-input v-model="formData.author" placeholder="请输入作者" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
<el-divider />
|
||||||
|
<div class="meta-actions">
|
||||||
|
<el-button type="primary" @click="handleSubmit"
|
||||||
|
><i class="fas fa-save"></i> 保存</el-button
|
||||||
|
>
|
||||||
|
<el-button @click="goBack">取消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 正文内容区 - 左右结构 -->
|
||||||
|
<div class="edit-body">
|
||||||
|
<!-- 左侧编辑器 -->
|
||||||
|
<div class="editor-panel">
|
||||||
|
<el-card shadow="never">
|
||||||
|
<WangEditor v-model="formData.content" />
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
<!-- 右侧预览 -->
|
||||||
|
<div class="preview-panel">
|
||||||
|
<el-card shadow="never">
|
||||||
|
<div slot="header">
|
||||||
|
<span>预览效果</span>
|
||||||
|
</div>
|
||||||
|
<div class="markdown-body" v-html="compiledMarkdown"></div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed, onMounted, watch } from "vue";
|
||||||
|
import { useRouter, useRoute } from "vue-router";
|
||||||
|
import { marked } from "marked";
|
||||||
|
import {
|
||||||
|
getKnowledgeDetail,
|
||||||
|
updateKnowledge,
|
||||||
|
createKnowledge,
|
||||||
|
getCategoryList,
|
||||||
|
getTagList,
|
||||||
|
} from "@/api/knowledge";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import type { FormInstance, FormRules } from "element-plus";
|
||||||
|
|
||||||
|
// 表单 DOM 引用
|
||||||
|
const formRef = ref<FormInstance>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formData = reactive<{
|
||||||
|
title: string;
|
||||||
|
category: string;
|
||||||
|
categoryId: number;
|
||||||
|
tags: string[];
|
||||||
|
author: string;
|
||||||
|
content: string;
|
||||||
|
}>({
|
||||||
|
title: "",
|
||||||
|
category: "",
|
||||||
|
categoryId: 0,
|
||||||
|
tags: [],
|
||||||
|
author: "",
|
||||||
|
content: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 校验规则
|
||||||
|
const rules = reactive<FormRules>({
|
||||||
|
title: [{ required: true, message: "请输入标题", trigger: "blur" }],
|
||||||
|
category: [{ required: true, message: "请选择分类", trigger: "change" }],
|
||||||
|
// tags: [
|
||||||
|
// { type: "array", required: true, message: "请选择标签", trigger: "change" },
|
||||||
|
// ],
|
||||||
|
author: [{ required: true, message: "请输入作者", trigger: "blur" }],
|
||||||
|
content: [{ required: true, message: "请输入正文", trigger: "blur" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分类与标签数据
|
||||||
|
interface CategoryItem {
|
||||||
|
categoryId: number;
|
||||||
|
categoryName: string;
|
||||||
|
}
|
||||||
|
const categoryList = ref<CategoryItem[]>([]);
|
||||||
|
const tagList = ref<string[]>([]);
|
||||||
|
|
||||||
|
// id: 编辑时有,新增为 'new'
|
||||||
|
const id = computed(() => route.params.id || route.query.id);
|
||||||
|
const isEdit = computed(() => {
|
||||||
|
const currentId = id.value;
|
||||||
|
return !!currentId && currentId !== "new" && currentId !== "";
|
||||||
|
});
|
||||||
|
|
||||||
|
// markdown 预览
|
||||||
|
const compiledMarkdown = computed(() => marked(formData.content || ""));
|
||||||
|
|
||||||
|
// TODO: 后续可以集成 wangEditor
|
||||||
|
// 暂时使用 textarea,监听内容变化
|
||||||
|
watch(
|
||||||
|
() => formData.content,
|
||||||
|
() => {
|
||||||
|
// 内容变化时自动更新预览
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 获取登录用户信息
|
||||||
|
const getLoginUser = () => {
|
||||||
|
const userStr = localStorage.getItem("user");
|
||||||
|
if (userStr) {
|
||||||
|
try {
|
||||||
|
const user = JSON.parse(userStr);
|
||||||
|
return user.username || user.name || user.userName || "";
|
||||||
|
} catch (e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取详情
|
||||||
|
const fetchDetail = async () => {
|
||||||
|
try {
|
||||||
|
const currentId = id.value as string;
|
||||||
|
if (currentId && currentId !== "new") {
|
||||||
|
const res = await getKnowledgeDetail(currentId);
|
||||||
|
const data = res.data;
|
||||||
|
|
||||||
|
// 映射后端数据到前端表单
|
||||||
|
// categoryName 是从数据库联查得到的分类名称
|
||||||
|
formData.title = data.title || "";
|
||||||
|
formData.category = data.categoryName || "";
|
||||||
|
formData.categoryId = data.categoryId || 0;
|
||||||
|
formData.author = data.author || "";
|
||||||
|
formData.content = data.content || "";
|
||||||
|
|
||||||
|
// 如果没有 categoryId,尝试根据 categoryName 查找
|
||||||
|
if (!formData.categoryId && formData.category) {
|
||||||
|
const foundCategory = categoryList.value.find(
|
||||||
|
(item) => item.categoryName === formData.category
|
||||||
|
);
|
||||||
|
if (foundCategory) {
|
||||||
|
formData.categoryId = foundCategory.categoryId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags 可能是 JSON 字符串,需要解析
|
||||||
|
if (data.tags) {
|
||||||
|
try {
|
||||||
|
formData.tags = JSON.parse(data.tags);
|
||||||
|
} catch {
|
||||||
|
// 如果解析失败,尝试作为字符串数组处理
|
||||||
|
formData.tags = Array.isArray(data.tags) ? data.tags : [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
formData.tags = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error("获取详情失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取分类和标签
|
||||||
|
const loadCategoryAndTag = async () => {
|
||||||
|
try {
|
||||||
|
const [catRes, tagResRes] = await Promise.all([
|
||||||
|
getCategoryList
|
||||||
|
? getCategoryList()
|
||||||
|
: Promise.resolve({ data: [] }),
|
||||||
|
getTagList ? getTagList() : Promise.resolve({ data: [] }),
|
||||||
|
]);
|
||||||
|
categoryList.value = catRes.data || [];
|
||||||
|
tagList.value = tagResRes.data || [];
|
||||||
|
} catch (e) {
|
||||||
|
categoryList.value = [];
|
||||||
|
tagList.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理分类变化
|
||||||
|
const handleCategoryChange = (categoryName: string) => {
|
||||||
|
const selectedCategory = categoryList.value.find(
|
||||||
|
(item) => item.categoryName === categoryName
|
||||||
|
);
|
||||||
|
if (selectedCategory) {
|
||||||
|
formData.categoryId = selectedCategory.categoryId;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回
|
||||||
|
const goBack = () => {
|
||||||
|
router.push("/apps/knowledge");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!formRef.value) return;
|
||||||
|
formRef.value.validate(async (valid: boolean) => {
|
||||||
|
if (!valid) return;
|
||||||
|
|
||||||
|
const currentId = id.value as string;
|
||||||
|
|
||||||
|
// 准备提交的数据(符合后端格式)
|
||||||
|
const submitData = {
|
||||||
|
id: isEdit.value ? parseInt(currentId as string) : 0,
|
||||||
|
title: formData.title,
|
||||||
|
categoryId: formData.categoryId,
|
||||||
|
author: formData.author,
|
||||||
|
content: formData.content,
|
||||||
|
tags: JSON.stringify(formData.tags), // 转换为 JSON 字符串
|
||||||
|
status: 1, // 默认已发布
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEdit.value && currentId !== "new") {
|
||||||
|
// 编辑
|
||||||
|
try {
|
||||||
|
await updateKnowledge(currentId, submitData);
|
||||||
|
ElMessage.success("保存成功");
|
||||||
|
goBack();
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e.message || "保存失败");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 新建
|
||||||
|
try {
|
||||||
|
await createKnowledge(submitData);
|
||||||
|
ElMessage.success("创建成功");
|
||||||
|
goBack();
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e.message || "创建失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(async () => {
|
||||||
|
// 获取作者信息
|
||||||
|
const author = getLoginUser();
|
||||||
|
if (author && !isEdit.value) {
|
||||||
|
formData.author = author;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先加载分类和标签
|
||||||
|
await loadCategoryAndTag();
|
||||||
|
|
||||||
|
// 如果是编辑模式,获取详情
|
||||||
|
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({
|
||||||
|
formRef,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.knowledge-edit {
|
||||||
|
position: fixed;
|
||||||
|
top: 81px;
|
||||||
|
left: 160px;
|
||||||
|
right: 0;
|
||||||
|
bottom: 81px;
|
||||||
|
z-index: 1000;
|
||||||
|
background: var(--background-color);
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
transition: left var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-header h2 {
|
||||||
|
margin: 0 0 0 12px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 基本信息区 */
|
||||||
|
.edit-meta {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-actions {
|
||||||
|
margin-top: 16px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 正文编辑区 - 左右结构 */
|
||||||
|
.edit-body {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
height: calc(100vh - 320px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-panel {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 编辑器容器样式 */
|
||||||
|
.editor-panel {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: var(--text-color);
|
||||||
|
min-height: 400px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :deep(h1),
|
||||||
|
.markdown-body :deep(h2),
|
||||||
|
.markdown-body :deep(h3),
|
||||||
|
.markdown-body :deep(h4),
|
||||||
|
.markdown-body :deep(h5),
|
||||||
|
.markdown-body :deep(h6) {
|
||||||
|
color: var(--text-color);
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :deep(p) {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :deep(code) {
|
||||||
|
background-color: var(--background-hover);
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :deep(pre) {
|
||||||
|
background-color: var(--background-hover);
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式布局 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.edit-body {
|
||||||
|
flex-direction: column;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-panel,
|
||||||
|
.preview-panel {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-header h2 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep() {
|
||||||
|
.el-form-item--default {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -11,7 +11,7 @@
|
|||||||
<el-select
|
<el-select
|
||||||
v-model="searchType"
|
v-model="searchType"
|
||||||
placeholder="知识库"
|
placeholder="知识库"
|
||||||
size="medium"
|
size="default"
|
||||||
class="search-select"
|
class="search-select"
|
||||||
style="width: auto"
|
style="width: auto"
|
||||||
>
|
>
|
||||||
@ -20,13 +20,13 @@
|
|||||||
<el-input
|
<el-input
|
||||||
v-model="keyword"
|
v-model="keyword"
|
||||||
placeholder="请输入关键字、产品编码进行查询"
|
placeholder="请输入关键字、产品编码进行查询"
|
||||||
size="medium"
|
size="default"
|
||||||
class="search-input"
|
class="search-input"
|
||||||
@keyup.enter.native="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
/>
|
/>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="medium"
|
size="default"
|
||||||
class="search-button"
|
class="search-button"
|
||||||
@click="handleSearch"
|
@click="handleSearch"
|
||||||
>
|
>
|
||||||
@ -60,24 +60,13 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
class="stat-col"
|
class="stat-col"
|
||||||
>
|
>
|
||||||
<div class="stat-card" :style="{ '--stat-color': stat.color }">
|
<div class="stat-card">
|
||||||
<div class="stat-icon">
|
<div class="stat-icon">
|
||||||
<i :class="stat.icon"></i>
|
<i :class="stat.icon"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-info">
|
<div class="stat-info">
|
||||||
<div class="stat-label">{{ stat.label }}</div>
|
<div class="stat-label">{{ stat.label }}</div>
|
||||||
<div class="stat-value">{{ stat.value }}</div>
|
<div class="stat-value">{{ stat.value }}</div>
|
||||||
<div
|
|
||||||
class="stat-trend"
|
|
||||||
:class="stat.trend > 0 ? 'positive' : 'negative'"
|
|
||||||
>
|
|
||||||
<i class="el-icon-arrow-up" v-if="stat.trend > 0"></i>
|
|
||||||
<i class="el-icon-arrow-down" v-else-if="stat.trend < 0"></i>
|
|
||||||
<span
|
|
||||||
>{{ Math.abs(stat.trend) }}%
|
|
||||||
{{ stat.trend > 0 ? "增长" : "变化" }}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
@ -106,21 +95,17 @@
|
|||||||
<!-- 卡片头部 -->
|
<!-- 卡片头部 -->
|
||||||
<div class="repo-header">
|
<div class="repo-header">
|
||||||
<div class="repo-icon">
|
<div class="repo-icon">
|
||||||
<i class="el-icon-folder"></i>
|
<i class="fa-solid fa-book"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="repo-title-container">
|
<div class="repo-title-container">
|
||||||
<h3 class="repo-name">{{ repo.name }}</h3>
|
<h3 class="repo-name">{{ repo.title }}</h3>
|
||||||
<div class="repo-meta">
|
<div class="repo-meta">
|
||||||
<el-tag v-if="repo.isPrivate" size="mini" class="private-tag">
|
<el-tag size="small" class="category-tag">
|
||||||
<i class="el-icon-lock"></i> 私有
|
{{ repo.categoryName || '未分类' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<span class="repo-owner">
|
<span class="repo-owner">
|
||||||
<img
|
<i class="fa-solid fa-user"></i>
|
||||||
src="https://picsum.photos/seed/{{repo.creatorName}}/24/24"
|
{{ repo.author }}
|
||||||
alt="创建者"
|
|
||||||
class="owner-avatar"
|
|
||||||
/>
|
|
||||||
{{ repo.creatorName }}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -128,22 +113,29 @@
|
|||||||
|
|
||||||
<!-- 卡片内容 -->
|
<!-- 卡片内容 -->
|
||||||
<div class="repo-content">
|
<div class="repo-content">
|
||||||
<p class="repo-description">
|
<div class="repo-tags" v-if="repo.tags">
|
||||||
{{ repo.description || "暂无描述信息" }}
|
<el-tag
|
||||||
</p>
|
v-for="tag in parseTags(repo.tags)"
|
||||||
|
:key="tag"
|
||||||
|
size="small"
|
||||||
|
class="tag-item"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="repo-stats">
|
<div class="repo-stats">
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<i class="el-icon-document"></i>
|
<i class="fa-solid fa-eye"></i>
|
||||||
<span>{{ repo.docCount }} 文档</span>
|
<span>{{ repo.viewCount || 0 }} 浏览</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<i class="el-icon-eye"></i>
|
<i class="fa-solid fa-heart"></i>
|
||||||
<span>{{ Math.floor(Math.random() * 1000) + 100 }} 浏览</span>
|
<span>{{ repo.likeCount || 0 }} 点赞</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<i class="el-icon-time"></i>
|
<i class="fa-solid fa-clock"></i>
|
||||||
<span>3天前</span>
|
<span>{{ formatDate(repo.createTime) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -192,148 +184,224 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
export default {
|
import { ref, reactive, computed, onMounted } from 'vue';
|
||||||
name: "KnowledgeHome",
|
import { useRouter } from 'vue-router';
|
||||||
data() {
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
return {
|
import { getKnowledgeList, deleteKnowledge } from '@/api/knowledge';
|
||||||
keyword: "",
|
|
||||||
filterType: "all",
|
// 类型定义
|
||||||
searchType: "knowledge",
|
interface Knowledge {
|
||||||
hotTags: ["化妆品", "汽车零部件", "口罩", "工业用品", "食品"],
|
id: number;
|
||||||
stats: {
|
title: string;
|
||||||
total: 0,
|
content: string;
|
||||||
docCount: 0,
|
categoryName: string;
|
||||||
memberCount: 0,
|
tags: string;
|
||||||
viewCount: 0,
|
author: string;
|
||||||
},
|
viewCount: number;
|
||||||
repoList: [],
|
likeCount: number;
|
||||||
total: 0,
|
createTime: string;
|
||||||
pageSize: 12,
|
updateTime: string;
|
||||||
currentPage: 1,
|
}
|
||||||
};
|
|
||||||
|
interface Stats {
|
||||||
|
total: number;
|
||||||
|
docCount: number;
|
||||||
|
memberCount: number;
|
||||||
|
viewCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatItem {
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 路由
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const keyword = ref('');
|
||||||
|
const searchType = ref('knowledge');
|
||||||
|
const hotTags = ref(['化妆品', '汽车零部件', '口罩', '工业用品', '食品']);
|
||||||
|
|
||||||
|
const stats = reactive<Stats>({
|
||||||
|
total: 0,
|
||||||
|
docCount: 0,
|
||||||
|
memberCount: 0,
|
||||||
|
viewCount: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const repoList = ref<Knowledge[]>([]);
|
||||||
|
const total = ref(0);
|
||||||
|
const pageSize = ref(12);
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const statsList = computed<StatItem[]>(() => [
|
||||||
|
{
|
||||||
|
label: '知识库总数',
|
||||||
|
value: repoList.value.length,
|
||||||
|
icon: 'fa fa-book',
|
||||||
},
|
},
|
||||||
computed: {
|
{
|
||||||
// 统计卡片数据
|
label: '今日新增',
|
||||||
// 统计数据
|
value: '12',
|
||||||
statsList() {
|
icon: 'fa fa-plus',
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: '知识库总数',
|
|
||||||
value: this.repoList.length,
|
|
||||||
icon: 'el-icon-document',
|
|
||||||
trend: '+12%'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '今日新增',
|
|
||||||
value: '12',
|
|
||||||
icon: 'el-icon-plus',
|
|
||||||
trend: '+8%'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '本周更新',
|
|
||||||
value: '28',
|
|
||||||
icon: 'el-icon-refresh',
|
|
||||||
trend: '+15%'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '协作项目',
|
|
||||||
value: '6',
|
|
||||||
icon: 'el-icon-user',
|
|
||||||
trend: '+5%'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
created() {
|
{
|
||||||
this.fetchStats();
|
label: '本周更新',
|
||||||
this.fetchRepoList();
|
value: '28',
|
||||||
|
icon: 'fa fa-refresh',
|
||||||
},
|
},
|
||||||
methods: {
|
{
|
||||||
// 获取统计数据
|
label: '协作项目',
|
||||||
async fetchStats() {
|
value: '6',
|
||||||
// 模拟接口
|
icon: 'fa fa-users',
|
||||||
this.stats = {
|
|
||||||
total: 42,
|
|
||||||
docCount: 1280,
|
|
||||||
memberCount: 256,
|
|
||||||
viewCount: 10240,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
// 获取知识库列表
|
|
||||||
async fetchRepoList() {
|
|
||||||
// 模拟接口
|
|
||||||
this.repoList = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: "产品手册",
|
|
||||||
description: "公司产品相关文档、使用说明、FAQ",
|
|
||||||
isPrivate: false,
|
|
||||||
creatorName: "张三",
|
|
||||||
docCount: 56,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: "技术规范",
|
|
||||||
description: "前后端开发规范、接口文档、部署手册",
|
|
||||||
isPrivate: true,
|
|
||||||
creatorName: "李四",
|
|
||||||
docCount: 128,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: "运营资料",
|
|
||||||
description: "市场活动、用户运营、数据分析",
|
|
||||||
isPrivate: false,
|
|
||||||
creatorName: "王五",
|
|
||||||
docCount: 89,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
this.total = 3;
|
|
||||||
},
|
|
||||||
// 搜索
|
|
||||||
handleSearch() {
|
|
||||||
this.currentPage = 1;
|
|
||||||
this.fetchRepoList();
|
|
||||||
},
|
|
||||||
// 分页
|
|
||||||
handlePageChange(page) {
|
|
||||||
this.currentPage = page;
|
|
||||||
this.fetchRepoList();
|
|
||||||
},
|
|
||||||
// 新建
|
|
||||||
handleCreate() {
|
|
||||||
this.$message.success("新建知识库");
|
|
||||||
},
|
|
||||||
// 查看
|
|
||||||
handleView(repo) {
|
|
||||||
// 使用路由名称导航到知识库详情组件,传递ID参数
|
|
||||||
this.$router.push({
|
|
||||||
name: 'apps-knowledge-components-detail',
|
|
||||||
params: { id: repo.id }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// 编辑
|
|
||||||
handleEdit(repo) {
|
|
||||||
this.$message.success(`编辑 ${repo.name}`);
|
|
||||||
},
|
|
||||||
// 删除
|
|
||||||
handleDelete(repo) {
|
|
||||||
this.$confirm(`确认删除知识库「${repo.name}」?`, "提示", {
|
|
||||||
confirmButtonText: "确定",
|
|
||||||
cancelButtonText: "取消",
|
|
||||||
type: "warning",
|
|
||||||
}).then(() => {
|
|
||||||
this.$message.success("删除成功");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// 热门标签搜索
|
|
||||||
handleHotSearch(tag) {
|
|
||||||
this.keyword = tag;
|
|
||||||
this.handleSearch();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
]);
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
async function fetchStats() {
|
||||||
|
try {
|
||||||
|
// 获取列表统计
|
||||||
|
const result = await getKnowledgeList({ page: 1, pageSize: 1 });
|
||||||
|
stats.total = result.total || 0;
|
||||||
|
|
||||||
|
// 计算今日新增和本周更新(这里需要后端提供具体接口,暂时使用总数)
|
||||||
|
stats.docCount = result.total || 0;
|
||||||
|
stats.memberCount = 0; // 需要后端提供
|
||||||
|
stats.viewCount = 0; // 需要后端提供
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('获取统计数据失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
currentPage.value = 1;
|
||||||
|
fetchRepoList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
currentPage.value = page;
|
||||||
|
fetchRepoList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重构 fetchRepoList 以支持关键词搜索
|
||||||
|
async function fetchRepoList() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await getKnowledgeList({
|
||||||
|
page: currentPage.value,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
status: 1, // 只查询已发布的
|
||||||
|
keyword: keyword.value, // 支持关键词搜索
|
||||||
|
});
|
||||||
|
|
||||||
|
repoList.value = result.list || [];
|
||||||
|
total.value = result.total || 0;
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error.message || '获取知识库列表失败');
|
||||||
|
repoList.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreate() {
|
||||||
|
// 跳转到新建知识库页面
|
||||||
|
router.push({
|
||||||
|
name: 'apps-knowledge-edit',
|
||||||
|
params: { id: 'new' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleView(repo: Knowledge) {
|
||||||
|
// 跳转到知识详情页面,使用 params 参数传递 id
|
||||||
|
router.push({
|
||||||
|
name: 'apps-knowledge-detail',
|
||||||
|
params: { id: repo.id.toString() },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEdit(repo: Knowledge) {
|
||||||
|
// 跳转到编辑知识页面,使用 params 参数传递 id
|
||||||
|
router.push({
|
||||||
|
name: 'apps-knowledge-edit',
|
||||||
|
params: { id: repo.id.toString() },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(repo: Knowledge) {
|
||||||
|
ElMessageBox.confirm(`确认删除知识「${repo.title}」?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
try {
|
||||||
|
await deleteKnowledge(repo.id);
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
fetchRepoList(); // 重新加载列表
|
||||||
|
fetchStats(); // 更新统计
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error.message || '删除失败');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 取消删除
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHotSearch(tag: string) {
|
||||||
|
keyword.value = tag;
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析标签字符串
|
||||||
|
function parseTags(tagsStr: string): string[] {
|
||||||
|
if (!tagsStr) return [];
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(tagsStr);
|
||||||
|
return Array.isArray(parsed) ? parsed : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
function formatDate(dateStr: string): string {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const now = new Date();
|
||||||
|
const diff = now.getTime() - date.getTime();
|
||||||
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (days < 1) {
|
||||||
|
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||||
|
if (hours < 1) {
|
||||||
|
const minutes = Math.floor(diff / (1000 * 60));
|
||||||
|
return `${minutes}分钟前`;
|
||||||
|
}
|
||||||
|
return `${hours}小时前`;
|
||||||
|
} else if (days < 7) {
|
||||||
|
return `${days}天前`;
|
||||||
|
} else if (days < 30) {
|
||||||
|
const weeks = Math.floor(days / 7);
|
||||||
|
return `${weeks}周前`;
|
||||||
|
} else {
|
||||||
|
return date.toLocaleDateString('zh-CN');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onMounted(() => {
|
||||||
|
fetchStats();
|
||||||
|
fetchRepoList();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -341,12 +409,13 @@ export default {
|
|||||||
|
|
||||||
// 顶部横幅样式
|
// 顶部横幅样式
|
||||||
.hero.new-style {
|
.hero.new-style {
|
||||||
background: linear-gradient(135deg, var(--primary-color), var(--info-color));
|
background: var(--card-bg1);
|
||||||
padding: 60px 20px;
|
padding: 60px 20px;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: var(--transition-base);
|
transition: var(--transition-base);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
// 背景装饰
|
// 背景装饰
|
||||||
&::after {
|
&::after {
|
||||||
@ -356,7 +425,7 @@ export default {
|
|||||||
right: 0;
|
right: 0;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
background-image: url('https://picsum.photos/seed/container/800/600');
|
// background-image: url('https://picsum.photos/seed/container/800/600');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: right bottom;
|
background-position: right bottom;
|
||||||
opacity: 0.1;
|
opacity: 0.1;
|
||||||
@ -510,25 +579,6 @@ export default {
|
|||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-trend {
|
|
||||||
font-size: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
i {
|
|
||||||
margin-right: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.positive {
|
|
||||||
color: var(--success-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.negative {
|
|
||||||
color: var(--error-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -536,6 +586,7 @@ export default {
|
|||||||
// 知识库列表样式
|
// 知识库列表样式
|
||||||
.knowledge-repos {
|
.knowledge-repos {
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
|
||||||
.repos-header {
|
.repos-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -647,17 +698,17 @@ export default {
|
|||||||
.repo-content {
|
.repo-content {
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
|
|
||||||
.repo-description {
|
.repo-tags {
|
||||||
font-size: 14px;
|
display: flex;
|
||||||
color: var(--text-secondary);
|
flex-wrap: wrap;
|
||||||
line-height: 1.5;
|
gap: 6px;
|
||||||
margin: 0 0 16px 0;
|
margin-bottom: 16px;
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
.tag-item {
|
||||||
line-clamp: 2;
|
background: var(--background-hover);
|
||||||
-webkit-box-orient: vertical;
|
color: var(--text-color);
|
||||||
overflow: hidden;
|
border-color: var(--border-color);
|
||||||
height: 42px;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-stats {
|
.repo-stats {
|
||||||
|
|||||||
@ -18,19 +18,12 @@
|
|||||||
circle
|
circle
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
<button
|
|
||||||
class="menu-toggle"
|
|
||||||
@click="toggleSidebar"
|
|
||||||
aria-label="Toggle menu"
|
|
||||||
>
|
|
||||||
<svg-icon name="menu" />
|
|
||||||
</button>
|
|
||||||
<h1 class="page-title">Dashboard</h1>
|
<h1 class="page-title">Dashboard</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="search-bar">
|
<div class="search-bar">
|
||||||
<svg-icon name="search" class="search-icon" />
|
<i class="fas fa-search search-icon"></i>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
@ -172,18 +165,6 @@ const isDark = computed(() => theme.isDark());
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
|
|
||||||
.menu-toggle {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--text-color);
|
|
||||||
transition: var(--transition-fast);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
@ -219,6 +200,7 @@ const isDark = computed(() => theme.isDark());
|
|||||||
.search-icon {
|
.search-icon {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
|
|||||||
@ -207,9 +207,9 @@ const handleMenuClick = (path: string) => {
|
|||||||
// 获取菜单数据
|
// 获取菜单数据
|
||||||
const loadMenuData = async () => {
|
const loadMenuData = async () => {
|
||||||
try {
|
try {
|
||||||
console.log("开始加载主侧边栏菜单数据...");
|
// console.log("开始加载主侧边栏菜单数据...");
|
||||||
const response = await menuAPI.getTopLevelMenus();
|
const response = await menuAPI.getTopLevelMenus();
|
||||||
console.log("主侧边栏菜单API响应:", response);
|
// console.log("主侧边栏菜单API响应:", response);
|
||||||
|
|
||||||
if (response && response.success) {
|
if (response && response.success) {
|
||||||
// 假设响应数据格式符合预期,直接使用 data 赋值
|
// 假设响应数据格式符合预期,直接使用 data 赋值
|
||||||
@ -222,7 +222,7 @@ const loadMenuData = async () => {
|
|||||||
icon: item.Icon,
|
icon: item.Icon,
|
||||||
children: item.Children || [],
|
children: item.Children || [],
|
||||||
})) || [];
|
})) || [];
|
||||||
console.log("主侧边栏菜单数据加载成功:", menuData.value);
|
// console.log("主侧边栏菜单数据加载成功:", menuData.value);
|
||||||
} else {
|
} else {
|
||||||
console.error("获取菜单数据失败:", response?.message || "未知错误");
|
console.error("获取菜单数据失败:", response?.message || "未知错误");
|
||||||
menuData.value = [];
|
menuData.value = [];
|
||||||
|
|||||||
@ -48,7 +48,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, watch, computed, onActivated } from "vue";
|
import { ref, shallowRef, onMounted, watch, computed, onActivated, markRaw } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import SubSidebar from "@/views/components/sub-sidebar.vue";
|
import SubSidebar from "@/views/components/sub-sidebar.vue";
|
||||||
import { menuAPI } from "@/services/api";
|
import { menuAPI } from "@/services/api";
|
||||||
@ -83,7 +83,7 @@ const pathToMenuIdMap = ref<Record<string, number>>({});
|
|||||||
|
|
||||||
// 其他状态(保持不变)
|
// 其他状态(保持不变)
|
||||||
const currentSubModule = ref<MenuItem | null>(null);
|
const currentSubModule = ref<MenuItem | null>(null);
|
||||||
const dynamicComponent = ref<any>(null);
|
const dynamicComponent = shallowRef<any>(null); // 使用 shallowRef 避免组件对象变成响应式
|
||||||
const componentLoading = ref(false);
|
const componentLoading = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const error = ref("");
|
const error = ref("");
|
||||||
@ -107,7 +107,7 @@ const initMenuMap = async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
pathToMenuIdMap.value = map;
|
pathToMenuIdMap.value = map;
|
||||||
console.log("路径-ID映射表:", map);
|
// console.log("路径-ID映射表:", map);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("初始化菜单映射失败:", err.message);
|
console.error("初始化菜单映射失败:", err.message);
|
||||||
@ -127,7 +127,7 @@ const computedParentId = computed(() => {
|
|||||||
if (route.query.id) {
|
if (route.query.id) {
|
||||||
const queryId = Number(route.query.id);
|
const queryId = Number(route.query.id);
|
||||||
if (!isNaN(queryId) && queryId > 0) {
|
if (!isNaN(queryId) && queryId > 0) {
|
||||||
console.log("从路由参数获取父ID:", queryId);
|
// console.log("从路由参数获取父ID:", queryId);
|
||||||
return queryId;
|
return queryId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -142,24 +142,24 @@ const computedParentId = computed(() => {
|
|||||||
if (firstLevelPath) {
|
if (firstLevelPath) {
|
||||||
const menuId = getMenuIdByPath(firstLevelPath);
|
const menuId = getMenuIdByPath(firstLevelPath);
|
||||||
if (menuId > 0) {
|
if (menuId > 0) {
|
||||||
console.log(`从路径 ${firstLevelPath} 匹配父ID:`, menuId);
|
// // console.log(`从路径 ${firstLevelPath} 匹配父ID:`, menuId);
|
||||||
return menuId;
|
return menuId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 都匹配不到时返回0(顶级菜单)
|
// 3. 都匹配不到时返回0(顶级菜单)
|
||||||
console.log("未匹配到父菜单ID,返回0");
|
// console.log("未匹配到父菜单ID,返回0");
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取二级菜单数据(基于路由计算的父ID)
|
// 获取二级菜单数据(基于路由计算的父ID)
|
||||||
const fetchSubMenuItems = async () => {
|
const fetchSubMenuItems = async () => {
|
||||||
console.log('开始获取二级菜单数据:', {
|
// console.log('开始获取二级菜单数据:', {
|
||||||
parentId: computedParentId.value,
|
// parentId: computedParentId.value,
|
||||||
currentPath: route.path,
|
// currentPath: route.path,
|
||||||
currentQuery: route.query
|
// currentQuery: route.query
|
||||||
});
|
// });
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = "";
|
error.value = "";
|
||||||
@ -167,14 +167,14 @@ const fetchSubMenuItems = async () => {
|
|||||||
try {
|
try {
|
||||||
const parentId = computedParentId.value;
|
const parentId = computedParentId.value;
|
||||||
if (parentId === 0) {
|
if (parentId === 0) {
|
||||||
console.log('父ID为0,清空菜单数据');
|
// console.log('父ID为0,清空菜单数据');
|
||||||
subMenuItems.value = [];
|
subMenuItems.value = [];
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await menuAPI.getMenusByParentId(parentId);
|
const response = await menuAPI.getMenusByParentId(parentId);
|
||||||
console.log('获取菜单数据响应:', response);
|
// console.log('获取菜单数据响应:', response);
|
||||||
|
|
||||||
if (response && response.success) {
|
if (response && response.success) {
|
||||||
subMenuItems.value = response.data
|
subMenuItems.value = response.data
|
||||||
@ -188,7 +188,7 @@ const fetchSubMenuItems = async () => {
|
|||||||
}))
|
}))
|
||||||
.filter((item: any) => item.path && item.title);
|
.filter((item: any) => item.path && item.title);
|
||||||
|
|
||||||
console.log('处理后的二级菜单数据:', subMenuItems.value);
|
// console.log('处理后的二级菜单数据:', subMenuItems.value);
|
||||||
|
|
||||||
// 新增:二级菜单加载完成后,默认选择第一个项(路由未匹配时)
|
// 新增:二级菜单加载完成后,默认选择第一个项(路由未匹配时)
|
||||||
await selectDefaultMenuItem();
|
await selectDefaultMenuItem();
|
||||||
@ -201,20 +201,20 @@ const fetchSubMenuItems = async () => {
|
|||||||
console.error('获取菜单数据异常:', err);
|
console.error('获取菜单数据异常:', err);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
console.log('获取二级菜单数据完成');
|
// console.log('获取二级菜单数据完成');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectDefaultMenuItem = async () => {
|
const selectDefaultMenuItem = async () => {
|
||||||
console.log('开始选择默认菜单项:', {
|
// console.log('开始选择默认菜单项:', {
|
||||||
currentPath: route.path,
|
// currentPath: route.path,
|
||||||
subMenuItems: subMenuItems.value
|
// subMenuItems: subMenuItems.value
|
||||||
});
|
// });
|
||||||
|
|
||||||
// 如果路由已匹配某个菜单项,则不触发默认选择
|
// 如果路由已匹配某个菜单项,则不触发默认选择
|
||||||
const matchedItem = subMenuItems.value.find(item => item.path === route.path);
|
const matchedItem = subMenuItems.value.find(item => item.path === route.path);
|
||||||
if (matchedItem) {
|
if (matchedItem) {
|
||||||
console.log('找到匹配的菜单项:', matchedItem);
|
// console.log('找到匹配的菜单项:', matchedItem);
|
||||||
currentSubModule.value = matchedItem;
|
currentSubModule.value = matchedItem;
|
||||||
await loadDynamicComponent(matchedItem);
|
await loadDynamicComponent(matchedItem);
|
||||||
return;
|
return;
|
||||||
@ -223,28 +223,28 @@ const selectDefaultMenuItem = async () => {
|
|||||||
// 路由未匹配时,默认选择第一个菜单项
|
// 路由未匹配时,默认选择第一个菜单项
|
||||||
if (subMenuItems.value.length > 0) {
|
if (subMenuItems.value.length > 0) {
|
||||||
const firstItem = subMenuItems.value[0];
|
const firstItem = subMenuItems.value[0];
|
||||||
console.log('未找到匹配项,选择第一个菜单项:', firstItem);
|
// console.log('未找到匹配项,选择第一个菜单项:', firstItem);
|
||||||
currentSubModule.value = firstItem;
|
currentSubModule.value = firstItem;
|
||||||
// 跳转到第一个项的路径(更新地址栏)
|
// 跳转到第一个项的路径(更新地址栏)
|
||||||
router.push(firstItem.path);
|
router.push(firstItem.path);
|
||||||
// 加载对应的组件
|
// 加载对应的组件
|
||||||
await loadDynamicComponent(firstItem);
|
await loadDynamicComponent(firstItem);
|
||||||
} else {
|
} else {
|
||||||
console.log('没有可用的菜单项');
|
// console.log('没有可用的菜单项');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkComponentExists = async (componentPath: string): Promise<boolean> => {
|
const checkComponentExists = async (componentPath: string): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
console.log('检查组件是否存在:', componentPath);
|
// console.log('检查组件是否存在:', componentPath);
|
||||||
// 使用Vite的import.meta.glob来检查文件是否存在
|
// 使用Vite的import.meta.glob来检查文件是否存在
|
||||||
const modules = import.meta.glob('@/views/**/*.vue');
|
const modules = import.meta.glob('@/views/**/*.vue');
|
||||||
console.log('可用模块:', Object.keys(modules));
|
// console.log('可用模块:', Object.keys(modules));
|
||||||
|
|
||||||
// 检查路径是否在可用模块中
|
// 检查路径是否在可用模块中
|
||||||
const normalizedPath = componentPath.replace('@', '/src');
|
const normalizedPath = componentPath.replace('@', '/src');
|
||||||
const exists = normalizedPath in modules;
|
const exists = normalizedPath in modules;
|
||||||
console.log('组件存在检查结果:', { componentPath, normalizedPath, exists });
|
// console.log('组件存在检查结果:', { componentPath, normalizedPath, exists });
|
||||||
|
|
||||||
return exists;
|
return exists;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -254,12 +254,12 @@ const checkComponentExists = async (componentPath: string): Promise<boolean> =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadDynamicComponent = async (menuItem: MenuItem) => {
|
const loadDynamicComponent = async (menuItem: MenuItem) => {
|
||||||
console.log('开始加载动态组件:', menuItem);
|
// console.log('开始加载动态组件:', menuItem);
|
||||||
|
|
||||||
if (!menuItem.componentPath) {
|
if (!menuItem.componentPath) {
|
||||||
error.value = "未指定组件路径";
|
error.value = "未指定组件路径";
|
||||||
dynamicComponent.value = null;
|
dynamicComponent.value = null;
|
||||||
console.log('组件路径为空');
|
// console.log('组件路径为空');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,8 +269,8 @@ const loadDynamicComponent = async (menuItem: MenuItem) => {
|
|||||||
try {
|
try {
|
||||||
// 检查组件是否已经加载过
|
// 检查组件是否已经加载过
|
||||||
if (loadedComponents[menuItem.componentPath]) {
|
if (loadedComponents[menuItem.componentPath]) {
|
||||||
console.log('组件已缓存,直接使用:', menuItem.componentPath);
|
// console.log('组件已缓存,直接使用:', menuItem.componentPath);
|
||||||
dynamicComponent.value = loadedComponents[menuItem.componentPath];
|
dynamicComponent.value = markRaw(loadedComponents[menuItem.componentPath]);
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -281,7 +281,7 @@ const loadDynamicComponent = async (menuItem: MenuItem) => {
|
|||||||
throw new Error(`组件文件不存在: ${menuItem.componentPath}`);
|
throw new Error(`组件文件不存在: ${menuItem.componentPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('开始动态导入组件:', menuItem.componentPath);
|
// console.log('开始动态导入组件:', menuItem.componentPath);
|
||||||
|
|
||||||
// 使用Vite的import.meta.glob来动态导入组件
|
// 使用Vite的import.meta.glob来动态导入组件
|
||||||
const modules = import.meta.glob('@/views/**/*.vue');
|
const modules = import.meta.glob('@/views/**/*.vue');
|
||||||
@ -289,24 +289,24 @@ const loadDynamicComponent = async (menuItem: MenuItem) => {
|
|||||||
|
|
||||||
if (modules[normalizedPath]) {
|
if (modules[normalizedPath]) {
|
||||||
const module = await modules[normalizedPath]();
|
const module = await modules[normalizedPath]();
|
||||||
console.log('动态导入完成:', module);
|
// console.log('动态导入完成:', module);
|
||||||
|
|
||||||
// 处理不同类型的导出
|
// 处理不同类型的导出
|
||||||
let component = null;
|
let component = null;
|
||||||
if (module.default) {
|
if (module.default) {
|
||||||
component = module.default;
|
component = module.default;
|
||||||
console.log('使用默认导出组件');
|
// console.log('使用默认导出组件');
|
||||||
} else if (Object.keys(module).length === 1) {
|
} else if (Object.keys(module).length === 1) {
|
||||||
// 如果只有一个导出,使用它
|
// 如果只有一个导出,使用它
|
||||||
const key = Object.keys(module)[0];
|
const key = Object.keys(module)[0];
|
||||||
component = module[key];
|
component = module[key];
|
||||||
console.log('使用单一导出组件:', key);
|
// console.log('使用单一导出组件:', key);
|
||||||
} else {
|
} else {
|
||||||
// 尝试寻找合适的组件
|
// 尝试寻找合适的组件
|
||||||
for (const key of Object.keys(module)) {
|
for (const key of Object.keys(module)) {
|
||||||
if (typeof module[key] === 'object' && module[key]?.__name) {
|
if (typeof module[key] === 'object' && module[key]?.__name) {
|
||||||
component = module[key];
|
component = module[key];
|
||||||
console.log('找到命名组件:', key);
|
// console.log('找到命名组件:', key);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -316,10 +316,12 @@ const loadDynamicComponent = async (menuItem: MenuItem) => {
|
|||||||
throw new Error(`无法加载组件: ${menuItem.componentPath}`);
|
throw new Error(`无法加载组件: ${menuItem.componentPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('组件加载成功:', component);
|
// console.log('组件加载成功:', component);
|
||||||
|
// 使用 markRaw 标记组件为非响应式
|
||||||
|
const rawComponent = markRaw(component);
|
||||||
// 缓存并设置组件
|
// 缓存并设置组件
|
||||||
loadedComponents[menuItem.componentPath] = component;
|
loadedComponents[menuItem.componentPath] = rawComponent;
|
||||||
dynamicComponent.value = component;
|
dynamicComponent.value = rawComponent;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`无法找到组件模块: ${menuItem.componentPath}`);
|
throw new Error(`无法找到组件模块: ${menuItem.componentPath}`);
|
||||||
}
|
}
|
||||||
@ -347,7 +349,7 @@ const retry = () => {
|
|||||||
watch(
|
watch(
|
||||||
() => [route.path, route.query.id],
|
() => [route.path, route.query.id],
|
||||||
(newVal, oldVal) => {
|
(newVal, oldVal) => {
|
||||||
console.log('路由变化监听:', { newVal, oldVal });
|
// console.log('路由变化监听:', { newVal, oldVal });
|
||||||
fetchSubMenuItems();
|
fetchSubMenuItems();
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
@ -357,7 +359,7 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => route.name,
|
() => route.name,
|
||||||
(newName, oldName) => {
|
(newName, oldName) => {
|
||||||
console.log('路由名称变化监听:', { newName, oldName });
|
// console.log('路由名称变化监听:', { newName, oldName });
|
||||||
if (newName !== oldName) {
|
if (newName !== oldName) {
|
||||||
fetchSubMenuItems();
|
fetchSubMenuItems();
|
||||||
}
|
}
|
||||||
@ -380,27 +382,27 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// 当组件被激活时重新加载数据(用于keep-alive场景)
|
// 当组件被激活时重新加载数据(用于keep-alive场景)
|
||||||
onActivated(async () => {
|
onActivated(async () => {
|
||||||
console.log('onActivated被调用:', {
|
// console.log('onActivated被调用:', {
|
||||||
routePath: route.path,
|
// routePath: route.path,
|
||||||
currentSubModulePath: currentSubModule.value?.path,
|
// currentSubModulePath: currentSubModule.value?.path,
|
||||||
subMenuItems: subMenuItems.value
|
// subMenuItems: subMenuItems.value
|
||||||
});
|
// });
|
||||||
|
|
||||||
// 检查路由是否发生变化
|
// 检查路由是否发生变化
|
||||||
if (route.path !== currentSubModule.value?.path) {
|
if (route.path !== currentSubModule.value?.path) {
|
||||||
console.log('路由路径发生变化,重新获取菜单数据');
|
// console.log('路由路径发生变化,重新获取菜单数据');
|
||||||
await fetchSubMenuItems();
|
await fetchSubMenuItems();
|
||||||
|
|
||||||
// 加载当前路由对应的组件
|
// 加载当前路由对应的组件
|
||||||
if (route.path && subMenuItems.value.length > 0) {
|
if (route.path && subMenuItems.value.length > 0) {
|
||||||
const menuItem = subMenuItems.value.find(item => item.path === route.path);
|
const menuItem = subMenuItems.value.find(item => item.path === route.path);
|
||||||
if (menuItem) {
|
if (menuItem) {
|
||||||
console.log('加载动态组件:', menuItem);
|
// console.log('加载动态组件:', menuItem);
|
||||||
await loadDynamicComponent(menuItem);
|
await loadDynamicComponent(menuItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('路由路径未发生变化,无需重新加载');
|
// console.log('路由路径未发生变化,无需重新加载');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
366
server/controllers/knowledge.go
Normal file
366
server/controllers/knowledge.go
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"server/models"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
beego "github.com/beego/beego/v2/server/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KnowledgeController 知识库控制器
|
||||||
|
type KnowledgeController struct {
|
||||||
|
beego.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取知识列表
|
||||||
|
// @router /api/knowledge/list [get]
|
||||||
|
func (c *KnowledgeController) List() {
|
||||||
|
// 获取查询参数
|
||||||
|
page, _ := c.GetInt("page", 1)
|
||||||
|
pageSize, _ := c.GetInt("pageSize", 10)
|
||||||
|
status, _ := c.GetInt8("status", -1) // -1表示查询所有
|
||||||
|
categoryId, _ := c.GetInt("categoryId", 0)
|
||||||
|
keyword := c.GetString("keyword", "")
|
||||||
|
|
||||||
|
knowledges, total, err := models.GetAllKnowledge(page, pageSize, status, categoryId, keyword)
|
||||||
|
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": "success",
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"list": knowledges,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"pageSize": pageSize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail 获取知识详情
|
||||||
|
// @router /api/knowledge/detail [get]
|
||||||
|
func (c *KnowledgeController) Detail() {
|
||||||
|
id, _ := strconv.Atoi(c.GetString("id"))
|
||||||
|
if id == 0 {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"code": 1,
|
||||||
|
"message": "知识ID不能为空",
|
||||||
|
"data": nil,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
knowledge, err := models.GetKnowledgeById(id)
|
||||||
|
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": "success",
|
||||||
|
"data": knowledge,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建知识
|
||||||
|
// @router /api/knowledge/create [post]
|
||||||
|
func (c *KnowledgeController) Create() {
|
||||||
|
// 解析请求体
|
||||||
|
var knowledge models.Knowledge
|
||||||
|
err := json.Unmarshal(c.Ctx.Input.RequestBody, &knowledge)
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"code": 1,
|
||||||
|
"message": "请求参数格式错误",
|
||||||
|
"data": nil,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证必填字段
|
||||||
|
if knowledge.Title == "" {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"code": 1,
|
||||||
|
"message": "标题不能为空",
|
||||||
|
"data": nil,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := models.AddKnowledge(&knowledge)
|
||||||
|
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{}{"id": id},
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新知识
|
||||||
|
// @router /api/knowledge/update [post]
|
||||||
|
func (c *KnowledgeController) Update() {
|
||||||
|
// 解析请求体
|
||||||
|
var knowledge models.Knowledge
|
||||||
|
err := json.Unmarshal(c.Ctx.Input.RequestBody, &knowledge)
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"code": 1,
|
||||||
|
"message": "请求参数格式错误",
|
||||||
|
"data": nil,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证必填字段
|
||||||
|
if knowledge.Id == 0 {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"code": 1,
|
||||||
|
"message": "知识ID不能为空",
|
||||||
|
"data": nil,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = models.UpdateKnowledge(knowledge.Id, &knowledge)
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 软删除知识
|
||||||
|
// @router /api/knowledge/delete [post]
|
||||||
|
func (c *KnowledgeController) Delete() {
|
||||||
|
var request map[string]interface{}
|
||||||
|
err := json.Unmarshal(c.Ctx.Input.RequestBody, &request)
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"code": 1,
|
||||||
|
"message": "请求参数格式错误",
|
||||||
|
"data": nil,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, ok := request["id"].(float64)
|
||||||
|
if !ok {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"code": 1,
|
||||||
|
"message": "知识ID不能为空",
|
||||||
|
"data": nil,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从请求头或 session 中获取当前用户信息
|
||||||
|
deleteBy := c.GetString("username")
|
||||||
|
if deleteBy == "" {
|
||||||
|
deleteBy = c.GetString("user")
|
||||||
|
}
|
||||||
|
if deleteBy == "" {
|
||||||
|
deleteBy = "system" // 默认值
|
||||||
|
}
|
||||||
|
|
||||||
|
err = models.DeleteKnowledge(int(id), deleteBy)
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCategories 获取分类列表
|
||||||
|
// @router /api/knowledge/categories [get]
|
||||||
|
func (c *KnowledgeController) GetCategories() {
|
||||||
|
categories, err := models.GetAllCategories()
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"code": 1,
|
||||||
|
"message": "获取分类列表失败: " + err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动转换数据格式,确保字段名正确
|
||||||
|
var result []map[string]interface{}
|
||||||
|
for _, cat := range categories {
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"categoryId": cat.CategoryId,
|
||||||
|
"categoryName": cat.CategoryName,
|
||||||
|
"categoryDesc": cat.CategoryDesc,
|
||||||
|
"parentId": cat.ParentId,
|
||||||
|
"sortOrder": cat.SortOrder,
|
||||||
|
"createTime": cat.CreateTime,
|
||||||
|
"updateTime": cat.UpdateTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": result,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTags 获取标签列表
|
||||||
|
// @router /api/knowledge/tags [get]
|
||||||
|
func (c *KnowledgeController) GetTags() {
|
||||||
|
tags, err := models.GetAllTags()
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"code": 1,
|
||||||
|
"message": "获取标签列表失败: " + err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动转换数据格式,确保字段名正确
|
||||||
|
var result []map[string]interface{}
|
||||||
|
for _, tag := range tags {
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"tagId": tag.TagId,
|
||||||
|
"tagName": tag.TagName,
|
||||||
|
"tagColor": tag.TagColor,
|
||||||
|
"usageCount": tag.UsageCount,
|
||||||
|
"createTime": tag.CreateTime,
|
||||||
|
"updateTime": tag.UpdateTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": result,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCategory 添加分类
|
||||||
|
// @router /api/knowledge/category/add [post]
|
||||||
|
func (c *KnowledgeController) AddCategory() {
|
||||||
|
var category models.KnowledgeCategory
|
||||||
|
err := json.Unmarshal(c.Ctx.Input.RequestBody, &category)
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"code": 1,
|
||||||
|
"message": "请求参数格式错误",
|
||||||
|
"data": nil,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := models.AddCategory(&category)
|
||||||
|
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{}{"categoryId": id},
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTag 添加标签
|
||||||
|
// @router /api/knowledge/tag/add [post]
|
||||||
|
func (c *KnowledgeController) AddTag() {
|
||||||
|
var tag models.KnowledgeTag
|
||||||
|
err := json.Unmarshal(c.Ctx.Input.RequestBody, &tag)
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"code": 1,
|
||||||
|
"message": "请求参数格式错误",
|
||||||
|
"data": nil,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := models.AddTag(&tag)
|
||||||
|
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{}{"tagId": id},
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
}
|
||||||
59
server/database/README_KNOWLEDGE.md
Normal file
59
server/database/README_KNOWLEDGE.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# 知识库数据库表创建说明
|
||||||
|
|
||||||
|
## 创建步骤
|
||||||
|
|
||||||
|
### 方法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
server/database/create_knowledge_tables.sql
Normal file
1
server/database/create_knowledge_tables.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -162,13 +162,108 @@ INSERT INTO yz_menus (name, path, parent_id, icon, `order`, status, component_pa
|
|||||||
('系统设置', '/settings', 0, 'fa-solid fa-gear', 99, 1, '@/views/settings/index.vue', 1, '系统参数设置'),
|
('系统设置', '/settings', 0, 'fa-solid fa-gear', 99, 1, '@/views/settings/index.vue', 1, '系统参数设置'),
|
||||||
('应用管理', '/apps', 0, 'fa-solid fa-gear', 4, 1, null, 2, '应用管理功能模块'),
|
('应用管理', '/apps', 0, 'fa-solid fa-gear', 4, 1, null, 2, '应用管理功能模块'),
|
||||||
('文件管理', '/system/files', 2, 'fa-solid fa-folder', 3, 1, '@/views/system/files/index.vue', 1, '文件管理系统'),
|
('文件管理', '/system/files', 2, 'fa-solid fa-folder', 3, 1, '@/views/system/files/index.vue', 1, '文件管理系统'),
|
||||||
|
('租户管理', '/system/tenant', 2, 'fas fa-user-friends', 2, 1, '@/views/system/tenant/index.vue', 1, '租户管理'),
|
||||||
('用户管理', '/system/users', 2, 'fa-solid fa-user', 1, 1, '@/views/system/users/index.vue', 1, '用户信息管理'),
|
('用户管理', '/system/users', 2, 'fa-solid fa-user', 1, 1, '@/views/system/users/index.vue', 1, '用户信息管理'),
|
||||||
('角色管理', '/system/roles', 2, 'fa-solid fa-user-tag', 2, 1, '@/views/system/roles/index.vue', 1, '角色权限管理'),
|
('角色管理', '/system/roles', 2, 'fa-solid fa-user-tag', 2, 1, '@/views/system/roles/index.vue', 1, '角色权限管理'),
|
||||||
('权限管理', '/system/permissions', 2, 'fa-solid fa-key', 2, 1, '@/views/system/permissions/index.vue', 1, '权限管理'),
|
('权限管理', '/system/permissions', 2, 'fa-solid fa-key', 2, 1, '@/views/system/permissions/index.vue', 1, '权限管理'),
|
||||||
('菜单管理', '/system/menus', 2, 'fa-solid fa-bars-progress', 2, 1, '@/views/system/menus/manager.vue', 1, '菜单权限管理'),
|
('菜单管理', '/system/menus', 2, 'fa-solid fa-bars-progress', 2, 1, '@/views/system/menus/manager.vue', 1, '菜单权限管理'),
|
||||||
('程序管理', '/system/programs', 2, 'fa-solid fa-grip', 3, 1, '@/views/system/programs/index.vue', 1, '程序功能管理'),
|
('程序管理', '/system/programs', 2, 'fa-solid fa-grip', 3, 1, '@/views/system/programs/index.vue', 1, '程序功能管理'),
|
||||||
('知识库', '/apps/knowledge', 4, 'fa-solid fa-book', 1, 1, '@/views/apps/knowledge/index.vue', 1, '知识库管理'),
|
('知识库', '/apps/knowledge', 4, 'fa-solid fa-book', 1, 1, '@/views/apps/knowledge/index.vue', 1, '知识库管理'),
|
||||||
|
('编辑', '/apps/knowledge/edit', 11, '', 1, 1, '@/views/apps/knowledge/components/edit.vue', 1, '知识库编辑'),
|
||||||
('详情', '/apps/knowledge/detail', 11, '', 1, 1, '@/views/apps/knowledge/components/detail.vue', 1, '知识库详情');
|
('详情', '/apps/knowledge/detail', 11, '', 1, 1, '@/views/apps/knowledge/components/detail.vue', 1, '知识库详情');
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 4. 知识库相关表
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
-- 创建知识库分类表
|
||||||
|
CREATE TABLE IF NOT EXISTS yz_knowledge_category (
|
||||||
|
category_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '分类ID',
|
||||||
|
category_name VARCHAR(100) NOT NULL COMMENT '分类名称',
|
||||||
|
category_desc VARCHAR(500) DEFAULT NULL COMMENT '分类描述',
|
||||||
|
parent_id INT DEFAULT 0 COMMENT '父分类ID,0表示顶级分类',
|
||||||
|
sort_order INT DEFAULT 0 COMMENT '排序序号',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
INDEX idx_parent_id (parent_id),
|
||||||
|
INDEX idx_sort_order (sort_order)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库分类表';
|
||||||
|
|
||||||
|
-- 创建知识库标签表
|
||||||
|
CREATE TABLE IF NOT EXISTS yz_knowledge_tags (
|
||||||
|
tag_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '标签ID',
|
||||||
|
tag_name VARCHAR(50) NOT NULL COMMENT '标签名称',
|
||||||
|
tag_color VARCHAR(20) DEFAULT NULL COMMENT '标签颜色',
|
||||||
|
usage_count INT DEFAULT 0 COMMENT '使用次数',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
UNIQUE KEY uk_tag_name (tag_name),
|
||||||
|
INDEX idx_usage_count (usage_count)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库标签表';
|
||||||
|
|
||||||
|
-- 创建知识库内容表
|
||||||
|
CREATE TABLE IF NOT EXISTS yz_knowledge (
|
||||||
|
knowledge_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '知识ID',
|
||||||
|
title VARCHAR(200) NOT NULL COMMENT '标题',
|
||||||
|
category_id INT DEFAULT NULL COMMENT '分类ID',
|
||||||
|
tags TEXT COMMENT '标签JSON数组,存储标签名称',
|
||||||
|
author VARCHAR(50) NOT NULL COMMENT '作者',
|
||||||
|
content LONGTEXT COMMENT '正文内容(富文本)',
|
||||||
|
summary VARCHAR(500) DEFAULT NULL COMMENT '摘要',
|
||||||
|
cover_url VARCHAR(500) DEFAULT NULL COMMENT '封面图片URL',
|
||||||
|
status TINYINT DEFAULT 0 COMMENT '状态:0-草稿,1-已发布,2-已归档',
|
||||||
|
view_count INT DEFAULT 0 COMMENT '查看次数',
|
||||||
|
like_count INT DEFAULT 0 COMMENT '点赞数',
|
||||||
|
is_recommend TINYINT DEFAULT 0 COMMENT '是否推荐:0-否,1-是',
|
||||||
|
is_top TINYINT DEFAULT 0 COMMENT '是否置顶:0-否,1-是',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
create_by VARCHAR(50) DEFAULT NULL COMMENT '创建人',
|
||||||
|
update_by VARCHAR(50) DEFAULT NULL COMMENT '更新人',
|
||||||
|
INDEX idx_category_id (category_id),
|
||||||
|
INDEX idx_author (author),
|
||||||
|
INDEX idx_title (title),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_create_time (create_time),
|
||||||
|
INDEX idx_view_count (view_count),
|
||||||
|
INDEX idx_is_recommend (is_recommend),
|
||||||
|
INDEX idx_is_top (is_top)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库内容表';
|
||||||
|
|
||||||
|
-- 创建知识库收藏表
|
||||||
|
CREATE TABLE IF NOT EXISTS yz_knowledge_favorites (
|
||||||
|
favorite_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '收藏ID',
|
||||||
|
knowledge_id INT NOT NULL COMMENT '知识ID',
|
||||||
|
user_id INT NOT NULL COMMENT '用户ID',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
UNIQUE KEY uk_knowledge_user (knowledge_id, user_id),
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
CONSTRAINT yz_fk_fav_knowledge FOREIGN KEY (knowledge_id) REFERENCES yz_knowledge (knowledge_id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT yz_fk_fav_user FOREIGN KEY (user_id) REFERENCES yz_users (id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库收藏表';
|
||||||
|
|
||||||
|
-- 插入默认知识库分类
|
||||||
|
INSERT INTO yz_knowledge_category (category_name, category_desc, parent_id, sort_order) VALUES
|
||||||
|
('技术文档', '技术相关的文档资料', 0, 1),
|
||||||
|
('产品手册', '产品使用手册和说明', 0, 2),
|
||||||
|
('用户指南', '用户操作指南和教程', 0, 3),
|
||||||
|
('常见问题', '常见问题解答', 0, 4),
|
||||||
|
('API文档', 'API接口文档', 0, 5),
|
||||||
|
('Vue', 'Vue框架相关', 1, 101),
|
||||||
|
('React', 'React框架相关', 1, 102),
|
||||||
|
('Go', 'Go语言相关', 1, 103);
|
||||||
|
|
||||||
|
-- 插入默认知识库标签
|
||||||
|
INSERT INTO yz_knowledge_tags (tag_name, tag_color, usage_count) VALUES
|
||||||
|
('Vue', '#4CAF50', 0),
|
||||||
|
('React', '#42A5F5', 0),
|
||||||
|
('TypeScript', '#3178C6', 0),
|
||||||
|
('Element Plus', '#409EFF', 0),
|
||||||
|
('Vue Router', '#4FC08D', 0),
|
||||||
|
('Pinia', '#FFD54F', 0),
|
||||||
|
('Go', '#00ADD8', 0),
|
||||||
|
('Python', '#3776AB', 0);
|
||||||
|
|
||||||
-- 插入默认程序分类
|
-- 插入默认程序分类
|
||||||
INSERT INTO yz_program_category (category_name, category_desc, parent_id, sort_order) VALUES
|
INSERT INTO yz_program_category (category_name, category_desc, parent_id, sort_order) VALUES
|
||||||
('办公软件', '办公相关程序', 0, 1),
|
('办公软件', '办公相关程序', 0, 1),
|
||||||
|
|||||||
97
server/database/update_knowledge_category_join.md
Normal file
97
server/database/update_knowledge_category_join.md
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# 知识库分类联查更新说明
|
||||||
|
|
||||||
|
## 更新内容
|
||||||
|
|
||||||
|
本次更新将 `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
|
||||||
|
|
||||||
94
server/database/yz_knowledge.sql
Normal file
94
server/database/yz_knowledge.sql
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
-- 创建知识库分类表
|
||||||
|
CREATE TABLE `yz_knowledge_category` (
|
||||||
|
`category_id` INT NOT NULL AUTO_INCREMENT COMMENT '分类ID',
|
||||||
|
`category_name` VARCHAR(100) NOT NULL COMMENT '分类名称',
|
||||||
|
`category_desc` VARCHAR(500) DEFAULT NULL COMMENT '分类描述',
|
||||||
|
`parent_id` INT DEFAULT 0 COMMENT '父分类ID,0表示顶级分类',
|
||||||
|
`sort_order` INT DEFAULT 0 COMMENT '排序序号',
|
||||||
|
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`category_id`),
|
||||||
|
KEY `idx_parent_id` (`parent_id`),
|
||||||
|
KEY `idx_sort_order` (`sort_order`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库分类表';
|
||||||
|
|
||||||
|
-- 创建知识库标签表
|
||||||
|
CREATE TABLE `yz_knowledge_tags` (
|
||||||
|
`tag_id` INT NOT NULL AUTO_INCREMENT COMMENT '标签ID',
|
||||||
|
`tag_name` VARCHAR(50) NOT NULL COMMENT '标签名称',
|
||||||
|
`tag_color` VARCHAR(20) DEFAULT NULL COMMENT '标签颜色',
|
||||||
|
`usage_count` INT DEFAULT 0 COMMENT '使用次数',
|
||||||
|
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`tag_id`),
|
||||||
|
UNIQUE KEY `uk_tag_name` (`tag_name`),
|
||||||
|
KEY `idx_usage_count` (`usage_count`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库标签表';
|
||||||
|
|
||||||
|
-- 创建知识库内容表
|
||||||
|
CREATE TABLE `yz_knowledge` (
|
||||||
|
`knowledge_id` INT NOT NULL AUTO_INCREMENT COMMENT '知识ID',
|
||||||
|
`title` VARCHAR(200) NOT NULL COMMENT '标题',
|
||||||
|
`category_id` INT DEFAULT NULL COMMENT '分类ID',
|
||||||
|
`tags` TEXT COMMENT '标签JSON数组,存储标签名称',
|
||||||
|
`author` VARCHAR(50) NOT NULL COMMENT '作者',
|
||||||
|
`content` LONGTEXT COMMENT '正文内容(富文本)',
|
||||||
|
`summary` VARCHAR(500) DEFAULT NULL COMMENT '摘要',
|
||||||
|
`cover_url` VARCHAR(500) DEFAULT NULL COMMENT '封面图片URL',
|
||||||
|
`status` TINYINT DEFAULT 0 COMMENT '状态:0-草稿,1-已发布,2-已归档',
|
||||||
|
`view_count` INT DEFAULT 0 COMMENT '查看次数',
|
||||||
|
`like_count` INT DEFAULT 0 COMMENT '点赞数',
|
||||||
|
`is_recommend` TINYINT DEFAULT 0 COMMENT '是否推荐:0-否,1-是',
|
||||||
|
`is_top` TINYINT DEFAULT 0 COMMENT '是否置顶:0-否,1-是',
|
||||||
|
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
`create_by` VARCHAR(50) DEFAULT NULL COMMENT '创建人',
|
||||||
|
`update_by` VARCHAR(50) DEFAULT NULL COMMENT '更新人',
|
||||||
|
PRIMARY KEY (`knowledge_id`),
|
||||||
|
KEY `idx_category_id` (`category_id`),
|
||||||
|
KEY `idx_author` (`author`),
|
||||||
|
KEY `idx_status` (`status`),
|
||||||
|
KEY `idx_create_time` (`create_time`),
|
||||||
|
KEY `idx_view_count` (`view_count`),
|
||||||
|
KEY `idx_is_recommend` (`is_recommend`),
|
||||||
|
KEY `idx_is_top` (`is_top`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库内容表';
|
||||||
|
|
||||||
|
-- 创建知识库收藏表
|
||||||
|
CREATE TABLE `yz_knowledge_favorites` (
|
||||||
|
`favorite_id` INT NOT NULL AUTO_INCREMENT COMMENT '收藏ID',
|
||||||
|
`knowledge_id` INT NOT NULL COMMENT '知识ID',
|
||||||
|
`user_id` INT NOT NULL COMMENT '用户ID',
|
||||||
|
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`favorite_id`),
|
||||||
|
UNIQUE KEY `uk_knowledge_user` (`knowledge_id`, `user_id`),
|
||||||
|
KEY `idx_user_id` (`user_id`),
|
||||||
|
CONSTRAINT `yz_fk_fav_knowledge` FOREIGN KEY (`knowledge_id`) REFERENCES `yz_knowledge` (`knowledge_id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `yz_fk_fav_user` FOREIGN KEY (`user_id`) REFERENCES `yz_users` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库收藏表';
|
||||||
|
|
||||||
|
-- 插入默认分类
|
||||||
|
INSERT INTO `yz_knowledge_category` (`category_name`, `category_desc`, `parent_id`, `sort_order`) VALUES
|
||||||
|
('技术文档', '技术相关的文档资料', 0, 1),
|
||||||
|
('产品手册', '产品使用手册和说明', 0, 2),
|
||||||
|
('用户指南', '用户操作指南和教程', 0, 3),
|
||||||
|
('常见问题', '常见问题解答', 0, 4),
|
||||||
|
('API文档', 'API接口文档', 0, 5),
|
||||||
|
('Vue', 'Vue框架相关', 1, 101),
|
||||||
|
('React', 'React框架相关', 1, 102),
|
||||||
|
('Go', 'Go语言相关', 1, 103);
|
||||||
|
|
||||||
|
-- 插入默认标签
|
||||||
|
INSERT INTO `yz_knowledge_tags` (`tag_name`, `tag_color`, `usage_count`) VALUES
|
||||||
|
('Vue', '#4CAF50', 0),
|
||||||
|
('React', '#42A5F5', 0),
|
||||||
|
('TypeScript', '#3178C6', 0),
|
||||||
|
('Element Plus', '#409EFF', 0),
|
||||||
|
('Vue Router', '#4FC08D', 0),
|
||||||
|
('Pinia', '#FFD54F', 0),
|
||||||
|
('Go', '#00ADD8', 0),
|
||||||
|
('Python', '#3776AB', 0);
|
||||||
|
|
||||||
|
--插入默认知识
|
||||||
|
INSERT INTO `yz_knowledge` (`title`, `category_id`, `tags`, `author`, `content`, `summary`, `cover_url`, `status`, `view_count`, `like_count`, `is_recommend`, `is_top`, `create_time`, `update_time`, `create_by`, `update_by`) VALUES
|
||||||
|
('Vue', 1, 'Vue', 'admin', 'Vue is a progressive framework for building user interfaces.', 'Vue is a progressive framework for building user interfaces.', 'https://vuejs.org/images/logo.png', 1, 0, 0, 0, 0, '2021-01-01 00:00:00', '2021-01-01 00:00:00', 'admin', 'admin');
|
||||||
318
server/models/knowledge.go
Normal file
318
server/models/knowledge.go
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/beego/beego/v2/client/orm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Knowledge 知识库模型
|
||||||
|
type Knowledge struct {
|
||||||
|
Id int `orm:"column(knowledge_id);pk;auto" json:"id"`
|
||||||
|
Title string `orm:"column(title);size(200)" json:"title"`
|
||||||
|
CategoryId int `orm:"column(category_id);default(0);null" json:"categoryId"`
|
||||||
|
CategoryName string `orm:"-" json:"categoryName"` // 不映射到数据库,从联查获取
|
||||||
|
Tags string `orm:"column(tags);type(text);null" json:"tags"`
|
||||||
|
Author string `orm:"column(author);size(50)" json:"author"`
|
||||||
|
Content string `orm:"column(content);type(longtext)" json:"content"`
|
||||||
|
Summary string `orm:"column(summary);size(500);null" json:"summary"`
|
||||||
|
CoverUrl string `orm:"column(cover_url);size(500);null" json:"coverUrl"`
|
||||||
|
Status int8 `orm:"column(status);default(0)" json:"status"`
|
||||||
|
ViewCount int `orm:"column(view_count);default(0)" json:"viewCount"`
|
||||||
|
LikeCount int `orm:"column(like_count);default(0)" json:"likeCount"`
|
||||||
|
IsRecommend int8 `orm:"column(is_recommend);default(0)" json:"isRecommend"`
|
||||||
|
IsTop int8 `orm:"column(is_top);default(0)" json:"isTop"`
|
||||||
|
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"createTime"`
|
||||||
|
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"updateTime"`
|
||||||
|
CreateBy string `orm:"column(create_by);size(50);null" json:"createBy"`
|
||||||
|
UpdateBy string `orm:"column(update_by);size(50);null" json:"updateBy"`
|
||||||
|
DeleteTime time.Time `orm:"column(delete_time);type(datetime);null" json:"deleteTime"`
|
||||||
|
DeleteBy string `orm:"column(delete_by);size(50);null" json:"deleteBy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 设置表名
|
||||||
|
func (k *Knowledge) TableName() string {
|
||||||
|
return "yz_knowledge"
|
||||||
|
}
|
||||||
|
|
||||||
|
// KnowledgeCategory 知识库分类模型
|
||||||
|
type KnowledgeCategory struct {
|
||||||
|
CategoryId int `orm:"column(category_id);pk;auto" json:"categoryId"`
|
||||||
|
CategoryName string `orm:"column(category_name);size(100)" json:"categoryName"`
|
||||||
|
CategoryDesc string `orm:"column(category_desc);size(500);null" json:"categoryDesc"`
|
||||||
|
ParentId int `orm:"column(parent_id);default(0)" json:"parentId"`
|
||||||
|
SortOrder int `orm:"column(sort_order);default(0)" json:"sortOrder"`
|
||||||
|
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"createTime"`
|
||||||
|
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"updateTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 设置表名
|
||||||
|
func (kc *KnowledgeCategory) TableName() string {
|
||||||
|
return "yz_knowledge_category"
|
||||||
|
}
|
||||||
|
|
||||||
|
// KnowledgeTag 知识库标签模型
|
||||||
|
type KnowledgeTag struct {
|
||||||
|
TagId int `orm:"column(tag_id);pk;auto" json:"tagId"`
|
||||||
|
TagName string `orm:"column(tag_name);size(50);unique" json:"tagName"`
|
||||||
|
TagColor string `orm:"column(tag_color);size(20);null" json:"tagColor"`
|
||||||
|
UsageCount int `orm:"column(usage_count);default(0)" json:"usageCount"`
|
||||||
|
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"createTime"`
|
||||||
|
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"updateTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 设置表名
|
||||||
|
func (kt *KnowledgeTag) TableName() string {
|
||||||
|
return "yz_knowledge_tags"
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddKnowledge 创建知识
|
||||||
|
func AddKnowledge(k *Knowledge) (int64, error) {
|
||||||
|
o := orm.NewOrm()
|
||||||
|
id, err := o.Insert(k)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKnowledgeById 根据ID获取知识详情(使用联查获取分类名称)
|
||||||
|
func GetKnowledgeById(id int) (*Knowledge, error) {
|
||||||
|
o := orm.NewOrm()
|
||||||
|
|
||||||
|
// 使用联查获取分类名称(只查询未删除的记录)
|
||||||
|
querySQL := `
|
||||||
|
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 = ? AND k.delete_time IS NULL
|
||||||
|
`
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
Id int `orm:"column(knowledge_id)"`
|
||||||
|
Title string `orm:"column(title)"`
|
||||||
|
CategoryId int `orm:"column(category_id)"`
|
||||||
|
CategoryName string `orm:"column(category_name)"`
|
||||||
|
Tags string `orm:"column(tags)"`
|
||||||
|
Author string `orm:"column(author)"`
|
||||||
|
Content string `orm:"column(content)"`
|
||||||
|
Summary string `orm:"column(summary)"`
|
||||||
|
CoverUrl string `orm:"column(cover_url)"`
|
||||||
|
Status int8 `orm:"column(status)"`
|
||||||
|
ViewCount int `orm:"column(view_count)"`
|
||||||
|
LikeCount int `orm:"column(like_count)"`
|
||||||
|
IsRecommend int8 `orm:"column(is_recommend)"`
|
||||||
|
IsTop int8 `orm:"column(is_top)"`
|
||||||
|
CreateTime time.Time `orm:"column(create_time)"`
|
||||||
|
UpdateTime time.Time `orm:"column(update_time)"`
|
||||||
|
CreateBy string `orm:"column(create_by)"`
|
||||||
|
UpdateBy string `orm:"column(update_by)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := o.Raw(querySQL, id).QueryRow(&result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
knowledge := &Knowledge{
|
||||||
|
Id: result.Id,
|
||||||
|
Title: result.Title,
|
||||||
|
CategoryId: result.CategoryId,
|
||||||
|
CategoryName: result.CategoryName,
|
||||||
|
Tags: result.Tags,
|
||||||
|
Author: result.Author,
|
||||||
|
Content: result.Content,
|
||||||
|
Summary: result.Summary,
|
||||||
|
CoverUrl: result.CoverUrl,
|
||||||
|
Status: result.Status,
|
||||||
|
ViewCount: result.ViewCount,
|
||||||
|
LikeCount: result.LikeCount,
|
||||||
|
IsRecommend: result.IsRecommend,
|
||||||
|
IsTop: result.IsTop,
|
||||||
|
CreateTime: result.CreateTime,
|
||||||
|
UpdateTime: result.UpdateTime,
|
||||||
|
CreateBy: result.CreateBy,
|
||||||
|
UpdateBy: result.UpdateBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 增加查看次数
|
||||||
|
knowledge.ViewCount++
|
||||||
|
_, err = o.Raw("UPDATE yz_knowledge SET view_count = ? WHERE knowledge_id = ?", knowledge.ViewCount, id).Exec()
|
||||||
|
|
||||||
|
return knowledge, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllKnowledge 获取所有知识(支持分页和筛选)
|
||||||
|
func GetAllKnowledge(page, pageSize int, status int8, categoryId int, keyword string) ([]*Knowledge, int64, error) {
|
||||||
|
o := orm.NewOrm()
|
||||||
|
|
||||||
|
// 构建 WHERE 条件(只查询未删除的记录)
|
||||||
|
whereSQL := "delete_time IS NULL"
|
||||||
|
params := []interface{}{}
|
||||||
|
|
||||||
|
// 状态筛选
|
||||||
|
if status >= 0 {
|
||||||
|
whereSQL += " AND status = ?"
|
||||||
|
params = append(params, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分类筛选
|
||||||
|
if categoryId > 0 {
|
||||||
|
whereSQL += " AND category_id = ?"
|
||||||
|
params = append(params, categoryId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关键词搜索
|
||||||
|
if keyword != "" {
|
||||||
|
whereSQL += " AND title LIKE ?"
|
||||||
|
params = append(params, "%"+keyword+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
var total int64
|
||||||
|
countSQL := "SELECT COUNT(*) FROM yz_knowledge WHERE " + whereSQL
|
||||||
|
err := o.Raw(countSQL, params...).QueryRow(&total)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 联查获取分类名称
|
||||||
|
querySQL := `
|
||||||
|
SELECT k.*, c.category_name
|
||||||
|
FROM yz_knowledge k
|
||||||
|
LEFT JOIN yz_knowledge_category c ON k.category_id = c.category_id
|
||||||
|
WHERE ` + whereSQL + `
|
||||||
|
ORDER BY k.is_top DESC, k.create_time DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
`
|
||||||
|
params = append(params, pageSize, (page-1)*pageSize)
|
||||||
|
|
||||||
|
var knowledges []*Knowledge
|
||||||
|
var results []struct {
|
||||||
|
Id int `orm:"column(knowledge_id)"`
|
||||||
|
Title string `orm:"column(title)"`
|
||||||
|
CategoryId int `orm:"column(category_id)"`
|
||||||
|
CategoryName string `orm:"column(category_name)"`
|
||||||
|
Tags string `orm:"column(tags)"`
|
||||||
|
Author string `orm:"column(author)"`
|
||||||
|
Content string `orm:"column(content)"`
|
||||||
|
Summary string `orm:"column(summary)"`
|
||||||
|
CoverUrl string `orm:"column(cover_url)"`
|
||||||
|
Status int8 `orm:"column(status)"`
|
||||||
|
ViewCount int `orm:"column(view_count)"`
|
||||||
|
LikeCount int `orm:"column(like_count)"`
|
||||||
|
IsRecommend int8 `orm:"column(is_recommend)"`
|
||||||
|
IsTop int8 `orm:"column(is_top)"`
|
||||||
|
CreateTime time.Time `orm:"column(create_time)"`
|
||||||
|
UpdateTime time.Time `orm:"column(update_time)"`
|
||||||
|
CreateBy string `orm:"column(create_by)"`
|
||||||
|
UpdateBy string `orm:"column(update_by)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = o.Raw(querySQL, params...).QueryRows(&results)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换结果
|
||||||
|
for _, r := range results {
|
||||||
|
k := &Knowledge{
|
||||||
|
Id: r.Id,
|
||||||
|
Title: r.Title,
|
||||||
|
CategoryId: r.CategoryId,
|
||||||
|
CategoryName: r.CategoryName,
|
||||||
|
Tags: r.Tags,
|
||||||
|
Author: r.Author,
|
||||||
|
Content: r.Content,
|
||||||
|
Summary: r.Summary,
|
||||||
|
CoverUrl: r.CoverUrl,
|
||||||
|
Status: r.Status,
|
||||||
|
ViewCount: r.ViewCount,
|
||||||
|
LikeCount: r.LikeCount,
|
||||||
|
IsRecommend: r.IsRecommend,
|
||||||
|
IsTop: r.IsTop,
|
||||||
|
CreateTime: r.CreateTime,
|
||||||
|
UpdateTime: r.UpdateTime,
|
||||||
|
CreateBy: r.CreateBy,
|
||||||
|
UpdateBy: r.UpdateBy,
|
||||||
|
}
|
||||||
|
knowledges = append(knowledges, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
return knowledges, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateKnowledge 更新知识
|
||||||
|
func UpdateKnowledge(id int, k *Knowledge) error {
|
||||||
|
o := orm.NewOrm()
|
||||||
|
knowledge := &Knowledge{Id: id}
|
||||||
|
err := o.Read(knowledge)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新字段(不包含category_name,因为它是从联查获取的)
|
||||||
|
knowledge.Title = k.Title
|
||||||
|
knowledge.CategoryId = k.CategoryId
|
||||||
|
knowledge.Tags = k.Tags
|
||||||
|
knowledge.Author = k.Author
|
||||||
|
knowledge.Content = k.Content
|
||||||
|
knowledge.Summary = k.Summary
|
||||||
|
knowledge.CoverUrl = k.CoverUrl
|
||||||
|
knowledge.Status = k.Status
|
||||||
|
knowledge.IsRecommend = k.IsRecommend
|
||||||
|
knowledge.IsTop = k.IsTop
|
||||||
|
knowledge.UpdateBy = k.UpdateBy
|
||||||
|
|
||||||
|
_, err = o.Update(knowledge, "title", "category_id", "tags", "author", "content", "summary", "cover_url", "status", "is_recommend", "is_top", "update_by", "update_time")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteKnowledge 软删除知识
|
||||||
|
func DeleteKnowledge(id int, deleteBy string) error {
|
||||||
|
o := orm.NewOrm()
|
||||||
|
knowledge := &Knowledge{Id: id}
|
||||||
|
err := o.Read(knowledge)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行软删除:设置 delete_time 和 delete_by
|
||||||
|
_, err = o.Raw("UPDATE yz_knowledge SET delete_time = ?, delete_by = ? WHERE knowledge_id = ?", time.Now(), deleteBy, id).Exec()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllCategories 获取所有分类
|
||||||
|
func GetAllCategories() ([]*KnowledgeCategory, error) {
|
||||||
|
o := orm.NewOrm()
|
||||||
|
var categories []*KnowledgeCategory
|
||||||
|
_, err := o.QueryTable("yz_knowledge_category").OrderBy("sort_order").All(&categories)
|
||||||
|
return categories, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCategoryById 根据ID获取分类
|
||||||
|
func GetCategoryById(id int) (*KnowledgeCategory, error) {
|
||||||
|
o := orm.NewOrm()
|
||||||
|
category := &KnowledgeCategory{CategoryId: id}
|
||||||
|
err := o.Read(category)
|
||||||
|
return category, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllTags 获取所有标签
|
||||||
|
func GetAllTags() ([]*KnowledgeTag, error) {
|
||||||
|
o := orm.NewOrm()
|
||||||
|
var tags []*KnowledgeTag
|
||||||
|
_, err := o.QueryTable("yz_knowledge_tags").All(&tags)
|
||||||
|
return tags, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCategory 添加分类
|
||||||
|
func AddCategory(category *KnowledgeCategory) (int64, error) {
|
||||||
|
o := orm.NewOrm()
|
||||||
|
id, err := o.Insert(category)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTag 添加标签
|
||||||
|
func AddTag(tag *KnowledgeTag) (int64, error) {
|
||||||
|
o := orm.NewOrm()
|
||||||
|
id, err := o.Insert(tag)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
@ -268,6 +268,9 @@ func Init() {
|
|||||||
orm.RegisterModel(new(ProgramCategory))
|
orm.RegisterModel(new(ProgramCategory))
|
||||||
orm.RegisterModel(new(ProgramInfo))
|
orm.RegisterModel(new(ProgramInfo))
|
||||||
orm.RegisterModel(new(FileInfo))
|
orm.RegisterModel(new(FileInfo))
|
||||||
|
orm.RegisterModel(new(Knowledge))
|
||||||
|
orm.RegisterModel(new(KnowledgeCategory))
|
||||||
|
orm.RegisterModel(new(KnowledgeTag))
|
||||||
|
|
||||||
ormConfig, err := beego.AppConfig.String("orm")
|
ormConfig, err := beego.AppConfig.String("orm")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -82,6 +82,17 @@ func init() {
|
|||||||
// 文件管理路由 - 自动映射到 /api/file/*
|
// 文件管理路由 - 自动映射到 /api/file/*
|
||||||
beego.AutoRouter(&controllers.FileController{})
|
beego.AutoRouter(&controllers.FileController{})
|
||||||
|
|
||||||
|
// 知识库路由
|
||||||
|
beego.Router("/api/knowledge/list", &controllers.KnowledgeController{}, "get:List")
|
||||||
|
beego.Router("/api/knowledge/detail", &controllers.KnowledgeController{}, "get:Detail")
|
||||||
|
beego.Router("/api/knowledge/create", &controllers.KnowledgeController{}, "post:Create")
|
||||||
|
beego.Router("/api/knowledge/update", &controllers.KnowledgeController{}, "post:Update")
|
||||||
|
beego.Router("/api/knowledge/delete", &controllers.KnowledgeController{}, "post:Delete")
|
||||||
|
beego.Router("/api/knowledge/categories", &controllers.KnowledgeController{}, "get:GetCategories")
|
||||||
|
beego.Router("/api/knowledge/tags", &controllers.KnowledgeController{}, "get:GetTags")
|
||||||
|
beego.Router("/api/knowledge/category/add", &controllers.KnowledgeController{}, "post:AddCategory")
|
||||||
|
beego.Router("/api/knowledge/tag/add", &controllers.KnowledgeController{}, "post:AddTag")
|
||||||
|
|
||||||
// 手动配置特殊路由(无法通过自动路由处理的)
|
// 手动配置特殊路由(无法通过自动路由处理的)
|
||||||
beego.Router("/api/menus/active", &controllers.MenuController{}, "get:GetActiveMenus")
|
beego.Router("/api/menus/active", &controllers.MenuController{}, "get:GetActiveMenus")
|
||||||
beego.Router("/api/allmenu", &controllers.MenuController{}, "get:GetAllMenus")
|
beego.Router("/api/allmenu", &controllers.MenuController{}, "get:GetAllMenus")
|
||||||
|
|||||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user