2024-09-23 15:03:54 +07:00
|
|
|
<script setup lang="ts">
|
|
|
|
|
const search = defineModel<string>('search');
|
|
|
|
|
const selectedItem = defineModel<unknown[]>('selectedItem', { default: [] });
|
|
|
|
|
|
|
|
|
|
const props = withDefaults(
|
|
|
|
|
defineProps<{
|
2024-09-24 11:25:39 +07:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
items: any;
|
2024-10-07 12:55:39 +07:00
|
|
|
newItems: any;
|
2024-09-23 15:03:54 +07:00
|
|
|
color?: string;
|
2024-10-02 15:06:39 +07:00
|
|
|
borderSearchSection?: boolean;
|
|
|
|
|
itemClass?: string;
|
|
|
|
|
noPadding?: boolean;
|
|
|
|
|
noItemsIcon?: string;
|
|
|
|
|
noItemsLabel?: string;
|
2024-09-23 15:03:54 +07:00
|
|
|
}>(),
|
|
|
|
|
{
|
|
|
|
|
items: () => [],
|
2024-10-07 12:55:39 +07:00
|
|
|
newItems: () => [],
|
2024-09-23 15:03:54 +07:00
|
|
|
color: 'var(--brand-1)',
|
2024-10-02 15:06:39 +07:00
|
|
|
noItemsIcon: 'mdi-close',
|
|
|
|
|
itemClass: 'col-md-2 col-sm-6 col-12',
|
2024-09-23 15:03:54 +07:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2024-10-03 18:10:40 +07:00
|
|
|
defineExpose({ select, assignSelect });
|
2024-10-02 15:06:39 +07:00
|
|
|
|
|
|
|
|
function select(item?: unknown, all?: boolean) {
|
|
|
|
|
if (all) {
|
|
|
|
|
if (props.items.every((item) => selectedItem.value.includes(item))) {
|
|
|
|
|
selectedItem.value = selectedItem.value.filter(
|
|
|
|
|
(item) => !props.items.includes(item),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
props.items.forEach((i) => {
|
|
|
|
|
const productExists = selectedItem.value.some((item) => item === i);
|
|
|
|
|
if (!productExists) {
|
|
|
|
|
selectedItem.value.push(i);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (selectedItem.value.includes(item)) {
|
|
|
|
|
const index = selectedItem.value.indexOf(item);
|
|
|
|
|
selectedItem.value.splice(index, 1);
|
|
|
|
|
} else selectedItem.value.push(item);
|
|
|
|
|
}
|
2024-09-23 15:03:54 +07:00
|
|
|
}
|
2024-10-03 18:10:40 +07:00
|
|
|
|
|
|
|
|
function assignSelect(to: unknown[], from: unknown[]) {
|
|
|
|
|
const existingItems = new Set(to);
|
|
|
|
|
|
|
|
|
|
for (let i = to.length - 1; i >= 0; i--) {
|
|
|
|
|
if (!from.includes(to[i])) {
|
|
|
|
|
to.splice(i, 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newItems = from.filter((item) => !existingItems.has(item));
|
|
|
|
|
to.push(...newItems);
|
|
|
|
|
}
|
2024-09-23 15:03:54 +07:00
|
|
|
</script>
|
|
|
|
|
<template>
|
2024-10-02 15:06:39 +07:00
|
|
|
<section class="full-width column">
|
|
|
|
|
<header
|
|
|
|
|
class="row items-center no-wrap q-px-md q-py-sm"
|
|
|
|
|
:class="{ 'bordered surface-3 ': borderSearchSection }"
|
|
|
|
|
>
|
2024-09-23 15:03:54 +07:00
|
|
|
<div class="col"><slot name="top"></slot></div>
|
|
|
|
|
<q-input
|
|
|
|
|
for="input-search"
|
|
|
|
|
outlined
|
|
|
|
|
dense
|
|
|
|
|
:label="$t('general.search')"
|
|
|
|
|
class="q-ml-auto"
|
|
|
|
|
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
|
|
|
|
v-model="search"
|
|
|
|
|
debounce="200"
|
|
|
|
|
>
|
|
|
|
|
<template #prepend>
|
|
|
|
|
<q-icon name="mdi-magnify" />
|
|
|
|
|
</template>
|
|
|
|
|
</q-input>
|
|
|
|
|
</header>
|
2024-10-02 15:06:39 +07:00
|
|
|
<slot name="tab"></slot>
|
2024-09-23 15:03:54 +07:00
|
|
|
|
2024-10-02 15:06:39 +07:00
|
|
|
<section class="col q-ma-md">
|
2024-10-07 12:55:39 +07:00
|
|
|
<div v-if="items.length > 0 || newItems.length > 0" class="column">
|
|
|
|
|
<!-- NOTE: START - item -->
|
|
|
|
|
<div class="row q-col-gutter-md">
|
|
|
|
|
<div v-for="(item, i) in items" :key="i" :class="`${itemClass}`">
|
2024-09-23 15:03:54 +07:00
|
|
|
<div
|
2024-10-07 12:55:39 +07:00
|
|
|
class="rounded cursor-pointer relative-position"
|
|
|
|
|
:style="`border: 1px solid ${selectedItem.includes(item) ? color : 'transparent'}`"
|
|
|
|
|
@click="() => select(item)"
|
2024-09-23 15:03:54 +07:00
|
|
|
>
|
2024-10-07 12:55:39 +07:00
|
|
|
<div
|
|
|
|
|
v-if="selectedItem.includes(item)"
|
|
|
|
|
class="badge absolute-top-right flex justify-center q-ma-sm"
|
|
|
|
|
:style="`background-color: ${color}`"
|
|
|
|
|
>
|
|
|
|
|
{{ selectedItem.indexOf(item) + 1 }}
|
|
|
|
|
</div>
|
|
|
|
|
<slot name="data" :item="item"></slot>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- NOTE: END - item -->
|
|
|
|
|
|
|
|
|
|
<q-separator
|
|
|
|
|
v-if="newItems.length !== 0"
|
|
|
|
|
dark
|
|
|
|
|
inset
|
|
|
|
|
:class="{ 'q-mb-md': !$q.screen.lt.md, 'q-my-md': $q.screen.lt.md }"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<!-- NOTE: START - newItem -->
|
|
|
|
|
<div v-if="newItems.length !== 0" class="row q-col-gutter-md">
|
|
|
|
|
<div v-for="(item, i) in newItems" :key="i" :class="`${itemClass}`">
|
|
|
|
|
<div
|
|
|
|
|
class="rounded cursor-pointer relative-position"
|
|
|
|
|
:style="`border: 1px solid ${selectedItem.includes(item) ? color : 'transparent'}`"
|
|
|
|
|
@click="() => select(item)"
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
v-if="selectedItem.includes(item)"
|
|
|
|
|
class="badge absolute-top-right flex justify-center q-ma-sm"
|
|
|
|
|
:style="`background-color: ${color}`"
|
|
|
|
|
>
|
|
|
|
|
{{ selectedItem.indexOf(item) + 1 }}
|
|
|
|
|
</div>
|
|
|
|
|
<slot name="newData" :item="item"></slot>
|
2024-09-23 15:03:54 +07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2024-10-07 12:55:39 +07:00
|
|
|
<!-- NOTE: END - newItem -->
|
|
|
|
|
|
2024-10-02 15:06:39 +07:00
|
|
|
<div v-else class="flex justify-center full-height">
|
|
|
|
|
<span class="col column items-center justify-center app-text-muted">
|
|
|
|
|
<q-avatar style="background: var(--surface-0)" class="q-mb-md">
|
|
|
|
|
<q-icon :name="noItemsIcon" />
|
|
|
|
|
</q-avatar>
|
|
|
|
|
{{ noItemsLabel || $t('general.noData') }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
2024-09-23 15:03:54 +07:00
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
</template>
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
.badge {
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
width: 20px;
|
|
|
|
|
height: 20px;
|
|
|
|
|
color: var(--surface-1);
|
|
|
|
|
}
|
|
|
|
|
</style>
|