custom.js

custom.js是聚宝前端嵌入式脚本的约定文件名,当需要对某个页面(如一个仪表板、报表或SuperPage页面等)进行个性化前端开发的时候,通过custom.js可以向特定的页面注入前端js脚本代码,可以实现如“自定义按钮点击的执行逻辑”、“自定义某个组件的渲染逻辑”之类的个性化需求。

#脚本位置

为了方便管理二次开发的代码,聚宝将多个页面的个性化脚本代码集中到一个custom.js文件进行管理,也就是说可以通过在特定的位置编写一个custom.js文件,实现多个目标页面的个性化处理。custom.js可以位于如下几个位置:

  1. /业务项目/fapp/xxx.fapp/custom.js - 表单应用内部页面加载
  2. /业务项目/app/xxx.app/custom.js - 应用内嵌入的页面加载
  3. /业务项目/public/hooks/custom.js - 项目内的报表、仪表板、应用、表单应用加载
  4. /sysdata/public/hooks/custom.js - 系统内的报表、仪表板、应用、表单应用、元数据管理界面等所有页面都加载

说明:

  1. custom.js中可以通过文件的路径、名称、类型等限定具体的脚本代码或事件代码作用到哪个具体的页面。
  2. 可以直接编辑custom.js文件,也可以在SuccIDE或元数据项目设置中通过脚本编辑器直接编辑ts语法的脚本文件custom.ts,编辑器会自动编译并生成custom.js
  3. 当存在多个脚本时,系统将以函数为粒度按以上顺序优先使用应用内的、其次使用项目全局的最后使用系统全局的进行调用(如onDidInitFileViewer在项目脚本和系统脚本都存在,那么将会只调用项目脚本的,但如果项目脚本文件存在但是并不包含onDidInitFileViewer函数,那么会继续调用系统脚本文件中的onDidInitFileViewer函数)。
  4. 未登录用户也可以访问这个文件,所以将它放在/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()

}