commit 8e329418f073d2da3f44249c024995d2464c0777 Author: 袁涛 Date: Mon Jan 12 01:07:01 2026 +0800 feat: 初始化 NFC 读卡器项目 - 添加 Electron 主进程和渲染进程 - 实现读卡器连接、断开和读取卡片 ID 功能 - 添加自定义窗口标题栏和窗口控制 - 实现简洁美观的用户界面 - 添加项目文档 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a1a3098 --- /dev/null +++ b/.gitignore @@ -0,0 +1,193 @@ +# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig +# Created by https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,node +# Edit at https://www.toptal.com/developers/gitignore?templates=windows,visualstudiocode,node + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,node + +# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) + diff --git a/README.md b/README.md new file mode 100644 index 0000000..c346e3d --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# NFC 读卡器 + +一个基于 Electron 的 NFC 卡片读取应用程序,支持连接读卡器并读取卡片 ID。 + +## 功能特性 + +- 连接/断开 NFC 读卡器 +- 读取 NFC 卡片 ID +- 实时状态显示 +- 简洁美观的用户界面 + +## 安装 + +```bash +npm install +``` + +## 运行 + +```bash +npm start +``` + +## 使用说明 + +1. 启动应用后,点击"连接读卡器"按钮连接读卡器 +2. 连接成功后,将 NFC 卡片放置在读卡器上 +3. 点击"读取卡片"按钮读取卡片 ID +4. 读取结果将显示在结果区域中 +5. 使用完毕后,点击"断开连接"按钮断开读卡器 + +## 项目结构 + +``` +NFCReader/ +├── index.html # 主页面 +├── main.js # Electron 主进程 +├── renderer.js # 渲染进程逻辑 +├── style.css # 样式文件 +├── lib/ +│ └── cardReader.es.js # 读卡器核心库 +└── package.json # 项目配置 +``` + +## 技术栈 + +- Electron +- JavaScript (ES6+) +- HTML5 +- CSS3 + +## 许可证 + +MIT \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..e636d47 --- /dev/null +++ b/index.html @@ -0,0 +1,60 @@ + + + + + + NFC 读卡器 + + + +
+
+
NFC 读卡器
+
+ + + +
+
+
+
+
+
+
+ + 未连接 +
+
+ +
+ + + +
+ +
+

读取结果

+
+

请先连接读卡器,然后点击"读取卡片"按钮

