Files
sf-nfc-reader-desktop/src/renderer/src/components/ConfigView.vue

308 lines
7.1 KiB
Vue

<script setup lang="ts">
import { ref } from "vue";
const printerUrl = ref(localStorage.getItem("printerUrl") || "192.168.1.254");
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;
// Basic IP regex
const ipRegex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
// Basic URL regex
const urlRegex = /^(http|https):\/\/[^ "]+$/;
return ipRegex.test(value) || urlRegex.test(value);
};
const handleSave = (): void => {
errorMsg.value = "";
isSuccess.value = false;
localStorage.setItem("printerUrl", printerUrl.value);
localStorage.setItem("serverUrl", serverUrl.value);
isSuccess.value = true;
setTimeout((): void => {
isSuccess.value = false;
}, 2000);
};
const performPing = async (type: "printer" | "server"): Promise<void> => {
errorMsg.value = "";
isSuccess.value = false;
const url = type === "printer" ? printerUrl.value : serverUrl.value;
pingStatus.value[type] = "pinging";
try {
const ok = await window.api.nfc.ping(url);
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-panel">
<div class="panel-header">
<h3>SYSTEM_CONFIGURATION</h3>
<div class="header-line"></div>
</div>
<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="write-success">WRITE_OK</span>
</div>
</div>
</template>
<style scoped>
.config-panel {
flex: 1;
display: flex;
flex-direction: column;
padding: 40px;
background-color: var(--te-bg);
}
.panel-header {
margin-bottom: 40px;
}
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 {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
}
.input-with-action {
display: flex;
border: 1px solid var(--te-fg);
}
input {
flex: 1;
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: var(--te-gray-light);
}
.action-btn {
border-left: 1px solid var(--te-fg);
padding: 0 12px;
font-size: 9px;
font-weight: 700;
color: var(--te-fg);
}
.action-btn:hover:not(:disabled) {
background-color: var(--te-fg);
color: var(--te-bg);
}
.field-status {
font-family: var(--font-mono);
font-size: 8px;
margin-top: 4px;
}
.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;
gap: 20px;
}
.save-btn {
appearance: none;
background-color: var(--te-orange);
color: #fff;
padding: 10px 24px;
font-size: 11px;
font-weight: 700;
}
.save-btn:active {
opacity: 0.8;
}
.write-success {
font-family: var(--font-mono);
font-size: 10px;
color: #00aa00;
}
@media (prefers-color-scheme: dark) {
.header-line {
background-color: #222;
}
input:focus {
background-color: #1a1a1a;
}
}
</style>