feat: Display Vercel configuration sync status and last sync time, and clear API tester streaming output before new requests.

This commit is contained in:
CJACK
2026-02-13 23:11:15 +08:00
parent 648b80bb7b
commit 85ac0d95a4
6 changed files with 118 additions and 18 deletions

View File

@@ -154,6 +154,10 @@ export default function ApiTester({ config, onMessage, authFetch }) {
}
const sendTest = async () => {
// 清除上次的流式/思考内容,防止残留
setStreamingContent('')
setStreamingThinking('')
if (selectedAccount) {
setLoading(true)
setResponse(null)
@@ -209,12 +213,12 @@ export default function ApiTester({ config, onMessage, authFetch }) {
onClick={() => setConfigExpanded(!configExpanded)}
className="lg:hidden flex items-center justify-between p-4 w-full bg-muted/20 hover:bg-muted/30 transition-colors"
>
<div className="flex items-center gap-2.5 font-medium text-sm text-foreground">
<div className="p-1.5 rounded-md bg-transparent text-foreground">
<Terminal className="w-4 h-4" />
</div>
<span>{t('apiTester.config')}</span>
<div className="flex items-center gap-2.5 font-medium text-sm text-foreground">
<div className="p-1.5 rounded-md bg-transparent text-foreground">
<Terminal className="w-4 h-4" />
</div>
<span>{t('apiTester.config')}</span>
</div>
<div className={clsx("transition-transform duration-300 text-muted-foreground", configExpanded ? "rotate-180" : "")}>
<ChevronDown className="w-4 h-4" />
</div>
@@ -365,9 +369,9 @@ export default function ApiTester({ config, onMessage, authFetch }) {
{/* Input Area */}
<div className="p-4 lg:p-6 border-t border-border bg-card">
<div className="max-w-4xl mx-auto relative group">
<textarea
className="w-full bg-[#09090b] border border-border rounded-xl pl-4 pr-12 py-3 text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all resize-none custom-scrollbar placeholder:text-muted-foreground/50 text-foreground shadow-inner"
placeholder={t('apiTester.enterMessage')}
<textarea
className="w-full bg-[#09090b] border border-border rounded-xl pl-4 pr-12 py-3 text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all resize-none custom-scrollbar placeholder:text-muted-foreground/50 text-foreground shadow-inner"
placeholder={t('apiTester.enterMessage')}
rows={1}
style={{ minHeight: '52px' }}
value={message}

View File

@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react'
import { Cloud, ArrowRight, ExternalLink, Info, CheckCircle2, XCircle } from 'lucide-react'
import { useState, useEffect, useCallback } from 'react'
import { Cloud, ArrowRight, ExternalLink, Info, CheckCircle2, XCircle, RefreshCw } from 'lucide-react'
import clsx from 'clsx'
import { useI18n } from '../i18n'
export default function VercelSync({ onMessage, authFetch }) {
@@ -10,9 +11,22 @@ export default function VercelSync({ onMessage, authFetch }) {
const [loading, setLoading] = useState(false)
const [result, setResult] = useState(null)
const [preconfig, setPreconfig] = useState(null)
const [syncStatus, setSyncStatus] = useState(null)
const apiFetch = authFetch || fetch
const fetchSyncStatus = useCallback(async () => {
try {
const res = await apiFetch('/admin/vercel/status')
if (res.ok) {
const data = await res.json()
setSyncStatus(data)
}
} catch (e) {
console.error('Failed to fetch sync status:', e)
}
}, [apiFetch])
useEffect(() => {
const loadPreconfig = async () => {
try {
@@ -28,7 +42,11 @@ export default function VercelSync({ onMessage, authFetch }) {
}
}
loadPreconfig()
}, [])
fetchSyncStatus()
// Poll every 15s to detect config changes
const interval = setInterval(fetchSyncStatus, 15000)
return () => clearInterval(interval)
}, [fetchSyncStatus])
const handleSync = async () => {
const tokenToUse = preconfig?.has_token && !vercelToken ? '__USE_PRECONFIG__' : vercelToken
@@ -58,6 +76,7 @@ export default function VercelSync({ onMessage, authFetch }) {
if (res.ok) {
setResult({ ...data, success: true })
onMessage('success', data.message)
fetchSyncStatus()
} else {
setResult({ ...data, success: false })
onMessage('error', data.detail || t('vercel.syncFailed'))
@@ -74,13 +93,41 @@ export default function VercelSync({ onMessage, authFetch }) {
{/* Configuration Form */}
<div className="bg-card border border-border rounded-xl shadow-sm p-6 space-y-6">
<div className="border-b border-border pb-6">
<h2 className="text-xl font-semibold flex items-center gap-2">
<Cloud className="w-6 h-6 text-primary" />
{t('vercel.title')}
</h2>
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold flex items-center gap-2">
<Cloud className="w-6 h-6 text-primary" />
{t('vercel.title')}
</h2>
{syncStatus && (
<div className={clsx(
"flex items-center gap-1.5 text-xs font-semibold px-2.5 py-1 rounded-full border transition-colors",
syncStatus.synced
? "text-emerald-500 bg-emerald-500/10 border-emerald-500/20"
: syncStatus.has_synced_before
? "text-amber-500 bg-amber-500/10 border-amber-500/20"
: "text-muted-foreground bg-muted/50 border-border"
)}>
<span className={clsx(
"w-1.5 h-1.5 rounded-full",
syncStatus.synced ? "bg-emerald-500" : syncStatus.has_synced_before ? "bg-amber-500 animate-pulse" : "bg-muted-foreground"
)} />
{syncStatus.synced
? t('vercel.statusSynced')
: syncStatus.has_synced_before
? t('vercel.statusNotSynced')
: t('vercel.statusNeverSynced')}
</div>
)}
</div>
<p className="text-muted-foreground text-sm mt-1">
{t('vercel.description')}
</p>
{syncStatus?.last_sync_time && (
<p className="text-xs text-muted-foreground/60 mt-1.5 flex items-center gap-1">
<RefreshCw className="w-3 h-3" />
{t('vercel.lastSyncTime', { time: new Date(syncStatus.last_sync_time * 1000).toLocaleString() })}
</p>
)}
</div>
<div className="space-y-4">

View File

@@ -221,6 +221,10 @@
"syncSucceeded": "Sync succeeded",
"syncFailedLabel": "Sync failed",
"openDeployment": "Open deployment",
"statusSynced": "Synced",
"statusNotSynced": "Not synced",
"statusNeverSynced": "Never synced",
"lastSyncTime": "Last sync: {time}",
"howItWorks": "How it works",
"steps": {
"one": "The current configuration (keys and accounts) is exported as JSON.",
@@ -229,4 +233,4 @@
"four": "Trigger a redeploy to apply the updated environment variables."
}
}
}
}

View File

@@ -221,6 +221,10 @@
"syncSucceeded": "同步成功",
"syncFailedLabel": "同步失败",
"openDeployment": "访问部署地址",
"statusSynced": "已同步",
"statusNotSynced": "未同步",
"statusNeverSynced": "从未同步",
"lastSyncTime": "上次同步: {time}",
"howItWorks": "工作原理",
"steps": {
"one": "当前配置 (密钥和账号) 被导出为 JSON 字符串。",
@@ -229,4 +233,4 @@
"four": "触发重新部署以应用新的环境变量。"
}
}
}
}