You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
besure_web/src/views/mes/printTemplate/PrintTemplateDesigner.vue

759 lines
22 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="92%" :fullscreen="false" top="4vh">
<div class="hiprint-preview">
<div class="hiprint-toolbar">
<div class="hiprint-paper">
<el-button
v-for="(value, type) in paperTypes"
:key="type"
size="small"
:type="curPaperType === type ? 'primary' : 'default'"
@click="setPaper(type, value)"
>
{{ type }}
</el-button>
<el-popover placement="bottom-start" :width="260" trigger="click" v-model:visible="paperPopVisible">
<template #reference>
<el-button size="small" :type="curPaperType === 'other' ? 'primary' : 'default'">自定义纸张</el-button>
</template>
<div class="paper-pop">
<div class="paper-pop-title">设置纸张宽高(mm)</div>
<div class="paper-pop-form">
<el-input-number v-model="paperWidth" :precision="1" :min="1" controls-position="right" />
<span>x</span>
<el-input-number v-model="paperHeight" :precision="1" :min="1" controls-position="right" />
</div>
<el-button class="mt-8px" size="small" type="primary" @click="setPaperOther">确定</el-button>
</div>
</el-popover>
</div>
<div class="hiprint-zoom">
<el-button size="small" @click="changeScale(false)">
<Icon icon="ep:zoom-out" />
</el-button>
<div class="zoom-value">{{ (scaleValue * 100).toFixed(0) }}%</div>
<el-button size="small" @click="changeScale(true)">
<Icon icon="ep:zoom-in" />
</el-button>
</div>
<el-button size="small" @click="rotatePaper">
<Icon icon="ep:refresh-right" class="mr-4px" />
旋转
</el-button>
<el-button size="small" type="warning" @click="handlePreview">
<Icon icon="ep:view" class="mr-4px" />
预览
</el-button>
<el-popconfirm title="是否确认清空?" @confirm="clearPaper">
<template #reference>
<el-button size="small" type="danger">
<Icon icon="ep:delete" class="mr-4px" />
清空
</el-button>
</template>
</el-popconfirm>
<el-button type="primary" size="small" :loading="saveLoading" @click="handleSave">
<Icon icon="ep:check" class="mr-4px" />
{{ t('common.save') }}
</el-button>
</div>
<div class="hiprint-body">
<div class="hiprint-left">
<div class="hiprint-title">基础元素</div>
<div :id="dragContainerId" class="hiprint-drag-wrap">
<div v-for="item in baseElements" :key="item.tid" class="ep-draggable-item hiprint-item" :tid="item.tid">
<span>{{ item.label }}</span>
</div>
</div>
<div v-if="barcodeGroups.length" :id="barcodeDragContainerId" class="hiprint-barcode-groups">
<div v-for="group in barcodeGroups" :key="group.value" class="hiprint-barcode-group">
<div class="hiprint-title hiprint-title-group">{{ group.label }}</div>
<div class="hiprint-drag-wrap hiprint-drag-wrap-compact">
<div v-for="item in group.elements" :key="item.tid" class="ep-draggable-item hiprint-item" :tid="item.tid">
<span style="display: block; text-align: center;">{{ item.label }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="hiprint-center">
<div :id="designerContainerId"></div>
</div>
<div class="hiprint-right">
<div :id="settingContainerId"></div>
<div v-if="selectedIconElement" class="hiprint-custom-setting">
<div class="hiprint-title hiprint-title-inline">图标属性</div>
<el-form label-position="top" size="small">
<el-form-item label="图标">
<IconSelect v-model="selectedIconName" :persistent="false" :teleported="false" />
</el-form-item>
<el-form-item label="颜色">
<el-color-picker v-model="selectedIconColor" show-alpha />
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
<el-dialog v-model="previewVisible" title="打印预览" width="70%" top="2vh" append-to-body>
<div v-html="previewHtml" style="background: #fff;"></div>
</el-dialog>
</Dialog>
</template>
<script setup lang="ts">
import { defaultElementTypeProvider, hiprint } from 'vue-plugin-hiprint'
import Iconify from '@purge-icons/generated'
import { PrintTemplateApi } from '@/api/mes/printtemplate'
import { getSimpleDictDataList } from '@/api/system/dict/dict.data'
import qrCodeImg from '@/assets/imgs/qrCode.png'
const { t } = useI18n()
const message = useMessage()
const baseElements = [
{ tid: 'defaultModule.text', label: '文本' },
{ tid: 'defaultModule.image', label: '图片' },
{ tid: 'qrcodeModule.qrcode', label: '二维码' },
{ tid: 'iconModule.icon', label: '图标' },
{ tid: 'defaultModule.longText', label: '长文' },
{ tid: 'defaultModule.table', label: '表格' },
{ tid: 'defaultModule.hline', label: '横线' },
{ tid: 'defaultModule.vline', label: '竖线' },
{ tid: 'defaultModule.rect', label: '矩形' },
{ tid: 'defaultModule.oval', label: '圆形' }
]
const defaultIconName = 'ep:star-filled'
const defaultIconColor = '#409EFF'
const buildIconHtml = (iconName?: string, color?: string) => {
const finalIconName = iconName || defaultIconName
const finalColor = color || defaultIconColor
const svg = Iconify.renderSVG(finalIconName, {
height: '100%',
width: '100%'
})
if (svg) {
svg.setAttribute('color', finalColor)
svg.style.color = finalColor
svg.style.display = 'block'
svg.style.width = '100%'
svg.style.height = '100%'
return svg.outerHTML
}
return `<span class="iconify" data-icon="${finalIconName}" data-width="100%" data-height="100%" style="display:inline-block;width:100%;height:100%;color:${finalColor};"></span>`
}
const buildIconFormatter = () => {
return `function(value, options) {
const iconName = options.iconName || '${defaultIconName}';
const color = options.color || '${defaultIconColor}';
if (window.__hiprintBuildIconHtml) {
return window.__hiprintBuildIconHtml(iconName, color);
}
return '<span class="iconify" data-icon="' + iconName + '" data-width="100%" data-height="100%" style="display:inline-block;width:100%;height:100%;color:' + color + ';"></span>';
}`
}
const ensureIconRuntime = () => {
;(window as any).__hiprintBuildIconHtml = buildIconHtml
}
type BarcodeElementItem = {
tid: string
label: string
}
type BarcodeElementGroup = {
value: string | number
label: string
elements: BarcodeElementItem[]
}
const barcodeGroups = ref<BarcodeElementGroup[]>([])
const barcodeDictData = ref<any[]>([])
const loadBarcodeDictData = async () => {
try {
const res = await getSimpleDictDataList()
const filtered = (res || []).filter((item: any) => item.dictType === 'print_template_type')
barcodeDictData.value = filtered
barcodeGroups.value = filtered.map((item: any) => ({
value: item.value,
label: item.label,
elements: [
{ tid: `barcodeModule.${item.value}_qrcode`, label: '二维码' },
{ tid: `barcodeModule.${item.value}_name`, label: '名称' },
{ tid: `barcodeModule.${item.value}_code`, label: '编码' }
]
}))
} catch (e) {
console.error('加载条码字典数据失败', e)
}
}
const barcodeProvider = function () {
const addElementTypes = function (context: any) {
context.removePrintElementTypes('barcodeModule')
if (!barcodeDictData.value.length) return
const groups: any[] = []
barcodeDictData.value.forEach((item: any) => {
const remark = item.remark || ''
const parts = remark.split(',').map((s: string) => s.trim())
const qrcodeField = parts[0] || ''
const nameField = parts[1] || ''
const codeField = parts[2] || parts[1] || ''
groups.push(
new hiprint.PrintElementTypeGroup(item.label, [
{
tid: `barcodeModule.${item.value}_qrcode`,
title: '二维码',
type: 'image',
options: {
field: qrcodeField,
src: qrCodeImg,
testData: qrCodeImg,
width: 80,
height: 80,
title: '二维码'
}
},
{
tid: `barcodeModule.${item.value}_name`,
title: '名称',
type: 'text',
options: {
field: nameField,
testData: '',
title: '名称',
fontSize: 10,
textAlign: 'center',
width: 80,
height: 16
}
},
{
tid: `barcodeModule.${item.value}_code`,
title: '编码',
type: 'text',
options: {
field: codeField,
testData: '',
title: '编码',
fontSize: 10,
textAlign: 'center',
width: 80,
height: 16
}
}
])
)
})
context.addPrintElementTypes('barcodeModule', groups)
}
return { addElementTypes }
}
const qrcodeProvider = function () {
const addElementTypes = function (context: any) {
context.removePrintElementTypes('qrcodeModule')
context.addPrintElementTypes('qrcodeModule', [
new hiprint.PrintElementTypeGroup('二维码', [
{
tid: 'qrcodeModule.qrcode',
title: '二维码',
type: 'image',
options: {
field: '',
src: qrCodeImg,
testData: qrCodeImg,
width: 80,
height: 80,
title: '二维码'
}
}
])
])
}
return { addElementTypes }
}
const iconProvider = function () {
const addElementTypes = function (context: any) {
context.removePrintElementTypes('iconModule')
context.addPrintElementTypes('iconModule', [
new hiprint.PrintElementTypeGroup('图标', [
{
tid: 'iconModule.icon',
title: '图标',
type: 'html',
options: {
left: 12,
top: 12,
width: 28,
height: 28,
iconName: defaultIconName,
color: defaultIconColor,
formatter: buildIconFormatter()
},
onRendered: function (target: any) {
const iconify = (window as any).Iconify
if (iconify?.scan) {
iconify.scan(target?.[0] || target)
}
},
supportOptions: [
{ name: 'iconName' },
{ name: 'color' },
{ name: 'widthHeight' },
{ name: 'coordinate' },
{ name: 'transform' },
{ name: 'zIndex' }
]
}
])
])
}
return { addElementTypes }
}
const dialogVisible = ref(false)
const dialogTitle = ref('模板配置')
const saveLoading = ref(false)
const currentRow = ref<any>(null)
const currentTemplateJson = ref<Record<string, any> | undefined>(undefined)
const instanceId = `hiprint-designer-${Math.random().toString(36).slice(2)}`
const dragContainerId = `${instanceId}-drag`
const barcodeDragContainerId = `${instanceId}-barcode-drag`
const designerContainerId = `${instanceId}-designer`
const settingContainerId = `${instanceId}-setting`
const paperTypes = {
A3: { width: 420, height: 296.6 },
A4: { width: 210, height: 296.6 },
A5: { width: 210, height: 147.6 },
B3: { width: 500, height: 352.6 },
B4: { width: 250, height: 352.6 },
B5: { width: 250, height: 175.6 }
}
const defaultPaperConfig = {
type: 'A4',
width: 210,
height: 296.6
}
const curPaper = ref({
...defaultPaperConfig
})
const paperPopVisible = ref(false)
const paperWidth = ref(220)
const paperHeight = ref(80)
const curPaperType = computed(() => {
let type = 'other'
for (const [key, value] of Object.entries(paperTypes)) {
if (value.width === curPaper.value.width && value.height === curPaper.value.height) {
type = key
break
}
}
return type
})
const scaleValue = ref(1)
const scaleMax = 5
const scaleMin = 0.5
const previewVisible = ref(false)
const previewHtml = ref('')
const selectedIconElement = shallowRef<any>(null)
const selectedIconName = ref(defaultIconName)
const selectedIconColor = ref(defaultIconColor)
let hiprintInited = false
let hiprintTemplate: any
let iconSelectEventKey = ''
let iconSelectHandler: ((payload: any) => void) | undefined
let clearSettingHandler: (() => void) | undefined
const normalizePaperConfig = (value: any) => {
const width = Number(value?.width)
const height = Number(value?.height)
if (width <= 0 || height <= 0 || Number.isNaN(width) || Number.isNaN(height)) {
return { ...defaultPaperConfig }
}
return {
type: typeof value?.type === 'string' && value.type ? value.type : 'other',
width,
height
}
}
const applyPaperConfig = (value: any) => {
const paper = normalizePaperConfig(value)
curPaper.value = paper
paperWidth.value = paper.width
paperHeight.value = paper.height
}
const getTemplatePaperConfig = (value: any) => {
const panel = Array.isArray(value?.panels) ? value.panels[0] : undefined
return panel
}
const ensureInit = () => {
if (hiprintInited) {
return
}
ensureIconRuntime()
hiprint.init({
providers: [new defaultElementTypeProvider(), qrcodeProvider(), barcodeProvider(), iconProvider()]
})
hiprintInited = true
}
const isIconPrintElement = (printElement: any) => {
if (!printElement) {
return false
}
if (printElement?.printElementType?.tid === 'iconModule.icon') {
return true
}
const formatter = String(printElement?.options?.formatter || printElement?.printElementType?.formatter || '')
return Boolean(
printElement?.printElementType?.type === 'html'
&& (
typeof printElement?.options?.iconName === 'string'
|| formatter.includes('__hiprintBuildIconHtml')
)
)
}
const normalizeIconPrintElement = (printElement: any) => {
if (!printElement?.options) {
return printElement
}
if (!printElement.options.iconName) {
printElement.options.iconName = defaultIconName
}
if (!printElement.options.color) {
printElement.options.color = defaultIconColor
}
if (!printElement.options.formatter) {
printElement.options.formatter = buildIconFormatter()
}
return printElement
}
const setSelectedIconState = (printElement: any | null) => {
const normalizedPrintElement = printElement ? normalizeIconPrintElement(printElement) : null
selectedIconElement.value = normalizedPrintElement
selectedIconName.value = normalizedPrintElement?.options?.iconName || defaultIconName
selectedIconColor.value = normalizedPrintElement?.options?.color || defaultIconColor
}
const updateSelectedIconElement = () => {
const printElement = selectedIconElement.value
if (!printElement) {
return
}
printElement.options.iconName = selectedIconName.value || defaultIconName
printElement.options.color = selectedIconColor.value || defaultIconColor
printElement.updateDesignViewFromOptions()
const iconify = (window as any).Iconify
if (iconify?.scan) {
iconify.scan(printElement.designTarget?.[0] || printElement.designTarget)
}
;(window as any).hinnn?.event?.trigger(`hiprintTemplateDataChanged_${printElement.templateId}`, '元素修改')
}
const bindIconCustomPanel = (template: any) => {
const event = (window as any).hinnn?.event
if (!event) {
return
}
if (iconSelectEventKey && iconSelectHandler) {
event.off(iconSelectEventKey, iconSelectHandler)
}
if (clearSettingHandler) {
event.off('clearSettingContainer', clearSettingHandler)
}
iconSelectEventKey = template?.getPrintElementSelectEventKey?.() || ''
iconSelectHandler = (payload: any) => {
const printElement = payload?.printElement
if (isIconPrintElement(printElement)) {
setSelectedIconState(printElement)
return
}
setSelectedIconState(null)
}
clearSettingHandler = () => {
setSelectedIconState(null)
}
if (iconSelectEventKey) {
event.on(iconSelectEventKey, iconSelectHandler)
}
event.on('clearSettingContainer', clearSettingHandler)
}
const buildLeftElement = () => {
const jquery = (window as any).$
if (!jquery) {
message.warning('未检测到 jQuery无法加载拖拽元素')
return
}
hiprint.PrintElementTypeManager.buildByHtml(jquery(`#${dragContainerId} .ep-draggable-item`))
if (barcodeGroups.value.length) {
hiprint.PrintElementTypeManager.buildByHtml(jquery(`#${barcodeDragContainerId} .ep-draggable-item`))
}
}
const buildDesigner = () => {
const jquery = (window as any).$
if (!jquery) {
message.warning('未检测到 jQuery无法初始化打印设计器')
return
}
jquery(`#${designerContainerId}`).empty()
const template = currentTemplateJson.value || undefined
hiprintTemplate = new hiprint.PrintTemplate({
template,
settingContainer: `#${settingContainerId}`
})
bindIconCustomPanel(hiprintTemplate)
hiprintTemplate.design(`#${designerContainerId}`)
setPaper(curPaperType.value, { width: curPaper.value.width, height: curPaper.value.height })
hiprintTemplate.zoom(scaleValue.value)
}
const setPaper = (type: string, value: { width: number; height: number }) => {
if (!hiprintTemplate) {
return
}
const width = Number(value.width)
const height = Number(value.height)
curPaper.value = { type, width, height }
hiprintTemplate.setPaper(width, height)
}
const setPaperOther = () => {
paperPopVisible.value = false
setPaper('other', { width: Number(paperWidth.value), height: Number(paperHeight.value) })
}
const changeScale = (isZoomIn: boolean) => {
if (!hiprintTemplate) {
return
}
let nextScale = scaleValue.value
if (isZoomIn) {
nextScale += 0.1
if (nextScale > scaleMax) nextScale = scaleMax
} else {
nextScale -= 0.1
if (nextScale < scaleMin) nextScale = scaleMin
}
scaleValue.value = nextScale
hiprintTemplate.zoom(nextScale)
}
const rotatePaper = () => {
if (!hiprintTemplate) return
hiprintTemplate.rotatePaper()
}
const handlePreview = () => {
if (!hiprintTemplate) return
const jquery = (window as any).$
const htmlResult = hiprintTemplate.getHtml({})
if (htmlResult && jquery) {
previewHtml.value = jquery('<div>').append(htmlResult).html() || ''
}
previewVisible.value = true
}
const clearPaper = () => {
if (!hiprintTemplate) return
try {
hiprintTemplate.clear()
setSelectedIconState(null)
} catch (error) {
message.error('清空失败')
}
}
const handleSave = async () => {
if (!hiprintTemplate) {
return
}
const templateJson = hiprintTemplate.getJson()
saveLoading.value = true
try {
await PrintTemplateApi.updatePrintTemplate({
id: currentRow.value.id,
templateCode: currentRow.value.templateCode,
templateName: currentRow.value.templateName,
templateType: currentRow.value.templateType,
templateBizType: currentRow.value.templateBizType ?? 1,
templateJson: JSON.stringify(templateJson),
} as any)
message.success(t('common.updateSuccess'))
dialogVisible.value = false
emit('success')
} finally {
saveLoading.value = false
}
}
const resetState = () => {
scaleValue.value = 1
setSelectedIconState(null)
applyPaperConfig(defaultPaperConfig)
paperPopVisible.value = false
}
const open = async (row: any) => {
currentRow.value = row
dialogTitle.value = `${t('TemplateManagement.PrintTemplate.designTitle')}${row.templateName ? ' - ' + row.templateName : ''}`
currentTemplateJson.value = row.templateJson ? (typeof row.templateJson === 'string' ? JSON.parse(row.templateJson) : row.templateJson) : undefined
resetState()
applyPaperConfig(getTemplatePaperConfig(currentTemplateJson.value))
await loadBarcodeDictData()
dialogVisible.value = true
await nextTick()
ensureInit()
buildLeftElement()
buildDesigner()
}
const emit = defineEmits(['success'])
defineExpose({ open })
watch(selectedIconName, () => {
updateSelectedIconElement()
})
watch(selectedIconColor, () => {
updateSelectedIconElement()
})
</script>
<style scoped lang="scss">
.hiprint-preview {
width: 100%;
}
.hiprint-toolbar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.hiprint-paper {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.hiprint-zoom {
display: flex;
align-items: center;
gap: 4px;
}
.zoom-value {
width: 56px;
text-align: center;
font-size: 13px;
color: var(--el-text-color-regular);
}
.paper-pop-title {
font-size: 14px;
font-weight: 600;
}
.paper-pop-form {
margin-top: 8px;
display: flex;
align-items: center;
gap: 8px;
}
.hiprint-body {
height: 75vh;
display: grid;
grid-template-columns: 220px 1fr 400px;
gap: 12px;
}
.hiprint-left,
.hiprint-center,
.hiprint-right {
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-light);
border-radius: 8px;
overflow: auto;
}
.hiprint-center {
padding: 16px;
}
.hiprint-right {
padding: 12px;
}
.hiprint-custom-setting {
margin-top: 12px;
border-top: 1px solid var(--el-border-color-light);
padding-top: 12px;
}
.hiprint-title {
padding: 10px 10px 0;
font-weight: 600;
}
.hiprint-title-group {
padding-top: 12px;
}
.hiprint-title-inline {
padding: 0 0 12px;
}
.hiprint-barcode-groups {
padding-bottom: 10px;
}
.hiprint-drag-wrap {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
padding: 10px;
}
.hiprint-drag-wrap-compact {
grid-template-columns: repeat(3, minmax(0, 1fr));
padding-top: 8px;
}
.hiprint-item {
min-height: 56px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
border: 1px solid var(--el-border-color);
background: var(--el-fill-color-light);
color: var(--el-text-color-primary);
cursor: grab;
}
</style>