2026-07-16-manual-entry-exit-plan.md 12 KB

手动进出场操作页面 — 实现计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 在停车管理下新增"进出场操作"页面,支持手动输入车牌/RFID 完成车辆进场和出场操作

Architecture: 前端新增 Vue 3 组件 entryExit.vue,Tab 切换进场/出场模式,复用已有 vehicle.js API。后端仅需在 seed.go 新增菜单记录和权限映射。

Tech Stack: Vue 3 + Element Plus + Pinia, Go 1.25 + GIN

预估文件变更: 2 files (1 new + 1 modified)


文件结构

文件 操作 职责
frontend/src/view/parking/entryExit.vue 新建 进出场操作页面:输入查询、车辆详情、进场/出场操作
internal/initialize/seed.go 修改 新增菜单记录(entryExit, ID=31) + authority_menu 映射

Task 1: 新增菜单和权限映射

Files:

  • Modify: internal/initialize/seed.go

  • [ ] Step 1: 新增菜单记录

pricing 菜单之后插入(第 122 行之后):

{MenuLevel: 0, ParentId: 20, Path: "entryExit", Name: "entryExit", Hidden: false, Component: "view/parking/entryExit.vue", Sort: 5, Meta: dao.Meta{Title: "进出场操作", Icon: "switch"}},
  • Step 2: 新增 authority_menu 映射

在第 173 行之后新增:

// 进出场操作 (31) — admin + operator
{MenuId: "31", AuthorityId: "618"}, {MenuId: "31", AuthorityId: "888"}, {MenuId: "31", AuthorityId: "9527"},
  • Step 3: 编译验证
cd lc_garage && go build -o build/bin/smart-parking.exe .
  • Step 4: 提交
git add internal/initialize/seed.go
git commit -m "feat: 新增进出场操作菜单和权限——MenuId=31, ParentId=20"

Task 2: 创建进出场操作页面

Files:

  • Create: frontend/src/view/parking/entryExit.vue

  • [ ] Step 1: 完整文件内容(template + script + style)

