mirror of
https://github.com/niezhicheng/pveui.git
synced 2026-05-06 22:03:04 +08:00
feat(md): 添加知识库
This commit is contained in:
2
backend/apps/knowledge/__init__.py
Normal file
2
backend/apps/knowledge/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
|
||||
13
backend/apps/knowledge/admin.py
Normal file
13
backend/apps/knowledge/admin.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import KnowledgeArticle
|
||||
|
||||
|
||||
@admin.register(KnowledgeArticle)
|
||||
class KnowledgeArticleAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'title', 'category', 'is_published', 'is_pinned', 'created_at', 'updated_at']
|
||||
list_filter = ['category', 'is_published', 'is_pinned', 'created_at']
|
||||
search_fields = ['title', 'summary', 'content', 'tags']
|
||||
readonly_fields = ['created_at', 'updated_at', 'created_by', 'updated_by']
|
||||
|
||||
|
||||
9
backend/apps/knowledge/apps.py
Normal file
9
backend/apps/knowledge/apps.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class KnowledgeConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.knowledge'
|
||||
verbose_name = '知识库'
|
||||
|
||||
|
||||
2
backend/apps/knowledge/migrations/__init__.py
Normal file
2
backend/apps/knowledge/migrations/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
|
||||
39
backend/apps/knowledge/models.py
Normal file
39
backend/apps/knowledge/models.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""知识库模型。"""
|
||||
|
||||
from django.db import models
|
||||
from apps.common.models import BaseAuditModel
|
||||
|
||||
|
||||
class KnowledgeArticle(BaseAuditModel):
|
||||
"""知识库文章。"""
|
||||
|
||||
CATEGORY_CHOICES = [
|
||||
('guide', '使用指南'),
|
||||
('faq', '常见问题'),
|
||||
('troubleshooting', '故障排查'),
|
||||
('other', '其他'),
|
||||
]
|
||||
|
||||
title = models.CharField(max_length=200, verbose_name='标题')
|
||||
summary = models.CharField(max_length=500, blank=True, default='', verbose_name='摘要')
|
||||
content = models.TextField(blank=True, default='', verbose_name='内容')
|
||||
category = models.CharField(max_length=32, choices=CATEGORY_CHOICES, default='guide', verbose_name='分类')
|
||||
tags = models.CharField(max_length=255, blank=True, default='', verbose_name='标签', help_text='逗号分隔')
|
||||
is_published = models.BooleanField(default=True, verbose_name='已发布')
|
||||
is_pinned = models.BooleanField(default=False, verbose_name='置顶')
|
||||
|
||||
class Meta:
|
||||
verbose_name = '知识库文章'
|
||||
verbose_name_plural = '知识库文章'
|
||||
ordering = ['-is_pinned', '-is_published', '-updated_at', '-id']
|
||||
indexes = [
|
||||
models.Index(fields=['category']),
|
||||
models.Index(fields=['is_published']),
|
||||
models.Index(fields=['is_pinned']),
|
||||
models.Index(fields=['created_at']),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.title
|
||||
|
||||
|
||||
82
backend/apps/knowledge/serializers.py
Normal file
82
backend/apps/knowledge/serializers.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""知识库序列化器。"""
|
||||
|
||||
from apps.common.serializers import BaseModelSerializer
|
||||
from apps.rbac.serializers import OrganizationSerializer
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import KnowledgeArticle
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class SimpleUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'username']
|
||||
|
||||
|
||||
class KnowledgeArticleListSerializer(BaseModelSerializer):
|
||||
owner_organization = OrganizationSerializer(read_only=True)
|
||||
created_by = SimpleUserSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = KnowledgeArticle
|
||||
fields = [
|
||||
'id',
|
||||
'title',
|
||||
'summary',
|
||||
'category',
|
||||
'tags',
|
||||
'is_published',
|
||||
'is_pinned',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'created_by',
|
||||
'owner_organization',
|
||||
]
|
||||
|
||||
|
||||
class KnowledgeArticleDetailSerializer(BaseModelSerializer):
|
||||
owner_organization = OrganizationSerializer(read_only=True)
|
||||
created_by = SimpleUserSerializer(read_only=True)
|
||||
updated_by = SimpleUserSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = KnowledgeArticle
|
||||
fields = '__all__'
|
||||
read_only_fields = ['created_at', 'updated_at', 'created_by', 'updated_by']
|
||||
|
||||
|
||||
class KnowledgeArticleCreateSerializer(BaseModelSerializer):
|
||||
class Meta:
|
||||
model = KnowledgeArticle
|
||||
fields = [
|
||||
'title',
|
||||
'summary',
|
||||
'content',
|
||||
'category',
|
||||
'tags',
|
||||
'is_published',
|
||||
'is_pinned',
|
||||
'owner_organization',
|
||||
'remark',
|
||||
]
|
||||
|
||||
|
||||
class KnowledgeArticleUpdateSerializer(BaseModelSerializer):
|
||||
class Meta:
|
||||
model = KnowledgeArticle
|
||||
fields = [
|
||||
'title',
|
||||
'summary',
|
||||
'content',
|
||||
'category',
|
||||
'tags',
|
||||
'is_published',
|
||||
'is_pinned',
|
||||
'owner_organization',
|
||||
'remark',
|
||||
]
|
||||
|
||||
|
||||
15
backend/apps/knowledge/urls.py
Normal file
15
backend/apps/knowledge/urls.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""知识库 URL 路由。"""
|
||||
|
||||
from django.urls import include, path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from .views import KnowledgeArticleViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'articles', KnowledgeArticleViewSet, basename='knowledge-article')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
|
||||
|
||||
63
backend/apps/knowledge/views.py
Normal file
63
backend/apps/knowledge/views.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""知识库视图集。"""
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import filters, permissions, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from apps.common.data_mixins import DataScopeFilterMixin
|
||||
from apps.common.mixins import AuditOwnerPopulateMixin, SoftDeleteMixin
|
||||
from apps.common.viewsets import ActionSerializerMixin
|
||||
|
||||
from .models import KnowledgeArticle
|
||||
from .serializers import (
|
||||
KnowledgeArticleListSerializer,
|
||||
KnowledgeArticleDetailSerializer,
|
||||
KnowledgeArticleCreateSerializer,
|
||||
KnowledgeArticleUpdateSerializer,
|
||||
)
|
||||
|
||||
|
||||
class KnowledgeArticleViewSet(
|
||||
DataScopeFilterMixin,
|
||||
AuditOwnerPopulateMixin,
|
||||
SoftDeleteMixin,
|
||||
ActionSerializerMixin,
|
||||
viewsets.ModelViewSet,
|
||||
):
|
||||
"""知识库文章 CRUD。"""
|
||||
|
||||
queryset = (
|
||||
KnowledgeArticle.objects.select_related(
|
||||
'owner_organization',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
)
|
||||
.all()
|
||||
.order_by('-is_pinned', '-updated_at', '-id')
|
||||
)
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
serializer_class = KnowledgeArticleDetailSerializer
|
||||
list_serializer_class = KnowledgeArticleListSerializer
|
||||
retrieve_serializer_class = KnowledgeArticleDetailSerializer
|
||||
create_serializer_class = KnowledgeArticleCreateSerializer
|
||||
update_serializer_class = KnowledgeArticleUpdateSerializer
|
||||
partial_update_serializer_class = KnowledgeArticleUpdateSerializer
|
||||
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_fields = ['category', 'is_published', 'is_pinned', 'owner_organization']
|
||||
search_fields = ['title', 'summary', 'content', 'tags']
|
||||
ordering_fields = ['id', 'title', 'created_at', 'updated_at', 'is_pinned']
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def categories(self, request):
|
||||
"""返回可选分类。"""
|
||||
return Response(
|
||||
[
|
||||
{'value': key, 'label': label}
|
||||
for key, label in KnowledgeArticle.CATEGORY_CHOICES
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
Binary file not shown.
@@ -65,6 +65,7 @@ class Command(BaseCommand):
|
||||
menu_monitor_root = self._get_or_create_menu('系统监控', 'monitor', '', 'icon-dashboard', None, 2)
|
||||
menu_tools = self._get_or_create_menu('系统工具', 'tools', '', 'icon-tool', None, 3)
|
||||
menu_office = self._get_or_create_menu('系统办公', 'office', '', 'icon-file', None, 4)
|
||||
menu_knowledge_root = self._get_or_create_menu('知识库', 'knowledge', '', 'icon-book', None, 5)
|
||||
|
||||
# 系统管理
|
||||
menu_user = self._get_or_create_menu('用户管理', 'user', 'system/user/index', 'icon-user', menu_system, 1)
|
||||
@@ -87,6 +88,8 @@ class Command(BaseCommand):
|
||||
|
||||
# 系统办公
|
||||
menu_document = self._get_or_create_menu('在线文档', 'document', 'office/document/index', 'icon-file', menu_office, 1)
|
||||
# 知识库
|
||||
menu_knowledge_article = self._get_or_create_menu('知识文章', 'knowledge-article', 'knowledge/article/index', 'icon-book', menu_knowledge_root, 1)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(' ✓ 创建菜单: 系统管理 / 系统监控 / 系统工具 分组完成'))
|
||||
|
||||
@@ -165,6 +168,13 @@ class Command(BaseCommand):
|
||||
perms.append(self._get_or_create_permission('文档部分更新', 'document:partial_update', 'PATCH', r'/api/office/documents/\\d+/', menu_document))
|
||||
perms.append(self._get_or_create_permission('文档删除', 'document:delete', 'DELETE', r'/api/office/documents/\\d+/', menu_document))
|
||||
perms.append(self._get_or_create_permission('文档置顶', 'document:toggle_pin', 'POST', r'/api/office/documents/\\d+/toggle_pin/', menu_document))
|
||||
# 知识库权限
|
||||
perms.append(self._get_or_create_permission('知识库列表', 'knowledge:list', 'GET', '/api/knowledge/articles/', menu_knowledge_article))
|
||||
perms.append(self._get_or_create_permission('知识库创建', 'knowledge:create', 'POST', '/api/knowledge/articles/', menu_knowledge_article))
|
||||
perms.append(self._get_or_create_permission('知识库更新', 'knowledge:update', 'PUT', r'/api/knowledge/articles/\\d+/', menu_knowledge_article))
|
||||
perms.append(self._get_or_create_permission('知识库部分更新', 'knowledge:partial_update', 'PATCH', r'/api/knowledge/articles/\\d+/', menu_knowledge_article))
|
||||
perms.append(self._get_or_create_permission('知识库删除', 'knowledge:delete', 'DELETE', r'/api/knowledge/articles/\\d+/', menu_knowledge_article))
|
||||
perms.append(self._get_or_create_permission('知识库分类查询', 'knowledge:categories', 'GET', '/api/knowledge/articles/categories/', menu_knowledge_article))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f' ✓ 创建权限: {len(perms)} 个'))
|
||||
|
||||
@@ -174,7 +184,7 @@ class Command(BaseCommand):
|
||||
role_admin.permissions.set(perms)
|
||||
role_admin.menus.set([
|
||||
# 顶级
|
||||
menu_dashboard, menu_system, menu_monitor_root, menu_tools, menu_office,
|
||||
menu_dashboard, menu_system, menu_monitor_root, menu_tools, menu_office, menu_knowledge_root,
|
||||
# 系统管理
|
||||
menu_user, menu_role, menu_menu, menu_permission, menu_org, menu_system_setting,
|
||||
# 系统监控
|
||||
@@ -183,6 +193,8 @@ class Command(BaseCommand):
|
||||
menu_codegen, menu_example,
|
||||
# 系统办公
|
||||
menu_document,
|
||||
# 知识库
|
||||
menu_knowledge_article,
|
||||
])
|
||||
role_admin.custom_data_organizations.set([org_root, org_admin])
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -58,6 +58,7 @@ INSTALLED_APPS = [
|
||||
'apps.chat.apps.ChatConfig',
|
||||
'apps.system.apps.SystemConfig',
|
||||
'apps.office.apps.OfficeConfig',
|
||||
'apps.knowledge.apps.KnowledgeConfig',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
@@ -38,6 +38,7 @@ urlpatterns = [
|
||||
path('api/chat/', include('apps.chat.urls')),
|
||||
path('api/system/', include('apps.system.urls')),
|
||||
path('api/office/', include('apps.office.urls')),
|
||||
path('api/knowledge/', include('apps.knowledge.urls')),
|
||||
]
|
||||
|
||||
# 仅当定义了 MEDIA_ROOT 时才添加媒体文件映射(避免导入期 AttributeError)
|
||||
|
||||
56
front-end/src/api/knowledge.js
Normal file
56
front-end/src/api/knowledge.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getKnowledgeList(params) {
|
||||
return request({
|
||||
url: '/api/knowledge/articles/',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
export function getKnowledgeDetail(id) {
|
||||
return request({
|
||||
url: `/api/knowledge/articles/${id}/`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
export function createKnowledge(data) {
|
||||
return request({
|
||||
url: '/api/knowledge/articles/',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function updateKnowledge(id, data) {
|
||||
return request({
|
||||
url: `/api/knowledge/articles/${id}/`,
|
||||
method: 'put',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function patchKnowledge(id, data) {
|
||||
return request({
|
||||
url: `/api/knowledge/articles/${id}/`,
|
||||
method: 'patch',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteKnowledge(id) {
|
||||
return request({
|
||||
url: `/api/knowledge/articles/${id}/`,
|
||||
method: 'delete',
|
||||
})
|
||||
}
|
||||
|
||||
export function getKnowledgeCategories() {
|
||||
return request({
|
||||
url: '/api/knowledge/articles/categories/',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
477
front-end/src/views/knowledge/article/index.vue
Normal file
477
front-end/src/views/knowledge/article/index.vue
Normal file
@@ -0,0 +1,477 @@
|
||||
<template>
|
||||
<div class="knowledge-page">
|
||||
<a-card>
|
||||
<template #title>
|
||||
<a-typography-title :heading="5">知识库</a-typography-title>
|
||||
</template>
|
||||
|
||||
<div class="toolbar">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleCreate">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
新建文章
|
||||
</a-button>
|
||||
<a-input-search
|
||||
v-model="searchText"
|
||||
placeholder="搜索标题、标签或内容"
|
||||
style="width: 240px"
|
||||
allow-clear
|
||||
@search="fetchList"
|
||||
@clear="fetchList"
|
||||
/>
|
||||
<a-select
|
||||
v-model="filters.category"
|
||||
placeholder="选择分类"
|
||||
style="width: 160px"
|
||||
allow-clear
|
||||
@change="handleFilterChange"
|
||||
>
|
||||
<a-option v-for="item in categoryOptions" :key="item.value" :value="item.value">
|
||||
{{ item.label }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
<a-select
|
||||
v-model="filters.is_published"
|
||||
placeholder="发布状态"
|
||||
style="width: 140px"
|
||||
allow-clear
|
||||
@change="handleFilterChange"
|
||||
>
|
||||
<a-option :value="true">已发布</a-option>
|
||||
<a-option :value="false">草稿</a-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
:pagination="pagination"
|
||||
@page-change="handlePageChange"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column title="标题" data-index="title" />
|
||||
<a-table-column title="分类" data-index="category">
|
||||
<template #cell="{ record }">
|
||||
{{ getCategoryLabel(record.category) }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="标签" data-index="tags">
|
||||
<template #cell="{ record }">
|
||||
<a-space wrap>
|
||||
<a-tag v-for="tag in parseTags(record.tags)" :key="tag" color="arcoblue">
|
||||
{{ tag }}
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="状态" data-index="is_published">
|
||||
<template #cell="{ record }">
|
||||
<a-tag :color="record.is_published ? 'green' : 'gray'">
|
||||
{{ record.is_published ? '已发布' : '草稿' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="置顶" data-index="is_pinned">
|
||||
<template #cell="{ record }">
|
||||
<a-tag :color="record.is_pinned ? 'gold' : 'blue'">
|
||||
{{ record.is_pinned ? '置顶' : '普通' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="更新时间" data-index="updated_at">
|
||||
<template #cell="{ record }">
|
||||
{{ formatDate(record.updated_at) }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="操作" :width="210">
|
||||
<template #cell="{ record }">
|
||||
<a-space :size="8">
|
||||
<a-button type="text" size="small" @click="handlePreview(record)">预览</a-button>
|
||||
<a-button type="text" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="editorVisible"
|
||||
:title="currentId ? '编辑文章' : '新建文章'"
|
||||
:fullscreen="true"
|
||||
:footer="false"
|
||||
>
|
||||
<div class="editor-wrapper">
|
||||
<a-space direction="vertical" style="width: 100%">
|
||||
<a-input v-model="form.title" placeholder="请输入标题" allow-clear size="large" />
|
||||
<a-textarea
|
||||
v-model="form.summary"
|
||||
placeholder="请输入摘要"
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
/>
|
||||
<a-select v-model="form.category" placeholder="请选择分类">
|
||||
<a-option v-for="item in categoryOptions" :key="item.value" :value="item.value">
|
||||
{{ item.label }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
<a-input v-model="form.tags" placeholder="标签(逗号分隔)" allow-clear />
|
||||
|
||||
<div class="editor-status">
|
||||
<a-space>
|
||||
<span>发布状态</span>
|
||||
<a-switch v-model="form.is_published" checked-text="已发布" unchecked-text="草稿" />
|
||||
</a-space>
|
||||
<a-space>
|
||||
<span>置顶</span>
|
||||
<a-switch v-model="form.is_pinned" />
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<div class="rich-editor">
|
||||
<Toolbar :editor="editorRef" :default-config="toolbarConfig" :mode="editorMode" />
|
||||
<Editor
|
||||
v-model="editorHtml"
|
||||
:default-config="editorConfig"
|
||||
:mode="editorMode"
|
||||
@onCreated="handleEditorCreated"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="editor-footer">
|
||||
<a-space style="margin-left: auto">
|
||||
<a-button @click="editorVisible = false">关闭</a-button>
|
||||
<a-button type="primary" :loading="saving" @click="handleSave">保存</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<a-modal v-model:visible="previewVisible" :title="previewData.title" :width="900" :footer="false">
|
||||
<div class="preview-wrapper">
|
||||
<div class="preview-meta">
|
||||
<a-space>
|
||||
<a-tag>{{ getCategoryLabel(previewData.category) }}</a-tag>
|
||||
<a-tag :color="previewData.is_published ? 'green' : 'gray'">
|
||||
{{ previewData.is_published ? '已发布' : '草稿' }}
|
||||
</a-tag>
|
||||
<a-tag v-if="previewData.is_pinned" color="gold">置顶</a-tag>
|
||||
</a-space>
|
||||
<div class="preview-time">更新时间:{{ formatDate(previewData.updated_at) }}</div>
|
||||
</div>
|
||||
<div class="preview-content" v-html="previewData.content"></div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onBeforeUnmount, shallowRef } from 'vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconPlus } from '@arco-design/web-vue/es/icon'
|
||||
import '@wangeditor/editor/dist/css/style.css'
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
import {
|
||||
getKnowledgeList,
|
||||
getKnowledgeDetail,
|
||||
createKnowledge,
|
||||
updateKnowledge,
|
||||
deleteKnowledge,
|
||||
getKnowledgeCategories,
|
||||
} from '@/api/knowledge'
|
||||
|
||||
const loading = ref(false)
|
||||
const searchText = ref('')
|
||||
const tableData = ref([])
|
||||
const categoryOptions = ref([])
|
||||
|
||||
const filters = reactive({
|
||||
category: '',
|
||||
is_published: undefined,
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showTotal: true,
|
||||
showPageSize: true,
|
||||
})
|
||||
|
||||
const editorVisible = ref(false)
|
||||
const previewVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
const currentId = ref(null)
|
||||
const previewData = reactive({
|
||||
title: '',
|
||||
category: '',
|
||||
is_published: true,
|
||||
is_pinned: false,
|
||||
updated_at: '',
|
||||
content: '',
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
title: '',
|
||||
summary: '',
|
||||
category: '',
|
||||
tags: '',
|
||||
is_published: true,
|
||||
is_pinned: false,
|
||||
})
|
||||
|
||||
const editorRef = shallowRef(null)
|
||||
const editorHtml = ref('')
|
||||
const editorMode = 'default'
|
||||
const toolbarConfig = {}
|
||||
const editorConfig = { placeholder: '请输入文章内容...' }
|
||||
|
||||
const handleEditorCreated = (editor) => {
|
||||
editorRef.value = editor
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
const editor = editorRef.value
|
||||
if (editor == null) return
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
function parseTags(tags) {
|
||||
if (!tags) return []
|
||||
return tags
|
||||
.split(',')
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function getCategoryLabel(value) {
|
||||
const match = categoryOptions.value.find((item) => item.value === value)
|
||||
return match ? match.label : value || '-'
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return '-'
|
||||
try {
|
||||
const d = new Date(value)
|
||||
if (Number.isNaN(d.getTime())) return value
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(
|
||||
d.getDate(),
|
||||
).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(
|
||||
d.getMinutes(),
|
||||
).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
|
||||
} catch (e) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCategories() {
|
||||
try {
|
||||
const res = await getKnowledgeCategories()
|
||||
categoryOptions.value = Array.isArray(res) ? res : res.data || []
|
||||
} catch (e) {
|
||||
categoryOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize,
|
||||
}
|
||||
if (searchText.value) params.search = searchText.value
|
||||
if (filters.category) params.category = filters.category
|
||||
if (typeof filters.is_published === 'boolean') params.is_published = filters.is_published
|
||||
|
||||
const res = await getKnowledgeList(params)
|
||||
const data = res
|
||||
if (Array.isArray(data.results)) {
|
||||
tableData.value = data.results
|
||||
pagination.total = data.count || data.results.length
|
||||
} else if (Array.isArray(data)) {
|
||||
tableData.value = data
|
||||
pagination.total = data.length
|
||||
} else {
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
}
|
||||
} catch (e) {
|
||||
Message.error('获取知识库列表失败:' + (e.message || '未知错误'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageChange(page) {
|
||||
pagination.current = page
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handlePageSizeChange(size) {
|
||||
pagination.pageSize = size
|
||||
pagination.current = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleFilterChange() {
|
||||
pagination.current = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.title = ''
|
||||
form.summary = ''
|
||||
form.category = ''
|
||||
form.tags = ''
|
||||
form.is_published = true
|
||||
form.is_pinned = false
|
||||
editorHtml.value = ''
|
||||
currentId.value = null
|
||||
}
|
||||
|
||||
function handleCreate() {
|
||||
resetForm()
|
||||
editorVisible.value = true
|
||||
}
|
||||
|
||||
async function handleEdit(record) {
|
||||
try {
|
||||
const res = await getKnowledgeDetail(record.id)
|
||||
const data = res
|
||||
currentId.value = data.id
|
||||
form.title = data.title || ''
|
||||
form.summary = data.summary || ''
|
||||
form.category = data.category || ''
|
||||
form.tags = data.tags || ''
|
||||
form.is_published = !!data.is_published
|
||||
form.is_pinned = !!data.is_pinned
|
||||
editorHtml.value = data.content || ''
|
||||
editorVisible.value = true
|
||||
} catch (e) {
|
||||
Message.error('获取文章详情失败:' + (e.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePreview(record) {
|
||||
try {
|
||||
const res = await getKnowledgeDetail(record.id)
|
||||
Object.assign(previewData, {
|
||||
title: res.title,
|
||||
category: res.category,
|
||||
is_published: res.is_published,
|
||||
is_pinned: res.is_pinned,
|
||||
updated_at: res.updated_at,
|
||||
content: res.content,
|
||||
})
|
||||
previewVisible.value = true
|
||||
} catch (e) {
|
||||
Message.error('预览失败:' + (e.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.title) {
|
||||
Message.warning('请输入标题')
|
||||
return
|
||||
}
|
||||
if (!editorHtml.value) {
|
||||
Message.warning('请输入文章内容')
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const payload = {
|
||||
title: form.title,
|
||||
summary: form.summary,
|
||||
category: form.category,
|
||||
tags: form.tags,
|
||||
is_published: form.is_published,
|
||||
is_pinned: form.is_pinned,
|
||||
content: editorHtml.value,
|
||||
}
|
||||
if (currentId.value) {
|
||||
await updateKnowledge(currentId.value, payload)
|
||||
Message.success('更新成功')
|
||||
} else {
|
||||
await createKnowledge(payload)
|
||||
Message.success('创建成功')
|
||||
}
|
||||
editorVisible.value = false
|
||||
fetchList()
|
||||
} catch (e) {
|
||||
Message.error('保存失败:' + (e.message || '未知错误'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete(record) {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除文章「${record.title}」吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteKnowledge(record.id)
|
||||
Message.success('删除成功')
|
||||
fetchList()
|
||||
} catch (e) {
|
||||
Message.error('删除失败:' + (e.message || '未知错误'))
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchCategories()
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.knowledge-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.editor-wrapper {
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.editor-status {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rich-editor {
|
||||
border: 1px solid #e5e6eb;
|
||||
}
|
||||
|
||||
.preview-wrapper {
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.preview-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
background: #fafafa;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
min-height: 200px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user