Merge branch 'dev' into preview
This commit is contained in:
commit
8169f8a973
23 changed files with 559 additions and 101 deletions
7
app.vue
7
app.vue
|
|
@ -17,6 +17,13 @@ useHead({
|
|||
htmlAttrs: {
|
||||
lang: 'en',
|
||||
},
|
||||
meta: [
|
||||
{
|
||||
name: 'viewport',
|
||||
content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0',
|
||||
tagPosition: 'head',
|
||||
},
|
||||
],
|
||||
link: [
|
||||
{
|
||||
rel: 'icon',
|
||||
|
|
|
|||
|
|
@ -11,36 +11,38 @@ const defaultData = Object.freeze({
|
|||
const counters = ref(defaultData)
|
||||
|
||||
const id = inject('id')
|
||||
const startAt = inject('startAt')
|
||||
const endAt = inject('endAt')
|
||||
|
||||
const time = inject('time')
|
||||
const filters = inject('filters')
|
||||
async function getLinkCounters() {
|
||||
counters.value = defaultData
|
||||
const { data } = await useAPI('/api/stats/counters', {
|
||||
query: {
|
||||
id: id.value,
|
||||
startAt: startAt.value,
|
||||
endAt: endAt.value,
|
||||
startAt: time.value.startAt,
|
||||
endAt: time.value.endAt,
|
||||
...filters.value,
|
||||
},
|
||||
})
|
||||
counters.value = data?.[0]
|
||||
}
|
||||
|
||||
const stopWatchTime = watch([startAt, endAt], getLinkCounters)
|
||||
const stopWatchQueryChange = watch([time, filters], getLinkCounters, {
|
||||
deep: true,
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
getLinkCounters()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopWatchTime()
|
||||
stopWatchQueryChange()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid gap-4 sm:gap-3 lg:gap-4 sm:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<CardHeader class="flex flex-row justify-between items-center pb-2 space-y-0">
|
||||
<CardTitle class="text-sm font-medium">
|
||||
Visits
|
||||
</CardTitle>
|
||||
|
|
@ -51,7 +53,7 @@ onBeforeUnmount(() => {
|
|||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<CardHeader class="flex flex-row justify-between items-center pb-2 space-y-0">
|
||||
<CardTitle class="text-sm font-medium">
|
||||
Visitors
|
||||
</CardTitle>
|
||||
|
|
@ -62,7 +64,7 @@ onBeforeUnmount(() => {
|
|||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<CardHeader class="flex flex-row justify-between items-center pb-2 space-y-0">
|
||||
<CardTitle class="text-sm font-medium">
|
||||
Referers
|
||||
</CardTitle>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
<script setup>
|
||||
import { now, startOfMonth, startOfWeek } from '@internationalized/date'
|
||||
import { useUrlSearchParams } from '@vueuse/core'
|
||||
import { safeDestr } from 'destr'
|
||||
|
||||
const emit = defineEmits(['update:dateRange'])
|
||||
|
||||
const startAt = inject('startAt')
|
||||
const endAt = inject('endAt')
|
||||
const time = inject('time')
|
||||
|
||||
const dateRange = ref('last-7d')
|
||||
const openCustomDateRange = ref(false)
|
||||
|
|
@ -62,6 +63,28 @@ watch(dateRange, (newValue) => {
|
|||
break
|
||||
}
|
||||
})
|
||||
|
||||
function restoreDateRange() {
|
||||
try {
|
||||
const searchParams = useUrlSearchParams('history')
|
||||
if (searchParams.time) {
|
||||
const time = safeDestr(searchParams.time)
|
||||
emit('update:dateRange', [time.startAt, time.endAt])
|
||||
dateRange.value = 'custom'
|
||||
nextTick(() => {
|
||||
openCustomDateRange.value = false
|
||||
customDateRange.value = undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('restore searchParams error', error)
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
restoreDateRange()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -69,7 +92,7 @@ watch(dateRange, (newValue) => {
|
|||
<SelectTrigger>
|
||||
<SelectValue v-if="dateRange" />
|
||||
<div v-else>
|
||||
{{ shortDate(startAt) }} - {{ shortDate(endAt) }}
|
||||
{{ shortDate(time.startAt) }} - {{ shortDate(time.endAt) }}
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
|
|||
103
components/dashboard/Filters.vue
Normal file
103
components/dashboard/Filters.vue
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<script setup>
|
||||
import { createReusableTemplate, useMediaQuery, useUrlSearchParams, watchDebounced } from '@vueuse/core'
|
||||
import { safeDestr } from 'destr'
|
||||
import { Check, ChevronsUpDown } from 'lucide-vue-next'
|
||||
import { VList } from 'virtua/vue'
|
||||
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
const [TriggerTemplate, TriggerComponent] = createReusableTemplate()
|
||||
const [FilterTemplate, FilterComponent] = createReusableTemplate()
|
||||
|
||||
const isDesktop = useMediaQuery('(min-width: 640px)')
|
||||
|
||||
const links = ref([])
|
||||
const isOpen = ref(false)
|
||||
const selectedLinks = ref([])
|
||||
|
||||
async function getLinks() {
|
||||
links.value = await useAPI('/api/link/search')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getLinks()
|
||||
})
|
||||
|
||||
watchDebounced(selectedLinks, (value) => {
|
||||
emit('change', 'slug', value.join(','))
|
||||
}, { debounce: 500, maxWait: 1000 })
|
||||
|
||||
function restoreFilters() {
|
||||
const searchParams = useUrlSearchParams('history')
|
||||
if (searchParams.filters) {
|
||||
const filters = safeDestr(searchParams.filters)
|
||||
if (filters.slug) {
|
||||
selectedLinks.value = filters.slug.split(',')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
restoreFilters()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TriggerTemplate>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
:aria-expanded="isOpen"
|
||||
class="flex justify-between px-3 w-full sm:w-48"
|
||||
>
|
||||
<div class="flex-1 text-left truncate" :class="selectedLinks.length ? 'text-foreground' : 'text-muted-foreground'">
|
||||
{{ selectedLinks.length ? selectedLinks.join(', ') : 'Filter Links...' }}
|
||||
</div>
|
||||
<ChevronsUpDown class="ml-2 w-4 h-4 opacity-50 shrink-0" />
|
||||
</Button>
|
||||
</TriggerTemplate>
|
||||
<FilterTemplate>
|
||||
<Command v-model="selectedLinks" multiple>
|
||||
<CommandInput :placeholder="selectedLinks.length ? selectedLinks.join(', ') : 'Filter Links...'" />
|
||||
<CommandEmpty>No link found.</CommandEmpty>
|
||||
<CommandList :class="{ 'max-h-none': !isDesktop }">
|
||||
<CommandGroup>
|
||||
<VList
|
||||
v-slot="{ item: link }"
|
||||
:data="links"
|
||||
:style="{ height: isDesktop ? '292px' : '420px' }"
|
||||
>
|
||||
<CommandItem
|
||||
:value="link.slug"
|
||||
>
|
||||
<Check
|
||||
:class="cn(
|
||||
'mr-2 h-4 w-4',
|
||||
selectedLinks.includes(link.slug) ? 'opacity-100' : 'opacity-0',
|
||||
)"
|
||||
/>
|
||||
{{ link.slug }}
|
||||
</CommandItem>
|
||||
</VList>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</FilterTemplate>
|
||||
<Popover v-if="isDesktop" v-model:open="isOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<TriggerComponent />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="p-0 w-full sm:w-48">
|
||||
<FilterComponent />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Drawer v-else v-model:open="isOpen">
|
||||
<DrawerTrigger as-child>
|
||||
<TriggerComponent />
|
||||
</DrawerTrigger>
|
||||
<DrawerContent class="h-[500px]">
|
||||
<FilterComponent />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</template>
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
<script setup>
|
||||
import { now } from '@internationalized/date'
|
||||
import { useUrlSearchParams } from '@vueuse/core'
|
||||
import { safeDestr } from 'destr'
|
||||
|
||||
defineProps({
|
||||
link: {
|
||||
|
|
@ -8,32 +10,70 @@ defineProps({
|
|||
},
|
||||
})
|
||||
|
||||
const startAt = ref(date2unix(now().subtract({ days: 7 })))
|
||||
const endAt = ref(date2unix(now()))
|
||||
const searchParams = useUrlSearchParams('history')
|
||||
|
||||
provide('startAt', startAt)
|
||||
provide('endAt', endAt)
|
||||
const time = ref({
|
||||
startAt: date2unix(now().subtract({ days: 7 })),
|
||||
endAt: date2unix(now()),
|
||||
})
|
||||
|
||||
function changeDate(time) {
|
||||
provide('time', time)
|
||||
|
||||
function changeDate(dateRange) {
|
||||
console.log('changeDate', dateRange)
|
||||
// console.log('dashboard date', new Date(time[0] * 1000), new Date(time[1] * 1000))
|
||||
startAt.value = time[0]
|
||||
endAt.value = time[1]
|
||||
time.value.startAt = dateRange[0]
|
||||
time.value.endAt = dateRange[1]
|
||||
|
||||
searchParams.time = JSON.stringify(time.value)
|
||||
}
|
||||
|
||||
const filters = ref({})
|
||||
|
||||
provide('filters', filters)
|
||||
|
||||
function changeFilter(type, value) {
|
||||
console.log('changeFilter', type, value)
|
||||
filters.value[type] = value
|
||||
|
||||
searchParams.filters = JSON.stringify(filters.value)
|
||||
}
|
||||
|
||||
function restoreSearchParams() {
|
||||
try {
|
||||
if (searchParams.time) {
|
||||
time.value = safeDestr(searchParams.time)
|
||||
}
|
||||
if (searchParams.filters) {
|
||||
filters.value = safeDestr(searchParams.filters)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('restore searchParams error', error)
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
restoreSearchParams()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="space-y-6">
|
||||
<DashboardNav>
|
||||
<template
|
||||
v-if="link"
|
||||
#left
|
||||
>
|
||||
<h3 class="text-xl font-bold leading-10">
|
||||
{{ link.slug }}'s Stats
|
||||
</h3>
|
||||
</template>
|
||||
<DashboardDatePicker @update:date-range="changeDate" />
|
||||
</DashboardNav>
|
||||
<div class="flex flex-col gap-6 sm:gap-2 sm:flex-row sm:justify-between">
|
||||
<DashboardNav class="flex-1">
|
||||
<template
|
||||
v-if="link"
|
||||
#left
|
||||
>
|
||||
<h3 class="text-xl font-bold leading-10">
|
||||
{{ link.slug }}'s Stats
|
||||
</h3>
|
||||
</template>
|
||||
<DashboardDatePicker @update:date-range="changeDate" />
|
||||
</DashboardNav>
|
||||
<DashboardFilters v-if="!link" @change="changeFilter" />
|
||||
</div>
|
||||
<DashboardCounters />
|
||||
<DashboardViews />
|
||||
<DashboardMetrics />
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ const views = ref([])
|
|||
const chart = computed(() => views.value.length > 1 ? AreaChart : BarChart)
|
||||
|
||||
const id = inject('id')
|
||||
const startAt = inject('startAt')
|
||||
const endAt = inject('endAt')
|
||||
const time = inject('time')
|
||||
const filters = inject('filters')
|
||||
|
||||
const OneDay = 24 * 60 * 60 // 1 day in seconds
|
||||
function getUnit(startAt, endAt) {
|
||||
|
|
@ -22,10 +22,11 @@ async function getLinkViews() {
|
|||
const { data } = await useAPI('/api/stats/views', {
|
||||
query: {
|
||||
id: id.value,
|
||||
unit: getUnit(startAt.value, endAt.value),
|
||||
unit: getUnit(time.value.startAt, time.value.endAt),
|
||||
clientTimezone: getTimeZone(),
|
||||
startAt: startAt.value,
|
||||
endAt: endAt.value,
|
||||
startAt: time.value.startAt,
|
||||
endAt: time.value.endAt,
|
||||
...filters.value,
|
||||
},
|
||||
})
|
||||
views.value = (data || []).map((item) => {
|
||||
|
|
@ -35,19 +36,21 @@ async function getLinkViews() {
|
|||
})
|
||||
}
|
||||
|
||||
const stopWatchTime = watch([startAt, endAt], getLinkViews)
|
||||
const stopWatchQueryChange = watch([time, filters], getLinkViews, {
|
||||
deep: true,
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
getLinkViews()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopWatchTime()
|
||||
stopWatchQueryChange()
|
||||
})
|
||||
|
||||
function formatTime(tick) {
|
||||
if (Number.isInteger(tick) && views.value[tick]) {
|
||||
if (getUnit(startAt.value, endAt.value) === 'hour')
|
||||
if (getUnit(time.value.startAt, time.value.endAt) === 'hour')
|
||||
return views.value[tick].time.split(' ')[1] || ''
|
||||
|
||||
return views.value[tick].time
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
<script setup>
|
||||
import { useMagicKeys } from '@vueuse/core'
|
||||
import { createReusableTemplate, useMagicKeys, useMediaQuery } from '@vueuse/core'
|
||||
import { useFuse } from '@vueuse/integrations/useFuse'
|
||||
|
||||
const [TriggerTemplate, TriggerComponent] = createReusableTemplate()
|
||||
const [SearchTemplate, SearchComponent] = createReusableTemplate()
|
||||
|
||||
const isDesktop = useMediaQuery('(min-width: 640px)')
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
|
@ -48,12 +53,11 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<TriggerTemplate>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="relative h-10 w-full justify-start bg-background text-muted-foreground sm:w-32 md:w-48"
|
||||
@click="isOpen = true"
|
||||
class="relative justify-start w-full h-10 bg-background text-muted-foreground sm:w-32 md:w-48"
|
||||
>
|
||||
<span class="hidden md:inline-flex">Search Links...</span>
|
||||
<span class="inline-flex md:hidden">Search</span>
|
||||
|
|
@ -61,36 +65,50 @@ onMounted(() => {
|
|||
<span class="text-xs">⌘</span>K
|
||||
</kbd>
|
||||
</Button>
|
||||
<Dialog :open="isOpen" @update:open="isOpen = !isOpen">
|
||||
<DialogContent class="overflow-hidden p-0 shadow-lg">
|
||||
<Command v-model:search-term="searchTerm" v-model="selectedLink" class="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
<CommandInput placeholder="Type to search..." />
|
||||
<CommandList>
|
||||
<CommandEmpty v-if="searchTerm">
|
||||
No links found.
|
||||
</CommandEmpty>
|
||||
<CommandGroup heading="Links">
|
||||
<CommandItem v-for="link in filteredLinks" :key="link.item?.id" class="cursor-pointer" :value="link.item" @select="selectLink(link.item)">
|
||||
<div class="flex gap-1 w-full">
|
||||
<div class="flex-1 overflow-hidden inline-flex gap-1 items-center">
|
||||
<div class="text-sm font-medium">
|
||||
{{ link.item?.slug }}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground flex-1 truncate">
|
||||
({{ link.item?.url }})
|
||||
</div>
|
||||
</div>
|
||||
<Badge v-if="link.item?.comment" variant="secondary">
|
||||
<div class="max-w-24 truncate">
|
||||
{{ link.item?.comment }}
|
||||
</div>
|
||||
</Badge>
|
||||
</TriggerTemplate>
|
||||
<SearchTemplate>
|
||||
<Command v-model:search-term="searchTerm" v-model="selectedLink" class="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
<CommandInput placeholder="Type to search..." />
|
||||
<CommandList :class="{ 'max-h-none': !isDesktop }">
|
||||
<CommandEmpty v-if="searchTerm">
|
||||
No links found.
|
||||
</CommandEmpty>
|
||||
<CommandGroup heading="Links">
|
||||
<CommandItem v-for="link in filteredLinks" :key="link.item?.id" class="cursor-pointer" :value="link.item" @select="selectLink(link.item)">
|
||||
<div class="flex gap-1 w-full">
|
||||
<div class="inline-flex overflow-hidden flex-1 gap-1 items-center">
|
||||
<div class="text-sm font-medium">
|
||||
{{ link.item?.slug }}
|
||||
</div>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
<div class="flex-1 text-xs truncate text-muted-foreground">
|
||||
({{ link.item?.url }})
|
||||
</div>
|
||||
</div>
|
||||
<Badge v-if="link.item?.comment" variant="secondary">
|
||||
<div class="truncate max-w-24">
|
||||
{{ link.item?.comment }}
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</SearchTemplate>
|
||||
<Dialog v-if="isDesktop" v-model:open="isOpen">
|
||||
<DialogTrigger as-child>
|
||||
<TriggerComponent />
|
||||
</DialogTrigger>
|
||||
<DialogContent class="overflow-hidden p-0 shadow-lg">
|
||||
<SearchComponent />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Drawer v-else v-model:open="isOpen">
|
||||
<DrawerTrigger as-child>
|
||||
<TriggerComponent />
|
||||
</DrawerTrigger>
|
||||
<DrawerContent class="h-[500px]">
|
||||
<SearchComponent />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { ChartTooltip } from '@/components/ui/chart'
|
|||
import { VisSingleContainer, VisTopoJSONMap, VisTopoJSONMapSelectors } from '@unovis/vue'
|
||||
|
||||
const id = inject('id')
|
||||
const startAt = inject('startAt')
|
||||
const endAt = inject('endAt')
|
||||
const time = inject('time')
|
||||
const filters = inject('filters')
|
||||
|
||||
const worldMapTopoJSON = ref({})
|
||||
const areaData = ref([])
|
||||
|
|
@ -20,8 +20,9 @@ async function getMapData() {
|
|||
query: {
|
||||
type: 'country',
|
||||
id: id.value,
|
||||
startAt: startAt.value,
|
||||
endAt: endAt.value,
|
||||
startAt: time.value.startAt,
|
||||
endAt: time.value.endAt,
|
||||
...filters.value,
|
||||
},
|
||||
})
|
||||
if (Array.isArray(data)) {
|
||||
|
|
@ -32,7 +33,9 @@ async function getMapData() {
|
|||
}
|
||||
}
|
||||
|
||||
const stopWatchTime = watch([startAt, endAt], getMapData)
|
||||
const stopWatchQueryChange = watch([time, filters], getMapData, {
|
||||
deep: true,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getWorldMapJSON()
|
||||
|
|
@ -40,7 +43,7 @@ onMounted(() => {
|
|||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopWatchTime()
|
||||
stopWatchQueryChange()
|
||||
})
|
||||
|
||||
const valueFormatter = v => v
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ const props = defineProps({
|
|||
})
|
||||
|
||||
const id = inject('id')
|
||||
const startAt = inject('startAt')
|
||||
const endAt = inject('endAt')
|
||||
const time = inject('time')
|
||||
const filters = inject('filters')
|
||||
|
||||
const total = ref(0)
|
||||
const metrics = ref([])
|
||||
|
|
@ -28,8 +28,9 @@ async function getLinkMetrics() {
|
|||
query: {
|
||||
type: props.type,
|
||||
id: id.value,
|
||||
startAt: startAt.value,
|
||||
endAt: endAt.value,
|
||||
startAt: time.value.startAt,
|
||||
endAt: time.value.endAt,
|
||||
...filters.value,
|
||||
},
|
||||
})
|
||||
if (Array.isArray(data)) {
|
||||
|
|
@ -44,14 +45,16 @@ async function getLinkMetrics() {
|
|||
}
|
||||
}
|
||||
|
||||
const stopWatchTime = watch([startAt, endAt], getLinkMetrics)
|
||||
const stopWatchQueryChange = watch([time, filters], getLinkMetrics, {
|
||||
deep: true,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getLinkMetrics()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopWatchTime()
|
||||
stopWatchQueryChange()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -73,7 +76,7 @@ onBeforeUnmount(() => {
|
|||
variant="link"
|
||||
>
|
||||
<Maximize
|
||||
class="w-4 h-4 mr-2"
|
||||
class="mr-2 w-4 h-4"
|
||||
/> DETAILS
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
|
@ -91,7 +94,7 @@ onBeforeUnmount(() => {
|
|||
</CardFooter>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex items-center justify-between h-12 px-4">
|
||||
<div class="flex justify-between items-center px-4 h-12">
|
||||
<Skeleton
|
||||
class="w-32 h-4 rounded-full"
|
||||
/>
|
||||
|
|
|
|||
19
components/ui/drawer/Drawer.vue
Normal file
19
components/ui/drawer/Drawer.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts" setup>
|
||||
import type { DrawerRootEmits, DrawerRootProps } from 'vaul-vue'
|
||||
import { useForwardPropsEmits } from 'radix-vue'
|
||||
import { DrawerRoot } from 'vaul-vue'
|
||||
|
||||
const props = withDefaults(defineProps<DrawerRootProps>(), {
|
||||
shouldScaleBackground: true,
|
||||
})
|
||||
|
||||
const emits = defineEmits<DrawerRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DrawerRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</DrawerRoot>
|
||||
</template>
|
||||
28
components/ui/drawer/DrawerContent.vue
Normal file
28
components/ui/drawer/DrawerContent.vue
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<script lang="ts" setup>
|
||||
import type { DialogContentEmits, DialogContentProps } from 'radix-vue'
|
||||
import type { HtmlHTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useForwardPropsEmits } from 'radix-vue'
|
||||
import { DrawerContent, DrawerPortal } from 'vaul-vue'
|
||||
import DrawerOverlay from './DrawerOverlay.vue'
|
||||
|
||||
const props = defineProps<DialogContentProps & { class?: HtmlHTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay />
|
||||
<DrawerContent
|
||||
v-bind="forwarded" :class="cn(
|
||||
'fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<div class="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||
<slot />
|
||||
</DrawerContent>
|
||||
</DrawerPortal>
|
||||
</template>
|
||||
21
components/ui/drawer/DrawerDescription.vue
Normal file
21
components/ui/drawer/DrawerDescription.vue
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts" setup>
|
||||
import type { DrawerDescriptionProps } from 'vaul-vue'
|
||||
import type { HtmlHTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { DrawerDescription } from 'vaul-vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<DrawerDescriptionProps & { class?: HtmlHTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DrawerDescription v-bind="delegatedProps" :class="cn('text-sm text-muted-foreground', props.class)">
|
||||
<slot />
|
||||
</DrawerDescription>
|
||||
</template>
|
||||
14
components/ui/drawer/DrawerFooter.vue
Normal file
14
components/ui/drawer/DrawerFooter.vue
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts" setup>
|
||||
import type { HtmlHTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HtmlHTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('mt-auto flex flex-col gap-2 p-4', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
14
components/ui/drawer/DrawerHeader.vue
Normal file
14
components/ui/drawer/DrawerHeader.vue
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts" setup>
|
||||
import type { HtmlHTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HtmlHTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('grid gap-1.5 p-4 text-center sm:text-left', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
19
components/ui/drawer/DrawerOverlay.vue
Normal file
19
components/ui/drawer/DrawerOverlay.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts" setup>
|
||||
import type { DialogOverlayProps } from 'radix-vue'
|
||||
import type { HtmlHTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { DrawerOverlay } from 'vaul-vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<DialogOverlayProps & { class?: HtmlHTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DrawerOverlay v-bind="delegatedProps" :class="cn('fixed inset-0 z-50 bg-black/80', props.class)" />
|
||||
</template>
|
||||
21
components/ui/drawer/DrawerTitle.vue
Normal file
21
components/ui/drawer/DrawerTitle.vue
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts" setup>
|
||||
import type { DrawerTitleProps } from 'vaul-vue'
|
||||
import type { HtmlHTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { DrawerTitle } from 'vaul-vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<DrawerTitleProps & { class?: HtmlHTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DrawerTitle v-bind="delegatedProps" :class="cn('text-lg font-semibold leading-none tracking-tight', props.class)">
|
||||
<slot />
|
||||
</DrawerTitle>
|
||||
</template>
|
||||
8
components/ui/drawer/index.ts
Normal file
8
components/ui/drawer/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export { default as Drawer } from './Drawer.vue'
|
||||
export { default as DrawerContent } from './DrawerContent.vue'
|
||||
export { default as DrawerDescription } from './DrawerDescription.vue'
|
||||
export { default as DrawerFooter } from './DrawerFooter.vue'
|
||||
export { default as DrawerHeader } from './DrawerHeader.vue'
|
||||
export { default as DrawerOverlay } from './DrawerOverlay.vue'
|
||||
export { default as DrawerTitle } from './DrawerTitle.vue'
|
||||
export { DrawerClose, DrawerPortal, DrawerTrigger } from 'vaul-vue'
|
||||
|
|
@ -35,6 +35,7 @@
|
|||
"qr-code-styling": "1.6.0-rc.1",
|
||||
"radix-vue": "^1.9.11",
|
||||
"ua-parser-js": "next",
|
||||
"vaul-vue": "^0.3.0",
|
||||
"vee-validate": "^4.15.0",
|
||||
"virtua": "^0.39.2",
|
||||
"vue-sonner": "^1.3.0",
|
||||
|
|
|
|||
105
pnpm-lock.yaml
105
pnpm-lock.yaml
|
|
@ -50,6 +50,9 @@ importers:
|
|||
ua-parser-js:
|
||||
specifier: next
|
||||
version: 2.0.0-beta.3
|
||||
vaul-vue:
|
||||
specifier: ^0.3.0
|
||||
version: 0.3.0(reka-ui@2.0.2(typescript@5.7.2)(vue@3.4.38(typescript@5.7.2)))(vue@3.4.38(typescript@5.7.2))
|
||||
vee-validate:
|
||||
specifier: ^4.15.0
|
||||
version: 4.15.0(vue@3.4.38(typescript@5.7.2))
|
||||
|
|
@ -1357,15 +1360,24 @@ packages:
|
|||
'@floating-ui/core@1.6.1':
|
||||
resolution: {integrity: sha512-42UH54oPZHPdRHdw6BgoBD6cg/eVTmVrFcgeRDM3jbO7uxSoipVcmcIGFcA5jmOHO5apcyvBhkSKES3fQJnu7A==}
|
||||
|
||||
'@floating-ui/dom@1.6.13':
|
||||
resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==}
|
||||
|
||||
'@floating-ui/dom@1.6.7':
|
||||
resolution: {integrity: sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==}
|
||||
|
||||
'@floating-ui/utils@0.2.4':
|
||||
resolution: {integrity: sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==}
|
||||
|
||||
'@floating-ui/utils@0.2.9':
|
||||
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
|
||||
|
||||
'@floating-ui/vue@1.1.1':
|
||||
resolution: {integrity: sha512-cyawjk9etPZPl/RVtMRnWrwtAhWbPVSrRVYARgOzhLIqxr0k2up1APrrFjqP9QwRQ0AwjKSvbWg4YC6jESutow==}
|
||||
|
||||
'@floating-ui/vue@1.1.6':
|
||||
resolution: {integrity: sha512-XFlUzGHGv12zbgHNk5FN2mUB7ROul3oG2ENdTpWdE+qMFxyNxWSRmsoyhiEnpmabNm6WnUvR1OvJfUfN4ojC1A==}
|
||||
|
||||
'@humanfs/core@0.19.1':
|
||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
|
|
@ -1993,9 +2005,17 @@ packages:
|
|||
'@swc/helpers@0.5.11':
|
||||
resolution: {integrity: sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==}
|
||||
|
||||
'@tanstack/virtual-core@3.13.2':
|
||||
resolution: {integrity: sha512-Qzz4EgzMbO5gKrmqUondCjiHcuu4B1ftHb0pjCut661lXZdGoHeze9f/M8iwsK1t5LGR6aNuNGU7mxkowaW6RQ==}
|
||||
|
||||
'@tanstack/virtual-core@3.8.1':
|
||||
resolution: {integrity: sha512-uNtAwenT276M9QYCjTBoHZ8X3MUeCRoGK59zPi92hMIxdfS9AyHjkDWJ94WroDxnv48UE+hIeo21BU84jKc8aQ==}
|
||||
|
||||
'@tanstack/vue-virtual@3.13.2':
|
||||
resolution: {integrity: sha512-z4swzjdhzCh95n9dw9lTvw+t3iwSkYRlVkYkra3C9mul/m5fTzHR7KmtkwH4qXMTXGJUbngtC/bz2cHQIHkO8g==}
|
||||
peerDependencies:
|
||||
vue: ^2.7.0 || ^3.0.0
|
||||
|
||||
'@tanstack/vue-virtual@3.8.1':
|
||||
resolution: {integrity: sha512-uhty1LzUbbcVc5zdMMSUjUt/ECTlMCtK49Ww7dH2m4lNNLGYwkj5SbfrAD8uCZxV1VeV7DRMXqhwUTELyR5rrA==}
|
||||
peerDependencies:
|
||||
|
|
@ -2494,6 +2514,9 @@ packages:
|
|||
'@vueuse/core@12.2.0':
|
||||
resolution: {integrity: sha512-jksyNu+5EGwggNkRWd6xX+8qBkYbmrwdFQMgCABsz+wq8bKF6w3soPFLB8vocFp3wFIzn0OYkSPM9JP+AFKwsg==}
|
||||
|
||||
'@vueuse/core@12.7.0':
|
||||
resolution: {integrity: sha512-jtK5B7YjZXmkGNHjviyGO4s3ZtEhbzSgrbX+s5o+Lr8i2nYqNyHuPVOeTdM1/hZ5Tkxg/KktAuAVDDiHMraMVA==}
|
||||
|
||||
'@vueuse/integrations@12.2.0':
|
||||
resolution: {integrity: sha512-Bc0unXiGNZ0w7xqSvzCuP7AflBRKcZX6ib7tGi7vAjOxhLd6GtN3J8qizIbSPnI62XyPy5fauOkq2i2HUPq6MQ==}
|
||||
peerDependencies:
|
||||
|
|
@ -2541,12 +2564,18 @@ packages:
|
|||
'@vueuse/metadata@12.2.0':
|
||||
resolution: {integrity: sha512-x6zynZtTh1l52m0y8d/EgzpshnMjg8cNZ2KWoncJ62Z5qPSGoc4FUunmMVrrRM/I/5542rTEY89CGftngZvrkQ==}
|
||||
|
||||
'@vueuse/metadata@12.7.0':
|
||||
resolution: {integrity: sha512-4VvTH9mrjXqFN5LYa5YfqHVRI6j7R00Vy4995Rw7PQxyCL3z0Lli86iN4UemWqixxEvYfRjG+hF9wL8oLOn+3g==}
|
||||
|
||||
'@vueuse/shared@10.11.1':
|
||||
resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
|
||||
|
||||
'@vueuse/shared@12.2.0':
|
||||
resolution: {integrity: sha512-SRr4AZwv/giS+EmyA1ZIzn3/iALjjnWAGaBNmoDTMEob9JwQaevAocuaMDnPAvU7Z35Y5g3CFRusCWgp1gVJ3Q==}
|
||||
|
||||
'@vueuse/shared@12.7.0':
|
||||
resolution: {integrity: sha512-coLlUw2HHKsm7rPN6WqHJQr18WymN4wkA/3ThFaJ4v4gWGWAQQGK+MJxLuJTBs4mojQiazlVWAKNJNpUWGRkNw==}
|
||||
|
||||
abbrev@1.1.1:
|
||||
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
||||
|
||||
|
|
@ -5510,6 +5539,11 @@ packages:
|
|||
resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==}
|
||||
hasBin: true
|
||||
|
||||
reka-ui@2.0.2:
|
||||
resolution: {integrity: sha512-pC2UF6Z+kJF96aJvIErhkSO4DJYIeq9pgvh3pntNqcZb3zFGMzw8h2uny+GnLX2CKiQV54kZNYXxecYIiPMGyg==}
|
||||
peerDependencies:
|
||||
vue: '>= 3.2.0'
|
||||
|
||||
replace-in-file@6.3.5:
|
||||
resolution: {integrity: sha512-arB9d3ENdKva2fxRnSjwBEXfK1npgyci7ZZuwysgAp7ORjHSyxz6oqIjTEv8R0Ydl4Ll7uOAZXL4vbkhGIizCg==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -6244,6 +6278,12 @@ packages:
|
|||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
vaul-vue@0.3.0:
|
||||
resolution: {integrity: sha512-LwhBd7xLJtbdhq+vnQk9etOjZxr09GNFtMjOEYh969y5f8JKV5oR30CfB4toPkbtVMxFAFhMrp77EVWQgbtpHA==}
|
||||
peerDependencies:
|
||||
reka-ui: ^2.0.0
|
||||
vue: ^3.3.0
|
||||
|
||||
vee-validate@4.15.0:
|
||||
resolution: {integrity: sha512-PGJh1QCFwCBjbHu5aN6vB8macYVWrajbDvgo1Y/8fz9n/RVIkLmZCJDpUgu7+mUmCOPMxeyq7vXUOhbwAqdXcA==}
|
||||
peerDependencies:
|
||||
|
|
@ -7622,6 +7662,11 @@ snapshots:
|
|||
dependencies:
|
||||
'@floating-ui/utils': 0.2.4
|
||||
|
||||
'@floating-ui/dom@1.6.13':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.6.1
|
||||
'@floating-ui/utils': 0.2.9
|
||||
|
||||
'@floating-ui/dom@1.6.7':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.6.1
|
||||
|
|
@ -7629,6 +7674,8 @@ snapshots:
|
|||
|
||||
'@floating-ui/utils@0.2.4': {}
|
||||
|
||||
'@floating-ui/utils@0.2.9': {}
|
||||
|
||||
'@floating-ui/vue@1.1.1(vue@3.4.38(typescript@5.7.2))':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.6.7
|
||||
|
|
@ -7638,6 +7685,15 @@ snapshots:
|
|||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
||||
'@floating-ui/vue@1.1.6(vue@3.4.38(typescript@5.7.2))':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.6.13
|
||||
'@floating-ui/utils': 0.2.9
|
||||
vue-demi: 0.14.10(vue@3.4.38(typescript@5.7.2))
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
|
||||
'@humanfs/node@0.16.6':
|
||||
|
|
@ -8500,8 +8556,15 @@ snapshots:
|
|||
dependencies:
|
||||
tslib: 2.6.3
|
||||
|
||||
'@tanstack/virtual-core@3.13.2': {}
|
||||
|
||||
'@tanstack/virtual-core@3.8.1': {}
|
||||
|
||||
'@tanstack/vue-virtual@3.13.2(vue@3.4.38(typescript@5.7.2))':
|
||||
dependencies:
|
||||
'@tanstack/virtual-core': 3.13.2
|
||||
vue: 3.4.38(typescript@5.7.2)
|
||||
|
||||
'@tanstack/vue-virtual@3.8.1(vue@3.4.38(typescript@5.7.2))':
|
||||
dependencies:
|
||||
'@tanstack/virtual-core': 3.8.1
|
||||
|
|
@ -9243,6 +9306,15 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@vueuse/core@12.7.0(typescript@5.7.2)':
|
||||
dependencies:
|
||||
'@types/web-bluetooth': 0.0.20
|
||||
'@vueuse/metadata': 12.7.0
|
||||
'@vueuse/shared': 12.7.0(typescript@5.7.2)
|
||||
vue: 3.5.13(typescript@5.7.2)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@vueuse/integrations@12.2.0(fuse.js@7.0.0)(typescript@5.7.2)':
|
||||
dependencies:
|
||||
'@vueuse/core': 12.2.0(typescript@5.7.2)
|
||||
|
|
@ -9257,6 +9329,8 @@ snapshots:
|
|||
|
||||
'@vueuse/metadata@12.2.0': {}
|
||||
|
||||
'@vueuse/metadata@12.7.0': {}
|
||||
|
||||
'@vueuse/shared@10.11.1(vue@3.4.38(typescript@5.7.2))':
|
||||
dependencies:
|
||||
vue-demi: 0.14.10(vue@3.4.38(typescript@5.7.2))
|
||||
|
|
@ -9270,6 +9344,12 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@vueuse/shared@12.7.0(typescript@5.7.2)':
|
||||
dependencies:
|
||||
vue: 3.5.13(typescript@5.7.2)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
abbrev@1.1.1: {}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
|
|
@ -12817,6 +12897,23 @@ snapshots:
|
|||
dependencies:
|
||||
jsesc: 0.5.0
|
||||
|
||||
reka-ui@2.0.2(typescript@5.7.2)(vue@3.4.38(typescript@5.7.2)):
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.6.13
|
||||
'@floating-ui/vue': 1.1.6(vue@3.4.38(typescript@5.7.2))
|
||||
'@internationalized/date': 3.5.4
|
||||
'@internationalized/number': 3.5.3
|
||||
'@tanstack/vue-virtual': 3.13.2(vue@3.4.38(typescript@5.7.2))
|
||||
'@vueuse/core': 12.7.0(typescript@5.7.2)
|
||||
'@vueuse/shared': 12.7.0(typescript@5.7.2)
|
||||
aria-hidden: 1.2.4
|
||||
defu: 6.1.4
|
||||
ohash: 1.1.4
|
||||
vue: 3.4.38(typescript@5.7.2)
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- typescript
|
||||
|
||||
replace-in-file@6.3.5:
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
|
|
@ -13643,6 +13740,14 @@ snapshots:
|
|||
|
||||
vary@1.1.2: {}
|
||||
|
||||
vaul-vue@0.3.0(reka-ui@2.0.2(typescript@5.7.2)(vue@3.4.38(typescript@5.7.2)))(vue@3.4.38(typescript@5.7.2)):
|
||||
dependencies:
|
||||
'@vueuse/core': 10.11.1(vue@3.4.38(typescript@5.7.2))
|
||||
reka-ui: 2.0.2(typescript@5.7.2)(vue@3.4.38(typescript@5.7.2))
|
||||
vue: 3.4.38(typescript@5.7.2)
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
|
||||
vee-validate@4.15.0(vue@3.4.38(typescript@5.7.2)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 7.6.8
|
||||
|
|
|
|||
BIN
public/apple-touch-icon-precomposed.png
Normal file
BIN
public/apple-touch-icon-precomposed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 599 B |
|
|
@ -22,6 +22,6 @@ export const QuerySchema = z.object({
|
|||
limit: z.coerce.number().int().safe().default(listQueryLimit),
|
||||
})
|
||||
|
||||
export const FilterSchema = QuerySchema.omit({ id: true, startAt: true, endAt: true, limit: true }).extend({
|
||||
index1: z.string().optional(),
|
||||
})
|
||||
// export const FilterSchema = QuerySchema.omit({ id: true, startAt: true, endAt: true, limit: true }).extend({
|
||||
// index1: z.string().optional(),
|
||||
// })
|
||||
|
|
|
|||
|
|
@ -1,23 +1,24 @@
|
|||
import type { FilterSchema, QuerySchema } from '@/schemas/query'
|
||||
import type { QuerySchema } from '@/schemas/query'
|
||||
import type { SelectStatement } from 'sql-bricks'
|
||||
import type { z } from 'zod'
|
||||
|
||||
export type Query = z.infer<typeof QuerySchema>
|
||||
export type Filter = z.infer<typeof FilterSchema>
|
||||
const { in: $in, and, eq } = SqlBricks
|
||||
|
||||
export function query2filter(query: Query): Filter {
|
||||
const filter: Filter = {}
|
||||
export type Query = z.infer<typeof QuerySchema>
|
||||
|
||||
export function query2filter(query: Query) {
|
||||
const filter = []
|
||||
if (query.id)
|
||||
filter.index1 = query.id
|
||||
filter.push(eq('index1', query.id))
|
||||
|
||||
Object.keys(logsMap).forEach((key) => {
|
||||
// @ts-expect-error todo
|
||||
if (query[key]) {
|
||||
// @ts-expect-error todo
|
||||
filter[logsMap[key]] = query[key]
|
||||
filter.push($in(logsMap[key], query[key].split(',')))
|
||||
}
|
||||
})
|
||||
return filter
|
||||
return filter.length ? and(...filter) : []
|
||||
}
|
||||
|
||||
export function appendTimeFilter(sql: SelectStatement, query: Query): unknown {
|
||||
|
|
|
|||
|
|
@ -1,2 +1,7 @@
|
|||
// @ts-expect-error todo
|
||||
export { default as SqlBricks } from 'mysql-bricks'
|
||||
import type SqlBricks from 'sql-bricks'
|
||||
// @ts-expect-error use SqlBricks as a type
|
||||
import MySqlBricks from 'mysql-bricks'
|
||||
|
||||
const Bricks = MySqlBricks as unknown as typeof SqlBricks
|
||||
|
||||
export { Bricks as SqlBricks }
|
||||
|
|
|
|||
Loading…
Reference in a new issue