|
|
@@ -36,7 +36,6 @@
|
|
|
<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>
|
|
|
@@ -51,25 +50,15 @@
|
|
|
</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-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">
|
|
|
@@ -77,7 +66,6 @@
|
|
|
<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" />
|
|
|
@@ -111,9 +99,9 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, reactive, onMounted } from 'vue'
|
|
|
+import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
-import { obtainVehicleLicense, vehicleEntry, exitPreview, exitConfirm } from '@/api/vehicle'
|
|
|
+import { obtainVehicleLicense, vehicleEntry, exitPreview, exitConfirm, getChannelEvents } from '@/api/vehicle'
|
|
|
import { obtainPullOverList } from '@/api/pullOver'
|
|
|
import { t } from '@/lang/index.js'
|
|
|
|
|
|
@@ -126,11 +114,13 @@ 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 })
|
|
|
@@ -138,8 +128,24 @@ onMounted(async () => {
|
|
|
} 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()
|
|
|
@@ -156,10 +162,7 @@ async function queryVehicle() {
|
|
|
if (res.code === 0 && res.data) {
|
|
|
vehicleInfo.value = res.data
|
|
|
vehicleLoaded.value = true
|
|
|
- // 出场模式:预览费用
|
|
|
- if (activeTab.value === 'exit') {
|
|
|
- await previewExitFee()
|
|
|
- }
|
|
|
+ if (activeTab.value === 'exit') { await previewExitFee() }
|
|
|
} else {
|
|
|
notFound.value = true
|
|
|
}
|
|
|
@@ -170,114 +173,53 @@ async function queryVehicle() {
|
|
|
|
|
|
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) {
|
|
|
- // preview 失败不影响,confirmExit 会再次校验
|
|
|
- console.error('Preview failed', e)
|
|
|
- }
|
|
|
+ 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 (!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) {
|
|
|
- const plate = res.data?.data?.plate_number || queryForm.plate_number.trim()
|
|
|
- ElMessage.success(`${t.value.entrySuccessMsg}!${plate}`)
|
|
|
- resetForm()
|
|
|
- } else {
|
|
|
- ElMessage.error(res.msg || t.value.entryFailed)
|
|
|
- }
|
|
|
- } catch (e) {
|
|
|
- ElMessage.error(e.message || t.value.entryFailed)
|
|
|
- } finally {
|
|
|
- submitting.value = false
|
|
|
- }
|
|
|
+ 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 (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
|
|
|
- })
|
|
|
+ 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))
|
|
|
+ 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
|
|
|
- }
|
|
|
+ } 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'
|
|
|
+ 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 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
|
|
|
+ const h = Math.floor(minutes / 60); const m = minutes % 60
|
|
|
return h > 0 ? `${h}h ${m}${t.value.minute}` : `${m}${t.value.minute}`
|
|
|
}
|
|
|
</script>
|