<template>
  <div class="entry-exit-page">
    <!-- Tab 切换进场/出场 -->
    <el-tabs v-model="activeTab" @tab-change="handleTabChange">
      <el-tab-pane label="车辆进场" name="entry" />
      <el-tab-pane label="车辆出场" name="exit" />
    </el-tabs>

    <!-- 输入区:车牌号 + RFID -->
    <el-card class="query-card">
      <el-form :model="queryForm" inline>
        <el-form-item label="车牌号">
          <el-input
            v-model="queryForm.plate_number"
            placeholder="请输入车牌号"
            clearable
            style="width: 200px"
            @keyup.enter="queryVehicle"
          />
        </el-form-item>
        <el-form-item label="RFID标签">
          <el-input
            v-model="queryForm.rfid_tag"
            placeholder="请输入RFID标签"
            clearable
            style="width: 240px"
            @keyup.enter="queryVehicle"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" icon="Search" @click="queryVehicle">查询</el-button>
          <el-button icon="Refresh" @click="resetForm">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>

    <!-- 车辆详情卡片(查询后显示) -->
    <el-card v-if="vehicleLoaded" class="detail-card">
      <template #header>
        <span class="card-title">{{ activeTab === 'entry' ? '车辆信息' : '出场信息' }}</span>
      </template>

      <el-descriptions :column="3" border>
        <el-descriptions-item label="车牌号">
          {{ vehicleInfo.plate_number || '-' }}
        </el-descriptions-item>
        <el-descriptions-item label="RFID标签">
          {{ vehicleInfo.rfid_tag || '-' }}
        </el-descriptions-item>
        <el-descriptions-item label="车辆类型">
          <el-tag v-if="vehicleInfo.VehicleType" :type="vehicleInfo.VehicleType.ID === 1 ? 'warning' : 'success'">
            {{ vehicleInfo.VehicleType.name }}
          </el-tag>
          <el-tag v-else type="warning">临时车</el-tag>
        </el-descriptions-item>
        <el-descriptions-item v-if="vehicleInfo.Owner" label="车主">
          {{ vehicleInfo.Owner.owner_name }}
        </el-descriptions-item>
        <el-descriptions-item v-if="vehicleInfo.Owner && vehicleInfo.Owner.is_vip" label="VIP">
          <el-tag type="danger">是</el-tag>
        </el-descriptions-item>
      </el-descriptions>

      <!-- 进场特有操作区 -->
      <div v-if="activeTab === 'entry'" class="action-area">
        <el-divider />
        <el-form :model="entryForm" inline>
          <el-form-item label="选择停车场" required>
            <el-select
              v-model="entryForm.parking_lot_id"
              placeholder="请选择停车场"
              style="width: 260px"
            >
              <el-option
                v-for="lot in parkingLotList"
                :key="lot.ID"
                :label="lot.lot_name"
                :value="lot.ID"
              />
            </el-select>
          </el-form-item>
          <el-form-item>
            <el-button
              type="success"
              icon="Check"
              :loading="submitting"
              @click="confirmEntry"
            >
              确认进场
            </el-button>
          </el-form-item>
        </el-form>
      </div>

      <!-- 出场特有操作区 -->
      <div v-if="activeTab === 'exit'" class="action-area">
        <el-divider />
        <p class="exit-tip">确认后系统将自动计算停车费用</p>
        <el-button
          type="danger"
          icon="Check"
          :loading="submitting"
          @click="confirmExit"
        >
          确认出场并收费
        </el-button>
      </div>
    </el-card>

    <!-- 查询结果:未找到车辆 -->
    <el-result
      v-if="notFound"
      icon="warning"
      title="未找到车辆信息"
      :sub-title="activeTab === 'entry' ? '将按临时车处理,请选择停车场后确认进场' : '该车辆未登记,请核实车牌号'"
    >
      <template v-if="activeTab === 'entry'" #extra>
        <el-form :model="entryForm" inline>
          <el-form-item label="选择停车场" required>
            <el-select
              v-model="entryForm.parking_lot_id"
              placeholder="请选择停车场"
              style="width: 260px"
            >
              <el-option
                v-for="lot in parkingLotList"
                :key="lot.ID"
                :label="lot.lot_name"
                :value="lot.ID"
              />
            </el-select>
          </el-form-item>
          <el-form-item>
            <el-button
              type="success"
              :loading="submitting"
              @click="confirmEntry"
            >
              确认进场(临时车)
            </el-button>
          </el-form-item>
        </el-form>
      </template>
    </el-result>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { obtainVehicleLicense, vehicleEntry, vehicleAppear } from '@/api/vehicle'
import { obtainLotList } from '@/api/pullOver'

// ---- 状态 ----
const activeTab = ref('entry')
const queryForm = reactive({ plate_number: '', rfid_tag: '' })
const vehicleInfo = ref(null)
const vehicleLoaded = ref(false)
const notFound = ref(false)
const submitting = ref(false)

const entryForm = reactive({ parking_lot_id: null })
const parkingLotList = ref([])

// ---- 初始化:加载停车场列表 ----
onMounted(async () => {
  try {
    const res = await obtainLotList({ page: 1, pageSize: 100 })
    parkingLotList.value = res.data?.list || []
  } catch (e) {
    console.error('加载停车场列表失败', e)
  }
})

// ---- 查询车辆 ----
async function queryVehicle() {
  const plate = queryForm.plate_number.trim()
  const rfid = queryForm.rfid_tag.trim()

  if (!plate && !rfid) {
    ElMessage.warning('请至少输入车牌号或RFID标签')
    return
  }

  vehicleInfo.value = null
  vehicleLoaded.value = false
  notFound.value = false

  try {
    const res = await obtainVehicleLicense(plate || rfid)
    if (res.code === 0 && res.data) {
      vehicleInfo.value = res.data
      vehicleLoaded.value = true
    } else {
      notFound.value = true
    }
  } catch (e) {
    notFound.value = true
  }
}

