feat(web): support time filter

This commit is contained in:
arkohut 2024-07-31 00:42:07 +08:00
parent d0cfd91f5a
commit 08c51a8259
22 changed files with 631 additions and 17 deletions

View File

@ -1,7 +1,7 @@
<!-- Modal.svelte -->
<script>
import { ScrollArea } from "$lib/components/ui/scroll-area";
import CopyToClipboard from "$lib/components/copy-to-clipboard.svelte"
import CopyToClipboard from "$lib/components/CopyToClipboard.svelte"
import OCRTable from './OCRTable.svelte';
import { marked } from 'marked';

View File

@ -0,0 +1,114 @@
<script lang="ts">
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import { Button } from '$lib/components/ui/button/index.js';
import { RangeCalendar } from '$lib/components/ui/range-calendar/index.js';
import {
CalendarDate,
DateFormatter,
type DateValue,
getLocalTimeZone
} from '@internationalized/date';
let now = +new Date();
const rangeMap = {
unlimited: {
label: '不限时间',
start: null,
end: null
},
today: {
label: '今天',
start: now - 24 * 60 * 60 * 1000,
end: now
},
week: {
label: '最近一周',
start: now - 7 * 24 * 60 * 60 * 1000,
end: now
},
month: {
label: '最近一个月',
start: now - 30 * 24 * 60 * 60 * 1000,
end: now
},
threeMonths: {
label: '最近三个月',
start: now - 90 * 24 * 60 * 60 * 1000,
end: now
},
custom: {
label: '自定义',
start: null,
end: null
}
};
let timeFilter = 'unlimited';
export let start: number;
export let end: number;
let customDateRange = {
start: null,
end: null
};
function updateCustomDateRange(range: { start: DateValue | null; end: DateValue | null }) {
if (range.start && range.end) {
rangeMap.custom.start = range.start.toDate(getLocalTimeZone()).getTime();
rangeMap.custom.end = range.end.toDate(getLocalTimeZone()).getTime();
if (timeFilter !== 'custom') {
timeFilter = 'custom';
}
}
}
$: updateCustomDateRange(customDateRange);
$: if (timeFilter !== 'custom') {
customDateRange = {
start: null,
end: null
};
}
$: displayText =
(timeFilter === 'custom' && customDateRange.start && customDateRange.end)
? `${customDateRange.start?.toString()} - ${customDateRange.end?.toString()}`
: rangeMap[timeFilter].label;
$: start = rangeMap[timeFilter].start;
$: end = rangeMap[timeFilter].end;
</script>
<div>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button
variant="outline"
size="sm"
class="border p-2 text-xs font-medium focus:outline-none"
builders={[builder]}
>
<span class="truncate">{displayText}</span>
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content class="w-56">
<DropdownMenu.Label>时间筛选</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.RadioGroup bind:value={timeFilter}>
<DropdownMenu.RadioItem value="unlimited">时间不限</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="today">今天</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="week">最近一周</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="month">最近一个月</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="threeMonths">最近三个月</DropdownMenu.RadioItem>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger>自定义</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent>
<RangeCalendar bind:value={customDateRange} />
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
</DropdownMenu.RadioGroup>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>

View File

