新增打印模块功能,支持图片分析生成原生模板JSON,查询可用打印机,服务端直打功能,优化打印设计器界面,添加打印机选择和快速打印选项,同时更新依赖项以支持PDF处理。

This commit is contained in:
geht
2026-04-14 17:18:50 +08:00
parent 0024c071ff
commit e04169a694
55 changed files with 30188 additions and 1595 deletions

932
hs_err_pid6216.log Normal file
View File

@@ -0,0 +1,932 @@
#
# There is insufficient memory for the Java Runtime Environment to continue.
# Native memory allocation (malloc) failed to allocate 664336 bytes. Error detail: Chunk::new
# Possible reasons:
# The system is out of physical RAM or swap space
# This process is running with CompressedOops enabled, and the Java Heap may be blocking the growth of the native heap
# Possible solutions:
# Reduce memory load on the system
# Increase physical memory or swap space
# Check if swap backing store is full
# Decrease Java heap size (-Xmx/-Xms)
# Decrease number of Java threads
# Decrease Java thread stack sizes (-Xss)
# Set larger code cache with -XX:ReservedCodeCacheSize=
# JVM is running with Zero Based Compressed Oops mode in which the Java heap is
# placed in the first 32GB address space. The Java Heap base address is the
# maximum limit for the native heap growth. Please use -XX:HeapBaseMinAddress
# to set the Java Heap base and to place the Java Heap above 32GB virtual address.
# This output file may be truncated or incomplete.
#
# Out of Memory Error (arena.cpp:168), pid=6216, tid=3000
#
# JRE version: OpenJDK Runtime Environment Temurin-21.0.10+7 (21.0.10+7) (build 21.0.10+7-LTS)
# Java VM: OpenJDK 64-Bit Server VM Temurin-21.0.10+7 (21.0.10+7-LTS, mixed mode, sharing, tiered, compressed oops, compressed class ptrs, parallel gc, windows-amd64)
# No core dump will be written. Minidumps are not enabled by default on client versions of Windows
#
--------------- S U M M A R Y ------------
Command Line: --add-modules=ALL-SYSTEM --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/sun.nio.fs=ALL-UNNAMED -Declipse.application=org.eclipse.jdt.ls.core.id1 -Dosgi.bundles.defaultStartLevel=4 -Declipse.product=org.eclipse.jdt.ls.core.product -Djava.import.generatesMetadataFilesAtProjectRoot=false -DDetectVMInstallationsJob.disabled=true -Dfile.encoding=utf8 -XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx4G -Xms100m -Xlog:disable -javaagent:c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\lombok\lombok-1.18.39-4050.jar -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=c:\Users\29470\AppData\Roaming\Cursor\User\workspaceStorage\edbcc896f3fe3d61a54e553c9ad653cc\redhat.java -Daether.dependencyCollector.impl=bf c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\server\plugins\org.eclipse.equinox.launcher_1.7.100.v20251111-0406.jar -configuration c:\Users\29470\AppData\Roaming\Cursor\User\globalStorage\redhat.java\1.53.0\config_win -data c:\Users\29470\AppData\Roaming\Cursor\User\workspaceStorage\edbcc896f3fe3d61a54e553c9ad653cc\redhat.java\jdt_ws --pipe=\\.\pipe\lsp-1253b08bf80e3a8568169725d2662b44-sock
Host: 12th Gen Intel(R) Core(TM) i5-12500H, 16 cores, 15G, Windows 11 , 64 bit Build 22621 (10.0.22621.5305)
Time: Mon Apr 13 10:20:20 2026 elapsed time: 261.333619 seconds (0d 0h 4m 21s)
--------------- T H R E A D ---------------
Current thread (0x0000019a7d7fc600): JavaThread "C2 CompilerThread0" daemon [_thread_in_native, id=3000, stack(0x00000034e2c00000,0x00000034e2d00000) (1024K)]
Current CompileTask:
C2:261333 22166 % 4 org.eclipse.jdt.internal.compiler.lookup.BinaryTypeBinding::createTypeVariables @ 54 (359 bytes)
Stack: [0x00000034e2c00000,0x00000034e2d00000]
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
V [jvm.dll+0x6d7a39]
V [jvm.dll+0x8b4b26]
V [jvm.dll+0x8b70de]
V [jvm.dll+0x8b77c3]
V [jvm.dll+0x284346]
V [jvm.dll+0xc66ed]
V [jvm.dll+0xc6c31]
V [jvm.dll+0x3bec01]
V [jvm.dll+0x38b162]
V [jvm.dll+0x38a5aa]
V [jvm.dll+0x24c560]
V [jvm.dll+0x24bb40]
V [jvm.dll+0x1cb34e]
V [jvm.dll+0x25b85d]
V [jvm.dll+0x259dea]
V [jvm.dll+0x3f857e]
V [jvm.dll+0x85f59d]
V [jvm.dll+0x6d626d]
C [ucrtbase.dll+0x29333]
C [KERNEL32.DLL+0x1259d]
C [ntdll.dll+0x5af78]
--------------- P R O C E S S ---------------
Threads class SMR info:
_java_thread_list=0x0000019a109240d0, length=95, elements={
0x0000019a1fe4b990, 0x0000019a1feea690, 0x0000019a1feebea0, 0x0000019a7d7f5ee0,
0x0000019a7d7f7080, 0x0000019a7d7fad60, 0x0000019a7d7fb7c0, 0x0000019a7d7fc600,
0x0000019a7d85e0b0, 0x0000019a7da64ef0, 0x0000019a7f0c80b0, 0x0000019a7d93a260,
0x0000019a7d86d7e0, 0x0000019a7f4e69e0, 0x0000019a7f4e7050, 0x0000019a044fb010,
0x0000019a040d25d0, 0x0000019a041190e0, 0x0000019a7f812370, 0x0000019a7f814440,
0x0000019a7f813090, 0x0000019a7f813720, 0x0000019a7f814ad0, 0x0000019a7f811ce0,
0x0000019a7f813db0, 0x0000019a7f812a00, 0x0000019a7f4c1a30, 0x0000019a7f4c3470,
0x0000019a7f4c13a0, 0x0000019a7f4c4820, 0x0000019a7f4c3b00, 0x0000019a7f4c20c0,
0x0000019a7f4c4190, 0x0000019a7f4c2750, 0x0000019a7f4c2de0, 0x0000019a047e89d0,
0x0000019a047e3b10, 0x0000019a047e8340, 0x0000019a047e7cb0, 0x0000019a047e6f90,
0x0000019a047e2760, 0x0000019a047e9060, 0x0000019a047e7620, 0x0000019a047e3480,
0x0000019a047e20d0, 0x0000019a047e5550, 0x0000019a047e96f0, 0x0000019a06ee5430,
0x0000019a06ede4a0, 0x0000019a06ee1920, 0x0000019a06ee3360, 0x0000019a06ee4da0,
0x0000019a06ee4710, 0x0000019a06ee0570, 0x0000019a06ee2cd0, 0x0000019a06ee2640,
0x0000019a06ee5ac0, 0x0000019a06edeb30, 0x0000019a06ee1290, 0x0000019a06edf850,
0x0000019a06ee39f0, 0x0000019a06ee1fb0, 0x0000019a06ee4080, 0x0000019a047e6900,
0x0000019a047e5be0, 0x0000019a047e6270, 0x0000019a08056290, 0x0000019a08059da0,
0x0000019a08057cd0, 0x0000019a08052e10, 0x0000019a080541c0, 0x0000019a08056fb0,
0x0000019a0805a430, 0x0000019a08053b30, 0x0000019a08057640, 0x0000019a08058360,
0x0000019a08055c00, 0x0000019a080534a0, 0x0000019a08059080, 0x0000019a08054850,
0x0000019a07c1beb0, 0x0000019a07c190c0, 0x0000019a07c1c540, 0x0000019a07c1d260,
0x0000019a07c19750, 0x0000019a07c17680, 0x0000019a07c16ff0, 0x0000019a07c1df80,
0x0000019a07c1d8f0, 0x0000019a07c17d10, 0x0000019a07c183a0, 0x0000019a07c19de0,
0x0000019a07c1a470, 0x0000019a07c1ab00, 0x0000019a07c1b190
}
Java Threads: ( => current thread )
0x0000019a1fe4b990 JavaThread "main" [_thread_blocked, id=2856, stack(0x00000034e2200000,0x00000034e2300000) (1024K)]
0x0000019a1feea690 JavaThread "Reference Handler" daemon [_thread_blocked, id=12440, stack(0x00000034e2600000,0x00000034e2700000) (1024K)]
0x0000019a1feebea0 JavaThread "Finalizer" daemon [_thread_blocked, id=21288, stack(0x00000034e2700000,0x00000034e2800000) (1024K)]
0x0000019a7d7f5ee0 JavaThread "Signal Dispatcher" daemon [_thread_blocked, id=4252, stack(0x00000034e2800000,0x00000034e2900000) (1024K)]
0x0000019a7d7f7080 JavaThread "Attach Listener" daemon [_thread_blocked, id=12828, stack(0x00000034e2900000,0x00000034e2a00000) (1024K)]
0x0000019a7d7fad60 JavaThread "Service Thread" daemon [_thread_blocked, id=16256, stack(0x00000034e2a00000,0x00000034e2b00000) (1024K)]
0x0000019a7d7fb7c0 JavaThread "Monitor Deflation Thread" daemon [_thread_blocked, id=12696, stack(0x00000034e2b00000,0x00000034e2c00000) (1024K)]
=>0x0000019a7d7fc600 JavaThread "C2 CompilerThread0" daemon [_thread_in_native, id=3000, stack(0x00000034e2c00000,0x00000034e2d00000) (1024K)]
0x0000019a7d85e0b0 JavaThread "C1 CompilerThread0" daemon [_thread_blocked, id=23824, stack(0x00000034e2d00000,0x00000034e2e00000) (1024K)]
0x0000019a7da64ef0 JavaThread "Common-Cleaner" daemon [_thread_blocked, id=4688, stack(0x00000034e3000000,0x00000034e3100000) (1024K)]
0x0000019a7f0c80b0 JavaThread "Notification Thread" daemon [_thread_blocked, id=12040, stack(0x00000034e3300000,0x00000034e3400000) (1024K)]
0x0000019a7d93a260 JavaThread "Active Thread: Equinox Container: 9b3f6009-1310-4abf-b0b4-b87bddef89ab" [_thread_blocked, id=12664, stack(0x00000034e2f00000,0x00000034e3000000) (1024K)]
0x0000019a7d86d7e0 JavaThread "Refresh Thread: Equinox Container: 9b3f6009-1310-4abf-b0b4-b87bddef89ab" daemon [_thread_blocked, id=16156, stack(0x00000034e2e00000,0x00000034e2f00000) (1024K)]
0x0000019a7f4e69e0 JavaThread "Framework Event Dispatcher: Equinox Container: 9b3f6009-1310-4abf-b0b4-b87bddef89ab" daemon [_thread_blocked, id=22704, stack(0x00000034e3900000,0x00000034e3a00000) (1024K)]
0x0000019a7f4e7050 JavaThread "Start Level: Equinox Container: 9b3f6009-1310-4abf-b0b4-b87bddef89ab" daemon [_thread_blocked, id=6468, stack(0x00000034e3a00000,0x00000034e3b00000) (1024K)]
0x0000019a044fb010 JavaThread "Bundle File Closer" daemon [_thread_blocked, id=18492, stack(0x00000034e3e00000,0x00000034e3f00000) (1024K)]
0x0000019a040d25d0 JavaThread "SCR Component Actor" daemon [_thread_blocked, id=16032, stack(0x00000034e4000000,0x00000034e4100000) (1024K)]
0x0000019a041190e0 JavaThread "Worker-JM" [_thread_blocked, id=20112, stack(0x00000034e4400000,0x00000034e4500000) (1024K)]
0x0000019a7f812370 JavaThread "JNA Cleaner" daemon [_thread_blocked, id=6856, stack(0x00000034e4200000,0x00000034e4300000) (1024K)]
0x0000019a7f814440 JavaThread "Worker-0" [_thread_blocked, id=17500, stack(0x00000034e4500000,0x00000034e4600000) (1024K)]
0x0000019a7f813090 JavaThread "Worker-1" [_thread_blocked, id=2036, stack(0x00000034e4600000,0x00000034e4700000) (1024K)]
0x0000019a7f813720 JavaThread "Worker-2: Building" [_thread_in_native, id=18696, stack(0x00000034e4700000,0x00000034e4800000) (1024K)]
0x0000019a7f814ad0 JavaThread "Java indexing" daemon [_thread_blocked, id=1064, stack(0x00000034e4a00000,0x00000034e4b00000) (1024K)]
0x0000019a7f811ce0 JavaThread "Worker-3" [_thread_blocked, id=10724, stack(0x00000034e4b00000,0x00000034e4c00000) (1024K)]
0x0000019a7f813db0 JavaThread "Worker-4" [_thread_blocked, id=3328, stack(0x00000034e1f00000,0x00000034e2000000) (1024K)]
0x0000019a7f812a00 JavaThread "Worker-5" [_thread_blocked, id=15472, stack(0x00000034e2100000,0x00000034e2200000) (1024K)]
0x0000019a7f4c1a30 JavaThread "Thread-1" daemon [_thread_in_native, id=22648, stack(0x00000034e4c00000,0x00000034e4d00000) (1024K)]
0x0000019a7f4c3470 JavaThread "Thread-2" daemon [_thread_in_native, id=14300, stack(0x00000034e4d00000,0x00000034e4e00000) (1024K)]
0x0000019a7f4c13a0 JavaThread "Thread-3" daemon [_thread_in_native, id=23412, stack(0x00000034e4e00000,0x00000034e4f00000) (1024K)]
0x0000019a7f4c4820 JavaThread "Thread-4" daemon [_thread_in_native, id=7704, stack(0x00000034e4f00000,0x00000034e5000000) (1024K)]
0x0000019a7f4c3b00 JavaThread "Thread-5" daemon [_thread_in_native, id=18232, stack(0x00000034e5000000,0x00000034e5100000) (1024K)]
0x0000019a7f4c20c0 JavaThread "Thread-6" daemon [_thread_in_native, id=21328, stack(0x00000034e5100000,0x00000034e5200000) (1024K)]
0x0000019a7f4c4190 JavaThread "Thread-7" daemon [_thread_in_native, id=17480, stack(0x00000034e5200000,0x00000034e5300000) (1024K)]
0x0000019a7f4c2750 JavaThread "Thread-8" daemon [_thread_in_native, id=23408, stack(0x00000034e5300000,0x00000034e5400000) (1024K)]
0x0000019a7f4c2de0 JavaThread "Thread-9" daemon [_thread_in_native, id=16284, stack(0x00000034e5400000,0x00000034e5500000) (1024K)]
0x0000019a047e89d0 JavaThread "Thread-10" daemon [_thread_in_native, id=21200, stack(0x00000034e5500000,0x00000034e5600000) (1024K)]
0x0000019a047e3b10 JavaThread "Thread-11" daemon [_thread_in_native, id=17556, stack(0x00000034e5600000,0x00000034e5700000) (1024K)]
0x0000019a047e8340 JavaThread "Thread-12" daemon [_thread_in_native, id=7336, stack(0x00000034e5700000,0x00000034e5800000) (1024K)]
0x0000019a047e7cb0 JavaThread "Thread-13" daemon [_thread_in_native, id=8660, stack(0x00000034e5800000,0x00000034e5900000) (1024K)]
0x0000019a047e6f90 JavaThread "Thread-14" daemon [_thread_in_native, id=18668, stack(0x00000034e5900000,0x00000034e5a00000) (1024K)]
0x0000019a047e2760 JavaThread "Thread-15" daemon [_thread_in_native, id=13816, stack(0x00000034e5a00000,0x00000034e5b00000) (1024K)]
0x0000019a047e9060 JavaThread "Thread-16" daemon [_thread_in_native, id=19888, stack(0x00000034e5b00000,0x00000034e5c00000) (1024K)]
0x0000019a047e7620 JavaThread "Thread-17" daemon [_thread_in_native, id=1196, stack(0x00000034e5c00000,0x00000034e5d00000) (1024K)]
0x0000019a047e3480 JavaThread "pool-2-thread-1" [_thread_blocked, id=6284, stack(0x00000034e5d00000,0x00000034e5e00000) (1024K)]
0x0000019a047e20d0 JavaThread "Worker-6" [_thread_blocked, id=18532, stack(0x00000034e6000000,0x00000034e6100000) (1024K)]
0x0000019a047e5550 JavaThread "WorkspaceEventsHandler" [_thread_blocked, id=16280, stack(0x00000034e6100000,0x00000034e6200000) (1024K)]
0x0000019a047e96f0 JavaThread "pool-1-thread-1" [_thread_blocked, id=1828, stack(0x00000034e6200000,0x00000034e6300000) (1024K)]
0x0000019a06ee5430 JavaThread "ForkJoinPool.commonPool-worker-1" daemon [_thread_blocked, id=3264, stack(0x00000034e6600000,0x00000034e6700000) (1024K)]
0x0000019a06ede4a0 JavaThread "ForkJoinPool.commonPool-worker-2" daemon [_thread_blocked, id=5416, stack(0x00000034e6700000,0x00000034e6800000) (1024K)]
0x0000019a06ee1920 JavaThread "LocalFile Deleter" daemon [_thread_blocked, id=24684, stack(0x00000034e2000000,0x00000034e2100000) (1024K)]
0x0000019a06ee3360 JavaThread "LocalFile Deleter" daemon [_thread_blocked, id=20560, stack(0x00000034e4900000,0x00000034e4a00000) (1024K)]
0x0000019a06ee4da0 JavaThread "LocalFile Deleter" daemon [_thread_blocked, id=3520, stack(0x00000034e5e00000,0x00000034e5f00000) (1024K)]
0x0000019a06ee4710 JavaThread "LocalFile Deleter" daemon [_thread_blocked, id=16808, stack(0x00000034e5f00000,0x00000034e6000000) (1024K)]
0x0000019a06ee0570 JavaThread "LocalFile Deleter" daemon [_thread_blocked, id=21908, stack(0x00000034e6300000,0x00000034e6400000) (1024K)]
0x0000019a06ee2cd0 JavaThread "LocalFile Deleter" daemon [_thread_blocked, id=14788, stack(0x00000034e6500000,0x00000034e6600000) (1024K)]
0x0000019a06ee2640 JavaThread "LocalFile Deleter" daemon [_thread_blocked, id=9396, stack(0x00000034e6800000,0x00000034e6900000) (1024K)]
0x0000019a06ee5ac0 JavaThread "LocalFile Deleter" daemon [_thread_blocked, id=19404, stack(0x00000034e6900000,0x00000034e6a00000) (1024K)]
0x0000019a06edeb30 JavaThread "LocalFile Deleter" daemon [_thread_blocked, id=20952, stack(0x00000034e6a00000,0x00000034e6b00000) (1024K)]
0x0000019a06ee1290 JavaThread "LocalFile Deleter" daemon [_thread_blocked, id=20928, stack(0x00000034e6b00000,0x00000034e6c00000) (1024K)]
0x0000019a06edf850 JavaThread "LocalFile Deleter" daemon [_thread_blocked, id=22360, stack(0x00000034e6c00000,0x00000034e6d00000) (1024K)]
0x0000019a06ee39f0 JavaThread "LocalFile Deleter" daemon [_thread_blocked, id=8892, stack(0x00000034e6d00000,0x00000034e6e00000) (1024K)]
0x0000019a06ee1fb0 JavaThread "Compiler Source File Reader" daemon [_thread_blocked, id=10996, stack(0x00000034e6e00000,0x00000034e6f00000) (1024K)]
0x0000019a06ee4080 JavaThread "Compiler Source File Reader" daemon [_thread_blocked, id=20440, stack(0x00000034e6f00000,0x00000034e7000000) (1024K)]
0x0000019a047e6900 JavaThread "Compiler Source File Reader" daemon [_thread_blocked, id=13404, stack(0x00000034e7000000,0x00000034e7100000) (1024K)]
0x0000019a047e5be0 JavaThread "Compiler Source File Reader" daemon [_thread_blocked, id=21668, stack(0x00000034e7100000,0x00000034e7200000) (1024K)]
0x0000019a047e6270 JavaThread "Compiler Source File Reader" daemon [_thread_blocked, id=14940, stack(0x00000034e7200000,0x00000034e7300000) (1024K)]
0x0000019a08056290 JavaThread "Compiler Processing Task" daemon [_thread_blocked, id=20104, stack(0x00000034e7400000,0x00000034e7500000) (1024K)]
0x0000019a08059da0 JavaThread "Compiler Class File Writer" daemon [_thread_blocked, id=11484, stack(0x00000034e7500000,0x00000034e7600000) (1024K)]
0x0000019a08057cd0 JavaThread "Compiler Class File Writer" daemon [_thread_blocked, id=3676, stack(0x00000034e7600000,0x00000034e7700000) (1024K)]
0x0000019a08052e10 JavaThread "Compiler Class File Writer" daemon [_thread_blocked, id=25556, stack(0x00000034e7700000,0x00000034e7800000) (1024K)]
0x0000019a080541c0 JavaThread "Compiler Class File Writer" daemon [_thread_blocked, id=3040, stack(0x00000034e7800000,0x00000034e7900000) (1024K)]
0x0000019a08056fb0 JavaThread "Compiler Class File Writer" daemon [_thread_blocked, id=24024, stack(0x00000034e7900000,0x00000034e7a00000) (1024K)]
0x0000019a0805a430 JavaThread "Compiler Class File Writer" daemon [_thread_blocked, id=6264, stack(0x00000034e7a00000,0x00000034e7b00000) (1024K)]
0x0000019a08053b30 JavaThread "ForkJoinPool.commonPool-worker-4" daemon [_thread_blocked, id=22252, stack(0x00000034e7c00000,0x00000034e7d00000) (1024K)]
0x0000019a08057640 JavaThread "LocalFile Deleter" daemon [_thread_blocked, id=14780, stack(0x00000034e7e00000,0x00000034e7f00000) (1024K)]
0x0000019a08058360 JavaThread "LocalFile Deleter" daemon [_thread_blocked, id=740, stack(0x00000034e7f00000,0x00000034e8000000) (1024K)]
0x0000019a08055c00 JavaThread "LocalFile Deleter" daemon [_thread_blocked, id=8824, stack(0x00000034e8000000,0x00000034e8100000) (1024K)]
0x0000019a080534a0 JavaThread "LocalFile Deleter" daemon [_thread_blocked, id=5524, stack(0x00000034e8100000,0x00000034e8200000) (1024K)]
0x0000019a08059080 JavaThread "Compiler Source File Reader" daemon [_thread_blocked, id=24500, stack(0x00000034e7d00000,0x00000034e7e00000) (1024K)]
0x0000019a08054850 JavaThread "Compiler Source File Reader" daemon [_thread_blocked, id=8248, stack(0x00000034e8300000,0x00000034e8400000) (1024K)]
0x0000019a07c1beb0 JavaThread "Compiler Source File Reader" daemon [_thread_blocked, id=14500, stack(0x00000034e8400000,0x00000034e8500000) (1024K)]
0x0000019a07c190c0 JavaThread "Compiler Source File Reader" daemon [_thread_blocked, id=21784, stack(0x00000034e8500000,0x00000034e8600000) (1024K)]
0x0000019a07c1c540 JavaThread "Compiler Source File Reader" daemon [_thread_blocked, id=23928, stack(0x00000034e8600000,0x00000034e8700000) (1024K)]
0x0000019a07c1d260 JavaThread "Compiler Source File Reader" daemon [_thread_blocked, id=996, stack(0x00000034e8700000,0x00000034e8800000) (1024K)]
0x0000019a07c19750 JavaThread "Compiler Source File Reader" daemon [_thread_blocked, id=948, stack(0x00000034e8800000,0x00000034e8900000) (1024K)]
0x0000019a07c17680 JavaThread "Compiler Source File Reader" daemon [_thread_blocked, id=23488, stack(0x00000034e8900000,0x00000034e8a00000) (1024K)]
0x0000019a07c16ff0 JavaThread "Compiler Source File Reader" daemon [_thread_blocked, id=4244, stack(0x00000034e8a00000,0x00000034e8b00000) (1024K)]
0x0000019a07c1df80 JavaThread "Compiler Class File Writer" daemon [_thread_blocked, id=17416, stack(0x00000034e8b00000,0x00000034e8c00000) (1024K)]
0x0000019a07c1d8f0 JavaThread "Compiler Class File Writer" daemon [_thread_blocked, id=23884, stack(0x00000034e8c00000,0x00000034e8d00000) (1024K)]
0x0000019a07c17d10 JavaThread "Compiler Class File Writer" daemon [_thread_blocked, id=24612, stack(0x00000034e8200000,0x00000034e8300000) (1024K)]
0x0000019a07c183a0 JavaThread "Compiler Class File Writer" daemon [_thread_blocked, id=21516, stack(0x00000034e8d00000,0x00000034e8e00000) (1024K)]
0x0000019a07c19de0 JavaThread "Compiler Class File Writer" daemon [_thread_blocked, id=25088, stack(0x00000034e8e00000,0x00000034e8f00000) (1024K)]
0x0000019a07c1a470 JavaThread "Compiler Class File Writer" daemon [_thread_blocked, id=17688, stack(0x00000034e8f00000,0x00000034e9000000) (1024K)]
0x0000019a07c1ab00 JavaThread "Compiler Class File Writer" daemon [_thread_blocked, id=16752, stack(0x00000034e9000000,0x00000034e9100000) (1024K)]
0x0000019a07c1b190 JavaThread "Compiler Class File Writer" daemon [_thread_blocked, id=3588, stack(0x00000034e9100000,0x00000034e9200000) (1024K)]
Total: 95
Other Threads:
0x0000019a7d7f4af0 VMThread "VM Thread" [id=24396, stack(0x00000034e2500000,0x00000034e2600000) (1024K)]
0x0000019a1feb55e0 WatcherThread "VM Periodic Task Thread" [id=19428, stack(0x00000034e2400000,0x00000034e2500000) (1024K)]
0x0000019a1fe69a70 WorkerThread "GC Thread#0" [id=22568, stack(0x00000034e2300000,0x00000034e2400000) (1024K)]
0x0000019a7da64b20 WorkerThread "GC Thread#1" [id=2212, stack(0x00000034e3100000,0x00000034e3200000) (1024K)]
0x0000019a7d8e9d60 WorkerThread "GC Thread#2" [id=15052, stack(0x00000034e3200000,0x00000034e3300000) (1024K)]
0x0000019a7f2229b0 WorkerThread "GC Thread#3" [id=16528, stack(0x00000034e3400000,0x00000034e3500000) (1024K)]
0x0000019a7f222d60 WorkerThread "GC Thread#4" [id=19140, stack(0x00000034e3500000,0x00000034e3600000) (1024K)]
0x0000019a7f223110 WorkerThread "GC Thread#5" [id=25096, stack(0x00000034e3600000,0x00000034e3700000) (1024K)]
0x0000019a7f2234c0 WorkerThread "GC Thread#6" [id=1300, stack(0x00000034e3700000,0x00000034e3800000) (1024K)]
0x0000019a7f5902d0 WorkerThread "GC Thread#7" [id=6564, stack(0x00000034e3800000,0x00000034e3900000) (1024K)]
0x0000019a0437c010 WorkerThread "GC Thread#8" [id=22424, stack(0x00000034e3b00000,0x00000034e3c00000) (1024K)]
0x0000019a7db483f0 WorkerThread "GC Thread#9" [id=12336, stack(0x00000034e3c00000,0x00000034e3d00000) (1024K)]
0x0000019a7db487a0 WorkerThread "GC Thread#10" [id=13912, stack(0x00000034e3d00000,0x00000034e3e00000) (1024K)]
0x0000019a04418d30 WorkerThread "GC Thread#11" [id=3304, stack(0x00000034e4100000,0x00000034e4200000) (1024K)]
0x0000019a7f1c4910 WorkerThread "GC Thread#12" [id=8964, stack(0x00000034e4800000,0x00000034e4900000) (1024K)]
Total: 15
Threads with active compile tasks:
C2 CompilerThread0 261411 22166 % 4 org.eclipse.jdt.internal.compiler.lookup.BinaryTypeBinding::createTypeVariables @ 54 (359 bytes)
Total: 1
VM state: not at safepoint (normal execution)
VM Mutex/Monitor currently owned by a thread: None
Heap address: 0x0000000700000000, size: 4096 MB, Compressed Oops mode: Zero based, Oop shift amount: 3
CDS archive(s) mapped at: [0x0000019a3c000000-0x0000019a3cba0000-0x0000019a3cba0000), size 12189696, SharedBaseAddress: 0x0000019a3c000000, ArchiveRelocationMode: 1.
Compressed class space mapped at: 0x0000019a3d000000-0x0000019a7d000000, reserved size: 1073741824
Narrow klass base: 0x0000019a3c000000, Narrow klass shift: 0, Narrow klass range: 0x100000000
GC Precious Log:
CardTable entry size: 512
CPUs: 16 total, 16 available
Memory: 16110M
Large Page Support: Disabled
NUMA Support: Disabled
Compressed Oops: Enabled (Zero based)
Alignments: Space 512K, Generation 512K, Heap 2M
Heap Min Capacity: 100M
Heap Initial Capacity: 100M
Heap Max Capacity: 4G
Pre-touch: Disabled
Parallel Workers: 13
Heap:
PSYoungGen total 3072K, used 2690K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 85% used [0x00000007aab00000,0x00000007aad20ba0,0x00000007aad80000)
from space 512K, 100% used [0x00000007aae00000,0x00000007aae80000,0x00000007aae80000)
to space 1024K, 0% used [0x00000007aae80000,0x00000007aae80000,0x00000007aaf80000)
ParOldGen total 222208K, used 219313K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 98% used [0x0000000700000000,0x000000070d62c448,0x000000070d900000)
Metaspace used 77280K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
Card table byte_map: [0x0000019a32040000,0x0000019a32850000] _byte_map_base: 0x0000019a2e840000
Marking Bits: (ParMarkBitMap*) 0x00007fff167c3450
Begin Bits: [0x0000019a32850000, 0x0000019a36850000)
End Bits: [0x0000019a36850000, 0x0000019a3a850000)
Polling page: 0x0000019a1f600000
Metaspace:
Usage:
Non-class: 67.94 MB used.
Class: 7.53 MB used.
Both: 75.47 MB used.
Virtual space:
Non-class space: 128.00 MB reserved, 68.81 MB ( 54%) committed, 2 nodes.
Class space: 1.00 GB reserved, 8.25 MB ( <1%) committed, 1 nodes.
Both: 1.12 GB reserved, 77.06 MB ( 7%) committed.
Chunk freelists:
Non-Class: 10.41 MB
Class: 7.60 MB
Both: 18.01 MB
MaxMetaspaceSize: unlimited
CompressedClassSpaceSize: 1.00 GB
Initial GC threshold: 21.00 MB
Current GC threshold: 128.38 MB
CDS: on
- commit_granule_bytes: 65536.
- commit_granule_words: 8192.
- virtual_space_node_default_size: 8388608.
- enlarge_chunks_in_place: 1.
- use_allocation_guard: 0.
Internal statistics:
num_allocs_failed_limit: 9.
num_arena_births: 1438.
num_arena_deaths: 34.
num_vsnodes_births: 3.
num_vsnodes_deaths: 0.
num_space_committed: 1233.
num_space_uncommitted: 0.
num_chunks_returned_to_freelist: 53.
num_chunks_taken_from_freelist: 5030.
num_chunk_merges: 17.
num_chunk_splits: 3024.
num_chunks_enlarged: 1650.
num_inconsistent_stats: 0.
CodeHeap 'non-profiled nmethods': size=119168Kb used=19623Kb max_used=28128Kb free=99544Kb
bounds [0x0000019a2abe0000, 0x0000019a2c760000, 0x0000019a32040000]
CodeHeap 'profiled nmethods': size=119104Kb used=31795Kb max_used=53987Kb free=87308Kb
bounds [0x0000019a23040000, 0x0000019a26510000, 0x0000019a2a490000]
CodeHeap 'non-nmethods': size=7488Kb used=1476Kb max_used=3187Kb free=6011Kb
bounds [0x0000019a2a490000, 0x0000019a2a880000, 0x0000019a2abe0000]
CodeCache: size=245760Kb, used=52894Kb, max_used=85302Kb, free=192863Kb
total_blobs=16091, nmethods=15298, adapters=695, full_count=0
Compilation: enabled, stopped_count=0, restarted_count=0
Compilation events (20 events):
Event: 260.618 Thread 0x0000019a7d85e0b0 nmethod 22156 0x0000019a231cd590 code [0x0000019a231cd760, 0x0000019a231cda90]
Event: 260.854 Thread 0x0000019a7d85e0b0 22157 3 org.eclipse.core.internal.dtree.DataTreeNode::compareWith (74 bytes)
Event: 260.854 Thread 0x0000019a7d85e0b0 nmethod 22157 0x0000019a23062790 code [0x0000019a23062980, 0x0000019a23062ec0]
Event: 260.857 Thread 0x0000019a7d7fc600 22158 4 org.eclipse.jdt.internal.core.PackageFragmentRoot::resource (17 bytes)
Event: 260.860 Thread 0x0000019a7d7fc600 nmethod 22158 0x0000019a2abea490 code [0x0000019a2abea640, 0x0000019a2abea840]
Event: 260.860 Thread 0x0000019a7d7fc600 22159 4 org.eclipse.jdt.internal.core.JarPackageFragmentRoot::equals (55 bytes)
Event: 260.864 Thread 0x0000019a7d7fc600 nmethod 22159 0x0000019a2ac3ed10 code [0x0000019a2ac3ef00, 0x0000019a2ac3f4a0]
Event: 261.241 Thread 0x0000019a7d7fc600 22160 4 org.eclipse.jdt.internal.core.builder.ClasspathMultiReleaseJar::initializeVersions (99 bytes)
Event: 261.246 Thread 0x0000019a7d85e0b0 22161 3 org.eclipse.jdt.internal.compiler.lookup.SplitPackageBinding::findPackage (231 bytes)
Event: 261.247 Thread 0x0000019a7d85e0b0 nmethod 22161 0x0000019a23c3c210 code [0x0000019a23c3c5c0, 0x0000019a23c3e138]
Event: 261.257 Thread 0x0000019a7d7fc600 nmethod 22160 0x0000019a2bd23890 code [0x0000019a2bd23b20, 0x0000019a2bd24e38]
Event: 261.257 Thread 0x0000019a7d7fc600 22162 4 org.eclipse.jdt.internal.compiler.lookup.MethodScope::checkAndSetModifiersForMethod (713 bytes)
Event: 261.261 Thread 0x0000019a7d7fc600 nmethod 22162 0x0000019a2b390410 code [0x0000019a2b390620, 0x0000019a2b390d28]
Event: 261.278 Thread 0x0000019a7d7fc600 22163 4 org.eclipse.jdt.internal.compiler.util.JRTUtil::getModulesDeclaringPackage (15 bytes)
Event: 261.279 Thread 0x0000019a7d7fc600 nmethod 22163 0x0000019a2abee390 code [0x0000019a2abee520, 0x0000019a2abee5d0]
Event: 261.282 Thread 0x0000019a7d7fc600 22164 4 org.eclipse.jdt.internal.compiler.classfmt.MethodInfo::decodeMethodAnnotations (177 bytes)
Event: 261.283 Thread 0x0000019a7d85e0b0 22165 3 org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding::computeId (3060 bytes)
Event: 261.287 Thread 0x0000019a7d85e0b0 nmethod 22165 0x0000019a24609690 code [0x0000019a2460a740, 0x0000019a24612238]
Event: 261.289 Thread 0x0000019a7d7fc600 nmethod 22164 0x0000019a2b545610 code [0x0000019a2b5457e0, 0x0000019a2b546000]
Event: 261.304 Thread 0x0000019a7d7fc600 22166 % 4 org.eclipse.jdt.internal.compiler.lookup.BinaryTypeBinding::createTypeVariables @ 54 (359 bytes)
GC Heap History (20 events):
Event: 260.662 GC heap before
{Heap before GC invocations=3043 (full 5):
PSYoungGen total 3072K, used 3042K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 98% used [0x00000007aab00000,0x00000007aad79238,0x00000007aad80000)
from space 512K, 99% used [0x00000007aae00000,0x00000007aae7f790,0x00000007aae80000)
to space 1024K, 0% used [0x00000007aae80000,0x00000007aae80000,0x00000007aaf80000)
ParOldGen total 222208K, used 198723K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 89% used [0x0000000700000000,0x000000070c210e00,0x000000070d900000)
Metaspace used 77278K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
}
Event: 260.662 GC heap after
{Heap after GC invocations=3043 (full 5):
PSYoungGen total 3584K, used 1018K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 0% used [0x00000007aab00000,0x00000007aab00000,0x00000007aad80000)
from space 1024K, 99% used [0x00000007aae80000,0x00000007aaf7e928,0x00000007aaf80000)
to space 512K, 0% used [0x00000007aae00000,0x00000007aae00000,0x00000007aae80000)
ParOldGen total 222208K, used 200694K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 90% used [0x0000000700000000,0x000000070c3fd8b0,0x000000070d900000)
Metaspace used 77278K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
}
Event: 260.690 GC heap before
{Heap before GC invocations=3044 (full 5):
PSYoungGen total 3584K, used 3578K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 100% used [0x00000007aab00000,0x00000007aad80000,0x00000007aad80000)
from space 1024K, 99% used [0x00000007aae80000,0x00000007aaf7e928,0x00000007aaf80000)
to space 512K, 0% used [0x00000007aae00000,0x00000007aae00000,0x00000007aae80000)
ParOldGen total 222208K, used 203162K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 91% used [0x0000000700000000,0x000000070c6668c8,0x000000070d900000)
Metaspace used 77278K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
}
Event: 260.692 GC heap after
{Heap after GC invocations=3044 (full 5):
PSYoungGen total 3072K, used 509K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 0% used [0x00000007aab00000,0x00000007aab00000,0x00000007aad80000)
from space 512K, 99% used [0x00000007aae00000,0x00000007aae7f500,0x00000007aae80000)
to space 1024K, 0% used [0x00000007aae80000,0x00000007aae80000,0x00000007aaf80000)
ParOldGen total 222208K, used 206041K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 92% used [0x0000000700000000,0x000000070c9365a0,0x000000070d900000)
Metaspace used 77278K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
}
Event: 260.702 GC heap before
{Heap before GC invocations=3045 (full 5):
PSYoungGen total 3072K, used 3046K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 99% used [0x00000007aab00000,0x00000007aad7a5b8,0x00000007aad80000)
from space 512K, 99% used [0x00000007aae00000,0x00000007aae7f500,0x00000007aae80000)
to space 1024K, 0% used [0x00000007aae80000,0x00000007aae80000,0x00000007aaf80000)
ParOldGen total 222208K, used 206041K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 92% used [0x0000000700000000,0x000000070c9365a0,0x000000070d900000)
Metaspace used 77278K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
}
Event: 260.705 GC heap after
{Heap after GC invocations=3045 (full 5):
PSYoungGen total 3584K, used 1020K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 0% used [0x00000007aab00000,0x00000007aab00000,0x00000007aad80000)
from space 1024K, 99% used [0x00000007aae80000,0x00000007aaf7f118,0x00000007aaf80000)
to space 512K, 0% used [0x00000007aae00000,0x00000007aae00000,0x00000007aae80000)
ParOldGen total 222208K, used 208039K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 93% used [0x0000000700000000,0x000000070cb29ec8,0x000000070d900000)
Metaspace used 77278K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
}
Event: 260.732 GC heap before
{Heap before GC invocations=3046 (full 5):
PSYoungGen total 3584K, used 3580K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 100% used [0x00000007aab00000,0x00000007aad80000,0x00000007aad80000)
from space 1024K, 99% used [0x00000007aae80000,0x00000007aaf7f118,0x00000007aaf80000)
to space 512K, 0% used [0x00000007aae00000,0x00000007aae00000,0x00000007aae80000)
ParOldGen total 222208K, used 208039K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 93% used [0x0000000700000000,0x000000070cb29ec8,0x000000070d900000)
Metaspace used 77278K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
}
Event: 260.733 GC heap after
{Heap after GC invocations=3046 (full 5):
PSYoungGen total 3072K, used 489K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 0% used [0x00000007aab00000,0x00000007aab00000,0x00000007aad80000)
from space 512K, 95% used [0x00000007aae00000,0x00000007aae7a628,0x00000007aae80000)
to space 1024K, 0% used [0x00000007aae80000,0x00000007aae80000,0x00000007aaf80000)
ParOldGen total 222208K, used 210966K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 94% used [0x0000000700000000,0x000000070ce05a18,0x000000070d900000)
Metaspace used 77278K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
}
Event: 260.755 GC heap before
{Heap before GC invocations=3047 (full 5):
PSYoungGen total 3072K, used 2981K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 97% used [0x00000007aab00000,0x00000007aad6ef48,0x00000007aad80000)
from space 512K, 95% used [0x00000007aae00000,0x00000007aae7a628,0x00000007aae80000)
to space 1024K, 0% used [0x00000007aae80000,0x00000007aae80000,0x00000007aaf80000)
ParOldGen total 222208K, used 210966K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 94% used [0x0000000700000000,0x000000070ce05a18,0x000000070d900000)
Metaspace used 77278K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
}
Event: 260.757 GC heap after
{Heap after GC invocations=3047 (full 5):
PSYoungGen total 3584K, used 1017K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 0% used [0x00000007aab00000,0x00000007aab00000,0x00000007aad80000)
from space 1024K, 99% used [0x00000007aae80000,0x00000007aaf7e688,0x00000007aaf80000)
to space 512K, 0% used [0x00000007aae00000,0x00000007aae00000,0x00000007aae80000)
ParOldGen total 222208K, used 212784K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 95% used [0x0000000700000000,0x000000070cfcc008,0x000000070d900000)
Metaspace used 77278K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
}
Event: 260.771 GC heap before
{Heap before GC invocations=3048 (full 5):
PSYoungGen total 3584K, used 3570K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 99% used [0x00000007aab00000,0x00000007aad7e570,0x00000007aad80000)
from space 1024K, 99% used [0x00000007aae80000,0x00000007aaf7e688,0x00000007aaf80000)
to space 512K, 0% used [0x00000007aae00000,0x00000007aae00000,0x00000007aae80000)
ParOldGen total 222208K, used 212784K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 95% used [0x0000000700000000,0x000000070cfcc008,0x000000070d900000)
Metaspace used 77278K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
}
Event: 260.772 GC heap after
{Heap after GC invocations=3048 (full 5):
PSYoungGen total 3072K, used 499K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 0% used [0x00000007aab00000,0x00000007aab00000,0x00000007aad80000)
from space 512K, 97% used [0x00000007aae00000,0x00000007aae7cd58,0x00000007aae80000)
to space 1024K, 0% used [0x00000007aae80000,0x00000007aae80000,0x00000007aaf80000)
ParOldGen total 222208K, used 215706K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 97% used [0x0000000700000000,0x000000070d2a69a0,0x000000070d900000)
Metaspace used 77278K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
}
Event: 261.200 GC heap before
{Heap before GC invocations=3049 (full 5):
PSYoungGen total 3072K, used 3050K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 99% used [0x00000007aab00000,0x00000007aad7dc48,0x00000007aad80000)
from space 512K, 97% used [0x00000007aae00000,0x00000007aae7cd58,0x00000007aae80000)
to space 1024K, 0% used [0x00000007aae80000,0x00000007aae80000,0x00000007aaf80000)
ParOldGen total 222208K, used 215706K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 97% used [0x0000000700000000,0x000000070d2a69a0,0x000000070d900000)
Metaspace used 77278K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
}
Event: 261.201 GC heap after
{Heap after GC invocations=3049 (full 5):
PSYoungGen total 3584K, used 1013K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 0% used [0x00000007aab00000,0x00000007aab00000,0x00000007aad80000)
from space 1024K, 98% used [0x00000007aae80000,0x00000007aaf7d558,0x00000007aaf80000)
to space 512K, 0% used [0x00000007aae00000,0x00000007aae00000,0x00000007aae80000)
ParOldGen total 222208K, used 217151K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 97% used [0x0000000700000000,0x000000070d40fc18,0x000000070d900000)
Metaspace used 77278K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
}
Event: 261.249 GC heap before
{Heap before GC invocations=3050 (full 5):
PSYoungGen total 3584K, used 3573K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 100% used [0x00000007aab00000,0x00000007aad80000,0x00000007aad80000)
from space 1024K, 98% used [0x00000007aae80000,0x00000007aaf7d558,0x00000007aaf80000)
to space 512K, 0% used [0x00000007aae00000,0x00000007aae00000,0x00000007aae80000)
ParOldGen total 222208K, used 217151K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 97% used [0x0000000700000000,0x000000070d40fc18,0x000000070d900000)
Metaspace used 77280K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
}
Event: 261.249 GC heap after
{Heap after GC invocations=3050 (full 5):
PSYoungGen total 3072K, used 501K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 0% used [0x00000007aab00000,0x00000007aab00000,0x00000007aad80000)
from space 512K, 98% used [0x00000007aae00000,0x00000007aae7d7e8,0x00000007aae80000)
to space 1024K, 0% used [0x00000007aae80000,0x00000007aae80000,0x00000007aaf80000)
ParOldGen total 222208K, used 218448K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 98% used [0x0000000700000000,0x000000070d554098,0x000000070d900000)
Metaspace used 77280K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
}
Event: 261.282 GC heap before
{Heap before GC invocations=3051 (full 5):
PSYoungGen total 3072K, used 3061K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 100% used [0x00000007aab00000,0x00000007aad80000,0x00000007aad80000)
from space 512K, 98% used [0x00000007aae00000,0x00000007aae7d7e8,0x00000007aae80000)
to space 1024K, 0% used [0x00000007aae80000,0x00000007aae80000,0x00000007aaf80000)
ParOldGen total 222208K, used 218448K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 98% used [0x0000000700000000,0x000000070d554098,0x000000070d900000)
Metaspace used 77280K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
}
Event: 261.283 GC heap after
{Heap after GC invocations=3051 (full 5):
PSYoungGen total 3584K, used 544K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 0% used [0x00000007aab00000,0x00000007aab00000,0x00000007aad80000)
from space 1024K, 53% used [0x00000007aae80000,0x00000007aaf08000,0x00000007aaf80000)
to space 512K, 0% used [0x00000007aae00000,0x00000007aae00000,0x00000007aae80000)
ParOldGen total 222208K, used 218953K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 98% used [0x0000000700000000,0x000000070d5d2448,0x000000070d900000)
Metaspace used 77280K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
}
Event: 261.306 GC heap before
{Heap before GC invocations=3052 (full 5):
PSYoungGen total 3584K, used 3104K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 100% used [0x00000007aab00000,0x00000007aad80000,0x00000007aad80000)
from space 1024K, 53% used [0x00000007aae80000,0x00000007aaf08000,0x00000007aaf80000)
to space 512K, 0% used [0x00000007aae00000,0x00000007aae00000,0x00000007aae80000)
ParOldGen total 222208K, used 218953K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 98% used [0x0000000700000000,0x000000070d5d2448,0x000000070d900000)
Metaspace used 77280K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
}
Event: 261.306 GC heap after
{Heap after GC invocations=3052 (full 5):
PSYoungGen total 3072K, used 512K [0x00000007aab00000, 0x00000007aaf80000, 0x0000000800000000)
eden space 2560K, 0% used [0x00000007aab00000,0x00000007aab00000,0x00000007aad80000)
from space 512K, 100% used [0x00000007aae00000,0x00000007aae80000,0x00000007aae80000)
to space 1024K, 0% used [0x00000007aae80000,0x00000007aae80000,0x00000007aaf80000)
ParOldGen total 222208K, used 219313K [0x0000000700000000, 0x000000070d900000, 0x00000007aab00000)
object space 222208K, 98% used [0x0000000700000000,0x000000070d62c448,0x000000070d900000)
Metaspace used 77280K, committed 78912K, reserved 1179648K
class space used 7707K, committed 8448K, reserved 1048576K
}
Dll operation events (12 events):
Event: 0.046 Loaded shared library c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\java.dll
Event: 0.184 Loaded shared library c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\zip.dll
Event: 0.258 Loaded shared library C:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\instrument.dll
Event: 0.263 Loaded shared library C:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\net.dll
Event: 0.267 Loaded shared library C:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\nio.dll
Event: 0.270 Loaded shared library C:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\zip.dll
Event: 0.287 Loaded shared library C:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\jimage.dll
Event: 0.366 Loaded shared library c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\verify.dll
Event: 30.026 Loaded shared library C:\Users\29470\AppData\Roaming\Cursor\User\globalStorage\redhat.java\1.53.0\config_win\org.eclipse.equinox.launcher\org.eclipse.equinox.launcher.win32.win32.x86_64_1.3.0.v20260203-2149\eclipse_11919.dll
Event: 53.796 Loaded shared library C:\Users\29470\AppData\Local\Temp\jna-47925862\jna6089012982961748806.dll
Event: 60.385 Loaded shared library C:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\sunmscapi.dll
Event: 61.112 Loaded shared library C:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\extnet.dll
Deoptimization events (20 events):
Event: 253.472 Thread 0x0000019a1feebea0 DEOPT PACKING pc=0x0000019a2ae22998 sp=0x00000034e27ff580
Event: 253.472 Thread 0x0000019a1feebea0 DEOPT UNPACKING pc=0x0000019a2a4e3aa2 sp=0x00000034e27ff500 mode 2
Event: 257.426 Thread 0x0000019a055a0620 Uncommon trap: trap_request=0xffffff45 fr.pc=0x0000019a2b474974 relative=0x00000000000000d4
Event: 257.426 Thread 0x0000019a055a0620 Uncommon trap: reason=unstable_if action=reinterpret pc=0x0000019a2b474974 method=java.util.concurrent.ConcurrentHashMap.sumCount()J @ 41 c2
Event: 257.426 Thread 0x0000019a055a0620 DEOPT PACKING pc=0x0000019a2b474974 sp=0x00000034e93feb30
Event: 257.426 Thread 0x0000019a055a0620 DEOPT UNPACKING pc=0x0000019a2a4e3aa2 sp=0x00000034e93feaa0 mode 2
Event: 259.227 Thread 0x0000019a7f813720 DEOPT PACKING pc=0x0000019a264ba843 sp=0x00000034e47fcd50
Event: 259.227 Thread 0x0000019a7f813720 DEOPT UNPACKING pc=0x0000019a2a4e4242 sp=0x00000034e47fc250 mode 0
Event: 259.389 Thread 0x0000019a7f813720 Uncommon trap: trap_request=0xffffff45 fr.pc=0x0000019a2be6f5ac relative=0x000000000000234c
Event: 259.389 Thread 0x0000019a7f813720 Uncommon trap: reason=unstable_if action=reinterpret pc=0x0000019a2be6f5ac method=org.eclipse.core.internal.dtree.AbstractDataTreeNode.assembleWith([Lorg/eclipse/core/internal/dtree/AbstractDataTreeNode;[Lorg/eclipse/core/internal/dtree/AbstractDataTreeNo
Event: 259.389 Thread 0x0000019a7f813720 DEOPT PACKING pc=0x0000019a2be6f5ac sp=0x00000034e47fdae0
Event: 259.389 Thread 0x0000019a7f813720 DEOPT UNPACKING pc=0x0000019a2a4e3aa2 sp=0x00000034e47fdab0 mode 2
Event: 260.854 Thread 0x0000019a047e20d0 Uncommon trap: trap_request=0xffffffd6 fr.pc=0x0000019a2c7257b0 relative=0x0000000000003e70
Event: 260.854 Thread 0x0000019a047e20d0 Uncommon trap: reason=array_check action=maybe_recompile pc=0x0000019a2c7257b0 method=org.eclipse.core.internal.events.NodeIDMap.put(JLorg/eclipse/core/runtime/IPath;Lorg/eclipse/core/runtime/IPath;)V @ 225 c2
Event: 260.854 Thread 0x0000019a047e20d0 DEOPT PACKING pc=0x0000019a2c7257b0 sp=0x00000034e60feae0
Event: 260.854 Thread 0x0000019a047e20d0 DEOPT UNPACKING pc=0x0000019a2a4e3aa2 sp=0x00000034e60fe8a0 mode 2
Event: 261.252 Thread 0x0000019a7f813720 Uncommon trap: trap_request=0xffffff45 fr.pc=0x0000019a2c7337a0 relative=0x00000000000035c0
Event: 261.252 Thread 0x0000019a7f813720 Uncommon trap: reason=unstable_if action=reinterpret pc=0x0000019a2c7337a0 method=org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding.computeId()V @ 2907 c2
Event: 261.252 Thread 0x0000019a7f813720 DEOPT PACKING pc=0x0000019a2c7337a0 sp=0x00000034e47fd3e0
Event: 261.252 Thread 0x0000019a7f813720 DEOPT UNPACKING pc=0x0000019a2a4e3aa2 sp=0x00000034e47fd360 mode 2
Classes loaded (20 events):
Event: 206.735 Loading class sun/security/ssl/PostHandshakeContext
Event: 206.735 Loading class sun/security/ssl/PostHandshakeContext done
Event: 206.736 Loading class sun/security/ssl/NewSessionTicket$T13NewSessionTicketMessage
Event: 206.736 Loading class sun/security/ssl/NewSessionTicket$T13NewSessionTicketMessage done
Event: 206.892 Loading class java/io/SequenceInputStream
Event: 206.892 Loading class java/io/SequenceInputStream done
Event: 206.892 Loading class java/util/zip/GZIPInputStream$1
Event: 206.892 Loading class java/util/zip/GZIPInputStream$1 done
Event: 230.770 Loading class java/io/CharArrayWriter
Event: 230.786 Loading class java/io/CharArrayWriter done
Event: 230.810 Loading class java/nio/file/attribute/PosixFileAttributes
Event: 230.811 Loading class java/nio/file/attribute/PosixFileAttributes done
Event: 251.321 Loading class jdk/internal/module/IllegalAccessLogger
Event: 251.321 Loading class jdk/internal/module/IllegalAccessLogger done
Event: 251.511 Loading class java/lang/Throwable$WrappedPrintWriter
Event: 251.511 Loading class java/lang/Throwable$PrintStreamOrWriter
Event: 251.511 Loading class java/lang/Throwable$PrintStreamOrWriter done
Event: 251.511 Loading class java/lang/Throwable$WrappedPrintWriter done
Event: 260.460 Loading class jdk/internal/module/IllegalAccessLogger
Event: 260.460 Loading class jdk/internal/module/IllegalAccessLogger done
Classes unloaded (17 events):
Event: 56.721 Thread 0x0000019a7d7f4af0 Unloading class 0x0000019a3d1a8800 'java/lang/invoke/LambdaForm$MH+0x0000019a3d1a8800'
Event: 56.721 Thread 0x0000019a7d7f4af0 Unloading class 0x0000019a3d1a8400 'java/lang/invoke/LambdaForm$MH+0x0000019a3d1a8400'
Event: 56.721 Thread 0x0000019a7d7f4af0 Unloading class 0x0000019a3d1a8000 'java/lang/invoke/LambdaForm$MH+0x0000019a3d1a8000'
Event: 56.721 Thread 0x0000019a7d7f4af0 Unloading class 0x0000019a3d1a7c00 'java/lang/invoke/LambdaForm$MH+0x0000019a3d1a7c00'
Event: 56.721 Thread 0x0000019a7d7f4af0 Unloading class 0x0000019a3d1a7800 'java/lang/invoke/LambdaForm$BMH+0x0000019a3d1a7800'
Event: 56.721 Thread 0x0000019a7d7f4af0 Unloading class 0x0000019a3d1a7400 'java/lang/invoke/LambdaForm$DMH+0x0000019a3d1a7400'
Event: 56.721 Thread 0x0000019a7d7f4af0 Unloading class 0x0000019a3d1a6400 'java/lang/invoke/LambdaForm$DMH+0x0000019a3d1a6400'
Event: 71.171 Thread 0x0000019a7d7f4af0 Unloading class 0x0000019a3d4b1400 'jdk/internal/reflect/GeneratedSerializationConstructorAccessor32'
Event: 71.171 Thread 0x0000019a7d7f4af0 Unloading class 0x0000019a3d49c000 'java/lang/invoke/LambdaForm$DMH+0x0000019a3d49c000'
Event: 71.171 Thread 0x0000019a7d7f4af0 Unloading class 0x0000019a3d488c00 'jdk/internal/reflect/GeneratedSerializationConstructorAccessor31'
Event: 71.171 Thread 0x0000019a7d7f4af0 Unloading class 0x0000019a3d488400 'jdk/internal/reflect/GeneratedSerializationConstructorAccessor29'
Event: 71.171 Thread 0x0000019a7d7f4af0 Unloading class 0x0000019a3d485c00 'jdk/internal/reflect/GeneratedSerializationConstructorAccessor26'
Event: 71.171 Thread 0x0000019a7d7f4af0 Unloading class 0x0000019a3d484800 'jdk/internal/reflect/GeneratedSerializationConstructorAccessor25'
Event: 253.253 Thread 0x0000019a7d7f4af0 Unloading class 0x0000019a3d700c00 'java/lang/invoke/LambdaForm$DMH+0x0000019a3d700c00'
Event: 253.253 Thread 0x0000019a7d7f4af0 Unloading class 0x0000019a3d700400 'java/lang/invoke/LambdaForm$DMH+0x0000019a3d700400'
Event: 253.253 Thread 0x0000019a7d7f4af0 Unloading class 0x0000019a3d701400 'java/lang/invoke/LambdaForm$DMH+0x0000019a3d701400'
Event: 253.253 Thread 0x0000019a7d7f4af0 Unloading class 0x0000019a3d701000 'java/lang/invoke/LambdaForm$DMH+0x0000019a3d701000'
Classes redefined (0 events):
No events
Internal exceptions (20 events):
Event: 259.422 Thread 0x0000019a7f813720 Exception <a 'sun/nio/fs/WindowsException'{0x00000007aad2d4f0}> (0x00000007aad2d4f0)
thrown [s\src\hotspot\share\prims\jni.cpp, line 520]
Event: 259.424 Thread 0x0000019a7f813720 Exception <a 'sun/nio/fs/WindowsException'{0x00000007aad61a80}> (0x00000007aad61a80)
thrown [s\src\hotspot\share\prims\jni.cpp, line 520]
Event: 259.424 Thread 0x0000019a06ee1920 Exception <a 'sun/nio/fs/WindowsException'{0x00000007aad6d208}> (0x00000007aad6d208)
thrown [s\src\hotspot\share\prims\jni.cpp, line 520]
Event: 259.424 Thread 0x0000019a06ee2640 Exception <a 'sun/nio/fs/WindowsException'{0x00000007aad6f538}> (0x00000007aad6f538)
thrown [s\src\hotspot\share\prims\jni.cpp, line 520]
Event: 259.424 Thread 0x0000019a08058360 Exception <a 'sun/nio/fs/WindowsException'{0x00000007aad15198}> (0x00000007aad15198)
thrown [s\src\hotspot\share\prims\jni.cpp, line 520]
Event: 259.424 Thread 0x0000019a06ee4da0 Exception <a 'sun/nio/fs/WindowsException'{0x00000007aad125f8}> (0x00000007aad125f8)
thrown [s\src\hotspot\share\prims\jni.cpp, line 520]
Event: 259.427 Thread 0x0000019a7f813720 Exception <a 'java/io/IOException'{0x00000007aab01f08}> (0x00000007aab01f08)
thrown [s\src\hotspot\share\prims\jni.cpp, line 520]
Event: 259.427 Thread 0x0000019a7f813720 Exception <a 'java/io/IOException'{0x00000007aab19110}> (0x00000007aab19110)
thrown [s\src\hotspot\share\prims\jni.cpp, line 520]
Event: 260.490 Thread 0x0000019a7f813720 Exception <a 'java/io/IOException'{0x00000007aad115d0}> (0x00000007aad115d0)
thrown [s\src\hotspot\share\prims\jni.cpp, line 520]
Event: 260.491 Thread 0x0000019a7f813720 Exception <a 'java/io/IOException'{0x00000007aad6b5a8}> (0x00000007aad6b5a8)
thrown [s\src\hotspot\share\prims\jni.cpp, line 520]
Event: 260.621 Thread 0x0000019a7f813720 Exception <a 'sun/nio/fs/WindowsException'{0x00000007aace9090}> (0x00000007aace9090)
thrown [s\src\hotspot\share\prims\jni.cpp, line 520]
Event: 260.622 Thread 0x0000019a06ee1920 Exception <a 'sun/nio/fs/WindowsException'{0x00000007aacebd68}> (0x00000007aacebd68)
thrown [s\src\hotspot\share\prims\jni.cpp, line 520]
Event: 260.622 Thread 0x0000019a06ee1920 Exception <a 'sun/nio/fs/WindowsException'{0x00000007aacedd88}> (0x00000007aacedd88)
thrown [s\src\hotspot\share\prims\jni.cpp, line 520]
Event: 260.622 Thread 0x0000019a08058360 Exception <a 'sun/nio/fs/WindowsException'{0x00000007aacee980}> (0x00000007aacee980)
thrown [s\src\hotspot\share\prims\jni.cpp, line 520]
Event: 260.623 Thread 0x0000019a06ee4da0 Exception <a 'sun/nio/fs/WindowsException'{0x00000007aacf5018}> (0x00000007aacf5018)
thrown [s\src\hotspot\share\prims\jni.cpp, line 520]
Event: 260.623 Thread 0x0000019a06ee1920 Exception <a 'sun/nio/fs/WindowsException'{0x00000007aacf1d38}> (0x00000007aacf1d38)
thrown [s\src\hotspot\share\prims\jni.cpp, line 520]
Event: 260.623 Thread 0x0000019a06ee2640 Exception <a 'sun/nio/fs/WindowsException'{0x00000007aacf29d0}> (0x00000007aacf29d0)
thrown [s\src\hotspot\share\prims\jni.cpp, line 520]
Event: 260.623 Thread 0x0000019a06edeb30 Exception <a 'sun/nio/fs/WindowsException'{0x00000007aacf8218}> (0x00000007aacf8218)
thrown [s\src\hotspot\share\prims\jni.cpp, line 520]
Event: 260.623 Thread 0x0000019a06ee4710 Exception <a 'sun/nio/fs/WindowsException'{0x00000007aacf96f0}> (0x00000007aacf96f0)
thrown [s\src\hotspot\share\prims\jni.cpp, line 520]
Event: 260.623 Thread 0x0000019a06ee2cd0 Exception <a 'sun/nio/fs/WindowsException'{0x00000007aacfac90}> (0x00000007aacfac90)
thrown [s\src\hotspot\share\prims\jni.cpp, line 520]
ZGC Phase Switch (0 events):
No events
VM Operations (20 events):
Event: 260.662 Executing safepoint VM operation: ParallelGCFailedAllocation (Allocation Failure)
Event: 260.662 Executing safepoint VM operation: ParallelGCFailedAllocation (Allocation Failure) done
Event: 260.690 Executing safepoint VM operation: ParallelGCFailedAllocation (Allocation Failure)
Event: 260.692 Executing safepoint VM operation: ParallelGCFailedAllocation (Allocation Failure) done
Event: 260.702 Executing safepoint VM operation: ParallelGCFailedAllocation (Allocation Failure)
Event: 260.705 Executing safepoint VM operation: ParallelGCFailedAllocation (Allocation Failure) done
Event: 260.732 Executing safepoint VM operation: ParallelGCFailedAllocation (Allocation Failure)
Event: 260.733 Executing safepoint VM operation: ParallelGCFailedAllocation (Allocation Failure) done
Event: 260.755 Executing safepoint VM operation: ParallelGCFailedAllocation (Allocation Failure)
Event: 260.757 Executing safepoint VM operation: ParallelGCFailedAllocation (Allocation Failure) done
Event: 260.771 Executing safepoint VM operation: ParallelGCFailedAllocation (Allocation Failure)
Event: 260.772 Executing safepoint VM operation: ParallelGCFailedAllocation (Allocation Failure) done
Event: 261.200 Executing safepoint VM operation: ParallelGCFailedAllocation (Allocation Failure)
Event: 261.201 Executing safepoint VM operation: ParallelGCFailedAllocation (Allocation Failure) done
Event: 261.249 Executing safepoint VM operation: ParallelGCFailedAllocation (Allocation Failure)
Event: 261.249 Executing safepoint VM operation: ParallelGCFailedAllocation (Allocation Failure) done
Event: 261.282 Executing safepoint VM operation: ParallelGCFailedAllocation (Allocation Failure)
Event: 261.283 Executing safepoint VM operation: ParallelGCFailedAllocation (Allocation Failure) done
Event: 261.306 Executing safepoint VM operation: ParallelGCFailedAllocation (Allocation Failure)
Event: 261.306 Executing safepoint VM operation: ParallelGCFailedAllocation (Allocation Failure) done
Memory protections (0 events):
No events
Nmethod flushes (20 events):
Event: 259.817 Thread 0x0000019a7d7f4af0 flushing nmethod 0x0000019a26445110
Event: 259.817 Thread 0x0000019a7d7f4af0 flushing nmethod 0x0000019a2644bb90
Event: 259.817 Thread 0x0000019a7d7f4af0 flushing nmethod 0x0000019a2644c090
Event: 259.817 Thread 0x0000019a7d7f4af0 flushing nmethod 0x0000019a2644c690
Event: 259.817 Thread 0x0000019a7d7f4af0 flushing nmethod 0x0000019a2644d010
Event: 259.817 Thread 0x0000019a7d7f4af0 flushing nmethod 0x0000019a2644d490
Event: 259.817 Thread 0x0000019a7d7f4af0 flushing nmethod 0x0000019a2644ea90
Event: 259.817 Thread 0x0000019a7d7f4af0 flushing nmethod 0x0000019a2644f190
Event: 259.817 Thread 0x0000019a7d7f4af0 flushing nmethod 0x0000019a26450e10
Event: 259.817 Thread 0x0000019a7d7f4af0 flushing nmethod 0x0000019a26459f90
Event: 259.817 Thread 0x0000019a7d7f4af0 flushing nmethod 0x0000019a2645ac10
Event: 259.817 Thread 0x0000019a7d7f4af0 flushing nmethod 0x0000019a2645e310
Event: 259.817 Thread 0x0000019a7d7f4af0 flushing nmethod 0x0000019a2645ee10
Event: 259.817 Thread 0x0000019a7d7f4af0 flushing nmethod 0x0000019a2645f610
Event: 259.817 Thread 0x0000019a7d7f4af0 flushing nmethod 0x0000019a26460290
Event: 259.817 Thread 0x0000019a7d7f4af0 flushing nmethod 0x0000019a26462d10
Event: 259.817 Thread 0x0000019a7d7f4af0 flushing osr nmethod 0x0000019a2646d510
Event: 259.817 Thread 0x0000019a7d7f4af0 flushing nmethod 0x0000019a2646dc90
Event: 259.817 Thread 0x0000019a7d7f4af0 flushing nmethod 0x0000019a26484310
Event: 259.817 Thread 0x0000019a7d7f4af0 flushing nmethod 0x0000019a264ec890
Events (20 events):
Event: 254.355 Thread 0x0000019a7f813720 Thread added: 0x0000019a055a6f20
Event: 254.356 Thread 0x0000019a055a6f20 Thread exited: 0x0000019a055a6f20
Event: 254.356 Thread 0x0000019a055a47c0 Thread exited: 0x0000019a055a47c0
Event: 254.356 Thread 0x0000019a055a4130 Thread exited: 0x0000019a055a4130
Event: 254.356 Thread 0x0000019a055a3aa0 Thread exited: 0x0000019a055a3aa0
Event: 254.356 Thread 0x0000019a055a26f0 Thread exited: 0x0000019a055a26f0
Event: 254.393 Thread 0x0000019a7f813720 Thread added: 0x0000019a055a3410
Event: 254.393 Thread 0x0000019a7f813720 Thread added: 0x0000019a055a26f0
Event: 254.393 Thread 0x0000019a7f813720 Thread added: 0x0000019a055a6f20
Event: 254.393 Thread 0x0000019a7f813720 Thread added: 0x0000019a055a3aa0
Event: 254.393 Thread 0x0000019a7f813720 Thread added: 0x0000019a055a0620
Event: 258.744 Thread 0x0000019a055a3aa0 Thread exited: 0x0000019a055a3aa0
Event: 258.744 Thread 0x0000019a055a6f20 Thread exited: 0x0000019a055a6f20
Event: 258.744 Thread 0x0000019a055a3410 Thread exited: 0x0000019a055a3410
Event: 258.744 Thread 0x0000019a055a0620 Thread exited: 0x0000019a055a0620
Event: 258.744 Thread 0x0000019a055a26f0 Thread exited: 0x0000019a055a26f0
Event: 259.319 Thread 0x0000019a7d7fc600 Thread added: 0x0000019a13696570
Event: 260.455 Thread 0x0000019a13696570 Thread exited: 0x0000019a13696570
Event: 260.533 Thread 0x0000019a7d85e0b0 Thread added: 0x0000019a05e1b160
Event: 260.854 Thread 0x0000019a05e1b160 Thread exited: 0x0000019a05e1b160
Dynamic libraries:
0x00007ff6ed120000 - 0x00007ff6ed12e000 c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\java.exe
0x00007fffa6370000 - 0x00007fffa6587000 C:\Windows\SYSTEM32\ntdll.dll
0x00007fffa5eb0000 - 0x00007fffa5f74000 C:\Windows\System32\KERNEL32.DLL
0x00007fffa38e0000 - 0x00007fffa3cb3000 C:\Windows\System32\KERNELBASE.dll
0x00007fffa37c0000 - 0x00007fffa38d1000 C:\Windows\System32\ucrtbase.dll
0x00007fff8d3c0000 - 0x00007fff8d3d8000 c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\jli.dll
0x00007fff7cb40000 - 0x00007fff7cb5e000 c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\VCRUNTIME140.dll
0x00007fffa4ea0000 - 0x00007fffa5051000 C:\Windows\System32\USER32.dll
0x00007fffa3fd0000 - 0x00007fffa3ff6000 C:\Windows\System32\win32u.dll
0x00007fffa5e80000 - 0x00007fffa5ea9000 C:\Windows\System32\GDI32.dll
0x00007fff85e50000 - 0x00007fff860eb000 C:\Windows\WinSxS\amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.22621.5240_none_2710eadf7384a318\COMCTL32.dll
0x00007fffa3460000 - 0x00007fffa3584000 C:\Windows\System32\gdi32full.dll
0x00007fffa6080000 - 0x00007fffa6127000 C:\Windows\System32\msvcrt.dll
0x00007fffa3d70000 - 0x00007fffa3e0a000 C:\Windows\System32\msvcp_win.dll
0x00007fffa4b20000 - 0x00007fffa4b51000 C:\Windows\System32\IMM32.DLL
0x00007fff99420000 - 0x00007fff9942c000 c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\vcruntime140_1.dll
0x00007fff26510000 - 0x00007fff2659d000 c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\msvcp140.dll
0x00007fff15b00000 - 0x00007fff168a1000 c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\server\jvm.dll
0x00007fffa5d50000 - 0x00007fffa5e01000 C:\Windows\System32\ADVAPI32.dll
0x00007fffa4cc0000 - 0x00007fffa4d67000 C:\Windows\System32\sechost.dll
0x00007fffa3d40000 - 0x00007fffa3d68000 C:\Windows\System32\bcrypt.dll
0x00007fffa4d70000 - 0x00007fffa4e84000 C:\Windows\System32\RPCRT4.dll
0x00007fffa6000000 - 0x00007fffa6071000 C:\Windows\System32\WS2_32.dll
0x00007fff9a2c0000 - 0x00007fff9a2f4000 C:\Windows\SYSTEM32\WINMM.dll
0x00007fffa3180000 - 0x00007fffa31cd000 C:\Windows\SYSTEM32\POWRPROF.dll
0x00007fff9b750000 - 0x00007fff9b75a000 C:\Windows\SYSTEM32\VERSION.dll
0x00007fffa3160000 - 0x00007fffa3173000 C:\Windows\SYSTEM32\UMPDC.dll
0x00007fffa24a0000 - 0x00007fffa24b8000 C:\Windows\SYSTEM32\kernel.appcore.dll
0x00007fff8a200000 - 0x00007fff8a20a000 c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\jimage.dll
0x00007fffa0da0000 - 0x00007fffa0fd2000 C:\Windows\SYSTEM32\DBGHELP.DLL
0x00007fffa50b0000 - 0x00007fffa5443000 C:\Windows\System32\combase.dll
0x00007fffa5a90000 - 0x00007fffa5b67000 C:\Windows\System32\OLEAUT32.dll
0x00007fff844e0000 - 0x00007fff84512000 C:\Windows\SYSTEM32\dbgcore.DLL
0x00007fffa3cc0000 - 0x00007fffa3d3b000 C:\Windows\System32\bcryptPrimitives.dll
0x00007fff7cb30000 - 0x00007fff7cb3f000 c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\instrument.dll
0x00007fff7cb10000 - 0x00007fff7cb30000 c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\java.dll
0x00007fffa41e0000 - 0x00007fffa4a82000 C:\Windows\System32\SHELL32.dll
0x00007fffa3e10000 - 0x00007fffa3f4f000 C:\Windows\System32\wintypes.dll
0x00007fffa13a0000 - 0x00007fffa1cbd000 C:\Windows\SYSTEM32\windows.storage.dll
0x00007fffa6220000 - 0x00007fffa632b000 C:\Windows\System32\SHCORE.dll
0x00007fffa5ce0000 - 0x00007fffa5d49000 C:\Windows\System32\shlwapi.dll
0x00007fffa3390000 - 0x00007fffa33bb000 C:\Windows\SYSTEM32\profapi.dll
0x00007fff59780000 - 0x00007fff59798000 c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\zip.dll
0x00007fff78bc0000 - 0x00007fff78bd0000 C:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\net.dll
0x00007fff9ba10000 - 0x00007fff9bb3c000 C:\Windows\SYSTEM32\WINHTTP.dll
0x00007fffa2920000 - 0x00007fffa298a000 C:\Windows\system32\mswsock.dll
0x00007fff59760000 - 0x00007fff59776000 C:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\nio.dll
0x00007fff749e0000 - 0x00007fff749f0000 c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\verify.dll
0x00007fff54840000 - 0x00007fff54885000 C:\Users\29470\AppData\Roaming\Cursor\User\globalStorage\redhat.java\1.53.0\config_win\org.eclipse.equinox.launcher\org.eclipse.equinox.launcher.win32.win32.x86_64_1.3.0.v20260203-2149\eclipse_11919.dll
0x00007fffa5450000 - 0x00007fffa55f1000 C:\Windows\System32\ole32.dll
0x00007fffa2b70000 - 0x00007fffa2b8b000 C:\Windows\SYSTEM32\CRYPTSP.dll
0x00007fffa2400000 - 0x00007fffa2437000 C:\Windows\system32\rsaenh.dll
0x00007fffa2a10000 - 0x00007fffa2a38000 C:\Windows\SYSTEM32\USERENV.dll
0x00007fffa2b90000 - 0x00007fffa2b9c000 C:\Windows\SYSTEM32\CRYPTBASE.dll
0x00007fffa1f60000 - 0x00007fffa1f8d000 C:\Windows\SYSTEM32\IPHLPAPI.DLL
0x00007fffa5a80000 - 0x00007fffa5a89000 C:\Windows\System32\NSI.dll
0x00007fff439e0000 - 0x00007fff43a29000 C:\Users\29470\AppData\Local\Temp\jna-47925862\jna6089012982961748806.dll
0x00007fffa6150000 - 0x00007fffa6158000 C:\Windows\System32\PSAPI.DLL
0x00007fff9b760000 - 0x00007fff9b779000 C:\Windows\SYSTEM32\dhcpcsvc6.DLL
0x00007fff9b350000 - 0x00007fff9b36f000 C:\Windows\SYSTEM32\dhcpcsvc.DLL
0x00007fff749d0000 - 0x00007fff749de000 C:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\sunmscapi.dll
0x00007fffa3590000 - 0x00007fffa36f7000 C:\Windows\System32\CRYPT32.dll
0x00007fffa2d10000 - 0x00007fffa2d3d000 C:\Windows\SYSTEM32\ncrypt.dll
0x00007fffa2cd0000 - 0x00007fffa2d07000 C:\Windows\SYSTEM32\NTASN1.dll
0x00007fffa1fe0000 - 0x00007fffa20d8000 C:\Windows\SYSTEM32\DNSAPI.dll
0x00000000676a0000 - 0x00000000676c6000 C:\Program Files\Bonjour\mdnsNSP.dll
0x00007fff9a070000 - 0x00007fff9a07a000 C:\Windows\System32\rasadhlp.dll
0x00007fff9a230000 - 0x00007fff9a2b3000 C:\Windows\System32\fwpuclnt.dll
0x00007fff73830000 - 0x00007fff73839000 C:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\extnet.dll
0x00007fffa25b0000 - 0x00007fffa25e4000 C:\Windows\SYSTEM32\ntmarta.dll
JVMTI agents:
c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\lombok\lombok-1.18.39-4050.jar path:c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\instrument.dll, loaded, initialized, instrumentlib options:none
dbghelp: loaded successfully - version: 4.0.5 - missing functions: none
symbol engine: initialized successfully - sym options: 0x614 - pdb path: .;c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin;C:\Windows\SYSTEM32;C:\Windows\WinSxS\amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.22621.5240_none_2710eadf7384a318;c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\jre\21.0.10-win32-x86_64\bin\server;C:\Users\29470\AppData\Roaming\Cursor\User\globalStorage\redhat.java\1.53.0\config_win\org.eclipse.equinox.launcher\org.eclipse.equinox.launcher.win32.win32.x86_64_1.3.0.v20260203-2149;C:\Users\29470\AppData\Local\Temp\jna-47925862;C:\Program Files\Bonjour
VM Arguments:
jvm_args: --add-modules=ALL-SYSTEM --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/sun.nio.fs=ALL-UNNAMED -Declipse.application=org.eclipse.jdt.ls.core.id1 -Dosgi.bundles.defaultStartLevel=4 -Declipse.product=org.eclipse.jdt.ls.core.product -Djava.import.generatesMetadataFilesAtProjectRoot=false -DDetectVMInstallationsJob.disabled=true -Dfile.encoding=utf8 -XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx4G -Xms100m -Xlog:disable -javaagent:c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\lombok\lombok-1.18.39-4050.jar -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=c:\Users\29470\AppData\Roaming\Cursor\User\workspaceStorage\edbcc896f3fe3d61a54e553c9ad653cc\redhat.java -Daether.dependencyCollector.impl=bf
java_command: c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\server\plugins\org.eclipse.equinox.launcher_1.7.100.v20251111-0406.jar -configuration c:\Users\29470\AppData\Roaming\Cursor\User\globalStorage\redhat.java\1.53.0\config_win -data c:\Users\29470\AppData\Roaming\Cursor\User\workspaceStorage\edbcc896f3fe3d61a54e553c9ad653cc\redhat.java\jdt_ws --pipe=\\.\pipe\lsp-1253b08bf80e3a8568169725d2662b44-sock
java_class_path (initial): c:\Users\29470\.cursor\extensions\redhat.java-1.53.0-win32-x64\server\plugins\org.eclipse.equinox.launcher_1.7.100.v20251111-0406.jar
Launcher Type: SUN_STANDARD
[Global flags]
uintx AdaptiveSizePolicyWeight = 90 {product} {command line}
intx CICompilerCount = 12 {product} {ergonomic}
uintx GCTimeRatio = 4 {product} {command line}
bool HeapDumpOnOutOfMemoryError = true {manageable} {command line}
ccstr HeapDumpPath = c:\Users\29470\AppData\Roaming\Cursor\User\workspaceStorage\edbcc896f3fe3d61a54e553c9ad653cc\redhat.java {manageable} {command line}
size_t InitialHeapSize = 104857600 {product} {command line}
size_t MaxHeapSize = 4294967296 {product} {command line}
size_t MaxNewSize = 1431306240 {product} {ergonomic}
size_t MinHeapDeltaBytes = 524288 {product} {ergonomic}
size_t MinHeapSize = 104857600 {product} {command line}
size_t NewSize = 34603008 {product} {ergonomic}
uintx NonNMethodCodeHeapSize = 7602480 {pd product} {ergonomic}
uintx NonProfiledCodeHeapSize = 122027880 {pd product} {ergonomic}
size_t OldSize = 70254592 {product} {ergonomic}
uintx ProfiledCodeHeapSize = 122027880 {pd product} {ergonomic}
uintx ReservedCodeCacheSize = 251658240 {pd product} {ergonomic}
bool SegmentedCodeCache = true {product} {ergonomic}
size_t SoftMaxHeapSize = 4294967296 {manageable} {ergonomic}
bool UseCompressedOops = true {product lp64_product} {ergonomic}
bool UseLargePagesIndividualAllocation = false {pd product} {ergonomic}
bool UseParallelGC = true {product} {command line}
Logging:
Log output configuration:
#0: stdout all=off uptime,level,tags foldmultilines=false
#1: stderr all=off uptime,level,tags foldmultilines=false
Release file:
JAVA_VERSION="21.0.10"
MODULES="java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.ec jdk.crypto.cryptoki jdk.crypto.mscapi jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.internal.le jdk.internal.vm.ci jdk.internal.vm.compiler jdk.internal.vm.compiler.management jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jshell jdk.jsobject jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.random jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom"
Environment Variables:
JAVA_HOME=C:\Program Files\Java\jdk1.8.0_191
CLASSPATH=.;C:\Program Files\Java\jdk1.8.0_191\lib;C:\Program Files\Java\jdk1.8.0_191\lib\dt.jar;C:\Program Files\Java\jdk1.8.0_191\lib\tools.jar;
PATH=C:\Users\29470\.local\bin;D:\mysql-9.6.0\bin;C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\Program Files\Common Files\Oracle\Java\javapath;G:\oracle11\product\11.2.0\dbhome_1\bin;C:\Program Files (x86)\Common Files\Oracle\Java\javapath;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Program Files\Java\jdk1.8.0_191\bin;C:\Windows\System32\HWAudioDriverLibs;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Users\Administrator\AppData\Local\Microsoft\WindowsApps;C:\<5C><>ѹ<EFBFBD><D1B9><EFBFBD><EFBFBD>\Bandizip\;F:\;VN\bin;F:\SDK\platform-tools;F:\SDK\tools;C:\Users\29470\AppData\Local\Microsoft\WindowsApps;C:\Program Files\Java\jdk1.8.0_191\bin;C:\Program Files\Java\jdk1.8.0_191\jre\bin;C:\Program Files (x86)\dotnet\;F:\maven-3.6.3-z\maven3\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files\MySQL\MySQL Server 8.0\bin\;C:\Program Files\apache-tomcat-8.5.84\bin\;D:\ChromeDownloads\flutter_windows_3.7.0-stable\flutter\bin\cache\dart-sdk\bin;D:\ChromeDownloads\flutter_windows_3.7.0-stable\flutter\bin\cache\dart-sdk\bin\cache\dart-sdk;C:\Program Files\Tailscale\;C:\Program Files\Redis\;C:\maven3.8.5\maven-3.8.5-רҵ<D7A8><D2B5>Maven˽<6E><CBBD>-4.4-2022-5-15<31><35><EFBFBD>ڲ<EFBFBD><DAB2><EFBFBD><EFBFBD>ϣ<EFBFBD><CFA3><EFBFBD>ֹ<EFBFBD><D6B9>й\maven3\bin;C:\Windows\sys;em32\config\systemprofile\AppData\Local\Microsoft\WindowsApps;C:\windows\system32\HWAudioDriver\;C:\Program Files\nodejs\node_global\node_modules;C:\ProgramData\Microsoft\Windows\Start Menu\Programs\MySQL\bin;C:\Program Files\MySQL\MySQL Server 5.7\bin;C:\Program Files\apache-tomcat-8.5.84\bin;F:\Git\cmd;C:\Program Files\TortoiseGit\bin;D:\һ<><D2BB>VUE<55><45>ĿAPP\HBuilderX.4.29.2024093009\HBuilderX\plugins\launcher-tools\tools\adbs;C:\Program Files (x86)\Tencent\΢<><CEA2>web<65><62><EFBFBD><EFBFBD><EFBFBD>߹<EFBFBD><DFB9><EFBFBD>\dll;C:\Program Files\nodejs\;C:\Program Files (x86)\Windows Kits\8.1\Windows Performance Toolkit\;C:\Program Files (x86)\Microsoft SQL Server\160\Tools\Binn\;C:\Program Files\Microsoft SQL Server\160\Tools\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;C:\Program Files\Microsoft SQL Server\160\DTS\Binn\;C:\Program Files\dotnet\;D:\pgsql\bin;D:\Windows Kits\10\Windows Performance Toolkit\;D:\<5C><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>͸\;C:\Program Files\Huawei\DevEco Studio\bin;C:\Program Files\nodejs\node_global;C:\Program Files\nodejs\node_modules;D:\<5C><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>\Microsoft VS Code\bin;C:\Users\29470\AppData\Roaming\npm;C:\Users\29470\.dotnet\tools;C:\Users\29470\AppData\Local\Programs\cursor\resources\app\bin
USERNAME=29470
OS=Windows_NT
PROCESSOR_IDENTIFIER=Intel64 Family 6 Model 154 Stepping 3, GenuineIntel
TMP=C:\Users\29470\AppData\Local\Temp
TEMP=C:\Users\29470\AppData\Local\Temp
Periodic native trim disabled
--------------- S Y S T E M ---------------
OS:
Windows 11 , 64 bit Build 22621 (10.0.22621.5305)
OS uptime: 0 days 0:29 hours
CPU: total 16 (initial active 16) (8 cores per cpu, 2 threads per core) family 6 model 154 stepping 3 microcode 0x41e, cx8, cmov, fxsr, ht, mmx, 3dnowpref, sse, sse2, sse3, ssse3, sse4.1, sse4.2, popcnt, lzcnt, tsc, tscinvbit, avx, avx2, aes, erms, clmul, bmi1, bmi2, adx, sha, fma, vzeroupper, clflush, clflushopt, clwb, serialize, rdtscp, rdpid, fsrm, f16c, pku, cet_ibt, cet_ss
Processor Information for processor 0
Max Mhz: 2500, Current Mhz: 2500, Mhz Limit: 2500
Processor Information for processor 1
Max Mhz: 2500, Current Mhz: 2500, Mhz Limit: 2500
Processor Information for processor 2
Max Mhz: 2500, Current Mhz: 2500, Mhz Limit: 2500
Processor Information for processor 3
Max Mhz: 2500, Current Mhz: 2500, Mhz Limit: 2500
Processor Information for processor 4
Max Mhz: 2500, Current Mhz: 2500, Mhz Limit: 2500
Processor Information for processor 5
Max Mhz: 2500, Current Mhz: 2500, Mhz Limit: 2500
Processor Information for processor 6
Max Mhz: 2500, Current Mhz: 2500, Mhz Limit: 2500
Processor Information for processor 7
Max Mhz: 2500, Current Mhz: 2500, Mhz Limit: 2500
Processor Information for processor 8
Max Mhz: 2500, Current Mhz: 1800, Mhz Limit: 1800
Processor Information for processor 9
Max Mhz: 2500, Current Mhz: 1800, Mhz Limit: 1800
Processor Information for processor 10
Max Mhz: 2500, Current Mhz: 1800, Mhz Limit: 1800
Processor Information for processor 11
Max Mhz: 2500, Current Mhz: 1800, Mhz Limit: 1800
Processor Information for processor 12
Max Mhz: 2500, Current Mhz: 1800, Mhz Limit: 1800
Processor Information for processor 13
Max Mhz: 2500, Current Mhz: 1800, Mhz Limit: 1800
Processor Information for processor 14
Max Mhz: 2500, Current Mhz: 1800, Mhz Limit: 1800
Processor Information for processor 15
Max Mhz: 2500, Current Mhz: 1800, Mhz Limit: 1800
Memory: 4k page, system-wide physical 16110M (655M free)
TotalPageFile size 34275M (AvailPageFile size 0M)
current process WorkingSet (physical memory assigned to process): 578M, peak: 1083M
current process commit charge ("private bytes"): 713M, peak: 3225M
vm_info: OpenJDK 64-Bit Server VM (21.0.10+7-LTS) for windows-amd64 JRE (21.0.10+7-LTS), built on 2026-01-20T00:00:00Z by "admin" with MS VC++ 17.12 (VS2022)
END.

View File

@@ -18,5 +18,10 @@
<groupId>org.jeecgframework.boot3</groupId> <groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-boot-base-core</artifactId> <artifactId>jeecg-boot-base-core</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.30</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -0,0 +1,15 @@
package org.jeecg.modules.print.ai;
import java.util.Map;
/** 根据上传的版式图片生成原生打印设计器可用的 templateJson + 模拟数据 */
public interface INativePrintTemplateImageAnalyzeService {
/**
* @param imageBytes 图片二进制
* @param mime 如 image/png可为空则按文件名猜测
* @param originalFilename 原始文件名(用于猜测 mime
* @return 包含 templateJson、mockDataJson、aiUsed、hint 等字段
*/
Map<String, Object> analyzeBytes(byte[] imageBytes, String mime, String originalFilename) throws Exception;
}

View File

@@ -0,0 +1,32 @@
package org.jeecg.modules.print.ai;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 原生打印模板:上传图片走视觉大模型生成 JSON 的配置OpenAI 兼容接口,如 OpenAI / 通义千问 compatible-mode
*/
@Data
@Component
@ConfigurationProperties(prefix = "print.native-template-ai")
public class NativePrintTemplateAiProperties {
/** 是否开启该能力(关闭时接口直接返回错误) */
private boolean enabled = true;
/** 为空时走本地占位布局(按图片尺寸估算纸张),不调用大模型 */
private String apiKey = "";
/** 例如 https://api.openai.com/v1 或 https://dashscope.aliyuncs.com/compatible-mode/v1 */
private String baseUrl = "https://api.openai.com/v1";
/** 例如 gpt-4o-mini、qwen-vl-plus */
private String model = "gpt-4o-mini";
/** 单张图片大小上限MB */
private int maxImageMb = 8;
/** HTTP 调用超时(秒) */
private int httpTimeoutSeconds = 120;
}

View File

@@ -6,15 +6,46 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import javax.print.Doc;
import javax.print.DocFlavor;
import javax.print.DocPrintJob;
import javax.print.PrintException;
import javax.print.PrintService;
import javax.print.PrintServiceLookup;
import javax.print.SimpleDoc;
import javax.print.attribute.HashPrintRequestAttributeSet;
import javax.print.attribute.standard.JobName;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterAbortException;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import java.io.ByteArrayInputStream;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog; import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.system.base.controller.JeecgController; import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.modules.print.ai.INativePrintTemplateImageAnalyzeService;
import org.jeecg.modules.print.entity.PrintTemplate; import org.jeecg.modules.print.entity.PrintTemplate;
import org.jeecg.modules.print.service.IPrintTemplateService; import org.jeecg.modules.print.service.IPrintTemplateService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.Map; import java.util.Map;
@@ -27,6 +58,11 @@ import java.util.Map;
@RestController @RestController
@RequestMapping("/print/template") @RequestMapping("/print/template")
public class PrintTemplateController extends JeecgController<PrintTemplate, IPrintTemplateService> { public class PrintTemplateController extends JeecgController<PrintTemplate, IPrintTemplateService> {
@Value("${print.network-printers:}")
private String networkPrinters;
@Autowired
private INativePrintTemplateImageAnalyzeService nativePrintTemplateImageAnalyzeService;
/** /**
* 分页列表 * 分页列表
@@ -161,4 +197,292 @@ public class PrintTemplateController extends JeecgController<PrintTemplate, IPri
} }
return Result.OK(t); return Result.OK(t);
} }
@AutoLog(value = "打印模板-图片分析生成原生JSON")
@Operation(summary = "打印模板-上传图片分析为原生模板JSON前端传 imageBase64可接 OpenAI 兼容视觉模型)")
@PostMapping(value = "/analyzeImageForNative")
@RequiresPermissions("print:template:edit")
public Result<Map<String, Object>> analyzeImageForNative(@RequestBody Map<String, String> body) {
try {
String imageBase64 = body == null ? null : body.get("imageBase64");
if (StringUtils.isBlank(imageBase64)) {
return Result.error("imageBase64 不能为空");
}
String filename = body.get("filename");
String mime = body.get("mime");
byte[] bytes = decodeImageBase64(imageBase64);
return Result.OK(nativePrintTemplateImageAnalyzeService.analyzeBytes(bytes, mime, filename));
} catch (Exception e) {
log.error("图片分析失败", e);
return Result.error("图片分析失败:" + e.getMessage());
}
}
private static byte[] decodeImageBase64(String imageBase64) {
String s = StringUtils.trimToEmpty(imageBase64);
int comma = s.indexOf(',');
if (s.startsWith("data:") && comma > 0) {
s = s.substring(comma + 1);
}
return Base64.getDecoder().decode(s.replaceAll("\\s", ""));
}
@Operation(summary = "打印模板-查询可用打印机")
@GetMapping(value = "/queryPrinters")
@RequiresPermissions("print:template:list")
public Result<Map<String, Object>> queryPrinters() {
Map<String, Object> res = new HashMap<>(8);
List<String> serverPrinters = new ArrayList<>();
String serverDefaultPrinter = "";
try {
PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
if (services != null) {
for (PrintService service : services) {
if (service != null && StringUtils.isNotBlank(service.getName())) {
serverPrinters.add(service.getName().trim());
}
}
}
PrintService defaultService = PrintServiceLookup.lookupDefaultPrintService();
if (defaultService != null && StringUtils.isNotBlank(defaultService.getName())) {
serverDefaultPrinter = defaultService.getName().trim();
}
} catch (Exception e) {
log.warn("查询服务器打印机失败: {}", e.getMessage());
}
List<String> networkPrinterList =
StringUtils.isBlank(networkPrinters)
? new ArrayList<>()
: java.util.Arrays.stream(networkPrinters.split(","))
.map(String::trim)
.filter(StringUtils::isNotBlank)
.distinct()
.collect(Collectors.toList());
Map<String, Object> capability = new LinkedHashMap<>(4);
capability.put("localSupported", false);
capability.put("localReason", "浏览器环境无法直接枚举客户端本地打印机,需要本地组件或客户端程序配合。");
capability.put("serverSupported", true);
capability.put("networkSupported", true);
res.put("capability", capability);
res.put("serverPrinters", serverPrinters);
res.put("serverDefaultPrinter", serverDefaultPrinter);
res.put("networkPrinters", networkPrinterList);
return Result.OK(res);
}
@AutoLog(value = "打印模板-服务端直打")
@Operation(summary = "打印模板-服务端直打")
@PostMapping(value = "/directPrint")
@RequiresPermissions("print:template:list")
public Result<String> directPrint(@RequestBody Map<String, Object> body) {
String templateCode = String.valueOf(body.getOrDefault("templateCode", "")).trim();
String printerName = String.valueOf(body.getOrDefault("printerName", "")).trim();
Object dataJsonObj = body.get("dataJson");
String dataJsonText = dataJsonObj == null ? "" : String.valueOf(dataJsonObj);
if (StringUtils.isBlank(templateCode)) {
return Result.error("templateCode 不能为空");
}
if (StringUtils.isBlank(dataJsonText)) {
return Result.error("dataJson 不能为空");
}
PrintTemplate tpl = service.getByCode(templateCode);
if (tpl == null) {
return Result.error("模板不存在: " + templateCode);
}
try {
PrintService target = null;
if (StringUtils.isNotBlank(printerName) && !"__system_default__".equals(printerName)) {
PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
if (services != null) {
for (PrintService serviceItem : services) {
if (serviceItem != null && printerName.equalsIgnoreCase(String.valueOf(serviceItem.getName()).trim())) {
target = serviceItem;
break;
}
}
}
if (target == null) {
return Result.error("未找到指定打印机: " + printerName);
}
}
if (target == null) {
target = PrintServiceLookup.lookupDefaultPrintService();
}
if (target == null) {
return Result.error("未找到可用打印机,请检查服务器打印机配置");
}
// 说明:当前接口实现的是服务端直打(纯文本)。若需按 hiprint 模板渲染版式,建议接入独立渲染服务。
String content =
"QH-MES 快速打印\n模板编号: "
+ templateCode
+ "\n模板名称: "
+ String.valueOf(tpl.getTemplateName())
+ "\n\n数据JSON:\n"
+ dataJsonText
+ "\n";
final String[] lines = content.replace("\r\n", "\n").split("\n", -1);
PrinterJob job = PrinterJob.getPrinterJob();
job.setPrintService(target);
job.setJobName("QH-MES-" + templateCode);
job.setPrintable(
new Printable() {
@Override
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
Graphics2D g2 = (Graphics2D) graphics;
g2.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
g2.setFont(new Font("Microsoft YaHei", Font.PLAIN, 10));
int lineHeight = g2.getFontMetrics().getHeight() + 2;
int maxLinesPerPage = Math.max(1, (int) (pageFormat.getImageableHeight() / lineHeight));
int start = pageIndex * maxLinesPerPage;
if (start >= lines.length) {
return Printable.NO_SUCH_PAGE;
}
int end = Math.min(lines.length, start + maxLinesPerPage);
int y = g2.getFontMetrics().getAscent();
for (int i = start; i < end; i += 1) {
g2.drawString(lines[i], 0, y);
y += lineHeight;
}
return Printable.PAGE_EXISTS;
}
});
job.print();
return Result.OK("已提交到服务器打印机: " + target.getName());
} catch (Exception e) {
log.error("服务端直打失败", e);
return Result.error("服务端直打失败: " + e.getMessage());
}
}
@AutoLog(value = "打印模板-PDF后端打印")
@Operation(summary = "打印模板-PDF后端打印")
@PostMapping(value = "/directPrintPdf")
@RequiresPermissions("print:template:list")
public Result<String> directPrintPdf(@RequestBody Map<String, Object> body) {
String templateCode = String.valueOf(body.getOrDefault("templateCode", "")).trim();
String printerName = String.valueOf(body.getOrDefault("printerName", "")).trim();
String pdfBase64 = String.valueOf(body.getOrDefault("pdfBase64", "")).trim();
String fileName = String.valueOf(body.getOrDefault("fileName", "")).trim();
if (StringUtils.isBlank(templateCode)) {
return Result.error("templateCode 不能为空");
}
if (StringUtils.isBlank(pdfBase64)) {
return Result.error("pdfBase64 不能为空");
}
String lastResolvedPrinterLabel = null;
try {
PrintService target = resolvePrintService(printerName);
if (target == null) {
return Result.error("未找到可用打印机,请检查服务器打印机配置");
}
final String resolvedPrinterLabel = target.getName();
lastResolvedPrinterLabel = resolvedPrinterLabel;
String base64Body = pdfBase64;
int commaIdx = pdfBase64.indexOf(",");
if (pdfBase64.startsWith("data:") && commaIdx > 0) {
base64Body = pdfBase64.substring(commaIdx + 1);
}
byte[] pdfBytes = Base64.getDecoder().decode(base64Body);
String printJobName = StringUtils.isNotBlank(fileName) ? fileName : ("QH-MES-" + templateCode + ".pdf");
if (tryPrintPdfBytesWithDocFlavor(target, pdfBytes, printJobName)) {
return Result.OK("已提交PDF到服务器打印机: " + resolvedPrinterLabel);
}
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(pdfBytes))) {
PDFRenderer renderer = new PDFRenderer(document);
PrinterJob job = PrinterJob.getPrinterJob();
job.setPrintService(target);
job.setJobName(printJobName);
job.setPrintable((graphics, pageFormat, pageIndex) -> {
if (pageIndex >= document.getNumberOfPages()) {
return Printable.NO_SUCH_PAGE;
}
BufferedImage image;
try {
image = renderer.renderImageWithDPI(pageIndex, 150);
} catch (Exception ex) {
throw new PrinterException("PDF页面渲染失败: " + ex.getMessage());
}
Graphics2D g2 = (Graphics2D) graphics;
double imageableX = pageFormat.getImageableX();
double imageableY = pageFormat.getImageableY();
double imageableWidth = pageFormat.getImageableWidth();
double imageableHeight = pageFormat.getImageableHeight();
double scale = Math.min(imageableWidth / image.getWidth(), imageableHeight / image.getHeight());
int drawWidth = (int) Math.round(image.getWidth() * scale);
int drawHeight = (int) Math.round(image.getHeight() * scale);
int drawX = (int) Math.round(imageableX + (imageableWidth - drawWidth) / 2);
int drawY = (int) Math.round(imageableY + (imageableHeight - drawHeight) / 2);
g2.drawImage(image, drawX, drawY, drawWidth, drawHeight, null);
return Printable.PAGE_EXISTS;
});
HashPrintRequestAttributeSet patts = new HashPrintRequestAttributeSet();
patts.add(new JobName(printJobName, Locale.getDefault()));
job.print(patts);
}
return Result.OK("已提交PDF到服务器打印机: " + resolvedPrinterLabel);
} catch (PrinterAbortException e) {
log.error("PDF后端打印失败(PrinterAbortException)", e);
return Result.error(buildPdfPrinterAbortHint(printerName, lastResolvedPrinterLabel));
} catch (Exception e) {
log.error("PDF后端打印失败", e);
return Result.error("PDF后端打印失败: " + e.getMessage());
}
}
private boolean tryPrintPdfBytesWithDocFlavor(PrintService printService, byte[] pdfBytes, String jobName) {
DocFlavor flavor = new DocFlavor.INPUT_STREAM("application/pdf");
if (!printService.isDocFlavorSupported(flavor)) {
return false;
}
try {
DocPrintJob docJob = printService.createPrintJob();
ByteArrayInputStream in = new ByteArrayInputStream(pdfBytes);
Doc doc = new SimpleDoc(in, flavor, null);
HashPrintRequestAttributeSet attrs = new HashPrintRequestAttributeSet();
if (StringUtils.isNotBlank(jobName)) {
attrs.add(new JobName(jobName, Locale.getDefault()));
}
docJob.print(doc, attrs);
return true;
} catch (PrintException e) {
log.warn("PDF DocFlavor 直送失败,将回退为位图渲染: {} - {}", printService.getName(), e.getMessage());
return false;
}
}
private static String buildPdfPrinterAbortHint(String requestedPrinterName, String resolvedPrintQueueName) {
StringBuilder sb = new StringBuilder();
sb.append("打印任务被系统取消PrinterAbortException。常见原因");
sb.append("1) 默认或所选为「Microsoft Print to PDF」等虚拟打印机在 Tomcat 等服务进程无交互桌面时无法弹出保存对话框,作业会被中止——请安装实体打印机并在前端指定 printerName");
sb.append("2) 打印机离线、队列暂停、缺纸或驱动报错;");
sb.append("3) 运行服务的 Windows 账户无权访问打印队列。");
if (StringUtils.isNotBlank(resolvedPrintQueueName)) {
sb.append(" 当前实际使用的打印队列: ").append(resolvedPrintQueueName.trim()).append("");
}
if (StringUtils.isNotBlank(requestedPrinterName) && !"__system_default__".equalsIgnoreCase(requestedPrinterName.trim())) {
sb.append(" 请求参数 printerName: ").append(requestedPrinterName.trim()).append("");
}
return sb.toString();
}
private PrintService resolvePrintService(String printerName) {
PrintService target = null;
if (StringUtils.isNotBlank(printerName) && !"__system_default__".equals(printerName)) {
PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
if (services != null) {
for (PrintService serviceItem : services) {
if (serviceItem != null && printerName.equalsIgnoreCase(String.valueOf(serviceItem.getName()).trim())) {
target = serviceItem;
break;
}
}
}
}
if (target == null) {
target = PrintServiceLookup.lookupDefaultPrintService();
}
return target;
}
} }

View File

@@ -61,6 +61,17 @@
<artifactId>jeecg-boot-module-airag</artifactId> <artifactId>jeecg-boot-module-airag</artifactId>
<version>${jeecgboot.version}</version> <version>${jeecgboot.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.30</version>
</dependency>
<!-- 原生模板图片识别等打印扩展(与 jeecg-module-print 共用包名时需与本模块控制器二选一加载,此处引入服务 Bean -->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-module-print</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -6,8 +6,36 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import javax.print.Doc;
import javax.print.DocFlavor;
import javax.print.DocPrintJob;
import javax.print.PrintException;
import javax.print.PrintService;
import javax.print.PrintServiceLookup;
import javax.print.SimpleDoc;
import javax.print.attribute.HashPrintRequestAttributeSet;
import javax.print.attribute.standard.JobName;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterAbortException;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import java.io.ByteArrayInputStream;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
@@ -16,7 +44,10 @@ import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.modules.print.entity.PrintTemplate; import org.jeecg.modules.print.entity.PrintTemplate;
import org.jeecg.modules.print.service.IPrintTemplateService; import org.jeecg.modules.print.service.IPrintTemplateService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.jeecg.modules.print.ai.INativePrintTemplateImageAnalyzeService;
/** /**
* 打印模板维护Hiprint * 打印模板维护Hiprint
@@ -26,6 +57,11 @@ import org.springframework.web.bind.annotation.*;
@RestController @RestController
@RequestMapping("/print/template") @RequestMapping("/print/template")
public class PrintTemplateController extends JeecgController<PrintTemplate, IPrintTemplateService> { public class PrintTemplateController extends JeecgController<PrintTemplate, IPrintTemplateService> {
@Value("${print.network-printers:}")
private String networkPrinters;
@Autowired
private INativePrintTemplateImageAnalyzeService nativePrintTemplateImageAnalyzeService;
@Operation(summary = "打印模板-分页列表") @Operation(summary = "打印模板-分页列表")
@GetMapping(value = "/list") @GetMapping(value = "/list")
@@ -128,6 +164,35 @@ public class PrintTemplateController extends JeecgController<PrintTemplate, IPri
return Result.OK(service.getById(id)); return Result.OK(service.getById(id));
} }
@AutoLog(value = "打印模板-图片分析生成原生JSON")
@Operation(summary = "打印模板-上传图片分析为原生模板JSON前端传 imageBase64可接 OpenAI 兼容视觉模型)")
@PostMapping(value = "/analyzeImageForNative")
@RequiresPermissions("print:template:edit")
public Result<Map<String, Object>> analyzeImageForNative(@RequestBody Map<String, String> body) {
try {
String imageBase64 = body == null ? null : body.get("imageBase64");
if (StringUtils.isBlank(imageBase64)) {
return Result.error("imageBase64 不能为空");
}
String filename = body.get("filename");
String mime = body.get("mime");
byte[] bytes = decodeImageBase64(imageBase64);
return Result.OK(nativePrintTemplateImageAnalyzeService.analyzeBytes(bytes, mime, filename));
} catch (Exception e) {
log.error("图片分析失败", e);
return Result.error("图片分析失败:" + e.getMessage());
}
}
private static byte[] decodeImageBase64(String imageBase64) {
String s = StringUtils.trimToEmpty(imageBase64);
int comma = s.indexOf(',');
if (s.startsWith("data:") && comma > 0) {
s = s.substring(comma + 1);
}
return Base64.getDecoder().decode(s.replaceAll("\\s", ""));
}
@Operation(summary = "打印模板-通过编码查询") @Operation(summary = "打印模板-通过编码查询")
@GetMapping(value = "/queryByCode") @GetMapping(value = "/queryByCode")
@RequiresPermissions("print:template:list") @RequiresPermissions("print:template:list")
@@ -138,4 +203,270 @@ public class PrintTemplateController extends JeecgController<PrintTemplate, IPri
} }
return Result.OK(t); return Result.OK(t);
} }
@Operation(summary = "打印模板-查询可用打印机")
@GetMapping(value = "/queryPrinters")
@RequiresPermissions("print:template:list")
public Result<Map<String, Object>> queryPrinters() {
Map<String, Object> res = new HashMap<>(8);
List<String> serverPrinters = new ArrayList<>();
String serverDefaultPrinter = "";
try {
PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
if (services != null) {
for (PrintService service : services) {
if (service != null && StringUtils.isNotBlank(service.getName())) {
serverPrinters.add(service.getName().trim());
}
}
}
PrintService defaultService = PrintServiceLookup.lookupDefaultPrintService();
if (defaultService != null && StringUtils.isNotBlank(defaultService.getName())) {
serverDefaultPrinter = defaultService.getName().trim();
}
} catch (Exception e) {
log.warn("查询服务器打印机失败: {}", e.getMessage());
}
List<String> networkPrinterList =
StringUtils.isBlank(networkPrinters)
? new ArrayList<>()
: java.util.Arrays.stream(networkPrinters.split(","))
.map(String::trim)
.filter(StringUtils::isNotBlank)
.distinct()
.collect(Collectors.toList());
Map<String, Object> capability = new LinkedHashMap<>(4);
capability.put("localSupported", false);
capability.put("localReason", "浏览器环境无法直接枚举客户端本地打印机,需要本地组件或客户端程序配合。");
capability.put("serverSupported", true);
capability.put("networkSupported", true);
res.put("capability", capability);
res.put("serverPrinters", serverPrinters);
res.put("serverDefaultPrinter", serverDefaultPrinter);
res.put("networkPrinters", networkPrinterList);
return Result.OK(res);
}
@AutoLog(value = "打印模板-服务端直打")
@Operation(summary = "打印模板-服务端直打")
@PostMapping(value = "/directPrint")
@RequiresPermissions("print:template:list")
public Result<String> directPrint(@RequestBody Map<String, Object> body) {
String templateCode = String.valueOf(body.getOrDefault("templateCode", "")).trim();
String printerName = String.valueOf(body.getOrDefault("printerName", "")).trim();
Object dataJsonObj = body.get("dataJson");
String dataJsonText = dataJsonObj == null ? "" : String.valueOf(dataJsonObj);
if (StringUtils.isBlank(templateCode)) {
return Result.error("templateCode 不能为空");
}
if (StringUtils.isBlank(dataJsonText)) {
return Result.error("dataJson 不能为空");
}
PrintTemplate tpl = service.getByCode(templateCode);
if (tpl == null) {
return Result.error("模板不存在: " + templateCode);
}
try {
PrintService target = null;
if (StringUtils.isNotBlank(printerName) && !"__system_default__".equals(printerName)) {
PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
if (services != null) {
for (PrintService serviceItem : services) {
if (serviceItem != null && printerName.equalsIgnoreCase(String.valueOf(serviceItem.getName()).trim())) {
target = serviceItem;
break;
}
}
}
if (target == null) {
return Result.error("未找到指定打印机: " + printerName);
}
}
if (target == null) {
target = PrintServiceLookup.lookupDefaultPrintService();
}
if (target == null) {
return Result.error("未找到可用打印机,请检查服务器打印机配置");
}
// 说明:当前接口实现的是服务端直打(纯文本)。若需按 hiprint 模板渲染版式,建议接入独立渲染服务。
String content =
"QH-MES 快速打印\n模板编号: "
+ templateCode
+ "\n模板名称: "
+ String.valueOf(tpl.getTemplateName())
+ "\n\n数据JSON:\n"
+ dataJsonText
+ "\n";
final String[] lines = content.replace("\r\n", "\n").split("\n", -1);
PrinterJob job = PrinterJob.getPrinterJob();
job.setPrintService(target);
job.setJobName("QH-MES-" + templateCode);
job.setPrintable(
new Printable() {
@Override
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
Graphics2D g2 = (Graphics2D) graphics;
g2.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
g2.setFont(new Font("Microsoft YaHei", Font.PLAIN, 10));
int lineHeight = g2.getFontMetrics().getHeight() + 2;
int maxLinesPerPage = Math.max(1, (int) (pageFormat.getImageableHeight() / lineHeight));
int start = pageIndex * maxLinesPerPage;
if (start >= lines.length) {
return Printable.NO_SUCH_PAGE;
}
int end = Math.min(lines.length, start + maxLinesPerPage);
int y = g2.getFontMetrics().getAscent();
for (int i = start; i < end; i += 1) {
g2.drawString(lines[i], 0, y);
y += lineHeight;
}
return Printable.PAGE_EXISTS;
}
});
job.print();
return Result.OK("已提交到服务器打印机: " + target.getName());
} catch (Exception e) {
log.error("服务端直打失败", e);
return Result.error("服务端直打失败: " + e.getMessage());
}
}
@AutoLog(value = "打印模板-PDF后端打印")
@Operation(summary = "打印模板-PDF后端打印")
@PostMapping(value = "/directPrintPdf")
@RequiresPermissions("print:template:list")
public Result<String> directPrintPdf(@RequestBody Map<String, Object> body) {
String templateCode = String.valueOf(body.getOrDefault("templateCode", "")).trim();
String printerName = String.valueOf(body.getOrDefault("printerName", "")).trim();
String pdfBase64 = String.valueOf(body.getOrDefault("pdfBase64", "")).trim();
String fileName = String.valueOf(body.getOrDefault("fileName", "")).trim();
if (StringUtils.isBlank(templateCode)) {
return Result.error("templateCode 不能为空");
}
if (StringUtils.isBlank(pdfBase64)) {
return Result.error("pdfBase64 不能为空");
}
String lastResolvedPrinterLabel = null;
try {
PrintService target = resolvePrintService(printerName);
if (target == null) {
return Result.error("未找到可用打印机,请检查服务器打印机配置");
}
final String resolvedPrinterLabel = target.getName();
lastResolvedPrinterLabel = resolvedPrinterLabel;
String base64Body = pdfBase64;
int commaIdx = pdfBase64.indexOf(",");
if (pdfBase64.startsWith("data:") && commaIdx > 0) {
base64Body = pdfBase64.substring(commaIdx + 1);
}
byte[] pdfBytes = Base64.getDecoder().decode(base64Body);
String printJobName = StringUtils.isNotBlank(fileName) ? fileName : ("QH-MES-" + templateCode + ".pdf");
// 优先直送 PDF 字节,避免走 RasterPrinterJob虚拟打印机/无界面会话下易触发 PrinterAbortException
if (tryPrintPdfBytesWithDocFlavor(target, pdfBytes, printJobName)) {
return Result.OK("已提交PDF到服务器打印机: " + resolvedPrinterLabel);
}
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(pdfBytes))) {
PDFRenderer renderer = new PDFRenderer(document);
PrinterJob job = PrinterJob.getPrinterJob();
job.setPrintService(target);
job.setJobName(printJobName);
job.setPrintable(
(graphics, pageFormat, pageIndex) -> {
if (pageIndex >= document.getNumberOfPages()) {
return Printable.NO_SUCH_PAGE;
}
BufferedImage image;
try {
image = renderer.renderImageWithDPI(pageIndex, 150);
} catch (Exception ex) {
throw new PrinterException("PDF页面渲染失败: " + ex.getMessage());
}
Graphics2D g2 = (Graphics2D) graphics;
double imageableX = pageFormat.getImageableX();
double imageableY = pageFormat.getImageableY();
double imageableWidth = pageFormat.getImageableWidth();
double imageableHeight = pageFormat.getImageableHeight();
double scale =
Math.min(imageableWidth / image.getWidth(), imageableHeight / image.getHeight());
int drawWidth = (int) Math.round(image.getWidth() * scale);
int drawHeight = (int) Math.round(image.getHeight() * scale);
int drawX = (int) Math.round(imageableX + (imageableWidth - drawWidth) / 2);
int drawY = (int) Math.round(imageableY + (imageableHeight - drawHeight) / 2);
g2.drawImage(image, drawX, drawY, drawWidth, drawHeight, null);
return Printable.PAGE_EXISTS;
});
HashPrintRequestAttributeSet patts = new HashPrintRequestAttributeSet();
patts.add(new JobName(printJobName, Locale.getDefault()));
job.print(patts);
}
return Result.OK("已提交PDF到服务器打印机: " + resolvedPrinterLabel);
} catch (PrinterAbortException e) {
log.error("PDF后端打印失败(PrinterAbortException)", e);
return Result.error(buildPdfPrinterAbortHint(printerName, lastResolvedPrinterLabel));
} catch (Exception e) {
log.error("PDF后端打印失败", e);
return Result.error("PDF后端打印失败: " + e.getMessage());
}
}
/**
* 若打印机声明支持 application/pdf则通过 DocPrintJob 提交,通常比 AWT 栅格化更稳定。
*/
private boolean tryPrintPdfBytesWithDocFlavor(PrintService printService, byte[] pdfBytes, String jobName) {
DocFlavor flavor = new DocFlavor.INPUT_STREAM("application/pdf");
if (!printService.isDocFlavorSupported(flavor)) {
return false;
}
try {
DocPrintJob docJob = printService.createPrintJob();
ByteArrayInputStream in = new ByteArrayInputStream(pdfBytes);
Doc doc = new SimpleDoc(in, flavor, null);
HashPrintRequestAttributeSet attrs = new HashPrintRequestAttributeSet();
if (StringUtils.isNotBlank(jobName)) {
attrs.add(new JobName(jobName, Locale.getDefault()));
}
docJob.print(doc, attrs);
return true;
} catch (PrintException e) {
log.warn("PDF DocFlavor 直送失败,将回退为位图渲染: {} - {}", printService.getName(), e.getMessage());
return false;
}
}
private static String buildPdfPrinterAbortHint(String requestedPrinterName, String resolvedPrintQueueName) {
StringBuilder sb = new StringBuilder();
sb.append("打印任务被系统取消PrinterAbortException。常见原因");
sb.append("1) 默认或所选为「Microsoft Print to PDF」等虚拟打印机在 Tomcat 等服务进程无交互桌面时无法弹出保存对话框,作业会被中止——请安装实体打印机并在前端指定 printerName");
sb.append("2) 打印机离线、队列暂停、缺纸或驱动报错;");
sb.append("3) 运行服务的 Windows 账户无权访问打印队列。");
if (StringUtils.isNotBlank(resolvedPrintQueueName)) {
sb.append(" 当前实际使用的打印队列: ").append(resolvedPrintQueueName.trim()).append("");
}
if (StringUtils.isNotBlank(requestedPrinterName) && !"__system_default__".equalsIgnoreCase(requestedPrinterName.trim())) {
sb.append(" 请求参数 printerName: ").append(requestedPrinterName.trim()).append("");
}
return sb.toString();
}
private PrintService resolvePrintService(String printerName) {
PrintService target = null;
if (StringUtils.isNotBlank(printerName) && !"__system_default__".equals(printerName)) {
PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
if (services != null) {
for (PrintService serviceItem : services) {
if (serviceItem != null
&& printerName.equalsIgnoreCase(String.valueOf(serviceItem.getName()).trim())) {
target = serviceItem;
break;
}
}
}
}
if (target == null) {
target = PrintServiceLookup.lookupDefaultPrintService();
}
return target;
}
} }

View File

@@ -355,3 +355,21 @@ justauth:
type: default type: default
prefix: 'demo::' prefix: 'demo::'
timeout: 1h timeout: 1h
# 打印模块配置
print:
# 预配置网络打印机(英文逗号分隔),例如:\\192.168.1.100\Zebra01,IPP_Printer_A
network-printers: ""
# 原生设计器「上传图片分析模板」OpenAI Chat Completions 兼容接口(通义用 compatible-mode 地址)
native-template-ai:
enabled: true
# api-key 生效方式(任选其一,否则解析结果为空,会走「占位模板」):
# ① 环境变量 DASHSCOPE_API_KEY=sk-xxx
# ② Spring 对应该项的标准环境变量名PRINT_NATIVE_TEMPLATE_AI_API_KEY=sk-xxxIDE「运行配置」里加
# ③ 本机调试可直接写明文(勿提交 Gitapi-key: sk-你的DashScope密钥
api-key: sk-b62f922d63cb420d999a8c8974e00218
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
# 通义视觉qwen-vl-plus / qwen-vl-max 等;勿填 gpt-4o-mini那是 OpenAI 模型名,仅 base-url=api.openai.com 时可用)
model: qwen-vl-plus
max-image-mb: 8
http-timeout-seconds: 120

View File

@@ -0,0 +1,169 @@
-- 打印模板分类字典 + 纸张规格字典幂等初始化
-- 分类字典
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), '打印模板分类', 'print_template_category', '打印模板分类', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (
SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'print_template_category' AND `del_flag` = 0
);
-- 纸张规格字典
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), '打印纸张规格', 'print_paper_preset', '打印纸张规格预设', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (
SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'print_paper_preset' AND `del_flag` = 0
);
-- 分类字典项
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '条码', 'barcode', 1, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_template_category'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'barcode');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '标签', 'label', 2, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_template_category'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'label');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '快递面单', 'waybill', 3, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_template_category'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'waybill');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '吊牌', 'hangtag', 4, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_template_category'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'hangtag');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '物料卡', 'materialCard', 5, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_template_category'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'materialCard');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '箱唛', 'cartonMark', 6, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_template_category'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'cartonMark');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '质检单', 'qc', 7, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_template_category'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'qc');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '入库单', 'inbound', 8, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_template_category'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'inbound');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '出库单', 'outbound', 9, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_template_category'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'outbound');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '工单', 'workOrder', 10, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_template_category'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'workOrder');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '表单套打', 'form', 11, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_template_category'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'form');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '报表', 'report', 12, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_template_category'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'report');
-- 纸张规格字典项
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, 'A4(210×297)', 'A4', 1, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_paper_preset'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'A4');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, 'A5(148×210)', 'A5', 2, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_paper_preset'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'A5');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, 'A6(105×148)', 'A6', 3, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_paper_preset'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'A6');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, 'B5(176×250)', 'B5', 4, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_paper_preset'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'B5');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, 'B6(125×176)', 'B6', 5, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_paper_preset'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'B6');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '10×12mm超小条码', 'L_10_12', 6, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_paper_preset'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'L_10_12');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '20×10mm珠宝标签', 'L_20_10', 7, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_paper_preset'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'L_20_10');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '25×15mm小标签', 'L_25_15', 8, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_paper_preset'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'L_25_15');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '30×20mm小标签', 'L_30_20', 9, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_paper_preset'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'L_30_20');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '40×30mm小条码', 'L_40_30', 10, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_paper_preset'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'L_40_30');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '50×30mm药盒/资产', 'L_50_30', 11, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_paper_preset'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'L_50_30');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '60×40mm常用条码', 'L_60_40', 12, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_paper_preset'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'L_60_40');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '70×50mm物流标签', 'L_70_50', 13, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_paper_preset'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'L_70_50');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '80×50mm物流标签', 'L_80_50', 14, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_paper_preset'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'L_80_50');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '90×60mm物流标签', 'L_90_60', 15, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_paper_preset'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'L_90_60');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '100×70mm物流标签', 'L_100_70', 16, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_paper_preset'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'L_100_70');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '100×150mm快递热敏', 'L_100_150', 17, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_paper_preset'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'L_100_150');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '100×180mm快递热敏', 'L_100_180', 18, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'print_paper_preset'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'L_100_180');

View File

@@ -39,8 +39,10 @@
"enquire.js": "^2.1.6", "enquire.js": "^2.1.6",
"event-source-polyfill": "^1.0.31", "event-source-polyfill": "^1.0.31",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"html2canvas": "^1.4.1",
"intro.js": "^7.2.0", "intro.js": "^7.2.0",
"jquery": "^3.7.1", "jquery": "^3.7.1",
"jspdf": "^4.2.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
@@ -4721,6 +4723,11 @@
"integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==", "integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==",
"dev": true "dev": true
}, },
"node_modules/@types/pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw=="
},
"node_modules/@types/pinyin": { "node_modules/@types/pinyin": {
"version": "2.10.2", "version": "2.10.2",
"resolved": "https://registry.npmjs.org/@types/pinyin/-/pinyin-2.10.2.tgz", "resolved": "https://registry.npmjs.org/@types/pinyin/-/pinyin-2.10.2.tgz",
@@ -4833,7 +4840,7 @@
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true "devOptional": true
}, },
"node_modules/@types/web-bluetooth": { "node_modules/@types/web-bluetooth": {
"version": "0.0.20", "version": "0.0.20",
@@ -8744,10 +8751,13 @@
} }
}, },
"node_modules/dompurify": { "node_modules/dompurify": {
"version": "2.5.9", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.9.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz",
"integrity": "sha512-i6mvVmWN4xo9LrhCOZrDgSs9noW6nOahbrmzjRbPF36YPyj5Ue5lgok0MHDWkG7xzpWFO2OYttXdzM7rJxHvNA==", "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==",
"optional": true "optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
}, },
"node_modules/domutils": { "node_modules/domutils": {
"version": "3.2.2", "version": "3.2.2",
@@ -10259,6 +10269,16 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true "dev": true
}, },
"node_modules/fast-png": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
"dependencies": {
"@types/pako": "^2.0.3",
"iobuffer": "^5.3.2",
"pako": "^2.1.0"
}
},
"node_modules/fast-uri": { "node_modules/fast-uri": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
@@ -11647,7 +11667,6 @@
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"optional": true,
"dependencies": { "dependencies": {
"css-line-break": "^2.1.0", "css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3" "text-segmentation": "^1.0.3"
@@ -11942,6 +11961,11 @@
"resolved": "https://registry.npmjs.org/intro.js/-/intro.js-7.2.0.tgz", "resolved": "https://registry.npmjs.org/intro.js/-/intro.js-7.2.0.tgz",
"integrity": "sha512-qbMfaB70rOXVBceIWNYnYTpVTiZsvQh/MIkfdQbpA9di9VBfj1GigUPfcCv3aOfsbrtPcri8vTLTA4FcEDcHSQ==" "integrity": "sha512-qbMfaB70rOXVBceIWNYnYTpVTiZsvQh/MIkfdQbpA9di9VBfj1GigUPfcCv3aOfsbrtPcri8vTLTA4FcEDcHSQ=="
}, },
"node_modules/iobuffer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA=="
},
"node_modules/is-accessor-descriptor": { "node_modules/is-accessor-descriptor": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz",
@@ -13472,19 +13496,18 @@
} }
}, },
"node_modules/jspdf": { "node_modules/jspdf": {
"version": "2.5.2", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz", "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz",
"integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==", "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.23.2", "@babel/runtime": "^7.28.6",
"atob": "^2.1.2", "fast-png": "^6.2.0",
"btoa": "^1.2.1",
"fflate": "^0.8.1" "fflate": "^0.8.1"
}, },
"optionalDependencies": { "optionalDependencies": {
"canvg": "^3.0.6", "canvg": "^3.0.11",
"core-js": "^3.6.0", "core-js": "^3.6.0",
"dompurify": "^2.5.4", "dompurify": "^3.3.1",
"html2canvas": "^1.0.0-rc.5" "html2canvas": "^1.0.0-rc.5"
} }
}, },
@@ -15761,6 +15784,11 @@
"integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==",
"dev": true "dev": true
}, },
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
},
"node_modules/param-case": { "node_modules/param-case": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
@@ -21826,6 +21854,29 @@
"node": ">=16" "node": ">=16"
} }
}, },
"node_modules/vue-plugin-hiprint/node_modules/dompurify": {
"version": "2.5.9",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.9.tgz",
"integrity": "sha512-i6mvVmWN4xo9LrhCOZrDgSs9noW6nOahbrmzjRbPF36YPyj5Ue5lgok0MHDWkG7xzpWFO2OYttXdzM7rJxHvNA==",
"optional": true
},
"node_modules/vue-plugin-hiprint/node_modules/jspdf": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz",
"integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==",
"dependencies": {
"@babel/runtime": "^7.23.2",
"atob": "^2.1.2",
"btoa": "^1.2.1",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.6",
"core-js": "^3.6.0",
"dompurify": "^2.5.4",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/vue-print-nb-jeecg": { "node_modules/vue-print-nb-jeecg": {
"version": "1.0.13", "version": "1.0.13",
"resolved": "https://registry.npmjs.org/vue-print-nb-jeecg/-/vue-print-nb-jeecg-1.0.13.tgz", "resolved": "https://registry.npmjs.org/vue-print-nb-jeecg/-/vue-print-nb-jeecg-1.0.13.tgz",

View File

@@ -25,6 +25,10 @@
"dependencies": { "dependencies": {
"@ant-design/colors": "^7.2.1", "@ant-design/colors": "^7.2.1",
"@ant-design/icons-vue": "^7.0.1", "@ant-design/icons-vue": "^7.0.1",
"@fontsource/noto-sans-sc": "^5.2.9",
"@fontsource/noto-serif-sc": "^5.2.9",
"@fontsource/open-sans": "^5.2.7",
"@fontsource/roboto": "^5.2.10",
"@iconify/iconify": "^3.1.1", "@iconify/iconify": "^3.1.1",
"@jeecg/aiflow": "3.9.1-beta", "@jeecg/aiflow": "3.9.1-beta",
"@jeecg/online": "3.9.1-beta", "@jeecg/online": "3.9.1-beta",
@@ -53,8 +57,11 @@
"enquire.js": "^2.1.6", "enquire.js": "^2.1.6",
"event-source-polyfill": "^1.0.31", "event-source-polyfill": "^1.0.31",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"html2canvas": "^1.4.1",
"intro.js": "^7.2.0", "intro.js": "^7.2.0",
"jquery": "^3.7.1", "jquery": "^3.7.1",
"jsbarcode": "^3.12.3",
"jspdf": "^4.2.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",

View File

@@ -14,6 +14,18 @@ importers:
'@ant-design/icons-vue': '@ant-design/icons-vue':
specifier: ^7.0.1 specifier: ^7.0.1
version: 7.0.1(vue@3.5.32(typescript@5.9.3)) version: 7.0.1(vue@3.5.32(typescript@5.9.3))
'@fontsource/noto-sans-sc':
specifier: ^5.2.9
version: 5.2.9
'@fontsource/noto-serif-sc':
specifier: ^5.2.9
version: 5.2.9
'@fontsource/open-sans':
specifier: ^5.2.7
version: 5.2.7
'@fontsource/roboto':
specifier: ^5.2.10
version: 5.2.10
'@iconify/iconify': '@iconify/iconify':
specifier: ^3.1.1 specifier: ^3.1.1
version: 3.1.1 version: 3.1.1
@@ -98,12 +110,21 @@ importers:
highlight.js: highlight.js:
specifier: ^11.11.1 specifier: ^11.11.1
version: 11.11.1 version: 11.11.1
html2canvas:
specifier: ^1.4.1
version: 1.4.1
intro.js: intro.js:
specifier: ^7.2.0 specifier: ^7.2.0
version: 7.2.0 version: 7.2.0
jquery: jquery:
specifier: ^3.7.1 specifier: ^3.7.1
version: 3.7.1 version: 3.7.1
jsbarcode:
specifier: ^3.12.3
version: 3.12.3
jspdf:
specifier: ^4.2.1
version: 4.2.1
lodash-es: lodash-es:
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.18.1 version: 4.18.1
@@ -1590,6 +1611,18 @@ packages:
resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
'@fontsource/noto-sans-sc@5.2.9':
resolution: {integrity: sha512-bTUIWGBgJDpwi5qAr+x0/lcgv80IHTB9vl6s2f6EymZEa7qYV99yNRBZuKFT+SYDKVunZrjCEhWtpxqmbXWl5Q==}
'@fontsource/noto-serif-sc@5.2.9':
resolution: {integrity: sha512-+wosdNmyrtRjnqJ/4A6i99jApm6ye34n8Nh7jUO3P/+6Hd1ZBY1gB9yGaI5jVxb56RS1lM2kTc0DzX1ifRO2MA==}
'@fontsource/open-sans@5.2.7':
resolution: {integrity: sha512-8yfgDYjE5O0vmTPdrcjV35y4yMnctsokmi9gN49Gcsr0sjzkMkR97AnKDe6OqZh2SFkYlR28fxOvi21bYEgMSw==}
'@fontsource/roboto@5.2.10':
resolution: {integrity: sha512-8HlA5FtSfz//oFSr2eL7GFXAiE7eIkcGOtx7tjsLKq+as702x9+GU7K95iDeWFapHC4M2hv9RrpXKRTGGBI8Zg==}
'@humanwhocodes/config-array@0.13.0': '@humanwhocodes/config-array@0.13.0':
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
engines: {node: '>=10.10.0'} engines: {node: '>=10.10.0'}
@@ -1906,67 +1939,56 @@ packages:
resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==} resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.52.5': '@rollup/rollup-linux-arm-musleabihf@4.52.5':
resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==} resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.52.5': '@rollup/rollup-linux-arm64-gnu@4.52.5':
resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==} resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.52.5': '@rollup/rollup-linux-arm64-musl@4.52.5':
resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==} resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.52.5': '@rollup/rollup-linux-loong64-gnu@4.52.5':
resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==} resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.52.5': '@rollup/rollup-linux-ppc64-gnu@4.52.5':
resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==} resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.52.5': '@rollup/rollup-linux-riscv64-gnu@4.52.5':
resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==} resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.52.5': '@rollup/rollup-linux-riscv64-musl@4.52.5':
resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==} resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.52.5': '@rollup/rollup-linux-s390x-gnu@4.52.5':
resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==} resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.52.5': '@rollup/rollup-linux-x64-gnu@4.52.5':
resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==} resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.52.5': '@rollup/rollup-linux-x64-musl@4.52.5':
resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==} resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-openharmony-arm64@4.52.5': '@rollup/rollup-openharmony-arm64@4.52.5':
resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==} resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==}
@@ -2127,6 +2149,9 @@ packages:
'@types/nprogress@0.2.3': '@types/nprogress@0.2.3':
resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==} resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==}
'@types/pako@2.0.4':
resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==}
'@types/pinyin@2.10.2': '@types/pinyin@2.10.2':
resolution: {integrity: sha512-jLzlRkaLRLg+lgYPjOuP3HX2cozUkhXls5GTXopsKuKJ9lDGlIAb88OoIztH6TbNUsoJnl/7e/kjaumA5IKKJg==} resolution: {integrity: sha512-jLzlRkaLRLg+lgYPjOuP3HX2cozUkhXls5GTXopsKuKJ9lDGlIAb88OoIztH6TbNUsoJnl/7e/kjaumA5IKKJg==}
@@ -3486,6 +3511,9 @@ packages:
dompurify@2.5.9: dompurify@2.5.9:
resolution: {integrity: sha512-i6mvVmWN4xo9LrhCOZrDgSs9noW6nOahbrmzjRbPF36YPyj5Ue5lgok0MHDWkG7xzpWFO2OYttXdzM7rJxHvNA==} resolution: {integrity: sha512-i6mvVmWN4xo9LrhCOZrDgSs9noW6nOahbrmzjRbPF36YPyj5Ue5lgok0MHDWkG7xzpWFO2OYttXdzM7rJxHvNA==}
dompurify@3.3.3:
resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==}
domutils@1.7.0: domutils@1.7.0:
resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==}
@@ -3969,6 +3997,9 @@ packages:
fast-levenshtein@2.0.6: fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
fast-png@6.4.0:
resolution: {integrity: sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==}
fast-uri@3.1.0: fast-uri@3.1.0:
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
@@ -4194,6 +4225,7 @@ packages:
glob@11.1.0: glob@11.1.0:
resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true hasBin: true
glob@7.2.3: glob@7.2.3:
@@ -4491,6 +4523,9 @@ packages:
intro.js@7.2.0: intro.js@7.2.0:
resolution: {integrity: sha512-qbMfaB70rOXVBceIWNYnYTpVTiZsvQh/MIkfdQbpA9di9VBfj1GigUPfcCv3aOfsbrtPcri8vTLTA4FcEDcHSQ==} resolution: {integrity: sha512-qbMfaB70rOXVBceIWNYnYTpVTiZsvQh/MIkfdQbpA9di9VBfj1GigUPfcCv3aOfsbrtPcri8vTLTA4FcEDcHSQ==}
iobuffer@5.4.0:
resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==}
is-accessor-descriptor@1.0.1: is-accessor-descriptor@1.0.1:
resolution: {integrity: sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==} resolution: {integrity: sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
@@ -5003,6 +5038,9 @@ packages:
jspdf@2.5.2: jspdf@2.5.2:
resolution: {integrity: sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==} resolution: {integrity: sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==}
jspdf@4.2.1:
resolution: {integrity: sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==}
katex@0.16.45: katex@0.16.45:
resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==} resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==}
hasBin: true hasBin: true
@@ -5624,6 +5662,9 @@ packages:
package-manager-detector@1.6.0: package-manager-detector@1.6.0:
resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
pako@2.1.0:
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
param-case@3.0.4: param-case@3.0.4:
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
@@ -8653,6 +8694,14 @@ snapshots:
'@eslint/js@8.57.1': {} '@eslint/js@8.57.1': {}
'@fontsource/noto-sans-sc@5.2.9': {}
'@fontsource/noto-serif-sc@5.2.9': {}
'@fontsource/open-sans@5.2.7': {}
'@fontsource/roboto@5.2.10': {}
'@humanwhocodes/config-array@0.13.0': '@humanwhocodes/config-array@0.13.0':
dependencies: dependencies:
'@humanwhocodes/object-schema': 2.0.3 '@humanwhocodes/object-schema': 2.0.3
@@ -9290,6 +9339,8 @@ snapshots:
'@types/nprogress@0.2.3': {} '@types/nprogress@0.2.3': {}
'@types/pako@2.0.4': {}
'@types/pinyin@2.10.2': {} '@types/pinyin@2.10.2': {}
'@types/qrcode@1.5.6': '@types/qrcode@1.5.6':
@@ -10886,6 +10937,11 @@ snapshots:
dompurify@2.5.9: dompurify@2.5.9:
optional: true optional: true
dompurify@3.3.3:
optionalDependencies:
'@types/trusted-types': 2.0.7
optional: true
domutils@1.7.0: domutils@1.7.0:
dependencies: dependencies:
dom-serializer: 0.2.2 dom-serializer: 0.2.2
@@ -11471,6 +11527,12 @@ snapshots:
fast-levenshtein@2.0.6: {} fast-levenshtein@2.0.6: {}
fast-png@6.4.0:
dependencies:
'@types/pako': 2.0.4
iobuffer: 5.4.0
pako: 2.1.0
fast-uri@3.1.0: {} fast-uri@3.1.0: {}
fastest-levenshtein@1.0.16: {} fastest-levenshtein@1.0.16: {}
@@ -11916,7 +11978,6 @@ snapshots:
dependencies: dependencies:
css-line-break: 2.1.0 css-line-break: 2.1.0
text-segmentation: 1.0.3 text-segmentation: 1.0.3
optional: true
htmlparser2@10.1.0: htmlparser2@10.1.0:
dependencies: dependencies:
@@ -12068,6 +12129,8 @@ snapshots:
intro.js@7.2.0: {} intro.js@7.2.0: {}
iobuffer@5.4.0: {}
is-accessor-descriptor@1.0.1: is-accessor-descriptor@1.0.1:
dependencies: dependencies:
hasown: 2.0.2 hasown: 2.0.2
@@ -12731,6 +12794,17 @@ snapshots:
dompurify: 2.5.9 dompurify: 2.5.9
html2canvas: 1.4.1 html2canvas: 1.4.1
jspdf@4.2.1:
dependencies:
'@babel/runtime': 7.29.2
fast-png: 6.4.0
fflate: 0.8.2
optionalDependencies:
canvg: 3.0.11
core-js: 3.49.0
dompurify: 3.3.3
html2canvas: 1.4.1
katex@0.16.45: katex@0.16.45:
dependencies: dependencies:
commander: 8.3.0 commander: 8.3.0
@@ -13368,6 +13442,8 @@ snapshots:
package-manager-detector@1.6.0: {} package-manager-detector@1.6.0: {}
pako@2.1.0: {}
param-case@3.0.4: param-case@3.0.4:
dependencies: dependencies:
dot-case: 3.0.4 dot-case: 3.0.4

View File

@@ -20,4 +20,50 @@ export const AI_ROUTE: AppRouteRecordRaw = {
], ],
}; };
export const staticRoutesList = [AI_ROUTE]; export const PRINT_DESIGNER_ROUTE: AppRouteRecordRaw = {
path: '',
name: 'print-designer-parent',
component: LAYOUT,
meta: {
title: 'print-designer',
hideMenu: true,
hideChildrenInMenu: true,
},
children: [
{
path: '/print/designer',
name: 'print-designer',
component: () => import('/@/views/print/template/PrintDesigner.vue'),
meta: {
title: '打印设计器',
hideMenu: true,
hideTab: false,
},
},
],
};
export const PRINT_NATIVE_DESIGNER_ROUTE: AppRouteRecordRaw = {
path: '',
name: 'print-native-designer-parent',
component: LAYOUT,
meta: {
title: 'print-native-designer',
hideMenu: true,
hideChildrenInMenu: true,
},
children: [
{
path: '/print/native-designer',
name: 'print-native-designer',
component: () => import('/@/views/print/template/native/NativePrintDesigner.vue'),
meta: {
title: '原生打印设计器',
hideMenu: true,
hideTab: false,
},
},
],
};
export const staticRoutesList = [AI_ROUTE, PRINT_DESIGNER_ROUTE, PRINT_NATIVE_DESIGNER_ROUTE];

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,724 @@
<template>
<a-modal
v-model:open="innerOpen"
title="模板预览"
width="1240px"
:footer="null"
:body-style="{ padding: '16px 20px 22px' }"
destroy-on-close
wrap-class-name="native-template-list-preview-modal"
@cancel="onClose"
>
<a-spin :spinning="loading">
<div v-if="errorText" class="native-preview-error">{{ errorText }}</div>
<a-row v-else-if="schema" :gutter="20" class="native-preview-row">
<a-col :xs="24" :lg="11" class="native-preview-left">
<a-tabs v-model:activeKey="jsonTabKey" size="small" class="json-tabs">
<a-tab-pane key="template" tab="模板JSON">
<div class="json-template-pane">
<div class="json-box-title">画布实际 JSON模板样式</div>
<a-textarea v-model:value="canvasJsonText" :rows="16" class="json-textarea" placeholder="模板 JSON" />
</div>
</a-tab-pane>
<a-tab-pane key="params" tab="参数JSON">
<div class="params-json-pane">
<div class="params-json-head">
<div class="json-sub-tabs json-sub-tabs--segmented" role="tablist" aria-label="参数数据来源">
<button
type="button"
role="tab"
:class="['capsule-tab', { 'is-active': dataSourceType === 'manual' }]"
@click="dataSourceType = 'manual'"
>
手动JSON
</button>
<button
type="button"
role="tab"
:class="['capsule-tab', { 'is-active': dataSourceType === 'mock' }]"
@click="dataSourceType = 'mock'"
>
模拟JSON
</button>
</div>
<a-button
v-show="dataSourceType === 'mock'"
size="small"
type="primary"
ghost
class="json-capsule-btn"
@click="onGenerateMock"
>
根据画布生成
</a-button>
</div>
<a-textarea
v-show="dataSourceType === 'manual'"
v-model:value="previewDataText"
:rows="14"
class="json-textarea json-textarea--main"
placeholder="手动输入预览数据 JSON"
/>
<template v-if="dataSourceType === 'mock'">
<a-textarea
v-model:value="mockDataText"
:rows="12"
class="json-textarea json-textarea--main"
placeholder="点击「根据画布生成」自动填充模拟数据"
/>
</template>
</div>
</a-tab-pane>
</a-tabs>
</a-col>
<a-col :xs="24" :lg="13" class="native-preview-right">
<div class="preview-header-row">
<div class="preview-title">预览样式</div>
<a-space v-if="previewHtml" size="small" align="center" wrap class="preview-header-actions" @click.stop>
<a-tooltip title="缩小">
<a-button type="default" size="small" class="preview-zoom-btn" @click="zoomPreviewOut">
<Icon icon="ant-design:zoom-out-outlined" />
</a-button>
</a-tooltip>
<span class="preview-zoom-label">{{ zoomPercentLabel }}</span>
<a-tooltip title="放大">
<a-button type="default" size="small" class="preview-zoom-btn" @click="zoomPreviewIn">
<Icon icon="ant-design:zoom-in-outlined" />
</a-button>
</a-tooltip>
<a-tooltip title="恢复按窗口适应">
<a-button type="default" size="small" class="preview-zoom-fit-btn" @click="resetPreviewZoom">适应</a-button>
</a-tooltip>
<a-tooltip title="使用浏览器打印当前预览内容">
<a-button type="primary" size="small" class="preview-print-btn" @click="handleBrowserPrint">
<Icon icon="ant-design:printer-outlined" />
打印
</a-button>
</a-tooltip>
</a-space>
</div>
<div v-if="layoutPaperPx" ref="previewHostRef" class="preview-frame-wrap">
<template v-if="previewHtml">
<!-- 内层用 margin:auto放大后父级可四向滚动避免 flex 居中导致裁切 -->
<div class="preview-scroll-flex">
<div class="preview-zoom-slot">
<div
class="preview-scale-shim"
:style="{
width: `${layoutPaperPx.wPx * previewDisplayScale}px`,
height: `${layoutPaperPx.hPx * previewDisplayScale}px`,
}"
>
<div
class="preview-scale-inner"
:style="{
width: `${layoutPaperPx.wPx}px`,
height: `${layoutPaperPx.hPx}px`,
transform: `scale(${previewDisplayScale})`,
}"
>
<iframe
ref="previewIframeRef"
class="preview-iframe"
title="原生模板预览"
:srcdoc="previewHtml"
:style="{
width: `${layoutPaperPx.wPx}px`,
height: `${layoutPaperPx.hPx}px`,
}"
@load="onPreviewIframeLoad"
/>
</div>
</div>
</div>
</div>
</template>
<a-empty v-else description="正在生成预览…" />
</div>
</a-col>
</a-row>
</a-spin>
</a-modal>
</template>
<script lang="ts" setup>
import { computed, nextTick, ref, watch } from 'vue';
import { useDebounceFn, useResizeObserver } from '@vueuse/core';
import { Icon } from '/@/components/Icon';
import { useMessage } from '/@/hooks/web/useMessage';
import { queryById } from '../printTemplate.api';
import { renderNativePrintHtml, resolvePrintPageCount } from '../native/core/printRenderer';
import { stringifyNativeTemplateStyle } from '../native/core/nativeTemplateStyleSerialize';
import { generateNativeMockDataObject } from '../native/core/nativeMockData';
import { normalizeImportedNativeSchema } from '../native/core/nativeSchemaNormalize';
import type { NativeTemplateSchema } from '../native/core/types';
const props = defineProps<{
open: boolean;
/** 打印模板主键 id */
templateId: string | null;
}>();
const emit = defineEmits<{
(e: 'update:open', v: boolean): void;
}>();
const { createMessage } = useMessage();
const innerOpen = computed({
get: () => props.open,
set: (v: boolean) => emit('update:open', v),
});
const loading = ref(false);
const errorText = ref('');
const schema = ref<NativeTemplateSchema | null>(null);
const canvasJsonText = ref('{}');
const jsonTabKey = ref<'template' | 'params'>('template');
const dataSourceType = ref<'manual' | 'mock'>('mock');
const previewDataText = ref('{}');
const mockDataText = ref('{}');
const previewHtml = ref('');
/** 预览区容器,用于根据可用空间计算缩放比 */
const previewHostRef = ref<HTMLElement | null>(null);
const previewIframeRef = ref<HTMLIFrameElement | null>(null);
/** 自动适应窗口时的缩放系数(相对纸张像素,通常 ≤1 */
const autoFitScale = ref(1);
/** 用户在「适应」基础上的缩放倍数1 表示与自动适应一致 */
const zoomMultiplier = ref(1);
const ZOOM_STEP = 1.15;
const ZOOM_MULT_MIN = 0.25;
const ZOOM_MULT_MAX = 4;
/** 最终用于 transform 的缩放 = 自动适应 × 手动倍数,并做安全上下限 */
const previewDisplayScale = computed(() => {
const raw = autoFitScale.value * zoomMultiplier.value;
if (!Number.isFinite(raw) || raw <= 0) {
return 1;
}
return Math.min(4, Math.max(0.08, raw));
});
/** 相对「适应窗口」的百分比,便于用户理解当前放大程度 */
const zoomPercentLabel = computed(() => `${Math.round(zoomMultiplier.value * 100)}%`);
/** CSS mm 转 px与浏览器常规 96dpi 一致) */
const MM_TO_CSS_PX = 96 / 25.4;
const activeDataText = computed(() => (dataSourceType.value === 'mock' ? mockDataText.value : previewDataText.value));
const previewData = computed(() => {
try {
return JSON.parse(activeDataText.value || '{}');
} catch (_e) {
return {};
}
});
/** 按纸张 mm × 页数换算为像素理论下限auto 表格等实际可能更高,见 contentMeasurePx */
const paperPixelSize = computed(() => {
if (!schema.value) {
return null;
}
const pageCount = Math.max(1, resolvePrintPageCount(schema.value, previewData.value));
const wMm = Number(schema.value.page?.width || 210);
const hMm = Number(schema.value.page?.height || 297);
const wPx = wMm * MM_TO_CSS_PX;
const hPx = hMm * pageCount * MM_TO_CSS_PX;
return { wPx, hPx, pageCount };
});
/** iframe 内实际排版测量(解决 autoPage 表格超出单页高度后仍被裁切) */
const contentMeasurePx = ref({ w: 0, h: 0 });
/** 预览/打印用的版面像素:取理论纸张与实测内容的较大值 */
const layoutPaperPx = computed(() => {
const ps = paperPixelSize.value;
if (!ps) {
return null;
}
const m = contentMeasurePx.value;
return {
...ps,
wPx: Math.max(ps.wPx, m.w || 0),
hPx: Math.max(ps.hPx, m.h || 0),
};
});
/** 遍历 iframe 文档与 scrollHeight估算真实内容宽高 */
function measureIframeContentBox(doc: Document): { w: number; h: number } {
const body = doc.body;
let minTop = Infinity;
let maxBottom = 0;
let minLeft = Infinity;
let maxRight = 0;
const visit = (el: Element) => {
if (el instanceof HTMLElement) {
const r = el.getBoundingClientRect();
if (r.height > 0.5 && r.width > 0.5) {
minTop = Math.min(minTop, r.top);
maxBottom = Math.max(maxBottom, r.bottom);
minLeft = Math.min(minLeft, r.left);
maxRight = Math.max(maxRight, r.right);
}
}
for (let i = 0; i < el.children.length; i++) {
visit(el.children[i]);
}
};
visit(body);
const byRect =
Number.isFinite(minTop) && maxBottom > 0
? { w: Math.ceil(maxRight - minLeft + 6), h: Math.ceil(maxBottom - minTop + 12) }
: { w: 0, h: 0 };
const sh = Math.max(doc.documentElement.scrollHeight, body.scrollHeight, byRect.h);
const sw = Math.max(doc.documentElement.scrollWidth, body.scrollWidth, byRect.w);
return { w: Math.ceil(sw), h: Math.ceil(sh) };
}
function updateContentMeasure() {
void nextTick(() => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
const doc = previewIframeRef.value?.contentDocument;
if (!doc?.body) {
contentMeasurePx.value = { w: 0, h: 0 };
scheduleComputeScale();
return;
}
contentMeasurePx.value = measureIframeContentBox(doc);
scheduleComputeScale();
});
});
});
}
async function refreshPreview() {
if (!schema.value) {
previewHtml.value = '';
return;
}
try {
previewHtml.value = await renderNativePrintHtml(schema.value, previewData.value);
} catch (e: any) {
previewHtml.value = '';
createMessage.error(`预览渲染失败:${e?.message || '未知错误'}`);
}
}
const debouncedRefreshPreview = useDebounceFn(() => void refreshPreview(), 320);
function computePreviewScale() {
const host = previewHostRef.value;
const ps = layoutPaperPx.value;
if (!host || !ps) {
autoFitScale.value = 1;
return;
}
const pad = 20;
const availW = Math.max(0, host.clientWidth - pad);
const availH = Math.max(0, host.clientHeight - pad);
if (availW <= 0 || availH <= 0 || ps.wPx <= 0 || ps.hPx <= 0) {
autoFitScale.value = 1;
return;
}
const raw = Math.min(availW / ps.wPx, availH / ps.hPx, 1) * 0.96;
autoFitScale.value = Number.isFinite(raw) && raw > 0 ? raw : 1;
}
function zoomPreviewIn() {
zoomMultiplier.value = Math.min(ZOOM_MULT_MAX, zoomMultiplier.value * ZOOM_STEP);
scheduleScrollPreviewCenter();
}
function zoomPreviewOut() {
zoomMultiplier.value = Math.max(ZOOM_MULT_MIN, zoomMultiplier.value / ZOOM_STEP);
scheduleScrollPreviewCenter();
}
/** 缩放后把可视区域滚到预览块大致居中,便于看到全貌 */
function scheduleScrollPreviewCenter() {
void nextTick(() => {
const host = previewHostRef.value;
if (!host) {
return;
}
const slot = host.querySelector('.preview-zoom-slot') as HTMLElement | null;
if (!slot) {
return;
}
const sr = slot.getBoundingClientRect();
const hr = host.getBoundingClientRect();
const nextLeft = host.scrollLeft + (sr.left - hr.left) + sr.width / 2 - host.clientWidth / 2;
const nextTop = host.scrollTop + (sr.top - hr.top) + sr.height / 2 - host.clientHeight / 2;
host.scrollTo({
left: Math.max(0, nextLeft),
top: Math.max(0, nextTop),
behavior: 'smooth',
});
});
}
/** 恢复为仅按窗口自动适应(清除手动缩放) */
function resetPreviewZoom() {
zoomMultiplier.value = 1;
scheduleComputeScale();
scheduleScrollPreviewCenter();
}
function onPreviewIframeLoad() {
updateContentMeasure();
// 二维码等异步绘制后再测一次,避免首次高度偏小
window.setTimeout(() => updateContentMeasure(), 400);
}
/** 调用浏览器打印预览 iframe 内文档(与当前预览 HTML 一致) */
function handleBrowserPrint() {
const win = previewIframeRef.value?.contentWindow;
if (!win) {
createMessage.warning('预览未就绪,请稍后再试');
return;
}
try {
win.focus();
win.print();
} catch (_e) {
createMessage.error('无法唤起打印,请检查浏览器是否拦截弹窗');
}
}
function scheduleComputeScale() {
void nextTick(() => computePreviewScale());
}
useResizeObserver(previewHostRef, () => {
scheduleComputeScale();
});
watch([paperPixelSize, previewHtml, () => props.open], () => {
contentMeasurePx.value = { w: 0, h: 0 };
scheduleComputeScale();
});
watch([layoutPaperPx, () => props.open], () => {
if (props.open) {
scheduleComputeScale();
}
});
function onClose() {
errorText.value = '';
schema.value = null;
previewHtml.value = '';
zoomMultiplier.value = 1;
autoFitScale.value = 1;
contentMeasurePx.value = { w: 0, h: 0 };
}
function onGenerateMock() {
if (!schema.value) {
return;
}
try {
const mockObj = generateNativeMockDataObject(schema.value.elements, canvasJsonText.value);
mockDataText.value = JSON.stringify(mockObj, null, 2);
createMessage.success('已根据画布组件生成模拟数据 JSON');
} catch (e: any) {
createMessage.error(`生成失败:${e?.message || '未知错误'}`);
}
}
watch(
() => [props.open, props.templateId] as const,
async ([isOpen, id]) => {
if (!isOpen || !id) {
return;
}
loading.value = true;
errorText.value = '';
schema.value = null;
previewHtml.value = '';
zoomMultiplier.value = 1;
autoFitScale.value = 1;
contentMeasurePx.value = { w: 0, h: 0 };
try {
const record = (await queryById(id)) as Record<string, any>;
const rawText = String(record?.templateJson || '').trim();
if (!rawText) {
errorText.value = '该记录没有模板 JSON';
return;
}
const parsed = JSON.parse(rawText);
if (parsed?.engine !== 'native') {
errorText.value = '仅原生模板engine=native支持此预览';
return;
}
const normalized = normalizeImportedNativeSchema(parsed);
const pw = Number(record?.paperWidthMm);
const ph = Number(record?.paperHeightMm);
if (pw > 0 && ph > 0) {
normalized.page.width = pw;
normalized.page.height = ph;
}
schema.value = normalized;
canvasJsonText.value = stringifyNativeTemplateStyle(normalized);
const mockObj = generateNativeMockDataObject(normalized.elements, canvasJsonText.value);
mockDataText.value = JSON.stringify(mockObj, null, 2);
previewDataText.value = mockDataText.value;
dataSourceType.value = 'mock';
jsonTabKey.value = 'params';
await refreshPreview();
scheduleComputeScale();
} catch (e: any) {
errorText.value = e?.message || '加载模板失败';
} finally {
loading.value = false;
}
},
{ immediate: false },
);
watch([activeDataText, schema, () => props.open], () => {
if (!props.open || !schema.value) {
return;
}
void debouncedRefreshPreview();
});
watch(
() => props.open,
(v) => {
if (!v) {
onClose();
}
},
);
</script>
<style scoped lang="less">
.native-preview-error {
padding: 12px 4px;
color: #cf1322;
}
.native-preview-row {
min-height: 440px;
align-items: stretch;
}
.native-preview-left {
display: flex;
flex-direction: column;
min-height: 0;
}
.native-preview-right {
display: flex;
flex-direction: column;
min-height: 440px;
max-height: 72vh;
}
.json-tabs {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
border: 1px solid #f0f0f0;
border-radius: 10px;
background: #fff;
padding: 10px 12px 12px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
}
.json-tabs :deep(.ant-tabs-content-holder) {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.json-tabs :deep(.ant-tabs-content),
.json-tabs :deep(.ant-tabs-tabpane) {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.json-template-pane,
.params-json-pane {
display: flex;
flex-direction: column;
gap: 12px;
flex: 1;
min-height: 0;
}
.json-box-title {
margin-bottom: 0;
font-size: 12px;
color: rgba(0, 0, 0, 0.55);
}
.json-textarea {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
font-size: 12px;
line-height: 1.45;
}
.json-textarea--main {
flex: 1;
min-height: 200px;
resize: none;
}
.params-json-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
/* 手动 / 模拟:胶囊分段(与设计器视觉一致) */
.json-sub-tabs.json-sub-tabs--segmented {
display: inline-flex;
align-items: center;
padding: 3px 5px;
background: #f0f2f5;
border-radius: 999px;
border: 1px solid #e8e8e8;
gap: 3px;
flex-shrink: 0;
}
.capsule-tab {
margin: 0;
padding: 4px 14px;
border: none;
border-radius: 999px;
background: transparent;
font-size: 12px;
line-height: 1.45;
color: rgba(0, 0, 0, 0.65);
cursor: pointer;
transition:
background 0.2s,
color 0.2s,
box-shadow 0.2s;
}
.capsule-tab:hover:not(.is-active) {
color: rgba(0, 0, 0, 0.88);
}
.capsule-tab.is-active {
background: #fff;
color: #1677ff;
font-weight: 500;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}
.params-json-head :deep(.json-capsule-btn.ant-btn) {
height: 28px;
padding: 0 16px;
line-height: 26px;
border-radius: 999px;
}
.preview-header-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
flex-shrink: 0;
}
.preview-title {
margin-bottom: 0;
font-size: 13px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
}
.preview-header-actions {
flex-shrink: 0;
justify-content: flex-end;
}
.preview-print-btn {
display: inline-flex;
align-items: center;
gap: 4px;
}
.preview-zoom-label {
min-width: 40px;
text-align: center;
font-size: 12px;
color: rgba(0, 0, 0, 0.55);
user-select: none;
}
.preview-zoom-btn,
.preview-zoom-fit-btn {
display: inline-flex;
align-items: center;
justify-content: center;
}
.preview-zoom-fit-btn {
padding: 0 10px;
font-size: 12px;
}
.preview-frame-wrap {
flex: 1;
min-height: 0;
overflow: auto;
border: 1px solid #f0f0f0;
border-radius: 10px;
background: #f5f5f5;
padding: 12px;
box-sizing: border-box;
}
/* 至少占满可视区;内容超出时由外层 preview-frame-wrap 滚动,避免 flex 居中裁切放大内容 */
.preview-scroll-flex {
min-height: 100%;
display: flex;
box-sizing: border-box;
}
.preview-zoom-slot {
margin: auto;
flex-shrink: 0;
}
.preview-scale-shim {
overflow: visible;
flex-shrink: 0;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.preview-scale-inner {
transform-origin: top left;
will-change: transform;
}
.preview-iframe {
display: block;
border: none;
background: #fff;
}
</style>
<!-- 弹窗在窄屏下不超出视口 -->
<style lang="less">
.native-template-list-preview-modal .ant-modal {
max-width: calc(100vw - 24px) !important;
}
</style>

View File

@@ -8,15 +8,30 @@
import { computed, ref, unref } from 'vue'; import { computed, ref, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal'; import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form'; import { BasicForm, useForm } from '/@/components/Form';
import { formSchema } from '../printTemplate.data'; import { formSchema, PAPER_PRESET_MAP } from '../printTemplate.data';
import { add, edit } from '../printTemplate.api'; import { add, edit } from '../printTemplate.api';
const emit = defineEmits(['success', 'register']); const emit = defineEmits(['success', 'register']);
const isUpdate = ref(false); const isUpdate = ref(false);
const isNativeMode = ref(false);
const title = computed(() => (!unref(isUpdate) ? '新增打印模板' : '编辑打印模板')); const title = computed(() => (!unref(isUpdate) ? '新增打印模板' : '编辑打印模板'));
function inferPresetBySize(record: Recordable) {
const width = Number(record?.paperWidthMm || 0);
const height = Number(record?.paperHeightMm || 0);
const orientation = String(record?.paperOrientation || 'portrait') as 'portrait' | 'landscape';
if (!width || !height) {
return undefined;
}
const key = Object.keys(PAPER_PRESET_MAP).find((presetKey) => {
const item = PAPER_PRESET_MAP[presetKey];
return item.width === width && item.height === height && item.orientation === orientation;
});
return key || 'CUSTOM';
}
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({ const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
labelWidth: 110, labelWidth: 110,
schemas: formSchema, schemas: formSchema,
@@ -28,30 +43,94 @@
resetFields(); resetFields();
setModalProps({ confirmLoading: false }); setModalProps({ confirmLoading: false });
isUpdate.value = !!data?.isUpdate; isUpdate.value = !!data?.isUpdate;
isNativeMode.value = data?.isNative === true;
if (unref(isUpdate) && data?.record) { if (unref(isUpdate) && data?.record) {
const paperPreset = inferPresetBySize(data.record);
setFieldsValue({ setFieldsValue({
...data.record, ...data.record,
paperPreset: paperPreset || 'CUSTOM',
}); });
return;
} }
setFieldsValue({
paperPreset: 'A4',
});
}); });
/** 编辑时在列表里改了纸张,需同步写回 templateJson.page否则原生设计器仍读旧尺寸 */
function mergeNativePaperIntoTemplateJson(templateJson: string, paperWidthMm: number, paperHeightMm: number): string {
const width = Math.max(10, Number(paperWidthMm) || 210);
const height = Math.max(10, Number(paperHeightMm) || 297);
try {
const parsed = JSON.parse(templateJson || '{}');
if (parsed?.engine === 'native') {
if (!parsed.page || typeof parsed.page !== 'object') {
parsed.page = { unit: 'mm', margin: [10, 10, 10, 10], gridSize: 2 };
}
parsed.page.width = width;
parsed.page.height = height;
return JSON.stringify(parsed);
}
} catch {
/* 非 JSON 或损坏则原样返回 */
}
return templateJson;
}
function buildNativeTemplateJson(values: Recordable) {
const width = Math.max(10, Number(values?.paperWidthMm || 210));
const height = Math.max(10, Number(values?.paperHeightMm || 297));
return JSON.stringify({
engine: 'native',
version: '1.0.0',
page: {
width,
height,
unit: 'mm',
margin: [10, 10, 10, 10],
gridSize: 2,
},
elements: [],
dataBinding: {
fieldMap: {},
tableSources: ['mainTable', 'detailList'],
},
});
}
async function handleSubmit() { async function handleSubmit() {
try { try {
const values = await validate(); const values = await validate();
delete values.paperPreset;
if (unref(isUpdate) && values.templateJson) {
values.templateJson = mergeNativePaperIntoTemplateJson(
String(values.templateJson),
Number(values.paperWidthMm),
Number(values.paperHeightMm),
);
}
if (!unref(isUpdate)) { if (!unref(isUpdate)) {
delete values.id; delete values.id;
if (!values.templateJson) { if (isNativeMode.value) {
values.templateJson = buildNativeTemplateJson(values);
} else if (!values.templateJson) {
values.templateJson = '{}'; values.templateJson = '{}';
} }
} }
setModalProps({ confirmLoading: true }); setModalProps({ confirmLoading: true });
let savedResult: any = null;
if (unref(isUpdate)) { if (unref(isUpdate)) {
await edit(values); savedResult = await edit(values);
} else { } else {
await add(values); savedResult = await add(values);
} }
closeModal(); closeModal();
emit('success'); emit('success', {
isNative: isNativeMode.value,
isUpdate: unref(isUpdate),
values,
savedResult,
});
} finally { } finally {
setModalProps({ confirmLoading: false }); setModalProps({ confirmLoading: false });
} }

View File

@@ -1,215 +1,252 @@
import { hiprint } from 'vue-plugin-hiprint'; export interface DesignerSampleData {
[key: string]: any;
/**
* QH-MES 自定义 provider参考 vue-plugin-hiprint 动态 provider 机制)
* - 新增一组“报表/套打”常用组件
* - 提供一个默认的“普通明细表”(单行表头)
*
* 注意:此 provider 不替换 defaultElementTypeProvider只做补充。
*/
export function createQhmesProvider() {
const key = 'qhmesModule';
const addElementTypes = function (context: any) {
// 避免重复注册
context.removePrintElementTypes(key);
const commonText = (tid: string, title: string, extraOptions: Record<string, any> = {}) => {
return {
tid,
title,
type: 'text',
options: {
title,
field: '',
testData: title,
...extraOptions,
},
};
};
const elements: any[] = [
commonText(`${key}.reportTitle`, '报表标题', { fontSize: 18, fontWeight: 'bold', textAlign: 'center' }),
commonText(`${key}.subTitle`, '副标题', { fontSize: 12, textAlign: 'center' }),
commonText(`${key}.labelValue`, '标签:值', { fontSize: 10 }),
commonText(`${key}.pageNo`, '页码', { field: 'pageNumber', testData: '1/1' }),
// 二维码/条码(用 text + textType
{
tid: `${key}.qrcode`,
title: '二维码',
type: 'text',
options: {
title: '二维码',
field: 'qrcode',
testData: 'QRCODE_DEMO',
textType: 'qrcode',
width: 35,
height: 35,
},
},
{
tid: `${key}.barcode`,
title: '条形码',
type: 'text',
options: {
title: '条形码',
field: 'barcode',
testData: '1234567890',
textType: 'barcode',
width: 80,
height: 25,
},
},
// 普通明细表(单行表头,支持多级分组合并)
{
tid: `${key}.tableSimple`,
title: '普通明细表',
type: 'html',
options: {
title: '普通明细表',
field: 'table',
testData: '',
width: 180,
height: 60,
__qhmesManaged: true,
columns: [
{ title: '物料', order: 0, field: 'name', width: 90, align: 'left' },
{ title: '数量', order: 1, field: 'qty', width: 45, align: 'right' },
{ title: '金额', order: 2, field: 'amount', width: 45, align: 'right' },
],
groupFields: [],
formatter: `
function(t,e,printData){
var opts = (t && t.options) ? t.options : {};
var list = printData && Array.isArray(printData[opts.field || 'table']) ? printData[opts.field || 'table'] : [];
var globalCols = printData && Array.isArray(printData.__qhmesTableColumns) ? printData.__qhmesTableColumns : [];
var columns = Array.isArray(opts.columns) && opts.columns.length ? opts.columns : (globalCols.length ? globalCols : [
{ title: '物料', order: 0, field: 'name', width: 90, align: 'left' },
{ title: '数量', order: 1, field: 'qty', width: 45, align: 'right' },
{ title: '金额', order: 2, field: 'amount', width: 45, align: 'right' }
]);
columns = columns.slice().sort(function(a,b){
var ao = Number(a && a.order);
var bo = Number(b && b.order);
var av = isFinite(ao) ? ao : 9999;
var bv = isFinite(bo) ? bo : 9999;
return av - bv;
});
var globalGroups = printData && Array.isArray(printData.__qhmesGroupFields) ? printData.__qhmesGroupFields : [];
var groupFields = Array.isArray(opts.groupFields) && opts.groupFields.length ? opts.groupFields : globalGroups;
var style = opts.__qhmesStyle || {};
var fontSize = style.fontSize || 10;
var borderColor = style.borderColor || '#000';
var borderWidth = style.borderWidth || 1;
var cellPadding = style.cellPadding || '2pt 4pt';
var headerBg = style.headerBg || '';
var tableWidth = style.tableWidth || '100%';
function esc(v){
if (v === null || v === undefined) return '';
return String(v).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function isGroupCol(field){
return groupFields.indexOf(field) > -1;
}
// 计算每个分组列在每行的 rowspan多级上层一致前提下再判断下层
var rowspanMap = {};
for (var c=0;c<columns.length;c++){
var f = columns[c].field;
rowspanMap[f] = new Array(list.length).fill(1);
}
for (var g=0; g<groupFields.length; g++){
var gf = groupFields[g];
// 分组字段可能不在 columns 中,需先兜底初始化,避免 rowspanMap[gf] 未定义报错
if (!rowspanMap[gf]) rowspanMap[gf] = new Array(list.length).fill(1);
var i = 0;
while(i < list.length){
var j = i + 1;
while(j < list.length){
var upperOk = true;
for (var up=0; up<g; up++){
var uf = groupFields[up];
if ((list[j][uf] ?? '') !== (list[i][uf] ?? '')) { upperOk = false; break; }
}
if (!upperOk) break;
if ((list[j][gf] ?? '') === (list[i][gf] ?? '')) j++;
else break;
}
var span = j - i;
if (!rowspanMap[gf]) rowspanMap[gf] = new Array(list.length).fill(1);
rowspanMap[gf][i] = span;
for (var k=i+1;k<j;k++) rowspanMap[gf][k] = 0;
i = j;
}
}
var html = '<table style="width:'+tableWidth+';border-collapse:collapse;table-layout:fixed;font-size:'+fontSize+'pt;">';
html += '<thead><tr>';
for (var h=0;h<columns.length;h++){
var hc = columns[h];
var hw = hc.width ? ('width:'+hc.width+'pt;') : '';
var colBg = hc.headerBg || '';
var hbg = (colBg || headerBg) ? ('background:'+(colBg || headerBg)+';') : '';
html += '<th style="border:'+borderWidth+'px solid '+borderColor+';padding:'+cellPadding+';text-align:center;'+hbg+hw+'">'+esc(hc.title || hc.field || '')+'</th>';
}
html += '</tr></thead><tbody>';
for (var r=0;r<list.length;r++){
html += '<tr>';
for (var cc=0;cc<columns.length;cc++){
var col = columns[cc];
var field = col.field;
var align = col.align || 'left';
if (isGroupCol(field)){
var rs = rowspanMap[field][r];
if (rs > 0){
html += '<td rowspan="'+rs+'" style="border:'+borderWidth+'px solid '+borderColor+';padding:'+cellPadding+';text-align:center;vertical-align:middle;">'+esc(list[r][field])+'</td>';
}
}else{
html += '<td style="border:'+borderWidth+'px solid '+borderColor+';padding:'+cellPadding+';text-align:'+align+';">'+esc(list[r][field])+'</td>';
}
}
html += '</tr>';
}
html += '</tbody></table>';
return html;
}
`,
},
},
{
tid: `${key}.tableSingleHeader`,
title: '单行表头表格',
type: 'table',
options: {
title: '单行表头表格',
field: 'table',
testData: '',
width: 180,
height: 60,
},
columns: [
[
{ title: '单号', field: 'fbillno', width: 55, align: 'left', colspan: 1, rowspan: 1 },
{ title: '物料', field: 'name', width: 75, align: 'left', colspan: 1, rowspan: 1 },
{ title: '数量', field: 'qty', width: 35, align: 'right', colspan: 1, rowspan: 1 },
{ title: '金额', field: 'amount', width: 35, align: 'right', colspan: 1, rowspan: 1 },
],
],
},
];
// 分组(左侧面板展示更友好)
const groups = [
new (hiprint as any).PrintElementTypeGroup('HttpPrinter风格组件', elements),
];
context.addPrintElementTypes(key, groups);
};
return { addElementTypes };
} }
export interface MultiHeaderColumnConfig {
id: string;
title: string;
field: string;
width?: number;
align?: 'left' | 'center' | 'right';
}
export interface MultiHeaderGroupConfig {
id: string;
title: string;
columns: MultiHeaderColumnConfig[];
}
export const defaultMultiHeaderConfig: MultiHeaderGroupConfig[] = [
{
id: 'group_base',
title: '基础信息',
columns: [{ id: 'col_day', title: '日期', field: 'day', width: 80, align: 'center' }],
},
{
id: 'group_qty',
title: '产量信息',
columns: [
{ id: 'col_planQty', title: '计划数', field: 'planQty', width: 90, align: 'right' },
{ id: 'col_actualQty', title: '实际数', field: 'actualQty', width: 90, align: 'right' },
{ id: 'col_passRate', title: '达成率', field: 'passRate', width: 90, align: 'center' },
],
},
];
export const defaultTemplateJson = {
panels: [
{
index: 0,
paperType: 'A4',
height: 297,
width: 210,
paperHeader: 8,
paperFooter: 8,
printElements: [
{
options: {
left: 12,
top: 10,
height: 16,
width: 186,
title: 'QH-MES生产工单',
textType: 'text',
fontSize: 16,
fontWeight: '700',
textAlign: 'center',
},
printElementType: {
title: '文本',
type: 'text',
},
},
],
},
],
};
export const defaultPrintData: DesignerSampleData = {
docNo: 'MO-20260409001',
orderNo: 'SO-20260408-003',
customerName: '华东电子科技',
printTime: '2026-04-09 14:30:21',
operator: '张三',
mainTable: [
{
materialCode: 'MAT-001',
materialName: '主控板',
spec: 'A版-24V',
qty: 1200,
unit: 'PCS',
remark: '优先排产',
tp: 'TP-MAT-001-20260409',
},
{
materialCode: 'MAT-002',
materialName: '传感器',
spec: 'TH-08',
qty: 3000,
unit: 'PCS',
remark: '',
tp: 'TP-MAT-002-20260409',
},
],
detailList: [
{
processName: '贴片',
machineNo: 'SMT-03',
worker: '李四',
startTime: '2026-04-09 08:10',
endTime: '2026-04-09 10:45',
okQty: 1180,
ngQty: 20,
tp: 'TP-SMT03-202604090810',
},
{
processName: '贴片',
machineNo: 'SMT-03',
worker: '李四',
startTime: '2026-04-09 10:50',
endTime: '2026-04-09 11:40',
okQty: 1210,
ngQty: 15,
tp: 'TP-SMT03-202604091050',
},
{
processName: '贴片',
machineNo: 'SMT-03',
worker: '李四',
startTime: '2026-04-09 13:00',
endTime: '2026-04-09 14:25',
okQty: 1195,
ngQty: 18,
tp: 'TP-SMT03-202604091300',
},
{
processName: '回流焊',
machineNo: 'RF-01',
worker: '王五',
startTime: '2026-04-09 11:00',
endTime: '2026-04-09 12:30',
okQty: 1170,
ngQty: 10,
tp: 'TP-RF01-202604091100',
},
{
processName: '回流焊',
machineNo: 'RF-01',
worker: '王五',
startTime: '2026-04-09 14:30',
endTime: '2026-04-09 15:40',
okQty: 1220,
ngQty: 12,
tp: 'TP-RF01-202604091430',
},
{
processName: '回流焊',
machineNo: 'RF-01',
worker: '王五',
startTime: '2026-04-09 15:45',
endTime: '2026-04-09 17:10',
okQty: 1205,
ngQty: 16,
tp: 'TP-RF01-202604091545',
},
{
processName: '插件',
machineNo: 'AI-02',
worker: '赵六',
startTime: '2026-04-10 08:05',
endTime: '2026-04-10 09:25',
okQty: 980,
ngQty: 8,
tp: 'TP-AI02-202604100805',
},
{
processName: '插件',
machineNo: 'AI-02',
worker: '赵六',
startTime: '2026-04-10 09:30',
endTime: '2026-04-10 11:00',
okQty: 1015,
ngQty: 11,
tp: 'TP-AI02-202604100930',
},
{
processName: '插件',
machineNo: 'AI-03',
worker: '赵六',
startTime: '2026-04-10 13:20',
endTime: '2026-04-10 14:50',
okQty: 990,
ngQty: 9,
tp: 'TP-AI03-202604101320',
},
{
processName: '测试',
machineNo: 'TEST-01',
worker: '孙七',
startTime: '2026-04-10 15:00',
endTime: '2026-04-10 16:10',
okQty: 950,
ngQty: 14,
tp: 'TP-TEST01-202604101500',
},
{
processName: '测试',
machineNo: 'TEST-01',
worker: '孙七',
startTime: '2026-04-10 16:15',
endTime: '2026-04-10 17:25',
okQty: 965,
ngQty: 10,
tp: 'TP-TEST01-202604101615',
},
{
processName: '包装',
machineNo: 'PK-01',
worker: '周八',
startTime: '2026-04-11 08:30',
endTime: '2026-04-11 10:00',
okQty: 1880,
ngQty: 6,
tp: 'TP-PK01-202604110830',
},
{
processName: '包装',
machineNo: 'PK-01',
worker: '周八',
startTime: '2026-04-11 10:05',
endTime: '2026-04-11 11:40',
okQty: 1920,
ngQty: 5,
tp: 'TP-PK01-202604111005',
},
],
multiHeaderTable: [
{ day: '周一', planQty: 600, actualQty: 580, passRate: '96.67%' },
{ day: '周二', planQty: 620, actualQty: 600, passRate: '96.77%' },
{ day: '周三', planQty: 640, actualQty: 635, passRate: '99.22%' },
],
};
export const dragElementList = [
{ label: '文本', tid: 'defaultModule.text', tip: '单行文本' },
{ label: '长文本', tid: 'defaultModule.longText', tip: '自动换行文本' },
{ label: '图片', tid: 'defaultModule.image', tip: '支持动态图片链接' },
{ label: '条形码', tid: 'defaultModule.barcode', tip: '常用于单据编码' },
{ label: '二维码', tid: 'defaultModule.qrcode', tip: '常用于追溯码' },
{ label: '表格', tid: 'defaultModule.table', tip: '主表或明细表' },
{ label: '横线', tid: 'defaultModule.hline', tip: '分割线' },
{ label: '竖线', tid: 'defaultModule.vline', tip: '分割线' },
{ label: '矩形', tid: 'defaultModule.rect', tip: '区域框选' },
];
export function resolveProviders(hiprintModule: Record<string, any>) {
const providers: any[] = [];
const defaultProviderCtor = hiprintModule?.defaultElementTypeProvider;
if (typeof defaultProviderCtor === 'function') {
providers.push(new defaultProviderCtor());
}
return providers;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,122 @@
/**
* 动态加载 C-Lodop 本地服务提供的 CLodopfuncs.js。
* 仅安装并启动 C-Lodop 客户端时若页面未引入该脚本window 上不会出现 getLodop导致误判「未检测到」。
*/
function hasLodopGlobal(): boolean {
const w = window as any;
return typeof w.getLodop === 'function' || !!w.LODOP || !!w.CLODOP;
}
let scriptIdSeq = 0;
const urlToScriptId = new Map<string, string>();
function scriptDomId(url: string): string {
let id = urlToScriptId.get(url);
if (!id) {
id = `qhmes-clodop-script-${++scriptIdSeq}`;
urlToScriptId.set(url, id);
}
return id;
}
function loadScriptOnce(src: string): Promise<void> {
return new Promise((resolve, reject) => {
const id = scriptDomId(src);
const existed = document.getElementById(id) as HTMLScriptElement | null;
if (existed) {
if (existed.getAttribute('data-loaded') === '1') {
resolve();
return;
}
existed.addEventListener('load', () => resolve(), { once: true });
existed.addEventListener(
'error',
() => reject(new Error(`加载失败: ${src}`)),
{ once: true },
);
return;
}
const s = document.createElement('script');
s.id = id;
s.src = src;
s.async = true;
s.setAttribute('data-qhmes-clodop-src', src);
s.onload = () => {
s.setAttribute('data-loaded', '1');
resolve();
};
s.onerror = () => {
s.remove();
urlToScriptId.delete(src);
reject(new Error(`加载失败: ${src}`));
};
document.head.appendChild(s);
});
}
function removeOurScript(url: string) {
const id = urlToScriptId.get(url);
if (id) {
document.getElementById(id)?.remove();
urlToScriptId.delete(url);
}
}
/** 按当前页面协议列出常见 C-Lodop 脚本地址(与官方文档端口一致) */
function candidateClodopScriptUrls(): string[] {
const isHttps = window.location.protocol === 'https:';
if (isHttps) {
return [
'https://localhost.lodop.net:8443/CLodopfuncs.js',
'https://127.0.0.1:8443/CLodopfuncs.js',
// 混合内容HTTPS 页面可能拦截下列地址,仅作兜底尝试
'http://localhost:8000/CLodopfuncs.js',
'http://127.0.0.1:8000/CLodopfuncs.js',
];
}
return [
'http://localhost:8000/CLodopfuncs.js',
'http://127.0.0.1:8000/CLodopfuncs.js',
'http://localhost:18000/CLodopfuncs.js',
'http://127.0.0.1:18000/CLodopfuncs.js',
];
}
let loadPromise: Promise<void> | null = null;
/**
* 确保已加载 CLodopfuncs.js多次调用共享同一 Promise失败后可重试
*/
export function ensureClodopScriptLoaded(): Promise<void> {
if (hasLodopGlobal()) {
return Promise.resolve();
}
if (!loadPromise) {
loadPromise = (async () => {
let lastError: Error | null = null;
for (const url of candidateClodopScriptUrls()) {
try {
await loadScriptOnce(url);
if (hasLodopGlobal()) {
return;
}
lastError = new Error(`已加载脚本但未发现 getLodop${url}`);
} catch (e: any) {
lastError = e instanceof Error ? e : new Error(String(e));
}
removeOurScript(url);
}
throw (
lastError ||
new Error(
'无法从本机加载 CLodopfuncs.js。若站点为 HTTPS请安装 C-Lodop 扩展版并在浏览器中信任 https://localhost.lodop.net:8443 证书后重试。',
)
);
})().catch((e) => {
loadPromise = null;
throw e;
});
}
return loadPromise;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,622 @@
<template>
<div class="binding-detail-fields-editor">
<div class="binding-actions-row">
<a-button type="primary" size="small" @click="openBatchTablesModal">批量新增明细表</a-button>
<a-button type="primary" size="small" :disabled="!detailTables.length" @click="openBatchFieldsModal">批量新增字段</a-button>
<a-button
size="small"
:danger="selectedTableRowKeys.length > 0"
:disabled="!selectedTableRowKeys.length"
@click="batchDeleteTables"
>
批量删除
</a-button>
</div>
<a-table
v-model:expandedRowKeys="expandedRowKeys"
size="small"
:columns="masterColumns"
:data-source="detailTables"
:row-key="(r) => r.tableKey"
:pagination="false"
:scroll="{ y: detailMainTableScrollY }"
:row-selection="{
selectedRowKeys: selectedTableRowKeys,
onChange: onTableSelectChange,
}"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'tableKey'">
<a-input
:value="record.tableKey"
size="small"
placeholder="数据源键 List1"
@update:value="(v) => onInlineTableKeyChange(record, String(v ?? ''), index)"
/>
</template>
<template v-else-if="column.dataIndex === 'label'">
<a-input
:value="record.label ?? ''"
size="small"
placeholder="显示名可选"
@update:value="(v) => onInlineTableLabelChange(record.tableKey, String(v ?? ''))"
/>
</template>
<template v-else-if="column.dataIndex === 'fieldCount'">
<span class="field-count">{{ (record.fields || []).length }} 个</span>
</template>
</template>
<template #expandedRowRender="{ record }">
<div class="nested-fields">
<div class="nested-fields-head">
<span>字段列表({{ record.tableKey }}</span>
<a-button type="link" size="small" @click="addFieldRow(record.tableKey)">添加字段</a-button>
</div>
<a-table
size="small"
:columns="fieldColumns"
:data-source="record.fields || []"
:row-key="(f) => `${record.tableKey}__${f.key}`"
:pagination="false"
:show-header="true"
:scroll="{ y: nestedFieldsTableScrollY }"
>
<template #bodyCell="{ column: fcol, record: frec }">
<template v-if="fcol.dataIndex === 'key'">
<a-input
:value="frec.key"
size="small"
placeholder="字段键 Field1"
@update:value="(v) => onInlineFieldKeyChange(record.tableKey, frec.key, String(v ?? ''))"
/>
</template>
<template v-else-if="fcol.dataIndex === 'label'">
<a-input
:value="frec.label ?? ''"
size="small"
placeholder="显示名"
@update:value="(v) => onInlineFieldLabelChange(record.tableKey, frec.key, String(v ?? ''))"
/>
</template>
<template v-else-if="fcol.dataIndex === 'actions'">
<a-button type="link" danger size="small" @click="removeField(record.tableKey, frec.key)">删除</a-button>
</template>
</template>
</a-table>
</div>
</template>
</a-table>
<!-- 批量新增明细表:与「参数」批量弹窗同一结构(快速生成 + 双输入手动行) -->
<a-modal
v-model:open="batchTablesModalOpen"
title="批量新增明细表"
ok-text="确定添加"
cancel-text="取消"
width="560px"
@ok="confirmBatchTables"
@cancel="resetBatchTablesModal"
>
<div class="modal-section">
<div class="section-title">快速生成</div>
<div class="section-desc">按数量自动生成,数据源键为 List + 数字,显示名为 列表 + 数字与参数侧「Parameter + 数字 / 参数 + 数字」对应)。生成结果在下方列表中,可再修改。</div>
<a-space align="center" wrap class="quick-row">
<span class="quick-label">数量</span>
<a-input-number v-model:value="quickTableCount" :min="1" :max="200" :precision="0" style="width: 120px" />
<a-button type="primary" @click="quickGenerateTables">一键新增</a-button>
</a-space>
</div>
<a-divider style="margin: 14px 0" />
<div class="modal-section">
<div class="section-title">手动添加</div>
<div class="section-desc">每行两个输入框:左侧数据源键,右侧显示名。可点「添加一行」增加空行。</div>
<div v-for="(row, idx) in batchTableRows" :key="idx" class="manual-row">
<a-input v-model:value="row.tableKey" size="small" placeholder="数据源键" allow-clear />
<a-input v-model:value="row.label" size="small" placeholder="显示名可选" allow-clear />
<a-button type="text" danger size="small" :disabled="batchTableRows.length <= 1" @click="removeBatchTableRow(idx)">删除</a-button>
</div>
<a-button type="dashed" block size="small" class="add-row-btn" @click="addBatchTableRow">添加一行</a-button>
</div>
</a-modal>
<!-- 批量新增字段:同一套区块样式;快速生成写入下方草稿行 -->
<a-modal
v-model:open="batchFieldsModalOpen"
title="批量新增字段"
ok-text="确定添加"
cancel-text="取消"
width="560px"
@ok="confirmBatchFields"
@cancel="resetBatchFieldsModal"
>
<div class="modal-section">
<div class="section-title">快速生成</div>
<div class="section-desc">
先选择父级明细表,再填数量:字段键为 Field + 数字,显示名为 字段 + 数字。生成结果追加到下方「手动添加」列表,可再修改后点「确定添加」。
</div>
<a-space direction="vertical" style="width: 100%" size="small">
<a-select
v-model:value="quickFieldParentKey"
:options="parentTableOptions"
show-search
option-filter-prop="label"
placeholder="选择父级明细数据源"
style="width: 100%"
/>
<a-space align="center" wrap class="quick-row">
<span class="quick-label">数量</span>
<a-input-number v-model:value="quickFieldCount" :min="1" :max="200" :precision="0" style="width: 120px" />
<a-button type="primary" :disabled="!quickFieldParentKey" @click="quickGenerateFieldsToRows">一键新增</a-button>
</a-space>
</a-space>
</div>
<a-divider style="margin: 14px 0" />
<div class="modal-section">
<div class="section-title">手动添加</div>
<div class="section-desc">每行:父级数据源键、字段键、显示名(可选)。父级须为已登记的明细表。</div>
<div v-for="(row, idx) in batchFieldRows" :key="idx" class="manual-row manual-row--triple">
<a-input v-model:value="row.parentKey" size="small" placeholder="父级数据源键" allow-clear />
<a-input v-model:value="row.fieldKey" size="small" placeholder="字段键" allow-clear />
<a-input v-model:value="row.label" size="small" placeholder="显示名可选" allow-clear />
<a-button type="text" danger size="small" :disabled="batchFieldRows.length <= 1" @click="removeBatchFieldRow(idx)">删除</a-button>
</div>
<a-button type="dashed" block size="small" class="add-row-btn" @click="addBatchFieldRow">添加一行</a-button>
</div>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { useMessage } from '/@/hooks/web/useMessage';
import type { NativeDataBindingDetailField, NativeDataBindingDetailTable } from '../core/types';
const props = defineProps<{
detailTables: NativeDataBindingDetailTable[];
}>();
const emit = defineEmits<{
(e: 'update:detailTables', value: NativeDataBindingDetailTable[]): void;
}>();
const { createMessage } = useMessage();
const viewportH = ref(typeof window !== 'undefined' ? window.innerHeight : 800);
function onResize() {
viewportH.value = window.innerHeight;
}
onMounted(() => window.addEventListener('resize', onResize));
onUnmounted(() => window.removeEventListener('resize', onResize));
/** 主表明细数据源列表可视高度 */
const detailMainTableScrollY = computed(() => {
const h = viewportH.value;
return Math.round(Math.min(500, Math.max(220, h - 380)));
});
/** 展开行内字段子表可视高度 */
const nestedFieldsTableScrollY = computed(() => {
const h = viewportH.value;
return Math.round(Math.min(300, Math.max(140, h - 520)));
});
const expandedRowKeys = ref<string[]>([]);
const selectedTableRowKeys = ref<string[]>([]);
const batchTablesModalOpen = ref(false);
const quickTableCount = ref(1);
const batchTableRows = ref<Array<{ tableKey: string; label: string }>>([{ tableKey: '', label: '' }]);
const batchFieldsModalOpen = ref(false);
const quickFieldParentKey = ref<string | undefined>(undefined);
const quickFieldCount = ref(1);
const batchFieldRows = ref<Array<{ parentKey: string; fieldKey: string; label: string }>>([
{ parentKey: '', fieldKey: '', label: '' },
]);
const parentTableOptions = computed(() =>
(props.detailTables || []).map((t) => ({
value: t.tableKey,
label: t.label ? `${t.tableKey}${t.label}` : t.tableKey,
})),
);
const masterColumns = [
{ title: '数据源', dataIndex: 'tableKey', ellipsis: true, width: 96 },
{ title: '显示名', dataIndex: 'label', ellipsis: true },
{ title: '字段数', dataIndex: 'fieldCount', width: 72, align: 'right' as const },
];
const fieldColumns = [
{ title: '字段键', dataIndex: 'key', ellipsis: true },
{ title: '显示名', dataIndex: 'label', ellipsis: true },
{ title: '操作', dataIndex: 'actions', width: 56, align: 'center' as const },
];
watch(
() => props.detailTables.map((t) => t.tableKey).join(','),
() => {
const keys = new Set(props.detailTables.map((t) => t.tableKey));
selectedTableRowKeys.value = selectedTableRowKeys.value.filter((k) => keys.has(k));
expandedRowKeys.value = expandedRowKeys.value.filter((k) => keys.has(k));
},
);
function onTableSelectChange(keys: string[]) {
selectedTableRowKeys.value = keys;
}
/** 已占用的数据源键:主列表 + 弹窗草稿 */
function collectUsedTableKeys(): Set<string> {
const s = new Set(props.detailTables.map((t) => t.tableKey.trim()).filter(Boolean));
batchTableRows.value.forEach((r) => {
const k = r.tableKey.trim();
if (k) s.add(k);
});
return s;
}
function maxListSuffix(used: Set<string>): number {
let max = 0;
for (const k of used) {
const m = String(k).match(/^List(\d+)$/i);
if (m) max = Math.max(max, Number(m[1]));
}
return max;
}
function maxFieldSuffixForParent(parentKey: string): number {
const used = new Set<string>();
const table = props.detailTables.find((t) => t.tableKey === parentKey);
(table?.fields || []).forEach((f) => used.add(f.key));
batchFieldRows.value.forEach((r) => {
if (String(r.parentKey ?? '').trim() === parentKey && r.fieldKey.trim()) used.add(r.fieldKey.trim());
});
let max = 0;
for (const k of used) {
const m = String(k).match(/^Field(\d+)$/i);
if (m) max = Math.max(max, Number(m[1]));
}
return max;
}
function resolveTableIndex(record: NativeDataBindingDetailTable, index?: number) {
if (typeof index === 'number' && index >= 0) return index;
return props.detailTables.findIndex((t) => t.tableKey === record.tableKey);
}
function onInlineTableKeyChange(record: NativeDataBindingDetailTable, newKey: string, index: number) {
const idx = resolveTableIndex(record, index);
const oldKey = props.detailTables[idx]?.tableKey ?? record.tableKey;
const nk = String(newKey || '').trim();
if (!nk) {
createMessage.warning('数据源键不能为空');
return;
}
if (nk !== oldKey && props.detailTables.some((t) => t.tableKey === nk)) {
createMessage.warning('该数据源键已存在');
return;
}
const list = [...props.detailTables];
if (idx < 0 || idx >= list.length) return;
list[idx] = { ...list[idx], tableKey: nk };
emit('update:detailTables', list);
selectedTableRowKeys.value = selectedTableRowKeys.value.map((k) => (k === oldKey ? nk : k));
expandedRowKeys.value = expandedRowKeys.value.map((k) => (k === oldKey ? nk : k));
}
function onInlineTableLabelChange(tableKey: string, label: string) {
const next = props.detailTables.map((t) =>
t.tableKey === tableKey ? { ...t, label: label.trim() || undefined } : t,
);
emit('update:detailTables', next);
}
function onInlineFieldKeyChange(tableKey: string, oldFieldKey: string, newKey: string) {
const fk = String(newKey || '').trim();
if (!fk) {
createMessage.warning('字段键不能为空');
return;
}
const table = props.detailTables.find((t) => t.tableKey === tableKey);
if (!table) return;
const fields = [...(table.fields || [])];
if (fk !== oldFieldKey && fields.some((f) => f.key === fk)) {
createMessage.warning('该字段键已存在');
return;
}
const nextFields = fields.map((f) => (f.key === oldFieldKey ? { ...f, key: fk } : f));
const next = props.detailTables.map((t) => (t.tableKey === tableKey ? { ...t, fields: nextFields } : t));
emit('update:detailTables', next);
}
function onInlineFieldLabelChange(tableKey: string, fieldKey: string, label: string) {
const next = props.detailTables.map((t) => {
if (t.tableKey !== tableKey) return t;
const fields = (t.fields || []).map((f) =>
f.key === fieldKey ? { ...f, label: label.trim() || undefined } : f,
);
return { ...t, fields };
});
emit('update:detailTables', next);
}
function addFieldRow(tableKey: string) {
const table = props.detailTables.find((t) => t.tableKey === tableKey);
if (!table) return;
const used = new Set((table.fields || []).map((f) => f.key));
let num = maxFieldSuffixForTable(tableKey) + 1;
while (used.has(`Field${num}`)) num += 1;
const key = `Field${num}`;
const next = props.detailTables.map((t) =>
t.tableKey === tableKey ? { ...t, fields: [...(t.fields || []), { key, label: `字段${num}` }] } : t,
);
emit('update:detailTables', next);
}
/** 仅统计当前表下 Field 数字后缀 */
function maxFieldSuffixForTable(tableKey: string): number {
const table = props.detailTables.find((t) => t.tableKey === tableKey);
let max = 0;
for (const f of table?.fields || []) {
const m = String(f.key).match(/^Field(\d+)$/i);
if (m) max = Math.max(max, Number(m[1]));
}
return max;
}
function removeField(tableKey: string, fieldKey: string) {
const next = props.detailTables.map((t) =>
t.tableKey === tableKey ? { ...t, fields: (t.fields || []).filter((f) => f.key !== fieldKey) } : t,
);
emit('update:detailTables', next);
}
function batchDeleteTables() {
if (!selectedTableRowKeys.value.length) return;
const drop = new Set(selectedTableRowKeys.value);
emit('update:detailTables', props.detailTables.filter((t) => !drop.has(t.tableKey)));
selectedTableRowKeys.value = [];
expandedRowKeys.value = [];
}
/* ---------- 批量:明细表 ---------- */
function resetBatchTablesModal() {
quickTableCount.value = 1;
batchTableRows.value = [{ tableKey: '', label: '' }];
}
function openBatchTablesModal() {
resetBatchTablesModal();
batchTablesModalOpen.value = true;
}
function addBatchTableRow() {
batchTableRows.value.push({ tableKey: '', label: '' });
}
function removeBatchTableRow(idx: number) {
if (batchTableRows.value.length <= 1) return;
batchTableRows.value.splice(idx, 1);
}
function quickGenerateTables() {
const count = Math.max(1, Math.min(200, Math.floor(Number(quickTableCount.value) || 1)));
const used = collectUsedTableKeys();
let num = Math.max(1, maxListSuffix(used) + 1);
for (let i = 0; i < count; i++) {
while (used.has(`List${num}`)) num += 1;
const key = `List${num}`;
used.add(key);
batchTableRows.value.push({ tableKey: key, label: `列表${num}` });
num += 1;
}
createMessage.success(`已新增 ${count} 行到下方列表,可修改后点「确定添加」。`);
}
function confirmBatchTables() {
const existing = new Set(props.detailTables.map((t) => t.tableKey.trim()).filter(Boolean));
const toAdd: NativeDataBindingDetailTable[] = [];
const seen = new Set<string>(existing);
for (const row of batchTableRows.value) {
const tk = String(row.tableKey || '').trim();
if (!tk) continue;
if (seen.has(tk)) continue;
seen.add(tk);
const lab = String(row.label || '').trim();
toAdd.push({ tableKey: tk, label: lab || undefined, fields: [] });
}
if (toAdd.length) emit('update:detailTables', [...props.detailTables, ...toAdd]);
batchTablesModalOpen.value = false;
resetBatchTablesModal();
}
/* ---------- 批量:字段 ---------- */
function resetBatchFieldsModal() {
quickFieldParentKey.value = undefined;
quickFieldCount.value = 1;
batchFieldRows.value = [{ parentKey: '', fieldKey: '', label: '' }];
}
function openBatchFieldsModal() {
resetBatchFieldsModal();
if (props.detailTables.length) {
quickFieldParentKey.value = props.detailTables[0].tableKey;
}
batchFieldsModalOpen.value = true;
}
function addBatchFieldRow() {
batchFieldRows.value.push({ parentKey: '', fieldKey: '', label: '' });
}
function removeBatchFieldRow(idx: number) {
if (batchFieldRows.value.length <= 1) return;
batchFieldRows.value.splice(idx, 1);
}
function quickGenerateFieldsToRows() {
const parent = String(quickFieldParentKey.value || '').trim();
if (!parent) {
createMessage.warning('请先选择父级明细数据源');
return;
}
if (!props.detailTables.some((t) => t.tableKey === parent)) {
createMessage.warning('父级不存在,请先在主表或「批量新增明细表」中登记');
return;
}
const count = Math.max(1, Math.min(200, Math.floor(Number(quickFieldCount.value) || 1)));
const used = new Set<string>();
const table = props.detailTables.find((t) => t.tableKey === parent);
(table?.fields || []).forEach((f) => used.add(f.key));
batchFieldRows.value.forEach((r) => {
if (String(r.parentKey ?? '').trim() === parent && r.fieldKey.trim()) used.add(r.fieldKey.trim());
});
let num = Math.max(1, maxFieldSuffixForParent(parent) + 1);
for (let i = 0; i < count; i++) {
while (used.has(`Field${num}`)) num += 1;
const key = `Field${num}`;
used.add(key);
batchFieldRows.value.push({ parentKey: parent, fieldKey: key, label: `字段${num}` });
num += 1;
}
createMessage.success(`已新增 ${count} 行到下方列表,可修改后点「确定添加」。`);
}
function confirmBatchFields() {
const byParent = new Map<string, NativeDataBindingDetailField[]>();
for (const row of batchFieldRows.value) {
const pk = String(row.parentKey || '').trim();
const fk = String(row.fieldKey || '').trim();
if (!pk || !fk) continue;
if (!props.detailTables.some((t) => t.tableKey === pk)) continue;
const lab = String(row.label || '').trim();
const list = byParent.get(pk) || [];
if (list.some((x) => x.key === fk)) continue;
list.push({ key: fk, label: lab || undefined });
byParent.set(pk, list);
}
if (!byParent.size) {
batchFieldsModalOpen.value = false;
resetBatchFieldsModal();
return;
}
const next = props.detailTables.map((t) => {
const add = byParent.get(t.tableKey);
if (!add?.length) return t;
const existing = new Set((t.fields || []).map((f) => f.key));
const merged = [...(t.fields || [])];
for (const f of add) {
if (!existing.has(f.key)) {
existing.add(f.key);
merged.push(f);
}
}
return { ...t, fields: merged };
});
emit('update:detailTables', next);
batchFieldsModalOpen.value = false;
resetBatchFieldsModal();
}
</script>
<style scoped lang="less">
.binding-detail-fields-editor {
padding: 6px 4px 4px;
}
.binding-actions-row {
display: flex;
flex-wrap: nowrap;
gap: 6px;
align-items: center;
justify-content: flex-end;
margin-bottom: 10px;
min-width: 0;
overflow-x: auto;
:deep(.ant-btn) {
flex-shrink: 0;
padding-inline: 7px;
}
}
:deep(.ant-table-thead > tr > th) {
white-space: nowrap;
}
.field-count {
color: #888;
font-size: 12px;
}
.nested-fields {
padding: 10px 10px 8px 20px;
margin-top: 4px;
background: #fafafa;
border-radius: 6px;
}
.nested-fields-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
font-size: 12px;
color: #555;
}
.modal-section {
.section-title {
font-weight: 600;
font-size: 13px;
margin-bottom: 6px;
}
.section-desc {
font-size: 12px;
color: #666;
line-height: 1.5;
margin-bottom: 10px;
}
}
.quick-row {
margin-top: 4px;
}
.quick-label {
font-size: 13px;
color: rgba(0, 0, 0, 0.85);
}
.manual-row {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 8px;
:deep(.ant-input) {
flex: 1;
min-width: 0;
}
}
/* 与双列 manual-row 视觉对齐:三列等宽 + 删除按钮固定列 */
.manual-row.manual-row--triple {
display: grid;
grid-template-columns: 1fr 1fr 1fr auto;
align-items: center;
gap: 8px;
margin-bottom: 8px;
:deep(.ant-input) {
min-width: 0;
}
}
.add-row-btn {
margin-top: 4px;
}
</style>

View File

@@ -0,0 +1,302 @@
<template>
<div class="binding-params-editor">
<div class="binding-actions-row">
<a-button type="primary" size="small" @click="openBatchAdd">批量新增</a-button>
<!-- 未勾选时不用 danger避免部分主题下 danger+disabled 出现异常图标 -->
<a-button
size="small"
:danger="selectedRowKeys.length > 0"
:disabled="!selectedRowKeys.length"
@click="batchDelete"
>
批量删除
</a-button>
</div>
<a-table
size="small"
:columns="columns"
:data-source="params"
:row-key="(r) => r.key"
:pagination="false"
:scroll="{ y: paramsTableScrollY }"
:row-selection="{
selectedRowKeys,
onChange: onSelectChange,
}"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'key'">
<a-input
:value="record.key"
size="small"
placeholder="参数键"
@update:value="(v) => patchParamAt(resolveRowIndex(record, index), 'key', String(v ?? ''))"
/>
</template>
<template v-else-if="column.dataIndex === 'label'">
<a-input
:value="record.label ?? ''"
size="small"
placeholder="显示名"
@update:value="(v) => patchParamAt(resolveRowIndex(record, index), 'label', String(v ?? ''))"
/>
</template>
</template>
</a-table>
<a-modal v-model:open="batchModalOpen" title="批量新增参数" ok-text="确定添加" cancel-text="取消" width="560px" @ok="confirmBatchAdd" @cancel="resetBatchModal">
<div class="modal-section">
<div class="section-title">快速生成</div>
<div class="section-desc">按数量自动生成,参数键为 Parameter + 数字,显示名为 参数 + 数字(与键后缀数字一致)。生成结果在下方列表中,可再修改。</div>
<a-space align="center" wrap class="quick-row">
<span class="quick-label">数量</span>
<a-input-number v-model:value="quickCount" :min="1" :max="200" :precision="0" style="width: 120px" />
<a-button type="primary" @click="quickGenerateToRows">一键新增</a-button>
</a-space>
</div>
<a-divider style="margin: 14px 0" />
<div class="modal-section">
<div class="section-title">手动添加</div>
<div class="section-desc">每行两个输入框:左侧参数键,右侧显示名。可点「添加一行」增加空行。</div>
<div v-for="(row, idx) in batchRows" :key="idx" class="manual-row">
<a-input v-model:value="row.key" size="small" placeholder="参数键" allow-clear />
<a-input v-model:value="row.label" size="small" placeholder="显示名可选" allow-clear />
<a-button type="text" danger size="small" :disabled="batchRows.length <= 1" @click="removeBatchRow(idx)">删除</a-button>
</div>
<a-button type="dashed" block size="small" class="add-row-btn" @click="addBatchRow">添加一行</a-button>
</div>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { useMessage } from '/@/hooks/web/useMessage';
import type { NativeDataBindingParam } from '../core/types';
const props = defineProps<{
params: NativeDataBindingParam[];
}>();
const emit = defineEmits<{
(e: 'update:params', value: NativeDataBindingParam[]): void;
}>();
const { createMessage } = useMessage();
/** 表格体可视高度:随窗口变化,与侧栏 tab 拉长区域匹配 */
const viewportH = ref(typeof window !== 'undefined' ? window.innerHeight : 800);
function onResize() {
viewportH.value = window.innerHeight;
}
onMounted(() => window.addEventListener('resize', onResize));
onUnmounted(() => window.removeEventListener('resize', onResize));
const paramsTableScrollY = computed(() => {
const h = viewportH.value;
return Math.round(Math.min(520, Math.max(260, h - 360)));
});
const columns = [
{ title: '参数键', dataIndex: 'key', ellipsis: true },
{ title: '显示名', dataIndex: 'label', ellipsis: true },
];
const selectedRowKeys = ref<string[]>([]);
const batchModalOpen = ref(false);
const quickCount = ref<number>(1);
const batchRows = ref<Array<{ key: string; label: string }>>([{ key: '', label: '' }]);
watch(
() => props.params,
() => {
const keys = new Set(props.params.map((p) => p.key));
selectedRowKeys.value = selectedRowKeys.value.filter((k) => keys.has(k));
},
);
function onSelectChange(keys: string[]) {
selectedRowKeys.value = keys;
}
function resetBatchModal() {
quickCount.value = 1;
batchRows.value = [{ key: '', label: '' }];
}
function openBatchAdd() {
resetBatchModal();
batchModalOpen.value = true;
}
/** 已占用的参数键:已有参数 + 弹窗内已填行 */
function collectUsedKeys(): Set<string> {
const s = new Set(props.params.map((p) => p.key.trim()).filter(Boolean));
batchRows.value.forEach((r) => {
const k = r.key.trim();
if (k) s.add(k);
});
return s;
}
/** 当前最大 Parameter 后缀数字,用于续号 */
function maxParameterSuffix(used: Set<string>): number {
let max = 0;
for (const k of used) {
const m = String(k).match(/^Parameter(\d+)$/i);
if (m) max = Math.max(max, Number(m[1]));
}
return max;
}
function quickGenerateToRows() {
const count = Math.max(1, Math.min(200, Math.floor(Number(quickCount.value) || 1)));
const used = collectUsedKeys();
let num = Math.max(1, maxParameterSuffix(used) + 1);
for (let i = 0; i < count; i++) {
while (used.has(`Parameter${num}`)) {
num += 1;
}
const key = `Parameter${num}`;
used.add(key);
batchRows.value.push({ key, label: `参数${num}` });
num += 1;
}
createMessage.success(`已新增 ${count} 行到下方列表,可修改后点「确定添加」。`);
}
function addBatchRow() {
batchRows.value.push({ key: '', label: '' });
}
function removeBatchRow(idx: number) {
if (batchRows.value.length <= 1) return;
batchRows.value.splice(idx, 1);
}
function confirmBatchAdd() {
const existing = new Set(props.params.map((p) => p.key.trim()).filter(Boolean));
const toAdd: NativeDataBindingParam[] = [];
const seen = new Set<string>(existing);
for (const row of batchRows.value) {
const k = row.key.trim();
if (!k) continue;
if (seen.has(k)) continue;
seen.add(k);
const lab = row.label.trim();
toAdd.push({ key: k, label: lab || undefined });
}
if (toAdd.length) {
emit('update:params', [...props.params, ...toAdd]);
}
batchModalOpen.value = false;
resetBatchModal();
}
function batchDelete() {
if (!selectedRowKeys.value.length) return;
const drop = new Set(selectedRowKeys.value);
emit('update:params', props.params.filter((p) => !drop.has(p.key)));
selectedRowKeys.value = [];
}
function resolveRowIndex(record: NativeDataBindingParam, index?: number) {
if (typeof index === 'number' && index >= 0) {
return index;
}
return props.params.findIndex((p) => p.key === record.key);
}
function patchParamAt(index: number, field: 'key' | 'label', raw: string) {
const list = [...props.params];
if (index < 0 || index >= list.length) return;
const cur = list[index];
if (field === 'label') {
const v = raw.trim();
list[index] = { ...cur, label: v || undefined };
emit('update:params', list);
return;
}
const newKey = raw.trim();
if (!newKey) {
createMessage.warning('参数键不能为空');
return;
}
if (newKey !== cur.key && list.some((p, i) => i !== index && p.key === newKey)) {
createMessage.warning('参数键已存在');
return;
}
const oldKey = cur.key;
list[index] = { ...cur, key: newKey };
emit('update:params', list);
selectedRowKeys.value = selectedRowKeys.value.map((k) => (k === oldKey ? newKey : k));
}
</script>
<style scoped lang="less">
.binding-params-editor {
padding: 6px 4px 4px;
}
.binding-actions-row {
display: flex;
flex-wrap: nowrap;
gap: 6px;
align-items: center;
justify-content: flex-end;
margin-bottom: 10px;
min-width: 0;
overflow-x: auto;
:deep(.ant-btn) {
flex-shrink: 0;
padding-inline: 7px;
}
}
:deep(.ant-table-thead > tr > th) {
white-space: nowrap;
}
.modal-section {
.section-title {
font-weight: 600;
font-size: 13px;
margin-bottom: 6px;
}
.section-desc {
font-size: 12px;
color: #666;
line-height: 1.5;
margin-bottom: 10px;
}
}
.quick-row {
margin-top: 4px;
}
.quick-label {
font-size: 13px;
color: rgba(0, 0, 0, 0.85);
}
.manual-row {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 8px;
:deep(.ant-input) {
flex: 1;
min-width: 0;
}
}
.add-row-btn {
margin-top: 4px;
}
</style>

View File

@@ -0,0 +1,527 @@
<template>
<div class="designer-canvas-wrap">
<div class="canvas-stage" :style="stageStyle">
<div class="ruler ruler-top">
<div
v-for="tick in topTicks"
:key="`top_${tick.mm}`"
class="tick"
:class="{ major: tick.major }"
:style="{ left: `${tick.posPx}px` }"
>
<span v-if="tick.major" class="tick-label">{{ tick.mm }}</span>
</div>
</div>
<div class="ruler ruler-left">
<div
v-for="tick in leftTicks"
:key="`left_${tick.mm}`"
class="tick"
:class="{ major: tick.major }"
:style="{ top: `${tick.posPx}px` }"
>
<span v-if="tick.major" class="tick-label">{{ tick.mm }}</span>
</div>
</div>
<div class="ruler-corner"></div>
<div class="designer-canvas" :style="canvasStyle" @click="emit('select', '')">
<div v-if="bandLayout.headerHeight > 0" class="band-area band-header" :style="{ height: `${bandLayout.headerHeight}mm` }">报表头区域</div>
<div
v-if="bandLayout.footerHeight > 0"
class="band-area band-footer"
:style="{ height: `${bandLayout.footerHeight}mm`, top: `${schema.page.height - bandLayout.footerHeight}mm` }"
>
报表尾区域
</div>
<div
class="band-divider"
:style="{ top: `${bandLayout.headerHeight}mm` }"
v-if="bandLayout.headerHeight > 0"
></div>
<div
class="band-divider"
:style="{ top: `${bandLayout.bodyBottom}mm` }"
v-if="bandLayout.footerHeight > 0"
></div>
<div v-if="guideState.showVertical" class="center-guide vertical" :style="{ left: `${pageCenterXPx}mm` }"></div>
<div v-if="guideState.showHorizontal" class="center-guide horizontal" :style="{ top: `${pageCenterYPx}mm` }"></div>
<ElementWrapper
v-for="element in sortedElements"
:key="element.id"
:element="element"
:active="selectedId === element.id"
:scale="scale"
:grid-size="schema.page.gridSize"
:page-width="schema.page.width"
:page-height="schema.page.height"
:movable="!isBandElement(element)"
:resizable="!isBandElement(element)"
:drag-bounds="resolveDragBounds(element)"
@select="emit('select', $event)"
@update="emit('update', $event)"
@dragging="handleElementDragging"
>
<TextElement v-if="isTextElement(element.type)" :element="element as any" :preview-data="previewData" />
<ImageElement v-else-if="element.type === 'image'" :element="element as any" :preview-data="previewData" />
<TableElement
v-else-if="element.type === 'table' || element.type === 'detailTable'"
:element="element as any"
:preview-data="previewData"
:is-element-selected="selectedId === element.id"
:selected-column-key="resolveSelectedColumnKey(element.id)"
@select-column="emit('select-table-column', { elementId: element.id, columnKey: $event.columnKey })"
@update-columns="handleTableColumnsUpdate(element.id, $event.columns)"
@update-header-config="handleTableHeaderConfigUpdate(element.id, $event.headerConfig)"
/>
<FreeTableElement
v-else-if="element.type === 'freeTable'"
:element="element as any"
:preview-data="previewData"
:scale="scale"
:is-element-selected="selectedId === element.id"
:selected-cell="resolveSelectedFreeTableCell(element.id)"
:merge-range-corner="resolveFreeTableMergeCorner(element.id)"
@select-cell="
emit('select-free-table-cell', {
elementId: element.id,
row: $event.row,
col: $event.col,
shiftKey: $event.shiftKey,
})
"
@swap-cells="handleFreeTableCellSwap(element.id, $event)"
@update-tracks="handleFreeTableTracksUpdate(element.id, $event)"
@edit-cell="
emit('edit-free-table-cell', {
elementId: element.id,
row: $event.row,
col: $event.col,
})
"
/>
<QrcodeElement v-else-if="element.type === 'qrcode'" :element="element as any" :preview-data="previewData" />
<BarcodeElement v-else-if="element.type === 'barcode'" :element="element as any" :preview-data="previewData" />
</ElementWrapper>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, reactive } from 'vue';
import ElementWrapper from './ElementWrapper.vue';
import TextElement from './elements/TextElement.vue';
import ImageElement from './elements/ImageElement.vue';
import TableElement from './elements/TableElement.vue';
import FreeTableElement from './elements/FreeTableElement.vue';
import QrcodeElement from './elements/QrcodeElement.vue';
import BarcodeElement from './elements/BarcodeElement.vue';
import type { NativeElement, NativeTemplateSchema } from '../core/types';
import { normalizeFreeTableAnchors, swapFreeTableOwnerPayloads } from '../core/freeTableGrid';
const PX_PER_MM = 3.7795275591;
const RULER_SIZE = 20;
const props = defineProps<{
schema: NativeTemplateSchema;
selectedId: string;
scale: number;
previewData: Record<string, any>;
selectedTableColumn?: { elementId: string; columnKey: string } | null;
selectedFreeTableCell?: { elementId: string; row: number; col: number } | null;
/** Shift 选取第二角(与 selectedFreeTableCell 同属一个自由表格时用于合并区域预览) */
selectedFreeTableMergeCorner?: { elementId: string; row: number; col: number } | null;
}>();
const emit = defineEmits<{
(e: 'select', id: string): void;
(e: 'update', payload: { id: string; patch: Partial<NativeElement> }): void;
(e: 'select-table-column', payload: { elementId: string; columnKey: string }): void;
(e: 'select-free-table-cell', payload: { elementId: string; row: number; col: number; shiftKey?: boolean }): void;
(e: 'edit-free-table-cell', payload: { elementId: string; row: number; col: number }): void;
}>();
const sortedElements = computed(() => [...props.schema.elements].sort((a, b) => a.zIndex - b.zIndex));
const bandLayout = computed(() => {
const pageHeight = props.schema.page.height;
const header = props.schema.elements.find((item) => item.type === 'reportHeader') as any;
const footer = props.schema.elements.find((item) => item.type === 'reportFooter') as any;
const headerHeight = Number(header?.h || 0);
const footerHeight = Number(footer?.h || 0);
const bodyBottom = Math.max(headerHeight, pageHeight - footerHeight);
return { headerHeight, footerHeight, bodyBottom };
});
const guideState = reactive({
showVertical: false,
showHorizontal: false,
});
const stageStyle = computed(() => ({
width: `${props.schema.page.width * PX_PER_MM * props.scale + RULER_SIZE}px`,
height: `${props.schema.page.height * PX_PER_MM * props.scale + RULER_SIZE}px`,
}));
const canvasStyle = computed(() => ({
left: `${RULER_SIZE}px`,
top: `${RULER_SIZE}px`,
width: `${props.schema.page.width}mm`,
height: `${props.schema.page.height}mm`,
transform: `scale(${props.scale})`,
transformOrigin: 'top left',
backgroundSize: `${props.schema.page.gridSize}mm ${props.schema.page.gridSize}mm`,
backgroundImage:
'linear-gradient(to right, rgba(22,119,255,0.08) 1px, transparent 1px),linear-gradient(to bottom, rgba(22,119,255,0.08) 1px, transparent 1px)',
}));
const pageCenterXPx = computed(() => props.schema.page.width / 2);
const pageCenterYPx = computed(() => props.schema.page.height / 2);
const topTicks = computed(() => buildRulerTicks(props.schema.page.width));
const leftTicks = computed(() => buildRulerTicks(props.schema.page.height));
function isTextElement(type: string) {
return ['title', 'subtitle', 'text', 'date', 'pageNo', 'reportHeader', 'reportFooter'].includes(type);
}
function isBandElement(element: NativeElement) {
return element.type === 'reportHeader' || element.type === 'reportFooter';
}
function resolveSelectedColumnKey(elementId: string) {
if (props.selectedTableColumn?.elementId === elementId) {
return props.selectedTableColumn.columnKey;
}
return '';
}
function resolveSelectedFreeTableCell(elementId: string) {
if (props.selectedFreeTableCell?.elementId === elementId) {
return {
row: Number(props.selectedFreeTableCell.row || 0),
col: Number(props.selectedFreeTableCell.col || 0),
};
}
return null;
}
function resolveFreeTableMergeCorner(elementId: string) {
if (props.selectedFreeTableMergeCorner?.elementId !== elementId) {
return null;
}
return {
row: Number(props.selectedFreeTableMergeCorner.row || 0),
col: Number(props.selectedFreeTableMergeCorner.col || 0),
};
}
function handleFreeTableTracksUpdate(
elementId: string,
payload: { colWidths?: number[]; rowHeights?: number[] },
) {
emit('update', {
id: elementId,
patch: { ...payload } as any,
});
}
function handleFreeTableCellSwap(
elementId: string,
payload: { fromRow: number; fromCol: number; toRow: number; toCol: number },
) {
const { fromRow, fromCol, toRow, toCol } = payload;
if (fromRow === toRow && fromCol === toCol) return;
const target = props.schema.elements.find((item) => item.id === elementId) as any;
if (!target || target.type !== 'freeTable') return;
const rowCount = Math.max(1, Number(target.rowCount || 1));
const colCount = Math.max(1, Number(target.colCount || 1));
const anchors = normalizeFreeTableAnchors(rowCount, colCount, target.cells || []);
const next = swapFreeTableOwnerPayloads(anchors, rowCount, colCount, fromRow, fromCol, toRow, toCol);
emit('update', {
id: elementId,
patch: { cells: next } as any,
});
}
function handleTableColumnsUpdate(elementId: string, columns: any[]) {
emit('update', {
id: elementId,
patch: {
columns,
} as any,
});
}
function handleTableHeaderConfigUpdate(elementId: string, headerConfig: any) {
const target = props.schema.elements.find((item) => item.id === elementId) as any;
const currentColumns = Array.isArray(target?.columns) ? target.columns : [];
const nextColumns = syncColumnTitlesByHeaderConfig(currentColumns, headerConfig);
emit('update', {
id: elementId,
patch: {
headerConfig,
columns: nextColumns,
} as any,
});
}
function syncColumnTitlesByHeaderConfig(columns: any[], headerConfig: any) {
const colCount = columns.length;
const rowCount = Math.max(1, Number(headerConfig?.rowCount || 1));
const owner: any[][] = Array.from({ length: rowCount }, () => Array.from({ length: colCount }, () => null));
const cells = Array.isArray(headerConfig?.cells) ? headerConfig.cells : [];
cells.forEach((item: any) => {
const row = Math.max(0, Number(item?.row || 0));
const col = Math.max(0, Number(item?.col || 0));
const rowspan = Math.max(1, Number(item?.rowspan || 1));
const colspan = Math.max(1, Number(item?.colspan || 1));
if (row >= rowCount || col >= colCount || owner[row][col]) return;
const maxRow = Math.min(rowCount, row + rowspan);
const maxCol = Math.min(colCount, col + colspan);
for (let r = row; r < maxRow; r += 1) {
for (let c = col; c < maxCol; c += 1) {
if (owner[r][c]) return;
}
}
const next = { ...item, row, col, rowspan: maxRow - row, colspan: maxCol - col };
for (let r = row; r < maxRow; r += 1) {
for (let c = col; c < maxCol; c += 1) {
owner[r][c] = next;
}
}
});
return columns.map((col, index) => {
const bottomCell = owner[rowCount - 1]?.[index];
const nextTitle =
bottomCell && Number(bottomCell?.row ?? rowCount - 1) < rowCount - 1
? String(col?.title || `${index + 1}`)
: String(bottomCell?.title || col?.title || `${index + 1}`);
return { ...col, title: nextTitle };
});
}
function buildRulerTicks(lengthMm: number) {
const length = Math.max(0, Math.floor(lengthMm));
const list: Array<{ mm: number; major: boolean; posPx: number }> = [];
for (let mm = 0; mm <= length; mm += 5) {
list.push({
mm,
major: mm % 10 === 0,
posPx: mm * PX_PER_MM * props.scale,
});
}
return list;
}
function handleElementDragging(payload: { id: string; rect: { x: number; y: number; w: number; h: number }; active: boolean }) {
if (!payload?.active) {
guideState.showVertical = false;
guideState.showHorizontal = false;
return;
}
const centerX = payload.rect.x + payload.rect.w / 2;
const centerY = payload.rect.y + payload.rect.h / 2;
const pageCenterX = props.schema.page.width / 2;
const pageCenterY = props.schema.page.height / 2;
const thresholdMm = Math.max(0.5, 1 / Math.max(0.2, props.scale));
guideState.showVertical = Math.abs(centerX - pageCenterX) <= thresholdMm;
guideState.showHorizontal = Math.abs(centerY - pageCenterY) <= thresholdMm;
}
function resolveElementRegion(element: NativeElement) {
const region = (element as any).region;
if (region === 'header' || region === 'footer' || region === 'body') {
return region;
}
const centerY = element.y + element.h / 2;
if (centerY <= bandLayout.value.headerHeight) return 'header';
if (centerY >= bandLayout.value.bodyBottom) return 'footer';
return 'body';
}
function resolveDragBounds(element: NativeElement) {
const pageWidth = props.schema.page.width;
const pageHeight = props.schema.page.height;
if (isBandElement(element)) {
const isHeader = element.type === 'reportHeader';
const fixedY = isHeader ? 0 : Math.max(0, props.schema.page.height - element.h);
return {
minX: 0,
maxX: 0,
minY: fixedY,
maxY: fixedY,
};
}
const region = resolveElementRegion(element);
const minX = 0;
const maxX = Math.max(0, pageWidth - element.w);
if (region === 'header') {
return {
minX,
maxX,
minY: 0,
maxY: Math.max(0, bandLayout.value.headerHeight - element.h),
};
}
if (region === 'footer') {
return {
minX,
maxX,
minY: Math.max(0, bandLayout.value.bodyBottom),
maxY: Math.max(0, pageHeight - element.h),
};
}
return {
minX,
maxX,
minY: bandLayout.value.headerHeight,
maxY: Math.max(bandLayout.value.headerHeight, bandLayout.value.bodyBottom - element.h),
};
}
</script>
<style scoped lang="less">
.designer-canvas-wrap {
padding: 16px;
overflow: auto;
height: calc(100vh - 150px);
background: #f0f2f5;
}
.canvas-stage {
position: relative;
margin: 0 auto;
}
.ruler {
position: absolute;
background: #f7f8fa;
z-index: 5;
}
.ruler-top {
left: 20px;
top: 0;
right: 0;
height: 20px;
border-bottom: 1px solid #d9d9d9;
}
.ruler-left {
left: 0;
top: 20px;
bottom: 0;
width: 20px;
border-right: 1px solid #d9d9d9;
}
.ruler-corner {
position: absolute;
left: 0;
top: 0;
width: 20px;
height: 20px;
background: #eef0f3;
border-right: 1px solid #d9d9d9;
border-bottom: 1px solid #d9d9d9;
z-index: 6;
}
.ruler-top .tick {
position: absolute;
top: 0;
width: 1px;
height: 8px;
background: #8c8c8c;
}
.ruler-top .tick.major {
height: 12px;
background: #595959;
}
.ruler-left .tick {
position: absolute;
left: 0;
width: 8px;
height: 1px;
background: #8c8c8c;
}
.ruler-left .tick.major {
width: 12px;
background: #595959;
}
.ruler-top .tick-label {
position: absolute;
top: 10px;
left: 2px;
font-size: 10px;
color: #666;
}
.ruler-left .tick-label {
position: absolute;
left: 10px;
top: -4px;
font-size: 10px;
color: #666;
}
.designer-canvas {
position: absolute;
margin: 0;
background: #fff;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
}
.band-area {
position: absolute;
left: 0;
width: 100%;
border-top: 1px dashed rgba(22, 119, 255, 0.4);
border-bottom: 1px dashed rgba(22, 119, 255, 0.4);
background: rgba(22, 119, 255, 0.06);
color: #1677ff;
font-size: 12px;
text-align: center;
line-height: 22px;
pointer-events: none;
z-index: 1;
}
.band-header {
top: 0;
}
.band-footer {
left: 0;
}
.band-divider {
position: absolute;
left: 0;
width: 100%;
border-top: 1px dashed rgba(82, 196, 26, 0.75);
pointer-events: none;
z-index: 2;
}
.center-guide {
position: absolute;
background: rgba(255, 77, 79, 0.85);
pointer-events: none;
z-index: 4;
}
.center-guide.vertical {
top: 0;
width: 1px;
height: 100%;
transform: translateX(-0.5px);
}
.center-guide.horizontal {
left: 0;
height: 1px;
width: 100%;
transform: translateY(-0.5px);
}
</style>

View File

@@ -0,0 +1,164 @@
<template>
<div class="element-wrapper" :class="{ active }" :style="wrapperStyle" @pointerdown.stop="startDrag" @click.stop="emit('select', element.id)">
<slot />
<template v-if="active && resizable">
<span v-for="handle in handles" :key="handle" class="resize-handle" :class="`handle-${handle}`" @pointerdown.stop="startResize($event, handle)" />
</template>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { calcDragRect, calcResizeRect } from '../core/dragResize';
import type { NativeElement } from '../core/types';
const PX_PER_MM = 3.7795275591;
const props = defineProps<{
element: NativeElement;
active: boolean;
scale: number;
gridSize: number;
pageWidth: number;
pageHeight: number;
movable?: boolean;
resizable?: boolean;
dragBounds?: {
minX: number;
maxX: number;
minY: number;
maxY: number;
};
}>();
const emit = defineEmits<{
(e: 'update', payload: { id: string; patch: Partial<NativeElement> }): void;
(e: 'select', id: string): void;
(e: 'dragging', payload: { id: string; rect: { x: number; y: number; w: number; h: number }; active: boolean }): void;
}>();
const handles = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
const wrapperStyle = computed(() => ({
position: 'absolute',
left: `${props.element.x}mm`,
top: `${props.element.y}mm`,
width: `${props.element.w}mm`,
height: `${props.element.h}mm`,
zIndex: props.element.zIndex,
outline: props.active ? '1px solid #1677ff' : '1px dashed transparent',
userSelect: 'none',
}));
const movable = computed(() => props.movable !== false);
const resizable = computed(() => props.resizable !== false);
function clampByBounds(rect: { x: number; y: number; w: number; h: number }) {
const bounds = props.dragBounds;
if (!bounds) return rect;
return {
...rect,
x: Math.max(bounds.minX, Math.min(bounds.maxX, rect.x)),
y: Math.max(bounds.minY, Math.min(bounds.maxY, rect.y)),
};
}
function startDrag(event: PointerEvent) {
emit('select', props.element.id);
if ((event.target as HTMLElement)?.classList.contains('resize-handle')) return;
if (!movable.value) return;
const start = { x: props.element.x, y: props.element.y, w: props.element.w, h: props.element.h };
const startX = event.clientX;
const startY = event.clientY;
const onMove = (moveEvent: PointerEvent) => {
// 鼠标位移是 px画布坐标是 mm这里必须做单位换算才会跟手
const deltaX = (moveEvent.clientX - startX) / props.scale / PX_PER_MM;
const deltaY = (moveEvent.clientY - startY) / props.scale / PX_PER_MM;
const next = calcDragRect(start, { width: props.pageWidth, height: props.pageHeight }, deltaX, deltaY, props.gridSize);
const bounded = clampByBounds(next);
emit('update', { id: props.element.id, patch: bounded });
emit('dragging', { id: props.element.id, rect: bounded, active: true });
};
const onUp = () => {
window.removeEventListener('pointermove', onMove);
window.removeEventListener('pointerup', onUp);
emit('dragging', { id: props.element.id, rect: { x: props.element.x, y: props.element.y, w: props.element.w, h: props.element.h }, active: false });
};
window.addEventListener('pointermove', onMove);
window.addEventListener('pointerup', onUp);
}
function startResize(event: PointerEvent, direction: any) {
emit('select', props.element.id);
if (!resizable.value) return;
const start = { x: props.element.x, y: props.element.y, w: props.element.w, h: props.element.h };
const startX = event.clientX;
const startY = event.clientY;
const onMove = (moveEvent: PointerEvent) => {
// 鼠标位移是 px画布尺寸是 mm缩放时同样要按单位换算
const deltaX = (moveEvent.clientX - startX) / props.scale / PX_PER_MM;
const deltaY = (moveEvent.clientY - startY) / props.scale / PX_PER_MM;
const next = calcResizeRect(direction, start, { width: props.pageWidth, height: props.pageHeight }, deltaX, deltaY, props.gridSize);
emit('update', { id: props.element.id, patch: next });
};
const onUp = () => {
window.removeEventListener('pointermove', onMove);
window.removeEventListener('pointerup', onUp);
};
window.addEventListener('pointermove', onMove);
window.addEventListener('pointerup', onUp);
}
</script>
<style scoped lang="less">
.element-wrapper {
box-sizing: border-box;
.resize-handle {
position: absolute;
width: 6px;
height: 6px;
background: #1677ff;
border-radius: 50%;
margin: -3px;
}
.handle-nw {
left: 0;
top: 0;
cursor: nwse-resize;
}
.handle-n {
left: 50%;
top: 0;
cursor: ns-resize;
}
.handle-ne {
left: 100%;
top: 0;
cursor: nesw-resize;
}
.handle-e {
left: 100%;
top: 50%;
cursor: ew-resize;
}
.handle-se {
left: 100%;
top: 100%;
cursor: nwse-resize;
}
.handle-s {
left: 50%;
top: 100%;
cursor: ns-resize;
}
.handle-sw {
left: 0;
top: 100%;
cursor: nesw-resize;
}
.handle-w {
left: 0;
top: 50%;
cursor: ew-resize;
}
}
</style>

View File

@@ -0,0 +1,307 @@
<template>
<a-modal
:open="open"
:title="modalTitle"
width="600px"
wrap-class-name="free-table-cell-edit-modal"
:body-style="{ padding: '16px 32px 8px' }"
destroy-on-close
ok-text="确定"
cancel-text="取消"
@ok="handleOk"
@cancel="onCancel"
@update:open="onUpdateOpen"
>
<a-form v-if="open && elementId" layout="vertical" class="free-table-cell-edit-form">
<a-row :gutter="[20, 4]">
<a-col :span="24">
<a-form-item label="单元格文本" class="form-item-tight">
<a-textarea
v-model:value="form.text"
:rows="2"
:maxlength="2000"
show-count
placeholder="静态文本;若选择绑定参数且预览有值,则优先显示参数值"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="绑定参数" class="form-item-tight">
<a-select
:value="resolveParamBindSelectValue(form.bindField)"
:options="bindingParamOptions"
allow-clear
show-search
option-filter-prop="label"
placeholder="请先在左侧「参数」页维护"
class="control-full"
@update:value="onModalBindParamChange"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12">
<a-form-item label="字号(px)" class="form-item-tight">
<a-input-number v-model:value="form.fontSize" :min="8" :max="72" class="control-full" />
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12">
<a-form-item label="文字对齐" class="form-item-tight">
<a-select v-model:value="form.align" :options="alignOptions" class="control-full" />
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12">
<a-form-item label="文字颜色" class="form-item-tight">
<a-space-compact block class="color-row">
<a-input v-model:value="form.color" placeholder="#111111" class="color-input" />
<input v-model="form.color" type="color" class="color-native" title="取色" aria-label="文字色取色" />
</a-space-compact>
</a-form-item>
</a-col>
<a-col :xs="24" :sm="12">
<a-form-item label="背景色" class="form-item-tight">
<a-space-compact block class="color-row">
<a-input v-model:value="form.backgroundColor" placeholder="#ffffff" class="color-input" />
<input
v-model="form.backgroundColor"
type="color"
class="color-native"
title="取色"
aria-label="背景色取色"
/>
</a-space-compact>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { computed, reactive, watch } from 'vue';
import { getFreeTableOwnerAt, normalizeFreeTableAnchors } from '../core/freeTableGrid';
import type { NativeDataBindingParam, NativeFreeTableElement, NativeTemplateSchema } from '../core/types';
const ALIGN_OPTIONS = [
{ label: '左对齐', value: 'left' },
{ label: '居中', value: 'center' },
{ label: '右对齐', value: 'right' },
];
const props = defineProps<{
open: boolean;
/** 当前编辑的自由表格元素 id */
elementId: string;
row: number;
col: number;
schema: NativeTemplateSchema;
}>();
const emit = defineEmits<{
(e: 'update:open', v: boolean): void;
(
e: 'save',
payload: {
elementId: string;
row: number;
col: number;
patch: {
text?: string;
bindField?: string;
fontSize?: number;
color?: string;
backgroundColor?: string;
align?: 'left' | 'center' | 'right';
};
},
): void;
}>();
const alignOptions = ALIGN_OPTIONS;
const form = reactive({
text: '',
bindField: '',
fontSize: 12 as number,
color: '#111111',
backgroundColor: '#ffffff',
align: 'left' as 'left' | 'center' | 'right',
});
const targetElement = computed(() => {
const list = props.schema?.elements || [];
const el = list.find((e) => e.id === props.elementId);
if (el && (el as any).type === 'freeTable') {
return el as NativeFreeTableElement;
}
return null;
});
const bindingParamOptions = computed(() =>
(props.schema.dataBinding?.params ?? []).map((p: NativeDataBindingParam) => ({
value: p.key,
label: p.label ? `${p.key}${p.label}` : p.key,
})),
);
function resolveParamBindSelectValue(raw: string | undefined | null) {
const k = String(raw || '').trim();
if (!k) return undefined;
const keys = new Set((props.schema.dataBinding?.params ?? []).map((p: NativeDataBindingParam) => p.key));
return keys.has(k) ? k : undefined;
}
function resolveParamDisplayName(paramKey: string) {
const k = String(paramKey || '').trim();
if (!k) return '';
const p = (props.schema.dataBinding?.params ?? []).find((x: NativeDataBindingParam) => x.key === k);
if (p?.label && String(p.label).trim()) return String(p.label).trim();
return k;
}
function onModalBindParamChange(v: string | undefined) {
form.bindField = v ?? '';
const k = String(v || '').trim();
if (k) {
form.text = resolveParamDisplayName(k);
}
}
const modalTitle = computed(() => {
const r = Number(props.row || 0) + 1;
const c = Number(props.col || 0) + 1;
return `编辑单元格(第 ${r} 行 · 第 ${c} 列)`;
});
function syncFormFromSchema() {
const el = targetElement.value;
if (!el) {
form.text = '';
form.bindField = '';
form.fontSize = 12;
form.color = '#111111';
form.backgroundColor = '#ffffff';
form.align = 'left';
return;
}
const rowCount = Math.max(1, Number(el.rowCount || 1));
const colCount = Math.max(1, Number(el.colCount || 1));
const anchors = normalizeFreeTableAnchors(rowCount, colCount, el.cells || []);
const owner = getFreeTableOwnerAt(anchors, props.row, props.col);
form.text = String(owner.text ?? '');
const rawBf = String(owner.bindField ?? '').trim();
const paramKeys = new Set((props.schema.dataBinding?.params ?? []).map((p: NativeDataBindingParam) => p.key));
form.bindField = rawBf && paramKeys.has(rawBf) ? rawBf : '';
form.fontSize = Number(owner.fontSize || 12);
form.color = String(owner.color || '#111111');
form.backgroundColor = String(owner.backgroundColor || '#ffffff');
const a = owner.align;
form.align = a === 'center' || a === 'right' ? a : 'left';
}
watch(
() => props.open,
(v) => {
if (v) {
syncFormFromSchema();
}
},
);
function onUpdateOpen(v: boolean) {
emit('update:open', v);
}
function onCancel() {
emit('update:open', false);
}
function handleOk() {
const fs = Number(form.fontSize);
const paramKeys = new Set((props.schema.dataBinding?.params ?? []).map((p: NativeDataBindingParam) => p.key));
const bfRaw = String(form.bindField || '').trim();
const bindFieldSafe = bfRaw && paramKeys.has(bfRaw) ? bfRaw : '';
emit('save', {
elementId: props.elementId,
row: props.row,
col: props.col,
patch: {
text: form.text,
bindField: bindFieldSafe,
fontSize: Number.isFinite(fs) ? Math.min(72, Math.max(8, fs)) : 12,
color: String(form.color || '#111111').trim() || '#111111',
backgroundColor: String(form.backgroundColor || '#ffffff').trim() || '#ffffff',
align: form.align,
},
});
}
</script>
<style scoped lang="less">
.free-table-cell-edit-form {
max-width: 100%;
}
.form-item-tight {
margin-bottom: 12px;
}
.control-full {
width: 100%;
}
:deep(.control-full.ant-input-number),
:deep(.control-full.ant-select .ant-select-selector) {
width: 100% !important;
}
:deep(.control-full.ant-select) {
width: 100%;
}
.color-row {
width: 100%;
}
.color-input {
flex: 1;
min-width: 0;
}
.color-native {
flex: 0 0 36px;
width: 36px;
height: 32px;
padding: 2px;
box-sizing: border-box;
border: 1px solid #d9d9d9;
border-radius: 0 6px 6px 0;
border-left: none;
background: #fff;
cursor: pointer;
}
</style>
<style lang="less">
/* 弹窗标题与底部按钮区略作收紧,与加宽后的表单区协调 */
.free-table-cell-edit-modal {
.ant-modal-header {
padding: 14px 28px;
border-bottom: 1px solid #f0f0f0;
}
.ant-modal-title {
font-weight: 600;
font-size: 15px;
}
.ant-modal-footer {
padding: 12px 28px 16px;
border-top: 1px solid #f0f0f0;
}
.ant-form-item-label > label {
color: rgba(0, 0, 0, 0.75);
font-weight: 500;
}
}
</style>

View File

@@ -0,0 +1,129 @@
<template>
<a-modal
v-model:open="modalOpen"
title="页面配置"
width="520px"
:footer="null"
destroy-on-close
wrap-class-name="native-page-config-modal"
:body-style="{ padding: '20px 24px 24px' }"
>
<div v-if="schema?.page" class="page-config-modal-body">
<div class="page-config-section-title">纸张与网格</div>
<a-space direction="vertical" :size="12" class="page-config-field-stack">
<a-input-number :value="schema.page.width" addon-before="(mm)" :min="10" :max="2000" style="width: 100%" @update:value="emitPage('width', $event)" />
<a-input-number :value="schema.page.height" addon-before="(mm)" :min="10" :max="2000" style="width: 100%" @update:value="emitPage('height', $event)" />
<a-input-number :value="schema.page.gridSize" addon-before="网格(mm)" :min="1" :max="20" style="width: 100%" @update:value="emitPage('gridSize', $event)" />
</a-space>
<a-divider class="page-config-divider" />
<div class="page-config-section-title">页面边距(mm)</div>
<a-row :gutter="[12, 12]" class="page-config-margin-grid">
<a-col :span="12">
<a-input-number
:value="Number(schema.page.margin?.[0] || 0)"
addon-before=""
:min="0"
:max="200"
style="width: 100%"
@update:value="emitPageMargin(0, $event)"
/>
</a-col>
<a-col :span="12">
<a-input-number
:value="Number(schema.page.margin?.[1] || 0)"
addon-before=""
:min="0"
:max="200"
style="width: 100%"
@update:value="emitPageMargin(1, $event)"
/>
</a-col>
<a-col :span="12">
<a-input-number
:value="Number(schema.page.margin?.[2] || 0)"
addon-before=""
:min="0"
:max="200"
style="width: 100%"
@update:value="emitPageMargin(2, $event)"
/>
</a-col>
<a-col :span="12">
<a-input-number
:value="Number(schema.page.margin?.[3] || 0)"
addon-before=""
:min="0"
:max="200"
style="width: 100%"
@update:value="emitPageMargin(3, $event)"
/>
</a-col>
</a-row>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import type { NativeTemplateSchema } from '../core/types';
const props = defineProps<{
open: boolean;
schema: NativeTemplateSchema;
}>();
const emit = defineEmits<{
(e: 'update:open', value: boolean): void;
(e: 'update-page', patch: Partial<NativeTemplateSchema['page']>): void;
}>();
const modalOpen = computed({
get: () => props.open,
set: (v: boolean) => emit('update:open', v),
});
function emitPage(key: string, value: any) {
emit('update-page', { [key]: Number(value || 0) } as any);
}
function emitPageMargin(index: 0 | 1 | 2 | 3, value: any) {
const current = Array.isArray(props.schema?.page?.margin) ? [...props.schema.page.margin] : [10, 10, 10, 10];
current[index] = Math.max(0, Number(value || 0));
emit('update-page', { margin: current as [number, number, number, number] });
}
</script>
<style scoped lang="less">
.page-config-modal-body {
min-width: 0;
}
.page-config-section-title {
font-size: 13px;
font-weight: 600;
color: rgba(0, 0, 0, 0.75);
margin-bottom: 10px;
line-height: 1.4;
}
.page-config-field-stack {
width: 100%;
}
.page-config-field-stack :deep(.ant-input-number) {
width: 100%;
}
.page-config-divider {
margin: 18px 0 16px;
border-color: #f0f0f0;
}
.page-config-margin-grid {
margin-top: 2px;
}
.page-config-margin-grid :deep(.ant-input-number) {
width: 100%;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,318 @@
<template>
<div class="header-config-editor">
<div class="toolbar">
<a-space>
<a-input-number :value="rowCount" :min="1" :max="6" addon-before="表头行数" @update:value="changeRowCount(Number($event || 1))" />
<a-select :value="activeCellAlign" style="width: 120px" @update:value="updateActiveCellAlign($event)">
<a-select-option value="left">左对齐</a-select-option>
<a-select-option value="center">居中</a-select-option>
<a-select-option value="right">右对齐</a-select-option>
</a-select>
<a-button size="small" @click="mergeSelection">合并选中</a-button>
<a-button size="small" @click="splitCurrent">拆分当前</a-button>
</a-space>
</div>
<div class="grid-wrap" @click="hideContextMenu">
<table class="grid-table">
<tbody>
<tr v-for="r in rowCount" :key="`row_${r - 1}`">
<template v-for="c in colCount" :key="`slot_${r - 1}_${c - 1}`">
<td
v-if="isCellStart(r - 1, c - 1)"
:rowspan="getCellByStart(r - 1, c - 1)?.rowspan || 1"
:colspan="getCellByStart(r - 1, c - 1)?.colspan || 1"
:class="cellClass(r - 1, c - 1)"
@mousedown.prevent="startSelect(r - 1, c - 1)"
@mousemove.prevent="moveSelect(r - 1, c - 1)"
@mouseup.prevent="endSelect"
@contextmenu.prevent="openContextMenu($event, r - 1, c - 1)"
>
<a-input
size="small"
:value="getCellByStart(r - 1, c - 1)?.title || ''"
@update:value="updateCellTitle(r - 1, c - 1, $event)"
/>
</td>
</template>
</tr>
</tbody>
</table>
</div>
<div v-if="contextMenu.visible" class="context-menu" :style="{ left: `${contextMenu.x}px`, top: `${contextMenu.y}px` }">
<div class="menu-item" @click="handleContextMerge">合并</div>
<div class="menu-item" @click="handleContextSplit">拆分</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, reactive, watch } from 'vue';
import type { NativeTableHeaderCell, NativeTableHeaderConfig } from '../core/types';
const props = defineProps<{
rowCount: number;
colCount: number;
columnTitles?: string[];
value?: NativeTableHeaderConfig;
}>();
const emit = defineEmits<{
(e: 'update:value', value: NativeTableHeaderConfig): void;
}>();
const state = reactive({
cells: [] as NativeTableHeaderCell[],
selecting: false,
selection: { r1: 0, c1: 0, r2: 0, c2: 0 },
contextMenu: { visible: false, x: 0, y: 0 },
});
const colCount = computed(() => Math.max(1, Number(props.colCount || 1)));
const rowCount = computed(() => Math.max(1, Number(props.rowCount || 1)));
function resolveColumnTitle(colIndex: number) {
const title = props.columnTitles?.[colIndex];
return String(title || `${colIndex + 1}`);
}
function createDefaultCells(rows: number, cols: number) {
const list: NativeTableHeaderCell[] = [];
for (let r = 0; r < rows; r += 1) {
for (let c = 0; c < cols; c += 1) {
list.push({
id: `h_${r}_${c}_${Math.random().toString(36).slice(2, 8)}`,
row: r,
col: c,
rowspan: 1,
colspan: 1,
title: r === rows - 1 ? resolveColumnTitle(c) : `表头${r + 1}-${c + 1}`,
align: 'center',
});
}
}
return list;
}
function normalizeCells(rows: number, cols: number, source?: NativeTableHeaderConfig) {
if (!source || !Array.isArray(source.cells) || source.colCount !== cols || source.rowCount !== rows) {
return createDefaultCells(rows, cols);
}
return source.cells.map((item) => ({ ...item }));
}
watch(
() => [props.rowCount, props.colCount, props.value],
() => {
state.cells = normalizeCells(rowCount.value, colCount.value, props.value);
},
{ immediate: true, deep: true },
);
function emitValue() {
emit('update:value', {
rowCount: rowCount.value,
colCount: colCount.value,
cells: state.cells.map((item) => ({ ...item })),
});
}
function getCellByStart(row: number, col: number) {
return state.cells.find((item) => item.row === row && item.col === col);
}
function findOwnerCell(row: number, col: number) {
return state.cells.find((item) => row >= item.row && row < item.row + item.rowspan && col >= item.col && col < item.col + item.colspan);
}
function isCellStart(row: number, col: number) {
return !!getCellByStart(row, col);
}
function inSelection(row: number, col: number) {
const minR = Math.min(state.selection.r1, state.selection.r2);
const maxR = Math.max(state.selection.r1, state.selection.r2);
const minC = Math.min(state.selection.c1, state.selection.c2);
const maxC = Math.max(state.selection.c1, state.selection.c2);
return row >= minR && row <= maxR && col >= minC && col <= maxC;
}
function cellClass(row: number, col: number) {
const cell = getCellByStart(row, col);
return {
selected: inSelection(row, col),
focused: cell?.id === findOwnerCell(state.selection.r2, state.selection.c2)?.id,
};
}
function startSelect(row: number, col: number) {
state.selecting = true;
state.selection = { r1: row, c1: col, r2: row, c2: col };
hideContextMenu();
}
function moveSelect(row: number, col: number) {
if (!state.selecting) return;
state.selection.r2 = row;
state.selection.c2 = col;
}
function endSelect() {
state.selecting = false;
}
function updateCellTitle(row: number, col: number, value: string) {
const cell = getCellByStart(row, col);
if (!cell) return;
cell.title = String(value || '');
emitValue();
}
function selectedRect() {
return {
minR: Math.min(state.selection.r1, state.selection.r2),
maxR: Math.max(state.selection.r1, state.selection.r2),
minC: Math.min(state.selection.c1, state.selection.c2),
maxC: Math.max(state.selection.c1, state.selection.c2),
};
}
function canMerge() {
const { minR, maxR, minC, maxC } = selectedRect();
const selectedCells = state.cells.filter((cell) => cell.row >= minR && cell.row <= maxR && cell.col >= minC && cell.col <= maxC);
const totalSlots = (maxR - minR + 1) * (maxC - minC + 1);
return selectedCells.length === totalSlots && selectedCells.every((cell) => cell.rowspan === 1 && cell.colspan === 1);
}
function mergeSelection() {
if (!canMerge()) return;
const { minR, maxR, minC, maxC } = selectedRect();
const previousBottomTitles = Array.from({ length: maxC - minC + 1 }).map((_, idx) => {
const col = minC + idx;
const owner = findOwnerCell(rowCount.value - 1, col);
return String(owner?.title || resolveColumnTitle(col));
});
const master = getCellByStart(minR, minC);
if (!master) return;
master.rowspan = maxR - minR + 1;
master.colspan = maxC - minC + 1;
// 纵向合并且覆盖到底层时,默认标题沿用底层(绑定字段对应)列标题
if (maxR === rowCount.value - 1) {
master.title = minC === maxC ? previousBottomTitles[0] : previousBottomTitles.join(' / ');
}
state.cells = state.cells.filter((cell) => cell === master || cell.row < minR || cell.row > maxR || cell.col < minC || cell.col > maxC);
emitValue();
}
function splitCurrent() {
const owner = findOwnerCell(state.selection.r2, state.selection.c2);
if (!owner || (owner.rowspan === 1 && owner.colspan === 1)) return;
const { row, col, rowspan, colspan, title } = owner;
state.cells = state.cells.filter((item) => item.id !== owner.id);
for (let r = row; r < row + rowspan; r += 1) {
for (let c = col; c < col + colspan; c += 1) {
state.cells.push({
id: `h_${r}_${c}_${Math.random().toString(36).slice(2, 8)}`,
row: r,
col: c,
rowspan: 1,
colspan: 1,
title: r === row && c === col ? title : '',
align: owner.align || 'center',
});
}
}
emitValue();
}
function changeRowCount(nextRows: number) {
const rows = Math.max(1, Math.min(6, Number(nextRows || 1)));
state.cells = createDefaultCells(rows, colCount.value);
emit('update:value', { rowCount: rows, colCount: colCount.value, cells: state.cells.map((item) => ({ ...item })) });
}
function openContextMenu(event: MouseEvent, row: number, col: number) {
if (!inSelection(row, col)) {
state.selection = { r1: row, c1: col, r2: row, c2: col };
}
state.contextMenu = { visible: true, x: event.clientX, y: event.clientY };
}
function hideContextMenu() {
state.contextMenu.visible = false;
}
function handleContextMerge() {
mergeSelection();
hideContextMenu();
}
function handleContextSplit() {
splitCurrent();
hideContextMenu();
}
const contextMenu = computed(() => state.contextMenu);
const activeCell = computed(() => findOwnerCell(state.selection.r2, state.selection.c2));
const activeCellAlign = computed(() => String(activeCell.value?.align || 'center'));
function updateActiveCellAlign(value: string) {
const cell = activeCell.value;
if (!cell) return;
cell.align = String(value || 'center') as any;
emitValue();
}
</script>
<style scoped lang="less">
.header-config-editor {
position: relative;
border: 1px solid #f0f0f0;
border-radius: 6px;
padding: 8px;
background: #fafafa;
.toolbar {
margin-bottom: 8px;
}
.grid-wrap {
overflow: auto;
max-height: 220px;
background: #fff;
border: 1px solid #f0f0f0;
}
.grid-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
td {
border: 1px solid #d9d9d9;
padding: 2px;
vertical-align: middle;
}
td.selected {
background: #e6f4ff;
}
}
.context-menu {
position: fixed;
z-index: 9999;
min-width: 120px;
background: #fff;
border: 1px solid #d9d9d9;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
.menu-item {
padding: 6px 10px;
cursor: pointer;
}
.menu-item:hover {
background: #f5f5f5;
}
}
}
</style>

View File

@@ -0,0 +1,160 @@
<template>
<div class="toolbar-palette">
<div class="palette-title">组件库</div>
<a-radio-group
class="palette-insert-region"
:value="insertRegion"
button-style="solid"
size="small"
@update:value="emit('update:insertRegion', $event)"
>
<a-radio-button value="header">新增到报表头</a-radio-button>
<a-radio-button value="body">新增到报表主体</a-radio-button>
</a-radio-group>
<a-tabs v-model:activeKey="activeTab" size="small" class="palette-tabs">
<a-tab-pane key="bands" tab="报表节">
<div class="tab-scroll">
<a-button v-for="item in bandItems" :key="item.type" block size="small" class="palette-btn" @click="emit('add', item.type)">
{{ item.label }}
</a-button>
</div>
</a-tab-pane>
<a-tab-pane key="components" tab="组件框">
<div class="tab-scroll">
<a-button v-for="item in componentItems" :key="item.type" block size="small" class="palette-btn" @click="emit('add', item.type)">
{{ item.label }}
</a-button>
</div>
</a-tab-pane>
<a-tab-pane key="params">
<template #tab>
<span class="palette-tab-help-label" :title="paramsTabHelp">参数</span>
</template>
<div class="tab-scroll">
<BindingParamsEditor :params="paramsList" @update:params="onUpdateParams" />
</div>
</a-tab-pane>
<a-tab-pane key="fields">
<template #tab>
<span class="palette-tab-help-label" :title="fieldsTabHelp">字段</span>
</template>
<div class="tab-scroll">
<BindingDetailFieldsEditor :detail-tables="detailTablesList" @update:detail-tables="onUpdateDetailTables" />
</div>
</a-tab-pane>
</a-tabs>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import BindingDetailFieldsEditor from './BindingDetailFieldsEditor.vue';
import BindingParamsEditor from './BindingParamsEditor.vue';
import type {
NativeDataBindingDetailTable,
NativeDataBindingParam,
NativeElementType,
NativeTemplateSchema,
} from '../core/types';
const props = defineProps<{
dataBinding?: NativeTemplateSchema['dataBinding'];
/** 新增元素落入报表头或主体(与画布逻辑一致) */
insertRegion: 'header' | 'body';
}>();
const emit = defineEmits<{
(e: 'add', type: NativeElementType): void;
(e: 'update-data-binding', value: Partial<NonNullable<NativeTemplateSchema['dataBinding']>>): void;
(e: 'update:insertRegion', value: 'header' | 'body'): void;
}>();
const activeTab = ref<'bands' | 'components' | 'params' | 'fields'>('bands');
/** 鼠标悬停在「参数」标签上时展示 */
const paramsTabHelp =
'用于非表格类组件(文本、标题、自由表格单元格等)的「绑定参数」下拉选项维护;普通/明细表格列请在「字段」页维护明细数据源与字段。';
/** 鼠标悬停在「字段」标签上时展示 */
const fieldsTabHelp =
'用于明细表格类组件的「数据源」与下属绑定字段维护(数据源键与画布明细表格的 source 一致)。';
const paramsList = computed(() => props.dataBinding?.params ?? []);
const detailTablesList = computed(() => props.dataBinding?.detailTables ?? []);
function onUpdateParams(params: NativeDataBindingParam[]) {
emit('update-data-binding', { params });
}
function onUpdateDetailTables(detailTables: NativeDataBindingDetailTable[]) {
emit('update-data-binding', { detailTables });
}
const bandItems: Array<{ type: NativeElementType; label: string }> = [
{ type: 'reportHeader', label: '报表头' },
{ type: 'reportFooter', label: '报表尾' },
];
const componentItems: Array<{ type: NativeElementType; label: string }> = [
{ type: 'title', label: '标题' },
{ type: 'subtitle', label: '副标题' },
{ type: 'text', label: '文本' },
{ type: 'date', label: '日期' },
{ type: 'pageNo', label: '页码' },
{ type: 'image', label: '图片' },
{ type: 'table', label: '普通表格' },
{ type: 'detailTable', label: '明细表格' },
{ type: 'freeTable', label: '自由表格' },
{ type: 'qrcode', label: '二维码' },
{ type: 'barcode', label: '条形码' },
];
</script>
<style scoped lang="less">
.toolbar-palette {
padding-inline: 2px;
.palette-title {
margin-bottom: 6px;
font-size: 13px;
font-weight: 600;
}
.palette-insert-region {
display: flex;
width: 100%;
margin-bottom: 8px;
:deep(.ant-radio-button-wrapper) {
flex: 1;
padding-inline: 6px;
text-align: center;
}
}
.palette-tabs {
:deep(.ant-tabs-nav) {
margin-bottom: 8px;
padding-inline: 2px;
}
:deep(.ant-tabs-content) {
padding: 6px 4px 4px;
}
}
/* 随视窗增高,参数/字段等列表可展示更多行 */
.tab-scroll {
max-height: clamp(300px, calc(100vh - 210px), 680px);
overflow: auto;
padding: 6px 10px 12px 8px;
}
.palette-btn {
margin-bottom: 6px;
text-align: left;
}
.palette-tab-help-label {
cursor: help;
}
}
</style>

View File

@@ -0,0 +1,66 @@
<template>
<div class="barcode-element">
<canvas ref="canvasRef"></canvas>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import type { NativeCodeElement } from '../../core/types';
const props = defineProps<{
element: NativeCodeElement;
previewData?: Record<string, any>;
}>();
const canvasRef = ref<HTMLCanvasElement>();
function resolveFieldValue(field?: string) {
if (!field) return undefined;
return field.split('.').reduce((acc: any, key) => acc?.[key], props.previewData || {});
}
async function renderBarcode() {
if (!canvasRef.value) return;
const module: any = await import('jsbarcode');
const JsBarcode = module.default || module;
const bindValue = resolveFieldValue(props.element.bindField);
const value = bindValue !== undefined && bindValue !== null ? String(bindValue) : props.element.value || '0000000000';
JsBarcode(canvasRef.value, value, {
format: 'CODE128',
displayValue: true,
margin: 0,
width: 1.5,
height: 40,
fontSize: 12,
});
}
onMounted(() => {
renderBarcode();
});
watch(
() => [props.element.value, props.element.bindField, props.previewData],
() => {
renderBarcode();
},
);
</script>
<style scoped lang="less">
.barcode-element {
width: 100%;
height: 100%;
border: 1px dashed #999;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
canvas {
width: 100%;
height: 100%;
}
}
</style>

View File

@@ -0,0 +1,679 @@
<template>
<div class="free-table-element" :data-free-table-id="element.id">
<!-- 选中时左上角四向箭头与画布整体拖动一致不在此阻止 pointerdown事件冒泡到 ElementWrapper -->
<button
v-if="isElementSelected"
type="button"
class="free-table-move-handle"
title="拖动移动整个表格"
aria-label="拖动移动整个表格"
@click.stop
>
<svg
class="free-table-move-icon"
viewBox="0 0 16 16"
width="11"
height="11"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M8 1.5v3M8 11.5v3M1.5 8h3M11.5 8h3"
stroke="currentColor"
stroke-width="1.35"
stroke-linecap="round"
/>
<path
fill="currentColor"
d="M8 2.35 6.15 4.55h3.7L8 2.35zm0 11.3 1.85-2.2H6.15L8 13.65zM2.35 8l2.2 1.85V6.15L2.35 8zm11.3 0-2.2-1.85v3.7L13.65 8z"
/>
</svg>
</button>
<div class="free-table-surface" @pointerdown.stop>
<table>
<colgroup>
<col v-for="(cw, ci) in colWidthsMm" :key="`col_${ci}`" :style="{ width: `${cw}mm` }" />
</colgroup>
<tbody>
<tr v-for="r in rowCount" :key="`tr_${r - 1}`" :style="{ height: `${rowHeightsMm[r - 1] ?? 6}mm` }">
<td
v-for="cell in anchorsForRow(r - 1)"
:key="`td_${cell.row}_${cell.col}`"
class="free-table-cell"
:class="{
'is-selected': isAnchorSelected(cell),
'is-merge-range': isAnchorInMergeRange(cell),
}"
:rowspan="Math.max(1, Number(cell.rowspan || 1))"
:colspan="Math.max(1, Number(cell.colspan || 1))"
:data-ft-row="cell.row"
:data-ft-col="cell.col"
:style="cellStyle(cell)"
@pointerdown.stop="handleSelectCell(cell, $event)"
@dblclick.stop="handleCellDblClick(cell)"
>
<span
class="cell-move-handle"
title="拖动交换单元格内容"
@pointerdown.stop.prevent="startCellSwapDrag($event, cell.row, cell.col)"
>
<span class="cell-move-handle-icon" aria-hidden="true"></span>
</span>
<span v-if="resolveCellContentType(cell) === 'text'" class="cell-body cell-body--text">{{ resolveCellTextForAnchor(cell) }}</span>
<span
v-else-if="resolveCellContentType(cell) === 'number' || resolveCellContentType(cell) === 'amount'"
class="cell-body cell-body--numeric"
>
{{ formatFreeCellNumeric(cell) }}
</span>
<img
v-else-if="resolveCellContentType(cell) === 'image'"
class="table-media-free"
alt=""
:src="resolveFreeCellImageSrc(cell)"
:style="freeCellMediaStyle(cell, 'image')"
/>
<img
v-else-if="resolveCellContentType(cell) === 'qrcode'"
class="table-media-free"
alt=""
:src="resolveFreeCellQrcodeSrc(cell)"
:style="freeCellMediaStyle(cell, 'qrcode')"
/>
<img
v-else-if="resolveCellContentType(cell) === 'barcode'"
class="table-media-free"
alt=""
:src="resolveFreeCellBarcodeSrc(cell)"
:style="freeCellMediaStyle(cell, 'barcode')"
/>
</td>
</tr>
</tbody>
</table>
<div v-if="isElementSelected" class="free-table-track-layer" aria-hidden="true">
<div
v-for="(pct, i) in colGripPositionsPct"
:key="`cg_${i}`"
class="track-grip track-grip--col"
:style="{ left: `${pct}%` }"
title="拖动调整列宽"
@pointerdown.stop.prevent="onColGripPointerDown(i, $event)"
/>
<div
v-for="(pct, i) in rowGripPositionsPct"
:key="`rg_${i}`"
class="track-grip track-grip--row"
:style="{ top: `${pct}%` }"
title="拖动调整行高"
@pointerdown.stop.prevent="onRowGripPointerDown(i, $event)"
/>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import QRCode from 'qrcode';
import { getValueByPath } from '../../core/tableBuilder';
import { normalizeFreeTableAnchors } from '../../core/freeTableGrid';
import { resolveFreeTableCellBorderSides } from '../../core/freeTableBorders';
import { lineStyleKeyToCssBorderStyle, resolveFreeTableCellLineStyleKeys } from '../../core/freeTableLineStyles';
import { redistributeColEdge, redistributeRowEdge, resolveFreeTableColWidthsMm, resolveFreeTableRowHeightsMm } from '../../core/freeTableTracks';
import type { NativeFreeTableCell, NativeFreeTableElement } from '../../core/types';
const PX_PER_MM = 3.7795275591;
const qrCodeCache = ref<Record<string, string>>({});
const barcodeCache = ref<Record<string, string>>({});
const props = withDefaults(
defineProps<{
element: NativeFreeTableElement;
previewData: Record<string, any>;
selectedCell?: { row: number; col: number } | null;
/** 与 selectedCell 配合Shift 选取的第二角(网格坐标,左上角锚点) */
mergeRangeCorner?: { row: number; col: number } | null;
isElementSelected?: boolean;
/** 画布缩放,用于把指针位移换算为 mm */
scale?: number;
}>(),
{ scale: 1 },
);
const emit = defineEmits<{
(e: 'select-cell', payload: { row: number; col: number; shiftKey?: boolean }): void;
(e: 'swap-cells', payload: { fromRow: number; fromCol: number; toRow: number; toCol: number }): void;
/** 双击单元格:打开仅针对本表的单元格编辑弹窗 */
(e: 'edit-cell', payload: { row: number; col: number }): void;
/** 列宽/行高变更mm */
(e: 'update-tracks', payload: { colWidths?: number[]; rowHeights?: number[] }): void;
}>();
const rowCount = computed(() => Math.max(1, Number(props.element?.rowCount || 1)));
const colCount = computed(() => Math.max(1, Number(props.element?.colCount || 1)));
const colWidthsMm = computed(() => resolveFreeTableColWidthsMm(props.element));
const rowHeightsMm = computed(() => resolveFreeTableRowHeightsMm(props.element));
const colGripPositionsPct = computed(() => {
const w = Math.max(0.01, Number(props.element?.w) || 0.01);
let x = 0;
const arr = colWidthsMm.value;
const out: number[] = [];
for (let i = 0; i < arr.length - 1; i += 1) {
x += arr[i];
out.push((x / w) * 100);
}
return out;
});
const rowGripPositionsPct = computed(() => {
const h = Math.max(0.01, Number(props.element?.h) || 0.01);
let y = 0;
const arr = rowHeightsMm.value;
const out: number[] = [];
for (let i = 0; i < arr.length - 1; i += 1) {
y += arr[i];
out.push((y / h) * 100);
}
return out;
});
const anchorsNormalized = computed(() =>
normalizeFreeTableAnchors(rowCount.value, colCount.value, props.element?.cells || []),
);
const mergeRect = computed(() => {
if (!props.selectedCell || !props.mergeRangeCorner) return null;
const a = props.selectedCell;
const b = props.mergeRangeCorner;
return {
r0: Math.min(a.row, b.row),
r1: Math.max(a.row, b.row),
c0: Math.min(a.col, b.col),
c1: Math.max(a.col, b.col),
};
});
function anchorsForRow(r: number) {
return anchorsNormalized.value.filter((c) => c.row === r).sort((a, b) => a.col - b.col);
}
function resolveCellContentType(cell: NativeFreeTableCell) {
return String((cell as any)?.contentType || 'text');
}
function resolveCellRawString(cell: NativeFreeTableCell) {
const bindField = String(cell?.bindField || '').trim();
if (bindField) {
const bound = getValueByPath(props.previewData || {}, bindField);
if (bound !== undefined && bound !== null && String(bound).trim()) {
return String(bound);
}
}
return String(cell?.text || '').trim();
}
function resolveCellTextForAnchor(cell: NativeFreeTableCell) {
const t = resolveCellContentType(cell);
if (t === 'number' || t === 'amount') {
return formatFreeCellNumeric(cell);
}
const raw = resolveCellRawString(cell);
return raw || ' ';
}
function formatFreeCellNumeric(cell: NativeFreeTableCell) {
const raw = resolveCellRawString(cell);
const numeric = Number(raw);
if (!Number.isFinite(numeric)) {
return raw || ' ';
}
const decimals = Math.max(0, Math.min(6, Number((cell as any)?.decimalPlaces ?? 2)));
const finalValue =
(cell as any)?.roundHalfUp === false
? Math.trunc(numeric * 10 ** decimals) / 10 ** decimals
: Number(numeric.toFixed(decimals));
const formatted = finalValue.toLocaleString(undefined, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
});
if (resolveCellContentType(cell) === 'amount') {
const symbol = (cell as any)?.amountType === 'USD' ? '$' : (cell as any)?.amountType === 'EUR' ? 'EUR ' : '¥';
return `${symbol}${formatted}`;
}
return formatted;
}
function resolveFreeCellImageSrc(cell: NativeFreeTableCell) {
const v = resolveCellRawString(cell);
if (v) {
return v;
}
return `https://via.placeholder.com/180x80.png?text=${encodeURIComponent('Image')}`;
}
function freeCellMediaStyle(cell: NativeFreeTableCell, type: 'image' | 'qrcode' | 'barcode') {
const fillCell = (cell as any)?.fillCell !== false;
const scale = Math.max(10, Math.min(100, Number((cell as any)?.contentScale || 100)));
const percent = `${scale}%`;
const base = {
display: 'block',
margin: '0 auto',
maxWidth: '100%',
maxHeight: '100%',
objectFit: (type === 'image' ? (cell as any)?.imageFit || 'contain' : 'contain') as string,
} as Record<string, any>;
if (fillCell) {
base.width = '100%';
base.height = '100%';
} else {
base.width = percent;
base.height = type === 'barcode' ? `${Math.max(20, scale * 0.6)}%` : percent;
}
return base;
}
function resolveFreeCellQrcodeSrc(cell: NativeFreeTableCell) {
const value = String(resolveCellRawString(cell) || 'qrcode_empty');
const key = `${(cell as any)?.qrLevel || 'M'}|${(cell as any)?.qrRenderType || 'image/png'}|${value}`;
if (qrCodeCache.value[key]) {
return qrCodeCache.value[key];
}
void QRCode.toDataURL(value, {
errorCorrectionLevel: (cell as any)?.qrLevel || 'M',
margin: 0,
type: (cell as any)?.qrRenderType || 'image/png',
width: 200,
})
.then((url) => {
qrCodeCache.value = { ...qrCodeCache.value, [key]: url };
})
.catch(() => {});
return '';
}
async function buildBarcodeDataUrl(value: string, format: string) {
const mod: any = await import('jsbarcode');
const JsBarcode = mod.default || mod;
const canvas = document.createElement('canvas');
JsBarcode(canvas, value || '00000000', {
format: format || 'CODE128',
displayValue: false,
margin: 0,
width: 2,
height: 70,
});
return canvas.toDataURL('image/png');
}
function resolveFreeCellBarcodeSrc(cell: NativeFreeTableCell) {
const value = String(resolveCellRawString(cell) || '00000000');
const format = String((cell as any)?.barcodeFormat || 'CODE128');
const key = `${format}|${value}`;
if (barcodeCache.value[key]) {
return barcodeCache.value[key];
}
void buildBarcodeDataUrl(value, format)
.then((url) => {
barcodeCache.value = { ...barcodeCache.value, [key]: url };
})
.catch(() => {});
return '';
}
function cellStyle(cell: NativeFreeTableCell) {
const rs = Math.max(1, Number(cell?.rowspan || 1));
const cs = Math.max(1, Number(cell?.colspan || 1));
const bw = Math.max(1, Number(props.element?.borderWidth || 1));
const bc = props.element?.borderColor || '#d9d9d9';
const sides = resolveFreeTableCellBorderSides(
props.element,
anchorsNormalized.value,
cell,
cell.row,
cell.col,
rs,
cs,
rowCount.value,
colCount.value,
);
const lineKeys = resolveFreeTableCellLineStyleKeys(
props.element,
cell.row,
cell.col,
rs,
cs,
rowCount.value,
colCount.value,
);
const nowrap = (cell as any).autoWrap === false;
return {
boxSizing: 'border-box',
textAlign: cell?.align || 'left',
verticalAlign: cell?.verticalAlign || 'middle',
fontSize: `${Number(cell?.fontSize || 12)}px`,
color: cell?.color || '#111111',
backgroundColor: cell?.backgroundColor || '#ffffff',
whiteSpace: nowrap ? 'nowrap' : 'pre-wrap',
wordBreak: nowrap ? 'normal' : 'break-all',
borderTop: sides.top ? `${bw}px ${lineStyleKeyToCssBorderStyle(lineKeys.top)} ${bc}` : 'none',
borderRight: sides.right ? `${bw}px ${lineStyleKeyToCssBorderStyle(lineKeys.right)} ${bc}` : 'none',
borderBottom: sides.bottom ? `${bw}px ${lineStyleKeyToCssBorderStyle(lineKeys.bottom)} ${bc}` : 'none',
borderLeft: sides.left ? `${bw}px ${lineStyleKeyToCssBorderStyle(lineKeys.left)} ${bc}` : 'none',
};
}
function handleSelectCell(cell: NativeFreeTableCell, e: PointerEvent) {
emit('select-cell', {
row: cell.row,
col: cell.col,
shiftKey: e.shiftKey === true,
});
}
function handleCellDblClick(cell: NativeFreeTableCell) {
emit('edit-cell', { row: cell.row, col: cell.col });
}
function isAnchorSelected(cell: NativeFreeTableCell) {
if (!props.isElementSelected || !props.selectedCell) return false;
return props.selectedCell.row === cell.row && props.selectedCell.col === cell.col;
}
function isAnchorInMergeRange(cell: NativeFreeTableCell) {
const rect = mergeRect.value;
if (!rect) return false;
const rs = Math.max(1, Number(cell.rowspan || 1));
const cs = Math.max(1, Number(cell.colspan || 1));
const a0 = cell.row;
const a1 = cell.row + rs - 1;
const b0 = cell.col;
const b1 = cell.col + cs - 1;
return !(a1 < rect.r0 || a0 > rect.r1 || b1 < rect.c0 || b0 > rect.c1);
}
function onColGripPointerDown(edge: number, e: PointerEvent) {
if (e.button !== 0) {
return;
}
const el = props.element;
const base = [...resolveFreeTableColWidthsMm(el)];
const totalW = Math.max(0.01, Number(el.w) || 0.01);
const startX = e.clientX;
const sc = Math.max(0.2, Number(props.scale) || 1);
const target = e.currentTarget as HTMLElement | null;
try {
target?.setPointerCapture?.(e.pointerId);
} catch (_err) {
/* 忽略 */
}
const onMove = (ev: PointerEvent) => {
const deltaMm = (ev.clientX - startX) / sc / PX_PER_MM;
const next = redistributeColEdge(base, edge, deltaMm, totalW);
if (next) {
emit('update-tracks', { colWidths: next });
}
};
const onUp = (ev: PointerEvent) => {
try {
target?.releasePointerCapture?.(ev.pointerId);
} catch (_err) {
/* 忽略 */
}
window.removeEventListener('pointermove', onMove);
window.removeEventListener('pointerup', onUp);
window.removeEventListener('pointercancel', onUp);
};
window.addEventListener('pointermove', onMove);
window.addEventListener('pointerup', onUp);
window.addEventListener('pointercancel', onUp);
}
function onRowGripPointerDown(edge: number, e: PointerEvent) {
if (e.button !== 0) {
return;
}
const el = props.element;
const base = [...resolveFreeTableRowHeightsMm(el)];
const totalH = Math.max(0.01, Number(el.h) || 0.01);
const startY = e.clientY;
const sc = Math.max(0.2, Number(props.scale) || 1);
const target = e.currentTarget as HTMLElement | null;
try {
target?.setPointerCapture?.(e.pointerId);
} catch (_err) {
/* 忽略 */
}
const onMove = (ev: PointerEvent) => {
const deltaMm = (ev.clientY - startY) / sc / PX_PER_MM;
const next = redistributeRowEdge(base, edge, deltaMm, totalH);
if (next) {
emit('update-tracks', { rowHeights: next });
}
};
const onUp = (ev: PointerEvent) => {
try {
target?.releasePointerCapture?.(ev.pointerId);
} catch (_err) {
/* 忽略 */
}
window.removeEventListener('pointermove', onMove);
window.removeEventListener('pointerup', onUp);
window.removeEventListener('pointercancel', onUp);
};
window.addEventListener('pointermove', onMove);
window.addEventListener('pointerup', onUp);
window.addEventListener('pointercancel', onUp);
}
function startCellSwapDrag(event: PointerEvent, fromRow: number, fromCol: number) {
if (event.button !== 0) return;
const startId = props.element.id;
const onMove = (_e: PointerEvent) => {};
const onUp = (up: PointerEvent) => {
window.removeEventListener('pointermove', onMove);
window.removeEventListener('pointerup', onUp);
const top = document.elementFromPoint(up.clientX, up.clientY) as HTMLElement | null;
const host = top?.closest?.('[data-free-table-id]') as HTMLElement | null;
if (!host || host.getAttribute('data-free-table-id') !== startId) {
return;
}
const td = top?.closest?.('td[data-ft-row]') as HTMLElement | null;
if (!td) return;
const toRow = Number(td.getAttribute('data-ft-row'));
const toCol = Number(td.getAttribute('data-ft-col'));
if (!Number.isFinite(toRow) || !Number.isFinite(toCol)) return;
if (fromRow === toRow && fromCol === toCol) return;
emit('swap-cells', { fromRow, fromCol, toRow, toCol });
};
window.addEventListener('pointermove', onMove);
window.addEventListener('pointerup', onUp);
}
</script>
<style scoped lang="less">
.free-table-element {
position: relative;
display: block;
width: 100%;
height: 100%;
overflow: hidden;
border: 1px dashed #999;
background: #fff;
box-sizing: border-box;
}
.free-table-move-handle {
position: absolute;
left: -1px;
top: -1px;
z-index: 5;
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
margin: 0;
padding: 0;
box-sizing: border-box;
border: 1px solid #bfbfbf;
border-radius: 2px;
background: linear-gradient(180deg, #ffffff 0%, #e8eef8 55%, #dce6f5 100%);
color: #1f1f1f;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
cursor: grab;
user-select: none;
line-height: 0;
&:active {
cursor: grabbing;
}
&:focus-visible {
outline: 2px solid #1677ff;
outline-offset: 1px;
}
}
.free-table-move-icon {
display: block;
flex-shrink: 0;
opacity: 0.92;
}
.free-table-surface {
position: relative;
width: 100%;
height: 100%;
min-height: 0;
/* 设计器内不显示滚动条,与打印区域一致;略溢出时裁切即可 */
overflow: hidden;
}
.free-table-track-layer {
position: absolute;
inset: 0;
z-index: 4;
pointer-events: none;
}
.track-grip {
position: absolute;
pointer-events: auto;
z-index: 6;
box-sizing: border-box;
}
.track-grip--col {
top: 0;
bottom: 0;
width: 10px;
margin-left: -5px;
cursor: col-resize;
background: rgba(22, 119, 255, 0.14);
border-radius: 2px;
}
.track-grip--row {
left: 0;
right: 0;
height: 10px;
margin-top: -5px;
cursor: row-resize;
background: rgba(22, 119, 255, 0.14);
border-radius: 2px;
}
.track-grip:hover {
background: rgba(22, 119, 255, 0.28);
}
.free-table-surface table {
width: 100%;
height: 100%;
border-collapse: collapse;
border-spacing: 0;
table-layout: fixed;
overflow: hidden;
box-sizing: border-box;
}
.free-table-surface tbody tr {
box-sizing: border-box;
}
.free-table-cell {
position: relative;
box-sizing: border-box;
min-width: 20px;
min-height: 20px;
padding: 10px 4px 4px;
white-space: pre-wrap;
word-break: break-all;
user-select: none;
cursor: pointer;
vertical-align: top;
}
.free-table-cell.is-merge-range:not(.is-selected) {
box-shadow: inset 0 0 0 1px rgba(250, 140, 22, 0.65);
background-color: rgba(255, 247, 230, 0.55);
}
.cell-body {
display: block;
}
.table-media-free {
box-sizing: border-box;
}
.cell-move-handle {
position: absolute;
left: 50%;
top: 1px;
transform: translateX(-50%);
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
min-width: 22px;
height: 12px;
padding: 0 4px;
border-radius: 3px;
background: #1677ff;
color: #fff;
font-size: 10px;
line-height: 1;
opacity: 0;
pointer-events: none;
cursor: grab;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
transition: opacity 0.12s ease;
&:active {
cursor: grabbing;
}
}
.cell-move-handle-icon {
display: block;
transform: scaleY(0.85);
font-weight: 700;
}
.free-table-cell:hover .cell-move-handle,
.free-table-cell.is-selected .cell-move-handle {
opacity: 1;
pointer-events: auto;
}
.free-table-cell.is-selected {
box-shadow: inset 0 0 0 2px #1677ff;
z-index: 1;
}
</style>

View File

@@ -0,0 +1,51 @@
<template>
<div class="image-element">
<img v-if="renderSrc" :src="renderSrc" :style="{ objectFit: element.fit || 'contain' }" />
<div v-else class="image-placeholder">图片</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import type { NativeImageElement } from '../../core/types';
const props = defineProps<{
element: NativeImageElement;
previewData?: Record<string, any>;
}>();
function resolveFieldValue(field?: string) {
if (!field) return undefined;
return field.split('.').reduce((acc: any, key) => acc?.[key], props.previewData || {});
}
const renderSrc = computed(() => {
const value = resolveFieldValue(props.element.bindField);
if (value) {
return String(value);
}
return props.element.src;
});
</script>
<style scoped lang="less">
.image-element {
width: 100%;
height: 100%;
border: 1px dashed #999;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
img {
width: 100%;
height: 100%;
}
.image-placeholder {
font-size: 12px;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<div class="qrcode-element">
<img v-if="url" :src="url" alt="qrcode" />
<div v-else class="qrcode-placeholder">二维码</div>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import QRCode from 'qrcode';
import type { NativeCodeElement } from '../../core/types';
const props = defineProps<{
element: NativeCodeElement;
previewData?: Record<string, any>;
}>();
const url = ref('');
function resolveFieldValue(field?: string) {
if (!field) return undefined;
return field.split('.').reduce((acc: any, key) => acc?.[key], props.previewData || {});
}
async function renderCode() {
const bindValue = resolveFieldValue(props.element.bindField);
const value = bindValue !== undefined && bindValue !== null ? String(bindValue) : props.element.value || 'empty';
url.value = await QRCode.toDataURL(value);
}
watch(
() => [props.element.value, props.element.bindField, props.previewData],
() => {
renderCode();
},
{ immediate: true },
);
</script>
<style scoped lang="less">
.qrcode-element {
width: 100%;
height: 100%;
border: 1px dashed #999;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
img {
width: 100%;
height: 100%;
}
.qrcode-placeholder {
color: #666;
font-size: 12px;
}
}
</style>

View File

@@ -0,0 +1,747 @@
<template>
<div ref="tableWrapRef" class="table-element">
<table>
<colgroup>
<col v-for="col in columns" :key="`col_width_${col.key}`" :style="{ width: `${col.widthPercent || 0}%` }" />
</colgroup>
<thead v-if="element.showHeader">
<tr v-for="(rowCells, rowIndex) in headerRenderRows" :key="`header_row_${rowIndex}`" :style="headerRowStyle">
<th
v-for="cell in rowCells"
:key="cell.id"
:rowspan="cell.rowspan"
:colspan="cell.colspan"
:class="{ 'is-selected-col': isCellSelected(cell) }"
:style="headerCellStyle(cell)"
@click="handleHeaderClick(cell)"
@dblclick.stop="startHeaderEdit(cell)"
>
<template v-if="editingHeaderCellId === cell.id">
<input
ref="editingInputRef"
class="header-inline-input"
:value="editingHeaderText"
@input="editingHeaderText = ($event.target as HTMLInputElement).value"
@blur="commitHeaderEdit()"
@keydown.enter.prevent="commitHeaderEdit()"
@keydown.esc.prevent="cancelHeaderEdit()"
/>
</template>
<template v-else>
{{ cell.title }}
</template>
<span v-if="canResizeFromCell(cell)" class="col-resize-handle" @pointerdown.stop.prevent="startResizeColumn($event, cell.col)" />
</th>
</tr>
</thead>
<tbody>
<tr v-for="item in renderRowsWithMarkers" :key="item.key" :style="item.kind === 'data' ? bodyRowStyle : undefined" :class="{ 'page-break-marker-row': item.kind === 'marker' }">
<td v-if="item.kind === 'marker'" :colspan="Math.max(1, columns.length)" class="page-break-marker-cell">
{{ item.pageNo }} 页起始
</td>
<template v-else-if="item.kind === 'footer'">
<td v-for="(col, colIndex) in columns" :key="`${item.key}_${col.key}`" :style="footerCellStyle(col, colIndex)">
<template v-if="isFooterColumn(col)">
{{ formatNumericValue(resolveFooterTotalByRange(col, item.start, item.end), col) }}
</template>
<template v-else-if="isFooterLabelColumn(col, colIndex)">{{ footerLabelText }}</template>
</td>
</template>
<template v-else>
<template v-for="col in columns" :key="`${item.rowIndex}_${col.key}`">
<td v-if="showCell(item.rowIndex, col.bindField || col.field)" :rowspan="rowSpan(item.rowIndex, col.bindField || col.field)" :style="bodyCellStyle(col, item.row)">
<template v-if="resolveColumnContentType(col) === 'text'">
{{ resolveCellValue(item.row, col.bindField || col.field) }}
</template>
<template v-else-if="resolveColumnContentType(col) === 'number' || resolveColumnContentType(col) === 'amount'">
{{ formatNumericValue(resolveCellValue(item.row, col.bindField || col.field), col) }}
</template>
<template v-else-if="resolveColumnContentType(col) === 'image'">
<img class="table-media" :src="resolveImageSrc(item.row, col)" :style="mediaStyle(col, 'image')" />
</template>
<template v-else-if="resolveColumnContentType(col) === 'qrcode'">
<img class="table-media" :src="resolveQrcodeSrc(item.row, col)" :style="mediaStyle(col, 'qrcode')" />
</template>
<template v-else-if="resolveColumnContentType(col) === 'barcode'">
<img class="table-media" :src="resolveBarcodeSrc(item.row, col)" :style="mediaStyle(col, 'barcode')" />
</template>
</td>
</template>
</template>
</tr>
</tbody>
<tfoot v-if="showFooterTotal && !showPagedFooterRows">
<tr class="table-footer-row">
<td v-for="(col, colIndex) in columns" :key="`footer_${col.key}`" :style="footerCellStyle(col, colIndex)">
<template v-if="isFooterColumn(col)">
{{ formatNumericValue(resolveFooterTotal(col), col) }}
</template>
<template v-else-if="isFooterLabelColumn(col, colIndex)">{{ footerLabelText }}</template>
</td>
</tr>
</tfoot>
</table>
</div>
</template>
<script lang="ts" setup>
import { computed, nextTick, ref, watchEffect } from 'vue';
import QRCode from 'qrcode';
import { buildRowSpanMap } from '../../core/tableMerge';
import { getValueByPath, normalizeTableWidths, resolveTableRows } from '../../core/tableBuilder';
import type { NativeTableElement } from '../../core/types';
const props = defineProps<{
element: NativeTableElement;
previewData: Record<string, any>;
selectedColumnKey?: string;
isElementSelected?: boolean;
}>();
const emit = defineEmits<{
(e: 'select-column', payload: { columnKey: string }): void;
(e: 'update-columns', payload: { columns: any[] }): void;
(e: 'update-header-config', payload: { headerConfig: any }): void;
}>();
const tableWrapRef = ref<HTMLElement | null>(null);
const editingInputRef = ref<HTMLInputElement | null>(null);
const editingHeaderCellId = ref('');
const editingHeaderColumnKey = ref('');
const editingHeaderText = ref('');
const qrCodeCache = ref<Record<string, string>>({});
const barcodeCache = ref<Record<string, string>>({});
const rows = computed(() => {
const resolved = resolveTableRows(props.element, props.previewData || {});
return resolved.length ? resolved : [{ field1: '示例A', field2: '示例B', field3: '示例C' }];
});
const renderedRows = computed(() => {
return rows.value;
});
const renderRowsWithMarkers = computed(() => {
const list: Array<
| { kind: 'marker'; key: string; pageNo: number }
| { kind: 'footer'; key: string; start: number; end: number }
| { kind: 'data'; key: string; row: Record<string, any>; rowIndex: number }
> = [];
const pageSize = Math.max(1, Number(props.element.fixedRows || 5));
const pushFooterRow = (start: number, end: number) => {
if (!showPagedFooterRows.value) return;
if (start >= end) return;
list.push({
kind: 'footer',
key: `footer_${start}_${end}`,
start,
end,
});
};
renderedRows.value.forEach((row, rowIndex) => {
if (isPageBreakRow(rowIndex)) {
const prevStart = Math.max(0, rowIndex - pageSize);
pushFooterRow(prevStart, rowIndex);
list.push({
kind: 'marker',
key: `marker_${rowIndex}`,
pageNo: resolvePageNo(rowIndex),
});
}
list.push({
kind: 'data',
key: `data_${rowIndex}`,
row,
rowIndex,
});
});
if (renderedRows.value.length) {
const total = renderedRows.value.length;
const pageStart = Math.floor((total - 1) / pageSize) * pageSize;
pushFooterRow(pageStart, total);
}
return list;
});
const columns = computed(() => normalizeTableWidths(props.element));
const headerRenderRows = computed(() => buildHeaderRenderRows());
const headerRowCount = computed(() => Math.max(1, headerRenderRows.value.length));
const spanMap = computed(() => {
if (!isFixedRowsPagination()) {
return buildRowSpanMap(
renderedRows.value,
props.element.columns,
(props.element as any).mergeColumnKeys || [],
(props.element as any).strictGrouping !== false,
);
}
const pageSize = Math.max(1, Number(props.element.fixedRows || 5));
const mergedMap: Record<string, number> = {};
for (let start = 0; start < renderedRows.value.length; start += pageSize) {
const chunk = renderedRows.value.slice(start, start + pageSize);
const chunkMap = buildRowSpanMap(
chunk,
props.element.columns,
(props.element as any).mergeColumnKeys || [],
(props.element as any).strictGrouping !== false,
);
Object.entries(chunkMap).forEach(([key, value]) => {
const [rowIndex, field] = key.split('_');
const absoluteRow = Number(rowIndex) + start;
mergedMap[`${absoluteRow}_${field}`] = value;
});
}
return mergedMap;
});
const showFooterTotal = computed(() => {
return (props.element as any).footerShowTotal !== false;
});
const showPagedFooterRows = computed(() => {
return showFooterTotal.value && isFixedRowsPagination() && String((props.element as any).footerTotalMode || 'overall') === 'page';
});
const footerLabelText = computed(() => String(props.element.footerLabelText || '合计'));
const footerLabelColumnKey = computed(() => String(props.element.footerLabelColumnKey || columns.value?.[0]?.key || ''));
const footerLabelCenter = computed(() => props.element.footerLabelCenter !== false);
const headerRowStyle = computed(() => ({
height: `${(props.element.headerHeight || 10) / headerRowCount.value}mm`,
}));
const bodyRowStyle = computed(() => ({
height: `${props.element.rowHeight || 8}mm`,
}));
function rowSpan(rowIndex: number, field: string) {
return spanMap.value[`${rowIndex}_${field}`] || 1;
}
function showCell(rowIndex: number, field: string) {
return spanMap.value[`${rowIndex}_${field}`] !== 0;
}
function resolveCellValue(row: Record<string, any>, field: string) {
const value = getValueByPath(row || {}, field);
return value ?? '';
}
function resolveColumnContentType(col: any) {
return String(col?.contentType || 'text');
}
function isNumericColumn(col: any) {
const type = resolveColumnContentType(col);
return type === 'number' || type === 'amount';
}
function isFooterColumn(col: any) {
return isNumericColumn(col) && !!col?.enableFooterTotal;
}
function resolveFooterTotal(col: any) {
const field = col?.bindField || col?.field;
return resolveFooterRows().reduce((sum, row) => {
const value = Number(resolveCellValue(row, field));
return sum + (Number.isFinite(value) ? value : 0);
}, 0);
}
function resolveFooterTotalByRange(col: any, start: number, end: number) {
const field = col?.bindField || col?.field;
return renderedRows.value.slice(start, end).reduce((sum, row) => {
const value = Number(resolveCellValue(row, field));
return sum + (Number.isFinite(value) ? value : 0);
}, 0);
}
function resolveFooterRows() {
const mode = String((props.element as any).footerTotalMode || 'overall');
if (mode !== 'page' || !isFixedRowsPagination()) {
return renderedRows.value;
}
const pageSize = Math.max(1, Number(props.element.fixedRows || 5));
const total = renderedRows.value.length;
if (!total) return [];
const lastPageStart = Math.floor((total - 1) / pageSize) * pageSize;
return renderedRows.value.slice(lastPageStart, lastPageStart + pageSize);
}
function formatNumericValue(value: any, col: any) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return String(value ?? '');
}
const decimals = Math.max(0, Math.min(6, Number(col?.decimalPlaces ?? 2)));
const finalValue = col?.roundHalfUp === false ? Math.trunc(numeric * 10 ** decimals) / 10 ** decimals : Number(numeric.toFixed(decimals));
const formatted = finalValue.toLocaleString(undefined, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
});
if (resolveColumnContentType(col) === 'amount') {
const symbol = col?.amountType === 'USD' ? '$' : col?.amountType === 'EUR' ? 'EUR ' : '¥';
return `${symbol}${formatted}`;
}
return formatted;
}
function isFooterLabelColumn(col: any, colIndex: number) {
const key = String(col?.key || '');
if (footerLabelColumnKey.value) {
return key === footerLabelColumnKey.value;
}
return colIndex === 0;
}
function footerCellStyle(col: any, colIndex: number) {
const base = bodyCellStyle(col, {}) as Record<string, any>;
if (isFooterLabelColumn(col, colIndex) && !isFooterColumn(col)) {
base.textAlign = footerLabelCenter.value ? 'center' : 'left';
base.fontWeight = 600;
}
if (isFooterColumn(col)) {
base.fontWeight = 600;
base.background = '#fafafa';
}
return base;
}
function resolveImageSrc(row: Record<string, any>, col: any) {
const raw = resolveCellValue(row, col.bindField || col.field);
const value = String(raw || '').trim();
if (value) return value;
return `https://via.placeholder.com/180x80.png?text=${encodeURIComponent(col?.title || 'Image')}`;
}
function mediaStyle(col: any, type: 'image' | 'qrcode' | 'barcode') {
const fillCell = col?.fillCell !== false;
const scale = Math.max(10, Math.min(100, Number(col?.contentScale || 100)));
const percent = `${scale}%`;
const base = {
display: 'block',
margin: '0 auto',
maxWidth: '100%',
maxHeight: '100%',
objectFit: type === 'image' ? col?.imageFit || 'contain' : 'contain',
} as Record<string, any>;
if (fillCell) {
base.width = '100%';
base.height = '100%';
} else {
base.width = percent;
base.height = type === 'barcode' ? `${Math.max(20, scale * 0.6)}%` : percent;
}
return base;
}
function resolveQrcodeSrc(row: Record<string, any>, col: any) {
const value = String(resolveCellValue(row, col.bindField || col.field) || `${col?.title || 'qrcode'}_empty`);
const key = `${col?.qrLevel || 'M'}|${col?.qrRenderType || 'image/png'}|${value}`;
if (qrCodeCache.value[key]) {
return qrCodeCache.value[key];
}
QRCode.toDataURL(value, {
errorCorrectionLevel: col?.qrLevel || 'M',
margin: 0,
type: col?.qrRenderType || 'image/png',
width: 200,
})
.then((url) => {
qrCodeCache.value = { ...qrCodeCache.value, [key]: url };
})
.catch(() => {
// ignore
});
return '';
}
async function buildBarcodeDataUrl(value: string, format: string) {
const mod: any = await import('jsbarcode');
const JsBarcode = mod.default || mod;
const canvas = document.createElement('canvas');
JsBarcode(canvas, value || '00000000', {
format: format || 'CODE128',
displayValue: false,
margin: 0,
width: 2,
height: 70,
});
return canvas.toDataURL('image/png');
}
function resolveBarcodeSrc(row: Record<string, any>, col: any) {
const value = String(resolveCellValue(row, col.bindField || col.field) || '00000000');
const format = String(col?.barcodeFormat || 'CODE128');
const key = `${format}|${value}`;
if (barcodeCache.value[key]) {
return barcodeCache.value[key];
}
buildBarcodeDataUrl(value, format)
.then((url) => {
barcodeCache.value = { ...barcodeCache.value, [key]: url };
})
.catch(() => {
// ignore
});
return '';
}
function handleSelectColumn(col: any) {
if (!isLeafColumnCell(col)) {
return;
}
const key = String(col?.columnKey || col?.key || '');
if (!key) return;
emit('select-column', { columnKey: key });
}
function handleHeaderClick(col: any) {
// 交互约定:第一次单击先选中组件;组件已选中后再次单击表头才选中列
if (!props.isElementSelected) {
return;
}
handleSelectColumn(col);
}
function startHeaderEdit(col: any) {
handleSelectColumn(col);
editingHeaderCellId.value = String(col?.id || '');
editingHeaderColumnKey.value = String(col?.columnKey || col?.key || '');
editingHeaderText.value = String(col?.title || '');
nextTick(() => {
editingInputRef.value?.focus();
editingInputRef.value?.select();
});
}
function commitHeaderEdit() {
const nextTitle = String(editingHeaderText.value || '').trim() || '未命名列';
const cellId = editingHeaderCellId.value;
const headerConfig = (props.element as any)?.headerConfig;
if (cellId && headerConfig?.cells?.length) {
const nextHeader = {
...headerConfig,
cells: headerConfig.cells.map((item: any) => (item?.id === cellId ? { ...item, title: nextTitle } : { ...item })),
};
emit('update-header-config', { headerConfig: nextHeader });
} else {
const key = editingHeaderColumnKey.value;
const nextColumns = (props.element.columns || []).map((item: any) => ({ ...item }));
const target = nextColumns.find((item: any) => item?.key === key);
if (target) {
target.title = nextTitle;
emit('update-columns', { columns: nextColumns });
}
}
editingHeaderCellId.value = '';
editingHeaderColumnKey.value = '';
}
function cancelHeaderEdit() {
editingHeaderCellId.value = '';
editingHeaderColumnKey.value = '';
}
function startResizeColumn(event: PointerEvent, colIndex: number) {
const baseColumns = Array.isArray(props.element.columns) ? props.element.columns : [];
if (!baseColumns.length || colIndex < 0 || colIndex >= baseColumns.length - 1) {
return;
}
const tableWidthPx = Math.max(1, tableWrapRef.value?.clientWidth || 1);
const totalWidth = baseColumns.reduce((sum, item) => sum + Number(item?.width || 0), 0) || 1;
const leftStart = Number(baseColumns[colIndex]?.width || 0);
const rightStart = Number(baseColumns[colIndex + 1]?.width || 0);
const startX = event.clientX;
const minWidth = 10;
const onMove = (moveEvent: PointerEvent) => {
const deltaPx = moveEvent.clientX - startX;
const deltaWidth = (deltaPx / tableWidthPx) * totalWidth;
const minDelta = -(leftStart - minWidth);
const maxDelta = rightStart - minWidth;
const clampedDelta = Math.max(minDelta, Math.min(maxDelta, deltaWidth));
const nextColumns = baseColumns.map((item) => ({ ...item }));
nextColumns[colIndex].width = Number((leftStart + clampedDelta).toFixed(2));
nextColumns[colIndex + 1].width = Number((rightStart - clampedDelta).toFixed(2));
emit('update-columns', { columns: nextColumns });
};
const onUp = () => {
window.removeEventListener('pointermove', onMove);
window.removeEventListener('pointerup', onUp);
};
window.addEventListener('pointermove', onMove);
window.addEventListener('pointerup', onUp);
}
function headerCellStyle(cell: any) {
const col = columns.value?.[cell.col] || {};
const fontSize = resolveHeaderFontSize(col);
const isSelected = isCellSelected(cell);
const heightMm = ((props.element.headerHeight || 10) / headerRowCount.value) * Number(cell.rowspan || 1);
return {
textAlign: cell.align || col.align || 'center',
height: `${heightMm}mm`,
lineHeight: col?.autoWrap === false ? `${heightMm}mm` : '1.3',
backgroundColor: isSelected ? '#e6f4ff' : props.element.headerBgColor || '#f5f5f5',
color: isSelected ? '#1677ff' : col?.fontColor || props.element.headerTextColor || '#111111',
fontFamily: col?.fontFamily || 'inherit',
fontSize: `${fontSize}px`,
whiteSpace: col?.autoWrap === false ? 'nowrap' : 'normal',
wordBreak: col?.autoWrap === false ? 'normal' : 'break-all',
overflowWrap: col?.autoWrap === false ? 'normal' : 'anywhere',
boxSizing: 'border-box',
boxShadow: isSelected ? 'inset 0 0 0 2px #1677ff' : 'none',
position: 'relative',
zIndex: isSelected ? 1 : 0,
};
}
function buildHeaderRenderRows() {
const cols = columns.value || [];
const colCount = cols.length;
if (!colCount) return [];
if (props.element.enableMultiHeader !== true) {
return [
cols.map((col: any, index: number) => ({
id: `single_${index}`,
row: 0,
col: index,
rowspan: 1,
colspan: 1,
title: String(col?.title || ''),
align: col?.align || 'center',
widthPercent: Number(col?.widthPercent || 0),
columnKey: col?.key,
})),
];
}
const headerConfig = (props.element as any)?.headerConfig;
const rowCount = Math.max(1, Number(headerConfig?.rowCount || 1));
const owner: any[][] = Array.from({ length: rowCount }, () => Array.from({ length: colCount }, () => null));
const cells: any[] = [];
const configCells = Array.isArray(headerConfig?.cells) && Number(headerConfig?.colCount || 0) === colCount ? headerConfig.cells : [];
configCells.forEach((item: any) => {
const row = Math.max(0, Number(item?.row || 0));
const col = Math.max(0, Number(item?.col || 0));
const rowspan = Math.max(1, Number(item?.rowspan || 1));
const colspan = Math.max(1, Number(item?.colspan || 1));
if (row >= rowCount || col >= colCount) return;
const maxRow = Math.min(rowCount, row + rowspan);
const maxCol = Math.min(colCount, col + colspan);
if (owner[row][col]) return;
for (let r = row; r < maxRow; r += 1) {
for (let c = col; c < maxCol; c += 1) {
if (owner[r][c]) return;
}
}
const next = {
id: String(item?.id || `h_${row}_${col}`),
row,
col,
rowspan: maxRow - row,
colspan: maxCol - col,
title: String(item?.title || ''),
align: String(item?.align || 'center'),
};
for (let r = row; r < maxRow; r += 1) {
for (let c = col; c < maxCol; c += 1) {
owner[r][c] = next;
}
}
cells.push(next);
});
for (let r = 0; r < rowCount; r += 1) {
for (let c = 0; c < colCount; c += 1) {
if (owner[r][c]) continue;
const fallback = {
id: `auto_${r}_${c}`,
row: r,
col: c,
rowspan: 1,
colspan: 1,
title: r === rowCount - 1 ? String(cols[c]?.title || '') : '',
align: cols[c]?.align || 'center',
};
owner[r][c] = fallback;
cells.push(fallback);
}
}
const rows = Array.from({ length: rowCount }, () => [] as any[]);
cells.forEach((cell) => {
if (owner[cell.row][cell.col] !== cell) return;
let widthPercent = 0;
for (let i = 0; i < cell.colspan; i += 1) {
widthPercent += Number(cols[cell.col + i]?.widthPercent || 0);
}
rows[cell.row].push({
...cell,
widthPercent,
columnKey: cols[cell.col]?.key,
});
});
rows.forEach((list) => list.sort((a, b) => a.col - b.col));
return rows;
}
function isCellSelected(cell: any) {
const key = String(props.selectedColumnKey || '');
if (!key) return false;
const selectedIndex = columns.value.findIndex((item: any) => item?.key === key);
if (selectedIndex < 0) return false;
return selectedIndex >= cell.col && selectedIndex < cell.col + cell.colspan;
}
function canResizeFromCell(cell: any) {
return cell.row + cell.rowspan === headerRowCount.value && cell.colspan === 1 && cell.col < columns.value.length - 1;
}
function isLeafColumnCell(cell: any) {
return cell.row + cell.rowspan === headerRowCount.value && cell.colspan === 1;
}
function bodyCellStyle(col: any, row: Record<string, any>) {
const raw = resolveCellValue(row, col.bindField || col.field);
const text = isNumericColumn(col) ? String(formatNumericValue(raw, col)) : String(raw ?? '');
const fontSize = resolveBodyFontSize(col, text);
return {
textAlign: col.align || 'left',
height: `${props.element.rowHeight || 8}mm`,
lineHeight: col?.autoWrap === false ? `${props.element.rowHeight || 8}mm` : '1.3',
fontFamily: col?.fontFamily || 'inherit',
fontSize: `${fontSize}px`,
color: col?.fontColor || '#111111',
whiteSpace: col?.autoWrap === false ? 'nowrap' : 'normal',
wordBreak: col?.autoWrap === false ? 'normal' : 'break-all',
overflowWrap: col?.autoWrap === false ? 'normal' : 'anywhere',
boxSizing: 'border-box',
};
}
function resolveAutoFontSize(col: any, text: string, rowHeightMm: number, baseSize: number) {
const base = Number(baseSize || 12);
if (!col?.autoFitFont) {
return base;
}
const tableWidthPx = Math.max(1, tableWrapRef.value?.clientWidth || 600);
const colWidthPx = Math.max(1, (tableWidthPx * Number(col?.widthPercent || 0)) / 100);
const heightPx = Math.max(1, rowHeightMm * 3.7795275591);
const textLen = Math.max(1, text.length);
const byWidth = colWidthPx / Math.max(1, textLen * 0.62);
const byHeight = col?.autoWrap === false ? heightPx * 0.55 : heightPx * 0.36;
const next = Math.min(base, byWidth, byHeight);
return Math.max(8, Math.round(next));
}
function resolveHeaderFontSize(_col: any) {
return Number(props.element.headerFontSize || 12);
}
function resolveBodyFontSize(col: any, text: string) {
const base = col?.useCustomFontSize ? Number(col?.fontSize || 12) : Number(props.element.bodyFontSize || 12);
return resolveAutoFontSize(col, text, props.element.rowHeight || 8, base);
}
function isPageBreakRow(rowIndex: number) {
if (!isFixedRowsPagination()) {
return false;
}
const pageSize = Math.max(1, Number(props.element.fixedRows || 5));
return rowIndex > 0 && rowIndex % pageSize === 0;
}
function resolvePageNo(rowIndex: number) {
const pageSize = Math.max(1, Number(props.element.fixedRows || 5));
return Math.floor(rowIndex / pageSize) + 1;
}
function isFixedRowsPagination() {
return String(props.element.tableHeightMode || 'autoPage') === 'fixedRows';
}
watchEffect(() => {
const rowsValue = renderedRows.value || [];
const cols = columns.value || [];
rowsValue.forEach((row) => {
cols.forEach((col: any) => {
const type = resolveColumnContentType(col);
if (type === 'qrcode') {
resolveQrcodeSrc(row, col);
} else if (type === 'barcode') {
resolveBarcodeSrc(row, col);
}
});
});
});
</script>
<style scoped lang="less">
.table-element {
width: 100%;
height: 100%;
overflow: auto;
border: 1px dashed #999;
background: #fff;
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
font-size: 12px;
}
th,
td {
border: 1px solid #d9d9d9;
padding: 2px 4px;
}
th {
cursor: pointer;
user-select: none;
position: relative;
}
.is-selected-col {
background: #e6f4ff;
color: #1677ff;
}
.col-resize-handle {
position: absolute;
top: 0;
right: -3px;
width: 6px;
height: 100%;
cursor: col-resize;
z-index: 2;
}
.header-inline-input {
width: calc(100% - 6px);
border: 1px solid #1677ff;
border-radius: 4px;
outline: none;
font-size: inherit;
line-height: 1.2;
padding: 1px 3px;
background: #fff;
color: inherit;
box-sizing: border-box;
}
.table-media {
background: transparent;
}
.table-footer-row {
td {
font-weight: 600;
background: #fafafa;
}
}
.page-break-marker-row td {
border-top: 2px dashed #fa8c16;
border-bottom: 1px dashed #fa8c16;
background: #fff7e6;
color: #d46b08;
text-align: center;
font-size: 11px;
line-height: 1.4;
padding: 2px 6px;
}
}
</style>

View File

@@ -0,0 +1,55 @@
<template>
<div class="text-element" :style="styleObject">
{{ displayText }}
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import dayjs from 'dayjs';
import type { NativeReportBandElement, NativeTextElement } from '../../core/types';
const props = defineProps<{
element: NativeTextElement | NativeReportBandElement;
previewData?: Record<string, any>;
}>();
function resolveFieldValue(field?: string) {
if (!field) return undefined;
return field.split('.').reduce((acc: any, key) => acc?.[key], props.previewData || {});
}
const displayText = computed(() => {
const bindValue = resolveFieldValue(props.element.bindField);
if (bindValue !== undefined && bindValue !== null && props.element.type !== 'pageNo') {
if (props.element.type === 'date') {
return dayjs(bindValue).format(props.element.format || 'YYYY-MM-DD');
}
return String(bindValue);
}
if (props.element.type === 'date') {
return dayjs().format(props.element.format || 'YYYY-MM-DD');
}
if (props.element.type === 'pageNo') {
return props.element.text || '第 1 / 1 页';
}
return props.element.text || '';
});
const styleObject = computed(() => ({
fontSize: `${props.element.style?.fontSize || 12}px`,
fontWeight: String(props.element.style?.fontWeight || 400),
color: props.element.style?.color || '#111',
textAlign: props.element.style?.textAlign || 'left',
lineHeight: String(props.element.style?.lineHeight || 1.4),
width: '100%',
height: '100%',
whiteSpace: 'pre-wrap',
overflow: 'hidden',
backgroundColor: props.element.style?.backgroundColor || 'transparent',
display: (props.element as any)?.visible === false ? 'none' : 'block',
borderTop: props.element.type === 'reportHeader' || props.element.type === 'reportFooter' ? '1px dashed rgba(22,119,255,0.5)' : 'none',
borderBottom: props.element.type === 'reportHeader' || props.element.type === 'reportFooter' ? '1px dashed rgba(22,119,255,0.5)' : 'none',
background: props.element.type === 'reportHeader' || props.element.type === 'reportFooter' ? 'rgba(22,119,255,0.06)' : props.element.style?.backgroundColor || 'transparent',
}));
</script>

View File

@@ -0,0 +1,59 @@
interface Rect {
x: number;
y: number;
w: number;
h: number;
}
type Direction = 'n' | 's' | 'w' | 'e' | 'nw' | 'ne' | 'sw' | 'se';
const MIN_SIZE = 6;
function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value));
}
function roundToGrid(value: number, gridSize: number) {
if (!gridSize || gridSize <= 1) return value;
return Math.round(value / gridSize) * gridSize;
}
export function calcDragRect(
startRect: Rect,
pageSize: { width: number; height: number },
deltaX: number,
deltaY: number,
gridSize: number,
) {
const x = clamp(roundToGrid(startRect.x + deltaX, gridSize), 0, pageSize.width - startRect.w);
const y = clamp(roundToGrid(startRect.y + deltaY, gridSize), 0, pageSize.height - startRect.h);
return { ...startRect, x, y };
}
export function calcResizeRect(
direction: Direction,
startRect: Rect,
pageSize: { width: number; height: number },
deltaX: number,
deltaY: number,
gridSize: number,
) {
const next = { ...startRect };
if (direction.includes('e')) {
next.w = clamp(roundToGrid(startRect.w + deltaX, gridSize), MIN_SIZE, pageSize.width - startRect.x);
}
if (direction.includes('s')) {
next.h = clamp(roundToGrid(startRect.h + deltaY, gridSize), MIN_SIZE, pageSize.height - startRect.y);
}
if (direction.includes('w')) {
const newX = clamp(roundToGrid(startRect.x + deltaX, gridSize), 0, startRect.x + startRect.w - MIN_SIZE);
next.w = clamp(roundToGrid(startRect.w + (startRect.x - newX), gridSize), MIN_SIZE, pageSize.width - newX);
next.x = newX;
}
if (direction.includes('n')) {
const newY = clamp(roundToGrid(startRect.y + deltaY, gridSize), 0, startRect.y + startRect.h - MIN_SIZE);
next.h = clamp(roundToGrid(startRect.h + (startRect.y - newY), gridSize), MIN_SIZE, pageSize.height - newY);
next.y = newY;
}
return next;
}

View File

@@ -0,0 +1,19 @@
import '@fontsource/noto-sans-sc/400.css';
import '@fontsource/noto-sans-sc/700.css';
import '@fontsource/noto-serif-sc/400.css';
import '@fontsource/noto-serif-sc/700.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/700.css';
import '@fontsource/open-sans/400.css';
import '@fontsource/open-sans/700.css';
export const TABLE_FONT_OPTIONS = [
{ label: '默认字体', value: '' },
{ label: 'Noto Sans SC思源黑体', value: '"Noto Sans SC", sans-serif' },
{ label: 'Noto Serif SC思源宋体', value: '"Noto Serif SC", serif' },
{ label: 'Roboto', value: 'Roboto, sans-serif' },
{ label: 'Open Sans', value: '"Open Sans", sans-serif' },
{ label: 'Microsoft YaHei微软雅黑', value: '"Microsoft YaHei", sans-serif' },
{ label: 'SimSun宋体', value: 'SimSun, serif' },
{ label: 'SimHei黑体', value: 'SimHei, sans-serif' },
];

View File

@@ -0,0 +1,143 @@
/**
* 自由表格:外框/内线与单元格单边隐藏 → 四边是否绘制边框
*/
import { lineStyleKeyToCssBorderStyle } from './freeTableLineStyles';
import { getFreeTableOwnerAt, type FreeTableAnchorCell } from './freeTableGrid';
export interface FreeTableBorderSides {
top: boolean;
right: boolean;
bottom: boolean;
left: boolean;
}
/** 表格外轮廓四边,缺省均为显示 */
export function resolveOuterBorderFlags(el: { outerBorder?: Record<string, boolean | undefined> } | null | undefined): FreeTableBorderSides {
const o = el?.outerBorder;
return {
top: o?.top !== false,
right: o?.right !== false,
bottom: o?.bottom !== false,
left: o?.left !== false,
};
}
/** 内部网格线:横向(行间)、纵向(列间),缺省均为显示 */
export function resolveInnerBorderFlags(el: { innerBorder?: { horizontal?: boolean; vertical?: boolean } } | null | undefined): {
horizontal: boolean;
vertical: boolean;
} {
const i = el?.innerBorder;
return {
horizontal: i?.horizontal !== false,
vertical: i?.vertical !== false,
};
}
/**
* 计算锚点单元格(含合并)四边是否画线。
* 共享边:若本格或相邻格任一侧声明隐藏(如上格的 bottom 与本格的 top则两边都不画避免预览/打印仍留线。
*/
export function resolveFreeTableCellBorderSides(
el: any,
anchors: FreeTableAnchorCell[],
cell: any,
anchorRow: number,
anchorCol: number,
rs: number,
cs: number,
rowCount: number,
colCount: number,
): FreeTableBorderSides {
const outer = resolveOuterBorderFlags(el);
const inner = resolveInnerBorderFlags(el);
const rEnd = anchorRow + rs - 1;
const cEnd = anchorCol + cs - 1;
let top = anchorRow === 0 ? outer.top : inner.horizontal;
let right = cEnd === colCount - 1 ? outer.right : inner.vertical;
let bottom = rEnd === rowCount - 1 ? outer.bottom : inner.horizontal;
let left = anchorCol === 0 ? outer.left : inner.vertical;
if (cell?.hideBorderTop === true) {
top = false;
}
if (cell?.hideBorderRight === true) {
right = false;
}
if (cell?.hideBorderBottom === true) {
bottom = false;
}
if (cell?.hideBorderLeft === true) {
left = false;
}
// 与上方格共享横线:对方 hideBorderBottom 则本格也不画上边
if (top && anchorRow > 0) {
for (let cc = anchorCol; cc <= anchorCol + cs - 1; cc += 1) {
const up = getFreeTableOwnerAt(anchors, anchorRow - 1, cc);
if (up?.hideBorderBottom === true) {
top = false;
break;
}
}
}
// 与下方格共享横线
if (bottom && rEnd < rowCount - 1) {
const belowRow = anchorRow + rs;
for (let cc = anchorCol; cc <= anchorCol + cs - 1; cc += 1) {
const dn = getFreeTableOwnerAt(anchors, belowRow, cc);
if (dn?.hideBorderTop === true) {
bottom = false;
break;
}
}
}
// 与左侧格共享竖线
if (left && anchorCol > 0) {
for (let rr = anchorRow; rr <= anchorRow + rs - 1; rr += 1) {
const lf = getFreeTableOwnerAt(anchors, rr, anchorCol - 1);
if (lf?.hideBorderRight === true) {
left = false;
break;
}
}
}
// 与右侧格共享竖线
if (right && cEnd < colCount - 1) {
const rightCol = anchorCol + cs;
for (let rr = anchorRow; rr <= anchorRow + rs - 1; rr += 1) {
const rt = getFreeTableOwnerAt(anchors, rr, rightCol);
if (rt?.hideBorderLeft === true) {
right = false;
break;
}
}
}
return { top, right, bottom, left };
}
/** 各边线型(外框线 / 横线 / 竖线),缺省均为 solid */
export interface FreeTableSideLineStyleKeys {
top: string;
right: string;
bottom: string;
left: string;
}
export function borderSidesToCssFragment(
sides: FreeTableBorderSides,
bw: number,
color: string,
lineStyles?: FreeTableSideLineStyleKeys | null,
): string {
const css = (side: keyof FreeTableBorderSides) =>
lineStyles ? lineStyleKeyToCssBorderStyle(lineStyles[side]) : 'solid';
const t = sides.top ? `${bw}px ${css('top')} ${color}` : 'none';
const r = sides.right ? `${bw}px ${css('right')} ${color}` : 'none';
const b = sides.bottom ? `${bw}px ${css('bottom')} ${color}` : 'none';
const l = sides.left ? `${bw}px ${css('left')} ${color}` : 'none';
return `border-top:${t};border-right:${r};border-bottom:${b};border-left:${l};`;
}

View File

@@ -0,0 +1,312 @@
/**
* 自由表格:锚点单元格 + rowspan/colspan与 HTML table 一致
*/
export interface FreeTableAnchorCell {
row: number;
col: number;
rowspan: number;
colspan: number;
text?: string;
bindField?: string;
contentType?: 'text' | 'image' | 'qrcode' | 'barcode' | 'number' | 'amount';
fillCell?: boolean;
contentScale?: number;
imageFit?: 'fill' | 'contain' | 'cover';
qrLevel?: 'L' | 'M' | 'Q' | 'H';
qrRenderType?: 'image/png' | 'image/jpeg' | 'image/webp';
barcodeFormat?: string;
decimalPlaces?: number;
roundHalfUp?: boolean;
amountType?: 'CNY' | 'USD' | 'EUR';
autoWrap?: boolean;
autoFitFont?: boolean;
align?: string;
verticalAlign?: string;
fontSize?: number;
color?: string;
backgroundColor?: string;
hideBorderTop?: boolean;
hideBorderRight?: boolean;
hideBorderBottom?: boolean;
hideBorderLeft?: boolean;
}
const FREE_CELL_CONTENT_TYPES = new Set(['text', 'image', 'qrcode', 'barcode', 'number', 'amount']);
function parseCell(c: any): FreeTableAnchorCell {
const row = {
row: Math.max(0, Number(c?.row || 0)),
col: Math.max(0, Number(c?.col || 0)),
rowspan: Math.max(1, Number(c?.rowspan || 1)),
colspan: Math.max(1, Number(c?.colspan || 1)),
text: String(c?.text ?? ''),
bindField: String(c?.bindField ?? ''),
align: String(c?.align || 'left'),
verticalAlign: String(c?.verticalAlign || 'middle'),
fontSize: Math.max(8, Number(c?.fontSize || 12)),
color: String(c?.color || '#111111'),
backgroundColor: String(c?.backgroundColor || '#ffffff'),
} as FreeTableAnchorCell;
const ct = String(c?.contentType || '').trim();
if (ct && FREE_CELL_CONTENT_TYPES.has(ct)) {
row.contentType = ct as FreeTableAnchorCell['contentType'];
}
if (typeof c?.fillCell === 'boolean') {
row.fillCell = c.fillCell;
}
if (c?.contentScale != null && Number.isFinite(Number(c.contentScale))) {
row.contentScale = Number(c.contentScale);
}
if (c?.imageFit === 'fill' || c?.imageFit === 'contain' || c?.imageFit === 'cover') {
row.imageFit = c.imageFit;
}
if (c?.qrLevel === 'L' || c?.qrLevel === 'M' || c?.qrLevel === 'Q' || c?.qrLevel === 'H') {
row.qrLevel = c.qrLevel;
}
if (c?.qrRenderType === 'image/png' || c?.qrRenderType === 'image/jpeg' || c?.qrRenderType === 'image/webp') {
row.qrRenderType = c.qrRenderType;
}
if (c?.barcodeFormat != null && String(c.barcodeFormat).trim()) {
row.barcodeFormat = String(c.barcodeFormat).trim();
}
if (c?.decimalPlaces != null && Number.isFinite(Number(c.decimalPlaces))) {
row.decimalPlaces = Number(c.decimalPlaces);
}
if (typeof c?.roundHalfUp === 'boolean') {
row.roundHalfUp = c.roundHalfUp;
}
if (c?.amountType === 'CNY' || c?.amountType === 'USD' || c?.amountType === 'EUR') {
row.amountType = c.amountType;
}
if (typeof c?.autoWrap === 'boolean') {
row.autoWrap = c.autoWrap;
}
if (typeof c?.autoFitFont === 'boolean') {
row.autoFitFont = c.autoFitFont;
}
if (c?.hideBorderTop === true) row.hideBorderTop = true;
if (c?.hideBorderRight === true) row.hideBorderRight = true;
if (c?.hideBorderBottom === true) row.hideBorderBottom = true;
if (c?.hideBorderLeft === true) row.hideBorderLeft = true;
return row;
}
export function defaultFreeTableCell(row: number, col: number): FreeTableAnchorCell {
return {
row,
col,
rowspan: 1,
colspan: 1,
text: '',
bindField: '',
align: 'left',
verticalAlign: 'middle',
fontSize: 12,
color: '#111111',
backgroundColor: '#ffffff',
};
}
/** 将 cells 规范为互不重叠的锚点列表,并填满网格中未覆盖的 1x1 默认格 */
export function normalizeFreeTableAnchors(rowCount: number, colCount: number, cellsInput: any[]): FreeTableAnchorCell[] {
const occ: boolean[][] = Array.from({ length: rowCount }, () => Array.from({ length: colCount }, () => false));
const anchors: FreeTableAnchorCell[] = [];
const parsed = (cellsInput || []).map(parseCell).sort((a, b) => a.row - b.row || a.col - b.col);
for (const c of parsed) {
const rs = Math.min(c.rowspan, rowCount - c.row);
const cs = Math.min(c.colspan, colCount - c.col);
if (rs < 1 || cs < 1 || c.row >= rowCount || c.col >= colCount) continue;
let overlap = false;
for (let dr = 0; dr < rs && !overlap; dr += 1) {
for (let dc = 0; dc < cs && !overlap; dc += 1) {
const r = c.row + dr;
const cc = c.col + dc;
if (r >= rowCount || cc >= colCount || occ[r][cc]) {
overlap = true;
}
}
}
if (overlap) continue;
for (let dr = 0; dr < rs; dr += 1) {
for (let dc = 0; dc < cs; dc += 1) {
occ[c.row + dr][c.col + dc] = true;
}
}
anchors.push({ ...c, rowspan: rs, colspan: cs });
}
for (let r = 0; r < rowCount; r += 1) {
for (let c = 0; c < colCount; c += 1) {
if (!occ[r][c]) {
occ[r][c] = true;
anchors.push(defaultFreeTableCell(r, c));
}
}
}
anchors.sort((a, b) => a.row - b.row || a.col - b.col);
return anchors;
}
export function getFreeTableOwnerAt(anchors: FreeTableAnchorCell[], r: number, c: number): FreeTableAnchorCell {
for (const cell of anchors) {
const rs = Math.max(1, Number(cell.rowspan || 1));
const cs = Math.max(1, Number(cell.colspan || 1));
if (r >= cell.row && r < cell.row + rs && c >= cell.col && c < cell.col + cs) {
return cell;
}
}
return defaultFreeTableCell(r, c);
}
/** 矩形区域内是否均为 1x1 独立锚点(可合并) */
export function canMergeFreeTableRegion(
anchors: FreeTableAnchorCell[],
rowCount: number,
colCount: number,
r0: number,
c0: number,
r1: number,
c1: number,
): boolean {
const rr0 = Math.min(r0, r1);
const rr1 = Math.max(r0, r1);
const cc0 = Math.min(c0, c1);
const cc1 = Math.max(c0, c1);
if (rr0 < 0 || cc0 < 0 || rr1 >= rowCount || cc1 >= colCount) return false;
for (let r = rr0; r <= rr1; r += 1) {
for (let c = cc0; c <= cc1; c += 1) {
const o = getFreeTableOwnerAt(anchors, r, c);
const rs = Math.max(1, Number(o.rowspan || 1));
const cs = Math.max(1, Number(o.colspan || 1));
if (o.row !== r || o.col !== c) return false;
if (rs !== 1 || cs !== 1) return false;
}
}
return (rr1 - rr0 + 1) * (cc1 - cc0 + 1) > 1;
}
export function mergeFreeTableRegion(
anchors: FreeTableAnchorCell[],
rowCount: number,
colCount: number,
r0: number,
c0: number,
r1: number,
c1: number,
): FreeTableAnchorCell[] {
const rr0 = Math.min(r0, r1);
const rr1 = Math.max(r0, r1);
const cc0 = Math.min(c0, c1);
const cc1 = Math.max(c0, c1);
if (!canMergeFreeTableRegion(anchors, rowCount, colCount, rr0, cc0, rr1, cc1)) {
return anchors;
}
const survivor = { ...getFreeTableOwnerAt(anchors, rr0, cc0) };
const next = anchors.filter((cell) => {
const r = cell.row;
const c = cell.col;
return r < rr0 || r > rr1 || c < cc0 || c > cc1;
});
survivor.row = rr0;
survivor.col = cc0;
survivor.rowspan = rr1 - rr0 + 1;
survivor.colspan = cc1 - cc0 + 1;
next.push(survivor);
next.sort((a, b) => a.row - b.row || a.col - b.col);
return next;
}
export function splitFreeTableAt(anchors: FreeTableAnchorCell[], rowCount: number, colCount: number, r: number, c: number): FreeTableAnchorCell[] {
const owner = getFreeTableOwnerAt(anchors, r, c);
const rs = Math.max(1, Number(owner.rowspan || 1));
const cs = Math.max(1, Number(owner.colspan || 1));
if (rs === 1 && cs === 1) {
return anchors;
}
const next = anchors.filter((cell) => !(cell.row === owner.row && cell.col === owner.col));
for (let dr = 0; dr < rs; dr += 1) {
for (let dc = 0; dc < cs; dc += 1) {
const nr = owner.row + dr;
const nc = owner.col + dc;
if (nr >= rowCount || nc >= colCount) continue;
if (dr === 0 && dc === 0) {
next.push({
...defaultFreeTableCell(nr, nc),
...(pickSwapPayload(owner) as any),
} as FreeTableAnchorCell);
} else {
next.push(defaultFreeTableCell(nr, nc));
}
}
}
next.sort((a, b) => a.row - b.row || a.col - b.col);
return next;
}
function pickSwapPayload(cell: FreeTableAnchorCell) {
const raw: Record<string, unknown> = {
text: String(cell?.text ?? ''),
bindField: String(cell?.bindField ?? ''),
contentType: cell.contentType || 'text',
fillCell: cell.fillCell,
contentScale: cell.contentScale,
imageFit: cell.imageFit,
qrLevel: cell.qrLevel,
qrRenderType: cell.qrRenderType,
barcodeFormat: cell.barcodeFormat,
decimalPlaces: cell.decimalPlaces,
roundHalfUp: cell.roundHalfUp,
amountType: cell.amountType,
autoWrap: cell.autoWrap,
autoFitFont: cell.autoFitFont,
align: String(cell?.align || 'left'),
verticalAlign: String(cell?.verticalAlign || 'middle'),
fontSize: Math.max(8, Number(cell?.fontSize || 12)),
color: String(cell?.color || '#111111'),
backgroundColor: String(cell?.backgroundColor || '#ffffff'),
hideBorderTop: cell.hideBorderTop === true ? true : undefined,
hideBorderRight: cell.hideBorderRight === true ? true : undefined,
hideBorderBottom: cell.hideBorderBottom === true ? true : undefined,
hideBorderLeft: cell.hideBorderLeft === true ? true : undefined,
};
return Object.fromEntries(Object.entries(raw).filter(([, v]) => v !== undefined)) as Record<string, unknown>;
}
/** 交换两个锚点格的可交换字段(不改变 rowspan/colspan */
export function swapFreeTableOwnerPayloads(
anchors: FreeTableAnchorCell[],
rowCount: number,
colCount: number,
fromRow: number,
fromCol: number,
toRow: number,
toCol: number,
): FreeTableAnchorCell[] {
const a = getFreeTableOwnerAt(anchors, fromRow, fromCol);
const b = getFreeTableOwnerAt(anchors, toRow, toCol);
if (a.row === b.row && a.col === b.col) return anchors;
const pa = pickSwapPayload(a);
const pb = pickSwapPayload(b);
return anchors.map((cell) => {
if (cell.row === a.row && cell.col === a.col) {
return { ...cell, ...pb };
}
if (cell.row === b.row && cell.col === b.col) {
return { ...cell, ...pa };
}
return { ...cell };
});
}
/** 删除/改维度前:去掉越界的锚点 */
export function clipAnchorsToGrid(rowCount: number, colCount: number, cellsInput: any[]): FreeTableAnchorCell[] {
const list = (cellsInput || []).map(parseCell);
return list.filter((c) => {
const rs = Math.max(1, Number(c.rowspan || 1));
const cs = Math.max(1, Number(c.colspan || 1));
return c.row >= 0 && c.col >= 0 && c.row + rs <= rowCount && c.col + cs <= colCount;
});
}

View File

@@ -0,0 +1,57 @@
/**
* 自由表格:外框线 / 行间横线 / 列间竖线 的线型(与属性面板、画布、打印 HTML 共用)
*/
export type FreeTableLineStyleKey = 'solid' | 'dashed' | 'dotted' | 'dash_dot' | 'double_dash_dot';
export const FREE_TABLE_LINE_STYLE_OPTIONS: { value: FreeTableLineStyleKey; label: string }[] = [
{ value: 'solid', label: '实线' },
{ value: 'dashed', label: '段线' },
{ value: 'dotted', label: '虚线' },
{ value: 'dash_dot', label: '点划线' },
{ value: 'double_dash_dot', label: '双点划线' },
];
const LINE_STYLE_SET = new Set(FREE_TABLE_LINE_STYLE_OPTIONS.map((o) => o.value));
export function normalizeFreeTableLineStyleKey(raw: unknown): FreeTableLineStyleKey {
const s = String(raw || 'solid');
return LINE_STYLE_SET.has(s as FreeTableLineStyleKey) ? (s as FreeTableLineStyleKey) : 'solid';
}
/**
* 将线型映射为 CSS border-style点划/双点划在标准边框中以最接近的样式近似)
*/
export function lineStyleKeyToCssBorderStyle(key: FreeTableLineStyleKey | string | undefined): string {
const k = normalizeFreeTableLineStyleKey(key);
if (k === 'dashed') return 'dashed';
if (k === 'dotted') return 'dotted';
if (k === 'dash_dot') return 'dashed';
if (k === 'double_dash_dot') return 'double';
return 'solid';
}
/**
* 根据单元格位置判断每条可见边属于外框还是内线,返回各边应使用的线型键(与 resolveFreeTableCellBorderSides 几何一致)
*/
export function resolveFreeTableCellLineStyleKeys(
el: any,
anchorRow: number,
anchorCol: number,
rs: number,
cs: number,
rowCount: number,
colCount: number,
): { top: FreeTableLineStyleKey; right: FreeTableLineStyleKey; bottom: FreeTableLineStyleKey; left: FreeTableLineStyleKey } {
const rEnd = anchorRow + rs - 1;
const cEnd = anchorCol + cs - 1;
const outerStyle = normalizeFreeTableLineStyleKey(el?.outerBorderLineStyle);
const hStyle = normalizeFreeTableLineStyleKey(el?.innerBorderHorizontalLineStyle);
const vStyle = normalizeFreeTableLineStyleKey(el?.innerBorderVerticalLineStyle);
return {
top: anchorRow === 0 ? outerStyle : hStyle,
right: cEnd === colCount - 1 ? outerStyle : vStyle,
bottom: rEnd === rowCount - 1 ? outerStyle : hStyle,
left: anchorCol === 0 ? outerStyle : vStyle,
};
}

View File

@@ -0,0 +1,223 @@
/**
* 自由表格:列宽、行高(单位 mm与元素 w/h 总和一致
*/
export const MIN_FREE_TABLE_TRACK_MM = 4;
export function round2(n: number): number {
return Math.round(n * 100) / 100;
}
/** 均分总尺寸(内部用于初始化) */
export function evenSplitTracks(totalMm: number, count: number): number[] {
const n = Math.max(1, count);
const t = Math.max(0.01, Number(totalMm) || 0.01);
const base = round2(t / n);
const arr = Array.from({ length: n }, () => base);
const sum = arr.reduce((a, b) => a + b, 0);
arr[n - 1] = round2(arr[n - 1] + (t - sum));
return arr;
}
/** 将各轨道缩放到总和 = totalMm且每项不小于 minMm */
export function clampTrackSumToTotal(tracks: number[], totalMm: number, minMm = MIN_FREE_TABLE_TRACK_MM): number[] {
const n = tracks.length;
if (n === 0) return [];
const t = Math.max(0.01, Number(totalMm) || 0.01);
let next = tracks.map((x) => round2(Math.max(minMm, Number(x) || minMm)));
let sum = next.reduce((a, b) => a + b, 0);
if (Math.abs(sum - t) < 0.02) {
return next;
}
const scale = t / sum;
next = next.map((x) => round2(x * scale));
sum = next.reduce((a, b) => a + b, 0);
next[n - 1] = round2(next[n - 1] + (t - sum));
return next;
}
export function resolveFreeTableColWidthsMm(el: { colCount?: number; w: number; colWidths?: number[] | null }): number[] {
const colCount = Math.max(1, Number(el?.colCount || 1));
const w = Math.max(0.01, Number(el?.w) || 0.01);
const raw = Array.isArray(el?.colWidths) ? el.colWidths : [];
if (raw.length !== colCount) {
return evenSplitTracks(w, colCount);
}
return clampTrackSumToTotal(raw.map((x) => Number(x) || MIN_FREE_TABLE_TRACK_MM), w);
}
export function resolveFreeTableRowHeightsMm(el: { rowCount?: number; h: number; rowHeights?: number[] | null }): number[] {
const rowCount = Math.max(1, Number(el?.rowCount || 1));
const h = Math.max(0.01, Number(el?.h) || 0.01);
const raw = Array.isArray(el?.rowHeights) ? el.rowHeights : [];
if (raw.length !== rowCount) {
return evenSplitTracks(h, rowCount);
}
return clampTrackSumToTotal(raw.map((x) => Number(x) || MIN_FREE_TABLE_TRACK_MM), h);
}
/** 拖拽列边界:在 edge 与 edge+1 之间移动edge ∈ [0, n-2] */
export function redistributeColEdge(
base: number[],
edge: number,
deltaMm: number,
totalW: number,
minMm = MIN_FREE_TABLE_TRACK_MM,
): number[] | null {
if (base.length < 2 || edge < 0 || edge >= base.length - 1) {
return null;
}
const next = [...base];
next[edge] = round2(next[edge] + deltaMm);
next[edge + 1] = round2(next[edge + 1] - deltaMm);
if (next[edge] < minMm || next[edge + 1] < minMm) {
return null;
}
return clampTrackSumToTotal(next, totalW, minMm);
}
/** 拖拽行边界:在 edge 与 edge+1 之间移动 */
export function redistributeRowEdge(
base: number[],
edge: number,
deltaMm: number,
totalH: number,
minMm = MIN_FREE_TABLE_TRACK_MM,
): number[] | null {
if (base.length < 2 || edge < 0 || edge >= base.length - 1) {
return null;
}
const next = [...base];
next[edge] = round2(next[edge] + deltaMm);
next[edge + 1] = round2(next[edge + 1] - deltaMm);
if (next[edge] < minMm || next[edge + 1] < minMm) {
return null;
}
return clampTrackSumToTotal(next, totalH, minMm);
}
/** 新增列后列宽(总宽不变,均分空间给新列) */
export function colWidthsAfterAddCol(widths: number[], totalW: number, minMm = MIN_FREE_TABLE_TRACK_MM): number[] {
const n = widths.length;
if (n < 1) {
return evenSplitTracks(totalW, 1);
}
const factor = n / (n + 1);
const scaled = widths.map((c) => round2(c * factor));
const sum = scaled.reduce((a, b) => a + b, 0);
scaled.push(round2(Math.max(minMm, totalW - sum)));
return clampTrackSumToTotal(scaled, totalW, minMm);
}
/** 删除列后列宽(被删列宽度并入相邻列) */
export function colWidthsAfterRemoveCol(widths: number[], removeIdx: number, totalW: number, minMm = MIN_FREE_TABLE_TRACK_MM): number[] {
if (widths.length <= 1) {
return [totalW];
}
const removed = widths[removeIdx] ?? 0;
const next = widths.filter((_, i) => i !== removeIdx);
const adj = Math.min(Math.max(0, removeIdx), next.length - 1);
next[adj] = round2((next[adj] ?? 0) + removed);
return clampTrackSumToTotal(next, totalW, minMm);
}
export function rowHeightsAfterAddRow(heights: number[], totalH: number, minMm = MIN_FREE_TABLE_TRACK_MM): number[] {
const n = heights.length;
if (n < 1) {
return evenSplitTracks(totalH, 1);
}
const factor = n / (n + 1);
const scaled = heights.map((h) => round2(h * factor));
const sum = scaled.reduce((a, b) => a + b, 0);
scaled.push(round2(Math.max(minMm, totalH - sum)));
return clampTrackSumToTotal(scaled, totalH, minMm);
}
export function rowHeightsAfterRemoveRow(heights: number[], removeIdx: number, totalH: number, minMm = MIN_FREE_TABLE_TRACK_MM): number[] {
if (heights.length <= 1) {
return [totalH];
}
const removed = heights[removeIdx] ?? 0;
const next = heights.filter((_, i) => i !== removeIdx);
const adj = Math.min(Math.max(0, removeIdx), next.length - 1);
next[adj] = round2((next[adj] ?? 0) + removed);
return clampTrackSumToTotal(next, totalH, minMm);
}
/** 整表缩放外框时,按比例缩放行列尺寸使总和仍贴合新 w/h */
export function scaleFreeTableTracks(
colWidths: number[],
rowHeights: number[],
prevW: number,
prevH: number,
newW: number,
newH: number,
minMm = MIN_FREE_TABLE_TRACK_MM,
): { colWidths: number[]; rowHeights: number[] } {
const pw = Math.max(0.01, Number(prevW) || 0.01);
const ph = Math.max(0.01, Number(prevH) || 0.01);
const nw = Math.max(0.01, Number(newW) || 0.01);
const nh = Math.max(0.01, Number(newH) || 0.01);
const sx = nw / pw;
const sy = nh / ph;
const nextCw = colWidths.length ? colWidths.map((c) => round2(c * sx)) : [];
const nextRh = rowHeights.length ? rowHeights.map((r) => round2(r * sy)) : [];
return {
colWidths: nextCw.length ? clampTrackSumToTotal(nextCw, nw, minMm) : nextCw,
rowHeights: nextRh.length ? clampTrackSumToTotal(nextRh, nh, minMm) : nextRh,
};
}
/** 修改某一列宽为指定值,差额由「另一列」消化(用于属性面板) */
export function setColWidthAt(colWidths: number[], index: number, valueMm: number, totalW: number, minMm = MIN_FREE_TABLE_TRACK_MM): number[] | null {
const n = colWidths.length;
if (n < 1 || index < 0 || index >= n) {
return null;
}
const next = [...colWidths];
const v = round2(Math.max(minMm, Number(valueMm) || minMm));
const diff = v - next[index];
const partner = index === n - 1 ? n - 2 : n - 1;
if (partner < 0) {
next[0] = totalW;
return clampTrackSumToTotal(next, totalW, minMm);
}
next[index] = v;
next[partner] = round2(next[partner] - diff);
if (next[partner] < minMm) {
return null;
}
return clampTrackSumToTotal(next, totalW, minMm);
}
/** 修改某一行高 */
export function setRowHeightAt(rowHeights: number[], index: number, valueMm: number, totalH: number, minMm = MIN_FREE_TABLE_TRACK_MM): number[] | null {
const n = rowHeights.length;
if (n < 1 || index < 0 || index >= n) {
return null;
}
const next = [...rowHeights];
const v = round2(Math.max(minMm, Number(valueMm) || minMm));
const diff = v - next[index];
const partner = index === n - 1 ? n - 2 : n - 1;
if (partner < 0) {
next[0] = totalH;
return clampTrackSumToTotal(next, totalH, minMm);
}
next[index] = v;
next[partner] = round2(next[partner] - diff);
if (next[partner] < minMm) {
return null;
}
return clampTrackSumToTotal(next, totalH, minMm);
}
/** 均分列宽(写入 colWidths */
export function buildEvenColWidths(colCount: number, totalW: number, minMm = MIN_FREE_TABLE_TRACK_MM): number[] {
return clampTrackSumToTotal(evenSplitTracks(totalW, Math.max(1, colCount)), totalW, minMm);
}
/** 均分行高(写入 rowHeights */
export function buildEvenRowHeights(rowCount: number, totalH: number, minMm = MIN_FREE_TABLE_TRACK_MM): number[] {
return clampTrackSumToTotal(evenSplitTracks(totalH, Math.max(1, rowCount)), totalH, minMm);
}

View File

@@ -0,0 +1,239 @@
import type { NativeElement } from './types';
/**
* 根据画布元素与「画布实际 JSON」文本生成模拟数据对象。
* 逻辑与 NativePrintDesigner.generateMockData 中根据画布生成部分一致。
*/
export function generateNativeMockDataObject(elements: NativeElement[], canvasJsonText: string): Record<string, any> {
const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
const randomPick = (list: any[]) => list[randomInt(0, Math.max(0, list.length - 1))];
const shortWords = ['标准件', '不锈钢', '铝板', '铜件', '塑胶件', '辅料', '组件'];
const longWords = [
'用于产线组装的关键部件,需按工艺要求进行批次追溯与检验记录。',
'该物料用于连续生产流程,建议结合库存周转与批次有效期进行动态补料。',
'本条数据为模拟长文本,主要用于验证列宽变化后自动换行与字号自适应效果。',
];
const buildShortText = (field: string, rowIndex: number) => `${field}_${randomPick(shortWords)}_${rowIndex + 1}`;
const buildLongText = (field: string, rowIndex: number) => `${field}_${rowIndex + 1}_${randomPick(longWords)}`;
const buildRandomText = (field: string, rowIndex: number, shouldWrap: boolean) => {
if (shouldWrap && Math.random() < 0.45) {
return buildLongText(field, rowIndex);
}
return buildShortText(field, rowIndex);
};
const toAlphaNumKey = (input: string) => {
const normalized = String(input || '')
.replace(/[^a-zA-Z0-9]/g, '_')
.replace(/_+/g, '_')
.replace(/^_+|_+$/g, '');
return normalized || 'CODE';
};
const buildQrValue = (field: string) => `QR_${toAlphaNumKey(field)}_${randomInt(100000, 999999)}`;
const buildBarcodeValue = (field: string) => `BAR${randomInt(100000000000, 999999999999)}${toAlphaNumKey(field).slice(0, 6).toUpperCase()}`;
const buildMergeValue = (field: string, groupIndex: number, shouldWrap: boolean) =>
shouldWrap ? `${field}_合并组${groupIndex + 1}_${randomPick(longWords)}` : `${field}_合并组${groupIndex + 1}_${randomPick(shortWords)}`;
const resolveTemplateColumns = (element: any) => {
let parsed: any = {};
try {
parsed = JSON.parse(canvasJsonText || '{}');
} catch (_error) {
parsed = {};
}
if (!Array.isArray(parsed?.elements)) {
return Array.isArray(element?.columns) ? element.columns : [];
}
const matched = parsed.elements.find((item: any) => {
const component = String(item?.component || '');
return item?.id === element?.id && (component === 'table' || component === 'detailTable');
});
if (Array.isArray(matched?.payload?.columns)) {
return matched.payload.columns;
}
return Array.isArray(element?.columns) ? element.columns : [];
};
const resolveTemplateFreeTableCells = (element: any) => {
const fromElement = Array.isArray(element?.cells) ? element.cells : [];
let parsed: any = {};
try {
parsed = JSON.parse(canvasJsonText || '{}');
} catch (_error) {
parsed = {};
}
if (!Array.isArray(parsed?.elements)) {
return fromElement;
}
const matched = parsed.elements.find(
(item: any) => item?.id === element?.id && String(item?.component || '') === 'freeTable',
);
const fromJson = matched?.payload?.cells;
if (!Array.isArray(fromJson) || !fromJson.length) {
return fromElement;
}
const map = new Map<string, any>();
fromElement.forEach((c: any) => {
const k = `${Number(c?.row ?? 0)}_${Number(c?.col ?? 0)}`;
map.set(k, { ...c });
});
fromJson.forEach((jc: any) => {
const k = `${Number(jc?.row ?? 0)}_${Number(jc?.col ?? 0)}`;
const prev = map.get(k) || {};
map.set(k, { ...prev, ...jc });
});
return Array.from(map.values());
};
const mock: Record<string, any> = {};
const placeholderReg = /{{\s*([\w.]+)\s*}}/g;
const setByPath = (target: Record<string, any>, path: string, value: any) => {
const segments = String(path || '')
.split('.')
.filter(Boolean);
if (!segments.length) {
return;
}
let cursor: Record<string, any> = target;
for (let i = 0; i < segments.length - 1; i += 1) {
const key = segments[i];
if (!cursor[key] || typeof cursor[key] !== 'object') {
cursor[key] = {};
}
cursor = cursor[key];
}
cursor[segments[segments.length - 1]] = value;
};
const getByPath = (target: Record<string, any> | null, path: string) => {
if (!target) return undefined;
return String(path || '')
.split('.')
.filter(Boolean)
.reduce((acc: any, key: string) => acc?.[key], target);
};
let qrcodeIndex = 1;
let barcodeIndex = 1;
elements.forEach((element) => {
if (element.type === 'table' || element.type === 'detailTable') {
const source = (element as any).source || 'mainTable';
const columns = resolveTemplateColumns(element);
const mergeKeys = Array.isArray((element as any).mergeColumnKeys) ? ((element as any).mergeColumnKeys as string[]) : [];
const strictGrouping = (element as any).strictGrouping !== false;
const mergeFieldOrder = mergeKeys
.map((key) => columns.find((col: any) => String(col?.key || '') === String(key || '')))
.filter(Boolean)
.map((col: any) => String(col?.bindField || col?.field || ''))
.filter(Boolean);
const rowCount = randomInt(6, 10);
const rowsDataCache: Record<string, any>[] = [];
const rows = Array.from({ length: rowCount }).map((_, rowIndex) => {
const row: Record<string, any> = {};
const prevRow = rowIndex > 0 ? (rowsDataCache[rowIndex - 1] || {}) : null;
columns.forEach((col: any, colIndex: number) => {
const field = String(col?.bindField || col?.field || `field${colIndex + 1}`);
const shouldWrap = col?.autoWrap !== false;
const contentType = String(col?.contentType || 'text');
const randomText = buildRandomText(field, rowIndex, shouldWrap);
const mergeIndex = mergeFieldOrder.findIndex((item) => item === field);
const enableMerge = mergeIndex >= 0;
const canFollowPrev =
!strictGrouping ||
mergeIndex <= 0 ||
mergeFieldOrder.slice(0, mergeIndex).every((parentField) => getByPath(prevRow, parentField) === getByPath(row, parentField));
if (enableMerge && rowIndex > 0 && prevRow && canFollowPrev && Math.random() < 0.5) {
const previousValue = getByPath(prevRow, field);
setByPath(row, field, previousValue ?? buildMergeValue(field, randomInt(0, 3), shouldWrap));
} else if (enableMerge) {
setByPath(row, field, buildMergeValue(field, randomInt(0, 3), shouldWrap));
} else {
if (contentType === 'image') {
setByPath(row, field, `https://picsum.photos/seed/${encodeURIComponent(`${field}_${rowIndex + 1}`)}/260/120`);
} else if (contentType === 'qrcode') {
setByPath(row, field, buildQrValue(field));
} else if (contentType === 'barcode') {
setByPath(row, field, buildBarcodeValue(field));
} else if (contentType === 'number' || contentType === 'amount') {
const decimals = Math.max(0, Math.min(6, Number(col?.decimalPlaces ?? 2)));
const base = randomInt(100, 50000) + Math.random();
const num = col?.roundHalfUp === false ? Math.trunc(base * 10 ** decimals) / 10 ** decimals : Number(base.toFixed(decimals));
setByPath(row, field, num);
} else {
setByPath(row, field, randomText);
}
}
});
rowsDataCache.push(row);
return row;
});
mock[source] = rows;
return;
}
if (element.type === 'freeTable') {
const cells = resolveTemplateFreeTableCells(element);
cells.forEach((cell: any, idx: number) => {
const bindField = String(cell?.bindField || '').trim();
if (!bindField) return;
if (bindField in mock) return;
const cellText = String(cell?.text || '').trim();
const contentType = String(cell?.contentType || 'text');
const shouldWrap = cell?.autoWrap !== false;
if (contentType === 'image') {
mock[bindField] = `https://picsum.photos/seed/${encodeURIComponent(`${bindField}_ft`)}/260/120`;
} else if (contentType === 'qrcode') {
mock[bindField] = buildQrValue(bindField || `ft${idx}`);
} else if (contentType === 'barcode') {
mock[bindField] = buildBarcodeValue(bindField || `ft${idx}`);
} else if (contentType === 'number' || contentType === 'amount') {
const decimals = Math.max(0, Math.min(6, Number(cell?.decimalPlaces ?? 2)));
const base = randomInt(100, 50000) + Math.random();
const num =
cell?.roundHalfUp === false ? Math.trunc(base * 10 ** decimals) / 10 ** decimals : Number(base.toFixed(decimals));
mock[bindField] = num;
} else {
mock[bindField] = cellText || buildRandomText(bindField, 0, shouldWrap);
}
});
return;
}
if (element.type === 'qrcode') {
const bindField = String((element as any).bindField || '').trim();
const key = bindField || `qrcodeValue${qrcodeIndex}`;
mock[key] = buildQrValue(bindField || `qrcode${qrcodeIndex}`);
qrcodeIndex += 1;
return;
}
if (element.type === 'barcode') {
const bindField = String((element as any).bindField || '').trim();
const key = bindField || `barcodeValue${barcodeIndex}`;
mock[key] = buildBarcodeValue(bindField || `barcode${barcodeIndex}`);
barcodeIndex += 1;
return;
}
const bindField = String((element as any).bindField || '').trim();
if (bindField && !(bindField in mock)) {
if (element.type === 'image') {
mock[bindField] = 'https://via.placeholder.com/180x80.png?text=Image';
} else if (element.type === 'date') {
mock[bindField] = '2026-01-01';
} else {
mock[bindField] = `${bindField}_示例值`;
}
}
const text = String((element as any).text || '');
const matches = Array.from(text.matchAll(placeholderReg));
matches.forEach((item) => {
const field = String(item?.[1] || '').split('.')[0];
if (!field) return;
if (!(field in mock)) {
mock[field] = `${field}_示例值`;
}
});
});
if (!Object.keys(mock).length) {
mock.docNo = 'DOC-001';
mock.orderNo = 'ORDER-001';
mock.mainTable = [{ field1: '值1', field2: '值2', field3: '值3' }];
}
return mock;
}

View File

@@ -0,0 +1,54 @@
import { createDefaultSchema } from './useDesignerStore';
import type { NativeElement, NativeTemplateSchema } from './types';
/** 将接口或导入的 JSON 规范为可用的 NativeTemplateSchema与 NativePrintDesigner 内原逻辑一致) */
export function normalizeImportedNativeSchema(raw: unknown): NativeTemplateSchema {
const base = createDefaultSchema();
const obj = raw as Record<string, any>;
if (!obj || obj.engine !== 'native') {
throw new Error('返回内容不是原生模板engine 需为 native');
}
const page = { ...base.page, ...(obj.page || {}) } as NativeTemplateSchema['page'];
page.unit = 'mm';
if (!Array.isArray(page.margin) || page.margin.length < 4) {
page.margin = [...base.page.margin];
}
const elements = Array.isArray(obj.elements) ? obj.elements : [];
let z = 1;
for (const el of elements as any[]) {
if (!el.id) {
el.id = `${String(el.type || 'el')}_${Math.random().toString(36).slice(2, 10)}`;
}
if (el.zIndex == null || el.zIndex === undefined) {
el.zIndex = z;
}
z += 1;
}
const dataBinding: NonNullable<NativeTemplateSchema['dataBinding']> = {
fieldMap: { ...(base.dataBinding?.fieldMap || {}), ...(obj.dataBinding?.fieldMap || {}) },
tableSources:
Array.isArray(obj.dataBinding?.tableSources) && obj.dataBinding.tableSources.length
? [...obj.dataBinding.tableSources]
: [...(base.dataBinding?.tableSources || ['mainTable', 'detailList'])],
params: Array.isArray(obj.dataBinding?.params) ? [...obj.dataBinding.params] : [...(base.dataBinding?.params || [])],
detailTables: Array.isArray(obj.dataBinding?.detailTables)
? obj.dataBinding.detailTables.map((t: any) => ({
tableKey: String(t.tableKey || ''),
label: t.label ? String(t.label) : undefined,
fields: Array.isArray(t.fields)
? t.fields.map((f: any) => ({
key: String(f.key || ''),
label: f.label ? String(f.label) : undefined,
}))
: [],
}))
: [...(base.dataBinding?.detailTables || [])],
};
return {
engine: 'native',
version: String(obj.version || '1.0.0'),
page,
elements: elements as NativeElement[],
dataBinding,
};
}

View File

@@ -0,0 +1,126 @@
import type { NativeElement, NativeTemplateSchema } from './types';
function defaultDataBinding(): NonNullable<NativeTemplateSchema['dataBinding']> {
return {
fieldMap: {},
tableSources: ['mainTable', 'detailList'],
params: [],
detailTables: [],
};
}
/** 与 NativePrintDesigner.mapElementToTemplateStyle 一致:画布元素 →「画布实际 JSON」中的元素节点 */
export function mapElementToTemplateStyle(element: NativeElement) {
return {
id: element.id,
component: element.type,
bindField: (element as any).bindField || '',
region: (element as any).region || '',
bandId: (element as any).bandId || '',
rect: { x: element.x, y: element.y, w: element.w, h: element.h, zIndex: element.zIndex },
style: { ...(element.style || {}) },
payload:
element.type === 'image'
? { src: (element as any).src, fit: (element as any).fit }
: element.type === 'table' || element.type === 'detailTable'
? {
source: (element as any).source,
mergeColumnKeys: (element as any).mergeColumnKeys || [],
strictGrouping: (element as any).strictGrouping !== false,
enableMultiHeader: (element as any).enableMultiHeader === true,
tableHeightMode: (element as any).tableHeightMode,
fixedRows: (element as any).fixedRows,
showHeader: (element as any).showHeader,
rowHeight: (element as any).rowHeight,
headerHeight: (element as any).headerHeight,
headerFontSize: (element as any).headerFontSize,
bodyFontSize: (element as any).bodyFontSize,
headerBgColor: (element as any).headerBgColor,
headerTextColor: (element as any).headerTextColor,
footerLabelColumnKey: (element as any).footerLabelColumnKey,
footerLabelText: (element as any).footerLabelText,
footerLabelCenter: (element as any).footerLabelCenter,
footerShowTotal: (element as any).footerShowTotal !== false,
footerTotalMode: (element as any).footerTotalMode || 'overall',
headerConfig: (element as any).headerConfig,
columns: (element as any).columns,
}
: element.type === 'freeTable'
? {
rowCount: Number((element as any).rowCount || 1),
colCount: Number((element as any).colCount || 1),
borderColor: (element as any).borderColor || '#d9d9d9',
borderWidth: Number((element as any).borderWidth || 1),
outerBorderLineStyle: (element as any).outerBorderLineStyle || 'solid',
innerBorderHorizontalLineStyle: (element as any).innerBorderHorizontalLineStyle || 'solid',
innerBorderVerticalLineStyle: (element as any).innerBorderVerticalLineStyle || 'solid',
colWidths: Array.isArray((element as any).colWidths) ? [...(element as any).colWidths] : undefined,
rowHeights: Array.isArray((element as any).rowHeights) ? [...(element as any).rowHeights] : undefined,
outerBorder: (element as any).outerBorder,
innerBorder: (element as any).innerBorder,
cells: Array.isArray((element as any).cells)
? (element as any).cells.map((cell: any) => ({
row: cell.row,
col: cell.col,
rowspan: Math.max(1, Number(cell?.rowspan || 1)),
colspan: Math.max(1, Number(cell?.colspan || 1)),
text: cell.text,
bindField: cell.bindField,
contentType: cell.contentType || 'text',
fillCell: cell.fillCell,
contentScale: cell.contentScale,
imageFit: cell.imageFit,
qrLevel: cell.qrLevel,
qrRenderType: cell.qrRenderType,
barcodeFormat: cell.barcodeFormat,
decimalPlaces: cell.decimalPlaces,
roundHalfUp: cell.roundHalfUp,
amountType: cell.amountType,
autoWrap: cell.autoWrap,
autoFitFont: cell.autoFitFont,
align: cell.align,
verticalAlign: cell.verticalAlign,
fontSize: cell.fontSize,
color: cell.color,
backgroundColor: cell.backgroundColor,
hideBorderTop: cell.hideBorderTop,
hideBorderRight: cell.hideBorderRight,
hideBorderBottom: cell.hideBorderBottom,
hideBorderLeft: cell.hideBorderLeft,
}))
: [],
}
: element.type === 'reportHeader' || element.type === 'reportFooter'
? {
text: (element as any).text,
bookmarkText: (element as any).bookmarkText,
keepTogether: (element as any).keepTogether,
centerWithDetail: (element as any).centerWithDetail,
refreshPage: (element as any).refreshPage,
visible: (element as any).visible,
stretch: (element as any).stretch,
shrink: (element as any).shrink,
printRepeated: (element as any).printRepeated,
printAtPageBottom: (element as any).printAtPageBottom,
removeBlankWhenNoData: (element as any).removeBlankWhenNoData,
}
: element.type === 'qrcode' || element.type === 'barcode'
? { value: (element as any).value }
: { text: (element as any).text, format: (element as any).format },
};
}
/** 与设计器「从画布生成」一致的画布实际 JSON 对象 */
export function buildNativeTemplateStylePayload(schema: NativeTemplateSchema) {
return {
engine: 'native-template-style',
version: '1.0.0',
page: { ...schema.page },
elements: schema.elements.map((item) => mapElementToTemplateStyle(item)),
dataBinding: schema.dataBinding || defaultDataBinding(),
};
}
export function stringifyNativeTemplateStyle(schema: NativeTemplateSchema, space = 2) {
return JSON.stringify(buildNativeTemplateStylePayload(schema), null, space);
}

View File

@@ -0,0 +1,547 @@
import dayjs from 'dayjs';
import QRCode from 'qrcode';
import { buildRowSpanMap } from './tableMerge';
import { getValueByPath, normalizeTableWidths, resolveTableRows } from './tableBuilder';
import type { NativeElement, NativeFreeTableElement, NativeTableElement, NativeTemplateSchema } from './types';
import { normalizeFreeTableAnchors } from './freeTableGrid';
import { borderSidesToCssFragment, resolveFreeTableCellBorderSides } from './freeTableBorders';
import { resolveFreeTableCellLineStyleKeys } from './freeTableLineStyles';
import { resolveFreeTableColWidthsMm, resolveFreeTableRowHeightsMm } from './freeTableTracks';
function resolveBoundValue(element: NativeElement, data: Record<string, any>) {
const bindField = (element as any).bindField;
if (!bindField) return undefined;
return String(bindField)
.split('.')
.reduce((acc: any, key: string) => acc?.[key], data || {});
}
function resolveText(element: NativeElement, data: Record<string, any>, pageNo = 1, totalPages = 1) {
const bindValue = resolveBoundValue(element, data);
if (bindValue !== undefined && bindValue !== null && element.type !== 'pageNo') {
if (element.type === 'date') {
return dayjs(bindValue).format((element as any).format || 'YYYY-MM-DD');
}
return String(bindValue);
}
if (element.type === 'date') {
return dayjs().format((element as any).format || 'YYYY-MM-DD');
}
if (element.type === 'pageNo') {
return (element as any).text.replace('{{pageNo}}', String(pageNo)).replace('{{totalPages}}', String(totalPages));
}
if ((element as any).text?.startsWith('{{') && (element as any).text?.endsWith('}}')) {
const key = (element as any).text.replaceAll('{', '').replaceAll('}', '').trim();
return String(getValueByPath(data || {}, key) ?? '');
}
return String((element as any).text ?? '');
}
async function renderTable(element: NativeTableElement, data: Record<string, any>) {
const sourceRows = resolveTableRows(element, data);
const columns = normalizeTableWidths(element);
return renderTablePage(element, columns as any[], sourceRows, true, sourceRows);
}
async function renderFreeTable(element: NativeFreeTableElement, data: Record<string, any>) {
const rowCount = Math.max(1, Number((element as any)?.rowCount || 1));
const colCount = Math.max(1, Number((element as any)?.colCount || 1));
const wMm = Math.max(0.01, Number((element as any)?.w) || 0.01);
const hMm = Math.max(0.01, Number((element as any)?.h) || 0.01);
const colWidthsMm = resolveFreeTableColWidthsMm(element as any);
const rowHeightsMm = resolveFreeTableRowHeightsMm(element as any);
const borderColor = String((element as any)?.borderColor || '#d9d9d9');
const borderWidth = Math.max(1, Number((element as any)?.borderWidth || 1));
const colgroup = `<colgroup>${colWidthsMm.map((cw) => `<col style="width:${cw}mm;box-sizing:border-box" />`).join('')}</colgroup>`;
const anchors = normalizeFreeTableAnchors(rowCount, colCount, (element as any)?.cells || []);
const body = (
await Promise.all(
Array.from({ length: rowCount }, async (_, row) => {
const rh = rowHeightsMm[row] ?? hMm / rowCount;
const rowCells = (
await Promise.all(
anchors
.filter((cell) => cell.row === row)
.sort((a, b) => a.col - b.col)
.map(async (cell) => {
const rs = Math.max(1, Number((cell as any).rowspan || 1));
const cs = Math.max(1, Number((cell as any).colspan || 1));
const bindField = String((cell as any)?.bindField || '').trim();
const cellValue = bindField ? getValueByPath(data || {}, bindField) ?? '' : (cell as any)?.text ?? '';
const raw = String(cellValue ?? '');
const contentType = String((cell as any)?.contentType || 'text');
const displayValue =
contentType === 'number' || contentType === 'amount'
? formatNumericValue(cellValue, cell as any)
: raw;
const innerArg =
contentType === 'image' || contentType === 'qrcode' || contentType === 'barcode' ? raw : displayValue;
const bodyInnerHtml = await resolvePrintCellInnerHtml(contentType, innerArg, cell as any);
const align = String((cell as any)?.align || 'left');
const verticalAlign = String((cell as any)?.verticalAlign || 'middle');
const fontSize = Math.max(8, Number((cell as any)?.fontSize || element.style?.fontSize || 12));
const color = String((cell as any)?.color || '#111111');
const backgroundColor = String((cell as any)?.backgroundColor || '#ffffff');
const rowspanAttr = rs > 1 ? ` rowspan="${rs}"` : '';
const colspanAttr = cs > 1 ? ` colspan="${cs}"` : '';
const spanW = colWidthsMm.slice(cell.col, cell.col + cs).reduce((a, b) => a + b, 0);
const colWidthStyle = `width:${spanW}mm;`;
const sides = resolveFreeTableCellBorderSides(element, anchors, cell, cell.row, cell.col, rs, cs, rowCount, colCount);
const lineKeys = resolveFreeTableCellLineStyleKeys(element, cell.row, cell.col, rs, cs, rowCount, colCount);
const borderCss = borderSidesToCssFragment(sides, borderWidth, borderColor, lineKeys);
const nowrap = (cell as any).autoWrap === false;
const ws = nowrap ? 'nowrap' : 'normal';
const wb = nowrap ? 'normal' : 'break-all';
const ow = nowrap ? 'normal' : 'anywhere';
return `<td${rowspanAttr}${colspanAttr} style="box-sizing:border-box;${borderCss}${colWidthStyle}padding:2mm;text-align:${align};vertical-align:${verticalAlign};font-size:${fontSize}px;color:${color};background:${backgroundColor};white-space:${ws};word-break:${wb};overflow-wrap:${ow};line-height:${
nowrap ? `${rh}mm` : '1.3'
};">${bodyInnerHtml}</td>`;
}),
)
).join('');
return `<tr style="height:${rh}mm;box-sizing:border-box">${rowCells}</tr>`;
}),
)
).join('');
// 不设 table 固定总高:固定 height 时边框/内边距易超出被裁切;行高之和已贴合元素 h。表格外框由单元格边框拼成。
return `<table style="width:${wMm}mm;border-collapse:collapse;border-spacing:0;table-layout:fixed;box-sizing:border-box;">${colgroup}<tbody>${body}</tbody></table>`;
}
async function renderFixedRowsTablePages(element: NativeTableElement, data: Record<string, any>) {
const sourceRows = resolveTableRows(element, data);
const columns = normalizeTableWidths(element);
const pageSize = Math.max(1, Number(element?.fixedRows || 5));
const pages = chunkRows(sourceRows, pageSize);
const chunks = pages.length ? pages : [[]];
const footerMode = String((element as any).footerTotalMode || 'overall');
return Promise.all(
chunks.map((rows, index) => {
const isLastPage = index === chunks.length - 1;
const showFooter = footerMode === 'page' ? true : isLastPage;
const footerRows = footerMode === 'page' ? rows : sourceRows;
return renderTablePage(element, columns as any[], rows, showFooter, footerRows);
}),
);
}
async function renderTablePage(
element: NativeTableElement,
columns: any[],
rows: Record<string, any>[],
showFooter: boolean,
footerRows: Record<string, any>[] = rows,
) {
const rowSpanMap = buildRowSpanMap(rows, element.columns, (element as any).mergeColumnKeys || [], (element as any).strictGrouping !== false);
const headerHtml = element.showHeader ? buildPrintHeaderHtml(element, columns as any[]) : '';
const bodyHtml = (
await Promise.all(
rows.map(async (row, rowIndex) => {
const cells = (
await Promise.all(
columns.map(async (column) => {
const fieldKey = column.bindField || column.field;
const span = rowSpanMap[`${rowIndex}_${fieldKey}`];
if (span === 0) return '';
const rowSpanText = span && span > 1 ? ` rowspan="${span}"` : '';
const cellValue = getValueByPath(row || {}, fieldKey) ?? '';
const contentType = String((column as any).contentType || 'text');
const displayValue = isNumericColumn(column as any) ? formatNumericValue(cellValue, column as any) : String(cellValue);
const bodyInnerHtml = await resolvePrintCellInnerHtml(contentType, displayValue, column as any);
const bodyBaseSize = (column as any)?.useCustomFontSize ? Number((column as any)?.fontSize || 12) : Number(element.bodyFontSize || 12);
const fontSize = resolvePrintAutoFontSize(column as any, displayValue, Number(column?.width || 30), element.rowHeight || 8, bodyBaseSize);
return `<td${rowSpanText} style="border:1px solid #222;padding:2mm;text-align:${column.align || 'left'};font-family:${(column as any).fontFamily || 'inherit'};font-size:${fontSize}px;color:${
(column as any).fontColor || '#111111'
};white-space:${(column as any).autoWrap === false ? 'nowrap' : 'normal'};word-break:${(column as any).autoWrap === false ? 'normal' : 'break-all'};overflow-wrap:${
(column as any).autoWrap === false ? 'normal' : 'anywhere'
};line-height:${(column as any).autoWrap === false ? `${element.rowHeight || 8}mm` : '1.3'};">${bodyInnerHtml}</td>`;
}),
)
).join('');
return `<tr style="height:${element.rowHeight}mm;">${cells}</tr>`;
}),
)
).join('');
const footerHtml = showFooter ? buildFooterHtml(footerRows, columns as any[], element) : '';
return `<table style="width:100%;border-collapse:collapse;table-layout:fixed;"><thead>${headerHtml}</thead><tbody>${bodyHtml}</tbody>${footerHtml}</table>`;
}
function chunkRows(rows: Record<string, any>[], pageSize: number) {
const size = Math.max(1, Number(pageSize || 1));
const list: Record<string, any>[][] = [];
for (let i = 0; i < rows.length; i += size) {
list.push(rows.slice(i, i + size));
}
return list;
}
function buildPrintHeaderHtml(element: NativeTableElement, columns: any[]) {
const headerRows = buildPrintHeaderRows(element, columns);
const rowCount = Math.max(1, headerRows.length);
const rowHeight = (element.headerHeight || 10) / rowCount;
return headerRows
.map((cells) => {
const cellHtml = cells
.map((cell) => {
const column = columns[cell.col] || {};
const widthMm = columns.slice(cell.col, cell.col + cell.colspan).reduce((sum: number, col: any) => sum + Number(col?.width || 0), 0);
const cellHeightMm = rowHeight * Number(cell.rowspan || 1);
const headerFontSize = resolvePrintAutoFontSize(
column as any,
String(cell?.title || ''),
Number(widthMm || column?.width || 30),
cellHeightMm,
Number(element.headerFontSize || 12),
);
return `<th rowspan="${cell.rowspan}" colspan="${cell.colspan}" style="border:1px solid #222;padding:2mm;text-align:${cell.align || column.align || 'center'};font-weight:600;width:${
cell.widthPercent
}%;background:${element.headerBgColor || '#f5f5f5'};color:${(column as any).fontColor || element.headerTextColor || '#111111'};font-family:${
(column as any).fontFamily || 'inherit'
};font-size:${headerFontSize}px;white-space:${(column as any).autoWrap === false ? 'nowrap' : 'normal'};word-break:${
(column as any).autoWrap === false ? 'normal' : 'break-all'
};overflow-wrap:${(column as any).autoWrap === false ? 'normal' : 'anywhere'};line-height:${
(column as any).autoWrap === false ? `${cellHeightMm}mm` : '1.3'
};">${cell.title || ''}</th>`;
})
.join('');
return `<tr style="height:${rowHeight}mm;">${cellHtml}</tr>`;
})
.join('');
}
function buildPrintHeaderRows(element: NativeTableElement, columns: any[]) {
const colCount = columns.length;
if (!colCount) return [];
if (element.enableMultiHeader !== true) {
return [
columns.map((col: any, index: number) => ({
row: 0,
col: index,
rowspan: 1,
colspan: 1,
title: String(col?.title || ''),
align: col?.align || 'center',
widthPercent: Number(col?.widthPercent || 0),
})),
];
}
const headerConfig = (element as any)?.headerConfig;
const rowCount = Math.max(1, Number(headerConfig?.rowCount || 1));
const owner: any[][] = Array.from({ length: rowCount }, () => Array.from({ length: colCount }, () => null));
const cells: any[] = [];
const configCells = Array.isArray(headerConfig?.cells) && Number(headerConfig?.colCount || 0) === colCount ? headerConfig.cells : [];
configCells.forEach((item: any) => {
const row = Math.max(0, Number(item?.row || 0));
const col = Math.max(0, Number(item?.col || 0));
const rowspan = Math.max(1, Number(item?.rowspan || 1));
const colspan = Math.max(1, Number(item?.colspan || 1));
if (row >= rowCount || col >= colCount || owner[row][col]) return;
const maxRow = Math.min(rowCount, row + rowspan);
const maxCol = Math.min(colCount, col + colspan);
for (let r = row; r < maxRow; r += 1) {
for (let c = col; c < maxCol; c += 1) {
if (owner[r][c]) return;
}
}
const next = { row, col, rowspan: maxRow - row, colspan: maxCol - col, title: String(item?.title || ''), align: String(item?.align || 'center') };
for (let r = row; r < maxRow; r += 1) {
for (let c = col; c < maxCol; c += 1) {
owner[r][c] = next;
}
}
cells.push(next);
});
for (let r = 0; r < rowCount; r += 1) {
for (let c = 0; c < colCount; c += 1) {
if (owner[r][c]) continue;
const fallback = { row: r, col: c, rowspan: 1, colspan: 1, title: r === rowCount - 1 ? String(columns[c]?.title || '') : '', align: columns[c]?.align || 'center' };
owner[r][c] = fallback;
cells.push(fallback);
}
}
const rows = Array.from({ length: rowCount }, () => [] as any[]);
cells.forEach((cell) => {
if (owner[cell.row][cell.col] !== cell) return;
const widthPercent = columns.slice(cell.col, cell.col + cell.colspan).reduce((sum: number, col: any) => sum + Number(col?.widthPercent || 0), 0);
rows[cell.row].push({ ...cell, widthPercent });
});
rows.forEach((list) => list.sort((a, b) => a.col - b.col));
return rows;
}
async function buildQrcodeDataUrl(value: string, column?: any) {
const text = String(value || 'empty');
return QRCode.toDataURL(text, {
errorCorrectionLevel: column?.qrLevel || 'M',
margin: 0,
type: column?.qrRenderType || 'image/png',
width: 220,
});
}
async function resolvePrintCellInnerHtml(contentType: string, value: string, column: any) {
const safeValue = String(value || '');
const fillCell = column?.fillCell !== false;
const scale = Math.max(10, Math.min(100, Number(column?.contentScale || 100)));
if (contentType === 'image') {
const src = safeValue || `https://via.placeholder.com/180x80.png?text=${encodeURIComponent(column?.title || 'Image')}`;
return `<img src="${src}" style="display:block;margin:0 auto;max-width:100%;max-height:100%;object-fit:${column?.imageFit || 'contain'};width:${
fillCell ? '100%' : `${scale}%`
};height:${fillCell ? '100%' : `${scale}%`};" />`;
}
if (contentType === 'qrcode') {
try {
const src = await buildQrcodeDataUrl(safeValue, column);
return `<img src="${src}" style="display:block;margin:0 auto;max-width:100%;max-height:100%;object-fit:contain;width:${fillCell ? '100%' : `${scale}%`};height:${
fillCell ? '100%' : `${scale}%`
};" />`;
} catch (_error) {
return `<div style="display:flex;align-items:center;justify-content:center;border:1px dashed #999;width:${fillCell ? '100%' : `${scale}%`};height:${
fillCell ? '100%' : `${scale}%`
};margin:0 auto;">QR:${safeValue}</div>`;
}
}
if (contentType === 'barcode') {
return `<div style="display:flex;align-items:center;justify-content:center;border:1px dashed #999;width:${fillCell ? '100%' : `${scale}%`};height:${
fillCell ? '100%' : `${Math.max(20, scale * 0.6)}%`
};margin:0 auto;">BAR:${safeValue}</div>`;
}
return safeValue;
}
function resolvePrintAutoFontSize(column: any, text: string, columnWidthMm: number, rowHeightMm: number, baseSize: number) {
const base = Number(baseSize || 12);
if (!column?.autoFitFont) {
return base;
}
const widthPx = Math.max(1, columnWidthMm * 3.7795275591);
const heightPx = Math.max(1, rowHeightMm * 3.7795275591);
const textLen = Math.max(1, text.length);
const byWidth = widthPx / Math.max(1, textLen * 0.62);
const byHeight = column?.autoWrap === false ? heightPx * 0.55 : heightPx * 0.36;
const next = Math.min(base, byWidth, byHeight);
return Math.max(8, Math.round(next));
}
function isNumericColumn(column: any) {
const type = String(column?.contentType || 'text');
return type === 'number' || type === 'amount';
}
function formatNumericValue(value: any, column: any) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return String(value ?? '');
}
const decimals = Math.max(0, Math.min(6, Number(column?.decimalPlaces ?? 2)));
const finalValue = column?.roundHalfUp === false ? Math.trunc(numeric * 10 ** decimals) / 10 ** decimals : Number(numeric.toFixed(decimals));
const formatted = finalValue.toLocaleString(undefined, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
});
if (String(column?.contentType || 'text') === 'amount') {
const symbol = column?.amountType === 'USD' ? '$' : column?.amountType === 'EUR' ? 'EUR ' : '¥';
return `${symbol}${formatted}`;
}
return formatted;
}
function buildFooterHtml(rows: Record<string, any>[], columns: any[], element: NativeTableElement) {
if ((element as any)?.footerShowTotal === false) {
return '';
}
const labelColumnKey = String(element?.footerLabelColumnKey || columns?.[0]?.key || '');
const labelText = String(element?.footerLabelText || '合计');
const labelAlign = element?.footerLabelCenter === false ? 'left' : 'center';
const cells = columns
.map((column, index) => {
if (isNumericColumn(column) && !!column?.enableFooterTotal) {
const fieldKey = column?.bindField || column?.field;
const total = rows.reduce((sum, row) => {
const value = Number(getValueByPath(row || {}, fieldKey));
return sum + (Number.isFinite(value) ? value : 0);
}, 0);
return `<td style="border:1px solid #222;padding:2mm;font-weight:600;text-align:${column.align || 'left'};background:#fafafa;">${formatNumericValue(total, column)}</td>`;
}
if (String(column?.key || '') === labelColumnKey || (!labelColumnKey && index === 0)) {
return `<td style="border:1px solid #222;padding:2mm;font-weight:600;background:#fafafa;text-align:${labelAlign};">${labelText}</td>`;
}
return `<td style="border:1px solid #222;padding:2mm;background:#fafafa;"></td>`;
})
.join('');
return `<tfoot><tr>${cells}</tr></tfoot>`;
}
export async function renderNativePrintHtml(schema: NativeTemplateSchema, data: Record<string, any>) {
const pageCount = resolvePrintPageCount(schema, data);
const totalHeight = schema.page.height * pageCount;
const repeatHeaderConfig = resolveRepeatHeaderConfig(schema);
const repeatHeaderByPage = repeatHeaderConfig.enabled;
const headerVisible = repeatHeaderConfig.visible;
const headerBandHeight = resolveHeaderBandHeight(schema);
const sorted = [...schema.elements].sort((a, b) => a.zIndex - b.zIndex);
const content = (
await Promise.all(
sorted.map(async (item) => {
if ((item as any).visible === false) {
return '';
}
const isReportFooter = item.type === 'reportFooter';
const isReportHeader = item.type === 'reportHeader';
const repeatReportHeader = isReportHeader && (item as any).printRepeated === true;
const isHeaderRegionElement = isElementInHeaderRegion(item, repeatHeaderConfig.headerId, headerBandHeight);
if (!headerVisible && isHeaderRegionElement) {
return '';
}
const repeatHeaderElement = isHeaderRegionElement && repeatHeaderByPage;
const renderX = isReportHeader || isReportFooter ? 0 : item.x;
const renderY = isReportHeader ? 0 : isReportFooter && (item as any).printAtPageBottom === true ? Math.max(0, schema.page.height - item.h) : item.y;
const renderW = isReportHeader || isReportFooter ? schema.page.width : item.w;
const styleParts = [
`position:absolute`,
`width:${renderW}mm`,
`height:${item.h}mm`,
`font-size:${item.style?.fontSize || 12}px`,
`font-weight:${item.style?.fontWeight || 400}`,
`color:${item.style?.color || '#111'}`,
`line-height:${item.style?.lineHeight || 1.4}`,
`text-align:${item.style?.textAlign || 'left'}`,
`background:${item.style?.backgroundColor || 'transparent'}`,
item.style?.borderWidth ? `border:${item.style.borderWidth}px solid ${item.style.borderColor || '#222'}` : '',
'overflow:hidden',
]
.filter(Boolean)
.join(';');
const style = (topMm: number) => [`left:${renderX}mm`, `top:${topMm}mm`, styleParts].join(';');
const shouldRepeat = (repeatReportHeader || repeatHeaderElement) && pageCount > 1;
const pages = shouldRepeat ? Array.from({ length: pageCount }, (_v, i) => i + 1) : [1];
const htmlByPage = await Promise.all(
pages.map(async (pageNo) => {
const top = renderY + (shouldRepeat ? (pageNo - 1) * schema.page.height : 0);
if (item.type === 'table' || item.type === 'detailTable') {
const tableMode = String((item as any).tableHeightMode || 'autoPage');
if (tableMode === 'fixedRows') {
const pageTables = await renderFixedRowsTablePages(item, data);
if (shouldRepeat) {
const firstPageTable = pageTables[0] || '';
return `<div style="${style(top)};overflow:visible;height:auto;">${firstPageTable}</div>`;
}
return pageTables
.map((tableHtml, pageIndex) => {
const pageTop = renderY + pageIndex * schema.page.height;
return `<div style="${style(pageTop)};overflow:visible;height:auto;">${tableHtml}</div>`;
})
.join('');
}
return `<div style="${style(top)};overflow:visible;height:auto;">${await renderTable(item, data)}</div>`;
}
if (item.type === 'freeTable') {
// 自由表格:避免 overflow:hidden 裁掉底边/竖线边框;由行高+box-sizing 控制占位
const ftHtml = await renderFreeTable(item as NativeFreeTableElement, data);
return `<div style="${style(top)};overflow:visible;">${ftHtml}</div>`;
}
if (item.type === 'qrcode') {
const value = resolveBoundValue(item, data) ?? (item as any).value;
try {
const src = await buildQrcodeDataUrl(String(value ?? ''), item as any);
return `<img src="${src}" style="${style(top)};object-fit:contain;" />`;
} catch (_error) {
return `<div style="${style(top)};display:flex;align-items:center;justify-content:center;border:1px dashed #999;">二维码:${value ?? ''}</div>`;
}
}
if (item.type === 'barcode') {
const value = resolveBoundValue(item, data) ?? (item as any).value;
return `<div style="${style(top)};display:flex;align-items:center;justify-content:center;border:1px dashed #999;">条形码:${value ?? ''}</div>`;
}
if (item.type === 'image') {
const image = item as any;
const src = (resolveBoundValue(item, data) ?? image.src) || '';
return `<img src="${src}" style="${style(top)};object-fit:${image.fit || 'contain'};" />`;
}
return `<div style="${style(top)};white-space:pre-wrap;">${resolveText(item, data, pageNo, pageCount)}</div>`;
}),
);
return htmlByPage.join('');
}),
)
).join('');
const pageBreakGuides = pageCount > 1
? Array.from({ length: pageCount - 1 })
.map((_v, index) => `<div style="position:absolute;left:0;top:${(index + 1) * schema.page.height}mm;width:${schema.page.width}mm;height:0;page-break-before:always;"></div>`)
.join('')
: '';
const pageMargin = resolvePageMarginCss(schema.page.margin);
return `<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<style>
@page { size: ${schema.page.width}mm ${schema.page.height}mm; margin: ${pageMargin}; }
html, body { margin: 0; padding: 0; overflow: visible; }
* {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
</style>
</head>
<body>
<div style="position:relative;width:${schema.page.width}mm;min-height:${totalHeight}mm;height:auto;overflow:visible;box-sizing:border-box;">
${content}
${pageBreakGuides}
</div>
</body>
</html>`;
}
function resolvePageMarginCss(margin?: [number, number, number, number]) {
if (!Array.isArray(margin) || margin.length < 4) {
return '0mm';
}
const top = Math.max(0, Number(margin[0] || 0));
const right = Math.max(0, Number(margin[1] || 0));
const bottom = Math.max(0, Number(margin[2] || 0));
const left = Math.max(0, Number(margin[3] || 0));
return `${top}mm ${right}mm ${bottom}mm ${left}mm`;
}
/** 与 renderNativePrintHtml 内部页数计算一致,供列表预览等比缩放使用 */
export function resolvePrintPageCount(schema: NativeTemplateSchema, data: Record<string, any>) {
const tablePages = schema.elements
.filter((item) => item.type === 'table' || item.type === 'detailTable')
.map((item: any) => {
const rows = resolveTableRows(item as NativeTableElement, data);
const mode = String(item?.tableHeightMode || 'autoPage');
if (mode !== 'fixedRows') {
return 1;
}
const pageSize = Math.max(1, Number(item?.fixedRows || 5));
return Math.max(1, Math.ceil(rows.length / pageSize));
});
return Math.max(1, ...tablePages);
}
function resolveRepeatHeaderConfig(schema: NativeTemplateSchema) {
const reportHeader = schema.elements.find((item) => item.type === 'reportHeader') as any;
if (!reportHeader) return { visible: true, enabled: false, headerId: '' };
if (reportHeader.visible === false) return { visible: false, enabled: false, headerId: String(reportHeader.id || '') };
return { visible: true, enabled: reportHeader.printRepeated === true, headerId: String(reportHeader.id || '') };
}
function resolveHeaderBandHeight(schema: NativeTemplateSchema) {
const reportHeader = schema.elements.find((item) => item.type === 'reportHeader') as any;
return Math.max(0, Number(reportHeader?.h || 0));
}
function isElementInHeaderRegion(element: NativeElement, repeatHeaderId: string, headerBandHeight: number) {
if (element.type === 'reportHeader') return true;
if (element.type === 'reportFooter') return false;
const bandId = String((element as any).bandId || '');
if (repeatHeaderId && bandId === repeatHeaderId) return true;
const region = String((element as any).region || '');
if (region === 'header') return true;
if (region === 'body' || region === 'footer') return false;
if (headerBandHeight <= 0) return false;
const topY = Number(element.y || 0);
const bottomY = topY + Number(element.h || 0);
// 兼容旧模板:未标注 region 时,按位置判断元素是否属于报表头区域
return topY < headerBandHeight && bottomY <= headerBandHeight + 0.2;
}

View File

@@ -0,0 +1,64 @@
let printingInProgress = false;
export function printHtml(html: string) {
if (printingInProgress) {
return;
}
printingInProgress = true;
const iframe = document.createElement('iframe');
iframe.style.position = 'fixed';
iframe.style.right = '0';
iframe.style.bottom = '0';
iframe.style.width = '0';
iframe.style.height = '0';
iframe.style.border = '0';
iframe.style.opacity = '0';
iframe.setAttribute('aria-hidden', 'true');
iframe.setAttribute('sandbox', 'allow-modals allow-same-origin');
let printTriggered = false;
let cleaned = false;
let timeoutId = 0;
const cleanup = () => {
if (cleaned) return;
cleaned = true;
if (timeoutId) {
window.clearTimeout(timeoutId);
}
iframe.removeEventListener('load', handleLoad);
setTimeout(() => {
iframe.remove();
printingInProgress = false;
}, 0);
};
const handleLoad = () => {
if (printTriggered) {
return;
}
printTriggered = true;
const win = iframe.contentWindow;
if (!win) {
cleanup();
return;
}
const handleAfterPrint = () => {
win.removeEventListener('afterprint', handleAfterPrint);
cleanup();
};
win.addEventListener('afterprint', handleAfterPrint);
setTimeout(() => {
win.focus();
win.print();
}, 50);
};
iframe.addEventListener('load', handleLoad);
iframe.srcdoc = html;
document.body.appendChild(iframe);
timeoutId = window.setTimeout(() => {
cleanup();
}, 60 * 1000);
}

View File

@@ -0,0 +1,25 @@
import type { NativeTableElement } from './types';
export function getValueByPath(data: Record<string, any>, path: string) {
if (!path) return undefined;
return String(path)
.split('.')
.reduce((acc: any, key: string) => acc?.[key], data || {});
}
export function resolveTableRows(element: NativeTableElement, data: Record<string, any>) {
const rows = data?.[element.source];
if (Array.isArray(rows)) {
return rows.filter((item) => item && typeof item === 'object');
}
return [];
}
export function normalizeTableWidths(element: NativeTableElement) {
const total = element.columns.reduce((sum, item) => sum + Number(item.width || 0), 0);
if (!total) return element.columns;
return element.columns.map((item) => ({
...item,
widthPercent: (Number(item.width || 0) / total) * 100,
}));
}

View File

@@ -0,0 +1,61 @@
import type { NativeTableColumn } from './types';
import { getValueByPath } from './tableBuilder';
interface MergeRange {
start: number;
end: number;
}
function resolveMergeFields(columns: NativeTableColumn[], mergeColumnKeys: string[] = []) {
const byKey = new Map(columns.map((col) => [String(col?.key || ''), col] as const));
const orderedFields = mergeColumnKeys
.map((key) => byKey.get(String(key || '')))
.filter(Boolean)
.map((col) => String(col?.bindField || col?.field || ''))
.filter(Boolean);
if (orderedFields.length) {
return orderedFields;
}
return columns
.filter((item) => item.mergeByValue)
.map((item) => String(item.bindField || item.field || ''))
.filter(Boolean);
}
function buildRangesByField(rows: Record<string, any>[], field: string, ranges: MergeRange[]) {
const nextRanges: MergeRange[] = [];
const map: Record<string, number> = {};
ranges.forEach((range) => {
let start = range.start;
while (start < range.end) {
const value = getValueByPath(rows[start] || {}, field);
let end = start + 1;
while (end < range.end && getValueByPath(rows[end] || {}, field) === value) {
end += 1;
}
map[`${start}_${field}`] = end - start;
for (let i = start + 1; i < end; i += 1) {
map[`${i}_${field}`] = 0;
}
nextRanges.push({ start, end });
start = end;
}
});
return { map, nextRanges };
}
export function buildRowSpanMap(rows: Record<string, any>[], columns: NativeTableColumn[], mergeColumnKeys: string[] = [], strictGrouping = false) {
const map: Record<string, number> = {};
if (!rows.length) return map;
const mergeFields = resolveMergeFields(columns, mergeColumnKeys);
if (!mergeFields.length) return map;
let currentRanges: MergeRange[] = [{ start: 0, end: rows.length }];
mergeFields.forEach((field) => {
const { map: fieldMap, nextRanges } = buildRangesByField(rows, field, currentRanges);
Object.assign(map, fieldMap);
if (strictGrouping) {
currentRanges = nextRanges;
}
});
return map;
}

View File

@@ -0,0 +1,267 @@
import type { FreeTableLineStyleKey } from './freeTableLineStyles';
export type NativeElementType =
| 'title'
| 'subtitle'
| 'text'
| 'date'
| 'pageNo'
| 'reportHeader'
| 'reportFooter'
| 'image'
| 'table'
| 'detailTable'
| 'freeTable'
| 'qrcode'
| 'barcode';
export interface NativeElementBase {
id: string;
type: NativeElementType;
bindField?: string;
region?: 'header' | 'body' | 'footer';
bandId?: string;
x: number;
y: number;
w: number;
h: number;
rotate?: number;
zIndex: number;
style?: {
fontSize?: number;
fontWeight?: number | string;
color?: string;
textAlign?: 'left' | 'center' | 'right';
lineHeight?: number;
borderWidth?: number;
borderColor?: string;
backgroundColor?: string;
};
}
export interface NativeTextElement extends NativeElementBase {
type: 'title' | 'subtitle' | 'text' | 'date' | 'pageNo';
text: string;
format?: string;
}
export interface NativeReportBandElement extends NativeElementBase {
type: 'reportHeader' | 'reportFooter';
text: string;
bookmarkText?: string;
keepTogether?: boolean;
centerWithDetail?: boolean;
refreshPage?: 'none' | 'always' | 'onOverflow';
visible?: boolean;
stretch?: boolean;
shrink?: boolean;
printRepeated?: boolean;
printAtPageBottom?: boolean;
removeBlankWhenNoData?: boolean;
}
export interface NativeImageElement extends NativeElementBase {
type: 'image';
src: string;
fit: 'fill' | 'contain' | 'cover';
}
export interface NativeCodeElement extends NativeElementBase {
type: 'qrcode' | 'barcode';
value: string;
}
export interface NativeTableColumn {
key: string;
title: string;
field: string;
bindField?: string;
width: number;
align?: 'left' | 'center' | 'right';
contentType?: 'text' | 'image' | 'qrcode' | 'barcode' | 'number' | 'amount';
fontFamily?: string;
fontSize?: number;
useCustomFontSize?: boolean;
fontColor?: string;
autoFitFont?: boolean;
autoWrap?: boolean;
fillCell?: boolean;
contentScale?: number;
imageFit?: 'fill' | 'contain' | 'cover';
qrLevel?: 'L' | 'M' | 'Q' | 'H';
qrRenderType?: 'image/png' | 'image/jpeg' | 'image/webp';
barcodeFormat?: string;
decimalPlaces?: number;
roundHalfUp?: boolean;
amountType?: 'CNY' | 'USD' | 'EUR';
enableFooterTotal?: boolean;
mergeByValue?: boolean;
}
export interface NativeTableHeaderCell {
id: string;
row: number;
col: number;
rowspan: number;
colspan: number;
title: string;
align?: 'left' | 'center' | 'right';
}
export interface NativeTableHeaderConfig {
rowCount: number;
colCount: number;
cells: NativeTableHeaderCell[];
}
export interface NativeTableElement extends NativeElementBase {
type: 'table' | 'detailTable';
source: string;
mergeColumnKeys?: string[];
strictGrouping?: boolean;
enableMultiHeader?: boolean;
tableHeightMode?: 'autoPage' | 'fixedRows';
fixedRows?: number;
showHeader: boolean;
rowHeight: number;
headerHeight: number;
headerFontSize?: number;
bodyFontSize?: number;
headerBgColor?: string;
headerTextColor?: string;
footerLabelColumnKey?: string;
footerLabelText?: string;
footerLabelCenter?: boolean;
footerShowTotal?: boolean;
footerTotalMode?: 'overall' | 'page';
headerConfig?: NativeTableHeaderConfig;
columns: NativeTableColumn[];
}
export interface NativeFreeTableCell {
row: number;
col: number;
rowspan?: number;
colspan?: number;
text?: string;
bindField?: string;
/** 单元格内容类型,与明细表列 contentType 一致 */
contentType?: 'text' | 'image' | 'qrcode' | 'barcode' | 'number' | 'amount';
fillCell?: boolean;
contentScale?: number;
imageFit?: 'fill' | 'contain' | 'cover';
qrLevel?: 'L' | 'M' | 'Q' | 'H';
qrRenderType?: 'image/png' | 'image/jpeg' | 'image/webp';
barcodeFormat?: string;
decimalPlaces?: number;
roundHalfUp?: boolean;
amountType?: 'CNY' | 'USD' | 'EUR';
autoWrap?: boolean;
autoFitFont?: boolean;
align?: 'left' | 'center' | 'right';
verticalAlign?: 'top' | 'middle' | 'bottom';
fontSize?: number;
color?: string;
backgroundColor?: string;
/** 为 true 时强制不绘制该侧边框(与表格局部内线/外框共同作用) */
hideBorderTop?: boolean;
hideBorderRight?: boolean;
hideBorderBottom?: boolean;
hideBorderLeft?: boolean;
}
/** 表格外轮廓:缺省四边均显示(字段为 false 时隐藏该边) */
export interface NativeFreeTableOuterBorder {
top?: boolean;
right?: boolean;
bottom?: boolean;
left?: boolean;
}
/** 表格内部网格线:缺省横向、纵向均显示 */
export interface NativeFreeTableInnerBorder {
/** 行间横线 */
horizontal?: boolean;
/** 列间竖线 */
vertical?: boolean;
}
export interface NativeFreeTableElement extends NativeElementBase {
type: 'freeTable';
rowCount: number;
colCount: number;
/** 各列宽度mm长度与 colCount 一致;未设置时按元素 w 均分 */
colWidths?: number[];
/** 各行高度mm长度与 rowCount 一致;未设置时按元素 h 均分 */
rowHeights?: number[];
borderColor?: string;
borderWidth?: number;
/** 表格外轮廓线型(四边最外一圈) */
outerBorderLineStyle?: FreeTableLineStyleKey;
/** 行间横线(内部水平网格线)线型 */
innerBorderHorizontalLineStyle?: FreeTableLineStyleKey;
/** 列间竖线(内部垂直网格线)线型 */
innerBorderVerticalLineStyle?: FreeTableLineStyleKey;
/** 表格外框四边显示开关 */
outerBorder?: NativeFreeTableOuterBorder;
/** 内部横线/竖线显示开关 */
innerBorder?: NativeFreeTableInnerBorder;
cells: NativeFreeTableCell[];
}
export type NativeElement =
| NativeTextElement
| NativeReportBandElement
| NativeImageElement
| NativeCodeElement
| NativeTableElement
| NativeFreeTableElement;
export interface NativePageConfig {
width: number;
height: number;
unit: 'mm';
margin: [number, number, number, number];
gridSize: number;
}
/** 非明细类组件可用的绑定参数(主数据字段路径等) */
export interface NativeDataBindingParam {
key: string;
label?: string;
}
/** 明细表下的字段定义 */
export interface NativeDataBindingDetailField {
key: string;
label?: string;
}
/** 明细数据源:表名 + 字段列表(树状维护) */
export interface NativeDataBindingDetailTable {
/** 与明细表格元素的 source 对应,如 detailList */
tableKey: string;
label?: string;
fields: NativeDataBindingDetailField[];
}
export interface NativeTemplateSchema {
engine: 'native';
version: string;
page: NativePageConfig;
elements: NativeElement[];
dataBinding?: {
fieldMap?: Record<string, string>;
tableSources?: string[];
/** 参数:供文本/自由表格等非明细组件 bindField 参考 */
params?: NativeDataBindingParam[];
/** 字段树:供明细表格等按明细 source 绑定参考 */
detailTables?: NativeDataBindingDetailTable[];
};
}
export interface NativeDesignerState {
schema: NativeTemplateSchema;
selectedId: string;
scale: number;
}

View File

@@ -0,0 +1,386 @@
import { computed, reactive } from 'vue';
import type {
NativeDesignerState,
NativeElement,
NativeElementType,
NativeFreeTableElement,
NativeTableElement,
NativeTemplateSchema,
} from './types';
const MM_TO_PX = 3.7795275591;
function uid(prefix: string) {
return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
}
export function createDefaultSchema(): NativeTemplateSchema {
return {
engine: 'native',
version: '1.0.0',
page: {
width: 210,
height: 297,
unit: 'mm',
margin: [10, 10, 10, 10],
gridSize: 2,
},
elements: [],
dataBinding: {
fieldMap: {},
tableSources: ['mainTable', 'detailList'],
params: [],
detailTables: [],
},
};
}
function createTableColumns() {
return [
{
key: uid('col'),
title: '列1',
field: 'field1',
bindField: 'field1',
width: 30,
align: 'left' as const,
contentType: 'text' as const,
fontFamily: '',
fontSize: 12,
useCustomFontSize: false,
fontColor: '#111111',
autoFitFont: false,
autoWrap: true,
fillCell: true,
contentScale: 100,
imageFit: 'contain' as const,
qrLevel: 'M' as const,
qrRenderType: 'image/png' as const,
barcodeFormat: 'CODE128',
decimalPlaces: 2,
roundHalfUp: true,
amountType: 'CNY' as const,
enableFooterTotal: false,
mergeByValue: false,
},
{
key: uid('col'),
title: '列2',
field: 'field2',
bindField: 'field2',
width: 30,
align: 'left' as const,
contentType: 'text' as const,
fontFamily: '',
fontSize: 12,
useCustomFontSize: false,
fontColor: '#111111',
autoFitFont: false,
autoWrap: true,
fillCell: true,
contentScale: 100,
imageFit: 'contain' as const,
qrLevel: 'M' as const,
qrRenderType: 'image/png' as const,
barcodeFormat: 'CODE128',
decimalPlaces: 2,
roundHalfUp: true,
amountType: 'CNY' as const,
enableFooterTotal: false,
mergeByValue: false,
},
{
key: uid('col'),
title: '列3',
field: 'field3',
bindField: 'field3',
width: 30,
align: 'left' as const,
contentType: 'text' as const,
fontFamily: '',
fontSize: 12,
useCustomFontSize: false,
fontColor: '#111111',
autoFitFont: false,
autoWrap: true,
fillCell: true,
contentScale: 100,
imageFit: 'contain' as const,
qrLevel: 'M' as const,
qrRenderType: 'image/png' as const,
barcodeFormat: 'CODE128',
decimalPlaces: 2,
roundHalfUp: true,
amountType: 'CNY' as const,
enableFooterTotal: false,
mergeByValue: false,
},
];
}
export function createElementByType(type: NativeElementType, zIndex: number, defaultTableSource = 'List1'): NativeElement {
const base = { id: uid(type), type, bindField: '', region: 'body' as const, bandId: '', x: 20, y: 20, w: 60, h: 12, zIndex };
if (type === 'title') {
return { ...base, type, text: '标题', h: 14, style: { fontSize: 20, fontWeight: 700, textAlign: 'center' } };
}
if (type === 'subtitle') {
return { ...base, type, text: '副标题', style: { fontSize: 14, textAlign: 'center' } };
}
if (type === 'text') {
return { ...base, type, text: '文本内容', style: { fontSize: 12 } };
}
if (type === 'date') {
return { ...base, type, text: '日期', format: 'YYYY-MM-DD', style: { fontSize: 12 } };
}
if (type === 'pageNo') {
return { ...base, type, text: '第 {{pageNo}} / {{totalPages}} 页', style: { fontSize: 12 } };
}
if (type === 'reportHeader') {
return {
...base,
type,
x: 10,
y: 10,
w: 190,
h: 16,
region: 'header',
text: '',
bookmarkText: '',
keepTogether: true,
centerWithDetail: true,
refreshPage: 'none',
visible: true,
stretch: false,
shrink: false,
printRepeated: false,
style: { fontSize: 12, fontWeight: 600, textAlign: 'left', backgroundColor: '#ffffff' },
} as any;
}
if (type === 'reportFooter') {
return {
...base,
type,
x: 10,
y: 260,
w: 190,
h: 18,
region: 'footer',
text: '',
bookmarkText: '',
keepTogether: true,
centerWithDetail: true,
refreshPage: 'none',
visible: true,
stretch: false,
shrink: false,
printRepeated: false,
printAtPageBottom: false,
removeBlankWhenNoData: false,
style: { fontSize: 12, fontWeight: 600, textAlign: 'left', backgroundColor: '#ffffff' },
} as any;
}
if (type === 'image') {
return { ...base, type, w: 36, h: 24, src: '', fit: 'contain' };
}
if (type === 'qrcode') {
return { ...base, type, w: 24, h: 24, value: 'https://www.jeecg.com' };
}
if (type === 'barcode') {
return { ...base, type, w: 48, h: 18, value: '1234567890' };
}
if (type === 'freeTable') {
return {
...base,
type,
w: 120,
h: 50,
rowCount: 3,
colCount: 3,
borderColor: '#d9d9d9',
borderWidth: 1,
outerBorderLineStyle: 'solid',
innerBorderHorizontalLineStyle: 'solid',
innerBorderVerticalLineStyle: 'solid',
cells: Array.from({ length: 3 }).flatMap((_, row) =>
Array.from({ length: 3 }).map((_c, col) => ({
row,
col,
rowspan: 1,
colspan: 1,
text: `单元格${row + 1}-${col + 1}`,
bindField: '',
align: 'left',
verticalAlign: 'middle',
fontSize: 12,
color: '#111111',
backgroundColor: '#ffffff',
})),
),
} as NativeFreeTableElement;
}
return {
...base,
type,
w: 120,
h: 36,
source: defaultTableSource,
mergeColumnKeys: [],
strictGrouping: true,
enableMultiHeader: false,
tableHeightMode: 'autoPage',
fixedRows: 5,
showHeader: true,
rowHeight: 8,
headerHeight: 10,
headerFontSize: 12,
bodyFontSize: 12,
headerBgColor: '#f5f5f5',
headerTextColor: '#111111',
footerLabelColumnKey: '',
footerLabelText: '合计',
footerLabelCenter: true,
footerShowTotal: true,
footerTotalMode: 'overall',
columns: createTableColumns(),
} as NativeTableElement;
}
export function useDesignerStore() {
const state = reactive<NativeDesignerState>({
schema: createDefaultSchema(),
selectedId: '',
scale: 1,
});
const selectedElement = computed(() => state.schema.elements.find((item) => item.id === state.selectedId) as NativeElement | undefined);
function setSchema(schema: NativeTemplateSchema) {
const merged: NativeTemplateSchema = {
...schema,
dataBinding: {
fieldMap: { ...(schema.dataBinding?.fieldMap || {}) },
tableSources: Array.isArray(schema.dataBinding?.tableSources)
? [...(schema.dataBinding!.tableSources as string[])]
: ['mainTable', 'detailList'],
params: Array.isArray(schema.dataBinding?.params) ? [...schema.dataBinding!.params!] : [],
detailTables: Array.isArray(schema.dataBinding?.detailTables)
? schema.dataBinding!.detailTables!.map((t) => ({
...t,
fields: Array.isArray(t.fields) ? [...t.fields] : [],
}))
: [],
},
};
state.schema = merged;
if (!merged.elements.some((item) => item.id === state.selectedId)) {
state.selectedId = '';
}
}
function addElement(type: NativeElementType) {
const zIndex = Math.max(0, ...state.schema.elements.map((item) => item.zIndex)) + 1;
const dts = state.schema.dataBinding?.detailTables ?? [];
const firstKey = dts.length && dts[0]?.tableKey ? String(dts[0].tableKey) : '';
const defaultTableSource = type === 'table' || type === 'detailTable' ? firstKey || 'List1' : '';
const element = createElementByType(type, zIndex, defaultTableSource);
state.schema.elements.push(element);
state.selectedId = element.id;
}
function removeSelected() {
if (!state.selectedId) return;
state.schema.elements = state.schema.elements.filter((item) => item.id !== state.selectedId);
state.selectedId = '';
}
function updateElement(id: string, patch: Partial<NativeElement>) {
const target = state.schema.elements.find((item) => item.id === id);
if (!target) return;
Object.assign(target, patch);
}
function setSelected(id: string) {
state.selectedId = id;
}
/** 合并更新模板 dataBinding参数、明细字段树等 */
function patchDataBinding(patch: Partial<NonNullable<NativeTemplateSchema['dataBinding']>>) {
const cur = state.schema.dataBinding || {
fieldMap: {},
tableSources: ['mainTable', 'detailList'],
params: [],
detailTables: [],
};
state.schema.dataBinding = {
...cur,
...patch,
};
}
function duplicateSelected() {
const source = selectedElement.value;
if (!source) return;
const zIndex = Math.max(0, ...state.schema.elements.map((item) => item.zIndex)) + 1;
const copied = JSON.parse(JSON.stringify(source)) as NativeElement;
copied.id = uid(source.type);
copied.x += 6;
copied.y += 6;
copied.zIndex = zIndex;
state.schema.elements.push(copied);
state.selectedId = copied.id;
}
function bringForward() {
const target = selectedElement.value;
if (!target) return;
target.zIndex += 1;
}
function sendBackward() {
const target = selectedElement.value;
if (!target) return;
target.zIndex = Math.max(1, target.zIndex - 1);
}
function setScale(scale: number) {
state.scale = Math.min(2, Math.max(0.2, scale));
}
function serialize() {
return JSON.stringify(state.schema);
}
function deserialize(raw: string) {
const parsed = JSON.parse(raw || '{}') as NativeTemplateSchema;
if (parsed?.engine !== 'native' || !Array.isArray(parsed.elements) || !parsed.page) {
throw new Error('模板 JSON 不是原生设计器格式');
}
setSchema(parsed);
}
function pagePxSize() {
return {
width: state.schema.page.width * MM_TO_PX,
height: state.schema.page.height * MM_TO_PX,
};
}
return {
MM_TO_PX,
state,
selectedElement,
setSchema,
patchDataBinding,
addElement,
removeSelected,
updateElement,
setSelected,
duplicateSelected,
bringForward,
sendBackward,
setScale,
serialize,
deserialize,
pagePxSize,
};
}

View File

@@ -7,7 +7,12 @@ enum Api {
deleteOne = '/print/template/delete', deleteOne = '/print/template/delete',
deleteBatch = '/print/template/deleteBatch', deleteBatch = '/print/template/deleteBatch',
queryById = '/print/template/queryById', queryById = '/print/template/queryById',
queryByCode = '/print/template/queryByCode',
queryPrinters = '/print/template/queryPrinters',
directPrint = '/print/template/directPrint',
directPrintPdf = '/print/template/directPrintPdf',
saveJson = '/print/template/saveJson', saveJson = '/print/template/saveJson',
analyzeImageForNative = '/print/template/analyzeImageForNative',
} }
export const list = (params) => defHttp.get({ url: Api.list, params }); export const list = (params) => defHttp.get({ url: Api.list, params });
@@ -28,7 +33,52 @@ export const batchDelete = (params, handleSuccess?) => {
}); });
}; };
export const queryById = (id: string) => defHttp.get({ url: Api.queryById, params: { id } }); export const queryById = (id: string) => defHttp.get({ url: Api.queryById, params: { id, _t: Date.now() } });
export const queryByCode = (code: string) => defHttp.get({ url: Api.queryByCode, params: { code, _t: Date.now() } });
export const queryPrinters = () => defHttp.get({ url: Api.queryPrinters });
/** 服务端直打,队列较慢时适当放宽 */
export const directPrint = (data: { templateCode: string; printerName?: string; dataJson: any }) =>
defHttp.post({ url: Api.directPrint, data, timeout: 60 * 1000 });
/** 上传 Base64 PDF + 后端渲染打印,耗时明显长于普通接口 */
export const directPrintPdf = (data: { templateCode: string; printerName?: string; dataJson: any; pdfBase64: string; fileName?: string }) =>
defHttp.post({ url: Api.directPrintPdf, data, timeout: 3 * 60 * 1000 });
export const saveJson = (data: { id: string; templateJson: string }) => export const saveJson = (data: { id: string; templateJson: string }) =>
defHttp.post({ url: Api.saveJson, data }, { successMessageMode: 'message' }); defHttp.post({ url: Api.saveJson, data }, { successMessageMode: 'message' });
/** 上传图片由后台生成原生模板 JSONBase64避免 FormData 与签名拦截问题) */
export const analyzeImageForNative = (data: { imageBase64: string; filename?: string; mime?: string }) =>
defHttp.post<{
templateJson: string;
mockDataJson: string;
aiUsed: boolean;
hint?: string;
}>({ url: Api.analyzeImageForNative, data, timeout: 120000 }, { successMessageMode: 'none' });
/** 读取本地图片为 Data URL 后调用 analyzeImageForNative */
export function analyzeImageForNativeFile(file: File): Promise<{
templateJson: string;
mockDataJson: string;
aiUsed: boolean;
hint?: string;
}> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async () => {
try {
const imageBase64 = String(reader.result || '');
const res = await analyzeImageForNative({
imageBase64,
filename: file.name,
mime: file.type || undefined,
});
resolve(res);
} catch (e) {
reject(e);
}
};
reader.onerror = () => reject(new Error('读取图片失败'));
reader.readAsDataURL(file);
});
}

View File

@@ -1,5 +1,52 @@
import { BasicColumn, FormSchema } from '/@/components/Table'; import { BasicColumn, FormSchema } from '/@/components/Table';
export const PRINT_TEMPLATE_CATEGORY_DICT = 'print_template_category';
export const PRINT_PAPER_PRESET_DICT = 'print_paper_preset';
export const CATEGORY_OPTIONS = [
{ label: '条码', value: 'barcode' },
{ label: '标签', value: 'label' },
{ label: '快递面单', value: 'waybill' },
{ label: '吊牌', value: 'hangtag' },
{ label: '物料卡', value: 'materialCard' },
{ label: '箱唛', value: 'cartonMark' },
{ label: '质检单', value: 'qc' },
{ label: '入库单', value: 'inbound' },
{ label: '出库单', value: 'outbound' },
{ label: '工单', value: 'workOrder' },
{ label: '表单套打', value: 'form' },
{ label: '报表', value: 'report' },
];
export const CATEGORY_LABEL_MAP = CATEGORY_OPTIONS.reduce<Record<string, string>>((acc, item) => {
acc[item.value] = item.label;
return acc;
}, {});
export const PAPER_PRESET_MAP: Record<
string,
{ width: number; height: number; orientation: 'portrait' | 'landscape' }
> = {
A4: { width: 210, height: 297, orientation: 'portrait' },
A5: { width: 148, height: 210, orientation: 'portrait' },
A6: { width: 105, height: 148, orientation: 'portrait' },
B5: { width: 176, height: 250, orientation: 'portrait' },
B6: { width: 125, height: 176, orientation: 'portrait' },
L_10_12: { width: 10, height: 12, orientation: 'portrait' },
L_20_10: { width: 20, height: 10, orientation: 'landscape' },
L_25_15: { width: 25, height: 15, orientation: 'landscape' },
L_30_20: { width: 30, height: 20, orientation: 'landscape' },
L_40_30: { width: 40, height: 30, orientation: 'landscape' },
L_50_30: { width: 50, height: 30, orientation: 'landscape' },
L_60_40: { width: 60, height: 40, orientation: 'landscape' },
L_70_50: { width: 70, height: 50, orientation: 'landscape' },
L_80_50: { width: 80, height: 50, orientation: 'landscape' },
L_90_60: { width: 90, height: 60, orientation: 'landscape' },
L_100_70: { width: 100, height: 70, orientation: 'landscape' },
L_100_150: { width: 100, height: 150, orientation: 'portrait' },
L_100_180: { width: 100, height: 180, orientation: 'portrait' },
};
export const columns: BasicColumn[] = [ export const columns: BasicColumn[] = [
{ title: '模板编码', dataIndex: 'templateCode', width: 140 }, { title: '模板编码', dataIndex: 'templateCode', width: 140 },
{ title: '模板名称', dataIndex: 'templateName', width: 180 }, { title: '模板名称', dataIndex: 'templateName', width: 180 },
@@ -7,10 +54,7 @@ export const columns: BasicColumn[] = [
title: '分类', title: '分类',
dataIndex: 'category', dataIndex: 'category',
width: 100, width: 100,
customRender: ({ text }) => { customRender: ({ text }) => CATEGORY_LABEL_MAP[text] || text,
const m = { barcode: '条码', form: '表单套打', report: '报表' };
return m[text] || text;
},
}, },
{ {
title: '纸张(mm)', title: '纸张(mm)',
@@ -31,13 +75,9 @@ export const searchFormSchema: FormSchema[] = [
{ {
label: '分类', label: '分类',
field: 'category', field: 'category',
component: 'Select', component: 'JDictSelectTag',
componentProps: { componentProps: {
options: [ dictCode: PRINT_TEMPLATE_CATEGORY_DICT,
{ label: '条码', value: 'barcode' },
{ label: '表单套打', value: 'form' },
{ label: '报表', value: 'report' },
],
allowClear: true, allowClear: true,
}, },
colProps: { span: 6 }, colProps: { span: 6 },
@@ -62,17 +102,33 @@ export const formSchema: FormSchema[] = [
{ {
label: '分类', label: '分类',
field: 'category', field: 'category',
component: 'Select', component: 'JDictSelectTag',
defaultValue: 'form', defaultValue: 'form',
componentProps: { componentProps: {
options: [ dictCode: PRINT_TEMPLATE_CATEGORY_DICT,
{ label: '条码', value: 'barcode' },
{ label: '表单套打', value: 'form' },
{ label: '报表', value: 'report' },
],
}, },
required: true, required: true,
}, },
{
label: '纸张规格',
field: 'paperPreset',
component: 'JDictSelectTag',
defaultValue: 'A4',
componentProps: ({ formModel }) => ({
dictCode: PRINT_PAPER_PRESET_DICT,
allowClear: true,
placeholder: '选择预设规格或自定义',
onChange: (value: string) => {
const preset = PAPER_PRESET_MAP[String(value || '')];
if (!preset) {
return;
}
formModel.paperWidthMm = preset.width;
formModel.paperHeightMm = preset.height;
formModel.paperOrientation = preset.orientation;
},
}),
},
{ {
label: '纸宽(mm)', label: '纸宽(mm)',
field: 'paperWidthMm', field: 'paperWidthMm',

View File

@@ -0,0 +1,2 @@
/** 快速打印「设计器同源预览」写入 sessionStorage 的键PrintDesigner 与列表页共用,勿随意改名) */
export const QUICK_PRINT_PREVIEW_STORAGE_KEY = 'qhmes_quick_print_preview_v1';

10155
replay_pid6216.log Normal file

File diff suppressed because one or more lines are too long