// ---- 确认进场 ----
async function confirmEntry() {
  if (!entryForm.parking_lot_id) {
    ElMessage.warning('请选择停车场')
    return
  }

  if (submitting.value) return
  submitting.value = true

  try {
    const res = await vehicleEntry({
      plate_number: queryForm.plate_number.trim(),
      rfid_tag: queryForm.rfid_tag.trim(),
      parking_lot_id: entryForm.parking_lot_id
    })
    if (res.code === 0) {
      const plate = res.data?.data?.plate_number || queryForm.plate_number.trim()
      ElMessage.success(`入场成功!${plate} 已进入停车场`)
      resetForm()
    } else {
      ElMessage.error(res.msg || '入场失败')
    }
  } catch (e) {
    ElMessage.error(e.message || '入场失败')
  } finally {
    submitting.value = false
  }
}

// ---- 确认出场 ----
async function confirmExit() {
  const plate = vehicleInfo.value?.plate_number || queryForm.plate_number.trim()

  await ElMessageBox.confirm(
    `确认车辆 ${plate} 出场?`,
    '确认出场',
    { confirmButtonText: '确认出场', cancelButtonText: '取消', type: 'warning' }
  )

  if (submitting.value) return
  submitting.value = true

  try {
    const res = await vehicleAppear({
      plate_number: queryForm.plate_number.trim(),
      rfid_tag: queryForm.rfid_tag.trim()
    })
    if (res.code === 0) {
      const data = res.data?.data || res.data || {}
      const fee = data.fee || 0
      const stayTime = data.stay_time || 0
      const entryTime = data.entry_time ? new Date(data.entry_time * 1000).toLocaleString('zh-CN') : '-'

      ElMessage.success(
        `出场成功!进场: ${entryTime} | 停留: ${formatDuration(stayTime)} | 费用: ¥${fee.toFixed(2)}`
      )
      resetForm()
    } else {
      ElMessage.error(res.msg || '出场失败')
    }
  } catch (e) {
    ElMessage.error(e.message || '出场失败')
  } finally {
    submitting.value = false
  }
}

// ---- 工具 ----
function handleTabChange() {
  resetForm()
}

function resetForm() {
  queryForm.plate_number = ''
  queryForm.rfid_tag = ''
  vehicleInfo.value = null
  vehicleLoaded.value = false
  notFound.value = false
  entryForm.parking_lot_id = null
}

function formatDuration(minutes) {
  if (!minutes && minutes !== 0) return '-'
  const h = Math.floor(minutes / 60)
  const m = minutes % 60
  return h > 0 ? `${h}小时${m}分钟` : `${m}分钟`
}
</script>

<style scoped>
.entry-exit-page {
  padding: 0;
}

.query-card {
  margin-bottom: 16px;
}

.detail-card {
  margin-bottom: 16px;
}

.card-title {
  font-weight: bold;
  font-size: 16px;
}

.action-area {
  margin-top: 8px;
}

.exit-tip {
  color: #909399;
  margin-bottom: 12px;
}
</style>
  • Step 2: 提交
git add frontend/src/view/parking/entryExit.vue
git commit -m "feat: 新增手动进出场操作页面——支持车牌/RFID查询进出场"

Task 3: 端到端验证

  • Step 1: 启动开发模式
cd lc_garage && wails dev
  • Step 2: 验证菜单显示
  1. 登录(admin / 123456)
  2. 侧边栏 → 停车管理 → 确认"进出场操作"菜单出现
  3. 点击进入页面,确认 Tab 切换正常
  • Step 3: 验证进场流程
  1. 输入新车牌号(如"测试001")
  2. 点查询 → 确认显示"未找到车辆信息" + 停车场选择
  3. 选停车场 → 点"确认进场(临时车)"
  4. 确认绿色"入场成功"提示
  • Step 4: 验证出场流程
  1. 切换到"车辆出场"Tab
  2. 输入刚才的车牌号 → 点查询
  3. 确认车辆信息卡片显示
  4. 点"确认出场并收费" → 确认弹窗 → 确认
  5. 确认成功消息显示费用信息
  • Step 5: 验证边界情况
场景 预期结果
空输入点查询 "请至少输入车牌号或RFID标签"
未选停车场点确认进场 "请选择停车场"
已出场车辆再次出场 "车辆未入场或已出场"
不在场车辆点出场 "车辆未入场或已出场"
  • Step 6: 提交
git add -A && git commit -m "verify: 手动进出场页面端到端验证通过"