From 6a64e3854ce702a26cf1d98cd381d074b2cff9cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Wed, 29 Oct 2025 17:49:51 +0800 Subject: [PATCH] Refactor Recharge component to support custom recharge amounts and quick selection options. Remove version package data and enhance payment handling with improved user feedback. Add AI knowledge management routes and UI elements in the workspace section. --- .../mobile/mine/recharge/index/index.tsx | 374 ++++++---- .../mobile/workspace/ai-knowledge/api 文档 | 168 +++++ .../workspace/ai-knowledge/detail/api.ts | 109 +++ .../workspace/ai-knowledge/detail/data.ts | 48 ++ .../ai-knowledge/detail/index.module.scss | 481 +++++++++++++ .../workspace/ai-knowledge/detail/index.tsx | 648 ++++++++++++++++++ .../mobile/workspace/ai-knowledge/form/api.ts | 4 + .../workspace/ai-knowledge/form/data.ts | 2 + .../ai-knowledge/form/index.module.scss | 318 +++++++++ .../workspace/ai-knowledge/form/index.tsx | 307 +++++++++ .../mobile/workspace/ai-knowledge/list/api.ts | 90 +++ .../list/components/GlobalPromptModal.tsx | 139 ++++ .../workspace/ai-knowledge/list/data.ts | 75 ++ .../ai-knowledge/list/index.module.scss | 435 ++++++++++++ .../workspace/ai-knowledge/list/index.tsx | 358 ++++++++++ .../workspace/ai-knowledge/接口对接说明.md | 183 +++++ .../src/pages/mobile/workspace/main/index.tsx | 17 +- Cunkebao/src/router/module/workspace.tsx | 24 + 18 files changed, 3635 insertions(+), 145 deletions(-) create mode 100644 Cunkebao/src/pages/mobile/workspace/ai-knowledge/api 文档 create mode 100644 Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/api.ts create mode 100644 Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/data.ts create mode 100644 Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/index.module.scss create mode 100644 Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/index.tsx create mode 100644 Cunkebao/src/pages/mobile/workspace/ai-knowledge/form/api.ts create mode 100644 Cunkebao/src/pages/mobile/workspace/ai-knowledge/form/data.ts create mode 100644 Cunkebao/src/pages/mobile/workspace/ai-knowledge/form/index.module.scss create mode 100644 Cunkebao/src/pages/mobile/workspace/ai-knowledge/form/index.tsx create mode 100644 Cunkebao/src/pages/mobile/workspace/ai-knowledge/list/api.ts create mode 100644 Cunkebao/src/pages/mobile/workspace/ai-knowledge/list/components/GlobalPromptModal.tsx create mode 100644 Cunkebao/src/pages/mobile/workspace/ai-knowledge/list/data.ts create mode 100644 Cunkebao/src/pages/mobile/workspace/ai-knowledge/list/index.module.scss create mode 100644 Cunkebao/src/pages/mobile/workspace/ai-knowledge/list/index.tsx create mode 100644 Cunkebao/src/pages/mobile/workspace/ai-knowledge/接口对接说明.md diff --git a/Cunkebao/src/pages/mobile/mine/recharge/index/index.tsx b/Cunkebao/src/pages/mobile/mine/recharge/index/index.tsx index c0a493b1..ac5ef9e5 100644 --- a/Cunkebao/src/pages/mobile/mine/recharge/index/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/recharge/index/index.tsx @@ -44,49 +44,8 @@ const aiServices = [ }, ]; -// 版本套餐数据 -const versionPackages = [ - { - id: 1, - name: "普通版本", - icon: "📦", - price: "免费", - description: "充值即可使用,包含基础AI功能", - features: ["基础AI服务", "标准客服支持", "基础数据统计"], - status: "当前使用中", - buttonText: null, - tagColor: undefined, - }, - { - id: 2, - name: "标准版本", - icon: "👑", - price: "¥98/月", - tag: "推荐", - tagColor: "blue", - description: "适合中小企业,AI功能更丰富", - features: ["高级AI服务", "优先客服支持", "详细数据分析", "API接口访问"], - status: null, - buttonText: "立即升级", - }, - { - id: 3, - name: "企业版本", - icon: "🏢", - price: "¥1980/月", - description: "适合大型企业,提供专属服务", - features: [ - "专属AI服务", - "24小时专属客服", - "高级数据分析", - "API接口访问", - "专属技术支持", - ], - status: null, - buttonText: "立即升级", - tagColor: undefined, - }, -]; +// 快捷金额选项(可选) +const quickAmounts = [50, 100, 200, 500, 1000, 2000]; const Recharge: React.FC = () => { const navigate = useNavigate(); @@ -96,6 +55,7 @@ const Recharge: React.FC = () => { const [loading, setLoading] = useState(false); const [activeTab, setActiveTab] = useState("account"); const [taocanList, setTaocanList] = useState([]); + const [customAmount, setCustomAmount] = useState(""); // 自定义充值金额 // 加载套餐列表 useEffect(() => { @@ -113,8 +73,68 @@ const Recharge: React.FC = () => { loadTaocanList(); }, []); - // 充值操作 - const handleRecharge = async () => { + // 账户充值操作(自定义金额) + const handleCustomRecharge = async () => { + const amount = parseFloat(customAmount); + if (!customAmount || isNaN(amount) || amount <= 0) { + Toast.show({ content: "请输入有效的充值金额", position: "top" }); + return; + } + if (amount < 1) { + Toast.show({ content: "充值金额不能小于1元", position: "top" }); + return; + } + if (amount > 100000) { + Toast.show({ content: "单次充值金额不能超过10万元", position: "top" }); + return; + } + + setLoading(true); + try { + const res = await pay({ + id: "", // 自定义充值不需要套餐ID + price: Math.round(amount * 100), // 转换为分 + }); + // 假设返回的是二维码链接,存储在res中 + if (res) { + // 显示二维码弹窗 + Dialog.show({ + content: ( +
+
+ 请使用微信扫码支付 +
+ 支付二维码 +
+ 支付金额: ¥{amount.toFixed(2)} +
+
+ ), + closeOnMaskClick: true, + }); + } + } catch (error) { + console.error("支付失败:", error); + Toast.show({ content: "支付失败,请重试", position: "top" }); + } finally { + setLoading(false); + } + }; + + // 版本套餐充值操作 + const handlePackageRecharge = async () => { if (!selected) { Toast.show({ content: "请选择充值套餐", position: "top" }); return; @@ -177,98 +197,121 @@ const Recharge: React.FC = () => { + -
选择套餐
-
- {taocanList.map(item => ( - - ))} +
充值金额
+ + {/* 快捷金额选择 */} +
+
+ 快捷选择 +
+
+ {quickAmounts.map(amount => ( + + ))} +
- {selected && ( + + {/* 自定义金额输入 */} +
+
+ 或输入自定义金额(元) +
+ setCustomAmount(e.target.value)} + style={{ + width: "100%", + padding: "12px 16px", + border: "1px solid #e5e5e5", + borderRadius: "8px", + fontSize: "16px", + outline: "none", + }} + min="1" + max="100000" + step="0.01" + />
-
- {selected.name} - {selected.isRecommend === 1 && ( - - 推荐 - - )} - {selected.isHot === 1 && ( - - 热门 - - )} -
-
- 包含 {selected.tokens} Tokens -
- {selected.originalPrice && ( -
+ • 单次充值上限:100,000元 +
• 支持微信支付 +
+
+ + {customAmount && parseFloat(customAmount) > 0 && ( +
+
+ 充值金额: + - 原价: ¥{selected.originalPrice / 100} -
- )} + ¥{parseFloat(customAmount).toFixed(2)} + +
+
+ 到账金额:¥{parseFloat(customAmount).toFixed(2)} +
)} + + -
服务消耗
+
充值说明
- 使用以下服务将从余额中扣除相应费用。 + • 充值金额实时到账,可用于购买AI服务和版本套餐 +
+ • 使用AI服务将从余额中扣除相应费用 +
• 余额支持退款,详情请联系客服
+ {balance < 10 && (
@@ -349,63 +392,106 @@ const Recharge: React.FC = () => {
- 存客宝版本套餐 + 充值套餐
- 选择适合的版本,享受不同级别的AI服务 + 选择合适的充值套餐,享受更多优惠
- {versionPackages.map(pkg => ( + {taocanList.map(pkg => (
-
{pkg.icon}
+
💰
{pkg.name} - {pkg.tag && ( + {pkg.isRecommend === 1 && ( - {pkg.tag} + 推荐 + + )} + {pkg.isHot === 1 && ( + + 热门 )}
-
{pkg.price}
+
+ ¥{pkg.price / 100} +
-
- {pkg.description} -
+ {pkg.description && ( +
+ {pkg.description} +
+ )}
-
包含功能:
- {pkg.features.map((feature, index) => ( -
- - {feature} +
套餐内容:
+
+ + 包含 {pkg.tokens} Tokens +
+ {pkg.originalPrice && ( +
+ 原价: ¥{pkg.originalPrice / 100}
- ))} + )}
- {pkg.status && ( -
{pkg.status}
- )} - {pkg.buttonText && ( - - )} + ))}
+ + {selected && ( +
+ +
+ )}
); diff --git a/Cunkebao/src/pages/mobile/workspace/ai-knowledge/api 文档 b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/api 文档 new file mode 100644 index 00000000..1659a0c3 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/api 文档 @@ -0,0 +1,168 @@ +初始化AI功能(每次都得执行) + GET /v1/knowledge/init + + + + +发布并应用AI工具(修改知识库需要重新发布) + GET /v1/knowledge/release + 传参: + { + id:number + } + + 返回参数: + { + "id": 1, + "companyId": 2130, + "userId": 128, + "config": { + "name": "魔兽世界", + "model_id": "1737521813", + "prompt_info": "# 角色\r\n你是一位全能知识客服,作为专业的客服智能体,具备全面的知识储备,能够回答用户提出的各类问题。在回答问题前,会仔细查阅知识库内容,并且始终严格遵守中国法律法规。\r\n\r\n## 技能\r\n### 技能 1: 回答用户问题\r\n1. 当用户提出问题时,首先在知识库中进行搜索查找相关信息。\r\n2. 依据知识库中的内容,为用户提供准确、清晰、完整的回答。\r\n \r\n## 限制\r\n- 仅依据知识库内容回答问题,对于知识库中没有的信息,如实告知用户无法回答。\r\n- 回答必须严格遵循中国法律法规,不得出现任何违法违规内容。\r\n- 回答需简洁明了,避免冗长复杂的表述。" + }, + "createTime": "2025-10-24 16:55:08", + "updateTime": "2025-10-24 16:56:28", + "isRelease": 1, + "releaseTime": 1761296188, + "botId": "7564707767488610345", + "datasetId": "7564708881499619366" + } + + + +知识库类型 - 列表 + GET /v1/knowledge/typeList + 传参: + { + page:number + limit:number + } + 返回参数: + "total": 5, + "per_page": 20, + "current_page": 1, + "last_page": 1, + "data": [ + { + "id": 1, + "type": 0, + "name": "产品介绍库", + "description": "包含所有产品相关的介绍文档、图片和视频资料", + "label": [ + "产品", + "营销" + ], + "prompt": null, + "companyId": 0, + "userId": 0, + "createTime": null, + "updateTime": null, + "isDel": 0, + "delTime": 0 + }, + + ] + + +知识库类型 - 添加 + POST /v1/knowledge/addType + 传参: + { + name:string + description:string + label:string[] + prompt:string + } + + +知识库类型 - 编辑 + POST /v1/knowledge/editType + 传参: + { + id:number + name:string + description:string + label:string[] + prompt:string + } + + +知识库类型 - 删除 + DELETE /v1/knowledge/deleteType + 传参: + { + id:number + } + + +知识库 - 列表 + GET /v1/knowledge/getList + 传参: + { + name:number + typeId:number + label:string[] + fileUrl:string + } + 返回参数: + { + "total": 1, + "per_page": 20, + "current_page": 1, + "last_page": 1, + "data": [ + { + "id": 1, + "typeId": 1, + "name": "存客宝项目介绍(面向开发人员).docx", + "label": [ + "1231", + "3453" + ], + "companyId": 2130, + "userId": 128, + "createTime": 1761296164, + "updateTime": 1761296165, + "isDel": 0, + "delTime": 0, + "documentId": "7564706328503189558", + "fileUrl": "http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/10/22/9de59fc8723f10973ade586650dfb235.docx", + "type": { + "id": 1, + "type": 0, + "name": "产品介绍库", + "description": "包含所有产品相关的介绍文档、图片和视频资料", + "label": [ + "产品", + "营销" + ], + "prompt": null, + "companyId": 0, + "userId": 0, + "createTime": null, + "updateTime": null, + "isDel": 0, + "delTime": 0 + } + } + ] + } + + +知识库 - 添加 + POST /v1/knowledge/add + 传参: + { + name:number + typeId:number + label:string[] + fileUrl:string + } + +知识库 - 删除 + DELETE /v1/knowledge/delete + 传参: + { + id:number + } diff --git a/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/api.ts b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/api.ts new file mode 100644 index 00000000..aecda9a0 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/api.ts @@ -0,0 +1,109 @@ +import request from "@/api/request"; +import type { + KnowledgeBaseDetailResponse, + MaterialListResponse, + CallerListResponse, +} from "./data"; + +// 获取知识库类型详情(复用列表接口) +export function getKnowledgeBaseDetail( + id: number, +): Promise { + // 接口文档中没有单独的详情接口,通过列表接口获取 + return request("/v1/knowledge/typeList", { page: 1, limit: 100 }, "GET").then( + (res: any) => { + const item = res.data?.find((item: any) => item.id === id); + if (!item) { + throw new Error("知识库不存在"); + } + // 转换数据格式 + return { + ...item, + tags: item.label || [], + useIndependentPrompt: !!item.prompt, + independentPrompt: item.prompt || "", + materials: [], // 需要单独获取 + callers: [], // 暂无接口 + }; + }, + ); +} + +// 获取知识库素材列表(对应接口的 knowledge/getList) +export function getMaterialList(params: { + knowledgeBaseId: number; + page?: number; + limit?: number; + name?: string; + label?: string[]; +}): Promise { + return request( + "/v1/knowledge/getList", + { + typeId: params.knowledgeBaseId, + name: params.name, + label: params.label, + page: params.page || 1, + limit: params.limit || 20, + }, + "GET", + ).then((res: any) => ({ + list: res.data || [], + total: res.total || 0, + })); +} + +// 添加素材 +export function uploadMaterial(data: { + typeId: number; + name: string; + label: string[]; + fileUrl: string; +}): Promise { + return request("/v1/knowledge/add", data, "POST"); +} + +// 删除素材 +export function deleteMaterial(id: number): Promise { + return request("/v1/knowledge/delete", { id }, "DELETE"); +} + +// 获取调用者列表(接口未提供) +export function getCallerList( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + params: { + knowledgeBaseId: number; + page?: number; + limit?: number; + }, +): Promise { + // 注意:实际接口未提供,需要后端补充 + console.warn("getCallerList 接口未提供"); + return Promise.resolve({ + list: [], + total: 0, + }); +} + +// 更新知识库配置(使用编辑接口) +export function updateKnowledgeBaseConfig(data: { + id: number; + name?: string; + description?: string; + label?: string[]; + aiCallEnabled?: boolean; + useIndependentPrompt?: boolean; + independentPrompt?: string; +}): Promise { + return request( + "/v1/knowledge/editType", + { + id: data.id, + name: data.name || "", + description: data.description || "", + label: data.label || [], + prompt: data.useIndependentPrompt ? data.independentPrompt || "" : "", + }, + "POST", + ); +} diff --git a/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/data.ts b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/data.ts new file mode 100644 index 00000000..b477d105 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/data.ts @@ -0,0 +1,48 @@ +// AI知识库详情相关类型定义 +import type { KnowledgeBase, Caller } from "../list/data"; + +export type { KnowledgeBase, Caller }; + +// 素材类型(对应接口的 knowledge) +export interface Material { + id: number; + typeId: number; // 知识库类型ID + name: string; // 文件名 + label: string[]; // 标签 + companyId: number; + userId: number; + createTime: number; + updateTime: number; + isDel: number; + delTime: number; + documentId: string; // 文档ID + fileUrl: string; // 文件URL + type?: KnowledgeBase; // 关联的知识库类型信息 + // 前端扩展字段 + fileName?: string; // 映射自 name + fileSize?: number; // 文件大小(前端计算) + fileType?: string; // 文件类型(从 name 提取) + filePath?: string; // 映射自 fileUrl + tags?: string[]; // 映射自 label + uploadTime?: string; // 映射自 createTime + uploaderId?: number; // 映射自 userId + uploaderName?: string; +} + +// 知识库详情响应 +export interface KnowledgeBaseDetailResponse extends KnowledgeBase { + materials: Material[]; + callers: Caller[]; +} + +// 素材列表响应 +export interface MaterialListResponse { + list: Material[]; + total: number; +} + +// 调用者列表响应 +export interface CallerListResponse { + list: Caller[]; + total: number; +} 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 new file mode 100644 index 00000000..ed1fbafa --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/index.module.scss @@ -0,0 +1,481 @@ +// 详情页容器 +.detailPage { + background: #f5f5f5; + min-height: 100vh; +} + +// Tab容器 +.tabContainer { + background: #fff; + border-bottom: 1px solid #f0f0f0; +} + +.tabs { + display: flex; + padding: 0 16px; +} + +.tab { + flex: 1; + padding: 14px 0; + text-align: center; + font-size: 15px; + color: #666; + border-bottom: 2px solid transparent; + cursor: pointer; + transition: all 0.2s; + background: none; + border: none; + outline: none; + + &:active { + opacity: 0.7; + } +} + +.tabActive { + color: #1890ff; + font-weight: 600; + border-bottom-color: #1890ff; +} + +// 知识库信息卡片 +.infoCard { + background: #fff; + margin: 12px 16px; + border-radius: 12px; + padding: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); +} + +.infoHeader { + display: flex; + align-items: flex-start; + gap: 12px; + margin-bottom: 16px; +} + +.infoIcon { + width: 56px; + height: 56px; + border-radius: 12px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 28px; + flex-shrink: 0; +} + +.infoContent { + flex: 1; + min-width: 0; +} + +.infoName { + font-size: 18px; + font-weight: 600; + color: #222; + margin-bottom: 6px; +} + +.infoDescription { + font-size: 13px; + color: #888; + line-height: 1.5; +} + +.infoStats { + display: flex; + justify-content: space-between; + padding: 12px 0; + border-top: 1px solid #f0f0f0; + border-bottom: 1px solid #f0f0f0; + margin-bottom: 16px; +} + +.statItem { + flex: 1; + text-align: center; +} + +.statValue { + font-size: 20px; + font-weight: 600; + color: #1890ff; + margin-bottom: 4px; +} + +.statValueSuccess { + color: #52c41a; +} + +.statLabel { + font-size: 12px; + color: #888; +} + +.infoTags { + margin-bottom: 16px; +} + +.tagTitle { + font-size: 13px; + color: #666; + margin-bottom: 8px; + font-weight: 500; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.tag { + padding: 4px 12px; + border-radius: 10px; + font-size: 12px; + background: rgba(24, 144, 255, 0.1); + color: #1890ff; + border: 1px solid rgba(24, 144, 255, 0.2); +} + +// 配置区域 +.configSection { + margin-bottom: 16px; +} + +.configItem { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 0; + border-bottom: 1px solid #f5f5f5; + + &:last-child { + border-bottom: none; + } +} + +.configLabel { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: #333; +} + +.configIcon { + font-size: 16px; + color: #1890ff; +} + +.configDescription { + font-size: 12px; + color: #888; + margin-top: 4px; +} + +// 功能说明列表 +.featureList { + background: #f9f9f9; + border-radius: 8px; + padding: 12px; +} + +.featureItem { + display: flex; + align-items: flex-start; + gap: 8px; + font-size: 13px; + color: #666; + line-height: 1.6; + margin-bottom: 8px; + + &:last-child { + margin-bottom: 0; + } +} + +.featureIcon { + color: #52c41a; + margin-top: 2px; + flex-shrink: 0; +} + +// 调用者名单 +.callerSection { + margin-top: 16px; +} + +.sectionHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + +.sectionTitle { + display: flex; + align-items: center; + gap: 6px; + font-size: 14px; + font-weight: 500; + color: #333; +} + +.sectionCount { + font-size: 13px; + color: #888; + font-weight: normal; +} + +.callerList { + background: #f9f9f9; + border-radius: 8px; + padding: 8px; +} + +.callerItem { + display: flex; + align-items: center; + gap: 10px; + padding: 8px; + background: #fff; + border-radius: 6px; + margin-bottom: 6px; + + &:last-child { + margin-bottom: 0; + } +} + +.callerAvatar { + width: 40px; + height: 40px; + border-radius: 50%; + flex-shrink: 0; +} + +.callerInfo { + flex: 1; + min-width: 0; +} + +.callerName { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 2px; +} + +.callerRole { + font-size: 12px; + color: #888; +} + +.callerTime { + font-size: 11px; + color: #999; + white-space: nowrap; +} + +// 素材列表 +.materialSection { + padding: 12px 16px; +} + +.uploadButton { + width: 100%; + margin-bottom: 16px; +} + +.materialList { + display: flex; + flex-direction: column; + gap: 12px; +} + +.materialItem { + background: #fff; + border-radius: 12px; + padding: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + border: 1px solid #ececec; + display: flex; + align-items: center; + gap: 12px; +} + +.materialIcon { + width: 48px; + height: 48px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + flex-shrink: 0; +} + +.fileIcon { + background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%); + color: #fff; +} + +.videoIcon { + background: linear-gradient(135deg, #a855f7 0%, #9333ea 100%); + color: #fff; +} + +.docIcon { + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); + color: #fff; +} + +.materialContent { + flex: 1; + min-width: 0; +} + +.materialHeader { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 6px; +} + +.materialName { + font-size: 14px; + font-weight: 500; + color: #333; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + margin-right: 8px; +} + +.materialMenu { + font-size: 16px; + color: #888; + cursor: pointer; + padding: 2px; + flex-shrink: 0; +} + +.materialMeta { + display: flex; + align-items: center; + gap: 12px; + font-size: 12px; + color: #888; +} + +.materialSize { + display: flex; + align-items: center; + gap: 4px; +} + +.materialDate { + display: flex; + align-items: center; + gap: 4px; +} + +.materialTags { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 6px; +} + +.materialTag { + padding: 2px 8px; + border-radius: 8px; + font-size: 11px; + background: rgba(0, 0, 0, 0.05); + color: #666; +} + +// 底部按钮组 +.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; + } +} + +.editButton { + color: #1890ff; + border-color: #1890ff; +} + +.deleteButton { + color: #ff4d4f; + border-color: #ff4d4f; +} + +// 空状态 +.empty { + text-align: center; + padding: 60px 20px; + color: #bbb; +} + +.emptyIcon { + font-size: 64px; + color: #d9d9d9; + margin-bottom: 16px; +} + +.emptyText { + font-size: 14px; + color: #999; +} + +// 编辑提示词弹窗 +.promptEditModal { + .promptTextarea { + width: 100%; + min-height: 200px; + padding: 12px; + border: 1px solid #d9d9d9; + border-radius: 8px; + font-size: 14px; + line-height: 1.6; + resize: vertical; + font-family: inherit; + + &:focus { + outline: none; + border-color: #1890ff; + } + } + + .promptHint { + font-size: 12px; + color: #888; + margin-top: 8px; + line-height: 1.5; + } +} diff --git a/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/index.tsx b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/index.tsx new file mode 100644 index 00000000..4d2705ed --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/index.tsx @@ -0,0 +1,648 @@ +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 { + BookOutlined, + CheckCircleOutlined, + UserOutlined, + UploadOutlined, + FileOutlined, + VideoCameraOutlined, + FileTextOutlined, + MoreOutlined, + EditOutlined, + DeleteOutlined, + SettingOutlined, + ApiOutlined, + BulbOutlined, + CalendarOutlined, + DatabaseOutlined, +} from "@ant-design/icons"; +import Layout from "@/components/Layout/Layout"; +import NavCommon from "@/components/NavCommon"; +import style from "./index.module.scss"; +import { + getKnowledgeBaseDetail, + getMaterialList, + deleteMaterial, + updateKnowledgeBaseConfig, + uploadMaterial, +} from "./api"; +import { deleteKnowledgeBase } from "../list/api"; +import type { KnowledgeBase, Material, Caller } from "./data"; + +type TabType = "info" | "materials"; + +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, + ); + const [materials, setMaterials] = useState([]); + const [callers, setCallers] = useState([]); + const [promptEditVisible, setPromptEditVisible] = useState(false); + const [independentPrompt, setIndependentPrompt] = useState(""); + + useEffect(() => { + if (id) { + fetchDetail(); + } + }, [id]); + + const fetchDetail = async () => { + if (!id) return; + setLoading(true); + try { + const detail = await getKnowledgeBaseDetail(Number(id)); + setKnowledgeBase(detail); + setCallers(detail.callers || []); + setIndependentPrompt(detail.independentPrompt || ""); + + // 获取素材列表 + 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 (detail) { + setKnowledgeBase({ + ...detail, + materialCount: transformedMaterials.length, + }); + } + } catch (error) { + message.error("获取详情失败"); + navigate(-1); + } finally { + setLoading(false); + } + }; + + const handleAICallToggle = async (checked: boolean) => { + if (!id || !knowledgeBase) return; + + // 系统预设不允许修改 + if (knowledgeBase.type === 0) { + message.warning("系统预设知识库不可修改"); + return; + } + + try { + await updateKnowledgeBaseConfig({ + id: Number(id), + name: knowledgeBase.name, + description: knowledgeBase.description, + label: knowledgeBase.tags || knowledgeBase.label || [], + aiCallEnabled: checked, + useIndependentPrompt: knowledgeBase.useIndependentPrompt, + independentPrompt: knowledgeBase.independentPrompt || "", + }); + message.success(checked ? "已启用AI调用" : "已关闭AI调用"); + setKnowledgeBase(prev => + prev ? { ...prev, aiCallEnabled: checked } : null, + ); + } catch (error) { + message.error("操作失败"); + } + }; + + 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()) { + message.error("请输入提示词内容"); + return; + } + + try { + await updateKnowledgeBaseConfig({ + id: Number(id), + name: knowledgeBase.name, + description: knowledgeBase.description, + label: knowledgeBase.tags || knowledgeBase.label || [], + useIndependentPrompt: true, + independentPrompt: independentPrompt.trim(), + }); + message.success("保存成功"); + setKnowledgeBase(prev => + prev + ? { + ...prev, + useIndependentPrompt: true, + independentPrompt: independentPrompt.trim(), + prompt: independentPrompt.trim(), + } + : null, + ); + setPromptEditVisible(false); + } catch (error) { + message.error("保存失败"); + } + }; + + const handleDeleteKnowledge = async () => { + if (!id || !knowledgeBase) return; + + // 系统预设不允许删除 + if (knowledgeBase.type === 0) { + message.warning("系统预设知识库不可删除"); + return; + } + + Modal.confirm({ + title: "确认删除", + content: "删除后数据无法恢复,确定要删除该知识库吗?", + okText: "确定", + cancelText: "取消", + okButtonProps: { danger: true }, + onOk: async () => { + try { + await deleteKnowledgeBase(Number(id)); + message.success("删除成功"); + navigate(-1); + } catch (error) { + message.error("删除失败"); + } + }, + }); + }; + + const handleDeleteMaterial = async (materialId: number) => { + Modal.confirm({ + title: "确认删除", + content: "确定要删除该素材吗?", + okText: "确定", + cancelText: "取消", + okButtonProps: { danger: true }, + onOk: async () => { + try { + await deleteMaterial(materialId); + message.success("删除成功"); + setMaterials(prev => prev.filter(m => m.id !== materialId)); + } catch (error) { + message.error("删除失败"); + } + }, + }); + }; + + const handleUpload = async (file: File) => { + if (!id) return; + + try { + // 注意:这里需要先上传文件获取 fileUrl + // 实际项目中应该有单独的文件上传接口 + // 这里暂时使用占位实现 + message.loading("正在上传文件...", 0); + + // TODO: 调用文件上传接口获取 fileUrl + // const fileUrl = await uploadFile(file); + + // 临时方案:直接使用文件名作为占位 + const fileUrl = `temp://${file.name}`; + + await uploadMaterial({ + typeId: Number(id), + name: file.name, + label: [], // 可以后续添加标签编辑功能 + fileUrl: fileUrl, + }); + + message.destroy(); + message.success("上传成功"); + fetchDetail(); + } catch (error) { + message.destroy(); + message.error("上传失败"); + } + }; + + const getFileIcon = (fileType: string) => { + const type = fileType.toLowerCase(); + if (["mp4", "avi", "mov", "wmv"].includes(type)) { + return ( +
+ +
+ ); + } else if (["doc", "docx", "pdf", "txt"].includes(type)) { + return ( +
+ +
+ ); + } else { + return ( +
+ +
+ ); + } + }; + + const formatFileSize = (bytes: number) => { + if (bytes < 1024) return bytes + " B"; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"; + return (bytes / (1024 * 1024)).toFixed(1) + " MB"; + }; + + const renderInfoTab = () => { + if (!knowledgeBase) return null; + + const isSystemPreset = knowledgeBase.type === 0; // 系统预设只读 + + return ( + <> +
+
+
+ +
+
+
+ {knowledgeBase.name} + {isSystemPreset && ( + + (系统预设) + + )} +
+ {knowledgeBase.description && ( +
+ {knowledgeBase.description} +
+ )} +
+
+ +
+
+
+ {knowledgeBase.materialCount || 0} +
+
素材总数
+
+
+
+ {knowledgeBase.aiCallEnabled ? "启用" : "关闭"} +
+
AI状态
+
+
+
+ {knowledgeBase.tags?.length || 0} +
+
标签数
+
+
+ + {knowledgeBase.tags && knowledgeBase.tags.length > 0 && ( +
+
内容库标签
+
+ {knowledgeBase.tags.map((tag, index) => ( + + {tag} + + ))} +
+
+ )} + +
+
+
+
+ + AI调用配置 +
+
+ AI助手可以使用此内容库的素材 +
+
+ +
+ +
+
+
+ + 支持智能应答和推荐 +
+
+
+ +
+
+
+ + 实时响应用户查询 +
+
+
+
+ +
+
+ + 与各知识库的回复基本合用 +
+
+ + 确保所有知识库回复的一致性和专业度 +
+
+ + {callers.length > 0 && ( +
+
+
+ + 调用者名单 + {callers.length} +
+
+
+ {callers.slice(0, 3).map(caller => ( +
+ {caller.name} +
+
{caller.name}
+
{caller.role}
+
+
+ 调用 {caller.callCount} 次 · {caller.lastCallTime} +
+
+ ))} +
+
+ )} +
+ + {/* 系统预设不显示编辑和删除按钮 */} + {!isSystemPreset && ( +
+ + +
+ )} + + ); + }; + + const renderMaterialsTab = () => { + const isSystemPreset = knowledgeBase?.type === 0; // 系统预设只读 + + return ( +
+ {/* 系统预设不显示上传按钮 */} + {!isSystemPreset && ( + { + handleUpload(file); + return false; + }} + > + + + )} + +
+ {materials.length > 0 ? ( + materials.map(material => ( +
+ {getFileIcon(material.fileType)} +
+
+
+ {material.fileName} +
+ {/* 系统预设不显示删除按钮 */} + {!isSystemPreset && ( + , + label: "删除", + danger: true, + }, + ], + onClick: () => handleDeleteMaterial(material.id), + }} + trigger={["click"]} + placement="bottomRight" + > + + + )} +
+
+
+ + {formatFileSize(material.fileSize)} +
+
+ + {material.uploadTime} +
+
+ {material.tags && material.tags.length > 0 && ( +
+ {material.tags.map((tag, index) => ( + + {tag} + + ))} +
+ )} +
+
+ )) + ) : ( +
+
+ +
+
暂无素材
+
+ )} +
+
+ ); + }; + + return ( + + navigate("/workspace/ai-knowledge")} + /> +
+
+ + +
+
+ + } + > +
+ {loading ? ( +
+ +
+ ) : ( + <> + {activeTab === "info" && renderInfoTab()} + {activeTab === "materials" && renderMaterialsTab()} + + )} +
+ + {/* 编辑独立提示词弹窗 */} + setPromptEditVisible(false)} + onOk={handlePromptSave} + okText="保存" + cancelText="取消" + className={style.promptEditModal} + > +