# MQTT 智能家居管理系统 - 前端实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 使用 Vue 3 + Tailwind CSS 构建响应式 SPA 前端,连接后端 REST API 和 WebSocket,实现设备管理、仪表盘、Broker 管理三大页面。 **Architecture:** Vue 3 Composition API + Vite 构建工具,Pinia 状态管理(WebSocket 实时更新设备状态),通过 API 模块与后端 FastAPI 通信,Vite 配置代理解决开发跨域。 **Tech Stack:** Vue 3, Vite, Tailwind CSS 3, Pinia, Vue Router 4, @heroicons/vue --- ## 后端 API 接口参考 前端需要对接的后端 API(已在后端实现中就绪): | 方法 | 路径 | 返回类型 | |------|------|----------| | GET | `/api/devices` | `DeviceResponse[]` | | POST | `/api/devices` | `DeviceResponse` (body: `{name, type, mqtt_topic, command_topic?}`) | | GET | `/api/devices/{id}` | `DeviceResponse` | | PUT | `/api/devices/{id}` | `DeviceResponse` (body: `{name?, type?, command_topic?}`) | | DELETE | `/api/devices/{id}` | 204 | | POST | `/api/devices/{id}/command` | `DeviceLogResponse` (body: `{payload: string}`) | | GET | `/api/devices/{id}/logs` | `DeviceLogResponse[]` | | GET | `/api/broker/status` | `{status, metrics}` | | GET | `/api/broker/clients` | `BrokerClient[]` | | GET | `/api/broker/topics` | `BrokerTopic[]` | | GET | `/api/dashboard` | `DashboardStats` | | GET | `/health` | `{status, mqtt_connected}` | | WS | `/ws/devices` | `{type:"device_update", device_id, state, is_online, last_seen}` | --- ## 文件结构总览 ``` frontend/ ├── index.html ├── package.json ├── vite.config.js ├── tailwind.config.js ├── postcss.config.js ├── src/ │ ├── main.js # 入口:创建 app、router、pinia │ ├── App.vue # 根组件:布局 + 侧边栏 │ ├── style.css # Tailwind 导入 │ ├── api/ │ │ └── index.js # HTTP 请求封装(fetch) │ ├── composables/ │ │ └── useWebSocket.js # WebSocket 连接与自动重连 │ ├── stores/ │ │ └── devices.js # Pinia store:设备列表 + 实时更新 │ ├── router/ │ │ └── index.js # Vue Router 路由配置 │ ├── components/ │ │ ├── DeviceCard.vue # 设备卡片(列表页用) │ │ ├── DeviceControl.vue # 设备控制面板(详情页用,按类型自适应) │ │ ├── DeviceLogList.vue # 消息日志列表 │ │ ├── StatsCard.vue # 统计数字卡片 │ │ ├── AddDeviceModal.vue # 添加设备弹窗 │ │ └── Sidebar.vue # 侧边导航栏 │ └── views/ │ ├── DashboardView.vue # 仪表盘页面 │ ├── DevicesView.vue # 设备列表页面 │ ├── DeviceDetailView.vue # 设备详情页面 │ └── BrokerView.vue # Broker 管理页面 ``` --- ### Task 1: Vue 项目初始化与配置 **Files:** - Create: `frontend/package.json` - Create: `frontend/vite.config.js` - Create: `frontend/tailwind.config.js` - Create: `frontend/postcss.config.js` - Create: `frontend/index.html` - Create: `frontend/src/main.js` - Create: `frontend/src/App.vue` - Create: `frontend/src/style.css` - [ ] **Step 1: 创建 `frontend/package.json`** ```json { "name": "mqtt-home-frontend", "private": true, "version": "0.1.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "vue": "^3.5.0", "vue-router": "^4.4.0", "pinia": "^2.2.0", "@heroicons/vue": "^2.2.0" }, "devDependencies": { "@vitejs/plugin-vue": "^5.1.0", "vite": "^5.4.0", "tailwindcss": "^3.4.0", "postcss": "^8.4.0", "autoprefixer": "^10.4.0" } } ``` - [ ] **Step 2: 创建 `frontend/vite.config.js`** ```javascript import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 3000, proxy: { '/api': 'http://localhost:8000', '/ws': { target: 'http://localhost:8000', ws: true, }, '/health': 'http://localhost:8000', }, }, build: { outDir: 'dist', }, }) ``` - [ ] **Step 3: 创建 `frontend/tailwind.config.js`** ```javascript /** @type {import('tailwindcss').Config} */ export default { content: ['./index.html', './src/**/*.{vue,js,ts}'], theme: { extend: { colors: { primary: { 50: '#eff6ff', 100: '#dbeafe', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', }, }, }, }, plugins: [], } ``` - [ ] **Step 4: 创建 `frontend/postcss.config.js`** ```javascript export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ``` - [ ] **Step 5: 创建 `frontend/index.html`** ```html MQTT Home
``` - [ ] **Step 6: 创建 `frontend/src/style.css`** ```css @tailwind base; @tailwind components; @tailwind utilities; body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } ``` - [ ] **Step 7: 创建 `frontend/src/main.js`** ```javascript import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' import router from './router' import './style.css' const app = createApp(App) app.use(createPinia()) app.use(router) app.mount('#app') ``` - [ ] **Step 8: 创建 `frontend/src/App.vue`** ```vue ``` - [ ] **Step 9: 创建占位 `frontend/src/router/index.js`** ```javascript import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', redirect: '/dashboard' }, { path: '/dashboard', name: 'Dashboard', component: () => import('../views/DashboardView.vue'), }, { path: '/devices', name: 'Devices', component: () => import('../views/DevicesView.vue'), }, { path: '/devices/:id', name: 'DeviceDetail', component: () => import('../views/DeviceDetailView.vue'), }, { path: '/broker', name: 'Broker', component: () => import('../views/BrokerView.vue'), }, ], }) export default router ``` - [ ] **Step 10: 创建占位页面文件** 创建以下 4 个最小占位文件: `frontend/src/views/DashboardView.vue`: ```vue ``` `frontend/src/views/DevicesView.vue`: ```vue ``` `frontend/src/views/DeviceDetailView.vue`: ```vue ``` `frontend/src/views/BrokerView.vue`: ```vue ``` 创建 `frontend/src/components/Sidebar.vue`: ```vue ``` - [ ] **Step 11: 安装依赖并验证构建** Run: `cd frontend && npm install` Run: `cd frontend && npm run dev` (后台启动验证无报错) Expected: Vite 启动成功,浏览器访问 localhost:3000 显示侧边栏和路由页面 - [ ] **Step 12: 提交** ```bash git add frontend/ git commit -m "feat: Vue 3 project scaffolding with Tailwind, Router, Pinia, and Sidebar" ``` --- ### Task 2: API 客户端模块 **Files:** - Create: `frontend/src/api/index.js` - [ ] **Step 1: 创建 `frontend/src/api/index.js`** ```javascript 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') ``` - [ ] **Step 2: 提交** ```bash git add frontend/src/api/ git commit -m "feat: API client module for all backend endpoints" ``` --- ### Task 3: WebSocket 组合式函数 **Files:** - Create: `frontend/src/composables/useWebSocket.js` - [ ] **Step 1: 创建 `frontend/src/composables/useWebSocket.js`** ```javascript 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 } } ``` - [ ] **Step 2: 提交** ```bash git add frontend/src/composables/ git commit -m "feat: WebSocket composable with auto-reconnect" ``` --- ### Task 4: Pinia 设备 Store **Files:** - Create: `frontend/src/stores/devices.js` - [ ] **Step 1: 创建 `frontend/src/stores/devices.js`** ```javascript 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) wsConnected.value = connected return { devices, loading, wsConnected, fetchDevices, addDevice, removeDevice, sendCommand, } }) ``` - [ ] **Step 2: 提交** ```bash git add frontend/src/stores/ git commit -m "feat: Pinia device store with WebSocket real-time updates" ``` --- ### Task 5: 仪表盘页面 **Files:** - Create: `frontend/src/components/StatsCard.vue` - Modify: `frontend/src/views/DashboardView.vue` - [ ] **Step 1: 创建 `frontend/src/components/StatsCard.vue`** ```vue ``` - [ ] **Step 2: 替换 `frontend/src/views/DashboardView.vue`** ```vue ``` - [ ] **Step 3: 验证** Run: `cd frontend && npm run dev` Expected: 访问 `/dashboard` 显示三个统计卡片和最近活动列表 - [ ] **Step 4: 提交** ```bash git add frontend/ git commit -m "feat: dashboard page with stats cards and recent activity timeline" ``` --- ### Task 6: 设备列表页面 **Files:** - Create: `frontend/src/components/DeviceCard.vue` - Create: `frontend/src/components/AddDeviceModal.vue` - Modify: `frontend/src/views/DevicesView.vue` - [ ] **Step 1: 创建 `frontend/src/components/DeviceCard.vue`** ```vue ``` - [ ] **Step 2: 创建 `frontend/src/components/AddDeviceModal.vue`** ```vue ``` - [ ] **Step 3: 替换 `frontend/src/views/DevicesView.vue`** ```vue ``` - [ ] **Step 4: 提交** ```bash git add frontend/ git commit -m "feat: devices list page with cards, quick toggle, and add device modal" ``` --- ### Task 7: 设备详情页面 **Files:** - Create: `frontend/src/components/DeviceControl.vue` - Create: `frontend/src/components/DeviceLogList.vue` - Modify: `frontend/src/views/DeviceDetailView.vue` - [ ] **Step 1: 创建 `frontend/src/components/DeviceControl.vue`** ```vue ``` - [ ] **Step 2: 创建 `frontend/src/components/DeviceLogList.vue`** ```vue ``` - [ ] **Step 3: 替换 `frontend/src/views/DeviceDetailView.vue`** ```vue ``` - [ ] **Step 4: 提交** ```bash git add frontend/ git commit -m "feat: device detail page with control panel, custom commands, and message log" ``` --- ### Task 8: Broker 管理页面 **Files:** - Modify: `frontend/src/views/BrokerView.vue` - [ ] **Step 1: 替换 `frontend/src/views/BrokerView.vue`** ```vue ``` - [ ] **Step 2: 提交** ```bash git add frontend/ git commit -m "feat: broker management page with status, clients, and topics" ``` --- ### Task 9: 生产构建与后端静态文件服务 **Files:** - Modify: `src/mqtt_home/main.py` - [ ] **Step 1: 构建前端** Run: `cd frontend && npm run build` Expected: `frontend/dist/` 目录生成,包含 index.html 和 JS/CSS 资源 - [ ] **Step 2: 修改 `src/mqtt_home/main.py`,添加静态文件服务** 在文件顶部 import 区域添加: ```python from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse import pathlib ``` 在 `app.include_router(api_router)` 之前添加: ```python # Serve frontend static files in production frontend_dist = pathlib.Path(__file__).parent.parent.parent / "frontend" / "dist" if frontend_dist.exists(): app.mount("/assets", StaticFiles(directory=str(frontend_dist / "assets")), name="assets") @app.get("/{full_path:path}") async def serve_spa(full_path: str): file = frontend_dist / full_path if file.exists() and file.is_file(): return FileResponse(str(file)) return FileResponse(str(frontend_dist / "index.html")) ``` - [ ] **Step 3: 验证生产模式** Run: `cd D:\home\mqtt && python -m mqtt_home serve` Expected: 访问 `http://localhost:8000/` 显示前端页面,API 正常工作 - [ ] **Step 4: 提交** ```bash git add frontend/ src/ git commit -m "feat: production build with FastAPI static file serving for SPA" ``` --- ### Task 10: 端到端验证与最终提交 - [ ] **Step 1: 启动服务** Run: `cd D:\home\mqtt && python -m mqtt_home serve` - [ ] **Step 2: 浏览器验证以下功能** 1. `http://localhost:8000/` — 仪表盘显示设备统计 2. `http://localhost:8000/devices` — 设备列表(网格布局) 3. 点击"添加设备"弹窗 — 填写表单创建设备 4. 点击设备卡片 — 进入详情页,显示控制面板和日志 5. `http://localhost:8000/broker` — Broker 状态、客户端、主题 6. 侧边栏导航切换正常 7. MQTT 连接状态指示灯正确 - [ ] **Step 3: 提交** ```bash git add -A git commit -m "feat: frontend complete - dashboard, devices, device detail, broker pages" ``` --- ## 自检结果 | 设计文档章节 | 对应 Task | |---|---| | 仪表盘 — 设备数量、在线/离线、最近活动 | Task 5 | | 设备列表 — 网格视图、类型图标、状态指示、快捷切换 | Task 6 | | 设备详情 — 状态展示、控制面板、消息日志 | Task 7 | | Broker 管理 — 客户端列表、活跃主题、健康状态 | Task 8 | | WebSocket 实时推送 | Task 3 + Task 4 | | 响应式布局 | Task 1 (Tailwind responsive classes) | | 控制面板按类型自适应 | Task 7 (DeviceControl.vue) | | 添加设备弹窗 | Task 6 (AddDeviceModal.vue) |