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 { 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 () {
|
||||
|
||||
12
src/preload/index.d.ts
vendored
12
src/preload/index.d.ts
vendored
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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="form-group">
|
||||
<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 class="config-panel">
|
||||
<div class="panel-header">
|
||||
<h3>SYSTEM_CONFIGURATION</h3>
|
||||
<div class="header-line"></div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" @click="handleSave">
|
||||
SAVE SETTINGS
|
||||
<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="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>
|
||||
<span v-if="isSuccess" class="success-text">Settings Saved!</span>
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 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
|
||||
<!-- 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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user