feat(web): 总览时间范围随日历推进,聚合与引擎状态周期刷新
This commit is contained in:
@@ -2,9 +2,8 @@
|
||||
import type { Dayjs } from "dayjs";
|
||||
import dayjs from "dayjs";
|
||||
import { computed } from "vue";
|
||||
import { recentDaysRange } from "@/utils/time.ts";
|
||||
|
||||
type RangeValue = { from: string; to: string };
|
||||
import { recentDaysRange, type RangeValue } from "@/utils/time.ts";
|
||||
import type { RangeKind } from "@/composables/use-live-time-range.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: RangeValue;
|
||||
@@ -14,6 +13,8 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: RangeValue): void;
|
||||
/** 相对区间可自动跟日历推进;fixed 为绝对窗口 */
|
||||
(e: "change", value: RangeValue, kind: RangeKind): void;
|
||||
}>();
|
||||
|
||||
const pickerValue = computed<[Dayjs, Dayjs] | null>(() => {
|
||||
@@ -43,12 +44,29 @@ function rangeToDayjs(range: RangeValue): [Dayjs, Dayjs] {
|
||||
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) {
|
||||
if (!dates || dates.length !== 2 || !dates[0] || !dates[1]) return;
|
||||
emit("update:modelValue", {
|
||||
const value = {
|
||||
from: dates[0].toISOString(),
|
||||
to: dates[1].toISOString(),
|
||||
});
|
||||
};
|
||||
emit("update:modelValue", value);
|
||||
emit("change", value, detectKind(value));
|
||||
}
|
||||
</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 };
|
||||
}
|
||||
@@ -27,6 +27,8 @@ export function useAdminOverview(range: () => TimeRange) {
|
||||
return useQuery({
|
||||
queryKey: () => qk.admin.overview(range()),
|
||||
queryFn: () => fetchOverview(range()),
|
||||
// 页面可长时间挂起:定期拉取引擎新写入的数据
|
||||
refetchInterval: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -39,9 +39,14 @@ export function beijingDayEndToIso(date: string): string {
|
||||
return dayjs.tz(date, DISPLAY_TZ).add(1, "day").startOf("day").toISOString();
|
||||
}
|
||||
|
||||
export type RangeValue = { from: string; to: string };
|
||||
|
||||
/** 快捷项:最近 N 天(含今日),返回 [from, to) 的 ISO UTC */
|
||||
export function recentDaysRange(days: number): { from: string; to: string } {
|
||||
const to = dayjs().tz(DISPLAY_TZ).add(1, "day").startOf("day");
|
||||
export function recentDaysRange(
|
||||
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");
|
||||
return { from: from.toISOString(), to: to.toISOString() };
|
||||
}
|
||||
|
||||
@@ -11,12 +11,17 @@ import TimeRangePicker from "@/components/common/time-range-picker.vue";
|
||||
import QuotaText from "@/components/common/quota-text.vue";
|
||||
import SourceBar from "@/components/common/source-bar.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 { recentDaysRange, formatMinute } from "@/utils/time.ts";
|
||||
import { formatMinute } from "@/utils/time.ts";
|
||||
import { formatInt } from "@/utils/format.ts";
|
||||
|
||||
const router = useRouter();
|
||||
const range = ref(recentDaysRange(30));
|
||||
const { range, setRange } = useLiveTimeRange("recent-30");
|
||||
|
||||
const overviewQuery = useAdminOverview(() => range.value);
|
||||
const engineQuery = useEngine();
|
||||
@@ -60,6 +65,10 @@ function memberPercent(quota: number): number {
|
||||
return total > 0 ? Math.round((quota / total) * 1000) / 10 : 0;
|
||||
}
|
||||
|
||||
function onRangeChange(value: RangeValue, kind: RangeKind) {
|
||||
setRange(value, kind);
|
||||
}
|
||||
|
||||
function onMemberClick(memberId: number | null) {
|
||||
void router.push({
|
||||
path: "/admin/keys",
|
||||
@@ -155,7 +164,11 @@ const keyColumns = [
|
||||
>
|
||||
<template #actions>
|
||||
<span class="overview-range-label">时间范围</span>
|
||||
<TimeRangePicker v-model="range" :extra-presets="extraPresets" />
|
||||
<TimeRangePicker
|
||||
:model-value="range"
|
||||
:extra-presets="extraPresets"
|
||||
@change="onRangeChange"
|
||||
/>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user