All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m56s
- 支付弹窗(PC/H5)支持选券与批量支付体验优化,运单管理复用我的订单支付弹窗;立即支付仅允许待支付订单 - 订单详情费用明细展示优惠券抵扣/会员折扣负数行与「含优惠」;价格配置与手工加费过滤抵扣费用项 - 申报明细新增产品标识编码/非标产品标识编码/标准产品标识编码(PC/H5 下单、改单、详情、开放接口文档、公式目录、Excel 模板) - 9610 页「批量申报」改为「创建9610订单」:选运单并补充出口资质备案、电商平台 - 店铺订单批量按钮按动作独立 loading,获取面单限并发并显示进度 - 另含:国家管理、登录门户切换、监控与工作台、公告弹窗、nginx/vite 配置等改动
646 lines
16 KiB
Vue
646 lines
16 KiB
Vue
<template>
|
|
<div
|
|
:class="[
|
|
'login-container',
|
|
{
|
|
'login-container--light': isLightMode,
|
|
'login-container--exit': isExiting,
|
|
},
|
|
]"
|
|
ref="containerRef"
|
|
>
|
|
<div ref="leftSplitRef" class="login-split login-split--left" aria-hidden="true" />
|
|
<div ref="rightSplitRef" class="login-split login-split--right" aria-hidden="true" />
|
|
|
|
<LoginCareBanner
|
|
v-if="shellReady && !isCustomerLogin"
|
|
:light="isLightMode"
|
|
/>
|
|
|
|
<div v-if="shellReady" ref="leftPaneRef" class="login-pane login-pane--left">
|
|
<div
|
|
v-if="!isCustomerLogin"
|
|
class="login-brand-corner"
|
|
:class="{ 'login-brand-corner--light': isLightMode }"
|
|
>
|
|
<span class="login-brand-corner__name">{{ t('brand.name') }}</span>
|
|
<span class="login-brand-corner__suffix">· {{ t('brand.fullName') }}</span>
|
|
</div>
|
|
<aside v-if="isCustomerLogin" class="login-hero-panel">
|
|
<LoginHero
|
|
:light="isLightMode"
|
|
portal="customer"
|
|
/>
|
|
</aside>
|
|
<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">
|
|
<LoginLangToggle />
|
|
<a-tooltip :content="isLightMode ? t('login.theme.toNight') : t('login.theme.toDay')">
|
|
<a-button
|
|
shape="circle"
|
|
type="text"
|
|
class="theme-toggle-btn"
|
|
:aria-label="isLightMode ? t('login.theme.toNight') : t('login.theme.toDay')"
|
|
@click="toggleAppearance"
|
|
>
|
|
<template #icon>
|
|
<icon-sun-fill v-if="!isLightMode" />
|
|
<icon-moon-fill v-else />
|
|
</template>
|
|
</a-button>
|
|
</a-tooltip>
|
|
</div>
|
|
<div class="login-content">
|
|
<LoginForm
|
|
v-if="activeKey === 'pwd'"
|
|
:login-type="loginType"
|
|
@switch-wechat="setLoginMode('wechat')"
|
|
@prefetch-wechat="prefetchWechatQr"
|
|
/>
|
|
<WechatLogin
|
|
v-if="isCustomerLogin && (activeKey === 'wechat' || wechatMounted)"
|
|
v-show="activeKey === 'wechat'"
|
|
ref="wechatLoginRef"
|
|
@switch-pwd="setLoginMode('pwd')"
|
|
/>
|
|
</div>
|
|
<LoginPortalSwitch :target="isCustomerLogin ? 'admin' : 'customer'" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import {
|
|
ref,
|
|
computed,
|
|
onMounted,
|
|
onBeforeUnmount,
|
|
provide,
|
|
shallowRef,
|
|
defineAsyncComponent,
|
|
} from 'vue';
|
|
import { useRoute } from 'vue-router';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { OmsLoginType } from '@/api/rbac';
|
|
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 LoginLangToggle from './components/LoginLangToggle.vue';
|
|
import LoginPortalSwitch from './components/LoginPortalSwitch.vue';
|
|
import {
|
|
LOGIN_CHARACTERS_KEY,
|
|
type LoginCharactersApi,
|
|
} from './composables/useLoginCharacters';
|
|
import { LOGIN_EXIT_KEY, type LoginExitApi } from './composables/useLoginExit';
|
|
|
|
/** Admin login never mounts WeChat; keep it out of the critical path */
|
|
const WechatLogin = defineAsyncComponent(
|
|
() => import('./components/wechat-login.vue'),
|
|
);
|
|
/** Characters + gsap are decorative; async so /admin first paint stays light */
|
|
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';
|
|
|
|
defineOptions({ name: 'Login' });
|
|
|
|
const { t } = useI18n();
|
|
const route = useRoute();
|
|
const loginType = computed(() => getLoginTypeFromRouteName(route.name as string));
|
|
const isCustomerLogin = computed(() => loginType.value === OmsLoginType.Customer);
|
|
/** 角色动画偏重,首屏延后一帧再挂,管理端也挂但异步分包 */
|
|
const showCharacters = ref(false);
|
|
const activeKey = ref<'pwd' | 'wechat'>('pwd');
|
|
const wechatMounted = ref(false);
|
|
const wechatLoginRef = ref<{ prefetch?: () => void } | null>(null);
|
|
const shellReady = ref(false);
|
|
const isExiting = ref(false);
|
|
const loginCharactersApi = shallowRef<LoginCharactersApi | null>(null);
|
|
const containerRef = ref<HTMLElement | null>(null);
|
|
const leftSplitRef = ref<HTMLElement | null>(null);
|
|
const rightSplitRef = ref<HTMLElement | null>(null);
|
|
const leftPaneRef = ref<HTMLElement | null>(null);
|
|
const rightPaneRef = ref<HTMLElement | null>(null);
|
|
const loginFormRef = ref<HTMLElement | null>(null);
|
|
|
|
function collectExitTargets(side: 'left' | 'right'): HTMLElement[] {
|
|
if (side === 'left') {
|
|
return [leftSplitRef.value, leftPaneRef.value].filter((el): el is HTMLElement => !!el);
|
|
}
|
|
return [rightSplitRef.value, rightPaneRef.value].filter((el): el is HTMLElement => !!el);
|
|
}
|
|
|
|
provide(LOGIN_EXIT_KEY, {
|
|
playExitAnimation(): Promise<void> {
|
|
if (isExiting.value) {
|
|
return Promise.resolve();
|
|
}
|
|
// Flips login-container--exit: hides admin overlays outside pane clip-paths
|
|
isExiting.value = true;
|
|
|
|
const leftTargets = collectExitTargets('left');
|
|
const rightTargets = collectExitTargets('right');
|
|
const formEl = loginFormRef.value;
|
|
|
|
return new Promise((resolve) => {
|
|
if (!leftTargets.length && !rightTargets.length) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
void import('gsap').then(({ default: gsap }) => {
|
|
gsap.set([...leftTargets, ...rightTargets], {
|
|
x: 0,
|
|
opacity: 1,
|
|
force3D: true,
|
|
});
|
|
|
|
const tl = gsap.timeline({
|
|
defaults: { ease: 'power2.inOut' },
|
|
onComplete: resolve,
|
|
});
|
|
|
|
if (formEl) {
|
|
tl.to(
|
|
formEl,
|
|
{
|
|
opacity: 0,
|
|
duration: 0.22,
|
|
ease: 'power1.out',
|
|
},
|
|
0,
|
|
);
|
|
}
|
|
|
|
tl.to(
|
|
leftTargets,
|
|
{
|
|
x: '-100%',
|
|
opacity: 0.96,
|
|
duration: 0.32,
|
|
ease: 'power3.inOut',
|
|
},
|
|
0.02,
|
|
);
|
|
|
|
tl.to(
|
|
rightTargets,
|
|
{
|
|
x: '100%',
|
|
opacity: 0.96,
|
|
duration: 0.32,
|
|
ease: 'power3.inOut',
|
|
},
|
|
0.04,
|
|
);
|
|
|
|
if (containerRef.value) {
|
|
const pageBg =
|
|
getComputedStyle(document.documentElement).getPropertyValue('--app-page-bg').trim() ||
|
|
'#f0f3f4';
|
|
tl.to(
|
|
containerRef.value,
|
|
{
|
|
backgroundColor: pageBg,
|
|
duration: 0.28,
|
|
ease: 'power1.inOut',
|
|
},
|
|
0.02,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
},
|
|
} satisfies LoginExitApi);
|
|
|
|
provide(LOGIN_CHARACTERS_KEY, loginCharactersApi);
|
|
|
|
function registerLoginCharacters(api: LoginCharactersApi) {
|
|
loginCharactersApi.value = api;
|
|
}
|
|
|
|
function readStoredAppearance(): boolean {
|
|
try {
|
|
return localStorage.getItem(LOGIN_APPEARANCE_KEY) === 'light';
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const isLightMode = ref(readStoredAppearance());
|
|
|
|
function toggleAppearance() {
|
|
isLightMode.value = !isLightMode.value;
|
|
try {
|
|
localStorage.setItem(LOGIN_APPEARANCE_KEY, isLightMode.value ? 'light' : 'dark');
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
function setLoginMode(mode: 'pwd' | 'wechat') {
|
|
if (mode === 'wechat' && !isCustomerLogin.value) return;
|
|
if (mode === 'wechat') {
|
|
wechatMounted.value = true;
|
|
}
|
|
activeKey.value = mode;
|
|
}
|
|
|
|
function prefetchWechatQr() {
|
|
if (!isCustomerLogin.value) return;
|
|
wechatMounted.value = true;
|
|
// 下一帧组件已挂载后再触发预取(若已在加载则内部会跳过)
|
|
requestAnimationFrame(() => {
|
|
wechatLoginRef.value?.prefetch?.();
|
|
});
|
|
}
|
|
|
|
onMounted(() => {
|
|
document.documentElement.classList.add(LOGIN_PAGE_SCROLL_LOCK_CLASS);
|
|
shellReady.value = true;
|
|
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(() => {
|
|
document.documentElement.classList.remove(LOGIN_PAGE_SCROLL_LOCK_CLASS);
|
|
});
|
|
</script>
|
|
|
|
<style lang="less" scoped>
|
|
.login-container {
|
|
--entry-btn-bg: rgba(255, 255, 255, 0.08);
|
|
--entry-btn-bg-hover: rgba(255, 255, 255, 0.14);
|
|
--entry-account-color: rgba(255, 255, 255, 0.88);
|
|
/* 斜线:顶 62% → 底 38%,右侧为上窄下宽的倒梯形 */
|
|
--login-split-top: 62%;
|
|
--login-split-bottom: 38%;
|
|
--login-left-bg: #142637;
|
|
--login-right-bg: var(--app-page-bg, #f0f3f4);
|
|
|
|
box-sizing: border-box;
|
|
height: 100dvh;
|
|
max-height: 100dvh;
|
|
min-height: 100dvh;
|
|
position: relative;
|
|
overflow: hidden;
|
|
overscroll-behavior: none;
|
|
}
|
|
|
|
.login-container--light {
|
|
--entry-btn-bg: rgba(0, 0, 0, 0.05);
|
|
--entry-btn-bg-hover: rgba(0, 0, 0, 0.09);
|
|
--entry-account-color: rgba(26, 45, 74, 0.85);
|
|
--login-left-bg: #e8eef5;
|
|
--login-right-bg: var(--app-page-bg, #f0f3f4);
|
|
}
|
|
|
|
.login-container--exit {
|
|
pointer-events: none;
|
|
|
|
/* Safety net: admin overlays are outside pane clip-paths */
|
|
.login-hero-panel--admin,
|
|
.login-characters-panel--admin,
|
|
:deep(.login-care-banner) {
|
|
opacity: 0 !important;
|
|
visibility: hidden !important;
|
|
pointer-events: none;
|
|
}
|
|
}
|
|
|
|
.login-split {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 0;
|
|
will-change: transform;
|
|
}
|
|
|
|
/* 左:正梯形(上宽下窄) */
|
|
.login-split--left {
|
|
background: var(--login-left-bg);
|
|
clip-path: polygon(0 0, var(--login-split-top) 0, var(--login-split-bottom) 100%, 0 100%);
|
|
}
|
|
|
|
/* 右:倒梯形(上窄下宽) */
|
|
.login-split--right {
|
|
background: var(--login-right-bg);
|
|
clip-path: polygon(
|
|
var(--login-split-top) 0,
|
|
100% 0,
|
|
100% 100%,
|
|
var(--login-split-bottom) 100%
|
|
);
|
|
}
|
|
|
|
.login-pane {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 2;
|
|
will-change: transform;
|
|
}
|
|
|
|
.login-pane--left {
|
|
clip-path: polygon(0 0, var(--login-split-top) 0, var(--login-split-bottom) 100%, 0 100%);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.login-brand-corner {
|
|
position: absolute;
|
|
top: 28px;
|
|
left: 32px;
|
|
z-index: 8;
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 0.35em;
|
|
line-height: 1;
|
|
color: #fff;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.login-brand-corner__name {
|
|
font-size: clamp(1.6rem, 2.4vw, 2.25rem);
|
|
font-weight: 800;
|
|
letter-spacing: 0.12em;
|
|
}
|
|
|
|
.login-brand-corner__suffix {
|
|
font-size: clamp(0.95rem, 1.55vw, 1.375rem);
|
|
font-weight: 600;
|
|
letter-spacing: 0.04em;
|
|
opacity: 0.88;
|
|
}
|
|
|
|
.login-brand-corner--light {
|
|
color: #1a2d4a;
|
|
}
|
|
|
|
.login-pane--right {
|
|
clip-path: polygon(
|
|
var(--login-split-top) 0,
|
|
100% 0,
|
|
100% 100%,
|
|
var(--login-split-bottom) 100%
|
|
);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.login-pane--right .login-form {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: calc((var(--login-split-top) + var(--login-split-bottom) + 100% + 100%) / 4);
|
|
transform: translate(-50%, calc(-50% + 16px));
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.login-pane--right .login-form--ready {
|
|
transform: translate(-50%, -50%);
|
|
}
|
|
|
|
.login-hero-panel {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: calc((var(--login-split-top) + var(--login-split-bottom)) / 4);
|
|
transform: translate(-50%, -50%);
|
|
width: min(920px, calc((var(--login-split-top) + var(--login-split-bottom)) / 2 - 24px));
|
|
max-width: calc(100% - 32px);
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.login-hero-panel--admin {
|
|
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 {
|
|
position: absolute;
|
|
left: 0;
|
|
bottom: 0;
|
|
right: auto;
|
|
z-index: 6;
|
|
pointer-events: auto;
|
|
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;
|
|
right: 14px;
|
|
z-index: 2;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.login-form-toolbar :deep(.theme-toggle-btn.arco-btn),
|
|
.login-form-toolbar :deep(.login-lang-toggle),
|
|
.login-form-toolbar :deep(.contact-service-trigger--login) {
|
|
width: 32px;
|
|
height: 32px;
|
|
flex-shrink: 0;
|
|
border: none !important;
|
|
box-shadow: none !important;
|
|
background: transparent !important;
|
|
color: rgba(26, 45, 74, 0.72) !important;
|
|
}
|
|
|
|
.login-form-toolbar :deep(.theme-toggle-btn.arco-btn:hover),
|
|
.login-form-toolbar :deep(.login-lang-toggle:hover),
|
|
.login-form-toolbar :deep(.contact-service-trigger--login:hover) {
|
|
border: none !important;
|
|
box-shadow: none !important;
|
|
background: var(--app-acrylic-hover-bg) !important;
|
|
color: var(--app-primary-color) !important;
|
|
}
|
|
|
|
.theme-toggle-btn {
|
|
width: 32px;
|
|
height: 32px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.login-form {
|
|
position: relative;
|
|
z-index: 10;
|
|
width: 100%;
|
|
max-width: 430px;
|
|
min-height: auto;
|
|
padding: 51px 32px 45px;
|
|
background: var(--app-acrylic-bg);
|
|
border: 1px solid var(--app-acrylic-border);
|
|
border-radius: 24px;
|
|
box-shadow: none;
|
|
backdrop-filter: var(--app-acrylic-blur);
|
|
-webkit-backdrop-filter: var(--app-acrylic-blur);
|
|
display: flex;
|
|
flex-direction: column;
|
|
opacity: 1;
|
|
transform: none;
|
|
transition: border-color 0.2s ease, background 0.2s ease;
|
|
}
|
|
|
|
.login-form:hover {
|
|
border-color: var(--app-acrylic-border-strong);
|
|
background: var(--app-acrylic-hover-bg);
|
|
box-shadow: none;
|
|
}
|
|
|
|
.login-form--ready {
|
|
opacity: 1;
|
|
transform: none;
|
|
}
|
|
|
|
.login-content {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.login-form :deep(.nova-login__header) {
|
|
padding: 0;
|
|
}
|
|
|
|
.login-form :deep(.account-entry-btn) {
|
|
background: var(--app-acrylic-bg);
|
|
border-color: var(--app-acrylic-border);
|
|
backdrop-filter: var(--app-acrylic-blur);
|
|
-webkit-backdrop-filter: var(--app-acrylic-blur);
|
|
}
|
|
|
|
.login-form :deep(.account-entry-btn:hover) {
|
|
background: var(--app-acrylic-hover-bg);
|
|
border-color: var(--app-acrylic-border-strong);
|
|
}
|
|
|
|
@media screen and (max-width: 1024px) {
|
|
.login-characters-panel {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 1250px) {
|
|
.login-container {
|
|
--login-split-top: 0%;
|
|
--login-split-bottom: 0%;
|
|
}
|
|
|
|
.login-split--left,
|
|
.login-pane--left {
|
|
display: none;
|
|
}
|
|
|
|
.login-pane--right .login-form {
|
|
position: relative;
|
|
top: auto;
|
|
left: auto;
|
|
transform: none;
|
|
margin: auto;
|
|
}
|
|
|
|
.login-pane--right .login-form--ready {
|
|
transform: none;
|
|
}
|
|
|
|
.login-split--right,
|
|
.login-pane--right {
|
|
clip-path: none;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 16px;
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 600px) {
|
|
.login-form {
|
|
max-width: 97% !important;
|
|
padding: 43px 20px 39px;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<style lang="less">
|
|
html.login-page-no-scroll,
|
|
html.login-page-no-scroll body {
|
|
overflow: hidden !important;
|
|
height: 100% !important;
|
|
margin: 0;
|
|
}
|
|
|
|
html.login-page-no-scroll #app {
|
|
height: 100%;
|
|
overflow: hidden;
|
|
}
|
|
</style>
|