OtherSetting.js 13 KB

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