├── res ├── Print_log.png ├── URLScheme.png ├── URLScheme_tips.png ├── electron-hiprint.png ├── electron-hiprint_set.png ├── electron-hiprint_set_transit.png ├── electron-hiprint_set_pluginVersion.png ├── electron-hiprint_LAN_network_topology.png └── electron-hiprint_transit_WAN_network_topology.png ├── assets ├── icons │ └── tray.png ├── element-ui │ └── fonts │ │ ├── element-icons.ttf │ │ └── element-icons.woff ├── loading.html ├── print.html ├── css │ ├── style.css │ └── print-lock.css ├── index.html ├── render.html ├── js │ └── dayjs.min.js ├── printLog.html └── set.html ├── .npmrc ├── .gitignore ├── .prettierrc.json ├── src ├── helper.js ├── pdf-print.js ├── printLog.js ├── set.js ├── render.js └── print.js ├── start.js ├── installer.nsh ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── release.yml ├── LICENSE ├── tools ├── database.js ├── rename.js └── code_compress.js ├── package.json ├── plugin ├── 0.0.52_print-lock.css ├── 0.0.54-fix_print-lock.css ├── 0.0.56_print-lock.css └── 0.0.60_print-lock.css ├── main.js └── README.md /res/Print_log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CcSimple/electron-hiprint/HEAD/res/Print_log.png -------------------------------------------------------------------------------- /res/URLScheme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CcSimple/electron-hiprint/HEAD/res/URLScheme.png -------------------------------------------------------------------------------- /assets/icons/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CcSimple/electron-hiprint/HEAD/assets/icons/tray.png -------------------------------------------------------------------------------- /res/URLScheme_tips.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CcSimple/electron-hiprint/HEAD/res/URLScheme_tips.png -------------------------------------------------------------------------------- /res/electron-hiprint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CcSimple/electron-hiprint/HEAD/res/electron-hiprint.png -------------------------------------------------------------------------------- /res/electron-hiprint_set.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CcSimple/electron-hiprint/HEAD/res/electron-hiprint_set.png -------------------------------------------------------------------------------- /res/electron-hiprint_set_transit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CcSimple/electron-hiprint/HEAD/res/electron-hiprint_set_transit.png -------------------------------------------------------------------------------- /assets/element-ui/fonts/element-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CcSimple/electron-hiprint/HEAD/assets/element-ui/fonts/element-icons.ttf -------------------------------------------------------------------------------- /assets/element-ui/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CcSimple/electron-hiprint/HEAD/assets/element-ui/fonts/element-icons.woff -------------------------------------------------------------------------------- /res/electron-hiprint_set_pluginVersion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CcSimple/electron-hiprint/HEAD/res/electron-hiprint_set_pluginVersion.png -------------------------------------------------------------------------------- /res/electron-hiprint_LAN_network_topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CcSimple/electron-hiprint/HEAD/res/electron-hiprint_LAN_network_topology.png -------------------------------------------------------------------------------- /res/electron-hiprint_transit_WAN_network_topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CcSimple/electron-hiprint/HEAD/res/electron-hiprint_transit_WAN_network_topology.png -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # 使用taobao镜像: electron 下载超时问题 2 | electron_mirror=https://cdn.npmmirror.com/binaries/electron/ 3 | electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | yarn-error.log 6 | 7 | # Editor directories and files 8 | .idea 9 | *.suo 10 | *.ntvs* 11 | *.njsproj 12 | *.sln 13 | 14 | .babelrc 15 | .editorconfig 16 | package-lock.json 17 | 18 | run/ 19 | out/ 20 | .history/ 21 | tools/database.sqlite 22 | /build-info.json 23 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "semi": true, 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "singleQuote": false, 7 | "singleAttributePerLine": true, 8 | "trailingComma": "all", 9 | "bracketSpacing": true, 10 | "bracketSameLine": false, 11 | "quoteProps": "as-needed", 12 | "arrowParens": "always", 13 | "endOfLine": "lf" 14 | } 15 | -------------------------------------------------------------------------------- /src/helper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { app } = require("electron"); 3 | 4 | /** 5 | * 退出应用 6 | * 7 | * @return {undefined} 8 | */ 9 | exports.appQuit = function() { 10 | console.log("==> Electron-hiprint 关闭 <=="); 11 | SET_WINDOW && SET_WINDOW.destroy(); 12 | PRINT_WINDOW && PRINT_WINDOW.destroy(); 13 | MAIN_WINDOW && MAIN_WINDOW.destroy(); 14 | APP_TRAY && APP_TRAY.destroy(); 15 | app.quit(); 16 | }; 17 | -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process'); 2 | const os = require('os'); 3 | 4 | const isWindows = os.platform() === 'win32'; 5 | 6 | // Windows下使用chcp 65001设置编码,其他平台直接运行electron 7 | const command = isWindows ? 'chcp' : 'electron'; 8 | const args = isWindows ? ['65001', '&&', 'electron', '.'] : ['.']; 9 | 10 | const electronProcess = spawn(command, args, { 11 | stdio: 'inherit', // 继承父进程的stdio配置 12 | shell: true // 使用shell来解析命令 13 | }); 14 | 15 | electronProcess.on('close', (code) => { 16 | console.log(`Electron process exited with code ${code}`); 17 | }); -------------------------------------------------------------------------------- /installer.nsh: -------------------------------------------------------------------------------- 1 | !macro customInstall 2 | ; 如果安装包路径下存在 config.json 文件,则将其复制到用户的 AppData 目录下 3 | IfFileExists "$EXEDIR\config.json" 0 +3 4 | CreateDirectory "$APPDATA\electron-hiprint" 5 | CopyFiles "$EXEDIR\config.json" "$APPDATA\electron-hiprint\config.json" 6 | ; 删除旧的 hiprint 伪协议 7 | DeleteRegKey HKCR "hiprint" 8 | ; 注册 hiprint 伪协议 9 | WriteRegStr HKCR "hiprint" "" "URL:hiprint" 10 | WriteRegStr HKCR "hiprint" "URL Protocol" "" 11 | WriteRegStr HKCR "hiprint\shell" "" "" 12 | WriteRegStr HKCR "hiprint\shell\Open" "" "" 13 | WriteRegStr HKCR "hiprint\shell\Open\command" "" "$INSTDIR\${APP_EXECUTABLE_FILENAME} %1" 14 | !macroend 15 | 16 | !macro customUnInstall 17 | ; 询问用户是否需要清除本地缓存数据 18 | MessageBox MB_YESNO|MB_ICONQUESTION "是否同时删除本地缓存数据?$\n这将清除所有设置和历史记录。" IDNO SkipDataDeletion 19 | RMDir /r "$APPDATA\electron-hiprint" 20 | SkipDataDeletion: 21 | ; 删除 hiprint 伪协议 22 | DeleteRegKey HKCR "hiprint" 23 | !macroend -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 CcSimple 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tools/database.js: -------------------------------------------------------------------------------- 1 | const { app } = require("electron"); 2 | const sqlite3 = require("sqlite3").verbose(); 3 | const path = require("path"); 4 | 5 | // 创建或打开数据库 6 | let dbPath = path.join(__dirname, "database.sqlite"); 7 | if (app.isPackaged) { 8 | dbPath = path.join(app.getAppPath(), "../", "database.sqlite"); 9 | } 10 | const db = new sqlite3.Database(dbPath, (err) => { 11 | if (err) { 12 | console.error("Could not connect to database", err); 13 | } else { 14 | console.log("Connected to database"); 15 | } 16 | }); 17 | 18 | // 创建打印日志记录表 19 | db.serialize(() => { 20 | db.run(` 21 | CREATE TABLE IF NOT EXISTS print_logs ( 22 | id INTEGER PRIMARY KEY AUTOINCREMENT, 23 | timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, 24 | socketId TEXT, 25 | clientType TEXT, 26 | printer TEXT, 27 | templateId TEXT, 28 | data TEXT, 29 | pageNum INTEGER, 30 | status TEXT, 31 | errorMessage TEXT 32 | ) 33 | `); 34 | 35 | // 添加新的可选字段 rePrintAble,默认值为 1 36 | db.run( 37 | ` 38 | ALTER TABLE print_logs ADD COLUMN rePrintAble INTEGER DEFAULT 1; 39 | `, 40 | (err) => { 41 | if (err && !err.message.includes("duplicate column")) { 42 | console.error("添加新字段时出错:", err); 43 | } 44 | }, 45 | ); 46 | }); 47 | 48 | module.exports = db; 49 | -------------------------------------------------------------------------------- /tools/rename.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const fs = require("fs"); 5 | 6 | class ReName { 7 | constructor() { 8 | this.basePath = path.normalize(__dirname + "/.."); 9 | this.dirs = path.join(this.basePath, "/out/"); 10 | } 11 | /** 12 | * 格式化参数 13 | */ 14 | formatArgvs() { 15 | let argv = {}; 16 | for (let i = 0; i < process.argv.length; i++) { 17 | const tmpArgv = process.argv[i]; 18 | if (tmpArgv.indexOf("--") !== -1) { 19 | let key = tmpArgv.substring(2); 20 | let val = process.argv[i + 1]; 21 | argv[key] = val; 22 | } 23 | } 24 | return argv; 25 | } 26 | 27 | rename(args) { 28 | let that = this; 29 | const pkgPath = path.join(that.basePath, "/package.json"); 30 | let pkg = JSON.parse(fs.readFileSync(pkgPath)); 31 | let version = pkg.version; 32 | let productName = pkg.build.productName; 33 | let fileName = `${productName}-${version}`; 34 | let extList = [".exe", ".dmg", ".tar.xz", ".deb"]; 35 | extList.forEach((e) => { 36 | let file = path.join(that.dirs, `${fileName}${e}`); 37 | let nFile = path.join( 38 | that.dirs, 39 | `${productName}_${args["tag"]}-${version}${e}`, 40 | ); 41 | if (fs.existsSync(file)) { 42 | console.log("exist ", file); 43 | console.log("rename ", nFile); 44 | fs.renameSync(file, nFile); 45 | } 46 | }); 47 | } 48 | } 49 | 50 | const r = new ReName(); 51 | let argvs = r.formatArgvs(); 52 | console.log("[electron] [rename] argvs:", argvs); 53 | r.rename(argvs); 54 | 55 | module.exports = ReName; 56 | -------------------------------------------------------------------------------- /assets/loading.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 92 | 93 | 94 | 95 |
96 | 97 | 98 | 99 | 100 | 101 | 102 |
103 | 104 | 105 | -------------------------------------------------------------------------------- /assets/print.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 打印窗口 13 | 14 | 15 | 21 | 63 | 64 | 65 | 66 |
67 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /assets/css/style.css: -------------------------------------------------------------------------------- 1 | /*定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/ 2 | ::-webkit-scrollbar { 3 | width: 3px; 4 | height: 1px; 5 | background-color: #004e8f; 6 | } 7 | /*定义滚动条轨道 内阴影+圆角*/ 8 | ::-webkit-scrollbar-track { 9 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 10 | border-radius: 3px; 11 | background-color: #004e8f; 12 | } 13 | /*定义滑块 内阴影+圆角*/ 14 | ::-webkit-scrollbar-thumb { 15 | border-radius: 3px; 16 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 17 | background-color: #004e8f; 18 | } 19 | 20 | body { 21 | padding: 0; 22 | margin: 0; 23 | --primary: #409eff; 24 | --success: #67c23a; 25 | --warning: #e6a23c; 26 | user-select: none; 27 | } 28 | 29 | .box { 30 | width: 100vw; 31 | height: 100vh; 32 | position: relative; 33 | overflow: hidden; 34 | } 35 | 36 | [class^="bg-"] { 37 | width: 0; 38 | height: 0; 39 | position: absolute; 40 | left: 250px; 41 | top: 300px; 42 | border-radius: 50%; 43 | z-index: -1; 44 | transition: all 0.6s; 45 | -moz-transition: all 0.6s; /* Firefox 4 */ 46 | -webkit-transition: all 0.6s; /* Safari 和 Chrome */ 47 | -o-transition: all 0.6s; /* Opera */ 48 | } 49 | 50 | [class^="bg-"].active { 51 | width: 800px; 52 | height: 800px; 53 | left: -150px; 54 | top: -100px; 55 | } 56 | 57 | .bg-primary { 58 | background-color: var(--primary); 59 | } 60 | 61 | .bg-success { 62 | background-color: var(--success); 63 | } 64 | 65 | .bg-warning { 66 | background-color: var(--warning); 67 | } 68 | 69 | .container { 70 | width: 100%; 71 | height: 100%; 72 | box-sizing: border-box; 73 | padding: 22px; 74 | display: flex; 75 | flex-direction: column; 76 | justify-content: space-between; 77 | color: #ffffff; 78 | z-index: 1; 79 | } 80 | 81 | .info, 82 | .status { 83 | display: flex; 84 | flex-direction: row; 85 | flex-wrap: wrap; 86 | justify-content: space-between; 87 | } 88 | 89 | .title { 90 | display: flex; 91 | width: 100%; 92 | font-size: 50px; 93 | align-items: center; 94 | } 95 | 96 | .privateIcon, 97 | .setIcon { 98 | position: relative; 99 | cursor: pointer; 100 | font-size: 32px; 101 | line-height: 50px; 102 | margin-left: 10px; 103 | } 104 | 105 | .privateIcon.hidden::after { 106 | position: absolute; 107 | content: "/"; 108 | left: calc(50% - 8px); 109 | } 110 | 111 | .setIcon { 112 | display: none !important; 113 | animation: rotatiing 2s linear infinite; 114 | -webkit-animation: rotatiing 2s linear infinite; 115 | } 116 | 117 | .title:hover > .setIcon, 118 | .title:hover > .privateIcon { 119 | display: inline-block !important; 120 | } 121 | 122 | .message { 123 | font-size: 12px; 124 | margin-bottom: 10px; 125 | } 126 | 127 | .row { 128 | width: 100%; 129 | flex: unset; 130 | } 131 | 132 | .ipAddress { 133 | font-size: 18px; 134 | } 135 | 136 | @keyframes rotatiing { 137 | 0% { 138 | transform: rotate(0deg); 139 | } 140 | 100% { 141 | transform: rotate(360deg); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-hiprint", 3 | "version": "1.0.14-beta6", 4 | "description": "vue-plugin-hiprint client", 5 | "main": "main.js", 6 | "author": "CcSimple<840054486@qq.com>", 7 | "license": "MIT", 8 | "homepage": "https://gitee.com/CcSimple", 9 | "scripts": { 10 | "start": "node ./build/write-build-info.js && node start.js", 11 | "postinstall": "npm run fix-sqlite3 && node ./build/write-build-info.js", 12 | "fix-sqlite3": "node ./build/fixSqlite3bug.js", 13 | "build-w": "electron-builder -w nsis:ia32 && node ./tools/rename --tag win_x32", 14 | "build-w-64": "electron-builder -w nsis:x64 && node ./tools/rename --tag win_x64", 15 | "build-m": "electron-builder -m --x64 && node ./tools/rename --tag mac_x64", 16 | "build-m-arm64": "electron-builder -m --arm64 && node ./tools/rename --tag mac_arm64", 17 | "build-m-universal": "electron-builder -m --universal && node ./tools/rename --tag mac_universal", 18 | "build-l": "electron-builder -l tar.xz && node ./tools/rename --tag linux_64", 19 | "build-l-arm64": "electron-builder -l --arm64 && node ./tools/rename --tag linux_arm64", 20 | "build-kylin": "electron-builder && node ./tools/rename --tag Kylin_64", 21 | "compress": "node ./tools/code_compress --compress", 22 | "restore": "node ./tools/code_compress --restore", 23 | "build-all": "npm run build-w && npm run build-w-64 && npm run build-m && npm run build-m-arm64 && npm run build-m-universal && npm run build-l && npm run build-l-arm64", 24 | "releases": "npm run compress && npm run build-all && npm run restore" 25 | }, 26 | "build": { 27 | "productName": "hiprint", 28 | "appId": "com.simple.cc.hiprint", 29 | "copyright": "CcSimple<840054486@qq.com>", 30 | "directories": { 31 | "output": "out" 32 | }, 33 | "asar": true, 34 | "files": [ 35 | "**/*" 36 | ], 37 | "extraResources": [ 38 | "plugin/*" 39 | ], 40 | "electronDownload": { 41 | "mirror": "https://npmmirror.com/mirrors/electron/" 42 | }, 43 | "nsis": { 44 | "oneClick": false, 45 | "allowElevation": true, 46 | "allowToChangeInstallationDirectory": true, 47 | "installerIcon": "./build/icons/icon.ico", 48 | "uninstallerIcon": "./build/icons/icon.ico", 49 | "installerHeaderIcon": "./build/icons/icon.ico", 50 | "createDesktopShortcut": true, 51 | "createStartMenuShortcut": true, 52 | "shortcutName": "Hiprint", 53 | "include": "installer.nsh" 54 | }, 55 | "mac": { 56 | "icon": "./build/icons/icon.icns", 57 | "artifactName": "${productName}-${version}.${ext}", 58 | "target": [ 59 | "dmg" 60 | ], 61 | "identity": null 62 | }, 63 | "dmg": { 64 | "sign": false 65 | }, 66 | "win": { 67 | "icon": "./build/icons/256x256.png", 68 | "artifactName": "${productName}-${version}.${ext}", 69 | "requestedExecutionLevel": "highestAvailable", 70 | "target": [ 71 | { 72 | "target": "nsis" 73 | } 74 | ] 75 | }, 76 | "linux": { 77 | "icon": "./build/icons/256x256.png", 78 | "artifactName": "${productName}-${version}.${ext}", 79 | "target": [ 80 | "tar.xz", 81 | "deb" 82 | ] 83 | }, 84 | "protocols": [ 85 | { 86 | "name": "hiprint", 87 | "schemes": [ 88 | "hiprint" 89 | ] 90 | } 91 | ], 92 | "publish": null 93 | }, 94 | "dependencies": { 95 | "address": "^1.2.0", 96 | "bwip-js": "^4.5.1", 97 | "concurrent-tasks": "^1.0.7", 98 | "dayjs": "^1.11.10", 99 | "electron-log": "^5.4.2", 100 | "electron-store": "^8.1.0", 101 | "ipp": "^2.0.1", 102 | "jimp": "^1.6.0", 103 | "jquery": "^3.6.0", 104 | "jsbarcode": "^3.11.6", 105 | "node-machine-id": "^1.1.12", 106 | "nzh": "^1.0.14", 107 | "pdf-to-printer": "^5.6.0", 108 | "socket.io": "^3.1.0", 109 | "socket.io-client": "^3.1.0", 110 | "sqlite3": "5.1.7", 111 | "unix-print": "^1.2.0", 112 | "uuid": "^11.0.3", 113 | "win32-pdf-printer": "^4.0.1" 114 | }, 115 | "devDependencies": { 116 | "electron": "^17.0.0", 117 | "electron-builder": "23.0.6", 118 | "fs-extra": "^9.1.0", 119 | "prettier": "^1.16.4", 120 | "uglify-js": "^3.14.3" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Build 2 | 3 | on: 4 | push: 5 | # 仅匹配 1.2.3 或 1.2.3-beta1 这样的 tag,例如 1.0.0、2.3.4-beta2 6 | tags: 7 | - "[0-9]+.[0-9]+.[0-9]+" 8 | - "[0-9]+.[0-9]+.[0-9]+-beta[0-9]+" 9 | 10 | permissions: write-all 11 | 12 | concurrency: 13 | group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}" 14 | cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} 15 | 16 | jobs: 17 | check-version: 18 | name: 校验版本号 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: 拉取代码 22 | uses: actions/checkout@v4 23 | 24 | - name: 校验 package.json version 与 tag 一致 25 | run: | 26 | TAG_NAME="${GITHUB_REF##refs/tags/}" 27 | PKG_VERSION=$(jq -r .version package.json) 28 | if [[ "$PKG_VERSION" != "$TAG_NAME" ]]; then 29 | echo "❌ package.json 里的 version ($PKG_VERSION) 和 tag ($TAG_NAME) 不一致,终止执行。" 30 | exit 1 31 | fi 32 | echo "✅ package.json 里的 version 和 tag ($TAG_NAME) 一致,继续执行。" 33 | 34 | build: 35 | name: 打包 36 | needs: check-version 37 | strategy: 38 | fail-fast: false 39 | matrix: 40 | include: 41 | - os: windows-latest 42 | script: build-w-64 43 | name-suffix: win_x64 44 | - os: windows-latest 45 | script: build-w 46 | name-suffix: win_ia32 47 | - os: macos-latest 48 | script: build-m-arm64 49 | name-suffix: mac_arm64 50 | - os: macos-latest 51 | script: build-m 52 | name-suffix: mac_x64 53 | - os: macos-latest 54 | script: build-m-universal 55 | name-suffix: mac_universal 56 | - os: ubuntu-latest 57 | script: build-l 58 | name-suffix: linux_x64 59 | - os: ubuntu-latest 60 | script: build-l-arm64 61 | name-suffix: linux_arm64 62 | 63 | runs-on: ${{ matrix.os }} 64 | steps: 65 | - name: 拉取代码 66 | uses: actions/checkout@v4 67 | 68 | - name: 设置 Node.js 69 | uses: actions/setup-node@v4 70 | with: 71 | node-version: "18" 72 | 73 | - name: 安装依赖 74 | run: npm install 75 | 76 | - name: 打包 ${{ matrix.name-suffix }} 77 | run: npm run ${{ matrix.script }} 78 | env: 79 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 80 | 81 | - name: 上传构建产物 82 | uses: actions/upload-artifact@v4 83 | with: 84 | name: ${{ matrix.name-suffix }} 85 | path: | 86 | out/*.exe 87 | out/*.dmg 88 | out/*.tar.xz 89 | out/*.deb 90 | retention-days: 7 91 | 92 | release: 93 | name: 自动发布 Release 94 | needs: build 95 | runs-on: ubuntu-latest 96 | steps: 97 | - name: 获取当前日期 98 | run: echo "RELEASE_DATE=$(date +'%Y-%m-%d')" >> $GITHUB_ENV 99 | 100 | - name: 下载 win64 产物 101 | uses: actions/download-artifact@v4 102 | with: 103 | name: win_x64 104 | path: artifacts/win 105 | 106 | - name: 下载 win32 产物 107 | uses: actions/download-artifact@v4 108 | with: 109 | name: win_ia32 110 | path: artifacts/win 111 | 112 | - name: 下载 mac ARM64 产物 113 | uses: actions/download-artifact@v4 114 | with: 115 | name: mac_arm64 116 | path: artifacts/mac 117 | 118 | - name: 下载 mac x64 产物 119 | uses: actions/download-artifact@v4 120 | with: 121 | name: mac_x64 122 | path: artifacts/mac 123 | 124 | - name: 下载 mac Universal 产物 125 | uses: actions/download-artifact@v4 126 | with: 127 | name: mac_universal 128 | path: artifacts/mac 129 | 130 | - name: 下载 linux x64 产物 131 | uses: actions/download-artifact@v4 132 | with: 133 | name: linux_x64 134 | path: artifacts/linux 135 | 136 | - name: 下载 linux ARM64 产物 137 | uses: actions/download-artifact@v4 138 | with: 139 | name: linux_arm64 140 | path: artifacts/linux 141 | 142 | - name: 发布 Release 143 | uses: softprops/action-gh-release@v2 144 | with: 145 | tag_name: ${{ github.ref_name }} 146 | name: ${{ github.ref_name }} (${{ env.RELEASE_DATE }}) 147 | draft: true 148 | generate_release_notes: true 149 | body: | 150 | Github Actions 自动发布的 Release 151 | 152 | > [!IMPORTANT] 153 | > macOS 提示应用程序已经损坏,解决方案 154 | > 155 | > [文件提示损坏解决办法 macwk.cn](https://macwk.cn/1144.html) 156 | > `sudo xattr -r -d com.apple.quarantine /应用程序路径/hiprint.app` 157 | files: | 158 | artifacts/win/** 159 | artifacts/mac/** 160 | artifacts/linux/** 161 | prerelease: ${{ contains(github.ref_name, 'beta') }} 162 | env: 163 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 164 | -------------------------------------------------------------------------------- /src/pdf-print.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: pdf打印 3 | * @Author: CcSimple 4 | * @Github: https://github.com/CcSimple 5 | * @Date: 2023-04-21 16:35:07 6 | * @LastEditors: JZT.吴健 7 | * @LastEditTime: 2025-09-26 14:10:48 8 | */ 9 | const pdfPrint1 = require("pdf-to-printer"); 10 | const pdfPrint2 = require("unix-print"); 11 | const path = require("path"); 12 | const fs = require("fs"); 13 | const os = require("os"); 14 | const { store } = require("../tools/utils"); 15 | const dayjs = require("dayjs"); 16 | const { v7: uuidv7 } = require("uuid"); 17 | 18 | const printPdfFunction = 19 | process.platform === "win32" ? pdfPrint1.print : pdfPrint2.print; 20 | 21 | const randomStr = () => { 22 | return Math.random() 23 | .toString(36) 24 | .substring(2); 25 | }; 26 | 27 | const realPrint = (pdfPath, printer, data, resolve, reject) => { 28 | if (!fs.existsSync(pdfPath)) { 29 | reject({ path: pdfPath, msg: "file not found" }); 30 | return; 31 | } 32 | 33 | if (process.platform === "win32") { 34 | data = Object.assign({}, data); 35 | data.printer = printer; 36 | console.log("print pdf:" + pdfPath + JSON.stringify(data)); 37 | // 参数见 node_modules/pdf-to-printer/dist/print/print.d.ts 38 | // pdf打印文档:https://www.sumatrapdfreader.org/docs/Command-line-arguments 39 | // pdf-to-printer 源码: https://github.com/artiebits/pdf-to-printer 40 | let pdfOptions = Object.assign(data, { paperSize: data.paperName }); 41 | printPdfFunction(pdfPath, pdfOptions) 42 | .then(() => { 43 | resolve(); 44 | }) 45 | .catch(() => { 46 | reject(); 47 | }); 48 | } else { 49 | // 参数见 lp 命令 使用方法, 使用外部传入的lp命令 50 | printPdfFunction(pdfPath, printer, data.unixPrintOptions || []) 51 | .then(() => { 52 | resolve(); 53 | }) 54 | .catch(() => { 55 | reject(); 56 | }); 57 | } 58 | }; 59 | 60 | const printPdf = (pdfPath, printer, data) => { 61 | return new Promise((resolve, reject) => { 62 | try { 63 | if (typeof pdfPath !== "string") { 64 | reject("pdfPath must be a string"); 65 | } 66 | if (/^https?:\/\/.+/.test(pdfPath)) { 67 | const client = pdfPath.startsWith("https") 68 | ? require("https") 69 | : require("http"); 70 | client 71 | .get(pdfPath, (res) => { 72 | const toSavePath = path.join( 73 | store.get("pdfPath") || os.tmpdir(), 74 | "url_pdf", 75 | dayjs().format(`YYYY_MM_DD HH_mm_ss_`) + `${uuidv7()}.pdf`, 76 | ); 77 | // 确保目录存在 78 | fs.mkdirSync(path.dirname(toSavePath), { recursive: true }); 79 | const file = fs.createWriteStream(toSavePath); 80 | res.pipe(file); 81 | file.on("finish", () => { 82 | file.close(); 83 | console.log("file downloaded:" + toSavePath); 84 | realPrint(toSavePath, printer, data, resolve, reject); 85 | }); 86 | }) 87 | .on("error", (err) => { 88 | console.log("download pdf error:" + err?.message); 89 | reject(err); 90 | }); 91 | return; 92 | } 93 | realPrint(pdfPath, printer, data, resolve, reject); 94 | } catch (error) { 95 | console.log("print error:" + error?.message); 96 | reject(error); 97 | } 98 | }); 99 | }; 100 | 101 | /** 102 | * @description: 打印Blob类型的PDF数据 103 | * @param {Blob|Uint8Array|Buffer} pdfBlob PDF的二进制数据 104 | * @param {string} printer 打印机名称 105 | * @param {object} data 打印参数 106 | * @return {Promise} 107 | */ 108 | const printPdfBlob = (pdfBlob, printer, data) => { 109 | return new Promise((resolve, reject) => { 110 | try { 111 | // 验证blob数据 实际是 Uint8Array 112 | if ( 113 | !pdfBlob || 114 | !( 115 | pdfBlob instanceof Uint8Array || Buffer.isBuffer(pdfBlob)) 116 | ) { 117 | reject(new Error("pdfBlob must be a Uint8Array, Buffer")); 118 | return; 119 | } 120 | 121 | // 生成临时文件路径 122 | const toSavePath = path.join( 123 | store.get("pdfPath") || os.tmpdir(), 124 | "blob_pdf", 125 | dayjs().format(`YYYY_MM_DD HH_mm_ss_`) + `${uuidv7()}.pdf`, 126 | ); 127 | 128 | // 确保目录存在 129 | fs.mkdirSync(path.dirname(toSavePath), { recursive: true }); 130 | 131 | // Uint8Array 2 Buffer 132 | const buffer = Buffer.isBuffer(pdfBlob) ? pdfBlob : Buffer.from(pdfBlob); 133 | 134 | // 写入文件 135 | fs.writeFile(toSavePath, buffer, (err) => { 136 | if (err) { 137 | console.log("save blob pdf error:" + err?.message); 138 | reject(err); 139 | return; 140 | } 141 | 142 | console.log("blob pdf saved:" + toSavePath); 143 | 144 | // 调用打印函数 145 | realPrint(toSavePath, printer, data, resolve, reject); 146 | }); 147 | } catch (error) { 148 | console.log("print blob error:" + error?.message); 149 | reject(error); 150 | } 151 | }); 152 | }; 153 | 154 | module.exports = { 155 | printPdf, 156 | printPdfBlob, 157 | }; 158 | -------------------------------------------------------------------------------- /assets/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | Electron-hiprint 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |
26 |
27 |
28 |
29 |
30 |
31 | 打印服务已启动 32 | 38 | 43 |
44 |
45 | 服务地址:{{ privateData(ipAddress) }} 46 |
47 |
48 | MAC地址:{{ privateData(macAddress) }} 49 |
50 |
51 |
52 |
53 | 中转状态: {{ transitActiveFlag ? '已' : '未' }}连接 54 |
55 |
56 | 本地连接: 57 | 58 | {{ socketActiveNum ? `已建立 ${socketActiveNum} 条` : '未' }}连接 59 | 60 |
61 |
62 | 打印状态:{{ printing ? "文档打印中" : "空闲" }} 63 |
64 |
65 | 设备编号:{{ privateData(deviceId) }} 66 |
67 |
68 | 插件版本:{{ pluginVersion }} 69 |
70 |
71 | 客户端版本:{{ version }} 72 |
73 |
74 |
75 |
76 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /tools/code_compress.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const fs = require("fs"); 5 | const fsPro = require("fs-extra"); 6 | const UglifyJS = require("uglify-js"); 7 | 8 | class CodeCompress { 9 | constructor() { 10 | this.dirs = ["src"]; 11 | this.basePath = path.normalize(__dirname + "/.."); 12 | this.backupCodeDir = path.join(this.basePath, "run/backup_code"); 13 | if (!fs.existsSync(this.backupCodeDir)) { 14 | this.mkdir(this.backupCodeDir); 15 | this.chmodPath(this.backupCodeDir, "777"); 16 | } 17 | } 18 | 19 | /** 20 | * 备份 app、electron目录代码 21 | */ 22 | backup() { 23 | console.log("[electron] [code_compress] [backup] start"); 24 | this.rmBackup(); 25 | 26 | for (let i = 0; i < this.dirs.length; i++) { 27 | // check code dir 28 | let codeDirPath = path.join(this.basePath, this.dirs[i]); 29 | if (!fs.existsSync(codeDirPath)) { 30 | console.log( 31 | "[electron] [code_compress] [backup] ERROR: %s is not exist", 32 | codeDirPath 33 | ); 34 | return; 35 | } 36 | 37 | // copy 38 | let targetDir = path.join(this.backupCodeDir, this.dirs[i]); 39 | console.log("[electron] [code_compress] [backup] targetDir:", targetDir); 40 | if (!fs.existsSync(targetDir)) { 41 | this.mkdir(targetDir); 42 | this.chmodPath(targetDir, "777"); 43 | } 44 | 45 | fsPro.copySync(codeDirPath, targetDir); 46 | } 47 | console.log("[electron] [code_compress] [backup] success"); 48 | } 49 | 50 | /** 51 | * 还原代码 52 | */ 53 | restore() { 54 | console.log("[electron] [code_compress] [restore] start"); 55 | for (let i = 0; i < this.dirs.length; i++) { 56 | let codeDirPath = path.join(this.backupCodeDir, this.dirs[i]); 57 | let targetDir = path.join(this.basePath, this.dirs[i]); 58 | fsPro.copySync(codeDirPath, targetDir); 59 | } 60 | console.log("[electron] [code_compress] [restore] success"); 61 | } 62 | 63 | /** 64 | * 压缩代码 65 | */ 66 | compress() { 67 | console.log("[electron] [code_compress] [compress] start"); 68 | for (let i = 0; i < this.dirs.length; i++) { 69 | let codeDirPath = path.join(this.basePath, this.dirs[i]); 70 | this.compressLoop(codeDirPath); 71 | } 72 | console.log("[electron] [code_compress] [compress] success"); 73 | } 74 | 75 | compressLoop(dirPath) { 76 | let files = []; 77 | if (fs.existsSync(dirPath)) { 78 | files = fs.readdirSync(dirPath); 79 | files.forEach((file, index) => { 80 | let curPath = dirPath + "/" + file; 81 | if (fs.statSync(curPath).isDirectory()) { 82 | this.compressLoop(curPath); 83 | } else { 84 | if (path.extname(curPath) === ".js") { 85 | this.miniFile(curPath); 86 | } 87 | } 88 | }); 89 | } 90 | } 91 | 92 | miniFile(file) { 93 | let code = fs.readFileSync(file, "utf8"); 94 | const options = { 95 | mangle: { 96 | toplevel: false, 97 | }, 98 | }; 99 | 100 | let result = UglifyJS.minify(code, options); 101 | fs.writeFileSync(file, result.code, "utf8"); 102 | } 103 | 104 | /** 105 | * 格式化参数 106 | */ 107 | formatArgvs() { 108 | // argv 109 | let argvs = []; 110 | for (let i = 0; i < process.argv.length; i++) { 111 | const tmpArgv = process.argv[i]; 112 | if (tmpArgv.indexOf("--") !== -1) { 113 | argvs.push(tmpArgv.substr(2)); 114 | } 115 | } 116 | return argvs; 117 | } 118 | 119 | /** 120 | * 移除备份 121 | */ 122 | rmBackup() { 123 | fs.rmSync(this.backupCodeDir, { recursive: true }); 124 | return; 125 | } 126 | 127 | /** 128 | * 检查文件是否存在 129 | */ 130 | fileExist(filePath) { 131 | try { 132 | return fs.statSync(filePath).isFile(); 133 | } catch (err) { 134 | return false; 135 | } 136 | } 137 | 138 | mkdir(dirpath, dirname) { 139 | // 判断是否是第一次调用 140 | if (typeof dirname === "undefined") { 141 | if (fs.existsSync(dirpath)) { 142 | return; 143 | } 144 | this.mkdir(dirpath, path.dirname(dirpath)); 145 | } else { 146 | // 判断第二个参数是否正常,避免调用时传入错误参数 147 | if (dirname !== path.dirname(dirpath)) { 148 | this.mkdir(dirpath); 149 | return; 150 | } 151 | if (fs.existsSync(dirname)) { 152 | fs.mkdirSync(dirpath); 153 | } else { 154 | this.mkdir(dirname, path.dirname(dirname)); 155 | fs.mkdirSync(dirpath); 156 | } 157 | } 158 | } 159 | 160 | chmodPath(path, mode) { 161 | let files = []; 162 | if (fs.existsSync(path)) { 163 | files = fs.readdirSync(path); 164 | files.forEach((file, index) => { 165 | const curPath = path + "/" + file; 166 | if (fs.statSync(curPath).isDirectory()) { 167 | this.chmodPath(curPath, mode); // 递归删除文件夹 168 | } else { 169 | fs.chmodSync(curPath, mode); 170 | } 171 | }); 172 | fs.chmodSync(path, mode); 173 | } 174 | } 175 | } 176 | 177 | const cc = new CodeCompress(); 178 | let argvs = cc.formatArgvs(); 179 | console.log("[electron] [code_compress] argvs:", argvs); 180 | if (argvs.indexOf("compress") != -1) { 181 | cc.backup(); 182 | cc.compress(); 183 | } else if (argvs.indexOf("restore") != -1) { 184 | cc.restore(); 185 | } 186 | 187 | module.exports = CodeCompress; 188 | -------------------------------------------------------------------------------- /src/printLog.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Date: 2024-12-14 23:59:49 3 | * @LastEditors: admin@54xavier.cn 4 | * @LastEditTime: 2024-12-15 02:55:48 5 | * @FilePath: /electron-hiprint/src/printlog.js 6 | */ 7 | "use strict"; 8 | const { 9 | app, 10 | BrowserWindow, 11 | BrowserView, 12 | ipcMain, 13 | dialog, 14 | } = require("electron"); 15 | const dayjs = require("dayjs"); 16 | const path = require("path"); 17 | const db = require("../tools/database"); 18 | 19 | function createPrintLogWindow() { 20 | const windowOptions = { 21 | width: 1080, 22 | height: 600, 23 | minWidth: 1040, 24 | minHeight: 550, 25 | webPreferences: { 26 | nodeIntegration: true, 27 | contextIsolation: false, 28 | }, 29 | }; 30 | 31 | // 创建打印日志窗口 32 | PRINT_LOG_WINDOW = new BrowserWindow(windowOptions); 33 | 34 | // 添加加载页面 解决白屏的问题 35 | loadingView(windowOptions); 36 | 37 | // 加载打印日志页面 38 | const printLogHtml = path.join( 39 | "file://", 40 | app.getAppPath(), 41 | "/assets/printLog.html", 42 | ); 43 | PRINT_LOG_WINDOW.loadURL(printLogHtml); 44 | 45 | // 未打包时打开开发者工具 46 | if (!app.isPackaged) { 47 | PRINT_LOG_WINDOW.webContents.openDevTools(); 48 | } 49 | 50 | // 绑定窗口事件 51 | initPrintLogEvent(); 52 | 53 | // 监听退出,移除所有事件 54 | PRINT_LOG_WINDOW.on("closed", removePrintLogEvent); 55 | 56 | return PRINT_LOG_WINDOW; 57 | } 58 | 59 | /** 60 | * @description: 加载等待页面,解决主窗口白屏问题 61 | * @param {Object} windowOptions 主窗口配置 62 | * @return {void} 63 | */ 64 | function loadingView(windowOptions) { 65 | const loadingBrowserView = new BrowserView(); 66 | PRINT_LOG_WINDOW.setBrowserView(loadingBrowserView); 67 | loadingBrowserView.setBounds({ 68 | x: 0, 69 | y: 0, 70 | width: windowOptions.width, 71 | height: windowOptions.height, 72 | }); 73 | 74 | const loadingHtml = path.join( 75 | "file://", 76 | app.getAppPath(), 77 | "assets/loading.html", 78 | ); 79 | loadingBrowserView.webContents.loadURL(loadingHtml); 80 | 81 | // 打印日志窗口 dom 加载完毕,移除 loadingBrowserView 82 | PRINT_LOG_WINDOW.webContents.on("dom-ready", async (event) => { 83 | loadingBrowserView.webContents.destroy(); 84 | PRINT_LOG_WINDOW.removeBrowserView(loadingBrowserView); 85 | }); 86 | } 87 | 88 | /** 89 | * @description: 获取打印日志 90 | * @param {IpcMainEvent} event 事件 91 | * @param {Array} condition 搜索条件 92 | * @param {Array} params 搜索参数 93 | * @param {Object} page 分页 94 | * @param {Object} sort 排序 95 | * @param {Function} callback 回调函数 96 | * @return {void} 97 | */ 98 | function fetchPrintLogs(event, { condition, params, page, sort }) { 99 | const baseQuery = `SELECT id, timestamp, socketId, clientType, printer, templateId, pageNum, status, rePrintAble, errorMessage FROM print_logs`; 100 | const totalQuery = `SELECT COUNT(*) AS total FROM print_logs`; 101 | let query = baseQuery; 102 | let total = totalQuery; 103 | 104 | if (condition.length > 0) { 105 | query += " WHERE " + condition.join(" AND "); 106 | total += " WHERE " + condition.join(" AND "); 107 | } 108 | 109 | if (sort.prop && sort.order) { 110 | query += ` ORDER BY ${sort.prop} ${sort.order 111 | .replace("ending", "") 112 | .toUpperCase()}`; 113 | } 114 | 115 | query += ` LIMIT ${page.pageSize} OFFSET ${(page.currentPage - 1) * 116 | page.pageSize}`; 117 | 118 | function allAsync(query, params) { 119 | return new Promise((resolve, reject) => { 120 | db.all(query, params, (err, rows) => { 121 | if (err) return reject(err); 122 | rows.forEach((row) => { 123 | row.timestamp = dayjs(row.timestamp) 124 | .add(8, "hour") 125 | .format("YYYY-MM-DD HH:mm:ss"); 126 | }); 127 | resolve(rows); 128 | }); 129 | }); 130 | } 131 | 132 | Promise.all([allAsync(query, params), allAsync(total, params)]) 133 | .then(([rows, total]) => { 134 | event.sender.send("print-logs", { 135 | rows, 136 | total: total[0].total, 137 | }); 138 | }) 139 | .catch((err) => { 140 | dialog.showMessageBox(PRINT_LOG_WINDOW, { 141 | type: "error", 142 | title: "错误", 143 | message: "获取打印日志失败!", 144 | detail: err.message, 145 | noLink: true, 146 | }); 147 | }); 148 | } 149 | 150 | /** 151 | * @description: 清空打印日志 152 | * @param {IpcMainEvent} event 事件 153 | * @return {void} 154 | */ 155 | function clearPrintLogs(event) { 156 | db.run("DELETE FROM print_logs"); 157 | } 158 | 159 | /** 160 | * @description: 重打打印 161 | * @param {IpcMainEvent} event 事件 162 | * @param {Object} data 打印日志 163 | * @return {void} 164 | */ 165 | function rePrint(event, data) { 166 | db.get("SELECT * FROM print_logs WHERE id = ?", [data.id], (err, row) => { 167 | if (err) return; 168 | PRINT_WINDOW.webContents.send("reprint", { 169 | ...JSON.parse(row.data), 170 | taskId: undefined, 171 | replyId: undefined, 172 | clientType: "local", 173 | socketId: undefined, 174 | }); 175 | }); 176 | } 177 | 178 | /** 179 | * @description: 绑定打印日志窗口事件 180 | * @return {void} 181 | */ 182 | function initPrintLogEvent() { 183 | ipcMain.on("request-logs", fetchPrintLogs); 184 | ipcMain.on("reprint", rePrint); 185 | ipcMain.on("clear-logs", clearPrintLogs); 186 | } 187 | 188 | /** 189 | * @description: 移除所有事件 190 | * @return {void} 191 | */ 192 | function removePrintLogEvent() { 193 | ipcMain.removeListener("request-logs", fetchPrintLogs); 194 | ipcMain.removeListener("reprint", rePrint); 195 | ipcMain.removeListener("clear-logs", clearPrintLogs); 196 | PRINT_LOG_WINDOW = null; 197 | } 198 | 199 | module.exports = async () => { 200 | // 创建设置窗口 201 | await createPrintLogWindow(); 202 | }; 203 | -------------------------------------------------------------------------------- /assets/render.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 17 | 打印窗口 18 | 19 | 20 | 21 |
22 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /assets/js/dayjs.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).dayjs=e()}(this,(function(){"use strict";var t=1e3,e=6e4,n=36e5,r="millisecond",i="second",s="minute",u="hour",a="day",o="week",c="month",f="quarter",h="year",d="date",l="Invalid Date",$=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,y=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,M={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),ordinal:function(t){var e=["th","st","nd","rd"],n=t%100;return"["+t+(e[(n-20)%10]||e[n]||e[0])+"]"}},m=function(t,e,n){var r=String(t);return!r||r.length>=e?t:""+Array(e+1-r.length).join(n)+t},v={s:m,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),i=n%60;return(e<=0?"+":"-")+m(r,2,"0")+":"+m(i,2,"0")},m:function t(e,n){if(e.date()1)return t(u[0])}else{var a=e.name;D[a]=e,i=a}return!r&&i&&(g=i),i||!r&&g},O=function(t,e){if(S(t))return t.clone();var n="object"==typeof e?e:{};return n.date=t,n.args=arguments,new _(n)},b=v;b.l=w,b.i=S,b.w=function(t,e){return O(t,{locale:e.$L,utc:e.$u,x:e.$x,$offset:e.$offset})};var _=function(){function M(t){this.$L=w(t.locale,null,!0),this.parse(t),this.$x=this.$x||t.x||{},this[p]=!0}var m=M.prototype;return m.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(b.u(e))return new Date;if(e instanceof Date)return new Date(e);if("string"==typeof e&&!/Z$/i.test(e)){var r=e.match($);if(r){var i=r[2]-1||0,s=(r[7]||"0").substring(0,3);return n?new Date(Date.UTC(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)):new Date(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)}}return new Date(e)}(t),this.init()},m.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},m.$utils=function(){return b},m.isValid=function(){return!(this.$d.toString()===l)},m.isSame=function(t,e){var n=O(t);return this.startOf(e)<=n&&n<=this.endOf(e)},m.isAfter=function(t,e){return O(t) div { 237 | display: table-cell; 238 | vertical-align: middle 239 | } 240 | 241 | .hiprint-text-content-bottom { 242 | display: table; 243 | } 244 | 245 | .hiprint-text-content-bottom > div { 246 | display: table-cell; 247 | vertical-align: bottom 248 | } 249 | 250 | /*hi-grid-row */ 251 | .hi-grid-row { 252 | position: relative; 253 | height: auto; 254 | margin-right: 0; 255 | margin-left: 0; 256 | zoom: 1; 257 | display: block; 258 | box-sizing: border-box; 259 | } 260 | 261 | .hi-grid-row::after, .hi-grid-row::before { 262 | display: table; 263 | content: ''; 264 | box-sizing: border-box; 265 | } 266 | 267 | .hi-grid-col { 268 | display: block; 269 | box-sizing: border-box; 270 | position: relative; 271 | float: left; 272 | flex: 0 0 auto; 273 | } 274 | 275 | .table-grid-row { 276 | margin-left: -0pt; 277 | margin-right: -0pt; 278 | } 279 | 280 | .tableGridColumnsGutterRow { 281 | padding-left: 0pt; 282 | padding-right: 0pt; 283 | } 284 | 285 | .hiprint-gridColumnsFooter { 286 | text-align: left; 287 | clear: both; 288 | } 289 | -------------------------------------------------------------------------------- /plugin/0.0.54-fix_print-lock.css: -------------------------------------------------------------------------------- 1 | @media print { 2 | body { 3 | margin: 0px; 4 | padding: 0px; 5 | } 6 | } 7 | 8 | @page { 9 | margin: 0; 10 | } 11 | 12 | .hiprint-printPaper * { 13 | box-sizing: border-box; 14 | -moz-box-sizing: border-box; /* Firefox */ 15 | -webkit-box-sizing: border-box; /* Safari */ 16 | } 17 | 18 | .hiprint-printPaper *:focus { 19 | outline: -webkit-focus-ring-color auto 0px; 20 | } 21 | 22 | .hiprint-printPaper { 23 | position: relative; 24 | padding: 0 0 0 0; 25 | page-break-after: always; 26 | -webkit-user-select: none; /* Chrome/Safari/Opera */ 27 | -moz-user-select: none; /* Firefox */ 28 | user-select: none; 29 | overflow-x: hidden; 30 | overflow: hidden; 31 | } 32 | 33 | .hiprint-printPaper .hiprint-printPaper-content { 34 | position: relative; 35 | } 36 | 37 | /* 火狐浏览器打印 第一页过后 重叠问题 */ 38 | @-moz-document url-prefix() { 39 | .hiprint-printPaper .hiprint-printPaper-content { 40 | position: relative; 41 | margin-top: 20px; 42 | top: -20px 43 | } 44 | } 45 | 46 | .hiprint-printPaper.design { 47 | overflow: visible; 48 | } 49 | 50 | 51 | .hiprint-printTemplate .hiprint-printPanel { 52 | page-break-after: always; 53 | } 54 | 55 | .hiprint-printPaper, hiprint-printPanel { 56 | box-sizing: border-box; 57 | border: 0px; 58 | } 59 | 60 | .hiprint-printPanel .hiprint-printPaper:last-child { 61 | page-break-after: avoid; 62 | } 63 | 64 | .hiprint-printTemplate .hiprint-printPanel:last-child { 65 | page-break-after: avoid; 66 | } 67 | 68 | .hiprint-printPaper .hideheaderLinetarget { 69 | border-top: 0px dashed rgb(201, 190, 190) !important; 70 | } 71 | 72 | .hiprint-printPaper .hidefooterLinetarget { 73 | border-top: 0px dashed rgb(201, 190, 190) !important; 74 | } 75 | 76 | .hiprint-printPaper.design { 77 | border: 1px dashed rgba(170, 170, 170, 0.7); 78 | } 79 | 80 | .design .hiprint-printElement-table-content, .design .hiprint-printElement-longText-content { 81 | overflow: hidden; 82 | box-sizing: border-box; 83 | } 84 | 85 | .design .resize-panel { 86 | box-sizing: border-box; 87 | border: 1px dotted; 88 | } 89 | 90 | .hiprint-printElement-text { 91 | background-color: transparent; 92 | background-repeat: repeat; 93 | padding: 0 0 0 0; 94 | border: 0.75pt none rgb(0, 0, 0); 95 | direction: ltr; 96 | font-family: 'SimSun'; 97 | font-size: 9pt; 98 | font-style: normal; 99 | font-weight: normal; 100 | padding-bottom: 0pt; 101 | padding-left: 0pt; 102 | padding-right: 0pt; 103 | padding-top: 0pt; 104 | text-align: left; 105 | text-decoration: none; 106 | line-height: 9.75pt; 107 | box-sizing: border-box; 108 | word-wrap: break-word; 109 | word-break: break-all; 110 | } 111 | 112 | .design .hiprint-printElement-text-content { 113 | border: 1px dashed rgb(206, 188, 188); 114 | box-sizing: border-box; 115 | } 116 | 117 | .hiprint-printElement-longText { 118 | background-color: transparent; 119 | background-repeat: repeat; 120 | border: 0.75pt none rgb(0, 0, 0); 121 | direction: ltr; 122 | font-family: 'SimSun'; 123 | font-size: 9pt; 124 | font-style: normal; 125 | font-weight: normal; 126 | padding-bottom: 0pt; 127 | padding-left: 0pt; 128 | padding-right: 0pt; 129 | padding-top: 0pt; 130 | text-align: left; 131 | text-decoration: none; 132 | line-height: 9.75pt; 133 | box-sizing: border-box; 134 | word-wrap: break-word; 135 | word-break: break-all; 136 | /*white-space: pre-wrap*/ 137 | } 138 | 139 | 140 | .hiprint-printElement-table { 141 | background-color: transparent; 142 | background-repeat: repeat; 143 | color: rgb(0, 0, 0); 144 | border-color: rgb(0, 0, 0); 145 | border-style: none; 146 | direction: ltr; 147 | font-family: 'SimSun'; 148 | font-size: 9pt; 149 | font-style: normal; 150 | font-weight: normal; 151 | padding-bottom: 0pt; 152 | padding-left: 0pt; 153 | padding-right: 0pt; 154 | padding-top: 0pt; 155 | text-align: left; 156 | text-decoration: none; 157 | padding: 0 0 0 0; 158 | box-sizing: border-box; 159 | line-height: 9.75pt; 160 | } 161 | 162 | .hiprint-printElement-table thead { 163 | background: #e8e8e8; 164 | font-weight: 700; 165 | } 166 | 167 | .hiprint-printElement-tableTarget, .hiprint-printElement-tableTarget tr, .hiprint-printElement-tableTarget td { 168 | border-color: rgb(0, 0, 0); 169 | /*border-style: none;*/ 170 | /*border: 1px solid rgb(0, 0, 0);*/ 171 | font-weight: normal; 172 | direction: ltr; 173 | padding-bottom: 0pt; 174 | padding-left: 4pt; 175 | padding-right: 4pt; 176 | padding-top: 0pt; 177 | text-decoration: none; 178 | vertical-align: middle; 179 | box-sizing: border-box; 180 | word-wrap: break-word; 181 | word-break: break-all; 182 | /*line-height: 9.75pt; 183 | font-size: 9pt;*/ 184 | } 185 | 186 | .hiprint-printElement-tableTarget-border-all { 187 | border: 1px solid; 188 | } 189 | .hiprint-printElement-tableTarget-border-none { 190 | border: 0px solid; 191 | } 192 | .hiprint-printElement-tableTarget-border-lr { 193 | border-left: 1px solid; 194 | border-right: 1px solid; 195 | } 196 | .hiprint-printElement-tableTarget-border-left { 197 | border-left: 1px solid; 198 | } 199 | .hiprint-printElement-tableTarget-border-right { 200 | border-right: 1px solid; 201 | } 202 | .hiprint-printElement-tableTarget-border-tb { 203 | border-top: 1px solid; 204 | border-bottom: 1px solid; 205 | } 206 | .hiprint-printElement-tableTarget-border-top { 207 | border-top: 1px solid; 208 | } 209 | .hiprint-printElement-tableTarget-border-bottom { 210 | border-bottom: 1px solid; 211 | } 212 | 213 | .hiprint-printElement-tableTarget-border-td-none td { 214 | border: 0px solid; 215 | } 216 | .hiprint-printElement-tableTarget-border-td-all td:not(:last-child) { 217 | border-right: 1px solid; 218 | } 219 | 220 | /*.hiprint-printElement-tableTarget tr,*/ 221 | .hiprint-printElement-tableTarget td { 222 | height: 18pt; 223 | } 224 | 225 | .hiprint-printPaper .hiprint-paperNumber { 226 | font-size: 9pt; 227 | } 228 | 229 | .design .hiprint-printElement-table-handle { 230 | position: absolute; 231 | height: 21pt; 232 | width: 21pt; 233 | background: red; 234 | z-index: 1; 235 | } 236 | 237 | .hiprint-printPaper .hiprint-paperNumber-disabled { 238 | float: right !important; 239 | right: 0 !important; 240 | color: gainsboro !important; 241 | } 242 | 243 | .hiprint-printElement-vline, .hiprint-printElement-hline { 244 | border: 0px none rgb(0, 0, 0); 245 | 246 | } 247 | 248 | .hiprint-printElement-vline { 249 | border-left: 0.75pt solid #000; 250 | border-right: 0px none rgb(0, 0, 0) !important; 251 | border-bottom: 0px none rgb(0, 0, 0) !important; 252 | border-top: 0px none rgb(0, 0, 0) !important; 253 | } 254 | 255 | .hiprint-printElement-hline { 256 | border-top: 0.75pt solid #000; 257 | border-right: 0px none rgb(0, 0, 0) !important; 258 | border-bottom: 0px none rgb(0, 0, 0) !important; 259 | border-left: 0px none rgb(0, 0, 0) !important; 260 | } 261 | 262 | .hiprint-printElement-oval, .hiprint-printElement-rect { 263 | border: 0.75pt solid #000; 264 | } 265 | 266 | .hiprint-text-content-middle { 267 | } 268 | 269 | .hiprint-text-content-middle > div { 270 | display: grid; 271 | align-items: center; 272 | } 273 | 274 | .hiprint-text-content-bottom { 275 | } 276 | 277 | .hiprint-text-content-bottom > div { 278 | display: grid; 279 | align-items: flex-end; 280 | } 281 | 282 | .hiprint-text-content-wrap { 283 | } 284 | 285 | .hiprint-text-content-wrap .hiprint-text-content-wrap-nowrap { 286 | white-space: nowrap; 287 | } 288 | 289 | .hiprint-text-content-wrap .hiprint-text-content-wrap-clip { 290 | white-space: nowrap; 291 | overflow: hidden; 292 | text-overflow: clip; 293 | } 294 | 295 | .hiprint-text-content-wrap .hiprint-text-content-wrap-ellipsis { 296 | white-space: nowrap; 297 | overflow: hidden; 298 | text-overflow: ellipsis; 299 | } 300 | 301 | /*hi-grid-row */ 302 | .hi-grid-row { 303 | position: relative; 304 | height: auto; 305 | margin-right: 0; 306 | margin-left: 0; 307 | zoom: 1; 308 | display: block; 309 | box-sizing: border-box; 310 | } 311 | 312 | .hi-grid-row::after, .hi-grid-row::before { 313 | display: table; 314 | content: ''; 315 | box-sizing: border-box; 316 | } 317 | 318 | .hi-grid-col { 319 | display: block; 320 | box-sizing: border-box; 321 | position: relative; 322 | float: left; 323 | flex: 0 0 auto; 324 | } 325 | 326 | .table-grid-row { 327 | margin-left: -0pt; 328 | margin-right: -0pt; 329 | } 330 | 331 | .tableGridColumnsGutterRow { 332 | padding-left: 0pt; 333 | padding-right: 0pt; 334 | } 335 | 336 | .hiprint-gridColumnsFooter { 337 | text-align: left; 338 | clear: both; 339 | } 340 | -------------------------------------------------------------------------------- /plugin/0.0.56_print-lock.css: -------------------------------------------------------------------------------- 1 | @media print { 2 | body { 3 | margin: 0px; 4 | padding: 0px; 5 | } 6 | } 7 | 8 | @page { 9 | margin: 0; 10 | } 11 | 12 | .hiprint-printPaper * { 13 | box-sizing: border-box; 14 | -moz-box-sizing: border-box; /* Firefox */ 15 | -webkit-box-sizing: border-box; /* Safari */ 16 | } 17 | 18 | .hiprint-printPaper *:focus { 19 | outline: -webkit-focus-ring-color auto 0px; 20 | } 21 | 22 | .hiprint-printPaper { 23 | position: relative; 24 | padding: 0 0 0 0; 25 | page-break-after: always; 26 | -webkit-user-select: none; /* Chrome/Safari/Opera */ 27 | -moz-user-select: none; /* Firefox */ 28 | user-select: none; 29 | overflow-x: hidden; 30 | overflow: hidden; 31 | } 32 | 33 | .hiprint-printPaper .hiprint-printPaper-content { 34 | position: relative; 35 | } 36 | 37 | /* 火狐浏览器打印 第一页过后 重叠问题 */ 38 | @-moz-document url-prefix() { 39 | .hiprint-printPaper .hiprint-printPaper-content { 40 | position: relative; 41 | margin-top: 20px; 42 | top: -20px 43 | } 44 | } 45 | 46 | .hiprint-printPaper.design { 47 | overflow: visible; 48 | } 49 | 50 | 51 | .hiprint-printTemplate .hiprint-printPanel { 52 | page-break-after: always; 53 | } 54 | 55 | .hiprint-printPaper, hiprint-printPanel { 56 | box-sizing: border-box; 57 | border: 0px; 58 | } 59 | 60 | .hiprint-printPanel .hiprint-printPaper:last-child { 61 | page-break-after: avoid; 62 | } 63 | 64 | .hiprint-printTemplate .hiprint-printPanel:last-child { 65 | page-break-after: avoid; 66 | } 67 | 68 | .hiprint-printPaper .hideheaderLinetarget { 69 | border-top: 0px dashed rgb(201, 190, 190) !important; 70 | } 71 | 72 | .hiprint-printPaper .hidefooterLinetarget { 73 | border-top: 0px dashed rgb(201, 190, 190) !important; 74 | } 75 | 76 | .hiprint-printPaper.design { 77 | border: 1px dashed rgba(170, 170, 170, 0.7); 78 | } 79 | 80 | .design .hiprint-printElement-table-content, .design .hiprint-printElement-longText-content { 81 | overflow: hidden; 82 | box-sizing: border-box; 83 | } 84 | 85 | .design .resize-panel { 86 | box-sizing: border-box; 87 | border: 1px dotted; 88 | } 89 | 90 | .hiprint-printElement-text { 91 | background-color: transparent; 92 | background-repeat: repeat; 93 | padding: 0 0 0 0; 94 | border: 0.75pt none rgb(0, 0, 0); 95 | direction: ltr; 96 | font-family: 'SimSun'; 97 | font-size: 9pt; 98 | font-style: normal; 99 | font-weight: normal; 100 | padding-bottom: 0pt; 101 | padding-left: 0pt; 102 | padding-right: 0pt; 103 | padding-top: 0pt; 104 | text-align: left; 105 | text-decoration: none; 106 | line-height: 9.75pt; 107 | box-sizing: border-box; 108 | word-wrap: break-word; 109 | word-break: break-all; 110 | } 111 | 112 | .design .hiprint-printElement-text-content { 113 | border: 1px dashed rgb(206, 188, 188); 114 | box-sizing: border-box; 115 | } 116 | 117 | .hiprint-printElement-longText { 118 | background-color: transparent; 119 | background-repeat: repeat; 120 | border: 0.75pt none rgb(0, 0, 0); 121 | direction: ltr; 122 | font-family: 'SimSun'; 123 | font-size: 9pt; 124 | font-style: normal; 125 | font-weight: normal; 126 | padding-bottom: 0pt; 127 | padding-left: 0pt; 128 | padding-right: 0pt; 129 | padding-top: 0pt; 130 | text-align: left; 131 | text-decoration: none; 132 | line-height: 9.75pt; 133 | box-sizing: border-box; 134 | word-wrap: break-word; 135 | word-break: break-all; 136 | /*white-space: pre-wrap*/ 137 | } 138 | 139 | 140 | .hiprint-printElement-table { 141 | background-color: transparent; 142 | background-repeat: repeat; 143 | color: rgb(0, 0, 0); 144 | border-color: rgb(0, 0, 0); 145 | border-style: none; 146 | direction: ltr; 147 | font-family: 'SimSun'; 148 | font-size: 9pt; 149 | font-style: normal; 150 | font-weight: normal; 151 | padding-bottom: 0pt; 152 | padding-left: 0pt; 153 | padding-right: 0pt; 154 | padding-top: 0pt; 155 | text-align: left; 156 | text-decoration: none; 157 | padding: 0 0 0 0; 158 | box-sizing: border-box; 159 | line-height: 9.75pt; 160 | } 161 | 162 | .hiprint-printElement-table thead { 163 | background: #e8e8e8; 164 | font-weight: 700; 165 | } 166 | 167 | table.hiprint-printElement-tableTarget { 168 | width: 100%; 169 | } 170 | 171 | .hiprint-printElement-tableTarget, .hiprint-printElement-tableTarget tr, .hiprint-printElement-tableTarget td { 172 | border-color: rgb(0, 0, 0); 173 | /*border-style: none;*/ 174 | /*border: 1px solid rgb(0, 0, 0);*/ 175 | font-weight: normal; 176 | direction: ltr; 177 | padding-bottom: 0pt; 178 | padding-left: 4pt; 179 | padding-right: 4pt; 180 | padding-top: 0pt; 181 | text-decoration: none; 182 | vertical-align: middle; 183 | box-sizing: border-box; 184 | word-wrap: break-word; 185 | word-break: break-all; 186 | /*line-height: 9.75pt; 187 | font-size: 9pt;*/ 188 | } 189 | 190 | .hiprint-printElement-tableTarget-border-all { 191 | border: 1px solid; 192 | } 193 | .hiprint-printElement-tableTarget-border-none { 194 | border: 0px solid; 195 | } 196 | .hiprint-printElement-tableTarget-border-lr { 197 | border-left: 1px solid; 198 | border-right: 1px solid; 199 | } 200 | .hiprint-printElement-tableTarget-border-left { 201 | border-left: 1px solid; 202 | } 203 | .hiprint-printElement-tableTarget-border-right { 204 | border-right: 1px solid; 205 | } 206 | .hiprint-printElement-tableTarget-border-tb { 207 | border-top: 1px solid; 208 | border-bottom: 1px solid; 209 | } 210 | .hiprint-printElement-tableTarget-border-top { 211 | border-top: 1px solid; 212 | } 213 | .hiprint-printElement-tableTarget-border-bottom { 214 | border-bottom: 1px solid; 215 | } 216 | 217 | .hiprint-printElement-tableTarget-border-td-none td { 218 | border: 0px solid; 219 | } 220 | .hiprint-printElement-tableTarget-border-td-all td:not(:nth-last-child(-n+2)) { 221 | border-right: 1px solid; 222 | } 223 | .hiprint-printElement-tableTarget-border-td-all td:last-child { 224 | border-left: 1px solid; 225 | } 226 | .hiprint-printElement-tableTarget-border-td-all td:last-child:first-child { 227 | border-left: none; 228 | } 229 | 230 | /*.hiprint-printElement-tableTarget tr,*/ 231 | .hiprint-printElement-tableTarget td { 232 | height: 18pt; 233 | } 234 | 235 | .hiprint-printPaper .hiprint-paperNumber { 236 | font-size: 9pt; 237 | } 238 | 239 | .design .hiprint-printElement-table-handle { 240 | position: absolute; 241 | height: 21pt; 242 | width: 21pt; 243 | background: red; 244 | z-index: 1; 245 | } 246 | 247 | .hiprint-printPaper .hiprint-paperNumber-disabled { 248 | float: right !important; 249 | right: 0 !important; 250 | color: gainsboro !important; 251 | } 252 | 253 | .hiprint-printElement-vline, .hiprint-printElement-hline { 254 | border: 0px none rgb(0, 0, 0); 255 | 256 | } 257 | 258 | .hiprint-printElement-vline { 259 | border-left: 0.75pt solid #000; 260 | border-right: 0px none rgb(0, 0, 0) !important; 261 | border-bottom: 0px none rgb(0, 0, 0) !important; 262 | border-top: 0px none rgb(0, 0, 0) !important; 263 | } 264 | 265 | .hiprint-printElement-hline { 266 | border-top: 0.75pt solid #000; 267 | border-right: 0px none rgb(0, 0, 0) !important; 268 | border-bottom: 0px none rgb(0, 0, 0) !important; 269 | border-left: 0px none rgb(0, 0, 0) !important; 270 | } 271 | 272 | .hiprint-printElement-oval, .hiprint-printElement-rect { 273 | border: 0.75pt solid #000; 274 | } 275 | 276 | .hiprint-text-content-middle { 277 | } 278 | 279 | .hiprint-text-content-middle > div { 280 | display: grid; 281 | align-items: center; 282 | } 283 | 284 | .hiprint-text-content-bottom { 285 | } 286 | 287 | .hiprint-text-content-bottom > div { 288 | display: grid; 289 | align-items: flex-end; 290 | } 291 | 292 | .hiprint-text-content-wrap { 293 | } 294 | 295 | .hiprint-text-content-wrap .hiprint-text-content-wrap-nowrap { 296 | white-space: nowrap; 297 | } 298 | 299 | .hiprint-text-content-wrap .hiprint-text-content-wrap-clip { 300 | white-space: nowrap; 301 | overflow: hidden; 302 | text-overflow: clip; 303 | } 304 | 305 | .hiprint-text-content-wrap .hiprint-text-content-wrap-ellipsis { 306 | white-space: nowrap; 307 | overflow: hidden; 308 | text-overflow: ellipsis; 309 | } 310 | 311 | /*hi-grid-row */ 312 | .hi-grid-row { 313 | position: relative; 314 | height: auto; 315 | margin-right: 0; 316 | margin-left: 0; 317 | zoom: 1; 318 | display: block; 319 | box-sizing: border-box; 320 | } 321 | 322 | .hi-grid-row::after, .hi-grid-row::before { 323 | display: table; 324 | content: ''; 325 | box-sizing: border-box; 326 | } 327 | 328 | .hi-grid-col { 329 | display: block; 330 | box-sizing: border-box; 331 | position: relative; 332 | float: left; 333 | flex: 0 0 auto; 334 | } 335 | 336 | .table-grid-row { 337 | margin-left: -0pt; 338 | margin-right: -0pt; 339 | } 340 | 341 | .tableGridColumnsGutterRow { 342 | padding-left: 0pt; 343 | padding-right: 0pt; 344 | } 345 | 346 | .hiprint-gridColumnsFooter { 347 | text-align: left; 348 | clear: both; 349 | } 350 | -------------------------------------------------------------------------------- /assets/css/print-lock.css: -------------------------------------------------------------------------------- 1 | @media print { 2 | body { 3 | margin: 0px; 4 | padding: 0px; 5 | } 6 | } 7 | 8 | @page { 9 | margin: 0; 10 | } 11 | 12 | .hiprint-printPaper * { 13 | box-sizing: border-box; 14 | -moz-box-sizing: border-box; /* Firefox */ 15 | -webkit-box-sizing: border-box; /* Safari */ 16 | } 17 | 18 | .hiprint-printPaper *:focus { 19 | outline: -webkit-focus-ring-color auto 0px; 20 | } 21 | 22 | .hiprint-printPaper { 23 | position: relative; 24 | padding: 0 0 0 0; 25 | page-break-after: always; 26 | -webkit-user-select: none; /* Chrome/Safari/Opera */ 27 | -moz-user-select: none; /* Firefox */ 28 | user-select: none; 29 | overflow-x: hidden; 30 | overflow: hidden; 31 | } 32 | 33 | .hiprint-printPaper .hiprint-printPaper-content { 34 | position: relative; 35 | } 36 | 37 | /* 火狐浏览器打印 第一页过后 重叠问题 */ 38 | @-moz-document url-prefix() { 39 | .hiprint-printPaper .hiprint-printPaper-content { 40 | position: relative; 41 | margin-top: 20px; 42 | top: -20px; 43 | } 44 | } 45 | 46 | .hiprint-printPaper.design { 47 | overflow: visible; 48 | } 49 | 50 | .hiprint-printTemplate .hiprint-printPanel { 51 | page-break-after: always; 52 | } 53 | 54 | .hiprint-printPaper, 55 | hiprint-printPanel { 56 | box-sizing: border-box; 57 | border: 0px; 58 | } 59 | 60 | .hiprint-printPanel .hiprint-printPaper:last-child { 61 | page-break-after: avoid; 62 | } 63 | 64 | .hiprint-printTemplate .hiprint-printPanel:last-child { 65 | page-break-after: avoid; 66 | } 67 | 68 | .hiprint-printPaper .hideheaderLinetarget { 69 | border-top: 0px dashed rgb(201, 190, 190) !important; 70 | } 71 | 72 | .hiprint-printPaper .hidefooterLinetarget { 73 | border-top: 0px dashed rgb(201, 190, 190) !important; 74 | } 75 | 76 | .hiprint-printPaper.design { 77 | border: 1px dashed rgba(170, 170, 170, 0.7); 78 | } 79 | 80 | .design .hiprint-printElement-table-content, 81 | .design .hiprint-printElement-longText-content { 82 | overflow: hidden; 83 | box-sizing: border-box; 84 | } 85 | 86 | .design .resize-panel { 87 | box-sizing: border-box; 88 | border: 1px dotted; 89 | } 90 | 91 | .hiprint-printElement-text { 92 | background-color: transparent; 93 | background-repeat: repeat; 94 | padding: 0 0 0 0; 95 | border: 0.75pt none rgb(0, 0, 0); 96 | direction: ltr; 97 | font-family: "SimSun"; 98 | font-size: 9pt; 99 | font-style: normal; 100 | font-weight: normal; 101 | padding-bottom: 0pt; 102 | padding-left: 0pt; 103 | padding-right: 0pt; 104 | padding-top: 0pt; 105 | text-align: left; 106 | text-decoration: none; 107 | line-height: 9.75pt; 108 | box-sizing: border-box; 109 | word-wrap: break-word; 110 | word-break: break-all; 111 | } 112 | 113 | .design .hiprint-printElement-text-content { 114 | border: 1px dashed rgb(206, 188, 188); 115 | box-sizing: border-box; 116 | } 117 | 118 | .hiprint-printElement-longText { 119 | background-color: transparent; 120 | background-repeat: repeat; 121 | border: 0.75pt none rgb(0, 0, 0); 122 | direction: ltr; 123 | font-family: "SimSun"; 124 | font-size: 9pt; 125 | font-style: normal; 126 | font-weight: normal; 127 | padding-bottom: 0pt; 128 | padding-left: 0pt; 129 | padding-right: 0pt; 130 | padding-top: 0pt; 131 | text-align: left; 132 | text-decoration: none; 133 | line-height: 9.75pt; 134 | box-sizing: border-box; 135 | word-wrap: break-word; 136 | word-break: break-all; 137 | /*white-space: pre-wrap*/ 138 | } 139 | 140 | .hiprint-printElement-table { 141 | background-color: transparent; 142 | background-repeat: repeat; 143 | color: rgb(0, 0, 0); 144 | border-color: rgb(0, 0, 0); 145 | border-style: none; 146 | direction: ltr; 147 | font-family: "SimSun"; 148 | font-size: 9pt; 149 | font-style: normal; 150 | font-weight: normal; 151 | padding-bottom: 0pt; 152 | padding-left: 0pt; 153 | padding-right: 0pt; 154 | padding-top: 0pt; 155 | text-align: left; 156 | text-decoration: none; 157 | padding: 0 0 0 0; 158 | box-sizing: border-box; 159 | line-height: 9.75pt; 160 | } 161 | 162 | .hiprint-printElement-table thead { 163 | background: #e8e8e8; 164 | font-weight: 700; 165 | } 166 | 167 | table.hiprint-printElement-tableTarget { 168 | width: 100%; 169 | } 170 | 171 | .hiprint-printElement-tableTarget, 172 | .hiprint-printElement-tableTarget tr, 173 | .hiprint-printElement-tableTarget td { 174 | border-color: rgb(0, 0, 0); 175 | /*border-style: none;*/ 176 | /*border: 1px solid rgb(0, 0, 0);*/ 177 | font-weight: normal; 178 | direction: ltr; 179 | padding-bottom: 0pt; 180 | padding-left: 4pt; 181 | padding-right: 4pt; 182 | padding-top: 0pt; 183 | text-decoration: none; 184 | vertical-align: middle; 185 | box-sizing: border-box; 186 | word-wrap: break-word; 187 | word-break: break-all; 188 | /*line-height: 9.75pt; 189 | font-size: 9pt;*/ 190 | } 191 | 192 | .hiprint-printElement-tableTarget-border-all { 193 | border: 1px solid; 194 | } 195 | .hiprint-printElement-tableTarget-border-none { 196 | border: 0px solid; 197 | } 198 | .hiprint-printElement-tableTarget-border-lr { 199 | border-left: 1px solid; 200 | border-right: 1px solid; 201 | } 202 | .hiprint-printElement-tableTarget-border-left { 203 | border-left: 1px solid; 204 | } 205 | .hiprint-printElement-tableTarget-border-right { 206 | border-right: 1px solid; 207 | } 208 | .hiprint-printElement-tableTarget-border-tb { 209 | border-top: 1px solid; 210 | border-bottom: 1px solid; 211 | } 212 | .hiprint-printElement-tableTarget-border-top { 213 | border-top: 1px solid; 214 | } 215 | .hiprint-printElement-tableTarget-border-bottom { 216 | border-bottom: 1px solid; 217 | } 218 | 219 | .hiprint-printElement-tableTarget-border-td-none td { 220 | border: 0px solid; 221 | } 222 | .hiprint-printElement-tableTarget-border-td-all td:not(:nth-last-child(-n + 2)) { 223 | border-right: 1px solid; 224 | } 225 | .hiprint-printElement-tableTarget-border-td-all td:last-child { 226 | border-left: 1px solid; 227 | } 228 | .hiprint-printElement-tableTarget-border-td-all td:last-child:first-child { 229 | border-left: none; 230 | } 231 | 232 | /*.hiprint-printElement-tableTarget tr,*/ 233 | .hiprint-printElement-tableTarget td { 234 | height: 18pt; 235 | } 236 | 237 | .hiprint-printPaper .hiprint-paperNumber { 238 | font-size: 9pt; 239 | } 240 | 241 | .design .hiprint-printElement-table-handle { 242 | position: absolute; 243 | height: 21pt; 244 | width: 21pt; 245 | background: red; 246 | z-index: 1; 247 | } 248 | 249 | .hiprint-printPaper .hiprint-paperNumber-disabled { 250 | float: right !important; 251 | right: 0 !important; 252 | color: gainsboro !important; 253 | } 254 | 255 | .hiprint-printElement-vline, 256 | .hiprint-printElement-hline { 257 | border: 0px none rgb(0, 0, 0); 258 | } 259 | 260 | .hiprint-printElement-vline { 261 | border-left: 0.75pt solid #000; 262 | border-right: 0px none rgb(0, 0, 0) !important; 263 | border-bottom: 0px none rgb(0, 0, 0) !important; 264 | border-top: 0px none rgb(0, 0, 0) !important; 265 | } 266 | 267 | .hiprint-printElement-hline { 268 | border-top: 0.75pt solid #000; 269 | border-right: 0px none rgb(0, 0, 0) !important; 270 | border-bottom: 0px none rgb(0, 0, 0) !important; 271 | border-left: 0px none rgb(0, 0, 0) !important; 272 | } 273 | 274 | .hiprint-printElement-oval, 275 | .hiprint-printElement-rect { 276 | border: 0.75pt solid #000; 277 | } 278 | 279 | .hiprint-text-content-middle { 280 | } 281 | 282 | .hiprint-text-content-middle > div { 283 | display: grid; 284 | align-items: center; 285 | } 286 | 287 | .hiprint-text-content-bottom { 288 | } 289 | 290 | .hiprint-text-content-bottom > div { 291 | display: grid; 292 | align-items: flex-end; 293 | } 294 | 295 | .hiprint-text-content-wrap { 296 | } 297 | 298 | .hiprint-text-content-wrap .hiprint-text-content-wrap-nowrap { 299 | white-space: nowrap; 300 | } 301 | 302 | .hiprint-text-content-wrap .hiprint-text-content-wrap-clip { 303 | white-space: nowrap; 304 | overflow: hidden; 305 | text-overflow: clip; 306 | } 307 | 308 | .hiprint-text-content-wrap .hiprint-text-content-wrap-ellipsis { 309 | white-space: nowrap; 310 | overflow: hidden; 311 | text-overflow: ellipsis; 312 | } 313 | 314 | /*hi-grid-row */ 315 | .hi-grid-row { 316 | position: relative; 317 | height: auto; 318 | margin-right: 0; 319 | margin-left: 0; 320 | zoom: 1; 321 | display: block; 322 | box-sizing: border-box; 323 | } 324 | 325 | .hi-grid-row::after, 326 | .hi-grid-row::before { 327 | display: table; 328 | content: ""; 329 | box-sizing: border-box; 330 | } 331 | 332 | .hi-grid-col { 333 | display: block; 334 | box-sizing: border-box; 335 | position: relative; 336 | float: left; 337 | flex: 0 0 auto; 338 | } 339 | 340 | .table-grid-row { 341 | margin-left: -0pt; 342 | margin-right: -0pt; 343 | } 344 | 345 | .tableGridColumnsGutterRow { 346 | padding-left: 0pt; 347 | padding-right: 0pt; 348 | } 349 | 350 | .hiprint-gridColumnsFooter { 351 | text-align: left; 352 | clear: both; 353 | } 354 | -------------------------------------------------------------------------------- /plugin/0.0.60_print-lock.css: -------------------------------------------------------------------------------- 1 | @media print { 2 | body { 3 | margin: 0px; 4 | padding: 0px; 5 | } 6 | } 7 | 8 | @page { 9 | margin: 0; 10 | } 11 | 12 | .hiprint-printPaper * { 13 | box-sizing: border-box; 14 | -moz-box-sizing: border-box; /* Firefox */ 15 | -webkit-box-sizing: border-box; /* Safari */ 16 | } 17 | 18 | .hiprint-printPaper *:focus { 19 | outline: -webkit-focus-ring-color auto 0px; 20 | } 21 | 22 | .hiprint-printPaper { 23 | position: relative; 24 | padding: 0 0 0 0; 25 | page-break-after: always; 26 | -webkit-user-select: none; /* Chrome/Safari/Opera */ 27 | -moz-user-select: none; /* Firefox */ 28 | user-select: none; 29 | overflow-x: hidden; 30 | overflow: hidden; 31 | } 32 | 33 | .hiprint-printPaper .hiprint-printPaper-content { 34 | position: relative; 35 | } 36 | 37 | /* 火狐浏览器打印 第一页过后 重叠问题 */ 38 | @-moz-document url-prefix() { 39 | .hiprint-printPaper .hiprint-printPaper-content { 40 | position: relative; 41 | margin-top: 20px; 42 | top: -20px 43 | } 44 | } 45 | 46 | .hiprint-printPaper.design { 47 | overflow: visible; 48 | } 49 | 50 | 51 | .hiprint-printTemplate .hiprint-printPanel { 52 | page-break-after: always; 53 | } 54 | 55 | .hiprint-printPaper, hiprint-printPanel { 56 | box-sizing: border-box; 57 | border: 0px; 58 | } 59 | 60 | .hiprint-printPanel .hiprint-printPaper:last-child { 61 | page-break-after: avoid; 62 | } 63 | 64 | .hiprint-printTemplate .hiprint-printPanel:last-child { 65 | page-break-after: avoid; 66 | } 67 | 68 | .hiprint-printPaper .hideheaderLinetarget { 69 | border-top: 0px dashed rgb(201, 190, 190) !important; 70 | } 71 | 72 | .hiprint-printPaper .hidefooterLinetarget { 73 | border-top: 0px dashed rgb(201, 190, 190) !important; 74 | } 75 | 76 | .hiprint-printPaper.design { 77 | border: 1px dashed rgba(170, 170, 170, 0.7); 78 | } 79 | 80 | .design .hiprint-printElement-table-content, .design .hiprint-printElement-longText-content { 81 | overflow: hidden; 82 | box-sizing: border-box; 83 | } 84 | 85 | .design .resize-panel { 86 | box-sizing: border-box; 87 | border: 1px dotted; 88 | } 89 | 90 | .hiprint-printElement-text { 91 | background-color: transparent; 92 | background-repeat: repeat; 93 | padding: 0 0 0 0; 94 | border: 0.75pt none rgb(0, 0, 0); 95 | direction: ltr; 96 | font-family: 'SimSun'; 97 | font-size: 9pt; 98 | font-style: normal; 99 | font-weight: normal; 100 | padding-bottom: 0pt; 101 | padding-left: 0pt; 102 | padding-right: 0pt; 103 | padding-top: 0pt; 104 | text-align: left; 105 | text-decoration: none; 106 | line-height: 9.75pt; 107 | box-sizing: border-box; 108 | word-wrap: break-word; 109 | word-break: break-all; 110 | } 111 | 112 | .design .hiprint-printElement-text-content { 113 | border: 1px dashed rgb(206, 188, 188); 114 | box-sizing: border-box; 115 | } 116 | 117 | .hiprint-printElement-longText { 118 | background-color: transparent; 119 | background-repeat: repeat; 120 | border: 0.75pt none rgb(0, 0, 0); 121 | direction: ltr; 122 | font-family: 'SimSun'; 123 | font-size: 9pt; 124 | font-style: normal; 125 | font-weight: normal; 126 | padding-bottom: 0pt; 127 | padding-left: 0pt; 128 | padding-right: 0pt; 129 | padding-top: 0pt; 130 | text-align: left; 131 | text-decoration: none; 132 | line-height: 9.75pt; 133 | box-sizing: border-box; 134 | word-wrap: break-word; 135 | word-break: break-all; 136 | /*white-space: pre-wrap*/ 137 | } 138 | 139 | 140 | .hiprint-printElement-table { 141 | background-color: transparent; 142 | background-repeat: repeat; 143 | color: rgb(0, 0, 0); 144 | border-color: rgb(0, 0, 0); 145 | border-style: none; 146 | direction: ltr; 147 | font-family: 'SimSun'; 148 | font-size: 9pt; 149 | font-style: normal; 150 | font-weight: normal; 151 | padding-bottom: 0pt; 152 | padding-left: 0pt; 153 | padding-right: 0pt; 154 | padding-top: 0pt; 155 | text-align: left; 156 | text-decoration: none; 157 | padding: 0 0 0 0; 158 | box-sizing: border-box; 159 | line-height: 9.75pt; 160 | } 161 | 162 | .hiprint-printElement-table thead { 163 | background: #e8e8e8; 164 | font-weight: 700; 165 | } 166 | 167 | table.hiprint-printElement-tableTarget { 168 | width: 100%; 169 | } 170 | 171 | .hiprint-printElement-tableTarget, .hiprint-printElement-tableTarget tr, .hiprint-printElement-tableTarget td { 172 | border-color: rgb(0, 0, 0); 173 | /*border-style: none;*/ 174 | /*border: 1px solid rgb(0, 0, 0);*/ 175 | font-weight: normal; 176 | direction: ltr; 177 | padding-bottom: 0pt; 178 | padding-left: 4pt; 179 | padding-right: 4pt; 180 | padding-top: 0pt; 181 | text-decoration: none; 182 | vertical-align: middle; 183 | box-sizing: border-box; 184 | word-wrap: break-word; 185 | word-break: break-all; 186 | /*line-height: 9.75pt; 187 | font-size: 9pt;*/ 188 | } 189 | 190 | .hiprint-printElement-tableTarget-border-all { 191 | border: 1px solid; 192 | } 193 | .hiprint-printElement-tableTarget-border-none { 194 | border: 0px solid; 195 | } 196 | .hiprint-printElement-tableTarget-border-lr { 197 | border-left: 1px solid; 198 | border-right: 1px solid; 199 | } 200 | .hiprint-printElement-tableTarget-border-left { 201 | border-left: 1px solid; 202 | } 203 | .hiprint-printElement-tableTarget-border-right { 204 | border-right: 1px solid; 205 | } 206 | .hiprint-printElement-tableTarget-border-tb { 207 | border-top: 1px solid; 208 | border-bottom: 1px solid; 209 | } 210 | .hiprint-printElement-tableTarget-border-top { 211 | border-top: 1px solid; 212 | } 213 | .hiprint-printElement-tableTarget-border-bottom { 214 | border-bottom: 1px solid; 215 | } 216 | 217 | .hiprint-printElement-tableTarget-border-td-none td { 218 | border: 0px solid; 219 | } 220 | .hiprint-printElement-tableTarget-border-td-all td:not(:nth-last-child(-n+2)) { 221 | border-right: 1px solid; 222 | } 223 | .hiprint-printElement-tableTarget-border-td-all td:not(last-child) { 224 | border-right: 1px solid; 225 | } 226 | .hiprint-printElement-tableTarget-border-td-all td:last-child { 227 | border-left: 1px solid; 228 | } 229 | .hiprint-printElement-tableTarget-border-td-all td:last-child:first-child { 230 | border-left: none; 231 | } 232 | 233 | /*.hiprint-printElement-tableTarget tr,*/ 234 | .hiprint-printElement-tableTarget td { 235 | height: 18pt; 236 | } 237 | 238 | .hiprint-printPaper .hiprint-paperNumber { 239 | font-size: 9pt; 240 | } 241 | 242 | .design .hiprint-printElement-table-handle { 243 | position: absolute; 244 | height: 21pt; 245 | width: 21pt; 246 | background: red; 247 | z-index: 1; 248 | } 249 | 250 | .hiprint-printPaper .hiprint-paperNumber-disabled { 251 | float: right !important; 252 | right: 0 !important; 253 | color: gainsboro !important; 254 | } 255 | 256 | .hiprint-printElement-vline, .hiprint-printElement-hline { 257 | border: 0px none rgb(0, 0, 0); 258 | 259 | } 260 | 261 | .hiprint-printElement-vline { 262 | border-left: 0.75pt solid #000; 263 | border-right: 0px none rgb(0, 0, 0) !important; 264 | border-bottom: 0px none rgb(0, 0, 0) !important; 265 | border-top: 0px none rgb(0, 0, 0) !important; 266 | } 267 | 268 | .hiprint-printElement-hline { 269 | border-top: 0.75pt solid #000; 270 | border-right: 0px none rgb(0, 0, 0) !important; 271 | border-bottom: 0px none rgb(0, 0, 0) !important; 272 | border-left: 0px none rgb(0, 0, 0) !important; 273 | } 274 | 275 | .hiprint-printElement-oval, .hiprint-printElement-rect { 276 | border: 0.75pt solid #000; 277 | } 278 | 279 | .hiprint-text-content-middle { 280 | } 281 | 282 | .hiprint-text-content-middle > div { 283 | display: grid; 284 | align-items: center; 285 | } 286 | 287 | .hiprint-text-content-bottom { 288 | } 289 | 290 | .hiprint-text-content-bottom > div { 291 | display: grid; 292 | align-items: flex-end; 293 | } 294 | 295 | .hiprint-text-content-wrap { 296 | } 297 | 298 | .hiprint-text-content-wrap .hiprint-text-content-wrap-nowrap { 299 | white-space: nowrap; 300 | } 301 | 302 | .hiprint-text-content-wrap .hiprint-text-content-wrap-clip { 303 | white-space: nowrap; 304 | overflow: hidden; 305 | text-overflow: clip; 306 | } 307 | 308 | .hiprint-text-content-wrap .hiprint-text-content-wrap-ellipsis { 309 | white-space: nowrap; 310 | overflow: hidden; 311 | text-overflow: ellipsis; 312 | } 313 | 314 | /*hi-grid-row */ 315 | .hi-grid-row { 316 | position: relative; 317 | height: auto; 318 | margin-right: 0; 319 | margin-left: 0; 320 | zoom: 1; 321 | display: block; 322 | box-sizing: border-box; 323 | } 324 | 325 | .hi-grid-row::after, .hi-grid-row::before { 326 | display: table; 327 | content: ''; 328 | box-sizing: border-box; 329 | } 330 | 331 | .hi-grid-col { 332 | display: block; 333 | box-sizing: border-box; 334 | position: relative; 335 | float: left; 336 | flex: 0 0 auto; 337 | } 338 | 339 | .table-grid-row { 340 | margin-left: -0pt; 341 | margin-right: -0pt; 342 | } 343 | 344 | .tableGridColumnsGutterRow { 345 | padding-left: 0pt; 346 | padding-right: 0pt; 347 | } 348 | 349 | .hiprint-gridColumnsFooter { 350 | text-align: left; 351 | clear: both; 352 | } 353 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Date: 2024-01-25 15:52:14 3 | * @LastEditors: admin@54xavier.cn 4 | * @LastEditTime: 2024-12-23 15:23:56 5 | * @FilePath: \electron-hiprint\main.js 6 | */ 7 | const { 8 | app, 9 | BrowserWindow, 10 | BrowserView, 11 | ipcMain, 12 | Notification, 13 | Tray, 14 | Menu, 15 | shell, 16 | } = require("electron"); 17 | const electronLog = require("electron-log"); 18 | const path = require("path"); 19 | const server = require("http").createServer(); 20 | const helper = require("./src/helper"); 21 | const printSetup = require("./src/print"); 22 | const renderSetup = require("./src/render"); 23 | const setSetup = require("./src/set"); 24 | const printLogSetup = require("./src/printLog"); 25 | const { 26 | store, 27 | address, 28 | initServeEvent, 29 | initClientEvent, 30 | getMachineId, 31 | showAboutDialog, 32 | } = require("./tools/utils"); 33 | 34 | const TaskRunner = require("concurrent-tasks"); 35 | const dayjs = require("dayjs"); 36 | 37 | const logPath = store.get("logPath") || app.getPath("logs"); 38 | 39 | Object.assign(console, electronLog.functions); 40 | 41 | electronLog.transports.file.resolvePathFn = () => 42 | path.join(logPath, dayjs().format("YYYY-MM-DD.log")); 43 | 44 | // 监听崩溃事件 45 | process.on("uncaughtException", (error) => { 46 | console.error(error); 47 | }); 48 | 49 | // 监听渲染进程崩溃 50 | app.on("web-contents-created", (event, contents) => { 51 | contents.on("render-process-gone", (event, details) => { 52 | console.error(details.reason); 53 | }); 54 | }); 55 | 56 | if (store.get("disabledGpu")) { 57 | app.commandLine.appendSwitch("disable-gpu"); 58 | } 59 | 60 | // 主进程 61 | global.MAIN_WINDOW = null; 62 | // 托盘 63 | global.APP_TRAY = null; 64 | // 打印窗口 65 | global.PRINT_WINDOW = null; 66 | // 设置窗口 67 | global.SET_WINDOW = null; 68 | // 渲染窗口 69 | global.RENDER_WINDOW = null; 70 | // 打印日志窗口 71 | global.PRINT_LOG_WINDOW = null; 72 | // socket.io 服务端 73 | global.SOCKET_SERVER = null; 74 | // socket.io-client 客户端 75 | global.SOCKET_CLIENT = null; 76 | // 打印队列,解决打印并发崩溃问题 77 | global.PRINT_RUNNER = new TaskRunner({ concurrency: 1 }); 78 | // 打印队列 done 集合 79 | global.PRINT_RUNNER_DONE = {}; 80 | // 分批打印任务的打印任务信息 81 | global.PRINT_FRAGMENTS_MAPPING = { 82 | // [id: string]: { // 当前打印任务id,当此任务完成或超过指定时间会删除该对象 83 | // { 84 | // total: number, // html片段总数 85 | // count: number, // 已经保存完成的片段数量,当count与total相同时,所有片段传输完成 86 | // fragments: Array, // 按照顺序摆放的html文本片段 87 | // updateTime: number, // 最后更新此任务信息的时间戳,用于超时时移除此对象 88 | // } 89 | // } 90 | }; 91 | global.RENDER_RUNNER = new TaskRunner({ concurrency: 1 }); 92 | global.RENDER_RUNNER_DONE = {}; 93 | 94 | // socket.io 服务端,用于创建本地服务 95 | const ioServer = (global.SOCKET_SERVER = new require("socket.io")(server, { 96 | pingInterval: 10000, 97 | pingTimeout: 5000, 98 | maxHttpBufferSize: 10000000000, 99 | allowEIO3: true, // 兼容 Socket.IO 2.x 100 | // 跨域问题(Socket.IO 3.x 使用这种方式) 101 | cors: { 102 | // origin: "*", 103 | // 兼容 Socket.IO 2.x 104 | origin: (requestOrigin, callback) => { 105 | // 允许所有域名连接 106 | callback(null, requestOrigin); 107 | }, 108 | methods: "GET, POST, PUT, DELETE, OPTIONS", 109 | allowedHeaders: "*", 110 | // 详情参数见 https://www.npmjs.com/package/cors 111 | credentials: false, 112 | }, 113 | })); 114 | 115 | // socket.io 客户端,用于连接中转服务 116 | const ioClient = require("socket.io-client").io; 117 | 118 | /** 119 | * @description: 初始化 120 | */ 121 | async function initialize() { 122 | // 限制一个窗口 123 | const gotTheLock = app.requestSingleInstanceLock(); 124 | if (!gotTheLock) { 125 | // 销毁所有窗口、托盘、退出应用 126 | helper.appQuit(); 127 | } 128 | 129 | // 当运行第二个实例时,聚焦到 MAIN_WINDOW 这个窗口 130 | app.on("second-instance", () => { 131 | if (MAIN_WINDOW) { 132 | if (MAIN_WINDOW.isMinimized()) { 133 | // 将窗口从最小化状态恢复到以前的状态 134 | MAIN_WINDOW.restore(); 135 | } 136 | MAIN_WINDOW.focus(); 137 | } 138 | }); 139 | 140 | // 允许渲染进程创建通知 141 | ipcMain.on("notification", (event, data) => { 142 | const notification = new Notification(data); 143 | // 显示通知 144 | notification.show(); 145 | }); 146 | 147 | // 打开设置窗口 148 | ipcMain.on("openSetting", openSetWindow); 149 | 150 | // 获取设备唯一id 151 | ipcMain.on("getMachineId", (event) => { 152 | const machineId = getMachineId(); 153 | event.sender.send("machineId", machineId); 154 | }); 155 | 156 | // 获取设备ip、mac等信息 157 | ipcMain.on("getAddress", (event) => { 158 | address.all().then((obj) => { 159 | event.sender.send("address", { 160 | ...obj, 161 | port: store.get("port"), 162 | }); 163 | }); 164 | }); 165 | 166 | // 当electron完成初始化 167 | app.whenReady().then(() => { 168 | // 创建浏览器窗口 169 | createWindow(); 170 | app.on("activate", function() { 171 | if (BrowserWindow.getAllWindows().length === 0) { 172 | createWindow(); 173 | } 174 | }); 175 | console.log("==> Electron-hiprint 启动 <=="); 176 | }); 177 | } 178 | 179 | /** 180 | * @description: 创建渲染进程 主窗口 181 | * @return {BrowserWindow} MAIN_WINDOW 主窗口 182 | */ 183 | async function createWindow() { 184 | const windowOptions = { 185 | width: 500, // 窗口宽度 186 | height: 300, // 窗口高度 187 | title: store.get("mainTitle") || "Electron-hiprint", 188 | useContentSize: true, // 窗口大小不包含边框 189 | center: true, // 居中 190 | resizable: false, // 禁止窗口缩放 191 | show: false, // 初始隐藏 192 | webPreferences: { 193 | // 设置此项为false后,才可在渲染进程中使用 electron api 194 | contextIsolation: false, 195 | nodeIntegration: true, 196 | }, 197 | }; 198 | 199 | // 窗口左上角图标 200 | if (!app.isPackaged) { 201 | windowOptions.icon = path.join(__dirname, "build/icons/256x256.png"); 202 | } else { 203 | app.setLoginItemSettings({ 204 | openAtLogin: store.get("openAtLogin"), 205 | openAsHidden: store.get("openAsHidden"), 206 | }); 207 | } 208 | 209 | // 创建主窗口 210 | MAIN_WINDOW = new BrowserWindow(windowOptions); 211 | 212 | // 添加加载页面 解决白屏的问题 213 | loadingView(windowOptions); 214 | 215 | // 初始化系统设置 216 | systemSetup(); 217 | 218 | // 加载主页面 219 | const indexHtml = path.join("file://", app.getAppPath(), "assets/index.html"); 220 | MAIN_WINDOW.webContents.loadURL(indexHtml); 221 | 222 | // 退出 223 | MAIN_WINDOW.on("closed", () => { 224 | MAIN_WINDOW = null; 225 | server.close(); 226 | }); 227 | 228 | // 点击关闭,最小化到托盘 229 | MAIN_WINDOW.on("close", (event) => { 230 | if (store.get("closeType") === "tray") { 231 | // 最小化到托盘 232 | MAIN_WINDOW.hide(); 233 | 234 | // 隐藏任务栏 235 | MAIN_WINDOW.setSkipTaskbar(true); 236 | 237 | // 阻止窗口关闭 238 | event.preventDefault(); 239 | } else { 240 | // 销毁所有窗口、托盘、退出应用 241 | helper.appQuit(); 242 | } 243 | }); 244 | 245 | // 主窗口 Dom 加载完毕 246 | MAIN_WINDOW.webContents.on("dom-ready", async () => { 247 | try { 248 | if (!store.get("openAsHidden")) { 249 | MAIN_WINDOW.show(); 250 | } 251 | // 未打包时打开开发者工具 252 | if (!app.isPackaged) { 253 | MAIN_WINDOW.webContents.openDevTools(); 254 | } 255 | // 本地服务开启端口监听 256 | server.listen(store.get("port") || 17521); 257 | // 初始化本地 服务端事件 258 | initServeEvent(ioServer); 259 | // 有配置中转服务时连接中转服务 260 | if ( 261 | store.get("connectTransit") && 262 | store.get("transitUrl") && 263 | store.get("transitToken") 264 | ) { 265 | global.SOCKET_CLIENT = ioClient(store.get("transitUrl"), { 266 | transports: ["websocket"], 267 | query: { 268 | client: "electron-hiprint", 269 | }, 270 | auth: { 271 | token: store.get("transitToken"), 272 | }, 273 | }); 274 | 275 | // 初始化中转 客户端事件 276 | initClientEvent(); 277 | } 278 | } catch (error) { 279 | console.error(error); 280 | } 281 | }); 282 | 283 | // 初始化托盘 284 | initTray(); 285 | // 打印窗口初始化 286 | await printSetup(); 287 | // 渲染窗口初始化 288 | await renderSetup(); 289 | 290 | return MAIN_WINDOW; 291 | } 292 | 293 | /** 294 | * @description: 加载等待页面,解决主窗口白屏问题 295 | * @param {Object} windowOptions 主窗口配置 296 | * @return {Void} 297 | */ 298 | function loadingView(windowOptions) { 299 | const loadingBrowserView = new BrowserView(); 300 | MAIN_WINDOW.setBrowserView(loadingBrowserView); 301 | loadingBrowserView.setBounds({ 302 | x: 0, 303 | y: 0, 304 | width: windowOptions.width, 305 | height: windowOptions.height, 306 | }); 307 | 308 | const loadingHtml = path.join( 309 | "file://", 310 | app.getAppPath(), 311 | "assets/loading.html", 312 | ); 313 | loadingBrowserView.webContents.loadURL(loadingHtml); 314 | 315 | // 主窗口 dom 加载完毕,移除 loadingBrowserView 316 | MAIN_WINDOW.webContents.on("dom-ready", async (event) => { 317 | loadingBrowserView.webContents.destroy(); 318 | MAIN_WINDOW.removeBrowserView(loadingBrowserView); 319 | }); 320 | } 321 | 322 | /** 323 | * @description: 初始化系统设置 324 | * @return {Void} 325 | */ 326 | function systemSetup() { 327 | // 隐藏菜单栏 328 | Menu.setApplicationMenu(null); 329 | } 330 | 331 | /** 332 | * @description: 显示主窗口 333 | * @return {Void} 334 | */ 335 | function showMainWindow() { 336 | if (MAIN_WINDOW.isMinimized()) { 337 | // 将窗口从最小化状态恢复到以前的状态 338 | MAIN_WINDOW.restore(); 339 | } 340 | if (!MAIN_WINDOW.isVisible()) { 341 | // 主窗口关闭不会被销毁,只是隐藏,重新显示即可 342 | MAIN_WINDOW.show(); 343 | } 344 | if (!MAIN_WINDOW.isFocused()) { 345 | // 主窗口未聚焦,使其聚焦 346 | MAIN_WINDOW.focus(); 347 | } 348 | MAIN_WINDOW.setSkipTaskbar(false); 349 | } 350 | 351 | /** 352 | * @description: 初始化托盘 353 | * @return {Tray} APP_TRAY 托盘实例 354 | */ 355 | function initTray() { 356 | let trayPath = path.join(app.getAppPath(), "assets/icons/tray.png"); 357 | 358 | APP_TRAY = new Tray(trayPath); 359 | 360 | // 托盘提示标题 361 | APP_TRAY.setToolTip("hiprint"); 362 | 363 | // 托盘菜单 364 | const trayMenuTemplate = [ 365 | { 366 | // 神知道为什么 linux 上无法识别 tray click、double-click,只能添加一个菜单 367 | label: "显示主窗口", 368 | click: () => { 369 | showMainWindow(); 370 | }, 371 | }, 372 | { 373 | label: "设置", 374 | click: () => { 375 | console.log("==>TRAY 打开设置窗口<=="); 376 | openSetWindow(); 377 | }, 378 | }, 379 | { 380 | label: "软件日志", 381 | click: () => { 382 | console.log("==>TRAY 查看软件日志<=="); 383 | shell.openPath(logPath); 384 | }, 385 | }, 386 | { 387 | label: "打印记录", 388 | click: () => { 389 | console.log("==>TRAY 打开打印记录窗口<=="); 390 | if (!PRINT_LOG_WINDOW) { 391 | printLogSetup(); 392 | } else { 393 | PRINT_LOG_WINDOW.show(); 394 | } 395 | }, 396 | }, 397 | { 398 | label: "关于", 399 | click: () => { 400 | console.log("==>TRAY 打开关于弹框<=="); 401 | showAboutDialog(); 402 | }, 403 | }, 404 | { 405 | label: "退出", 406 | click: () => { 407 | console.log("==>TRAY 退出应用<=="); 408 | helper.appQuit(); 409 | }, 410 | }, 411 | ]; 412 | 413 | APP_TRAY.setContextMenu(Menu.buildFromTemplate(trayMenuTemplate)); 414 | 415 | // 监听点击事件 416 | APP_TRAY.on("click", function() { 417 | console.log("==>TRAY 点击托盘图标<=="); 418 | showMainWindow(); 419 | }); 420 | return APP_TRAY; 421 | } 422 | 423 | /** 424 | * @description: 打开设置窗口 425 | * @return {BrowserWindow} SET_WINDOW 设置窗口 426 | */ 427 | async function openSetWindow() { 428 | if (!SET_WINDOW) { 429 | await setSetup(); 430 | } else { 431 | SET_WINDOW.show(); 432 | } 433 | return SET_WINDOW; 434 | } 435 | 436 | // 初始化主窗口 437 | initialize(); 438 | -------------------------------------------------------------------------------- /src/set.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Date: 2023-09-05 17:34:28 3 | * @LastEditors: admin@54xavier.cn 4 | * @LastEditTime: 2024-12-22 16:50:24 5 | * @FilePath: \xavier9896-electron-hiprint\src\set.js 6 | */ 7 | "use strict"; 8 | 9 | const { 10 | app, 11 | BrowserWindow, 12 | BrowserView, 13 | ipcMain, 14 | dialog, 15 | shell, 16 | } = require("electron"); 17 | const path = require("path"); 18 | const https = require("node:https"); 19 | const fs = require("node:fs"); 20 | const { store } = require("../tools/utils"); 21 | 22 | /** 23 | * @description: 创建设置窗口 24 | * @return {BrowserWindow} SET_WINDOW 设置窗口 25 | */ 26 | async function createSetWindow() { 27 | const windowOptions = { 28 | width: 440, // 窗口宽度 29 | height: 591, // 窗口高度 30 | title: "设置", 31 | useContentSize: true, // 窗口大小不包含边框 32 | center: true, // 居中 33 | alwaysOnTop: true, // 永远置顶 34 | resizable: false, // 不可缩放 35 | webPreferences: { 36 | contextIsolation: false, // 设置此项为false后,才可在渲染进程中使用 electron api 37 | nodeIntegration: true, 38 | }, 39 | }; 40 | 41 | // 创建设置窗口 42 | SET_WINDOW = new BrowserWindow(windowOptions); 43 | 44 | // 添加加载页面 解决白屏的问题 45 | loadingView(windowOptions); 46 | 47 | // 加载设置渲染进程页面 48 | const setHtmlUrl = path.join("file://", app.getAppPath(), "assets/set.html"); 49 | SET_WINDOW.webContents.loadURL(setHtmlUrl); 50 | 51 | // 未打包时打开开发者工具 52 | if (!app.isPackaged) { 53 | SET_WINDOW.webContents.openDevTools(); 54 | } 55 | 56 | // 绑定窗口事件 57 | initSetEvent(); 58 | 59 | // 监听退出,移除所有事件 60 | SET_WINDOW.on("closed", removeEvent); 61 | 62 | SET_WINDOW.webContents.on("did-finish-load", () => { 63 | const downloadedVersions = getDownloadedVersions(); 64 | SET_WINDOW.webContents.send("downloadedVersions", downloadedVersions); 65 | }); 66 | 67 | return SET_WINDOW; 68 | } 69 | 70 | /** 71 | * @description: 加载等待页面,解决主窗口白屏问题 72 | * @param {Object} windowOptions 主窗口配置 73 | * @return {void} 74 | */ 75 | function loadingView(windowOptions) { 76 | const loadingBrowserView = new BrowserView(); 77 | SET_WINDOW.setBrowserView(loadingBrowserView); 78 | loadingBrowserView.setBounds({ 79 | x: 0, 80 | y: 0, 81 | width: windowOptions.width, 82 | height: windowOptions.height, 83 | }); 84 | 85 | const loadingHtml = path.join( 86 | "file://", 87 | app.getAppPath(), 88 | "assets/loading.html", 89 | ); 90 | loadingBrowserView.webContents.loadURL(loadingHtml); 91 | 92 | // 设置窗口 dom 加载完毕,移除 loadingBrowserView 93 | SET_WINDOW.webContents.on("dom-ready", async (event) => { 94 | loadingBrowserView.webContents.destroy(); 95 | SET_WINDOW.removeBrowserView(loadingBrowserView); 96 | }); 97 | } 98 | 99 | /** 100 | * @description: 渲染进程触发写入配置 101 | * @param {IpcMainEvent} event 102 | * @param {Object} data 配置数据 103 | * @return {void} 104 | */ 105 | function setConfig(event, data) { 106 | console.log("==> 设置窗口:保存配置 <=="); 107 | // 保存配置前,弹出 dialog 确认 108 | dialog 109 | .showMessageBox(SET_WINDOW, { 110 | type: "question", 111 | title: "提示", 112 | message: 113 | "保存设置需要重启软件,如有正在执行中的打印任务可能会被中断,是否确定要保存并重启?", 114 | buttons: ["确定", "取消"], 115 | }) 116 | .then((res) => { 117 | if (res.response === 0) { 118 | try { 119 | let pdfPath = path.join(data.pdfPath, "url_pdf"); 120 | fs.mkdirSync(pdfPath, { recursive: true }); 121 | pdfPath = path.join(data.pdfPath, "blob_pdf"); 122 | fs.mkdirSync(pdfPath, { recursive: true }); 123 | pdfPath = path.join(data.pdfPath, "hiprint"); 124 | fs.mkdirSync(pdfPath, { recursive: true }); 125 | } catch { 126 | dialog.showMessageBox(SET_WINDOW, { 127 | type: "error", 128 | title: "提示", 129 | message: "pdf 保存路径无法写入数据,请重新设置!", 130 | buttons: ["确定"], 131 | noLink: true, 132 | }); 133 | return; 134 | } 135 | try { 136 | fs.accessSync(data.logPath, fs.constants.W_OK); 137 | } catch (err) { 138 | dialog.showMessageBox(SET_WINDOW, { 139 | type: "error", 140 | title: "提示", 141 | message: "日志保存路径无法写入数据,请重新设置!", 142 | buttons: ["确定"], 143 | noLink: true, 144 | }); 145 | return; 146 | } 147 | store.set(data); 148 | setTimeout(() => { 149 | app.relaunch(); 150 | app.exit(); 151 | }, 500); 152 | } 153 | }); 154 | } 155 | 156 | /** 157 | * @description: 渲染进程触发下载插件 158 | * @param {IpcMainEvent} event 159 | * @param {Object} data 插件版本号 160 | * @return {void} 161 | */ 162 | function downloadPlugin(event, data) { 163 | const fileList = ["vue-plugin-hiprint.js", "print-lock.css"]; 164 | Promise.all( 165 | fileList.map((url) => { 166 | return new Promise((resolve, reject) => { 167 | https.get( 168 | `https://registry.npmmirror.com/vue-plugin-hiprint/${data}/files/dist/${url}`, 169 | (res) => { 170 | let filePath = ""; 171 | if (app.isPackaged) { 172 | filePath = path.join( 173 | app.getAppPath(), 174 | "../", 175 | `plugin/${data}_${url}`, 176 | ); 177 | } else { 178 | filePath = path.join(app.getAppPath(), `plugin/${data}_${url}`); 179 | } 180 | const fileStream = fs.createWriteStream(filePath); 181 | res.pipe(fileStream); 182 | res.on("end", () => { 183 | resolve(); 184 | }); 185 | res.on("error", () => { 186 | reject(); 187 | }); 188 | }, 189 | ); 190 | }); 191 | }), 192 | ) 193 | .then(() => { 194 | dialog.showMessageBox(SET_WINDOW, { 195 | type: "info", 196 | title: "提示", 197 | message: "插件下载成功!", 198 | buttons: ["确定"], 199 | noLink: true, 200 | }); 201 | const downloadedVersions = getDownloadedVersions(); 202 | SET_WINDOW.webContents.send("downloadedVersions", downloadedVersions); 203 | }) 204 | .catch(() => { 205 | dialog.showMessageBox(SET_WINDOW, { 206 | type: "error", 207 | title: "提示", 208 | message: "插件下载失败!", 209 | buttons: ["确定"], 210 | noLink: true, 211 | }); 212 | }); 213 | } 214 | 215 | /** 216 | * @description: 渲染进程触发设置工作区大小 217 | * @param {IpcMainEvent} event 218 | * @param {Object} data {width, height[, animate]} 219 | * @return {void} 220 | */ 221 | function setContentSize(event, data) { 222 | SET_WINDOW.setContentSize(data.width, data.height, data.animate ?? true); 223 | } 224 | 225 | /** 226 | * @description: 渲染进程触发弹出消息框 227 | * @param {IpcMainEvent} event 228 | * @param {Object} data https://www.electronjs.org/zh/docs/latest/api/dialog#dialogshowmessageboxbrowserwindow-options 229 | * @return {void} 230 | */ 231 | function showMessageBox(event, data) { 232 | dialog.showMessageBox(SET_WINDOW, { noLink: true, ...data }); 233 | } 234 | 235 | /** 236 | * @description: 渲染进程触发选择目录 237 | * @param {IpcMainEvent} event 238 | * @param {Object} data https://www.electronjs.org/zh/docs/latest/api/dialog#dialogshowopendialogbrowserwindow-options 239 | * @return {void} 240 | */ 241 | function showOpenDialog(event, data) { 242 | dialog.showOpenDialog(SET_WINDOW, data).then((result) => { 243 | if (!result.canceled) { 244 | try { 245 | fs.accessSync(result.filePaths[0], fs.constants.W_OK); 246 | } catch { 247 | dialog.showMessageBox(SET_WINDOW, { 248 | type: "error", 249 | title: "提示", 250 | message: "路径无法写入,请重新选择!", 251 | buttons: ["确定"], 252 | noLink: true, 253 | }); 254 | result.canceled = true; 255 | } 256 | } 257 | event.reply("openDialog", result); 258 | }); 259 | } 260 | 261 | /** 262 | * @description: 渲染进程触发打开目录 263 | * @param {IpcMainEvent} event 264 | * @param {Object} data 目录路径 265 | * @return {void} 266 | */ 267 | function openDirectory(event, data) { 268 | shell.openPath(data); 269 | } 270 | 271 | /** 272 | * @description: 渲染进程触发测试连接中转服务 273 | * @param {IpcMainEvent} event 274 | * @param {Object} data {url, token} 275 | * @return {void} 276 | */ 277 | function testTransit(event, data) { 278 | const { io } = require("socket.io-client"); 279 | const socket = io(data.url, { 280 | transports: ["websocket"], 281 | reconnection: false, // 关闭自动重连 282 | query: { 283 | test: true, // 标识为测试连通性 284 | }, 285 | auth: { 286 | token: data.token, // 身份令牌 287 | }, 288 | }); 289 | 290 | // 连接错误 291 | socket.on("connect_error", (err) => { 292 | dialog.showMessageBox(SET_WINDOW, { 293 | type: "error", 294 | title: "提示", 295 | message: `${err.message},请检查设置!`, 296 | buttons: ["确定"], 297 | noLink: true, 298 | }); 299 | socket.close(); 300 | }); 301 | 302 | // 连接成功 303 | socket.on("connect", () => { 304 | dialog.showMessageBox(SET_WINDOW, { 305 | type: "info", 306 | title: "提示", 307 | message: "连接成功!", 308 | buttons: ["确定"], 309 | noLink: true, 310 | }); 311 | }); 312 | 313 | // 中转服务信息 314 | socket.on("serverInfo", (data) => { 315 | // TODO: 根据服务器返回信息判断服务器是否满足连接条件 316 | // { 317 | // version: '0.0.4', // 中转服务版本号 318 | // currentClients: 1, // 当前 token client 连接数 319 | // allClients: 1, // 所有 token client 连接数 320 | // webClients: 1, // web client 连接数 321 | // allWebClients: 1, // 所有 web client 连接数 322 | // totalmem: 17179869184, // 总内存 323 | // freemem: 94961664, // 可用内存 324 | // } 325 | 326 | console.log(data); 327 | // 关闭测试连接 328 | socket.close(); 329 | }); 330 | } 331 | 332 | /** 333 | * @description: 关闭设置窗口 334 | * @return {void} 335 | */ 336 | function closeSetWindow() { 337 | SET_WINDOW && SET_WINDOW.close(); 338 | } 339 | 340 | /** 341 | * @description: 绑定设置窗口事件 342 | * @return {void} 343 | */ 344 | function initSetEvent() { 345 | ipcMain.on("setConfig", setConfig); 346 | ipcMain.on("setContentSize", setContentSize); 347 | ipcMain.on("showMessageBox", showMessageBox); 348 | ipcMain.on("showOpenDialog", showOpenDialog); 349 | ipcMain.on("openDirectory", openDirectory); 350 | ipcMain.on("testTransit", testTransit); 351 | ipcMain.on("closeSetWindow", closeSetWindow); 352 | ipcMain.on("downloadPlugin", downloadPlugin); 353 | ipcMain.on("getPrintersList", getPrintersList); 354 | } 355 | 356 | /** 357 | * @description: 移除所有事件 358 | * @return {void} 359 | */ 360 | function removeEvent() { 361 | ipcMain.removeListener("setConfig", setConfig); 362 | ipcMain.removeListener("setContentSize", setContentSize); 363 | ipcMain.removeListener("showMessageBox", showMessageBox); 364 | ipcMain.removeListener("showOpenDialog", showOpenDialog); 365 | ipcMain.removeListener("openDirectory", openDirectory); 366 | ipcMain.removeListener("testTransit", testTransit); 367 | ipcMain.removeListener("closeSetWindow", closeSetWindow); 368 | ipcMain.removeListener("downloadPlugin", downloadPlugin); 369 | ipcMain.removeListener("getPrintersList", getPrintersList); 370 | SET_WINDOW = null; 371 | } 372 | 373 | function getDownloadedVersions() { 374 | let pluginDir = path.join(app.getAppPath(), "plugin"); 375 | if (app.isPackaged) { 376 | pluginDir = path.join(app.getAppPath(), "../", "plugin"); 377 | } 378 | if (!fs.existsSync(pluginDir)) { 379 | return []; 380 | } 381 | return fs 382 | .readdirSync(pluginDir) 383 | .filter((file) => file.endsWith(".js")) // 假设插件文件以 .js 结尾 384 | .map((file) => file.split("_")[0]); // 提取版本号 385 | } 386 | 387 | /** 388 | * @description: 获取打印机列表并发送给渲染进程 389 | * @param {IpcMainEvent} event 390 | * @return {void} 391 | */ 392 | async function getPrintersList(event) { 393 | try { 394 | const printers = await SET_WINDOW.webContents.getPrintersAsync(); 395 | let list = printers.map((item) => { 396 | return { value: item.name }; 397 | }); 398 | SET_WINDOW.webContents.send("getPrintersList", list); 399 | } catch (error) { 400 | console.error("获取打印机列表失败:", error); 401 | SET_WINDOW.webContents.send("getPrintersList", []); 402 | } 403 | } 404 | 405 | module.exports = async () => { 406 | // 创建设置窗口 407 | await createSetWindow(); 408 | }; 409 | -------------------------------------------------------------------------------- /assets/printLog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 打印日志 7 | 8 | 9 | 10 | 11 | 12 | 49 | 50 | 51 |
52 | 59 |
60 | 93 |
94 | 99 | 搜索 100 | 101 | 106 | 清空 107 | 108 |
109 |
110 |
111 | 112 | 119 | 124 | 133 | 134 | 135 | 148 |
149 | 150 | 423 | 424 | 425 | -------------------------------------------------------------------------------- /src/render.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const { app, BrowserWindow, ipcMain, dialog, screen } = require("electron"); 5 | const path = require("path"); 6 | const { Jimp } = require("jimp"); 7 | const dayjs = require("dayjs"); 8 | 9 | const { store, getCurrentPrintStatusByName } = require("../tools/utils"); 10 | const db = require("../tools/database"); 11 | 12 | // 这是 1920 * 1080 屏幕常规工作区域尺寸 13 | let windowWorkArea = { 14 | width: 1920, 15 | height: 1032, 16 | }; 17 | 18 | /** 19 | * @typedef {Object} CapturePageData 截图数据 20 | * @property {string} clientType socket 客户端类型 'local' | 'transit' 21 | * @property {string} socketId socket id 22 | * @property {string} replyId 中转回复 id 23 | * @property {string} templateId 模版 id 24 | * @property {string} taskId 任务 id 25 | * @property {number} x x坐标 26 | * @property {number} y y坐标 27 | * @property {number} width 宽度 28 | * @property {number} height 高度 29 | */ 30 | 31 | /** 32 | * @typedef {object} PageSize PDF 尺寸 33 | * @property {number} width 宽度 34 | * @property {number} height 高度 35 | */ 36 | 37 | /** 38 | * @typedef {object} Margins 边距 39 | * @property {number} top 上边距 40 | * @property {number} bottom 下边距 41 | * @property {number} left 左边距 42 | * @property {number} right 右边距 43 | */ 44 | 45 | /** 46 | * @typedef {Object} PrintToPDFData 47 | * @property {string} clientType socket 客户端类型 'local' | 'transit' 48 | * @property {string} socketId socket id 49 | * @property {string} replyId 中转回复 id 50 | * @property {string} templateId 模版 id 51 | * @property {string} taskId 任务 id 52 | * @property {boolean} landscape 网页是否应以横向模式打印 默认 true 53 | * @property {boolean} displayHeaderFooter 是否显示页眉和页脚 默认 false 54 | * @property {boolean} printBackground 是否打印背景图形 默认 false 55 | * @property {number} scale 网页渲染的比例 默认 1 56 | * @property {string | PageSize} pageSize 指定生成的 PDF 的页面大小 默认 Letter 57 | * @property {string | Margins} margins 边距 58 | * @property {string} pageRanges 要打印的页面范围 例如 '1-5, 8, 11-13' 59 | * @property {string} headerTemplate 打印标题的 HTML 模板 60 | * @property {string} footerTemplate 打印页脚的 HTML 模板 61 | * @property {number} preferCSSPageSize 是否优先使用 css 定义的页面大小 62 | */ 63 | 64 | /** 65 | * @description: 创建打印窗口 66 | * @return {BrowserWindow} RENDER_WINDOW 打印窗口 67 | */ 68 | async function createRenderWindow() { 69 | const windowOptions = { 70 | width: 300, // 窗口宽度 71 | height: 500, // 窗口高度 72 | show: false, // 不显示 73 | alwaysOnTop: true, 74 | webPreferences: { 75 | contextIsolation: false, // 设置此项为false后,才可在渲染进程中使用electron api 76 | nodeIntegration: true, 77 | }, 78 | // 为窗口设置背景色可能优化字体模糊问题 79 | // https://www.electronjs.org/zh/docs/latest/faq#文字看起来很模糊这是什么原因造成的怎么解决这个问题呢 80 | backgroundColor: "#fff", 81 | }; 82 | 83 | // 创建打印窗口 84 | RENDER_WINDOW = new BrowserWindow(windowOptions); 85 | 86 | // 加载打印渲染进程页面 87 | let printHtml = path.join("file://", app.getAppPath(), "/assets/render.html"); 88 | RENDER_WINDOW.webContents.loadURL(printHtml); 89 | 90 | RENDER_WINDOW.on("ready-to-show", () => { 91 | const windowBounds = RENDER_WINDOW.getBounds(); 92 | const display = screen.getDisplayNearestPoint({ 93 | x: windowBounds.x, 94 | y: windowBounds.y, 95 | }); 96 | 97 | windowWorkArea = display.workAreaSize; 98 | 99 | // 未打包时打开开发者工具 100 | if (!app.isPackaged) { 101 | // !打开开发者模式时,窗口尺寸变化将在右上角显示窗口尺寸,对 capturePage 功能会造成一定的误解 102 | RENDER_WINDOW.webContents.openDevTools(); 103 | } 104 | }); 105 | 106 | // 绑定窗口事件 107 | initEvent(); 108 | 109 | RENDER_WINDOW.on("closed", removeEvent); 110 | 111 | return RENDER_WINDOW; 112 | } 113 | 114 | /** 115 | * @description: 截图 116 | * @param {IpcMainEvent} event 事件 117 | * @param {CapturePageData} data 截图数据 118 | */ 119 | async function capturePage(event, data) { 120 | let socket = null; 121 | if (data.clientType === "local") { 122 | socket = SOCKET_SERVER.sockets.sockets.get(data.socketId); 123 | } else { 124 | socket = SOCKET_CLIENT; 125 | } 126 | // !在 win 上窗口可以超出屏幕尺寸,直接使用 webContents.capturePage api 截图没有问题 127 | // !在 mac 上窗口不能超出屏幕尺寸,需要一点儿点儿截图最后拼接 128 | try { 129 | let images = []; 130 | 131 | // 打印元素宽度 132 | const printWidth = Math.ceil(data.width); 133 | // 打印元素高度 134 | const printHeight = Math.ceil(data.height); 135 | 136 | // 窗口内容区域宽度 137 | let innerWidth = await RENDER_WINDOW.webContents.executeJavaScript( 138 | "window.innerWidth", 139 | ); 140 | innerWidth = Math.ceil(innerWidth); 141 | 142 | // 窗口内容区域高度 143 | let innerHeight = await RENDER_WINDOW.webContents.executeJavaScript( 144 | "window.innerHeight", 145 | ); 146 | innerHeight = Math.ceil(innerHeight); 147 | 148 | // 元素高度与窗口高度获取最小值,如果窗口比元素小 149 | const height = Math.min(printHeight, innerHeight); 150 | 151 | // 将窗口移至屏幕左上角 152 | RENDER_WINDOW.setBounds({ 153 | x: 0, 154 | y: 0, 155 | }); 156 | // 设置内容区大小 157 | RENDER_WINDOW.setContentSize(printWidth, height, false); 158 | 159 | const captureOptions = { 160 | x: 0, 161 | y: 0, 162 | width: printWidth, 163 | height, 164 | }; 165 | 166 | // 截取首图 167 | const nativeImage = await RENDER_WINDOW.webContents.capturePage( 168 | captureOptions, 169 | ); 170 | // 有说法 toJPEG 性能比 toPNG 更高 171 | images.push(nativeImage.resize({ width: printWidth }).toJPEG(100)); 172 | 173 | // 截取剩余图 174 | for (let offset = height; offset < data.height; offset += height) { 175 | await RENDER_WINDOW.webContents.executeJavaScript( 176 | `window.scrollTo(0, ${offset})`, 177 | false, 178 | ); 179 | 180 | // 等待滚动完成 181 | await new Promise((resolve) => setTimeout(resolve, 50)); 182 | 183 | // 计算最后一页需要截取的高度 184 | captureOptions.height = offset > height ? offset - height : height; 185 | 186 | const image = await RENDER_WINDOW.webContents.capturePage(captureOptions); 187 | // 有说法 toJPEG 性能比 toPNG 更高 188 | images.push(image.resize({ width: printWidth }).toJPEG(100)); 189 | } 190 | 191 | // 使用 jimp 拼接图片 192 | const result = new Jimp({ width: printWidth, height: printHeight }); 193 | 194 | for (let idx = 0; idx < images.length; idx++) { 195 | const jimpImg = await Jimp.fromBuffer(images[idx]); 196 | result.composite(jimpImg, 0, idx * height); 197 | } 198 | 199 | result 200 | .getBuffer("image/jpeg", { 201 | quality: 100, 202 | }) 203 | .then((buffer) => { 204 | // 未打包调试模式下将图片保存到桌面 205 | if (!app.isPackaged) { 206 | fs.writeFile( 207 | path.join( 208 | app.getPath("desktop"), 209 | `capture_${dayjs().format("YYYY-MM-DD HH_mm_ss")}.png`, 210 | ), 211 | buffer, 212 | () => {}, 213 | ); 214 | } 215 | console.log( 216 | `${data.replyId ? "中转服务" : "插件端"} ${socket.id} 模版 【${ 217 | data.templateId 218 | }】 获取 png 成功`, 219 | ); 220 | socket.emit("render-jpeg-success", { 221 | msg: `获取 jpeg 成功`, 222 | templateId: data.templateId, 223 | buffer, 224 | replyId: data.replyId, 225 | }); 226 | }); 227 | } catch (error) { 228 | console.log( 229 | `${data.replyId ? "中转服务" : "插件端"} ${socket.id} 模版 【${ 230 | data.templateId 231 | }】 获取 png 失败`, 232 | ); 233 | socket && 234 | socket.emit("render-jpeg-error", { 235 | msg: `获取 png 失败`, 236 | templateId: data.templateId, 237 | replyId: data.replyId, 238 | }); 239 | } finally { 240 | RENDER_RUNNER_DONE[data.taskId](); 241 | delete RENDER_RUNNER_DONE[data.taskId]; 242 | } 243 | } 244 | 245 | /** 246 | * @description: 打印到PDF 247 | * @param {IpcMainEvent} event 事件 248 | * @param {PrintToPDFData} data 打印数据 249 | */ 250 | function printToPDF(event, data) { 251 | let socket = null; 252 | if (data.clientType === "local") { 253 | socket = SOCKET_SERVER.sockets.sockets.get(data.socketId); 254 | } else { 255 | socket = SOCKET_CLIENT; 256 | } 257 | RENDER_WINDOW.webContents 258 | .printToPDF({ 259 | landscape: data.landscape ?? false, // 横向打印 260 | displayHeaderFooter: data.displayHeaderFooter ?? false, // 显示页眉页脚 261 | printBackground: data.printBackground ?? true, // 打印背景色 262 | scale: data.scale ?? 1, // 渲染比例 默认 1 263 | pageSize: data.pageSize, 264 | margins: data.margins, // 边距 265 | pageRanges: data.pageRanges, // 打印页数范围 266 | headerTemplate: data.headerTemplate, // 页头模板 (html) 267 | footerTemplate: data.footerTemplate, // 页脚模板 (html) 268 | preferCSSPageSize: data.preferCSSPageSize ?? false, 269 | }) 270 | .then((buffer) => { 271 | // 未打包调试模式下将pdf保存到桌面 272 | if (!app.isPackaged) { 273 | fs.writeFile( 274 | path.join( 275 | app.getPath("desktop"), 276 | `pdf_${dayjs().format("YYYY-MM-DD HH_mm_ss")}.pdf`, 277 | ), 278 | buffer, 279 | () => {}, 280 | ); 281 | } 282 | socket.emit("render-pdf-success", { 283 | templateId: data.templateId, 284 | buffer, 285 | replyId: data.replyId, 286 | }); 287 | }) 288 | .catch((error) => { 289 | console.log( 290 | `${data.replyId ? "中转服务" : "插件端"} ${socket.id} 模版 【${ 291 | data.templateId 292 | }】 获取 pdf 失败`, 293 | ); 294 | socket && 295 | socket.emit("render-pdf-error", { 296 | msg: `获取 pdf 失败`, 297 | templateId: data.templateId, 298 | replyId: data.replyId, 299 | }); 300 | }) 301 | .finally(() => { 302 | RENDER_RUNNER_DONE[data.taskId](); 303 | delete RENDER_RUNNER_DONE[data.taskId]; 304 | }); 305 | } 306 | 307 | /** 308 | * @description: 打印 309 | * @param {IpcMainEvent} event 事件 310 | * @param {object} data 打印数据 311 | * 312 | * */ 313 | async function printFun(event, data) { 314 | let socket = null; 315 | if (data.clientType === "local") { 316 | socket = SOCKET_SERVER.sockets.sockets.get(data.socketId); 317 | } else { 318 | socket = SOCKET_CLIENT; 319 | } 320 | const printers = await RENDER_WINDOW.webContents.getPrintersAsync(); 321 | let havePrinter = false; 322 | let defaultPrinter = data.printer || store.get("defaultPrinter", ""); 323 | let printerError = false; 324 | printers.forEach((element) => { 325 | // 获取默认打印机 326 | if ( 327 | element.isDefault && 328 | (defaultPrinter == "" || defaultPrinter == void 0) 329 | ) { 330 | defaultPrinter = element.name; 331 | } 332 | // 判断打印机是否存在 333 | if (element.name === defaultPrinter) { 334 | // todo: 打印机状态对照表 335 | // win32: https://learn.microsoft.com/en-us/windows/win32/printdocs/printer-info-2 336 | // cups: https://www.cups.org/doc/cupspm.html#ipp_status_e 337 | if (process.platform === "win32") { 338 | // 512 忙(Busy) 339 | // 1024 正在打印(Printing) 340 | if (![0, 512, 1024].includes(element.status)) { 341 | printerError = true; 342 | } 343 | } else { 344 | if (element.status != 3) { 345 | printerError = true; 346 | } 347 | } 348 | havePrinter = true; 349 | } 350 | }); 351 | if (printerError) { 352 | const { StatusMsg } = getCurrentPrintStatusByName(defaultPrinter); 353 | console.log( 354 | `${data.replyId ? "中转服务" : "插件端"} ${socket.id} 模板 【${ 355 | data.templateId 356 | }】 打印失败,打印机异常,打印机:${ 357 | data.printer 358 | },打印机状态:${StatusMsg}`, 359 | ); 360 | socket && 361 | socket.emit("render-print-error", { 362 | msg: data.printer + "打印机异常", 363 | templateId: data.templateId, 364 | replyId: data.replyId, 365 | }); 366 | // 通过 taskMap 调用 task done 回调 367 | PRINT_RUNNER_DONE[data.taskId](); 368 | delete PRINT_RUNNER_DONE[data.taskId]; 369 | return; 370 | } 371 | let deviceName = defaultPrinter; 372 | 373 | const logPrintResult = (status, errorMessage = "") => { 374 | db.run( 375 | `INSERT INTO print_logs (socketId, clientType, printer, templateId, data, pageNum, status, rePrintAble, errorMessage) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, 376 | [ 377 | socket?.id, 378 | data.clientType, 379 | deviceName, 380 | data.templateId, 381 | JSON.stringify(data), 382 | data.pageNum, 383 | status, 384 | data.rePrintAble ?? 1, 385 | errorMessage, 386 | ], 387 | (err) => { 388 | if (err) { 389 | console.error("Failed to log print result", err); 390 | } 391 | }, 392 | ); 393 | }; 394 | 395 | // 打印 详见https://www.electronjs.org/zh/docs/latest/api/web-contents 396 | RENDER_WINDOW.webContents.print( 397 | { 398 | silent: data.silent ?? true, // 静默打印 399 | printBackground: data.printBackground ?? true, // 是否打印背景 400 | deviceName: deviceName, // 打印机名称 401 | color: data.color ?? true, // 是否打印颜色 402 | margins: data.margins ?? { 403 | marginType: "none", 404 | }, // 边距 405 | landscape: data.landscape ?? false, // 是否横向打印 406 | scaleFactor: data.scaleFactor ?? 100, // 打印缩放比例 407 | pagesPerSheet: data.pagesPerSheet ?? 1, // 每张纸的页数 408 | collate: data.collate ?? true, // 是否排序 409 | copies: data.copies ?? 1, // 打印份数 410 | pageRanges: data.pageRanges ?? {}, // 打印页数 411 | duplexMode: data.duplexMode, // 打印模式 simplex,shortEdge,longEdge 412 | dpi: data.dpi ?? 300, // 打印机DPI 413 | header: data.header, // 打印头 414 | footer: data.footer, // 打印尾 415 | pageSize: data.pageSize, // 打印纸张 416 | }, 417 | (success, failureReason) => { 418 | if (socket) { 419 | if (success) { 420 | console.log( 421 | `${data.replyId ? "中转服务" : "插件端"} ${socket.id} 模板 【${ 422 | data.templateId 423 | }】 打印成功,打印类型 JSON,打印机:${deviceName},页数:${ 424 | data.pageNum 425 | }`, 426 | ); 427 | const result = { 428 | msg: "打印成功", 429 | templateId: data.templateId, 430 | replyId: data.replyId, 431 | }; 432 | logPrintResult("success"); 433 | socket.emit("render-print-success", result); 434 | } else { 435 | console.log( 436 | `${data.replyId ? "中转服务" : "插件端"} ${socket.id} 模板 【${ 437 | data.templateId 438 | }】 打印失败,打印类型 JSON,打印机:${deviceName},原因:${failureReason}`, 439 | ); 440 | logPrintResult("failed", failureReason); 441 | socket.emit("render-print-error", { 442 | msg: failureReason, 443 | templateId: data.templateId, 444 | replyId: data.replyId, 445 | }); 446 | } 447 | } 448 | // 通过 taskMap 调用 task done 回调 449 | RENDER_RUNNER_DONE[data.taskId](); 450 | // 删除 task 451 | delete RENDER_RUNNER_DONE[data.taskId]; 452 | }, 453 | ); 454 | } 455 | 456 | /** 457 | * @description: 渲染进程触发弹出消息框 458 | * @param {IpcMainEvent} event 459 | * @param {Object} data https://www.electronjs.org/zh/docs/latest/api/dialog#dialogshowmessageboxbrowserwindow-options 460 | * @return {void} 461 | */ 462 | function showMessageBox(event, data) { 463 | dialog.showMessageBox(SET_WINDOW, { noLink: true, ...data }); 464 | } 465 | 466 | /** 467 | * @description: 初始化事件 468 | */ 469 | function initEvent() { 470 | ipcMain.on("capturePage", capturePage); 471 | ipcMain.on("printToPDF", printToPDF); 472 | ipcMain.on("print", printFun); 473 | ipcMain.on("showMessageBox", showMessageBox); 474 | } 475 | 476 | /** 477 | * @description: 移除事件 478 | * @return {void} 479 | */ 480 | function removeEvent() { 481 | ipcMain.removeListener("capturePage", capturePage); 482 | ipcMain.removeListener("printToPDF", printToPDF); 483 | ipcMain.removeListener("print", printFun); 484 | ipcMain.removeListener("showMessageBox", showMessageBox); 485 | RENDER_WINDOW = null; 486 | } 487 | 488 | module.exports = async () => { 489 | // 创建渲染窗口 490 | await createRenderWindow(); 491 | }; 492 | -------------------------------------------------------------------------------- /src/print.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { app, BrowserWindow, ipcMain } = require("electron"); 4 | const path = require("path"); 5 | const os = require("os"); 6 | const fs = require("fs"); 7 | const { printPdf, printPdfBlob } = require("./pdf-print"); 8 | const { store, getCurrentPrintStatusByName } = require("../tools/utils"); 9 | const db = require("../tools/database"); 10 | const dayjs = require("dayjs"); 11 | const { v7: uuidv7 } = require("uuid"); 12 | 13 | /** 14 | * @description: 创建打印窗口 15 | * @return {BrowserWindow} PRINT_WINDOW 打印窗口 16 | */ 17 | async function createPrintWindow() { 18 | const windowOptions = { 19 | width: 100, // 窗口宽度 20 | height: 100, // 窗口高度 21 | show: false, // 不显示 22 | webPreferences: { 23 | contextIsolation: false, // 设置此项为false后,才可在渲染进程中使用electron api 24 | nodeIntegration: true, 25 | }, 26 | // 为窗口设置背景色可能优化字体模糊问题 27 | // https://www.electronjs.org/zh/docs/latest/faq#文字看起来很模糊这是什么原因造成的怎么解决这个问题呢 28 | backgroundColor: "#fff", 29 | }; 30 | 31 | // 创建打印窗口 32 | PRINT_WINDOW = new BrowserWindow(windowOptions); 33 | 34 | // 加载打印渲染进程页面 35 | let printHtml = path.join("file://", app.getAppPath(), "/assets/print.html"); 36 | PRINT_WINDOW.webContents.loadURL(printHtml); 37 | 38 | // 未打包时打开开发者工具 39 | // if (!app.isPackaged) { 40 | // PRINT_WINDOW.webContents.openDevTools(); 41 | // } 42 | 43 | // 绑定窗口事件 44 | initPrintEvent(); 45 | 46 | return PRINT_WINDOW; 47 | } 48 | 49 | /** 50 | * @description: 绑定打印窗口事件 51 | * @return {Void} 52 | */ 53 | function initPrintEvent() { 54 | ipcMain.on("do", async (event, data) => { 55 | let socket = null; 56 | if (data.clientType === "local") { 57 | socket = SOCKET_SERVER.sockets.sockets.get(data.socketId); 58 | } else { 59 | socket = SOCKET_CLIENT; 60 | } 61 | const printers = await PRINT_WINDOW.webContents.getPrintersAsync(); 62 | let havePrinter = false; 63 | let defaultPrinter = data.printer || store.get("defaultPrinter", ""); 64 | let printerError = false; 65 | printers.forEach((element) => { 66 | // 获取默认打印机 67 | if ( 68 | element.isDefault && 69 | (defaultPrinter == "" || defaultPrinter == void 0) 70 | ) { 71 | defaultPrinter = element.name; 72 | } 73 | // 判断打印机是否存在 74 | if (element.name === defaultPrinter) { 75 | // todo: 打印机状态对照表 76 | // win32: https://learn.microsoft.com/en-us/windows/win32/printdocs/printer-info-2 77 | // cups: https://www.cups.org/doc/cupspm.html#ipp_status_e 78 | if (process.platform === "win32") { 79 | if (element.status != 0) { 80 | printerError = true; 81 | } 82 | } else { 83 | if (element.status != 3) { 84 | printerError = true; 85 | } 86 | } 87 | havePrinter = true; 88 | } 89 | }); 90 | if (printerError) { 91 | const { StatusMsg } = getCurrentPrintStatusByName(defaultPrinter); 92 | console.log( 93 | `${data.replyId ? "中转服务" : "插件端"} ${socket.id} 模板 【${ 94 | data.templateId 95 | }】 打印失败,打印机异常,打印机:${defaultPrinter}, 打印机状态:${StatusMsg}`, 96 | ); 97 | socket && 98 | socket.emit("error", { 99 | msg: data.printer + "打印机异常", 100 | templateId: data.templateId, 101 | replyId: data.replyId, 102 | }); 103 | if (data.taskId) { 104 | // 通过 taskMap 调用 task done 回调 105 | PRINT_RUNNER_DONE[data.taskId](); 106 | delete PRINT_RUNNER_DONE[data.taskId]; 107 | } 108 | MAIN_WINDOW.webContents.send("printTask", PRINT_RUNNER.isBusy()); 109 | return; 110 | } 111 | let deviceName = defaultPrinter; 112 | 113 | const logPrintResult = (status, errorMessage = "") => { 114 | db.run( 115 | `INSERT INTO print_logs (socketId, clientType, printer, templateId, data, pageNum, status, rePrintAble, errorMessage) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, 116 | [ 117 | socket?.id, 118 | data.clientType, 119 | deviceName, 120 | data.templateId, 121 | JSON.stringify(data), 122 | data.pageNum, 123 | status, 124 | data.rePrintAble ?? 1, 125 | errorMessage, 126 | ], 127 | (err) => { 128 | if (err) { 129 | console.error("Failed to log print result", err); 130 | } 131 | }, 132 | ); 133 | }; 134 | 135 | // pdf 打印 136 | let isPdf = data.type && `${data.type}`.toLowerCase() === "pdf"; 137 | if (isPdf) { 138 | const pdfPath = path.join( 139 | store.get("pdfPath") || os.tmpdir(), 140 | "hiprint", 141 | dayjs().format(`YYYY_MM_DD HH_mm_ss_`) + `${uuidv7()}.pdf`, 142 | ); 143 | fs.mkdirSync(path.dirname(pdfPath), { 144 | recursive: true, 145 | }); 146 | PRINT_WINDOW.webContents 147 | .printToPDF({ 148 | landscape: data.landscape ?? false, // 横向打印 149 | displayHeaderFooter: data.displayHeaderFooter ?? false, // 显示页眉页脚 150 | printBackground: data.printBackground ?? true, // 打印背景色 151 | scale: data.scale ?? 1, // 渲染比例 默认 1 152 | pageSize: data.pageSize, 153 | margins: data.margins ?? { 154 | marginType: "none", 155 | }, // 边距 156 | pageRanges: data.pageRanges, // 打印页数范围 157 | headerTemplate: data.headerTemplate, // 页头模板 (html) 158 | footerTemplate: data.footerTemplate, // 页脚模板 (html) 159 | preferCSSPageSize: data.preferCSSPageSize ?? false, 160 | }) 161 | .then((pdfData) => { 162 | fs.writeFileSync(pdfPath, pdfData); 163 | printPdf(pdfPath, deviceName, data) 164 | .then(() => { 165 | console.log( 166 | `${data.replyId ? "中转服务" : "插件端"} ${socket.id} 模板 【${ 167 | data.templateId 168 | }】 打印成功,打印类型:PDF,打印机:${deviceName},页数:${ 169 | data.pageNum 170 | }`, 171 | ); 172 | if (socket) { 173 | const result = { 174 | msg: "打印成功", 175 | templateId: data.templateId, 176 | replyId: data.replyId, 177 | }; 178 | socket.emit("successs", result); // 兼容 vue-plugin-hiprint 0.0.56 之前包 179 | socket.emit("success", result); 180 | } 181 | logPrintResult("success"); 182 | }) 183 | .catch((err) => { 184 | console.log( 185 | `${data.replyId ? "中转服务" : "插件端"} ${socket.id} 模板 【${ 186 | data.templateId 187 | }】 打印失败,打印类型:PDF,打印机:${deviceName},原因:${ 188 | err.message 189 | }`, 190 | ); 191 | socket && 192 | socket.emit("error", { 193 | msg: "打印失败: " + err.message, 194 | templateId: data.templateId, 195 | replyId: data.replyId, 196 | }); 197 | logPrintResult("failed", err.message); 198 | }) 199 | .finally(() => { 200 | if (data.taskId) { 201 | // 通过taskMap 调用 task done 回调 202 | PRINT_RUNNER_DONE[data.taskId](); 203 | // 删除 task 204 | delete PRINT_RUNNER_DONE[data.taskId]; 205 | } 206 | MAIN_WINDOW.webContents.send("printTask", PRINT_RUNNER.isBusy()); 207 | }); 208 | }); 209 | return; 210 | } 211 | // url_pdf 打印 212 | const isUrlPdf = data.type && `${data.type}`.toLowerCase() === "url_pdf"; 213 | if (isUrlPdf) { 214 | printPdf(data.pdf_path, deviceName, data) 215 | .then(() => { 216 | console.log( 217 | `${data.replyId ? "中转服务" : "插件端"} ${socket.id} 模板 【${ 218 | data.templateId 219 | }】 打印成功,打印类型:URL_PDF,打印机:${deviceName},页数:${ 220 | data.pageNum 221 | }`, 222 | ); 223 | if (socket) { 224 | checkPrinterStatus(deviceName, () => { 225 | const result = { 226 | msg: "打印成功", 227 | templateId: data.templateId, 228 | replyId: data.replyId, 229 | }; 230 | socket.emit("successs", result); // 兼容 vue-plugin-hiprint 0.0.56 之前包 231 | socket.emit("success", result); 232 | }); 233 | } 234 | logPrintResult("success"); 235 | }) 236 | .catch((err) => { 237 | console.log( 238 | `${data.replyId ? "中转服务" : "插件端"} ${socket.id} 模板 【${ 239 | data.templateId 240 | }】 打印失败,打印类型:URL_PDF,打印机:${deviceName},原因:${ 241 | err.message 242 | }`, 243 | ); 244 | socket && 245 | socket.emit("error", { 246 | msg: "打印失败: " + err.message, 247 | templateId: data.templateId, 248 | replyId: data.replyId, 249 | }); 250 | logPrintResult("failed", err.message); 251 | }) 252 | .finally(() => { 253 | if (data.taskId) { 254 | // 通过 taskMap 调用 task done 回调 255 | PRINT_RUNNER_DONE[data.taskId](); 256 | // 删除 task 257 | delete PRINT_RUNNER_DONE[data.taskId]; 258 | } 259 | MAIN_WINDOW.webContents.send("printTask", PRINT_RUNNER.isBusy()); 260 | }); 261 | return; 262 | } 263 | 264 | // blob_pdf 打印 - 直接接收二进制PDF数据 265 | const isBlobPdf = data.type && `${data.type}`.toLowerCase() === "blob_pdf"; 266 | if (isBlobPdf) { 267 | // 验证必要参数 268 | if (!data.pdf_blob) { 269 | const errorMsg = "blob_pdf类型打印缺少pdf_blob参数"; 270 | console.log( 271 | `${data.replyId ? "中转服务" : "插件端"} ${socket?.id} 模板 【${ 272 | data.templateId 273 | }】 打印失败,原因:${errorMsg}`, 274 | ); 275 | socket && 276 | socket.emit("error", { 277 | msg: errorMsg, 278 | templateId: data.templateId, 279 | replyId: data.replyId, 280 | }); 281 | logPrintResult("failed", errorMsg); 282 | if (data.taskId) { 283 | PRINT_RUNNER_DONE[data.taskId](); 284 | delete PRINT_RUNNER_DONE[data.taskId]; 285 | } 286 | MAIN_WINDOW.webContents.send("printTask", PRINT_RUNNER.isBusy()); 287 | return; 288 | } 289 | let pdfBlob = data.pdf_blob; 290 | delete data.pdf_blob; 291 | printPdfBlob(pdfBlob, deviceName, data) 292 | .then(() => { 293 | console.log( 294 | `${data.replyId ? "中转服务" : "插件端"} ${socket.id} 模板 【${ 295 | data.templateId 296 | }】 打印成功,打印类型:BLOB_PDF,打印机:${deviceName},页数:${ 297 | data.pageNum 298 | }`, 299 | ); 300 | if (socket) { 301 | checkPrinterStatus(deviceName, () => { 302 | const result = { 303 | msg: "打印成功", 304 | templateId: data.templateId, 305 | replyId: data.replyId, 306 | }; 307 | socket.emit("successs", result); // 兼容 vue-plugin-hiprint 0.0.56 之前包 308 | socket.emit("success", result); 309 | }); 310 | } 311 | logPrintResult("success"); 312 | }) 313 | .catch((err) => { 314 | console.log( 315 | `${data.replyId ? "中转服务" : "插件端"} ${socket.id} 模板 【${ 316 | data.templateId 317 | }】 打印失败,打印类型:BLOB_PDF,打印机:${deviceName},原因:${ 318 | err.message 319 | }`, 320 | ); 321 | socket && 322 | socket.emit("error", { 323 | msg: "打印失败: " + err.message, 324 | templateId: data.templateId, 325 | replyId: data.replyId, 326 | }); 327 | logPrintResult("failed", err.message); 328 | }) 329 | .finally(() => { 330 | if (data.taskId) { 331 | // 通过 taskMap 调用 task done 回调 332 | PRINT_RUNNER_DONE[data.taskId](); 333 | // 删除 task 334 | delete PRINT_RUNNER_DONE[data.taskId]; 335 | } 336 | MAIN_WINDOW.webContents.send("printTask", PRINT_RUNNER.isBusy()); 337 | }); 338 | return; 339 | } 340 | // 打印 详见https://www.electronjs.org/zh/docs/latest/api/web-contents 341 | PRINT_WINDOW.webContents.print( 342 | { 343 | silent: data.silent ?? true, // 静默打印 344 | printBackground: data.printBackground ?? true, // 是否打印背景 345 | deviceName: deviceName, // 打印机名称 346 | color: data.color ?? true, // 是否打印颜色 347 | margins: data.margins ?? { 348 | marginType: "none", 349 | }, // 边距 350 | landscape: data.landscape ?? false, // 是否横向打印 351 | scaleFactor: data.scaleFactor ?? 100, // 打印缩放比例 352 | pagesPerSheet: data.pagesPerSheet ?? 1, // 每张纸的页数 353 | collate: data.collate ?? true, // 是否排序 354 | copies: data.copies ?? 1, // 打印份数 355 | pageRanges: data.pageRanges ?? {}, // 打印页数 356 | duplexMode: data.duplexMode, // 打印模式 simplex,shortEdge,longEdge 357 | dpi: data.dpi ?? 300, // 打印机DPI 358 | header: data.header, // 打印头 359 | footer: data.footer, // 打印尾 360 | pageSize: data.pageSize, // 打印纸张 361 | }, 362 | (success, failureReason) => { 363 | if (success) { 364 | console.log( 365 | `${data.replyId ? "中转服务" : "插件端"} ${socket?.id} 模板 【${ 366 | data.templateId 367 | }】 打印成功,打印类型 HTML,打印机:${deviceName},页数:${ 368 | data.pageNum 369 | }`, 370 | ); 371 | logPrintResult("success"); 372 | } else { 373 | console.log( 374 | `${data.replyId ? "中转服务" : "插件端"} ${socket?.id} 模板 【${ 375 | data.templateId 376 | }】 打印失败,打印类型 HTML,打印机:${deviceName},原因:${failureReason}`, 377 | ); 378 | logPrintResult("failed", failureReason); 379 | } 380 | if (socket) { 381 | if (success) { 382 | const result = { 383 | msg: "打印成功", 384 | templateId: data.templateId, 385 | replyId: data.replyId, 386 | }; 387 | socket.emit("successs", result); // 兼容 vue-plugin-hiprint 0.0.56 之前包 388 | socket.emit("success", result); 389 | } else { 390 | socket.emit("error", { 391 | msg: failureReason, 392 | templateId: data.templateId, 393 | replyId: data.replyId, 394 | }); 395 | } 396 | } 397 | // 通过 taskMap 调用 task done 回调 398 | if (data.taskId) { 399 | PRINT_RUNNER_DONE[data.taskId](); 400 | // 删除 task 401 | delete PRINT_RUNNER_DONE[data.taskId]; 402 | } 403 | MAIN_WINDOW.webContents.send("printTask", PRINT_RUNNER.isBusy()); 404 | }, 405 | ); 406 | }); 407 | } 408 | 409 | function checkPrinterStatus(deviceName, callback) { 410 | const intervalId = setInterval(() => { 411 | PRINT_WINDOW.webContents 412 | .getPrintersAsync() 413 | .then((printers) => { 414 | const printer = printers.find((printer) => printer.name === deviceName); 415 | console.log(`current printer: ${JSON.stringify(printer)}`); 416 | const ISCAN_STATUS = process.platform === "win32" ? 0 : 3; 417 | if (printer && printer.status === ISCAN_STATUS) { 418 | callback && callback(); 419 | clearInterval(intervalId); // Stop polling when status is 0 420 | console.log( 421 | `Printer ${deviceName} is now ready (status: ${ISCAN_STATUS})`, 422 | ); 423 | // You can add any additional logic here for when the printer is ready 424 | } 425 | }) 426 | .catch((error) => { 427 | clearInterval(intervalId); // Also clear interval on error 428 | console.log(`Error checking printer status: ${error}`); 429 | }); 430 | }, 1000); // Check every 1 second (adjust interval as needed) 431 | 432 | return intervalId; // Return the interval ID in case you need to cancel it externally 433 | } 434 | 435 | module.exports = async () => { 436 | // 创建打印窗口 437 | await createPrintWindow(); 438 | }; 439 | -------------------------------------------------------------------------------- /assets/set.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 设置 12 | 13 | 14 | 15 | 16 | 17 | 18 | 56 | 57 | 58 | 59 |
60 | 61 | 64 | 65 | 72 | 73 | 125 | 126 | 127 | 128 | 129 | 130 | 应用 131 | 132 | 133 | 134 | 135 | 关闭 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 |
144 | 693 | 694 | 695 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # electron-hiprint 2 | 3 | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/CcSimple/electron-hiprint) 4 | 5 |
6 | 7 | ![logo](./build/icons/100x100.png) 8 | 9 |
10 | 11 | 该项目是为 [vue-plugin-hiprint](https://github.com/CcSimple/vue-plugin-hiprint) 配套开发的静默打印解决方案。我们发现部分使用此项目的开发者拥有自定义的设计器或渲染方案,或者仅需要静默打印一段 HTML、PDF。如果您也有类似需求,electron-hiprint 将是您的理想选择。 12 | 13 |
14 | 15 | ![主界面](./res/electron-hiprint.png) 16 | 17 |
18 | 19 | ## 快速开始 20 | 21 | #### 下载发行版 22 | 23 | 1. [github releases](https://github.com/CcSimple/electron-hiprint/releases) 24 | 2. [gitee releases](https://gitee.com/CcSimple/electron-hiprint/releases) 25 | 26 | #### 本地调试与打包 27 | 28 | ```shell 29 | git clone https://github.com/CcSimple/electron-hiprint.git 30 | # or 31 | git clone https://gitee.com/CcSimple/electron-hiprint.git 32 | 33 | # init 34 | cd electron-hiprint 35 | npm install 36 | 37 | # 调试预览 38 | npm run start 39 | 40 | # 打包 win x64,其余平台版本详情见 package.json 41 | npm run build-w-64 42 | ``` 43 | 44 | ## 推荐的 Web 打印设计渲染插件 45 | 46 | [vue-plugin-hiprint](https://github.com/CcSimple/vue-plugin-hiprint.git) 47 | 48 | ## 打印原理 49 | 50 | 1. **客户端服务**:通过 socket.io (默认端口 17521)提供服务。 51 | 52 | - 使用 `socket.io-client@4.x` 连接: `http://localhost:17521` 53 | 54 | 2. **发送打印数据**:通过 `socket.emit` 方法发送打印数据 55 | 56 | - 示例: 57 | 58 | ```js 59 | socket.emit("news", { html, templateId, printer, pageSize }); 60 | ``` 61 | 62 | - 参数说明: 63 | - `html`: HTML 字符串。 64 | - `templateId`: 用于标识成功或失败回调的 ID。 65 | - `printer`: 打印机名称。 66 | - `pageSize`: 打印纸张大小。 67 | 68 | ## 拓扑结构 69 | 70 |
71 | 72 | ![image](./res/electron-hiprint_LAN_network_topology.png) 73 | 74 |
75 | 76 | ## 客户端设置 77 | 78 | 在 `v1.0.7` 后续版本中,支持这些设置 79 | 80 |
81 | 82 | ![image](./res/electron-hiprint_set.png) 83 | 84 |
85 | 86 | ```json 87 | { 88 | "mainTitle": "Electron-hiprint", 89 | "nickName": "", 90 | "openAtLogin": true, 91 | "openAsHidden": true, 92 | "port": "17521", 93 | "token": null, 94 | "connectTransit": true, 95 | "transitUrl": "https://v4.printjs.cn:17521", 96 | "transitToken": "hiprint-youcode", 97 | "allowNotify": true, 98 | "closeType": "tray", 99 | "pluginVersion": "0.0.58-fix", 100 | "logPath": "C:\\Users\\Administrator\\AppData\\Roaming\\hiprint\\logs", 101 | "pdfPath": "C:\\Users\\Administrator\\AppData\\Local\\Temp", 102 | "defaultPrinter": "", 103 | "disabledGpu": false, 104 | "rePrint": true 105 | } 106 | ``` 107 | 108 | ### 配置项说明 109 | 110 | | 序号 | 字段名 | 类型 | 说明 | 111 | | ---- | ---------------------- | ---------------- | ----------------------------------------------------- | 112 | | 1 | mainTitle[[1]](#tips1) | String | 主标题 | 113 | | 2 | nickName | String | 可设置的便于识别的友好设备名称 | 114 | | 3 | openAtLogin | Boolean | 系统登录时启动 | 115 | | 4 | openAsHidden | Boolean | 启动时隐藏窗口 | 116 | | 5 | connectTransit | Boolean | 连接中转服务 | 117 | | 6 | port | String \| Number | 端口号(10000 - 65535),默认为 17521 | 118 | | 7 | token[[2]](#tips2) | String \| null | 身份验证令牌,支持固定 Token | 119 | | 8 | transitUrl | String | 中转服务地址 | 120 | | 9 | transitToken | String | 中转服务令牌 | 121 | | 10 | closeType | String | 窗口关闭行为(tray 或 quit) | 122 | | 11 | pluginVersion | String | vue-plugin-hiprint 插件版本 | 123 | | 12 | logPath | String | 日志路径 | 124 | | 13 | pdfPath | String | 临时文件路径 | 125 | | 14 | defaultPrinter | String | 默认打印机 | 126 | | 15 | disabledGpu | Boolean | 禁用 GPU 加速,可解决部分设备打印模糊问题,默认 false | 127 | | 16 | rePrint[[1]](#tips1) | Boolean | 是否允许重打,默认 true | 128 | 129 | > [1] `mainTitle` 和 `rePrint` 字段在设置页面中未显式提供设置,方便各位可以在不修改源码二开的情况下通过配置快速实现定制化和高级功能,且不易被客户篡改,详见下方[覆盖默认配置方法](#覆盖默认配置方法)。 130 | 131 | > [2] `vue-plugin-hiprint` 需要使用 ^0.0.55 版本才可使用 `token` 否则请勿设置 `token`。 132 | 133 | ### 覆盖默认配置方法 134 | 135 | 1. 二开项目,直接修改 [项目源码 /tools/utils.js](./tools/utils.js) 并重新打包 136 | 2. win `v1.0.12-beta6` 后续版本可在 `exe` 安装包路径添加 `config.json`,安装包会自动检测并使用该配置 137 | 138 | ``` 139 | hiprint_win_x64-1.0.12-beta7.exe 140 | config.json 141 | ``` 142 | 143 | ## 中转服务 144 | 145 | 项目支持 `node-hiprint-transit`,可解决跨域问题并实现云打印功能,详见 [node-hiprint-transit](https://github.com/Xavier9896/node-hiprint-transit) 146 | 147 |
148 | 149 | ![image](./res/electron-hiprint_set_transit.png) 150 | 151 |
152 | 153 |
154 | 155 | ![image](./res/electron-hiprint_transit_WAN_network_topology.png) 156 | 157 |
158 | 159 | # 与非 `vue-plugin-hiprint` 项目的兼容 160 | 161 | 目标用户:只需要实现 HTML、PDF 静默打印,未使用 `vue-plugin-hiprint` 设计插件。 162 | 163 | ## 客户端连接 164 | 165 | 1. 安装依赖 166 | 167 | ```console 168 | npm install socket.io-client@4 --save 169 | ``` 170 | 171 | 2. 连接示例 172 | 173 | ```js 174 | import { io } from "socket.io-client"; 175 | 176 | const socket = io("http://localhost:17521", { 177 | transports: ["websocket"], 178 | auth: { 179 | token: "vue-plugin-hiprint", 180 | }, 181 | }); 182 | 183 | socket.on("connect", () => { 184 | globalThis.connect = true; 185 | // TODO: Do something for your project 186 | }); 187 | ``` 188 | 189 | 3. 获取客户端信息 190 | 191 |
192 | 193 | 与 electron-hiprint 建立连接 194 | 195 | 196 | - 连接成功后 `electron-hiprint` 会主动发送 `clientInfo`、`printerList` 事件,你只需要监听这两个事件即可获取到客户端信息与打印机列表。 197 | 198 | ```js 199 | socket.on("clientInfo", (clientInfo) => { 200 | globalThis.clientInfo = clientInfo; 201 | }); 202 | 203 | socket.on("printerList", (printerList) => { 204 | globalThis.printerList = printerList; 205 | }); 206 | ``` 207 | 208 | ```js 209 | // clientInfo 210 | { 211 | "hostname": "Admin", // 主机名 212 | "version": "1.0.12-beta9", // 客户端版本 213 | "platform": "win32", // 平台类型 214 | "arch": "x64", // 系统架构 215 | "mac": "d0:46:0c:97:4b:68", // mac 地址 216 | "ip": "192.168.0.114", // 设备 ip 217 | "ipv6": "fe80::2157:4b26:1c2f:c4ca", // 设备 ipv6 218 | "clientUrl": "http://192.168.0.114:17521", // 本地服务地址 219 | "machineId": "0e8b222e-517b-491e-883a-b6283a62e280", // 设备唯一 ID 220 | "nickName": "打印客户端", // 友好昵称 221 | } 222 | 223 | // printerList 224 | [{ 225 | description: "", 226 | displayName: "Microsoft Print to PDF", 227 | isDefault: true, 228 | name: "Microsoft Print to PDF", 229 | options: {, 230 | "printer-location": "", 231 | "printer-make-and-model": "Microsoft Print To PDF", 232 | "system_driverinfo": "Microsoft Print To PDF;10.0.19041.3570 (WinBuild.160101.0800);Microsoft® Windows® Operating System;10.0.19041.3570" 233 | }, 234 | status: 0 235 | }, {…}, {…}, {…}, {…}, {…}] 236 | ``` 237 | 238 | 你也可以主动向 `electron-hiprint` 发送 `getClientInfo`、`refreshPrinterList` 事件,来获取客户端打印机列表。 239 | 240 | ```js 241 | socket.emit("getClientInfo"); 242 | socket.emit("refreshPrinterList"); 243 | ``` 244 | 245 |
246 | 247 |
248 | 249 | 与 node-hiprint-transit 建立连接 250 | 251 | 252 | - 连接成功后 `node-hiprint-transit` 会主动发送 `clients`、`printerList` 事件,你只需要监听这两个事件即可获取到客户端信息与打印机列表。 253 | 254 | ```js 255 | socket.on("clients", (clients) => { 256 | globalThis.clients = clients; 257 | }); 258 | 259 | socket.on("printerList", (printerList) => { 260 | globalThis.printerList = printerList; 261 | }); 262 | ``` 263 | 264 | ```js 265 | // clients 266 | { 267 | "AlBaUCNs3AIMFPLZAAAh": { 268 | "hostname": "Admin", // 主机名 269 | "version": "1.0.12-beta9", // 客户端版本 270 | "platform": "win32", // 平台类型 271 | "arch": "x64", // 系统架构 272 | "mac": "d0:46:0c:97:4b:68", // mac 地址 273 | "ip": "192.168.0.114", // 设备 ip 274 | "ipv6": "fe80::2157:4b26:1c2f:c4ca", // 设备 ipv6 275 | "clientUrl": "http://192.168.0.114:17521", // 本地服务地址 276 | "machineId": "0e8b222e-517b-491e-883a-b6283a62e280", // 设备唯一 ID 277 | "nickName": "打印客户端", // 友好昵称 278 | printerList: [{ 279 | description: "", 280 | displayName: "Microsoft Print to PDF", 281 | isDefault: true, 282 | name: "Microsoft Print to PDF", 283 | options: {, 284 | "printer-location": "", 285 | "printer-make-and-model": "Microsoft Print To PDF", 286 | "system_driverinfo": "Microsoft Print To PDF;10.0.19041.3570 (WinBuild.160101.0800);Microsoft® Windows® Operating System;10.0.19041.3570" 287 | }, 288 | status: 0 289 | }, {…}, {…}, {…}, {…}, {…}], 290 | version: "1.0.7", 291 | }, 292 | "clientid": {…}, 293 | ... 294 | } 295 | 296 | // printerList 297 | [{ 298 | clientId: "AlBaUCNs3AIMFPLZAAAh", 299 | description: "", 300 | displayName: "Microsoft Print to PDF", 301 | isDefault: true, 302 | name: "Microsoft Print to PDF", 303 | options: {, 304 | "printer-location": "", 305 | "printer-make-and-model": "Microsoft Print To PDF", 306 | "system_driverinfo": "Microsoft Print To PDF;10.0.19041.3570 (WinBuild.160101.0800);Microsoft® Windows® Operating System;10.0.19041.3570" 307 | }, 308 | status: 0 309 | }, {…}, {…}, {…}, {…}, {…}] 310 | ``` 311 | 312 | 你也可以主动向 `node-hiprint-transit` 发送 `getClients`、`refreshPrinterList` 事件,来获取客户端打印机列表。 313 | 314 | ```js 315 | socket.emit("getClients"); 316 | 317 | socket.emit("refreshPrinterList"); 318 | // node-hiprint-transit 会将这个请求再转发给所有连接的 electron-hiprint ,以获取最新的打印机列表,但是并没有等待所有 electron-hiprint 响应结束,而是在延迟 2s 后直接返回了缓存及新获取到的打印机列表。并且 node-hiprint-transit 每 10min 都会主动向 electron-hiprint 请求一次 printerList,所以这应该并无大碍。或者你也可以优化这一个功能。 319 | 320 | // https://github.com/Xavier9896/node-hiprint-transit/blob/main/index.js#L139 321 | ``` 322 | 323 |
324 | 325 | ## 获取打印机纸张信息 326 | 327 | > [!IMPORTANT] 328 | > 该功能暂时只在 Window 环境下安装的 `electron-hiprint` 中支持,`node-hiprint-transit` 中转暂时也未支持! 329 | 330 |
331 | 332 | 连接为 electron-hiprint 333 | 334 | 335 | ```js 336 | // printerName: 打印机名称 可选值,缺省时返回所有打印机的纸张信息 337 | if (globalThis.connect) { 338 | socket.emit("getPaperSizeInfo", printerName); 339 | } else { 340 | alert("未连接客户端!"); 341 | window.open("hiprint://"); 342 | } 343 | 344 | socket.on("paperSizeInfo", (paperSizes) => { 345 | console.log(paperSizes); 346 | }); 347 | ``` 348 | 349 | ```js 350 | [ 351 | { 352 | "PrinterName": "Microsoft Print to PDF", 353 | "TaskNumber": 0, // 打印队列任务数 354 | "Status": 0, // 打印机状态码 355 | "StatusMsg": "准备就绪(Ready)", // 打印机状态信息 356 | "PaperSizes": [ 357 | { 358 | "Height": 1100, 359 | "Kind": 1, 360 | "PaperName": "信纸", 361 | "RawKind": 1, 362 | "Width": 850 363 | }, 364 | {...}, {...}, {...} 365 | ] 366 | } 367 | ] 368 | ``` 369 | 370 |
371 | 372 | ## 打印 HTML 373 | 374 |
375 | 376 | 连接为 electron-hiprint 377 | 378 | 379 | ```js 380 | /** 381 | * @description: 打印 html 字符串 382 | * @param {String} html 打印的html字符串 383 | * @param {String|number} templateId vue-plugin-hiprint 中的模板id,你可以自定义传入一个 Id,用于回调 success/error 判断 384 | * @param {String} printer 打印机名称 printer.name 可为空,缺省默认使用设备默认打印机 385 | * @param {pageSize} pageSize 打印纸张大小 { height: 80 * 1000, width: 60 * 1000 } 可为空,缺省默认使用打印机默认纸张 386 | * @description: 其他参数参考 默认打印参数说明 387 | */ 388 | if (globalThis.connect) { 389 | socket.emit("news", { html, templateId, printer, pageSize }); 390 | } else { 391 | alert("未连接客户端!"); 392 | window.open("hiprint://"); 393 | } 394 | ``` 395 | 396 | [打印回调](#打印回调) 397 | 398 |
399 | 400 |
401 | 402 | 连接为 node-hiprint-transit 403 | 404 | 405 | > [!IMPORTANT] 406 | > 当你连接中转服务时,需要在参数中指定 `client` 407 | 408 | ```js 409 | // 你可以自行在项目中实现一个选择客户端、打印机的功能 410 | const clientId = "AlBaUCNs3AIMFPLZAAAh"; 411 | const client = globalThis.clients[clientId]; 412 | const printer = globalThis.clients[0].printerList[0]; 413 | 414 | if (globalThis.connect) { 415 | socket.emit("news", { 416 | html, 417 | client: clientId, 418 | templateId, 419 | printer, 420 | pageSize, 421 | }); 422 | socket.emit("news", { 423 | html, 424 | client: client.clientId, 425 | templateId, 426 | printer: printer.name, 427 | pageSize, 428 | }); 429 | } else { 430 | alert("未连接客户端!"); 431 | window.open("hiprint://"); 432 | } 433 | ``` 434 | 435 |
436 | 437 | ### 默认打印参数说明 438 | 439 | ```js 440 | // 详见electron文档: https://www.electronjs.org/zh/docs/latest/api/web-contents 441 | { 442 | silent: data.silent ?? true, // 静默打印 443 | printBackground: data.printBackground ?? true, // 是否打印背景 444 | printer: printer, // 打印机名称 445 | color: data.color ?? true, // 是否打印颜色 446 | margins: data.margins ?? { 447 | marginType: "none", 448 | }, // 边距 449 | landscape: data.landscape ?? false, // 是否横向打印 450 | scaleFactor: data.scaleFactor ?? 100, // 打印缩放比例 451 | pagesPerSheet: data.pagesPerSheet ?? 1, // 每张纸的页数 452 | collate: data.collate ?? true, // 是否排序 453 | copies: data.copies ?? 1, // 打印份数 454 | pageRanges: data.pageRanges ?? {}, // 打印页数 455 | duplexMode: data.duplexMode, // 打印模式 simplex,shortEdge,longEdge 456 | dpi: data.dpi, // 打印机DPI 457 | header: data.header, // 打印头 458 | footer: data.footer, // 打印尾 459 | pageSize: data.pageSize, // 打印纸张 // A0, A1, A2, A3, A4, A5, A6, Legal, Letter, Tabloid 460 | } 461 | // 其中纸张大小参数 pageSize 如果传自定义大小, 需要乘以 1000 462 | { height: 80 * 1000, width: 60 * 1000 } 463 | ``` 464 | 465 | ## 使用 pdf 打印功能 466 | 467 | 原理: 468 | 469 | 1. 通过 electron 的 printToPDF 先导出 pdf 文件 470 | 2. 再通过 pdf-to-printer 或 unix-print 打印 pdf 文件 471 | 472 | > 传数据时需要传入: { type:'pdf' } 473 | 474 | > 如果是自定义的纸张大小, 别忘了传自定义的 paperName (纸张名称) 475 | 476 | ```js 477 | { 478 | client?: string; // 客户端id,连接中转服务必填 479 | printer?: string; // 打印机名称 480 | pages?: string; // 打印页数 481 | subset?: string; // 奇偶页 even、odd 482 | orientation?: string; // 纸张方向 portrait、landscape 483 | scale?: string; // 缩放 noscale、shrink、fit 484 | monochrome?: boolean; // 黑白打印 true、false 485 | side?: string; // 单双面 duplex, duplexshort, duplexlong, and simplex 486 | bin?: string; // select tray to print to 487 | paperName?: string; // 纸张大小 A2, A3, A4, A5, A6, letter, legal, tabloid, statement 488 | silent?: boolean; // Silences error messages. 489 | printDialog?: boolean; // 显示打印对话框 true、false 490 | copies?: number; // 打印份数 491 | } 492 | 493 | // vue-plugin-hiprint 494 | hiprint.hiwebSocket.send({ html, client, printer, type: 'pdf'}) 495 | 496 | // 非vue-plugin-hiprint 497 | socket.emit("news", { html, client, printer, type: 'pdf'}) 498 | ``` 499 | 500 | ## 下载网络 pdf 打印 501 | 502 | 原理: 503 | 504 | 1. 通过 node 的 http 或 https 库下载网络 pdf 文件至用户临时目录 505 | 2. 后续内容同使用 pdf 打印功能 506 | 507 | > 因为打印网络 pdf 不存在模板拼接,所以打印时直接如下调用即可 508 | 509 | > 参数同 pdf 打印功能 510 | 511 | ```js 512 | // vue-plugin-hiprint 513 | hiprint.hiwebSocket.send({ 514 | client, 515 | printer, 516 | type: "url_pdf", 517 | templateId: "自定义Id,用于判断任务是否成功", 518 | pdf_path: "网络PDF的下载url", 519 | }); 520 | 521 | // 非vue-plugin-hiprint 522 | socket.emit("news", { 523 | client, 524 | printer, 525 | type: "url_pdf", 526 | templateId: "自定义Id,用于判断任务是否成功", 527 | pdf_path: "网络PDF的下载url", 528 | }); 529 | ``` 530 | 531 | ## Blob 打印 PDF (🧪实验性功能) 532 | 533 | > ^1.0.14-beta4 534 | 535 | ```js 536 | // vue-plugin-hiprint 537 | hiprint.hiwebSocket.send({ 538 | client, 539 | printer, 540 | type: "blob_pdf", 541 | templateId: "自定义Id,用于判断任务是否成功", 542 | pdf_blob: Blob, 543 | }); 544 | 545 | // 非vue-plugin-hiprint 546 | socket.emit("news", { 547 | client, 548 | printer, 549 | type: "blob_pdf", 550 | templateId: "自定义Id,用于判断任务是否成功", 551 | pdf_blob: Blob, 552 | }); 553 | ``` 554 | 555 | ## 打印回调 556 | 557 | ```js 558 | socket.on("success", (res) => { 559 | console.log(res.templateId); 560 | // TODO: Do something for your project 561 | }); 562 | 563 | socket.on("error", (res) => { 564 | console.log(res.templateId); 565 | // TODO: Do something for your project 566 | }); 567 | ``` 568 | 569 | ## 模板+data 或 html 返回 jpeg、pdf、打印 570 | 571 | > [!TIP] 572 | > 该功能依赖 electron-hiprint@^1.0.12-beta7 版本 573 | 574 | 现在,你可以通过对应 socket 事件,调用 electron-hiprint 生成 jpeg、矢量 pdf 和直接打印了。 575 | 576 | 对于 vue-plugin-hiprint 模板,只需要提供 template(json、jsonString) 和 data(json) 即可。 577 | 578 | 非 vue-plugin-hiprint 模板,你需要提供 html(需要提供完整的样式含 UI、项目内部样式)。 579 | 580 |
581 | 582 | ![image](./res/electron-hiprint_set_pluginVersion.png) 583 | 584 |
585 | 586 | | apiName | 参数 | 说明 | 587 | | -------------------- | --------------------------- | -------------------------------------------------- | 588 | | render-jpeg | `template`,`data` / `html` | 调用 electron 生成 jpeg | 589 | | render-jpeg-success | `templateId`,`buffer`,`msg` | 成功回调,返回 templateId 和生成的 jpeg 二进制数据 | 590 | | render-jpeg-error | `templateId`,`msg` | 错误回调,返回 templateId 和错误信息 | 591 | | render-pdf | `template`,`data` / `html` | 调用 electron 生成 pdf | 592 | | render-pdf-success | `templateId`,`buffer`,`msg` | 成功回调,返回 templateId 和生成的 pdf 二进制数据 | 593 | | render-pdf-error | `templateId`,`msg` | 错误回调,返回 templateId 和错误信息 | 594 | | render-print | `template`,`data` / `html` | 调用 electron 打印 | 595 | | render-print-success | `templateId`,`msg` | 成功回调,返回 templateId 和打印成功信息 | 596 | | render-print-error | `templateId`,`msg` | 错误回调,返回 templateId 和错误信息 | 597 | 598 |
599 | vue-plugin-hiprint 600 | 601 | ```js 602 | hiprint.hiwebSocket.socket.emit("render-jpeg", { 603 | template: panel, 604 | data: printData, 605 | html: "heml字符串", 606 | }) 607 | socket.on("render-jpeg-success", (data) => { 608 | // data.buffer 609 | }); 610 | socket.on("render-jpeg-error", (data) => { 611 | // data.error 612 | }); 613 | 614 | hiprint.hiwebSocket.socket.emit("render-pdf", { 615 | template: panel, 616 | data: printData, 617 | html: "heml字符串", 618 | }) 619 | socket.on("render-pdf-success", (data) => { 620 | // data.buffer 621 | }); 622 | socket.on("render-pdf-error", (data) => { 623 | // data.error 624 | }); 625 | 626 | hiprint.hiwebSocket.socket.emit("render-print", { 627 | template: panel, 628 | data: printData, 629 | html: "heml字符串", 630 | printer: "Microsoft Print to PDF", 631 | ... 632 | }) 633 | socket.on("render-print-success", (data) => { 634 | // data.templateId 635 | }); 636 | socket.on("render-print-error", (data) => { 637 | // data.templateId 638 | }); 639 | ``` 640 | 641 |
642 | 643 |
644 | node.js demo 645 | 646 | ```node 647 | const io = require("socket.io-client"); 648 | const fs = require("fs"); 649 | 650 | const panel = require("./panel.json"); 651 | const printData = require("./print-data.json"); 652 | 653 | const socket = io("http://localhost:17521", { 654 | transports: ["websocket"], 655 | reconnectionAttempts: 5, 656 | auth: { 657 | token: "vue-plugin-hiprint", 658 | }, 659 | }); 660 | socket.on("connect", () => { 661 | socket.emit("render-jpeg", { 662 | template: panel, 663 | data: printData, 664 | html: "heml字符串", 665 | }); 666 | socket.on("render-jpeg-success", (data) => { 667 | fs.writeFile("./capture.jpeg", data.buffer, () => {}); 668 | }); 669 | // render-pdf 同上 670 | // render-print 同上 671 | }); 672 | ``` 673 | 674 |
675 | 676 | 打印参数同上 [默认打印参数说明](#默认打印参数说明) 677 | 678 | ## 断开连接 679 | 680 | ```js 681 | socket.on("disconnect", () => { 682 | globalThis.connect = false; 683 | // TODO: Do something for your project 684 | }); 685 | ``` 686 | 687 | ## 打印记录 688 | 689 | > [!TIP] 690 | > 打印记录功能属于 ^1.0.12-beta1 功能 691 | 692 | 客户端将会记录每一条 `news` ,你可以从这里查询历史打印记录,是否成功,重打操作等。 693 | 694 |
695 | 696 | ![打印记录](./res/Print_log.png) 697 | 698 |
699 | 700 | ### 禁用重打 701 | 702 | 1. 通过配置全局禁用重打 703 | 704 | 适合全局禁用重打,只提供日志查询,需在全局配置中设置禁用(设置页面未显式提供设置) 705 | 706 | - [覆盖默认配置方法](#覆盖默认配置方法) 707 | 708 | 2. 通过 api 禁用单条数据重打 709 | 710 | ```js 711 | // socket.io-client 712 | socket.emit("news", { 713 | html, 714 | templateId, 715 | printer, 716 | pageSize, 717 | rePrintAble: false, 718 | }); 719 | socket.emit("render-print", { template, data, rePrintAble: false }); 720 | 721 | // vue-plugin-hiprint 722 | hiprintTemplate.print2(printData, { printer, title, rePrintAble: false }); 723 | hiprint.hiwebSocket.socket.emit("render-print", { 724 | template, 725 | data, 726 | rePrintAble: false, 727 | }); 728 | ``` 729 | 730 | ## URL Scheme 支持 731 | 732 | 通过 `hiprint://` 协议,可以从 Web 项目中调起 `electron-hiprint` 客户端,以便未建立连接时主动唤起客户端。 733 | 734 | > [!TIP] 735 | > 注意: 安装客户端时需以管理员身份运行。 736 | 737 |
738 | 739 | ![URLScheme](./res/URLScheme.png) 740 | 741 | ![alert弹框](./res/URLScheme_tips.png) 742 | 743 |
744 | 745 | ```js 746 | // js 747 | window.open("hiprint://"); 748 | 749 | // element-ui 750 | this.$alert( 751 | `连接【${hiwebSocket.host}】失败!
请确保目标服务器已 下载 运行 打印服务!`, 752 | "客户端未连接", 753 | { dangerouslyUseHtmlString: true }, 754 | ); 755 | 756 | // ant-design v1 757 | this.$error({ 758 | title: "客户端未连接", 759 | content: (h) => ( 760 |
761 | 连接【{hiwebSocket.host}】失败! 762 |
763 | 请确保目标服务器已 764 | 768 | 下载 769 | 770 | 运行 771 | 772 | 打印服务! 773 |
774 | ), 775 | }); 776 | ``` 777 | 778 | ## 周边生态项目 779 | 780 | | 项目名称 | 项目地址 | 下载地址 | 描述 | 781 | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- | ------------------------------------------------------------------ | 782 | | vue-plugin-hiprint | [github](https://github.com/CcSimple/vue-plugin-hiprint)、[gitee](https://gitee.com/CcSimple/vue-plugin-hiprint) | [npm](https://www.npmjs.com/package/vue-plugin-hiprint) | 打印设计器 | 783 | | electron-hiprint | [github](https://github.com/CcSimple/electron-hiprint)、[gitee](https://gitee.com/CcSimple/electron-hiprint) | [releases](https://github.com/CcSimple/electron-hiprint/releases) | 直接打印客户端 | 784 | | node-hiprint-transit | [github](https://github.com/Xavier9896/node-hiprint-transit)、[gitee](https://gitee.com/Xavier9896/node-hiprint-transit) | [releases](https://github.com/Xavier9896/node-hiprint-transit/releases) | web 与客户端中转服务 Node 实现 | 785 | | hiprint-transporter-java | [github](https://github.com/LyingDoc/hiprint-transit-java)、[gitee](https://gitee.com/dut_cc/hiprint-transporter-java) | - | web 与客户端中转服务 Java 实现 | 786 | | hiprint-transit-java | [github](https://github.com/weaponready/hiprint-transit-java) | - | web 与客户端中转服务 Java 实现 | 787 | | uni-app-hiprint | [github](https://github.com/Xavier9896/uni-app-hiprint) | - | uni-app 项目通过 webview 使用 vue-plugin-hiprint demo | 788 | | node-hiprint-pdf | [github](https://github.com/CcSimple/node-hiprint-pdf) | - | 提供通过 node 对 vue-plugin-hiprint 模板生成 矢量 pdf、image、html | 789 | 790 | ## 参考资源 791 | 792 | - [electron](https://www.electronjs.org/zh/docs/latest/) 793 | - [electron-egg](https://gitee.com/wallace5303/electron-egg/) 794 | - [pdf-to-printer](https://github.com/artiebits/pdf-to-printer) 795 | - [unix-printer](https://github.com/artiebits/unix-print) 796 | 797 | ## 感谢 798 | 799 | logo 设计:[橙色](mailto:tong567@foxmail.com) 800 | --------------------------------------------------------------------------------