PersonalSetting.js 67 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566
  1. import React, { useContext, useEffect, useState } from 'react';
  2. import { useNavigate } from 'react-router-dom';
  3. import {
  4. API,
  5. copy,
  6. isRoot,
  7. isAdmin,
  8. showError,
  9. showInfo,
  10. showSuccess,
  11. renderQuota,
  12. renderQuotaWithPrompt,
  13. stringToColor,
  14. onGitHubOAuthClicked,
  15. onOIDCClicked,
  16. onLinuxDOOAuthClicked,
  17. renderModelTag,
  18. getModelCategories
  19. } from '../../helpers';
  20. import Turnstile from 'react-turnstile';
  21. import { UserContext } from '../../context/User';
  22. import { useTheme } from '../../context/Theme';
  23. import {
  24. Avatar,
  25. Banner,
  26. Button,
  27. Card,
  28. Empty,
  29. Image,
  30. Input,
  31. Layout,
  32. Modal,
  33. Skeleton,
  34. Space,
  35. Tag,
  36. Typography,
  37. Collapsible,
  38. Radio,
  39. RadioGroup,
  40. AutoComplete,
  41. Checkbox,
  42. Tabs,
  43. TabPane
  44. } from '@douyinfe/semi-ui';
  45. import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
  46. import {
  47. IconMail,
  48. IconLock,
  49. IconShield,
  50. IconUser,
  51. IconSetting,
  52. IconBell,
  53. IconGithubLogo,
  54. IconKey,
  55. IconDelete,
  56. IconChevronDown,
  57. IconChevronUp
  58. } from '@douyinfe/semi-icons';
  59. import { SiTelegram, SiWechat, SiLinux } from 'react-icons/si';
  60. import { Bell, Shield, Webhook, Globe, Settings, UserPlus, ShieldCheck } from 'lucide-react';
  61. import TelegramLoginButton from 'react-telegram-login';
  62. import { useTranslation } from 'react-i18next';
  63. const PersonalSetting = () => {
  64. const [userState, userDispatch] = useContext(UserContext);
  65. let navigate = useNavigate();
  66. const { t } = useTranslation();
  67. const theme = useTheme();
  68. const [inputs, setInputs] = useState({
  69. wechat_verification_code: '',
  70. email_verification_code: '',
  71. email: '',
  72. self_account_deletion_confirmation: '',
  73. original_password: '',
  74. set_new_password: '',
  75. set_new_password_confirmation: '',
  76. });
  77. const [status, setStatus] = useState({});
  78. const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
  79. const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
  80. const [showEmailBindModal, setShowEmailBindModal] = useState(false);
  81. const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
  82. const [turnstileEnabled, setTurnstileEnabled] = useState(false);
  83. const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
  84. const [turnstileToken, setTurnstileToken] = useState('');
  85. const [loading, setLoading] = useState(false);
  86. const [disableButton, setDisableButton] = useState(false);
  87. const [countdown, setCountdown] = useState(30);
  88. const [systemToken, setSystemToken] = useState('');
  89. const [models, setModels] = useState([]);
  90. const [isModelsExpanded, setIsModelsExpanded] = useState(() => {
  91. // Initialize from localStorage if available
  92. const savedState = localStorage.getItem('modelsExpanded');
  93. return savedState ? JSON.parse(savedState) : false;
  94. });
  95. const [activeModelCategory, setActiveModelCategory] = useState('all');
  96. const MODELS_DISPLAY_COUNT = 25; // 默认显示的模型数量
  97. const [notificationSettings, setNotificationSettings] = useState({
  98. warningType: 'email',
  99. warningThreshold: 100000,
  100. webhookUrl: '',
  101. webhookSecret: '',
  102. notificationEmail: '',
  103. acceptUnsetModelRatioModel: false,
  104. recordIpLog: false,
  105. });
  106. const [modelsLoading, setModelsLoading] = useState(true);
  107. const [showWebhookDocs, setShowWebhookDocs] = useState(true);
  108. useEffect(() => {
  109. let status = localStorage.getItem('status');
  110. if (status) {
  111. status = JSON.parse(status);
  112. setStatus(status);
  113. if (status.turnstile_check) {
  114. setTurnstileEnabled(true);
  115. setTurnstileSiteKey(status.turnstile_site_key);
  116. }
  117. }
  118. getUserData().then((res) => {
  119. console.log(userState);
  120. });
  121. loadModels().then();
  122. }, []);
  123. useEffect(() => {
  124. let countdownInterval = null;
  125. if (disableButton && countdown > 0) {
  126. countdownInterval = setInterval(() => {
  127. setCountdown(countdown - 1);
  128. }, 1000);
  129. } else if (countdown === 0) {
  130. setDisableButton(false);
  131. setCountdown(30);
  132. }
  133. return () => clearInterval(countdownInterval); // Clean up on unmount
  134. }, [disableButton, countdown]);
  135. useEffect(() => {
  136. if (userState?.user?.setting) {
  137. const settings = JSON.parse(userState.user.setting);
  138. setNotificationSettings({
  139. warningType: settings.notify_type || 'email',
  140. warningThreshold: settings.quota_warning_threshold || 500000,
  141. webhookUrl: settings.webhook_url || '',
  142. webhookSecret: settings.webhook_secret || '',
  143. notificationEmail: settings.notification_email || '',
  144. acceptUnsetModelRatioModel:
  145. settings.accept_unset_model_ratio_model || false,
  146. recordIpLog: settings.record_ip_log || false,
  147. });
  148. }
  149. }, [userState?.user?.setting]);
  150. // Save models expanded state to localStorage whenever it changes
  151. useEffect(() => {
  152. localStorage.setItem('modelsExpanded', JSON.stringify(isModelsExpanded));
  153. }, [isModelsExpanded]);
  154. const handleInputChange = (name, value) => {
  155. setInputs((inputs) => ({ ...inputs, [name]: value }));
  156. };
  157. const generateAccessToken = async () => {
  158. const res = await API.get('/api/user/token');
  159. const { success, message, data } = res.data;
  160. if (success) {
  161. setSystemToken(data);
  162. await copy(data);
  163. showSuccess(t('令牌已重置并已复制到剪贴板'));
  164. } else {
  165. showError(message);
  166. }
  167. };
  168. const getUserData = async () => {
  169. let res = await API.get(`/api/user/self`);
  170. const { success, message, data } = res.data;
  171. if (success) {
  172. userDispatch({ type: 'login', payload: data });
  173. } else {
  174. showError(message);
  175. }
  176. };
  177. const loadModels = async () => {
  178. setModelsLoading(true);
  179. try {
  180. let res = await API.get(`/api/user/models`);
  181. const { success, message, data } = res.data;
  182. if (success) {
  183. if (data != null) {
  184. setModels(data);
  185. }
  186. } else {
  187. showError(message);
  188. }
  189. } catch (error) {
  190. showError(t('加载模型列表失败'));
  191. } finally {
  192. setModelsLoading(false);
  193. }
  194. };
  195. const handleSystemTokenClick = async (e) => {
  196. e.target.select();
  197. await copy(e.target.value);
  198. showSuccess(t('系统令牌已复制到剪切板'));
  199. };
  200. const deleteAccount = async () => {
  201. if (inputs.self_account_deletion_confirmation !== userState.user.username) {
  202. showError(t('请输入你的账户名以确认删除!'));
  203. return;
  204. }
  205. const res = await API.delete('/api/user/self');
  206. const { success, message } = res.data;
  207. if (success) {
  208. showSuccess(t('账户已删除!'));
  209. await API.get('/api/user/logout');
  210. userDispatch({ type: 'logout' });
  211. localStorage.removeItem('user');
  212. navigate('/login');
  213. } else {
  214. showError(message);
  215. }
  216. };
  217. const bindWeChat = async () => {
  218. if (inputs.wechat_verification_code === '') return;
  219. const res = await API.get(
  220. `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`,
  221. );
  222. const { success, message } = res.data;
  223. if (success) {
  224. showSuccess(t('微信账户绑定成功!'));
  225. setShowWeChatBindModal(false);
  226. } else {
  227. showError(message);
  228. }
  229. };
  230. const changePassword = async () => {
  231. if (inputs.original_password === '') {
  232. showError(t('请输入原密码!'));
  233. return;
  234. }
  235. if (inputs.set_new_password === '') {
  236. showError(t('请输入新密码!'));
  237. return;
  238. }
  239. if (inputs.original_password === inputs.set_new_password) {
  240. showError(t('新密码需要和原密码不一致!'));
  241. return;
  242. }
  243. if (inputs.set_new_password !== inputs.set_new_password_confirmation) {
  244. showError(t('两次输入的密码不一致!'));
  245. return;
  246. }
  247. const res = await API.put(`/api/user/self`, {
  248. original_password: inputs.original_password,
  249. password: inputs.set_new_password,
  250. });
  251. const { success, message } = res.data;
  252. if (success) {
  253. showSuccess(t('密码修改成功!'));
  254. setShowWeChatBindModal(false);
  255. } else {
  256. showError(message);
  257. }
  258. setShowChangePasswordModal(false);
  259. };
  260. const sendVerificationCode = async () => {
  261. if (inputs.email === '') {
  262. showError(t('请输入邮箱!'));
  263. return;
  264. }
  265. setDisableButton(true);
  266. if (turnstileEnabled && turnstileToken === '') {
  267. showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!'));
  268. return;
  269. }
  270. setLoading(true);
  271. const res = await API.get(
  272. `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
  273. );
  274. const { success, message } = res.data;
  275. if (success) {
  276. showSuccess(t('验证码发送成功,请检查邮箱!'));
  277. } else {
  278. showError(message);
  279. }
  280. setLoading(false);
  281. };
  282. const bindEmail = async () => {
  283. if (inputs.email_verification_code === '') {
  284. showError(t('请输入邮箱验证码!'));
  285. return;
  286. }
  287. setLoading(true);
  288. const res = await API.get(
  289. `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`,
  290. );
  291. const { success, message } = res.data;
  292. if (success) {
  293. showSuccess(t('邮箱账户绑定成功!'));
  294. setShowEmailBindModal(false);
  295. userState.user.email = inputs.email;
  296. } else {
  297. showError(message);
  298. }
  299. setLoading(false);
  300. };
  301. const getUsername = () => {
  302. if (userState.user) {
  303. return userState.user.username;
  304. } else {
  305. return 'null';
  306. }
  307. };
  308. const getAvatarText = () => {
  309. const username = getUsername();
  310. if (username && username.length > 0) {
  311. // 获取前两个字符,支持中文和英文
  312. return username.slice(0, 2).toUpperCase();
  313. }
  314. return 'NA';
  315. };
  316. const copyText = async (text) => {
  317. if (await copy(text)) {
  318. showSuccess(t('已复制:') + text);
  319. } else {
  320. // setSearchKeyword(text);
  321. Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
  322. }
  323. };
  324. const handleNotificationSettingChange = (type, value) => {
  325. setNotificationSettings((prev) => ({
  326. ...prev,
  327. [type]: value.target ? value.target.value !== undefined ? value.target.value : value.target.checked : value, // handle checkbox properly
  328. }));
  329. };
  330. const saveNotificationSettings = async () => {
  331. try {
  332. const res = await API.put('/api/user/setting', {
  333. notify_type: notificationSettings.warningType,
  334. quota_warning_threshold: parseFloat(
  335. notificationSettings.warningThreshold,
  336. ),
  337. webhook_url: notificationSettings.webhookUrl,
  338. webhook_secret: notificationSettings.webhookSecret,
  339. notification_email: notificationSettings.notificationEmail,
  340. accept_unset_model_ratio_model:
  341. notificationSettings.acceptUnsetModelRatioModel,
  342. record_ip_log: notificationSettings.recordIpLog,
  343. });
  344. if (res.data.success) {
  345. showSuccess(t('设置保存成功'));
  346. await getUserData();
  347. } else {
  348. showError(res.data.message);
  349. }
  350. } catch (error) {
  351. showError(t('设置保存失败'));
  352. }
  353. };
  354. return (
  355. <div className="bg-gray-50 mt-[60px]">
  356. <div className="flex justify-center">
  357. <div className="w-full">
  358. {/* 主卡片容器 */}
  359. <Card className="!rounded-2xl shadow-lg border-0">
  360. {/* 顶部用户信息区域 */}
  361. <Card
  362. className="!rounded-2xl !border-0 !shadow-lg overflow-hidden"
  363. style={{
  364. background: theme === 'dark'
  365. ? 'linear-gradient(135deg, #1e293b 0%, #334155 50%, #475569 100%)'
  366. : 'linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #cbd5e1 100%)',
  367. position: 'relative'
  368. }}
  369. bodyStyle={{ padding: 0 }}
  370. >
  371. {/* 装饰性背景元素 */}
  372. <div className="absolute inset-0 overflow-hidden">
  373. <div className="absolute -top-10 -right-10 w-40 h-40 bg-slate-400 dark:bg-slate-500 opacity-5 rounded-full"></div>
  374. <div className="absolute -bottom-16 -left-16 w-48 h-48 bg-slate-300 dark:bg-slate-400 opacity-8 rounded-full"></div>
  375. <div className="absolute top-1/2 right-1/4 w-24 h-24 bg-slate-400 dark:bg-slate-500 opacity-6 rounded-full"></div>
  376. </div>
  377. <div className="relative p-4 sm:p-6 md:p-8 text-gray-600 dark:text-gray-300">
  378. <div className="flex justify-between items-start mb-4 sm:mb-6">
  379. <div className="flex items-center flex-1 min-w-0">
  380. <Avatar
  381. size='large'
  382. className="mr-3 sm:mr-4 shadow-md flex-shrink-0 bg-slate-500 dark:bg-slate-400"
  383. >
  384. {getAvatarText()}
  385. </Avatar>
  386. <div className="flex-1 min-w-0">
  387. <div className="text-base sm:text-lg font-semibold truncate text-gray-800 dark:text-gray-100">
  388. {getUsername()}
  389. </div>
  390. <div className="mt-1 flex flex-wrap gap-1 sm:gap-2">
  391. {isRoot() ? (
  392. <Tag
  393. size='small'
  394. className="!rounded-full bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300"
  395. style={{ fontWeight: '500' }}
  396. >
  397. {t('超级管理员')}
  398. </Tag>
  399. ) : isAdmin() ? (
  400. <Tag
  401. size='small'
  402. className="!rounded-full bg-gray-50 dark:bg-gray-700 text-gray-600 dark:text-gray-300"
  403. style={{ fontWeight: '500' }}
  404. >
  405. {t('管理员')}
  406. </Tag>
  407. ) : (
  408. <Tag
  409. size='small'
  410. className="!rounded-full bg-slate-50 dark:bg-slate-700 text-slate-600 dark:text-slate-300"
  411. style={{ fontWeight: '500' }}
  412. >
  413. {t('普通用户')}
  414. </Tag>
  415. )}
  416. <Tag
  417. size='small'
  418. className="!rounded-full bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300"
  419. style={{ fontWeight: '500' }}
  420. >
  421. ID: {userState?.user?.id}
  422. </Tag>
  423. </div>
  424. </div>
  425. </div>
  426. <div className="w-10 h-10 sm:w-12 sm:h-12 rounded-lg flex items-center justify-center shadow-md flex-shrink-0 ml-2 bg-slate-400 dark:bg-slate-500">
  427. <IconUser size="default" className="text-white" />
  428. </div>
  429. </div>
  430. <div className="mb-4 sm:mb-6">
  431. <div className="text-xs sm:text-sm mb-1 sm:mb-2 text-gray-500 dark:text-gray-400">
  432. {t('当前余额')}
  433. </div>
  434. <div className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-wide text-gray-900 dark:text-gray-100">
  435. {renderQuota(userState?.user?.quota)}
  436. </div>
  437. </div>
  438. <div className="flex flex-col sm:flex-row sm:justify-between sm:items-end">
  439. <div className="grid grid-cols-3 gap-2 sm:flex sm:space-x-6 lg:space-x-8 mb-3 sm:mb-0">
  440. <div className="text-center sm:text-left">
  441. <div className="text-xs text-gray-400 dark:text-gray-500">
  442. {t('历史消耗')}
  443. </div>
  444. <div className="text-xs sm:text-sm font-medium truncate text-gray-600 dark:text-gray-300">
  445. {renderQuota(userState?.user?.used_quota)}
  446. </div>
  447. </div>
  448. <div className="text-center sm:text-left">
  449. <div className="text-xs text-gray-400 dark:text-gray-500">
  450. {t('请求次数')}
  451. </div>
  452. <div className="text-xs sm:text-sm font-medium truncate text-gray-600 dark:text-gray-300">
  453. {userState.user?.request_count || 0}
  454. </div>
  455. </div>
  456. <div className="text-center sm:text-left">
  457. <div className="text-xs text-gray-400 dark:text-gray-500">
  458. {t('用户分组')}
  459. </div>
  460. <div className="text-xs sm:text-sm font-medium truncate text-gray-600 dark:text-gray-300">
  461. {userState?.user?.group || t('默认')}
  462. </div>
  463. </div>
  464. </div>
  465. </div>
  466. <div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-slate-300 via-slate-400 to-slate-500 dark:from-slate-600 dark:via-slate-500 dark:to-slate-400 opacity-40"></div>
  467. </div>
  468. </Card>
  469. {/* 主内容区域 - 使用Tabs组织不同功能模块 */}
  470. <div className="p-4">
  471. <Tabs type='line' defaultActiveKey='models' className="modern-tabs">
  472. {/* 可用模型Tab */}
  473. <TabPane
  474. tab={
  475. <div className="flex items-center">
  476. <Settings size={16} className="mr-2" />
  477. {t('可用模型')}
  478. </div>
  479. }
  480. itemKey='models'
  481. >
  482. <div className="gap-6 py-4">
  483. {/* 可用模型部分 */}
  484. <div className="bg-gray-50 dark:bg-gray-800 rounded-xl">
  485. <div className="flex items-center mb-4">
  486. <div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
  487. <Settings size={20} className="text-slate-600 dark:text-slate-300" />
  488. </div>
  489. <div>
  490. <Typography.Title heading={6} className="mb-0">{t('模型列表')}</Typography.Title>
  491. <div className="text-gray-500 text-sm">{t('点击模型名称可复制')}</div>
  492. </div>
  493. </div>
  494. {modelsLoading ? (
  495. // 骨架屏加载状态 - 模拟实际加载后的布局
  496. <div className="space-y-4">
  497. {/* 模拟分类标签 */}
  498. <div className="mb-4" style={{ borderBottom: '1px solid var(--semi-color-border)' }}>
  499. <div className="flex overflow-x-auto py-2 gap-2">
  500. {Array.from({ length: 8 }).map((_, index) => (
  501. <Skeleton.Button key={`cat-${index}`} style={{
  502. width: index === 0 ? 130 : 100 + Math.random() * 50,
  503. height: 36,
  504. borderRadius: 8
  505. }} />
  506. ))}
  507. </div>
  508. </div>
  509. {/* 模拟模型标签列表 */}
  510. <div className="flex flex-wrap gap-2">
  511. {Array.from({ length: 20 }).map((_, index) => (
  512. <Skeleton.Button
  513. key={`model-${index}`}
  514. style={{
  515. width: 100 + Math.random() * 100,
  516. height: 32,
  517. borderRadius: 16,
  518. margin: '4px'
  519. }}
  520. />
  521. ))}
  522. </div>
  523. </div>
  524. ) : models.length === 0 ? (
  525. <div className="py-8">
  526. <Empty
  527. image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
  528. darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />}
  529. description={t('没有可用模型')}
  530. style={{ padding: '24px 0' }}
  531. />
  532. </div>
  533. ) : (
  534. <>
  535. {/* 模型分类标签页 */}
  536. <div className="mb-4">
  537. <Tabs
  538. type="card"
  539. activeKey={activeModelCategory}
  540. onChange={key => setActiveModelCategory(key)}
  541. className="mt-2"
  542. >
  543. {Object.entries(getModelCategories(t)).map(([key, category]) => {
  544. // 计算该分类下的模型数量
  545. const modelCount = key === 'all'
  546. ? models.length
  547. : models.filter(model => category.filter({ model_name: model })).length;
  548. if (modelCount === 0 && key !== 'all') return null;
  549. return (
  550. <TabPane
  551. tab={
  552. <span className="flex items-center gap-2">
  553. {category.icon && <span className="w-4 h-4">{category.icon}</span>}
  554. {category.label}
  555. <Tag
  556. color={activeModelCategory === key ? 'red' : 'grey'}
  557. size='small'
  558. shape='circle'
  559. >
  560. {modelCount}
  561. </Tag>
  562. </span>
  563. }
  564. itemKey={key}
  565. key={key}
  566. />
  567. );
  568. })}
  569. </Tabs>
  570. </div>
  571. <div className="bg-white dark:bg-gray-700 rounded-lg p-3">
  572. {(() => {
  573. // 根据当前选中的分类过滤模型
  574. const categories = getModelCategories(t);
  575. const filteredModels = activeModelCategory === 'all'
  576. ? models
  577. : models.filter(model => categories[activeModelCategory].filter({ model_name: model }));
  578. // 如果过滤后没有模型,显示空状态
  579. if (filteredModels.length === 0) {
  580. return (
  581. <Empty
  582. image={<IllustrationNoContent style={{ width: 120, height: 120 }} />}
  583. darkModeImage={<IllustrationNoContentDark style={{ width: 120, height: 120 }} />}
  584. description={t('该分类下没有可用模型')}
  585. style={{ padding: '16px 0' }}
  586. />
  587. );
  588. }
  589. if (filteredModels.length <= MODELS_DISPLAY_COUNT) {
  590. return (
  591. <Space wrap>
  592. {filteredModels.map((model) => (
  593. renderModelTag(model, {
  594. size: 'large',
  595. shape: 'circle',
  596. onClick: () => copyText(model),
  597. })
  598. ))}
  599. </Space>
  600. );
  601. } else {
  602. return (
  603. <>
  604. <Collapsible isOpen={isModelsExpanded}>
  605. <Space wrap>
  606. {filteredModels.map((model) => (
  607. renderModelTag(model, {
  608. size: 'large',
  609. shape: 'circle',
  610. onClick: () => copyText(model),
  611. })
  612. ))}
  613. <Tag
  614. color='grey'
  615. type='light'
  616. className="cursor-pointer !rounded-lg"
  617. onClick={() => setIsModelsExpanded(false)}
  618. icon={<IconChevronUp />}
  619. >
  620. {t('收起')}
  621. </Tag>
  622. </Space>
  623. </Collapsible>
  624. {!isModelsExpanded && (
  625. <Space wrap>
  626. {filteredModels
  627. .slice(0, MODELS_DISPLAY_COUNT)
  628. .map((model) => (
  629. renderModelTag(model, {
  630. size: 'large',
  631. shape: 'circle',
  632. onClick: () => copyText(model),
  633. })
  634. ))}
  635. <Tag
  636. color='grey'
  637. type='light'
  638. className="cursor-pointer !rounded-lg"
  639. onClick={() => setIsModelsExpanded(true)}
  640. icon={<IconChevronDown />}
  641. >
  642. {t('更多')} {filteredModels.length - MODELS_DISPLAY_COUNT} {t('个模型')}
  643. </Tag>
  644. </Space>
  645. )}
  646. </>
  647. );
  648. }
  649. })()}
  650. </div>
  651. </>
  652. )}
  653. </div>
  654. </div>
  655. </TabPane>
  656. {/* 账户绑定Tab */}
  657. <TabPane
  658. tab={
  659. <div className="flex items-center">
  660. <UserPlus size={16} className="mr-2" />
  661. {t('账户绑定')}
  662. </div>
  663. }
  664. itemKey='account'
  665. >
  666. <div className="py-4">
  667. <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
  668. {/* 邮箱绑定 */}
  669. <Card
  670. className="!rounded-xl transition-shadow"
  671. bodyStyle={{ padding: '16px' }}
  672. shadows='hover'
  673. >
  674. <div className="flex items-center justify-between">
  675. <div className="flex items-center flex-1">
  676. <div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
  677. <IconMail size="default" className="text-slate-600 dark:text-slate-300" />
  678. </div>
  679. <div className="flex-1 min-w-0">
  680. <div className="font-medium text-gray-900">{t('邮箱')}</div>
  681. <div className="text-sm text-gray-500 truncate">
  682. {userState.user && userState.user.email !== ''
  683. ? userState.user.email
  684. : t('未绑定')}
  685. </div>
  686. </div>
  687. </div>
  688. <Button
  689. type="primary"
  690. theme="outline"
  691. size="small"
  692. onClick={() => setShowEmailBindModal(true)}
  693. className="!rounded-lg"
  694. >
  695. {userState.user && userState.user.email !== ''
  696. ? t('修改绑定')
  697. : t('绑定')}
  698. </Button>
  699. </div>
  700. </Card>
  701. {/* 微信绑定 */}
  702. <Card
  703. className="!rounded-xl transition-shadow"
  704. bodyStyle={{ padding: '16px' }}
  705. shadows='hover'
  706. >
  707. <div className="flex items-center justify-between">
  708. <div className="flex items-center flex-1">
  709. <div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
  710. <SiWechat size={20} className="text-slate-600 dark:text-slate-300" />
  711. </div>
  712. <div className="flex-1 min-w-0">
  713. <div className="font-medium text-gray-900">{t('微信')}</div>
  714. <div className="text-sm text-gray-500 truncate">
  715. {userState.user && userState.user.wechat_id !== ''
  716. ? t('已绑定')
  717. : t('未绑定')}
  718. </div>
  719. </div>
  720. </div>
  721. <Button
  722. type="primary"
  723. theme="outline"
  724. size="small"
  725. disabled={!status.wechat_login}
  726. onClick={() => setShowWeChatBindModal(true)}
  727. className="!rounded-lg"
  728. >
  729. {userState.user && userState.user.wechat_id !== ''
  730. ? t('修改绑定')
  731. : status.wechat_login
  732. ? t('绑定')
  733. : t('未启用')}
  734. </Button>
  735. </div>
  736. </Card>
  737. {/* GitHub绑定 */}
  738. <Card
  739. className="!rounded-xl transition-shadow"
  740. bodyStyle={{ padding: '16px' }}
  741. shadows='hover'
  742. >
  743. <div className="flex items-center justify-between">
  744. <div className="flex items-center flex-1">
  745. <div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
  746. <IconGithubLogo size="default" className="text-slate-600 dark:text-slate-300" />
  747. </div>
  748. <div className="flex-1 min-w-0">
  749. <div className="font-medium text-gray-900">{t('GitHub')}</div>
  750. <div className="text-sm text-gray-500 truncate">
  751. {userState.user && userState.user.github_id !== ''
  752. ? userState.user.github_id
  753. : t('未绑定')}
  754. </div>
  755. </div>
  756. </div>
  757. <Button
  758. type="primary"
  759. theme="outline"
  760. size="small"
  761. onClick={() => onGitHubOAuthClicked(status.github_client_id)}
  762. disabled={
  763. (userState.user && userState.user.github_id !== '') ||
  764. !status.github_oauth
  765. }
  766. className="!rounded-lg"
  767. >
  768. {status.github_oauth ? t('绑定') : t('未启用')}
  769. </Button>
  770. </div>
  771. </Card>
  772. {/* OIDC绑定 */}
  773. <Card
  774. className="!rounded-xl transition-shadow"
  775. bodyStyle={{ padding: '16px' }}
  776. shadows='hover'
  777. >
  778. <div className="flex items-center justify-between">
  779. <div className="flex items-center flex-1">
  780. <div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
  781. <IconShield size="default" className="text-slate-600 dark:text-slate-300" />
  782. </div>
  783. <div className="flex-1 min-w-0">
  784. <div className="font-medium text-gray-900">{t('OIDC')}</div>
  785. <div className="text-sm text-gray-500 truncate">
  786. {userState.user && userState.user.oidc_id !== ''
  787. ? userState.user.oidc_id
  788. : t('未绑定')}
  789. </div>
  790. </div>
  791. </div>
  792. <Button
  793. type="primary"
  794. theme="outline"
  795. size="small"
  796. onClick={() => onOIDCClicked(
  797. status.oidc_authorization_endpoint,
  798. status.oidc_client_id,
  799. )}
  800. disabled={
  801. (userState.user && userState.user.oidc_id !== '') ||
  802. !status.oidc_enabled
  803. }
  804. className="!rounded-lg"
  805. >
  806. {status.oidc_enabled ? t('绑定') : t('未启用')}
  807. </Button>
  808. </div>
  809. </Card>
  810. {/* Telegram绑定 */}
  811. <Card
  812. className="!rounded-xl transition-shadow"
  813. bodyStyle={{ padding: '16px' }}
  814. shadows='hover'
  815. >
  816. <div className="flex items-center justify-between">
  817. <div className="flex items-center flex-1">
  818. <div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
  819. <SiTelegram size={20} className="text-slate-600 dark:text-slate-300" />
  820. </div>
  821. <div className="flex-1 min-w-0">
  822. <div className="font-medium text-gray-900">{t('Telegram')}</div>
  823. <div className="text-sm text-gray-500 truncate">
  824. {userState.user && userState.user.telegram_id !== ''
  825. ? userState.user.telegram_id
  826. : t('未绑定')}
  827. </div>
  828. </div>
  829. </div>
  830. <div className="flex-shrink-0">
  831. {status.telegram_oauth ? (
  832. userState.user.telegram_id !== '' ? (
  833. <Button disabled={true} size="small" className="!rounded-lg">
  834. {t('已绑定')}
  835. </Button>
  836. ) : (
  837. <div className="scale-75">
  838. <TelegramLoginButton
  839. dataAuthUrl='/api/oauth/telegram/bind'
  840. botName={status.telegram_bot_name}
  841. />
  842. </div>
  843. )
  844. ) : (
  845. <Button disabled={true} size="small" className="!rounded-lg">
  846. {t('未启用')}
  847. </Button>
  848. )}
  849. </div>
  850. </div>
  851. </Card>
  852. {/* LinuxDO绑定 */}
  853. <Card
  854. className="!rounded-xl transition-shadow"
  855. bodyStyle={{ padding: '16px' }}
  856. shadows='hover'
  857. >
  858. <div className="flex items-center justify-between">
  859. <div className="flex items-center flex-1">
  860. <div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
  861. <SiLinux size={20} className="text-slate-600 dark:text-slate-300" />
  862. </div>
  863. <div className="flex-1 min-w-0">
  864. <div className="font-medium text-gray-900">{t('LinuxDO')}</div>
  865. <div className="text-sm text-gray-500 truncate">
  866. {userState.user && userState.user.linux_do_id !== ''
  867. ? userState.user.linux_do_id
  868. : t('未绑定')}
  869. </div>
  870. </div>
  871. </div>
  872. <Button
  873. type="primary"
  874. theme="outline"
  875. size="small"
  876. onClick={() => onLinuxDOOAuthClicked(status.linuxdo_client_id)}
  877. disabled={
  878. (userState.user && userState.user.linux_do_id !== '') ||
  879. !status.linuxdo_oauth
  880. }
  881. className="!rounded-lg"
  882. >
  883. {status.linuxdo_oauth ? t('绑定') : t('未启用')}
  884. </Button>
  885. </div>
  886. </Card>
  887. </div>
  888. </div>
  889. </TabPane>
  890. {/* 安全设置Tab */}
  891. <TabPane
  892. tab={
  893. <div className="flex items-center">
  894. <ShieldCheck size={16} className="mr-2" />
  895. {t('安全设置')}
  896. </div>
  897. }
  898. itemKey='security'
  899. >
  900. <div className="py-4">
  901. <div className="space-y-6">
  902. <Space vertical className='w-full'>
  903. {/* 系统访问令牌 */}
  904. <Card
  905. className="!rounded-xl w-full"
  906. bodyStyle={{ padding: '20px' }}
  907. shadows='hover'
  908. >
  909. <div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
  910. <div className="flex items-start w-full sm:w-auto">
  911. <div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
  912. <IconKey size="large" className="text-slate-600" />
  913. </div>
  914. <div className="flex-1">
  915. <Typography.Title heading={6} className="mb-1">
  916. {t('系统访问令牌')}
  917. </Typography.Title>
  918. <Typography.Text type="tertiary" className="text-sm">
  919. {t('用于API调用的身份验证令牌,请妥善保管')}
  920. </Typography.Text>
  921. {systemToken && (
  922. <div className="mt-3">
  923. <Input
  924. readonly
  925. value={systemToken}
  926. onClick={handleSystemTokenClick}
  927. size="large"
  928. className="!rounded-lg"
  929. prefix={<IconKey />}
  930. />
  931. </div>
  932. )}
  933. </div>
  934. </div>
  935. <Button
  936. type="primary"
  937. theme="solid"
  938. onClick={generateAccessToken}
  939. className="!rounded-lg !bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
  940. icon={<IconKey />}
  941. >
  942. {systemToken ? t('重新生成') : t('生成令牌')}
  943. </Button>
  944. </div>
  945. </Card>
  946. {/* 密码管理 */}
  947. <Card
  948. className="!rounded-xl w-full"
  949. bodyStyle={{ padding: '20px' }}
  950. shadows='hover'
  951. >
  952. <div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
  953. <div className="flex items-start w-full sm:w-auto">
  954. <div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
  955. <IconLock size="large" className="text-slate-600" />
  956. </div>
  957. <div>
  958. <Typography.Title heading={6} className="mb-1">
  959. {t('密码管理')}
  960. </Typography.Title>
  961. <Typography.Text type="tertiary" className="text-sm">
  962. {t('定期更改密码可以提高账户安全性')}
  963. </Typography.Text>
  964. </div>
  965. </div>
  966. <Button
  967. type="primary"
  968. theme="solid"
  969. onClick={() => setShowChangePasswordModal(true)}
  970. className="!rounded-lg !bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
  971. icon={<IconLock />}
  972. >
  973. {t('修改密码')}
  974. </Button>
  975. </div>
  976. </Card>
  977. {/* 危险区域 */}
  978. <Card
  979. className="!rounded-xl border-red-200 w-full"
  980. bodyStyle={{ padding: '20px' }}
  981. shadows='hover'
  982. >
  983. <div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
  984. <div className="flex items-start w-full sm:w-auto">
  985. <div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
  986. <IconDelete size="large" className="text-slate-600" />
  987. </div>
  988. <div>
  989. <Typography.Title heading={6} className="mb-1 text-slate-700">
  990. {t('删除账户')}
  991. </Typography.Title>
  992. <Typography.Text type="tertiary" className="text-sm">
  993. {t('此操作不可逆,所有数据将被永久删除')}
  994. </Typography.Text>
  995. </div>
  996. </div>
  997. <Button
  998. type="danger"
  999. theme="solid"
  1000. onClick={() => setShowAccountDeleteModal(true)}
  1001. className="!rounded-lg w-full sm:w-auto !bg-slate-500 hover:!bg-slate-600"
  1002. icon={<IconDelete />}
  1003. >
  1004. {t('删除账户')}
  1005. </Button>
  1006. </div>
  1007. </Card>
  1008. </Space>
  1009. </div>
  1010. </div>
  1011. </TabPane>
  1012. {/* 通知设置Tab */}
  1013. <TabPane
  1014. tab={
  1015. <div className="flex items-center">
  1016. <Bell size={16} className="mr-2" />
  1017. {t('其他设置')}
  1018. </div>
  1019. }
  1020. itemKey='notification'
  1021. >
  1022. <div className="py-4">
  1023. <Tabs type='card' defaultActiveKey='notify' className="!rounded-lg">
  1024. <TabPane
  1025. tab={t('通知设置')}
  1026. itemKey='notify'
  1027. >
  1028. <div className="space-y-6">
  1029. {/* 通知方式选择 */}
  1030. <div className="bg-gray-50 rounded-xl">
  1031. <Typography.Text strong className="block mb-4 pt-4">{t('通知方式')}</Typography.Text>
  1032. <RadioGroup
  1033. value={notificationSettings.warningType}
  1034. onChange={(value) =>
  1035. handleNotificationSettingChange('warningType', value)
  1036. }
  1037. type="pureCard"
  1038. >
  1039. <Radio value='email' className="!p-4 !rounded-lg">
  1040. <div className="flex items-center">
  1041. <IconMail className="mr-2 text-slate-600" />
  1042. <div>
  1043. <div className="font-medium">{t('邮件通知')}</div>
  1044. <div className="text-sm text-gray-500">{t('通过邮件接收通知')}</div>
  1045. </div>
  1046. </div>
  1047. </Radio>
  1048. <Radio value='webhook' className="!p-4 !rounded-lg">
  1049. <div className="flex items-center">
  1050. <Webhook size={16} className="mr-2 text-slate-600" />
  1051. <div>
  1052. <div className="font-medium">{t('Webhook通知')}</div>
  1053. <div className="text-sm text-gray-500">{t('通过HTTP请求接收通知')}</div>
  1054. </div>
  1055. </div>
  1056. </Radio>
  1057. </RadioGroup>
  1058. </div>
  1059. {/* Webhook设置 */}
  1060. {notificationSettings.warningType === 'webhook' && (
  1061. <div className="space-y-4">
  1062. <div className="bg-white rounded-xl">
  1063. <Typography.Text strong className="block mb-3">{t('Webhook地址')}</Typography.Text>
  1064. <Input
  1065. value={notificationSettings.webhookUrl}
  1066. onChange={(val) =>
  1067. handleNotificationSettingChange('webhookUrl', val)
  1068. }
  1069. placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')}
  1070. size="large"
  1071. className="!rounded-lg"
  1072. prefix={<Webhook size={16} className="m-2" />}
  1073. />
  1074. <div className="text-gray-500 text-sm mt-2">
  1075. {t('只支持https,系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求')}
  1076. </div>
  1077. </div>
  1078. <div className="bg-white rounded-xl">
  1079. <Typography.Text strong className="block mb-3">{t('接口凭证(可选)')}</Typography.Text>
  1080. <Input
  1081. value={notificationSettings.webhookSecret}
  1082. onChange={(val) =>
  1083. handleNotificationSettingChange('webhookSecret', val)
  1084. }
  1085. placeholder={t('请输入密钥')}
  1086. size="large"
  1087. className="!rounded-lg"
  1088. prefix={<IconKey />}
  1089. />
  1090. <div className="text-gray-500 text-sm mt-2">
  1091. {t('密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性')}
  1092. </div>
  1093. </div>
  1094. <div className="bg-slate-50 rounded-xl">
  1095. <div className="flex items-center justify-between cursor-pointer" onClick={() => setShowWebhookDocs(!showWebhookDocs)}>
  1096. <div className="flex items-center">
  1097. <Globe size={16} className="mr-2 text-slate-600" />
  1098. <Typography.Text strong className="text-slate-700">
  1099. {t('Webhook请求结构')}
  1100. </Typography.Text>
  1101. </div>
  1102. {showWebhookDocs ? <IconChevronUp /> : <IconChevronDown />}
  1103. </div>
  1104. <Collapsible isOpen={showWebhookDocs}>
  1105. <pre className="mt-4 bg-gray-800 text-gray-100 rounded-lg text-sm overflow-x-auto">
  1106. {`{
  1107. "type": "quota_exceed", // 通知类型
  1108. "title": "标题", // 通知标题
  1109. "content": "通知内容", // 通知内容,支持 {{value}} 变量占位符
  1110. "values": ["值1", "值2"], // 按顺序替换content中的 {{value}} 占位符
  1111. "timestamp": 1739950503 // 时间戳
  1112. }
  1113. 示例:
  1114. {
  1115. "type": "quota_exceed",
  1116. "title": "额度预警通知",
  1117. "content": "您的额度即将用尽,当前剩余额度为 {{value}}",
  1118. "values": ["$0.99"],
  1119. "timestamp": 1739950503
  1120. }`}
  1121. </pre>
  1122. </Collapsible>
  1123. </div>
  1124. </div>
  1125. )}
  1126. {/* 邮件设置 */}
  1127. {notificationSettings.warningType === 'email' && (
  1128. <div className="bg-white rounded-xl">
  1129. <Typography.Text strong className="block mb-3">{t('通知邮箱')}</Typography.Text>
  1130. <Input
  1131. value={notificationSettings.notificationEmail}
  1132. onChange={(val) =>
  1133. handleNotificationSettingChange('notificationEmail', val)
  1134. }
  1135. placeholder={t('留空则使用账号绑定的邮箱')}
  1136. size="large"
  1137. className="!rounded-lg"
  1138. prefix={<IconMail />}
  1139. />
  1140. <div className="text-gray-500 text-sm mt-2">
  1141. {t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')}
  1142. </div>
  1143. </div>
  1144. )}
  1145. {/* 预警阈值 */}
  1146. <div className="bg-white rounded-xl">
  1147. <Typography.Text strong className="block mb-3">
  1148. {t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}
  1149. </Typography.Text>
  1150. <AutoComplete
  1151. value={notificationSettings.warningThreshold}
  1152. onChange={(val) =>
  1153. handleNotificationSettingChange('warningThreshold', val)
  1154. }
  1155. size="large"
  1156. className="!rounded-lg w-full max-w-xs"
  1157. placeholder={t('请输入预警额度')}
  1158. data={[
  1159. { value: 100000, label: '0.2$' },
  1160. { value: 500000, label: '1$' },
  1161. { value: 1000000, label: '5$' },
  1162. { value: 5000000, label: '10$' },
  1163. ]}
  1164. prefix={<IconBell />}
  1165. />
  1166. <div className="text-gray-500 text-sm mt-2">
  1167. {t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')}
  1168. </div>
  1169. </div>
  1170. </div>
  1171. </TabPane>
  1172. <TabPane
  1173. tab={t('价格设置')}
  1174. itemKey='price'
  1175. >
  1176. <div className="py-4">
  1177. <div className="space-y-4">
  1178. {/* 接受未设置价格模型 */}
  1179. <div className="bg-white rounded-xl">
  1180. <div className="flex items-start">
  1181. <div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center mt-1">
  1182. <Shield size={20} className="text-slate-600" />
  1183. </div>
  1184. <div className="flex-1">
  1185. <div className="flex items-center justify-between">
  1186. <div>
  1187. <Typography.Text strong className="block mb-2">
  1188. {t('接受未设置价格模型')}
  1189. </Typography.Text>
  1190. <div className="text-gray-500 text-sm">
  1191. {t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')}
  1192. </div>
  1193. </div>
  1194. <Checkbox
  1195. checked={notificationSettings.acceptUnsetModelRatioModel}
  1196. onChange={(e) =>
  1197. handleNotificationSettingChange(
  1198. 'acceptUnsetModelRatioModel',
  1199. e.target.checked,
  1200. )
  1201. }
  1202. className="ml-4"
  1203. />
  1204. </div>
  1205. </div>
  1206. </div>
  1207. </div>
  1208. </div>
  1209. </div>
  1210. </TabPane>
  1211. <TabPane
  1212. tab={t('IP记录')}
  1213. itemKey='ip'
  1214. >
  1215. <div className="py-4">
  1216. <div className="bg-white rounded-xl">
  1217. <div className="flex items-start">
  1218. <div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center mt-1">
  1219. <ShieldCheck size={20} className="text-slate-600" />
  1220. </div>
  1221. <div className="flex-1">
  1222. <div className="flex items-center justify-between">
  1223. <div>
  1224. <Typography.Text strong className="block mb-2">
  1225. {t('记录请求与错误日志 IP')}
  1226. </Typography.Text>
  1227. <div className="text-gray-500 text-sm">
  1228. {t('开启后,仅“消费”和“错误”日志将记录您的客户端 IP 地址')}
  1229. </div>
  1230. </div>
  1231. <Checkbox
  1232. checked={notificationSettings.recordIpLog}
  1233. onChange={(e) =>
  1234. handleNotificationSettingChange(
  1235. 'recordIpLog',
  1236. e.target.checked,
  1237. )
  1238. }
  1239. className="ml-4"
  1240. />
  1241. </div>
  1242. </div>
  1243. </div>
  1244. </div>
  1245. </div>
  1246. </TabPane>
  1247. </Tabs>
  1248. <div className="mt-6 flex justify-end">
  1249. <Button
  1250. type='primary'
  1251. onClick={saveNotificationSettings}
  1252. size="large"
  1253. className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
  1254. icon={<IconSetting />}
  1255. >
  1256. {t('保存设置')}
  1257. </Button>
  1258. </div>
  1259. </div>
  1260. </TabPane>
  1261. </Tabs>
  1262. </div>
  1263. </Card>
  1264. </div>
  1265. </div>
  1266. {/* 邮箱绑定模态框 */}
  1267. <Modal
  1268. title={
  1269. <div className="flex items-center">
  1270. <IconMail className="mr-2 text-blue-500" />
  1271. {t('绑定邮箱地址')}
  1272. </div>
  1273. }
  1274. visible={showEmailBindModal}
  1275. onCancel={() => setShowEmailBindModal(false)}
  1276. onOk={bindEmail}
  1277. size={'small'}
  1278. centered={true}
  1279. maskClosable={false}
  1280. className="modern-modal"
  1281. >
  1282. <div className="space-y-4 py-4">
  1283. <div className="flex gap-3">
  1284. <Input
  1285. placeholder={t('输入邮箱地址')}
  1286. onChange={(value) => handleInputChange('email', value)}
  1287. name='email'
  1288. type='email'
  1289. size="large"
  1290. className="!rounded-lg flex-1"
  1291. prefix={<IconMail />}
  1292. />
  1293. <Button
  1294. onClick={sendVerificationCode}
  1295. disabled={disableButton || loading}
  1296. className="!rounded-lg"
  1297. type="primary"
  1298. theme="outline"
  1299. size='large'
  1300. >
  1301. {disableButton ? `${t('重新发送')} (${countdown})` : t('获取验证码')}
  1302. </Button>
  1303. </div>
  1304. <Input
  1305. placeholder={t('验证码')}
  1306. name='email_verification_code'
  1307. value={inputs.email_verification_code}
  1308. onChange={(value) =>
  1309. handleInputChange('email_verification_code', value)
  1310. }
  1311. size="large"
  1312. className="!rounded-lg"
  1313. prefix={<IconKey />}
  1314. />
  1315. {turnstileEnabled && (
  1316. <div className="flex justify-center">
  1317. <Turnstile
  1318. sitekey={turnstileSiteKey}
  1319. onVerify={(token) => {
  1320. setTurnstileToken(token);
  1321. }}
  1322. />
  1323. </div>
  1324. )}
  1325. </div>
  1326. </Modal>
  1327. {/* 微信绑定模态框 */}
  1328. <Modal
  1329. title={
  1330. <div className="flex items-center">
  1331. <SiWechat className="mr-2 text-green-500" size={20} />
  1332. {t('绑定微信账户')}
  1333. </div>
  1334. }
  1335. visible={showWeChatBindModal}
  1336. onCancel={() => setShowWeChatBindModal(false)}
  1337. footer={null}
  1338. size={'small'}
  1339. centered={true}
  1340. className="modern-modal"
  1341. >
  1342. <div className="space-y-4 py-4 text-center">
  1343. <Image src={status.wechat_qrcode} className="mx-auto" />
  1344. <div className="text-gray-600">
  1345. <p>{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}</p>
  1346. </div>
  1347. <Input
  1348. placeholder={t('验证码')}
  1349. name='wechat_verification_code'
  1350. value={inputs.wechat_verification_code}
  1351. onChange={(v) =>
  1352. handleInputChange('wechat_verification_code', v)
  1353. }
  1354. size="large"
  1355. className="!rounded-lg"
  1356. prefix={<IconKey />}
  1357. />
  1358. <Button
  1359. type="primary"
  1360. theme="solid"
  1361. size='large'
  1362. onClick={bindWeChat}
  1363. className="!rounded-lg w-full !bg-slate-600 hover:!bg-slate-700"
  1364. icon={<SiWechat size={16} />}
  1365. >
  1366. {t('绑定')}
  1367. </Button>
  1368. </div>
  1369. </Modal>
  1370. {/* 账户删除模态框 */}
  1371. <Modal
  1372. title={
  1373. <div className="flex items-center">
  1374. <IconDelete className="mr-2 text-red-500" />
  1375. {t('删除账户确认')}
  1376. </div>
  1377. }
  1378. visible={showAccountDeleteModal}
  1379. onCancel={() => setShowAccountDeleteModal(false)}
  1380. onOk={deleteAccount}
  1381. size={'small'}
  1382. centered={true}
  1383. className="modern-modal"
  1384. >
  1385. <div className="space-y-4 py-4">
  1386. <Banner
  1387. type='danger'
  1388. description={t('您正在删除自己的帐户,将清空所有数据且不可恢复')}
  1389. closeIcon={null}
  1390. className="!rounded-lg"
  1391. />
  1392. <div>
  1393. <Typography.Text strong className="block mb-2 text-red-600">
  1394. {t('请输入您的用户名以确认删除')}
  1395. </Typography.Text>
  1396. <Input
  1397. placeholder={t('输入你的账户名{{username}}以确认删除', { username: ` ${userState?.user?.username} ` })}
  1398. name='self_account_deletion_confirmation'
  1399. value={inputs.self_account_deletion_confirmation}
  1400. onChange={(value) =>
  1401. handleInputChange('self_account_deletion_confirmation', value)
  1402. }
  1403. size="large"
  1404. className="!rounded-lg"
  1405. prefix={<IconUser />}
  1406. />
  1407. </div>
  1408. {turnstileEnabled && (
  1409. <div className="flex justify-center">
  1410. <Turnstile
  1411. sitekey={turnstileSiteKey}
  1412. onVerify={(token) => {
  1413. setTurnstileToken(token);
  1414. }}
  1415. />
  1416. </div>
  1417. )}
  1418. </div>
  1419. </Modal>
  1420. {/* 修改密码模态框 */}
  1421. <Modal
  1422. title={
  1423. <div className="flex items-center">
  1424. <IconLock className="mr-2 text-orange-500" />
  1425. {t('修改密码')}
  1426. </div>
  1427. }
  1428. visible={showChangePasswordModal}
  1429. onCancel={() => setShowChangePasswordModal(false)}
  1430. onOk={changePassword}
  1431. size={'small'}
  1432. centered={true}
  1433. className="modern-modal"
  1434. >
  1435. <div className="space-y-4 py-4">
  1436. <div>
  1437. <Typography.Text strong className="block mb-2">{t('原密码')}</Typography.Text>
  1438. <Input
  1439. name='original_password'
  1440. placeholder={t('请输入原密码')}
  1441. type='password'
  1442. value={inputs.original_password}
  1443. onChange={(value) =>
  1444. handleInputChange('original_password', value)
  1445. }
  1446. size="large"
  1447. className="!rounded-lg"
  1448. prefix={<IconLock />}
  1449. />
  1450. </div>
  1451. <div>
  1452. <Typography.Text strong className="block mb-2">{t('新密码')}</Typography.Text>
  1453. <Input
  1454. name='set_new_password'
  1455. placeholder={t('请输入新密码')}
  1456. type='password'
  1457. value={inputs.set_new_password}
  1458. onChange={(value) =>
  1459. handleInputChange('set_new_password', value)
  1460. }
  1461. size="large"
  1462. className="!rounded-lg"
  1463. prefix={<IconLock />}
  1464. />
  1465. </div>
  1466. <div>
  1467. <Typography.Text strong className="block mb-2">{t('确认新密码')}</Typography.Text>
  1468. <Input
  1469. name='set_new_password_confirmation'
  1470. placeholder={t('请再次输入新密码')}
  1471. type='password'
  1472. value={inputs.set_new_password_confirmation}
  1473. onChange={(value) =>
  1474. handleInputChange('set_new_password_confirmation', value)
  1475. }
  1476. size="large"
  1477. className="!rounded-lg"
  1478. prefix={<IconLock />}
  1479. />
  1480. </div>
  1481. {turnstileEnabled && (
  1482. <div className="flex justify-center">
  1483. <Turnstile
  1484. sitekey={turnstileSiteKey}
  1485. onVerify={(token) => {
  1486. setTurnstileToken(token);
  1487. }}
  1488. />
  1489. </div>
  1490. )}
  1491. </div>
  1492. </Modal>
  1493. </div>
  1494. );
  1495. };
  1496. export default PersonalSetting;