feat(scope): add company scope switcher — two views (by-task all companies / by-company all tasks)

- CompanyScopeContext + Provider with localStorage persistence
- Sidebar company selector (desktop + mobile)
- apiFetch auto-injects company_id on GET requests
- Dashboard summary API supports company_id filter
- Milestones/Financial/DigitalTwins pages use scope instead of manual input
- HealthHeatmap/HealthTrends components react to scope changes
This commit is contained in:
selfrelease
2026-07-20 07:47:53 +08:00
parent 8b8926bde3
commit e81aa6828e
10 changed files with 306 additions and 53 deletions
+88
View File
@@ -0,0 +1,88 @@
/** 企业视角切换 Context — 全局企业筛选器。 */
"use client";
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
import { listCompanies, type Company } from "@/lib/companies";
interface CompanyScopeValue {
/** 选中的企业 IDnull 表示"全部企业" */
companyId: string | null;
/** 选中的企业名称,null 表示"全部企业" */
companyName: string | null;
/** 企业列表 */
companies: Company[];
/** 切换企业 */
setCompanyId: (id: string | null) => void;
/** 是否加载中 */
isLoading: boolean;
}
const CompanyScopeContext = createContext<CompanyScopeValue | undefined>(undefined);
/**
* 企业视角 Provider。
*
* 在侧边栏顶部提供企业选择器,选中后所有页面自动按该企业过滤数据。
* 选择"全部企业"时恢复全局视图。
*/
export function CompanyScopeProvider({ children }: { children: ReactNode }) {
const [companyId, setCompanyIdState] = useState<string | null>(null);
const [companies, setCompanies] = useState<Company[]>([]);
const [isLoading, setIsLoading] = useState(true);
// 加载企业列表
useEffect(() => {
listCompanies({ page_size: 100 })
.then((resp) => {
if (resp.data?.items) setCompanies(resp.data.items);
})
.catch(() => {
// ignore
})
.finally(() => setIsLoading(false));
}, []);
// 从 localStorage 恢复选择
useEffect(() => {
const saved = localStorage.getItem("company_scope_id");
if (saved && saved !== "all") {
setCompanyIdState(saved);
}
}, []);
// 持久化选择
function setCompanyId(id: string | null) {
setCompanyIdState(id);
if (id) {
localStorage.setItem("company_scope_id", id);
} else {
localStorage.setItem("company_scope_id", "all");
}
}
const companyName = companyId
? companies.find((c) => c.id === companyId)?.name ?? null
: null;
return (
<CompanyScopeContext.Provider
value={{ companyId, companyName, companies, setCompanyId, isLoading }}
>
{children}
</CompanyScopeContext.Provider>
);
}
/**
* 使用企业视角 Context。
*
* 返回当前选中的企业 ID,用于各页面过滤数据。
*/
export function useCompanyScope() {
const ctx = useContext(CompanyScopeContext);
if (!ctx) {
throw new Error("useCompanyScope 必须在 CompanyScopeProvider 内使用");
}
return ctx;
}