207 lines
6.8 KiB
JavaScript
207 lines
6.8 KiB
JavaScript
// 社交 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;
|
||
}
|