# 监控仪表盘 — 实现计划

> **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秒轮询"
```
