feat: add shared select dialog
This commit is contained in:
parent
cd1848c5fb
commit
894900bd71
1 changed files with 193 additions and 0 deletions
193
src/components/dialog/DialogSelect.vue
Normal file
193
src/components/dialog/DialogSelect.vue
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
<script setup lang="ts" generic="T extends { id: string }">
|
||||
import { Ref } from 'vue';
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
|
||||
import { CancelButton, MainButton } from 'components/button';
|
||||
import DialogContainer from './DialogContainer.vue';
|
||||
import DialogHeader from './DialogHeader.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
getList: (search?: string) => T[] | Promise<T[]>;
|
||||
|
||||
dialog: {
|
||||
title: string;
|
||||
};
|
||||
|
||||
preselectedItem?: T[];
|
||||
disabledItemId?: T['id'][];
|
||||
|
||||
/** @returns true - to auto close dialog */
|
||||
onSubmit: (selected: T[]) => void | boolean | Promise<void | boolean>;
|
||||
}>();
|
||||
|
||||
const open = defineModel<boolean>('open', { default: false });
|
||||
const list: Ref<T[]> = ref([]);
|
||||
const selected: Ref<T[]> = ref([]);
|
||||
|
||||
const state = reactive({
|
||||
search: '',
|
||||
});
|
||||
|
||||
function clean() {
|
||||
list.value = [];
|
||||
selected.value = [];
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
function selectedIndex(item: T) {
|
||||
return selected.value.findIndex((v) => v.id === item.id);
|
||||
}
|
||||
|
||||
function toggleSelect(item: T) {
|
||||
if (props.disabledItemId?.some((id) => id === item.id)) return;
|
||||
|
||||
const index = selectedIndex(item);
|
||||
|
||||
if (index === -1) {
|
||||
selected.value.push(item);
|
||||
} else {
|
||||
selected.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (props.preselectedItem) {
|
||||
selected.value = JSON.parse(JSON.stringify(props.preselectedItem));
|
||||
}
|
||||
getList();
|
||||
}
|
||||
|
||||
async function getList() {
|
||||
list.value = await props.getList(state.search);
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
const result = await props.onSubmit(selected.value);
|
||||
if (typeof result === 'boolean') open.value = !result;
|
||||
}
|
||||
|
||||
watch(() => state.search, getList);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogContainer v-model="open" @open="init" @close="clean">
|
||||
<template #header>
|
||||
<DialogHeader :title="dialog.title">
|
||||
<template #title-before>
|
||||
<span class="q-mr-auto"></span>
|
||||
</template>
|
||||
<template #title-after>
|
||||
<span class="q-ml-auto" v-if="!$slots['title-after']" />
|
||||
<div class="q-ml-auto" v-else><slot name="title-after" /></div>
|
||||
</template>
|
||||
</DialogHeader>
|
||||
</template>
|
||||
<div class="column q-pa-md full-height">
|
||||
<section class="row justify-end q-mb-md">
|
||||
<q-input
|
||||
for="input-search"
|
||||
outlined
|
||||
dense
|
||||
:label="$t('general.search')"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="state.search"
|
||||
debounce="500"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
</q-input>
|
||||
</section>
|
||||
<!-- wrapper -->
|
||||
<div class="col scroll">
|
||||
<section
|
||||
:class="{ ['items-center']: list.length === 0 }"
|
||||
class="row q-col-gutter-md"
|
||||
>
|
||||
<div
|
||||
style="display: inline-block; margin-inline: auto"
|
||||
v-if="list.length === 0"
|
||||
>
|
||||
<slot name="empty" :data="{ notFound: !!state.search }" />
|
||||
</div>
|
||||
<div
|
||||
:key="item.id"
|
||||
v-for="(item, index) in list.map((data) => ({
|
||||
...data,
|
||||
_selectedIndex: selectedIndex(data),
|
||||
}))"
|
||||
class="col-2"
|
||||
>
|
||||
<button
|
||||
class="selectable-item full-width"
|
||||
:class="{
|
||||
['selectable-item__selected']: item._selectedIndex !== -1,
|
||||
['selectable-item__disabled']: disabledItemId?.some(
|
||||
(id) => id === item.id,
|
||||
),
|
||||
}"
|
||||
@click="toggleSelect(item)"
|
||||
>
|
||||
<span class="selectable-item__pos">
|
||||
{{ item._selectedIndex + 1 }}
|
||||
</span>
|
||||
<slot name="card" :data="{ index, item }" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="q-gutter-x-xs q-ml-auto">
|
||||
<CancelButton outlined @click="clean" />
|
||||
<MainButton icon="mdi-check" color="207 96% 32%" solid @click="submit">
|
||||
{{ $t('general.select') }}
|
||||
</MainButton>
|
||||
</div>
|
||||
</template>
|
||||
</DialogContainer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.selectable-item {
|
||||
padding: 0;
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
color: inherit;
|
||||
|
||||
& > .selectable-item__pos {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.selectable-item__selected {
|
||||
& > :deep(*) {
|
||||
border: 1px solid var(--_color, var(--brand-1)) !important;
|
||||
}
|
||||
|
||||
& > .selectable-item__pos {
|
||||
display: block;
|
||||
position: absolute;
|
||||
margin: var(--size-2);
|
||||
right: 0;
|
||||
top: 0;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--surface-1);
|
||||
background: var(--brand-1);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.selectable-item__disabled {
|
||||
filter: grayscale(1);
|
||||
opacity: 0.5;
|
||||
|
||||
& :deep(*) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue