|
@@ -0,0 +1,597 @@
|
|
|
|
|
+# 包月车管理 — 实现计划
|
|
|
|
|
+
|
|
|
|
|
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans.
|
|
|
|
|
+
|
|
|
|
|
+**Goal:** 实现包月车(月/季/年卡)办理、续费、退卡。办卡自动写 shortlist 白名单,进出场复用已有检查。
|
|
|
|
|
+
|
|
|
|
|
+**Architecture:** 新增 `internal/modules/monthly/` 模块(DAO/service/repository/api)。办卡时创建 monthly_card + shortlist + payment_record。前端新增包月车管理页面。
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 文件结构
|
|
|
|
|
+
|
|
|
|
|
+| 文件 | 操作 |
|
|
|
|
|
+|------|------|
|
|
|
|
|
+| `internal/dao/monthly_card.go` | 新建 |
|
|
|
|
|
+| `internal/modules/monthly/api.go` | 新建 |
|
|
|
|
|
+| `internal/modules/monthly/service/service.go` | 新建 |
|
|
|
|
|
+| `internal/modules/monthly/repository/repo.go` | 新建 |
|
|
|
|
|
+| `internal/initialize/gorm.go` | 修改 |
|
|
|
|
|
+| `internal/initialize/router.go` | 修改 |
|
|
|
|
|
+| `internal/initialize/seed.go` | 修改 |
|
|
|
|
|
+| `frontend/src/view/parking/monthlyCard.vue` | 新建 |
|
|
|
|
|
+| `frontend/src/api/monthlyCard.js` | 新建 |
|
|
|
|
|
+| `frontend/src/lang/zh-CN.js` | 修改 |
|
|
|
|
|
+| `frontend/src/lang/en.js` | 修改 |
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### Task 1: 数据层 + 后端核心
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: 创建 DAO**
|
|
|
|
|
+
|
|
|
|
|
+Write `internal/dao/monthly_card.go`:
|
|
|
|
|
+
|
|
|
|
|
+```go
|
|
|
|
|
+package dao
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "time"
|
|
|
|
|
+ "wails-app/internal/global"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+type MonthlyCard struct {
|
|
|
|
|
+ global.GVA_MODEL
|
|
|
|
|
+ VehicleID uint `gorm:"index" json:"vehicle_id"`
|
|
|
|
|
+ Vehicle *Vehicle `gorm:"foreignKey:VehicleID" json:"vehicle"`
|
|
|
|
|
+ CardType string `gorm:"size:20" json:"card_type"` // month / quarter / year
|
|
|
|
|
+ StartDate time.Time `json:"start_date"`
|
|
|
|
|
+ EndDate time.Time `json:"end_date"`
|
|
|
|
|
+ Fee float64 `json:"fee"`
|
|
|
|
|
+ PaymentStatus string `gorm:"size:20;default:paid" json:"payment_status"` // paid / refunded
|
|
|
|
|
+ OperatorID uint `json:"operator_id"`
|
|
|
|
|
+ Remark string `gorm:"size:200" json:"remark"`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (MonthlyCard) TableName() string { return "monthly_card" }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: 注册 AutoMigrate**
|
|
|
|
|
+
|
|
|
|
|
+Edit `internal/initialize/gorm.go`, add `dao.MonthlyCard{}` to the AutoMigrate list (after `dao.Shortlist{}`):
|
|
|
|
|
+
|
|
|
|
|
+```go
|
|
|
|
|
+dao.Shortlist{},
|
|
|
|
|
+dao.MonthlyCard{},
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: 创建 Repository**
|
|
|
|
|
+
|
|
|
|
|
+Write `internal/modules/monthly/repository/repo.go`:
|
|
|
|
|
+
|
|
|
|
|
+```go
|
|
|
|
|
+package repository
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "wails-app/internal/dao"
|
|
|
|
|
+ "wails-app/internal/global"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+type MonthlyCardRepository struct{}
|
|
|
|
|
+
|
|
|
|
|
+func (r *MonthlyCardRepository) Create(card *dao.MonthlyCard) error {
|
|
|
|
|
+ return global.GVA_DB.Create(card).Error
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (r *MonthlyCardRepository) GetByVehicleID(vehicleID uint) (*dao.MonthlyCard, error) {
|
|
|
|
|
+ var card dao.MonthlyCard
|
|
|
|
|
+ err := global.GVA_DB.Where("vehicle_id = ? AND payment_status = ?", vehicleID, "paid").Preload("Vehicle").First(&card).Error
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ return &card, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (r *MonthlyCardRepository) Update(card *dao.MonthlyCard) error {
|
|
|
|
|
+ return global.GVA_DB.Save(card).Error
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type MonthlyCardQuery struct {
|
|
|
|
|
+ PlateNumber string `form:"plate_number"`
|
|
|
|
|
+ CardType string `form:"card_type"`
|
|
|
|
|
+ Page int `form:"page"`
|
|
|
|
|
+ PageSize int `form:"page_size"`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type MonthlyCardResult struct {
|
|
|
|
|
+ ID uint `json:"id"`
|
|
|
|
|
+ PlateNumber string `json:"plate_number"`
|
|
|
|
|
+ CardType string `json:"card_type"`
|
|
|
|
|
+ StartDate string `json:"start_date"`
|
|
|
|
|
+ EndDate string `json:"end_date"`
|
|
|
|
|
+ Fee float64 `json:"fee"`
|
|
|
|
|
+ PaymentStatus string `json:"payment_status"`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (r *MonthlyCardRepository) List(q MonthlyCardQuery) ([]MonthlyCardResult, int64, error) {
|
|
|
|
|
+ db := global.GVA_DB.Table("monthly_card").
|
|
|
|
|
+ Select("monthly_card.id, vehicle.plate_number, monthly_card.card_type, monthly_card.start_date, monthly_card.end_date, monthly_card.fee, monthly_card.payment_status").
|
|
|
|
|
+ Joins("LEFT JOIN vehicle ON vehicle.id = monthly_card.vehicle_id")
|
|
|
|
|
+
|
|
|
|
|
+ if q.PlateNumber != "" {
|
|
|
|
|
+ db = db.Where("vehicle.plate_number LIKE ?", "%"+q.PlateNumber+"%")
|
|
|
|
|
+ }
|
|
|
|
|
+ if q.CardType != "" {
|
|
|
|
|
+ db = db.Where("monthly_card.card_type = ?", q.CardType)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var total int64
|
|
|
|
|
+ db.Count(&total)
|
|
|
|
|
+
|
|
|
|
|
+ if q.Page <= 0 { q.Page = 1 }
|
|
|
|
|
+ if q.PageSize <= 0 { q.PageSize = 10 }
|
|
|
|
|
+ offset := (q.Page - 1) * q.PageSize
|
|
|
|
|
+
|
|
|
|
|
+ var results []MonthlyCardResult
|
|
|
|
|
+ err := db.Order("monthly_card.id DESC").Offset(offset).Limit(q.PageSize).Scan(&results).Error
|
|
|
|
|
+ return results, total, err
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 4: 创建 Service**
|
|
|
|
|
+
|
|
|
|
|
+Write `internal/modules/monthly/service/service.go`:
|
|
|
|
|
+
|
|
|
|
|
+```go
|
|
|
|
|
+package service
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "errors"
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ "time"
|
|
|
|
|
+ "wails-app/internal/dao"
|
|
|
|
|
+ "wails-app/internal/global"
|
|
|
|
|
+ "wails-app/internal/modules/monthly/repository"
|
|
|
|
|
+ paymentSvc "wails-app/internal/modules/payment/service"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+type MonthlyCardService struct {
|
|
|
|
|
+ repo *repository.MonthlyCardRepository
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func NewMonthlyCardService() *MonthlyCardService {
|
|
|
|
|
+ return &MonthlyCardService{repo: &repository.MonthlyCardRepository{}}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func cardDays(cardType string) int {
|
|
|
|
|
+ switch cardType {
|
|
|
|
|
+ case "month": return 30
|
|
|
|
|
+ case "quarter": return 90
|
|
|
|
|
+ case "year": return 365
|
|
|
|
|
+ default: return 30
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Create 办理月卡:创建 monthly_card + shortlist 白名单 + payment_record
|
|
|
|
|
+func (s *MonthlyCardService) Create(vehicleID uint, cardType string, fee float64, operatorID uint, remark string) (*dao.MonthlyCard, error) {
|
|
|
|
|
+ // 检查是否已有有效月卡
|
|
|
|
|
+ existing, _ := s.repo.GetByVehicleID(vehicleID)
|
|
|
|
|
+ if existing != nil {
|
|
|
|
|
+ return nil, errors.New("该车辆已有有效月卡,请先退卡或等过期后续费")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var vehicle dao.Vehicle
|
|
|
|
|
+ if err := global.GVA_DB.First(&vehicle, vehicleID).Error; err != nil {
|
|
|
|
|
+ return nil, errors.New("车辆不存在")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ now := time.Now()
|
|
|
|
|
+ startDate := now
|
|
|
|
|
+ endDate := now.AddDate(0, 0, cardDays(cardType))
|
|
|
|
|
+
|
|
|
|
|
+ card := &dao.MonthlyCard{
|
|
|
|
|
+ VehicleID: vehicleID, CardType: cardType,
|
|
|
|
|
+ StartDate: startDate, EndDate: endDate,
|
|
|
|
|
+ Fee: fee, PaymentStatus: "paid", OperatorID: operatorID, Remark: remark,
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := s.repo.Create(card); err != nil {
|
|
|
|
|
+ return nil, fmt.Errorf("创建月卡失败: %w", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 写入白名单(shortlist),到期日与月卡一致
|
|
|
|
|
+ shortlist := dao.Shortlist{
|
|
|
|
|
+ VehicleId: int(vehicleID),
|
|
|
|
|
+ ListType: "白名单",
|
|
|
|
|
+ ExpirationTime: &endDate,
|
|
|
|
|
+ }
|
|
|
|
|
+ global.GVA_DB.Create(&shortlist)
|
|
|
|
|
+
|
|
|
|
|
+ // 创建 payment_record
|
|
|
|
|
+ payRecord := dao.PaymentRecord{
|
|
|
|
|
+ RecordID: 0, PaymentMethod: paymentSvc.PaymentCash,
|
|
|
|
|
+ Amount: fee, PaidAmount: fee, ChangeAmount: 0,
|
|
|
|
|
+ OperatorID: operatorID, PaidAt: now,
|
|
|
|
|
+ Remark: fmt.Sprintf("包月卡办理(%s)", cardType),
|
|
|
|
|
+ }
|
|
|
|
|
+ global.GVA_DB.Create(&payRecord)
|
|
|
|
|
+
|
|
|
|
|
+ return card, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Renew 续费:延长到期日 + 更新 shortlist
|
|
|
|
|
+func (s *MonthlyCardService) Renew(cardID uint, cardType string, fee float64, operatorID uint) error {
|
|
|
|
|
+ var card dao.MonthlyCard
|
|
|
|
|
+ if err := global.GVA_DB.First(&card, cardID).Error; err != nil {
|
|
|
|
|
+ return errors.New("月卡记录不存在")
|
|
|
|
|
+ }
|
|
|
|
|
+ if card.PaymentStatus != "paid" {
|
|
|
|
|
+ return errors.New("月卡已退卡,无法续费")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ newEnd := card.EndDate.AddDate(0, 0, cardDays(cardType))
|
|
|
|
|
+ card.EndDate = newEnd
|
|
|
|
|
+ card.Fee += fee
|
|
|
|
|
+ global.GVA_DB.Save(&card)
|
|
|
|
|
+
|
|
|
|
|
+ // 更新 shortlist 到期日
|
|
|
|
|
+ global.GVA_DB.Model(&dao.Shortlist{}).Where("vehicle_id = ? AND list_type = ?", card.VehicleID, "白名单").
|
|
|
|
|
+ Update("expiration_time", newEnd)
|
|
|
|
|
+
|
|
|
|
|
+ return nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Refund 退卡:标记 refunded + 移除 shortlist
|
|
|
|
|
+func (s *MonthlyCardService) Refund(cardID uint) error {
|
|
|
|
|
+ var card dao.MonthlyCard
|
|
|
|
|
+ if err := global.GVA_DB.First(&card, cardID).Error; err != nil {
|
|
|
|
|
+ return errors.New("月卡记录不存在")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ card.PaymentStatus = "refunded"
|
|
|
|
|
+ global.GVA_DB.Save(&card)
|
|
|
|
|
+
|
|
|
|
|
+ // 移除白名单
|
|
|
|
|
+ global.GVA_DB.Where("vehicle_id = ? AND list_type = ?", card.VehicleID, "白名单").Unscoped().Delete(&dao.Shortlist{})
|
|
|
|
|
+
|
|
|
|
|
+ return nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (s *MonthlyCardService) List(q repository.MonthlyCardQuery) ([]repository.MonthlyCardResult, int64, error) {
|
|
|
|
|
+ return s.repo.List(q)
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 5: 构建 + 提交**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+mkdir -p internal/modules/monthly/service internal/modules/monthly/repository
|
|
|
|
|
+go build -o build/bin/smart-parking.exe .
|
|
|
|
|
+git add internal/dao/monthly_card.go internal/modules/monthly/ internal/initialize/gorm.go
|
|
|
|
|
+git commit -m "feat: 包月车管理后端——DAO/service/repository,办卡=白名单+支付记录"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### Task 2: API Handler + 路由注册
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: 创建 api.go**
|
|
|
|
|
+
|
|
|
|
|
+Write `internal/modules/monthly/api.go`:
|
|
|
|
|
+
|
|
|
|
|
+```go
|
|
|
|
|
+package monthly
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "strconv"
|
|
|
|
|
+ "github.com/gin-gonic/gin"
|
|
|
|
|
+ "wails-app/internal/model/common/response"
|
|
|
|
|
+ "wails-app/internal/modules/monthly/repository"
|
|
|
|
|
+ "wails-app/internal/modules/monthly/service"
|
|
|
|
|
+ utils "wails-app/internal/pkg"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+var monthlySvc = service.NewMonthlyCardService()
|
|
|
|
|
+
|
|
|
|
|
+type createReq struct {
|
|
|
|
|
+ VehicleID uint `json:"vehicle_id"`
|
|
|
|
|
+ CardType string `json:"card_type"`
|
|
|
|
|
+ Fee float64 `json:"fee"`
|
|
|
|
|
+ Remark string `json:"remark"`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func CreateCard(c *gin.Context) {
|
|
|
|
|
+ var req createReq
|
|
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
+ response.FailWithMessage(err.Error(), c)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ card, err := monthlySvc.Create(req.VehicleID, req.CardType, req.Fee, utils.GetUserID(c), req.Remark)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ response.FailWithMessage(err.Error(), c)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ response.OkWithData(card, c)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type renewReq struct {
|
|
|
|
|
+ CardID uint `json:"card_id"`
|
|
|
|
|
+ CardType string `json:"card_type"`
|
|
|
|
|
+ Fee float64 `json:"fee"`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func RenewCard(c *gin.Context) {
|
|
|
|
|
+ var req renewReq
|
|
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
+ response.FailWithMessage(err.Error(), c)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := monthlySvc.Renew(req.CardID, req.CardType, req.Fee, utils.GetUserID(c)); err != nil {
|
|
|
|
|
+ response.FailWithMessage(err.Error(), c)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ response.OkWithMessage("续费成功", c)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func RefundCard(c *gin.Context) {
|
|
|
|
|
+ id, _ := strconv.Atoi(c.Query("id"))
|
|
|
|
|
+ if err := monthlySvc.Refund(uint(id)); err != nil {
|
|
|
|
|
+ response.FailWithMessage(err.Error(), c)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ response.OkWithMessage("退卡成功", c)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func ListCards(c *gin.Context) {
|
|
|
|
|
+ var q repository.MonthlyCardQuery
|
|
|
|
|
+ if err := c.ShouldBindQuery(&q); err != nil {
|
|
|
|
|
+ response.FailWithMessage(err.Error(), c)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ list, total, err := monthlySvc.List(q)
|
|
|
|
|
+ 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 SetupMonthlyCardRouter(router *gin.RouterGroup) {
|
|
|
|
|
+ mc := router.Group("/monthly-card")
|
|
|
|
|
+ {
|
|
|
|
|
+ mc.POST("/create", CreateCard)
|
|
|
|
|
+ mc.POST("/renew", RenewCard)
|
|
|
|
|
+ mc.DELETE("/refund", RefundCard)
|
|
|
|
|
+ mc.GET("/list", ListCards)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: 注册路由**
|
|
|
|
|
+
|
|
|
|
|
+Edit `internal/initialize/router.go`:
|
|
|
|
|
+- Import: `"wails-app/internal/modules/monthly"`
|
|
|
|
|
+- In PrivateGroup add: `monthly.SetupMonthlyCardRouter(PrivateGroup)`
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: 构建 + 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','/monthly-card/create','POST'),('p','618','/monthly-card/create','POST'),('p','888','/monthly-card/renew','POST'),('p','618','/monthly-card/renew','POST'),('p','888','/monthly-card/refund','DELETE'),('p','618','/monthly-card/refund','DELETE'),('p','888','/monthly-card/list','GET'),('p','618','/monthly-card/list','GET');"
|
|
|
|
|
+git add internal/modules/monthly/api.go internal/initialize/router.go
|
|
|
|
|
+git commit -m "feat: 包月车管理API——create/renew/refund/list路由注册"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### Task 3: 前端页面 + 菜单
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: API 封装**
|
|
|
|
|
+
|
|
|
|
|
+Write `frontend/src/api/monthlyCard.js`:
|
|
|
|
|
+
|
|
|
|
|
+```js
|
|
|
|
|
+import service from '@/utils/request'
|
|
|
|
|
+
|
|
|
|
|
+export const createMonthlyCard = (data) => service({ url: '/monthly-card/create', method: 'post', data })
|
|
|
|
|
+export const renewMonthlyCard = (data) => service({ url: '/monthly-card/renew', method: 'post', data })
|
|
|
|
|
+export const refundMonthlyCard = (id) => service({ url: '/monthly-card/refund?id=' + id, method: 'delete' })
|
|
|
|
|
+export const getMonthlyCardList = (params) => service({ url: '/monthly-card/list', method: 'get', params })
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: 页面**
|
|
|
|
|
+
|
|
|
|
|
+Write `frontend/src/view/parking/monthlyCard.vue`:
|
|
|
|
|
+
|
|
|
|
|
+```vue
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <el-form inline>
|
|
|
|
|
+ <el-form-item :label="$t.value.LicensePlateNo">
|
|
|
|
|
+ <el-input v-model="queryForm.plate_number" :placeholder="$t.value.carPlateInput" clearable />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="卡类型">
|
|
|
|
|
+ <el-select v-model="queryForm.card_type" clearable placeholder="全部" style="width:120px">
|
|
|
|
|
+ <el-option label="月卡" value="month" />
|
|
|
|
|
+ <el-option label="季卡" value="quarter" />
|
|
|
|
|
+ <el-option label="年卡" value="year" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item>
|
|
|
|
|
+ <el-button type="primary" @click="fetchList">{{ $t.value.Search }}</el-button>
|
|
|
|
|
+ <el-button @click="resetQuery">{{ $t.value.Reset }}</el-button>
|
|
|
|
|
+ <el-button type="success" @click="openCreate">{{ $t.value.new }}</el-button>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+
|
|
|
|
|
+ <el-table :data="list" height="605" border>
|
|
|
|
|
+ <el-table-column :label="$t.value.licensePlate" prop="plate_number" align="center" width="120" />
|
|
|
|
|
+ <el-table-column label="卡类型" align="center" width="100">
|
|
|
|
|
+ <template #default="{ row }">{{ cardTypeLabel(row.card_type) }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="生效日期" prop="start_date" align="center" width="120" />
|
|
|
|
|
+ <el-table-column label="到期日期" prop="end_date" align="center" width="120" />
|
|
|
|
|
+ <el-table-column label="金额" align="center" width="100">
|
|
|
|
|
+ <template #default="{ row }">¥{{ row.fee.toFixed(2) }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="状态" align="center" width="80">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-tag :type="row.payment_status === 'paid' ? 'success' : 'danger'">{{ row.payment_status === 'paid' ? '有效' : '已退' }}</el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column :label="$t.value.Action" align="center" width="160">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <template v-if="row.payment_status === 'paid'">
|
|
|
|
|
+ <el-button text type="primary" @click="openRenew(row)">续费</el-button>
|
|
|
|
|
+ <el-button text type="danger" @click="doRefund(row.id)">退卡</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+
|
|
|
|
|
+ <el-pagination v-model:current-page="queryForm.page" :page-size="queryForm.page_size" :total="total" layout="total, prev, pager, next" style="margin-top:12px; justify-content:flex-end" @current-change="fetchList" />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 办理弹窗 -->
|
|
|
|
|
+ <el-dialog v-model="createVisible" title="办理月卡" width="460px">
|
|
|
|
|
+ <el-form :model="createForm" label-width="100px">
|
|
|
|
|
+ <el-form-item label="选择车辆" required>
|
|
|
|
|
+ <el-select v-model="createForm.vehicle_id" filterable placeholder="搜索车牌号" style="width:100%">
|
|
|
|
|
+ <el-option v-for="v in vehicleList" :key="v.ID" :label="v.plate_number" :value="v.ID" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="卡类型" required>
|
|
|
|
|
+ <el-select v-model="createForm.card_type" style="width:100%">
|
|
|
|
|
+ <el-option label="月卡 (30天)" value="month" />
|
|
|
|
|
+ <el-option label="季卡 (90天)" value="quarter" />
|
|
|
|
|
+ <el-option label="年卡 (365天)" value="year" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="到期日期">{{ computedEndDate }}</el-form-item>
|
|
|
|
|
+ <el-form-item label="金额" required>
|
|
|
|
|
+ <el-input-number v-model="createForm.fee" :min="0" :precision="2" style="width:100%" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="createVisible = false">{{ $t.value.Cancel }}</el-button>
|
|
|
|
|
+ <el-button type="primary" :loading="submitting" @click="doCreate">{{ $t.value.Confirm }}</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 续费弹窗 -->
|
|
|
|
|
+ <el-dialog v-model="renewVisible" title="续费" width="400px">
|
|
|
|
|
+ <el-form :model="renewForm" label-width="100px">
|
|
|
|
|
+ <el-form-item label="续费时长" required>
|
|
|
|
|
+ <el-select v-model="renewForm.card_type" style="width:100%">
|
|
|
|
|
+ <el-option label="月卡 (30天)" value="month" />
|
|
|
|
|
+ <el-option label="季卡 (90天)" value="quarter" />
|
|
|
|
|
+ <el-option label="年卡 (365天)" value="year" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="金额" required>
|
|
|
|
|
+ <el-input-number v-model="renewForm.fee" :min="0" :precision="2" style="width:100%" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="renewVisible = false">{{ $t.value.Cancel }}</el-button>
|
|
|
|
|
+ <el-button type="primary" :loading="submitting" @click="doRenew">{{ $t.value.Confirm }}</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { ref, reactive, computed, onMounted } from 'vue'
|
|
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
+import { createMonthlyCard, renewMonthlyCard, refundMonthlyCard, getMonthlyCardList } from '@/api/monthlyCard'
|
|
|
|
|
+import { queryAllVehicles } from '@/api/vehicle'
|
|
|
|
|
+
|
|
|
|
|
+const queryForm = reactive({ plate_number: '', card_type: '', page: 1, page_size: 10 })
|
|
|
|
|
+const list = ref([]), total = ref(0)
|
|
|
|
|
+const vehicleList = ref([])
|
|
|
|
|
+const createVisible = ref(false), renewVisible = ref(false)
|
|
|
|
|
+const submitting = ref(false)
|
|
|
|
|
+const createForm = reactive({ vehicle_id: null, card_type: 'month', fee: 0 })
|
|
|
|
|
+const renewForm = reactive({ card_id: null, card_type: 'month', fee: 0 })
|
|
|
|
|
+
|
|
|
|
|
+const cardDays = { month: 30, quarter: 90, year: 365 }
|
|
|
|
|
+const cardTypeLabel = (t) => ({ month: '月卡', quarter: '季卡', year: '年卡' }[t] || t)
|
|
|
|
|
+
|
|
|
|
|
+const computedEndDate = computed(() => {
|
|
|
|
|
+ if (!createForm.card_type) return '-'
|
|
|
|
|
+ const d = new Date(); d.setDate(d.getDate() + cardDays[createForm.card_type])
|
|
|
|
|
+ return d.toLocaleDateString('zh-CN')
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => { fetchList(); queryAllVehicles().then(r => { if (r.code === 0) vehicleList.value = r.data || [] }) })
|
|
|
|
|
+
|
|
|
|
|
+function fetchList() { getMonthlyCardList(queryForm).then(r => { if (r.code === 0) { list.value = r.data.list || []; total.value = r.data.total || 0 } }) }
|
|
|
|
|
+function resetQuery() { queryForm.plate_number = ''; queryForm.card_type = ''; queryForm.page = 1; fetchList() }
|
|
|
|
|
+
|
|
|
|
|
+function openCreate() { createForm.vehicle_id = null; createForm.card_type = 'month'; createForm.fee = 0; createVisible.value = true }
|
|
|
|
|
+function openRenew(row) { renewForm.card_id = row.id; renewForm.card_type = 'month'; renewForm.fee = 0; renewVisible.value = true }
|
|
|
|
|
+
|
|
|
|
|
+async function doCreate() {
|
|
|
|
|
+ if (!createForm.vehicle_id) { ElMessage.warning('请选择车辆'); return }
|
|
|
|
|
+ submitting.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const r = await createMonthlyCard(createForm)
|
|
|
|
|
+ if (r.code === 0) { ElMessage.success('办理成功'); createVisible.value = false; fetchList() }
|
|
|
|
|
+ else ElMessage.error(r.msg)
|
|
|
|
|
+ } catch (e) { ElMessage.error(e.message) }
|
|
|
|
|
+ finally { submitting.value = false }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function doRenew() {
|
|
|
|
|
+ submitting.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const r = await renewMonthlyCard(renewForm)
|
|
|
|
|
+ if (r.code === 0) { ElMessage.success('续费成功'); renewVisible.value = false; fetchList() }
|
|
|
|
|
+ else ElMessage.error(r.msg)
|
|
|
|
|
+ } catch (e) { ElMessage.error(e.message) }
|
|
|
|
|
+ finally { submitting.value = false }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function doRefund(id) {
|
|
|
|
|
+ await ElMessageBox.confirm('确定退卡?将移除白名单。', '退卡确认', { type: 'warning' })
|
|
|
|
|
+ try {
|
|
|
|
|
+ const r = await refundMonthlyCard(id)
|
|
|
|
|
+ if (r.code === 0) { ElMessage.success('退卡成功'); fetchList() }
|
|
|
|
|
+ else ElMessage.error(r.msg)
|
|
|
|
|
+ } catch (e) { ElMessage.error(e.message) }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: 翻译键**
|
|
|
|
|
+
|
|
|
|
|
+zh-CN.js:
|
|
|
|
|
+```js
|
|
|
|
|
+monthlyCard: '包月车管理',
|
|
|
|
|
+monthCard: '月卡',
|
|
|
|
|
+quarterCard: '季卡',
|
|
|
|
|
+yearCard: '年卡',
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+en.js:
|
|
|
|
|
+```js
|
|
|
|
|
+monthlyCard: 'Monthly Card',
|
|
|
|
|
+monthCard: 'Month',
|
|
|
|
|
+quarterCard: 'Quarter',
|
|
|
|
|
+yearCard: 'Year',
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 4: 菜单**
|
|
|
|
|
+
|
|
|
|
|
+Seed + DB insert, ParentId: 20, Sort: 6:
|
|
|
|
|
+```bash
|
|
|
|
|
+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, 'monthlyCard', 'monthlyCard', 0, 'view/parking/monthlyCard.vue', 6, '包月车管理', 'calendar', datetime('now'), datetime('now')); INSERT INTO sys_authority_menus VALUES ('618', 33), ('888', 33), ('9527', 33);"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Seed.go: add menu entry + authority_menu + Casbin rules.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 5: 构建 + 提交**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+cd frontend && npm run build && cd ..
|
|
|
|
|
+git add frontend/src/ internal/initialize/seed.go
|
|
|
|
|
+git commit -m "feat: 包月车管理前端——办理/续费/退卡页面+菜单+多语言"
|
|
|
|
|
+```
|