import { useEffect, useMemo, useState } from "react"; import { fetchMoneySession } from "./api.js"; import { CountryChannelPanel } from "./components/CountryChannelPanel.jsx"; import { MoneyHeader } from "./components/MoneyHeader.jsx"; import { MoneyOverview } from "./components/MoneyOverview.jsx"; import { MONEY_VIEWS, MoneySidebar } from "./components/MoneySidebar.jsx"; import { OperatorPerformancePanel } from "./components/OperatorPerformancePanel.jsx"; import { PaymentLinkPanel } from "./components/PaymentLinkPanel.jsx"; import { PerformanceTable } from "./components/PerformanceTable.jsx"; import { filterMoneyModules, moneyAppOptions, moneyCountries, moneyOperators, summarizeMoneyModules } from "./data.js"; import { appOptionsFromMasterData, countryOptionsFromMasterData, fetchMoneyMasterData, regionOptionsFromMasterData } from "./masterDataApi.js"; const emptyMasterData = { apps: [], countries: [], error: "", loading: false, modules: [], performanceSummary: {}, regions: [], scopes: [] }; export function MoneyApp() { const [sessionState, setSessionState] = useState({ error: "", loading: true, session: null }); const [masterDataState, setMasterDataState] = useState(emptyMasterData); const [activeView, setActiveView] = useState("overview"); const [mode, setMode] = useState("finance"); const [activeOperatorId, setActiveOperatorId] = useState("sarah-ali"); const [appCode, setAppCode] = useState(""); const [countryCode, setCountryCode] = useState(""); const [regionId, setRegionId] = useState(""); const [operatorKeyword, setOperatorKeyword] = useState(""); const [links, setLinks] = useState([]); const [toast, setToast] = useState(""); useEffect(() => { let mounted = true; fetchMoneySession() .then((session) => { if (mounted) { setSessionState({ error: "", loading: false, session }); } }) .catch((err) => { if (mounted) { setSessionState({ error: err.message || "会话校验失败", loading: false, session: null }); } }); return () => { mounted = false; }; }, []); useEffect(() => { if (!sessionState.session?.canViewMoney) { return undefined; } let mounted = true; setMasterDataState((current) => ({ ...current, error: "", loading: true })); fetchMoneyMasterData() .then((data) => { if (mounted) { setMasterDataState({ ...data, loading: false }); } }) .catch((err) => { if (mounted) { setMasterDataState({ ...emptyMasterData, error: err.message || "主数据加载失败", loading: false }); } }); return () => { mounted = false; }; }, [sessionState.session?.canViewMoney]); const appOptions = useMemo(() => appOptionsFromMasterData(masterDataState.apps, moneyAppOptions), [masterDataState.apps]); const countryOptions = useMemo( () => countryOptionsFromMasterData(masterDataState.countries, moneyCountries), [masterDataState.countries] ); const regionOptions = useMemo(() => regionOptionsFromMasterData(masterDataState.regions), [masterDataState.regions]); useEffect(() => { if (appCode && !hasOption(appOptions, appCode)) { setAppCode(""); } }, [appCode, appOptions]); useEffect(() => { if (countryCode && !hasOption(countryOptions, countryCode)) { setCountryCode(""); } }, [countryCode, countryOptions]); useEffect(() => { if (regionId && !hasOption(regionOptions, regionId)) { setRegionId(""); } }, [regionId, regionOptions]); const filteredModules = useMemo( () => filterMoneyModules({ activeOperatorId, appCode, countryCode, mode, modules: masterDataState.modules, operatorKeyword: mode === "finance" ? operatorKeyword : "", regionId, regions: masterDataState.regions }), [activeOperatorId, appCode, countryCode, masterDataState.modules, masterDataState.regions, mode, operatorKeyword, regionId] ); const summary = useMemo(() => summarizeMoneyModules(filteredModules), [filteredModules]); const visibleModuleIds = useMemo(() => new Set(filteredModules.map((module) => module.id)), [filteredModules]); const visibleLinks = useMemo(() => links.filter((link) => visibleModuleIds.has(link.moduleId)), [links, visibleModuleIds]); const operators = useMemo(() => operatorsFromModules(masterDataState.modules), [masterDataState.modules]); const activeOperator = operators.find((operator) => operator.id === activeOperatorId) || operators[0] || moneyOperators[0]; const view = MONEY_VIEWS.find((item) => item.id === activeView) || MONEY_VIEWS[0]; const resetFilters = () => { setAppCode(""); setCountryCode(""); setRegionId(""); setOperatorKeyword(""); }; const showToast = (message) => { setToast(message); window.setTimeout(() => setToast(""), 1600); }; const copyLink = async (value) => { await copyText(value); showToast("链接已复制"); }; const addPaymentLink = (link) => { setLinks((current) => [link, ...current.filter((item) => (item.orderId || item.id) !== (link.orderId || link.id))]); showToast("支付链接已创建"); }; const exportReport = () => { downloadTextFile(createCsv(filteredModules), `hyapp-money-${Date.now()}.csv`); showToast("财务报表已导出"); }; if (sessionState.loading) { return ; } if (sessionState.error) { return ; } if (!sessionState.session?.canViewMoney) { return ; } return ( {renderContent(activeView, { copyLink, countries: masterDataState.countries, filteredModules, onPaymentLinkCreated: addPaymentLink, regions: masterDataState.regions, summary, visibleLinks })} {toast ? {toast} : null} ); } function renderContent(activeView, { copyLink, countries, filteredModules, onPaymentLinkCreated, regions, summary, visibleLinks }) { if (activeView === "modules") { return ; } if (activeView === "operators") { return ; } if (activeView === "payments") { return ; } if (activeView === "countries") { return ; } return ; } function hasOption(options, value) { return options.some(([optionValue]) => String(optionValue) === String(value)); } function StateScreen({ detail = "", title }) { return ( {title} {detail ? {detail} : null} ); } function createCsv(modules) { const header = ["区域", "负责人", "国家", "充值USD", "流水USD", "存量工资USD", "新增", "日活", "当日目标USD", "当月目标USD"]; const rows = modules.map((module) => [ module.moduleName, module.operatorName, module.countryName, module.rechargeUsd, module.revenueUsd, module.salaryUsd, module.newUsers, module.dau, module.dayTargetUsd, module.monthTargetUsd ]); return [header, ...rows].map((row) => row.map(csvCell).join(",")).join("\n"); } function operatorsFromModules(modules) { const seen = new Set(); return modules .map((module) => ({ department: module.department || "", id: module.operatorId, label: module.operatorName || module.operatorId })) .filter((operator) => { if (!operator.id || seen.has(operator.id)) { return false; } seen.add(operator.id); return true; }); } function csvCell(value) { const text = String(value ?? ""); return /[",\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text; } function downloadTextFile(content, filename) { const blob = new Blob([content], { type: "text/csv;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); } async function copyText(value) { if (navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(value); return; } catch { // 本地 HTTP 调试时浏览器可能拒绝 Clipboard API,退回同步复制以保证财务能拿到刚生成的收款链接。 } } const input = document.createElement("textarea"); input.value = value; input.setAttribute("readonly", ""); input.style.position = "fixed"; input.style.opacity = "0"; document.body.appendChild(input); input.select(); document.execCommand("copy"); input.remove(); }
{detail}