feat(web): 总览时间范围随日历推进,聚合与引擎状态周期刷新
This commit is contained in:
@@ -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 };
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user