// 社交 BI v2 Shell:深色侧栏(视图导航)+ 顶栏全局筛选(日期/App/区域/粒度), // 视图组件通过 useSocialBi() 消费统一的筛选状态与数据派生结果。 import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import AccountTreeOutlined from "@mui/icons-material/AccountTreeOutlined"; import GroupsOutlined from "@mui/icons-material/GroupsOutlined"; import InsightsOutlined from "@mui/icons-material/InsightsOutlined"; import PublicOutlined from "@mui/icons-material/PublicOutlined"; import RefreshOutlined from "@mui/icons-material/RefreshOutlined"; import RepeatOutlined from "@mui/icons-material/RepeatOutlined"; import TableChartOutlined from "@mui/icons-material/TableChartOutlined"; import { ALL, DATE_PRESETS, GRANULARITIES, createDefaultFilters, rangeLabel, readStateFromURL, toggleMultiValue, writeStateToURL } from "./state.js"; import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx"; import { AppIdentity } from "@/shared/ui/AppIdentity.jsx"; import { SOCIAL_BI_FUNNEL_APP_CODES, useSocialBiData } from "./useSocialBiData.js"; import { OverviewView } from "./views/OverviewView.jsx"; import { RegionsView } from "./views/RegionsView.jsx"; import { TeamKpiView } from "./views/TeamKpiView.jsx"; import { RetentionView } from "./views/RetentionView.jsx"; import { DataTableView } from "./views/DataTableView.jsx"; import { FunnelView } from "./views/FunnelView.jsx"; import "../styles/social-v2.css"; const VIEWS = [ { component: OverviewView, icon: InsightsOutlined, key: "overview", label: "经营概览" }, { component: RegionsView, icon: PublicOutlined, key: "regions", label: "地区洞察" }, { component: TeamKpiView, icon: GroupsOutlined, key: "team", label: "运营中心" }, { component: RetentionView, icon: RepeatOutlined, key: "retention", label: "留存质量" }, { component: DataTableView, icon: TableChartOutlined, key: "table", label: "数据明细" }, { component: FunnelView, icon: AccountTreeOutlined, key: "funnel", label: "埋点漏斗" } ]; const SocialBiContext = createContext(null); export function useSocialBi() { const context = useContext(SocialBiContext); if (!context) { throw new Error("useSocialBi must be used inside SocialBiApp"); } return context; } // Social BI 的统计结果只携带 app_code/app_name;Logo 从 master 目录按 app_code 回填,保证所有视图使用同一份品牌事实。 export function SocialAppIdentity({ app, appCode, appName, className = "", size = "small" }) { const context = useSocialBi(); const resolvedCode = String(app?.app_code || app?.appCode || appCode || "").trim().toLowerCase(); const catalogApp = (context.master?.apps || []).find((item) => String(item.app_code || item.appCode || "").trim().toLowerCase() === resolvedCode); return ; } export function SocialBiApp() { const initial = useMemo(() => readStateFromURL(), []); const [filters, setFilters] = useState(initial.filters); const [viewKey, setViewKey] = useState(() => (VIEWS.some((view) => view.key === initial.view) ? initial.view : "overview")); const data = useSocialBiData(filters, { includeFunnel: viewKey === "funnel", includeKpi: viewKey === "team" }); useEffect(() => { writeStateToURL(filters, viewKey); }, [filters, viewKey]); const updateFilter = useCallback((key, value) => { setFilters((current) => ({ ...current, [key]: value })); }, []); const resetFilters = useCallback(() => { setFilters(createDefaultFilters()); }, []); const contextValue = useMemo( () => ({ ...data, filters, resetFilters, setViewKey, updateFilter, viewKey }), [data, filters, resetFilters, updateFilter, viewKey] ); const ActiveView = (VIEWS.find((view) => view.key === viewKey) || VIEWS[0]).component; return (
{data.errors.length ? (
{data.errors.map((message, index) => ( {message} ))}
) : null}
); } function Sidebar({ data, onViewChange, viewKey }) { const access = data.master?.access; const scopeLabel = !data.master ? "" : access?.all ? "全量数据范围" : `负责 ${access?.scopes?.length || 0} 个 App/区域`; return ( ); } function TopBar({ data, filters, updateFilter, viewKey }) { return (
{rangeLabel(data.range)} {data.isLoading ? : null}
); } function DatePresetControl({ filters, updateFilter }) { return (
{DATE_PRESETS.map((preset) => ( ))} {filters.preset === "custom" ? ( { updateFilter("customStart", msToDayText(range.startMs)); updateFilter("customEnd", msToDayText(range.endMs)); }} /> ) : null}
); } // 自定义区间在筛选状态里以天字符串(YYYY-MM-DD)存储,选择器以本地时区毫秒交互,这里做双向换算。 function parseDayText(value) { const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value || "")); return match ? { day: Number(match[3]), month: Number(match[2]), year: Number(match[1]) } : null; } function dayTextToStartMs(dayText) { const parts = parseDayText(dayText); return parts ? new Date(parts.year, parts.month - 1, parts.day, 0, 0, 0, 0).getTime() : ""; } function dayTextToEndMs(dayText) { const parts = parseDayText(dayText); return parts ? new Date(parts.year, parts.month - 1, parts.day, 23, 59, 59, 999).getTime() : ""; } function msToDayText(ms) { if (!ms) { return ""; } const date = new Date(Number(ms)); const pad = (value) => String(value).padStart(2, "0"); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; } function GranularityControl({ filters, updateFilter }) { return (
{GRANULARITIES.map((item) => ( ))}
); } function AppChips({ data, filters, updateFilter, viewKey }) { const apps = viewKey === "funnel" ? (data.master?.apps || []).filter((app) => SOCIAL_BI_FUNNEL_APP_CODES.includes(app.app_code)) : data.master?.apps || []; const allCodes = apps.map((app) => app.app_code); const hasSelectedVisibleApp = filters.apps.some((appCode) => allCodes.includes(appCode)); const isAll = filters.apps.includes(ALL) || (viewKey === "funnel" && !hasSelectedVisibleApp); return (
{apps.map((app) => { const selected = !isAll && filters.apps.includes(app.app_code); return ( ); })}
); } function RegionSelect({ data, filters, updateFilter }) { const [isOpen, setIsOpen] = useState(false); const containerRef = useRef(null); const apps = data.master?.apps || []; const regions = data.master?.regions || []; const groups = apps .map((app) => ({ app, regions: regions.filter((region) => region.app_code === app.app_code) })) .filter((group) => group.regions.length); useEffect(() => { const handlePointerDown = (event) => { if (!containerRef.current?.contains(event.target)) { setIsOpen(false); } }; document.addEventListener("pointerdown", handlePointerDown); return () => document.removeEventListener("pointerdown", handlePointerDown); }, []); const selectedCount = filters.regions.includes(ALL) ? 0 : filters.regions.length; return (
{isOpen ? (
{groups.map((group) => (
{group.regions.map((region) => { const value = `${region.app_code}:${region.region_id}`; return ( ); })}
))} {!groups.length ?

暂无区域目录

: null}
) : null}
); }