haixunMaster/haixun-backend/web/src/components/AccountConnectionMode.tsx

131 lines
4.3 KiB
TypeScript
Raw Normal View History

2026-06-23 16:55:10 +00:00
import { useEffect, useState } from 'react'
import { api, ApiError } from '../api/client'
import {
connectionModeDescription,
connectionModeLabel,
connectionReadyLabel,
} from '../lib/connectionMode'
import type { ThreadsAccountConnectionData } from '../types/api'
import { useOnboarding } from '../onboarding/OnboardingContext'
import { AcLink, Badge, ChoiceCard, ErrorText, SectionTitle, SuccessText } from './ui'
type AccountConnectionModeProps = {
accountId: string
connectionsPath: string
}
export function AccountConnectionMode({ accountId, connectionsPath }: AccountConnectionModeProps) {
const { refresh: refreshOnboarding } = useOnboarding()
const [connection, setConnection] = useState<ThreadsAccountConnectionData | null>(null)
const [loading, setLoading] = useState(true)
const [busy, setBusy] = useState(false)
const [error, setError] = useState('')
const [message, setMessage] = useState('')
const load = async () => {
if (!accountId) return
setLoading(true)
setError('')
try {
const data = await api.get<ThreadsAccountConnectionData>(
`/api/v1/threads-accounts/${encodeURIComponent(accountId)}/connection`,
{ auth: true },
)
setConnection(data)
} catch (e) {
setError(e instanceof ApiError ? e.message : '載入連線設定失敗')
setConnection(null)
} finally {
setLoading(false)
}
}
useEffect(() => {
load().catch(() => undefined)
}, [accountId])
const saveDevMode = async (devMode: boolean) => {
if (!accountId) return
setBusy(true)
setError('')
setMessage('')
try {
const data = await api.patch<ThreadsAccountConnectionData>(
`/api/v1/threads-accounts/${encodeURIComponent(accountId)}/connection`,
{ dev_mode: devMode },
{ auth: true },
)
setConnection(data)
setMessage(devMode ? '已切換為開發模式(爬蟲)' : '已切換為正式模式API')
await refreshOnboarding()
} catch (e) {
setError(e instanceof ApiError ? e.message : '儲存失敗')
} finally {
setBusy(false)
}
}
const prefs = connection?.prefs
const devMode = !!prefs?.dev_mode
return (
<div className="grid gap-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="grid gap-2">
<SectionTitle></SectionTitle>
<p className="text-sm leading-relaxed text-ink-secondary">
Threads APIOAuthChrome {' '}
<AcLink to={connectionsPath}></AcLink>
</p>
</div>
{connection ? (
<div className="flex flex-wrap gap-2">
<Badge tone={devMode ? 'warning' : 'brand'}>{connectionModeLabel(devMode)}</Badge>
<Badge
tone={
devMode
? connection.browser_connected
? 'success'
: 'warning'
: connection.api_connected
? 'success'
: 'warning'
}
>
{connectionReadyLabel(devMode, connection.browser_connected, connection.api_connected)}
</Badge>
</div>
) : null}
</div>
{loading ? (
<p className="text-sm text-muted"></p>
) : prefs ? (
<>
<p className="text-sm text-ink-secondary">{connectionModeDescription(devMode)}</p>
<div className="grid gap-3 sm:grid-cols-2">
<ChoiceCard
title="正式模式API"
hint="搜尋、發文、留言走官方 API"
active={!devMode}
disabled={busy || !devMode}
onClick={() => saveDevMode(false)}
/>
<ChoiceCard
title="開發模式(爬蟲)"
hint="本機除錯用,需同步 Chrome Session"
active={devMode}
disabled={busy || devMode}
onClick={() => saveDevMode(true)}
/>
</div>
</>
) : (
<p className="text-sm text-ink-secondary"></p>
)}
<ErrorText message={error} />
<SuccessText message={message} />
</div>
)
}