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.
This commit is contained in:
2026-10-19 13:47:12 +08:00
parent 282624f801
commit 336d08fc05
4 changed files with 755 additions and 69 deletions
+8
View File
@@ -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',
+8
View File
@@ -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 个字符',
+299 -2
View File
@@ -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;
+440 -67
View File
@@ -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<ProbeState>("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();
}
</script>
<template>
<div class="login-view">
<!-- 装饰位面:只承载 ArkScope 动画,不承载信息/交互 -->
<div class="login-decor" aria-hidden="true">
<div class="login-grid"></div>
<div class="login-scanline"></div>
<IsometricWave />
</div>
<div class="login-card">
<div class="login-logo">
<img src="/logo.png" alt="Hermes" width="80" height="80" />
</div>
<h1 class="login-title">
<ScrambleTitle :content="t('login.title')" />
</h1>
<p class="login-desc">{{ t("login.description") }}</p>
<p class="login-default-hint">{{ t("login.defaultCredentialsHint") }}</p>
<div class="login-neon-edge" aria-hidden="true"></div>
<header class="login-hd">
<div class="login-badge" aria-hidden="true">H</div>
<h1 class="login-title">
<ScrambleTitle :content="t('login.title')" />
</h1>
<p class="login-desc">{{ t("login.description") }}</p>
<p class="login-probe">
<span class="login-led" :class="probeState" aria-hidden="true"></span>
<span>{{ probeLabel() }}</span>
</p>
</header>
<form class="login-form" @submit.prevent="handleLogin">
<a-input
v-model="username"
ref="usernameInput"
class="login-input"
size="large"
:placeholder="t('login.usernamePlaceholder')"
/>
<a-input
v-model="password"
class="login-input"
size="large"
type="password"
:placeholder="t('login.passwordPlaceholder')"
@keyup.enter="handleLogin"
/>
<div class="login-field">
<label for="login-username">{{ t("login.username") }}</label>
<div class="login-input-wrap">
<span class="login-input-icon" aria-hidden="true">◉</span>
<a-input
id="login-username"
v-model="username"
ref="usernameInput"
class="login-input"
size="large"
:placeholder="t('login.usernamePlaceholder')"
/>
</div>
</div>
<div v-if="errorMsg" class="login-error">{{ errorMsg }}</div>
<div class="login-field">
<label for="login-password">{{ t("login.passwordPlaceholder") }}</label>
<div class="login-input-wrap">
<span class="login-input-icon" aria-hidden="true">⌸</span>
<a-input
id="login-password"
v-model="password"
class="login-input"
size="large"
:type="showPassword ? 'text' : 'password'"
:placeholder="t('login.passwordPlaceholder')"
@keyup.enter="handleLogin"
/>
<button
type="button"
class="login-eye"
:aria-label="showPassword ? t('login.hidePassword') : t('login.showPassword')"
@click="showPassword = !showPassword"
>
{{ showPassword ? "⊘" : "◉" }}
</button>
</div>
</div>
<p v-if="errorMsg" class="login-error">{{ errorMsg }}</p>
<div v-if="showLockResetHint" class="login-lock-hint">
<span>{{ t("login.lockResetHint") }}</span>
<code>hermes-ui clear-login-locks --restart</code>
<span>{{ t("login.defaultLoginResetHint") }}</span>
<code>hermes-ui reset-default-login</code>
</div>
<a-button
html-type="submit"
type="primary"
@@ -114,6 +167,21 @@ async function handlePasswordLogin() {
{{ loading ? "..." : t("login.submit") }}
</a-button>
</form>
<footer class="login-ft">
<div class="login-sep"><span>{{ t("login.defaultCredentialsHint") }}</span></div>
<div class="login-chips">
<span class="login-chip"><span class="dot"></span>{{ t("login.username") }} <b>admin</b></span>
<span class="login-chip"><span class="dot"></span>{{ t("login.passwordPlaceholder") }} <b>123456</b></span>
</div>
<button type="button" class="login-copy" @click="fillDefaultCredentials">
⧉ {{ t("login.fillDefaultCredentials") }}
</button>
<p class="login-legal">
{{ t("login.localDeploymentNote") }}<br />
<b>{{ t("login.tokenHint") }}</b>
</p>
</footer>
</div>
</div>
</template>
@@ -121,6 +189,7 @@ async function handlePasswordLogin() {
<style scoped lang="scss">
@use "@/styles/variables" as *;
// ─── 布局 ────────────────────────────────────────────────────────────────
.login-view {
position: relative;
height: calc(100 * var(--vh));
@@ -131,102 +200,406 @@ async function handlePasswordLogin() {
overflow: hidden;
}
// 装饰位面:只负责好看,一律 pointer-events: none(基线硬规则)
// ─── 装饰位面 ────────────────────────────────────────────────────────────
// 只承载 ArkScope 动画,一律 pointer-events: none(基线硬规则)。
// 不承载任何信息,也不能出现在点击密集区。
.login-decor {
position: absolute;
left: 50%;
bottom: -48px;
transform: translateX(-50%);
width: min(100%, 760px);
opacity: 0.55;
inset: 0;
overflow: hidden;
pointer-events: none;
z-index: 0;
}
// 32px 网格 mask,与原型一致
.login-grid {
position: absolute;
inset: -40%;
background:
repeating-linear-gradient(0deg, rgba(45, 212, 191, 0.055) 0 1px, transparent 1px 32px),
repeating-linear-gradient(90deg, rgba(45, 212, 191, 0.055) 0 1px, transparent 1px 32px);
mask-image: radial-gradient(62% 52% at 50% 34%, #000, transparent 78%);
}
// 缓慢下移的扫描线
@keyframes login-scan {
0% { transform: translateY(-64px); }
100% { transform: translateY(100vh); }
}
.login-scanline {
position: absolute;
left: 0;
right: 0;
height: 64px;
background: linear-gradient(
180deg,
transparent,
rgba(45, 212, 191, 0.07) 45%,
rgba(45, 212, 191, 0.14) 50%,
rgba(45, 212, 191, 0.07) 55%,
transparent
);
animation: login-scan 7.5s linear infinite;
}
// ArkScope 等距波形,对齐原型的位置与透明度
.login-decor :deep(.iso-wrap) {
position: absolute;
left: 50%;
bottom: -56px;
transform: translateX(-50%);
width: min(100%, 780px);
opacity: 0.42;
pointer-events: none;
}
// ─── 卡片 ────────────────────────────────────────────────────────────────
.login-card {
position: relative;
z-index: 1;
width: 480px;
max-width: calc(100vw - 32px);
padding: 56px;
z-index: 2;
width: 100%;
max-width: 424px;
padding: 0;
border: 1px solid $border-color;
border-radius: $radius-lg;
background: $bg-card;
text-align: center;
background: $bg-secondary;
box-shadow: 0 24px 64px oklch(0 0 0 / 0.6);
overflow: hidden;
@media (max-width: $breakpoint-mobile) {
padding: 32px 24px;
max-width: calc(100vw - 32px);
}
}
.login-logo {
margin-bottom: 24px;
// 卡片顶部的霓虹发丝线
.login-neon-edge {
position: absolute;
left: 16px;
right: 16px;
top: 0;
height: 1px;
background: linear-gradient(90deg, transparent, $accent-primary, transparent);
opacity: 0.6;
pointer-events: none;
}
.login-hd {
padding: 30px 30px 0;
text-align: center;
}
.login-badge {
width: 62px;
height: 62px;
margin: 0 auto 16px;
border: 1px solid $accent-primary;
border-radius: $radius-lg;
display: grid;
place-items: center;
color: $accent-primary;
font-family: $font-code;
font-size: 24px;
font-weight: 700;
line-height: 1;
box-shadow:
0 0 0 1px rgba(var(--accent-primary-rgb), 0.2),
0 0 30px rgba(var(--accent-primary-rgb), 0.18) inset;
}
.login-title {
font-size: 26px;
margin: 0;
font-family: $font-code;
font-size: 19px;
font-weight: 600;
line-height: 1.25;
letter-spacing: 0.08em;
color: $text-primary;
margin: 0 0 10px;
}
.login-desc {
font-size: 14px;
color: $text-muted;
margin: 0 0 12px;
margin: 9px 0 0;
font-size: 12.5px;
line-height: 1.6;
color: $text-muted;
}
.login-default-hint {
margin: 0 0 28px;
// 服务端探测状态灯
.login-probe {
margin: 14px 0 0;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 5px 10px;
border: 1px dashed var(--border-light);
border-radius: $radius-sm;
background: $bg-card;
font-family: $font-code;
font-size: 13px;
color: $text-secondary;
font-size: 11.5px;
line-height: 1.5;
color: $text-muted;
}
@keyframes login-blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0.25; }
}
.login-led {
width: 7px;
height: 7px;
flex: 0 0 auto;
border-radius: 50%;
background: var(--text-faint);
box-shadow: 0 0 6px currentColor;
}
.login-led.online {
background: $success;
color: $success;
}
.login-led.offline {
background: $error;
color: $error;
}
.login-led.pending {
background: $accent-primary;
color: $accent-primary;
animation: login-blink 1s ease-in-out infinite;
}
// ─── 表单 ────────────────────────────────────────────────────────────────
.login-form {
padding: 22px 30px 26px;
display: flex;
flex-direction: column;
gap: 14px;
}
.login-input {
width: 100%;
.login-field {
display: flex;
flex-direction: column;
gap: 6px;
// Arco owns the chrome (border/focus ring); only align type tokens here.
:deep(.arco-input) {
font-family: $font-code;
font-size: 15px;
> label {
font-size: 11.5px;
letter-spacing: 0.04em;
color: $text-muted;
}
}
.login-error {
font-size: 13px;
color: $error;
text-align: left;
.login-input-wrap {
position: relative;
display: flex;
align-items: center;
}
.login-lock-hint {
.login-input-icon {
position: absolute;
left: 11px;
z-index: 1;
font-family: $font-code;
font-size: 13px;
color: var(--text-faint);
pointer-events: none;
}
.login-eye {
position: absolute;
right: 9px;
z-index: 1;
width: 22px;
height: 22px;
padding: 0;
border: 0;
border-radius: 4px;
background: transparent;
color: var(--text-faint);
font-family: $font-code;
font-size: 12px;
line-height: 1;
cursor: pointer;
transition: color 0.14s ease, background-color 0.14s ease;
&:hover {
color: $accent-primary;
background: $bg-card-hover;
}
}
// Arco owns the input chrome; only reserve room for the leading icon.
.login-input-wrap :deep(.arco-input-wrapper) {
padding-left: 32px;
}
.login-input-wrap:has(.login-eye) :deep(.arco-input-wrapper) {
padding-right: 34px;
}
.login-field :deep(.arco-input-wrapper) {
height: 40px;
border-radius: $radius-sm;
background-color: $bg-input;
font-family: $font-code;
font-size: 14px;
}
// ─── 错误提示 ────────────────────────────────────────────────────────────
.login-error {
margin: 0;
padding: 10px 12px;
border: 1px solid rgba(var(--warning-rgb), 0.35);
border: 1px solid rgba(var(--error-rgb), 0.42);
border-radius: $radius-sm;
background: rgba(var(--error-rgb), 0.09);
font-size: 12px;
line-height: 1.55;
text-align: left;
color: $error;
}
// 锁定时的解锁指引:与错误条分开,用发丝线框承载命令
.login-lock-hint {
display: flex;
flex-direction: column;
gap: 5px;
padding: 10px 12px;
border: 1px solid rgba(var(--warning-rgb), 0.4);
border-radius: $radius-sm;
background: rgba(var(--warning-rgb), 0.08);
color: $text-secondary;
font-size: 12px;
line-height: 1.5;
line-height: 1.55;
text-align: left;
color: $warning;
code {
display: block;
margin-top: 4px;
color: $text-primary;
padding: 3px 6px;
border: 1px solid var(--border-light);
border-radius: 4px;
background: $code-bg;
font-family: $font-code;
font-size: 11.5px;
color: $text-primary;
word-break: break-all;
user-select: all;
}
}
// Arco owns the button chrome (long/size/loading); only align the radius token.
.login-btn {
height: 40px;
justify-content: center;
letter-spacing: 0.06em;
font-size: 13.5px;
}
// ─── 页脚 ────────────────────────────────────────────────────────────────
.login-ft {
padding: 0 30px 24px;
text-align: center;
}
.login-sep {
display: flex;
align-items: center;
gap: 11px;
margin: 0 0 14px;
font-size: 11px;
color: var(--text-faint);
&::before,
&::after {
content: "";
flex: 1;
height: 1px;
background: var(--border-light);
}
}
.login-chips {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 7px;
margin-bottom: 14px;
}
.login-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 9px;
border: 1px solid var(--border-light);
border-radius: 20px;
background: $bg-card;
font-family: $font-code;
font-size: 11px;
line-height: 1.4;
color: $text-muted;
b {
color: $accent-primary;
font-weight: 600;
}
.dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: $success;
box-shadow: 0 0 5px $success;
}
}
.login-copy {
width: 100%;
height: 34px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
padding: 0;
border: 1px solid $border-color;
border-radius: $radius-sm;
background: transparent;
font-family: $font-code;
font-size: 12.5px;
color: $text-secondary;
cursor: pointer;
transition: color 0.16s ease, border-color 0.16s ease,
box-shadow 0.16s ease, background-color 0.16s ease;
&:hover {
color: $accent-primary;
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);
}
}
.login-legal {
margin: 14px 0 0;
font-size: 10.5px;
line-height: 1.7;
color: var(--text-faint);
b {
color: $text-muted;
font-weight: 400;
}
}
@media (max-width: $breakpoint-mobile) {
.login-hd {
padding: 26px 22px 0;
}
.login-form {
padding: 20px 22px 22px;
}
.login-ft {
padding: 0 22px 20px;
}
}
</style>