完成知识库模块

This commit is contained in:
2025-10-28 16:08:40 +08:00
parent 6cd942fb29
commit 1062bcfb70
25 changed files with 3170 additions and 384 deletions
+616 -15
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -11,6 +11,10 @@
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@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",
"chart.js": "^4.5.1",
"element-plus": "^2.10.7",
+125
View 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;
}
+2
View File
@@ -71,6 +71,7 @@ $transition-base: all 0.3s cubic-bezier(.25,.8,.25,1);
// 组件颜色
--card-bg: #ffffff;
--card-bg1: #1890ff;
--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
--sidebar-bg: #f8f9fa;
--sidebar-hover: #e9ecef;
@@ -146,6 +147,7 @@ $transition-base: all 0.3s cubic-bezier(.25,.8,.25,1);
// 组件颜色
--card-bg: #2d2d2d;
--card-bg1: #2d2d2d;
--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
--sidebar-bg: #1f1f1f;
--sidebar-hover: #3d3d3d;
+174
View File
@@ -27,3 +27,177 @@
gap: 8px;
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
View 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>
+6
View File
@@ -14,6 +14,9 @@ import router from './router';
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
// 导入全局组件
import WangEditor from '@/components/WangEditor.vue';
const app = createApp(App);
// 注册 Element Plus
@@ -24,6 +27,9 @@ for (const [key, component] of Object.entries(Icons)) {
app.component(key, component);
}
// 全局注册 WangEditor 组件
app.component('WangEditor', WangEditor);
// 创建 Pinia 实例
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
+45 -4
View File
@@ -74,7 +74,7 @@ class DynamicRouteManager {
}
const menus = response.data;
// console.log('从数据库获取到菜单数据:', menus);
// // console.log('从数据库获取到菜单数据:', menus);
// 获取主布局路由
const mainRoute = router.getRoutes().find(route => route.name === 'main');
@@ -90,7 +90,7 @@ class DynamicRouteManager {
}
this.routesLoaded = true;
// console.log("动态路由生成完成,共添加", menus.length, "个菜单路由");
// // console.log("动态路由生成完成,共添加", menus.length, "个菜单路由");
} catch (error) {
console.error("从数据库生成动态路由失败:", error);
// 即使失败也标记为已加载,避免重复尝试
@@ -125,7 +125,48 @@ class DynamicRouteManager {
// 检查路由是否已存在
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;
}
@@ -182,7 +223,7 @@ class DynamicRouteManager {
};
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="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>
</div>
@@ -11,23 +13,25 @@
<!-- 左侧信息面板 -->
<div class="info-panel">
<el-card shadow="never">
<div slot="header">
<div slot="header" class="header">
<span>基本信息</span>
</div>
<el-divider />
<el-form label-width="80px" size="small">
<el-form-item label="标题:">
<span>{{ formData.title }}</span>
</el-form-item>
<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 label="标签:">
<el-tag
v-for="tag in formData.tags"
:key="tag"
size="mini"
size="small"
style="margin-right: 4px"
>{{ tag }}</el-tag>
>{{ tag }}</el-tag
>
</el-form-item>
<el-form-item label="作者:">
<span>{{ formData.author }}</span>
@@ -39,13 +43,16 @@
<span>{{ formData.updateTime }}</span>
</el-form-item>
</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>
<!-- 操作按钮 -->
<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>
<!-- 右侧内容面板 -->
@@ -61,110 +68,154 @@
</div>
</template>
<script>
import { marked } from 'marked'
import { getKnowledgeDetail, deleteKnowledge } from '@/api/knowledge'
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { marked } from "marked"
import { getKnowledgeDetail, deleteKnowledge } from "@/api/knowledge"
export default {
name: 'KnowledgeDetail',
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 route = useRoute()
const router = useRouter()
//
goBack() {
this.$router.push('/apps/knowledge')
},
const knowledgeTitle = ref('知识详情')
//
handleEdit() {
this.$router.push(`/apps/knowledge/edit/${this.id}`)
},
interface FormData {
title: string,
category: string,
tags: string[],
author: string,
createTime: string,
updateTime: string,
content: string,
}
const formData = ref<FormData>({
title: "",
category: "",
tags: [],
author: "",
createTime: "",
updateTime: "",
content: "",
})
//
handleDelete() {
this.$confirm('确认删除该知识?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await deleteKnowledge(this.id)
this.$message.success('删除成功')
this.goBack()
} catch (e) {
this.$message.error('删除失败')
}
})
}
const id = computed(() => {
const queryId = route.query.id
const paramId = route.params.id
if (queryId) return Array.isArray(queryId) ? queryId[0] : queryId
if (paramId) return Array.isArray(paramId) ? paramId[0] : paramId
return ''
})
const compiledMarkdown = computed(() => marked(formData.value.content || ""))
function parseTags(tagsStr: string): string[] {
if (!tagsStr) return []
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>
<style scoped>
<style scoped lang="scss">
/* 全屏显示,覆盖父容器 */
.knowledge-detail {
padding: 16px;
background: #f5f5f5;
min-height: 100vh;
position: fixed;
top: 81px; /* 减去 header 高度 */
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 {
display: flex;
align-items: center;
margin-bottom: 16px;
background: #fff;
padding: 12px 16px;
border-radius: 4px;
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);
}
.detail-header h2 {
margin: 0 0 0 12px;
font-size: 18px;
font-weight: 500;
font-size: 20px;
font-weight: 600;
color: var(--text-color);
}
.detail-body {
display: flex;
gap: 16px;
gap: 20px;
/* padding: 0 24px 0 24px; */
}
.info-panel {
width: 320px;
flex-shrink: 0;
.header{
// margin-bottom: 14px;
}
}
.content-panel {
@@ -179,5 +230,26 @@ export default {
.markdown-body {
font-size: 14px;
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>
@@ -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>
+257 -206
View File
@@ -11,7 +11,7 @@
<el-select
v-model="searchType"
placeholder="知识库"
size="medium"
size="default"
class="search-select"
style="width: auto"
>
@@ -20,13 +20,13 @@
<el-input
v-model="keyword"
placeholder="请输入关键字、产品编码进行查询"
size="medium"
size="default"
class="search-input"
@keyup.enter.native="handleSearch"
@keyup.enter="handleSearch"
/>
<el-button
type="primary"
size="medium"
size="default"
class="search-button"
@click="handleSearch"
>
@@ -60,24 +60,13 @@
:key="index"
class="stat-col"
>
<div class="stat-card" :style="{ '--stat-color': stat.color }">
<div class="stat-card">
<div class="stat-icon">
<i :class="stat.icon"></i>
</div>
<div class="stat-info">
<div class="stat-label">{{ stat.label }}</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 class="stat-value">{{ stat.value }}</div>
</div>
</div>
</el-col>
@@ -106,21 +95,17 @@
<!-- 卡片头部 -->
<div class="repo-header">
<div class="repo-icon">
<i class="el-icon-folder"></i>
<i class="fa-solid fa-book"></i>
</div>
<div class="repo-title-container">
<h3 class="repo-name">{{ repo.name }}</h3>
<h3 class="repo-name">{{ repo.title }}</h3>
<div class="repo-meta">
<el-tag v-if="repo.isPrivate" size="mini" class="private-tag">
<i class="el-icon-lock"></i> 私有
<el-tag size="small" class="category-tag">
{{ repo.categoryName || '未分类' }}
</el-tag>
<span class="repo-owner">
<img
src="https://picsum.photos/seed/{{repo.creatorName}}/24/24"
alt="创建者"
class="owner-avatar"
/>
{{ repo.creatorName }}
<i class="fa-solid fa-user"></i>
{{ repo.author }}
</span>
</div>
</div>
@@ -128,22 +113,29 @@
<!-- 卡片内容 -->
<div class="repo-content">
<p class="repo-description">
{{ repo.description || "暂无描述信息" }}
</p>
<div class="repo-tags" v-if="repo.tags">
<el-tag
v-for="tag in parseTags(repo.tags)"
:key="tag"
size="small"
class="tag-item"
>
{{ tag }}
</el-tag>
</div>
<div class="repo-stats">
<div class="stat-item">
<i class="el-icon-document"></i>
<span>{{ repo.docCount }} 文档</span>
<i class="fa-solid fa-eye"></i>
<span>{{ repo.viewCount || 0 }} 浏览</span>
</div>
<div class="stat-item">
<i class="el-icon-eye"></i>
<span>{{ Math.floor(Math.random() * 1000) + 100 }} 浏览</span>
<i class="fa-solid fa-heart"></i>
<span>{{ repo.likeCount || 0 }} 点赞</span>
</div>
<div class="stat-item">
<i class="el-icon-time"></i>
<span>3天前</span>
<i class="fa-solid fa-clock"></i>
<span>{{ formatDate(repo.createTime) }}</span>
</div>
</div>
</div>
@@ -192,148 +184,224 @@
</div>
</template>
<script>
export default {
name: "KnowledgeHome",
data() {
return {
keyword: "",
filterType: "all",
searchType: "knowledge",
hotTags: ["化妆品", "汽车零部件", "口罩", "工业用品", "食品"],
stats: {
total: 0,
docCount: 0,
memberCount: 0,
viewCount: 0,
},
repoList: [],
total: 0,
pageSize: 12,
currentPage: 1,
};
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import { getKnowledgeList, deleteKnowledge } from '@/api/knowledge';
//
interface Knowledge {
id: number;
title: string;
content: string;
categoryName: string;
tags: string;
author: string;
viewCount: number;
likeCount: number;
createTime: string;
updateTime: string;
}
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: {
//
//
statsList() {
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%'
}
];
},
{
label: '今日新增',
value: '12',
icon: 'fa fa-plus',
},
created() {
this.fetchStats();
this.fetchRepoList();
{
label: '本周更新',
value: '28',
icon: 'fa fa-refresh',
},
methods: {
//
async fetchStats() {
//
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();
},
{
label: '协作项目',
value: '6',
icon: 'fa fa-users',
},
};
]);
//
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>
<style lang="scss" scoped>
@@ -341,12 +409,13 @@ export default {
//
.hero.new-style {
background: linear-gradient(135deg, var(--primary-color), var(--info-color));
background: var(--card-bg1);
padding: 60px 20px;
margin-bottom: 30px;
position: relative;
overflow: hidden;
transition: var(--transition-base);
border-radius: 8px;
//
&::after {
@@ -356,7 +425,7 @@ export default {
right: 0;
width: 500px;
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-position: right bottom;
opacity: 0.1;
@@ -510,25 +579,6 @@ export default {
margin-bottom: 4px;
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 {
margin-bottom: 40px;
padding-bottom: 40px;
.repos-header {
display: flex;
@@ -647,17 +698,17 @@ export default {
.repo-content {
padding: 16px 20px;
.repo-description {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.5;
margin: 0 0 16px 0;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
height: 42px;
.repo-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 16px;
.tag-item {
background: var(--background-hover);
color: var(--text-color);
border-color: var(--border-color);
}
}
.repo-stats {
+2 -20
View File
@@ -18,19 +18,12 @@
circle
size="small"
/>
<button
class="menu-toggle"
@click="toggleSidebar"
aria-label="Toggle menu"
>
<svg-icon name="menu" />
</button>
<h1 class="page-title">Dashboard</h1>
</div>
<div class="header-right">
<div class="search-bar">
<svg-icon name="search" class="search-icon" />
<i class="fas fa-search search-icon"></i>
<input
type="text"
placeholder="Search..."
@@ -172,18 +165,6 @@ const isDark = computed(() => theme.isDark());
align-items: center;
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 {
margin: 0;
font-size: 1.25rem;
@@ -219,6 +200,7 @@ const isDark = computed(() => theme.isDark());
.search-icon {
color: var(--text-secondary);
margin-right: 0.5rem;
font-size: 14px;
}
input {
+3 -3
View File
@@ -207,9 +207,9 @@ const handleMenuClick = (path: string) => {
//
const loadMenuData = async () => {
try {
console.log("开始加载主侧边栏菜单数据...");
// console.log("...");
const response = await menuAPI.getTopLevelMenus();
console.log("主侧边栏菜单API响应:", response);
// console.log("API:", response);
if (response && response.success) {
// 使 data
@@ -222,7 +222,7 @@ const loadMenuData = async () => {
icon: item.Icon,
children: item.Children || [],
})) || [];
console.log("主侧边栏菜单数据加载成功:", menuData.value);
// console.log(":", menuData.value);
} else {
console.error("获取菜单数据失败:", response?.message || "未知错误");
menuData.value = [];
@@ -48,7 +48,7 @@
</template>
<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 SubSidebar from "@/views/components/sub-sidebar.vue";
import { menuAPI } from "@/services/api";
@@ -83,7 +83,7 @@ const pathToMenuIdMap = ref<Record<string, number>>({});
//
const currentSubModule = ref<MenuItem | null>(null);
const dynamicComponent = ref<any>(null);
const dynamicComponent = shallowRef<any>(null); // 使 shallowRef
const componentLoading = ref(false);
const loading = ref(false);
const error = ref("");
@@ -107,7 +107,7 @@ const initMenuMap = async () => {
}
});
pathToMenuIdMap.value = map;
console.log("路径-ID映射表:", map);
// console.log("-ID:", map);
}
} catch (err: any) {
console.error("初始化菜单映射失败:", err.message);
@@ -127,7 +127,7 @@ const computedParentId = computed(() => {
if (route.query.id) {
const queryId = Number(route.query.id);
if (!isNaN(queryId) && queryId > 0) {
console.log("从路由参数获取父ID:", queryId);
// console.log("ID:", queryId);
return queryId;
}
}
@@ -142,24 +142,24 @@ const computedParentId = computed(() => {
if (firstLevelPath) {
const menuId = getMenuIdByPath(firstLevelPath);
if (menuId > 0) {
console.log(`从路径 ${firstLevelPath} 匹配父ID:`, menuId);
// // console.log(` ${firstLevelPath} ID:`, menuId);
return menuId;
}
}
}
// 3. 0
console.log("未匹配到父菜单ID,返回0");
// console.log("ID0");
return 0;
});
// ID
const fetchSubMenuItems = async () => {
console.log('开始获取二级菜单数据:', {
parentId: computedParentId.value,
currentPath: route.path,
currentQuery: route.query
});
// console.log(':', {
// parentId: computedParentId.value,
// currentPath: route.path,
// currentQuery: route.query
// });
loading.value = true;
error.value = "";
@@ -167,14 +167,14 @@ const fetchSubMenuItems = async () => {
try {
const parentId = computedParentId.value;
if (parentId === 0) {
console.log('父ID为0,清空菜单数据');
// console.log('ID0');
subMenuItems.value = [];
loading.value = false;
return;
}
const response = await menuAPI.getMenusByParentId(parentId);
console.log('获取菜单数据响应:', response);
// console.log(':', response);
if (response && response.success) {
subMenuItems.value = response.data
@@ -188,7 +188,7 @@ const fetchSubMenuItems = async () => {
}))
.filter((item: any) => item.path && item.title);
console.log('处理后的二级菜单数据:', subMenuItems.value);
// console.log(':', subMenuItems.value);
//
await selectDefaultMenuItem();
@@ -201,20 +201,20 @@ const fetchSubMenuItems = async () => {
console.error('获取菜单数据异常:', err);
} finally {
loading.value = false;
console.log('获取二级菜单数据完成');
// console.log('');
}
};
const selectDefaultMenuItem = async () => {
console.log('开始选择默认菜单项:', {
currentPath: route.path,
subMenuItems: subMenuItems.value
});
// console.log(':', {
// currentPath: route.path,
// subMenuItems: subMenuItems.value
// });
//
const matchedItem = subMenuItems.value.find(item => item.path === route.path);
if (matchedItem) {
console.log('找到匹配的菜单项:', matchedItem);
// console.log(':', matchedItem);
currentSubModule.value = matchedItem;
await loadDynamicComponent(matchedItem);
return;
@@ -223,28 +223,28 @@ const selectDefaultMenuItem = async () => {
//
if (subMenuItems.value.length > 0) {
const firstItem = subMenuItems.value[0];
console.log('未找到匹配项,选择第一个菜单项:', firstItem);
// console.log(':', firstItem);
currentSubModule.value = firstItem;
//
router.push(firstItem.path);
//
await loadDynamicComponent(firstItem);
} else {
console.log('没有可用的菜单项');
// console.log('');
}
};
const checkComponentExists = async (componentPath: string): Promise<boolean> => {
try {
console.log('检查组件是否存在:', componentPath);
// console.log(':', componentPath);
// 使Viteimport.meta.glob
const modules = import.meta.glob('@/views/**/*.vue');
console.log('可用模块:', Object.keys(modules));
// console.log(':', Object.keys(modules));
//
const normalizedPath = componentPath.replace('@', '/src');
const exists = normalizedPath in modules;
console.log('组件存在检查结果:', { componentPath, normalizedPath, exists });
// console.log(':', { componentPath, normalizedPath, exists });
return exists;
} catch (error) {
@@ -254,12 +254,12 @@ const checkComponentExists = async (componentPath: string): Promise<boolean> =>
};
const loadDynamicComponent = async (menuItem: MenuItem) => {
console.log('开始加载动态组件:', menuItem);
// console.log(':', menuItem);
if (!menuItem.componentPath) {
error.value = "未指定组件路径";
dynamicComponent.value = null;
console.log('组件路径为空');
// console.log('');
return;
}
@@ -269,8 +269,8 @@ const loadDynamicComponent = async (menuItem: MenuItem) => {
try {
//
if (loadedComponents[menuItem.componentPath]) {
console.log('组件已缓存,直接使用:', menuItem.componentPath);
dynamicComponent.value = loadedComponents[menuItem.componentPath];
// console.log('使:', menuItem.componentPath);
dynamicComponent.value = markRaw(loadedComponents[menuItem.componentPath]);
loading.value = false;
return;
}
@@ -281,7 +281,7 @@ const loadDynamicComponent = async (menuItem: MenuItem) => {
throw new Error(`组件文件不存在: ${menuItem.componentPath}`);
}
console.log('开始动态导入组件:', menuItem.componentPath);
// console.log(':', menuItem.componentPath);
// 使Viteimport.meta.glob
const modules = import.meta.glob('@/views/**/*.vue');
@@ -289,24 +289,24 @@ const loadDynamicComponent = async (menuItem: MenuItem) => {
if (modules[normalizedPath]) {
const module = await modules[normalizedPath]();
console.log('动态导入完成:', module);
// console.log(':', module);
//
let component = null;
if (module.default) {
component = module.default;
console.log('使用默认导出组件');
// console.log('使');
} else if (Object.keys(module).length === 1) {
// 使
const key = Object.keys(module)[0];
component = module[key];
console.log('使用单一导出组件:', key);
// console.log('使:', key);
} else {
//
for (const key of Object.keys(module)) {
if (typeof module[key] === 'object' && module[key]?.__name) {
component = module[key];
console.log('找到命名组件:', key);
// console.log(':', key);
break;
}
}
@@ -316,10 +316,12 @@ const loadDynamicComponent = async (menuItem: MenuItem) => {
throw new Error(`无法加载组件: ${menuItem.componentPath}`);
}
console.log('组件加载成功:', component);
// console.log(':', component);
// 使 markRaw
const rawComponent = markRaw(component);
//
loadedComponents[menuItem.componentPath] = component;
dynamicComponent.value = component;
loadedComponents[menuItem.componentPath] = rawComponent;
dynamicComponent.value = rawComponent;
} else {
throw new Error(`无法找到组件模块: ${menuItem.componentPath}`);
}
@@ -347,7 +349,7 @@ const retry = () => {
watch(
() => [route.path, route.query.id],
(newVal, oldVal) => {
console.log('路由变化监听:', { newVal, oldVal });
// console.log(':', { newVal, oldVal });
fetchSubMenuItems();
},
{ immediate: true }
@@ -357,7 +359,7 @@ watch(
watch(
() => route.name,
(newName, oldName) => {
console.log('路由名称变化监听:', { newName, oldName });
// console.log(':', { newName, oldName });
if (newName !== oldName) {
fetchSubMenuItems();
}
@@ -380,27 +382,27 @@ onMounted(async () => {
// keep-alive
onActivated(async () => {
console.log('onActivated被调用:', {
routePath: route.path,
currentSubModulePath: currentSubModule.value?.path,
subMenuItems: subMenuItems.value
});
// console.log('onActivated:', {
// routePath: route.path,
// currentSubModulePath: currentSubModule.value?.path,
// subMenuItems: subMenuItems.value
// });
//
if (route.path !== currentSubModule.value?.path) {
console.log('路由路径发生变化,重新获取菜单数据');
// console.log('');
await fetchSubMenuItems();
//
if (route.path && subMenuItems.value.length > 0) {
const menuItem = subMenuItems.value.find(item => item.path === route.path);
if (menuItem) {
console.log('加载动态组件:', menuItem);
// console.log(':', menuItem);
await loadDynamicComponent(menuItem);
}
}
} else {
console.log('路由路径未发生变化,无需重新加载');
// console.log('');
}
});
</script>