2026-07-16 11:48:12 +08:00

239 lines
8.1 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";
const TABLE_MODES = new Set(["wide", "requirements"]);
const REQUIREMENT_SECTIONS = new Set(["revenue", "new_users", "active", "gift", "game", "retention"]);
export function createDefaultTableState() {
return {
mode: "wide",
requirementSection: "revenue"
};
}
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",
operator: ALL,
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", "operator", "view", "granularity", "mode", "section"];
export function readStateFromURL(location = window.location) {
const params = new URLSearchParams(location.search);
const filters = createDefaultFilters();
const table = createDefaultTableState();
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 (/^\d+$/.test(params.get("operator") || "")) {
filters.operator = params.get("operator");
}
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");
}
const requestedTableMode = params.get("mode");
if (view === "table" && TABLE_MODES.has(requestedTableMode)) {
table.mode = requestedTableMode;
if (requestedTableMode === "requirements" && REQUIREMENT_SECTIONS.has(params.get("section"))) {
table.requirementSection = params.get("section");
}
}
if (!filters.apps.length) {
filters.apps = [ALL];
}
if (!filters.regions.length) {
filters.regions = [ALL];
}
return { filters, table, view };
}
export function writeStateToURL(filters, view, table, 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.operator !== ALL) {
params.set("operator", filters.operator);
}
if (filters.granularity !== "day") {
params.set("granularity", filters.granularity);
}
if (view && view !== "overview") {
params.set("view", view);
}
if (view === "table" && table?.mode === "requirements") {
// 非默认分组与数据需求模式一起入 URL保证概览下钻和复制后的链接都能落到同一个业务明细视图。
params.set("mode", "requirements");
if (REQUIREMENT_SECTIONS.has(table.requirementSection) && table.requirementSection !== "revenue") {
params.set("section", table.requirementSection);
}
}
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;
}