OtherSetting.js 14 KB

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