+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/lib/cardReader.es.js b/lib/cardReader.es.js new file mode 100644 index 0000000..7e839c4 --- /dev/null +++ b/lib/cardReader.es.js @@ -0,0 +1,120 @@ +var v = { + OBJ: function() { + var r = {}, e = null, t = null; + r.onResult = function(s) { + t.addEvent("Result", s); + }; + var o = function() { + }, l = function(s) { + var i = ""; + i = s.data; + var w = { + type: "Result", + strStatus: i.substr(4, 2), + strData: i.substr(6), + strCmdCode: i.substr(0, 4) + }; + t != null && t.fireEvent(w); + }, d = function() { + }, h = function() { + alert("Server not running !"); + }; + return r.createSocket = function() { + try { + if ("WebSocket" in window) + e = new WebSocket("ws://localhost:39002/RFID Reader Service"); + else if ("MozWebSocket" in window) + e = new MozWebSocket("ws://localhost:39002/RFID Reader Service"); + else + return alert("None"), !1; + return e.onopen = o, e.onmessage = l, e.onclose = d, e.onerror = h, t = new u(), !0; + } catch { + return !1; + } + }, r.Disconnect = function() { + e != null && e.close(); + }, r.send = function(s) { + e.send(s); + }, r; + } +}; +function u() { + this.handlers = {}; +} +u.prototype = { + constructor: u, + addEvent: function(r, e) { + typeof this.handlers[r] > "u" && (this.handlers[r] = []), this.handlers[r].push(e); + }, + fireEvent: function(r) { + if (r.target || (r.target = this), this.handlers[r.type] instanceof Array) + for (var e = this.handlers[r.type], t = 0; t < e.length; t++) + e[t](r); + }, + removeEvent: function(r, e) { + if (this.handlers[r] instanceof Array) { + for (var t = this.handlers[r], o = 0; o < t.length && t[o] != e; o++) + ; + t.splice(o, 1); + } + } +}; +try { + var f = v.OBJ(); +} catch { +} +f.createSocket(); +let n = "00", c = !1; +function a(r) { + return new Promise((e, t) => { + f.onResult(function(o) { + o.strStatus === "00" ? e(o) : t(o.strStatus); + }), f.send(r); + }); +} +async function y() { + await a(n + "010702"); +} +async function E() { + await a(n + "010701"); +} +async function S() { + if (c) + try { + await a(n + "0009"); + } catch (r) { + throw new Error(`关闭读卡器失败: EerorCode 0x${r}`); + } + try { + await a(n + "000700"), await a(n + "010941"), await a(n + "010801"), y(), await a(n + "010610"), c = !0; + } catch (r) { + throw new Error(`连接读卡器失败: EerorCode 0x${r}`); + } +} +async function R() { + if (!c) + throw new Error("读卡器未连接,请先连接读卡器!"); + try { + await a(n + "100152"); + } catch { + return null; + } + try { + return (await a(n + "1002")).strData; + } catch (r) { + throw new Error(`读取ID出错: EerorCode 0x${r}`); + } +} +async function g() { + if (c) + try { + await a(n + "0009"), E(), await a(n + "010610"), c = !1; + } catch (r) { + throw new Error(`关闭读卡器失败: EerorCode 0x${r}`); + } +} +export { + g as closeReader, + S as connectReader, + R as readId +}; diff --git a/main.js b/main.js new file mode 100644 index 0000000..c1f94c1 --- /dev/null +++ b/main.js @@ -0,0 +1,59 @@ +const { app, BrowserWindow, ipcMain } = require('electron') + +function createWindow() { + const win = new BrowserWindow({ + width: 440, + height: 400, + frame: false, + transparent: false, + backgroundColor: '#f9f9f9', + roundedCorners: true, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + }, + }) + + win.loadFile('index.html') +} + +app.whenReady().then(() => { + createWindow() + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } + }) +}) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) + +ipcMain.on('window-minimize', () => { + const focusedWindow = BrowserWindow.getFocusedWindow() + if (focusedWindow) { + focusedWindow.minimize() + } +}) + +ipcMain.on('window-maximize', () => { + const focusedWindow = BrowserWindow.getFocusedWindow() + if (focusedWindow) { + if (focusedWindow.isMaximized()) { + focusedWindow.unmaximize() + } else { + focusedWindow.maximize() + } + } +}) + +ipcMain.on('window-close', () => { + const focusedWindow = BrowserWindow.getFocusedWindow() + if (focusedWindow) { + focusedWindow.close() + } +}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..215e263 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "nfc-reader", + "version": "1.0.0", + "description": "NFC Card Reader Application", + "main": "main.js", + "scripts": { + "start": "electron ." + }, + "keywords": ["nfc", "reader", "electron"], + "author": "", + "license": "MIT", + "devDependencies": { + "electron": "^28.0.0" + } +} \ No newline at end of file diff --git a/renderer.js b/renderer.js new file mode 100644 index 0000000..937aa89 --- /dev/null +++ b/renderer.js @@ -0,0 +1,106 @@ +import { connectReader, readId, closeReader } from './lib/cardReader.es.js'; +const { ipcRenderer } = require('electron'); + +const connectBtn = document.getElementById('connectBtn'); +const disconnectBtn = document.getElementById('disconnectBtn'); +const readBtn = document.getElementById('readBtn'); +const statusDot = document.getElementById('statusDot'); +const statusText = document.getElementById('statusText'); +const resultBox = document.getElementById('resultBox'); + +const minimizeBtn = document.getElementById('minimizeBtn'); +const maximizeBtn = document.getElementById('maximizeBtn'); +const closeBtn = document.getElementById('closeBtn'); + +let isConnected = false; + +function updateStatus(status) { + statusDot.className = 'status-dot ' + status; + + switch(status) { + case 'connected': + statusText.textContent = '已连接'; + isConnected = true; + connectBtn.disabled = true; + disconnectBtn.disabled = false; + readBtn.disabled = false; + break; + case 'disconnected': + statusText.textContent = '未连接'; + isConnected = false; + connectBtn.disabled = false; + disconnectBtn.disabled = true; + readBtn.disabled = true; + break; + case 'connecting': + statusText.textContent = '连接中...'; + connectBtn.disabled = true; + disconnectBtn.disabled = true; + readBtn.disabled = true; + break; + } +} + +function showResult(content, isError = false) { + if (isError) { + resultBox.innerHTML = '

' + content + '

'; + } else { + resultBox.innerHTML = '

卡片 ID: ' + content + '

'; + } +} + +connectBtn.addEventListener('click', async () => { + updateStatus('connecting'); + resultBox.innerHTML = '

正在连接读卡器...

'; + + try { + await connectReader(); + updateStatus('connected'); + showResult('读卡器连接成功!请将卡片放在读卡器上,然后点击"读取卡片"按钮'); + } catch (error) { + updateStatus('disconnected'); + showResult('连接失败: ' + error.message, true); + } +}); + +disconnectBtn.addEventListener('click', async () => { + try { + await closeReader(); + updateStatus('disconnected'); + showResult('读卡器已断开连接'); + } catch (error) { + showResult('断开连接失败: ' + error.message, true); + } +}); + +readBtn.addEventListener('click', async () => { + if (!isConnected) { + showResult('请先连接读卡器', true); + return; + } + + resultBox.innerHTML = '

正在读取卡片...

'; + + try { + const cardId = await readId(); + if (cardId) { + showResult(cardId); + } else { + showResult('未检测到卡片,请确保卡片已放置在读卡器上', true); + } + } catch (error) { + showResult('读取失败: ' + error.message, true); + } +}); + +minimizeBtn.addEventListener('click', () => { + ipcRenderer.send('window-minimize'); +}); + +maximizeBtn.addEventListener('click', () => { + ipcRenderer.send('window-maximize'); +}); + +closeBtn.addEventListener('click', () => { + ipcRenderer.send('window-close'); +}); \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..3c5da38 --- /dev/null +++ b/style.css @@ -0,0 +1,294 @@ +@import url('https://fonts.googleapis.com/css2?family=Segoe+UI+Variable:wght@300;400;500;600;700&display=swap'); + +:root { + --window-bg: #f9f9f9; + --border-color: rgba(0, 0, 0, 0.08); + --border-hover: rgba(0, 0, 0, 0.12); + --text-primary: #1a1a1a; + --text-secondary: #5c5c5c; + --text-disabled: rgba(0, 0, 0, 0.4); + --accent-color: #0067C0; + --accent-hover: #005A9E; + --success-color: #107C10; + --danger-color: #E81123; + --surface-bg: rgba(255, 255, 255, 0.6); + --surface-hover: rgba(255, 255, 255, 0.9); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI Variable', 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif; + background: var(--window-bg); + min-height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; + -webkit-font-smoothing: antialiased; +} + +.titlebar { + height: 32px; + background: var(--window-bg); + display: flex; + align-items: center; + position: relative; + -webkit-app-region: drag; + border-bottom: 1px solid var(--border-color); +} + +.titlebar-drag-region { + position: absolute; + top: 0; + left: 0; + right: 138px; + height: 32px; + -webkit-app-region: drag; +} + +.titlebar-title { + flex: 1; + padding-left: 16px; + font-size: 12px; + font-weight: 400; + color: var(--text-primary); + user-select: none; + pointer-events: none; + opacity: 0.9; +} + +.window-controls { + display: flex; + height: 32px; + position: absolute; + right: 0; + top: 0; +} + +.window-control { + width: 46px; + height: 32px; + border: none; + background: transparent; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-primary); + transition: background-color 0.1s ease; + -webkit-app-region: no-drag; + opacity: 0.9; +} + +.window-control:hover { + background-color: rgba(0, 0, 0, 0.04); + opacity: 1; +} + +.window-control:active { + background-color: rgba(0, 0, 0, 0.08); +} + +.window-control.close:hover { + background-color: var(--danger-color); + color: white; +} + +.window-control svg { + pointer-events: none; +} + +.container { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + overflow-y: auto; +} + +.card { + background: rgba(255, 255, 255, 0.6); + border: 1px solid var(--border-color); + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.04), + 0 0 0 1px rgba(255, 255, 255, 0.8) inset; + border-radius: 8px; + overflow: hidden; + width: 100%; + max-width: 400px; +} + +.card-body { + padding: 20px; +} + +.status-section { + text-align: center; + padding: 12px 16px; + background: rgba(255, 255, 255, 0.5); + border-radius: 6px; + border: 1px solid var(--border-color); + margin-bottom: 16px; +} + +.status-indicator { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; +} + +.status-dot.connected { + background-color: var(--success-color); + box-shadow: 0 0 0 2px rgba(16, 124, 16, 0.2); +} + +.status-dot.disconnected { + background-color: var(--danger-color); + box-shadow: 0 0 0 2px rgba(232, 17, 35, 0.2); +} + +.status-dot.connecting { + background-color: #FF8C00; + box-shadow: 0 0 0 2px rgba(255, 140, 0, 0.2); + animation: blink 1s infinite; +} + +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +#statusText { + font-size: 13px; + font-weight: 500; + color: var(--text-primary); +} + +.control-buttons { + display: flex; + gap: 8px; + justify-content: center; + margin-bottom: 16px; +} + +.control-buttons button { + flex: 1; + font-weight: 400; + font-size: 13px; + padding: 8px 14px; + border: 1px solid var(--border-color); + border-radius: 4px; + transition: all 0.1s ease; + cursor: pointer; + font-family: inherit; + letter-spacing: 0.1px; + background: var(--surface-bg); + color: var(--text-primary); +} + +.control-buttons button:hover:not(:disabled) { + background: var(--surface-hover); + border-color: var(--border-hover); +} + +.control-buttons button:active:not(:disabled) { + background: rgba(0, 0, 0, 0.04); + transform: scale(0.98); +} + +.control-buttons button:disabled { + opacity: 0.4; + cursor: default; +} + +.btn-primary { + background: var(--accent-color); + color: white; + border-color: var(--accent-color); +} + +.btn-primary:hover:not(:disabled) { + background: var(--accent-hover); + border-color: var(--accent-hover); +} + +.btn-secondary { + background: var(--surface-bg); + color: var(--text-primary); + border-color: var(--border-color); +} + +.btn-secondary:hover:not(:disabled) { + background: var(--surface-hover); + border-color: var(--border-hover); +} + +.btn-success { + background: var(--success-color); + color: white; + border-color: var(--success-color); +} + +.btn-success:hover:not(:disabled) { + background: #0B5A0B; + border-color: #0B5A0B; +} + +.result-section { + margin-top: 16px; +} + +.result-section h4 { + margin-bottom: 8px; + color: var(--text-primary); + font-weight: 600; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.result-box { + padding: 12px 16px; + background: rgba(255, 255, 255, 0.5); + border-radius: 6px; + min-height: 60px; + border: 1px solid var(--border-color); +} + +.placeholder-text { + color: var(--text-secondary); + margin: 0; + text-align: center; + font-size: 13px; + line-height: 1.5; +} + +.result-content { + word-break: break-all; + font-family: 'Segoe UI Variable', 'Segoe UI', 'Consolas', monospace; + font-size: 14px; + color: var(--text-primary); + font-weight: 400; + text-align: center; + padding: 4px; +} + +.error-message { + color: var(--danger-color); + font-weight: 500; + text-align: center; + font-size: 13px; +} \ No newline at end of file