feat: 首页重造为光湖世界大门——编号门(灯塔查号只答有效/无效)→域浮起(欢迎语+湖湾升起)→钥匙门(账号密码),复用zero_point校验器官(冰朔20260816谕:先校验编号再路由到域)

This commit is contained in:
冰朔 2026-08-16 01:12:54 +08:00
commit ff10aaaea8
2 changed files with 94 additions and 1 deletions

View file

@ -257,6 +257,11 @@ function HoloLakeApp() {
const [loginBusy, setLoginBusy] = useState(false) const [loginBusy, setLoginBusy] = useState(false)
const [loginRising, setLoginRising] = useState(false) const [loginRising, setLoginRising] = useState(false)
const [loginMessage, setLoginMessage] = useState('') const [loginMessage, setLoginMessage] = useState('')
const [gateStage, setGateStage] = useState<'number' | 'key'>('number')
const [gateNumber, setGateNumber] = useState('')
const [gateBusy, setGateBusy] = useState(false)
const [gateMessage, setGateMessage] = useState('')
const [gateRising, setGateRising] = useState(false)
const [systemBusy, setSystemBusy] = useState(false) const [systemBusy, setSystemBusy] = useState(false)
const [zeroPoint, setZeroPoint] = useState<ZeroPointSnapshot | null>(null) const [zeroPoint, setZeroPoint] = useState<ZeroPointSnapshot | null>(null)
const [zpNumber, setZpNumber] = useState('') const [zpNumber, setZpNumber] = useState('')
@ -313,6 +318,30 @@ function HoloLakeApp() {
if (zeroPoint.route !== 'persona' && view === 'parlor') setView('generic') if (zeroPoint.route !== 'persona' && view === 'parlor') setView('generic')
if (zeroPoint.route === 'persona' && view === 'generic') setView('knowledge') if (zeroPoint.route === 'persona' && view === 'generic') setView('knowledge')
}, [zeroPoint, view]) }, [zeroPoint, view])
// 大门·编号门:本机已有绑定编号时回填,省得主人重报家门
useEffect(() => {
if (gateStage === 'number' && !gateNumber && zeroPoint?.binding === 'bound' && zeroPoint.userNumber) setGateNumber(zeroPoint.userNumber)
}, [zeroPoint, gateStage, gateNumber])
// 大门·编号门:先报编号→灯塔查号→只答有效/无效→域浮起→才见钥匙门
const gateVerifyNumber = async () => {
setGateBusy(true)
setGateMessage('')
try {
await invoke<ZeroPointSnapshot>('zero_point_bind', { input: { number: gateNumber.trim() } })
const snapshot = await invoke<ZeroPointSnapshot>('zero_point_verify')
setZeroPoint(snapshot)
if (snapshot.route === 'persona') {
setGateRising(true)
window.setTimeout(() => { setGateRising(false); setGateStage('key') }, 1900)
} else {
setGateMessage('编号未获灯塔放行——灯塔只答有效或无效,不透露编号表。')
}
} catch (error) {
setGateMessage(humanError(error, 'login'))
} finally {
setGateBusy(false)
}
}
useEffect(() => { if (view === 'receipts') void loadReceipts() }, [loadReceipts, view]) useEffect(() => { if (view === 'receipts') void loadReceipts() }, [loadReceipts, view])
const visibleDocuments = useMemo(() => { const visibleDocuments = useMemo(() => {
@ -877,7 +906,48 @@ function HoloLakeApp() {
if (!repoLogin) { if (!repoLogin) {
return <div className="app-shell"> return <div className="app-shell">
<div className={`onboarding-backdrop lake-scene${loginRising ? ' sinking' : ''}`}><div className="lake-bays" aria-hidden="true">{([['main', '主域'], ['sub', '分域'], ['zero', '零域'], ['sense', '零感域'], ['fifth', '第五域']] as const).map(([bayId, bayName]) => <span key={bayId} className={`lake-bay ${bayId}${loginRising && bayId === 'fifth' ? ' rising' : ''}`}><i/><b>{bayName}</b></span>)}</div><section className={`onboarding-card${loginRising ? ' fade-out' : ''}`} role="dialog"><span className="onboarding-mark"></span><span className="kicker">CODE REPOSITORY ENTRY</span><h1></h1><p>使guanghulab.com</p><form onSubmit={(event) => void performRepoLogin(event)}><label htmlFor="repo-login-username"></label><input id="repo-login-username" autoFocus maxLength={40} value={loginUsername} placeholder="代码仓库用户名" onChange={(event) => setLoginUsername(event.target.value)}/><label htmlFor="repo-login-password"></label><input id="repo-login-password" type="password" maxLength={512} value={loginPassword} placeholder="代码仓库密码" onChange={(event) => setLoginPassword(event.target.value)}/><button className="primary-button" disabled={loginBusy || !loginUsername.trim() || !loginPassword}>{loginBusy ? '正在验证…' : '进入 HoloLake'}</button></form>{loginMessage && <p>{loginMessage}</p>}</section></div> <div className={`onboarding-backdrop lake-scene${loginRising ? ' sinking' : ''}`}>
<div className="lake-bays" aria-hidden="true">{([['main', '主域'], ['sub', '分域'], ['zero', '零域'], ['sense', '零感域'], ['fifth', '第五域']] as const).map(([bayId, bayName]) => <span key={bayId} className={`lake-bay ${bayId}${(gateRising || loginRising) && bayId === 'fifth' ? ' rising' : ''}`}><i/><b>{bayName}</b></span>)}</div>
{gateRising ? (
<section className="gate-welcome" role="status">
<span className="gate-welcome-kicker"> · </span>
<h1> · </h1>
<p> {gateNumber} </p>
</section>
) : (
<section className={`onboarding-card gate-card${loginRising ? ' fade-out' : ''}`} role="dialog">
<span className="onboarding-mark gate-mark"></span>
<div className="gate-steps" aria-hidden="true"><span className={gateStage === 'number' ? 'active' : 'done'}><i></i></span><em/><span className={gateStage === 'key' ? 'active' : ''}><i></i></span></div>
{gateStage === 'number' ? (
<>
<span className="kicker">GATE OF GUANGHU WORLD</span>
<h1></h1>
<p>"有效 / 无效"</p>
<form onSubmit={(event) => { event.preventDefault(); void gateVerifyNumber() }}>
<label htmlFor="gate-number"></label>
<input id="gate-number" autoFocus maxLength={40} value={gateNumber} placeholder="如 ICE-GL∞" onChange={(event) => setGateNumber(event.target.value)}/>
<button className="primary-button" disabled={gateBusy || !gateNumber.trim()}>{gateBusy ? '灯塔查号中…' : '校验编号'}</button>
</form>
</>
) : (
<>
<span className="kicker">FIFTH DOMAIN · KEY GATE</span>
<h1> · </h1>
<p> <b className="gate-number-chip">{gateNumber}</b> guanghulab.com</p>
<form onSubmit={(event) => void performRepoLogin(event)}>
<label htmlFor="repo-login-username"></label>
<input id="repo-login-username" autoFocus maxLength={40} value={loginUsername} placeholder="代码仓库用户名" onChange={(event) => setLoginUsername(event.target.value)}/>
<label htmlFor="repo-login-password"></label>
<input id="repo-login-password" type="password" maxLength={512} value={loginPassword} placeholder="代码仓库密码" onChange={(event) => setLoginPassword(event.target.value)}/>
<button className="primary-button" disabled={loginBusy || !loginUsername.trim() || !loginPassword}>{loginBusy ? '正在验证…' : '进入 HoloLake'}</button>
</form>
<button className="gate-back" type="button" onClick={() => { setGateStage('number'); setGateMessage(''); setLoginMessage('') }}></button>
</>
)}
{(gateMessage || loginMessage) && <p className="gate-message">{gateMessage || loginMessage}</p>}
</section>
)}
</div>
</div> </div>
} }

View file

@ -436,3 +436,26 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
@keyframes bay-rise { 0% { transform: translateY(0) scale(1); } 55% { transform: translateY(-52px) scale(1.2); } 100% { transform: translateY(-150px) scale(1.9); } } @keyframes bay-rise { 0% { transform: translateY(0) scale(1); } 55% { transform: translateY(-52px) scale(1.2); } 100% { transform: translateY(-150px) scale(1.9); } }
.onboarding-card { position: relative; z-index: 1; transition: opacity .7s ease, transform .7s ease; } .onboarding-card { position: relative; z-index: 1; transition: opacity .7s ease, transform .7s ease; }
.onboarding-card.fade-out { opacity: 0; transform: translateY(16px) scale(.985); } .onboarding-card.fade-out { opacity: 0; transform: translateY(16px) scale(.985); }
/* ===== 光湖世界大门(编号门→域浮起→钥匙门)===== */
.gate-card { width: min(520px, 100%); text-align: center; }
.gate-card form { text-align: left; }
.gate-mark { width: 72px; height: 72px; margin: 0 auto 20px; border-radius: 22px; font-size: 22px; border-color: color-mix(in srgb, var(--primitive-warm-glow) 46%, transparent); color: var(--accent-light); box-shadow: 0 0 42px color-mix(in srgb, var(--primitive-warm-glow) 30%, transparent), inset 0 0 22px color-mix(in srgb, var(--primitive-warm-glow) 14%, transparent); animation: gate-mark-breathe 4.5s ease-in-out infinite; }
@keyframes gate-mark-breathe { 0%, 100% { box-shadow: 0 0 30px color-mix(in srgb, var(--primitive-warm-glow) 22%, transparent), inset 0 0 18px color-mix(in srgb, var(--primitive-warm-glow) 10%, transparent); } 50% { box-shadow: 0 0 58px color-mix(in srgb, var(--primitive-warm-glow) 40%, transparent), inset 0 0 26px color-mix(in srgb, var(--primitive-warm-glow) 18%, transparent); } }
.gate-card h1 { font-size: 30px; letter-spacing: .04em; }
.gate-card .kicker { letter-spacing: .3em; color: color-mix(in srgb, var(--primitive-warm-glow) 78%, var(--content-muted)); }
.gate-steps { display: flex; align-items: center; justify-content: center; gap: 12px; margin: 0 0 22px; }
.gate-steps span { display: inline-flex; align-items: center; gap: 7px; padding: 5px 13px; border: 1px solid var(--panel-edge); border-radius: 999px; color: var(--content-faint); font-size: 12px; letter-spacing: .12em; transition: all .4s ease; }
.gate-steps span i { display: grid; place-items: center; width: 18px; height: 18px; border-radius: 50%; border: 1px solid currentColor; font-style: normal; font-size: 10px; }
.gate-steps span.active { color: var(--accent-light); border-color: color-mix(in srgb, var(--primitive-warm-glow) 52%, transparent); background: color-mix(in srgb, var(--primitive-warm-glow) 9%, transparent); box-shadow: 0 0 18px color-mix(in srgb, var(--primitive-warm-glow) 18%, transparent); }
.gate-steps span.done { color: var(--content-secondary); border-color: color-mix(in srgb, var(--accent-light) 34%, transparent); }
.gate-steps em { width: 34px; height: 1px; background: linear-gradient(90deg, var(--panel-edge), color-mix(in srgb, var(--primitive-warm-glow) 42%, transparent)); }
.gate-number-chip { padding: 1px 8px; border: 1px solid color-mix(in srgb, var(--primitive-warm-glow) 44%, transparent); border-radius: 7px; color: var(--accent-light); font-weight: 600; letter-spacing: .03em; }
.gate-back { margin-top: 16px; border: 0; background: none; color: var(--content-faint); font-size: 12.5px; letter-spacing: .08em; cursor: pointer; transition: color .25s ease; }
.gate-back:hover { color: var(--accent-light); }
.gate-message { margin-top: 14px; color: var(--content-muted); font-size: 13px; }
.gate-welcome { position: relative; z-index: 2; text-align: center; animation: gate-welcome-in 1s cubic-bezier(.3, .7, .35, 1) both; }
.gate-welcome-kicker { display: inline-block; padding: 6px 16px; border: 1px solid color-mix(in srgb, var(--primitive-warm-glow) 40%, transparent); border-radius: 999px; color: var(--accent-light); font-size: 12px; letter-spacing: .26em; text-indent: .26em; background: color-mix(in srgb, var(--primitive-warm-glow) 8%, transparent); }
.gate-welcome h1 { margin: 20px 0 12px; color: var(--content-primary); font-size: clamp(30px, 4.6vw, 44px); font-weight: 650; letter-spacing: .06em; text-shadow: 0 0 46px color-mix(in srgb, var(--primitive-warm-glow) 34%, transparent); }
.gate-welcome p { color: var(--content-muted); font-size: 14.5px; line-height: 1.7; }
@keyframes gate-welcome-in { 0% { opacity: 0; transform: translateY(26px) scale(.97); } 100% { opacity: 1; transform: translateY(0) scale(1); } }