Files
dsh_shenxian/web/login.html
T
admin a06885295c feat(品牌标记): 统一换用「AI 芯片」标记,浮层改抠底发光版
- 新增 web/mark-glow-192.png(抠底透明版,浮层专用)及 favicon.ico/.png/-180/-192
- portal/login/admin/register 四页 head 由 /favicon.svg 改为 ico+png+180 三行
- portal.html 顶栏品牌位 /favicon.svg -> /favicon-192.png
- 浮层 assets/inject/recovery.js:三辐条 hub 改为抠底标记(光环 + 虚线环,
  青色 #38d6d0 / 紫 #7c58ff),标记 96->132px;发光由 box-shadow 改为
  filter:drop-shadow(box-shadow 沿元素矩形绘制,会在抠底图外画出一圈方形光晕)
- 顺带清掉旧品牌蓝 #4d7cfe / rgba(77,124,254) -> #7c58ff

标记真源 = 用户 2026-09-20 提供的六边形芯片图标。旧三辐条 favicon.svg 至此无引用。
2026-09-21 17:22:03 +08:00

148 lines
6.8 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title data-i18n="login.title">Sign in</title>
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" type="image/png" href="/favicon.png" />
<link rel="apple-touch-icon" href="/favicon-180.png" />
<link rel="stylesheet" href="/design.css" />
<link rel="stylesheet" href="/auth-ui.css" />
<!-- 档案 81 · R5:平台多语言运行时(词条表 + data-i18n 就地替换;默认英语) -->
<script src="/i18n.js"></script>
<style>
/* 档案 81 · R2-UI:提交忙碌态(点击后立刻可见;对齐 06-工作台UI规范 的按钮/圆角/主色) */
.btn[disabled] { opacity: .72; cursor: not-allowed; }
.btn.is-busy { display: inline-flex; align-items: center; justify-content: center; gap: 8px; }
.btn .sp { width: 14px; height: 14px; flex: 0 0 14px; border: 2px solid rgba(255,255,255,.45);
border-top-color: #fff; border-radius: 50%; animation: btnsp .7s linear infinite; }
@keyframes btnsp { to { transform: rotate(360deg); } }
.msg { min-height: 20px; }
/* 档案 81 · R5:语言切换(页脚式下拉,不新增页面;中性样式,不引外链) */
.auth-lang { margin-top: 14px; display: flex; align-items: center; justify-content: center; gap: 8px; }
.auth-lang .i18n-lang-label { font-size: 13px; opacity: .72; }
.auth-lang .i18n-lang { font-size: 13px; padding: 3px 8px; border-radius: 6px; cursor: pointer;
border: 1px solid rgba(0,0,0,.16); background: transparent; color: inherit; }
@media (prefers-reduced-motion: reduce) { .btn .sp { animation: none; } }
</style>
</head>
<body class="auth-bg">
<div class="auth-card">
<div class="auth-brand">
<span class="wordmark" data-i18n="brand.name">CapabilityNet</span>
</div>
<p class="auth-sub" data-i18n="login.sub">Welcome to the AI world</p>
<form id="login">
<div class="field">
<label for="username" data-i18n="login.username">Username</label>
<input id="username" name="username" autocomplete="username" required />
</div>
<div class="field">
<label for="password" data-i18n="login.password">Password</label>
<div class="pw-wrap">
<input id="password" name="password" type="password" autocomplete="current-password" required />
<button class="pw-toggle" type="button" id="pwToggle" data-i18n="common.show"
data-i18n-attr="aria-label:common.showPw" aria-label="Show password" aria-pressed="false">Show</button>
</div>
</div>
<button class="btn primary" type="submit" data-i18n="login.submit">Sign in</button>
<p class="msg" id="msg"></p>
</form>
<p class="auth-alt"><span data-i18n="login.noAccount">Don't have an account?</span>
<a href="/register.html" data-i18n="login.register">Sign up</a></p>
<!-- 档案 81 · R5:语言切换(默认英语;切换写 cookie dsh_lang 后重载) -->
<div class="auth-lang"><div data-i18n-switch="label"></div></div>
</div>
<script>
// 档案 83 · 显示/隐藏密码(对齐 06-工作台UI规范 的 .btn-icon 交互;键盘可达 = 原生 button)
const pwInput = document.getElementById('password')
const pwBtn = document.getElementById('pwToggle')
if (pwInput && pwBtn) {
pwBtn.addEventListener('click', () => {
const show = pwInput.type === 'password'
pwInput.type = show ? 'text' : 'password'
pwBtn.textContent = I18N.t(show ? 'common.hide' : 'common.show')
pwBtn.setAttribute('aria-label', I18N.t(show ? 'common.hidePw' : 'common.showPw'))
pwBtn.setAttribute('aria-pressed', String(show))
pwInput.focus()
})
}
const form = document.getElementById('login')
const msg = document.getElementById('msg')
// 档案 81 · R2-UI:任何提交都要有"看得见"的忙碌态(否则几秒内用户会以为没反应)
function setBusy(busy, label) {
const btn = form.querySelector('button[type=submit]')
if (!btn) return
if (busy) {
if (!btn.dataset.idle) btn.dataset.idle = btn.innerHTML
btn.disabled = true
btn.classList.add('is-busy')
btn.innerHTML = '<span class="sp"></span><span>' + (label || I18N.t('common.busy')) + '</span>'
} else {
btn.disabled = false
btn.classList.remove('is-busy')
if (btn.dataset.idle) btn.innerHTML = btn.dataset.idle
}
}
form.addEventListener('submit', async (event) => {
event.preventDefault()
msg.textContent = ''
msg.className = 'msg err'
setBusy(true, I18N.t('login.signingIn'))
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ username: form.username.value, password: form.password.value }),
})
if (!res.ok) {
const body = await res.json().catch(() => ({}))
msg.textContent =
body.error === 'pending_review' ? I18N.t('login.errPending')
: body.error === 'disabled' ? I18N.t('login.errDisabled')
: I18N.t('login.errBadCreds')
setBusy(false)
return
}
const body = await res.json()
const role = body && body.user ? body.user.role : null
// 所有用户(admin/active)登录后统一直达 DSH 会话窗口
setBusy(true, I18N.t('login.entering'))
try {
const r = await fetch('/api/dsh/enter', { method: 'POST' })
const b = await r.json().catch(() => ({}))
if (r.ok && b.kind === 'session' && b.url) {
location.href = b.url
return
}
// 失败兜底:admin 可手动进管理台 /portal.html
const errText =
b.error === 'instance_starting' ? I18N.t('login.errStarting')
: (b.error ?? 'HTTP ' + r.status)
// 档案 81 · R5:不再用 innerHTML 拼文案(安全);管理台入口用 DOM 组装
msg.textContent = I18N.t('login.errEnter') + errText
if (role === 'admin') {
const link = document.createElement('a')
link.href = '/portal.html'
link.textContent = I18N.t('login.gotoAdmin')
msg.appendChild(document.createTextNode('('))
msg.appendChild(link)
msg.appendChild(document.createTextNode(')'))
}
setBusy(false)
} catch {
msg.textContent = I18N.t('login.errNetwork')
setBusy(false)
}
})
</script>
</body>
</html>