import type { UserInfo } from '@vben/types';
|
|
import type { OpenOfficeDocumentConfig } from '#/components/GlobalOfficeDocumentModal/types';
|
import type { ReportViewConfig } from '#/components/GlobalReportViewModal/types';
|
|
import { createApp, h } from 'vue';
|
import type { RouteLocationRaw } from 'vue-router';
|
|
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 mitt from 'mitt';
|
|
import { defHttp } from '#/api/request';
|
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_CUSTOM_VIEW_MODAL: OpenCustomViewEvent;
|
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: {
|
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;
|
}
|
|
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;
|
onSubmit?: (data: any) => Promise<void> | void;
|
params?: Record<string, any>;
|
submitMode?: 'custom' | 'default';
|
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'];
|
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 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;
|
}
|
|
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;
|
}
|
|
export const onlineUtils = {
|
/**
|
* 计算指定日期的前后周期日期。
|
* @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 });
|
},
|
/**
|
* 打开流程详情
|
* @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 {}
|
},
|
});
|
},
|
/**
|
* 打开低代码表单弹窗
|
* @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(自定义提交)
|
* @param config.onSubmit 自定义提交回调(submitMode='custom'时生效)
|
* @param config.onConfirm 确认回调(submitMode='default'时生效)
|
* @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, 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,
|
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;
|
},
|
/**
|
* 打开自定义打印弹窗
|
* @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 错误回调
|
* @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: {
|
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 | view,默认 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;
|
}
|
emitter.emit('OPEN_OFFICE_DOCUMENT', config);
|
},
|
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;
|
}
|