filters
Filters with i18n support
Filters with i18n support
Загрузка превью…
src/filters/c-filters-9.vue
<!--
Порт c-filters-9.tsx ("Filters with i18n support", registry-reui/bases/
radix/components/filters/c-filters-9.tsx, MIT).
`FilterI18nConfig` принимается компонентом `<Filters>` через проп `i18n`
(packages/ui/src/reui/filters/Filters.vue, см. `mergedI18n` — сливается с
`DEFAULT_I18N` по ключу, частичный конфиг допустим). Здесь передан полный
набор строк на пяти языках (en/es/fr/de/ja, как в оригинале), а не только
различающиеся ключи, чтобы условный `currentI18n` был типобезопасным
`FilterI18nConfig` без `Partial<...>`.
`IconPlaceholder` -> инлайновый `<svg>` (lucide-react v0.545.0: user, mail,
building, circle-check, map-pin, list-filter, chevron-down).
-->
<script setup lang="ts">
import { computed, h, ref } from "vue"
import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { createFilter, Filters, type Filter, type FilterFieldConfig, type FilterI18nConfig } from "@/components/reui/filters"
function UserIcon() {
return h(
"svg",
{
xmlns: "http://www.w3.org/2000/svg",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
class: "size-3.5",
},
[
h("path", { d: "M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" }),
h("circle", { cx: "12", cy: "7", r: "4" }),
]
)
}
function MailIcon() {
return h(
"svg",
{
xmlns: "http://www.w3.org/2000/svg",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
class: "size-3.5",
},
[
h("path", { d: "m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7" }),
h("rect", { x: "2", y: "4", width: "20", height: "16", rx: "2" }),
]
)
}
function BuildingIcon() {
return h(
"svg",
{
xmlns: "http://www.w3.org/2000/svg",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
class: "size-3.5",
},
[
h("path", { d: "M12 10h.01" }),
h("path", { d: "M12 14h.01" }),
h("path", { d: "M12 6h.01" }),
h("path", { d: "M16 10h.01" }),
h("path", { d: "M16 14h.01" }),
h("path", { d: "M16 6h.01" }),
h("path", { d: "M8 10h.01" }),
h("path", { d: "M8 14h.01" }),
h("path", { d: "M8 6h.01" }),
h("path", { d: "M9 22v-3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3" }),
h("rect", { x: "4", y: "2", width: "16", height: "20", rx: "2" }),
]
)
}
function CircleCheckIcon() {
return h(
"svg",
{
xmlns: "http://www.w3.org/2000/svg",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
class: "size-3.5",
},
[h("circle", { cx: "12", cy: "12", r: "10" }), h("path", { d: "m9 12 2 2 4-4" })]
)
}
function MapPinIcon() {
return h(
"svg",
{
xmlns: "http://www.w3.org/2000/svg",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
class: "size-3.5",
},
[
h("path", {
d: "M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0",
}),
h("circle", { cx: "12", cy: "10", r: "3" }),
]
)
}
function ListFilterIcon() {
return h(
"svg",
{
xmlns: "http://www.w3.org/2000/svg",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
},
[h("path", { d: "M2 5h20" }), h("path", { d: "M6 12h12" }), h("path", { d: "M9 19h6" })]
)
}
function ChevronDownIcon() {
return h(
"svg",
{
xmlns: "http://www.w3.org/2000/svg",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
class: "size-4",
},
[h("path", { d: "m6 9 6 6 6-6" })]
)
}
// Internationalization configurations
const i18nConfigs: Record<string, FilterI18nConfig> = {
en: {
addFilter: "Add filter",
searchFields: "Search fields...",
noFieldsFound: "No fields found.",
noResultsFound: "No results found.",
select: "Select...",
true: "True",
false: "False",
min: "Min",
max: "Max",
to: "to",
typeAndPressEnter: "Type and press Enter to add tag",
selected: "selected",
selectedCount: "selected",
percent: "%",
defaultCurrency: "$",
defaultColor: "#000000",
addFilterTitle: "Add filter",
operators: {
is: "is",
isNot: "is not",
isAnyOf: "is any of",
isNotAnyOf: "is not any of",
includesAll: "includes all",
excludesAll: "excludes all",
before: "before",
after: "after",
between: "between",
notBetween: "not between",
contains: "contains",
notContains: "does not contain",
startsWith: "starts with",
endsWith: "ends with",
isExactly: "is exactly",
equals: "equals",
notEquals: "not equals",
greaterThan: "greater than",
lessThan: "less than",
overlaps: "overlaps",
includes: "includes",
excludes: "excludes",
includesAllOf: "includes all of",
includesAnyOf: "includes any of",
empty: "is empty",
notEmpty: "is not empty",
},
placeholders: {
enterField: (fieldType: string) => `Enter ${fieldType}...`,
selectField: "Select...",
searchField: (fieldName: string) => `Search ${fieldName.toLowerCase()}...`,
enterKey: "Enter key...",
enterValue: "Enter value...",
},
helpers: {
formatOperator: (operator: string) => operator.replace(/_/g, " "),
},
validation: {
invalidEmail: "Invalid email format",
invalidUrl: "Invalid URL format",
invalidTel: "Invalid phone format",
invalid: "Invalid input format",
},
},
es: {
addFilter: "Agregar filtro",
searchFields: "Buscar campos...",
noFieldsFound: "No se encontraron campos.",
noResultsFound: "No se encontraron resultados.",
select: "Seleccionar...",
true: "Verdadero",
false: "Falso",
min: "Mín",
max: "Máx",
to: "a",
typeAndPressEnter: "Escriba y presione Enter para agregar etiqueta",
selected: "seleccionado",
selectedCount: "seleccionados",
percent: "%",
defaultCurrency: "€",
defaultColor: "#000000",
addFilterTitle: "Agregar filtro",
operators: {
is: "es",
isNot: "no es",
isAnyOf: "es cualquiera de",
isNotAnyOf: "no es cualquiera de",
includesAll: "incluye todos",
excludesAll: "excluye todos",
before: "antes de",
after: "después de",
between: "entre",
notBetween: "no entre",
contains: "contiene",
notContains: "no contiene",
startsWith: "comienza con",
endsWith: "termina con",
isExactly: "es exactamente",
equals: "igual a",
notEquals: "no igual a",
greaterThan: "mayor que",
lessThan: "menor que",
overlaps: "se superpone",
includes: "incluye",
excludes: "excluye",
includesAllOf: "incluye todos de",
includesAnyOf: "incluye cualquiera de",
empty: "está vacío",
notEmpty: "no está vacío",
},
placeholders: {
enterField: (fieldType: string) => `Ingrese ${fieldType}...`,
selectField: "Seleccionar...",
searchField: (fieldName: string) => `Buscar ${fieldName.toLowerCase()}...`,
enterKey: "Ingrese clave...",
enterValue: "Ingrese valor...",
},
helpers: {
formatOperator: (operator: string) => operator.replace(/_/g, " "),
},
validation: {
invalidEmail: "Formato de email inválido",
invalidUrl: "Formato de URL inválido",
invalidTel: "Formato de teléfono inválido",
invalid: "Formato de entrada inválido",
},
},
fr: {
addFilter: "Ajouter un filtre",
searchFields: "Rechercher des champs...",
noFieldsFound: "Aucun champ trouvé.",
noResultsFound: "Aucun résultat trouvé.",
select: "Sélectionner...",
true: "Vrai",
false: "Faux",
min: "Min",
max: "Max",
to: "à",
typeAndPressEnter: "Tapez et appuyez sur Entrée pour ajouter une étiquette",
selected: "sélectionné",
selectedCount: "sélectionnés",
percent: "%",
defaultCurrency: "€",
defaultColor: "#000000",
addFilterTitle: "Ajouter un filtre",
operators: {
is: "est",
isNot: "n'est pas",
isAnyOf: "est l'un de",
isNotAnyOf: "n'est pas l'un de",
includesAll: "inclut tous",
excludesAll: "exclut tous",
before: "avant",
after: "après",
between: "entre",
notBetween: "pas entre",
contains: "contient",
notContains: "ne contient pas",
startsWith: "commence par",
endsWith: "se termine par",
isExactly: "est exactement",
equals: "égal à",
notEquals: "pas égal à",
greaterThan: "supérieur à",
lessThan: "inférieur à",
overlaps: "se chevauche",
includes: "inclut",
excludes: "exclut",
includesAllOf: "inclut tous de",
includesAnyOf: "inclut l'un de",
empty: "est vide",
notEmpty: "n'est pas vide",
},
placeholders: {
enterField: (fieldType: string) => `Entrez ${fieldType}...`,
selectField: "Sélectionner...",
searchField: (fieldName: string) => `Rechercher ${fieldName.toLowerCase()}...`,
enterKey: "Entrez la clé...",
enterValue: "Entrez la valeur...",
},
helpers: {
formatOperator: (operator: string) => operator.replace(/_/g, " "),
},
validation: {
invalidEmail: "Format d'email invalide",
invalidUrl: "Format d'URL invalide",
invalidTel: "Format de téléphone invalide",
invalid: "Format de saisie invalide",
},
},
de: {
addFilter: "Filter hinzufügen",
searchFields: "Felder suchen...",
noFieldsFound: "Keine Felder gefunden.",
noResultsFound: "Keine Ergebnisse gefunden.",
select: "Auswählen...",
true: "Wahr",
false: "Falsch",
min: "Min",
max: "Max",
to: "bis",
typeAndPressEnter: "Tippen und Enter drücken, um Tag hinzuzufügen",
selected: "ausgewählt",
selectedCount: "ausgewählt",
percent: "%",
defaultCurrency: "€",
defaultColor: "#000000",
addFilterTitle: "Filter hinzufügen",
operators: {
is: "ist",
isNot: "ist nicht",
isAnyOf: "ist eines von",
isNotAnyOf: "ist nicht eines von",
includesAll: "enthält alle",
excludesAll: "schließt alle aus",
before: "vor",
after: "nach",
between: "zwischen",
notBetween: "nicht zwischen",
contains: "enthält",
notContains: "enthält nicht",
startsWith: "beginnt mit",
endsWith: "endet mit",
isExactly: "ist genau",
equals: "gleich",
notEquals: "nicht gleich",
greaterThan: "größer als",
lessThan: "kleiner als",
overlaps: "überschneidet sich",
includes: "enthält",
excludes: "schließt aus",
includesAllOf: "enthält alle von",
includesAnyOf: "enthält eines von",
empty: "ist leer",
notEmpty: "ist nicht leer",
},
placeholders: {
enterField: (fieldType: string) => `${fieldType} eingeben...`,
selectField: "Auswählen...",
searchField: (fieldName: string) => `${fieldName.toLowerCase()} suchen...`,
enterKey: "Schlüssel eingeben...",
enterValue: "Wert eingeben...",
},
helpers: {
formatOperator: (operator: string) => operator.replace(/_/g, " "),
},
validation: {
invalidEmail: "Ungültiges E-Mail-Format",
invalidUrl: "Ungültiges URL-Format",
invalidTel: "Ungültiges Telefonformat",
invalid: "Ungültiges Format",
},
},
ja: {
addFilter: "フィルターを追加",
searchFields: "フィールドを検索...",
noFieldsFound: "フィールドが見つかりません。",
noResultsFound: "結果が見つかりません。",
select: "選択...",
true: "真",
false: "偽",
min: "最小",
max: "最大",
to: "から",
typeAndPressEnter: "入力してEnterキーを押してタグを追加",
selected: "選択済み",
selectedCount: "選択済み",
percent: "%",
defaultCurrency: "¥",
defaultColor: "#000000",
addFilterTitle: "フィルターを追加",
operators: {
is: "は",
isNot: "ではない",
isAnyOf: "のいずれか",
isNotAnyOf: "のいずれでもない",
includesAll: "すべて含む",
excludesAll: "すべて除外",
before: "より前",
after: "より後",
between: "の間",
notBetween: "の間ではない",
contains: "含む",
notContains: "含まない",
startsWith: "で始まる",
endsWith: "で終わる",
isExactly: "正確に",
equals: "等しい",
notEquals: "等しくない",
greaterThan: "より大きい",
lessThan: "より小さい",
overlaps: "重複する",
includes: "含む",
excludes: "除外",
includesAllOf: "すべて含む",
includesAnyOf: "いずれか含む",
empty: "空",
notEmpty: "空でない",
},
placeholders: {
enterField: (fieldType: string) => `${fieldType}を入力...`,
selectField: "選択...",
searchField: (fieldName: string) => `${fieldName.toLowerCase()}を検索...`,
enterKey: "キーを入力...",
enterValue: "値を入力...",
},
helpers: {
formatOperator: (operator: string) => operator.replace(/_/g, " "),
},
validation: {
invalidEmail: "無効なメール形式",
invalidUrl: "無効なURL形式",
invalidTel: "無効な電話番号形式",
invalid: "無効な形式",
},
},
}
// Language options for the selector
const languageOptions = [
{ value: "en", label: "English", flag: "us" },
{ value: "es", label: "Español", flag: "es" },
{ value: "fr", label: "Français", flag: "fr" },
{ value: "de", label: "Deutsch", flag: "de" },
{ value: "ja", label: "日本語", flag: "jp" },
]
const currentLanguage = ref<string>("es")
const currentI18n = computed<FilterI18nConfig>(() => i18nConfigs[currentLanguage.value] ?? i18nConfigs.en!)
const currentLangOption = computed(() => languageOptions.find((l) => l.value === currentLanguage.value))
const fieldLabels: Record<string, Record<string, string>> = {
en: {
name: "Name",
email: "Email",
company: "Company",
role: "Role",
status: "Status",
location: "Location",
joined: "Joined Date",
balance: "Balance",
rating: "Rating",
},
es: {
name: "Nombre",
email: "Correo electrónico",
company: "Empresa",
role: "Rol",
status: "Estado",
location: "Ubicación",
joined: "Fecha de ingreso",
balance: "Saldo",
rating: "Calificación",
},
fr: {
name: "Nom",
email: "E-mail",
company: "Entreprise",
role: "Rôle",
status: "Statut",
location: "Localisation",
joined: "Date d'adhésion",
balance: "Solde",
rating: "Note",
},
de: {
name: "Name",
email: "E-Mail",
company: "Unternehmen",
role: "Rolle",
status: "Status",
location: "Standort",
joined: "Beitrittsdatum",
balance: "Guthaben",
rating: "Bewertung",
},
ja: {
name: "名前",
email: "メール",
company: "会社",
role: "役割",
status: "ステータス",
location: "場所",
joined: "参加日",
balance: "残高",
rating: "評価",
},
}
function localizedPlaceholder(kind: "names" | "locations"): string {
const lang = currentLanguage.value
if (kind === "names") {
if (lang === "en") return "Search names..."
if (lang === "es") return "Buscar nombres..."
if (lang === "fr") return "Rechercher des noms..."
if (lang === "de") return "Namen suchen..."
return "名前を検索..."
}
if (lang === "en") return "Search locations..."
if (lang === "es") return "Buscar ubicaciones..."
if (lang === "fr") return "Rechercher des lieux..."
if (lang === "de") return "Standorte suchen..."
return "場所を検索..."
}
function localizedStatus(value: "active" | "inactive"): string {
const lang = currentLanguage.value
if (value === "active") {
if (lang === "en") return "Active"
if (lang === "es") return "Activo"
if (lang === "fr") return "Actif"
if (lang === "de") return "Aktiv"
return "アクティブ"
}
if (lang === "en") return "Inactive"
if (lang === "es") return "Inactivo"
if (lang === "fr") return "Inactif"
if (lang === "de") return "Inaktiv"
return "非アクティブ"
}
const fields = computed<FilterFieldConfig<string>[]>(() => {
const labels = fieldLabels[currentLanguage.value] ?? fieldLabels.en!
return [
{
key: "name",
label: labels.name,
icon: UserIcon,
type: "text",
class: "w-40",
placeholder: localizedPlaceholder("names"),
},
{
key: "email",
label: labels.email,
icon: MailIcon,
type: "text",
class: "w-48",
placeholder: "user@example.com",
},
{
key: "company",
label: labels.company,
icon: BuildingIcon,
type: "select",
searchable: true,
class: "w-[180px]",
options: [
{ value: "TechCorp", label: "TechCorp" },
{ value: "StartupCo", label: "StartupCo" },
{ value: "BigCorp", label: "BigCorp" },
{ value: "InnovateTech", label: "InnovateTech" },
{ value: "GlobalNet", label: "GlobalNet" },
],
},
{
key: "role",
label: labels.role,
icon: UserIcon,
type: "select",
searchable: true,
class: "w-[160px]",
options: [
{ value: "Developer", label: "Developer" },
{ value: "Designer", label: "Designer" },
{ value: "Manager", label: "Manager" },
{ value: "Product Manager", label: "Product Manager" },
{ value: "Sales Rep", label: "Sales Rep" },
],
},
{
key: "status",
label: labels.status,
icon: CircleCheckIcon,
type: "select",
searchable: false,
class: "w-[140px]",
options: [
{ value: "active", label: localizedStatus("active") },
{ value: "inactive", label: localizedStatus("inactive") },
],
},
{
key: "location",
label: labels.location,
icon: MapPinIcon,
type: "text",
class: "w-40",
placeholder: localizedPlaceholder("locations"),
},
]
})
const filters = ref<Filter<string>[]>([createFilter("status", "is", ["active"])])
function handleFiltersChange(next: Filter<string>[]) {
filters.value = next
}
</script>
<template>
<div class="flex w-full grow items-start justify-between space-y-6 self-start">
<Filters :filters="filters" :fields="fields" size="sm" :i18n="currentI18n" @change="handleFiltersChange">
<template #trigger>
<Button variant="outline" size="icon-sm">
<ListFilterIcon />
</Button>
</template>
</Filters>
<div class="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline" size="sm" class="flex items-center gap-2">
<img
v-if="currentLangOption"
:src="`https://flagcdn.com/${currentLangOption.flag.toLowerCase()}.svg`"
:alt="currentLangOption.flag"
class="size-4 rounded-full object-cover"
/>
<span>{{ currentLangOption?.label }}</span>
<ChevronDownIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
v-for="lang in languageOptions"
:key="lang.value"
class="flex items-center gap-2"
@click="currentLanguage = lang.value"
>
<img
:src="`https://flagcdn.com/${lang.flag.toLowerCase()}.svg`"
:alt="lang.flag"
class="size-4 rounded-full object-cover"
/>
<span>{{ lang.label }}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</template>
Установка
npx shadcn-vue@latest add https://revueui.rootapi.dev/r/c-filters-9.jsonЗависимости реестра
Источник: порт из ReUI (Keenthemes, MIT)