| name | yida-chart |
| description | 宜搭 ECharts 高级报表技能。通过 ECharts + 自定义页面 JSX 实现高度定制化、更美观的数据可视化报表。本技能不负责创建宜搭原生报表(标准报表由 yida-report 技能负责),但 ECharts 报表必须依赖宜搭原生报表的 getDataAsync.json 或 getCacheData.json 接口获取聚合数据,禁止前端聚合。当用户提到「更美观」「高级」「定制化」「ECharts」「echarts」「Dashboard 大屏」「数据大屏」等关键词,或用户提供了报表 URL 要求优化时,使用此技能。普通的「报表」「统计」等需求默认由 yida-report 技能处理。 |
宜搭 ECharts 高级报表技能
严格禁止 (NEVER DO)
- 不要在前端直接聚合表单数据,必须通过宜搭原生报表接口(
getDataAsync.json / getCacheData.json)获取聚合数据
- 不要在没有原生报表的情况下直接创建 ECharts 页面,必须先用
yida-report 创建原生报表作为数据源
- 不要支持多表关联数据源,当前仅支持单表数据源
- 不要将 ECharts 图表的输出误认为是"优化原生报表",输出始终是 ECharts 自定义页面
- 不要编造
reportId、datasetId,必须从报表 URL 或 Schema 中提取
严格要求 (MUST DO)
- 用户提供报表 URL 时,必须先解析 URL 提取
appType 和 reportId,再获取报表 Schema
- 若用户没有原生报表,必须先调用
yida-report 技能创建原生报表,再基于其数据源创建 ECharts 页面
- ECharts 必须通过
this.utils.loadScript 加载 CDN,不得 import
- 数据加载失败时必须显示错误状态,不得静默失败
适用场景
| 用户意图 | 触发条件 |
|---|
| 高级定制化可视化 | "更美观"、"ECharts"、"大屏"、"Dashboard"、"定制化" |
| 优化已有报表 | 用户提供报表 URL 要求优化 |
| 普通报表需求 | → 改用 yida-report 技能 |
与 yida-report 的分工
| 技能 | 定位 | 典型场景 |
|---|
| yida-report(原生报表) | 创建宜搭平台内置报表,作为数据源 | 普通「报表」「统计」需求 |
| yida-chart(本技能) | 基于原生报表数据,用 ECharts 实现自定义可视化页面 | 「更美观」「大屏」「ECharts」「定制化」需求 |
使用本技能前:若用户尚无原生报表,需先调用 yida-report 技能创建原生报表作为数据源,再由本技能创建 ECharts 自定义页面。
概述
本技能负责通过 ECharts + 自定义页面 JSX 实现高度定制化的数据可视化报表。本技能不负责创建宜搭原生报表(标准报表由 yida-report 技能负责),但 ECharts 报表依赖原生报表的 getDataAsync.json 接口作为数据源。
| 方案 | 技术栈 | 适用场景 | 数据源 |
|---|
| 方案 B:ECharts 高级报表(从头创建) | ECharts + 自定义页面 JSX | 高度定制化图表、复杂交互、更美观的视觉效果 | 用户无原生报表 → 调用 yida-report 新建;用户已有 → 直接复用 |
| 方案 C:基于已有报表创建 ECharts 页面 | 解析现有报表 Schema + ECharts 自定义页面 | 用户已有原生报表,希望用 ECharts 实现更美观的展示 | 用户提供的报表 URL 中的原生报表作为数据源 |
💡 与 yida-report 的分工:普通的"报表"、"统计"需求默认由 yida-report 技能处理。只有当用户明确要求"更美观"、"高级"、"定制化"、"ECharts",或提供了报表 URL 要求用 ECharts 优化时,才使用本技能。
⚠️ 核心规则(必须遵守)
1. 方案选择规则
场景 A: 用户提供了已有报表 URL(如 https://www.aliwork.com/APP_XXX/admin/REPORT-XXX)
→ 使用【方案 C:基于已有报表创建 ECharts 页面】
→ 从 URL 中解析 appType 和 formUuid,获取现有 Schema 作为数据源
→ 最终输出:ECharts 自定义页面(非优化后的原生报表)
场景 B: 用户未提供报表 URL,从头创建 ECharts 高级报表
→ 使用【方案 B:ECharts 高级报表】
→ 数据源获取:
- 用户已有原生报表 → 直接读取已有报表信息作为数据源
- 用户没有原生报表 → 先调用【yida-report 技能】创建原生报表作为数据源
→ 最终输出:ECharts 自定义页面
⚠️ 注意:本技能的所有方案最终输出都是 ECharts 自定义页面。如果用户只需要标准原生报表(无 ECharts 定制需求),应直接使用 yida-report 技能。
2. cid 与 fieldId 的区分(易混淆,必须注意)
报表 Schema 中有两种 ID,绝对不能混用:
| 名称 | 格式 | 用途 | 示例 |
|---|
cid | node_xxx | getDataAsync.json 的请求参数 | node_oc8u7tmwt95z55 |
fieldId | YoushuXxx_xxx | Schema 中组件的标识符,不能用于 API 请求 | YoushuSimpleIndicatorCard_5rugy68y |
获取 cid 的方法:执行 openyida get-schema <appType> <reportFormUuid>,在 componentsTree 中找到目标组件节点,其 id 字段即为 cid(node_xxx 格式)。
3. ECharts 高级报表必须依赖原生报表
ECharts 高级报表的数据来源必须是宜搭原生报表的接口,禁止前端聚合。
【ECharts 高级报表创建流程】
Step 1: 分析报表需求,确定需要哪些图表、指标
↓
Step 2: 调用【yida-report 技能】创建宜搭原生报表页面
↓ (由 yida-report 负责配置图表组件,生成 getDataAsync.json / getCacheData.json 数据接口)
↓
Step 3: 从原生报表 Schema 中提取 getDataAsync.json 所需的关键参数
↓ (通过 openyida get-schema 获取,解析 cubeCode、cid、componentClassName 等)
↓
Step 4: 在宜搭管理后台将原生报表页面设置为【双端隐藏】
↓ (PC 端和移动端均不显示在导航中,需手动操作或通过管理接口)
↓
Step 5: 创建【ECharts 自定义页面】,通过原生报表接口获取聚合数据
↓
Step 6: 记录关联关系到 .cache/<项目名>-report-bindding.json
4. 原生报表与 ECharts 报表的关联关系
两种报表高度耦合,必须记录关联关系并同步更新:
关联关系存储:.cache/<项目名>-report-bindding.json
{
"binddings": [
{
"echartsPageUuid": "FORM-ECHARTS-XXX",
"echartsPageName": "销售数据大屏",
"nativeReportUuid": "REPORT-NATIVE-XXX",
"nativeReportName": "销售数据报表(数据源)",
"bindingTime": "2024-01-15T10:30:00Z",
"components": [
{
"echartsChartId": "chart-sales-trend",
"nativeComponentCid": "node_ocmmwwwhdmg",
"nativeComponentName": "折线图_销售趋势",
"nativeComponentType": "YoushuLineChart"
}
]
}
]
}
5. 报表更新规则
后续报表需求变化时,必须同步更新两个页面:
【报表更新流程】
Step 1: 读取 .cache/<项目名>-report-bindding.json 获取关联关系
↓
Step 2: 调用【yida-report 技能】更新原生报表的 Schema
↓ (修改图表配置、字段定义等,由 yida-report 负责)
↓
Step 3: 调用 saveFormSchema.json 更新【ECharts 页面】的代码
↓ (同步修改图表渲染逻辑)
↓
Step 4: 更新 .cache/<项目名>-report-bindding.json 中的组件映射(如有变化)
⚠️ 禁止:为新需求创建新的页面关系,必须在已有的关联页面上更新
6. 原生报表页面隐藏规则
一旦创建了 ECharts 高级报表,对应的原生报表页面必须双端隐藏:
隐藏方式:在宜搭应用管理后台 → 页面导航设置中,将原生报表页面的 PC 端和移动端可见性均设置为隐藏。
或通过 updateFormNavigation.json 接口(需在服务端/CLI 环境中调用,非自定义页面前端代码):
- formUuid: 原生报表页面 UUID
- pcVisible: false
- mobileVisible: false
7. 开发者态与用户态的展示规则
| 视角 | 原生报表页面 | ECharts 报表页面 | 关联关系 |
|---|
| 用户态(普通用户) | 隐藏 | 显示 | 隐藏 |
| 管理态(开发者/管理员) | 显示(标注为"数据源") | 显示 | 显示在 ECharts 页面配置中 |
ECharts 页面管理态展示:
- 在页面配置或代码注释中明确标注数据来源的原生报表
- 格式:
/* 数据源报表: REPORT-NATIVE-XXX (销售数据报表) */
核心原则
- 聚合统计禁止前端聚合:必须通过
getDataAsync.json 或 getCacheData.json 接口由服务端完成,详见上方核心规则第 2 节
- 安全引用:通过
this.utils.loadScript() 从可信 CDN 动态加载 ECharts,禁止内联大段脚本
- 遵循自定义页面规范:所有代码必须遵循
yida-custom-page 的开发规范
何时使用
当用户提出以下需求时使用此技能:
- 创建数据报表、统计图表(柱状图、折线图、饼图等)
- 基于表单数据做数据可视化分析
- 搭建数据看板 / Dashboard
- 需要对表单数据进行聚合统计并图形化展示
- 优化已有报表(用户提供了报表 URL)
方案选择:
- 用户提供了已有报表 URL → 使用【方案 C:基于已有报表创建 ECharts 页面】,以已有报表为数据源,输出 ECharts 自定义页面
- 用户未提供报表 URL → 使用【方案 B:ECharts 高级报表】,若用户无原生报表则先调用 yida-report 创建数据源
- 用户只需要标准报表(无 ECharts 定制需求)→ 不使用本技能,直接使用
yida-report 技能
前置依赖
- 必须先加载
yida-custom-page 技能,遵循其编码规范
- ECharts 高级报表需要依赖
yida-report 技能创建原生报表作为数据源(yida-chart 本身不包含原生报表的创建逻辑)
- 需要已创建好数据源表单(通过
yida-create-form-page 创建)
- 需要知道数据源表单的
formUuid 和字段 fieldId(通过 yida-get-schema 获取)
ECharts 安全引入方案
可信 CDN 地址(按优先级排序)
| CDN | URL | 说明 |
|---|
| 阿里 CDN(推荐) | https://g.alicdn.com/code/lib/echarts/5.6.0/echarts.min.js | 阿里内网外网均可访问,速度最快 |
| cdnjs | https://cdnjs.cloudflare.com/ajax/libs/echarts/5.6.0/echarts.min.js | 国际通用,Cloudflare 托管 |
中国地图 GeoJSON 数据源
ECharts 5 不再内置中国地图数据,需要额外加载 GeoJSON 并通过 echarts.registerMap('china', geoJson) 注册。
| 数据源 | URL | 说明 |
|---|
| 阿里云 DataV(推荐) | https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json | 阿里云公开数据服务,地图数据权威合规 |
fetch('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json')
.then(function(response) { return response.json(); })
.then(function(geoJson) {
window.echarts.registerMap('china', geoJson);
});
⚠️ 地图安全要求:
- 必须使用阿里云 DataV 提供的官方 GeoJSON 数据,确保地图边界合规
- 禁止加载旧版内置中国地图脚本或旧 map chart 包;ECharts 5.x 已废弃内置地图包
- 省份名称需使用全称(如"北京市"、"广东省"、"内蒙古自治区"),与 GeoJSON 中的
name 字段匹配
- 示例中提供了
normalizeProvinceName() 函数,自动将简称转换为全称
⚠️ 安全要求:
- 必须使用上述可信 CDN 之一,禁止使用来源不明的第三方 URL
- 必须锁定版本号(如
5.6.0),禁止使用 latest 或不带版本的 URL
- 推荐优先使用阿里 CDN(
g.alicdn.com),在宜搭环境中加载速度最快
- ECharts
label.formatter 返回 rich text 模板在宜搭自定义页面环境不稳定;优先使用普通 formatter 字符串或预处理标签文本
加载方式
使用宜搭内置的 this.utils.loadScript() 动态加载,在 didMount 中执行:
var ECHARTS_CDN = 'https://g.alicdn.com/code/lib/echarts/5.6.0/echarts.min.js';
export function didMount() {
this.utils.loadScript(ECHARTS_CDN)
.then(function() {
this.bindChartResize();
this.loadChartData();
}.bind(this))
.catch(function(error) {
this.utils.toast({ title: 'ECharts 加载失败,请刷新重试', type: 'error' });
}.bind(this));
}
防重复加载
如果页面有多个图表,ECharts 只需加载一次:
export function loadECharts() {
if (window.echarts) {
this.initCharts();
return;
}
this.utils.loadScript(ECHARTS_CDN)
.then(function() {
this.initCharts();
}.bind(this))
.catch(function(error) {
this.utils.toast({ title: 'ECharts 加载失败', type: 'error' });
}.bind(this));
}
⚠️ ECharts 页面代码必备结构(必须遵守)
以下是 ECharts 自定义页面代码的必备结构清单,缺少任何一项都会导致页面运行失败。生成代码前必须逐项检查。
必备函数清单
| 函数 | 类型 | 作用 | 缺少后果 |
|---|
_customState | var 变量 | 存储所有业务状态(loading、数据等) | 无法管理页面状态 |
getCustomState | export function | 获取状态 | 编译警告 |
setCustomState | export function | 设置状态并触发重渲染 | setCustomState is not a function |
forceUpdate | export function | 通过 this.setState({ timestamp }) 触发 React 重渲染 | forceUpdate is not a function |
didMount | export function | 页面加载完成时初始化 | 页面无法初始化 |
didUnmount | export function | 页面卸载时清理资源 | 内存泄漏 |
renderJsx | export function | 页面渲染入口 | 页面空白 |
必备代码模板
var _customState = {
loading: true,
};
export function getCustomState(key) {
if (key) return _customState[key];
return _.clone(_customState);
}
export function setCustomState(newState) {
_.assign(_customState, newState);
this.forceUpdate();
}
export function forceUpdate() {
this.setState({ timestamp: new Date().getTime() });
}
renderJsx 的 timestamp 隐藏 div(必须包含)
⚠️ renderJsx 的每个 return 分支都必须包含 <div style={{ display: 'none' }}>{this.state.timestamp}</div>,否则 forceUpdate 调用 this.setState({ timestamp }) 后,React 无法检测到输出变化,页面将无法更新。
export function renderJsx() {
if (_customState.loading) {
return (
<div>
<div style={{ display: 'none' }}>{this.state.timestamp}</div>
<div>加载中...</div>
</div>
);
}
return (
<div>
<div style={{ display: 'none' }}>{this.state.timestamp}</div>
{/* 页面内容 */}
</div>
);
}
CDN 地址规则(必须遵守)
| 规则 | 说明 |
|---|
| 必须使用阿里 CDN | https://g.alicdn.com/code/lib/echarts/5.6.0/echarts.min.js |
| 禁止使用 cdnjs.cloudflare.com | 宜搭环境(aliwork.com)对 cloudflare CDN 有安全策略限制,会加载失败 |
| 必须锁定版本 5.6.0 | 禁止使用 latest 或其他未验证版本 |
函数声明规则
| 场景 | 正确写法 | 错误写法 | 原因 |
|---|
需要 this 的组件方法 | export function loadAllData() {} | var loadAllData = function() {} | 需要宜搭运行时绑定 this |
不需要 this 的纯工具函数 | var _fetchData = function() {} | export function fetchData() {} | export function 不在白名单中会被 UglifyJS 消除 |
| 模块级常量/变量 | var APP_TYPE = 'xxx' | const APP_TYPE = 'xxx' | 兼容性 |
prdId(topicId)动态获取(必须遵守)
⚠️ prdId 不能硬编码,必须在运行时通过 getFormNavigationListByOrder 接口动态获取。
var _prdId = null;
var _fetchPrdId = function () {
var appType = window.pageConfig && window.pageConfig.appType;
var csrfToken = window.g_config && window.g_config._csrf_token;
var baseUrl = window.location.origin;
var url = baseUrl + '/dingtalk/web/' + appType
+ '/query/formnav/getFormNavigationListByOrder.json'
+ '?_api=Nav.queryList&_mock=false&_csrf_token=' + encodeURIComponent(csrfToken);
return fetch(url, {
method: 'GET',
credentials: 'include',
headers: { 'accept': 'application/json', 'x-requested-with': 'XMLHttpRequest' },
})
.then(function (resp) { return resp.json(); })
.then(function (res) {
if (res.success && Array.isArray(res.content)) {
var targetNav = res.content.find(function (item) { return item.formUuid === REPORT_FORM_UUID; });
if (targetNav && targetNav.topicId) {
_prdId = targetNav.topicId;
return _prdId;
}
var reportNav = res.content.find(function (item) { return item.formType === 'report' && item.topicId; });
if (reportNav) {
_prdId = reportNav.topicId;
return _prdId;
}
throw new Error('未找到报表的 topicId');
}
throw new Error(res.errorMsg || '获取导航菜单失败');
});
};
报表数据请求函数模板(必须用 var 声明)
var _fetchReportData = function (component, filterValueMap) {
var appType = window.pageConfig && window.pageConfig.appType;
var csrfToken = window.g_config && window.g_config._csrf_token;
var body = new URLSearchParams({
timezone: 'GMT+8',
_tb_token_: csrfToken, _csrf_token: csrfToken, _csrf: csrfToken,
prdId: _prdId,
pageId: REPORT_FORM_UUID,
pageName: 'report',
cid: component.cid,
cname: component.cname || '',
componentClassName: component.className,
queryContext: JSON.stringify({ filterValueMap: filterValueMap || {}, dim2table: true }),
dataSetKey: component.dataSetKey,
});
var url = '/alibaba/web/' + appType + '/visual/visualizationDataRpc/getDataAsync.json';
return fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: body.toString(),
credentials: 'include',
})
.then(function (r) { return r.json(); })
.then(function (result) {
if (result.success) return result.content;
throw new Error(result.errorMsg || '报表数据获取失败');
});
};
didMount 初始化流程模板
export function didMount() {
var self = this;
var loadEcharts = new Promise(function (resolve, reject) {
if (window.echarts) { resolve(); return; }
var script = document.createElement('script');
script.src = ECHARTS_CDN;
script.onload = function () { resolve(); };
script.onerror = function () { reject(new Error('ECharts 加载失败')); };
document.head.appendChild(script);
});
Promise.all([loadEcharts, _fetchPrdId()])
.then(function () { return self.loadAllData(); })
.catch(function (err) {
console.error('[看板] 初始化失败:', err);
self.utils.toast({ title: '初始化失败: ' + err.message, type: 'error' });
_customState.loading = false;
self.forceUpdate();
});
window.addEventListener('resize', function () {
});
}
生成代码前的自检清单
生成 ECharts 页面代码前,必须逐项确认:
数据获取方案
⚠️ 核心规则:
- 数据明细表(展示每条记录的详细信息):使用
this.utils.yida.searchFormDatas 获取原始数据
- 所有聚合统计(分组计数、求和、平均、趋势等图表数据):必须通过宜搭原生报表接口(
getDataAsync.json 或 getCacheData.json)实现
- 禁止前端聚合:禁止在浏览器端对
searchFormDatas 返回的数据做分组、计数、求和等聚合计算,所有聚合必须由服务端完成
方案一:this.utils.yida.searchFormDatas(仅用于数据明细表)
仅适用于报表中的数据明细表格,展示每条表单记录的原始字段值。禁止用于聚合统计场景。
核心接口
| 接口 | 用途 | 关键参数 |
|---|
searchFormDatas | 获取表单数据列表(含字段值),用于数据明细表 | formUuid, pageSize(≤100), currentPage, searchFieldJson |
getFormDataById | 获取单条数据详情 | formInstId |
分页拉取数据(用于数据明细表)
export function fetchAllFormData(formUuid, searchCondition, maxRecords) {
var allData = [];
var pageSize = 100;
var limit = maxRecords || 2000;
var fetchPage = function(currentPage) {
var params = {
formUuid: formUuid,
currentPage: currentPage,
pageSize: pageSize,
};
if (searchCondition) {
params.searchFieldJson = JSON.stringify(searchCondition);
}
return this.utils.yida.searchFormDatas(params)
.then(function(res) {
allData = allData.concat(res.data || []);
var totalCount = res.totalCount || 0;
if (allData.length >= limit) {
console.warn('数据量(' + totalCount + ')超过上限(' + limit + '),仅加载前 ' + limit + ' 条');
return allData.slice(0, limit);
}
if (currentPage * pageSize < totalCount) {
return fetchPage.call(this, currentPage + 1);
}
return allData;
}.bind(this));
}.bind(this);
return fetchPage(1);
}
方案二:宜搭原生报表数据接口(ECharts 高级报表的数据来源)
所有聚合统计需求(分组计数、求和、平均值、趋势分析、占比分布等)必须通过宜搭原生报表的数据接口实现。
⚠️ ECharts 高级报表必须依赖原生报表
ECharts 高级报表的数据来源必须是宜搭原生报表的查询接口,禁止前端聚合。
因此,创建 ECharts 高级报表前,必须先创建对应的宜搭原生报表页面,配置好所需的图表组件,然后通过以下接口获取聚合数据。
可用接口
| 接口 | 用途 | 特点 |
|---|
getDataAsync.json | 实时获取报表组件数据 | 每次请求实时计算,数据最新 |
getCacheData.json | 获取缓存的报表数据 | 性能更好,适合大数据量场景 |
适用场景
- 所有需要聚合统计的图表(柱状图、折线图、饼图、指标卡等)
- 分组计数、求和、平均值等聚合计算
- 跨多个表单做关联统计
- 任意数据量的聚合分析(服务端计算,无性能瓶颈)
接口说明:getDataAsync.json
宜搭报表页面在渲染时会调用 getDataAsync.json 接口获取各个报表组件的数据。该接口是报表数据的核心接口。
- 地址:
POST /alibaba/web/{appType}/visual/visualizationDataRpc/getDataAsync.json
- Query 参数:
?_api=EDataService.getDataAsync&_mock=false
- Content-Type:
application/x-www-form-urlencoded
Body 参数:
| 参数 | 类型 | 必填 | 说明 | 示例 |
|---|
pageId | String | 是 | 报表页面 UUID | REPORT-YRD66CC1P4W34O3FKNPP8BKM5SNV3VIE6WWMMC |
cid | String | 是 | 组件节点 ID(从 Network 面板获取) | node_ocmmwwwhdmg |
cname | String | 是 | 组件显示名称 | 基础表格_1 |
componentClassName | String | 是 | 组件类型(见下表) | YoushuTable |
queryContext | String (JSON) | 是 | 查询上下文,包含筛选、排序、分页等 | 见下方说明 |
dataSetKey | String | 是 | 数据集标识 | table |
_csrf_token | String | 是 | CSRF Token | window.g_config._csrf_token |
_tb_token_ | String | 是 | 同 CSRF Token | 同上 |
_csrf | String | 是 | 同 CSRF Token | 同上 |
timezone | String | 否 | 时区 | GMT+8 |
pageName | String | 否 | 固定值 | report |
enabledCache | String | 否 | 是否启用缓存 | true |
常见组件类型(componentClassName):
| 类型 | 说明 |
|---|
YoushuTable | 基础表格 |
YoushuBar | 柱状图 |
YoushuLine | 折线图 |
YoushuPie | 饼图 |
YoushuNumber | 数字指标卡 |
YoushuFunnel | 漏斗图 |
queryContext 结构:
{
"aliasList": [],
"filterValueMap": {
"filter-xxx": ["筛选值"]
},
"dim2table": true,
"orderByList": [],
"needTotalCount": false,
"variableParams": {},
"paging": {
"start": 0,
"limit": 10
}
}
| 字段 | 说明 |
|---|
filterValueMap | 筛选条件,key 为筛选器 ID,value 为筛选值数组 |
paging.start | 分页起始位置(从 0 开始) |
paging.limit | 每页数据量 |
orderByList | 排序规则数组 |
needTotalCount | 是否返回总数 |
如何获取接口参数
getDataAsync.json 的参数(特别是 cid、componentClassName)需要从已创建的报表页面中获取:
方式一:从报表 Schema 解析(推荐,AI 友好)
执行 openyida get-schema <appType> <reportFormUuid> 获取报表 Schema,从 Schema 的组件树中提取各组件的 cid 和 componentClassName。Schema 中每个报表组件节点包含这些信息。
方式二:从浏览器 Network 面板抓取
- 在浏览器中打开报表页面,如:
https://www.aliwork.com/{appType}/preview/{reportUuid}
- 打开 DevTools → Network 面板
- 筛选
getDataAsync 关键词
- 查看请求的 Form Data,获取
pageId、cid、componentClassName 等参数
- 查看 Response,了解返回数据的结构
- 报表页面中每个组件(表格、图表、指标卡)都会发起独立的
getDataAsync 请求,可按 cname 区分
报表数据接口调用
export function fetchReportData(appType, reportPageId, componentNodeId, componentName, componentClassName, options) {
var csrfToken = window.g_config._csrf_token;
var baseUrl = window.location.origin;
options = options || {};
var queryContext = {
aliasList: [],
filterValueMap: options.filterValueMap || {},
dim2table: true,
orderByList: [],
needTotalCount: false,
variableParams: {},
paging: {
start: options.start || 0,
limit: options.limit || 100,
},
};
var params = {
timezone: 'GMT+8',
_tb_token_: csrfToken,
_csrf_token: csrfToken,
_csrf: csrfToken,
pageId: reportPageId,
pageName: 'report',
cid: componentNodeId,
cname: componentName,
componentClassName: componentClassName,
queryContext: JSON.stringify(queryContext),
dataSetKey: options.dataSetKey || 'table',
enabledCache: 'true',
queryTimestamp: String(new Date().getTime()),
appendTraceId: 'true',
};
var apiUrl = baseUrl + '/alibaba/web/' + appType
+ '/visual/visualizationDataRpc/getDataAsync.json'
+ '?_api=EDataService.getDataAsync&_mock=false&_stamp=' + new Date().getTime();
return fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'accept': 'application/json, text/json',
'x-requested-with': 'XMLHttpRequest',
},
credentials: 'include',
body: Object.keys(params).map(function(key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
}).join('&'),
})
.then(function(response) { return response.json(); })
.then(function(result) {
if (!result.success) {
throw new Error(result.errorMsg || '报表数据获取失败');
}
return result.content;
});
}
使用示例
export function loadReportChart() {
var appType = window.pageConfig.appType;
this.fetchReportData(
appType,
'REPORT-YRD66CC1P4W34O3FKNPP8BKM5SNV3VIE6WWMMC',
'node_ocmmwwwhdmg',
'基础表格_1',
'YoushuTable',
{
start: 0,
limit: 100,
filterValueMap: {
'filter-xxx': ['筛选值'],
},
}
)
.then(function(content) {
console.log('报表数据:', content);
this.renderChartFromReportData(content);
}.bind(this))
.catch(function(error) {
this.utils.toast({ title: '报表数据获取失败: ' + error.message, type: 'error' });
}.bind(this));
}
⚠️ 注意事项:
- 报表接口需要先通过
yida-report 技能创建原生报表页面并配置好报表组件
cid(组件节点 ID)和 componentClassName(组件类型)可通过以下方式获取:
- 从报表 Schema 解析(推荐):执行
openyida get-schema <appType> <reportFormUuid> 获取报表 Schema,从 Schema 的组件树中提取各组件的 cid(即 componentName 或 id 字段)和组件类型
- 从浏览器 Network 面板抓取:打开报表页面,在 DevTools Network 中筛选
getDataAsync 请求,查看请求参数
- 每个报表组件都有独立的
cid,一个报表页面可能包含多个组件,每个组件发起独立的请求
- 如果尚未创建报表页面,必须先通过 yida-report 技能创建,而非退回使用前端聚合方案
接口说明:getCacheData.json
getCacheData.json 是 getDataAsync.json 的缓存版本,适用于大数据量场景,性能更好。
- 地址:
POST /alibaba/web/{appType}/visual/visualizationDataRpc/getCacheData.json
- Query 参数:
?_api=EDataService.getCacheData&_mock=false
- 参数:与
getDataAsync.json 完全相同
选择建议:
- 数据实时性要求高 → 使用
getDataAsync.json
- 数据量大、性能优先 → 使用
getCacheData.json
隐藏原生报表页面
详见上方「核心规则 → 第 5 节:原生报表页面隐藏规则」。
更新报表 Schema:saveFormSchema.json
当报表需求变化时,需要同步更新原生报表和 ECharts 页面的 Schema。
接口说明
- 地址:
POST /dingtalk/web/{appType}/_view/query/formdesign/saveFormSchema.json
- Content-Type:
application/x-www-form-urlencoded
Body 参数:
| 参数 | 类型 | 必填 | 说明 |
|---|
formUuid | String | 是 | 页面 UUID |
content | String (JSON) | 是 | 页面 Schema JSON |
schemaVersion | String | 是 | 固定 V5 |
importSchema | String | 是 | 固定 "true" |
_csrf_token | String | 是 | CSRF Token |
⚠️ 同步更新规则:
- 修改原生报表的图表配置后,需要同步更新 ECharts 页面的数据获取逻辑
- 修改 ECharts 页面的展示需求后,如需新的聚合数据,需要先更新原生报表添加对应组件
- 两个页面的更新必须在同一次操作中完成,保持一致性
图表渲染规范
DOM 容器约定
每个图表需要一个固定 id 的 DOM 容器,在 renderJsx 中声明:
⚠️ 关键约束:renderJsx 的每个 return 分支都必须包含 <div style={{ display: 'none' }}>{this.state.timestamp}</div>,否则 forceUpdate 调用 this.setState({ timestamp }) 后,React 无法检测到输出变化,renderJsx 不会被重新执行,图表和数据将无法更新。
export function renderJsx() {
var timestamp = this.state.timestamp;
var isMobile = this.utils.isMobile();
return (
<div>
<div style={{ display: 'none' }}>{timestamp}</div>
<div style={styles.container}>
<div style={styles.chartTitle}>销售数据统计</div>
<div
id="chart-bar"
style={{
width: '100%',
height: isMobile ? '300px' : '400px',
}}
/>
</div>
</div>
);
}
图表初始化与销毁
export function createChart(domId) {
var container = document.getElementById(domId);
if (!container) {
console.warn('图表容器不存在: ' + domId);
return null;
}
var existingInstance = window.echarts.getInstanceByDom(container);
if (existingInstance) {
existingInstance.dispose();
}
var chart = window.echarts.init(container);
var rawSetOption = chart.setOption.bind(chart);
var firstOptionApplied = false;
chart.setOption = function(option, notMerge) {
rawSetOption(option, notMerge);
if (!firstOptionApplied) {
firstOptionApplied = true;
setTimeout(function() {
if (window.echarts.getInstanceByDom(container) === chart) {
chart.resize();
rawSetOption(option, true);
}
}, 120);
}
};
return chart;
}
export function didUnmount() {
var chartIds = this.getCustomState('chartIds') || [];
chartIds.forEach(function(domId) {
var container = document.getElementById(domId);
if (container) {
var instance = window.echarts.getInstanceByDom(container);
if (instance) {
instance.dispose();
}
}
});
if (this._resizeHandler) {
window.removeEventListener('resize', this._resizeHandler);
}
}
窗口 resize 自适应
export function bindChartResize() {
this._resizeHandler = function() {
var chartIds = this.getCustomState('chartIds') || [];
chartIds.forEach(function(domId) {
var container = document.getElementById(domId);
if (container) {
var instance = window.echarts.getInstanceByDom(container);
if (instance) {
instance.resize();
}
}
});
}.bind(this);
window.addEventListener('resize', this._resizeHandler);
}
常用图表配置模板
柱状图
export function renderBarChart(categories, values, title) {
var chart = this.createChart('chart-bar');
if (!chart) return;
chart.setOption({
title: { text: title || '', left: 'center' },
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: categories,
axisLabel: {
rotate: categories.length > 8 ? 30 : 0,
fontSize: this.utils.isMobile() ? 10 : 12,
},
},
yAxis: { type: 'value' },
series: [{
type: 'bar',
data: values,
itemStyle: { color: '#0089FF' },
barMaxWidth: 40,
}],
grid: { left: '3%', right: '4%', bottom: '10%', containLabel: true },
});
}
更多图表模板(折线图、饼图、仪表盘等)请参考 examples/ 目录下的完整示例文件。
多端适配
ECharts 图表需要适配 PC 和移动端:
export function getChartHeight(chartType) {
var isMobile = this.utils.isMobile();
var heightMap = {
bar: isMobile ? '280px' : '400px',
line: isMobile ? '280px' : '400px',
pie: isMobile ? '300px' : '380px',
gauge: isMobile ? '250px' : '300px',
};
return heightMap[chartType] || (isMobile ? '280px' : '400px');
}
布局建议:
- PC 端:使用
flexWrap: 'wrap' 实现多图表网格布局,每个图表占 48% 宽度
- 移动端:单列布局,每个图表占
100% 宽度
编码注意事项
- 必须遵循
yida-custom-page 规范:export function 定义方法、事件绑定用箭头函数包裹、禁止 React Hooks
- ECharts 加载时序:所有图表操作必须在
loadScript 的 .then() 回调中执行,确保 window.echarts 已就绪
- DOM 就绪:
echarts.init() 必须在 didMount 之后调用,确保 DOM 已渲染
- 内存管理:
didUnmount 中必须调用 dispose() 销毁所有图表实例,并移除 resize 监听
- pageSize 上限:
searchFormDatas 的 pageSize 最大 100,数据量超过时必须分页拉取
- loading 状态:数据加载期间应展示 loading 提示,使用 ECharts 内置的
chart.showLoading() / chart.hideLoading()
- 错误处理:所有 API 调用和 ECharts 操作都必须有
.catch() 错误处理
- 样式内联:所有样式通过 JS 对象定义,不使用外部 CSS
与其他技能配合
| 技能 | 配合方式 |
|---|
yida-custom-page | 必须先加载,遵循其编码规范编写图表页面 |
yida-report | ECharts 报表的数据源依赖。yida-chart 不包含原生报表创建逻辑,需调用 yida-report 创建原生报表,获取 getDataAsync.json 接口所需参数 |
yida-create-form-page | 创建数据源表单 |
yida-get-schema | 获取表单/报表字段 ID 和 Schema,用于解析 getDataAsync.json 所需的 cubeCode、cid 等参数 |
yida-create-page | 创建自定义页面容器 |
yida-publish-page | 编译并发布图表页面 |
报表设计规范
默认风格:白底简洁商务风
除非用户明确指定了其他风格偏好(如大屏科技风、暗色主题等),报表默认使用白底简洁商务风。
配色方案:
var PALETTE = {
primary: '#1e40af',
primaryLight: '#3b82f6',
accent: '#0ea5e9',
success: '#059669',
warning: '#d97706',
danger: '#dc2626',
neutral: '#64748b',
bg: '#f8fafc',
cardBg: '#ffffff',
border: '#e2e8f0',
textPrimary: '#0f172a',
textSecondary: '#475569',
textMuted: '#94a3b8',
};
设计要素:
| 要素 | 规范 |
|---|
| 页面背景 | #f8fafc(极浅灰),避免纯白 |
| 卡片 | 白底 + border-radius: 10px + 1px solid #e2e8f0 细边框,无阴影或极淡阴影 |
| 字体 | -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, sans-serif |
| KPI 数字 | font-size: 26px + font-weight: 700 + font-feature-settings: "tnum"(等宽数字) |
| 图表 tooltip | 深色半透明背景 rgba(15, 23, 42, 0.92) + 圆角 8px + 柔和阴影 |
| 分割线 | #f1f5f9 虚线(type: [4, 4]),不使用实线 |
| 标签文字 | 12-13px,颜色用 textSecondary 或 textMuted |
报表必备组件
一个完整的数据报表应包含以下组件:
| 组件 | 作用 | 必要性 |
|---|
| KPI 指标卡 | 展示核心数字(总数、完成率、预算等) | ✅ 必须 |
| 图表区域 | 展示趋势和分布(饼图、柱状图、折线图等) | ✅ 必须 |
| 数据明细表格 | 展示每条数据的详细信息,支持排序和分页 | ✅ 必须 |
| 全局筛选栏 | 按维度筛选数据,联动所有图表和表格 | ✅ 推荐 |
| 局部筛选 | 单个图表卡片内的维度切换 | 可选 |
全局筛选器位置
筛选器应放在页面顶层(标题栏下方、KPI 卡片上方),作为全局筛选器,而不是放在明细表上方。这样用户一眼就能看到当前的筛选条件,且筛选操作不会被图表遮挡。
筛选器状态定义
var _customState = {
loading: true,
refreshing: false,
filterStatus: '全部',
filterDateStart: '',
filterDateEnd: '',
};
筛选触发 → 局部刷新(最佳实践)
核心原则:筛选切换时,各区域独立加载、独立更新,不触发全局重渲染。
| 场景 | 加载方式 | 说明 |
|---|
| 首次加载 | loadAllData(全屏 loading) | 页面无数据,需要全屏等待 |
| 筛选刷新 | refreshAllData(局部刷新) | 页面已有数据,各区域独立更新 |
export function applyFilters() {
_customState.refreshing = true;
this.forceUpdate();
this.refreshAllData();
}
export function refreshAllData() {
var self = this;
var pendingCount = 4;
var checkDone = function() {
pendingCount--;
if (pendingCount <= 0) {
_customState.refreshing = false;
self.forceUpdate();
}
};
_fetchReportData(REPORT_COMPONENTS.indicator, 'indicator')
.then(function(content) {
self.forceUpdate();
})
.catch(function(err) { console.error('[看板] 指标卡刷新失败:', err); })
.then(checkDone);
_fetchReportData(REPORT_COMPONENTS.pie, 'pie')
.then(function(content) {
self.renderPieChart();
self.forceUpdate();
})
.catch(function(err) { console.error('[看板] 饼图刷新失败:', err); })
.then(checkDone);
_fetchReportData(REPORT_COMPONENTS.bar, 'bar')
.then(function(content) {
self.renderBarChart();
})
.catch(function(err) { console.error('[看板] 柱状图刷新失败:', err); })
.then(checkDone);
self.loadDetailData(1)
.then(function() { self.forceUpdate(); })
.catch(function(err) { console.error('[看板] 明细表刷新失败:', err); })
.then(checkDone);
}
图表 setOption 原地更新(禁止 dispose 重建)
筛选刷新时,禁止 dispose() 后重新 init(),这会导致图表闪烁。应使用 setOption(option, true) 原地更新。首次初始化使用上方 createChart(),它会在首个 setOption 后延迟执行 resize() + setOption(option, true),规避宜搭 CSS layout 尚未稳定导致 series 不绘制的问题:
export function renderPieChart() {
var container = document.getElementById('pie-chart');
if (!container || !window.echarts) return;
var option = { };
if (_chartInstances.pie) {
_chartInstances.pie.setOption(option, true);
} else {
_chartInstances.pie = this.createChart('pie-chart');
if (_chartInstances.pie) {
_chartInstances.pie.setOption(option, true);
}
}
}
刷新状态 UI 提示
筛选刷新时,不显示全屏 loading 遮罩,而是:
- 标题旁显示"🔄 刷新中..."文字
- 页面顶部显示蓝色进度条
{_customState.refreshing && <span style={{ marginLeft: 8, color: '#3b82f6' }}>🔄 刷新中...</span>}
{_customState.refreshing && (
<div style={{
position: 'fixed', top: 0, left: 0, right: 0,
height: 3, background: 'linear-gradient(90deg, #3b82f6 0%, #60a5fa 50%, #3b82f6 100%)',
zIndex: 9999,
}}></div>
)}
日期筛选格式兼容
searchFormDatas 接口的日期筛选需要毫秒时间戳数组,不能直接传日期字符串:
searchCondition[FIELD.planDate] = JSON.stringify(['2025-03-01', '2025-03-31']);
var startTimestamp = new Date('2025-03-01T00:00:00').getTime();
var endTimestamp = new Date('2025-03-31T23:59:59').getTime();
searchCondition[FIELD.planDate] = JSON.stringify([startTimestamp, endTimestamp]);
数据明细表格
报表中必须包含数据明细表格,用于展示每条数据的详细信息。表格使用纯 JSX 实现(非 ECharts),支持分页、排序和详情跳转。
表单详情页跳转
searchFormDatas 返回的每条记录包含 formInstId 字段,可拼接表单详情页 URL:
https://www.aliwork.com/{appType}/formDetail/{formUuid}?formInstId={formInstId}
export function getDetailUrl(formInstId) {
var appType = window.pageConfig && window.pageConfig.appType;
if (!appType || !formInstId) return '';
return 'https://www.aliwork.com/' + appType + '/formDetail/' + FORM_UUID + '?formInstId=' + formInstId;
}
表格功能要求
| 功能 | 说明 |
|---|
| 列排序 | 点击表头切换升序/降序,支持数值和文本排序 |
| 分页 | 每页 10 条,底部显示分页器(上/下一页 + 页码按钮) |
| 详情链接 | 项目名称和"详情"列可点击,跳转到表单详情页(新窗口打开) |
| 状态标签 | 状态、优先级等字段使用彩色标签(badge)展示 |
| 进度条 | 进度字段使用迷你进度条 + 百分比数字展示 |
| 斑马纹 | 奇偶行交替背景色,提升可读性 |
| 筛选联动 | 表格数据跟随全局筛选器实时过滤 |
表格样式规范
var thStyle = {
padding: '10px 12px', textAlign: 'left', fontWeight: 600,
color: '#475569', fontSize: 12,
borderBottom: '2px solid #e2e8f0',
background: '#f8fafc', whiteSpace: 'nowrap', cursor: 'pointer',
};
var tdStyle = {
padding: '10px 12px', borderBottom: '1px solid #e2e8f0',
color: '#475569', fontSize: 13,
};
var statusBadgeStyle = {
display: 'inline-block', padding: '2px 8px', borderRadius: 4,
fontSize: 11, fontWeight: 600, lineHeight: '18px',
color: statusColor, background: statusColor + '14',
border: '1px solid ' + statusColor + '30',
};
var detailLinkStyle = {
color: '#3b82f6', fontSize: 12, textDecoration: 'none',
cursor: 'pointer', fontWeight: 500,
};
日期格式化
export function formatDate(timestamp) {
if (!timestamp) return '-';
var date = new Date(Number(timestamp));
if (isNaN(date.getTime())) return '-';
var year = date.getFullYear();
var month = date.getMonth() + 1;
var day = date.getDate();
return year + '-' + (month < 10 ? '0' + month : '' + month) + '-' + (day < 10 ? '0' + day : '' + day);
}
原生报表 Schema 构建
原生报表的 Schema 构建(vc-yida-report 组件库、build-yida-report-schema.js 构建脚本)已迁移至 yida-report 技能文档中。
如需创建或更新原生报表,请调用 yida-report 技能,并通过 openyida create-report <appType> "<报表名称>" <配置JSON文件路径> 让 CLI 内部生成 Schema。不要尝试读取 build-yida-report-schema.js 或 vc-yida-report-components-doc.md,这两个文件不随技能包提供。
方案 C:基于已有报表创建 ECharts 页面
概述
当用户提供了一个已有的宜搭原生报表 URL(如 https://www.aliwork.com/APP_XXX/admin/REPORT-XXX),应以该原生报表作为数据源,创建 ECharts 自定义页面实现更美观的展示效果。最终输出是 ECharts 自定义页面,而非优化后的原生报表。
触发条件
用户消息中包含符合以下格式的报表 URL:
https://www.aliwork.com/{appType}/admin/{formUuid}
其中 formUuid 以 REPORT- 开头,表明这是一个宜搭原生报表页面。
执行流程
Step 1: 从 URL 中解析 appType 和 formUuid
↓
Step 2: 执行 openyida env 检测环境和登录态
↓
Step 3: 执行 openyida get-schema <appType> <formUuid> 获取现有报表 Schema
↓ (将完整输出重定向到文件,避免终端截断)
↓ 命令:openyida get-schema <appType> <formUuid> > .cache/report-schema-output.txt 2>&1
↓
Step 4: 解析现有 Schema,提取关键信息:
↓ - cubeCode(数据集编码)
↓ - cubeTenantId(租户 ID)
↓ - 各组件的 fieldDefinitionList(字段定义)
↓ - 各组件的 settings(配置项)
↓ - 组件结构和层级关系
↓
Step 5: 基于提取的数据源参数,创建 ECharts 自定义页面
↓ - 复用原有的 cubeCode、cubeTenantId、cid、componentClassName 等数据源配置
↓ - 通过 getDataAsync.json 接口获取聚合数据
↓ - 使用 ECharts 实现更美观的图表展示
↓
Step 6: 隐藏原生报表页面(双端隐藏)
↓ - 在宜搭管理后台设置原生报表页面 PC 端和移动端均不可见
↓
Step 7: 记录关联关系到 .cache/<项目名>-report-bindding.json
↓
Step 8: 输出 ECharts 自定义页面访问链接
URL 解析规则
Schema 获取注意事项
-
必须将输出重定向到文件:报表 Schema 通常非常大(数千行),终端输出会被截断
openyida get-schema <appType> <formUuid> > .cache/report-schema-output.txt 2>&1
-
提取 JSON 部分:输出文件包含前缀日志信息,需要从 { 开始的行提取纯 JSON
grep -n "^{" .cache/report-schema-output.txt
tail -n +<行号> .cache/report-schema-output.txt > .cache/report-schema.json
-
使用 Node.js 脚本解析:编写脚本提取组件结构、数据源配置等关键信息
ECharts 页面设计策略
基于原生报表的数据源参数创建 ECharts 自定义页面时,遵循以下策略:
| 设计维度 | 具体操作 |
|---|
| 布局设计 | 指标卡在上、图表居中、数据明细表在下 |
| 图表选择 | 根据原报表中的组件类型选择对应的 ECharts 图表(如柱状图→bar、折线图→line、饼图→pie) |
| 筛选交互 | 通过 getDataAsync.json 的 filterValueMap 参数实现筛选联动 |
| 样式定制 | 使用 ECharts 主题、渐变色、动画等实现更美观的视觉效果 |
| 响应式 | 监听 window resize 事件,调用 chart.resize() 适配不同屏幕 |
关键约束
- 必须复用原有数据源:
cubeCode、cubeTenantId、cid、componentClassName 等必须从原报表 Schema 中提取,保持一致
- 不要修改原生报表和数据源表单:只基于原报表的数据接口创建 ECharts 展示层
- 数据获取必须通过 getDataAsync.json:禁止前端聚合,所有聚合统计由服务端完成
- 隐藏原生报表页面:创建 ECharts 页面后,将原生报表设置为双端隐藏,避免用户看到两个入口
- getDataAsync.json 接口参数必须从 Schema 中提取,禁止猜测:
prdId:必须在运行时通过 getFormNavigationListByOrder 接口动态获取(详见上方「prdId 动态获取」章节),不能硬编码,不能用 cubeCode 或其他值替代
pageName:固定为 'report',不能用 'custom' 或其他值
pageId:使用报表的 REPORT-xxx formUuid,不能用自定义页面的 formUuid
cid、componentClassName、dataSetKey:必须从报表 Schema 中各组件的配置提取
- 报表接口返回的是聚合数据,不含
formInstId:
- 报表
getDataAsync 接口返回的 data 是聚合/展示数据,没有 formInstId
- 明细表场景必须用
searchFormDatas 接口(this.utils.yida.searchFormDatas),它返回完整的 formInstId 和 formData
- 详情链接必须用数据源表单的 formUuid(
FORM-xxx),不能用报表的 REPORT-xxx
- 筛选器的
filterValueMap key 必须用 filterKey:
- 报表筛选器的 key 格式是
filter-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx(UUID 格式)
- 必须从报表 Schema 中提取筛选器组件的
filterKey,不能用字段 ID(field_xxx)或 alias
- 筛选值必须是数组格式,如
{ 'filter-xxx': ['已完成'] }
- 纯工具函数必须用
var 声明,不能用 export function:
- 宜搭 Babel 编译器会把
export function 转成组件方法,但纯工具函数(如 fetchReportData)不在白名单中,会被 UglifyJS 消除
- 正确做法:
var _fetchReportData = function(...) { ... };,在 loadAllData 等 export function 中直接调用 _fetchReportData(...)
- 错误做法:
export function fetchReportData(...) + this.fetchReportData(...) → 运行时报 is not a function
常见问题
Q:ECharts 加载失败怎么办?
必须使用阿里 CDN(https://g.alicdn.com/code/lib/echarts/5.6.0/echarts.min.js)。宜搭环境(aliwork.com)对 cdnjs.cloudflare.com 有安全策略限制,会导致脚本加载失败。禁止使用 cloudflare CDN。
Q:forceUpdate is not a function 报错?
代码中缺少宜搭自定义页面必需的 forceUpdate 函数定义。必须在代码中包含以下三个函数:
export function getCustomState(key) { if (key) return _customState[key]; return _.clone(_customState); }
export function setCustomState(newState) { _.assign(_customState, newState); this.forceUpdate(); }
export function forceUpdate() { this.setState({ timestamp: new Date().getTime() }); }
详见上方「ECharts 页面代码必备结构」章节。
Q:页面数据更新后不刷新?
renderJsx 的每个 return 分支都必须包含 <div style={{ display: 'none' }}>{this.state.timestamp}</div>,否则 forceUpdate 无法触发 React 重渲染。
Q:图表不显示?
- 确认 DOM 容器有明确的
height(ECharts 要求容器有高度)
- 确认
echarts.init() 在 DOM 渲染完成后调用(在 didMount 中)
- 打开浏览器控制台查看是否有报错
Q:数据量很大,页面卡顿?
- 使用
chart.showLoading() 展示加载状态
- 考虑只展示最近 N 条数据或按时间范围筛选
- 所有聚合统计必须使用方案二(宜搭报表
getDataAsync.json 接口),由服务端完成聚合计算
Q:如何实现图表联动筛选?
在筛选条件变化时,重新调用数据获取函数并更新图表。
注意:数据请求函数必须用 var 声明为模块级变量(避免被 UglifyJS 消除),筛选 key 必须用报表 Schema 中的 filterKey(filter-xxx 格式):
var _prdId = null;
var _fetchPrdId = function() {
var appType = window.pageConfig && window.pageConfig.appType;
var csrfToken = window.g_config && window.g_config._csrf_token;
var baseUrl = window.location.origin;
var url = baseUrl + '/dingtalk/web/' + appType
+ '/query/formnav/getFormNavigationListByOrder.json'
+ '?_api=Nav.queryList&_mock=false&_csrf_token=' + encodeURIComponent(csrfToken);
console.log('[报表] 正在通过导航菜单获取 prdId(topicId)');
return fetch(url, {
method: 'GET',
credentials: 'include',
headers: {
'accept': 'application/json, text/json',
'x-requested-with': 'XMLHttpRequest',
},
})
.then(function(resp) { return resp.json(); })
.then(function(res) {
if (res.success && Array.isArray(res.content)) {
var targetNav = res.content.find(function(item) {
return item.formUuid === REPORT_FORM_UUID;
});
if (targetNav && targetNav.topicId) {
_prdId = targetNav.topicId;
console.log('[报表] prdId(topicId) 获取成功(精确匹配):', _prdId);
return _prdId;
}
var reportNav = res.content.find(function(item) {
return item.formType === 'report' && item.topicId;
});
if (reportNav) {
_prdId = reportNav.topicId;
console.log('[报表] prdId(topicId) 获取成功(兜底匹配):', _prdId, '来自:', reportNav.formUuid);
return _prdId;
}
throw new Error('应用导航菜单中未找到包含 topicId 的报表');
}
throw new Error(res.errorMsg || '获取应用导航菜单失败');
});
};
var _fetchReportData = function(cid, cname, componentClassName, dataSetKey, filterValueMap) {
var appType = window.pageConfig && window.pageConfig.appType;
var csrfToken = window.g_config && window.g_config._csrf_token;
var body = new URLSearchParams({
timezone: 'GMT+8',
_tb_token_: csrfToken, _csrf_token: csrfToken, _csrf: csrfToken,
prdId: _prdId,
pageId: 'REPORT-XXX',
pageName: 'report',
cid: cid, cname: cname || '',
componentClassName: componentClassName,
queryContext: JSON.stringify({ filterValueMap: filterValueMap || {}, dim2table: true }),
dataSetKey: dataSetKey,
});
var url = '/alibaba/web/' + appType + '/visual/visualizationDataRpc/getDataAsync.json';
return fetch(url, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: body.toString(), credentials: 'include' })
.then(function(r) { return r.json(); })
.then(function(result) { if (result.success) return result.content; throw new Error(result.errorMsg); });
};
export function onFilterChange(filterValue) {
this.setCustomState({ loading: true });
var self = this;
var filterValueMap = { 'filter-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx': [filterValue] };
_fetchReportData('YoushuBar_xxx', '柱状图_1', 'YoushuBar', 'chartData', filterValueMap)
.then(function(content) {
var data = content.data || [];
var meta = content.meta || [];
var dimField = meta[0] && meta[0].alias;
var measureField = meta[1] && meta[1].alias;
var categories = data.map(function(row) { return row[dimField]; });
var values = data.map(function(row) { return row[measureField]; });
self.renderBarChart(categories, values, '筛选结果');
self.setCustomState({ loading: false });
})
.catch(function(error) {
self.utils.toast({ title: error.message, type: 'error' });
self.setCustomState({ loading: false });
});
}