Cambios esteticos y de ping para el servidor

This commit is contained in:
2026-03-05 12:01:06 +01:00
parent 70c24f170b
commit 869933c858
8 changed files with 740 additions and 343 deletions

View File

@@ -4,7 +4,6 @@ import { electronApp, optimizer, is } from "@electron-toolkit/utils";
import icon from "../../resources/icon.png?asset";
import { NfcService } from "../services/NfcService";
let nfcService: NfcService | null = null;
function createWindow(): void {
// 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.show();
@@ -57,6 +60,19 @@ app.whenReady().then(() => {
// IPC test
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();
app.on("activate", function () {

View File

@@ -1,8 +1,18 @@
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 {
interface Window {
electron: ElectronAPI;
api: unknown;
api: {
nfc: NfcAPI;
};
}
}

View File

@@ -1,8 +1,28 @@
import { contextBridge } from "electron";
import { contextBridge, ipcRenderer } from "electron";
import { electronAPI } from "@electron-toolkit/preload";
// 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
// renderer only if context isolation is enabled, otherwise

View File

@@ -13,15 +13,18 @@ const navigateTo = (view: "main" | "config"): void => {
<template>
<div class="app-container">
<header class="app-header">
<div class="logo">NFC READER TERMINAL</div>
<nav v-if="currentView === 'main'">
<button class="nav-btn" @click="navigateTo('config')">
SET CONFIG
<div class="logo-section">
<div class="logo-icon"></div>
<div class="logo-text">NFC.INTERFACE.v1</div>
<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>
</nav>
<nav v-else>
<button class="nav-btn" @click="navigateTo('main')">
BACK TO MAIN
<button v-else class="nav-btn" @click="navigateTo('main')">
EXIT_CONFIG
</button>
</nav>
</header>
@@ -34,12 +37,10 @@ const navigateTo = (view: "main" | "config"): void => {
</template>
<style>
/* Global resets and simple styles */
/* Global resets and TE Base */
* {
box-sizing: border-box;
/* Disable transitions entirely */
transition: none !important;
animation: none !important;
cursor: default;
}
html,
@@ -49,20 +50,7 @@ body,
width: 100%;
margin: 0;
padding: 0;
}
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;
}
background-color: var(--te-bg);
}
</style>
@@ -70,64 +58,82 @@ body {
.app-container {
display: flex;
flex-direction: column;
min-height: 100vh;
height: 100vh;
width: 100vw;
background-color: var(--te-bg);
color: var(--te-fg);
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background-color: #111111;
color: #ffffff;
border-bottom: 4px solid #111111;
padding: 12px 24px;
border-bottom: 1px solid var(--te-gray-light);
}
.logo {
font-size: 1.25rem;
font-weight: 800;
.logo-section {
display: flex;
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;
}
.technical-info {
font-family: var(--font-mono);
font-size: 9px;
color: var(--te-gray);
text-transform: uppercase;
}
.nav-section {
display: flex;
}
.nav-btn {
background: #ffffff;
border: 2px solid #ffffff;
color: #111111;
padding: 0.5rem 1rem;
font-size: 1rem;
font-weight: 800;
appearance: none;
border: 1px solid var(--te-fg);
background: transparent;
color: var(--te-fg);
padding: 4px 12px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.05em;
cursor: pointer;
text-transform: uppercase;
}
.nav-btn:hover {
background-color: var(--te-fg);
color: var(--te-bg);
}
.nav-btn:active {
background-color: #cccccc;
border-color: #cccccc;
transform: translateY(1px);
}
.app-content {
flex: 1;
overflow: hidden;
display: flex;
padding: 2rem;
}
@media (prefers-color-scheme: dark) {
.app-header {
background-color: #ffffff;
color: #111111;
border-color: #ffffff;
}
.nav-btn {
background-color: #111111;
border-color: #111111;
color: #ffffff;
}
.nav-btn:active {
background-color: #333333;
border-color: #333333;
border-color: #222;
}
}
</style>

View File

@@ -1,34 +1,35 @@
:root {
--ev-c-white: #ffffff;
--ev-c-white-soft: #f8f8f8;
--ev-c-white-mute: #f2f2f2;
/* Teenage Engineering Palette - LIGHT THEME DEFAULT */
--te-bg: #f5f5f5;
--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;
--ev-c-black-soft: #222222;
--ev-c-black-mute: #282828;
--color-background: var(--te-bg);
--color-text: var(--te-fg);
--ev-c-gray-1: #515c67;
--ev-c-gray-2: #414853;
--ev-c-gray-3: #32363f;
--font-main: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-mono: "JetBrains Mono", "Fira Code", monospace;
--ev-c-text-1: rgba(255, 255, 245, 0.86);
--ev-c-text-2: rgba(235, 235, 245, 0.6);
--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);
--spacing-unit: 4px;
--border-width: 1px;
}
:root {
--color-background: var(--ev-c-black);
--color-background-soft: var(--ev-c-black-soft);
--color-background-mute: var(--ev-c-black-mute);
@media (prefers-color-scheme: dark) {
:root {
--te-bg: #121212;
--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 {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
ul {
list-style: none;
padding: 0;
transition: none !important;
/* TE style is instant */
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Fira Sans",
"Droid Sans",
"Helvetica Neue",
sans-serif;
font-family: var(--font-main);
font-size: 14px;
font-weight: 400;
line-height: 1.2;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-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);
}

View File

@@ -1,9 +1,18 @@
<script setup lang="ts">
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 isSuccess = ref(false);
const pingStatus = ref<{
[key: string]: "idle" | "pinging" | "success" | "error";
}>({
printer: "idle",
server: "idle",
});
const validateIpOrUrl = (value: string): boolean => {
if (!value) return false;
@@ -21,11 +30,17 @@ const handleSave = (): void => {
isSuccess.value = false;
if (!validateIpOrUrl(printerUrl.value)) {
errorMsg.value = "Invalid IP or URL format";
errorMsg.value = "Invalid Printer IP or URL format";
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;
setTimeout((): void => {
@@ -33,181 +48,281 @@ const handleSave = (): void => {
}, 2000);
};
const handlePing = (): void => {
const performPing = async (type: "printer" | "server"): Promise<void> => {
errorMsg.value = "";
isSuccess.value = false;
if (!validateIpOrUrl(printerUrl.value)) {
errorMsg.value = "Invalid IP/URL to ping";
const url = type === "printer" ? printerUrl.value : serverUrl.value;
if (!validateIpOrUrl(url)) {
errorMsg.value = `Invalid ${type === "printer" ? "Printer" : "Server"} IP/URL format`;
return;
}
// Ping actual mock logic
console.log("Pinging", printerUrl.value);
alert(`Pinging ${printerUrl.value}... (Not implemented yet)`);
// Ensure URL has protocol for the ping check
let pingUrl = url;
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>
<template>
<div class="config-view">
<h2>PRINTER CONFIGURATION</h2>
<div class="config-panel">
<div class="panel-header">
<h3>SYSTEM_CONFIGURATION</h3>
<div class="header-line"></div>
</div>
<div class="form-group">
<label for="printer-url">Printer IP / URL</label>
<div class="input-row">
<div class="config-grid">
<!-- Printer Config -->
<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="e.g. 192.168.1.100 or http://printer.local"
:class="{ 'input-error': errorMsg }"
placeholder="IP_OR_URL"
/>
<button
type="button"
class="btn btn-secondary"
@click="handlePing"
class="action-btn"
:disabled="pingStatus.printer === 'pinging'"
@click="handlePingPrinter"
>
PING PRINTER
{{ pingStatus.printer === "pinging" ? "..." : "PING" }}
</button>
</div>
<div v-if="errorMsg" class="error-text">{{ errorMsg }}</div>
<div
v-if="pingStatus.printer === 'success'"
class="field-status success"
>
ONLINE
</div>
</div>
<div class="actions">
<button class="btn btn-primary" @click="handleSave">
SAVE SETTINGS
<!-- 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>
<span v-if="isSuccess" class="success-text">Settings Saved!</span>
</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>
<span v-if="isSuccess" class="write-success">WRITE_OK</span>
</div>
</div>
</template>
<style scoped>
.config-view {
width: 100%;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
background-color: #f9f9f9;
border: 4px solid #111;
.config-panel {
flex: 1;
display: flex;
flex-direction: column;
padding: 40px;
background-color: var(--te-bg);
}
h2 {
font-size: 1.75rem;
font-weight: bold;
margin-top: 0;
margin-bottom: 1.5rem;
border-bottom: 4px solid #111;
padding-bottom: 0.5rem;
.panel-header {
margin-bottom: 40px;
}
.form-group {
margin-bottom: 2rem;
h3 {
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 {
display: block;
font-size: 1.25rem;
font-weight: bold;
margin-bottom: 0.5rem;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
}
.input-row {
.input-with-action {
display: flex;
gap: 1rem;
border: 1px solid var(--te-fg);
}
input {
flex: 1;
padding: 0.75rem;
font-size: 1.25rem;
border: 4px solid #111;
background-color: #fff;
color: #111;
border: none;
background: transparent;
padding: 8px 12px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--te-fg);
width: 100%;
}
input:focus {
outline: none;
background-color: #ffffe0;
background-color: var(--te-gray-light);
}
.input-error {
border-color: #cc0000;
background-color: #ffeeee;
.action-btn {
border-left: 1px solid var(--te-fg);
padding: 0 12px;
font-size: 9px;
font-weight: 700;
color: var(--te-fg);
}
.error-text {
color: #cc0000;
font-weight: bold;
font-size: 1.25rem;
margin-top: 0.5rem;
.action-btn:hover:not(:disabled) {
background-color: var(--te-fg);
color: var(--te-bg);
}
.success-text {
color: #008822;
font-weight: bold;
font-size: 1.25rem;
margin-left: 1rem;
.field-status {
font-family: var(--font-mono);
font-size: 8px;
margin-top: 4px;
}
.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;
align-items: center;
border-top: 4px solid #111;
padding-top: 2rem;
gap: 20px;
}
.btn {
padding: 0.75rem 1.5rem;
font-size: 1.25rem;
font-weight: bold;
cursor: pointer;
border: 4px solid #111;
}
.btn-primary {
background-color: #111;
.save-btn {
appearance: none;
background-color: var(--te-orange);
color: #fff;
padding: 10px 24px;
font-size: 11px;
font-weight: 700;
}
.btn-secondary {
background-color: #ddd;
color: #111;
.save-btn:active {
opacity: 0.8;
}
.write-success {
font-family: var(--font-mono);
font-size: 10px;
color: #00aa00;
}
@media (prefers-color-scheme: dark) {
.config-view {
.header-line {
background-color: #222;
border-color: #eee;
}
h2,
.actions {
border-color: #eee;
}
input {
background-color: #333;
color: #eee;
border-color: #eee;
}
input:focus {
background-color: #444;
}
.btn {
border-color: #eee;
}
.btn-primary {
background-color: #eee;
color: #111;
}
.btn-secondary {
background-color: #444;
color: #eee;
background-color: #1a1a1a;
}
}
</style>

View File

@@ -1,7 +1,40 @@
<script setup lang="ts">
import { ref } from "vue";
import { ref, onMounted, onUnmounted } from "vue";
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 => {
if (readerState.value !== "waiting") return;
@@ -26,130 +59,301 @@ const triggerError = (): void => {
setTimeout((): void => {
readerState.value = "waiting";
errorMsg.value = null;
}, 3000);
};
</script>
<template>
<div class="main-view">
<div class="status-container" :class="readerState">
<h1 v-if="readerState === 'waiting'">TAP PAYMENT CARD</h1>
<h1 v-else-if="readerState === 'reading'">READING CARD...</h1>
<h1 v-else-if="readerState === 'success'">STICKER PRINTED</h1>
<h1 v-else-if="readerState === 'error'">ERROR READING CARD</h1>
<div class="main-display">
<div class="display-container">
<div class="display-grid"></div>
<div class="top-meta">
<div class="meta-item">
<span class="label">MODULE</span>
<span class="value">NFC_RD_01</span>
</div>
</div>
<!-- Hidden buttons to trigger mock state changes for manual testing -->
<div class="mock-controls">
<p>Developer Testing Controls:</p>
<button @click="mockReadCard" :disabled="readerState !== 'waiting'">
Simulate Card Tap
<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>
<!-- Discrete mock controls for dev testing -->
<div class="dev-panel">
<button
class="dev-btn"
:disabled="readerState !== 'waiting'"
@click="mockReadCard"
>
FORCE_TAP
</button>
<button
@click="triggerError"
class="dev-btn danger"
:disabled="readerState !== 'waiting'"
class="error-btn"
@click="triggerError"
>
Simulate Error
FORCE_ERR
</button>
</div>
</div>
</template>
<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;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 24px;
gap: 12px;
background-color: var(--te-bg);
color: var(--te-fg);
font-family: var(--font-mono);
}
.display-container {
flex: 1;
width: 100%;
}
.status-container {
position: relative;
background-color: var(--te-white);
border: 1px solid var(--te-fg);
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 250px;
border: 4px solid var(--color-text);
margin-bottom: 1.5rem;
flex-direction: column;
padding: 24px;
overflow: hidden;
}
.status-container h1 {
font-size: 2.5rem;
font-weight: 700;
text-align: center;
margin: 0;
padding: 1.5rem;
}
/* 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 {
.display-grid {
position: absolute;
inset: 0;
background-image: radial-gradient(
var(--te-gray-light) 1px,
transparent 1px
);
background-size: 20px 20px;
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.error-btn {
border-color: #cc0000;
color: #cc0000;
.top-meta,
.bottom-meta {
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) {
.waiting {
background-color: #222222;
color: #eeeeee;
border-color: #eeeeee;
.main-display {
background-color: var(--te-bg);
}
.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>

View File

@@ -1,39 +1,46 @@
//import PCSC , { Reader } from '@tockawa/nfc-pcsc'
//import { NFC, Tag, Reader } from 'nfc-pcsc';
import PCSC, { Card, Tag, Reader } from "@tockawa/nfc-pcsc";
//import type Card from "@tockawa/nfc-pcsc"
import PCSC, { Tag, Reader } from "@tockawa/nfc-pcsc";
export type NfcEvent =
| { type: "tag"; uid: string }
| { type: "removed"; uid: string }
| { type: "error"; message: string };
export class NfcService {
private nfc: PCSC;
constructor() {
this.nfc = new PCSC.default(); // Podrías pasar un logger aquí
private onEvent?: (event: NfcEvent) => void;
constructor(onEvent?: (event: NfcEvent) => void) {
this.nfc = new PCSC.default();
this.onEvent = onEvent;
this.init();
}
private init() {
private init(): void {
this.nfc.on("reader", (reader: Reader) => {
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) => {
// tag.uid es el identificador único del chip NFC
console.log(card);
console.log(`Tarjeta detectada! UID: ${card.uid}`);
// Aquí enviarías el UID al proceso de renderizado (tu UI)
// this.sendToWindow('nfc-tag-read', tag.uid);
if (this.onEvent) {
this.onEvent({ type: "tag", uid: card.uid });
}
});
reader.on("card.off", async (card: Tag) => {
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);
if (this.onEvent) {
this.onEvent({
type: "error",
message: err.message || String(err),
});
}
});
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);
if (this.onEvent) {
this.onEvent({
type: "error",
message: err.message || String(err),
});
}
});
}
// Método para detener el servicio si es necesario
public stop() {
public stop(): void {
// La mayoría de los lectores se cierran solos al cerrar la app
}
}