feat(miniprogram): add centered pound QR popup component.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-09-24 21:19:34 +08:00
1 parent 35563e9709
commit be0764779c
1 file changed
+285
@@ -0,0 +1,285 @@
<template>
<view v-if="show" class="pound-qr-mask" @click="close">
<view class="pound-qr-panel" @click.stop>
<view class="pound-qr-panel__title">{{ title }}</view>
<view v-if="loading" class="pound-qr-panel__loading">加载中...</view>
<view v-else-if="row" class="pound-slip-capture-inline">
<view class="capture-card">
<view class="capture-row capture-row-plate">
<view class="vehicle-plate">
<view class="vehicle-plate-dot vehicle-plate-dot-lt" />
<view class="vehicle-plate-dot vehicle-plate-dot-lb" />
<text class="vehicle-plate-text">{{ row.vehicleNo || '未填车牌' }}</text>
<view class="vehicle-plate-dot vehicle-plate-dot-rt" />
<view class="vehicle-plate-dot vehicle-plate-dot-rb" />
</view>
</view>
<view class="capture-row capture-row-large">
<text class="label">往来单位:</text>
<text class="value">{{ row.partnerCompany || '--' }}</text>
</view>
<view class="capture-row capture-row-large">
<text class="label">物料名称:</text>
<text class="value">{{ productLine }}</text>
</view>
<view v-if="remarksText" class="capture-row">
<text class="label">备注:</text>
<text class="value">{{ remarksText }}</text>
</view>
</view>
<view class="capture-card capture-qrcode">
<view class="capture-id">
{{ row.date || '' }}
{{ row.businessType === '出库' ? '出库单ID:' : '入库单ID:' }}{{ row.id }}
</view>
<view class="capture-qr-wrap">
<image v-if="qrPath" class="capture-qr-img" :src="qrPath" mode="aspectFit" />
</view>
<view class="capture-desc">出入库单标识(扫码核验)</view>
</view>
</view>
<!-- 隐藏 canvas,供 uqrcodejs 绘制 -->
<canvas
canvas-id="poundQrCanvas"
id="poundQrCanvas"
class="pound-qr-canvas"
:style="{ width: canvasSize + 'px', height: canvasSize + 'px' }"
/>
</view>
</view>
</template>
<script setup>
import { computed, getCurrentInstance, ref, watch } from 'vue'
import { inventoryApi } from '@/pages/db/api/db'
import {
formatPoundQrProductLine,
normalizePoundQrRemarks,
poundQrDialogTitle
} from '@/utils/dbPoundQr'
import { makeQrTempFile } from '@/utils/makeQrTempFile'
const props = defineProps({
show: Boolean,
inventoryId: { type: String, default: '' },
/** 列表行快照(可选);仍会按需 get 补全 */
snapshot: { type: Object, default: null }
})
const emit = defineEmits(['close', 'update:show'])
const loading = ref(false)
const row = ref(null)
const qrPath = ref('')
const canvasSize = 200
const instance = getCurrentInstance()
const title = computed(() => poundQrDialogTitle(row.value?.businessType))
const productLine = computed(() =>
row.value ? formatPoundQrProductLine(row.value, '吨') : ''
)
const remarksText = computed(() => (row.value ? normalizePoundQrRemarks(row.value) : ''))
function close() {
emit('update:show', false)
emit('close')
}
function reset() {
row.value = null
qrPath.value = ''
loading.value = false
}
async function load() {
const id = String(props.inventoryId || '').trim()
if (!id) {
uni.showToast({ title: '缺少单据ID', icon: 'none' })
close()
return
}
loading.value = true
row.value = null
qrPath.value = ''
try {
let data = props.snapshot && props.snapshot.id === id ? { ...props.snapshot } : null
if (!data || data.quantity == null || !data.partnerCompany) {
const res = await inventoryApi.get(id)
data = res?.data ?? res
}
if (!data || !data.id) throw new Error('单据不存在')
row.value = data
await new Promise((r) => setTimeout(r, 50))
const proxy = instance?.proxy
qrPath.value = await makeQrTempFile({
text: data.id,
canvasId: 'poundQrCanvas',
componentInstance: proxy,
size: canvasSize
})
} catch (e) {
uni.showToast({ title: e?.message || '加载二维码失败', icon: 'none' })
close()
} finally {
loading.value = false
}
}
watch(
() => [props.show, props.inventoryId],
([show]) => {
if (show) load()
else reset()
}
)
</script>
<style scoped lang="scss">
.pound-qr-mask {
position: fixed;
inset: 0;
z-index: 1100;
background: rgba(0, 0, 0, 0.92);
display: flex;
align-items: center;
justify-content: center;
padding: 24px 16px;
box-sizing: border-box;
}
.pound-qr-panel {
width: 100%;
max-width: 360px;
max-height: 90vh;
overflow: auto;
background: #f5f7fa;
border-radius: 12px;
position: relative;
}
.pound-qr-panel__title {
text-align: center;
font-size: 17px;
font-weight: 700;
color: #1e293b;
padding: 14px 12px 6px;
}
.pound-qr-panel__loading {
text-align: center;
color: #9ca3af;
padding: 40px 0;
font-size: 14px;
}
.pound-qr-canvas {
position: fixed;
left: -9999px;
top: 0;
width: 200px;
height: 200px;
}
.pound-slip-capture-inline {
background: #f5f7fa;
padding: 4px 8px 12px;
margin: 0 6px 10px;
}
.capture-card {
background: #fff;
border-radius: 8px;
margin: 10px;
padding: 6px 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}
.capture-row {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 4px;
font-size: 13px;
}
.capture-row .label {
color: #1e293b;
min-width: 82px;
font-size: 12px;
font-weight: 400;
}
.capture-row .value {
color: #1e293b;
flex: 1;
font-size: 13px;
}
.capture-row-large .value {
font-size: 15px;
font-weight: 600;
}
.capture-row-plate {
width: 100%;
margin-bottom: 8px;
}
.vehicle-plate {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 10px 32px;
box-sizing: border-box;
background: #f5d000;
border: 3px solid #fff;
border-radius: 8px;
box-shadow: 0 0 0 2px #fff;
}
.vehicle-plate-text {
font-size: 24px;
font-weight: 700;
color: #000;
letter-spacing: 1px;
}
.vehicle-plate-dot {
position: absolute;
width: 8px;
height: 8px;
border-radius: 50%;
background: #fff;
}
.vehicle-plate-dot-lt {
left: 6px;
top: 8px;
}
.vehicle-plate-dot-lb {
left: 6px;
bottom: 8px;
}
.vehicle-plate-dot-rt {
right: 6px;
top: 8px;
}
.vehicle-plate-dot-rb {
right: 6px;
bottom: 8px;
}
.capture-qrcode {
text-align: center;
}
.capture-id {
font-size: 16px;
font-weight: 700;
color: #1e293b;
margin-bottom: 6px;
word-break: break-all;
padding: 0 4px;
}
.capture-qr-wrap {
padding: 8px 0;
min-height: 200px;
}
.capture-qr-img {
width: 200px;
height: 200px;
display: inline-block;
}
.capture-desc {
font-size: 11px;
font-weight: 500;
color: #64748b;
margin-top: 4px;
padding-bottom: 8px;
}
</style>