2026-07-03 19:52:18 +08:00

207 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 社交 BI v2 的全局筛选状态:日期预设/自定义区间、App 多选、区域多选、趋势粒度,
// 并同步到 URL query保证筛选状态可以直接作为链接分享。
import { lastDaysRange, thisMonthRange, todayRange } from "../utils/time.js";
export const ALL = "all";
export const DATE_PRESETS = [
{ key: "today", label: "今日" },
{ key: "yesterday", label: "昨日" },
{ key: "last7", label: "近 7 日" },
{ key: "last30", label: "近 30 日" },
{ key: "month", label: "本月" },
{ key: "prevMonth", label: "上月" },
{ key: "custom", label: "自定义" }
];
export const GRANULARITIES = [
{ key: "day", label: "日" },
{ key: "week", label: "周" },
{ key: "month", label: "月" }
];
export function createDefaultFilters() {
return {
apps: [ALL],
customEnd: "",
customStart: "",
granularity: "day",
preset: "yesterday",
regions: [ALL]
};
}
export function resolveDateRange(filters, timeZone) {
const today = todayRange(timeZone).start;
switch (filters.preset) {
case "today":
return { end: today, start: today };
case "yesterday": {
const yesterday = shiftDate(today, -1);
return { end: yesterday, start: yesterday };
}
case "last7": {
const range = lastDaysRange(7, timeZone);
return { end: range.end, start: range.start };
}
case "last30": {
const range = lastDaysRange(30, timeZone);
return { end: range.end, start: range.start };
}
case "month": {
const range = thisMonthRange(timeZone);
return { end: range.end, start: range.start };
}
case "prevMonth": {
const monthStart = thisMonthRange(timeZone).start;
const prevEnd = shiftDate(monthStart, -1);
return { end: prevEnd, start: `${prevEnd.slice(0, 7)}-01` };
}
case "custom": {
const start = normalizeDateText(filters.customStart) || shiftDate(today, -7);
const end = normalizeDateText(filters.customEnd) || today;
return end < start ? { end: start, start: end } : { end, start };
}
default: {
const yesterday = shiftDate(today, -1);
return { end: yesterday, start: yesterday };
}
}
}
export function rangeDayCount(range) {
const start = Date.parse(`${range.start}T00:00:00Z`);
const end = Date.parse(`${range.end}T00:00:00Z`);
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) {
return 1;
}
return Math.round((end - start) / 86400000) + 1;
}
export function rangeLabel(range) {
return range.start === range.end ? range.start : `${range.start} ~ ${range.end}`;
}
export function shiftDate(dateText, days) {
const [year, month, day] = String(dateText).split("-").map(Number);
const date = new Date(Date.UTC(year || 1970, (month || 1) - 1, (day || 1) + days));
return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())}`;
}
function normalizeDateText(value) {
const text = String(value || "").trim();
return /^\d{4}-\d{2}-\d{2}$/.test(text) ? text : "";
}
function pad2(value) {
return String(value).padStart(2, "0");
}
// stat_day → 周/月粒度的分桶 key周取周一为锚点与运营周报口径一致。
export function bucketDay(statDay, granularity) {
if (granularity === "month") {
return String(statDay).slice(0, 7);
}
if (granularity === "week") {
const time = Date.parse(`${statDay}T00:00:00Z`);
if (!Number.isFinite(time)) {
return statDay;
}
const date = new Date(time);
const weekday = (date.getUTCDay() + 6) % 7;
date.setUTCDate(date.getUTCDate() - weekday);
return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())}`;
}
return statDay;
}
const URL_KEYS = ["preset", "start", "end", "apps", "regions", "view", "granularity"];
export function readStateFromURL(location = window.location) {
const params = new URLSearchParams(location.search);
const filters = createDefaultFilters();
let view = "";
if (params.get("preset") && DATE_PRESETS.some((item) => item.key === params.get("preset"))) {
filters.preset = params.get("preset");
}
filters.customStart = normalizeDateText(params.get("start"));
filters.customEnd = normalizeDateText(params.get("end"));
if (params.get("apps")) {
filters.apps = params.get("apps").split(",").map((item) => item.trim().toLowerCase()).filter(Boolean);
}
if (params.get("regions")) {
filters.regions = params.get("regions").split(",").map((item) => item.trim()).filter(Boolean);
}
if (params.get("granularity") && GRANULARITIES.some((item) => item.key === params.get("granularity"))) {
filters.granularity = params.get("granularity");
}
if (params.get("view")) {
view = params.get("view");
}
if (!filters.apps.length) {
filters.apps = [ALL];
}
if (!filters.regions.length) {
filters.regions = [ALL];
}
return { filters, view };
}
export function writeStateToURL(filters, view, history = window.history, location = window.location) {
const params = new URLSearchParams(location.search);
URL_KEYS.forEach((key) => params.delete(key));
if (filters.preset !== "yesterday") {
params.set("preset", filters.preset);
}
if (filters.preset === "custom") {
if (filters.customStart) {
params.set("start", filters.customStart);
}
if (filters.customEnd) {
params.set("end", filters.customEnd);
}
}
if (filters.apps.length && !filters.apps.includes(ALL)) {
params.set("apps", filters.apps.join(","));
}
if (filters.regions.length && !filters.regions.includes(ALL)) {
params.set("regions", filters.regions.join(","));
}
if (filters.granularity !== "day") {
params.set("granularity", filters.granularity);
}
if (view && view !== "overview") {
params.set("view", view);
}
const query = params.toString();
history.replaceState(null, "", `${location.pathname}${query ? `?${query}` : ""}`);
}
// 区域选择值格式:整 App = `app:<appCode>`,单区域 = `<appCode>:<regionId>`。
export function regionSelectionMatches(selection, appCode, regionId) {
if (!selection?.length || selection.includes(ALL)) {
return true;
}
return selection.includes(`app:${appCode}`) || selection.includes(`${appCode}:${regionId}`);
}
export function appSelectionMatches(selection, appCode) {
if (!selection?.length || selection.includes(ALL)) {
return true;
}
return selection.includes(String(appCode || "").toLowerCase());
}
export function toggleMultiValue(current, value, allValues) {
if (value === ALL) {
return [ALL];
}
const withoutAll = current.filter((item) => item !== ALL);
const next = withoutAll.includes(value) ? withoutAll.filter((item) => item !== value) : [...withoutAll, value];
if (!next.length || (allValues?.length && allValues.every((item) => next.includes(item)))) {
return [ALL];
}
return next;
}