| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240 |
- <template>
- <div class="entry-exit-page">
- <el-tabs v-model="activeTab" @tab-change="handleTabChange">
- <el-tab-pane :label="$t.value.entryVehicle" name="entry" />
- <el-tab-pane :label="$t.value.exitVehicle" name="exit" />
- </el-tabs>
- <el-card class="query-card">
- <el-form :model="queryForm" inline>
- <el-form-item :label="$t.value.LicensePlateNo">
- <el-input v-model="queryForm.plate_number" :placeholder="$t.value.carPlateInput" clearable style="width: 200px" @keyup.enter="queryVehicle" />
- </el-form-item>
- <el-form-item :label="$t.value.RFID_Tag">
- <el-input v-model="queryForm.rfid_tag" :placeholder="$t.value.rfidInput" clearable style="width: 240px" @keyup.enter="queryVehicle" />
- </el-form-item>
- <el-form-item>
- <el-button type="primary" icon="Search" @click="queryVehicle">{{ $t.value.Search }}</el-button>
- <el-button icon="Refresh" @click="resetForm">{{ $t.value.Reset }}</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' ? $t.value.vehicleDetail : $t.value.exitDetail }}</span>
- </template>
- <el-descriptions :column="3" border>
- <el-descriptions-item :label="$t.value.LicensePlateNo">{{ vehicleInfo.plate_number || '-' }}</el-descriptions-item>
- <el-descriptions-item :label="$t.value.RFID_Tag">{{ vehicleInfo.rfid_tag || '-' }}</el-descriptions-item>
- <el-descriptions-item :label="$t.value.VehicleType">
- <el-tag v-if="vehicleInfo.VehicleType" :type="vehicleInfo.VehicleType.is_system ? 'warning' : 'success'">{{ vehicleInfo.VehicleType.name }}</el-tag>
- <el-tag v-else type="warning">{{ $t.value.tempCar }}</el-tag>
- </el-descriptions-item>
- <el-descriptions-item v-if="vehicleInfo.Owner" :label="$t.value.CarOwner">{{ vehicleInfo.Owner.owner_name }}</el-descriptions-item>
- <el-descriptions-item v-if="vehicleInfo.Owner && vehicleInfo.Owner.is_vip" label="VIP"><el-tag type="danger">{{ $t.value.yes }}</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="$t.value.selectParkArea" required>
- <el-select v-model="entryForm.parking_lot_id" :placeholder="$t.value.pleaseSelectPark" 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">{{ $t.value.confirmEntry }}</el-button>
- </el-form-item>
- </el-form>
- </div>
- <div v-if="activeTab === 'exit' && exitData" class="action-area">
- <el-divider />
- <el-descriptions :column="2" border>
- <el-descriptions-item :label="$t.value.EntryTime">{{ formatTime(exitData.entry_time) }}</el-descriptions-item>
- <el-descriptions-item :label="$t.value.StayDuration">{{ formatDuration(exitData.stay_time) }}</el-descriptions-item>
- <el-descriptions-item :label="$t.value.Fee"><span class="fee-amount">¥{{ exitData.fee.toFixed(2) }}</span></el-descriptions-item>
- <el-descriptions-item :label="$t.value.VehicleType">{{ exitData.vehicle_type || '-' }}</el-descriptions-item>
- </el-descriptions>
- <div v-if="exitData.is_temp" class="payment-section">
- <p class="section-label">{{ $t.value.paymentMethod }}:</p>
- <el-radio-group v-model="paymentMethod">
- <el-radio-button value="cash">{{ $t.value.cashPayment }}</el-radio-button>
- <el-radio-button value="wechat" disabled>{{ $t.value.wechatPayment }}</el-radio-button>
- <el-radio-button value="pos" disabled>{{ $t.value.posPayment }}</el-radio-button>
- </el-radio-group>
- <div v-if="paymentMethod === 'cash'" class="cash-input">
- <span class="cash-label">{{ $t.value.cashReceived }}:</span>
- <el-input-number v-model="paidAmount" :min="0" :precision="1" style="width: 160px" />
- <span class="change-label">{{ $t.value.change }}: ¥{{ Math.max(0, paidAmount - exitData.fee).toFixed(1) }}</span>
- </div>
- </div>
- <div style="margin-top: 16px">
- <el-button type="danger" icon="Check" :loading="submitting" @click="confirmExit">
- {{ exitData.is_temp ? $t.value.confirmPayAndExit : $t.value.confirmExitAndPay }}
- </el-button>
- </div>
- </div>
- </el-card>
- <el-result v-if="notFound" icon="warning" :title="$t.value.notFoundVehicle" :sub-title="activeTab === 'entry' ? $t.value.tempCarEntryHint : $t.value.plateNotRegHint">
- <template v-if="activeTab === 'entry'" #extra>
- <el-form :model="entryForm" inline>
- <el-form-item :label="$t.value.selectParkArea" required>
- <el-select v-model="entryForm.parking_lot_id" :placeholder="$t.value.pleaseSelectPark" 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">{{ $t.value.confirmEntryTemp }}</el-button>
- </el-form-item>
- </el-form>
- </template>
- </el-result>
- </div>
- </template>
- <script setup>
- import { ref, reactive, onMounted, onUnmounted } from 'vue'
- import { ElMessage, ElMessageBox } from 'element-plus'
- import { obtainVehicleLicense, vehicleEntry, exitPreview, exitConfirm, getChannelEvents } from '@/api/vehicle'
- import { obtainPullOverList } from '@/api/pullOver'
- import { t } from '@/lang/index.js'
- 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([])
- const exitData = ref(null)
- const paymentMethod = ref('cash')
- const paidAmount = ref(0)
- let eventTimer = null
- const lastEventTs = ref(0)
- onMounted(async () => {
- try {
- const res = await obtainPullOverList({ page: 1, pageSize: 100 })
- parkingLotList.value = res.data?.list || []
- } catch (e) {
- console.error('Failed to load parking lots', e)
- }
- eventTimer = setInterval(pollEvents, 3000)
- })
- onUnmounted(() => { clearInterval(eventTimer) })
- async function pollEvents() {
- try {
- const r = await getChannelEvents(lastEventTs.value)
- if (r.code === 0 && r.data && r.data.length > 0) {
- const latest = r.data[r.data.length - 1]
- lastEventTs.value = latest.timestamp
- queryForm.rfid_tag = latest.rfid_tag
- activeTab.value = latest.direction === 'out' ? 'exit' : 'entry'
- queryVehicle()
- }
- } catch (e) { /* ignore */ }
- }
- async function queryVehicle() {
- const plate = queryForm.plate_number.trim()
- const rfid = queryForm.rfid_tag.trim()
- if (!plate && !rfid) {
- ElMessage.warning(t.value.pleaseEnterPlateOrRfid)
- return
- }
- vehicleInfo.value = null
- vehicleLoaded.value = false
- notFound.value = false
- exitData.value = null
- try {
- const res = await obtainVehicleLicense(plate || rfid)
- if (res.code === 0 && res.data) {
- vehicleInfo.value = res.data
- vehicleLoaded.value = true
- if (activeTab.value === 'exit') { await previewExitFee() }
- } else {
- notFound.value = true
- }
- } catch (e) {
- notFound.value = true
- }
- }
- async function previewExitFee() {
- try {
- const res = await exitPreview({ plate_number: queryForm.plate_number.trim(), rfid_tag: queryForm.rfid_tag.trim() })
- if (res.code === 0) { exitData.value = res.data; paidAmount.value = 0; paymentMethod.value = 'cash' }
- } catch (e) { console.error('Preview failed', e) }
- }
- async function confirmEntry() {
- if (!entryForm.parking_lot_id) { ElMessage.warning(t.value.pleaseSelectPark); 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) { ElMessage.success(`${t.value.entrySuccessMsg}!${res.data?.data?.plate_number || queryForm.plate_number.trim()}`); resetForm() }
- else ElMessage.error(res.msg || t.value.entryFailed)
- } catch (e) { ElMessage.error(e.message || t.value.entryFailed) } finally { submitting.value = false }
- }
- async function confirmExit() {
- if (exitData.value?.is_temp && paymentMethod.value === 'cash') { if (paidAmount.value < exitData.value.fee) { ElMessage.warning(t.value.insufficientAmount); return } }
- await ElMessageBox.confirm(t.value.confirmExitMsg, t.value.confirmExitTitle, { confirmButtonText: t.value.Confirm, cancelButtonText: t.value.Cancel, type: 'warning' })
- if (submitting.value) return
- submitting.value = true
- try {
- const pm = exitData.value?.is_temp ? paymentMethod.value : 'free'
- const pa = exitData.value?.is_temp ? paidAmount.value : 0
- const res = await exitConfirm({ plate_number: queryForm.plate_number.trim(), rfid_tag: queryForm.rfid_tag.trim(), payment_method: pm, paid_amount: pa })
- if (res.code === 0) {
- const d = res.data
- const detail = t.value.exitRecordDetail.replace('{entryTime}', new Date(d.entry_time * 1000).toLocaleString('zh-CN')).replace('{duration}', formatDuration(d.stay_time)).replace('{fee}', d.fee.toFixed(2))
- ElMessage.success(`${t.value.exitSuccessMsg}!${detail}`)
- resetForm()
- } else ElMessage.error(res.msg || t.value.exitFailed)
- } catch (e) { ElMessage.error(e.message || t.value.exitFailed) } finally { submitting.value = false }
- }
- function handleTabChange() { resetForm() }
- function resetForm() {
- queryForm.plate_number = ''; queryForm.rfid_tag = ''
- vehicleInfo.value = null; vehicleLoaded.value = false; notFound.value = false; exitData.value = null
- entryForm.parking_lot_id = null; paidAmount.value = 0; paymentMethod.value = 'cash'
- }
- function formatTime(ts) { if (!ts) return '-'; return new Date(ts * 1000).toLocaleString('zh-CN') }
- function formatDuration(minutes) {
- if (!minutes && minutes !== 0) return '-'
- const h = Math.floor(minutes / 60); const m = minutes % 60
- return h > 0 ? `${h}h ${m}${t.value.minute}` : `${m}${t.value.minute}`
- }
- </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; }
- .fee-amount { color: #e6a23c; font-size: 20px; font-weight: bold; }
- .payment-section { margin-top: 16px; }
- .section-label { font-weight: bold; margin-bottom: 8px; }
- .cash-input { margin-top: 12px; }
- .cash-label { margin-right: 8px; }
- .change-label { margin-left: 16px; color: #67c23a; font-weight: bold; }
- </style>
|