├── icon.ico ├── .gitignore ├── app.manifest ├── go.mod ├── .github └── FUNDING.yml ├── README.md ├── go.sum ├── static ├── config │ ├── index.html │ ├── styles.css │ └── script.js ├── index.html └── sweetalert2.all.min-10.8.0.js ├── LICENSE └── main.go /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ij369/scanners-go/HEAD/icon.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | .DS_Store 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | # Go workspace file 22 | go.work 23 | go.work.sum 24 | 25 | # env file 26 | .env 27 | 28 | rsrc.syso 29 | 30 | scanner_config.json -------------------------------------------------------------------------------- /app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | true 12 | PerMonitorV2, PerMonitor 13 | 14 | 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module scannerapp 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/getlantern/systray v1.2.2 7 | github.com/gorilla/websocket v1.5.3 8 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 9 | golang.org/x/sys v0.25.0 10 | ) 11 | 12 | require ( 13 | github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect 14 | github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect 15 | github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect 16 | github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect 17 | github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect 18 | github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect 19 | github.com/go-stack/stack v1.8.0 // indirect 20 | github.com/google/uuid v1.6.0 // indirect 21 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 7 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 8 | liberapay: # Replace with a single Liberapay username 9 | issuehunt: # Replace with a single IssueHunt username 10 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 11 | polar: # Replace with a single Polar username 12 | buy_me_a_coffee: ij369 # Replace with a single Buy Me a Coffee username 13 | ko_fi: ij369 # Replace with a single Ko-fi username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: ["https://paypal.me/wf9"] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scanners-go 扫码枪转发器 2 | 3 | 本项目是一个用于转发扫码枪(条码枪)输入的工具,可以实现无焦点的情况下,将扫码结果转发到 http 服务器,基于 Golang 开发,适用于 Windows ,便于响应 Bar Code 或 Qr Code , 开箱即用。 4 | 5 | ## 特征 6 | 7 | - 扫码枪输入内容不需要扫码枪有串口模式,兼容市面大多数种类扫码枪。 8 | - 通过本项目可以后台无焦点实现监听。 9 | - 转发到后端 http 服务器,支持执行外部命令。 10 | - 使用 Golang 构建后的 exe 体积小,可执行文件约 10M 上下。 11 | 12 | ## 实现原理 13 | 14 | 1. 扫码枪属于 HID 设备,所有字符几乎能在同一瞬间按顺序输入完毕。 15 | 2. 通常扫码枪在输入完毕后会带回车键作为结束符。 16 | 3. 正则匹配内容。 17 | 18 | 本程序基于以上三点,实现扫码枪输入的准确识别,几乎不受扫码枪(扫描枪,条码枪)品牌限制。 19 | 20 | ## 使用指南 21 | ### 基本功能 22 | 23 | 通常默认就已经适合大多数情况使用了,你可以在 [配置页(./config/)](./config/) 里按照以下表格说明进行更精细的控制,配置页里可以实时生效修改,或者如果你熟悉 json 可以直接修改 **config.json** 文件并重新打开生效。 24 | 25 | | 设置项 | 使用说明 | Key 值 | 26 | | ---------------------------------- | ------------------------------------------------------------ | ---------------- | 27 | | 转发接口 URL | 默认程序会在后台运行,将识别到的结果以 JSON 对象的格式 POST 给输入的后端 API | forwardURL | 28 | | 重置间隔 (毫秒) | 默认 500ms,可以调到更低,这个取决于扫码枪两个按键之间延迟,超过缓冲时间不会当成是扫码枪录入,会进行重置,用于区分人手和扫码枪,过滤掉人手输入。 | resetInterval | 29 | | 结束符后缀 | 默认为回车键,即 CR,Enter 键,通常买的扫码枪,出厂都是带回车键作为结束键入的,有的扫码枪说明书可以自定义设置,如果和回车冲突,可以换 [TAB] 作为结尾标志来识别 | endSuffix | 30 | | 启动时打开主页 | 打开后,在启动时会自动调用浏览器打开主页 | autoOpenPage | 31 | | 显示特殊按键(如 [LSHIFT], [TAB]) | 开启后,你需要自己处理扫码枪的组合按键,扫码枪大小写通常是[LSHIFT]+字母,有的扫码枪说明书可以设置 | showSpecialChars | 32 | | 显示控制台窗口 | 如果你下载的是文件名里有 debug 的,启动时会显示黑色的控制台窗口,可用来调试,debug 版即使关闭也不影响,注意,debug 版如果不打开此项,启动时会瞬时闪一下控制台窗口,非 debug 的版本不会闪。 | showConsole | 33 | | 开机自动启动 | 打开后,可以开机自启 | - | 34 | | 正则匹配 | 可输入正则表达式,用来加强过滤。
例如:`ij369/scanners-go`可以写作`[a-z]{2}\d{3}/[a-z]{8}\-[a-z]{2}` 如果留空则会匹配所有内容 | outputRegex | 35 | | 执行动作 | 开启后,可以在识别后触发额外外部命令,可以实现使用浏览器打开等操作 | actions | 36 | 37 | 除了 http 转发结果到后端接口,如果是纯前端项目,可以监听本 origin 的 Websocket ,端口同应用端口,路径为`/ws`,可以打开 WebTools 参考如何连接。 38 | 39 | ### 执行动作 40 | 41 | 执行动作是指会在发送给后端结果的同时,执行一次外部命令,满足特殊需求。 42 | 43 | | 设置项 | 说明 | 举例 | Key 值 | 44 | | -------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | --------- | 45 | | 命令或可执行文件路径 | 命令或完整的可执行文件路径 | 1. **调用 CMD**:`cmd`
2. **可执行文件路径**: `C:\Program Files\Google\Chrome\Application\chrome.exe` | command | 46 | | 数据前缀 | 可以在扫码结果前加上特定字符串 | `https://github.com/ij369/scanners-go?id=` | prefix | 47 | | 数据后缀 | 可以在扫码结果后加上特定字符串 | `&lang=zh-CN` | suffix | 48 | | 命令参数 | 可以每行一个参数。使用 `{data}` 表示扫码结果插入位置, `{data}` 包含前缀和后缀。也可以不包含 `{data}`. | 1. 执行 CMD 命令`/C echo {data} | clip`
2. Chrome 参数启动 `--new-window {data}` | arguments | 49 | 50 | 占位符 51 | 52 | | 占位符 | 作用 | 53 | | ----------- | ------------------------------------------------------------ | 54 | | {timestamp} | 用于数据前缀,数据后缀,{timestamp} 的部分会替换成当前的 Unix 时间戳,可用于防止浏览器缓存,例如 `&t={timestamp}` . | 55 | | {uuid} | 用于数据前缀,数据后缀,作用同上,会替换成 4 代 UUID. | 56 | | {urlencode} | 用于数据前缀,数据后缀,可以使用两个 {urlencode} 包裹 URL 查询参数来实现编码,如果 {urlencode} 的个数为奇数,则最后一个的实现是将其后的部分进行 URI 编码。 | 57 | 58 | 59 | 60 | ------ 61 | 62 | 如果本项目对你有帮助的话,欢迎 Fork / Star 本项目 63 | 64 | 项目地址 65 | 66 | [𝐡𝐭𝐭𝐩𝐬://𝐠𝐢𝐭𝐡𝐮𝐛.𝐜𝐨𝐦/𝐢𝐣𝟑𝟔𝟗/𝐬𝐜𝐚𝐧𝐧𝐞𝐫𝐬-𝐠𝐨](https://github.com/ij369/scanners-go) -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4= 4 | github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY= 5 | github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So= 6 | github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A= 7 | github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk= 8 | github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc= 9 | github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0= 10 | github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o= 11 | github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc= 12 | github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA= 13 | github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA= 14 | github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA= 15 | github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE= 16 | github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE= 17 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= 18 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 19 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 20 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 21 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 22 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 23 | github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= 24 | github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= 25 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= 26 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= 27 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 28 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 29 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= 30 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= 31 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 32 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 33 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 34 | golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 35 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 37 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 38 | gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= 39 | -------------------------------------------------------------------------------- /static/config/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 扫码枪配置 8 | 9 | 10 | 11 | 12 | 13 |
14 | 返回 15 |

扫码枪配置

16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 | 24 |
25 |
26 | 30 |
31 |
32 | 36 |
37 |
38 | 42 |
43 |
44 | 48 |
49 |
50 | 51 | 55 |
56 |
57 | 58 | 59 | 例如:ij369/scanners-go可以写作[a-z]{2}\d{3}/[a-z]{8}\-[a-z]{2} 61 | 如果留空则会匹配所有内容 62 |
63 | 64 |
65 | 70 |
71 |
72 |
73 | 74 | 75 | 例如:cmd 或 76 | C:\Program Files\Google\Chrome\Application\chrome.exe 77 |
78 |
79 | 80 | 81 | 在扫码结果前添加的字符 82 |
83 |
84 | 85 | 86 | 在扫码结果后添加的字符 87 |
88 |
89 | 90 | 91 | 可以每行一个参数。使用 {data} 表示扫码结果插入位置, {data} 92 | 包含前缀和后缀。例如:
93 |
--new-window
{data}
94 |
95 |
96 |
97 | 98 |
99 | 100 |

最近扫码结果

101 | 102 |
103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /static/config/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #3498db; 3 | --primary-color-a: #2980b9; 4 | --secondary-color: #2c3e50; 5 | --background-color: #f5f7fa; 6 | --text-color: #333; 7 | --border-color: #e0e0e0; 8 | } 9 | 10 | body { 11 | font-family: 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 12 | line-height: 1.6; 13 | margin: 0; 14 | padding: 0; 15 | background-color: var(--background-color); 16 | color: var(--text-color); 17 | } 18 | 19 | .container { 20 | max-width: 800px; 21 | margin: 2rem auto; 22 | padding: 2rem; 23 | background-color: #fff; 24 | border-radius: 8px; 25 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 26 | } 27 | 28 | a.back-button { 29 | text-decoration: none; 30 | display: inline-block; 31 | padding: .3rem 0; 32 | padding-right: 2rem; 33 | color: #999; 34 | background: transparent; 35 | transition: all 0.3s ease; 36 | position: relative; 37 | } 38 | 39 | a.back-button::before { 40 | content: "<"; 41 | font-size: .6rem; 42 | position: absolute; 43 | left: -1rem; 44 | opacity: 0; 45 | transition: all 0.3s ease; 46 | top: 50%; 47 | transform: translateY(-50%); 48 | } 49 | 50 | a.back-button:hover::before { 51 | opacity: 1; 52 | padding: 1rem; 53 | left: -1rem; 54 | } 55 | 56 | a.back-button:hover { 57 | color: #000; 58 | padding: .3rem 1rem; 59 | font-weight: 500; 60 | } 61 | 62 | label, 63 | button, 64 | select, 65 | a.back-button { 66 | user-select: none; 67 | } 68 | 69 | h1, 70 | h2 { 71 | color: var(--secondary-color); 72 | text-align: center; 73 | margin-bottom: 1.5rem; 74 | } 75 | 76 | h2 { 77 | margin-top: 3rem; 78 | } 79 | 80 | h3 { 81 | color: var(--primary-color); 82 | } 83 | 84 | .form-group { 85 | margin-bottom: 1.5rem; 86 | display: flex; 87 | flex-direction: column; 88 | } 89 | 90 | label { 91 | display: block; 92 | margin-bottom: 0.5rem; 93 | font-weight: 500; 94 | } 95 | 96 | input[type="text"], 97 | input[type="number"], 98 | textarea, 99 | select { 100 | padding: 0.75rem; 101 | border: 1px solid var(--border-color); 102 | border-radius: 4px; 103 | font-size: 1rem; 104 | transition: border-color 0.3s ease; 105 | } 106 | 107 | input[type="text"]:focus, 108 | input[type="number"]:focus, 109 | textarea:focus, 110 | select:focus { 111 | outline: none; 112 | border-color: var(--primary-color); 113 | } 114 | 115 | button { 116 | display: block; 117 | width: 100%; 118 | padding: 0.75rem; 119 | background-color: var(--primary-color); 120 | color: #fff; 121 | border: none; 122 | border-radius: 4px; 123 | cursor: pointer; 124 | font-size: 1rem; 125 | font-weight: 500; 126 | transition: background-color 0.3s ease; 127 | } 128 | 129 | button:hover { 130 | background-color: var(--primary-color-a); 131 | } 132 | 133 | #scanResults { 134 | list-style-type: none; 135 | padding: 0; 136 | margin-top: 2rem; 137 | } 138 | 139 | #scanResults li { 140 | background-color: #fff; 141 | border: 1px solid var(--border-color); 142 | padding: 1rem; 143 | margin-bottom: 0.5rem; 144 | border-radius: 4px; 145 | transition: box-shadow 0.3s ease; 146 | } 147 | 148 | #scanResults li:hover { 149 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 150 | } 151 | 152 | /* 复选框样式 */ 153 | input[type="checkbox"] { 154 | -webkit-appearance: none; 155 | -moz-appearance: none; 156 | appearance: none; 157 | width: 18px; 158 | height: 18px; 159 | border: 2px solid var(--border-color); 160 | border-radius: 3px; 161 | outline: none; 162 | transition: all 0.3s ease; 163 | position: relative; 164 | cursor: pointer; 165 | vertical-align: middle; 166 | margin-right: 0.5rem; 167 | } 168 | 169 | input[type="checkbox"]:checked { 170 | background-color: var(--primary-color); 171 | border-color: var(--primary-color); 172 | } 173 | 174 | input[type="checkbox"]:checked::before { 175 | content: '✔'; 176 | position: absolute; 177 | top: 50%; 178 | left: 50%; 179 | transform: translate(-50%, -50%); 180 | color: #fff; 181 | font-size: 12px; 182 | } 183 | 184 | input[type="checkbox"]:hover { 185 | border-color: var(--primary-color); 186 | } 187 | 188 | input[type="checkbox"]+label { 189 | display: inline-block; 190 | vertical-align: middle; 191 | cursor: pointer; 192 | } 193 | 194 | .help-text { 195 | color: #666; 196 | margin: .5rem 0; 197 | } 198 | 199 | select { 200 | appearance: none; 201 | -webkit-appearance: none; 202 | -moz-appearance: none; 203 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M4 4l2 2 2-2z' fill='gray'/%3E%3C/svg%3E"); 204 | background-repeat: no-repeat; 205 | background-position: right 1rem center; 206 | background-size: 1em; 207 | } 208 | 209 | fieldset { 210 | border: none; 211 | padding: 0; 212 | margin: 0; 213 | } 214 | 215 | fieldset[disabled] { 216 | opacity: .5; 217 | } 218 | 219 | textarea { 220 | resize: vertical; 221 | min-height: 3rem; 222 | } 223 | 224 | code, 225 | pre { 226 | background-color: #f0f0f0; 227 | border-radius: 3px; 228 | color: #333; 229 | } 230 | 231 | code { 232 | padding: 0.2em 0.4em; 233 | } 234 | 235 | pre { 236 | padding: .6rem; 237 | border: 1px solid #ddd; 238 | } 239 | 240 | pre code { 241 | background-color: transparent; 242 | padding: 0; 243 | border-radius: 0; 244 | } 245 | 246 | .github-corner:hover .octo-arm { 247 | animation: octocat-wave 560ms ease-in-out 248 | } 249 | 250 | @keyframes octocat-wave { 251 | 252 | 0%, 253 | 100% { 254 | transform: rotate(0) 255 | } 256 | 257 | 20%, 258 | 60% { 259 | transform: rotate(-25deg) 260 | } 261 | 262 | 40%, 263 | 80% { 264 | transform: rotate(10deg) 265 | } 266 | } 267 | 268 | @media (max-width:500px) { 269 | .github-corner:hover .octo-arm { 270 | animation: none 271 | } 272 | 273 | .github-corner .octo-arm { 274 | animation: octocat-wave 560ms ease-in-out 275 | } 276 | } 277 | 278 | .table-figure { 279 | font-size: 0.875rem; 280 | margin: 1rem 0; 281 | color: #333; 282 | } 283 | 284 | table { 285 | width: 100%; 286 | border-collapse: collapse; 287 | margin-bottom: 1rem; 288 | background-color: #fff; 289 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 290 | } 291 | 292 | th, 293 | td { 294 | padding: 0.75rem; 295 | text-align: left; 296 | border-bottom: 1px solid #e0e0e0; 297 | } 298 | 299 | th { 300 | background-color: #f5f5f5; 301 | font-weight: bold; 302 | color: #333; 303 | } 304 | 305 | th:first-child { 306 | min-width: 5rem; 307 | } 308 | 309 | tr:hover { 310 | background-color: #f9f9f9; 311 | } 312 | 313 | @media (max-width:960px) { 314 | h1 { 315 | margin-bottom: 3rem; 316 | transition: all 0.3s ease; 317 | } 318 | 319 | .container { 320 | max-width: 100%; 321 | margin: 0; 322 | transition: all 0.3s ease; 323 | } 324 | } 325 | 326 | @media (max-width: 600px) { 327 | table { 328 | font-size: 0.9rem; 329 | } 330 | 331 | th, 332 | td { 333 | padding: 0.5rem; 334 | } 335 | } -------------------------------------------------------------------------------- /static/config/script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function () { 2 | // 检查是否在配置页面 3 | const isConfigPage = window.location.pathname.startsWith('/config'); 4 | 5 | if (isConfigPage) { 6 | // 加载配置 7 | fetch('/api/config') 8 | .then(response => response.json()) 9 | .then(data => { 10 | document.getElementById('forwardURL').value = data.forwardURL; 11 | document.getElementById('resetInterval').value = data.resetInterval; 12 | document.getElementById('autoOpenPage').checked = data.autoOpenPage; 13 | document.getElementById('showSpecialChars').checked = data.showSpecialChars; 14 | document.getElementById('showConsole').checked = data.showConsole; 15 | document.getElementById('startOnBoot').checked = data.startOnBoot; 16 | document.getElementById('endSuffix').value = data.endSuffix || 'ENTER'; 17 | document.getElementById('outputRegex').value = data.outputRegex || ''; 18 | 19 | document.getElementById('actionPrefix').value = data.actions.prefix || ''; 20 | document.getElementById('actionSuffix').value = data.actions.suffix || ''; 21 | document.getElementById('actionCommand').value = data.actions.command || ''; 22 | document.getElementById('actionArguments').value = data.actions.arguments ? data.actions.arguments.join('\n') : ''; 23 | document.getElementById('actionEnable').checked = data.actions.command !== ''; 24 | document.getElementById('actionFieldset').disabled = !document.getElementById('actionEnable').checked; 25 | }); 26 | 27 | // 提交表单 28 | document.getElementById('configForm').addEventListener('submit', function (e) { 29 | e.preventDefault(); 30 | const formData = { 31 | forwardURL: document.getElementById('forwardURL').value, 32 | resetInterval: parseInt(document.getElementById('resetInterval').value), 33 | autoOpenPage: document.getElementById('autoOpenPage').checked, 34 | showSpecialChars: document.getElementById('showSpecialChars').checked, 35 | showConsole: document.getElementById('showConsole').checked, 36 | startOnBoot: document.getElementById('startOnBoot').checked, 37 | endSuffix: document.getElementById('endSuffix').value, 38 | outputRegex: document.getElementById('outputRegex').value, 39 | }; 40 | 41 | if (document.getElementById('actionEnable').checked) { 42 | formData.actions = { 43 | prefix: document.getElementById('actionPrefix').value, 44 | suffix: document.getElementById('actionSuffix').value, 45 | command: document.getElementById('actionCommand').value, 46 | arguments: document.getElementById('actionArguments').value 47 | .split('\n') 48 | .map(arg => arg.trim()) 49 | .filter(arg => arg !== ''), 50 | dataIndex: document.getElementById('actionArguments').value 51 | .split('\n') 52 | .findIndex(arg => arg.includes('{data}')), 53 | }; 54 | } 55 | 56 | fetch('/api/config', { 57 | method: 'POST', 58 | headers: { 59 | 'Content-Type': 'application/json', 60 | }, 61 | body: JSON.stringify(formData), 62 | }) 63 | .then(response => { 64 | if (response.ok) { 65 | Swal.fire({ 66 | icon: 'success', 67 | title: '配置已保存', 68 | showConfirmButton: false, 69 | toast: true, 70 | position: 'top-end', 71 | timer: 1500 72 | }); 73 | } else { 74 | Swal.fire({ 75 | icon: 'error', 76 | title: '保存配置失败', 77 | showConfirmButton: false, 78 | toast: true, 79 | position: 'top-end', 80 | timer: 3000 81 | }); 82 | } 83 | }) 84 | .catch((error) => { 85 | console.error('Error:', error); 86 | Swal.fire({ 87 | icon: 'error', 88 | title: '保存配置时发生错误', 89 | showConfirmButton: false, 90 | timer: 3000, 91 | toast: true, 92 | position: 'top-end', 93 | }); 94 | }); 95 | }); 96 | } 97 | 98 | // WebSocket 连接(在主页和配置页都需要) 99 | function connectWebSocket() { 100 | const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; 101 | const socket = new WebSocket(protocol + '//' + window.location.host + '/ws'); 102 | const scanResults = document.getElementById('scanResults'); 103 | 104 | socket.onmessage = function (event) { 105 | const li = document.createElement('li'); 106 | li.textContent = event.data; 107 | Swal.fire({ 108 | title: event.data, 109 | icon: 'success', 110 | showConfirmButton: false, 111 | toast: true, 112 | position: 'top-end', 113 | timerProgressBar: event.data !== 'CONNECTED', 114 | timer: 3000 115 | }); 116 | scanResults.insertBefore(li, scanResults.firstChild); 117 | 118 | // 保持最多显示 10 条结果 119 | while (scanResults.children.length > 10) { 120 | scanResults.removeChild(scanResults.lastChild); 121 | } 122 | }; 123 | 124 | socket.onerror = function (error) { 125 | console.error('WebSocket 错误:', error); 126 | }; 127 | 128 | socket.onclose = function () { 129 | console.debug('WebSocket 连接已关闭'); 130 | Swal.fire({ 131 | icon: 'error', 132 | title: 'WebSocket 连接已关闭', 133 | timer: 5000, 134 | toast: true, 135 | showConfirmButton: false, 136 | position: 'top-end', 137 | }); 138 | 139 | // 每隔 5 秒尝试重新连接 140 | const retryInterval = setInterval(() => { 141 | console.debug('尝试重新连接 WebSocket...'); 142 | const newSocket = new WebSocket(protocol + '//' + window.location.host + '/ws'); 143 | 144 | newSocket.onopen = function () { 145 | console.log('WebSocket 重新连接成功'); 146 | Swal.fire({ 147 | icon: 'success', 148 | title: 'WebSocket 重新连接成功', 149 | showConfirmButton: false, 150 | toast: true, 151 | position: 'top-end', 152 | timer: 3000 153 | }); 154 | clearInterval(retryInterval); 155 | // 重新绑定事件处理程序 156 | newSocket.onmessage = socket.onmessage; 157 | newSocket.onerror = socket.onerror; 158 | newSocket.onclose = socket.onclose; 159 | }; 160 | 161 | newSocket.onerror = function (error) { 162 | console.error('WebSocket 重新连接错误:', error); 163 | Swal.fire({ 164 | icon: 'error', 165 | title: 'WebSocket 连接已关闭, 正在尝试连接...', 166 | timer: 5000, 167 | toast: true, 168 | showConfirmButton: false, 169 | position: 'top-end', 170 | }); 171 | }; 172 | }, 5000); 173 | }; 174 | } 175 | 176 | connectWebSocket(); 177 | }); -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 扫码枪转发 9 | 10 | 11 | 12 | 13 |
14 | 15 | 25 | 26 | 27 |

scanners-go 扫码枪转发器

28 |

本项目是一个用于转发扫码枪(条码枪)输入的工具,可以实现无焦点的情况下,将扫码结果转发到 http 服务器,基于 Golang 开发,适用于 Windows ,便于响应 Bar Code 或 Qr Code , 29 | 开箱即用。

30 |

特征

31 | 38 |

实现原理

39 |
    40 |
  1. 扫码枪属于 HID 设备,所有字符几乎能在同一瞬间按顺序输入完毕。
  2. 41 |
  3. 通常扫码枪在输入完毕后会带回车键作为结束符。
  4. 42 |
  5. 正则匹配内容。
  6. 43 | 44 |
45 |

本程序基于以上三点,实现扫码枪输入的准确识别,几乎不受扫码枪(扫描枪,条码枪)品牌限制。

46 |

使用指南

47 |

基本功能

48 |

通常默认就已经适合大多数情况使用了,你可以在 配置页(./config/) 里按照以下表格说明进行更精细的控制,配置页里可以实时生效修改,或者如果你熟悉 json 49 | 可以直接修改 config.json 文件并重新打开生效。

50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 |
设置项使用说明Key 值
转发接口 URL默认程序会在后台运行,将识别到的结果以 JSON 对象的格式 POST 给输入的后端 APIforwardURL
重置间隔 (毫秒)默认 500ms,可以调到更低,这个取决于扫码枪两个按键之间延迟,超过缓冲时间不会当成是扫码枪录入,会进行重置,用于区分人手和扫码枪,过滤掉人手输入。resetInterval
结束符后缀默认为回车键,即 CR,Enter 键,通常买的扫码枪,出厂都是带回车键作为结束键入的,有的扫码枪说明书可以自定义设置,如果和回车冲突,可以换 [TAB] 作为结尾标志来识别endSuffix
启动时打开主页打开后,在启动时会自动调用浏览器打开主页autoOpenPage
显示特殊按键(如 [LSHIFT], [TAB])开启后,你需要自己处理扫码枪的组合按键,扫码枪大小写通常是[LSHIFT]+字母,有的扫码枪说明书可以设置showSpecialChars
显示控制台窗口如果你下载的是文件名里有 debug 的,启动时会显示黑色的控制台窗口,可用来调试,debug 版即使关闭也不影响,注意,debug 版如果不打开此项,启动时会瞬时闪一下控制台窗口,非 88 | debug 的版本不会闪。showConsole
开机自动启动打开后,可以开机自启-
正则匹配可输入正则表达式,用来加强过滤。
例如:ij369/scanners-go可以写作[a-z]{2}\d{3}/[a-z]{8}\-[a-z]{2} 99 | 如果留空则会匹配所有内容
outputRegex
执行动作开启后,可以在识别后触发额外外部命令,可以实现使用浏览器打开等操作actions
109 |
110 |

除了 http 转发结果到后端接口,如果是纯前端项目,可以监听本 origin 的 Websocket ,端口同应用端口,路径为/ws,可以打开 WebTools 参考如何连接。

111 |

执行动作

112 |

执行动作是指会在发送给后端结果的同时,执行一次外部命令,满足特殊需求。

113 |
114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 149 | 152 | 153 | 154 | 155 |
设置项说明举例Key 值
命令或可执行文件路径命令或完整的可执行文件路径1. 调用 cmdcmd
2. 可执行文件路径: 128 | C:\Program Files\Google\Chrome\Application\chrome.exe 129 |
command
数据前缀可以在扫码结果前加上特定字符串https://github.com/ij369/scanners-go?id=prefix
数据后缀可以在扫码结果后加上特定字符串&lang=zh-CNsuffix
命令参数可以每行一个参数。使用 {data} 表示扫码结果插入位置, {data} 包含前缀和后缀。也可以不包含 147 | {data}. 148 | 1. 执行 cmd 命令/C echo {data} | clip
2. Chrome 参数启动 150 | --new-window {data} 151 |
arguments
156 |
157 |

占位符

158 |
159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 180 | 181 | 182 |
占位符作用
{timestamp}用于数据前缀,数据后缀,{timestamp} 的部分会替换成当前的 Unix 时间戳,可用于防止浏览器缓存,例如 &t={timestamp} . 170 |
{uuid}用于数据前缀,数据后缀,作用同上,会替换成 4 代 UUID.
{urlencode}用于数据前缀,数据后缀,可以使用两个 {urlencode} 包裹 URL 查询参数来实现编码,如果 {urlencode} 的个数为奇数,则最后一个的实现是将其后的部分进行 URI 179 | 编码。
183 |
184 |

 

185 |
186 |

如果本项目对你有帮助的话,欢迎 Fork / Star 本项目

187 |

项目地址

188 | 𝐡𝐭𝐭𝐩𝐬://𝐠𝐢𝐭𝐡𝐮𝐛.𝐜𝐨𝐦/𝐢𝐣𝟑𝟔𝟗/𝐬𝐜𝐚𝐧𝐧𝐞𝐫𝐬-𝐠𝐨 190 |
191 | 进入配置页 192 |
193 | 194 | 195 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "embed" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "net" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | "os/exec" 15 | "path/filepath" 16 | "regexp" 17 | "strings" 18 | "sync" 19 | "syscall" 20 | "time" 21 | "unsafe" 22 | 23 | "github.com/getlantern/systray" 24 | "github.com/google/uuid" 25 | "github.com/gorilla/websocket" 26 | "github.com/skratchdot/open-golang/open" 27 | "golang.org/x/sys/windows/registry" 28 | ) 29 | 30 | //go:embed static 31 | var staticFiles embed.FS 32 | 33 | //go:embed icon.ico 34 | var iconBytes []byte 35 | 36 | var ( 37 | user32 = syscall.NewLazyDLL("user32.dll") 38 | procSetWindowsHookEx = user32.NewProc("SetWindowsHookExW") 39 | procCallNextHookEx = user32.NewProc("CallNextHookEx") 40 | procUnhookWindowsHookEx = user32.NewProc("UnhookWindowsHookEx") 41 | procGetMessage = user32.NewProc("GetMessageW") 42 | procTranslateMessage = user32.NewProc("TranslateMessage") 43 | procDispatchMessage = user32.NewProc("DispatchMessageW") 44 | keyboardHook uintptr 45 | input strings.Builder 46 | lastKeyTime time.Time 47 | ) 48 | 49 | const ( 50 | WH_KEYBOARD_LL = 13 51 | WM_KEYDOWN = 256 52 | WM_KEYUP = 257 53 | WM_SYSKEYDOWN = 260 54 | WM_SYSKEYUP = 261 55 | VK_RETURN = 0x0D 56 | VK_TAB = 0x09 57 | VK_CAPITAL = 0x14 58 | VK_SHIFT = 0x10 59 | VK_LSHIFT = 0xA0 60 | VK_RSHIFT = 0xA1 61 | ) 62 | 63 | var ( 64 | leftShiftPressed bool 65 | rightShiftPressed bool 66 | capsLockOn bool 67 | ) 68 | 69 | type KBDLLHOOKSTRUCT struct { 70 | VkCode uint32 71 | ScanCode uint32 72 | Flags uint32 73 | Time uint32 74 | DwExtraInfo uintptr 75 | } 76 | 77 | type MSG struct { 78 | Hwnd uintptr 79 | Message uint32 80 | WParam uintptr 81 | LParam uintptr 82 | Time uint32 83 | Pt struct{ X, Y int32 } 84 | } 85 | 86 | type Config struct { 87 | ForwardURL string `json:"forwardURL"` 88 | ResetInterval int `json:"resetInterval"` 89 | Port int `json:"port"` 90 | AutoOpenPage bool `json:"autoOpenPage"` 91 | ShowSpecialChars bool `json:"showSpecialChars"` 92 | ShowConsole bool `json:"showConsole"` 93 | StartOnBoot bool `json:"startOnBoot"` 94 | EndSuffix string `json:"endSuffix"` 95 | OutputRegex string `json:"outputRegex"` 96 | Actions struct { 97 | Prefix string `json:"prefix"` 98 | Suffix string `json:"suffix"` 99 | Command string `json:"command"` 100 | Arguments []string `json:"arguments"` 101 | DataIndex int `json:"dataIndex"` 102 | } `json:"actions"` 103 | mu sync.RWMutex 104 | } 105 | 106 | var ( 107 | config = Config{ 108 | ForwardURL: "http://localhost:5000/scanner/endpoint", 109 | ResetInterval: 500, 110 | Port: 8080, 111 | AutoOpenPage: true, 112 | ShowSpecialChars: false, 113 | ShowConsole: false, 114 | StartOnBoot: false, 115 | EndSuffix: "ENTER", 116 | OutputRegex: "", 117 | } 118 | configFile string 119 | ) 120 | 121 | var ( 122 | upgrader = websocket.Upgrader{ 123 | CheckOrigin: func(r *http.Request) bool { 124 | return true 125 | }, 126 | } 127 | clients = make(map[*websocket.Conn]bool) 128 | broadcast = make(chan string) 129 | ) 130 | 131 | var ( 132 | kernel32 = syscall.NewLazyDLL("kernel32.dll") 133 | procGetConsoleWindow = kernel32.NewProc("GetConsoleWindow") 134 | procShowWindow = user32.NewProc("ShowWindow") 135 | ) 136 | 137 | const ( 138 | SW_HIDE = 0 139 | ) 140 | 141 | func hideConsole() { 142 | hwnd, _, _ := procGetConsoleWindow.Call() 143 | if hwnd != 0 { 144 | procShowWindow.Call(hwnd, SW_HIDE) 145 | } 146 | } 147 | 148 | func main() { 149 | loadConfig() 150 | 151 | config.mu.RLock() 152 | showConsole := config.ShowConsole 153 | config.mu.RUnlock() 154 | 155 | if !showConsole { 156 | hideConsole() 157 | } 158 | 159 | go startWebServer() 160 | 161 | // 启动系统托盘 162 | systray.Run(onReady, onExit) 163 | } 164 | 165 | func onReady() { 166 | systray.SetIcon(iconBytes) 167 | updateSystrayTitle(5) // 传递一个整数参数 168 | 169 | mOpenHome := systray.AddMenuItem("打开主页", "打开主页") 170 | mOpenConfig := systray.AddMenuItem("打开配置页", "打开配置页") 171 | mAbout := systray.AddMenuItem("关于", "查看 GitHub 页") 172 | systray.AddSeparator() 173 | mQuit := systray.AddMenuItem("退出", "退出程序") 174 | 175 | go func() { 176 | for { 177 | select { 178 | case <-mOpenHome.ClickedCh: 179 | config.mu.RLock() 180 | port := config.Port 181 | config.mu.RUnlock() 182 | open.Run(fmt.Sprintf("http://localhost:%d", port)) 183 | case <-mOpenConfig.ClickedCh: 184 | config.mu.RLock() 185 | port := config.Port 186 | config.mu.RUnlock() 187 | open.Run(fmt.Sprintf("http://localhost:%d/config", port)) 188 | case <-mAbout.ClickedCh: 189 | open.Run("https://github.com/ij369/scanners-go") 190 | case <-mQuit.ClickedCh: 191 | systray.Quit() 192 | return 193 | } 194 | } 195 | }() 196 | 197 | fmt.Println("开始监听键盘输入...") 198 | fmt.Println("请访问 http://localhost:8080 进行配置") 199 | 200 | keyboardHook, _, _ = procSetWindowsHookEx.Call( 201 | WH_KEYBOARD_LL, 202 | syscall.NewCallback(hookCallback), 203 | 0, 204 | 0, 205 | ) 206 | defer procUnhookWindowsHookEx.Call(keyboardHook) 207 | 208 | var msg MSG 209 | for { 210 | ret, _, _ := procGetMessage.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0) 211 | if ret == 0 { 212 | break 213 | } 214 | procTranslateMessage.Call(uintptr(unsafe.Pointer(&msg))) 215 | procDispatchMessage.Call(uintptr(unsafe.Pointer(&msg))) 216 | } 217 | } 218 | 219 | func onExit() { 220 | // 清理资源 221 | procUnhookWindowsHookEx.Call(keyboardHook) 222 | } 223 | 224 | func hookCallback(nCode int, wparam, lparam uintptr) uintptr { 225 | if nCode >= 0 { 226 | kbdstruct := (*KBDLLHOOKSTRUCT)(unsafe.Pointer(lparam)) 227 | vkCode := kbdstruct.VkCode 228 | 229 | config.mu.RLock() 230 | showSpecialChars := config.ShowSpecialChars 231 | endSuffix := config.EndSuffix 232 | config.mu.RUnlock() 233 | 234 | switch wparam { 235 | case WM_KEYDOWN, WM_SYSKEYDOWN: 236 | now := time.Now() 237 | 238 | config.mu.RLock() 239 | resetInterval := time.Duration(config.ResetInterval) * time.Millisecond 240 | config.mu.RUnlock() 241 | 242 | if now.Sub(lastKeyTime) > resetInterval { 243 | input.Reset() 244 | } 245 | lastKeyTime = now 246 | 247 | switch vkCode { 248 | case VK_RETURN: 249 | if endSuffix == "ENTER" && input.Len() > 0 { 250 | go sendData(input.String()) 251 | fmt.Printf("发送数据: %s\n", input.String()) 252 | input.Reset() 253 | } else if showSpecialChars { 254 | input.WriteString("[ENTER]") 255 | } 256 | case VK_TAB: 257 | if endSuffix == "TAB" && input.Len() > 0 { 258 | go sendData(input.String()) 259 | fmt.Printf("发送数据: %s\n", input.String()) 260 | input.Reset() 261 | } else if showSpecialChars { 262 | input.WriteString("[TAB]") 263 | } 264 | case VK_SHIFT, VK_LSHIFT, VK_RSHIFT: 265 | if vkCode == VK_LSHIFT { 266 | leftShiftPressed = true 267 | if showSpecialChars { 268 | input.WriteString("[LSHIFT]") 269 | } 270 | } else if vkCode == VK_RSHIFT { 271 | rightShiftPressed = true 272 | if showSpecialChars { 273 | input.WriteString("[RSHIFT]") 274 | } 275 | } else { 276 | leftShiftPressed = true 277 | rightShiftPressed = true 278 | if showSpecialChars { 279 | input.WriteString("[SHIFT]") 280 | } 281 | } 282 | case VK_CAPITAL: 283 | capsLockOn = !capsLockOn 284 | default: 285 | char := getChar(vkCode, leftShiftPressed || rightShiftPressed, capsLockOn) 286 | if char != 0 { 287 | input.WriteRune(char) 288 | } 289 | } 290 | case WM_KEYUP, WM_SYSKEYUP: 291 | switch vkCode { 292 | case VK_SHIFT, VK_LSHIFT, VK_RSHIFT: 293 | if vkCode == VK_LSHIFT { 294 | leftShiftPressed = false 295 | if showSpecialChars { 296 | input.WriteString("[/LSHIFT]") 297 | } 298 | } else if vkCode == VK_RSHIFT { 299 | rightShiftPressed = false 300 | if showSpecialChars { 301 | input.WriteString("[/RSHIFT]") 302 | } 303 | } else { 304 | leftShiftPressed = false 305 | rightShiftPressed = false 306 | if showSpecialChars { 307 | input.WriteString("[/SHIFT]") 308 | } 309 | } 310 | } 311 | } 312 | } 313 | ret, _, _ := procCallNextHookEx.Call(keyboardHook, uintptr(nCode), wparam, lparam) 314 | return ret 315 | } 316 | 317 | func getChar(vkCode uint32, shiftPressed, capsLock bool) rune { 318 | // 处理特殊字符 319 | if shiftPressed { 320 | return getShiftChar(vkCode) 321 | } 322 | 323 | // 基本字符映射 324 | char := rune(mapVirtualKey(vkCode)) 325 | 326 | // 处理字母 327 | if char >= 'A' && char <= 'Z' { 328 | // 默认为小写 329 | char += 32 330 | 331 | // 如果 Caps Lock 开启,则使用大写 332 | if capsLock { 333 | char -= 32 334 | } 335 | } 336 | 337 | return char 338 | } 339 | 340 | func getShiftChar(vkCode uint32) rune { 341 | shiftMap := map[uint32]rune{ 342 | '0': ')', '1': '!', '2': '@', '3': '#', '4': '$', '5': '%', 343 | '6': '^', '7': '&', '8': '*', '9': '(', 344 | 0xBD: '_', // - 345 | 0xBB: '+', // = 346 | 0xDB: '{', // [ 347 | 0xDD: '}', // ] 348 | 0xDC: '|', // \ 349 | 0xBA: ':', // ; 350 | 0xDE: '"', // ' 351 | 0xBC: '<', // , 352 | 0xBE: '>', // . 353 | 0xBF: '?', // / 354 | 0xC0: '~', // ` 355 | } 356 | 357 | if char, ok := shiftMap[vkCode]; ok { 358 | return char 359 | } 360 | 361 | // 如果不在映射表中,返回大写字母 362 | return rune(mapVirtualKey(vkCode)) 363 | } 364 | 365 | // 将虚拟键码映射到实际字符 366 | func mapVirtualKey(vkCode uint32) uint16 { 367 | ret, _, _ := syscall.NewLazyDLL("user32.dll").NewProc("MapVirtualKeyW").Call( 368 | uintptr(vkCode), 369 | uintptr(2), // MAPVK_VK_TO_CHAR 370 | ) 371 | return uint16(ret) 372 | } 373 | 374 | func sendData(data string) { 375 | config.mu.RLock() 376 | regex := config.OutputRegex 377 | url := config.ForwardURL 378 | actions := config.Actions 379 | config.mu.RUnlock() 380 | 381 | // 如果设置了正则表达式,进行匹配 382 | if regex != "" { 383 | matched, err := regexp.MatchString(regex, data) 384 | if err != nil { 385 | fmt.Println("正则表达式匹配错误:", err) 386 | return 387 | } 388 | if !matched { 389 | fmt.Println("数据不匹配正则表达式,不发送") 390 | return 391 | } 392 | } 393 | 394 | timestamp := fmt.Sprintf("%d", time.Now().Unix()) // 时间戳 395 | uuid := uuid.New().String() // UUIDv4 396 | 397 | processedData := actions.Prefix + data + actions.Suffix // 应用前缀和后缀 398 | processedData = strings.ReplaceAll(processedData, "{timestamp}", timestamp) 399 | processedData = strings.ReplaceAll(processedData, "{uuid}", uuid) 400 | 401 | processedData = handleURLEncode(processedData) // 处理 {urlencode} 占位符 402 | 403 | jsonData := map[string]string{ 404 | "data": data, // 原始数据 405 | "processedData": processedData, // 原始数据加前缀和后缀 406 | } 407 | jsonStr, err := json.Marshal(jsonData) 408 | if err != nil { 409 | fmt.Println("JSON 序列化失败:", err) 410 | return 411 | } 412 | 413 | // 发送 POST 请求 414 | go func() { 415 | resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonStr)) 416 | if err != nil { 417 | fmt.Println("发送数据失败:", err) 418 | } else { 419 | defer resp.Body.Close() 420 | fmt.Println("数据发送成功,状态码:", resp.StatusCode) 421 | } 422 | }() 423 | 424 | // 执行外部命令 425 | if actions.Command != "" { 426 | go executeCommand(actions.Command, actions.Arguments, actions.DataIndex, processedData) 427 | } 428 | 429 | // 广播到 WebSocket 430 | broadcast <- data 431 | } 432 | 433 | func handleURLEncode(data string) string { 434 | var result strings.Builder 435 | start := 0 436 | for { 437 | startIndex := strings.Index(data[start:], "{urlencode}") 438 | if startIndex == -1 { 439 | result.WriteString(data[start:]) 440 | break 441 | } 442 | startIndex += start 443 | result.WriteString(data[start:startIndex]) 444 | endIndex := strings.Index(data[startIndex+len("{urlencode}"):], "{urlencode}") 445 | if endIndex == -1 { 446 | // 如果找不到下一个 {urlencode},则其后的部分进行 URI 编码 447 | toEncode := data[startIndex+len("{urlencode}"):] 448 | result.WriteString(url.QueryEscape(toEncode)) 449 | break 450 | } 451 | endIndex += startIndex + len("{urlencode}") 452 | toEncode := data[startIndex+len("{urlencode}"):endIndex] 453 | result.WriteString(url.QueryEscape(toEncode)) 454 | start = endIndex + len("{urlencode}") 455 | } 456 | return result.String() 457 | } 458 | 459 | func executeCommand(command string, args []string, dataIndex int, data string) { 460 | command = filepath.Clean(command) 461 | 462 | // 创建一个新的参数切片,将扫码结果插入到指定位置 463 | newArgs := make([]string, 0, len(args)) 464 | for _, arg := range args { 465 | // 替换参数中的 {data} 占位 466 | arg = strings.TrimSpace(strings.ReplaceAll(arg, "{data}", data)) 467 | if arg != "" { 468 | newArgs = append(newArgs, arg) 469 | } 470 | } 471 | 472 | fmt.Printf("执行命令: %s\n", command) 473 | fmt.Printf("执行参数: %v\n", newArgs) 474 | 475 | // 使用 exec.Command 执行命令 476 | cmd := exec.Command(command, newArgs...) 477 | 478 | // 设置工作目录为当前可执行文件所在目录 479 | exePath, err := os.Executable() 480 | if err == nil { 481 | cmd.Dir = filepath.Dir(exePath) 482 | } 483 | 484 | err = cmd.Start() 485 | if err != nil { 486 | fmt.Printf("执行命令失败: %v\n", err) 487 | } else { 488 | fmt.Println("命令执行成功") 489 | } 490 | } 491 | 492 | func startWebServer() { 493 | // 前端相关路由 494 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 495 | filePath := strings.TrimPrefix(r.URL.Path, "/") 496 | if filePath == "" || strings.HasSuffix(filePath, "/") { 497 | filePath += "index.html" 498 | } 499 | 500 | content, err := staticFiles.ReadFile("static/" + filePath) 501 | if err != nil { 502 | // 如果文件不存在,尝试读取对应目录下的 index.html 503 | if strings.HasSuffix(err.Error(), "file does not exist") { 504 | indexPath := filepath.Join(filepath.Dir(filePath), "index.html") 505 | content, err = staticFiles.ReadFile("static/" + indexPath) 506 | if err != nil { 507 | http.NotFound(w, r) 508 | return 509 | } 510 | } else { 511 | http.NotFound(w, r) 512 | return 513 | } 514 | } 515 | 516 | contentType := getContentType(filePath) 517 | w.Header().Set("Content-Type", contentType) 518 | w.Write(content) 519 | }) 520 | 521 | // API 路由 522 | http.HandleFunc("/api/config", handleConfig) 523 | 524 | // WebSocket 路由 525 | http.HandleFunc("/ws", handleConnections) 526 | 527 | // 启动广播协程 528 | go handleBroadcasts() 529 | 530 | config.mu.RLock() 531 | initialPort := config.Port 532 | autoOpenPage := config.AutoOpenPage 533 | config.mu.RUnlock() 534 | 535 | maxAttempts := 5 536 | var server *http.Server 537 | var successPort int 538 | 539 | for attempt := 0; attempt < maxAttempts; attempt++ { 540 | port := initialPort + attempt 541 | addr := fmt.Sprintf(":%d", port) 542 | server = &http.Server{Addr: addr, Handler: nil} 543 | 544 | errChan := make(chan error, 1) 545 | go func() { 546 | fmt.Printf("尝试在端口 %d 启动 Web 服务器\n", port) 547 | errChan <- server.ListenAndServe() 548 | }() 549 | 550 | // 等待服务器启动或出错 551 | select { 552 | case err := <-errChan: 553 | if err != nil { 554 | log.Printf("在端口 %d 启动服务器失败: %v\n", port, err) 555 | continue // 尝试下一个端口 556 | } 557 | case <-time.After(500 * time.Millisecond): 558 | // 进行进一步检查服务器 559 | if isPortAvailable(port) { 560 | successPort = port 561 | break 562 | } 563 | } 564 | 565 | if successPort != 0 { 566 | break 567 | } 568 | } 569 | 570 | if successPort == 0 { 571 | log.Fatal("无法启动 Web 服务器") 572 | } 573 | 574 | fmt.Printf("Web 服务器成功启动在 http://localhost:%d\n", successPort) 575 | 576 | // 更新系统托盘标题 577 | updateSystrayTitle(successPort) 578 | 579 | if autoOpenPage { 580 | open.Run(fmt.Sprintf("http://localhost:%d/", successPort)) 581 | } 582 | } 583 | 584 | func isPortAvailable(port int) bool { 585 | conn, err := net.DialTimeout("tcp", fmt.Sprintf("localhost:%d", port), time.Second) 586 | if err != nil { 587 | return false 588 | } 589 | conn.Close() 590 | return true 591 | } 592 | 593 | func handleConnections(w http.ResponseWriter, r *http.Request) { 594 | ws, err := upgrader.Upgrade(w, r, nil) 595 | if err != nil { 596 | log.Fatal(err) 597 | } 598 | defer ws.Close() 599 | 600 | clients[ws] = true 601 | 602 | // 发送初始连接消息 603 | ws.WriteMessage(websocket.TextMessage, []byte("CONNECTED")) 604 | 605 | for { 606 | _, _, err := ws.ReadMessage() 607 | if err != nil { 608 | delete(clients, ws) 609 | break 610 | } 611 | } 612 | } 613 | 614 | func handleBroadcasts() { 615 | for { 616 | msg := <-broadcast 617 | for client := range clients { 618 | err := client.WriteMessage(websocket.TextMessage, []byte(msg)) 619 | if err != nil { 620 | log.Printf("Websocket error: %v", err) 621 | client.Close() 622 | delete(clients, client) 623 | } 624 | } 625 | } 626 | } 627 | 628 | func handleConfig(w http.ResponseWriter, r *http.Request) { 629 | if r.Method == "GET" { 630 | config.mu.RLock() 631 | json.NewEncoder(w).Encode(config) 632 | config.mu.RUnlock() 633 | } else if r.Method == "POST" { 634 | var newConfig Config 635 | if err := json.NewDecoder(r.Body).Decode(&newConfig); err != nil { 636 | http.Error(w, err.Error(), http.StatusBadRequest) 637 | return 638 | } 639 | config.mu.Lock() 640 | config.ForwardURL = newConfig.ForwardURL 641 | config.ResetInterval = newConfig.ResetInterval 642 | config.AutoOpenPage = newConfig.AutoOpenPage 643 | config.ShowSpecialChars = newConfig.ShowSpecialChars 644 | config.ShowConsole = newConfig.ShowConsole 645 | config.StartOnBoot = newConfig.StartOnBoot 646 | config.EndSuffix = newConfig.EndSuffix 647 | config.OutputRegex = newConfig.OutputRegex 648 | config.Actions = newConfig.Actions 649 | // 不更新端口,当前端口可能已经被调整 650 | config.mu.Unlock() 651 | saveConfig() 652 | 653 | // 如果启用或禁用了开机自启动,更新注册表 654 | updateStartOnBoot(newConfig.StartOnBoot) 655 | 656 | w.WriteHeader(http.StatusOK) 657 | } else { 658 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 659 | } 660 | } 661 | 662 | func updateStartOnBoot(enable bool) { 663 | key, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Run`, registry.ALL_ACCESS) 664 | if err != nil { 665 | log.Printf("无法打开注册表键: %v", err) 666 | return 667 | } 668 | defer key.Close() 669 | 670 | exePath, err := os.Executable() 671 | if err != nil { 672 | log.Printf("无法获取可执行文件路径: %v", err) 673 | return 674 | } 675 | 676 | // 删除所有旧的开机启动设置 677 | key.DeleteValue("ScannerApp") 678 | 679 | if enable { 680 | err = key.SetStringValue("ScannerApp", exePath) 681 | if err != nil { 682 | log.Printf("设置开机自启动失败: %v", err) 683 | } 684 | } else { 685 | log.Println("开机自启动已取消") 686 | } 687 | } 688 | 689 | func loadConfig() { 690 | data, err := ioutil.ReadFile(configFile) 691 | if err != nil { 692 | if !os.IsNotExist(err) { 693 | log.Printf("读取配置文失败: %v", err) 694 | } 695 | return 696 | } 697 | 698 | config.mu.Lock() 699 | defer config.mu.Unlock() 700 | if err := json.Unmarshal(data, &config); err != nil { 701 | log.Printf("解析配置文件失败: %v", err) 702 | } 703 | } 704 | 705 | func saveConfig() { 706 | config.mu.RLock() 707 | data, err := json.MarshalIndent(config, "", " ") 708 | config.mu.RUnlock() 709 | 710 | if err != nil { 711 | log.Printf("序列化配置失败: %v", err) 712 | return 713 | } 714 | 715 | err = ioutil.WriteFile(configFile, data, 0644) 716 | if err != nil { 717 | log.Printf("保存配置文件失败: %v", err) 718 | } 719 | } 720 | 721 | func init() { 722 | ex, err := os.Executable() 723 | if err != nil { 724 | log.Fatal(err) 725 | } 726 | exPath := filepath.Dir(ex) 727 | configFile = filepath.Join(exPath, "scanner_config.json") 728 | } 729 | 730 | func updateSystrayTitle(port int) { 731 | systray.SetTitle(fmt.Sprintf("扫码枪程序:%d", port)) 732 | systray.SetTooltip(fmt.Sprintf("扫码枪程序 (端口: %d)", port)) 733 | } 734 | 735 | // 获取文件的 Content-Type 736 | func getContentType(filePath string) string { 737 | ext := filepath.Ext(filePath) 738 | switch ext { 739 | case ".html": 740 | return "text/html" 741 | case ".css": 742 | return "text/css" 743 | case ".js": 744 | return "application/javascript" 745 | case ".png": 746 | return "image/png" 747 | case ".jpg", ".jpeg": 748 | return "image/jpeg" 749 | case ".gif": 750 | return "image/gif" 751 | case ".svg": 752 | return "image/svg+xml" 753 | case ".ico": 754 | return "image/x-icon" 755 | case ".wav": 756 | return "audio/wav" 757 | case ".avif": 758 | return "image/avif" 759 | case ".webp": 760 | return "image/webp" 761 | case ".mp4": 762 | return "video/mp4" 763 | case ".webm": 764 | return "video/webm" 765 | default: 766 | return "application/octet-stream" 767 | } 768 | } 769 | -------------------------------------------------------------------------------- /static/sweetalert2.all.min-10.8.0.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sweetalert2=e()}(this,function(){"use strict";function r(t){return(r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function a(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function o(t,e){for(var n=0;nt.clientHeight)}function ut(t){var e=window.getComputedStyle(t),t=parseFloat(e.getPropertyValue("animation-duration")||"0"),e=parseFloat(e.getPropertyValue("transition-duration")||"0");return 0\n
\n
    \n
    \n
    \n
    \n
    \n
    \n \n

    \n \n
    \n
    \n
    \n \n \n
    \n \n \n
    \n \n
    \n \n \n
    \n
    \n
    \n
    \n \n \n \n
    \n
    \n
    \n
    \n
    \n \n').replace(/(^|\n)\s*/g,""),Ct=function(t){var e,n,o,i,r,a=!!(i=C())&&(i.parentNode.removeChild(i),vt([document.documentElement,document.body],[$["no-backdrop"],$["toast-shown"],$["has-column"]]),!0);dt()?W("SweetAlert2 requires document to initialize"):((r=document.createElement("div")).className=$.container,a&>(r,$["no-transition"]),U(r,wt),(i="string"==typeof(e=t.target)?document.querySelector(e):e).appendChild(r),a=t,(e=x()).setAttribute("role",a.toast?"alert":"dialog"),e.setAttribute("aria-live",a.toast?"polite":"assertive"),a.toast||e.setAttribute("aria-modal","true"),r=i,"rtl"===window.getComputedStyle(r).direction&>(C(),$.rtl),t=A(),a=yt(t,$.input),e=yt(t,$.file),n=t.querySelector(".".concat($.range," input")),o=t.querySelector(".".concat($.range," output")),i=yt(t,$.select),r=t.querySelector(".".concat($.checkbox," input")),t=yt(t,$.textarea),a.oninput=pt,e.onchange=pt,i.onchange=pt,r.onchange=pt,t.oninput=pt,n.oninput=function(t){pt(t),o.value=n.value},n.onchange=function(t){pt(t),n.nextSibling.value=n.value})},kt=function(t,e){t.jquery?xt(e,t):U(e,t.toString())},xt=function(t,e){if(t.textContent="",0 in e)for(var n=0;n in e;n++)t.appendChild(e[n].cloneNode(!0));else t.appendChild(e.cloneNode(!0))},Bt=function(){if(dt())return!1;var t,e=document.createElement("div"),n={WebkitAnimation:"webkitAnimationEnd",OAnimation:"oAnimationEnd oanimationend",animation:"animationend"};for(t in n)if(Object.prototype.hasOwnProperty.call(n,t)&&void 0!==e.style[t])return n[t];return!1}();function Pt(t,e,n){var o;st(t,n["show".concat((o=e).charAt(0).toUpperCase()+o.slice(1),"Button")],"inline-block"),U(t,n["".concat(e,"ButtonText")]),t.setAttribute("aria-label",n["".concat(e,"ButtonAriaLabel")]),t.className=$[e],F(t,n,"".concat(e,"Button")),gt(t,n["".concat(e,"ButtonClass")])}function At(t,e){var n,o,i=C();i&&(o=i,"string"==typeof(n=e.backdrop)?o.style.background=n:n||gt([document.documentElement,document.body],$["no-backdrop"]),!e.backdrop&&e.allowOutsideClick&&z('"allowOutsideClick" parameter requires `backdrop` parameter to be set to `true`'),o=i,(n=e.position)in $?gt(o,$[n]):(z('The "position" parameter is not valid, defaulting to "center"'),gt(o,$.center)),n=i,!(o=e.grow)||"string"!=typeof o||(o="grow-".concat(o))in $&>(n,$[o]),F(i,e,"container"),(e=document.body.getAttribute("data-swal2-queue-step"))&&(i.setAttribute("data-queue-step",e),document.body.removeAttribute("data-swal2-queue-step")))}function Et(t,e){t.placeholder&&!e.inputPlaceholder||(t.placeholder=e.inputPlaceholder)}function Ot(t,e,n){var o,i;n.inputLabel&&(t.id=$.input,o=document.createElement("label"),i=$["input-label"],o.setAttribute("for",t.id),o.className=i,o.innerText=n.inputLabel,e.insertAdjacentElement("beforebegin",o))}var St={promise:new WeakMap,innerParams:new WeakMap,domCache:new WeakMap},Tt=["input","file","range","select","radio","checkbox","textarea"],Lt=function(t){if(!jt[t.input])return W('Unexpected type of input! Expected "text", "email", "password", "number", "tel", "select", "radio", "checkbox", "textarea", "file" or "url", got "'.concat(t.input,'"'));var e=qt(t.input),n=jt[t.input](e,t);it(n),setTimeout(function(){et(n)})},Dt=function(t,e){var n=tt(A(),t);if(n)for(var o in!function(t){for(var e=0;e=o.progressSteps.length&&z("Invalid currentProgressStep parameter, it should be less than progressSteps.length (currentProgressStep like JS arrays starts from 0)"),o.progressSteps.forEach(function(t,e){var n,t=(n=t,t=document.createElement("li"),gt(t,$["progress-step"]),U(t,n),t);i.appendChild(t),e===r&>(t,$["active-progress-step"]),e!==o.progressSteps.length-1&&(t=o,e=document.createElement("li"),gt(e,$["progress-step-line"]),t.progressStepsDistance&&(e.style.width=t.progressStepsDistance),e=e,i.appendChild(e))})}function Rt(t,e){var n=j();F(n,e,"header"),Ht(0,e),function(t,e){t=St.innerParams.get(t);t&&e.icon===t.icon&&B()?Ft(B(),e):(_t(),e.icon&&(-1!==Object.keys(J).indexOf(e.icon)?(t=k(".".concat($.icon,".").concat(J[e.icon])),it(t),Wt(t,e),Ft(t,e),gt(t,e.showClass.icon)):W('Unknown icon! Expected "success", "error", "warning", "info" or "question", got "'.concat(e.icon,'"'))))}(t,e),function(t){var e=E();if(!t.imageUrl)return rt(e);it(e,""),e.setAttribute("src",t.imageUrl),e.setAttribute("alt",t.imageAlt),ot(e,"width",t.imageWidth),ot(e,"height",t.imageHeight),e.className=$.image,F(e,t,"image")}(e),n=e,t=P(),st(t,n.title||n.titleText),n.title&&ft(n.title,t),n.titleText&&(t.innerText=n.titleText),F(t,n,"title"),n=e,e=H(),U(e,n.closeButtonHtml),F(e,n,"closeButton"),st(e,n.showCloseButton),e.setAttribute("aria-label",n.closeButtonAriaLabel)}function Nt(t,e){var n,o;o=e,n=x(),ot(n,"width",o.width),ot(n,"padding",o.padding),o.background&&(n.style.background=o.background),Qt(n,o),At(0,e),Rt(t,e),Mt(t,e),mt(0,e),o=e,t=M(),st(t,o.footer),o.footer&&ft(o.footer,t),F(t,o,"footer"),"function"==typeof e.didRender?e.didRender(x()):"function"==typeof e.onRender&&e.onRender(x())}function Ut(){return T()&&T().click()}var _t=function(){for(var t=n(),e=0;e\n \n
    \n
    \n '):"error"===e.icon?U(t,'\n \n \n \n \n '):U(t,Yt({question:"?",warning:"!",info:"i"}[e.icon]))},Kt=function(t,e){if(e.iconColor){t.style.color=e.iconColor,t.style.borderColor=e.iconColor;for(var n=0,o=[".swal2-success-line-tip",".swal2-success-line-long",".swal2-x-mark-line-left",".swal2-x-mark-line-right"];n').concat(t,"")},Zt=[],Qt=function(t,e){t.className="".concat($.popup," ").concat(bt(t)?e.showClass.popup:""),e.toast?(gt([document.documentElement,document.body],$["toast-shown"]),gt(t,$.toast)):gt(t,$.modal),F(t,e,"popup"),"string"==typeof e.customClass&>(t,e.customClass),e.icon&>(t,$["icon-".concat(e.icon)])};function $t(){var t=x();t||wn.fire(),t=x();var e=q(),n=T(),o=D();it(e),rt(n),gt([t,e],$.loading),it(o),t.setAttribute("data-loading",!0),t.setAttribute("aria-busy",!0),t.focus()}function Jt(){return new Promise(function(t){var e=window.scrollX,n=window.scrollY;oe.restoreFocusTimeout=setTimeout(function(){oe.previousActiveElement&&oe.previousActiveElement.focus?(oe.previousActiveElement.focus(),oe.previousActiveElement=null):document.body&&document.body.focus(),t()},100),void 0!==e&&void 0!==n&&window.scrollTo(e,n)})}function Xt(){if(oe.timeout)return function(){var t=V(),e=parseInt(window.getComputedStyle(t).width);t.style.removeProperty("transition"),t.style.width="100%";var n=parseInt(window.getComputedStyle(t).width),n=parseInt(e/n*100);t.style.removeProperty("transition"),t.style.width="".concat(n,"%")}(),oe.timeout.stop()}function Gt(){if(oe.timeout){var t=oe.timeout.start();return lt(t),t}}function te(t){return Object.prototype.hasOwnProperty.call(ie,t)}function ee(t){return ae[t]}function ne(t){for(var e in t)te(o=e)||z('Unknown parameter "'.concat(o,'"')),t.toast&&(n=e,-1!==se.indexOf(n)&&z('The parameter "'.concat(n,'" is incompatible with toasts'))),ee(n=e)&&g(n,ee(n));var n,o}var oe={},ie={title:"",titleText:"",text:"",html:"",footer:"",icon:void 0,iconColor:void 0,iconHtml:void 0,toast:!1,animation:!0,showClass:{popup:"swal2-show",backdrop:"swal2-backdrop-show",icon:"swal2-icon-show"},hideClass:{popup:"swal2-hide",backdrop:"swal2-backdrop-hide",icon:"swal2-icon-hide"},customClass:void 0,target:"body",backdrop:!0,heightAuto:!0,allowOutsideClick:!0,allowEscapeKey:!0,allowEnterKey:!0,stopKeydownPropagation:!0,keydownListenerCapture:!1,showConfirmButton:!0,showDenyButton:!1,showCancelButton:!1,preConfirm:void 0,confirmButtonText:"OK",confirmButtonAriaLabel:"",confirmButtonColor:void 0,denyButtonText:"No",denyButtonAriaLabel:"",denyButtonColor:void 0,cancelButtonText:"Cancel",cancelButtonAriaLabel:"",cancelButtonColor:void 0,buttonsStyling:!0,reverseButtons:!1,focusConfirm:!0,focusDeny:!1,focusCancel:!1,showCloseButton:!1,closeButtonHtml:"×",closeButtonAriaLabel:"Close this dialog",loaderHtml:"",showLoaderOnConfirm:!1,imageUrl:void 0,imageWidth:void 0,imageHeight:void 0,imageAlt:"",timer:void 0,timerProgressBar:!1,width:void 0,padding:void 0,background:void 0,input:void 0,inputPlaceholder:"",inputLabel:"",inputValue:"",inputOptions:{},inputAutoTrim:!0,inputAttributes:{},inputValidator:void 0,returnInputValueOnDeny:!1,validationMessage:void 0,grow:!1,position:"center",progressSteps:[],currentProgressStep:void 0,progressStepsDistance:void 0,onBeforeOpen:void 0,onOpen:void 0,willOpen:void 0,didOpen:void 0,onRender:void 0,didRender:void 0,onClose:void 0,onAfterClose:void 0,willClose:void 0,didClose:void 0,onDestroy:void 0,didDestroy:void 0,scrollbarPadding:!0},re=["allowEscapeKey","allowOutsideClick","background","buttonsStyling","cancelButtonAriaLabel","cancelButtonColor","cancelButtonText","closeButtonAriaLabel","closeButtonHtml","confirmButtonAriaLabel","confirmButtonColor","confirmButtonText","currentProgressStep","customClass","denyButtonAriaLabel","denyButtonColor","denyButtonText","didClose","didDestroy","footer","hideClass","html","icon","iconColor","imageAlt","imageHeight","imageUrl","imageWidth","onAfterClose","onClose","onDestroy","progressSteps","reverseButtons","showCancelButton","showCloseButton","showConfirmButton","showDenyButton","text","title","titleText","willClose"],ae={animation:'showClass" and "hideClass',onBeforeOpen:"willOpen",onOpen:"didOpen",onRender:"didRender",onClose:"willClose",onAfterClose:"didClose",onDestroy:"didDestroy"},se=["allowOutsideClick","allowEnterKey","backdrop","focusConfirm","focusDeny","focusCancel","heightAuto","keydownListenerCapture"],ce=Object.freeze({isValidParameter:te,isUpdatableParameter:function(t){return-1!==re.indexOf(t)},isDeprecatedParameter:ee,argsToParams:function(n){var o={};return"object"!==r(n[0])||w(n[0])?["title","html","icon"].forEach(function(t,e){e=n[e];"string"==typeof e||w(e)?o[t]=e:void 0!==e&&W("Unexpected type of ".concat(t,'! Expected "string" or "Element", got ').concat(r(e)))}):c(o,n[0]),o},isVisible:function(){return bt(x())},clickConfirm:Ut,clickDeny:function(){return L()&&L().click()},clickCancel:function(){return I()&&I().click()},getContainer:C,getPopup:x,getTitle:P,getContent:A,getHtmlContainer:function(){return t($["html-container"])},getImage:E,getIcon:B,getIcons:n,getInputLabel:function(){return t($["input-label"])},getCloseButton:H,getActions:q,getConfirmButton:T,getDenyButton:L,getCancelButton:I,getLoader:D,getHeader:j,getFooter:M,getTimerProgressBar:V,getFocusableElements:R,getValidationMessage:S,isLoading:function(){return x().hasAttribute("data-loading")},fire:function(){for(var t=arguments.length,e=new Array(t),n=0;nwindow.innerHeight&&(G.previousBodyPadding=parseInt(window.getComputedStyle(document.body).getPropertyValue("padding-right")),document.body.style.paddingRight="".concat(G.previousBodyPadding+function(){var t=document.createElement("div");t.className=$["scrollbar-measure"],document.body.appendChild(t);var e=t.getBoundingClientRect().width-t.clientWidth;return document.body.removeChild(t),e}(),"px"))}function de(){return!!window.MSInputMethodContext&&!!document.documentMode}function pe(){var t=C(),e=x();t.style.removeProperty("align-items"),e.offsetTop<0&&(t.style.alignItems="flex-start")}var fe=function(){navigator.userAgent.match(/(CriOS|FxiOS|EdgiOS|YaBrowser|UCBrowser)/i)||x().scrollHeight>window.innerHeight-44&&(C().style.paddingBottom="".concat(44,"px"))},me=function(){var e,t=C();t.ontouchstart=function(t){e=he(t)},t.ontouchmove=function(t){e&&(t.preventDefault(),t.stopPropagation())}},he=function(t){var e=t.target,n=C();return(!t.touches||!t.touches.length||"stylus"!==t.touches[0].touchType)&&(e===n||!(ct(n)||"INPUT"===e.tagName||ct(A())&&A().contains(e)))},ge={swalPromiseResolve:new WeakMap};function ve(t,e,n,o){n?xe(t,o):(Jt().then(function(){return xe(t,o)}),oe.keydownTarget.removeEventListener("keydown",oe.keydownHandler,{capture:oe.keydownListenerCapture}),oe.keydownHandlerAdded=!1),e.parentNode&&!document.body.getAttribute("data-swal2-queue-step")&&e.parentNode.removeChild(e),N()&&(null!==G.previousBodyPadding&&(document.body.style.paddingRight="".concat(G.previousBodyPadding,"px"),G.previousBodyPadding=null),_(document.body,$.iosfix)&&(e=parseInt(document.body.style.top,10),vt(document.body,$.iosfix),document.body.style.top="",document.body.scrollTop=-1*e),"undefined"!=typeof window&&de()&&window.removeEventListener("resize",pe),h(document.body.children).forEach(function(t){t.hasAttribute("data-previous-aria-hidden")?(t.setAttribute("aria-hidden",t.getAttribute("data-previous-aria-hidden")),t.removeAttribute("data-previous-aria-hidden")):t.removeAttribute("aria-hidden")})),vt([document.documentElement,document.body],[$.shown,$["height-auto"],$["no-backdrop"],$["toast-shown"],$["toast-column"]])}function ye(t){var e,n,o,i=x();i&&(t=be(t),(e=St.innerParams.get(this))&&!_(i,e.hideClass.popup)&&(n=ge.swalPromiseResolve.get(this),vt(i,e.showClass.popup),gt(i,e.hideClass.popup),o=C(),vt(o,e.showClass.backdrop),gt(o,e.hideClass.backdrop),we(this,i,e),n(t)))}function be(t){return void 0===t?{isConfirmed:!1,isDenied:!1,isDismissed:!0}:c({isConfirmed:!1,isDenied:!1,isDismissed:!1},t)}function we(t,e,n){var o=C(),i=Bt&&ut(e),r=n.onClose,a=n.onAfterClose,s=n.willClose,n=n.didClose;Ce(e,s,r),i?ke(t,e,o,n||a):ve(t,o,X(),n||a)}var Ce=function(t,e,n){null!==e&&"function"==typeof e?e(t):null!==n&&"function"==typeof n&&n(t)},ke=function(t,e,n,o){oe.swalCloseEventFinishedCallback=ve.bind(null,t,n,X(),o),e.addEventListener(Bt,function(t){t.target===e&&(oe.swalCloseEventFinishedCallback(),delete oe.swalCloseEventFinishedCallback)})},xe=function(t,e){setTimeout(function(){"function"==typeof e&&e(),t._destroy()})};function Be(t,e,n){var o=St.domCache.get(t);e.forEach(function(t){o[t].disabled=n})}function Pe(t,e){if(!t)return!1;if("radio"===t.type)for(var n=t.parentNode.parentNode.querySelectorAll("input"),o=0;o")),Ct(t)}function Se(t){var e=C(),n=x();"function"==typeof t.willOpen?t.willOpen(n):"function"==typeof t.onBeforeOpen&&t.onBeforeOpen(n);var o=window.getComputedStyle(document.body).overflowY;Me(e,n,t),setTimeout(function(){qe(e,n)},10),N()&&(je(e,t.scrollbarPadding,o),h(document.body.children).forEach(function(t){t===C()||function(t,e){if("function"==typeof t.contains)return t.contains(e)}(t,C())||(t.hasAttribute("aria-hidden")&&t.setAttribute("data-previous-aria-hidden",t.getAttribute("aria-hidden")),t.setAttribute("aria-hidden","true"))})),X()||oe.previousActiveElement||(oe.previousActiveElement=document.activeElement),Ie(n,t),vt(e,$["no-transition"])}function Te(t){var e=x();t.target===e&&(t=C(),e.removeEventListener(Bt,Te),t.style.overflowY="auto")}function Le(t,e){t.closePopup({isConfirmed:!0,value:e})}function De(t,e,n){var o=R(),i=0;if(i:first-child,.swal2-container.swal2-bottom-left>:first-child,.swal2-container.swal2-bottom-right>:first-child,.swal2-container.swal2-bottom-start>:first-child,.swal2-container.swal2-bottom>:first-child{margin-top:auto}.swal2-container.swal2-grow-fullscreen>.swal2-modal{display:flex!important;flex:1;align-self:stretch;justify-content:center}.swal2-container.swal2-grow-row>.swal2-modal{display:flex!important;flex:1;align-content:center;justify-content:center}.swal2-container.swal2-grow-column{flex:1;flex-direction:column}.swal2-container.swal2-grow-column.swal2-bottom,.swal2-container.swal2-grow-column.swal2-center,.swal2-container.swal2-grow-column.swal2-top{align-items:center}.swal2-container.swal2-grow-column.swal2-bottom-left,.swal2-container.swal2-grow-column.swal2-bottom-start,.swal2-container.swal2-grow-column.swal2-center-left,.swal2-container.swal2-grow-column.swal2-center-start,.swal2-container.swal2-grow-column.swal2-top-left,.swal2-container.swal2-grow-column.swal2-top-start{align-items:flex-start}.swal2-container.swal2-grow-column.swal2-bottom-end,.swal2-container.swal2-grow-column.swal2-bottom-right,.swal2-container.swal2-grow-column.swal2-center-end,.swal2-container.swal2-grow-column.swal2-center-right,.swal2-container.swal2-grow-column.swal2-top-end,.swal2-container.swal2-grow-column.swal2-top-right{align-items:flex-end}.swal2-container.swal2-grow-column>.swal2-modal{display:flex!important;flex:1;align-content:center;justify-content:center}.swal2-container.swal2-no-transition{transition:none!important}.swal2-container:not(.swal2-top):not(.swal2-top-start):not(.swal2-top-end):not(.swal2-top-left):not(.swal2-top-right):not(.swal2-center-start):not(.swal2-center-end):not(.swal2-center-left):not(.swal2-center-right):not(.swal2-bottom):not(.swal2-bottom-start):not(.swal2-bottom-end):not(.swal2-bottom-left):not(.swal2-bottom-right):not(.swal2-grow-fullscreen)>.swal2-modal{margin:auto}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){.swal2-container .swal2-modal{margin:0!important}}.swal2-popup{display:none;position:relative;box-sizing:border-box;flex-direction:column;justify-content:center;width:32em;max-width:100%;padding:1.25em;border:none;border-radius:.3125em;background:#fff;font-family:inherit;font-size:1rem}.swal2-popup:focus{outline:0}.swal2-popup.swal2-loading{overflow-y:hidden}.swal2-header{display:flex;flex-direction:column;align-items:center;padding:0 1.8em}.swal2-title{position:relative;max-width:100%;margin:0 0 .4em;padding:0;color:#595959;font-size:1.875em;font-weight:600;text-align:center;text-transform:none;word-wrap:break-word}.swal2-actions{display:flex;z-index:1;box-sizing:border-box;flex-wrap:wrap;align-items:center;justify-content:center;width:100%;margin:1.25em auto 0;padding:0 1.6em}.swal2-actions:not(.swal2-loading) .swal2-styled[disabled]{opacity:.4}.swal2-actions:not(.swal2-loading) .swal2-styled:hover{background-image:linear-gradient(rgba(0,0,0,.1),rgba(0,0,0,.1))}.swal2-actions:not(.swal2-loading) .swal2-styled:active{background-image:linear-gradient(rgba(0,0,0,.2),rgba(0,0,0,.2))}.swal2-loader{display:none;align-items:center;justify-content:center;width:2.2em;height:2.2em;margin:0 1.875em;-webkit-animation:swal2-rotate-loading 1.5s linear 0s infinite normal;animation:swal2-rotate-loading 1.5s linear 0s infinite normal;border-width:.25em;border-style:solid;border-radius:100%;border-color:#2778c4 transparent #2778c4 transparent}.swal2-styled{margin:.3125em;padding:.625em 2em;box-shadow:none;font-weight:500}.swal2-styled:not([disabled]){cursor:pointer}.swal2-styled.swal2-confirm{border:0;border-radius:.25em;background:initial;background-color:#2778c4;color:#fff;font-size:1.0625em}.swal2-styled.swal2-deny{border:0;border-radius:.25em;background:initial;background-color:#d14529;color:#fff;font-size:1.0625em}.swal2-styled.swal2-cancel{border:0;border-radius:.25em;background:initial;background-color:#757575;color:#fff;font-size:1.0625em}.swal2-styled:focus{outline:0;box-shadow:0 0 0 1px #fff,0 0 0 3px rgba(50,100,150,.4)}.swal2-styled::-moz-focus-inner{border:0}.swal2-footer{justify-content:center;margin:1.25em 0 0;padding:1em 0 0;border-top:1px solid #eee;color:#545454;font-size:1em}.swal2-timer-progress-bar-container{position:absolute;right:0;bottom:0;left:0;height:.25em;overflow:hidden;border-bottom-right-radius:.3125em;border-bottom-left-radius:.3125em}.swal2-timer-progress-bar{width:100%;height:.25em;background:rgba(0,0,0,.2)}.swal2-image{max-width:100%;margin:1.25em auto}.swal2-close{position:absolute;z-index:2;top:0;right:0;align-items:center;justify-content:center;width:1.2em;height:1.2em;padding:0;overflow:hidden;transition:color .1s ease-out;border:none;border-radius:0;background:0 0;color:#ccc;font-family:serif;font-size:2.5em;line-height:1.2;cursor:pointer}.swal2-close:hover{transform:none;background:0 0;color:#f27474}.swal2-close::-moz-focus-inner{border:0}.swal2-content{z-index:1;justify-content:center;margin:0;padding:0 1.6em;color:#545454;font-size:1.125em;font-weight:400;line-height:normal;text-align:center;word-wrap:break-word}.swal2-checkbox,.swal2-file,.swal2-input,.swal2-radio,.swal2-select,.swal2-textarea{margin:1em auto}.swal2-file,.swal2-input,.swal2-textarea{box-sizing:border-box;width:100%;transition:border-color .3s,box-shadow .3s;border:1px solid #d9d9d9;border-radius:.1875em;background:inherit;box-shadow:inset 0 1px 1px rgba(0,0,0,.06);color:inherit;font-size:1.125em}.swal2-file.swal2-inputerror,.swal2-input.swal2-inputerror,.swal2-textarea.swal2-inputerror{border-color:#f27474!important;box-shadow:0 0 2px #f27474!important}.swal2-file:focus,.swal2-input:focus,.swal2-textarea:focus{border:1px solid #b4dbed;outline:0;box-shadow:0 0 3px #c4e6f5}.swal2-file::-moz-placeholder,.swal2-input::-moz-placeholder,.swal2-textarea::-moz-placeholder{color:#ccc}.swal2-file:-ms-input-placeholder,.swal2-input:-ms-input-placeholder,.swal2-textarea:-ms-input-placeholder{color:#ccc}.swal2-file::placeholder,.swal2-input::placeholder,.swal2-textarea::placeholder{color:#ccc}.swal2-range{margin:1em auto;background:#fff}.swal2-range input{width:80%}.swal2-range output{width:20%;color:inherit;font-weight:600;text-align:center}.swal2-range input,.swal2-range output{height:2.625em;padding:0;font-size:1.125em;line-height:2.625em}.swal2-input{height:2.625em;padding:0 .75em}.swal2-input[type=number]{max-width:10em}.swal2-file{background:inherit;font-size:1.125em}.swal2-textarea{height:6.75em;padding:.75em}.swal2-select{min-width:50%;max-width:100%;padding:.375em .625em;background:inherit;color:inherit;font-size:1.125em}.swal2-checkbox,.swal2-radio{align-items:center;justify-content:center;background:#fff;color:inherit}.swal2-checkbox label,.swal2-radio label{margin:0 .6em;font-size:1.125em}.swal2-checkbox input,.swal2-radio input{margin:0 .4em}.swal2-input-label{display:flex;justify-content:center;margin:1em auto}.swal2-validation-message{display:none;align-items:center;justify-content:center;margin:0 -2.7em;padding:.625em;overflow:hidden;background:#f0f0f0;color:#666;font-size:1em;font-weight:300}.swal2-validation-message::before{content:\"!\";display:inline-block;width:1.5em;min-width:1.5em;height:1.5em;margin:0 .625em;border-radius:50%;background-color:#f27474;color:#fff;font-weight:600;line-height:1.5em;text-align:center}.swal2-icon{position:relative;box-sizing:content-box;justify-content:center;width:5em;height:5em;margin:1.25em auto 1.875em;border:.25em solid transparent;border-radius:50%;font-family:inherit;line-height:5em;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.swal2-icon .swal2-icon-content{display:flex;align-items:center;font-size:3.75em}.swal2-icon.swal2-error{border-color:#f27474;color:#f27474}.swal2-icon.swal2-error .swal2-x-mark{position:relative;flex-grow:1}.swal2-icon.swal2-error [class^=swal2-x-mark-line]{display:block;position:absolute;top:2.3125em;width:2.9375em;height:.3125em;border-radius:.125em;background-color:#f27474}.swal2-icon.swal2-error [class^=swal2-x-mark-line][class$=left]{left:1.0625em;transform:rotate(45deg)}.swal2-icon.swal2-error [class^=swal2-x-mark-line][class$=right]{right:1em;transform:rotate(-45deg)}.swal2-icon.swal2-error.swal2-icon-show{-webkit-animation:swal2-animate-error-icon .5s;animation:swal2-animate-error-icon .5s}.swal2-icon.swal2-error.swal2-icon-show .swal2-x-mark{-webkit-animation:swal2-animate-error-x-mark .5s;animation:swal2-animate-error-x-mark .5s}.swal2-icon.swal2-warning{border-color:#facea8;color:#f8bb86}.swal2-icon.swal2-info{border-color:#9de0f6;color:#3fc3ee}.swal2-icon.swal2-question{border-color:#c9dae1;color:#87adbd}.swal2-icon.swal2-success{border-color:#a5dc86;color:#a5dc86}.swal2-icon.swal2-success [class^=swal2-success-circular-line]{position:absolute;width:3.75em;height:7.5em;transform:rotate(45deg);border-radius:50%}.swal2-icon.swal2-success [class^=swal2-success-circular-line][class$=left]{top:-.4375em;left:-2.0635em;transform:rotate(-45deg);transform-origin:3.75em 3.75em;border-radius:7.5em 0 0 7.5em}.swal2-icon.swal2-success [class^=swal2-success-circular-line][class$=right]{top:-.6875em;left:1.875em;transform:rotate(-45deg);transform-origin:0 3.75em;border-radius:0 7.5em 7.5em 0}.swal2-icon.swal2-success .swal2-success-ring{position:absolute;z-index:2;top:-.25em;left:-.25em;box-sizing:content-box;width:100%;height:100%;border:.25em solid rgba(165,220,134,.3);border-radius:50%}.swal2-icon.swal2-success .swal2-success-fix{position:absolute;z-index:1;top:.5em;left:1.625em;width:.4375em;height:5.625em;transform:rotate(-45deg)}.swal2-icon.swal2-success [class^=swal2-success-line]{display:block;position:absolute;z-index:2;height:.3125em;border-radius:.125em;background-color:#a5dc86}.swal2-icon.swal2-success [class^=swal2-success-line][class$=tip]{top:2.875em;left:.8125em;width:1.5625em;transform:rotate(45deg)}.swal2-icon.swal2-success [class^=swal2-success-line][class$=long]{top:2.375em;right:.5em;width:2.9375em;transform:rotate(-45deg)}.swal2-icon.swal2-success.swal2-icon-show .swal2-success-line-tip{-webkit-animation:swal2-animate-success-line-tip .75s;animation:swal2-animate-success-line-tip .75s}.swal2-icon.swal2-success.swal2-icon-show .swal2-success-line-long{-webkit-animation:swal2-animate-success-line-long .75s;animation:swal2-animate-success-line-long .75s}.swal2-icon.swal2-success.swal2-icon-show .swal2-success-circular-line-right{-webkit-animation:swal2-rotate-success-circular-line 4.25s ease-in;animation:swal2-rotate-success-circular-line 4.25s ease-in}.swal2-progress-steps{flex-wrap:wrap;align-items:center;max-width:100%;margin:0 0 1.25em;padding:0;background:inherit;font-weight:600}.swal2-progress-steps li{display:inline-block;position:relative}.swal2-progress-steps .swal2-progress-step{z-index:20;flex-shrink:0;width:2em;height:2em;border-radius:2em;background:#2778c4;color:#fff;line-height:2em;text-align:center}.swal2-progress-steps .swal2-progress-step.swal2-active-progress-step{background:#2778c4}.swal2-progress-steps .swal2-progress-step.swal2-active-progress-step~.swal2-progress-step{background:#add8e6;color:#fff}.swal2-progress-steps .swal2-progress-step.swal2-active-progress-step~.swal2-progress-step-line{background:#add8e6}.swal2-progress-steps .swal2-progress-step-line{z-index:10;flex-shrink:0;width:2.5em;height:.4em;margin:0 -1px;background:#2778c4}[class^=swal2]{-webkit-tap-highlight-color:transparent}.swal2-show{-webkit-animation:swal2-show .3s;animation:swal2-show .3s}.swal2-hide{-webkit-animation:swal2-hide .15s forwards;animation:swal2-hide .15s forwards}.swal2-noanimation{transition:none}.swal2-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}.swal2-rtl .swal2-close{right:auto;left:0}.swal2-rtl .swal2-timer-progress-bar{right:0;left:auto}@supports (-ms-accelerator:true){.swal2-range input{width:100%!important}.swal2-range output{display:none}}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){.swal2-range input{width:100%!important}.swal2-range output{display:none}}@-moz-document url-prefix(){.swal2-close:focus{outline:2px solid rgba(50,100,150,.4)}}@-webkit-keyframes swal2-toast-show{0%{transform:translateY(-.625em) rotateZ(2deg)}33%{transform:translateY(0) rotateZ(-2deg)}66%{transform:translateY(.3125em) rotateZ(2deg)}100%{transform:translateY(0) rotateZ(0)}}@keyframes swal2-toast-show{0%{transform:translateY(-.625em) rotateZ(2deg)}33%{transform:translateY(0) rotateZ(-2deg)}66%{transform:translateY(.3125em) rotateZ(2deg)}100%{transform:translateY(0) rotateZ(0)}}@-webkit-keyframes swal2-toast-hide{100%{transform:rotateZ(1deg);opacity:0}}@keyframes swal2-toast-hide{100%{transform:rotateZ(1deg);opacity:0}}@-webkit-keyframes swal2-toast-animate-success-line-tip{0%{top:.5625em;left:.0625em;width:0}54%{top:.125em;left:.125em;width:0}70%{top:.625em;left:-.25em;width:1.625em}84%{top:1.0625em;left:.75em;width:.5em}100%{top:1.125em;left:.1875em;width:.75em}}@keyframes swal2-toast-animate-success-line-tip{0%{top:.5625em;left:.0625em;width:0}54%{top:.125em;left:.125em;width:0}70%{top:.625em;left:-.25em;width:1.625em}84%{top:1.0625em;left:.75em;width:.5em}100%{top:1.125em;left:.1875em;width:.75em}}@-webkit-keyframes swal2-toast-animate-success-line-long{0%{top:1.625em;right:1.375em;width:0}65%{top:1.25em;right:.9375em;width:0}84%{top:.9375em;right:0;width:1.125em}100%{top:.9375em;right:.1875em;width:1.375em}}@keyframes swal2-toast-animate-success-line-long{0%{top:1.625em;right:1.375em;width:0}65%{top:1.25em;right:.9375em;width:0}84%{top:.9375em;right:0;width:1.125em}100%{top:.9375em;right:.1875em;width:1.375em}}@-webkit-keyframes swal2-show{0%{transform:scale(.7)}45%{transform:scale(1.05)}80%{transform:scale(.95)}100%{transform:scale(1)}}@keyframes swal2-show{0%{transform:scale(.7)}45%{transform:scale(1.05)}80%{transform:scale(.95)}100%{transform:scale(1)}}@-webkit-keyframes swal2-hide{0%{transform:scale(1);opacity:1}100%{transform:scale(.5);opacity:0}}@keyframes swal2-hide{0%{transform:scale(1);opacity:1}100%{transform:scale(.5);opacity:0}}@-webkit-keyframes swal2-animate-success-line-tip{0%{top:1.1875em;left:.0625em;width:0}54%{top:1.0625em;left:.125em;width:0}70%{top:2.1875em;left:-.375em;width:3.125em}84%{top:3em;left:1.3125em;width:1.0625em}100%{top:2.8125em;left:.8125em;width:1.5625em}}@keyframes swal2-animate-success-line-tip{0%{top:1.1875em;left:.0625em;width:0}54%{top:1.0625em;left:.125em;width:0}70%{top:2.1875em;left:-.375em;width:3.125em}84%{top:3em;left:1.3125em;width:1.0625em}100%{top:2.8125em;left:.8125em;width:1.5625em}}@-webkit-keyframes swal2-animate-success-line-long{0%{top:3.375em;right:2.875em;width:0}65%{top:3.375em;right:2.875em;width:0}84%{top:2.1875em;right:0;width:3.4375em}100%{top:2.375em;right:.5em;width:2.9375em}}@keyframes swal2-animate-success-line-long{0%{top:3.375em;right:2.875em;width:0}65%{top:3.375em;right:2.875em;width:0}84%{top:2.1875em;right:0;width:3.4375em}100%{top:2.375em;right:.5em;width:2.9375em}}@-webkit-keyframes swal2-rotate-success-circular-line{0%{transform:rotate(-45deg)}5%{transform:rotate(-45deg)}12%{transform:rotate(-405deg)}100%{transform:rotate(-405deg)}}@keyframes swal2-rotate-success-circular-line{0%{transform:rotate(-45deg)}5%{transform:rotate(-45deg)}12%{transform:rotate(-405deg)}100%{transform:rotate(-405deg)}}@-webkit-keyframes swal2-animate-error-x-mark{0%{margin-top:1.625em;transform:scale(.4);opacity:0}50%{margin-top:1.625em;transform:scale(.4);opacity:0}80%{margin-top:-.375em;transform:scale(1.15)}100%{margin-top:0;transform:scale(1);opacity:1}}@keyframes swal2-animate-error-x-mark{0%{margin-top:1.625em;transform:scale(.4);opacity:0}50%{margin-top:1.625em;transform:scale(.4);opacity:0}80%{margin-top:-.375em;transform:scale(1.15)}100%{margin-top:0;transform:scale(1);opacity:1}}@-webkit-keyframes swal2-animate-error-icon{0%{transform:rotateX(100deg);opacity:0}100%{transform:rotateX(0);opacity:1}}@keyframes swal2-animate-error-icon{0%{transform:rotateX(100deg);opacity:0}100%{transform:rotateX(0);opacity:1}}@-webkit-keyframes swal2-rotate-loading{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}@keyframes swal2-rotate-loading{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}body.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown){overflow:hidden}body.swal2-height-auto{height:auto!important}body.swal2-no-backdrop .swal2-container{top:auto;right:auto;bottom:auto;left:auto;max-width:calc(100% - .625em * 2);background-color:transparent!important}body.swal2-no-backdrop .swal2-container>.swal2-modal{box-shadow:0 0 10px rgba(0,0,0,.4)}body.swal2-no-backdrop .swal2-container.swal2-top{top:0;left:50%;transform:translateX(-50%)}body.swal2-no-backdrop .swal2-container.swal2-top-left,body.swal2-no-backdrop .swal2-container.swal2-top-start{top:0;left:0}body.swal2-no-backdrop .swal2-container.swal2-top-end,body.swal2-no-backdrop .swal2-container.swal2-top-right{top:0;right:0}body.swal2-no-backdrop .swal2-container.swal2-center{top:50%;left:50%;transform:translate(-50%,-50%)}body.swal2-no-backdrop .swal2-container.swal2-center-left,body.swal2-no-backdrop .swal2-container.swal2-center-start{top:50%;left:0;transform:translateY(-50%)}body.swal2-no-backdrop .swal2-container.swal2-center-end,body.swal2-no-backdrop .swal2-container.swal2-center-right{top:50%;right:0;transform:translateY(-50%)}body.swal2-no-backdrop .swal2-container.swal2-bottom{bottom:0;left:50%;transform:translateX(-50%)}body.swal2-no-backdrop .swal2-container.swal2-bottom-left,body.swal2-no-backdrop .swal2-container.swal2-bottom-start{bottom:0;left:0}body.swal2-no-backdrop .swal2-container.swal2-bottom-end,body.swal2-no-backdrop .swal2-container.swal2-bottom-right{right:0;bottom:0}@media print{body.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown){overflow-y:scroll!important}body.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown)>[aria-hidden=true]{display:none}body.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown) .swal2-container{position:static!important}}body.swal2-toast-shown .swal2-container{background-color:transparent}body.swal2-toast-shown .swal2-container.swal2-top{top:0;right:auto;bottom:auto;left:50%;transform:translateX(-50%)}body.swal2-toast-shown .swal2-container.swal2-top-end,body.swal2-toast-shown .swal2-container.swal2-top-right{top:0;right:0;bottom:auto;left:auto}body.swal2-toast-shown .swal2-container.swal2-top-left,body.swal2-toast-shown .swal2-container.swal2-top-start{top:0;right:auto;bottom:auto;left:0}body.swal2-toast-shown .swal2-container.swal2-center-left,body.swal2-toast-shown .swal2-container.swal2-center-start{top:50%;right:auto;bottom:auto;left:0;transform:translateY(-50%)}body.swal2-toast-shown .swal2-container.swal2-center{top:50%;right:auto;bottom:auto;left:50%;transform:translate(-50%,-50%)}body.swal2-toast-shown .swal2-container.swal2-center-end,body.swal2-toast-shown .swal2-container.swal2-center-right{top:50%;right:0;bottom:auto;left:auto;transform:translateY(-50%)}body.swal2-toast-shown .swal2-container.swal2-bottom-left,body.swal2-toast-shown .swal2-container.swal2-bottom-start{top:auto;right:auto;bottom:0;left:0}body.swal2-toast-shown .swal2-container.swal2-bottom{top:auto;right:auto;bottom:0;left:50%;transform:translateX(-50%)}body.swal2-toast-shown .swal2-container.swal2-bottom-end,body.swal2-toast-shown .swal2-container.swal2-bottom-right{top:auto;right:0;bottom:0;left:auto}body.swal2-toast-column .swal2-toast{flex-direction:column;align-items:stretch}body.swal2-toast-column .swal2-toast .swal2-actions{flex:1;align-self:stretch;height:2.2em;margin-top:.3125em}body.swal2-toast-column .swal2-toast .swal2-loading{justify-content:center}body.swal2-toast-column .swal2-toast .swal2-input{height:2em;margin:.3125em auto;font-size:1em}body.swal2-toast-column .swal2-toast .swal2-validation-message{font-size:1em}"); --------------------------------------------------------------------------------