page.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737
  1. "use client";
  2. import { useState, useEffect } from "react";
  3. import { useRouter } from "next/navigation";
  4. import { ResponsiveLayout } from "@/components/layout";
  5. import { Button } from "@/components/ui/button";
  6. import { Input } from "@/components/ui/input";
  7. import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
  8. import {
  9. fetchAIConfigs,
  10. createAIConfig,
  11. updateAIConfig,
  12. deleteAIConfig,
  13. type AIConfig,
  14. type CreateAIConfigRequest,
  15. type UpdateAIConfigRequest,
  16. } from "@/features/agent/services/aiConfigApi";
  17. import {
  18. fetchEmbeddingConfig,
  19. updateEmbeddingConfig,
  20. type EmbeddingConfig,
  21. type UpdateEmbeddingConfigRequest,
  22. } from "@/features/agent/services/embeddingConfigApi";
  23. import { useProfile } from "@/features/agent/hooks/useProfile";
  24. import { apiUrl } from "@/lib/config";
  25. import { Checkbox } from "@/components/ui/checkbox";
  26. import { Label } from "@/components/ui/label";
  27. import { toast } from "@/hooks/useToast";
  28. export default function SettingsPage(props: any = {}) {
  29. const { embedded = false } = props;
  30. const router = useRouter();
  31. const [userId, setUserId] = useState<number | null>(null);
  32. const [configs, setConfigs] = useState<AIConfig[]>([]);
  33. const [loading, setLoading] = useState(true);
  34. const [editingId, setEditingId] = useState<number | null>(null);
  35. const [formData, setFormData] = useState<CreateAIConfigRequest>({
  36. provider: "",
  37. api_url: "",
  38. api_key: "",
  39. model: "",
  40. model_type: "text",
  41. is_active: true,
  42. is_public: false,
  43. description: "",
  44. });
  45. const [submitting, setSubmitting] = useState(false);
  46. const [error, setError] = useState("");
  47. // 知识库向量配置(平台级,仅管理员可修改)
  48. const [embeddingConfig, setEmbeddingConfig] = useState<EmbeddingConfig | null>(null);
  49. const [embeddingForm, setEmbeddingForm] = useState({
  50. embedding_type: "openai",
  51. api_url: "",
  52. api_key: "",
  53. model: "text-embedding-3-small",
  54. customer_can_use_kb: true,
  55. visitor_web_search_enabled: false,
  56. web_search_source: "custom" as "vendor" | "custom",
  57. });
  58. const [embeddingLoading, setEmbeddingLoading] = useState(false);
  59. const [embeddingSubmitting, setEmbeddingSubmitting] = useState(false);
  60. const [embeddingError, setEmbeddingError] = useState("");
  61. // 检查登录状态
  62. useEffect(() => {
  63. const storedUserId = localStorage.getItem("agent_user_id");
  64. if (!storedUserId) {
  65. router.push("/");
  66. return;
  67. }
  68. setUserId(Number.parseInt(storedUserId, 10));
  69. }, [router]);
  70. // 加载个人资料(用于获取和更新 AI 对话接收设置)
  71. const {
  72. profile,
  73. loading: profileLoading,
  74. update: updateProfile,
  75. } = useProfile({
  76. userId: userId ?? null,
  77. enabled: Boolean(userId),
  78. });
  79. // 加载配置列表
  80. const loadConfigs = async () => {
  81. if (!userId) return;
  82. try {
  83. setLoading(true);
  84. const data = await fetchAIConfigs(userId);
  85. setConfigs(data);
  86. } catch (error) {
  87. console.error("加载配置失败:", error);
  88. setError("加载配置失败");
  89. } finally {
  90. setLoading(false);
  91. }
  92. };
  93. useEffect(() => {
  94. if (userId) {
  95. loadConfigs();
  96. }
  97. }, [userId]);
  98. // 加载知识库向量配置
  99. const loadEmbeddingConfig = async () => {
  100. if (!userId) return;
  101. try {
  102. setEmbeddingLoading(true);
  103. const data = await fetchEmbeddingConfig(userId);
  104. setEmbeddingConfig(data);
  105. setEmbeddingForm({
  106. embedding_type: data.embedding_type || "openai",
  107. api_url: data.api_url || "",
  108. api_key: "",
  109. model: data.model || "text-embedding-3-small",
  110. customer_can_use_kb: data.customer_can_use_kb ?? true,
  111. visitor_web_search_enabled: data.visitor_web_search_enabled ?? false,
  112. web_search_source: data.web_search_source === "vendor" ? "vendor" : "custom",
  113. });
  114. } catch (e) {
  115. console.error("加载知识库向量配置失败:", e);
  116. setEmbeddingError("加载失败");
  117. } finally {
  118. setEmbeddingLoading(false);
  119. }
  120. };
  121. useEffect(() => {
  122. if (userId) {
  123. loadEmbeddingConfig();
  124. }
  125. }, [userId]);
  126. // 保存知识库向量配置(仅管理员;保存后立即生效,无需重启)
  127. const handleSaveEmbeddingConfig = async (e: React.FormEvent) => {
  128. e.preventDefault();
  129. if (!userId) return;
  130. setEmbeddingSubmitting(true);
  131. setEmbeddingError("");
  132. try {
  133. const data: UpdateEmbeddingConfigRequest = {
  134. embedding_type: embeddingForm.embedding_type,
  135. api_url: embeddingForm.api_url || undefined,
  136. model: embeddingForm.model || undefined,
  137. customer_can_use_kb: embeddingForm.customer_can_use_kb,
  138. visitor_web_search_enabled: embeddingForm.visitor_web_search_enabled,
  139. web_search_source: embeddingForm.web_search_source,
  140. };
  141. if (embeddingForm.api_key) {
  142. data.api_key = embeddingForm.api_key;
  143. }
  144. await updateEmbeddingConfig(userId, data);
  145. await loadEmbeddingConfig();
  146. toast.success("保存成功,配置已立即生效。");
  147. } catch (err) {
  148. setEmbeddingError((err as Error).message);
  149. } finally {
  150. setEmbeddingSubmitting(false);
  151. }
  152. };
  153. // 重置表单
  154. const resetForm = () => {
  155. setFormData({
  156. provider: "",
  157. api_url: "",
  158. api_key: "",
  159. model: "",
  160. model_type: "text",
  161. is_active: true,
  162. is_public: false,
  163. description: "",
  164. });
  165. setEditingId(null);
  166. setError("");
  167. };
  168. // 开始编辑
  169. const handleEdit = (config: AIConfig) => {
  170. setFormData({
  171. provider: config.provider,
  172. api_url: config.api_url,
  173. api_key: "", // 不显示 API Key(已加密)
  174. model: config.model,
  175. model_type: config.model_type,
  176. is_active: config.is_active,
  177. is_public: config.is_public,
  178. description: config.description,
  179. });
  180. setEditingId(config.id);
  181. };
  182. // 提交表单
  183. const handleSubmit = async (e: React.FormEvent) => {
  184. e.preventDefault();
  185. if (!userId) return;
  186. setSubmitting(true);
  187. setError("");
  188. try {
  189. if (editingId) {
  190. // 更新配置
  191. const updateData: UpdateAIConfigRequest = {
  192. provider: formData.provider,
  193. api_url: formData.api_url,
  194. model: formData.model,
  195. model_type: formData.model_type,
  196. is_active: formData.is_active,
  197. is_public: formData.is_public,
  198. description: formData.description,
  199. };
  200. // 如果提供了新的 API Key,才更新
  201. if (formData.api_key) {
  202. updateData.api_key = formData.api_key;
  203. }
  204. await updateAIConfig(userId, editingId, updateData);
  205. } else {
  206. // 创建配置
  207. await createAIConfig(userId, formData);
  208. }
  209. resetForm();
  210. await loadConfigs();
  211. } catch (error) {
  212. setError((error as Error).message || "操作失败");
  213. } finally {
  214. setSubmitting(false);
  215. }
  216. };
  217. // 删除配置
  218. const handleDelete = async (id: number) => {
  219. if (!userId) return;
  220. if (!confirm("确定要删除这个配置吗?")) return;
  221. try {
  222. await deleteAIConfig(userId, id);
  223. await loadConfigs();
  224. } catch (error) {
  225. setError((error as Error).message || "删除失败");
  226. }
  227. };
  228. // 退出登录
  229. const handleLogout = async () => {
  230. try {
  231. await fetch(apiUrl("/logout"), { method: "POST" });
  232. } catch (error) {
  233. console.error("退出登录失败:", error);
  234. } finally {
  235. localStorage.removeItem("agent_user_id");
  236. localStorage.removeItem("agent_username");
  237. localStorage.removeItem("agent_role");
  238. router.push("/");
  239. }
  240. };
  241. if (!userId) {
  242. return null;
  243. }
  244. // 构建头部内容
  245. const headerContent = (
  246. <div className="bg-card border-b p-4 shadow-sm">
  247. <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
  248. <div>
  249. <h1 className="text-xl font-bold text-foreground">AI 配置管理</h1>
  250. <div className="text-sm text-muted-foreground mt-1">管理 AI 服务商配置</div>
  251. </div>
  252. {!embedded && (
  253. <div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
  254. <Button
  255. onClick={() => router.push("/agent/dashboard")}
  256. variant="outline"
  257. size="sm"
  258. className="w-full sm:w-auto"
  259. >
  260. 返回工作台
  261. </Button>
  262. <Button
  263. onClick={handleLogout}
  264. variant="outline"
  265. size="sm"
  266. className="w-full sm:w-auto"
  267. >
  268. 退出登录
  269. </Button>
  270. </div>
  271. )}
  272. </div>
  273. </div>
  274. );
  275. // 构建主内容区
  276. const mainContent = (
  277. <div className="flex-1 overflow-auto p-4 md:p-6">
  278. <div className="max-w-6xl mx-auto space-y-6">
  279. {/* 全局设置 */}
  280. <Card>
  281. <CardHeader>
  282. <CardTitle>全局设置</CardTitle>
  283. </CardHeader>
  284. <CardContent>
  285. <div className="flex items-center space-x-2">
  286. <Checkbox
  287. id="receive_ai_conversations"
  288. checked={!(profile?.receive_ai_conversations ?? false)}
  289. onCheckedChange={async (checked) => {
  290. if (userId) {
  291. try {
  292. await updateProfile({
  293. receive_ai_conversations: !checked,
  294. });
  295. } catch (error) {
  296. console.error("更新设置失败:", error);
  297. toast.error("更新设置失败,请重试");
  298. }
  299. }
  300. }}
  301. disabled={profileLoading}
  302. />
  303. <Label
  304. htmlFor="receive_ai_conversations"
  305. className="text-sm font-medium cursor-pointer"
  306. >
  307. 客服不接收 AI 对话
  308. </Label>
  309. </div>
  310. <p className="text-xs text-muted-foreground mt-2">
  311. 开启后,AI 对话将不会显示在对话列表中,也不会收到 AI 消息通知。
  312. 但您仍可以在会话页面手动开启&quot;显示 AI 消息&quot;来查看 AI 对话历史。
  313. </p>
  314. </CardContent>
  315. </Card>
  316. {/* 知识库向量模型(平台级,仅管理员可修改;保存后立即生效) */}
  317. <Card>
  318. <CardHeader>
  319. <CardTitle>知识库向量模型</CardTitle>
  320. <p className="text-sm text-muted-foreground mt-1">
  321. 用于知识库文档向量化与 RAG 检索。仅管理员可修改;保存后立即生效,无需重启。
  322. </p>
  323. </CardHeader>
  324. <CardContent>
  325. {embeddingLoading ? (
  326. <div className="text-center py-6 text-muted-foreground">加载中...</div>
  327. ) : (
  328. <form onSubmit={handleSaveEmbeddingConfig} className="space-y-4">
  329. {embeddingError && (
  330. <div className="p-3 bg-red-50 border border-red-200 rounded-md text-red-600 text-sm">
  331. {embeddingError}
  332. </div>
  333. )}
  334. <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
  335. <div>
  336. <Label className="block text-sm font-medium mb-1">类型</Label>
  337. <select
  338. value={embeddingForm.embedding_type}
  339. onChange={(e) =>
  340. setEmbeddingForm({ ...embeddingForm, embedding_type: e.target.value })
  341. }
  342. className="w-full px-3 py-2 border border-input rounded-md text-sm bg-background"
  343. >
  344. <option value="openai">OpenAI / 兼容 API</option>
  345. <option value="bge">BGE 本地</option>
  346. </select>
  347. </div>
  348. <div>
  349. <Label className="block text-sm font-medium mb-1">API 地址</Label>
  350. <Input
  351. value={embeddingForm.api_url}
  352. onChange={(e) =>
  353. setEmbeddingForm({ ...embeddingForm, api_url: e.target.value })
  354. }
  355. placeholder="https://api.openai.com/v1 或兼容地址"
  356. />
  357. </div>
  358. <div>
  359. <Label className="block text-sm font-medium mb-1">API Key</Label>
  360. <Input
  361. type="password"
  362. value={embeddingForm.api_key}
  363. onChange={(e) =>
  364. setEmbeddingForm({ ...embeddingForm, api_key: e.target.value })
  365. }
  366. placeholder={embeddingConfig?.api_key_masked ? "留空则不更新" : "输入 API Key"}
  367. />
  368. </div>
  369. <div>
  370. <Label className="block text-sm font-medium mb-1">模型</Label>
  371. <Input
  372. value={embeddingForm.model}
  373. onChange={(e) =>
  374. setEmbeddingForm({ ...embeddingForm, model: e.target.value })
  375. }
  376. placeholder="text-embedding-3-small"
  377. />
  378. </div>
  379. </div>
  380. <div className="flex items-center gap-2">
  381. <Checkbox
  382. id="customer_can_use_kb"
  383. checked={embeddingForm.customer_can_use_kb}
  384. onCheckedChange={(checked) =>
  385. setEmbeddingForm({
  386. ...embeddingForm,
  387. customer_can_use_kb: checked === true,
  388. })
  389. }
  390. />
  391. <Label htmlFor="customer_can_use_kb" className="text-sm cursor-pointer">
  392. 开放知识库给客服使用(允许创建知识库、上传文档、对话中引用)
  393. </Label>
  394. </div>
  395. <Button type="submit" disabled={embeddingSubmitting}>
  396. {embeddingSubmitting ? "保存中..." : "保存配置"}
  397. </Button>
  398. </form>
  399. )}
  400. </CardContent>
  401. </Card>
  402. {/* 联网搜索设置(与知识库向量模型独立;实际仍写入同一配置,仅 UI 分离) */}
  403. <Card>
  404. <CardHeader>
  405. <CardTitle>联网搜索设置</CardTitle>
  406. <p className="text-sm text-muted-foreground mt-1">
  407. 控制对话中的联网搜索方式与访客端是否显示联网选项。与上方「知识库向量模型」无关,仅影响 AI 对话时的联网行为。
  408. </p>
  409. </CardHeader>
  410. <CardContent>
  411. {embeddingLoading ? (
  412. <div className="text-center py-6 text-muted-foreground">加载中...</div>
  413. ) : (
  414. <form onSubmit={handleSaveEmbeddingConfig} className="space-y-4">
  415. {embeddingError && (
  416. <div className="p-3 bg-red-50 border border-red-200 rounded-md text-red-600 text-sm">
  417. {embeddingError}
  418. </div>
  419. )}
  420. <div>
  421. <Label className="block text-sm font-medium mb-1">联网方式</Label>
  422. <select
  423. value={embeddingForm.web_search_source}
  424. onChange={(e) =>
  425. setEmbeddingForm({
  426. ...embeddingForm,
  427. web_search_source: e.target.value as "vendor" | "custom",
  428. })
  429. }
  430. className="w-full max-w-xs px-3 py-2 border border-input rounded-md text-sm bg-background"
  431. >
  432. <option value="custom">自建(Serper)</option>
  433. <option value="vendor">厂商内置</option>
  434. </select>
  435. <p className="text-xs text-muted-foreground mt-1">
  436. 自建:由后端通过 Serper(MCP 或 HTTP)执行;厂商内置:使用当前对话所用 AI 厂商自带的 web search,不占用 Serper。
  437. </p>
  438. </div>
  439. <div className="flex items-center gap-2">
  440. <Checkbox
  441. id="visitor_web_search_enabled_standalone"
  442. checked={embeddingForm.visitor_web_search_enabled}
  443. onCheckedChange={(checked) =>
  444. setEmbeddingForm({
  445. ...embeddingForm,
  446. visitor_web_search_enabled: checked === true,
  447. })
  448. }
  449. />
  450. <Label htmlFor="visitor_web_search_enabled_standalone" className="text-sm cursor-pointer">
  451. 访客小窗显示「本回合联网搜索」选项
  452. </Label>
  453. </div>
  454. <Button type="submit" disabled={embeddingSubmitting}>
  455. {embeddingSubmitting ? "保存中..." : "保存联网设置"}
  456. </Button>
  457. </form>
  458. )}
  459. </CardContent>
  460. </Card>
  461. {/* 配置表单 */}
  462. <Card>
  463. <CardHeader>
  464. <CardTitle>
  465. {editingId ? "编辑 AI 配置" : "添加 AI 配置"}
  466. </CardTitle>
  467. </CardHeader>
  468. <CardContent>
  469. <form onSubmit={handleSubmit} className="space-y-4">
  470. {error && (
  471. <div className="p-3 bg-red-50 border border-red-200 rounded-md text-red-600 text-sm">
  472. {error}
  473. </div>
  474. )}
  475. <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
  476. <div>
  477. <label className="block text-sm font-medium mb-1">
  478. 服务商名称 <span className="text-red-500">*</span>
  479. </label>
  480. <Input
  481. value={formData.provider}
  482. onChange={(e) =>
  483. setFormData({ ...formData, provider: e.target.value })
  484. }
  485. placeholder="例如:OpenAI、Claude、自定义"
  486. required
  487. />
  488. </div>
  489. <div>
  490. <label className="block text-sm font-medium mb-1">
  491. API 地址 <span className="text-red-500">*</span>
  492. </label>
  493. <Input
  494. value={formData.api_url}
  495. onChange={(e) =>
  496. setFormData({ ...formData, api_url: e.target.value })
  497. }
  498. placeholder="https://api.openai.com/v1/chat/completions"
  499. required
  500. />
  501. </div>
  502. <div>
  503. <label className="block text-sm font-medium mb-1">
  504. API Key <span className="text-red-500">*</span>
  505. </label>
  506. <Input
  507. type="password"
  508. value={formData.api_key}
  509. onChange={(e) =>
  510. setFormData({ ...formData, api_key: e.target.value })
  511. }
  512. placeholder={editingId ? "留空则不更新" : "输入 API Key"}
  513. required={!editingId}
  514. />
  515. </div>
  516. <div>
  517. <label className="block text-sm font-medium mb-1">
  518. 模型名称 <span className="text-red-500">*</span>
  519. </label>
  520. <Input
  521. value={formData.model}
  522. onChange={(e) =>
  523. setFormData({ ...formData, model: e.target.value })
  524. }
  525. placeholder="例如:gpt-3.5-turbo、gpt-4"
  526. required
  527. />
  528. </div>
  529. <div>
  530. <label className="block text-sm font-medium mb-1">
  531. 模型类型
  532. </label>
  533. <select
  534. value={formData.model_type}
  535. onChange={(e) =>
  536. setFormData({ ...formData, model_type: e.target.value })
  537. }
  538. className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-primary"
  539. >
  540. <option value="text">文本</option>
  541. <option value="image">图片</option>
  542. <option value="audio">语音</option>
  543. <option value="video">视频</option>
  544. </select>
  545. </div>
  546. </div>
  547. <div>
  548. <label className="block text-sm font-medium mb-1">
  549. 配置描述
  550. </label>
  551. <Input
  552. value={formData.description}
  553. onChange={(e) =>
  554. setFormData({ ...formData, description: e.target.value })
  555. }
  556. placeholder="例如:OpenAI GPT-3.5 Turbo 模型"
  557. />
  558. </div>
  559. <div className="flex items-center gap-4">
  560. <label className="flex items-center gap-2">
  561. <input
  562. type="checkbox"
  563. checked={formData.is_active}
  564. onChange={(e) =>
  565. setFormData({ ...formData, is_active: e.target.checked })
  566. }
  567. className="w-4 h-4"
  568. />
  569. <span className="text-sm">启用配置</span>
  570. </label>
  571. <label className="flex items-center gap-2">
  572. <input
  573. type="checkbox"
  574. checked={formData.is_public}
  575. onChange={(e) =>
  576. setFormData({ ...formData, is_public: e.target.checked })
  577. }
  578. className="w-4 h-4"
  579. />
  580. <span className="text-sm">开放给访客使用</span>
  581. </label>
  582. </div>
  583. <div className="flex gap-2">
  584. <Button type="submit" disabled={submitting}>
  585. {submitting
  586. ? "提交中..."
  587. : editingId
  588. ? "更新配置"
  589. : "创建配置"}
  590. </Button>
  591. {editingId && (
  592. <Button
  593. type="button"
  594. variant="outline"
  595. onClick={resetForm}
  596. >
  597. 取消
  598. </Button>
  599. )}
  600. </div>
  601. </form>
  602. </CardContent>
  603. </Card>
  604. {/* 配置列表 */}
  605. <Card>
  606. <CardHeader>
  607. <CardTitle>已配置的 AI 服务</CardTitle>
  608. </CardHeader>
  609. <CardContent>
  610. {loading ? (
  611. <div className="text-center py-8 text-gray-500">
  612. 加载中...
  613. </div>
  614. ) : configs.length === 0 ? (
  615. <div className="text-center py-8 text-gray-500">
  616. 暂无配置,请添加
  617. </div>
  618. ) : (
  619. <div className="space-y-4">
  620. {configs.map((config) => (
  621. <div
  622. key={config.id}
  623. className="p-4 border rounded-lg hover:shadow-md transition-shadow"
  624. >
  625. <div className="flex justify-between items-start">
  626. <div className="flex-1">
  627. <div className="flex items-center gap-2 mb-2">
  628. <h3 className="font-semibold">
  629. {config.provider} - {config.model}
  630. </h3>
  631. {config.is_active && (
  632. <span className="px-2 py-1 text-xs bg-green-100 text-green-800 rounded">
  633. 启用
  634. </span>
  635. )}
  636. {config.is_public && (
  637. <span className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded">
  638. 开放
  639. </span>
  640. )}
  641. </div>
  642. <div className="text-sm text-gray-600 space-y-1">
  643. <p>
  644. <span className="font-medium">API 地址:</span>
  645. {config.api_url}
  646. </p>
  647. <p>
  648. <span className="font-medium">模型类型:</span>
  649. {config.model_type}
  650. </p>
  651. {config.description && (
  652. <p>
  653. <span className="font-medium">描述:</span>
  654. {config.description}
  655. </p>
  656. )}
  657. </div>
  658. </div>
  659. <div className="flex gap-2">
  660. <Button
  661. size="sm"
  662. variant="outline"
  663. onClick={() => handleEdit(config)}
  664. >
  665. 编辑
  666. </Button>
  667. <Button
  668. size="sm"
  669. variant="destructive"
  670. onClick={() => handleDelete(config.id)}
  671. >
  672. 删除
  673. </Button>
  674. </div>
  675. </div>
  676. </div>
  677. ))}
  678. </div>
  679. )}
  680. </CardContent>
  681. </Card>
  682. </div>
  683. </div>
  684. );
  685. // 如果是嵌入模式,只返回内容,不包含 ResponsiveLayout
  686. if (embedded) {
  687. return (
  688. <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
  689. {headerContent}
  690. {mainContent}
  691. </div>
  692. );
  693. }
  694. return (
  695. <ResponsiveLayout
  696. main={mainContent}
  697. header={headerContent}
  698. />
  699. );
  700. }