feat(system): 登录页重做、布局白化与首页 Bento 仪表盘(视觉体系统一落地)

- 登录页:雾灰点阵底 + 右上涟漪同心圆 + 顶部靛蓝天光;居中纸白卡片(品牌区/欢迎回来/表单);逻辑不变
- BasicLayout:侧栏深色面板改为中性灰微渐变 + 白底浮起当前项(左 2px 靛蓝条);logo 区水线;内容区雾灰点阵
- 首页:Bento Grid 仪表盘(参数 16 项/岗位 3 个为真实数据;绩效/回款/待办为业务上线前的精致空态)
- AppMenu:激活项白底浮起样式,菜单变量随浅色侧栏调整

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
liuwenxin 2026-10-13 00:09:41 +08:00
parent 4c442b8cf2
commit d7fbec1177
5 changed files with 545 additions and 292 deletions

View File

@ -30,6 +30,9 @@
--ys-text: #1c2333; --ys-text: #1c2333;
--ys-muted: #5b6577; --ys-muted: #5b6577;
/* 品牌字:思源黑体(实施时自托管 4 字子集,内网可用) */
--ys-font-brand: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
/* /*
* Element Plus 主色 = 靛蓝(白字对比约 7.5:1)。 * Element Plus 主色 = 靛蓝(白字对比约 7.5:1)。
* 色阶为按 Element Plus 公式「主色与白色混合」预先算好的静态值 —— * 色阶为按 Element Plus 公式「主色与白色混合」预先算好的静态值 ——

View File

@ -24,26 +24,42 @@ const { topLevelMenus, groupedMenus } = useAppMenu()
<style scoped> <style scoped>
/* /*
* 深色底上的菜单:覆盖 Element Plus 的主题变量而非直接写颜色 —— * 中性灰侧栏上的菜单:覆盖 Element Plus 的主题变量而非直接写颜色 ——
* 后者与组件库规则同权重,受样式注入顺序影响而不稳定。 * 后者与组件库规则同权重,受样式注入顺序影响而不稳定。
* 激活色用亮金(深色底专用档,见 index.css 的金色两档约定)。 * 激活项 = 白底浮起(微渐变 + 内高光 + 分层阴影)+ 左侧 2px 靛蓝条。
*/ */
.app-menu { .app-menu {
--el-menu-bg-color: transparent; --el-menu-bg-color: transparent;
--el-menu-text-color: rgb(250 250 247 / 78%); --el-menu-text-color: var(--ys-ink-2);
--el-menu-active-color: var(--ys-gold-bright); --el-menu-active-color: var(--ys-indigo);
--el-menu-hover-bg-color: rgb(255 255 255 / 6%); --el-menu-hover-bg-color: rgba(255, 255, 255, 0.6);
--el-menu-item-height: 44px; --el-menu-item-height: 40px;
border-right: none; border-right: none;
} }
.app-menu :deep(.el-menu-item.is-active) { .app-menu :deep(.el-menu-item.is-active) {
background-color: rgb(201 162 39 / 12%); position: relative;
font-weight: 600; background: linear-gradient(180deg, #ffffff, #fbfcfe);
box-shadow:
inset 0 1px 0 #fff,
0 1px 2px rgba(28, 35, 51, 0.06),
0 4px 10px rgba(28, 35, 51, 0.06);
font-weight: 500;
}
.app-menu :deep(.el-menu-item.is-active)::before {
content: '';
position: absolute;
left: -14px;
top: 9px;
bottom: 9px;
width: 2px;
border-radius: 1px;
background: var(--ys-indigo);
} }
.app-menu :deep(.el-sub-menu.is-active > .el-sub-menu__title) { .app-menu :deep(.el-sub-menu.is-active > .el-sub-menu__title) {
color: var(--ys-gold-bright); color: var(--ys-indigo);
} }
</style> </style>

View File

@ -42,6 +42,7 @@ async function handleLogout(): Promise<void> {
<div class="app-brand"> <div class="app-brand">
<span class="app-brand__name">衍水集团</span> <span class="app-brand__name">衍水集团</span>
<span class="app-brand__sub">营销人员工资管理系统</span> <span class="app-brand__sub">营销人员工资管理系统</span>
<span class="app-brand__line"></span>
</div> </div>
<AppMenu class="app-aside__menu" /> <AppMenu class="app-aside__menu" />
</el-aside> </el-aside>
@ -96,6 +97,7 @@ async function handleLogout(): Promise<void> {
<div class="app-brand"> <div class="app-brand">
<span class="app-brand__name">衍水集团</span> <span class="app-brand__name">衍水集团</span>
<span class="app-brand__sub">营销人员工资管理系统</span> <span class="app-brand__sub">营销人员工资管理系统</span>
<span class="app-brand__line"></span>
</div> </div>
<AppMenu /> <AppMenu />
</el-drawer> </el-drawer>
@ -110,27 +112,39 @@ async function handleLogout(): Promise<void> {
.app-aside { .app-aside {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: linear-gradient(180deg, var(--ys-ink-900) 0%, var(--ys-ink-700) 100%); /* 中性灰侧栏:比页面底沉一档,与纸白卡片拉开层次 */
background: linear-gradient(180deg, #f2f4f8 0%, #ecedf3 100%);
border-right: 1px solid var(--ys-line);
} }
.app-brand { .app-brand {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px; gap: 3px;
padding: 18px 20px 14px; padding: 18px 20px 12px;
} }
.app-brand__name { .app-brand__name {
font-family: 'Songti SC', SimSun, serif; font-family: var(--ys-font-brand);
font-size: 18px; font-size: 16px;
letter-spacing: 2px; font-weight: 500;
color: var(--ys-paper); letter-spacing: 4px;
color: var(--ys-ink);
} }
.app-brand__sub { .app-brand__sub {
font-size: 12px; font-size: 11px;
letter-spacing: 1px; letter-spacing: 0.06em;
color: rgb(250 250 247 / 55%); color: var(--ys-ink-3);
}
/* 品牌签名:logo 下的渐隐水线(全站唯一装饰元素,页面标题处同款) */
.app-brand__line {
width: 24px;
height: 2px;
border-radius: 1px;
margin-top: 8px;
background: linear-gradient(90deg, var(--ys-indigo), rgba(46, 75, 168, 0));
} }
.app-aside__menu { .app-aside__menu {
@ -143,8 +157,8 @@ async function handleLogout(): Promise<void> {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
height: 56px; height: 56px;
background-color: #fff; background: linear-gradient(180deg, #ffffff, #fdfdfe);
border-bottom: 1px solid #e5e7eb; border-bottom: 1px solid var(--ys-line);
} }
.app-header__left { .app-header__left {
@ -173,7 +187,7 @@ async function handleLogout(): Promise<void> {
} }
.app-header__menu-toggle:focus-visible { .app-header__menu-toggle:focus-visible {
outline: 2px solid var(--ys-gold-deep); outline: 2px solid var(--ys-indigo);
outline-offset: 2px; outline-offset: 2px;
} }
@ -190,7 +204,10 @@ async function handleLogout(): Promise<void> {
.app-main { .app-main {
padding: 20px; padding: 20px;
background-color: #f5f7fa; /* 雾灰底 + 点阵网格:内容区的秩序与密度 */
background-color: var(--ys-mist);
background-image: radial-gradient(rgba(28, 35, 51, 0.05) 1px, transparent 1px);
background-size: 24px 24px;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@ -211,9 +228,9 @@ async function handleLogout(): Promise<void> {
</style> </style>
<style> <style>
/* 抽屉内容铺满深色底(非 scoped:目标元素由 el-drawer 渲染在组件根之外) */ /* 抽屉内容铺满侧栏同款灰(非 scoped:目标元素由 el-drawer 渲染在组件根之外) */
.app-drawer .el-drawer__body { .app-drawer .el-drawer__body {
padding: 0; padding: 0;
background: linear-gradient(180deg, var(--ys-ink-900) 0%, var(--ys-ink-700) 100%); background: linear-gradient(180deg, #f2f4f8 0%, #ecedf3 100%);
} }
</style> </style>

View File

@ -1,49 +1,350 @@
<script setup lang="ts"> <script setup lang="ts">
import { useUserStore } from '@/store/user' import { onMounted, ref } from 'vue'
import { formatDateTime } from '@/utils/format' import { useRouter } from 'vue-router'
import * as businessRoleApi from '@/api/businessRoles'
import * as paramsApi from '@/api/params'
import { handledError } from '@/utils/error'
const router = useRouter()
/** /**
* 首页:登录链路验证页。 * 首页仪表盘(Bento Grid,2026-10-12 视觉方向)。
* *
* 顶部栏(用户信息、退出登录)已由 BasicLayout 提供,本页只保留内容区; * M1 阶段:计提参数与岗位配置已就绪(真实数据);绩效/回款/待办等业务卡
* 用户名等字段的展示是端到端脚本的断言对象,调整时需同步 e2e。 * 展示空态 —— 业务数据管理上线后接入真实统计(图表用 ECharts 渲染)。
* 空态文案遵循全局规范:空态是邀请,说明「这里将出现什么、怎么让它出现」。
*/ */
const userStore = useUserStore() const roleCount = ref<number | null>(null)
const paramCount = ref<number | null>(null)
const latestVersion = ref<string | null>(null)
onMounted(() => {
// 三项统计各自容错:一张卡失败不影响其余卡片
void (async () => {
try {
const page = await businessRoleApi.listBusinessRoles({ pageNum: 1, pageSize: 1 })
roleCount.value = page.total
} catch (error) {
handledError(error)
}
})()
void (async () => {
try {
const catalog = await paramsApi.getCatalog()
paramCount.value = catalog.length
} catch (error) {
handledError(error)
}
})()
void (async () => {
try {
const page = await paramsApi.listBatches({ pageNum: 1, pageSize: 1 })
latestVersion.value = page.list[0]?.version_no ?? null
} catch (error) {
handledError(error)
}
})()
})
</script> </script>
<template> <template>
<el-card shadow="never"> <div>
<template #header> <div class="page-head">
<span>登录状态验证通过</span> <h1 class="page-title">仪表盘</h1>
</template> <p class="page-desc">系统概览 · 计提参数与岗位配置就绪,业务数据管理上线后接入核算统计</p>
<span class="waterline"></span>
</div>
<el-descriptions :column="2" border> <div class="bento">
<el-descriptions-item label="用户 ID">{{ userStore.userInfo?.id }}</el-descriptions-item> <!-- 绩效总额:业务上线前的空态 -->
<el-descriptions-item label="用户名">{{ userStore.userInfo?.username }}</el-descriptions-item> <div class="bento-card b-2x2">
<el-descriptions-item label="姓名"> <div class="b-eyebrow">本月绩效总额</div>
{{ userStore.userInfo?.real_name || '—' }} <div class="b-empty">
</el-descriptions-item> <div class="b-empty-title">暂无核算数据</div>
<el-descriptions-item label="角色"> <div class="b-empty-desc">回款录入并核算后,这里将显示本月绩效总额与趋势</div>
{{ userStore.roles.join('、') || '—' }} </div>
</el-descriptions-item> </div>
<el-descriptions-item label="上次登录">
{{ formatDateTime(userStore.userInfo?.last_login_time) }}
</el-descriptions-item>
</el-descriptions>
<el-alert <!-- 回款趋势:业务上线前的空态 -->
class="dashboard-hint" <div class="bento-card b-2x2">
type="info" <div class="b-eyebrow">近 6 个月回款</div>
:closable="false" <div class="b-empty">
show-icon <div class="b-empty-title">暂无回款数据</div>
title="这是登录链路的验证页" <div class="b-empty-desc">业务数据管理上线后,此处展示按月回款趋势</div>
description="刷新页面可验证会话恢复(守卫会调用 /me);「系统管理」菜单已接入用户管理、角色管理与日志查询。" </div>
/> </div>
</el-card>
<!-- 待办 -->
<div class="bento-card b-2x1">
<div class="b-eyebrow">待办</div>
<div class="b-empty-mini">业务数据管理上线后,待核算回款会出现在这里</div>
</div>
<!-- 营销人员:人员管理上线后接入 -->
<div class="bento-card">
<div class="b-eyebrow">营销人员</div>
<div class="b-empty-mini">人员管理上线后同步</div>
</div>
<!-- 岗位角色:真实数据 -->
<div class="bento-card">
<div class="b-eyebrow">岗位角色</div>
<div class="b-num">{{ roleCount ?? '—' }}<small>个</small></div>
<button class="b-link" type="button" @click="router.push('/system/business-roles')">
管理 ›
</button>
</div>
<!-- 计提参数:真实数据 -->
<div class="bento-card">
<div class="b-eyebrow">计提参数</div>
<div class="b-num">{{ paramCount ?? '—' }}<small>项</small></div>
<button class="b-link" type="button" @click="router.push('/system/params')">
维护 ›
</button>
</div>
<!-- 常用操作 -->
<div class="bento-card b-2x1">
<div class="b-eyebrow">常用操作</div>
<div class="b-links">
<button class="b-op" type="button" @click="router.push('/system/params')">
计提参数
</button>
<button class="b-op" type="button" @click="router.push('/system/business-roles')">
岗位角色
</button>
<button class="b-op" type="button" @click="router.push('/system/logs')">
操作日志
</button>
<span class="b-op b-op-disabled" title="业务数据管理上线后开放">录入回款</span>
</div>
</div>
<!-- T+ 同步 -->
<div class="bento-card">
<div class="b-eyebrow">T+ 同步</div>
<div class="b-state">待接入</div>
<div class="b-sub">每周一次 · 手动触发</div>
</div>
</div>
</div>
</template> </template>
<style scoped> <style scoped>
.dashboard-hint { .page-head {
margin-top: 18px; margin-bottom: 18px;
}
.page-title {
margin: 0;
font-size: 18px;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--ys-ink);
}
.page-desc {
margin: 4px 0 0;
font-size: 12px;
color: var(--ys-ink-3);
}
.waterline {
display: block;
width: 56px;
height: 2px;
margin-top: 10px;
border-radius: 1px;
background: linear-gradient(90deg, var(--ys-indigo), rgba(46, 75, 168, 0));
}
/* Bento Grid:4 列基格,卡片按重要性混排大小 */
.bento {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 102px;
gap: 14px;
}
.bento-card {
display: flex;
flex-direction: column;
padding: 17px 20px;
overflow: hidden;
background: linear-gradient(180deg, #ffffff, #fcfcfe);
border: 1px solid var(--ys-line);
border-radius: 10px;
box-shadow:
0 1px 2px rgba(28, 35, 51, 0.04),
0 3px 10px rgba(28, 35, 51, 0.03);
transition:
transform 180ms cubic-bezier(0.25, 0.1, 0.25, 1),
box-shadow 180ms cubic-bezier(0.25, 0.1, 0.25, 1);
}
.bento-card:hover {
transform: translateY(-2px);
box-shadow:
0 1px 2px rgba(28, 35, 51, 0.05),
0 6px 18px rgba(28, 35, 51, 0.07);
}
.b-2x2 {
grid-column: span 2;
grid-row: span 2;
}
.b-2x1 {
grid-column: span 2;
}
.b-eyebrow {
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
color: var(--ys-ink-2);
}
.b-num {
margin-top: auto;
font-size: 24px;
font-weight: 600;
font-variant-numeric: tabular-nums;
line-height: 1.2;
letter-spacing: -0.01em;
color: var(--ys-ink);
}
.b-num small {
margin-left: 3px;
font-size: 12px;
font-weight: 400;
color: var(--ys-ink-3);
}
.b-sub {
margin-top: 3px;
font-size: 11px;
color: var(--ys-ink-3);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.b-state {
margin-top: auto;
font-size: 15px;
font-weight: 600;
color: var(--ys-ink-2);
}
.b-empty {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.b-empty-title {
font-size: 14px;
font-weight: 500;
color: var(--ys-ink-2);
}
.b-empty-desc {
max-width: 260px;
margin-top: 6px;
font-size: 12px;
color: var(--ys-ink-3);
line-height: 1.7;
}
.b-empty-mini {
margin-top: auto;
margin-bottom: auto;
font-size: 12px;
color: var(--ys-ink-3);
line-height: 1.7;
}
.b-links {
display: flex;
gap: 8px;
align-items: center;
margin-top: auto;
}
.b-link {
padding: 0;
margin-top: 6px;
font-size: 12px;
color: var(--ys-indigo);
text-align: left;
cursor: pointer;
background: none;
border: none;
}
.b-link:hover {
color: var(--ys-indigo-deep);
}
.b-op {
height: 30px;
padding: 0 13px;
font-size: 12px;
font-family: inherit;
color: var(--ys-ink);
cursor: pointer;
background: linear-gradient(180deg, #ffffff, #fbfcfe);
border: 1px solid var(--ys-line);
border-radius: 7px;
transition:
color 150ms cubic-bezier(0.25, 0.1, 0.25, 1),
border-color 150ms cubic-bezier(0.25, 0.1, 0.25, 1);
}
button.b-op:hover {
color: var(--ys-indigo);
border-color: rgba(46, 75, 168, 0.45);
}
.b-op-disabled {
color: var(--ys-ink-3);
cursor: not-allowed;
}
@media (max-width: 1024px) {
.bento {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.bento {
grid-template-columns: 1fr;
grid-auto-rows: auto;
}
.bento-card {
min-height: 90px;
}
.b-2x2,
.b-2x1 {
grid-column: span 1;
grid-row: span 1;
min-height: 130px;
}
}
@media (prefers-reduced-motion: reduce) {
.bento-card {
transition: none;
}
} }
</style> </style>

View File

@ -77,53 +77,29 @@ onMounted(refreshCaptcha)
<template> <template>
<div class="login"> <div class="login">
<!-- 品牌面板:以「水」为意象 —— 公司名衍水,业务是流水之账 --> <!-- 背景装饰:右上涟漪同心圆(「衍水」品牌意象);顶部天光由 .login::before 承担 -->
<section class="brand"> <div class="login-ripple" aria-hidden="true">
<!-- 水纹层理:越往下越密,如潜向深处。纯装饰,对读屏隐藏 --> <i></i><i></i><i></i><i></i>
<svg class="brand-flow" viewBox="0 0 600 1000" preserveAspectRatio="xMidYMid slice" aria-hidden="true"> </div>
<path class="brand-flow__line" style="--i: 0" d="M-40,150 C 150,95 260,205 420,155 S 620,95 660,120" />
<path class="brand-flow__line" style="--i: 1" d="M-40,265 C 170,200 300,320 460,255 S 630,190 660,215" />
<path class="brand-flow__line" style="--i: 2" d="M-40,372 C 160,318 320,420 470,360 S 640,300 660,320" />
<path class="brand-flow__line" style="--i: 3" d="M-40,470 C 180,428 310,510 480,462 S 645,410 660,425" />
<path class="brand-flow__line" style="--i: 4" d="M-40,562 C 150,532 330,596 470,560 S 640,520 660,530" />
<path class="brand-flow__line" style="--i: 5" d="M-40,650 C 190,626 300,678 490,648 S 650,618 660,624" />
<path class="brand-flow__line" style="--i: 6" d="M-40,732 C 160,714 320,752 460,730 S 640,708 660,712" />
<path class="brand-flow__line" style="--i: 7" d="M-40,810 C 175,798 305,826 485,808 S 645,792 660,795" />
<path class="brand-flow__line" style="--i: 8" d="M-40,882 C 155,873 330,894 470,880 S 645,868 660,870" />
<path class="brand-flow__line" style="--i: 9" d="M-40,946 C 185,939 300,955 490,944 S 650,934 660,935" />
</svg>
<!-- 品牌内容整体居中成块。此前用 space-between 把品牌与标语顶到两端, <div class="login-wrap">
中间空出大片,两处都成了孤岛;聚成一块才有构图 --> <div class="login-card">
<div class="brand-inner"> <div class="card-brand">
<div class="brand-block"> <div class="card-brand__name">衍水集团</div>
<h1 class="brand-name">衍水集团</h1> <p class="card-brand__slogan">把每一笔回款,算成一份踏实。</p>
<hr class="brand-rule" /> <span class="waterline"></span>
<p class="brand-system">营销人员工资管理系统</p>
<!-- 断行位置显式控制:中文可在任意字间断行,交给浏览器自动换行
会把「楚」这样的单字挤到末行 -->
<p class="brand-hero">
<span>每一笔回款,</span>
<span>都算得清楚</span>
</p>
<p class="brand-stages">回款 · 计提 · 核算 · 发放</p>
</div> </div>
</div>
</section>
<!-- 表单面板 --> <div class="card-head">
<section class="panel"> <h2 class="card-title">欢迎回来</h2>
<div class="panel-inner"> <p class="card-hint">请输入账号密码进入系统</p>
<h2 class="panel-title">登录</h2> </div>
<p class="panel-hint">请使用管理员分配的账号</p>
<el-form <el-form
ref="formRef" ref="formRef"
:model="form" :model="form"
:rules="rules" :rules="rules"
label-position="top" label-position="top"
size="large"
@keyup.enter="handleSubmit" @keyup.enter="handleSubmit"
> >
<el-form-item label="用户名" prop="username"> <el-form-item label="用户名" prop="username">
@ -173,175 +149,149 @@ onMounted(refreshCaptcha)
</el-button> </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<p class="panel-foot">衍水集团 · 内部系统</p>
</div> </div>
</section>
<p class="login-foot">© 衍水集团 · 内部系统</p>
</div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
/* /* 视觉体系(2026-10-12 确认):企业级扁平 · 轻质感 · 品牌色点缀 · 微动效 */
* 配色:品牌 token 已提升为全局(见 assets/styles/index.css,布局框架与
* 系统管理页面共用),此处只做局部别名,用法与取值均与原先一致。
*/
.login { .login {
--ink-900: var(--ys-ink-900);
--ink-800: var(--ys-ink-800);
--ink-700: var(--ys-ink-700);
--ink-600: var(--ys-ink-600);
--gold-bright: var(--ys-gold-bright);
--gold-deep: var(--ys-gold-deep);
--paper: var(--ys-paper);
--text: var(--ys-text);
--muted: var(--ys-muted);
display: flex;
min-height: 100vh;
}
/* ============ 品牌面板 ============ */
.brand {
position: relative; position: relative;
display: flex; display: flex;
flex: 1 1 56%; flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 40px 24px;
overflow: hidden; overflow: hidden;
background: linear-gradient(158deg, var(--ink-800) 0%, var(--ink-900) 62%, #04141b 100%); background-color: #f2f4f8;
/* 点阵网格:秩序与密度 */
background-image: radial-gradient(rgba(28, 35, 51, 0.07) 1px, transparent 1px);
background-size: 24px 24px;
} }
/* 水下的光:给居中的内容一个落点,否则深色面板会显得空 */ /* 顶部天光:靛蓝微光从上方洒下,给页面空气感 */
.brand::before { .login::before {
position: absolute;
inset: 0;
content: ''; content: '';
background: radial-gradient(58% 46% at 32% 46%, rgb(32 116 146 / 26%), transparent 72%); position: absolute;
top: -180px;
left: 50%;
width: 900px;
height: 480px;
transform: translateX(-50%);
pointer-events: none;
background: radial-gradient(closest-side, rgba(46, 75, 168, 0.09), rgba(46, 75, 168, 0));
}
/* 右上涟漪:同心圆水波,部分出画 */
.login-ripple {
position: absolute;
top: -190px;
right: -150px;
width: 560px;
height: 560px;
pointer-events: none; pointer-events: none;
} }
.brand-flow { .login-ripple i {
position: absolute; position: absolute;
inset: 0; inset: 0;
width: 100%; border-radius: 50%;
height: 100%; border: 1px solid rgba(46, 75, 168, 0.1);
} }
.brand-flow__line { .login-ripple i:nth-child(2) {
fill: none; inset: 70px;
stroke: var(--ink-600); border-color: rgba(46, 75, 168, 0.075);
stroke-width: 1;
opacity: 0.55;
/* 逐条画出:一个统一的入场动作,而非散落特效 */
stroke-dasharray: 1200;
stroke-dashoffset: 1200;
animation: flow-draw 2.4s cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
animation-delay: calc(var(--i) * 0.13s);
} }
@keyframes flow-draw { .login-ripple i:nth-child(3) {
to { inset: 140px;
stroke-dashoffset: 0; border-color: rgba(46, 75, 168, 0.055);
}
} }
.brand-inner { .login-ripple i:nth-child(4) {
inset: 210px;
border: none;
background: radial-gradient(circle, rgba(46, 75, 168, 0.05), rgba(46, 75, 168, 0.015));
}
.login-wrap {
position: relative; position: relative;
z-index: 1;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
width: 100%;
padding: 56px clamp(40px, 6vw, 104px);
} }
/* 内容成块并限宽:超宽屏下文字块不会被拉散 */ /* 登录卡:纸白微渐变 + 半透明描边 + 分层阴影 + 入场上浮 */
.brand-block { .login-card {
max-width: 30rem; width: 400px;
max-width: 100%;
padding: 30px 36px 32px;
overflow: hidden;
animation: card-rise 320ms cubic-bezier(0.25, 0.1, 0.25, 1) backwards;
background: linear-gradient(180deg, #ffffff, #fcfcfe);
border: 1px solid rgba(28, 35, 51, 0.09);
border-radius: 14px;
box-shadow:
0 2px 4px rgba(28, 35, 51, 0.04),
0 16px 40px rgba(28, 35, 51, 0.1);
} }
.brand-name { @keyframes card-rise {
margin: 0;
font-family: 'Source Han Serif SC', 'Noto Serif SC', 'Songti SC', 'SimSun', serif;
font-size: clamp(36px, 3.6vw, 54px);
font-weight: 600;
line-height: 1.15;
letter-spacing: 0.2em;
color: #f2f6f7;
}
.brand-rule {
width: 52px;
height: 2px;
margin: 24px 0 18px;
background-color: var(--gold-bright);
border: 0;
}
.brand-system {
margin: 0;
font-size: clamp(13px, 1.1vw, 16px);
font-weight: 300;
letter-spacing: 0.28em;
color: #9db6c0;
}
.brand-hero {
margin: 58px 0 0;
font-size: clamp(16px, 1.4vw, 21px);
font-weight: 400;
line-height: 1.8;
letter-spacing: 0.06em;
color: #d9e5ea;
}
.brand-hero span {
display: block;
}
/* 三个业务环节:用间隔点连成一行,比带框的标签轻得多,
也不会被误读成可点击的按钮 */
.brand-stages {
margin: 18px 0 0;
font-size: 12px;
letter-spacing: 0.24em;
color: #6f8b97;
}
/* ============ 表单面板 ============ */
.panel {
display: flex;
flex: 1 1 44%;
align-items: center;
justify-content: center;
padding: 48px clamp(28px, 5vw, 80px);
background-color: var(--paper);
}
.panel-inner {
width: 100%;
max-width: 356px;
animation: panel-rise 0.7s cubic-bezier(0.22, 0.61, 0.36, 1) 0.35s both;
}
@keyframes panel-rise {
from { from {
opacity: 0; opacity: 0;
transform: translateY(14px); transform: translateY(8px);
} }
} }
.panel-title { .card-brand {
margin: 0 0 6px; text-align: center;
font-size: 25px;
font-weight: 600;
letter-spacing: 0.16em;
color: var(--text);
} }
.panel-hint { .card-brand__name {
margin: 0 0 30px; font-family: var(--ys-font-brand);
font-size: 13px; font-size: 17px;
color: var(--muted); font-weight: 500;
letter-spacing: 0.34em;
color: var(--ys-ink);
}
.card-brand__slogan {
margin-top: 8px;
font-size: 12.5px;
letter-spacing: 0.02em;
color: var(--ys-ink-3);
}
/* 签名元素:渐隐靛蓝水线 */
.waterline {
display: block;
width: 40px;
height: 2px;
margin: 14px auto 0;
border-radius: 1px;
background: linear-gradient(90deg, var(--ys-indigo), rgba(46, 75, 168, 0));
}
.card-head {
margin-top: 26px;
}
.card-title {
margin: 0;
font-size: 15px;
font-weight: 600;
color: var(--ys-ink);
}
.card-hint {
margin: 4px 0 0;
font-size: 12px;
color: var(--ys-ink-3);
} }
.captcha { .captcha {
@ -370,14 +320,15 @@ onMounted(refreshCaptcha)
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
background-color: #fff; background: linear-gradient(180deg, #f7f8fb, #f1f3f8);
border: 1px solid #dcdfe6; border: 1px solid var(--ys-line);
border-radius: 4px; border-radius: 7px;
transition: border-color 0.2s; box-shadow: inset 0 1px 2px rgba(28, 35, 51, 0.04);
transition: border-color 150ms var(--ys-ease, ease);
} }
.captcha__image:hover { .captcha__image:hover {
border-color: var(--gold-deep); border-color: var(--ys-indigo);
} }
.captcha__image img { .captcha__image img {
@ -389,89 +340,54 @@ onMounted(refreshCaptcha)
.captcha__placeholder { .captcha__placeholder {
font-size: 12px; font-size: 12px;
color: var(--muted); color: var(--ys-ink-3);
} }
.form-actions :deep(.el-form-item__content) { .form-actions :deep(.el-form-item__content) {
margin-top: 4px; margin-top: 4px;
} }
/* 覆盖 Element Plus 的按钮变量而非直接写 background-color ——
后者与 .el-button--primary 同权重,会受样式注入顺序影响而不稳定 */
.submit { .submit {
width: 100%; width: 100%;
letter-spacing: 0.4em; letter-spacing: 0.4em;
--el-button-bg-color: var(--gold-deep);
--el-button-border-color: var(--gold-deep);
--el-button-hover-bg-color: #a3762a;
--el-button-hover-border-color: #a3762a;
--el-button-active-bg-color: #7d5719;
--el-button-active-border-color: #7d5719;
} }
.panel-foot { .login-foot {
margin: 32px 0 0; margin: 16px 0 0;
font-size: 12px; font-size: 11px;
text-align: center; letter-spacing: 0.08em;
letter-spacing: 0.1em; color: var(--ys-ink-3);
color: #9aa8af;
} }
/* 输入框聚焦态也落在同一套配色上 */ /* 输入聚焦落在体系主色上(Element Plus 主色已随全局 token 换为靛蓝) */
.panel :deep(.el-input__wrapper) { .login :deep(.el-input__wrapper) {
--el-input-focus-border-color: var(--gold-deep); box-shadow: 0 0 0 1px rgba(28, 35, 51, 0.13) inset;
} }
/* ============ 响应式 ============ */ .login :deep(.el-input__wrapper.is-focus) {
box-shadow:
0 0 0 1px var(--ys-indigo) inset,
0 0 0 3px rgba(46, 75, 168, 0.13);
}
@media (max-width: 900px) { @media (max-width: 640px) {
.login { .login {
flex-direction: column; padding: 24px 16px;
} }
.brand { .login-card {
flex: 0 0 auto; padding: 26px 24px 28px;
padding: 36px 26px 32px;
} }
.brand-inner { /* 窄屏上涟漪出画更多,避免挤压卡片 */
padding: 0; .login-ripple {
} top: -260px;
right: -240px;
.brand-flow {
opacity: 0.5;
}
.brand-name {
font-size: 30px;
letter-spacing: 0.16em;
}
.brand-rule {
margin: 16px 0 12px;
}
/* 窄屏上只留品牌识别,标语与环节行让位给表单 */
.brand-hero,
.brand-stages {
display: none;
}
.panel {
flex: 1;
padding: 38px 26px 46px;
} }
} }
/* 尊重系统的「减弱动效」偏好:保留终态,去掉过渡 */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.brand-flow__line { .login-card {
stroke-dashoffset: 0;
animation: none;
}
.panel-inner {
opacity: 1;
animation: none; animation: none;
} }
} }