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 |
修改 |
Write internal/service/system/sys_dashboard.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
}
Write internal/api/v1/system/sys_dashboard.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)
}
Write internal/router/system/sys_dashboard.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)
}
}
Edit internal/router/enter.go, add DashboardRouter to RouterGroup:
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:
systemRouter.InitDashboardRouter(PrivateGroup) // add this line
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');"
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"
Edit frontend/src/api/dashboard.js (add to existing or create):
import service from '@/utils/request'
export const getMonitor = () => {
return service({ url: '/dashboard/monitor', method: 'get' })
}
Write frontend/src/view/dashboard/index.vue (replace existing):
<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>
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秒轮询"