Compare commits

...

8 Commits

Author SHA1 Message Date
1808837298@qq.com
1819c4d5f5 feat(error): Enhance error handling with optional detailed error messages 2025-03-11 17:25:06 +08:00
1808837298@qq.com
6f24dddcb2 feat(relay): Add pass-through request option for global settings 2025-03-11 17:02:35 +08:00
1808837298@qq.com
8de29fbb83 Merge remote-tracking branch 'origin/main' 2025-03-11 16:41:18 +08:00
Calcium-Ion
f2163acf2b Merge pull request #849 from OrdinarySF/main
feat(setting): add 'Document Link' option i18n support
2025-03-11 16:27:37 +08:00
Ordinary
5259acfacd feat(setting): add 'Document Link' option i18n support 2025-03-11 08:22:59 +00:00
1808837298@qq.com
3122b8a36a fix: Improve mobile layout and scrolling behavior 2025-03-11 15:05:23 +08:00
1808837298@qq.com
bbe7223a85 Merge remote-tracking branch 'origin/main' 2025-03-11 14:55:56 +08:00
1808837298@qq.com
2af05c166c feat: Improve route handling and dynamic chat navigation in SiderBar 2025-03-11 14:55:48 +08:00
16 changed files with 278 additions and 117 deletions

View File

@@ -125,7 +125,7 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
if resp != nil {
httpResp = resp.(*http.Response)
if httpResp.StatusCode != http.StatusOK {
err := service.RelayErrorHandler(httpResp)
err := service.RelayErrorHandler(httpResp, true)
return fmt.Errorf("status code %d: %s", httpResp.StatusCode, err.Error.Message), err
}
}

View File

