review:【bpm 工作流】流程打印

pull/817/head
YunaiV 2025-09-03 13:25:05 +08:00
parent 1c6e6eb24e
commit be964a6287
13 changed files with 139 additions and 126 deletions

View File

@ -42,8 +42,8 @@ import Logger from '@/utils/Logger'
import VueDOMPurifyHTML from 'vue-dompurify-html' // 解决v-html 的安全隐患
// wangeditor插件注册
import {setupWangeditorPlugin} from "@/views/bpm/model/form/PrintTemplate";
// wangeditor 插件注册
import { setupWangeditorPlugin } from '@/views/bpm/model/form/PrintTemplate'
import print from 'vue3-print-nb' // 打印插件
@ -67,12 +67,14 @@ const setupAll = async () => {
setupAuth(app)
setupMountedFocus(app)
// wangeditor 插件注册
setupWangeditorPlugin()
await router.isReady()
app.use(VueDOMPurifyHTML)
// 打印
app.use(print)
app.mount('#app')

View File

@ -254,7 +254,7 @@
</div>
</el-form-item>
</el-form>
<print-template ref="printTemplateRef" @confirm="confirmPrintTemplate"/>
<print-template ref="printTemplateRef" @confirm="confirmPrintTemplate" />
</template>
<script setup lang="ts">
@ -491,8 +491,9 @@ watch(
{ immediate: true }
)
// TODO defaultTemplate infra_config
const defaultTemplate = '<p style="text-align: center;"><span data-w-e-type="mention" data-w-e-is-void="" data-w-e-is-inline="" data-value="流程名称" data-info="%7B%22id%22%3A%22processName%22%7D">@流程名称</span></p><p style="text-align: right;">打印人:<span data-w-e-type="mention" data-w-e-is-void="" data-w-e-is-inline="" data-value="打印人" data-info="%7B%22id%22%3A%22printUser%22%7D">@打印人</span></p><p style="text-align: right;">流程编号:<span data-w-e-type="mention" data-w-e-is-void="" data-w-e-is-inline="" data-value="流程编号" data-info="%7B%22id%22%3A%22processNum%22%7D">@流程编号</span> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;打印时间:<span data-w-e-type="mention" data-w-e-is-void="" data-w-e-is-inline="" data-value="打印时间" data-info="%7B%22id%22%3A%22printTime%22%7D">@打印时间</span></p><table style="width: 100%;"><tbody><tr><td colSpan="1" rowSpan="1" width="auto">发起人</td><td colSpan="1" rowSpan="1" width="auto"><span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="发起人" data-info="%7B%22id%22%3A%22startUser%22%7D">@发起人</span></td><td colSpan="1" rowSpan="1" width="auto">发起时间</td><td colSpan="1" rowSpan="1" width="auto"><span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="发起时间" data-info="%7B%22id%22%3A%22startTime%22%7D">@发起时间</span></td></tr><tr><td colSpan="1" rowSpan="1" width="auto">所属部门</td><td colSpan="1" rowSpan="1" width="auto"><span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="发起人部门" data-info="%7B%22id%22%3A%22startUserDept%22%7D">@发起人部门</span></td><td colSpan="1" rowSpan="1" width="auto">流程状态</td><td colSpan="1" rowSpan="1" width="auto"><span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="流程状态" data-info="%7B%22id%22%3A%22processStatus%22%7D">@流程状态</span></td></tr></tbody></table><p><span data-w-e-type="process-record" data-w-e-is-void data-w-e-is-inline>流程记录</span></p>'
// TODO defaultTemplate infra_config TODO @lesan default
const defaultTemplate =
'<p style="text-align: center;"><span data-w-e-type="mention" data-w-e-is-void="" data-w-e-is-inline="" data-value="流程名称" data-info="%7B%22id%22%3A%22processName%22%7D">@流程名称</span></p><p style="text-align: right;">打印人:<span data-w-e-type="mention" data-w-e-is-void="" data-w-e-is-inline="" data-value="打印人" data-info="%7B%22id%22%3A%22printUser%22%7D">@打印人</span></p><p style="text-align: right;">流程编号:<span data-w-e-type="mention" data-w-e-is-void="" data-w-e-is-inline="" data-value="流程编号" data-info="%7B%22id%22%3A%22processNum%22%7D">@流程编号</span> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;打印时间:<span data-w-e-type="mention" data-w-e-is-void="" data-w-e-is-inline="" data-value="打印时间" data-info="%7B%22id%22%3A%22printTime%22%7D">@打印时间</span></p><table style="width: 100%;"><tbody><tr><td colSpan="1" rowSpan="1" width="auto">发起人</td><td colSpan="1" rowSpan="1" width="auto"><span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="发起人" data-info="%7B%22id%22%3A%22startUser%22%7D">@发起人</span></td><td colSpan="1" rowSpan="1" width="auto">发起时间</td><td colSpan="1" rowSpan="1" width="auto"><span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="发起时间" data-info="%7B%22id%22%3A%22startTime%22%7D">@发起时间</span></td></tr><tr><td colSpan="1" rowSpan="1" width="auto">所属部门</td><td colSpan="1" rowSpan="1" width="auto"><span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="发起人部门" data-info="%7B%22id%22%3A%22startUserDept%22%7D">@发起人部门</span></td><td colSpan="1" rowSpan="1" width="auto">流程状态</td><td colSpan="1" rowSpan="1" width="auto"><span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="流程状态" data-info="%7B%22id%22%3A%22processStatus%22%7D">@流程状态</span></td></tr></tbody></table><p><span data-w-e-type="process-record" data-w-e-is-void data-w-e-is-inline>流程记录</span></p>'
const handlePrintTemplateEnableChange = (val: boolean) => {
if (val) {
if (!modelData.value.printTemplateSetting.template) {
@ -504,7 +505,7 @@ const printTemplateRef = ref()
const handleEditPrintTemplate = () => {
printTemplateRef.value.open(modelData.value.printTemplateSetting.template)
}
const confirmPrintTemplate = (template) => {
const confirmPrintTemplate = (template: any) => {
modelData.value.printTemplateSetting.template = template
}
</script>

View File

@ -1,11 +1,11 @@
<script setup lang="ts">
import {Editor, Toolbar} from '@wangeditor/editor-for-vue'
import {IDomEditor} from '@wangeditor/editor'
import MentionModal from "./MentionModal.vue";
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { IDomEditor } from '@wangeditor/editor'
import MentionModal from './MentionModal.vue'
const emit = defineEmits(['confirm'])
// mention
// @mention
const isShowModal = ref(false)
const showModal = () => {
isShowModal.value = true
@ -13,12 +13,12 @@ const showModal = () => {
const hideModal = () => {
isShowModal.value = false
}
const insertMention = (id, name) => {
const insertMention = (id: any, name: any) => {
const mentionNode = {
type: 'mention',
value: name,
info: {id},
children: [{text: ''}],
info: { id },
children: [{ text: '' }]
}
const editor = editorRef.value
if (editor) {
@ -29,20 +29,19 @@ const insertMention = (id, name) => {
}
}
// Dialog
// Dialog
const dialogVisible = ref(false)
const open = async (template) => {
const open = async (template: string) => {
dialogVisible.value = true
valueHtml.value = template
console.log(template)
}
defineExpose({open})
defineExpose({ open })
const handleConfirm = () => {
emit('confirm', valueHtml.value)
dialogVisible.value = false
}
// Editor
// Editor
const editorRef = shallowRef<IDomEditor>()
const editorId = ref('wangeEditor-1')
const toolbarConfig = {
@ -57,41 +56,45 @@ const editorConfig = {
EXTEND_CONF: {
mentionConfig: {
showModal,
hideModal,
},
},
hideModal
}
}
}
const valueHtml = ref()
const handleCreated = (editor: IDomEditor) => {
editorRef.value = editor
}
// onBeforeUnmount
/** 初始化 */
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
if (editor == null) {
return
}
editor.destroy()
})
</script>
<template>
<el-dialog v-model="dialogVisible" title="自定义模板" fullscreen>
<div style="margin: 0 10px;">
<div style="margin: 0 10px">
<el-alert
title="输入 @ 可选择插入流程表单选项和默认选项"
type="info"
show-icon
:closable="false"/>
:closable="false"
/>
</div>
<div style="border: 1px solid #ccc;margin: 10px;">
<!-- TODO @unocss 简化 style -->
<div style="border: 1px solid #ccc; margin: 10px">
<Toolbar
style="border-bottom: 1px solid #ccc;"
style="border-bottom: 1px solid #ccc"
:editor="editorRef"
:editorId="editorId"
:defaultConfig="toolbarConfig"
/>
<Editor
style="height: 500px; overflow-y: hidden;"
style="height: 500px; overflow-y: hidden"
v-model="valueHtml"
:defaultConfig="editorConfig"
:editorId="editorId"
@ -100,9 +103,10 @@ onBeforeUnmount(() => {
<MentionModal
v-if="isShowModal"
@hide-mention-modal="hideModal"
@insert-mention="insertMention"/>
@insert-mention="insertMention"
/>
</div>
<div style="margin-right: 10px;float: right;">
<div style="margin-right: 10px; float: right">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="handleConfirm"> </el-button>
</div>

View File

@ -6,47 +6,48 @@ const top = ref('')
const left = ref('')
const searchVal = ref('')
const list = ref([
{id: 'startUser', name: '发起人'},
{id: 'startUserDept', name: '发起人部门'},
{id: 'processName', name: '流程名称'},
{id: 'processNum', name: '流程编号'},
{id: 'startTime', name: '发起时间'},
{id: 'endTime', name: '发起时间'},
{id: 'processStatus', name: '流程状态'},
{id: 'processResult', name: '流程结果'},
{id: 'printUser', name: '打印人'},
{id: 'printTime', name: '打印时间'},
{ id: 'startUser', name: '发起人' },
{ id: 'startUserDept', name: '发起人部门' },
{ id: 'processName', name: '流程名称' },
{ id: 'processNum', name: '流程编号' },
{ id: 'startTime', name: '发起时间' },
{ id: 'endTime', name: '发起时间' },
{ id: 'processStatus', name: '流程状态' },
{ id: 'processResult', name: '流程结果' },
{ id: 'printUser', name: '打印人' },
{ id: 'printTime', name: '打印时间' }
])
const searchedList = computed(() => {
const searchValStr = searchVal.value.trim().toLowerCase()
return list.value.filter(item => {
return list.value.filter((item) => {
const name = item.name.toLowerCase()
return name.indexOf(searchValStr) >= 0;
return name.indexOf(searchValStr) >= 0
})
})
const inputKeyupHandler = (event) => {
const inputKeyupHandler = (event: any) => {
if (event.key === 'Escape') {
emit('hideMentionModal')
}
if (event.key === 'Enter') {
const firstOne = searchedList.value[0]
if (firstOne) {
const {id, name} = firstOne
const { id, name } = firstOne
insertMentionHandler(id, name)
}
}
}
const insertMentionHandler = (id, name) => {
const insertMentionHandler = (id: any, name: any) => {
emit('insertMention', id, name)
emit('hideMentionModal')
}
const formFields = inject('formFieldsObj')
onMounted(()=> {
onMounted(() => {
// TODO @lesan idea
if (formFields.value && formFields.value.length > 0) {
const cloneFormField = formFields.value.map((item) => {
return {
name: '[表单]'+item.title,
name: '[表单]' + item.title,
id: item.field
}
})
@ -66,13 +67,15 @@ onMounted(()=> {
<template>
<div id="mention-modal" :style="{ top: top, left: left }">
<!-- TODO @lesancss 可以用 unocss -->
<input id="mention-input" v-model="searchVal" ref="inputRef" @keyup="inputKeyupHandler" />
<ul id="mention-list">
<li
v-for="item in searchedList"
:key="item.id"
@click="insertMentionHandler(item.id, item.name)"
>{{ item.name }}
>
{{ item.name }}
</li>
</ul>
</div>

View File

@ -1,8 +1,9 @@
import {Boot} from '@wangeditor/editor'
import processRecordModule from "./module";
import mentionModule from "@wangeditor/plugin-mention";
import { Boot } from '@wangeditor/editor'
import processRecordModule from './module'
import mentionModule from '@wangeditor/plugin-mention'
// 注册。要在创建编辑器之前注册,且只能注册一次,不可重复注册。
// 注册:要在创建编辑器之前注册,且只能注册一次,不可重复注册
// TODO @lesanWangEditor 拼写
export const setupWangeditorPlugin = () => {
Boot.registerModule(processRecordModule)
Boot.registerModule(mentionModule)

View File

@ -1,12 +1,12 @@
import { SlateElement } from '@wangeditor/editor'
function processRecordToHtml(elem: SlateElement, childrenHtml: string): string {
function processRecordToHtml(_elem: SlateElement, _childrenHtml: string): string {
return `<span data-w-e-type="process-record" data-w-e-is-void data-w-e-is-inline>流程记录</span>`
}
const conf = {
type: 'process-record',
elemToHtml: processRecordToHtml,
elemToHtml: processRecordToHtml
}
export default conf

View File

@ -1,16 +1,17 @@
import {IModuleConf} from '@wangeditor/editor'
import { IModuleConf } from '@wangeditor/editor'
import withProcessRecord from './plugin'
import renderElemConf from './render-elem'
import elemToHtmlConf from './elem-to-html'
import parseHtmlConf from './parse-elem-html'
import processRecordMenu from "./menu/ProcessRecordMenu"
import processRecordMenu from './menu/ProcessRecordMenu'
// TODO @lesanPrintTemplate 是参考了哪些文档哇?要不要在 index.ts 稍微写点注释,方便大家理解;
const module: Partial<IModuleConf> = {
editorPlugin: withProcessRecord,
renderElems: [renderElemConf],
elemsToHtml: [elemToHtmlConf],
parseElemsHtml: [parseHtmlConf],
menus: [processRecordMenu],
menus: [processRecordMenu]
}
export default module

View File

@ -1,31 +1,31 @@
import { IButtonMenu, IDomEditor } from '@wangeditor/editor'
class ProcessRecordMenu implements IButtonMenu {
readonly tag: string;
readonly title: string;
readonly tag: string
readonly title: string
constructor() {
this.title = '流程记录'
this.tag = 'button'
}
getValue(editor: IDomEditor): string {
getValue(_editor: IDomEditor): string {
return ''
}
isActive(editor: IDomEditor): boolean {
isActive(_editor: IDomEditor): boolean {
return false
}
isDisabled(editor: IDomEditor): boolean {
isDisabled(_editor: IDomEditor): boolean {
return false
}
exec(editor: IDomEditor, value: string) {
exec(editor: IDomEditor, _value: string) {
if (this.isDisabled(editor)) return
const processRecordElem = {
type: 'process-record',
children: [{ text: '' }],
children: [{ text: '' }]
}
editor.insertNode(processRecordElem)
editor.move(1)

View File

@ -2,19 +2,20 @@ import { DOMElement } from './utils/dom'
import { IDomEditor, SlateDescendant, SlateElement } from '@wangeditor/editor'
function parseHtml(
elem: DOMElement,
children: SlateDescendant[],
editor: IDomEditor
_elem: DOMElement,
_children: SlateDescendant[],
_editor: IDomEditor
): SlateElement {
return {
// TODO @lesan这里有个红色告警可以去掉哇
type: 'process-record',
children: [{ text: '' }],
children: [{ text: '' }]
}
}
const parseHtmlConf = {
selector: 'span[data-w-e-type="process-record"]',
parseElemHtml: parseHtml,
parseElemHtml: parseHtml
}
export default parseHtmlConf

View File

@ -4,7 +4,7 @@ function withProcessRecord<T extends IDomEditor>(editor: T) {
const { isInline, isVoid } = editor
const newEditor = editor
newEditor.isInline = elem => {
newEditor.isInline = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'process-record') {
return true
@ -13,7 +13,7 @@ function withProcessRecord<T extends IDomEditor>(editor: T) {
return isInline(elem)
}
newEditor.isVoid = elem => {
newEditor.isVoid = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'process-record') {
return true

View File

@ -1,72 +1,73 @@
import {h, VNode} from 'snabbdom'
import {DomEditor, IDomEditor, SlateElement} from '@wangeditor/editor'
import { h, VNode } from 'snabbdom'
import { DomEditor, IDomEditor, SlateElement } from '@wangeditor/editor'
function renderProcessRecord(elem: SlateElement, children: VNode[] | null, editor: IDomEditor): VNode {
function renderProcessRecord(
elem: SlateElement,
_children: VNode[] | null,
editor: IDomEditor
): VNode {
const selected = DomEditor.isNodeSelected(editor, elem)
const vnode = h(
return h(
'table',
{
props: {
contentEditable: false,
contentEditable: false
},
style: {
width: '100%',
border: selected
? '2px solid var(--w-e-textarea-selected-border-color)'
: '',
},
border: selected ? '2px solid var(--w-e-textarea-selected-border-color)' : ''
}
},
[
h('thead', [
h('tr', [h('th', {attrs: {colSpan: 3}}, '流程记录')])
]),
h('thead', [h('tr', [h('th', { attrs: { colSpan: 3 } }, '流程记录')])]),
h('tbody', [
h('tr', [
h('td', [h(
'span',
{
props: {
contentEditable: false,
h('td', [
h(
'span',
{
props: {
contentEditable: false
},
style: {
marginLeft: '3px',
marginRight: '3px',
backgroundColor: 'var(--w-e-textarea-slight-bg-color)',
borderRadius: '3px',
padding: '0 3px'
}
},
style: {
marginLeft: '3px',
marginRight: '3px',
backgroundColor: 'var(--w-e-textarea-slight-bg-color)',
borderRadius: '3px',
padding: '0 3px',
},
},
`节点`
)
`节点`
)
]),
h('td', [h(
'span',
{
props: {
contentEditable: false,
h('td', [
h(
'span',
{
props: {
contentEditable: false
},
style: {
marginLeft: '3px',
marginRight: '3px',
backgroundColor: 'var(--w-e-textarea-slight-bg-color)',
borderRadius: '3px',
padding: '0 3px'
}
},
style: {
marginLeft: '3px',
marginRight: '3px',
backgroundColor: 'var(--w-e-textarea-slight-bg-color)',
borderRadius: '3px',
padding: '0 3px',
},
},
`操作`
)
`操作`
)
])
])
])
]
)
return vnode
}
const conf = {
type: 'process-record',
renderElem: renderProcessRecord,
renderElem: renderProcessRecord
}
export default conf

View File

@ -12,7 +12,7 @@ const printData = ref()
const userName = computed(() => userStore.user.nickname ?? '')
const printTime = ref(formatDate(new Date(), 'YYYY-MM-DD HH:mm'))
const open = async (id) => {
const open = async (id: string) => {
loading.value = true
try {
printData.value = await ProcessInstanceApi.getProcessInstancePrintData(id)
@ -98,6 +98,7 @@ const printObj = ref({
</div>
</template>
</el-dialog>
<!-- TODO @lesan最下面的 table 央视可以去掉么 -->
</template>
<style lang="scss">

View File

@ -10,7 +10,7 @@
/>
<div class="flex">
<div class="text-#878c93 h-15px">编号{{ id }}</div>
<Icon icon="ep:printer" class="ml-15px cursor-pointer" @click="handlePrint"/>
<Icon icon="ep:printer" class="ml-15px cursor-pointer" @click="handlePrint" />
</div>
<el-divider class="!my-8px" />
<div class="flex items-center gap-5 mb-10px h-40px">
@ -128,6 +128,8 @@
</el-scrollbar>
</div>
</ContentWrap>
<!-- 打印预览弹窗 -->
<PrintDialog ref="printRef" />
</template>
<script lang="ts" setup>
@ -192,6 +194,7 @@ const getDetail = () => {
/** 加载流程实例 */
const BusinessFormComponent = ref<any>(null) //
/** 获取审批详情 */
const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) //
const getApprovalDetail = async () => {
processInstanceLoading.value = true
try {
@ -270,11 +273,7 @@ const getProcessModelView = async () => {
}
}
//
const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([])
/**
* 设置表单权限
*/
/** 设置表单权限 */
const setFieldPermission = (field: string, permission: string) => {
if (permission === FieldPermissionType.READ) {
//@ts-ignore
@ -292,20 +291,19 @@ const setFieldPermission = (field: string, permission: string) => {
}
}
/**
* 操作成功后刷新
*/
/** 操作成功后刷新 */
const refresh = () => {
//
getDetail()
}
/** 处理打印 */
const printRef = ref()
const handlePrint = async () => {
printRef.value.open(props.id)
}
/** 当前的Tab */
/** 当前的 Tab */
const activeTab = ref('form')
/** 初始化 */