|
|
@@ -32,6 +32,7 @@ export function Deployment() {
|
|
|
|
|
|
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
|
const servicesPollingRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
|
+ const [loadingTaskIds, setLoadingTaskIds] = useState<Set<string>>(new Set())
|
|
|
|
|
|
// 加载 API Keys
|
|
|
const loadApiKeys = useCallback(() => {
|
|
|
@@ -129,15 +130,19 @@ export function Deployment() {
|
|
|
}
|
|
|
|
|
|
const handleStop = (taskId: string) => {
|
|
|
+ setLoadingTaskIds(prev => new Set(prev).add(taskId))
|
|
|
api.deployment.stop(taskId)
|
|
|
.then(() => loadServices())
|
|
|
.catch(() => {})
|
|
|
+ .finally(() => setLoadingTaskIds(prev => { const next = new Set(prev); next.delete(taskId); return next }))
|
|
|
}
|
|
|
|
|
|
const handleRestart = (taskId: string) => {
|
|
|
+ setLoadingTaskIds(prev => new Set(prev).add(taskId))
|
|
|
api.deployment.restart(taskId)
|
|
|
.then(() => loadServices())
|
|
|
.catch(() => {})
|
|
|
+ .finally(() => setLoadingTaskIds(prev => { const next = new Set(prev); next.delete(taskId); return next }))
|
|
|
}
|
|
|
|
|
|
const tabStyle = (active: boolean): React.CSSProperties => ({
|
|
|
@@ -468,7 +473,7 @@ export function Deployment() {
|
|
|
) : (
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
|
{services.map(svc => (
|
|
|
- <ServiceCard key={svc.task_id} service={svc} onStop={() => handleStop(svc.task_id)} onRestart={() => handleRestart(svc.task_id)} />
|
|
|
+ <ServiceCard key={svc.task_id} service={svc} onStop={() => handleStop(svc.task_id)} onRestart={() => handleRestart(svc.task_id)} loading={loadingTaskIds.has(svc.task_id)} />
|
|
|
))}
|
|
|
</div>
|
|
|
)}
|
|
|
@@ -523,7 +528,7 @@ function TaskStatus({ result }: { result: DeployResponse }) {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-function ServiceCard({ service, onStop, onRestart }: { service: DeployedServiceInfo; onStop: () => void; onRestart: () => void }) {
|
|
|
+function ServiceCard({ service, onStop, onRestart, loading }: { service: DeployedServiceInfo; onStop: () => void; onRestart: () => void; loading?: boolean }) {
|
|
|
const [showUsage, setShowUsage] = useState(false)
|
|
|
const isRunning = service.status === 'running'
|
|
|
const isStopped = service.status === 'stopped'
|
|
|
@@ -578,26 +583,38 @@ function ServiceCard({ service, onStop, onRestart }: { service: DeployedServiceI
|
|
|
</button>
|
|
|
<button
|
|
|
onClick={onStop}
|
|
|
+ disabled={loading}
|
|
|
style={{
|
|
|
padding: '6px 12px', borderRadius: 6,
|
|
|
- border: '1px solid #fca5a5', background: '#fff', color: '#dc2626',
|
|
|
- cursor: 'pointer', fontSize: 12, fontWeight: 500,
|
|
|
+ border: '1px solid #fca5a5',
|
|
|
+ background: loading ? '#fee2e2' : '#fff',
|
|
|
+ color: loading ? '#fca5a5' : '#dc2626',
|
|
|
+ cursor: loading ? 'not-allowed' : 'pointer',
|
|
|
+ fontSize: 12, fontWeight: 500,
|
|
|
+ display: 'inline-flex', alignItems: 'center', gap: 4,
|
|
|
}}
|
|
|
>
|
|
|
- 停止
|
|
|
+ {loading && <span style={{ animation: 'spin 1s linear infinite', display: 'inline-block' }}>⟳</span>}
|
|
|
+ {loading ? '停止中...' : '停止'}
|
|
|
</button>
|
|
|
</>
|
|
|
)}
|
|
|
{isStopped && (
|
|
|
<button
|
|
|
onClick={onRestart}
|
|
|
+ disabled={loading}
|
|
|
style={{
|
|
|
padding: '6px 12px', borderRadius: 6,
|
|
|
- border: '1px solid #86efac', background: '#fff', color: '#16a34a',
|
|
|
- cursor: 'pointer', fontSize: 12, fontWeight: 500,
|
|
|
+ border: '1px solid #86efac',
|
|
|
+ background: loading ? '#dcfce7' : '#fff',
|
|
|
+ color: loading ? '#86efac' : '#16a34a',
|
|
|
+ cursor: loading ? 'not-allowed' : 'pointer',
|
|
|
+ fontSize: 12, fontWeight: 500,
|
|
|
+ display: 'inline-flex', alignItems: 'center', gap: 4,
|
|
|
}}
|
|
|
>
|
|
|
- 重启
|
|
|
+ {loading && <span style={{ animation: 'spin 1s linear infinite', display: 'inline-block' }}>⟳</span>}
|
|
|
+ {loading ? '重启中...' : '重启'}
|
|
|
</button>
|
|
|
)}
|
|
|
{isPending && (
|