feat: 打印预览组件化

This commit is contained in:
R524809
2026-04-22 15:02:48 +08:00
parent 4ad387e99d
commit de74666c9f
24 changed files with 552 additions and 273 deletions
@@ -0,0 +1,8 @@
{
"component": true,
"usingComponents": {
"toy-icon": "../../toy/icon/icon"
},
"styleIsolation": "apply-shared",
"addGlobalClass": true
}
@@ -0,0 +1,112 @@
@import '../../style/theme.less';
.pc-wrap {
position: relative;
}
.pc-card {
position: relative;
background: @bg-white;
border-radius: @radius-xl;
box-shadow: @shadow;
padding: 40rpx;
border: @border;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: stretch;
}
.pc-card--with-fab {
padding-bottom: 56rpx;
}
.pc-kicker {
display: block;
text-align: center;
font-size: @fs-section-head-title;
font-weight: bold;
color: @text-secondary;
letter-spacing: 0.12em;
text-transform: uppercase;
margin-bottom: 30rpx;
}
.pc-canvas-wrap {
display: flex;
justify-content: center;
align-items: center;
min-height: 400rpx;
background: @bg-gray;
border-radius: @radius-sm;
border: 2rpx dashed rgba(50, 46, 37, 0.12);
overflow: hidden;
}
.pc-canvas {
max-width: 100%;
border-radius: @radius-sm;
box-shadow: @shadow;
}
/* 底部角上的圆形操作按钮 */
@pc-fab-size: 88rpx;
@keyframes pc-fab-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.pc-fab {
position: absolute;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
width: @pc-fab-size;
height: @pc-fab-size;
border-radius: 50%;
box-shadow: 0 6rpx 18rpx rgba(50, 46, 37, 0.12);
box-sizing: border-box;
}
.pc-fab--hover {
opacity: 0.88;
transform: scale(0.96);
}
.pc-fab--refresh {
left: 12rpx;
bottom: 12rpx;
background: #9add4c;
}
.pc-fab--favorite {
right: 12rpx;
bottom: 12rpx;
background: #f2eee4;
}
.pc-fab__toy {
display: flex;
align-items: center;
justify-content: center;
}
.pc-fab__icon-wrap--a .pc-fab__toy,
.pc-fab__icon-wrap--b .pc-fab__toy {
animation: pc-fab-spin 0.55s linear 1;
}
.pc-fab__icon-wrap {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
@@ -0,0 +1,145 @@
import { PAPER_SIZE, A4_EXPORT_SIZE_150DPI } from '../../constants/colors';
Component({
options: {
styleIsolation: 'apply-shared',
addGlobalClass: true,
},
properties: {
kicker: {
type: String,
value: '打印预览',
},
showRefresh: {
type: Boolean,
value: false,
},
showFavorite: {
type: Boolean,
value: false,
},
favorited: {
type: Boolean,
value: false,
},
favoritedIconColor: {
type: String,
value: '#E53935',
},
favoriteIdleIconColor: {
type: String,
value: '#605b50',
},
},
data: {
boxWidth: 0,
boxHeight: 0,
refreshIconAnimClass: '',
withFab: false,
},
observers: {
'showRefresh, showFavorite'(
showRefresh: boolean,
showFavorite: boolean,
) {
this.setData({ withFab: !!showRefresh || !!showFavorite });
},
},
lifetimes: {
ready() {
this.initCanvas();
},
},
methods: {
initCanvas() {
const query = this.createSelectorQuery();
query
.select('#canvasWrapper')
.boundingClientRect((rect) => {
if (!rect) return;
const { width, height } = PAPER_SIZE['A4'];
const boxWidth = rect.width;
const boxHeight = boxWidth / (width / height);
this.setData({ boxWidth, boxHeight });
this.createSelectorQuery()
.select('#canvasContent')
.fields({ node: true, size: true })
.exec((res) => {
if (!res[0]) return;
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
const dpr = wx.getSystemInfoSync().pixelRatio;
canvas.width = boxWidth * dpr;
canvas.height = boxHeight * dpr;
ctx.scale(dpr, dpr);
this._canvas = canvas;
this.triggerEvent('canvas-ready', {
canvas,
ctx,
width: boxWidth,
height: boxHeight,
});
});
})
.exec();
},
onRefreshTap() {
this.triggerEvent('refresh', {});
if ((this as any)._refreshSpinTimer) {
clearTimeout((this as any)._refreshSpinTimer);
}
this.setData({ refreshIconAnimClass: '' });
const n = ((this as any)._refreshTapCount || 0) + 1;
(this as any)._refreshTapCount = n;
const sub = n % 2 === 1 ? 'a' : 'b';
(this as any)._refreshSpinTimer = setTimeout(() => {
(this as any)._refreshSpinTimer = null;
this.setData({
refreshIconAnimClass: `pc-fab__icon-wrap--${sub}`,
});
}, 20);
},
onFavoriteTap() {
this.triggerEvent('favorite', {});
},
getCanvas(): WechatMiniprogram.Canvas | null {
return this._canvas || null;
},
exportToTempFile(
options?: { quality?: number; fileType?: 'png' | 'jpg' },
): Promise<string> {
const canvas = this._canvas;
if (!canvas) {
return Promise.reject(new Error('Canvas not initialized'));
}
return new Promise((resolve, reject) => {
wx.canvasToTempFilePath({
canvas,
fileType: options?.fileType || 'png',
quality: options?.quality ?? 1,
destWidth: A4_EXPORT_SIZE_150DPI.width,
destHeight: A4_EXPORT_SIZE_150DPI.height,
success: (res) => resolve(res.tempFilePath),
fail: (err) => reject(err),
});
});
},
},
});
@@ -0,0 +1,43 @@
<view class="pc-wrap">
<view class="pc-card {{withFab ? 'pc-card--with-fab' : ''}}">
<text class="pc-kicker">{{kicker}}</text>
<view id="canvasWrapper" class="pc-canvas-wrap">
<canvas
type="2d"
id="canvasContent"
class="pc-canvas"
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
</view>
<view
wx:if="{{showRefresh}}"
class="pc-fab pc-fab--refresh"
hover-class="pc-fab--hover"
hover-start-time="0"
hover-stay-time="70"
catchtap="onRefreshTap">
<view
class="pc-fab__icon-wrap {{refreshIconAnimClass}}">
<toy-icon
name="refresh"
size="40rpx"
color="#2d5016"
custom-class="pc-fab__toy" />
</view>
</view>
<view
wx:if="{{showFavorite}}"
class="pc-fab pc-fab--favorite"
hover-class="pc-fab--hover"
hover-start-time="0"
hover-stay-time="70"
catchtap="onFavoriteTap">
<toy-icon
name="{{favorited ? 'heart' : 'heart-o'}}"
size="40rpx"
color="{{favorited ? favoritedIconColor : favoriteIdleIconColor}}"
custom-class="pc-fab__toy" />
</view>
</view>
</view>