@ -11,16 +11,38 @@
import { Button } from "$lib/components/ui/button/index.js";
import { RangeCalendar } from "$lib/components/ui/range-calendar/index.js";
import * as Popover from "$lib/components/ui/popover/index.js";
import { onMount } from "svelte";
const df = new DateFormatter("en-US", {
dateStyle: "medium"
});
export let startTimestamp: number;
export let endTimestamp: number;
let value: DateRange | undefined = {
start: new CalendarDate(2022, 1, 20),
end: new CalendarDate(2022, 1, 20).add({ days: 20 })
};
let value: DateRange | undefined;
let initialized = false; // Flag to control reactive updates
let userSelected = false; // New flag to track user selection
onMount(() => {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1; // getMonth() returns 0-11
const date = now.getDate();
value = {
start: new CalendarDate(year, month, date).subtract({ days: 30 }),
end: new CalendarDate(year, month, date)
};
console.log(initialized);
initialized = true;
});
$: if (initialized && value && value.start && value.end && !userSelected) {
userSelected = true; // Set flag when user selects a range
startTimestamp = value.start.toDate(getLocalTimeZone()).getTime() / 1000;
endTimestamp = value.end.toDate(getLocalTimeZone()).getTime() / 1000;
}
let startValue: DateValue | undefined = undefined;
</script>
@ -30,13 +52,15 @@
<Button
variant="outline"
class={cn(
"w-[300px] justify-start text-left font-normal",
!value && "text-muted-foreground"
"w-[220px] justify-start text-center font-normal text-xs",
(startTimestamp === -1 && endTimestamp === -1) && "text-muted-foreground"
)}
builders={[builder]}
>
<Calendar class="mr-2 h-4 w-4" />
{#if value && value.start}
{#if startTimestamp === -1 && endTimestamp === -1}
不限时间
{:else if value && value.start}
{#if value.end}
{df.format(value.start.toDate(getLocalTimeZone()))} - {df.format(
value.end.toDate(getLocalTimeZone())

View File

@ -0,0 +1,35 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Check from "lucide-svelte/icons/check";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
let className: $$Props["class"] = undefined;
export let checked: $$Props["checked"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.CheckboxItem
bind:checked
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.CheckboxIndicator>
<Check class="h-4 w-4" />
</DropdownMenuPrimitive.CheckboxIndicator>
</span>
<slot />
</DropdownMenuPrimitive.CheckboxItem>

View File

@ -0,0 +1,27 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.ContentProps;
type $$Events = DropdownMenuPrimitive.ContentEvents;
let className: $$Props["class"] = undefined;
export let sideOffset: $$Props["sideOffset"] = 4;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Content
{transition}
{transitionConfig}
{sideOffset}
class={cn(
"bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-md focus:outline-none",
className
)}
{...$$restProps}
on:keydown
>
<slot />
</DropdownMenuPrimitive.Content>

View File

@ -0,0 +1,31 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
};
type $$Events = DropdownMenuPrimitive.ItemEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Item
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<slot />
</DropdownMenuPrimitive.Item>

View File

@ -0,0 +1,19 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.LabelProps & {
inset?: boolean;
};
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Label
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...$$restProps}
>
<slot />
</DropdownMenuPrimitive.Label>

View File

@ -0,0 +1,11 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type $$Props = DropdownMenuPrimitive.RadioGroupProps;
export let value: $$Props["value"] = undefined;
</script>
<DropdownMenuPrimitive.RadioGroup {...$$restProps} bind:value>
<slot />
</DropdownMenuPrimitive.RadioGroup>

View File

@ -0,0 +1,35 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Circle from "lucide-svelte/icons/circle";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.RadioItemProps;
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
</script>
<DropdownMenuPrimitive.RadioItem
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{value}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.RadioIndicator>
<Circle class="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.RadioIndicator>
</span>
<slot />
</DropdownMenuPrimitive.RadioItem>

View File

@ -0,0 +1,14 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.SeparatorProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Separator
class={cn("bg-muted -mx-1 my-1 h-px", className)}
{...$$restProps}
/>

View File

@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<span class={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...$$restProps}>
<slot />
</span>

View File

@ -0,0 +1,30 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.SubContentProps;
type $$Events = DropdownMenuPrimitive.SubContentEvents;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = {
x: -10,
y: 0,
};
export { className as class };
</script>
<DropdownMenuPrimitive.SubContent
{transition}
{transitionConfig}
class={cn(
"bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-lg focus:outline-none",
className
)}
{...$$restProps}
on:keydown
on:focusout
on:pointermove
>
<slot />
</DropdownMenuPrimitive.SubContent>

View File

@ -0,0 +1,32 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
};
type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.SubTrigger
class={cn(
"data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
inset && "pl-8",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
>
<slot />
<ChevronRight class="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>

View File

@ -0,0 +1,48 @@
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Item from "./dropdown-menu-item.svelte";
import Label from "./dropdown-menu-label.svelte";
import Content from "./dropdown-menu-content.svelte";
import Shortcut from "./dropdown-menu-shortcut.svelte";
import RadioItem from "./dropdown-menu-radio-item.svelte";
import Separator from "./dropdown-menu-separator.svelte";
import RadioGroup from "./dropdown-menu-radio-group.svelte";
import SubContent from "./dropdown-menu-sub-content.svelte";
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root;
const Trigger = DropdownMenuPrimitive.Trigger;
const Group = DropdownMenuPrimitive.Group;
export {
Sub,
Root,
Item,
Label,
Group,
Trigger,
Content,
Shortcut,
Separator,
RadioItem,
SubContent,
SubTrigger,
RadioGroup,
CheckboxItem,
//
Root as DropdownMenu,
Sub as DropdownMenuSub,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
Group as DropdownMenuGroup,
Content as DropdownMenuContent,
Trigger as DropdownMenuTrigger,
Shortcut as DropdownMenuShortcut,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
RadioGroup as DropdownMenuRadioGroup,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
CheckboxItem as DropdownMenuCheckboxItem,
};

View File

@ -0,0 +1,34 @@
import { Select as SelectPrimitive } from "bits-ui";
import Label from "./select-label.svelte";
import Item from "./select-item.svelte";
import Content from "./select-content.svelte";
import Trigger from "./select-trigger.svelte";
import Separator from "./select-separator.svelte";
const Root = SelectPrimitive.Root;
const Group = SelectPrimitive.Group;
const Input = SelectPrimitive.Input;
const Value = SelectPrimitive.Value;
export {
Root,
Group,
Input,
Label,
Item,
Value,
Content,
Trigger,
Separator,
//
Root as Select,
Group as SelectGroup,
Input as SelectInput,
Label as SelectLabel,
Item as SelectItem,
Value as SelectValue,
Content as SelectContent,
Trigger as SelectTrigger,
Separator as SelectSeparator,
};

View File

@ -0,0 +1,39 @@
<script lang="ts">
import { Select as SelectPrimitive } from "bits-ui";
import { scale } from "svelte/transition";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = SelectPrimitive.ContentProps;
type $$Events = SelectPrimitive.ContentEvents;
export let sideOffset: $$Props["sideOffset"] = 4;
export let inTransition: $$Props["inTransition"] = flyAndScale;
export let inTransitionConfig: $$Props["inTransitionConfig"] = undefined;
export let outTransition: $$Props["outTransition"] = scale;
export let outTransitionConfig: $$Props["outTransitionConfig"] = {
start: 0.95,
opacity: 0,
duration: 50,
};
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<SelectPrimitive.Content
{inTransition}
{inTransitionConfig}
{outTransition}
{outTransitionConfig}
{sideOffset}
class={cn(
"bg-popover text-popover-foreground relative z-50 min-w-[8rem] overflow-hidden rounded-md border shadow-md outline-none",
className
)}
{...$$restProps}
on:keydown
>
<div class="w-full p-1">
<slot />
</div>
</SelectPrimitive.Content>

View File

@ -0,0 +1,40 @@
<script lang="ts">
import Check from "lucide-svelte/icons/check";
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = SelectPrimitive.ItemProps;
type $$Events = SelectPrimitive.ItemEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export let label: $$Props["label"] = undefined;
export let disabled: $$Props["disabled"] = undefined;
export { className as class };
</script>
<SelectPrimitive.Item
{value}
{disabled}
{label}
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check class="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<slot>
{label || value}
</slot>
</SelectPrimitive.Item>

View File

@ -0,0 +1,16 @@
<script lang="ts">
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = SelectPrimitive.LabelProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<SelectPrimitive.Label
class={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...$$restProps}
>
<slot />
</SelectPrimitive.Label>

View File

@ -0,0 +1,11 @@
<script lang="ts">
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = SelectPrimitive.SeparatorProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<SelectPrimitive.Separator class={cn("bg-muted -mx-1 my-1 h-px", className)} {...$$restProps} />

View File

@ -0,0 +1,27 @@
<script lang="ts">
import { Select as SelectPrimitive } from "bits-ui";
import ChevronDown from "lucide-svelte/icons/chevron-down";
import { cn } from "$lib/utils.js";
type $$Props = SelectPrimitive.TriggerProps;
type $$Events = SelectPrimitive.TriggerEvents;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<SelectPrimitive.Trigger
class={cn(
"border-input bg-background ring-offset-background focus-visible:ring-ring aria-[invalid]:border-destructive data-[placeholder]:[&>span]:text-muted-foreground flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...$$restProps}
let:builder
on:click
on:keydown
>
<slot {builder} />
<div>
<ChevronDown class="h-4 w-4 opacity-50" />
</div>
</SelectPrimitive.Trigger>

View File

@ -1,5 +1,6 @@
<script>
import Figure from '$lib/Figure.svelte';
import TimeFilter from '$lib/components/TimeFilter.svelte';
import { Input } from "$lib/components/ui/input";
import { PUBLIC_API_ENDPOINT } from '$env/static/public';
@ -12,8 +13,9 @@
let debounceTimer;
let showModal = false;
let selectedImage = 0;
let datetimeFilterStart = '';
let datetimeFilterEnd = '';
let startTimestamp = -1;
let endTimestamp = -1;
const debounceDelay = 300;
const apiEndpoint =
@ -22,11 +24,18 @@
/**
* @param {string} query
*/
async function searchItems(query) {
async function searchItems(query, start, end) {
isLoading = true;
try {
const response = await fetch(`${apiEndpoint}/search?q=${encodeURIComponent(query)}`);
let url = `${apiEndpoint}/search?q=${encodeURIComponent(query)}`;
if (start > 0) {
url += `&start=${Math.floor(start / 1000)}`;
}
if (end > 0) {
url += `&end=${Math.floor(end / 1000)}`;
}
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
@ -41,10 +50,10 @@
/**
* @param {string} query
*/
function debounceSearch(query) {
function debounceSearch(query, start, end) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
searchItems(query);
searchItems(query, start, end);
}, debounceDelay);
}
@ -95,10 +104,14 @@
};
$: if (searchString.trim()) {
debounceSearch(searchString);
debounceSearch(searchString, startTimestamp, endTimestamp);
} else {
searchResults = [];
}
$: if ((startTimestamp !== -1 || endTimestamp !== -1) && searchString.trim()) {
debounceSearch(searchString, startTimestamp, endTimestamp);
}
</script>
<svelte:window on:keydown={handleKeydown} />
@ -110,6 +123,7 @@
bind:value={searchString}
placeholder="Type to search..."
/>
<TimeFilter bind:start={startTimestamp} bind:end={endTimestamp} />
</div>
<div class="container mx-auto">