add step for entrance-transition

This commit is contained in:
Timi
2026-10-12 16:17:50 +08:00
parent b09011251a
commit 393bc8aa1b
4 changed files with 130 additions and 45 deletions
+8 -4
View File
@@ -36,9 +36,11 @@
}, },
"dependencies": { "dependencies": {
"tdesign-mobile-vue": "^1.13.2", "tdesign-mobile-vue": "^1.13.2",
"timi-web": "link:E:/WebProject/timi-web", "timi-web": "link:E:/WebProject/timi-web"
"vue": "3.5.26", },
"vue-router": "4.5.0" "peerDependencies": {
"vue": "^3.5.0",
"vue-router": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.13.5", "@types/node": "^22.13.5",
@@ -63,7 +65,9 @@
"vite-plugin-dts": "^4.5.4", "vite-plugin-dts": "^4.5.4",
"vite-plugin-vue-setup-extend": "^0.4.0", "vite-plugin-vue-setup-extend": "^0.4.0",
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.7.0",
"vue": "3.5.26",
"vue-router": "4.5.0",
"vite": "7.1.11", "vite": "7.1.11",
"vue-tsc": "^2.2.10" "vue-tsc": "^2.2.12"
} }
} }
+38 -17
View File
@@ -14,12 +14,6 @@ importers:
timi-web: timi-web:
specifier: link:E:/WebProject/timi-web specifier: link:E:/WebProject/timi-web
version: link:../timi-web version: link:../timi-web
vue:
specifier: 3.5.26
version: 3.5.26(typescript@5.8.3)
vue-router:
specifier: 4.5.0
version: 4.5.0(vue@3.5.26(typescript@5.8.3))
devDependencies: devDependencies:
'@types/node': '@types/node':
specifier: ^22.13.5 specifier: ^22.13.5
@@ -90,9 +84,15 @@ importers:
vite-plugin-vue-setup-extend: vite-plugin-vue-setup-extend:
specifier: ^0.4.0 specifier: ^0.4.0
version: 0.4.0(vite@7.1.11(@types/node@22.13.9)(less@4.3.0)(terser@5.46.0)) version: 0.4.0(vite@7.1.11(@types/node@22.13.9)(less@4.3.0)(terser@5.46.0))
vue:
specifier: 3.5.26
version: 3.5.26(typescript@5.8.3)
vue-router:
specifier: 4.5.0
version: 4.5.0(vue@3.5.26(typescript@5.8.3))
vue-tsc: vue-tsc:
specifier: ^2.2.10 specifier: ^2.2.12
version: 2.2.10(typescript@5.8.3) version: 2.2.12(typescript@5.8.3)
packages: packages:
@@ -1309,12 +1309,21 @@ packages:
'@volar/language-core@2.4.12': '@volar/language-core@2.4.12':
resolution: {integrity: sha512-RLrFdXEaQBWfSnYGVxvR2WrO6Bub0unkdHYIdC31HzIEqATIuuhRRzYu76iGPZ6OtA4Au1SnW0ZwIqPP217YhA==} resolution: {integrity: sha512-RLrFdXEaQBWfSnYGVxvR2WrO6Bub0unkdHYIdC31HzIEqATIuuhRRzYu76iGPZ6OtA4Au1SnW0ZwIqPP217YhA==}
'@volar/language-core@2.4.15':
resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==}
'@volar/source-map@2.4.12': '@volar/source-map@2.4.12':
resolution: {integrity: sha512-bUFIKvn2U0AWojOaqf63ER0N/iHIBYZPpNGogfLPQ68F5Eet6FnLlyho7BS0y2HJ1jFhSif7AcuTx1TqsCzRzw==} resolution: {integrity: sha512-bUFIKvn2U0AWojOaqf63ER0N/iHIBYZPpNGogfLPQ68F5Eet6FnLlyho7BS0y2HJ1jFhSif7AcuTx1TqsCzRzw==}
'@volar/source-map@2.4.15':
resolution: {integrity: sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==}
'@volar/typescript@2.4.12': '@volar/typescript@2.4.12':
resolution: {integrity: sha512-HJB73OTJDgPc80K30wxi3if4fSsZZAOScbj2fcicMuOPoOkcf9NNAINb33o+DzhBdF9xTKC1gnPmIRDous5S0g==} resolution: {integrity: sha512-HJB73OTJDgPc80K30wxi3if4fSsZZAOScbj2fcicMuOPoOkcf9NNAINb33o+DzhBdF9xTKC1gnPmIRDous5S0g==}
'@volar/typescript@2.4.15':
resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==}
'@vue/babel-helper-vue-jsx-merge-props@1.4.0': '@vue/babel-helper-vue-jsx-merge-props@1.4.0':
resolution: {integrity: sha512-JkqXfCkUDp4PIlFdDQ0TdXoIejMtTHP67/pvxlgeY+u5k3LEdKuWZ3LK6xkxo52uDoABIVyRwqVkfLQJhk7VBA==} resolution: {integrity: sha512-JkqXfCkUDp4PIlFdDQ0TdXoIejMtTHP67/pvxlgeY+u5k3LEdKuWZ3LK6xkxo52uDoABIVyRwqVkfLQJhk7VBA==}
@@ -1512,8 +1521,8 @@ packages:
typescript: typescript:
optional: true optional: true
'@vue/language-core@2.2.10': '@vue/language-core@2.2.12':
resolution: {integrity: sha512-+yNoYx6XIKuAO8Mqh1vGytu8jkFEOH5C8iOv3i8Z/65A7x9iAOXA97Q+PqZ3nlm2lxf5rOJuIGI/wDtx/riNYw==} resolution: {integrity: sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==}
peerDependencies: peerDependencies:
typescript: '*' typescript: '*'
peerDependenciesMeta: peerDependenciesMeta:
@@ -4772,8 +4781,8 @@ packages:
vue-template-es2015-compiler@1.9.1: vue-template-es2015-compiler@1.9.1:
resolution: {integrity: sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==} resolution: {integrity: sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==}
vue-tsc@2.2.10: vue-tsc@2.2.12:
resolution: {integrity: sha512-jWZ1xSaNbabEV3whpIDMbjVSVawjAyW+x1n3JeGQo7S0uv2n9F/JMgWW90tGWNFRKya4YwKMZgCtr0vRAM7DeQ==} resolution: {integrity: sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
typescript: '>=5.0.0' typescript: '>=5.0.0'
@@ -6272,14 +6281,26 @@ snapshots:
dependencies: dependencies:
'@volar/source-map': 2.4.12 '@volar/source-map': 2.4.12
'@volar/language-core@2.4.15':
dependencies:
'@volar/source-map': 2.4.15
'@volar/source-map@2.4.12': {} '@volar/source-map@2.4.12': {}
'@volar/source-map@2.4.15': {}
'@volar/typescript@2.4.12': '@volar/typescript@2.4.12':
dependencies: dependencies:
'@volar/language-core': 2.4.12 '@volar/language-core': 2.4.12
path-browserify: 1.0.1 path-browserify: 1.0.1
vscode-uri: 3.1.0 vscode-uri: 3.1.0
'@volar/typescript@2.4.15':
dependencies:
'@volar/language-core': 2.4.15
path-browserify: 1.0.1
vscode-uri: 3.1.0
'@vue/babel-helper-vue-jsx-merge-props@1.4.0': {} '@vue/babel-helper-vue-jsx-merge-props@1.4.0': {}
'@vue/babel-helper-vue-transform-on@1.3.0': {} '@vue/babel-helper-vue-transform-on@1.3.0': {}
@@ -6779,9 +6800,9 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 5.8.3 typescript: 5.8.3
'@vue/language-core@2.2.10(typescript@5.8.3)': '@vue/language-core@2.2.12(typescript@5.8.3)':
dependencies: dependencies:
'@volar/language-core': 2.4.12 '@volar/language-core': 2.4.15
'@vue/compiler-dom': 3.5.41 '@vue/compiler-dom': 3.5.41
'@vue/compiler-vue2': 2.7.16 '@vue/compiler-vue2': 2.7.16
'@vue/shared': 3.5.41 '@vue/shared': 3.5.41
@@ -10075,10 +10096,10 @@ snapshots:
vue-template-es2015-compiler@1.9.1: {} vue-template-es2015-compiler@1.9.1: {}
vue-tsc@2.2.10(typescript@5.8.3): vue-tsc@2.2.12(typescript@5.8.3):
dependencies: dependencies:
'@volar/typescript': 2.4.12 '@volar/typescript': 2.4.15
'@vue/language-core': 2.2.10(typescript@5.8.3) '@vue/language-core': 2.2.12(typescript@5.8.3)
typescript: 5.8.3 typescript: 5.8.3
vue@3.5.26(typescript@5.8.3): vue@3.5.26(typescript@5.8.3):
@@ -12,9 +12,3 @@
opacity: 1; opacity: 1;
transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0);
} }
@media (prefers-reduced-motion: reduce) {
.entrance-fade-up-enter-active {
transition: none;
}
}
+84 -18
View File
@@ -1,18 +1,31 @@
import type { ObjectDirective } from "vue"; import type { ObjectDirective } from "vue";
const ENTRANCE_DURATION = 400; const ENTRANCE_DURATION = 400;
const ENTRANCE_DELAY_STEP = 70;
const ACTIVE_CLASS = "entrance-fade-up-enter-active"; const ACTIVE_CLASS = "entrance-fade-up-enter-active";
const FROM_CLASS = "entrance-fade-up-enter-from"; const FROM_CLASS = "entrance-fade-up-enter-from";
const TO_CLASS = "entrance-fade-up-enter-to"; const TO_CLASS = "entrance-fade-up-enter-to";
const STATE_KEY = Symbol("entrance-transition-state"); const STATE_KEY = Symbol("entrance-transition-state");
const AUTO_ELEMENTS = new WeakSet<HTMLElement>();
/** 入场动画指令支持的延迟值。 */ /** 入场动画指令支持的延迟配置;省略时按同级元素顺序自动错开。 */
export type EntranceTransitionValue = number | string | { delay?: number | string } | undefined; export type EntranceTransitionValue =
| number
| string
| {
/** 固定延迟;设置 step 时启用自动步进,step 优先。 */
delay?: number | string;
/** 自动步进间隔,单位为毫秒。 */
step?: number | string;
}
| undefined;
interface EntranceTransitionState { interface EntranceTransitionState {
frameId?: number; frameId?: number;
timerId?: number; timerId?: number;
delay: number; delay: number;
step: number;
auto: boolean;
} }
type EntranceTransitionElement = HTMLElement & { type EntranceTransitionElement = HTMLElement & {
@@ -29,22 +42,51 @@ function setDelay(element: EntranceTransitionElement, delay: number): void {
element.style.setProperty("--entrance-delay", `${delay}ms`); element.style.setProperty("--entrance-delay", `${delay}ms`);
} }
function cleanup(element: EntranceTransitionElement): void { function isAuto(value: EntranceTransitionValue): boolean {
return (
value === undefined ||
(typeof value === "object" && value !== null && value.step !== undefined)
);
}
function getAutoStep(value: EntranceTransitionValue): number {
const rawStep =
typeof value === "object" && value !== null
? Number(value.step ?? ENTRANCE_DELAY_STEP)
: ENTRANCE_DELAY_STEP;
return Number.isFinite(rawStep) ? Math.max(0, rawStep) : ENTRANCE_DELAY_STEP;
}
function getAutoDelay(element: EntranceTransitionElement, step: number): number {
const parent = element.parentElement;
if (!parent) {
return 0;
}
const siblings = Array.from(parent.children).filter(
(sibling): sibling is HTMLElement => sibling instanceof HTMLElement && AUTO_ELEMENTS.has(sibling)
);
return Math.max(0, siblings.indexOf(element)) * step;
}
function cleanup(element: EntranceTransitionElement, isUnmounting = false): void {
const state = element[STATE_KEY]; const state = element[STATE_KEY];
if (!state) { if (state) {
return; if (state.frameId !== undefined) {
window.cancelAnimationFrame(state.frameId);
}
if (state.timerId !== undefined) {
window.clearTimeout(state.timerId);
}
element.classList.remove(ACTIVE_CLASS, FROM_CLASS, TO_CLASS);
element.style.removeProperty("--entrance-delay");
delete element[STATE_KEY];
} }
if (state.frameId !== undefined) { if (isUnmounting) {
window.cancelAnimationFrame(state.frameId); AUTO_ELEMENTS.delete(element);
} }
if (state.timerId !== undefined) {
window.clearTimeout(state.timerId);
}
element.classList.remove(ACTIVE_CLASS, FROM_CLASS, TO_CLASS);
element.style.removeProperty("--entrance-delay");
delete element[STATE_KEY];
} }
/** 为元素添加一次性的渐入上移动画。 */ /** 为元素添加一次性的渐入上移动画。 */
@@ -55,8 +97,15 @@ export const VEntranceTransition: ObjectDirective<HTMLElement, EntranceTransitio
} }
const target = element as EntranceTransitionElement; const target = element as EntranceTransitionElement;
const delay = getDelay(binding.value); const auto = isAuto(binding.value);
target[STATE_KEY] = { delay }; const step = auto ? getAutoStep(binding.value) : ENTRANCE_DELAY_STEP;
const delay = auto ? 0 : getDelay(binding.value);
target[STATE_KEY] = { delay, step, auto };
if (auto) {
AUTO_ELEMENTS.add(target);
} else {
AUTO_ELEMENTS.delete(target);
}
setDelay(target, delay); setDelay(target, delay);
target.classList.add(ACTIVE_CLASS, FROM_CLASS); target.classList.add(ACTIVE_CLASS, FROM_CLASS);
}, },
@@ -75,6 +124,10 @@ export const VEntranceTransition: ObjectDirective<HTMLElement, EntranceTransitio
state.frameId = window.requestAnimationFrame(() => { state.frameId = window.requestAnimationFrame(() => {
state.frameId = window.requestAnimationFrame(() => { state.frameId = window.requestAnimationFrame(() => {
state.frameId = undefined; state.frameId = undefined;
if (state.auto) {
state.delay = getAutoDelay(target, state.step);
setDelay(target, state.delay);
}
target.classList.remove(FROM_CLASS); target.classList.remove(FROM_CLASS);
target.classList.add(TO_CLASS); target.classList.add(TO_CLASS);
state.timerId = window.setTimeout(() => cleanup(target), ENTRANCE_DURATION + state.delay); state.timerId = window.setTimeout(() => cleanup(target), ENTRANCE_DURATION + state.delay);
@@ -88,18 +141,31 @@ export const VEntranceTransition: ObjectDirective<HTMLElement, EntranceTransitio
} }
const target = element as EntranceTransitionElement; const target = element as EntranceTransitionElement;
const auto = isAuto(binding.value);
if (auto) {
AUTO_ELEMENTS.add(target);
} else {
AUTO_ELEMENTS.delete(target);
}
const state = target[STATE_KEY]; const state = target[STATE_KEY];
if (!state) { if (!state) {
return; return;
} }
state.delay = getDelay(binding.value); state.auto = auto;
state.step = auto ? getAutoStep(binding.value) : ENTRANCE_DELAY_STEP;
if (state.auto) {
state.delay = getAutoDelay(target, state.step);
} else {
state.delay = getDelay(binding.value);
}
setDelay(target, state.delay); setDelay(target, state.delay);
}, },
beforeUnmount(element) { beforeUnmount(element) {
if (element instanceof HTMLElement) { if (element instanceof HTMLElement) {
cleanup(element as EntranceTransitionElement); cleanup(element as EntranceTransitionElement, true);
} }
} }
}; };