refactor file tree
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
<template>
|
||||
<div v-can="permissionCodes.qrCode.create" class="qr-page">
|
||||
<t-form class="form" label-align="left">
|
||||
<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">
|
||||
<t-cell title="最终起始编码" :note="finalStartCode" />
|
||||
<t-cell title="最终结束编码" :note="finalEndCode" />
|
||||
</t-cell-group>
|
||||
|
||||
<section class="preview">
|
||||
<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 class="actions">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Toast } from "tdesign-mobile-vue";
|
||||
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) {
|
||||
Toast({
|
||||
theme: "warning",
|
||||
direction: "row",
|
||||
message: 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 6rem 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;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
gap: .75rem;
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
||||
display: grid;
|
||||
position: fixed;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
: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>
|
||||
|
||||
Reference in New Issue
Block a user