OtherSetting.js 12 KB

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