fix(scope): all pages react to company scope change via useScopeEffect

- Created useScopeEffect hook that auto-triggers reload on scope change
- Replaced useEffect([], []) with useScopeEffect in 20+ pages
- Risks/Reports/Dashboard also use useScopeEffect
- Removed redundant per-page useCompanyScope where only effect was needed
This commit is contained in:
selfrelease
2026-07-20 08:09:33 +08:00
parent 4265f4bc4c
commit c3232f73c8
23 changed files with 105 additions and 63 deletions
+4 -3
View File
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useScopeEffect } from "@/lib/company-scope";
import {Sparkles } from "lucide-react";
import { listAARs, generateAAR } from "@/lib/api-v2";
import { PageContainer, Card} from "@/components/shared/PageContainer";
@@ -22,12 +23,12 @@ export default function AARsPage() {
const [originalPlan, setOriginalPlan] = useState("");
const [actualResult, setActualResult] = useState("");
useEffect(() => {
useScopeEffect(() => {
listAARs()
.then((resp) => setItems((resp.data as AARItem[]) ?? []))
.catch(() => setItems([]))
.finally(() => setIsLoading(false));
}, []);
});
const handleGenerate = async () => {
if (!triggerEvent.trim()) {
+3 -2
View File
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useScopeEffect } from "@/lib/company-scope";
import { listAgentExecutions, reviewAgentExecution } from "@/lib/api-v2";
import { PageContainer, Badge} from "@/components/shared/PageContainer";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
@@ -28,7 +29,7 @@ export default function AgentsPage() {
.finally(() => setIsLoading(false));
};
useEffect(() => { load(); }, []);
useScopeEffect(() => { load(); });
const handleReview = async (id: string, status: string) => {
try {
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useScopeEffect } from "@/lib/company-scope";
import {Plus } from "lucide-react";
import { listAgreements } from "@/lib/api-v2";
import { PageContainer, Badge, TableEmpty } from "@/components/shared/PageContainer";
@@ -19,12 +20,12 @@ export default function AgreementsPage() {
const [items, setItems] = useState<Agreement[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
useScopeEffect(() => {
listAgreements()
.then((resp) => setItems((resp.data as Agreement[]) ?? []))
.catch(() => setItems([]))
.finally(() => setIsLoading(false));
}, []);
});
return (
<PageContainer
+4 -3
View File
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useScopeEffect } from "@/lib/company-scope";
import {Plus } from "lucide-react";
import { listInterventions } from "@/lib/api-v2";
import { PageContainer, Badge, Card } from "@/components/shared/PageContainer";
@@ -19,12 +20,12 @@ export default function AlphaPage() {
const [items, setItems] = useState<Intervention[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
useScopeEffect(() => {
listInterventions()
.then((resp) => setItems((resp.data as Intervention[]) ?? []))
.catch(() => setItems([]))
.finally(() => setIsLoading(false));
}, []);
});
return (
<PageContainer
+4 -3
View File
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useScopeEffect } from "@/lib/company-scope";
import { listBoardMeetings } from "@/lib/api-v2";
import { PageContainer, Badge} from "@/components/shared/PageContainer";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
@@ -18,12 +19,12 @@ export default function BoardPage() {
const [items, setItems] = useState<BoardMeeting[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
useScopeEffect(() => {
listBoardMeetings()
.then((resp) => setItems((resp.data as BoardMeeting[]) ?? []))
.catch(() => setItems([]))
.finally(() => setIsLoading(false));
}, []);
});
return (
<PageContainer title="董事会管理" description="AI 会前材料摘要 → 决议追踪 → 提问清单生成">
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useScopeEffect } from "@/lib/company-scope";
import { apiFetch } from "@/lib/api";
import { PlanCard } from "@/components/customer/PlanCard";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
@@ -34,10 +35,10 @@ export default function CustomerGrowthPage() {
}
}
useEffect(() => {
useScopeEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
loadPlans();
}, []);
});
async function handleGenerate() {
if (!companyContext.trim()) return;
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useScopeEffect } from "@/lib/company-scope";
import { apiFetch } from "@/lib/api";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
import { EmptyState } from "@/components/shared/EmptyState";
@@ -50,10 +51,10 @@ export default function CustomerSuccessPage() {
}
}
useEffect(() => {
useScopeEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
loadChurnRisks();
}, []);
});
async function handleQBR() {
if (!qbrData.trim()) return;
@@ -1,10 +1,10 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { Building2, Heart, AlertTriangle, FileText, TrendingUp, TrendingDown, Minus } from "lucide-react";
import Link from "next/link";
import { getDashboardSummary, type DashboardSummary } from "@/lib/dashboard";
import { useCompanyScope } from "@/lib/company-scope";
import { useCompanyScope, useScopeEffect } from "@/lib/company-scope";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
import { EmptyState } from "@/components/shared/EmptyState";
import { HealthScoreBadge } from "@/components/shared/HealthScoreBadge";
@@ -18,7 +18,7 @@ export default function InvestorDashboardPage() {
const [summary, setSummary] = useState<DashboardSummary | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
useScopeEffect(() => {
async function load() {
try {
const resp = await getDashboardSummary();
@@ -30,7 +30,7 @@ export default function InvestorDashboardPage() {
}
}
load();
}, []);
});
if (isLoading) {
return (
+4 -3
View File
@@ -1,7 +1,8 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { listMajorEvents, listInquiries } from "@/lib/api-v2";
import { useScopeEffect } from "@/lib/company-scope";
import { PageContainer, Badge } from "@/components/shared/PageContainer";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
import { EmptyState } from "@/components/shared/EmptyState";
@@ -26,7 +27,7 @@ export default function EventsPage() {
const [isLoading, setIsLoading] = useState(true);
const [tab, setTab] = useState<"events" | "inquiries">("events");
useEffect(() => {
useScopeEffect(() => {
Promise.all([listMajorEvents(), listInquiries()])
.then(([e, i]) => {
setEvents((e.data as MajorEvent[]) ?? []);
@@ -37,7 +38,7 @@ export default function EventsPage() {
setInquiries([]);
})
.finally(() => setIsLoading(false));
}, []);
});
return (
<PageContainer title="重大事项 & 追问清单" description="AI 从月报/弱信号中自动识别重大事项 + 生成追问清单">
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useScopeEffect } from "@/lib/company-scope";
import {Sparkles } from "lucide-react";
import { listExitPredictions, predictExit } from "@/lib/api-v2";
import { PageContainer, Badge, Card } from "@/components/shared/PageContainer";
@@ -22,12 +23,12 @@ export default function ExitSignalsPage() {
const [companyData, setCompanyData] = useState("");
const [predicting, setPredicting] = useState(false);
useEffect(() => {
useScopeEffect(() => {
listExitPredictions()
.then((resp) => setItems((resp.data as ExitPrediction[]) ?? []))
.catch(() => setItems([]))
.finally(() => setIsLoading(false));
}, []);
});
const handlePredict = async () => {
if (!companyData.trim()) {
+4 -3
View File
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useScopeEffect } from "@/lib/company-scope";
import { apiFetch } from "@/lib/api";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
import { EmptyState } from "@/components/shared/EmptyState";
@@ -41,10 +42,10 @@ export default function NudgesPage() {
}
}
useEffect(() => {
useScopeEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
loadNudges();
}, []);
});
async function handleSelect() {
if (!context.trim()) return;
+4 -3
View File
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useScopeEffect } from "@/lib/company-scope";
import {Plus } from "lucide-react";
import { listOKRs } from "@/lib/api-v2";
import { PageContainer, Badge, Card } from "@/components/shared/PageContainer";
@@ -27,12 +28,12 @@ export default function OKRsPage() {
const [items, setItems] = useState<OKRItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
useScopeEffect(() => {
listOKRs()
.then((resp) => setItems((resp.data as OKRItem[]) ?? []))
.catch(() => setItems([]))
.finally(() => setIsLoading(false));
}, []);
});
return (
<PageContainer
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useScopeEffect } from "@/lib/company-scope";
import { apiFetch } from "@/lib/api";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
import { EmptyState } from "@/components/shared/EmptyState";
@@ -49,10 +50,10 @@ export default function PeerCirclesPage() {
}
}
useEffect(() => {
useScopeEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
loadCircles();
}, []);
});
async function handleMatch() {
if (!foundersContext.trim()) return;
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useScopeEffect } from "@/lib/company-scope";
import {Sparkles } from "lucide-react";
import { listPreMortems, runPreMortem, listRedTeams, runRedTeam } from "@/lib/api-v2";
import { PageContainer, Card, Badge } from "@/components/shared/PageContainer";
@@ -33,7 +34,7 @@ export default function PreMortemsPage() {
const [input, setInput] = useState("");
const [perspective, setPerspective] = useState("competitor");
useEffect(() => {
useScopeEffect(() => {
Promise.all([listPreMortems(), listRedTeams()])
.then(([pm, rt]) => {
setPreMortems((pm.data as PreMortemItem[]) ?? []);
@@ -44,7 +45,7 @@ export default function PreMortemsPage() {
setRedTeams([]);
})
.finally(() => setIsLoading(false));
}, []);
});
const handleRun = async () => {
if (!input.trim()) {
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useScopeEffect } from "@/lib/company-scope";
import { apiFetch } from "@/lib/api";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
import { EmptyState } from "@/components/shared/EmptyState";
@@ -46,10 +47,10 @@ export default function ProductDiagnosticsPage() {
}
}
useEffect(() => {
useScopeEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
loadDiagnostics();
}, []);
});
async function handleDiagnose() {
if (!productInfo.trim()) return;
+3 -2
View File
@@ -1,8 +1,9 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import { useState, useCallback } from "react";
import {Plus, Trash2, Send } from "lucide-react";
import { listReports, submitReport, deleteReport, STATUS_LABELS, STATUS_COLORS, type MonthlyReport } from "@/lib/reports";
import { useScopeEffect } from "@/lib/company-scope";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
import { EmptyState } from "@/components/shared/EmptyState";
@@ -31,7 +32,7 @@ export default function ReportsPage() {
}
}, [page]);
useEffect(() => {
useScopeEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
loadReports();
}, [loadReports]);
+3 -2
View File
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import { useState, useCallback } from "react";
import { Filter } from "lucide-react";
import { toast } from "sonner";
import {
@@ -10,6 +10,7 @@ import {
RISK_STATUS_LABELS,
type RiskEvent,
} from "@/lib/risks";
import { useScopeEffect } from "@/lib/company-scope";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
import { EmptyState } from "@/components/shared/EmptyState";
import { RiskCard } from "@/components/risk/RiskCard";
@@ -44,7 +45,7 @@ export default function RisksPage() {
}
}, [page, statusFilter]);
useEffect(() => {
useScopeEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
loadRisks();
}, [loadRisks]);
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useScopeEffect } from "@/lib/company-scope";
import { listDecisionSentinels } from "@/lib/api-v2";
import { PageContainer, Badge} from "@/components/shared/PageContainer";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
@@ -18,12 +19,12 @@ export default function SentinelsPage() {
const [items, setItems] = useState<DecisionSentinel[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
useScopeEffect(() => {
listDecisionSentinels()
.then((resp) => setItems((resp.data as DecisionSentinel[]) ?? []))
.catch(() => setItems([]))
.finally(() => setIsLoading(false));
}, []);
});
return (
<PageContainer title="决策前哨" description="AI 识别关键决策岔路口 → 场景分析 → 提前预警">
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useScopeEffect } from "@/lib/company-scope";
import { listSynergies, authorizeSynergy } from "@/lib/api-v2";
import { PageContainer, Badge } from "@/components/shared/PageContainer";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
@@ -37,7 +38,7 @@ export default function SynergiesPage() {
.finally(() => setIsLoading(false));
};
useEffect(() => { load(); }, []);
useScopeEffect(() => { load(); });
const handleAuthorize = async (id: string) => {
try {
+4 -3
View File
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useScopeEffect } from "@/lib/company-scope";
import { listTalents } from "@/lib/api-v2";
import { PageContainer, Badge } from "@/components/shared/PageContainer";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
@@ -20,12 +21,12 @@ export default function TalentsPage() {
const [items, setItems] = useState<Talent[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
useScopeEffect(() => {
listTalents()
.then((resp) => setItems((resp.data as Talent[]) ?? []))
.catch(() => setItems([]))
.finally(() => setIsLoading(false));
}, []);
});
return (
<PageContainer title="人才引力场" description="人才流动预测 + 主动推荐 + 9-Box 矩阵">
+3 -2
View File
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useScopeEffect } from "@/lib/company-scope";
import {Plus } from "lucide-react";
import { listTasks, updateTask } from "@/lib/api-v2";
import { PageContainer, Badge } from "@/components/shared/PageContainer";
@@ -42,7 +43,7 @@ export default function TasksPage() {
.finally(() => setIsLoading(false));
};
useEffect(() => { load(); }, []);
useScopeEffect(() => { load(); });
const handleStatusChange = async (id: string, status: string) => {
try {
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useScopeEffect } from "@/lib/company-scope";
import { listWeakSignals } from "@/lib/api-v2";
import { PageContainer, Badge} from "@/components/shared/PageContainer";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
@@ -26,12 +27,12 @@ export default function WeakSignalsPage() {
const [items, setItems] = useState<WeakSignal[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
useScopeEffect(() => {
listWeakSignals()
.then((resp) => setItems((resp.data as WeakSignal[]) ?? []))
.catch(() => setItems([]))
.finally(() => setIsLoading(false));
}, []);
});
return (
<PageContainer title="弱信号监控" description="技术/情绪/组织/市场四类弱信号采集与关联分析">
+22 -1
View File
@@ -51,7 +51,7 @@ export function CompanyScopeProvider({ children }: { children: ReactNode }) {
}
}, []);
// 持久化选择
// 持久化选择 + 派发事件通知各页面重新加载
function setCompanyId(id: string | null) {
setCompanyIdState(id);
if (id) {
@@ -59,6 +59,8 @@ export function CompanyScopeProvider({ children }: { children: ReactNode }) {
} else {
localStorage.setItem("company_scope_id", "all");
}
// 派发全局事件,让监听了 useScopeEffect 的页面重新加载数据
window.dispatchEvent(new CustomEvent("company-scope-change", { detail: { companyId: id } }));
}
const companyName = companyId
@@ -86,3 +88,22 @@ export function useCompanyScope() {
}
return ctx;
}
/**
* 监听企业视角变化,自动触发回调重新加载数据。
*
* 各页面在 useEffect 中调用此 hook,当全局企业选择变化时,
* 会自动触发回调重新加载(apiFetch 会自动注入新的 company_id)。
*
* @param callback 数据加载回调
* @param deps 额外依赖(如分页、筛选状态)
*/
export function useScopeEffect(callback: () => void, deps: unknown[] = []) {
const { companyId } = useCompanyScope();
useEffect(() => {
callback();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [companyId, ...deps]);
}