import type { RouteLocationRaw } from 'vue-router';
|
|
import type { UserInfo } from '@vben/types';
|
|
import type { DmsPrintOperationOptions } from '#/api/x/dms/printOperation';
|
import type { PrintAuditOptions } from '#/api/x/lims/printAudit';
|
import type { OpenBatchFormConfig } from '#/components/GlobalBatchFormPopup/types';
|
import type { DmsPreviewPrintOptions, OpenOfficeDocumentConfig } from '#/components/GlobalOfficeDocumentModal/types';
|
import type { ReportViewConfig } from '#/components/GlobalReportViewModal/types';
|
import type { PrintAuditTargetMapping } from '#/utils/printAudit';
|
|
import { createApp, h } from 'vue';
|
|
import { useGlobSetting, useMessage } from '@jnpf/hooks';
|
import { isNullOrUnDef, isNumber, isString } from '@jnpf/utils';
|
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
|
import { Spin } from 'ant-design-vue';
|
import dayjs from 'dayjs';
|
import { cloneDeep } from 'lodash-es';
|
import { autoConnect, hiprint, hiwebSocket } from 'vue-plugin-hiprint';
|
import mitt from 'mitt';
|
import { v4 as uuidv4 } from 'uuid';
|
|
import { createAuditCorrelationId } from '#/api/onlineDev/auditHeaders';
|
import { defHttp } from '#/api/request';
|
import { createDmsFilePreviewTicket } from '#/api/x/dms/preview';
|
import { type DmsWatermarkParameters, watermarkParameters } from '#/api/x/dms/watermarkParameters';
|
import { createDmsPrintOperation } from '#/api/x/dms/printOperation';
|
import { createDmsFilePrintTicket } from '#/api/x/dms/print';
|
import { getDmsWenjianZhongxinBanben } from '#/api/x/dms/wenjianBanben';
|
import { recordPrintAudit } from '#/api/x/lims/printAudit';
|
import { buildGlobalAuditRequestHeaders } from '#/components/FormGenerator/src/helper/auditDisplay';
|
import { $t } from '#/locales';
|
import { router } from '#/router';
|
|
import { APP_BACKEND_PREFIX, APP_PREFIX } from './constants';
|
import { ELECTRONIC_SIGNATURE_MODEL_ID, REVIEW_SIGNATURE_MODEL_ID } from './constants/electronicSignature';
|
import { activateCustomViewParams, getCustomViewParam } from './custom-view-context';
|
|
export const JNPF_ROUTE_TITLE_QUERY = 'jnpfTitle';
|
|
// 创建事件总线
|
const emitter = mitt<{
|
CLOSE_REPORT_VIEW: undefined;
|
OPEN_BATCH_FORM: OpenBatchFormConfig;
|
OPEN_CUSTOM_VIEW_MODAL: OpenCustomViewEvent;
|
OPEN_DMS_FOLDER_SELECTOR: OpenDmsFolderSelectorConfig;
|
DMS_REPRINT_COMPLETED: { printRecordId: string; requestId: string };
|
DMS_REPRINT_LOCK_STATUS: import('#/views/x/dms/shared/dmsReprint').DmsReprintLockDialog;
|
OPEN_FLOW_DETAIL: OpenFlowDetailConfig;
|
OPEN_FLOW_EDIT: OpenFlowEditConfig;
|
OPEN_FLOW_FORM: OpenFlowFormConfig;
|
OPEN_FLOW_LIST_MODAL: OpenFlowListConfig;
|
OPEN_FORM_MODAL: OpenFormModalConfig;
|
OPEN_LIST_MODAL: OpenListConfig;
|
OPEN_OFFICE_DOCUMENT: OpenOfficeDocumentConfig;
|
OPEN_PRINT_MODAL: {
|
audit?:
|
| false
|
| (PrintAuditTargetMapping & {
|
bizType?: string;
|
printName?: string;
|
recordTitle?: string;
|
targets?: PrintAuditOptions['targets'];
|
targetTable?: string;
|
});
|
data: any;
|
onDownloadPdf?: (data: any) => void;
|
onError?: (error: any) => void;
|
onPrint?: (data: any) => void;
|
showPdfBtn?: boolean;
|
template: string;
|
title?: string;
|
type: 'html' | 'html-file' | 'vue';
|
};
|
OPEN_REPORT_VIEW: ReportViewConfig;
|
}>();
|
|
interface OnlineUserInfo extends UserInfo {
|
token?: string;
|
}
|
|
export interface PreviewDmsFileOptions extends DmsWatermarkParameters {
|
buildCustomBarcode?: import('#/views/x/dms/shared/dmsPrintBarcode').DmsPrintBarcodeBuilder;
|
reprint?: import('#/components/GlobalOfficeDocumentModal/types').DmsReprintPreview;
|
/** 打印台账任务ID,启用持久化发送与恢复。 */
|
printTaskId?: string;
|
/** 任务累计成功发送份数;作废不返还额度。 */
|
printQuantity?: { total: number; printed: number };
|
/** 预览票据使用的水印场景;空字符串或其他值沿用预览水印。 */
|
watermarkType?: string;
|
/** 用户在预览弹窗点击打印后、创建打印票据前执行。抛错会终止打印。 */
|
beforePrint?: (context: { copies: number }) => Promise<void> | void;
|
/** 客户端确认提交后执行;失败时仅重试本回调,不再次发送打印。 */
|
confirmPrint?: (context: { copies: number }) => Promise<void> | void;
|
/** Hiprint 客户端确认打印任务已成功提交后执行;不代表纸张已实际输出。 */
|
onPrintSuccess?: () => Promise<void> | void;
|
/** 所有打印批次均确认提交后关闭预览弹窗,默认 false。 */
|
closeOnPrintSuccess?: boolean;
|
/** 推荐参数:DMS 资源 ID。 */
|
resourceId?: number | string;
|
/** 与 resourceId 配套的资源类型。 */
|
resourceType?: 'DOCUMENT' | 'FILE_VERSION';
|
/** 兼容参数:业务文件版本 ID。 */
|
versionId?: number | string;
|
/** 兼容参数:文件中心版本 ID。 */
|
wenjianZhongxinId?: number | string;
|
/** 是否显示全局预览弹窗的打印按钮,默认 false。 */
|
showPrintButton?: boolean;
|
/** 本次预览的前端打印参数。 */
|
printOptions?: DmsPreviewPrintOptions;
|
/** 预览格式,默认 pdf;source 表示保留源文件格式。 */
|
previewFormat?: 'pdf' | 'source';
|
}
|
|
interface PrintDmsFileOptions extends PreviewDmsFileOptions {
|
copies?: number;
|
printPages?: string;
|
}
|
|
async function resolveDmsFileCenterVersionId(options?: PreviewDmsFileOptions) {
|
const fileCenterId = isNullOrUnDef(options?.wenjianZhongxinId) ? '' : String(options.wenjianZhongxinId).trim();
|
if (fileCenterId) return fileCenterId;
|
const versionId = isNullOrUnDef(options?.versionId) ? '' : String(options.versionId).trim();
|
if (!versionId) return '';
|
const response = await getDmsWenjianZhongxinBanben(versionId);
|
return response.data?.wenjianZhongxinBanbenId?.trim() || '';
|
}
|
|
interface OpenFlowDetailConfig {
|
f_id?: number | string;
|
f_flow_state?: number | string;
|
flow_id?: number | string;
|
flowId?: number | string;
|
flowState?: number | string;
|
flowTaskId?: number | string;
|
flow_state?: number | string;
|
id?: number | string;
|
isFlow?: number | string;
|
opType?: number | string;
|
operatorId?: number | string;
|
taskId?: number | string;
|
[key: string]: any;
|
}
|
|
interface OpenFlowEditConfig extends OpenFlowDetailConfig {
|
defaultFullscreen?: boolean;
|
hideCancelBtn?: boolean;
|
hideSaveBtn?: boolean;
|
showFullscreen?: boolean;
|
}
|
|
interface OpenFlowFormConfig {
|
data?: Record<string, any>;
|
defaultFullscreen?: boolean;
|
flow_id?: number | string;
|
flowId?: number | string;
|
formData?: Record<string, any>;
|
hideCancelBtn?: boolean;
|
hideSaveBtn?: boolean;
|
id?: number | string;
|
isFlow?: number | string;
|
onSuccess?: () => void;
|
params?: Record<string, any>;
|
query?: Record<string, any>;
|
showFullscreen?: boolean;
|
success?: () => void;
|
template?: number | string;
|
title?: string;
|
[key: string]: any;
|
}
|
|
interface OpenFlowListConfig {
|
flow_id?: number | string;
|
flowId?: number | string;
|
menuId?: number | string;
|
params?: Record<string, any>;
|
path?: string;
|
query?: Record<string, any>;
|
routeQuery?: Record<string, any>;
|
title?: string;
|
}
|
|
interface OpenFormModalConfig {
|
fieldMapping?: Record<string, string>;
|
id?: string;
|
mode?: 'detail' | 'form';
|
modelId: string;
|
onCancel?: () => void;
|
onConfirm?: (data: any) => void;
|
onOpen?: () => void;
|
onOpenError?: (error: unknown) => void;
|
onSubmit?: (data: any) => Promise<void> | void;
|
params?: Record<string, any>;
|
submitMode?: 'custom' | 'default' | 'form';
|
title?: string;
|
type?: 'drawer' | 'fullScreen' | 'modal';
|
width?: string;
|
}
|
|
interface SignMetaData {
|
biz_button?: string;
|
biz_data?: any[];
|
biz_form_id?: string;
|
biz_module?: string;
|
biz_title?: string;
|
is_biz_form?: boolean;
|
is_review_button?: boolean;
|
}
|
|
interface SignConfig {
|
allowMyself?: boolean;
|
isFaceToFace?: boolean;
|
metaData?: SignMetaData;
|
onCancel?: OpenFormModalConfig['onCancel'];
|
onOpen?: OpenFormModalConfig['onOpen'];
|
onOpenError?: OpenFormModalConfig['onOpenError'];
|
onSubmit?: OpenFormModalConfig['onSubmit'];
|
title?: string;
|
}
|
|
interface OpenListConfig {
|
menuId?: number | string;
|
modelId?: number | string;
|
params?: Record<string, any>;
|
path?: string;
|
query?: Record<string, any>;
|
replace?: boolean;
|
routeQuery?: Record<string, any>;
|
title?: string;
|
}
|
|
export interface OpenDmsFolderSelectorConfig {
|
/** 已选目录 ID。传入后会在目录树中自动定位并选中该目录。 */
|
folderId?: string;
|
/** 用户关闭选择器时触发。 */
|
onCancel?: () => void;
|
/** 用户确认选择时触发,第二个参数为按层级拼接的目录名称。 */
|
onConfirm?: (folderId: string, folderPath: string) => void;
|
/** 弹窗标题。 */
|
title?: string;
|
}
|
|
export interface OpenCustomViewConfig {
|
/** 相对 src/views 的页面路径,仅支持 x/** 目录。 */
|
page: string;
|
/** 弹窗关闭时回传的数据。 */
|
onClose?: (result?: any) => void;
|
/** 传给自定义页面的参数,可通过 onlineUtils.getViewParam 获取。 */
|
params?: Record<string, any>;
|
/** 弹窗标题。 */
|
title?: string;
|
}
|
|
interface OpenCustomViewEvent extends OpenCustomViewConfig {
|
instanceId: number;
|
params: Record<string, any>;
|
}
|
|
interface RequestFormDataConfig {
|
data?: Record<string, any>;
|
f_id?: number | string;
|
flow_id?: number | string;
|
flowId?: number | string;
|
id?: number | string;
|
menuId?: number | string;
|
modelId?: number | string;
|
onlineUtilsOpen?: boolean;
|
propsValue?: any;
|
row?: Record<string, any>;
|
rowKey?: string;
|
useDataChange?: boolean;
|
[key: string]: any;
|
}
|
|
interface OnlineRouteOptions {
|
query?: Record<string, any>;
|
replace?: boolean;
|
title?: string;
|
}
|
|
export type HiprintPrintMode = 'hiprint';
|
|
export interface HiprintPrintResult {
|
mode: HiprintPrintMode;
|
}
|
|
export interface HiprintPrintConfig {
|
clientOptions?: Record<string, any>;
|
data?: any;
|
/** 错误提示模式;DMS 等需要由上层统一处理错误的场景可设为 none。 */
|
errorMessageMode?: 'message' | 'none';
|
html?: string;
|
/**
|
* HTML 文件模板通常会在 screen 媒体下隐藏实际打印区;Hiprint 客户端渲染时需要显式展示该区域。
|
*/
|
htmlPrintMode?: 'document' | 'template';
|
onError?: (error: unknown) => void;
|
onPrint?: (result: HiprintPrintResult) => void;
|
styleHandler?: () => string;
|
template?: {
|
on?: (eventName: string, callback: (payload?: any) => void) => void;
|
print: (data: any, options?: Record<string, any>, callbacks?: Record<string, any>) => void;
|
print2: (data: any, options?: Record<string, any>) => void;
|
printByHtml2?: (html: string, options?: Record<string, any>) => void;
|
};
|
timeout?: number;
|
}
|
|
const HIPRINT_CONNECT_TIMEOUT = 5000;
|
let hiprintConnection: null | Promise<boolean> = null;
|
|
function getHiprintSocket() {
|
return (window as any).hiwebSocket || hiwebSocket;
|
}
|
|
function createHiprintHtmlFragment(html: string, htmlPrintMode: HiprintPrintConfig['htmlPrintMode'] = 'document') {
|
if (typeof DOMParser === 'undefined') return html;
|
|
const document = new DOMParser().parseFromString(html, 'text/html');
|
const wrapper = document.createElement('div');
|
wrapper.className = 'jnpf-hiprint-html';
|
|
document.head.querySelectorAll('style, link[rel~="stylesheet"]').forEach((node) => {
|
const clonedNode = node.cloneNode(true) as HTMLElement;
|
if (clonedNode instanceof HTMLLinkElement) clonedNode.href = (node as HTMLLinkElement).href;
|
wrapper.append(clonedNode);
|
});
|
|
document.body.querySelectorAll('script').forEach((node) => node.remove());
|
wrapper.insertAdjacentHTML('beforeend', document.body.innerHTML);
|
|
if (htmlPrintMode === 'template') {
|
const printStyle = document.createElement('style');
|
printStyle.textContent = `
|
.jnpf-hiprint-html .screen-view { display: none !important; }
|
.jnpf-hiprint-html .print-pages { display: block !important; }
|
`;
|
wrapper.append(printStyle);
|
}
|
|
return wrapper.outerHTML;
|
}
|
|
function waitForHiprintClient(timeout: number) {
|
if (getHiprintSocket()?.opened) return Promise.resolve(true);
|
if (hiprintConnection) return hiprintConnection;
|
|
hiprintConnection = new Promise<boolean>((resolve) => {
|
try {
|
// vue-plugin-hiprint keeps a failed socket instance, causing start() to no-op on later autoConnect() calls.
|
const socket = getHiprintSocket();
|
if (socket && !socket.opened && typeof socket.stop === 'function') socket.stop();
|
autoConnect();
|
} catch {
|
resolve(false);
|
return;
|
}
|
|
const deadline = Date.now() + Math.max(0, timeout);
|
const checkConnection = () => {
|
if (getHiprintSocket()?.opened) return resolve(true);
|
if (Date.now() >= deadline) return resolve(false);
|
window.setTimeout(checkConnection, 50);
|
};
|
checkConnection();
|
}).finally(() => {
|
hiprintConnection = null;
|
});
|
|
return hiprintConnection;
|
}
|
|
function invokeHiprintCallback(callback: HiprintPrintConfig['onPrint'], result: HiprintPrintResult) {
|
try {
|
callback?.(result);
|
} catch (error) {
|
console.error('[onlineUtils.hiprint] print callback failed', error);
|
}
|
}
|
|
function invokeHiprintErrorCallback(callback: HiprintPrintConfig['onError'], error: unknown) {
|
try {
|
callback?.(error);
|
} catch (callbackError) {
|
console.error('[onlineUtils.hiprint] error callback failed', callbackError);
|
}
|
}
|
|
function showHiprintError(message: string) {
|
onlineUtils.toast(message, 'error');
|
}
|
|
function bindHiprintLifecycleCallbacks(
|
template: NonNullable<HiprintPrintConfig['template']>,
|
onPrint: HiprintPrintConfig['onPrint'],
|
onError: HiprintPrintConfig['onError'],
|
errorMessageMode: HiprintPrintConfig['errorMessageMode'],
|
) {
|
let settled = false;
|
template.on?.('printSuccess', () => {
|
if (settled) return;
|
settled = true;
|
invokeHiprintCallback(onPrint, { mode: 'hiprint' });
|
});
|
template.on?.('printError', (error) => {
|
if (settled) return;
|
settled = true;
|
if (errorMessageMode !== 'none') showHiprintError('打印失败,请检查打印客户端和打印机后重试');
|
invokeHiprintErrorCallback(onError, error || new Error('Print failed'));
|
});
|
}
|
|
type OnlineRouteConfig = OnlineRouteOptions & {
|
hash?: string;
|
name?: string;
|
params?: Record<string, any>;
|
path?: string;
|
};
|
|
export function getJnpfAppEnCode() {
|
let appEnCode: string = '';
|
if (window.location.pathname?.startsWith(`/${APP_PREFIX}`)) {
|
const list = window.location.pathname.split('/');
|
appEnCode = list[1] ? list[1].replace(APP_PREFIX, '') : '';
|
}
|
if (window.location.pathname?.startsWith(`/${APP_PREFIX}`.toUpperCase())) {
|
const list = window.location.pathname.split('/');
|
appEnCode = list[1] ? list[1].replace(APP_PREFIX.toUpperCase(), '') : '';
|
}
|
return appEnCode;
|
}
|
export function getRealJnpfAppEnCode() {
|
let appEnCode: string = getJnpfAppEnCode();
|
if (!appEnCode) return appEnCode;
|
if (appEnCode.startsWith(`${APP_BACKEND_PREFIX}`)) {
|
appEnCode = appEnCode.replace(APP_BACKEND_PREFIX, '');
|
}
|
if (appEnCode.startsWith(`${APP_BACKEND_PREFIX}`.toUpperCase())) {
|
appEnCode = appEnCode.replace(APP_BACKEND_PREFIX.toUpperCase(), '');
|
}
|
return appEnCode;
|
}
|
|
export function getJnpfRouteTitle(query?: Record<string, any>) {
|
const value = query?.[JNPF_ROUTE_TITLE_QUERY];
|
const title = Array.isArray(value) ? value[0] : value;
|
if (isNullOrUnDef(title) || title === '') return '';
|
const titleString = String(title);
|
try {
|
return decodeURIComponent(titleString);
|
} catch {
|
return titleString;
|
}
|
}
|
|
function parseJnpfListQuery(value) {
|
const raw = Array.isArray(value) ? value[0] : value;
|
if (!raw || typeof raw !== 'string') return {};
|
const parseJson = (str) => {
|
try {
|
const data = JSON.parse(str);
|
return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
|
} catch {
|
return null;
|
}
|
};
|
const data = parseJson(raw);
|
if (data) return data;
|
try {
|
return parseJson(decodeURIComponent(raw)) || {};
|
} catch {
|
return {};
|
}
|
}
|
|
export function getJnpfRouteParam(paramName: string) {
|
if (!paramName) return undefined;
|
const query = router.currentRoute.value?.query || {};
|
const listQueryParams = parseJnpfListQuery(query.jnpfListQuery);
|
if (Object.prototype.hasOwnProperty.call(listQueryParams, paramName)) return listQueryParams[paramName];
|
if (Object.prototype.hasOwnProperty.call(query, paramName)) {
|
const value = query[paramName];
|
return Array.isArray(value) ? value[0] : value;
|
}
|
return undefined;
|
}
|
|
function normalizeRouteQuery(query: Record<string, any> = {}) {
|
return Object.keys(query).reduce<Record<string, any>>((res, key) => {
|
const value = query[key];
|
if (!isNullOrUnDef(value)) res[key] = value;
|
return res;
|
}, {});
|
}
|
|
function appendQueryToUrl(url: string, query: Record<string, any> = {}) {
|
if (!url || !Object.keys(query).length) return url;
|
const hashIndex = url.indexOf('#');
|
const pathWithQuery = hashIndex > -1 ? url.slice(0, hashIndex) : url;
|
const hash = hashIndex > -1 ? url.slice(hashIndex) : '';
|
const queryIndex = pathWithQuery.indexOf('?');
|
const path = queryIndex > -1 ? pathWithQuery.slice(0, queryIndex) : pathWithQuery;
|
const search = queryIndex > -1 ? pathWithQuery.slice(queryIndex + 1) : '';
|
const searchParams = new URLSearchParams(search);
|
|
Object.keys(query).forEach((key) => {
|
const value = query[key];
|
if (isNullOrUnDef(value)) return;
|
searchParams.delete(key);
|
if (Array.isArray(value)) {
|
value.forEach((item) => {
|
if (!isNullOrUnDef(item)) searchParams.append(key, String(item));
|
});
|
return;
|
}
|
searchParams.set(key, String(value));
|
});
|
|
const newSearch = searchParams.toString();
|
return `${path}${newSearch ? `?${newSearch}` : ''}${hash}`;
|
}
|
|
function isPlainRecord(value: any): value is Record<string, any> {
|
return value && typeof value === 'object' && !Array.isArray(value);
|
}
|
|
function getRequestFormDataId(config: RequestFormDataConfig) {
|
const row = isPlainRecord(config.row) ? config.row : {};
|
const rowKey = config.rowKey || 'id';
|
return config.id ?? config.f_id ?? row[rowKey] ?? row.id ?? row.f_id;
|
}
|
|
function getRequestFormDataFlowId(config: RequestFormDataConfig) {
|
return config.flowId ?? config.flow_id;
|
}
|
|
function parseRequestFormData(res: any, id: number | string) {
|
const dataForm = isPlainRecord(res?.data) ? res.data : isPlainRecord(res) ? res : {};
|
const rawData = dataForm.data;
|
if (!rawData) return { id: dataForm.id || id };
|
if (isPlainRecord(rawData)) return { ...rawData, id: dataForm.id || rawData.id || id };
|
try {
|
const formData = JSON.parse(rawData);
|
return { ...formData, id: dataForm.id || formData.id || id };
|
} catch {
|
throw new Error('[onlineUtils.requestFormData] invalid form data');
|
}
|
}
|
|
const flowTemplateIdCache = new Map<string, Promise<number | string | undefined>>();
|
const flowFormModelIdCache = new Map<string, Promise<number | string | undefined>>();
|
|
async function getTemplateIdByFlowVersionId(flowId: number | string) {
|
const key = String(flowId);
|
if (!flowTemplateIdCache.has(key)) {
|
flowTemplateIdCache.set(
|
key,
|
defHttp
|
.get({ url: `/api/workflow/template/Info/${key}` }, { errorMessageMode: 'none' })
|
.then((res) => {
|
const flowInfo = res?.data ?? res;
|
return flowInfo?.id && (!flowInfo?.flowId || String(flowInfo.flowId) === key) ? flowInfo.id : undefined;
|
})
|
.catch(() => undefined),
|
);
|
}
|
return flowTemplateIdCache.get(key);
|
}
|
|
function normalizeIdResult(res: any) {
|
const value = res?.data ?? res;
|
return isNullOrUnDef(value) || value === '' ? undefined : value;
|
}
|
|
function getFormModelIdByTemplateId(templateId: number | string) {
|
return defHttp
|
.get({ url: `/api/workflow/template/StartFormId/${templateId}` }, { errorMessageMode: 'none' })
|
.then((res) => {
|
const data = res?.data ?? res;
|
return normalizeIdResult(data?.formId ?? data?.id);
|
})
|
.catch(() => undefined);
|
}
|
|
async function getFormModelIdByFlowId(flowId: number | string) {
|
const key = String(flowId);
|
if (!flowFormModelIdCache.has(key)) {
|
flowFormModelIdCache.set(
|
key,
|
getFormModelIdByTemplateId(key).then(async (modelId) => {
|
if (modelId) return modelId;
|
const templateId = await getTemplateIdByFlowVersionId(key);
|
return templateId ? getFormModelIdByTemplateId(templateId) : undefined;
|
}),
|
);
|
}
|
return flowFormModelIdCache.get(key);
|
}
|
|
export async function resolveOpenFlowEditConfig<T extends Record<string, any>>(config: T) {
|
const flowId = config.flowId ?? config.flow_id;
|
if (!flowId) return config;
|
const templateId = await getTemplateIdByFlowVersionId(flowId);
|
return templateId ? { ...config, flowId: templateId } : config;
|
}
|
|
export async function resolveOpenFlowListConfig<T extends Record<string, any>>(config: T) {
|
const flowId = config.flowId ?? config.flow_id;
|
if (!flowId) return config;
|
const templateId = await getTemplateIdByFlowVersionId(flowId);
|
return templateId ? { ...config, flowId: templateId } : config;
|
}
|
|
export function normalizeOpenFlowFormConfig(configOrFlowId: number | OpenFlowFormConfig | string, paramsArg?: Record<string, any>) {
|
const config: OpenFlowFormConfig = typeof configOrFlowId === 'object' ? { ...configOrFlowId } : { flowId: configOrFlowId, params: paramsArg };
|
const flowId = config.flowId ?? config.flow_id ?? config.template ?? config.id;
|
if (!flowId) return null;
|
const params = config.params || config.query || config.data || {};
|
const normalizedParams = isPlainRecord(params) ? params : {};
|
return {
|
...config,
|
flowId,
|
formData: { ...config.formData, ...normalizedParams },
|
id: '',
|
isFlow: config.isFlow ?? 0,
|
opType: '-1',
|
params: normalizedParams,
|
};
|
}
|
|
export function isDraftFlowConfig(config: Record<string, any> = {}) {
|
const flowState = config.flowState ?? config.f_flow_state ?? config.flow_state;
|
return flowState !== undefined && String(flowState) === '0';
|
}
|
|
export function normalizeOpenFlowEditConfig(configOrFlowId: number | OpenFlowEditConfig | string, taskIdArg?: number | string) {
|
const config: OpenFlowEditConfig = typeof configOrFlowId === 'object' ? { ...configOrFlowId } : { flowId: configOrFlowId, taskId: taskIdArg };
|
const flowId = config.flowId ?? config.flow_id;
|
const isDraftFlow = isDraftFlowConfig(config);
|
const taskIdValue = isDraftFlow
|
? (config.id ?? config.f_id ?? config.taskId ?? config.flowTaskId)
|
: (config.taskId ?? config.flowTaskId ?? config.id ?? config.f_id);
|
if (!flowId || !taskIdValue) return null;
|
return {
|
...config,
|
flowId,
|
id: taskIdValue,
|
isFlow: config.isFlow ?? 0,
|
opType: config.opType ?? '-1',
|
showHeaderCancelBtn: config.showHeaderCancelBtn ?? true,
|
taskId: taskIdValue,
|
};
|
}
|
|
let globalLoadingApp: null | ReturnType<typeof createApp> = null;
|
let globalLoadingEl: HTMLElement | null = null;
|
|
function createLoadingEl(tip?: string): HTMLElement {
|
const el = document.createElement('div');
|
el.dataset.jnpfLoading = '';
|
el.style.cssText = 'position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,0.65)';
|
globalLoadingApp = createApp({ render: () => h(Spin, { spinning: true, tip }) });
|
globalLoadingApp.mount(el);
|
return el;
|
}
|
|
// 仅防止DMS打印入口重复打开,加载提示由调用入口按自身作用域管理。
|
let dmsPrintEntryOpening = false;
|
function beginDmsPrintEntryOpening(): false | (() => void) {
|
if (dmsPrintEntryOpening) return false;
|
dmsPrintEntryOpening = true;
|
return () => {
|
dmsPrintEntryOpening = false;
|
};
|
}
|
|
export const onlineUtils = {
|
/** 普通打印及重打共享在线数据接口,仅显示文字可编辑,流水身份仍校验。 */
|
createDmsPrintBarcodeBuilder(interfaceId: string): import('#/views/x/dms/shared/dmsPrintBarcode').DmsPrintBarcodeBuilder {
|
return async (number) => {
|
const { createDmsPrintBarcodeBuilder } = await import('#/views/x/dms/shared/dmsPrintBarcode');
|
return createDmsPrintBarcodeBuilder(interfaceId)(number);
|
};
|
},
|
|
/**
|
* 静默检测本机 Hiprint 客户端是否已连接,供可降级功能决定是否使用客户端能力。
|
*/
|
isHiprintClientConnected(timeout = 1000) {
|
return waitForHiprintClient(timeout);
|
},
|
/**
|
* 计算指定日期的前后周期日期。
|
* @param date 指定日期,支持毫秒时间戳和常用日期字符串
|
* @param period 周期数,默认为 1
|
* @param unit 周期单位:day | week | month | quarter | year
|
* @param isAdvance 是否提前计算,默认为 false
|
* @returns YYYY-MM-DD 格式的日期;参数无效时返回空字符串
|
*/
|
calculateDate(date: Date | number | string, period: number = 1, unit: 'day' | 'month' | 'quarter' | 'week' | 'year', isAdvance = false) {
|
const normalizedDate = isString(date) && /^\d{11,}$/.test(date.trim()) ? Number(date) : date;
|
const targetDate = dayjs(normalizedDate);
|
const amount = Number(period);
|
if (!targetDate.isValid() || !Number.isFinite(amount)) return '';
|
|
const units = {
|
day: 'day',
|
month: 'month',
|
quarter: 'month',
|
week: 'week',
|
year: 'year',
|
} as const;
|
if (!(unit in units)) return '';
|
|
const signedAmount = (isAdvance ? -1 : 1) * amount * (unit === 'quarter' ? 3 : 1);
|
return targetDate.add(signedAmount, units[unit]).format('YYYY-MM-DD');
|
},
|
// 获取用户信息
|
getUserInfo() {
|
const accessStore = useAccessStore();
|
const userStore = useUserStore();
|
const userInfo: OnlineUserInfo = userStore.getUserInfo as OnlineUserInfo;
|
userInfo.token = accessStore.accessToken as string;
|
return userInfo;
|
},
|
// 获取设备信息
|
getDeviceInfo() {
|
const deviceInfo = { vueVersion: '3', origin: 'pc' };
|
return deviceInfo;
|
},
|
// 请求
|
request(url: string, method: string, data = {}, headers = {}) {
|
const auditHeaders = buildGlobalAuditRequestHeaders(url, method, data, headers);
|
return defHttp[method ? method.toLowerCase() : 'get']({ url, data, headers: auditHeaders });
|
},
|
/**
|
* 获取低代码表单完整 formData
|
* @param configOrModelId 表单模型ID,或查询配置对象
|
* @param idArg 数据ID
|
*/
|
requestFormData(configOrModelId?: number | RequestFormDataConfig | string, idArg?: number | string) {
|
const config: RequestFormDataConfig =
|
configOrModelId && typeof configOrModelId === 'object' ? { ...configOrModelId } : { modelId: configOrModelId, id: idArg };
|
const modelId = config.modelId;
|
const flowId = getRequestFormDataFlowId(config);
|
const id = getRequestFormDataId(config);
|
if (((isNullOrUnDef(modelId) || modelId === '') && (isNullOrUnDef(flowId) || flowId === '')) || isNullOrUnDef(id) || id === '') {
|
return Promise.reject(new Error('[onlineUtils.requestFormData] modelId or flowId, and id are required'));
|
}
|
|
const resolveModelId = isNullOrUnDef(flowId) || flowId === '' ? Promise.resolve(modelId) : getFormModelIdByFlowId(flowId).then((id) => id || modelId);
|
return resolveModelId.then((resolvedModelId) => {
|
if (isNullOrUnDef(resolvedModelId) || resolvedModelId === '') {
|
return Promise.reject(new Error('[onlineUtils.requestFormData] modelId not found by flowId'));
|
}
|
|
const data = isPlainRecord(config.data) ? config.data : {};
|
const shouldUseDataChange = config.useDataChange ?? false;
|
if (shouldUseDataChange) {
|
const query: Record<string, any> = { id, menuId: config.menuId, onlineUtilsOpen: config.onlineUtilsOpen ?? true, ...data };
|
if (!isNullOrUnDef(config.propsValue) && config.propsValue !== '') query.propsValue = config.propsValue;
|
return defHttp.post({ url: `/api/visualdev/OnlineDev/${resolvedModelId}/DataChange`, data: query }).then((res) => parseRequestFormData(res, id));
|
}
|
|
return defHttp
|
.get({
|
url: `/api/visualdev/OnlineDev/${resolvedModelId}/${id}`,
|
data: { menuId: config.menuId, onlineUtilsOpen: config.onlineUtilsOpen ?? true, ...data },
|
})
|
.then((res) => parseRequestFormData(res, id));
|
});
|
},
|
/**
|
* 获取当前 URL 或 openList 传入参数
|
* @param paramName 参数名称
|
*/
|
getParam(paramName: string) {
|
return getJnpfRouteParam(paramName);
|
},
|
/** 获取当前自定义页面弹窗的参数。 */
|
getViewParam(paramName: string) {
|
return getCustomViewParam(paramName);
|
},
|
/**
|
* 路由跳转
|
* @param url 目标地址,或 vue-router 路由对象
|
* @param options 扩展配置。传 title 后,页面标题和页签标题会优先显示该标题
|
*/
|
route(url: OnlineRouteConfig | string, options: OnlineRouteOptions = {}) {
|
if (!url) return;
|
if (isString(url)) {
|
const query = normalizeRouteQuery(options.query);
|
if (options.title) query[JNPF_ROUTE_TITLE_QUERY] = options.title;
|
const targetUrl = appendQueryToUrl(url, query);
|
return options.replace ? router.replace(targetUrl) : router.push(targetUrl);
|
}
|
|
const { replace, title, ...routeConfig } = url;
|
const query = normalizeRouteQuery({ ...(routeConfig.query || {}), ...(options.query || {}) });
|
const routeTitle = options.title || title;
|
if (routeTitle) query[JNPF_ROUTE_TITLE_QUERY] = routeTitle;
|
const target = { ...routeConfig, query } as RouteLocationRaw;
|
return options.replace || replace ? router.replace(target) : router.push(target);
|
},
|
/**
|
* 打开低代码列表页
|
* @param config 列表配置
|
* @param config.menuId 菜单ID,优先通过菜单ID定位列表路由
|
* @param config.modelId 模型ID,未传 menuId 时通过模型ID定位列表路由
|
* @param config.path 目标路由路径,传入时优先使用
|
* @param config.params 传给目标列表的参数,需在目标列表过滤规则中选择“URL/openList参数”后才参与查询
|
* @param config.query 传给目标列表的参数,params 的别名
|
* @param config.routeQuery 额外 URL query 参数
|
* @param config.title 弹窗标题,不传时使用目标菜单或列表名称
|
* @param config.replace 是否替换当前路由
|
*/
|
openList(config: OpenListConfig) {
|
if (!config?.path && !config?.menuId && !config?.modelId) {
|
console.error('[onlineUtils.openList] path, menuId or modelId is required');
|
return;
|
}
|
emitter.emit('OPEN_LIST_MODAL', config);
|
},
|
/**
|
* 在当前页签内容区域打开 src/views/x 下的自定义页面。
|
* @param config.page 相对 src/views 的页面路径,例如 x/eln/demo
|
* @param config.params 页面参数,可通过 getViewParam 获取
|
*/
|
openCustomView(config: OpenCustomViewConfig) {
|
if (!config?.page) {
|
console.error('[onlineUtils.openCustomView] page is required');
|
return;
|
}
|
const params = isPlainRecord(config.params) ? { ...config.params } : {};
|
const instanceId = activateCustomViewParams(params);
|
emitter.emit('OPEN_CUSTOM_VIEW_MODAL', { ...config, instanceId, params });
|
},
|
/** 打开在线单表批量新增或批量编辑容器。 */
|
openBatchForm(config: OpenBatchFormConfig) {
|
if (!config?.modelId) {
|
console.error('[onlineUtils.openBatchForm] modelId is required');
|
return;
|
}
|
if (config.mode === 'edit' && !config.ids?.length) {
|
console.error('[onlineUtils.openBatchForm] ids are required in edit mode');
|
return;
|
}
|
emitter.emit('OPEN_BATCH_FORM', config);
|
},
|
/**
|
* 打开 DMS 目录选择器。
|
* @example
|
* onlineUtils.openDmsFolderSelector({
|
* folderId: formData.folderId,
|
* onConfirm: (folderId, folderPath) => {
|
* formData.folderId = folderId;
|
* formData.folderPath = folderPath;
|
* },
|
* });
|
*/
|
openDmsFolderSelector(config?: OpenDmsFolderSelectorConfig | string, onConfirm?: OpenDmsFolderSelectorConfig['onConfirm']) {
|
const selectorConfig =
|
typeof config === 'string' ? { folderId: config, onConfirm } : { ...(config || {}), ...(onConfirm && !config?.onConfirm ? { onConfirm } : {}) };
|
emitter.emit('OPEN_DMS_FOLDER_SELECTOR', selectorConfig);
|
},
|
/**
|
* 打开流程详情
|
* @param configOrFlowId 流程 flowId,或流程详情配置对象
|
* @param taskIdArg 流程任务 taskId
|
*/
|
openFlowDetail(configOrFlowId: number | OpenFlowDetailConfig | string, taskIdArg?: number | string) {
|
const config: OpenFlowDetailConfig = typeof configOrFlowId === 'object' ? { ...configOrFlowId } : { flowId: configOrFlowId, taskId: taskIdArg };
|
const flowId = config.flowId ?? config.flow_id;
|
const taskIdValue = isDraftFlowConfig(config)
|
? (config.id ?? config.f_id ?? config.taskId ?? config.flowTaskId)
|
: (config.taskId ?? config.flowTaskId ?? config.id ?? config.f_id);
|
if (!flowId) {
|
console.error('[onlineUtils.openFlowDetail] flowId is required');
|
return;
|
}
|
if (!taskIdValue) {
|
console.error('[onlineUtils.openFlowDetail] id is required');
|
return;
|
}
|
const detailConfig = {
|
...config,
|
flowId,
|
id: taskIdValue,
|
isFlow: config.isFlow ?? 0,
|
opType: config.opType ?? 0,
|
taskId: taskIdValue,
|
};
|
emitter.emit('OPEN_FLOW_DETAIL', detailConfig);
|
},
|
/**
|
* 打开流程编辑/处理页面
|
* @param configOrFlowId 流程 flowId,或流程编辑配置对象
|
* @param taskIdArg 流程任务/业务数据 id
|
*/
|
openFlowEdit(configOrFlowId: number | OpenFlowEditConfig | string, taskIdArg?: number | string) {
|
const flowEditConfig = normalizeOpenFlowEditConfig(configOrFlowId, taskIdArg);
|
if (!flowEditConfig) {
|
console.error('[onlineUtils.openFlowEdit] flowId and id are required');
|
return;
|
}
|
emitter.emit('OPEN_FLOW_EDIT', flowEditConfig);
|
},
|
/**
|
* 打开发起流程表单
|
* @param configOrFlowId 流程 flowId,或发起流程配置对象
|
* @param paramsArg 初始表单参数
|
*/
|
openFlowForm(configOrFlowId: number | OpenFlowFormConfig | string, paramsArg?: Record<string, any>) {
|
const flowFormConfig = normalizeOpenFlowFormConfig(configOrFlowId, paramsArg);
|
if (!flowFormConfig) {
|
console.error('[onlineUtils.openFlowForm] flowId is required');
|
return;
|
}
|
emitter.emit('OPEN_FLOW_FORM', flowFormConfig);
|
},
|
/**
|
* 弹窗打开流程关联的数据列表页
|
* @param configOrFlowId 流程模板ID,或流程列表配置对象
|
* @param paramsArg 传给目标列表的参数,需在目标列表过滤规则中选择“URL/openList参数”后才参与查询
|
*/
|
openFlowList(configOrFlowId: number | OpenFlowListConfig | string, paramsArg?: Record<string, any>) {
|
const config: OpenFlowListConfig = typeof configOrFlowId === 'object' ? { ...configOrFlowId } : { flowId: configOrFlowId, params: paramsArg };
|
const flowId = config?.flowId ?? config?.flow_id;
|
if (!flowId && !config?.menuId && !config?.path) {
|
console.error('[onlineUtils.openFlowList] flowId, menuId or path is required');
|
return;
|
}
|
emitter.emit('OPEN_FLOW_LIST_MODAL', config);
|
},
|
// 消息提示
|
toast(message: number | string, type: string = 'info', duration: number = 3000) {
|
const { createMessage } = useMessage();
|
if (!isString(message) && !isNumber(message)) return;
|
const newDuration = duration / 1000;
|
const config = { content: message, type, duration: newDuration };
|
createMessage[type] && createMessage[type](config);
|
},
|
// 确认
|
confirm(message: string, handleOk: () => void, handleCancel: () => void = () => {}) {
|
const { createConfirm } = useMessage();
|
if (!isString(message)) return;
|
|
createConfirm({
|
iconType: 'warning',
|
title: $t('common.tipTitle'),
|
content: message,
|
onOk: () => {
|
try {
|
handleOk();
|
} catch {}
|
},
|
onCancel: () => {
|
try {
|
handleCancel();
|
} catch {}
|
},
|
});
|
},
|
// 提示,仅提供确认按钮
|
alert(message: string, handleOk: () => void = () => {}) {
|
const { createInfoModal } = useMessage();
|
if (!isString(message)) return;
|
|
createInfoModal({
|
title: $t('common.tipTitle'),
|
content: message,
|
onOk: () => {
|
try {
|
handleOk();
|
} catch {}
|
},
|
});
|
},
|
/**
|
* 打开低代码表单弹窗
|
* @param config 弹窗配置
|
* @param config.modelId 表单模型ID(必填)
|
* @param config.id 数据ID(编辑时传入)
|
* @param config.title 弹窗标题
|
* @param config.width 弹窗宽度
|
* @param config.type 弹窗类型:modal(居中弹窗) | drawer(右侧弹窗) | fullScreen(全屏)
|
* @param config.params 额外参数
|
* @param config.submitMode 提交模式:default(通用保存) | custom(调用方 onSubmit) | form(按目标表单设计提交)
|
* @param config.onSubmit 自定义提交回调(submitMode='custom'时生效)
|
* @param config.onConfirm 提交成功回调;form 模式等待目标自定义提交完成
|
* @param config.onCancel 取消回调
|
* @param config.mode 展示模式:form(表单编辑) | detail(详情展示),默认为 form
|
*/
|
openFormModal(config: OpenFormModalConfig) {
|
if (!config.modelId) {
|
console.error('[onlineUtils.openFormModal] modelId is required');
|
return;
|
}
|
emitter.emit('OPEN_FORM_MODAL', config);
|
},
|
/**
|
* 打开签名表单弹窗
|
* @param config 签名配置
|
* @param config.isFaceToFace 是否面签,默认 false
|
* @param config.allowMyself 面签是否允许自己,默认 false
|
* @param config.metaData 签名业务元数据,序列化后作为 meta_data 传给签名表单
|
*/
|
sign(config: SignConfig = {}) {
|
const { allowMyself = false, isFaceToFace = false, metaData, onCancel, onOpen, onOpenError, onSubmit, title = '' } = config;
|
const hasMetaDataValue =
|
metaData &&
|
Object.values(metaData).some((value) =>
|
Array.isArray(value) ? value.length > 0 : typeof value === 'string' ? value.trim() !== '' : value !== undefined && value !== null,
|
);
|
const metaDataString = hasMetaDataValue ? (JSON.stringify({ is_review_button: false, ...metaData }) ?? '') : '';
|
const formConfig: OpenFormModalConfig = {
|
modelId: isFaceToFace ? REVIEW_SIGNATURE_MODEL_ID : ELECTRONIC_SIGNATURE_MODEL_ID,
|
title,
|
type: 'modal',
|
width: '800px',
|
submitMode: 'custom',
|
onSubmit,
|
onCancel,
|
onOpen,
|
onOpenError,
|
params: { meta_data: metaDataString },
|
fieldMapping: { meta_data: 'meta_data' },
|
};
|
if (isFaceToFace && allowMyself === true) {
|
formConfig.params.allow_self = 'yes';
|
formConfig.fieldMapping.biz_action = 'allow_self';
|
}
|
this.openFormModal(formConfig);
|
},
|
// 获取事件总线(供组件监听使用)
|
getEmitter() {
|
return emitter;
|
},
|
/**
|
* 异步提交一次打印审计。该方法不触发打印,由业务按钮在需要留痕的位置显式调用。
|
* 批量 targets 仍只提交一个审计请求。
|
*/
|
auditPrint(options: PrintAuditOptions): string | undefined {
|
const printName = options?.printName?.trim();
|
if (!printName) {
|
console.error('[onlineUtils.auditPrint] printName is required');
|
return;
|
}
|
if (!Array.isArray(options.targets) || options.targets.length === 0) {
|
console.error('[onlineUtils.auditPrint] targets is required');
|
return;
|
}
|
|
const requestId = options.requestId?.trim() || createAuditCorrelationId();
|
try {
|
void recordPrintAudit({ ...options, printName, requestId }).catch((error) => {
|
console.error('[onlineUtils.auditPrint] record failed', error);
|
});
|
} catch (error) {
|
console.error('[onlineUtils.auditPrint] record failed', error);
|
}
|
return requestId;
|
},
|
/**
|
* 异步记录一次 DMS 自定义打印操作。该方法不触发打印,也不写审计日志。
|
* 调用方应在实际打印动作前显式调用;记录失败不阻断打印。
|
*/
|
recordDmsPrintOperation(options: DmsPrintOperationOptions): string | undefined {
|
const printName = options?.printName?.trim();
|
if (!printName) {
|
console.error('[onlineUtils.recordDmsPrintOperation] printName is required');
|
return;
|
}
|
if (!Array.isArray(options.targets) || options.targets.length === 0) {
|
console.error('[onlineUtils.recordDmsPrintOperation] targets is required');
|
return;
|
}
|
|
const clientEventId = options.clientEventId?.trim() || uuidv4();
|
try {
|
void createDmsPrintOperation({ ...options, clientEventId, copies: options.copies ?? 1, printName }).catch((error) => {
|
console.error('[onlineUtils.recordDmsPrintOperation] record failed', error);
|
});
|
} catch (error) {
|
console.error('[onlineUtils.recordDmsPrintOperation] record failed', error);
|
}
|
return clientEventId;
|
},
|
/**
|
* 通过本机 Hiprint 客户端直接打印;客户端未连接或直打失败时显示错误提示。
|
* 支持 Hiprint 模板和原始 HTML。原始 HTML 使用 printByHtml2 直接交由客户端打印。
|
*/
|
async hiprint(config: HiprintPrintConfig): Promise<HiprintPrintResult | undefined> {
|
if (!config?.template && !config?.html) {
|
const error = new Error('template or html is required');
|
console.error('[onlineUtils.hiprint] template or html is required');
|
invokeHiprintErrorCallback(config?.onError, error);
|
return;
|
}
|
|
if (!(await waitForHiprintClient(config.timeout ?? HIPRINT_CONNECT_TIMEOUT))) {
|
const error = new Error('Print client is unavailable');
|
if (config.errorMessageMode !== 'none') showHiprintError('打印客户端未连接,请启动客户端后重试');
|
invokeHiprintErrorCallback(config.onError, error);
|
return;
|
}
|
|
try {
|
const template = config.template || new hiprint.PrintTemplate({});
|
bindHiprintLifecycleCallbacks(template, config.onPrint, config.onError, config.errorMessageMode);
|
const options = {
|
printer: '',
|
...config.clientOptions,
|
styleHandler: config.styleHandler,
|
};
|
if (config.html) {
|
if (!template.printByHtml2) throw new Error('Hiprint printByHtml2 is unavailable');
|
template.printByHtml2(createHiprintHtmlFragment(config.html, config.htmlPrintMode), options);
|
} else {
|
template.print2(config.data, options);
|
}
|
const result = { mode: 'hiprint' as const };
|
return result;
|
} catch (error) {
|
console.error('[onlineUtils.hiprint] direct print failed', error);
|
if (config.errorMessageMode !== 'none') showHiprintError('打印失败,请检查打印客户端和打印机后重试');
|
invokeHiprintErrorCallback(config.onError, error);
|
}
|
},
|
/** 仅检测/尝试连接 Hiprint 客户端,不创建任何打印业务记录。 */
|
isHiprintClientReady(timeout = HIPRINT_CONNECT_TIMEOUT) {
|
return waitForHiprintClient(timeout);
|
},
|
/**
|
* 打开自定义打印弹窗
|
* @param config 打印配置
|
* @param config.template 模板内容(HTML字符串、Vue组件名称或HTML文件路径)
|
* @param config.type 模板类型:'html' | 'vue' | 'html-file'
|
* - 'html': 直接使用传入的HTML字符串作为模板
|
* - 'vue': 使用Vue组件名称作为模板
|
* - 'html-file': 通过文件路径加载独立HTML文件
|
* @param config.data 打印数据(任意对象,模板中通过 window.PRINT_DATA 访问)
|
* @param config.title 弹窗标题,默认"打印预览"
|
* @param config.showPdfBtn 是否显示导出PDF按钮,默认true
|
* @param config.onPrint 打印回调
|
* @param config.onDownloadPdf 导出PDF回调
|
* @param config.onError 错误回调
|
* @param config.audit 打印审计字段映射;传 false 可关闭自动打印审计
|
* @example
|
* // 1. HTML字符串方式
|
* onlineUtils.print({
|
* template: '<div>...</div>',
|
* type: 'html',
|
* data: { name: '张三', age: 25 }
|
* });
|
*
|
* // 2. Vue组件方式
|
* onlineUtils.print({
|
* template: 'ReportTemplate',
|
* type: 'vue',
|
* data: { reportInfo: {...}, testItems: [...] }
|
* });
|
*
|
* // 3. HTML文件方式(推荐,方便版本管理)
|
* onlineUtils.print({
|
* template: '/print-templates/quality-report.html',
|
* type: 'html-file',
|
* data: { reportInfo: {...}, testItems: [...] }
|
* });
|
*/
|
print(config: {
|
audit?:
|
| false
|
| (PrintAuditTargetMapping & {
|
bizType?: string;
|
printName?: string;
|
recordTitle?: string;
|
targets?: PrintAuditOptions['targets'];
|
targetTable?: string;
|
});
|
data: any;
|
onDownloadPdf?: (data: any) => void;
|
onError?: (error: any) => void;
|
onPrint?: (data: any) => void;
|
showPdfBtn?: boolean;
|
template: string;
|
title?: string;
|
type: 'html' | 'html-file' | 'vue';
|
}) {
|
if (!config.template) {
|
console.error('[onlineUtils.print] template is required');
|
return;
|
}
|
emitter.emit('OPEN_PRINT_MODAL', config);
|
},
|
/** 打开报告查看弹窗 */
|
openReportView(config: ReportViewConfig) {
|
if (!config.tabs?.length) {
|
console.error('[onlineUtils.openReportView] tabs is required');
|
return;
|
}
|
emitter.emit('OPEN_REPORT_VIEW', config);
|
},
|
/** 关闭报告查看弹窗 */
|
closeReportView() {
|
emitter.emit('CLOSE_REPORT_VIEW');
|
},
|
/**
|
* 用 OnlyOffice 在线打开附件(docx/xlsx/pptx 等),可编辑并保存回原附件。
|
* @param config.file 附件字段里的 fileItem 对象,必填
|
* @param config.mode edit | qianpiye | view,默认 edit;qianpiye 当前按 edit 打开,最终以后端返回的模式为准(他人持锁时会降级只读)
|
* @param config.onSave 编辑期间有过改动、且弹窗关闭时触发;**不代表后端已写回**,详见类型定义注释
|
* @example
|
* onlineUtils.openOfficeDocument({ file });
|
* onlineUtils.openOfficeDocument({
|
* file,
|
* mode: 'view',
|
* bizModule: 'lims_jianyan',
|
* bizDataId: row.f_id,
|
* onSave: () => reloadAttachments(),
|
* });
|
*/
|
openOfficeDocument(config: OpenOfficeDocumentConfig) {
|
if (!config?.file?.fileId) {
|
console.error('[onlineUtils.openOfficeDocument] file.fileId is required');
|
return;
|
}
|
if (!emitter.all.get('OPEN_OFFICE_DOCUMENT')?.length) {
|
const error = new Error('文档窗口尚未就绪,请刷新页面后重试');
|
useMessage().createMessage.error(error.message);
|
config.onOpenError?.(error);
|
return;
|
}
|
emitter.emit('OPEN_OFFICE_DOCUMENT', config);
|
},
|
/**
|
* 根据传入的文件标识打开 DMS 文件预览。
|
* 优先使用文件中心版本 ID;未传入时,通过业务文件版本 ID 查询文件中心版本。
|
* @returns 找到文件中心版本并发起预览时返回 true;参数暂不支持或未关联时返回 false
|
*/
|
async previewDmsFile(options?: PreviewDmsFileOptions) {
|
const releaseOpening = options?.showPrintButton ? beginDmsPrintEntryOpening() : undefined;
|
if (releaseOpening === false) return false;
|
try {
|
const directResourceId = isNullOrUnDef(options?.resourceId) ? '' : String(options.resourceId).trim();
|
let fileCenterVersionId = isNullOrUnDef(options?.wenjianZhongxinId) ? '' : String(options.wenjianZhongxinId).trim();
|
const resolvedVersionId = isNullOrUnDef(options?.versionId) ? '' : String(options.versionId).trim();
|
const sourceCount = [directResourceId, fileCenterVersionId, resolvedVersionId].filter(Boolean).length;
|
if (sourceCount > 1) throw new Error('DMS 文件定位参数只能使用 resourceId、versionId 或 wenjianZhongxinId 中的一种');
|
if (directResourceId && !options?.resourceType) throw new Error('使用 resourceId 时必须同时传入 resourceType');
|
if (!directResourceId && !fileCenterVersionId && !resolvedVersionId) {
|
onlineUtils.toast('业务开发中', 'info');
|
return false;
|
}
|
|
let printOptions: DmsPreviewPrintOptions | undefined;
|
if (options?.showPrintButton) {
|
const dmsHiprint = await import('#/views/x/dms/shared/dmsHiprint');
|
printOptions = dmsHiprint.normalizeDmsPrintOptions(options.printOptions);
|
}
|
|
if (!directResourceId && !fileCenterVersionId) {
|
const versionResponse = await getDmsWenjianZhongxinBanben(resolvedVersionId);
|
fileCenterVersionId = versionResponse.data?.wenjianZhongxinBanbenId?.trim() || '';
|
if (!fileCenterVersionId) return false;
|
}
|
|
const watermarks = watermarkParameters(options);
|
if (options?.showPrintButton && options.printTaskId) {
|
const { loadDmsPrintPreviewNumber } = await import('#/views/x/dms/shared/dmsPrintAttempt');
|
const taskFileVersionId = fileCenterVersionId || (options.resourceType === 'FILE_VERSION' ? directResourceId : '');
|
if (!taskFileVersionId) throw new Error('打印任务预览需要明确的文件版本');
|
const { buildDmsPrintBarcode } = await import('#/views/x/dms/shared/dmsPrintBarcode');
|
const number = await loadDmsPrintPreviewNumber(options.printTaskId, taskFileVersionId);
|
if (!number?.serialNumber || !/^\d{8}$/.test(number.countSerialNumber || '')) throw new Error('任务固定流水号缺失,无法显示打印预览');
|
watermarks.customBarcode = await buildDmsPrintBarcode(number, options.buildCustomBarcode);
|
}
|
const watermarkType = options?.watermarkType ?? (options?.showPrintButton && (options.printTaskId || options.reprint) ? 'print' : '');
|
const ticketResponse = await createDmsFilePreviewTicket({
|
...watermarks,
|
operationId: uuidv4(),
|
previewFormat: options?.previewFormat ?? 'pdf',
|
resourceId: directResourceId || fileCenterVersionId,
|
resourceType: directResourceId ? options!.resourceType! : 'FILE_VERSION',
|
watermarkType,
|
});
|
const ticket = ticketResponse.data;
|
if (!ticket?.accessTicket || !ticket.fileVersionId || !ticket.fileName) {
|
throw new Error('后端返回的 DMS 预览票据不完整');
|
}
|
|
const imagePreview = await import('#/views/x/dms/shared/dmsFilePreview');
|
if (imagePreview.supportsImagePreview(ticket.fileName, ticket.mimeType)) {
|
const { createImgPreview } = await import('@jnpf/ui');
|
createImgPreview({ imageList: [imagePreview.dmsPreviewStreamUrl(ticket.fileVersionId, ticket.accessTicket)] });
|
return true;
|
}
|
|
onlineUtils.openOfficeDocument({
|
dmsAccessTicket: ticket.accessTicket,
|
dmsPreviewExpiresAt: ticket.expiresAt,
|
refreshDmsPreviewTicket: async () => {
|
const refreshed = await createDmsFilePreviewTicket({
|
...watermarks,
|
operationId: crypto.randomUUID(),
|
previewFormat: options?.previewFormat ?? 'pdf',
|
resourceId: ticket.fileVersionId,
|
resourceType: 'FILE_VERSION',
|
watermarkType,
|
});
|
return refreshed.data;
|
},
|
file: {
|
fileId: ticket.fileVersionId,
|
fileSize: ticket.sizeBytes,
|
name: ticket.fileName,
|
},
|
fileType: 'dms',
|
mode: 'view',
|
title: ticket.fileName,
|
...(printOptions
|
? {
|
dmsPrint: {
|
...printOptions,
|
...watermarkParameters(watermarks),
|
buildCustomBarcode: options?.buildCustomBarcode,
|
reprint: options?.reprint,
|
printTaskId: options?.printTaskId,
|
printQuantity: options?.printQuantity,
|
beforePrint: options?.beforePrint,
|
confirmPrint: options?.confirmPrint,
|
closeOnPrintSuccess: options?.closeOnPrintSuccess,
|
fileVersionId: ticket.fileVersionId,
|
onPrintSuccess: options?.onPrintSuccess,
|
},
|
}
|
: {}),
|
});
|
return true;
|
} finally {
|
releaseOpening?.();
|
}
|
},
|
async openDmsReprint(recordId: string, onConfirm?: () => void, buildCustomBarcode?: import('#/views/x/dms/shared/dmsPrintBarcode').DmsPrintBarcodeBuilder, watermarkBusinessTypes?: string[]) {
|
const releaseOpening = beginDmsPrintEntryOpening();
|
if (!releaseOpening) return;
|
try {
|
const { openDmsReprint } = await import('#/views/x/dms/shared/dmsReprint');
|
return await openDmsReprint(recordId, onConfirm, buildCustomBarcode, watermarkBusinessTypes);
|
} finally {
|
releaseOpening();
|
}
|
},
|
async submitDmsReprint(context: Parameters<typeof import('#/views/x/dms/shared/dmsReprint').submitDmsReprint>[0]) {
|
const { submitDmsReprint } = await import('#/views/x/dms/shared/dmsReprint');
|
return submitDmsReprint(context);
|
},
|
/** 使用 PRINT 票据打开只读打印页面;发起打印不等于纸张已打印。 */
|
async printDmsFile(options?: PrintDmsFileOptions) {
|
const fileCenterVersionId = await resolveDmsFileCenterVersionId(options);
|
if (!fileCenterVersionId) return false;
|
const response = await createDmsFilePrintTicket({
|
...watermarkParameters(options),
|
copies: options?.copies ?? 1,
|
operationId: uuidv4(),
|
printPages: options?.printPages,
|
resourceId: fileCenterVersionId,
|
resourceType: 'FILE_VERSION',
|
});
|
const ticket = response.data;
|
if (!ticket?.accessTicket || !ticket.fileVersionId || !ticket.fileName) {
|
throw new Error('后端返回的 DMS 打印票据不完整');
|
}
|
onlineUtils.openOfficeDocument({
|
dmsAccessTicket: ticket.accessTicket,
|
file: { fileId: ticket.fileVersionId, fileSize: ticket.sizeBytes, name: ticket.fileName },
|
fileType: 'dms',
|
mode: 'view',
|
title: ticket.fileName,
|
});
|
return true;
|
},
|
showLoading(tip?: string) {
|
if (globalLoadingEl) return;
|
globalLoadingEl = createLoadingEl(tip);
|
document.body.append(globalLoadingEl);
|
},
|
hideLoading() {
|
if (!globalLoadingEl) return;
|
globalLoadingApp?.unmount();
|
globalLoadingApp = null;
|
globalLoadingEl.remove();
|
globalLoadingEl = null;
|
},
|
};
|
export function getParamList(templateJson, data?, rowKey = 'id') {
|
if (!templateJson?.length) return [];
|
for (const e of templateJson) {
|
if (e.sourceType == 1 && data) {
|
e.defaultValue = data[e.relationField] || data[e.relationField] == 0 || data[e.relationField] == false ? data[e.relationField] : '';
|
}
|
if (e.sourceType == 4 && e.relationField == '@formId') e.defaultValue = data[rowKey] || '';
|
}
|
return templateJson;
|
}
|
export function getLaunchFlowParamList(transferList, data?, rowKey = 'id') {
|
transferList = cloneDeep(transferList);
|
if (!transferList?.length) return [];
|
for (const e of transferList) {
|
if (e.sourceType == 1) {
|
if (e.sourceValue == '@formId') {
|
e.defaultValue = data[rowKey] || '';
|
} else {
|
if (e.sourceValue.includes('-')) {
|
const tableVModel = e.sourceValue.split('-')[0];
|
const childVModel = e.sourceValue.split('-')[1];
|
e.defaultValue = (data[tableVModel] || []).map((o) => o[`${childVModel}_jnpfId`]);
|
} else {
|
const key = `${e.sourceValue}_jnpfId`;
|
e.defaultValue = isNullOrUnDef(data[key]) ? (isNullOrUnDef(data[e.sourceValue]) ? '' : data[e.sourceValue]) : data[key];
|
}
|
}
|
} else {
|
e.defaultValue = e.sourceValue;
|
}
|
}
|
return transferList;
|
}
|
|
// 开始:解决老的vue2动态导入文件语法vite不支持的问题
|
const allModules: any = import.meta.glob('../views/**/*.vue');
|
export function importViewsFile(path): Promise<any> {
|
if (path.startsWith('/')) {
|
path = path.slice(1);
|
}
|
let page = '';
|
let realPage = '';
|
if (path.endsWith('.vue')) {
|
page = `../views/${path}`;
|
realPage = `../views/${path}`;
|
} else {
|
page = `../views/${path}.vue`;
|
realPage = `../views/${path}/index.vue`;
|
}
|
return new Promise((resolve, reject) => {
|
let flag = true;
|
for (const path in allModules) {
|
if (path == page || path == realPage) {
|
flag = false;
|
allModules[path]().then((mod) => {
|
resolve(mod);
|
});
|
}
|
}
|
if (flag) {
|
reject(new Error(`该文件不存在:${page}`));
|
}
|
});
|
}
|
// 结束:解决老的vue2动态导入文件语法 vite不支持的问题
|
|
export function getAuthMediaUrl(url, isRedirect = true) {
|
if (!url) return '';
|
// eslint-disable-next-line regexp/no-unused-capturing-group
|
const base64WithPrefixRegex = /^data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+);base64,([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$/;
|
if (base64WithPrefixRegex.test(url)) return url;
|
const userStore = useUserStore();
|
const userInfo: OnlineUserInfo = userStore.getUserInfo as OnlineUserInfo;
|
const securityKey = userInfo?.securityKey || '';
|
const globSetting = useGlobSetting();
|
if (!securityKey) return globSetting.apiURL + url;
|
const realUrl = `${globSetting.apiURL + url + (url.includes('?') ? '&' : '?')}s=${securityKey}${isRedirect ? '' : '&t=t'}`;
|
return realUrl;
|
}
|