feat: 管理端登录视觉调整,并减轻客户登录首屏等待

登录页不再预热 VXE/Element,顾问 CTA 预热 /login,角色与全量语言包延后加载。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-10-06 14:29:29 +08:00
parent 5178b6379f
commit 0d5ebdb591
8 changed files with 248 additions and 134 deletions

View File

@ -752,6 +752,30 @@
return omsUrl(preferOmsH5() ? '/h5/login' : '/login');
}
let omsLoginWarmed = false;
function warmupOmsLogin() {
if (omsLoginWarmed) return;
omsLoginWarmed = true;
try {
const url = loginPortalUrl();
const origin = resolveOmsOrigin();
const preconnect = document.createElement('link');
preconnect.rel = 'preconnect';
preconnect.href = origin;
document.head.appendChild(preconnect);
const prefetch = document.createElement('link');
prefetch.rel = 'prefetch';
prefetch.href = url;
document.head.appendChild(prefetch);
if (/localhost|127\.0\.0\.1/i.test(new URL(origin).hostname)) {
fetch(new URL('/src/views/login/index.vue', origin), {
credentials: 'omit',
mode: 'cors',
}).catch(function () {});
}
} catch (e) {}
}
function registerUrl(dest) {
// 官网「注册询价」走客户登录页微信扫码(注册/登录同一入口)
const url = new URL(loginPortalUrl());
@ -2654,7 +2678,13 @@
if (heroCta) {
heroCta.textContent = t('heroCta');
heroCta.href = loginPortalUrl();
if (!heroCta.dataset.omsWarm) {
heroCta.dataset.omsWarm = '1';
heroCta.addEventListener('pointerenter', warmupOmsLogin, { once: true });
heroCta.addEventListener('focus', warmupOmsLogin, { once: true });
}
}
warmupOmsLogin();
const welcomeHuman = shell.querySelector('.ai-welcome-human');
if (welcomeHuman) welcomeHuman.textContent = t('human') + ' →';
renderWelcomeStats();

View File

@ -66,6 +66,14 @@
: 'IDE客户中心'
: 'IDE I Faster & Simpler';
} catch (e2) {}
if (path === '/login') {
var plane = document.createElement('link');
plane.rel = 'preload';
plane.as = 'image';
plane.href = '/login-assets/hero-plane.webp';
plane.type = 'image/webp';
document.head.appendChild(plane);
}
})();
</script>
<link rel="icon" type="image/png" href="/favicon.png" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

View File

