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:
@@ -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',
|
||||
|
||||
@@ -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 个字符',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user