diff --git a/.gitignore b/.gitignore index 6b5df63..b44b4b3 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +dev-server.log +install.log .idea yarn.lock /openspec/ diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 85253c9..0000000 --- a/.npmrc +++ /dev/null @@ -1,4 +0,0 @@ -public-hoist-pattern[]=*loader -public-hoist-pattern[]=html-webpack-plugin -public-hoist-pattern[]=react-refresh -public-hoist-pattern[]=@rspack/plugin-react-refresh diff --git a/docs/dependency-issues-guide.md b/docs/dependency-issues-guide.md new file mode 100644 index 0000000..cb22981 --- /dev/null +++ b/docs/dependency-issues-guide.md @@ -0,0 +1,252 @@ +# 依赖问题排查指南 + +本文档记录本项目在依赖安装与启动过程中的实际问题、根因与解决方案。 + +## 项目环境基线 + +| 项 | 值 | +| --- | --- | +| 包管理器 | **pnpm 11.17.0**(唯一,仓库只有 `pnpm-lock.yaml`,无 `package-lock.json`) | +| Node | v22.23.1 | +| 构建工具 | Rspack 1.7.12(经 `@cqsjjb/scripts` 驱动) | +| 开发端口 | **8080**(定义在 `jjb.config.js`) | +| 启动命令 | `npm run serve:development` | +| pnpm 配置文件 | **`pnpm-workspace.yaml`**(项目已无 `.npmrc`) | + +> **最重要的一条**:本项目使用 pnpm 11。**pnpm 11 不再从 `.npmrc` 读取 `public-hoist-pattern`、`hoist`、`node-linker` 等安装配置**,这些配置已迁移到 `pnpm-workspace.yaml`,并改用小驼峰命名(`publicHoistPattern`、`nodeLinker`)。项目原有的 `.npmrc` 因此完全失效,已被删除。 + +--- + +## 1. `Unable to resolve loader babel-loader` — loader 未被提升 + +这是本项目实际遇到的**唯一阻塞启动**的错误。 + +### 现象 + +```text +ERROR in × Unable to resolve loader babel-loader??ruleSet[1].rules[2].use[0] +1 ERROR in child compilations +Rspack 1.7.12 compiled with 2 errors +``` + +### 根因 + +`babel-loader` 是 `@cqsjjb/scripts` 的**传递依赖**,并未写在本项目 `package.json` 中。pnpm 默认采用隔离式(isolated)`node_modules` 布局,传递依赖只存在于 `node_modules/.pnpm/` 虚拟存储中,不会出现在顶层。Rspack 解析 loader 时按名称在顶层查找,因而失败。 + +项目原本试图用 `.npmrc` 的 `public-hoist-pattern[]=*loader` 解决,但如上所述,pnpm 11 已不读取该文件。可用以下命令验证配置是否真正生效: + +```powershell +pnpm config get public-hoist-pattern # 失效时返回 undefined +``` + +### 解决方案 + +在 `pnpm-workspace.yaml` 中声明 `publicHoistPattern`: + +```yaml +publicHoistPattern: + - '*loader*' + - '*webpack-plugin*' + - 'react-refresh' + - '@babel/*' + - 'postcss' + - 'autoprefixer' +``` + +注意通配符要写成 `*loader*` 而非 `*loader`,确保能匹配 `babel-loader`、`css-loader` 等带连字符的包名。 + +修改后必须重新安装才能生效(见下方「配置变更后如何让安装生效」)。 + +### 验证 + +```powershell +@('babel-loader','style-loader','css-loader','less-loader','react-refresh','html-webpack-plugin') | + ForEach-Object { "$_ : " + (Test-Path "node_modules/$_") } +``` + +全部为 `True` 即成功。正常情况下 `node_modules` 顶层目录数约为 44 个(未提升时为 32 个)。 + +--- + +## 2. `ERR_PNPM_IGNORED_BUILDS` — build scripts 被忽略 + +### 现象 + +```text +[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: @parcel/watcher, core-js, es5-ext, +x-data-spreadsheet, zy-react-library +Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts. +``` + +### 根因 + +pnpm v10+ 出于安全考虑,默认禁止依赖执行 `postinstall` 等 build scripts,需显式放行。 + +项目 `pnpm-workspace.yaml` 中原先写的是 `allowBuilds`,且值为占位文本 `set this to true or false`,属于无效配置。 + +### 解决方案 + +改用 `onlyBuiltDependencies` 数组格式,并删除无效的 `allowBuilds` 块: + +```yaml +onlyBuiltDependencies: + - '@parcel/watcher' + - core-js + - es5-ext + - x-data-spreadsheet + - zy-react-library +``` + +若安装后警告仍然出现,显式执行一次重建即可: + +```powershell +pnpm rebuild @parcel/watcher core-js es5-ext x-data-spreadsheet zy-react-library +``` + +> 该报错会让 `pnpm install` 以退出码 1 结束,但依赖本身已装完,属于**非阻塞**问题,不影响启动。 + +--- + +## 3. 配置变更后如何让安装生效 + +### 现象一:改了配置但没有任何效果 + +`pnpm install` 在 lockfile 未变化时会跳过重新链接,`publicHoistPattern` 之类的布局配置不会被应用。 + +**解决**: + +```powershell +pnpm install --force +``` + +### 现象二:安装中断,提示需要清空 `node_modules` + +```text +The modules directory at "node_modules" will be removed and reinstalled from scratch. +Proceed? (Y/n) +``` + +改变 hoist 配置会要求重建整个 `node_modules`,而在无 TTY 的自动化环境中该确认无法响应,安装会挂起或中断。 + +**解决**:以 CI 模式跳过交互确认。 + +```powershell +$env:CI='true'; pnpm install +``` + +--- + +## 4. DVA namespace 缺失 + +### 现象 + +控制台错误: + +```text +[ERROR] 注册数据层失败,原因:无法匹配路径'xxx' +``` + +### 根因 + +`@cqsjjb/jjb-dva-runtime` 会扫描 `src/api/` 下的子目录,并用目录名去匹配 `src/enumerate/namespace/index.js` 中 `defineNamespace("xxx")` 声明的字符串,匹配不上则报错。 + +新增了 `src/api//` 却没有同步添加 namespace 定义时就会触发。 + +### 解决方案 + +在 `src/enumerate/namespace/index.js` 中补充: + +```js +export const NS_XXX = defineNamespace("xxx"); +``` + +### 本项目当前状态 + +`src/api/` 下共 6 个目录,而 namespace 只定义了 4 个: + +| 目录 | namespace | 说明 | +| --- | --- | --- | +| `global` | `NS_GLOBAL` | 已定义 | +| `bi` | `NS_BI` | 已定义 | +| `driver` | `NS_DRIVER` | 已定义 | +| `register` | `NS_REGISTER` | 已定义 | +| `institution` | — | **未定义** | +| `supervision` | — | **未定义** | + +`institution` 与 `supervision` 导出的是普通 fetch 函数,并非 DVA model 结构,当前启动日志中**未出现**注册报错,因此不影响运行。是否补充 namespace 属于业务设计决策:若后续要把它们改造成 DVA model,则必须同步添加定义。 + +> **约定**:新增 `src/api//index.js` 并按 DVA model 结构编写时,务必在 `src/enumerate/namespace/index.js` 中添加 `defineNamespace("")`。 + +--- + +## 5. 端口占用 `EADDRINUSE` + +### 现象 + +```text +Error: listen EADDRINUSE: address already in use 0.0.0.0:8080 +``` + +本项目端口为 **8080**(见 `jjb.config.js`),通常是上次的开发服务器进程未正常退出。 + +### 解决方案 + +```powershell +# 查看占用情况 +netstat -ano | findstr :8080 + +# 只杀真正处于 LISTEN 状态的进程 +Get-NetTCPConnection -LocalPort 8080 -State Listen -ErrorAction SilentlyContinue | + ForEach-Object { Stop-Process -Id $_.OwningProcess -Force } +``` + +> `netstat` 输出中的 `TIME_WAIT` / `FIN_WAIT` 是待回收的残留连接,**不占用**端口,无需处理。只有 `LISTENING` 才是真正的占用。 + +--- + +## 标准启动流程 + +```powershell +# 1. 安装依赖(首次或配置变更后) +$env:CI='true'; pnpm install + +# 2. 若提示 build scripts 被忽略 +pnpm rebuild @parcel/watcher core-js es5-ext x-data-spreadsheet zy-react-library + +# 3. 启动开发服务器 +npm run serve:development +``` + +成功标志: + +```text +Rspack 1.7.12 compiled successfully in 22.97 s +``` + +服务地址 。首次编译约 20-30 秒,日志停在 `wait until bundle finished: /` 属正常现象,需耐心等待打包完成。 + +--- + +## 检查清单 + +- [ ] 确认使用 **pnpm**,不要混用 npm 安装(会破坏 `node_modules` 结构) +- [ ] pnpm 安装类配置一律写在 `pnpm-workspace.yaml`,**不要**写进 `.npmrc` +- [ ] 用 `pnpm config get ` 验证配置是否真正生效,别假设它已加载 +- [ ] 改动 hoist 类配置后使用 `pnpm install --force`,并设 `$env:CI='true'` 跳过交互确认 +- [ ] 遇到 `Unable to resolve loader` 时,检查该 loader 是否已提升到 `node_modules` 顶层 +- [ ] 新增 DVA model 目录时同步添加 namespace 定义 +- [ ] 端口冲突只需关注 `LISTENING` 状态的进程 + +--- + +## 附:已废弃的排查思路 + +以下做法在早期版本文档中出现过,**当前项目不适用**,请勿采纳: + +| 废弃做法 | 说明 | +| --- | --- | +| 手动添加 `history@^4.10.1` 作为直接依赖 | `@cqsjjb/jjb-common-lib` 自身已声明 `history@^5.3.0`,pnpm 会正确链接到 `.pnpm` store。强行加 v4 会造成版本冲突。 | +| 把 `resolutions` 迁移为 `pnpm.overrides` | 当前 `package.json` 已无 `resolutions` 字段,无需处理。 | +| 改用 `npm install --legacy-peer-deps` | 项目已无 `package-lock.json`,统一使用 pnpm。npm 的扁平化布局会与 pnpm 结构冲突。 | +| 在 `.npmrc` 中设置 `shamefully-hoist` / `hoist` / `node-linker` | pnpm 11 不再从 `.npmrc` 读取这些配置,`.npmrc` 已从项目中删除。 | +| 处理 `react-router` 相关依赖问题 | 项目未依赖 `react-router`,`src` 中亦无引用。 | diff --git a/jjb.config.js b/jjb.config.js index b3ef0a8..fa188c3 100644 --- a/jjb.config.js +++ b/jjb.config.js @@ -10,7 +10,7 @@ module.exports = { javaGitBranch: "", // 接口服务地址(注册/填报开放接口需对接网关,本地后端可通过 sessionStorage.API_HOST 覆盖) //API_HOST: "https://gbs-gateway.qhdsafety.com", - API_HOST: "http://192.168.0.152", + API_HOST: "http://192.168.0.103", }, production: { // 应用后端分支名称,部署上线需要 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..a9d4eda --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,14 @@ +onlyBuiltDependencies: + - '@parcel/watcher' + - core-js + - es5-ext + - x-data-spreadsheet + - zy-react-library + +publicHoistPattern: + - '*loader*' + - '*webpack-plugin*' + - 'react-refresh' + - '@babel/*' + - 'postcss' + - 'autoprefixer' diff --git a/src/api/institution/index.js b/src/api/institution/index.js index 2d96bd4..8b27f81 100644 --- a/src/api/institution/index.js +++ b/src/api/institution/index.js @@ -5,6 +5,12 @@ const getApiHost = () => { return ''; }; +// 与 @cqsjjb/jjb-common-lib http 层保持一致:从 sessionStorage 读取 token 并以同名请求头传递 +function getTokenHeader() { + const token = window.sessionStorage.getItem('token'); + return token ? { token } : {}; +} + async function request(path, params = {}) { const apiHost = getApiHost(); const url = new URL(path, apiHost); @@ -13,7 +19,7 @@ async function request(path, params = {}) { url.searchParams.append(k, v); } }); - const response = await fetch(url.toString()); + const response = await fetch(url.toString(), { headers: getTokenHeader() }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } diff --git a/src/api/supervision/index.js b/src/api/supervision/index.js index 06666ec..6909784 100644 --- a/src/api/supervision/index.js +++ b/src/api/supervision/index.js @@ -5,6 +5,12 @@ const getApiHost = () => { return ''; }; +// 与 @cqsjjb/jjb-common-lib http 层保持一致:从 sessionStorage 读取 token 并以同名请求头传递 +function getTokenHeader() { + const token = window.sessionStorage.getItem('token'); + return token ? { token } : {}; +} + async function request(path, params = {}) { const apiHost = getApiHost(); const url = new URL(path, apiHost); @@ -13,7 +19,7 @@ async function request(path, params = {}) { url.searchParams.append(k, v); } }); - const response = await fetch(url.toString()); + const response = await fetch(url.toString(), { headers: getTokenHeader() }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } diff --git a/src/pages/Container/Institution/index.js b/src/pages/Container/Institution/index.js index 8f6d1ec..3de426f 100644 --- a/src/pages/Container/Institution/index.js +++ b/src/pages/Container/Institution/index.js @@ -24,8 +24,6 @@ import { } from '~/api/institution'; // ==================== UI 展示常量(与原型保持一致,非数据映射) ==================== -const NODE_CHARS = ['险', '合', '组', '场', '报', '内', '技', '控']; -const NODE_COLORS = ['blue', 'violet', 'green', 'orange', 'blue', 'violet', 'orange', 'green']; function mockIndustryToApiFormat() { return { @@ -122,22 +120,27 @@ export default function InstitutionDashboard() { .catch(() => {}); }, []); - // 衍生展示数据:API 成功则用 API 字段直接构建,失败则回退 mock + // 衍生展示数据:始终渲染固定卡片集合(8 个节点 + 已归档 + 项目延期), + // 仅用 API 数据覆盖对应数值,保证无论接口返回多少节点,卡片项数都不减少。 const statusCards = useMemo(() => { if (!nodeStatsApi) return STATISTIC_CARDS; - const { nodeStats, archivedProjectCount, delayedProjectCount } = nodeStatsApi; - const nodes = (nodeStats || []).map((node, i) => ({ - key: node.nodeCode || `NODE_${i}`, - title: node.nodeName, - value: Number(node.pendingCount) || 0, - char: NODE_CHARS[i] || node.nodeName.charAt(1) || '-', - colorClass: NODE_COLORS[i] || 'blue', - })); - nodes.push( - { key: 'archived', title: '已归档项目', value: Number(archivedProjectCount) || 0, char: '档', colorClass: 'cyan' }, - { key: 'delayed', title: '项目延期', value: Number(delayedProjectCount) || 0, char: '延', colorClass: 'red' }, - ); - return nodes; + const { nodeStats = [], archivedProjectCount = 0, delayedProjectCount = 0 } = nodeStatsApi || {}; + const nodeByName = new Map(); + (nodeStats || []).forEach((n) => { + if (n && n.nodeName) nodeByName.set(n.nodeName, n); + }); + return STATISTIC_CARDS.map((card) => { + let value = card.value; + if (card.key === 'archived') { + value = Number(archivedProjectCount) || 0; + } else if (card.key === 'delayed') { + value = Number(delayedProjectCount) || 0; + } else { + const node = nodeByName.get(card.title); + if (node) value = Number(node.pendingCount) || 0; + } + return { ...card, value }; + }); }, [nodeStatsApi]); const projectSummary = useMemo(() => { diff --git a/src/pages/Container/Supervision/Cockpit/index.css b/src/pages/Container/Supervision/Cockpit/index.css index 42bb708..df558ca 100644 --- a/src/pages/Container/Supervision/Cockpit/index.css +++ b/src/pages/Container/Supervision/Cockpit/index.css @@ -256,8 +256,9 @@ .todo-item p { margin: 0; color: #d8edff; font-size: 11px; white-space: normal; line-height: 1.1; overflow-wrap: anywhere; } .todo-item b { color: #13c8ff; font-size: 15px; } .period-tabs { position: absolute; left: 14px; top: 42px; display: flex; z-index: 2; } -.period-tabs span { width: 34px; height: 19px; display: grid; place-items: center; color: #d8f2ff; border: 1px solid rgba(46, 159, 232, .48); background: rgba(7, 37, 88, .6); font-size: 11px; } -.period-tabs span:first-child { background: rgba(5, 126, 176, .5); } +.period-tabs span { width: 34px; height: 19px; display: grid; place-items: center; color: #d8f2ff; border: 1px solid rgba(46, 159, 232, .48); background: rgba(7, 37, 88, .6); font-size: 11px; cursor: pointer; } +.period-tabs span + span { border-left: none; } +.period-tabs span.active { background: rgba(5, 126, 176, .5); color: #fff; } .line-chart { height: 176px; } .review-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 11px 14px; } diff --git a/src/pages/Container/Supervision/Cockpit/index.js b/src/pages/Container/Supervision/Cockpit/index.js index 71246e3..ce59288 100644 --- a/src/pages/Container/Supervision/Cockpit/index.js +++ b/src/pages/Container/Supervision/Cockpit/index.js @@ -40,11 +40,17 @@ const iconMap = [ ContainerOutlined, ]; +// 格式化当前时间为 yyyy年MM月dd日 HH:mm:ss +function formatNow(date = new Date()) { + const pad = n => String(n).padStart(2, '0'); + return `${date.getFullYear()}年${pad(date.getMonth() + 1)}月${pad(date.getDate())}日 ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; +} + // 将 mock 数据转为 API 结构,作为初始值 function buildInitialFromMock(m) { return { title: m.title, - currentTime: m.currentTime, + currentTime: formatNow(), lifecycleStats: { newFilingOrgCount: m.lifecycleStats[0]?.value || 0, currentFilingOrgCount: m.lifecycleStats[1]?.value || 0, @@ -121,13 +127,14 @@ function buildInitialFromMock(m) { // ==================== 子组件 ==================== -function EChart({ option, className, events }) { +function EChart({ option, className, events, onInstance }) { const chartRef = useRef(null); const instanceRef = useRef(null); useEffect(() => { if (!chartRef.current) return undefined; instanceRef.current = echarts.init(chartRef.current, null, { renderer: 'svg' }); + if (onInstance) onInstance(instanceRef.current); const resize = () => instanceRef.current && instanceRef.current.resize(); const observer = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(resize) : null; @@ -139,6 +146,7 @@ function EChart({ option, className, events }) { } return () => { + if (onInstance) onInstance(null); if (events && instanceRef.current) { Object.entries(events).forEach(([eventName, handler]) => instanceRef.current.off(eventName, handler)); } @@ -289,6 +297,8 @@ function KpiCards({ data }) { // ==================== 地图区域分布 ==================== function MapSection({ data }) { const [selectedAreaName, setSelectedAreaName] = useState('重庆市'); + const zoomRef = useRef(1.1); + const chartInstanceRef = useRef(null); const { regionList, regionSummary, mapScatterPoints } = data; const statMap = useMemo(() => { @@ -304,19 +314,16 @@ function MapSection({ data }) { return map; }, [regionList]); + // 使用后端真实数据;后端未返回的区县补 0,不填充伪造数字 const mapData = useMemo(() => { - return (chongqingGeoJson.features || []).map((feature, index) => { + return (chongqingGeoJson.features || []).map((feature) => { const name = feature.properties && feature.properties.name; - const stat = statMap.get(name) || { - name, - evalProjectCount: 18 + index * 7, - filingOrgCount: 0, - }; + const stat = statMap.get(name); return { name, - districtCode: stat.districtCode || '', - evalProjectCount: stat.evalProjectCount, - filingOrgCount: stat.filingOrgCount, + districtCode: (stat && stat.districtCode) || '', + evalProjectCount: (stat && stat.evalProjectCount) || 0, + filingOrgCount: (stat && stat.filingOrgCount) || 0, }; }); }, [statMap]); @@ -333,6 +340,7 @@ function MapSection({ data }) { const mapOption = useMemo(() => ({ tooltip: { trigger: 'item', + confine: false, backgroundColor: 'rgba(2, 31, 82, .96)', borderColor: '#1aa7ff', borderWidth: 1, @@ -341,11 +349,15 @@ function MapSection({ data }) { formatter: params => { const item = params.data || statMap.get(params.name); if (!item) return params.name; - return [ - `${params.name}`, - `评价项目数:${item.evalProjectCount || 0}`, - `备案机构数:${item.filingOrgCount || 0}`, - ].join('
'); + // 随地图缩放比例同步缩放悬浮框字体 + const z = zoomRef.current || 1; + const fs = Math.max(10, Math.round(12 * z)); + const title = Math.round(13 * z); + return `
+ ${params.name}
+ 评价项目数:${item.evalProjectCount || 0}
+ 备案机构数:${item.filingOrgCount || 0} +
`; }, }, visualMap: { @@ -411,11 +423,26 @@ function MapSection({ data }) { click: params => { if (params && params.name) setSelectedAreaName(params.name); }, - }), []); + // 地图缩放/平移时更新缩放比例,tooltip 同步跟随并按比例缩放 + georoam: params => { + if (params && typeof params.zoom === 'number') { + zoomRef.current = params.zoom; + // 刷新已显示的 tooltip,使其随缩放实时更新字体大小(无 hover 时无副作用) + const chart = chartInstanceRef.current; + if (chart) { + chart.dispatchAction({ type: 'showTip' }); + } + } + }, + }), [selectedAreaName]); + + const handleMapInstance = useMemo(() => (instance) => { + chartInstanceRef.current = instance; + }, []); return (
- + {mapScatterPoints.map((item, index) =>
{item.name}正在开展评价项目数:{item.value}
)}
{selectedArea.name} @@ -443,7 +470,13 @@ function TodoPanel({ data }) { } // ==================== 评价类型趋势 ==================== -function ReviewLinePanel({ data }) { +const PERIOD_TABS = [ + { key: 'year', label: '年' }, + { key: 'quarter', label: '季' }, + { key: 'month', label: '月' }, +]; + +function ReviewLinePanel({ data, period, onPeriodChange }) { const { trendBuckets, trendSeries } = data; const lineOption = useMemo(() => { @@ -468,7 +501,15 @@ function ReviewLinePanel({ data }) { return ( -
+
+ {PERIOD_TABS.map(tab => ( + onPeriodChange && onPeriodChange(tab.key)} + >{tab.label} + ))} +
); @@ -528,11 +569,22 @@ export default function SupervisionCockpit() { // 取当前年份/月份 const currentYear = useMemo(() => new Date().getFullYear(), []); + + // 右上角实时日期时间,每秒刷新 + useEffect(() => { + const timer = setInterval(() => { + setData(prev => ({ ...prev, currentTime: formatNow() })); + }, 1000); + return () => clearInterval(timer); + }, []); const currentMonth = useMemo(() => { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; }, []); + // 评价类型趋势的时间粒度(年/季/月) + const [trendPeriod, setTrendPeriod] = useState('year'); + // 标记是否使用 API 数据(仅当对应 API 成功时为 true) const [apiFlags, setApiFlags] = useState({ qualification: false, @@ -583,17 +635,6 @@ export default function SupervisionCockpit() { }) .catch(() => {}); - // 5. 评价类型趋势 - fetchEvalTypeTrend('month', currentYear) - .then((apiData) => { - setData(prev => ({ - ...prev, - trendBuckets: apiData.buckets || prev.trendBuckets, - trendSeries: apiData.series || prev.trendSeries, - })); - }) - .catch(() => {}); - // 6. 复盘评估改进提效 fetchReviewSummary(currentYear) .then((apiData) => { @@ -610,6 +651,19 @@ export default function SupervisionCockpit() { .catch(() => {}); }, [currentYear, currentMonth]); + // 5. 评价类型趋势(随 年/季/月 tab 切换重新查询) + useEffect(() => { + fetchEvalTypeTrend(trendPeriod, currentYear) + .then((apiData) => { + setData(prev => ({ + ...prev, + trendBuckets: apiData.buckets || prev.trendBuckets, + trendSeries: apiData.series || prev.trendSeries, + })); + }) + .catch(() => {}); + }, [trendPeriod, currentYear]); + // 项目流程面板数据:API 有值时用 processNodes,否则用 mock processMockNodes const processPanelNodes = useMemo(() => { if (apiFlags.process && data.processNodes && data.processNodes.length > 0) { @@ -634,7 +688,11 @@ export default function SupervisionCockpit() {