新增知识库相关接口定义,优化知识库API逻辑,提供获取、创建、更新和删除知识的功能。同时,更新页面组件以支持知识库管理,提升代码可维护性和用户体验。

This commit is contained in:
zyh
2025-04-11 06:59:42 +00:00
parent 6e49f62aff
commit 602317ea44
7 changed files with 1164 additions and 396 deletions

View File

@@ -6,7 +6,7 @@ import type {
User, FileLibrary, FileCategory, ThemeSettings,
SystemSetting, SystemSettingGroupData,
LoginLocation, LoginLocationDetail,
Message, UserMessage
Message, UserMessage, KnowInfo
} from '../share/types.ts';
@@ -605,6 +605,37 @@ export interface LoginLocationUpdateResponse {
data: LoginLocationDetail;
}
// 知识库相关接口类型定义
interface KnowInfoListResponse {
data: KnowInfo[];
pagination: {
total: number;
current: number;
pageSize: number;
totalPages: number;
};
}
interface KnowInfoResponse {
data: KnowInfo;
message?: string;
}
interface KnowInfoCreateResponse {
message: string;
data: KnowInfo;
}
interface KnowInfoUpdateResponse {
message: string;
data: KnowInfo;
}
interface KnowInfoDeleteResponse {
message: string;
id: number;
}
// 地图相关API
export const MapAPI = {
@@ -648,6 +679,64 @@ export const MapAPI = {
};
// 系统设置API
// 知识库API
export const KnowInfoAPI = {
// 获取知识库列表
getKnowInfos: async (params?: {
page?: number;
pageSize?: number;
search?: string;
categoryId?: number;
}): Promise<KnowInfoListResponse> => {
try {
const response = await axios.get(`${API_BASE_URL}/know-infos`, { params });
return response.data;
} catch (error) {
throw error;
}
},
// 获取单个知识详情
getKnowInfo: async (id: number): Promise<KnowInfoResponse> => {
try {
const response = await axios.get(`${API_BASE_URL}/know-infos/${id}`);
return response.data;
} catch (error) {
throw error;
}
},
// 创建知识
createKnowInfo: async (data: Partial<KnowInfo>): Promise<KnowInfoCreateResponse> => {
try {
const response = await axios.post(`${API_BASE_URL}/know-infos`, data);
return response.data;
} catch (error) {
throw error;
}
},
// 更新知识
updateKnowInfo: async (id: number, data: Partial<KnowInfo>): Promise<KnowInfoUpdateResponse> => {
try {
const response = await axios.put(`${API_BASE_URL}/know-infos/${id}`, data);
return response.data;
} catch (error) {
throw error;
}
},
// 删除知识
deleteKnowInfo: async (id: number): Promise<KnowInfoDeleteResponse> => {
try {
const response = await axios.delete(`${API_BASE_URL}/know-infos/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
};
export const SystemAPI = {
// 获取所有系统设置
getSettings: async (): Promise<SystemSettingGroupData[]> => {

440
client/admin/pages_know.tsx Normal file
View File

@@ -0,0 +1,440 @@
import React, { useState, useEffect } from 'react';
import {
Layout, Menu, Button, Table, Space,
Form, Input, Select, message, Modal,
Card, Spin, Row, Col, Breadcrumb, Avatar,
Dropdown, ConfigProvider, theme, Typography,
Switch, Badge, Image, Upload, Divider, Descriptions,
Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer
} from 'antd';
import {
UploadOutlined,
FileImageOutlined,
FileExcelOutlined,
FileWordOutlined,
FilePdfOutlined,
FileOutlined,
} from '@ant-design/icons';
import {
useQuery,
} from '@tanstack/react-query';
import dayjs from 'dayjs';
import weekday from 'dayjs/plugin/weekday';
import localeData from 'dayjs/plugin/localeData';
import 'dayjs/locale/zh-cn';
import type {
FileLibrary, FileCategory, KnowInfo
} from '../share/types.ts';
import {
AuditStatus,AuditStatusNameMap,
OssType,
} from '../share/types.ts';
import { getEnumOptions } from './utils.ts';
import {
FileAPI,
UserAPI,
} from './api.ts';
// 配置 dayjs 插件
dayjs.extend(weekday);
dayjs.extend(localeData);
// 设置 dayjs 语言
dayjs.locale('zh-cn');
const { Title } = Typography;
// 知识库管理页面组件
export const KnowInfoPage = () => {
const [modalVisible, setModalVisible] = useState(false);
const [formMode, setFormMode] = useState<'create' | 'edit'>('create');
const [editingId, setEditingId] = useState<number | null>(null);
const [form] = Form.useForm();
const [isLoading, setIsLoading] = useState(false);
const [searchParams, setSearchParams] = useState({
title: '',
category: '',
page: 1,
limit: 10,
});
// 使用React Query获取知识库文章列表
const { data: articlesData, isLoading: isListLoading, refetch } = useQuery({
queryKey: ['articles', searchParams],
queryFn: async () => {
const { title, category, page, limit } = searchParams;
const params = new URLSearchParams();
if (title) params.append('title', title);
if (category) params.append('category', category);
params.append('page', String(page));
params.append('limit', String(limit));
const response = await fetch(`/api/know-info?${params.toString()}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
});
if (!response.ok) {
throw new Error('获取知识库文章列表失败');
}
return await response.json();
}
});
const articles = articlesData?.data || [];
const pagination = articlesData?.pagination || { current: 1, pageSize: 10, total: 0 };
// 获取单个知识库文章
const fetchArticle = async (id: number) => {
try {
const response = await fetch(`/api/know-info/${id}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
});
if (!response.ok) {
throw new Error('获取知识库文章详情失败');
}
return await response.json();
} catch (error) {
message.error('获取知识库文章详情失败');
return null;
}
};
// 处理表单提交
const handleSubmit = async (values: Partial<KnowInfo>) => {
setIsLoading(true);
try {
const url = formMode === 'create'
? '/api/know-info'
: `/api/know-info/${editingId}`;
const method = formMode === 'create' ? 'POST' : 'PUT';
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
body: JSON.stringify(values),
});
if (!response.ok) {
throw new Error(formMode === 'create' ? '创建知识库文章失败' : '更新知识库文章失败');
}
message.success(formMode === 'create' ? '创建知识库文章成功' : '更新知识库文章成功');
setModalVisible(false);
form.resetFields();
refetch();
} catch (error) {
message.error((error as Error).message);
} finally {
setIsLoading(false);
}
};
// 处理编辑
const handleEdit = async (id: number) => {
const article = await fetchArticle(id);
if (article) {
setFormMode('edit');
setEditingId(id);
form.setFieldsValue(article);
setModalVisible(true);
}
};
// 处理删除
const handleDelete = async (id: number) => {
try {
const response = await fetch(`/api/know-info/${id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
});
if (!response.ok) {
throw new Error('删除知识库文章失败');
}
message.success('删除知识库文章成功');
refetch();
} catch (error) {
message.error((error as Error).message);
}
};
// 处理搜索
const handleSearch = (values: any) => {
setSearchParams(prev => ({
...prev,
title: values.title || '',
category: values.category || '',
page: 1,
}));
};
// 处理分页
const handlePageChange = (page: number, pageSize?: number) => {
setSearchParams(prev => ({
...prev,
page,
limit: pageSize || prev.limit,
}));
};
// 处理添加
const handleAdd = () => {
setFormMode('create');
setEditingId(null);
form.resetFields();
setModalVisible(true);
};
// 审核状态映射
const auditStatusOptions = getEnumOptions(AuditStatus, AuditStatusNameMap);
// 表格列定义
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
},
{
title: '标题',
dataIndex: 'title',
key: 'title',
},
{
title: '分类',
dataIndex: 'category',
key: 'category',
},
{
title: '标签',
dataIndex: 'tags',
key: 'tags',
render: (tags: string) => tags ? tags.split(',').map(tag => (
<Tag key={tag}>{tag}</Tag>
)) : null,
},
{
title: '作者',
dataIndex: 'author',
key: 'author',
},
{
title: '审核状态',
dataIndex: 'audit_status',
key: 'audit_status',
render: (status: AuditStatus) => {
let color = '';
let text = '';
switch(status) {
case AuditStatus.PENDING:
color = 'orange';
text = '待审核';
break;
case AuditStatus.APPROVED:
color = 'green';
text = '已通过';
break;
case AuditStatus.REJECTED:
color = 'red';
text = '已拒绝';
break;
default:
color = 'default';
text = '未知';
}
return <Tag color={color}>{text}</Tag>;
},
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
render: (date: string) => new Date(date).toLocaleString(),
},
{
title: '操作',
key: 'action',
render: (_: any, record: KnowInfo) => (
<Space size="middle">
<Button type="link" onClick={() => handleEdit(record.id)}></Button>
<Popconfirm
title="确定要删除这篇文章吗?"
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
>
<Button type="link" danger></Button>
</Popconfirm>
</Space>
),
},
];
return (
<div>
<Card title="知识库管理" className="mb-4">
<Form
layout="inline"
onFinish={handleSearch}
style={{ marginBottom: '16px' }}
>
<Form.Item name="title" label="标题">
<Input placeholder="请输入文章标题" />
</Form.Item>
<Form.Item name="category" label="分类">
<Input placeholder="请输入文章分类" />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
</Button>
<Button onClick={() => {
setSearchParams({
title: '',
category: '',
page: 1,
limit: 10,
});
}}>
</Button>
<Button type="primary" onClick={handleAdd}>
</Button>
</Space>
</Form.Item>
</Form>
<Table
columns={columns}
dataSource={articles}
rowKey="id"
loading={isListLoading}
pagination={{
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total,
onChange: handlePageChange,
showSizeChanger: true,
}}
/>
</Card>
<Modal
title={formMode === 'create' ? '添加知识库文章' : '编辑知识库文章'}
open={modalVisible}
onOk={() => form.submit()}
onCancel={() => setModalVisible(false)}
width={800}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
initialValues={{
audit_status: AuditStatus.PENDING,
}}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="title"
label="文章标题"
rules={[{ required: true, message: '请输入文章标题' }]}
>
<Input placeholder="请输入文章标题" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="category"
label="文章分类"
>
<Input placeholder="请输入文章分类" />
</Form.Item>
</Col>
</Row>
<Form.Item
name="tags"
label="文章标签"
help="多个标签请用英文逗号分隔,如: 服务器,网络,故障"
>
<Input placeholder="请输入文章标签,多个标签请用英文逗号分隔" />
</Form.Item>
<Form.Item
name="content"
label="文章内容"
rules={[{ required: true, message: '请输入文章内容' }]}
>
<Input.TextArea rows={15} placeholder="请输入文章内容支持Markdown格式" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="author"
label="文章作者"
>
<Input placeholder="请输入文章作者" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="cover_url"
label="封面图片URL"
>
<Input placeholder="请输入封面图片URL" />
</Form.Item>
</Col>
</Row>
<Form.Item
name="audit_status"
label="审核状态"
>
<Select options={auditStatusOptions} />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit" loading={isLoading}>
{formMode === 'create' ? '创建' : '保存'}
</Button>
<Button onClick={() => setModalVisible(false)}></Button>
</Space>
</Form.Item>
</Form>
</Modal>
</div>
);
};

