custom.js
custom.js
是聚宝前端嵌入式脚本的约定文件名,当需要对某个页面(如一个仪表板、报表或SuperPage页面等)进行个性化前端开发的时候,通过custom.js
可以向特定的页面注入前端js脚本代码,可以实现如“自定义按钮点击的执行逻辑”、“自定义某个组件的渲染逻辑”之类的个性化需求。
#脚本位置
为了方便管理二次开发的代码,聚宝将多个页面的个性化脚本代码集中到一个custom.js
文件进行管理,也就是说可以通过在特定的位置编写一个custom.js
文件,实现多个目标页面的个性化处理。custom.js
可以位于如下几个位置:
/业务项目/fapp/xxx.fapp/custom.js
- 表单应用内部页面加载/业务项目/app/xxx.app/custom.js
- 应用内嵌入的页面加载/业务项目/public/hooks/custom.js
- 项目内的报表、仪表板、应用、表单应用加载/sysdata/public/hooks/custom.js
- 系统内的报表、仪表板、应用、表单应用、元数据管理界面等所有页面都加载
说明:
- 在
custom.js
中可以通过文件的路径、名称、类型等限定具体的脚本代码或事件代码作用到哪个具体的页面。 - 可以直接编辑
custom.js
文件,也可以在SuccIDE或元数据项目设置中通过脚本编辑器直接编辑ts语法的脚本文件custom.ts
,编辑器会自动编译并生成custom.js
。 - 当存在多个脚本时,系统将以函数为粒度按以上顺序优先使用应用内的、其次使用项目全局的最后使用系统全局的进行调用(如
onDidInitFileViewer
在项目脚本和系统脚本都存在,那么将会只调用项目脚本的,但如果项目脚本文件存在但是并不包含onDidInitFileViewer
函数,那么会继续调用系统脚本文件中的onDidInitFileViewer
函数)。 - 未登录用户也可以访问这个文件,所以将它放在/sysdata/public下面,这样就不需要给它分配权限。
#支持的事件
前端脚本支持大量的事件,不同的类型可能支持不同的事件,具体见 前端脚本支持的事件。
#自定义表达式脚本函数
前端表达式计算时,有时需要执行用户自定义逻辑,并在表达式中引用脚本函数执行结果(如:在表达式中引用数据库查询结果)。当产品内置表达式无法满足需求时,可以通过javascript
脚本扩展表达式的执行能力。
在custom.js
中的CustomExpFunctions
定义个性化的脚本函数,然后通过表达式函数SCRIPT_STR调用脚本函数:
/**
* 提供给表达式中执行的个性化脚本函数。
* 1. 用户定义的个性化脚本函数,在表达式中调用`SCRIPT_STR`函数即可执行。
* 2. context: 表达式计算上下文
* 3. args: 脚本函数执行参数,参数类型只支持基本类型`string|number|boolean|Date`或者基本类型的数组形式`Array<string|number|boolean|Date>`
*/
CustomExpFunctions?: {
[funcName: string]: (context: IExpEvalDataProvider, ...args) => string | Promise<string>;
}
脚本示例:
CustomExpFunctions: {
queryUserData: (context: IExpEvalDataProvider, arg1,...,argN) => string | Promise<string> {
// TODO something
// 在此处实现个性化需求,并返回结果
// let data = null;
// return data;
}
}
在表达式中如何引用:
SCRIPT_STR('queryUserData', 控件1, 控件2);
#脚本模版
下面的钩子函数按需实现,需要实现哪个就把它的注释去掉即可,也可以添加新的事件,支持的事件见脚本前端脚本支持的事件。
/**
* `custom.js`是聚宝前端嵌入式脚本的约定文件名,当需要对某个页面(如一个仪表板、报表或SuperPage页面等)进
* 行个性化前端开发的时候,通过`custom.js`可以向特定的页面注入前端js脚本代码,通过前端脚本开发可以实现如“自定
* 义按钮点击的执行逻辑”、“自定义某个组件的渲染逻辑”之类的个性化需求。
*
* 为了方便管理二次开发的代码,聚宝将多个页面的个性化脚本代码集中到一个`custom.js`文件进行管理,也就是说可以
* 通过在特定的位置编写一个`custom.js`文件,可以多个目标页面的个性化处理。
*
* 更多信息见:</develop/hooks/custom-js/>
*/
import {
throwError,
assign,
message,
rc,
Component,
showSuccessMessage,
ajax,
browser,
rc1,
showErrorMessage,
IPageContainer,
UrlInfo,
encodeUrlInfo,
ctx,
showWaiting,
showDialog,
ctxIf,
AjaxArgs,
} from "sys/sys";
import {
IMetaFileViewer,
MetaFileViewerArgs,
IMetaFileCustomJS
} from "metadata/metadata";
import {
IAppCustomJS,
IFAppCustomJS
} from "metadata/metadata-script-api";
/**
* 可以在此处通过路径、资源ID、文件名、类型指定脚本:
* 优先级顺序为:id>路径>名称>类型>*。
* id/路径/名称 匹配到的脚本之间不会合并,取优先级最高的,与类型、*匹配到的脚本按优先级进行合并。
*
* 1. path或resID, 完整的路径或资源id。
* 2. name,文件名(无路径,带扩展名)
* 3. type,类型
* 4. *,默认
*/
export const CustomJS: { [file_OR_type_OR_resid: string]: IMetaFileCustomJS } = {
/**
* 用完整的路径设置脚本作用到哪个具体的仪表板、报表或spg等,路径以/开头,包含项目名称和文件扩展名
*/
"/path/to/file": {
/**
* 在这里定义报表类型的元数据的个性化脚本,支持的函数见
* </develop/hooks/customjs-events-api/>
*/
},
/**
* 用文件名设置脚本作用到哪个具体的仪表板、报表或spg等,文件名包含文件扩展名
*/
"xxxxx.spg": {
}
}
#示例
#导出pdf
import {
IMetaFileCustomJS
} from "metadata/metadata-script-api";
export const CustomJS: { [type: string]: IMetaFileCustomJS } = {
spg: {
CustomActions: {
// 可下载一个压缩包,含多个pdf文件
/**
* @param pathColumn 文件路径所在列的id,用于获取文件路径
*/
exportPdf: (event: InterActionEvent) => {
let pathColumn = event.params.pathColumn;
if (!pathColumn) {
showWarningMessage('需要指定文件路径所在列的id');
return;
}
var listNode = <AnaNodeData>event.page.getComponent('list1');
var rows = <JSONObject[]>listNode.getCheckedDataRows();
// 获取勾选的pdf文件路径
let paths = rows ? rows.map(row => row[pathColumn]["field"]) : []
if (paths.length === 0) {
showWarningMessage('导出必须选择文件!');
return;
}
return import('ana/anabrowser').then(m => m.exportPDFByPaths(paths));
},
// 可下载一个压缩包,含多个pdf文件
downloadPdf: (event: InterActionEvent) => {
let row = event.dataRow;
//let paths = [row['list1.column6'].field];
let paths = [row['YSJWZLJ']];//这里获取路径,用模型表的物理字段获取,没有用id,因为id无法查看,只能调试才能知道
return import('ana/anabrowser').then(m => m.exportPDFByPaths(paths));
},
// 前端创建iframe渲染页面下载pdf,下载多个pdf
exportOnePdf: (event: InterActionEvent) => {
// let paths = ['/DEMO/ana/test/图表.dash', '/DEMO/ana/test/海洋王国.dash']
let paths = ['/datacvg/ana/商管大数据/商管1/商场看板/1项目总览.dash', '/datacvg/ana/商管大数据/商管1/商场看板/2财务分析.dash']
return import('ana/anabrowser').then(m => {
Promise.all(paths.map(path => {
// 创建 iframe
const iframe = document.createElement('iframe');
// 设置 iframe 属性
iframe.src = path;
let _width = document.body.clientWidth + '';
let height = document.body.clientHeight + '';
iframe.width = _width;
iframe.height = height;
iframe.style.left = '-' + _width + 'px';
iframe.style.top = '-' + height + 'px';
iframe.className = 'export-contain-iframe';
document.body.appendChild(iframe);
return m.exportPDFByPath(path, iframe);
})).then(pdfInfos => {
let pdfInfo = pdfInfos[0];
for (let i = 1, len = pdfInfos.length; i < len; i++) {
pdfInfo.pages.pushAll(pdfInfos[i].pages);
}
return import("commons/jspdf/pdfgen").then((m) => {
return m.saveAsPdf('', pdfInfo, '导出文件').then(() => {
let exportIframes = document.getElementsByClassName('export-contain-iframe');
for (let i = exportIframes.length - 1; i >= 0; i--) {
document.body.removeChild(exportIframes[i]);
}
return <ExportAnaObjectResult>{ type: ExportAnaObjectResultType.PdfInfo, value: pdfInfo }
});
});
});
});
},
// 定义一个动作ignite_full_page_download,用于下载单个pdf
ignite_full_page_download: (event: InterActionEvent) => {
superPage.exportPDFContent().then((PdfBuilder) => PdfBuilder.getPdfInfo().then((PdfInfo) => { import("commons/jspdf/pdfgen").then((m) => { return m.saveAsPdf("", PdfInfo, event.params!.name || PdfInfo.name) }) }))
},
// 定义一个动作downloadMultipleEmployeePdf,用于下载多个员工的PDF, exportPDFByPaths入参paths目前还不支持带参数的路径
downloadMultipleEmployeePdf: (event: InterActionEvent) => {
// 定义页面路径数组
let paths = ['/IMS_DATACVG/app/人事、人力资源模块.app/技能/技能表维护.spg', '/IMS_DATACVG/app/人事、人力资源模块.app/员工信息/个人信息展示.spg'];
// 导出指定路径的PDF内容
return import('ana/anabrowser').then(m => m.exportPDFByPaths(paths));
},
// 定义一个动作ignite_some_page_download,用于下载多个pdf
ignite_some_page_download: (event: InterActionEvent) => {
// 获取员工编号数组和姓名数组
let employee_num_array: Array<number> = event.params!.employee_num;
let name_array: Array<string> = event.params!.name; // 获取姓名数组
// 动态导入元数据模块
import('metadata/metadata').then(
(meta) => {
// 遍历员工编号数组
employee_num_array.forEach(
(employee_num, index) => { // 包含索引参数的回调
// 渲染元数据文件
meta.renderMetaFile(
{
url: `/IMS_DATACVG/app/人事、人力资源模块.app/员工信息/个人信息展示.spg?Employee_Num=${employee_num}`
}
).then(
(component) => {
// 获取超级页面对象
let superpage_object: SuperPage = component.getInnerComponnet();
// 导出页面PDF内容
superpage_object.exportPDFContent().then(
(pdfbuilder) => {
// 获取PDF信息
pdfbuilder.getPdfInfo().then(
(pdfinfo) => {
// 动态导入PDF生成模块
import('commons/jspdf/pdfgen').then(
(pdfgen) => {
// 使用姓名数组中的对应姓名作为文件名保存PDF
pdfgen.saveAsPdf(`${name_array[index]}`, pdfinfo, name_array[index]!);
}
);
}
);
}
);
}
);
}
);
}
);
},
}
}
};
#跳转小程序
import {
encodeUrlInfo,
} from "sys/sys";
import {
IMetaFileCustomJS,
} from "metadata/metadata";
import {
InterActionEvent,
} from "metadata/metadata-script-api";
const GoMiniPage_CustomActions = {
goToLogin: (event: InterActionEvent) => {
// console.log('goToLogin', event)
let _url = event?.params?.url
let _title = event?.params?.title
if (!_url) {
return;
}
_url = window.location.origin + _url;
if (/MicroMessenger/i.test(window.navigator.userAgent)) {
//微信环境
// import("/xiaoshouyi/public/hooks/weixin/jweixin-1.6.0.js").then((wx) => {
wx.miniProgram.getEnv(function (res) {
console.log(res)
if (res.miniprogram) {
//微信小程序
const url = encodeUrlInfo({
path: '/pages/needLoginPage/needLoginPage',
params: {
url: _url,
title: _title,
}
});
wx.miniProgram.navigateTo({
url
});
} else {
//微信网页
location.href = _url;
}
});
// })
} else {
//非微信浏览器
location.href = _url;
}
},
goToMiniProgram: (event: InterActionEvent) => {
let _url = event?.params?.url; // 网页跳转url路径
_url = window.location.origin + _url;
let _path = event?.params?.path; // 原生小程序路径
if (/MicroMessenger/i.test(window.navigator.userAgent)) {
//微信环境
// import("/xiaoshouyi/public/hooks/weixin/jweixin-1.6.0.js").then((wx) => {
wx.miniProgram.getEnv(function (res) {
if (res.miniprogram) {
// console.log('微信小程序')
//微信小程序
const url = encodeUrlInfo({
path: _path || '/pages/index/index',
params: {
logout: event?.params?.logout, // 小程序是否退出登录标识 传true为退出登录清楚小程序user中token缓存,否则不退出
}
});
wx.miniProgram.reLaunch({ // 用navigateTo对于tabbar页面不生效
url
});
} else {
//微信网页
location.href = _url;
}
});
// })
} else {
//非微信浏览器
location.href = _url;
}
},
}
export const CustomJS: { [file_OR_type_OR_resid: string]: IMetaFileCustomJS } = {
"会员未注册.spg": {
/**
* 在这里定义报表类型的元数据的个性化脚本,支持的函数
* <https://dpro.datacvg.com/dprodevelop/hooks/customjs-events-api/>
*/
onRender: async (event: InterActionEvent): Promise<void> => {
console.log('onRenderonRender')
},
CustomActions: GoMiniPage_CustomActions
},
"/xiaoshouyi/app/售后.app/绑定车辆/会员已注册.spg": {
CustomActions: GoMiniPage_CustomActions
}
#门户自定义菜单
import {
ctx,
SZEvent,
CommandItemInfo,
} from "sys/sys";
import {
IMetaFileCustomJS
} from "metadata/metadata";
import {
IAppCustomJS
} from "metadata/metadata-script-api";
/**
* 社区门户自定义。
*
* * 自定义用户菜单
*/
class TpgCustomCommunity implements IAppCustomJS {
// 用户菜单
static USER_MENU_ITEMS: (CommandItemInfo & { onclick?: (sze: SZEvent) => void })[] = [
// 个人信息
{
id: "userInfo",
cmd: "showUserInfo",
icon: "icon-user",
caption: "个人信息",
onclick: function () {
window.open(ctx("/me/settings"));
},
},
// 修改密码
{
id:"updatePassword",
cmd: "showModifyPasswordDialog",
icon: "icon-edit",
},
// 分割线
{
id: "-",
},
// 注销
{
id: "logout",
cmd: "logout",
icon: "icon-logout"
}
]
onDidInitFrame(frame: any): void | Promise<void> {
const _old_getAllAvailableCommands = frame.getAllAvailableCommands;
frame.getAllAvailableCommands = function (pcmd) {
return _old_getAllAvailableCommands.call(this, pcmd).then(results => {
if (pcmd?.id === "userMenu") {
results = TpgCustomCommunity.USER_MENU_ITEMS;
} else if (pcmd?.id === "narrowIcon") {
// 收起后的菜单项
}
return results;
});
}
TpgCustomCommunity.USER_MENU_ITEMS.forEach(e => {
if (e.onclick) {
frame[e.cmd] = e.onclick;
}
});
}
}
export const CustomJS: { [file_OR_type_OR_resid: string]: IMetaFileCustomJS } = {
"/tmp20240320/app/tmp.app/index.tpg": new TpgCustomCommunity() as IMetaFileCustomJS
}
#echarts图表定制化渲染
import {
IMetaFileCustomJS
} from "metadata/metadata";
class SumBar {
public onRender(event) {
// let fileInfo = event.page.getFileInfo();
let component = event.component;
if (!component) {
return;
}
if (component.getId().includes('bar')) {
console.log('event', event);
let option = event.data.echartOption as JSONObject;
let echart = event.uicomponent.echart;
let calcSum = (dataset: Array<JSONObject>, filter?: (row) => boolean) => {
let sumMap = {};
dataset.forEach(ds => {
let rows = ds.source;
for (let i = 1; i < rows.length; i++) {
let row = rows[i];
let key = row[0];
if (filter && !filter(row)) {
continue;
}
//只考虑一个指标的话,最后2个就是指标,最后一个是描述,倒数第二个是值
let measureValue = row[row.length - 2] ?? 0;
let v = sumMap[key];
if (v === undefined) {
v = {
caption: row[1],
value: measureValue
};
sumMap[key] = v;
} else {
// let _value = v.value + measureValue
// 处理小数相加精度丢失问题
v.value = (Math.round(Number(v.value) * 100) + Math.round(Number(measureValue) * 100)) / 100;
}
}
});
let sumRow = [['dimensionField', 'dimensionField_desc', 'measureField', 'measureField_desc']];
for (const key in sumMap) {
let v = sumMap[key];
let value = v.value;
let row = [key, v.caption, value, `${value}`];
sumRow.push(row);
}
return sumRow;
};
let dataset = option.dataset;
let sumRow = calcSum(dataset);
dataset.push({ source: sumRow });
//添加系列
let series = option.series as Array<JSONObject>;
series.forEach(serie => {
serie.z = 1;
});
let sumSeries = Object.assign({}, series[0], {
z: 0,
name: '总数',
barGap: -1,
type: "bar",
seriesIndex: series.length,
datasetIndex: dataset.length - 1,
stack: 'measureField',
encode: { x: 0, y: 'measureField', label: 'measureField_desc' },
tooltip: { show: false },
itemStyle: Object.assign({}, series[0]?.itemStyle, { color: "rgba(0,0,0,0)" }),
label: Object.assign({}, series[0]?.label, {
position: 'top',
formatter: (p) => {
return p.data[p.data.length - 1];
}
})
});
series.push(sumSeries);
echart.instance.on("legendselectchanged", function (event) {
console.log(event)
let selected = event.selected as JSONObject;
let dataset = option.dataset as Array<JSONObject>;
let sumRow = calcSum(dataset.slice(0, dataset.length - 1), (row) => {
return selected[row[2]];
});
dataset[dataset.length - 1] = { source: sumRow };
echart.setOption(option);
});
}
}
}
export const CustomJS: { [file_OR_type_OR_resid: string]: IMetaFileCustomJS } = {
/**
* 用完整的路径设置脚本作用到哪个具体的仪表板、报表或spg等,路径以/开头,包含项目名称和文件扩展名
*/
"02 对比分析_hl.dash": new SumBar(),
"02 对比分析.dash": new SumBar()
}
#表格定制化渲染
import {
IMetaFileCustomJS
} from "metadata/metadata";
class SumBar {
public onRender(event) {
// let fileInfo = event.page.getFileInfo();
let component = event.component;
if (!component) {
return;
}
if(component.getId()==='list1'){
let tableBuilder=event.uicomponent.tableBuilder;
// console.log( tableBuilder',tableBuilder儿表格所有单元格内容
let cells =tableBuilder.getcells()as any[][];
cells.forEach((rowcells,i)=>{
if(i > 0){
let cell1= rowcells[5]
let cel12 = rowcells[6]
let cell1ValueArr = cell1.value.split('|')
let cel12ValueArr = cel12.value.split('|')
cell1ValueArr.forEach((item,i)=>{
let _index= cel12ValueArr.findIndex(elem=>elem==item)
if( _index>-1){
cell1ValueArr.splice(i, 1, `<span style="color: red;">${item}</span>`)
cel12ValueArr.splice(_index, 1 ,`<span style="color: red;">${item}</span>`)
}
})
let cell1html = `<span class="tablecelleditor-text ellipsis">${cell1ValueArr.join('|')}</span>`
cell1.setValue(cell1html)
cell1.setHtml(cell1html)
cell1.setProperty("contentType","html")
let cell2html = `<span class="tablecelleditor-text ellipsis">${cel12ValueArr.join('|')}</span>`
cel12.setValue(cell2html)
cel12.setHtml(cell2html)
cel12.setProperty("contentType","html")
}
})
}
}
}
export const CustomJS: { [file_OR_type_OR_resid: string]: IMetaFileCustomJS } = {
/**
* 用完整的路径设置脚本作用到哪个具体的仪表板、报表或spg等,路径以/开头,包含项目名称和文件扩展名
*/
"02 对比分析_hl.dash": new SumBar(),
"02 对比分析.dash": new SumBar()
}