refactor(projects): 登录页面重新设计

This commit is contained in:
2026-06-12 22:42:23 +08:00
parent 6896a86130
commit 5061eced32
9 changed files with 891 additions and 98 deletions

4
.env
View File

@@ -2,9 +2,9 @@
# 如果部署在子目录下,结尾必须带 "/",例如 "/admin/",不能写成 "/admin"
VITE_BASE_URL=/
VITE_APP_TITLE=研发内部管理系统
VITE_APP_TITLE=研发管理系统
VITE_APP_DESC=Frontend application for 灿能研发内部管理系统
VITE_APP_DESC=Frontend application for 灿能研发管理系统
# 图标名称前缀
VITE_ICON_PREFIX=icon

View File

@@ -341,18 +341,53 @@ onBeforeUnmount(() => {
.notification-bell__badge {
position: absolute;
top: 4px;
right: 4px;
min-width: 16px;
height: 16px;
padding: 0 4px;
top: 2px;
right: 2px;
min-width: 18px;
height: 18px;
padding: 0 5px;
border: 1px solid #fff;
border-radius: 999px;
background-color: var(--el-color-danger);
color: #fff;
font-size: 10px;
font-weight: 600;
font-size: 11px;
font-weight: 700;
line-height: 16px;
text-align: center;
animation: notification-badge-pulse 1.6s ease-in-out infinite;
}
/* 扩散波纹:跟随心跳节奏向外晕开,增强未读提醒的醒目度 */
.notification-bell__badge::before {
content: '';
position: absolute;
inset: -1px;
border-radius: 999px;
background-color: var(--el-color-danger);
animation: notification-badge-ping 1.6s ease-out infinite;
z-index: -1;
}
@keyframes notification-badge-pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.18);
}
}
@keyframes notification-badge-ping {
0% {
transform: scale(1);
opacity: 0.6;
}
70%,
100% {
transform: scale(1.9);
opacity: 0;
}
}
.notification-bell__panel {

View File

@@ -41,11 +41,6 @@ const { isFullscreen, toggle } = useFullscreen();
<div>
<FullScreen v-if="!appStore.isMobile" :full="isFullscreen" @click="toggle" />
</div>
<ThemeSchemaSwitch
:theme-schema="themeStore.themeScheme"
:is-dark="themeStore.darkMode"
@switch="themeStore.toggleThemeScheme"
/>
<div>
<ThemeButton />
</div>

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue';
import { themeSchemaRecord } from '@/constants/app';
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
import SettingItem from '../components/setting-item.vue';
@@ -9,16 +8,6 @@ defineOptions({ name: 'DarkMode' });
const themeStore = useThemeStore();
const icons: Record<UnionKey.ThemeScheme, string> = {
light: 'material-symbols:sunny',
dark: 'material-symbols:nightlight-rounded',
auto: 'material-symbols:hdr-auto'
};
function handleSegmentChange(value: string | number) {
themeStore.setThemeScheme(value as UnionKey.ThemeScheme);
}
function handleGrayscaleChange(value: boolean) {
themeStore.setGrayscale(value);
}
@@ -33,15 +22,6 @@ const showSiderInverted = computed(() => !themeStore.darkMode && themeStore.layo
<template>
<ElDivider>{{ $t('theme.themeSchema.title') }}</ElDivider>
<div class="flex-col-stretch gap-16px">
<div class="i-flex-center">
<ElTabs v-model="themeStore.themeScheme" type="border-card" class="segment" @tab-change="handleSegmentChange">
<ElTabPane v-for="(_, key) in themeSchemaRecord" :key="key" :name="key">
<template #label>
<SvgIcon :icon="icons[key]" class="h-23px text-icon-small" />
</template>
</ElTabPane>
</ElTabs>
</div>
<Transition name="sider-inverted">
<SettingItem v-if="showSiderInverted" :label="$t('theme.sider.inverted')">
<ElSwitch v-model="themeStore.sider.inverted" />

View File

@@ -1,6 +1,6 @@
const local: App.I18n.Schema = {
system: {
title: '研发内部管理系统'
title: '研发管理系统'
},
common: {
action: '操作',
@@ -255,7 +255,7 @@ const local: App.I18n.Schema = {
about: {
title: '关于',
introduction:
'灿能研发内部管理系统是灿能电力内部使用的研发管理前端系统,用于承载内部业务模块、工程协作流程和日常管理能力。',
'灿能研发管理系统是灿能电力内部使用的研发管理前端系统,用于承载内部业务模块、工程协作流程和日常管理能力。',
projectInfo: {
title: '项目信息',
version: '版本',

View File

@@ -89,4 +89,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
*
* If publish new version, use `overrideThemeSettings` to override certain theme settings
*/
export const overrideThemeSettings: Partial<App.Theme.ThemeSetting> = {};
// 系统固定亮色主题:切换入口已全部移除,发新版时把老用户缓存的暗色设置刷回亮色
export const overrideThemeSettings: Partial<App.Theme.ThemeSetting> = {
themeScheme: 'light'
};

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { Component } from 'vue';
import { getPaletteColorByNumber, mixColor } from '@sa/color';
import { computed, reactive } from 'vue';
import type { CSSProperties, Component } from 'vue';
import { loginModuleRecord } from '@/constants/app';
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
@@ -31,46 +30,791 @@ const moduleMap: Record<UnionKey.LoginModule, LoginModule> = {
const activeModule = computed(() => moduleMap[props.module || 'pwd-login']);
const bgThemeColor = computed(() =>
themeStore.darkMode ? getPaletteColorByNumber(themeStore.themeColor, 600) : themeStore.themeColor
);
const currentYear = new Date().getFullYear();
const bgColor = computed(() => {
const COLOR_WHITE = '#ffffff';
/** 登录页品牌色:取自公司 logo 的湛蓝,不跟随系统主题色(主题色偏紫,与企业蓝不符) */
const LOGIN_BRAND = '#1e80df';
const ratio = themeStore.darkMode ? 0.5 : 0.2;
/** 鼠标视差:归一化指针位置,不同景深的层按系数反向位移 */
const pointer = reactive({ x: 0, y: 0 });
return mixColor(COLOR_WHITE, themeStore.themeColor, ratio);
});
function onPointerMove(event: MouseEvent) {
pointer.x = (event.clientX / window.innerWidth - 0.5) * 2;
pointer.y = (event.clientY / window.innerHeight - 0.5) * 2;
}
function layerStyle(depth: number) {
return {
transform: `translate3d(${(-pointer.x * depth).toFixed(1)}px, ${(-pointer.y * depth).toFixed(1)}px, 0)`
};
}
/** 协作分支:角色 → 颜色 → 汇入主干的路径git 分支汇流的意象),曲线两端均水平相切,过渡柔和 */
const branches = [
{
key: 'demand',
label: '需求',
color: '#f59e0b',
y0: 195,
mergeX: 660,
path: 'M -80,195 C 320,195 440,470 660,470',
dur: '7.5s',
begin: '0s'
},
{
key: 'design',
label: '设计',
color: '#ec4899',
y0: 330,
mergeX: 780,
path: 'M -80,330 C 360,330 540,470 780,470',
dur: '6.5s',
begin: '1.2s'
},
{
key: 'dev',
label: '开发',
color: '#0ea5e9',
y0: 615,
mergeX: 880,
path: 'M -80,615 C 380,615 560,470 880,470',
dur: '7s',
begin: '2.1s'
},
{
key: 'test',
label: '测试',
color: '#22c55e',
y0: 745,
mergeX: 970,
path: 'M -80,745 C 420,745 620,470 970,470',
dur: '8s',
begin: '0.6s'
}
];
/** 分支汇入主干的节点位置 */
const mergePoints = [
{ x: 660, color: '#f59e0b' },
{ x: 780, color: '#ec4899' },
{ x: 880, color: '#0ea5e9' },
{ x: 970, color: '#22c55e' }
];
/** 角色徽章在场景中的落位(跟随分支起始段) */
const roleChips: { label: string; color: string; style: CSSProperties }[] = [
{ label: '需求', color: '#f59e0b', style: { left: '5%', top: '20%', '--float-d': '0s' } },
{ label: '设计', color: '#ec4899', style: { left: '11%', top: '35%', '--float-d': '0.8s' } },
{ label: '开发', color: '#0ea5e9', style: { left: '8%', top: '66%', '--float-d': '1.6s' } },
{ label: '测试', color: '#22c55e', style: { left: '13%', top: '80%', '--float-d': '2.4s' } }
];
/**
* 电能质量波形(公司主营:电能质量监测)
*
* 主干汇流完成后,尾段"输出"为基波 + 谐波叠加的正弦波组,寓意协作成果守护电能质量。
* 用二次贝塞尔 Q/T 拼接出周期波形CSS 平移一个整周期实现无缝流动。
*/
interface WaveShape {
/** 波形中线 y */
mid: number;
/** 振幅 */
amp: number;
/** 半周期x 方向) */
half: number;
}
function buildWavePath(shape: WaveShape, from: number, to: number) {
const { mid, amp, half } = shape;
let d = `M ${from} ${mid} Q ${from + half / 2} ${mid - amp} ${from + half} ${mid}`;
for (let x = from + 2 * half; x <= to; x += half) {
d += ` T ${x} ${mid}`;
}
return d;
}
const waves = [
// 基波:主题色,振幅最大
{ key: 'fundamental', mid: 470, amp: 26, half: 110, color: 'var(--brand)', width: 2, opacity: 0.5, dur: '7s' },
// 高次谐波:短周期小振幅
{ key: 'harmonic', mid: 470, amp: 10, half: 55, color: '#0ea5e9', width: 1.5, opacity: 0.45, dur: '4.5s' },
// 低频包络:慢速衬底
{ key: 'flux', mid: 474, amp: 40, half: 220, color: '#60a5fa', width: 2, opacity: 0.22, dur: '14s' }
].map(wave => ({
...wave,
path: buildWavePath(wave, 900 - wave.half * 2, 2000 + wave.half * 2),
shift: `${-2 * wave.half}px`
}));
/** 电力场景剪影:输电铁塔(底部接地,局部坐标基点为塔脚中心) */
const towers = [
{ x: 150, s: 1 },
{ x: 540, s: 0.85 },
{ x: 1280, s: 0.7 }
];
/** 塔间悬垂导线(悬链线意象),坐标对应各塔最宽横担端点 */
const powerLines = [
{ path: 'M -60,762 Q 30,800 92,750' },
{ path: 'M 208,750 Q 350,819 491,768' },
{ path: 'M 589,768 Q 914,867 1239,786' },
{ path: 'M 1321,786 Q 1430,824 1520,804' }
];
/** 导线上滑过的电流光点 */
const lineSparks = [
{ key: 'spark-1', path: 'M 208,750 Q 350,819 491,768', dur: '5s', begin: '0s' },
{ key: 'spark-2', path: 'M 589,768 Q 914,867 1239,786', dur: '7s', begin: '2s' }
];
/** 风机新能源应用场景dur 为叶轮旋转周期 */
const turbines = [
{ key: 'turbine-1', x: 715, s: 0.9, dur: '9s' },
{ key: 'turbine-2', x: 828, s: 0.6, dur: '13s' }
];
</script>
<template>
<div class="relative size-full flex-center overflow-hidden" :style="{ backgroundColor: bgColor }">
<WaveBg :theme-color="bgThemeColor" />
<ElCard class="relative z-4 w-auto rd-12px">
<div class="w-400px lt-sm:w-300px">
<header class="flex-y-center justify-between">
<SystemLogo class="text-64px text-primary lt-sm:text-48px" />
<h3 class="text-28px text-primary font-500 lt-sm:text-22px">{{ $t('system.title') }}</h3>
<div class="i-flex-col">
<ThemeSchemaSwitch
:theme-schema="themeStore.themeScheme"
:show-tooltip="false"
class="text-20px lt-sm:text-18px"
@switch="themeStore.toggleThemeScheme"
<div class="login-scene" :style="{ '--brand': LOGIN_BRAND }" @mousemove="onPointerMove">
<!-- 远景浮尘微粒 -->
<div class="scene-motes" :style="layerStyle(6)"></div>
<!-- 中景协作汇流图需求/设计/开发/测试 主干 登录入口 -->
<div class="scene-graph" :style="layerStyle(14)">
<svg class="scene-graph__svg" viewBox="0 0 1440 900" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
<defs>
<linearGradient id="trunk-grad" x1="0" y1="0" x2="1" y2="0">
<!-- presentation attribute 不解析 CSS var必须用 style -->
<stop offset="0" style="stop-color: var(--brand); stop-opacity: 0" />
<stop offset="0.45" style="stop-color: var(--brand); stop-opacity: 0.85" />
<stop offset="1" style="stop-color: #0ea5e9; stop-opacity: 0.9" />
</linearGradient>
<!-- 每条分支一个渐变起点透明临近汇入处渐显模拟光流自然汇聚 -->
<linearGradient
v-for="branch in branches"
:id="`branch-grad-${branch.key}`"
:key="`grad-${branch.key}`"
gradientUnits="userSpaceOnUse"
:x1="-80"
:y1="branch.y0"
:x2="branch.mergeX"
:y2="470"
>
<stop offset="0" :stop-color="branch.color" stop-opacity="0" />
<stop offset="0.45" :stop-color="branch.color" stop-opacity="0.2" />
<stop offset="1" :stop-color="branch.color" stop-opacity="0.65" />
</linearGradient>
<filter id="trunk-glow" x="-30%" y="-300%" width="160%" height="700%">
<feGaussianBlur stdDeviation="5" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<!-- 波形渐显遮罩汇流完成后波形才"长出来"遮罩静止波形在其下平移 -->
<linearGradient id="wave-fade" gradientUnits="userSpaceOnUse" x1="920" y1="0" x2="1980" y2="0">
<stop offset="0" stop-color="#fff" stop-opacity="0" />
<stop offset="0.3" stop-color="#fff" stop-opacity="0.9" />
<stop offset="1" stop-color="#fff" stop-opacity="1" />
</linearGradient>
<mask id="wave-mask">
<rect x="920" y="330" width="1100" height="300" fill="url(#wave-fade)" />
</mask>
</defs>
<!-- 主干 -->
<path class="trunk" d="M -60,470 L 1520,470" stroke="url(#trunk-grad)" filter="url(#trunk-glow)" />
<!-- 四条角色分支 -->
<path
v-for="(branch, index) in branches"
:key="branch.key"
class="branch"
:d="branch.path"
:stroke="`url(#branch-grad-${branch.key})`"
:style="{ '--breathe-d': `${index * 1.4}s` }"
/>
<!-- 分支上行进的光点 -->
<circle
v-for="branch in branches"
:key="`dot-${branch.key}`"
r="3.5"
:fill="branch.color"
class="travel-dot"
:style="{ color: branch.color }"
>
<animateMotion :dur="branch.dur" :begin="branch.begin" repeatCount="indefinite" :path="branch.path" />
</circle>
<!-- 主干上行进的光点 -->
<circle r="3" fill="#5db1f5" class="travel-dot" style="color: #5db1f5">
<animateMotion dur="4.5s" begin="0.5s" repeatCount="indefinite" path="M 660,470 L 1520,470" />
</circle>
<circle r="2.5" fill="#5db1f5" class="travel-dot" style="color: #5db1f5">
<animateMotion dur="5.5s" begin="2.6s" repeatCount="indefinite" path="M 660,470 L 1520,470" />
</circle>
<!-- 电能质量波形基波 + 谐波 + 低频包络沿主干尾段流出 -->
<g mask="url(#wave-mask)">
<path
v-for="wave in waves"
:key="wave.key"
class="wave"
:d="wave.path"
:stroke-width="wave.width"
:style="{
stroke: wave.color,
opacity: wave.opacity,
'--wave-shift': wave.shift,
'--wave-dur': wave.dur
}"
/>
</g>
<!-- 电力场景剪影输电铁塔 + 悬垂导线 + 风机 -->
<g class="industry">
<!-- 输电铁塔 -->
<g
v-for="tower in towers"
:key="`tower-${tower.x}`"
:transform="`translate(${tower.x}, 870) scale(${tower.s})`"
>
<path d="M -32 0 L -10 -150 L 0 -178 L 10 -150 L 32 0" />
<path
d="M -28 -25 L 28 -45 M 28 -25 L -28 -45 M -24 -70 L 24 -88 M 24 -70 L -24 -88 M -19 -112 L 19 -126 M 19 -112 L -19 -126"
/>
</div>
</header>
<main class="pt-15px">
<div class="pt-15px">
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
<component :is="activeModule.component" />
</Transition>
</div>
</main>
</div>
</ElCard>
<path d="M -58 -120 L 58 -120 M -46 -150 L 46 -150" />
<path d="M -58 -120 L -58 -110 M 58 -120 L 58 -110 M -46 -150 L -46 -140 M 46 -150 L 46 -140" />
</g>
<!-- 塔间导线 -->
<path v-for="line in powerLines" :key="line.path" :d="line.path" />
<!-- 风机 -->
<g
v-for="turbine in turbines"
:key="turbine.key"
:transform="`translate(${turbine.x}, 870) scale(${turbine.s})`"
>
<path d="M -3 0 L 0 -120 M 3 0 L 0 -120" />
<g transform="translate(0, -120)">
<g>
<path d="M 0 0 L 0 -52" />
<path d="M 0 0 L 0 -52" transform="rotate(120)" />
<path d="M 0 0 L 0 -52" transform="rotate(240)" />
<animateTransform
attributeName="transform"
type="rotate"
from="0 0 0"
to="360 0 0"
:dur="turbine.dur"
repeatCount="indefinite"
/>
</g>
</g>
<circle cx="0" cy="-120" r="3" class="industry__hub" />
</g>
</g>
<!-- 导线上的电流光点 -->
<circle v-for="spark in lineSparks" :key="spark.key" r="2.5" class="travel-dot industry__spark">
<animateMotion :dur="spark.dur" :begin="spark.begin" repeatCount="indefinite" :path="spark.path" />
</circle>
<!-- 汇入节点脉冲 -->
<g v-for="point in mergePoints" :key="`merge-${point.x}`">
<circle :cx="point.x" cy="470" r="5" :fill="point.color" />
<circle :cx="point.x" cy="470" r="5" :stroke="point.color" class="merge-pulse" />
</g>
</svg>
<!-- 角色徽章 -->
<span v-for="chip in roleChips" :key="chip.label" class="role-chip" :style="chip.style">
<i class="role-chip__dot" :style="{ backgroundColor: chip.color }"></i>
{{ chip.label }}
</span>
</div>
<!-- 顶部品牌 -->
<header class="scene-header reveal" style="--d: 0s">
<SystemLogo class="text-36px" />
<span class="scene-header__name">{{ $t('system.title') }}</span>
</header>
<!-- 主文案 -->
<div class="scene-hero" :style="layerStyle(10)">
<p class="scene-hero__eyebrow reveal" style="--d: 0.15s">BUILD TOGETHER · GUARD POWER QUALITY</p>
<h1 class="scene-hero__slogan reveal" style="--d: 0.25s">
独行快
<span class="scene-hero__comma"></span>
<br />
众行
<em></em>
</h1>
<p class="scene-hero__sub reveal" style="--d: 0.4s">每一次提交都让电能质量的守护更进一步</p>
</div>
<!-- 登录卡片 -->
<div class="login-card">
<header class="login-card__header reveal" style="--d: 0.3s">
<SystemLogo class="login-card__logo text-52px" />
<h2 class="login-card__title">{{ $t('system.title') }}</h2>
<p class="login-card__subtitle">欢迎回来开始今天的协作</p>
</header>
<main class="reveal" style="--d: 0.45s">
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
<component :is="activeModule.component" />
</Transition>
</main>
</div>
<footer class="scene-footer reveal" style="--d: 0.6s">
© {{ currentYear }} 南京灿能电力自动化股份有限公司 · {{ $t('system.title') }}
</footer>
</div>
</template>
<style scoped></style>
<style scoped lang="scss">
.login-scene {
position: relative;
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
height: 100%;
padding-right: 9vw;
overflow: hidden;
background:
radial-gradient(90% 70% at 80% 42%, color-mix(in srgb, var(--brand) 10%, transparent) 0%, transparent 60%),
radial-gradient(80% 60% at 6% 92%, rgb(56 189 248 / 10%) 0%, transparent 60%),
radial-gradient(70% 50% at 18% 8%, rgb(14 165 233 / 6%) 0%, transparent 55%),
linear-gradient(160deg, #f5f9ff 0%, #ecf2fb 50%, #fafcff 100%);
@media (max-width: 1023px) {
justify-content: center;
padding-right: 0;
}
}
/* ---------- 远景浮尘微粒 ---------- */
.scene-motes {
position: absolute;
inset: -40px;
background-image:
radial-gradient(2px 2px at 12% 22%, rgb(30 128 223 / 30%) 50%, transparent 51%),
radial-gradient(1.5px 1.5px at 28% 68%, rgb(23 44 84 / 16%) 50%, transparent 51%),
radial-gradient(2.5px 2.5px at 44% 12%, rgb(30 128 223 / 22%) 50%, transparent 51%),
radial-gradient(1.5px 1.5px at 58% 44%, rgb(23 44 84 / 12%) 50%, transparent 51%),
radial-gradient(2px 2px at 72% 78%, rgb(56 189 248 / 25%) 50%, transparent 51%),
radial-gradient(1.5px 1.5px at 86% 28%, rgb(23 44 84 / 14%) 50%, transparent 51%),
radial-gradient(2px 2px at 94% 62%, rgb(30 128 223 / 20%) 50%, transparent 51%),
radial-gradient(1.5px 1.5px at 6% 86%, rgb(56 189 248 / 18%) 50%, transparent 51%);
background-size: 520px 520px;
background-repeat: repeat;
animation: motes-breathe 6s ease-in-out infinite alternate;
transition: transform 0.25s ease-out;
pointer-events: none;
}
@keyframes motes-breathe {
from {
opacity: 0.5;
}
to {
opacity: 1;
}
}
/* ---------- 协作汇流图 ---------- */
.scene-graph {
position: absolute;
inset: 0;
transition: transform 0.25s ease-out;
pointer-events: none;
}
.scene-graph__svg {
width: 100%;
height: 100%;
}
.trunk {
fill: none;
stroke-width: 2.5;
stroke-linecap: round;
}
.branch {
fill: none;
stroke-width: 2;
stroke-linecap: round;
animation: branch-breathe 6s ease-in-out infinite alternate;
animation-delay: var(--breathe-d, 0s);
}
@keyframes branch-breathe {
from {
opacity: 0.55;
}
to {
opacity: 1;
}
}
.travel-dot {
filter: drop-shadow(0 0 6px currentColor);
}
.wave {
fill: none;
stroke-linecap: round;
animation: wave-drift var(--wave-dur) linear infinite;
}
@keyframes wave-drift {
to {
transform: translateX(var(--wave-shift));
}
}
/* 电力场景剪影 */
.industry {
fill: none;
stroke: #424a8c;
stroke-width: 1.5;
stroke-linecap: round;
opacity: 0.22;
}
.industry__hub {
fill: #424a8c;
stroke: none;
}
.industry__spark {
fill: var(--brand);
color: var(--brand);
opacity: 0.65;
}
.merge-pulse {
fill: none;
stroke-width: 1.5;
transform-box: fill-box;
transform-origin: center;
animation: merge-pulse 2.6s ease-out infinite;
}
@keyframes merge-pulse {
0% {
opacity: 0.8;
transform: scale(1);
}
70%,
100% {
opacity: 0;
transform: scale(3.2);
}
}
/* 角色徽章 */
.role-chip {
position: absolute;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 7px 16px;
border: 1px solid rgb(30 35 80 / 10%);
border-radius: 999px;
font-size: 13px;
letter-spacing: 0.14em;
color: rgb(30 35 80 / 72%);
background: rgb(255 255 255 / 65%);
box-shadow: 0 6px 18px -8px rgb(23 92 171 / 22%);
backdrop-filter: blur(8px);
animation: chip-float 5.5s ease-in-out infinite alternate;
animation-delay: var(--float-d, 0s);
}
.role-chip__dot {
width: 7px;
height: 7px;
border-radius: 50%;
box-shadow: 0 0 8px 1px currentcolor;
}
@keyframes chip-float {
from {
transform: translateY(-6px);
}
to {
transform: translateY(8px);
}
}
/* ---------- 品牌与文案 ---------- */
.scene-header {
position: absolute;
top: 40px;
left: 56px;
z-index: 2;
display: flex;
align-items: center;
gap: 12px;
color: #232850;
}
.scene-header__name {
font-size: 17px;
font-weight: 600;
letter-spacing: 0.08em;
}
.scene-hero {
position: absolute;
top: 24%;
left: 6.5%;
z-index: 2;
color: #1b2050;
transition: transform 0.25s ease-out;
pointer-events: none;
@media (max-width: 1023px) {
display: none;
}
}
.scene-hero__eyebrow {
margin-bottom: 26px;
font-family: Georgia, 'Times New Roman', serif;
font-size: 13px;
letter-spacing: 0.46em;
color: rgb(30 35 80 / 45%);
}
.scene-hero__slogan {
font-size: 64px;
font-weight: 600;
line-height: 1.3;
letter-spacing: 0.14em;
text-shadow: 0 8px 32px rgb(255 255 255 / 70%);
em {
font-style: normal;
background: linear-gradient(120deg, var(--brand) 0%, #0b66c3 55%, #38bdf8 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
}
.scene-hero__comma {
color: rgb(30 35 80 / 28%);
}
.scene-hero__sub {
margin-top: 26px;
font-size: 16px;
letter-spacing: 0.22em;
color: rgb(30 35 80 / 52%);
}
.scene-footer {
position: absolute;
bottom: 26px;
left: 56px;
z-index: 2;
font-size: 12px;
letter-spacing: 0.1em;
color: rgb(30 35 80 / 32%);
}
/* ---------- 登录卡片(白玻璃质感) ---------- */
.login-card {
position: relative;
z-index: 3;
width: 420px;
padding: 44px 40px 40px;
border: 1px solid rgb(255 255 255 / 75%);
border-radius: 20px;
background: linear-gradient(168deg, rgb(255 255 255 / 82%) 0%, rgb(248 250 255 / 88%) 100%);
backdrop-filter: blur(20px) saturate(140%);
box-shadow:
0 30px 70px -24px rgb(23 92 171 / 26%),
0 0 0 1px rgb(30 35 80 / 5%),
0 1px 0 rgb(255 255 255 / 90%) inset;
}
.login-card__header {
margin-bottom: 32px;
text-align: center;
}
.login-card__logo {
display: block;
margin: 0 auto 16px;
filter: drop-shadow(0 10px 26px color-mix(in srgb, var(--brand) 35%, transparent));
}
.login-card__title {
font-size: 24px;
font-weight: 600;
letter-spacing: 0.1em;
color: #20254d;
}
.login-card__subtitle {
margin-top: 10px;
font-size: 13.5px;
letter-spacing: 0.06em;
color: rgb(30 35 80 / 48%);
}
/* 卡片内表单:浅色质感统一覆盖(作用于子模块) */
.login-card :deep(.el-input__wrapper) {
height: 48px;
padding: 0 14px;
border-radius: 10px;
background-color: rgb(30 128 223 / 5%);
box-shadow: 0 0 0 1px rgb(30 35 80 / 12%) inset;
transition:
box-shadow 0.2s ease,
background-color 0.2s ease;
&:hover {
box-shadow: 0 0 0 1px color-mix(in srgb, var(--brand) 55%, rgb(30 35 80 / 20%)) inset;
}
&.is-focus {
background-color: #fff;
box-shadow:
0 0 0 1.5px var(--brand) inset,
0 0 0 4px color-mix(in srgb, var(--brand) 14%, transparent);
}
.el-input__inner {
color: #1f244a;
caret-color: var(--brand);
&::placeholder {
color: rgb(30 35 80 / 34%);
}
}
.el-input__prefix,
.el-input__suffix {
font-size: 18px;
color: rgb(30 35 80 / 35%);
}
.el-input__prefix {
margin-right: 6px;
}
}
.login-card :deep(.el-form-item) {
margin-bottom: 22px;
}
.login-card :deep(.el-checkbox__label) {
letter-spacing: 0.04em;
color: rgb(30 35 80 / 58%);
}
/* 选中态跟随登录页品牌蓝,而非系统主题色 */
.login-card :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
border-color: var(--brand);
background-color: var(--brand);
}
.login-card :deep(.el-checkbox__input.is-checked + .el-checkbox__label) {
color: var(--brand);
}
.login-card :deep(.login-submit-button) {
position: relative;
width: 100%;
height: 48px;
margin-top: 4px;
border: none;
border-radius: 10px;
overflow: hidden;
font-size: 16px;
font-weight: 600;
letter-spacing: 0.32em;
text-indent: 0.32em;
background: linear-gradient(135deg, var(--brand) 0%, color-mix(in srgb, var(--brand) 68%, #0a3f8f) 100%);
box-shadow: 0 12px 26px -10px color-mix(in srgb, var(--brand) 60%, transparent);
transition:
transform 0.2s ease,
box-shadow 0.2s ease,
filter 0.2s ease;
/* 流光扫过 */
&::after {
content: '';
position: absolute;
top: 0;
left: -60%;
width: 40%;
height: 100%;
background: linear-gradient(100deg, transparent 0%, rgb(255 255 255 / 35%) 50%, transparent 100%);
transform: skewX(-20deg);
transition: left 0.55s ease;
}
&:hover {
transform: translateY(-1px);
filter: brightness(1.06);
box-shadow: 0 16px 32px -10px color-mix(in srgb, var(--brand) 70%, transparent);
&::after {
left: 130%;
}
}
&:active {
transform: translateY(0);
}
}
.login-card :deep(.login-back-button) {
width: 100%;
height: 44px;
margin-top: 14px;
margin-left: 0;
border: 1px solid rgb(30 35 80 / 15%);
border-radius: 10px;
color: rgb(30 35 80 / 70%);
background: transparent;
&:hover {
border-color: color-mix(in srgb, var(--brand) 60%, transparent);
color: var(--brand);
background: color-mix(in srgb, var(--brand) 6%, transparent);
}
}
/* ---------- 入场动效 ---------- */
.reveal {
animation: reveal-up 0.7s cubic-bezier(0.22, 0.61, 0.36, 1) both;
animation-delay: var(--d, 0s);
}
@keyframes reveal-up {
from {
opacity: 0;
transform: translateY(18px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -36,24 +36,38 @@ async function handleSubmit() {
</script>
<template>
<ElForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
<ElForm ref="formRef" :model="model" :rules="rules" size="large" @keyup.enter="handleSubmit">
<ElFormItem prop="userName">
<ElInput v-model="model.userName" :placeholder="$t('page.login.common.userNamePlaceholder')" />
<ElInput v-model="model.userName" :placeholder="$t('page.login.common.userNamePlaceholder')">
<template #prefix>
<SvgIcon icon="mdi:account-outline" />
</template>
</ElInput>
</ElFormItem>
<ElFormItem prop="password">
<ElInput
v-model="model.password"
type="password"
show-password-on="click"
show-password
:placeholder="$t('page.login.common.passwordPlaceholder')"
/>
>
<template #prefix>
<SvgIcon icon="mdi:lock-outline" />
</template>
</ElInput>
</ElFormItem>
<ElSpace direction="vertical" :size="24" class="w-full" fill>
<div class="pb-18px">
<ElCheckbox>{{ $t('page.login.pwdLogin.rememberMe') }}</ElCheckbox>
<ElButton type="primary" size="large" round block :loading="authStore.loginLoading" @click="handleSubmit">
{{ $t('common.confirm') }}
</ElButton>
</ElSpace>
</div>
<ElButton
type="primary"
size="large"
class="login-submit-button"
:loading="authStore.loginLoading"
@click="handleSubmit"
>
{{ $t('route.login') }}
</ElButton>
</ElForm>
</template>

View File

@@ -43,38 +43,60 @@ async function handleSubmit() {
</script>
<template>
<ElForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
<ElForm ref="formRef" :model="model" :rules="rules" size="large" @keyup.enter="handleSubmit">
<ElFormItem prop="phone">
<ElInput v-model="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
<ElInput v-model="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')">
<template #prefix>
<SvgIcon icon="mdi:cellphone" />
</template>
</ElInput>
</ElFormItem>
<ElFormItem prop="code">
<ElInput v-model="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
<ElInput v-model="model.code" :placeholder="$t('page.login.common.codePlaceholder')">
<template #prefix>
<SvgIcon icon="mdi:shield-check-outline" />
</template>
</ElInput>
</ElFormItem>
<ElFormItem prop="password">
<ElInput
v-model="model.password"
type="password"
show-password-on="click"
show-password
:placeholder="$t('page.login.common.passwordPlaceholder')"
/>
>
<template #prefix>
<SvgIcon icon="mdi:lock-outline" />
</template>
</ElInput>
</ElFormItem>
<ElFormItem prop="confirmPassword">
<ElInput
v-model="model.confirmPassword"
type="password"
show-password-on="click"
show-password
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
/>
>
<template #prefix>
<SvgIcon icon="mdi:lock-check-outline" />
</template>
</ElInput>
</ElFormItem>
<ElSpace direction="vertical" fill :size="18" class="w-full">
<ElButton type="primary" size="large" round @click="handleSubmit">
{{ $t('common.confirm') }}
</ElButton>
<ElButton size="large" round @click="toggleLoginModule('pwd-login')">
{{ $t('page.login.common.back') }}
</ElButton>
</ElSpace>
<ElButton type="primary" size="large" class="login-submit-button" @click="handleSubmit">
{{ $t('common.confirm') }}
</ElButton>
<ElButton size="large" class="login-back-button" @click="toggleLoginModule('pwd-login')">
{{ $t('page.login.common.back') }}
</ElButton>
</ElForm>
</template>
<style scoped></style>
<style scoped>
.login-back-button {
width: 100%;
height: 44px;
margin-top: 14px;
margin-left: 0;
border-radius: 10px;
}
</style>