feat: 故障推送 / 公需课自动重学 / 插件解耦,发版 v0.0.8
体检修复:启动日志不再打印 AUTH_TOKEN、密钥可清空、推送失败不占整段冷却、目录名与文案统一成 plugins/。
This commit is contained in:
+63
-17
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Plus, RefreshCw, Users, WifiOff } from 'lucide-react'
|
||||
|
||||
import { AccountCard } from './components/AccountCard'
|
||||
import { AccountEditDialog, type AccountEdit } from './components/AccountEditDialog'
|
||||
import { AddAccountDialog, type NewAccount } from './components/AddAccountDialog'
|
||||
import { PluginsDialog } from './components/PluginsDialog'
|
||||
import { QuizDialog } from './components/QuizDialog'
|
||||
@@ -11,6 +12,7 @@ import { SystemLogDialog } from './components/SystemLogDialog'
|
||||
import { Toast, type ToastState } from './components/Toast'
|
||||
import { TopBar } from './components/TopBar'
|
||||
import { Button } from './components/ui/Button'
|
||||
import { useConfirm } from './components/ui/ConfirmDialog'
|
||||
import { ApiError, api } from './lib/api'
|
||||
import { countCourses, toastMessage } from './lib/utils'
|
||||
import type { Account, Cookie, Course, LogResponse, PlatformInfo, RunState } from './types'
|
||||
@@ -48,6 +50,7 @@ interface InlineLog {
|
||||
|
||||
type DialogState =
|
||||
| { type: 'add' }
|
||||
| { type: 'edit'; accountId: string }
|
||||
| { type: 'settings' }
|
||||
| { type: 'plugins' }
|
||||
| { type: 'system' }
|
||||
@@ -72,12 +75,11 @@ function persistOpenLogs(ids: Set<string>) {
|
||||
// 隐私模式禁用 localStorage 时忽略
|
||||
}
|
||||
}
|
||||
|
||||
function accountPayload(account: Account) {
|
||||
return {
|
||||
id: account.id,
|
||||
username: account.username,
|
||||
platform: account.platform || 'guanyixue',
|
||||
platform: account.platform || '',
|
||||
note: account.note || '',
|
||||
user: account.user || '',
|
||||
cookies: account.cookies || [],
|
||||
@@ -95,10 +97,8 @@ function accountPayload(account: Account) {
|
||||
export default function App() {
|
||||
const [accounts, setAccounts] = useState<Account[]>([])
|
||||
const accountsRef = useRef(accounts)
|
||||
const [platforms, setPlatforms] = useState<Record<string, string>>({ guanyixue: '莞易学' })
|
||||
const [platformList, setPlatformList] = useState<PlatformInfo[]>([
|
||||
{ id: 'guanyixue', label: '莞易学' },
|
||||
])
|
||||
const [platforms, setPlatforms] = useState<Record<string, string>>({})
|
||||
const [platformList, setPlatformList] = useState<PlatformInfo[]>([])
|
||||
const [offline, setOffline] = useState(false)
|
||||
const [toast, setToast] = useState<ToastState | null>(null)
|
||||
const [pending, setPending] = useState<Record<string, string>>({})
|
||||
@@ -109,6 +109,7 @@ export default function App() {
|
||||
const [logs, setLogs] = useState<Record<string, InlineLog>>({})
|
||||
const logsRef = useRef(logs)
|
||||
const toastTimer = useRef<number | undefined>(undefined)
|
||||
const { ask: askConfirm, dialog: confirmDialog } = useConfirm()
|
||||
|
||||
useEffect(() => {
|
||||
accountsRef.current = accounts
|
||||
@@ -166,6 +167,10 @@ export default function App() {
|
||||
setOffline(false)
|
||||
|
||||
for (const account of next) {
|
||||
// 只同步「正在跑」的那一轮:运行结束后 run 里留的是开跑时的旧会话,
|
||||
// 再回写会把刚登录的新 cookie 盖回失效的那一份,刷新课程就会一直重登。
|
||||
const status = account.run?.status
|
||||
if (status !== 'discovering' && status !== 'studying') continue
|
||||
const cookies = account.run?.cookies
|
||||
if (
|
||||
cookies &&
|
||||
@@ -383,7 +388,7 @@ export default function App() {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
cookies: account.cookies,
|
||||
platform: account.platform || 'guanyixue',
|
||||
platform: account.platform || '',
|
||||
}),
|
||||
})
|
||||
if (!response.ok && response.code === 'session_expired') {
|
||||
@@ -393,7 +398,11 @@ export default function App() {
|
||||
if (!latest) return
|
||||
response = await api<CoursesResponse>('/api/courses', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ cookies: latest.cookies }),
|
||||
body: JSON.stringify({
|
||||
cookies: latest.cookies,
|
||||
// 重试也要带平台:漏了它后端会拿到空平台,报「平台「」还没有实现刷新课程」
|
||||
platform: latest.platform || account.platform || '',
|
||||
}),
|
||||
})
|
||||
}
|
||||
if (!response.ok) {
|
||||
@@ -529,9 +538,31 @@ export default function App() {
|
||||
})
|
||||
}
|
||||
|
||||
async function saveAccountEdit(account: Account, data: AccountEdit) {
|
||||
const updated: Account = {
|
||||
...account,
|
||||
username: data.username,
|
||||
note: data.note,
|
||||
platform: data.platform,
|
||||
}
|
||||
const response = await api<{ ok?: boolean; error?: string }>('/api/accounts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ...accountPayload(updated), password: data.password }),
|
||||
})
|
||||
if (response.ok === false) throw new Error(response.error || '保存失败')
|
||||
replaceAccount(updated)
|
||||
notify({ message: '账号信息已保存,正在用新凭据重新登录…' })
|
||||
// 先让弹窗关掉,重新登录在后台跑:失败了会走 toast + 卡片红条提示
|
||||
void login(updated)
|
||||
}
|
||||
|
||||
async function removeAccount(account: Account) {
|
||||
if (!window.confirm(`删除账号「${account.user || account.username}」?课程缓存和日志不会自动清理。`))
|
||||
return
|
||||
const ok = await askConfirm({
|
||||
title: '删除账号',
|
||||
message: `删除「${account.user || account.username}」?课程缓存和日志不会自动清理,删掉后要重新添加账号才能继续学习。`,
|
||||
confirmText: '删除账号',
|
||||
})
|
||||
if (!ok) return
|
||||
try {
|
||||
await api(`/api/accounts/${encodeURIComponent(account.id)}`, { method: 'DELETE' })
|
||||
setAccounts((previous) => {
|
||||
@@ -642,9 +673,17 @@ export default function App() {
|
||||
function platformLabel(account: Account) {
|
||||
const id =
|
||||
account.platform || (account.courses || []).find((course) => course.platform)?.platform
|
||||
return (id && platforms[id]) || '莞易学'
|
||||
return (id && platforms[id]) || id || '未选择平台'
|
||||
}
|
||||
|
||||
function platformCaps(account: Account) {
|
||||
return platformList.find((item) => item.id === account.platform)?.capabilities || []
|
||||
}
|
||||
|
||||
const editTarget =
|
||||
dialog?.type === 'edit'
|
||||
? accounts.find((account) => account.id === dialog.accountId) || null
|
||||
: null
|
||||
const quizAccount =
|
||||
dialog?.type === 'quiz'
|
||||
? accounts.find((account) => account.id === dialog.accountId) || null
|
||||
@@ -686,9 +725,6 @@ export default function App() {
|
||||
<div className="mb-3 mt-6 flex items-end justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-[14px] font-semibold text-slate-900">账号列表</h2>
|
||||
<p className="mt-0.5 text-[11.5px] text-slate-400">
|
||||
新旧版课程自动识别为「莞易学」,每 5 秒自动刷新状态
|
||||
</p>
|
||||
</div>
|
||||
{accounts.length > 0 && (
|
||||
<Button
|
||||
@@ -710,8 +746,7 @@ export default function App() {
|
||||
</div>
|
||||
<h3 className="mt-4 text-[14px] font-semibold text-slate-800">还没有账号</h3>
|
||||
<p className="mt-1.5 max-w-md text-[12.5px] leading-relaxed text-slate-500">
|
||||
添加东莞市教师培训平台账号后,可自动读取课程、勾选学习、提交反馈表与测验。
|
||||
新旧版课程页会统一识别为「莞易学」。
|
||||
添加平台账号后,可自动读取课程、勾选学习、提交反馈表与测验。
|
||||
</p>
|
||||
<Button
|
||||
variant="primary"
|
||||
@@ -729,6 +764,7 @@ export default function App() {
|
||||
key={account.id}
|
||||
account={account}
|
||||
platformLabel={platformLabel(account)}
|
||||
capabilities={platformCaps(account)}
|
||||
pending={pending[account.id]}
|
||||
openLog={openLogs.has(account.id)}
|
||||
logLines={logs[account.id]?.lines || []}
|
||||
@@ -739,6 +775,7 @@ export default function App() {
|
||||
onStop={() => void stopStudy(account)}
|
||||
onCapture={() => void capture(account)}
|
||||
onStopCapture={() => void stopCapture(account)}
|
||||
onEdit={() => setDialog({ type: 'edit', accountId: account.id })}
|
||||
onDelete={() => void removeAccount(account)}
|
||||
onQuiz={() => {
|
||||
const list = (account.courses || []).filter((course) => course.in_study !== false)
|
||||
@@ -765,7 +802,7 @@ export default function App() {
|
||||
</main>
|
||||
|
||||
<footer className="border-t border-line/70 py-5 text-center text-[11px] text-slate-400">
|
||||
莞易学 · 多账号学习控制台 · 数据保存在本机
|
||||
多账号学习控制台 · 数据保存在本机
|
||||
</footer>
|
||||
|
||||
<AddAccountDialog
|
||||
@@ -774,6 +811,13 @@ export default function App() {
|
||||
onClose={() => setDialog(null)}
|
||||
onSubmit={addAccount}
|
||||
/>
|
||||
<AccountEditDialog
|
||||
open={dialog?.type === 'edit' && !!editTarget}
|
||||
account={editTarget}
|
||||
platforms={platformList}
|
||||
onClose={() => setDialog(null)}
|
||||
onSubmit={(data) => (editTarget ? saveAccountEdit(editTarget, data) : Promise.resolve())}
|
||||
/>
|
||||
<SettingsDialog
|
||||
open={dialog?.type === 'settings'}
|
||||
onClose={() => setDialog(null)}
|
||||
@@ -795,6 +839,8 @@ export default function App() {
|
||||
onSubmitted={() => void load()}
|
||||
/>
|
||||
|
||||
{confirmDialog}
|
||||
|
||||
<Toast toast={toast} />
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user