refactor file tree

This commit is contained in:
Timi
2026-08-11 15:24:36 +08:00
parent 575d41b2e2
commit 819c1623eb
42 changed files with 86 additions and 86 deletions
+361
View File
@@ -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>