loading —— Маска загрузки
Маска загрузки накрывает область (или страницу целиком), показывая «тут идёт работа». core/loading занимается разрешением цели монтирования и починкой позиционирования: директиве v-loading="true" нужно спозиционировать маску absolutely поверх элемента-триггера, но элемент со статическим позиционированием не может быть позиционированным предком — эту починку и берёт на себя core.
Когда использовать
Заголовок раздела «Когда использовать»- В каждой платформе реализована директива
v-loadingили компонент<LhLoading>: отправка формы кнопкой, обновление таблицы, загрузка целой страницы. - Когда нужно явно замаскировать конкретную область (а не весь экран).
- Не использовать для: текстовой обратной связи (используйте
message); фоновых задач неизвестной длительности (используйтеnotificationс возможностью отмены); каркасов (используйтеskeleton— loading предполагает, что раскладка содержимого не изменится, а skeleton предполагает, что изменится).
import { getLoadingClassNames, resolveLoadingTarget, isLoadingFullscreen, ensurePositioned, getLoadingAria,} from '@lh-ui/share/core/loading'import type { LhLoadingOptions, LhLoadingHandle, LhLoadingState } from '@lh-ui/share/core/loading'Замысел дизайна
Заголовок раздела «Замысел дизайна»Три состояния resolveLoadingTarget: ничего не передано → document.body (полный экран); строка → querySelector, при промахе откат к body (опечатка в селекторе не приведёт к падению, просто маска станет полноэкранной); элемент возвращается как есть. Этот откат критичен: императивный API получает target из пользовательских ref/селекторов, и грязные данные должны быть аккуратно обработаны.
«Занятое нужно вернуть» в ensurePositioned: элемент со static-позиционированием временно меняется на relative, а возвращаемая функция restore() отменяет изменение при размонтировании. Меняется только position в inline-стиле, классы и остальные стили не трогаются — при отмене inline-позиция возвращается к исходному значению (возможно, пустой строке). Если у элемента уже есть позиционирование (relative/absolute/fixed/sticky), ничего не делается, а restore остаётся пустой функцией — вызывающей стороне не нужно ничего проверять, можно вызывать безусловно.
Признак полноэкранности — «является ли цель body»: isLoadingFullscreen(target) вместо дополнительного флага — если target это body, считается, что маска полноэкранная, и к имени класса добавляется --fullscreen (позиционирование fixed + более высокий z-index). SSR-гард: при отсутствии document возвращается false.
Двойная семантика getLoadingAria: aria-busy="true" помечает замаскированную область (программы чтения экрана не зачитывают её устаревшее содержимое до завершения загрузки), а вращающийся значок — декоративная графика → aria-hidden (чтобы программы чтения не зачитывали бесполезное «вращающийся кружок»). busy: false возвращает пустой объект — UI-слой может использовать его в v-bind безусловно.
Таблица функций
Заголовок раздела «Таблица функций»| Функция | Сигнатура | Возвращаемое значение | Назначение |
|---|---|---|---|
getLoadingClassNames |
(state: LhLoadingState) => string[] |
имена классов | полноэкранность / наличие текста |
resolveLoadingTarget |
(target: string | HTMLElement | undefined) => HTMLElement |
целевой элемент | при промахе строкового селектора откат к body |
isLoadingFullscreen |
(target: HTMLElement) => boolean |
полноэкранная ли | цель — body, значит полноэкранная |
ensurePositioned |
(target: HTMLElement) => { restore } |
дескриптор отмены | static → relative, отмена при размонтировании |
getLoadingAria |
(busy?: boolean) => LhAriaAttrs |
пакет aria-атрибутов | aria-busy + aria-hidden значка |
Определения типов
Заголовок раздела «Определения типов»export interface LhLoadingOptions { text?: string // текст маски fullscreen?: boolean // принудительно полноэкранная background?: string // цвет фона маски (переопределяет полупрозрачный по умолчанию) spinner?: boolean // показывать вращающийся значок (по умолчанию true) target?: string | HTMLElement // цель монтирования delay?: number // задержка показа в миллисекундах (против мигания)}
export interface LhLoadingHandle { close: () => void // закрыть и размонтировать маску setText: (text: string) => void // динамически обновить текст}
export interface LhLoadingState { fullscreen: boolean withText: boolean // при наличии текста оставляем место под текстовый контейнер}Доступность
Заголовок раздела «Доступность»getLoadingAria(true) // { 'aria-hidden': true, 'aria-busy': true }getLoadingAria(false) // {} (для безусловного v-bind)aria-busy="true"— стандартная WAI-ARIA метка «занятой области»: программы чтения экрана по ней понимают, что содержимое скоро изменится, и пока не зачитывают текущее устаревшее. Вешается на замаскированный контейнер с содержимым, а не на сам слой маски.- Вращающийся значок с
aria-hidden: это чисто визуальная обратная связь, а текст (если есть) уже передаёт смысл. - Текст должен быть осмысленным: «Загрузка…» лучше, чем «loading»; прогресс в процентах («Загружено 76%») дружественнее к пользователям программ чтения экрана — при изменении текста маски программы чтения его зачитывают (использование
aria-liveостаётся за UI-слоем). - Полноэкранная маска и фокус: при полноэкранной маске содержимое всё ещё может получать фокус через Tab — если нужно заблокировать взаимодействие, UI-слой должен перехватывать его на маске или делать область содержимого
inert(см. механизм inert вshared/focus-trap).
Примеры использования
Заголовок раздела «Примеры использования»import { resolveLoadingTarget, ensurePositioned, isLoadingFullscreen, getLoadingClassNames } from '@lh-ui/share/core/loading'
resolveLoadingTarget(undefined) // document.body (полный экран)resolveLoadingTarget('.table-wrapper') // найденный элемент; при промахе → bodyresolveLoadingTarget(elRef) // возвращается как есть
const el = resolveLoadingTarget(props.target)const restore = ensurePositioned(el).restore // static → relative, запоминает исходное значениеtry { // …рендер маски…} finally { restore() // при размонтировании восстанавливает position}
isLoadingFullscreen(document.body) // trueisLoadingFullscreen(tableEl) // false// Императивный сервис (UI-слой): создание / закрытие / подсчёт ссылокconst instances = new Map<HTMLElement, { count: number; restore: () => void; el: HTMLElement }>()
function showLoading(target: HTMLElement, text?: string) { const hit = instances.get(target) if (hit) { hit.count++; return } const { restore } = ensurePositioned(target) const mask = createMask(text, isLoadingFullscreen(target)) target.appendChild(mask) instances.set(target, { count: 1, restore, el: mask })}
function closeLoading(target: HTMLElement) { const hit = instances.get(target) if (!hit) return hit.count-- if (hit.count > 0) return // подсчёт ссылок: несколько loading закрываются по одному hit.el.remove() hit.restore() // восстанавливает position instances.delete(target)}<!-- Vue3: директива v-loading --><template> <div class="table-wrapper" v-loading="loading"> <LhTable :data="rows" /> </div></template>
<script setup>// Реализация директивы: на mounted по binding.value определяется видимостьconst loading = ref(false)async function refresh() { loading.value = true try { rows.value = await fetch() } finally { loading.value = false }}</script>// React: хук useLoading (ref + useEffect управляют монтированием маски и restore)// uniapp: маска через <view> + absolute; ensurePositioned работает только на h5,// на стороне мини-программ поведение fixed/absolute отличается, адаптация за UI-слоемСтили и токены
Заголовок раздела «Стили и токены»| Имя класса | Семантика |
|---|---|
.lh-loading |
контейнер маски (absolute, накрывает цель) |
.lh-loading--fullscreen |
полноэкранная (fixed + более высокий z-index) |
.lh-loading--with-text |
с текстом (вертикально: значок + текст) |
.lh-loading__spinner |
вращающийся значок (LhSpinner) |
.lh-loading__text |
текст |
Цвет фона маски --lh-mask-color (полупрозрачный, отдельный для светлой и тёмной темы); цвет текста --lh-text-primary-regular; z-index --lh-z-index-loading.
Замечания
Заголовок раздела «Замечания»restore()нужно вызывать обязательно:ensurePositionedменяет inline-позицию целевого элемента, и без отмены останется загрязнение «relative», ломающее раскладку (особенно еслиposition: staticбыл задан намеренно). Используйте try/finally.- Промах селектора молча делает маску полноэкранной:
resolveLoadingTarget('.typo-selector')откатится к body — при разборе «почему маска накрыла весь экран» первым делом проверяйте селектор. - Подсчёт ссылок: несколько параллельных загрузок одной области (два одновременных запроса) требуют подсчёта, чтобы маска закрылась только после завершения последней. Core не хранит состояние, учёт ведёт UI-слой.
delayпротив мигания: быстро завершающиеся запросы (меньше 200 мс) заставляют маску мелькнуть, что похоже на баг. UI-слой может использовать delay для отложенного показа, а при опережающем завершении запроса — отменять показ.- Вложенные маски: если внутри маски области A есть маска области B, то z-index и pointer-events для B должны обеспечиваться UI-слоем так, чтобы маска B накрывала только B — core считает позиционирование только для одной цели.
- SSR-гарды:
isLoadingFullscreenиensurePositionedимеют гардыtypeof document/window === 'undefined', но сама маска загрузки — клиентское понятие, при SSR вызывать сервис не следует. - Полноэкранная маска не блокирует взаимодействие: по умолчанию это только визуальная маска, кнопки на фоне остаются кликабельными (если слой маски не обрабатывает pointer-events). Для принудительной блокировки добавьте
inertна стороне UI-слоя.
@lh-ui/share/core/loading → getLoadingAria( )(填好参数后点运行)Copyright © 2024-2026 FengMo WenXue