188 lines
8.2 KiB
HTML
188 lines
8.2 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>JT808 Server Dashboard</title>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<style>
|
|
body { background-color: #f8f9fa; }
|
|
.dashboard-card { transition: all 0.3s; }
|
|
.dashboard-card:hover { transform: translateY(-5px); shadow: 0 4px 8px rgba(0,0,0,0.1); }
|
|
.log-console {
|
|
background-color: #1e1e1e;
|
|
color: #00ff00;
|
|
font-family: 'Courier New', Courier, monospace;
|
|
height: 300px;
|
|
overflow-y: auto;
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
font-size: 0.9rem;
|
|
}
|
|
.log-entry { margin-bottom: 5px; border-bottom: 1px solid #333; padding-bottom: 2px; }
|
|
.log-time { color: #888; margin-right: 10px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<nav class="navbar navbar-dark bg-dark">
|
|
<div class="container-fluid">
|
|
<span class="navbar-brand mb-0 h1">JT808 Transport Server</span>
|
|
<span class="text-light">
|
|
Status:
|
|
<span class="badge" :class="connected ? 'bg-success' : 'bg-danger'">
|
|
{{ connected ? 'Connected' : 'Disconnected' }}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="container mt-4">
|
|
<div class="row">
|
|
<!-- Device Simulation Card -->
|
|
<div class="col-md-5">
|
|
<div class="card dashboard-card mb-4">
|
|
<div class="card-header bg-primary text-white">
|
|
Device Simulation (Typed API)
|
|
</div>
|
|
<div class="card-body">
|
|
<form @submit.prevent="sendReport">
|
|
<div class="mb-3">
|
|
<label class="form-label">IMEI</label>
|
|
<input v-model="form.imei" class="form-control" placeholder="123456789012">
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-6 mb-3">
|
|
<label class="form-label">Lat</label>
|
|
<input v-model.number="form.lat" type="number" step="0.000001" class="form-control">
|
|
</div>
|
|
<div class="col-6 mb-3">
|
|
<label class="form-label">Lon</label>
|
|
<input v-model.number="form.lon" type="number" step="0.000001" class="form-control">
|
|
</div>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary w-100">Send Location</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Generic Upload Test -->
|
|
<div class="card dashboard-card mb-4">
|
|
<div class="card-header bg-info text-white">
|
|
Universal Upload Test (/upload)
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Arbitrary JSON Payload</label>
|
|
<textarea v-model="customJson" class="form-control" rows="3"></textarea>
|
|
</div>
|
|
<button @click="sendCustomJson" class="btn btn-info text-white w-100">Send to /upload</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Live Log Console -->
|
|
<div class="col-md-7">
|
|
<div class="card dashboard-card h-100">
|
|
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
|
<span>Live Data Stream (/api/v1/device/upload)</span>
|
|
<button @click="logs = []" class="btn btn-sm btn-outline-secondary">Clear</button>
|
|
</div>
|
|
<div class="card-body bg-dark p-0">
|
|
<div class="log-console" ref="console">
|
|
<div v-if="logs.length === 0" class="text-muted text-center mt-5">Waiting for data...</div>
|
|
<div v-for="(log, index) in logs" :key="index" class="log-entry">
|
|
<span class="log-time">[{{ log.time }}]</span>
|
|
<span>{{ log.data }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const { createApp } = Vue
|
|
createApp({
|
|
data() {
|
|
return {
|
|
form: {
|
|
imei: '13800138000',
|
|
lat: 34.215432,
|
|
lon: 108.924231,
|
|
speed: 60.5
|
|
},
|
|
customJson: '{\n "sensor": "temp-01",\n "value": 25.5,\n "unit": "C"\n}',
|
|
logs: [],
|
|
connected: false,
|
|
eventSource: null
|
|
}
|
|
},
|
|
mounted() {
|
|
this.connectSSE();
|
|
},
|
|
methods: {
|
|
connectSSE() {
|
|
this.eventSource = new EventSource('/api/v1/device/logs/stream');
|
|
|
|
this.eventSource.onopen = () => {
|
|
this.connected = true;
|
|
this.addLog('System connected. Listening for /upload events...');
|
|
};
|
|
|
|
this.eventSource.onerror = () => {
|
|
this.connected = false;
|
|
this.eventSource.close();
|
|
// Reconnect after 3s
|
|
setTimeout(() => this.connectSSE(), 3000);
|
|
};
|
|
|
|
this.eventSource.addEventListener('api-log', (event) => {
|
|
const data = JSON.parse(event.data);
|
|
this.addLog(data);
|
|
});
|
|
},
|
|
addLog(data) {
|
|
const now = new Date().toLocaleTimeString();
|
|
this.logs.unshift({
|
|
time: now,
|
|
data: typeof data === 'string' ? data : JSON.stringify(data)
|
|
});
|
|
// Keep last 50 logs
|
|
if (this.logs.length > 50) this.logs.pop();
|
|
},
|
|
async sendReport() {
|
|
try {
|
|
await fetch('/api/v1/device/location', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(this.form)
|
|
});
|
|
// Note: /location endpoint doesn't broadcast to SSE in this demo, only /upload does
|
|
// But we could add it if needed.
|
|
} catch (e) {
|
|
alert('Error: ' + e.message);
|
|
}
|
|
},
|
|
async sendCustomJson() {
|
|
try {
|
|
const res = await fetch('/api/v1/device/upload', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: this.customJson
|
|
});
|
|
const result = await res.json();
|
|
if (result.code !== 200) alert('Error: ' + result.message);
|
|
} catch (e) {
|
|
alert('Error: ' + e.message);
|
|
}
|
|
}
|
|
}
|
|
}).mount('#app')
|
|
</script>
|
|
</body>
|
|
</html>
|