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:
超级老白兔
2025-10-29 17:49:51 +08:00
parent 5638d8c66a
commit 6a64e3854c
18 changed files with 3635 additions and 145 deletions

View File

@@ -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>
);

View 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
传参:
{
idnumber
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
}

View 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",
);
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -0,0 +1,4 @@
// 表单页API - 复用列表页的接口
export { createKnowledgeBase, updateKnowledgeBase } from "../list/api";
export { getKnowledgeBaseDetail } from "../detail/api";

View File

@@ -0,0 +1,2 @@
// AI知识库表单相关类型定义
export type { KnowledgeBaseFormData } from "../list/data";

View File

@@ -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;
}

View 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;

View 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 });
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}
}

View 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)}>
&ldquo;&rdquo;
</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;

View 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

View File

@@ -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 (

View File

@@ -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;