New report system and views
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { computed, ref, watch } from "vue";
|
||||
import SkeletonTable from "@/Components/Skeleton/SkeletonTable.vue";
|
||||
import EmptyState from "@/Components/EmptyState.vue";
|
||||
import DataTablePaginationClient from "@/Components/DataTable/DataTablePaginationClient.vue";
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
@@ -10,6 +11,8 @@ import {
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from "@/Components/ui/table";
|
||||
import { Button } from "../ui/button";
|
||||
import { ArrowDownNarrowWide, ArrowUpWideNarrowIcon } from "lucide-vue-next";
|
||||
|
||||
const props = defineProps({
|
||||
columns: { type: Array, required: true }, // [{ key, label, sortable?, align?, class?, formatter? }]
|
||||
@@ -29,6 +32,7 @@ const props = defineProps({
|
||||
rowKey: { type: [String, Function], default: "id" },
|
||||
showToolbar: { type: Boolean, default: true },
|
||||
// Pagination UX options
|
||||
showPagination: { type: Boolean, default: true },
|
||||
showPageStats: { type: Boolean, default: true },
|
||||
showGoto: { type: Boolean, default: true },
|
||||
maxPageLinks: { type: Number, default: 5 }, // odd number preferred
|
||||
@@ -139,29 +143,6 @@ const pageRows = computed(() => sortedRows.value.slice(startIndex.value, endInde
|
||||
const showingFrom = computed(() => (total.value === 0 ? 0 : startIndex.value + 1));
|
||||
const showingTo = computed(() => (total.value === 0 ? 0 : endIndex.value));
|
||||
|
||||
const gotoInput = ref("");
|
||||
function goToPageInput() {
|
||||
const raw = String(gotoInput.value || "").trim();
|
||||
const n = Number(raw);
|
||||
if (!Number.isFinite(n)) return;
|
||||
const target = Math.max(1, Math.min(lastPage.value, Math.floor(n)));
|
||||
if (target !== currentPage.value) setPage(target);
|
||||
gotoInput.value = "";
|
||||
}
|
||||
|
||||
const visiblePages = computed(() => {
|
||||
const pages = [];
|
||||
const count = lastPage.value;
|
||||
if (count <= 1) return [1];
|
||||
const windowSize = Math.max(3, props.maxPageLinks);
|
||||
const half = Math.floor(windowSize / 2);
|
||||
let start = Math.max(1, currentPage.value - half);
|
||||
let end = Math.min(count, start + windowSize - 1);
|
||||
start = Math.max(1, Math.min(start, end - windowSize + 1));
|
||||
for (let p = start; p <= end; p++) pages.push(p);
|
||||
return pages;
|
||||
});
|
||||
|
||||
function setPage(p) {
|
||||
emit("update:page", Math.min(Math.max(1, p), lastPage.value));
|
||||
}
|
||||
@@ -196,28 +177,26 @@ function setPageSize(ps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="relative overflow-x-auto rounded-lg border border-gray-200 bg-white shadow-sm"
|
||||
>
|
||||
<Table class="text-sm">
|
||||
<TableHeader
|
||||
class="sticky top-0 z-10 bg-gray-50/90 backdrop-blur border-b border-gray-200 shadow-sm"
|
||||
>
|
||||
<div>
|
||||
<Table class="border-t">
|
||||
<TableHeader>
|
||||
<TableRow class="border-b">
|
||||
<TableHead v-for="col in columns" :key="col.key" :class="col.class">
|
||||
<button
|
||||
<Button
|
||||
v-if="col.sortable"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 hover:text-indigo-600"
|
||||
variant="ghost"
|
||||
class="text-left gap-1 p-1"
|
||||
@click="toggleSort(col)"
|
||||
:aria-sort="sort?.key === col.key ? sort.direction || 'none' : 'none'"
|
||||
>
|
||||
<span class="uppercase">{{ col.label }}</span>
|
||||
<span v-if="sort?.key === col.key && sort.direction === 'asc'">▲</span>
|
||||
<span v-if="sort?.key === col.key && sort.direction === 'asc'"
|
||||
><ArrowDownNarrowWide
|
||||
/></span>
|
||||
<span v-else-if="sort?.key === col.key && sort.direction === 'desc'"
|
||||
>▼</span
|
||||
>
|
||||
</button>
|
||||
><ArrowUpWideNarrowIcon
|
||||
/></span>
|
||||
</Button>
|
||||
<span v-else>{{ col.label }}</span>
|
||||
</TableHead>
|
||||
<TableHead v-if="$slots.actions" class="w-px"> </TableHead>
|
||||
@@ -232,11 +211,7 @@ function setPageSize(ps) {
|
||||
@click="$emit('row:click', row)"
|
||||
class="cursor-default hover:bg-gray-50/50"
|
||||
>
|
||||
<TableCell
|
||||
v-for="col in columns"
|
||||
:key="col.key"
|
||||
:class="col.class"
|
||||
>
|
||||
<TableCell v-for="col in columns" :key="col.key" :class="col.class">
|
||||
<template v-if="$slots['cell-' + col.key]">
|
||||
<slot
|
||||
:name="'cell-' + col.key"
|
||||
@@ -275,10 +250,7 @@ function setPageSize(ps) {
|
||||
<TableRow>
|
||||
<TableCell :colspan="columns.length + ($slots.actions ? 1 : 0)">
|
||||
<slot name="empty">
|
||||
<EmptyState
|
||||
:title="emptyText"
|
||||
size="sm"
|
||||
/>
|
||||
<EmptyState :title="emptyText" size="sm" />
|
||||
</slot>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -286,112 +258,19 @@ function setPageSize(ps) {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<nav
|
||||
v-if="showPagination"
|
||||
class="mt-3 flex flex-wrap items-center justify-between gap-3 text-sm text-gray-700"
|
||||
aria-label="Pagination"
|
||||
>
|
||||
<div v-if="showPageStats">
|
||||
<span v-if="total > 0"
|
||||
>Prikazano: {{ showingFrom }}–{{ showingTo }} od {{ total }}</span
|
||||
>
|
||||
<span v-else>Ni zadetkov</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- First -->
|
||||
<button
|
||||
class="px-2 py-1 rounded border border-gray-300 hover:bg-gray-50 disabled:opacity-50"
|
||||
:disabled="currentPage <= 1"
|
||||
@click="setPage(1)"
|
||||
aria-label="Prva stran"
|
||||
>
|
||||
««
|
||||
</button>
|
||||
<!-- Prev -->
|
||||
<button
|
||||
class="px-2 py-1 rounded border border-gray-300 hover:bg-gray-50 disabled:opacity-50"
|
||||
:disabled="currentPage <= 1"
|
||||
@click="setPage(currentPage - 1)"
|
||||
aria-label="Prejšnja stran"
|
||||
>
|
||||
«
|
||||
</button>
|
||||
|
||||
<!-- Leading ellipsis / first page when window doesn't include 1 -->
|
||||
<button
|
||||
v-if="visiblePages[0] > 1"
|
||||
class="px-2 py-1 rounded border border-gray-300 hover:bg-gray-50"
|
||||
@click="setPage(1)"
|
||||
>
|
||||
1
|
||||
</button>
|
||||
<span v-if="visiblePages[0] > 2" class="px-1">…</span>
|
||||
|
||||
<!-- Page numbers -->
|
||||
<button
|
||||
v-for="p in visiblePages"
|
||||
:key="p"
|
||||
class="px-3 py-1 rounded border transition-colors"
|
||||
:class="
|
||||
p === currentPage
|
||||
? 'border-indigo-600 bg-indigo-600 text-white'
|
||||
: 'border-gray-300 hover:bg-gray-50'
|
||||
"
|
||||
:aria-current="p === currentPage ? 'page' : undefined"
|
||||
@click="setPage(p)"
|
||||
>
|
||||
{{ p }}
|
||||
</button>
|
||||
|
||||
<!-- Trailing ellipsis / last page when window doesn't include last -->
|
||||
<span v-if="visiblePages[visiblePages.length - 1] < lastPage - 1" class="px-1"
|
||||
>…</span
|
||||
>
|
||||
<button
|
||||
v-if="visiblePages[visiblePages.length - 1] < lastPage"
|
||||
class="px-2 py-1 rounded border border-gray-300 hover:bg-gray-50"
|
||||
@click="setPage(lastPage)"
|
||||
>
|
||||
{{ lastPage }}
|
||||
</button>
|
||||
|
||||
<!-- Next -->
|
||||
<button
|
||||
class="px-2 py-1 rounded border border-gray-300 hover:bg-gray-50 disabled:opacity-50"
|
||||
:disabled="currentPage >= lastPage"
|
||||
@click="setPage(currentPage + 1)"
|
||||
aria-label="Naslednja stran"
|
||||
>
|
||||
»
|
||||
</button>
|
||||
<!-- Last -->
|
||||
<button
|
||||
class="px-2 py-1 rounded border border-gray-300 hover:bg-gray-50 disabled:opacity-50"
|
||||
:disabled="currentPage >= lastPage"
|
||||
@click="setPage(lastPage)"
|
||||
aria-label="Zadnja stran"
|
||||
>
|
||||
»»
|
||||
</button>
|
||||
|
||||
<!-- Goto page -->
|
||||
<div v-if="showGoto" class="ms-2 flex items-center gap-1">
|
||||
<input
|
||||
v-model="gotoInput"
|
||||
type="number"
|
||||
min="1"
|
||||
:max="lastPage"
|
||||
inputmode="numeric"
|
||||
class="w-16 rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 text-sm"
|
||||
:placeholder="String(currentPage)"
|
||||
aria-label="Pojdi na stran"
|
||||
@keyup.enter="goToPageInput"
|
||||
@blur="goToPageInput"
|
||||
/>
|
||||
<span class="text-gray-500">/ {{ lastPage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="px-2 py-4 border-t">
|
||||
<DataTablePaginationClient
|
||||
v-if="showPagination"
|
||||
:current-page="currentPage"
|
||||
:last-page="lastPage"
|
||||
:total="total"
|
||||
:showing-from="showingFrom"
|
||||
:showing-to="showingTo"
|
||||
:show-page-stats="showPageStats"
|
||||
:show-goto="showGoto"
|
||||
:max-page-links="maxPageLinks"
|
||||
@update:page="setPage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-vue-next";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationEllipsis,
|
||||
PaginationFirst,
|
||||
PaginationItem,
|
||||
PaginationLast,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/Components/ui/pagination";
|
||||
import { Separator } from "@/Components/ui/separator";
|
||||
|
||||
const props = defineProps({
|
||||
currentPage: { type: Number, required: true },
|
||||
lastPage: { type: Number, required: true },
|
||||
total: { type: Number, required: true },
|
||||
showingFrom: { type: Number, required: true },
|
||||
showingTo: { type: Number, required: true },
|
||||
showPageStats: { type: Boolean, default: true },
|
||||
showGoto: { type: Boolean, default: true },
|
||||
maxPageLinks: { type: Number, default: 5 },
|
||||
perPage: { type: Number, default: 10 },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:page"]);
|
||||
|
||||
const gotoInput = ref("");
|
||||
|
||||
function goToPageInput() {
|
||||
const raw = String(gotoInput.value || "").trim();
|
||||
const n = Number(raw);
|
||||
if (!Number.isFinite(n)) return;
|
||||
const target = Math.max(1, Math.min(props.lastPage, Math.floor(n)));
|
||||
if (target !== props.currentPage) setPage(target);
|
||||
gotoInput.value = "";
|
||||
}
|
||||
|
||||
const visiblePages = computed(() => {
|
||||
const pages = [];
|
||||
const count = props.lastPage;
|
||||
if (count <= 1) return [1];
|
||||
const windowSize = Math.max(3, props.maxPageLinks);
|
||||
const half = Math.floor(windowSize / 2);
|
||||
let start = Math.max(1, props.currentPage - half);
|
||||
let end = Math.min(count, start + windowSize - 1);
|
||||
start = Math.max(1, Math.min(start, end - windowSize + 1));
|
||||
|
||||
// Handle first page
|
||||
if (start > 1) {
|
||||
pages.push(1);
|
||||
if (start > 2) pages.push("...");
|
||||
}
|
||||
|
||||
// Add pages in window
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
// Handle last page
|
||||
if (end < count) {
|
||||
if (end < count - 1) pages.push("...");
|
||||
pages.push(count);
|
||||
}
|
||||
|
||||
return pages;
|
||||
});
|
||||
|
||||
function setPage(p) {
|
||||
emit("update:page", Math.min(Math.max(1, p), props.lastPage));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav
|
||||
class="flex flex-wrap items-center justify-between gap-3 px-2 text-sm text-gray-700 sm:px-1"
|
||||
aria-label="Pagination"
|
||||
>
|
||||
<!-- Mobile: Simple prev/next -->
|
||||
<div class="flex flex-1 justify-between sm:hidden">
|
||||
<button
|
||||
@click="setPage(currentPage - 1)"
|
||||
:disabled="currentPage <= 1"
|
||||
class="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Prejšnja
|
||||
</button>
|
||||
<button
|
||||
@click="setPage(currentPage + 1)"
|
||||
:disabled="currentPage >= lastPage"
|
||||
class="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Naslednja
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Desktop: Full pagination -->
|
||||
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
|
||||
<!-- Page stats with modern badge style -->
|
||||
<div v-if="showPageStats">
|
||||
<div v-if="total > 0" class="flex items-center gap-2">
|
||||
<span class="text-sm text-muted-foreground">Prikazano</span>
|
||||
<div
|
||||
class="inline-flex items-center gap-1 rounded-md bg-muted px-2 py-1 text-sm font-medium"
|
||||
>
|
||||
<span class="text-foreground">{{ showingFrom }}</span>
|
||||
<span class="text-muted-foreground">-</span>
|
||||
<span class="text-foreground">{{ showingTo }}</span>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">od</span>
|
||||
<div
|
||||
class="inline-flex items-center rounded-md bg-primary/10 px-2.5 py-1 text-sm font-semibold text-primary"
|
||||
>
|
||||
{{ total }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="inline-flex items-center gap-2 rounded-lg bg-muted/50 px-3 py-1.5"
|
||||
>
|
||||
<span class="text-sm font-medium text-muted-foreground">Ni zadetkov</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination controls -->
|
||||
<Pagination
|
||||
v-slot="{ page }"
|
||||
:total="total"
|
||||
:items-per-page="perPage"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
:default-page="currentPage"
|
||||
:page="currentPage"
|
||||
>
|
||||
<PaginationContent>
|
||||
<!-- First -->
|
||||
<PaginationFirst :disabled="currentPage <= 1" @click="setPage(1)">
|
||||
<ChevronsLeft />
|
||||
</PaginationFirst>
|
||||
|
||||
<!-- Previous -->
|
||||
<PaginationPrevious
|
||||
:disabled="currentPage <= 1"
|
||||
@click="setPage(currentPage - 1)"
|
||||
>
|
||||
<ChevronLeft />
|
||||
</PaginationPrevious>
|
||||
|
||||
<!-- Page numbers -->
|
||||
<template v-for="(item, index) in visiblePages" :key="index">
|
||||
<PaginationEllipsis v-if="item === '...'" />
|
||||
<PaginationItem
|
||||
v-else
|
||||
:value="item"
|
||||
:is-active="currentPage === item"
|
||||
@click="setPage(item)"
|
||||
>
|
||||
{{ item }}
|
||||
</PaginationItem>
|
||||
</template>
|
||||
|
||||
<!-- Next -->
|
||||
<PaginationNext
|
||||
:disabled="currentPage >= lastPage"
|
||||
@click="setPage(currentPage + 1)"
|
||||
>
|
||||
<ChevronRight />
|
||||
</PaginationNext>
|
||||
|
||||
<!-- Last -->
|
||||
<PaginationLast
|
||||
:disabled="currentPage >= lastPage"
|
||||
@click="setPage(lastPage)"
|
||||
>
|
||||
<ChevronsRight />
|
||||
</PaginationLast>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
|
||||
<!-- Goto page input -->
|
||||
<div v-if="showGoto" class="flex items-center gap-3">
|
||||
<div
|
||||
class="inline-flex items-center gap-2 rounded-md border border-input bg-background px-2 h-8"
|
||||
>
|
||||
<input
|
||||
v-model="gotoInput"
|
||||
type="number"
|
||||
min="1"
|
||||
:max="lastPage"
|
||||
inputmode="numeric"
|
||||
class="w-10 h-full text-sm text-center bg-transparent border-0 outline-none focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||
:placeholder="String(currentPage)"
|
||||
aria-label="Pojdi na stran"
|
||||
@keyup.enter="goToPageInput"
|
||||
@blur="goToPageInput"
|
||||
/>
|
||||
<Separator orientation="vertical" class="h-full" />
|
||||
<span class="text-sm text-muted-foreground">{{ lastPage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { Checkbox } from "@/Components/ui/checkbox";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [Boolean, Array], required: true },
|
||||
value: { type: [String, Number], required: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
id: { type: String, required: false },
|
||||
class: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const isChecked = computed(() => {
|
||||
if (Array.isArray(props.modelValue)) {
|
||||
return props.modelValue.includes(props.value);
|
||||
}
|
||||
return props.modelValue;
|
||||
});
|
||||
|
||||
function handleChange(checked) {
|
||||
if (Array.isArray(props.modelValue)) {
|
||||
const newValue = [...props.modelValue];
|
||||
if (checked) {
|
||||
if (!newValue.includes(props.value)) {
|
||||
newValue.push(props.value);
|
||||
}
|
||||
} else {
|
||||
const index = newValue.indexOf(props.value);
|
||||
if (index > -1) {
|
||||
newValue.splice(index, 1);
|
||||
}
|
||||
}
|
||||
emit("update:modelValue", newValue);
|
||||
} else {
|
||||
emit("update:modelValue", checked);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Checkbox
|
||||
:model-value="isChecked"
|
||||
@update:model-value="handleChange"
|
||||
:disabled="disabled"
|
||||
:id="id"
|
||||
:class="class"
|
||||
/>
|
||||
</template>
|
||||
@@ -55,6 +55,7 @@ const selectedItem = computed(() =>
|
||||
function selectItem(selectedValue) {
|
||||
const newValue = selectedValue === props.modelValue ? "" : selectedValue;
|
||||
emit("update:modelValue", newValue);
|
||||
console.log(selectedValue);
|
||||
open.value = false;
|
||||
}
|
||||
</script>
|
||||
@@ -83,7 +84,11 @@ function selectItem(selectedValue) {
|
||||
v-for="item in items"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
@select="selectItem"
|
||||
@select="
|
||||
(ev) => {
|
||||
selectItem(ev.detail.value);
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ item.label }}
|
||||
<CheckIcon
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
const props = defineProps({
|
||||
variant: {
|
||||
type: String,
|
||||
default: "default",
|
||||
validator: (value) => ["default", "destructive"].includes(value),
|
||||
},
|
||||
class: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn(alertVariants({ variant }), props.class)" role="alert">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
class: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('text-sm [&_p]:leading-relaxed', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
class: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h5 :class="cn('mb-1 font-medium leading-none tracking-tight', props.class)">
|
||||
<slot />
|
||||
</h5>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as Alert } from "./Alert.vue";
|
||||
export { default as AlertTitle } from "./AlertTitle.vue";
|
||||
export { default as AlertDescription } from "./AlertDescription.vue";
|
||||
@@ -0,0 +1,50 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { Checkbox } from "@/Components/ui/checkbox";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [Boolean, Array], required: true },
|
||||
value: { type: [String, Number], required: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
id: { type: String, required: false },
|
||||
class: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const isChecked = computed(() => {
|
||||
if (Array.isArray(props.modelValue)) {
|
||||
return props.modelValue.includes(props.value);
|
||||
}
|
||||
return props.modelValue;
|
||||
});
|
||||
|
||||
function handleChange(checked) {
|
||||
if (Array.isArray(props.modelValue)) {
|
||||
const newValue = [...props.modelValue];
|
||||
if (checked) {
|
||||
if (!newValue.includes(props.value)) {
|
||||
newValue.push(props.value);
|
||||
}
|
||||
} else {
|
||||
const index = newValue.indexOf(props.value);
|
||||
if (index > -1) {
|
||||
newValue.splice(index, 1);
|
||||
}
|
||||
}
|
||||
emit("update:modelValue", newValue);
|
||||
} else {
|
||||
emit("update:modelValue", checked);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Checkbox
|
||||
:model-value="isChecked"
|
||||
@update:model-value="handleChange"
|
||||
:disabled="disabled"
|
||||
:id="id"
|
||||
:class="class"
|
||||
/>
|
||||
</template>
|
||||
Reference in New Issue
Block a user