tree —— شجرة
الشجرة هي الحاوية العامة للبيانات الهرمية: أدلة الملفات، وهياكل المؤسسات، وأشجار الصلاحيات، والقوائم المتسلسلة. ووعورتها في إدارة حالة البنية العودية — فالتوسيع والتحديد والتحميل الكسول كلها عمليات على شجرة فرعية بأكملها، مع التعامل مع البيانات ذات المراجع الدائرية، والعُقد المعطَّلة، والحالة النصفية. وقد جمع core/tree هذه الخوارزميات بالدوال الصرفة + التحديث غير القابل للتغيير.
متى يُستخدم
Section titled “متى يُستخدم”- عندما يُطبّق كل طرف
LhTree: تصفُّح الأدلة، وتوزيع الصلاحيات، وعرض الهيكل التنظيمي، وتصفية التصنيفات. - البيانات الهرمية التي تحتاج إلى توسيع/طي، وتحديد (يشمل ربط الأب بالأبناء)، وتحميل كسول.
- لا يجب استخدامه: التصنيفات المسطَّحة بمستويين كحد أقصى (الاختيار المتسلسل أو المتحكم المجزَّأ أوفر جهدًا)؛ والقوائم الخطية (استخدم
virtual-list).
طريقة الاستيراد
Section titled “طريقة الاستيراد”import { isTreeNodeLeaf, flattenTree, getDescendantKeys, findTreeNode, getNodeCheckState, collectLeafKeys, toggleCheckKeys, findParentNode, moveTreeFocus, mergeLoadedChildren, isNodeLoadable, getTreeClassNames, getTreeNodeClassNames, getTreeAria, getTreeNodeAria,} from '@lh-ui/share/core/tree'import type { LhTreeNode, LhFlatTreeNode, LhTreeCheckState,} from '@lh-ui/share/core/tree'نية التصميم
Section titled “نية التصميم”التصيير هو التسطيح: الشجرة في الواجهة في جوهرها قائمة بمستويات إزاحة داخلية. يجتاز flattenTree أولًا بالعمق، ولا يُوسِّع سوى الأشجار الفرعية التي يصيبها expandedKeys، ويُخرِج مصفوفة مسطَّحة (يحمل كل عنصر level / parentKey / ancestorKeys).فيقوم طرف العرض بـ v-for على مصفوفة أحادية البعد — وهو أبسط بكثير من المكوّن العودي، ومتوافق بطبيعته مع التمرير الافتراضي.
الحكم بالتكرار عبر سلسلة الأجداد مقابل الحكم العام: تستخدم الدوال العودية في هذه الوحدة عمومًا seen بسلسلة الأجداد (ancestors / nextSeen تُمرَّر طبقةً تلو الأخرى) لا Set عامًّا. وثمن ذلك أن الحماية نافذة فقط داخل الاستدعاء الواحد، لكن فائدتها أن الأشجار الفرعية المشتركة قانونًا في DAG لا يُخطَأ تخطيها، ولا يوجد تسريب حالة عبر النسخ. ولاحظ أن هذا يختلف عن مفاضلة flattenLeafColumns في table (الذي يستخدم seen عامًّا) — فمشاركة DAG في الجدول يجب أن تُنبَثق مرة واحدة فقط (لمحاذاة عدد الأعمدة)، بينما مشاركة DAG في الشجرة يجب أن تُحسَب مرة لكل مسار مختلف.
العُقدة المعطَّلة لا تدخل مقام التحديد الكامل: تتخطى collectCheckableLeafKeys الأشجار الفرعية المعطَّلة — وإلا فبعد تحديد الأب سيبقى في حالة indeterminate دائمًا لأن الأبناء المعطَّلين ناقصون، ولن يصبح محددًا بالكامل أبدًا. وهذا نوع حقيقي من أخطاء التجربة.
العُقدة غير الورقية غير المُحمَّلة: عندما يكون isLeaf !== true ولا توجد children، تُرجِع collectCheckableLeafKeys مصفوفة فارغة، لكن toggleCheckKeys يكتب مفتاحها نفسه في checkedKeys — ففي سيناريو التحميل الكسول يمكن للمستخدم تحديد عُقدة لم تُحمَّل أبناؤها بعد، ثم يُترَك الأمر لطرف العرض بعد التحميل في أن يُسلسل أو لا.
moveTreeFocus مهمل @deprecated: التقت خوارزمية التنقل في core/shared/keyboard.getNextListIndex، ويحتفظ هذا الغلاف بالتوقيع القديم والدلالة القائمة «الرجوع إلى العنصر الأول عند بطلان القيمة الحالية».
جدول الدوال
Section titled “جدول الدوال”| الدالة | التوقيع | القيمة المُعادة | الدور |
|---|---|---|---|
isTreeNodeLeaf |
(node) => boolean |
هل هي ورقة | الأولوية لـ isLeaf، وإلا يُستنتَج من children |
flattenTree |
(nodes, expandedKeys, …) => LhFlatTreeNode[] |
قائمة مسطَّحة | لا يُوسِّع سوى الأشجار المصابة |
getDescendantKeys |
(node, seen?) => string[] |
مفاتيح الأحفاد | حكم سلسلة الأجداد |
findTreeNode |
(nodes, key, seen?) => LhTreeNode | null |
العُقدة | بحث بالعمق |
getNodeCheckState |
(node, checkedKeys, checkStrictly?) => LhTreeCheckState |
ثلاثي الحالات | الشجرة الفرعية المعطَّلة لا تُحتسَب في المقام |
collectLeafKeys |
(node, seen?) => string[] |
مفاتيح الأوراق القابلة للتحديد | يتخطى المعطَّل |
toggleCheckKeys |
(roots, checkedKeys, key, options?) => string[] |
المفاتيح المحدَّدة الجديدة | تسلسل + رجوع إلى الأجداد |
findParentNode |
(nodes, key, …) => LhTreeNode | null |
العُقدة الأب | تُرجع null في المستوى الأعلى |
moveTreeFocus |
(flat, currentKey, direction) => string | null |
المفتاح الهدف | @deprecated |
mergeLoadedChildren |
(nodes, loadedMap, seen?) => LhTreeNode[] |
شجرة جديدة | دمج غير قابل للتغيير في التحميل الكسول |
isNodeLoadable |
(node, lazy, loadedKeys) => boolean |
هل يُحمَّل | غير مُصرَّح بكونه ورقة وغير محمَّل |
getTreeClassNames |
(state: { showCheckbox }) => string[] |
أسماء الأصناف | وضع التحديد |
getTreeNodeClassNames |
(state) => string[] |
أسماء الأصناف | توسيع / تحديد / تعطيل / ورقة |
getTreeAria |
(id?) => LhAriaAttrs |
حِزَمة سمات aria | role=tree |
getTreeNodeAria |
(options) => LhAriaAttrs |
حِزَمة سمات aria | دلالات treeitem الكاملة |
تعريف الأنواع
Section titled “تعريف الأنواع”export interface LhTreeNode { key: string label: string children?: LhTreeNode[] disabled?: boolean isLeaf?: boolean // إن لم يُمرَّر يُستنتَج من children}
export interface LhFlatTreeNode { node: LhTreeNode key: string level: number // بدءًا من 0 parentKey: string | null ancestorKeys: string[] // سلسلة الأجداد (لا تشمل العُقدة نفسها) isLeaf: boolean}
export type LhTreeCheckState = 'checked' | 'indeterminate' | 'unchecked'إمكانية الوصول
Section titled “إمكانية الوصول”تُخرِج getTreeAria / getTreeNodeAria الدلالات الكاملة لنمط treeview في APG:
getTreeAria('lh-tree-1') // { role: 'tree', id: 'lh-tree-1' }
getTreeNodeAria({ level: 2, expanded: true, selected: true, checked: 'indeterminate' })// { role: 'treeitem', 'aria-level': 2, 'aria-expanded': true,// 'aria-selected': true, 'aria-checked': 'indeterminate' }
getTreeNodeAria({ level: 1, leaf: true })// { role: 'treeitem', 'aria-level': 1 } ← العُقدة الورقية لا تُخرِج aria-expanded- يبدأ
aria-levelمن 1 (لاحظ أنlevelفيflattenTreeيبدأ من 0، فعند تمريره إلى aria زِدْه 1). - العُقدة الورقية لا تُخرِج
aria-expanded— تجنبًا لقيمةaria-expanded="false"بلا معنى، وهذا ما تنص عليه APG صراحةً. - القيم الثلاث لـ
aria-checked:true/false/'indeterminate'(سلسلة نصية). - التفاعل بلوحة المفاتيح (treeview في APG): ↑/↓ للتنقل بين العُقد المرئية، و→ للتوسيع/الدخول إلى العُقدة الابنة، و← للطي/الرجوع إلى العُقدة الأب، وHome/End للوصول إلى الأولى والأخيرة، وEnter لتشغيل التحديد.
- مصدر
aria-levelبعد التسطيح: هوlevel + 1منflattenTree، فلا تستنتجه من عمق الإزاحة في DOM.
أمثلة الاستخدام
Section titled “أمثلة الاستخدام”import { flattenTree, toggleCheckKeys, getNodeCheckState } from '@lh-ui/share/core/tree'
const tree = [ { key: '1', label: 'قسم البحث والتطوير', children: [ { key: '1-1', label: 'فريق الواجهة الأمامية', children: [{ key: '1-1-1', label: 'lh-ui' }] }, { key: '1-2', label: 'فريق الواجهة الخلفية' }, ]}, { key: '2', label: 'قسم المنتج' },]
flattenTree(tree, ['1'])// [قسم البحث والتطوير(l0), فريق الواجهة الأمامية(l1), فريق الواجهة الخلفية(l1), قسم المنتج(l0)]// لاحظ غياب 1-1-1: لأن '1-1' ليست ضمن expandedKeys
flattenTree(tree, ['1', '1-1'])// [قسم البحث والتطوير, فريق الواجهة الأمامية, lh-ui(l2), فريق الواجهة الخلفية, قسم المنتج]
toggleCheckKeys(tree, [], '1')// ['1', '1-1', '1-1-1', '1-2'] (تحديد كل الأحفاد تسلسليًّا، مع العُقدة نفسها)
toggleCheckKeys(tree, ['1', '1-1', '1-1-1', '1-2'], '1-1-1')// ['1-2'] —— بعد إلغاء '1-1-1' يرجع التسلسل: '1-1' لم تعد محدَّدة بالكامل فتُحذَف،// و'1' لم تعد محدَّدة بالكامل أيضًا فتُحذَف (الرجوع يمتد إلى الجذر)
toggleCheckKeys(tree, ['1-1'], '1-2')// ['1-1', '1-2'] —— رجوع إلى الأجداد فقط، دون باقي الفروع
getNodeCheckState(tree[0], ['1', '1-1', '1-1-1', '1-2']) // 'checked' (جميع الأوراق القابلة للتحديد محدَّدة)getNodeCheckState(tree[0], ['1-1-1']) // 'indeterminate' (واحدة من أصل ورقتين قابلتين للتحديد)getNodeCheckState(tree[0], ['1-1']) // 'unchecked' ('1-1' ليست ورقة، فلا تُحتسَب)getNodeCheckState(tree[0], []) // 'unchecked'<!-- Vue3: تصيير مسطَّح وودود للتمرير الافتراضي --><div :class="getTreeClassNames({ showCheckbox })" v-bind="getTreeAria(treeId)"> <div v-for="item in flatNodes" :key="item.key" class="lh-tree-node-row" :style="{ paddingLeft: item.level * 18 + 'px' }" > <div :class="getTreeNodeClassNames({ expanded: expandedKeys.includes(item.key), selected: selectedKey === item.key, disabled: item.node.disabled, leaf: item.isLeaf, })" role="treeitem" :aria-level="item.level + 1" :aria-expanded="item.isLeaf ? undefined : expandedKeys.includes(item.key)" @click="onSelect(item)" > <span v-if="!item.isLeaf" class="lh-tree-node__arrow" @click.stop="onToggle(item)"> {{ expandedKeys.includes(item.key) ? '▾' : '▸' }} </span> <input v-if="showCheckbox" type="checkbox" :checked="checkState(item) === 'checked'" :indeterminate.prop="checkState(item) === 'indeterminate'" :disabled="item.node.disabled" @change="onCheck(item)" /> <span class="lh-tree-node__label">{{ item.node.label }}</span> </div> </div></div>// توصيل التحديدfunction onCheck(item: LhFlatTreeNode) { checkedKeys.value = toggleCheckKeys(props.data, checkedKeys.value, item.key, { checkStrictly })}function checkState(item: LhFlatTreeNode) { return getNodeCheckState(item.node, checkedKeys.value, checkStrictly)}// التحميل الكسولasync function onLoad(item: LhFlatTreeNode) { if (!isNodeLoadable(item.node, lazy, loadedKeys.value)) return loadingKeys.value = [...loadingKeys.value, item.key] const children = await fetchChildren(item.key) loadedMap.value = { ...loadedMap.value, [item.key]: children } loadedKeys.value = [...loadedKeys.value, item.key] // دمج غير قابل للتغيير ثم إعادة التسطيح}const merged = computed(() => mergeLoadedChildren(props.data, loadedMap.value))// React: expandedKeys / checkedKeys / selectedKey كلها حالات مُتحكَّم بها؛// flatNodes = useMemo(() => flattenTree(merged, expandedKeys), [merged, expandedKeys])// uniapp: <view> + إزاحة داخلية padding؛ وجميع دوال core محايدة للطرف، تُستخدم كما هيالأنماط والرموز
Section titled “الأنماط والرموز”| اسم الصنف | الدلالة |
|---|---|
.lh-tree |
الصنف الأساسي |
.lh-tree--checkbox |
وضع التحديد (حشو داخلي للعُقدة يفسح مكانًا لخانة الاختيار) |
.lh-tree-node |
العُقدة |
.lh-tree-node--expanded |
موسَّعة (يدور السهم) |
.lh-tree-node--selected |
محدَّدة (خلفية باللون الأساسي) |
.lh-tree-node--disabled |
معطَّلة |
.lh-tree-node--leaf |
ورقة (يُطوى مكان السهم) |
.lh-tree-node__arrow / __label |
سهم التوسيع / النص |
تتحقق الإزاحة الداخلية في طرف العرض عبر paddingLeft: level * 18px (لا يتدخل core في البكسلات)، وخلفية التحديد --lh-primary-color-light، والسهم --lh-text-color-secondary.
ملاحظات
Section titled “ملاحظات”levelيبدأ من 0 وaria-levelيبدأ من 1 — وهذا أكثر موضع يسهو فيه المستخدم في هذه الوحدة، فلا بد من+1عند ربطaria-level.- نطاق نفاذ حكم سلسلة الأجداد: الحماية نافذة فقط داخل الاستدعاء الواحد. فاستدعاءان مستقلان لـ
findTreeNodeعلى الشجرة نفسها يحكمان كلٌّ منهما على حدة، ويُجتاز الشجرة الفرعية المشتركة مرتين — وهذا مقصود (مشاركة DAG قانونية)، وليس خطأً. checkStrictly(لا ربط بين الأب والأبناء): تصبحgetNodeCheckStateثنائية الحالات، ولا يُغيِّرtoggleCheckKeysسوى مفتاح العُقدة نفسه، دون رجوع إلى الأجداد. وشائعة الاستخدام في سيناريوهات توزيع الصلاحيات.- الثابت المتعلق بتسلسل العُقد المعطَّلة: يتخطى رجوع الأجداد في
toggleCheckKeysالأب المعطَّل — فلا يُعاد كتابة مفتاحه بالتسلسل. وإن احتاج العمل إلى «تحديد أب معطَّل برمجيًّا»، فليُعامَلcheckedKeysمباشرة بدلًا من استخدام هذه الدالة. isLeafله الأولوية علىchildren: العُقدة المُصرَّح لها بـisLeaf: trueتُعالَج كورقة حتى لو كان لهاchildren(شائع في التحميل الكسول: التصريح بالورقة يمنع التوسيع).- عدم قابلية التغيير في
mergeLoadedChildren: تُرجِع شجرة جديدة ولا تُعدِّلnodesالمُمرَّرة — فـ props.data يملكه المستخدم، والتعديل المباشر يكسر تدفق البيانات في اتجاه واحد. - أداء
flattenTree: إعادة حساب كاملة في كل مرة تتغير فيها expandedKeys. فعند المستويات العميقة + البيانات الضخمة فكِّر في memo للأشجار الفرعية (تخزين مؤقت في طرف العرض مجمَّع بحسب parentKey). - البيانات ذات المراجع الدائرية: جميع الدوال العودية محميَّة بحكم سلسلة الأجداد فلا تدور في حلقة لا نهائية — ولكن وجود حلقة في البيانات نفسها خطأٌ في حد ذاته، والحكم يمنع انهيارها فحسب.
@lh-ui/share/core/tree → getNodeCheckState( )(填好参数后点运行)Copyright © 2024-2026 FengMo WenXue