import type { Workbook } from '@univerjs/core';
|
|
import { Disposable, DisposableCollection, generateRandomId, Inject, Injector, IUniverInstanceService } from '@univerjs/core';
|
import {
|
CanvasRenderMode,
|
DEFAULT_FONTFACE_PLANE,
|
Engine,
|
IRenderManagerService,
|
ITextWatermarkConfig,
|
Scene,
|
SHEET_VIEWPORT_KEY,
|
Spreadsheet,
|
SpreadsheetSkeleton,
|
Viewport,
|
} from '@univerjs/engine-render';
|
import { UniverType } from '@univerjs/protocol';
|
import { SheetPrintInterceptorService, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
|
|
import { translateHeaderFooterText } from '../utils';
|
import { JnpfPrintAlignEnum, JnpfPrintCustomFooterEnum, JnpfPrintCustomHeaderEnum, JnpfPrintOtherParamsEnum } from '../utils/define';
|
import { buildUUID } from '../utils/uuid';
|
|
export class JnpfSheetsPrintUiService extends Disposable {
|
// 获取容器元素
|
get container() {
|
return this._containerEle;
|
}
|
|
get root() {
|
return this._root;
|
}
|
|
private _containerEle = document.createElement('div');
|
private _dirty: boolean = false;
|
|
private _effects: DisposableCollection = new DisposableCollection();
|
private _engine!: Engine;
|
private _key: string;
|
private _layoutConfig: any;
|
|
private _leftTopViewport!: Viewport;
|
private _leftViewport!: Viewport;
|
private _mainViewport!: Viewport;
|
|
private readonly _previewContainerScale: number;
|
private readonly _printConfig: any;
|
private _root: HTMLDivElement = document.createElement('div');
|
|
private _scene!: Scene;
|
|
private readonly _sceneKey: string;
|
private _sheetPrintInterceptorService: SheetPrintInterceptorService;
|
private readonly _skeleton!: SpreadsheetSkeleton;
|
private _spreadsheetObject: any;
|
|
private _topViewport!: Viewport;
|
|
private _totalHeight: number = 0;
|
private _totalWidth: number = 0;
|
private _univerInstanceService: IUniverInstanceService;
|
private readonly _watermarkConfig: any;
|
|
constructor(
|
@Inject(Injector) private readonly _accessor: Injector,
|
layoutConfig = {},
|
printConfig = {},
|
watermarkConfig = {},
|
previewContainerScale = 1, // 打印预览容器的伸缩比
|
) {
|
super();
|
|
this._univerInstanceService = this._getUniverInstanceService();
|
this._sheetPrintInterceptorService = this._getSheetPrintInterceptorService();
|
|
this._layoutConfig = layoutConfig as any;
|
this._printConfig = printConfig as any;
|
this._watermarkConfig = watermarkConfig as any;
|
this._previewContainerScale = previewContainerScale;
|
|
const { subUnitId, unitId } = this._layoutConfig;
|
this._sceneKey = this._getSceneKey(unitId, subUnitId);
|
|
this._key = `${unitId}_${subUnitId}`;
|
const skeleton = this._getSkeleton(unitId, subUnitId);
|
if (!skeleton) {
|
return;
|
}
|
this._skeleton = skeleton;
|
this._root.className = '__root';
|
this._initRender(); // 初始化渲染
|
}
|
|
public _prepare() {
|
this._resizeViewport();
|
this._effects.dispose();
|
this._effects = new DisposableCollection();
|
const { offsetX, offsetY, scale } = this._getTranslateOffset();
|
const { subUnitId, unitId } = this._layoutConfig;
|
this._root.style.left = `${offsetX}px`;
|
this._root.style.top = `${offsetY}px`;
|
const width = this._totalWidth * scale;
|
const height = this._totalHeight * scale;
|
this._root.style.width = `${width}px`;
|
this._root.style.height = `${height}px`;
|
this._root.style.position = 'absolute';
|
this._root.style.overflow = 'hidden';
|
this._sheetPrintInterceptorService.interceptor.fetchThroughInterceptors(
|
this._sheetPrintInterceptorService.interceptor.getInterceptPoints().PRINTING_DOM_COLLECT,
|
)(this._effects, {
|
unitId,
|
subUnitId,
|
scene: this._scene,
|
engine: this._engine,
|
root: this._root,
|
worksheet: this._skeleton.worksheet,
|
skeleton: this._skeleton,
|
offset: this._getTranslateOffset(),
|
});
|
}
|
|
clearMemory() {
|
this._skeleton.resetCache();
|
}
|
|
/**
|
* 释放资源
|
*/
|
dispose() {
|
super.dispose(); // 调用父类的dispose方法
|
|
if (this._containerEle.parentElement) {
|
// eslint-disable-next-line unicorn/prefer-dom-node-remove
|
this._containerEle.parentElement?.removeChild(this._containerEle); // 移除容器
|
}
|
}
|
|
/**
|
* 监听变换并在准备好时渲染
|
*/
|
renderOnReady() {
|
const observable = this._engine?.onTransformChange$?.subscribeEvent(() => {
|
this.renderPage();
|
});
|
|
this.disposeWithMe(observable); // 订阅事件并在销毁时清理
|
}
|
|
/**
|
* 渲染页面
|
*/
|
renderPage() {
|
this._clearEngineCanvas(); // 清除画布
|
|
this._renderMainScene(); // 渲染主场景(核心内容)
|
this._renderHeaderFooter(); // 渲染页眉页脚
|
}
|
|
/**
|
* 标记当前状态为脏(需要重新渲染)
|
*/
|
setDirty(state: boolean) {
|
this._dirty = state;
|
}
|
|
/**
|
* 将电子表格对象添加到场景中
|
*/
|
private _addSpreadsheetToScene() {
|
// 创建电子表格对象,传入唯一标识符、骨架对象,并设置初始化选项
|
const spreadsheet = new Spreadsheet('__JnpfSheetPrintRender__', this._skeleton, false);
|
|
// 将电子表格设置为打印模式,确保以打印样式进行渲染
|
spreadsheet.isPrinting = true;
|
|
// 将电子表格对象添加到当前场景中,纳入渲染流程
|
this._scene.addObject(spreadsheet);
|
|
// 缓存电子表格对象,便于后续访问或操作
|
this._spreadsheetObject = spreadsheet;
|
|
// 从布局配置中获取单元 ID 和子单元 ID,用于拦截器逻辑
|
const { subUnitId, unitId } = this._layoutConfig ?? {};
|
|
// 获取拦截器实例与拦截点,确保存在后再进行触发
|
const interceptor = this._sheetPrintInterceptorService?.interceptor;
|
const interceptPoints = interceptor?.getInterceptPoints();
|
const printCollectPoint = interceptPoints?.PRINTING_COMPONENT_COLLECT;
|
|
// 如果拦截器和拦截点存在,则触发拦截逻辑
|
if (interceptor && printCollectPoint) {
|
interceptor.fetchThroughInterceptors(printCollectPoint)(
|
undefined, // 没有特定请求参数,传入 undefined
|
{
|
scene: this._scene,
|
subUnitId,
|
unitId,
|
engine: this._engine,
|
root: this._root,
|
worksheet: this._skeleton.worksheet,
|
skeleton: this._skeleton,
|
offset: this._getTranslateOffset(),
|
spreadsheet: this._spreadsheetObject,
|
}, // 将单元 ID、子单元 ID 和场景信息传递给拦截器
|
);
|
}
|
}
|
|
/**
|
* 添加并初始化多个视口(Viewport)到场景中
|
*/
|
private _addViewport() {
|
// 从视口键名常量中解构出各个视口的标识符
|
const { VIEW_MAIN, VIEW_MAIN_LEFT, VIEW_MAIN_LEFT_TOP, VIEW_MAIN_TOP } = SHEET_VIEWPORT_KEY;
|
|
// 创建主视口,负责渲染主要内容区域
|
this._mainViewport = new Viewport(VIEW_MAIN, this._scene);
|
|
// 创建左侧视口,负责渲染左侧固定区域
|
this._leftViewport = new Viewport(VIEW_MAIN_LEFT, this._scene);
|
|
// 创建顶部视口,负责渲染顶部固定区域
|
this._topViewport = new Viewport(VIEW_MAIN_TOP, this._scene);
|
|
// 创建左上角视口,负责渲染左上角交叉固定区域
|
this._leftTopViewport = new Viewport(VIEW_MAIN_LEFT_TOP, this._scene);
|
}
|
|
/**
|
* 清空引擎内画布
|
*/
|
private _clearEngineCanvas() {
|
this._engine?.clearCanvas();
|
}
|
|
/**
|
* 绘制水印
|
* @param ctx Canvas 2D 渲染上下文
|
* @param config 水印配置参数
|
*/
|
private _drawWatermark(ctx: CanvasRenderingContext2D, config: ITextWatermarkConfig) {
|
const { bold, color, content, direction, fontSize: fontSizeConf, italic, opacity, repeat, rotate, spacingX, spacingY, x, y } = config;
|
|
if (!content) return;
|
|
const fontSize = fontSizeConf * this._previewContainerScale;
|
const { h, w } = this._getPaperContainerSize();
|
|
ctx.save();
|
ctx.globalAlpha = opacity;
|
|
const fontWeight = bold ? 'bold' : 'normal';
|
const fontStyle = italic ? 'italic' : 'normal';
|
ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${DEFAULT_FONTFACE_PLANE}`;
|
ctx.fillStyle = color;
|
ctx.textBaseline = 'top';
|
|
// **计算文本尺寸**
|
const textWidth = ctx.measureText(content).width;
|
const textHeight = fontSize; // 大部分情况下,高度近似等于字体大小
|
|
// **方向控制**
|
let stepX = spacingX + textWidth; // 修正水平间距
|
let startX = x;
|
let textAlign: CanvasTextAlign = 'left';
|
|
if (direction === 'rtl') {
|
stepX = -(spacingX + textWidth);
|
startX = w - x;
|
textAlign = 'right';
|
}
|
|
ctx.textAlign = textAlign;
|
|
if (repeat) {
|
// **修正垂直间距**
|
const stepY = spacingY + textHeight;
|
|
// **计算水印覆盖区域,防止边缘空白**
|
const cols = Math.ceil(w / Math.abs(stepX)) + 2;
|
const rows = Math.ceil(h / stepY) + 2;
|
|
for (let col = -1; col < cols; col++) {
|
for (let row = -1; row < rows; row++) {
|
const posX = startX + col * stepX;
|
const posY = y + row * stepY;
|
|
ctx.save();
|
ctx.translate(posX, posY);
|
ctx.rotate((rotate * Math.PI) / 180);
|
ctx.fillText(content, 0, 0);
|
ctx.restore();
|
}
|
}
|
} else {
|
ctx.save();
|
ctx.translate(startX, y);
|
ctx.rotate((rotate * Math.PI) / 180);
|
ctx.fillText(content, 0, 0);
|
ctx.restore();
|
}
|
|
ctx.restore();
|
}
|
|
/**
|
* 省略号裁剪文本
|
*/
|
private _ellipsisText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number) {
|
if (ctx.measureText(text).width <= maxWidth) {
|
return text;
|
}
|
|
let truncated = text;
|
while (ctx.measureText(`${truncated}...`).width > maxWidth && truncated.length > 0) {
|
truncated = truncated.slice(0, -1);
|
}
|
|
return `${truncated}...`;
|
}
|
|
/**
|
* 获取纸张容器的尺寸
|
*/
|
private _getPaperContainerSize() {
|
// 从布局配置中解构出纸张尺寸(宽度 w 和高度 h)
|
const { h = 0, w = 0 } = this._layoutConfig?.paperSize ?? {};
|
|
// 检查宽度和高度是否有效
|
if (w <= 0 || h <= 0) {
|
console.warn('纸张尺寸配置无效,宽度或高度小于等于0。');
|
}
|
|
// 根据预览容器的缩放比例,计算实际渲染的尺寸
|
const scaledWidth = w * this._previewContainerScale; // 宽度按缩放比例调整
|
const scaledHeight = h * this._previewContainerScale; // 高度按缩放比例调整
|
|
// 返回计算后的尺寸对象
|
return {
|
h: scaledHeight,
|
w: scaledWidth,
|
};
|
}
|
|
/**
|
* 获取纸张边距(Padding)
|
*/
|
private _getPaperPadding() {
|
const { bottom = 0, left = 0, right = 0, top = 0 } = this._layoutConfig?.paperPadding ?? {};
|
const previewContainerScale = this._previewContainerScale;
|
|
return {
|
bottom: bottom * previewContainerScale,
|
left: left * previewContainerScale,
|
right: right * previewContainerScale,
|
top: top * previewContainerScale,
|
};
|
}
|
|
/**
|
* 获得场景专一key
|
* @param unitId
|
* @param subUnitId
|
*/
|
private _getSceneKey(unitId: string, subUnitId: string) {
|
return `${unitId}_${subUnitId}_${buildUUID()}`;
|
}
|
|
/**
|
* 获取到sheetPrintInterceptorService
|
*/
|
private _getSheetPrintInterceptorService() {
|
return this._accessor.get(SheetPrintInterceptorService);
|
}
|
|
/**
|
* 获取到skeleton
|
* @param unitId
|
* @param subUnitId
|
*/
|
private _getSkeleton(unitId: string, subUnitId: string) {
|
return this._accessor.get(IRenderManagerService)?.getRenderById(unitId)?.with(SheetSkeletonManagerService)?.getOrCreateSkeleton({ sheetId: subUnitId });
|
}
|
|
/**
|
* 获取平移偏移量,用于将打印内容定位到页面上的正确位置
|
*
|
* 根据纸张的尺寸、内边距、打印配置中的对齐方式以及总内容的宽度和高度,计算水平和垂直方向的偏移量。
|
* 最终返回的偏移量用于定位打印内容的起始位置,以便它能够根据配置进行正确的对齐。
|
*/
|
private _getTranslateOffset() {
|
// 获取纸张的内边距和纸张容器的尺寸
|
const paperContainerSize = this._getPaperContainerSize();
|
const paperPadding = this._getPaperPadding();
|
|
// 解构获取打印配置中的对齐方式(水平对齐和垂直对齐)
|
const { hAlign, vAlign } = this._printConfig;
|
|
// 计算最大缩放比例,确保内容在X和Y方向上都能够缩放适应
|
const scale = Math.max(this._scene.scaleX, this._scene.scaleY);
|
|
// 计算水平偏移量(offsetX)
|
let offsetX: number;
|
if (hAlign === JnpfPrintAlignEnum.start) {
|
// 水平对齐方式为左对齐,偏移量等于左边距
|
offsetX = paperPadding.left;
|
} else if (hAlign === JnpfPrintAlignEnum.end) {
|
// 水平对齐方式为右对齐,偏移量等于纸张宽度减去内容宽度和右边距
|
offsetX = paperContainerSize.w - this._totalWidth * scale - paperPadding.right;
|
} else {
|
// 水平对齐方式为居中对齐,计算居中位置
|
offsetX = paperPadding.left + (paperContainerSize.w - this._totalWidth * scale - paperPadding.left - paperPadding.right) / 2;
|
}
|
|
// 计算垂直偏移量(offsetY)
|
let offsetY: number;
|
if (vAlign === JnpfPrintAlignEnum.start) {
|
// 垂直对齐方式为上对齐,偏移量等于上边距
|
offsetY = paperPadding.top;
|
} else if (vAlign === JnpfPrintAlignEnum.end) {
|
// 垂直对齐方式为下对齐,偏移量等于纸张高度减去内容高度和下边距
|
offsetY = paperContainerSize.h - this._totalHeight * scale - paperPadding.bottom;
|
} else {
|
// 垂直对齐方式为居中对齐,计算居中位置
|
offsetY = (paperContainerSize.h - this._totalHeight * scale - paperPadding.top - paperPadding.bottom) / 2 + paperPadding.top;
|
}
|
|
// 返回四舍五入后的偏移量,确保偏移值为整数
|
return { offsetX: Math.round(offsetX), offsetY: Math.round(offsetY), scale };
|
}
|
|
/**
|
* 获取到univerInstanceService
|
*/
|
private _getUniverInstanceService() {
|
return this._accessor.get(IUniverInstanceService);
|
}
|
|
/**
|
* 初始化渲染引擎、场景、视口,并配置缩放比例和资源管理。
|
*/
|
private _initRender() {
|
// 获取纸张容器的宽度和高度,确保渲染区域的尺寸准确
|
const { h: paperContainerH, w: paperContainerW } = this._getPaperContainerSize();
|
|
// 设置像素比为 1,通常用于标准屏幕显示
|
const pixelRatio = 1;
|
|
// 创建渲染引擎实例
|
// 参数:宽度、高度、像素比、渲染模式
|
this._engine = new Engine('', {
|
elementWidth: paperContainerW,
|
elementHeight: paperContainerH,
|
dpr: pixelRatio,
|
renderMode: CanvasRenderMode.Printing, // 设置渲染模式为打印模式(Printing),确保画布的内容适配打印需求
|
});
|
|
// 创建场景对象,并将其与渲染引擎关联
|
// 参数:场景的唯一标识符、渲染引擎实例
|
this._scene = new Scene(this._sceneKey, this._engine);
|
|
// 将渲染引擎绑定到指定的 DOM 容器中
|
// 参数:容器元素、是否清除现有内容(false 表示不清除)
|
this._engine.setContainer(this._containerEle, false);
|
this._engine
|
.getCanvas()
|
.getContext()
|
.setId(`${this._key}_${generateRandomId(4)}`);
|
// 从布局配置中获取打印缩放比例,若未配置则默认为 1
|
const { printScale = 1 } = this._layoutConfig;
|
|
// 计算场景的最终缩放比例,结合打印缩放比例和预览容器的缩放比例
|
const sceneScale = printScale * this._previewContainerScale;
|
|
// 将计算得到的缩放比例应用到场景中
|
// 参数:x 方向缩放比例,y 方向缩放比例
|
this._scene.scale(sceneScale, sceneScale);
|
|
// 将电子表格组件添加到场景中
|
this._addSpreadsheetToScene();
|
|
// 创建视口并与当前场景关联,确保视口正确渲染内容
|
this._addViewport();
|
|
// 注册资源销毁回调,确保在对象生命周期结束时释放资源,防止内存泄漏
|
this.disposeWithMe({
|
dispose: () => {
|
// 释放引擎资源,确保渲染引擎的内存被回收
|
this._engine.dispose();
|
|
// 释放场景资源,确保场景相关的内存被回收
|
this._scene.dispose();
|
this.clearMemory();
|
},
|
});
|
}
|
|
/**
|
* 渲染页眉和页脚
|
*/
|
private _renderHeaderFooter() {
|
// 从布局配置中解构获取单元ID和子单元ID
|
const { subUnitId, unitId } = this._layoutConfig ?? {};
|
|
// 获取工作簿对象
|
const workbook = this._univerInstanceService?.getUnit<Workbook>(unitId, UniverType.UNIVER_SHEET);
|
if (!workbook) return; // 如果未找到工作簿,直接返回
|
|
// 获取工作表对象
|
const worksheet = workbook?.getSheetBySheetId(subUnitId);
|
if (!worksheet) return; // 如果未找到工作表,直接返回
|
|
// 获取Canvas上下文对象
|
const ctx = this._engine?.getCanvas()?.getContext();
|
ctx.save(); // 保存当前绘图状态
|
ctx.font = `13px ${DEFAULT_FONTFACE_PLANE}`; // 设置字体样式
|
|
// 获取纸张尺寸并计算绘制位置
|
const { h, w } = this._getPaperContainerSize();
|
const x = 20 * this._previewContainerScale;
|
const y = 20 * this._previewContainerScale;
|
|
// 获取页眉页脚的参数
|
const { headerFooterParams, headerFooterType } = this._printConfig ?? {};
|
|
if (headerFooterType === 'default') {
|
const maxTextWidth = (w - 2 * x) / 2;
|
|
// 【左上角】渲染工作簿名称
|
if (headerFooterParams?.includes(JnpfPrintOtherParamsEnum?.workbookTitle)) {
|
const truncatedText = this._ellipsisText(ctx, this._printConfig.workbookTitleText, maxTextWidth);
|
ctx.fillText(truncatedText, x, y);
|
}
|
|
// 【右上角】渲染工作表名称
|
if (headerFooterParams?.includes(JnpfPrintOtherParamsEnum?.worksheetTitle)) {
|
const sheetName = worksheet?.getName() ?? '';
|
const truncatedText = this._ellipsisText(ctx, sheetName, maxTextWidth);
|
const textMetrics = ctx.measureText(truncatedText);
|
const textWidth = textMetrics.width;
|
ctx.fillText(truncatedText, w - textWidth - x, y); // 右对齐
|
}
|
|
// 【左下角】渲染日期和时间
|
if (headerFooterParams?.includes(JnpfPrintOtherParamsEnum?.printDate) || headerFooterParams?.includes(JnpfPrintOtherParamsEnum?.printTime)) {
|
let dateStr = new Date().toLocaleString();
|
if (headerFooterParams?.includes(JnpfPrintOtherParamsEnum?.printDate) && !headerFooterParams?.includes(JnpfPrintOtherParamsEnum?.printTime)) {
|
dateStr = new Date().toLocaleDateString();
|
} else if (headerFooterParams?.includes(JnpfPrintOtherParamsEnum?.printTime) && !headerFooterParams?.includes(JnpfPrintOtherParamsEnum?.printDate)) {
|
dateStr = new Date().toLocaleTimeString();
|
}
|
|
const truncatedText = this._ellipsisText(ctx, dateStr, maxTextWidth);
|
ctx.fillText(truncatedText, x, h - y); // 绘制时间/日期
|
}
|
|
// 【右下角】渲染页码
|
if (headerFooterParams?.includes(JnpfPrintOtherParamsEnum?.pageNumber)) {
|
const pageNumberText = `${this._layoutConfig?.sheetPageNumber}`;
|
const truncatedText = this._ellipsisText(ctx, pageNumberText, maxTextWidth);
|
const textMetrics = ctx.measureText(truncatedText);
|
const textWidth = textMetrics.width;
|
ctx.fillText(truncatedText, w - textWidth - x, h - y); // 右对齐
|
}
|
} else {
|
const maxTextWidth = (w - 3 * x) / 3;
|
|
const printConfigCache = {
|
...this._printConfig,
|
bookPageNumber: this._layoutConfig?.bookPageNumber,
|
bookTotalPage: this._layoutConfig?.bookTotalPage,
|
sheetPageNumber: this._layoutConfig?.sheetPageNumber,
|
sheetTotalPage: this._layoutConfig?.sheetTotalPage,
|
workSheetTitleText: worksheet?.getName(),
|
};
|
|
// 左上角渲染
|
if (headerFooterParams?.includes(JnpfPrintCustomHeaderEnum?.leftTop)) {
|
const text = translateHeaderFooterText(JnpfPrintCustomHeaderEnum.leftTop, printConfigCache);
|
const truncatedText = this._ellipsisText(ctx, text, maxTextWidth);
|
ctx.fillText(truncatedText, x, y);
|
}
|
|
// 中上角渲染
|
if (headerFooterParams?.includes(JnpfPrintCustomHeaderEnum?.centerTop)) {
|
const text = translateHeaderFooterText(JnpfPrintCustomHeaderEnum.centerTop, printConfigCache);
|
const truncatedText = this._ellipsisText(ctx, text, maxTextWidth);
|
const textMetrics = ctx.measureText(truncatedText);
|
const textWidth = textMetrics.width;
|
ctx.fillText(truncatedText, (w - textWidth) / 2, y);
|
}
|
|
// 右上角渲染
|
if (headerFooterParams?.includes(JnpfPrintCustomHeaderEnum?.rightTop)) {
|
const text = translateHeaderFooterText(JnpfPrintCustomHeaderEnum.rightTop, printConfigCache);
|
const truncatedText = this._ellipsisText(ctx, text, maxTextWidth);
|
const textMetrics = ctx.measureText(truncatedText);
|
const textWidth = textMetrics.width;
|
ctx.fillText(truncatedText, w - textWidth - x, y);
|
}
|
|
// 左下角渲染
|
if (headerFooterParams?.includes(JnpfPrintCustomFooterEnum?.leftBottom)) {
|
const text = translateHeaderFooterText(JnpfPrintCustomFooterEnum.leftBottom, printConfigCache);
|
const truncatedText = this._ellipsisText(ctx, text, maxTextWidth);
|
ctx.fillText(truncatedText, x, h - y);
|
}
|
|
// 中下角渲染
|
if (headerFooterParams?.includes(JnpfPrintCustomFooterEnum?.centerBottom)) {
|
const text = translateHeaderFooterText(JnpfPrintCustomFooterEnum.centerBottom, printConfigCache);
|
const truncatedText = this._ellipsisText(ctx, text, maxTextWidth);
|
const textMetrics = ctx.measureText(truncatedText);
|
const textWidth = textMetrics.width;
|
ctx.fillText(truncatedText, (w - textWidth) / 2, h - y);
|
}
|
|
// 右下角渲染
|
if (headerFooterParams?.includes(JnpfPrintCustomFooterEnum?.rightBottom)) {
|
const text = translateHeaderFooterText(JnpfPrintCustomFooterEnum.rightBottom, printConfigCache);
|
const truncatedText = this._ellipsisText(ctx, text, maxTextWidth);
|
const textMetrics = ctx.measureText(truncatedText);
|
const textWidth = textMetrics.width;
|
ctx.fillText(truncatedText, w - textWidth - x, h - y);
|
}
|
}
|
|
ctx.restore(); // 恢复绘图状态
|
}
|
|
/**
|
* 渲染主场景
|
*/
|
private _renderMainScene() {
|
// 如果场景被标记为脏,需要重新调整视口大小
|
this._dirty && this._prepare();
|
|
// 获取Canvas的绘图上下文
|
const context = this._engine.getCanvas().getContext();
|
|
// 获取打印配置中的网格线设置和偏移量
|
const { gridlines } = this._printConfig;
|
const { offsetX, offsetY } = this._getTranslateOffset();
|
|
// 根据网格线设置禁用或启用网格线
|
this._spreadsheetObject.setForceDisableGridlines(!gridlines);
|
|
// 强制标记表格为脏,以便重新渲染
|
this._spreadsheetObject.makeForceDirty();
|
this._scene.makeDirty();
|
|
// 保存当前绘图上下文状态
|
context.save();
|
// 应用偏移量
|
context.translateWithPrecision(offsetX, offsetY);
|
|
// 渲染场景
|
this._scene.render();
|
|
// 恢复绘图上下文状态
|
context.restore();
|
|
// 添加水印(判断下,如果不需要水印就不用配置了)!!!目前和0.6.2版本一致,都是盖在了内容上面
|
if (this._watermarkConfig?.show) {
|
this._drawWatermark(context, this._watermarkConfig?.config);
|
}
|
}
|
|
/**
|
* 调整视口大小和滚动值
|
*/
|
private _resizeViewport() {
|
// 从布局配置中解构冻结和范围设置
|
const { freeze, printScale = 1, range } = this._layoutConfig;
|
// 从冻结设置中解构x轴和y轴的分割点以及起始行列
|
const { startColumn, startRow, xSplit, ySplit } = freeze;
|
// 从骨架配置中解构行头宽度和列头高度
|
const { columnHeaderHeight, rowHeaderWidth } = this._skeleton;
|
|
// 缩放场景以适应预览容器的缩放比例
|
const sceneScale = printScale * this._previewContainerScale;
|
this._scene.scale(sceneScale, sceneScale);
|
|
// 获取无合并单元格的位置,基于起始行列
|
const cellPosition = this._skeleton?.getNoMergeCellPositionByIndexWithNoHeader(startRow, startColumn);
|
// 获取无合并单元格的位置,基于起始行列减去分割点
|
const cellPositionSplit = this._skeleton?.getNoMergeCellPositionByIndexWithNoHeader(startRow - ySplit, startColumn - xSplit);
|
|
// 计算起始X分割位置
|
const startXSplit = xSplit > 0 ? cellPosition.startX - cellPositionSplit.startX : 0;
|
// 计算起始Y分割位置
|
const startYSplit = ySplit > 0 ? cellPosition.startY - cellPositionSplit.startY : 0;
|
|
// 获取场景的精度缩放比例
|
const precisionScale = this._scene.getPrecisionScale();
|
// 定义用于缩放X值的函数
|
const getScaleX = (value: number) => Math.round(value * precisionScale.scaleX) / precisionScale.scaleX;
|
// 定义用于缩放Y值的函数
|
const getScaleY = (value: number) => Math.round(value * precisionScale.scaleY) / precisionScale.scaleY;
|
|
// 获取范围起始单元格的位置
|
const startCellPosition = this._skeleton.getNoMergeCellPositionByIndexWithNoHeader(range.startRow, range.startColumn);
|
// 计算结束单元格的位置,并处理边界情况
|
const endCellPosition = this._skeleton.getNoMergeCellPositionByIndexWithNoHeader(range.endRow, range.endColumn);
|
|
// 定义范围对象,包含起始和结束的X、Y坐标
|
const _range = {
|
endX: xSplit > 0 ? cellPosition.startX : 0,
|
endY: ySplit > 0 ? cellPosition.startY : 0,
|
startX: xSplit > 0 ? cellPositionSplit.startX : 0,
|
startY: ySplit > 0 ? cellPositionSplit.startY : 0,
|
};
|
|
const endX = Math.max(0, getScaleX(endCellPosition.endX) - Math.max(startCellPosition.startX, _range.endX));
|
const endY = Math.max(0, getScaleY(endCellPosition.endY) - getScaleY(Math.max(startCellPosition.startY, _range.endY)));
|
// 计算缩放比例
|
const scale = 1 / Math.max(precisionScale.scaleX, precisionScale.scaleY);
|
|
this._totalWidth = startXSplit + endX + scale;
|
this._totalHeight = startYSplit + endY + scale;
|
|
const startSplit = { x: startXSplit, y: startYSplit };
|
|
const scaleXY = {
|
x: getScaleX(endCellPosition.endX) - getScaleX(endX) - getScaleX(Math.max(startCellPosition.startX, _range.endX)),
|
y: getScaleY(endCellPosition.endY) - getScaleY(endY) - getScaleY(Math.max(startCellPosition.startY, _range.endY)),
|
};
|
|
// 如果x轴和y轴都有分割点,启用左上角视口并调整其大小和滚动值
|
if (xSplit > 0 && ySplit > 0) {
|
this._leftTopViewport.enable();
|
this._leftTopViewport.resizeWhenFreezeChange({
|
height: getScaleY(startSplit.y),
|
left: 0,
|
top: 0,
|
width: getScaleX(startSplit.x),
|
});
|
this._leftTopViewport.updateScrollVal({
|
viewportScrollX: getScaleX(_range.startX) + getScaleX(rowHeaderWidth),
|
viewportScrollY: getScaleY(_range.startY) + getScaleY(columnHeaderHeight),
|
});
|
} else {
|
// 否则禁用左上角视口
|
this._leftTopViewport.disable();
|
}
|
|
// 如果x轴有分割点,启用左视口并调整其大小和滚动值
|
if (xSplit > 0) {
|
this._leftViewport.enable();
|
this._leftViewport.resizeWhenFreezeChange({
|
height: getScaleY(endY) + scale + scaleXY.y,
|
left: 0,
|
top: getScaleY(startSplit.y),
|
width: getScaleX(startSplit.x),
|
});
|
this._leftViewport.updateScrollVal({
|
viewportScrollX: getScaleX(_range.startX) + getScaleX(rowHeaderWidth),
|
viewportScrollY: getScaleY(startCellPosition.startY) + getScaleY(columnHeaderHeight) - getScaleY(startSplit.y),
|
});
|
} else {
|
// 否则禁用左视口
|
this._leftViewport.disable();
|
}
|
|
// 如果y轴有分割点,启用顶视口并调整其大小和滚动值
|
if (ySplit > 0) {
|
this._topViewport.enable();
|
this._topViewport.resizeWhenFreezeChange({
|
height: getScaleY(startSplit.y),
|
left: getScaleX(startSplit.x),
|
top: 0,
|
width: getScaleX(endX) + scale + scaleXY.x,
|
});
|
this._topViewport.updateScrollVal({
|
viewportScrollX: getScaleX(startCellPosition.startX) + getScaleX(rowHeaderWidth) - getScaleX(startSplit.x),
|
viewportScrollY: getScaleY(_range.startY) + getScaleY(columnHeaderHeight),
|
});
|
} else {
|
// 否则禁用顶视口
|
this._topViewport.disable();
|
}
|
|
// 调整主视口的大小和滚动值
|
this._mainViewport.resizeWhenFreezeChange({
|
height: getScaleY(endY) + scale + scaleXY.y,
|
left: getScaleX(startSplit.x),
|
top: getScaleY(startSplit.y),
|
width: getScaleX(endX) + scale + scaleXY.x,
|
});
|
this._mainViewport.updateScrollVal({
|
viewportScrollX: getScaleX(startCellPosition.startX) + getScaleX(rowHeaderWidth) - getScaleX(startSplit.x),
|
viewportScrollY: getScaleY(startCellPosition.startY) + getScaleY(columnHeaderHeight) - getScaleY(startSplit.y),
|
});
|
}
|
}
|