// 社交 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:`,单区域 = `:`。 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; }