@@ -117,7 +117,7 @@ func AudioHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
if resp != nil {
httpResp = resp.(*http.Response)
if httpResp.StatusCode != http.StatusOK {
openaiErr = service.RelayErrorHandler(httpResp)
openaiErr = service.RelayErrorHandler(httpResp, false)
// reset status code 重置状态码
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
return openaiErr

View File

@@ -155,7 +155,7 @@ func ImageHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
httpResp = resp.(*http.Response)
relayInfo.IsStream = relayInfo.IsStream || strings.HasPrefix(httpResp.Header.Get("Content-Type"), "text/event-stream")
if httpResp.StatusCode != http.StatusOK {
openaiErr := service.RelayErrorHandler(httpResp)
openaiErr := service.RelayErrorHandler(httpResp, false)
// reset status code 重置状态码
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
return openaiErr

View File

@@ -17,6 +17,7 @@ import (
"one-api/relay/helper"
"one-api/service"
"one-api/setting"
"one-api/setting/model_setting"
"strings"
"time"
@@ -152,38 +153,37 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
adaptor.Init(relayInfo)
var requestBody io.Reader
//if relayInfo.ChannelType == common.ChannelTypeOpenAI && !isModelMapped {
// body, err := common.GetRequestBody(c)
// if err != nil {
// return service.OpenAIErrorWrapperLocal(err, "get_request_body_failed", http.StatusInternalServerError)
// }
// requestBody = bytes.NewBuffer(body)
//} else {
//
//}
convertedRequest, err := adaptor.ConvertRequest(c, relayInfo, textRequest)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError)
if model_setting.GetGlobalSettings().PassThroughRequestEnabled {
body, err := common.GetRequestBody(c)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "get_request_body_failed", http.StatusInternalServerError)
}
requestBody = bytes.NewBuffer(body)
} else {
convertedRequest, err := adaptor.ConvertRequest(c, relayInfo, textRequest)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError)
}
jsonData, err := json.Marshal(convertedRequest)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
}
requestBody = bytes.NewBuffer(jsonData)
}
jsonData, err := json.Marshal(convertedRequest)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
}
requestBody = bytes.NewBuffer(jsonData)
statusCodeMappingStr := c.GetString("status_code_mapping")
var httpResp *http.Response
resp, err := adaptor.DoRequest(c, relayInfo, requestBody)
if err != nil {
return service.OpenAIErrorWrapper(err, "do_request_failed", http.StatusInternalServerError)
}
statusCodeMappingStr := c.GetString("status_code_mapping")
if resp != nil {
httpResp = resp.(*http.Response)
relayInfo.IsStream = relayInfo.IsStream || strings.HasPrefix(httpResp.Header.Get("Content-Type"), "text/event-stream")
if httpResp.StatusCode != http.StatusOK {
openaiErr = service.RelayErrorHandler(httpResp)
openaiErr = service.RelayErrorHandler(httpResp, false)
// reset status code 重置状态码
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
return openaiErr

View File

@@ -98,7 +98,7 @@ func EmbeddingHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode)
if resp != nil {
httpResp = resp.(*http.Response)
if httpResp.StatusCode != http.StatusOK {
openaiErr = service.RelayErrorHandler(httpResp)
openaiErr = service.RelayErrorHandler(httpResp, false)
// reset status code 重置状态码
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
return openaiErr

View File

@@ -90,7 +90,7 @@ func RerankHelper(c *gin.Context, relayMode int) (openaiErr *dto.OpenAIErrorWith
if resp != nil {
httpResp = resp.(*http.Response)
if httpResp.StatusCode != http.StatusOK {
openaiErr = service.RelayErrorHandler(httpResp)
openaiErr = service.RelayErrorHandler(httpResp, false)
// reset status code 重置状态码
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
return openaiErr

View File

@@ -50,7 +50,7 @@ func OpenAIErrorWrapperLocal(err error, code string, statusCode int) *dto.OpenAI
return openaiErr
}
func RelayErrorHandler(resp *http.Response) (errWithStatusCode *dto.OpenAIErrorWithStatusCode) {
func RelayErrorHandler(resp *http.Response, showBodyWhenFail bool) (errWithStatusCode *dto.OpenAIErrorWithStatusCode) {
errWithStatusCode = &dto.OpenAIErrorWithStatusCode{
StatusCode: resp.StatusCode,
Error: dto.OpenAIError{
@@ -70,6 +70,11 @@ func RelayErrorHandler(resp *http.Response) (errWithStatusCode *dto.OpenAIErrorW
var errResponse dto.GeneralErrorResponse
err = json.Unmarshal(responseBody, &errResponse)
if err != nil {
if showBodyWhenFail {
errWithStatusCode.Error.Message = string(responseBody)
} else {
errWithStatusCode.Error.Message = fmt.Sprintf("bad response status code %d", resp.StatusCode)
}
return
}
if errResponse.Error.Message != "" {

View File

@@ -0,0 +1,26 @@
package model_setting
import (
"one-api/setting/config"
)
type GlobalSettings struct {
PassThroughRequestEnabled bool `json:"pass_through_request_enabled"`
}
// 默认配置
var defaultOpenaiSettings = GlobalSettings{
PassThroughRequestEnabled: false,
}
// 全局实例
var globalSettings = defaultOpenaiSettings
func init() {
// 注册到全局配置管理器
config.GlobalConfig.Register("global", &globalSettings)
}
func GetGlobalSettings() *GlobalSettings {
return &globalSettings
}

View File

@@ -1,5 +1,5 @@
import React, { lazy, Suspense, useContext, useEffect } from 'react';
import { Route, Routes } from 'react-router-dom';
import { Route, Routes, useLocation } from 'react-router-dom';
import Loading from './components/Loading';
import User from './pages/User';
import { PrivateRoute } from './components/PrivateRoute';
@@ -8,10 +8,8 @@ import LoginForm from './components/LoginForm';
import NotFound from './pages/NotFound';
import Setting from './pages/Setting';
import EditUser from './pages/User/EditUser';
import { getLogo, getSystemName } from './helpers';
import PasswordResetForm from './components/PasswordResetForm';
import PasswordResetConfirm from './components/PasswordResetConfirm';
import { UserContext } from './context/User';
import Channel from './pages/Channel';
import Token from './pages/Token';
import EditChannel from './pages/Channel/EditChannel';
@@ -26,10 +24,6 @@ import Pricing from './pages/Pricing/index.js';
import Task from "./pages/Task/index.js";
import Playground from './pages/Playground/Playground.js';
import OAuth2Callback from "./components/OAuth2Callback.js";
import { useTranslation } from 'react-i18next';
import { StatusContext } from './context/Status';
import { setStatusData } from './helpers/data.js';
import { API, showError } from './helpers';
import PersonalSetting from './components/PersonalSetting.js';
const Home = lazy(() => import('./pages/Home'));
@@ -37,13 +31,15 @@ const Detail = lazy(() => import('./pages/Detail'));
const About = lazy(() => import('./pages/About'));
function App() {
const location = useLocation();
return (
<>
<Routes>
<Route
path='/'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Home />
</Suspense>
}
@@ -59,7 +55,7 @@ function App() {
<Route
path='/channel/edit/:id'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<EditChannel />
</Suspense>
}
@@ -67,7 +63,7 @@ function App() {
<Route
path='/channel/add'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<EditChannel />
</Suspense>
}
@@ -107,7 +103,7 @@ function App() {
<Route
path='/user/edit/:id'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<EditUser />
</Suspense>
}
@@ -115,7 +111,7 @@ function App() {
<Route
path='/user/edit'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<EditUser />
</Suspense>
}
@@ -123,7 +119,7 @@ function App() {
<Route
path='/user/reset'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<PasswordResetConfirm />
</Suspense>
}
@@ -131,7 +127,7 @@ function App() {
<Route
path='/login'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<LoginForm />
</Suspense>
}
@@ -139,7 +135,7 @@ function App() {
<Route
path='/register'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<RegisterForm />
</Suspense>
}
@@ -147,7 +143,7 @@ function App() {
<Route
path='/reset'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<PasswordResetForm />
</Suspense>
}
@@ -155,7 +151,7 @@ function App() {
<Route
path='/oauth/github'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<OAuth2Callback type='github'></OAuth2Callback>
</Suspense>
}
@@ -163,7 +159,7 @@ function App() {
<Route
path='/oauth/linuxdo'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<OAuth2Callback type='linuxdo'></OAuth2Callback>
</Suspense>
}
@@ -172,7 +168,7 @@ function App() {
path='/setting'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Setting />
</Suspense>
</PrivateRoute>
@@ -182,7 +178,7 @@ function App() {
path='/personal'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<PersonalSetting />
</Suspense>
</PrivateRoute>
@@ -192,7 +188,7 @@ function App() {
path='/topup'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<TopUp />
</Suspense>
</PrivateRoute>
@@ -210,7 +206,7 @@ function App() {
path='/detail'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Detail />
</Suspense>
</PrivateRoute>
@@ -220,7 +216,7 @@ function App() {
path='/midjourney'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Midjourney />
</Suspense>
</PrivateRoute>
@@ -230,7 +226,7 @@ function App() {
path='/task'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Task />
</Suspense>
</PrivateRoute>
@@ -239,7 +235,7 @@ function App() {
<Route
path='/pricing'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Pricing />
</Suspense>
}
@@ -247,7 +243,7 @@ function App() {
<Route
path='/about'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<About />
</Suspense>
}
@@ -255,7 +251,7 @@ function App() {
<Route
path='/chat/:id?'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Chat />
</Suspense>
}
@@ -265,7 +261,7 @@ function App() {
path='/chat2link'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Chat2Link />
</Suspense>
</PrivateRoute>

View File

@@ -61,7 +61,7 @@ const FooterBar = () => {
return (
<div style={{
textAlign: 'center',
paddingBottom: styleState?.isMobile ? '112px' : '5px',
paddingBottom: '5px',
}}>
{footer ? (
<div

View File

@@ -6,6 +6,7 @@ import { API, showError, showSuccess } from '../helpers';
import { useTranslation } from 'react-i18next';
import SettingGeminiModel from '../pages/Setting/Model/SettingGeminiModel.js';
import SettingClaudeModel from '../pages/Setting/Model/SettingClaudeModel.js';
import SettingGlobalModel from '../pages/Setting/Model/SettingGlobalModel.js';
const ModelSetting = () => {
const { t } = useTranslation();
@@ -16,6 +17,7 @@ const ModelSetting = () => {
'claude.thinking_adapter_enabled': true,
'claude.default_max_tokens': '',
'claude.thinking_adapter_budget_tokens_percentage': 0.8,
'global.pass_through_request_enabled': false,
});
let [loading, setLoading] = useState(false);
@@ -35,7 +37,7 @@ const ModelSetting = () => {
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
}
if (
item.key.endsWith('Enabled')
item.key.endsWith('Enabled') || item.key.endsWith('enabled')
) {
newInputs[item.key] = item.value === 'true' ? true : false;
} else {
@@ -67,6 +69,10 @@ const ModelSetting = () => {
return (
<>
<Spin spinning={loading} size='large'>
{/* OpenAI */}
<Card style={{ marginTop: '10px' }}>
<SettingGlobalModel options={inputs} refresh={onRefresh} />
</Card>
{/* Gemini */}
<Card style={{ marginTop: '10px' }}>
<SettingGeminiModel options={inputs} refresh={onRefresh} />

View File

@@ -75,13 +75,13 @@ const PageLayout = () => {
height: '100vh',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden'
overflow: styleState.isMobile ? 'visible' : 'hidden'
}}>
<Header style={{
padding: 0,
height: 'auto',
lineHeight: 'normal',
position: 'fixed',
position: styleState.isMobile ? 'sticky' : 'fixed',
width: '100%',
top: 0,
zIndex: 100,
@@ -90,9 +90,9 @@ const PageLayout = () => {
<HeaderBar />
</Header>
<Layout style={{
marginTop: '56px',
height: 'calc(100vh - 56px)',
overflow: 'auto',
marginTop: styleState.isMobile ? '0' : '56px',
height: styleState.isMobile ? 'auto' : 'calc(100vh - 56px)',
overflow: styleState.isMobile ? 'visible' : 'auto',
display: 'flex',
flexDirection: 'column'
}}>
@@ -121,10 +121,11 @@ const PageLayout = () => {
<Content
style={{
flex: '1 0 auto',
overflowY: 'auto',
overflowY: styleState.isMobile ? 'visible' : 'auto',
WebkitOverflowScrolling: 'touch',
padding: styleState.shouldInnerPadding? '24px': '0',
position: 'relative',
marginTop: styleState.isMobile ? '2px' : '0',
}}
>
<App />

View File

@@ -62,6 +62,25 @@ const iconStyle = (itemKey, selectedKeys) => {
};
};
// Define routerMap as a constant outside the component
const routerMap = {
home: '/',
channel: '/channel',
token: '/token',
redemption: '/redemption',
topup: '/topup',
user: '/user',
log: '/log',
midjourney: '/midjourney',
setting: '/setting',
about: '/about',
detail: '/detail',
pricing: '/pricing',
task: '/task',
playground: '/playground',
personal: '/personal',
};
const SiderBar = () => {
const { t } = useTranslation();
const [styleState, styleDispatch] = useContext(StyleContext);
@@ -76,6 +95,7 @@ const SiderBar = () => {
const theme = useTheme();
const setTheme = useSetTheme();
const location = useLocation();
const [routerMapState, setRouterMapState] = useState(routerMap);
// 预先计算所有可能的图标样式
const allItemKeys = useMemo(() => {
@@ -97,25 +117,6 @@ const SiderBar = () => {
return styles;
}, [allItemKeys, selectedKeys]);
const routerMap = {
home: '/',
channel: '/channel',
token: '/token',
redemption: '/redemption',
topup: '/topup',
user: '/user',
log: '/log',
midjourney: '/midjourney',
setting: '/setting',
about: '/about',
chat: '/chat',
detail: '/detail',
pricing: '/pricing',
task: '/task',
playground: '/playground',
personal: '/personal',
};
const workspaceItems = useMemo(
() => [
{
@@ -237,21 +238,24 @@ const SiderBar = () => {
[chatItems, t],
);
useEffect(() => {
const currentPath = location.pathname;
const matchingKey = Object.keys(routerMap).find(key => routerMap[key] === currentPath);
if (matchingKey) {
setSelectedKeys([matchingKey]);
} else if (currentPath.startsWith('/chat/')) {
setSelectedKeys(['chat']);
// Function to update router map with chat routes
const updateRouterMapWithChats = (chats) => {
const newRouterMap = { ...routerMap };
if (Array.isArray(chats) && chats.length > 0) {
for (let i = 0; i < chats.length; i++) {
newRouterMap['chat' + i] = '/chat/' + i;
}
}
}, [location.pathname]);
setRouterMapState(newRouterMap);
return newRouterMap;
};
// Update the useEffect for chat items
useEffect(() => {
let chats = localStorage.getItem('chats');
if (chats) {
// console.log(chats);
try {
chats = JSON.parse(chats);
if (Array.isArray(chats)) {
@@ -263,19 +267,44 @@ const SiderBar = () => {
chat.itemKey = 'chat' + i;
chat.to = '/chat/' + i;
}
// setRouterMap({ ...routerMap, chat: '/chat/' + i })
chatItems.push(chat);
}
setChatItems(chatItems);
// Update router map with chat routes
updateRouterMapWithChats(chats);
}
} catch (e) {
console.error(e);
showError('聊天数据解析失败')
}
}
}, []);
// Update the useEffect for route selection
useEffect(() => {
const currentPath = location.pathname;
let matchingKey = Object.keys(routerMapState).find(key => routerMapState[key] === currentPath);
// Handle chat routes
if (!matchingKey && currentPath.startsWith('/chat/')) {
const chatIndex = currentPath.split('/').pop();
if (!isNaN(chatIndex)) {
matchingKey = 'chat' + chatIndex;
} else {
matchingKey = 'chat';
}
}
// If we found a matching key, update the selected keys
if (matchingKey) {
setSelectedKeys([matchingKey]);
}
}, [location.pathname, routerMapState]);
useEffect(() => {
setIsCollapsed(styleState.siderCollapsed);
})
}, [styleState.siderCollapsed]);
// Custom divider style
const dividerStyle = {
@@ -322,14 +351,14 @@ const SiderBar = () => {
// 确保在收起侧边栏时有选中的项目,避免不必要的计算
if (selectedKeys.length === 0) {
const currentPath = location.pathname;
const matchingKey = Object.keys(routerMap).find(key => routerMap[key] === currentPath);
const matchingKey = Object.keys(routerMapState).find(key => routerMapState[key] === currentPath);
if (matchingKey) {
setSelectedKeys([matchingKey]);
} else if (currentPath.startsWith('/chat/')) {
setSelectedKeys(['chat']);
} else {
setSelectedKeys([]); // 默认选中首页
setSelectedKeys(['detail']); // 默认选中首页
}
}
}}
@@ -338,28 +367,10 @@ const SiderBar = () => {
hoverStyle={navItemHoverStyle}
selectedStyle={navItemSelectedStyle}
renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
let chats = localStorage.getItem('chats');
if (chats) {
chats = JSON.parse(chats);
if (Array.isArray(chats) && chats.length > 0) {
for (let i = 0; i < chats.length; i++) {
routerMap['chat' + i] = '/chat/' + i;
}
if (chats.length > 1) {
// delete /chat
if (routerMap['chat']) {
delete routerMap['chat'];
}
} else {
// rename /chat to /chat/0
routerMap['chat'] = '/chat/0';
}
}
}
return (
<Link
style={{ textDecoration: 'none' }}
to={routerMap[props.itemKey]}
to={routerMapState[props.itemKey] || routerMap[props.itemKey]}
>
{itemElement}
</Link>
@@ -466,7 +477,7 @@ const SiderBar = () => {
<Nav.Footer
style={{
paddingBottom: styleState?.isMobile ? '112px' : '20px',
paddingBottom: styleState?.isMobile ? '112px' : '',
}}
collapseButton={true}
collapseText={(collapsed)=>

View File

@@ -1,5 +1,6 @@
{
"主页": "Home",
"文档": "Docs",
"控制台": "Console",
"$%.6f 额度": "$%.6f quota",
"%d 点额度": "%d point quota",
@@ -192,6 +193,8 @@
"通用设置": "General Settings",
"充值链接": "Recharge Link",
"例如发卡网站的购买链接": "E.g., purchase link from card issuing website",
"文档地址": "Document Link",
"例如 https://docs.newapi.pro": "E.g., https://docs.newapi.pro",
"聊天页面链接": "Chat Page Link",
"例如 ChatGPT Next Web 的部署地址": "E.g., ChatGPT Next Web deployment address",
"单位美元额度": "Quota per USD",

View File

@@ -1,7 +1,6 @@
body {
margin: 0;
padding-top: 0;
overflow: hidden;
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei',
sans-serif;
-webkit-font-smoothing: antialiased;
@@ -13,7 +12,8 @@ body {
}
#root {
height: 100vh;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
@@ -86,8 +86,23 @@ body {
/* 确保移动端内容可滚动 */
.semi-layout-content {
-webkit-overflow-scrolling: touch !important;
overscroll-behavior-y: auto !important;
}
/* 修复移动端下拉刷新 */
body {
overflow: visible !important;
overscroll-behavior-y: auto !important;
position: static !important;
height: 100% !important;
}
/* 确保内容区域在移动端可以正常滚动 */
#root {
overflow: visible !important;
height: 100% !important;
}
/* 隐藏在移动设备上 */
.hide-on-mobile {
display: none !important;

View File

@@ -0,0 +1,98 @@
import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
import {
compareObjects,
API,
showError,
showSuccess,
showWarning, verifyJSON
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function SettingGlobalModel(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
'global.pass_through_request_enabled': false,
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
value = String(inputs[item.key]);
} else {
value = inputs[item.key];
}
return API.put('/api/option/', {
key: item.key,
value,
});
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
}
showSuccess(t('保存成功'));
props.refresh();
})
.catch(() => {
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
});
}
useEffect(() => {
const currentInputs = {};
for (let key in props.options) {
if (Object.keys(inputs).includes(key)) {
currentInputs[key] = props.options[key];
}
}
setInputs(currentInputs);
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
}, [props.options]);
return (
<>
<Spin spinning={loading}>
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={t('全局设置')}>
<Row>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
label={t('启用请求透传')}
field={'global.pass_through_request_enabled'}
onChange={(value) => setInputs({ ...inputs, 'global.pass_through_request_enabled': value })}
extraText={'开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启'}
/>
</Col>
</Row>
<Row>
<Button size='default' onClick={onSubmit}>
{t('保存')}
</Button>
</Row>
</Form.Section>
</Form>
</Spin>
</>
);
}