Files
gao-customer-h5/src/pages/customer/CustomerQrCode.vue
T
2026-08-24 17:53:21 +08:00

367 lines
8.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div v-can="permissionCodes.qrCode.create" class="qr-page">
<t-form class="form" label-align="left" v-entrance-transition>
<t-form-item label="编码前缀">
<t-input v-model.trim="formModel.prefix" placeholder="如 GAO" borderless />
</t-form-item>
<t-form-item label="起始编号">
<t-input
:model-value="formModel.start"
type="number"
inputmode="numeric"
borderless
@change="value => handleNumberChange('start', value)"
/>
</t-form-item>
<t-form-item label="编号位数" contentAlign="right">
<t-stepper
v-model="formModel.digits"
:min="startMinDigits"
:max="maxDigits"
default-value="3"
theme="outline"
/>
</t-form-item>
<t-form-item label="生成数量">
<t-input
:model-value="formModel.quantity"
type="number"
inputmode="numeric"
borderless
@change="value => handleNumberChange('quantity', value)"
/>
</t-form-item>
</t-form>
<t-cell-group theme="card" v-entrance-transition="70">
<t-cell title="最终起始编码" :note="finalStartCode" />
<t-cell title="最终结束编码" :note="finalEndCode" />
</t-cell-group>
<div class="actions" v-entrance-transition="140">
<t-button
class="bg-white"
theme="primary"
variant="outline"
block
:loading="printing"
@click="printQrCode"
>打印</t-button>
<t-button
theme="primary"
block
:loading="downloading"
@click="downloadImages"
>
<span v-text="downloadText" />
</t-button>
</div>
<div class="actions orange tips" v-entrance-transition="140">微信内可能无法发起打印或批量下载,建议使用默认浏览器打开</div>
<section class="preview" v-entrance-transition="210">
<div class="header">
<h2 class="title">预览</h2>
<p class="tips" v-text="previewSummary" />
</div>
<div class="paper" :style="paperStyle">
<div
class="item"
v-for="item in visiblePreviewPages[0].codes"
:key="item"
>
<div class="qr-code" />
<div class="text-mark bg-gray" />
</div>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { Message } from "@/utils/message";
import { calculateQrCodeGrid, downloadQrCodeImages, printQrCodeImages, splitQrCodePages } from "@/utils/qrCode";
import { permissionCodes } from "@/utils/permission";
defineOptions({
name: "CustomerQrCode"
});
interface QrCodeFormModel {
prefix: string;
start: string;
digits: number;
quantity: string;
}
const minDigits = 1;
const maxDigits = 16;
const defaultDigits = 3;
const storageKey = "management-qr-code-config";
const formModel = reactive<QrCodeFormModel>({
prefix: "GAO",
start: "1",
digits: defaultDigits,
quantity: "22"
});
const downloading = ref(false);
const printing = ref(false);
const codeList = computed(() => {
const start = Number(formModel.start || 0);
const digits = effectiveDigits.value;
const quantity = Number(formModel.quantity || 1);
return Array.from({ length: quantity }, (_, index) => {
const codeNumber = String(start + index).padStart(digits, "0");
return `${formModel.prefix}${codeNumber}`;
});
});
const startMinDigits = computed(() => Math.max(minDigits, Math.min(maxDigits, normalizeNumber(formModel.start).length || minDigits)));
const effectiveDigits = computed(() => Math.max(startMinDigits.value, Math.min(maxDigits, formModel.digits || defaultDigits)));
const maxCodeLength = computed(() => formModel.prefix.length + effectiveDigits.value);
const previewPages = computed(() => splitQrCodePages(codeList.value, maxCodeLength.value));
const visiblePreviewPages = computed(() => previewPages.value.slice(0, 1));
const paperGrid = computed(() => calculateQrCodeGrid(maxCodeLength.value));
const paperStyle = computed(() => ({
gridTemplateRows: `repeat(${paperGrid.value.rows}, minmax(0, 1fr))`,
gridTemplateColumns: `repeat(${paperGrid.value.columns}, minmax(0, 1fr))`
}));
const paperCount = computed(() => previewPages.value.length);
const finalStartCode = computed(() => codeList.value[0] || "");
const finalEndCode = computed(() => codeList.value[codeList.value.length - 1] || "");
const downloadText = computed(() => `下载图片(${paperCount.value} 张)`);
const previewSummary = computed(() => `共 ${paperCount.value} 张 A4,每张最多 ${paperGrid.value.pageSize} 枚,${paperGrid.value.columns} × ${paperGrid.value.rows} 网格`);
function normalizeNumber(value: string | number | undefined): string {
return String(value ?? "").replace(/\D/g, "");
}
function normalizeDigits(value: string | number | undefined, minValue = minDigits): number {
const nextValue = Number(normalizeNumber(value));
const validValue = Number.isFinite(nextValue) && 0 < nextValue ? nextValue : defaultDigits;
return Math.max(minValue, Math.min(maxDigits, validValue));
}
function loadLocalConfig(): void {
if (typeof window === "undefined") {
return;
}
const cache = window.localStorage.getItem(storageKey);
if (!cache) {
return;
}
try {
const config = JSON.parse(cache) as Partial<QrCodeFormModel>;
const start = normalizeNumber(config.start);
const startDigits = Math.max(minDigits, Math.min(maxDigits, start.length || minDigits));
formModel.prefix = String(config.prefix ?? formModel.prefix);
formModel.start = start;
formModel.digits = normalizeDigits(config.digits, startDigits);
formModel.quantity = normalizeNumber(config.quantity) || formModel.quantity;
} catch {
window.localStorage.removeItem(storageKey);
}
}
function saveLocalConfig(): void {
if (typeof window === "undefined") {
return;
}
window.localStorage.setItem(storageKey, JSON.stringify({
prefix: formModel.prefix,
start: formModel.start,
digits: formModel.digits,
quantity: formModel.quantity
}));
}
function handleNumberChange(key: "start" | "quantity", value: string | number | undefined): void {
const nextValue = normalizeNumber(value);
formModel[key] = nextValue;
}
loadLocalConfig();
watch(startMinDigits, value => {
if (formModel.digits < value) {
formModel.digits = value;
}
});
watch(formModel, () => {
saveLocalConfig();
}, {
deep: true
});
async function downloadImages(): Promise<void> {
if (downloading.value) {
return;
}
downloading.value = true;
try {
await downloadQrCodeImages({
codes: codeList.value,
filePrefix: `${finalStartCode.value}-${finalEndCode.value}`,
maxCodeLength: maxCodeLength.value
});
} finally {
downloading.value = false;
}
}
async function printQrCode(): Promise<void> {
if (printing.value) {
return;
}
printing.value = true;
try {
await printQrCodeImages({
codes: codeList.value,
maxCodeLength: maxCodeLength.value,
title: `${finalStartCode.value}-${finalEndCode.value}`
});
} catch (err) {
Message.warning(err instanceof Error ? err.message : "打印失败");
} finally {
printing.value = false;
}
}
</script>
<style scoped lang="less">
.qr-page {
gap: 1rem;
min-height: 100vh;
padding: calc(var(--app-nav-height) + 1rem) 0 2rem 0;
display: flex;
flex-direction: column;
.form {
padding: .5rem 0;
background: rgba(255, 255, 255, .92);
box-shadow: var(--td-shadow-1);
border-radius: 1.4rem;
:deep(.t-input__control) {
text-align: right;
}
}
.actions {
--td-button-border-radius: 1rem;
&.tips {
font-size: .875rem;
}
gap: .75rem;
display: flex;
margin: 0 1rem;
}
.preview {
padding: 0 1rem;
.header {
margin: 0 0 .75rem 0;
display: flex;
align-items: center;
justify-content: space-between;
.title {
color: #0f172a;
margin: 0;
font-size: 1rem;
font-weight: 700;
}
.tips {
color: #52607a;
margin: 0;
font-size: .8125rem;
}
}
.paper {
gap: 0;
width: min(100%, 28rem);
padding: .75rem;
display: grid;
margin: 0 auto;
aspect-ratio: 210 / 297;
background: #fff;
overflow: hidden;
box-shadow: 0 .25rem 1rem rgba(15, 23, 42, .2);
.item {
gap: .1875rem;
border: 1px dashed #cbd5e1;
padding: .25rem;
display: flex;
min-width: 0;
align-items: center;
flex-direction: column;
justify-content: center;
.qr-code {
width: 58%;
background: repeating-linear-gradient(45deg, #000 0 .125rem, #000 .125rem .25rem, #fff .25rem .375rem, #fff .375rem .5rem);
aspect-ratio: 1;
}
.text-mark {
width: 50%;
height: 3px;
}
}
}
}
}
:global(html.theme-dark) {
.qr-page {
.form {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
}
.code-range {
div {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
}
span {
color: #94a3b8;
}
strong {
color: #e2e8f0;
}
}
.preview-head {
h2 {
color: #e2e8f0;
}
p {
color: #94a3b8;
}
}
.paper {
background: #f8fafc;
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
}
}
}
</style>