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;
}