@ -135,8 +135,8 @@ async function bootstrap() {
installWeChatH5HistoryGuard(router);
initAuthKeepAlive();
// PC 登录/首页可后台预热;H5 全程跳过,避免全局 CSS 打乱首屏
if ((light || isHomePath(entryPath)) && !isH5LikePath(entryPath)) {
// 登录页勿预热 VXE/Element:体积大且会注入全局 CSS,和首屏抢带宽
if (isHomePath(entryPath) && !isH5LikePath(entryPath) && !light) {
void ensureHeavyPlugins();
}

View File

@ -26,8 +26,6 @@
<template v-else>
<div class="login-hero__admin-stack">
<p class="login-hero__brief">{{ t('login.hero.admin.brief') }}</p>
<ol
class="login-hero__principles"
@mouseenter="hovered = true"
@ -38,12 +36,12 @@
<li
v-for="(pillar, index) in adminPillars"
:key="pillar.key"
class="login-hero__principle"
class="login-hero__principle login-hero__spine-item login-hero__spine-item--left"
:class="{
'is-active': activePrinciple === index,
'is-dim': activePrinciple >= 0 && activePrinciple !== index,
}"
:style="{ '--i': index }"
:style="{ '--i': index, '--t': pillar.t }"
tabindex="0"
@mouseenter="activePrinciple = index"
@focus="activePrinciple = index"
@ -61,27 +59,23 @@
</li>
</ol>
<div class="login-hero__ai" role="note">
<div
class="login-hero__ai login-hero__spine-item login-hero__spine-item--left"
role="note"
style="--t: 0.72"
>
<span class="login-hero__ai-badge">
<icon-robot :size="14" />
AI
</span>
<p class="login-hero__ai-text">{{ t('login.hero.admin.ai') }}</p>
</div>
<p class="login-hero__flow" aria-hidden="true">
<span>{{ t('login.hero.admin.flow.pickup') }}</span>
<i />
<span>{{ t('login.hero.admin.flow.transit') }}</span>
<i />
<span>{{ t('login.hero.admin.flow.delivery') }}</span>
</p>
</div>
</template>
</div>
<div class="login-hero__visual">
<picture>
<picture v-if="isCustomer">
<source srcset="/login-assets/hero-plane.webp" type="image/webp" />
<img
class="login-hero__plane"
@ -90,6 +84,13 @@
draggable="false"
/>
</picture>
<img
v-else
class="login-hero__truck"
src="/login-assets/hero-truck.png"
alt=""
draggable="false"
/>
</div>
</div>
</div>
@ -116,8 +117,37 @@ interface AdminPillar {
icon: Component;
titleKey: string;
descKey: string;
side: 'left' | 'right';
t: number;
}
const adminPillars: AdminPillar[] = [
{
key: 'care',
icon: IconCheckCircle,
titleKey: 'login.hero.admin.pillar.care.title',
descKey: 'login.hero.admin.pillar.care.desc',
side: 'left',
t: 0.24,
},
{
key: 'speed',
icon: IconThunderbolt,
titleKey: 'login.hero.admin.pillar.speed.title',
descKey: 'login.hero.admin.pillar.speed.desc',
side: 'left',
t: 0.40,
},
{
key: 'reach',
icon: IconMindMapping,
titleKey: 'login.hero.admin.pillar.reach.title',
descKey: 'login.hero.admin.pillar.reach.desc',
side: 'left',
t: 0.56,
},
];
const props = withDefaults(
defineProps<{
light?: boolean;
@ -138,27 +168,6 @@ let cycleTimer: ReturnType<typeof window.setInterval> | null = null;
const isCustomer = computed(() => props.portal === 'customer');
const adminPillars: AdminPillar[] = [
{
key: 'care',
icon: IconCheckCircle,
titleKey: 'login.hero.admin.pillar.care.title',
descKey: 'login.hero.admin.pillar.care.desc',
},
{
key: 'speed',
icon: IconThunderbolt,
titleKey: 'login.hero.admin.pillar.speed.title',
descKey: 'login.hero.admin.pillar.speed.desc',
},
{
key: 'reach',
icon: IconMindMapping,
titleKey: 'login.hero.admin.pillar.reach.title',
descKey: 'login.hero.admin.pillar.reach.desc',
},
];
function startCycle() {
stopCycle();
if (isCustomer.value) return;
@ -258,25 +267,75 @@ onBeforeUnmount(() => {
max-width: 520px;
}
.login-hero--admin .login-hero__inner {
justify-content: flex-start;
align-items: center;
.login-hero--admin {
width: 100%;
height: 100%;
}
.login-hero--admin .login-hero__content {
flex: 1 1 auto;
width: 100%;
max-width: 520px;
.login-hero--admin .login-hero__inner {
display: block;
position: relative;
width: 100%;
height: 100%;
}
.login-hero--admin .login-hero__content,
.login-hero--admin .login-hero__admin-stack,
.login-hero--admin .login-hero__principles {
display: contents;
}
.login-hero--admin .login-hero__spine-item {
--split-x: calc(
var(--login-split-top) + (var(--login-split-bottom) - var(--login-split-top)) * var(--t, 0.5)
);
position: absolute;
top: calc(var(--t, 0.5) * 100%);
left: auto;
right: calc(100% - var(--split-x) + 20px);
width: min(360px, 30vw);
margin: 0;
padding-right: 0;
pointer-events: auto;
}
.login-hero--admin .login-hero__spine-item--left {
transform: translateY(-50%);
}
.login-hero--admin .login-hero__principle {
animation-name: login-hero-principle-in-admin;
transition:
background 0.25s ease,
opacity 0.25s ease;
&::before {
display: none;
}
&.is-active {
transform: translateY(-50%);
}
}
@keyframes login-hero-principle-in-admin {
from {
opacity: 0;
transform: translateY(-50%);
}
to {
opacity: 1;
transform: translateY(-50%);
}
}
.login-hero__admin-stack {
width: 100%;
max-width: 460px;
max-width: 440px;
}
.login-hero__brief {
margin: 0 0 22px;
margin: 0 0 18px;
font-size: 14px;
font-weight: 500;
letter-spacing: 0.08em;
@ -299,8 +358,8 @@ onBeforeUnmount(() => {
display: grid;
grid-template-columns: 44px minmax(0, 1fr) 36px;
align-items: center;
gap: 14px;
padding: 16px 12px 16px 4px;
gap: 12px;
padding: 12px 8px 12px 2px;
border-radius: 10px;
cursor: default;
outline: none;
@ -371,8 +430,8 @@ onBeforeUnmount(() => {
}
.login-hero__principle-title {
margin: 0 0 6px;
font-size: 22px;
margin: 0 0 4px;
font-size: 20px;
font-weight: 700;
line-height: 1.2;
letter-spacing: 0.04em;
@ -409,7 +468,7 @@ onBeforeUnmount(() => {
display: flex;
align-items: flex-start;
gap: 12px;
margin: 22px 0 0;
margin: 16px 0 0;
padding: 12px 14px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.14);
@ -443,29 +502,20 @@ onBeforeUnmount(() => {
color: rgba(255, 255, 255, 0.72);
}
.login-hero__flow {
.login-hero--admin .login-hero__visual {
position: absolute;
left: calc((var(--login-split-top) + var(--login-split-bottom)) / 4 - 150px);
top: 50%;
bottom: auto;
z-index: 1;
width: min(380px, 32vw);
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
margin: 20px 0 0;
padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.12);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.12em;
color: rgba(255, 255, 255, 0.45);
i {
flex: 1;
height: 1px;
min-width: 16px;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.08),
rgba(255, 255, 255, 0.35),
rgba(255, 255, 255, 0.08)
);
}
transform: translate(-50%, -50%) scale(1.3);
justify-content: center;
align-self: auto;
}
@keyframes login-hero-principle-in {
@ -542,6 +592,17 @@ onBeforeUnmount(() => {
user-select: none;
}
.login-hero--admin .login-hero__truck {
width: 100%;
max-width: none;
height: auto;
display: block;
margin: 0;
transform: none;
pointer-events: none;
user-select: none;
}
.login-hero--light {
color: rgba(26, 45, 74, 0.88);
}
@ -607,30 +668,26 @@ onBeforeUnmount(() => {
color: rgba(26, 45, 74, 0.68);
}
.login-hero--light .login-hero__flow {
border-top-color: rgba(26, 45, 74, 0.1);
color: rgba(26, 45, 74, 0.42);
i {
background: linear-gradient(
90deg,
rgba(26, 45, 74, 0.04),
rgba(26, 45, 74, 0.22),
rgba(26, 45, 74, 0.04)
);
}
}
@media screen and (max-width: 1250px) {
.login-hero__visual {
display: none;
}
.login-hero--admin .login-hero__visual {
display: flex;
}
.login-hero__content {
flex: 1 1 100%;
}
}
@media screen and (max-width: 900px) {
.login-hero--admin .login-hero__visual {
display: none;
}
}
@media screen and (max-width: 768px) {
.login-hero__title {
font-size: 2rem;
@ -653,10 +710,6 @@ onBeforeUnmount(() => {
.login-hero__principle-title {
font-size: 18px;
}
.login-hero__flow {
letter-spacing: 0.06em;
}
}
@media (prefers-reduced-motion: reduce) {

View File

@ -24,19 +24,36 @@
:class="{ 'login-brand-corner--light': isLightMode }"
>
<span class="login-brand-corner__name">{{ t('brand.name') }}</span>
<span class="login-brand-corner__suffix">I {{ t('brand.fullName') }}</span>
<span class="login-brand-corner__suffix">· {{ t('brand.fullName') }}</span>
</div>
<aside :class="['login-hero-panel', { 'login-hero-panel--admin': !isCustomerLogin }]">
<aside v-if="isCustomerLogin" class="login-hero-panel">
<LoginHero
:light="isLightMode"
:portal="isCustomerLogin ? 'customer' : 'admin'"
portal="customer"
/>
</aside>
<div class="login-characters-panel">
<div v-if="isCustomerLogin" class="login-characters-panel">
<LoginCharacters v-if="showCharacters" @register="registerLoginCharacters" />
</div>
</div>
<aside
v-if="shellReady && !isCustomerLogin"
class="login-hero-panel login-hero-panel--admin"
>
<LoginHero
:light="isLightMode"
portal="admin"
/>
</aside>
<div
v-if="shellReady && !isCustomerLogin"
class="login-characters-panel login-characters-panel--admin"
>
<LoginCharacters v-if="showCharacters" @register="registerLoginCharacters" />
</div>
<div v-if="shellReady" ref="rightPaneRef" class="login-pane login-pane--right">
<div ref="loginFormRef" :class="['login-form', { 'login-form--ready': shellReady }]">
<div class="login-form-toolbar">
@ -72,10 +89,6 @@
</div>
</div>
</div>
<footer v-if="shellReady" class="login-footer">
{{ t('login.footer.copyright') }}
</footer>
</div>
</template>
@ -96,7 +109,6 @@ import { getLoginTypeFromRouteName } from '@/utils/login-portal';
import { IconSunFill, IconMoonFill } from '@arco-design/web-vue/es/icon';
import LoginForm from './components/pwd-login/index.vue';
import LoginHero from './components/LoginHero.vue';
import LoginCareBanner from './components/LoginCareBanner.vue';
import LoginLangToggle from './components/LoginLangToggle.vue';
import {
LOGIN_CHARACTERS_KEY,
@ -112,6 +124,9 @@ const WechatLogin = defineAsyncComponent(
const LoginCharacters = defineAsyncComponent(
() => import('./components/LoginCharacters.vue'),
);
const LoginCareBanner = defineAsyncComponent(
() => import('./components/LoginCareBanner.vue'),
);
const LOGIN_APPEARANCE_KEY = 'login-appearance';
const LOGIN_PAGE_SCROLL_LOCK_CLASS = 'login-page-no-scroll';
@ -271,14 +286,18 @@ function prefetchWechatQr() {
onMounted(() => {
document.documentElement.classList.add(LOGIN_PAGE_SCROLL_LOCK_CLASS);
shellReady.value = true;
// 首屏先出表单,下一帧再挂角色动画分包
requestAnimationFrame(() => {
const idle = window.requestIdleCallback
? (cb: () => void, timeout = 1200) => window.requestIdleCallback(cb, { timeout })
: (cb: () => void, timeout = 1200) => window.setTimeout(cb, Math.min(timeout, 400));
// 表单先出,空闲后再挂角色,避免和首屏 JS/图抢带宽
idle(() => {
showCharacters.value = true;
});
// 后台预拉完整语言包,进系统时几乎不等
}, 1600);
idle(() => {
void import('@/locale').then(({ ensureFullLocaleMessages }) => {
void ensureFullLocaleMessages();
});
}, 2500);
});
onBeforeUnmount(() => {
@ -318,24 +337,6 @@ onBeforeUnmount(() => {
pointer-events: none;
}
.login-footer {
position: fixed;
left: 0;
right: 0;
bottom: 16px;
z-index: 12;
text-align: center;
padding: 0 16px;
font-size: 12px;
line-height: 1.6;
color: rgba(148, 163, 184, 0.92);
pointer-events: none;
}
.login-container--light .login-footer {
color: rgba(100, 116, 139, 0.88);
}
.login-split {
position: absolute;
inset: 0;
@ -435,10 +436,22 @@ onBeforeUnmount(() => {
}
.login-hero-panel--admin {
display: flex;
align-items: center;
justify-content: center;
width: min(800px, calc((var(--login-split-top) + var(--login-split-bottom)) / 2 - 16px));
inset: 0;
top: 0;
left: 0;
bottom: 0;
right: 0;
transform: none;
z-index: 3;
display: block;
width: auto;
max-width: none;
pointer-events: none;
}
.login-hero-panel--admin .login-hero {
width: 100%;
height: 100%;
}
.login-characters-panel {
@ -451,6 +464,22 @@ onBeforeUnmount(() => {
padding: 0 0 16px 8px;
}
.login-characters-panel--admin {
left: auto;
right: 0;
bottom: 0;
z-index: 6;
padding: 0;
transform: scale(0.82);
transform-origin: bottom right;
opacity: 1;
}
.login-characters-panel--admin :deep(.login-characters__stage) {
width: 450px;
transform-origin: bottom right;
}
.login-form-toolbar {
position: absolute;
top: 14px;

View File

@ -46,9 +46,6 @@
'login.hero.admin.pillar.reach.desc': 'Pickup to delivery · Clear flow · Fewer detours',
'login.hero.admin.ai':
'Embrace AI: spot issues, protect SLAs, simplify flows — people decide, AI accelerates.',
'login.hero.admin.flow.pickup': 'Pickup',
'login.hero.admin.flow.transit': 'In transit',
'login.hero.admin.flow.delivery': 'Delivered',
'login.care.period.morning': 'Good morning — a fresh start',
'login.care.period.afternoon': 'Good afternoon — steady wins',
'login.care.period.evening': 'Good evening — thanks for today',

View File

@ -45,9 +45,6 @@
'login.hero.admin.pillar.reach.title': '更简单',
'login.hero.admin.pillar.reach.desc': '揽收到签收 · 链路清晰 · 少绕少错',
'login.hero.admin.ai': '积极拥抱 AI:帮你盯异常、催时效、简流程——人做决策,AI 做加速。',
'login.hero.admin.flow.pickup': '揽收',
'login.hero.admin.flow.transit': '在途',
'login.hero.admin.flow.delivery': '签收',
'login.care.period.morning': '早上好,新的一天开始了',
'login.care.period.afternoon': '下午好,稳稳推进就好',
'login.care.period.evening': '傍晚好,今天也辛苦了',