|
@@ -0,0 +1,491 @@
|
|
|
|
|
+# 交接班 + 收入报表 — 实现计划
|
|
|
|
|
+
|
|
|
|
|
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans.
|
|
|
|
|
+
|
|
|
|
|
+**Goal:** 交接班(接班/交班/差异统计)+ 收入报表(按日+停车场汇总)
|
|
|
|
|
+
|
|
|
|
|
+**Architecture:** 新增两个独立模块 `modules/shift/` 和 `modules/report/`,共享 payment_record 数据。
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 文件结构
|
|
|
|
|
+
|
|
|
|
|
+| 文件 | 操作 |
|
|
|
|
|
+|------|------|
|
|
|
|
|
+| `internal/dao/shift_record.go` | 新建 |
|
|
|
|
|
+| `internal/modules/shift/api.go` | 新建 |
|
|
|
|
|
+| `internal/modules/shift/service/service.go` | 新建 |
|
|
|
|
|
+| `internal/modules/shift/repository/repo.go` | 新建 |
|
|
|
|
|
+| `internal/modules/report/api.go` | 新建 |
|
|
|
|
|
+| `internal/modules/report/service/service.go` | 新建 |
|
|
|
|
|
+| `internal/initialize/gorm.go` | 修改 |
|
|
|
|
|
+| `internal/initialize/router.go` | 修改 |
|
|
|
|
|
+| `internal/initialize/seed.go` | 修改 |
|
|
|
|
|
+| `frontend/src/view/parking/shiftRecord.vue` | 新建 |
|
|
|
|
|
+| `frontend/src/view/report/revenue.vue` | 新建 |
|
|
|
|
|
+| `frontend/src/api/shift.js` | 新建 |
|
|
|
|
|
+| `frontend/src/api/report.js` | 新建 |
|
|
|
|
|
+| `frontend/src/lang/zh-CN.js` | 修改 |
|
|
|
|
|
+| `frontend/src/lang/en.js` | 修改 |
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### Task 1: 交接班——后端
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: DAO**
|
|
|
|
|
+
|
|
|
|
|
+Write `internal/dao/shift_record.go`:
|
|
|
|
|
+
|
|
|
|
|
+```go
|
|
|
|
|
+package dao
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "time"
|
|
|
|
|
+ "wails-app/internal/global"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+type ShiftRecord struct {
|
|
|
|
|
+ global.GVA_MODEL
|
|
|
|
|
+ OperatorID uint `gorm:"index" json:"operator_id"`
|
|
|
|
|
+ StartTime time.Time `json:"start_time"`
|
|
|
|
|
+ EndTime *time.Time `json:"end_time"`
|
|
|
|
|
+ StartingCash float64 `json:"starting_cash"`
|
|
|
|
|
+ CollectedCash float64 `json:"collected_cash"`
|
|
|
|
|
+ ExpectedTotal float64 `json:"expected_total"`
|
|
|
|
|
+ ActualTotal float64 `json:"actual_total"`
|
|
|
|
|
+ Difference float64 `json:"difference"`
|
|
|
|
|
+ Status string `gorm:"size:20;default:active" json:"status"`
|
|
|
|
|
+ Remark string `gorm:"size:200" json:"remark"`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (ShiftRecord) TableName() string { return "shift_record" }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Add to `internal/initialize/gorm.go` AutoMigrate (after `dao.DigitalTicket{}`):
|
|
|
|
|
+
|
|
|
|
|
+```go
|
|
|
|
|
+dao.DigitalTicket{},
|
|
|
|
|
+dao.ShiftRecord{},
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: Repository + Service**
|
|
|
|
|
+
|
|
|
|
|
+Write `internal/modules/shift/repository/repo.go`:
|
|
|
|
|
+
|
|
|
|
|
+```go
|
|
|
|
|
+package repository
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "wails-app/internal/dao"
|
|
|
|
|
+ "wails-app/internal/global"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+type ShiftRepository struct{}
|
|
|
|
|
+
|
|
|
|
|
+func (r *ShiftRepository) Create(record *dao.ShiftRecord) error { return global.GVA_DB.Create(record).Error }
|
|
|
|
|
+func (r *ShiftRepository) Update(record *dao.ShiftRecord) error { return global.GVA_DB.Save(record).Error }
|
|
|
|
|
+
|
|
|
|
|
+func (r *ShiftRepository) GetActive(operatorID uint) (*dao.ShiftRecord, error) {
|
|
|
|
|
+ var s dao.ShiftRecord
|
|
|
|
|
+ err := global.GVA_DB.Where("operator_id = ? AND status = ?", operatorID, "active").First(&s).Error
|
|
|
|
|
+ if err != nil { return nil, err }
|
|
|
|
|
+ return &s, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type ShiftListQuery struct { Page int `form:"page"`; PageSize int `form:"page_size"` }
|
|
|
|
|
+
|
|
|
|
|
+func (r *ShiftRepository) List(q ShiftListQuery, operatorID uint) ([]dao.ShiftRecord, int64, error) {
|
|
|
|
|
+ db := global.GVA_DB.Model(&dao.ShiftRecord{}).Where("operator_id = ?", operatorID)
|
|
|
|
|
+ var total int64; db.Count(&total)
|
|
|
|
|
+ if q.Page <= 0 { q.Page = 1 }; if q.PageSize <= 0 { q.PageSize = 10 }
|
|
|
|
|
+ var list []dao.ShiftRecord
|
|
|
|
|
+ err := db.Order("id DESC").Offset((q.Page-1)*q.PageSize).Limit(q.PageSize).Find(&list).Error
|
|
|
|
|
+ return list, total, err
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Write `internal/modules/shift/service/service.go`:
|
|
|
|
|
+
|
|
|
|
|
+```go
|
|
|
|
|
+package service
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "errors"
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ "time"
|
|
|
|
|
+ "wails-app/internal/dao"
|
|
|
|
|
+ "wails-app/internal/global"
|
|
|
|
|
+ "wails-app/internal/modules/shift/repository"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+type ShiftService struct { repo *repository.ShiftRepository }
|
|
|
|
|
+func NewShiftService() *ShiftService { return &ShiftService{repo: &repository.ShiftRepository{}} }
|
|
|
|
|
+
|
|
|
|
|
+// Start 接班
|
|
|
|
|
+func (s *ShiftService) Start(operatorID uint, startingCash float64) (*dao.ShiftRecord, error) {
|
|
|
|
|
+ active, _ := s.repo.GetActive(operatorID)
|
|
|
|
|
+ if active != nil { return nil, errors.New("已有当班记录,请先交班") }
|
|
|
|
|
+ rec := &dao.ShiftRecord{OperatorID: operatorID, StartTime: time.Now(), StartingCash: startingCash, Status: "active"}
|
|
|
|
|
+ if err := s.repo.Create(rec); err != nil { return nil, err }
|
|
|
|
|
+ return rec, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// End 交班
|
|
|
|
|
+func (s *ShiftService) End(operatorID uint, actualCash float64, remark string) (*dao.ShiftRecord, error) {
|
|
|
|
|
+ active, err := s.repo.GetActive(operatorID)
|
|
|
|
|
+ if err != nil { return nil, errors.New("无当班记录") }
|
|
|
|
|
+
|
|
|
|
|
+ // 统计当班现金收款
|
|
|
|
|
+ var collected float64
|
|
|
|
|
+ global.GVA_DB.Raw("SELECT COALESCE(SUM(amount),0) FROM payment_record WHERE payment_method='cash' AND paid_at >= ? AND paid_at < ?", active.StartTime, time.Now()).Scan(&collected)
|
|
|
|
|
+
|
|
|
|
|
+ now := time.Now()
|
|
|
|
|
+ expected := active.StartingCash + collected
|
|
|
|
|
+ diff := actualCash - expected
|
|
|
|
|
+ active.EndTime = &now
|
|
|
|
|
+ active.CollectedCash = collected
|
|
|
|
|
+ active.ExpectedTotal = expected
|
|
|
|
|
+ active.ActualTotal = actualCash
|
|
|
|
|
+ active.Difference = diff
|
|
|
|
|
+ active.Status = "closed"
|
|
|
|
|
+ active.Remark = remark
|
|
|
|
|
+ if err := s.repo.Update(active); err != nil { return nil, err }
|
|
|
|
|
+ fmt.Printf("交班完成: 接班%.2f + 当班%.2f = 应交%.2f, 实交%.2f, 差异%.2f\n", active.StartingCash, collected, expected, actualCash, diff)
|
|
|
|
|
+ return active, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (s *ShiftService) GetCurrent(operatorID uint) (*dao.ShiftRecord, error) { return s.repo.GetActive(operatorID) }
|
|
|
|
|
+func (s *ShiftService) List(q repository.ShiftListQuery, operatorID uint) ([]dao.ShiftRecord, int64, error) { return s.repo.List(q, operatorID) }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: API + 路由注册**
|
|
|
|
|
+
|
|
|
|
|
+Write `internal/modules/shift/api.go`:
|
|
|
|
|
+
|
|
|
|
|
+```go
|
|
|
|
|
+package shift
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "github.com/gin-gonic/gin"
|
|
|
|
|
+ "wails-app/internal/model/common/response"
|
|
|
|
|
+ "wails-app/internal/modules/shift/repository"
|
|
|
|
|
+ "wails-app/internal/modules/shift/service"
|
|
|
|
|
+ utils "wails-app/internal/pkg"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+var shiftSvc = service.NewShiftService()
|
|
|
|
|
+
|
|
|
|
|
+type startReq struct { StartingCash float64 `json:"starting_cash"` }
|
|
|
|
|
+type endReq struct { ActualCash float64 `json:"actual_cash"`; Remark string `json:"remark"` }
|
|
|
|
|
+
|
|
|
|
|
+func StartShift(c *gin.Context) {
|
|
|
|
|
+ var req startReq
|
|
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c); return }
|
|
|
|
|
+ rec, err := shiftSvc.Start(utils.GetUserID(c), req.StartingCash)
|
|
|
|
|
+ if err != nil { response.FailWithMessage(err.Error(), c); return }
|
|
|
|
|
+ response.OkWithData(rec, c)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func EndShift(c *gin.Context) {
|
|
|
|
|
+ var req endReq
|
|
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c); return }
|
|
|
|
|
+ rec, err := shiftSvc.End(utils.GetUserID(c), req.ActualCash, req.Remark)
|
|
|
|
|
+ if err != nil { response.FailWithMessage(err.Error(), c); return }
|
|
|
|
|
+ response.OkWithData(rec, c)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func GetCurrentShift(c *gin.Context) {
|
|
|
|
|
+ rec, err := shiftSvc.GetCurrent(utils.GetUserID(c))
|
|
|
|
|
+ if err != nil { response.FailWithDetailed(gin.H{"has_active": false}, "无当班", c); return }
|
|
|
|
|
+ response.OkWithData(rec, c)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func ListShifts(c *gin.Context) {
|
|
|
|
|
+ var q repository.ShiftListQuery
|
|
|
|
|
+ if err := c.ShouldBindQuery(&q); err != nil { response.FailWithMessage(err.Error(), c); return }
|
|
|
|
|
+ list, total, err := shiftSvc.List(q, utils.GetUserID(c))
|
|
|
|
|
+ if err != nil { response.FailWithMessage(err.Error(), c); return }
|
|
|
|
|
+ response.OkWithDetailed(response.PageResult{List: list, Total: total, Page: q.Page, PageSize: q.PageSize}, "查询成功", c)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func SetupShiftRouter(router *gin.RouterGroup) {
|
|
|
|
|
+ sr := router.Group("/shift")
|
|
|
|
|
+ sr.POST("/start", StartShift)
|
|
|
|
|
+ sr.POST("/end", EndShift)
|
|
|
|
|
+ sr.GET("/current", GetCurrentShift)
|
|
|
|
|
+ sr.GET("/list", ListShifts)
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Register in `internal/initialize/router.go` — import `"wails-app/internal/modules/shift"`, add:
|
|
|
|
|
+
|
|
|
|
|
+```go
|
|
|
|
|
+shift.SetupShiftRouter(PrivateGroup)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 4: Casbin + 编译+提交**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+go build -o build/bin/smart-parking.exe .
|
|
|
|
|
+sqlite3 "$APPDATA/smart-parking/lc_garage.db" "INSERT INTO casbin_rule (ptype,v0,v1,v2) VALUES ('p','888','/shift/start','POST'),('p','618','/shift/start','POST'),('p','888','/shift/end','POST'),('p','618','/shift/end','POST'),('p','888','/shift/current','GET'),('p','618','/shift/current','GET'),('p','888','/shift/list','GET'),('p','618','/shift/list','GET');"
|
|
|
|
|
+git add internal/dao/shift_record.go internal/modules/shift/ internal/initialize/
|
|
|
|
|
+git commit -m "feat: 交接班模块——接班/交班/当班统计/差异计算"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### Task 2: 收入报表——后端
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: Service + API**
|
|
|
|
|
+
|
|
|
|
|
+Write `internal/modules/report/service/service.go`:
|
|
|
|
|
+
|
|
|
|
|
+```go
|
|
|
|
|
+package service
|
|
|
|
|
+
|
|
|
|
|
+import "wails-app/internal/global"
|
|
|
|
|
+
|
|
|
|
|
+type RevenueItem struct {
|
|
|
|
|
+ Date string `json:"date"`
|
|
|
|
|
+ ParkingLot string `json:"parking_lot"`
|
|
|
|
|
+ Cash float64 `json:"cash"`
|
|
|
|
|
+ Free float64 `json:"free"`
|
|
|
|
|
+ Total float64 `json:"total"`
|
|
|
|
|
+ Count int64 `json:"count"`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type ReportService struct{}
|
|
|
|
|
+
|
|
|
|
|
+func (s *ReportService) RevenueReport(startDate, endDate string, lotID uint) ([]RevenueItem, error) {
|
|
|
|
|
+ var items []RevenueItem
|
|
|
|
|
+ db := global.GVA_DB.Table("payment_record p").
|
|
|
|
|
+ Select(`DATE(p.paid_at) as date, COALESCE(pr.lot_name,'-') as parking_lot,
|
|
|
|
|
+ COALESCE(SUM(CASE WHEN p.payment_method='cash' THEN p.amount ELSE 0 END),0) as cash,
|
|
|
|
|
+ COALESCE(SUM(CASE WHEN p.payment_method='free' THEN p.amount ELSE 0 END),0) as free,
|
|
|
|
|
+ COALESCE(SUM(p.amount),0) as total, COUNT(*) as count`).
|
|
|
|
|
+ Joins("LEFT JOIN vehicle_record vr ON vr.id = p.record_id").
|
|
|
|
|
+ Joins("LEFT JOIN parking_lot pr ON pr.id = vr.parking_lot_id").
|
|
|
|
|
+ Where("p.paid_at >= ? AND p.paid_at <= ?", startDate, endDate+" 23:59:59")
|
|
|
|
|
+ if lotID > 0 { db = db.Where("vr.parking_lot_id = ?", lotID) }
|
|
|
|
|
+ err := db.Group("DATE(p.paid_at), vr.parking_lot_id").Order("date DESC").Scan(&items).Error
|
|
|
|
|
+ return items, err
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Write `internal/modules/report/api.go`:
|
|
|
|
|
+
|
|
|
|
|
+```go
|
|
|
|
|
+package report
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "strconv"
|
|
|
|
|
+ "github.com/gin-gonic/gin"
|
|
|
|
|
+ "wails-app/internal/model/common/response"
|
|
|
|
|
+ "wails-app/internal/modules/report/service"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+var reportSvc = service.ReportService{}
|
|
|
|
|
+
|
|
|
|
|
+func RevenueReport(c *gin.Context) {
|
|
|
|
|
+ lotID, _ := strconv.Atoi(c.Query("parking_lot_id"))
|
|
|
|
|
+ items, err := reportSvc.RevenueReport(c.Query("start_date"), c.Query("end_date"), uint(lotID))
|
|
|
|
|
+ if err != nil { response.FailWithMessage(err.Error(), c); return }
|
|
|
|
|
+ response.OkWithData(items, c)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func SetupReportRouter(router *gin.RouterGroup) {
|
|
|
|
|
+ router.GET("/report/revenue", RevenueReport)
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Register in `internal/initialize/router.go` — import `"wails-app/internal/modules/report"`, add:
|
|
|
|
|
+
|
|
|
|
|
+```go
|
|
|
|
|
+report.SetupReportRouter(PrivateGroup)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: Casbin + 编译+提交**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+go build -o build/bin/smart-parking.exe .
|
|
|
|
|
+sqlite3 "$APPDATA/smart-parking/lc_garage.db" "INSERT INTO casbin_rule (ptype,v0,v1,v2) VALUES ('p','888','/report/revenue','GET'),('p','618','/report/revenue','GET');"
|
|
|
|
|
+git add internal/modules/report/ internal/initialize/router.go
|
|
|
|
|
+git commit -m "feat: 收入报表——按日+停车场汇总payment_record"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### Task 3: 前端
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: 交接班页面**
|
|
|
|
|
+
|
|
|
|
|
+Write `frontend/src/view/parking/shiftRecord.vue`:
|
|
|
|
|
+
|
|
|
|
|
+```vue
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <el-card v-if="current" class="active-shift">
|
|
|
|
|
+ <template #header>当班中</template>
|
|
|
|
|
+ <el-descriptions :column="2" border>
|
|
|
|
|
+ <el-descriptions-item label="接班时间">{{ current.start_time }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="接班现金">¥{{ current.starting_cash.toFixed(2) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="当班收款">¥{{ current.collected_cash.toFixed(2) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="已收笔数">--</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="应交总额"><b>¥{{ (current.starting_cash + current.collected_cash).toFixed(2) }}</b></el-descriptions-item>
|
|
|
|
|
+ </el-descriptions>
|
|
|
|
|
+ <div style="margin-top:16px">
|
|
|
|
|
+ <el-input-number v-model="endForm.actual_cash" :min="0" :precision="2" placeholder="实交现金" style="width:200px" />
|
|
|
|
|
+ <el-button type="danger" style="margin-left:12px" @click="doEnd">交班</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+
|
|
|
|
|
+ <el-card v-else>
|
|
|
|
|
+ <el-form inline>
|
|
|
|
|
+ <el-form-item label="接班现金">
|
|
|
|
|
+ <el-input-number v-model="startForm.starting_cash" :min="0" :precision="2" style="width:200px" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item><el-button type="primary" @click="doStart">接班</el-button></el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+
|
|
|
|
|
+ <el-table :data="list" border style="margin-top:16px">
|
|
|
|
|
+ <el-table-column label="接班时间" prop="start_time" width="170" />
|
|
|
|
|
+ <el-table-column label="交班时间" prop="end_time" width="170" />
|
|
|
|
|
+ <el-table-column label="接班现金" width="100"><template #default="{row}">¥{{ row.starting_cash.toFixed(2) }}</template></el-table-column>
|
|
|
|
|
+ <el-table-column label="当班收款" width="100"><template #default="{row}">¥{{ row.collected_cash.toFixed(2) }}</template></el-table-column>
|
|
|
|
|
+ <el-table-column label="应交" width="100"><template #default="{row}">¥{{ row.expected_total.toFixed(2) }}</template></el-table-column>
|
|
|
|
|
+ <el-table-column label="实交" width="100"><template #default="{row}">¥{{ row.actual_total.toFixed(2) }}</template></el-table-column>
|
|
|
|
|
+ <el-table-column label="差异" width="100"><template #default="{row}"><span :style="{color:row.difference<0?'#f56c6c':'#67c23a'}">¥{{ row.difference.toFixed(2) }}</span></template></el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { ref, reactive, onMounted } from 'vue'
|
|
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
|
|
+import { startShift, endShift, getCurrentShift, getShiftList } from '@/api/shift'
|
|
|
|
|
+
|
|
|
|
|
+const current = ref(null), list = ref([])
|
|
|
|
|
+const startForm = reactive({ starting_cash: 0 })
|
|
|
|
|
+const endForm = reactive({ actual_cash: 0 })
|
|
|
|
|
+
|
|
|
|
|
+async function fetch() {
|
|
|
|
|
+ const r = await getCurrentShift(); if (r.code === 0) current.value = r.data; else current.value = null
|
|
|
|
|
+ const l = await getShiftList({ page: 1, page_size: 20 }); if (l.code === 0) { list.value = l.data.list || []; list.value.forEach(s => s.collected_cash = s.collected_cash || 0) }
|
|
|
|
|
+}
|
|
|
|
|
+async function doStart() { const r = await startShift(startForm); if (r.code === 0) { ElMessage.success('接班成功'); fetch() } else ElMessage.error(r.msg) }
|
|
|
|
|
+async function doEnd() { const r = await endShift(endForm); if (r.code === 0) { ElMessage.success(`交班完成,差异¥${r.data.difference.toFixed(2)}`); fetch() } else ElMessage.error(r.msg) }
|
|
|
|
|
+
|
|
|
|
|
+onMounted(fetch)
|
|
|
|
|
+</script>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Write `frontend/src/api/shift.js`:
|
|
|
|
|
+
|
|
|
|
|
+```js
|
|
|
|
|
+import service from '@/utils/request'
|
|
|
|
|
+export const startShift = (data) => service({ url: '/shift/start', method: 'post', data })
|
|
|
|
|
+export const endShift = (data) => service({ url: '/shift/end', method: 'post', data })
|
|
|
|
|
+export const getCurrentShift = () => service({ url: '/shift/current', method: 'get' })
|
|
|
|
|
+export const getShiftList = (params) => service({ url: '/shift/list', method: 'get', params })
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: 收入报表页面**
|
|
|
|
|
+
|
|
|
|
|
+Write `frontend/src/view/report/revenue.vue`:
|
|
|
|
|
+
|
|
|
|
|
+```vue
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <el-form inline>
|
|
|
|
|
+ <el-form-item label="日期">
|
|
|
|
|
+ <el-date-picker v-model="dateRange" type="daterange" start-placeholder="开始" end-placeholder="结束" value-format="YYYY-MM-DD" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="停车场">
|
|
|
|
|
+ <el-select v-model="lotId" clearable placeholder="全部" style="width:180px">
|
|
|
|
|
+ <el-option v-for="l in lotList" :key="l.ID" :label="l.lot_name" :value="l.ID" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item>
|
|
|
|
|
+ <el-button type="primary" @click="fetch">{{ $t.value.Search }}</el-button>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ <el-table :data="list" border show-summary :summary-method="summaries">
|
|
|
|
|
+ <el-table-column label="日期" prop="date" width="120" />
|
|
|
|
|
+ <el-table-column label="停车场" prop="parking_lot" width="140" />
|
|
|
|
|
+ <el-table-column label="现金" width="100"><template #default="{row}">¥{{ row.cash.toFixed(2) }}</template></el-table-column>
|
|
|
|
|
+ <el-table-column label="免密" width="100"><template #default="{row}">¥{{ row.free.toFixed(2) }}</template></el-table-column>
|
|
|
|
|
+ <el-table-column label="总收入" width="120"><template #default="{row}">¥{{ row.total.toFixed(2) }}</template></el-table-column>
|
|
|
|
|
+ <el-table-column label="笔数" prop="count" width="80" />
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { ref, reactive, onMounted } from 'vue'
|
|
|
|
|
+import { getRevenueReport } from '@/api/report'
|
|
|
|
|
+import { obtainPullOverList } from '@/api/pullOver'
|
|
|
|
|
+
|
|
|
|
|
+const dateRange = ref([]), lotId = ref(null), list = ref([]), lotList = ref([])
|
|
|
|
|
+
|
|
|
|
|
+function summaries({ columns, data }) {
|
|
|
|
|
+ const sums = ['合计', '', 0, 0, 0, 0]
|
|
|
|
|
+ data.forEach(r => { sums[2] += r.cash; sums[3] += r.free; sums[4] += r.total; sums[5] += r.count })
|
|
|
|
|
+ return columns.map((c, i) => i >= 2 ? (i === 5 ? sums[i] : '¥' + sums[i].toFixed(2)) : sums[i])
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function fetch() {
|
|
|
|
|
+ const [s, e] = dateRange.value && dateRange.value.length === 2 ? dateRange.value : ['', '']
|
|
|
|
|
+ const r = await getRevenueReport({ start_date: s, end_date: e, parking_lot_id: lotId.value || '' })
|
|
|
|
|
+ if (r.code === 0) list.value = r.data || []
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+onMounted(async () => {
|
|
|
|
|
+ const today = new Date().toISOString().slice(0, 10)
|
|
|
|
|
+ dateRange.value = [today, today]
|
|
|
|
|
+ const l = await obtainPullOverList({ page: 1, pageSize: 100 })
|
|
|
|
|
+ if (l.code === 0) lotList.value = l.data?.list || []
|
|
|
|
|
+ fetch()
|
|
|
|
|
+})
|
|
|
|
|
+</script>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Write `frontend/src/api/report.js`:
|
|
|
|
|
+
|
|
|
|
|
+```js
|
|
|
|
|
+import service from '@/utils/request'
|
|
|
|
|
+export const getRevenueReport = (params) => service({ url: '/report/revenue', method: 'get', params })
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: 菜单 + 翻译**
|
|
|
|
|
+
|
|
|
|
|
+DB inserts:
|
|
|
|
|
+```bash
|
|
|
|
|
+# 交接班: ParentId=20, Sort=7
|
|
|
|
|
+sqlite3 "$APPDATA/smart-parking/lc_garage.db" "INSERT INTO sys_base_menus (menu_level,parent_id,path,name,hidden,component,sort,title,icon,created_at,updated_at) VALUES (0,20,'shiftRecord','shiftRecord',0,'view/parking/shiftRecord.vue',7,'交接班管理','clock',datetime('now'),datetime('now')); INSERT INTO sys_authority_menus VALUES ('618',34),('888',34),('9527',34);"
|
|
|
|
|
+
|
|
|
|
|
+# 收入报表: ParentId=25, Sort=3
|
|
|
|
|
+sqlite3 "$APPDATA/smart-parking/lc_garage.db" "INSERT INTO sys_base_menus (menu_level,parent_id,path,name,hidden,component,sort,title,icon,created_at,updated_at) VALUES (0,25,'revenue','revenue',0,'view/report/revenue.vue',3,'收入报表','money',datetime('now'),datetime('now')); INSERT INTO sys_authority_menus VALUES ('618',35),('888',35),('9527',35);"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Seed.go: add menu entries + authority_menu + Casbin rules.
|
|
|
|
|
+
|
|
|
|
|
+zh-CN.js:
|
|
|
|
|
+```js
|
|
|
|
|
+shiftRecord: '交接班管理',
|
|
|
|
|
+revenueReport: '收入报表',
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+en.js:
|
|
|
|
|
+```js
|
|
|
|
|
+shiftRecord: 'Shift Record',
|
|
|
|
|
+revenueReport: 'Revenue Report',
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 4: 编译+提交**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+cd frontend && npm run build && cd ..
|
|
|
|
|
+git add frontend/src/ internal/initialize/seed.go
|
|
|
|
|
+git commit -m "feat: 交接班前端+收入报表+菜单+多语言"
|
|
|
|
|
+```
|