Spaces:
Build error
Build error
| /* | |
| Copyright (C) 2025 QuantumNous | |
| This program is free software: you can redistribute it and/or modify | |
| it under the terms of the GNU Affero General Public License as | |
| published by the Free Software Foundation, either version 3 of the | |
| License, or (at your option) any later version. | |
| This program is distributed in the hope that it will be useful, | |
| but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| GNU Affero General Public License for more details. | |
| You should have received a copy of the GNU Affero General Public License | |
| along with this program. If not, see <https://www.gnu.org/licenses/>. | |
| For commercial licensing, please contact support@quantumnous.com | |
| */ | |
| import React, { useContext, useEffect, useRef, useState } from 'react'; | |
| import { | |
| Banner, | |
| Button, | |
| Col, | |
| Form, | |
| Row, | |
| Modal, | |
| Space, | |
| Card, | |
| } from '@douyinfe/semi-ui'; | |
| import { API, showError, showSuccess, timestamp2string } from '../../helpers'; | |
| import { marked } from 'marked'; | |
| import { useTranslation } from 'react-i18next'; | |
| import { StatusContext } from '../../context/Status'; | |
| import Text from '@douyinfe/semi-ui/lib/es/typography/text'; | |
| const OtherSetting = () => { | |
| const { t } = useTranslation(); | |
| let [inputs, setInputs] = useState({ | |
| Notice: '', | |
| SystemName: '', | |
| Logo: '', | |
| Footer: '', | |
| About: '', | |
| HomePageContent: '', | |
| }); | |
| let [loading, setLoading] = useState(false); | |
| const [showUpdateModal, setShowUpdateModal] = useState(false); | |
| const [statusState, statusDispatch] = useContext(StatusContext); | |
| const [updateData, setUpdateData] = useState({ | |
| tag_name: '', | |
| content: '', | |
| }); | |
| const updateOption = async (key, value) => { | |
| setLoading(true); | |
| const res = await API.put('/api/option/', { | |
| key, | |
| value, | |
| }); | |
| const { success, message } = res.data; | |
| if (success) { | |
| setInputs((inputs) => ({ ...inputs, [key]: value })); | |
| } else { | |
| showError(message); | |
| } | |
| setLoading(false); | |
| }; | |
| const [loadingInput, setLoadingInput] = useState({ | |
| Notice: false, | |
| SystemName: false, | |
| Logo: false, | |
| HomePageContent: false, | |
| About: false, | |
| Footer: false, | |
| CheckUpdate: false, | |
| }); | |
| const handleInputChange = async (value, e) => { | |
| const name = e.target.id; | |
| setInputs((inputs) => ({ ...inputs, [name]: value })); | |
| }; | |
| // 通用设置 | |
| const formAPISettingGeneral = useRef(); | |
| // 通用设置 - Notice | |
| const submitNotice = async () => { | |
| try { | |
| setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: true })); | |
| await updateOption('Notice', inputs.Notice); | |
| showSuccess(t('公告已更新')); | |
| } catch (error) { | |
| console.error(t('公告更新失败'), error); | |
| showError(t('公告更新失败')); | |
| } finally { | |
| setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: false })); | |
| } | |
| }; | |
| // 个性化设置 | |
| const formAPIPersonalization = useRef(); | |
| // 个性化设置 - SystemName | |
| const submitSystemName = async () => { | |
| try { | |
| setLoadingInput((loadingInput) => ({ | |
| ...loadingInput, | |
| SystemName: true, | |
| })); | |
| await updateOption('SystemName', inputs.SystemName); | |
| showSuccess(t('系统名称已更新')); | |
| } catch (error) { | |
| console.error(t('系统名称更新失败'), error); | |
| showError(t('系统名称更新失败')); | |
| } finally { | |
| setLoadingInput((loadingInput) => ({ | |
| ...loadingInput, | |
| SystemName: false, | |
| })); | |
| } | |
| }; | |
| // 个性化设置 - Logo | |
| const submitLogo = async () => { | |
| try { | |
| setLoadingInput((loadingInput) => ({ ...loadingInput, Logo: true })); | |
| await updateOption('Logo', inputs.Logo); | |
| showSuccess('Logo 已更新'); | |
| } catch (error) { | |
| console.error('Logo 更新失败', error); | |
| showError('Logo 更新失败'); | |
| } finally { | |
| setLoadingInput((loadingInput) => ({ ...loadingInput, Logo: false })); | |
| } | |
| }; | |
| // 个性化设置 - 首页内容 | |
| const submitOption = async (key) => { | |
| try { | |
| setLoadingInput((loadingInput) => ({ | |
| ...loadingInput, | |
| HomePageContent: true, | |
| })); | |
| await updateOption(key, inputs[key]); | |
| showSuccess('首页内容已更新'); | |
| } catch (error) { | |
| console.error('首页内容更新失败', error); | |
| showError('首页内容更新失败'); | |
| } finally { | |
| setLoadingInput((loadingInput) => ({ | |
| ...loadingInput, | |
| HomePageContent: false, | |
| })); | |
| } | |
| }; | |
| // 个性化设置 - 关于 | |
| const submitAbout = async () => { | |
| try { | |
| setLoadingInput((loadingInput) => ({ ...loadingInput, About: true })); | |
| await updateOption('About', inputs.About); | |
| showSuccess('关于内容已更新'); | |
| } catch (error) { | |
| console.error('关于内容更新失败', error); | |
| showError('关于内容更新失败'); | |
| } finally { | |
| setLoadingInput((loadingInput) => ({ ...loadingInput, About: false })); | |
| } | |
| }; | |
| // 个性化设置 - 页脚 | |
| const submitFooter = async () => { | |
| try { | |
| setLoadingInput((loadingInput) => ({ ...loadingInput, Footer: true })); | |
| await updateOption('Footer', inputs.Footer); | |
| showSuccess('页脚内容已更新'); | |
| } catch (error) { | |
| console.error('页脚内容更新失败', error); | |
| showError('页脚内容更新失败'); | |
| } finally { | |
| setLoadingInput((loadingInput) => ({ ...loadingInput, Footer: false })); | |
| } | |
| }; | |
| const checkUpdate = async () => { | |
| try { | |
| setLoadingInput((loadingInput) => ({ | |
| ...loadingInput, | |
| CheckUpdate: true, | |
| })); | |
| // Use a CORS proxy to avoid direct cross-origin requests to GitHub API | |
| // Option 1: Use a public CORS proxy service | |
| // const proxyUrl = 'https://cors-anywhere.herokuapp.com/'; | |
| // const res = await API.get( | |
| // `${proxyUrl}https://api.github.com/repos/Calcium-Ion/new-api/releases/latest`, | |
| // ); | |
| // Option 2: Use the JSON proxy approach which often works better with GitHub API | |
| const res = await fetch( | |
| 'https://api.github.com/repos/Calcium-Ion/new-api/releases/latest', | |
| { | |
| headers: { | |
| Accept: 'application/json', | |
| 'Content-Type': 'application/json', | |
| // Adding User-Agent which is often required by GitHub API | |
| 'User-Agent': 'new-api-update-checker', | |
| }, | |
| }, | |
| ).then((response) => response.json()); | |
| // Option 3: Use a local proxy endpoint | |
| // Create a cached version of the response to avoid frequent GitHub API calls | |
| // const res = await API.get('/api/status/github-latest-release'); | |
| const { tag_name, body } = res; | |
| if (tag_name === statusState?.status?.version) { | |
| showSuccess(`已是最新版本:${tag_name}`); | |
| } else { | |
| setUpdateData({ | |
| tag_name: tag_name, | |
| content: marked.parse(body), | |
| }); | |
| setShowUpdateModal(true); | |
| } | |
| } catch (error) { | |
| console.error('Failed to check for updates:', error); | |
| showError('检查更新失败,请稍后再试'); | |
| } finally { | |
| setLoadingInput((loadingInput) => ({ | |
| ...loadingInput, | |
| CheckUpdate: false, | |
| })); | |
| } | |
| }; | |
| const getOptions = async () => { | |
| const res = await API.get('/api/option/'); | |
| const { success, message, data } = res.data; | |
| if (success) { | |
| let newInputs = {}; | |
| data.forEach((item) => { | |
| if (item.key in inputs) { | |
| newInputs[item.key] = item.value; | |
| } | |
| }); | |
| setInputs(newInputs); | |
| formAPISettingGeneral.current.setValues(newInputs); | |
| formAPIPersonalization.current.setValues(newInputs); | |
| } else { | |
| showError(message); | |
| } | |
| }; | |
| useEffect(() => { | |
| getOptions(); | |
| }, []); | |
| // Function to open GitHub release page | |
| const openGitHubRelease = () => { | |
| window.open( | |
| `https://github.com/Calcium-Ion/new-api/releases/tag/${updateData.tag_name}`, | |
| '_blank', | |
| ); | |
| }; | |
| const getStartTimeString = () => { | |
| const timestamp = statusState?.status?.start_time; | |
| return statusState.status ? timestamp2string(timestamp) : ''; | |
| }; | |
| return ( | |
| <Row> | |
| <Col | |
| span={24} | |
| style={{ | |
| marginTop: '10px', | |
| display: 'flex', | |
| flexDirection: 'column', | |
| gap: '10px', | |
| }} | |
| > | |
| {/* 版本信息 */} | |
| <Form> | |
| <Card> | |
| <Form.Section text={t('系统信息')}> | |
| <Row> | |
| <Col span={16}> | |
| <Space> | |
| <Text> | |
| {t('当前版本')}: | |
| {statusState?.status?.version || t('未知')} | |
| </Text> | |
| <Button | |
| type='primary' | |
| onClick={checkUpdate} | |
| loading={loadingInput['CheckUpdate']} | |
| > | |
| {t('检查更新')} | |
| </Button> | |
| </Space> | |
| </Col> | |
| </Row> | |
| <Row> | |
| <Col span={16}> | |
| <Text> | |
| {t('启动时间')}:{getStartTimeString()} | |
| </Text> | |
| </Col> | |
| </Row> | |
| </Form.Section> | |
| </Card> | |
| </Form> | |
| {/* 通用设置 */} | |
| <Form | |
| values={inputs} | |
| getFormApi={(formAPI) => (formAPISettingGeneral.current = formAPI)} | |
| > | |
| <Card> | |
| <Form.Section text={t('通用设置')}> | |
| <Form.TextArea | |
| label={t('公告')} | |
| placeholder={t( | |
| '在此输入新的公告内容,支持 Markdown & HTML 代码', | |
| )} | |
| field={'Notice'} | |
| onChange={handleInputChange} | |
| style={{ fontFamily: 'JetBrains Mono, Consolas' }} | |
| autosize={{ minRows: 6, maxRows: 12 }} | |
| /> | |
| <Button onClick={submitNotice} loading={loadingInput['Notice']}> | |
| {t('设置公告')} | |
| </Button> | |
| </Form.Section> | |
| </Card> | |
| </Form> | |
| {/* 个性化设置 */} | |
| <Form | |
| values={inputs} | |
| getFormApi={(formAPI) => (formAPIPersonalization.current = formAPI)} | |
| > | |
| <Card> | |
| <Form.Section text={t('个性化设置')}> | |
| <Form.Input | |
| label={t('系统名称')} | |
| placeholder={t('在此输入系统名称')} | |
| field={'SystemName'} | |
| onChange={handleInputChange} | |
| /> | |
| <Button | |
| onClick={submitSystemName} | |
| loading={loadingInput['SystemName']} | |
| > | |
| {t('设置系统名称')} | |
| </Button> | |
| <Form.Input | |
| label={t('Logo 图片地址')} | |
| placeholder={t('在此输入 Logo 图片地址')} | |
| field={'Logo'} | |
| onChange={handleInputChange} | |
| /> | |
| <Button onClick={submitLogo} loading={loadingInput['Logo']}> | |
| {t('设置 Logo')} | |
| </Button> | |
| <Form.TextArea | |
| label={t('首页内容')} | |
| placeholder={t( | |
| '在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页', | |
| )} | |
| field={'HomePageContent'} | |
| onChange={handleInputChange} | |
| style={{ fontFamily: 'JetBrains Mono, Consolas' }} | |
| autosize={{ minRows: 6, maxRows: 12 }} | |
| /> | |
| <Button | |
| onClick={() => submitOption('HomePageContent')} | |
| loading={loadingInput['HomePageContent']} | |
| > | |
| {t('设置首页内容')} | |
| </Button> | |
| <Form.TextArea | |
| label={t('关于')} | |
| placeholder={t( | |
| '在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面', | |
| )} | |
| field={'About'} | |
| onChange={handleInputChange} | |
| style={{ fontFamily: 'JetBrains Mono, Consolas' }} | |
| autosize={{ minRows: 6, maxRows: 12 }} | |
| /> | |
| <Button onClick={submitAbout} loading={loadingInput['About']}> | |
| {t('设置关于')} | |
| </Button> | |
| {/* */} | |
| <Banner | |
| fullMode={false} | |
| type='info' | |
| description={t( | |
| '移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目', | |
| )} | |
| closeIcon={null} | |
| style={{ marginTop: 15 }} | |
| /> | |
| <Form.Input | |
| label={t('页脚')} | |
| placeholder={t( | |
| '在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码', | |
| )} | |
| field={'Footer'} | |
| onChange={handleInputChange} | |
| /> | |
| <Button onClick={submitFooter} loading={loadingInput['Footer']}> | |
| {t('设置页脚')} | |
| </Button> | |
| </Form.Section> | |
| </Card> | |
| </Form> | |
| </Col> | |
| <Modal | |
| title={t('新版本') + ':' + updateData.tag_name} | |
| visible={showUpdateModal} | |
| onCancel={() => setShowUpdateModal(false)} | |
| footer={[ | |
| <Button | |
| key='details' | |
| type='primary' | |
| onClick={() => { | |
| setShowUpdateModal(false); | |
| openGitHubRelease(); | |
| }} | |
| > | |
| {t('详情')} | |
| </Button>, | |
| ]} | |
| > | |
| <div dangerouslySetInnerHTML={{ __html: updateData.content }}></div> | |
| </Modal> | |
| </Row> | |
| ); | |
| }; | |
| export default OtherSetting; | |