anchor مرساة —— تنقل
المرساة هي «شريط دليل جانبي للوثائق الطويلة»: في الجانب الأخر من موقع التوثيق تجد
«الاستخدام الأساسي / API / ملاحظات»، وعند النقر تنتقل إلى العنوان المقابل، وعند التمرير
يُبرز القسم الحالي. وفرقها عن menu هو أن المرساة ترتبط بموضع محتوى داخل الصفحة نفسها،
وليس بصفحات مختلفة.
متى يُستخدم
Section titled “متى يُستخدم”- لتطبيق
<LhAnchor>في كل طرف: أدلة مواقع التوثيق، وأدلة القراءة للمقالات الطويلة، وتنقّل فصول وثائق API، وتنقّل أقسام الصفحات الرئيسية. - عندما يكون محتوى الصفحة طويلاً بما يستدعي قفزًا سريعًا، وللأقسام عناوين واضحة.
- لا ينبغي استخدامه: التنقّل بين صفحات الموقع (استخدم
menu/breadcrumb)؛ الصفحات القصيرة (لا حاجة لدليل في صفحة بشاشة واحدة)؛ الأقساع بلا عناوين (تعتمد المرساة على نص العنوان كنص للرابط).
طريقة الاستيراد
Section titled “طريقة الاستيراد”import { flattenAnchorItems, collectAnchorIds, resolveActiveAnchor, getAnchorClassNames, getAnchorLinkClassNames,} from '@lh-ui/share/core/anchor'import type { LhAnchorItem, LhFlatAnchorItem } from '@lh-ui/share/core/anchor'نية التصميم
Section titled “نية التصميم”بنية شجرية → قائمة مسطّحة: تكوين المرساة شجري (أقسام تتوّج أقسامًا فرعية)، لكن التكوين
المعروض قائمة أحادية البعد (مع مسافة بادئة). تعبر flattenAnchorItems بأولوية العمق محافظةً على ترتيب
الوثيقة——فالدليل المرتبب بشكل خاطئ أسوأ من عدم وجود دليل. ويسجّل level العمق الأصلي
ليحرّك أنماط المسافة البادئة.
سلسلة الأسلاف seen مضادّة لمراجع الحلقات: seen هي مجموعة الأسلاف على مسار DFS الحالي
(وليست مجموعة شاملة). ويذكر التعليق صراحةً «حماية سلسلة الأسلاف لاكتشاف مراجع الحلقات»——إذا
ضمّت بيانات أطفالَ عنصر A العنصر A نفسه (بيانات قذرة)، فإن المجموعة الشاملة تُزيل التكرار فقط،
بينما سلسلة الأسلاف تقطع العودية مباشرةً فلا تحدث حلقة لا نهائية. وهذا ضمان ضد الأخطاء
النوعية في البنى الشجرية.
resolveActiveAnchor «الlast الذي تجاوز العتبة»: يجد في إزاحات مرتّبة بترتيب الوثيقة
آخر مرساة ذات offset <= currentTop. والتفصيل الجوهري: عند اصطدام أول offset > currentTop
يتم break——لأن الإزاحات مرتّبة، فما بعدها أكبر فقط، ومتابعة الاجتياز إهدار. هذا تنفيذ O(n)
لكنه مع إنهاء مبكّر.
عدم الإصابة يعود إلى العنصر الأول: عند التمرير إلى ما قبل كل المراسي (أعلى الصفحة) يُرجع
ids[0]——يظل العنصر الأول مبرّزًا دائمًا، لتجنّب الحالة المحيّرة «لا يوجد عنصر مبرّز». وتُرجع
القائمة الفارغة سلسلة فارغة (لا تُبرز طبقة واجهة المستخدم شيئًا).
offsets و ids يجب أن يكونا بنفس الترتيب: يتطلّب توقيع الدالة مصفوفتين——تُقاس الإزاحات
من طبقة واجهة المستخدم عبر getBoundingClientRect() لكل عنصر وتُمرَّر بالترتيب نفسه. واختلال
الترتيب يعطي عنصرًا مبرّزًا خاطئًا، وهذا عقد استدعاء لا يمكن لـ core التحقق منه.
جدول الدوال
Section titled “جدول الدوال”| الدالة | التوقيع | الناتج | الدور |
|---|---|---|---|
flattenAnchorItems |
(items, level?, seen?) => LhFlatAnchorItem[] |
قائمة مسطّحة | أولوية العمق مع الحفاظ على الترتيب |
collectAnchorIds |
(items) => string[] |
قائمة id | مصفوفة id بترتيب المسطّح |
resolveActiveAnchor |
(ids, offsets, currentTop) => string |
id النشط | تحديد الارتباط مع التمرير |
getAnchorClassNames |
(state: { direction }) => string[] |
أسماء الأصناف | اتجاه الحاوية |
getAnchorLinkClassNames |
(state: { active, level }) => string[] |
أسماء الأصناف | الحالة النشطة + مستوى المسافة البادئة |
تعريفات الأنواع
Section titled “تعريفات الأنواع”export interface LhAnchorItem { id: string // id للعنصر الهدف title: string // النص الظاهر children?: LhAnchorItem[]}
export interface LhFlatAnchorItem { id: string title: string level: number // العمق الأصلي (الجذر 0)، يحرّك المسافة البادئة}إمكانية الوصول
Section titled “إمكانية الوصول”تشير روابط المراسي إلى مراسي داخل الصفحة نفسها، والطريقة المعيارية هي عنصر <a href="#section">
الأصلي:
<a href="#usage" class="lh-anchor__link">مثال الاستخدام</a>...<section id="usage" tabindex="-1">…</section>- قفز مرساة أصلي + tabindex=“-1”: عند النقر على
<a href="#id">يمرّر المتصفح ويحاول تركيز الهدف. وعندما لا يكون الهدف قابلًا للتركيز، يبقى التركيز ثابتًا بعد القفز——فإضافةtabindex="-1"للهدف تجعله قادرًا على استقبال التركيز البرمجي، فيبدأ قارئ الشاشة القراءة منه. وهذا هو نمط WAI-ARIA المعتمد للمراسي سهلة الوصول. - aria-current لتمييز القسم الحالي:
--activeالخاص بـgetAnchorLinkClassNamesصنف بصري، ويُستحسن من ناحية إمكانية الوصول أن تضيف طبقة واجهة المستخدمaria-current="location"للرابط النشط (دلالة الموضع في الصفحة، تمييزًا لها عنaria-current="page"الخاص بـbreadcrumb). - القفز دون تأثير جانبي على عنوان URL: يدخل تغيير الـ hash في تأريخ المتصفح، فبعد خمس نقرات
على المراسي تحتاج إلى خمس ضغطات على زر الرجوع لمغادرة الصفحة. يمكن لطبقة واجهة المستخدم
استخدام
history.replaceStateأوscrollIntoViewبدلاً من القفز الافتراضي——وهذا قرار تفاعلي لا تعنيه core. - تغليف
nav+ aria-label: مثلbreadcrumb، يصرِّح<nav aria-label="محتوى هذه الصفحة">عن كونها منطقة تنقّل.
أمثلة الاستخدام
Section titled “أمثلة الاستخدام”import { flattenAnchorItems, collectAnchorIds, resolveActiveAnchor,} from '@lh-ui/share/core/anchor'
const items = [ { id: 'intro', title: 'مقدمة', children: [ { id: 'why', title: 'لماذا تستخدمه' }, ] }, { id: 'usage', title: 'الاستخدام' },]
flattenAnchorItems(items)// [ { id: 'intro', title: 'مقدمة', level: 0 },// { id: 'why', title: 'لماذا تستخدمه', level: 1 },// { id: 'usage', title: 'الاستخدام', level: 0 } ]
collectAnchorIds(items) // ['intro', 'why', 'usage']
// الارتباط مع التمرير: offsets هي مواضع المراسي نسبةً إلى أعلى الحاوية (بنفس ترتيب ids)resolveActiveAnchor(['intro', 'why', 'usage'], [0, 300, 600], 350) // 'why'resolveActiveAnchor(['intro', 'why', 'usage'], [0, 300, 600], 50) // 'intro' (لم يتجاوز why)resolveActiveAnchor(['intro', 'why', 'usage'], [0, 300, 600], 0) // 'intro' (عدم إصابة يعود للأول)resolveActiveAnchor([], [], 0) // '' (قائمة فارغة)// طبقة واجهة المستخدم: مراقبة التمرير + قياس الإزاحاتconst activeId = ref('')
function refresh() { const flat = flattenAnchorItems(props.items) const ids = flat.map((item) => item.id) const offsets = ids.map((id) => { const el = document.getElementById(id) return el ? el.getBoundingClientRect().top - containerTop : Infinity }) activeId.value = resolveActiveAnchor(ids, offsets, scrollTop.value)}
onMounted(() => window.addEventListener('scroll', refresh, { passive: true }))<!-- Vue3 --><template> <LhAnchor :items="items" :offset-top="80" @click="onAnchorClick" /></template>// React: useEffect يُسجِّل مراقبة scroll؛ ويُقاس الإزاحات عبر مصفوفة ref + getBoundingClientRect؛// ويُستخدم requestAnimationFrame أو throttle للتخفيف (scroll يعمل بتكرار عالٍ)// uniapp: `<scroll-view scroll-into-view="id">` هو قفز المراسي الأصلي في// التطبيقات المصغّرة؛ ويُؤخذ scrollTop من حدث scroll الخاص بـ scroll-viewالأنماط والرموز
Section titled “الأنماط والرموز”| اسم الصنف | الدلالة |
|---|---|
.lh-anchor |
الحاوية |
.lh-anchor--{vertical|horizontal} |
الاتجاه (شريط جانبي عمودي / شريط علوي أفقي) |
.lh-anchor__link |
رابط المرساة |
.lh-anchor__link--level-{0..n} |
مستوى المسافة البادئة (padding-inline-start يتزايد) |
.lh-anchor__link--active |
الحالة النشطة (اللون الأساسي + شريط مؤشر على جانب البداية) |
لون التفعيل --lh-primary-color؛ الروابط العادية --lh-text-regular؛ وحدة المسافة البادئة
--lh-anchor-indent (مثل 12px/مستوى).
ملاحظات
Section titled “ملاحظات”- قياس الإزاحة يجب أن يطرح إزاحة الحاوية: إذا كانت المراسي داخل حاوية ذات تمرير داخلي
(وليس تمرير window)، يجب طرح موضع أعلى الحاوية من
getBoundingClientRect().top، وإلا سيظل الإبراز منحرفًا. هذا أكثر الأخطاء شيوعًا. getBoundingClientRectيجبر التخطيط: استدعاؤها على N عنصرًا في كل حدث scroll يُطلق N إعادة ترتيب. التحسين: تخزين الإزاحات مؤقتًا وإعادة الحساب فقط عند تغيّر أبعاد المحتوى؛ أو استخدام IntersectionObserver لمراقبة كل عنصر على حدة.- تعويض إزاحة الحجب العلوي: عندما تكون للصفحة ترويسة مثبّتة (
affix)، قد يحجب الترويسةَ الهدفَ بعد قفز المرساة. أضِفscroll-margin-top(CSS) للعنصر الهدف أو اطرح ارتفاع الترويسة من قيمة التمرير في طبقة واجهة المستخدم——وهذه نقطة تعاون في التخطيط. - انتهاء صلاحية الإزاحات مع المحتوى الديناميكي: عند تغيّر ارتفاع المحتوى (توسيع/طي، اكتمال تحميل الصور) تصبح الإزاحات المخزّنة مؤقتًا خاطئة. يجب إعادة القياس عند تغيّر المحتوى.
- دلالة سلسلة الأسلاف لـ
seen: المعامل الثالثseenالخاص بـflattenAnchorItemsهو الأسلاف على المسار، وافتراضهnew Set(). لا يمرّره جانب العمل عادةً——فهو أساسًا لمنع الحلقات اللانهائية في البيانات القذرة. وإذا أحالت شجرتان شقیقتان إلى العقدة نفسها (مشاركة DAG)، فستجعل سلسلة الأسلاف العقدة تظهر مرة واحدة في كل مسار (وهو انتخاب القسم نفسه بين سلسلة الأسلاف فيtreeوseen الشاملة فيtable). idالفارغ وidالمكرّر: يجب أن يكونidفريدًا عالميًا (متطلّب مراسي HTML). ولا يُرجعdocument.getElementByIdسوى المطابقة الأولى——المعرفات المكررة تسبّب قفزًا خاطئًا، ولا يمكن لـ core التحقق من ذلك.- المرساة مقابل توجيه الدليل: تربط المرساة مواضع في الصفحة نفسها؛ وإذا كان كل عنصر من
«الدليل» صفحة مختلفة، فهو
menuلاanchor——لا تخلط بينهما (المرساة لا تُحدّث الصفحة، وتختلف في طريقة إدارة تأريخ التوجيه).
@lh-ui/share/core/anchor → resolveActiveAnchor( )(填好参数后点运行)Copyright © 2024-2026 FengMo WenXue