@xtdev/xt-miniprogram-ui 1.2.79 → 1.2.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,11 +2,11 @@
2
2
  const utils = require('./utils');
3
3
  // 节流函数
4
4
  function _throttle(fn, wait = 500, isImmediate = false) {
5
- var flag = true;
5
+ let flag = true;
6
6
  return function () {
7
7
  if (flag == true) {
8
- var context = this;
9
- var args = arguments;
8
+ let context = this;
9
+ let args = arguments;
10
10
  flag = false;
11
11
  isImmediate && fn.apply(context, args);
12
12
  setTimeout(() => {
@@ -74,13 +74,25 @@ Component({
74
74
  type: Object | null,
75
75
  value: null,
76
76
  },
77
+ version: { // 版本控制
78
+ type: String,
79
+ value: 'v1',
80
+ },
81
+ // 每次点击拍摄时 获取水印数据
82
+ getWatermarkData: {
83
+ type: Function,
84
+ value: () => {},
85
+ },
77
86
  },
78
87
 
79
88
  /**
80
89
  * 组件的初始数据
81
90
  */
82
91
  data: {
92
+ showCamera: false,
83
93
  tempFileList: [], // 临时文件列表
94
+ // 新版本的水印
95
+ watermarkData: {},
84
96
  },
85
97
  /**
86
98
  * 初始化本地数据
@@ -152,6 +164,32 @@ Component({
152
164
  onUploadError(errMsg) {
153
165
  this.triggerEvent('error', { errMsg });
154
166
  },
167
+ onPhotoConfirm(e) {
168
+ // 确认照片
169
+ const {tempFilePath} = e.detail;
170
+ console.log("onPhotoConfirm:", e);
171
+ let {tempFileList} = this.data;
172
+ const tempFile = {
173
+ url: tempFilePath,
174
+ fileType: 'image',
175
+ updateStatus: 'uploading',
176
+ processText: '照片上传中',
177
+ id: Date.now(),
178
+ };
179
+ tempFileList = [...tempFileList, tempFile];
180
+ this.setData({
181
+ tempFileList,
182
+ showCamera: false,
183
+ });
184
+
185
+ this.startUpload({
186
+ url: tempFilePath,
187
+ updateStatus: 'uploading'
188
+ });
189
+ },
190
+ onCameraError(e) {
191
+ console.log("onCameraError:", e);
192
+ },
155
193
  /**
156
194
  * 重新上传
157
195
  * @param {*} e
@@ -170,14 +208,31 @@ Component({
170
208
  });
171
209
  this.startUpload(reloadItem);
172
210
  },
211
+ /**
212
+ * 关闭相机
213
+ */
214
+ closeCamera() {
215
+ this.setData({
216
+ showCamera: false,
217
+ });
218
+ },
173
219
  // 点击上传
174
220
  startTakePhoto: _throttle(
175
221
  function (e) {
176
222
  console.log('onStartTakePhoto', e);
177
223
  const _this = this;
178
- const { customUpload, uploadApi, max, sourceType, fileList, fileType, tempFileList } = this.properties;
224
+ const { customUpload, uploadApi, max, sourceType, fileList, fileType, tempFileList, version } = this.properties;
225
+ if (version === 'v2') {
226
+ _this.properties.getWatermarkData().then(watermarkData => {
227
+ _this.setData({
228
+ showCamera: true,
229
+ watermarkData,
230
+ });
231
+ })
232
+ return false;
233
+ }
179
234
  if (customUpload) {
180
- this.triggerEvent('upload', {});
235
+ _this.triggerEvent('upload', {});
181
236
  return;
182
237
  }
183
238
  if (!uploadApi || !uploadApi.url) {
@@ -1,5 +1,7 @@
1
1
  {
2
2
  "component": true,
3
3
  "styleIsolation": "apply-shared",
4
- "usingComponents": {}
4
+ "usingComponents": {
5
+ "xt-water-camera": "../xt-water-camera/index"
6
+ }
5
7
  }
@@ -51,3 +51,7 @@
51
51
  </view>
52
52
  </view>
53
53
  </view>
54
+ <!-- 相机页面 -->
55
+ <xt-water-camera wx:if="{{showCamera}}" watermarkType="banquet" watermarkData="{{watermarkData}}" bind:confirm="onPhotoConfirm" bind:error="onCameraError" />
56
+
57
+
@@ -136,3 +136,39 @@
136
136
  width: 100%;
137
137
  }
138
138
  }
139
+
140
+ .camera-page {
141
+ position: fixed;
142
+ top: 0;
143
+ left: 0;
144
+ right: 0;
145
+ bottom: 0;
146
+ background-color: #000;
147
+ display: flex;
148
+ flex-direction: column;
149
+ }
150
+
151
+ .camera-header {
152
+ display: flex;
153
+ align-items: center;
154
+ justify-content: space-between;
155
+ padding: 20rpx;
156
+ padding-top: calc(20rpx + env(safe-area-inset-top));
157
+ background-color: rgba(0, 0, 0, 0.5);
158
+ position: absolute;
159
+ top: 0;
160
+ left: 0;
161
+ right: 0;
162
+ z-index: 1000;
163
+
164
+ }
165
+
166
+ .back-btn {
167
+ color: #fff;
168
+ font-size: 28rpx;
169
+ padding: 16rpx 24rpx;
170
+ }
171
+ .camera-body {
172
+ flex: 1;
173
+ position: relative;
174
+ }
@@ -0,0 +1,522 @@
1
+ /**
2
+ * 水印相机组件
3
+ * 所有水印内容通过 watermarkData 传入
4
+ */
5
+
6
+ /**
7
+ * watermarkData 支持的字段:
8
+ * - tagText: string 顶部标签文字,如 "宴席执行"、"常规陈列"
9
+ * - personTitle: string 人员标题,如 "潭小二"、"商小二"
10
+ * - personName: string 人员名称
11
+ * - businessInfo: string 业务信息,如 "红潭宴席:12桌|开瓶8瓶"
12
+ * - highlightText: string 高亮文字(绿色)
13
+ * - shopName: string 店铺名称
14
+ * - address: string 地址(不传则自动定位)
15
+ * - serviceName: string 服务商名称
16
+ * - codeText: string 编号行完整文本,如 "宴席编号:YX2601074295"
17
+ * - timeWarning: boolean 时间异常标签
18
+ * - locationWarning: boolean 位置异常标签
19
+ */
20
+
21
+ Component({
22
+ properties: {
23
+ // 水印数据
24
+ watermarkData: {
25
+ type: Object,
26
+ value: {},
27
+ },
28
+ // 闪光灯模式:auto/on/off/torch
29
+ flash: {
30
+ type: String,
31
+ value: 'off',
32
+ },
33
+ },
34
+
35
+ data: {
36
+ currentTime: '', // 当前时间 HH:mm
37
+ currentDateTime: '', // 完整日期时间
38
+ previewImage: '', // 预览图片路径
39
+ isLoading: false,
40
+ systemInfo: null,
41
+ dpr: 2,
42
+ },
43
+
44
+ lifetimes: {
45
+ attached() {
46
+ this.initComponent();
47
+ },
48
+ detached() {
49
+ this.cleanup();
50
+ },
51
+ },
52
+
53
+ methods: {
54
+ /**
55
+ * 初始化组件
56
+ */
57
+ initComponent() {
58
+ // 获取系统信息
59
+ const systemInfo = wx.getSystemInfoSync();
60
+ this.setData({
61
+ systemInfo,
62
+ dpr: systemInfo.pixelRatio || 2,
63
+ });
64
+
65
+ // 初始化相机
66
+ this.cameraContext = wx.createCameraContext();
67
+
68
+ // 启动时钟
69
+ this.startClock();
70
+ },
71
+
72
+ /**
73
+ * 启动时钟
74
+ */
75
+ startClock() {
76
+ const {watermarkData} = this.properties;
77
+ const {currentTime = Date.now()} = watermarkData;
78
+ const updateTime = () => {
79
+ const now = new Date(currentTime);
80
+ const hours = String(now.getHours()).padStart(2, '0');
81
+ const minutes = String(now.getMinutes()).padStart(2, '0');
82
+ const year = now.getFullYear();
83
+ const month = String(now.getMonth() + 1).padStart(2, '0');
84
+ const day = String(now.getDate()).padStart(2, '0');
85
+
86
+ this.setData({
87
+ currentTime: `${hours}:${minutes}`,
88
+ currentDateTime: `${year}年${month}月${day}日${hours}:${minutes}`,
89
+ });
90
+ };
91
+
92
+ updateTime();
93
+ this.clockInterval = setInterval(updateTime, 1000);
94
+ },
95
+
96
+
97
+ /**
98
+ * 拍照
99
+ */
100
+ takePhoto() {
101
+ if (this.data.isLoading) return;
102
+
103
+ this.setData({ isLoading: true });
104
+
105
+ this.cameraContext.takePhoto({
106
+ quality: 'high',
107
+ success: res => {
108
+ this.processPhoto(res.tempImagePath);
109
+ },
110
+ fail: err => {
111
+ console.error('拍照失败:', err);
112
+ this.setData({ isLoading: false });
113
+ wx.showToast({
114
+ title: '拍照失败',
115
+ icon: 'error',
116
+ });
117
+ },
118
+ });
119
+ },
120
+
121
+ /**
122
+ * 处理照片 - 合成水印
123
+ */
124
+ async processPhoto(imagePath) {
125
+ try {
126
+ // 获取图片信息
127
+ const imgInfo = await this.getImageInfo(imagePath);
128
+
129
+ // 创建带水印的图片
130
+ const finalImagePath = await this.createWatermarkedImage(imagePath, imgInfo);
131
+
132
+ this.setData({
133
+ previewImage: finalImagePath,
134
+ isLoading: false,
135
+ });
136
+ } catch (error) {
137
+ console.error('处理照片失败:', error);
138
+ this.setData({ isLoading: false });
139
+ wx.showToast({
140
+ title: '处理失败',
141
+ icon: 'error',
142
+ });
143
+ }
144
+ },
145
+
146
+ /**
147
+ * 获取图片信息
148
+ */
149
+ getImageInfo(imagePath) {
150
+ return new Promise((resolve, reject) => {
151
+ wx.getImageInfo({
152
+ src: imagePath,
153
+ success: resolve,
154
+ fail: reject,
155
+ });
156
+ });
157
+ },
158
+
159
+ /**
160
+ * 创建带水印的图片
161
+ */
162
+ createWatermarkedImage(imagePath, imgInfo) {
163
+ return new Promise((resolve, reject) => {
164
+ const query = this.createSelectorQuery();
165
+ query
166
+ .select('#watermarkCanvas')
167
+ .fields({ node: true, size: true })
168
+ .exec(res => {
169
+ if (!res[0] || !res[0].node) {
170
+ // 使用离屏Canvas
171
+ this.createWithOffscreenCanvas(imagePath, imgInfo).then(resolve).catch(reject);
172
+ return;
173
+ }
174
+
175
+ const canvas = res[0].node;
176
+ const ctx = canvas.getContext('2d');
177
+
178
+ // 设置Canvas尺寸
179
+ canvas.width = imgInfo.width;
180
+ canvas.height = imgInfo.height;
181
+
182
+ // 加载并绘制图片
183
+ const img = canvas.createImage();
184
+ img.src = imagePath;
185
+
186
+ img.onload = () => {
187
+ ctx.drawImage(img, 0, 0, imgInfo.width, imgInfo.height);
188
+ this.drawWatermark(ctx, imgInfo.width, imgInfo.height);
189
+
190
+ wx.canvasToTempFilePath({
191
+ canvas,
192
+ success: result => resolve(result.tempFilePath),
193
+ fail: reject,
194
+ });
195
+ };
196
+
197
+ img.onerror = reject;
198
+ });
199
+ });
200
+ },
201
+
202
+ /**
203
+ * 使用离屏Canvas创建水印图片
204
+ */
205
+ createWithOffscreenCanvas(imagePath, imgInfo) {
206
+ return new Promise((resolve, reject) => {
207
+ const canvas = wx.createOffscreenCanvas({
208
+ type: '2d',
209
+ width: imgInfo.width,
210
+ height: imgInfo.height,
211
+ });
212
+
213
+ const ctx = canvas.getContext('2d');
214
+ canvas.width = imgInfo.width;
215
+ canvas.height = imgInfo.height;
216
+
217
+ const img = canvas.createImage();
218
+ img.src = imagePath;
219
+
220
+ img.onload = () => {
221
+ ctx.drawImage(img, 0, 0, imgInfo.width, imgInfo.height);
222
+ this.drawWatermark(ctx, imgInfo.width, imgInfo.height);
223
+
224
+ wx.canvasToTempFilePath({
225
+ canvas,
226
+ success: result => resolve(result.tempFilePath),
227
+ fail: reject,
228
+ });
229
+ };
230
+
231
+ img.onerror = reject;
232
+ });
233
+ },
234
+
235
+ /**
236
+ * 绘制圆角矩形(兼容方法)
237
+ * @param {CanvasRenderingContext2D} ctx Canvas上下文
238
+ * @param {number} x 左上角x坐标
239
+ * @param {number} y 左上角y坐标
240
+ * @param {number} w 宽度
241
+ * @param {number} h 高度
242
+ * @param {number|number[]} r 圆角半径,可以是数字或数组 [左上, 右上, 右下, 左下]
243
+ */
244
+ drawRoundRect(ctx, x, y, w, h, r) {
245
+ let radiusTL, radiusTR, radiusBR, radiusBL;
246
+
247
+ if (typeof r === 'number') {
248
+ radiusTL = radiusTR = radiusBR = radiusBL = r;
249
+ } else if (Array.isArray(r)) {
250
+ [radiusTL = 0, radiusTR = 0, radiusBR = 0, radiusBL = 0] = r;
251
+ } else {
252
+ radiusTL = radiusTR = radiusBR = radiusBL = 0;
253
+ }
254
+
255
+ ctx.beginPath();
256
+ ctx.moveTo(x + radiusTL, y);
257
+ ctx.lineTo(x + w - radiusTR, y);
258
+ if (radiusTR > 0) {
259
+ ctx.arcTo(x + w, y, x + w, y + radiusTR, radiusTR);
260
+ }
261
+ ctx.lineTo(x + w, y + h - radiusBR);
262
+ if (radiusBR > 0) {
263
+ ctx.arcTo(x + w, y + h, x + w - radiusBR, y + h, radiusBR);
264
+ }
265
+ ctx.lineTo(x + radiusBL, y + h);
266
+ if (radiusBL > 0) {
267
+ ctx.arcTo(x, y + h, x, y + h - radiusBL, radiusBL);
268
+ }
269
+ ctx.lineTo(x, y + radiusTL);
270
+ if (radiusTL > 0) {
271
+ ctx.arcTo(x, y, x + radiusTL, y, radiusTL);
272
+ }
273
+ ctx.closePath();
274
+ },
275
+
276
+ /**
277
+ * 绘制水印
278
+ */
279
+ drawWatermark(ctx, width, height) {
280
+ // 使用短边作为参考,确保横屏和竖屏时水印大小一致
281
+ const shortSide = Math.min(width, height);
282
+ const scale = shortSide / 750; // 以750rpx为基准计算缩放比
283
+ const isLandscape = width > height; // 是否横屏
284
+
285
+ const { watermarkData, currentTime, currentDateTime } = this.data;
286
+ const tagText = watermarkData.tagText || '拍照记录';
287
+ const tagColor = '#7b2d8e'; // 固定紫色
288
+
289
+ // ============ 尺寸参数(与CSS保持一致)============
290
+ const radius = 8 * scale;
291
+ const tagPaddingV = 12 * scale; // 标签垂直padding
292
+ const tagPaddingH = 24 * scale; // 标签水平padding
293
+ const tagFontSize = 40 * scale; // 标签字体大小
294
+ const timeFontSize = 40 * scale; // 时间字体大小
295
+
296
+ const infoPaddingV = 24 * scale; // 信息区垂直padding
297
+ const infoPaddingH = 20 * scale; // 信息区水平padding
298
+ const infoX = 16 * scale; // 距离左边16rpx
299
+
300
+ // 文字尺寸
301
+ const nameFontSize = 40 * scale;
302
+ const businessFontSize = 30 * scale;
303
+ const infoFontSize = 24 * scale;
304
+
305
+ // 警告标签尺寸
306
+ const warningFontSize = 20 * scale;
307
+ const warningPaddingH = 12 * scale;
308
+ const warningPaddingV = 4 * scale;
309
+ const warningRadius = 4 * scale;
310
+ const warningMarginRight = 8 * scale;
311
+
312
+ // 行间距
313
+ const nameRowMargin = 16 * scale;
314
+ const businessRowMargin = 16 * scale;
315
+ const infoRowMargin = 12 * scale;
316
+
317
+ // ============ 计算信息区高度 ============
318
+ let infoContentHeight = infoPaddingV * 2; // 上下padding
319
+ infoContentHeight += nameFontSize + nameRowMargin; // 人员名称行
320
+ if (watermarkData.businessInfo) {
321
+ infoContentHeight += businessFontSize + businessRowMargin; // 业务信息行
322
+ }
323
+ if (watermarkData.shopName) {
324
+ infoContentHeight += infoFontSize + infoRowMargin; // 店铺名称行
325
+ }
326
+ infoContentHeight += infoFontSize + infoRowMargin; // 日期时间行
327
+ infoContentHeight += infoFontSize + infoRowMargin; // 地址行
328
+ if (watermarkData.serviceName) {
329
+ infoContentHeight += infoFontSize + infoRowMargin; // 服务商行
330
+ }
331
+ if (watermarkData.codeText) {
332
+ infoContentHeight += infoFontSize; // 编号行(最后一行不加间距)
333
+ }
334
+
335
+ const infoAreaHeight = infoContentHeight;
336
+ // 横屏时信息区宽度按短边的85%计算,避免太宽
337
+ const infoAreaWidth = isLandscape ? shortSide * 0.85 : width * 0.85;
338
+ const infoY = height - infoAreaHeight - 16 * scale; // 距离底部16rpx
339
+
340
+ // ============ 绘制标签(在info-container上方16rpx) ============
341
+ ctx.font = `bold ${tagFontSize}px "PingFang SC", sans-serif`;
342
+ const tagTextWidth = ctx.measureText(tagText).width;
343
+ const tagWidth = tagTextWidth + tagPaddingH * 2;
344
+ const tagHeight = tagFontSize + tagPaddingV * 2;
345
+
346
+ const tagX = infoX; // 与info-container左对齐
347
+ const tagY = infoY - tagHeight - 16 * scale; // 在info-container上方16rpx
348
+
349
+ // 标签背景(左上和左下圆角)
350
+ ctx.fillStyle = tagColor;
351
+ this.drawRoundRect(ctx, tagX, tagY, tagWidth, tagHeight, [radius, 0, 0, radius]);
352
+ ctx.fill();
353
+
354
+ // 标签文字
355
+ ctx.fillStyle = '#ffffff';
356
+ ctx.textBaseline = 'middle';
357
+ ctx.fillText(tagText, tagX + tagPaddingH, tagY + tagHeight / 2);
358
+
359
+ // 时间背景(右上和右下圆角)
360
+ ctx.font = `bold ${timeFontSize}px "PingFang SC", sans-serif`;
361
+ const timeTextWidth = ctx.measureText(currentTime).width;
362
+ const timeWidth = timeTextWidth + tagPaddingH * 2;
363
+
364
+ ctx.fillStyle = '#ffffff';
365
+ this.drawRoundRect(ctx, tagX + tagWidth, tagY, timeWidth, tagHeight, [0, radius, radius, 0]);
366
+ ctx.fill();
367
+
368
+ // 时间文字
369
+ ctx.fillStyle = '#333333';
370
+ ctx.fillText(currentTime, tagX + tagWidth + tagPaddingH, tagY + tagHeight / 2);
371
+
372
+ // ============ 绘制信息区背景 ============
373
+ // 渐变背景(从左到右,90deg)
374
+ const gradient = ctx.createLinearGradient(infoX, infoY, infoX + infoAreaWidth, infoY);
375
+ gradient.addColorStop(0, 'rgba(0, 0, 0, 0.5)');
376
+ gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
377
+ ctx.fillStyle = gradient;
378
+ this.drawRoundRect(ctx, infoX, infoY, infoAreaWidth, infoAreaHeight, radius);
379
+ ctx.fill();
380
+
381
+ // ============ 绘制信息区文字 ============
382
+ let currentY = infoY + infoPaddingV;
383
+ const textX = infoX + infoPaddingH;
384
+ ctx.textBaseline = 'top';
385
+
386
+ // 人员名称(黄色)
387
+ ctx.font = `bold ${nameFontSize}px "PingFang SC", sans-serif`;
388
+ ctx.fillStyle = '#ffffff';
389
+ const personText = `${watermarkData.personTitle || '潭小二'}:${watermarkData.personName || '--'}`;
390
+ ctx.fillText(personText, textX, currentY);
391
+ currentY += nameFontSize + nameRowMargin;
392
+
393
+ // 业务信息行
394
+ if (watermarkData.businessInfo) {
395
+ ctx.font = `${businessFontSize}px "PingFang SC", sans-serif`;
396
+ ctx.fillStyle = '#ffffff';
397
+ ctx.fillText(watermarkData.businessInfo, textX, currentY);
398
+
399
+ // 高亮文字
400
+ if (watermarkData.highlightText) {
401
+ const businessWidth = ctx.measureText(watermarkData.businessInfo).width;
402
+ ctx.fillStyle = '#00ff00';
403
+ ctx.fillText(watermarkData.highlightText, textX + businessWidth + 8 * scale, currentY);
404
+ }
405
+ currentY += businessFontSize + businessRowMargin;
406
+ }
407
+
408
+ // 店铺名称
409
+ if (watermarkData.shopName) {
410
+ ctx.font = `${businessFontSize}px "PingFang SC", sans-serif`;
411
+ ctx.fillStyle = '#ffffff';
412
+ ctx.fillText(watermarkData.shopName, textX, currentY);
413
+ currentY += businessFontSize + infoRowMargin;
414
+ }
415
+
416
+ // 日期时间(可能带时间异常标签)
417
+ let dateTimeX = textX;
418
+ if (watermarkData.timeWarning) {
419
+ // 绘制时间异常标签
420
+ ctx.font = `${warningFontSize}px "PingFang SC", sans-serif`;
421
+ const warningText = '时间异常';
422
+ const warningTextWidth = ctx.measureText(warningText).width;
423
+ const warningTagWidth = warningTextWidth + warningPaddingH * 2;
424
+ const warningTagHeight = warningFontSize + warningPaddingV * 2;
425
+
426
+ ctx.fillStyle = '#ff4d4f';
427
+ this.drawRoundRect(ctx, textX, currentY, warningTagWidth, warningTagHeight, warningRadius);
428
+ ctx.fill();
429
+
430
+ ctx.fillStyle = '#ffffff';
431
+ ctx.textBaseline = 'middle';
432
+ ctx.fillText(warningText, textX + warningPaddingH, currentY + warningTagHeight / 2);
433
+ ctx.textBaseline = 'top';
434
+
435
+ dateTimeX = textX + warningTagWidth + warningMarginRight;
436
+ }
437
+ ctx.font = `${infoFontSize}px "PingFang SC", sans-serif`;
438
+ ctx.fillStyle = '#ffffff';
439
+ ctx.fillText(currentDateTime, dateTimeX, currentY);
440
+ currentY += infoFontSize + infoRowMargin;
441
+
442
+ // 地址(可能带位置异常标签)
443
+ let addressX = textX;
444
+ if (watermarkData.locationWarning) {
445
+ // 绘制位置异常标签
446
+ ctx.font = `${warningFontSize}px "PingFang SC", sans-serif`;
447
+ const warningText = '位置异常';
448
+ const warningTextWidth = ctx.measureText(warningText).width;
449
+ const warningTagWidth = warningTextWidth + warningPaddingH * 2;
450
+ const warningTagHeight = warningFontSize + warningPaddingV * 2;
451
+
452
+ ctx.fillStyle = '#ff4d4f';
453
+ this.drawRoundRect(ctx, textX, currentY, warningTagWidth, warningTagHeight, warningRadius);
454
+ ctx.fill();
455
+
456
+ ctx.fillStyle = '#ffffff';
457
+ ctx.textBaseline = 'middle';
458
+ ctx.fillText(warningText, textX + warningPaddingH, currentY + warningTagHeight / 2);
459
+ ctx.textBaseline = 'top';
460
+
461
+ addressX = textX + warningTagWidth + warningMarginRight;
462
+ }
463
+ const address = watermarkData.address;
464
+ ctx.font = `${infoFontSize}px "PingFang SC", sans-serif`;
465
+ ctx.fillStyle = '#ffffff';
466
+ ctx.fillText(address, addressX, currentY);
467
+ currentY += infoFontSize + infoRowMargin;
468
+
469
+ // 服务商
470
+ if (watermarkData.serviceName) {
471
+ ctx.fillText(`服务商:${watermarkData.serviceName}`, textX, currentY);
472
+ currentY += infoFontSize + infoRowMargin;
473
+ }
474
+
475
+ // 编号行(直接显示完整文本)
476
+ if (watermarkData.codeText) {
477
+ ctx.fillText(watermarkData.codeText, textX, currentY);
478
+ }
479
+ },
480
+
481
+ /**
482
+ * 重拍
483
+ */
484
+ retakePhoto() {
485
+ this.setData({
486
+ previewImage: '',
487
+ });
488
+ },
489
+
490
+ /**
491
+ * 确认使用照片
492
+ */
493
+ confirmPhoto() {
494
+ const { previewImage } = this.data;
495
+ if (!previewImage) return;
496
+
497
+ // 触发事件,将图片路径传递给父组件
498
+ this.triggerEvent('confirm', {
499
+ tempFilePath: previewImage,
500
+ watermarkData: this.properties.watermarkData,
501
+ });
502
+ },
503
+
504
+ /**
505
+ * 相机错误处理
506
+ */
507
+ onCameraError(e) {
508
+ console.error('相机错误:', e.detail);
509
+ this.triggerEvent('error', e.detail);
510
+ },
511
+
512
+ /**
513
+ * 清理资源
514
+ */
515
+ cleanup() {
516
+ if (this.clockInterval) {
517
+ clearInterval(this.clockInterval);
518
+ this.clockInterval = null;
519
+ }
520
+ },
521
+ },
522
+ });
@@ -0,0 +1,4 @@
1
+ {
2
+ "component": true,
3
+ "usingComponents": {}
4
+ }
@@ -0,0 +1,250 @@
1
+ .watermark-camera-container {
2
+ width: 100%;
3
+ height: 100%;
4
+ position: fixed;
5
+ left: 0;
6
+ right: 0;
7
+ top: 0;
8
+ bottom: 0;
9
+ z-index: 99;
10
+ background-color: #000;
11
+ }
12
+
13
+ // 相机区域
14
+ .camera-section {
15
+ width: 100%;
16
+ height: 100%;
17
+ position: relative;
18
+
19
+ .camera {
20
+ width: 100%;
21
+ height: 100%;
22
+ }
23
+ }
24
+
25
+ // 水印预览层
26
+ .watermark-preview-layer {
27
+ position: absolute;
28
+ top: 0;
29
+ left: 0;
30
+ right: 0;
31
+ bottom: 120rpx;
32
+ pointer-events: none;
33
+ display: flex;
34
+ flex-direction: column;
35
+ justify-content: flex-end;
36
+ }
37
+
38
+ // 标签容器(在info-container上方)
39
+ .tag-container {
40
+ display: flex;
41
+ align-items: center;
42
+ margin-left: 16rpx;
43
+ margin-bottom: 16rpx;
44
+
45
+ .tag-label {
46
+ padding: 12rpx 24rpx;
47
+ background-color: #7b2d8e;
48
+ color: #fff;
49
+ font-size: 40rpx;
50
+ font-weight: bold;
51
+ border-radius: 8rpx 0 0 8rpx;
52
+ }
53
+
54
+ .tag-time {
55
+ padding: 12rpx 24rpx;
56
+ background-color: #fff;
57
+ color: #333;
58
+ font-size: 40rpx;
59
+ font-weight: bold;
60
+ border-radius: 0 8rpx 8rpx 0;
61
+ }
62
+ }
63
+
64
+ // 底部信息区
65
+ .info-container {
66
+ background: linear-gradient(90deg, #000000 0%, rgba(0, 0, 0, 0) 100%);
67
+ opacity: 0.5;
68
+ padding: 24rpx 20rpx;
69
+ margin-left: 16rpx;
70
+ margin-bottom: 16rpx;
71
+ border-radius: 8rpx;
72
+ font-weight: 800;
73
+ }
74
+
75
+ .info-row {
76
+ margin-bottom: 8rpx;
77
+ display: flex;
78
+ align-items: center;
79
+ flex-wrap: wrap;
80
+ line-height: 34rpx;
81
+ margin-bottom: 2rpx;
82
+
83
+ &.name-row {
84
+ line-height: 56rpx;
85
+ }
86
+
87
+ &.business-row {
88
+ line-height: 42rpx;
89
+ }
90
+ }
91
+
92
+ .name-text {
93
+ color: #FFFFFF;
94
+ font-size: 40rpx;
95
+ font-weight: bold;
96
+ }
97
+
98
+ .business-text {
99
+ color: #fff;
100
+ font-size: 30rpx;
101
+ }
102
+
103
+ .highlight-text {
104
+ color: #00ff00;
105
+ font-size: 28rpx;
106
+ margin-left: 8rpx;
107
+ }
108
+
109
+ .info-text {
110
+ color: #fff;
111
+ font-size: 24rpx;
112
+ }
113
+
114
+ .warning-tag {
115
+ display: inline-block;
116
+ background-color: #ff4d4f;
117
+ color: #fff;
118
+ font-size: 20rpx;
119
+ padding: 4rpx 12rpx;
120
+ border-radius: 4rpx;
121
+ margin-right: 8rpx;
122
+ }
123
+
124
+ // 拍照按钮
125
+ .shutter-area {
126
+ position: absolute;
127
+ bottom: 40rpx;
128
+ left: 0;
129
+ right: 0;
130
+ display: flex;
131
+ justify-content: center;
132
+ align-items: center;
133
+ pointer-events: auto;
134
+ }
135
+
136
+ .shutter-btn {
137
+ width: 140rpx;
138
+ height: 140rpx;
139
+ border-radius: 50%;
140
+ background-color: rgba(255, 255, 255, 0.3);
141
+ display: flex;
142
+ justify-content: center;
143
+ align-items: center;
144
+ border: 6rpx solid #fff;
145
+
146
+ .shutter-inner {
147
+ width: 100rpx;
148
+ height: 100rpx;
149
+ border-radius: 50%;
150
+ background-color: #fff;
151
+ }
152
+
153
+ &:active {
154
+ transform: scale(0.95);
155
+
156
+ .shutter-inner {
157
+ background-color: #ddd;
158
+ }
159
+ }
160
+ }
161
+
162
+ // 预览区域
163
+ .preview-section {
164
+ width: 100%;
165
+ height: 100%;
166
+ display: flex;
167
+ flex-direction: column;
168
+ background-color: #000;
169
+
170
+ .preview-image {
171
+ flex: 1;
172
+ width: 100%;
173
+ }
174
+
175
+ .preview-buttons {
176
+ display: flex;
177
+ padding: 30rpx 40rpx;
178
+ padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
179
+ background-color: #000;
180
+
181
+ .btn-retake,
182
+ .btn-confirm {
183
+ flex: 1;
184
+ height: 88rpx;
185
+ line-height: 88rpx;
186
+ text-align: center;
187
+ font-size: 32rpx;
188
+ border-radius: 44rpx;
189
+ margin: 0 20rpx;
190
+ }
191
+
192
+ .btn-retake {
193
+ background-color: #333;
194
+ color: #fff;
195
+ }
196
+
197
+ .btn-confirm {
198
+ background-color: #7b2d8e;
199
+ color: #fff;
200
+ }
201
+ }
202
+ }
203
+
204
+ // 离屏Canvas
205
+ .offscreen-canvas {
206
+ position: fixed;
207
+ left: -9999rpx;
208
+ top: -9999rpx;
209
+ width: 1px;
210
+ height: 1px;
211
+ }
212
+
213
+ // 加载提示
214
+ .loading-overlay {
215
+ position: absolute;
216
+ top: 0;
217
+ left: 0;
218
+ right: 0;
219
+ bottom: 0;
220
+ background-color: rgba(0, 0, 0, 0.7);
221
+ display: flex;
222
+ flex-direction: column;
223
+ justify-content: center;
224
+ align-items: center;
225
+ z-index: 100;
226
+ }
227
+
228
+ .loading-spinner {
229
+ width: 80rpx;
230
+ height: 80rpx;
231
+ border: 6rpx solid #fff;
232
+ border-top-color: transparent;
233
+ border-radius: 50%;
234
+ animation: spin 1s linear infinite;
235
+ }
236
+
237
+ @keyframes spin {
238
+ from {
239
+ transform: rotate(0deg);
240
+ }
241
+ to {
242
+ transform: rotate(360deg);
243
+ }
244
+ }
245
+
246
+ .loading-text {
247
+ color: #fff;
248
+ font-size: 28rpx;
249
+ margin-top: 20rpx;
250
+ }
@@ -0,0 +1,82 @@
1
+ <view class="watermark-camera-container">
2
+ <!-- 相机区域 -->
3
+ <view class="camera-section" wx:if="{{!previewImage}}">
4
+ <camera device-position="back" flash="{{flash}}" class="camera" binderror="onCameraError" />
5
+
6
+ <!-- 实时水印预览层 -->
7
+ <view class="watermark-preview-layer">
8
+ <!-- 左上角标签 -->
9
+ <view class="tag-container">
10
+ <view class="tag-label">{{watermarkData.tagText || '拍照记录'}}</view>
11
+ <view class="tag-time">{{currentTime}}</view>
12
+ </view>
13
+
14
+ <!-- 底部信息区 -->
15
+ <view class="info-container">
16
+ <!-- 人员名称 -->
17
+ <view class="info-row name-row">
18
+ <text class="name-text">{{watermarkData.personTitle || '潭小二'}}:{{watermarkData.personName || '--'}}</text>
19
+ </view>
20
+
21
+ <!-- 业务信息行 -->
22
+ <view class="info-row business-row" wx:if="{{watermarkData.businessInfo}}">
23
+ <text class="business-text">{{watermarkData.businessInfo}}</text>
24
+ <text wx:if="{{watermarkData.highlightText}}" class="highlight-text">{{watermarkData.highlightText}}</text>
25
+ </view>
26
+
27
+ <!-- 店铺名称 -->
28
+ <view class="info-row business-row" wx:if="{{watermarkData.shopName}}">
29
+ <text class="business-text">{{watermarkData.shopName}}</text>
30
+ </view>
31
+
32
+ <!-- 日期时间 -->
33
+ <view class="info-row">
34
+ <text wx:if="{{watermarkData.timeWarning}}" class="warning-tag">时间异常</text>
35
+ <text class="info-text">{{currentDateTime}}</text>
36
+ </view>
37
+
38
+ <!-- 地址 -->
39
+ <view class="info-row">
40
+ <text wx:if="{{watermarkData.locationWarning}}" class="warning-tag">位置异常</text>
41
+ <text class="info-text">{{watermarkData.address || currentAddress}}</text>
42
+ </view>
43
+
44
+ <!-- 服务商 -->
45
+ <view class="info-row" wx:if="{{watermarkData.serviceName}}">
46
+ <text class="info-text">服务商:{{watermarkData.serviceName}}</text>
47
+ </view>
48
+
49
+ <!-- 编号行 -->
50
+ <view class="info-row" wx:if="{{watermarkData.codeText}}">
51
+ <text class="info-text">{{watermarkData.codeText}}</text>
52
+ </view>
53
+ </view>
54
+ </view>
55
+
56
+ <!-- 拍照按钮 -->
57
+ <view class="shutter-area">
58
+ <view class="shutter-btn" bindtap="takePhoto">
59
+ <view class="shutter-inner"></view>
60
+ </view>
61
+ </view>
62
+ </view>
63
+
64
+ <!-- 预览区域 -->
65
+ <view class="preview-section" wx:if="{{previewImage}}">
66
+ <image src="{{previewImage}}" mode="aspectFit" class="preview-image" />
67
+
68
+ <view class="preview-buttons">
69
+ <view class="btn-retake" bindtap="retakePhoto">重拍</view>
70
+ <view class="btn-confirm" bindtap="confirmPhoto">确认使用</view>
71
+ </view>
72
+ </view>
73
+
74
+ <!-- 离屏Canvas用于合成水印 -->
75
+ <canvas type="2d" id="watermarkCanvas" class="offscreen-canvas" />
76
+
77
+ <!-- 加载提示 -->
78
+ <view wx:if="{{isLoading}}" class="loading-overlay">
79
+ <view class="loading-spinner"></view>
80
+ <text class="loading-text">正在生成水印图片...</text>
81
+ </view>
82
+ </view>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtdev/xt-miniprogram-ui",
3
- "version": "1.2.79",
3
+ "version": "1.2.80",
4
4
  "description": "",
5
5
  "miniprogram": "libs",
6
6
  "publishConfig": {