refactor(knowledge): separate file upload into independent entry point
将知识库的"新建文章"和"上传文件"拆分为两个独立入口: UI 改动: - 移除 Segmented 切换器,"新建文章"弹窗恢复为纯手动输入 - 新增独立的"上传文件"按钮 + 上传弹窗(Upload.Dragger) - 上传提取完成后自动打开"确认提取内容"弹窗,预填标题+内容 - 管理员编辑确认后保存,文章来源标记为 EXTRACT 后端改动: - CreateArticleDto 新增可选 source 字段 - Controller 使用 dto.source || MANUAL(不再硬编码 MANUAL) 流程: - 新建文章 → 手动输入 → source = MANUAL - 上传文件 → 提取文本 → 编辑确认 → source = EXTRACT Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fc9e0cd17b
commit
93ed3343de
|
|
@ -24,6 +24,7 @@ export interface CreateArticleParams {
|
||||||
content: string;
|
content: string;
|
||||||
category: string;
|
category: string;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
source?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtractedTextResponse {
|
export interface ExtractedTextResponse {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import {
|
||||||
Typography,
|
Typography,
|
||||||
Drawer,
|
Drawer,
|
||||||
Upload,
|
Upload,
|
||||||
Segmented,
|
|
||||||
message,
|
message,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
|
|
@ -55,10 +54,11 @@ export function KnowledgePage() {
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const [categoryFilter, setCategoryFilter] = useState<string>();
|
const [categoryFilter, setCategoryFilter] = useState<string>();
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
const [selectedArticle, setSelectedArticle] = useState<Article | null>(null);
|
const [selectedArticle, setSelectedArticle] = useState<Article | null>(null);
|
||||||
const [inputMode, setInputMode] = useState<'manual' | 'upload'>('manual');
|
|
||||||
const [isExtracting, setIsExtracting] = useState(false);
|
const [isExtracting, setIsExtracting] = useState(false);
|
||||||
|
const [articleSource, setArticleSource] = useState<'MANUAL' | 'EXTRACT'>('MANUAL');
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const { data, isLoading } = useKnowledgeArticles(categoryFilter);
|
const { data, isLoading } = useKnowledgeArticles(categoryFilter);
|
||||||
|
|
@ -98,10 +98,11 @@ export function KnowledgePage() {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
createMutation.mutate(values, {
|
createMutation.mutate({ ...values, source: articleSource }, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
setArticleSource('MANUAL');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -111,19 +112,24 @@ export function KnowledgePage() {
|
||||||
setIsExtracting(true);
|
setIsExtracting(true);
|
||||||
uploadMutation.mutate(file, {
|
uploadMutation.mutate(file, {
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
|
// 关闭上传弹窗,打开文章编辑弹窗(预填提取内容)
|
||||||
|
setIsUploadModalOpen(false);
|
||||||
|
setSelectedArticle(null);
|
||||||
|
setArticleSource('EXTRACT');
|
||||||
|
form.resetFields();
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
title: result.suggestedTitle,
|
title: result.suggestedTitle,
|
||||||
content: result.extractedText,
|
content: result.extractedText,
|
||||||
});
|
});
|
||||||
|
setIsModalOpen(true);
|
||||||
const info = result.pageCount
|
const info = result.pageCount
|
||||||
? `已提取 ${result.wordCount} 字(${result.pageCount} 页)`
|
? `已提取 ${result.wordCount} 字(${result.pageCount} 页),请编辑后保存`
|
||||||
: `已提取 ${result.wordCount} 字`;
|
: `已提取 ${result.wordCount} 字,请编辑后保存`;
|
||||||
message.success(info);
|
message.success(info);
|
||||||
setInputMode('manual');
|
|
||||||
},
|
},
|
||||||
onSettled: () => setIsExtracting(false),
|
onSettled: () => setIsExtracting(false),
|
||||||
});
|
});
|
||||||
return false; // prevent default upload
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
|
|
@ -256,18 +262,26 @@ export function KnowledgePage() {
|
||||||
options={CATEGORIES}
|
options={CATEGORIES}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
<Button
|
<Space>
|
||||||
type="primary"
|
<Button
|
||||||
icon={<PlusOutlined />}
|
icon={<UploadOutlined />}
|
||||||
onClick={() => {
|
onClick={() => setIsUploadModalOpen(true)}
|
||||||
setSelectedArticle(null);
|
>
|
||||||
form.resetFields();
|
上传文件
|
||||||
setInputMode('manual');
|
</Button>
|
||||||
setIsModalOpen(true);
|
<Button
|
||||||
}}
|
type="primary"
|
||||||
>
|
icon={<PlusOutlined />}
|
||||||
新建文章
|
onClick={() => {
|
||||||
</Button>
|
setSelectedArticle(null);
|
||||||
|
setArticleSource('MANUAL');
|
||||||
|
form.resetFields();
|
||||||
|
setIsModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
新建文章
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
|
|
@ -283,13 +297,48 @@ export function KnowledgePage() {
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* 上传文件弹窗 */}
|
||||||
|
<Modal
|
||||||
|
title="上传文件"
|
||||||
|
open={isUploadModalOpen}
|
||||||
|
onCancel={() => {
|
||||||
|
if (!isExtracting) setIsUploadModalOpen(false);
|
||||||
|
}}
|
||||||
|
footer={null}
|
||||||
|
width={520}
|
||||||
|
>
|
||||||
|
<Upload.Dragger
|
||||||
|
accept=".pdf,.docx,.txt,.md"
|
||||||
|
showUploadList={false}
|
||||||
|
beforeUpload={handleFileUpload}
|
||||||
|
disabled={isExtracting}
|
||||||
|
>
|
||||||
|
<p className="ant-upload-drag-icon">
|
||||||
|
<InboxOutlined />
|
||||||
|
</p>
|
||||||
|
<p className="ant-upload-text">
|
||||||
|
{isExtracting ? '正在提取文本...' : '点击或拖拽文件到此区域'}
|
||||||
|
</p>
|
||||||
|
<p className="ant-upload-hint">
|
||||||
|
支持 PDF、Word(.docx)、TXT、Markdown 格式,最大 200MB
|
||||||
|
</p>
|
||||||
|
</Upload.Dragger>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{/* 编辑/新建弹窗 */}
|
{/* 编辑/新建弹窗 */}
|
||||||
<Modal
|
<Modal
|
||||||
title={selectedArticle ? '编辑文章' : '新建文章'}
|
title={
|
||||||
|
selectedArticle
|
||||||
|
? '编辑文章'
|
||||||
|
: articleSource === 'EXTRACT'
|
||||||
|
? '确认提取内容'
|
||||||
|
: '新建文章'
|
||||||
|
}
|
||||||
open={isModalOpen}
|
open={isModalOpen}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
setSelectedArticle(null);
|
setSelectedArticle(null);
|
||||||
|
setArticleSource('MANUAL');
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
}}
|
}}
|
||||||
footer={null}
|
footer={null}
|
||||||
|
|
@ -316,47 +365,13 @@ export function KnowledgePage() {
|
||||||
<Select mode="tags" placeholder="输入标签后回车" />
|
<Select mode="tags" placeholder="输入标签后回车" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{!selectedArticle && (
|
<Form.Item
|
||||||
<Form.Item label="内容来源">
|
name="content"
|
||||||
<Segmented
|
label="内容"
|
||||||
value={inputMode}
|
rules={[{ required: true, message: '请输入内容' }]}
|
||||||
onChange={(val) => setInputMode(val as 'manual' | 'upload')}
|
>
|
||||||
options={[
|
<TextArea rows={12} placeholder="支持Markdown格式" />
|
||||||
{ label: '手动输入', value: 'manual', icon: <EditOutlined /> },
|
</Form.Item>
|
||||||
{ label: '文件上传', value: 'upload', icon: <UploadOutlined /> },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{inputMode === 'upload' && !selectedArticle ? (
|
|
||||||
<Form.Item label="上传文件">
|
|
||||||
<Upload.Dragger
|
|
||||||
accept=".pdf,.docx,.txt,.md"
|
|
||||||
showUploadList={false}
|
|
||||||
beforeUpload={handleFileUpload}
|
|
||||||
disabled={isExtracting}
|
|
||||||
>
|
|
||||||
<p className="ant-upload-drag-icon">
|
|
||||||
<InboxOutlined />
|
|
||||||
</p>
|
|
||||||
<p className="ant-upload-text">
|
|
||||||
{isExtracting ? '正在提取文本...' : '点击或拖拽文件到此区域'}
|
|
||||||
</p>
|
|
||||||
<p className="ant-upload-hint">
|
|
||||||
支持 PDF、Word(.docx)、TXT、Markdown 格式,最大 200MB
|
|
||||||
</p>
|
|
||||||
</Upload.Dragger>
|
|
||||||
</Form.Item>
|
|
||||||
) : (
|
|
||||||
<Form.Item
|
|
||||||
name="content"
|
|
||||||
label="内容"
|
|
||||||
rules={[{ required: true, message: '请输入内容' }]}
|
|
||||||
>
|
|
||||||
<TextArea rows={12} placeholder="支持Markdown格式" />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form.Item className="mb-0 text-right">
|
<Form.Item className="mb-0 text-right">
|
||||||
<Space>
|
<Space>
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export class KnowledgeController {
|
||||||
async createArticle(@Body() dto: CreateArticleDto) {
|
async createArticle(@Body() dto: CreateArticleDto) {
|
||||||
const article = await this.knowledgeService.createArticle({
|
const article = await this.knowledgeService.createArticle({
|
||||||
...dto,
|
...dto,
|
||||||
source: KnowledgeSource.MANUAL,
|
source: dto.source || KnowledgeSource.MANUAL,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ export class CreateArticleDto {
|
||||||
content: string;
|
content: string;
|
||||||
category: string;
|
category: string;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
source?: KnowledgeSource;
|
||||||
sourceUrl?: string;
|
sourceUrl?: string;
|
||||||
autoPublish?: boolean;
|
autoPublish?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue