refactor(projects): 登录页面重新设计
This commit is contained in:
4
.env
4
.env
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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: '版本',
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user