entryExit.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. <template>
  2. <div class="entry-exit-page">
  3. <el-tabs v-model="activeTab" @tab-change="handleTabChange">
  4. <el-tab-pane :label="$t.value.entryVehicle" name="entry" />
  5. <el-tab-pane :label="$t.value.exitVehicle" name="exit" />
  6. </el-tabs>
  7. <el-card class="query-card">
  8. <el-form :model="queryForm" inline>
  9. <el-form-item :label="$t.value.LicensePlateNo">
  10. <el-input v-model="queryForm.plate_number" :placeholder="$t.value.carPlateInput" clearable style="width: 200px" @keyup.enter="queryVehicle" />
  11. </el-form-item>
  12. <el-form-item :label="$t.value.RFID_Tag">
  13. <el-input v-model="queryForm.rfid_tag" :placeholder="$t.value.rfidInput" clearable style="width: 240px" @keyup.enter="queryVehicle" />
  14. </el-form-item>
  15. <el-form-item>
  16. <el-button type="primary" icon="Search" @click="queryVehicle">{{ $t.value.Search }}</el-button>
  17. <el-button icon="Refresh" @click="resetForm">{{ $t.value.Reset }}</el-button>
  18. </el-form-item>
  19. </el-form>
  20. </el-card>
  21. <el-card v-if="vehicleLoaded" class="detail-card">
  22. <template #header>
  23. <span class="card-title">{{ activeTab === 'entry' ? $t.value.vehicleDetail : $t.value.exitDetail }}</span>
  24. </template>
  25. <el-descriptions :column="3" border>
  26. <el-descriptions-item :label="$t.value.LicensePlateNo">{{ vehicleInfo.plate_number || '-' }}</el-descriptions-item>
  27. <el-descriptions-item :label="$t.value.RFID_Tag">{{ vehicleInfo.rfid_tag || '-' }}</el-descriptions-item>
  28. <el-descriptions-item :label="$t.value.VehicleType">
  29. <el-tag v-if="vehicleInfo.VehicleType" :type="vehicleInfo.VehicleType.is_system ? 'warning' : 'success'">{{ vehicleInfo.VehicleType.name }}</el-tag>
  30. <el-tag v-else type="warning">{{ $t.value.tempCar }}</el-tag>
  31. </el-descriptions-item>
  32. <el-descriptions-item v-if="vehicleInfo.Owner" :label="$t.value.CarOwner">{{ vehicleInfo.Owner.owner_name }}</el-descriptions-item>
  33. <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>
  34. </el-descriptions>
  35. <div v-if="activeTab === 'entry'" class="action-area">
  36. <el-divider />
  37. <el-form :model="entryForm" inline>
  38. <el-form-item :label="$t.value.selectParkArea" required>
  39. <el-select v-model="entryForm.parking_lot_id" :placeholder="$t.value.pleaseSelectPark" style="width: 260px">
  40. <el-option v-for="lot in parkingLotList" :key="lot.ID" :label="lot.lot_name" :value="lot.ID" />
  41. </el-select>
  42. </el-form-item>
  43. <el-form-item>
  44. <el-button type="success" icon="Check" :loading="submitting" @click="confirmEntry">{{ $t.value.confirmEntry }}</el-button>
  45. </el-form-item>
  46. </el-form>
  47. </div>
  48. <div v-if="activeTab === 'exit' && exitData" class="action-area">
  49. <el-divider />
  50. <el-descriptions :column="2" border>
  51. <el-descriptions-item :label="$t.value.EntryTime">{{ formatTime(exitData.entry_time) }}</el-descriptions-item>
  52. <el-descriptions-item :label="$t.value.StayDuration">{{ formatDuration(exitData.stay_time) }}</el-descriptions-item>
  53. <el-descriptions-item :label="$t.value.Fee"><span class="fee-amount">¥{{ exitData.fee.toFixed(2) }}</span></el-descriptions-item>
  54. <el-descriptions-item :label="$t.value.VehicleType">{{ exitData.vehicle_type || '-' }}</el-descriptions-item>
  55. </el-descriptions>
  56. <div v-if="exitData.is_temp" class="payment-section">
  57. <p class="section-label">{{ $t.value.paymentMethod }}:</p>
  58. <el-radio-group v-model="paymentMethod">
  59. <el-radio-button value="cash">{{ $t.value.cashPayment }}</el-radio-button>
  60. <el-radio-button value="wechat" disabled>{{ $t.value.wechatPayment }}</el-radio-button>
  61. <el-radio-button value="pos" disabled>{{ $t.value.posPayment }}</el-radio-button>
  62. </el-radio-group>
  63. <div v-if="paymentMethod === 'cash'" class="cash-input">
  64. <span class="cash-label">{{ $t.value.cashReceived }}:</span>
  65. <el-input-number v-model="paidAmount" :min="0" :precision="1" style="width: 160px" />
  66. <span class="change-label">{{ $t.value.change }}: ¥{{ Math.max(0, paidAmount - exitData.fee).toFixed(1) }}</span>
  67. </div>
  68. </div>
  69. <div style="margin-top: 16px">
  70. <el-button type="danger" icon="Check" :loading="submitting" @click="confirmExit">
  71. {{ exitData.is_temp ? $t.value.confirmPayAndExit : $t.value.confirmExitAndPay }}
  72. </el-button>
  73. </div>
  74. </div>
  75. </el-card>
  76. <el-result v-if="notFound" icon="warning" :title="$t.value.notFoundVehicle" :sub-title="activeTab === 'entry' ? $t.value.tempCarEntryHint : $t.value.plateNotRegHint">
  77. <template v-if="activeTab === 'entry'" #extra>
  78. <el-form :model="entryForm" inline>
  79. <el-form-item :label="$t.value.selectParkArea" required>
  80. <el-select v-model="entryForm.parking_lot_id" :placeholder="$t.value.pleaseSelectPark" style="width: 260px">
  81. <el-option v-for="lot in parkingLotList" :key="lot.ID" :label="lot.lot_name" :value="lot.ID" />
  82. </el-select>
  83. </el-form-item>
  84. <el-form-item>
  85. <el-button type="success" :loading="submitting" @click="confirmEntry">{{ $t.value.confirmEntryTemp }}</el-button>
  86. </el-form-item>
  87. </el-form>
  88. </template>
  89. </el-result>
  90. </div>
  91. </template>
  92. <script setup>
  93. import { ref, reactive, onMounted, onUnmounted } from 'vue'
  94. import { ElMessage, ElMessageBox } from 'element-plus'
  95. import { obtainVehicleLicense, vehicleEntry, exitPreview, exitConfirm, getChannelEvents } from '@/api/vehicle'
  96. import { obtainPullOverList } from '@/api/pullOver'
  97. import { t } from '@/lang/index.js'
  98. const activeTab = ref('entry')
  99. const queryForm = reactive({ plate_number: '', rfid_tag: '' })
  100. const vehicleInfo = ref(null)
  101. const vehicleLoaded = ref(false)
  102. const notFound = ref(false)
  103. const submitting = ref(false)
  104. const entryForm = reactive({ parking_lot_id: null })
  105. const parkingLotList = ref([])
  106. const exitData = ref(null)
  107. const paymentMethod = ref('cash')
  108. const paidAmount = ref(0)
  109. let eventTimer = null
  110. const lastEventTs = ref(0)
  111. onMounted(async () => {
  112. try {
  113. const res = await obtainPullOverList({ page: 1, pageSize: 100 })
  114. parkingLotList.value = res.data?.list || []
  115. } catch (e) {
  116. console.error('Failed to load parking lots', e)
  117. }
  118. eventTimer = setInterval(pollEvents, 3000)
  119. })
  120. onUnmounted(() => { clearInterval(eventTimer) })
  121. async function pollEvents() {
  122. try {
  123. const r = await getChannelEvents(lastEventTs.value)
  124. if (r.code === 0 && r.data && r.data.length > 0) {
  125. const latest = r.data[r.data.length - 1]
  126. lastEventTs.value = latest.timestamp
  127. queryForm.rfid_tag = latest.rfid_tag
  128. activeTab.value = latest.direction === 'out' ? 'exit' : 'entry'
  129. queryVehicle()
  130. }
  131. } catch (e) { /* ignore */ }
  132. }
  133. async function queryVehicle() {
  134. const plate = queryForm.plate_number.trim()
  135. const rfid = queryForm.rfid_tag.trim()
  136. if (!plate && !rfid) {
  137. ElMessage.warning(t.value.pleaseEnterPlateOrRfid)
  138. return
  139. }
  140. vehicleInfo.value = null
  141. vehicleLoaded.value = false
  142. notFound.value = false
  143. exitData.value = null
  144. try {
  145. const res = await obtainVehicleLicense(plate || rfid)
  146. if (res.code === 0 && res.data) {
  147. vehicleInfo.value = res.data
  148. vehicleLoaded.value = true
  149. if (activeTab.value === 'exit') { await previewExitFee() }
  150. } else {
  151. notFound.value = true
  152. }
  153. } catch (e) {
  154. notFound.value = true
  155. }
  156. }
  157. async function previewExitFee() {
  158. try {
  159. const res = await exitPreview({ plate_number: queryForm.plate_number.trim(), rfid_tag: queryForm.rfid_tag.trim() })
  160. if (res.code === 0) { exitData.value = res.data; paidAmount.value = 0; paymentMethod.value = 'cash' }
  161. } catch (e) { console.error('Preview failed', e) }
  162. }
  163. async function confirmEntry() {
  164. if (!entryForm.parking_lot_id) { ElMessage.warning(t.value.pleaseSelectPark); return }
  165. if (submitting.value) return
  166. submitting.value = true
  167. try {
  168. const res = await vehicleEntry({ plate_number: queryForm.plate_number.trim(), rfid_tag: queryForm.rfid_tag.trim(), parking_lot_id: entryForm.parking_lot_id })
  169. if (res.code === 0) { ElMessage.success(`${t.value.entrySuccessMsg}!${res.data?.data?.plate_number || queryForm.plate_number.trim()}`); resetForm() }
  170. else ElMessage.error(res.msg || t.value.entryFailed)
  171. } catch (e) { ElMessage.error(e.message || t.value.entryFailed) } finally { submitting.value = false }
  172. }
  173. async function confirmExit() {
  174. if (exitData.value?.is_temp && paymentMethod.value === 'cash') { if (paidAmount.value < exitData.value.fee) { ElMessage.warning(t.value.insufficientAmount); return } }
  175. await ElMessageBox.confirm(t.value.confirmExitMsg, t.value.confirmExitTitle, { confirmButtonText: t.value.Confirm, cancelButtonText: t.value.Cancel, type: 'warning' })
  176. if (submitting.value) return
  177. submitting.value = true
  178. try {
  179. const pm = exitData.value?.is_temp ? paymentMethod.value : 'free'
  180. const pa = exitData.value?.is_temp ? paidAmount.value : 0
  181. const res = await exitConfirm({ plate_number: queryForm.plate_number.trim(), rfid_tag: queryForm.rfid_tag.trim(), payment_method: pm, paid_amount: pa })
  182. if (res.code === 0) {
  183. const d = res.data
  184. 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))
  185. ElMessage.success(`${t.value.exitSuccessMsg}!${detail}`)
  186. resetForm()
  187. } else ElMessage.error(res.msg || t.value.exitFailed)
  188. } catch (e) { ElMessage.error(e.message || t.value.exitFailed) } finally { submitting.value = false }
  189. }
  190. function handleTabChange() { resetForm() }
  191. function resetForm() {
  192. queryForm.plate_number = ''; queryForm.rfid_tag = ''
  193. vehicleInfo.value = null; vehicleLoaded.value = false; notFound.value = false; exitData.value = null
  194. entryForm.parking_lot_id = null; paidAmount.value = 0; paymentMethod.value = 'cash'
  195. }
  196. function formatTime(ts) { if (!ts) return '-'; return new Date(ts * 1000).toLocaleString('zh-CN') }
  197. function formatDuration(minutes) {
  198. if (!minutes && minutes !== 0) return '-'
  199. const h = Math.floor(minutes / 60); const m = minutes % 60
  200. return h > 0 ? `${h}h ${m}${t.value.minute}` : `${m}${t.value.minute}`
  201. }
  202. </script>
  203. <style scoped>
  204. .entry-exit-page { padding: 0; }
  205. .query-card { margin-bottom: 16px; }
  206. .detail-card { margin-bottom: 16px; }
  207. .card-title { font-weight: bold; font-size: 16px; }
  208. .action-area { margin-top: 8px; }
  209. .exit-tip { color: #909399; margin-bottom: 12px; }
  210. .fee-amount { color: #e6a23c; font-size: 20px; font-weight: bold; }
  211. .payment-section { margin-top: 16px; }
  212. .section-label { font-weight: bold; margin-bottom: 8px; }
  213. .cash-input { margin-top: 12px; }
  214. .cash-label { margin-right: 8px; }
  215. .change-label { margin-left: 16px; color: #67c23a; font-weight: bold; }
  216. </style>