Files
Ant-Browser/frontend/src/modules/browser/pages/BrowserDetailPage.tsx
T
Ant Browser Release Bot 6f58a6c19a publish: 1.0.0 snapshot (bad2ec1)
channel: master
version: 1.0.0
source-ref: master
published-at-utc: 2026-03-13T15:19:28Z
2026-03-13 23:19:29 +08:00

295 lines
11 KiB
TypeScript

import { useEffect, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import { Copy, Globe, Play, RefreshCw, RotateCcw, Square } from 'lucide-react'
import { Badge, Button, Card, Input, Table, toast } from '../../../shared/components'
import type { TableColumn } from '../../../shared/components/Table'
import type { BrowserProfile, BrowserTab } from '../types'
import {
fetchBrowserProfiles,
fetchBrowserTabs,
openBrowserUrl,
regenerateBrowserProfileCode,
restartBrowserInstance,
startBrowserInstance,
stopBrowserInstance,
} from '../api'
import { CookieManagerCard } from '../components/CookieManagerCard'
import { SnapshotTab } from '../components/SnapshotTab'
import { resolveActionErrorMessage } from '../utils/actionErrors'
const statusVariant = (running: boolean) => (running ? 'success' : 'warning')
const formatTime = (value?: string) => {
if (!value) return '-'
const date = new Date(value)
if (Number.isNaN(date.getTime())) return '-'
return date.toLocaleString('zh-CN')
}
type TabKey = 'overview' | 'snapshot'
const TABS: { key: TabKey; label: string }[] = [
{ key: 'overview', label: '概览' },
{ key: 'snapshot', label: '快照管理' },
]
export function BrowserDetailPage() {
const { id } = useParams()
const [profile, setProfile] = useState<BrowserProfile | null>(null)
const [tabs, setTabs] = useState<BrowserTab[]>([])
const [targetUrl, setTargetUrl] = useState('https://example.com')
const [activeTab, setActiveTab] = useState<TabKey>('overview')
const loadProfile = async () => {
const list = await fetchBrowserProfiles()
const current = list.find(item => item.profileId === id) || null
setProfile(current)
}
const loadTabs = async () => {
if (!id) return
const list = await fetchBrowserTabs(id)
setTabs(list)
}
useEffect(() => { loadProfile() }, [id])
useEffect(() => { loadTabs() }, [id])
if (!profile) {
return (
<div className="flex items-center justify-center h-64 text-sm text-[var(--color-text-muted)]">
暂无实例信息
</div>
)
}
const handleOpenUrl = async () => {
await openBrowserUrl(profile.profileId, targetUrl)
toast.success('已发送打开指令')
}
const handleStart = async () => {
try {
await startBrowserInstance(profile.profileId)
toast.success('实例已启动')
} catch (error: any) {
toast.error(resolveActionErrorMessage(error, '实例启动失败'))
} finally {
loadProfile()
}
}
const handleStop = async () => {
try {
await stopBrowserInstance(profile.profileId)
toast.success('实例已停止')
} catch (error: any) {
toast.error(resolveActionErrorMessage(error, '实例停止失败'))
} finally {
loadProfile()
}
}
const handleRestart = async () => {
try {
await restartBrowserInstance(profile.profileId)
toast.success('实例已重启')
} catch (error: any) {
toast.error(resolveActionErrorMessage(error, '实例重启失败'))
} finally {
loadProfile()
}
}
const tabsColumns: TableColumn<BrowserTab>[] = [
{ key: 'title', title: '标题' },
{ key: 'url', title: '地址' },
{
key: 'active',
title: '状态',
render: value => (
<Badge variant={value ? 'success' : 'default'}>{value ? '当前' : '后台'}</Badge>
),
},
]
return (
<div className="space-y-5 animate-fade-in">
{/* 页头 */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-semibold text-[var(--color-text-primary)]">实例详情</h1>
<p className="text-sm text-[var(--color-text-muted)] mt-1">{profile.profileName}</p>
</div>
<div className="flex gap-2">
<Link to={`/browser/edit/${profile.profileId}`}>
<Button variant="secondary" size="sm">编辑配置</Button>
</Link>
<Link to="/browser/list">
<Button variant="ghost" size="sm">返回列表</Button>
</Link>
</div>
</div>
{/* Tab 导航 */}
<div className="flex border-b border-[var(--color-border)]">
{TABS.map(tab => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={[
'px-4 py-2 text-sm font-medium transition-colors',
activeTab === tab.key
? 'border-b-2 border-[var(--color-primary)] text-[var(--color-primary)]'
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]',
].join(' ')}
>
{tab.label}
</button>
))}
</div>
{/* 概览 Tab */}
{activeTab === 'overview' && (
<div className="space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card title="运行信息" subtitle="实例运行状态与端口信息">
<div className="space-y-3 text-sm text-[var(--color-text-secondary)]">
<div className="flex justify-between">
<span>状态</span>
<Badge variant={statusVariant(profile.running)} dot>{profile.running ? '运行中' : '已停止'}</Badge>
</div>
<div className="flex justify-between">
<span>进程 PID</span>
<span>{profile.pid || '-'}</span>
</div>
<div className="flex justify-between">
<span>调试端口</span>
<span>{profile.debugPort || '-'}</span>
</div>
<div className="flex justify-between">
<span>最近启动</span>
<span>{formatTime(profile.lastStartAt)}</span>
</div>
<div className="flex justify-between">
<span>最近停止</span>
<span>{formatTime(profile.lastStopAt)}</span>
</div>
</div>
</Card>
<Card title="配置摘要" subtitle="指纹与启动参数">
<div className="space-y-3 text-sm text-[var(--color-text-secondary)]">
<div className="flex justify-between">
<span>用户数据目录</span>
<span>{profile.userDataDir}</span>
</div>
<div className="flex justify-between">
<span>内核</span>
<span>{profile.coreId || '默认'}</span>
</div>
<div className="flex justify-between">
<span>代理配置</span>
<span>{profile.proxyConfig || '-'}</span>
</div>
<div className="flex justify-between">
<span>指纹参数</span>
<span>{profile.fingerprintArgs?.length || 0} </span>
</div>
<div className="flex justify-between">
<span>启动参数</span>
<span>{profile.launchArgs?.length || 0} </span>
</div>
<div className="flex justify-between">
<span>标签</span>
<span>{profile.tags?.join(', ') || '-'}</span>
</div>
<div className="flex justify-between items-center">
<span>快捷码</span>
<div className="flex items-center gap-1">
{profile.launchCode ? (
<>
<code className="text-xs font-mono bg-[var(--color-bg-secondary)] px-1.5 py-0.5 rounded text-[var(--color-accent)]">{profile.launchCode}</code>
<button
onClick={() => navigator.clipboard.writeText(profile.launchCode!).then(() => toast.success('已复制快捷码'))}
className="p-0.5 hover:text-[var(--color-accent)] text-[var(--color-text-muted)] transition-colors"
title="复制"
>
<Copy className="w-3 h-3" />
</button>
<button
onClick={async () => {
await regenerateBrowserProfileCode(profile.profileId)
loadProfile()
toast.success('快捷码已重新生成')
}}
className="p-0.5 hover:text-[var(--color-accent)] text-[var(--color-text-muted)] transition-colors"
title="重新生成"
>
<RefreshCw className="w-3 h-3" />
</button>
</>
) : (
<span className="text-[var(--color-text-muted)]">-</span>
)}
</div>
</div>
</div>
</Card>
</div>
<Card title="快捷操作" subtitle="快速控制实例">
<div className="flex flex-wrap items-center gap-2">
<Button size="sm" onClick={handleStart}>
<Play className="w-4 h-4" />
启动
</Button>
<Button size="sm" variant="secondary" onClick={handleStop}>
<Square className="w-4 h-4" />
停止
</Button>
<Button size="sm" variant="ghost" onClick={handleRestart}>
<RotateCcw className="w-4 h-4" />
重启
</Button>
</div>
</Card>
{profile.lastError && (
<Card title="最近错误" subtitle="最近一次启动或运行失败原因">
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 whitespace-pre-line">
{profile.lastError}
</div>
</Card>
)}
<Card title="打开地址" subtitle="向实例发送打开 URL 指令">
<div className="flex flex-col md:flex-row gap-3">
<Input value={targetUrl} onChange={e => setTargetUrl(e.target.value)} placeholder="请输入目标地址" />
<Button onClick={handleOpenUrl}>
<Globe className="w-4 h-4" />
打开
</Button>
</div>
</Card>
<Card title="标签页列表" subtitle="当前实例标签页信息">
<Table columns={tabsColumns} data={tabs} rowKey="tabId" />
</Card>
<CookieManagerCard
profileId={profile.profileId}
profileName={profile.profileName}
running={profile.running}
/>
</div>
)}
{/* 快照管理 Tab */}
{activeTab === 'snapshot' && (
<SnapshotTab profileId={profile.profileId} running={profile.running} />
)}
</div>
)
}