فهرست منبع

docs: 监控仪表盘实现计划

lq 1 ماه پیش
والد
کامیت
6eee627ee5
1فایلهای تغییر یافته به همراه266 افزوده شده و 0 حذف شده
  1. 266 0
      docs/superpowers/plans/2026-07-20-monitor-dashboard-plan.md

+ 266 - 0
docs/superpowers/plans/2026-07-20-monitor-dashboard-plan.md

@@ -0,0 +1,266 @@
+# 监控仪表盘 — 实现计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans.
+
+**Goal:** 升级仪表盘为停车监控大屏,4统计卡片+设备状态,10秒轮询
+
+**Architecture:** 后端 `GET /dashboard/monitor`(system 模块,SQL 聚合)。前端替换 `dashboard/index.vue`,`el-statistic` + `setInterval`。
+
+---
+
+## 文件结构
+
+| 文件 | 操作 |
+|------|------|
+| `internal/service/system/sys_dashboard.go` | 新建 |
+| `internal/api/v1/system/sys_dashboard.go` | 新建 |
+| `internal/router/system/sys_dashboard.go` | 新建 |
+| `internal/router/enter.go` | 修改 |
+| `internal/initialize/router.go` | 修改 |
+| `frontend/src/view/dashboard/index.vue` | 修改 |
+| `frontend/src/api/dashboard.js` | 修改 |
+
+---
+
+### Task 1: 后端 API
+
+- [ ] **Step 1: Service**
+
+Write `internal/service/system/sys_dashboard.go`:
+
+```go
+package system
+
+import (
+	"time"
+	"wails-app/internal/global"
+)
+
+type DeviceStatus struct {
+	ChannelName  string `json:"channel_name"`
+	Direction    string `json:"direction"`
+	DeviceName   string `json:"device_name"`
+	DeviceStatus string `json:"device_status"`
+}
+
+type MonitorData struct {
+	TotalSpaces int64          `json:"total_spaces"`
+	UsedSpaces  int64          `json:"used_spaces"`
+	InPark      int64          `json:"in_park"`
+	TodayIncome float64        `json:"today_income"`
+	TodayEntry  int64          `json:"today_entry"`
+	TodayExit   int64          `json:"today_exit"`
+	Devices     []DeviceStatus `json:"devices"`
+}
+
+type DashboardService struct{}
+
+func (s *DashboardService) GetMonitorData() (MonitorData, error) {
+	var d MonitorData
+	today := time.Now().Format("2006-01-02")
+	global.GVA_DB.Raw("SELECT COALESCE(SUM(capacity), 0) FROM parking_lot").Scan(&d.TotalSpaces)
+	global.GVA_DB.Raw("SELECT COUNT(*) FROM vehicle_record WHERE exit_time IS NULL").Scan(&d.InPark)
+	d.UsedSpaces = d.InPark
+	global.GVA_DB.Raw("SELECT COALESCE(SUM(amount), 0) FROM payment_record WHERE paid_at >= ? AND paid_at < ?", today, today+" 23:59:59").Scan(&d.TodayIncome)
+	global.GVA_DB.Raw("SELECT COUNT(*) FROM vehicle_record WHERE entry_time >= ? AND entry_time < ?", today, today+" 23:59:59").Scan(&d.TodayEntry)
+	global.GVA_DB.Raw("SELECT COUNT(*) FROM vehicle_record WHERE exit_time >= ? AND exit_time < ?", today, today+" 23:59:59").Scan(&d.TodayExit)
+	global.GVA_DB.Raw(`SELECT ch.channel_name, ch.direction, COALESCE(uhf.device_name, '-') as device_name, COALESCE(uhf.status, 'offline') as device_status FROM channel ch LEFT JOIN uhf_reader uhf ON uhf.channel_id = ch.id WHERE ch.deleted_at IS NULL ORDER BY ch.id`).Scan(&d.Devices)
+	return d, nil
+}
+```
+
+- [ ] **Step 2: Handler**
+
+Write `internal/api/v1/system/sys_dashboard.go`:
+
+```go
+package system
+
+import (
+	"github.com/gin-gonic/gin"
+	"wails-app/internal/model/common/response"
+	systemService "wails-app/internal/service/system"
+)
+
+var dashboardSvc = systemService.DashboardService{}
+
+func GetMonitor(c *gin.Context) {
+	data, err := dashboardSvc.GetMonitorData()
+	if err != nil {
+		response.FailWithMessage(err.Error(), c)
+		return
+	}
+	response.OkWithData(data, c)
+}
+```
+
+- [ ] **Step 3: Router**
+
+Write `internal/router/system/sys_dashboard.go`:
+
+```go
+package system
+
+import (
+	"wails-app/internal/api/v1/system"
+	"github.com/gin-gonic/gin"
+)
+
+type DashboardRouter struct{}
+
+func (s *DashboardRouter) InitDashboardRouter(Router *gin.RouterGroup) {
+	dashboardRouter := Router.Group("dashboard")
+	{
+		dashboardRouter.GET("monitor", system.GetMonitor)
+	}
+}
+```
+
+- [ ] **Step 4: 注册路由**
+
+Edit `internal/router/enter.go`, add DashboardRouter to RouterGroup:
+
+```go
+type RouterGroup struct {
+	System    system.RouterGroup
+	Example   example.RouterGroup
+	Vehicle   vehicle.RouterGroup
+	Dashboard system.DashboardRouter  // add this line
+}
+```
+
+Edit `internal/initialize/router.go`, register in PrivateGroup block:
+
+```go
+systemRouter.InitDashboardRouter(PrivateGroup)  // add this line
+```
+
+- [ ] **Step 5: Casbin**
+
+```bash
+sqlite3 "$APPDATA/smart-parking/lc_garage.db" "INSERT INTO casbin_rule (ptype, v0, v1, v2) VALUES ('p', '888', '/dashboard/monitor', 'GET'), ('p', '618', '/dashboard/monitor', 'GET');"
+```
+
+- [ ] **Step 6: 编译+提交**
+
+```bash
+go build -o build/bin/smart-parking.exe .
+git add internal/service/system/sys_dashboard.go internal/api/v1/system/sys_dashboard.go internal/router/system/sys_dashboard.go internal/router/enter.go internal/initialize/router.go
+git commit -m "feat: 新增监控仪表盘API——GET /dashboard/monitor"
+```
+
+---
+
+### Task 2: 前端监控大屏
+
+- [ ] **Step 1: API**
+
+Edit `frontend/src/api/dashboard.js` (add to existing or create):
+
+```js
+import service from '@/utils/request'
+
+export const getMonitor = () => {
+  return service({ url: '/dashboard/monitor', method: 'get' })
+}
+```
+
+- [ ] **Step 2: 页面**
+
+Write `frontend/src/view/dashboard/index.vue` (replace existing):
+
+```vue
+<template>
+  <div class="monitor-page">
+    <div class="monitor-header">
+      <h2>智慧停车监控</h2>
+      <span class="time">{{ now }}</span>
+    </div>
+
+    <el-row :gutter="16" class="stat-row">
+      <el-col :span="6">
+        <el-card><el-statistic title="车位余量" :value="data.used_spaces + ' / ' + data.total_spaces">
+          <template #suffix><span :style="{ color: data.total_spaces > 0 && data.used_spaces >= data.total_spaces ? '#f56c6c' : '#67c23a' }">车位</span></template>
+        </el-statistic></el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card><el-statistic title="在场车辆" :value="data.in_park">
+          <template #suffix>台</template>
+        </el-statistic></el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card><el-statistic title="今日收入" :value="data.today_income" prefix="¥" :precision="2" /></el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card>
+          <el-statistic title="今日进出" :value="data.today_entry">
+            <template #suffix>/ {{ data.today_exit }}</template>
+          </el-statistic>
+          <span style="font-size:12px;color:#909399">进 / 出</span>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-card class="device-card">
+      <template #header>设备状态</template>
+      <div class="device-list">
+        <div v-for="d in data.devices" :key="d.channel_name" class="device-item">
+          <el-tag :type="d.device_status === 'online' ? 'success' : 'danger'" size="small">{{ d.device_status === 'online' ? '在线' : '离线' }}</el-tag>
+          <span>{{ d.channel_name }}</span>
+          <span class="device-name">{{ d.device_name }} ({{ d.direction === 'in' ? '入口' : '出口' }})</span>
+        </div>
+        <el-empty v-if="!data.devices || data.devices.length === 0" description="暂无设备" :image-size="60" />
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onUnmounted } from 'vue'
+import { getMonitor } from '@/api/dashboard'
+
+const data = reactive({ total_spaces: 0, used_spaces: 0, in_park: 0, today_income: 0, today_entry: 0, today_exit: 0, devices: [] })
+const now = ref('')
+let timer = null
+
+function updateTime() {
+  now.value = new Date().toLocaleString('zh-CN')
+}
+
+async function fetchData() {
+  try {
+    const res = await getMonitor()
+    if (res.code === 0) Object.assign(data, res.data)
+  } catch (e) { /* keep last data on error */ }
+}
+
+onMounted(() => {
+  updateTime()
+  fetchData()
+  timer = setInterval(() => { updateTime(); fetchData() }, 10000)
+})
+
+onUnmounted(() => clearInterval(timer))
+</script>
+
+<style scoped>
+.monitor-page { padding: 16px; }
+.monitor-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
+.monitor-header h2 { margin: 0; }
+.time { color: #909399; font-size: 14px; }
+.stat-row { margin-bottom: 16px; }
+.stat-row .el-card { text-align: center; }
+.device-card { margin-top: 16px; }
+.device-list { display: flex; flex-wrap: wrap; gap: 12px; }
+.device-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: #f5f7fa; border-radius: 6px; }
+.device-name { color: #909399; font-size: 13px; }
+</style>
+```
+
+- [ ] **Step 3: 编译+提交**
+
+```bash
+cd frontend && npm run build && cd ..
+git add frontend/src/view/dashboard/index.vue frontend/src/api/dashboard.js
+git commit -m "feat: 升级仪表盘为停车监控大屏——4统计卡片+设备状态+10秒轮询"
+```