steps шаги —— Навигация
Шаги — это «индикатор прогресса процесса»: «Корзина → Заполнение данных → Оплата → Готово» в корзине покупок, форма-мастер. Отличие от tabs — в семантике порядка: шаги идут строго друг за другом, а состояние выводится из позиции (выполнен/выполняется/ожидает). core/steps отвечает за вывод состояния и за одну легко допускающую ошибку деталь раскладки: при горизонтальном переносе шагов соединительную линию в конце строки нужно скрыть.
Когда использовать
Заголовок раздела «Когда использовать»- Реализация
<LhSteps>/<LhStep>на всех концах: процесс заказа, форма-мастер, прогресс задачи, многошаговая настройка. - Процесс имеет чёткий порядок, и пользователю нужно знать «на каком мы этапе».
- Не следует использовать: переключение однорангового содержимого (используйте
tabs— нет семантики порядка); простой прогресс в процентах (используйтеprogress); иерархическая навигация (используйтеmenuилиbreadcrumb).
import { getStepStatus, getStepsClassNames, getStepClassNames, getStepsLineLastIndices, getStepAria,} from '@lh-ui/share/core/steps'import type { LhStepStatus, LhStepsDirection, LhStepStatusState, LhStepClassState,} from '@lh-ui/share/core/steps'Замысел
Заголовок раздела «Замысел»Состояние выводится из позиции: основное правило getStepStatus — index < current → finish, === → process, > → wait. В большинстве сценариев достаточно передать current, и состояния вычисляются автоматически — UI-слою не нужно явно объявлять состояние каждого шага.
forced — это общее переопределение: состояние forced уровня Steps (например, error) делает все шаги такими. Это семантика «процесс целиком failed»: при неудаче оплаты неудачей считается не один шаг, а весь процесс становится недоступным для продолжения. Обратите внимание: его приоритет наивысший (возврат идёт до вывода состояния из позиции).
Последний элемент не получает класс — это берёт на себя :last-child: самое тонкое место модуля. В комментарии указана причина: дочерние элементы рендерятся на стадии своего собственного setup, и в этот момент последующие братья ещё не зарегистрированы, поэтому isLast, вычисленный по общему числу зарегистрированных, на первом экране (включая SSR/SSG) ошибочно посчитает последним каждый элемент. Структурный селектор :last-child вычисляется при отрисовке CSS и потому всегда корректен — расплата в том, что стиль соединительной линии должен быть реализован через структурный псевдокласс.
getStepsLineLastIndices решает проблему переноса: когда горизонтальные шаги фиксированной ширины переносятся на несколько строк, соединительная линия последнего элемента каждой строки будет указывать на пустое место в конце строки (начало следующей строки) — визуальный разрыв. Функция находит точки переноса по разнице offsetTop соседних элементов: offsetTops[i+1] > offsetTops[i] означает, что i+1 перешёл на следующую строку, и i — конец строки. UI-слой добавляет элементу в конце строки (включая глобально последний) класс line-last, скрывающий соединительную линию.
lineLast и isLast — разные вещи: isLast (глобально последний) берёт на себя :last-child; lineLast (элемент в конце строки) вычисляется — UI-слой после рендера должен прочитать offsetTop и заполнить класс — это нужно только в сценарии горизонтального переноса.
Таблица функций
Заголовок раздела «Таблица функций»| Функция | Сигнатура | Возврат | Назначение |
|---|---|---|---|
getStepStatus |
(state: LhStepStatusState) => LhStepStatus |
состояние | переопределение forced / вывод из позиции |
getStepsClassNames |
(direction: LhStepsDirection) => string[] |
классы | направление |
getStepClassNames |
(state: LhStepClassState) => string[] |
классы | состояние + кликабельность + конец строки |
getStepsLineLastIndices |
(offsetTops: number[]) => number[] |
индексы концов строк | поиск переносов в горизонтальном режиме |
getStepAria |
(status: LhStepStatus) => LhAriaAttrs |
aria-пакет | aria-current=“step” |
Определения типов
Заголовок раздела «Определения типов»export type LhStepStatus = 'wait' | 'process' | 'finish' | 'error'export type LhStepsDirection = 'horizontal' | 'vertical'
export interface LhStepStatusState { index: number // индекс шага (с 0) current: number // индекс текущего шага forced?: LhStepStatus // состояние общего переопределения уровня Steps}
export interface LhStepClassState { status: LhStepStatus clickable: boolean lineLast?: boolean // последний ли элемент в текущей строке (измеряется UI-слоем при горизонтальном переносе)}Доступность
Заголовок раздела «Доступность»getStepAria('process') // { 'aria-current': 'step' }getStepAria('finish') // {}getStepAria('error') // {}- aria-current=“step” отмечает текущий шаг: это специальная семантика WAI-ARIA, скринридер озвучивает «текущий шаг» — это стандартнее, чем текстовая подсказка «выполняется». Выводится только в состоянии process.
- Семантика упорядоченного списка: шаги рекомендуется рендерить через
<ol>— семантика порядка встроена, скринридер озвучит «пункт 2 из 4». Если реализация на<div>, добавьтеrole="list"и каждому элементуrole="listitem". - Состояние — текст, а не только цвет: галочка для finish, цифра для process, серый для wait — пользователь с дальтонизмом их не различит. Полными будут только заголовок каждого шага («Заполнение данных») в сочетании с значком.
- Озвучивание шага с ошибкой: состояние ошибки следует в UI-слое выражать через
aria-invalidили добавлением после заголовка шага текста «(не выполнено)», одного красного значка недостаточно. - Кнопочная семантика кликабельных шагов: при
clickable: trueзаголовок шага следует рендерить как<button>(откат к выполненным шагам), доступный с клавиатуры; некликабельный — обычный текст.
Примеры использования
Заголовок раздела «Примеры использования»import { getStepStatus, getStepsLineLastIndices } from '@lh-ui/share/core/steps'
getStepStatus({ index: 0, current: 2 }) // 'finish'getStepStatus({ index: 2, current: 2 }) // 'process'getStepStatus({ index: 3, current: 2 }) // 'wait'getStepStatus({ index: 0, current: 2, forced: 'error' }) // 'error' (forced переопределяет все)getStepStatus({ index: 2, current: 2, forced: 'wait' }) // 'wait'
// Горизонтальный перенос (по 2 элемента в строке, 4 элемента в 2 строки):getStepsLineLastIndices([10, 10, 60, 60]) // [1, 3] (у индекса 2 offsetTop больше → 1 конец строки; 3 — глобально последний)getStepsLineLastIndices([10, 10, 10]) // [2] (переноса нет, только глобально последний)getStepsLineLastIndices([]) // []// UI-слой: после рендера измеряем offsetTop и заполняем класс lineLast// (нужно только для горизонтального режима)const items = ref<HTMLElement[]>([])const lineLastSet = ref<Set<number>>(new Set())
onMounted(() => { const offsetTops = items.value.map((el) => el.offsetTop) lineLastSet.value = new Set(getStepsLineLastIndices(offsetTops))})// Слушаем resize окна: точки переноса меняются с шириной<!-- Vue3: процесс заказа --><template> <LhSteps :current="1"> <LhStep title="Корзина" description="Выбрано 3 товара" /> <LhStep title="Заполнение данных" description="Адрес доставки и счёт" /> <LhStep title="Оплата" /> <LhStep title="Готово" /> </LhSteps></template><!-- Общая неудача (при ошибке оплаты весь процесс отмечается красным) --><template> <LhSteps :current="2" status="error"> <LhStep title="Корзина" /><LhStep title="Заполнение данных" /> <LhStep title="Оплата" description="Время оплаты истекло, повторите попытку" /> <LhStep title="Готово" /> </LhSteps></template>// React: useLayoutEffect для измерения offsetTop (избежание мерцания первого кадра),// плюс слушатель resize для пересчёта// uniapp: <view> + flex; при горизонтальном переносе на стороне мини-программ// top получается через запрос узлов createSelectorQueryСтили и токены
Заголовок раздела «Стили и токены»| Класс | Семантика |
|---|---|
.lh-steps |
корневой контейнер (flex) |
.lh-steps--{horizontal|vertical} |
направление |
.lh-steps__item |
элемент шага |
.lh-steps__item--{wait|process|finish|error} |
состояние (цвет кружка + цвет текста + цвет соединительной линии) |
.lh-steps__item--clickable |
кликабельный (подсветка при hover + указатель) |
.lh-steps__item--line-last |
элемент в конце строки (скрывает линию, ведущую в пустоту) |
.lh-steps__circle / __title / __description |
кружок / заголовок / описание |
.lh-steps__line |
соединительная линия (через :last-child скрывается линия глобально последнего элемента) |
Цвета состояний: wait --lh-text-placeholder, process --lh-primary-color, finish --lh-success-color, error --lh-danger-color; соединительная линия обычно --lh-border-color, выполненный отрезок — цветом finish.
Замечания
Заголовок раздела «Замечания»lineLastнужен только при горизонтальном переносе: в вертикальных шагах каждый элемент занимает строку, и соединительная линия идёт строго вниз — проблемы конца строки нет. Горизонтальные шаги фиксированной ширины (без переноса) тоже не нуждаются в измерении — только когда фиксированная ширина + разрешён перенос.- Момент измерения:
offsetTopнужно читать после отрисовки DOM (onMounted / useLayoutEffect) и пересчитывать при resize. В первом кадре возможно мерцание (линия сначала показывается, потом скрывается) — это допустимо; в строгих сценариях показ откладывается черезvisibility. forced— это общее переопределение, а не одного шага: при передачеstatus= error все шаги становятся error, включая выполненные — это семантика «весь процесс failed». Чтобы отметить только один шаг, передайте этому LhStep отдельный status (UI-слой объединяет пошаговый status в пошаговую версию forced либо напрямую переопределяет класс элемента).currentможет выходить за границы: приcurrent: 5и всего 4 шагах все станут finish (все index < 5) — используется для финального отображения «процесс завершён», отдельная проверка не нужна.- Ловушка последнего элемента в SSR/SSG: именно поэтому
getStepClassNamesне принимает параметр isLast. Любая реализация, вычисляющая последний элемент на стадии setup по «общему числу зарегистрированных», ошибётся на первом экране для всех элементов — используйте:last-child. - Бизнес-ограничение кликабельного отката:
clickableобычно включается только для выполненных шагов (просмотр информации), а текущие и ожидающие шаги недоступны для перехода. Разрешён ли переход на произвольный шаг, определяет бизнес, core выдаёт только имена классов. - Пространство под description: у горизонтальных шагов с описанием увеличивается высота, а высота строки при переносе определяется самым высоким элементом — перед измерением offsetTop убедитесь, что все элементы отрисованы.
@lh-ui/share/core/steps → getStepStatus( )(填好参数后点运行)Copyright © 2024-2026 FengMo WenXue