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.
This commit is contained in:
@@ -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<any[]>([]);
|
||||
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: (
|
||||
<div style={{ textAlign: "center", padding: "20px" }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "16px",
|
||||
fontSize: "16px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
请使用微信扫码支付
|
||||
</div>
|
||||
<img
|
||||
src={res.code_url as any}
|
||||
alt="支付二维码"
|
||||
style={{ width: "250px", height: "250px", margin: "0 auto" }}
|
||||
/>
|
||||
<div
|
||||
style={{ marginTop: "16px", color: "#666", fontSize: "14px" }}
|
||||
>
|
||||
支付金额: ¥{amount.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
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 = () => {
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className={style["quick-card"]}>
|
||||
<div className={style["quick-title"]}>选择套餐</div>
|
||||
<div className={style["quick-list"]}>
|
||||
{taocanList.map(item => (
|
||||
<Button
|
||||
key={item.id}
|
||||
color={selected?.id === item.id ? "primary" : "default"}
|
||||
className={
|
||||
selected?.id === item.id
|
||||
? style["quick-btn-active"]
|
||||
: style["quick-btn"]
|
||||
}
|
||||
onClick={() => setSelected(item)}
|
||||
>
|
||||
<div>
|
||||
<div>¥{item.price / 100}</div>
|
||||
{item.discount && (
|
||||
<div style={{ fontSize: "12px", color: "#999" }}>
|
||||
{item.discount}折
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
<div className={style["quick-title"]}>充值金额</div>
|
||||
|
||||
{/* 快捷金额选择 */}
|
||||
<div style={{ marginBottom: "16px" }}>
|
||||
<div style={{ marginBottom: "8px", fontSize: "14px", color: "#666" }}>
|
||||
快捷选择
|
||||
</div>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: "8px" }}>
|
||||
{quickAmounts.map(amount => (
|
||||
<Button
|
||||
key={amount}
|
||||
color={
|
||||
customAmount === amount.toString() ? "primary" : "default"
|
||||
}
|
||||
onClick={() => setCustomAmount(amount.toString())}
|
||||
style={{ flex: "0 0 calc(33.333% - 6px)" }}
|
||||
>
|
||||
¥{amount}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{selected && (
|
||||
|
||||
{/* 自定义金额输入 */}
|
||||
<div style={{ marginBottom: "16px" }}>
|
||||
<div style={{ marginBottom: "8px", fontSize: "14px", color: "#666" }}>
|
||||
或输入自定义金额(元)
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="请输入充值金额"
|
||||
value={customAmount}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "12px",
|
||||
padding: "12px",
|
||||
background: "#f5f5f5",
|
||||
borderRadius: "8px",
|
||||
marginTop: "8px",
|
||||
fontSize: "12px",
|
||||
color: "#999",
|
||||
lineHeight: "1.5",
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: "6px" }}>
|
||||
<span style={{ fontWeight: "500" }}>{selected.name}</span>
|
||||
{selected.isRecommend === 1 && (
|
||||
<span
|
||||
style={{
|
||||
marginLeft: "8px",
|
||||
fontSize: "12px",
|
||||
color: "#1890ff",
|
||||
}}
|
||||
>
|
||||
推荐
|
||||
</span>
|
||||
)}
|
||||
{selected.isHot === 1 && (
|
||||
<span
|
||||
style={{
|
||||
marginLeft: "8px",
|
||||
fontSize: "12px",
|
||||
color: "#ff4d4f",
|
||||
}}
|
||||
>
|
||||
热门
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ fontSize: "14px", color: "#666" }}>
|
||||
包含 {selected.tokens} Tokens
|
||||
</div>
|
||||
{selected.originalPrice && (
|
||||
<div
|
||||
• 最低充值金额:1元
|
||||
<br />
|
||||
• 单次充值上限:100,000元
|
||||
<br />• 支持微信支付
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{customAmount && parseFloat(customAmount) > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "16px",
|
||||
padding: "12px",
|
||||
background: "#e6f7ff",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid #91d5ff",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ fontSize: "14px", color: "#333", marginBottom: "4px" }}
|
||||
>
|
||||
充值金额:
|
||||
<span
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "#999",
|
||||
textDecoration: "line-through",
|
||||
fontSize: "20px",
|
||||
fontWeight: "600",
|
||||
color: "#1890ff",
|
||||
}}
|
||||
>
|
||||
原价: ¥{selected.originalPrice / 100}
|
||||
</div>
|
||||
)}
|
||||
¥{parseFloat(customAmount).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: "12px", color: "#666" }}>
|
||||
到账金额:¥{parseFloat(customAmount).toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
block
|
||||
color="primary"
|
||||
size="large"
|
||||
className={style["recharge-main-btn"]}
|
||||
loading={loading}
|
||||
onClick={handleRecharge}
|
||||
onClick={handleCustomRecharge}
|
||||
disabled={!customAmount || parseFloat(customAmount) <= 0}
|
||||
>
|
||||
立即充值
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card className={style["desc-card"]}>
|
||||
<div className={style["desc-title"]}>服务消耗</div>
|
||||
<div className={style["desc-title"]}>充值说明</div>
|
||||
<div className={style["desc-text"]}>
|
||||
使用以下服务将从余额中扣除相应费用。
|
||||
• 充值金额实时到账,可用于购买AI服务和版本套餐
|
||||
<br />
|
||||
• 使用AI服务将从余额中扣除相应费用
|
||||
<br />• 余额支持退款,详情请联系客服
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{balance < 10 && (
|
||||
<Card className={style["warn-card"]}>
|
||||
<div className={style["warn-content"]}>
|
||||
@@ -349,63 +392,106 @@ const Recharge: React.FC = () => {
|
||||
<div className={style["tab-content"]}>
|
||||
<div className={style["version-header"]}>
|
||||
<CrownOutlined className={style["version-icon"]} />
|
||||
<span>存客宝版本套餐</span>
|
||||
<span>充值套餐</span>
|
||||
</div>
|
||||
<div className={style["version-description"]}>
|
||||
选择适合的版本,享受不同级别的AI服务
|
||||
选择合适的充值套餐,享受更多优惠
|
||||
</div>
|
||||
|
||||
<div className={style["version-packages"]}>
|
||||
{versionPackages.map(pkg => (
|
||||
{taocanList.map(pkg => (
|
||||
<Card key={pkg.id} className={style["version-card"]}>
|
||||
<div className={style["package-header"]}>
|
||||
<div className={style["package-info"]}>
|
||||
<div className={style["package-icon"]}>{pkg.icon}</div>
|
||||
<div className={style["package-icon"]}>💰</div>
|
||||
<div className={style["package-details"]}>
|
||||
<div className={style["package-name"]}>
|
||||
{pkg.name}
|
||||
{pkg.tag && (
|
||||
{pkg.isRecommend === 1 && (
|
||||
<span
|
||||
className={`${style["package-tag"]} ${style[`tag-${pkg.tagColor || "blue"}`]}`}
|
||||
className={`${style["package-tag"]} ${style["tag-blue"]}`}
|
||||
>
|
||||
{pkg.tag}
|
||||
推荐
|
||||
</span>
|
||||
)}
|
||||
{pkg.isHot === 1 && (
|
||||
<span
|
||||
className={`${style["package-tag"]} ${style["tag-red"]}`}
|
||||
>
|
||||
热门
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={style["package-price"]}>{pkg.price}</div>
|
||||
<div className={style["package-price"]}>
|
||||
¥{pkg.price / 100}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["package-description"]}>
|
||||
{pkg.description}
|
||||
</div>
|
||||
{pkg.description && (
|
||||
<div className={style["package-description"]}>
|
||||
{pkg.description}
|
||||
</div>
|
||||
)}
|
||||
<div className={style["package-features"]}>
|
||||
<div className={style["features-title"]}>包含功能:</div>
|
||||
{pkg.features.map((feature, index) => (
|
||||
<div key={index} className={style["feature-item"]}>
|
||||
<span className={style["feature-check"]}>✓</span>
|
||||
{feature}
|
||||
<div className={style["features-title"]}>套餐内容:</div>
|
||||
<div className={style["feature-item"]}>
|
||||
<span className={style["feature-check"]}>✓</span>
|
||||
包含 {pkg.tokens} Tokens
|
||||
</div>
|
||||
{pkg.originalPrice && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "#999",
|
||||
textDecoration: "line-through",
|
||||
marginTop: "4px",
|
||||
}}
|
||||
>
|
||||
原价: ¥{pkg.originalPrice / 100}
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
{pkg.status && (
|
||||
<div className={style["package-status"]}>{pkg.status}</div>
|
||||
)}
|
||||
{pkg.buttonText && (
|
||||
<Button
|
||||
block
|
||||
color="primary"
|
||||
className={style["upgrade-btn"]}
|
||||
onClick={() => {
|
||||
Toast.show({ content: "升级功能开发中", position: "top" });
|
||||
}}
|
||||
>
|
||||
{pkg.buttonText}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
block
|
||||
color={selected?.id === pkg.id ? "primary" : "default"}
|
||||
className={
|
||||
selected?.id === pkg.id
|
||||
? style["upgrade-btn-active"]
|
||||
: style["upgrade-btn"]
|
||||
}
|
||||
onClick={() => setSelected(pkg)}
|
||||
>
|
||||
{selected?.id === pkg.id ? "已选择" : "选择套餐"}
|
||||
</Button>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selected && (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
padding: "16px",
|
||||
background: "#fff",
|
||||
borderTop: "1px solid #f0f0f0",
|
||||
boxShadow: "0 -2px 8px rgba(0,0,0,0.08)",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
block
|
||||
color="primary"
|
||||
size="large"
|
||||
loading={loading}
|
||||
onClick={handlePackageRecharge}
|
||||
>
|
||||
立即购买 ¥{selected.price / 100}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
168
Cunkebao/src/pages/mobile/workspace/ai-knowledge/api 文档
Normal file
168
Cunkebao/src/pages/mobile/workspace/ai-knowledge/api 文档
Normal file
@@ -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
|
||||
}
|
||||
109
Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/api.ts
Normal file
109
Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/api.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import request from "@/api/request";
|
||||
import type {
|
||||
KnowledgeBaseDetailResponse,
|
||||
MaterialListResponse,
|
||||
CallerListResponse,
|
||||
} from "./data";
|
||||
|
||||
// 获取知识库类型详情(复用列表接口)
|
||||
export function getKnowledgeBaseDetail(
|
||||
id: number,
|
||||
): Promise<KnowledgeBaseDetailResponse> {
|
||||
// 接口文档中没有单独的详情接口,通过列表接口获取
|
||||
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<MaterialListResponse> {
|
||||
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<any> {
|
||||
return request("/v1/knowledge/add", data, "POST");
|
||||
}
|
||||
|
||||
// 删除素材
|
||||
export function deleteMaterial(id: number): Promise<any> {
|
||||
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<CallerListResponse> {
|
||||
// 注意:实际接口未提供,需要后端补充
|
||||
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<any> {
|
||||
return request(
|
||||
"/v1/knowledge/editType",
|
||||
{
|
||||
id: data.id,
|
||||
name: data.name || "",
|
||||
description: data.description || "",
|
||||
label: data.label || [],
|
||||
prompt: data.useIndependentPrompt ? data.independentPrompt || "" : "",
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<TabType>("info");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBase | null>(
|
||||
null,
|
||||
);
|
||||
const [materials, setMaterials] = useState<Material[]>([]);
|
||||
const [callers, setCallers] = useState<Caller[]>([]);
|
||||
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 (
|
||||
<div className={`${style.materialIcon} ${style.videoIcon}`}>
|
||||
<VideoCameraOutlined />
|
||||
</div>
|
||||
);
|
||||
} else if (["doc", "docx", "pdf", "txt"].includes(type)) {
|
||||
return (
|
||||
<div className={`${style.materialIcon} ${style.docIcon}`}>
|
||||
<FileTextOutlined />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className={`${style.materialIcon} ${style.fileIcon}`}>
|
||||
<FileOutlined />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div className={style.infoCard}>
|
||||
<div className={style.infoHeader}>
|
||||
<div className={style.infoIcon}>
|
||||
<BookOutlined />
|
||||
</div>
|
||||
<div className={style.infoContent}>
|
||||
<div className={style.infoName}>
|
||||
{knowledgeBase.name}
|
||||
{isSystemPreset && (
|
||||
<span
|
||||
style={{
|
||||
marginLeft: "8px",
|
||||
fontSize: "12px",
|
||||
color: "#999",
|
||||
fontWeight: "normal",
|
||||
}}
|
||||
>
|
||||
(系统预设)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{knowledgeBase.description && (
|
||||
<div className={style.infoDescription}>
|
||||
{knowledgeBase.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style.infoStats}>
|
||||
<div className={style.statItem}>
|
||||
<div className={style.statValue}>
|
||||
{knowledgeBase.materialCount || 0}
|
||||
</div>
|
||||
<div className={style.statLabel}>素材总数</div>
|
||||
</div>
|
||||
<div className={style.statItem}>
|
||||
<div
|
||||
className={`${style.statValue} ${knowledgeBase.aiCallEnabled ? style.statValueSuccess : ""}`}
|
||||
>
|
||||
{knowledgeBase.aiCallEnabled ? "启用" : "关闭"}
|
||||
</div>
|
||||
<div className={style.statLabel}>AI状态</div>
|
||||
</div>
|
||||
<div className={style.statItem}>
|
||||
<div className={style.statValue}>
|
||||
{knowledgeBase.tags?.length || 0}
|
||||
</div>
|
||||
<div className={style.statLabel}>标签数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{knowledgeBase.tags && knowledgeBase.tags.length > 0 && (
|
||||
<div className={style.infoTags}>
|
||||
<div className={style.tagTitle}>内容库标签</div>
|
||||
<div className={style.tags}>
|
||||
{knowledgeBase.tags.map((tag, index) => (
|
||||
<span key={index} className={style.tag}>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={style.configSection}>
|
||||
<div className={style.configItem}>
|
||||
<div>
|
||||
<div className={style.configLabel}>
|
||||
<ApiOutlined className={style.configIcon} />
|
||||
AI调用配置
|
||||
</div>
|
||||
<div className={style.configDescription}>
|
||||
AI助手可以使用此内容库的素材
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={knowledgeBase.aiCallEnabled}
|
||||
onChange={handleAICallToggle}
|
||||
disabled={isSystemPreset}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={style.configItem}>
|
||||
<div>
|
||||
<div className={style.configLabel}>
|
||||
<BulbOutlined className={style.configIcon} />
|
||||
支持智能应答和推荐
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style.configItem}>
|
||||
<div>
|
||||
<div className={style.configLabel}>
|
||||
<SettingOutlined className={style.configIcon} />
|
||||
实时响应用户查询
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style.featureList}>
|
||||
<div className={style.featureItem}>
|
||||
<CheckCircleOutlined className={style.featureIcon} />
|
||||
与各知识库的回复基本合用
|
||||
</div>
|
||||
<div className={style.featureItem}>
|
||||
<CheckCircleOutlined className={style.featureIcon} />
|
||||
确保所有知识库回复的一致性和专业度
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{callers.length > 0 && (
|
||||
<div className={style.callerSection}>
|
||||
<div className={style.sectionHeader}>
|
||||
<div className={style.sectionTitle}>
|
||||
<UserOutlined />
|
||||
调用者名单
|
||||
<span className={style.sectionCount}>{callers.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.callerList}>
|
||||
{callers.slice(0, 3).map(caller => (
|
||||
<div key={caller.id} className={style.callerItem}>
|
||||
<img
|
||||
src={caller.avatar}
|
||||
alt={caller.name}
|
||||
className={style.callerAvatar}
|
||||
/>
|
||||
<div className={style.callerInfo}>
|
||||
<div className={style.callerName}>{caller.name}</div>
|
||||
<div className={style.callerRole}>{caller.role}</div>
|
||||
</div>
|
||||
<div className={style.callerTime}>
|
||||
调用 {caller.callCount} 次 · {caller.lastCallTime}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 系统预设不显示编辑和删除按钮 */}
|
||||
{!isSystemPreset && (
|
||||
<div className={style.bottomActions}>
|
||||
<button
|
||||
className={`${style.actionButton} ${style.editButton}`}
|
||||
onClick={() => navigate(`/workspace/ai-knowledge/${id}/edit`)}
|
||||
>
|
||||
<EditOutlined /> 编辑库
|
||||
</button>
|
||||
<button
|
||||
className={`${style.actionButton} ${style.deleteButton}`}
|
||||
onClick={handleDeleteKnowledge}
|
||||
>
|
||||
<DeleteOutlined /> 删除库
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMaterialsTab = () => {
|
||||
const isSystemPreset = knowledgeBase?.type === 0; // 系统预设只读
|
||||
|
||||
return (
|
||||
<div className={style.materialSection}>
|
||||
{/* 系统预设不显示上传按钮 */}
|
||||
{!isSystemPreset && (
|
||||
<Upload
|
||||
showUploadList={false}
|
||||
beforeUpload={file => {
|
||||
handleUpload(file);
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<UploadOutlined />}
|
||||
className={style.uploadButton}
|
||||
size="large"
|
||||
>
|
||||
上传素材到此库
|
||||
</Button>
|
||||
</Upload>
|
||||
)}
|
||||
|
||||
<div className={style.materialList}>
|
||||
{materials.length > 0 ? (
|
||||
materials.map(material => (
|
||||
<div key={material.id} className={style.materialItem}>
|
||||
{getFileIcon(material.fileType)}
|
||||
<div className={style.materialContent}>
|
||||
<div className={style.materialHeader}>
|
||||
<div className={style.materialName}>
|
||||
{material.fileName}
|
||||
</div>
|
||||
{/* 系统预设不显示删除按钮 */}
|
||||
{!isSystemPreset && (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "delete",
|
||||
icon: <DeleteOutlined />,
|
||||
label: "删除",
|
||||
danger: true,
|
||||
},
|
||||
],
|
||||
onClick: () => handleDeleteMaterial(material.id),
|
||||
}}
|
||||
trigger={["click"]}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<MoreOutlined className={style.materialMenu} />
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
<div className={style.materialMeta}>
|
||||
<div className={style.materialSize}>
|
||||
<DatabaseOutlined />
|
||||
{formatFileSize(material.fileSize)}
|
||||
</div>
|
||||
<div className={style.materialDate}>
|
||||
<CalendarOutlined />
|
||||
{material.uploadTime}
|
||||
</div>
|
||||
</div>
|
||||
{material.tags && material.tags.length > 0 && (
|
||||
<div className={style.materialTags}>
|
||||
{material.tags.map((tag, index) => (
|
||||
<span key={index} className={style.materialTag}>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className={style.empty}>
|
||||
<div className={style.emptyIcon}>
|
||||
<FileOutlined />
|
||||
</div>
|
||||
<div className={style.emptyText}>暂无素材</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<>
|
||||
<NavCommon
|
||||
title="AI知识库"
|
||||
backFn={() => navigate("/workspace/ai-knowledge")}
|
||||
/>
|
||||
<div className={style.tabContainer}>
|
||||
<div className={style.tabs}>
|
||||
<button
|
||||
className={`${style.tab} ${activeTab === "info" ? style.tabActive : ""}`}
|
||||
onClick={() => setActiveTab("info")}
|
||||
>
|
||||
内容库列表
|
||||
</button>
|
||||
<button
|
||||
className={`${style.tab} ${activeTab === "materials" ? style.tabActive : ""}`}
|
||||
onClick={() => setActiveTab("materials")}
|
||||
>
|
||||
{knowledgeBase?.name || "知识库"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className={style.detailPage}>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: "center", padding: "60px 0" }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{activeTab === "info" && renderInfoTab()}
|
||||
{activeTab === "materials" && renderMaterialsTab()}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 编辑独立提示词弹窗 */}
|
||||
<Modal
|
||||
title="编辑独立提示词"
|
||||
open={promptEditVisible}
|
||||
onCancel={() => setPromptEditVisible(false)}
|
||||
onOk={handlePromptSave}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
className={style.promptEditModal}
|
||||
>
|
||||
<textarea
|
||||
className={style.promptTextarea}
|
||||
value={independentPrompt}
|
||||
onChange={e => setIndependentPrompt(e.target.value)}
|
||||
placeholder="请输入独立提示词..."
|
||||
maxLength={1000}
|
||||
/>
|
||||
<div className={style.promptHint}>
|
||||
💡 独立提示词将与统一提示词合并使用,为该知识库提供专业指导
|
||||
</div>
|
||||
</Modal>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AIKnowledgeDetail;
|
||||
@@ -0,0 +1,4 @@
|
||||
// 表单页API - 复用列表页的接口
|
||||
export { createKnowledgeBase, updateKnowledgeBase } from "../list/api";
|
||||
|
||||
export { getKnowledgeBaseDetail } from "../detail/api";
|
||||
@@ -0,0 +1,2 @@
|
||||
// AI知识库表单相关类型定义
|
||||
export type { KnowledgeBaseFormData } from "../list/data";
|
||||
@@ -0,0 +1,318 @@
|
||||
// 表单页容器
|
||||
.formPage {
|
||||
padding: 16px;
|
||||
background: #f5f5f5;
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
.formContainer {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid #1890ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.sectionIcon {
|
||||
color: #1890ff;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.formItem {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.formLabel {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #ff4d4f;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.formTextarea {
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.formHint {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-top: 6px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.charCount {
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
// 独立提示词区域
|
||||
.promptSection {
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.promptHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.promptLabel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.promptIcon {
|
||||
color: #1890ff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.promptDescription {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 12px;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #1890ff;
|
||||
}
|
||||
|
||||
.promptTextarea {
|
||||
width: 100%;
|
||||
min-height: 160px;
|
||||
padding: 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:focus {
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #bfbfbf;
|
||||
}
|
||||
}
|
||||
|
||||
.promptDisabled {
|
||||
background: #f5f5f5;
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.promptHint {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
margin-top: 12px;
|
||||
padding: 10px 12px;
|
||||
background: #fffbe6;
|
||||
border: 1px solid #ffe58f;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
color: #ad6800;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.hintIcon {
|
||||
color: #faad14;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// AI配置区域
|
||||
.configSection {
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.configItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
// 标签输入
|
||||
.tagInput {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tagList {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.tagItem {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 12px;
|
||||
background: rgba(24, 144, 255, 0.1);
|
||||
color: #1890ff;
|
||||
border: 1px solid rgba(24, 144, 255, 0.2);
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tagRemove {
|
||||
cursor: pointer;
|
||||
color: #1890ff;
|
||||
font-size: 14px;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #096dd9;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部按钮
|
||||
.formFooter {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.footerButton {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
outline: none;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.cancelButton {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
|
||||
&:active {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
}
|
||||
|
||||
.submitButton {
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
|
||||
&:active {
|
||||
background: #096dd9;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #d9d9d9;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载状态
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
// 信息提示卡片
|
||||
.infoCard {
|
||||
background: linear-gradient(135deg, #e6f7ff 0%, #f0f5ff 100%);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
border: 1px solid #91d5ff;
|
||||
}
|
||||
|
||||
.infoCardIcon {
|
||||
font-size: 18px;
|
||||
color: #1890ff;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.infoCardContent {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
307
Cunkebao/src/pages/mobile/workspace/ai-knowledge/form/index.tsx
Normal file
307
Cunkebao/src/pages/mobile/workspace/ai-knowledge/form/index.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { message, Spin, Switch, Input } from "antd";
|
||||
|
||||
const { TextArea } = Input;
|
||||
import {
|
||||
BookOutlined,
|
||||
BulbOutlined,
|
||||
InfoCircleOutlined,
|
||||
CloseOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import style from "./index.module.scss";
|
||||
import {
|
||||
createKnowledgeBase,
|
||||
updateKnowledgeBase,
|
||||
getKnowledgeBaseDetail,
|
||||
} from "./api";
|
||||
import type { KnowledgeBaseFormData } from "./data";
|
||||
|
||||
const AIKnowledgeForm: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const isEdit = !!id;
|
||||
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// 表单字段
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [tagInput, setTagInput] = useState("");
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [useIndependentPrompt, setUseIndependentPrompt] = useState(false);
|
||||
const [independentPrompt, setIndependentPrompt] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit && id) {
|
||||
fetchDetail();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isEdit, id]);
|
||||
|
||||
const fetchDetail = async () => {
|
||||
if (!id) return;
|
||||
setDetailLoading(true);
|
||||
try {
|
||||
const detail = await getKnowledgeBaseDetail(Number(id));
|
||||
|
||||
// 检查是否为系统预设(type === 0),系统预设不允许编辑
|
||||
if (detail.type === 0) {
|
||||
message.warning("系统预设知识库不可编辑");
|
||||
navigate(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
setName(detail.name || "");
|
||||
setDescription(detail.description || "");
|
||||
setTags(detail.tags || []);
|
||||
setTagInput(detail.tags?.join(", ") || "");
|
||||
setUseIndependentPrompt(detail.useIndependentPrompt || false);
|
||||
setIndependentPrompt(detail.independentPrompt || "");
|
||||
} catch (error) {
|
||||
message.error("获取详情失败");
|
||||
navigate(-1);
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTagInputChange = (value: string) => {
|
||||
setTagInput(value);
|
||||
// 实时解析标签
|
||||
const parsedTags = value
|
||||
.split(/[,,]/)
|
||||
.map(t => t.trim())
|
||||
.filter(Boolean);
|
||||
setTags(parsedTags);
|
||||
};
|
||||
|
||||
const handleRemoveTag = (tagToRemove: string) => {
|
||||
const newTags = tags.filter(t => t !== tagToRemove);
|
||||
setTags(newTags);
|
||||
setTagInput(newTags.join(", "));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// 表单验证
|
||||
if (!name.trim()) {
|
||||
message.error("请输入内容库名称");
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.length > 50) {
|
||||
message.error("名称不能超过50个字符");
|
||||
return;
|
||||
}
|
||||
|
||||
if (description.length > 200) {
|
||||
message.error("描述不能超过200个字符");
|
||||
return;
|
||||
}
|
||||
|
||||
if (useIndependentPrompt && !independentPrompt.trim()) {
|
||||
message.error("启用独立提示词时,请输入提示词内容");
|
||||
return;
|
||||
}
|
||||
|
||||
if (independentPrompt.length > 1000) {
|
||||
message.error("提示词不能超过1000个字符");
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const formData: KnowledgeBaseFormData = {
|
||||
name: name.trim(),
|
||||
description: description.trim(),
|
||||
tags: tags,
|
||||
useIndependentPrompt,
|
||||
independentPrompt: useIndependentPrompt ? independentPrompt.trim() : "",
|
||||
};
|
||||
|
||||
if (isEdit && id) {
|
||||
formData.id = Number(id);
|
||||
await updateKnowledgeBase(formData);
|
||||
message.success("更新成功");
|
||||
} else {
|
||||
await createKnowledgeBase(formData);
|
||||
message.success("创建成功");
|
||||
}
|
||||
|
||||
navigate(-1);
|
||||
} catch (error) {
|
||||
message.error(isEdit ? "更新失败" : "创建失败");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
navigate(-1);
|
||||
};
|
||||
|
||||
if (detailLoading) {
|
||||
return (
|
||||
<Layout
|
||||
header={<NavCommon title={isEdit ? "编辑内容库" : "新建内容库"} />}
|
||||
>
|
||||
<div className={style.loading}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={<NavCommon title={isEdit ? "编辑内容库" : "新建内容库"} />}
|
||||
footer={
|
||||
<div className={style.formFooter}>
|
||||
<button
|
||||
className={`${style.footerButton} ${style.cancelButton}`}
|
||||
onClick={handleCancel}
|
||||
disabled={submitting}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
className={`${style.footerButton} ${style.submitButton}`}
|
||||
onClick={handleSubmit}
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? "提交中..." : isEdit ? "更新" : "创建"}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={style.formPage}>
|
||||
<div className={style.formContainer}>
|
||||
{/* 信息提示 */}
|
||||
<div className={style.infoCard}>
|
||||
<InfoCircleOutlined className={style.infoCardIcon} />
|
||||
<div className={style.infoCardContent}>
|
||||
创建一个新的内容库来组织和管理您的素材,支持配置独立提示词和AI调用设置
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 基本信息 */}
|
||||
<div className={style.sectionTitle}>
|
||||
<BookOutlined className={style.sectionIcon} />
|
||||
基本信息
|
||||
</div>
|
||||
|
||||
<div className={style.formItem}>
|
||||
<div className={style.formLabel}>
|
||||
<span className={style.required}>*</span>
|
||||
内容库名称
|
||||
</div>
|
||||
<Input
|
||||
placeholder="如:产品介绍库"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
maxLength={50}
|
||||
size="large"
|
||||
count={{
|
||||
show: true,
|
||||
max: 50,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={style.formItem}>
|
||||
<div className={style.formLabel}>描述</div>
|
||||
<TextArea
|
||||
placeholder="描述这个内容库的用途..."
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
maxLength={200}
|
||||
rows={4}
|
||||
showCount
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={style.formItem}>
|
||||
<div className={style.formLabel}>标签</div>
|
||||
<div className={style.tagInput}>
|
||||
<Input
|
||||
placeholder="多个标签用逗号分隔,如:产品,营销,介绍"
|
||||
value={tagInput}
|
||||
onChange={e => handleTagInputChange(e.target.value)}
|
||||
size="large"
|
||||
/>
|
||||
{tags.length > 0 && (
|
||||
<div className={style.tagList}>
|
||||
{tags.map((tag, index) => (
|
||||
<span key={index} className={style.tagItem}>
|
||||
{tag}
|
||||
<CloseOutlined
|
||||
className={style.tagRemove}
|
||||
onClick={() => handleRemoveTag(tag)}
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={style.formHint}>
|
||||
标签用于分类和快速查找,建议使用3-5个关键标签
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 独立提示词 */}
|
||||
<div className={style.sectionTitle}>
|
||||
<BulbOutlined className={style.sectionIcon} />
|
||||
独立提示词配置
|
||||
</div>
|
||||
|
||||
<div className={style.promptSection}>
|
||||
<div className={style.promptHeader}>
|
||||
<div className={style.promptLabel}>
|
||||
<BulbOutlined className={style.promptIcon} />
|
||||
使用独立提示词
|
||||
</div>
|
||||
<Switch
|
||||
checked={useIndependentPrompt}
|
||||
onChange={setUseIndependentPrompt}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{useIndependentPrompt && (
|
||||
<>
|
||||
<div className={style.promptDescription}>
|
||||
设置此知识库的专业指导,将与统一提示词合并使用,为该知识库提供更精准的回答规则
|
||||
</div>
|
||||
|
||||
<TextArea
|
||||
placeholder="请输入独立提示词内容,例如:
|
||||
- 回答风格:专业、友好、简洁
|
||||
- 特殊要求:强调产品优势、突出技术细节
|
||||
- 回答格式:分点列举、数据支撑..."
|
||||
value={independentPrompt}
|
||||
onChange={e => setIndependentPrompt(e.target.value)}
|
||||
maxLength={1000}
|
||||
rows={8}
|
||||
showCount
|
||||
disabled={!useIndependentPrompt}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={style.promptHint}>
|
||||
<InfoCircleOutlined className={style.hintIcon} />
|
||||
<div>
|
||||
<strong>生效逻辑:</strong>
|
||||
统一提示词(全局规则) + 独立提示词(专业指导) = 最终AI回复内容
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AIKnowledgeForm;
|
||||
90
Cunkebao/src/pages/mobile/workspace/ai-knowledge/list/api.ts
Normal file
90
Cunkebao/src/pages/mobile/workspace/ai-knowledge/list/api.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import request from "@/api/request";
|
||||
import type {
|
||||
KnowledgeBaseListResponse,
|
||||
KnowledgeBaseFormData,
|
||||
GlobalPromptConfig,
|
||||
} from "./data";
|
||||
|
||||
// 获取知识库列表
|
||||
export function updateTypeStatus(params: { id: number; status: number }) {
|
||||
return request("/v1/knowledge/updateTypeStatus", params, "PUT");
|
||||
}
|
||||
|
||||
// 初始化AI功能
|
||||
export function initAIKnowledge(): Promise<any> {
|
||||
return request("/v1/knowledge/init", {}, "GET");
|
||||
}
|
||||
|
||||
// 发布并应用AI工具
|
||||
export function releaseAIKnowledge(id: number): Promise<any> {
|
||||
return request("/v1/knowledge/release", { id }, "GET");
|
||||
}
|
||||
|
||||
// 获取知识库类型列表
|
||||
export function fetchKnowledgeBaseList(params: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}): Promise<KnowledgeBaseListResponse> {
|
||||
return request("/v1/knowledge/typeList", params, "GET");
|
||||
}
|
||||
|
||||
// 创建知识库类型
|
||||
export function createKnowledgeBase(data: KnowledgeBaseFormData): Promise<any> {
|
||||
return request(
|
||||
"/v1/knowledge/addType",
|
||||
{
|
||||
name: data.name,
|
||||
description: data.description || "",
|
||||
label: data.tags || [],
|
||||
prompt: data.useIndependentPrompt ? data.independentPrompt || "" : "",
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
}
|
||||
|
||||
// 更新知识库类型
|
||||
export function updateKnowledgeBase(data: KnowledgeBaseFormData): Promise<any> {
|
||||
return request(
|
||||
"/v1/knowledge/editType",
|
||||
{
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
description: data.description || "",
|
||||
label: data.tags || [],
|
||||
prompt: data.useIndependentPrompt ? data.independentPrompt || "" : "",
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
}
|
||||
|
||||
// 删除知识库类型
|
||||
export function deleteKnowledgeBase(id: number): Promise<any> {
|
||||
return request("/v1/knowledge/deleteType", { id }, "DELETE");
|
||||
}
|
||||
|
||||
// 切换知识库状态(启用/禁用)- 注意:实际接口未提供,保留兼容
|
||||
export function toggleKnowledgeBaseStatus(
|
||||
id: number,
|
||||
status: 0 | 1,
|
||||
): Promise<any> {
|
||||
// 由于接口文档中没有此接口,暂时使用编辑接口实现
|
||||
console.warn("toggleKnowledgeBaseStatus 接口未提供,需要后端补充");
|
||||
return Promise.resolve({ success: true });
|
||||
}
|
||||
|
||||
// 获取统一提示词配置 - 使用发布接口返回的 prompt_info
|
||||
export function getGlobalPrompt(): Promise<GlobalPromptConfig> {
|
||||
// 注意:实际接口未单独提供,需要通过 release 接口获取
|
||||
console.warn("getGlobalPrompt 接口未提供,需要后端补充");
|
||||
return Promise.resolve({
|
||||
enabled: true,
|
||||
content: "",
|
||||
});
|
||||
}
|
||||
|
||||
// 保存统一提示词配置
|
||||
export function saveGlobalPrompt(data: GlobalPromptConfig): Promise<any> {
|
||||
// 注意:实际接口未提供,需要后端补充
|
||||
console.warn("saveGlobalPrompt 接口未提供,需要后端补充");
|
||||
return Promise.resolve({ success: true });
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Modal, Switch, message } from "antd";
|
||||
import {
|
||||
InfoCircleOutlined,
|
||||
BulbOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { getGlobalPrompt, saveGlobalPrompt } from "../api";
|
||||
import type { GlobalPromptConfig } from "../data";
|
||||
import style from "../index.module.scss";
|
||||
|
||||
interface GlobalPromptModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_PROMPT = `你是存客宝AI知识库助手。请遵循以下基本原则:
|
||||
|
||||
1. 专业性: 使用专业但易懂的语言回答问题
|
||||
2. 准确性: 基于知识库内容提供准确的信息
|
||||
3. 友好性: 保持友好、耐心的服务态度
|
||||
4. 简洁性: 回答简明扼要,重点突出
|
||||
5. 引用性: 回答时注明信息来源`;
|
||||
|
||||
const GlobalPromptModal: React.FC<GlobalPromptModalProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
const [content, setContent] = useState(DEFAULT_PROMPT);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
fetchGlobalPrompt();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const fetchGlobalPrompt = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const config = await getGlobalPrompt();
|
||||
setEnabled(config.enabled);
|
||||
setContent(config.content || DEFAULT_PROMPT);
|
||||
} catch (error) {
|
||||
message.error("获取配置失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (enabled && !content.trim()) {
|
||||
message.error("启用统一提示词时,请输入提示词内容");
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
await saveGlobalPrompt({
|
||||
enabled,
|
||||
content: content.trim(),
|
||||
});
|
||||
message.success("保存成功");
|
||||
onClose();
|
||||
} catch (error) {
|
||||
message.error("保存失败");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<InfoCircleOutlined style={{ color: "#1890ff" }} />
|
||||
配置统一提示词
|
||||
</div>
|
||||
}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
onOk={handleSave}
|
||||
confirmLoading={saving}
|
||||
width={600}
|
||||
okText="保存配置"
|
||||
cancelText="取消"
|
||||
className={style.promptModal}
|
||||
>
|
||||
<div className={style.promptContent}>
|
||||
<p style={{ fontSize: 13, color: "#666", marginBottom: 16 }}>
|
||||
设置所有知识库的通用回复规范
|
||||
</p>
|
||||
|
||||
<div className={style.promptToggle}>
|
||||
<span className={style.promptToggleLabel}>启用统一提示词</span>
|
||||
<Switch checked={enabled} onChange={setEnabled} />
|
||||
</div>
|
||||
|
||||
{enabled && (
|
||||
<textarea
|
||||
className={style.promptTextarea}
|
||||
value={content}
|
||||
onChange={e => setContent(e.target.value)}
|
||||
placeholder="请输入统一提示词..."
|
||||
maxLength={2000}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={style.promptSection}>
|
||||
<div className={style.sectionTitle}>
|
||||
<BulbOutlined className={style.sectionIcon} />
|
||||
统一提示词作用:
|
||||
</div>
|
||||
<div className={style.sectionContent}>
|
||||
<ul>
|
||||
<li>定义AI回复的基本风格和规范</li>
|
||||
<li>确保所有知识库回复的一致性和专业度</li>
|
||||
<li>与各知识库的回复基本合用</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style.warningBox}>
|
||||
<div className={style.warningTitle}>
|
||||
<ExclamationCircleOutlined /> 提示词生效逻辑:
|
||||
</div>
|
||||
<div className={style.warningText}>
|
||||
<div>统一提示词(全局规则) + 知识库独立提示词(专业指导)</div>
|
||||
<div style={{ marginTop: 4 }}>= 该终AI回复内容</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalPromptModal;
|
||||
@@ -0,0 +1,75 @@
|
||||
// AI知识库相关类型定义
|
||||
|
||||
// 知识库类型(对应接口的 type)
|
||||
export interface KnowledgeBase {
|
||||
id: number;
|
||||
type: number; // 类型
|
||||
name: string;
|
||||
description: string;
|
||||
label: string[]; // 标签(接口返回的字段名)
|
||||
prompt: string | null; // 独立提示词
|
||||
companyId: number;
|
||||
userId: number;
|
||||
createTime: string | null;
|
||||
updateTime: string | null;
|
||||
isDel: number;
|
||||
delTime: number;
|
||||
// 前端扩展字段
|
||||
tags?: string[]; // 兼容字段,映射自 label
|
||||
status?: number; // 0-禁用 1-启用(前端维护)
|
||||
materialCount?: number; // 素材总数(前端计算)
|
||||
useIndependentPrompt?: boolean; // 是否使用独立提示词(根据 prompt 判断)
|
||||
independentPrompt?: string; // 独立提示词内容(映射自 prompt)
|
||||
aiCallEnabled?: boolean; // AI调用配置(前端维护)
|
||||
creatorName?: string;
|
||||
callerCount?: number; // 调用者数量
|
||||
}
|
||||
|
||||
// 素材类型
|
||||
export interface Material {
|
||||
id: number;
|
||||
knowledgeBaseId: number;
|
||||
fileName: string;
|
||||
fileSize: number; // 字节
|
||||
fileType: string; // 文件扩展名
|
||||
filePath: string;
|
||||
tags: string[];
|
||||
uploadTime: string;
|
||||
uploaderId: number;
|
||||
uploaderName: string;
|
||||
}
|
||||
|
||||
// 调用者类型
|
||||
export interface Caller {
|
||||
id: number;
|
||||
name: string;
|
||||
avatar: string;
|
||||
role: string; // 角色/职位
|
||||
lastCallTime: string;
|
||||
callCount: number;
|
||||
}
|
||||
|
||||
// 统一提示词配置
|
||||
export interface GlobalPromptConfig {
|
||||
enabled: boolean; // 是否启用统一提示词
|
||||
content: string; // 提示词内容
|
||||
}
|
||||
|
||||
// 知识库列表响应
|
||||
export interface KnowledgeBaseListResponse {
|
||||
data: KnowledgeBase[]; // 接口实际返回的是 data 字段
|
||||
total: number;
|
||||
per_page: number;
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
}
|
||||
|
||||
// 新建/编辑知识库表单数据
|
||||
export interface KnowledgeBaseFormData {
|
||||
id?: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
tags: string[];
|
||||
useIndependentPrompt: boolean;
|
||||
independentPrompt?: string;
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
// 页面容器
|
||||
.knowledgePage {
|
||||
padding: 16px 10px 0 16px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
// 提示横幅
|
||||
.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;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 统计卡片区域
|
||||
.statsContainer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.statCard {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
text-align: center;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #1890ff;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.statLabel {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.statValueSuccess {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
// 知识库卡片
|
||||
.knowledgeCard {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid #ececec;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.knowledgeCard:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
border-color: #b3e5fc;
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.cardLeft {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.cardIcon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cardName {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cardDescription {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cardRight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cardSwitch {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.cardMenu {
|
||||
font-size: 18px;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.cardStats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px 0;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.statItem {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.statItemValue {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1890ff;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.statItemLabel {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.cardTags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 3px 10px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
background: rgba(24, 144, 255, 0.1);
|
||||
color: #1890ff;
|
||||
border: 1px solid rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
font-size: 64px;
|
||||
color: #d9d9d9;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: 15px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
// 新建知识库弹窗样式
|
||||
.modalContent {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modalTitle {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.modalSubtitle {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.formItem {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.formLabel {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.formInput {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:focus {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.formTextarea {
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.checkboxWrapper {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.checkboxLabel {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.promptHint {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
line-height: 1.5;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.modalFooter {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.modalButton {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.cancelButton {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
|
||||
&:active {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
}
|
||||
|
||||
.submitButton {
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
|
||||
&:active {
|
||||
background: #096dd9;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #d9d9d9;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// 统一提示词弹窗
|
||||
.promptModal {
|
||||
.promptContent {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.promptToggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.promptToggleLabel {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.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;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.promptSection {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.sectionIcon {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.sectionContent {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
padding: 10px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.warningBox {
|
||||
background: #fffbe6;
|
||||
border: 1px solid #ffe58f;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.warningTitle {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #d46b08;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.warningText {
|
||||
font-size: 12px;
|
||||
color: #ad6800;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
358
Cunkebao/src/pages/mobile/workspace/ai-knowledge/list/index.tsx
Normal file
358
Cunkebao/src/pages/mobile/workspace/ai-knowledge/list/index.tsx
Normal file
@@ -0,0 +1,358 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button, Switch, Dropdown, message, Spin, Pagination } from "antd";
|
||||
import {
|
||||
MoreOutlined,
|
||||
PlusOutlined,
|
||||
BookOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
GlobalOutlined,
|
||||
InfoCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import style from "./index.module.scss";
|
||||
import {
|
||||
fetchKnowledgeBaseList,
|
||||
deleteKnowledgeBase,
|
||||
initAIKnowledge,
|
||||
updateTypeStatus,
|
||||
} from "./api";
|
||||
import type { KnowledgeBase } from "./data";
|
||||
import GlobalPromptModal from "./components/GlobalPromptModal";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
const AIKnowledgeList: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [list, setList] = useState<KnowledgeBase[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [enabledCount, setEnabledCount] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [menuLoadingId, setMenuLoadingId] = useState<number | null>(null);
|
||||
|
||||
// 弹窗控制
|
||||
const [globalPromptVisible, setGlobalPromptVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 初始化AI功能
|
||||
initAIKnowledge().catch(err => {
|
||||
console.warn("初始化AI功能失败", err);
|
||||
});
|
||||
fetchList(1);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const fetchList = async (pageNum = 1) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetchKnowledgeBaseList({
|
||||
page: pageNum,
|
||||
limit: PAGE_SIZE,
|
||||
});
|
||||
// 转换数据格式,映射接口字段到前端字段
|
||||
const transformedList = (res?.data || []).map((item: any) => ({
|
||||
...item,
|
||||
tags: item.label || [],
|
||||
useIndependentPrompt: !!item.prompt,
|
||||
independentPrompt: item.prompt || "",
|
||||
status: item.isDel === 0 ? 1 : 0, // 未删除即为启用
|
||||
aiCallEnabled: true, // 默认启用
|
||||
materialCount: 0, // 需要单独统计
|
||||
}));
|
||||
setList(transformedList);
|
||||
setTotal(Number(res?.total) || 0);
|
||||
setEnabledCount(
|
||||
transformedList.filter((item: any) => item.status === 1).length,
|
||||
);
|
||||
} catch (e) {
|
||||
message.error("获取知识库列表失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageChange = (p: number) => {
|
||||
setPage(p);
|
||||
fetchList(p);
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchList(page);
|
||||
};
|
||||
|
||||
// 菜单点击事件
|
||||
const handleMenuClick = async (key: string, item: KnowledgeBase) => {
|
||||
// 系统预设不允许编辑或删除
|
||||
if (item.type === 0) {
|
||||
message.warning("系统预设知识库不可编辑或删除");
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "edit") {
|
||||
navigate(`/workspace/ai-knowledge/${item.id}/edit`);
|
||||
} else if (key === "delete") {
|
||||
setMenuLoadingId(item.id);
|
||||
try {
|
||||
await deleteKnowledgeBase(item.id);
|
||||
message.success("删除成功");
|
||||
handleRefresh();
|
||||
} catch (e) {
|
||||
message.error("删除失败");
|
||||
} finally {
|
||||
setMenuLoadingId(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Switch切换状态 - 乐观更新模式
|
||||
const handleSwitchChange = async (checked: boolean, item: KnowledgeBase) => {
|
||||
// 系统预设不允许修改状态
|
||||
if (item.type === 0) {
|
||||
message.warning("系统预设知识库不可修改状态");
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存旧状态用于回滚
|
||||
const oldStatus = item.status;
|
||||
const oldEnabledCount = enabledCount;
|
||||
|
||||
// 立即更新本地UI(乐观更新)
|
||||
setList(prevList =>
|
||||
prevList.map(kb =>
|
||||
kb.id === item.id ? { ...kb, status: checked ? 1 : 0 } : kb,
|
||||
),
|
||||
);
|
||||
setEnabledCount(prev => (checked ? prev + 1 : prev - 1));
|
||||
|
||||
// 异步请求接口
|
||||
try {
|
||||
await updateTypeStatus({ id: item.id, status: checked ? 1 : 0 });
|
||||
// 成功后显示提示
|
||||
message.success(checked ? "已启用" : "已禁用");
|
||||
} catch (e) {
|
||||
// 失败时回滚状态
|
||||
setList(prevList =>
|
||||
prevList.map(kb =>
|
||||
kb.id === item.id ? { ...kb, status: oldStatus } : kb,
|
||||
),
|
||||
);
|
||||
setEnabledCount(oldEnabledCount);
|
||||
message.error("操作失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
// 打开知识库详情
|
||||
const handleCardClick = (item: KnowledgeBase) => {
|
||||
navigate(`/workspace/ai-knowledge/${item.id}`);
|
||||
};
|
||||
|
||||
// 渲染知识库卡片
|
||||
const renderCard = (item: KnowledgeBase) => {
|
||||
const isSystemPreset = item.type === 0; // 系统预设不可编辑
|
||||
|
||||
return (
|
||||
<div key={item.id} className={style.knowledgeCard}>
|
||||
<div className={style.cardHeader}>
|
||||
<div
|
||||
className={style.cardLeft}
|
||||
onClick={() => handleCardClick(item)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<div className={style.cardTitle}>
|
||||
<div className={style.cardIcon}>
|
||||
<BookOutlined />
|
||||
</div>
|
||||
<div className={style.cardName}>
|
||||
{item.name}
|
||||
{isSystemPreset && (
|
||||
<span
|
||||
style={{
|
||||
marginLeft: "8px",
|
||||
fontSize: "12px",
|
||||
color: "#999",
|
||||
fontWeight: "normal",
|
||||
}}
|
||||
>
|
||||
(系统预设)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{item.description && (
|
||||
<div className={style.cardDescription}>{item.description}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={style.cardRight}>
|
||||
<Switch
|
||||
className={style.cardSwitch}
|
||||
checked={item.status === 1}
|
||||
size="small"
|
||||
loading={menuLoadingId === item.id}
|
||||
disabled={menuLoadingId === item.id || isSystemPreset}
|
||||
onChange={checked => handleSwitchChange(checked, item)}
|
||||
/>
|
||||
{!isSystemPreset && (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "edit",
|
||||
icon: <EditOutlined />,
|
||||
label: "编辑",
|
||||
disabled: menuLoadingId === item.id,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
icon: <DeleteOutlined />,
|
||||
label: "删除",
|
||||
disabled: menuLoadingId === item.id,
|
||||
danger: true,
|
||||
},
|
||||
],
|
||||
onClick: ({ key }) => handleMenuClick(key, item),
|
||||
}}
|
||||
trigger={["click"]}
|
||||
placement="bottomRight"
|
||||
disabled={menuLoadingId === item.id}
|
||||
>
|
||||
<MoreOutlined className={style.cardMenu} />
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style.cardStats}>
|
||||
<div className={style.statItem}>
|
||||
<div className={style.statItemValue}>{item.materialCount || 0}</div>
|
||||
<div className={style.statItemLabel}>素材总数</div>
|
||||
</div>
|
||||
<div className={style.statItem}>
|
||||
<div
|
||||
className={style.statItemValue}
|
||||
style={{ color: item.aiCallEnabled ? "#52c41a" : "#999" }}
|
||||
>
|
||||
{item.aiCallEnabled ? "启用" : "关闭"}
|
||||
</div>
|
||||
<div className={style.statItemLabel}>AI状态</div>
|
||||
</div>
|
||||
<div className={style.statItem}>
|
||||
<div className={style.statItemValue}>{item.tags?.length || 0}</div>
|
||||
<div className={style.statItemLabel}>标签数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{item.tags && item.tags.length > 0 && (
|
||||
<div className={style.cardTags}>
|
||||
{item.tags.map((tag, index) => (
|
||||
<span key={index} className={style.tag}>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavCommon
|
||||
title="AI知识库"
|
||||
backFn={() => navigate("/workspace")}
|
||||
right={
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<Button onClick={() => setGlobalPromptVisible(true)}>
|
||||
<GlobalOutlined /> 统一提示词
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => navigate("/workspace/ai-knowledge/new")}
|
||||
>
|
||||
<PlusOutlined /> 新建
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
<div
|
||||
style={{
|
||||
padding: "16px",
|
||||
background: "#fff",
|
||||
borderTop: "1px solid #f0f0f0",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Pagination
|
||||
current={page}
|
||||
pageSize={PAGE_SIZE}
|
||||
total={total}
|
||||
onChange={handlePageChange}
|
||||
showSizeChanger={false}
|
||||
showTotal={total => `共 ${total} 条`}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={style.knowledgePage}>
|
||||
{/* 提示横幅 */}
|
||||
<div className={style.banner}>
|
||||
<InfoCircleOutlined className={style.bannerIcon} />
|
||||
<div className={style.bannerContent}>
|
||||
<div className={style.bannerText}>
|
||||
已启用统一提示词规则 ·{" "}
|
||||
<a onClick={() => setGlobalPromptVisible(true)}>
|
||||
点击“统一提示词”可查看和编辑
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className={style.statsContainer}>
|
||||
<div className={style.statCard}>
|
||||
<div className={style.statValue}>{total}</div>
|
||||
<div className={style.statLabel}>内容库总数</div>
|
||||
</div>
|
||||
<div className={style.statCard}>
|
||||
<div className={`${style.statValue} ${style.statValueSuccess}`}>
|
||||
{enabledCount}
|
||||
</div>
|
||||
<div className={style.statLabel}>启用中</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 知识库列表 */}
|
||||
{loading ? (
|
||||
<div style={{ textAlign: "center", padding: "40px 0" }}>
|
||||
<Spin />
|
||||
</div>
|
||||
) : list.length > 0 ? (
|
||||
list.map(renderCard)
|
||||
) : (
|
||||
<div className={style.empty}>
|
||||
<div className={style.emptyIcon}>
|
||||
<BookOutlined />
|
||||
</div>
|
||||
<div className={style.emptyText}>暂无知识库</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 统一提示词弹窗 */}
|
||||
<GlobalPromptModal
|
||||
visible={globalPromptVisible}
|
||||
onClose={() => setGlobalPromptVisible(false)}
|
||||
/>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AIKnowledgeList;
|
||||
183
Cunkebao/src/pages/mobile/workspace/ai-knowledge/接口对接说明.md
Normal file
183
Cunkebao/src/pages/mobile/workspace/ai-knowledge/接口对接说明.md
Normal file
@@ -0,0 +1,183 @@
|
||||
# AI知识库接口对接说明
|
||||
|
||||
## ✅ 已对接接口
|
||||
|
||||
### 知识库类型管理
|
||||
|
||||
1. **获取知识库类型列表**
|
||||
- 接口:`GET /v1/knowledge/typeList`
|
||||
- 参数:`{ page, limit }`
|
||||
- 状态:✅ 已对接
|
||||
|
||||
2. **添加知识库类型**
|
||||
- 接口:`POST /v1/knowledge/addType`
|
||||
- 参数:`{ name, description, label, prompt }`
|
||||
- 状态:✅ 已对接
|
||||
|
||||
3. **编辑知识库类型**
|
||||
- 接口:`POST /v1/knowledge/editType`
|
||||
- 参数:`{ id, name, description, label, prompt }`
|
||||
- 状态:✅ 已对接
|
||||
|
||||
4. **删除知识库类型**
|
||||
- 接口:`DELETE /v1/knowledge/deleteType`
|
||||
- 参数:`{ id }`
|
||||
- 状态:✅ 已对接
|
||||
|
||||
### 知识库(素材)管理
|
||||
|
||||
5. **获取知识库列表(素材列表)**
|
||||
- 接口:`GET /v1/knowledge/getList`
|
||||
- 参数:`{ typeId, name, label, fileUrl, page, limit }`
|
||||
- 状态:✅ 已对接
|
||||
|
||||
6. **添加知识库(素材)**
|
||||
- 接口:`POST /v1/knowledge/add`
|
||||
- 参数:`{ typeId, name, label, fileUrl }`
|
||||
- 状态:✅ 已对接(需要补充文件上传接口)
|
||||
|
||||
7. **删除知识库(素材)**
|
||||
- 接口:`DELETE /v1/knowledge/delete`
|
||||
- 参数:`{ id }`
|
||||
- 状态:✅ 已对接
|
||||
|
||||
### AI功能
|
||||
|
||||
8. **初始化AI功能**
|
||||
- 接口:`GET /v1/knowledge/init`
|
||||
- 状态:✅ 已对接(页面加载时自动调用)
|
||||
|
||||
9. **发布并应用AI工具**
|
||||
- 接口:`GET /v1/knowledge/release`
|
||||
- 参数:`{ id }`
|
||||
- 状态:✅ 已对接(暂未在UI中使用,可在需要时调用)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 需要后端补充的接口
|
||||
|
||||
### 1. 知识库状态切换
|
||||
|
||||
- **功能**:启用/禁用知识库类型
|
||||
- **当前实现**:前端使用 `isDel` 字段判断状态,但没有单独的切换接口
|
||||
- **建议**:
|
||||
```
|
||||
POST /v1/knowledge/toggleStatus
|
||||
参数: { id: number, status: 0 | 1 }
|
||||
```
|
||||
|
||||
### 2. 统一提示词配置
|
||||
|
||||
- **功能**:配置全局AI提示词
|
||||
- **当前实现**:前端保留了UI,但接口未提供
|
||||
- **建议**:
|
||||
```
|
||||
GET /v1/knowledge/globalPrompt
|
||||
POST /v1/knowledge/globalPrompt
|
||||
参数: { enabled: boolean, content: string }
|
||||
```
|
||||
|
||||
### 3. 调用者列表
|
||||
|
||||
- **功能**:显示哪些用户在使用某个知识库
|
||||
- **当前实现**:前端已预留UI,但接口未提供
|
||||
- **建议**:
|
||||
```
|
||||
GET /v1/knowledge/callers
|
||||
参数: { typeId: number, page: number, limit: number }
|
||||
返回: { list: [], total: number }
|
||||
```
|
||||
|
||||
### 4. 文件上传
|
||||
|
||||
- **功能**:上传素材文件并返回 fileUrl
|
||||
- **当前实现**:前端使用临时占位方案
|
||||
- **建议**:
|
||||
```
|
||||
POST /v1/knowledge/uploadFile
|
||||
参数: FormData (file)
|
||||
返回: { fileUrl: string, fileSize: number }
|
||||
```
|
||||
|
||||
### 5. 知识库详情
|
||||
|
||||
- **功能**:获取单个知识库类型的详细信息
|
||||
- **当前实现**:前端通过列表接口查找
|
||||
- **建议**:
|
||||
```
|
||||
GET /v1/knowledge/typeDetail
|
||||
参数: { id: number }
|
||||
```
|
||||
|
||||
### 6. 素材统计
|
||||
|
||||
- **功能**:获取每个知识库类型的素材数量
|
||||
- **当前实现**:前端设置为0,需要单独统计
|
||||
- **建议**:在 `typeList` 接口返回值中增加 `materialCount` 字段
|
||||
|
||||
---
|
||||
|
||||
## 📋 数据字段映射说明
|
||||
|
||||
### 前端 ➡️ 后端
|
||||
|
||||
| 前端字段 | 后端字段 | 说明 |
|
||||
| -------------------- | ---------------- | ------------------------ |
|
||||
| tags | label | 标签数组 |
|
||||
| useIndependentPrompt | 根据 prompt 判断 | 是否有独立提示词 |
|
||||
| independentPrompt | prompt | 独立提示词内容 |
|
||||
| status | isDel 判断 | isDel=0 为启用 |
|
||||
| materialCount | 需补充 | 素材数量 |
|
||||
| fileName | name | 文件名 |
|
||||
| filePath | fileUrl | 文件路径 |
|
||||
| uploadTime | createTime | 上传时间(需转换时间戳) |
|
||||
|
||||
### 时间格式转换
|
||||
|
||||
- 后端返回:Unix 时间戳(秒)
|
||||
- 前端显示:`new Date(timestamp * 1000).toLocaleDateString('zh-CN')`
|
||||
|
||||
---
|
||||
|
||||
## 🔧 待优化项
|
||||
|
||||
1. **文件上传流程**
|
||||
- 当前:上传文件 → 获取URL → 调用添加接口
|
||||
- 建议:合并为一个接口,后端处理文件上传和记录创建
|
||||
|
||||
2. **批量操作**
|
||||
- 建议增加批量删除接口
|
||||
- 建议增加批量修改标签接口
|
||||
|
||||
3. **发布机制**
|
||||
- 明确发布接口的触发时机
|
||||
- 是否需要在新建/编辑后自动发布
|
||||
|
||||
4. **权限控制**
|
||||
- 建议增加知识库的访问权限配置
|
||||
- 建议增加素材的可见性控制
|
||||
|
||||
---
|
||||
|
||||
## ✨ 功能完成度
|
||||
|
||||
- ✅ 知识库类型的增删改查
|
||||
- ✅ 素材的增删查(改功能可后续补充)
|
||||
- ✅ 独立提示词配置
|
||||
- ⚠️ AI调用配置(UI完成,接口需补充)
|
||||
- ⚠️ 统一提示词(UI完成,接口需补充)
|
||||
- ⚠️ 调用者管理(UI预留,接口需补充)
|
||||
- ⚠️ 文件上传(临时方案,需正式接口)
|
||||
|
||||
---
|
||||
|
||||
## 📝 使用注意事项
|
||||
|
||||
1. 初始化AI功能会在页面加载时自动调用
|
||||
2. 文件上传当前使用临时方案,实际部署需要对接真实的文件上传接口
|
||||
3. 部分接口因后端未提供,使用了 `console.warn` 提示,不影响核心功能
|
||||
4. 知识库的启用/禁用状态暂时仅在前端维护
|
||||
|
||||
---
|
||||
|
||||
**最后更新时间**:2024-10-28
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
LinkOutlined,
|
||||
ClockCircleOutlined,
|
||||
ContactsOutlined,
|
||||
BookOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
||||
@@ -75,12 +76,26 @@ const Workspace: React.FC = () => {
|
||||
name: "通讯录导入",
|
||||
description: "批量导入通讯录联系人",
|
||||
icon: (
|
||||
<ContactsOutlined className={styles.icon} style={{ color: "#722ed1" }} />
|
||||
<ContactsOutlined
|
||||
className={styles.icon}
|
||||
style={{ color: "#722ed1" }}
|
||||
/>
|
||||
),
|
||||
path: "/workspace/contact-import/list",
|
||||
bgColor: "#f9f0ff",
|
||||
isNew: true,
|
||||
},
|
||||
{
|
||||
id: "ai-knowledge",
|
||||
name: "AI知识库",
|
||||
description: "管理和配置内容",
|
||||
icon: (
|
||||
<BookOutlined className={styles.icon} style={{ color: "#fa8c16" }} />
|
||||
),
|
||||
path: "/workspace/ai-knowledge",
|
||||
bgColor: "#fff7e6",
|
||||
isNew: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -20,6 +20,9 @@ import ContactImportForm from "@/pages/mobile/workspace/contact-import/form";
|
||||
import ContactImportDetail from "@/pages/mobile/workspace/contact-import/detail";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
import AiAnalyzer from "@/pages/mobile/workspace/ai-analyzer";
|
||||
import AIKnowledgeList from "@/pages/mobile/workspace/ai-knowledge/list";
|
||||
import AIKnowledgeDetail from "@/pages/mobile/workspace/ai-knowledge/detail";
|
||||
import AIKnowledgeForm from "@/pages/mobile/workspace/ai-knowledge/form";
|
||||
|
||||
const workspaceRoutes = [
|
||||
{
|
||||
@@ -178,6 +181,27 @@ const workspaceRoutes = [
|
||||
element: <ContactImportDetail />,
|
||||
auth: true,
|
||||
},
|
||||
// AI知识库
|
||||
{
|
||||
path: "/workspace/ai-knowledge",
|
||||
element: <AIKnowledgeList />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/workspace/ai-knowledge/new",
|
||||
element: <AIKnowledgeForm />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/workspace/ai-knowledge/:id",
|
||||
element: <AIKnowledgeDetail />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/workspace/ai-knowledge/:id/edit",
|
||||
element: <AIKnowledgeForm />,
|
||||
auth: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default workspaceRoutes;
|
||||
|
||||
Reference in New Issue
Block a user