优化 CK 管理和商品缓存
This commit is contained in:
+506
-96
@@ -6,6 +6,7 @@ import {
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Statistic,
|
||||
@@ -21,11 +22,15 @@ import {
|
||||
ClipboardList,
|
||||
ClipboardPaste,
|
||||
ListChecks,
|
||||
Pencil,
|
||||
Play,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Save,
|
||||
ShoppingCart,
|
||||
Square,
|
||||
Settings,
|
||||
Trash2,
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
DefaultConfig,
|
||||
@@ -42,6 +47,8 @@ import {
|
||||
import { ClipboardGetText, EventsOn } from '../wailsjs/runtime/runtime'
|
||||
|
||||
const storageKey = 'huya-go.gui.config.v2'
|
||||
const cookieStorageKey = 'huya-go.gui.cookies.v1'
|
||||
const prizeCacheStorageKey = 'huya-go.gui.prizes.v1'
|
||||
|
||||
type UiConfig = {
|
||||
transport: string
|
||||
@@ -76,6 +83,15 @@ type AccountPreview = {
|
||||
error?: string
|
||||
}
|
||||
|
||||
type CookieEntry = {
|
||||
id: string
|
||||
text: string
|
||||
uid: number
|
||||
valid: boolean
|
||||
error?: string
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
type LogEntry = {
|
||||
time: string
|
||||
level: string
|
||||
@@ -95,6 +111,12 @@ type PrizeInfo = {
|
||||
img?: string
|
||||
}
|
||||
|
||||
type PrizeCache = {
|
||||
prizes: PrizeInfo[]
|
||||
updatedAt: number
|
||||
sid?: number
|
||||
}
|
||||
|
||||
type ScoreResult = {
|
||||
uid: number
|
||||
score: number
|
||||
@@ -227,6 +249,55 @@ function readSavedConfig(): Partial<SavedConfig> {
|
||||
}
|
||||
}
|
||||
|
||||
function readSavedCookies(): CookieEntry[] {
|
||||
try {
|
||||
const items = JSON.parse(localStorage.getItem(cookieStorageKey) || '[]') as Partial<CookieEntry>[]
|
||||
return items
|
||||
.map((item, index) => ({
|
||||
id: item.id || `ck-${Date.now()}-${index}`,
|
||||
text: String(item.text || '').trim(),
|
||||
uid: Number(item.uid || 0),
|
||||
valid: Boolean(item.valid),
|
||||
error: item.error,
|
||||
updatedAt: Number(item.updatedAt || Date.now()),
|
||||
}))
|
||||
.filter((item) => item.text)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function writeSavedCookies(items: CookieEntry[]) {
|
||||
localStorage.setItem(cookieStorageKey, JSON.stringify(items))
|
||||
}
|
||||
|
||||
function readSavedPrizeCache(): PrizeCache {
|
||||
try {
|
||||
const cache = JSON.parse(localStorage.getItem(prizeCacheStorageKey) || '{}') as Partial<PrizeCache>
|
||||
const prizes = Array.isArray(cache.prizes)
|
||||
? cache.prizes.filter((item) => Number(item?.id || 0) > 0 && String(item?.name || '').trim())
|
||||
: []
|
||||
return {
|
||||
prizes: prizes as PrizeInfo[],
|
||||
updatedAt: Number(cache.updatedAt || 0),
|
||||
sid: Number(cache.sid || 0) || undefined,
|
||||
}
|
||||
} catch {
|
||||
return { prizes: [], updatedAt: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
function writeSavedPrizeCache(prizes: PrizeInfo[], sid: number) {
|
||||
localStorage.setItem(
|
||||
prizeCacheStorageKey,
|
||||
JSON.stringify({
|
||||
prizes,
|
||||
sid,
|
||||
updatedAt: Date.now(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function normalizeCookieText(text: string): string {
|
||||
const normalized = text
|
||||
.replace(/\r\n?/g, '\n')
|
||||
@@ -244,9 +315,20 @@ function normalizeCookieText(text: string): string {
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
function cookieTextFromEntries(items: CookieEntry[]): string {
|
||||
return items
|
||||
.map((item) => item.text.trim())
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
function newCookieID(): string {
|
||||
return `ck-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||
}
|
||||
|
||||
function pickConfig(values: Partial<UiConfig>): UiConfig {
|
||||
return {
|
||||
transport: values.transport || 'http',
|
||||
transport: values.transport || 'wss',
|
||||
wssHost: values.wssHost || '',
|
||||
sid: Number(values.sid || 0),
|
||||
pid: Number(values.pid || 0),
|
||||
@@ -309,9 +391,10 @@ function normalizeMonitorResult(row: MonitorResult): MonitorResult {
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const { message } = AntApp.useApp()
|
||||
const { message, modal } = AntApp.useApp()
|
||||
const [form] = Form.useForm<UiConfig>()
|
||||
const previewedCookieText = useRef('')
|
||||
const watchedTransport = Form.useWatch('transport', form)
|
||||
const watchedPID = Form.useWatch('pid', form)
|
||||
const watchedName = Form.useWatch('name', form)
|
||||
const watchedCookieText = Form.useWatch('cookieText', form)
|
||||
@@ -327,19 +410,75 @@ export default function Dashboard() {
|
||||
const [results, setResults] = useState<MonitorResult[]>([])
|
||||
const [logs, setLogs] = useState<LogEntry[]>([])
|
||||
const [now, setNow] = useState(new Date())
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
const [cookieManagerOpen, setCookieManagerOpen] = useState(false)
|
||||
const [cookieEditOpen, setCookieEditOpen] = useState(false)
|
||||
const [editingCookie, setEditingCookie] = useState<CookieEntry | null>(null)
|
||||
const [cookieDraft, setCookieDraft] = useState('')
|
||||
const [cookieEntries, setCookieEntries] = useState<CookieEntry[]>([])
|
||||
const [prizeCacheUpdatedAt, setPrizeCacheUpdatedAt] = useState(0)
|
||||
|
||||
const applyCookieEntries = async (entries: CookieEntry[], persist = true, refreshLatest = false) => {
|
||||
const normalizedEntries = entries
|
||||
.map((entry) => ({
|
||||
...entry,
|
||||
text: normalizeCookieText(entry.text).split('\n')[0] || '',
|
||||
}))
|
||||
.filter((entry) => entry.text)
|
||||
|
||||
const cookieText = cookieTextFromEntries(normalizedEntries)
|
||||
previewedCookieText.current = cookieText
|
||||
form.setFieldsValue({ cookieText })
|
||||
setSelectedPrizeKeys([])
|
||||
|
||||
if (!cookieText) {
|
||||
setCookieEntries([])
|
||||
setAccounts([])
|
||||
setSelectedAccountLines([])
|
||||
if (persist) writeSavedCookies([])
|
||||
return []
|
||||
}
|
||||
|
||||
const previews = (await PreviewAccounts(cookieText)) as AccountPreview[]
|
||||
const enriched = normalizedEntries.map((entry, index) => {
|
||||
const preview = previews.find((item) => item.line === index + 1)
|
||||
return {
|
||||
...entry,
|
||||
uid: preview?.uid || 0,
|
||||
valid: Boolean(preview?.valid),
|
||||
error: preview?.error,
|
||||
}
|
||||
})
|
||||
setCookieEntries(enriched)
|
||||
setAccounts(previews || [])
|
||||
setSelectedAccountLines((previews || []).filter((item) => item.valid).map((item) => item.line))
|
||||
if (persist) writeSavedCookies(enriched)
|
||||
if (refreshLatest && enriched.some((item) => item.valid)) {
|
||||
window.setTimeout(() => queryPrizes(true), 0)
|
||||
}
|
||||
return enriched
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true
|
||||
Promise.all([DefaultConfig(), GetLogs(), IsRunning()])
|
||||
.then(([defaults, logItems, isRunning]) => {
|
||||
if (!mounted) return
|
||||
const savedCookies = readSavedCookies()
|
||||
const savedPrizeCache = readSavedPrizeCache()
|
||||
form.setFieldsValue({
|
||||
...(defaults as UiConfig),
|
||||
...readSavedConfig(),
|
||||
cookieText: '',
|
||||
cookieText: cookieTextFromEntries(savedCookies),
|
||||
})
|
||||
setCookieEntries(savedCookies)
|
||||
setPrizes(savedPrizeCache.prizes)
|
||||
setPrizeCacheUpdatedAt(savedPrizeCache.updatedAt)
|
||||
setLogs((logItems || []) as LogEntry[])
|
||||
setRunning(Boolean(isRunning))
|
||||
if (savedCookies.length > 0) {
|
||||
applyCookieEntries(savedCookies, true, true).catch((err: unknown) => message.error(String(err)))
|
||||
}
|
||||
})
|
||||
.catch((err: unknown) => message.error(String(err)))
|
||||
|
||||
@@ -371,11 +510,14 @@ export default function Dashboard() {
|
||||
if ((watchedCookieText || '') === previewedCookieText.current) return
|
||||
setAccounts([])
|
||||
setSelectedAccountLines([])
|
||||
setPrizes([])
|
||||
setSelectedPrizeKeys([])
|
||||
}, [watchedCookieText])
|
||||
|
||||
const validAccounts = useMemo(() => accounts.filter((item) => item.valid), [accounts])
|
||||
const importedCookieCount = useMemo(
|
||||
() => normalizeCookieText(watchedCookieText || '').split('\n').filter(Boolean).length,
|
||||
[watchedCookieText],
|
||||
)
|
||||
const selectedAccountCount = selectedAccountLines.length
|
||||
const selectedPrizeID =
|
||||
selectedPrizeKeys.length === 1 ? Number(selectedPrizeKeys[0]) || undefined : undefined
|
||||
@@ -425,15 +567,6 @@ export default function Dashboard() {
|
||||
)
|
||||
}, [prizes, productSearch])
|
||||
|
||||
const prizeOptions = useMemo(
|
||||
() =>
|
||||
prizes.map((item) => ({
|
||||
value: item.id,
|
||||
label: `${item.name} · ${item.id} · ${priceOf(item)}积分`,
|
||||
})),
|
||||
[prizes],
|
||||
)
|
||||
|
||||
const config = () => ({
|
||||
...pickConfig(form.getFieldsValue(true)),
|
||||
selectedAccountLines: selectedAccountLines.map(Number).filter((line) => line > 0),
|
||||
@@ -456,12 +589,43 @@ export default function Dashboard() {
|
||||
}
|
||||
}
|
||||
|
||||
const previewAccounts = async (cookieText?: string) => {
|
||||
const text = cookieText ?? form.getFieldValue('cookieText') ?? ''
|
||||
previewedCookieText.current = text
|
||||
const items = (await PreviewAccounts(text)) as AccountPreview[]
|
||||
setAccounts(items || [])
|
||||
setSelectedAccountLines((items || []).filter((item) => item.valid).map((item) => item.line))
|
||||
const upsertCookieLines = async (lines: string[]) => {
|
||||
const normalizedText = normalizeCookieText(lines.join('\n'))
|
||||
const nextLines = normalizedText.split('\n').filter(Boolean)
|
||||
if (nextLines.length === 0) return { added: 0, updated: 0 }
|
||||
|
||||
const previews = (await PreviewAccounts(nextLines.join('\n'))) as AccountPreview[]
|
||||
const nextEntries = [...cookieEntries]
|
||||
let added = 0
|
||||
let updated = 0
|
||||
|
||||
nextLines.forEach((text, index) => {
|
||||
const preview = previews.find((item) => item.line === index + 1)
|
||||
const uid = preview?.uid || 0
|
||||
const valid = Boolean(preview?.valid)
|
||||
const existingIndex =
|
||||
valid && uid > 0
|
||||
? nextEntries.findIndex((item) => item.valid && item.uid === uid)
|
||||
: nextEntries.findIndex((item) => item.text === text)
|
||||
const entry: CookieEntry = {
|
||||
id: existingIndex >= 0 ? nextEntries[existingIndex].id : newCookieID(),
|
||||
text,
|
||||
uid,
|
||||
valid,
|
||||
error: preview?.error,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
if (existingIndex >= 0) {
|
||||
nextEntries[existingIndex] = entry
|
||||
updated += 1
|
||||
} else {
|
||||
nextEntries.push(entry)
|
||||
added += 1
|
||||
}
|
||||
})
|
||||
|
||||
await applyCookieEntries(nextEntries, true, true)
|
||||
return { added, updated }
|
||||
}
|
||||
|
||||
const importCookiesFromClipboard = async () => {
|
||||
@@ -473,9 +637,8 @@ export default function Dashboard() {
|
||||
message.warning('剪贴板没有可导入的 CK')
|
||||
return
|
||||
}
|
||||
form.setFieldsValue({ cookieText })
|
||||
await previewAccounts(cookieText)
|
||||
message.success(`已从剪贴板导入 ${cookieText.split('\n').length} 条 CK`)
|
||||
const result = await upsertCookieLines(cookieText.split('\n'))
|
||||
message.success(`已导入 ${result.added} 条,更新 ${result.updated} 条 CK`)
|
||||
} catch (err: unknown) {
|
||||
message.error(`剪贴板导入失败: ${String(err)}`)
|
||||
} finally {
|
||||
@@ -483,20 +646,120 @@ export default function Dashboard() {
|
||||
}
|
||||
}
|
||||
|
||||
const queryPrizes = () => {
|
||||
if (!String(form.getFieldValue('cookieText') || '').trim()) {
|
||||
message.warning('Cookie 不能为空')
|
||||
const openNewCookie = () => {
|
||||
setEditingCookie(null)
|
||||
setCookieDraft('')
|
||||
setCookieEditOpen(true)
|
||||
}
|
||||
|
||||
const openEditCookie = (entry: CookieEntry) => {
|
||||
setEditingCookie(entry)
|
||||
setCookieDraft(entry.text)
|
||||
setCookieEditOpen(true)
|
||||
}
|
||||
|
||||
const saveCookieDraft = async () => {
|
||||
const normalizedText = normalizeCookieText(cookieDraft)
|
||||
if (!normalizedText) {
|
||||
message.warning('CK 不能为空')
|
||||
return
|
||||
}
|
||||
runAction('list', () => ListPrizes(config()), (data) => {
|
||||
const result = data as { prizes?: PrizeInfo[] }
|
||||
const nextPrizes = result?.prizes || []
|
||||
setPrizes(nextPrizes)
|
||||
setSelectedPrizeKeys((prev) =>
|
||||
prev.filter((key) => nextPrizes.some((item) => item.id === Number(key))),
|
||||
)
|
||||
if (nextPrizes.length === 0) {
|
||||
message.warning('当前 CK 未返回可选商品')
|
||||
setLoading('cookie-save')
|
||||
try {
|
||||
if (editingCookie) {
|
||||
const text = normalizedText.split('\n')[0]
|
||||
const nextEntries = cookieEntries.map((entry) =>
|
||||
entry.id === editingCookie.id ? { ...entry, text, updatedAt: Date.now() } : entry,
|
||||
)
|
||||
await applyCookieEntries(nextEntries, true, true)
|
||||
message.success('CK 已更新')
|
||||
} else {
|
||||
const result = await upsertCookieLines(normalizedText.split('\n'))
|
||||
message.success(`已新增 ${result.added} 条,更新 ${result.updated} 条 CK`)
|
||||
}
|
||||
setCookieEditOpen(false)
|
||||
setCookieDraft('')
|
||||
setEditingCookie(null)
|
||||
} catch (err: unknown) {
|
||||
message.error(String(err))
|
||||
} finally {
|
||||
setLoading('')
|
||||
}
|
||||
}
|
||||
|
||||
const deleteCookie = async (entry: CookieEntry) => {
|
||||
await applyCookieEntries(cookieEntries.filter((item) => item.id !== entry.id))
|
||||
message.success('CK 已删除')
|
||||
}
|
||||
|
||||
const clearCookies = () => {
|
||||
modal.confirm({
|
||||
title: '清空 CK',
|
||||
content: '确认删除所有已保存 CK?',
|
||||
okText: '清空',
|
||||
cancelText: '取消',
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
await applyCookieEntries([])
|
||||
message.success('CK 已清空')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const reloadSavedCookies = async () => {
|
||||
const savedCookies = readSavedCookies()
|
||||
await applyCookieEntries(savedCookies, true, true)
|
||||
message.success('已重新载入本地 CK')
|
||||
}
|
||||
|
||||
function queryPrizes(quiet = false) {
|
||||
if (!String(form.getFieldValue('cookieText') || '').trim()) {
|
||||
if (!quiet) message.warning('Cookie 不能为空')
|
||||
return
|
||||
}
|
||||
setLoading('list')
|
||||
ListPrizes(config())
|
||||
.then((data) => {
|
||||
const result = data as { prizes?: PrizeInfo[] }
|
||||
const nextPrizes = result?.prizes || []
|
||||
if (nextPrizes.length === 0) {
|
||||
if (!quiet) message.warning('当前 CK 未返回可选商品')
|
||||
return
|
||||
}
|
||||
const updatedAt = Date.now()
|
||||
setPrizes(nextPrizes)
|
||||
setPrizeCacheUpdatedAt(updatedAt)
|
||||
writeSavedPrizeCache(nextPrizes, Number(form.getFieldValue('sid') || 0))
|
||||
setSelectedPrizeKeys((prev) =>
|
||||
prev.filter((key) => nextPrizes.some((item) => item.id === Number(key))),
|
||||
)
|
||||
if (!quiet) message.success('商品已刷新')
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (!quiet) message.error(String(err))
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading('')
|
||||
})
|
||||
}
|
||||
|
||||
const productListUpdatedAt = prizeCacheUpdatedAt
|
||||
? new Date(prizeCacheUpdatedAt).toLocaleString('zh-CN', { hour12: false })
|
||||
: ''
|
||||
|
||||
const clearPrizeCache = () => {
|
||||
modal.confirm({
|
||||
title: '清空商品缓存',
|
||||
content: '确认清空本地缓存的商品列表?',
|
||||
okText: '清空',
|
||||
cancelText: '取消',
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => {
|
||||
localStorage.removeItem(prizeCacheStorageKey)
|
||||
setPrizes([])
|
||||
setSelectedPrizeKeys([])
|
||||
setPrizeCacheUpdatedAt(0)
|
||||
message.success('商品缓存已清空')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -546,8 +809,6 @@ export default function Dashboard() {
|
||||
})
|
||||
}
|
||||
|
||||
const choosePrize = (pid?: number) => applyPrizeSelection(pid ? [pid] : [])
|
||||
|
||||
const startMonitor = (scheduled: boolean) => {
|
||||
const values = config()
|
||||
if (!scheduled) values.startAt = ''
|
||||
@@ -621,6 +882,51 @@ export default function Dashboard() {
|
||||
[currentPrize?.name, latestResultByUID, scoreMap, watchedName],
|
||||
)
|
||||
|
||||
const cookieColumns: ColumnsType<CookieEntry> = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: '账号',
|
||||
width: 150,
|
||||
render: (_, row) => (
|
||||
<div>
|
||||
<strong>{row.uid || '-'}</strong>
|
||||
<div className="muted">{row.valid ? '有效 CK' : row.error || '待校验'}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
width: 86,
|
||||
render: (_, row) => (row.valid ? <Tag color="green">有效</Tag> : <Tag color="error">无效</Tag>),
|
||||
},
|
||||
{
|
||||
title: 'CK',
|
||||
ellipsis: true,
|
||||
render: (_, row) => <code className="ck-text-preview">{row.text}</code>,
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
width: 170,
|
||||
render: (_, row) => new Date(row.updatedAt).toLocaleString('zh-CN', { hour12: false }),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 150,
|
||||
render: (_, row) => (
|
||||
<Space size={6}>
|
||||
<Button size="small" icon={<Pencil size={14} />} onClick={() => openEditCookie(row)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button size="small" danger icon={<Trash2 size={14} />} onClick={() => deleteCookie(row)}>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
],
|
||||
[cookieEntries],
|
||||
)
|
||||
|
||||
const prizeColumns: ColumnsType<PrizeInfo> = useMemo(
|
||||
() => [
|
||||
{ title: 'num', dataIndex: 'id', width: 92 },
|
||||
@@ -759,7 +1065,8 @@ export default function Dashboard() {
|
||||
block
|
||||
icon={<RefreshCw size={16} />}
|
||||
loading={loading === 'list'}
|
||||
onClick={queryPrizes}
|
||||
disabled={validAccounts.length === 0}
|
||||
onClick={() => queryPrizes()}
|
||||
>
|
||||
用 CK 刷新商品
|
||||
</Button>
|
||||
@@ -775,15 +1082,15 @@ export default function Dashboard() {
|
||||
<CurrentProduct prize={currentPrize} />
|
||||
|
||||
<div className="inventory-line">
|
||||
<span>实时库存 {filteredPrizes.length} 个</span>
|
||||
<span>{now.toLocaleString('zh-CN', { hour12: false })}</span>
|
||||
<span>商品缓存 {filteredPrizes.length} 个</span>
|
||||
<span>{productListUpdatedAt ? `更新 ${productListUpdatedAt}` : '未缓存'}</span>
|
||||
</div>
|
||||
|
||||
<div className="product-list">
|
||||
{filteredPrizes.length === 0 ? (
|
||||
<div className="product-empty">
|
||||
<strong>未加载 CK 商品</strong>
|
||||
<span>列表为空</span>
|
||||
<strong>暂无商品缓存</strong>
|
||||
<span>导入有效 CK 后刷新最新商品</span>
|
||||
</div>
|
||||
) : (
|
||||
filteredPrizes.map((item) => (
|
||||
@@ -817,19 +1124,14 @@ export default function Dashboard() {
|
||||
<header className="topbar">
|
||||
<div className="tab-strip">
|
||||
<Button type="primary">抢购控制台</Button>
|
||||
<Button>CK 管理</Button>
|
||||
<Button onClick={() => setCookieManagerOpen(true)}>CK 管理</Button>
|
||||
<Button>等待操作</Button>
|
||||
</div>
|
||||
<Space size={16}>
|
||||
<Form.Item name="transport" noStyle>
|
||||
<Select
|
||||
className="transport-select"
|
||||
options={[
|
||||
{ value: 'http', label: 'HTTP' },
|
||||
{ value: 'wss', label: 'WSS' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Space size={12}>
|
||||
<Tag>{String(watchedTransport || 'wss').toUpperCase()}</Tag>
|
||||
<Button icon={<Settings size={16} />} onClick={() => setSettingsOpen(true)}>
|
||||
设置
|
||||
</Button>
|
||||
<Tag color={running ? 'processing' : 'default'}>{running ? '运行中' : '空闲'}</Tag>
|
||||
</Space>
|
||||
</header>
|
||||
@@ -967,19 +1269,142 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="bottom-config">
|
||||
<div>
|
||||
<Typography.Text strong>目标商品</Typography.Text>
|
||||
</main>
|
||||
</div>
|
||||
<Form.Item name="name" hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="targetText" hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="cookieText" hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Modal
|
||||
title="CK 管理"
|
||||
open={cookieManagerOpen}
|
||||
width={980}
|
||||
onCancel={() => setCookieManagerOpen(false)}
|
||||
footer={null}
|
||||
>
|
||||
<div className="ck-manager">
|
||||
<div className="ck-manager-tools">
|
||||
<Space wrap>
|
||||
<Button type="primary" icon={<Plus size={16} />} onClick={openNewCookie}>
|
||||
新增 CK
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ClipboardPaste size={16} />}
|
||||
loading={loading === 'clipboard'}
|
||||
onClick={importCookiesFromClipboard}
|
||||
>
|
||||
剪贴板导入
|
||||
</Button>
|
||||
<Button icon={<RefreshCw size={16} />} onClick={reloadSavedCookies}>
|
||||
重新载入
|
||||
</Button>
|
||||
</Space>
|
||||
<Space wrap>
|
||||
<Tag>{cookieEntries.length} 条 CK</Tag>
|
||||
<Tag color="green">{validAccounts.length} 个有效</Tag>
|
||||
<Button
|
||||
danger
|
||||
icon={<Trash2 size={16} />}
|
||||
disabled={cookieEntries.length === 0}
|
||||
onClick={clearCookies}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
size="small"
|
||||
rowKey={(row) => row.id}
|
||||
columns={cookieColumns}
|
||||
dataSource={cookieEntries}
|
||||
pagination={{ pageSize: 8, showSizeChanger: false }}
|
||||
scroll={{ x: 900, y: 430 }}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
title={editingCookie ? '编辑 CK' : '新增 CK'}
|
||||
open={cookieEditOpen}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
confirmLoading={loading === 'cookie-save'}
|
||||
onOk={saveCookieDraft}
|
||||
onCancel={() => {
|
||||
setCookieEditOpen(false)
|
||||
setCookieDraft('')
|
||||
setEditingCookie(null)
|
||||
}}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={editingCookie ? 5 : 8}
|
||||
spellCheck={false}
|
||||
value={cookieDraft}
|
||||
onChange={(event) => setCookieDraft(event.target.value)}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal
|
||||
title="设置"
|
||||
open={settingsOpen}
|
||||
width={760}
|
||||
onCancel={() => setSettingsOpen(false)}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setSettingsOpen(false)}>
|
||||
关闭
|
||||
</Button>,
|
||||
<Button
|
||||
key="save"
|
||||
type="primary"
|
||||
icon={<Save size={16} />}
|
||||
onClick={() => {
|
||||
saveSettings()
|
||||
setSettingsOpen(false)
|
||||
}}
|
||||
>
|
||||
保存
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div className="settings-modal">
|
||||
<div className="settings-grid">
|
||||
<Form.Item name="transport" label="传输">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
className="target-select"
|
||||
optionFilterProp="label"
|
||||
value={selectedPrizeID}
|
||||
options={prizeOptions}
|
||||
onChange={choosePrize}
|
||||
options={[
|
||||
{ value: 'wss', label: 'WSS' },
|
||||
{ value: 'http', label: 'HTTP' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="wssHost" label="WSS Host">
|
||||
<Input allowClear />
|
||||
</Form.Item>
|
||||
<div className="settings-summary settings-wide">
|
||||
<Typography.Text type="secondary">CK 来源</Typography.Text>
|
||||
<strong>
|
||||
{importedCookieCount > 0
|
||||
? `已导入 ${validAccounts.length}/${importedCookieCount} 个有效账号`
|
||||
: '未导入 CK'}
|
||||
</strong>
|
||||
</div>
|
||||
<div className="settings-summary settings-wide">
|
||||
<Typography.Text type="secondary">商品缓存</Typography.Text>
|
||||
<Space size={8}>
|
||||
<strong>
|
||||
{prizes.length > 0
|
||||
? `${prizes.length} 个 · ${productListUpdatedAt || '未知时间'}`
|
||||
: '未缓存'}
|
||||
</strong>
|
||||
<Button size="small" disabled={prizes.length === 0} onClick={clearPrizeCache}>
|
||||
清空
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-param-grid">
|
||||
<Form.Item name="sid" label="SID">
|
||||
<InputNumber min={1} />
|
||||
</Form.Item>
|
||||
@@ -998,44 +1423,29 @@ export default function Dashboard() {
|
||||
<Form.Item name="leadMs" label="提前">
|
||||
<InputNumber min={0} max={10000} addonAfter="ms" />
|
||||
</Form.Item>
|
||||
</section>
|
||||
|
||||
<section className="advanced-config">
|
||||
<Form.Item name="name" label="名称">
|
||||
<Input allowClear />
|
||||
</div>
|
||||
<div className="settings-switch-grid">
|
||||
<Form.Item name="prewarm" label="预热" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="targetText" label="多目标">
|
||||
<Input.TextArea rows={2} spellCheck={false} />
|
||||
<Form.Item name="directExchange" label="直兑" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="wssHost" label="WSS Host">
|
||||
<Input allowClear />
|
||||
<Form.Item name="sharePrizeList" label="共享列表" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="cookieText" label="Cookie">
|
||||
<Input.TextArea rows={3} spellCheck={false} className="cookie-box" />
|
||||
<Form.Item name="autoSelect" label="自动选品" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<div className="switch-grid">
|
||||
<Form.Item name="prewarm" label="预热" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="directExchange" label="直兑" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="sharePrizeList" label="共享列表" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="autoSelect" label="自动选品" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="stopAccountAfterSuccess" label="账号成功停" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="refreshAfterSuccess" label="成功刷新" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
<Form.Item name="stopAccountAfterSuccess" label="账号成功停" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="refreshAfterSuccess" label="成功刷新" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
+77
-38
@@ -165,7 +165,7 @@ body {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
grid-template-rows: 64px 48px 72px minmax(0, 1fr) auto auto;
|
||||
grid-template-rows: 64px 48px 72px minmax(0, 1fr);
|
||||
background: #f7fafc;
|
||||
}
|
||||
|
||||
@@ -184,10 +184,6 @@ body {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.transport-select {
|
||||
width: 96px;
|
||||
}
|
||||
|
||||
.statusbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -329,9 +325,7 @@ body {
|
||||
padding: 8px 2px;
|
||||
}
|
||||
|
||||
.stats-pane .ant-statistic,
|
||||
.bottom-config,
|
||||
.advanced-config {
|
||||
.stats-pane .ant-statistic {
|
||||
border: 1px solid #e6edf3;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
@@ -341,43 +335,87 @@ body {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.bottom-config {
|
||||
.settings-modal {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 1fr) repeat(6, max-content);
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
margin: 0 20px 10px;
|
||||
padding: 10px 12px;
|
||||
gap: 16px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.bottom-config .ant-form-item,
|
||||
.advanced-config .ant-form-item {
|
||||
.ck-manager {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ck-manager-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ck-text-preview {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
color: #344054;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.settings-wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.settings-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 38px;
|
||||
border: 1px solid #e6edf3;
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.settings-grid .ant-form-item,
|
||||
.settings-param-grid .ant-form-item,
|
||||
.settings-switch-grid .ant-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.target-select {
|
||||
width: 100%;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.advanced-config {
|
||||
display: grid;
|
||||
grid-template-columns: 180px minmax(220px, 0.8fr) minmax(220px, 1fr) minmax(320px, 1.2fr);
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
margin: 0 20px 14px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.switch-grid {
|
||||
.settings-param-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cookie-box textarea,
|
||||
.cookie-box {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
.settings-param-grid .ant-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-switch-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.settings-switch-grid .ant-form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 38px;
|
||||
border: 1px solid #e6edf3;
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.settings-switch-grid .ant-form-item-row {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ant-table-wrapper {
|
||||
@@ -400,9 +438,10 @@ body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.bottom-config,
|
||||
.advanced-config {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
.settings-grid,
|
||||
.settings-param-grid,
|
||||
.settings-switch-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -125,7 +125,7 @@ func (a *App) Shutdown(ctx context.Context) {
|
||||
|
||||
func (a *App) DefaultConfig() Config {
|
||||
return Config{
|
||||
Transport: "http",
|
||||
Transport: "wss",
|
||||
WSSHost: huya.DefaultWSSHost,
|
||||
SID: 2203,
|
||||
Loops: 20,
|
||||
|
||||
Reference in New Issue
Block a user