机构端、监管端首页修改
parent
ad3307f863
commit
de49125981
|
|
@ -11,6 +11,8 @@
|
|||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
dev-server.log
|
||||
install.log
|
||||
.idea
|
||||
yarn.lock
|
||||
/openspec/
|
||||
|
|
|
|||
4
.npmrc
4
.npmrc
|
|
@ -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
|
||||
|
|
@ -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/<name>/` 却没有同步添加 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/<name>/index.js` 并按 DVA model 结构编写时,务必在 `src/enumerate/namespace/index.js` 中添加 `defineNamespace("<name>")`。
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
服务地址 <http://localhost:8080/>。首次编译约 20-30 秒,日志停在 `wait until bundle finished: /` 属正常现象,需耐心等待打包完成。
|
||||
|
||||
---
|
||||
|
||||
## 检查清单
|
||||
|
||||
- [ ] 确认使用 **pnpm**,不要混用 npm 安装(会破坏 `node_modules` 结构)
|
||||
- [ ] pnpm 安装类配置一律写在 `pnpm-workspace.yaml`,**不要**写进 `.npmrc`
|
||||
- [ ] 用 `pnpm config get <key>` 验证配置是否真正生效,别假设它已加载
|
||||
- [ ] 改动 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` 中亦无引用。 |
|
||||
|
|
@ -10,7 +10,7 @@ module.exports = {
|
|||
javaGitBranch: "<branch-name>",
|
||||
// 接口服务地址(注册/填报开放接口需对接网关,本地后端可通过 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: {
|
||||
// 应用后端分支名称,部署上线需要
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
`<strong>${params.name}</strong>`,
|
||||
`评价项目数:${item.evalProjectCount || 0}`,
|
||||
`备案机构数:${item.filingOrgCount || 0}`,
|
||||
].join('<br/>');
|
||||
// 随地图缩放比例同步缩放悬浮框字体
|
||||
const z = zoomRef.current || 1;
|
||||
const fs = Math.max(10, Math.round(12 * z));
|
||||
const title = Math.round(13 * z);
|
||||
return `<div style="font-size:${fs}px;line-height:1.7">
|
||||
<strong style="font-size:${title}px">${params.name}</strong><br/>
|
||||
评价项目数:${item.evalProjectCount || 0}<br/>
|
||||
备案机构数:${item.filingOrgCount || 0}
|
||||
</div>`;
|
||||
},
|
||||
},
|
||||
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 (
|
||||
<div className="map-shell">
|
||||
<EChart className="map-chart" option={mapOption} events={mapEvents} />
|
||||
<EChart className="map-chart" option={mapOption} events={mapEvents} onInstance={handleMapInstance} />
|
||||
{mapScatterPoints.map((item, index) => <div className={`map-card map-card--${index + 1}`} key={item.name}><strong>{item.name}</strong><span>正在开展评价项目数:{item.value}</span></div>)}
|
||||
<div className="map-summary">
|
||||
<strong>{selectedArea.name}</strong>
|
||||
|
|
@ -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 (
|
||||
<Panel title="评价类型趋势" className="line-panel">
|
||||
<div className="period-tabs"><span>年</span><span>季</span><span>月</span></div>
|
||||
<div className="period-tabs">
|
||||
{PERIOD_TABS.map(tab => (
|
||||
<span
|
||||
key={tab.key}
|
||||
className={period === tab.key ? 'active' : ''}
|
||||
onClick={() => onPeriodChange && onPeriodChange(tab.key)}
|
||||
>{tab.label}</span>
|
||||
))}
|
||||
</div>
|
||||
<EChart className="line-chart" option={lineOption} />
|
||||
</Panel>
|
||||
);
|
||||
|
|
@ -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() {
|
|||
</section>
|
||||
<aside className="cockpit-right">
|
||||
<TodoPanel data={data} />
|
||||
<ReviewLinePanel data={data} />
|
||||
<ReviewLinePanel
|
||||
data={data}
|
||||
period={trendPeriod}
|
||||
onPeriodChange={setTrendPeriod}
|
||||
/>
|
||||
<ReviewProgressPanel data={data} />
|
||||
</aside>
|
||||
</main>
|
||||
|
|
|
|||
Loading…
Reference in New Issue