@@ -1,11 +1,12 @@
< script setup lang = "ts" >
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance' ;
import type { SystemAreaApi } from '#/api/system/area' ;
import { computed , ref } from 'vue' ;
import { useVbenModal } from '@vben/common-ui' ;
import { DICT _TYPE } from '@vben/constants' ;
import { getDictLabel } from '@vben/hooks' ;
import { getDictLabel , getDictOptions } from '@vben/hooks' ;
import { useUserStore } from '@vben/stores' ;
import { formatDate } from '@vben/utils' ;
@@ -14,15 +15,45 @@ import { Button } from 'ant-design-vue';
import vPrint from 'vue3-print-nb' ;
import { getProcessInstancePrintData } from '#/api/bpm/processInstance' ;
import { getAreaTree } from '#/api/system/area' ;
import { getSimpleDeptList } from '#/api/system/dept' ;
import { getSimpleUserList } from '#/api/system/user' ;
import { decodeFields } from '#/components/form-create' ;
const userStore = useUserStore ( ) ;
interface FormFieldItem {
html : string ;
id : string ;
name : string ;
}
interface FormFieldOption {
label ? : string ;
value ? : unknown ;
}
type FormFieldRule = Record < string , unknown > & {
field ? : string ;
options ? : FormFieldOption [ ] ;
props ? : Record < string , unknown > ;
title ? : string ;
type ? : string ;
} ;
type PrintableRecord = Record < string , unknown > ;
interface PrintLookupMaps {
areaMap : Map < string , string > ;
deptMap : Map < string , string > ;
userMap : Map < string , string > ;
}
const printData = ref < BpmProcessInstanceApi .ProcessPrintDataRespVO > ( ) ;
const userName = computed ( ( ) => userStore . userInfo ? . nickname ? ? '' ) ;
const printTime = ref ( formatDate ( new Date ( ) , 'YYYY-MM-DD HH:mm' ) ) ;
const formFields = ref < any [ ] > ( [ ] ) ;
const printDataMap = ref < Record < string , any > > ( { } ) ;
const formFields = ref < FormFieldItem [ ] > ( [ ] ) ;
const printDataMap = ref < Record < string , string > > ( { } ) ;
/** 打印配置 */
const printObj = ref ( {
@@ -60,57 +91,26 @@ const [Modal, modalApi] = useVbenModal({
async function fetchPrintData ( id : string ) {
printData . value = await getProcessInstancePrintData ( id ) ;
initPrintDataMap ( ) ;
parseFormFields ( ) ;
await parseFormFields( ) ;
}
/** 解析表单字段 */
function parseFormFields ( ) {
async function parseFormFields ( ) {
if ( ! printData . value ) return ;
const formFieldsObj = decodeFields (
printData . value . processInstance . processDefinition ? . formFields || [ ] ,
) ;
const processVariables = printData . value . processInstance . formVariables ;
const res : any = [ ] ;
) as unknown as FormFieldRule [ ] ;
const processVariables = printData . value . processInstance . formVariables ? ? { } ;
const lookupMaps = await loadPrintLookupMaps ( formFieldsObj ) ;
const res : FormFieldItem [ ] = [ ] ;
for ( const item of formFieldsObj ) {
const id = item . field ;
const name = item . title ;
const fieldKey = item . field as string ;
const fieldKey = String ( item . field ? ? '' ) ;
const id = fieldKey ;
const name = String ( item . title ? ? fieldKey ) ;
const variable = processVariables [ fieldKey ] ;
le t html = variable ;
switch ( item . type ) {
case 'checkbox' :
case 'radio' :
case 'select' : {
const options = item . options ;
const temp : any = [ ] ;
if ( Array . isArray ( options ) ) {
if ( Array . isArray ( variable ) ) {
const labels = options
. filter ( ( o : any ) => variable . includes ( o . value ) )
. map ( ( o : any ) => o . label ) ;
temp . push ( ... labels ) ;
} else {
const opt = options . find ( ( o : any ) => o . value === variable ) ;
if ( opt ) {
temp . push ( opt . label ) ;
}
}
}
html = temp . join ( ',' ) ;
break ;
}
case 'UploadImg' : {
const imgEl = document . createElement ( 'img' ) ;
imgEl . setAttribute ( 'src' , variable ) ;
imgEl . setAttribute ( 'style' , 'max-width: 600px;' ) ;
html = imgEl . outerHTML ;
break ;
}
// TODO 更多表单打印展示
}
cons t html = formatPrintField ( item , variable , lookupMaps ) ;
printDataMap . value [ fieldKey ] = html ;
res . push ( { id , name , html } ) ;
@@ -119,6 +119,308 @@ function parseFormFields() {
formFields . value = res ;
}
function getRuleProp ( rule : FormFieldRule , key : string ) {
return rule ? . [ key ] ? ? rule ? . props ? . [ key ] ;
}
function isPrintableRecord ( value : unknown ) : value is PrintableRecord {
return typeof value === 'object' && value !== null ;
}
function getRecordValue ( record : PrintableRecord , key : string ) {
return record [ key ] ;
}
function isNotEmptyString ( value : string ) {
return value . length > 0 ;
}
function isEmptyValue ( value : unknown ) {
return value === undefined || value === null || value === '' ;
}
function toValueArray ( value : unknown ) {
if ( Array . isArray ( value ) ) {
return value ;
}
if ( isEmptyValue ( value ) ) {
return [ ] ;
}
return [ value ] ;
}
function tryFormatDate ( value : unknown ) {
if ( isEmptyValue ( value ) ) {
return '' ;
}
const formatted = formatDate ( value as Date | number | string ) ;
return formatted === 'Invalid Date' ? String ( value ) : formatted ;
}
function formatDateValue ( value : unknown ) {
if ( Array . isArray ( value ) ) {
return value . map ( ( item ) => tryFormatDate ( item ) ) . join ( ' ~ ' ) ;
}
return tryFormatDate ( value ) ;
}
function formatPrimitiveValue ( value : unknown ) : string {
if ( isEmptyValue ( value ) ) {
return '' ;
}
if ( Array . isArray ( value ) ) {
return value
. map ( ( item ) => formatPrimitiveValue ( item ) )
. filter ( ( s ) => isNotEmptyString ( s ) )
. join ( ', ' ) ;
}
if ( typeof value === 'boolean' ) {
return value ? '是' : '否' ;
}
if ( isPrintableRecord ( value ) ) {
const record = value ;
const displayValue =
getRecordValue ( record , 'label' ) ? ?
getRecordValue ( record , 'name' ) ? ?
getRecordValue ( record , 'url' ) ? ?
getRecordValue ( record , 'value' ) ? ?
JSON . stringify ( value ) ;
return String ( displayValue ) ;
}
return String ( value ) ;
}
function createImageHtml ( url : string ) {
const imgEl = document . createElement ( 'img' ) ;
imgEl . setAttribute ( 'src' , url ) ;
imgEl . setAttribute ( 'style' , 'max-width: 600px; max-height: 300px;' ) ;
return imgEl . outerHTML ;
}
function renderImageListHtml ( value : unknown ) {
return toValueArray ( value )
. map ( ( item ) => {
let url : string | undefined ;
if ( typeof item === 'string' ) {
url = item ;
} else if ( isPrintableRecord ( item ) ) {
const recordUrl = getRecordValue ( item , 'url' ) ;
url = recordUrl ? String ( recordUrl ) : undefined ;
}
return url ? createImageHtml ( String ( url ) ) : '' ;
} )
. filter ( ( s ) => isNotEmptyString ( s ) )
. join ( '<br/>' ) ;
}
function createFileLinkHtml ( file : unknown ) {
const record = isPrintableRecord ( file ) ? file : undefined ;
const recordUrl = record ? getRecordValue ( record , 'url' ) : undefined ;
const url = typeof file === 'string' ? file : String ( recordUrl ? ? '' ) ;
if ( ! url ) {
return '' ;
}
const linkEl = document . createElement ( 'a' ) ;
linkEl . setAttribute ( 'href' , String ( url ) ) ;
linkEl . setAttribute ( 'target' , '_blank' ) ;
linkEl . setAttribute ( 'rel' , 'noopener noreferrer' ) ;
const fallbackName = String ( url ) . slice ( Math . max ( 0 , String ( url ) . lastIndexOf ( '/' ) + 1 ) ) || String ( url ) ;
const recordName = record ? getRecordValue ( record , 'name' ) : undefined ;
linkEl . textContent = recordName ? String ( recordName ) : fallbackName ;
return linkEl . outerHTML ;
}
function renderFileListHtml ( value : unknown ) {
return toValueArray ( value )
. map ( ( item ) => createFileLinkHtml ( item ) )
. filter ( ( s ) => isNotEmptyString ( s ) )
. join ( '<br/>' ) ;
}
function mapValuesWithOptions (
value : unknown ,
options : FormFieldOption [ ] = [ ] ,
) {
const values = toValueArray ( value ) ;
const labels = values
. map ( ( item ) => {
const matched = options . find (
( option ) =>
option ? . value === item || String ( option ? . value ? ? '' ) === String ( item ) ,
) ;
return matched ? . label ? ? String ( item ) ;
} )
. filter ( ( s ) => isNotEmptyString ( s ) ) ;
return labels . join ( ', ' ) ;
}
function flattenAreaTree (
list : Array < SystemAreaApi .Area & { children ? : SystemAreaApi.Area [ ] } > = [ ] ,
map : Map < string , string > = new Map ( ) ,
) {
list . forEach ( ( item ) => {
if ( item . id !== undefined ) {
map . set ( String ( item . id ) , item . name ) ;
}
if ( Array . isArray ( item . children ) && item . children . length > 0 ) {
flattenAreaTree ( item . children , map ) ;
}
} ) ;
return map ;
}
function mapValueWithLabelMap (
value : unknown ,
labelMap : Map < string , string > ,
separator = ', ' ,
) {
const values = toValueArray ( value ) ;
const labels = values
. map ( ( item ) => labelMap . get ( String ( item ) ) ? ? String ( item ) )
. filter ( ( s ) => isNotEmptyString ( s ) ) ;
return labels . length > 0 ? labels . join ( separator ) : formatPrimitiveValue ( values ) ;
}
/**
* 按当前表单字段类型按需加载打印所需的关联数据,并构建查找映射表。
*
* @param formFieldsObj - 已解码的表单字段规则列表
* @returns 打印展示时使用的区域、部门、用户名称映射
*/
async function loadPrintLookupMaps ( formFieldsObj : FormFieldRule [ ] ) {
const hasAreaSelect = formFieldsObj . some ( ( item ) => item . type === 'AreaSelect' ) ;
const hasUserSelect = formFieldsObj . some ( ( item ) => item . type === 'UserSelect' ) ;
const hasDeptSelect = formFieldsObj . some ( ( item ) => item . type === 'DeptSelect' ) ;
const [ areaList , userList , deptList ] = await Promise . all ( [
hasAreaSelect ? getAreaTree ( ) : Promise . resolve ( [ ] ) ,
hasUserSelect ? getSimpleUserList ( ) : Promise . resolve ( [ ] ) ,
hasDeptSelect ? getSimpleDeptList ( ) : Promise . resolve ( [ ] ) ,
] ) ;
return {
areaMap : flattenAreaTree ( areaList as Array < SystemAreaApi .Area > ) ,
deptMap : new Map (
( deptList ? ? [ ] ) . map ( ( item ) => [ String ( item . id ) , item . name ] as const ) ,
) ,
userMap : new Map (
( userList ? ? [ ] ) . map (
( item ) => [ String ( item . id ) , item . nickname ? ? item . username ] as const ,
) ,
) ,
} satisfies PrintLookupMaps ;
}
/**
* 根据表单字段规则和字段值,格式化为适合打印展示的 HTML 字符串。
*
* @param rule - 表单字段规则, 包含字段类型、选项、props 等配置信息
* @param value - 该字段在流程实例中的实际值
* @param lookupMaps - 预加载的查找映射表,包含区域、部门、用户的 id→name 映射
* @returns 格式化后的 HTML 字符串(纯文本或富文本,取决于字段类型)
*/
function formatPrintField (
rule : FormFieldRule ,
value : unknown ,
lookupMaps : PrintLookupMaps ,
) {
const type = String ( rule . type ? ? '' ) ;
switch ( type ) {
case 'AreaSelect' : {
const separator = String ( getRuleProp ( rule , 'separator' ) || '/' ) ;
return mapValueWithLabelMap ( value , lookupMaps . areaMap , separator ) ;
}
case 'cascader' :
case 'checkbox' :
case 'radio' :
case 'select' :
case 'treeSelect' : {
const options = getRuleProp ( rule , 'options' ) ;
return Array . isArray ( options ) && options . length > 0
? mapValuesWithOptions ( value , options as FormFieldOption [ ] )
: formatPrimitiveValue ( value ) ;
}
case 'date' :
case 'DatePicker' :
case 'datePicker' :
case 'daterange' :
case 'datetime' :
case 'datetimerange' :
case 'month' :
case 'monthrange' :
case 'RangePicker' :
case 'rangePicker' :
case 'TimePicker' :
case 'timePicker' :
case 'TimeRangePicker' :
case 'timeRangePicker' : {
return formatDateValue ( value ) ;
}
case 'DeptSelect' : {
if ( String ( getRuleProp ( rule , 'returnType' ) ) === 'name' ) {
return formatPrimitiveValue ( value ) ;
}
return mapValueWithLabelMap ( value , lookupMaps . deptMap ) ;
}
case 'DictSelect' : {
const dictType = getRuleProp ( rule , 'dictType' ) ;
if ( typeof dictType !== 'string' || ! dictType ) {
return formatPrimitiveValue ( value ) ;
}
const valueTypeMap = {
bool : 'boolean' ,
int : 'number' ,
str : 'string' ,
} as const ;
const rawValueType = String ( getRuleProp ( rule , 'valueType' ) ? ? '' ) ;
const valueType =
( valueTypeMap as Record < string , ' boolean ' | ' number ' | ' string ' > ) [ rawValueType ] ? ?
'string' ;
const options = getDictOptions ( dictType , valueType ) ;
return mapValuesWithOptions ( value , options ) ;
}
case 'FileUpload' : {
return renderFileListHtml ( value ) ;
}
case 'IframeComponent' : {
const propsObj = rule . props ;
const propsUrl =
isPrintableRecord ( propsObj )
? String ( getRecordValue ( propsObj , 'url' ) ? ? '' )
: '' ;
const iframeUrl = isEmptyValue ( value ) ? propsUrl : String ( value ? ? '' ) ;
return iframeUrl ? createFileLinkHtml ( iframeUrl ) : '' ;
}
case 'ImagesUpload' :
case 'ImageUpload' :
case 'UploadImg' : {
return renderImageListHtml ( value ) ;
}
case 'switch' : {
if ( isEmptyValue ( value ) ) return '否' ;
const checkedVal = getRuleProp ( rule , 'checkedValue' ) ;
const isChecked =
checkedVal !== undefined && checkedVal !== null
? value === checkedVal
: Boolean ( value ) ;
return isChecked ? '是' : '否' ;
}
case 'Tinymce' : {
return isEmptyValue ( value ) ? '' : String ( value ) ;
}
case 'UserSelect' : {
if ( String ( getRuleProp ( rule , 'returnType' ) ) === 'name' ) {
return formatPrimitiveValue ( value ) ;
}
return mapValueWithLabelMap ( value , lookupMaps . userMap ) ;
}
default : {
return formatPrimitiveValue ( value ) ;
}
}
}
/** 初始化打印数据映射 */
function initPrintDataMap ( ) {
if ( ! printData . value ) return ;
@@ -128,16 +430,18 @@ function initPrintDataMap() {
printDataMap . value . startUserDept =
printData . value . processInstance . startUser ? . deptName || '' ;
printDataMap . value . processName = printData . value . processInstance . name ;
printDataMap . value . processNum = printData . value . processInstance . id ;
printDataMap . value . processNum = String ( printData . value . processInstance . id ? ? '' ) ;
printDataMap . value . startTime = formatDate (
printData . value . processInstance . startTime ,
) ;
printDataMap . value . endTime = formatDate (
printData . value . processInstance . endTime ,
) ;
printDataMap . value . processStatus = getDictLabel (
DICT _TYPE . BPM _PROCESS _INSTANCE _STATUS ,
printData . value . processInstance . status ,
printDataMap . value . processStatus = String (
getDictLabel (
DICT _TYPE . BPM _PROCESS _INSTANCE _STATUS ,
printData . value . processInstance . status ,
) ? ? '' ,
) ;
printDataMap . value . printUser = userName . value ;
printDataMap . value . printTime = printTime . value ;