refactor: restore zq admin design baseline

This commit is contained in:
2026-06-22 07:58:57 +08:00
parent 17031cc3c9
commit 0de9c085d5
564 changed files with 154513 additions and 209 deletions
+80
View File
@@ -0,0 +1,80 @@
{
"name": "@zq/univer",
"version": "0.2.0",
"type": "module",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
},
"./style": {
"development": "./src/components/Report/style/index.scss",
"default": "./src/components/Report/style/index.scss"
}
},
"dependencies": {
"@univerjs-pro/engine-pivot": "0.8.3",
"@univerjs-pro/sheets-pivot": "0.8.3",
"@univerjs-pro/sheets-pivot-ui": "0.8.3",
"@univerjs/core": "0.8.3",
"@univerjs/data-validation": "0.8.3",
"@univerjs/design": "0.8.3",
"@univerjs/docs": "0.8.3",
"@univerjs/docs-drawing": "0.8.3",
"@univerjs/docs-drawing-ui": "0.8.3",
"@univerjs/docs-ui": "0.8.3",
"@univerjs/drawing": "0.8.3",
"@univerjs/drawing-ui": "0.8.3",
"@univerjs/engine-formula": "0.8.3",
"@univerjs/engine-render": "0.8.3",
"@univerjs/facade": "0.5.5",
"@univerjs/find-replace": "0.8.3",
"@univerjs/icons": "0.4.4",
"@univerjs/protocol": "0.1.46",
"@univerjs/sheets": "0.8.3",
"@univerjs/sheets-conditional-formatting": "0.8.3",
"@univerjs/sheets-conditional-formatting-ui": "0.8.3",
"@univerjs/sheets-crosshair-highlight": "0.8.3",
"@univerjs/sheets-data-validation": "0.8.3",
"@univerjs/sheets-data-validation-ui": "0.8.3",
"@univerjs/sheets-drawing": "0.8.3",
"@univerjs/sheets-drawing-ui": "0.8.3",
"@univerjs/sheets-filter": "0.8.3",
"@univerjs/sheets-filter-ui": "0.8.3",
"@univerjs/sheets-find-replace": "0.8.3",
"@univerjs/sheets-formula": "0.8.3",
"@univerjs/sheets-formula-ui": "0.8.3",
"@univerjs/sheets-hyper-link": "0.8.3",
"@univerjs/sheets-hyper-link-ui": "0.8.3",
"@univerjs/sheets-numfmt": "0.8.3",
"@univerjs/sheets-numfmt-ui": "0.8.3",
"@univerjs/sheets-sort": "0.8.3",
"@univerjs/sheets-sort-ui": "0.8.3",
"@univerjs/sheets-thread-comment": "0.8.3",
"@univerjs/sheets-thread-comment-ui": "0.8.3",
"@univerjs/sheets-ui": "0.8.3",
"@univerjs/sheets-zen-editor": "0.8.3",
"@univerjs/thread-comment": "0.8.3",
"@univerjs/thread-comment-ui": "0.8.3",
"@univerjs/ui": "0.8.3",
"@univerjs/uniscript": "0.8.3",
"@univerjs/watermark": "0.8.3",
"@vben/locales": "workspace:*",
"element-plus": "catalog:",
"echarts": "catalog:",
"jsbarcode": "^3.12.1",
"lodash-es": "catalog:",
"pinia": "catalog:",
"qrcode": "^1.5.4",
"react": "18.3.1",
"react-dom": "18.3.1",
"rxjs": "^7.8.2",
"vue": "catalog:"
},
"devDependencies": {
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@univerjs/vite-plugin": "0.5.1",
"typescript": "catalog:"
}
}
@@ -0,0 +1,72 @@
import type { ICellData, IWorkbookData } from '@univerjs/core';
import * as echarts from 'echarts';
export interface DesignStateProps {
[prop: string]: any;
activeWorkbook: any;
activeWorkbookId: any;
activeWorksheet: any;
activeWorksheetId: any;
beforeCommandExecuteMonitor: any;
commandExecuteMonitor: any;
configDialogCellTarget: null;
containerEleId: any;
dialogCellInstance: any;
zqUniverAPI: any;
loading: boolean;
openDialogCell: boolean;
selectionChangeMonitor: any;
univer: any;
univerCreateMode: 'design' | 'preview' | 'print';
}
export interface CreateUnitProps {
cellEcharts?: Record<string, any>;
defaultActiveSheetId?: string;
floatEcharts?: Record<string, any>;
floatImages?: Record<string, any>;
loading?: boolean;
mode?: 'design' | 'preview' | 'print';
readonly?: boolean;
snapshot?: IWorkbookData;
uiContextMenu?: boolean;
uiFooter?: boolean;
uiHeader?: boolean;
watermark?: Record<string, any>;
darkMode?: boolean;
/** 应用语言:zh-CN | en-US | zh-TW,用于设计器自定义菜单文案 */
appLocale?: string;
}
export interface DeliverFloatEchartOptionProps {
drawingId: string;
echartType: 'bar' | 'line' | 'pie' | 'radar';
option: echarts.EChartsOption;
}
export interface DeliverFloatImageOptionProps {
drawingId: string;
imageType: 'BASE64' | 'URL';
option: any;
}
export interface DeliverCellDataProps {
cellData: ICellData;
startColumn: number;
startRow: number;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,157 @@
import { ref } from 'vue';
import { defineStore } from 'pinia';
import { getAlphabetFromIndexRule, isNullOrUndefined } from '../univer/utils';
// 初始状态的默认值
const defaultFocusFloatDomData = {
domId: undefined,
drawingId: undefined,
option: {},
};
export const useZqUniverStore = (id: string) => {
return defineStore(id, () => {
const zqUniverApiCache: any = ref(null); // 整个api实例
const univerCreateModeCache: any = ref(null); // univer创建模式
const focusedFloatEchartDataCache = ref({ ...defaultFocusFloatDomData }); // 已聚焦的悬浮图表数据
const floatEchartDataCaches: Record<string, any> = ref({}); // 工作表中所有悬浮图表的数据
const floatEchartToUniverImgDataCaches: Record<string, any> = ref({}); // 工作表中所有悬浮图表转成univer图片后的数据
const focusedFloatImageDataCache = ref({ ...defaultFocusFloatDomData }); // 已聚焦的悬浮图片数据
const floatImageDataCaches: Record<string, any> = ref({}); // 工作表中所有悬浮图片的数据
const floatImageToUniverImgDataCaches: Record<string, any> = ref({}); // 工作表中所有悬浮图片转成univer图片后的数据
const dialogSelectCellDataCache: any = ref(null); // 弹窗选择区域的单元格信息
const dialogSelectCellStateCache: any = ref(null); // 弹窗选择单元格状态 --- 值为时间戳,用于触发页面的状态更新
// 重置存储状态为初始值
const resetStore = () => {
zqUniverApiCache.value = null;
univerCreateModeCache.value = null;
focusedFloatEchartDataCache.value = { ...defaultFocusFloatDomData };
floatEchartDataCaches.value = {};
floatEchartToUniverImgDataCaches.value = {};
focusedFloatImageDataCache.value = { ...defaultFocusFloatDomData };
floatImageDataCaches.value = {};
floatImageToUniverImgDataCaches.value = {};
dialogSelectCellDataCache.value = null;
dialogSelectCellStateCache.value = null;
};
// 存储univer创建模式
const setUniverCreateModeCache = (type: string) => {
univerCreateModeCache.value = type;
};
// 存储zqUniverApi
const setZqUniverApiCache = (value: any) => {
zqUniverApiCache.value = value ?? null;
};
// 根据drawingId获取悬浮图表信息
const getFloatEchartDataByDrawingId = (drawingId: string) => {
return floatEchartDataCaches.value?.[drawingId] ?? null;
};
// 更新已聚焦的悬浮图表数据
const updateFocusedFloatEchartDataCache = (value: any = {}) => {
focusedFloatEchartDataCache.value = value;
};
// 存储工作本中所有悬浮图表的数据
const setFloatEchartDataCaches = (value: any = {}) => {
floatEchartDataCaches.value = value;
zqUniverApiCache.value?.getSheetsFloatDom()?.saveFloatEchartItems(value);
};
// 存储工作本中所有悬浮图表转成univer图片后的数据
const setFloatEchartToUniverImgDataCaches = (value: any = {}) => {
floatEchartToUniverImgDataCaches.value = value;
};
// 根据drawingId获取悬浮图片信息
const getFloatImageDataByDrawingId = (drawingId: string) => {
return floatImageDataCaches.value?.[drawingId] ?? null;
};
// 更新已聚焦的悬浮图片数据
const updateFocusedFloatImageDataCache = (deliverConfig: any = {}) => {
focusedFloatImageDataCache.value = deliverConfig;
};
// 存储工作本中所有悬浮图片的数据
const setFloatImageDataCaches = (value: any = {}) => {
floatImageDataCaches.value = value;
zqUniverApiCache.value?.getSheetsFloatDom()?.saveFloatImageItems(value);
};
// 存储工作本中所有悬浮图片转成univer图片后的数据
const setFloatImageToUniverImgDataCaches = (value: any) => {
floatImageToUniverImgDataCaches.value = value;
};
// 存储弹窗选择单元格的值
const setDialogSelectCellDataCache = (value: any) => {
if (!value || isNullOrUndefined(value?.startRow) || isNullOrUndefined(value?.startColumn)) {
dialogSelectCellDataCache.value = null;
return;
}
dialogSelectCellDataCache.value = `${getAlphabetFromIndexRule(value.startColumn)}${value.startRow + 1}`;
};
// 取消弹窗选择单元格结果
const cancelDialogSelectCell = () => {
setDialogSelectCellDataCache(null);
dialogSelectCellStateCache.value = Date.now();
};
// 确认弹窗选择单元格结果
const confirmDialogSelectCell = () => {
dialogSelectCellStateCache.value = Date.now();
};
return {
cancelDialogSelectCell,
confirmDialogSelectCell,
dialogSelectCellDataCache,
dialogSelectCellStateCache,
floatEchartDataCaches,
floatEchartToUniverImgDataCaches,
floatImageDataCaches,
floatImageToUniverImgDataCaches,
focusedFloatEchartDataCache,
focusedFloatImageDataCache,
getFloatEchartDataByDrawingId,
getFloatImageDataByDrawingId,
resetStore,
setDialogSelectCellDataCache,
setFloatEchartDataCaches,
setFloatEchartToUniverImgDataCaches,
setFloatImageDataCaches,
setFloatImageToUniverImgDataCaches,
setZqUniverApiCache,
setUniverCreateModeCache,
univerCreateModeCache,
updateFocusedFloatEchartDataCache,
updateFocusedFloatImageDataCache,
};
})();
};
@@ -0,0 +1,34 @@
import type { IAccessor, ICommand } from '@univerjs/core';
import { CommandType } from '@univerjs/core';
import { ZqSheetsExcelFileService } from '../../services/sheet-excel-file.service';
import { ZqCommandIds } from '../../utils/define';
export const ZqSheetsImportExcelFileOperation: ICommand = {
handler: async (_: IAccessor) => {
return true;
},
id: ZqCommandIds.importExcelFile,
type: CommandType.OPERATION,
};
export const ZqSheetsDownloadExcelFileOperation: ICommand = {
handler: async (accessor: IAccessor) => {
accessor.get(ZqSheetsExcelFileService)?.handleDownloadExcel();
return true;
},
id: ZqCommandIds.downloadExcelFile,
type: CommandType.OPERATION,
};
export const ZqSheetsImportCsvFileOperation: ICommand = {
handler: async (accessor: IAccessor) => {
const jnpfSheetsExcelFileService = accessor.get(ZqSheetsExcelFileService);
jnpfSheetsExcelFileService?.handleImportCsv();
return true;
},
id: ZqCommandIds.importCsvFile,
type: CommandType.OPERATION,
};
@@ -0,0 +1,66 @@
import type { IAccessor, ICommand } from '@univerjs/core';
import { CommandType } from '@univerjs/core';
import { ZqSheetsFloatEchartService } from '../../services/sheet-float-echart.service';
import { ZqCommandIds } from '../../utils/define';
export const ZqSheetsInsertedFloatEchartOperation: ICommand = {
handler: async (_: IAccessor) => {
return true;
},
id: ZqCommandIds.insertedFloatEchart,
type: CommandType.OPERATION,
};
export const ZqSheetsInsertFloatBarEchartOperation: ICommand = {
handler: async (accessor: IAccessor) => {
const jnpfSheetsFloatEchartService = accessor.get(ZqSheetsFloatEchartService);
jnpfSheetsFloatEchartService.insertFloatEchart('ZqUniverFloatBarEchart');
return true;
},
id: ZqCommandIds.insertFloatBarEchart,
type: CommandType.OPERATION,
};
export const ZqSheetsInsertFloatLineEchartOperation: ICommand = {
handler: async (accessor: IAccessor) => {
const jnpfSheetsFloatEchartService = accessor.get(ZqSheetsFloatEchartService);
jnpfSheetsFloatEchartService.insertFloatEchart('ZqUniverFloatLineEchart');
return true;
},
id: ZqCommandIds.insertFloatLineEchart,
type: CommandType.OPERATION,
};
export const ZqSheetsInsertFloatPieEchartOperation: ICommand = {
handler: async (accessor: IAccessor) => {
const jnpfSheetsFloatEchartService = accessor.get(ZqSheetsFloatEchartService);
jnpfSheetsFloatEchartService.insertFloatEchart('ZqUniverFloatPieEchart');
return true;
},
id: ZqCommandIds.insertFloatPieEchart,
type: CommandType.OPERATION,
};
export const ZqSheetsInsertFloatRadarEchartOperation: ICommand = {
handler: async (accessor: IAccessor) => {
const jnpfSheetsFloatEchartService = accessor.get(ZqSheetsFloatEchartService);
jnpfSheetsFloatEchartService.insertFloatEchart('ZqUniverFloatRadarEchart');
return true;
},
id: ZqCommandIds.insertFloatRadarEchart,
type: CommandType.OPERATION,
};
export const ZqSheetsFocusEchartOperation: ICommand = {
handler: async (_: IAccessor) => {
return true;
},
id: ZqCommandIds.focusFloatEchart,
type: CommandType.OPERATION,
};
@@ -0,0 +1,33 @@
import type { IAccessor, ICommand } from '@univerjs/core';
import { CommandType } from '@univerjs/core';
import { ZqSheetsFloatImageService } from '../../services/sheet-float-image.service';
import { ZqCommandIds } from '../../utils/define';
export const ZqSheetsInsertFloatImageOperation: ICommand = {
handler: async (accessor: IAccessor) => {
const jnpfSheetsFloatImageService = accessor.get(ZqSheetsFloatImageService);
jnpfSheetsFloatImageService.insertFloatImage();
return true;
},
id: ZqCommandIds.insertFloatImage,
type: CommandType.OPERATION,
};
export const ZqSheetsInsertedFloatImageOperation: ICommand = {
handler: async (_: IAccessor) => {
return true;
},
id: ZqCommandIds.insertedFloatImage,
type: CommandType.OPERATION,
};
export const ZqSheetsFocusFloatImageOperation: ICommand = {
handler: async (_: IAccessor) => {
return true;
},
id: ZqCommandIds.focusFloatImage,
type: CommandType.OPERATION,
};
@@ -0,0 +1,13 @@
import type { IAccessor, ICommand } from '@univerjs/core';
import { CommandType } from '@univerjs/core';
import { ZqCommandIds } from '../../utils/define';
export const ZqSheetsPreviewOperation: ICommand = {
handler: async (_: IAccessor) => {
return true;
},
id: ZqCommandIds.preview,
type: CommandType.OPERATION,
};
@@ -0,0 +1,57 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useZqUniverStore } from '../../../store';
defineOptions({ name: 'ZqUniverDialogSelectCell' });
const props = defineProps<PropsType>();
interface PropsType {
value: string;
}
const zqUniverStore = useZqUniverStore(props?.value);
const { dialogSelectCellDataCache } = storeToRefs(zqUniverStore);
// 点击取消
const handleCancel = () => {
zqUniverStore?.cancelDialogSelectCell();
};
// 点击确认
const handleConfirm = () => {
zqUniverStore?.confirmDialogSelectCell();
};
</script>
<template>
<div class="dialog-select-cell-content">
<div>
<div class="univer-mb-2 univer-flex univer-items-center univer-gap-4">
<span
class="univer-relative univer-inline-flex univer-w-full univer-items-center univer-rounded-md univer-w-full univer-border-primary-600"
style="width: 100%">
<input
type="text"
placeholder="请选择"
readonly
class="univer-box-border univer-w-full univer-rounded-md univer-bg-white univer-transition-colors univer-duration-200 placeholder:univer-text-gray-400 focus:univer-border-primary-600 focus:univer-outline-none focus:univer-ring-2 focus:univer-ring-primary-50 dark:!univer-bg-gray-700 dark:!univer-text-white dark:focus:!univer-ring-primary-900 univer-h-8 univer-px-2 univer-text-sm univer-border-gray-200 dark:!univer-border-gray-600 univer-border-solid univer-border"
:value="dialogSelectCellDataCache" />
</span>
</div>
</div>
<footer class="univer-flex univer-gap-2" style="display: flex; justify-content: flex-end; margin-top: 20px">
<button
class="univer-box-border univer-inline-flex univer-cursor-pointer univer-select-none univer-items-center univer-justify-center univer-gap-2 univer-whitespace-nowrap univer-rounded-md univer-border univer-border-solid univer-text-sm univer-font-medium univer-transition-colors disabled:univer-pointer-events-none disabled:univer-cursor-not-allowed disabled:univer-opacity-50 [&_svg]:univer-pointer-events-none [&_svg]:univer-size-4 [&_svg]:univer-shrink-0 univer-border-gray-200 univer-bg-white univer-text-gray-700 hover:univer-bg-gray-100 active:univer-bg-gray-200 dark:!univer-border-gray-600 dark:!univer-bg-gray-700 dark:!univer-text-white dark:hover:!univer-bg-gray-600 dark:active:!univer-bg-gray-700 univer-h-8 univer-rounded-lg univer-px-2 univer-text-sm"
@click="handleCancel">
取消
</button>
<button
class="univer-box-border univer-inline-flex univer-cursor-pointer univer-select-none univer-items-center univer-justify-center univer-gap-2 univer-whitespace-nowrap univer-rounded-md univer-border univer-border-solid univer-text-sm univer-font-medium univer-transition-colors disabled:univer-pointer-events-none disabled:univer-cursor-not-allowed disabled:univer-opacity-50 [&_svg]:univer-pointer-events-none [&_svg]:univer-size-4 [&_svg]:univer-shrink-0 univer-border-primary-600 univer-bg-primary-600 univer-text-white hover:univer-bg-primary-500 active:univer-bg-primary-700 univer-h-8 univer-rounded-lg univer-px-2 univer-text-sm"
:disabled="!dialogSelectCellDataCache"
style="margin-left: 10px"
@click="handleConfirm">
确认
</button>
</footer>
</div>
</template>
@@ -0,0 +1,67 @@
<script lang="ts" setup>
import * as echarts from 'echarts';
defineOptions({ name: 'ZqUniverCellEchart' });
const props = defineProps<PropsType>();
interface PropsType {
univerCreateMode: 'design' | 'preview' | 'print';
}
// 渲染
function render(option: echarts.EChartsOption, width: number, height: number) {
// 创建一个临时容器
const tempContainer = document.createElement('div');
tempContainer.style.width = `${width}px`;
tempContainer.style.height = `${height}px`;
tempContainer.style.position = 'absolute';
tempContainer.style.visibility = 'hidden'; // 避免影响页面视觉
document.body.append(tempContainer);
let chartToImageUrl: null | string = null;
if (props.univerCreateMode !== 'print') {
try {
// 初始化 ECharts 实例
const tempChartInstance = echarts.init(tempContainer);
const { chartType, color, drawingId, height, styleType, type, width, ...restOption } = option;
// 配置图表选项
const updateOption = {
...restOption,
animation: false, // 禁用动画以提高生成效率
};
// 设置图表选项
tempChartInstance.setOption(updateOption);
// 避免失真
tempChartInstance.resize();
// 获取图表的 Base64 图片 URL
chartToImageUrl = tempChartInstance.getDataURL({
backgroundColor: '#f9f9f9',
pixelRatio: 1,
type: 'jpeg',
});
// 销毁图表实例
tempChartInstance.dispose();
} catch (error) {
console.error('图表生成失败:', error);
} finally {
// 移除临时容器
tempContainer.remove();
}
return chartToImageUrl;
}
}
defineExpose({
render,
});
</script>
<template>
<div style="display: none; width: 0; height: 0"></div>
</template>
@@ -0,0 +1,111 @@
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import * as echarts from 'echarts';
import { debounce } from 'lodash-es';
import { storeToRefs } from 'pinia';
import { useZqUniverStore } from '../../../store';
interface Data {
id: string;
piniaStoreId: string;
}
interface PropsType {
data: Data;
}
defineOptions({ name: 'ZqUniverFloatEchart' });
const props = defineProps<PropsType>();
const floatChartContainerRef = ref<HTMLDivElement | null>(null);
const zqUniverStore = useZqUniverStore(props?.data?.piniaStoreId);
const { focusedFloatEchartDataCache } = storeToRefs(zqUniverStore);
let floatChartInstance: echarts.ECharts | null = null;
let resizeObserver: null | ResizeObserver = null;
// 处理窗口大小变化的函数,带300ms 防抖
const handleResize = debounce(() => {
floatChartInstance?.resize();
}, 300);
// 初始化或更新图表
function initialize(option: echarts.EChartsOption) {
if (!floatChartContainerRef.value) {
return;
}
if (!floatChartInstance) {
floatChartInstance = echarts?.init(floatChartContainerRef.value);
}
floatChartInstance?.clear();
// 使用传入的 options 配置图表
floatChartInstance?.setOption(option);
// 设置 ResizeObserver 监听容器大小变化
if (!resizeObserver) {
resizeObserver = new ResizeObserver(handleResize);
resizeObserver?.observe(floatChartContainerRef.value);
}
}
onMounted(() => {
const { id: domId, piniaStoreId } = props.data ?? {};
if (!floatChartContainerRef.value || !domId || !piniaStoreId) {
return;
}
const floatEchartDataCaches = zqUniverStore?.floatEchartDataCaches ?? {};
const targetOption = Object.values(floatEchartDataCaches as Record<string, { domId: string; option: any }>)?.find(item => item?.domId === domId)?.option;
initialize(targetOption);
});
// 销毁
onBeforeUnmount(() => {
if (resizeObserver && floatChartContainerRef.value) {
resizeObserver?.unobserve(floatChartContainerRef.value);
}
resizeObserver?.disconnect();
floatChartInstance?.dispose();
});
watch(
() => focusedFloatEchartDataCache.value,
value => {
const { domId, drawingId, option } = value;
if (!domId && !drawingId) {
// store置空的情况下,不响应
return;
}
const containerId = floatChartContainerRef.value?.getAttribute('id');
if (domId !== containerId) {
return;
}
initialize(option);
},
);
</script>
<template>
<div ref="floatChartContainerRef" :id="data.id" class="zq-univer-float-echart-ele"></div>
</template>
<style lang="scss" scoped>
.zq-univer-float-echart-ele {
position: absolute;
width: 100%;
height: 100%;
background: #f9f9f9;
}
</style>
@@ -0,0 +1,67 @@
<script lang="ts" setup>
import * as echarts from 'echarts';
defineOptions({ name: 'ZqUniverFloatEchartVM' });
const props = defineProps<PropsType>();
interface PropsType {
univerCreateMode: 'design' | 'preview' | 'print';
}
// 渲染
function render(domId: string, option: echarts.EChartsOption, width: number, height: number) {
// 创建一个临时容器
const tempContainer = document.createElement('div');
tempContainer.style.width = `${width}px`;
tempContainer.style.height = `${height}px`;
tempContainer.style.position = 'absolute';
tempContainer.style.visibility = 'hidden'; // 避免影响页面视觉
document.body.append(tempContainer);
let chartToImageUrl: null | string = null;
if (props.univerCreateMode === 'preview') {
try {
// 初始化 ECharts 实例
const tempChartInstance = echarts.init(tempContainer);
// 配置图表选项
const updateOption = {
...option,
animation: false, // 禁用动画以提高生成效率
};
const { color, ...restOption } = updateOption;
// 设置图表选项
tempChartInstance.setOption(restOption);
// 获取图表的 Base64 图片 URL
chartToImageUrl = tempChartInstance.getDataURL({
backgroundColor: '#f9f9f9',
type: 'jpeg',
});
// 销毁图表实例
tempChartInstance.dispose();
} catch (error) {
console.error('图表生成失败:', error);
} finally {
// 移除临时容器
tempContainer.remove();
}
return {
domId,
source: chartToImageUrl,
};
}
}
defineExpose({
render,
});
</script>
<template>
<div style="display: none; width: 0; height: 0"></div>
</template>
@@ -0,0 +1,107 @@
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useZqUniverStore } from '../../../store';
interface Data {
id: string;
piniaStoreId: string;
}
interface PropsType {
data: Data;
}
defineOptions({ name: 'ZqUniverFloatImage' });
const props = defineProps<PropsType>();
const floatImageContainerRef = ref<HTMLDivElement | null>(null);
const zqUniverStore = useZqUniverStore(props?.data?.piniaStoreId);
const { floatImageToUniverImgDataCaches, focusedFloatImageDataCache, univerCreateModeCache } = storeToRefs(zqUniverStore);
// 初始化或更新图表
function initialize(option: any) {
if (!floatImageContainerRef.value) {
return;
}
const imageEle = floatImageContainerRef.value?.querySelector('#imageEle') as HTMLImageElement | null;
if (!imageEle) {
return;
}
const { alt = '', src = '' } = option ?? {};
imageEle.setAttribute('src', src);
imageEle.setAttribute('alt', alt);
// 非设计状态下,图片直接转成univer图片
if (univerCreateModeCache.value !== 'design') {
const { id: domId } = props.data ?? {};
if (domId) {
const caches = {
...floatImageToUniverImgDataCaches.value,
[domId]: src,
};
zqUniverStore?.setFloatImageToUniverImgDataCaches(caches);
}
}
}
onMounted(() => {
const { id: domId, piniaStoreId } = props.data ?? {};
if (!floatImageContainerRef.value || !domId || !piniaStoreId) {
return;
}
const floatImageDataCaches = zqUniverStore?.floatImageDataCaches ?? {};
const targetOption = Object.values(floatImageDataCaches as Record<string, { domId: string; option: any }>)?.find(item => item?.domId === domId)?.option;
initialize(targetOption);
});
watch(
() => focusedFloatImageDataCache.value,
value => {
const { domId, drawingId, option } = value;
if (!domId && !drawingId) {
// store置空的情况下,不响应
return;
}
const containerId = floatImageContainerRef.value?.getAttribute('id');
if (domId !== containerId) {
return;
}
initialize(option);
},
);
</script>
<template>
<div ref="floatImageContainerRef" :id="data.id" class="zq-univer-float-image-ele">
<img id="imageEle" src="" alt="" />
</div>
</template>
<style lang="scss" scoped>
.zq-univer-float-image-ele {
position: absolute;
width: 100%;
height: 100%;
background: #fff;
img {
display: block;
width: 100%;
height: 100%;
}
}
</style>
@@ -0,0 +1,48 @@
import type { IMenuButtonItem, IMenuSelectorItem } from '@univerjs/ui';
import { MenuItemType } from '@univerjs/ui';
import {
ZqSheetsDownloadExcelFileOperation,
ZqSheetsImportCsvFileOperation,
ZqSheetsImportExcelFileOperation,
} from '../../commands/operations/sheet-excel-file.operation';
import { ZqCommandIds } from '../../utils/define';
export const ZqSheetsExcelFileMenuFactory = (): IMenuSelectorItem => {
return {
icon: 'DirectExportSingle',
id: ZqCommandIds.excelFileOperations,
tooltip: 'jnpfSheetExcelFileMenu.tooltip',
type: MenuItemType.SUBITEMS,
};
};
export const ZqSheetsImportExcelFileMenuFactory = (): IMenuButtonItem => {
return {
icon: 'UploadSingle',
id: ZqSheetsImportExcelFileOperation.id,
title: 'jnpfSheetImportExcelFileMenu.title',
tooltip: 'jnpfSheetImportExcelFileMenu.tooltip',
type: MenuItemType.BUTTON,
};
};
export const ZqSheetsDownloadExcelFileMenuFactory = (): IMenuButtonItem => {
return {
icon: 'DirectExportSingle',
id: ZqSheetsDownloadExcelFileOperation.id,
title: 'jnpfSheetDownloadExcelFileMenu.title',
tooltip: 'jnpfSheetDownloadExcelFileMenu.tooltip',
type: MenuItemType.BUTTON,
};
};
export const ZqSheetsImportCsvFileMenuFactory = (): IMenuButtonItem => {
return {
icon: 'UpperFloorSingle',
id: ZqSheetsImportCsvFileOperation.id,
title: 'jnpfSheetImportCsvFileMenu.title',
tooltip: 'jnpfSheetImportCsvFileMenu.tooltip',
type: MenuItemType.BUTTON,
};
};
@@ -0,0 +1,59 @@
import type { IMenuButtonItem, IMenuSelectorItem } from '@univerjs/ui';
import { MenuItemType } from '@univerjs/ui';
import {
ZqSheetsInsertFloatBarEchartOperation,
ZqSheetsInsertFloatLineEchartOperation,
ZqSheetsInsertFloatPieEchartOperation,
ZqSheetsInsertFloatRadarEchartOperation,
} from '../../commands/operations/sheet-float-echart.operation';
import { ZqCommandIds } from '../../utils/define';
export const ZqSheetsFloatEchartMenuFactory = (): IMenuSelectorItem => {
return {
icon: 'SystemSingle',
id: ZqCommandIds.floatEchartOperations,
tooltip: 'jnpfSheetFloatEchartMenu.tooltip',
type: MenuItemType.SUBITEMS,
};
};
export const ZqSheetsInsertFloatBarEchartMenuFactory = (): IMenuButtonItem => {
return {
icon: 'ChartSingle',
id: ZqSheetsInsertFloatBarEchartOperation.id,
title: 'jnpfSheetInsertFloatBarEchartMenu.title',
tooltip: 'jnpfSheetInsertFloatBarEchartMenu.tooltip',
type: MenuItemType.BUTTON,
};
};
export const ZqSheetsInsertFloatLineEchartMenuFactory = (): IMenuButtonItem => {
return {
icon: 'LineChartSingle',
id: ZqSheetsInsertFloatLineEchartOperation.id,
title: 'jnpfSheetInsertFloatLineEchartMenu.title',
tooltip: 'jnpfSheetInsertFloatLineEchartMenu.tooltip',
type: MenuItemType.BUTTON,
};
};
export const ZqSheetsInsertFloatPieEchartMenuFactory = (): IMenuButtonItem => {
return {
icon: 'PieChartSingle',
id: ZqSheetsInsertFloatPieEchartOperation.id,
title: 'jnpfSheetInsertFloatPieEchartMenu.title',
tooltip: 'jnpfSheetInsertFloatPieEchartMenu.tooltip',
type: MenuItemType.BUTTON,
};
};
export const ZqSheetsInsertFloatRadarEchartMenuFactory = (): IMenuButtonItem => {
return {
icon: 'RadarChartSingle',
id: ZqSheetsInsertFloatRadarEchartOperation.id,
title: 'jnpfSheetInsertFloatRadarEchartMenu.title',
tooltip: 'jnpfSheetInsertFloatRadarEchartMenu.tooltip',
type: MenuItemType.BUTTON,
};
};
@@ -0,0 +1,13 @@
import type { IMenuButtonItem } from '@univerjs/ui';
import { MenuItemType } from '@univerjs/ui';
import { ZqSheetsInsertFloatImageOperation } from '../../commands/operations/sheet-float-image.operation';
export const ZqSheetsInsertFloatImageMenuFactory = (): IMenuButtonItem => {
return {
id: ZqSheetsInsertFloatImageOperation.id,
title: 'jnpfSheetInsertFloatImageMenu.title',
tooltip: 'jnpfSheetInsertFloatImageMenu.tooltip',
type: MenuItemType.BUTTON,
};
};
@@ -0,0 +1,14 @@
import type { IMenuButtonItem } from '@univerjs/ui';
import { MenuItemType } from '@univerjs/ui';
import { ZqSheetsPreviewOperation } from '../../commands/operations/sheet-preview.operation';
export const ZqSheetsPreviewMenuFactory = (): IMenuButtonItem => {
return {
icon: 'ViewModeSingle',
id: ZqSheetsPreviewOperation.id,
title: 'jnpfSheetPreviewMenu.title',
tooltip: 'jnpfSheetPreviewMenu.tooltip',
type: MenuItemType.BUTTON,
};
};
@@ -0,0 +1,18 @@
import { Disposable, Inject } from '@univerjs/core';
import { ComponentManager } from '@univerjs/ui';
import univerDialogSelectCellComponent from '../components/Dialog/selectCell.vue';
import { ZqUniverDialogSelectCellKey } from '../utils/define';
export class ZqSheetsDialogController extends Disposable {
constructor(@Inject(ComponentManager) private readonly _componentManager: ComponentManager) {
super();
this._registerComponents();
}
private _registerComponents(): void {
// 注册选区组件进来
this.disposeWithMe(this._componentManager.register(ZqUniverDialogSelectCellKey, univerDialogSelectCellComponent, { framework: 'vue3' }));
}
}
@@ -0,0 +1,48 @@
import { Disposable, ICommandService, Inject } from '@univerjs/core';
import { UploadSingle, UpperFloorSingle } from '@univerjs/icons';
import { ComponentManager, IMenuManagerService, RibbonStartGroup } from '@univerjs/ui';
import {
ZqSheetsDownloadExcelFileOperation,
ZqSheetsImportCsvFileOperation,
ZqSheetsImportExcelFileOperation,
} from '../commands/operations/sheet-excel-file.operation';
import { ZqSheetsImportExcelFileMenuFactory } from '../controllers/menus/sheet-excel-file.menu';
export class ZqSheetsExcelFileController extends Disposable {
constructor(
@ICommandService private readonly _commandService: ICommandService,
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
) {
super();
this._initCommands();
this._registerComponents();
this._initMenus();
}
private _initCommands(): void {
[ZqSheetsImportExcelFileOperation, ZqSheetsDownloadExcelFileOperation, ZqSheetsImportCsvFileOperation].forEach(command => {
this.disposeWithMe(this._commandService.registerCommand(command));
});
}
private _initMenus(): void {
this._menuManagerService.mergeMenu({
[RibbonStartGroup.OTHERS]: {
[ZqSheetsImportExcelFileOperation.id]: {
menuItemFactory: ZqSheetsImportExcelFileMenuFactory,
order: 101,
},
},
});
}
private _registerComponents(): void {
// 注册按钮图标
this.disposeWithMe(this._componentManager.register('UploadSingle', UploadSingle));
// this.disposeWithMe(this._componentManager.register('ExportSingle', ExportSingle));
this.disposeWithMe(this._componentManager.register('UpperFloorSingle', UpperFloorSingle));
}
}
@@ -0,0 +1,88 @@
import { Disposable, ICommandService, Inject } from '@univerjs/core';
import { ChartSingle, LineChartSingle, PieChartSingle, RadarChartSingle, SystemSingle } from '@univerjs/icons';
import { ComponentManager, IMenuManagerService, RibbonInsertGroup } from '@univerjs/ui';
import {
ZqSheetsFocusEchartOperation,
ZqSheetsInsertedFloatEchartOperation,
ZqSheetsInsertFloatBarEchartOperation,
ZqSheetsInsertFloatLineEchartOperation,
ZqSheetsInsertFloatPieEchartOperation,
ZqSheetsInsertFloatRadarEchartOperation,
} from '../commands/operations/sheet-float-echart.operation';
import univerFloatEchartComponent from '../components/Echart/float.vue';
import { ZqCommandIds, ZqUniverFloatEchartKey } from '../utils/define';
import {
ZqSheetsFloatEchartMenuFactory,
ZqSheetsInsertFloatBarEchartMenuFactory,
ZqSheetsInsertFloatLineEchartMenuFactory,
ZqSheetsInsertFloatPieEchartMenuFactory,
ZqSheetsInsertFloatRadarEchartMenuFactory,
} from './menus/sheet-float-echart.menu';
export class ZqSheetsFloatEchartController extends Disposable {
constructor(
@ICommandService private readonly _commandService: ICommandService,
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
) {
super();
this._initCommands();
this._registerComponents();
this._initMenus();
}
private _initCommands(): void {
[
ZqSheetsInsertFloatBarEchartOperation,
ZqSheetsInsertFloatLineEchartOperation,
ZqSheetsInsertFloatPieEchartOperation,
ZqSheetsInsertFloatRadarEchartOperation,
ZqSheetsInsertedFloatEchartOperation,
ZqSheetsFocusEchartOperation,
].forEach(command => {
this.disposeWithMe(this._commandService.registerCommand(command));
});
}
private _initMenus(): void {
this._menuManagerService.mergeMenu({
// 主菜单
[RibbonInsertGroup.MEDIA]: {
[ZqCommandIds.floatEchartOperations]: {
[ZqSheetsInsertFloatBarEchartOperation.id]: {
menuItemFactory: ZqSheetsInsertFloatBarEchartMenuFactory,
order: 0,
},
[ZqSheetsInsertFloatLineEchartOperation.id]: {
menuItemFactory: ZqSheetsInsertFloatLineEchartMenuFactory,
order: 1,
},
[ZqSheetsInsertFloatPieEchartOperation.id]: {
menuItemFactory: ZqSheetsInsertFloatPieEchartMenuFactory,
order: 2,
},
[ZqSheetsInsertFloatRadarEchartOperation.id]: {
menuItemFactory: ZqSheetsInsertFloatRadarEchartMenuFactory,
order: 3,
},
menuItemFactory: ZqSheetsFloatEchartMenuFactory,
order: 1,
},
},
});
}
private _registerComponents(): void {
// 注册图表组件进来
this.disposeWithMe(this._componentManager.register(ZqUniverFloatEchartKey, univerFloatEchartComponent, { framework: 'vue3' }));
// 注册按钮图标
this.disposeWithMe(this._componentManager.register('SystemSingle', SystemSingle));
this.disposeWithMe(this._componentManager.register('ChartSingle', ChartSingle));
this.disposeWithMe(this._componentManager.register('LineChartSingle', LineChartSingle));
this.disposeWithMe(this._componentManager.register('PieChartSingle', PieChartSingle));
this.disposeWithMe(this._componentManager.register('RadarChartSingle', RadarChartSingle));
}
}
@@ -0,0 +1,50 @@
import { Disposable, ICommandService, Inject } from '@univerjs/core';
import { SHEETS_IMAGE_MENU_ID } from '@univerjs/sheets-drawing-ui';
import { ComponentManager, IMenuManagerService, RibbonInsertGroup } from '@univerjs/ui';
import {
ZqSheetsFocusFloatImageOperation,
ZqSheetsInsertedFloatImageOperation,
ZqSheetsInsertFloatImageOperation,
} from '../commands/operations/sheet-float-image.operation';
import univerFloatImageComponent from '../components/Image/float.vue';
import { ZqUniverFloatImageKey } from '../utils/define';
import { ZqSheetsInsertFloatImageMenuFactory } from './menus/sheet-float-image.menu';
export class ZqSheetsFloatImageController extends Disposable {
constructor(
@ICommandService private readonly _commandService: ICommandService,
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
) {
super();
this._initCommands();
this._registerComponents();
this._initMenus();
}
private _initCommands(): void {
[ZqSheetsInsertFloatImageOperation, ZqSheetsInsertedFloatImageOperation, ZqSheetsFocusFloatImageOperation].forEach(command => {
this.disposeWithMe(this._commandService.registerCommand(command));
});
}
private _initMenus(): void {
this._menuManagerService.mergeMenu({
[RibbonInsertGroup.MEDIA]: {
[SHEETS_IMAGE_MENU_ID]: {
[ZqSheetsInsertFloatImageOperation.id]: {
menuItemFactory: ZqSheetsInsertFloatImageMenuFactory,
order: 10,
},
},
},
});
}
private _registerComponents(): void {
// 注册图片组件进来
this.disposeWithMe(this._componentManager.register(ZqUniverFloatImageKey, univerFloatImageComponent, { framework: 'vue3' }));
}
}
@@ -0,0 +1,52 @@
import { Disposable, ICommandService, Inject } from '@univerjs/core';
import { ViewModeSingle } from '@univerjs/icons';
import { ComponentManager, IMenuManagerService, RibbonStartGroup } from '@univerjs/ui';
import { ZqSheetsPreviewOperation } from '../commands/operations/sheet-preview.operation';
import { ZqSheetsPreviewMenuFactory } from '../controllers/menus/sheet-preview.menu';
export class ZqSheetsPreviewController extends Disposable {
constructor(
@ICommandService private readonly _commandService: ICommandService,
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
) {
super();
this._initCommands();
this._registerComponents();
this._initMenus();
}
private _initCommands(): void {
[ZqSheetsPreviewOperation].forEach(command => {
this.disposeWithMe(this._commandService.registerCommand(command));
});
}
private _initMenus(): void {
this._menuManagerService.mergeMenu({
// 主菜单
[RibbonStartGroup.OTHERS]: {
[ZqSheetsPreviewOperation.id]: {
menuItemFactory: ZqSheetsPreviewMenuFactory,
order: 99,
},
},
// 右键菜单
// [ContextMenuPosition.MAIN_AREA]: {
// [ContextMenuGroup.OTHERS]: {
// [ZqSheetsPreviewOperation.id]: {
// order: 0,
// menuItemFactory: ZqSheetsPreviewMenuFactory,
// },
// },
// },
});
}
private _registerComponents(): void {
// 注册按钮图标
this.disposeWithMe(this._componentManager.register('ViewModeSingle', ViewModeSingle));
}
}
@@ -0,0 +1,179 @@
import type { IScale } from '@univerjs/core';
import type { SpreadsheetSkeleton, UniverRenderingContext } from '@univerjs/engine-render';
import { DEFAULT_FONTFACE_PLANE, FIX_ONE_PIXEL_BLUR_OFFSET, getColor, SheetExtension } from '@univerjs/engine-render';
type CellCoordinates = Array<{ col: number; row: number }>; // 单元格坐标数组
interface ClearSheetFillDirectionProps {
key: string;
sheetId: string;
}
interface SetSheetFillDirectionProps extends ClearSheetFillDirectionProps {
downArrowCells?: CellCoordinates;
rightArrowCells?: CellCoordinates;
}
interface RenderArrowsProps {
arrow: string;
cells: CellCoordinates;
colWidths: number[];
ctx: UniverRenderingContext;
endCol: number;
endRow: number;
offsets: { xOffset: number; yOffset: number };
rowHeights: number[];
startCol: number;
startRow: number;
}
// 定义主要扩展类,用于在表格中绘制自定义内容
export class SetSheetFillDirectionExtension extends SheetExtension {
override uKey: string;
// 设置扩展的层级(z-index),必须大于 50
override get zIndex() {
return 50;
}
private readonly _downArrowCells: CellCoordinates; // 渲染 ↓ 的条件数组
private readonly _rightArrowCells: CellCoordinates; // 渲染 → 的条件数组
private readonly _sheetId: string; // 用于存储动态传入的 SheetId
constructor(props: SetSheetFillDirectionProps) {
super();
const { downArrowCells, key, rightArrowCells, sheetId } = props ?? {};
this.uKey = key;
this._sheetId = sheetId;
this._rightArrowCells = rightArrowCells ?? [];
this._downArrowCells = downArrowCells ?? [];
}
// 重写绘制方法
override draw(ctx: UniverRenderingContext, _parentScale: IScale, skeleton: SpreadsheetSkeleton) {
if (!this.isValidSkeleton(skeleton)) {
return;
}
if (!this._rightArrowCells?.length && !this._downArrowCells?.length) {
return;
}
const { columnTotalWidth, columnWidthAccumulation, rowColumnSegment, rowHeightAccumulation, rowTotalHeight } = skeleton;
const { endColumn, endRow, startColumn, startRow } = rowColumnSegment;
if (!rowHeightAccumulation || !columnWidthAccumulation || columnTotalWidth === undefined || rowTotalHeight === undefined) {
return;
}
this.setupCanvas(ctx);
// 绘制 → 箭头
this.renderArrows({
arrow: '→',
cells: this._rightArrowCells,
colWidths: columnWidthAccumulation,
ctx,
endCol: endColumn,
endRow,
offsets: { xOffset: 2, yOffset: -2 },
rowHeights: rowHeightAccumulation,
startCol: startColumn,
startRow,
});
// 绘制 ↓ 箭头
this.renderArrows({
arrow: '↓',
cells: this._downArrowCells,
colWidths: columnWidthAccumulation,
ctx,
endCol: endColumn,
endRow,
offsets: { xOffset: 0, yOffset: 1 },
rowHeights: rowHeightAccumulation,
startCol: startColumn,
startRow,
});
// 绘制分隔线
ctx.stroke();
}
// 验证骨架和必要属性是否有效
private isValidSkeleton(skeleton: SpreadsheetSkeleton): boolean {
// 如果骨架信息不存在,则退出绘制
if (!skeleton) {
return false;
}
// 限制扩展仅在指定的工作表上生效
return skeleton?.worksheet?.getSheetId() === this._sheetId;
}
// 渲染箭头
private renderArrows(props: RenderArrowsProps) {
const {
arrow,
cells,
colWidths,
ctx,
endCol,
endRow,
offsets = { xOffset: 0, yOffset: 0 }, // 默认偏移量
rowHeights,
startCol,
startRow,
} = props ?? {};
for (const { col, row } of cells) {
if (row >= startRow - 1 && row <= endRow && col >= startCol - 1 && col <= endCol) {
const rowStart = rowHeights[row - 1] || 0;
const colStart = colWidths[col - 1] || 0;
ctx.fillText(arrow, colStart + offsets.xOffset, rowStart + offsets.yOffset);
}
}
}
// 设置 Canvas 的绘图属性
private setupCanvas(ctx: UniverRenderingContext) {
// 设置背景色为浅灰色
ctx.fillStyle = getColor([255, 255, 255]);
// 设置文本水平对齐方式
ctx.textAlign = 'left';
// 设置文本垂直对齐方式
ctx.textBaseline = 'top';
// 设置文本颜色为黑色
ctx.fillStyle = getColor([0, 0, 0])!;
// 开始绘制路径
ctx.beginPath();
// 设置线宽为 1
ctx.lineWidth = 1;
// 防止边界模糊的偏移值,使绘制的边界在高分辨率屏幕下更清晰
ctx.translateWithPrecisionRatio(FIX_ONE_PIXEL_BLUR_OFFSET, FIX_ONE_PIXEL_BLUR_OFFSET);
// 设置分隔线颜色为浅灰色
ctx.strokeStyle = getColor([217, 217, 217]);
// 设置字体样式
ctx.font = `10px ${DEFAULT_FONTFACE_PLANE}`;
}
}
// 定义清理扩展类,用于覆盖并清除扩展效果
export class ClearSheetFillDirectionExtension extends SheetExtension {
override uKey: string;
private readonly _sheetId: string; // 用于存储动态传入的 SheetId
constructor(props: ClearSheetFillDirectionProps) {
super();
const { key, sheetId } = props ?? {};
this.uKey = key;
this._sheetId = sheetId; // 将传入的 SheetId 存储到类中
}
// 空的绘制方法,不进行任何绘制操作
override draw(_ctx: UniverRenderingContext, _parentScale: IScale, skeleton: SpreadsheetSkeleton) {
if (skeleton?.worksheet?.getSheetId() !== this._sheetId) {
}
}
}
@@ -0,0 +1,183 @@
import type { IScale } from '@univerjs/core';
import type { SpreadsheetSkeleton, UniverRenderingContext } from '@univerjs/engine-render';
import {
DEFAULT_FONTFACE_PLANE,
FIX_ONE_PIXEL_BLUR_OFFSET,
getColor,
SheetExtension,
} from '@univerjs/engine-render';
type CellCoordinates = Array<{ col: number; row: number }>; // 单元格坐标数组
interface ClearSheetRelationCellProps {
key: string;
sheetId: string;
}
interface SetSheetRelationCellProps extends ClearSheetRelationCellProps {
downArrowCells?: CellCoordinates;
rightArrowCells?: CellCoordinates;
}
interface RenderArrowsProps {
arrow: string;
cells: CellCoordinates;
colWidths: number[];
ctx: UniverRenderingContext;
endCol: number;
endRow: number;
offsetsX?: number;
offsetsY?: number;
rowHeights: number[];
startCol: number;
startRow: number;
}
// 定义主要扩展类,用于在表格中绘制自定义内容
export class SetSheetRelationCellExtension extends SheetExtension {
override uKey: string;
// 设置扩展的层级(z-index),必须大于 50
override get zIndex() {
return 50;
}
private readonly _downArrowCells: CellCoordinates; // 渲染 ↓ 的条件数组
private readonly _rightArrowCells: CellCoordinates; // 渲染 → 的条件数组
private readonly _sheetId: string; // 用于存储动态传入的 SheetId
constructor(props: SetSheetRelationCellProps) {
super();
const { downArrowCells, key, rightArrowCells, sheetId } = props ?? {};
this.uKey = key;
this._sheetId = sheetId;
this._rightArrowCells = rightArrowCells ?? [];
this._downArrowCells = downArrowCells ?? [];
}
// 重写绘制方法
override draw(ctx: UniverRenderingContext, _parentScale: IScale, skeleton: SpreadsheetSkeleton) {
if (!this.isValidSkeleton(skeleton)) {
return;
}
if (!this._rightArrowCells?.length && !this._downArrowCells?.length) {
return;
}
const { columnTotalWidth, columnWidthAccumulation, rowColumnSegment, rowHeightAccumulation, rowTotalHeight } = skeleton;
const { endColumn, endRow, startColumn, startRow } = rowColumnSegment;
if (!rowHeightAccumulation || !columnWidthAccumulation || columnTotalWidth === undefined || rowTotalHeight === undefined) {
return;
}
this.setupCanvas(ctx);
// 绘制 → 箭头
this.renderArrows({
arrow: '→',
cells: this._rightArrowCells,
colWidths: columnWidthAccumulation,
ctx,
endCol: endColumn,
endRow,
offsetsX: undefined,
offsetsY: -2,
rowHeights: rowHeightAccumulation,
startCol: startColumn,
startRow,
});
// 绘制 ↓ 箭头
this.renderArrows({
arrow: '↓',
cells: this._downArrowCells,
colWidths: columnWidthAccumulation,
ctx,
endCol: endColumn,
endRow,
offsetsX: 0,
offsetsY: 12,
rowHeights: rowHeightAccumulation,
startCol: startColumn,
startRow,
});
// 绘制分隔线
ctx.stroke();
}
// 验证骨架和必要属性是否有效
private isValidSkeleton(skeleton: SpreadsheetSkeleton): boolean {
// 如果骨架信息不存在,则退出绘制
if (!skeleton) {
return false;
}
// 限制扩展仅在指定的工作表上生效
return skeleton?.worksheet?.getSheetId() === this._sheetId;
}
// 渲染箭头
private renderArrows(props: RenderArrowsProps) {
const { arrow, cells, colWidths, ctx, endCol, endRow, offsetsX, offsetsY = 0, rowHeights, startCol, startRow } = props ?? {};
for (const { col, row } of cells) {
if (row >= startRow - 1 && row <= endRow && col >= startCol - 1 && col <= endCol) {
const rowStart = rowHeights[row - 1] || 0;
const colStart = colWidths[col - 1] || 0;
const colEnd = colWidths[col] || colWidths[col - 1] || 0;
const cellWidth = colEnd - colStart;
const xPosition = offsetsX === undefined ? colStart + cellWidth / 2 - 6 : colStart + offsetsX;
const yPosition = rowStart + offsetsY;
ctx.fillText(arrow, xPosition, yPosition);
}
}
}
// 设置 Canvas 的绘图属性
private setupCanvas(ctx: UniverRenderingContext) {
// 设置背景色为浅灰色
ctx.fillStyle = getColor([255, 255, 255]);
// 设置文本水平对齐方式
ctx.textAlign = 'left';
// 设置文本垂直对齐方式
ctx.textBaseline = 'top';
// 设置文本颜色为黑色
ctx.fillStyle = getColor([24, 144, 255])!;
// 开始绘制路径
ctx.beginPath();
// 设置线宽为 1
ctx.lineWidth = 1;
// 防止边界模糊的偏移值,使绘制的边界在高分辨率屏幕下更清晰
ctx.translateWithPrecisionRatio(FIX_ONE_PIXEL_BLUR_OFFSET, FIX_ONE_PIXEL_BLUR_OFFSET);
// 设置分隔线颜色为浅灰色
ctx.strokeStyle = getColor([217, 217, 217]);
// 设置字体样式
ctx.font = `10px ${DEFAULT_FONTFACE_PLANE}`;
}
}
// 定义清理扩展类,用于覆盖并清除扩展效果
export class ClearSheetRelationCellExtension extends SheetExtension {
override uKey: string;
private readonly _sheetId: string; // 用于存储动态传入的 SheetId
constructor(props: ClearSheetRelationCellProps) {
super();
const { key, sheetId } = props ?? {};
this.uKey = key;
this._sheetId = sheetId; // 将传入的 SheetId 存储到类中
}
// 空的绘制方法,不进行任何绘制操作
override draw(_ctx: UniverRenderingContext, _parentScale: IScale, skeleton: SpreadsheetSkeleton) {
if (skeleton?.worksheet?.getSheetId() !== this._sheetId) {
}
}
}
@@ -0,0 +1,9 @@
import { ZqSheetsCellImageService } from '../services/sheet-cell-image.service';
export class JnpfFacadeSheetsCellImage {
constructor(private readonly _jnpfSheetsCellImageService: ZqSheetsCellImageService) {}
insertCellImage(file: File, row: number, col: number, cellData: any) {
return this._jnpfSheetsCellImageService?.insertCellImage(file, row, col, cellData);
}
}
@@ -0,0 +1,31 @@
import type { ICellData } from '@univerjs/core';
import { ZqSheetsCellService } from '../services/sheet-cell.service';
export class JnpfFacadeSheetsCell {
constructor(private readonly _jnpfSheetsCellService: ZqSheetsCellService) {}
getAllCellData() {
return this._jnpfSheetsCellService?.getAllCellData();
}
getCellData(col: number, row: number) {
return this._jnpfSheetsCellService?.getCellData(col, row);
}
getCurrentSheetCellsCustom() {
return this._jnpfSheetsCellService?.getCurrentSheetCellsCustom();
}
getTargetCellSize(col: number, row: number) {
return this._jnpfSheetsCellService?.getTargetCellSize(col, row);
}
refreshRangeCellsView(range: any) {
this._jnpfSheetsCellService?.refreshRangeCellsView(range);
}
setCellData(col: number, row: number, cellData: ICellData) {
this._jnpfSheetsCellService?.setCellData(col, row, cellData);
}
}
@@ -0,0 +1,17 @@
import { ZqSheetsFloatDomService } from '../services/sheet-float-dom.service';
export class JnpfFacadeSheetsFloatDom {
constructor(private readonly _jnpfSheetsFloatDomService: ZqSheetsFloatDomService) {}
saveFloatEchartItems(data: any) {
this._jnpfSheetsFloatDomService.saveFloatEchartItems(data);
}
saveFloatImageItems(data: any) {
this._jnpfSheetsFloatDomService.saveFloatImageItems(data);
}
savePiniaStoreId(value: any) {
this._jnpfSheetsFloatDomService.savePiniaStoreId(value);
}
}
@@ -0,0 +1,13 @@
import { ZqSheetsFloatEchartService } from '../services/sheet-float-echart.service';
export class JnpfFacadeSheetsFloatEchart {
constructor(private readonly _jnpfSheetsFloatEchartService: ZqSheetsFloatEchartService) {}
clearFocusDrawingId() {
this._jnpfSheetsFloatEchartService.clearFocusDrawingId();
}
savePiniaStoreId(value: any) {
this._jnpfSheetsFloatEchartService.savePiniaStoreId(value);
}
}
@@ -0,0 +1,21 @@
import { ZqSheetsFloatImageService } from '../services/sheet-float-image.service';
import { JnpfSheetImageIoImplService } from '../services/sheet-image-io-impl.service';
export class JnpfFacadeSheetsFloatImage {
constructor(
private readonly _jnpfSheetsFloatImageService: ZqSheetsFloatImageService,
private readonly _jnpfSheetImageIoImplService: JnpfSheetImageIoImplService,
) {}
cacheImages() {
this._jnpfSheetImageIoImplService.cacheImages();
}
clearFocusDrawingId() {
this._jnpfSheetsFloatImageService.clearFocusDrawingId();
}
savePiniaStoreId(value: any) {
this._jnpfSheetsFloatImageService.savePiniaStoreId(value);
}
}
@@ -0,0 +1,9 @@
import { ZqSheetsPrintService } from '../services/sheet-print.service';
export class JnpfFacadeSheetsPrint {
constructor(private readonly _jnpfSheetsPrintService: ZqSheetsPrintService) {}
getLayouts(config: any) {
return this._jnpfSheetsPrintService.getLayouts(config);
}
}
@@ -0,0 +1,9 @@
import { ZqSheetsRangeService } from '../services/sheet-range.service';
export class JnpfFacadeSheetsRange {
constructor(private readonly _jnpfSheetsRangeService: ZqSheetsRangeService) {}
recoveryRange(row: number, col: number) {
return this._jnpfSheetsRangeService?.recoveryRange(row, col);
}
}
@@ -0,0 +1,132 @@
import { ICommandService, IImageIoService, Injector, IUniverInstanceService, LifecycleService, Univer } from '@univerjs/core';
import { FUniver } from '@univerjs/core/facade';
import { JnpfFacadeSheetsCell } from '../facade/f-sheet-cell';
import { JnpfFacadeSheetsCellImage } from '../facade/f-sheet-cell-image';
import { JnpfFacadeSheetsFloatDom } from '../facade/f-sheet-float-dom';
import { JnpfFacadeSheetsPrint } from '../facade/f-sheet-print';
import { JnpfFacadeSheetsRange } from '../facade/f-sheet-range';
import { ZqSheetsCellImageService } from '../services/sheet-cell-image.service';
import { ZqSheetsCellService } from '../services/sheet-cell.service';
import { ZqSheetsFloatDomService } from '../services/sheet-float-dom.service';
import { ZqSheetsFloatEchartService } from '../services/sheet-float-echart.service';
import { ZqSheetsFloatImageService } from '../services/sheet-float-image.service';
import { JnpfSheetImageIoImplService } from '../services/sheet-image-io-impl.service';
import { ZqSheetsPrintService } from '../services/sheet-print.service';
import { ZqSheetsRangeService } from '../services/sheet-range.service';
import { JnpfFacadeSheetsFloatEchart } from './f-sheet-float-echart';
import { JnpfFacadeSheetsFloatImage } from './f-sheet-float-image';
import '@univerjs/sheets/facade';
import '@univerjs/ui/facade';
import '@univerjs/docs-ui/facade';
import '@univerjs/sheets-ui/facade';
import '@univerjs/sheets-filter/facade';
import '@univerjs/engine-formula/facade';
import '@univerjs/sheets-formula/facade';
import '@univerjs/sheets-data-validation/facade';
import '@univerjs/sheets-hyper-link/facade';
import '@univerjs/sheets-hyper-link-ui/facade';
import '@univerjs/watermark/facade';
import '@univerjs/sheets-thread-comment/facade';
import '@univerjs/sheets-crosshair-highlight/facade';
export class ZqFUniver extends FUniver {
constructor(injector: Injector, commandService: ICommandService, instanceService: IUniverInstanceService) {
const lifecycleService = injector.get<LifecycleService>(LifecycleService);
if (!lifecycleService) {
throw new Error('LifecycleService 未正确注入');
}
super(injector, commandService, instanceService, lifecycleService);
}
static newAPI(wrapped: Injector | Univer): ZqFUniver {
const injector = wrapped instanceof Univer ? wrapped.__getInjector() : wrapped;
const commandService = injector.get<ICommandService>(ICommandService);
const instanceService = injector.get<IUniverInstanceService>(IUniverInstanceService);
if (!commandService || !instanceService) {
throw new Error('注入ICommandService或IUniverInstanceService发生错误');
}
return new ZqFUniver(injector, commandService, instanceService);
}
getInjector() {
return this._injector;
}
getSheetsCell(): JnpfFacadeSheetsCell | null {
const jnpfSheetsCellService = this._injector.get(ZqSheetsCellService);
if (!jnpfSheetsCellService) {
return null;
}
return this._injector.createInstance(JnpfFacadeSheetsCell, jnpfSheetsCellService);
}
getSheetsCellImage(): JnpfFacadeSheetsCellImage | null {
const jnpfSheetsCellImageService = this._injector.get(ZqSheetsCellImageService);
if (!jnpfSheetsCellImageService) {
return null;
}
return this._injector.createInstance(JnpfFacadeSheetsCellImage, jnpfSheetsCellImageService);
}
getSheetsFloatDom(): JnpfFacadeSheetsFloatDom | null {
const jnpfSheetsFloatDomService = this._injector.get(ZqSheetsFloatDomService);
if (!jnpfSheetsFloatDomService) {
return null;
}
return this._injector.createInstance(JnpfFacadeSheetsFloatDom, jnpfSheetsFloatDomService);
}
getSheetsFloatEchart(): JnpfFacadeSheetsFloatEchart | null {
const jnpfSheetsFloatEchartService = this._injector.get(ZqSheetsFloatEchartService);
if (!jnpfSheetsFloatEchartService) {
return null;
}
return this._injector.createInstance(JnpfFacadeSheetsFloatEchart, jnpfSheetsFloatEchartService);
}
getSheetsFloatImage(): JnpfFacadeSheetsFloatImage | null {
const jnpfSheetsFloatImageService = this._injector.get(ZqSheetsFloatImageService);
const jnpfSheetImageIoImplService = this._injector.get(IImageIoService);
if (!jnpfSheetImageIoImplService) {
return null;
}
if (!jnpfSheetsFloatImageService) {
return null;
}
return this._injector.createInstance(JnpfFacadeSheetsFloatImage, jnpfSheetsFloatImageService, jnpfSheetImageIoImplService as JnpfSheetImageIoImplService);
}
getSheetsPrint(): JnpfFacadeSheetsPrint | null {
const jnpfSheetsPrintService = this._injector.get(ZqSheetsPrintService);
if (!jnpfSheetsPrintService) {
return null;
}
return this._injector.createInstance(JnpfFacadeSheetsPrint, jnpfSheetsPrintService);
}
getSheetsRange(): JnpfFacadeSheetsRange | null {
const jnpfSheetsRangeService = this._injector.get(ZqSheetsRangeService);
if (!jnpfSheetsRangeService) {
return null;
}
return this._injector.createInstance(JnpfFacadeSheetsRange, jnpfSheetsRangeService);
}
}
@@ -0,0 +1,48 @@
export default {
jnpfSheetDownloadExcelFileMenu: {
title: 'Export Excel',
tooltip: 'Export Excel',
},
jnpfSheetExcelFileMenu: {
tooltip: 'Excel file',
},
jnpfSheetFloatEchartMenu: {
tooltip: 'Charts',
},
jnpfSheetImportCsvFileMenu: {
title: 'Import CSV',
tooltip: 'Import CSV',
},
jnpfSheetImportExcelFileMenu: {
title: 'Import Excel',
tooltip: 'Import Excel',
},
jnpfSheetInsertFloatBarEchartMenu: {
title: 'Bar chart',
tooltip: 'Bar chart',
},
jnpfSheetInsertFloatImageMenu: {
title: 'Floating image',
tooltip: 'Floating image',
},
jnpfSheetInsertFloatLineEchartMenu: {
title: 'Line chart',
tooltip: 'Line chart',
},
jnpfSheetInsertFloatPieEchartMenu: {
title: 'Pie chart',
tooltip: 'Pie chart',
},
jnpfSheetInsertFloatRadarEchartMenu: {
title: 'Radar chart',
tooltip: 'Radar chart',
},
jnpfSheetPreviewMenu: {
title: 'Preview',
tooltip: 'Design preview',
},
jnpfSheetPrintMenu: {
title: 'Print',
tooltip: 'Print',
},
};
@@ -0,0 +1,23 @@
import type { LocaleService } from '@univerjs/core';
import enUS from './en-US';
import zhCN from './zh-CN';
import zhTW from './zh-TW';
/** 注册报表设计器自定义菜单文案(简中 / 英文 / 繁中) */
export function registerZqSheetLocales(localeService: LocaleService) {
localeService.load({
zhCN,
enUS,
zhTW,
});
}
export type ZqAppLocale = 'zh-CN' | 'en-US' | 'zh-TW';
/** 将应用 locale 映射为插件 LocaleService 自定义文案 key */
export function mapAppLocaleToZqPackKey(appLocale?: string): 'enUS' | 'zhCN' | 'zhTW' {
if (appLocale === 'en-US') return 'enUS';
if (appLocale === 'zh-TW') return 'zhTW';
return 'zhCN';
}
@@ -0,0 +1,48 @@
export default {
jnpfSheetDownloadExcelFileMenu: {
title: '导出Excel',
tooltip: '导出Excel',
},
jnpfSheetExcelFileMenu: {
tooltip: 'Excel文件',
},
jnpfSheetFloatEchartMenu: {
tooltip: '图表',
},
jnpfSheetImportCsvFileMenu: {
title: '导入CSV',
tooltip: '导入CSV',
},
jnpfSheetImportExcelFileMenu: {
title: '导入Excel',
tooltip: '导入Excel',
},
jnpfSheetInsertFloatBarEchartMenu: {
title: '柱状图',
tooltip: '柱状图-图表',
},
jnpfSheetInsertFloatImageMenu: {
title: '自定义悬浮图片',
tooltip: '自定义悬浮图片-图片',
},
jnpfSheetInsertFloatLineEchartMenu: {
title: '折线图',
tooltip: '折线图-图表',
},
jnpfSheetInsertFloatPieEchartMenu: {
title: '饼图',
tooltip: '饼图-图表',
},
jnpfSheetInsertFloatRadarEchartMenu: {
title: '雷达图',
tooltip: '雷达图-图表',
},
jnpfSheetPreviewMenu: {
title: '设计预览功能',
tooltip: '设计预览',
},
jnpfSheetPrintMenu: {
title: '打印功能',
tooltip: '打印',
},
};
@@ -0,0 +1,48 @@
export default {
jnpfSheetDownloadExcelFileMenu: {
title: '匯出 Excel',
tooltip: '匯出 Excel',
},
jnpfSheetExcelFileMenu: {
tooltip: 'Excel 檔案',
},
jnpfSheetFloatEchartMenu: {
tooltip: '圖表',
},
jnpfSheetImportCsvFileMenu: {
title: '匯入 CSV',
tooltip: '匯入 CSV',
},
jnpfSheetImportExcelFileMenu: {
title: '匯入 Excel',
tooltip: '匯入 Excel',
},
jnpfSheetInsertFloatBarEchartMenu: {
title: '柱狀圖',
tooltip: '柱狀圖',
},
jnpfSheetInsertFloatImageMenu: {
title: '自訂懸浮圖片',
tooltip: '自訂懸浮圖片',
},
jnpfSheetInsertFloatLineEchartMenu: {
title: '折線圖',
tooltip: '折線圖',
},
jnpfSheetInsertFloatPieEchartMenu: {
title: '圓餅圖',
tooltip: '圓餅圖',
},
jnpfSheetInsertFloatRadarEchartMenu: {
title: '雷達圖',
tooltip: '雷達圖',
},
jnpfSheetPreviewMenu: {
title: '設計預覽',
tooltip: '設計預覽',
},
jnpfSheetPrintMenu: {
title: '列印',
tooltip: '列印',
},
};
@@ -0,0 +1,27 @@
import type { Dependency } from '@univerjs/core';
import { DependentOn, Inject, Injector, LocaleService, Plugin, UniverInstanceType } from '@univerjs/core';
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
import { registerZqSheetLocales } from '../locales/register';
import { ZqSheetsCellImageService } from '../services/sheet-cell-image.service';
@DependentOn(UniverSheetsUIPlugin)
export class ZqSheetsCellImagePlugin extends Plugin {
static override pluginName = 'JNPF_SHEET_CELL_IMAGE_PLUGIN';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: unknown,
@Inject(Injector) protected readonly _injector: Injector,
@Inject(LocaleService) private readonly _localeService: LocaleService,
) {
super();
registerZqSheetLocales(this._localeService);
}
override onStarting(): void {
([[ZqSheetsCellImageService]] as Dependency[]).forEach(d => this._injector.add(d));
}
}
@@ -0,0 +1,27 @@
import type { Dependency } from '@univerjs/core';
import { DependentOn, Inject, Injector, LocaleService, Plugin, UniverInstanceType } from '@univerjs/core';
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
import { registerZqSheetLocales } from '../locales/register';
import { ZqSheetsCellService } from '../services/sheet-cell.service';
@DependentOn(UniverSheetsUIPlugin)
export class ZqSheetsCellPlugin extends Plugin {
static override pluginName = 'JNPF_SHEET_CELL_PLUGIN';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: unknown,
@Inject(Injector) protected readonly _injector: Injector,
@Inject(LocaleService) private readonly _localeService: LocaleService,
) {
super();
registerZqSheetLocales(this._localeService);
}
override onStarting(): void {
([[ZqSheetsCellService]] as Dependency[]).forEach(d => this._injector.add(d));
}
}
@@ -0,0 +1,29 @@
import type { Dependency } from '@univerjs/core';
import { DependentOn, Inject, Injector, LocaleService, Plugin, UniverInstanceType } from '@univerjs/core';
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
import { ZqSheetsDialogController } from '../controllers/sheet-dialog.controller';
import { registerZqSheetLocales } from '../locales/register';
@DependentOn(UniverSheetsUIPlugin)
export class ZqSheetsDialogPlugin extends Plugin {
static override pluginName = 'JNPF_SHEET_DIALOG_PLUGIN';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: unknown,
@Inject(Injector) protected readonly _injector: Injector,
@Inject(LocaleService) private readonly _localeService: LocaleService,
) {
super();
registerZqSheetLocales(this._localeService);
}
override onStarting(): void {
([[ZqSheetsDialogController]] as Dependency[]).forEach(d => this._injector.add(d));
this._injector.get(ZqSheetsDialogController);
}
}
@@ -0,0 +1,30 @@
import type { Dependency } from '@univerjs/core';
import { DependentOn, Inject, Injector, LocaleService, Plugin, UniverInstanceType } from '@univerjs/core';
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
import { ZqSheetsExcelFileController } from '../controllers/sheet-excel-file.controller';
import { registerZqSheetLocales } from '../locales/register';
import { ZqSheetsExcelFileService } from '../services/sheet-excel-file.service';
@DependentOn(UniverSheetsUIPlugin)
export class ZqSheetsExcelFilePlugin extends Plugin {
static override pluginName = 'JNPF_SHEET_EXCEL_FILE_PLUGIN';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: unknown,
@Inject(Injector) protected readonly _injector: Injector,
@Inject(LocaleService) private readonly _localeService: LocaleService,
) {
super();
registerZqSheetLocales(this._localeService);
}
override onStarting(): void {
([[ZqSheetsExcelFileController], [ZqSheetsExcelFileService]] as Dependency[]).forEach(d => this._injector.add(d));
this._injector.get(ZqSheetsExcelFileController);
}
}
@@ -0,0 +1,27 @@
import type { Dependency } from '@univerjs/core';
import { DependentOn, Inject, Injector, LocaleService, Plugin, UniverInstanceType } from '@univerjs/core';
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
import { registerZqSheetLocales } from '../locales/register';
import { ZqSheetsFloatDomService } from '../services/sheet-float-dom.service';
@DependentOn(UniverSheetsUIPlugin)
export class ZqSheetsFloatDomPlugin extends Plugin {
static override pluginName = 'JNPF_SHEET_FLOAT_DOM_PLUGIN';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: unknown,
@Inject(Injector) protected readonly _injector: Injector,
@Inject(LocaleService) private readonly _localeService: LocaleService,
) {
super();
registerZqSheetLocales(this._localeService);
}
override onStarting(): void {
([[ZqSheetsFloatDomService]] as Dependency[]).forEach(d => this._injector.add(d));
}
}
@@ -0,0 +1,30 @@
import type { Dependency } from '@univerjs/core';
import { DependentOn, Inject, Injector, LocaleService, Plugin, UniverInstanceType } from '@univerjs/core';
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
import { ZqSheetsFloatEchartController } from '../controllers/sheet-float-echart.controller';
import { registerZqSheetLocales } from '../locales/register';
import { ZqSheetsFloatEchartService } from '../services/sheet-float-echart.service';
@DependentOn(UniverSheetsUIPlugin)
export class ZqSheetsFloatEchartPlugin extends Plugin {
static override pluginName = 'JNPF_SHEET_FLOAT_ECHART_PLUGIN';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: unknown,
@Inject(Injector) protected readonly _injector: Injector,
@Inject(LocaleService) private readonly _localeService: LocaleService,
) {
super();
registerZqSheetLocales(this._localeService);
}
override onStarting(): void {
([[ZqSheetsFloatEchartController], [ZqSheetsFloatEchartService]] as Dependency[]).forEach(d => this._injector.add(d));
this._injector.get(ZqSheetsFloatEchartController);
}
}
@@ -0,0 +1,34 @@
import type { Dependency } from '@univerjs/core';
import { DependentOn, IImageIoService, Inject, Injector, LocaleService, mergeOverrideWithDependencies, Plugin, UniverInstanceType } from '@univerjs/core';
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
import { ZqSheetsFloatImageController } from '../controllers/sheet-float-image.controller';
import { registerZqSheetLocales } from '../locales/register';
import { ZqSheetsFloatImageService } from '../services/sheet-float-image.service';
import { JnpfSheetImageIoImplService } from '../services/sheet-image-io-impl.service';
@DependentOn(UniverSheetsUIPlugin)
export class ZqSheetsFloatImagePlugin extends Plugin {
static override pluginName = 'JNPF_SHEET_FLOAT_IMAGE_PLUGIN';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: unknown,
@Inject(Injector) protected readonly _injector: Injector,
@Inject(LocaleService) private readonly _localeService: LocaleService,
) {
super();
registerZqSheetLocales(this._localeService);
}
override onStarting(): void {
mergeOverrideWithDependencies(
[[ZqSheetsFloatImageController], [ZqSheetsFloatImageService], [IImageIoService, { useClass: JnpfSheetImageIoImplService }]] as Dependency[],
[],
).forEach(d => this._injector.add(d));
this._injector.get(ZqSheetsFloatImageController);
}
}
@@ -0,0 +1,29 @@
import type { Dependency } from '@univerjs/core';
import { DependentOn, Inject, Injector, LocaleService, Plugin, UniverInstanceType } from '@univerjs/core';
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
import { ZqSheetsPreviewController } from '../controllers/sheet-preview.controller';
import { registerZqSheetLocales } from '../locales/register';
@DependentOn(UniverSheetsUIPlugin)
export class ZqSheetsPreviewPlugin extends Plugin {
static override pluginName = 'JNPF_SHEET_PREVIEW_PLUGIN';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: unknown,
@Inject(Injector) protected readonly _injector: Injector,
@Inject(LocaleService) private readonly _localeService: LocaleService,
) {
super();
registerZqSheetLocales(this._localeService);
}
override onStarting(): void {
([[ZqSheetsPreviewController]] as Dependency[]).forEach(d => this._injector.add(d));
this._injector.get(ZqSheetsPreviewController);
}
}
@@ -0,0 +1,28 @@
import type { Dependency } from '@univerjs/core';
import { DependentOn, Inject, Injector, LocaleService, Plugin, UniverInstanceType } from '@univerjs/core';
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
import { registerZqSheetLocales } from '../locales/register';
import { ZqSheetsPrintUiService } from '../services/sheet-print-ui.service';
import { ZqSheetsPrintService } from '../services/sheet-print.service';
@DependentOn(UniverSheetsUIPlugin)
export class ZqSheetsPrintPlugin extends Plugin {
static override pluginName = 'JNPF_SHEET_PRINT_PLUGIN';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: unknown,
@Inject(Injector) protected readonly _injector: Injector,
@Inject(LocaleService) private readonly _localeService: LocaleService,
) {
super();
registerZqSheetLocales(this._localeService);
}
override onStarting(): void {
([[ZqSheetsPrintService], [ZqSheetsPrintUiService]] as Dependency[]).forEach(d => this._injector.add(d));
}
}
@@ -0,0 +1,27 @@
import type { Dependency } from '@univerjs/core';
import { DependentOn, Inject, Injector, LocaleService, Plugin, UniverInstanceType } from '@univerjs/core';
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
import { registerZqSheetLocales } from '../locales/register';
import { ZqSheetsRangeService } from '../services/sheet-range.service';
@DependentOn(UniverSheetsUIPlugin)
export class ZqSheetsRangePlugin extends Plugin {
static override pluginName = 'JNPF_SHEET_RANGE_PLUGIN';
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
_config: unknown,
@Inject(Injector) protected readonly _injector: Injector,
@Inject(LocaleService) private readonly _localeService: LocaleService,
) {
super();
registerZqSheetLocales(this._localeService);
}
override onStarting(): void {
([[ZqSheetsRangeService]] as Dependency[]).forEach(d => this._injector.add(d));
}
}
@@ -0,0 +1,205 @@
import type { IAccessor, IImageIoServiceParam, Nullable } from '@univerjs/core';
import {
BooleanNumber,
BuildTextUtils,
createDocumentModelWithStyle,
Disposable,
DrawingTypeEnum,
ICommandService,
IImageIoService,
Inject,
Injector,
IUniverInstanceService,
ObjectRelativeFromH,
ObjectRelativeFromV,
PositionedObjectLayoutType,
UniverInstanceType,
Workbook,
WrapTextType,
} from '@univerjs/core';
import { docDrawingPositionToTransform } from '@univerjs/docs-ui';
import { getImageSize } from '@univerjs/drawing';
import { IRenderManagerService } from '@univerjs/engine-render';
import type { ISheetLocationBase } from '@univerjs/sheets';
import { SetRangeValuesCommand } from '@univerjs/sheets';
import { SheetSkeletonManagerService } from '@univerjs/sheets-ui';
import { rotatedBoundingBox } from '../utils';
export class ZqSheetsCellImageService extends Disposable {
constructor(
@Inject(Injector) private readonly _injector: Injector,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@ICommandService private readonly _commandService: ICommandService,
@IImageIoService private readonly _imageIoService: IImageIoService,
) {
super();
}
/**
* 在当前选中的单元格插入图片
* @param file 要插入的图片文件
* @param row 行
* @param col 列
* @param cellData 单元格配置信息
* @returns 插入结果,成功返回执行命令的结果,失败返回 `false` 或 `null`
*/
public async insertCellImage(file: File, row: number, col: number, cellData: any) {
// 获取当前的工作簿实例
const workbook = this._univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET);
if (!workbook) {
return false;
}
const unitId = workbook.getUnitId();
// 获取当前激活的工作表
const worksheet = workbook.getActiveSheet();
if (!worksheet) {
return false;
}
const subUnitId = worksheet.getSheetId();
// 将图片文件保存到 ImageIoService 并获取图片参数
let imageParam: Nullable<IImageIoServiceParam>;
try {
imageParam = await this._imageIoService.saveImage(file);
} catch {
return false;
}
if (imageParam == null) {
return false;
}
// 获取图片相关信息
const { base64Cache, imageId, imageSourceType, source } = imageParam;
const { height, image, width } = await getImageSize(base64Cache || '');
// 缓存图片数据
this._imageIoService.addImageSourceCache(source, imageSourceType, image);
const docDataModel = createDocumentModelWithStyle('', {});
// 计算图片适应单元格后的尺寸
const imageSize = this._getDrawingSizeByCell(
this._injector,
{
col,
row,
subUnitId,
unitId,
},
width,
height,
0,
);
if (!imageSize) {
return false;
}
// 定义图片的文档变换参数
const docTransform = {
angle: 0,
positionH: {
posOffset: 0,
relativeFrom: ObjectRelativeFromH.PAGE,
},
positionV: {
posOffset: 0,
relativeFrom: ObjectRelativeFromV.PARAGRAPH,
},
size: {
height: imageSize.height,
width: imageSize.width,
},
};
// 定义文档中的绘图参数
const docDrawingParam = {
behindDoc: BooleanNumber.FALSE,
description: '',
distB: 0,
distL: 0,
distR: 0,
distT: 0,
docTransform,
drawingId: imageId,
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
imageSourceType,
layoutType: PositionedObjectLayoutType.INLINE, // Insert inline drawing by default.
source,
subUnitId: docDataModel.getUnitId(),
title: '',
transform: docDrawingPositionToTransform(docTransform),
unitId: docDataModel.getUnitId(),
wrapText: WrapTextType.BOTH_SIDES,
};
// 生成插入绘图的 JSON 结构
const jsonXActions = BuildTextUtils.drawing.add({
documentDataModel: docDataModel,
drawings: [docDrawingParam],
selection: {
collapsed: true,
endOffset: 0,
startOffset: 0,
},
});
if (!jsonXActions) {
return false;
}
docDataModel.apply(jsonXActions);
return this._commandService.syncExecuteCommand(SetRangeValuesCommand.id, {
value: {
[row]: {
[col]: {
custom: {
...cellData.custom,
drawingId: imageId,
},
p: docDataModel.getSnapshot(),
t: 1,
v: cellData.v ?? '',
},
},
},
});
}
/**
* 获取单元格内图形的大小
* @param accessor 访问器对象
* @param location 单元格位置信息
* @param originImageWidth 原始图像宽度
* @param originImageHeight 原始图像高度
* @param angle 旋转角度(0-360,单位:度)
* @returns 计算后的图像尺寸,或在获取失败时返回 `false`
*/
private _getDrawingSizeByCell(accessor: IAccessor, location: ISheetLocationBase, originImageWidth: number, originImageHeight: number, angle: number) {
const { rotatedHeight, rotatedWidth } = rotatedBoundingBox(originImageWidth, originImageHeight, angle);
const renderManagerService = accessor.get(IRenderManagerService);
const currentRender = renderManagerService.getRenderById(location.unitId);
if (!currentRender) {
return false;
}
const skeletonManagerService = currentRender.with(SheetSkeletonManagerService);
const skeleton = skeletonManagerService.getWorksheetSkeleton(location.subUnitId)?.skeleton;
if (skeleton == null) {
return false;
}
const cellInfo = skeleton.getCellWithCoordByIndex(location.row, location.col);
const cellWidth = cellInfo.mergeInfo.endX - cellInfo.mergeInfo.startX - 2;
const cellHeight = cellInfo.mergeInfo.endY - cellInfo.mergeInfo.startY - 2;
const imageRatio = rotatedWidth / rotatedHeight;
const imageWidth = Math.ceil(Math.min(cellWidth, cellHeight * imageRatio));
const scale = imageWidth / rotatedWidth;
const realScale = !scale || Number.isNaN(scale) ? 0.001 : scale;
return {
height: originImageHeight * realScale,
width: originImageWidth * realScale,
};
}
}
@@ -0,0 +1,125 @@
import type { ICellData } from '@univerjs/core';
import { Disposable, IUniverInstanceService, UniverInstanceType, Workbook } from '@univerjs/core';
import { IRenderManagerService } from '@univerjs/engine-render';
import { SheetSkeletonManagerService } from '@univerjs/sheets-ui';
export class ZqSheetsCellService extends Disposable {
constructor(
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
) {
super();
}
// 获取所有单元格数据
public getAllCellData() {
const sheet = this._univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet();
return sheet.getCellMatrix().getMatrix();
}
// 获取指定单元格数据
public getCellData(col: number, row: number) {
const sheet = this._univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet();
const cellMatrix = sheet.getCellMatrix().getMatrix();
return cellMatrix?.[row]?.[col] ?? null; // 如果未找到匹配的单元格,返回 null
}
// 获取当前单元格的自定义属性
public getCurrentSheetCellsCustom() {
const sheet = this._univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet();
const cellMatrix = sheet?.getCellMatrix()?.getMatrix() ?? {};
const result: { [row: string]: { [col: string]: any } } = {};
Object.entries(cellMatrix)?.forEach(([row, colObj]) => {
if (colObj && typeof colObj === 'object') {
const rowData: { [col: string]: any } = {};
Object.entries(colObj)?.forEach(([col, value]: any) => {
if (value?.custom) {
rowData[col] = { ...value };
}
});
if (Object.keys(rowData)?.length) {
result[row] = rowData;
}
}
});
return result;
}
// 获取激活单元格的尺寸信息
public getTargetCellSize(col: number, row: number): null | { cellHeight: number; cellWidth: number } {
// 获取当前的工作簿实例
const workbook = this._univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET);
if (!workbook) {
return null;
}
const unitId = workbook.getUnitId();
// 获取当前激活的工作表
const worksheet = workbook.getActiveSheet();
if (!worksheet) {
return null;
}
const subUnitId = worksheet.getSheetId();
// 获取对应的骨架渲染服务
const skeleton = this._renderManagerService?.getRenderById(unitId)?.with(SheetSkeletonManagerService)?.getUnitSkeleton(unitId, subUnitId)?.skeleton as any;
if (!skeleton) {
return null;
}
// 获取指定单元格的位置信息
const cellInfo = skeleton.getCellWithCoordByIndex(row, col);
if (!cellInfo) {
return null;
}
// 解构单元格信息
const { endX, endY, isMergedMainCell, mergeInfo, startX, startY } = cellInfo;
let cellWidth: number;
let cellHeight: number;
if (isMergedMainCell && mergeInfo) {
cellWidth = mergeInfo.endX - mergeInfo.startX;
cellHeight = mergeInfo.endY - mergeInfo.startY;
} else {
cellWidth = endX - startX;
cellHeight = endY - startY;
}
return {
cellHeight: Math.floor(cellHeight) - 2,
cellWidth: Math.floor(cellWidth) - 2,
};
}
// 刷新所有单元格的视图
public refreshRangeCellsView(range: any) {
if (!range) {
return;
}
const originalValue = range?.getValue() ?? '';
range?.setValue(originalValue);
}
// 设置单元格数据
public setCellData(col: number, row: number, cellData: ICellData) {
const sheet = this._univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet();
const cellMatrix = sheet.getCellMatrix();
try {
cellMatrix.setValue(row, col, cellData);
} catch (error) {
console.warn(error);
}
}
}
@@ -0,0 +1,148 @@
import type { ICellData } from '@univerjs/core';
import { Disposable, ICommandService, IUniverInstanceService, UniverInstanceType, Workbook } from '@univerjs/core';
import { SetRangeValuesCommand } from '@univerjs/sheets';
import { ZQ_REPORT_IMPORT_EXCEL_URL } from '../utils/define';
export class ZqSheetsExcelFileService extends Disposable {
private _exportExcelHandler: (() => void) | null = null;
constructor(
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@ICommandService private readonly _commandService: ICommandService,
) {
super();
}
setExportExcelHandler(handler: (() => void) | null) {
this._exportExcelHandler = handler;
}
handleDownloadExcel() {
this._exportExcelHandler?.();
}
public handleImportCsv() {
const sheet = this._univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet();
this._waitUserSelectSpreadsheetFile(({ colsCount, data, rowsCount }) => {
// 设置工作表大小
sheet.setColumnCount(colsCount);
sheet.setRowCount(rowsCount);
// 设置工作表数据
this._commandService
.executeCommand(SetRangeValuesCommand.id, {
range: {
endColumn: colsCount - 1, // 结束列索引
endRow: rowsCount - 1, // 结束行索引
startColumn: 0, // 起始列索引
startRow: 0, // 起始行索引
},
value: this._parseCSV2UniverData(data),
})
.then();
});
}
/**
* 将 CSV 解析为 Univer 数据
* @param csv CSV 数据
* @returns { v: string }[][] 返回解析后的数据
*/
private _parseCSV2UniverData(csv: string[][]): ICellData[][] {
return csv.map(row => {
return row.map(cell => {
return {
v: cell || '',
};
});
});
}
private _applyGridToSheet(
sheet: any,
payload: { colsCount: number; data: ICellData[][]; rowsCount: number },
) {
const { colsCount, data, rowsCount } = payload;
sheet.setColumnCount(colsCount);
sheet.setRowCount(rowsCount);
this._commandService
.executeCommand(SetRangeValuesCommand.id, {
range: {
endColumn: colsCount - 1,
endRow: rowsCount - 1,
startColumn: 0,
startRow: 0,
},
value: data,
})
.then();
}
/**
* 等待用户选择 CSV / Excel 文件
*/
private _waitUserSelectSpreadsheetFile(
onSelect: (data: { colsCount: number; data: ICellData[][]; rowsCount: number }) => void,
) {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.csv,.xlsx,.xls';
input.click();
input.addEventListener('change', async () => {
const file = input.files?.[0];
if (!file) return;
const name = file.name.toLowerCase();
if (name.endsWith('.xlsx') || name.endsWith('.xls')) {
try {
const form = new FormData();
form.append('file', file);
const resp = await fetch(ZQ_REPORT_IMPORT_EXCEL_URL, {
method: 'POST',
body: form,
credentials: 'include',
});
if (!resp.ok) {
console.error('Excel import failed', await resp.text());
return;
}
const json = await resp.json();
const body = json?.data !== undefined && json?.code !== undefined ? json.data : json;
const gridData = body as {
rowsCount?: number;
colsCount?: number;
data?: ICellData[][];
};
onSelect({
colsCount: gridData.colsCount || 0,
rowsCount: gridData.rowsCount || 0,
data: gridData.data || [],
});
} catch (err) {
console.error('Excel import error', err);
}
return;
}
const reader = new FileReader();
reader.addEventListener('load', () => {
const text = reader.result;
if (typeof text !== 'string') return;
const rows = text.split(/\r\n|\n/);
const csv = rows.map(line => line.split(','));
const colsCount = csv.reduce((max, row) => Math.max(max, row.length), 0);
onSelect({
colsCount,
data: this._parseCSV2UniverData(csv),
rowsCount: csv.length,
});
});
reader.readAsText(file);
});
}
}
@@ -0,0 +1,53 @@
import { Disposable } from '@univerjs/core';
export class ZqSheetsFloatDomService extends Disposable {
// private _floatEchartItems: Record<string, any> = {};
//
// private _floatImageItems: Record<string, any> = {};
// private _piniaStoreId: null | string = null;
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor() {
super();
// this._addFloatDomHook();
}
// 存入图表列表数据
public saveFloatEchartItems(data: any) {
// this._floatEchartItems = data;
}
// 存入图片列表数据
public saveFloatImageItems(data: any) {
// this._floatImageItems = data;
}
// 存入store的id
public savePiniaStoreId(data: any) {
// this._piniaStoreId = data;
}
// 增加Hook
// private _addFloatDomHook() {
// this.disposeWithMe(
// this._sheetCanvasFloatDomManagerService.addHook({
// onGetFloatDomProps: (id: string) => {
// if (this._floatImageItems?.hasOwnProperty(id)) {
// const { domId } = this._floatImageItems[id] ?? {};
// return { id: domId, piniaStoreId: this._piniaStoreId };
// }
//
// if (this._floatEchartItems?.hasOwnProperty(id)) {
// const { domId } = this._floatEchartItems[id] ?? {};
// return { id: domId, piniaStoreId: this._piniaStoreId };
// }
//
// return {
// id,
// };
// },
// }),
// );
// }
}
@@ -0,0 +1,153 @@
import { Disposable, ICommandService, Inject, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { IDrawingManagerService } from '@univerjs/drawing';
import { DeviceInputEventType, IRenderManagerService } from '@univerjs/engine-render';
import { SheetsSelectionsService } from '@univerjs/sheets';
import { SheetCanvasFloatDomManagerService } from '@univerjs/sheets-drawing-ui';
import { attachRangeWithCoord, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
import { DefaultFloatEchartHeight, DefaultFloatEchartOptions, DefaultFloatEchartWidth, ZqCommandIds, ZqUniverFloatEchartKey } from '../utils/define';
import { buildUUID } from '../utils/uuid';
const regex = /^ZqUniverFloat(.*?)Echart$/; // 正则表达式匹配 ZqUniver 和 Echart
export class ZqSheetsFloatEchartService extends Disposable {
private _focusDrawingId: null | string = null;
private _piniaStoreId: null | string = null;
private readonly defaultPosition = {
endX: DefaultFloatEchartWidth,
endY: DefaultFloatEchartHeight,
startX: 100,
startY: 100,
};
constructor(
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@ICommandService private readonly _commandService: ICommandService,
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
@Inject(SheetCanvasFloatDomManagerService) private readonly _sheetCanvasFloatDomManagerService: SheetCanvasFloatDomManagerService,
@Inject(SheetsSelectionsService) private readonly _selectionManagerService: SheetsSelectionsService,
) {
super();
this._subscribeToFocusEvents();
}
// 清空焦点对象的id
public clearFocusDrawingId() {
if (!this._focusDrawingId) {
return;
}
this._focusDrawingId = null;
}
// 创建并插入图表DOM
public insertFloatEchart(componentKey: string) {
this._commandService
.executeCommand('sheet.operation.set-cell-edit-visible', {
_eventType: DeviceInputEventType.PointerUp,
visible: false,
})
.then();
const initPosition = this._calculateInitPosition();
const domId = `echart_${buildUUID()}`;
const addResult = this._sheetCanvasFloatDomManagerService?.addFloatDomToPosition({
allowTransform: true,
componentKey: ZqUniverFloatEchartKey,
initPosition,
data: {
id: domId,
piniaStoreId: this._piniaStoreId,
},
});
if (!addResult) {
return;
}
const type = componentKey?.replace(regex, '$1');
const echartType = type ? type.charAt(0)?.toLowerCase() + type.slice(1) : undefined;
const newParams = {
domId,
drawingId: addResult.id,
echartType,
option: JSON.parse(JSON.stringify(DefaultFloatEchartOptions?.[echartType as keyof typeof DefaultFloatEchartOptions])),
};
// 自定义命令出来,告知相关的信息
this._commandService.executeCommand(ZqCommandIds?.insertedFloatEchart, newParams).then();
}
// 存入store的id
public savePiniaStoreId(data: string) {
this._piniaStoreId = data;
}
// 计算初始位置
private _calculateInitPosition() {
// 获取当前单元格数据
const unit = this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET);
if (!unit) {
console.warn('警告:未找到单元。使用默认位置 (100, 100)。');
return this.defaultPosition;
}
const unitId = unit.getUnitId();
const currentSelections = this._selectionManagerService.getCurrentSelections();
const firstSelection = currentSelections?.[0];
if (!firstSelection?.range) {
console.warn('警告:未找到选择范围。使用默认位置 (100, 100)。');
return this.defaultPosition;
}
// 获取当前表格骨架信息
const sheetSkeletonService = this._renderManagerService.getRenderById(unitId)?.with(SheetSkeletonManagerService);
const currentSkeleton = sheetSkeletonService?.getCurrent()?.skeleton;
if (!currentSkeleton) {
console.warn('警告:未找到骨架。使用默认位置 (100, 100)。');
return this.defaultPosition;
}
// 解构表格尺寸信息
const { columnHeaderHeight = 0, columnTotalWidth = 0, rowHeaderWidth = 0, rowTotalHeight = 0 } = currentSkeleton;
const columnScreenTotalWidth = columnTotalWidth + rowHeaderWidth;
const columnScreenTotalHeight = rowTotalHeight + columnHeaderHeight;
// 获取范围信息并计算坐标
const rangeInfo = attachRangeWithCoord(currentSkeleton, firstSelection.range) || {
startX: 0,
startY: 0,
};
const { startX: rangeStartX, startY: rangeStartY } = rangeInfo;
// 计算图表的边界坐标
const startX = Math.min(rangeStartX + DefaultFloatEchartWidth, columnScreenTotalWidth) - DefaultFloatEchartWidth;
const endX = Math.min(rangeStartX + DefaultFloatEchartWidth, columnScreenTotalWidth);
const startY = Math.min(rangeStartY + DefaultFloatEchartHeight, columnScreenTotalHeight) - DefaultFloatEchartHeight;
const endY = Math.min(rangeStartY + DefaultFloatEchartHeight, columnScreenTotalHeight);
return { endX, endY, startX, startY };
}
// 订阅图表的焦点事件
private _subscribeToFocusEvents() {
this.disposeWithMe(
this._drawingManagerService.focus$.subscribe(params => {
if (params && params?.length) {
const firstParam = (params?.[0] ?? {}) as any;
const { componentKey, drawingId } = firstParam;
if (drawingId !== this._focusDrawingId && componentKey === ZqUniverFloatEchartKey) {
this._focusDrawingId = drawingId;
this._commandService?.executeCommand(ZqCommandIds.focusFloatEchart, firstParam);
}
}
}),
);
}
}
@@ -0,0 +1,140 @@
import { Disposable, ICommandService, Inject, IUniverInstanceService, UniverInstanceType, Workbook } from '@univerjs/core';
import { IDrawingManagerService } from '@univerjs/drawing';
import { DeviceInputEventType, IRenderManagerService } from '@univerjs/engine-render';
import { SheetsSelectionsService } from '@univerjs/sheets';
import { SheetCanvasFloatDomManagerService } from '@univerjs/sheets-drawing-ui';
import { attachRangeWithCoord, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
import { DefaultFloatImageHeight, DefaultFloatImageOption, DefaultFloatImageWidth, ZqCommandIds, ZqUniverFloatImageKey } from '../utils/define';
import { buildUUID } from '../utils/uuid';
export class ZqSheetsFloatImageService extends Disposable {
private _focusDrawingId: null | string = null;
private _piniaStoreId: null | string = null;
private readonly defaultPosition = { endX: DefaultFloatImageWidth, endY: DefaultFloatImageHeight, startX: 100, startY: 100 };
constructor(
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@ICommandService private readonly _commandService: ICommandService,
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
@Inject(SheetCanvasFloatDomManagerService) private readonly _sheetCanvasFloatDomManagerService: SheetCanvasFloatDomManagerService,
@Inject(SheetsSelectionsService) private readonly _selectionManagerService: SheetsSelectionsService,
) {
super();
this._subscribeToFocusEvents();
}
// 清空焦点对象的id
public clearFocusDrawingId() {
if (!this._focusDrawingId) {
return;
}
this._focusDrawingId = null;
}
// 创建并插入悬浮图片DOM
public insertFloatImage() {
this._commandService
.executeCommand('sheet.operation.set-cell-edit-visible', {
_eventType: DeviceInputEventType.PointerUp,
visible: false,
})
.then();
const initPosition = this._calculateInitPosition();
const domId = `float_image_${buildUUID()}`;
const addResult = this._sheetCanvasFloatDomManagerService?.addFloatDomToPosition({
allowTransform: true,
componentKey: ZqUniverFloatImageKey,
initPosition,
data: {
id: domId,
piniaStoreId: this._piniaStoreId,
},
});
if (!addResult) {
return;
}
const newParams = {
domId,
drawingId: addResult.id,
imageType: 'BASE64',
option: JSON.parse(JSON.stringify(DefaultFloatImageOption)),
};
this._commandService.executeCommand(ZqCommandIds?.insertedFloatImage, newParams).then();
}
// 存入store的id
public savePiniaStoreId(data: string) {
this._piniaStoreId = data;
}
// 计算初始位置
private _calculateInitPosition() {
// 获取当前单元格数据
const unit = this._univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET);
if (!unit) {
console.warn('警告:未找到单元。使用默认位置 (100, 100)。');
return this.defaultPosition;
}
const unitId = unit!.getUnitId();
const currentSelections = this._selectionManagerService.getCurrentSelections();
const firstSelection = currentSelections?.[0] ?? {};
if (!firstSelection?.range) {
console.warn('警告:未找到选择范围。使用默认位置 (100, 100)。');
return this.defaultPosition;
}
// 获取当前表格骨架信息
const sheetSkeletonService = this._renderManagerService.getRenderById(unitId)?.with(SheetSkeletonManagerService);
const currentSkeleton = sheetSkeletonService?.getCurrent()?.skeleton;
if (!currentSkeleton) {
console.warn('警告:未找到骨架。使用默认位置 (100, 100)。');
return this.defaultPosition;
}
// 解构表格尺寸信息
const { columnHeaderHeight = 0, columnTotalWidth = 0, rowHeaderWidth = 0, rowTotalHeight = 0 } = currentSkeleton;
const columnScreenTotalWidth = columnTotalWidth + rowHeaderWidth;
const columnScreenTotalHeight = rowTotalHeight + columnHeaderHeight;
// 获取范围信息并计算坐标
const rangeInfo = attachRangeWithCoord(currentSkeleton, firstSelection.range) || { startX: 0, startY: 0 };
const { startX: rangeStartX, startY: rangeStartY } = rangeInfo;
// 计算图表的边界坐标
const startX = Math.min(rangeStartX + DefaultFloatImageWidth, columnScreenTotalWidth) - DefaultFloatImageWidth;
const endX = Math.min(rangeStartX + DefaultFloatImageWidth, columnScreenTotalWidth);
const startY = Math.min(rangeStartY + DefaultFloatImageHeight, columnScreenTotalHeight) - DefaultFloatImageHeight;
const endY = Math.min(rangeStartY + DefaultFloatImageHeight, columnScreenTotalHeight);
return { endX, endY, startX, startY };
}
// 订阅图表的焦点事件
private _subscribeToFocusEvents() {
this.disposeWithMe(
this._drawingManagerService.focus$.subscribe(params => {
if (params && params?.length) {
const firstParam = (params?.[0] ?? {}) as any;
const { componentKey, drawingId } = firstParam;
if (drawingId !== this._focusDrawingId && componentKey === ZqUniverFloatImageKey) {
this._focusDrawingId = drawingId;
this._commandService?.executeCommand(ZqCommandIds.focusFloatImage, firstParam);
}
}
}),
);
}
}
@@ -0,0 +1,155 @@
import type {
IImageIoService,
IImageIoServiceParam,
Nullable,
Workbook,
Worksheet,
} from '@univerjs/core';
import type { Observable } from 'rxjs';
import {
generateRandomId,
ImageSourceType,
ImageUploadStatusType,
Inject,
Injector,
IUniverInstanceService,
UniverInstanceType,
} from '@univerjs/core';
import { IRenderManagerService } from '@univerjs/engine-render';
import { SheetSkeletonManagerService } from '@univerjs/sheets-ui';
import { Subject } from 'rxjs';
export const DRAWING_IMAGE_ALLOW_SIZE = 5 * 1024 * 1024;
export const DRAWING_IMAGE_ALLOW_IMAGE_LIST = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/bmp'];
export class JnpfSheetImageIoImplService implements IImageIoService {
private _change$ = new Subject<number>();
change$ = this._change$ as Observable<number>;
private _imageSourceCache: Map<string, HTMLImageElement> = new Map();
private _waitCount = 0;
constructor(
@Inject(Injector) private readonly _accessor: Injector,
@IUniverInstanceService protected readonly _univerInstanceService: IUniverInstanceService,
) {}
addImageSourceCache(source: string, imageSourceType: ImageSourceType, imageSource: Nullable<HTMLImageElement>) {
if (imageSourceType === ImageSourceType.BASE64 || imageSource == null) {
return;
}
this._imageSourceCache.set(source, imageSource);
}
cacheImages() {
const sheetInfo = this._sheet();
if (!sheetInfo) {
return;
}
const { sheet, unitId, subUnitId } = sheetInfo;
const skeleton = this._getSkeleton(unitId, subUnitId);
if (!skeleton) {
return;
}
skeleton.resetCache();
sheet.getCellMatrix().forValue((_row, _col, value) => {
if (value && value.p && value.p.drawings) {
Object.keys(value.p.drawings).forEach(drawing => {
const drawingItem: any = value.p?.drawings?.[drawing];
if (value.p && value.p.drawings && drawingItem && drawingItem.imageSourceType === 'BASE64' && drawingItem.source) {
this._imageSourceCache.set(drawingItem.imageSourceType, drawingItem.source);
// 强制缓存
skeleton.imageCacheMap.getImage(drawingItem.imageSourceType, drawingItem.source);
}
});
}
});
}
clearMemory() {}
async getImage(imageId: string): Promise<string> {
return imageId;
}
getImageSourceCache(source: string, imageSourceType: ImageSourceType) {
if (imageSourceType === ImageSourceType.BASE64) {
const image = new Image();
image.src = source;
return image;
}
return this._imageSourceCache.get(source);
}
async saveImage(imageFile: File): Promise<Nullable<IImageIoServiceParam>> {
return new Promise((resolve, reject) => {
if (!DRAWING_IMAGE_ALLOW_IMAGE_LIST.includes(imageFile.type)) {
reject(new Error(ImageUploadStatusType.ERROR_IMAGE_TYPE));
this._decreaseWaiting();
return;
}
if (imageFile.size > DRAWING_IMAGE_ALLOW_SIZE) {
reject(new Error(ImageUploadStatusType.ERROR_EXCEED_SIZE));
this._decreaseWaiting();
return;
}
// 获取上传的图片的宽高
const reader = new FileReader();
reader.readAsDataURL(imageFile);
reader.addEventListener('load', evt => {
const replaceSrc = evt.target?.result as string;
if (replaceSrc == null) {
reject(new Error(ImageUploadStatusType.ERROR_IMAGE));
this._decreaseWaiting();
return;
}
const imageId = generateRandomId(6);
resolve({
imageId,
imageSourceType: ImageSourceType.BASE64,
source: replaceSrc,
base64Cache: replaceSrc,
status: ImageUploadStatusType.SUCCUSS,
});
this._decreaseWaiting();
});
});
}
setWaitCount(count: number) {
this._waitCount = count;
this._change$.next(count);
}
private _decreaseWaiting() {
this._waitCount -= 1;
this._change$.next(this._waitCount);
}
private _getSkeleton(unitId: string, subUnitId: string) {
return this._accessor.get(IRenderManagerService)?.getRenderById(unitId)?.with(SheetSkeletonManagerService)?.getOrCreateSkeleton({ sheetId: subUnitId });
}
private _sheet() {
const unit: Nullable<Workbook> = this._univerInstanceService.getCurrentUnitForType<Workbook>(
UniverInstanceType.UNIVER_SHEET,
);
if (!unit) {
return null;
}
const unitId = unit.getUnitId();
const sheet: Worksheet = unit.getActiveSheet();
const subUnitId = sheet?.getSheetId();
return {
unit,
sheet,
unitId,
subUnitId,
};
}
}
@@ -0,0 +1,794 @@
import type { Workbook } from '@univerjs/core';
import type { ITextWatermarkConfig } from '@univerjs/engine-render';
import { Disposable, DisposableCollection, generateRandomId, Inject, Injector, IUniverInstanceService } from '@univerjs/core';
import {
CanvasRenderMode,
DEFAULT_FONTFACE_PLANE,
Engine,
IRenderManagerService,
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 ZqSheetsPrintUiService 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),
});
}
}
@@ -0,0 +1,792 @@
import type { IFreeze, Workbook } from '@univerjs/core';
import type { SpreadsheetSkeleton } from '@univerjs/engine-render';
import { Disposable, Inject, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { IRenderManagerService } from '@univerjs/engine-render';
import { SheetsSelectionsService } from '@univerjs/sheets';
import { SheetPrintInterceptorService, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
import { isEmptyObject, isNullOrUndefined } from '../utils';
import {
DefaultPrintConfig,
JnpfPaperPaddingForType,
JnpfPrintAreaEnum,
JnpfPrintCustomFooterEnum,
JnpfPrintCustomHeaderEnum,
JnpfPrintDirectionEnum,
JnpfPrintFreezeEnum,
JnpfPrintOtherParamsEnum,
JnpfPrintPaperSizeForType,
JnpfPrintScaleEnum,
} from '../utils/define';
import { buildUUID } from '../utils/uuid';
export class ZqSheetsPrintService extends Disposable {
private _configParams: Record<string, any> = {};
private _layoutHeaderFooterParams: any[] = [];
constructor(
@IUniverInstanceService protected readonly _univerInstanceService: IUniverInstanceService,
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
@Inject(SheetsSelectionsService) private readonly _selectionManagerService: SheetsSelectionsService,
@Inject(SheetPrintInterceptorService) private readonly _sheetPrintInterceptorService: SheetPrintInterceptorService,
) {
super();
}
/**
* 根据配置获取打印布局。
* @param {any} printConfig - 打印配置对象,默认为 `DefaultPrintConfig`。
*/
getLayouts(printConfig: any = DefaultPrintConfig) {
// 分析布局的页眉和页脚配置,并将结果存储在 `_layoutHeaderFooterParams` 中
this._layoutHeaderFooterParams = this._analyseLayoutHeaderFooterConfig(printConfig);
// 获取参数出来判断是不是存在冻结
const subUnitId = this._getWorkbook()!.getActiveSheet()?.getSheetId();
if (!subUnitId) {
return;
}
const snapshot = this._getWorkbook()!.save();
let freeze = [];
switch (printConfig.printArea) {
case JnpfPrintAreaEnum.currentSheet: {
const { xSplit = 0, ySplit = 0 } = snapshot?.sheets?.[subUnitId]?.freeze ?? {};
// 正确的冻结状态判断
const colHasFreeze = xSplit > 0; // 列冻结(xSplit 对应列冻结)
const rowHasFreeze = ySplit > 0; // 行冻结(ySplit 对应行冻结)
// 判断冻结状态【判断是单体还是全部,全部的话判断有开就有,没开就没有】
freeze = [
rowHasFreeze && printConfig?.yFreeze && JnpfPrintFreezeEnum.row, // 行冻结
colHasFreeze && printConfig?.xFreeze && JnpfPrintFreezeEnum.column, // 列冻结
].filter(Boolean); // 过滤掉 false/null/undefined
break;
}
case JnpfPrintAreaEnum.partSheets: {
const { sheets } = snapshot;
const { partSheets = [] } = printConfig;
const colHasFreeze =
Object.values(sheets).some((sheet: any) => {
return partSheets.includes(sheet.id) ? sheet?.freeze?.xSplit > 0 : false;
}) && printConfig?.xFreeze;
const rowHasFreeze =
Object.values(sheets).some((sheet: any) => {
return partSheets.includes(sheet.id) ? sheet?.freeze?.ySplit > 0 : false;
}) && printConfig?.yFreeze;
if (colHasFreeze) {
freeze.push('column');
}
if (rowHasFreeze) {
freeze.push('row');
}
break;
}
case JnpfPrintAreaEnum.workbook: {
const { sheets } = snapshot;
const colHasFreeze = Object.values(sheets).some((sheet: any) => sheet?.freeze?.xSplit > 0) && printConfig?.xFreeze;
const rowHasFreeze = Object.values(sheets).some((sheet: any) => sheet?.freeze?.ySplit > 0) && printConfig?.yFreeze;
if (colHasFreeze) {
freeze.push('column');
}
if (rowHasFreeze) {
freeze.push('row');
}
break;
}
// No default
}
// 提取剩余配置
const { xFreeze, yFreeze, ...restPrintConfig } = printConfig ?? {};
// 将配置参数存储在 `_configParams` 中
this._configParams = {
...restPrintConfig,
freeze,
};
// 获取所有需要打印的范围
const printRanges = this._computePrintRanges();
// 初始化空数组,用于存储计算出的布局
const layouts: any[] = [];
// 遍历每个打印范围,计算对应的页面布局
printRanges?.forEach(range => {
// 如果当前范围存在,继续处理
if (range) {
// 计算页面布局
const pageLayout = this._computePageLayout(range);
// 如果页面布局存在,将其添加到 layouts 数组中
if (pageLayout) {
layouts.push(pageLayout);
}
}
});
// 获取纸张大小
const paperSize = this._getPaperSize();
// 获取纸张边距
const paperPadding = this._getPaperPadding();
// 整个需要打印的页码数量
const bookTotalPage = layouts.reduce((total, layout) => total + layout.pages.length, 0);
let bookPageNumber = 1;
const pagesLayout = layouts.flatMap(({ pages: pageRanges, ...info }) =>
pageRanges.map((range: any, rangeIndex: any) => ({
...info,
bookPageNumber: bookPageNumber++,
bookTotalPage,
browserPreviewScale: 1,
layoutId: `layout_${buildUUID()}`,
paperPadding,
paperSize,
range,
sheetPageNumber: rangeIndex + 1, // layout 内部页码从 1 开始
sheetTotalPage: pageRanges?.length,
})),
);
return {
pagesLayout,
paperSize,
printConfig: {
...this._configParams,
currentDateTime: new Date(Date.now() + 8 * 3600 * 1000).toJSON().slice(0, 19).replace('T', ' '),
headerFooterParams: this._layoutHeaderFooterParams,
},
};
}
/**
* 解析页面布局中的页眉和页脚配置。
* @param {any} printConfig - 页眉页脚的配置对象,包含多个布尔值属性来决定是否显示某些信息。
* @returns 返回需要在页眉或页脚中包含的参数数组。
*/
private _analyseLayoutHeaderFooterConfig(printConfig: any) {
// 定义一个数组,用于存储需要包含的页眉和页脚参数
const layoutHeaderFooterParams = [];
if (printConfig.headerFooterType === 'default') {
if (printConfig?.workbookTitle) {
layoutHeaderFooterParams.push(JnpfPrintOtherParamsEnum.workbookTitle);
}
if (printConfig?.worksheetTitle) {
layoutHeaderFooterParams.push(JnpfPrintOtherParamsEnum.worksheetTitle);
}
if (printConfig?.printDate) {
layoutHeaderFooterParams.push(JnpfPrintOtherParamsEnum.printDate);
}
if (printConfig?.printTime) {
layoutHeaderFooterParams.push(JnpfPrintOtherParamsEnum.printTime);
}
if (printConfig?.pageNumber) {
layoutHeaderFooterParams.push(JnpfPrintOtherParamsEnum.pageNumber);
}
} else {
if (printConfig.customHeaderFooterValue?.leftTop) {
layoutHeaderFooterParams.push(JnpfPrintCustomHeaderEnum.leftTop);
}
if (printConfig.customHeaderFooterValue?.centerTop) {
layoutHeaderFooterParams.push(JnpfPrintCustomHeaderEnum.centerTop);
}
if (printConfig.customHeaderFooterValue?.rightTop) {
layoutHeaderFooterParams.push(JnpfPrintCustomHeaderEnum.rightTop);
}
if (printConfig.customHeaderFooterValue?.leftBottom) {
layoutHeaderFooterParams.push(JnpfPrintCustomFooterEnum.leftBottom);
}
if (printConfig.customHeaderFooterValue?.centerBottom) {
layoutHeaderFooterParams.push(JnpfPrintCustomFooterEnum.centerBottom);
}
if (printConfig.customHeaderFooterValue?.rightBottom) {
layoutHeaderFooterParams.push(JnpfPrintCustomFooterEnum.rightBottom);
}
}
// 返回最终解析出来的页眉页脚参数数组
return layoutHeaderFooterParams;
}
/**
* 计算页面布局,根据给定的打印范围和工作表的骨架信息,计算每一页的布局。
* @param printRange {any} 打印范围,包含工作簿信息、工作表信息、打印范围、冻结信息、缩放比例及内容尺寸
* @returns 返回计算出的页面布局信息
*/
private _computePageLayout(printRange: any) {
// 解构打印范围信息
const { contentSize, freeze, printScale, range, subUnitId, unitId } = printRange ?? {};
// 获取工作表的骨架信息
const skeleton = this._getUnitSkeleton(unitId, subUnitId);
if (!skeleton) {
return;
}
// 解构工作表的行高和列宽累计值
const { columnWidthAccumulation, rowHeightAccumulation } = skeleton;
// 解构打印范围的行列信息
const { endColumn, endRow, startColumn, startRow } = range;
const calcRow = (start: any) => {
let end = start;
const offset = start === 0 ? 0 : rowHeightAccumulation[start - 1];
for (; end < endRow; end++) {
const current = (rowHeightAccumulation[end] - offset) * printScale;
const next = (rowHeightAccumulation[end + 1] - offset) * printScale;
if (current > contentSize.h || (current < contentSize.h && next > contentSize.h)) return { endRow: end - 1, startRow: start };
}
return { endRow: end, startRow: start };
};
const calcColumn = (start: any) => {
let end = start;
const offset = start === 0 ? 0 : columnWidthAccumulation[start - 1];
for (; end < endColumn; end++) {
const current = (columnWidthAccumulation[end] - offset) * printScale;
const next = (columnWidthAccumulation[end + 1] - offset) * printScale;
if (current >= contentSize.w || (current < contentSize.w && next > contentSize.w)) {
return { endColumn: end, startColumn: start };
}
}
return { endColumn: end, startColumn: start };
};
// 计算所有行的分页范围
const rows = [];
for (let start = startRow; start <= endRow; ) {
const row = calcRow(start);
rows.push(row);
start = row.endRow + 1; // 移动到下一个分页起始行
}
// 计算所有列的分页范围
const columns: any[] = [];
for (let start = startColumn; start <= endColumn; ) {
const column = calcColumn(start);
columns.push(column);
start = column.endColumn + 1; // 移动到下一个分页起始列
}
// 将行列的分页信息组合成每一页的布局
const pages: any[] = [];
rows.forEach(row => {
columns.forEach(column => {
pages.push({ ...row, ...column });
});
});
// 返回计算的页面布局信息,包括单位ID、子单位ID、页面信息、冻结信息和缩放比例
return { freeze, pages, printScale, subUnitId, unitId };
}
/**
* 计算并返回打印页面的范围,包括内容区域、冻结行列以及打印比例。
* @param {string} subUnitId - 工作表的唯一标识符。
* @param {any} sheetRange - 需要打印的工作表范围,包含起始和结束的行列。
* @returns 返回打印页面的范围、冻结信息、内容区域和打印比例。
*/
private _computePrintPageRange(subUnitId: string, sheetRange: any) {
// 获取工作簿实例
const workbook = this._getWorkbook();
// 如果没有获取到工作簿,返回 undefined
if (!workbook) {
return;
}
// 获取工作簿的唯一标识符
const unitId = workbook!.getUnitId();
// 获取纸张大小
const paperSize = this._getPaperSize();
if (!paperSize) {
return;
}
// 获取纸张的边距
const paperPadding = this._getPaperPadding();
if (!paperPadding) {
return;
}
// 计算内容区域的大小,减去左右、上下的边距
const contentSize = {
h: paperSize.h - (paperPadding.top + paperPadding.bottom),
w: paperSize.w - (paperPadding.left + paperPadding.right),
};
// 如果起始行列小于等于 -1,返回默认的打印范围,表示无效的打印范围
if (sheetRange?.startRow <= -1 || sheetRange?.startColumn <= -1) {
return {
contentSize,
freeze: { startColumn: -1, startRow: -1, xSplit: 0, ySplit: 0 },
printScale: 1,
range: { endColumn: 0, endRow: 0, startColumn: 0, startRow: 0 },
subUnitId,
unitId,
};
}
// 获取工作表实例
const worksheet = workbook!.getSheetBySheetId(subUnitId);
if (!worksheet) {
return;
}
// 获取工作表的冻结信息
const freeze = worksheet?.getFreeze();
if (!freeze) {
return;
}
// 获取工作表的骨架信息(如列宽、行高等)
const skeleton = this._getUnitSkeleton(unitId, subUnitId);
if (!skeleton) {
return;
}
// 计算打印比例
const printScale = this._computePrintScale(skeleton, sheetRange, freeze);
// 初始化打印范围和冻结信息的默认值
let startColumn = -1;
let startRow = -1;
let xSplit = 0;
let ySplit = 0;
// 获取骨架中的列宽和行高累积数据
const { columnWidthAccumulation, rowHeightAccumulation } = skeleton ?? {};
let { endColumn: sheetRangeEndColumn, endRow: sheetRangeEndRow, startColumn: sheetRangeStartColumn, startRow: sheetRangeStartRow } = sheetRange ?? {};
const { startColumn: freezeStartColumn, startRow: freezeStartRow, xSplit: freezeXSplit, ySplit: freezeYSplit } = freeze ?? {};
// 处理冻结行(如果配置了冻结行)
if (freezeYSplit && this._configParams?.freeze.includes(JnpfPrintFreezeEnum?.row)) {
// 如果打印范围的结束行大于等于冻结行,则计算冻结区域的高度并调整打印区域的高度
if (sheetRangeEndRow >= freezeStartRow) {
const offsetHeight = rowHeightAccumulation[freezeStartRow] - rowHeightAccumulation[freezeStartRow - freezeYSplit];
if (offsetHeight < contentSize?.h) {
startRow = freezeStartRow;
ySplit = freezeYSplit;
contentSize.h -= offsetHeight;
}
} else {
// 如果打印范围的结束行小于冻结行,则计算冻结区域的高度并调整打印区域的高度
startRow = sheetRangeEndRow + 1;
ySplit = sheetRangeEndRow + 1 - (freezeStartRow - freezeYSplit);
contentSize.h = contentSize?.h - rowHeightAccumulation[freezeStartRow] - rowHeightAccumulation[freezeStartRow - freezeYSplit];
}
}
// 更新起始行,确保不小于冻结区域的起始行
sheetRangeStartRow = Math.max(startRow, sheetRangeStartRow);
// 处理冻结列(如果配置了冻结列)
if (freezeXSplit && this._configParams?.freeze.includes(JnpfPrintFreezeEnum?.column)) {
// 如果打印范围的结束列大于等于冻结列,则计算冻结区域的宽度并调整打印区域的宽度
if (sheetRangeEndColumn >= freezeStartColumn) {
const offsetWidth =
printScale * (columnWidthAccumulation[freezeStartColumn - 1] - (columnWidthAccumulation[freezeStartColumn - freezeXSplit - 1] || 0));
if (offsetWidth < contentSize?.w) {
startColumn = freezeStartColumn;
xSplit = freezeXSplit;
contentSize.w -= offsetWidth;
}
} else {
// 如果打印范围的结束列小于冻结列,则计算冻结区域的宽度并调整打印区域的宽度
const offsetWidth =
printScale * (columnWidthAccumulation[freezeStartColumn - 1] - (columnWidthAccumulation[freezeStartColumn - freezeXSplit] - 1 || 0));
if (offsetWidth < contentSize?.w) {
const startColumnCache = sheetRangeEndColumn + 1;
const xSplitCache = sheetRangeEndColumn + 1 - (freezeStartColumn - freezeXSplit);
startColumn = startColumnCache;
xSplit = xSplitCache;
contentSize.w -= offsetWidth;
}
}
}
// 更新起始列,确保不小于冻结区域的起始列
sheetRangeStartColumn = Math.max(startColumn, sheetRangeStartColumn);
// 返回计算出的打印页面范围,包含打印区域、冻结信息、内容大小和打印比例
return {
contentSize,
freeze: { startColumn, startRow, xSplit, ySplit },
printScale,
range: { endColumn: sheetRangeEndColumn, endRow: sheetRangeEndRow, startColumn: sheetRangeStartColumn, startRow: sheetRangeStartRow },
subUnitId,
unitId,
};
}
/**
* 计算打印范围,根据配置的区域选择合适的打印范围。
* @returns 返回包含一个或多个打印范围的数组。
*/
private _computePrintRanges() {
// 初始化空数组 result,用于存储计算的打印范围
let result: any[] = [];
// 根据配置的打印区域 执行不同的处理
switch (this._configParams.printArea) {
// 如果区域是当前选择的区域
case JnpfPrintAreaEnum.currentSelection: {
// 计算当前选择区域的打印范围,并返回该范围的数组
result = [this._computePrintSelectionRange()];
break;
}
// 如果区域是当前工作表
case JnpfPrintAreaEnum.currentSheet: {
// 获取当前活动工作表的 ID
const subUnitId = this._getWorkbook()!.getActiveSheet()?.getSheetId();
// 如果工作表 ID 不存在,则跳出
if (!subUnitId) {
break;
}
// 计算当前工作表的打印范围
const range = this._computePrintSheetRange(subUnitId);
// 如果计算结果有效,返回一个包含该范围的数组
result = range ? [range] : [];
break;
}
// 如果区域是部分工作簿
case JnpfPrintAreaEnum.partSheets: {
const partSheets = new Set(this._configParams.partSheets ?? []);
// 获取工作簿中的所有工作表
const sheets = this._getWorkbook()!.getSheets();
const targetSheets = sheets.filter(sheet => partSheets.has(sheet?.getSheetId()));
// 对每个工作表计算打印范围,并过滤掉无效的范围
result = targetSheets
?.map(sheet => {
const subUnitId = sheet?.getSheetId();
// 对每个工作表计算并返回其打印范围
return this._computePrintSheetRange(subUnitId);
})
.filter(Boolean); // 过滤掉无效值(如 null 或 undefined
break;
}
// 如果区域是整个工作簿
case JnpfPrintAreaEnum.workbook: {
// 获取工作簿中的所有工作表
const sheets = this._getWorkbook()!.getSheets();
// 对每个工作表计算打印范围,并过滤掉无效的范围
result = sheets
?.map(sheet => {
const subUnitId = sheet?.getSheetId();
// 对每个工作表计算并返回其打印范围
return this._computePrintSheetRange(subUnitId);
})
.filter(Boolean); // 过滤掉无效值(如 null 或 undefined
break;
}
// 如果区域类型不匹配,返回空数组
default: {
break;
}
}
// 返回计算出的打印范围数组
return result;
}
/**
* 计算打印比例。
* @param {SpreadsheetSkeleton} skeleton - 工作表的骨架信息,包括行高、列宽累积数据。
* @param {any} sheetRange - 打印的单元格范围,包括起始行、起始列、结束行、结束列。
* @param {IFreeze} freeze - 冻结范围信息,包括冻结的起始行列和冻结的分割范围。
* @returns 返回适合的打印缩放比例,范围在 [0, 1] 之间。
*/
private _computePrintScale(skeleton: SpreadsheetSkeleton, sheetRange: any, freeze: IFreeze) {
// 解构骨架中的列宽和行高累积信息
const { columnWidthAccumulation, rowHeightAccumulation } = skeleton ?? {};
// 骨架信息为空时,返回默认比例 1
if (isNullOrUndefined(columnWidthAccumulation) || isNullOrUndefined(rowHeightAccumulation)) {
return 1;
}
// 解构打印范围的起始和结束行列
const { endColumn: sheetRangeEndColumn, endRow: sheetRangeEndRow, startColumn: sheetRangeStartColumn, startRow: sheetRangeStartRow } = sheetRange ?? {};
// 打印范围信息为空时,返回默认比例 1
if (
isNullOrUndefined(sheetRangeStartRow) ||
isNullOrUndefined(sheetRangeStartColumn) ||
isNullOrUndefined(sheetRangeEndRow) ||
isNullOrUndefined(sheetRangeEndColumn)
) {
return 1;
}
// 解构冻结范围的起始行列和分割范围
const { startColumn: freezeStartColumn, startRow: freezeStartRow, xSplit: freezeXSplit, ySplit: freezeYSplit } = freeze ?? {};
// 冻结范围信息为空时,返回默认比例 1
if (isNullOrUndefined(freezeXSplit) || isNullOrUndefined(freezeYSplit) || isNullOrUndefined(freezeStartRow) || isNullOrUndefined(freezeStartColumn)) {
return 1;
}
// 确定打印范围的起始行:对冻结范围与打印范围取交集
const startRow = sheetRangeEndRow < freezeStartRow ? sheetRangeStartRow : Math.max(freezeStartRow, sheetRangeStartRow);
// 确定打印范围的起始列:对冻结范围与打印范围取交集
const startColumn = sheetRangeEndColumn < freezeStartColumn ? sheetRangeStartColumn : Math.max(freezeStartColumn, sheetRangeStartColumn);
// 获取纸张尺寸
const paperSize = this._getPaperSize();
if (!paperSize) {
return 1;
}
// 获取纸张边距
const paperPadding = this._getPaperPadding();
if (!paperPadding) {
return 1;
}
// 计算冻结区域的宽度(列)和高度(行)
const xSplit = freezeXSplit > 0 ? columnWidthAccumulation[freezeStartColumn - 1] - (columnWidthAccumulation[freezeStartColumn - freezeXSplit - 1] || 0) : 0;
const ySplit = freezeYSplit > 0 ? rowHeightAccumulation[freezeStartRow - 1] - (rowHeightAccumulation[freezeStartRow - freezeYSplit - 1] || 0) : 0;
// 计算打印范围的高度和宽度
const rowHeight = rowHeightAccumulation[sheetRangeEndRow] - (rowHeightAccumulation[startRow - 1] || 0);
const columnWidth = columnWidthAccumulation[sheetRangeEndColumn] - (columnWidthAccumulation[startColumn - 1] || 0);
// 加上冻结区域的尺寸,得到总的打印高度和宽度
const xScale = columnWidth + xSplit;
const yScale = rowHeight + ySplit;
let result = 1;
// 根据配置参数计算打印比例
switch (this._configParams.printScale) {
case JnpfPrintScaleEnum.fitHeight: {
// 高度适应比例
result = Math.min(1, (paperSize.h - paperPadding.top - paperPadding.bottom) / yScale);
break;
}
case JnpfPrintScaleEnum.fitPage: {
// 页面完全适应比例(宽度和高度都适应)
result = Math.min(1, (paperSize.w - paperPadding.left - paperPadding.right) / xScale, (paperSize.h - paperPadding.top - paperPadding.bottom) / yScale);
break;
}
case JnpfPrintScaleEnum.fitWidth: {
// 宽度适应比例
result = Math.min(1, (paperSize.w - paperPadding.left - paperPadding.right) / xScale);
break;
}
default: {
break;
}
}
// 返回比例值,保留两位小数
return Math.floor(result * 100) / 100;
}
/**
* 计算并返回打印选择范围的详细信息。
* @returns 返回选区的打印范围信息。
*/
private _computePrintSelectionRange() {
// 获取当前工作簿实例
const workbook = this._getWorkbook();
// 如果没有获取到工作簿,返回 undefined
if (!workbook) {
return;
}
// 获取工作簿的唯一标识符
const unitId = workbook!.getUnitId();
// 获取当前活动工作表
const worksheet = workbook!.getActiveSheet();
// 如果没有获取到工作表,返回 undefined
if (!worksheet) {
return;
}
// 获取当前工作表的唯一标识符(subUnitId)
const subUnitId = worksheet?.getSheetId();
// 获取当前工作簿的最后选择区域
const lastSelection = this._selectionManagerService?.getWorkbookSelections(unitId)?.getCurrentLastSelection();
// 如果没有获取到选择区域,返回 undefined
if (!lastSelection) {
return;
}
// 根据最后选择的范围计算打印的页面范围
return this._computePrintPageRange(subUnitId, lastSelection?.range);
}
/**
* 计算工作表的打印范围。
* @param {string} subUnitId - 工作表的唯一标识符。
* @returns 返回计算出的打印范围,包含页面范围、冻结信息等。
*/
private _computePrintSheetRange(subUnitId: string) {
// 获取工作簿实例
const workbook = this._getWorkbook();
// 如果没有获取到工作簿,返回 undefined
if (!workbook) {
return;
}
// 获取工作簿的唯一标识符
const unitId = workbook!.getUnitId();
// 获取当前工作表
const worksheet = workbook!.getSheetBySheetId(subUnitId);
// 如果工作表不存在,返回 undefined
if (!worksheet) {
return;
}
// 获取工作表的骨架信息,包含如行列宽度、冻结状态等数据
const skeleton = this._getUnitSkeleton(unitId, subUnitId);
// 如果工作表的骨架信息不存在,返回 undefined
if (!skeleton) {
return;
}
// 获取工作表的冻结信息,包括冻结行列位置
const freeze = worksheet?.getFreeze();
// 如果冻结信息不存在,返回 undefined
if (!freeze) {
return;
}
// 获取当前工作表的单元格矩阵打印范围
const cellMatrixPrintRange = worksheet?.getCellMatrixPrintRange();
// 如果没有获取到单元格矩阵打印范围,返回 undefined
if (!cellMatrixPrintRange) {
return;
}
// 获取当前工作表骨架中的溢出缓存数据
const { overflowCache } = skeleton;
if (overflowCache) {
// 如果有溢出缓存,遍历其值并调整打印范围
overflowCache.forValue((_row: any, _col: any, value: any) => {
const { endColumn } = value;
// 如果溢出的单元格列超过当前打印范围的结束列,则调整打印范围
if (endColumn > cellMatrixPrintRange?.endColumn) {
cellMatrixPrintRange.endColumn = endColumn;
}
});
}
// 使用拦截器调整打印范围
const realCellMatrixPrintRange = this._sheetPrintInterceptorService.interceptor?.fetchThroughInterceptors(
this._sheetPrintInterceptorService.interceptor?.getInterceptPoints()?.PRINTING_RANGE,
)(cellMatrixPrintRange, { subUnitId, unitId });
// 返回最终计算出的打印页面范围,包括冻结行列的调整
return this._computePrintPageRange(subUnitId, {
// 调整起始行列位置,确保不小于零
...realCellMatrixPrintRange,
startColumn: Math.max(freeze.startColumn - freeze.xSplit, 0),
startRow: Math.max(freeze.startRow - freeze.ySplit, 0),
});
}
/**
* 获取当前配置的纸张边距信息。
* @returns 返回包含边距信息的对象,或者返回 `null`。
*/
private _getPaperPadding() {
// 从配置参数中解构获取边距类型(padding)
const { padding } = this._configParams ?? {};
// 根据边距类型从预定义的边距配置映射中查找对应的边距值
const paperPadding = JnpfPaperPaddingForType[padding as keyof typeof JnpfPaperPaddingForType] ?? {};
// 如果找到有效的边距配置,返回边距配置;否则返回 null
return isEmptyObject(paperPadding) ? null : paperPadding;
}
/**
* 获取当前配置的纸张大小。
* @returns 返回包含宽度和高度的纸张尺寸对象,或者返回 `null`。
*/
private _getPaperSize() {
// 从配置参数中解构获取纸张类型(paperType)和方向(direction
const { direction, paperType } = this._configParams ?? {};
// 根据纸张类型获取对应的尺寸数据
const paperSize = JnpfPrintPaperSizeForType[paperType as keyof typeof JnpfPrintPaperSizeForType] ?? {};
// 如果纸张尺寸为空,返回 null
if (isEmptyObject(paperSize)) {
return null;
}
// 根据打印方向返回调整后的纸张尺寸
return direction === JnpfPrintDirectionEnum?.portrait
? { ...paperSize } // 如果是纵向模式,直接返回原始纸张尺寸
: {
h: paperSize?.w,
w: paperSize?.h, // 如果是横向模式,交换宽度和高度
};
}
/**
* 获取指定单元的骨架信息。
* @param {string} unitId - 单元的唯一标识符。
* @param {string} subUnitId - 子单元的唯一标识符。
* @returns 返回单元骨架信息对象,如果未找到则返回空对象。
*/
private _getUnitSkeleton(unitId: string, subUnitId: string) {
// 从 `RenderManagerService` 中获取指定单元 ID 的渲染服务
return (this._renderManagerService
?.getRenderById(unitId) // 根据单元 ID 获取对应的渲染服务
?.with(SheetSkeletonManagerService) // 使用骨架管理服务进行扩展
?.getUnitSkeleton(unitId, subUnitId)?.skeleton ?? {}) as any; // 获取单元对应的骨架信息 // 如果骨架信息不存在,则返回空对象 // 强制将返回值类型设置为 `any`
}
/**
* 获取当前活动的工作簿实例。
* @returns 返回当前活动的工作簿实例,如果不存在则返回 `undefined`。
*/
private _getWorkbook() {
// 使用 `UniverInstanceService` 获取当前类型为 `UNIVER_SHEET` 的活动单元实例
return this._univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET);
}
}
@@ -0,0 +1,38 @@
import { Disposable, ICommandService, IUniverInstanceService, UniverInstanceType, Workbook } from '@univerjs/core';
import { SetSelectionsOperation } from '@univerjs/sheets';
export class ZqSheetsRangeService extends Disposable {
constructor(
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@ICommandService private readonly _commandService: ICommandService,
) {
super();
}
// 恢复选区
public recoveryRange(row: number, col: number) {
const unit = this._univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET);
const unitId = unit!.getUnitId();
const sheet = unit!.getActiveSheet();
const subUnitId = sheet?.getSheetId();
this._commandService
.executeCommand(SetSelectionsOperation.id, {
selections: [
{
range: {
endColumn: col,
endRow: row,
rangeType: 0,
startColumn: col,
startRow: row,
},
},
],
subUnitId,
type: 2,
unitId,
})
.then();
}
}
@@ -0,0 +1,79 @@
export type CodeDisplayType = 'default' | 'jsbarcode' | 'qrCode';
export interface CodeCellTarget {
col: number;
content: string;
custom: Record<string, any>;
displayType: 'jsbarcode' | 'qrCode';
row: number;
}
export function resolveCodeDisplayType(custom: Record<string, any> | undefined): CodeDisplayType {
if (!custom) {
return 'default';
}
if (custom.type === 'qrCode') {
return 'qrCode';
}
if (custom.type === 'jsbarcode') {
return 'jsbarcode';
}
const dt = String(custom.displayType || 'default');
if (dt === 'qrCode' || dt === 'jsbarcode') {
return dt;
}
return 'default';
}
export function resolveCodeContent(custom: Record<string, any>, v: unknown): string {
const raw = v ?? custom?.field ?? '';
if (raw === null || raw === undefined) {
return '';
}
const text = String(raw);
if (custom?.type === 'dataSource') {
const m = text.match(/^\$\{(.+)\}$/);
if (m) {
return m[1] || custom.field || text;
}
}
return text;
}
export function scanCodeCellTargets(
matrix: Record<string, any> | null | undefined,
): CodeCellTarget[] {
if (!matrix) {
return [];
}
const targets: CodeCellTarget[] = [];
Object.entries(matrix).forEach(([rowStr, colObj]) => {
const row = Number(rowStr);
if (!colObj || typeof colObj !== 'object') {
return;
}
Object.entries(colObj as Record<string, any>).forEach(([colStr, cellData]) => {
if (!cellData || typeof cellData !== 'object') {
return;
}
const custom = cellData.custom || {};
const displayType = resolveCodeDisplayType(custom);
if (displayType !== 'qrCode' && displayType !== 'jsbarcode') {
return;
}
const content = resolveCodeContent(custom, cellData.v);
if (!content.trim()) {
return;
}
targets.push({
row,
col: Number(colStr),
cellData,
custom,
displayType,
content,
});
});
});
return targets;
}
@@ -0,0 +1,22 @@
/** 插入悬浮图片时的默认占位图(无品牌标识) */
const DEFAULT_FLOAT_IMAGE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300" viewBox="0 0 300 300" role="img" aria-label="image placeholder">
<defs>
<linearGradient id="zq-float-img-bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#f8fafc"/>
<stop offset="100%" stop-color="#e2e8f0"/>
</linearGradient>
</defs>
<rect width="300" height="300" fill="url(#zq-float-img-bg)"/>
<rect x="48" y="56" width="204" height="168" rx="12" fill="#ffffff" stroke="#94a3b8" stroke-width="2" stroke-dasharray="10 8"/>
<circle cx="108" cy="118" r="20" fill="#cbd5e1"/>
<path d="M72 200 L128 148 L176 172 L228 124 L252 200 Z" fill="#94a3b8"/>
</svg>`;
export const DEFAULT_FLOAT_IMAGE_SRC = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(
DEFAULT_FLOAT_IMAGE_SVG,
)}`;
export const DefaultFloatImageOption = {
alt: '',
src: DEFAULT_FLOAT_IMAGE_SRC,
};
@@ -0,0 +1,790 @@
import type { ITextWatermarkConfig } from '@univerjs/engine-render';
export const ZqCommandIds = {
downloadExcelFile: 'jnpf-sheets.operation.download-excel-file',
excelFileOperations: 'jnpf-sheets.operation.excel-file',
floatEchartOperations: 'jnpf-sheets.operation.float-echart',
focusFloatEchart: 'jnpf-sheets.operation.focus-float-echart',
focusFloatImage: 'jnpf-sheets.operation.focus-float-image',
importCsvFile: 'jnpf-sheets.operation.import-csv-file',
importExcelFile: 'jnpf-sheets.operation.import-excel-file',
insertedFloatEchart: 'jnpf-sheets.operation.inserted-float-echart',
insertedFloatImage: 'jnpf-sheets.operation.inserted-float-image',
insertFloatBarEchart: 'jnpf-sheets.operation.insert-float-bar-echart',
insertFloatImage: 'jnpf-sheets.operation.insert-float-image',
insertFloatLineEchart: 'jnpf-sheets.operation.insert-float-line-echart',
insertFloatPieEchart: 'jnpf-sheets.operation.insert-float-pie-echart',
insertFloatRadarEchart: 'jnpf-sheets.operation.insert-float-radar-echart',
preview: 'jnpf-sheets.operation.preview',
print: 'jnpf-sheets.operation.print',
};
export const DefaultFloatEchartWidth = 300;
export const DefaultFloatEchartHeight = 300;
export const DefaultFloatImageWidth = 300;
export const DefaultFloatImageHeight = 300;
/** ZQ 单体报表服务 Excel 导入 APIJNPF 走 reportHttp 微服务) */
export const ZQ_REPORT_IMPORT_EXCEL_URL =
'/api/online_dev/report/data/import-excel';
export const ZqUniverFloatEchartKey = 'ZqUniverFloatEchart';
export const ZqUniverFloatImageKey = 'ZqUniverFloatImage';
export const ZqUniverDialogSelectCellKey = 'ZqUniverDialogSelectCell';
// 等待废弃
export const SheetMainFillDirectionExtensionKey = 'SheetMainFillDirectionExtension';
export const SheetMainRelationCellExtensionKey = 'SheetMainRelationCellExtension';
export const DefaultDialogSelectCellConfig = {
closable: false,
destroyOnClose: false,
draggable: true,
maskClosable: false,
mask: false,
preservePositionOnDestroy: true,
title: { title: '请选择单元格' },
width: 326,
};
export const DefaultWatermarkConfig: ITextWatermarkConfig = {
bold: false,
color: '#000000',
content: 'ZQ',
direction: 'inherit',
fontSize: 16,
italic: false,
opacity: 0.1,
repeat: true,
rotate: -30,
spacingX: 200, // X 轴间距
spacingY: 150, // Y 轴间距
x: 0,
y: 0,
};
export enum JnpfPrintAreaEnum {
currentSelection = 'currentSelection',
currentSheet = 'currentSheet',
partSheets = 'partSheets',
workbook = 'workbook',
}
export const jnpfPrintAreaOptions = [
// { fullName: '所有工作表', id: JnpfPrintAreaEnum.workbook },
// { fullName: '部分工作表', id: JnpfPrintAreaEnum.partSheets },
{ fullName: '当前工作表', id: JnpfPrintAreaEnum.currentSheet },
];
export enum JnpfPaperTypeEnum {
a3 = 'a3',
a4 = 'a4',
a5 = 'a5',
b4 = 'b4',
b5 = 'b5',
executive = 'executive',
folio = 'folio',
legal = 'legal',
letter = 'letter',
statement = 'statement',
tabloid = 'tabloid',
}
export const jnpfPaperTypeOptions = [
{
fullName: 'A329.7厘米 x 42.0厘米)',
id: JnpfPaperTypeEnum.a3,
},
{
fullName: 'A421.0厘米 x 29.7厘米)',
id: JnpfPaperTypeEnum.a4,
},
{
fullName: 'A514.8厘米 x 21.0厘米)',
id: JnpfPaperTypeEnum.a5,
},
{
fullName: 'B425.0厘米 x 35.3厘米)',
id: JnpfPaperTypeEnum.b4,
},
{
fullName: 'B517.6厘米 x 25.0厘米)',
id: JnpfPaperTypeEnum.b5,
},
{
fullName: '信纸(21.6厘米 x 27.9厘米)',
id: JnpfPaperTypeEnum.letter,
},
{
fullName: '行政公文纸(18.4厘米 x 26.7厘米)',
id: JnpfPaperTypeEnum.executive,
},
{
fullName: '报表(14.0厘米 x 21.6厘米)',
id: JnpfPaperTypeEnum.statement,
},
];
export const printA4PaperWidth = 794;
export const printA4PaperHeight = 1124;
export const JnpfPrintPaperSizeForType = {
[JnpfPaperTypeEnum.a3]: { h: 1587, w: 1123 },
[JnpfPaperTypeEnum.a4]: { h: printA4PaperHeight, w: printA4PaperWidth },
[JnpfPaperTypeEnum.a5]: { h: 794, w: 559 },
[JnpfPaperTypeEnum.b4]: { h: 1344, w: 944 },
[JnpfPaperTypeEnum.b5]: { h: 944, w: 665 },
[JnpfPaperTypeEnum.executive]: { h: 1008, w: 696 },
[JnpfPaperTypeEnum.folio]: { h: 1248, w: 816 },
[JnpfPaperTypeEnum.legal]: { h: 1344, w: 816 },
[JnpfPaperTypeEnum.letter]: { h: 1056, w: 816 },
[JnpfPaperTypeEnum.statement]: { h: 816, w: 528 },
[JnpfPaperTypeEnum.tabloid]: { h: 1632, w: 1056 },
};
export enum JnpfPrintDirectionEnum {
landscape = 'landscape',
portrait = 'portrait',
}
export const jnpfPrintDirectionOptions = [
{
fullName: '纵向',
id: JnpfPrintDirectionEnum.portrait,
},
{
fullName: '横向',
id: JnpfPrintDirectionEnum.landscape,
},
];
export enum JnpfPrintScaleEnum {
fitHeight = 'fitHeight',
fitPage = 'fitPage',
fitWidth = 'fitWidth',
origin = 'origin',
}
export const jnpfPrintScaleOptions = [
{
fullName: '正常(100%',
id: JnpfPrintScaleEnum.origin,
},
{
fullName: '适合宽度',
id: JnpfPrintScaleEnum.fitWidth,
},
{
fullName: '适合高度',
id: JnpfPrintScaleEnum.fitHeight,
},
{
fullName: '适合页面大小',
id: JnpfPrintScaleEnum.fitPage,
},
];
export enum JnpfPrintAlignEnum {
end = 'end',
middle = 'middle',
start = 'start',
}
export const jnpfPrintVAlignOptions = [
{
fullName: '顶部对齐',
id: JnpfPrintAlignEnum.start,
},
{
fullName: '垂直居中',
id: JnpfPrintAlignEnum.middle,
},
{
fullName: '底部对齐',
id: JnpfPrintAlignEnum.end,
},
];
export const jnpfPrintHAlignOptions = [
{
fullName: '左对齐',
id: JnpfPrintAlignEnum.start,
},
{
fullName: '水平居中',
id: JnpfPrintAlignEnum.middle,
},
{
fullName: '右对齐',
id: JnpfPrintAlignEnum.end,
},
];
export enum JnpfPaperPaddingTypeEnum {
narrow = 'narrow',
normal = 'normal',
wide = 'wide',
}
export const jnpfPaperPaddingTypeOptions = [
{
fullName: '正常',
id: JnpfPaperPaddingTypeEnum.normal,
},
{
fullName: '窄',
id: JnpfPaperPaddingTypeEnum.narrow,
},
{
fullName: '宽',
id: JnpfPaperPaddingTypeEnum.wide,
},
];
export const JnpfPaperPaddingForType = {
[JnpfPaperPaddingTypeEnum.narrow]: { bottom: 56, left: 19, right: 19, top: 56 },
[JnpfPaperPaddingTypeEnum.normal]: { bottom: 56, left: 53, right: 53, top: 56 },
[JnpfPaperPaddingTypeEnum.wide]: { bottom: 75, left: 75, right: 75, top: 75 },
};
export enum JnpfPrintFreezeEnum {
column = 'column',
row = 'row',
}
export enum JnpfPrintOtherParamsEnum {
pageNumber = 'pageNumber',
printDate = 'printDate',
printTime = 'printTime',
workbookTitle = 'workbookTitle',
worksheetTitle = 'worksheetTitle',
}
export enum JnpfPrintCustomHeaderEnum {
centerTop = 'centerTop',
leftTop = 'leftTop',
rightTop = 'rightTop',
}
export enum JnpfPrintCustomFooterEnum {
centerBottom = 'centerBottom',
leftBottom = 'leftBottom',
rightBottom = 'rightBottom',
}
// 打印默认的配置
export const DefaultPrintConfig = {
customHeaderFooterValue: {
centerBottom: '',
centerTop: '',
leftBottom: '',
leftTop: '',
rightBottom: '',
rightTop: '',
},
direction: JnpfPrintDirectionEnum.portrait, // 页面方向
gridlines: true, // 网格线
hAlign: JnpfPrintAlignEnum.middle, // 水平对齐方式
headerFooterType: 'default', // 页眉页脚类型
padding: JnpfPaperPaddingTypeEnum.normal, // 边距
pageNumber: false, // 页码
paperType: JnpfPaperTypeEnum.a4, // 纸张类型
partSheets: [], // 部分工作表
printArea: JnpfPrintAreaEnum.currentSheet, // 打印范围
printDate: false, // 当前日期
printScale: JnpfPrintScaleEnum.origin, // 缩放
printTime: false, // 当前时间
vAlign: JnpfPrintAlignEnum.start, // 垂直对齐方式
workbookTitle: false, // 是否显示工作簿标题
workbookTitleText: '', // 工作簿标题文本
worksheetTitle: false, // 工作本名称
xFreeze: false, // 重复冻结列
yFreeze: false, // 重复冻结行
};
export { DefaultFloatImageOption } from './default-float-image';
export const DefaultFloatEchartOptions = {
bar: {
animation: false,
bar: { barTypeList: [] },
barWidth: 25, // 宽度设置
chartType: 'bar',
classifyNameField: undefined,
// 自定义配色
color: {
AxisLineStyleColor: '', // X、Y轴轴线颜色全局设置
AxisTextStyleColor: '', // X、Y轴字体的颜色全局设置
list: [],
},
// 坐标轴边距设置
grid: {
bottom: 50,
left: 30,
right: 10,
top: 60,
},
itemStyle: {
borderRadius: 0, // 弧度设置
},
// 数值设置
label: {
backgroundColor: '', // 数值背景色
color: '#303133', // 数值颜色
fontSize: 14, // 数值大小
fontWeight: '', // 数值加粗
seriesCenterLeft: 50,
seriesCenterTop: 50,
seriesLabelPosition: 'outside',
seriesLabelShowInfo: ['count', 'percent'],
seriesLineStyleWidth: 2,
seriesSymbolRotate: 4,
show: false, // 显示数值
},
// 图例设置
legend: {
fontSize: 14,
left: '40%',
orient: 'horizontal',
show: true,
textStyle: {
fontSize: 14,
},
top: '90%',
},
legendLeft: 40,
legendTop: 90,
series: [
{
data: [5, 20, 36, 10, 10, 20],
name: '销量',
type: 'bar',
},
{
data: [10, 30, 90, 30, 30, 80],
name: '销量1',
type: 'bar',
},
],
seriesDataField: undefined,
seriesNameField: undefined,
styleType: 1, // 风格类型
summaryType: 'none',
// 标题设置
title: {
left: 'center',
show: true,
showSub: true,
subtext: '子标题',
subtextI18nCode: '',
subtextStyle: {
color: '#aaa',
fontSize: 12,
fontWeight: 'normal',
},
text: '柱状图示例',
textI18nCode: '',
textStyle: { color: '#333', fontSize: 18, fontWeight: 'normal' },
},
// 提示语设置
tooltip: {
backgroundColor: '#fff',
show: true,
textStyle: {
color: '#303133',
fontSize: 14,
fontWeight: 'normal',
},
},
xAxis: {
axisLabel: {
color: '#303133', // X轴标签字体颜色
fontSize: 14, // X轴标签字体大小
fontWeight: '', // X轴标签是否加粗
rotate: 0, // X轴标签角度
},
axisLine: {
lineStyle: {
color: '#303133', // X轴线颜色
},
show: true,
},
axisTick: { alignWithLabel: false }, // 刻度对齐
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'],
inverse: false, // X轴反转
name: '', // X轴名称
nameTextStyle: {
color: '#303133', // X轴名称字体颜色
fontSize: 14, // X轴名称字体大小
fontWeight: '', // X轴名称是否加粗
},
show: true, // X轴是否显示
splitLine: {
lineStyle: {
color: '#DFDFDF', // X轴网格线颜色
},
show: false, // X轴网格线
},
type: 'category', // X轴类型[category,value]
},
yAxis: {
axisLabel: {
color: '#303133', // y轴标签字体颜色
fontSize: 14, // y轴标签字体大小
fontWeight: '', // y轴标签是否加粗
rotate: 0, // y轴标签角度
},
axisLine: {
lineStyle: {
color: '#303133', // y轴线颜色
},
show: true,
},
inverse: false, // y轴反转
name: '', // y轴名称
nameTextStyle: {
color: '#303133', // y轴名称字体颜色
fontSize: 14, // y轴名称字体大小
fontWeight: '', // y轴名称是否加粗
},
show: true, // y轴是否显示
splitLine: {
lineStyle: {
color: '#DFDFDF', // y轴网格线颜色
},
show: false, // y轴网格线
},
},
},
line: {
animation: false,
areaStyle: false, // 面积堆积
chartType: 'line',
classifyNameField: undefined,
// 自定义配色
color: {
AxisLineStyleColor: '', // X、Y轴轴线颜色全局设置
AxisTextStyleColor: '', // X、Y轴字体的颜色全局设置
list: [],
},
// 坐标轴边距设置
grid: {
bottom: 50,
left: 30,
right: 10,
top: 60,
},
label: {
backgroundColor: '', // 数值背景色
color: '#303133', // 数值颜色
fontSize: 14, // 数值大小
fontWeight: '', // 数值加粗
show: false, // 显示数值
},
// 图例设置
legend: {
fontSize: 14,
left: '40%',
orient: 'horizontal',
show: true,
textStyle: {
fontSize: 14,
},
top: '90%',
},
legendLeft: 40,
legendTop: 90,
line: {
smooth: false,
symbolSize: 4, // 点的大小
},
lineStyle: {
Width: 2, // 线条宽度
},
series: [
{
data: [5, 20, 36, 10, 10, 20],
name: '销量',
type: 'line',
},
{
data: [10, 30, 90, 30, 30, 80],
name: '销量1',
type: 'line',
},
],
seriesDataField: undefined,
seriesNameField: undefined,
styleType: 1, // 风格类型.
summaryType: 'none',
// 标题设置
title: {
left: 'center',
show: true,
showSub: true,
subtext: '子标题',
subtextI18nCode: '',
subtextStyle: {
color: '#aaa',
fontSize: 12,
fontWeight: 'normal',
},
text: '折线图示例',
textI18nCode: '',
textStyle: { color: '#333', fontSize: 18, fontWeight: 'normal' },
},
// 提示语设置
tooltip: {
backgroundColor: '#fff',
show: true,
textStyle: {
color: '#303133',
fontSize: 14,
fontWeight: 'normal',
},
},
xAxis: {
axisLabel: {
color: '#303133', // X轴标签字体颜色
fontSize: 14, // X轴标签字体大小
fontWeight: '', // X轴标签是否加粗
rotate: 0, // X轴标签角度
},
axisLine: {
lineStyle: {
color: '#303133', // X轴线颜色
},
show: true,
},
axisTick: { alignWithLabel: false }, // 刻度对齐
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'],
inverse: false, // X轴反转
name: '', // X轴名称
nameTextStyle: {
color: '#303133', // X轴名称字体颜色
fontSize: 14, // X轴名称字体大小
fontWeight: '', // X轴名称是否加粗
},
show: true, // X轴是否显示
splitLine: {
lineStyle: {
color: '#DFDFDF', // X轴网格线颜色
},
show: false, // X轴网格线
},
type: 'category', // X轴类型[category,value]
},
yAxis: {
axisLabel: {
color: '#303133', // y轴标签字体颜色
fontSize: 14, // y轴标签字体大小
fontWeight: '', // y轴标签是否加粗
rotate: 0, // y轴标签角度
},
axisLine: {
lineStyle: {
color: '#303133', // y轴线颜色
},
show: true,
},
inverse: false, // y轴反转
name: '', // y轴名称
nameTextStyle: {
color: '#303133', // y轴名称字体颜色
fontSize: 14, // y轴名称字体大小
fontWeight: '', // y轴名称是否加粗
},
show: true, // y轴是否显示
splitLine: {
lineStyle: {
color: '#DFDFDF', // y轴网格线颜色
},
show: false, // y轴网格线
},
},
},
pie: {
animation: false,
chartType: 'pie',
classifyNameField: undefined,
// 自定义配色
color: {
AxisLineStyleColor: '', // X、Y轴轴线颜色全局设置
AxisTextStyleColor: '', // X、Y轴字体的颜色全局设置
list: [],
},
label: {
backgroundColor: '', // 数值背景色
color: '#303133', // 数值颜色
fontSize: 14, // 数值大小
fontWeight: '', // 数值加粗
formatter: '{b}: {c} ({d}%)',
show: true, // 显示数值
},
// 图例设置
legend: {
fontSize: 14,
orient: 'horizontal',
show: true,
textStyle: {
fontSize: 14,
},
top: 'bottom',
},
pie: { roseType: false },
series: [
{
data: [
{ name: '衬衫', value: 5 },
{ name: '羊毛衫', value: 20 },
{ name: '雪纺衫', value: 36 },
{ name: '裤子', value: 10 },
{ name: '高跟鞋', value: 10 },
{ name: '袜子', value: 20 },
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.5)',
shadowOffsetX: 0,
},
},
label: {
formatter: '{b}: {c} ({d}%)',
show: true, // 显示数值
},
name: '衣物',
radius: '50%',
type: 'pie',
},
],
seriesCenter: {
seriesCenterLeft: 50,
seriesCenterTop: 50,
},
seriesDataField: undefined,
seriesLabelPosition: 'outside',
seriesLabelShowInfo: ['count', 'percent'],
seriesNameField: undefined,
styleType: 1, // 风格类型
summaryType: 'none',
// 标题设置
title: {
left: 'center',
show: true,
showSub: true,
subtext: '子标题',
subtextI18nCode: '',
subtextStyle: {
color: '#aaa',
fontSize: 12,
fontWeight: 'normal',
},
text: '饼图示例',
textI18nCode: '',
textStyle: { color: '#333', fontSize: 18, fontWeight: 'normal' },
},
tooltip: {
backgroundColor: '#fff',
show: true,
textStyle: {
color: '#303133',
fontSize: 14,
fontWeight: 'normal',
},
},
},
radar: {
animation: false,
classifyNameField: undefined,
// 自定义配色
color: {
AxisLineStyleColor: '', // X、Y轴轴线颜色全局设置
AxisTextStyleColor: '', // X、Y轴字体的颜色全局设置
list: [],
},
label: {
backgroundColor: '', // 数值背景色
color: '#303133', // 数值颜色
fontSize: 14, // 数值大小
fontWeight: '', // 数值加粗
show: false, // 显示数值
},
// 图例设置
legend: {
fontSize: 14,
orient: 'horizontal',
show: true,
textStyle: {
fontSize: 14,
},
top: 'bottom',
},
maxField: undefined,
radar: {
axisName: {
color: '#303133',
fontSize: 14,
fontWeight: 'normal',
},
indicator: [
{ max: 6500, name: '销售' },
{ max: 16_000, name: '行政管理' },
{ max: 30_000, name: '信息技术' },
{ max: 38_000, name: '客户支持' },
{ max: 52_000, name: '开发' },
{ max: 25_000, name: '市场营销' },
],
},
series: [
{
areaStyle: { opacity: 0.3 },
data: [
{
name: '分配预算',
value: [4200, 3000, 20_000, 35_000, 50_000, 18_000],
},
],
name: '预算',
type: 'radar',
},
],
seriesAreaStyleOpacity: 0.3,
seriesCenter: {
seriesCenterLeft: 50,
seriesCenterTop: 50,
},
seriesDataField: undefined,
seriesNameField: undefined,
styleType: 1, // 风格类型
summaryType: 'none',
// 标题设置
title: {
left: 'center',
show: true,
showSub: true,
subtext: '子标题',
subtextI18nCode: '',
subtextStyle: {
color: '#aaa',
fontSize: 12,
fontWeight: 'normal',
},
text: '雷达图示例',
textI18nCode: '',
textStyle: { color: '#333', fontSize: 18, fontWeight: 'normal' },
},
// 提示语设置
tooltip: {
backgroundColor: '#fff',
show: true,
textStyle: {
color: '#303133',
fontSize: 14,
fontWeight: 'normal',
},
},
},
};
@@ -0,0 +1,66 @@
/**
* 报表表达式单元格(对齐 JNPF:f=公式串,t=4 强制字符串展示)
*/
export function normalizeReportFormula(raw: string): string {
const text = (raw || '').trim();
if (!text) {
return '';
}
return text.startsWith('=') ? text : `=${text}`;
}
export function buildExpressionCellData(
oldCellData: Record<string, any>,
custom: Record<string, any>,
rawFormula: string,
): Record<string, any> {
const formula = normalizeReportFormula(
custom?.formula || custom?.field || rawFormula,
);
const bare = formula.startsWith('=') ? formula.slice(1) : formula;
return {
...oldCellData,
custom: {
...oldCellData?.custom,
...custom,
type: 'expression',
field: custom?.field || bare,
formula: custom?.formula || formula,
},
t: 4,
f: formula,
v: formula,
};
}
/** 加载 snapshot 时补齐历史表达式单元格的 f/t */
export function hydrateExpressionCellsInSnapshot(snapshot: Record<string, any>) {
if (!snapshot?.sheets) {
return snapshot;
}
for (const sheet of Object.values(snapshot.sheets) as Record<string, any>[]) {
const cellData = sheet?.cellData;
if (!cellData) {
continue;
}
for (const row of Object.values(cellData) as Record<string, any>[]) {
for (const cell of Object.values(row) as Record<string, any>[]) {
const custom = cell?.custom;
if (custom?.type !== 'expression') {
continue;
}
const raw =
custom.formula ||
custom.field ||
cell.f ||
(typeof cell.v === 'string' ? cell.v : '');
if (!raw) {
continue;
}
const built = buildExpressionCellData(cell, custom, raw);
Object.assign(cell, built);
}
}
}
return snapshot;
}
@@ -0,0 +1,676 @@
import { JnpfPrintDirectionEnum, JnpfPrintPaperSizeForType } from './define';
/**
* 检查值是否是 null 或 undefined
* @param value 要检查的值
* @returns 如果值是 null 或 undefined,返回 true,否则返回 false
*/
export const isNullOrUndefined = (value: any) => {
return value === null || value === undefined;
};
/**
* 判断一个值是否为空对象
* @param obj - 待检查的值
* @returns 如果是空对象,返回 true;否则返回 false
*/
export const isEmptyObject = (obj: unknown): boolean => {
return obj !== null && typeof obj === 'object' && !Array.isArray(obj) && Object.keys(obj).length === 0;
};
/**
* 将指定值从数组中移除并将其添加到数组的末尾。
* @param array - 要操作的数组。
* @param value - 要移动到数组末尾的值。
* @returns - 操作后的数组(原数组被修改)。
*/
export const moveArrayValueToEnd = (array: any[], value: number | string) => {
const index = array?.indexOf(value); // 找到目标值的索引
if (index !== -1) {
// 如果值存在于数组中
const [item] = array.splice(index, 1); // 删除目标值
array.push(item); // 添加到数组末尾
}
return array;
};
/**
* 根据字母规则计算输入字符串的索引值。
* @param input {string} - 输入的字母字符串(如 "A", "B", "AA", "AB" 等)。
* @returns {number} - 对应的索引值。
*/
export const getIndexFromAlphabetRule = (input: string): null | number => {
if (isNullOrUndefined(input)) {
return null;
}
const base = 26; // 字母表的长度
const charCodeA = 'A'.charCodeAt(0);
let index = 0;
for (let i = 0; i < input.length; i++) {
index = index * base + (input.charCodeAt(i) - charCodeA + 1);
}
return index - 1; // 数组索引从 0 开始
};
/**
* 根据索引值获取字母字符串。
* @param index {number} - 输入的索引值(如 0, 1, 26, 27 等)。
* @returns {string} - 对应的字母字符串(如 "A", "B", "AA", "AB" 等)。
*/
export const getAlphabetFromIndexRule = (index: number): string => {
const base = 26; // 字母表的长度
const charCodeA = 'A'.charCodeAt(0);
let result = '';
index += 1; // 转为从 1 开始的规则
while (index > 0) {
const remainder = (index - 1) % base;
result = String.fromCharCode(charCodeA + remainder) + result;
index = Math.floor((index - 1) / base);
}
return result;
};
/**
* 根据类型和坐标获取箭头单元格数组
* @param parentCellType - 父单元格类型
* @param colName - 列名
* @param rowName - 行名
* @returns 单元格坐标数组
*/
// export const getSheetRelationCell = (parentCellType: string, colName?: string, rowName?: string): { row: number; col: number }[] => {
// if (parentCellType === 'none' || !colName || !rowName) return [];
//
// const col = getIndexFromAlphabetRule(colName);
// const row = Number(rowName) - 1;
//
// return [{ row, col }];
// };
/**
* 修正工作表中的单元格数据。
* 根据传入的单元格数据 (`cellData`) 和指定的行、列数量,判断是否需要修正数据。
* 如果所有单元格数据异常(如未定义、空字符串、或无效值),则修正为默认配置;
* 如果存在正常单元格,则返回原始数据。
*
*/
export const correctSheetCellData = (cellData: any = {}, rowCount: number, columnCount: number, isFloatDom: boolean = false) => {
// 单元格纠正配置
const correctCellDataConfig = {
t: 1,
v: ' ',
};
// 如果 cellData 是空对象,直接返回默认初始化的单元格数据
if (isEmptyObject(cellData)) {
return {
0: {
0: correctCellDataConfig,
},
};
}
let totalCells = 0;
let abnormalCells = 0;
// 统计单元格数据中正常与异常的数量
for (const rowKey in cellData) {
const rowValue = cellData[rowKey] ?? {};
for (const colKey in rowValue) {
const { s, t, v, p } = rowValue[colKey] ?? {};
// 判断异常单元格条件
if (isNullOrUndefined(s) && !p && (v === undefined || (v === '' && t === 1) || (v === 0 && (t === 2 || t === 3)))) {
abnormalCells++;
}
totalCells++;
}
}
// 如果存在正常单元格,直接返回原始数据
if (totalCells > abnormalCells) {
return cellData;
}
// 否则需要修正数据
for (let i = 0; i < rowCount; i++) {
for (let j = 0; j < columnCount; j++) {
const { custom, t, v } = cellData?.[i]?.[j] ?? {};
// 判断是否需要修正(根据 isFloatDom 区分)
const needsCorrection = isFloatDom
? custom === undefined && (v === undefined || (v === '' && t === 1))
: custom === undefined && (v === undefined || (v === '' && t === 1) || (v === 0 && (t === 2 || t === 3)));
// 如果发现第一个需要修正的单元格,修正并结束所有循环
if (needsCorrection) {
if (!cellData[i]) {
cellData[i] = {};
}
cellData[i][j] = correctCellDataConfig;
return cellData; // 修正完成后直接返回
}
}
}
// 排查不出来问题,只能返回了
return cellData;
};
/**
* 将 Base64 编码的字符串转换为 File 对象
* @param base64String Base64 字符串(必须以 `data:` 开头)
* @param fileName 生成的文件名
* @param mimeType 文件 MIME 类型(如 "image/png", "application/pdf"
* @returns 返回一个 Promise,解析后得到 File 对象
*/
export function base64ToFile(base64String: string, fileName: string, mimeType: string): Promise<File> {
return fetch(base64String)
.then(res => res.blob()) // 将 Base64 转换为 Blob
.then(blob => new File([blob], fileName, { type: mimeType })); // 生成 File 对象
}
/**
* 计算旋转后的边界框尺寸
* @param {number} width 原始宽度
* @param {number} height 原始高度
* @param {number} angleDegrees 旋转角度(0-360,单位:度)
* @returns {{ rotatedWidth: number; rotatedHeight: number }} 旋转后的宽度和高度
*/
export function rotatedBoundingBox(width: number, height: number, angleDegrees: number): { rotatedHeight: number; rotatedWidth: number } {
const angle = (angleDegrees * Math.PI) / 180; // 角度转换为弧度
const rotatedWidth = Math.abs(width * Math.cos(angle)) + Math.abs(height * Math.sin(angle));
const rotatedHeight = Math.abs(width * Math.sin(angle)) + Math.abs(height * Math.cos(angle));
return { rotatedHeight, rotatedWidth };
}
/**
* 计算插入行和列后父格的位置
* @param range - 选中的区域,包含起始行、列(`startRow`、`startColumn`)以及结束行、列(`endRow`、`endColumn`
* @param axis - 操作方向,`'row'` 表示插入行,`'col'` 表示插入列
* @param offset - 插入的行数或列数,决定偏移量
* @param custom - 当前单元格的数据,包含自定义父格信息(如 `topParentCellType` 和 `leftParentCellType`
*/
export function getParentCellPosWhenInsert(range: any, axis: 'col' | 'row', offset: number, custom: any) {
const { startColumn = 0, startRow = 0 } = range ?? {};
let {
leftParentCellCustomColName,
leftParentCellCustomRowName,
leftParentCellType,
topParentCellCustomColName,
topParentCellCustomRowName,
topParentCellType,
} = custom ?? {};
// 上父格自定义
if (topParentCellType === 'custom' && !isNullOrUndefined(topParentCellCustomColName) && !isNullOrUndefined(topParentCellCustomRowName)) {
// 行操作
if (axis === 'row') {
const targetTopColIndex = Number(topParentCellCustomColName) - 1;
if (startRow <= targetTopColIndex) {
topParentCellCustomColName = (targetTopColIndex + offset + 1).toString();
}
}
// 列操作
if (axis === 'col') {
const targetTopRowIndex = getIndexFromAlphabetRule(topParentCellCustomRowName);
if (targetTopRowIndex !== null && startColumn <= targetTopRowIndex) {
topParentCellCustomRowName = getAlphabetFromIndexRule(targetTopRowIndex + offset);
}
}
}
// 左父格自定义
if (leftParentCellType === 'custom' && !isNullOrUndefined(leftParentCellCustomColName) && !isNullOrUndefined(leftParentCellCustomRowName)) {
// 行操作
if (axis === 'row') {
const targetLeftColIndex = Number(leftParentCellCustomColName) - 1;
if (startRow <= targetLeftColIndex) {
leftParentCellCustomColName = (targetLeftColIndex + offset + 1).toString();
}
}
// 列操作
if (axis === 'col') {
const targetLeftRowIndex = getIndexFromAlphabetRule(leftParentCellCustomRowName);
if (targetLeftRowIndex !== null && startColumn <= targetLeftRowIndex) {
leftParentCellCustomRowName = getAlphabetFromIndexRule(targetLeftRowIndex + offset);
}
}
}
return {
...custom,
leftParentCellCustomColName,
leftParentCellCustomRowName,
topParentCellCustomColName,
topParentCellCustomRowName,
};
}
/**
* 计算删除行和列后父格的位置
* @param range - 选中的区域,包含起始行、列(`startRow`、`startColumn`)以及结束行、列(`endRow`、`endColumn`
* @param axis - 操作方向,`'row'` 表示插入行,`'col'` 表示插入列
* @param offset - 插入的行数或列数,决定偏移量
* @param custom - 当前单元格的数据,包含自定义父格信息(如 `topParentCellType` 和 `leftParentCellType`
*/
export function getParentCellPosWhenDelete(range: any, axis: 'col' | 'row', offset: number, custom: any) {
const { endColumn = 0, endRow = 0, startColumn = 0, startRow = 0 } = range ?? {};
let {
leftParentCellCustomColName,
leftParentCellCustomRowName,
leftParentCellType,
topParentCellCustomColName,
topParentCellCustomRowName,
topParentCellType,
} = custom ?? {};
// 上父格自定义
if (topParentCellType === 'custom' && !isNullOrUndefined(topParentCellCustomColName) && !isNullOrUndefined(topParentCellCustomRowName)) {
// 行操作
if (axis === 'row') {
const targetTopColIndex = Number(topParentCellCustomColName) - 1;
if (startRow <= targetTopColIndex && endRow >= targetTopColIndex) {
topParentCellCustomColName = null;
topParentCellCustomRowName = null;
} else if (startRow <= targetTopColIndex) {
topParentCellCustomColName = (targetTopColIndex - offset + 1).toString();
}
}
// 列操作
if (axis === 'col') {
const targetTopRowIndex = getIndexFromAlphabetRule(topParentCellCustomRowName);
if (targetTopRowIndex !== null) {
if (startColumn <= targetTopRowIndex && endColumn >= targetTopRowIndex) {
topParentCellCustomColName = null;
topParentCellCustomRowName = null;
} else if (startColumn <= targetTopRowIndex) {
topParentCellCustomRowName = getAlphabetFromIndexRule(targetTopRowIndex - offset);
}
}
}
}
// 左父格自定义
if (leftParentCellType === 'custom' && !isNullOrUndefined(leftParentCellCustomColName) && !isNullOrUndefined(leftParentCellCustomRowName)) {
// 行操作
if (axis === 'row') {
const targetLeftColIndex = Number(leftParentCellCustomColName) - 1;
if (startRow <= targetLeftColIndex && endRow >= targetLeftColIndex) {
leftParentCellCustomColName = null;
leftParentCellCustomRowName = null;
} else if (startRow <= targetLeftColIndex) {
leftParentCellCustomColName = (targetLeftColIndex - offset + 1).toString();
}
}
// 列操作
if (axis === 'col') {
const targetLeftRowIndex = getIndexFromAlphabetRule(leftParentCellCustomRowName);
if (targetLeftRowIndex !== null) {
if (startColumn <= targetLeftRowIndex && endColumn >= targetLeftRowIndex) {
leftParentCellCustomColName = null;
leftParentCellCustomRowName = null;
} else if (startColumn <= targetLeftRowIndex) {
leftParentCellCustomRowName = getAlphabetFromIndexRule(targetLeftRowIndex - offset);
}
}
}
}
return {
...custom,
leftParentCellCustomColName,
leftParentCellCustomRowName,
topParentCellCustomColName,
topParentCellCustomRowName,
};
}
/**
* 计算移动行和列后父格的位置
* @param sourceRange - 原选中的区域,包含起始行、列(`startRow`、`startColumn`)以及结束行、列(`endRow`、`endColumn`
* @param targetRange - 后影响的区域,包含起始行、列(`startRow`、`startColumn`)以及结束行、列(`endRow`、`endColumn`
* @param axis - 操作方向,`'row'` 表示插入行,`'col'` 表示插入列
* @param offset - 插入的行数或列数,决定偏移量
* @param involved - 涉及的行或列的数量
* @param custom - 当前单元格的数据,包含自定义父格信息(如 `topParentCellType` 和 `leftParentCellType`
*/
export function getParentCellPosWhenMove(sourceRange: any, targetRange: any, axis: 'col' | 'row', offset: number, involved: number, custom: any) {
let {
leftParentCellCustomColName,
leftParentCellCustomRowName,
leftParentCellType,
topParentCellCustomColName,
topParentCellCustomRowName,
topParentCellType,
} = custom ?? {};
// 上父格自定义
if (topParentCellType === 'custom' && !isNullOrUndefined(topParentCellCustomColName) && !isNullOrUndefined(topParentCellCustomRowName)) {
// 行操作
if (axis === 'row') {
const targetTopColIndex = Number(topParentCellCustomColName) - 1;
if (sourceRange.startRow <= targetTopColIndex && targetTopColIndex <= sourceRange.endRow) {
topParentCellCustomColName = (targetTopColIndex + offset + 1).toString();
} else if (
(sourceRange.startRow < targetTopColIndex && targetTopColIndex < targetRange.startRow) ||
(sourceRange.startRow > targetTopColIndex && targetTopColIndex >= targetRange.startRow)
) {
const moveValue = offset > 0 ? involved : -involved;
topParentCellCustomColName = (targetTopColIndex - moveValue + 1).toString();
}
}
// 列操作
if (axis === 'col') {
const targetTopRowIndex = getIndexFromAlphabetRule(topParentCellCustomRowName);
if (targetTopRowIndex !== null) {
if (sourceRange.startColumn <= targetTopRowIndex && targetTopRowIndex <= sourceRange.endColumn) {
topParentCellCustomRowName = getAlphabetFromIndexRule(targetTopRowIndex + offset);
} else if (
(sourceRange.startColumn < targetTopRowIndex && targetTopRowIndex < targetRange.startColumn) ||
(sourceRange.startColumn > targetTopRowIndex && targetTopRowIndex >= targetRange.startColumn)
) {
const moveValue = offset > 0 ? involved : -involved;
topParentCellCustomRowName = getAlphabetFromIndexRule(targetTopRowIndex - moveValue);
}
}
}
}
// 左父格自定义
if (leftParentCellType === 'custom' && !isNullOrUndefined(leftParentCellCustomColName) && !isNullOrUndefined(leftParentCellCustomRowName)) {
// 行操作
if (axis === 'row') {
const targetLeftColIndex = Number(leftParentCellCustomColName) - 1;
if (sourceRange.startRow <= targetLeftColIndex && targetLeftColIndex <= sourceRange.endRow) {
leftParentCellCustomColName = (targetLeftColIndex + offset + 1).toString();
} else if (
(sourceRange.startRow < targetLeftColIndex && targetLeftColIndex < targetRange.startRow) ||
(sourceRange.startRow > targetLeftColIndex && targetLeftColIndex >= targetRange.startRow)
) {
const moveValue = offset > 0 ? involved : -involved;
leftParentCellCustomColName = (targetLeftColIndex - moveValue + 1).toString();
}
}
// 列操作
if (axis === 'col') {
const targetLeftRowIndex = getIndexFromAlphabetRule(leftParentCellCustomRowName);
if (targetLeftRowIndex !== null) {
if (sourceRange.startColumn <= targetLeftRowIndex && targetLeftRowIndex <= sourceRange.endColumn) {
leftParentCellCustomRowName = getAlphabetFromIndexRule(targetLeftRowIndex + offset);
} else if (
(sourceRange.startColumn < targetLeftRowIndex && targetLeftRowIndex < targetRange.startColumn) ||
(sourceRange.startColumn > targetLeftRowIndex && targetLeftRowIndex >= targetRange.startColumn)
) {
const moveValue = offset > 0 ? involved : -involved;
leftParentCellCustomRowName = getAlphabetFromIndexRule(targetLeftRowIndex - moveValue);
}
}
}
}
return {
...custom,
leftParentCellCustomColName,
leftParentCellCustomRowName,
topParentCellCustomColName,
topParentCellCustomRowName,
};
}
/**
* 计算移动单元格后父格的位置
* @param {object} fromRange - 被移动的单元格的范围,包含起始和结束行列坐标
* @param {object} offset - 行列偏移量,包含纵向(offsetRow)和横向(offsetCol)的偏移
* @param {object} custom - 当前单元格的自定义信息,包含上父格和左父格的信息
*/
export function getParentCellPosWhenMoveCell(fromRange: any, offset: any, custom: any) {
const { offsetCol, offsetRow } = offset ?? {};
let {
leftParentCellCustomColName,
leftParentCellCustomRowName,
leftParentCellType,
topParentCellCustomColName,
topParentCellCustomRowName,
topParentCellType,
} = custom ?? {};
// 上父格自定义
if (topParentCellType === 'custom' && !isNullOrUndefined(topParentCellCustomColName) && !isNullOrUndefined(topParentCellCustomRowName)) {
const targetTopColIndex = Number(topParentCellCustomColName) - 1;
const targetTopRowIndex = getIndexFromAlphabetRule(topParentCellCustomRowName);
if (targetTopRowIndex !== null) {
const isLinchpin =
fromRange.startRow <= targetTopColIndex &&
targetTopColIndex <= fromRange.endRow &&
fromRange.startColumn <= targetTopRowIndex &&
targetTopRowIndex <= fromRange.endColumn; // 当事单元格
if (
offsetRow !== 0 && // 纵向操作
isLinchpin
) {
topParentCellCustomColName = (targetTopColIndex + offsetRow + 1).toString();
}
if (
offsetCol !== 0 && // 横向操作
isLinchpin
) {
topParentCellCustomRowName = getAlphabetFromIndexRule(targetTopRowIndex + offsetCol);
}
}
}
// 左父格自定义
if (leftParentCellType === 'custom' && !isNullOrUndefined(leftParentCellCustomColName) && !isNullOrUndefined(leftParentCellCustomRowName)) {
const targetLeftColIndex = Number(leftParentCellCustomColName) - 1;
const targetLeftRowIndex = getIndexFromAlphabetRule(leftParentCellCustomRowName);
if (targetLeftRowIndex !== null) {
const isLinchpin =
fromRange.startRow <= targetLeftColIndex &&
targetLeftColIndex <= fromRange.endRow &&
fromRange.startColumn <= targetLeftRowIndex &&
targetLeftRowIndex <= fromRange.endColumn; // 当事单元格
if (
offsetRow !== 0 && // 纵向操作
isLinchpin
) {
leftParentCellCustomColName = (targetLeftColIndex + offsetRow + 1).toString();
}
if (
offsetCol !== 0 && // 横向操作
isLinchpin
) {
leftParentCellCustomRowName = getAlphabetFromIndexRule(targetLeftRowIndex + offsetCol);
}
}
}
return {
...custom,
leftParentCellCustomColName,
leftParentCellCustomRowName,
topParentCellCustomColName,
topParentCellCustomRowName,
};
}
/**
* 计算清楚单元格后父格的位置
* @param ranges - 当前选择的单元格范围列表,每个范围包含起始和结束行列
* @param custom - 父格的自定义信息,包括行列名称和类型
*/
export function getParentCellPosWhenClearCell(ranges: any, custom: any) {
let {
leftParentCellCustomColName,
leftParentCellCustomRowName,
leftParentCellType,
topParentCellCustomColName,
topParentCellCustomRowName,
topParentCellType,
} = custom ?? {};
// 上父格自定义
if (topParentCellType === 'custom' && !isNullOrUndefined(topParentCellCustomColName) && !isNullOrUndefined(topParentCellCustomRowName)) {
const targetTopColIndex = Number(topParentCellCustomColName) - 1;
const targetTopRowIndex = getIndexFromAlphabetRule(topParentCellCustomRowName);
if (targetTopRowIndex !== null) {
const isLinchpin = ranges.some(({ endColumn, endRow, startColumn, startRow }: any) => {
return startRow <= targetTopColIndex && targetTopColIndex <= endRow && startColumn <= targetTopRowIndex && targetTopRowIndex <= endColumn;
});
if (isLinchpin) {
topParentCellCustomColName = null;
topParentCellCustomRowName = null;
}
}
}
// 左父格自定义
if (leftParentCellType === 'custom' && !isNullOrUndefined(leftParentCellCustomColName) && !isNullOrUndefined(leftParentCellCustomRowName)) {
const targetLeftColIndex = Number(leftParentCellCustomColName) - 1;
const targetLeftRowIndex = getIndexFromAlphabetRule(leftParentCellCustomRowName);
if (targetLeftRowIndex !== null) {
const isLinchpin = ranges.some(({ endColumn, endRow, startColumn, startRow }: any) => {
return startRow <= targetLeftColIndex && targetLeftColIndex <= endRow && startColumn <= targetLeftRowIndex && targetLeftRowIndex <= endColumn;
});
if (isLinchpin) {
leftParentCellCustomColName = null;
leftParentCellCustomRowName = null;
}
}
}
return {
...custom,
leftParentCellCustomColName,
leftParentCellCustomRowName,
topParentCellCustomColName,
topParentCellCustomRowName,
};
}
/**
* 翻译小时
* @param hour
*/
function to12HourFormat(hour: any) {
const h = hour % 12 || 12; // 0 => 12, 13 => 1, 14 => 2 ...
return h.toString().padStart(2, '0'); // 补零,例如 2 => '02'
}
/**
* 翻译页眉页脚文本
*/
/**
* 翻译页眉页脚文本
*/
export function translateHeaderFooterText(orientation: string, printConfig: any): string {
if (!printConfig?.customHeaderFooterValue?.[orientation]) return '';
let targetValue = printConfig?.customHeaderFooterValue[orientation];
// 日期和时间的预计算,减少 slice 调用
const dateTime = printConfig?.currentDateTime || '';
const dateA = dateTime.slice(0, 10); // YYYY-MM-DD
const dateB = dateTime.slice(5, 10); // YYYY-MM
const dateC = `${dateTime.slice(5, 7)}/${dateTime.slice(8, 10)}/${dateTime.slice(0, 4)}`; // MM/DD/YYYY
const dateD = `${dateTime.slice(5, 7)}/${dateTime.slice(8, 10)}`; // MM/DD
const timeA = dateTime.slice(11, 19); // HH:mm:ss
const timeB = dateTime.slice(11, 16); // HH:mm
const hourFormat = to12HourFormat(dateTime.slice(11, 13));
const hour = Number(dateTime.slice(11, 13));
const timeC = `${hour < 13 ? 'AM' : 'PM'} ${hourFormat}:${dateTime.slice(14, 19)}`; // HH:mm:ss
const timeD = `${hour < 13 ? 'AM' : 'PM'} ${hourFormat}:${dateTime.slice(14, 16)}`; // HH:mm:ss
// 依次替换占位符,保持原有业务逻辑
targetValue = targetValue
.replace('@ReportName', printConfig.workbookTitleText)
.replace('@SheetName', printConfig.workSheetTitleText)
.replace('@TotalPages', printConfig.bookTotalPage)
.replace('@PageNumbers', printConfig.bookPageNumber)
.replace('@SheetTotalPages', printConfig.sheetTotalPage)
.replace('@SheetPageNumbers', printConfig.sheetPageNumber)
.replace('@DateA', dateA)
.replace('@DateB', dateB)
.replace('@DateC', dateC)
.replace('@DateD', dateD)
.replace('@TimeA', timeA)
.replace('@TimeB', timeB)
.replace('@TimeC', timeC)
.replace('@TimeD', timeD);
return targetValue;
}
/**
* 获取打印页面的样式
* @param paperType - 纸张类型,对应 `JnpfPrintPaperSizeForType` 中的键值
* @param direction - 打印方向,取值为 `JnpfPrintDirectionEnum.portrait`(纵向)或 `JnpfPrintDirectionEnum.landscape`(横向)
* @returns 一个包含打印样式的 `<style>` 元素
*/
export function getPrintPageStyle(paperType: string, direction: string) {
const { h, w } = JnpfPrintPaperSizeForType[paperType as keyof typeof JnpfPrintPaperSizeForType] ?? {};
const width = direction === JnpfPrintDirectionEnum?.portrait ? w : h;
const height = direction === JnpfPrintDirectionEnum?.portrait ? h : w;
const style = `
@page {
size: ${width}px ${height}px;
}
@page {
margin: 0;
visibility: hidden;
}
@media print {
body > * {
display: none!important;
}
#jnpfReportPrint, #jnpfReportPrint * {
display: block!important;
height: fit-content;
overflow: visible;
top: 0;
width: fit-content;
}
#jnpfReportPrint .printContainer {
page-break-after: always!important;
height: ${height}px;
width: ${width}px;
position: relative;
}
}`;
const $style = document.createElement('style');
$style.innerHTML = style;
$style.className = 'jnpfPrintCss';
return $style;
}
@@ -0,0 +1,129 @@
/**
* Univer 0.8.3 内置 UI 语言包聚合(简中 / 英文)
* 繁中无官方包:沿用 ZH_CN 内置 + zq 自定义 zh-TW 菜单文案
*/
import { LocaleType, merge } from '@univerjs/core';
import DesignEnUS from '@univerjs/design/locale/en-US';
import DesignZhCN from '@univerjs/design/locale/zh-CN';
import DocsUIEnUS from '@univerjs/docs-ui/locale/en-US';
import DocsUIZhCN from '@univerjs/docs-ui/locale/zh-CN';
import DrawingUIEnUS from '@univerjs/drawing-ui/locale/en-US';
import DrawingUIZhCN from '@univerjs/drawing-ui/locale/zh-CN';
import FindReplaceEnUS from '@univerjs/find-replace/locale/en-US';
import FindReplaceZhCN from '@univerjs/find-replace/locale/zh-CN';
import SheetsConditionalFormattingUIEnUS from '@univerjs/sheets-conditional-formatting-ui/locale/en-US';
import SheetsConditionalFormattingUIZhCN from '@univerjs/sheets-conditional-formatting-ui/locale/zh-CN';
import SheetsCrosshairHighlightEnUS from '@univerjs/sheets-crosshair-highlight/locale/en-US';
import SheetsCrosshairHighlightZhCN from '@univerjs/sheets-crosshair-highlight/locale/zh-CN';
import SheetsDataValidationUIEnUS from '@univerjs/sheets-data-validation-ui/locale/en-US';
import SheetsDataValidationZhCN from '@univerjs/sheets-data-validation-ui/locale/zh-CN';
import SheetsDrawingUIEnUS from '@univerjs/sheets-drawing-ui/locale/en-US';
import SheetsDrawingUIZhCN from '@univerjs/sheets-drawing-ui/locale/zh-CN';
import SheetsFilterUIEnUS from '@univerjs/sheets-filter-ui/locale/en-US';
import SheetsFilterUIZhCN from '@univerjs/sheets-filter-ui/locale/zh-CN';
import SheetsFindReplaceEnUS from '@univerjs/sheets-find-replace/locale/en-US';
import SheetsFindReplaceZhCN from '@univerjs/sheets-find-replace/locale/zh-CN';
import SheetsFormulaUIEnUS from '@univerjs/sheets-formula-ui/locale/en-US';
import SheetsFormulaUIZhCN from '@univerjs/sheets-formula-ui/locale/zh-CN';
import SheetsHyperLinkUIEnUS from '@univerjs/sheets-hyper-link-ui/locale/en-US';
import SheetsHyperLinkUIZhCN from '@univerjs/sheets-hyper-link-ui/locale/zh-CN';
import SheetsNumfmtUIEnUS from '@univerjs/sheets-numfmt-ui/locale/en-US';
import SheetsNumfmtUIZhCN from '@univerjs/sheets-numfmt-ui/locale/zh-CN';
import SheetsSortUIEnUS from '@univerjs/sheets-sort-ui/locale/en-US';
import SheetsSortUIZhCN from '@univerjs/sheets-sort-ui/locale/zh-CN';
import SheetsThreadCommentUIEnUS from '@univerjs/sheets-thread-comment-ui/locale/en-US';
import SheetsThreadCommentUIZhCN from '@univerjs/sheets-thread-comment-ui/locale/zh-CN';
import SheetsUIEnUS from '@univerjs/sheets-ui/locale/en-US';
import SheetsUIZhCN from '@univerjs/sheets-ui/locale/zh-CN';
import SheetsZenEditorEnUS from '@univerjs/sheets-zen-editor/locale/en-US';
import SheetsZenEditorZhCN from '@univerjs/sheets-zen-editor/locale/zh-CN';
import SheetsEnUS from '@univerjs/sheets/locale/en-US';
import SheetsZhCN from '@univerjs/sheets/locale/zh-CN';
import ThreadCommentUIEnUS from '@univerjs/thread-comment-ui/locale/en-US';
import ThreadCommentUIZhCN from '@univerjs/thread-comment-ui/locale/zh-CN';
import UIEnUS from '@univerjs/ui/locale/en-US';
import UIZhCN from '@univerjs/ui/locale/zh-CN';
import zqEnUS from '../locales/en-US';
import zqZhCN from '../locales/zh-CN';
import zqZhTW from '../locales/zh-TW';
function mergeZhCNBuiltin() {
return merge(
{},
DesignZhCN,
UIZhCN,
DocsUIZhCN,
SheetsZhCN,
SheetsUIZhCN,
SheetsNumfmtUIZhCN,
SheetsFormulaUIZhCN,
SheetsFilterUIZhCN,
SheetsSortUIZhCN,
SheetsDataValidationZhCN,
SheetsConditionalFormattingUIZhCN,
SheetsHyperLinkUIZhCN,
DrawingUIZhCN,
SheetsDrawingUIZhCN,
ThreadCommentUIZhCN,
SheetsThreadCommentUIZhCN,
FindReplaceZhCN,
SheetsFindReplaceZhCN,
SheetsCrosshairHighlightZhCN,
SheetsZenEditorZhCN,
);
}
function mergeEnUSBuiltin() {
return merge(
{},
DesignEnUS,
UIEnUS,
DocsUIEnUS,
SheetsEnUS,
SheetsUIEnUS,
SheetsNumfmtUIEnUS,
SheetsFormulaUIEnUS,
SheetsFilterUIEnUS,
SheetsSortUIEnUS,
SheetsDataValidationUIEnUS,
SheetsConditionalFormattingUIEnUS,
SheetsHyperLinkUIEnUS,
DrawingUIEnUS,
SheetsDrawingUIEnUS,
ThreadCommentUIEnUS,
SheetsThreadCommentUIEnUS,
FindReplaceEnUS,
SheetsFindReplaceEnUS,
SheetsCrosshairHighlightEnUS,
SheetsZenEditorEnUS,
);
}
/** 根据应用语言解析 Univer 实例 locale 与 locales 配置 */
export function resolveUniverLocaleConfig(appLocale?: string): {
locale: LocaleType;
locales: Partial<Record<LocaleType, object>>;
} {
const zhBuiltin = mergeZhCNBuiltin();
const enBuiltin = mergeEnUSBuiltin();
if (appLocale === 'en-US') {
return {
locale: LocaleType.EN_US,
locales: {
[LocaleType.EN_US]: merge({}, enBuiltin, zqEnUS),
[LocaleType.ZH_CN]: merge({}, zhBuiltin, zqZhCN),
},
};
}
const zqOverlay = appLocale === 'zh-TW' ? zqZhTW : zqZhCN;
return {
locale: LocaleType.ZH_CN,
locales: {
[LocaleType.ZH_CN]: merge({}, zhBuiltin, zqOverlay),
[LocaleType.EN_US]: merge({}, enBuiltin, zqEnUS),
},
};
}
@@ -0,0 +1,590 @@
/* eslint-disable no-prototype-builtins */
import type { IWorkbookData } from '@univerjs/core';
import { IImageIoService, Univer, UniverInstanceType } from '@univerjs/core';
// 数据验证
import { UniverDataValidationPlugin } from '@univerjs/data-validation';
import { defaultTheme } from '@univerjs/design';
import { UniverDocsPlugin } from '@univerjs/docs';
// 图片
import { UniverDocsDrawingPlugin } from '@univerjs/docs-drawing';
import { UniverDocsUIPlugin } from '@univerjs/docs-ui';
import { UniverDrawingPlugin } from '@univerjs/drawing';
import { UniverDrawingUIPlugin } from '@univerjs/drawing-ui';
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula';
import { UniverRenderEnginePlugin } from '@univerjs/engine-render';
// 查找 & 替换
import { UniverFindReplacePlugin } from '@univerjs/find-replace';
import { UniverSheetsPlugin } from '@univerjs/sheets';
// 条件格式
import { UniverSheetsConditionalFormattingPlugin } from '@univerjs/sheets-conditional-formatting';
import { UniverSheetsConditionalFormattingUIPlugin } from '@univerjs/sheets-conditional-formatting-ui';
// 十字高亮
import { UniverSheetsCrosshairHighlightPlugin } from '@univerjs/sheets-crosshair-highlight';
import { UniverSheetsDataValidationPlugin } from '@univerjs/sheets-data-validation';
import { UniverSheetsDataValidationUIPlugin } from '@univerjs/sheets-data-validation-ui';
import { UniverSheetsDrawingPlugin } from '@univerjs/sheets-drawing';
import { UniverSheetsDrawingUIPlugin } from '@univerjs/sheets-drawing-ui';
// 筛选
import { UniverSheetsFilterPlugin } from '@univerjs/sheets-filter';
import { UniverSheetsFilterUIPlugin } from '@univerjs/sheets-filter-ui';
import { UniverSheetsFindReplacePlugin } from '@univerjs/sheets-find-replace';
import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula';
import { UniverSheetsFormulaUIPlugin } from '@univerjs/sheets-formula-ui';
// 超链接
import { UniverSheetsHyperLinkPlugin } from '@univerjs/sheets-hyper-link';
import { UniverSheetsHyperLinkUIPlugin } from '@univerjs/sheets-hyper-link-ui';
import { UniverSheetsNumfmtPlugin } from '@univerjs/sheets-numfmt';
import { UniverSheetsNumfmtUIPlugin } from '@univerjs/sheets-numfmt-ui';
// 排序
import { UniverSheetsSortPlugin } from '@univerjs/sheets-sort';
import { UniverSheetsSortUIPlugin } from '@univerjs/sheets-sort-ui';
import { UniverSheetsThreadCommentPlugin } from '@univerjs/sheets-thread-comment';
// import { UniverSheetsThreadCommentUIPlugin } from '@univerjs/sheets-thread-comment-ui';
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
// 禅编译器
import { UniverSheetsZenEditorPlugin } from '@univerjs/sheets-zen-editor';
// 评论/批注
import { UniverThreadCommentPlugin } from '@univerjs/thread-comment';
import { UniverThreadCommentUIPlugin } from '@univerjs/thread-comment-ui';
import { UniverUIPlugin } from '@univerjs/ui';
// uniscript
// import { UniverUniscriptPlugin } from '@univerjs/uniscript';
// 水印
import { UniverWatermarkPlugin } from '@univerjs/watermark';
// 打印
// import { UniverSheetsPrintPlugin } from '@univerjs-pro/sheets-print';
import { ZqSheetsCellImagePlugin } from '../plugins/sheet-cell-image.plugin';
// 自定义
import { ZqSheetsCellPlugin } from '../plugins/sheet-cell.plugin';
import { ZqSheetsDialogPlugin } from '../plugins/sheet-dialog.plugin';
import { ZqSheetsExcelFilePlugin } from '../plugins/sheet-excel-file.plugin';
import { ZqSheetsFloatDomPlugin } from '../plugins/sheet-float-dom.plugin';
import { ZqSheetsFloatEchartPlugin } from '../plugins/sheet-float-echart.plugin';
import { ZqSheetsFloatImagePlugin } from '../plugins/sheet-float-image.plugin';
import { ZqSheetsPreviewPlugin } from '../plugins/sheet-preview.plugin';
import { ZqSheetsPrintPlugin } from '../plugins/sheet-print.plugin';
import { ZqSheetsRangePlugin } from '../plugins/sheet-range.plugin';
import { DefaultFloatEchartHeight, DefaultFloatEchartWidth, ZqUniverFloatEchartKey, ZqUniverFloatImageKey } from './define';
import { hydrateExpressionCellsInSnapshot } from './expression-cell';
import { correctSheetCellData, isEmptyObject, isNullOrUndefined, moveArrayValueToEnd } from './index';
import { resolveUniverLocaleConfig } from './locale-packs';
// 创建univer实例
export function createUniverInstance(config: any) {
const { container, contextMenu, footer, header, workbookData, darkMode, appLocale } = config ?? {};
const { locale, locales } = resolveUniverLocaleConfig(appLocale);
const univerInstance = new Univer({
locale,
locales,
theme: defaultTheme,
darkMode,
});
univerInstance.registerPlugin(UniverRenderEnginePlugin);
univerInstance.registerPlugin(UniverFormulaEnginePlugin);
univerInstance.registerPlugin(UniverUIPlugin, {
container,
contextMenu,
footer,
header,
});
univerInstance.registerPlugin(UniverDocsPlugin);
univerInstance.registerPlugin(UniverDocsUIPlugin);
univerInstance.registerPlugin(UniverSheetsPlugin);
univerInstance.registerPlugin(UniverSheetsUIPlugin, {
menu: {
'sheet.command.delete-range-move-left-confirm': {
hidden: true,
},
'sheet.command.delete-range-move-up-confirm': {
hidden: true,
},
'sheet.command.add-range-protection-from-toolbar': {
hidden: true,
},
'sheet.command.add-range-protection-from-context-menu': {
hidden: true,
},
'sheet.contextMenu.permission': {
hidden: true,
}
},
});
// univerInstance.registerPlugin(UniverSheetsUIPlugin);
univerInstance.registerPlugin(UniverSheetsFormulaPlugin);
univerInstance.registerPlugin(UniverSheetsFormulaUIPlugin);
univerInstance.registerPlugin(UniverSheetsNumfmtPlugin);
univerInstance.registerPlugin(UniverSheetsNumfmtUIPlugin);
// 这个和注销的冲突掉了
univerInstance.registerPlugin(UniverSheetsHyperLinkPlugin);
univerInstance.registerPlugin(UniverSheetsHyperLinkUIPlugin);
univerInstance.registerPlugin(UniverDocsDrawingPlugin);
univerInstance.registerPlugin(UniverDrawingPlugin, {
override: [[IImageIoService, null]],
});
univerInstance.registerPlugin(UniverDrawingUIPlugin);
univerInstance.registerPlugin(UniverSheetsDrawingPlugin);
univerInstance.registerPlugin(UniverSheetsDrawingUIPlugin);
univerInstance.registerPlugin(UniverFindReplacePlugin);
univerInstance.registerPlugin(UniverSheetsFindReplacePlugin);
univerInstance.registerPlugin(UniverSheetsFilterPlugin);
univerInstance.registerPlugin(UniverSheetsFilterUIPlugin);
univerInstance.registerPlugin(UniverSheetsSortPlugin);
univerInstance.registerPlugin(UniverSheetsSortUIPlugin);
univerInstance.registerPlugin(UniverDataValidationPlugin);
univerInstance.registerPlugin(UniverSheetsDataValidationPlugin);
univerInstance.registerPlugin(UniverSheetsDataValidationUIPlugin, {
showEditOnDropdown: true,
});
univerInstance.registerPlugin(UniverSheetsConditionalFormattingPlugin);
univerInstance.registerPlugin(UniverSheetsConditionalFormattingUIPlugin);
univerInstance.registerPlugin(UniverSheetsCrosshairHighlightPlugin);
univerInstance.registerPlugin(UniverThreadCommentPlugin);
univerInstance.registerPlugin(UniverThreadCommentUIPlugin);
univerInstance.registerPlugin(UniverSheetsThreadCommentPlugin);
// univerInstance.registerPlugin(UniverSheetsThreadCommentUIPlugin);
// univerInstance.registerPlugin(UniverUniscriptPlugin);
univerInstance.registerPlugin(UniverSheetsZenEditorPlugin);
univerInstance.registerPlugin(UniverWatermarkPlugin);
// univerInstance.registerPlugin(UniverSheetsPrintPlugin);
univerInstance.registerPlugin(ZqSheetsCellPlugin);
univerInstance.registerPlugin(ZqSheetsRangePlugin);
univerInstance.registerPlugin(ZqSheetsDialogPlugin);
univerInstance.registerPlugin(ZqSheetsExcelFilePlugin);
univerInstance.registerPlugin(ZqSheetsFloatDomPlugin);
univerInstance.registerPlugin(ZqSheetsFloatEchartPlugin);
univerInstance.registerPlugin(ZqSheetsCellImagePlugin);
univerInstance.registerPlugin(ZqSheetsFloatImagePlugin);
univerInstance.registerPlugin(ZqSheetsPreviewPlugin);
univerInstance.registerPlugin(ZqSheetsPrintPlugin);
univerInstance.createUnit(UniverInstanceType.UNIVER_SHEET, workbookData);
return univerInstance;
}
// 解析创建univer实例的快照数据
export function analysisCreateUnitData(workbookData: IWorkbookData | string, createMode: string, isReadonly: boolean) {
try {
const parseWorkbookData = typeof workbookData === 'string' ? JSON.parse(workbookData) : workbookData;
if (createMode === 'design' && !isReadonly) {
// 设计模式:补齐表达式单元格 f/t,便于公式栏展示
return hydrateExpressionCellsInSnapshot(
JSON.parse(JSON.stringify(parseWorkbookData)),
);
}
const { resources = [] } = parseWorkbookData ?? {};
// 主要是预览的情况下不让图表可操作
const updatedResources = resources?.map((resource: any) => {
if (resource?.name === 'SHEET_DRAWING_PLUGIN') {
const parseResourceData = JSON.parse(resource?.data ?? '{}');
const updatedParseResourceData = Object.fromEntries(
Object.entries(parseResourceData)?.map(([sheetKey, sheetResourceOption]: any) => {
const sheetResourceOptionData = sheetResourceOption?.data || {};
const updateSheetResourceOption = Object.fromEntries(
Object.entries(sheetResourceOptionData).map(([floatDomKey, floatDomValue]: any) => {
const { componentKey, ...floatDomValueRest } = floatDomValue || {};
return componentKey === ZqUniverFloatEchartKey || componentKey === ZqUniverFloatImageKey
? [
floatDomKey,
{
...floatDomValueRest,
allowTransform: false,
componentKey,
},
]
: [floatDomKey, floatDomValue];
}),
);
return [sheetKey, { ...sheetResourceOption, data: updateSheetResourceOption }];
}),
);
return {
...resource,
data: JSON.stringify(updatedParseResourceData),
};
} else {
return resource;
}
});
return {
...parseWorkbookData,
resources: updatedResources,
};
} catch (error) {
console.error(error);
return {};
}
}
// 整理设计的数据
export function arrangeDesignWorkbookData(snapshot: any, floatEchartDataCaches: any, floatImageDataCaches: any) {
const updateFloatEcharts: Record<string, any> = {};
const updateCellEcharts: Record<string, any> = {};
const updateFloatImages: Record<string, any> = {};
// 给悬浮图表和悬浮图片增加float dom的id 并且剔除已被删除的悬浮图表和悬浮图片数据
const hydratedSnapshot = hydrateExpressionCellsInSnapshot(
JSON.parse(JSON.stringify(snapshot ?? {})),
);
const { resources = [], sheets = {} } = hydratedSnapshot ?? {};
const updatedResources = resources?.map((resource: any) => {
if (resource?.name === 'SHEET_DRAWING_PLUGIN') {
const parseResourceData = JSON.parse(resource?.data ?? '{}');
const updatedParseResourceData = Object.fromEntries(
Object.entries(parseResourceData)?.map(([sheetKey, sheetResourceOption]: any) => {
const sheetResourceOptionData = sheetResourceOption?.data || {};
const updateSheetResourceOption = Object.fromEntries(
Object.entries(sheetResourceOptionData).map(([floatDomKey, floatDomValue]: any) => {
const { componentKey, drawingId, data = {}, ...floatDomValueRest } = floatDomValue || {};
if (componentKey === ZqUniverFloatEchartKey) {
if (floatEchartDataCaches?.hasOwnProperty(drawingId)) {
const { height = DefaultFloatEchartHeight, width = DefaultFloatEchartWidth } = floatDomValue.transform ?? {};
updateFloatEcharts[drawingId] = {
...floatEchartDataCaches[drawingId],
height: Math.floor(height),
width: Math.floor(width),
}; // store中的悬浮图表数据过滤
}
// const domId = floatEchartDataCaches?.[drawingId]?.domId;
// const updatedProps = {
// ...props,
// id: domId,
// };
return [
floatDomKey,
{
...floatDomValueRest,
componentKey,
drawingId,
data,
},
];
} else if (componentKey === ZqUniverFloatImageKey) {
if (floatImageDataCaches?.hasOwnProperty(drawingId)) {
updateFloatImages[drawingId] = { ...floatImageDataCaches[drawingId] }; // store中的悬浮图片数据过滤
}
// const domId = floatImageDataCaches?.[drawingId]?.domId;
// const updatedProps = {
// ...data,
// id: domId,
// };
return [
floatDomKey,
{
...floatDomValueRest,
componentKey,
drawingId,
data,
},
];
} else {
return [floatDomKey, floatDomValue];
}
}),
);
return [sheetKey, { ...sheetResourceOption, data: updateSheetResourceOption }];
}),
);
return {
...resource,
data: JSON.stringify(updatedParseResourceData),
};
} else {
return resource;
}
});
// 取出所有自定义的custom并整理单元格图表的数据
const customs: any[] = [];
for (const sheetKey in sheets) {
const sheet = sheets[sheetKey];
const sheetCellData = sheet?.cellData;
if (!sheetCellData) {
continue;
}
for (const rowKey in sheetCellData) {
const row = sheetCellData[rowKey];
for (const colKey in row) {
const cellData = row[colKey] ?? {};
const custom = cellData?.custom;
if (custom && !isEmptyObject(custom)) {
if (custom.type === 'chart') {
if (!isNullOrUndefined(custom)) {
const { chartType, color, drawingId, height, type, width, ...restOption } = custom ?? {};
updateCellEcharts[drawingId] = {
col: colKey,
drawingId,
echartType: chartType,
height,
option: restOption,
row: rowKey,
sheet: sheetKey,
width,
};
}
} else {
if (custom.type !== 'jsbarcode' && custom.type !== 'qrCode') {
customs.push({
cellData,
col: colKey,
row: rowKey,
sheetId: sheetKey,
});
}
}
}
}
}
}
return {
cellEcharts: updateCellEcharts,
customs,
floatEcharts: updateFloatEcharts,
floatImages: updateFloatImages,
snapshot: {
...hydratedSnapshot,
resources: updatedResources,
},
};
}
// 整理预览的数据
export function arrangePreviewWorkbookData(designWorkbookData: any, floatEchartToUniverImgDataCaches: any, floatImageToUniverImgDataCaches: any) {
const { customs, floatEcharts, floatImages, snapshot } = designWorkbookData ?? {};
const { resources = [], sheets } = snapshot ?? {};
const updateSheets = { ...sheets };
Object.entries(sheets)?.forEach(([sheetKey, sheetInfo]: any) => {
const { cellData, columnCount, rowCount } = sheetInfo ?? {};
const resource = resources.find((resourceInfo: any) => resourceInfo?.name === 'SHEET_DRAWING_PLUGIN');
const parseResourceData = JSON.parse(resource?.data ?? '{}');
const hasFloatDom = !isEmptyObject(parseResourceData) && parseResourceData[sheetKey]?.order?.length;
const updateCellData = hasFloatDom ? correctSheetCellData(cellData, rowCount, columnCount, true) : correctSheetCellData(cellData, rowCount, columnCount);
updateSheets[sheetKey] = {
...sheets[sheetKey],
cellData: updateCellData,
};
});
const updatedResources = resources?.map((resource: any) => {
if (resource?.name === 'SHEET_DRAWING_PLUGIN') {
const parseResourceData = JSON.parse(resource?.data ?? '{}');
const updatedParseResourceData = Object.fromEntries(
Object.entries(parseResourceData)?.map(([sheetKey, sheetResourceOption]: any) => {
const { data: sheetResourceOptionData = {}, order: originalOrder = [] } = sheetResourceOption || {};
let sheetOrder = [...originalOrder]; // 使用一个可变的引用来追踪顺序更新
const updatedSheetData = Object.fromEntries(
Object.entries(sheetResourceOptionData).map(([floatDomKey, floatDomValue]: any) => {
const { componentKey, drawingId, data, sheetTransform, subUnitId, transform, unitId } = floatDomValue || {};
if (componentKey === ZqUniverFloatEchartKey) {
const { id: domId } = data ?? {};
sheetOrder = moveArrayValueToEnd(sheetOrder, drawingId);
const source = floatEchartToUniverImgDataCaches?.[domId] ?? '';
return [
floatDomKey,
{
drawingId,
drawingType: 0,
imageSourceType: 'BASE64',
sheetTransform,
source,
subUnitId,
transform,
unitId,
data,
},
];
} else if (componentKey === ZqUniverFloatImageKey) {
const { id: domId } = data ?? {};
sheetOrder = moveArrayValueToEnd(sheetOrder, drawingId);
const source = floatImageToUniverImgDataCaches?.[domId] ?? '';
return [
floatDomKey,
{
drawingId,
drawingType: 0,
imageSourceType: source?.startsWith('data:image/') ? 'BASE64' : 'URL',
sheetTransform,
source,
subUnitId,
transform,
unitId,
data,
},
];
} else {
return [floatDomKey, floatDomValue];
}
}),
);
return [sheetKey, { ...sheetResourceOption, data: updatedSheetData, order: sheetOrder }];
}),
);
return {
...resource,
data: JSON.stringify(updatedParseResourceData),
};
} else {
return resource;
}
});
return {
customs,
floatEcharts,
floatImages,
snapshot: {
...snapshot,
resources: updatedResources,
sheets: updateSheets,
},
};
}
// 判断当前工作本是不是含有自定义的 float dom
export function judgeSheetHasCustomFloatDom(data: any): boolean {
return Object.values(data).some(
(floatDomValue: any) => floatDomValue?.componentKey === ZqUniverFloatEchartKey || floatDomValue?.componentKey === ZqUniverFloatImageKey,
);
}
// 监听插入悬浮图表
export function onInsertedFloatEchart(zqUniverStore: any, command: any) {
const { drawingId } = command?.params ?? {};
if (!drawingId) {
return;
}
const floatEchartDataCaches = zqUniverStore?.floatEchartDataCaches ?? {};
const updateItems = {
...floatEchartDataCaches,
[drawingId]: command?.params,
};
zqUniverStore?.setFloatEchartDataCaches(updateItems);
}
// 监听插入悬浮图片
export function onInsertedFloatImage(zqUniverStore: any, command: any) {
const { drawingId } = command?.params ?? {};
if (!drawingId) {
return;
}
const floatImageDataCaches = zqUniverStore?.floatImageDataCaches ?? {};
const updateItems = {
...floatImageDataCaches,
[drawingId]: command?.params,
};
zqUniverStore?.setFloatImageDataCaches(updateItems);
}
// 更新浮动元素的storeId
export function updateFloatStoreId(snapshot: any, dynamicZqUniverStoreId: string) {
const { resources, ...rest } = snapshot ?? {};
if (!resources) {
return '{}';
}
const updatedResources = resources?.map((resource: any) => {
if (resource?.name === 'SHEET_DRAWING_PLUGIN') {
const parseResourceData = JSON.parse(resource?.data ?? '{}');
const updatedParseResourceData = Object.fromEntries(
Object.entries(parseResourceData)?.map(([sheetKey, sheetResourceOption]: any) => {
const { data: sheetResourceOptionData = {}, order: originalOrder = [] } = sheetResourceOption || {};
const sheetOrder = [...originalOrder]; // 使用一个可变的引用来追踪顺序更新
const updatedSheetData = Object.fromEntries(
Object.entries(sheetResourceOptionData).map(([floatDomKey, floatDomValue]: any) => {
const { componentKey, data } = floatDomValue || {};
if (componentKey === ZqUniverFloatEchartKey || componentKey === ZqUniverFloatImageKey) {
const newData = {
...data,
piniaStoreId: dynamicZqUniverStoreId,
};
return [
floatDomKey,
{
...floatDomValue,
data: newData,
},
];
} else {
return [floatDomKey, floatDomValue];
}
}),
);
return [sheetKey, { ...sheetResourceOption, data: updatedSheetData, order: sheetOrder }];
}),
);
return {
...resource,
data: JSON.stringify(updatedParseResourceData),
};
} else {
return resource;
}
});
return JSON.stringify({
...rest,
resources: updatedResources,
});
}
@@ -0,0 +1,45 @@
const hexList: string[] = [];
for (let i = 0; i <= 15; i++) {
hexList[i] = i.toString(16);
}
export function buildUUID(): string {
let uuid = '';
for (let i = 1; i <= 36; i++) {
switch (i) {
case 9:
case 14:
case 19:
case 24: {
uuid += '-';
break;
}
case 15: {
uuid += 4;
break;
}
case 20: {
uuid += hexList[(Math.random() * 4) | 8];
break;
}
default: {
uuid += hexList[(Math.random() * 16) | 0];
}
}
}
return uuid.replaceAll('-', '');
}
export function buildBitUUID(length = 6): string {
return buildUUID().slice(0, Math.max(0, length));
}
let unique = 0;
export function buildShortUUID(prefix = ''): string {
const time = Date.now();
const random = Math.floor(Math.random() * 1_000_000_000);
unique++;
return `${prefix}_${random}${unique}${String(time)}`;
}
@@ -0,0 +1,254 @@
<script lang="ts" setup>
import { nextTick, reactive, ref, toRefs, unref } from 'vue';
import { Button as AButton, Modal as AModal } from 'ant-design-vue';
import type { CreateUnitProps } from '../Design/index';
import ZqUniverDesign from '../Design/index.vue';
import { useZqUniverStore } from '../Design/store';
import ZqUniverCellEchart from '../Design/univer/components/Echart/cell.vue';
import ZqUniverFloatEchartVM from '../Design/univer/components/Echart/floatVM.vue';
const emits = defineEmits(['print']);
const zqUniverDesignPreviewRef = ref();
const zqUniverFloatEchartVMRef = ref();
const zqUniverCellEchartRef = ref();
const state = reactive<{
echartsImageCache: any;
internalOpen: boolean;
zqUniverAPI: any;
zqUniverStore: any;
}>({
echartsImageCache: {},
internalOpen: false,
zqUniverAPI: null,
zqUniverStore: null,
});
const { echartsImageCache, internalOpen, zqUniverStore } = toRefs(state);
// 创建预览实例
function handleCreatePreviewUnit(options?: CreateUnitProps) {
const { cellEcharts: cellEchartsObject, defaultActiveSheetId, floatEcharts, floatImages, snapshot } = options ?? {};
if (snapshot === undefined) {
console.warn('预览数据错误');
return;
}
state.internalOpen = true;
const updateSheets = JSON.parse(JSON.stringify(snapshot?.sheets ?? {}));
nextTick(() => {
// 单元格图表数据转成成数组
const cellEchartsArray = cellEchartsObjectToArray(cellEchartsObject);
// 单元格图表转单元格图片
cellEchartsArray.forEach(cellEchartOption => {
const { colKey, drawingId, height: echartHeight, option: echartOption, rowKey, sheetKey, width: echartWidth } = cellEchartOption;
const echartImg = unref(zqUniverCellEchartRef).render(echartOption, echartWidth, echartHeight);
try {
const targetDrawing = updateSheets?.[sheetKey]?.cellData?.[rowKey]?.[colKey]?.p?.drawings?.[drawingId];
updateSheets[sheetKey].cellData[rowKey][colKey].p.drawings[drawingId] = {
...targetDrawing,
source: echartImg,
};
} catch {
console.log('渲染单元格图表失败');
}
});
// 将单元格图表的图片更新到快照中
snapshot.sheets = JSON.parse(JSON.stringify(updateSheets));
// 构建univer实例
const { zqUniverAPI } = unref(zqUniverDesignPreviewRef)?.handleCreateDesignUnit({
defaultActiveSheetId,
floatEcharts,
floatImages,
loading: true,
mode: 'preview',
snapshot,
uiContextMenu: false,
uiHeader: false,
workbookReadonly: true,
});
state.zqUniverAPI = zqUniverAPI ?? null;
// 创建悬浮图表的图片
createFloatEchartsImage(floatEcharts);
});
}
// 打印
function handlePrint() {
const info = getInstanceInfo();
emits('print', { ...info });
}
// 导出
function handleExport() {
const info = getInstanceInfo();
console.log(info?.snapshot);
alert('看控制台的快照');
}
// 关闭弹窗 (建议有个时间缓存周期再关闭弹窗,要销毁设计器)
function handleCancel() {
unref(zqUniverDesignPreviewRef)?.handleDisposeUnit();
resetUniverState(); // 重置实例状态
}
// 单元格图表数据转成成数组
function cellEchartsObjectToArray(data: any) {
const res: any[] = [];
for (const sheetKey in data) {
const rows = data[sheetKey];
for (const rowKey in rows) {
const cols = rows[rowKey];
for (const colKey in cols) {
const { drawingId, height, option, width } = cols[colKey];
res.push({
colKey,
drawingId,
height,
option,
rowKey,
sheetKey,
width,
});
}
}
}
return res;
}
// 创建悬浮图表的图片信息
function createFloatEchartsImage(echarts: any) {
const dynamicZqUniverStoreId = unref(zqUniverDesignPreviewRef)?.getDynamicStoreId();
if (!dynamicZqUniverStoreId) {
return;
}
const zqUniverStore = useZqUniverStore(dynamicZqUniverStoreId);
state.zqUniverStore = zqUniverStore; // 先赋值给 state
const caches = { ...unref(zqUniverStore.floatEchartToUniverImgDataCaches) };
for (const sheetKey in echarts) {
const target = echarts[sheetKey];
const { domId, height, option, width } = target;
const res = unref(zqUniverFloatEchartVMRef).render(domId, option, width, height);
caches[res.domId] = res?.source ?? {};
Object.assign(state.echartsImageCache, caches); // 直接修改原对象,保持响应性
}
}
// 获取实例数据
function getInstanceInfo() {
zqUniverStore.value?.setFloatEchartToUniverImgDataCaches(echartsImageCache.value);
const { snapshot } = unref(zqUniverDesignPreviewRef)?.getPreviewWorkbookData();
const defaultActiveSheetId = unref(zqUniverDesignPreviewRef)?.getActiveWorksheetId();
return {
defaultActiveSheetId,
snapshot,
};
}
// 重置实例状态
function resetUniverState() {
state.zqUniverAPI = null;
state.zqUniverStore = null;
state.echartsImageCache = {};
}
defineExpose({ handleCreatePreviewUnit });
</script>
<template>
<AModal v-model:open="internalOpen" title="设计预览" width="100%" wrap-class-name="preview-full-modal" @cancel="handleCancel">
<!-- 测试按钮随时可以删除掉 -->
<div class="buttons-wrap">
<AButton type="primary" @click="handlePrint">打印预览</AButton>
<AButton style="margin-left: 10px; background: deeppink" type="primary" @click="handleExport">导出快照</AButton>
</div>
<div class="zq-univer-preview-module">
<ZqUniverDesign v-if="internalOpen" ref="zqUniverDesignPreviewRef" />
<div id="floatEchartVMContent" class="cell-echarts-vm-content">
<ZqUniverFloatEchartVM ref="zqUniverFloatEchartVMRef" univer-create-mode="preview" />
</div>
<div id="cellEchartsContent" class="cell-echarts-content">
<ZqUniverCellEchart ref="zqUniverCellEchartRef" univer-create-mode="preview" />
</div>
</div>
</AModal>
</template>
<style lang="scss">
.preview-full-modal {
.ant-modal {
top: 0;
max-width: 100%;
padding-bottom: 0;
margin: 0;
}
.ant-modal-content {
display: flex;
flex-direction: column;
height: calc(100vh);
padding: 20px 10px 0;
}
.ant-modal-body {
display: flex;
flex: 1;
flex-direction: column;
.zq-univer-preview-module {
position: relative;
flex: 1;
}
}
.ant-modal-footer {
display: none;
}
// 隐藏的dom节点
.cell-echarts-content {
position: absolute;
inset: 0;
z-index: -1;
}
// 隐藏的dom节点
.cell-echarts-vm-content {
position: absolute;
inset: 0;
z-index: -2;
}
}
</style>
@@ -0,0 +1,229 @@
<script lang="ts" setup>
import { nextTick, reactive, toRefs, watch } from 'vue';
import { $t } from '@vben/locales';
import {
ElForm,
ElFormItem,
ElOption,
ElRadioButton,
ElRadioGroup,
ElSelect,
ElSwitch,
} from 'element-plus';
import { debounce } from 'lodash-es';
import {
jnpfPaperPaddingTypeOptions,
jnpfPaperTypeOptions,
jnpfPrintAreaOptions,
jnpfPrintDirectionOptions,
jnpfPrintHAlignOptions,
jnpfPrintScaleOptions,
jnpfPrintVAlignOptions,
} from '../../Design/univer/utils/define';
const props = defineProps(['defaultPrintConfig', 'sheetFreezeStatus']);
const emits = defineEmits(['change']);
interface State {
formData: Record<string, any>;
}
const state = reactive<State>({
formData: {},
});
const { formData } = toRefs(state);
let isInitialized = false;
function getRadioGroupOptions(data: any[] = []) {
return data?.map((item: any) => ({
label: item.fullName,
value: item.id,
}));
}
function labelForOption(item: { fullName: string; id: string }) {
const key = `report-manager.printForm.options.${item.id}`;
const text = $t(key);
return text === key ? item.fullName : text;
}
function labelForAlignOption(
item: { fullName: string; id: string },
group: 'hAlign' | 'vAlign',
) {
const key = `report-manager.printForm.options.${group}.${item.id}`;
const text = $t(key);
return text === key ? item.fullName : text;
}
watch(
() => props.sheetFreezeStatus,
() => {
const updatePrintConfig = {
...props.defaultPrintConfig,
xFreeze: props.sheetFreezeStatus?.colHasFreeze,
yFreeze: props.sheetFreezeStatus?.rowHasFreeze,
};
state.formData = JSON.parse(JSON.stringify(updatePrintConfig));
nextTick(() => {
isInitialized = true;
});
},
{ immediate: true },
);
const emitChangeDebounced = debounce((val: any) => {
emits('change', val);
}, 300);
watch(
() => state.formData,
(val) => {
if (isInitialized) {
emitChangeDebounced(val);
}
},
{ deep: true, flush: 'post' },
);
</script>
<template>
<div class="print-render-form-content m-3 rounded-[8px]">
<ElForm :model="formData" label-position="top" size="small">
<ElFormItem :label="$t('report-manager.printForm.printArea')">
<ElSelect v-model="formData.printArea" class="w-full">
<ElOption
v-for="item in jnpfPrintAreaOptions"
:key="item.id"
:label="labelForOption(item)"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem :label="$t('report-manager.printForm.paperType')">
<ElSelect v-model="formData.paperType" class="w-full">
<ElOption
v-for="item in jnpfPaperTypeOptions"
:key="item.id"
:label="labelForOption(item)"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem :label="$t('report-manager.printForm.padding')">
<ElSelect v-model="formData.padding" class="w-full">
<ElOption
v-for="item in jnpfPaperPaddingTypeOptions"
:key="item.id"
:label="labelForOption(item)"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem :label="$t('report-manager.printForm.direction')">
<ElRadioGroup v-model="formData.direction" size="small">
<ElRadioButton
v-for="opt in getRadioGroupOptions(jnpfPrintDirectionOptions)"
:key="opt.value"
:value="opt.value"
>
{{ labelForOption({ fullName: opt.label, id: opt.value }) }}
</ElRadioButton>
</ElRadioGroup>
</ElFormItem>
<ElFormItem :label="$t('report-manager.printForm.scale')">
<ElSelect v-model="formData.printScale" class="w-full">
<ElOption
v-for="item in jnpfPrintScaleOptions"
:key="item.id"
:label="labelForOption(item)"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem :label="$t('report-manager.printForm.hAlign')">
<ElRadioGroup v-model="formData.hAlign" size="small">
<ElRadioButton
v-for="opt in getRadioGroupOptions(jnpfPrintHAlignOptions)"
:key="opt.value"
:value="opt.value"
>
{{ labelForAlignOption({ fullName: opt.label, id: opt.value }, 'hAlign') }}
</ElRadioButton>
</ElRadioGroup>
</ElFormItem>
<ElFormItem :label="$t('report-manager.printForm.vAlign')">
<ElRadioGroup v-model="formData.vAlign" size="small">
<ElRadioButton
v-for="opt in getRadioGroupOptions(jnpfPrintVAlignOptions)"
:key="opt.value"
:value="opt.value"
>
{{ labelForAlignOption({ fullName: opt.label, id: opt.value }, 'vAlign') }}
</ElRadioButton>
</ElRadioGroup>
</ElFormItem>
<ElFormItem :label="$t('report-manager.printForm.gridlines')">
<ElSwitch v-model="formData.gridlines" />
</ElFormItem>
<div class="flex gap-2">
<ElFormItem :label="$t('report-manager.printForm.workbookTitle')" class="flex-1">
<ElSwitch v-model="formData.workbookTitle" />
</ElFormItem>
<ElFormItem :label="$t('report-manager.printForm.worksheetTitle')" class="flex-1">
<ElSwitch v-model="formData.worksheetTitle" />
</ElFormItem>
</div>
<div class="flex gap-2">
<ElFormItem :label="$t('report-manager.printForm.printDate')" class="flex-1">
<ElSwitch v-model="formData.printDate" />
</ElFormItem>
<ElFormItem :label="$t('report-manager.printForm.printTime')" class="flex-1">
<ElSwitch v-model="formData.printTime" />
</ElFormItem>
</div>
<ElFormItem :label="$t('report-manager.printForm.pageNumber')">
<ElSwitch v-model="formData.pageNumber" />
</ElFormItem>
<div class="flex gap-2">
<ElFormItem :label="$t('report-manager.printForm.yFreeze')" class="flex-1">
<ElSwitch
v-model="formData.yFreeze"
:disabled="!sheetFreezeStatus?.rowHasFreeze"
/>
</ElFormItem>
<ElFormItem :label="$t('report-manager.printForm.xFreeze')" class="flex-1">
<ElSwitch
v-model="formData.xFreeze"
:disabled="!sheetFreezeStatus?.colHasFreeze"
/>
</ElFormItem>
</div>
</ElForm>
</div>
</template>
<style lang="scss">
.print-render-form-content {
box-sizing: border-box;
width: 290px;
height: calc(100% - 24px);
padding: 16px;
overflow: hidden auto;
background: var(--el-bg-color, #fff);
.el-form-item {
margin-bottom: 9px;
}
.el-form-item__label {
padding-bottom: 2px;
}
}
</style>
@@ -0,0 +1,87 @@
<script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, reactive, ref, toRefs, unref } from 'vue';
import { ZqSheetsPrintUiService } from '../../../Design/univer/services/sheet-print-ui.service';
import { printA4PaperHeight, printA4PaperWidth } from '../../../Design/univer/utils/define';
const props = defineProps(['zqUniverApi', 'pageLayout', 'printConfig', 'watermarkConfig']);
const emits = defineEmits(['pageRendered']);
// ✅ 页面容器引用
const pageContainerRef = ref<HTMLElement | null>(null);
const defaultContainerStyle = {
height: printA4PaperHeight,
width: printA4PaperWidth,
};
const state = reactive<{
containerStyle: any;
renderInstance: any;
}>({
containerStyle: defaultContainerStyle,
renderInstance: null,
});
const { containerStyle } = toRefs(state);
onMounted(() => {
const { zqUniverApi, pageLayout = {}, printConfig = {}, watermarkConfig = {} } = props ?? {};
const accessor = zqUniverApi?.getInjector();
if (!accessor) {
return;
}
const { paperSize = {} } = pageLayout;
const containerScale = countContainerScale(paperSize); // 打印预览容器的伸缩比
state.renderInstance = new ZqSheetsPrintUiService(accessor, pageLayout, printConfig, watermarkConfig, containerScale);
if (!state.renderInstance) {
return;
}
const { h: paperSizeHeight = printA4PaperHeight, w: paperSizeWidth = printA4PaperWidth } = paperSize;
state.containerStyle = {
height: `${paperSizeHeight * containerScale}px`,
width: `${paperSizeWidth * containerScale}px`,
};
state.renderInstance.container.style.cssText = 'width: 100%; height: 100%;';
nextTick(() => {
unref(pageContainerRef)?.append(state.renderInstance.container);
state.renderInstance.container.append(state.renderInstance.root);
state.renderInstance?.setDirty(true); // 设置脏状态(用于调整视口大小和滚动值的状态)
state.renderInstance?.renderPage(); // 渲染页面
state.renderInstance?.renderOnReady(); // 主要是在页面变换时确保页面内容的及时更新,并且做了订阅和销毁的管理。
emits('pageRendered');
});
});
onUnmounted(async () => {
await state.renderInstance?.dispose();
state.renderInstance = null;
state.containerStyle = defaultContainerStyle;
});
// 计算打印预览容器的伸缩比
function countContainerScale(paperSize: any) {
const { w = printA4PaperWidth } = paperSize;
return Math.round((printA4PaperWidth / w) * 10) / 10;
}
</script>
<template>
<div ref="pageContainerRef" :style="containerStyle" class="print-page-container"></div>
</template>
<style lang="scss">
.print-page-container {
position: relative;
flex: 0 0 auto;
margin: 28px auto;
background-color: #fff;
box-shadow: 0 4px 24px 0 rgb(30 34 43 / 8%);
}
</style>
@@ -0,0 +1,584 @@
<script lang="ts" setup>
import { computed, nextTick, reactive, ref, toRefs } from 'vue';
import { debounce } from 'lodash-es';
import { ZqSheetsPrintUiService } from '../../Design/univer/services/sheet-print-ui.service';
import { getPrintPageStyle } from '../../Design/univer/utils';
import { printA4PaperHeight, printA4PaperWidth } from '../../Design/univer/utils/define';
import ZqUniverPrintRenderPage from './Page/index.vue';
defineOptions({ name: 'ZqUniverPrintRender' });
const props = defineProps(['zqUniverApi']);
const emits = defineEmits(['previewRenderProgress', 'printRenderProgress', 'browserPrintShow']);
const batchRenderSize = 10; // 分页加载的数量
const pagesContainerRef = ref();
const state = reactive<{
completePagesLayout: any[];
currentViewPageNumber: number;
pagingRenderedQuantity: number;
pagingTargetQuantity: number;
paperSize: any;
paperViewHeight: number;
printConfig: any;
printRenderInstances: any[];
processPagesLayout: any[];
totalRenderedQuantity: number;
watermarkConfig: any;
}>({
completePagesLayout: [], // 完整的页面架构
currentViewPageNumber: 1, // 当前视图页码
pagingRenderedQuantity: 0,
pagingTargetQuantity: 0,
paperSize: null,
paperViewHeight: printA4PaperHeight,
printConfig: null,
printRenderInstances: [], // 打印渲染的实例
processPagesLayout: [], // 进程中页面架构
totalRenderedQuantity: 0,
watermarkConfig: {},
});
const { completePagesLayout, currentViewPageNumber, printConfig, processPagesLayout, watermarkConfig } = toRefs(state);
// 禁止向前翻页状态
const disabledForward = computed(() => {
return state.currentViewPageNumber <= 1;
});
// 禁止向后翻页状态
const disabledBackward = computed(() => {
return state.currentViewPageNumber >= state.completePagesLayout.length;
});
// 翻页 - 去首页
function setFirstViewPageNumber() {
state.currentViewPageNumber = 1;
handleScrollToPosition();
}
// 翻页 - 去尾页
function setLastViewPageNumber() {
state.currentViewPageNumber = state.completePagesLayout.length;
handleScrollToPosition();
}
// 翻页 - 前后单页控制
function setSingleViewPageNumber(direction: 'next' | 'prev') {
const maxPage = state.completePagesLayout.length;
const currentPage = state.currentViewPageNumber;
// 根据方向计算目标页码
const newPageNumber =
direction === 'prev'
? Math.max(currentPage - 1, 1) // 前一页,不能小于 1
: Math.min(currentPage + 1, maxPage); // 下一页,不能大于总页数
// 如果页码发生变化,更新并滚动
if (newPageNumber !== currentPage) {
state.currentViewPageNumber = newPageNumber;
handleScrollToPosition();
}
}
// 翻页 - 直接输入页码
function setInputViewPageNumber() {
const pageNumber = Number(state.currentViewPageNumber);
if (Number.isNaN(pageNumber) || pageNumber < 1) {
state.currentViewPageNumber = 1;
} else if (pageNumber > state.completePagesLayout?.length) {
state.currentViewPageNumber = state.completePagesLayout.length;
}
handleScrollToPosition();
}
// 设置滚动的位置
function handleScrollToPosition() {
const targetScrollTop = state.paperViewHeight * (state.currentViewPageNumber - 1) + 28 * (state.currentViewPageNumber - 1);
pagesContainerRef.value.scrollTo({
behavior: 'smooth',
top: targetScrollTop,
});
}
// 定义防抖后的函数
const debouncedOnPagesContainerScroll = debounce(() => {
const scrollTop = pagesContainerRef.value.scrollTop;
state.currentViewPageNumber = Math.ceil((scrollTop + 28) / (state.paperViewHeight + 28));
}, 100); // 100 毫秒的防抖时间,可以根据需要调整
// 开启监听容器滚动事件
function onPagesContainerScroll() {
if (pagesContainerRef.value) {
pagesContainerRef.value.addEventListener('scroll', debouncedOnPagesContainerScroll);
}
}
// 关闭监听容器滚动事件
function offPagesContainerScroll() {
if (pagesContainerRef.value) {
pagesContainerRef.value.removeEventListener('scroll', debouncedOnPagesContainerScroll);
}
}
// 暴露 - 处理打印渲染
async function handlePrintRender(printConfig: any, watermarkConfig: any) {
resetState();
state.watermarkConfig = watermarkConfig ?? {};
const { zqUniverApi } = props ?? {};
try {
if (!zqUniverApi) {
emits('previewRenderProgress', {
rendered: 0,
total: 0,
});
return;
}
const res = await getLayoutsAsync(printConfig);
const { pagesLayout, paperSize, printConfig: updatePrintConfig } = (res ?? {}) as any;
if (!pagesLayout.length) {
emits('previewRenderProgress', {
rendered: 0,
total: 0,
});
return;
}
state.completePagesLayout = Array.isArray(pagesLayout) ? [...pagesLayout] : [];
state.printConfig = { ...updatePrintConfig };
state.paperSize = { ...paperSize };
batchRenderPreviewPages();
} catch (error) {
console.error('Error:', error);
}
}
// 获得布局的结果
function getLayoutsAsync(config: any) {
const { zqUniverApi } = props ?? {};
return new Promise((resolve, reject) => {
try {
const result = zqUniverApi?.getSheetsPrint()?.getLayouts(config);
resolve(result);
} catch (error) {
reject(error);
}
});
}
// 批量加载预览页面
function batchRenderPreviewPages() {
const { completePagesLayout, processPagesLayout } = state;
state.pagingRenderedQuantity = 0;
// 全部都渲染完了
if (processPagesLayout.length >= completePagesLayout.length) {
state.pagingTargetQuantity = 0;
return;
}
if (completePagesLayout.length <= batchRenderSize) {
state.pagingTargetQuantity = completePagesLayout.length;
state.processPagesLayout = [...completePagesLayout];
} else {
const lastPageIndex = processPagesLayout.length;
const batchData = completePagesLayout.slice(lastPageIndex, lastPageIndex + batchRenderSize) ?? [];
state.pagingTargetQuantity = batchData.length;
state.processPagesLayout = [...processPagesLayout, ...batchData];
}
}
// 预览页面渲染回调
function handlePreviewPageRendered() {
state.pagingRenderedQuantity += 1;
state.totalRenderedQuantity += 1;
emits('previewRenderProgress', {
rendered: state.totalRenderedQuantity,
total: state.completePagesLayout.length,
});
if (state.pagingRenderedQuantity === state.pagingTargetQuantity && state.processPagesLayout.length < state.completePagesLayout.length) {
setTimeout(batchRenderPreviewPages, 500);
} else if (state.pagingRenderedQuantity === state.pagingTargetQuantity && state.processPagesLayout.length >= state.completePagesLayout.length) {
nextTick(() => {
const firstPageDom = document.getElementsByClassName('print-page-container')?.[0] as HTMLElement | undefined;
if (firstPageDom) {
const styleHeight = firstPageDom?.style?.height;
state.paperViewHeight = Number.parseInt(styleHeight);
} else {
state.paperViewHeight = printA4PaperHeight;
}
// 绑定容器的滚动事件
onPagesContainerScroll();
});
}
}
// 暴露 - 浏览器打印
function handleBrowserPrint() {
emits('printRenderProgress', {
rendered: 0,
total: state.completePagesLayout.length,
});
state.printRenderInstances = [];
const { zqUniverApi } = props ?? {};
const accessor = zqUniverApi?.getInjector();
if (!accessor) {
return;
}
batchRenderPrintPages(accessor);
}
// 批量加载打印页面
function batchRenderPrintPages(accessor: any) {
const { completePagesLayout, printConfig, printRenderInstances } = state;
// 已经全部渲染完成,直接返回
if (printRenderInstances.length >= completePagesLayout.length) {
createPrintEle();
return;
}
// 获取未渲染的页面数据
const lastPageIndex = printRenderInstances.length;
const batchData = completePagesLayout.slice(lastPageIndex, lastPageIndex + batchRenderSize) ?? [];
if (!batchData.length) {
return printRenderInstances;
}
// 批量创建渲染实例
const renderInstances = batchData.map((pageLayout: any) => {
// 这里要加水印配置
const renderInstance = new ZqSheetsPrintUiService(accessor, pageLayout, printConfig, state.watermarkConfig);
renderInstance.container.style.cssText = 'width: 100%; height: 100%;';
return renderInstance;
});
// 更新数据
state.printRenderInstances.push(...renderInstances);
emits('printRenderProgress', {
rendered: state.printRenderInstances.length,
total: state.completePagesLayout.length,
});
setTimeout(() => batchRenderPrintPages(accessor), 500);
}
// 创建打印节点
function createPrintEle() {
const { completePagesLayout, paperSize, printConfig, printRenderInstances } = state;
const { h: paperSizeHeight, w: paperSizeWidth = printA4PaperWidth } = paperSize ?? {};
const { direction, paperType } = printConfig ?? {};
const printStyle = getPrintPageStyle(paperType, direction);
const containerEle = document.createElement('div');
containerEle.id = 'jnpfReportPrint';
printRenderInstances.forEach((renderInstance: any, index: number) => {
const pageLayout = completePagesLayout[index];
if (pageLayout) {
const { browserPreviewScale } = pageLayout ?? {};
const scale = Math.round(browserPreviewScale * (printA4PaperWidth / paperSizeWidth) * 10) / 10;
const pageEle = document.createElement('div');
pageEle.className = 'printContainer';
Object.assign(pageEle.style, {
height: `${paperSizeHeight * scale}px`,
width: `${paperSizeWidth * scale}px`,
});
pageEle.append(renderInstance.container);
containerEle.append(pageEle);
} else {
containerEle.append(renderInstance.container);
}
});
document.head.append(printStyle);
document.body.append(containerEle);
nextTick(() => {
setTimeout(() => {
emits('browserPrintShow');
window.addEventListener('beforeprint', () => {
printRenderInstances.forEach((renderInstance: any) => {
renderInstance?.setDirty(true); // 设置脏状态(用于调整视口大小和滚动值的状态)
renderInstance?.renderPage(); // 渲染页面
renderInstance?.renderOnReady(); // 主要是在页面变换时确保页面内容的及时更新,并且做了订阅和销毁的管理。
});
});
window.addEventListener('afterprint', () => {
printRenderInstances.forEach(async (renderInstance: any) => {
await renderInstance.dispose();
renderInstance = null;
});
printStyle.remove();
containerEle.remove();
});
window.print();
}, 100);
});
}
// 暴露 - 重置
function resetState() {
if (pagesContainerRef.value) {
pagesContainerRef.value.scrollTop = 0;
}
state.completePagesLayout = [];
state.processPagesLayout = [];
state.printRenderInstances = [];
state.pagingTargetQuantity = 0;
state.pagingRenderedQuantity = 0;
state.totalRenderedQuantity = 0;
state.printConfig = null;
state.paperSize = null;
state.paperViewHeight = printA4PaperHeight;
state.currentViewPageNumber = 1;
state.watermarkConfig = {};
offPagesContainerScroll();
}
defineExpose({ handleBrowserPrint, handlePrintRender, resetState });
</script>
<template>
<div class="print-render-content">
<div class="tools-wrap">
<span class="first-page" :class="[{ disabled: disabledForward }]" @click="setFirstViewPageNumber" title="首页">
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path
d="M507.733333 490.666667L768 230.4 704 170.666667 384 490.666667l320 320 59.733333-59.733334-256-260.266666zM341.333333 170.666667H256v640h85.333333V170.666667z" />
</svg>
<em>首页</em>
</span>
<span class="prev-page" :class="[{ disabled: disabledForward }]" @click="setSingleViewPageNumber('prev')" title="上一页" style="margin-right: 12px">
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path
d="M473.6 490.666667L789.333333 170.666667 853.333333 230.4l-260.266666 260.266667 260.266666 260.266666-64 59.733334-315.733333-320z m-302.933333 0L490.666667 170.666667l59.733333 59.733333-260.266667 260.266667 260.266667 260.266666-59.733333 59.733334L170.666667 490.666667z" />
</svg>
<em>上一页</em>
</span>
<input class="target-page" v-model="currentViewPageNumber" type="number" @blur="setInputViewPageNumber" min="1" :max="completePagesLayout.length" />
<span class="total-page-number">
<em>/</em>
<strong>{{ completePagesLayout.length }}</strong>
</span>
<span class="next-page" :class="[{ disabled: disabledBackward }]" @click="setSingleViewPageNumber('next')" title="下一页" style="margin-left: 12px">
<em>下一页</em>
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path
d="M550.4 490.666667L230.4 170.666667 170.666667 230.4l260.266666 260.266667L170.666667 750.933333 230.4 810.666667l320-320z m298.666667 0L533.333333 170.666667 469.333333 230.4l260.266667 260.266667-260.266667 260.266666 59.733334 59.733334 320-320z" />
</svg>
</span>
<span class="last-page" :class="[{ disabled: disabledBackward }]" @click="setLastViewPageNumber" title="尾页">
<em>尾页</em>
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path
d="M516.266667 490.666667L256 230.4 315.733333 170.666667l320 320L315.733333 810.666667 256 750.933333l260.266667-260.266666zM678.4 170.666667h85.333333v640h-85.333333V170.666667z" />
</svg>
</span>
</div>
<div ref="pagesContainerRef" class="print-render-pages-content">
<template v-for="pageLayout in processPagesLayout" :key="pageLayout?.layoutId">
<ZqUniverPrintRenderPage
:zq-univer-api="props.zqUniverApi"
:page-layout="pageLayout"
:print-config="printConfig"
:watermark-config="watermarkConfig"
@page-rendered="handlePreviewPageRendered" />
</template>
</div>
</div>
</template>
<style lang="scss">
.print-render-content {
display: flex;
flex: 1;
flex-direction: column;
overflow: auto;
.tools-wrap {
display: flex;
justify-content: center;
min-width: 500px;
padding: 10px 0;
user-select: none;
.first-page,
.prev-page,
.next-page,
.last-page {
display: flex;
align-items: center;
justify-content: center;
//width: 32px;
height: 32px;
padding: 0 8px;
color: rgb(0 0 0 / 88%);
cursor: pointer;
border-radius: 6px;
transition: all 0.2s;
.icon {
fill: rgb(0 0 0 / 88%);
}
&.disabled {
color: rgb(0 0 0 / 25%);
cursor: not-allowed;
.icon {
fill: rgb(0 0 0 / 25%);
}
}
em {
margin-left: 2px;
font-size: 14px;
font-style: normal;
}
&:hover {
background: rgb(0 0 0 / 6%);
}
}
.target-page {
box-sizing: border-box;
width: 48px;
height: 32px;
font-size: 14px;
color: rgb(0 0 0 / 88%);
text-align: center;
appearance: textfield;
appearance: none;
outline: none;
background: #fff;
border: 1px solid #d9d9d9;
border-radius: 6px;
transition: all 0.2s;
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
margin: 0;
appearance: none;
}
}
.total-page-number {
display: flex;
line-height: 32px;
color: rgb(0 0 0 / 88%);
em {
margin: 0 4px;
font-style: normal;
color: #1677ff;
}
strong {
font-weight: normal;
}
}
}
.print-render-pages-content {
position: relative;
flex: 1;
overflow: auto;
.print-page-container:first-child {
margin-top: 0;
}
}
}
.dark,
.dark[data-theme='custom'],
.dark[data-theme='default'] {
.print-render-content {
color: #fff;
.tools-wrap {
.first-page,
.prev-page,
.next-page,
.last-page,
.total-page-number {
color: #fff;
em {
color: #fff;
}
&.disabled {
color: rgb(255 255 255 / 70%);
em {
color: rgb(255 255 255 / 70%);
}
.icon {
fill: rgb(255 255 255 / 70%);
}
}
}
.icon {
fill: #fff;
}
}
}
}
</style>
@@ -0,0 +1,320 @@
<script lang="ts" setup>
import { nextTick, reactive, ref, toRefs, unref } from 'vue';
import type { IWorkbookData } from '@univerjs/core';
import { SetWorksheetActiveOperation } from '@univerjs/sheets';
import { $t } from '@vben/locales';
import { ElButton, ElDialog } from 'element-plus';
import ZqUniverDesign from '../Design/index.vue';
import { DefaultPrintConfig } from '../Design/univer/utils/define';
import ReportDialogHeader from '../shared/ReportDialogHeader.vue';
import ZqUniverPrintForm from './Form/index.vue';
import ZqUniverPrintRender from './Render/index.vue';
defineOptions({ name: 'ZqUniverPrint' });
const zqUniverDesignPrintRef = ref();
const zqUniverPrintRenderRef = ref();
const defaultRenderProgressTip = ' ';
const defaultFreezeStatus = {
colHasFreeze: false,
rowHasFreeze: false,
};
interface State {
internalOpen: boolean;
zqUniverAPI: any;
loading: boolean;
printRenderTip: string;
sheetFreezeStatus: any;
watermarkConfig: Record<string, any>;
reportName: string;
reportCode: string;
}
const state = reactive<State>({
internalOpen: false,
zqUniverAPI: null,
loading: false,
printRenderTip: defaultRenderProgressTip,
sheetFreezeStatus: { ...defaultFreezeStatus },
watermarkConfig: {},
reportName: '',
reportCode: '',
});
const {
internalOpen,
zqUniverAPI,
loading,
printRenderTip,
sheetFreezeStatus,
watermarkConfig,
reportName,
reportCode,
} = toRefs(state);
function handleCreatePrintUnit(data: {
defaultActiveSheetId?: string;
snapshot: IWorkbookData;
watermarkConfig?: Record<string, any>;
reportName?: string;
reportCode?: string;
}) {
state.internalOpen = true;
state.printRenderTip = defaultRenderProgressTip;
state.loading = true;
state.watermarkConfig = data?.watermarkConfig ?? {};
state.reportName = data?.reportName ?? '';
state.reportCode = data?.reportCode ?? '';
void initPrintUnit(data);
}
async function initPrintUnit(data: {
defaultActiveSheetId?: string;
snapshot: IWorkbookData;
}) {
const { defaultActiveSheetId, snapshot } = data ?? {};
await nextTick();
await nextTick();
try {
const result = await unref(zqUniverDesignPrintRef)?.handleCreateDesignUnit({
defaultActiveSheetId,
loading: false,
mode: 'print',
snapshot,
uiHeader: false,
uiFooter: false,
uiContextMenu: false,
});
state.zqUniverAPI = result?.zqUniverAPI ?? null;
if (!state.zqUniverAPI) {
state.loading = false;
return;
}
state.zqUniverAPI.getHooks()?.onSteady(() => {
const subUnitId =
defaultActiveSheetId ?? state.zqUniverAPI?.getActiveWorkbook()?.getActiveSheet()?.getSheetId();
const { xSplit = 0, ySplit = 0 } = snapshot?.sheets?.[subUnitId ?? '']?.freeze ?? {};
state.sheetFreezeStatus = {
colHasFreeze: xSplit > 0,
rowHasFreeze: ySplit > 0,
};
unref(zqUniverPrintRenderRef)?.handlePrintRender(
JSON.parse(JSON.stringify(DefaultPrintConfig)),
JSON.parse(JSON.stringify(state.watermarkConfig)),
);
});
} catch (error) {
console.error('initPrintUnit failed:', error);
state.loading = false;
}
}
function handleChangePrintConfig(config: any) {
state.printRenderTip = defaultRenderProgressTip;
state.loading = true;
unref(zqUniverPrintRenderRef)?.handlePrintRender(
JSON.parse(JSON.stringify(config)),
JSON.parse(JSON.stringify(state.watermarkConfig)),
);
}
function getPreviewRenderProgress(res: any) {
const { rendered, total } = res ?? {};
state.printRenderTip = `${total}页,已加载${rendered}`;
if (rendered === total) {
state.loading = false;
const activeWorkbook = zqUniverAPI.value?.getActiveWorkbook();
const unitId = activeWorkbook?.getId();
const sheets = activeWorkbook.save().sheets;
const sheetKeys = Object.keys(sheets);
const activeSubUnitId = activeWorkbook?.getActiveSheet()?.getSheetId();
function autoRenderAllSheets(index: number) {
if (index >= sheetKeys?.length) {
zqUniverAPI.value?.executeCommand(SetWorksheetActiveOperation.id, {
subUnitId: activeSubUnitId,
unitId,
});
return;
}
const sheetKey = sheetKeys[index];
zqUniverAPI.value?.executeCommand(SetWorksheetActiveOperation.id, {
subUnitId: sheetKey,
unitId,
});
setTimeout(() => {
autoRenderAllSheets(index + 1);
}, 100);
}
autoRenderAllSheets(0);
}
}
function handleBrowserPrint() {
state.printRenderTip = defaultRenderProgressTip;
state.loading = true;
unref(zqUniverPrintRenderRef)?.handleBrowserPrint();
}
function getPrintRenderProgress(res: any) {
const { rendered, total } = res ?? {};
state.printRenderTip = `${total}页,已准备${rendered}`;
if (rendered === total) {
state.printRenderTip = '整理中...';
}
}
function handleBrowserShow() {
state.loading = false;
}
function handleCancel() {
resetUniverState();
unref(zqUniverDesignPrintRef)?.handleDisposeUnit();
state.internalOpen = false;
}
function resetUniverState() {
state.zqUniverAPI = null;
state.loading = false;
state.printRenderTip = defaultRenderProgressTip;
state.sheetFreezeStatus = JSON.parse(JSON.stringify(defaultFreezeStatus));
state.reportName = '';
state.reportCode = '';
unref(zqUniverPrintRenderRef)?.resetState();
}
defineExpose({ handleCreatePrintUnit });
</script>
<template>
<ElDialog
v-model="internalOpen"
class="zq-print-full-dialog"
fullscreen
destroy-on-close
append-to-body
:show-close="false"
:close-on-click-modal="false"
:z-index="4000"
@close="handleCancel"
>
<div class="bg-background flex h-full min-h-0 flex-col">
<ReportDialogHeader
:report-name="reportName"
:report-code="reportCode"
:fallback-title="$t('report-manager.print.previewTitle')"
>
<template #actions>
<ElButton @click="handleBrowserPrint">
{{ $t('report-manager.print.browserPrint') }}
</ElButton>
<ElButton @click="handleCancel">
{{ $t('common.close') }}
</ElButton>
</template>
</ReportDialogHeader>
<main
v-loading="loading"
class="content-main m-3 min-h-0 flex-1 overflow-hidden rounded-lg"
:element-loading-text="printRenderTip"
>
<div class="print-container">
<div class="print-design-content">
<ZqUniverDesign v-if="internalOpen" ref="zqUniverDesignPrintRef" />
</div>
<div class="print-main-container">
<ZqUniverPrintRender
ref="zqUniverPrintRenderRef"
:zq-univer-api="zqUniverAPI"
@preview-render-progress="getPreviewRenderProgress"
@print-render-progress="getPrintRenderProgress"
@browser-print-show="handleBrowserShow"
/>
<ZqUniverPrintForm
v-if="internalOpen"
:default-print-config="DefaultPrintConfig"
:sheet-freeze-status="sheetFreezeStatus"
@change="handleChangePrintConfig"
/>
</div>
</div>
</main>
</div>
</ElDialog>
</template>
<style lang="scss">
.zq-print-full-dialog.el-dialog {
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
.el-dialog__header {
display: none;
}
.el-dialog__body {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
overflow: hidden;
padding: 0;
}
}
.zq-print-full-dialog {
.content-main {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
}
.print-container {
position: relative;
flex: 1;
min-height: 0;
.print-design-content {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
.zq-univer-design-content {
opacity: 0;
}
}
.print-main-container {
position: absolute;
inset: 0;
z-index: 99;
display: flex;
overflow: hidden;
background: var(--el-bg-color-page, #f2f3f4);
}
}
}
</style>
@@ -0,0 +1,26 @@
import type { App } from 'vue';
import ZqUniver from './Design/index.vue';
import ZqUniverCellEchart from './Design/univer/components/Echart/cell.vue';
import ZqUniverFloatEchartVM from './Design/univer/components/Echart/floatVM.vue';
import ZqUniverPrint from './Print/index.vue';
import ReportDialogHeader from './shared/ReportDialogHeader.vue';
ZqUniver.install = (app: App) => app.component(ZqUniver.name as string, ZqUniver);
ZqUniverPrint.install = (app: App) =>
app.component(ZqUniverPrint.name as string, ZqUniverPrint);
ZqUniverFloatEchartVM.install = (app: App) =>
app.component(ZqUniverFloatEchartVM.name as string, ZqUniverFloatEchartVM);
ZqUniverCellEchart.install = (app: App) =>
app.component(ZqUniverCellEchart.name as string, ZqUniverCellEchart);
export { ZqUniver, ZqUniverCellEchart, ZqUniverFloatEchartVM, ZqUniverPrint, ReportDialogHeader };
export default {
install(app: App) {
app.component(ZqUniver.name as string, ZqUniver);
app.component(ZqUniverPrint.name as string, ZqUniverPrint);
app.component(ZqUniverFloatEchartVM.name as string, ZqUniverFloatEchartVM);
app.component(ZqUniverCellEchart.name as string, ZqUniverCellEchart);
},
};
@@ -0,0 +1,25 @@
<script lang="ts" setup>
defineProps<{
reportName?: string;
reportCode?: string;
fallbackTitle?: string;
}>();
</script>
<template>
<header
class="bg-background-deep mx-3 mt-3 flex h-14 shrink-0 items-center justify-between rounded-lg px-4 shadow-sm"
>
<div class="min-w-0">
<div class="text-foreground truncate font-medium">
{{ reportName || fallbackTitle }}
</div>
<div v-if="reportCode" class="text-muted-foreground truncate text-xs">
{{ reportCode }}
</div>
</div>
<div class="flex shrink-0 gap-2">
<slot name="actions" />
</div>
</header>
</template>
@@ -0,0 +1,10 @@
/**
* Configure and register global directives
*/
import type { App } from 'vue';
import { setupLoadingDirective } from './loading';
export function setupGlobDirectives(app: App) {
setupLoadingDirective(app);
}
@@ -0,0 +1,40 @@
import type { App, Directive } from 'vue';
import { createLoading } from '../components/Loading';
const loadingDirective: Directive = {
mounted(el, binding) {
const tip = el.getAttribute('loading-tip');
const background = el.getAttribute('loading-background');
const size = el.getAttribute('loading-size');
const fullscreen = !!binding.modifiers.fullscreen;
const instance = createLoading(
{
absolute: !fullscreen,
background,
loading: !!binding.value,
size: size || 'large',
tip,
},
fullscreen ? document.body : el,
);
el.instance = instance;
},
unmounted(el) {
el?.instance?.close();
},
updated(el, binding) {
const instance = el.instance;
if (!instance) return;
instance.setTip(el.getAttribute('loading-tip'));
if (binding.oldValue !== binding.value) {
instance.setLoading?.(binding.value && !instance.loading);
}
},
};
export function setupLoadingDirective(app: App) {
app.directive('loading', loadingDirective);
}
export default loadingDirective;
+7
View File
@@ -0,0 +1,7 @@
export {
ZqUniver,
ZqUniverCellEchart,
ZqUniverFloatEchartVM,
ZqUniverPrint,
ReportDialogHeader,
} from './components/Report/index';
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "@vben/tsconfig/web.json",
"compilerOptions": {
"rootDir": "src"
},
"include": ["src/**/*"]
}