feat(dist,deploy,website): 交付产物统一归集到 releases/<组件>/<版本>,补齐官网与 Linux 验收

三个打包脚本原先各写各的输出目录,现在一律直接落在仓库根 releases/<组件>/<版本>/,分层固定为安装包/免安装/构建中间;dist-win 用 --config.directories.output 覆盖 electron-builder 的目录后再整理,显式传入时不覆盖。插件侧顺手修掉一个既有 bug:原先 rmSync 整个组件目录,重打一次就把上一版的留档抹掉,现在只清当前版本目录。verify-packaged 的 --exe 默认值跟着改到新落点。

deploy/verify-linux.sh 修掉管道在 pipefail 下的 SIGPIPE 误判(命中被算成失败);CI workflow 的出包与上传路径同步;electron-builder.yml 显式指定安装包图标。

website/ 是产品官网(纯静态、无构建步骤,不进 pnpm workspace),含 beUI 动效 token 与实机留档图;客户端冒烟相应加了采集留档图用的外观开关。
This commit is contained in:
LoveChengke committed 2026-10-06 15:31:38 +08:00
1 parent 7b1ae29774
commit 82c85b59c1
40 files changed
+4976 -76

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

File diff suppressed because it is too large. Load diff
+303
View File
@@ -0,0 +1,303 @@
/**
* 动效层 —— 每一条曲线、每一个弹簧参数都取自 beUI(beui.dev)的源码:
*
* lib/ease.ts 共享 token(EASE_OUT / SPRING_PRESS / …)
* components/motion/dynamic-island 壳 spring 用 Apple 的 duration+bounce 形式
* components/motion/text-animation TextReveal:逐字上浮 + 去模糊
* components/motion/number NumberTicker:数字竖排滚轮
* components/motion/shared-layout-bg 悬停胶囊在条目之间滑移
* components/motion/range-slider 位置用 SPRING_GLIDE,拖拽把手 scaleY 用 SPRING_BOUNCY
* components/motion/scroll-animation 阅读进度条,PROGRESS_SPRING 缓跟
* components/motion/button whileTap 0.93 / whileHover 1.02
*
* 这里只有引擎,没有页面逻辑;页面逻辑在 main.js 里。
*/
(() => {
'use strict';
/* ═══════════════════════════════ 曲线 ═══════════════════════════════════ */
/** 进场与退场:立刻响应,然后安静地落位 */
const EASE_OUT = 'cubic-bezier(0.16, 1, 0.3, 1)';
/** 已经在屏幕上的东西开始移动 */
const EASE_IN_OUT = 'cubic-bezier(0.77, 0, 0.175, 1)';
/** 抽屉 */
const EASE_DRAWER = 'cubic-bezier(0.32, 0.72, 0, 1)';
/* ═══════════════════════════════ 弹簧 ═══════════════════════════════════ */
/** 按钮等可按压表面:快、有分量 */
const SPRING_PRESS = { stiffness: 500, damping: 30, mass: 0.6 };
/** 控件内部文字 / 图标交换位置 */
const SPRING_SWAP = { stiffness: 460, damping: 30, mass: 0.55 };
/** 由指针唤出的浮层 */
const SPRING_PANEL = { stiffness: 420, damping: 40, mass: 0.5 };
/** 共享布局滑移:胶囊、指示器、面板换位置 */
const SPRING_LAYOUT = { stiffness: 360, damping: 32, mass: 0.6 };
/** 装饰性的鼠标跟随(磁吸、倾斜、Dock) */
const SPRING_MOUSE = { stiffness: 200, damping: 15, mass: 0.3 };
/** 拖拽把手与填充:临界阻尼,跟手且不会在两端回弹 */
const SPRING_GLIDE = { stiffness: 700, damping: 50, mass: 0.5 };
/** 范围滑块把手的抓取反馈(只做 scaleY) */
const SPRING_BOUNCY = { stiffness: 500, damping: 14, mass: 0.7 };
/** 阅读进度条:故意比 UI 弹簧松,让它跟着滚动慢慢追上来 */
const SPRING_PROGRESS = { stiffness: 120, damping: 30, mass: 0.6 };
/** 标题逐字上浮(TextReveal 的 DEFAULT_SPRING) */
const SPRING_TEXT = { stiffness: 140, damping: 26, mass: 1.2 };
/* ═════════════════════════ 时长口径(beUI motion guide) ═════════════════════════
按压反馈 100–160ms · tooltip 125–200 · 下拉 150–250 · 弹窗抽屉 200–500
界面动效默认 < 300ms;更长的动效只属于讲解型演示。 */
const DURATION = {
press: 0.14,
swap: 0.2,
reveal: 0.22,
islandShell: 0.8,
islandContent: 0.8,
islandExit: 0.08,
number: 0.9,
numberStagger: 0.04,
textStagger: 0.03,
};
/* ═══════════════════════════ duration + bounce ═══════════════════════════
motion 的 Apple 式弹簧写法:给一个「感知时长」和一个回弹量,反解出刚度。
bounce = 0 无回弹(临界阻尼),bounce = 1 最弹;阻尼比 ζ = 1 - bounce。
做法是拿单位阶跃响应去二分刚度,让它在 duration 秒内落到静止 —— 与
motion 的 springFromDuration 同一套语义(因此感知时长与位移距离无关)。 */
const REST_DELTA = 0.01;
const REST_SPEED = 0.01;
const SOLVE_STEP = 1 / 480;
/** 运行时积分用的固定子步,与求解器同口径 */
const INTERNAL_STEP_MS = 1000 / 480;
/** 单帧最多补多少个子步,防止切回标签页时一次算几千步 */
const MAX_SUBSTEPS = 96;
function settleTime(stiffness, damping, mass) {
let x = 0;
let velocity = 0;
for (let t = 0; t < 6; t += SOLVE_STEP) {
const acceleration = (-stiffness * (x - 1) - damping * velocity) / mass;
velocity += acceleration * SOLVE_STEP;
x += velocity * SOLVE_STEP;
if (Math.abs(x - 1) < REST_DELTA && Math.abs(velocity) < REST_SPEED) return t;
}
return 6;
}
const durationCache = new Map();
function springFromDuration(duration, bounce, mass = 1) {
const key = `${duration}|${bounce}|${mass}`;
const cached = durationCache.get(key);
if (cached) return cached;
const zeta = Math.max(0.05, Math.min(1, 1 - bounce));
// 刚度越大落位越快,settleTime 对它单调递减 —— 二分找出刚好在 duration 落位的那个
let low = 1;
let high = 4000;
for (let i = 0; i < 40; i += 1) {
const stiffness = (low + high) / 2;
const damping = 2 * zeta * Math.sqrt(stiffness * mass);
if (settleTime(stiffness, damping, mass) > duration) low = stiffness;
else high = stiffness;
}
const stiffness = (low + high) / 2;
const result = { stiffness, damping: 2 * zeta * Math.sqrt(stiffness * mass), mass };
durationCache.set(key, result);
return result;
}
/** 把 {stiffness,damping,mass} 或 {duration,bounce} 统一成系数 */
function resolveConfig(config) {
if (config.duration !== undefined) {
return springFromDuration(config.duration, config.bounce ?? 0, config.mass ?? 1);
}
return { stiffness: config.stiffness, damping: config.damping, mass: config.mass ?? 1 };
}
/* ═══════════════════════════ 共享帧循环 ═══════════════════════════
全站一个 rAF:所有弹簧都挂在这上面,谁在动就推进谁,都停了就整条循环歇着。
可见性每帧现读,不缓存 —— 缓存过的标记一旦翻不回来,动画就永久停住。 */
const running = new Set();
let frameId = 0;
let lastTime = 0;
const reduceQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
const reduceMotion = () => reduceQuery.matches;
function frame(time) {
const delta = lastTime ? Math.min(64, time - lastTime) : 16;
lastTime = time;
for (const spring of [...running]) {
if (!spring.step(delta)) running.delete(spring);
}
if (running.size > 0) {
frameId = requestAnimationFrame(frame);
} else {
frameId = 0;
lastTime = 0;
}
}
function wake() {
if (!frameId) frameId = requestAnimationFrame(frame);
}
/* ═══════════════════════════════ 弹簧 ═══════════════════════════════ */
class Spring {
/**
* @param {number} initial 初值
* @param {object} config {stiffness,damping,mass} 或 {duration,bounce}
* @param {(value:number)=>void} onUpdate 每帧回调
* @param {{restRatio?:number, restFloor?:number}} [rest] 静止阈值
*/
constructor(initial, config, onUpdate, rest = {}) {
const resolved = resolveConfig(config);
this.stiffness = resolved.stiffness;
this.damping = resolved.damping;
this.mass = resolved.mass;
// duration+bounce 那套是按「本次位移的 1%」判定静止的,这里跟着同口径缩放 ——
// 用绝对阈值的话,同一组参数走 20px 和走 200px 的观感时长会不一样。
this.restRatio = rest.restRatio ?? REST_DELTA;
this.restFloor = rest.restFloor ?? 1e-4;
this.restDelta = this.restFloor;
this.restSpeed = this.restFloor;
this.value = initial;
this.target = initial;
this.velocity = 0;
this.onUpdate = onUpdate;
}
get settled() {
return this.value === this.target && this.velocity === 0;
}
/** 换目标。保留当前速度 —— 真实弹簧就是这样,中途改向不会顿一下。 */
set(target, options = {}) {
if (options.velocity !== undefined) this.velocity = options.velocity;
if (target === this.target && this.settled) return;
this.target = target;
if (reduceMotion() || options.instant) {
this.jump(target);
return;
}
const travel = Math.abs(target - this.value);
this.restDelta = Math.max(this.restFloor, travel * this.restRatio);
this.restSpeed = this.restDelta;
running.add(this);
wake();
}
/** 直接落位,不做动画 */
jump(target) {
this.target = target;
this.value = target;
this.velocity = 0;
running.delete(this);
this.onUpdate?.(this.value, this);
}
/** 把当前值刷一次到 DOM。构造之后、或 set() 因为目标没变而早退时,靠它把首帧对上。 */
paint() {
this.onUpdate?.(this.value, this);
}
step(deltaMs) {
// 定步长积分:显式欧拉在 60/120Hz 这种大步长下会明显耗散,同一组参数的
// 观感时长会随屏幕刷新率变(0.8s 的壳在 120Hz 下会 0.48s 就停)。
// 这里把它拆成 1/480s 的固定子步,几时落位只由参数决定,与刷新率无关。
const sub = INTERNAL_STEP_MS;
let remaining = Math.min(deltaMs, sub * MAX_SUBSTEPS);
let moving = false;
while (remaining > 0) {
const dt = Math.min(sub, remaining) / 1000;
remaining -= sub;
moving = this.#integrate(dt) || moving;
}
return moving;
}
#integrate(dt) {
const distance = this.target - this.value;
if (distance === 0 && Math.abs(this.velocity) < this.restSpeed) {
this.value = this.target;
this.velocity = 0;
this.onUpdate?.(this.value, this);
return false;
}
// 胡克定律 + 阻尼:a = (k·(target − value) − c·v) / m
const acceleration = (this.stiffness * distance - this.damping * this.velocity) / this.mass;
this.velocity += acceleration * dt;
this.value += this.velocity * dt;
if (Math.abs(this.target - this.value) < this.restDelta && Math.abs(this.velocity) < this.restSpeed) {
this.value = this.target;
this.velocity = 0;
this.onUpdate?.(this.value, this);
return false;
}
if (!Number.isFinite(this.value) || !Number.isFinite(this.velocity)) {
this.value = this.target;
this.velocity = 0;
this.onUpdate?.(this.value, this);
return false;
}
this.onUpdate?.(this.value, this);
return true;
}
}
/* ═══════════════════════════ 通用小工具 ═══════════════════════════ */
const round2 = (value) => Math.round(value * 100) / 100;
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
/** 指针能悬停吗 —— 触摸设备上不要挂 hover 态 */
const hoverCapable = () => window.matchMedia('(hover: hover) and (pointer: fine)').matches;
function setTransition(el, properties, duration, ease = EASE_OUT, delay = 0) {
el.style.transition = properties
.map((property) => `${property} ${duration}s ${ease} ${delay ? `${delay}s` : '0s'}`)
.join(', ');
}
/** 动画重定向:先落 initial,下一帧再说 —— 保证浏览器真的重绘一次 */
function nextFrame(callback) {
requestAnimationFrame(() => requestAnimationFrame(callback));
}
window.CHMotion = {
EASE_OUT,
EASE_IN_OUT,
EASE_DRAWER,
SPRING_PRESS,
SPRING_SWAP,
SPRING_PANEL,
SPRING_LAYOUT,
SPRING_MOUSE,
SPRING_GLIDE,
SPRING_BOUNCY,
SPRING_PROGRESS,
SPRING_TEXT,
DURATION,
Spring,
springFromDuration,
reduceMotion,
hoverCapable,
round2,
clamp,
setTransition,
nextFrame,
};
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

File diff suppressed because it is too large. Load diff