feat(frontend): 视觉规范 v4 全站落地与实测修复

token 补齐与 EP 形体基准、全站版式工具类(.page-head/.card 系/.filter-bar/.ys-pill 等)、布局与登录页、工作台 Bento 与图表主题、打印稿纸、全部业务页统一到全局类;StatusTag 改 pill、AppPagination 加 background。

浏览器实测修复:表格行高收到 1.5;月结生命周期条圆点按阶段着色并加操作提示;工作台空态垂直居中;状态/结果列宽按胶囊内容给足(96/108/120),消除 Element Plus 省略号伪『小点』。设计稿反向同步圆点着色与流程条提示。
This commit is contained in:
liuwenxin 2026-10-23 09:58:48 +08:00
parent 1f65913002
commit 8e8bcaa5fb
16 changed files with 415 additions and 132 deletions

View File

@ -508,12 +508,15 @@
.flow-step:last-child { border-radius: 0 var(--ys-radius-sm) var(--ys-radius-sm) 0; }
.flow-step + .flow-step { border-left: 0; }
.flow-step .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ys-ink-3); }
.flow-step.done { color: var(--ys-ink); }
.flow-step.done .dot { background: var(--ys-ok); }
/* 圆点按「阶段」着色,与选中与否无关 —— 未选中时也能看出四段各自代表什么;
颜色之外每段仍有文字,不靠颜色单独表意 */
.flow-step.tone-wait .dot { background: var(--ys-warn); }
.flow-step.tone-lock .dot { background: var(--ys-indigo); }
.flow-step.tone-reject .dot { background: var(--ys-danger); }
.flow-step.cur { color: var(--ys-indigo); font-weight: 500; background: var(--ys-indigo-wash); border-color: rgba(46, 75, 168, 0.3); }
.flow-step.cur .dot { background: var(--ys-indigo); }
.flow-step.rejected .dot { background: var(--ys-danger); }
.flow-step.rejected { color: var(--ys-danger); }
/* 流程条右侧挂一行操作提示:没有提示时,四段读起来像四颗孤立的小按钮 */
.flow-hint { margin-left: 10px; font-size: 12px; color: var(--ys-ink-3); }
/* 打印稿(A4):独立顶层路由,浏览器打印只出报表本身 */
.sheet {
@ -1204,10 +1207,11 @@
<div class="comp-card" style="margin-bottom:14px;padding:16px 20px">
<div class="b-eyebrow" style="margin-bottom:12px">月结生命周期</div>
<div class="flow">
<div class="flow-step done"><span class="dot"></span>草稿</div>
<div class="flow-step cur"><span class="dot"></span>待审批</div>
<div class="flow-step"><span class="dot"></span>已审定(锁定)</div>
<div class="flow-step rejected"><span class="dot"></span>已驳回 → 回到草稿</div>
<div class="flow-step"><span class="dot"></span>草稿</div>
<div class="flow-step cur tone-wait"><span class="dot"></span>待审批</div>
<div class="flow-step tone-lock"><span class="dot"></span>已审定(锁定)</div>
<div class="flow-step tone-reject"><span class="dot"></span>已驳回 → 回到草稿</div>
<span class="flow-hint">点状态只看该阶段,再点一次取消</span>
</div>
</div>
<div class="filter">

View File

@ -102,7 +102,9 @@ pnpm build # 类型检查 + 生产构建
**语义三色**(2026-10-22 落地):成功 `#1e9e6a` / 警示 `#c98a1b` / 危险 `#c94f4f`,已覆盖 `--el-color-success/warning/danger/error` 及其 `-rgb`、`light-3/5/7/8/9`、`dark-2` 全阶。Element Plus 默认的绿黄红饱和度过高,放在灰底卡片上会跳色。**`-rgb` 变体必须一并覆盖**——组件库的半透明底色用 `rgba(var(--el-color-success-rgb), .1)` 取色,漏改会出现「文字是新的、底色还是旧的」。色阶按 Element Plus 公式预计算,不用 `color-mix()`。
**组件基调**(同样写在 `src/assets/styles/index.css`,页面不要再重复):按钮高 34px / 圆角 7px / 字号 13px,主按钮是 `#3a58bc → #2e4ba8` 微渐变 + 内高光 + 悬停上抬 1px 并加深阴影;输入框高 34px,焦点为靛蓝描边加 3px 柔光外环;表头 12px/500/字距 `0.04em` + 极浅渐变底,单元格内距 12px 16px,行悬停 120ms;分页需 `background` 模式,当前页与主按钮同材质;`el-tag` 压到 11px 方角。动效统一 120–180ms,只做颜色、阴影与 1px 位移,并尊重 `prefers-reduced-motion`。
**组件基调**(同样写在 `src/assets/styles/index.css`,页面不要再重复):按钮高 34px / 圆角 7px / 字号 13px,主按钮是 `#3a58bc → #2e4ba8` 微渐变 + 内高光 + 悬停上抬 1px 并加深阴影;输入框高 34px,焦点为靛蓝描边加 3px 柔光外环;表头 12px/500/字距 `0.04em` + 极浅渐变底,单元格内距 12px 16px、行高 1.5,行悬停 120ms;分页需 `background` 模式,当前页与主按钮同材质;`el-tag` 压到 11px 方角。动效统一 120–180ms,只做颜色、阴影与 1px 位移,并尊重 `prefers-reduced-motion`。
**表格行高必须是 1.5**(全局 `.el-table .cell`):Element Plus 默认给单元格 23px 行高(≈1.77 倍),加上 12px 上下内距,单行数据行会到 51px——一屏少看两行。收到 1.5 后单行数据行 44px、纯文本行 44px,与设计稿「12px 内距 + 13px 字」对齐;多行单元格仍按内容撑开。
**页面版式:只用全局类**(新页面不再各写一套)
@ -119,7 +121,25 @@ pnpm build # 类型检查 + 生产构建
`.cell-num` 有两种用法:直接挂在 `td` 上,或包一层 `<span class="cell-num">` 处理「一行里既文本又数字」。给后者单独撑满一行是必要的——行内元素的 `text-align` 无效;但**不能**给 `.cell-num` 统一加 `display`,那会把 `td` 从 `table-cell` 变成 `inline-block`,表格列宽算法随即失效。
**菜单图标**(2026-10-22 落地,M1 遗留项):路由 `meta.icon` 只写**语义 key**,图标实现集中在 `src/components/layout/menuIcons.ts` 的注册表(`@element-plus/icons-vue` 具名导入,可摇树)。换图标库或换风格只改注册表,路由表不动。视觉口径沿用设计稿:16px、透明度 0.7(激活项 1)、右间距 9px;分组标题**不加图标**,只有菜单项有。**新增页面必须补 `meta.icon`**,否则菜单项只有文字。
**表单页版式(公司信息页为样板)**
设置类表单页不要把表单缩在左上角一小块——内容区再宽也只占三分之一,页面看着像没做完。`views/system/settings/index.vue` 是基准:
| 做法 | 口径 |
| --- | --- |
| 网格 | 左列 `minmax(0, 1fr)` + 右列固定 480px(实景预览),`align-items: start`;≤1200px 折成单列 |
| 控件行 | 主控件与说明同一行(`.field-control`):控件 ≤420px,说明跟在后面 20px;≥1600px 说明改右对齐贴卡片右缘,整行铺满而不是缩在左上角 |
| 实景预览 | 右侧「登录页预览」按**真实登录页 1:1 复刻**(控件 38px 高、品牌名 17px / 0.34em 字距、logo ≤56×36、三项输入含验证码位、登录按钮、版权行),不是缩比示意——控件高度对不上就等于没有所见即所得 |
| 生效范围 | 独立一张卡列出这份设置会出现在哪(登录页品牌位与版权行 / Excel 导出页眉 / 打印稿页眉),用户不用存完再去别处找 |
| 预览态 | 预览卡 `aria-hidden` + 标注「示意,非交互」,避免被当成可点的登录框 |
**状态与流程表达(2026-10-23 实测补)**
- 月结生命周期条四段圆点按阶段着色:草稿中性 / 待审批琥珀 / 已审定靛蓝 / 已驳回砖红。颜色之外每段仍有文字,不靠颜色单独表意。
- 流程条这类分段控件旁要挂一行操作提示(如「点状态只看该阶段,再点一次取消」)。没有提示时,未选中态的四段读起来像四颗孤立的小按钮,不像一条流程。
- 卡片内空态(工作台 `.b-empty-mini`)在卡片剩余高度内居中,不要全挤在顶部留一整块空白。
**菜单图标**(2026-10-22 落地,M1 遗留项):路由 `meta.icon` 只写**语义 key**,图标实现集中在 `src/components/layout/menuIcons.ts` 的注册表(`@element-plus/icons-vue` 具名导入,可摇树)。换图标库或换风格只改注册表,路由表不动。视觉口径沿用设计稿:16px、透明度 0.7(激活项 1)、右间距 9px;分组标题**不加图标**,只有菜单项有。**新增页面必须补 `meta.icon`**,否则菜单项只有文字。注册表是 **UTF-8** 文件——批量转码脚本会把中文注释转成乱码(2026-10-23 已在该文件踩过一次,已修回),改这个文件不要过任何编码转换。
**签名元素**:**渐隐靛蓝水线**——各页面标题下的细线(56px,左实右虚)与登录页品牌名下的短线(40px),「衍水」之水的提纯,全站唯一的装饰性元素;登录页右上角另有涟漪同心圆与顶部靛蓝天光作背景(低透明度)。与早期深色面板 + 水纹层理同一意象,品牌连续而非推倒重来。
@ -135,6 +155,7 @@ pnpm build # 类型检查 + 生产构建
- 新页面需响应式到移动端、保留键盘焦点可见、并尊重 `prefers-reduced-motion`
- **确认框统一加 `customClass: 'ys-confirm-box'`,不要再传 `type: 'warning'`** —— Element Plus 的默认 warning 图标会渲染在正文左侧独立成列(多行正文时图标悬在文字块中间),与 v3 靛蓝体系也冲突。全局样式(`src/assets/styles/index.css`)已把图标改为 18px 描边样式并放在标题行左侧,正文与标题文字对齐
- **页面 scoped 里只写本页特有的东西**:表格 / 按钮 / 输入框 / 表头 / 分页的皮肤已经全局覆盖,页面再写一遍与组件库同权重,会受样式注入顺序影响而不稳定;结构用全局类(见上一节表格),样式只补本页独有的块(如展开区、抽屉内的键值表)
- **状态 / 结果列的固定宽度要按胶囊内容给足**:`.cell` 左右各 16px 内距(共 32px),2 字胶囊约 55px、3 字约 67px、4 字约 79px,宽度不够时 Element Plus 会在胶囊右侧画一个省略号(看起来像多了个小点)。口径:2 字列 ≥96px(如 启用/禁用、成功/失败)、3 字列 ≥108px(如 已核算/已替代/执行中)、4 字列 ≥120px(如 以物抵顶)
## 验证码图片

View File

@ -589,6 +589,12 @@ span.cell-num {
.el-table .cell {
padding: 0 16px;
/*
* Element Plus 默认给单元格 23px 行高(≈1.77 倍),加上 12px 上下内距,
* 单行数据行会到 51px —— 一屏少看两行,也与设计稿的「12px 内距 + 13px 字」
* 不符。收到 1.5(中文正文常用行高)后单行约 45px;多行单元格仍按内容撑开。
*/
line-height: 1.5;
}
.el-table__body tr:hover > td.el-table__cell {

View File

@ -25,19 +25,19 @@ import {
} from '@element-plus/icons-vue'
/**
* 鑿滃崟鍥炬爣娉ㄥ唽琛ㄣ??
* 菜单图标注册表。
*
* **璺敱鍙啓璇箟 key锛坢eta.icon锛夛紝涓嶇洿鎺ュ紩鐢ㄥ浘鏍囩粍浠?**锛氳矾鐢辫〃淇濇寔杞婚噺鍙?
* 搴忓垪鍖栵紝鍥炬爣瀹炵幇鎹㈠簱鎴栨崲椋庢牸鏃跺彧鏀规湰鏂囦欢锛岃矾鐢变笌鑿滃崟缁勪欢涓嶅姩銆?
* **路由只写语义 key(meta.icon),不直接引用图标组件**:路由表保持轻量、
* 序列化,图标实现换库或换风格时只改本文件,路由与菜单组件不动。
*
* 鍏峰悕瀵煎叆锛堜笉鐢? `import * as`锛変互淇濊瘉鎽囨爲 鈥斺?? 鏁村寘寮曞叆浼氭妸 200+ 鍥炬爣鎵撹繘
* 浜х墿銆傛敞鍐岃〃閲屽浣欑殑鍥炬爣鍚屾牱浼氳鎽囨帀锛屼絾浼氳銆岀敤浜嗗摢浜涖?嶉毦浠ユ牳瀵癸紝鏁?
* 鍙繚鐣欏疄闄呯敤鍒扮殑銆?
* 具名导入(不用 `import * as`)以保证摇树 —— 整包引入会把 200+ 图标打进
* 产物。注册表里多余的图标同样会被摇掉,但会让「用了哪些」难以核对,
* 只保留实际用到的。
*
* **鍏ㄩ儴鍙栫嚎鎬э紙outline锛夊浘鏍囷紝涓嶇敤 *-filled 鍙樹綋**锛氫晶鏍忓悓涓?灞傜骇娣风敤瀹炲績涓?
* 绾挎?у浘鏍囦細璁╄彍鍗曠湅鐫?涓嶆槸涓?濂楄瑷?锛堣璁$ǹ 搂04 鐨勫浘鏍囨棌鍗? 16px 绾挎?э級銆?
* 棣栭〉鐢? House锛堢嚎鎬ф埧灞嬶級鑰岄潪 HomeFilled锛屼汉鍛樼鐞嗙敤 Postcard锛堜汉鍛樻。妗堝崱锛?
* 鑰岄潪 UserFilled 鈥斺?? 鐢ㄦ埛绠$悊宸插崰 User锛屼汉鍛樻敼鐢ㄦ。妗堝崱浠ュ厤涓や釜銆屼汉銆嶆挒褰€??
* **全部取线性(outline)图标,不用 *-filled 变体**:侧栏同一层级混用实心与
* 线性图标会让菜单看着不是一套语汇(设计稿 §04 的图标族:16px 线性)。
* 首页用 House(线性房屋)而非 HomeFilled,人员管理用 Postcard(人员档案卡)
* 而非 UserFilled —— 用户管理已占 User,人员改用档案卡以免两个「人」撞形。
*/
const ICONS = {
home: House,
@ -63,10 +63,10 @@ const ICONS = {
'report-payment': PieChart,
} satisfies Record<string, Component>
/** 鑿滃崟鍥炬爣鐨勮涔? key锛堣矾鐢? meta.icon 鐨勫彇鍊艰寖鍥达級銆? */
/** 菜单图标的语义 key(路由 meta.icon 的取值范围)。 */
export type MenuIconName = keyof typeof ICONS
/** 鎸夎涔? key 鍙栧浘鏍囷紱鏈厤缃椂杩斿洖 undefined锛堣彍鍗曢」鍙樉绀烘枃瀛楋級銆? */
/** 按语义 key 取图标;未配置时返回 undefined(菜单项只显示文字)。 */
export function menuIcon(name?: string): Component | undefined {
return name ? (ICONS[name as MenuIconName] ?? undefined) : undefined
}

View File

@ -460,7 +460,7 @@ onMounted(() => {
<span class="cell-num">{{ row.salary_standard ?? '—' }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<el-table-column label="状态" width="96" align="center">
<template #default="{ row }">
<StatusTag :value="row.status" active-text="在职" inactive-text="离职" />
</template>

View File

@ -283,7 +283,7 @@ onMounted(() => {
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<el-table-column label="状态" width="96" align="center">
<template #default="{ row }">
<StatusTag :value="row.status" />
</template>

View File

@ -692,7 +692,7 @@ onMounted(() => {
<el-table-column prop="business_date" label="业务日期" min-width="110">
<template #default="{ row }">{{ row.business_date ?? '—' }}</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<el-table-column label="状态" width="108" align="center">
<template #default="{ row }">
<!-- 状态列统一用 pill:颜色之外必有文字,色觉差异不影响判读 -->
<span :class="statusPillClass(row.status)">

View File

@ -348,7 +348,7 @@ onMounted(() => {
<span class="cell-num">{{ row.amount }}</span>
</template>
</el-table-column>
<el-table-column label="以物抵顶" width="110" align="center">
<el-table-column label="以物抵顶" width="120" align="center">
<template #default="{ row }">
<!-- 以物抵顶影响折算比例,属需要一眼看见的分支,用 pill 而非普通文字 -->
<span v-if="row.is_barter === 1" class="ys-pill ys-pill--wait">以物抵顶</span>
@ -361,7 +361,7 @@ onMounted(() => {
<span v-else class="cell-muted">未指定批次</span>
</template>
</el-table-column>
<el-table-column label="核算状态" width="100" align="center">
<el-table-column label="核算状态" width="108" align="center">
<template #default="{ row }">
<!-- 已核算=已确认(靛绿)、未核算=待处理(琥珀);已核算行禁改禁删 -->
<span

View File

@ -158,7 +158,7 @@ async function submitRecalc(): Promise<void> {
<span class="cell-num">{{ row.payable_amount }}</span>
</template>
</el-table-column>
<el-table-column label="版本" width="100">
<el-table-column label="版本" width="108">
<template #default="{ row }">
<span class="cell-code">v{{ row.calc_version }}</span>
<!-- 历史版本用中性 pill:被替代不是异常,只是不再是生效版本 -->

View File

@ -411,6 +411,8 @@ function formatTime(value: string): string {
.bento-card {
position: relative;
display: flex;
flex-direction: column;
padding: 16px 18px;
overflow: hidden;
background: linear-gradient(180deg, var(--ys-paper), #fcfcfe);
@ -511,10 +513,19 @@ function formatTime(value: string): string {
color: var(--ys-ink-3);
}
/*
* 空态在卡片剩余高度里居中:待办/预警是 2×2 的卡,只有一行提示时
* 内容全挤在顶部、下方留一大片空白,看起来像没加载完。
*/
.b-empty-mini {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
margin-top: 8px;
font-size: 12px;
color: var(--ys-ink-3);
text-align: center;
}
.chart {

View File

@ -45,10 +45,10 @@ function resetFilters(): void {
* 复用 search() 意味着**不新增任何后端请求口径**。
*/
const FLOW_STEPS = [
{ value: 1, label: '草稿' },
{ value: 2, label: '待审批' },
{ value: 3, label: '已审定(锁定)' },
{ value: 4, label: '已驳回' },
{ value: 1, label: '草稿', tone: 'draft' },
{ value: 2, label: '待审批', tone: 'wait' },
{ value: 3, label: '已审定(锁定)', tone: 'lock' },
{ value: 4, label: '已驳回', tone: 'reject' },
] as const
/** 点当前段再点一次即取消筛选(idle 态),避免「点了没反应」的死角 */
@ -149,8 +149,10 @@ async function handleSubmit(row: Settlement): Promise<void> {
:class="{ 'flow-step__item--cur': filters.status === step.value }"
@click="pickStatus(step.value)"
>
<span class="flow-step__dot"></span>{{ step.label }}
<span class="flow-step__dot" :class="`flow-step__dot--${step.tone}`"></span>
{{ step.label }}
</button>
<span class="flow-step__hint">点状态只看该阶段,再点一次取消</span>
</div>
<!-- 生成行:文案保留「生成月结快照」,对应真实动作(生成的是快照批次) -->
@ -328,6 +330,7 @@ async function handleSubmit(row: Settlement): Promise<void> {
.flow-step {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-bottom: 14px;
}
@ -350,6 +353,14 @@ async function handleSubmit(row: Settlement): Promise<void> {
border-color 150ms var(--ys-ease);
}
/* 未选中时四段必须读起来仍是「一条流程」:给右侧挂一行说明,
高度与分段齐平,避免出现四颗孤立小按钮的观感 */
.flow-step__hint {
margin-left: 10px;
font-size: 12px;
color: var(--ys-ink-3);
}
.flow-step__item:first-child {
border-radius: var(--ys-radius-sm) 0 0 var(--ys-radius-sm);
}
@ -378,6 +389,19 @@ async function handleSubmit(row: Settlement): Promise<void> {
background: var(--ys-ink-3);
}
/* 圆点按阶段着色:未选中时也能看出四段各自代表什么(颜色之外仍有文字) */
.flow-step__dot--wait {
background: var(--ys-warn);
}
.flow-step__dot--lock {
background: var(--ys-indigo);
}
.flow-step__dot--reject {
background: var(--ys-danger);
}
.flow-step__item--cur {
font-weight: 500;
color: var(--ys-indigo);

View File

@ -276,7 +276,7 @@ onMounted(() => {
<el-table-column label="排序" width="70" align="right">
<template #default="{ row }"><span class="cell-num">{{ row.sort_order }}</span></template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<el-table-column label="状态" width="96" align="center">
<template #default="{ row }">
<StatusTag :value="row.status" />
</template>

View File

@ -265,7 +265,7 @@ watch(activeTab, (tab) => {
<el-table-column prop="module" label="模块" width="100" />
<el-table-column prop="action" label="操作" width="90" />
<el-table-column prop="summary" label="摘要" min-width="200" show-overflow-tooltip />
<el-table-column label="结果" width="80" align="center">
<el-table-column label="结果" width="96" align="center">
<template #default="{ row }">
<el-tooltip
:content="row.error_message ?? ''"
@ -368,7 +368,7 @@ watch(activeTab, (tab) => {
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column prop="username" label="用户名" min-width="130" />
<el-table-column label="结果" width="80" align="center">
<el-table-column label="结果" width="96" align="center">
<template #default="{ row }">
<StatusTag
:value="row.success"

View File

@ -245,7 +245,7 @@ onMounted(load)
<el-table-column label="排序" width="80" align="right">
<template #default="{ row }"><span class="cell-num">{{ row.sort_order }}</span></template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<el-table-column label="状态" width="96" align="center">
<template #default="{ row }">
<StatusTag :value="row.status" />
</template>

View File

@ -10,7 +10,10 @@ import { handledError } from '@/utils/error'
* 公司信息设置(2026-10-15 补充需求:公司名称、logo)。
*
* 签名元素是右侧「登录页实景预览」:品牌名与 logo 改动即时反映在一张
* 缩比登录卡上 —— 所见即所得,避免「保存后去登录页核对」的往返。
* **1:1 等比**的登录卡上 —— 所见即所得,避免「保存后去登录页核对」的往返。
* 预览按登录页的真实版式复刻(品牌区 / 欢迎回来 / 三项输入 / 登录按钮 /
* 版权行),控件尺寸与登录页一致,logo 展示位也按登录页的 ≤56×36 约束。
*
* logo 校验(png/jpg ≤2MB)与后端三重校验同口径,前端先拦一道。
*
* 进入页面即加载当前设置(设置页即所见即所得,无需手动刷新)。
@ -99,79 +102,131 @@ const previewName = computed(() => form.company_name.trim() || '衍水集团')
</div>
<div class="settings-grid">
<div class="card settings-card">
<!-- 卡片标题刻意不写「公司信息」:页面标题已经说明了,再重复一遍只是噪音 -->
<div class="card-head">
<span class="card-title">基础信息</span>
<!-- 左列:基础信息(表单)+ 生效范围(这份设置会出现在哪里) -->
<div class="settings-col">
<div class="card">
<!-- 卡片标题刻意不写「公司信息」:页面标题已经说明了,再重复一遍只是噪音 -->
<div class="card-head">
<span class="card-title">基础信息</span>
</div>
<div class="card-body">
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="92px"
@submit.prevent
>
<el-form-item label="公司名称" prop="company_name">
<!-- 控件与说明并排:说明靠行尾对齐,宽屏下整行铺满卡片而非缩在左上角 -->
<div class="field-control">
<el-input
v-model="form.company_name"
class="field-control__main"
maxlength="64"
placeholder="展示在登录页与导出页眉"
clearable
/>
<p class="field-control__note">最多 64 个字符;保存后对所有在线用户立即生效。</p>
</div>
</el-form-item>
<el-form-item label="logo">
<div class="field-control">
<el-upload
class="logo-uploader"
:show-file-list="false"
:http-request="handleUpload"
:before-upload="beforeUpload"
accept="image/png,image/jpeg"
>
<div v-loading="uploading" class="logo-box" title="点击上传 png / jpg(≤2MB)">
<img v-if="logoUrl" :src="logoUrl" alt="公司 logo" />
<span v-else class="logo-plus">+</span>
</div>
</el-upload>
<p class="field-control__note">
png / jpg,不超过 2MB;仅展示于登录页品牌位,按 ≤56×36 显示。
</p>
</div>
</el-form-item>
</el-form>
</div>
<!--
保存按钮移进 card-foot:表单项里的按钮会跟着 label 列缩进、并对齐到输入框左缘,
作为整张表单的收尾动作用卡片底栏右对齐更符合阅读顺序。
按钮在 form 之外仍安全:handleSubmit 走 formRef 校验,且 el-button 默认 type="button",
不会触发浏览器原生提交。
-->
<div class="card-foot">
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
</div>
</div>
<div class="card-body">
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="88px"
@submit.prevent
>
<el-form-item label="公司名称" prop="company_name">
<el-input
v-model="form.company_name"
maxlength="64"
placeholder="展示在登录页与导出页眉"
clearable
/>
</el-form-item>
<el-form-item label="logo">
<div class="logo-field">
<el-upload
class="logo-uploader"
:show-file-list="false"
:http-request="handleUpload"
:before-upload="beforeUpload"
accept="image/png,image/jpeg"
>
<div v-loading="uploading" class="logo-box" title="点击上传 png / jpg(≤2MB)">
<img v-if="logoUrl" :src="logoUrl" alt="公司 logo" />
<span v-else class="logo-plus">+</span>
</div>
</el-upload>
<p class="logo-hint">png / jpg,不超过 2MB;<br />展示于登录页品牌位与导出页眉。</p>
<div class="card">
<div class="card-head">
<span class="card-title">生效范围</span>
<span class="preview-note">保存后对所有人生效</span>
</div>
<div class="card-body">
<dl class="scope-list">
<div class="scope-item">
<dt>公司名称</dt>
<dd>登录页品牌位与版权行 · Excel 导出页眉 · 打印稿页眉</dd>
</div>
</el-form-item>
</el-form>
</div>
<!--
保存按钮移进 card-foot:表单项里的按钮会跟着 label 列缩进、并对齐到输入框左缘,
作为整张表单的收尾动作用卡片底栏右对齐更符合阅读顺序。
按钮在 form 之外仍安全:handleSubmit 走 formRef 校验,且 el-button 默认 type="button",
不会触发浏览器原生提交。
-->
<div class="card-foot">
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
<div class="scope-item">
<dt>logo</dt>
<dd>登录页品牌位(≤56×36)</dd>
</div>
</dl>
</div>
</div>
</div>
<!-- 签名元素:登录页实景预览(缩比登录卡,品牌名/logo 即时反映) -->
<!-- 签名元素:登录页实景预览(1:1 等比登录卡,非 iframe 截图) -->
<div class="card preview-card">
<div class="card-head">
<span class="card-title">登录页预览</span>
<span class="preview-note">保存后对所有人生效</span>
<span class="preview-note">1:1 等比</span>
</div>
<div class="card-body">
<div class="login-mock">
<div class="mock-brand">
<img v-if="logoUrl" :src="logoUrl" alt="" class="mock-logo" />
<div class="mock-name">{{ previewName }}</div>
<p class="mock-slogan">把每一笔回款,算成一份踏实。</p>
<span class="mock-waterline"></span>
<div class="login-stage" aria-hidden="true">
<div class="mock-card">
<div class="mock-brand">
<img v-if="logoUrl" :src="logoUrl" alt="" class="mock-logo" />
<div class="mock-name">{{ previewName }}</div>
<p class="mock-slogan">把每一笔回款,算成一份踏实。</p>
<span class="mock-waterline"></span>
</div>
<div class="mock-head">
<div class="mock-title">欢迎回来</div>
<div class="mock-hint">请输入账号密码进入系统</div>
</div>
<div class="mock-field">
<span class="mock-label">用户名</span>
<span class="mock-box"></span>
</div>
<div class="mock-field">
<span class="mock-label">密码</span>
<span class="mock-box"></span>
</div>
<div class="mock-field">
<span class="mock-label">验证码</span>
<span class="mock-captcha-row">
<span class="mock-box"></span>
<span class="mock-captcha"></span>
</span>
</div>
<div class="mock-submit">登 录</div>
</div>
<div class="mock-field"></div>
<div class="mock-field"></div>
<div class="mock-submit">登 录</div>
<div class="mock-foot">© {{ previewName }} · 内部系统</div>
</div>
<p class="preview-caption">登录页品牌位实景(示意,非交互)</p>
<p class="preview-caption">登录页品牌位实景(示意,非交互)——显示的是当前编辑值</p>
</div>
</div>
</div>
@ -181,11 +236,18 @@ const previewName = computed(() => form.company_name.trim() || '衍水集团')
<style scoped>
.settings-grid {
display: grid;
grid-template-columns: minmax(360px, 460px) minmax(300px, 380px);
grid-template-columns: minmax(0, 1fr) 480px;
gap: 16px;
align-items: start;
}
.settings-col {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 0;
}
/*
* 说明文字不再用 float:right 顶到卡片头右侧 —— .card-head 已是 flex +
* space-between,第二个子元素自然落在右端,float 反而会破坏行内布局。
@ -195,10 +257,37 @@ const previewName = computed(() => form.company_name.trim() || '衍水集团')
color: var(--ys-ink-3);
}
.logo-field {
/*
* 控件行:主控件在左、说明在右,两者共享一行高度。
* 说明用 flex-grow 撑满剩余宽度并右对齐 —— 宽屏下整行铺到卡片右缘,
* 不会出现「输入框缩在左上角、右侧留一整块空白」的观感;
* 窄屏放不下时自动换行,说明退回左对齐。
*/
.field-control {
display: flex;
align-items: flex-start;
gap: 14px;
flex-wrap: wrap;
align-items: center;
gap: 8px 20px;
width: 100%;
}
.field-control__main {
flex: 0 1 420px;
min-width: 240px;
}
.field-control__note {
flex: 1 1 240px;
min-width: 240px;
margin: 0;
font-size: 12px;
line-height: 1.7;
color: var(--ys-ink-3);
text-align: left;
}
.logo-uploader {
flex: 0 0 auto;
}
.logo-box {
@ -232,22 +321,76 @@ const previewName = computed(() => form.company_name.trim() || '衍水集团')
color: var(--ys-ink-3);
}
.logo-hint {
margin: 4px 0 0;
font-size: 12px;
line-height: 1.7;
/* ── 生效范围:定义列表(项名 / 取值) ── */
.scope-list {
margin: 0;
}
.scope-item {
display: grid;
grid-template-columns: 92px minmax(0, 1fr);
column-gap: 20px;
align-items: baseline;
}
.scope-item + .scope-item {
padding-top: 10px;
margin-top: 10px;
border-top: 1px solid var(--ys-line-soft);
}
.scope-item dt {
font-size: 13px;
font-weight: 500;
color: var(--ys-ink-2);
}
.scope-item dd {
margin: 0;
font-size: 12.5px;
color: var(--ys-ink-3);
}
/* ── 登录页实景预览:按登录卡视觉 0.8 缩比重建(非 iframe 截图) ── */
.login-mock {
padding: 22px 26px 24px;
/* ── 登录页实景预览:按登录页真实版式 1:1 复刻 ── */
.login-stage {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 0 20px;
overflow: hidden;
background-color: #f2f4f8;
/* 点阵网格:与登录页同一套秩序感 */
background-image: radial-gradient(rgba(28, 35, 51, 0.07) 1px, transparent 1px);
background-size: 24px 24px;
border-radius: var(--ys-radius-sm);
user-select: none;
}
/* 顶部天光:登录页的落点光源,等比搬过来 */
.login-stage::before {
content: '';
position: absolute;
top: -180px;
left: 50%;
width: 860px;
height: 460px;
transform: translateX(-50%);
pointer-events: none;
background: radial-gradient(closest-side, rgba(46, 75, 168, 0.08), rgba(46, 75, 168, 0));
}
.mock-card {
position: relative;
width: 400px;
max-width: 100%;
padding: 30px 36px 32px;
background: linear-gradient(180deg, #ffffff, #fcfcfe);
border: 1px solid var(--ys-line);
border-radius: 12px;
border: 1px solid rgba(28, 35, 51, 0.09);
border-radius: 14px;
box-shadow:
0 2px 4px rgba(28, 35, 51, 0.04),
0 10px 26px rgba(28, 35, 51, 0.08);
0 16px 40px rgba(28, 35, 51, 0.1);
}
.mock-brand {
@ -256,62 +399,136 @@ const previewName = computed(() => form.company_name.trim() || '衍水集团')
.mock-logo {
display: block;
max-width: 64px;
max-height: 40px;
margin: 0 auto 8px;
max-width: 56px;
max-height: 36px;
margin: 0 auto 10px;
object-fit: contain;
}
.mock-name {
font-family: var(--ys-font-brand);
font-size: 14px;
font-size: 17px;
font-weight: 500;
letter-spacing: 0.34em;
color: var(--ys-ink);
}
.mock-slogan {
margin: 6px 0 0;
font-size: 11px;
margin: 8px 0 0;
font-size: 12.5px;
color: var(--ys-ink-3);
}
.mock-waterline {
display: block;
width: 34px;
width: 40px;
height: 2px;
margin: 10px auto 0;
margin: 14px auto 0;
border-radius: 1px;
background: linear-gradient(90deg, var(--ys-indigo), rgba(46, 75, 168, 0));
}
.mock-head {
margin-top: 26px;
}
.mock-title {
font-size: 15px;
font-weight: 600;
color: var(--ys-ink);
}
.mock-hint {
margin-top: 4px;
font-size: 12px;
color: var(--ys-ink-3);
}
.mock-field {
height: 32px;
margin-top: 12px;
margin-top: 16px;
}
.mock-field + .mock-field {
margin-top: 18px;
}
.mock-label {
display: block;
margin-bottom: 6px;
font-size: 13px;
line-height: 22px;
color: var(--ys-ink-2);
}
.mock-box {
display: block;
height: 38px;
background: #f4f5f8;
border: 1px solid rgba(28, 35, 51, 0.07);
border-radius: 6px;
border-radius: 7px;
}
.mock-captcha-row {
display: flex;
gap: 10px;
}
.mock-captcha-row .mock-box {
flex: 1;
}
/* 验证码位:留空壳 + 斜纹质感,不伪造字符,避免与真实验证码混淆 */
.mock-captcha {
flex: 0 0 104px;
height: 38px;
background-color: #f1f3f8;
background-image: repeating-linear-gradient(
45deg,
rgba(28, 35, 51, 0.07) 0 1px,
transparent 1px 7px
);
border: 1px solid var(--ys-line);
border-radius: 7px;
}
.mock-submit {
height: 32px;
margin-top: 14px;
font-size: 12px;
line-height: 32px;
height: 38px;
margin-top: 20px;
font-size: 13px;
line-height: 38px;
letter-spacing: 0.4em;
text-align: center;
color: #fff;
background: var(--ys-indigo);
border-radius: 6px;
background: linear-gradient(180deg, var(--ys-indigo-hi), var(--ys-indigo));
border-radius: 7px;
}
.mock-foot {
margin-top: 16px;
font-size: 11px;
letter-spacing: 0.08em;
color: var(--ys-ink-3);
}
.preview-caption {
margin: 10px 0 0;
margin: 12px 0 0;
font-size: 11px;
text-align: center;
color: var(--ys-ink-3);
}
@media (max-width: 900px) {
/*
* 宽屏(≥1600px)说明文字靠右缘对齐,与左侧输入框形成「两端锚点」,
* 整行铺满卡片而不是缩在左上角;1600px 以下说明跟着输入框走,
* 放不下时整体换行到控件下方。
*/
@media (min-width: 1600px) {
.field-control__note {
text-align: right;
}
}
@media (max-width: 1200px) {
.settings-grid {
grid-template-columns: 1fr;
}

View File

@ -342,7 +342,7 @@ onMounted(() => {
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<el-table-column label="状态" width="96" align="center">
<template #default="{ row }">
<!-- 状态统一交给 StatusTag(内部已是 ys-pill),页面不再自造标签形态 -->
<StatusTag :value="row.status" />