filter
This commit is contained in:
1 parent
4a124a2bb7
commit
bb4ddf3f36
3 files changed
+151
-169
No files matched your search
@@ -1,20 +1,148 @@
|
|||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { itemsList } from '@/api/user.api.ts'
|
||||||
|
import type { User } from '@/api/user.api.ts'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
selected?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), { selected: () => [] })
|
||||||
|
const emit = defineEmits<{ changed: [value: string[]] }>()
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20
|
||||||
|
const AVATAR_LIMIT = 5
|
||||||
|
|
||||||
|
const users = ref<User[]>([])
|
||||||
|
const page = ref(0)
|
||||||
|
const hasMore = ref(true)
|
||||||
|
const totalCount = ref(0)
|
||||||
|
const hoveredId = ref<string | null>(null)
|
||||||
|
|
||||||
|
const avatarUsers = computed(() => users.value.slice(0, AVATAR_LIMIT))
|
||||||
|
const extraCount = computed(() => Math.max(totalCount.value - AVATAR_LIMIT, 0))
|
||||||
|
|
||||||
|
function initials(user: Pick<User, 'name' | 'email'>) {
|
||||||
|
return (user.name || user.email || '?')[0]!.toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelected(id: string) {
|
||||||
|
return props.selected.includes(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(id: string) {
|
||||||
|
const next = isSelected(id) ? props.selected.filter((el) => el !== id) : [...props.selected, id]
|
||||||
|
emit('changed', next)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMenuSelect(value: unknown) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
emit('changed', value.filter((el): el is string => typeof el === 'string'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMore({ done }: { done: (status: 'ok' | 'empty' | 'error') => void }) {
|
||||||
|
if (!hasMore.value) {
|
||||||
|
done('empty')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const nextPage = page.value + 1
|
||||||
|
const res = await itemsList({ page: nextPage, limit: PAGE_SIZE })
|
||||||
|
users.value.push(...res.list)
|
||||||
|
page.value = nextPage
|
||||||
|
totalCount.value = res.meta.totalCount
|
||||||
|
hasMore.value = nextPage < res.meta.totalPages
|
||||||
|
done(hasMore.value ? 'ok' : 'empty')
|
||||||
|
} catch {
|
||||||
|
done('error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadMore({ done: () => {} })
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-select
|
<div class="d-flex align-center">
|
||||||
variant="outlined"
|
<v-avatar
|
||||||
clearable
|
v-for="(user, index) in avatarUsers"
|
||||||
:items="[
|
:key="user.id"
|
||||||
{ id: 1, image: 'A' },
|
rounded="circle"
|
||||||
{ id: 2, image: 'B' },
|
border
|
||||||
{ id: 3, image: 'C' },
|
density="compact"
|
||||||
{ id: 4, image: 'D' },
|
:image="user.image ?? undefined"
|
||||||
]"
|
:text="initials(user)"
|
||||||
>
|
class="stack-avatar cursor-pointer"
|
||||||
<template #item>
|
:class="{ 'bg-primary': isSelected(user.id) }"
|
||||||
<div>a</div>
|
:style="{ zIndex: hoveredId === user.id ? avatarUsers.length + 1 : avatarUsers.length - index }"
|
||||||
</template>
|
@mouseenter="hoveredId = user.id"
|
||||||
</v-select>
|
@mouseleave="hoveredId = null"
|
||||||
|
@click="toggle(user.id)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-menu>
|
||||||
|
<template v-slot:activator="{ props: activatorProps }">
|
||||||
|
<v-avatar
|
||||||
|
rounded="circle"
|
||||||
|
border
|
||||||
|
density="compact"
|
||||||
|
variant="tonal"
|
||||||
|
class="stack-avatar cursor-pointer"
|
||||||
|
:style="{ zIndex: hoveredId === 'more' ? avatarUsers.length + 1 : 0 }"
|
||||||
|
v-bind="activatorProps"
|
||||||
|
@mouseenter="hoveredId = 'more'"
|
||||||
|
@mouseleave="hoveredId = null"
|
||||||
|
>
|
||||||
|
<span v-if="extraCount > 0" class="text-caption">+{{ extraCount }}</span>
|
||||||
|
<v-icon v-else icon="mdi-dots-horizontal" size="small" />
|
||||||
|
</v-avatar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-infinite-scroll height="16rem" @load="loadMore">
|
||||||
|
<v-list
|
||||||
|
border
|
||||||
|
width="12rem"
|
||||||
|
class="mt-1"
|
||||||
|
select-strategy="leaf"
|
||||||
|
density="compact"
|
||||||
|
:selected="selected"
|
||||||
|
@update:selected="onMenuSelect"
|
||||||
|
>
|
||||||
|
<v-list-item v-for="user in users" :key="user.id" :value="user.id">
|
||||||
|
<div class="d-flex align-center ga-2">
|
||||||
|
<v-avatar
|
||||||
|
:image="user.image ?? undefined"
|
||||||
|
:text="initials(user)"
|
||||||
|
rounded="circle"
|
||||||
|
density="compact"
|
||||||
|
border
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
<div>{{ user.name || user.email }}</div>
|
||||||
|
</div>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<template #empty />
|
||||||
|
</v-infinite-scroll>
|
||||||
|
</v-menu>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss">
|
||||||
|
.stack-avatar {
|
||||||
|
position: relative;
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-left: -0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-0.25rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, onMounted, ref } from 'vue'
|
|
||||||
import { itemsList } from '@/api/user.api.ts'
|
|
||||||
import type { User } from '@/api/user.api.ts'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
selected?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), { selected: () => [] })
|
|
||||||
const emit = defineEmits<{ changed: [value: string[]] }>()
|
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
|
||||||
const AVATAR_LIMIT = 5
|
|
||||||
|
|
||||||
const users = ref<User[]>([])
|
|
||||||
const page = ref(0)
|
|
||||||
const hasMore = ref(true)
|
|
||||||
const totalCount = ref(0)
|
|
||||||
const hoveredId = ref<string | null>(null)
|
|
||||||
|
|
||||||
const avatarUsers = computed(() => users.value.slice(0, AVATAR_LIMIT))
|
|
||||||
const extraCount = computed(() => Math.max(totalCount.value - AVATAR_LIMIT, 0))
|
|
||||||
|
|
||||||
function initials(user: Pick<User, 'name' | 'email'>) {
|
|
||||||
return (user.name || user.email || '?')[0]!.toUpperCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSelected(id: string) {
|
|
||||||
return props.selected.includes(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle(id: string) {
|
|
||||||
const next = isSelected(id) ? props.selected.filter((el) => el !== id) : [...props.selected, id]
|
|
||||||
emit('changed', next)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMenuSelect(value: unknown) {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
emit('changed', value.filter((el): el is string => typeof el === 'string'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadMore({ done }: { done: (status: 'ok' | 'empty' | 'error') => void }) {
|
|
||||||
if (!hasMore.value) {
|
|
||||||
done('empty')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const nextPage = page.value + 1
|
|
||||||
const res = await itemsList({ page: nextPage, limit: PAGE_SIZE })
|
|
||||||
users.value.push(...res.list)
|
|
||||||
page.value = nextPage
|
|
||||||
totalCount.value = res.meta.totalCount
|
|
||||||
hasMore.value = nextPage < res.meta.totalPages
|
|
||||||
done(hasMore.value ? 'ok' : 'empty')
|
|
||||||
} catch {
|
|
||||||
done('error')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadMore({ done: () => {} })
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="d-flex align-center">
|
|
||||||
<v-avatar
|
|
||||||
v-for="(user, index) in avatarUsers"
|
|
||||||
:key="user.id"
|
|
||||||
rounded="circle"
|
|
||||||
border
|
|
||||||
density="compact"
|
|
||||||
:image="user.image ?? undefined"
|
|
||||||
:text="initials(user)"
|
|
||||||
class="stack-avatar cursor-pointer"
|
|
||||||
:class="{ 'bg-primary': isSelected(user.id) }"
|
|
||||||
:style="{ zIndex: hoveredId === user.id ? avatarUsers.length + 1 : avatarUsers.length - index }"
|
|
||||||
@mouseenter="hoveredId = user.id"
|
|
||||||
@mouseleave="hoveredId = null"
|
|
||||||
@click="toggle(user.id)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<v-menu>
|
|
||||||
<template v-slot:activator="{ props: activatorProps }">
|
|
||||||
<v-avatar
|
|
||||||
rounded="circle"
|
|
||||||
border
|
|
||||||
density="compact"
|
|
||||||
variant="tonal"
|
|
||||||
class="stack-avatar cursor-pointer"
|
|
||||||
:style="{ zIndex: hoveredId === 'more' ? avatarUsers.length + 1 : 0 }"
|
|
||||||
v-bind="activatorProps"
|
|
||||||
@mouseenter="hoveredId = 'more'"
|
|
||||||
@mouseleave="hoveredId = null"
|
|
||||||
>
|
|
||||||
<span v-if="extraCount > 0" class="text-caption">+{{ extraCount }}</span>
|
|
||||||
<v-icon v-else icon="mdi-dots-horizontal" size="small" />
|
|
||||||
</v-avatar>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<v-infinite-scroll height="16rem" @load="loadMore">
|
|
||||||
<v-list
|
|
||||||
border
|
|
||||||
width="12rem"
|
|
||||||
class="mt-1"
|
|
||||||
select-strategy="leaf"
|
|
||||||
density="compact"
|
|
||||||
:selected="selected"
|
|
||||||
@update:selected="onMenuSelect"
|
|
||||||
>
|
|
||||||
<v-list-item v-for="user in users" :key="user.id" :value="user.id">
|
|
||||||
<div class="d-flex align-center ga-2">
|
|
||||||
<v-avatar
|
|
||||||
:image="user.image ?? undefined"
|
|
||||||
:text="initials(user)"
|
|
||||||
rounded="circle"
|
|
||||||
density="compact"
|
|
||||||
border
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
<div>{{ user.name || user.email }}</div>
|
|
||||||
</div>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
|
|
||||||
<template #empty />
|
|
||||||
</v-infinite-scroll>
|
|
||||||
</v-menu>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.stack-avatar {
|
|
||||||
position: relative;
|
|
||||||
transition: transform 0.15s ease;
|
|
||||||
|
|
||||||
&:not(:first-child) {
|
|
||||||
margin-left: -0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: translateY(-0.25rem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import AssigneeFilter from '@/components/AssigneeFilter.vue'
|
import AssigneeFilter from '@/components/AssigneeFilter.vue'
|
||||||
import AssigneeFilterAvatar from '@/components/AssigneeFilterAvatar.vue'
|
|
||||||
import TextFilter from '@/components/TextFilter.vue'
|
import TextFilter from '@/components/TextFilter.vue'
|
||||||
|
|
||||||
|
function onChanged(value: string[]) {
|
||||||
|
console.log(value)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="u-grid2 ga-1 align-center">
|
<div class="u-grid2 ga-4 align-center">
|
||||||
<text-filter />
|
<text-filter />
|
||||||
<assignee-filter />
|
<assignee-filter @changed="onChanged" :selected="['4Jpoz4EgViFrMpl7jQ969mVyX2IFYg5H']" />
|
||||||
<assignee-filter-avatar />
|
|
||||||
<v-btn variant="text">clear filters</v-btn>
|
<v-btn variant="text">clear filters</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -17,6 +19,6 @@ import TextFilter from '@/components/TextFilter.vue'
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.u-grid2 {
|
.u-grid2 {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 10rem auto auto;
|
grid-template-columns: 1fr auto auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in new issue
Block a user