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