Gavia UIv1.0.0

Рецепты

Рабочие сценарии с валидацией, отменой, повтором и операциями на стороне приложения.

Операции остаются у потребителя

Рецепты собираются из публичных компонентов. Замените учебные операции своими запросами и сохраните загрузку, ошибку, отмену и повтор. Выбранный рецепт отдельно загружает живой пример и SFC для копирования.

Форматируйте размер файла

formatWlBytes сохраняет шаг 1024 и формат Intl для прежнего вызова с двумя аргументами. В третьем аргументе можно задать подписи единиц, десятичный разделитель, группировку, точность и предельную единицу. FileUpload использует этот же форматтер: настройки локали 0.x, округление и предел в мегабайтах сохраняются.

Показать Vue-код
import { formatWlBytes } from "gavia-ui/utils";
formatWlBytes("ru", 1536);
formatWlBytes("ru", 1536, { unitLabels: { kilobyte: "КиБ" } });

Поиск, фильтры, сортировка и страницы. Создание и редактирование в Drawer, подтверждение удаления и Toast.

Материалы команды

Документы, договорённости и исследования.
12 материалов
Фильтры материалов
МатериалСтатусДействия
Доступы и ролиВ работе
Доступы и роли · 12В работе
Доступы и роли · 8В работе
План исследованияВ работе

Найдено: 12 · страница 1 из 3

Показать Vue-код
<script setup lang="ts">
import { useWlConfirm, useWlToast, WlButton, WlDrawer, WlEmpty, WlField, WlFilterBar, WlInput, WlPageHeader, WlPagination, WlPill, WlSelect, WlTable, type WlTableColumn, WlTag } from "gavia-ui";
import { computed, nextTick, ref, watch } from "vue";
type Material = { id: number; title: string; status: string };
const materials = ref<Material[]>(Array.from({ length: 12 }, (_, index) => ({
  id: index + 1, title: ["Руководство команды", "План исследования", "Правила ревью", "Доступы и роли"][index % 4] + (index < 4 ? "" : ` · ${index + 1}`),
  status: index % 2 ? "progress" : "done"
})));
const search = ref("");
const status = ref<string | null>(null);
const order = ref<string | null>("asc");
const page = ref(1);
const confirm = useWlConfirm();
const toast = useWlToast();
const filtered = computed(() => materials.value.filter((item) =>
  item.title.toLocaleLowerCase("ru").includes(search.value.toLocaleLowerCase("ru")) && (!status.value || item.status === status.value))
  .sort((a, b) => a.title.localeCompare(b.title, "ru") * (order.value === "desc" ? -1 : 1)));
const pageCount = computed(() => Math.max(1, Math.ceil(filtered.value.length / 4)));
const visibleRows = computed(() => filtered.value.slice((page.value - 1) * 4, page.value * 4));
watch([search, status, order], () => {
  page.value = 1;
});
watch(pageCount, (count) => {
  page.value = Math.min(page.value, count);
});
const columns: readonly WlTableColumn<Material>[] = [{ key: "title", label: "Материал" }, { key: "status", label: "Статус" }, { key: "actions", label: "Действия", kind: "virtual" }];
function clear(): void {
  search.value = "";
  status.value = null;
}
const editing = ref(false);
const editingId = ref<number | null>(null);
const title = ref("");
const titleError = ref("");
let nextId = 13;
function edit(item?: Material): void {
  editingId.value = item?.id ?? null;
  title.value = item?.title ?? "";
  titleError.value = "";
  editing.value = true;
}
async function save(): Promise<void> {
  titleError.value = title.value.trim() ? "" : "Введите название.";
  if (titleError.value) {
    await nextTick();
    document.getElementById("recipe-material-title")?.focus();
    return;
  }
  const current = materials.value.find((item) => item.id === editingId.value);
  if (current) current.title = title.value.trim();
  else materials.value.push({ id: nextId++, title: title.value.trim(), status: "progress" });
  editing.value = false;
  toast.ok("Материал сохранён");
}
function remove(id: number): void {
  const item = materials.value.find((entry) => entry.id === id);
  if (!item) return;
  confirm.confirmDanger({
    header: "Удалить материал?", message: `«${item.title}${"» будет удалён."}`, acceptLabel: "Удалить", rejectLabel: "Отмена",
    accept: () => {
      materials.value = materials.value.filter((entry) => entry.id !== id);
      toast.ok("Материал удалён");
    }
  });
}
</script>

<template>
  <section class="wl-stack" data-space="lg" aria-label="Список материалов">
    <WlPageHeader
      title="Материалы команды"
      description="Документы, договорённости и исследования."
      :heading-level="2"
      size="md"
    >
      <template #meta><WlTag>{{ materials.length }} {{ "материалов" }}</WlTag></template>
      <template #actions><WlButton variant="primary" @click="edit()">{{ "Создать материал" }}</WlButton></template>
    </WlPageHeader>
    <WlInput
      v-model="search"
      type="search"
      placeholder="Найти материал"
      aria-label="Найти материал"
    />
    <WlFilterBar
      :active-count="Number(Boolean(status))"
      aria-label="Фильтры материалов"
      panel-title="Фильтры материалов"
      @clear="clear"
    >
      <WlSelect
        v-model="status"
        :options="[{ label: 'Готово', value: 'done' }, { label: 'В работе', value: 'progress' }]"
        option-label="label"
        option-value="value"
        aria-label="Статус материалов"
        placeholder="Все статусы"
      />
      <WlSelect
        v-model="order"
        :options="[{ label: 'Название А–Я', value: 'asc' }, { label: 'Название Я–А', value: 'desc' }]"
        option-label="label"
        option-value="value"
        aria-label="Порядок материалов"
      />
      <template #summary><WlTag
        v-if="status"
        removable
        remove-label="Убрать фильтр статуса"
        @remove="status = null"
      >{{ status === 'done' ? "Готово" : "В работе" }}</WlTag></template>
    </WlFilterBar>
    <div
      v-if="filtered.length"
      class="ds-material-table"
      role="region"
      aria-label="Материалы команды"
      tabindex="0"
    ><WlTable :columns="columns" :value="visibleRows" :pt="{ root: { style: { minWidth: '640px' } } }">
      <template #cell-status="{ value }">
        <WlPill
          :tone="value === 'done' ? 'success' : 'info'"
          :label="value === 'done' ? 'Готово' : 'В работе'"
        />
      </template>
      <template #cell-actions="{ row }"><div class="wl-inline" data-space="xs"><WlButton
        size="sm"
        variant="ghost"
        :aria-label="`${'Редактировать '}${row.title}`"
        @click="edit(materials.find((item) => item.id === row.id))"
      >{{ "Изменить" }}</WlButton><WlButton
        size="sm"
        variant="ghost"
        tone="danger"
        :aria-label="`${'Удалить '}${row.title}`"
        @click="remove(Number(row.id))"
      >{{ "Удалить" }}</WlButton></div></template>
    </WlTable></div>
    <WlEmpty
      v-else
      icon="search"
      title="Материалы не найдены"
      description="Измените запрос или сбросьте фильтры."
    ><template #action><WlButton @click="clear">{{ "Сбросить поиск" }}</WlButton></template></WlEmpty>
    <div class="wl-inline" data-space="md">
      <WlPagination v-if="filtered.length" v-model:page="page" :page-count="pageCount" />
      <p class="wl-text-small wl-text-muted" role="status">
        {{ "Найдено:" }} {{ filtered.length }} {{ "· страница" }}
        {{ page }} {{ "из" }} {{ pageCount }}
      </p>
    </div>
    <WlDrawer v-model:visible="editing" :header="editingId ? 'Редактирование материала' : 'Новый материал'">
      <form
        id="recipe-material-form"
        class="wl-stack"
        data-space="lg"
        @submit.prevent="save"
      >
        <WlField
          id="recipe-material-title"
          v-slot="field"
          label="Название"
          required
          :error="titleError"
        ><WlInput
          :id="field.id"
          v-model="title"
          :invalid="field.invalid"
          :aria-describedby="field.ariaDescribedby"
          maxlength="120"
        /></WlField>
        <p class="wl-text-small wl-text-muted">{{ "Чтобы применить изменения, нажмите «Сохранить»." }}</p>
      </form>
      <template #footer>
        <WlButton variant="ghost" @click="editing = false">{{ "Отмена" }}</WlButton>
        <WlButton variant="primary" type="submit" form="recipe-material-form">{{ "Сохранить" }}</WlButton>
      </template>
    </WlDrawer>
  </section>
</template>

<style scoped>
.ds-material-table { overflow: auto; max-width: 100%; }
</style>

В примерах тестовые данные. Сохранение и загрузка имитируются в браузере. В своём приложении подключите запросы к серверу. Сценарий и код для копирования берутся из одного Vue-файла.

Подключение стилей, темы и сервисов
// main.ts — CSS подключается явно; тему задаёт data-wl-theme на html.
import { createApp } from "vue";
import { WlConfig, WlToastService, WlConfirmationService, createWlPt, wlLocaleRu } from "gavia-ui";
import "gavia-ui/styles/reset.css";
import "gavia-ui/styles/base.css";
import "gavia-ui/styles/primitives.css";
import "gavia-ui/themes/white.css";
import "gavia-ui/themes/graphite.css";
import "gavia-ui/themes/newspaper.css";
import "gavia-ui/styles/fonts/gavia.css";
import "gavia-ui/themes/gavia.css";
import "gavia-ui/themes/gavia-dark.css";
import App from "./App.vue";

document.documentElement.dataset.wlTheme = "white";

// WlConfig merges application pt with defaults; local component pt is applied last.
const pt = createWlPt({ button: { root: { "data-ui": "action" } } });
createApp(App).use(WlConfig, { locale: wlLocaleRu, motion: true, pt })
  .use(WlToastService).use(WlConfirmationService).mount("#app");

// App.vue: импортируйте WlToast/WlConfirmDialog и смонтируйте по одному
// контейнеру рядом со страницей. Уведомления и подтверждения используют их.
// <template><YourPage /><WlToast /><WlConfirmDialog /></template>
Исходники и руководство