/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import React, { useState, useEffect, useMemo, useRef } from 'react'; import { Modal, Form, Input, Select, InputNumber, Switch, Collapse, Card, Divider, Button, Typography, Space, Spin, Tag, Row, Col, Tooltip, Radio, } from '@douyinfe/semi-ui'; import { IconPlus, IconMinus, IconHelpCircle, IconCopy } from '@douyinfe/semi-icons'; import { API } from '../../../../helpers'; import { showError, showSuccess, copy } from '../../../../helpers'; const { Text, Title } = Typography; const { Option } = Select; const RadioGroup = Radio.Group; const BUILTIN_IMAGE = 'ollama/ollama:latest'; const DEFAULT_TRAFFIC_PORT = 11434; const generateRandomKey = () => { try { if (typeof crypto !== 'undefined' && crypto.randomUUID) { return `ionet-${crypto.randomUUID().replace(/-/g, '')}`; } } catch (error) { // ignore } return `ionet-${Math.random().toString(36).slice(2)}${Math.random() .toString(36) .slice(2)}`; }; const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => { const [formApi, setFormApi] = useState(null); const [loading, setLoading] = useState(false); const [submitting, setSubmitting] = useState(false); // Resource data states const [hardwareTypes, setHardwareTypes] = useState([]); 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 [entrypoint, setEntrypoint] = useState(['']); const [args, setArgs] = useState(['']); const [imageMode, setImageMode] = useState('builtin'); const [autoOllamaKey, setAutoOllamaKey] = useState(''); const customSecretEnvRef = useRef(null); const customEnvRef = useRef(null); const customImageRef = useRef(''); const customTrafficPortRef = useRef(null); const prevImageModeRef = useRef('builtin'); 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: '', image_url: BUILTIN_IMAGE, gpus_per_container: 1, replica_count: 1, duration_hours: 1, traffic_port: DEFAULT_TRAFFIC_PORT, location_ids: [], }); const [formKey, setFormKey] = useState(0); const [priceCurrency, setPriceCurrency] = useState('usdc'); const normalizeCurrencyValue = (value) => { if (typeof value === 'string') return value.toLowerCase(); if (value && typeof value === 'object') { if (typeof value.value === 'string') return value.value.toLowerCase(); if (typeof value.target?.value === 'string') { return value.target.value.toLowerCase(); } } return 'usdc'; }; const handleCurrencyChange = (value) => { const normalized = normalizeCurrencyValue(value); setPriceCurrency(normalized); }; const hardwareLabelMap = useMemo(() => { const map = {}; hardwareTypes.forEach((hardware) => { const displayName = hardware.brand_name ? `${hardware.brand_name} ${hardware.name}`.trim() : hardware.name; map[hardware.id] = displayName; }); return map; }, [hardwareTypes]); const locationLabelMap = useMemo(() => { const map = {}; locations.forEach((location) => { map[location.id] = location.name; }); return map; }, [locations]); // Form values for price calculation const [selectedHardwareId, setSelectedHardwareId] = useState(null); const [selectedLocationIds, setSelectedLocationIds] = useState([]); const [gpusPerContainer, setGpusPerContainer] = useState(1); const [durationHours, setDurationHours] = useState(1); const [replicaCount, setReplicaCount] = useState(1); // Load initial data when modal opens useEffect(() => { if (visible) { loadHardwareTypes(); resetFormState(); } }, [visible]); // Load available replicas when hardware or locations change useEffect(() => { if (!visible) { return; } if (selectedHardwareId && gpusPerContainer > 0) { loadAvailableReplicas(selectedHardwareId, gpusPerContainer); } }, [selectedHardwareId, gpusPerContainer, visible]); // Calculate price when relevant parameters change useEffect(() => { if (!visible) { return; } if ( selectedHardwareId && selectedLocationIds.length > 0 && gpusPerContainer > 0 && durationHours > 0 && replicaCount > 0 ) { calculatePrice(); } else { setPriceEstimation(null); } }, [ selectedHardwareId, selectedLocationIds, gpusPerContainer, durationHours, replicaCount, priceCurrency, visible, ]); useEffect(() => { if (!visible) { return; } const prevMode = prevImageModeRef.current; if (prevMode === imageMode) { return; } if (imageMode === 'builtin') { if (prevMode === 'custom') { if (formApi) { customImageRef.current = formApi.getValue('image_url') || customImageRef.current; customTrafficPortRef.current = formApi.getValue('traffic_port') ?? customTrafficPortRef.current; } customSecretEnvRef.current = secretEnvVariables.map((item) => ({ ...item })); customEnvRef.current = envVariables.map((item) => ({ ...item })); } const newKey = generateRandomKey(); setAutoOllamaKey(newKey); setSecretEnvVariables([{ key: 'OLLAMA_API_KEY', value: newKey }]); setEnvVariables([{ key: '', value: '' }]); if (formApi) { formApi.setValue('image_url', BUILTIN_IMAGE); formApi.setValue('traffic_port', DEFAULT_TRAFFIC_PORT); } } else { const restoredSecrets = customSecretEnvRef.current && customSecretEnvRef.current.length > 0 ? customSecretEnvRef.current.map((item) => ({ ...item })) : [{ key: '', value: '' }]; const restoredEnv = customEnvRef.current && customEnvRef.current.length > 0 ? customEnvRef.current.map((item) => ({ ...item })) : [{ key: '', value: '' }]; setSecretEnvVariables(restoredSecrets); setEnvVariables(restoredEnv); if (formApi) { const restoredImage = customImageRef.current || ''; formApi.setValue('image_url', restoredImage); if (customTrafficPortRef.current) { formApi.setValue('traffic_port', customTrafficPortRef.current); } } } prevImageModeRef.current = imageMode; }, [imageMode, visible, secretEnvVariables, envVariables, formApi]); useEffect(() => { if (!visible || !formApi) { return; } if (imageMode === 'builtin') { formApi.setValue('image_url', BUILTIN_IMAGE); } }, [formApi, imageMode, visible]); useEffect(() => { if (!formApi) { return; } if (selectedHardwareId !== null && selectedHardwareId !== undefined) { formApi.setValue('hardware_id', selectedHardwareId); } }, [formApi, selectedHardwareId]); useEffect(() => { if (!formApi) { return; } formApi.setValue('location_ids', selectedLocationIds); }, [formApi, selectedLocationIds]); useEffect(() => { if (!visible) { return; } if (selectedHardwareId) { loadLocations(selectedHardwareId); } else { setLocations([]); setSelectedLocationIds([]); setAvailableReplicas([]); setLocationTotalAvailable(null); setLoadingLocations(false); setLoadingReplicas(false); locationRequestIdRef.current = 0; replicaRequestIdRef.current = 0; if (formApi) { formApi.setValue('location_ids', []); } } }, [selectedHardwareId, visible, formApi]); const resetFormState = () => { const randomName = `deployment-${Math.random().toString(36).slice(2, 8)}`; const generatedKey = generateRandomKey(); setSelectedHardwareId(null); setSelectedLocationIds([]); setGpusPerContainer(1); setDurationHours(1); setReplicaCount(1); setPriceEstimation(null); setAvailableReplicas([]); setLocations([]); setLocationTotalAvailable(null); setHardwareTotalAvailable(null); setEnvVariables([{ key: '', value: '' }]); setSecretEnvVariables([{ key: 'OLLAMA_API_KEY', value: generatedKey }]); setEntrypoint(['']); setArgs(['']); setShowAdvanced(false); setImageMode('builtin'); setAutoOllamaKey(generatedKey); customSecretEnvRef.current = null; customEnvRef.current = null; customImageRef.current = ''; customTrafficPortRef.current = DEFAULT_TRAFFIC_PORT; prevImageModeRef.current = 'builtin'; setFormDefaults({ resource_private_name: randomName, image_url: BUILTIN_IMAGE, gpus_per_container: 1, replica_count: 1, duration_hours: 1, traffic_port: DEFAULT_TRAFFIC_PORT, location_ids: [], }); setFormKey((prev) => prev + 1); setPriceCurrency('usdc'); }; const arraysEqual = (a = [], b = []) => a.length === b.length && a.every((value, index) => value === b[index]); const loadHardwareTypes = async () => { try { setLoadingHardware(true); const response = await API.get('/api/deployments/hardware-types'); if (response.data.success) { const { hardware_types: hardwareList = [], total_available } = response.data.data || {}; const normalizedHardware = hardwareList.map((hardware) => { const availableCountValue = Number(hardware.available_count); const availableCount = Number.isNaN(availableCountValue) ? 0 : availableCountValue; const availableBool = typeof hardware.available === 'boolean' ? hardware.available : availableCount > 0; return { ...hardware, available: availableBool, available_count: availableCount, }; }); const providedTotal = Number(total_available); const fallbackTotal = normalizedHardware.reduce( (acc, item) => acc + (Number.isNaN(item.available_count) ? 0 : item.available_count), 0, ); const hasProvidedTotal = total_available !== undefined && total_available !== null && total_available !== '' && !Number.isNaN(providedTotal); setHardwareTypes(normalizedHardware); setHardwareTotalAvailable( hasProvidedTotal ? providedTotal : fallbackTotal, ); } else { showError(t('获取硬件类型失败: ') + response.data.message); } } catch (error) { showError(t('获取硬件类型失败: ') + error.message); } finally { setLoadingHardware(false); } }; 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([]); setLocationTotalAvailable(null); setLoadingReplicas(false); return; } const requestId = Date.now(); replicaRequestIdRef.current = requestId; setLoadingReplicas(true); setAvailableReplicas([]); try { const response = await API.get( `/api/deployments/available-replicas?hardware_id=${hardwareId}&gpu_count=${gpuCount}`, ); if (replicaRequestIdRef.current !== requestId) { return; } 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, ); 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 { if (replicaRequestIdRef.current === requestId) { setLoadingReplicas(false); } } }; const calculatePrice = async () => { try { setLoadingPrice(true); const requestData = { location_ids: selectedLocationIds, hardware_id: selectedHardwareId, gpus_per_container: gpusPerContainer, duration_hours: durationHours, replica_count: replicaCount, currency: priceCurrency?.toLowerCase?.() || priceCurrency, duration_type: 'hour', duration_qty: durationHours, hardware_qty: gpusPerContainer, }; const response = await API.post('/api/deployments/price-estimation', requestData); if (response.data.success) { setPriceEstimation(response.data.data); } else { showError(t('价格计算失败: ') + response.data.message); setPriceEstimation(null); } } catch (error) { console.error('Price calculation error:', error); setPriceEstimation(null); } finally { setLoadingPrice(false); } }; const handleSubmit = async (values) => { try { setSubmitting(true); // Prepare environment variables const envVars = {}; envVariables.forEach(env => { if (env.key && env.value) { envVars[env.key] = env.value; } }); const secretEnvVars = {}; secretEnvVariables.forEach(env => { if (env.key && env.value) { secretEnvVars[env.key] = env.value; } }); if (imageMode === 'builtin') { if (!secretEnvVars.OLLAMA_API_KEY) { const ensuredKey = autoOllamaKey || generateRandomKey(); secretEnvVars.OLLAMA_API_KEY = ensuredKey; setAutoOllamaKey(ensuredKey); } } // Prepare entrypoint and args const cleanEntrypoint = entrypoint.filter(item => item.trim() !== ''); const cleanArgs = args.filter(item => item.trim() !== ''); const resolvedImage = imageMode === 'builtin' ? BUILTIN_IMAGE : values.image_url; const resolvedTrafficPort = 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, hardware_id: values.hardware_id, location_ids: values.location_ids, container_config: { replica_count: values.replica_count, env_variables: envVars, secret_env_variables: secretEnvVars, entrypoint: cleanEntrypoint.length > 0 ? cleanEntrypoint : undefined, args: cleanArgs.length > 0 ? cleanArgs : undefined, traffic_port: resolvedTrafficPort, }, registry_config: { image_url: resolvedImage, registry_username: values.registry_username || undefined, registry_secret: values.registry_secret || undefined, }, }; const response = await API.post('/api/deployments', requestData); if (response.data.success) { showSuccess(t('容器创建成功')); onSuccess?.(response.data.data); onCancel(); } else { showError(t('容器创建失败: ') + response.data.message); } } catch (error) { showError(t('容器创建失败: ') + error.message); } finally { setSubmitting(false); } }; const handleAddEnvVariable = (type) => { if (type === 'env') { setEnvVariables([...envVariables, { key: '', value: '' }]); } else { setSecretEnvVariables([...secretEnvVariables, { key: '', value: '' }]); } }; const handleRemoveEnvVariable = (index, type) => { if (type === 'env') { const newEnvVars = envVariables.filter((_, i) => i !== index); setEnvVariables(newEnvVars.length > 0 ? newEnvVars : [{ key: '', value: '' }]); } else { const newSecretEnvVars = secretEnvVariables.filter((_, i) => i !== index); setSecretEnvVariables(newSecretEnvVars.length > 0 ? newSecretEnvVars : [{ key: '', value: '' }]); } }; const handleEnvVariableChange = (index, field, value, type) => { if (type === 'env') { const newEnvVars = [...envVariables]; newEnvVars[index][field] = value; setEnvVariables(newEnvVars); } else { const newSecretEnvVars = [...secretEnvVariables]; newSecretEnvVars[index][field] = value; setSecretEnvVariables(newSecretEnvVars); } }; const handleArrayFieldChange = (index, value, type) => { if (type === 'entrypoint') { const newEntrypoint = [...entrypoint]; newEntrypoint[index] = value; setEntrypoint(newEntrypoint); } else { const newArgs = [...args]; newArgs[index] = value; setArgs(newArgs); } }; const handleAddArrayField = (type) => { if (type === 'entrypoint') { setEntrypoint([...entrypoint, '']); } else { setArgs([...args, '']); } }; const handleRemoveArrayField = (index, type) => { if (type === 'entrypoint') { const newEntrypoint = entrypoint.filter((_, i) => i !== index); setEntrypoint(newEntrypoint.length > 0 ? newEntrypoint : ['']); } else { const newArgs = args.filter((_, i) => i !== index); setArgs(newArgs.length > 0 ? newArgs : ['']); } }; useEffect(() => { if (!visible) { return; } if (!selectedHardwareId) { if (selectedLocationIds.length > 0) { setSelectedLocationIds([]); if (formApi) { formApi.setValue('location_ids', []); } } return; } const validLocationIds = availableReplicas.length > 0 ? availableReplicas.map((item) => item.location_id) : locations.map((location) => location.id); if (validLocationIds.length === 0) { if (selectedLocationIds.length > 0) { setSelectedLocationIds([]); if (formApi) { formApi.setValue('location_ids', []); } } return; } if (selectedLocationIds.length === 0) { return; } const filteredSelection = selectedLocationIds.filter((id) => validLocationIds.includes(id), ); if (!arraysEqual(selectedLocationIds, filteredSelection)) { setSelectedLocationIds(filteredSelection); if (formApi) { formApi.setValue('location_ids', filteredSelection); } } }, [ availableReplicas, 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]); const isPriceReady = useMemo( () => selectedHardwareId && selectedLocationIds.length > 0 && gpusPerContainer > 0 && durationHours > 0 && replicaCount > 0, [ selectedHardwareId, selectedLocationIds, gpusPerContainer, durationHours, replicaCount, ], ); const currencyLabel = (priceEstimation?.currency || priceCurrency || '').toUpperCase(); const selectedHardwareLabel = selectedHardwareId ? hardwareLabelMap[selectedHardwareId] : ''; const selectedLocationNames = selectedLocationIds .map((id) => locationLabelMap[id]) .filter(Boolean); const totalGpuHours = Number(gpusPerContainer || 0) * Number(replicaCount || 0) * Number(durationHours || 0); const priceSummaryItems = [ { key: 'hardware', label: t('硬件类型'), value: selectedHardwareLabel || '--', }, { key: 'locations', label: t('部署位置'), value: selectedLocationNames.length ? selectedLocationNames.join('、') : '--', }, { key: 'replicas', label: t('副本数量'), value: (replicaCount ?? 0).toString(), }, { key: 'gpus', label: t('每容器GPU数量'), value: (gpusPerContainer ?? 0).toString(), }, { key: 'duration', label: t('运行时长(小时)'), value: durationHours ? durationHours.toString() : '0', }, { key: 'gpu-hours', label: t('总 GPU 小时'), value: totalGpuHours > 0 ? totalGpuHours.toLocaleString() : '0', }, ]; const scrollToSection = (ref) => { if (ref?.current && typeof ref.current.scrollIntoView === 'function') { ref.current.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }; const priceUnavailableContent = (
{loadingPrice ? ( {t('价格计算中...')} ) : ( {isPriceReady ? t('价格暂时不可用,请稍后重试') : t('完成硬件类型、部署位置、副本数量等配置后,将自动计算价格')} )}
); useEffect(() => { if (!visible || !formApi) { return; } if (maxAvailableReplicas > 0 && replicaCount > maxAvailableReplicas) { setReplicaCount(maxAvailableReplicas); formApi.setValue('replica_count', maxAvailableReplicas); } }, [maxAvailableReplicas, replicaCount, visible, formApi]); return ( formApi?.submitForm()} okText={t('创建')} cancelText={t('取消')} width={800} confirmLoading={submitting} style={{ top: 20 }} >
{t('部署配置')}
{t('镜像选择')}
setImageMode(value?.target?.value ?? value)} > {t('内置 Ollama 镜像')} {t('自定义镜像')}
{ if (imageMode === 'custom') { customImageRef.current = value; } }} /> {imageMode === 'builtin' && ( {t('系统已为该部署准备 Ollama 镜像与随机 API Key')} )} { setSelectedHardwareId(value); setSelectedLocationIds([]); if (formApi) { formApi.setValue('location_ids', []); } }} style={{ width: '100%' }} dropdownStyle={{ maxHeight: 360, overflowY: 'auto' }} renderSelectedItem={(optionNode) => optionNode ? hardwareLabelMap[optionNode?.value] || optionNode?.label || optionNode?.value || '' : '' } > {hardwareTypes.map((hardware) => { const displayName = hardware.brand_name ? `${hardware.brand_name} ${hardware.name}`.trim() : hardware.name; const availableCount = typeof hardware.available_count === 'number' ? hardware.available_count : 0; const hasAvailability = availableCount > 0; return ( ); })} h.id === selectedHardwareId)?.max_gpus : 8} step={1} innerButtons rules={[{ required: true, message: t('请输入GPU数量') }]} onChange={(value) => setGpusPerContainer(value)} style={{ width: '100%' }} /> {typeof hardwareTotalAvailable === 'number' && ( {t('全部硬件总可用资源')}: {hardwareTotalAvailable} )} {t('部署位置')} {loadingReplicas && } } placeholder={ !selectedHardwareId ? t('请先选择硬件类型') : loadingLocations || loadingReplicas ? t('正在加载可用部署位置...') : t('选择部署位置(可多选)') } multiple loading={loadingLocations || loadingReplicas} disabled={!selectedHardwareId || loadingLocations || 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 || '', })} > {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 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; return ( ); })} {typeof locationTotalAvailable === 'number' && ( {t('全部地区总可用资源')}: {locationTotalAvailable} )} setReplicaCount(value)} style={{ width: '100%' }} /> {maxAvailableReplicas > 0 && ( {t('最大可用')}: {maxAvailableReplicas} )} setDurationHours(value)} style={{ width: '100%' }} /> {t('流量端口')} } placeholder={DEFAULT_TRAFFIC_PORT} min={1} max={65535} style={{ width: '100%' }} disabled={imageMode === 'builtin'} />
{t('镜像仓库配置')} {t('容器启动配置')}
{t('启动命令 (Entrypoint)')} {entrypoint.map((cmd, index) => (
handleArrayFieldChange(index, value, 'entrypoint')} style={{ flex: 1, marginRight: 8 }} />
))}
{t('启动参数 (Args)')} {args.map((arg, index) => (
handleArrayFieldChange(index, value, 'args')} style={{ flex: 1, marginRight: 8 }} />
))}
{t('环境变量')}
{t('普通环境变量')} {envVariables.map((env, index) => ( handleEnvVariableChange(index, 'key', value, 'env')} /> handleEnvVariableChange(index, 'value', value, 'env')} />
{t('密钥环境变量')} {secretEnvVariables.map((env, index) => { const isAutoSecret = imageMode === 'builtin' && env.key === 'OLLAMA_API_KEY'; return ( handleEnvVariableChange(index, 'key', value, 'secret')} disabled={isAutoSecret} /> handleEnvVariableChange(index, 'value', value, 'secret')} disabled={isAutoSecret} />
{t('价格预估')} {t('计价币种')} USDC IOCOIN {currencyLabel}
{priceEstimation ? (
{t('预估总费用')}
{typeof priceEstimation.estimated_cost === 'number' ? `${priceEstimation.estimated_cost.toFixed(4)} ${currencyLabel}` : '--'}
{t('小时费率')} {typeof priceEstimation.price_breakdown?.hourly_rate === 'number' ? `${priceEstimation.price_breakdown.hourly_rate.toFixed(4)} ${currencyLabel}/h` : '--'}
{t('计算成本')} {typeof priceEstimation.price_breakdown?.compute_cost === 'number' ? `${priceEstimation.price_breakdown.compute_cost.toFixed(4)} ${currencyLabel}` : '--'}
{priceSummaryItems.map((item) => (
{item.label} {item.value}
))}
) : ( priceUnavailableContent )} {priceEstimation && loadingPrice && ( {t('价格重新计算中...')} )}
); }; export default CreateDeploymentModal;