Merge branch 'dev' into preview

This commit is contained in:
ccbikai 2025-03-02 18:12:04 +08:00
commit 8169f8a973
23 changed files with 559 additions and 101 deletions

View file

@ -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',

View file

@ -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>

View file

@ -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>

View 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>

View file

@ -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 />

View file

@ -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

View file

@ -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>

View file

@ -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

View file

@ -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"
/>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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'

View file

@ -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",

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 B

View file

@ -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(),
// })

View file

@ -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 {

View file

@ -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 }