|
|
@@ -29,13 +29,14 @@
|
|
|
<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.ID === 1 ? 'warning' : 'success'">{{ vehicleInfo.VehicleType.name }}</el-tag>
|
|
|
+ <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>
|
|
|
@@ -50,10 +51,45 @@
|
|
|
</el-form>
|
|
|
</div>
|
|
|
|
|
|
- <div v-if="activeTab === 'exit'" class="action-area">
|
|
|
+ <!-- 出场操作区 -->
|
|
|
+ <div v-if="activeTab === 'exit' && exitData" class="action-area">
|
|
|
<el-divider />
|
|
|
- <p class="exit-tip">{{ $t.value.exitAutoCalcFee }}</p>
|
|
|
- <el-button type="danger" icon="Check" :loading="submitting" @click="confirmExit">{{ $t.value.confirmExitAndPay }}</el-button>
|
|
|
+ <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>
|
|
|
|
|
|
@@ -77,7 +113,7 @@
|
|
|
<script setup>
|
|
|
import { ref, reactive, onMounted } from 'vue'
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
-import { obtainVehicleLicense, vehicleEntry, vehicleAppear } from '@/api/vehicle'
|
|
|
+import { obtainVehicleLicense, vehicleEntry, exitPreview, exitConfirm } from '@/api/vehicle'
|
|
|
import { obtainPullOverList } from '@/api/pullOver'
|
|
|
import { t } from '@/lang/index.js'
|
|
|
|
|
|
@@ -90,6 +126,11 @@ 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)
|
|
|
+
|
|
|
onMounted(async () => {
|
|
|
try {
|
|
|
const res = await obtainPullOverList({ page: 1, pageSize: 100 })
|
|
|
@@ -109,11 +150,16 @@ async function queryVehicle() {
|
|
|
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
|
|
|
}
|
|
|
@@ -122,6 +168,23 @@ 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)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
async function confirmEntry() {
|
|
|
if (!entryForm.parking_lot_id) {
|
|
|
ElMessage.warning(t.value.pleaseSelectPark)
|
|
|
@@ -150,6 +213,14 @@ async function confirmEntry() {
|
|
|
}
|
|
|
|
|
|
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,
|
|
|
@@ -158,19 +229,20 @@ async function confirmExit() {
|
|
|
if (submitting.value) return
|
|
|
submitting.value = true
|
|
|
try {
|
|
|
- const res = await vehicleAppear({
|
|
|
+ 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()
|
|
|
+ rfid_tag: queryForm.rfid_tag.trim(),
|
|
|
+ payment_method: pm,
|
|
|
+ paid_amount: pa
|
|
|
})
|
|
|
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') : '-'
|
|
|
+ const d = res.data
|
|
|
const detail = t.value.exitRecordDetail
|
|
|
- .replace('{entryTime}', entryTime)
|
|
|
- .replace('{duration}', formatDuration(stayTime))
|
|
|
- .replace('{fee}', fee.toFixed(2))
|
|
|
+ .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 {
|
|
|
@@ -191,7 +263,15 @@ function resetForm() {
|
|
|
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) {
|
|
|
@@ -209,4 +289,10 @@ function formatDuration(minutes) {
|
|
|
.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>
|