diff --git a/.gitignore b/.gitignore index e49275a..e20482a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .env state/ logs/ +output/ proxy/config.yaml *.bak-* *.tar.gz diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a7fc51..b763a3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 2026-05-30 +- Redesigned the Web UI into dedicated overview, login workspace, accounts, send console, logs, and settings views. +- Moved the console styling and interaction scripts into `DouYinSparkFlow/webui/static/` with a denser admin-console layout and mobile drawer navigation. +- Added safer confirmations for manual operations and localized the main Web UI feedback messages. - Added a project disclaimer to the root README. - Added a Linux Do friendly link and GitHub star history chart. - Refined the root README wording and structure for an open source project style. diff --git a/DouYinSparkFlow/webui/app.py b/DouYinSparkFlow/webui/app.py index 7efcf50..036ae5c 100644 --- a/DouYinSparkFlow/webui/app.py +++ b/DouYinSparkFlow/webui/app.py @@ -237,6 +237,14 @@ def create_app(): return redirect("/login") return None + def console_context(request): + return { + "flash": pop_flash(request), + "accounts": get_userData(force_reload=True), + "runtime_config": get_config(force_reload=True), + "ops": get_ops_snapshot(), + } + def flash(request, message, level="info"): request.session["flash"] = {"message": message, "level": level} @@ -259,7 +267,7 @@ def create_app(): @app.post("/bootstrap") async def bootstrap(request: Request): if is_bootstrapped(): - flash(request, "Admin login is already configured.", "warning") + flash(request, "管理员账号已初始化,请直接登录。", "warning") return redirect("/login") form = await request.form() @@ -267,17 +275,17 @@ def create_app(): password = str(form.get("password", "")) confirm = str(form.get("confirm_password", "")) if not password or password != confirm: - flash(request, "Password setup failed. Please enter matching passwords.", "error") + flash(request, "初始化失败,请输入一致的管理员密码。", "error") return redirect("/login") bootstrap_admin_password(password, username=username) - flash(request, "Admin credentials created. Please log in.", "success") + flash(request, "管理员账号已创建,请登录控制台。", "success") return redirect("/login") @app.post("/login") async def login_action(request: Request): if not is_bootstrapped(): - flash(request, "Create the admin password first.", "warning") + flash(request, "请先创建管理员密码。", "warning") return redirect("/login") form = await request.form() @@ -285,11 +293,11 @@ def create_app(): password = str(form.get("password", "")) settings = get_app_settings(force_reload=True) if username != settings["admin_username"] or not verify_password(password, settings["admin_password_hash"]): - flash(request, "Invalid username or password.", "error") + flash(request, "用户名或密码不正确。", "error") return redirect("/login") issue_session(request, username) - flash(request, "Signed in successfully.", "success") + flash(request, "已登录控制台。", "success") return redirect("/") @app.post("/logout") @@ -306,12 +314,43 @@ def create_app(): return render_template( request, "dashboard.html", - { - "flash": pop_flash(request), - "accounts": get_userData(force_reload=True), - "runtime_config": get_config(force_reload=True), - "ops": get_ops_snapshot(), - }, + console_context(request), + ) + + @app.get("/login-workspace", response_class=HTMLResponse) + async def login_workspace_page(request: Request): + maybe_redirect = require_user(request) + if maybe_redirect: + return maybe_redirect + + return render_template( + request, + "login_workspace.html", + console_context(request), + ) + + @app.get("/accounts", response_class=HTMLResponse) + async def accounts_page(request: Request): + maybe_redirect = require_user(request) + if maybe_redirect: + return maybe_redirect + + return render_template( + request, + "accounts.html", + console_context(request), + ) + + @app.get("/settings", response_class=HTMLResponse) + async def settings_page(request: Request): + maybe_redirect = require_user(request) + if maybe_redirect: + return maybe_redirect + + return render_template( + request, + "settings.html", + console_context(request), ) @app.get("/ops/send-console", response_class=HTMLResponse) @@ -349,11 +388,11 @@ def create_app(): account["targets"] = targets account["enabled"] = str(form.get("enabled", "")) == "on" save_userData(accounts) - flash(request, f"Updated account {account['username']}.", "success") + flash(request, f"已更新账号 {account['username']}。", "success") else: - flash(request, "Account not found.", "error") + flash(request, "未找到账号。", "error") - return redirect("/") + return redirect("/accounts") @app.post("/accounts/{unique_id}/toggle-enabled") async def toggle_account_enabled(request: Request, unique_id: str): @@ -368,8 +407,8 @@ def create_app(): accounts = get_userData(force_reload=True) account = find_account(accounts, unique_id) if not account: - flash(request, "Account not found.", "error") - return redirect("/") + flash(request, "未找到账号。", "error") + return redirect("/accounts") account["enabled"] = not is_account_enabled(account) save_userData(accounts) @@ -378,7 +417,7 @@ def create_app(): f"{account.get('username', 'Account')} 已{'启用' if account['enabled'] else '停用'}自动续火花。", "success", ) - return redirect("/") + return redirect("/accounts") @app.post("/accounts/{unique_id}/friends/refresh") async def refresh_account_friend_list(request: Request, unique_id: str): @@ -424,10 +463,10 @@ def create_app(): updated_accounts = [item for item in accounts if normalize_unique_id(item.get("unique_id")) != normalize_unique_id(unique_id)] if len(updated_accounts) != len(accounts): save_userData(updated_accounts) - flash(request, "Account deleted.", "success") + flash(request, "账号已删除。", "success") else: - flash(request, "Account not found.", "error") - return redirect("/") + flash(request, "未找到账号。", "error") + return redirect("/accounts") @app.post("/accounts/{unique_id}/retry-target") async def retry_account_target(request: Request, unique_id: str): @@ -441,13 +480,13 @@ def create_app(): target_name = str(form.get("target", "")).strip() if not target_name: - flash(request, "Target is required for retry.", "error") + flash(request, "请选择需要重试的目标。", "error") return redirect("/ops/send-console") accounts = get_userData(force_reload=True) account = find_account(accounts, unique_id) if not account: - flash(request, "Account not found.", "error") + flash(request, "未找到账号。", "error") return redirect("/ops/send-console") account_copy = dict(account) @@ -458,16 +497,16 @@ def create_app(): try: await run_browser_tasks(config, [account_copy]) except Exception as exc: - flash(request, f"Retry failed for {account.get('username', 'Account')} / {target_name}: {exc}", "error") + flash(request, f"{account.get('username', '账号')} / {target_name} 重试失败:{exc}", "error") return redirect("/ops/send-console") updated_account = find_account(get_userData(force_reload=True), unique_id) or {} if _target_sent_today(updated_account, target_name): - flash(request, f"Retried {account.get('username', 'Account')} / {target_name} successfully.", "success") + flash(request, f"{account.get('username', '账号')} / {target_name} 已重试成功。", "success") else: failure_entry = dict(updated_account.get("failure_queue") or {}).get(target_name) or {} - reason = str(failure_entry.get("reason") or "Retry did not confirm a successful send.") - flash(request, f"Retry did not succeed for {account.get('username', 'Account')} / {target_name}: {reason}", "error") + reason = str(failure_entry.get("reason") or "重试未确认发送成功。") + flash(request, f"{account.get('username', '账号')} / {target_name} 重试未成功:{reason}", "error") return redirect("/ops/send-console") @app.post("/config") @@ -533,8 +572,8 @@ def create_app(): config["happyNewYear"] = happy_new_year save_config(config) - flash(request, "Runtime config saved.", "success") - return redirect("/") + flash(request, "运行配置已保存。", "success") + return redirect("/settings") @app.post("/settings") async def save_panel_settings(request: Request): @@ -566,12 +605,12 @@ def create_app(): confirm_password = str(form.get("confirm_password", "")) if new_password: if new_password != confirm_password: - flash(request, "Admin password was not updated because the confirmation did not match.", "error") - return redirect("/") + flash(request, "管理员密码未更新:两次输入不一致。", "error") + return redirect("/settings") update_admin_password(new_password) - flash(request, "Panel settings saved.", "success") - return redirect("/") + flash(request, "面板与服务设置已保存。", "success") + return redirect("/settings") @app.post("/ops/run-now") async def run_now(request: Request): @@ -585,10 +624,10 @@ def create_app(): pid = run_task_now() if pid == -1: - flash(request, "Failed to start the full resend run. Check server logs for details.", "error") + flash(request, "补发全部对象启动失败,请查看服务日志。", "error") else: - flash(request, f"Triggered a full resend run in the background (pid {pid}).", "success") - return redirect("/") + flash(request, f"已启动补发全部对象任务(pid {pid})。", "success") + return redirect("/ops/send-console") @app.post("/ops/run-unsent") async def run_unsent(request: Request): @@ -602,9 +641,9 @@ def create_app(): pid = run_unsent_retry_now() if pid == -1: - flash(request, "Failed to start the unsent-target fallback run. Check server logs for details.", "error") + flash(request, "补发未成功目标启动失败,请查看服务日志。", "error") else: - flash(request, f"Triggered an unsent-target fallback run in the background (pid {pid}).", "success") + flash(request, f"已启动补发未成功目标任务(pid {pid})。", "success") return redirect("/ops/send-console") @app.post("/ops/proxy/refresh") @@ -618,8 +657,8 @@ def create_app(): return Response("Invalid CSRF token", status_code=403) refresh_proxy() - flash(request, "Proxy subscription refreshed.", "success") - return redirect("/") + flash(request, "代理订阅已刷新。", "success") + return redirect("/settings") @app.post("/ops/proxy/restart") async def proxy_restart(request: Request): @@ -632,8 +671,8 @@ def create_app(): return Response("Invalid CSRF token", status_code=403) restart_proxy() - flash(request, "Proxy container restarted.", "success") - return redirect("/") + flash(request, "代理容器已重启。", "success") + return redirect("/settings") @app.post("/ops/schedule") async def save_schedule(request: Request): @@ -648,10 +687,10 @@ def create_app(): time_string = str(form.get("daily_schedule", "")).strip() result = update_daily_schedule(time_string) if getattr(result, "returncode", 1) == 0: - flash(request, f"Updated the daily schedule to {time_string}.", "success") + flash(request, f"发送窗口已更新为 {time_string}。", "success") else: - flash(request, f"Failed to update the daily schedule to {time_string}: {getattr(result, 'stderr', '')}", "error") - return redirect("/") + flash(request, f"发送窗口更新失败:{getattr(result, 'stderr', '')}", "error") + return redirect("/settings") @app.get("/ops/logs", response_class=HTMLResponse) async def logs_page(request: Request): diff --git a/DouYinSparkFlow/webui/static/app.css b/DouYinSparkFlow/webui/static/app.css new file mode 100644 index 0000000..39fc7ff --- /dev/null +++ b/DouYinSparkFlow/webui/static/app.css @@ -0,0 +1,1177 @@ +:root { + --bg: #f5f7fb; + --surface: #ffffff; + --surface-muted: #f8fafc; + --surface-soft: #f1f5f9; + --border: #e2e8f0; + --border-strong: #cbd5e1; + --text: #0f172a; + --text-muted: #64748b; + --text-soft: #94a3b8; + --primary: #2563eb; + --primary-hover: #1d4ed8; + --primary-soft: #eff6ff; + --success: #16a34a; + --success-soft: #ecfdf3; + --danger: #dc2626; + --danger-soft: #fef2f2; + --warning: #d97706; + --warning-soft: #fff7ed; + --info: #0284c7; + --info-soft: #eff6ff; + --shadow: 0 14px 34px rgba(15, 23, 42, 0.07); + --shadow-soft: 0 8px 22px rgba(15, 23, 42, 0.05); + --radius-lg: 18px; + --radius-md: 14px; + --radius-sm: 10px; + --sidebar-width: 248px; +} + +* { + box-sizing: border-box; +} + +html, +body { + min-height: 100%; + margin: 0; +} + +body { + color: var(--text); + background: linear-gradient(180deg, #f8fafc 0%, var(--bg) 100%); + font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; + font-size: 14px; +} + +a { + color: inherit; + text-decoration: none; +} + +button, +input, +select, +textarea { + font: inherit; +} + +button { + border: 0; +} + +.app-shell { + display: grid; + grid-template-columns: var(--sidebar-width) minmax(0, 1fr); + min-height: 100vh; +} + +.sidebar { + position: sticky; + top: 0; + height: 100vh; + padding: 22px 16px; + border-right: 1px solid var(--border); + background: rgba(255, 255, 255, 0.92); + display: flex; + flex-direction: column; + gap: 24px; + z-index: 20; +} + +.brand, +.mobile-brand { + display: flex; + align-items: center; + gap: 12px; +} + +.brand-mark { + width: 34px; + height: 34px; + border-radius: 10px; + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--primary); + color: #fff; + font-weight: 800; + box-shadow: 0 10px 20px rgba(37, 99, 235, 0.2); +} + +.brand-title { + font-size: 18px; + font-weight: 800; +} + +.brand-subtitle, +.sidebar-footer-subtitle { + margin-top: 3px; + color: var(--text-muted); + font-size: 12px; +} + +.nav-groups { + display: grid; + gap: 18px; +} + +.nav-group-title { + margin: 0 10px 8px; + color: var(--text-soft); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.nav-group { + display: grid; + gap: 6px; +} + +.nav-item { + min-height: 42px; + border-radius: 12px; + padding: 10px 12px; + color: var(--text-muted); + display: flex; + align-items: center; + gap: 10px; + transition: + background 0.16s ease, + color 0.16s ease; +} + +.nav-item:hover { + background: var(--surface-muted); + color: var(--text); +} + +.nav-item.active { + background: var(--primary-soft); + color: var(--primary); + font-weight: 800; +} + +.nav-icon { + width: 18px; + text-align: center; + flex: 0 0 18px; +} + +.sidebar-footer { + margin-top: auto; + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 14px; + background: var(--surface-muted); +} + +.sidebar-footer-title { + font-weight: 800; +} + +.main-area { + min-width: 0; + padding: 28px; +} + +.page-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 20px; + margin-bottom: 24px; +} + +.eyebrow { + color: var(--primary); + font-size: 12px; + font-weight: 800; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.page-title-row { + display: flex; + align-items: baseline; + flex-wrap: wrap; + gap: 12px; + margin-top: 6px; +} + +.page-title { + margin: 0; + font-size: 30px; + line-height: 1.15; + letter-spacing: 0; +} + +.page-subtitle { + margin: 0; + color: var(--text-muted); + font-weight: 600; +} + +.page-header-actions { + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + gap: 10px; +} + +.page-body, +.stack, +.dashboard-shell, +.console-shell, +.account-list, +.settings-stack { + display: grid; + gap: 18px; +} + +.mobile-topbar, +.sidebar-backdrop { + display: none; +} + +.panel, +.data-card, +.account-card, +.quick-card, +.empty-state { + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: var(--surface); + box-shadow: var(--shadow-soft); +} + +.panel { + padding: 22px; +} + +.section-title-row, +.panel-header, +.card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 18px; +} + +h2, +h3, +h4, +p { + margin-top: 0; +} + +h2 { + margin-bottom: 4px; + font-size: 22px; +} + +h3 { + margin-bottom: 4px; + font-size: 16px; +} + +.muted { + color: var(--text-muted); +} + +.compact { + margin-bottom: 0; + font-size: 13px; + line-height: 1.6; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 14px; +} + +.stat-card { + min-height: 112px; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--surface); + padding: 18px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.stat-meta { + min-width: 0; + display: grid; + gap: 7px; +} + +.stat-label { + color: var(--text-muted); + font-size: 13px; + font-weight: 700; +} + +.stat-value { + font-size: 28px; + line-height: 1; + font-weight: 850; +} + +.stat-help { + color: var(--text-muted); + font-size: 12px; +} + +.stat-icon { + width: 42px; + height: 42px; + border-radius: 12px; + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 42px; + font-weight: 900; +} + +.stat-icon.blue { + color: var(--primary); + background: var(--primary-soft); +} + +.stat-icon.green { + color: var(--success); + background: var(--success-soft); +} + +.stat-icon.red { + color: var(--danger); + background: var(--danger-soft); +} + +.stat-icon.orange { + color: var(--warning); + background: var(--warning-soft); +} + +.stat-icon.gray { + color: var(--text-muted); + background: var(--surface-soft); +} + +.layout-grid, +.dashboard-layout, +.settings-layout { + display: grid; + grid-template-columns: minmax(0, 1fr) 360px; + gap: 18px; + align-items: start; +} + +.content-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; +} + +.quick-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; +} + +.quick-card { + padding: 16px; + display: grid; + gap: 12px; +} + +.quick-card strong { + font-size: 16px; +} + +.link-list { + display: grid; + gap: 10px; +} + +.link-list > a, +.link-list > button, +.link-list > form > button { + width: 100%; +} + +.summary-list { + display: grid; + gap: 12px; +} + +.summary-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 14px; + align-items: center; +} + +.summary-row strong { + display: block; +} + +.summary-row span { + color: var(--text-muted); + font-size: 12px; +} + +.button-row, +.button-grid, +.form-actions { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 10px; +} + +.button-grid { + align-items: stretch; +} + +.button-grid > form, +.button-grid > a, +.button-grid > button { + flex: 1 1 150px; +} + +.space-below-sm { + margin-bottom: 12px; +} + +.space-above-md { + margin-top: 14px; +} + +button, +.link-button, +.ghost-button, +.soft-button, +.danger-button, +.success-button { + min-height: 40px; + border-radius: 10px; + padding: 10px 14px; + background: var(--primary); + color: #fff; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + font-weight: 800; + white-space: nowrap; + transition: + background 0.16s ease, + border-color 0.16s ease, + color 0.16s ease; +} + +button:hover, +.link-button:hover, +.ghost-button:hover, +.soft-button:hover, +.danger-button:hover, +.success-button:hover { + background: var(--primary-hover); +} + +.ghost-button, +.soft-button, +.danger-button, +.success-button { + border: 1px solid var(--border); + box-shadow: none; +} + +.ghost-button { + color: var(--text); + background: #fff; +} + +.ghost-button:hover { + background: var(--surface-muted); +} + +.soft-button { + color: var(--primary); + background: var(--primary-soft); + border-color: #bfdbfe; +} + +.soft-button:hover { + color: #fff; + background: var(--primary); +} + +.danger-button { + color: var(--danger); + background: var(--danger-soft); + border-color: #fecaca; +} + +.danger-button:hover { + color: #fff; + background: var(--danger); +} + +.success-button { + color: var(--success); + background: var(--success-soft); + border-color: #bbf7d0; +} + +.success-button:hover { + color: #fff; + background: var(--success); +} + +.icon-button, +.copy-button { + width: 40px; + min-width: 40px; + padding: 0; +} + +.stack-form { + display: grid; + gap: 14px; +} + +.form-grid, +.settings-grid, +.metric-row, +.account-metrics { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.metric-row, +.account-metrics { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +label { + display: grid; + gap: 7px; +} + +label span, +.field-label { + color: var(--text-muted); + font-size: 13px; + font-weight: 700; +} + +input[type="text"], +input[type="password"], +input[type="search"], +input[type="number"], +textarea, +select { + width: 100%; + min-height: 40px; + border: 1px solid var(--border-strong); + border-radius: 10px; + background: #fff; + color: var(--text); + padding: 10px 12px; + outline: none; +} + +input:focus, +textarea:focus, +select:focus { + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12); +} + +textarea { + min-height: 96px; + resize: vertical; +} + +.check-row { + display: flex; + align-items: center; + gap: 8px; +} + +.check-row input { + width: 16px; + height: 16px; +} + +.pill, +.status-chip, +.status-pill { + min-height: 26px; + border-radius: 999px; + padding: 4px 10px; + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--text-muted); + background: var(--surface-soft); + font-size: 12px; + font-weight: 800; +} + +.pill.success, +.status-chip.success { + color: var(--success); + background: var(--success-soft); +} + +.pill.warning, +.status-chip.warning { + color: var(--warning); + background: var(--warning-soft); +} + +.pill.danger, +.status-chip.danger { + color: var(--danger); + background: var(--danger-soft); +} + +.pill.info, +.status-chip.info { + color: var(--info); + background: var(--info-soft); +} + +.flash { + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 12px 14px; + background: #fff; + box-shadow: var(--shadow-soft); +} + +.flash.success { + color: #166534; + background: var(--success-soft); + border-color: #bbf7d0; +} + +.flash.warning { + color: #92400e; + background: var(--warning-soft); + border-color: #fed7aa; +} + +.flash.error { + color: #991b1b; + background: var(--danger-soft); + border-color: #fecaca; +} + +.table-shell { + overflow-x: auto; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: #fff; +} + +table { + width: 100%; + min-width: 720px; + border-collapse: collapse; +} + +th, +td { + padding: 12px 14px; + border-bottom: 1px solid var(--border); + text-align: left; + vertical-align: middle; + font-size: 13px; +} + +th { + color: var(--text-muted); + background: var(--surface-muted); + font-weight: 800; +} + +tbody tr:last-child td { + border-bottom: 0; +} + +.account-card, +.data-card { + padding: 18px; +} + +.account-card { + display: grid; + gap: 16px; +} + +.account-list { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.account-head, +.account-panel__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 14px; +} + +.account-ident, +.account-panel__meta { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} + +.avatar-photo, +.account-avatar { + width: 40px; + height: 40px; + border-radius: 12px; + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 40px; + color: #fff; + background: var(--primary); + font-weight: 850; +} + +.account-name, +.account-panel__name { + font-size: 17px; + font-weight: 850; +} + +.account-sub, +.account-panel__subline { + margin-top: 4px; + color: var(--text-muted); + font-size: 12px; +} + +.metric-box { + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 12px; + background: var(--surface-muted); + text-align: center; +} + +.metric-box .label { + display: block; + color: var(--text-muted); + font-size: 12px; + margin-bottom: 6px; +} + +.metric-box strong { + font-size: 18px; +} + +.friend-picker { + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--surface-muted); + padding: 14px; + display: grid; + gap: 12px; +} + +.friend-picker-toolbar { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.friend-picker-title { + color: var(--text); + font-weight: 850; +} + +.friend-picker-list { + max-height: 240px; + overflow: auto; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: #fff; + padding: 8px; + display: grid; + gap: 6px; +} + +.friend-option { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + border: 1px solid transparent; + border-radius: 10px; + padding: 8px 10px; +} + +.friend-option.selected { + border-color: #bfdbfe; + background: var(--primary-soft); +} + +.friend-option input { + width: 16px; + height: 16px; + accent-color: var(--primary); +} + +.friend-picker-empty, +.empty-state { + padding: 24px 16px; + text-align: center; + color: var(--text-muted); + border-style: dashed; + background: var(--surface-muted); +} + +.login-workspace-grid { + display: grid; + grid-template-columns: 340px minmax(0, 1fr); + gap: 18px; + align-items: start; +} + +.browser-frame-shell { + border: 1px solid var(--border); + border-radius: var(--radius-lg); + overflow: hidden; + background: #0f172a; +} + +.browser-frame-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px; + background: #111827; + color: #e5e7eb; +} + +.url-field { + display: flex; + gap: 8px; + align-items: center; +} + +.desktop-frame { + width: 100%; + height: min(68vh, 720px); + min-height: 520px; + border: 0; + display: block; + background: #0f172a; +} + +.frame-help { + border-top: 1px solid rgba(255, 255, 255, 0.08); + padding: 12px 14px; + color: #cbd5e1; + background: #111827; + font-size: 13px; +} + +.status-list { + display: grid; + gap: 10px; +} + +.status-item { + display: flex; + align-items: center; + gap: 10px; + color: var(--text-muted); + font-weight: 700; +} + +.status-dot { + width: 10px; + height: 10px; + border-radius: 999px; + background: var(--border-strong); +} + +.status-item.pending .status-dot { + background: var(--primary); +} + +.status-item.success .status-dot { + background: var(--success); +} + +.status-item.error .status-dot { + background: var(--danger); +} + +.info-card { + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--surface-muted); + padding: 14px; + line-height: 1.65; +} + +.data-card { + overflow: hidden; +} + +.data-card__head, +.data-card__footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.data-card__title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 850; +} + +.data-card__title.success { + color: var(--success); +} + +.data-card__title.danger { + color: var(--danger); +} + +.data-card__title.warning { + color: var(--warning); +} + +.data-card__title.info { + color: var(--info); +} + +.data-card__table { + margin-top: 12px; +} + +.data-card__extra { + display: none; + margin-top: 12px; +} + +.data-card__extra.is-open { + display: block; +} + +.data-card__footer { + justify-content: flex-end; + margin-top: 12px; +} + +.data-card__footer-button { + min-height: auto; + padding: 0; + color: var(--primary); + background: transparent; +} + +.data-card__footer-button:hover { + background: transparent; + color: var(--primary-hover); + text-decoration: underline; +} + +.account-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; +} + +.console-account-list { + display: grid; + gap: 18px; +} + +.console-footer { + color: var(--text-muted); + text-align: center; +} + +.code-block { + margin: 0; + max-height: 520px; + overflow: auto; + border-radius: var(--radius-md); + padding: 14px; + background: #0f172a; + color: #dbeafe; + font-family: "Cascadia Code", "SFMono-Regular", Consolas, monospace; + font-size: 12px; + line-height: 1.6; +} + +.code-block.light { + color: var(--text); + background: var(--surface-muted); + border: 1px solid var(--border); +} + +.code-block.tall { + min-height: 420px; +} + +.login-page { + min-height: 100vh; + display: grid; + place-items: center; + padding: 24px; + background: linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%); +} + +.login-card { + width: min(100%, 420px); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 28px; + background: #fff; + box-shadow: var(--shadow); +} + +.login-card h1 { + margin: 0 0 10px; + font-size: 28px; +} + +.login-brand { + margin-bottom: 22px; +} + +.login-flash { + margin: 16px 0; +} + +@media (max-width: 1480px) { + .stats-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .content-grid, + .account-list, + .account-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 1180px) { + .app-shell { + grid-template-columns: 1fr; + } + + .mobile-topbar { + position: sticky; + top: 0; + z-index: 25; + height: 58px; + padding: 10px 16px; + border-bottom: 1px solid var(--border); + background: rgba(255, 255, 255, 0.94); + display: flex; + align-items: center; + justify-content: space-between; + } + + .sidebar { + position: fixed; + inset: 0 auto 0 0; + width: min(86vw, 300px); + transform: translateX(-100%); + transition: transform 0.2s ease; + } + + body.nav-open .sidebar { + transform: translateX(0); + } + + .sidebar-backdrop { + position: fixed; + inset: 0; + z-index: 15; + background: rgba(15, 23, 42, 0.32); + } + + body.nav-open .sidebar-backdrop { + display: block; + } + + .main-area { + padding: 20px 16px 28px; + } + + .page-header, + .section-title-row, + .panel-header, + .account-head, + .account-panel__header { + flex-direction: column; + align-items: stretch; + } + + .layout-grid, + .dashboard-layout, + .settings-layout, + .login-workspace-grid, + .quick-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 720px) { + .stats-grid, + .form-grid, + .settings-grid, + .metric-row, + .account-metrics { + grid-template-columns: 1fr; + } + + .page-title { + font-size: 25px; + } + + .panel, + .account-card, + .data-card { + padding: 16px; + } + + table { + min-width: 640px; + } + + .desktop-frame { + min-height: 420px; + } +} diff --git a/DouYinSparkFlow/webui/static/app.js b/DouYinSparkFlow/webui/static/app.js new file mode 100644 index 0000000..b943fb4 --- /dev/null +++ b/DouYinSparkFlow/webui/static/app.js @@ -0,0 +1,411 @@ +(() => { + const body = document.body; + const navToggle = document.querySelector("[data-nav-toggle]"); + const navClose = document.querySelector("[data-nav-close]"); + + navToggle?.addEventListener("click", () => body.classList.toggle("nav-open")); + navClose?.addEventListener("click", () => body.classList.remove("nav-open")); + document.querySelectorAll(".nav-item").forEach((item) => { + item.addEventListener("click", () => body.classList.remove("nav-open")); + }); +})(); + +(() => { + document.querySelectorAll("[data-confirm]").forEach((node) => { + const message = node.getAttribute("data-confirm") || "确认执行此操作?"; + const handler = (event) => { + if (!window.confirm(message)) { + event.preventDefault(); + event.stopImmediatePropagation(); + } + }; + if (node.tagName === "FORM") { + node.addEventListener("submit", handler); + } else { + node.addEventListener("click", handler); + } + }); +})(); + +(() => { + document.querySelectorAll(".data-card__footer-button").forEach((button) => { + button.addEventListener("click", () => { + const targetId = button.dataset.expandTarget; + if (!targetId) return; + const panel = document.getElementById(targetId); + if (!panel) return; + const isOpen = panel.classList.toggle("is-open"); + button.textContent = isOpen + ? "收起" + : button.dataset.totalLabel || "查看全部"; + }); + }); +})(); + +(() => { + const root = document.getElementById("login-desktop-controls"); + if (!root) return; + + const csrfToken = root.dataset.csrfToken || ""; + const publicUrl = root.dataset.publicUrl || ""; + const runtimeStateEl = document.getElementById("login-desktop-runtime-state"); + const statusTextEl = document.getElementById("login-desktop-status-text"); + const openButtons = document.querySelectorAll(".login-desktop-open"); + const saveButtons = document.querySelectorAll(".login-desktop-save"); + const resetButtons = document.querySelectorAll(".login-desktop-reset"); + const copyPublicUrlButton = document.getElementById("copy-public-url"); + const statusMap = { + checking: document.getElementById("desktop-status-checking"), + pending: document.getElementById("desktop-status-pending"), + success: document.getElementById("desktop-status-success"), + error: document.getElementById("desktop-status-error"), + }; + + const setVisualStatus = (state) => { + Object.values(statusMap).forEach((node) => { + if (!node) return; + node.style.opacity = "0.46"; + }); + if (statusMap[state]) { + statusMap[state].style.opacity = "1"; + } + }; + + const setStatus = (text, tone = "", state = "checking") => { + if (statusTextEl) statusTextEl.textContent = text; + if (runtimeStateEl) { + runtimeStateEl.className = `pill${tone ? ` ${tone}` : ""}`; + } + setVisualStatus(state); + }; + + const postForm = async (url, payload = {}) => { + const formData = new FormData(); + formData.set("csrf_token", csrfToken); + Object.entries(payload).forEach(([key, value]) => { + formData.set(key, String(value ?? "")); + }); + const response = await fetch(url, { + method: "POST", + body: formData, + credentials: "same-origin", + }); + const data = await response.json().catch(() => ({})); + if (!response.ok || data.ok === false) { + throw new Error(data.error || `请求失败:${response.status}`); + } + return data; + }; + + const openDesktopWindow = () => { + const popup = window.open(publicUrl, "_blank"); + if (!popup) { + setStatus( + "浏览器阻止了登录工作区弹窗,请允许弹窗后再试。", + "danger", + "error", + ); + return false; + } + return true; + }; + + const pollStatus = async () => { + try { + const response = await fetch("/login-desktop/status", { + credentials: "same-origin", + }); + const data = await response.json(); + if (!response.ok || data.ok === false) { + if (runtimeStateEl) runtimeStateEl.textContent = "不可用"; + setStatus( + data.error || "登录工作区不可用,请检查 login-desktop 服务。", + "warning", + "error", + ); + return; + } + if (runtimeStateEl) + runtimeStateEl.textContent = data.logged_in ? "已登录" : "待登录"; + if (data.logged_in) { + setStatus( + `当前浏览器已登录:${data.username}(${data.unique_id})`, + "success", + "success", + ); + } else { + setStatus( + "当前浏览器尚未登录,可打开登录工作区开始人工登录。", + "", + "pending", + ); + } + } catch (error) { + if (runtimeStateEl) runtimeStateEl.textContent = "异常"; + setStatus(`登录工作区状态检查失败:${error.message}`, "danger", "error"); + } + }; + + copyPublicUrlButton?.addEventListener("click", async () => { + try { + await navigator.clipboard.writeText(publicUrl); + setStatus("登录工作区地址已复制。", "success", "pending"); + } catch (error) { + setStatus(`复制失败:${error.message}`, "warning", "error"); + } + }); + + openButtons.forEach((button) => { + button.addEventListener("click", async () => { + const reloginUniqueId = String( + button.dataset.reloginUniqueId || "", + ).trim(); + const accountName = String(button.dataset.accountName || "").trim(); + try { + await postForm("/login-desktop/open"); + openDesktopWindow(); + setStatus( + reloginUniqueId + ? `请使用账号 ${accountName || reloginUniqueId} 完成登录,然后保存登录态。` + : "请在远端浏览器中完成抖音创作者中心登录,然后保存账号。", + "", + "pending", + ); + } catch (error) { + setStatus(`打开登录工作区失败:${error.message}`, "danger", "error"); + } + }); + }); + + saveButtons.forEach((button) => { + button.addEventListener("click", async () => { + const reloginUniqueId = String( + button.dataset.reloginUniqueId || "", + ).trim(); + const accountName = String(button.dataset.accountName || "").trim(); + try { + const data = await postForm("/login-desktop/save", { + relogin_unique_id: reloginUniqueId, + }); + setStatus( + reloginUniqueId + ? `已把当前浏览器登录保存到账号:${accountName || reloginUniqueId}` + : `已保存当前登录账号:${data.account?.username || ""}`, + "success", + "success", + ); + window.setTimeout(() => window.location.reload(), 900); + } catch (error) { + setStatus(`保存当前登录账号失败:${error.message}`, "danger", "error"); + } + }); + }); + + resetButtons.forEach((button) => { + button.addEventListener("click", async () => { + try { + await postForm("/login-desktop/reset"); + setStatus( + "登录工作区已重置,正在重新初始化浏览器。", + "warning", + "checking", + ); + await pollStatus(); + } catch (error) { + setStatus(`重置登录工作区失败:${error.message}`, "danger", "error"); + } + }); + }); + + setVisualStatus("checking"); + pollStatus(); + window.setInterval(pollStatus, 5000); +})(); + +(() => { + const pickers = document.querySelectorAll(".friend-picker"); + if (!pickers.length) return; + + const parseJsonScript = (id) => { + const el = document.getElementById(id); + if (!el) return []; + try { + return JSON.parse(el.textContent || "[]"); + } catch (error) { + console.error("Failed to parse friend picker JSON", id, error); + return []; + } + }; + + const escapeHtml = (value) => + String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); + + pickers.forEach((picker) => { + const accountId = picker.dataset.accountId; + const refreshUrl = picker.dataset.refreshUrl; + const csrfToken = picker.dataset.csrfToken; + const searchInput = picker.querySelector(".friend-search-input"); + const refreshButton = picker.querySelector(".friend-refresh-button"); + const listEl = picker.querySelector(".friend-picker-list"); + const summaryEl = picker.querySelector(".friend-picker-summary"); + const statusEl = picker.querySelector(".friend-picker-status"); + const hiddenInputsEl = picker.querySelector(".friend-selected-inputs"); + const formEl = picker.closest("form"); + const targetsTextarea = formEl?.querySelector(".targets-textarea"); + const currentTargetsEl = picker.querySelector( + ".friend-picker-current-targets span", + ); + + let friends = parseJsonScript(`friends-cache-${accountId}`); + let selected = new Set(parseJsonScript(`selected-targets-${accountId}`)); + + const splitTargetText = (value) => { + const seen = new Set(); + return String(value || "") + .replaceAll(",", "\n") + .split(/\r?\n/) + .map((name) => name.trim()) + .filter((name) => { + if (!name || seen.has(name)) return false; + seen.add(name); + return true; + }); + }; + + const combinedFriends = () => { + const merged = []; + const seen = new Set(); + [...selected, ...friends].forEach((name) => { + if (!name || seen.has(name)) return; + seen.add(name); + merged.push(name); + }); + return merged; + }; + + const syncTextareaFromSelected = () => { + if (targetsTextarea) { + targetsTextarea.value = [...selected].join("\n"); + } + }; + + const syncSelectedFromTextarea = () => { + if (targetsTextarea) { + selected = new Set(splitTargetText(targetsTextarea.value)); + } + }; + + const renderHiddenInputs = () => { + hiddenInputsEl.innerHTML = ""; + [...selected].forEach((name) => { + const input = document.createElement("input"); + input.type = "hidden"; + input.name = "targets"; + input.value = name; + hiddenInputsEl.appendChild(input); + }); + }; + + const updateSummary = () => { + summaryEl.textContent = `已选 ${selected.size} 人`; + if (currentTargetsEl) { + currentTargetsEl.textContent = selected.size + ? [...selected].join("、") + : "未选择"; + } + }; + + const renderList = () => { + const query = (searchInput.value || "").trim().toLowerCase(); + const allNames = combinedFriends(); + const displayNames = allNames.filter((name) => + name.toLowerCase().includes(query), + ); + renderHiddenInputs(); + updateSummary(); + + if (!allNames.length) { + listEl.innerHTML = + '
点击“刷新好友列表”后再勾选目标好友。
'; + return; + } + + if (!displayNames.length) { + listEl.innerHTML = + '
没有匹配的好友。
'; + return; + } + + listEl.innerHTML = displayNames + .map( + (name) => ` + + `, + ) + .join(""); + + listEl.querySelectorAll('input[type="checkbox"]').forEach((checkbox) => { + checkbox.addEventListener("change", () => { + const option = checkbox.closest(".friend-option"); + const value = checkbox.value; + if (checkbox.checked) { + selected.add(value); + option?.classList.add("selected"); + } else { + selected.delete(value); + option?.classList.remove("selected"); + } + syncTextareaFromSelected(); + renderHiddenInputs(); + updateSummary(); + }); + }); + }; + + refreshButton.addEventListener("click", async () => { + refreshButton.disabled = true; + const originalText = refreshButton.textContent; + refreshButton.textContent = "刷新中..."; + statusEl.textContent = "正在读取好友列表..."; + try { + const formData = new FormData(); + formData.set("csrf_token", csrfToken); + const response = await fetch(refreshUrl, { + method: "POST", + body: formData, + credentials: "same-origin", + }); + const payload = await response.json(); + if (!response.ok) { + throw new Error(payload.error || "刷新好友列表失败"); + } + friends = Array.isArray(payload.friends) ? payload.friends : []; + statusEl.textContent = + payload.message || `已刷新 ${friends.length} 个好友`; + renderList(); + } catch (error) { + statusEl.textContent = error.message || "刷新好友列表失败"; + } finally { + refreshButton.disabled = false; + refreshButton.textContent = originalText; + } + }); + + searchInput.addEventListener("input", renderList); + targetsTextarea?.addEventListener("input", () => { + syncSelectedFromTextarea(); + renderList(); + }); + + syncSelectedFromTextarea(); + renderList(); + }); +})(); diff --git a/DouYinSparkFlow/webui/templates/accounts.html b/DouYinSparkFlow/webui/templates/accounts.html new file mode 100644 index 0000000..5a9cf55 --- /dev/null +++ b/DouYinSparkFlow/webui/templates/accounts.html @@ -0,0 +1,184 @@ +{% extends "base.html" %} + +{% block nav_key %}accounts{% endblock %} +{% block title %}账号与目标 | 自动续火花{% endblock %} +{% block page_title %}账号与目标{% endblock %} +{% block page_subtitle %}集中维护账号开关、目标好友、好友缓存和登录态同步{% endblock %} + +{% block topbar_actions %} +登录工作区 +发送控制台 +{% endblock %} + +{% block content %} + + +{% if accounts %} +
+ {% for account in accounts %} +
+ + + + +
+ + + + + +
+
+
+ 好友选择器 +

+ {% if account.friends_cache_updated_at %} 上次刷新:{{ + account.friends_cache_updated_at }} {% else %} 还没有读取好友列表 + {% endif %} +

+
+ +
+ + +

+ 当前目标好友: + {{ account.targets|join('、') if account.targets else '未选择' + }} +

+

已选 0 人

+
+
+ + + +
+ + +
+ +
+ + +
+ + +
+
+ + +
+
+
+ {% endfor %} +
+{% else %} +
+ 当前没有账号。请先前往登录工作区完成扫码登录并保存账号。 +
+{% endif %} {% endblock %} diff --git a/DouYinSparkFlow/webui/templates/base.html b/DouYinSparkFlow/webui/templates/base.html index d364ca7..1713170 100644 --- a/DouYinSparkFlow/webui/templates/base.html +++ b/DouYinSparkFlow/webui/templates/base.html @@ -2,758 +2,121 @@ - - + + {% block title %}自动续火花{% endblock %} - +
-
@@ -761,12 +124,12 @@
{% if flash %}
{{ flash.message }}
- {% endif %} - {% block content %}{% endblock %} + {% endif %} {% block content %}{% endblock %}
+ {% block scripts %}{% endblock %} diff --git a/DouYinSparkFlow/webui/templates/dashboard.html b/DouYinSparkFlow/webui/templates/dashboard.html index 64c4387..9ad4077 100644 --- a/DouYinSparkFlow/webui/templates/dashboard.html +++ b/DouYinSparkFlow/webui/templates/dashboard.html @@ -1,650 +1,99 @@ {% extends "base.html" %} {% block nav_key %}dashboard{% endblock %} -{% block title %}自动续火花控制台{% endblock %} -{% block page_title %}自动续火花控制台{% endblock %} -{% block page_subtitle %}多账号发送与运营管理{% endblock %} +{% block title %}控制台概览 | 自动续火花{% endblock %} +{% block page_title %}控制台概览{% endblock %} +{% block page_subtitle %}发送状态、账号健康和关键运维入口{% endblock %} {% block topbar_actions %} -✦ 发送控制台 -
- - -
-
- - -
+登录工作区 +发送控制台 {% endblock %} {% block content %} {% set enabled_accounts = accounts | selectattr("enabled", "equalto", true) | list %} {% set send_summary = ops.send_console.summary %} +{% set pending_total = send_summary.today_pending_targets + send_summary.today_unprocessed_targets %} {% set proxy_rows = ops.containers | selectattr("Names", "equalto", "mihomo") | list %} - -
-
-
首页总览 · 当前系统状态与发送概况
+
+
+
+ 启用账号 + {{ enabled_accounts|length }} + 总账号 {{ accounts|length }} +
+
A
+
-
-
-
- 启用账号 - {{ enabled_accounts|length }} -
- 总账号 {{ accounts|length }} -
-
-
👥
-
+
+
+ 今日成功 + {{ send_summary.today_sent_targets }} + 浏览器确认发送成功 +
+
+ ✓ +
+
-
-
- 今日成功目标 - {{ send_summary.today_sent_targets }} -
- 成功结果来自浏览器真实发送确认 -
-
-
🎯
-
+
+
+ 失败待补发 + {{ send_summary.today_failed_targets }} + 进入重试队列 +
+
+ ! +
+
-
-
- 失败待补发 - {{ send_summary.today_failed_targets }} -
- 失败会进入补发与重试队列 -
-
-
-
+
+
+ 待发送 + {{ send_summary.today_pending_targets }} + {{ ops.daily_schedule or "未配置窗口" }} +
+
+
-
-
- 待发送 / 未处理 - {{ send_summary.today_pending_targets + send_summary.today_unprocessed_targets }} -
- {{ ops.daily_schedule or "未配置发送窗口" }} -
-
-
-
-
+
+
+ 未处理 + {{ send_summary.today_unprocessed_targets }} + 等待后续调度 +
+
+
-
-
-
+
+
+
-

交互式登录浏览器

-

直接在网页中操作远端浏览器完成扫码、验证码与创作者中心登录,登录完成后可一键保存当前账号。

+

今日发送摘要

+

+ 按账号展示今日发送、失败和待处理状态,详细明细在发送控制台查看。 +

+ 查看明细
- - - -
- -
-
-
-

发送控制台摘要

-

查看每个账号今天的发送状态分布,并快速进入发送控制台做批量补发或针对性重试。

-
- 打开发送控制台 -
- -
-
-
- 启用账号 - {{ send_summary.enabled_accounts }} -
-
A
-
-
-
- 今日成功目标 - {{ send_summary.today_sent_targets }} -
-
-
-
-
- 失败待补发 - {{ send_summary.today_failed_targets }} -
-
!
-
-
-
- 待发送 / 未处理 - {{ send_summary.today_pending_targets + send_summary.today_unprocessed_targets }} -
-
-
-
- -
+
@@ -660,8 +109,8 @@ {% for row in ops.send_console.accounts %} @@ -670,660 +119,145 @@ {% else %} - + + + {% endfor %}
- {{ row.username }}
- {{ row.unique_id }} + {{ row.username }}
+ {{ row.unique_id }}
{{ row.sent_targets|length }} {{ row.failed_targets|length }}{{ row.last_failure_reason or "-" }}
当前没有发送摘要数据。
当前没有发送摘要数据。
-
+
-

账号管理

-

集中管理账号开关、目标好友、好友缓存、登录同步和删除操作。布局与发送控制台保持同一套卡片系统。

+

账号健康

+

+ 快速确认账号启用状态、目标好友数量和登录态缓存规模。 +

+ 管理账号
{% if accounts %} -
- -
-
-

容器状态

-
- - - - - - - - - - {% for row in ops.containers %} - - - - - - {% else %} - - {% endfor %} - -
NameStatusImage
{{ row.Names }}{{ row.Status }}{{ row.Image }}
当前没有可见容器状态。
-
-
- -
-

Cron / 调度

-
{{ ops.crontab or "当前没有 crontab 任务。" }}
-
- -
-

日志预览

-
{{ ops.log_tail or "暂无日志" }}
-
-
-
{% endblock %} - -{% block scripts %} - -{% endblock %} diff --git a/DouYinSparkFlow/webui/templates/login.html b/DouYinSparkFlow/webui/templates/login.html index df6f8eb..610abae 100644 --- a/DouYinSparkFlow/webui/templates/login.html +++ b/DouYinSparkFlow/webui/templates/login.html @@ -1,117 +1,59 @@ - - - 登录 | 抖音多账号续火花控制台 - + + + 登录 | 自动续火花控制台 + - -
+ +
+ +

控制台登录

-

用于管理抖音多账号、目标好友、自动续火花任务与运维配置。

+

+ 用于管理抖音多账号、目标好友、自动续火花任务与运维配置。 +

{% if flash %} -
{{ flash.message }}
- {% endif %} - - {% if not bootstrapped %} -
+ + {% endif %} {% if not bootstrapped %} +
{% else %} -
+
{% endif %} -
+ diff --git a/DouYinSparkFlow/webui/templates/login_workspace.html b/DouYinSparkFlow/webui/templates/login_workspace.html new file mode 100644 index 0000000..bb74498 --- /dev/null +++ b/DouYinSparkFlow/webui/templates/login_workspace.html @@ -0,0 +1,150 @@ +{% extends "base.html" %} + +{% block nav_key %}login_workspace{% endblock %} +{% block title %}登录工作区 | 自动续火花{% endblock %} +{% block page_title %}登录工作区{% endblock %} +{% block page_subtitle %}通过远端浏览器完成扫码、验证和登录态保存{% endblock %} + +{% block topbar_actions %} +账号管理 +发送控制台 +{% endblock %} + +{% block content %} + +{% endblock %} diff --git a/DouYinSparkFlow/webui/templates/logs.html b/DouYinSparkFlow/webui/templates/logs.html index e55199b..feedaac 100644 --- a/DouYinSparkFlow/webui/templates/logs.html +++ b/DouYinSparkFlow/webui/templates/logs.html @@ -1,16 +1,20 @@ {% extends "base.html" %} +{% block nav_key %}logs{% endblock %} {% block title %}运行日志 | 抖音多账号续火花控制台{% endblock %} {% block page_title %}运行日志{% endblock %} +{% block page_subtitle %}查看最近任务输出和服务诊断信息{% endblock %} {% block content %}

详细日志

-

展示最近的任务输出,便于检查登录同步、发送过程和容器状态。

+

+ 展示最近的任务输出,便于检查登录同步、发送过程和容器状态。 +

- 返回控制台 + 返回系统设置
{{ log_tail or "暂无日志" }}
diff --git a/DouYinSparkFlow/webui/templates/send_console.html b/DouYinSparkFlow/webui/templates/send_console.html index bceb62f..7da5766 100644 --- a/DouYinSparkFlow/webui/templates/send_console.html +++ b/DouYinSparkFlow/webui/templates/send_console.html @@ -1,334 +1,96 @@ {% extends "base.html" %} {% block nav_key %}send_console{% endblock %} -{% block title %}发送控制台{% endblock %} +{% block title %}发送控制台 | 自动续火花{% endblock %} {% block page_title %}发送控制台{% endblock %} -{% block page_subtitle %}今日发送总览{% endblock %} +{% block page_subtitle %}今日发送明细、失败队列和针对性重试{% endblock %} {% block topbar_actions %} -⌂ 返回首页 +返回概览
- +
-
- - + + +
{% endblock %} {% block content %} {% set send_console = ops.send_console %} - -
-
当前时间:{{ send_console.now|replace('T', ' ') }}
+
+ 当前时间:{{ send_console.now|replace('T', ' ') }} +
启用账号 - {{ send_console.summary.enabled_accounts }} - 实时统计 · 当前可发送账号 + {{ send_console.summary.enabled_accounts }} + 当前可发送账号
-
👥
+
A
- 今日成功目标 - {{ send_console.summary.today_sent_targets }} - 发送成功后立即写回历史 + 今日成功 + {{ send_console.summary.today_sent_targets }} + 写回发送历史 +
+
+ ✓
-
🎯
失败待补发 - {{ send_console.summary.today_failed_targets }} - 按失败分类进入补发与重试 + {{ send_console.summary.today_failed_targets }} + 按失败分类重试 +
+
+ !
-
- 待发送 / 未处理 - {{ send_console.summary.today_pending_targets + send_console.summary.today_unprocessed_targets }} - 窗口内按计划发送,窗口外展示剩余目标 + 待发送 + {{ send_console.summary.today_pending_targets }} + 窗口内按计划发送
-
+
+
+ +
+
+ 未处理 + {{ send_console.summary.today_unprocessed_targets }} + 等待调度 +
+
@@ -343,25 +105,33 @@
- + + +
+{% endblock %}