<script lang="ts" setup>
|
import type { ListPrintColumn, ListPrintPage } from '../helpers/listPrint';
|
|
import { computed, nextTick, reactive, ref } from 'vue';
|
|
import { useMessage } from '@jnpf/hooks';
|
import { BasicModal, useModalInner } from '@jnpf/ui/modal';
|
|
import { buildListPrintDocument, getListPrintTotalPages, validateListPrintPageRange } from '../helpers/listPrint';
|
|
interface FetchPageResult {
|
list: Record<string, any>[];
|
total: number;
|
}
|
|
interface ListPrintModalOptions {
|
columns: ListPrintColumn[];
|
defaultPageSize?: number;
|
direction?: 'landscape' | 'portrait';
|
fetchPage: (currentPage: number, pageSize: number) => Promise<FetchPageResult>;
|
paperType?: 'A3' | 'A4';
|
title?: string;
|
}
|
|
interface State {
|
columns: ListPrintColumn[];
|
direction: 'landscape' | 'portrait';
|
endPage: number;
|
fetchPage: ListPrintModalOptions['fetchPage'] | null;
|
loading: boolean;
|
pageSize: number;
|
paperType: 'A3' | 'A4';
|
startPage: number;
|
title: string;
|
total: number;
|
}
|
|
const { createMessage } = useMessage();
|
const [registerModal, { changeOkLoading, closeModal }] = useModalInner(init);
|
const state = reactive<State>({
|
columns: [],
|
direction: 'landscape',
|
endPage: 1,
|
fetchPage: null,
|
loading: false,
|
pageSize: 20,
|
paperType: 'A3',
|
startPage: 1,
|
title: '列表报表',
|
total: 0,
|
});
|
const requestId = ref(0);
|
const pageSizeOptions = [10, 20, 30, 50, 100].map((value) => ({ label: `${value} 条/页`, value }));
|
const totalPages = computed(() => getListPrintTotalPages(state.total, state.pageSize));
|
|
async function init(options: ListPrintModalOptions) {
|
const currentRequestId = ++requestId.value;
|
state.columns = options.columns || [];
|
state.direction = options.direction || 'landscape';
|
state.fetchPage = options.fetchPage;
|
state.pageSize = options.defaultPageSize || 20;
|
state.paperType = options.paperType || 'A3';
|
state.startPage = 1;
|
state.endPage = 1;
|
state.title = options.title || '列表报表';
|
state.total = 0;
|
state.loading = true;
|
changeOkLoading(true);
|
|
try {
|
const result = await state.fetchPage(1, state.pageSize);
|
if (currentRequestId !== requestId.value) return;
|
state.total = Number(result.total) || 0;
|
state.endPage = getListPrintTotalPages(state.total, state.pageSize);
|
} catch {
|
if (currentRequestId === requestId.value) createMessage.error('读取打印数据失败');
|
} finally {
|
if (currentRequestId === requestId.value) {
|
state.loading = false;
|
changeOkLoading(false);
|
}
|
}
|
}
|
|
function handlePageSizeChange() {
|
state.startPage = 1;
|
state.endPage = totalPages.value;
|
}
|
|
async function handleSubmit() {
|
if (!state.fetchPage) return;
|
if (!state.columns.length) return createMessage.warning('当前列表没有可打印字段');
|
if (!state.total) return createMessage.warning('当前查询没有可打印数据');
|
|
const rangeError = validateListPrintPageRange(state.startPage, state.endPage, totalPages.value);
|
if (rangeError) return createMessage.warning(rangeError);
|
|
state.loading = true;
|
changeOkLoading(true);
|
try {
|
const pages: ListPrintPage[] = [];
|
for (let pageNumber = state.startPage; pageNumber <= state.endPage; pageNumber++) {
|
const result = await state.fetchPage(pageNumber, state.pageSize);
|
pages.push({ pageNumber, rows: result.list || [] });
|
}
|
|
const html = buildListPrintDocument({
|
columns: state.columns,
|
direction: state.direction,
|
pageSize: state.pageSize,
|
pages,
|
paperType: state.paperType,
|
title: state.title,
|
total: state.total,
|
});
|
await printDocument(html);
|
closeModal();
|
} catch (error) {
|
console.error('[ListPrintModal] 打印失败:', error);
|
createMessage.error('生成打印报表失败');
|
} finally {
|
state.loading = false;
|
changeOkLoading(false);
|
}
|
}
|
|
function printDocument(html: string) {
|
return new Promise<void>((resolve, reject) => {
|
const iframe = document.createElement('iframe');
|
iframe.setAttribute('aria-hidden', 'true');
|
Object.assign(iframe.style, {
|
border: '0',
|
height: '1px',
|
position: 'fixed',
|
right: '0',
|
top: '0',
|
visibility: 'hidden',
|
width: '1px',
|
});
|
|
const cleanup = () => {
|
setTimeout(() => iframe.remove(), 100);
|
resolve();
|
};
|
|
iframe.addEventListener('load', () => {
|
const printWindow = iframe.contentWindow;
|
if (!printWindow) {
|
iframe.remove();
|
reject(new Error('无法创建打印窗口'));
|
return;
|
}
|
printWindow.addEventListener('afterprint', cleanup, { once: true });
|
nextTick(() => {
|
printWindow.focus();
|
printWindow.print();
|
setTimeout(cleanup, 1000);
|
});
|
});
|
|
document.body.append(iframe);
|
iframe.srcdoc = html;
|
});
|
}
|
</script>
|
|
<template>
|
<BasicModal
|
v-bind="$attrs"
|
@register="registerModal"
|
title="打印设置"
|
ok-text="打印"
|
:width="560"
|
:confirm-loading="state.loading"
|
destroy-on-close
|
@ok="handleSubmit">
|
<a-form :model="state" :colon="false" :label-col="{ style: { width: '96px' } }">
|
<a-form-item label="报表名称">
|
<a-input v-model:value="state.title" :maxlength="100" />
|
</a-form-item>
|
<a-form-item label="每页条数">
|
<a-select v-model:value="state.pageSize" :options="pageSizeOptions" @change="handlePageSizeChange" />
|
</a-form-item>
|
<a-form-item label="打印页码">
|
<div class="page-range">
|
<a-input-number v-model:value="state.startPage" :min="1" :max="totalPages" :precision="0" />
|
<span>至</span>
|
<a-input-number v-model:value="state.endPage" :min="1" :max="totalPages" :precision="0" />
|
<span class="page-total">共 {{ totalPages }} 页,{{ state.total }} 条</span>
|
</div>
|
</a-form-item>
|
<a-form-item label="纸张类型">
|
<a-radio-group v-model:value="state.paperType" button-style="solid">
|
<a-radio-button value="A4">A4</a-radio-button>
|
<a-radio-button value="A3">A3</a-radio-button>
|
</a-radio-group>
|
</a-form-item>
|
<a-form-item label="纸张方向">
|
<a-radio-group v-model:value="state.direction" button-style="solid">
|
<a-radio-button value="portrait">纵向</a-radio-button>
|
<a-radio-button value="landscape">横向</a-radio-button>
|
</a-radio-group>
|
</a-form-item>
|
</a-form>
|
</BasicModal>
|
</template>
|
|
<style lang="scss" scoped>
|
.page-range {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
}
|
|
.page-total {
|
color: var(--text-color-secondary);
|
white-space: nowrap;
|
}
|
</style>
|