feat(web): 总览时间范围随日历推进,聚合与引擎状态周期刷新

This commit is contained in:
2026-10-07 21:17:22 +08:00
parent 3fb75ec699
commit 2446d3bf2c
5 changed files with 83 additions and 10 deletions
@@ -2,9 +2,8 @@
import type { Dayjs } from "dayjs"; import type { Dayjs } from "dayjs";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { computed } from "vue"; import { computed } from "vue";
import { recentDaysRange } from "@/utils/time.ts"; import { recentDaysRange, type RangeValue } from "@/utils/time.ts";
import type { RangeKind } from "@/composables/use-live-time-range.ts";
type RangeValue = { from: string; to: string };
const props = defineProps<{ const props = defineProps<{
modelValue: RangeValue; modelValue: RangeValue;
@@ -14,6 +13,8 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", value: RangeValue): void; (e: "update:modelValue", value: RangeValue): void;
/** 相对区间可自动跟日历推进;fixed 为绝对窗口 */
(e: "change", value: RangeValue, kind: RangeKind): void;
}>(); }>();
const pickerValue = computed<[Dayjs, Dayjs] | null>(() => { const pickerValue = computed<[Dayjs, Dayjs] | null>(() => {
@@ -43,12 +44,29 @@ function rangeToDayjs(range: RangeValue): [Dayjs, Dayjs] {
return [dayjs(range.from), dayjs(range.to)]; return [dayjs(range.from), dayjs(range.to)];
} }
function detectKind(value: RangeValue): RangeKind {
const fromD = dayjs(value.from).format("YYYY-MM-DD");
const toD = dayjs(value.to).format("YYYY-MM-DD");
for (const days of [7, 30] as const) {
const r = recentDaysRange(days);
if (
dayjs(r.from).format("YYYY-MM-DD") === fromD &&
dayjs(r.to).format("YYYY-MM-DD") === toD
) {
return days === 7 ? "recent-7" : "recent-30";
}
}
return "fixed";
}
function onChange(dates: Dayjs[] | null) { function onChange(dates: Dayjs[] | null) {
if (!dates || dates.length !== 2 || !dates[0] || !dates[1]) return; if (!dates || dates.length !== 2 || !dates[0] || !dates[1]) return;
emit("update:modelValue", { const value = {
from: dates[0].toISOString(), from: dates[0].toISOString(),
to: dates[1].toISOString(), to: dates[1].toISOString(),
}); };
emit("update:modelValue", value);
emit("change", value, detectKind(value));
} }
</script> </script>
@@ -0,0 +1,35 @@
import { computed, onUnmounted, ref } from "vue";
import { recentDaysRange } from "@/utils/time.ts";
export type RangeValue = { from: string; to: string };
export type RangeKind = "recent-7" | "recent-30" | "fixed";
/**
* 可跟随「今天」推进的时间范围:
* - 最近 7/30 天:定时重算窗口(页面挂过夜也不会陈旧)
* - fixed:自定义/订阅期间等绝对区间,窗口不挪
*/
export function useLiveTimeRange(initial: Exclude<RangeKind, "fixed"> = "recent-30") {
const kind = ref<RangeKind>(initial);
const fixed = ref<RangeValue>(recentDaysRange(30));
const nowTick = ref(Date.now());
const timer = setInterval(() => {
nowTick.value = Date.now();
}, 60_000);
onUnmounted(() => clearInterval(timer));
const range = computed<RangeValue>(() => {
void nowTick.value;
if (kind.value === "recent-7") return recentDaysRange(7);
if (kind.value === "recent-30") return recentDaysRange(30);
return fixed.value;
});
function setRange(value: RangeValue, nextKind: RangeKind) {
kind.value = nextKind;
if (nextKind === "fixed") fixed.value = value;
}
return { range, setRange, kind };
}
+2
View File
@@ -27,6 +27,8 @@ export function useAdminOverview(range: () => TimeRange) {
return useQuery({ return useQuery({
queryKey: () => qk.admin.overview(range()), queryKey: () => qk.admin.overview(range()),
queryFn: () => fetchOverview(range()), queryFn: () => fetchOverview(range()),
// 页面可长时间挂起:定期拉取引擎新写入的数据
refetchInterval: 60_000,
}); });
} }
+7 -2
View File
@@ -39,9 +39,14 @@ export function beijingDayEndToIso(date: string): string {
return dayjs.tz(date, DISPLAY_TZ).add(1, "day").startOf("day").toISOString(); return dayjs.tz(date, DISPLAY_TZ).add(1, "day").startOf("day").toISOString();
} }
export type RangeValue = { from: string; to: string };
/** 快捷项:最近 N 天(含今日),返回 [from, to) 的 ISO UTC */ /** 快捷项:最近 N 天(含今日),返回 [from, to) 的 ISO UTC */
export function recentDaysRange(days: number): { from: string; to: string } { export function recentDaysRange(
const to = dayjs().tz(DISPLAY_TZ).add(1, "day").startOf("day"); days: number,
nowMs: number = Date.now(),
): { from: string; to: string } {
const to = dayjs(nowMs).tz(DISPLAY_TZ).add(1, "day").startOf("day");
const from = to.subtract(days, "day"); const from = to.subtract(days, "day");
return { from: from.toISOString(), to: to.toISOString() }; return { from: from.toISOString(), to: to.toISOString() };
} }
+16 -3
View File
@@ -11,12 +11,17 @@ import TimeRangePicker from "@/components/common/time-range-picker.vue";
import QuotaText from "@/components/common/quota-text.vue"; import QuotaText from "@/components/common/quota-text.vue";
import SourceBar from "@/components/common/source-bar.vue"; import SourceBar from "@/components/common/source-bar.vue";
import HealthBand from "@/components/admin/health-band.vue"; import HealthBand from "@/components/admin/health-band.vue";
import {
useLiveTimeRange,
type RangeKind,
type RangeValue,
} from "@/composables/use-live-time-range.ts";
import { useAdminOverview, useEngine, usePayments } from "@/queries/admin.ts"; import { useAdminOverview, useEngine, usePayments } from "@/queries/admin.ts";
import { recentDaysRange, formatMinute } from "@/utils/time.ts"; import { formatMinute } from "@/utils/time.ts";
import { formatInt } from "@/utils/format.ts"; import { formatInt } from "@/utils/format.ts";
const router = useRouter(); const router = useRouter();
const range = ref(recentDaysRange(30)); const { range, setRange } = useLiveTimeRange("recent-30");
const overviewQuery = useAdminOverview(() => range.value); const overviewQuery = useAdminOverview(() => range.value);
const engineQuery = useEngine(); const engineQuery = useEngine();
@@ -60,6 +65,10 @@ function memberPercent(quota: number): number {
return total > 0 ? Math.round((quota / total) * 1000) / 10 : 0; return total > 0 ? Math.round((quota / total) * 1000) / 10 : 0;
} }
function onRangeChange(value: RangeValue, kind: RangeKind) {
setRange(value, kind);
}
function onMemberClick(memberId: number | null) { function onMemberClick(memberId: number | null) {
void router.push({ void router.push({
path: "/admin/keys", path: "/admin/keys",
@@ -155,7 +164,11 @@ const keyColumns = [
> >
<template #actions> <template #actions>
<span class="overview-range-label">时间范围</span> <span class="overview-range-label">时间范围</span>
<TimeRangePicker v-model="range" :extra-presets="extraPresets" /> <TimeRangePicker
:model-value="range"
:extra-presets="extraPresets"
@change="onRangeChange"
/>
</template> </template>
</PageHeader> </PageHeader>