Cambios esteticos y de ping para el servidor
This commit is contained in:
@@ -4,7 +4,6 @@ import { electronApp, optimizer, is } from "@electron-toolkit/utils";
|
|||||||
import icon from "../../resources/icon.png?asset";
|
import icon from "../../resources/icon.png?asset";
|
||||||
import { NfcService } from "../services/NfcService";
|
import { NfcService } from "../services/NfcService";
|
||||||
|
|
||||||
let nfcService: NfcService | null = null;
|
|
||||||
|
|
||||||
function createWindow(): void {
|
function createWindow(): void {
|
||||||
// Create the browser window.
|
// Create the browser window.
|
||||||
@@ -20,7 +19,11 @@ function createWindow(): void {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
nfcService = new NfcService();
|
new NfcService((event) => {
|
||||||
|
if (!mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send(`nfc:${event.type}`, event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
mainWindow.on("ready-to-show", () => {
|
mainWindow.on("ready-to-show", () => {
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
@@ -57,6 +60,19 @@ app.whenReady().then(() => {
|
|||||||
// IPC test
|
// IPC test
|
||||||
ipcMain.on("ping", () => console.log("pong"));
|
ipcMain.on("ping", () => console.log("pong"));
|
||||||
|
|
||||||
|
ipcMain.handle("ping:url", async (_event, url: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "HEAD",
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
});
|
||||||
|
return response.ok;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Ping failed for ${url}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
createWindow();
|
createWindow();
|
||||||
|
|
||||||
app.on("activate", function () {
|
app.on("activate", function () {
|
||||||
|
|||||||
12
src/preload/index.d.ts
vendored
12
src/preload/index.d.ts
vendored
@@ -1,8 +1,18 @@
|
|||||||
import { ElectronAPI } from "@electron-toolkit/preload";
|
import { ElectronAPI } from "@electron-toolkit/preload";
|
||||||
|
|
||||||
|
export interface NfcAPI {
|
||||||
|
onTag: (callback: (event: { uid: string }) => void) => void;
|
||||||
|
onRemoved: (callback: (event: { uid: string }) => void) => void;
|
||||||
|
onError: (callback: (event: { message: string }) => void) => void;
|
||||||
|
ping: (url: string) => Promise<boolean>;
|
||||||
|
removeAllListeners: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
electron: ElectronAPI;
|
electron: ElectronAPI;
|
||||||
api: unknown;
|
api: {
|
||||||
|
nfc: NfcAPI;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,28 @@
|
|||||||
import { contextBridge } from "electron";
|
import { contextBridge, ipcRenderer } from "electron";
|
||||||
import { electronAPI } from "@electron-toolkit/preload";
|
import { electronAPI } from "@electron-toolkit/preload";
|
||||||
|
|
||||||
// Custom APIs for renderer
|
// Custom APIs for renderer
|
||||||
const api = {};
|
const api = {
|
||||||
|
nfc: {
|
||||||
|
onTag: (callback: (event: { uid: string }) => void): void => {
|
||||||
|
ipcRenderer.on("nfc:tag", (_event, value) => callback(value));
|
||||||
|
},
|
||||||
|
onRemoved: (callback: (event: { uid: string }) => void): void => {
|
||||||
|
ipcRenderer.on("nfc:removed", (_event, value) => callback(value));
|
||||||
|
},
|
||||||
|
onError: (callback: (event: { message: string }) => void): void => {
|
||||||
|
ipcRenderer.on("nfc:error", (_event, value) => callback(value));
|
||||||
|
},
|
||||||
|
ping: (url: string): Promise<boolean> => {
|
||||||
|
return ipcRenderer.invoke("ping:url", url);
|
||||||
|
},
|
||||||
|
removeAllListeners: (): void => {
|
||||||
|
ipcRenderer.removeAllListeners("nfc:tag");
|
||||||
|
ipcRenderer.removeAllListeners("nfc:removed");
|
||||||
|
ipcRenderer.removeAllListeners("nfc:error");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Use `contextBridge` APIs to expose Electron APIs to
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
// renderer only if context isolation is enabled, otherwise
|
// renderer only if context isolation is enabled, otherwise
|
||||||
|
|||||||
@@ -13,15 +13,18 @@ const navigateTo = (view: "main" | "config"): void => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<div class="logo">NFC READER TERMINAL</div>
|
<div class="logo-section">
|
||||||
<nav v-if="currentView === 'main'">
|
<div class="logo-icon"></div>
|
||||||
<button class="nav-btn" @click="navigateTo('config')">
|
<div class="logo-text">NFC.INTERFACE.v1</div>
|
||||||
SET CONFIG
|
<div class="technical-info">READY // LOCAL_ACCESS</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-section">
|
||||||
|
<button v-if="currentView === 'main'" class="nav-btn" @click="navigateTo('config')">
|
||||||
|
CONFIG
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
<button v-else class="nav-btn" @click="navigateTo('main')">
|
||||||
<nav v-else>
|
EXIT_CONFIG
|
||||||
<button class="nav-btn" @click="navigateTo('main')">
|
|
||||||
BACK TO MAIN
|
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -34,12 +37,10 @@ const navigateTo = (view: "main" | "config"): void => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Global resets and simple styles */
|
/* Global resets and TE Base */
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
/* Disable transitions entirely */
|
cursor: default;
|
||||||
transition: none !important;
|
|
||||||
animation: none !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
@@ -49,20 +50,7 @@ body,
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
background-color: var(--te-bg);
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
background-color: #ffffff;
|
|
||||||
color: #111111;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body {
|
|
||||||
background-color: #000000;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -70,64 +58,82 @@ body {
|
|||||||
.app-container {
|
.app-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100vh;
|
height: 100vh;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
|
background-color: var(--te-bg);
|
||||||
|
color: var(--te-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header {
|
.app-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 1rem 2rem;
|
padding: 12px 24px;
|
||||||
background-color: #111111;
|
border-bottom: 1px solid var(--te-gray-light);
|
||||||
color: #ffffff;
|
|
||||||
border-bottom: 4px solid #111111;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo-section {
|
||||||
font-size: 1.25rem;
|
display: flex;
|
||||||
font-weight: 800;
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background-color: var(--te-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-family: var(--font-main);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.technical-info {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--te-gray);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-section {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-btn {
|
.nav-btn {
|
||||||
background: #ffffff;
|
appearance: none;
|
||||||
border: 2px solid #ffffff;
|
border: 1px solid var(--te-fg);
|
||||||
color: #111111;
|
background: transparent;
|
||||||
padding: 0.5rem 1rem;
|
color: var(--te-fg);
|
||||||
font-size: 1rem;
|
padding: 4px 12px;
|
||||||
font-weight: 800;
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-btn:hover {
|
||||||
|
background-color: var(--te-fg);
|
||||||
|
color: var(--te-bg);
|
||||||
|
}
|
||||||
|
|
||||||
.nav-btn:active {
|
.nav-btn:active {
|
||||||
background-color: #cccccc;
|
transform: translateY(1px);
|
||||||
border-color: #cccccc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-content {
|
.app-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.app-header {
|
.app-header {
|
||||||
background-color: #ffffff;
|
border-color: #222;
|
||||||
color: #111111;
|
|
||||||
border-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn {
|
|
||||||
background-color: #111111;
|
|
||||||
border-color: #111111;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn:active {
|
|
||||||
background-color: #333333;
|
|
||||||
border-color: #333333;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,34 +1,35 @@
|
|||||||
:root {
|
:root {
|
||||||
--ev-c-white: #ffffff;
|
/* Teenage Engineering Palette - LIGHT THEME DEFAULT */
|
||||||
--ev-c-white-soft: #f8f8f8;
|
--te-bg: #f5f5f5;
|
||||||
--ev-c-white-mute: #f2f2f2;
|
--te-fg: #000000;
|
||||||
|
--te-orange: #ff5c00;
|
||||||
|
--te-gray: #888888;
|
||||||
|
--te-gray-light: #dadada;
|
||||||
|
--te-gray-dark: #333333;
|
||||||
|
--te-black: #000000;
|
||||||
|
--te-white: #ffffff;
|
||||||
|
|
||||||
--ev-c-black: #1b1b1f;
|
--color-background: var(--te-bg);
|
||||||
--ev-c-black-soft: #222222;
|
--color-text: var(--te-fg);
|
||||||
--ev-c-black-mute: #282828;
|
|
||||||
|
|
||||||
--ev-c-gray-1: #515c67;
|
--font-main: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
--ev-c-gray-2: #414853;
|
--font-mono: "JetBrains Mono", "Fira Code", monospace;
|
||||||
--ev-c-gray-3: #32363f;
|
|
||||||
|
|
||||||
--ev-c-text-1: rgba(255, 255, 245, 0.86);
|
--spacing-unit: 4px;
|
||||||
--ev-c-text-2: rgba(235, 235, 245, 0.6);
|
--border-width: 1px;
|
||||||
--ev-c-text-3: rgba(235, 235, 245, 0.38);
|
|
||||||
|
|
||||||
--ev-button-alt-border: transparent;
|
|
||||||
--ev-button-alt-text: var(--ev-c-text-1);
|
|
||||||
--ev-button-alt-bg: var(--ev-c-gray-3);
|
|
||||||
--ev-button-alt-hover-border: transparent;
|
|
||||||
--ev-button-alt-hover-text: var(--ev-c-text-1);
|
|
||||||
--ev-button-alt-hover-bg: var(--ev-c-gray-2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
@media (prefers-color-scheme: dark) {
|
||||||
--color-background: var(--ev-c-black);
|
:root {
|
||||||
--color-background-soft: var(--ev-c-black-soft);
|
--te-bg: #121212;
|
||||||
--color-background-mute: var(--ev-c-black-mute);
|
--te-fg: #ffffff;
|
||||||
|
--te-gray: #666666;
|
||||||
|
--te-gray-light: #2a2a2a;
|
||||||
|
--te-gray-dark: #888888;
|
||||||
|
|
||||||
--color-text: var(--ev-c-text-1);
|
--color-background: var(--te-bg);
|
||||||
|
--color-text: var(--te-fg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
@@ -36,32 +37,45 @@
|
|||||||
*::after {
|
*::after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: normal;
|
padding: 0;
|
||||||
}
|
transition: none !important;
|
||||||
|
/* TE style is instant */
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
line-height: 1.6;
|
font-family: var(--font-main);
|
||||||
font-family:
|
font-size: 14px;
|
||||||
Inter,
|
font-weight: 400;
|
||||||
-apple-system,
|
line-height: 1.2;
|
||||||
BlinkMacSystemFont,
|
|
||||||
"Segoe UI",
|
|
||||||
Roboto,
|
|
||||||
Oxygen,
|
|
||||||
Ubuntu,
|
|
||||||
Cantarell,
|
|
||||||
"Fira Sans",
|
|
||||||
"Droid Sans",
|
|
||||||
"Helvetica Neue",
|
|
||||||
sans-serif;
|
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
overflow: hidden;
|
||||||
|
/* Desktop app feel */
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-family: var(--font-main);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
|
||||||
const printerUrl = ref("192.168.1.100");
|
const printerUrl = ref(localStorage.getItem("printerUrl") || "192.168.1.100");
|
||||||
|
const serverUrl = ref(
|
||||||
|
localStorage.getItem("serverUrl") || "http://localhost:3000",
|
||||||
|
);
|
||||||
const errorMsg = ref("");
|
const errorMsg = ref("");
|
||||||
const isSuccess = ref(false);
|
const isSuccess = ref(false);
|
||||||
|
const pingStatus = ref<{
|
||||||
|
[key: string]: "idle" | "pinging" | "success" | "error";
|
||||||
|
}>({
|
||||||
|
printer: "idle",
|
||||||
|
server: "idle",
|
||||||
|
});
|
||||||
|
|
||||||
const validateIpOrUrl = (value: string): boolean => {
|
const validateIpOrUrl = (value: string): boolean => {
|
||||||
if (!value) return false;
|
if (!value) return false;
|
||||||
@@ -21,11 +30,17 @@ const handleSave = (): void => {
|
|||||||
isSuccess.value = false;
|
isSuccess.value = false;
|
||||||
|
|
||||||
if (!validateIpOrUrl(printerUrl.value)) {
|
if (!validateIpOrUrl(printerUrl.value)) {
|
||||||
errorMsg.value = "Invalid IP or URL format";
|
errorMsg.value = "Invalid Printer IP or URL format";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save logic here (e.g. electron store)
|
if (!validateIpOrUrl(serverUrl.value)) {
|
||||||
|
errorMsg.value = "Invalid Server IP or URL format";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem("printerUrl", printerUrl.value);
|
||||||
|
localStorage.setItem("serverUrl", serverUrl.value);
|
||||||
isSuccess.value = true;
|
isSuccess.value = true;
|
||||||
|
|
||||||
setTimeout((): void => {
|
setTimeout((): void => {
|
||||||
@@ -33,181 +48,281 @@ const handleSave = (): void => {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePing = (): void => {
|
const performPing = async (type: "printer" | "server"): Promise<void> => {
|
||||||
errorMsg.value = "";
|
errorMsg.value = "";
|
||||||
isSuccess.value = false;
|
isSuccess.value = false;
|
||||||
|
|
||||||
if (!validateIpOrUrl(printerUrl.value)) {
|
const url = type === "printer" ? printerUrl.value : serverUrl.value;
|
||||||
errorMsg.value = "Invalid IP/URL to ping";
|
|
||||||
|
if (!validateIpOrUrl(url)) {
|
||||||
|
errorMsg.value = `Invalid ${type === "printer" ? "Printer" : "Server"} IP/URL format`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ping actual mock logic
|
// Ensure URL has protocol for the ping check
|
||||||
console.log("Pinging", printerUrl.value);
|
let pingUrl = url;
|
||||||
alert(`Pinging ${printerUrl.value}... (Not implemented yet)`);
|
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||||
|
pingUrl = `http://${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
pingStatus.value[type] = "pinging";
|
||||||
|
try {
|
||||||
|
const ok = await window.api.nfc.ping(pingUrl);
|
||||||
|
pingStatus.value[type] = ok ? "success" : "error";
|
||||||
|
if (!ok) {
|
||||||
|
errorMsg.value = `${type === "printer" ? "Printer" : "Server"} is unreachable`;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Ping error:", err);
|
||||||
|
pingStatus.value[type] = "error";
|
||||||
|
errorMsg.value = "An error occurred during ping";
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (pingStatus.value[type] !== "pinging") {
|
||||||
|
pingStatus.value[type] = "idle";
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePingPrinter = (): void => {
|
||||||
|
performPing("printer");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePingServer = (): void => {
|
||||||
|
performPing("server");
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="config-view">
|
<div class="config-panel">
|
||||||
<h2>PRINTER CONFIGURATION</h2>
|
<div class="panel-header">
|
||||||
|
<h3>SYSTEM_CONFIGURATION</h3>
|
||||||
<div class="form-group">
|
<div class="header-line"></div>
|
||||||
<label for="printer-url">Printer IP / URL</label>
|
|
||||||
<div class="input-row">
|
|
||||||
<input
|
|
||||||
id="printer-url"
|
|
||||||
v-model="printerUrl"
|
|
||||||
type="text"
|
|
||||||
placeholder="e.g. 192.168.1.100 or http://printer.local"
|
|
||||||
:class="{ 'input-error': errorMsg }"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-secondary"
|
|
||||||
@click="handlePing"
|
|
||||||
>
|
|
||||||
PING PRINTER
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="errorMsg" class="error-text">{{ errorMsg }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="config-grid">
|
||||||
<button class="btn btn-primary" @click="handleSave">
|
<!-- Printer Config -->
|
||||||
SAVE SETTINGS
|
<div class="config-item">
|
||||||
|
<div class="item-meta">
|
||||||
|
<span class="item-index">01</span>
|
||||||
|
<label for="printer-url">PRINTER_ADDRESS</label>
|
||||||
|
</div>
|
||||||
|
<div class="input-with-action">
|
||||||
|
<input
|
||||||
|
id="printer-url"
|
||||||
|
v-model="printerUrl"
|
||||||
|
type="text"
|
||||||
|
placeholder="IP_OR_URL"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="action-btn"
|
||||||
|
:disabled="pingStatus.printer === 'pinging'"
|
||||||
|
@click="handlePingPrinter"
|
||||||
|
>
|
||||||
|
{{ pingStatus.printer === "pinging" ? "..." : "PING" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="pingStatus.printer === 'success'"
|
||||||
|
class="field-status success"
|
||||||
|
>
|
||||||
|
ONLINE
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Server Config -->
|
||||||
|
<div class="config-item">
|
||||||
|
<div class="item-meta">
|
||||||
|
<span class="item-index">02</span>
|
||||||
|
<label for="server-url">GATEWAY_SERVER</label>
|
||||||
|
</div>
|
||||||
|
<div class="input-with-action">
|
||||||
|
<input
|
||||||
|
id="server-url"
|
||||||
|
v-model="serverUrl"
|
||||||
|
type="text"
|
||||||
|
placeholder="IP_OR_URL"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="action-btn"
|
||||||
|
:disabled="pingStatus.server === 'pinging'"
|
||||||
|
@click="handlePingServer"
|
||||||
|
>
|
||||||
|
{{ pingStatus.server === "pinging" ? "..." : "PING" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="pingStatus.server === 'success'"
|
||||||
|
class="field-status success"
|
||||||
|
>
|
||||||
|
REACHABLE
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="pingStatus.server === 'error'"
|
||||||
|
class="field-status error"
|
||||||
|
>
|
||||||
|
OFFLINE
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="errorMsg" class="form-error">
|
||||||
|
<span>[ERR]</span> {{ errorMsg }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-actions">
|
||||||
|
<button class="save-btn" @click="handleSave">
|
||||||
|
WRITE_TO_MEMORY
|
||||||
</button>
|
</button>
|
||||||
<span v-if="isSuccess" class="success-text">Settings Saved!</span>
|
<span v-if="isSuccess" class="write-success">WRITE_OK</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.config-view {
|
.config-panel {
|
||||||
width: 100%;
|
flex: 1;
|
||||||
max-width: 800px;
|
display: flex;
|
||||||
margin: 0 auto;
|
flex-direction: column;
|
||||||
padding: 2rem;
|
padding: 40px;
|
||||||
background-color: #f9f9f9;
|
background-color: var(--te-bg);
|
||||||
border: 4px solid #111;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
.panel-header {
|
||||||
font-size: 1.75rem;
|
margin-bottom: 40px;
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
border-bottom: 4px solid #111;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
h3 {
|
||||||
margin-bottom: 2rem;
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--te-gray);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-line {
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--te-gray-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-index {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--te-orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: block;
|
font-size: 10px;
|
||||||
font-size: 1.25rem;
|
font-weight: 700;
|
||||||
font-weight: bold;
|
text-transform: uppercase;
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-row {
|
.input-with-action {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
border: 1px solid var(--te-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0.75rem;
|
border: none;
|
||||||
font-size: 1.25rem;
|
background: transparent;
|
||||||
border: 4px solid #111;
|
padding: 8px 12px;
|
||||||
background-color: #fff;
|
font-family: var(--font-mono);
|
||||||
color: #111;
|
font-size: 11px;
|
||||||
|
color: var(--te-fg);
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:focus {
|
input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
background-color: #ffffe0;
|
background-color: var(--te-gray-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-error {
|
.action-btn {
|
||||||
border-color: #cc0000;
|
border-left: 1px solid var(--te-fg);
|
||||||
background-color: #ffeeee;
|
padding: 0 12px;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--te-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-text {
|
.action-btn:hover:not(:disabled) {
|
||||||
color: #cc0000;
|
background-color: var(--te-fg);
|
||||||
font-weight: bold;
|
color: var(--te-bg);
|
||||||
font-size: 1.25rem;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-text {
|
.field-status {
|
||||||
color: #008822;
|
font-family: var(--font-mono);
|
||||||
font-weight: bold;
|
font-size: 8px;
|
||||||
font-size: 1.25rem;
|
margin-top: 4px;
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.field-status.success {
|
||||||
|
color: #00aa00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-status.error {
|
||||||
|
color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-error {
|
||||||
|
margin-top: 40px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-actions {
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-top: 4px solid #111;
|
gap: 20px;
|
||||||
padding-top: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.save-btn {
|
||||||
padding: 0.75rem 1.5rem;
|
appearance: none;
|
||||||
font-size: 1.25rem;
|
background-color: var(--te-orange);
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 4px solid #111;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background-color: #111;
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
padding: 10px 24px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.save-btn:active {
|
||||||
background-color: #ddd;
|
opacity: 0.8;
|
||||||
color: #111;
|
}
|
||||||
|
|
||||||
|
.write-success {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
color: #00aa00;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.config-view {
|
.header-line {
|
||||||
background-color: #222;
|
background-color: #222;
|
||||||
border-color: #eee;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h2,
|
|
||||||
.actions {
|
|
||||||
border-color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
background-color: #333;
|
|
||||||
color: #eee;
|
|
||||||
border-color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus {
|
input:focus {
|
||||||
background-color: #444;
|
background-color: #1a1a1a;
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
border-color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background-color: #eee;
|
|
||||||
color: #111;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background-color: #444;
|
|
||||||
color: #eee;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,40 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref, onMounted, onUnmounted } from "vue";
|
||||||
|
|
||||||
const readerState = ref<"waiting" | "reading" | "success" | "error">("waiting");
|
const readerState = ref<"waiting" | "reading" | "success" | "error">("waiting");
|
||||||
|
const uid = ref<string | null>(null);
|
||||||
|
const errorMsg = ref<string | null>(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.api.nfc.onTag((event) => {
|
||||||
|
uid.value = event.uid;
|
||||||
|
readerState.value = "success";
|
||||||
|
|
||||||
|
// Reset state after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
readerState.value = "waiting";
|
||||||
|
uid.value = null;
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.api.nfc.onRemoved(() => {
|
||||||
|
console.log("Card removed");
|
||||||
|
// Optional: you could reset state here if you wanted immediate reset on removal
|
||||||
|
});
|
||||||
|
|
||||||
|
window.api.nfc.onError((event) => {
|
||||||
|
console.error("NFC Error:", event.message);
|
||||||
|
readerState.value = "error";
|
||||||
|
setTimeout(() => {
|
||||||
|
readerState.value = "waiting";
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.api.nfc.removeAllListeners();
|
||||||
|
});
|
||||||
|
|
||||||
const mockReadCard = (): void => {
|
const mockReadCard = (): void => {
|
||||||
if (readerState.value !== "waiting") return;
|
if (readerState.value !== "waiting") return;
|
||||||
@@ -26,130 +59,301 @@ const triggerError = (): void => {
|
|||||||
|
|
||||||
setTimeout((): void => {
|
setTimeout((): void => {
|
||||||
readerState.value = "waiting";
|
readerState.value = "waiting";
|
||||||
|
errorMsg.value = null;
|
||||||
}, 3000);
|
}, 3000);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="main-view">
|
<div class="main-display">
|
||||||
<div class="status-container" :class="readerState">
|
<div class="display-container">
|
||||||
<h1 v-if="readerState === 'waiting'">TAP PAYMENT CARD</h1>
|
<div class="display-grid"></div>
|
||||||
<h1 v-else-if="readerState === 'reading'">READING CARD...</h1>
|
|
||||||
<h1 v-else-if="readerState === 'success'">STICKER PRINTED</h1>
|
<div class="top-meta">
|
||||||
<h1 v-else-if="readerState === 'error'">ERROR READING CARD</h1>
|
<div class="meta-item">
|
||||||
|
<span class="label">MODULE</span>
|
||||||
|
<span class="value">NFC_RD_01</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-center">
|
||||||
|
<div v-if="readerState === 'waiting'" class="waiting-state">
|
||||||
|
<div class="pulse-ring"></div>
|
||||||
|
<div class="pulse-inner"></div>
|
||||||
|
<h1 class="status-msg">TAP CARD TO INITIALIZE</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="readerState === 'reading'"
|
||||||
|
class="reading-state"
|
||||||
|
>
|
||||||
|
<div class="reading-indicator"></div>
|
||||||
|
<h1 class="status-msg reading">PROCESSING_DATA...</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="readerState === 'success'"
|
||||||
|
class="success-state"
|
||||||
|
>
|
||||||
|
<div class="success-indicator"></div>
|
||||||
|
<h1 class="status-msg success">SESSION_START_OK</h1>
|
||||||
|
<div class="id-capture">
|
||||||
|
<span class="id-label">UID_IDENTIFIED:</span>
|
||||||
|
<span class="id-value">{{ uid }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="readerState === 'error'" class="error-state">
|
||||||
|
<div class="error-indicator"></div>
|
||||||
|
<h1 class="status-msg error">INTERFACE_ERROR</h1>
|
||||||
|
<p class="error-desc">
|
||||||
|
{{ errorMsg || "UNKNOWN_FAILURE" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hidden buttons to trigger mock state changes for manual testing -->
|
<!-- Discrete mock controls for dev testing -->
|
||||||
<div class="mock-controls">
|
<div class="dev-panel">
|
||||||
<p>Developer Testing Controls:</p>
|
<button
|
||||||
<button @click="mockReadCard" :disabled="readerState !== 'waiting'">
|
class="dev-btn"
|
||||||
Simulate Card Tap
|
:disabled="readerState !== 'waiting'"
|
||||||
|
@click="mockReadCard"
|
||||||
|
>
|
||||||
|
FORCE_TAP
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="triggerError"
|
class="dev-btn danger"
|
||||||
:disabled="readerState !== 'waiting'"
|
:disabled="readerState !== 'waiting'"
|
||||||
class="error-btn"
|
@click="triggerError"
|
||||||
>
|
>
|
||||||
Simulate Error
|
FORCE_ERR
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main-view {
|
@font-face {
|
||||||
|
font-family: "Share Tech Mono";
|
||||||
|
src: url("https://fonts.gstatic.com/s/sharetechmono/v15/J7aHnp1_hDHR2LdAKxptgXch5g.woff2")
|
||||||
|
format("woff2");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-display {
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
padding: 24px;
|
||||||
align-items: center;
|
gap: 12px;
|
||||||
|
background-color: var(--te-bg);
|
||||||
|
color: var(--te-fg);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
position: relative;
|
||||||
}
|
background-color: var(--te-white);
|
||||||
|
border: 1px solid var(--te-fg);
|
||||||
.status-container {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
flex-direction: column;
|
||||||
align-items: center;
|
padding: 24px;
|
||||||
width: 100%;
|
overflow: hidden;
|
||||||
height: 250px;
|
|
||||||
border: 4px solid var(--color-text);
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-container h1 {
|
.display-grid {
|
||||||
font-size: 2.5rem;
|
position: absolute;
|
||||||
font-weight: 700;
|
inset: 0;
|
||||||
text-align: center;
|
background-image: radial-gradient(
|
||||||
margin: 0;
|
var(--te-gray-light) 1px,
|
||||||
padding: 1.5rem;
|
transparent 1px
|
||||||
}
|
);
|
||||||
|
background-size: 20px 20px;
|
||||||
/* State changes - Solid high contrast colors, ZERO animations */
|
|
||||||
.waiting {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
color: #111111;
|
|
||||||
border-color: #111111;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reading {
|
|
||||||
background-color: #ffcc00; /* Yellow for processing */
|
|
||||||
color: #111111;
|
|
||||||
border-color: #111111;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success {
|
|
||||||
background-color: #00cc44; /* Green for success */
|
|
||||||
color: #ffffff;
|
|
||||||
border-color: #008822; /* Darker green border */
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
background-color: #cc0000; /* Red for error */
|
|
||||||
color: #ffffff;
|
|
||||||
border-color: #880000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Developer testing controls */
|
|
||||||
.mock-controls {
|
|
||||||
margin-top: auto;
|
|
||||||
padding: 1rem;
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
border: 2px dashed #999;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-controls p {
|
|
||||||
margin: 0 0 1rem 0;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 2px solid #111;
|
|
||||||
background-color: #fff;
|
|
||||||
color: #111;
|
|
||||||
margin: 0 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-btn {
|
.top-meta,
|
||||||
border-color: #cc0000;
|
.bottom-meta {
|
||||||
color: #cc0000;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
z-index: 1;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 8px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--te-gray);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-center {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-msg {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
margin-top: 16px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting-state,
|
||||||
|
.reading-state,
|
||||||
|
.success-state,
|
||||||
|
.error-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse-inner {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background-color: var(--te-orange);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reading-indicator {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background-color: var(--te-orange);
|
||||||
|
animation: blink 1s infinite step-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-indicator {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background-color: #00aa44; /* Stronger green for light theme */
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-capture {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-label {
|
||||||
|
font-size: 8px;
|
||||||
|
color: var(--te-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-value {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-indicator {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background-color: var(--te-red);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-desc {
|
||||||
|
font-size: 9px;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: var(--te-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-bars {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
width: 6px;
|
||||||
|
height: 4px;
|
||||||
|
background-color: var(--te-gray-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar.active {
|
||||||
|
background-color: var(--te-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-panel {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-btn {
|
||||||
|
appearance: none;
|
||||||
|
border: 1px solid var(--te-gray-light);
|
||||||
|
color: var(--te-gray);
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-btn:hover:not(:disabled) {
|
||||||
|
border-color: var(--te-fg);
|
||||||
|
color: var(--te-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-btn.danger:hover:not(:disabled) {
|
||||||
|
border-color: var(--te-red);
|
||||||
|
color: var(--te-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-btn:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uppercase {
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.waiting {
|
.main-display {
|
||||||
background-color: #222222;
|
background-color: var(--te-bg);
|
||||||
color: #eeeeee;
|
}
|
||||||
border-color: #eeeeee;
|
.display-container {
|
||||||
|
background-color: #111;
|
||||||
|
}
|
||||||
|
.display-grid {
|
||||||
|
background-image: radial-gradient(
|
||||||
|
var(--te-gray-dark) 1px,
|
||||||
|
transparent 1px
|
||||||
|
);
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
.success-indicator {
|
||||||
|
background-color: var(--te-fg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,39 +1,46 @@
|
|||||||
//import PCSC , { Reader } from '@tockawa/nfc-pcsc'
|
import PCSC, { Tag, Reader } from "@tockawa/nfc-pcsc";
|
||||||
//import { NFC, Tag, Reader } from 'nfc-pcsc';
|
|
||||||
import PCSC, { Card, Tag, Reader } from "@tockawa/nfc-pcsc";
|
export type NfcEvent =
|
||||||
//import type Card from "@tockawa/nfc-pcsc"
|
| { type: "tag"; uid: string }
|
||||||
|
| { type: "removed"; uid: string }
|
||||||
|
| { type: "error"; message: string };
|
||||||
|
|
||||||
export class NfcService {
|
export class NfcService {
|
||||||
private nfc: PCSC;
|
private nfc: PCSC;
|
||||||
constructor() {
|
private onEvent?: (event: NfcEvent) => void;
|
||||||
this.nfc = new PCSC.default(); // Podrías pasar un logger aquí
|
|
||||||
|
constructor(onEvent?: (event: NfcEvent) => void) {
|
||||||
|
this.nfc = new PCSC.default();
|
||||||
|
this.onEvent = onEvent;
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
private init() {
|
private init(): void {
|
||||||
this.nfc.on("reader", (reader: Reader) => {
|
this.nfc.on("reader", (reader: Reader) => {
|
||||||
console.log(`Lector detectado: ${reader.name}`);
|
console.log(`Lector detectado: ${reader.name}`);
|
||||||
//reader.autoProcessing = false;
|
|
||||||
//reader.aid = 'F222222222';
|
|
||||||
|
|
||||||
// Configuración del lector (opcional)
|
|
||||||
// reader.aid = 'F222222222';
|
|
||||||
|
|
||||||
reader.on("card", async (card: Tag) => {
|
reader.on("card", async (card: Tag) => {
|
||||||
// tag.uid es el identificador único del chip NFC
|
|
||||||
console.log(card);
|
|
||||||
console.log(`Tarjeta detectada! UID: ${card.uid}`);
|
console.log(`Tarjeta detectada! UID: ${card.uid}`);
|
||||||
|
if (this.onEvent) {
|
||||||
// Aquí enviarías el UID al proceso de renderizado (tu UI)
|
this.onEvent({ type: "tag", uid: card.uid });
|
||||||
// this.sendToWindow('nfc-tag-read', tag.uid);
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
reader.on("card.off", async (card: Tag) => {
|
reader.on("card.off", async (card: Tag) => {
|
||||||
console.log(`Tarjeta retirada: ${card.uid}`);
|
console.log(`Tarjeta retirada: ${card.uid}`);
|
||||||
|
if (this.onEvent) {
|
||||||
|
this.onEvent({ type: "removed", uid: card.uid });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
reader.on("error", (err: any) => {
|
reader.on("error", (err: Error) => {
|
||||||
console.error(`Error en el lector ${reader.name}:`, err);
|
console.error(`Error en el lector ${reader.name}:`, err);
|
||||||
|
if (this.onEvent) {
|
||||||
|
this.onEvent({
|
||||||
|
type: "error",
|
||||||
|
message: err.message || String(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
reader.on("end", () => {
|
reader.on("end", () => {
|
||||||
@@ -41,13 +48,18 @@ export class NfcService {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.nfc.on("error", (err: any) => {
|
this.nfc.on("error", (err: Error) => {
|
||||||
console.error("Error general de NFC:", err);
|
console.error("Error general de NFC:", err);
|
||||||
|
if (this.onEvent) {
|
||||||
|
this.onEvent({
|
||||||
|
type: "error",
|
||||||
|
message: err.message || String(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Método para detener el servicio si es necesario
|
public stop(): void {
|
||||||
public stop() {
|
|
||||||
// La mayoría de los lectores se cierran solos al cerrar la app
|
// La mayoría de los lectores se cierran solos al cerrar la app
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user