From 27764460c97a1ce41d63771d7051da8456d70593 Mon Sep 17 00:00:00 2001 From: sakurayinfei <970412446@qq.com> Date: Fri, 31 Oct 2025 11:24:49 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix(is):=20isArrayEqual=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=8C=89=E9=A1=BA=E5=BA=8F=E5=88=A4=E6=96=AD=E4=BB=A5=E5=8F=8A?= =?UTF-8?q?=E4=B8=8D=E6=8C=89=E9=A1=BA=E5=BA=8F=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/opendesign/src/_utils/is.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/opendesign/src/_utils/is.ts b/packages/opendesign/src/_utils/is.ts index 305b6ca7e..bb94071d7 100644 --- a/packages/opendesign/src/_utils/is.ts +++ b/packages/opendesign/src/_utils/is.ts @@ -32,7 +32,7 @@ export function isEmptyArray(val: unknown): val is Array { return isArray(val) && val.length === 0; } -export function isArrayEqual(arr1: Array, arr2: Array): boolean { +export function isArrayEqual(arr1: Array, arr2: Array, order = false): boolean { if (!isArray(arr1) || !isArray(arr2)) { return false; } @@ -41,9 +41,18 @@ export function isArrayEqual(arr1: Array, arr2: Array): boolean { return false; } - for (let i = 0; i < len; i++) { - if (!arr2.includes(arr1[i])) { - return false; + if (order) { + for (let i = 0; i < len; i++) { + if (arr1[i] !== arr2[i]) { + return false; + } + } + } else { + const arr2Set = new Set(arr2); + for (let i = 0; i < len; i++) { + if (!arr2Set.has(arr1[i])) { + return false; + } } } -- Gitee From c83b72c261e21bbcf09f26e862c7dffb94ead4f5 Mon Sep 17 00:00:00 2001 From: sakurayinfei <970412446@qq.com> Date: Fri, 31 Oct 2025 11:33:41 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat(vue-utils):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E5=87=BD=E6=95=B0isTheTypeVNode=EF=BC=8Cflat?= =?UTF-8?q?ComponentVNode=EF=BC=8CrunOnceNextTick=EF=BC=8CuseSortedChildre?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/opendesign/src/_utils/vue-utils.ts | 153 +++++++++++++++++++- 1 file changed, 146 insertions(+), 7 deletions(-) diff --git a/packages/opendesign/src/_utils/vue-utils.ts b/packages/opendesign/src/_utils/vue-utils.ts index f1e33db44..0f082373d 100644 --- a/packages/opendesign/src/_utils/vue-utils.ts +++ b/packages/opendesign/src/_utils/vue-utils.ts @@ -1,5 +1,25 @@ -import { Component, onMounted, ref, Slots, Slot, VNode, VNodeTypes, Comment, ComponentPublicInstance, watchEffect, Ref, isRef, watch } from 'vue'; -import { isArray } from './is'; +import { + type Component, + onMounted, + ref, + Slots, + type Slot, + VNode, + VNodeTypes, + Comment, + ComponentPublicInstance, + Ref, + isRef, + watch, + isVNode, + type VNodeNormalizedChildren, + type ComponentInternalInstance, + shallowRef, + nextTick, + getCurrentInstance, + onBeforeUnmount, +} from 'vue'; +import { isArray, isArrayEqual, isString } from './is'; import { isHtmlElement } from './dom'; import { log } from './log.ts'; @@ -36,7 +56,7 @@ export const isTextElement = (vnode: VNode) => { * @param vnode vnode节点 * @param type 组件信息 */ -export function isComponent(vnode: VNode, _type?: VNodeTypes): _type is Component { +export function isComponent(vnode: VNode, _type?: VNodeTypes): _type is Component & { __name?: string; name?: string } { return Boolean(vnode && vnode.shapeFlag & ShapeFlags.COMPONENT); } /** @@ -170,9 +190,7 @@ const queryElement = (el: string | HTMLElement | null | undefined): HTMLElement } return null; }; -export const resolveHtmlElement = ( - elRef: Ref | ElementQuery -): Promise => { +export const resolveHtmlElement = (elRef: Ref | ElementQuery): Promise => { return new Promise((resolve) => { const resolveElement = (el: ElementQuery) => { if (isComponentPublicInstance(el)) { @@ -190,7 +208,9 @@ export const resolveHtmlElement = ( resolveElement(el); closeWatch(); } else { - log.warn(`resolveHtmlElement: elRef value is falsy, this might be a bug and could cause the promise to remain pending. Please check elRef.value: ${oldEl} -> ${el}`); + log.warn( + `resolveHtmlElement: elRef value is falsy, this might be a bug and could cause the promise to remain pending. Please check elRef.value: ${oldEl} -> ${el}` + ); } }); } @@ -229,3 +249,122 @@ export function filterSlots(slots: Slots, slotNames: { [key: string]: string }) const r = keys.filter((item) => names.includes(item)); return r || []; } + +/** 判断该 VNode 是否由该 type 创建的 */ +export const isTheTypeVNode = (vn: VNode, type: Component | string) => { + if (isComponent(vn, vn.type) && isString(type)) { + return (vn.type.name || vn.type.__name) === type; + } + return vn.type === type; +}; +/** 从虚拟节点树中获取指定组件生成的虚拟节点 */ +export const flatComponentVNode = (vn: VNodeNormalizedChildren | VNode[] | VNode, type: Component | string) => { + const res: VNode[] = []; + const _vn = isArray(vn) ? vn : [vn]; + + _vn.forEach((child) => { + if (isArray(child)) { + res.push(...flatComponentVNode(child, type)); + return; + } + if (!isVNode(child)) { + return; + } + if (isTheTypeVNode(child, type)) { + res.push(child); + } + if (child.component?.subTree) { + res.push(...flatComponentVNode(child.component.subTree, type)); + } else if (child.children) { + res.push(...flatComponentVNode(child.children, type)); + } + }); + + return res; +}; + +const tickJobs = new Set<() => any>(); +/** 对同一个函数只会执行一次 */ +export const runOnceNextTick = (fn: () => any) => { + if (tickJobs.size === 0) { + nextTick(() => { + tickJobs.forEach((item) => item()); + tickJobs.clear(); + }); + } + tickJobs.add(fn); +}; + +export type PublicChildT = { + uid: number; + getVNode: () => VNode; +} & T; +type ComponentInternalInstanceWithRender = ComponentInternalInstance & { + // render 函数可能来源于sfc模板编译, + // 也可能 `template` 属性的运行时编译, + // 还可能来源于 setup 返回的函数 + render: () => any; +}; + +/** + * 按模板顺序维护指定类型后代组件的排序 + * + * @description + * 在 Vue 中,当使用 Teleport 或动态注册组件时,子组件的渲染顺序可能与模板书写顺序不一致。 + * 该 Hook 通过监听 DOM 节点变化,确保子组件始终按模板中的书写顺序排列。 + * + * @param vm - 父组件实例 + * @param childType - 需要排序的后代组件类型 + * + * @note + * - 排序操作会在下一帧执行 + * - SSR 环境下无法工作(依赖响应式变量触发重渲染) + * - 适用于需要严格保持子组件顺序的场景 + */ +export const useSortedChildren = (vm: ComponentInternalInstance, childType: Component | string) => { + const children: Record = {}; + const sortedChildren = shallowRef([]); + const parentVms = new WeakSet(); + + const sortChildren = () => { + const newSortedChildren: T[] = []; + flatComponentVNode(vm.subTree, childType).forEach((child) => { + if (child.component?.uid && children[child.component.uid]) { + newSortedChildren.push(children[child.component.uid]); + } + }); + if (!isArrayEqual(sortedChildren.value, newSortedChildren, true)) { + sortedChildren.value = newSortedChildren; + } + }; + + const removeChild = (uid: number) => { + const child = children[uid]; + if (child) { + runOnceNextTick(sortChildren); + delete children[uid]; + } + }; + + const addChild = (child: T) => { + const childVm = getCurrentInstance()!; + const parentVm = childVm.parent as ComponentInternalInstanceWithRender; + if (!parentVms.has(parentVm)) { + const originRender = parentVm.render; + if (originRender) { + parentVm.render = function (...args) { + runOnceNextTick(sortChildren); + return originRender.apply(this, args); + }; + } + parentVms.add(parentVm); + } + onBeforeUnmount(() => { + removeChild(child.uid); + }); + children[child.uid] = child; + runOnceNextTick(sortChildren); + }; + + return { children: sortedChildren, childMap: children, addChild }; +}; -- Gitee From 94e7067179f9f9bcc99c788cd91807bc37d1660e Mon Sep 17 00:00:00 2001 From: sakurayinfei <970412446@qq.com> Date: Fri, 31 Oct 2025 14:30:57 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix(tab):=20=E4=BF=AE=E5=A4=8Dtabpane?= =?UTF-8?q?=E5=9C=A8=E5=8D=B8=E8=BD=BD=E5=92=8C=E6=8C=82=E8=BD=BD=E7=9A=84?= =?UTF-8?q?=E8=BF=87=E7=A8=8B=E4=B8=ADnav=E9=A1=BA=E5=BA=8F=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=9B=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=9F=90=E4=BA=9B=E5=9C=BA=E6=99=AF=E4=B8=8Banchor=E6=9C=AA?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BD=8D=E7=BD=AE=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/opendesign/src/tab/OTab.vue | 99 ++++++++++++------- packages/opendesign/src/tab/OTabNav.vue | 63 ++++++++++++ packages/opendesign/src/tab/OTabPane.vue | 86 +++++----------- .../opendesign/src/tab/__demo__/IndexTab.vue | 2 + .../src/tab/__demo__/TabSortedChild.vue | 69 +++++++++++++ packages/opendesign/src/tab/provide.ts | 20 ++-- 6 files changed, 238 insertions(+), 101 deletions(-) create mode 100644 packages/opendesign/src/tab/OTabNav.vue create mode 100644 packages/opendesign/src/tab/__demo__/TabSortedChild.vue diff --git a/packages/opendesign/src/tab/OTab.vue b/packages/opendesign/src/tab/OTab.vue index c19655b7b..b9018f9a4 100644 --- a/packages/opendesign/src/tab/OTab.vue +++ b/packages/opendesign/src/tab/OTab.vue @@ -1,18 +1,21 @@ + diff --git a/packages/opendesign/src/tab/OTabPane.vue b/packages/opendesign/src/tab/OTabPane.vue index 840c6dd46..a1e25ccc6 100644 --- a/packages/opendesign/src/tab/OTabPane.vue +++ b/packages/opendesign/src/tab/OTabPane.vue @@ -1,33 +1,25 @@ -