├── 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 |
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 |
61 |
62 |
68 |
75 |
76 |
77 | {{ option.content || option.label }}
78 |
79 |
80 |
81 |
88 | {{ item.content }}
89 |
90 |
91 |
92 |
93 |
94 |
99 | 搜索
100 |
101 |
106 | 清空
107 |
108 |
109 |
110 |
111 |
112 |
119 |
124 |
125 |
130 | 重打
131 |
132 |
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 |
62 |
63 |
64 |
65 |
72 |
73 |
74 |
75 |
82 |
83 |
84 | {{item.label}}
85 |
86 | {{item.label}}
87 |
88 |
95 |
96 |
97 |
102 |
106 |
107 |
108 |
109 | {{ option.content || option.value }}
110 |
111 |
112 |
113 |
120 | {{ item.content }}
121 |
122 |
123 |
124 |
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 | [](https://deepwiki.com/CcSimple/electron-hiprint)
4 |
5 |
6 |
7 | 
8 |
9 |
10 |
11 | 该项目是为 [vue-plugin-hiprint](https://github.com/CcSimple/vue-plugin-hiprint) 配套开发的静默打印解决方案。我们发现部分使用此项目的开发者拥有自定义的设计器或渲染方案,或者仅需要静默打印一段 HTML、PDF。如果您也有类似需求,electron-hiprint 将是您的理想选择。
12 |
13 |
14 |
15 | 
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 | 
73 |
74 |
75 |
76 | ## 客户端设置
77 |
78 | 在 `v1.0.7` 后续版本中,支持这些设置
79 |
80 |
81 |
82 | 
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 | 
150 |
151 |
152 |
153 |
154 |
155 | 
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 | 
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 | 
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 | 
740 |
741 | 
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 |
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 |
--------------------------------------------------------------------------------