Browse Source

feat: 新增手动进出场操作页面——支持车牌/RFID查询进出场

lq 1 tháng trước cách đây
mục cha
commit
157ad368df
1 tập tin đã thay đổi với 208 bổ sung0 xóa
  1. 208 0
      frontend/src/view/parking/entryExit.vue

+ 208 - 0
frontend/src/view/parking/entryExit.vue

@@ -0,0 +1,208 @@
+<template>
+  <div class="entry-exit-page">
+    <el-tabs v-model="activeTab" @tab-change="handleTabChange">
+      <el-tab-pane label="车辆进场" name="entry" />
+      <el-tab-pane label="车辆出场" name="exit" />
+    </el-tabs>
+
+    <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>