OtherSetting.jsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  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, { useContext, useEffect, useRef, useState } from 'react';
  16. import {
  17. Banner,
  18. Button,
  19. Col,
  20. Form,
  21. Row,
  22. Modal,
  23. Space,
  24. Card,
  25. } from '@douyinfe/semi-ui';
  26. import { API, showError, showSuccess, timestamp2string } from '../../helpers';
  27. import { marked } from 'marked';
  28. import { useTranslation } from 'react-i18next';
  29. import { StatusContext } from '../../context/Status';
  30. import Text from '@douyinfe/semi-ui/lib/es/typography/text';
  31. const OtherSetting = () => {
  32. const { t } = useTranslation();
  33. let [inputs, setInputs] = useState({
  34. Notice: '',
  35. UserAgreement: '',
  36. PrivacyPolicy: '',
  37. SystemName: '',
  38. Logo: '',
  39. Footer: '',
  40. About: '',
  41. HomePageContent: '',
  42. });
  43. let [loading, setLoading] = useState(false);
  44. const [showUpdateModal, setShowUpdateModal] = useState(false);
  45. const [statusState, statusDispatch] = useContext(StatusContext);
  46. const [updateData, setUpdateData] = useState({
  47. tag_name: '',
  48. content: '',
  49. });
  50. const updateOption = async (key, value) => {
  51. setLoading(true);
  52. const res = await API.put('/api/option/', {
  53. key,
  54. value,
  55. });
  56. const { success, message } = res.data;
  57. if (success) {
  58. setInputs((inputs) => ({ ...inputs, [key]: value }));
  59. } else {
  60. showError(message);
  61. }
  62. setLoading(false);
  63. };
  64. const [loadingInput, setLoadingInput] = useState({
  65. Notice: false,
  66. UserAgreement: false,
  67. PrivacyPolicy: false,
  68. SystemName: false,
  69. Logo: false,
  70. HomePageContent: false,
  71. About: false,
  72. Footer: false,
  73. CheckUpdate: false,
  74. });
  75. const handleInputChange = async (value, e) => {
  76. const name = e.target.id;
  77. setInputs((inputs) => ({ ...inputs, [name]: value }));
  78. };
  79. // 通用设置
  80. const formAPISettingGeneral = useRef();
  81. // 通用设置 - Notice
  82. const submitNotice = async () => {
  83. try {
  84. setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: true }));
  85. await updateOption('Notice', inputs.Notice);
  86. showSuccess(t('公告已更新'));
  87. } catch (error) {
  88. console.error(t('公告更新失败'), error);
  89. showError(t('公告更新失败'));
  90. } finally {
  91. setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: false }));
  92. }
  93. };
  94. // 通用设置 - UserAgreement
  95. const submitUserAgreement = async () => {
  96. try {
  97. setLoadingInput((loadingInput) => ({ ...loadingInput, UserAgreement: true }));
  98. await updateOption('UserAgreement', inputs.UserAgreement);
  99. showSuccess(t('用户协议已更新'));
  100. } catch (error) {
  101. console.error(t('用户协议更新失败'), error);
  102. showError(t('用户协议更新失败'));
  103. } finally {
  104. setLoadingInput((loadingInput) => ({ ...loadingInput, UserAgreement: false }));
  105. }
  106. };
  107. // 通用设置 - PrivacyPolicy
  108. const submitPrivacyPolicy = async () => {
  109. try {
  110. setLoadingInput((loadingInput) => ({ ...loadingInput, PrivacyPolicy: true }));
  111. await updateOption('PrivacyPolicy', inputs.PrivacyPolicy);
  112. showSuccess(t('隐私政策已更新'));
  113. } catch (error) {
  114. console.error(t('隐私政策更新失败'), error);
  115. showError(t('隐私政策更新失败'));
  116. } finally {
  117. setLoadingInput((loadingInput) => ({ ...loadingInput, PrivacyPolicy: false }));
  118. }
  119. };
  120. // 个性化设置
  121. const formAPIPersonalization = useRef();
  122. // 个性化设置 - SystemName
  123. const submitSystemName = async () => {
  124. try {
  125. setLoadingInput((loadingInput) => ({
  126. ...loadingInput,
  127. SystemName: true,
  128. }));
  129. await updateOption('SystemName', inputs.SystemName);
  130. showSuccess(t('系统名称已更新'));
  131. } catch (error) {
  132. console.error(t('系统名称更新失败'), error);
  133. showError(t('系统名称更新失败'));
  134. } finally {
  135. setLoadingInput((loadingInput) => ({
  136. ...loadingInput,
  137. SystemName: false,
  138. }));
  139. }
  140. };
  141. // 个性化设置 - Logo
  142. const submitLogo = async () => {
  143. try {
  144. setLoadingInput((loadingInput) => ({ ...loadingInput, Logo: true }));
  145. await updateOption('Logo', inputs.Logo);
  146. showSuccess('Logo 已更新');
  147. } catch (error) {
  148. console.error('Logo 更新失败', error);
  149. showError('Logo 更新失败');
  150. } finally {
  151. setLoadingInput((loadingInput) => ({ ...loadingInput, Logo: false }));
  152. }
  153. };
  154. // 个性化设置 - 首页内容
  155. const submitOption = async (key) => {
  156. try {
  157. setLoadingInput((loadingInput) => ({
  158. ...loadingInput,
  159. HomePageContent: true,
  160. }));
  161. await updateOption(key, inputs[key]);
  162. showSuccess('首页内容已更新');
  163. } catch (error) {
  164. console.error('首页内容更新失败', error);
  165. showError('首页内容更新失败');
  166. } finally {
  167. setLoadingInput((loadingInput) => ({
  168. ...loadingInput,
  169. HomePageContent: false,
  170. }));
  171. }
  172. };
  173. // 个性化设置 - 关于
  174. const submitAbout = async () => {
  175. try {
  176. setLoadingInput((loadingInput) => ({ ...loadingInput, About: true }));
  177. await updateOption('About', inputs.About);
  178. showSuccess('关于内容已更新');
  179. } catch (error) {
  180. console.error('关于内容更新失败', error);
  181. showError('关于内容更新失败');
  182. } finally {
  183. setLoadingInput((loadingInput) => ({ ...loadingInput, About: false }));
  184. }
  185. };
  186. // 个性化设置 - 页脚
  187. const submitFooter = async () => {
  188. try {
  189. setLoadingInput((loadingInput) => ({ ...loadingInput, Footer: true }));
  190. await updateOption('Footer', inputs.Footer);
  191. showSuccess('页脚内容已更新');
  192. } catch (error) {
  193. console.error('页脚内容更新失败', error);
  194. showError('页脚内容更新失败');
  195. } finally {
  196. setLoadingInput((loadingInput) => ({ ...loadingInput, Footer: false }));
  197. }
  198. };
  199. const checkUpdate = async () => {
  200. try {
  201. setLoadingInput((loadingInput) => ({
  202. ...loadingInput,
  203. CheckUpdate: true,
  204. }));
  205. // Use a CORS proxy to avoid direct cross-origin requests to GitHub API
  206. // Option 1: Use a public CORS proxy service
  207. // const proxyUrl = 'https://cors-anywhere.herokuapp.com/';
  208. // const res = await API.get(
  209. // `${proxyUrl}https://api.github.com/repos/Calcium-Ion/new-api/releases/latest`,
  210. // );
  211. // Option 2: Use the JSON proxy approach which often works better with GitHub API
  212. const res = await fetch(
  213. 'https://api.github.com/repos/Calcium-Ion/new-api/releases/latest',
  214. {
  215. headers: {
  216. Accept: 'application/json',
  217. 'Content-Type': 'application/json',
  218. // Adding User-Agent which is often required by GitHub API
  219. 'User-Agent': 'new-api-update-checker',
  220. },
  221. },
  222. ).then((response) => response.json());
  223. // Option 3: Use a local proxy endpoint
  224. // Create a cached version of the response to avoid frequent GitHub API calls
  225. // const res = await API.get('/api/status/github-latest-release');
  226. const { tag_name, body } = res;
  227. if (tag_name === statusState?.status?.version) {
  228. showSuccess(`已是最新版本:${tag_name}`);
  229. } else {
  230. setUpdateData({
  231. tag_name: tag_name,
  232. content: marked.parse(body),
  233. });
  234. setShowUpdateModal(true);
  235. }
  236. } catch (error) {
  237. console.error('Failed to check for updates:', error);
  238. showError('检查更新失败,请稍后再试');
  239. } finally {
  240. setLoadingInput((loadingInput) => ({
  241. ...loadingInput,
  242. CheckUpdate: false,
  243. }));
  244. }
  245. };
  246. const getOptions = async () => {
  247. const res = await API.get('/api/option/');
  248. const { success, message, data } = res.data;
  249. if (success) {
  250. let newInputs = {};
  251. data.forEach((item) => {
  252. if (item.key in inputs) {
  253. newInputs[item.key] = item.value;
  254. }
  255. });
  256. setInputs(newInputs);
  257. formAPISettingGeneral.current.setValues(newInputs);
  258. formAPIPersonalization.current.setValues(newInputs);
  259. } else {
  260. showError(message);
  261. }
  262. };
  263. useEffect(() => {
  264. getOptions();
  265. }, []);
  266. // Function to open GitHub release page
  267. const openGitHubRelease = () => {
  268. window.open(
  269. `https://github.com/Calcium-Ion/new-api/releases/tag/${updateData.tag_name}`,
  270. '_blank',
  271. );
  272. };
  273. const getStartTimeString = () => {
  274. const timestamp = statusState?.status?.start_time;
  275. return statusState.status ? timestamp2string(timestamp) : '';
  276. };
  277. return (
  278. <Row>
  279. <Col
  280. span={24}
  281. style={{
  282. marginTop: '10px',
  283. display: 'flex',
  284. flexDirection: 'column',
  285. gap: '10px',
  286. }}
  287. >
  288. {/* 版本信息 */}
  289. <Form>
  290. <Card>
  291. <Form.Section text={t('系统信息')}>
  292. <Row>
  293. <Col span={16}>
  294. <Space>
  295. <Text>
  296. {t('当前版本')}:
  297. {statusState?.status?.version || t('未知')}
  298. </Text>
  299. <Button
  300. type='primary'
  301. onClick={checkUpdate}
  302. loading={loadingInput['CheckUpdate']}
  303. >
  304. {t('检查更新')}
  305. </Button>
  306. </Space>
  307. </Col>
  308. </Row>
  309. <Row>
  310. <Col span={16}>
  311. <Text>
  312. {t('启动时间')}:{getStartTimeString()}
  313. </Text>
  314. </Col>
  315. </Row>
  316. </Form.Section>
  317. </Card>
  318. </Form>
  319. {/* 通用设置 */}
  320. <Form
  321. values={inputs}
  322. getFormApi={(formAPI) => (formAPISettingGeneral.current = formAPI)}
  323. >
  324. <Card>
  325. <Form.Section text={t('通用设置')}>
  326. <Form.TextArea
  327. label={t('公告')}
  328. placeholder={t(
  329. '在此输入新的公告内容,支持 Markdown & HTML 代码',
  330. )}
  331. field={'Notice'}
  332. onChange={handleInputChange}
  333. style={{ fontFamily: 'JetBrains Mono, Consolas' }}
  334. autosize={{ minRows: 6, maxRows: 12 }}
  335. />
  336. <Button onClick={submitNotice} loading={loadingInput['Notice']}>
  337. {t('设置公告')}
  338. </Button>
  339. <Form.TextArea
  340. label={t('用户协议')}
  341. placeholder={t(
  342. '在此输入用户协议内容,支持 Markdown & HTML 代码',
  343. )}
  344. field={'UserAgreement'}
  345. onChange={handleInputChange}
  346. style={{ fontFamily: 'JetBrains Mono, Consolas' }}
  347. autosize={{ minRows: 6, maxRows: 12 }}
  348. helpText={t('填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议')}
  349. />
  350. <Button onClick={submitUserAgreement} loading={loadingInput['UserAgreement']}>
  351. {t('设置用户协议')}
  352. </Button>
  353. <Form.TextArea
  354. label={t('隐私政策')}
  355. placeholder={t(
  356. '在此输入隐私政策内容,支持 Markdown & HTML 代码',
  357. )}
  358. field={'PrivacyPolicy'}
  359. onChange={handleInputChange}
  360. style={{ fontFamily: 'JetBrains Mono, Consolas' }}
  361. autosize={{ minRows: 6, maxRows: 12 }}
  362. helpText={t('填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策')}
  363. />
  364. <Button onClick={submitPrivacyPolicy} loading={loadingInput['PrivacyPolicy']}>
  365. {t('设置隐私政策')}
  366. </Button>
  367. </Form.Section>
  368. </Card>
  369. </Form>
  370. {/* 个性化设置 */}
  371. <Form
  372. values={inputs}
  373. getFormApi={(formAPI) => (formAPIPersonalization.current = formAPI)}
  374. >
  375. <Card>
  376. <Form.Section text={t('个性化设置')}>
  377. <Form.Input
  378. label={t('系统名称')}
  379. placeholder={t('在此输入系统名称')}
  380. field={'SystemName'}
  381. onChange={handleInputChange}
  382. />
  383. <Button
  384. onClick={submitSystemName}
  385. loading={loadingInput['SystemName']}
  386. >
  387. {t('设置系统名称')}
  388. </Button>
  389. <Form.Input
  390. label={t('Logo 图片地址')}
  391. placeholder={t('在此输入 Logo 图片地址')}
  392. field={'Logo'}
  393. onChange={handleInputChange}
  394. />
  395. <Button onClick={submitLogo} loading={loadingInput['Logo']}>
  396. {t('设置 Logo')}
  397. </Button>
  398. <Form.TextArea
  399. label={t('首页内容')}
  400. placeholder={t(
  401. '在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页',
  402. )}
  403. field={'HomePageContent'}
  404. onChange={handleInputChange}
  405. style={{ fontFamily: 'JetBrains Mono, Consolas' }}
  406. autosize={{ minRows: 6, maxRows: 12 }}
  407. />
  408. <Button
  409. onClick={() => submitOption('HomePageContent')}
  410. loading={loadingInput['HomePageContent']}
  411. >
  412. {t('设置首页内容')}
  413. </Button>
  414. <Form.TextArea
  415. label={t('关于')}
  416. placeholder={t(
  417. '在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面',
  418. )}
  419. field={'About'}
  420. onChange={handleInputChange}
  421. style={{ fontFamily: 'JetBrains Mono, Consolas' }}
  422. autosize={{ minRows: 6, maxRows: 12 }}
  423. />
  424. <Button onClick={submitAbout} loading={loadingInput['About']}>
  425. {t('设置关于')}
  426. </Button>
  427. {/* */}
  428. <Banner
  429. fullMode={false}
  430. type='info'
  431. description={t(
  432. '移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目',
  433. )}
  434. closeIcon={null}
  435. style={{ marginTop: 15 }}
  436. />
  437. <Form.Input
  438. label={t('页脚')}
  439. placeholder={t(
  440. '在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码',
  441. )}
  442. field={'Footer'}
  443. onChange={handleInputChange}
  444. />
  445. <Button onClick={submitFooter} loading={loadingInput['Footer']}>
  446. {t('设置页脚')}
  447. </Button>
  448. </Form.Section>
  449. </Card>
  450. </Form>
  451. </Col>
  452. <Modal
  453. title={t('新版本') + ':' + updateData.tag_name}
  454. visible={showUpdateModal}
  455. onCancel={() => setShowUpdateModal(false)}
  456. footer={[
  457. <Button
  458. key='details'
  459. type='primary'
  460. onClick={() => {
  461. setShowUpdateModal(false);
  462. openGitHubRelease();
  463. }}
  464. >
  465. {t('详情')}
  466. </Button>,
  467. ]}
  468. >
  469. <div dangerouslySetInnerHTML={{ __html: updateData.content }}></div>
  470. </Modal>
  471. </Row>
  472. );
  473. };
  474. export default OtherSetting;