Browse Source

fix: fix model deployment style issues, lint problems, and i18n gaps. (#2556)

* fix: fix model deployment style issues, lint problems, and i18n gaps.

* fix: adjust the key not to be displayed on the frontend, tested via the backend.

* fix: adjust the sidebar configuration logic to use the default configuration items if they are not defined.
Seefs 6 days ago
parent
commit
22d0b73d21
29 changed files with 3663 additions and 1699 deletions
  1. 33 4
      controller/deployment.go
  2. 5 1
      controller/option.go
  3. 2 16
      router/api-router.go
  4. 3 1
      web/i18next.config.js
  5. 143 108
      web/src/components/model-deployments/DeploymentAccessGuard.jsx
  6. 35 9
      web/src/components/settings/personal/cards/NotificationSettings.jsx
  7. 112 140
      web/src/components/table/channels/modals/OllamaModelModal.jsx
  8. 2 2
      web/src/components/table/model-deployments/DeploymentsActions.jsx
  9. 137 107
      web/src/components/table/model-deployments/DeploymentsColumnDefs.jsx
  10. 8 8
      web/src/components/table/model-deployments/DeploymentsTable.jsx
  11. 8 3
      web/src/components/table/model-deployments/index.jsx
  12. 506 457
      web/src/components/table/model-deployments/modals/CreateDeploymentModal.jsx
  13. 143 121
      web/src/components/table/model-deployments/modals/UpdateConfigModal.jsx
  14. 313 229
      web/src/components/table/model-deployments/modals/ViewDetailsModal.jsx
  15. 203 140
      web/src/components/table/model-deployments/modals/ViewLogsModal.jsx
  16. 53 34
      web/src/hooks/common/useSidebar.js
  17. 99 53
      web/src/hooks/model-deployments/useDeploymentResources.js
  18. 110 95
      web/src/hooks/model-deployments/useDeploymentsData.jsx
  19. 89 52
      web/src/hooks/model-deployments/useEnhancedDeploymentActions.jsx
  20. 11 33
      web/src/hooks/model-deployments/useModelDeploymentSettings.js
  21. 262 10
      web/src/i18n/locales/en.json
  22. 263 10
      web/src/i18n/locales/fr.json
  23. 278 9
      web/src/i18n/locales/ja.json
  24. 265 10
      web/src/i18n/locales/ru.json
  25. 279 9
      web/src/i18n/locales/vi.json
  26. 260 10
      web/src/i18n/locales/zh.json
  27. 1 2
      web/src/pages/ModelDeployment/index.jsx
  28. 4 18
      web/src/pages/Setting/Model/SettingModelDeployment.jsx
  29. 36 8
      web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx

+ 33 - 4
controller/deployment.go

@@ -1,6 +1,8 @@
 package controller
 
 import (
+	"bytes"
+	"encoding/json"
 	"fmt"
 	"strconv"
 	"strings"
@@ -23,6 +25,20 @@ func getIoAPIKey(c *gin.Context) (string, bool) {
 	return apiKey, true
 }
 
+func GetModelDeploymentSettings(c *gin.Context) {
+	common.OptionMapRWMutex.RLock()
+	enabled := common.OptionMap["model_deployment.ionet.enabled"] == "true"
+	hasAPIKey := strings.TrimSpace(common.OptionMap["model_deployment.ionet.api_key"]) != ""
+	common.OptionMapRWMutex.RUnlock()
+
+	common.ApiSuccess(c, gin.H{
+		"provider":    "io.net",
+		"enabled":     enabled,
+		"configured":  hasAPIKey,
+		"can_connect": enabled && hasAPIKey,
+	})
+}
+
 func getIoClient(c *gin.Context) (*ionet.Client, bool) {
 	apiKey, ok := getIoAPIKey(c)
 	if !ok {
@@ -44,15 +60,28 @@ func TestIoNetConnection(c *gin.Context) {
 		APIKey string `json:"api_key"`
 	}
 
-	if err := c.ShouldBindJSON(&req); err != nil {
-		common.ApiErrorMsg(c, "invalid request payload")
+	rawBody, err := c.GetRawData()
+	if err != nil {
+		common.ApiError(c, err)
 		return
 	}
+	if len(bytes.TrimSpace(rawBody)) > 0 {
+		if err := json.Unmarshal(rawBody, &req); err != nil {
+			common.ApiErrorMsg(c, "invalid request payload")
+			return
+		}
+	}
 
 	apiKey := strings.TrimSpace(req.APIKey)
 	if apiKey == "" {
-		common.ApiErrorMsg(c, "api_key is required")
-		return
+		common.OptionMapRWMutex.RLock()
+		storedKey := strings.TrimSpace(common.OptionMap["model_deployment.ionet.api_key"])
+		common.OptionMapRWMutex.RUnlock()
+		if storedKey == "" {
+			common.ApiErrorMsg(c, "api_key is required")
+			return
+		}
+		apiKey = storedKey
 	}
 
 	client := ionet.NewEnterpriseClient(apiKey)

+ 5 - 1
controller/option.go

@@ -20,7 +20,11 @@ func GetOptions(c *gin.Context) {
 	var options []*model.Option
 	common.OptionMapRWMutex.Lock()
 	for k, v := range common.OptionMap {
-		if strings.HasSuffix(k, "Token") || strings.HasSuffix(k, "Secret") || strings.HasSuffix(k, "Key") {
+		if strings.HasSuffix(k, "Token") ||
+			strings.HasSuffix(k, "Secret") ||
+			strings.HasSuffix(k, "Key") ||
+			strings.HasSuffix(k, "secret") ||
+			strings.HasSuffix(k, "api_key") {
 			continue
 		}
 		options = append(options, &model.Option{

+ 2 - 16
router/api-router.go

@@ -269,24 +269,18 @@ func SetApiRouter(router *gin.Engine) {
 		deploymentsRoute := apiRouter.Group("/deployments")
 		deploymentsRoute.Use(middleware.AdminAuth())
 		{
-			// List and search deployments
+			deploymentsRoute.GET("/settings", controller.GetModelDeploymentSettings)
+			deploymentsRoute.POST("/settings/test-connection", controller.TestIoNetConnection)
 			deploymentsRoute.GET("/", controller.GetAllDeployments)
 			deploymentsRoute.GET("/search", controller.SearchDeployments)
-
-			// Connection utilities
 			deploymentsRoute.POST("/test-connection", controller.TestIoNetConnection)
-
-			// Resource and configuration endpoints
 			deploymentsRoute.GET("/hardware-types", controller.GetHardwareTypes)
 			deploymentsRoute.GET("/locations", controller.GetLocations)
 			deploymentsRoute.GET("/available-replicas", controller.GetAvailableReplicas)
 			deploymentsRoute.POST("/price-estimation", controller.GetPriceEstimation)
 			deploymentsRoute.GET("/check-name", controller.CheckClusterNameAvailability)
-
-			// Create new deployment
 			deploymentsRoute.POST("/", controller.CreateDeployment)
 
-			// Individual deployment operations
 			deploymentsRoute.GET("/:id", controller.GetDeployment)
 			deploymentsRoute.GET("/:id/logs", controller.GetDeploymentLogs)
 			deploymentsRoute.GET("/:id/containers", controller.ListDeploymentContainers)
@@ -295,14 +289,6 @@ func SetApiRouter(router *gin.Engine) {
 			deploymentsRoute.PUT("/:id/name", controller.UpdateDeploymentName)
 			deploymentsRoute.POST("/:id/extend", controller.ExtendDeployment)
 			deploymentsRoute.DELETE("/:id", controller.DeleteDeployment)
-
-			// Future batch operations:
-			// deploymentsRoute.POST("/:id/start", controller.StartDeployment)
-			// deploymentsRoute.POST("/:id/stop", controller.StopDeployment)
-			// deploymentsRoute.POST("/:id/restart", controller.RestartDeployment)
-			// deploymentsRoute.POST("/batch_delete", controller.BatchDeleteDeployments)
-			// deploymentsRoute.POST("/batch_start", controller.BatchStartDeployments)
-			// deploymentsRoute.POST("/batch_stop", controller.BatchStopDeployments)
 		}
 	}
 }

+ 3 - 1
web/i18next.config.js

@@ -25,7 +25,9 @@ export default defineConfig({
     "zh",
     "en",
     "fr",
-    "ru"
+    "ru",
+    "ja",
+    "vi"
   ],
   extract: {
     input: [

+ 143 - 108
web/src/components/model-deployments/DeploymentAccessGuard.jsx

@@ -46,7 +46,7 @@ const DeploymentAccessGuard = ({
       <div className='mt-[60px] px-2'>
         <Card loading={true} style={{ minHeight: '400px' }}>
           <div style={{ textAlign: 'center', padding: '50px 0' }}>
-            <Text type="secondary">{t('加载设置中...')}</Text>
+            <Text type='secondary'>{t('加载设置中...')}</Text>
           </div>
         </Card>
       </div>
@@ -55,21 +55,21 @@ const DeploymentAccessGuard = ({
 
   if (!isEnabled) {
     return (
-      <div 
-        className='mt-[60px] px-4' 
+      <div
+        className='mt-[60px] px-4'
         style={{
           minHeight: 'calc(100vh - 60px)',
           display: 'flex',
           alignItems: 'center',
-          justifyContent: 'center'
+          justifyContent: 'center',
         }}
       >
-        <div 
+        <div
           style={{
             maxWidth: '600px',
             width: '100%',
             textAlign: 'center',
-            padding: '0 20px'
+            padding: '0 20px',
           }}
         >
           <Card
@@ -78,45 +78,49 @@ const DeploymentAccessGuard = ({
               borderRadius: '16px',
               border: '1px solid var(--semi-color-border)',
               boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)',
-              background: 'linear-gradient(135deg, var(--semi-color-bg-0) 0%, var(--semi-color-fill-0) 100%)'
+              background:
+                'linear-gradient(135deg, var(--semi-color-bg-0) 0%, var(--semi-color-fill-0) 100%)',
             }}
           >
             {/* 图标区域 */}
             <div style={{ marginBottom: '32px' }}>
-              <div style={{ 
-                display: 'inline-flex', 
-                alignItems: 'center',
-                justifyContent: 'center',
-                width: '120px',
-                height: '120px',
-                borderRadius: '50%',
-                background: 'linear-gradient(135deg, rgba(var(--semi-orange-4), 0.15) 0%, rgba(var(--semi-orange-5), 0.1) 100%)',
-                border: '3px solid rgba(var(--semi-orange-4), 0.3)',
-                marginBottom: '24px'
-              }}>
-                <AlertCircle size={56} color="var(--semi-color-warning)" />
+              <div
+                style={{
+                  display: 'inline-flex',
+                  alignItems: 'center',
+                  justifyContent: 'center',
+                  width: '120px',
+                  height: '120px',
+                  borderRadius: '50%',
+                  background:
+                    'linear-gradient(135deg, rgba(var(--semi-orange-4), 0.15) 0%, rgba(var(--semi-orange-5), 0.1) 100%)',
+                  border: '3px solid rgba(var(--semi-orange-4), 0.3)',
+                  marginBottom: '24px',
+                }}
+              >
+                <AlertCircle size={56} color='var(--semi-color-warning)' />
               </div>
             </div>
 
             {/* 标题区域 */}
             <div style={{ marginBottom: '24px' }}>
-              <Title 
-                heading={2} 
-                style={{ 
-                  color: 'var(--semi-color-text-0)', 
+              <Title
+                heading={2}
+                style={{
+                  color: 'var(--semi-color-text-0)',
                   margin: '0 0 12px 0',
                   fontSize: '28px',
-                  fontWeight: '700'
+                  fontWeight: '700',
                 }}
               >
                 {t('模型部署服务未启用')}
               </Title>
-              <Text 
-                style={{ 
-                  fontSize: '18px', 
+              <Text
+                style={{
+                  fontSize: '18px',
                   lineHeight: '1.6',
                   color: 'var(--semi-color-text-1)',
-                  display: 'block'
+                  display: 'block',
                 }}
               >
                 {t('访问模型部署功能需要先启用 io.net 部署服务')}
@@ -124,75 +128,99 @@ const DeploymentAccessGuard = ({
             </div>
 
             {/* 配置要求区域 */}
-            <div 
-              style={{ 
-                backgroundColor: 'var(--semi-color-bg-1)', 
-                padding: '24px', 
+            <div
+              style={{
+                backgroundColor: 'var(--semi-color-bg-1)',
+                padding: '24px',
                 borderRadius: '12px',
                 border: '1px solid var(--semi-color-border)',
                 margin: '32px 0',
-                boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)'
+                boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
               }}
             >
-              <div style={{ 
-                display: 'flex', 
-                alignItems: 'center', 
-                justifyContent: 'center',
-                gap: '12px', 
-                marginBottom: '16px' 
-              }}>
-                <div style={{
+              <div
+                style={{
                   display: 'flex',
                   alignItems: 'center',
                   justifyContent: 'center',
-                  width: '32px',
-                  height: '32px',
-                  borderRadius: '8px',
-                  backgroundColor: 'rgba(var(--semi-blue-4), 0.15)'
-                }}>
-                  <Server size={20} color="var(--semi-color-primary)" />
+                  gap: '12px',
+                  marginBottom: '16px',
+                }}
+              >
+                <div
+                  style={{
+                    display: 'flex',
+                    alignItems: 'center',
+                    justifyContent: 'center',
+                    width: '32px',
+                    height: '32px',
+                    borderRadius: '8px',
+                    backgroundColor: 'rgba(var(--semi-blue-4), 0.15)',
+                  }}
+                >
+                  <Server size={20} color='var(--semi-color-primary)' />
                 </div>
-                <Text 
-                  strong 
-                  style={{ 
-                    fontSize: '16px', 
-                    color: 'var(--semi-color-text-0)' 
+                <Text
+                  strong
+                  style={{
+                    fontSize: '16px',
+                    color: 'var(--semi-color-text-0)',
                   }}
                 >
                   {t('需要配置的项目')}
                 </Text>
               </div>
-              
-              <div style={{ 
-                display: 'flex', 
-                flexDirection: 'column', 
-                gap: '12px',
-                alignItems: 'flex-start',
-                textAlign: 'left',
-                maxWidth: '320px',
-                margin: '0 auto'
-              }}>
-                <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
-                  <div style={{
-                    width: '6px',
-                    height: '6px',
-                    borderRadius: '50%',
-                    backgroundColor: 'var(--semi-color-primary)',
-                    flexShrink: 0
-                  }}></div>
-                  <Text style={{ fontSize: '15px', color: 'var(--semi-color-text-1)' }}>
+
+              <div
+                style={{
+                  display: 'flex',
+                  flexDirection: 'column',
+                  gap: '12px',
+                  alignItems: 'flex-start',
+                  textAlign: 'left',
+                  maxWidth: '320px',
+                  margin: '0 auto',
+                }}
+              >
+                <div
+                  style={{ display: 'flex', alignItems: 'center', gap: '12px' }}
+                >
+                  <div
+                    style={{
+                      width: '6px',
+                      height: '6px',
+                      borderRadius: '50%',
+                      backgroundColor: 'var(--semi-color-primary)',
+                      flexShrink: 0,
+                    }}
+                  ></div>
+                  <Text
+                    style={{
+                      fontSize: '15px',
+                      color: 'var(--semi-color-text-1)',
+                    }}
+                  >
                     {t('启用 io.net 部署开关')}
                   </Text>
                 </div>
-                <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
-                  <div style={{
-                    width: '6px',
-                    height: '6px',
-                    borderRadius: '50%',
-                    backgroundColor: 'var(--semi-color-primary)',
-                    flexShrink: 0
-                  }}></div>
-                  <Text style={{ fontSize: '15px', color: 'var(--semi-color-text-1)' }}>
+                <div
+                  style={{ display: 'flex', alignItems: 'center', gap: '12px' }}
+                >
+                  <div
+                    style={{
+                      width: '6px',
+                      height: '6px',
+                      borderRadius: '50%',
+                      backgroundColor: 'var(--semi-color-primary)',
+                      flexShrink: 0,
+                    }}
+                  ></div>
+                  <Text
+                    style={{
+                      fontSize: '15px',
+                      color: 'var(--semi-color-text-1)',
+                    }}
+                  >
                     {t('配置有效的 io.net API Key')}
                   </Text>
                 </div>
@@ -201,9 +229,9 @@ const DeploymentAccessGuard = ({
 
             {/* 操作链接区域 */}
             <div style={{ marginBottom: '20px' }}>
-              <div 
+              <div
                 onClick={handleGoToSettings}
-                style={{ 
+                style={{
                   display: 'inline-flex',
                   alignItems: 'center',
                   gap: '8px',
@@ -216,17 +244,18 @@ const DeploymentAccessGuard = ({
                   background: 'var(--semi-color-fill-0)',
                   border: '1px solid var(--semi-color-border)',
                   transition: 'all 0.2s ease',
-                  textDecoration: 'none'
+                  textDecoration: 'none',
                 }}
                 onMouseEnter={(e) => {
-                  e.target.style.background = 'var(--semi-color-fill-1)';
-                  e.target.style.transform = 'translateY(-1px)';
-                  e.target.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.1)';
+                  e.currentTarget.style.background = 'var(--semi-color-fill-1)';
+                  e.currentTarget.style.transform = 'translateY(-1px)';
+                  e.currentTarget.style.boxShadow =
+                    '0 2px 8px rgba(0, 0, 0, 0.1)';
                 }}
                 onMouseLeave={(e) => {
-                  e.target.style.background = 'var(--semi-color-fill-0)';
-                  e.target.style.transform = 'translateY(0)';
-                  e.target.style.boxShadow = 'none';
+                  e.currentTarget.style.background = 'var(--semi-color-fill-0)';
+                  e.currentTarget.style.transform = 'translateY(0)';
+                  e.currentTarget.style.boxShadow = 'none';
                 }}
               >
                 <Settings size={18} />
@@ -235,12 +264,12 @@ const DeploymentAccessGuard = ({
             </div>
 
             {/* 底部提示 */}
-            <Text 
-              type="tertiary" 
-              style={{ 
+            <Text
+              type='tertiary'
+              style={{
                 fontSize: '14px',
                 color: 'var(--semi-color-text-2)',
-                lineHeight: '1.5'
+                lineHeight: '1.5',
               }}
             >
               {t('配置完成后刷新页面即可使用模型部署功能')}
@@ -256,7 +285,7 @@ const DeploymentAccessGuard = ({
       <div className='mt-[60px] px-2'>
         <Card loading={true} style={{ minHeight: '400px' }}>
           <div style={{ textAlign: 'center', padding: '50px 0' }}>
-            <Text type="secondary">{t('Checking io.net connection...')}</Text>
+            <Text type='secondary'>{t('正在检查 io.net 连接...')}</Text>
           </div>
         </Card>
       </div>
@@ -265,12 +294,10 @@ const DeploymentAccessGuard = ({
 
   if (connectionOk === false) {
     const isExpired = connectionError?.type === 'expired';
-    const title = isExpired
-      ? t('API key expired')
-      : t('io.net connection unavailable');
+    const title = isExpired ? t('接口密钥已过期') : t('无法连接 io.net');
     const description = isExpired
-      ? t('The current API key is expired. Please update it in settings.')
-      : t('Unable to connect to io.net with the current configuration.');
+      ? t('当前 API 密钥已过期,请在设置中更新。')
+      : t('当前配置无法连接到 io.net。');
     const detail = connectionError?.message || '';
 
     return (
@@ -297,7 +324,8 @@ const DeploymentAccessGuard = ({
               borderRadius: '16px',
               border: '1px solid var(--semi-color-border)',
               boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)',
-              background: 'linear-gradient(135deg, var(--semi-color-bg-0) 0%, var(--semi-color-fill-0) 100%)',
+              background:
+                'linear-gradient(135deg, var(--semi-color-bg-0) 0%, var(--semi-color-fill-0) 100%)',
             }}
           >
             <div style={{ marginBottom: '32px' }}>
@@ -309,12 +337,13 @@ const DeploymentAccessGuard = ({
                   width: '120px',
                   height: '120px',
                   borderRadius: '50%',
-                  background: 'linear-gradient(135deg, rgba(var(--semi-red-4), 0.15) 0%, rgba(var(--semi-red-5), 0.1) 100%)',
+                  background:
+                    'linear-gradient(135deg, rgba(var(--semi-red-4), 0.15) 0%, rgba(var(--semi-red-5), 0.1) 100%)',
                   border: '3px solid rgba(var(--semi-red-4), 0.3)',
                   marginBottom: '24px',
                 }}
               >
-                <WifiOff size={56} color="var(--semi-color-danger)" />
+                <WifiOff size={56} color='var(--semi-color-danger)' />
               </div>
             </div>
 
@@ -342,7 +371,7 @@ const DeploymentAccessGuard = ({
               </Text>
               {detail ? (
                 <Text
-                  type="tertiary"
+                  type='tertiary'
                   style={{
                     fontSize: '14px',
                     lineHeight: '1.5',
@@ -355,13 +384,19 @@ const DeploymentAccessGuard = ({
               ) : null}
             </div>
 
-            <div style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}>
-              <Button type="primary" icon={<Settings size={18} />} onClick={handleGoToSettings}>
-                {t('Go to settings')}
+            <div
+              style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}
+            >
+              <Button
+                type='primary'
+                icon={<Settings size={18} />}
+                onClick={handleGoToSettings}
+              >
+                {t('前往设置')}
               </Button>
               {onRetry ? (
-                <Button type="tertiary" onClick={onRetry}>
-                  {t('Retry connection')}
+                <Button type='tertiary' onClick={onRetry}>
+                  {t('重试连接')}
                 </Button>
               ) : null}
             </div>

+ 35 - 9
web/src/components/settings/personal/cards/NotificationSettings.jsx

@@ -44,7 +44,10 @@ import CodeViewer from '../../../playground/CodeViewer';
 import { StatusContext } from '../../../../context/Status';
 import { UserContext } from '../../../../context/User';
 import { useUserPermissions } from '../../../../hooks/common/useUserPermissions';
-import { useSidebar } from '../../../../hooks/common/useSidebar';
+import {
+  mergeAdminConfig,
+  useSidebar,
+} from '../../../../hooks/common/useSidebar';
 
 const NotificationSettings = ({
   t,
@@ -82,6 +85,7 @@ const NotificationSettings = ({
       enabled: true,
       channel: true,
       models: true,
+      deployment: true,
       redemption: true,
       user: true,
       setting: true,
@@ -164,6 +168,7 @@ const NotificationSettings = ({
         enabled: true,
         channel: true,
         models: true,
+        deployment: true,
         redemption: true,
         user: true,
         setting: true,
@@ -178,14 +183,27 @@ const NotificationSettings = ({
       try {
         // 获取管理员全局配置
         if (statusState?.status?.SidebarModulesAdmin) {
-          const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
-          setAdminConfig(adminConf);
+          try {
+            const adminConf = JSON.parse(
+              statusState.status.SidebarModulesAdmin,
+            );
+            setAdminConfig(mergeAdminConfig(adminConf));
+          } catch (error) {
+            setAdminConfig(mergeAdminConfig(null));
+          }
+        } else {
+          setAdminConfig(mergeAdminConfig(null));
         }
 
         // 获取用户个人配置
         const userRes = await API.get('/api/user/self');
         if (userRes.data.success && userRes.data.data.sidebar_modules) {
-          const userConf = JSON.parse(userRes.data.data.sidebar_modules);
+          let userConf;
+          if (typeof userRes.data.data.sidebar_modules === 'string') {
+            userConf = JSON.parse(userRes.data.data.sidebar_modules);
+          } else {
+            userConf = userRes.data.data.sidebar_modules;
+          }
           setSidebarModulesUser(userConf);
         }
       } catch (error) {
@@ -273,6 +291,11 @@ const NotificationSettings = ({
       modules: [
         { key: 'channel', title: t('渠道管理'), description: t('API渠道配置') },
         { key: 'models', title: t('模型管理'), description: t('AI模型配置') },
+        {
+          key: 'deployment',
+          title: t('模型部署'),
+          description: t('模型部署管理'),
+        },
         {
           key: 'redemption',
           title: t('兑换码管理'),
@@ -812,7 +835,9 @@ const NotificationSettings = ({
                             </Typography.Text>
                           </div>
                           <Switch
-                            checked={sidebarModulesUser[section.key]?.enabled}
+                            checked={
+                              sidebarModulesUser[section.key]?.enabled !== false
+                            }
                             onChange={handleSectionChange(section.key)}
                             size='default'
                           />
@@ -835,7 +860,8 @@ const NotificationSettings = ({
                               >
                                 <Card
                                   className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
-                                    sidebarModulesUser[section.key]?.enabled
+                                    sidebarModulesUser[section.key]?.enabled !==
+                                    false
                                       ? ''
                                       : 'opacity-50'
                                   }`}
@@ -866,7 +892,7 @@ const NotificationSettings = ({
                                         checked={
                                           sidebarModulesUser[section.key]?.[
                                             module.key
-                                          ]
+                                          ] !== false
                                         }
                                         onChange={handleModuleChange(
                                           section.key,
@@ -874,8 +900,8 @@ const NotificationSettings = ({
                                         )}
                                         size='default'
                                         disabled={
-                                          !sidebarModulesUser[section.key]
-                                            ?.enabled
+                                          sidebarModulesUser[section.key]
+                                            ?.enabled === false
                                         }
                                       />
                                     </div>

+ 112 - 140
web/src/components/table/channels/modals/OllamaModelModal.jsx

@@ -30,30 +30,24 @@ import {
   Spin,
   Popconfirm,
   Tag,
-  Avatar,
   Empty,
-  Divider,
   Row,
   Col,
   Progress,
   Checkbox,
-  Radio,
 } from '@douyinfe/semi-ui';
 import {
-  IconClose,
   IconDownload,
   IconDelete,
   IconRefresh,
   IconSearch,
   IconPlus,
-  IconServer,
 } from '@douyinfe/semi-icons';
 import {
   API,
   authHeader,
   getUserIdFromLocalStorage,
   showError,
-  showInfo,
   showSuccess,
 } from '../../../../helpers';
 
@@ -85,9 +79,7 @@ const resolveOllamaBaseUrl = (info) => {
   }
 
   const alt =
-    typeof info.ollama_base_url === 'string'
-      ? info.ollama_base_url.trim()
-      : '';
+    typeof info.ollama_base_url === 'string' ? info.ollama_base_url.trim() : '';
   if (alt) {
     return alt;
   }
@@ -125,7 +117,8 @@ const normalizeModels = (items) => {
       }
 
       if (typeof item === 'object') {
-        const candidateId = item.id || item.ID || item.name || item.model || item.Model;
+        const candidateId =
+          item.id || item.ID || item.name || item.model || item.Model;
         if (!candidateId) {
           return null;
         }
@@ -147,7 +140,10 @@ const normalizeModels = (items) => {
           if (!normalized.digest && typeof metadata.digest === 'string') {
             normalized.digest = metadata.digest;
           }
-          if (!normalized.modified_at && typeof metadata.modified_at === 'string') {
+          if (
+            !normalized.modified_at &&
+            typeof metadata.modified_at === 'string'
+          ) {
             normalized.modified_at = metadata.modified_at;
           }
           if (metadata.details && !normalized.details) {
@@ -440,7 +436,6 @@ const OllamaModelModal = ({
       };
 
       await processStream();
-
     } catch (error) {
       if (error?.name !== 'AbortError') {
         showError(t('模型拉取失败: {{error}}', { error: error.message }));
@@ -461,7 +456,7 @@ const OllamaModelModal = ({
           model_name: modelName,
         },
       });
-      
+
       if (res.data.success) {
         showSuccess(t('模型删除成功'));
         await fetchModels(); // 重新获取模型列表
@@ -481,8 +476,8 @@ const OllamaModelModal = ({
     if (!searchValue) {
       setFilteredModels(models);
     } else {
-      const filtered = models.filter(model =>
-        model.id.toLowerCase().includes(searchValue.toLowerCase())
+      const filtered = models.filter((model) =>
+        model.id.toLowerCase().includes(searchValue.toLowerCase()),
       );
       setFilteredModels(filtered);
     }
@@ -527,60 +522,38 @@ const OllamaModelModal = ({
   const formatModelSize = (size) => {
     if (!size) return '-';
     const gb = size / (1024 * 1024 * 1024);
-    return gb >= 1 ? `${gb.toFixed(1)} GB` : `${(size / (1024 * 1024)).toFixed(0)} MB`;
+    return gb >= 1
+      ? `${gb.toFixed(1)} GB`
+      : `${(size / (1024 * 1024)).toFixed(0)} MB`;
   };
 
   return (
     <Modal
-      title={
-        <div className='flex items-center'>
-          <Avatar
-            size='small'
-            color='blue'
-            className='mr-3 shadow-md'
-          >
-            <IconServer size={16} />
-          </Avatar>
-          <div>
-            <Title heading={4} className='m-0'>
-              {t('Ollama 模型管理')}
-            </Title>
-            <Text type='tertiary' size='small'>
-              {channelInfo?.name && `${channelInfo.name} - `}
-              {t('管理 Ollama 模型的拉取和删除')}
-            </Text>
-          </div>
-        </div>
-      }
+      title={t('Ollama 模型管理')}
       visible={visible}
       onCancel={onCancel}
-      width={800}
+      width={720}
       style={{ maxWidth: '95vw' }}
       footer={
-        <div className='flex justify-end'>
-          <Button
-            theme='light'
-            type='primary'
-            onClick={onCancel}
-            icon={<IconClose />}
-          >
-            {t('关闭')}
-          </Button>
-        </div>
+        <Button theme='solid' type='primary' onClick={onCancel}>
+          {t('关闭')}
+        </Button>
       }
     >
-      <div className='space-y-6'>
+      <Space vertical spacing='medium' style={{ width: '100%' }}>
+        <div>
+          <Text type='tertiary' size='small'>
+            {channelInfo?.name ? `${channelInfo.name} - ` : ''}
+            {t('管理 Ollama 模型的拉取和删除')}
+          </Text>
+        </div>
+
         {/* 拉取新模型 */}
-        <Card className='!rounded-2xl shadow-sm border-0'>
-          <div className='flex items-center mb-4'>
-            <Avatar size='small' color='green' className='mr-2'>
-              <IconPlus size={16} />
-            </Avatar>
-            <Title heading={5} className='m-0'>
-              {t('拉取新模型')}
-            </Title>
-          </div>
-          
+        <Card>
+          <Title heading={6} className='m-0 mb-3'>
+            {t('拉取新模型')}
+          </Title>
+
           <Row gutter={12} align='middle'>
             <Col span={16}>
               <Input
@@ -606,76 +579,81 @@ const OllamaModelModal = ({
               </Button>
             </Col>
           </Row>
-          
+
           {/* 进度条显示 */}
-          {pullProgress && (() => {
-            const completedBytes = Number(pullProgress.completed) || 0;
-            const totalBytes = Number(pullProgress.total) || 0;
-            const hasTotal = Number.isFinite(totalBytes) && totalBytes > 0;
-            const safePercent = hasTotal
-              ? Math.min(
-                  100,
-                  Math.max(0, Math.round((completedBytes / totalBytes) * 100)),
-                )
-              : null;
-            const percentText = hasTotal && safePercent !== null
-              ? `${safePercent.toFixed(0)}%`
-              : pullProgress.status || t('处理中');
-
-            return (
-              <div className='mt-3 p-3 bg-gray-50 rounded-lg'>
-                <div className='flex items-center justify-between mb-2'>
-                  <Text strong>{t('拉取进度')}</Text>
-                  <Text type='tertiary' size='small'>{percentText}</Text>
-                </div>
+          {pullProgress &&
+            (() => {
+              const completedBytes = Number(pullProgress.completed) || 0;
+              const totalBytes = Number(pullProgress.total) || 0;
+              const hasTotal = Number.isFinite(totalBytes) && totalBytes > 0;
+              const safePercent = hasTotal
+                ? Math.min(
+                    100,
+                    Math.max(
+                      0,
+                      Math.round((completedBytes / totalBytes) * 100),
+                    ),
+                  )
+                : null;
+              const percentText =
+                hasTotal && safePercent !== null
+                  ? `${safePercent.toFixed(0)}%`
+                  : pullProgress.status || t('处理中');
+
+              return (
+                <div style={{ marginTop: 12 }}>
+                  <div className='flex items-center justify-between mb-2'>
+                    <Text strong>{t('拉取进度')}</Text>
+                    <Text type='tertiary' size='small'>
+                      {percentText}
+                    </Text>
+                  </div>
 
-                {hasTotal && safePercent !== null ? (
-                  <div>
-                    <Progress
-                      percent={safePercent}
-                      showInfo={false}
-                      stroke='#1890ff'
-                      size='small'
-                    />
-                    <div className='flex justify-between mt-1'>
-                      <Text type='tertiary' size='small'>
-                        {(completedBytes / (1024 * 1024 * 1024)).toFixed(2)} GB
-                      </Text>
-                      <Text type='tertiary' size='small'>
-                        {(totalBytes / (1024 * 1024 * 1024)).toFixed(2)} GB
-                      </Text>
+                  {hasTotal && safePercent !== null ? (
+                    <div>
+                      <Progress
+                        percent={safePercent}
+                        showInfo={false}
+                        stroke='#1890ff'
+                        size='small'
+                      />
+                      <div className='flex justify-between mt-1'>
+                        <Text type='tertiary' size='small'>
+                          {(completedBytes / (1024 * 1024 * 1024)).toFixed(2)}{' '}
+                          GB
+                        </Text>
+                        <Text type='tertiary' size='small'>
+                          {(totalBytes / (1024 * 1024 * 1024)).toFixed(2)} GB
+                        </Text>
+                      </div>
                     </div>
-                  </div>
-                ) : (
-                  <div className='flex items-center gap-2 text-xs text-[var(--semi-color-text-2)]'>
-                    <Spin size='small' />
-                    <span>{t('准备中...')}</span>
-                  </div>
-                )}
-              </div>
-            );
-          })()}
-          
+                  ) : (
+                    <div className='flex items-center gap-2 text-xs text-[var(--semi-color-text-2)]'>
+                      <Spin size='small' />
+                      <span>{t('准备中...')}</span>
+                    </div>
+                  )}
+                </div>
+              );
+            })()}
+
           <Text type='tertiary' size='small' className='mt-2 block'>
-            {t('支持拉取 Ollama 官方模型库中的所有模型,拉取过程可能需要几分钟时间')}
+            {t(
+              '支持拉取 Ollama 官方模型库中的所有模型,拉取过程可能需要几分钟时间',
+            )}
           </Text>
         </Card>
 
         {/* 已有模型列表 */}
-        <Card className='!rounded-2xl shadow-sm border-0'>
-          <div className='flex items-center justify-between mb-4'>
-            <div className='flex items-center'>
-              <Avatar size='small' color='purple' className='mr-2'>
-                <IconServer size={16} />
-              </Avatar>
-              <Title heading={5} className='m-0'>
+        <Card>
+          <div className='flex items-center justify-between mb-3'>
+            <div className='flex items-center gap-2'>
+              <Title heading={6} className='m-0'>
                 {t('已有模型')}
-                {models.length > 0 && (
-                  <Tag color='blue' className='ml-2'>
-                    {models.length}
-                  </Tag>
-                )}
               </Title>
+              {models.length > 0 ? (
+                <Tag color='blue'>{models.length}</Tag>
+              ) : null}
             </div>
             <Space wrap>
               <Input
@@ -688,7 +666,7 @@ const OllamaModelModal = ({
               />
               <Button
                 size='small'
-                theme='borderless'
+                theme='light'
                 onClick={handleSelectAll}
                 disabled={models.length === 0}
               >
@@ -696,7 +674,7 @@ const OllamaModelModal = ({
               </Button>
               <Button
                 size='small'
-                theme='borderless'
+                theme='light'
                 onClick={handleClearSelection}
                 disabled={selectedModelIds.length === 0}
               >
@@ -728,11 +706,10 @@ const OllamaModelModal = ({
           <Spin spinning={loading}>
             {filteredModels.length === 0 ? (
               <Empty
-                image={<IconServer size={60} />}
                 title={searchValue ? t('未找到匹配的模型') : t('暂无模型')}
                 description={
-                  searchValue 
-                    ? t('请尝试其他搜索关键词') 
+                  searchValue
+                    ? t('请尝试其他搜索关键词')
                     : t('您可以在上方拉取需要的模型')
                 }
                 style={{ padding: '40px 0' }}
@@ -740,25 +717,17 @@ const OllamaModelModal = ({
             ) : (
               <List
                 dataSource={filteredModels}
-                split={false}
-                renderItem={(model, index) => (
-                  <List.Item
-                    key={model.id}
-                    className='hover:bg-gray-50 rounded-lg p-3 transition-colors'
-                  >
+                split
+                renderItem={(model) => (
+                  <List.Item key={model.id}>
                     <div className='flex items-center justify-between w-full'>
                       <div className='flex items-center flex-1 min-w-0 gap-3'>
                         <Checkbox
                           checked={selectedModelIds.includes(model.id)}
-                          onChange={(checked) => handleToggleModel(model.id, checked)}
+                          onChange={(checked) =>
+                            handleToggleModel(model.id, checked)
+                          }
                         />
-                        <Avatar
-                          size='small'
-                          color='blue'
-                          className='flex-shrink-0'
-                        >
-                          {model.id.charAt(0).toUpperCase()}
-                        </Avatar>
                         <div className='flex-1 min-w-0'>
                           <Text strong className='block truncate'>
                             {model.id}
@@ -775,10 +744,13 @@ const OllamaModelModal = ({
                           </div>
                         </div>
                       </div>
-                    <div className='flex items-center space-x-2 ml-4'>
+                      <div className='flex items-center space-x-2 ml-4'>
                         <Popconfirm
                           title={t('确认删除模型')}
-                          content={t('删除后无法恢复,确定要删除模型 "{{name}}" 吗?', { name: model.id })}
+                          content={t(
+                            '删除后无法恢复,确定要删除模型 "{{name}}" 吗?',
+                            { name: model.id },
+                          )}
                           onConfirm={() => deleteModel(model.id)}
                           okText={t('确认')}
                           cancelText={t('取消')}
@@ -798,7 +770,7 @@ const OllamaModelModal = ({
             )}
           </Spin>
         </Card>
-      </div>
+      </Space>
     </Modal>
   );
 };

+ 2 - 2
web/src/components/table/model-deployments/DeploymentsActions.jsx

@@ -27,13 +27,14 @@ const DeploymentsActions = ({
   setEditingDeployment,
   setShowEdit,
   batchDeleteDeployments,
+  batchOperationsEnabled = true,
   compactMode,
   setCompactMode,
   showCreateModal,
   setShowCreateModal,
   t,
 }) => {
-  const hasSelected = selectedKeys.length > 0;
+  const hasSelected = batchOperationsEnabled && selectedKeys.length > 0;
 
   const handleAddDeployment = () => {
     if (setShowCreateModal) {
@@ -53,7 +54,6 @@ const DeploymentsActions = ({
     setSelectedKeys([]);
   };
 
-
   return (
     <div className='flex flex-wrap gap-2 w-full md:w-auto order-2 md:order-1'>
       <Button

+ 137 - 107
web/src/components/table/model-deployments/DeploymentsColumnDefs.jsx

@@ -18,17 +18,8 @@ For commercial licensing, please contact [email protected]
 */
 
 import React from 'react';
-import { 
-  Button,
-  Dropdown,
-  Tag,
-  Typography,
-} from '@douyinfe/semi-ui';
-import {
-  timestamp2string,
-  showSuccess,
-  showError,
-} from '../../../helpers';
+import { Button, Dropdown, Tag, Typography } from '@douyinfe/semi-ui';
+import { timestamp2string, showSuccess, showError } from '../../../helpers';
 import { IconMore } from '@douyinfe/semi-icons';
 import {
   FaPlay,
@@ -50,7 +41,6 @@ import {
   FaHourglassHalf,
   FaGlobe,
 } from 'react-icons/fa';
-import {t} from "i18next";
 
 const normalizeStatus = (status) =>
   typeof status === 'string' ? status.trim().toLowerCase() : '';
@@ -58,59 +48,59 @@ const normalizeStatus = (status) =>
 const STATUS_TAG_CONFIG = {
   running: {
     color: 'green',
-    label: t('运行中'),
+    labelKey: '运行中',
     icon: <FaPlay size={12} className='text-green-600' />,
   },
   deploying: {
     color: 'blue',
-    label: t('部署中'),
+    labelKey: '部署中',
     icon: <FaSpinner size={12} className='text-blue-600' />,
   },
   pending: {
     color: 'orange',
-    label: t('待部署'),
+    labelKey: '待部署',
     icon: <FaClock size={12} className='text-orange-600' />,
   },
   stopped: {
     color: 'grey',
-    label: t('已停止'),
+    labelKey: '已停止',
     icon: <FaStop size={12} className='text-gray-500' />,
   },
   error: {
     color: 'red',
-    label: t('错误'),
+    labelKey: '错误',
     icon: <FaExclamationCircle size={12} className='text-red-500' />,
   },
   failed: {
     color: 'red',
-    label: t('失败'),
+    labelKey: '失败',
     icon: <FaExclamationCircle size={12} className='text-red-500' />,
   },
   destroyed: {
     color: 'red',
-    label: t('已销毁'),
+    labelKey: '已销毁',
     icon: <FaBan size={12} className='text-red-500' />,
   },
   completed: {
     color: 'green',
-    label: t('已完成'),
+    labelKey: '已完成',
     icon: <FaCheckCircle size={12} className='text-green-600' />,
   },
   'deployment requested': {
     color: 'blue',
-    label: t('部署请求中'),
+    labelKey: '部署请求中',
     icon: <FaSpinner size={12} className='text-blue-600' />,
   },
   'termination requested': {
     color: 'orange',
-    label: t('终止请求中'),
+    labelKey: '终止请求中',
     icon: <FaClock size={12} className='text-orange-600' />,
   },
 };
 
 const DEFAULT_STATUS_CONFIG = {
   color: 'grey',
-  label: null,
+  labelKey: null,
   icon: <FaInfoCircle size={12} className='text-gray-500' />,
 };
 
@@ -190,7 +180,9 @@ const renderStatus = (status, t) => {
   const normalizedStatus = normalizeStatus(status);
   const config = STATUS_TAG_CONFIG[normalizedStatus] || DEFAULT_STATUS_CONFIG;
   const statusText = typeof status === 'string' ? status : '';
-  const labelText = config.label ? t(config.label) : statusText || t('未知状态');
+  const labelText = config.labelKey
+    ? t(config.labelKey)
+    : statusText || t('未知状态');
 
   return (
     <Tag
@@ -206,20 +198,24 @@ const renderStatus = (status, t) => {
 
 // Container Name Cell Component - to properly handle React hooks
 const ContainerNameCell = ({ text, record, t }) => {
-  const handleCopyId = () => {
-    navigator.clipboard.writeText(record.id);
-    showSuccess(t('ID已复制到剪贴板'));
+  const handleCopyId = async () => {
+    try {
+      await navigator.clipboard.writeText(record.id);
+      showSuccess(t('已复制 ID 到剪贴板'));
+    } catch (err) {
+      showError(t('复制失败'));
+    }
   };
 
   return (
-    <div className="flex flex-col gap-1">
-      <Typography.Text strong className="text-base">
+    <div className='flex flex-col gap-1'>
+      <Typography.Text strong className='text-base'>
         {text}
       </Typography.Text>
-      <Typography.Text 
-        type="secondary" 
-        size="small" 
-        className="text-xs cursor-pointer hover:text-blue-600 transition-colors select-all"
+      <Typography.Text
+        type='secondary'
+        size='small'
+        className='text-xs cursor-pointer hover:text-blue-600 transition-colors select-all'
         onClick={handleCopyId}
         title={t('点击复制ID')}
       >
@@ -232,26 +228,26 @@ const ContainerNameCell = ({ text, record, t }) => {
 // Render resource configuration
 const renderResourceConfig = (resource, t) => {
   if (!resource) return '-';
-  
+
   const { cpu, memory, gpu } = resource;
-  
+
   return (
-    <div className="flex flex-col gap-1">
+    <div className='flex flex-col gap-1'>
       {cpu && (
-        <div className="flex items-center gap-1 text-xs">
-          <FaMicrochip className="text-blue-500" />
+        <div className='flex items-center gap-1 text-xs'>
+          <FaMicrochip className='text-blue-500' />
           <span>CPU: {cpu}</span>
         </div>
       )}
       {memory && (
-        <div className="flex items-center gap-1 text-xs">
-          <FaMemory className="text-green-500" />
+        <div className='flex items-center gap-1 text-xs'>
+          <FaMemory className='text-green-500' />
           <span>内存: {memory}</span>
         </div>
       )}
       {gpu && (
-        <div className="flex items-center gap-1 text-xs">
-          <FaServer className="text-purple-500" />
+        <div className='flex items-center gap-1 text-xs'>
+          <FaServer className='text-purple-500' />
           <span>GPU: {gpu}</span>
         </div>
       )}
@@ -266,7 +262,7 @@ const renderInstanceCount = (count, record, t) => {
   const countColor = statusConfig?.color ?? 'grey';
 
   return (
-    <Tag color={countColor} size="small" shape='circle'>
+    <Tag color={countColor} size='small' shape='circle'>
       {count || 0} {t('个实例')}
     </Tag>
   );
@@ -299,11 +295,7 @@ export const getDeploymentsColumns = ({
       width: 300,
       ellipsis: true,
       render: (text, record) => (
-        <ContainerNameCell 
-          text={text} 
-          record={record} 
-          t={t}
-        />
+        <ContainerNameCell text={text} record={record} t={t} />
       ),
     },
     {
@@ -312,9 +304,7 @@ export const getDeploymentsColumns = ({
       key: COLUMN_KEYS.status,
       width: 140,
       render: (status) => (
-        <div className="flex items-center gap-2">
-          {renderStatus(status, t)}
-        </div>
+        <div className='flex items-center gap-2'>{renderStatus(status, t)}</div>
       ),
     },
     {
@@ -325,18 +315,22 @@ export const getDeploymentsColumns = ({
       render: (provider) =>
         provider ? (
           <div
-            className="flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide"
+            className='flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide'
             style={{
               borderColor: 'rgba(59, 130, 246, 0.4)',
               backgroundColor: 'rgba(59, 130, 246, 0.08)',
               color: '#2563eb',
             }}
           >
-            <FaGlobe className="text-[11px]" />
+            <FaGlobe className='text-[11px]' />
             <span>{provider}</span>
           </div>
         ) : (
-          <Typography.Text type="tertiary" size="small" className="text-xs text-gray-500">
+          <Typography.Text
+            type='tertiary'
+            size='small'
+            className='text-xs text-gray-500'
+          >
             {t('暂无')}
           </Typography.Text>
         ),
@@ -345,7 +339,7 @@ export const getDeploymentsColumns = ({
       title: t('剩余时间'),
       dataIndex: 'time_remaining',
       key: COLUMN_KEYS.time_remaining,
-      width: 140,
+      width: 200,
       render: (text, record) => {
         const normalizedStatus = normalizeStatus(record?.status);
         const percentUsedRaw = parsePercentValue(record?.completed_percent);
@@ -380,43 +374,43 @@ export const getDeploymentsColumns = ({
           percentRemaining !== null;
 
         return (
-          <div className="flex flex-col gap-1 leading-tight text-xs">
-            <div className="flex items-center gap-1.5">
+          <div className='flex flex-col gap-1 leading-tight text-xs'>
+            <div className='flex items-center gap-1.5'>
               <FaHourglassHalf
-                className="text-sm"
+                className='text-sm'
                 style={{ color: theme.iconColor }}
               />
-              <Typography.Text className="text-sm font-medium text-[var(--semi-color-text-0)]">
+              <Typography.Text className='text-sm font-medium text-[var(--semi-color-text-0)]'>
                 {timeDisplay}
               </Typography.Text>
               {showProgress && percentRemaining !== null ? (
-                <Tag size="small" color={theme.tagColor}>
+                <Tag size='small' color={theme.tagColor}>
                   {percentRemaining}%
                 </Tag>
               ) : statusOverride ? (
-                <Tag size="small" color="grey">
+                <Tag size='small' color='grey'>
                   {statusOverride}
                 </Tag>
               ) : null}
             </div>
             {showExtraInfo && (
-              <div className="flex items-center gap-3 text-[var(--semi-color-text-2)]">
+              <div className='flex items-center gap-3 text-[var(--semi-color-text-2)]'>
                 {humanReadable && (
-                  <span className="flex items-center gap-1">
-                    <FaClock className="text-[11px]" />
+                  <span className='flex items-center gap-1'>
+                    <FaClock className='text-[11px]' />
                     {t('约')} {humanReadable}
                   </span>
                 )}
                 {percentUsed !== null && (
-                  <span className="flex items-center gap-1">
-                    <FaCheckCircle className="text-[11px]" />
+                  <span className='flex items-center gap-1'>
+                    <FaCheckCircle className='text-[11px]' />
                     {t('已用')} {percentUsed}%
                   </span>
                 )}
               </div>
             )}
             {showProgress && showRemainingMeta && (
-              <div className="text-[10px]" style={{ color: theme.textColor }}>
+              <div className='text-[10px]' style={{ color: theme.textColor }}>
                 {t('剩余')} {record.compute_minutes_remaining} {t('分钟')}
               </div>
             )}
@@ -431,14 +425,16 @@ export const getDeploymentsColumns = ({
       width: 220,
       ellipsis: true,
       render: (text, record) => (
-        <div className="flex items-center gap-2">
-          <div className="flex items-center gap-1 px-2 py-1 bg-green-50 border border-green-200 rounded-md">
-            <FaServer className="text-green-600 text-xs" />
-            <span className="text-xs font-medium text-green-700">
+        <div className='flex items-center gap-2'>
+          <div className='flex items-center gap-1 px-2 py-1 bg-green-50 border border-green-200 rounded-md'>
+            <FaServer className='text-green-600 text-xs' />
+            <span className='text-xs font-medium text-green-700'>
               {record.hardware_name}
             </span>
           </div>
-          <span className="text-xs text-gray-500 font-medium">x{record.hardware_quantity}</span>
+          <span className='text-xs text-gray-500 font-medium'>
+            x{record.hardware_quantity}
+          </span>
         </div>
       ),
     },
@@ -448,7 +444,7 @@ export const getDeploymentsColumns = ({
       key: COLUMN_KEYS.created_at,
       width: 150,
       render: (text) => (
-        <span className="text-sm text-gray-600">{timestamp2string(text)}</span>
+        <span className='text-sm text-gray-600'>{timestamp2string(text)}</span>
       ),
     },
     {
@@ -459,7 +455,8 @@ export const getDeploymentsColumns = ({
       render: (_, record) => {
         const { status, id } = record;
         const normalizedStatus = normalizeStatus(status);
-        const isEnded = normalizedStatus === 'completed' || normalizedStatus === 'destroyed';
+        const isEnded =
+          normalizedStatus === 'completed' || normalizedStatus === 'destroyed';
 
         const handleDelete = () => {
           // Use enhanced confirmation dialog
@@ -471,7 +468,7 @@ export const getDeploymentsColumns = ({
           switch (normalizedStatus) {
             case 'running':
               return {
-                icon: <FaInfoCircle className="text-xs" />,
+                icon: <FaInfoCircle className='text-xs' />,
                 text: t('查看详情'),
                 onClick: () => onViewDetails?.(record),
                 type: 'secondary',
@@ -480,7 +477,7 @@ export const getDeploymentsColumns = ({
             case 'failed':
             case 'error':
               return {
-                icon: <FaPlay className="text-xs" />,
+                icon: <FaPlay className='text-xs' />,
                 text: t('重试'),
                 onClick: () => startDeployment(id),
                 type: 'primary',
@@ -488,7 +485,7 @@ export const getDeploymentsColumns = ({
               };
             case 'stopped':
               return {
-                icon: <FaPlay className="text-xs" />,
+                icon: <FaPlay className='text-xs' />,
                 text: t('启动'),
                 onClick: () => startDeployment(id),
                 type: 'primary',
@@ -497,7 +494,7 @@ export const getDeploymentsColumns = ({
             case 'deployment requested':
             case 'deploying':
               return {
-                icon: <FaClock className="text-xs" />,
+                icon: <FaClock className='text-xs' />,
                 text: t('部署中'),
                 onClick: () => {},
                 type: 'secondary',
@@ -506,7 +503,7 @@ export const getDeploymentsColumns = ({
               };
             case 'pending':
               return {
-                icon: <FaClock className="text-xs" />,
+                icon: <FaClock className='text-xs' />,
                 text: t('待部署'),
                 onClick: () => {},
                 type: 'secondary',
@@ -515,7 +512,7 @@ export const getDeploymentsColumns = ({
               };
             case 'termination requested':
               return {
-                icon: <FaClock className="text-xs" />,
+                icon: <FaClock className='text-xs' />,
                 text: t('终止中'),
                 onClick: () => {},
                 type: 'secondary',
@@ -526,7 +523,7 @@ export const getDeploymentsColumns = ({
             case 'destroyed':
             default:
               return {
-                icon: <FaInfoCircle className="text-xs" />,
+                icon: <FaInfoCircle className='text-xs' />,
                 text: t('已结束'),
                 onClick: () => {},
                 type: 'tertiary',
@@ -542,13 +539,13 @@ export const getDeploymentsColumns = ({
 
         if (isEnded) {
           return (
-            <div className="flex w-full items-center justify-start gap-1 pr-2">
+            <div className='flex w-full items-center justify-start gap-1 pr-2'>
               <Button
-                size="small"
-                type="tertiary"
-                theme="borderless"
+                size='small'
+                type='tertiary'
+                theme='borderless'
                 onClick={() => onViewDetails?.(record)}
-                icon={<FaInfoCircle className="text-xs" />}
+                icon={<FaInfoCircle className='text-xs' />}
               >
                 {t('查看详情')}
               </Button>
@@ -558,14 +555,22 @@ export const getDeploymentsColumns = ({
 
         // All actions dropdown with enhanced operations
         const dropdownItems = [
-          <Dropdown.Item key="details" onClick={() => onViewDetails?.(record)} icon={<FaInfoCircle />}>
+          <Dropdown.Item
+            key='details'
+            onClick={() => onViewDetails?.(record)}
+            icon={<FaInfoCircle />}
+          >
             {t('查看详情')}
           </Dropdown.Item>,
         ];
 
         if (!isEnded) {
           dropdownItems.push(
-            <Dropdown.Item key="logs" onClick={() => onViewLogs?.(record)} icon={<FaTerminal />}>
+            <Dropdown.Item
+              key='logs'
+              onClick={() => onViewLogs?.(record)}
+              icon={<FaTerminal />}
+            >
               {t('查看日志')}
             </Dropdown.Item>,
           );
@@ -575,7 +580,11 @@ export const getDeploymentsColumns = ({
         if (normalizedStatus === 'running') {
           if (onSyncToChannel) {
             managementItems.push(
-              <Dropdown.Item key="sync-channel" onClick={() => onSyncToChannel(record)} icon={<FaLink />}>
+              <Dropdown.Item
+                key='sync-channel'
+                onClick={() => onSyncToChannel(record)}
+                icon={<FaLink />}
+              >
                 {t('同步到渠道')}
               </Dropdown.Item>,
             );
@@ -583,28 +592,44 @@ export const getDeploymentsColumns = ({
         }
         if (normalizedStatus === 'failed' || normalizedStatus === 'error') {
           managementItems.push(
-            <Dropdown.Item key="retry" onClick={() => startDeployment(id)} icon={<FaPlay />}>
+            <Dropdown.Item
+              key='retry'
+              onClick={() => startDeployment(id)}
+              icon={<FaPlay />}
+            >
               {t('重试')}
             </Dropdown.Item>,
           );
         }
         if (normalizedStatus === 'stopped') {
           managementItems.push(
-            <Dropdown.Item key="start" onClick={() => startDeployment(id)} icon={<FaPlay />}>
+            <Dropdown.Item
+              key='start'
+              onClick={() => startDeployment(id)}
+              icon={<FaPlay />}
+            >
               {t('启动')}
             </Dropdown.Item>,
           );
         }
 
         if (managementItems.length > 0) {
-          dropdownItems.push(<Dropdown.Divider key="management-divider" />);
+          dropdownItems.push(<Dropdown.Divider key='management-divider' />);
           dropdownItems.push(...managementItems);
         }
 
         const configItems = [];
-        if (!isEnded && (normalizedStatus === 'running' || normalizedStatus === 'deployment requested')) {
+        if (
+          !isEnded &&
+          (normalizedStatus === 'running' ||
+            normalizedStatus === 'deployment requested')
+        ) {
           configItems.push(
-            <Dropdown.Item key="extend" onClick={() => onExtendDuration?.(record)} icon={<FaPlus />}>
+            <Dropdown.Item
+              key='extend'
+              onClick={() => onExtendDuration?.(record)}
+              icon={<FaPlus />}
+            >
               {t('延长时长')}
             </Dropdown.Item>,
           );
@@ -618,13 +643,18 @@ export const getDeploymentsColumns = ({
         // }
 
         if (configItems.length > 0) {
-          dropdownItems.push(<Dropdown.Divider key="config-divider" />);
+          dropdownItems.push(<Dropdown.Divider key='config-divider' />);
           dropdownItems.push(...configItems);
         }
         if (!isEnded) {
-          dropdownItems.push(<Dropdown.Divider key="danger-divider" />);
+          dropdownItems.push(<Dropdown.Divider key='danger-divider' />);
           dropdownItems.push(
-            <Dropdown.Item key="delete" type="danger" onClick={handleDelete} icon={<FaTrash />}>
+            <Dropdown.Item
+              key='delete'
+              type='danger'
+              onClick={handleDelete}
+              icon={<FaTrash />}
+            >
               {t('销毁容器')}
             </Dropdown.Item>,
           );
@@ -634,31 +664,31 @@ export const getDeploymentsColumns = ({
         const hasDropdown = dropdownItems.length > 0;
 
         return (
-          <div className="flex w-full items-center justify-start gap-1 pr-2">
+          <div className='flex w-full items-center justify-start gap-1 pr-2'>
             <Button
-              size="small"
+              size='small'
               theme={primaryTheme}
               type={primaryType}
               icon={primaryAction.icon}
               onClick={primaryAction.onClick}
-              className="px-2 text-xs"
+              className='px-2 text-xs'
               disabled={primaryAction.disabled}
             >
               {primaryAction.text}
             </Button>
-            
+
             {hasDropdown && (
               <Dropdown
-                trigger="click"
-                position="bottomRight"
+                trigger='click'
+                position='bottomRight'
                 render={allActions}
               >
                 <Button
-                  size="small"
-                  theme="light"
-                  type="tertiary"
+                  size='small'
+                  theme='light'
+                  type='tertiary'
                   icon={<IconMore />}
-                  className="px-1"
+                  className='px-1'
                 />
               </Dropdown>
             )}

+ 8 - 8
web/src/components/table/model-deployments/DeploymentsTable.jsx

@@ -43,7 +43,8 @@ const DeploymentsTable = (deploymentsData) => {
     deploymentCount,
     compactMode,
     visibleColumns,
-    setSelectedKeys,
+    rowSelection,
+    batchOperationsEnabled = true,
     handlePageChange,
     handlePageSizeChange,
     handleRow,
@@ -95,7 +96,10 @@ const DeploymentsTable = (deploymentsData) => {
   };
 
   const handleConfirmAction = () => {
-    if (selectedDeployment && confirmOperation === 'delete') {
+    if (
+      selectedDeployment &&
+      (confirmOperation === 'delete' || confirmOperation === 'destroy')
+    ) {
       deleteDeployment(selectedDeployment.id);
     }
     setShowConfirmDialog(false);
@@ -179,11 +183,7 @@ const DeploymentsTable = (deploymentsData) => {
         hidePagination={true}
         expandAllRows={false}
         onRow={handleRow}
-        rowSelection={{
-          onChange: (selectedRowKeys, selectedRows) => {
-            setSelectedKeys(selectedRows);
-          },
-        }}
+        rowSelection={batchOperationsEnabled ? rowSelection : undefined}
         empty={
           <Empty
             image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
@@ -235,7 +235,7 @@ const DeploymentsTable = (deploymentsData) => {
         onCancel={() => setShowConfirmDialog(false)}
         onConfirm={handleConfirmAction}
         title={t('确认操作')}
-        type="danger"
+        type='danger'
         deployment={selectedDeployment}
         operation={confirmOperation}
         t={t}

+ 8 - 3
web/src/components/table/model-deployments/index.jsx

@@ -32,9 +32,10 @@ import { createCardProPagination } from '../../../helpers/utils';
 const DeploymentsPage = () => {
   const deploymentsData = useDeploymentsData();
   const isMobile = useIsMobile();
-  
+
   // Create deployment modal state
   const [showCreateModal, setShowCreateModal] = useState(false);
+  const batchOperationsEnabled = false;
 
   const {
     // Edit state
@@ -81,7 +82,7 @@ const DeploymentsPage = () => {
         visible={showEdit}
         handleClose={closeEdit}
       />
-      
+
       <CreateDeploymentModal
         visible={showCreateModal}
         onCancel={() => setShowCreateModal(false)}
@@ -109,6 +110,7 @@ const DeploymentsPage = () => {
               setEditingDeployment={setEditingDeployment}
               setShowEdit={setShowEdit}
               batchDeleteDeployments={batchDeleteDeployments}
+              batchOperationsEnabled={batchOperationsEnabled}
               compactMode={compactMode}
               setCompactMode={setCompactMode}
               showCreateModal={showCreateModal}
@@ -138,7 +140,10 @@ const DeploymentsPage = () => {
         })}
         t={deploymentsData.t}
       >
-        <DeploymentsTable {...deploymentsData} />
+        <DeploymentsTable
+          {...deploymentsData}
+          batchOperationsEnabled={batchOperationsEnabled}
+        />
       </CardPro>
     </>
   );

+ 506 - 457
web/src/components/table/model-deployments/modals/CreateDeploymentModal.jsx

@@ -38,7 +38,12 @@ import {
   Tooltip,
   Radio,
 } from '@douyinfe/semi-ui';
-import { IconPlus, IconMinus, IconHelpCircle, IconCopy } from '@douyinfe/semi-icons';
+import {
+  IconPlus,
+  IconMinus,
+  IconHelpCircle,
+  IconCopy,
+} from '@douyinfe/semi-icons';
 import { API } from '../../../../helpers';
 import { showError, showSuccess, copy } from '../../../../helpers';
 
@@ -72,17 +77,17 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
   const [hardwareTotalAvailable, setHardwareTotalAvailable] = useState(null);
   const [locations, setLocations] = useState([]);
   const [locationTotalAvailable, setLocationTotalAvailable] = useState(null);
-  const [availableReplicas, setAvailableReplicas] = useState([]);
   const [priceEstimation, setPriceEstimation] = useState(null);
 
   // UI states
   const [loadingHardware, setLoadingHardware] = useState(false);
-  const [loadingLocations, setLoadingLocations] = useState(false);
   const [loadingReplicas, setLoadingReplicas] = useState(false);
   const [loadingPrice, setLoadingPrice] = useState(false);
   const [showAdvanced, setShowAdvanced] = useState(false);
   const [envVariables, setEnvVariables] = useState([{ key: '', value: '' }]);
-  const [secretEnvVariables, setSecretEnvVariables] = useState([{ key: '', value: '' }]);
+  const [secretEnvVariables, setSecretEnvVariables] = useState([
+    { key: '', value: '' },
+  ]);
   const [entrypoint, setEntrypoint] = useState(['']);
   const [args, setArgs] = useState(['']);
   const [imageMode, setImageMode] = useState('builtin');
@@ -95,7 +100,6 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
   const basicSectionRef = useRef(null);
   const priceSectionRef = useRef(null);
   const advancedSectionRef = useRef(null);
-  const locationRequestIdRef = useRef(0);
   const replicaRequestIdRef = useRef(0);
   const [formDefaults, setFormDefaults] = useState({
     resource_private_name: '',
@@ -143,6 +147,13 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
     return map;
   }, [locations]);
 
+  const getHardwareMaxGpus = (hardwareId) => {
+    if (!hardwareId) return 1;
+    const hardware = hardwareTypes.find((h) => h.id === hardwareId);
+    const maxGpus = Number(hardware?.max_gpus);
+    return Number.isFinite(maxGpus) && maxGpus > 0 ? maxGpus : 1;
+  };
+
   // Form values for price calculation
   const [selectedHardwareId, setSelectedHardwareId] = useState(null);
   const [selectedLocationIds, setSelectedLocationIds] = useState([]);
@@ -150,6 +161,20 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
   const [durationHours, setDurationHours] = useState(1);
   const [replicaCount, setReplicaCount] = useState(1);
 
+  useEffect(() => {
+    if (!selectedHardwareId) {
+      return;
+    }
+
+    const nextMaxGpus = getHardwareMaxGpus(selectedHardwareId);
+    if (gpusPerContainer !== nextMaxGpus) {
+      setGpusPerContainer(nextMaxGpus);
+    }
+    if (formApi) {
+      formApi.setValue('gpus_per_container', nextMaxGpus);
+    }
+  }, [selectedHardwareId, hardwareTypes, formApi, gpusPerContainer]);
+
   // Load initial data when modal opens
   useEffect(() => {
     if (visible) {
@@ -206,10 +231,14 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
     if (imageMode === 'builtin') {
       if (prevMode === 'custom') {
         if (formApi) {
-          customImageRef.current = formApi.getValue('image_url') || customImageRef.current;
-          customTrafficPortRef.current = formApi.getValue('traffic_port') ?? customTrafficPortRef.current;
+          customImageRef.current =
+            formApi.getValue('image_url') || customImageRef.current;
+          customTrafficPortRef.current =
+            formApi.getValue('traffic_port') ?? customTrafficPortRef.current;
         }
-        customSecretEnvRef.current = secretEnvVariables.map((item) => ({ ...item }));
+        customSecretEnvRef.current = secretEnvVariables.map((item) => ({
+          ...item,
+        }));
         customEnvRef.current = envVariables.map((item) => ({ ...item }));
       }
       const newKey = generateRandomKey();
@@ -273,15 +302,12 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
       return;
     }
     if (selectedHardwareId) {
-      loadLocations(selectedHardwareId);
+      return;
     } else {
       setLocations([]);
       setSelectedLocationIds([]);
-      setAvailableReplicas([]);
       setLocationTotalAvailable(null);
-      setLoadingLocations(false);
       setLoadingReplicas(false);
-      locationRequestIdRef.current = 0;
       replicaRequestIdRef.current = 0;
       if (formApi) {
         formApi.setValue('location_ids', []);
@@ -299,7 +325,6 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
     setDurationHours(1);
     setReplicaCount(1);
     setPriceEstimation(null);
-    setAvailableReplicas([]);
     setLocations([]);
     setLocationTotalAvailable(null);
     setHardwareTotalAvailable(null);
@@ -336,11 +361,14 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
       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 { 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 availableCount = Number.isNaN(availableCountValue)
+            ? 0
+            : availableCountValue;
           const availableBool =
             typeof hardware.available === 'boolean'
               ? hardware.available
@@ -355,7 +383,9 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
 
         const providedTotal = Number(total_available);
         const fallbackTotal = normalizedHardware.reduce(
-          (acc, item) => acc + (Number.isNaN(item.available_count) ? 0 : item.available_count),
+          (acc, item) =>
+            acc +
+            (Number.isNaN(item.available_count) ? 0 : item.available_count),
           0,
         );
         const hasProvidedTotal =
@@ -378,81 +408,9 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
     }
   };
 
-  const loadLocations = async (hardwareId) => {
-    if (!hardwareId) {
-      setLocations([]);
-      setLocationTotalAvailable(null);
-      return;
-    }
-
-    const requestId = Date.now();
-    locationRequestIdRef.current = requestId;
-    setLoadingLocations(true);
-    setLocations([]);
-    setLocationTotalAvailable(null);
-
-    try {
-      const response = await API.get('/api/deployments/locations', {
-        params: { hardware_id: hardwareId },
-      });
-
-      if (locationRequestIdRef.current !== requestId) {
-        return;
-      }
-
-      if (response.data.success) {
-        const { locations: locationsList = [], total } =
-          response.data.data || {};
-
-        const normalizedLocations = locationsList.map((location) => {
-          const iso2 = (location.iso2 || '').toString().toUpperCase();
-          const availableValue = Number(location.available);
-          const available = Number.isNaN(availableValue) ? 0 : availableValue;
-
-          return {
-            ...location,
-            iso2,
-            available,
-          };
-        });
-
-        const providedTotal = Number(total);
-        const fallbackTotal = normalizedLocations.reduce(
-          (acc, item) =>
-            acc + (Number.isNaN(item.available) ? 0 : item.available),
-          0,
-        );
-        const hasProvidedTotal =
-          total !== undefined &&
-          total !== null &&
-          total !== '' &&
-          !Number.isNaN(providedTotal);
-
-        setLocations(normalizedLocations);
-        setLocationTotalAvailable(
-          hasProvidedTotal ? providedTotal : fallbackTotal,
-        );
-      } else {
-        showError(t('获取部署位置失败: ') + response.data.message);
-        setLocations([]);
-        setLocationTotalAvailable(null);
-      }
-    } catch (error) {
-      if (locationRequestIdRef.current === requestId) {
-        showError(t('获取部署位置失败: ') + error.message);
-        setLocations([]);
-        setLocationTotalAvailable(null);
-      }
-    } finally {
-      if (locationRequestIdRef.current === requestId) {
-        setLoadingLocations(false);
-      }
-    }
-  };
-
   const loadAvailableReplicas = async (hardwareId, gpuCount) => {
     if (!hardwareId || !gpuCount) {
-      setAvailableReplicas([]);
+      setLocations([]);
       setLocationTotalAvailable(null);
       setLoadingReplicas(false);
       return;
@@ -461,7 +419,8 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
     const requestId = Date.now();
     replicaRequestIdRef.current = requestId;
     setLoadingReplicas(true);
-    setAvailableReplicas([]);
+    setLocations([]);
+    setLocationTotalAvailable(null);
 
     try {
       const response = await API.get(
@@ -474,24 +433,67 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
 
       if (response.data.success) {
         const replicasList = response.data.data?.replicas || [];
-        const filteredReplicas = replicasList.filter(
-          (replica) => (replica.available_count || 0) > 0,
-        );
-        setAvailableReplicas(filteredReplicas);
-        const totalAvailableForHardware = filteredReplicas.reduce(
-          (total, replica) => total + (replica.available_count || 0),
-          0,
+
+        const nextLocationsMap = new Map();
+        replicasList.forEach((replica) => {
+          const rawId = replica?.location_id ?? replica?.location?.id;
+          if (rawId === null || rawId === undefined) {
+            return;
+          }
+          const id = rawId;
+          const mapKey = String(rawId);
+          const existing = nextLocationsMap.get(mapKey) || null;
+
+          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 ??
+            id;
+
+          const available = Number(replica?.available_count) || 0;
+          if (existing) {
+            existing.available += available;
+            return;
+          }
+
+          nextLocationsMap.set(mapKey, {
+            id,
+            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,
+          });
+        });
+
+        setLocations(Array.from(nextLocationsMap.values()));
+        setLocationTotalAvailable(
+          Array.from(nextLocationsMap.values()).reduce(
+            (total, location) => total + (location.available || 0),
+            0,
+          ),
         );
-        setLocationTotalAvailable(totalAvailableForHardware);
       } else {
         showError(t('获取可用资源失败: ') + response.data.message);
-        setAvailableReplicas([]);
         setLocationTotalAvailable(null);
       }
     } catch (error) {
       if (replicaRequestIdRef.current === requestId) {
         console.error('Load available replicas error:', error);
-        setAvailableReplicas([]);
         setLocationTotalAvailable(null);
       }
     } finally {
@@ -516,7 +518,10 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
         hardware_qty: gpusPerContainer,
       };
 
-      const response = await API.post('/api/deployments/price-estimation', requestData);
+      const response = await API.post(
+        '/api/deployments/price-estimation',
+        requestData,
+      );
       if (response.data.success) {
         setPriceEstimation(response.data.data);
       } else {
@@ -537,14 +542,14 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
 
       // Prepare environment variables
       const envVars = {};
-      envVariables.forEach(env => {
+      envVariables.forEach((env) => {
         if (env.key && env.value) {
           envVars[env.key] = env.value;
         }
       });
 
       const secretEnvVars = {};
-      secretEnvVariables.forEach(env => {
+      secretEnvVariables.forEach((env) => {
         if (env.key && env.value) {
           secretEnvVars[env.key] = env.value;
         }
@@ -559,17 +564,19 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
       }
 
       // Prepare entrypoint and args
-      const cleanEntrypoint = entrypoint.filter(item => item.trim() !== '');
-      const cleanArgs = args.filter(item => item.trim() !== '');
+      const cleanEntrypoint = entrypoint.filter((item) => item.trim() !== '');
+      const cleanArgs = args.filter((item) => item.trim() !== '');
 
-      const resolvedImage = imageMode === 'builtin' ? BUILTIN_IMAGE : values.image_url;
+      const resolvedImage =
+        imageMode === 'builtin' ? BUILTIN_IMAGE : values.image_url;
       const resolvedTrafficPort =
-        values.traffic_port || (imageMode === 'builtin' ? DEFAULT_TRAFFIC_PORT : undefined);
+        values.traffic_port ||
+        (imageMode === 'builtin' ? DEFAULT_TRAFFIC_PORT : undefined);
 
       const requestData = {
         resource_private_name: values.resource_private_name,
         duration_hours: values.duration_hours,
-        gpus_per_container: values.gpus_per_container,
+        gpus_per_container: gpusPerContainer,
         hardware_id: values.hardware_id,
         location_ids: values.location_ids,
         container_config: {
@@ -588,7 +595,7 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
       };
 
       const response = await API.post('/api/deployments', requestData);
-      
+
       if (response.data.success) {
         showSuccess(t('容器创建成功'));
         onSuccess?.(response.data.data);
@@ -614,10 +621,16 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
   const handleRemoveEnvVariable = (index, type) => {
     if (type === 'env') {
       const newEnvVars = envVariables.filter((_, i) => i !== index);
-      setEnvVariables(newEnvVars.length > 0 ? newEnvVars : [{ key: '', value: '' }]);
+      setEnvVariables(
+        newEnvVars.length > 0 ? newEnvVars : [{ key: '', value: '' }],
+      );
     } else {
       const newSecretEnvVars = secretEnvVariables.filter((_, i) => i !== index);
-      setSecretEnvVariables(newSecretEnvVars.length > 0 ? newSecretEnvVars : [{ key: '', value: '' }]);
+      setSecretEnvVariables(
+        newSecretEnvVars.length > 0
+          ? newSecretEnvVars
+          : [{ key: '', value: '' }],
+      );
     }
   };
 
@@ -678,10 +691,9 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
       return;
     }
 
-    const validLocationIds =
-      availableReplicas.length > 0
-        ? availableReplicas.map((item) => item.location_id)
-        : locations.map((location) => location.id);
+    const validLocationIds = locations
+      .filter((location) => (Number(location.available) || 0) > 0)
+      .map((location) => location.id);
 
     if (validLocationIds.length === 0) {
       if (selectedLocationIds.length > 0) {
@@ -707,31 +719,18 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
         formApi.setValue('location_ids', filteredSelection);
       }
     }
-  }, [
-    availableReplicas,
-    locations,
-    selectedHardwareId,
-    selectedLocationIds,
-    visible,
-    formApi,
-  ]);
+  }, [locations, selectedHardwareId, selectedLocationIds, visible, formApi]);
 
   const maxAvailableReplicas = useMemo(() => {
     if (!selectedLocationIds.length) return 0;
 
-    if (availableReplicas.length > 0) {
-      return availableReplicas
-        .filter((replica) => selectedLocationIds.includes(replica.location_id))
-        .reduce((total, replica) => total + (replica.available_count || 0), 0);
-    }
-
     return locations
       .filter((location) => selectedLocationIds.includes(location.id))
       .reduce((total, location) => {
         const availableValue = Number(location.available);
         return total + (Number.isNaN(availableValue) ? 0 : availableValue);
       }, 0);
-  }, [availableReplicas, selectedLocationIds, locations]);
+  }, [selectedLocationIds, locations]);
 
   const isPriceReady = useMemo(
     () =>
@@ -749,7 +748,11 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
     ],
   );
 
-  const currencyLabel = (priceEstimation?.currency || priceCurrency || '').toUpperCase();
+  const currencyLabel = (
+    priceEstimation?.currency ||
+    priceCurrency ||
+    ''
+  ).toUpperCase();
   const selectedHardwareLabel = selectedHardwareId
     ? hardwareLabelMap[selectedHardwareId]
     : '';
@@ -769,7 +772,9 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
     {
       key: 'locations',
       label: t('部署位置'),
-      value: selectedLocationNames.length ? selectedLocationNames.join('、') : '--',
+      value: selectedLocationNames.length
+        ? selectedLocationNames.join('、')
+        : '--',
     },
     {
       key: 'replicas',
@@ -778,7 +783,7 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
     },
     {
       key: 'gpus',
-      label: t('每容器GPU数量'),
+      label: t('最大GPU数量'),
       value: (gpusPerContainer ?? 0).toString(),
     },
     {
@@ -802,14 +807,14 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
   const priceUnavailableContent = (
     <div style={{ marginTop: 12 }}>
       {loadingPrice ? (
-        <Space spacing={8} align="center">
-          <Spin size="small" />
-          <Text size="small" type="tertiary">
+        <Space spacing={8} align='center'>
+          <Spin size='small' />
+          <Text size='small' type='tertiary'>
             {t('价格计算中...')}
           </Text>
         </Space>
       ) : (
-        <Text size="small" type="tertiary">
+        <Text size='small' type='tertiary'>
           {isPriceReady
             ? t('价格暂时不可用,请稍后重试')
             : t('完成硬件类型、部署位置、副本数量等配置后,将自动计算价格')}
@@ -846,7 +851,7 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
         getFormApi={setFormApi}
         onSubmit={handleSubmit}
         style={{ maxHeight: '70vh', overflowY: 'auto' }}
-        labelPosition="top"
+        labelPosition='top'
       >
         <Space
           wrap
@@ -854,25 +859,25 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
           style={{ justifyContent: 'flex-end', width: '100%', marginBottom: 8 }}
         >
           <Button
-            size="small"
-            theme="borderless"
-            type="tertiary"
+            size='small'
+            theme='borderless'
+            type='tertiary'
             onClick={() => scrollToSection(basicSectionRef)}
           >
             {t('部署配置')}
           </Button>
           <Button
-            size="small"
-            theme="borderless"
-            type="tertiary"
+            size='small'
+            theme='borderless'
+            type='tertiary'
             onClick={() => scrollToSection(priceSectionRef)}
           >
             {t('价格预估')}
           </Button>
           <Button
-            size="small"
-            theme="borderless"
-            type="tertiary"
+            size='small'
+            theme='borderless'
+            type='tertiary'
             onClick={() => scrollToSection(advancedSectionRef)}
           >
             {t('高级配置')}
@@ -880,32 +885,34 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
         </Space>
 
         <div ref={basicSectionRef}>
-          <Card className="mb-4">
+          <Card className='mb-4'>
             <Title heading={6}>{t('部署配置')}</Title>
-            
+
             <Form.Input
-              field="resource_private_name"
+              field='resource_private_name'
               label={t('容器名称')}
               placeholder={t('请输入容器名称')}
               rules={[{ required: true, message: t('请输入容器名称') }]}
             />
 
-            <div className="mt-2">
+            <div className='mt-2'>
               <Text strong>{t('镜像选择')}</Text>
               <div style={{ marginTop: 8 }}>
                 <RadioGroup
-                  type="button"
+                  type='button'
                   value={imageMode}
-                  onChange={(value) => setImageMode(value?.target?.value ?? value)}
+                  onChange={(value) =>
+                    setImageMode(value?.target?.value ?? value)
+                  }
                 >
-                  <Radio value="builtin">{t('内置 Ollama 镜像')}</Radio>
-                  <Radio value="custom">{t('自定义镜像')}</Radio>
+                  <Radio value='builtin'>{t('内置 Ollama 镜像')}</Radio>
+                  <Radio value='custom'>{t('自定义镜像')}</Radio>
                 </RadioGroup>
               </div>
             </div>
 
             <Form.Input
-              field="image_url"
+              field='image_url'
               label={t('镜像地址')}
               placeholder={t('例如:nginx:latest')}
               rules={[{ required: true, message: t('请输入镜像地址') }]}
@@ -918,20 +925,20 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
             />
 
             {imageMode === 'builtin' && (
-              <Space align="center" spacing={8} className="mt-2">
-                <Text size="small" type="tertiary">
+              <Space align='center' spacing={8} className='mt-2'>
+                <Text size='small' type='tertiary'>
                   {t('系统已为该部署准备 Ollama 镜像与随机 API Key')}
                 </Text>
                 <Input
                   readOnly
                   value={autoOllamaKey}
-                  size="small"
+                  size='small'
                   style={{ width: 220 }}
                 />
                 <Button
                   icon={<IconCopy />}
-                  size="small"
-                  theme="borderless"
+                  size='small'
+                  theme='borderless'
                   onClick={async () => {
                     if (!autoOllamaKey) {
                       return;
@@ -952,16 +959,19 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
             <Row gutter={16}>
               <Col xs={24} md={12}>
                 <Form.Select
-                  field="hardware_id"
+                  field='hardware_id'
                   label={t('硬件类型')}
                   placeholder={t('选择硬件类型')}
                   loading={loadingHardware}
                   rules={[{ required: true, message: t('请选择硬件类型') }]}
                   onChange={(value) => {
+                    const nextMaxGpus = getHardwareMaxGpus(value);
                     setSelectedHardwareId(value);
+                    setGpusPerContainer(nextMaxGpus);
                     setSelectedLocationIds([]);
                     if (formApi) {
                       formApi.setValue('location_ids', []);
+                      formApi.setValue('gpus_per_container', nextMaxGpus);
                     }
                   }}
                   style={{ width: '100%' }}
@@ -987,13 +997,16 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
 
                     return (
                       <Option key={hardware.id} value={hardware.id}>
-                        <div className="flex flex-col gap-1">
+                        <div className='flex flex-col gap-1'>
                           <Text strong>{displayName}</Text>
-                          <div className="flex items-center gap-2 text-xs text-[var(--semi-color-text-2)]">
+                          <div className='flex items-center gap-2 text-xs text-[var(--semi-color-text-2)]'>
                             <span>
-                              {t('最大GPU数')}: {hardware.max_gpus}
+                              {t('最大GPU数')}: {hardware.max_gpus}
                             </span>
-                            <Tag color={hasAvailability ? 'green' : 'red'} size="small">
+                            <Tag
+                              color={hasAvailability ? 'green' : 'red'}
+                              size='small'
+                            >
                               {t('可用数量')}: {availableCount}
                             </Tag>
                           </div>
@@ -1005,83 +1018,68 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
               </Col>
               <Col xs={24} md={12}>
                 <Form.InputNumber
-                  field="gpus_per_container"
-                  label={t('每容器GPU数量')}
+                  field='gpus_per_container'
+                  label={t('最大GPU数量')}
                   placeholder={1}
                   min={1}
-                  max={selectedHardwareId ? hardwareTypes.find((h) => h.id === selectedHardwareId)?.max_gpus : 8}
+                  max={getHardwareMaxGpus(selectedHardwareId)}
                   step={1}
-                  innerButtons
-                  rules={[{ required: true, message: t('请输入GPU数量') }]}
-                  onChange={(value) => setGpusPerContainer(value)}
+                  disabled
                   style={{ width: '100%' }}
                 />
               </Col>
             </Row>
 
             {typeof hardwareTotalAvailable === 'number' && (
-              <Text size="small" type="tertiary">
+              <Text size='small' type='tertiary'>
                 {t('全部硬件总可用资源')}: {hardwareTotalAvailable}
               </Text>
             )}
 
             <Form.Select
-              field="location_ids"
+              field='location_ids'
               label={
                 <Space>
                   {t('部署位置')}
-                  {loadingReplicas && <Spin size="small" />}
+                  {loadingReplicas && <Spin size='small' />}
                 </Space>
               }
               placeholder={
                 !selectedHardwareId
                   ? t('请先选择硬件类型')
-                  : loadingLocations || loadingReplicas
+                  : loadingReplicas
                     ? t('正在加载可用部署位置...')
                     : t('选择部署位置(可多选)')
               }
               multiple
-              loading={loadingLocations || loadingReplicas}
-              disabled={!selectedHardwareId || loadingLocations || loadingReplicas}
+              loading={loadingReplicas}
+              disabled={!selectedHardwareId || loadingReplicas}
               rules={[{ required: true, message: t('请选择至少一个部署位置') }]}
               onChange={(value) => setSelectedLocationIds(value)}
               style={{ width: '100%' }}
               dropdownStyle={{ maxHeight: 360, overflowY: 'auto' }}
               renderSelectedItem={(optionNode) => ({
                 isRenderInTag: true,
-                content:
-                  !optionNode
-                    ? ''
-                    : loadingLocations || loadingReplicas
-                      ? t('部署位置加载中...')
-                      : locationLabelMap[optionNode?.value] ||
-                        optionNode?.label ||
-                        optionNode?.value ||
-                        '',
+                content: !optionNode
+                  ? ''
+                  : loadingReplicas
+                    ? t('部署位置加载中...')
+                    : locationLabelMap[optionNode?.value] ||
+                      optionNode?.label ||
+                      optionNode?.value ||
+                      '',
               })}
             >
               {locations.map((location) => {
-                const replicaEntry = availableReplicas.find(
-                  (r) => r.location_id === location.id,
-                );
-                const hasReplicaData = availableReplicas.length > 0;
-                const availableCount = hasReplicaData
-                  ? replicaEntry?.available_count ?? 0
-                  : (() => {
-                      const numeric = Number(location.available);
-                      return Number.isNaN(numeric) ? 0 : numeric;
-                    })();
+                const numeric = Number(location.available);
+                const availableCount = Number.isNaN(numeric) ? 0 : numeric;
                 const locationLabel =
                   location.region ||
                   location.country ||
                   (location.iso2 ? location.iso2.toUpperCase() : '') ||
                   location.code ||
                   '';
-                const disableOption = hasReplicaData
-                  ? availableCount === 0
-                  : typeof location.available === 'number'
-                    ? location.available === 0
-                    : false;
+                const disableOption = availableCount === 0;
 
                 return (
                   <Option
@@ -1089,17 +1087,17 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
                     value={location.id}
                     disabled={disableOption}
                   >
-                    <div className="flex flex-col gap-1">
-                      <div className="flex items-center gap-2">
+                    <div className='flex flex-col gap-1'>
+                      <div className='flex items-center gap-2'>
                         <Text strong>{location.name}</Text>
                         {locationLabel && (
-                          <Tag color="blue" size="small">
+                          <Tag color='blue' size='small'>
                             {locationLabel}
                           </Tag>
                         )}
                       </div>
                       <Text
-                        size="small"
+                        size='small'
                         type={availableCount > 0 ? 'success' : 'danger'}
                       >
                         {t('可用数量')}: {availableCount}
@@ -1111,16 +1109,16 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
             </Form.Select>
 
             {typeof locationTotalAvailable === 'number' && (
-              <Text size="small" type="tertiary">
+              <Text size='small' type='tertiary'>
                 {t('全部地区总可用资源')}: {locationTotalAvailable}
               </Text>
             )}
 
-          <Row gutter={16}>
-            <Col xs={24} md={8}>
-              <Form.InputNumber
-                field="replica_count"
-                label={t('副本数量')}
+            <Row gutter={16}>
+              <Col xs={24} md={8}>
+                <Form.InputNumber
+                  field='replica_count'
+                  label={t('副本数量')}
                   placeholder={1}
                   min={1}
                   max={maxAvailableReplicas || 100}
@@ -1129,14 +1127,14 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
                   style={{ width: '100%' }}
                 />
                 {maxAvailableReplicas > 0 && (
-                  <Text size="small" type="tertiary">
+                  <Text size='small' type='tertiary'>
                     {t('最大可用')}: {maxAvailableReplicas}
                   </Text>
                 )}
               </Col>
               <Col xs={24} md={8}>
                 <Form.InputNumber
-                  field="duration_hours"
+                  field='duration_hours'
                   label={t('运行时长(小时)')}
                   placeholder={1}
                   min={1}
@@ -1148,7 +1146,7 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
               </Col>
               <Col xs={24} md={8}>
                 <Form.InputNumber
-                  field="traffic_port"
+                  field='traffic_port'
                   label={
                     <Space>
                       {t('流量端口')}
@@ -1162,298 +1160,349 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
                   max={65535}
                   style={{ width: '100%' }}
                   disabled={imageMode === 'builtin'}
-              />
-            </Col>
-          </Row>
-
-          <div ref={advancedSectionRef}>
-            <Collapse className="mt-4">
-              <Collapse.Panel header={t('高级配置')} itemKey="advanced">
-                <Card>
-                  <Title heading={6}>{t('镜像仓库配置')}</Title>
-                  <Row gutter={16}>
-                    <Col span={12}>
-                      <Form.Input
-                        field="registry_username"
-                        label={t('镜像仓库用户名')}
-                        placeholder={t('私有镜像仓库的用户名')}
-                      />
-                    </Col>
-                    <Col span={12}>
-                      <Form.Input
-                        field="registry_secret"
-                        label={t('镜像仓库密码')}
-                        type="password"
-                        placeholder={t('私有镜像仓库的密码')}
-                      />
-                    </Col>
-                  </Row>
-                </Card>
-
-                <Divider />
-
-                <Card>
-                  <Title heading={6}>{t('容器启动配置')}</Title>
-
-                  <div style={{ marginBottom: 16 }}>
-                    <Text strong>{t('启动命令 (Entrypoint)')}</Text>
-                    {entrypoint.map((cmd, index) => (
-                      <div key={index} style={{ display: 'flex', marginTop: 8 }}>
-                        <Input
-                          value={cmd}
-                          placeholder={t('例如:/bin/bash')}
-                          onChange={(value) => handleArrayFieldChange(index, value, 'entrypoint')}
-                          style={{ flex: 1, marginRight: 8 }}
-                        />
-                        <Button
-                          icon={<IconMinus />}
-                          onClick={() => handleRemoveArrayField(index, 'entrypoint')}
-                          disabled={entrypoint.length === 1}
-                        />
-                      </div>
-                    ))}
-                    <Button
-                      icon={<IconPlus />}
-                      onClick={() => handleAddArrayField('entrypoint')}
-                      style={{ marginTop: 8 }}
-                    >
-                      {t('添加启动命令')}
-                    </Button>
-                  </div>
+                />
+              </Col>
+            </Row>
 
-                  <div style={{ marginBottom: 16 }}>
-                    <Text strong>{t('启动参数 (Args)')}</Text>
-                    {args.map((arg, index) => (
-                      <div key={index} style={{ display: 'flex', marginTop: 8 }}>
-                        <Input
-                          value={arg}
-                          placeholder={t('例如:-c')}
-                          onChange={(value) => handleArrayFieldChange(index, value, 'args')}
-                          style={{ flex: 1, marginRight: 8 }}
+            <div ref={advancedSectionRef}>
+              <Collapse className='mt-4'>
+                <Collapse.Panel header={t('高级配置')} itemKey='advanced'>
+                  <Card>
+                    <Title heading={6}>{t('镜像仓库配置')}</Title>
+                    <Row gutter={16}>
+                      <Col span={12}>
+                        <Form.Input
+                          field='registry_username'
+                          label={t('镜像仓库用户名')}
+                          placeholder={t('私有镜像仓库的用户名')}
                         />
-                        <Button
-                          icon={<IconMinus />}
-                          onClick={() => handleRemoveArrayField(index, 'args')}
-                          disabled={args.length === 1}
+                      </Col>
+                      <Col span={12}>
+                        <Form.Input
+                          field='registry_secret'
+                          label={t('镜像仓库密码')}
+                          type='password'
+                          placeholder={t('私有镜像仓库的密码')}
                         />
-                      </div>
-                    ))}
-                    <Button
-                      icon={<IconPlus />}
-                      onClick={() => handleAddArrayField('args')}
-                      style={{ marginTop: 8 }}
-                    >
-                      {t('添加启动参数')}
-                    </Button>
-                  </div>
-                </Card>
-
-                <Divider />
-
-                <Card>
-                  <Title heading={6}>{t('环境变量')}</Title>
-
-                  <div style={{ marginBottom: 16 }}>
-                    <Text strong>{t('普通环境变量')}</Text>
-                    {envVariables.map((env, index) => (
-                      <Row key={index} gutter={8} style={{ marginTop: 8 }}>
-                        <Col span={10}>
+                      </Col>
+                    </Row>
+                  </Card>
+
+                  <Divider />
+
+                  <Card>
+                    <Title heading={6}>{t('容器启动配置')}</Title>
+
+                    <div style={{ marginBottom: 16 }}>
+                      <Text strong>{t('启动命令 (Entrypoint)')}</Text>
+                      {entrypoint.map((cmd, index) => (
+                        <div
+                          key={index}
+                          style={{ display: 'flex', marginTop: 8 }}
+                        >
                           <Input
-                            placeholder={t('变量名')}
-                            value={env.key}
-                            onChange={(value) => handleEnvVariableChange(index, 'key', value, 'env')}
+                            value={cmd}
+                            placeholder={t('例如:/bin/bash')}
+                            onChange={(value) =>
+                              handleArrayFieldChange(index, value, 'entrypoint')
+                            }
+                            style={{ flex: 1, marginRight: 8 }}
                           />
-                        </Col>
-                        <Col span={10}>
+                          <Button
+                            icon={<IconMinus />}
+                            onClick={() =>
+                              handleRemoveArrayField(index, 'entrypoint')
+                            }
+                            disabled={entrypoint.length === 1}
+                          />
+                        </div>
+                      ))}
+                      <Button
+                        icon={<IconPlus />}
+                        onClick={() => handleAddArrayField('entrypoint')}
+                        style={{ marginTop: 8 }}
+                      >
+                        {t('添加启动命令')}
+                      </Button>
+                    </div>
+
+                    <div style={{ marginBottom: 16 }}>
+                      <Text strong>{t('启动参数 (Args)')}</Text>
+                      {args.map((arg, index) => (
+                        <div
+                          key={index}
+                          style={{ display: 'flex', marginTop: 8 }}
+                        >
                           <Input
-                            placeholder={t('变量值')}
-                            value={env.value}
-                            onChange={(value) => handleEnvVariableChange(index, 'value', value, 'env')}
+                            value={arg}
+                            placeholder={t('例如:-c')}
+                            onChange={(value) =>
+                              handleArrayFieldChange(index, value, 'args')
+                            }
+                            style={{ flex: 1, marginRight: 8 }}
                           />
-                        </Col>
-                        <Col span={4}>
                           <Button
                             icon={<IconMinus />}
-                            onClick={() => handleRemoveEnvVariable(index, 'env')}
-                            disabled={envVariables.length === 1}
+                            onClick={() =>
+                              handleRemoveArrayField(index, 'args')
+                            }
+                            disabled={args.length === 1}
                           />
-                        </Col>
-                      </Row>
-                    ))}
-                    <Button
-                      icon={<IconPlus />}
-                      onClick={() => handleAddEnvVariable('env')}
-                      style={{ marginTop: 8 }}
-                    >
-                      {t('添加环境变量')}
-                    </Button>
-                  </div>
+                        </div>
+                      ))}
+                      <Button
+                        icon={<IconPlus />}
+                        onClick={() => handleAddArrayField('args')}
+                        style={{ marginTop: 8 }}
+                      >
+                        {t('添加启动参数')}
+                      </Button>
+                    </div>
+                  </Card>
+
+                  <Divider />
+
+                  <Card>
+                    <Title heading={6}>{t('环境变量')}</Title>
 
-                  <div>
-                    <Text strong>{t('密钥环境变量')}</Text>
-                    {secretEnvVariables.map((env, index) => {
-                      const isAutoSecret =
-                        imageMode === 'builtin' && env.key === 'OLLAMA_API_KEY';
-                      return (
+                    <div style={{ marginBottom: 16 }}>
+                      <Text strong>{t('普通环境变量')}</Text>
+                      {envVariables.map((env, index) => (
                         <Row key={index} gutter={8} style={{ marginTop: 8 }}>
                           <Col span={10}>
                             <Input
                               placeholder={t('变量名')}
                               value={env.key}
-                              onChange={(value) => handleEnvVariableChange(index, 'key', value, 'secret')}
-                              disabled={isAutoSecret}
+                              onChange={(value) =>
+                                handleEnvVariableChange(
+                                  index,
+                                  'key',
+                                  value,
+                                  'env',
+                                )
+                              }
                             />
                           </Col>
                           <Col span={10}>
                             <Input
                               placeholder={t('变量值')}
-                              type="password"
                               value={env.value}
-                              onChange={(value) => handleEnvVariableChange(index, 'value', value, 'secret')}
-                              disabled={isAutoSecret}
+                              onChange={(value) =>
+                                handleEnvVariableChange(
+                                  index,
+                                  'value',
+                                  value,
+                                  'env',
+                                )
+                              }
                             />
                           </Col>
                           <Col span={4}>
                             <Button
                               icon={<IconMinus />}
-                              onClick={() => handleRemoveEnvVariable(index, 'secret')}
-                              disabled={secretEnvVariables.length === 1 || isAutoSecret}
+                              onClick={() =>
+                                handleRemoveEnvVariable(index, 'env')
+                              }
+                              disabled={envVariables.length === 1}
                             />
                           </Col>
                         </Row>
-                      );
-                    })}
-                    <Button
-                      icon={<IconPlus />}
-                      onClick={() => handleAddEnvVariable('secret')}
-                      style={{ marginTop: 8 }}
-                    >
-                      {t('添加密钥环境变量')}
-                    </Button>
-                  </div>
-                </Card>
-              </Collapse.Panel>
-            </Collapse>
-          </div>
-        </Card>
+                      ))}
+                      <Button
+                        icon={<IconPlus />}
+                        onClick={() => handleAddEnvVariable('env')}
+                        style={{ marginTop: 8 }}
+                      >
+                        {t('添加环境变量')}
+                      </Button>
+                    </div>
+
+                    <div>
+                      <Text strong>{t('密钥环境变量')}</Text>
+                      {secretEnvVariables.map((env, index) => {
+                        const isAutoSecret =
+                          imageMode === 'builtin' &&
+                          env.key === 'OLLAMA_API_KEY';
+                        return (
+                          <Row key={index} gutter={8} style={{ marginTop: 8 }}>
+                            <Col span={10}>
+                              <Input
+                                placeholder={t('变量名')}
+                                value={env.key}
+                                onChange={(value) =>
+                                  handleEnvVariableChange(
+                                    index,
+                                    'key',
+                                    value,
+                                    'secret',
+                                  )
+                                }
+                                disabled={isAutoSecret}
+                              />
+                            </Col>
+                            <Col span={10}>
+                              <Input
+                                placeholder={t('变量值')}
+                                type='password'
+                                value={env.value}
+                                onChange={(value) =>
+                                  handleEnvVariableChange(
+                                    index,
+                                    'value',
+                                    value,
+                                    'secret',
+                                  )
+                                }
+                                disabled={isAutoSecret}
+                              />
+                            </Col>
+                            <Col span={4}>
+                              <Button
+                                icon={<IconMinus />}
+                                onClick={() =>
+                                  handleRemoveEnvVariable(index, 'secret')
+                                }
+                                disabled={
+                                  secretEnvVariables.length === 1 ||
+                                  isAutoSecret
+                                }
+                              />
+                            </Col>
+                          </Row>
+                        );
+                      })}
+                      <Button
+                        icon={<IconPlus />}
+                        onClick={() => handleAddEnvVariable('secret')}
+                        style={{ marginTop: 8 }}
+                      >
+                        {t('添加密钥环境变量')}
+                      </Button>
+                    </div>
+                  </Card>
+                </Collapse.Panel>
+              </Collapse>
+            </div>
+          </Card>
         </div>
 
         <div ref={priceSectionRef}>
-          <Card className="mb-4">
-              <div className="flex flex-wrap items-center justify-between gap-3">
-                <Title heading={6} style={{ margin: 0 }}>
-                  {t('价格预估')}
-                </Title>
-                <Space align="center" spacing={12} className="flex flex-wrap">
-                  <Text type="secondary" size="small">
-                    {t('计价币种')}
-                  </Text>
-                  <RadioGroup
-                    type="button"
-                    value={priceCurrency}
-                    onChange={handleCurrencyChange}
-                  >
-                    <Radio value="usdc">USDC</Radio>
-                    <Radio value="iocoin">IOCOIN</Radio>
-                  </RadioGroup>
-                  <Tag size="small" color="blue">
-                    {currencyLabel}
-                  </Tag>
-                </Space>
-              </div>
+          <Card className='mb-4'>
+            <div className='flex flex-wrap items-center justify-between gap-3'>
+              <Title heading={6} style={{ margin: 0 }}>
+                {t('价格预估')}
+              </Title>
+              <Space align='center' spacing={12} className='flex flex-wrap'>
+                <Text type='secondary' size='small'>
+                  {t('计价币种')}
+                </Text>
+                <RadioGroup
+                  type='button'
+                  value={priceCurrency}
+                  onChange={handleCurrencyChange}
+                >
+                  <Radio value='usdc'>USDC</Radio>
+                  <Radio value='iocoin'>IOCOIN</Radio>
+                </RadioGroup>
+                <Tag size='small' color='blue'>
+                  {currencyLabel}
+                </Tag>
+              </Space>
+            </div>
 
-              {priceEstimation ? (
-                <div className="mt-4 flex w-full flex-col gap-4">
-                  <div className="grid w-full gap-4 md:grid-cols-2 lg:grid-cols-3">
-                    <div
-                      className="flex flex-col gap-1 rounded-md px-4 py-3"
-                      style={{
-                        border: '1px solid var(--semi-color-border)',
-                        backgroundColor: 'var(--semi-color-fill-0)',
-                      }}
-                    >
-                      <Text size="small" type="tertiary">
-                        {t('预估总费用')}
-                      </Text>
-                      <div
-                        style={{
-                          fontSize: 24,
-                          fontWeight: 600,
-                          color: 'var(--semi-color-text-0)',
-                        }}
-                      >
-                        {typeof priceEstimation.estimated_cost === 'number'
-                          ? `${priceEstimation.estimated_cost.toFixed(4)} ${currencyLabel}`
-                          : '--'}
-                      </div>
-                    </div>
+            {priceEstimation ? (
+              <div className='mt-4 flex w-full flex-col gap-4'>
+                <div className='grid w-full gap-4 md:grid-cols-2 lg:grid-cols-3'>
+                  <div
+                    className='flex flex-col gap-1 rounded-md px-4 py-3'
+                    style={{
+                      border: '1px solid var(--semi-color-border)',
+                      backgroundColor: 'var(--semi-color-fill-0)',
+                    }}
+                  >
+                    <Text size='small' type='tertiary'>
+                      {t('预估总费用')}
+                    </Text>
                     <div
-                      className="flex flex-col gap-1 rounded-md px-4 py-3"
                       style={{
-                        border: '1px solid var(--semi-color-border)',
-                        backgroundColor: 'var(--semi-color-fill-0)',
+                        fontSize: 24,
+                        fontWeight: 600,
+                        color: 'var(--semi-color-text-0)',
                       }}
                     >
-                      <Text size="small" type="tertiary">
-                        {t('小时费率')}
-                      </Text>
-                      <Text strong>
-                        {typeof priceEstimation.price_breakdown?.hourly_rate === 'number'
-                          ? `${priceEstimation.price_breakdown.hourly_rate.toFixed(4)} ${currencyLabel}/h`
-                          : '--'}
-                      </Text>
+                      {typeof priceEstimation.estimated_cost === 'number'
+                        ? `${priceEstimation.estimated_cost.toFixed(4)} ${currencyLabel}`
+                        : '--'}
                     </div>
+                  </div>
+                  <div
+                    className='flex flex-col gap-1 rounded-md px-4 py-3'
+                    style={{
+                      border: '1px solid var(--semi-color-border)',
+                      backgroundColor: 'var(--semi-color-fill-0)',
+                    }}
+                  >
+                    <Text size='small' type='tertiary'>
+                      {t('小时费率')}
+                    </Text>
+                    <Text strong>
+                      {typeof priceEstimation.price_breakdown?.hourly_rate ===
+                      'number'
+                        ? `${priceEstimation.price_breakdown.hourly_rate.toFixed(4)} ${currencyLabel}/h`
+                        : '--'}
+                    </Text>
+                  </div>
+                  <div
+                    className='flex flex-col gap-1 rounded-md px-4 py-3'
+                    style={{
+                      border: '1px solid var(--semi-color-border)',
+                      backgroundColor: 'var(--semi-color-fill-0)',
+                    }}
+                  >
+                    <Text size='small' type='tertiary'>
+                      {t('计算成本')}
+                    </Text>
+                    <Text strong>
+                      {typeof priceEstimation.price_breakdown?.compute_cost ===
+                      'number'
+                        ? `${priceEstimation.price_breakdown.compute_cost.toFixed(4)} ${currencyLabel}`
+                        : '--'}
+                    </Text>
+                  </div>
+                </div>
+
+                <div className='grid gap-3 sm:grid-cols-2 lg:grid-cols-3'>
+                  {priceSummaryItems.map((item) => (
                     <div
-                      className="flex flex-col gap-1 rounded-md px-4 py-3"
+                      key={item.key}
+                      className='flex items-center justify-between gap-3 rounded-md px-3 py-2'
                       style={{
                         border: '1px solid var(--semi-color-border)',
                         backgroundColor: 'var(--semi-color-fill-0)',
                       }}
                     >
-                      <Text size="small" type="tertiary">
-                        {t('计算成本')}
-                      </Text>
-                      <Text strong>
-                        {typeof priceEstimation.price_breakdown?.compute_cost === 'number'
-                          ? `${priceEstimation.price_breakdown.compute_cost.toFixed(4)} ${currencyLabel}`
-                          : '--'}
+                      <Text size='small' type='tertiary'>
+                        {item.label}
                       </Text>
+                      <Text strong>{item.value}</Text>
                     </div>
-                  </div>
-
-                  <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
-                    {priceSummaryItems.map((item) => (
-                      <div
-                        key={item.key}
-                        className="flex items-center justify-between gap-3 rounded-md px-3 py-2"
-                        style={{
-                          border: '1px solid var(--semi-color-border)',
-                          backgroundColor: 'var(--semi-color-fill-0)',
-                        }}
-                      >
-                        <Text size="small" type="tertiary">
-                          {item.label}
-                        </Text>
-                        <Text strong>{item.value}</Text>
-                      </div>
-                    ))}
-                  </div>
+                  ))}
                 </div>
-              ) : (
-                priceUnavailableContent
-              )}
-
-              {priceEstimation && loadingPrice && (
-                <Space align="center" spacing={8} style={{ marginTop: 12 }}>
-                  <Spin size="small" />
-                  <Text size="small" type="tertiary">
-                    {t('价格重新计算中...')}
-                  </Text>
-                </Space>
-              )}
+              </div>
+            ) : (
+              priceUnavailableContent
+            )}
+
+            {priceEstimation && loadingPrice && (
+              <Space align='center' spacing={8} style={{ marginTop: 12 }}>
+                <Spin size='small' />
+                <Text size='small' type='tertiary'>
+                  {t('价格重新计算中...')}
+                </Text>
+              </Space>
+            )}
           </Card>
         </div>
-
       </Form>
     </Modal>
   );

+ 143 - 121
web/src/components/table/model-deployments/modals/UpdateConfigModal.jsx

@@ -34,27 +34,21 @@ import {
   TextArea,
   Switch,
 } from '@douyinfe/semi-ui';
-import { 
-  FaCog, 
+import {
+  FaCog,
   FaDocker,
   FaKey,
   FaTerminal,
   FaNetworkWired,
   FaExclamationTriangle,
   FaPlus,
-  FaMinus
+  FaMinus,
 } from 'react-icons/fa';
 import { API, showError, showSuccess } from '../../../../helpers';
 
 const { Text, Title } = Typography;
 
-const UpdateConfigModal = ({ 
-  visible, 
-  onCancel, 
-  deployment, 
-  onSuccess,
-  t 
-}) => {
+const UpdateConfigModal = ({ visible, onCancel, deployment, onSuccess, t }) => {
   const formRef = useRef(null);
   const [loading, setLoading] = useState(false);
   const [envVars, setEnvVars] = useState([]);
@@ -72,18 +66,21 @@ const UpdateConfigModal = ({
         registry_secret: '',
         command: '',
       };
-      
+
       if (formRef.current) {
         formRef.current.setValues(initialValues);
       }
-      
+
       // Initialize environment variables
-      const envVarsList = deployment.container_config?.env_variables 
-        ? Object.entries(deployment.container_config.env_variables).map(([key, value]) => ({
-            key, value: String(value)
-          }))
+      const envVarsList = deployment.container_config?.env_variables
+        ? Object.entries(deployment.container_config.env_variables).map(
+            ([key, value]) => ({
+              key,
+              value: String(value),
+            }),
+          )
         : [];
-      
+
       setEnvVars(envVarsList);
       setSecretEnvVars([]);
     }
@@ -91,23 +88,30 @@ const UpdateConfigModal = ({
 
   const handleUpdate = async () => {
     try {
-      const formValues = formRef.current ? await formRef.current.validate() : {};
+      const formValues = formRef.current
+        ? await formRef.current.validate()
+        : {};
       setLoading(true);
 
       // Prepare the update payload
       const payload = {};
-      
+
       if (formValues.image_url) payload.image_url = formValues.image_url;
-      if (formValues.traffic_port) payload.traffic_port = formValues.traffic_port;
-      if (formValues.registry_username) payload.registry_username = formValues.registry_username;
-      if (formValues.registry_secret) payload.registry_secret = formValues.registry_secret;
+      if (formValues.traffic_port)
+        payload.traffic_port = formValues.traffic_port;
+      if (formValues.registry_username)
+        payload.registry_username = formValues.registry_username;
+      if (formValues.registry_secret)
+        payload.registry_secret = formValues.registry_secret;
       if (formValues.command) payload.command = formValues.command;
-      
+
       // Process entrypoint
       if (formValues.entrypoint) {
-        payload.entrypoint = formValues.entrypoint.split(' ').filter(cmd => cmd.trim());
+        payload.entrypoint = formValues.entrypoint
+          .split(' ')
+          .filter((cmd) => cmd.trim());
       }
-      
+
       // Process environment variables
       if (envVars.length > 0) {
         payload.env_variables = envVars.reduce((acc, env) => {
@@ -117,7 +121,7 @@ const UpdateConfigModal = ({
           return acc;
         }, {});
       }
-      
+
       // Process secret environment variables
       if (secretEnvVars.length > 0) {
         payload.secret_env_variables = secretEnvVars.reduce((acc, env) => {
@@ -128,7 +132,10 @@ const UpdateConfigModal = ({
         }, {});
       }
 
-      const response = await API.put(`/api/deployments/${deployment.id}`, payload);
+      const response = await API.put(
+        `/api/deployments/${deployment.id}`,
+        payload,
+      );
 
       if (response.data.success) {
         showSuccess(t('容器配置更新成功'));
@@ -136,7 +143,11 @@ const UpdateConfigModal = ({
         handleCancel();
       }
     } catch (error) {
-      showError(t('更新配置失败') + ': ' + (error.response?.data?.message || error.message));
+      showError(
+        t('更新配置失败') +
+          ': ' +
+          (error.response?.data?.message || error.message),
+      );
     } finally {
       setLoading(false);
     }
@@ -184,8 +195,8 @@ const UpdateConfigModal = ({
   return (
     <Modal
       title={
-        <div className="flex items-center gap-2">
-          <FaCog className="text-blue-500" />
+        <div className='flex items-center gap-2'>
+          <FaCog className='text-blue-500' />
           <span>{t('更新容器配置')}</span>
         </div>
       }
@@ -196,130 +207,131 @@ const UpdateConfigModal = ({
       cancelText={t('取消')}
       confirmLoading={loading}
       width={700}
-      className="update-config-modal"
+      className='update-config-modal'
     >
-      <div className="space-y-4 max-h-[600px] overflow-y-auto">
+      <div className='space-y-4 max-h-[600px] overflow-y-auto'>
         {/* Container Info */}
-        <Card className="border-0 bg-gray-50">
-          <div className="flex items-center justify-between">
+        <Card className='border-0 bg-gray-50'>
+          <div className='flex items-center justify-between'>
             <div>
-              <Text strong className="text-base">
+              <Text strong className='text-base'>
                 {deployment?.container_name}
               </Text>
-              <div className="mt-1">
-                <Text type="secondary" size="small">
+              <div className='mt-1'>
+                <Text type='secondary' size='small'>
                   ID: {deployment?.id}
                 </Text>
               </div>
             </div>
-            <Tag color="blue">{deployment?.status}</Tag>
+            <Tag color='blue'>{deployment?.status}</Tag>
           </div>
         </Card>
 
         {/* Warning Banner */}
         <Banner
-          type="warning"
+          type='warning'
           icon={<FaExclamationTriangle />}
           title={t('重要提醒')}
           description={
-            <div className="space-y-2">
-              <p>{t('更新容器配置可能会导致容器重启,请确保在合适的时间进行此操作。')}</p>
+            <div className='space-y-2'>
+              <p>
+                {t(
+                  '更新容器配置可能会导致容器重启,请确保在合适的时间进行此操作。',
+                )}
+              </p>
               <p>{t('某些配置更改可能需要几分钟才能生效。')}</p>
             </div>
           }
         />
 
-        <Form
-          getFormApi={(api) => (formRef.current = api)}
-          layout="vertical"
-        >
+        <Form getFormApi={(api) => (formRef.current = api)} layout='vertical'>
           <Collapse defaultActiveKey={['docker']}>
             {/* Docker Configuration */}
-            <Collapse.Panel 
+            <Collapse.Panel
               header={
-                <div className="flex items-center gap-2">
-                  <FaDocker className="text-blue-600" />
-                  <span>{t('Docker 配置')}</span>
+                <div className='flex items-center gap-2'>
+                  <FaDocker className='text-blue-600' />
+                  <span>{t('镜像配置')}</span>
                 </div>
               }
-              itemKey="docker"
+              itemKey='docker'
             >
-              <div className="space-y-4">
+              <div className='space-y-4'>
                 <Form.Input
-                  field="image_url"
+                  field='image_url'
                   label={t('镜像地址')}
                   placeholder={t('例如: nginx:latest')}
                   rules={[
-                    { 
+                    {
                       type: 'string',
-                      message: t('请输入有效的镜像地址') 
-                    }
+                      message: t('请输入有效的镜像地址'),
+                    },
                   ]}
                 />
 
                 <Form.Input
-                  field="registry_username"
+                  field='registry_username'
                   label={t('镜像仓库用户名')}
                   placeholder={t('如果镜像为私有,请填写用户名')}
                 />
 
                 <Form.Input
-                  field="registry_secret"
+                  field='registry_secret'
                   label={t('镜像仓库密码')}
-                  mode="password"
+                  mode='password'
                   placeholder={t('如果镜像为私有,请填写密码或Token')}
                 />
               </div>
             </Collapse.Panel>
 
             {/* Network Configuration */}
-            <Collapse.Panel 
+            <Collapse.Panel
               header={
-                <div className="flex items-center gap-2">
-                  <FaNetworkWired className="text-green-600" />
+                <div className='flex items-center gap-2'>
+                  <FaNetworkWired className='text-green-600' />
                   <span>{t('网络配置')}</span>
                 </div>
               }
-              itemKey="network"
+              itemKey='network'
             >
               <Form.InputNumber
-                field="traffic_port"
+                field='traffic_port'
                 label={t('流量端口')}
                 placeholder={t('容器对外暴露的端口')}
                 min={1}
                 max={65535}
                 style={{ width: '100%' }}
                 rules={[
-                  { 
+                  {
                     type: 'number',
                     min: 1,
                     max: 65535,
-                    message: t('端口号必须在1-65535之间') 
-                  }
+                    message: t('端口号必须在1-65535之间'),
+                  },
                 ]}
               />
             </Collapse.Panel>
 
             {/* Startup Configuration */}
-            <Collapse.Panel 
+            <Collapse.Panel
               header={
-                <div className="flex items-center gap-2">
-                  <FaTerminal className="text-purple-600" />
+                <div className='flex items-center gap-2'>
+                  <FaTerminal className='text-purple-600' />
                   <span>{t('启动配置')}</span>
                 </div>
               }
-              itemKey="startup"
+              itemKey='startup'
             >
-              <div className="space-y-4">
+              <div className='space-y-4'>
                 <Form.Input
-                  field="entrypoint"
+                  field='entrypoint'
                   label={t('启动命令 (Entrypoint)')}
                   placeholder={t('例如: /bin/bash -c "python app.py"')}
                   helpText={t('多个命令用空格分隔')}
                 />
 
                 <Form.Input
-                  field="command"
+                  field='command'
                   label={t('运行命令 (Command)')}
                   placeholder={t('容器启动后执行的命令')}
                 />
@@ -327,34 +339,34 @@ const UpdateConfigModal = ({
             </Collapse.Panel>
 
             {/* Environment Variables */}
-            <Collapse.Panel 
+            <Collapse.Panel
               header={
-                <div className="flex items-center gap-2">
-                  <FaKey className="text-orange-600" />
+                <div className='flex items-center gap-2'>
+                  <FaKey className='text-orange-600' />
                   <span>{t('环境变量')}</span>
-                  <Tag size="small">{envVars.length}</Tag>
+                  <Tag size='small'>{envVars.length}</Tag>
                 </div>
               }
-              itemKey="env"
+              itemKey='env'
             >
-              <div className="space-y-4">
+              <div className='space-y-4'>
                 {/* Regular Environment Variables */}
                 <div>
-                  <div className="flex items-center justify-between mb-3">
+                  <div className='flex items-center justify-between mb-3'>
                     <Text strong>{t('普通环境变量')}</Text>
                     <Button
-                      size="small"
+                      size='small'
                       icon={<FaPlus />}
                       onClick={addEnvVar}
-                      theme="borderless"
-                      type="primary"
+                      theme='borderless'
+                      type='primary'
                     >
                       {t('添加')}
                     </Button>
                   </div>
-                  
+
                   {envVars.map((envVar, index) => (
-                    <div key={index} className="flex items-end gap-2 mb-2">
+                    <div key={index} className='flex items-end gap-2 mb-2'>
                       <Input
                         placeholder={t('变量名')}
                         value={envVar.key}
@@ -365,22 +377,24 @@ const UpdateConfigModal = ({
                       <Input
                         placeholder={t('变量值')}
                         value={envVar.value}
-                        onChange={(value) => updateEnvVar(index, 'value', value)}
+                        onChange={(value) =>
+                          updateEnvVar(index, 'value', value)
+                        }
                         style={{ flex: 2 }}
                       />
                       <Button
-                        size="small"
+                        size='small'
                         icon={<FaMinus />}
                         onClick={() => removeEnvVar(index)}
-                        theme="borderless"
-                        type="danger"
+                        theme='borderless'
+                        type='danger'
                       />
                     </div>
                   ))}
-                  
+
                   {envVars.length === 0 && (
-                    <div className="text-center text-gray-500 py-4 border-2 border-dashed border-gray-300 rounded-lg">
-                      <Text type="secondary">{t('暂无环境变量')}</Text>
+                    <div className='text-center text-gray-500 py-4 border-2 border-dashed border-gray-300 rounded-lg'>
+                      <Text type='secondary'>{t('暂无环境变量')}</Text>
                     </div>
                   )}
                 </div>
@@ -389,61 +403,67 @@ const UpdateConfigModal = ({
 
                 {/* Secret Environment Variables */}
                 <div>
-                  <div className="flex items-center justify-between mb-3">
-                    <div className="flex items-center gap-2">
+                  <div className='flex items-center justify-between mb-3'>
+                    <div className='flex items-center gap-2'>
                       <Text strong>{t('机密环境变量')}</Text>
-                      <Tag size="small" type="danger">
+                      <Tag size='small' type='danger'>
                         {t('加密存储')}
                       </Tag>
                     </div>
                     <Button
-                      size="small"
+                      size='small'
                       icon={<FaPlus />}
                       onClick={addSecretEnvVar}
-                      theme="borderless"
-                      type="danger"
+                      theme='borderless'
+                      type='danger'
                     >
                       {t('添加')}
                     </Button>
                   </div>
-                  
+
                   {secretEnvVars.map((envVar, index) => (
-                    <div key={index} className="flex items-end gap-2 mb-2">
+                    <div key={index} className='flex items-end gap-2 mb-2'>
                       <Input
                         placeholder={t('变量名')}
                         value={envVar.key}
-                        onChange={(value) => updateSecretEnvVar(index, 'key', value)}
+                        onChange={(value) =>
+                          updateSecretEnvVar(index, 'key', value)
+                        }
                         style={{ flex: 1 }}
                       />
                       <Text>=</Text>
                       <Input
-                        mode="password"
+                        mode='password'
                         placeholder={t('变量值')}
                         value={envVar.value}
-                        onChange={(value) => updateSecretEnvVar(index, 'value', value)}
+                        onChange={(value) =>
+                          updateSecretEnvVar(index, 'value', value)
+                        }
                         style={{ flex: 2 }}
                       />
                       <Button
-                        size="small"
+                        size='small'
                         icon={<FaMinus />}
                         onClick={() => removeSecretEnvVar(index)}
-                        theme="borderless"
-                        type="danger"
+                        theme='borderless'
+                        type='danger'
                       />
                     </div>
                   ))}
-                  
+
                   {secretEnvVars.length === 0 && (
-                    <div className="text-center text-gray-500 py-4 border-2 border-dashed border-red-200 rounded-lg bg-red-50">
-                      <Text type="secondary">{t('暂无机密环境变量')}</Text>
+                    <div className='text-center text-gray-500 py-4 border-2 border-dashed border-red-200 rounded-lg bg-red-50'>
+                      <Text type='secondary'>{t('暂无机密环境变量')}</Text>
                     </div>
                   )}
-                  
+
                   <Banner
-                    type="info"
+                    type='info'
                     title={t('机密环境变量说明')}
-                    description={t('机密环境变量将被加密存储,适用于存储密码、API密钥等敏感信息。')}
-                    size="small"
+                    description={t(
+                      '机密环境变量将被加密存储,适用于存储密码、API密钥等敏感信息。',
+                    )}
+                    size='small'
                   />
                 </div>
               </div>
@@ -452,16 +472,18 @@ const UpdateConfigModal = ({
         </Form>
 
         {/* Final Warning */}
-        <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
-          <div className="flex items-start gap-2">
-            <FaExclamationTriangle className="text-yellow-600 mt-0.5" />
+        <div className='bg-yellow-50 border border-yellow-200 rounded-lg p-3'>
+          <div className='flex items-start gap-2'>
+            <FaExclamationTriangle className='text-yellow-600 mt-0.5' />
             <div>
-              <Text strong className="text-yellow-800">
+              <Text strong className='text-yellow-800'>
                 {t('配置更新确认')}
               </Text>
-              <div className="mt-1">
-                <Text size="small" className="text-yellow-700">
-                  {t('更新配置后,容器可能需要重启以应用新的设置。请确保您了解这些更改的影响。')}
+              <div className='mt-1'>
+                <Text size='small' className='text-yellow-700'>
+                  {t(
+                    '更新配置后,容器可能需要重启以应用新的设置。请确保您了解这些更改的影响。',
+                  )}
                 </Text>
               </div>
             </div>
@@ -472,4 +494,4 @@ const UpdateConfigModal = ({
   );
 };
 
-export default UpdateConfigModal;
+export default UpdateConfigModal;

+ 313 - 229
web/src/components/table/model-deployments/modals/ViewDetailsModal.jsx

@@ -31,8 +31,8 @@ import {
   Badge,
   Tooltip,
 } from '@douyinfe/semi-ui';
-import { 
-  FaInfoCircle, 
+import {
+  FaInfoCircle,
   FaServer,
   FaClock,
   FaMapMarkerAlt,
@@ -43,16 +43,16 @@ import {
   FaLink,
 } from 'react-icons/fa';
 import { IconRefresh } from '@douyinfe/semi-icons';
-import { API, showError, showSuccess, timestamp2string } from '../../../../helpers';
+import {
+  API,
+  showError,
+  showSuccess,
+  timestamp2string,
+} from '../../../../helpers';
 
 const { Text, Title } = Typography;
 
-const ViewDetailsModal = ({ 
-  visible, 
-  onCancel, 
-  deployment, 
-  t 
-}) => {
+const ViewDetailsModal = ({ visible, onCancel, deployment, t }) => {
   const [details, setDetails] = useState(null);
   const [loading, setLoading] = useState(false);
   const [containers, setContainers] = useState([]);
@@ -60,7 +60,7 @@ const ViewDetailsModal = ({
 
   const fetchDetails = async () => {
     if (!deployment?.id) return;
-    
+
     setLoading(true);
     try {
       const response = await API.get(`/api/deployments/${deployment.id}`);
@@ -68,7 +68,11 @@ const ViewDetailsModal = ({
         setDetails(response.data.data);
       }
     } catch (error) {
-      showError(t('获取详情失败') + ': ' + (error.response?.data?.message || error.message));
+      showError(
+        t('获取详情失败') +
+          ': ' +
+          (error.response?.data?.message || error.message),
+      );
     } finally {
       setLoading(false);
     }
@@ -79,12 +83,18 @@ const ViewDetailsModal = ({
 
     setContainersLoading(true);
     try {
-      const response = await API.get(`/api/deployments/${deployment.id}/containers`);
+      const response = await API.get(
+        `/api/deployments/${deployment.id}/containers`,
+      );
       if (response.data.success) {
         setContainers(response.data.data?.containers || []);
       }
     } catch (error) {
-      showError(t('获取容器信息失败') + ': ' + (error.response?.data?.message || error.message));
+      showError(
+        t('获取容器信息失败') +
+          ': ' +
+          (error.response?.data?.message || error.message),
+      );
     } finally {
       setContainersLoading(false);
     }
@@ -102,7 +112,7 @@ const ViewDetailsModal = ({
 
   const handleCopyId = () => {
     navigator.clipboard.writeText(deployment?.id);
-    showSuccess(t('ID已复制到剪贴板'));
+    showSuccess(t('已复制 ID 到剪贴板'));
   };
 
   const handleRefresh = () => {
@@ -112,12 +122,16 @@ const ViewDetailsModal = ({
 
   const getStatusConfig = (status) => {
     const statusConfig = {
-      'running': { color: 'green', text: '运行中', icon: '🟢' },
-      'completed': { color: 'green', text: '已完成', icon: '✅' },
+      running: { color: 'green', text: '运行中', icon: '🟢' },
+      completed: { color: 'green', text: '已完成', icon: '✅' },
       'deployment requested': { color: 'blue', text: '部署请求中', icon: '🔄' },
-      'termination requested': { color: 'orange', text: '终止请求中', icon: '⏸️' },
-      'destroyed': { color: 'red', text: '已销毁', icon: '🔴' },
-      'failed': { color: 'red', text: '失败', icon: '❌' }
+      'termination requested': {
+        color: 'orange',
+        text: '终止请求中',
+        icon: '⏸️',
+      },
+      destroyed: { color: 'red', text: '已销毁', icon: '🔴' },
+      failed: { color: 'red', text: '失败', icon: '❌' },
     };
     return statusConfig[status] || { color: 'grey', text: status, icon: '❓' };
   };
@@ -127,149 +141,167 @@ const ViewDetailsModal = ({
   return (
     <Modal
       title={
-        <div className="flex items-center gap-2">
-          <FaInfoCircle className="text-blue-500" />
+        <div className='flex items-center gap-2'>
+          <FaInfoCircle className='text-blue-500' />
           <span>{t('容器详情')}</span>
         </div>
       }
       visible={visible}
       onCancel={onCancel}
       footer={
-        <div className="flex justify-between">
-          <Button 
-            icon={<IconRefresh />} 
+        <div className='flex justify-between'>
+          <Button
+            icon={<IconRefresh />}
             onClick={handleRefresh}
             loading={loading || containersLoading}
-            theme="borderless"
+            theme='borderless'
           >
             {t('刷新')}
           </Button>
-          <Button onClick={onCancel}>
-            {t('关闭')}
-          </Button>
+          <Button onClick={onCancel}>{t('关闭')}</Button>
         </div>
       }
       width={800}
-      className="deployment-details-modal"
+      className='deployment-details-modal'
     >
       {loading && !details ? (
-        <div className="flex items-center justify-center py-12">
-          <Spin size="large" tip={t('加载详情中...')} />
+        <div className='flex items-center justify-center py-12'>
+          <Spin size='large' tip={t('加载详情中...')} />
         </div>
       ) : details ? (
-        <div className="space-y-4 max-h-[600px] overflow-y-auto">
+        <div className='space-y-4 max-h-[600px] overflow-y-auto'>
           {/* Basic Info */}
-          <Card 
+          <Card
             title={
-              <div className="flex items-center gap-2">
-                <FaServer className="text-blue-500" />
+              <div className='flex items-center gap-2'>
+                <FaServer className='text-blue-500' />
                 <span>{t('基本信息')}</span>
               </div>
             }
-            className="border-0 shadow-sm"
-          >
-            <Descriptions data={[
-              {
-                key: t('容器名称'),
-                value: (
-                  <div className="flex items-center gap-2">
-                    <Text strong className="text-base">
-                      {details.deployment_name || details.id}
-                    </Text>
-                    <Button
-                      size="small"
-                      theme="borderless"
-                      icon={<FaCopy />}
-                      onClick={handleCopyId}
-                      className="opacity-70 hover:opacity-100"
-                    />
-                  </div>
-                )
-              },
-              {
-                key: t('容器ID'),
-                value: (
-                  <Text type="secondary" className="font-mono text-sm">
-                    {details.id}
-                  </Text>
-                )
-              },
-              {
-                key: t('状态'),
-                value: (
-                  <div className="flex items-center gap-2">
-                    <span>{statusConfig.icon}</span>
-                    <Tag color={statusConfig.color}>
-                      {t(statusConfig.text)}
-                    </Tag>
-                  </div>
-                )
-              },
-              {
-                key: t('创建时间'),
-                value: timestamp2string(details.created_at)
-              }
-            ]} />
-          </Card>
-
-          {/* Hardware & Performance */}
-          <Card 
-            title={
-              <div className="flex items-center gap-2">
-                <FaChartLine className="text-green-500" />
-                <span>{t('硬件与性能')}</span>
-              </div>
-            }
-            className="border-0 shadow-sm"
+            className='border-0 shadow-sm'
           >
-            <div className="space-y-4">
-              <Descriptions data={[
+            <Descriptions
+              data={[
                 {
-                  key: t('硬件类型'),
+                  key: t('容器名称'),
                   value: (
-                    <div className="flex items-center gap-2">
-                      <Tag color="blue">{details.brand_name}</Tag>
-                      <Text strong>{details.hardware_name}</Text>
+                    <div className='flex items-center gap-2'>
+                      <Text strong className='text-base'>
+                        {details.deployment_name || details.id}
+                      </Text>
+                      <Button
+                        size='small'
+                        theme='borderless'
+                        icon={<FaCopy />}
+                        onClick={handleCopyId}
+                        className='opacity-70 hover:opacity-100'
+                      />
                     </div>
-                  )
+                  ),
                 },
                 {
-                  key: t('GPU数量'),
+                  key: t('容器ID'),
                   value: (
-                    <div className="flex items-center gap-2">
-                      <Badge count={details.total_gpus} theme="solid" type="primary">
-                        <FaServer className="text-purple-500" />
-                      </Badge>
-                      <Text>{t('总计')} {details.total_gpus} {t('个GPU')}</Text>
-                    </div>
-                  )
+                    <Text type='secondary' className='font-mono text-sm'>
+                      {details.id}
+                    </Text>
+                  ),
                 },
                 {
-                  key: t('容器配置'),
+                  key: t('状态'),
                   value: (
-                    <div className="space-y-1">
-                      <div>{t('每容器GPU数')}: {details.gpus_per_container}</div>
-                      <div>{t('容器总数')}: {details.total_containers}</div>
+                    <div className='flex items-center gap-2'>
+                      <span>{statusConfig.icon}</span>
+                      <Tag color={statusConfig.color}>
+                        {t(statusConfig.text)}
+                      </Tag>
                     </div>
-                  )
-                }
-              ]} />
+                  ),
+                },
+                {
+                  key: t('创建时间'),
+                  value: timestamp2string(details.created_at),
+                },
+              ]}
+            />
+          </Card>
+
+          {/* Hardware & Performance */}
+          <Card
+            title={
+              <div className='flex items-center gap-2'>
+                <FaChartLine className='text-green-500' />
+                <span>{t('硬件与性能')}</span>
+              </div>
+            }
+            className='border-0 shadow-sm'
+          >
+            <div className='space-y-4'>
+              <Descriptions
+                data={[
+                  {
+                    key: t('硬件类型'),
+                    value: (
+                      <div className='flex items-center gap-2'>
+                        <Tag color='blue'>{details.brand_name}</Tag>
+                        <Text strong>{details.hardware_name}</Text>
+                      </div>
+                    ),
+                  },
+                  {
+                    key: t('GPU数量'),
+                    value: (
+                      <div className='flex items-center gap-2'>
+                        <Badge
+                          count={details.total_gpus}
+                          theme='solid'
+                          type='primary'
+                        >
+                          <FaServer className='text-purple-500' />
+                        </Badge>
+                        <Text>
+                          {t('总计')} {details.total_gpus} {t('个GPU')}
+                        </Text>
+                      </div>
+                    ),
+                  },
+                  {
+                    key: t('容器配置'),
+                    value: (
+                      <div className='space-y-1'>
+                        <div>
+                          {t('每容器GPU数')}: {details.gpus_per_container}
+                        </div>
+                        <div>
+                          {t('容器总数')}: {details.total_containers}
+                        </div>
+                      </div>
+                    ),
+                  },
+                ]}
+              />
 
               {/* Progress Bar */}
-              <div className="space-y-2">
-                <div className="flex items-center justify-between">
+              <div className='space-y-2'>
+                <div className='flex items-center justify-between'>
                   <Text strong>{t('完成进度')}</Text>
                   <Text>{details.completed_percent}%</Text>
                 </div>
                 <Progress
                   percent={details.completed_percent}
-                  status={details.completed_percent === 100 ? 'success' : 'normal'}
+                  status={
+                    details.completed_percent === 100 ? 'success' : 'normal'
+                  }
                   strokeWidth={8}
                   showInfo={false}
                 />
-                <div className="flex justify-between text-xs text-gray-500">
-                  <span>{t('已服务')}: {details.compute_minutes_served} {t('分钟')}</span>
-                  <span>{t('剩余')}: {details.compute_minutes_remaining} {t('分钟')}</span>
+                <div className='flex justify-between text-xs text-gray-500'>
+                  <span>
+                    {t('已服务')}: {details.compute_minutes_served} {t('分钟')}
+                  </span>
+                  <span>
+                    {t('剩余')}: {details.compute_minutes_remaining} {t('分钟')}
+                  </span>
                 </div>
               </div>
             </div>
@@ -277,56 +309,70 @@ const ViewDetailsModal = ({
 
           {/* Container Configuration */}
           {details.container_config && (
-            <Card 
+            <Card
               title={
-                <div className="flex items-center gap-2">
-                  <FaDocker className="text-blue-600" />
+                <div className='flex items-center gap-2'>
+                  <FaDocker className='text-blue-600' />
                   <span>{t('容器配置')}</span>
                 </div>
               }
-              className="border-0 shadow-sm"
+              className='border-0 shadow-sm'
             >
-              <div className="space-y-3">
-                <Descriptions data={[
-                  {
-                    key: t('镜像地址'),
-                    value: (
-                      <Text className="font-mono text-sm break-all">
-                        {details.container_config.image_url || 'N/A'}
-                      </Text>
-                    )
-                  },
-                  {
-                    key: t('流量端口'),
-                    value: details.container_config.traffic_port || 'N/A'
-                  },
-                  {
-                    key: t('启动命令'),
-                    value: (
-                      <Text className="font-mono text-sm">
-                        {details.container_config.entrypoint ? 
-                          details.container_config.entrypoint.join(' ') : 'N/A'
-                        }
-                      </Text>
-                    )
-                  }
-                ]} />
+              <div className='space-y-3'>
+                <Descriptions
+                  data={[
+                    {
+                      key: t('镜像地址'),
+                      value: (
+                        <Text className='font-mono text-sm break-all'>
+                          {details.container_config.image_url || 'N/A'}
+                        </Text>
+                      ),
+                    },
+                    {
+                      key: t('流量端口'),
+                      value: details.container_config.traffic_port || 'N/A',
+                    },
+                    {
+                      key: t('启动命令'),
+                      value: (
+                        <Text className='font-mono text-sm'>
+                          {details.container_config.entrypoint
+                            ? details.container_config.entrypoint.join(' ')
+                            : 'N/A'}
+                        </Text>
+                      ),
+                    },
+                  ]}
+                />
 
                 {/* Environment Variables */}
-                {details.container_config.env_variables && 
-                 Object.keys(details.container_config.env_variables).length > 0 && (
-                  <div className="mt-4">
-                    <Text strong className="block mb-2">{t('环境变量')}:</Text>
-                    <div className="bg-gray-50 p-3 rounded-lg max-h-32 overflow-y-auto">
-                      {Object.entries(details.container_config.env_variables).map(([key, value]) => (
-                        <div key={key} className="flex gap-2 text-sm font-mono mb-1">
-                          <span className="text-blue-600 font-medium">{key}=</span>
-                          <span className="text-gray-700 break-all">{String(value)}</span>
-                        </div>
-                      ))}
+                {details.container_config.env_variables &&
+                  Object.keys(details.container_config.env_variables).length >
+                    0 && (
+                    <div className='mt-4'>
+                      <Text strong className='block mb-2'>
+                        {t('环境变量')}:
+                      </Text>
+                      <div className='bg-gray-50 p-3 rounded-lg max-h-32 overflow-y-auto'>
+                        {Object.entries(
+                          details.container_config.env_variables,
+                        ).map(([key, value]) => (
+                          <div
+                            key={key}
+                            className='flex gap-2 text-sm font-mono mb-1'
+                          >
+                            <span className='text-blue-600 font-medium'>
+                              {key}=
+                            </span>
+                            <span className='text-gray-700 break-all'>
+                              {String(value)}
+                            </span>
+                          </div>
+                        ))}
+                      </div>
                     </div>
-                  </div>
-                )}
+                  )}
               </div>
             </Card>
           )}
@@ -334,50 +380,63 @@ const ViewDetailsModal = ({
           {/* Containers List */}
           <Card
             title={
-              <div className="flex items-center gap-2">
-                <FaServer className="text-indigo-500" />
+              <div className='flex items-center gap-2'>
+                <FaServer className='text-indigo-500' />
                 <span>{t('容器实例')}</span>
               </div>
             }
-            className="border-0 shadow-sm"
+            className='border-0 shadow-sm'
           >
             {containersLoading ? (
-              <div className="flex items-center justify-center py-6">
+              <div className='flex items-center justify-center py-6'>
                 <Spin tip={t('加载容器信息中...')} />
               </div>
             ) : containers.length === 0 ? (
-              <Empty description={t('暂无容器信息')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
+              <Empty
+                description={t('暂无容器信息')}
+                image={Empty.PRESENTED_IMAGE_SIMPLE}
+              />
             ) : (
-              <div className="space-y-3">
+              <div className='space-y-3'>
                 {containers.map((ctr) => (
                   <Card
                     key={ctr.container_id}
-                    className="bg-gray-50 border border-gray-100"
+                    className='bg-gray-50 border border-gray-100'
                     bodyStyle={{ padding: '12px 16px' }}
                   >
-                    <div className="flex flex-wrap items-center justify-between gap-3">
-                      <div className="flex flex-col gap-1">
-                        <Text strong className="font-mono text-sm">
+                    <div className='flex flex-wrap items-center justify-between gap-3'>
+                      <div className='flex flex-col gap-1'>
+                        <Text strong className='font-mono text-sm'>
                           {ctr.container_id}
                         </Text>
-                        <Text size="small" type="secondary">
-                          {t('设备')} {ctr.device_id || '--'} · {t('状态')} {ctr.status || '--'}
+                        <Text size='small' type='secondary'>
+                          {t('设备')} {ctr.device_id || '--'} · {t('状态')}{' '}
+                          {ctr.status || '--'}
                         </Text>
-                        <Text size="small" type="secondary">
-                          {t('创建时间')}: {ctr.created_at ? timestamp2string(ctr.created_at) : '--'}
+                        <Text size='small' type='secondary'>
+                          {t('创建时间')}:{' '}
+                          {ctr.created_at
+                            ? timestamp2string(ctr.created_at)
+                            : '--'}
                         </Text>
                       </div>
-                      <div className="flex flex-col items-end gap-2">
-                        <Tag color="blue" size="small">
+                      <div className='flex flex-col items-end gap-2'>
+                        <Tag color='blue' size='small'>
                           {t('GPU/容器')}: {ctr.gpus_per_container ?? '--'}
                         </Tag>
                         {ctr.public_url && (
                           <Tooltip content={ctr.public_url}>
                             <Button
                               icon={<FaLink />}
-                              size="small"
-                              theme="light"
-                              onClick={() => window.open(ctr.public_url, '_blank', 'noopener,noreferrer')}
+                              size='small'
+                              theme='light'
+                              onClick={() =>
+                                window.open(
+                                  ctr.public_url,
+                                  '_blank',
+                                  'noopener,noreferrer',
+                                )
+                              }
                             >
                               {t('访问容器')}
                             </Button>
@@ -387,17 +446,26 @@ const ViewDetailsModal = ({
                     </div>
 
                     {ctr.events && ctr.events.length > 0 && (
-                      <div className="mt-3 bg-white rounded-md border border-gray-100 p-3">
-                        <Text size="small" type="secondary" className="block mb-2">
+                      <div className='mt-3 bg-white rounded-md border border-gray-100 p-3'>
+                        <Text
+                          size='small'
+                          type='secondary'
+                          className='block mb-2'
+                        >
                           {t('最近事件')}
                         </Text>
-                        <div className="space-y-2 max-h-32 overflow-y-auto">
+                        <div className='space-y-2 max-h-32 overflow-y-auto'>
                           {ctr.events.map((event, index) => (
-                            <div key={`${ctr.container_id}-${event.time}-${index}`} className="flex gap-3 text-xs font-mono">
-                              <span className="text-gray-500 min-w-[140px]">
-                                {event.time ? timestamp2string(event.time) : '--'}
+                            <div
+                              key={`${ctr.container_id}-${event.time}-${index}`}
+                              className='flex gap-3 text-xs font-mono'
+                            >
+                              <span className='text-gray-500 min-w-[140px]'>
+                                {event.time
+                                  ? timestamp2string(event.time)
+                                  : '--'}
                               </span>
-                              <span className="text-gray-700 break-all flex-1">
+                              <span className='text-gray-700 break-all flex-1'>
                                 {event.message || '--'}
                               </span>
                             </div>
@@ -413,21 +481,23 @@ const ViewDetailsModal = ({
 
           {/* Location Information */}
           {details.locations && details.locations.length > 0 && (
-            <Card 
+            <Card
               title={
-                <div className="flex items-center gap-2">
-                  <FaMapMarkerAlt className="text-orange-500" />
+                <div className='flex items-center gap-2'>
+                  <FaMapMarkerAlt className='text-orange-500' />
                   <span>{t('部署位置')}</span>
                 </div>
               }
-              className="border-0 shadow-sm"
+              className='border-0 shadow-sm'
             >
-              <div className="flex flex-wrap gap-2">
+              <div className='flex flex-wrap gap-2'>
                 {details.locations.map((location) => (
-                  <Tag key={location.id} color="orange" size="large">
-                    <div className="flex items-center gap-1">
+                  <Tag key={location.id} color='orange' size='large'>
+                    <div className='flex items-center gap-1'>
                       <span>🌍</span>
-                      <span>{location.name} ({location.iso2})</span>
+                      <span>
+                        {location.name} ({location.iso2})
+                      </span>
                     </div>
                   </Tag>
                 ))}
@@ -436,68 +506,82 @@ const ViewDetailsModal = ({
           )}
 
           {/* Cost Information */}
-          <Card 
+          <Card
             title={
-              <div className="flex items-center gap-2">
-                <FaMoneyBillWave className="text-green-500" />
+              <div className='flex items-center gap-2'>
+                <FaMoneyBillWave className='text-green-500' />
                 <span>{t('费用信息')}</span>
               </div>
             }
-            className="border-0 shadow-sm"
+            className='border-0 shadow-sm'
           >
-            <div className="space-y-3">
-              <div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
+            <div className='space-y-3'>
+              <div className='flex items-center justify-between p-3 bg-green-50 rounded-lg'>
                 <Text>{t('已支付金额')}</Text>
-                <Text strong className="text-lg text-green-600">
-                  ${details.amount_paid ? details.amount_paid.toFixed(2) : '0.00'} USDC
+                <Text strong className='text-lg text-green-600'>
+                  $
+                  {details.amount_paid
+                    ? details.amount_paid.toFixed(2)
+                    : '0.00'}{' '}
+                  USDC
                 </Text>
               </div>
-              
-              <div className="grid grid-cols-2 gap-4 text-sm">
-                <div className="flex justify-between">
-                  <Text type="secondary">{t('计费开始')}:</Text>
-                  <Text>{details.started_at ? timestamp2string(details.started_at) : 'N/A'}</Text>
+
+              <div className='grid grid-cols-2 gap-4 text-sm'>
+                <div className='flex justify-between'>
+                  <Text type='secondary'>{t('计费开始')}:</Text>
+                  <Text>
+                    {details.started_at
+                      ? timestamp2string(details.started_at)
+                      : 'N/A'}
+                  </Text>
                 </div>
-                <div className="flex justify-between">
-                  <Text type="secondary">{t('预计结束')}:</Text>
-                  <Text>{details.finished_at ? timestamp2string(details.finished_at) : 'N/A'}</Text>
+                <div className='flex justify-between'>
+                  <Text type='secondary'>{t('预计结束')}:</Text>
+                  <Text>
+                    {details.finished_at
+                      ? timestamp2string(details.finished_at)
+                      : 'N/A'}
+                  </Text>
                 </div>
               </div>
             </div>
           </Card>
 
           {/* Time Information */}
-          <Card 
+          <Card
             title={
-              <div className="flex items-center gap-2">
-                <FaClock className="text-purple-500" />
+              <div className='flex items-center gap-2'>
+                <FaClock className='text-purple-500' />
                 <span>{t('时间信息')}</span>
               </div>
             }
-            className="border-0 shadow-sm"
+            className='border-0 shadow-sm'
           >
-            <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
-              <div className="space-y-2">
-                <div className="flex items-center justify-between">
-                  <Text type="secondary">{t('已运行时间')}:</Text>
+            <div className='grid grid-cols-1 md:grid-cols-2 gap-4'>
+              <div className='space-y-2'>
+                <div className='flex items-center justify-between'>
+                  <Text type='secondary'>{t('已运行时间')}:</Text>
                   <Text strong>
-                    {Math.floor(details.compute_minutes_served / 60)}h {details.compute_minutes_served % 60}m
+                    {Math.floor(details.compute_minutes_served / 60)}h{' '}
+                    {details.compute_minutes_served % 60}m
                   </Text>
                 </div>
-                <div className="flex items-center justify-between">
-                  <Text type="secondary">{t('剩余时间')}:</Text>
-                  <Text strong className="text-orange-600">
-                    {Math.floor(details.compute_minutes_remaining / 60)}h {details.compute_minutes_remaining % 60}m
+                <div className='flex items-center justify-between'>
+                  <Text type='secondary'>{t('剩余时间')}:</Text>
+                  <Text strong className='text-orange-600'>
+                    {Math.floor(details.compute_minutes_remaining / 60)}h{' '}
+                    {details.compute_minutes_remaining % 60}m
                   </Text>
                 </div>
               </div>
-              <div className="space-y-2">
-                <div className="flex items-center justify-between">
-                  <Text type="secondary">{t('创建时间')}:</Text>
+              <div className='space-y-2'>
+                <div className='flex items-center justify-between'>
+                  <Text type='secondary'>{t('创建时间')}:</Text>
                   <Text>{timestamp2string(details.created_at)}</Text>
                 </div>
-                <div className="flex items-center justify-between">
-                  <Text type="secondary">{t('最后更新')}:</Text>
+                <div className='flex items-center justify-between'>
+                  <Text type='secondary'>{t('最后更新')}:</Text>
                   <Text>{timestamp2string(details.updated_at)}</Text>
                 </div>
               </div>
@@ -505,7 +589,7 @@ const ViewDetailsModal = ({
           </Card>
         </div>
       ) : (
-        <Empty 
+        <Empty
           image={Empty.PRESENTED_IMAGE_SIMPLE}
           description={t('无法获取容器详情')}
         />

+ 203 - 140
web/src/components/table/model-deployments/modals/ViewLogsModal.jsx

@@ -44,18 +44,19 @@ import {
   FaLink,
 } from 'react-icons/fa';
 import { IconRefresh, IconDownload } from '@douyinfe/semi-icons';
-import { API, showError, showSuccess, copy, timestamp2string } from '../../../../helpers';
+import {
+  API,
+  showError,
+  showSuccess,
+  copy,
+  timestamp2string,
+} from '../../../../helpers';
 
 const { Text } = Typography;
 
 const ALL_CONTAINERS = '__all__';
 
-const ViewLogsModal = ({ 
-  visible, 
-  onCancel, 
-  deployment, 
-  t 
-}) => {
+const ViewLogsModal = ({ visible, onCancel, deployment, t }) => {
   const [logLines, setLogLines] = useState([]);
   const [loading, setLoading] = useState(false);
   const [autoRefresh, setAutoRefresh] = useState(false);
@@ -63,12 +64,13 @@ const ViewLogsModal = ({
   const [following, setFollowing] = useState(false);
   const [containers, setContainers] = useState([]);
   const [containersLoading, setContainersLoading] = useState(false);
-  const [selectedContainerId, setSelectedContainerId] = useState(ALL_CONTAINERS);
+  const [selectedContainerId, setSelectedContainerId] =
+    useState(ALL_CONTAINERS);
   const [containerDetails, setContainerDetails] = useState(null);
   const [containerDetailsLoading, setContainerDetailsLoading] = useState(false);
   const [streamFilter, setStreamFilter] = useState('stdout');
   const [lastUpdatedAt, setLastUpdatedAt] = useState(null);
-  
+
   const logContainerRef = useRef(null);
   const autoRefreshRef = useRef(null);
 
@@ -100,7 +102,10 @@ const ViewLogsModal = ({
   const fetchLogs = async (containerIdOverride = undefined) => {
     if (!deployment?.id) return;
 
-    const containerId = typeof containerIdOverride === 'string' ? containerIdOverride : selectedContainerId;
+    const containerId =
+      typeof containerIdOverride === 'string'
+        ? containerIdOverride
+        : selectedContainerId;
 
     if (!containerId || containerId === ALL_CONTAINERS) {
       setLogLines([]);
@@ -120,10 +125,13 @@ const ViewLogsModal = ({
       }
       if (following) params.append('follow', 'true');
 
-      const response = await API.get(`/api/deployments/${deployment.id}/logs?${params}`);
+      const response = await API.get(
+        `/api/deployments/${deployment.id}/logs?${params}`,
+      );
 
       if (response.data.success) {
-        const rawContent = typeof response.data.data === 'string' ? response.data.data : '';
+        const rawContent =
+          typeof response.data.data === 'string' ? response.data.data : '';
         const normalized = rawContent.replace(/\r\n?/g, '\n');
         const lines = normalized ? normalized.split('\n') : [];
 
@@ -133,7 +141,11 @@ const ViewLogsModal = ({
         setTimeout(scrollToBottom, 100);
       }
     } catch (error) {
-      showError(t('获取日志失败') + ': ' + (error.response?.data?.message || error.message));
+      showError(
+        t('获取日志失败') +
+          ': ' +
+          (error.response?.data?.message || error.message),
+      );
     } finally {
       setLoading(false);
     }
@@ -144,14 +156,19 @@ const ViewLogsModal = ({
 
     setContainersLoading(true);
     try {
-      const response = await API.get(`/api/deployments/${deployment.id}/containers`);
+      const response = await API.get(
+        `/api/deployments/${deployment.id}/containers`,
+      );
 
       if (response.data.success) {
         const list = response.data.data?.containers || [];
         setContainers(list);
 
         setSelectedContainerId((current) => {
-          if (current !== ALL_CONTAINERS && list.some(item => item.container_id === current)) {
+          if (
+            current !== ALL_CONTAINERS &&
+            list.some((item) => item.container_id === current)
+          ) {
             return current;
           }
 
@@ -163,7 +180,11 @@ const ViewLogsModal = ({
         }
       }
     } catch (error) {
-      showError(t('获取容器列表失败') + ': ' + (error.response?.data?.message || error.message));
+      showError(
+        t('获取容器列表失败') +
+          ': ' +
+          (error.response?.data?.message || error.message),
+      );
     } finally {
       setContainersLoading(false);
     }
@@ -177,13 +198,19 @@ const ViewLogsModal = ({
 
     setContainerDetailsLoading(true);
     try {
-      const response = await API.get(`/api/deployments/${deployment.id}/containers/${containerId}`);
+      const response = await API.get(
+        `/api/deployments/${deployment.id}/containers/${containerId}`,
+      );
 
       if (response.data.success) {
         setContainerDetails(response.data.data || null);
       }
     } catch (error) {
-      showError(t('获取容器详情失败') + ': ' + (error.response?.data?.message || error.message));
+      showError(
+        t('获取容器详情失败') +
+          ': ' +
+          (error.response?.data?.message || error.message),
+      );
     } finally {
       setContainerDetailsLoading(false);
     }
@@ -205,13 +232,14 @@ const ViewLogsModal = ({
   const renderContainerStatusTag = (status) => {
     if (!status) {
       return (
-        <Tag color="grey" size="small">
+        <Tag color='grey' size='small'>
           {t('未知状态')}
         </Tag>
       );
     }
 
-    const normalized = typeof status === 'string' ? status.trim().toLowerCase() : '';
+    const normalized =
+      typeof status === 'string' ? status.trim().toLowerCase() : '';
     const statusMap = {
       running: { color: 'green', label: '运行中' },
       pending: { color: 'orange', label: '准备中' },
@@ -225,15 +253,16 @@ const ViewLogsModal = ({
     const config = statusMap[normalized] || { color: 'grey', label: status };
 
     return (
-      <Tag color={config.color} size="small">
+      <Tag color={config.color} size='small'>
         {t(config.label)}
       </Tag>
     );
   };
 
-  const currentContainer = selectedContainerId !== ALL_CONTAINERS
-    ? containers.find((ctr) => ctr.container_id === selectedContainerId)
-    : null;
+  const currentContainer =
+    selectedContainerId !== ALL_CONTAINERS
+      ? containers.find((ctr) => ctr.container_id === selectedContainerId)
+      : null;
 
   const refreshLogs = () => {
     if (selectedContainerId && selectedContainerId !== ALL_CONTAINERS) {
@@ -254,9 +283,10 @@ const ViewLogsModal = ({
     const url = URL.createObjectURL(blob);
     const a = document.createElement('a');
     a.href = url;
-    const safeContainerId = selectedContainerId && selectedContainerId !== ALL_CONTAINERS
-      ? selectedContainerId.replace(/[^a-zA-Z0-9_-]/g, '-')
-      : '';
+    const safeContainerId =
+      selectedContainerId && selectedContainerId !== ALL_CONTAINERS
+        ? selectedContainerId.replace(/[^a-zA-Z0-9_-]/g, '-')
+        : '';
     const fileName = safeContainerId
       ? `deployment-${deployment.id}-container-${safeContainerId}-logs.txt`
       : `deployment-${deployment.id}-logs.txt`;
@@ -265,7 +295,7 @@ const ViewLogsModal = ({
     a.click();
     document.body.removeChild(a);
     URL.revokeObjectURL(url);
-    
+
     showSuccess(t('日志已下载'));
   };
 
@@ -346,14 +376,15 @@ const ViewLogsModal = ({
   // Filter logs based on search term
   const filteredLogs = logLines
     .map((line) => line ?? '')
-    .filter((line) =>
-      !searchTerm || line.toLowerCase().includes(searchTerm.toLowerCase()),
+    .filter(
+      (line) =>
+        !searchTerm || line.toLowerCase().includes(searchTerm.toLowerCase()),
     );
 
   const renderLogEntry = (line, index) => (
     <div
       key={`${index}-${line.slice(0, 20)}`}
-      className="py-1 px-3 hover:bg-gray-50 font-mono text-sm border-b border-gray-100 whitespace-pre-wrap break-words"
+      className='py-1 px-3 hover:bg-gray-50 font-mono text-sm border-b border-gray-100 whitespace-pre-wrap break-words'
     >
       {line}
     </div>
@@ -362,10 +393,10 @@ const ViewLogsModal = ({
   return (
     <Modal
       title={
-        <div className="flex items-center gap-2">
-          <FaTerminal className="text-blue-500" />
+        <div className='flex items-center gap-2'>
+          <FaTerminal className='text-blue-500' />
           <span>{t('容器日志')}</span>
-          <Text type="secondary" size="small">
+          <Text type='secondary' size='small'>
             - {deployment?.container_name || deployment?.id}
           </Text>
         </div>
@@ -375,13 +406,13 @@ const ViewLogsModal = ({
       footer={null}
       width={1000}
       height={700}
-      className="logs-modal"
+      className='logs-modal'
       style={{ top: 20 }}
     >
-      <div className="flex flex-col h-full max-h-[600px]">
+      <div className='flex flex-col h-full max-h-[600px]'>
         {/* Controls */}
-        <Card className="mb-4 border-0 shadow-sm">
-          <div className="flex items-center justify-between flex-wrap gap-3">
+        <Card className='mb-4 border-0 shadow-sm'>
+          <div className='flex items-center justify-between flex-wrap gap-3'>
             <Space wrap>
               <Select
                 prefix={<FaServer />}
@@ -389,7 +420,7 @@ const ViewLogsModal = ({
                 value={selectedContainerId}
                 onChange={handleContainerChange}
                 style={{ width: 240 }}
-                size="small"
+                size='small'
                 loading={containersLoading}
                 dropdownStyle={{ maxHeight: 320, overflowY: 'auto' }}
               >
@@ -397,10 +428,15 @@ const ViewLogsModal = ({
                   {t('全部容器')}
                 </Select.Option>
                 {containers.map((ctr) => (
-                  <Select.Option key={ctr.container_id} value={ctr.container_id}>
-                    <div className="flex flex-col">
-                      <span className="font-mono text-xs">{ctr.container_id}</span>
-                      <span className="text-xs text-gray-500">
+                  <Select.Option
+                    key={ctr.container_id}
+                    value={ctr.container_id}
+                  >
+                    <div className='flex flex-col'>
+                      <span className='font-mono text-xs'>
+                        {ctr.container_id}
+                      </span>
+                      <span className='text-xs text-gray-500'>
                         {ctr.brand_name || 'IO.NET'}
                         {ctr.hardware ? ` · ${ctr.hardware}` : ''}
                       </span>
@@ -415,114 +451,118 @@ const ViewLogsModal = ({
                 value={searchTerm}
                 onChange={setSearchTerm}
                 style={{ width: 200 }}
-                size="small"
+                size='small'
               />
-              
-              <Space align="center" className="ml-2">
-                <Text size="small" type="secondary">
+
+              <Space align='center' className='ml-2'>
+                <Text size='small' type='secondary'>
                   {t('日志流')}
                 </Text>
                 <Radio.Group
-                  type="button"
-                  size="small"
+                  type='button'
+                  size='small'
                   value={streamFilter}
                   onChange={handleStreamChange}
                 >
-                  <Radio value="stdout">STDOUT</Radio>
-                  <Radio value="stderr">STDERR</Radio>
+                  <Radio value='stdout'>STDOUT</Radio>
+                  <Radio value='stderr'>STDERR</Radio>
                 </Radio.Group>
               </Space>
 
-              <div className="flex items-center gap-2">
+              <div className='flex items-center gap-2'>
                 <Switch
                   checked={autoRefresh}
                   onChange={setAutoRefresh}
-                  size="small"
+                  size='small'
                 />
-                <Text size="small">{t('自动刷新')}</Text>
+                <Text size='small'>{t('自动刷新')}</Text>
               </div>
 
-              <div className="flex items-center gap-2">
+              <div className='flex items-center gap-2'>
                 <Switch
                   checked={following}
                   onChange={setFollowing}
-                  size="small"
+                  size='small'
                 />
-                <Text size="small">{t('跟随日志')}</Text>
+                <Text size='small'>{t('跟随日志')}</Text>
               </div>
             </Space>
 
             <Space>
               <Tooltip content={t('刷新日志')}>
-                <Button 
-                  icon={<IconRefresh />} 
+                <Button
+                  icon={<IconRefresh />}
                   onClick={refreshLogs}
                   loading={loading}
-                  size="small"
-                  theme="borderless"
+                  size='small'
+                  theme='borderless'
                 />
               </Tooltip>
-              
+
               <Tooltip content={t('复制日志')}>
-                <Button 
-                  icon={<FaCopy />} 
+                <Button
+                  icon={<FaCopy />}
                   onClick={copyAllLogs}
-                  size="small"
-                  theme="borderless"
+                  size='small'
+                  theme='borderless'
                   disabled={logLines.length === 0}
                 />
               </Tooltip>
-              
+
               <Tooltip content={t('下载日志')}>
-                <Button 
-                  icon={<IconDownload />} 
+                <Button
+                  icon={<IconDownload />}
                   onClick={downloadLogs}
-                  size="small"
-                  theme="borderless"
+                  size='small'
+                  theme='borderless'
                   disabled={logLines.length === 0}
                 />
               </Tooltip>
             </Space>
           </div>
-          
+
           {/* Status Info */}
-          <Divider margin="12px" />
-          <div className="flex items-center justify-between">
-            <Space size="large">
-              <Text size="small" type="secondary">
+          <Divider margin='12px' />
+          <div className='flex items-center justify-between'>
+            <Space size='large'>
+              <Text size='small' type='secondary'>
                 {t('共 {{count}} 条日志', { count: logLines.length })}
               </Text>
               {searchTerm && (
-                <Text size="small" type="secondary">
-                  {t('(筛选后显示 {{count}} 条)', { count: filteredLogs.length })}
+                <Text size='small' type='secondary'>
+                  {t('(筛选后显示 {{count}} 条)', {
+                    count: filteredLogs.length,
+                  })}
                 </Text>
               )}
               {autoRefresh && (
-                <Tag color="green" size="small">
-                  <FaClock className="mr-1" />
+                <Tag color='green' size='small'>
+                  <FaClock className='mr-1' />
                   {t('自动刷新中')}
                 </Tag>
               )}
             </Space>
-            
-            <Text size="small" type="secondary">
+
+            <Text size='small' type='secondary'>
               {t('状态')}: {deployment?.status || 'unknown'}
             </Text>
           </div>
 
           {selectedContainerId !== ALL_CONTAINERS && (
             <>
-              <Divider margin="12px" />
-              <div className="flex flex-col gap-3">
-                <div className="flex items-center justify-between flex-wrap gap-2">
+              <Divider margin='12px' />
+              <div className='flex flex-col gap-3'>
+                <div className='flex items-center justify-between flex-wrap gap-2'>
                   <Space>
-                    <Tag color="blue" size="small">
+                    <Tag color='blue' size='small'>
                       {t('容器')}
                     </Tag>
-                    <Text className="font-mono text-xs">
+                    <Text className='font-mono text-xs'>
                       {selectedContainerId}
                     </Text>
-                    {renderContainerStatusTag(containerDetails?.status || currentContainer?.status)}
+                    {renderContainerStatusTag(
+                      containerDetails?.status || currentContainer?.status,
+                    )}
                   </Space>
 
                   <Space>
@@ -530,9 +570,11 @@ const ViewLogsModal = ({
                       <Tooltip content={containerDetails.public_url}>
                         <Button
                           icon={<FaLink />}
-                          size="small"
-                          theme="borderless"
-                          onClick={() => window.open(containerDetails.public_url, '_blank')}
+                          size='small'
+                          theme='borderless'
+                          onClick={() =>
+                            window.open(containerDetails.public_url, '_blank')
+                          }
                         />
                       </Tooltip>
                     )}
@@ -540,8 +582,8 @@ const ViewLogsModal = ({
                       <Button
                         icon={<IconRefresh />}
                         onClick={refreshContainerDetails}
-                        size="small"
-                        theme="borderless"
+                        size='small'
+                        theme='borderless'
                         loading={containerDetailsLoading}
                       />
                     </Tooltip>
@@ -549,27 +591,36 @@ const ViewLogsModal = ({
                 </div>
 
                 {containerDetailsLoading ? (
-                  <div className="flex items-center justify-center py-6">
+                  <div className='flex items-center justify-center py-6'>
                     <Spin tip={t('加载容器详情中...')} />
                   </div>
                 ) : containerDetails ? (
-                  <div className="grid gap-4 md:grid-cols-2 text-sm">
-                    <div className="flex items-center gap-2">
-                      <FaInfoCircle className="text-blue-500" />
-                      <Text type="secondary">{t('硬件')}</Text>
+                  <div className='grid gap-4 md:grid-cols-2 text-sm'>
+                    <div className='flex items-center gap-2'>
+                      <FaInfoCircle className='text-blue-500' />
+                      <Text type='secondary'>{t('硬件')}</Text>
                       <Text>
-                        {containerDetails?.brand_name || currentContainer?.brand_name || t('未知品牌')}
-                        {(containerDetails?.hardware || currentContainer?.hardware) ? ` · ${containerDetails?.hardware || currentContainer?.hardware}` : ''}
+                        {containerDetails?.brand_name ||
+                          currentContainer?.brand_name ||
+                          t('未知品牌')}
+                        {containerDetails?.hardware ||
+                        currentContainer?.hardware
+                          ? ` · ${containerDetails?.hardware || currentContainer?.hardware}`
+                          : ''}
                       </Text>
                     </div>
-                    <div className="flex items-center gap-2">
-                      <FaServer className="text-purple-500" />
-                      <Text type="secondary">{t('GPU/容器')}</Text>
-                      <Text>{containerDetails?.gpus_per_container ?? currentContainer?.gpus_per_container ?? 0}</Text>
+                    <div className='flex items-center gap-2'>
+                      <FaServer className='text-purple-500' />
+                      <Text type='secondary'>{t('GPU/容器')}</Text>
+                      <Text>
+                        {containerDetails?.gpus_per_container ??
+                          currentContainer?.gpus_per_container ??
+                          0}
+                      </Text>
                     </div>
-                    <div className="flex items-center gap-2">
-                      <FaClock className="text-orange-500" />
-                      <Text type="secondary">{t('创建时间')}</Text>
+                    <div className='flex items-center gap-2'>
+                      <FaClock className='text-orange-500' />
+                      <Text type='secondary'>{t('创建时间')}</Text>
                       <Text>
                         {containerDetails?.created_at
                           ? timestamp2string(containerDetails.created_at)
@@ -578,51 +629,64 @@ const ViewLogsModal = ({
                             : t('未知')}
                       </Text>
                     </div>
-                    <div className="flex items-center gap-2">
-                      <FaInfoCircle className="text-green-500" />
-                      <Text type="secondary">{t('运行时长')}</Text>
-                      <Text>{containerDetails?.uptime_percent ?? currentContainer?.uptime_percent ?? 0}%</Text>
+                    <div className='flex items-center gap-2'>
+                      <FaInfoCircle className='text-green-500' />
+                      <Text type='secondary'>{t('运行时长')}</Text>
+                      <Text>
+                        {containerDetails?.uptime_percent ??
+                          currentContainer?.uptime_percent ??
+                          0}
+                        %
+                      </Text>
                     </div>
                   </div>
                 ) : (
-                  <Text size="small" type="secondary">
+                  <Text size='small' type='secondary'>
                     {t('暂无容器详情')}
                   </Text>
                 )}
 
-                {containerDetails?.events && containerDetails.events.length > 0 && (
-                  <div className="bg-gray-50 rounded-lg p-3">
-                    <Text size="small" type="secondary">
-                      {t('最近事件')}
-                    </Text>
-                    <div className="mt-2 space-y-2 max-h-32 overflow-y-auto">
-                      {containerDetails.events.slice(0, 5).map((event, index) => (
-                        <div key={`${event.time}-${index}`} className="flex gap-3 text-xs font-mono">
-                          <span className="text-gray-500">
-                            {event.time ? timestamp2string(event.time) : '--'}
-                          </span>
-                          <span className="text-gray-700 break-all flex-1">
-                            {event.message}
-                          </span>
-                        </div>
-                      ))}
+                {containerDetails?.events &&
+                  containerDetails.events.length > 0 && (
+                    <div className='bg-gray-50 rounded-lg p-3'>
+                      <Text size='small' type='secondary'>
+                        {t('最近事件')}
+                      </Text>
+                      <div className='mt-2 space-y-2 max-h-32 overflow-y-auto'>
+                        {containerDetails.events
+                          .slice(0, 5)
+                          .map((event, index) => (
+                            <div
+                              key={`${event.time}-${index}`}
+                              className='flex gap-3 text-xs font-mono'
+                            >
+                              <span className='text-gray-500'>
+                                {event.time
+                                  ? timestamp2string(event.time)
+                                  : '--'}
+                              </span>
+                              <span className='text-gray-700 break-all flex-1'>
+                                {event.message}
+                              </span>
+                            </div>
+                          ))}
+                      </div>
                     </div>
-                  </div>
-                )}
+                  )}
               </div>
             </>
           )}
         </Card>
 
         {/* Log Content */}
-        <div className="flex-1 flex flex-col border rounded-lg bg-gray-50 overflow-hidden">
-          <div 
+        <div className='flex-1 flex flex-col border rounded-lg bg-gray-50 overflow-hidden'>
+          <div
             ref={logContainerRef}
-            className="flex-1 overflow-y-auto bg-white"
+            className='flex-1 overflow-y-auto bg-white'
             style={{ maxHeight: '400px' }}
           >
             {loading && logLines.length === 0 ? (
-              <div className="flex items-center justify-center p-8">
+              <div className='flex items-center justify-center p-8'>
                 <Spin tip={t('加载日志中...')} />
               </div>
             ) : filteredLogs.length === 0 ? (
@@ -639,15 +703,14 @@ const ViewLogsModal = ({
               </div>
             )}
           </div>
-          
+
           {/* Footer status */}
           {logLines.length > 0 && (
-            <div className="flex items-center justify-between px-3 py-2 bg-gray-50 border-t text-xs text-gray-500">
-              <span>
-                {following ? t('正在跟随最新日志') : t('日志已加载')}
-              </span>
+            <div className='flex items-center justify-between px-3 py-2 bg-gray-50 border-t text-xs text-gray-500'>
+              <span>{following ? t('正在跟随最新日志') : t('日志已加载')}</span>
               <span>
-                {t('最后更新')}: {lastUpdatedAt ? lastUpdatedAt.toLocaleTimeString() : '--'}
+                {t('最后更新')}:{' '}
+                {lastUpdatedAt ? lastUpdatedAt.toLocaleTimeString() : '--'}
               </span>
             </div>
           )}

+ 53 - 34
web/src/hooks/common/useSidebar.js

@@ -25,6 +25,56 @@ import { API } from '../../helpers';
 const sidebarEventTarget = new EventTarget();
 const SIDEBAR_REFRESH_EVENT = 'sidebar-refresh';
 
+export const DEFAULT_ADMIN_CONFIG = {
+  chat: {
+    enabled: true,
+    playground: true,
+    chat: true,
+  },
+  console: {
+    enabled: true,
+    detail: true,
+    token: true,
+    log: true,
+    midjourney: true,
+    task: true,
+  },
+  personal: {
+    enabled: true,
+    topup: true,
+    personal: true,
+  },
+  admin: {
+    enabled: true,
+    channel: true,
+    models: true,
+    deployment: true,
+    redemption: true,
+    user: true,
+    setting: true,
+  },
+};
+
+const deepClone = (value) => JSON.parse(JSON.stringify(value));
+
+export const mergeAdminConfig = (savedConfig) => {
+  const merged = deepClone(DEFAULT_ADMIN_CONFIG);
+  if (!savedConfig || typeof savedConfig !== 'object') return merged;
+
+  for (const [sectionKey, sectionConfig] of Object.entries(savedConfig)) {
+    if (!sectionConfig || typeof sectionConfig !== 'object') continue;
+
+    if (!merged[sectionKey]) {
+      merged[sectionKey] = { ...sectionConfig };
+      continue;
+    }
+
+    merged[sectionKey] = { ...merged[sectionKey], ...sectionConfig };
+  }
+
+  return merged;
+};
+
 export const useSidebar = () => {
   const [statusState] = useContext(StatusContext);
   const [userConfig, setUserConfig] = useState(null);
@@ -37,48 +87,17 @@ export const useSidebar = () => {
     instanceIdRef.current = `sidebar-${Date.now()}-${randomPart}`;
   }
 
-  // 默认配置
-  const defaultAdminConfig = {
-    chat: {
-      enabled: true,
-      playground: true,
-      chat: true,
-    },
-    console: {
-      enabled: true,
-      detail: true,
-      token: true,
-      log: true,
-      midjourney: true,
-      task: true,
-    },
-    personal: {
-      enabled: true,
-      topup: true,
-      personal: true,
-    },
-    admin: {
-      enabled: true,
-      channel: true,
-      models: true,
-      deployment: true,
-      redemption: true,
-      user: true,
-      setting: true,
-    },
-  };
-
   // 获取管理员配置
   const adminConfig = useMemo(() => {
     if (statusState?.status?.SidebarModulesAdmin) {
       try {
         const config = JSON.parse(statusState.status.SidebarModulesAdmin);
-        return config;
+        return mergeAdminConfig(config);
       } catch (error) {
-        return defaultAdminConfig;
+        return mergeAdminConfig(null);
       }
     }
-    return defaultAdminConfig;
+    return mergeAdminConfig(null);
   }, [statusState?.status?.SidebarModulesAdmin]);
 
   // 加载用户配置的通用方法

+ 99 - 53
web/src/hooks/model-deployments/useDeploymentResources.js

@@ -39,10 +39,13 @@ export const useDeploymentResources = () => {
       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 { 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 availableCount = Number.isNaN(availableCountValue)
+            ? 0
+            : availableCountValue;
           const availableBool =
             typeof hardware.available === 'boolean'
               ? hardware.available
@@ -57,7 +60,9 @@ export const useDeploymentResources = () => {
 
         const providedTotal = Number(total_available);
         const fallbackTotal = normalizedHardware.reduce(
-          (acc, item) => acc + (Number.isNaN(item.available_count) ? 0 : item.available_count),
+          (acc, item) =>
+            acc +
+            (Number.isNaN(item.available_count) ? 0 : item.available_count),
           0,
         );
         const hasProvidedTotal =
@@ -85,37 +90,64 @@ export const useDeploymentResources = () => {
     }
   }, []);
 
-  const fetchLocations = useCallback(async () => {
+  const fetchLocations = useCallback(async (hardwareId, gpuCount = 1) => {
+    if (!hardwareId) {
+      setLocations([]);
+      setLocationsTotalAvailable(0);
+      return [];
+    }
+
     try {
       setLoadingLocations(true);
-      const response = await API.get('/api/deployments/locations');
+      const response = await API.get(
+        `/api/deployments/available-replicas?hardware_id=${hardwareId}&gpu_count=${gpuCount}`,
+      );
       if (response.data.success) {
-        const { locations: locationsList = [], total } = response.data.data || {};
-        const normalizedLocations = locationsList.map((location) => {
-          const iso2 = (location.iso2 || '').toString().toUpperCase();
-          const availableValue = Number(location.available);
-          const available = Number.isNaN(availableValue) ? 0 : availableValue;
+        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;
 
-          return {
-            ...location,
+          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,
-            available,
-          };
+            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 providedTotal = Number(total);
-        const fallbackTotal = normalizedLocations.reduce(
-          (acc, item) => acc + (Number.isNaN(item.available) ? 0 : item.available),
-          0,
-        );
-        const hasProvidedTotal =
-          total !== undefined &&
-          total !== null &&
-          total !== '' &&
-          !Number.isNaN(providedTotal);
 
+        const normalizedLocations = Array.from(nextLocationsMap.values());
         setLocations(normalizedLocations);
         setLocationsTotalAvailable(
-          hasProvidedTotal ? providedTotal : fallbackTotal,
+          normalizedLocations.reduce(
+            (acc, item) => acc + (item.available || 0),
+            0,
+          ),
         );
         return normalizedLocations;
       } else {
@@ -132,34 +164,37 @@ export const useDeploymentResources = () => {
     }
   }, []);
 
-  const fetchAvailableReplicas = useCallback(async (hardwareId, gpuCount = 1) => {
-    if (!hardwareId) {
-      setAvailableReplicas([]);
-      return [];
-    }
+  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);
+      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);
       }
-    } catch (error) {
-      console.error('Load available replicas error:', error);
-      setAvailableReplicas([]);
-      return [];
-    } finally {
-      setLoadingReplicas(false);
-    }
-  }, []);
+    },
+    [],
+  );
 
   const calculatePrice = useCallback(async (params) => {
     const {
@@ -167,10 +202,16 @@ export const useDeploymentResources = () => {
       hardwareId,
       gpusPerContainer,
       durationHours,
-      replicaCount
+      replicaCount,
     } = params;
 
-    if (!locationIds?.length || !hardwareId || !gpusPerContainer || !durationHours || !replicaCount) {
+    if (
+      !locationIds?.length ||
+      !hardwareId ||
+      !gpusPerContainer ||
+      !durationHours ||
+      !replicaCount
+    ) {
       setPriceEstimation(null);
       return null;
     }
@@ -185,7 +226,10 @@ export const useDeploymentResources = () => {
         replica_count: replicaCount,
       };
 
-      const response = await API.post('/api/deployments/price-estimation', requestData);
+      const response = await API.post(
+        '/api/deployments/price-estimation',
+        requestData,
+      );
       if (response.data.success) {
         const estimation = response.data.data;
         setPriceEstimation(estimation);
@@ -208,7 +252,9 @@ export const useDeploymentResources = () => {
     if (!name?.trim()) return false;
 
     try {
-      const response = await API.get(`/api/deployments/check-name?name=${encodeURIComponent(name.trim())}`);
+      const response = await API.get(
+        `/api/deployments/check-name?name=${encodeURIComponent(name.trim())}`,
+      );
       if (response.data.success) {
         return response.data.data.available;
       } else {

+ 110 - 95
web/src/hooks/model-deployments/useDeploymentsData.jsx

@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
 For commercial licensing, please contact [email protected]
 */
 
-import { useState, useEffect, useMemo } from 'react';
+import { useState, useEffect, useMemo, useRef } from 'react';
 import { useTranslation } from 'react-i18next';
 import { API, showError, showSuccess } from '../../helpers';
 import { ITEMS_PER_PAGE } from '../../constants';
@@ -26,6 +26,7 @@ import { useTableCompactMode } from '../common/useTableCompactMode';
 export const useDeploymentsData = () => {
   const { t } = useTranslation();
   const [compactMode, setCompactMode] = useTableCompactMode('deployments');
+  const requestSeq = useRef(0);
 
   // State management
   const [deployments, setDeployments] = useState([]);
@@ -34,6 +35,7 @@ export const useDeploymentsData = () => {
   const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
   const [searching, setSearching] = useState(false);
   const [deploymentCount, setDeploymentCount] = useState(0);
+  const [query, setQuery] = useState({ keyword: '', status: '' });
 
   // Modal states
   const [showEdit, setShowEdit] = useState(false);
@@ -80,18 +82,12 @@ export const useDeploymentsData = () => {
     }, 500);
   };
 
-  // Set deployment format with key field
-  const setDeploymentFormat = (deployments) => {
-    for (let i = 0; i < deployments.length; i++) {
-      deployments[i].key = deployments[i].id;
-    }
-    setDeployments(deployments);
+  const normalizeQuery = (terms) => {
+    const keyword = (terms?.searchKeyword ?? '').trim();
+    const status = (terms?.searchStatus ?? '').trim();
+    return { keyword, status };
   };
 
-  // Status tabs
-  const [activeStatusKey, setActiveStatusKey] = useState('all');
-  const [statusCounts, setStatusCounts] = useState({});
-
   // Column visibility
   const COLUMN_KEYS = useMemo(
     () => ({
@@ -160,114 +156,127 @@ export const useDeploymentsData = () => {
   // Save column visibility to localStorage
   const saveColumnVisibility = (newVisibleColumns) => {
     const normalized = ensureRequiredColumns(newVisibleColumns);
-    localStorage.setItem('deployments_visible_columns', JSON.stringify(normalized));
+    localStorage.setItem(
+      'deployments_visible_columns',
+      JSON.stringify(normalized),
+    );
     setVisibleColumnsState(normalized);
   };
 
-  // Load deployments data
-  const loadDeployments = async (
-    page = 1,
-    size = pageSize,
-    statusKey = activeStatusKey,
-  ) => {
-    setLoading(true);
-    try {
-      let url = `/api/deployments/?p=${page}&page_size=${size}`;
-      if (statusKey && statusKey !== 'all') {
-        url = `/api/deployments/search?status=${statusKey}&p=${page}&page_size=${size}`;
-      }
+  const applyDeploymentsData = ({ data, page }) => {
+    const items = extractItems(data);
+    setActivePage(data?.page ?? page);
+    setDeploymentCount(data?.total ?? items.length);
+    setSelectedKeys([]);
+    setDeployments(
+      items.map((deployment) => ({ ...deployment, key: deployment.id })),
+    );
+  };
 
-      const res = await API.get(url);
-      const { success, message, data } = res.data;
-      if (success) {
-        const newPageData = extractItems(data);
-        setActivePage(data.page || page);
-        setDeploymentCount(data.total || newPageData.length);
-        setDeploymentFormat(newPageData);
-
-        if (data.status_counts) {
-          const sumAll = Object.values(data.status_counts).reduce(
-            (acc, v) => acc + v,
-            0,
-          );
-          setStatusCounts({ ...data.status_counts, all: sumAll });
-        }
-      } else {
-        showError(message);
-        setDeployments([]);
-      }
-    } catch (error) {
-      console.error(error);
-      showError(t('获取部署列表失败'));
-      setDeployments([]);
+  const fetchDeployments = async ({ page, size, keyword, status }) => {
+    const seq = ++requestSeq.current;
+    const isSearchMode = Boolean(keyword) || Boolean(status);
+
+    if (isSearchMode) {
+      setSearching(true);
+    } else {
+      setLoading(true);
     }
-    setLoading(false);
-  };
 
-  // Search deployments
-  const searchDeployments = async (searchTerms) => {
-    setSearching(true);
     try {
-      const { searchKeyword, searchStatus } = searchTerms;
-      const params = new URLSearchParams({
-        p: '1',
-        page_size: pageSize.toString(),
-      });
+      let url;
+      if (isSearchMode) {
+        const params = new URLSearchParams({
+          p: String(page),
+          page_size: String(size),
+        });
 
-      if (searchKeyword?.trim()) {
-        params.append('keyword', searchKeyword.trim());
-      }
-      if (searchStatus && searchStatus !== 'all') {
-        params.append('status', searchStatus);
+        if (keyword) params.append('keyword', keyword);
+        if (status) params.append('status', status);
+
+        url = `/api/deployments/search?${params.toString()}`;
+      } else {
+        url = `/api/deployments/?p=${page}&page_size=${size}`;
       }
 
-      const res = await API.get(`/api/deployments/search?${params}`);
+      const res = await API.get(url);
+      if (seq !== requestSeq.current) return;
+
       const { success, message, data } = res.data;
-      
-      if (success) {
-        const items = extractItems(data);
-        setActivePage(1);
-        setDeploymentCount(data.total || items.length);
-        setDeploymentFormat(items);
-      } else {
+      if (!success) {
         showError(message);
         setDeployments([]);
+        setDeploymentCount(0);
+        return;
       }
+
+      applyDeploymentsData({ data, page });
     } catch (error) {
-      console.error('Search error:', error);
-      showError(t('搜索失败'));
+      if (seq !== requestSeq.current) return;
+      console.error(error);
+      showError(isSearchMode ? t('搜索失败') : t('获取部署列表失败'));
       setDeployments([]);
+      setDeploymentCount(0);
+    } finally {
+      if (seq !== requestSeq.current) return;
+      setLoading(false);
+      setSearching(false);
     }
-    setSearching(false);
   };
 
   // Refresh data
   const refresh = async (page = activePage) => {
-    await loadDeployments(page, pageSize);
+    await fetchDeployments({
+      page,
+      size: pageSize,
+      keyword: query.keyword,
+      status: query.status,
+    });
   };
 
   // Handle page change
   const handlePageChange = (page) => {
     setActivePage(page);
-    if (!searching) {
-      loadDeployments(page, pageSize);
-    }
+    fetchDeployments({
+      page,
+      size: pageSize,
+      keyword: query.keyword,
+      status: query.status,
+    });
   };
 
   // Handle page size change
   const handlePageSizeChange = (size) => {
     setPageSize(size);
     setActivePage(1);
-    if (!searching) {
-      loadDeployments(1, size);
-    }
+    fetchDeployments({
+      page: 1,
+      size,
+      keyword: query.keyword,
+      status: query.status,
+    });
+  };
+
+  const loadDeployments = async (page = 1, size = pageSize) => {
+    await fetchDeployments({
+      page,
+      size,
+      keyword: query.keyword,
+      status: query.status,
+    });
   };
 
-  // Handle tab change
-  const handleTabChange = (statusKey) => {
-    setActiveStatusKey(statusKey);
+  // Search deployments (also supports pagination)
+  const searchDeployments = async (searchTerms) => {
+    const nextQuery = normalizeQuery(searchTerms);
+    setQuery(nextQuery);
     setActivePage(1);
-    loadDeployments(1, pageSize, statusKey);
+    await fetchDeployments({
+      page: 1,
+      size: pageSize,
+      keyword: nextQuery.keyword,
+      status: nextQuery.status,
+    });
   };
 
   // Deployment operations
@@ -323,7 +332,9 @@ export const useDeploymentsData = () => {
     }
 
     try {
-      const containersResp = await API.get(`/api/deployments/${deployment.id}/containers`);
+      const containersResp = await API.get(
+        `/api/deployments/${deployment.id}/containers`,
+      );
       if (!containersResp.data?.success) {
         showError(containersResp.data?.message || t('获取容器信息失败'));
         return;
@@ -344,15 +355,20 @@ export const useDeploymentsData = () => {
         return;
       }
 
-      const baseName = deployment.container_name || deployment.deployment_name || deployment.name || deployment.id;
+      const baseName =
+        deployment.container_name ||
+        deployment.deployment_name ||
+        deployment.name ||
+        deployment.id;
       const safeName = String(baseName || 'ionet').slice(0, 60);
       const channelName = `[IO.NET] ${safeName}`;
 
       let randomKey;
       try {
-        randomKey = (typeof crypto !== 'undefined' && crypto.randomUUID)
-          ? `ionet-${crypto.randomUUID().replace(/-/g, '')}`
-          : null;
+        randomKey =
+          typeof crypto !== 'undefined' && crypto.randomUUID
+            ? `ionet-${crypto.randomUUID().replace(/-/g, '')}`
+            : null;
       } catch (err) {
         randomKey = null;
       }
@@ -396,7 +412,9 @@ export const useDeploymentsData = () => {
 
   const updateDeploymentName = async (deploymentId, newName) => {
     try {
-      const res = await API.put(`/api/deployments/${deploymentId}/name`, { name: newName });
+      const res = await API.put(`/api/deployments/${deploymentId}/name`, {
+        name: newName,
+      });
       if (res.data.success) {
         showSuccess(t('部署名称更新成功'));
         await refresh();
@@ -415,9 +433,9 @@ export const useDeploymentsData = () => {
   // Batch operations
   const batchDeleteDeployments = async () => {
     if (selectedKeys.length === 0) return;
-    
+
     try {
-      const ids = selectedKeys.map(deployment => deployment.id);
+      const ids = selectedKeys.map((deployment) => deployment.id);
       const res = await API.post('/api/deployments/batch_delete', { ids });
       if (res.data.success) {
         showSuccess(t('批量删除成功'));
@@ -452,8 +470,6 @@ export const useDeploymentsData = () => {
     activePage,
     pageSize,
     deploymentCount,
-    statusCounts,
-    activeStatusKey,
     compactMode,
     setCompactMode,
 
@@ -488,7 +504,6 @@ export const useDeploymentsData = () => {
     refresh,
     handlePageChange,
     handlePageSizeChange,
-    handleTabChange,
     handleRow,
 
     // Deployment operations

+ 89 - 52
web/src/hooks/model-deployments/useEnhancedDeploymentActions.jsx

@@ -25,9 +25,9 @@ export const useEnhancedDeploymentActions = (t) => {
 
   // Set loading state for specific operation
   const setOperationLoading = (operation, deploymentId, isLoading) => {
-    setLoading(prev => ({
+    setLoading((prev) => ({
       ...prev,
-      [`${operation}_${deploymentId}`]: isLoading
+      [`${operation}_${deploymentId}`]: isLoading,
     }));
   };
 
@@ -38,20 +38,26 @@ export const useEnhancedDeploymentActions = (t) => {
 
   // Extend deployment duration
   const extendDeployment = async (deploymentId, durationHours) => {
-    const operationKey = `extend_${deploymentId}`;
     try {
       setOperationLoading('extend', deploymentId, true);
-      
-      const response = await API.post(`/api/deployments/${deploymentId}/extend`, {
-        duration_hours: durationHours
-      });
+
+      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));
+      showError(
+        t('延长时长失败') +
+          ': ' +
+          (error.response?.data?.message || error.message),
+      );
       throw error;
     } finally {
       setOperationLoading('extend', deploymentId, false);
@@ -62,14 +68,18 @@ export const useEnhancedDeploymentActions = (t) => {
   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));
+      showError(
+        t('获取详情失败') +
+          ': ' +
+          (error.response?.data?.message || error.message),
+      );
       throw error;
     } finally {
       setOperationLoading('details', deploymentId, false);
@@ -80,24 +90,31 @@ export const useEnhancedDeploymentActions = (t) => {
   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.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}`);
-      
+
+      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));
+      showError(
+        t('获取日志失败') +
+          ': ' +
+          (error.response?.data?.message || error.message),
+      );
       throw error;
     } finally {
       setOperationLoading('logs', deploymentId, false);
@@ -108,15 +125,22 @@ export const useEnhancedDeploymentActions = (t) => {
   const updateDeploymentConfig = async (deploymentId, config) => {
     try {
       setOperationLoading('config', deploymentId, true);
-      
-      const response = await API.put(`/api/deployments/${deploymentId}`, config);
-      
+
+      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));
+      showError(
+        t('更新配置失败') +
+          ': ' +
+          (error.response?.data?.message || error.message),
+      );
       throw error;
     } finally {
       setOperationLoading('config', deploymentId, false);
@@ -127,15 +151,19 @@ export const useEnhancedDeploymentActions = (t) => {
   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));
+      showError(
+        t('销毁容器失败') +
+          ': ' +
+          (error.response?.data?.message || error.message),
+      );
       throw error;
     } finally {
       setOperationLoading('delete', deploymentId, false);
@@ -146,17 +174,21 @@ export const useEnhancedDeploymentActions = (t) => {
   const updateDeploymentName = async (deploymentId, newName) => {
     try {
       setOperationLoading('rename', deploymentId, true);
-      
+
       const response = await API.put(`/api/deployments/${deploymentId}/name`, {
-        name: newName
+        name: newName,
       });
-      
+
       if (response.data.success) {
         showSuccess(t('容器名称更新成功'));
         return response.data.data;
       }
     } catch (error) {
-      showError(t('更新名称失败') + ': ' + (error.response?.data?.message || error.message));
+      showError(
+        t('更新名称失败') +
+          ': ' +
+          (error.response?.data?.message || error.message),
+      );
       throw error;
     } finally {
       setOperationLoading('rename', deploymentId, false);
@@ -167,21 +199,23 @@ export const useEnhancedDeploymentActions = (t) => {
   const batchDelete = async (deploymentIds) => {
     try {
       setOperationLoading('batch_delete', 'all', true);
-      
+
       const results = await Promise.allSettled(
-        deploymentIds.map(id => deleteDeployment(id))
+        deploymentIds.map((id) => deleteDeployment(id)),
       );
-      
-      const successful = results.filter(r => r.status === 'fulfilled').length;
-      const failed = results.filter(r => r.status === 'rejected').length;
-      
+
+      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 
-        }));
+        showSuccess(
+          t('批量操作完成: {{success}}个成功, {{failed}}个失败', {
+            success: successful,
+            failed: failed,
+          }),
+        );
       }
-      
+
       return { successful, failed };
     } catch (error) {
       showError(t('批量操作失败') + ': ' + error.message);
@@ -195,17 +229,20 @@ export const useEnhancedDeploymentActions = (t) => {
   const exportLogs = async (deploymentId, options = {}) => {
     try {
       setOperationLoading('export_logs', deploymentId, true);
-      
+
       const logs = await getDeploymentLogs(deploymentId, {
         ...options,
-        limit: 10000 // Get more logs for export
+        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 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');
@@ -215,7 +252,7 @@ export const useEnhancedDeploymentActions = (t) => {
         a.click();
         document.body.removeChild(a);
         URL.revokeObjectURL(url);
-        
+
         showSuccess(t('日志导出成功'));
       }
     } catch (error) {
@@ -236,14 +273,14 @@ export const useEnhancedDeploymentActions = (t) => {
     updateDeploymentName,
     batchDelete,
     exportLogs,
-    
+
     // Loading states
     isOperationLoading,
     loading,
-    
+
     // Utility
-    setOperationLoading
+    setOperationLoading,
   };
 };
 
-export default useEnhancedDeploymentActions;
+export default useEnhancedDeploymentActions;

+ 11 - 33
web/src/hooks/model-deployments/useModelDeploymentSettings.js

@@ -18,13 +18,12 @@ For commercial licensing, please contact [email protected]
 */
 
 import { useCallback, useEffect, useState } from 'react';
-import { API, toBoolean } from '../../helpers';
+import { API } from '../../helpers';
 
 export const useModelDeploymentSettings = () => {
   const [loading, setLoading] = useState(true);
   const [settings, setSettings] = useState({
     'model_deployment.ionet.enabled': false,
-    'model_deployment.ionet.api_key': '',
   });
   const [connectionState, setConnectionState] = useState({
     loading: false,
@@ -35,24 +34,13 @@ export const useModelDeploymentSettings = () => {
   const getSettings = async () => {
     try {
       setLoading(true);
-      const res = await API.get('/api/option/');
+      const res = await API.get('/api/deployments/settings');
       const { success, data } = res.data;
-      
+
       if (success) {
-        const newSettings = {
-          'model_deployment.ionet.enabled': false,
-          'model_deployment.ionet.api_key': '',
-        };
-        
-        data.forEach((item) => {
-          if (item.key.endsWith('enabled')) {
-            newSettings[item.key] = toBoolean(item.value);
-          } else if (newSettings.hasOwnProperty(item.key)) {
-            newSettings[item.key] = item.value || '';
-          }
+        setSettings({
+          'model_deployment.ionet.enabled': data?.enabled === true,
         });
-        
-        setSettings(newSettings);
       }
     } catch (error) {
       console.error('Failed to get model deployment settings:', error);
@@ -65,10 +53,7 @@ export const useModelDeploymentSettings = () => {
     getSettings();
   }, []);
 
-  const apiKey = settings['model_deployment.ionet.api_key'];
-  const isIoNetEnabled = settings['model_deployment.ionet.enabled'] && 
-                        apiKey && 
-                        apiKey.trim() !== '';
+  const isIoNetEnabled = settings['model_deployment.ionet.enabled'];
 
   const buildConnectionError = (rawMessage, fallbackMessage = 'Connection failed') => {
     const message = (rawMessage || fallbackMessage).trim();
@@ -85,18 +70,12 @@ export const useModelDeploymentSettings = () => {
     return { type: 'unknown', message };
   };
 
-  const testConnection = useCallback(async (apiKey) => {
-    const key = (apiKey || '').trim();
-    if (key === '') {
-      setConnectionState({ loading: false, ok: null, error: null });
-      return;
-    }
-
+  const testConnection = useCallback(async () => {
     setConnectionState({ loading: true, ok: null, error: null });
     try {
       const response = await API.post(
-        '/api/deployments/test-connection',
-        { api_key: key },
+        '/api/deployments/settings/test-connection',
+        {},
         { skipErrorHandler: true },
       );
 
@@ -123,16 +102,15 @@ export const useModelDeploymentSettings = () => {
 
   useEffect(() => {
     if (!loading && isIoNetEnabled) {
-      testConnection(apiKey);
+      testConnection();
       return;
     }
     setConnectionState({ loading: false, ok: null, error: null });
-  }, [loading, isIoNetEnabled, apiKey, testConnection]);
+  }, [loading, isIoNetEnabled, testConnection]);
 
   return {
     loading,
     settings,
-    apiKey,
     isIoNetEnabled,
     refresh: getSettings,
     connectionLoading: connectionState.loading,

File diff suppressed because it is too large
+ 262 - 10
web/src/i18n/locales/en.json


File diff suppressed because it is too large
+ 263 - 10
web/src/i18n/locales/fr.json


File diff suppressed because it is too large
+ 278 - 9
web/src/i18n/locales/ja.json


File diff suppressed because it is too large
+ 265 - 10
web/src/i18n/locales/ru.json


File diff suppressed because it is too large
+ 279 - 9
web/src/i18n/locales/vi.json


File diff suppressed because it is too large
+ 260 - 10
web/src/i18n/locales/zh.json


+ 1 - 2
web/src/pages/ModelDeployment/index.jsx

@@ -29,7 +29,6 @@ const ModelDeploymentPage = () => {
     connectionLoading,
     connectionOk,
     connectionError,
-    apiKey,
     testConnection,
   } = useModelDeploymentSettings();
 
@@ -40,7 +39,7 @@ const ModelDeploymentPage = () => {
       connectionLoading={connectionLoading}
       connectionOk={connectionOk}
       connectionError={connectionError}
-      onRetry={() => testConnection(apiKey)}
+      onRetry={() => testConnection()}
     >
       <div className='mt-[60px] px-2'>
         <DeploymentsTable />

+ 4 - 18
web/src/pages/Setting/Model/SettingModelDeployment.jsx

@@ -48,10 +48,6 @@ export default function SettingModelDeployment(props) {
 
   const testApiKey = async () => {
     const apiKey = inputs['model_deployment.ionet.api_key'];
-    if (!apiKey || apiKey.trim() === '') {
-      showError(t('请先填写 API Key'));
-      return;
-    }
 
     const getLocalizedMessage = (message) => {
       switch (message) {
@@ -69,10 +65,8 @@ export default function SettingModelDeployment(props) {
     setTesting(true);
     try {
       const response = await API.post(
-        '/api/deployments/test-connection',
-        {
-          api_key: apiKey.trim(),
-        },
+        '/api/deployments/settings/test-connection',
+        apiKey && apiKey.trim() !== '' ? { api_key: apiKey.trim() } : {},
         {
           skipErrorHandler: true,
         },
@@ -108,12 +102,6 @@ export default function SettingModelDeployment(props) {
   };
 
   function onSubmit() {
-    // 前置校验:如果启用了 io.net 但没有填写 API Key
-    if (inputs['model_deployment.ionet.enabled'] && 
-        (!inputs['model_deployment.ionet.api_key'] || inputs['model_deployment.ionet.api_key'].trim() === '')) {
-      return showError(t('启用 io.net 部署时必须填写 API Key'));
-    }
-
     const updateArray = compareObjects(inputs, inputsRow);
     if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
     
@@ -229,7 +217,7 @@ export default function SettingModelDeployment(props) {
                     <Form.Input
                       label={t('API Key')}
                       field={'model_deployment.ionet.api_key'}
-                      placeholder={t('请输入 io.net API Key')}
+                      placeholder={t('请输入 io.net API Key(敏感信息不显示)')}
                       onChange={(value) =>
                         setInputs({
                           ...inputs,
@@ -248,9 +236,7 @@ export default function SettingModelDeployment(props) {
                         onClick={testApiKey}
                         loading={testing}
                         disabled={
-                          !inputs['model_deployment.ionet.enabled'] ||
-                          !inputs['model_deployment.ionet.api_key'] ||
-                          inputs['model_deployment.ionet.api_key'].trim() === ''
+                          !inputs['model_deployment.ionet.enabled']
                         }
                         style={{
                           height: '32px',

+ 36 - 8
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx

@@ -32,7 +32,7 @@ import { API, showSuccess, showError } from '../../../helpers';
 import { StatusContext } from '../../../context/Status';
 import { UserContext } from '../../../context/User';
 import { useUserPermissions } from '../../../hooks/common/useUserPermissions';
-import { useSidebar } from '../../../hooks/common/useSidebar';
+import { mergeAdminConfig, useSidebar } from '../../../hooks/common/useSidebar';
 import { Settings } from 'lucide-react';
 
 const { Text } = Typography;
@@ -198,9 +198,25 @@ export default function SettingsSidebarModulesUser() {
       try {
         // 获取管理员全局配置
         if (statusState?.status?.SidebarModulesAdmin) {
-          const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
-          setAdminConfig(adminConf);
-          console.log('加载管理员边栏配置:', adminConf);
+          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);
         }
 
         // 获取用户个人配置
@@ -323,6 +339,11 @@ export default function SettingsSidebarModulesUser() {
       modules: [
         { key: 'channel', title: t('渠道管理'), description: t('API渠道配置') },
         { key: 'models', title: t('模型管理'), description: t('AI模型配置') },
+        {
+          key: 'deployment',
+          title: t('模型部署'),
+          description: t('模型部署管理'),
+        },
         {
           key: 'redemption',
           title: t('兑换码管理'),
@@ -389,7 +410,7 @@ export default function SettingsSidebarModulesUser() {
               </Text>
             </div>
             <Switch
-              checked={sidebarModulesUser[section.key]?.enabled}
+              checked={sidebarModulesUser[section.key]?.enabled !== false}
               onChange={handleSectionChange(section.key)}
               size='default'
             />
@@ -401,7 +422,9 @@ export default function SettingsSidebarModulesUser() {
               <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 ? '' : 'opacity-50'
+                    sidebarModulesUser[section.key]?.enabled !== false
+                      ? ''
+                      : 'opacity-50'
                   }`}
                   bodyStyle={{ padding: '16px' }}
                   hoverable
@@ -417,10 +440,15 @@ export default function SettingsSidebarModulesUser() {
                     </div>
                     <div className='ml-4'>
                       <Switch
-                        checked={sidebarModulesUser[section.key]?.[module.key]}
+                        checked={
+                          sidebarModulesUser[section.key]?.[module.key] !==
+                          false
+                        }
                         onChange={handleModuleChange(section.key, module.key)}
                         size='default'
-                        disabled={!sidebarModulesUser[section.key]?.enabled}
+                        disabled={
+                          sidebarModulesUser[section.key]?.enabled === false
+                        }
                       />
                     </div>
                   </div>

Some files were not shown because too many files changed in this diff