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:
LoveChengke committed 2026-10-06 15:32:01 +08:00
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>