diff --git a/backend/apps/knowledge/__init__.py b/backend/apps/knowledge/__init__.py new file mode 100644 index 0000000..139597f --- /dev/null +++ b/backend/apps/knowledge/__init__.py @@ -0,0 +1,2 @@ + + diff --git a/backend/apps/knowledge/admin.py b/backend/apps/knowledge/admin.py new file mode 100644 index 0000000..38b5564 --- /dev/null +++ b/backend/apps/knowledge/admin.py @@ -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'] + + diff --git a/backend/apps/knowledge/apps.py b/backend/apps/knowledge/apps.py new file mode 100644 index 0000000..a2bf5b7 --- /dev/null +++ b/backend/apps/knowledge/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class KnowledgeConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.knowledge' + verbose_name = '知识库' + + diff --git a/backend/apps/knowledge/migrations/__init__.py b/backend/apps/knowledge/migrations/__init__.py new file mode 100644 index 0000000..139597f --- /dev/null +++ b/backend/apps/knowledge/migrations/__init__.py @@ -0,0 +1,2 @@ + + diff --git a/backend/apps/knowledge/models.py b/backend/apps/knowledge/models.py new file mode 100644 index 0000000..061ff7e --- /dev/null +++ b/backend/apps/knowledge/models.py @@ -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 + + diff --git a/backend/apps/knowledge/serializers.py b/backend/apps/knowledge/serializers.py new file mode 100644 index 0000000..fc02455 --- /dev/null +++ b/backend/apps/knowledge/serializers.py @@ -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', + ] + + diff --git a/backend/apps/knowledge/urls.py b/backend/apps/knowledge/urls.py new file mode 100644 index 0000000..e310234 --- /dev/null +++ b/backend/apps/knowledge/urls.py @@ -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)), +] + + diff --git a/backend/apps/knowledge/views.py b/backend/apps/knowledge/views.py new file mode 100644 index 0000000..68e11ca --- /dev/null +++ b/backend/apps/knowledge/views.py @@ -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 + ] + ) + + diff --git a/backend/apps/rbac/management/commands/__pycache__/init_rbac.cpython-312.pyc b/backend/apps/rbac/management/commands/__pycache__/init_rbac.cpython-312.pyc index eaacc21..92665c7 100644 Binary files a/backend/apps/rbac/management/commands/__pycache__/init_rbac.cpython-312.pyc and b/backend/apps/rbac/management/commands/__pycache__/init_rbac.cpython-312.pyc differ diff --git a/backend/apps/rbac/management/commands/init_rbac.py b/backend/apps/rbac/management/commands/init_rbac.py index ffd0472..2c5409d 100644 --- a/backend/apps/rbac/management/commands/init_rbac.py +++ b/backend/apps/rbac/management/commands/init_rbac.py @@ -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]) diff --git a/backend/db.sqlite3 b/backend/db.sqlite3 index 198b496..9066729 100644 Binary files a/backend/db.sqlite3 and b/backend/db.sqlite3 differ diff --git a/backend/django_vue_adminx/__pycache__/settings.cpython-312.pyc b/backend/django_vue_adminx/__pycache__/settings.cpython-312.pyc index 42a70e7..b095f76 100644 Binary files a/backend/django_vue_adminx/__pycache__/settings.cpython-312.pyc and b/backend/django_vue_adminx/__pycache__/settings.cpython-312.pyc differ diff --git a/backend/django_vue_adminx/__pycache__/urls.cpython-312.pyc b/backend/django_vue_adminx/__pycache__/urls.cpython-312.pyc index 87b2189..8ac8fea 100644 Binary files a/backend/django_vue_adminx/__pycache__/urls.cpython-312.pyc and b/backend/django_vue_adminx/__pycache__/urls.cpython-312.pyc differ diff --git a/backend/django_vue_adminx/settings.py b/backend/django_vue_adminx/settings.py index 9d28381..13013a7 100644 --- a/backend/django_vue_adminx/settings.py +++ b/backend/django_vue_adminx/settings.py @@ -58,6 +58,7 @@ INSTALLED_APPS = [ 'apps.chat.apps.ChatConfig', 'apps.system.apps.SystemConfig', 'apps.office.apps.OfficeConfig', + 'apps.knowledge.apps.KnowledgeConfig', ] MIDDLEWARE = [ diff --git a/backend/django_vue_adminx/urls.py b/backend/django_vue_adminx/urls.py index e7e2030..acbaa26 100644 --- a/backend/django_vue_adminx/urls.py +++ b/backend/django_vue_adminx/urls.py @@ -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) diff --git a/front-end/src/api/knowledge.js b/front-end/src/api/knowledge.js new file mode 100644 index 0000000..6993533 --- /dev/null +++ b/front-end/src/api/knowledge.js @@ -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', + }) +} + + diff --git a/front-end/src/views/knowledge/article/index.vue b/front-end/src/views/knowledge/article/index.vue new file mode 100644 index 0000000..0d95287 --- /dev/null +++ b/front-end/src/views/knowledge/article/index.vue @@ -0,0 +1,477 @@ + + + + +