From 336d08fc052458ae761f52a5965be9a5ec99241a Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 19 Oct 2026 13:47:12 +0800 Subject: [PATCH] render buttons as neon tubes and rebuild the login page The previous commit only swapped theme tokens, so buttons kept the solid blue from arco.css: a-button fills with rgb(var(--primary-6)), which maps to --arcoblue-6, not --color-primary. Bridge both naming families so no Arco blue survives. Style buttons as hollow tubes rather than filled blocks: transparent fill, 1px cyan wall, inner glow plus two outer halos, and a glowing label. Hover brightens the wall, active pinches the halo inward, loading pulses, disabled cuts power entirely. Same treatment in red for destructive actions, and hollow-line chips for inputs, tags, switches and popups. Rebuild LoginView to match the approved prototype: 32px masked grid, scanline, IsometricWave, neon card edge, badge, probe status lamp, and default-credential chips. Logic, API calls and lock/reset messaging are unchanged. --- packages/client/src/i18n/locales/en.ts | 8 + packages/client/src/i18n/locales/zh.ts | 8 + packages/client/src/styles/global.scss | 301 +++++++++++++- packages/client/src/views/LoginView.vue | 507 ++++++++++++++++++++---- 4 files changed, 755 insertions(+), 69 deletions(-) diff --git a/packages/client/src/i18n/locales/en.ts b/packages/client/src/i18n/locales/en.ts index b0524b7..332e68c 100644 --- a/packages/client/src/i18n/locales/en.ts +++ b/packages/client/src/i18n/locales/en.ts @@ -19,6 +19,14 @@ export default { lockResetHint: 'If this is your server, clear the login lock with:', defaultLoginResetHint: 'To reset the default admin password, run:', sessionExpired: 'Login expired. Please sign in again.', + probing: 'Probing server…', + online: 'Auth service online · password login available', + offline: 'Cannot reach the server · you can still try to log in', + showPassword: 'Show password', + hidePassword: 'Hide password', + fillDefaultCredentials: 'Fill in admin / 123456', + localDeploymentNote: 'Self-hosted · no telemetry · data never leaves this machine', + tokenHint: 'The token returned by POST /api/auth/login is stored in localStorage', accessDenied: 'You do not have permission to access this resource.', passwordMismatch: 'Passwords do not match', passwordTooShort: 'Password must be at least 6 characters', diff --git a/packages/client/src/i18n/locales/zh.ts b/packages/client/src/i18n/locales/zh.ts index 266360c..be99588 100644 --- a/packages/client/src/i18n/locales/zh.ts +++ b/packages/client/src/i18n/locales/zh.ts @@ -19,6 +19,14 @@ export default { lockResetHint: '如果这是你的服务器,可以执行以下命令清除登录锁定:', defaultLoginResetHint: '如需重置默认 admin 密码,可以执行:', sessionExpired: '登录已过期,请重新登录', + probing: '正在探测服务端…', + online: '认证服务在线 · 密码登录可用', + offline: '无法连接到服务器 · 登录仍可尝试', + showPassword: '显示密码', + hidePassword: '隐藏密码', + fillDefaultCredentials: '填入 admin / 123456', + localDeploymentNote: '本地部署 · 无遥测 · 数据不出机器', + tokenHint: '登录后 POST /api/auth/login 返回的 token 存入 localStorage', accessDenied: '你没有权限访问该资源', passwordMismatch: '两次密码不一致', passwordTooShort: '密码长度至少 6 个字符', diff --git a/packages/client/src/styles/global.scss b/packages/client/src/styles/global.scss index 42db6fa..992b312 100644 --- a/packages/client/src/styles/global.scss +++ b/packages/client/src/styles/global.scss @@ -29,11 +29,39 @@ textarea { // ─── Arco Design token bridge ────────────────────────────────── // arco-theme.ts 不是运行时来源(没有 ConfigProvider),Arco 的实际外观由 arco.css -// 里的 --color-* 变量决定。这里把 CYBER 令牌翻译成 Arco 变量,让 a-select / -// a-input / a-tag / a-table / a-modal 等自有控件与项目配色对齐。 +// 里的 CSS 变量决定。这里把 CYBER 令牌翻译成 Arco 变量。 +// +// 关键:Arco 有两套命名。--primary-6 是 rgb 三元组,--color-primary 是颜色值。 +// a-button / a-tag / a-switch 的底色走 rgb(var(--primary-6)),也就是 +// --primary-6 → --arcoblue-6 这一族;只覆盖 --color-primary 不会改变按钮底色。 +// 两套都必须覆盖,否则会残留 Arco 蓝(22,93,255)。 // 变量表见 node_modules/@arco-design/web-vue/dist/arco.css 的 :root 块。 html[arco-theme='dark'], :root { + // Arco 的 rgb 三元组族 —— 实心控件(按钮底、开关滑块、进度条)的真正来源。 + // 霓虹青 oklch(0.805 0.148 188) ≈ rgb(45,212,191),按亮度排 1→10。 + --arcoblue-1: 12, 58, 56; + --arcoblue-2: 16, 84, 78; + --arcoblue-3: 22, 118, 108; + --arcoblue-4: 28, 148, 135; + --arcoblue-5: 34, 172, 156; + --arcoblue-6: 45, 212, 191; + --arcoblue-7: 70, 229, 209; + --arcoblue-8: 120, 239, 224; + --arcoblue-9: 172, 246, 238; + --arcoblue-10: 218, 250, 246; + + --green-6: 47, 197, 138; + --green-5: 56, 184, 130; + --green-7: 38, 178, 122; + --orange-6: 226, 175, 32; + --orange-5: 214, 165, 34; + --orange-7: 236, 190, 44; + --red-6: 232, 78, 61; + --red-5: 222, 74, 58; + --red-7: 240, 96, 78; + + // Arco 的颜色值族 —— 描边、文字、浅底 --color-primary-light-1: oklch(0.30 0.060 188); --color-primary-light-2: oklch(0.39 0.075 188); --color-primary-light-3: oklch(0.50 0.095 188); @@ -121,6 +149,275 @@ body { background-color: $border-color !important; } +// ─── Neon tube buttons ───────────────────────────────────────── +// CYBER 的按钮是「霓虹灯管」而不是实心色块:底色透明 / 内部暗,只有 1px 青色管壁 +// 与外发光。hover / active 时管壁增亮、光晕收紧,模拟通电。 +// 这里直接覆盖 Arco 的按钮外观类,因为 a-button 的默认样式来自 arco.css, +// token 桥接改不到「实心 vs 镂空」这一层。 +$neon: var(--accent-primary); +$neon-hv: var(--accent-hover); + +// 镂空管壁 + 双层外发光(内紧外散) +@mixin neon-tube($glow: 0.32, $wall: 1px) { + color: $neon; + background: transparent; + border: $wall solid rgba(var(--accent-primary-rgb), 0.55); + box-shadow: + inset 0 0 12px rgba(var(--accent-primary-rgb), $glow * 0.35), + 0 0 10px rgba(var(--accent-primary-rgb), $glow), + 0 0 22px rgba(var(--accent-primary-rgb), $glow * 0.5); + text-shadow: 0 0 8px rgba(var(--accent-primary-rgb), 0.45); +} + +// 默认按钮(secondary / 默认态):不发光的幽灵按钮,只在 hover 时点亮管壁 +.arco-btn-secondary, +.arco-btn-secondary.arco-btn-status-warning, +.arco-btn-outline, +.arco-btn-default { + color: $text-secondary; + background: transparent; + border: 1px solid $border-color; + box-shadow: none; + transition: color 0.16s ease, border-color 0.16s ease, + box-shadow 0.16s ease, background-color 0.16s ease; + + &:not(.arco-btn-disabled):hover { + color: $neon; + border-color: rgba(var(--accent-primary-rgb), 0.55); + background: rgba(var(--accent-primary-rgb), 0.05); + box-shadow: 0 0 10px rgba(var(--accent-primary-rgb), 0.22); + text-shadow: 0 0 8px rgba(var(--accent-primary-rgb), 0.35); + } +} + +// 主按钮:霓虹管 +.arco-btn-primary { + @include neon-tube(0.34); + font-weight: 600; + + &:not(.arco-btn-disabled):hover { + @include neon-tube(0.52); + color: $neon-hv; + border-color: rgba(var(--accent-primary-rgb), 0.85); + } + + &:not(.arco-btn-disabled):active { + // 按下时管壁收窄、光晕内收,像灯管被压住 + box-shadow: + inset 0 0 16px rgba(var(--accent-primary-rgb), 0.22), + 0 0 6px rgba(var(--accent-primary-rgb), 0.34); + transform: translateY(1px); + } + + &:not(.arco-btn-disabled):focus-visible { + outline: none; + box-shadow: + inset 0 0 12px rgba(var(--accent-primary-rgb), 0.12), + 0 0 0 3px rgba(var(--accent-primary-rgb), 0.16), + 0 0 16px rgba(var(--accent-primary-rgb), 0.4); + } +} + +// 危险操作:同构的霓虹管,颜色换成红 +@mixin neon-tube-danger($glow: 0.28) { + color: $error; + background: transparent; + border: 1px solid rgba(var(--error-rgb), 0.5); + box-shadow: + inset 0 0 12px rgba(var(--error-rgb), $glow * 0.3), + 0 0 10px rgba(var(--error-rgb), $glow); + text-shadow: 0 0 8px rgba(var(--error-rgb), 0.4); +} + +.arco-btn-dangerous { + @include neon-tube-danger(0.26); + font-weight: 500; + + &:not(.arco-btn-disabled):hover { + @include neon-tube-danger(0.44); + background: rgba(var(--error-rgb), 0.06); + } +} + +.arco-btn-dangerous.arco-btn-secondary, +.arco-btn-dangerous.arco-btn-outline { + color: rgba(var(--error-rgb), 0.85); + border-color: rgba(var(--error-rgb), 0.38); + background: transparent; + box-shadow: none; + + &:not(.arco-btn-disabled):hover { + @include neon-tube-danger(0.4); + } +} + +// 文字 / 链接按钮:无管壁,只让文字带辉光 +.arco-btn-text, +.arco-btn-link { + background: transparent; + border: 1px solid transparent; + box-shadow: none; + color: $neon; + text-shadow: 0 0 8px rgba(var(--accent-primary-rgb), 0.3); + + &:not(.arco-btn-disabled):hover { + color: $neon-hv; + background: rgba(var(--accent-primary-rgb), 0.07); + text-shadow: 0 0 12px rgba(var(--accent-primary-rgb), 0.55); + } +} + +// 禁用态:灯管熄灭 +.arco-btn-primary.arco-btn-disabled, +.arco-btn-secondary.arco-btn-disabled, +.arco-btn-outline.arco-btn-disabled, +.arco-btn-dangerous.arco-btn-disabled, +.arco-btn-text.arco-btn-disabled, +.arco-btn-link.arco-btn-disabled { + color: var(--text-faint); + background: transparent; + border-color: var(--border-light); + box-shadow: none; + text-shadow: none; + cursor: not-allowed; +} + +// 加载态:保留管壁不熄灭,用脉冲表示"通电中" +@keyframes neon-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.62; } +} + +.arco-btn-primary.arco-btn-loading { + @include neon-tube(0.4); + animation: neon-pulse 1.1s ease-in-out infinite; +} + +// 尺寸统一:CYBER 的按钮偏小方硬 +.arco-btn { + border-radius: $radius-sm; + font-family: $font-code; + letter-spacing: 0.02em; + transition: color 0.16s ease, border-color 0.16s ease, + box-shadow 0.16s ease, background-color 0.16s ease, + transform 0.08s ease; +} + +// 兜底:任何未显式分类的实心底按钮(Arco 的 .arco-btn 默认无 type class 时) +// 也不能漏成白块或蓝块。 +.arco-btn:not([class*='arco-btn-primary']):not([class*='arco-btn-secondary']):not([class*='arco-btn-outline']):not([class*='arco-btn-text']):not([class*='arco-btn-link']):not([class*='arco-btn-dangerous']) { + background: transparent; + color: $text-secondary; + border: 1px solid $border-color; + + &:not(.arco-btn-disabled):hover { + color: $neon; + border-color: rgba(var(--accent-primary-rgb), 0.55); + } +} + +// ─── Neon inputs / selects / tags ─────────────────────────────── +// 输入区是「凹陷」的:底色比卡片更沉,聚焦时管壁点亮并带一圈内发光。 +.arco-input-wrapper, +.arco-textarea-wrapper, +.arco-select-view, +.arco-picker, +.arco-input-tag { + background-color: $bg-input !important; + border-color: $border-color; + box-shadow: none; + transition: border-color 0.16s ease, box-shadow 0.16s ease; + + &:hover { + border-color: rgba(var(--accent-primary-rgb), 0.4); + } +} + +.arco-input-wrapper-focus, +.arco-textarea-wrapper-focus, +.arco-select-view-focus, +.arco-picker-focused, +.arco-input-tag-focus { + border-color: rgba(var(--accent-primary-rgb), 0.85); + box-shadow: 0 0 0 3px rgba(var(--accent-primary-rgb), 0.12), + inset 0 0 14px rgba(var(--accent-primary-rgb), 0.06); +} + +.arco-input, +.arco-textarea, +.arco-select-view-single, +.arco-select-view-value { + color: $text-primary; +} + +.arco-input::placeholder, +.arco-textarea::placeholder { + color: var(--text-faint); +} + +// 标签:状态语义用发丝线底 + 彩色字,不用实心块 +.arco-tag { + border-radius: 4px; + font-family: $font-code; + font-size: 11.5px; + background: transparent; + border: 1px solid currentColor; +} + +// 开关 / 复选 / 单选的滑块用霓虹青 +.arco-switch-checked { + background-color: rgba(var(--accent-primary-rgb), 0.22); + border-color: rgba(var(--accent-primary-rgb), 0.7); + box-shadow: 0 0 10px rgba(var(--accent-primary-rgb), 0.3); +} + +.arco-switch-checked .arco-switch-slider { + background-color: var(--accent-primary); + box-shadow: 0 0 10px rgba(var(--accent-primary-rgb), 0.7); +} + +.arco-checkbox-checked .arco-checkbox-inner { + background-color: var(--accent-primary); + border-color: var(--accent-primary); + box-shadow: 0 0 8px rgba(var(--accent-primary-rgb), 0.5); +} + +// 下拉 / 弹层的浮层背景 +.arco-trigger-popup, +.arco-select-popup, +.arco-picker-dropdown, +.arco-dropdown, +.arco-modal, +.arco-drawer { + background-color: var(--surface-2); + border: 1px solid var(--border-color); + box-shadow: var(--shadow, 0 18px 48px oklch(0 0 0 / 0.55)); +} + +// 表格:去掉亮色分隔线的突兀感,表头沉下去 +.arco-table-th { + background-color: var(--surface-2); + color: $text-muted; + border-color: $border-color; + font-family: $font-code; + font-weight: 600; +} + +.arco-table-td { + border-color: var(--border-light); +} + +.arco-table-tr:hover .arco-table-td { + background-color: rgba(var(--accent-primary-rgb), 0.04); +} + +// 进度条 / 加载条 +.arco-progress-line .arco-progress-line-bar, +.arco-slider-track-bar { + background-color: var(--accent-primary); + box-shadow: 0 0 8px rgba(var(--accent-primary-rgb), 0.5); +} + html, body { position: fixed; top: 0; diff --git a/packages/client/src/views/LoginView.vue b/packages/client/src/views/LoginView.vue index cca143b..d6729ce 100644 --- a/packages/client/src/views/LoginView.vue +++ b/packages/client/src/views/LoginView.vue @@ -15,10 +15,15 @@ const password = ref(""); const loading = ref(false); const errorMsg = ref(""); const showLockResetHint = ref(false); +const showPassword = ref(false); // Arco's a-input sets inheritAttrs: false (no autofocus passthrough), // so focus the input explicitly on mount. const usernameInput = ref<{ focus: () => void } | null>(null); +// 探测结果只用于驱动状态灯,不改变登录流程(真实请求仍在提交时发出)。 +type ProbeState = "pending" | "online" | "offline"; +const probeState = ref("pending"); + if (hasApiKey()) { router.replace("/hermes/chat"); } @@ -26,9 +31,11 @@ if (hasApiKey()) { onMounted(async () => { usernameInput.value?.focus(); try { - await fetchAuthStatus(); + const status = await fetchAuthStatus(); + probeState.value = status?.hasPasswordLogin === false ? "offline" : "online"; } catch { // Login remains available; the submit request will surface connection errors. + probeState.value = "offline"; } }); @@ -61,48 +68,94 @@ async function handlePasswordLogin() { loading.value = false; } } + +const probeLabel = () => + probeState.value === "pending" + ? t("login.probing") + : probeState.value === "online" + ? t("login.online") + : t("login.offline"); + +function fillDefaultCredentials() { + username.value = "admin"; + password.value = "123456"; + errorMsg.value = ""; + usernameInput.value?.focus(); +} @@ -121,6 +189,7 @@ async function handlePasswordLogin() {