diff --git a/Cunkebao/src/components/Upload/FileUpload/index.tsx b/Cunkebao/src/components/Upload/FileUpload/index.tsx index 9edf1c4f..88825bda 100644 --- a/Cunkebao/src/components/Upload/FileUpload/index.tsx +++ b/Cunkebao/src/components/Upload/FileUpload/index.tsx @@ -72,25 +72,83 @@ const FileUpload: React.FC = ({ name: "PPT文件", extensions: ["pptx", "ppt"], }, + pdf: { + accept: ".pdf", + mimeTypes: ["application/pdf"], + icon: FileOutlined, + name: "PDF文件", + extensions: ["pdf"], + }, + txt: { + accept: ".txt", + mimeTypes: ["text/plain"], + icon: FileOutlined, + name: "文本文件", + extensions: ["txt"], + }, + md: { + accept: ".md", + mimeTypes: ["text/markdown"], + icon: FileOutlined, + name: "Markdown文件", + extensions: ["md"], + }, + mp4: { + accept: ".mp4", + mimeTypes: ["video/mp4"], + icon: FileOutlined, + name: "MP4视频", + extensions: ["mp4"], + }, + avi: { + accept: ".avi", + mimeTypes: ["video/x-msvideo"], + icon: FileOutlined, + name: "AVI视频", + extensions: ["avi"], + }, }; // 生成accept字符串 const generateAcceptString = () => { - return acceptTypes - .map(type => fileTypeConfig[type as keyof typeof fileTypeConfig]?.accept) - .filter(Boolean) - .join(","); + const accepts: string[] = []; + + for (const type of acceptTypes) { + // 如果是配置中的类型键(如 "word", "pdf") + const config = fileTypeConfig[type as keyof typeof fileTypeConfig]; + if (config) { + accepts.push(config.accept); + } else { + // 如果是扩展名(如 "doc", "docx"),直接添加 + accepts.push(`.${type}`); + } + } + + return accepts.filter(Boolean).join(","); }; // 获取文件类型信息 const getFileTypeInfo = (file: File) => { const extension = file.name.split(".").pop()?.toLowerCase(); + if (!extension) return null; + + // 首先尝试通过 acceptTypes 中指定的类型键来查找 for (const type of acceptTypes) { const config = fileTypeConfig[type as keyof typeof fileTypeConfig]; - if (config && config.extensions.includes(extension || "")) { + if (config && config.extensions.includes(extension)) { return config; } } + + // 如果 acceptTypes 中包含扩展名本身(如 "doc", "docx"),查找所有包含该扩展名的配置 + if (acceptTypes.includes(extension)) { + for (const [key, config] of Object.entries(fileTypeConfig)) { + if (config.extensions.includes(extension)) { + return config; + } + } + } + return null; }; @@ -116,12 +174,29 @@ const FileUpload: React.FC = ({ } }, [value]); + // 获取类型名称 + const getTypeName = (type: string) => { + const config = fileTypeConfig[type as keyof typeof fileTypeConfig]; + if (config) return config.name; + // 如果是扩展名,返回友好的名称 + const extensionNames: Record = { + doc: "Word文件", + docx: "Word文件", + pdf: "PDF文件", + txt: "文本文件", + md: "Markdown文件", + mp4: "MP4视频", + avi: "AVI视频", + }; + return extensionNames[type] || `${type.toUpperCase()}文件`; + }; + // 文件验证 const beforeUpload = (file: File) => { const typeInfo = getFileTypeInfo(file); if (!typeInfo) { const allowedTypes = acceptTypes - .map(type => fileTypeConfig[type as keyof typeof fileTypeConfig]?.name) + .map(type => getTypeName(type)) .filter(Boolean) .join("、"); message.error(`只能上传${allowedTypes}!`); @@ -310,10 +385,7 @@ const FileUpload: React.FC = ({
支持{" "} {acceptTypes - .map( - type => - fileTypeConfig[type as keyof typeof fileTypeConfig]?.name, - ) + .map(type => getTypeName(type)) .filter(Boolean) .join("、")} ,最大 {maxSize}MB diff --git a/Cunkebao/src/components/Upload/FileUploadButton/index.module.scss b/Cunkebao/src/components/Upload/FileUploadButton/index.module.scss new file mode 100644 index 00000000..1d832e7c --- /dev/null +++ b/Cunkebao/src/components/Upload/FileUploadButton/index.module.scss @@ -0,0 +1,13 @@ +.uploadButtonWrapper { + // 使用 :global() 包装 Ant Design 的全局类名 + :global { + .ant-upload-select { + // 这里可以修改 .ant-upload-select 的样式 + display: block; + width: 100%; + span { + display: block; + } + } + } +} diff --git a/Cunkebao/src/components/Upload/FileUploadButton/index.tsx b/Cunkebao/src/components/Upload/FileUploadButton/index.tsx new file mode 100644 index 00000000..72efd753 --- /dev/null +++ b/Cunkebao/src/components/Upload/FileUploadButton/index.tsx @@ -0,0 +1,282 @@ +import React, { useState } from "react"; +import { Upload, message, Button } from "antd"; +import { + LoadingOutlined, + CloudUploadOutlined, + FileExcelOutlined, + FileWordOutlined, + FilePptOutlined, + FileOutlined, +} from "@ant-design/icons"; +import type { UploadProps } from "antd/es/upload/interface"; +import style from "./index.module.scss"; + +export interface FileUploadResult { + fileName: string; // 文件名 + fileUrl: string; // 文件URL +} + +interface FileUploadProps { + onChange?: (result: FileUploadResult) => void; // 上传成功后的回调,返回文件名和URL + disabled?: boolean; + className?: string; + maxSize?: number; // 最大文件大小(MB) + acceptTypes?: string[]; // 接受的文件类型 + buttonText?: string; // 按钮文本 + buttonType?: "default" | "primary" | "dashed" | "text" | "link"; // 按钮类型 + block?: boolean; + size?: "small" | "middle" | "large"; + showSuccessMessage?: boolean; // 是否显示上传成功提示,默认不显示 +} + +const FileUpload: React.FC = ({ + onChange, + disabled = false, + className, + maxSize = 10, + acceptTypes = ["excel", "word", "ppt"], + buttonText = "上传文件", + buttonType = "primary", + block = false, + size = "middle", + showSuccessMessage = false, +}) => { + const [loading, setLoading] = useState(false); + const [fileName, setFileName] = useState(""); // 保存文件名 + + // 文件类型配置 + const fileTypeConfig = { + excel: { + accept: ".xlsx,.xls", + mimeTypes: [ + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.ms-excel", + ], + icon: FileExcelOutlined, + name: "Excel文件", + extensions: ["xlsx", "xls"], + }, + word: { + accept: ".docx,.doc", + mimeTypes: [ + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/msword", + ], + icon: FileWordOutlined, + name: "Word文件", + extensions: ["docx", "doc"], + }, + ppt: { + accept: ".pptx,.ppt", + mimeTypes: [ + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/vnd.ms-powerpoint", + ], + icon: FilePptOutlined, + name: "PPT文件", + extensions: ["pptx", "ppt"], + }, + pdf: { + accept: ".pdf", + mimeTypes: ["application/pdf"], + icon: FileOutlined, + name: "PDF文件", + extensions: ["pdf"], + }, + txt: { + accept: ".txt", + mimeTypes: ["text/plain"], + icon: FileOutlined, + name: "文本文件", + extensions: ["txt"], + }, + doc: { + accept: ".doc,.docx", + mimeTypes: [ + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/msword", + ], + icon: FileWordOutlined, + name: "Word文件", + extensions: ["doc", "docx"], + }, + docx: { + accept: ".docx,.doc", + mimeTypes: [ + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/msword", + ], + icon: FileWordOutlined, + name: "Word文件", + extensions: ["docx", "doc"], + }, + md: { + accept: ".md", + mimeTypes: ["text/markdown"], + icon: FileOutlined, + name: "Markdown文件", + extensions: ["md"], + }, + }; + + // 生成accept字符串 + const generateAcceptString = () => { + const accepts: string[] = []; + + for (const type of acceptTypes) { + // 如果是配置中的类型键(如 "word", "pdf") + const config = fileTypeConfig[type as keyof typeof fileTypeConfig]; + if (config) { + accepts.push(config.accept); + } else { + // 如果是扩展名(如 "doc", "docx"),直接添加 + accepts.push(`.${type}`); + } + } + + return accepts.filter(Boolean).join(","); + }; + + // 获取文件类型信息 + const getFileTypeInfo = (file: File) => { + const extension = file.name.split(".").pop()?.toLowerCase(); + if (!extension) return null; + + // 首先尝试通过 acceptTypes 中指定的类型键来查找 + for (const type of acceptTypes) { + const config = fileTypeConfig[type as keyof typeof fileTypeConfig]; + if (config && config.extensions.includes(extension)) { + return config; + } + } + + // 如果 acceptTypes 中包含扩展名本身(如 "doc", "docx"),查找所有包含该扩展名的配置 + if (acceptTypes.includes(extension)) { + for (const [, config] of Object.entries(fileTypeConfig)) { + if (config.extensions.includes(extension)) { + return config; + } + } + } + + return null; + }; + + // 获取类型名称 + const getTypeName = (type: string) => { + const config = fileTypeConfig[type as keyof typeof fileTypeConfig]; + if (config) return config.name; + // 如果是扩展名,返回友好的名称 + const extensionNames: Record = { + doc: "Word文件", + docx: "Word文件", + pdf: "PDF文件", + txt: "文本文件", + md: "Markdown文件", + }; + return extensionNames[type] || `${type.toUpperCase()}文件`; + }; + + // 文件验证 + const beforeUpload = (file: File) => { + // 保存文件名 + setFileName(file.name); + + const typeInfo = getFileTypeInfo(file); + if (!typeInfo) { + const allowedTypes = acceptTypes + .map(type => getTypeName(type)) + .filter(Boolean) + .join("、"); + message.error(`只能上传${allowedTypes}!`); + return false; + } + + const isLtMaxSize = file.size / 1024 / 1024 < maxSize; + if (!isLtMaxSize) { + message.error(`文件大小不能超过${maxSize}MB!`); + return false; + } + + return true; + }; + + // 处理文件变化 + const handleChange: UploadProps["onChange"] = info => { + // 处理上传状态 + if (info.file.status === "uploading") { + setLoading(true); + } else if (info.file.status === "done") { + setLoading(false); + if (showSuccessMessage) { + message.success("文件上传成功!"); + } + + // 从响应中获取上传后的URL + let uploadedUrl = ""; + + if (info.file.response) { + if (typeof info.file.response === "string") { + uploadedUrl = info.file.response; + } else if (info.file.response.data) { + uploadedUrl = + typeof info.file.response.data === "string" + ? info.file.response.data + : info.file.response.data.url || ""; + } else if (info.file.response.url) { + uploadedUrl = info.file.response.url; + } + } + + // 获取文件名,优先使用保存的文件名,如果没有则使用文件对象的名称 + const finalFileName = fileName || info.file.name || ""; + + if (uploadedUrl && finalFileName) { + onChange?.({ + fileName: finalFileName, + fileUrl: uploadedUrl, + }); + // 清空保存的文件名,为下次上传做准备 + setFileName(""); + } + } else if (info.file.status === "error") { + setLoading(false); + message.error("上传失败,请重试"); + // 清空保存的文件名 + setFileName(""); + } + }; + + const action = import.meta.env.VITE_API_BASE_URL + "/v1/attachment/upload"; + + return ( +
+ + + +
+ ); +}; + +export default FileUpload; diff --git a/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/data.ts b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/data.ts index b477d105..751e8762 100644 --- a/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/data.ts +++ b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/data.ts @@ -20,7 +20,7 @@ export interface Material { type?: KnowledgeBase; // 关联的知识库类型信息 // 前端扩展字段 fileName?: string; // 映射自 name - fileSize?: number; // 文件大小(前端计算) + size?: number; // 文件大小(前端计算) fileType?: string; // 文件类型(从 name 提取) filePath?: string; // 映射自 fileUrl tags?: string[]; // 映射自 label diff --git a/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/index.module.scss b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/index.module.scss index ed1fbafa..89793022 100644 --- a/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/index.module.scss +++ b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/index.module.scss @@ -4,6 +4,49 @@ min-height: 100vh; } +.detailContent { + padding: 16px; +} + +// 提示横幅 +.banner { + background: linear-gradient(135deg, #e6f7ff 0%, #f0f5ff 100%); + border-radius: 12px; + padding: 12px 16px; + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 12px; + border: 1px solid #91d5ff; +} + +.bannerIcon { + font-size: 20px; + color: #1890ff; + flex-shrink: 0; +} + +.bannerContent { + flex: 1; +} + +.bannerText { + font-size: 14px; + color: #333; + line-height: 1.5; + + a { + color: #1890ff; + text-decoration: none; + font-weight: 500; + cursor: pointer; + + &:active { + opacity: 0.7; + } + } +} + // Tab容器 .tabContainer { background: #fff; @@ -42,7 +85,6 @@ // 知识库信息卡片 .infoCard { background: #fff; - margin: 12px 16px; border-radius: 12px; padding: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); @@ -78,6 +120,15 @@ font-weight: 600; color: #222; margin-bottom: 6px; + display: flex; + align-items: center; +} + +.systemPresetLabel { + margin-left: 8px; + font-size: 12px; + color: #999; + font-weight: normal; } .infoDescription { @@ -127,6 +178,66 @@ font-weight: 500; } +// 提示词生效规则 +.promptRulesSection { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid #f0f0f0; +} + +.rulesList { + margin: 12px 0 0 0; + padding-left: 20px; + font-size: 13px; + color: #666; + line-height: 1.8; + + li { + margin-bottom: 8px; + } +} + +// 知识库独立提示词 +.independentPromptSection { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid #f0f0f0; +} + +.promptDisplay { + background: #f9f9f9; + border-radius: 8px; + padding: 12px; + margin-top: 8px; + min-height: 80px; + max-height: 200px; + overflow-y: auto; +} + +.promptText { + font-size: 13px; + color: #333; + line-height: 1.6; + white-space: pre-wrap; +} + +.promptEmpty { + font-size: 13px; + color: #999; + text-align: center; + padding: 20px 0; +} + +// 上传素材区域 +.uploadSection { + margin: 16px 0; +} + +// 库内素材区域 +.materialsSection { + margin-top: 16px; +} + .tags { display: flex; flex-wrap: wrap; @@ -226,10 +337,16 @@ color: #333; } +.sectionIcon { + font-size: 16px; + color: #1890ff; +} + .sectionCount { font-size: 13px; color: #888; font-weight: normal; + margin-left: 4px; } .callerList { @@ -257,6 +374,22 @@ height: 40px; border-radius: 50%; flex-shrink: 0; + background: #f0f0f0; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + + .anticon { + font-size: 20px; + color: #999; + } } .callerInfo { @@ -405,34 +538,16 @@ .bottomActions { display: flex; gap: 12px; - padding: 16px; - background: #fff; - border-top: 1px solid #f0f0f0; -} - -.actionButton { - flex: 1; - padding: 12px; - border: 1px solid #d9d9d9; - border-radius: 8px; - background: #fff; - font-size: 14px; - cursor: pointer; - transition: all 0.2s; - - &:active { - opacity: 0.7; - } + padding: 16px 0; + margin-top: 16px; } .editButton { - color: #1890ff; - border-color: #1890ff; + flex: 1; } .deleteButton { - color: #ff4d4f; - border-color: #ff4d4f; + flex: 1; } // 空状态 diff --git a/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/index.tsx b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/index.tsx index 398a1caa..1728f924 100644 --- a/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/index.tsx +++ b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/index.tsx @@ -1,20 +1,10 @@ import React, { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { - Button, - Switch, - message, - Spin, - Dropdown, - Modal, - Input, - Upload, -} from "antd"; +import { Button, Switch, message, Spin, Dropdown, Modal } from "antd"; +import { Dialog, Toast } from "antd-mobile"; import { BookOutlined, - CheckCircleOutlined, UserOutlined, - UploadOutlined, FileOutlined, VideoCameraOutlined, FileTextOutlined, @@ -23,9 +13,11 @@ import { DeleteOutlined, SettingOutlined, ApiOutlined, - BulbOutlined, - CalendarOutlined, - DatabaseOutlined, + CheckCircleOutlined, + GlobalOutlined, + PlusOutlined, + InfoCircleOutlined, + MessageOutlined, } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import NavCommon from "@/components/NavCommon"; @@ -39,14 +31,12 @@ import { } from "./api"; import { deleteKnowledgeBase } from "../list/api"; import type { KnowledgeBase, Material, Caller } from "./data"; -import FileUpload from "@/components/Upload/FileUpload"; - -type TabType = "info" | "materials"; +import FileUpload from "@/components/Upload/FileUploadButton"; +import GlobalPromptModal from "../list/components/GlobalPromptModal"; const AIKnowledgeDetail: React.FC = () => { const navigate = useNavigate(); const { id } = useParams<{ id: string }>(); - const [activeTab, setActiveTab] = useState("info"); const [loading, setLoading] = useState(false); const [knowledgeBase, setKnowledgeBase] = useState( null, @@ -55,6 +45,7 @@ const AIKnowledgeDetail: React.FC = () => { const [callers, setCallers] = useState([]); const [promptEditVisible, setPromptEditVisible] = useState(false); const [independentPrompt, setIndependentPrompt] = useState(""); + const [globalPromptVisible, setGlobalPromptVisible] = useState(false); useEffect(() => { if (id) { @@ -111,6 +102,46 @@ const AIKnowledgeDetail: React.FC = () => { } }; + // 更新素材列表 + const fetchMaterialList = async () => { + if (!id) return; + try { + const materialRes = await getMaterialList({ + knowledgeBaseId: Number(id), + page: 1, + limit: 100, + }); + + // 转换素材数据格式 + const transformedMaterials = (materialRes.list || []).map( + (item: any) => ({ + ...item, + fileName: item.name, + tags: item.label || [], + filePath: item.fileUrl, + uploadTime: item.createTime + ? new Date(item.createTime * 1000).toLocaleDateString("zh-CN") + : "-", + uploaderId: item.userId, + fileType: item.name?.split(".").pop() || "file", + fileSize: 0, // 接口未返回,需要前端计算或后端补充 + }), + ); + + setMaterials(transformedMaterials); + + // 更新知识库的素材数量 + if (knowledgeBase) { + setKnowledgeBase({ + ...knowledgeBase, + materialCount: transformedMaterials.length, + }); + } + } catch (error) { + console.error("获取素材列表失败", error); + } + }; + const handleAICallToggle = async (checked: boolean) => { if (!id || !knowledgeBase) return; @@ -139,46 +170,6 @@ const AIKnowledgeDetail: React.FC = () => { } }; - const handleIndependentPromptToggle = async (checked: boolean) => { - if (!id || !knowledgeBase) return; - - // 系统预设不允许修改 - if (knowledgeBase.type === 0) { - message.warning("系统预设知识库不可修改"); - return; - } - - if (checked) { - // 启用时打开编辑弹窗 - setPromptEditVisible(true); - } else { - // 禁用时直接更新 - try { - await updateKnowledgeBaseConfig({ - id: Number(id), - name: knowledgeBase.name, - description: knowledgeBase.description, - label: knowledgeBase.tags || knowledgeBase.label || [], - useIndependentPrompt: false, - independentPrompt: "", - }); - message.success("已关闭独立提示词"); - setKnowledgeBase(prev => - prev - ? { - ...prev, - useIndependentPrompt: false, - independentPrompt: "", - prompt: null, - } - : null, - ); - } catch (error) { - message.error("操作失败"); - } - } - }; - const handlePromptSave = async () => { if (!id || !knowledgeBase) return; if (!independentPrompt.trim()) { @@ -221,41 +212,52 @@ const AIKnowledgeDetail: React.FC = () => { return; } - Modal.confirm({ - title: "确认删除", + const result = await Dialog.confirm({ content: "删除后数据无法恢复,确定要删除该知识库吗?", - okText: "确定", + confirmText: "确定", cancelText: "取消", - okButtonProps: { danger: true }, - onOk: async () => { - try { - await deleteKnowledgeBase(Number(id)); - message.success("删除成功"); - navigate(-1); - } catch (error) { - message.error("删除失败"); - } - }, }); + + if (result) { + try { + await deleteKnowledgeBase(Number(id)); + Toast.show({ + content: "删除成功", + icon: "success", + }); + // 刷新素材列表 + await fetchMaterialList(); + } catch (error) { + Toast.show({ + content: "删除失败", + icon: "fail", + }); + } + } }; const handleDeleteMaterial = async (materialId: number) => { - Modal.confirm({ - title: "确认删除", + const result = await Dialog.confirm({ content: "确定要删除该素材吗?", - okText: "确定", + confirmText: "确定", cancelText: "取消", - okButtonProps: { danger: true }, - onOk: async () => { - try { - await deleteMaterial(materialId); - message.success("删除成功"); - setMaterials(prev => prev.filter(m => m.id !== materialId)); - } catch (error) { - message.error("删除失败"); - } - }, }); + + if (result) { + try { + await deleteMaterial(materialId); + Toast.show({ + content: "删除成功", + icon: "success", + }); + setMaterials(prev => prev.filter(m => m.id !== materialId)); + } catch (error) { + Toast.show({ + content: "删除失败", + icon: "fail", + }); + } + } }; const handleUpload = async (file: File) => { @@ -318,13 +320,27 @@ const AIKnowledgeDetail: React.FC = () => { return (bytes / (1024 * 1024)).toFixed(1) + " MB"; }; - const renderInfoTab = () => { + const renderContent = () => { if (!knowledgeBase) return null; - const isSystemPreset = knowledgeBase.type === 0; // 系统预设只读 + const isSystemPreset = knowledgeBase.type === 0; return ( - <> +
+ {/* 提示横幅 */} +
+ + +
+ + {/* 知识库信息卡片 */}
@@ -377,9 +393,10 @@ const AIKnowledgeDetail: React.FC = () => {
+ {/* 内容标签 */} {knowledgeBase.tags && knowledgeBase.tags.length > 0 && (
-
内容库标签
+
内容标签
{knowledgeBase.tags.map((tag, index) => ( @@ -397,9 +414,6 @@ const AIKnowledgeDetail: React.FC = () => { AI调用配置
-
- AI助手可以使用此内容库的素材 -
{
- + + AI助手可以使用此内容库的素材 +
+
+
+
+
+
+ 支持智能应答和推荐
@@ -420,47 +442,90 @@ const AIKnowledgeDetail: React.FC = () => {
- + 实时响应用户查询
-
-
- - 与各知识库的回复基本合用 -
-
- - 确保所有知识库回复的一致性和专业度 + {/* 提示词生效规则 */} +
+
+
+ + 提示词生效规则 +
+
    +
  1. 1、先应用统一提示词 (全局规范)
  2. +
  3. 2、再结合知识库独立提示词 (专业指导)
  4. +
  5. 3、最终形成针对性的回复风格
  6. +
+ {/* 知识库独立提示词 */} +
+
+
+ + 知识库独立提示词 +
+ +
+
+ {knowledgeBase.independentPrompt || independentPrompt ? ( +
+ {knowledgeBase.independentPrompt || independentPrompt} +
+ ) : ( +
+ 暂无独立提示词,点击编辑按钮添加 +
+ )} +
+ +
+ + {/* 调用客服名单 */} {callers.length > 0 && (
- - 调用者名单 + + 调用客服名单 {callers.length}
{callers.slice(0, 3).map(caller => (
- {caller.name} +
+ {caller.avatar ? ( + {caller.name} + ) : ( + + )} +
{caller.name}
{caller.role}
- 调用 {caller.callCount} 次 · {caller.lastCallTime} + 调用{caller.callCount}次 · {caller.lastCallTime}
))} @@ -469,52 +534,26 @@ const AIKnowledgeDetail: React.FC = () => { )}
- {/* 系统预设不显示编辑和删除按钮 */} + {/* 上传素材按钮 */} {!isSystemPreset && ( -
- - -
- )} - - ); - }; - - const renderMaterialsTab = () => { - const isSystemPreset = knowledgeBase?.type === 0; // 系统预设只读 - - return ( -
- {/* 系统预设不显示上传按钮 */} - {!isSystemPreset && ( -
+
{ - // v 可能为 string 或 string[]:单传取 string - const uploadUrl = Array.isArray(v) ? v[0] : v; - if (uploadUrl && id) { + block={true} + size="large" + onChange={async result => { + if (result && result.fileUrl && result.fileName && id) { try { await uploadMaterial({ typeId: Number(id), - name: uploadUrl.split("/").pop() || "已上传文件", + name: result.fileName, label: [], - fileUrl: uploadUrl, + fileUrl: result.fileUrl, }); message.success("上传成功"); - fetchDetail(); + // 更新素材列表 + await fetchMaterialList(); } catch (e) { message.error("上传失败"); } @@ -523,68 +562,94 @@ const AIKnowledgeDetail: React.FC = () => { />
)} -
- {materials.length > 0 ? ( - materials.map(material => ( -
- {getFileIcon(material.fileType)} -
-
-
- {material.fileName} + + {/* 库内素材 */} +
+
+
+ 库内素材 + {materials.length} +
+
+
+ {materials.length > 0 ? ( + materials.map(material => ( +
+ {getFileIcon(material.fileType)} +
+
+
+ {material.fileName} +
+ {!isSystemPreset && ( + , + label: "删除", + danger: true, + }, + ], + onClick: () => handleDeleteMaterial(material.id), + }} + trigger={["click"]} + placement="bottomRight" + > + + + )}
- {/* 系统预设不显示删除按钮 */} - {!isSystemPreset && ( - , - label: "删除", - danger: true, - }, - ], - onClick: () => handleDeleteMaterial(material.id), - }} - trigger={["click"]} - placement="bottomRight" - > - - +
+
+ {formatFileSize(material?.size || 0)} +
+
+ {material.uploadTime} +
+
+ {material.tags && material.tags.length > 0 && ( +
+ {material.tags.map((tag, index) => ( + + {tag} + + ))} +
)}
-
-
- - {formatFileSize(material.fileSize)} -
-
- - {material.uploadTime} -
-
- {material.tags && material.tags.length > 0 && ( -
- {material.tags.map((tag, index) => ( - - {tag} - - ))} -
- )}
+ )) + ) : ( +
+
+ +
+
暂无素材
- )) - ) : ( -
-
- -
-
暂无素材
-
- )} + )} +
+ + {/* 底部操作按钮 */} + {!isSystemPreset && ( +
+ + +
+ )}
); }; @@ -596,23 +661,20 @@ const AIKnowledgeDetail: React.FC = () => { navigate("/workspace/ai-knowledge")} + right={ +
+ + +
+ } /> -
-
- - -
-
} > @@ -622,10 +684,7 @@ const AIKnowledgeDetail: React.FC = () => {
) : ( - <> - {activeTab === "info" && renderMaterialsTab()} - {activeTab === "materials" && renderInfoTab()} - + renderContent() )}
@@ -650,6 +709,12 @@ const AIKnowledgeDetail: React.FC = () => { 💡 独立提示词将与统一提示词合并使用,为该知识库提供专业指导
+ + {/* 统一提示词弹窗 */} + setGlobalPromptVisible(false)} + /> ); }; diff --git a/Cunkebao/src/pages/mobile/workspace/ai-knowledge/list/index.tsx b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/list/index.tsx index eaa2f0ee..5bf52f78 100644 --- a/Cunkebao/src/pages/mobile/workspace/ai-knowledge/list/index.tsx +++ b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/list/index.tsx @@ -1,6 +1,14 @@ import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { Button, Switch, Dropdown, message, Spin, Pagination } from "antd"; +import { + Button, + Switch, + Dropdown, + message, + Spin, + Pagination, + Input, +} from "antd"; import { MoreOutlined, PlusOutlined, @@ -9,6 +17,8 @@ import { DeleteOutlined, GlobalOutlined, InfoCircleOutlined, + SearchOutlined, + ArrowRightOutlined, } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import NavCommon from "@/components/NavCommon"; @@ -32,6 +42,7 @@ const AIKnowledgeList: React.FC = () => { const [enabledCount, setEnabledCount] = useState(0); const [page, setPage] = useState(1); const [menuLoadingId, setMenuLoadingId] = useState(null); + const [searchValue, setSearchValue] = useState(""); // 搜索内容 // 弹窗控制 const [globalPromptVisible, setGlobalPromptVisible] = useState(false); @@ -80,6 +91,21 @@ const AIKnowledgeList: React.FC = () => { fetchList(page); }; + const handleSearch = () => { + setPage(1); + }; + + const getFilteredList = () => { + const keyword = searchValue.trim().toLowerCase(); + if (!keyword) return list; + return list.filter(item => { + return ( + item.name.toLowerCase().includes(keyword) || + (item.description || "").toLowerCase().includes(keyword) + ); + }); + }; + // 菜单点击事件 const handleMenuClick = async (key: string, item: KnowledgeBase) => { // 系统预设不允许编辑或删除 @@ -257,23 +283,75 @@ const AIKnowledgeList: React.FC = () => { return ( navigate("/workspace")} - right={ -
- - + <> + navigate("/workspace")} + right={ +
+ + +
+ } + /> +
+ {/* 提示横幅 */} + - } - /> + + {/* 统计卡片 */} +
+
+
{total}
+
内容库总数
+
+
+
+ {enabledCount} +
+
启用中
+
+
+ + {/* 搜索和客户案例库按钮 */} +
+ setSearchValue(e.target.value)} + prefix={} + allowClear + size="large" + /> +
+
+ } footer={
{ } >
- {/* 提示横幅 */} -
- - -
- - {/* 统计卡片 */} -
-
-
{total}
-
内容库总数
-
-
-
- {enabledCount} -
-
启用中
-
-
- {/* 知识库列表 */} {loading ? (
- ) : list.length > 0 ? ( - list.map(renderCard) + ) : getFilteredList().length > 0 ? ( + getFilteredList().map(renderCard) ) : (