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": {
"tdesign-mobile-vue": "^1.13.2",
"timi-web": "link:E:/WebProject/timi-web",
"vue": "3.5.26",
"vue-router": "4.5.0"
"timi-web": "link:E:/WebProject/timi-web"
},
"peerDependencies": {
"vue": "^3.5.0",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@types/node": "^22.13.5",
@@ -63,7 +65,9 @@
"vite-plugin-dts": "^4.5.4",
"vite-plugin-vue-setup-extend": "^0.4.0",
"@vue/tsconfig": "^0.7.0",
"vue": "3.5.26",
"vue-router": "4.5.0",
"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:
specifier: link:E:/WebProject/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:
'@types/node':
specifier: ^22.13.5
@@ -90,9 +84,15 @@ importers:
vite-plugin-vue-setup-extend:
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))
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:
specifier: ^2.2.10
version: 2.2.10(typescript@5.8.3)
specifier: ^2.2.12
version: 2.2.12(typescript@5.8.3)
packages:
@@ -1309,12 +1309,21 @@ packages:
'@volar/language-core@2.4.12':
resolution: {integrity: sha512-RLrFdXEaQBWfSnYGVxvR2WrO6Bub0unkdHYIdC31HzIEqATIuuhRRzYu76iGPZ6OtA4Au1SnW0ZwIqPP217YhA==}
'@volar/language-core@2.4.15':
resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==}
'@volar/source-map@2.4.12':
resolution: {integrity: sha512-bUFIKvn2U0AWojOaqf63ER0N/iHIBYZPpNGogfLPQ68F5Eet6FnLlyho7BS0y2HJ1jFhSif7AcuTx1TqsCzRzw==}
'@volar/source-map@2.4.15':
resolution: {integrity: sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==}
'@volar/typescript@2.4.12':
resolution: {integrity: sha512-HJB73OTJDgPc80K30wxi3if4fSsZZAOScbj2fcicMuOPoOkcf9NNAINb33o+DzhBdF9xTKC1gnPmIRDous5S0g==}
'@volar/typescript@2.4.15':
resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==}
'@vue/babel-helper-vue-jsx-merge-props@1.4.0':
resolution: {integrity: sha512-JkqXfCkUDp4PIlFdDQ0TdXoIejMtTHP67/pvxlgeY+u5k3LEdKuWZ3LK6xkxo52uDoABIVyRwqVkfLQJhk7VBA==}
@@ -1512,8 +1521,8 @@ packages:
typescript:
optional: true
'@vue/language-core@2.2.10':
resolution: {integrity: sha512-+yNoYx6XIKuAO8Mqh1vGytu8jkFEOH5C8iOv3i8Z/65A7x9iAOXA97Q+PqZ3nlm2lxf5rOJuIGI/wDtx/riNYw==}
'@vue/language-core@2.2.12':
resolution: {integrity: sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
@@ -4772,8 +4781,8 @@ packages:
vue-template-es2015-compiler@1.9.1:
resolution: {integrity: sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==}
vue-tsc@2.2.10:
resolution: {integrity: sha512-jWZ1xSaNbabEV3whpIDMbjVSVawjAyW+x1n3JeGQo7S0uv2n9F/JMgWW90tGWNFRKya4YwKMZgCtr0vRAM7DeQ==}
vue-tsc@2.2.12:
resolution: {integrity: sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==}
hasBin: true
peerDependencies:
typescript: '>=5.0.0'
@@ -6272,14 +6281,26 @@ snapshots:
dependencies:
'@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.15': {}
'@volar/typescript@2.4.12':
dependencies:
'@volar/language-core': 2.4.12
path-browserify: 1.0.1
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-transform-on@1.3.0': {}
@@ -6779,9 +6800,9 @@ snapshots:
optionalDependencies:
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:
'@volar/language-core': 2.4.12
'@volar/language-core': 2.4.15
'@vue/compiler-dom': 3.5.41
'@vue/compiler-vue2': 2.7.16
'@vue/shared': 3.5.41
@@ -10075,10 +10096,10 @@ snapshots:
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:
'@volar/typescript': 2.4.12
'@vue/language-core': 2.2.10(typescript@5.8.3)
'@volar/typescript': 2.4.15
'@vue/language-core': 2.2.12(typescript@5.8.3)
typescript: 5.8.3
vue@3.5.26(typescript@5.8.3):
@@ -12,9 +12,3 @@
opacity: 1;
transform: translate3d(0, 0, 0);
}
@media (prefers-reduced-motion: reduce) {
.entrance-fade-up-enter-active {
transition: none;
}
}
+75 -9
View File
@@ -1,18 +1,31 @@
import type { ObjectDirective } from "vue";
const ENTRANCE_DURATION = 400;
const ENTRANCE_DELAY_STEP = 70;
const ACTIVE_CLASS = "entrance-fade-up-enter-active";
const FROM_CLASS = "entrance-fade-up-enter-from";
const TO_CLASS = "entrance-fade-up-enter-to";
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 {
frameId?: number;
timerId?: number;
delay: number;
step: number;
auto: boolean;
}
type EntranceTransitionElement = HTMLElement & {
@@ -29,12 +42,36 @@ function setDelay(element: EntranceTransitionElement, delay: number): void {
element.style.setProperty("--entrance-delay", `${delay}ms`);
}
function cleanup(element: EntranceTransitionElement): void {
const state = element[STATE_KEY];
if (!state) {
return;
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];
if (state) {
if (state.frameId !== undefined) {
window.cancelAnimationFrame(state.frameId);
}
@@ -47,6 +84,11 @@ function cleanup(element: EntranceTransitionElement): void {
delete element[STATE_KEY];
}
if (isUnmounting) {
AUTO_ELEMENTS.delete(element);
}
}
/** 为元素添加一次性的渐入上移动画。 */
export const VEntranceTransition: ObjectDirective<HTMLElement, EntranceTransitionValue> = {
beforeMount(element, binding) {
@@ -55,8 +97,15 @@ export const VEntranceTransition: ObjectDirective<HTMLElement, EntranceTransitio
}
const target = element as EntranceTransitionElement;
const delay = getDelay(binding.value);
target[STATE_KEY] = { delay };
const auto = isAuto(binding.value);
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);
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 = undefined;
if (state.auto) {
state.delay = getAutoDelay(target, state.step);
setDelay(target, state.delay);
}
target.classList.remove(FROM_CLASS);
target.classList.add(TO_CLASS);
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 auto = isAuto(binding.value);
if (auto) {
AUTO_ELEMENTS.add(target);
} else {
AUTO_ELEMENTS.delete(target);
}
const state = target[STATE_KEY];
if (!state) {
return;
}
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);
},
beforeUnmount(element) {
if (element instanceof HTMLElement) {
cleanup(element as EntranceTransitionElement);
cleanup(element as EntranceTransitionElement, true);
}
}
};