|
|
@@ -0,0 +1,510 @@
|
|
|
+<template>
|
|
|
+ <div class="wide-table-container">
|
|
|
+ <!-- 操作按钮区域 -->
|
|
|
+ <!-- <div class="controls" v-if="showControls">
|
|
|
+ <el-button size="small" type="primary" icon="el-icon-printer" @click="printTable">打印表格</el-button>
|
|
|
+ <button @click="togglePreview" class="preview-btn">{{ isPreviewing ? '关闭预览' : '打印预览' }}</button>
|
|
|
+ <label class="orientation-toggle">
|
|
|
+ <input type="checkbox" v-model="landscape"> 横向打印
|
|
|
+ </label>
|
|
|
+ </div> -->
|
|
|
+
|
|
|
+ <!-- 打印预览区域 -->
|
|
|
+ <div class="preview-container" v-if="isPreviewing">
|
|
|
+ <div class="preview-content" :style="previewStyle">
|
|
|
+ <div class="print-header">
|
|
|
+ <h2>{{ printTitle }}</h2>
|
|
|
+ <div class="print-meta">打印时间: {{ new Date().toLocaleString() }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="table-wrapper" ref="tableWrapper">
|
|
|
+ <table class="wide-table" ref="wideTable">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th v-for="(col, index) in columns" :key="index"
|
|
|
+ :style="getColumnStyle(col)"
|
|
|
+ :class="{ 'fixed-column': col.fixed }">
|
|
|
+ {{ col.label }}
|
|
|
+ </th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <tr v-for="(row, rowIndex) in paginatedData" :key="rowIndex">
|
|
|
+ <td v-for="(col, colIndex) in columns" :key="colIndex"
|
|
|
+ :style="getColumnStyle(col)"
|
|
|
+ :class="{
|
|
|
+ 'fixed-column': col.fixed,
|
|
|
+ 'text-center': col.align === 'center',
|
|
|
+ 'text-right': col.align === 'right'
|
|
|
+ }">
|
|
|
+ <template v-if="col.formatter">
|
|
|
+ {{ col.formatter(row[col.prop], row) }}
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ {{ row[col.prop] }}
|
|
|
+ </template>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ <div class="page-footer">
|
|
|
+ 第 {{ currentPage }} 页,共 {{ totalPages }} 页
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="preview-controls">
|
|
|
+ <button @click="prevPage" :disabled="currentPage === 1">上一页</button>
|
|
|
+ <span>第 {{ currentPage }} 页 / 共 {{ totalPages }} 页</span>
|
|
|
+ <button @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 实际表格渲染区域(用于打印) -->
|
|
|
+ <div v-show="false" class="print-source">
|
|
|
+ <div v-for="page in totalPages" :key="page" class="print-page" :style="printPageStyle">
|
|
|
+ <div class="print-header">
|
|
|
+ <h2>{{ printTitle }}</h2>
|
|
|
+ <div class="print-meta">打印时间: {{ new Date().toLocaleString() }}</div>
|
|
|
+ </div>
|
|
|
+ <table class="wide-table">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th v-for="(col, index) in columns" :key="index"
|
|
|
+ :style="getColumnStyle(col)"
|
|
|
+ :class="{ 'fixed-column': col.fixed }">
|
|
|
+ {{ col.label }}
|
|
|
+ </th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <tr v-for="(row, rowIndex) in getPageData(page)" :key="rowIndex">
|
|
|
+ <td v-for="(col, colIndex) in columns" :key="colIndex"
|
|
|
+ :style="getColumnStyle(col)"
|
|
|
+ :class="{
|
|
|
+ 'fixed-column': col.fixed,
|
|
|
+ 'text-center': col.align === 'center',
|
|
|
+ 'text-right': col.align === 'right'
|
|
|
+ }">
|
|
|
+ <template v-if="col.formatter">
|
|
|
+ {{ col.formatter(row[col.prop], row) }}
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ {{ row[col.prop] }}
|
|
|
+ </template>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ <div class="page-footer">
|
|
|
+ 第 {{ page }} 页,共 {{ totalPages }} 页
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+export default {
|
|
|
+ name: 'WideTablePrinter',
|
|
|
+ props: {
|
|
|
+ columns: {
|
|
|
+ type: Array,
|
|
|
+ required: true,
|
|
|
+ validator: value => value.every(col => col.prop && col.label)
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ type: Array,
|
|
|
+ required: true
|
|
|
+ },
|
|
|
+ printTitle: {
|
|
|
+ type: String,
|
|
|
+ default: '宽形表格打印'
|
|
|
+ },
|
|
|
+ showControls: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true
|
|
|
+ },
|
|
|
+ rowsPerPage: {
|
|
|
+ type: Number,
|
|
|
+ default: 30
|
|
|
+ },
|
|
|
+ defaultLandscape: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true
|
|
|
+ }
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ landscape: this.defaultLandscape,
|
|
|
+ isPreviewing: false,
|
|
|
+ currentPage: 1,
|
|
|
+ columnWidths: {},
|
|
|
+ tableWidth: 0
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ totalPages() {
|
|
|
+ return Math.ceil(this.data.length / this.rowsPerPage)
|
|
|
+ },
|
|
|
+ paginatedData() {
|
|
|
+ const start = (this.currentPage - 1) * this.rowsPerPage
|
|
|
+ const end = start + this.rowsPerPage
|
|
|
+ return this.data.slice(start, end)
|
|
|
+ },
|
|
|
+ previewStyle() {
|
|
|
+ return {
|
|
|
+ width: this.landscape ? 'calc(297mm - 40px)' : 'calc(210mm - 40px)',
|
|
|
+ height: this.landscape ? 'calc(210mm - 40px)' : 'calc(297mm - 40px)',
|
|
|
+ transform: `scale(${this.getPreviewScale()})`
|
|
|
+ }
|
|
|
+ },
|
|
|
+ printPageStyle() {
|
|
|
+ return {
|
|
|
+ size: this.landscape ? 'landscape' : 'portrait',
|
|
|
+ width: this.landscape ? '100%' : '100%',
|
|
|
+ height: this.landscape ? '210mm' : '297mm'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.calculateColumnWidths()
|
|
|
+ window.addEventListener('resize', this.calculateColumnWidths)
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ window.removeEventListener('resize', this.calculateColumnWidths)
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ getColumnStyle(col) {
|
|
|
+ const style = {}
|
|
|
+ if (this.columnWidths[col.prop]) {
|
|
|
+ style.width = `${this.columnWidths[col.prop]}px`
|
|
|
+ style.minWidth = `${this.columnWidths[col.prop]}px`
|
|
|
+ } else if (col.width) {
|
|
|
+ style.width = typeof col.width === 'number' ? `${col.width}px` : col.width
|
|
|
+ }
|
|
|
+ if (col.minWidth) {
|
|
|
+ style.minWidth = typeof col.minWidth === 'number' ? `${col.minWidth}px` : col.minWidth
|
|
|
+ }
|
|
|
+ if (col.align === 'center') style.textAlign = 'center'
|
|
|
+ if (col.align === 'right') style.textAlign = 'right'
|
|
|
+ return style
|
|
|
+ },
|
|
|
+ calculateColumnWidths() {
|
|
|
+ if (!this.$refs.wideTable || !this.isPreviewing) return
|
|
|
+
|
|
|
+ const table = this.$refs.wideTable
|
|
|
+ const wrapper = this.$refs.tableWrapper
|
|
|
+ const availableWidth = wrapper.clientWidth - 20 // 减去滚动条和边距
|
|
|
+
|
|
|
+ // 计算内容宽度
|
|
|
+ let contentWidth = 0
|
|
|
+ const cols = table.querySelectorAll('th')
|
|
|
+ cols.forEach(th => {
|
|
|
+ contentWidth += th.scrollWidth
|
|
|
+ })
|
|
|
+
|
|
|
+ // 如果内容宽度小于可用宽度,按比例扩展
|
|
|
+ if (contentWidth < availableWidth) {
|
|
|
+ const scale = availableWidth / contentWidth
|
|
|
+ cols.forEach(th => {
|
|
|
+ const colKey = this.columns[th.cellIndex].key
|
|
|
+ this.$set(this.columnWidths, colKey, th.scrollWidth * scale)
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ // 否则使用实际宽度
|
|
|
+ cols.forEach(th => {
|
|
|
+ const colKey = this.columns[th.cellIndex].key
|
|
|
+ this.$set(this.columnWidths, colKey, th.scrollWidth)
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ this.tableWidth = contentWidth > availableWidth ? contentWidth : availableWidth
|
|
|
+ },
|
|
|
+ getPageData(page) {
|
|
|
+ const start = (page - 1) * this.rowsPerPage
|
|
|
+ const end = start + this.rowsPerPage
|
|
|
+ return this.data.slice(start, end)
|
|
|
+ },
|
|
|
+ getPreviewScale() {
|
|
|
+ if (!this.$refs.tableWrapper) return 1
|
|
|
+
|
|
|
+ const containerWidth = this.$refs.tableWrapper.clientWidth - 20
|
|
|
+ const containerHeight = this.$refs.tableWrapper.clientHeight - 100
|
|
|
+
|
|
|
+ const pageWidth = this.landscape ? 297 : 210 // mm
|
|
|
+ const pageHeight = this.landscape ? 210 : 297 // mm
|
|
|
+
|
|
|
+ // 转换为像素 (1mm ≈ 3.78px)
|
|
|
+ const pageWidthPx = pageWidth * 3.78
|
|
|
+ const pageHeightPx = pageHeight * 3.78
|
|
|
+
|
|
|
+ const widthScale = containerWidth / pageWidthPx
|
|
|
+ const heightScale = containerHeight / pageHeightPx
|
|
|
+
|
|
|
+ return Math.min(widthScale, heightScale, 1)
|
|
|
+ },
|
|
|
+ printTable() {
|
|
|
+ const printWindow = window.open('', '_blank')
|
|
|
+
|
|
|
+ printWindow.document.open()
|
|
|
+ printWindow.document.write(`
|
|
|
+ <!DOCTYPE html>
|
|
|
+ <html>
|
|
|
+ <head>
|
|
|
+ <title>${this.printTitle}</title>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <style>
|
|
|
+ body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
|
|
|
+ .print-page {
|
|
|
+ page-break-after: always;
|
|
|
+ width: ${this.landscape ? '297mm' : '210mm'};
|
|
|
+ height: ${this.landscape ? '210mm' : '297mm'};
|
|
|
+ padding: 10mm;
|
|
|
+ box-sizing: border-box;
|
|
|
+ margin: 0 auto;
|
|
|
+ }
|
|
|
+ .print-page:last-child {
|
|
|
+ page-break-after: auto;
|
|
|
+ }
|
|
|
+ .print-header {
|
|
|
+ text-align: center;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ }
|
|
|
+ .print-header h2 {
|
|
|
+ margin: 0 0 5px 0;
|
|
|
+ font-size: 18pt;
|
|
|
+ }
|
|
|
+ .print-meta {
|
|
|
+ font-size: 10pt;
|
|
|
+ color: #666;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ }
|
|
|
+ .wide-table {
|
|
|
+ width: 100%;
|
|
|
+ border-collapse: collapse;
|
|
|
+ font-size: 10pt;
|
|
|
+ table-layout: fixed;
|
|
|
+ }
|
|
|
+ .wide-table th, .wide-table td {
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ padding: 5px 8px;
|
|
|
+ word-break: break-word;
|
|
|
+ overflow-wrap: break-word;
|
|
|
+ }
|
|
|
+ .wide-table th {
|
|
|
+ background-color: #f5f5f5;
|
|
|
+ font-weight: bold;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+ .wide-table .fixed-column {
|
|
|
+ position: sticky;
|
|
|
+ left: 0;
|
|
|
+ background-color: white;
|
|
|
+ z-index: 1;
|
|
|
+ }
|
|
|
+ .text-center { text-align: center; }
|
|
|
+ .text-right { text-align: right; }
|
|
|
+ .page-footer {
|
|
|
+ margin-top: 10px;
|
|
|
+ font-size: 10pt;
|
|
|
+ text-align: center;
|
|
|
+ color: #666;
|
|
|
+ }
|
|
|
+ @page {
|
|
|
+ size: ${this.landscape ? 'A4 landscape' : 'A4 portrait'};
|
|
|
+ margin: 10mm;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+ </head>
|
|
|
+ <body>
|
|
|
+ ${document.querySelector('.print-source').innerHTML}
|
|
|
+ </body>
|
|
|
+ </html>
|
|
|
+ `)
|
|
|
+ printWindow.document.close()
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ printWindow.print()
|
|
|
+ printWindow.close()
|
|
|
+ }, 300)
|
|
|
+ },
|
|
|
+ togglePreview() {
|
|
|
+ this.isPreviewing = !this.isPreviewing
|
|
|
+ if (this.isPreviewing) {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.calculateColumnWidths()
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ prevPage() {
|
|
|
+ if (this.currentPage > 1) {
|
|
|
+ this.currentPage--
|
|
|
+ }
|
|
|
+ },
|
|
|
+ nextPage() {
|
|
|
+ if (this.currentPage < this.totalPages) {
|
|
|
+ this.currentPage++
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ isPreviewing(val) {
|
|
|
+ if (val) {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.calculateColumnWidths()
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ landscape() {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.calculateColumnWidths()
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.wide-table-container {
|
|
|
+ width: 100%;
|
|
|
+ /* margin: 0 auto; */
|
|
|
+ /* padding-left: -260px; */
|
|
|
+}
|
|
|
+
|
|
|
+.controls {
|
|
|
+ margin-bottom: 15px;
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ align-items: center;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.print-btn, .preview-btn {
|
|
|
+ padding: 8px 16px;
|
|
|
+ background-color: #409eff;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.print-btn:hover, .preview-btn:hover {
|
|
|
+ background-color: #66b1ff;
|
|
|
+}
|
|
|
+
|
|
|
+.orientation-toggle {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 5px;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-container {
|
|
|
+ width: 100%;
|
|
|
+ margin-top: 15px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-content {
|
|
|
+ background-color: white;
|
|
|
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
|
|
+ margin-bottom: 15px;
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ padding: 20px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.print-header {
|
|
|
+ text-align: center;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.print-header h2 {
|
|
|
+ margin: 0 0 5px 0;
|
|
|
+ font-size: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+.print-meta {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+
|
|
|
+.table-wrapper {
|
|
|
+ width: 100%;
|
|
|
+ overflow-x: auto;
|
|
|
+ flex-grow: 1;
|
|
|
+ margin-bottom: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.wide-table {
|
|
|
+ width: 100%;
|
|
|
+ border-collapse: collapse;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.wide-table th, .wide-table td {
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ padding: 8px 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.wide-table th {
|
|
|
+ background-color: #f5f5f5;
|
|
|
+ font-weight: bold;
|
|
|
+ text-align: left;
|
|
|
+}
|
|
|
+
|
|
|
+.wide-table .fixed-column {
|
|
|
+ position: sticky;
|
|
|
+ left: 0;
|
|
|
+ background-color: white;
|
|
|
+ z-index: 1;
|
|
|
+ box-shadow: 1px 0 5px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.text-center {
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.text-right {
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+
|
|
|
+.page-footer {
|
|
|
+ font-size: 12px;
|
|
|
+ text-align: center;
|
|
|
+ color: #666;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-controls {
|
|
|
+ display: flex;
|
|
|
+ gap: 15px;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-controls button {
|
|
|
+ padding: 5px 10px;
|
|
|
+ background-color: #f5f5f5;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-controls button:disabled {
|
|
|
+ opacity: 0.5;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+.print-source {
|
|
|
+ position: absolute;
|
|
|
+ left: -9999px;
|
|
|
+ visibility: hidden;
|
|
|
+}
|
|
|
+</style>
|