page.tsx 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818
  1. "use client";
  2. import { useCallback, useEffect, useState } from "react";
  3. import { useRouter } from "next/navigation";
  4. import { useAuth } from "@/features/agent/hooks/useAuth";
  5. import { ResponsiveLayout } from "@/components/layout";
  6. import {
  7. fetchUsers,
  8. createUser,
  9. updateUser,
  10. deleteUser,
  11. updateUserPassword,
  12. type UserSummary,
  13. type CreateUserRequest,
  14. type UpdateUserRequest,
  15. type UpdatePasswordRequest,
  16. } from "@/features/agent/services/userApi";
  17. import { Button } from "@/components/ui/button";
  18. import { Input } from "@/components/ui/input";
  19. import {
  20. Dialog,
  21. DialogContent,
  22. DialogHeader,
  23. DialogTitle,
  24. } from "@/components/ui/dialog";
  25. import { Badge } from "@/components/ui/badge";
  26. import { Card } from "@/components/ui/card";
  27. import { Label } from "@/components/ui/label";
  28. import { toast } from "@/hooks/useToast";
  29. import {
  30. PERMISSION_OPTIONS,
  31. defaultAgentPermissions,
  32. type PermissionKey,
  33. } from "@/lib/constants/agent-permissions";
  34. import type { I18nKey } from "@/lib/i18n/dict";
  35. import { useI18n } from "@/lib/i18n/provider";
  36. import {
  37. Edit,
  38. Trash2,
  39. Lock,
  40. Search,
  41. UserPlus,
  42. } from "lucide-react";
  43. const PERM_LABEL: Record<PermissionKey, I18nKey> = {
  44. chat: "agent.perm.chat",
  45. kb_test: "agent.perm.kb_test",
  46. knowledge: "agent.perm.knowledge",
  47. faqs: "agent.perm.faqs",
  48. analytics: "agent.perm.analytics",
  49. logs: "agent.perm.logs",
  50. prompts: "agent.perm.prompts",
  51. settings: "agent.perm.settings",
  52. users: "agent.perm.users",
  53. };
  54. export default function UsersPage(props: any = {}) {
  55. const { embedded = false } = props;
  56. const router = useRouter();
  57. const { agent } = useAuth();
  58. const { t, lang } = useI18n();
  59. const tr = (key: I18nKey, vars?: Record<string, string>) => {
  60. let s = t(key);
  61. if (!vars) return s;
  62. for (const k of Object.keys(vars)) {
  63. s = s.replaceAll(`{{${k}}}`, vars[k] ?? "");
  64. }
  65. return s;
  66. };
  67. const [users, setUsers] = useState<UserSummary[]>([]);
  68. const [loading, setLoading] = useState(true);
  69. const [searchQuery, setSearchQuery] = useState("");
  70. const [createDialogOpen, setCreateDialogOpen] = useState(false);
  71. const [editDialogOpen, setEditDialogOpen] = useState(false);
  72. const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
  73. const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
  74. const [selectedUser, setSelectedUser] = useState<UserSummary | null>(null);
  75. const [submitting, setSubmitting] = useState(false);
  76. // 创建用户表单
  77. const [createForm, setCreateForm] = useState<CreateUserRequest>({
  78. username: "",
  79. password: "",
  80. role: "agent",
  81. permissions: defaultAgentPermissions(),
  82. nickname: "",
  83. email: "",
  84. });
  85. // 编辑用户表单
  86. const [editForm, setEditForm] = useState<UpdateUserRequest>({
  87. role: "agent",
  88. permissions: defaultAgentPermissions(),
  89. nickname: "",
  90. email: "",
  91. receive_ai_conversations: true,
  92. });
  93. // 修改密码表单
  94. const [passwordForm, setPasswordForm] = useState<UpdatePasswordRequest>({
  95. old_password: "",
  96. new_password: "",
  97. });
  98. // 检查权限
  99. useEffect(() => {
  100. if (agent && agent.role !== "admin") {
  101. router.push("/agent/dashboard");
  102. }
  103. }, [agent, router]);
  104. // 加载用户列表
  105. const loadUsers = useCallback(async () => {
  106. if (!agent?.id) {
  107. return;
  108. }
  109. setLoading(true);
  110. try {
  111. const data = await fetchUsers(agent.id);
  112. setUsers(data);
  113. } catch (error) {
  114. console.error("加载用户列表失败:", error);
  115. toast.error((error as Error).message || t("agent.users.toast.loadFailed"));
  116. } finally {
  117. setLoading(false);
  118. }
  119. }, [agent?.id, t]);
  120. // 初始加载
  121. useEffect(() => {
  122. loadUsers();
  123. }, [loadUsers]);
  124. // 过滤用户列表
  125. const filteredUsers = users.filter((user) => {
  126. if (!searchQuery.trim()) {
  127. return true;
  128. }
  129. const query = searchQuery.toLowerCase();
  130. return (
  131. user.username.toLowerCase().includes(query) ||
  132. (user.nickname && user.nickname.toLowerCase().includes(query)) ||
  133. (user.email && user.email.toLowerCase().includes(query))
  134. );
  135. });
  136. // 打开创建对话框
  137. const handleOpenCreate = () => {
  138. setCreateForm({
  139. username: "",
  140. password: "",
  141. role: "agent",
  142. permissions: defaultAgentPermissions(),
  143. nickname: "",
  144. email: "",
  145. });
  146. setCreateDialogOpen(true);
  147. };
  148. // 创建用户
  149. const handleCreate = async () => {
  150. if (!agent?.id) {
  151. return;
  152. }
  153. if (!createForm.username.trim() || !createForm.password.trim()) {
  154. toast.error(t("agent.users.toast.usernamePasswordRequired"));
  155. return;
  156. }
  157. setSubmitting(true);
  158. try {
  159. await createUser(createForm, agent.id);
  160. setCreateDialogOpen(false);
  161. await loadUsers();
  162. toast.success(t("agent.users.toast.createSuccess"));
  163. } catch (error) {
  164. toast.error((error as Error).message || t("agent.users.toast.createFailed"));
  165. } finally {
  166. setSubmitting(false);
  167. }
  168. };
  169. // 打开编辑对话框
  170. const handleOpenEdit = (user: UserSummary) => {
  171. setSelectedUser(user);
  172. setEditForm({
  173. role: user.role as "admin" | "agent",
  174. permissions:
  175. user.role === "admin"
  176. ? PERMISSION_OPTIONS.map((p) => p.key)
  177. : ((user.permissions as PermissionKey[] | undefined) ??
  178. defaultAgentPermissions()),
  179. nickname: user.nickname || "",
  180. email: user.email || "",
  181. receive_ai_conversations: user.receive_ai_conversations,
  182. });
  183. setEditDialogOpen(true);
  184. };
  185. // 更新用户
  186. const handleUpdate = async () => {
  187. if (!agent?.id || !selectedUser) {
  188. return;
  189. }
  190. setSubmitting(true);
  191. try {
  192. await updateUser(selectedUser.id, editForm, agent.id);
  193. setEditDialogOpen(false);
  194. setSelectedUser(null);
  195. await loadUsers();
  196. toast.success(t("agent.users.toast.updateSuccess"));
  197. } catch (error) {
  198. toast.error((error as Error).message || t("agent.users.toast.updateFailed"));
  199. } finally {
  200. setSubmitting(false);
  201. }
  202. };
  203. // 打开修改密码对话框
  204. const handleOpenPassword = (user: UserSummary) => {
  205. if (user.role === "admin") {
  206. toast.error(t("agent.users.toast.adminPasswordDisabled"));
  207. return;
  208. }
  209. setSelectedUser(user);
  210. setPasswordForm({
  211. old_password: "",
  212. new_password: "",
  213. });
  214. setPasswordDialogOpen(true);
  215. };
  216. // 更新密码
  217. const handleUpdatePassword = async () => {
  218. if (!agent?.id || !selectedUser) {
  219. return;
  220. }
  221. if (!passwordForm.new_password.trim()) {
  222. toast.error(t("agent.users.toast.newPasswordRequired"));
  223. return;
  224. }
  225. // 如果修改的是当前用户,需要旧密码;如果是其他用户,不需要旧密码
  226. const isCurrentUser = selectedUser.id === agent.id;
  227. if (isCurrentUser && !passwordForm.old_password?.trim()) {
  228. toast.error(t("agent.users.toast.oldPasswordRequired"));
  229. return;
  230. }
  231. setSubmitting(true);
  232. try {
  233. await updateUserPassword(
  234. selectedUser.id,
  235. isCurrentUser ? passwordForm : { new_password: passwordForm.new_password },
  236. agent.id
  237. );
  238. setPasswordDialogOpen(false);
  239. setSelectedUser(null);
  240. setPasswordForm({ old_password: "", new_password: "" });
  241. toast.success(t("agent.users.toast.passwordSuccess"));
  242. } catch (error) {
  243. toast.error((error as Error).message || t("agent.users.toast.passwordFailed"));
  244. } finally {
  245. setSubmitting(false);
  246. }
  247. };
  248. // 打开删除对话框
  249. const handleOpenDelete = (user: UserSummary) => {
  250. if (user.role === "admin") {
  251. toast.error(t("agent.users.toast.adminDeleteDisabled"));
  252. return;
  253. }
  254. setSelectedUser(user);
  255. setDeleteDialogOpen(true);
  256. };
  257. // 删除用户
  258. const handleDelete = async () => {
  259. if (!agent?.id || !selectedUser) {
  260. return;
  261. }
  262. setSubmitting(true);
  263. try {
  264. const result = await deleteUser(selectedUser.id, agent.id);
  265. setDeleteDialogOpen(false);
  266. setSelectedUser(null);
  267. await loadUsers();
  268. if (result.transferredAIConfigs > 0) {
  269. toast.success(
  270. tr("agent.users.toast.deleteTransferred", {
  271. count: String(result.transferredAIConfigs),
  272. })
  273. );
  274. } else {
  275. toast.success(t("agent.users.toast.deleteSuccess"));
  276. }
  277. } catch (error) {
  278. toast.error((error as Error).message || t("agent.users.toast.deleteFailed"));
  279. } finally {
  280. setSubmitting(false);
  281. }
  282. };
  283. // 格式化时间
  284. const formatTime = (dateStr: string) => {
  285. const date = new Date(dateStr);
  286. return date.toLocaleString(lang === "en" ? "en-US" : "zh-CN", {
  287. year: "numeric",
  288. month: "2-digit",
  289. day: "2-digit",
  290. hour: "2-digit",
  291. minute: "2-digit",
  292. });
  293. };
  294. if (!agent || agent.role !== "admin") {
  295. return null; // 或者显示"权限不足"页面
  296. }
  297. // 构建头部内容
  298. const headerContent = (
  299. <div className="border-b bg-card p-3 shadow-sm sm:p-4">
  300. <div className="flex items-center justify-between mb-4">
  301. <h1 className="text-xl font-bold text-foreground">{t("agent.users.title")}</h1>
  302. {!embedded && (
  303. <Button
  304. variant="ghost"
  305. size="sm"
  306. onClick={() => router.push("/agent/dashboard")}
  307. >
  308. {t("agent.common.back")}
  309. </Button>
  310. )}
  311. </div>
  312. {/* 搜索和操作栏 */}
  313. <div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
  314. <div className="flex-1 relative">
  315. <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
  316. <Input
  317. type="text"
  318. placeholder={t("agent.users.search.placeholder")}
  319. value={searchQuery}
  320. onChange={(e) => setSearchQuery(e.target.value)}
  321. className="pl-10"
  322. />
  323. </div>
  324. <Button
  325. onClick={handleOpenCreate}
  326. className="w-full sm:w-auto"
  327. >
  328. <UserPlus className="w-4 h-4 mr-2" />
  329. {t("agent.users.createButton")}
  330. </Button>
  331. </div>
  332. </div>
  333. );
  334. // 构建主内容区
  335. const mainContent = (
  336. <div className="scrollbar-auto flex-1 overflow-y-auto p-3 sm:p-4">
  337. {loading ? (
  338. <div className="flex items-center justify-center h-full">
  339. <span className="text-muted-foreground">{t("common.loading")}</span>
  340. </div>
  341. ) : filteredUsers.length === 0 ? (
  342. <div className="flex items-center justify-center h-full">
  343. <span className="text-muted-foreground">
  344. {searchQuery ? t("agent.users.empty.filtered") : t("agent.users.empty")}
  345. </span>
  346. </div>
  347. ) : (
  348. <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  349. {filteredUsers.map((user) => (
  350. <Card key={user.id} className="p-4 flex flex-col">
  351. <div className="mb-3 flex-1">
  352. <div className="flex items-center gap-2 mb-2">
  353. <span className="font-medium text-foreground">
  354. {user.nickname || user.username}
  355. </span>
  356. <Badge
  357. variant={user.role === "admin" ? "default" : "secondary"}
  358. >
  359. {user.role === "admin"
  360. ? t("agent.users.role.admin")
  361. : t("agent.users.role.agent")}
  362. </Badge>
  363. </div>
  364. <div className="text-sm text-muted-foreground space-y-1 mb-2">
  365. <div>
  366. {t("agent.users.field.username")}: {user.username}
  367. </div>
  368. {user.email && (
  369. <div>
  370. {t("agent.users.field.email")}: {user.email}
  371. </div>
  372. )}
  373. </div>
  374. <div className="text-xs text-muted-foreground">
  375. {t("agent.users.field.createdAt")}: {formatTime(user.created_at)}
  376. </div>
  377. </div>
  378. <div className="flex items-center gap-2 mt-auto pt-3 border-t">
  379. <Button
  380. variant="outline"
  381. size="sm"
  382. onClick={() => handleOpenEdit(user)}
  383. className="flex-1"
  384. >
  385. <Edit className="w-4 h-4 mr-1" />
  386. {t("agent.users.card.edit")}
  387. </Button>
  388. <Button
  389. variant="outline"
  390. size="sm"
  391. onClick={() => handleOpenPassword(user)}
  392. className="flex-1"
  393. disabled={user.role === "admin"}
  394. title={
  395. user.role === "admin"
  396. ? t("agent.users.tooltip.adminPasswordDbOnly")
  397. : ""
  398. }
  399. >
  400. <Lock className="w-4 h-4 mr-1" />
  401. {t("agent.users.card.password")}
  402. </Button>
  403. <Button
  404. variant="destructive"
  405. size="sm"
  406. onClick={() => handleOpenDelete(user)}
  407. disabled={user.id === agent.id || user.role === "admin"}
  408. title={
  409. user.role === "admin"
  410. ? t("agent.users.tooltip.adminDeleteDbOnly")
  411. : user.id === agent.id
  412. ? t("agent.users.tooltip.cannotDeleteSelf")
  413. : ""
  414. }
  415. >
  416. <Trash2 className="w-4 h-4" />
  417. </Button>
  418. </div>
  419. </Card>
  420. ))}
  421. </div>
  422. )}
  423. </div>
  424. );
  425. const userDialogs = (
  426. <>
  427. {/* 创建用户对话框 */}
  428. <Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
  429. <DialogContent>
  430. <DialogHeader>
  431. <DialogTitle>{t("agent.users.dialog.createTitle")}</DialogTitle>
  432. </DialogHeader>
  433. <div className="space-y-4">
  434. <div>
  435. <Label htmlFor="create-username">{t("agent.users.form.username")} *</Label>
  436. <Input
  437. id="create-username"
  438. value={createForm.username}
  439. onChange={(e) =>
  440. setCreateForm({ ...createForm, username: e.target.value })
  441. }
  442. placeholder={t("agent.users.placeholder.username")}
  443. />
  444. </div>
  445. <div>
  446. <Label htmlFor="create-password">{t("agent.users.form.password")} *</Label>
  447. <Input
  448. id="create-password"
  449. type="password"
  450. value={createForm.password}
  451. onChange={(e) =>
  452. setCreateForm({ ...createForm, password: e.target.value })
  453. }
  454. placeholder={t("agent.users.placeholder.password")}
  455. />
  456. </div>
  457. <div>
  458. <Label htmlFor="create-role">{t("agent.users.form.role")} *</Label>
  459. <select
  460. id="create-role"
  461. value={createForm.role}
  462. onChange={(e) =>
  463. setCreateForm({
  464. ...createForm,
  465. role: e.target.value as "admin" | "agent",
  466. permissions:
  467. e.target.value === "admin"
  468. ? PERMISSION_OPTIONS.map((p) => p.key)
  469. : defaultAgentPermissions(),
  470. })
  471. }
  472. className="w-full px-3 py-2 border border-border rounded-md bg-background"
  473. >
  474. <option value="agent">{t("agent.users.role.agent")}</option>
  475. <option value="admin">{t("agent.users.role.admin")}</option>
  476. </select>
  477. </div>
  478. {/* 功能权限(开/关一级;role=admin 默认全开) */}
  479. {createForm.role !== "admin" && (
  480. <div>
  481. <Label>{t("agent.users.form.permissions")}</Label>
  482. <div className="mt-2 grid grid-cols-2 gap-2">
  483. {PERMISSION_OPTIONS.map((p) => {
  484. const checked = (createForm.permissions ?? []).includes(p.key);
  485. return (
  486. <label
  487. key={p.key}
  488. className="flex items-center gap-2 rounded-md border border-border/70 bg-background px-3 py-2 text-sm"
  489. >
  490. <input
  491. type="checkbox"
  492. checked={checked}
  493. onChange={(e) => {
  494. const next = new Set(createForm.permissions ?? []);
  495. if (e.target.checked) next.add(p.key);
  496. else next.delete(p.key);
  497. setCreateForm({ ...createForm, permissions: Array.from(next) });
  498. }}
  499. />
  500. <span>{t(PERM_LABEL[p.key])}</span>
  501. </label>
  502. );
  503. })}
  504. </div>
  505. <p className="mt-1 text-xs text-muted-foreground">
  506. {t("agent.users.form.permissionsHint")}
  507. </p>
  508. </div>
  509. )}
  510. <div>
  511. <Label htmlFor="create-nickname">{t("agent.users.form.nickname")}</Label>
  512. <Input
  513. id="create-nickname"
  514. value={createForm.nickname}
  515. onChange={(e) =>
  516. setCreateForm({ ...createForm, nickname: e.target.value })
  517. }
  518. placeholder={t("agent.users.placeholder.nicknameOptional")}
  519. />
  520. </div>
  521. <div>
  522. <Label htmlFor="create-email">{t("agent.users.form.email")}</Label>
  523. <Input
  524. id="create-email"
  525. type="email"
  526. value={createForm.email}
  527. onChange={(e) =>
  528. setCreateForm({ ...createForm, email: e.target.value })
  529. }
  530. placeholder={t("agent.users.placeholder.emailOptional")}
  531. />
  532. </div>
  533. <div className="flex justify-end gap-2">
  534. <Button
  535. variant="outline"
  536. onClick={() => setCreateDialogOpen(false)}
  537. disabled={submitting}
  538. >
  539. {t("agent.common.cancel")}
  540. </Button>
  541. <Button onClick={handleCreate} disabled={submitting}>
  542. {submitting ? t("agent.users.submit.creating") : t("agent.common.create")}
  543. </Button>
  544. </div>
  545. </div>
  546. </DialogContent>
  547. </Dialog>
  548. {/* 编辑用户对话框 */}
  549. <Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
  550. <DialogContent>
  551. <DialogHeader>
  552. <DialogTitle>{t("agent.users.dialog.editTitle")}</DialogTitle>
  553. </DialogHeader>
  554. {selectedUser && (
  555. <div className="space-y-4">
  556. <div>
  557. <Label>{t("agent.users.field.username")}</Label>
  558. <Input value={selectedUser.username} disabled />
  559. <p className="text-xs text-muted-foreground mt-1">
  560. {t("agent.users.usernameImmutableHint")}
  561. </p>
  562. </div>
  563. <div>
  564. <Label htmlFor="edit-role">{t("agent.users.form.role")} *</Label>
  565. <select
  566. id="edit-role"
  567. value={editForm.role}
  568. onChange={(e) =>
  569. setEditForm({
  570. ...editForm,
  571. role: e.target.value as "admin" | "agent",
  572. permissions:
  573. e.target.value === "admin"
  574. ? PERMISSION_OPTIONS.map((p) => p.key)
  575. : defaultAgentPermissions(),
  576. })
  577. }
  578. className="w-full px-3 py-2 border border-border rounded-md bg-background"
  579. >
  580. <option value="agent">{t("agent.users.role.agent")}</option>
  581. <option value="admin">{t("agent.users.role.admin")}</option>
  582. </select>
  583. </div>
  584. {editForm.role !== "admin" && (
  585. <div>
  586. <Label>{t("agent.users.form.permissions")}</Label>
  587. <div className="mt-2 grid grid-cols-2 gap-2">
  588. {PERMISSION_OPTIONS.map((p) => {
  589. const checked = (editForm.permissions ?? []).includes(p.key);
  590. return (
  591. <label
  592. key={p.key}
  593. className="flex items-center gap-2 rounded-md border border-border/70 bg-background px-3 py-2 text-sm"
  594. >
  595. <input
  596. type="checkbox"
  597. checked={checked}
  598. onChange={(e) => {
  599. const next = new Set(editForm.permissions ?? []);
  600. if (e.target.checked) next.add(p.key);
  601. else next.delete(p.key);
  602. setEditForm({ ...editForm, permissions: Array.from(next) });
  603. }}
  604. />
  605. <span>{t(PERM_LABEL[p.key])}</span>
  606. </label>
  607. );
  608. })}
  609. </div>
  610. </div>
  611. )}
  612. <div>
  613. <Label htmlFor="edit-nickname">{t("agent.users.form.nickname")}</Label>
  614. <Input
  615. id="edit-nickname"
  616. value={editForm.nickname || ""}
  617. onChange={(e) =>
  618. setEditForm({ ...editForm, nickname: e.target.value })
  619. }
  620. placeholder={t("agent.users.placeholder.nickname")}
  621. />
  622. </div>
  623. <div>
  624. <Label htmlFor="edit-email">{t("agent.users.form.email")}</Label>
  625. <Input
  626. id="edit-email"
  627. type="email"
  628. value={editForm.email || ""}
  629. onChange={(e) =>
  630. setEditForm({ ...editForm, email: e.target.value })
  631. }
  632. placeholder={t("agent.users.placeholder.email")}
  633. />
  634. </div>
  635. <div className="flex items-center gap-2">
  636. <input
  637. type="checkbox"
  638. id="edit-receive-ai"
  639. checked={editForm.receive_ai_conversations ?? true}
  640. onChange={(e) =>
  641. setEditForm({
  642. ...editForm,
  643. receive_ai_conversations: e.target.checked,
  644. })
  645. }
  646. className="w-4 h-4"
  647. />
  648. <Label htmlFor="edit-receive-ai" className="cursor-pointer">
  649. {t("agent.users.receiveAiLabel")}
  650. </Label>
  651. </div>
  652. <div className="flex justify-end gap-2">
  653. <Button
  654. variant="outline"
  655. onClick={() => setEditDialogOpen(false)}
  656. disabled={submitting}
  657. >
  658. {t("agent.common.cancel")}
  659. </Button>
  660. <Button onClick={handleUpdate} disabled={submitting}>
  661. {submitting ? t("agent.users.submit.updating") : t("agent.common.update")}
  662. </Button>
  663. </div>
  664. </div>
  665. )}
  666. </DialogContent>
  667. </Dialog>
  668. {/* 修改密码对话框 */}
  669. <Dialog open={passwordDialogOpen} onOpenChange={setPasswordDialogOpen}>
  670. <DialogContent>
  671. <DialogHeader>
  672. <DialogTitle>{t("agent.users.dialog.passwordTitle")}</DialogTitle>
  673. </DialogHeader>
  674. {selectedUser && (
  675. <div className="space-y-4">
  676. <div>
  677. <Label>{t("agent.users.field.username")}</Label>
  678. <Input value={selectedUser.username} disabled />
  679. </div>
  680. {selectedUser.id === agent?.id && (
  681. <div>
  682. <Label htmlFor="password-old">{t("agent.users.form.oldPassword")} *</Label>
  683. <Input
  684. id="password-old"
  685. type="password"
  686. value={passwordForm.old_password || ""}
  687. onChange={(e) =>
  688. setPasswordForm({
  689. ...passwordForm,
  690. old_password: e.target.value,
  691. })
  692. }
  693. placeholder={t("agent.users.placeholder.oldPassword")}
  694. />
  695. </div>
  696. )}
  697. <div>
  698. <Label htmlFor="password-new">{t("agent.users.form.newPassword")} *</Label>
  699. <Input
  700. id="password-new"
  701. type="password"
  702. value={passwordForm.new_password}
  703. onChange={(e) =>
  704. setPasswordForm({
  705. ...passwordForm,
  706. new_password: e.target.value,
  707. })
  708. }
  709. placeholder={t("agent.users.placeholder.password")}
  710. />
  711. </div>
  712. <div className="flex justify-end gap-2">
  713. <Button
  714. variant="outline"
  715. onClick={() => setPasswordDialogOpen(false)}
  716. disabled={submitting}
  717. >
  718. {t("agent.common.cancel")}
  719. </Button>
  720. <Button onClick={handleUpdatePassword} disabled={submitting}>
  721. {submitting ? t("agent.users.submit.updating") : t("agent.common.update")}
  722. </Button>
  723. </div>
  724. </div>
  725. )}
  726. </DialogContent>
  727. </Dialog>
  728. {/* 删除确认对话框 */}
  729. <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
  730. <DialogContent>
  731. <DialogHeader>
  732. <DialogTitle>{t("agent.users.dialog.deleteTitle")}</DialogTitle>
  733. </DialogHeader>
  734. {selectedUser && (
  735. <div className="space-y-4">
  736. <p className="text-foreground">
  737. {tr("agent.users.dialog.deleteConfirm", {
  738. username: selectedUser.username,
  739. })}
  740. </p>
  741. <p className="text-sm text-muted-foreground">
  742. {t("agent.users.dialog.deleteNote")}
  743. </p>
  744. <div className="flex justify-end gap-2">
  745. <Button
  746. variant="outline"
  747. onClick={() => setDeleteDialogOpen(false)}
  748. disabled={submitting}
  749. >
  750. {t("agent.common.cancel")}
  751. </Button>
  752. <Button
  753. variant="destructive"
  754. onClick={handleDelete}
  755. disabled={submitting}
  756. >
  757. {submitting ? t("agent.users.submit.deleting") : t("agent.common.delete")}
  758. </Button>
  759. </div>
  760. </div>
  761. )}
  762. </DialogContent>
  763. </Dialog>
  764. </>
  765. );
  766. if (embedded) {
  767. return (
  768. <>
  769. <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
  770. {headerContent}
  771. {mainContent}
  772. </div>
  773. {userDialogs}
  774. </>
  775. );
  776. }
  777. return (
  778. <>
  779. <ResponsiveLayout main={mainContent} header={headerContent} />
  780. {userDialogs}
  781. </>
  782. );
  783. }