outrank Arco button variants and drop the credential hint

The neon tube rules never applied. Arco emits .arco-btn-primary along
with three attribute variants ([type='button'], [type='submit'],
[type='reset']) that lift specificity to (0,2,0), and their :hover forms
reach (0,3,0) — all of which beat a bare .arco-btn-primary. Every rule
now uses .arco-btn.arco-btn-X[type='...'] so the skin wins regardless of
the rendered html-type; login is a submit button, which is why it stayed
solid.

Verified with jsdom computed styles against the real arco.css.

Also remove the default-credential chips from the login page: printing
admin / 123456 on a sign-in screen leaks the credentials of a reachable
deployment. The one-click fill shortcut stays. The test that asserted
the hint is now shown is inverted to assert it is absent.
This commit is contained in:
2026-10-19 13:55:02 +08:00
parent 336d08fc05
commit ce184fc151
5 changed files with 67 additions and 79 deletions
+1 -2
View File
@@ -12,7 +12,6 @@ export default {
tokenLogin: 'Token',
usernamePlaceholder: 'Username',
passwordPlaceholder: 'Password',
defaultCredentialsHint: 'Default username: admin. Default password: 123456.',
credentialsRequired: 'Please enter username and password',
invalidCredentials: 'Invalid username or password',
tooManyAttempts: 'Too many failed attempts, please try again later',
@@ -24,7 +23,7 @@ export default {
offline: 'Cannot reach the server · you can still try to log in',
showPassword: 'Show password',
hidePassword: 'Hide password',
fillDefaultCredentials: 'Fill in admin / 123456',
fillDefaults: 'Sign in with the default account',
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.',
+1 -2
View File
@@ -12,7 +12,6 @@ export default {
tokenLogin: '令牌登录',
usernamePlaceholder: '用户名',
passwordPlaceholder: '密码',
defaultCredentialsHint: '默认登录名:admin,默认密码:123456',
credentialsRequired: '请输入用户名和密码',
invalidCredentials: '用户名或密码错误',
tooManyAttempts: '登录失败次数过多,请稍后重试',
@@ -24,7 +23,7 @@ export default {
offline: '无法连接到服务器 · 登录仍可尝试',
showPassword: '显示密码',
hidePassword: '隐藏密码',
fillDefaultCredentials: '填入 admin / 123456',
fillDefaults: '使用默认账号登录',
localDeploymentNote: '本地部署 · 无遥测 · 数据不出机器',
tokenHint: '登录后 POST /api/auth/login 返回的 token 存入 localStorage',
accessDenied: '你没有权限访问该资源',
+50 -15
View File
@@ -169,11 +169,26 @@ $neon-hv: var(--accent-hover);
text-shadow: 0 0 8px rgba(var(--accent-primary-rgb), 0.45);
}
// Arco 为按钮写了 `.arco-btn-primary[type='button']` / `[type='submit']` / `[type='reset']`
// 三个属性选择器变体,把特异性从 (0,1,0) 抬到 (0,2,0);它的 hover 变体则是
// `.arco-btn-primary[type='submit']:hover` → (0,3,0)。所以下面每条规则都用
// `.arco-btn.arco-btn-X[type='...']`(0,3,0)打底,状态规则用 (0,4,0),
// 稳压过 Arco 的一整族变体。只写 `.arco-btn-primary` 会被压过,而 a-button
// 默认 html-type 就是 button —— 漏掉属性选择器等于所有按钮都不生效。
// 默认按钮(secondary / 默认态):不发光的幽灵按钮,只在 hover 时点亮管壁
.arco-btn-secondary,
.arco-btn-secondary.arco-btn-status-warning,
.arco-btn-outline,
.arco-btn-default {
.arco-btn.arco-btn-secondary,
.arco-btn.arco-btn-secondary[type='button'],
.arco-btn.arco-btn-secondary[type='submit'],
.arco-btn.arco-btn-secondary[type='reset'],
.arco-btn.arco-btn-outline,
.arco-btn.arco-btn-outline[type='button'],
.arco-btn.arco-btn-outline[type='submit'],
.arco-btn.arco-btn-outline[type='reset'],
.arco-btn.arco-btn-default,
.arco-btn.arco-btn-default[type='button'],
.arco-btn.arco-btn-default[type='submit'],
.arco-btn.arco-btn-default[type='reset'] {
color: $text-secondary;
background: transparent;
border: 1px solid $border-color;
@@ -191,7 +206,10 @@ $neon-hv: var(--accent-hover);
}
// 主按钮:霓虹管
.arco-btn-primary {
.arco-btn.arco-btn-primary,
.arco-btn.arco-btn-primary[type='button'],
.arco-btn.arco-btn-primary[type='submit'],
.arco-btn.arco-btn-primary[type='reset'] {
@include neon-tube(0.34);
font-weight: 600;
@@ -229,7 +247,10 @@ $neon-hv: var(--accent-hover);
text-shadow: 0 0 8px rgba(var(--error-rgb), 0.4);
}
.arco-btn-dangerous {
.arco-btn.arco-btn-dangerous,
.arco-btn.arco-btn-dangerous[type='button'],
.arco-btn.arco-btn-dangerous[type='submit'],
.arco-btn.arco-btn-dangerous[type='reset'] {
@include neon-tube-danger(0.26);
font-weight: 500;
@@ -252,8 +273,14 @@ $neon-hv: var(--accent-hover);
}
// 文字 / 链接按钮:无管壁,只让文字带辉光
.arco-btn-text,
.arco-btn-link {
.arco-btn.arco-btn-text,
.arco-btn.arco-btn-link,
.arco-btn.arco-btn-text[type='button'],
.arco-btn.arco-btn-text[type='submit'],
.arco-btn.arco-btn-text[type='reset'],
.arco-btn.arco-btn-link[type='button'],
.arco-btn.arco-btn-link[type='submit'],
.arco-btn.arco-btn-link[type='reset'] {
background: transparent;
border: 1px solid transparent;
box-shadow: none;
@@ -268,12 +295,18 @@ $neon-hv: var(--accent-hover);
}
// 禁用态:灯管熄灭
.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 {
.arco-btn.arco-btn-primary.arco-btn-disabled,
.arco-btn.arco-btn-secondary.arco-btn-disabled,
.arco-btn.arco-btn-outline.arco-btn-disabled,
.arco-btn.arco-btn-dangerous.arco-btn-disabled,
.arco-btn.arco-btn-text.arco-btn-disabled,
.arco-btn.arco-btn-link.arco-btn-disabled,
.arco-btn.arco-btn-primary[type='button'].arco-btn-disabled,
.arco-btn.arco-btn-primary[type='submit'].arco-btn-disabled,
.arco-btn.arco-btn-secondary[type='button'].arco-btn-disabled,
.arco-btn.arco-btn-secondary[type='submit'].arco-btn-disabled,
.arco-btn.arco-btn-dangerous[type='button'].arco-btn-disabled,
.arco-btn.arco-btn-dangerous[type='submit'].arco-btn-disabled {
color: var(--text-faint);
background: transparent;
border-color: var(--border-light);
@@ -288,7 +321,9 @@ $neon-hv: var(--accent-hover);
50% { opacity: 0.62; }
}
.arco-btn-primary.arco-btn-loading {
.arco-btn.arco-btn-primary.arco-btn-loading,
.arco-btn.arco-btn-primary[type='button'].arco-btn-loading,
.arco-btn.arco-btn-primary[type='submit'].arco-btn-loading {
@include neon-tube(0.4);
animation: neon-pulse 1.1s ease-in-out infinite;
}
+1 -58
View File
@@ -169,13 +169,8 @@ function fillDefaultCredentials() {
</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") }}
⧉ {{ t("login.fillDefaults") }}
</button>
<p class="login-legal">
{{ t("login.localDeploymentNote") }}<br />
@@ -499,58 +494,6 @@ function fillDefaultCredentials() {
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;