feat: API client, WebSocket composable, and Pinia device store
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
46
frontend/src/api/index.js
Normal file
46
frontend/src/api/index.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const BASE_URL = '/api'
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const url = `${BASE_URL}${path}`
|
||||
const config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
...options,
|
||||
}
|
||||
if (config.body && typeof config.body === 'object') {
|
||||
config.body = JSON.stringify(config.body)
|
||||
}
|
||||
const res = await fetch(url, config)
|
||||
if (!res.ok) {
|
||||
const error = new Error(`API error: ${res.status}`)
|
||||
error.status = res.status
|
||||
try {
|
||||
error.data = await res.json()
|
||||
} catch {
|
||||
error.data = await res.text()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
if (res.status === 204) return null
|
||||
return res.json()
|
||||
}
|
||||
|
||||
// Devices
|
||||
export const getDevices = () => request('/devices')
|
||||
export const createDevice = (data) => request('/devices', { method: 'POST', body: data })
|
||||
export const getDevice = (id) => request(`/devices/${id}`)
|
||||
export const updateDevice = (id, data) => request(`/devices/${id}`, { method: 'PUT', body: data })
|
||||
export const deleteDevice = (id) => request(`/devices/${id}`, { method: 'DELETE' })
|
||||
export const sendCommand = (id, payload) =>
|
||||
request(`/devices/${id}/command`, { method: 'POST', body: { payload } })
|
||||
export const getDeviceLogs = (id, limit = 20) => request(`/devices/${id}/logs?limit=${limit}`)
|
||||
|
||||
// Broker
|
||||
export const getBrokerStatus = () => request('/broker/status')
|
||||
export const getBrokerClients = () => request('/broker/clients')
|
||||
export const getBrokerTopics = () => request('/broker/topics')
|
||||
|
||||
// Dashboard
|
||||
export const getDashboardStats = () => request('/dashboard')
|
||||
48
frontend/src/composables/useWebSocket.js
Normal file
48
frontend/src/composables/useWebSocket.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
export function useWebSocket(url, onMessage) {
|
||||
const connected = ref(false)
|
||||
let ws = null
|
||||
let reconnectTimer = null
|
||||
let retryDelay = 1000
|
||||
|
||||
function connect() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const fullUrl = `${protocol}//${window.location.host}${url}`
|
||||
|
||||
ws = new WebSocket(fullUrl)
|
||||
|
||||
ws.onopen = () => {
|
||||
connected.value = true
|
||||
retryDelay = 1000
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
onMessage(data)
|
||||
} catch {
|
||||
// ignore non-JSON messages
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
connected.value = false
|
||||
reconnectTimer = setTimeout(connect, retryDelay)
|
||||
retryDelay = Math.min(retryDelay * 2, 30000)
|
||||
}
|
||||
|
||||
ws.onerror = () => {
|
||||
ws.close()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => connect())
|
||||
|
||||
onUnmounted(() => {
|
||||
clearTimeout(reconnectTimer)
|
||||
if (ws) ws.close()
|
||||
})
|
||||
|
||||
return { connected }
|
||||
}
|
||||
60
frontend/src/stores/devices.js
Normal file
60
frontend/src/stores/devices.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { getDevices, createDevice, deleteDevice, sendCommand as apiSendCommand } from '../api'
|
||||
import { useWebSocket } from '../composables/useWebSocket'
|
||||
|
||||
export const useDeviceStore = defineStore('devices', () => {
|
||||
const devices = ref([])
|
||||
const loading = ref(false)
|
||||
const wsConnected = ref(false)
|
||||
|
||||
async function fetchDevices() {
|
||||
loading.value = true
|
||||
try {
|
||||
devices.value = await getDevices()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function addDevice(data) {
|
||||
const device = await createDevice(data)
|
||||
devices.value.unshift(device)
|
||||
return device
|
||||
}
|
||||
|
||||
async function removeDevice(id) {
|
||||
await deleteDevice(id)
|
||||
devices.value = devices.value.filter((d) => d.id !== id)
|
||||
}
|
||||
|
||||
async function sendCommand(id, payload) {
|
||||
return await apiSendCommand(id, payload)
|
||||
}
|
||||
|
||||
function handleWsMessage(data) {
|
||||
if (data.type !== 'device_update') return
|
||||
const idx = devices.value.findIndex((d) => d.id === data.device_id)
|
||||
if (idx === -1) return
|
||||
const device = devices.value[idx]
|
||||
devices.value.splice(idx, 1, {
|
||||
...device,
|
||||
state: data.state,
|
||||
is_online: data.is_online,
|
||||
last_seen: data.last_seen,
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize WebSocket
|
||||
const { connected } = useWebSocket('/ws/devices', handleWsMessage)
|
||||
|
||||
return {
|
||||
devices,
|
||||
loading,
|
||||
wsConnected,
|
||||
fetchDevices,
|
||||
addDevice,
|
||||
removeDevice,
|
||||
sendCommand,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user