feat(shared,web-admin,desktop-client,build): beUI 动效层与两端深色主题,全项目图标改用设计导出的新图标
动效层:把 beUI 的动效语言移植到两端,令牌放在 packages/shared/src/motion.ts(纯常量,后端也引用得到)。弹簧用数值积分生成 CSS linear(),纯 CSS 即可驱动,不必每组件一个 rAF 循环;两端各一份 composables/motion.ts 负责 DOM 部分,在 app.mount() 之前注入 :root。含 reduced-motion 全局降级与 hover 能力探测;灵动岛只移植单调曲线。
深色主题:Web 管理端补齐,机制与桌面客户端一致;语义色令牌只覆盖颜色,不动圆角与动效令牌。默认跟随系统,用户手动点过之后以用户为准。
图标:build/classhelper.{png,ico} 成为全项目唯一图形源,pnpm icons 从它派生各端图标与界面内品牌标。版本号统一到 1.1.2。
This commit is contained in:
1 parent
82c85b59c1
commit
ce9bed73f4
59 files changed
+2974
-336
No files matched your search
@@ -316,7 +316,7 @@ function parityLabel(item: ClassPlanEntryDto): string {
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-top: 10px;
|
||||
color: #606266;
|
||||
color: var(--ch-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.mb-8 {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { useCountUp } from '@/composables/motion';
|
||||
|
||||
/**
|
||||
* 统计卡片。抽成组件而不是写在 DashboardView 的 v-for 里,是因为数字滚动要用到
|
||||
* `useCountUp()` —— 它必须在 setup 阶段调用(内部有 `watch` 与 `onUnmounted`),
|
||||
* 而 v-for 里没法给每一项单独跑一遍 setup。
|
||||
*
|
||||
* 外观类名走全局样式(`styles/index.css` 的 `.stat-card` / `.stat-value`…),
|
||||
* 与仪表盘原先的写法完全一致,这里只负责"数字滚起来"。
|
||||
*/
|
||||
const props = defineProps<{
|
||||
label: string;
|
||||
value: number;
|
||||
hint: string;
|
||||
}>();
|
||||
|
||||
/**
|
||||
* 数字滚动(beUI 的 number 原语)。0 → 实际值的那一跳就是它存在的意义:
|
||||
* 数据是异步来的,直接跳到终值会让人错过"这一栏有内容"这件事。
|
||||
*/
|
||||
const displayed = useCountUp(() => props.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">{{ label }}</div>
|
||||
<div class="stat-value">{{ Math.round(displayed) }}</div>
|
||||
<div class="stat-hint">{{ hint }}</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -388,7 +388,7 @@ async function submit(): Promise<void> {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.file-name {
|
||||
color: #606266;
|
||||
color: var(--ch-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.hidden-input {
|
||||
@@ -399,7 +399,7 @@ async function submit(): Promise<void> {
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
margin: 12px 0 4px;
|
||||
color: #606266;
|
||||
color: var(--ch-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.required-hint {
|
||||
@@ -409,7 +409,7 @@ async function submit(): Promise<void> {
|
||||
}
|
||||
.mode-hint {
|
||||
width: 100%;
|
||||
color: #909399;
|
||||
color: var(--ch-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.result-stats {
|
||||
|
||||
@@ -293,7 +293,7 @@ function fillSample(): void {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 10px;
|
||||
color: #606266;
|
||||
color: var(--ch-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.mb-8 {
|
||||
|
||||
@@ -73,7 +73,13 @@ function handleConfirm(): void {
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="urgent-fade">
|
||||
<!--
|
||||
`:duration` 是必需的:这块遮罩是 `position: fixed; inset: 0`,铺满整屏、吃掉所有点击。
|
||||
靠 `animationend` 判断退场结束的话,窗口被遮挡时 Chromium 会冻结 CSS 动画、
|
||||
事件永不触发 ⇒ **遮罩永远不卸载,整个管理端再也点不动**。
|
||||
给的数值取「卡片弹簧的落定时间」与「遮罩淡出的时长」里较大的那个,保证不会被提前截断。
|
||||
-->
|
||||
<Transition name="urgent-fade" :duration="{ enter: 480, leave: 160 }">
|
||||
<div v-if="visible" class="urgent-mask" role="alertdialog" aria-modal="true">
|
||||
<div class="urgent-card">
|
||||
<div class="urgent-icon">
|
||||
@@ -116,7 +122,14 @@ function handleConfirm(): void {
|
||||
:show-text="false"
|
||||
color="#f56c6c"
|
||||
/>
|
||||
<span class="ring-text">{{ remaining }}</span>
|
||||
<!--
|
||||
倒计时数字按秒滚动(beUI 的 number 原语)。这里刻意**不用** `mode="out-in"`:
|
||||
出与进同时进行、共用一个绝对定位的中心,才读得出"数字往下滚";
|
||||
串行执行会变成"先空一拍、再出现",那是闪一下而不是滚动。
|
||||
-->
|
||||
<Transition name="ring-roll">
|
||||
<span class="ring-text" :key="remaining">{{ remaining }}</span>
|
||||
</Transition>
|
||||
</div>
|
||||
<span class="progress-hint">
|
||||
{{
|
||||
@@ -149,7 +162,7 @@ function handleConfirm(): void {
|
||||
max-width: calc(100vw - 48px);
|
||||
padding: 28px 30px 22px;
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
background: var(--ch-surface);
|
||||
border-top: 4px solid #f56c6c;
|
||||
box-shadow: 0 24px 60px rgba(120, 20, 20, 0.35);
|
||||
text-align: center;
|
||||
@@ -164,7 +177,7 @@ function handleConfirm(): void {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #f56c6c;
|
||||
background: #fef0f0;
|
||||
background: rgba(245, 108, 108, 0.1);
|
||||
animation: urgent-pulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@@ -183,14 +196,14 @@ function handleConfirm(): void {
|
||||
.urgent-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 22px;
|
||||
color: #303133;
|
||||
color: var(--ch-text);
|
||||
}
|
||||
|
||||
.urgent-subtitle {
|
||||
margin: 0 0 18px;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: #606266;
|
||||
color: var(--ch-text-secondary);
|
||||
}
|
||||
|
||||
.urgent-subtitle strong {
|
||||
@@ -210,17 +223,17 @@ function handleConfirm(): void {
|
||||
}
|
||||
|
||||
.urgent-period {
|
||||
background: #fef0f0;
|
||||
background: rgba(245, 108, 108, 0.1);
|
||||
}
|
||||
|
||||
.urgent-preview {
|
||||
background: #f5f7fa;
|
||||
background: var(--ch-surface-alt);
|
||||
}
|
||||
|
||||
.period-label,
|
||||
.preview-label {
|
||||
flex: 0 0 64px;
|
||||
color: #909399;
|
||||
color: var(--ch-text-muted);
|
||||
}
|
||||
|
||||
.period-value {
|
||||
@@ -229,7 +242,7 @@ function handleConfirm(): void {
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
color: #303133;
|
||||
color: var(--ch-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -266,17 +279,31 @@ function handleConfirm(): void {
|
||||
|
||||
.progress-hint {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
color: var(--ch-text-muted);
|
||||
}
|
||||
|
||||
/*
|
||||
* 进出的曲线统一走共享令牌(`packages/shared/src/motion.ts`)——
|
||||
* 原先这里是手写的 `cubic-bezier(0.34, 1.56, 0.64, 1)`,与全站其它浮层不是一套手感。
|
||||
* PANEL 弹簧同样是"落位时轻微过冲",但它和弹窗、抽屉用的是同一条曲线,
|
||||
* 连续操作时不会觉得这个警告框是"另一个世界的弹窗"。
|
||||
*/
|
||||
.urgent-fade-enter-active,
|
||||
.urgent-fade-leave-active {
|
||||
transition: opacity 0.22s ease;
|
||||
transition: opacity var(--ch-dur-base) var(--ch-ease-out);
|
||||
}
|
||||
|
||||
.urgent-fade-enter-active .urgent-card,
|
||||
.urgent-fade-enter-active .urgent-card {
|
||||
transition:
|
||||
transform var(--ch-spring-panel-dur) var(--ch-spring-panel),
|
||||
opacity var(--ch-dur-base) var(--ch-ease-out);
|
||||
}
|
||||
|
||||
/* 退出比入场快:用户已经决定关掉它了,再让他等一段"有分量"的动画只会显得卡 */
|
||||
.urgent-fade-leave-active .urgent-card {
|
||||
transition: transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
transition:
|
||||
transform var(--ch-dur-fast) var(--ch-ease-out),
|
||||
opacity var(--ch-dur-fast) var(--ch-ease-out);
|
||||
}
|
||||
|
||||
.urgent-fade-enter-from,
|
||||
@@ -284,8 +311,42 @@ function handleConfirm(): void {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.urgent-fade-enter-from .urgent-card,
|
||||
.urgent-fade-enter-from .urgent-card {
|
||||
transform: scale(0.94) translateY(10px);
|
||||
}
|
||||
|
||||
.urgent-fade-leave-to .urgent-card {
|
||||
transform: scale(0.92) translateY(10px);
|
||||
transform: scale(0.97) translateY(-6px);
|
||||
}
|
||||
|
||||
/* 倒计时数字的滚动:旧的向上淡出、新的从下方升上来,两者同时进行 */
|
||||
@keyframes ring-roll-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(0.4em);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ring-roll-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(-0.4em);
|
||||
}
|
||||
}
|
||||
|
||||
.ring-roll-enter-active {
|
||||
animation: ring-roll-in var(--ch-dur-fast) var(--ch-ease-out) both;
|
||||
}
|
||||
|
||||
.ring-roll-leave-active {
|
||||
animation: ring-roll-out var(--ch-dur-instant) var(--ch-ease-out) both;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user