AccountManagement.jsx 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React from 'react';
  16. import {
  17. Button,
  18. Card,
  19. Input,
  20. Space,
  21. Typography,
  22. Avatar,
  23. Tabs,
  24. TabPane,
  25. Popover,
  26. Modal,
  27. } from '@douyinfe/semi-ui';
  28. import {
  29. IconMail,
  30. IconShield,
  31. IconGithubLogo,
  32. IconKey,
  33. IconLock,
  34. IconDelete,
  35. } from '@douyinfe/semi-icons';
  36. import { SiTelegram, SiWechat, SiLinux, SiDiscord } from 'react-icons/si';
  37. import { UserPlus, ShieldCheck } from 'lucide-react';
  38. import TelegramLoginButton from 'react-telegram-login';
  39. import {
  40. API,
  41. showError,
  42. showSuccess,
  43. onGitHubOAuthClicked,
  44. onOIDCClicked,
  45. onLinuxDOOAuthClicked,
  46. onDiscordOAuthClicked,
  47. onCustomOAuthClicked,
  48. } from '../../../../helpers';
  49. import TwoFASetting from '../components/TwoFASetting';
  50. const AccountManagement = ({
  51. t,
  52. userState,
  53. status,
  54. systemToken,
  55. setShowEmailBindModal,
  56. setShowWeChatBindModal,
  57. generateAccessToken,
  58. handleSystemTokenClick,
  59. setShowChangePasswordModal,
  60. setShowAccountDeleteModal,
  61. passkeyStatus,
  62. passkeySupported,
  63. passkeyRegisterLoading,
  64. passkeyDeleteLoading,
  65. onPasskeyRegister,
  66. onPasskeyDelete,
  67. }) => {
  68. const renderAccountInfo = (accountId, label) => {
  69. if (!accountId || accountId === '') {
  70. return <span className='text-gray-500'>{t('未绑定')}</span>;
  71. }
  72. const popContent = (
  73. <div className='text-xs p-2'>
  74. <Typography.Paragraph copyable={{ content: accountId }}>
  75. {accountId}
  76. </Typography.Paragraph>
  77. {label ? (
  78. <div className='mt-1 text-[11px] text-gray-500'>{label}</div>
  79. ) : null}
  80. </div>
  81. );
  82. return (
  83. <Popover content={popContent} position='top' trigger='hover'>
  84. <span className='block max-w-full truncate text-gray-600 hover:text-blue-600 cursor-pointer'>
  85. {accountId}
  86. </span>
  87. </Popover>
  88. );
  89. };
  90. const isBound = (accountId) => Boolean(accountId);
  91. const [showTelegramBindModal, setShowTelegramBindModal] =
  92. React.useState(false);
  93. const [customOAuthBindings, setCustomOAuthBindings] = React.useState([]);
  94. const [customOAuthLoading, setCustomOAuthLoading] = React.useState({});
  95. // Fetch custom OAuth bindings
  96. const loadCustomOAuthBindings = async () => {
  97. try {
  98. const res = await API.get('/api/user/oauth/bindings');
  99. if (res.data.success) {
  100. setCustomOAuthBindings(res.data.data || []);
  101. } else {
  102. showError(res.data.message || t('获取绑定信息失败'));
  103. }
  104. } catch (error) {
  105. showError(error.response?.data?.message || error.message || t('获取绑定信息失败'));
  106. }
  107. };
  108. // Unbind custom OAuth provider
  109. const handleUnbindCustomOAuth = async (providerId, providerName) => {
  110. Modal.confirm({
  111. title: t('确认解绑'),
  112. content: t('确定要解绑 {{name}} 吗?', { name: providerName }),
  113. okText: t('确认'),
  114. cancelText: t('取消'),
  115. onOk: async () => {
  116. setCustomOAuthLoading((prev) => ({ ...prev, [providerId]: true }));
  117. try {
  118. const res = await API.delete(`/api/user/oauth/bindings/${providerId}`);
  119. if (res.data.success) {
  120. showSuccess(t('解绑成功'));
  121. await loadCustomOAuthBindings();
  122. } else {
  123. showError(res.data.message);
  124. }
  125. } catch (error) {
  126. showError(error.response?.data?.message || error.message || t('操作失败'));
  127. } finally {
  128. setCustomOAuthLoading((prev) => ({ ...prev, [providerId]: false }));
  129. }
  130. },
  131. });
  132. };
  133. // Handle bind custom OAuth
  134. const handleBindCustomOAuth = (provider) => {
  135. onCustomOAuthClicked(provider);
  136. };
  137. // Check if custom OAuth provider is bound
  138. const isCustomOAuthBound = (providerId) => {
  139. return customOAuthBindings.some((b) => b.provider_id === providerId);
  140. };
  141. // Get binding info for a provider
  142. const getCustomOAuthBinding = (providerId) => {
  143. return customOAuthBindings.find((b) => b.provider_id === providerId);
  144. };
  145. React.useEffect(() => {
  146. loadCustomOAuthBindings();
  147. }, []);
  148. const passkeyEnabled = passkeyStatus?.enabled;
  149. const lastUsedLabel = passkeyStatus?.last_used_at
  150. ? new Date(passkeyStatus.last_used_at).toLocaleString()
  151. : t('尚未使用');
  152. return (
  153. <Card className='!rounded-2xl'>
  154. {/* 卡片头部 */}
  155. <div className='flex items-center mb-4'>
  156. <Avatar size='small' color='teal' className='mr-3 shadow-md'>
  157. <UserPlus size={16} />
  158. </Avatar>
  159. <div>
  160. <Typography.Text className='text-lg font-medium'>
  161. {t('账户管理')}
  162. </Typography.Text>
  163. <div className='text-xs text-gray-600'>
  164. {t('账户绑定、安全设置和身份验证')}
  165. </div>
  166. </div>
  167. </div>
  168. <Tabs type='card' defaultActiveKey='binding'>
  169. {/* 账户绑定 Tab */}
  170. <TabPane
  171. tab={
  172. <div className='flex items-center'>
  173. <UserPlus size={16} className='mr-2' />
  174. {t('账户绑定')}
  175. </div>
  176. }
  177. itemKey='binding'
  178. >
  179. <div className='py-4'>
  180. <div className='grid grid-cols-1 lg:grid-cols-2 gap-4'>
  181. {/* 邮箱绑定 */}
  182. <Card className='!rounded-xl'>
  183. <div className='flex items-center justify-between gap-3'>
  184. <div className='flex items-center flex-1 min-w-0'>
  185. <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
  186. <IconMail
  187. size='default'
  188. className='text-slate-600 dark:text-slate-300'
  189. />
  190. </div>
  191. <div className='flex-1 min-w-0'>
  192. <div className='font-medium text-gray-900'>
  193. {t('邮箱')}
  194. </div>
  195. <div className='text-sm text-gray-500 truncate'>
  196. {renderAccountInfo(
  197. userState.user?.email,
  198. t('邮箱地址'),
  199. )}
  200. </div>
  201. </div>
  202. </div>
  203. <div className='flex-shrink-0'>
  204. <Button
  205. type='primary'
  206. theme='outline'
  207. size='small'
  208. onClick={() => setShowEmailBindModal(true)}
  209. >
  210. {isBound(userState.user?.email)
  211. ? t('修改绑定')
  212. : t('绑定')}
  213. </Button>
  214. </div>
  215. </div>
  216. </Card>
  217. {/* 微信绑定 */}
  218. <Card className='!rounded-xl'>
  219. <div className='flex items-center justify-between gap-3'>
  220. <div className='flex items-center flex-1 min-w-0'>
  221. <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
  222. <SiWechat
  223. size={20}
  224. className='text-slate-600 dark:text-slate-300'
  225. />
  226. </div>
  227. <div className='flex-1 min-w-0'>
  228. <div className='font-medium text-gray-900'>
  229. {t('微信')}
  230. </div>
  231. <div className='text-sm text-gray-500 truncate'>
  232. {!status.wechat_login
  233. ? t('未启用')
  234. : isBound(userState.user?.wechat_id)
  235. ? t('已绑定')
  236. : t('未绑定')}
  237. </div>
  238. </div>
  239. </div>
  240. <div className='flex-shrink-0'>
  241. <Button
  242. type='primary'
  243. theme='outline'
  244. size='small'
  245. disabled={!status.wechat_login}
  246. onClick={() => setShowWeChatBindModal(true)}
  247. >
  248. {isBound(userState.user?.wechat_id)
  249. ? t('修改绑定')
  250. : status.wechat_login
  251. ? t('绑定')
  252. : t('未启用')}
  253. </Button>
  254. </div>
  255. </div>
  256. </Card>
  257. {/* GitHub绑定 */}
  258. <Card className='!rounded-xl'>
  259. <div className='flex items-center justify-between gap-3'>
  260. <div className='flex items-center flex-1 min-w-0'>
  261. <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
  262. <IconGithubLogo
  263. size='default'
  264. className='text-slate-600 dark:text-slate-300'
  265. />
  266. </div>
  267. <div className='flex-1 min-w-0'>
  268. <div className='font-medium text-gray-900'>
  269. {t('GitHub')}
  270. </div>
  271. <div className='text-sm text-gray-500 truncate'>
  272. {renderAccountInfo(
  273. userState.user?.github_id,
  274. t('GitHub ID'),
  275. )}
  276. </div>
  277. </div>
  278. </div>
  279. <div className='flex-shrink-0'>
  280. <Button
  281. type='primary'
  282. theme='outline'
  283. size='small'
  284. onClick={() =>
  285. onGitHubOAuthClicked(status.github_client_id)
  286. }
  287. disabled={
  288. isBound(userState.user?.github_id) ||
  289. !status.github_oauth
  290. }
  291. >
  292. {status.github_oauth ? t('绑定') : t('未启用')}
  293. </Button>
  294. </div>
  295. </div>
  296. </Card>
  297. {/* Discord绑定 */}
  298. <Card className='!rounded-xl'>
  299. <div className='flex items-center justify-between gap-3'>
  300. <div className='flex items-center flex-1 min-w-0'>
  301. <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
  302. <SiDiscord
  303. size={20}
  304. className='text-slate-600 dark:text-slate-300'
  305. />
  306. </div>
  307. <div className='flex-1 min-w-0'>
  308. <div className='font-medium text-gray-900'>
  309. {t('Discord')}
  310. </div>
  311. <div className='text-sm text-gray-500 truncate'>
  312. {renderAccountInfo(
  313. userState.user?.discord_id,
  314. t('Discord ID'),
  315. )}
  316. </div>
  317. </div>
  318. </div>
  319. <div className='flex-shrink-0'>
  320. <Button
  321. type='primary'
  322. theme='outline'
  323. size='small'
  324. onClick={() =>
  325. onDiscordOAuthClicked(status.discord_client_id)
  326. }
  327. disabled={
  328. isBound(userState.user?.discord_id) ||
  329. !status.discord_oauth
  330. }
  331. >
  332. {status.discord_oauth ? t('绑定') : t('未启用')}
  333. </Button>
  334. </div>
  335. </div>
  336. </Card>
  337. {/* OIDC绑定 */}
  338. <Card className='!rounded-xl'>
  339. <div className='flex items-center justify-between gap-3'>
  340. <div className='flex items-center flex-1 min-w-0'>
  341. <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
  342. <IconShield
  343. size='default'
  344. className='text-slate-600 dark:text-slate-300'
  345. />
  346. </div>
  347. <div className='flex-1 min-w-0'>
  348. <div className='font-medium text-gray-900'>
  349. {t('OIDC')}
  350. </div>
  351. <div className='text-sm text-gray-500 truncate'>
  352. {renderAccountInfo(
  353. userState.user?.oidc_id,
  354. t('OIDC ID'),
  355. )}
  356. </div>
  357. </div>
  358. </div>
  359. <div className='flex-shrink-0'>
  360. <Button
  361. type='primary'
  362. theme='outline'
  363. size='small'
  364. onClick={() =>
  365. onOIDCClicked(
  366. status.oidc_authorization_endpoint,
  367. status.oidc_client_id,
  368. )
  369. }
  370. disabled={
  371. isBound(userState.user?.oidc_id) || !status.oidc_enabled
  372. }
  373. >
  374. {status.oidc_enabled ? t('绑定') : t('未启用')}
  375. </Button>
  376. </div>
  377. </div>
  378. </Card>
  379. {/* Telegram绑定 */}
  380. <Card className='!rounded-xl'>
  381. <div className='flex items-center justify-between gap-3'>
  382. <div className='flex items-center flex-1 min-w-0'>
  383. <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
  384. <SiTelegram
  385. size={20}
  386. className='text-slate-600 dark:text-slate-300'
  387. />
  388. </div>
  389. <div className='flex-1 min-w-0'>
  390. <div className='font-medium text-gray-900'>
  391. {t('Telegram')}
  392. </div>
  393. <div className='text-sm text-gray-500 truncate'>
  394. {renderAccountInfo(
  395. userState.user?.telegram_id,
  396. t('Telegram ID'),
  397. )}
  398. </div>
  399. </div>
  400. </div>
  401. <div className='flex-shrink-0'>
  402. {status.telegram_oauth ? (
  403. isBound(userState.user?.telegram_id) ? (
  404. <Button
  405. disabled
  406. size='small'
  407. type='primary'
  408. theme='outline'
  409. >
  410. {t('已绑定')}
  411. </Button>
  412. ) : (
  413. <Button
  414. type='primary'
  415. theme='outline'
  416. size='small'
  417. onClick={() => setShowTelegramBindModal(true)}
  418. >
  419. {t('绑定')}
  420. </Button>
  421. )
  422. ) : (
  423. <Button
  424. disabled
  425. size='small'
  426. type='primary'
  427. theme='outline'
  428. >
  429. {t('未启用')}
  430. </Button>
  431. )}
  432. </div>
  433. </div>
  434. </Card>
  435. <Modal
  436. title={t('绑定 Telegram')}
  437. visible={showTelegramBindModal}
  438. onCancel={() => setShowTelegramBindModal(false)}
  439. footer={null}
  440. >
  441. <div className='my-3 text-sm text-gray-600'>
  442. {t('点击下方按钮通过 Telegram 完成绑定')}
  443. </div>
  444. <div className='flex justify-center'>
  445. <div className='scale-90'>
  446. <TelegramLoginButton
  447. dataAuthUrl='/api/oauth/telegram/bind'
  448. botName={status.telegram_bot_name}
  449. />
  450. </div>
  451. </div>
  452. </Modal>
  453. {/* LinuxDO绑定 */}
  454. <Card className='!rounded-xl'>
  455. <div className='flex items-center justify-between gap-3'>
  456. <div className='flex items-center flex-1 min-w-0'>
  457. <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
  458. <SiLinux
  459. size={20}
  460. className='text-slate-600 dark:text-slate-300'
  461. />
  462. </div>
  463. <div className='flex-1 min-w-0'>
  464. <div className='font-medium text-gray-900'>
  465. {t('LinuxDO')}
  466. </div>
  467. <div className='text-sm text-gray-500 truncate'>
  468. {renderAccountInfo(
  469. userState.user?.linux_do_id,
  470. t('LinuxDO ID'),
  471. )}
  472. </div>
  473. </div>
  474. </div>
  475. <div className='flex-shrink-0'>
  476. <Button
  477. type='primary'
  478. theme='outline'
  479. size='small'
  480. onClick={() =>
  481. onLinuxDOOAuthClicked(status.linuxdo_client_id)
  482. }
  483. disabled={
  484. isBound(userState.user?.linux_do_id) ||
  485. !status.linuxdo_oauth
  486. }
  487. >
  488. {status.linuxdo_oauth ? t('绑定') : t('未启用')}
  489. </Button>
  490. </div>
  491. </div>
  492. </Card>
  493. {/* 自定义 OAuth 提供商绑定 */}
  494. {status.custom_oauth_providers &&
  495. status.custom_oauth_providers.map((provider) => {
  496. const bound = isCustomOAuthBound(provider.id);
  497. const binding = getCustomOAuthBinding(provider.id);
  498. return (
  499. <Card key={provider.slug} className='!rounded-xl'>
  500. <div className='flex items-center justify-between gap-3'>
  501. <div className='flex items-center flex-1 min-w-0'>
  502. <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
  503. <IconLock
  504. size='default'
  505. className='text-slate-600 dark:text-slate-300'
  506. />
  507. </div>
  508. <div className='flex-1 min-w-0'>
  509. <div className='font-medium text-gray-900'>
  510. {provider.name}
  511. </div>
  512. <div className='text-sm text-gray-500 truncate'>
  513. {bound
  514. ? renderAccountInfo(
  515. binding?.provider_user_id,
  516. t('{{name}} ID', { name: provider.name }),
  517. )
  518. : t('未绑定')}
  519. </div>
  520. </div>
  521. </div>
  522. <div className='flex-shrink-0'>
  523. {bound ? (
  524. <Button
  525. type='danger'
  526. theme='outline'
  527. size='small'
  528. loading={customOAuthLoading[provider.id]}
  529. onClick={() =>
  530. handleUnbindCustomOAuth(provider.id, provider.name)
  531. }
  532. >
  533. {t('解绑')}
  534. </Button>
  535. ) : (
  536. <Button
  537. type='primary'
  538. theme='outline'
  539. size='small'
  540. onClick={() => handleBindCustomOAuth(provider)}
  541. >
  542. {t('绑定')}
  543. </Button>
  544. )}
  545. </div>
  546. </div>
  547. </Card>
  548. );
  549. })}
  550. </div>
  551. </div>
  552. </TabPane>
  553. {/* 安全设置 Tab */}
  554. <TabPane
  555. tab={
  556. <div className='flex items-center'>
  557. <ShieldCheck size={16} className='mr-2' />
  558. {t('安全设置')}
  559. </div>
  560. }
  561. itemKey='security'
  562. >
  563. <div className='py-4'>
  564. <div className='space-y-6'>
  565. <Space vertical className='w-full'>
  566. {/* 系统访问令牌 */}
  567. <Card className='!rounded-xl w-full'>
  568. <div className='flex flex-col sm:flex-row items-start sm:justify-between gap-4'>
  569. <div className='flex items-start w-full sm:w-auto'>
  570. <div className='w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0'>
  571. <IconKey size='large' className='text-slate-600' />
  572. </div>
  573. <div className='flex-1'>
  574. <Typography.Title heading={6} className='mb-1'>
  575. {t('系统访问令牌')}
  576. </Typography.Title>
  577. <Typography.Text type='tertiary' className='text-sm'>
  578. {t('用于API调用的身份验证令牌,请妥善保管')}
  579. </Typography.Text>
  580. {systemToken && (
  581. <div className='mt-3'>
  582. <Input
  583. readonly
  584. value={systemToken}
  585. onClick={handleSystemTokenClick}
  586. size='large'
  587. prefix={<IconKey />}
  588. />
  589. </div>
  590. )}
  591. </div>
  592. </div>
  593. <Button
  594. type='primary'
  595. theme='solid'
  596. onClick={generateAccessToken}
  597. className='!bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto'
  598. icon={<IconKey />}
  599. >
  600. {systemToken ? t('重新生成') : t('生成令牌')}
  601. </Button>
  602. </div>
  603. </Card>
  604. {/* 密码管理 */}
  605. <Card className='!rounded-xl w-full'>
  606. <div className='flex flex-col sm:flex-row items-start sm:justify-between gap-4'>
  607. <div className='flex items-start w-full sm:w-auto'>
  608. <div className='w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0'>
  609. <IconLock size='large' className='text-slate-600' />
  610. </div>
  611. <div>
  612. <Typography.Title heading={6} className='mb-1'>
  613. {t('密码管理')}
  614. </Typography.Title>
  615. <Typography.Text type='tertiary' className='text-sm'>
  616. {t('定期更改密码可以提高账户安全性')}
  617. </Typography.Text>
  618. </div>
  619. </div>
  620. <Button
  621. type='primary'
  622. theme='solid'
  623. onClick={() => setShowChangePasswordModal(true)}
  624. className='!bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto'
  625. icon={<IconLock />}
  626. >
  627. {t('修改密码')}
  628. </Button>
  629. </div>
  630. </Card>
  631. {/* Passkey 设置 */}
  632. <Card className='!rounded-xl w-full'>
  633. <div className='flex flex-col sm:flex-row items-start sm:justify-between gap-4'>
  634. <div className='flex items-start w-full sm:w-auto'>
  635. <div className='w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0'>
  636. <IconKey size='large' className='text-slate-600' />
  637. </div>
  638. <div>
  639. <Typography.Title heading={6} className='mb-1'>
  640. {t('Passkey 登录')}
  641. </Typography.Title>
  642. <Typography.Text type='tertiary' className='text-sm'>
  643. {passkeyEnabled
  644. ? t('已启用 Passkey,无需密码即可登录')
  645. : t('使用 Passkey 实现免密且更安全的登录体验')}
  646. </Typography.Text>
  647. <div className='mt-2 text-xs text-gray-500 space-y-1'>
  648. <div>
  649. {t('最后使用时间')}:{lastUsedLabel}
  650. </div>
  651. {/*{passkeyEnabled && (*/}
  652. {/* <div>*/}
  653. {/* {t('备份支持')}:*/}
  654. {/* {passkeyStatus?.backup_eligible*/}
  655. {/* ? t('支持备份')*/}
  656. {/* : t('不支持')}*/}
  657. {/* ,{t('备份状态')}:*/}
  658. {/* {passkeyStatus?.backup_state ? t('已备份') : t('未备份')}*/}
  659. {/* </div>*/}
  660. {/*)}*/}
  661. {!passkeySupported && (
  662. <div className='text-amber-600'>
  663. {t('当前设备不支持 Passkey')}
  664. </div>
  665. )}
  666. </div>
  667. </div>
  668. </div>
  669. <Button
  670. type={passkeyEnabled ? 'danger' : 'primary'}
  671. theme={passkeyEnabled ? 'solid' : 'solid'}
  672. onClick={
  673. passkeyEnabled
  674. ? () => {
  675. Modal.confirm({
  676. title: t('确认解绑 Passkey'),
  677. content: t(
  678. '解绑后将无法使用 Passkey 登录,确定要继续吗?',
  679. ),
  680. okText: t('确认解绑'),
  681. cancelText: t('取消'),
  682. okType: 'danger',
  683. onOk: onPasskeyDelete,
  684. });
  685. }
  686. : onPasskeyRegister
  687. }
  688. className={`w-full sm:w-auto ${passkeyEnabled ? '!bg-slate-500 hover:!bg-slate-600' : ''}`}
  689. icon={<IconKey />}
  690. disabled={!passkeySupported && !passkeyEnabled}
  691. loading={
  692. passkeyEnabled
  693. ? passkeyDeleteLoading
  694. : passkeyRegisterLoading
  695. }
  696. >
  697. {passkeyEnabled ? t('解绑 Passkey') : t('注册 Passkey')}
  698. </Button>
  699. </div>
  700. </Card>
  701. {/* 两步验证设置 */}
  702. <TwoFASetting t={t} />
  703. {/* 危险区域 */}
  704. <Card className='!rounded-xl w-full'>
  705. <div className='flex flex-col sm:flex-row items-start sm:justify-between gap-4'>
  706. <div className='flex items-start w-full sm:w-auto'>
  707. <div className='w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0'>
  708. <IconDelete size='large' className='text-slate-600' />
  709. </div>
  710. <div>
  711. <Typography.Title
  712. heading={6}
  713. className='mb-1 text-slate-700'
  714. >
  715. {t('删除账户')}
  716. </Typography.Title>
  717. <Typography.Text type='tertiary' className='text-sm'>
  718. {t('此操作不可逆,所有数据将被永久删除')}
  719. </Typography.Text>
  720. </div>
  721. </div>
  722. <Button
  723. type='danger'
  724. theme='solid'
  725. onClick={() => setShowAccountDeleteModal(true)}
  726. className='w-full sm:w-auto !bg-slate-500 hover:!bg-slate-600'
  727. icon={<IconDelete />}
  728. >
  729. {t('删除账户')}
  730. </Button>
  731. </div>
  732. </Card>
  733. </Space>
  734. </div>
  735. </div>
  736. </TabPane>
  737. </Tabs>
  738. </Card>
  739. );
  740. };
  741. export default AccountManagement;