From 10fbd0390c2f37eabfb3416827d80f98af5e4461 Mon Sep 17 00:00:00 2001 From: andy Date: Thu, 18 Jun 2026 10:58:44 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AC=AC=E4=BA=8C=E6=AC=A1=E8=BF=AD=E4=BB=A3?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +- src/api/mock.ts | 1 + src/styles/main.css | 30 +++++++++++++- src/types/order.ts | 1 + src/utils/writeOffCode.ts | 32 +++++++++++++++ src/views/home/HomeView.vue | 8 ++-- src/views/login/LoginView.vue | 60 +++++++++++++++++++++++++++- src/views/orders/OrderDetailView.vue | 12 +++++- src/views/verify/VerifyView.vue | 49 +++++++++++++++++++---- yarn.lock | 5 +++ 10 files changed, 183 insertions(+), 18 deletions(-) create mode 100644 src/utils/writeOffCode.ts diff --git a/package.json b/package.json index 1305d55..f4b60b6 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@vitejs/plugin-vue": "^5.2.4", "axios": "^1.10.0", "dayjs": "^1.11.13", + "jsqr": "^1.4.0", "lucide-vue-next": "^0.468.0", "pinia": "^2.3.1", "vant": "^4.9.16", @@ -25,8 +26,8 @@ "vue-router": "^4.5.0" }, "devDependencies": { - "@types/node": "^22.13.1", "@tsconfig/node22": "^22.0.0", + "@types/node": "^22.13.1", "@vue/tsconfig": "^0.7.0", "typescript": "^5.7.3", "vite": "^6.0.7", diff --git a/src/api/mock.ts b/src/api/mock.ts index eb2e772..8c622be 100644 --- a/src/api/mock.ts +++ b/src/api/mock.ts @@ -238,6 +238,7 @@ export const mockApi = { { id: `WO${order.id}`, orderId: order.id, + commodityName: order.commodityName, packageName: order.commodityName, userName: '当前员工', createTime: order.writeOffTime diff --git a/src/styles/main.css b/src/styles/main.css index dbda731..f313e61 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -39,13 +39,16 @@ html, body, #app { + width: 100%; + max-width: 100%; min-height: 100%; margin: 0; background: var(--app-bg); + overflow-x: hidden; } body { - min-width: 320px; + min-width: 0; } button, @@ -58,6 +61,12 @@ button { color: inherit; } +img, +video, +canvas { + max-width: 100%; +} + .app-shell { min-height: 100vh; padding: 0 14px 78px; @@ -76,6 +85,7 @@ button { .page { width: min(100%, 560px); + min-width: 0; margin: 0 auto; padding: 14px 0 18px; } @@ -162,6 +172,7 @@ button { .panel { overflow: hidden; + min-width: 0; border: 1px solid rgba(226, 232, 228, 0.9); border-radius: var(--radius); background: var(--surface); @@ -292,7 +303,7 @@ button { .order-card__main { display: grid; - grid-template-columns: 76px 1fr; + grid-template-columns: 76px minmax(0, 1fr); gap: 10px; } @@ -334,14 +345,29 @@ button { line-height: 1.5; } +.meta-row > *, +.detail-row > * { + min-width: 0; + overflow-wrap: anywhere; +} + +.meta-row span, +.detail-row span { + flex: 1 1 auto; +} + .meta-row strong, .detail-row strong { + flex: 0 1 auto; color: var(--text-main); font-weight: 650; + text-align: right; + overflow-wrap: anywhere; } .stack { display: flex; + min-width: 0; flex-direction: column; gap: 8px; } diff --git a/src/types/order.ts b/src/types/order.ts index b303b0a..3da08b4 100644 --- a/src/types/order.ts +++ b/src/types/order.ts @@ -62,6 +62,7 @@ export interface CommodityPackageConfig { export interface WriteOffRecord { id?: string orderId?: string + commodityName?: string packageName?: string userId?: string userName?: string diff --git a/src/utils/writeOffCode.ts b/src/utils/writeOffCode.ts new file mode 100644 index 0000000..5c3b914 --- /dev/null +++ b/src/utils/writeOffCode.ts @@ -0,0 +1,32 @@ +export interface WriteOffCodePayload { + orderId: string + packageName?: string +} + +const safeDecode = (value: string) => { + try { + return decodeURIComponent(value) + } catch { + return value + } +} + +export const parseWriteOffCode = (rawValue: string): WriteOffCodePayload => { + const raw = rawValue.trim() + if (!raw) { + throw new Error('未识别到核销码') + } + + const splitIndex = raw.indexOf('&') + const orderId = safeDecode(splitIndex >= 0 ? raw.slice(0, splitIndex) : raw).trim() + const packageName = splitIndex >= 0 ? safeDecode(raw.slice(splitIndex + 1)).trim() : '' + + if (!orderId) { + throw new Error('核销码缺少订单号') + } + + return { + orderId, + packageName: packageName || undefined + } +} diff --git a/src/views/home/HomeView.vue b/src/views/home/HomeView.vue index 08daa30..2cd5bdf 100644 --- a/src/views/home/HomeView.vue +++ b/src/views/home/HomeView.vue @@ -28,7 +28,7 @@ const events = ref([]) const loading = ref(false) const todoOrders = computed(() => orders.value.filter((item) => item.orderStatus === '2')) -const doneOrders = computed(() => orders.value.filter((item) => item.orderStatus === '6')) +const confirmOrders = computed(() => orders.value.filter((item) => item.orderStatus === '1')) const activeEvents = computed(() => events.value.filter((item) => item.showStatusDesc === '生效中')) const loadData = async () => { @@ -69,9 +69,9 @@ onMounted(loadData)
可进入详情核销
-
今日核销
-
{{ doneOrders.length }}
-
含列表记录
+
待确认订单
+
{{ confirmOrders.length }}
+
需要员工处理
生效事件
diff --git a/src/views/login/LoginView.vue b/src/views/login/LoginView.vue index 0f1e427..5e5a3a1 100644 --- a/src/views/login/LoginView.vue +++ b/src/views/login/LoginView.vue @@ -136,8 +136,13 @@ const handleLogin = async () => { diff --git a/src/views/orders/OrderDetailView.vue b/src/views/orders/OrderDetailView.vue index 39464b9..c23ba6f 100644 --- a/src/views/orders/OrderDetailView.vue +++ b/src/views/orders/OrderDetailView.vue @@ -128,7 +128,8 @@ onMounted(loadDetail)
- {{ record.packageName || detail.commodityName }} + {{ record.commodityName || detail.commodityName }} +

套餐名称:{{ record.packageName }}

{{ record.createTime }} · {{ record.userName || '员工' }}

@@ -156,11 +157,17 @@ onMounted(loadDetail) .order-hero { display: grid; - grid-template-columns: 112px 1fr; + grid-template-columns: 112px minmax(0, 1fr); gap: 12px; padding: 12px; } +.order-hero__content, +.package-row div, +.record-row div { + min-width: 0; +} + .order-hero img { width: 112px; height: 112px; @@ -215,6 +222,7 @@ onMounted(loadDetail) .package-row strong, .record-row strong { color: var(--text-strong); + overflow-wrap: anywhere; } .package-row p, diff --git a/src/views/verify/VerifyView.vue b/src/views/verify/VerifyView.vue index 4f2f643..4b61754 100644 --- a/src/views/verify/VerifyView.vue +++ b/src/views/verify/VerifyView.vue @@ -3,6 +3,7 @@ import { nextTick, onBeforeUnmount, ref } from 'vue' import { useRouter } from 'vue-router' import { showToast } from 'vant' import { QrCode, X } from 'lucide-vue-next' +import jsQR from 'jsqr' import { parseWriteOffCode, type WriteOffCodePayload } from '@/utils/writeOffCode' const router = useRouter() @@ -13,7 +14,10 @@ const scanError = ref('') let stream: MediaStream | null = null let detector: { detect: (source: HTMLVideoElement) => Promise> } | null = null +let canvas: HTMLCanvasElement | null = null +let canvasContext: CanvasRenderingContext2D | null = null let frameId = 0 +let lastDecodeAt = 0 const getBarcodeDetector = () => { return (window as unknown as { @@ -36,6 +40,8 @@ const stopScan = () => { if (videoRef.value) { videoRef.value.srcObject = null } + detector = null + lastDecodeAt = 0 } const goConfirm = (payload: WriteOffCodePayload) => { @@ -54,14 +60,41 @@ const handleCodeValue = (rawValue: string) => { goConfirm(payload) } +const detectWithBarcodeDetector = async (video: HTMLVideoElement) => { + if (!detector) return '' + const results = await detector.detect(video) + return results[0]?.rawValue || '' +} + +const detectWithJsQr = (video: HTMLVideoElement) => { + const width = video.videoWidth + const height = video.videoHeight + if (!width || !height) return '' + + if (!canvas) { + canvas = document.createElement('canvas') + } + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width + canvas.height = height + canvasContext = canvas.getContext('2d', { willReadFrequently: true }) + } + if (!canvasContext) return '' + + canvasContext.drawImage(video, 0, 0, width, height) + const imageData = canvasContext.getImageData(0, 0, width, height) + return jsQR(imageData.data, width, height, { inversionAttempts: 'dontInvert' })?.data || '' +} + const detectLoop = async () => { - if (!scanning.value || !videoRef.value || !detector) return + if (!scanning.value || !videoRef.value) return const video = videoRef.value try { - if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) { - const results = await detector.detect(video) - const value = results[0]?.rawValue + const now = Date.now() + if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && now - lastDecodeAt > 120) { + lastDecodeAt = now + const value = detector ? await detectWithBarcodeDetector(video) : detectWithJsQr(video) if (value) { handleCodeValue(value) return @@ -79,16 +112,16 @@ const detectLoop = async () => { const startScan = async () => { if (scanning.value || loading.value) return const BarcodeDetector = getBarcodeDetector() - if (!BarcodeDetector) { - scanError.value = '当前浏览器不支持摄像头扫码' - showToast('当前浏览器不支持摄像头扫码') + if (!navigator.mediaDevices?.getUserMedia) { + scanError.value = '当前浏览器无法打开摄像头' + showToast('当前浏览器无法打开摄像头') return } loading.value = true scanError.value = '' try { - detector = new BarcodeDetector({ formats: ['qr_code'] }) + detector = BarcodeDetector ? new BarcodeDetector({ formats: ['qr_code'] }) : null stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: { diff --git a/yarn.lock b/yarn.lock index 3eea8cb..ade11b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -703,6 +703,11 @@ https-proxy-agent@^5.0.1: agent-base "6" debug "4" +jsqr@^1.4.0: + version "1.4.0" + resolved "https://registry.npmmirror.com/jsqr/-/jsqr-1.4.0.tgz#8efb8d0a7cc6863cb6d95116b9069123ce9eb2d1" + integrity sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A== + lucide-vue-next@^0.468.0: version "0.468.0" resolved "https://registry.npmmirror.com/lucide-vue-next/-/lucide-vue-next-0.468.0.tgz#5e3c84b534f43d5e82fb757b91933cfb4ab24bf9"