View File

@@ -349,396 +349,6 @@ export const UsersPage = () => {
);
};
// 知识库管理页面组件
export const KnowInfoPage = () => {
const [modalVisible, setModalVisible] = useState(false);
const [formMode, setFormMode] = useState<'create' | 'edit'>('create');
const [editingId, setEditingId] = useState<number | null>(null);
const [form] = Form.useForm();
const [isLoading, setIsLoading] = useState(false);
const [searchParams, setSearchParams] = useState({
title: '',
category: '',
page: 1,
limit: 10,
});
// 使用React Query获取知识库文章列表
const { data: articlesData, isLoading: isListLoading, refetch } = useQuery({
queryKey: ['articles', searchParams],
queryFn: async () => {
const { title, category, page, limit } = searchParams;
const params = new URLSearchParams();
if (title) params.append('title', title);
if (category) params.append('category', category);
params.append('page', String(page));
params.append('limit', String(limit));
const response = await fetch(`/api/know-info?${params.toString()}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
});
if (!response.ok) {
throw new Error('获取知识库文章列表失败');
}
return await response.json();
}
});
const articles = articlesData?.data || [];
const pagination = articlesData?.pagination || { current: 1, pageSize: 10, total: 0 };
// 获取单个知识库文章
const fetchArticle = async (id: number) => {
try {
const response = await fetch(`/api/know-info/${id}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
});
if (!response.ok) {
throw new Error('获取知识库文章详情失败');
}
return await response.json();
} catch (error) {
message.error('获取知识库文章详情失败');
return null;
}
};
// 处理表单提交
const handleSubmit = async (values: Partial<KnowInfo>) => {
setIsLoading(true);
try {
const url = formMode === 'create'
? '/api/know-info'
: `/api/know-info/${editingId}`;
const method = formMode === 'create' ? 'POST' : 'PUT';
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
body: JSON.stringify(values),
});
if (!response.ok) {
throw new Error(formMode === 'create' ? '创建知识库文章失败' : '更新知识库文章失败');
}
message.success(formMode === 'create' ? '创建知识库文章成功' : '更新知识库文章成功');
setModalVisible(false);
form.resetFields();
refetch();
} catch (error) {
message.error((error as Error).message);
} finally {
setIsLoading(false);
}
};
// 处理编辑
const handleEdit = async (id: number) => {
const article = await fetchArticle(id);
if (article) {
setFormMode('edit');
setEditingId(id);
form.setFieldsValue(article);
setModalVisible(true);
}
};
// 处理删除
const handleDelete = async (id: number) => {
try {
const response = await fetch(`/api/know-info/${id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
});
if (!response.ok) {
throw new Error('删除知识库文章失败');
}
message.success('删除知识库文章成功');
refetch();
} catch (error) {
message.error((error as Error).message);
}
};
// 处理搜索
const handleSearch = (values: any) => {
setSearchParams(prev => ({
...prev,
title: values.title || '',
category: values.category || '',
page: 1,
}));
};
// 处理分页
const handlePageChange = (page: number, pageSize?: number) => {
setSearchParams(prev => ({
...prev,
page,
limit: pageSize || prev.limit,
}));
};
// 处理添加
const handleAdd = () => {
setFormMode('create');
setEditingId(null);
form.resetFields();
setModalVisible(true);
};
// 审核状态映射
const auditStatusOptions = getEnumOptions(AuditStatus, AuditStatusNameMap);
// 表格列定义
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
},
{
title: '标题',
dataIndex: 'title',
key: 'title',
},
{
title: '分类',
dataIndex: 'category',
key: 'category',
},
{
title: '标签',
dataIndex: 'tags',
key: 'tags',
render: (tags: string) => tags ? tags.split(',').map(tag => (
<Tag key={tag}>{tag}</Tag>
)) : null,
},
{
title: '作者',
dataIndex: 'author',
key: 'author',
},
{
title: '审核状态',
dataIndex: 'audit_status',
key: 'audit_status',
render: (status: AuditStatus) => {
let color = '';
let text = '';
switch(status) {
case AuditStatus.PENDING:
color = 'orange';
text = '待审核';
break;
case AuditStatus.APPROVED:
color = 'green';
text = '已通过';
break;
case AuditStatus.REJECTED:
color = 'red';
text = '已拒绝';
break;
default:
color = 'default';
text = '未知';
}
return <Tag color={color}>{text}</Tag>;
},
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
render: (date: string) => new Date(date).toLocaleString(),
},
{
title: '操作',
key: 'action',
render: (_: any, record: KnowInfo) => (
<Space size="middle">
<Button type="link" onClick={() => handleEdit(record.id)}></Button>
<Popconfirm
title="确定要删除这篇文章吗?"
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
>
<Button type="link" danger></Button>
</Popconfirm>
</Space>
),
},
];
return (
<div>
<Card title="知识库管理" className="mb-4">
<Form
layout="inline"
onFinish={handleSearch}
style={{ marginBottom: '16px' }}
>
<Form.Item name="title" label="标题">
<Input placeholder="请输入文章标题" />
</Form.Item>
<Form.Item name="category" label="分类">
<Input placeholder="请输入文章分类" />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
</Button>
<Button onClick={() => {
setSearchParams({
title: '',
category: '',
page: 1,
limit: 10,
});
}}>
</Button>
<Button type="primary" onClick={handleAdd}>
</Button>
</Space>
</Form.Item>
</Form>
<Table
columns={columns}
dataSource={articles}
rowKey="id"
loading={isListLoading}
pagination={{
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total,
onChange: handlePageChange,
showSizeChanger: true,
}}
/>
</Card>
<Modal
title={formMode === 'create' ? '添加知识库文章' : '编辑知识库文章'}
open={modalVisible}
onOk={() => form.submit()}
onCancel={() => setModalVisible(false)}
width={800}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
initialValues={{
audit_status: AuditStatus.PENDING,
}}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="title"
label="文章标题"
rules={[{ required: true, message: '请输入文章标题' }]}
>
<Input placeholder="请输入文章标题" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="category"
label="文章分类"
>
<Input placeholder="请输入文章分类" />
</Form.Item>
</Col>
</Row>
<Form.Item
name="tags"
label="文章标签"
help="多个标签请用英文逗号分隔,如: 服务器,网络,故障"
>
<Input placeholder="请输入文章标签,多个标签请用英文逗号分隔" />
</Form.Item>
<Form.Item
name="content"
label="文章内容"
rules={[{ required: true, message: '请输入文章内容' }]}
>
<Input.TextArea rows={15} placeholder="请输入文章内容支持Markdown格式" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="author"
label="文章作者"
>
<Input placeholder="请输入文章作者" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="cover_url"
label="封面图片URL"
>
<Input placeholder="请输入封面图片URL" />
</Form.Item>
</Col>
</Row>
<Form.Item
name="audit_status"
label="审核状态"
>
<Select options={auditStatusOptions} />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit" loading={isLoading}>
{formMode === 'create' ? '创建' : '保存'}
</Button>
<Button onClick={() => setModalVisible(false)}></Button>
</Space>
</Form.Item>
</Form>
</Modal>
</div>
);
};
// 文件库管理页面
export const FileLibraryPage = () => {
const [loading, setLoading] = useState(false);

View File

@@ -1,6 +1,5 @@
import { JSDOM } from 'npm:jsdom'
import { JSDOM } from 'jsdom'
import React from 'react'
import 'npm:jsdom-global'
import {render, fireEvent, within, screen} from '@testing-library/react'
import { ThemeSettingsPage } from "../pages_settings.tsx"
import { ThemeProvider } from "../hooks_sys.tsx"
@@ -79,7 +78,7 @@ Deno.test('主题设置页面测试', async (t) => {
</QueryClientProvider>
)
debug(await findByRole('radio', { name: /浅色模式/i }))
// debug(await findByRole('radio', { name: /浅色模式/i }))
// 测试1: 渲染基本元素
await t.step('应渲染主题设置标题', async () => {

View File

@@ -72,9 +72,9 @@ import {
import {
DashboardPage,
UsersPage,
KnowInfoPage,
FileLibraryPage
} from './pages_sys.tsx';
import { KnowInfoPage } from './pages_know.tsx';
import { MessagesPage } from './pages_messages.tsx';
import {
SettingsPage,

View File

@@ -29,7 +29,8 @@
"react-hook-form": "https://esm.d8d.fun/react-hook-form@7.55.0?dev&deps=react@19.0.0,react-dom@19.0.0",
"@heroicons/react/24/outline": "https://esm.d8d.fun/@heroicons/react@2.1.1/24/outline?dev&deps=react@19.0.0,react-dom@19.0.0",
"@heroicons/react/24/solid": "https://esm.d8d.fun/@heroicons/react@2.1.1/24/solid?dev&deps=react@19.0.0,react-dom@19.0.0",
"@testing-library/react": "https://esm.d8d.fun/@testing-library/react@16.3.0?dev&deps=react@19.0.0,react-dom@19.0.0"
"@testing-library/react": "https://esm.d8d.fun/@testing-library/react@16.3.0?dev&deps=react@19.0.0,react-dom@19.0.0",
"jsdom":"npm:jsdom@26.0.0"
},
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext", "deno.ns"]

629
deno.lock generated

File diff suppressed because it is too large Load Diff