Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
a9982ef2
authored
Feb 05, 2026
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Remove deprecated components and hooks
parent
d13fa743
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
0 additions
and
1835 deletions
+0
-1835
.gitattributes
+0
-1
web/src/components/common/examples/ChannelKeyViewExample.jsx
+0
-113
web/src/components/common/modals/TwoFactorAuthModal.jsx
+0
-148
web/src/components/playground/index.js
+0
-40
web/src/components/settings/personal/cards/ModelsList.jsx
+0
-280
web/src/components/table/models/ModelsDescription.jsx
+0
-44
web/src/components/table/users/modals/BindSubscriptionModal.jsx
+0
-123
web/src/hooks/model-deployments/useDeploymentResources.js
+0
-312
web/src/hooks/model-deployments/useEnhancedDeploymentActions.jsx
+0
-286
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx
+0
-488
No files found.
.gitattributes
View file @
a9982ef2
...
@@ -35,5 +35,4 @@
...
@@ -35,5 +35,4 @@
# GitHub Linguist - Language Detection
# GitHub Linguist - Language Detection
# ============================================
# ============================================
# Mark web frontend as vendored so GitHub recognizes this as a Go project
# Mark web frontend as vendored so GitHub recognizes this as a Go project
web/** linguist-vendored
electron/** linguist-vendored
electron/** linguist-vendored
web/src/components/common/examples/ChannelKeyViewExample.jsx
deleted
100644 → 0
View file @
d13fa743
/*
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
,
{
useState
}
from
'react'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
Button
,
Modal
}
from
'@douyinfe/semi-ui'
;
import
{
useSecureVerification
}
from
'../../../hooks/common/useSecureVerification'
;
import
{
createApiCalls
}
from
'../../../services/secureVerification'
;
import
SecureVerificationModal
from
'../modals/SecureVerificationModal'
;
import
ChannelKeyDisplay
from
'../ui/ChannelKeyDisplay'
;
/**
* 渠道密钥查看组件使用示例
* 展示如何使用通用安全验证系统
*/
const
ChannelKeyViewExample
=
({
channelId
})
=>
{
const
{
t
}
=
useTranslation
();
const
[
keyData
,
setKeyData
]
=
useState
(
''
);
const
[
showKeyModal
,
setShowKeyModal
]
=
useState
(
false
);
// 使用通用安全验证 Hook
const
{
isModalVisible
,
verificationMethods
,
verificationState
,
startVerification
,
executeVerification
,
cancelVerification
,
setVerificationCode
,
switchVerificationMethod
,
}
=
useSecureVerification
({
onSuccess
:
(
result
)
=>
{
// 验证成功后处理结果
if
(
result
.
success
&&
result
.
data
?.
key
)
{
setKeyData
(
result
.
data
.
key
);
setShowKeyModal
(
true
);
}
},
successMessage
:
t
(
'密钥获取成功'
),
});
// 开始查看密钥流程
const
handleViewKey
=
async
()
=>
{
const
apiCall
=
createApiCalls
.
viewChannelKey
(
channelId
);
await
startVerification
(
apiCall
,
{
title
:
t
(
'查看渠道密钥'
),
description
:
t
(
'为了保护账户安全,请验证您的身份。'
),
preferredMethod
:
'passkey'
,
// 可以指定首选验证方式
});
};
return
(
<>
{
/* 查看密钥按钮 */
}
<
Button
type=
'primary'
theme=
'outline'
onClick=
{
handleViewKey
}
>
{
t
(
'查看密钥'
)
}
</
Button
>
{
/* 安全验证模态框 */
}
<
SecureVerificationModal
visible=
{
isModalVisible
}
verificationMethods=
{
verificationMethods
}
verificationState=
{
verificationState
}
onVerify=
{
executeVerification
}
onCancel=
{
cancelVerification
}
onCodeChange=
{
setVerificationCode
}
onMethodSwitch=
{
switchVerificationMethod
}
title=
{
verificationState
.
title
}
description=
{
verificationState
.
description
}
/>
{
/* 密钥显示模态框 */
}
<
Modal
title=
{
t
(
'渠道密钥信息'
)
}
visible=
{
showKeyModal
}
onCancel=
{
()
=>
setShowKeyModal
(
false
)
}
footer=
{
<
Button
type=
'primary'
onClick=
{
()
=>
setShowKeyModal
(
false
)
}
>
{
t
(
'完成'
)
}
</
Button
>
}
width=
{
700
}
style=
{
{
maxWidth
:
'90vw'
}
}
>
<
ChannelKeyDisplay
keyData=
{
keyData
}
showSuccessIcon=
{
true
}
successText=
{
t
(
'密钥获取成功'
)
}
showWarning=
{
true
}
/>
</
Modal
>
</>
);
};
export
default
ChannelKeyViewExample
;
web/src/components/common/modals/TwoFactorAuthModal.jsx
deleted
100644 → 0
View file @
d13fa743
/*
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
from
'react'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
Modal
,
Button
,
Input
,
Typography
}
from
'@douyinfe/semi-ui'
;
/**
* 可复用的两步验证模态框组件
* @param {Object} props
* @param {boolean} props.visible - 是否显示模态框
* @param {string} props.code - 验证码值
* @param {boolean} props.loading - 是否正在验证
* @param {Function} props.onCodeChange - 验证码变化回调
* @param {Function} props.onVerify - 验证回调
* @param {Function} props.onCancel - 取消回调
* @param {string} props.title - 模态框标题
* @param {string} props.description - 验证描述文本
* @param {string} props.placeholder - 输入框占位文本
*/
const
TwoFactorAuthModal
=
({
visible
,
code
,
loading
,
onCodeChange
,
onVerify
,
onCancel
,
title
,
description
,
placeholder
,
})
=>
{
const
{
t
}
=
useTranslation
();
const
handleKeyDown
=
(
e
)
=>
{
if
(
e
.
key
===
'Enter'
&&
code
&&
!
loading
)
{
onVerify
();
}
};
return
(
<
Modal
title=
{
<
div
className=
'flex items-center'
>
<
div
className=
'w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center mr-3'
>
<
svg
className=
'w-4 h-4 text-blue-600 dark:text-blue-400'
fill=
'currentColor'
viewBox=
'0 0 20 20'
>
<
path
fillRule=
'evenodd'
d=
'M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z'
clipRule=
'evenodd'
/>
</
svg
>
</
div
>
{
title
||
t
(
'安全验证'
)
}
</
div
>
}
visible=
{
visible
}
onCancel=
{
onCancel
}
footer=
{
<>
<
Button
onClick=
{
onCancel
}
>
{
t
(
'取消'
)
}
</
Button
>
<
Button
type=
'primary'
loading=
{
loading
}
disabled=
{
!
code
||
loading
}
onClick=
{
onVerify
}
>
{
t
(
'验证'
)
}
</
Button
>
</>
}
width=
{
500
}
style=
{
{
maxWidth
:
'90vw'
}
}
>
<
div
className=
'space-y-6'
>
{
/* 安全提示 */
}
<
div
className=
'bg-blue-50 dark:bg-blue-900 rounded-lg p-4'
>
<
div
className=
'flex items-start'
>
<
svg
className=
'w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 mr-3 flex-shrink-0'
fill=
'currentColor'
viewBox=
'0 0 20 20'
>
<
path
fillRule=
'evenodd'
d=
'M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z'
clipRule=
'evenodd'
/>
</
svg
>
<
div
>
<
Typography
.
Text
strong
className=
'text-blue-800 dark:text-blue-200'
>
{
t
(
'安全验证'
)
}
</
Typography
.
Text
>
<
Typography
.
Text
className=
'block text-blue-700 dark:text-blue-300 text-sm mt-1'
>
{
description
||
t
(
'为了保护账户安全,请验证您的两步验证码。'
)
}
</
Typography
.
Text
>
</
div
>
</
div
>
</
div
>
{
/* 验证码输入 */
}
<
div
>
<
Typography
.
Text
strong
className=
'block mb-2'
>
{
t
(
'验证身份'
)
}
</
Typography
.
Text
>
<
Input
placeholder=
{
placeholder
||
t
(
'请输入认证器验证码或备用码'
)
}
value=
{
code
}
onChange=
{
onCodeChange
}
size=
'large'
maxLength=
{
8
}
onKeyDown=
{
handleKeyDown
}
autoFocus
/>
<
Typography
.
Text
type=
'tertiary'
size=
'small'
className=
'mt-2 block'
>
{
t
(
'支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。'
,
)
}
</
Typography
.
Text
>
</
div
>
</
div
>
</
Modal
>
);
};
export
default
TwoFactorAuthModal
;
web/src/components/playground/index.js
deleted
100644 → 0
View file @
d13fa743
/*
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
*/
export
{
default
as
SettingsPanel
}
from
'./SettingsPanel'
;
export
{
default
as
ChatArea
}
from
'./ChatArea'
;
export
{
default
as
DebugPanel
}
from
'./DebugPanel'
;
export
{
default
as
MessageContent
}
from
'./MessageContent'
;
export
{
default
as
MessageActions
}
from
'./MessageActions'
;
export
{
default
as
CustomInputRender
}
from
'./CustomInputRender'
;
export
{
default
as
SSEViewer
}
from
'./SSEViewer'
;
export
{
default
as
ParameterControl
}
from
'./ParameterControl'
;
export
{
default
as
ImageUrlInput
}
from
'./ImageUrlInput'
;
export
{
default
as
FloatingButtons
}
from
'./FloatingButtons'
;
export
{
default
as
ConfigManager
}
from
'./ConfigManager'
;
export
{
saveConfig
,
loadConfig
,
clearConfig
,
hasStoredConfig
,
getConfigTimestamp
,
exportConfig
,
importConfig
,
}
from
'./configStorage'
;
web/src/components/settings/personal/cards/ModelsList.jsx
deleted
100644 → 0
View file @
d13fa743
/*
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
,
{
useState
,
useEffect
}
from
'react'
;
import
{
Empty
,
Skeleton
,
Space
,
Tag
,
Collapsible
,
Tabs
,
TabPane
,
Typography
,
Avatar
,
}
from
'@douyinfe/semi-ui'
;
import
{
IllustrationNoContent
,
IllustrationNoContentDark
,
}
from
'@douyinfe/semi-illustrations'
;
import
{
IconChevronDown
,
IconChevronUp
}
from
'@douyinfe/semi-icons'
;
import
{
Settings
}
from
'lucide-react'
;
import
{
renderModelTag
,
getModelCategories
}
from
'../../../../helpers'
;
const
ModelsList
=
({
t
,
models
,
modelsLoading
,
copyText
})
=>
{
const
[
isModelsExpanded
,
setIsModelsExpanded
]
=
useState
(()
=>
{
// Initialize from localStorage if available
const
savedState
=
localStorage
.
getItem
(
'modelsExpanded'
);
return
savedState
?
JSON
.
parse
(
savedState
)
:
false
;
});
const
[
activeModelCategory
,
setActiveModelCategory
]
=
useState
(
'all'
);
const
MODELS_DISPLAY_COUNT
=
25
;
// 默认显示的模型数量
// Save models expanded state to localStorage whenever it changes
useEffect
(()
=>
{
localStorage
.
setItem
(
'modelsExpanded'
,
JSON
.
stringify
(
isModelsExpanded
));
},
[
isModelsExpanded
]);
return
(
<
div
className=
'py-4'
>
{
/* 卡片头部 */
}
<
div
className=
'flex items-center mb-4'
>
<
Avatar
size=
'small'
color=
'green'
className=
'mr-3 shadow-md'
>
<
Settings
size=
{
16
}
/>
</
Avatar
>
<
div
>
<
Typography
.
Text
className=
'text-lg font-medium'
>
{
t
(
'可用模型'
)
}
</
Typography
.
Text
>
<
div
className=
'text-xs text-gray-600'
>
{
t
(
'查看当前可用的所有模型'
)
}
</
div
>
</
div
>
</
div
>
{
/* 可用模型部分 */
}
<
div
className=
'bg-gray-50 dark:bg-gray-800 rounded-xl'
>
{
modelsLoading
?
(
// 骨架屏加载状态 - 模拟实际加载后的布局
<
div
className=
'space-y-4'
>
{
/* 模拟分类标签 */
}
<
div
className=
'mb-4'
style=
{
{
borderBottom
:
'1px solid var(--semi-color-border)'
}
}
>
<
div
className=
'flex overflow-x-auto py-2 gap-2'
>
{
Array
.
from
({
length
:
8
}).
map
((
_
,
index
)
=>
(
<
Skeleton
.
Button
key=
{
`cat-${index}`
}
style=
{
{
width
:
index
===
0
?
130
:
100
+
Math
.
random
()
*
50
,
height
:
36
,
borderRadius
:
8
,
}
}
/>
))
}
</
div
>
</
div
>
{
/* 模拟模型标签列表 */
}
<
div
className=
'flex flex-wrap gap-2'
>
{
Array
.
from
({
length
:
20
}).
map
((
_
,
index
)
=>
(
<
Skeleton
.
Button
key=
{
`model-${index}`
}
style=
{
{
width
:
100
+
Math
.
random
()
*
100
,
height
:
32
,
borderRadius
:
16
,
margin
:
'4px'
,
}
}
/>
))
}
</
div
>
</
div
>
)
:
models
.
length
===
0
?
(
<
div
className=
'py-8'
>
<
Empty
image=
{
<
IllustrationNoContent
style=
{
{
width
:
150
,
height
:
150
}
}
/>
}
darkModeImage=
{
<
IllustrationNoContentDark
style=
{
{
width
:
150
,
height
:
150
}
}
/>
}
description=
{
t
(
'没有可用模型'
)
}
style=
{
{
padding
:
'24px 0'
}
}
/>
</
div
>
)
:
(
<>
{
/* 模型分类标签页 */
}
<
div
className=
'mb-4'
>
<
Tabs
type=
'card'
activeKey=
{
activeModelCategory
}
onChange=
{
(
key
)
=>
setActiveModelCategory
(
key
)
}
className=
'mt-2'
collapsible
>
{
Object
.
entries
(
getModelCategories
(
t
)).
map
(
([
key
,
category
])
=>
{
// 计算该分类下的模型数量
const
modelCount
=
key
===
'all'
?
models
.
length
:
models
.
filter
((
model
)
=>
category
.
filter
({
model_name
:
model
}),
).
length
;
if
(
modelCount
===
0
&&
key
!==
'all'
)
return
null
;
return
(
<
TabPane
tab=
{
<
span
className=
'flex items-center gap-2'
>
{
category
.
icon
&&
(
<
span
className=
'w-4 h-4'
>
{
category
.
icon
}
</
span
>
)
}
{
category
.
label
}
<
Tag
color=
{
activeModelCategory
===
key
?
'red'
:
'grey'
}
size=
'small'
shape=
'circle'
>
{
modelCount
}
</
Tag
>
</
span
>
}
itemKey=
{
key
}
key=
{
key
}
/>
);
},
)
}
</
Tabs
>
</
div
>
<
div
className=
'bg-white dark:bg-gray-700 rounded-lg p-3'
>
{
(()
=>
{
// 根据当前选中的分类过滤模型
const
categories
=
getModelCategories
(
t
);
const
filteredModels
=
activeModelCategory
===
'all'
?
models
:
models
.
filter
((
model
)
=>
categories
[
activeModelCategory
].
filter
({
model_name
:
model
,
}),
);
// 如果过滤后没有模型,显示空状态
if
(
filteredModels
.
length
===
0
)
{
return
(
<
Empty
image=
{
<
IllustrationNoContent
style=
{
{
width
:
120
,
height
:
120
}
}
/>
}
darkModeImage=
{
<
IllustrationNoContentDark
style=
{
{
width
:
120
,
height
:
120
}
}
/>
}
description=
{
t
(
'该分类下没有可用模型'
)
}
style=
{
{
padding
:
'16px 0'
}
}
/>
);
}
if
(
filteredModels
.
length
<=
MODELS_DISPLAY_COUNT
)
{
return
(
<
Space
wrap
>
{
filteredModels
.
map
((
model
)
=>
renderModelTag
(
model
,
{
size
:
'small'
,
shape
:
'circle'
,
onClick
:
()
=>
copyText
(
model
),
}),
)
}
</
Space
>
);
}
else
{
return
(
<>
<
Collapsible
isOpen=
{
isModelsExpanded
}
>
<
Space
wrap
>
{
filteredModels
.
map
((
model
)
=>
renderModelTag
(
model
,
{
size
:
'small'
,
shape
:
'circle'
,
onClick
:
()
=>
copyText
(
model
),
}),
)
}
<
Tag
color=
'grey'
type=
'light'
className=
'cursor-pointer !rounded-lg'
onClick=
{
()
=>
setIsModelsExpanded
(
false
)
}
icon=
{
<
IconChevronUp
/>
}
>
{
t
(
'收起'
)
}
</
Tag
>
</
Space
>
</
Collapsible
>
{
!
isModelsExpanded
&&
(
<
Space
wrap
>
{
filteredModels
.
slice
(
0
,
MODELS_DISPLAY_COUNT
)
.
map
((
model
)
=>
renderModelTag
(
model
,
{
size
:
'small'
,
shape
:
'circle'
,
onClick
:
()
=>
copyText
(
model
),
}),
)
}
<
Tag
color=
'grey'
type=
'light'
className=
'cursor-pointer !rounded-lg'
onClick=
{
()
=>
setIsModelsExpanded
(
true
)
}
icon=
{
<
IconChevronDown
/>
}
>
{
t
(
'更多'
)
}{
' '
}
{
filteredModels
.
length
-
MODELS_DISPLAY_COUNT
}{
' '
}
{
t
(
'个模型'
)
}
</
Tag
>
</
Space
>
)
}
</>
);
}
})()
}
</
div
>
</>
)
}
</
div
>
</
div
>
);
};
export
default
ModelsList
;
web/src/components/table/models/ModelsDescription.jsx
deleted
100644 → 0
View file @
d13fa743
/*
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
from
'react'
;
import
{
Typography
}
from
'@douyinfe/semi-ui'
;
import
{
Layers
}
from
'lucide-react'
;
import
CompactModeToggle
from
'../../common/ui/CompactModeToggle'
;
const
{
Text
}
=
Typography
;
const
ModelsDescription
=
({
compactMode
,
setCompactMode
,
t
})
=>
{
return
(
<
div
className=
'flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full'
>
<
div
className=
'flex items-center text-green-500'
>
<
Layers
size=
{
16
}
className=
'mr-2'
/>
<
Text
>
{
t
(
'模型管理'
)
}
</
Text
>
</
div
>
<
CompactModeToggle
compactMode=
{
compactMode
}
setCompactMode=
{
setCompactMode
}
t=
{
t
}
/>
</
div
>
);
};
export
default
ModelsDescription
;
web/src/components/table/users/modals/BindSubscriptionModal.jsx
deleted
100644 → 0
View file @
d13fa743
/*
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
,
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
Modal
,
Select
,
Space
,
Typography
}
from
'@douyinfe/semi-ui'
;
import
{
API
,
showError
,
showSuccess
}
from
'../../../../helpers'
;
const
{
Text
}
=
Typography
;
const
BindSubscriptionModal
=
({
visible
,
onCancel
,
user
,
t
,
onSuccess
})
=>
{
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
plans
,
setPlans
]
=
useState
([]);
const
[
selectedPlanId
,
setSelectedPlanId
]
=
useState
(
null
);
const
loadPlans
=
async
()
=>
{
setLoading
(
true
);
try
{
const
res
=
await
API
.
get
(
'/api/subscription/admin/plans'
);
if
(
res
.
data
?.
success
)
{
setPlans
(
res
.
data
.
data
||
[]);
}
else
{
showError
(
res
.
data
?.
message
||
t
(
'加载失败'
));
}
}
catch
(
e
)
{
showError
(
t
(
'请求失败'
));
}
finally
{
setLoading
(
false
);
}
};
useEffect
(()
=>
{
if
(
visible
)
{
setSelectedPlanId
(
null
);
loadPlans
();
}
},
[
visible
]);
const
planOptions
=
useMemo
(()
=>
{
return
(
plans
||
[]).
map
((
p
)
=>
({
label
:
`
${
p
?.
plan
?.
title
||
''
}
(
$
{
p
?.
plan
?.
currency
||
'USD'
}
$
{
Number
(
p
?.
plan
?.
price_amount
||
0
)})
`,
value: p?.plan?.id,
}));
}, [plans]);
const bind = async () => {
if (!user?.id) {
showError(t('用户信息缺失'));
return;
}
if (!selectedPlanId) {
showError(t('请选择订阅套餐'));
return;
}
setLoading(true);
try {
const res = await API.post('/api/subscription/admin/bind', {
user_id: user.id,
plan_id: selectedPlanId,
});
if (res.data?.success) {
showSuccess(t('绑定成功'));
onSuccess?.();
onCancel?.();
} else {
showError(res.data?.message || t('绑定失败'));
}
} catch (e) {
showError(t('请求失败'));
} finally {
setLoading(false);
}
};
return (
<Modal
title={t('绑定订阅套餐')}
visible={visible}
onCancel={onCancel}
onOk={bind}
confirmLoading={loading}
maskClosable={false}
centered
>
<Space vertical style={{ width: '100%' }} spacing='medium'>
<div className='text-sm'>
<Text strong>{t('用户')}:</Text>
<Text>{user?.username}</Text>
<Text type='tertiary'> (ID: {user?.id})</Text>
</div>
<Select
placeholder={t('选择订阅套餐')}
optionList={planOptions}
value={selectedPlanId}
onChange={setSelectedPlanId}
loading={loading}
filter
style={{ width: '100%' }}
/>
<div className='text-xs text-gray-500'>
{t('绑定后会立即生成用户订阅(无需支付),有效期按套餐配置计算。')}
</div>
</Space>
</Modal>
);
};
export default BindSubscriptionModal;
web/src/hooks/model-deployments/useDeploymentResources.js
deleted
100644 → 0
View file @
d13fa743
/*
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
{
useState
,
useCallback
}
from
'react'
;
import
{
API
}
from
'../../helpers'
;
import
{
showError
}
from
'../../helpers'
;
export
const
useDeploymentResources
=
()
=>
{
const
[
hardwareTypes
,
setHardwareTypes
]
=
useState
([]);
const
[
hardwareTotalAvailable
,
setHardwareTotalAvailable
]
=
useState
(
0
);
const
[
locations
,
setLocations
]
=
useState
([]);
const
[
locationsTotalAvailable
,
setLocationsTotalAvailable
]
=
useState
(
0
);
const
[
availableReplicas
,
setAvailableReplicas
]
=
useState
([]);
const
[
priceEstimation
,
setPriceEstimation
]
=
useState
(
null
);
const
[
loadingHardware
,
setLoadingHardware
]
=
useState
(
false
);
const
[
loadingLocations
,
setLoadingLocations
]
=
useState
(
false
);
const
[
loadingReplicas
,
setLoadingReplicas
]
=
useState
(
false
);
const
[
loadingPrice
,
setLoadingPrice
]
=
useState
(
false
);
const
fetchHardwareTypes
=
useCallback
(
async
()
=>
{
try
{
setLoadingHardware
(
true
);
const
response
=
await
API
.
get
(
'/api/deployments/hardware-types'
);
if
(
response
.
data
.
success
)
{
const
{
hardware_types
:
hardwareList
=
[],
total_available
}
=
response
.
data
.
data
||
{};
const
normalizedHardware
=
hardwareList
.
map
((
hardware
)
=>
{
const
availableCountValue
=
Number
(
hardware
.
available_count
);
const
availableCount
=
Number
.
isNaN
(
availableCountValue
)
?
0
:
availableCountValue
;
const
availableBool
=
typeof
hardware
.
available
===
'boolean'
?
hardware
.
available
:
availableCount
>
0
;
return
{
...
hardware
,
available
:
availableBool
,
available_count
:
availableCount
,
};
});
const
providedTotal
=
Number
(
total_available
);
const
fallbackTotal
=
normalizedHardware
.
reduce
(
(
acc
,
item
)
=>
acc
+
(
Number
.
isNaN
(
item
.
available_count
)
?
0
:
item
.
available_count
),
0
,
);
const
hasProvidedTotal
=
total_available
!==
undefined
&&
total_available
!==
null
&&
total_available
!==
''
&&
!
Number
.
isNaN
(
providedTotal
);
setHardwareTypes
(
normalizedHardware
);
setHardwareTotalAvailable
(
hasProvidedTotal
?
providedTotal
:
fallbackTotal
,
);
return
normalizedHardware
;
}
else
{
showError
(
'获取硬件类型失败: '
+
response
.
data
.
message
);
setHardwareTotalAvailable
(
0
);
return
[];
}
}
catch
(
error
)
{
showError
(
'获取硬件类型失败: '
+
error
.
message
);
setHardwareTotalAvailable
(
0
);
return
[];
}
finally
{
setLoadingHardware
(
false
);
}
},
[]);
const
fetchLocations
=
useCallback
(
async
(
hardwareId
,
gpuCount
=
1
)
=>
{
if
(
!
hardwareId
)
{
setLocations
([]);
setLocationsTotalAvailable
(
0
);
return
[];
}
try
{
setLoadingLocations
(
true
);
const
response
=
await
API
.
get
(
`/api/deployments/available-replicas?hardware_id=
${
hardwareId
}
&gpu_count=
${
gpuCount
}
`
,
);
if
(
response
.
data
.
success
)
{
const
replicas
=
response
.
data
.
data
?.
replicas
||
[];
const
nextLocationsMap
=
new
Map
();
replicas
.
forEach
((
replica
)
=>
{
const
rawId
=
replica
?.
location_id
??
replica
?.
location
?.
id
;
if
(
rawId
===
null
||
rawId
===
undefined
)
return
;
const
mapKey
=
String
(
rawId
);
if
(
nextLocationsMap
.
has
(
mapKey
))
return
;
const
rawIso2
=
replica
?.
iso2
??
replica
?.
location_iso2
??
replica
?.
location
?.
iso2
;
const
iso2
=
rawIso2
?
String
(
rawIso2
).
toUpperCase
()
:
''
;
const
name
=
replica
?.
location_name
??
replica
?.
location
?.
name
??
replica
?.
name
??
String
(
rawId
);
nextLocationsMap
.
set
(
mapKey
,
{
id
:
rawId
,
name
:
String
(
name
),
iso2
,
region
:
replica
?.
region
??
replica
?.
location_region
??
replica
?.
location
?.
region
,
country
:
replica
?.
country
??
replica
?.
location_country
??
replica
?.
location
?.
country
,
code
:
replica
?.
code
??
replica
?.
location_code
??
replica
?.
location
?.
code
,
available
:
Number
(
replica
?.
available_count
)
||
0
,
});
});
const
normalizedLocations
=
Array
.
from
(
nextLocationsMap
.
values
());
setLocations
(
normalizedLocations
);
setLocationsTotalAvailable
(
normalizedLocations
.
reduce
(
(
acc
,
item
)
=>
acc
+
(
item
.
available
||
0
),
0
,
),
);
return
normalizedLocations
;
}
else
{
showError
(
'获取部署位置失败: '
+
response
.
data
.
message
);
setLocationsTotalAvailable
(
0
);
return
[];
}
}
catch
(
error
)
{
showError
(
'获取部署位置失败: '
+
error
.
message
);
setLocationsTotalAvailable
(
0
);
return
[];
}
finally
{
setLoadingLocations
(
false
);
}
},
[]);
const
fetchAvailableReplicas
=
useCallback
(
async
(
hardwareId
,
gpuCount
=
1
)
=>
{
if
(
!
hardwareId
)
{
setAvailableReplicas
([]);
return
[];
}
try
{
setLoadingReplicas
(
true
);
const
response
=
await
API
.
get
(
`/api/deployments/available-replicas?hardware_id=
${
hardwareId
}
&gpu_count=
${
gpuCount
}
`
,
);
if
(
response
.
data
.
success
)
{
const
replicas
=
response
.
data
.
data
.
replicas
||
[];
setAvailableReplicas
(
replicas
);
return
replicas
;
}
else
{
showError
(
'获取可用资源失败: '
+
response
.
data
.
message
);
setAvailableReplicas
([]);
return
[];
}
}
catch
(
error
)
{
console
.
error
(
'Load available replicas error:'
,
error
);
setAvailableReplicas
([]);
return
[];
}
finally
{
setLoadingReplicas
(
false
);
}
},
[],
);
const
calculatePrice
=
useCallback
(
async
(
params
)
=>
{
const
{
locationIds
,
hardwareId
,
gpusPerContainer
,
durationHours
,
replicaCount
,
}
=
params
;
if
(
!
locationIds
?.
length
||
!
hardwareId
||
!
gpusPerContainer
||
!
durationHours
||
!
replicaCount
)
{
setPriceEstimation
(
null
);
return
null
;
}
try
{
setLoadingPrice
(
true
);
const
requestData
=
{
location_ids
:
locationIds
,
hardware_id
:
hardwareId
,
gpus_per_container
:
gpusPerContainer
,
duration_hours
:
durationHours
,
replica_count
:
replicaCount
,
};
const
response
=
await
API
.
post
(
'/api/deployments/price-estimation'
,
requestData
,
);
if
(
response
.
data
.
success
)
{
const
estimation
=
response
.
data
.
data
;
setPriceEstimation
(
estimation
);
return
estimation
;
}
else
{
showError
(
'价格计算失败: '
+
response
.
data
.
message
);
setPriceEstimation
(
null
);
return
null
;
}
}
catch
(
error
)
{
console
.
error
(
'Price calculation error:'
,
error
);
setPriceEstimation
(
null
);
return
null
;
}
finally
{
setLoadingPrice
(
false
);
}
},
[]);
const
checkClusterNameAvailability
=
useCallback
(
async
(
name
)
=>
{
if
(
!
name
?.
trim
())
return
false
;
try
{
const
response
=
await
API
.
get
(
`/api/deployments/check-name?name=
${
encodeURIComponent
(
name
.
trim
())}
`
,
);
if
(
response
.
data
.
success
)
{
return
response
.
data
.
data
.
available
;
}
else
{
showError
(
'检查名称可用性失败: '
+
response
.
data
.
message
);
return
false
;
}
}
catch
(
error
)
{
console
.
error
(
'Check cluster name availability error:'
,
error
);
return
false
;
}
},
[]);
const
createDeployment
=
useCallback
(
async
(
deploymentData
)
=>
{
try
{
const
response
=
await
API
.
post
(
'/api/deployments'
,
deploymentData
);
if
(
response
.
data
.
success
)
{
return
response
.
data
.
data
;
}
else
{
throw
new
Error
(
response
.
data
.
message
||
'创建部署失败'
);
}
}
catch
(
error
)
{
throw
error
;
}
},
[]);
return
{
// Data
hardwareTypes
,
hardwareTotalAvailable
,
locations
,
locationsTotalAvailable
,
availableReplicas
,
priceEstimation
,
// Loading states
loadingHardware
,
loadingLocations
,
loadingReplicas
,
loadingPrice
,
// Functions
fetchHardwareTypes
,
fetchLocations
,
fetchAvailableReplicas
,
calculatePrice
,
checkClusterNameAvailability
,
createDeployment
,
// Clear functions
clearPriceEstimation
:
()
=>
setPriceEstimation
(
null
),
clearAvailableReplicas
:
()
=>
setAvailableReplicas
([]),
};
};
export
default
useDeploymentResources
;
web/src/hooks/model-deployments/useEnhancedDeploymentActions.jsx
deleted
100644 → 0
View file @
d13fa743
/*
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
{
useState
}
from
'react'
;
import
{
API
,
showError
,
showSuccess
}
from
'../../helpers'
;
export
const
useEnhancedDeploymentActions
=
(
t
)
=>
{
const
[
loading
,
setLoading
]
=
useState
({});
// Set loading state for specific operation
const
setOperationLoading
=
(
operation
,
deploymentId
,
isLoading
)
=>
{
setLoading
((
prev
)
=>
({
...
prev
,
[
`
${
operation
}
_
${
deploymentId
}
`
]:
isLoading
,
}));
};
// Get loading state for specific operation
const
isOperationLoading
=
(
operation
,
deploymentId
)
=>
{
return
loading
[
`
${
operation
}
_
${
deploymentId
}
`
]
||
false
;
};
// Extend deployment duration
const
extendDeployment
=
async
(
deploymentId
,
durationHours
)
=>
{
try
{
setOperationLoading
(
'extend'
,
deploymentId
,
true
);
const
response
=
await
API
.
post
(
`/api/deployments/
${
deploymentId
}
/extend`
,
{
duration_hours
:
durationHours
,
},
);
if
(
response
.
data
.
success
)
{
showSuccess
(
t
(
'容器时长延长成功'
));
return
response
.
data
.
data
;
}
}
catch
(
error
)
{
showError
(
t
(
'延长时长失败'
)
+
': '
+
(
error
.
response
?.
data
?.
message
||
error
.
message
),
);
throw
error
;
}
finally
{
setOperationLoading
(
'extend'
,
deploymentId
,
false
);
}
};
// Get deployment details
const
getDeploymentDetails
=
async
(
deploymentId
)
=>
{
try
{
setOperationLoading
(
'details'
,
deploymentId
,
true
);
const
response
=
await
API
.
get
(
`/api/deployments/
${
deploymentId
}
`
);
if
(
response
.
data
.
success
)
{
return
response
.
data
.
data
;
}
}
catch
(
error
)
{
showError
(
t
(
'获取详情失败'
)
+
': '
+
(
error
.
response
?.
data
?.
message
||
error
.
message
),
);
throw
error
;
}
finally
{
setOperationLoading
(
'details'
,
deploymentId
,
false
);
}
};
// Get deployment logs
const
getDeploymentLogs
=
async
(
deploymentId
,
options
=
{})
=>
{
try
{
setOperationLoading
(
'logs'
,
deploymentId
,
true
);
const
params
=
new
URLSearchParams
();
if
(
options
.
containerId
)
params
.
append
(
'container_id'
,
options
.
containerId
);
if
(
options
.
level
)
params
.
append
(
'level'
,
options
.
level
);
if
(
options
.
limit
)
params
.
append
(
'limit'
,
options
.
limit
.
toString
());
if
(
options
.
cursor
)
params
.
append
(
'cursor'
,
options
.
cursor
);
if
(
options
.
follow
)
params
.
append
(
'follow'
,
'true'
);
if
(
options
.
startTime
)
params
.
append
(
'start_time'
,
options
.
startTime
);
if
(
options
.
endTime
)
params
.
append
(
'end_time'
,
options
.
endTime
);
const
response
=
await
API
.
get
(
`/api/deployments/
${
deploymentId
}
/logs?
${
params
}
`
,
);
if
(
response
.
data
.
success
)
{
return
response
.
data
.
data
;
}
}
catch
(
error
)
{
showError
(
t
(
'获取日志失败'
)
+
': '
+
(
error
.
response
?.
data
?.
message
||
error
.
message
),
);
throw
error
;
}
finally
{
setOperationLoading
(
'logs'
,
deploymentId
,
false
);
}
};
// Update deployment configuration
const
updateDeploymentConfig
=
async
(
deploymentId
,
config
)
=>
{
try
{
setOperationLoading
(
'config'
,
deploymentId
,
true
);
const
response
=
await
API
.
put
(
`/api/deployments/
${
deploymentId
}
`
,
config
,
);
if
(
response
.
data
.
success
)
{
showSuccess
(
t
(
'容器配置更新成功'
));
return
response
.
data
.
data
;
}
}
catch
(
error
)
{
showError
(
t
(
'更新配置失败'
)
+
': '
+
(
error
.
response
?.
data
?.
message
||
error
.
message
),
);
throw
error
;
}
finally
{
setOperationLoading
(
'config'
,
deploymentId
,
false
);
}
};
// Delete (destroy) deployment
const
deleteDeployment
=
async
(
deploymentId
)
=>
{
try
{
setOperationLoading
(
'delete'
,
deploymentId
,
true
);
const
response
=
await
API
.
delete
(
`/api/deployments/
${
deploymentId
}
`
);
if
(
response
.
data
.
success
)
{
showSuccess
(
t
(
'容器销毁请求已提交'
));
return
response
.
data
.
data
;
}
}
catch
(
error
)
{
showError
(
t
(
'销毁容器失败'
)
+
': '
+
(
error
.
response
?.
data
?.
message
||
error
.
message
),
);
throw
error
;
}
finally
{
setOperationLoading
(
'delete'
,
deploymentId
,
false
);
}
};
// Update deployment name
const
updateDeploymentName
=
async
(
deploymentId
,
newName
)
=>
{
try
{
setOperationLoading
(
'rename'
,
deploymentId
,
true
);
const
response
=
await
API
.
put
(
`/api/deployments/
${
deploymentId
}
/name`
,
{
name
:
newName
,
});
if
(
response
.
data
.
success
)
{
showSuccess
(
t
(
'容器名称更新成功'
));
return
response
.
data
.
data
;
}
}
catch
(
error
)
{
showError
(
t
(
'更新名称失败'
)
+
': '
+
(
error
.
response
?.
data
?.
message
||
error
.
message
),
);
throw
error
;
}
finally
{
setOperationLoading
(
'rename'
,
deploymentId
,
false
);
}
};
// Batch operations
const
batchDelete
=
async
(
deploymentIds
)
=>
{
try
{
setOperationLoading
(
'batch_delete'
,
'all'
,
true
);
const
results
=
await
Promise
.
allSettled
(
deploymentIds
.
map
((
id
)
=>
deleteDeployment
(
id
)),
);
const
successful
=
results
.
filter
((
r
)
=>
r
.
status
===
'fulfilled'
).
length
;
const
failed
=
results
.
filter
((
r
)
=>
r
.
status
===
'rejected'
).
length
;
if
(
successful
>
0
)
{
showSuccess
(
t
(
'批量操作完成: {{success}}个成功, {{failed}}个失败'
,
{
success
:
successful
,
failed
:
failed
,
}),
);
}
return
{
successful
,
failed
};
}
catch
(
error
)
{
showError
(
t
(
'批量操作失败'
)
+
': '
+
error
.
message
);
throw
error
;
}
finally
{
setOperationLoading
(
'batch_delete'
,
'all'
,
false
);
}
};
// Export logs
const
exportLogs
=
async
(
deploymentId
,
options
=
{})
=>
{
try
{
setOperationLoading
(
'export_logs'
,
deploymentId
,
true
);
const
logs
=
await
getDeploymentLogs
(
deploymentId
,
{
...
options
,
limit
:
10000
,
// Get more logs for export
});
if
(
logs
&&
logs
.
logs
)
{
const
logText
=
logs
.
logs
.
map
(
(
log
)
=>
`[
${
new
Date
(
log
.
timestamp
).
toISOString
()}
] [
${
log
.
level
}
]
${
log
.
source
?
`[
${
log
.
source
}
] `
:
''
}${
log
.
message
}
`
,
)
.
join
(
'\n'
);
const
blob
=
new
Blob
([
logText
],
{
type
:
'text/plain'
});
const
url
=
URL
.
createObjectURL
(
blob
);
const
a
=
document
.
createElement
(
'a'
);
a
.
href
=
url
;
a
.
download
=
`deployment-
${
deploymentId
}
-logs-
${
new
Date
().
toISOString
().
split
(
'T'
)[
0
]}
.txt`
;
document
.
body
.
appendChild
(
a
);
a
.
click
();
document
.
body
.
removeChild
(
a
);
URL
.
revokeObjectURL
(
url
);
showSuccess
(
t
(
'日志导出成功'
));
}
}
catch
(
error
)
{
showError
(
t
(
'导出日志失败'
)
+
': '
+
error
.
message
);
throw
error
;
}
finally
{
setOperationLoading
(
'export_logs'
,
deploymentId
,
false
);
}
};
return
{
// Actions
extendDeployment
,
getDeploymentDetails
,
getDeploymentLogs
,
updateDeploymentConfig
,
deleteDeployment
,
updateDeploymentName
,
batchDelete
,
exportLogs
,
// Loading states
isOperationLoading
,
loading
,
// Utility
setOperationLoading
,
};
};
export
default
useEnhancedDeploymentActions
;
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx
deleted
100644 → 0
View file @
d13fa743
/*
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
{
useState
,
useEffect
,
useContext
}
from
'react'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
Card
,
Button
,
Switch
,
Typography
,
Row
,
Col
,
Avatar
,
}
from
'@douyinfe/semi-ui'
;
import
{
API
,
showSuccess
,
showError
}
from
'../../../helpers'
;
import
{
StatusContext
}
from
'../../../context/Status'
;
import
{
UserContext
}
from
'../../../context/User'
;
import
{
useUserPermissions
}
from
'../../../hooks/common/useUserPermissions'
;
import
{
mergeAdminConfig
,
useSidebar
}
from
'../../../hooks/common/useSidebar'
;
import
{
Settings
}
from
'lucide-react'
;
const
{
Text
}
=
Typography
;
export
default
function
SettingsSidebarModulesUser
()
{
const
{
t
}
=
useTranslation
();
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
statusState
]
=
useContext
(
StatusContext
);
// 使用后端权限验证替代前端角色判断
const
{
permissions
,
loading
:
permissionsLoading
,
hasSidebarSettingsPermission
,
isSidebarSectionAllowed
,
isSidebarModuleAllowed
,
}
=
useUserPermissions
();
// 使用useSidebar钩子获取刷新方法
const
{
refreshUserConfig
}
=
useSidebar
();
// 如果没有边栏设置权限,不显示此组件
if
(
!
permissionsLoading
&&
!
hasSidebarSettingsPermission
())
{
return
null
;
}
// 权限加载中,显示加载状态
if
(
permissionsLoading
)
{
return
null
;
}
// 根据用户权限生成默认配置
const
generateDefaultConfig
=
()
=>
{
const
defaultConfig
=
{};
// 聊天区域 - 所有用户都可以访问
if
(
isSidebarSectionAllowed
(
'chat'
))
{
defaultConfig
.
chat
=
{
enabled
:
true
,
playground
:
isSidebarModuleAllowed
(
'chat'
,
'playground'
),
chat
:
isSidebarModuleAllowed
(
'chat'
,
'chat'
),
};
}
// 控制台区域 - 所有用户都可以访问
if
(
isSidebarSectionAllowed
(
'console'
))
{
defaultConfig
.
console
=
{
enabled
:
true
,
detail
:
isSidebarModuleAllowed
(
'console'
,
'detail'
),
token
:
isSidebarModuleAllowed
(
'console'
,
'token'
),
log
:
isSidebarModuleAllowed
(
'console'
,
'log'
),
midjourney
:
isSidebarModuleAllowed
(
'console'
,
'midjourney'
),
task
:
isSidebarModuleAllowed
(
'console'
,
'task'
),
};
}
// 个人中心区域 - 所有用户都可以访问
if
(
isSidebarSectionAllowed
(
'personal'
))
{
defaultConfig
.
personal
=
{
enabled
:
true
,
topup
:
isSidebarModuleAllowed
(
'personal'
,
'topup'
),
personal
:
isSidebarModuleAllowed
(
'personal'
,
'personal'
),
};
}
// 管理员区域 - 只有管理员可以访问
if
(
isSidebarSectionAllowed
(
'admin'
))
{
defaultConfig
.
admin
=
{
enabled
:
true
,
channel
:
isSidebarModuleAllowed
(
'admin'
,
'channel'
),
models
:
isSidebarModuleAllowed
(
'admin'
,
'models'
),
deployment
:
isSidebarModuleAllowed
(
'admin'
,
'deployment'
),
redemption
:
isSidebarModuleAllowed
(
'admin'
,
'redemption'
),
user
:
isSidebarModuleAllowed
(
'admin'
,
'user'
),
subscription
:
isSidebarModuleAllowed
(
'admin'
,
'subscription'
),
setting
:
isSidebarModuleAllowed
(
'admin'
,
'setting'
),
};
}
return
defaultConfig
;
};
// 用户个人左侧边栏模块设置
const
[
sidebarModulesUser
,
setSidebarModulesUser
]
=
useState
({});
// 管理员全局配置
const
[
adminConfig
,
setAdminConfig
]
=
useState
(
null
);
// 处理区域级别开关变更
function
handleSectionChange
(
sectionKey
)
{
return
(
checked
)
=>
{
const
newModules
=
{
...
sidebarModulesUser
,
[
sectionKey
]:
{
...
sidebarModulesUser
[
sectionKey
],
enabled
:
checked
,
},
};
setSidebarModulesUser
(
newModules
);
console
.
log
(
'用户边栏区域配置变更:'
,
sectionKey
,
checked
,
newModules
);
};
}
// 处理功能级别开关变更
function
handleModuleChange
(
sectionKey
,
moduleKey
)
{
return
(
checked
)
=>
{
const
newModules
=
{
...
sidebarModulesUser
,
[
sectionKey
]:
{
...
sidebarModulesUser
[
sectionKey
],
[
moduleKey
]:
checked
,
},
};
setSidebarModulesUser
(
newModules
);
console
.
log
(
'用户边栏功能配置变更:'
,
sectionKey
,
moduleKey
,
checked
,
newModules
,
);
};
}
// 重置为默认配置(基于权限过滤)
function
resetSidebarModules
()
{
const
defaultConfig
=
generateDefaultConfig
();
setSidebarModulesUser
(
defaultConfig
);
showSuccess
(
t
(
'已重置为默认配置'
));
console
.
log
(
'用户边栏配置重置为默认:'
,
defaultConfig
);
}
// 保存配置
async
function
onSubmit
()
{
setLoading
(
true
);
try
{
console
.
log
(
'保存用户边栏配置:'
,
sidebarModulesUser
);
const
res
=
await
API
.
put
(
'/api/user/self'
,
{
sidebar_modules
:
JSON
.
stringify
(
sidebarModulesUser
),
});
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
showSuccess
(
t
(
'保存成功'
));
console
.
log
(
'用户边栏配置保存成功'
);
// 刷新useSidebar钩子中的用户配置,实现实时更新
await
refreshUserConfig
();
console
.
log
(
'用户边栏配置已刷新,边栏将立即更新'
);
}
else
{
showError
(
message
);
console
.
error
(
'用户边栏配置保存失败:'
,
message
);
}
}
catch
(
error
)
{
showError
(
t
(
'保存失败,请重试'
));
console
.
error
(
'用户边栏配置保存异常:'
,
error
);
}
finally
{
setLoading
(
false
);
}
}
// 统一的配置加载逻辑
useEffect
(()
=>
{
const
loadConfigs
=
async
()
=>
{
try
{
// 获取管理员全局配置
if
(
statusState
?.
status
?.
SidebarModulesAdmin
)
{
try
{
const
adminConf
=
JSON
.
parse
(
statusState
.
status
.
SidebarModulesAdmin
,
);
const
mergedAdminConf
=
mergeAdminConfig
(
adminConf
);
setAdminConfig
(
mergedAdminConf
);
console
.
log
(
'加载管理员边栏配置:'
,
mergedAdminConf
);
}
catch
(
error
)
{
const
mergedAdminConf
=
mergeAdminConfig
(
null
);
setAdminConfig
(
mergedAdminConf
);
console
.
log
(
'加载管理员边栏配置失败,使用默认配置:'
,
mergedAdminConf
,
);
}
}
else
{
const
mergedAdminConf
=
mergeAdminConfig
(
null
);
setAdminConfig
(
mergedAdminConf
);
console
.
log
(
'管理员边栏配置缺失,使用默认配置:'
,
mergedAdminConf
);
}
// 获取用户个人配置
const
userRes
=
await
API
.
get
(
'/api/user/self'
);
if
(
userRes
.
data
.
success
&&
userRes
.
data
.
data
.
sidebar_modules
)
{
let
userConf
;
// 检查sidebar_modules是字符串还是对象
if
(
typeof
userRes
.
data
.
data
.
sidebar_modules
===
'string'
)
{
userConf
=
JSON
.
parse
(
userRes
.
data
.
data
.
sidebar_modules
);
}
else
{
userConf
=
userRes
.
data
.
data
.
sidebar_modules
;
}
console
.
log
(
'从API加载的用户配置:'
,
userConf
);
// 确保用户配置也经过权限过滤
const
filteredUserConf
=
{};
Object
.
keys
(
userConf
).
forEach
((
sectionKey
)
=>
{
if
(
isSidebarSectionAllowed
(
sectionKey
))
{
filteredUserConf
[
sectionKey
]
=
{
...
userConf
[
sectionKey
]
};
// 过滤不允许的模块
Object
.
keys
(
userConf
[
sectionKey
]).
forEach
((
moduleKey
)
=>
{
if
(
moduleKey
!==
'enabled'
&&
!
isSidebarModuleAllowed
(
sectionKey
,
moduleKey
)
)
{
delete
filteredUserConf
[
sectionKey
][
moduleKey
];
}
});
}
});
setSidebarModulesUser
(
filteredUserConf
);
console
.
log
(
'权限过滤后的用户配置:'
,
filteredUserConf
);
}
else
{
// 如果用户没有配置,使用权限过滤后的默认配置
const
defaultConfig
=
generateDefaultConfig
();
setSidebarModulesUser
(
defaultConfig
);
console
.
log
(
'用户无配置,使用默认配置:'
,
defaultConfig
);
}
}
catch
(
error
)
{
console
.
error
(
'加载边栏配置失败:'
,
error
);
// 出错时也使用默认配置
const
defaultConfig
=
generateDefaultConfig
();
setSidebarModulesUser
(
defaultConfig
);
}
};
// 只有权限加载完成且有边栏设置权限时才加载配置
if
(
!
permissionsLoading
&&
hasSidebarSettingsPermission
())
{
loadConfigs
();
}
},
[
statusState
,
permissionsLoading
,
hasSidebarSettingsPermission
,
isSidebarSectionAllowed
,
isSidebarModuleAllowed
,
]);
// 检查功能是否被管理员允许
const
isAllowedByAdmin
=
(
sectionKey
,
moduleKey
=
null
)
=>
{
if
(
!
adminConfig
)
return
true
;
if
(
moduleKey
)
{
return
(
adminConfig
[
sectionKey
]?.
enabled
&&
adminConfig
[
sectionKey
]?.[
moduleKey
]
);
}
else
{
return
adminConfig
[
sectionKey
]?.
enabled
;
}
};
// 区域配置数据(根据后端权限过滤)
const
sectionConfigs
=
[
{
key
:
'chat'
,
title
:
t
(
'聊天区域'
),
description
:
t
(
'操练场和聊天功能'
),
modules
:
[
{
key
:
'playground'
,
title
:
t
(
'操练场'
),
description
:
t
(
'AI模型测试环境'
),
},
{
key
:
'chat'
,
title
:
t
(
'聊天'
),
description
:
t
(
'聊天会话管理'
)
},
],
},
{
key
:
'console'
,
title
:
t
(
'控制台区域'
),
description
:
t
(
'数据管理和日志查看'
),
modules
:
[
{
key
:
'detail'
,
title
:
t
(
'数据看板'
),
description
:
t
(
'系统数据统计'
)
},
{
key
:
'token'
,
title
:
t
(
'令牌管理'
),
description
:
t
(
'API令牌管理'
)
},
{
key
:
'log'
,
title
:
t
(
'使用日志'
),
description
:
t
(
'API使用记录'
)
},
{
key
:
'midjourney'
,
title
:
t
(
'绘图日志'
),
description
:
t
(
'绘图任务记录'
),
},
{
key
:
'task'
,
title
:
t
(
'任务日志'
),
description
:
t
(
'系统任务记录'
)
},
],
},
{
key
:
'personal'
,
title
:
t
(
'个人中心区域'
),
description
:
t
(
'用户个人功能'
),
modules
:
[
{
key
:
'topup'
,
title
:
t
(
'钱包管理'
),
description
:
t
(
'余额充值管理'
)
},
{
key
:
'personal'
,
title
:
t
(
'个人设置'
),
description
:
t
(
'个人信息设置'
),
},
],
},
{
key
:
'admin'
,
title
:
t
(
'管理员区域'
),
description
:
t
(
'系统管理功能'
),
modules
:
[
{
key
:
'channel'
,
title
:
t
(
'渠道管理'
),
description
:
t
(
'API渠道配置'
)
},
{
key
:
'models'
,
title
:
t
(
'模型管理'
),
description
:
t
(
'AI模型配置'
)
},
{
key
:
'deployment'
,
title
:
t
(
'模型部署'
),
description
:
t
(
'模型部署管理'
),
},
{
key
:
'subscription'
,
title
:
t
(
'订阅管理'
),
description
:
t
(
'订阅套餐管理'
),
},
{
key
:
'redemption'
,
title
:
t
(
'兑换码管理'
),
description
:
t
(
'兑换码生成管理'
),
},
{
key
:
'user'
,
title
:
t
(
'用户管理'
),
description
:
t
(
'用户账户管理'
)
},
{
key
:
'setting'
,
title
:
t
(
'系统设置'
),
description
:
t
(
'系统参数配置'
),
},
],
},
]
.
filter
((
section
)
=>
{
// 使用后端权限验证替代前端角色判断
return
isSidebarSectionAllowed
(
section
.
key
);
})
.
map
((
section
)
=>
({
...
section
,
modules
:
section
.
modules
.
filter
((
module
)
=>
isSidebarModuleAllowed
(
section
.
key
,
module
.
key
),
),
}))
.
filter
(
(
section
)
=>
// 过滤掉没有可用模块的区域
section
.
modules
.
length
>
0
&&
isAllowedByAdmin
(
section
.
key
),
);
return
(
<
Card
className=
'!rounded-2xl shadow-sm border-0'
>
{
/* 卡片头部 */
}
<
div
className=
'flex items-center mb-4'
>
<
Avatar
size=
'small'
color=
'purple'
className=
'mr-3 shadow-md'
>
<
Settings
size=
{
16
}
/>
</
Avatar
>
<
div
>
<
Typography
.
Text
className=
'text-lg font-medium'
>
{
t
(
'左侧边栏个人设置'
)
}
</
Typography
.
Text
>
<
div
className=
'text-xs text-gray-600'
>
{
t
(
'个性化设置左侧边栏的显示内容'
)
}
</
div
>
</
div
>
</
div
>
<
div
className=
'mb-4'
>
<
Text
type=
'secondary'
className=
'text-sm text-gray-600'
>
{
t
(
'您可以个性化设置侧边栏的要显示功能'
)
}
</
Text
>
</
div
>
{
sectionConfigs
.
map
((
section
)
=>
(
<
div
key=
{
section
.
key
}
className=
'mb-6'
>
{
/* 区域标题和总开关 */
}
<
div
className=
'flex justify-between items-center mb-4 p-4 bg-gray-50 rounded-xl border border-gray-200'
>
<
div
>
<
div
className=
'font-semibold text-base text-gray-900 mb-1'
>
{
section
.
title
}
</
div
>
<
Text
className=
'text-xs text-gray-600'
>
{
section
.
description
}
</
Text
>
</
div
>
<
Switch
checked=
{
sidebarModulesUser
[
section
.
key
]?.
enabled
!==
false
}
onChange=
{
handleSectionChange
(
section
.
key
)
}
size=
'default'
/>
</
div
>
{
/* 功能模块网格 */
}
<
Row
gutter=
{
[
12
,
12
]
}
>
{
section
.
modules
.
map
((
module
)
=>
(
<
Col
key=
{
module
.
key
}
xs=
{
24
}
sm=
{
12
}
md=
{
8
}
lg=
{
6
}
xl=
{
6
}
>
<
Card
className=
{
`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
sidebarModulesUser[section.key]?.enabled !== false
? ''
: 'opacity-50'
}`
}
bodyStyle=
{
{
padding
:
'16px'
}
}
hoverable
>
<
div
className=
'flex justify-between items-center h-full'
>
<
div
className=
'flex-1 text-left'
>
<
div
className=
'font-semibold text-sm text-gray-900 mb-1'
>
{
module
.
title
}
</
div
>
<
Text
className=
'text-xs text-gray-600 leading-relaxed block'
>
{
module
.
description
}
</
Text
>
</
div
>
<
div
className=
'ml-4'
>
<
Switch
checked=
{
sidebarModulesUser
[
section
.
key
]?.[
module
.
key
]
!==
false
}
onChange=
{
handleModuleChange
(
section
.
key
,
module
.
key
)
}
size=
'default'
disabled=
{
sidebarModulesUser
[
section
.
key
]?.
enabled
===
false
}
/>
</
div
>
</
div
>
</
Card
>
</
Col
>
))
}
</
Row
>
</
div
>
))
}
{
/* 底部按钮 */
}
<
div
className=
'flex justify-end gap-3 mt-6 pt-4 border-t border-gray-200'
>
<
Button
type=
'tertiary'
onClick=
{
resetSidebarModules
}
className=
'!rounded-lg'
>
{
t
(
'重置为默认'
)
}
</
Button
>
<
Button
type=
'primary'
onClick=
{
onSubmit
}
loading=
{
loading
}
className=
'!rounded-lg'
>
{
t
(
'保存设置'
)
}
</
Button
>
</
div
>
</
Card
>
);
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment