z-certificate-editor 1.0.3 → 1.0.4

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.
@@ -0,0 +1,2128 @@
1
+ import { ref, computed, createElementBlock, openBlock, createElementVNode, normalizeClass, createCommentVNode, createTextVNode, toDisplayString, Fragment, renderList, normalizeStyle, onUnmounted, withModifiers, withDirectives, vModelText, watch, createVNode, vModelSelect, createStaticVNode, vModelRadio, onMounted, createBlock } from "vue";
2
+ const PRESET_TEMPLATES = [
3
+ {
4
+ id: "vertical-1",
5
+ name: "竖版模板1",
6
+ type: "vertical",
7
+ width: 972,
8
+ height: 1378,
9
+ background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
10
+ description: "经典竖版证书模板"
11
+ },
12
+ {
13
+ id: "vertical-2",
14
+ name: "竖版模板2",
15
+ type: "vertical",
16
+ width: 972,
17
+ height: 1378,
18
+ background: "linear-gradient(135deg, #f093fb 0%, #f5576c 100%)",
19
+ description: "优雅竖版证书模板"
20
+ },
21
+ {
22
+ id: "vertical-3",
23
+ name: "竖版模板3",
24
+ type: "vertical",
25
+ width: 972,
26
+ height: 1378,
27
+ background: "linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)",
28
+ description: "现代竖版证书模板"
29
+ },
30
+ {
31
+ id: "horizontal-1",
32
+ name: "横版模板1",
33
+ type: "horizontal",
34
+ width: 1303,
35
+ height: 897,
36
+ background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
37
+ description: "经典横版证书模板"
38
+ },
39
+ {
40
+ id: "horizontal-2",
41
+ name: "横版模板2",
42
+ type: "horizontal",
43
+ width: 1303,
44
+ height: 897,
45
+ background: "linear-gradient(135deg, #f093fb 0%, #f5576c 100%)",
46
+ description: "优雅横版证书模板"
47
+ },
48
+ {
49
+ id: "horizontal-3",
50
+ name: "横版模板3",
51
+ type: "horizontal",
52
+ width: 1303,
53
+ height: 897,
54
+ background: "linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)",
55
+ description: "现代横版证书模板"
56
+ }
57
+ ];
58
+ const validateImageFile = (file) => {
59
+ const allowedTypes = ["image/jpeg", "image/jpg", "image/png"];
60
+ const maxSize = 2 * 1024 * 1024;
61
+ if (!allowedTypes.includes(file.type)) {
62
+ return { valid: false, error: "只支持 JPG、JPEG、PNG 格式的图片" };
63
+ }
64
+ if (file.size > maxSize) {
65
+ return { valid: false, error: "图片大小不能超过 2MB" };
66
+ }
67
+ return { valid: true };
68
+ };
69
+ const fileToBase64 = (file) => {
70
+ return new Promise((resolve, reject) => {
71
+ const reader = new FileReader();
72
+ reader.onload = () => resolve(reader.result);
73
+ reader.onerror = reject;
74
+ reader.readAsDataURL(file);
75
+ });
76
+ };
77
+ const getTemplateSizeSuggestion = (type) => {
78
+ if (type === "vertical") {
79
+ return { width: 972, height: 1378 };
80
+ } else {
81
+ return { width: 1303, height: 897 };
82
+ }
83
+ };
84
+ const _export_sfc = (sfc, props) => {
85
+ const target = sfc.__vccOpts || sfc;
86
+ for (const [key, val] of props) {
87
+ target[key] = val;
88
+ }
89
+ return target;
90
+ };
91
+ const _hoisted_1$6 = { class: "design-panel" };
92
+ const _hoisted_2$6 = { class: "panel-tabs" };
93
+ const _hoisted_3$6 = { class: "panel-content" };
94
+ const _hoisted_4$6 = {
95
+ key: 0,
96
+ class: "elements-section"
97
+ };
98
+ const _hoisted_5$5 = { class: "elements-grid" };
99
+ const _hoisted_6$5 = {
100
+ key: 1,
101
+ class: "templates-section"
102
+ };
103
+ const _hoisted_7$5 = { class: "upload-section" };
104
+ const _hoisted_8$4 = { class: "upload-hint" };
105
+ const _hoisted_9$4 = { class: "template-type-switch" };
106
+ const _hoisted_10$4 = { class: "templates-list" };
107
+ const _hoisted_11$3 = ["onClick"];
108
+ const _hoisted_12$3 = { class: "template-name" };
109
+ const _sfc_main$6 = {
110
+ __name: "DesignPanel",
111
+ emits: ["add-element", "select-template", "upload-template"],
112
+ setup(__props, { emit: __emit }) {
113
+ const emit = __emit;
114
+ const activeTab = ref("elements");
115
+ const templateType = ref("vertical");
116
+ const fileInputRef = ref(null);
117
+ const filteredTemplates = computed(() => {
118
+ return PRESET_TEMPLATES.filter((t) => t.type === templateType.value);
119
+ });
120
+ const handleSelectTemplate = (template) => {
121
+ emit("select-template", template);
122
+ };
123
+ const triggerUpload = () => {
124
+ var _a;
125
+ (_a = fileInputRef.value) == null ? void 0 : _a.click();
126
+ };
127
+ const handleFileUpload = async (event) => {
128
+ var _a;
129
+ const file = (_a = event.target.files) == null ? void 0 : _a[0];
130
+ if (!file) return;
131
+ const validation = validateImageFile(file);
132
+ if (!validation.valid) {
133
+ alert(validation.error);
134
+ return;
135
+ }
136
+ try {
137
+ const base64 = await fileToBase64(file);
138
+ const suggestion = getTemplateSizeSuggestion(templateType.value);
139
+ const customTemplate = {
140
+ id: `custom-${Date.now()}`,
141
+ name: "自定义模板",
142
+ type: templateType.value,
143
+ width: suggestion.width,
144
+ height: suggestion.height,
145
+ background: base64,
146
+ description: "自定义上传的模板"
147
+ };
148
+ emit("upload-template", customTemplate);
149
+ } catch (error) {
150
+ alert("上传失败,请重试");
151
+ console.error(error);
152
+ }
153
+ if (fileInputRef.value) {
154
+ fileInputRef.value.value = "";
155
+ }
156
+ };
157
+ return (_ctx, _cache) => {
158
+ return openBlock(), createElementBlock("div", _hoisted_1$6, [
159
+ createElementVNode("div", _hoisted_2$6, [
160
+ createElementVNode("div", {
161
+ class: normalizeClass(["tab-item", { active: activeTab.value === "elements" }]),
162
+ onClick: _cache[0] || (_cache[0] = ($event) => activeTab.value = "elements")
163
+ }, " 设计元素 ", 2),
164
+ createElementVNode("div", {
165
+ class: normalizeClass(["tab-item", { active: activeTab.value === "templates" }]),
166
+ onClick: _cache[1] || (_cache[1] = ($event) => activeTab.value = "templates")
167
+ }, " 底图模板 ", 2)
168
+ ]),
169
+ createElementVNode("div", _hoisted_3$6, [
170
+ activeTab.value === "elements" ? (openBlock(), createElementBlock("div", _hoisted_4$6, [
171
+ createElementVNode("div", _hoisted_5$5, [
172
+ createElementVNode("div", {
173
+ class: "element-item",
174
+ onClick: _cache[2] || (_cache[2] = ($event) => _ctx.$emit("add-element", "text"))
175
+ }, [..._cache[8] || (_cache[8] = [
176
+ createElementVNode("div", { class: "element-icon" }, "📝", -1),
177
+ createElementVNode("div", { class: "element-label" }, "短文本", -1)
178
+ ])]),
179
+ createElementVNode("div", {
180
+ class: "element-item",
181
+ onClick: _cache[3] || (_cache[3] = ($event) => _ctx.$emit("add-element", "longtext"))
182
+ }, [..._cache[9] || (_cache[9] = [
183
+ createElementVNode("div", { class: "element-icon" }, "📄", -1),
184
+ createElementVNode("div", { class: "element-label" }, "长文本", -1)
185
+ ])]),
186
+ createElementVNode("div", {
187
+ class: "element-item",
188
+ onClick: _cache[4] || (_cache[4] = ($event) => _ctx.$emit("add-element", "image"))
189
+ }, [..._cache[10] || (_cache[10] = [
190
+ createElementVNode("div", { class: "element-icon" }, "🖼️", -1),
191
+ createElementVNode("div", { class: "element-label" }, "图片", -1)
192
+ ])]),
193
+ createElementVNode("div", {
194
+ class: "element-item",
195
+ onClick: _cache[5] || (_cache[5] = ($event) => _ctx.$emit("add-element", "qrcode"))
196
+ }, [..._cache[11] || (_cache[11] = [
197
+ createElementVNode("div", { class: "element-icon" }, "🔲", -1),
198
+ createElementVNode("div", { class: "element-label" }, "二维码", -1)
199
+ ])])
200
+ ])
201
+ ])) : createCommentVNode("", true),
202
+ activeTab.value === "templates" ? (openBlock(), createElementBlock("div", _hoisted_6$5, [
203
+ createElementVNode("div", _hoisted_7$5, [
204
+ createElementVNode("input", {
205
+ ref_key: "fileInputRef",
206
+ ref: fileInputRef,
207
+ type: "file",
208
+ accept: "image/jpeg,image/jpg,image/png",
209
+ onChange: handleFileUpload,
210
+ style: { "display": "none" }
211
+ }, null, 544),
212
+ createElementVNode("button", {
213
+ class: "upload-btn",
214
+ onClick: triggerUpload
215
+ }, [..._cache[12] || (_cache[12] = [
216
+ createElementVNode("span", null, "📁", -1),
217
+ createElementVNode("span", null, "上传模板", -1)
218
+ ])]),
219
+ createElementVNode("div", _hoisted_8$4, [
220
+ _cache[13] || (_cache[13] = createTextVNode(" 支持:jpg、jpeg、png 小于2M", -1)),
221
+ _cache[14] || (_cache[14] = createElementVNode("br", null, null, -1)),
222
+ createTextVNode(" 建议尺寸:" + toDisplayString(templateType.value === "vertical" ? "972 x 1378" : "1303 x 897"), 1)
223
+ ])
224
+ ]),
225
+ createElementVNode("div", _hoisted_9$4, [
226
+ createElementVNode("button", {
227
+ class: normalizeClass(["switch-btn", { active: templateType.value === "vertical" }]),
228
+ onClick: _cache[6] || (_cache[6] = ($event) => templateType.value = "vertical")
229
+ }, " 竖版 ", 2),
230
+ createElementVNode("button", {
231
+ class: normalizeClass(["switch-btn", { active: templateType.value === "horizontal" }]),
232
+ onClick: _cache[7] || (_cache[7] = ($event) => templateType.value = "horizontal")
233
+ }, " 横版 ", 2)
234
+ ]),
235
+ createElementVNode("div", _hoisted_10$4, [
236
+ (openBlock(true), createElementBlock(Fragment, null, renderList(filteredTemplates.value, (template) => {
237
+ return openBlock(), createElementBlock("div", {
238
+ key: template.id,
239
+ class: "template-item",
240
+ onClick: ($event) => handleSelectTemplate(template)
241
+ }, [
242
+ createElementVNode("div", {
243
+ class: "template-preview",
244
+ style: normalizeStyle({ background: template.background })
245
+ }, null, 4),
246
+ createElementVNode("div", _hoisted_12$3, toDisplayString(template.name), 1)
247
+ ], 8, _hoisted_11$3);
248
+ }), 128))
249
+ ])
250
+ ])) : createCommentVNode("", true)
251
+ ])
252
+ ]);
253
+ };
254
+ }
255
+ };
256
+ const DesignPanel = /* @__PURE__ */ _export_sfc(_sfc_main$6, [["__scopeId", "data-v-b694f9ff"]]);
257
+ const SYSTEM_FIELDS = [
258
+ {
259
+ key: "name",
260
+ label: "姓名",
261
+ placeholder: "{{姓名}}",
262
+ category: "基本信息",
263
+ description: "证书持有人的姓名"
264
+ },
265
+ {
266
+ key: "gender",
267
+ label: "性别称呼",
268
+ placeholder: "{{性别称呼}}",
269
+ category: "基本信息",
270
+ description: "先生/女士"
271
+ },
272
+ {
273
+ key: "examName",
274
+ label: "考试名称",
275
+ placeholder: "{{考试名称}}",
276
+ category: "考试信息",
277
+ description: "参加的考试名称"
278
+ },
279
+ {
280
+ key: "score",
281
+ label: "成绩",
282
+ placeholder: "{{成绩}}",
283
+ category: "考试信息",
284
+ description: "考试成绩"
285
+ },
286
+ {
287
+ key: "scoreLevel",
288
+ label: "成绩等级",
289
+ placeholder: "{{成绩等级}}",
290
+ category: "考试信息",
291
+ description: "成绩等级(如:优秀、良好、合格等)"
292
+ },
293
+ {
294
+ key: "certificateNumber",
295
+ label: "证书编号",
296
+ placeholder: "{{证书编号}}",
297
+ category: "证书信息",
298
+ description: "证书的唯一编号"
299
+ },
300
+ {
301
+ key: "validityPeriod",
302
+ label: "有效期",
303
+ placeholder: "{{有效期}}",
304
+ category: "证书信息",
305
+ description: "证书的有效期限"
306
+ },
307
+ {
308
+ key: "issueDate",
309
+ label: "颁发日期",
310
+ placeholder: "{{颁发日期}}",
311
+ category: "证书信息",
312
+ description: "证书颁发的日期"
313
+ },
314
+ {
315
+ key: "organization",
316
+ label: "颁发机构",
317
+ placeholder: "{{颁发机构}}",
318
+ category: "证书信息",
319
+ description: "颁发证书的机构名称"
320
+ },
321
+ {
322
+ key: "idNumber",
323
+ label: "身份证号",
324
+ placeholder: "{{身份证号}}",
325
+ category: "基本信息",
326
+ description: "身份证号码"
327
+ },
328
+ {
329
+ key: "phone",
330
+ label: "联系电话",
331
+ placeholder: "{{联系电话}}",
332
+ category: "基本信息",
333
+ description: "联系电话"
334
+ },
335
+ {
336
+ key: "email",
337
+ label: "电子邮箱",
338
+ placeholder: "{{电子邮箱}}",
339
+ category: "基本信息",
340
+ description: "电子邮箱地址"
341
+ },
342
+ {
343
+ key: "account",
344
+ label: "账号",
345
+ placeholder: "{{账号}}",
346
+ category: "基本信息",
347
+ description: "用户账号"
348
+ },
349
+ {
350
+ key: "examScore",
351
+ label: "考试得分",
352
+ placeholder: "{{考试得分}}",
353
+ category: "考试信息",
354
+ description: "考试得分"
355
+ },
356
+ {
357
+ key: "issueDateMonth",
358
+ label: "颁证时间(月)",
359
+ placeholder: "{{颁证时间(月)}}",
360
+ category: "证书信息",
361
+ description: "证书颁发的月份"
362
+ },
363
+ {
364
+ key: "issueDateDay",
365
+ label: "颁证时间(日)",
366
+ placeholder: "{{颁证时间(日)}}",
367
+ category: "证书信息",
368
+ description: "证书颁发的日期"
369
+ }
370
+ ];
371
+ const FIELDS_BY_CATEGORY = SYSTEM_FIELDS.reduce((acc, field) => {
372
+ if (!acc[field.category]) {
373
+ acc[field.category] = [];
374
+ }
375
+ acc[field.category].push(field);
376
+ return acc;
377
+ }, {});
378
+ const getFieldByKey = (key) => {
379
+ return SYSTEM_FIELDS.find((field) => field.key === key);
380
+ };
381
+ const getPlaceholderByKey = (key) => {
382
+ const field = getFieldByKey(key);
383
+ return field ? field.placeholder : `{{${key}}}`;
384
+ };
385
+ const replaceFields = (text, fieldData = {}) => {
386
+ if (!text) return "";
387
+ let result = text;
388
+ SYSTEM_FIELDS.forEach((field) => {
389
+ const placeholder = field.placeholder;
390
+ const value = fieldData[field.key] || field.placeholder;
391
+ const regex = new RegExp(placeholder.replace(/[{}]/g, "\\$&"), "g");
392
+ result = result.replace(regex, value);
393
+ });
394
+ return result;
395
+ };
396
+ const extractFields = (text) => {
397
+ if (!text) return [];
398
+ const fields = [];
399
+ SYSTEM_FIELDS.forEach((field) => {
400
+ if (text.includes(field.placeholder)) {
401
+ fields.push(field);
402
+ }
403
+ });
404
+ return fields;
405
+ };
406
+ const hasFields = (text) => {
407
+ if (!text) return false;
408
+ return SYSTEM_FIELDS.some((field) => text.includes(field.placeholder));
409
+ };
410
+ const DEFAULT_FIELD_DATA = {
411
+ name: "张三",
412
+ gender: "先生",
413
+ examName: "计算机应用能力考试",
414
+ score: "95",
415
+ scoreLevel: "优秀",
416
+ certificateNumber: "CERT-2024-001234",
417
+ validityPeriod: "2024-01-01 至 2027-01-01",
418
+ issueDate: "2024-01-01",
419
+ organization: "考试认证中心",
420
+ idNumber: "110101199001011234",
421
+ phone: "13800138000",
422
+ email: "zhangsan@example.com",
423
+ account: "user001",
424
+ examScore: "95",
425
+ issueDateMonth: "01",
426
+ issueDateDay: "01"
427
+ };
428
+ const _hoisted_1$5 = { class: "canvas-area" };
429
+ const _hoisted_2$5 = {
430
+ key: 0,
431
+ class: "certificate-border"
432
+ };
433
+ const _hoisted_3$5 = ["onClick", "onMousedown"];
434
+ const _hoisted_4$5 = {
435
+ key: 0,
436
+ class: "text-content"
437
+ };
438
+ const _hoisted_5$4 = ["innerHTML"];
439
+ const _hoisted_6$4 = {
440
+ key: 1,
441
+ class: "image-element"
442
+ };
443
+ const _hoisted_7$4 = ["src"];
444
+ const _hoisted_8$3 = {
445
+ key: 1,
446
+ class: "image-placeholder"
447
+ };
448
+ const _hoisted_9$3 = {
449
+ key: 2,
450
+ class: "qrcode-element"
451
+ };
452
+ const _hoisted_10$3 = ["src"];
453
+ const _hoisted_11$2 = {
454
+ key: 1,
455
+ class: "qrcode-content"
456
+ };
457
+ const _hoisted_12$2 = { class: "qrcode-info" };
458
+ const _hoisted_13$1 = {
459
+ key: 2,
460
+ class: "qrcode-placeholder"
461
+ };
462
+ const _hoisted_14$1 = {
463
+ key: 3,
464
+ class: "resize-handles"
465
+ };
466
+ const _hoisted_15$1 = ["onMousedown"];
467
+ const _hoisted_16$1 = ["onMousedown"];
468
+ const _hoisted_17$1 = ["onMousedown"];
469
+ const _hoisted_18$1 = ["onMousedown"];
470
+ const _hoisted_19$1 = ["onClick"];
471
+ const _sfc_main$5 = {
472
+ __name: "CanvasArea",
473
+ props: {
474
+ elements: {
475
+ type: Array,
476
+ default: () => []
477
+ },
478
+ selectedElement: {
479
+ type: Object,
480
+ default: null
481
+ },
482
+ fieldData: {
483
+ type: Object,
484
+ default: () => ({})
485
+ },
486
+ template: {
487
+ type: Object,
488
+ default: null
489
+ },
490
+ systemFields: {
491
+ type: Array,
492
+ default: () => SYSTEM_FIELDS
493
+ }
494
+ },
495
+ emits: ["select-element", "update-element", "delete-element"],
496
+ setup(__props, { emit: __emit }) {
497
+ const props = __props;
498
+ const emit = __emit;
499
+ const renderTextWithFields = (text) => {
500
+ if (!text) return "";
501
+ const fieldData = props.fieldData || {};
502
+ let result = text;
503
+ props.systemFields.forEach((field) => {
504
+ const placeholder = field.placeholder;
505
+ const value = fieldData[field.key] || placeholder;
506
+ if (fieldData[field.key]) {
507
+ const regex = new RegExp(placeholder.replace(/[{}]/g, "\\$&"), "g");
508
+ result = result.replace(regex, `<span class="field-value">${value}</span>`);
509
+ } else {
510
+ const regex = new RegExp(placeholder.replace(/[{}]/g, "\\$&"), "g");
511
+ result = result.replace(regex, `<span class="field-placeholder-highlight">${placeholder}</span>`);
512
+ }
513
+ });
514
+ return result;
515
+ };
516
+ const getImageSrc = (element) => {
517
+ if (!element) return null;
518
+ if (element.imageSource === "field") {
519
+ const fieldKey = element.imageField;
520
+ if (fieldKey && props.fieldData && props.fieldData[fieldKey]) {
521
+ return props.fieldData[fieldKey];
522
+ }
523
+ return null;
524
+ } else {
525
+ return element.imageUrl || null;
526
+ }
527
+ };
528
+ const getQrcodeContent = (element) => {
529
+ if (!element) return null;
530
+ if (element.qrcodeSource === "field") {
531
+ const fieldKey = element.qrcodeField;
532
+ if (fieldKey && props.fieldData && props.fieldData[fieldKey]) {
533
+ return props.fieldData[fieldKey];
534
+ }
535
+ return null;
536
+ } else {
537
+ return element.qrcodeContent || null;
538
+ }
539
+ };
540
+ const isImageUrl = (url) => {
541
+ if (!url) return false;
542
+ if (url.startsWith("data:image/")) return true;
543
+ if (url.startsWith("http://") || url.startsWith("https://")) {
544
+ const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".bmp"];
545
+ const lowerUrl = url.toLowerCase();
546
+ return imageExtensions.some((ext) => lowerUrl.includes(ext));
547
+ }
548
+ return false;
549
+ };
550
+ const getCanvasStyle = () => {
551
+ const baseStyle = {};
552
+ if (props.template) {
553
+ baseStyle.width = `${props.template.width}px`;
554
+ baseStyle.minHeight = `${props.template.height}px`;
555
+ } else {
556
+ baseStyle.width = "800px";
557
+ baseStyle.minHeight = "1000px";
558
+ }
559
+ if (props.template && props.template.background) {
560
+ if (props.template.background.startsWith("data:") || props.template.background.startsWith("http")) {
561
+ baseStyle.backgroundImage = `url(${props.template.background})`;
562
+ baseStyle.backgroundSize = "100% 100%";
563
+ baseStyle.backgroundPosition = "center";
564
+ baseStyle.backgroundRepeat = "no-repeat";
565
+ } else {
566
+ baseStyle.background = props.template.background;
567
+ baseStyle.backgroundSize = "100% 100%";
568
+ baseStyle.backgroundPosition = "center";
569
+ baseStyle.backgroundRepeat = "no-repeat";
570
+ }
571
+ }
572
+ return baseStyle;
573
+ };
574
+ const canvasRef = ref(null);
575
+ const isDragging = ref(false);
576
+ const isResizing = ref(false);
577
+ const dragStart = ref({ x: 0, y: 0 });
578
+ const resizeStart = ref({ x: 0, y: 0, width: 0, height: 0 });
579
+ const currentElement = ref(null);
580
+ const resizeDirection = ref("");
581
+ const getElementStyle = (element) => {
582
+ var _a, _b, _c, _d, _e, _f, _g, _h;
583
+ const baseStyle = {
584
+ position: "absolute",
585
+ left: `${element.x}px`,
586
+ top: `${element.y}px`,
587
+ width: `${element.width}px`,
588
+ minHeight: `${element.height}px`
589
+ };
590
+ if (element.type === "text" || element.type === "longtext") {
591
+ return {
592
+ ...baseStyle,
593
+ fontSize: `${((_a = element.style) == null ? void 0 : _a.fontSize) || 18}px`,
594
+ fontFamily: ((_b = element.style) == null ? void 0 : _b.fontFamily) || "方正书宋简体",
595
+ fontWeight: ((_c = element.style) == null ? void 0 : _c.fontWeight) || "normal",
596
+ fontStyle: ((_d = element.style) == null ? void 0 : _d.fontStyle) || "normal",
597
+ textDecoration: ((_e = element.style) == null ? void 0 : _e.textDecoration) || "none",
598
+ color: ((_f = element.style) == null ? void 0 : _f.color) || "#000000",
599
+ textAlign: ((_g = element.style) == null ? void 0 : _g.textAlign) || "left",
600
+ lineHeight: ((_h = element.style) == null ? void 0 : _h.lineHeight) || 1.2
601
+ };
602
+ }
603
+ return baseStyle;
604
+ };
605
+ const handleMouseDown = (e, element) => {
606
+ if (e.target.closest(".handle") || e.target.closest(".delete-btn")) {
607
+ return;
608
+ }
609
+ isDragging.value = true;
610
+ currentElement.value = element;
611
+ dragStart.value = {
612
+ x: e.clientX - element.x,
613
+ y: e.clientY - element.y
614
+ };
615
+ document.addEventListener("mousemove", handleMouseMove);
616
+ document.addEventListener("mouseup", handleMouseUp);
617
+ };
618
+ const handleMouseMove = (e) => {
619
+ if (isDragging.value && currentElement.value) {
620
+ const newX = e.clientX - dragStart.value.x;
621
+ const newY = e.clientY - dragStart.value.y;
622
+ const updatedElement = {
623
+ ...currentElement.value,
624
+ x: Math.max(0, newX),
625
+ y: Math.max(0, newY)
626
+ };
627
+ emit("update-element", updatedElement);
628
+ } else if (isResizing.value && currentElement.value) {
629
+ const deltaX = e.clientX - resizeStart.value.x;
630
+ const deltaY = e.clientY - resizeStart.value.y;
631
+ let newWidth = resizeStart.value.width;
632
+ let newHeight = resizeStart.value.height;
633
+ let newX = currentElement.value.x;
634
+ let newY = currentElement.value.y;
635
+ if (resizeDirection.value.includes("e")) {
636
+ newWidth = Math.max(50, resizeStart.value.width + deltaX);
637
+ }
638
+ if (resizeDirection.value.includes("w")) {
639
+ newWidth = Math.max(50, resizeStart.value.width - deltaX);
640
+ newX = currentElement.value.x + (resizeStart.value.width - newWidth);
641
+ }
642
+ if (resizeDirection.value.includes("s")) {
643
+ newHeight = Math.max(30, resizeStart.value.height + deltaY);
644
+ }
645
+ if (resizeDirection.value.includes("n")) {
646
+ newHeight = Math.max(30, resizeStart.value.height - deltaY);
647
+ newY = currentElement.value.y + (resizeStart.value.height - newHeight);
648
+ }
649
+ const updatedElement = {
650
+ ...currentElement.value,
651
+ x: newX,
652
+ y: newY,
653
+ width: newWidth,
654
+ height: newHeight
655
+ };
656
+ emit("update-element", updatedElement);
657
+ }
658
+ };
659
+ const handleMouseUp = () => {
660
+ isDragging.value = false;
661
+ isResizing.value = false;
662
+ currentElement.value = null;
663
+ resizeDirection.value = "";
664
+ document.removeEventListener("mousemove", handleMouseMove);
665
+ document.removeEventListener("mouseup", handleMouseUp);
666
+ };
667
+ const handleResizeStart = (e, element, direction) => {
668
+ e.stopPropagation();
669
+ isResizing.value = true;
670
+ currentElement.value = element;
671
+ resizeDirection.value = direction;
672
+ resizeStart.value = {
673
+ x: e.clientX,
674
+ y: e.clientY,
675
+ width: element.width,
676
+ height: element.height
677
+ };
678
+ document.addEventListener("mousemove", handleMouseMove);
679
+ document.addEventListener("mouseup", handleMouseUp);
680
+ };
681
+ onUnmounted(() => {
682
+ document.removeEventListener("mousemove", handleMouseMove);
683
+ document.removeEventListener("mouseup", handleMouseUp);
684
+ });
685
+ return (_ctx, _cache) => {
686
+ return openBlock(), createElementBlock("div", _hoisted_1$5, [
687
+ createElementVNode("div", {
688
+ class: "certificate-canvas",
689
+ ref_key: "canvasRef",
690
+ ref: canvasRef,
691
+ style: normalizeStyle(getCanvasStyle())
692
+ }, [
693
+ !props.template || !props.template.background ? (openBlock(), createElementBlock("div", _hoisted_2$5)) : createCommentVNode("", true),
694
+ (openBlock(true), createElementBlock(Fragment, null, renderList(__props.elements, (element) => {
695
+ var _a, _b, _c;
696
+ return openBlock(), createElementBlock("div", {
697
+ key: element.id,
698
+ class: normalizeClass(["editable-element", element.type, { selected: ((_a = __props.selectedElement) == null ? void 0 : _a.id) === element.id }]),
699
+ style: normalizeStyle(getElementStyle(element)),
700
+ onClick: withModifiers(($event) => _ctx.$emit("select-element", element), ["stop"]),
701
+ onMousedown: ($event) => handleMouseDown($event, element)
702
+ }, [
703
+ element.type === "text" || element.type === "longtext" ? (openBlock(), createElementBlock("div", _hoisted_4$5, [
704
+ createElementVNode("span", {
705
+ innerHTML: renderTextWithFields(element.content)
706
+ }, null, 8, _hoisted_5$4)
707
+ ])) : element.type === "image" ? (openBlock(), createElementBlock("div", _hoisted_6$4, [
708
+ getImageSrc(element) ? (openBlock(), createElementBlock("img", {
709
+ key: 0,
710
+ src: getImageSrc(element),
711
+ alt: "图片",
712
+ class: "element-image"
713
+ }, null, 8, _hoisted_7$4)) : (openBlock(), createElementBlock("div", _hoisted_8$3, [..._cache[0] || (_cache[0] = [
714
+ createElementVNode("span", null, "🖼️ 请设置图片", -1)
715
+ ])]))
716
+ ])) : element.type === "qrcode" ? (openBlock(), createElementBlock("div", _hoisted_9$3, [
717
+ getQrcodeContent(element) && isImageUrl(getQrcodeContent(element)) ? (openBlock(), createElementBlock("img", {
718
+ key: 0,
719
+ src: getQrcodeContent(element),
720
+ alt: "二维码",
721
+ class: "element-image"
722
+ }, null, 8, _hoisted_10$3)) : getQrcodeContent(element) ? (openBlock(), createElementBlock("div", _hoisted_11$2, [
723
+ _cache[1] || (_cache[1] = createElementVNode("div", { class: "qrcode-placeholder-text" }, "二维码", -1)),
724
+ createElementVNode("div", _hoisted_12$2, toDisplayString(getQrcodeContent(element)), 1)
725
+ ])) : (openBlock(), createElementBlock("div", _hoisted_13$1, [..._cache[2] || (_cache[2] = [
726
+ createElementVNode("span", null, "🔲 请设置二维码", -1)
727
+ ])]))
728
+ ])) : createCommentVNode("", true),
729
+ ((_b = __props.selectedElement) == null ? void 0 : _b.id) === element.id ? (openBlock(), createElementBlock("div", _hoisted_14$1, [
730
+ createElementVNode("div", {
731
+ class: "handle handle-nw",
732
+ onMousedown: withModifiers(($event) => handleResizeStart($event, element, "nw"), ["stop"])
733
+ }, null, 40, _hoisted_15$1),
734
+ createElementVNode("div", {
735
+ class: "handle handle-ne",
736
+ onMousedown: withModifiers(($event) => handleResizeStart($event, element, "ne"), ["stop"])
737
+ }, null, 40, _hoisted_16$1),
738
+ createElementVNode("div", {
739
+ class: "handle handle-sw",
740
+ onMousedown: withModifiers(($event) => handleResizeStart($event, element, "sw"), ["stop"])
741
+ }, null, 40, _hoisted_17$1),
742
+ createElementVNode("div", {
743
+ class: "handle handle-se",
744
+ onMousedown: withModifiers(($event) => handleResizeStart($event, element, "se"), ["stop"])
745
+ }, null, 40, _hoisted_18$1)
746
+ ])) : createCommentVNode("", true),
747
+ ((_c = __props.selectedElement) == null ? void 0 : _c.id) === element.id ? (openBlock(), createElementBlock("button", {
748
+ key: 4,
749
+ class: "delete-btn",
750
+ onClick: withModifiers(($event) => _ctx.$emit("delete-element", element.id), ["stop"])
751
+ }, " × ", 8, _hoisted_19$1)) : createCommentVNode("", true)
752
+ ], 46, _hoisted_3$5);
753
+ }), 128))
754
+ ], 4)
755
+ ]);
756
+ };
757
+ }
758
+ };
759
+ const CanvasArea = /* @__PURE__ */ _export_sfc(_sfc_main$5, [["__scopeId", "data-v-81f2131e"]]);
760
+ const _hoisted_1$4 = { class: "field-dialog" };
761
+ const _hoisted_2$4 = { class: "dialog-content" };
762
+ const _hoisted_3$4 = { class: "search-box" };
763
+ const _hoisted_4$4 = { class: "table-container" };
764
+ const _hoisted_5$3 = { class: "fields-table" };
765
+ const _hoisted_6$3 = { class: "field-name" };
766
+ const _hoisted_7$3 = { class: "field-action" };
767
+ const _hoisted_8$2 = ["onClick"];
768
+ const _hoisted_9$2 = {
769
+ key: 0,
770
+ class: "pagination"
771
+ };
772
+ const _hoisted_10$2 = ["disabled"];
773
+ const _hoisted_11$1 = ["onClick"];
774
+ const _hoisted_12$1 = ["disabled"];
775
+ const pageSize = 10;
776
+ const _sfc_main$4 = {
777
+ __name: "FieldDialog",
778
+ props: {
779
+ visible: {
780
+ type: Boolean,
781
+ default: false
782
+ },
783
+ currentText: {
784
+ type: String,
785
+ default: ""
786
+ },
787
+ selectionStart: {
788
+ type: Number,
789
+ default: 0
790
+ },
791
+ selectionEnd: {
792
+ type: Number,
793
+ default: 0
794
+ },
795
+ systemFields: {
796
+ type: Array,
797
+ default: () => SYSTEM_FIELDS
798
+ }
799
+ },
800
+ emits: ["close", "insert"],
801
+ setup(__props, { emit: __emit }) {
802
+ const props = __props;
803
+ const emit = __emit;
804
+ const searchText = ref("");
805
+ const currentPage = ref(1);
806
+ const filteredFields = computed(() => {
807
+ if (!searchText.value.trim()) {
808
+ return props.systemFields;
809
+ }
810
+ const searchLower = searchText.value.toLowerCase();
811
+ return props.systemFields.filter(
812
+ (field) => field.label.toLowerCase().includes(searchLower) || field.key.toLowerCase().includes(searchLower) || field.description && field.description.toLowerCase().includes(searchLower)
813
+ );
814
+ });
815
+ const totalPages = computed(() => {
816
+ return Math.ceil(filteredFields.value.length / pageSize);
817
+ });
818
+ const paginatedFields = computed(() => {
819
+ const start = (currentPage.value - 1) * pageSize;
820
+ const end = start + pageSize;
821
+ return filteredFields.value.slice(start, end);
822
+ });
823
+ const handleSearch = () => {
824
+ currentPage.value = 1;
825
+ };
826
+ const goToPage = (page) => {
827
+ if (page >= 1 && page <= totalPages.value) {
828
+ currentPage.value = page;
829
+ }
830
+ };
831
+ const handleSelectField = (field) => {
832
+ emit("insert", [field]);
833
+ };
834
+ const handleClose = () => {
835
+ searchText.value = "";
836
+ currentPage.value = 1;
837
+ emit("close");
838
+ };
839
+ return (_ctx, _cache) => {
840
+ return __props.visible ? (openBlock(), createElementBlock("div", {
841
+ key: 0,
842
+ class: "field-dialog-overlay",
843
+ onClick: withModifiers(handleClose, ["self"])
844
+ }, [
845
+ createElementVNode("div", _hoisted_1$4, [
846
+ createElementVNode("div", { class: "dialog-header" }, [
847
+ _cache[3] || (_cache[3] = createElementVNode("h3", null, "选择字段", -1)),
848
+ createElementVNode("button", {
849
+ class: "close-btn",
850
+ onClick: handleClose
851
+ }, "×")
852
+ ]),
853
+ createElementVNode("div", _hoisted_2$4, [
854
+ createElementVNode("div", _hoisted_3$4, [
855
+ withDirectives(createElementVNode("input", {
856
+ type: "text",
857
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => searchText.value = $event),
858
+ placeholder: "搜索字段...",
859
+ class: "search-input",
860
+ onInput: handleSearch
861
+ }, null, 544), [
862
+ [vModelText, searchText.value]
863
+ ])
864
+ ]),
865
+ createElementVNode("div", _hoisted_4$4, [
866
+ createElementVNode("table", _hoisted_5$3, [
867
+ _cache[4] || (_cache[4] = createElementVNode("thead", null, [
868
+ createElementVNode("tr", null, [
869
+ createElementVNode("th", null, "系统字段"),
870
+ createElementVNode("th", null, "操作")
871
+ ])
872
+ ], -1)),
873
+ createElementVNode("tbody", null, [
874
+ (openBlock(true), createElementBlock(Fragment, null, renderList(paginatedFields.value, (field) => {
875
+ return openBlock(), createElementBlock("tr", {
876
+ key: field.key,
877
+ class: "field-row"
878
+ }, [
879
+ createElementVNode("td", _hoisted_6$3, toDisplayString(field.label), 1),
880
+ createElementVNode("td", _hoisted_7$3, [
881
+ createElementVNode("button", {
882
+ class: "select-btn",
883
+ onClick: ($event) => handleSelectField(field)
884
+ }, " 选择 ", 8, _hoisted_8$2)
885
+ ])
886
+ ]);
887
+ }), 128))
888
+ ])
889
+ ])
890
+ ]),
891
+ totalPages.value > 1 ? (openBlock(), createElementBlock("div", _hoisted_9$2, [
892
+ createElementVNode("button", {
893
+ class: "page-btn",
894
+ disabled: currentPage.value === 1,
895
+ onClick: _cache[1] || (_cache[1] = ($event) => goToPage(currentPage.value - 1))
896
+ }, " < ", 8, _hoisted_10$2),
897
+ (openBlock(true), createElementBlock(Fragment, null, renderList(totalPages.value, (page) => {
898
+ return openBlock(), createElementBlock("button", {
899
+ key: page,
900
+ class: normalizeClass(["page-btn", { active: page === currentPage.value }]),
901
+ onClick: ($event) => goToPage(page)
902
+ }, toDisplayString(page), 11, _hoisted_11$1);
903
+ }), 128)),
904
+ createElementVNode("button", {
905
+ class: "page-btn",
906
+ disabled: currentPage.value === totalPages.value,
907
+ onClick: _cache[2] || (_cache[2] = ($event) => goToPage(currentPage.value + 1))
908
+ }, " > ", 8, _hoisted_12$1)
909
+ ])) : createCommentVNode("", true)
910
+ ])
911
+ ])
912
+ ])) : createCommentVNode("", true);
913
+ };
914
+ }
915
+ };
916
+ const FieldDialog = /* @__PURE__ */ _export_sfc(_sfc_main$4, [["__scopeId", "data-v-c4ecd92f"]]);
917
+ const _hoisted_1$3 = { class: "properties-panel" };
918
+ const _hoisted_2$3 = { class: "panel-content" };
919
+ const _hoisted_3$3 = {
920
+ key: 0,
921
+ class: "content-section"
922
+ };
923
+ const _hoisted_4$3 = { class: "form-group" };
924
+ const _hoisted_5$2 = { class: "content-input-wrapper" };
925
+ const _hoisted_6$2 = ["rows"];
926
+ const _hoisted_7$2 = {
927
+ key: 1,
928
+ class: "style-section"
929
+ };
930
+ const _hoisted_8$1 = { class: "form-group" };
931
+ const _hoisted_9$1 = { class: "form-group" };
932
+ const _hoisted_10$1 = { class: "form-group" };
933
+ const _hoisted_11 = { class: "style-buttons" };
934
+ const _hoisted_12 = { class: "form-group" };
935
+ const _hoisted_13 = { class: "form-group" };
936
+ const _hoisted_14 = { class: "align-buttons" };
937
+ const _hoisted_15 = { class: "form-group" };
938
+ const _hoisted_16 = {
939
+ key: 2,
940
+ class: "image-section"
941
+ };
942
+ const _hoisted_17 = { class: "form-group" };
943
+ const _hoisted_18 = { class: "radio-group" };
944
+ const _hoisted_19 = { class: "radio-label" };
945
+ const _hoisted_20 = { class: "radio-label" };
946
+ const _hoisted_21 = {
947
+ key: 0,
948
+ class: "form-group"
949
+ };
950
+ const _hoisted_22 = { class: "image-upload-wrapper" };
951
+ const _hoisted_23 = {
952
+ key: 0,
953
+ class: "image-preview"
954
+ };
955
+ const _hoisted_24 = ["src"];
956
+ const _hoisted_25 = {
957
+ key: 1,
958
+ class: "form-group"
959
+ };
960
+ const _hoisted_26 = ["value"];
961
+ const _hoisted_27 = {
962
+ key: 0,
963
+ class: "field-preview"
964
+ };
965
+ const _hoisted_28 = {
966
+ key: 3,
967
+ class: "qrcode-section"
968
+ };
969
+ const _hoisted_29 = { class: "form-group" };
970
+ const _hoisted_30 = { class: "radio-group" };
971
+ const _hoisted_31 = { class: "radio-label" };
972
+ const _hoisted_32 = { class: "radio-label" };
973
+ const _hoisted_33 = {
974
+ key: 0,
975
+ class: "form-group"
976
+ };
977
+ const _hoisted_34 = { class: "qrcode-upload-wrapper" };
978
+ const _hoisted_35 = {
979
+ key: 0,
980
+ class: "qrcode-preview"
981
+ };
982
+ const _hoisted_36 = ["src"];
983
+ const _hoisted_37 = {
984
+ key: 1,
985
+ class: "form-group"
986
+ };
987
+ const _hoisted_38 = ["value"];
988
+ const _hoisted_39 = {
989
+ key: 0,
990
+ class: "field-preview"
991
+ };
992
+ const _hoisted_40 = { class: "position-section" };
993
+ const _hoisted_41 = { class: "form-row" };
994
+ const _hoisted_42 = { class: "form-group" };
995
+ const _hoisted_43 = { class: "form-group" };
996
+ const _hoisted_44 = { class: "form-row" };
997
+ const _hoisted_45 = { class: "form-group" };
998
+ const _hoisted_46 = { class: "form-group" };
999
+ const _sfc_main$3 = {
1000
+ __name: "PropertiesPanel",
1001
+ props: {
1002
+ element: {
1003
+ type: Object,
1004
+ required: true
1005
+ },
1006
+ systemFields: {
1007
+ type: Array,
1008
+ default: () => SYSTEM_FIELDS
1009
+ }
1010
+ },
1011
+ emits: ["update-element"],
1012
+ setup(__props, { emit: __emit }) {
1013
+ const props = __props;
1014
+ const emit = __emit;
1015
+ const localContent = ref("");
1016
+ const localStyle = ref({});
1017
+ const localImageSource = ref("url");
1018
+ const localImageUrl = ref("");
1019
+ const localImageField = ref("");
1020
+ const localQrcodeSource = ref("url");
1021
+ const localQrcodeContent = ref("");
1022
+ const localQrcodeField = ref("");
1023
+ const showFieldDialog = ref(false);
1024
+ const selectionStart = ref(0);
1025
+ const selectionEnd = ref(0);
1026
+ const contentTextareaRef = ref(null);
1027
+ const imageFileInputRef = ref(null);
1028
+ const qrcodeFileInputRef = ref(null);
1029
+ const initLocalState = () => {
1030
+ localContent.value = props.element.content || "";
1031
+ localStyle.value = { ...props.element.style };
1032
+ if (props.element.type === "image") {
1033
+ localImageSource.value = props.element.imageSource || "url";
1034
+ localImageUrl.value = props.element.imageUrl || "";
1035
+ localImageField.value = props.element.imageField || "";
1036
+ }
1037
+ if (props.element.type === "qrcode") {
1038
+ localQrcodeSource.value = props.element.qrcodeSource || "url";
1039
+ localQrcodeContent.value = props.element.qrcodeContent || "";
1040
+ localQrcodeField.value = props.element.qrcodeField || "";
1041
+ }
1042
+ };
1043
+ watch(() => props.element, initLocalState, { immediate: true, deep: true });
1044
+ const updateElement = () => {
1045
+ const updated = { ...props.element };
1046
+ if (props.element.type === "text" || props.element.type === "longtext") {
1047
+ updated.content = localContent.value;
1048
+ updated.style = { ...localStyle.value };
1049
+ }
1050
+ if (props.element.type === "image") {
1051
+ updated.imageSource = localImageSource.value;
1052
+ updated.imageUrl = localImageUrl.value;
1053
+ updated.imageField = localImageField.value;
1054
+ }
1055
+ if (props.element.type === "qrcode") {
1056
+ updated.qrcodeSource = localQrcodeSource.value;
1057
+ updated.qrcodeContent = localQrcodeContent.value;
1058
+ updated.qrcodeField = localQrcodeField.value;
1059
+ }
1060
+ emit("update-element", updated);
1061
+ };
1062
+ const handleContentChange = () => {
1063
+ updateElement();
1064
+ };
1065
+ const handleTextareaFocus = (e) => {
1066
+ selectionStart.value = e.target.selectionStart;
1067
+ selectionEnd.value = e.target.selectionEnd;
1068
+ };
1069
+ const handleInsertField = (fields) => {
1070
+ if (fields.length > 0) {
1071
+ const field = fields[0];
1072
+ const textarea = contentTextareaRef.value;
1073
+ if (textarea) {
1074
+ const start = selectionStart.value;
1075
+ const end = selectionEnd.value;
1076
+ const text = localContent.value;
1077
+ const newText = text.substring(0, start) + field.placeholder + text.substring(end);
1078
+ localContent.value = newText;
1079
+ setTimeout(() => {
1080
+ const newPos = start + field.placeholder.length;
1081
+ textarea.setSelectionRange(newPos, newPos);
1082
+ textarea.focus();
1083
+ }, 0);
1084
+ updateElement();
1085
+ }
1086
+ }
1087
+ };
1088
+ const toggleStyle = (property, value1, value2) => {
1089
+ localStyle.value[property] = localStyle.value[property] === value1 ? value2 : value1;
1090
+ updateElement();
1091
+ };
1092
+ const setAlign = (align) => {
1093
+ localStyle.value.textAlign = align;
1094
+ updateElement();
1095
+ };
1096
+ const handleImageSourceChange = () => {
1097
+ updateElement();
1098
+ };
1099
+ const triggerImageUpload = () => {
1100
+ var _a;
1101
+ (_a = imageFileInputRef.value) == null ? void 0 : _a.click();
1102
+ };
1103
+ const handleImageUpload = async (event) => {
1104
+ var _a;
1105
+ const file = (_a = event.target.files) == null ? void 0 : _a[0];
1106
+ if (!file) return;
1107
+ const validation = validateImageFile(file);
1108
+ if (!validation.valid) {
1109
+ alert(validation.error);
1110
+ return;
1111
+ }
1112
+ try {
1113
+ const base64 = await fileToBase64(file);
1114
+ localImageUrl.value = base64;
1115
+ localImageSource.value = "url";
1116
+ updateElement();
1117
+ } catch (error) {
1118
+ alert("上传失败,请重试");
1119
+ console.error(error);
1120
+ }
1121
+ if (imageFileInputRef.value) {
1122
+ imageFileInputRef.value.value = "";
1123
+ }
1124
+ };
1125
+ const removeImage = () => {
1126
+ localImageUrl.value = "";
1127
+ updateElement();
1128
+ };
1129
+ const handleQrcodeSourceChange = () => {
1130
+ updateElement();
1131
+ };
1132
+ const triggerQrcodeUpload = () => {
1133
+ var _a;
1134
+ (_a = qrcodeFileInputRef.value) == null ? void 0 : _a.click();
1135
+ };
1136
+ const handleQrcodeUpload = async (event) => {
1137
+ var _a;
1138
+ const file = (_a = event.target.files) == null ? void 0 : _a[0];
1139
+ if (!file) return;
1140
+ const validation = validateImageFile(file);
1141
+ if (!validation.valid) {
1142
+ alert(validation.error);
1143
+ return;
1144
+ }
1145
+ try {
1146
+ const base64 = await fileToBase64(file);
1147
+ localQrcodeContent.value = base64;
1148
+ localQrcodeSource.value = "url";
1149
+ updateElement();
1150
+ } catch (error) {
1151
+ alert("上传失败,请重试");
1152
+ console.error(error);
1153
+ }
1154
+ if (qrcodeFileInputRef.value) {
1155
+ qrcodeFileInputRef.value.value = "";
1156
+ }
1157
+ };
1158
+ const removeQrcode = () => {
1159
+ localQrcodeContent.value = "";
1160
+ updateElement();
1161
+ };
1162
+ const getFieldLabel = (key) => {
1163
+ const field = props.systemFields.find((f) => f.key === key);
1164
+ if (field) return field.label;
1165
+ const defaultField = getFieldByKey(key);
1166
+ return defaultField ? defaultField.label : key;
1167
+ };
1168
+ const isImageUrl = (url) => {
1169
+ if (!url) return false;
1170
+ if (url.startsWith("data:image/")) return true;
1171
+ if (url.startsWith("http://") || url.startsWith("https://")) {
1172
+ const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".bmp"];
1173
+ const lowerUrl = url.toLowerCase();
1174
+ return imageExtensions.some((ext) => lowerUrl.includes(ext));
1175
+ }
1176
+ return false;
1177
+ };
1178
+ return (_ctx, _cache) => {
1179
+ return openBlock(), createElementBlock("div", _hoisted_1$3, [
1180
+ _cache[63] || (_cache[63] = createElementVNode("div", { class: "panel-header" }, [
1181
+ createElementVNode("h3", null, "属性设置")
1182
+ ], -1)),
1183
+ createElementVNode("div", _hoisted_2$3, [
1184
+ __props.element.type === "text" || __props.element.type === "longtext" ? (openBlock(), createElementBlock("div", _hoisted_3$3, [
1185
+ createElementVNode("div", _hoisted_4$3, [
1186
+ _cache[26] || (_cache[26] = createElementVNode("label", { class: "form-label" }, "内容", -1)),
1187
+ createElementVNode("div", _hoisted_5$2, [
1188
+ withDirectives(createElementVNode("textarea", {
1189
+ ref_key: "contentTextareaRef",
1190
+ ref: contentTextareaRef,
1191
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => localContent.value = $event),
1192
+ onInput: handleContentChange,
1193
+ onFocus: handleTextareaFocus,
1194
+ rows: __props.element.type === "longtext" ? 6 : 3,
1195
+ class: "content-textarea"
1196
+ }, null, 40, _hoisted_6$2), [
1197
+ [vModelText, localContent.value]
1198
+ ]),
1199
+ createElementVNode("button", {
1200
+ class: "insert-field-btn",
1201
+ onClick: _cache[1] || (_cache[1] = ($event) => showFieldDialog.value = true)
1202
+ }, [..._cache[25] || (_cache[25] = [
1203
+ createElementVNode("span", null, "+", -1),
1204
+ createElementVNode("span", null, "在内容中关联系统字段", -1)
1205
+ ])])
1206
+ ])
1207
+ ])
1208
+ ])) : createCommentVNode("", true),
1209
+ __props.element.type === "text" || __props.element.type === "longtext" ? (openBlock(), createElementBlock("div", _hoisted_7$2, [
1210
+ _cache[38] || (_cache[38] = createElementVNode("div", { class: "section-title" }, "文本样式", -1)),
1211
+ createElementVNode("div", _hoisted_8$1, [
1212
+ _cache[28] || (_cache[28] = createElementVNode("label", { class: "form-label" }, "字体大小", -1)),
1213
+ withDirectives(createElementVNode("select", {
1214
+ "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => localStyle.value.fontSize = $event),
1215
+ onChange: updateElement,
1216
+ class: "form-select"
1217
+ }, [..._cache[27] || (_cache[27] = [
1218
+ createElementVNode("option", { value: 12 }, "12px", -1),
1219
+ createElementVNode("option", { value: 14 }, "14px", -1),
1220
+ createElementVNode("option", { value: 16 }, "16px", -1),
1221
+ createElementVNode("option", { value: 18 }, "18px", -1),
1222
+ createElementVNode("option", { value: 20 }, "20px", -1),
1223
+ createElementVNode("option", { value: 24 }, "24px", -1),
1224
+ createElementVNode("option", { value: 28 }, "28px", -1),
1225
+ createElementVNode("option", { value: 32 }, "32px", -1),
1226
+ createElementVNode("option", { value: 36 }, "36px", -1),
1227
+ createElementVNode("option", { value: 48 }, "48px", -1)
1228
+ ])], 544), [
1229
+ [
1230
+ vModelSelect,
1231
+ localStyle.value.fontSize,
1232
+ void 0,
1233
+ { number: true }
1234
+ ]
1235
+ ])
1236
+ ]),
1237
+ createElementVNode("div", _hoisted_9$1, [
1238
+ _cache[30] || (_cache[30] = createElementVNode("label", { class: "form-label" }, "字体", -1)),
1239
+ withDirectives(createElementVNode("select", {
1240
+ "onUpdate:modelValue": _cache[3] || (_cache[3] = ($event) => localStyle.value.fontFamily = $event),
1241
+ onChange: updateElement,
1242
+ class: "form-select"
1243
+ }, [..._cache[29] || (_cache[29] = [
1244
+ createStaticVNode('<option value="方正书宋简体" data-v-7662a3c6>方正书宋简体</option><option value="微软雅黑" data-v-7662a3c6>微软雅黑</option><option value="宋体" data-v-7662a3c6>宋体</option><option value="黑体" data-v-7662a3c6>黑体</option><option value="Arial" data-v-7662a3c6>Arial</option>', 5)
1245
+ ])], 544), [
1246
+ [vModelSelect, localStyle.value.fontFamily]
1247
+ ])
1248
+ ]),
1249
+ createElementVNode("div", _hoisted_10$1, [
1250
+ _cache[34] || (_cache[34] = createElementVNode("label", { class: "form-label" }, "文字样式", -1)),
1251
+ createElementVNode("div", _hoisted_11, [
1252
+ createElementVNode("button", {
1253
+ class: normalizeClass(["style-btn", { active: localStyle.value.fontWeight === "bold" }]),
1254
+ onClick: _cache[4] || (_cache[4] = ($event) => toggleStyle("fontWeight", "bold", "normal"))
1255
+ }, [..._cache[31] || (_cache[31] = [
1256
+ createElementVNode("strong", null, "B", -1)
1257
+ ])], 2),
1258
+ createElementVNode("button", {
1259
+ class: normalizeClass(["style-btn", { active: localStyle.value.fontStyle === "italic" }]),
1260
+ onClick: _cache[5] || (_cache[5] = ($event) => toggleStyle("fontStyle", "italic", "normal"))
1261
+ }, [..._cache[32] || (_cache[32] = [
1262
+ createElementVNode("em", null, "I", -1)
1263
+ ])], 2),
1264
+ createElementVNode("button", {
1265
+ class: normalizeClass(["style-btn", { active: localStyle.value.textDecoration === "underline" }]),
1266
+ onClick: _cache[6] || (_cache[6] = ($event) => toggleStyle("textDecoration", "underline", "none"))
1267
+ }, [..._cache[33] || (_cache[33] = [
1268
+ createElementVNode("u", null, "U", -1)
1269
+ ])], 2)
1270
+ ])
1271
+ ]),
1272
+ createElementVNode("div", _hoisted_12, [
1273
+ _cache[35] || (_cache[35] = createElementVNode("label", { class: "form-label" }, "颜色", -1)),
1274
+ withDirectives(createElementVNode("input", {
1275
+ type: "color",
1276
+ "onUpdate:modelValue": _cache[7] || (_cache[7] = ($event) => localStyle.value.color = $event),
1277
+ onChange: updateElement,
1278
+ class: "color-input"
1279
+ }, null, 544), [
1280
+ [vModelText, localStyle.value.color]
1281
+ ])
1282
+ ]),
1283
+ createElementVNode("div", _hoisted_13, [
1284
+ _cache[36] || (_cache[36] = createElementVNode("label", { class: "form-label" }, "对齐方式", -1)),
1285
+ createElementVNode("div", _hoisted_14, [
1286
+ createElementVNode("button", {
1287
+ class: normalizeClass(["align-btn", { active: localStyle.value.textAlign === "left" }]),
1288
+ onClick: _cache[8] || (_cache[8] = ($event) => setAlign("left"))
1289
+ }, " 左对齐 ", 2),
1290
+ createElementVNode("button", {
1291
+ class: normalizeClass(["align-btn", { active: localStyle.value.textAlign === "center" }]),
1292
+ onClick: _cache[9] || (_cache[9] = ($event) => setAlign("center"))
1293
+ }, " 居中 ", 2),
1294
+ createElementVNode("button", {
1295
+ class: normalizeClass(["align-btn", { active: localStyle.value.textAlign === "right" }]),
1296
+ onClick: _cache[10] || (_cache[10] = ($event) => setAlign("right"))
1297
+ }, " 右对齐 ", 2)
1298
+ ])
1299
+ ]),
1300
+ createElementVNode("div", _hoisted_15, [
1301
+ _cache[37] || (_cache[37] = createElementVNode("label", { class: "form-label" }, "行高", -1)),
1302
+ withDirectives(createElementVNode("input", {
1303
+ type: "number",
1304
+ "onUpdate:modelValue": _cache[11] || (_cache[11] = ($event) => localStyle.value.lineHeight = $event),
1305
+ onInput: updateElement,
1306
+ step: "0.1",
1307
+ min: "0.5",
1308
+ max: "3",
1309
+ class: "form-input"
1310
+ }, null, 544), [
1311
+ [
1312
+ vModelText,
1313
+ localStyle.value.lineHeight,
1314
+ void 0,
1315
+ { number: true }
1316
+ ]
1317
+ ])
1318
+ ])
1319
+ ])) : createCommentVNode("", true),
1320
+ __props.element.type === "image" ? (openBlock(), createElementBlock("div", _hoisted_16, [
1321
+ _cache[49] || (_cache[49] = createElementVNode("div", { class: "section-title" }, "图片设置", -1)),
1322
+ createElementVNode("div", _hoisted_17, [
1323
+ _cache[41] || (_cache[41] = createElementVNode("label", { class: "form-label" }, "图片来源", -1)),
1324
+ createElementVNode("div", _hoisted_18, [
1325
+ createElementVNode("label", _hoisted_19, [
1326
+ withDirectives(createElementVNode("input", {
1327
+ type: "radio",
1328
+ value: "url",
1329
+ "onUpdate:modelValue": _cache[12] || (_cache[12] = ($event) => localImageSource.value = $event),
1330
+ onChange: handleImageSourceChange
1331
+ }, null, 544), [
1332
+ [vModelRadio, localImageSource.value]
1333
+ ]),
1334
+ _cache[39] || (_cache[39] = createElementVNode("span", null, "URL", -1))
1335
+ ]),
1336
+ createElementVNode("label", _hoisted_20, [
1337
+ withDirectives(createElementVNode("input", {
1338
+ type: "radio",
1339
+ value: "field",
1340
+ "onUpdate:modelValue": _cache[13] || (_cache[13] = ($event) => localImageSource.value = $event),
1341
+ onChange: handleImageSourceChange
1342
+ }, null, 544), [
1343
+ [vModelRadio, localImageSource.value]
1344
+ ]),
1345
+ _cache[40] || (_cache[40] = createElementVNode("span", null, "关联字段", -1))
1346
+ ])
1347
+ ])
1348
+ ]),
1349
+ localImageSource.value === "url" ? (openBlock(), createElementBlock("div", _hoisted_21, [
1350
+ _cache[43] || (_cache[43] = createElementVNode("label", { class: "form-label" }, "图片URL", -1)),
1351
+ createElementVNode("div", _hoisted_22, [
1352
+ createElementVNode("input", {
1353
+ ref_key: "imageFileInputRef",
1354
+ ref: imageFileInputRef,
1355
+ type: "file",
1356
+ accept: "image/jpeg,image/jpg,image/png",
1357
+ onChange: handleImageUpload,
1358
+ style: { "display": "none" }
1359
+ }, null, 544),
1360
+ createElementVNode("button", {
1361
+ class: "upload-btn",
1362
+ onClick: triggerImageUpload
1363
+ }, [..._cache[42] || (_cache[42] = [
1364
+ createElementVNode("span", null, "📁", -1),
1365
+ createElementVNode("span", null, "本地上传", -1)
1366
+ ])]),
1367
+ withDirectives(createElementVNode("input", {
1368
+ type: "text",
1369
+ "onUpdate:modelValue": _cache[14] || (_cache[14] = ($event) => localImageUrl.value = $event),
1370
+ onInput: updateElement,
1371
+ placeholder: "请输入图片URL",
1372
+ class: "image-url-input"
1373
+ }, null, 544), [
1374
+ [vModelText, localImageUrl.value]
1375
+ ]),
1376
+ localImageUrl.value ? (openBlock(), createElementBlock("button", {
1377
+ key: 0,
1378
+ class: "remove-btn",
1379
+ onClick: removeImage
1380
+ }, "移除")) : createCommentVNode("", true)
1381
+ ]),
1382
+ localImageUrl.value ? (openBlock(), createElementBlock("div", _hoisted_23, [
1383
+ createElementVNode("img", {
1384
+ src: localImageUrl.value,
1385
+ alt: "预览"
1386
+ }, null, 8, _hoisted_24)
1387
+ ])) : createCommentVNode("", true)
1388
+ ])) : createCommentVNode("", true),
1389
+ localImageSource.value === "field" ? (openBlock(), createElementBlock("div", _hoisted_25, [
1390
+ _cache[48] || (_cache[48] = createElementVNode("label", { class: "form-label" }, "关联字段", -1)),
1391
+ withDirectives(createElementVNode("select", {
1392
+ "onUpdate:modelValue": _cache[15] || (_cache[15] = ($event) => localImageField.value = $event),
1393
+ onChange: updateElement,
1394
+ class: "form-select"
1395
+ }, [
1396
+ _cache[44] || (_cache[44] = createElementVNode("option", { value: "" }, "请选择字段", -1)),
1397
+ _cache[45] || (_cache[45] = createElementVNode("option", { value: "avatar" }, "头像 (avatar)", -1)),
1398
+ _cache[46] || (_cache[46] = createElementVNode("option", { value: "photo" }, "照片 (photo)", -1)),
1399
+ _cache[47] || (_cache[47] = createElementVNode("option", { value: "headImage" }, "头像图片 (headImage)", -1)),
1400
+ (openBlock(true), createElementBlock(Fragment, null, renderList(__props.systemFields, (field) => {
1401
+ return openBlock(), createElementBlock("option", {
1402
+ key: field.key,
1403
+ value: field.key
1404
+ }, toDisplayString(field.label) + " (" + toDisplayString(field.placeholder) + ") ", 9, _hoisted_26);
1405
+ }), 128))
1406
+ ], 544), [
1407
+ [vModelSelect, localImageField.value]
1408
+ ]),
1409
+ localImageField.value ? (openBlock(), createElementBlock("div", _hoisted_27, " 字段: " + toDisplayString(getFieldLabel(localImageField.value)), 1)) : createCommentVNode("", true)
1410
+ ])) : createCommentVNode("", true)
1411
+ ])) : createCommentVNode("", true),
1412
+ __props.element.type === "qrcode" ? (openBlock(), createElementBlock("div", _hoisted_28, [
1413
+ _cache[57] || (_cache[57] = createElementVNode("div", { class: "section-title" }, "二维码设置", -1)),
1414
+ createElementVNode("div", _hoisted_29, [
1415
+ _cache[52] || (_cache[52] = createElementVNode("label", { class: "form-label" }, "二维码来源", -1)),
1416
+ createElementVNode("div", _hoisted_30, [
1417
+ createElementVNode("label", _hoisted_31, [
1418
+ withDirectives(createElementVNode("input", {
1419
+ type: "radio",
1420
+ value: "url",
1421
+ "onUpdate:modelValue": _cache[16] || (_cache[16] = ($event) => localQrcodeSource.value = $event),
1422
+ onChange: handleQrcodeSourceChange
1423
+ }, null, 544), [
1424
+ [vModelRadio, localQrcodeSource.value]
1425
+ ]),
1426
+ _cache[50] || (_cache[50] = createElementVNode("span", null, "URL/内容", -1))
1427
+ ]),
1428
+ createElementVNode("label", _hoisted_32, [
1429
+ withDirectives(createElementVNode("input", {
1430
+ type: "radio",
1431
+ value: "field",
1432
+ "onUpdate:modelValue": _cache[17] || (_cache[17] = ($event) => localQrcodeSource.value = $event),
1433
+ onChange: handleQrcodeSourceChange
1434
+ }, null, 544), [
1435
+ [vModelRadio, localQrcodeSource.value]
1436
+ ]),
1437
+ _cache[51] || (_cache[51] = createElementVNode("span", null, "关联字段", -1))
1438
+ ])
1439
+ ])
1440
+ ]),
1441
+ localQrcodeSource.value === "url" ? (openBlock(), createElementBlock("div", _hoisted_33, [
1442
+ _cache[54] || (_cache[54] = createElementVNode("label", { class: "form-label" }, "二维码内容/图片URL", -1)),
1443
+ createElementVNode("div", _hoisted_34, [
1444
+ createElementVNode("input", {
1445
+ ref_key: "qrcodeFileInputRef",
1446
+ ref: qrcodeFileInputRef,
1447
+ type: "file",
1448
+ accept: "image/jpeg,image/jpg,image/png",
1449
+ onChange: handleQrcodeUpload,
1450
+ style: { "display": "none" }
1451
+ }, null, 544),
1452
+ createElementVNode("button", {
1453
+ class: "upload-btn",
1454
+ onClick: triggerQrcodeUpload
1455
+ }, [..._cache[53] || (_cache[53] = [
1456
+ createElementVNode("span", null, "📁", -1),
1457
+ createElementVNode("span", null, "本地上传", -1)
1458
+ ])]),
1459
+ withDirectives(createElementVNode("input", {
1460
+ type: "text",
1461
+ "onUpdate:modelValue": _cache[18] || (_cache[18] = ($event) => localQrcodeContent.value = $event),
1462
+ onInput: updateElement,
1463
+ placeholder: "请输入二维码内容或图片URL",
1464
+ class: "qrcode-input"
1465
+ }, null, 544), [
1466
+ [vModelText, localQrcodeContent.value]
1467
+ ]),
1468
+ localQrcodeContent.value ? (openBlock(), createElementBlock("button", {
1469
+ key: 0,
1470
+ class: "remove-btn",
1471
+ onClick: removeQrcode
1472
+ }, "移除")) : createCommentVNode("", true)
1473
+ ]),
1474
+ localQrcodeContent.value && isImageUrl(localQrcodeContent.value) ? (openBlock(), createElementBlock("div", _hoisted_35, [
1475
+ createElementVNode("img", {
1476
+ src: localQrcodeContent.value,
1477
+ alt: "预览"
1478
+ }, null, 8, _hoisted_36)
1479
+ ])) : createCommentVNode("", true)
1480
+ ])) : createCommentVNode("", true),
1481
+ localQrcodeSource.value === "field" ? (openBlock(), createElementBlock("div", _hoisted_37, [
1482
+ _cache[56] || (_cache[56] = createElementVNode("label", { class: "form-label" }, "关联字段", -1)),
1483
+ withDirectives(createElementVNode("select", {
1484
+ "onUpdate:modelValue": _cache[19] || (_cache[19] = ($event) => localQrcodeField.value = $event),
1485
+ onChange: updateElement,
1486
+ class: "form-select"
1487
+ }, [
1488
+ _cache[55] || (_cache[55] = createStaticVNode('<option value="" data-v-7662a3c6>请选择字段</option><option value="certificateNumber" data-v-7662a3c6>证书编号 (certificateNumber)</option><option value="idNumber" data-v-7662a3c6>身份证号 (idNumber)</option><option value="url" data-v-7662a3c6>链接地址 (url)</option><option value="examName" data-v-7662a3c6>考试名称 (examName)</option>', 5)),
1489
+ (openBlock(true), createElementBlock(Fragment, null, renderList(__props.systemFields, (field) => {
1490
+ return openBlock(), createElementBlock("option", {
1491
+ key: field.key,
1492
+ value: field.key
1493
+ }, toDisplayString(field.label) + " (" + toDisplayString(field.placeholder) + ") ", 9, _hoisted_38);
1494
+ }), 128))
1495
+ ], 544), [
1496
+ [vModelSelect, localQrcodeField.value]
1497
+ ]),
1498
+ localQrcodeField.value ? (openBlock(), createElementBlock("div", _hoisted_39, " 字段: " + toDisplayString(getFieldLabel(localQrcodeField.value)), 1)) : createCommentVNode("", true)
1499
+ ])) : createCommentVNode("", true)
1500
+ ])) : createCommentVNode("", true),
1501
+ createElementVNode("div", _hoisted_40, [
1502
+ _cache[62] || (_cache[62] = createElementVNode("div", { class: "section-title" }, "位置和尺寸", -1)),
1503
+ createElementVNode("div", _hoisted_41, [
1504
+ createElementVNode("div", _hoisted_42, [
1505
+ _cache[58] || (_cache[58] = createElementVNode("label", { class: "form-label" }, "X", -1)),
1506
+ withDirectives(createElementVNode("input", {
1507
+ type: "number",
1508
+ "onUpdate:modelValue": _cache[20] || (_cache[20] = ($event) => __props.element.x = $event),
1509
+ onInput: updateElement,
1510
+ class: "form-input"
1511
+ }, null, 544), [
1512
+ [
1513
+ vModelText,
1514
+ __props.element.x,
1515
+ void 0,
1516
+ { number: true }
1517
+ ]
1518
+ ])
1519
+ ]),
1520
+ createElementVNode("div", _hoisted_43, [
1521
+ _cache[59] || (_cache[59] = createElementVNode("label", { class: "form-label" }, "Y", -1)),
1522
+ withDirectives(createElementVNode("input", {
1523
+ type: "number",
1524
+ "onUpdate:modelValue": _cache[21] || (_cache[21] = ($event) => __props.element.y = $event),
1525
+ onInput: updateElement,
1526
+ class: "form-input"
1527
+ }, null, 544), [
1528
+ [
1529
+ vModelText,
1530
+ __props.element.y,
1531
+ void 0,
1532
+ { number: true }
1533
+ ]
1534
+ ])
1535
+ ])
1536
+ ]),
1537
+ createElementVNode("div", _hoisted_44, [
1538
+ createElementVNode("div", _hoisted_45, [
1539
+ _cache[60] || (_cache[60] = createElementVNode("label", { class: "form-label" }, "宽度", -1)),
1540
+ withDirectives(createElementVNode("input", {
1541
+ type: "number",
1542
+ "onUpdate:modelValue": _cache[22] || (_cache[22] = ($event) => __props.element.width = $event),
1543
+ onInput: updateElement,
1544
+ class: "form-input"
1545
+ }, null, 544), [
1546
+ [
1547
+ vModelText,
1548
+ __props.element.width,
1549
+ void 0,
1550
+ { number: true }
1551
+ ]
1552
+ ])
1553
+ ]),
1554
+ createElementVNode("div", _hoisted_46, [
1555
+ _cache[61] || (_cache[61] = createElementVNode("label", { class: "form-label" }, "高度", -1)),
1556
+ withDirectives(createElementVNode("input", {
1557
+ type: "number",
1558
+ "onUpdate:modelValue": _cache[23] || (_cache[23] = ($event) => __props.element.height = $event),
1559
+ onInput: updateElement,
1560
+ class: "form-input"
1561
+ }, null, 544), [
1562
+ [
1563
+ vModelText,
1564
+ __props.element.height,
1565
+ void 0,
1566
+ { number: true }
1567
+ ]
1568
+ ])
1569
+ ])
1570
+ ])
1571
+ ])
1572
+ ]),
1573
+ createVNode(FieldDialog, {
1574
+ visible: showFieldDialog.value,
1575
+ "current-text": localContent.value,
1576
+ "selection-start": selectionStart.value,
1577
+ "selection-end": selectionEnd.value,
1578
+ "system-fields": __props.systemFields,
1579
+ onClose: _cache[24] || (_cache[24] = ($event) => showFieldDialog.value = false),
1580
+ onInsert: handleInsertField
1581
+ }, null, 8, ["visible", "current-text", "selection-start", "selection-end", "system-fields"])
1582
+ ]);
1583
+ };
1584
+ }
1585
+ };
1586
+ const PropertiesPanel = /* @__PURE__ */ _export_sfc(_sfc_main$3, [["__scopeId", "data-v-7662a3c6"]]);
1587
+ const _hoisted_1$2 = { class: "field-data-panel" };
1588
+ const _hoisted_2$2 = { class: "panel-content" };
1589
+ const _hoisted_3$2 = { class: "category-title" };
1590
+ const _hoisted_4$2 = { class: "fields-list" };
1591
+ const _hoisted_5$1 = { class: "field-label" };
1592
+ const _hoisted_6$1 = ["onUpdate:modelValue", "placeholder"];
1593
+ const _hoisted_7$1 = { class: "field-hint" };
1594
+ const _sfc_main$2 = {
1595
+ __name: "FieldDataPanel",
1596
+ props: {
1597
+ fieldData: {
1598
+ type: Object,
1599
+ default: () => ({})
1600
+ }
1601
+ },
1602
+ emits: ["update-field-data"],
1603
+ setup(__props, { emit: __emit }) {
1604
+ const props = __props;
1605
+ const emit = __emit;
1606
+ const localFieldData = ref({ ...props.fieldData });
1607
+ const fieldsByCategory = computed(() => FIELDS_BY_CATEGORY);
1608
+ watch(() => props.fieldData, (newData) => {
1609
+ localFieldData.value = { ...newData };
1610
+ }, { deep: true });
1611
+ const updateFieldData = () => {
1612
+ emit("update-field-data", { ...localFieldData.value });
1613
+ };
1614
+ const resetData = () => {
1615
+ localFieldData.value = { ...DEFAULT_FIELD_DATA };
1616
+ updateFieldData();
1617
+ };
1618
+ return (_ctx, _cache) => {
1619
+ return openBlock(), createElementBlock("div", _hoisted_1$2, [
1620
+ createElementVNode("div", { class: "panel-header" }, [
1621
+ _cache[0] || (_cache[0] = createElementVNode("h3", null, "字段数据", -1)),
1622
+ createElementVNode("button", {
1623
+ class: "reset-btn",
1624
+ onClick: resetData
1625
+ }, "重置")
1626
+ ]),
1627
+ createElementVNode("div", _hoisted_2$2, [
1628
+ (openBlock(true), createElementBlock(Fragment, null, renderList(fieldsByCategory.value, (fields, category) => {
1629
+ return openBlock(), createElementBlock("div", {
1630
+ key: category,
1631
+ class: "field-category"
1632
+ }, [
1633
+ createElementVNode("div", _hoisted_3$2, toDisplayString(category), 1),
1634
+ createElementVNode("div", _hoisted_4$2, [
1635
+ (openBlock(true), createElementBlock(Fragment, null, renderList(fields, (field) => {
1636
+ return openBlock(), createElementBlock("div", {
1637
+ key: field.key,
1638
+ class: "field-item"
1639
+ }, [
1640
+ createElementVNode("label", _hoisted_5$1, toDisplayString(field.label), 1),
1641
+ withDirectives(createElementVNode("input", {
1642
+ "onUpdate:modelValue": ($event) => localFieldData.value[field.key] = $event,
1643
+ onInput: updateFieldData,
1644
+ type: "text",
1645
+ class: "field-input",
1646
+ placeholder: field.placeholder
1647
+ }, null, 40, _hoisted_6$1), [
1648
+ [vModelText, localFieldData.value[field.key]]
1649
+ ]),
1650
+ createElementVNode("div", _hoisted_7$1, toDisplayString(field.description), 1)
1651
+ ]);
1652
+ }), 128))
1653
+ ])
1654
+ ]);
1655
+ }), 128))
1656
+ ])
1657
+ ]);
1658
+ };
1659
+ }
1660
+ };
1661
+ const FieldDataPanel = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["__scopeId", "data-v-33e53b63"]]);
1662
+ const _hoisted_1$1 = { class: "preview-dialog" };
1663
+ const _hoisted_2$1 = { class: "preview-content" };
1664
+ const _hoisted_3$1 = { class: "preview-canvas-wrapper" };
1665
+ const _hoisted_4$1 = {
1666
+ key: 0,
1667
+ class: "preview-text-content"
1668
+ };
1669
+ const _hoisted_5 = ["innerHTML"];
1670
+ const _hoisted_6 = {
1671
+ key: 1,
1672
+ class: "preview-image-element"
1673
+ };
1674
+ const _hoisted_7 = ["src"];
1675
+ const _hoisted_8 = {
1676
+ key: 2,
1677
+ class: "preview-qrcode-element"
1678
+ };
1679
+ const _hoisted_9 = ["src"];
1680
+ const _hoisted_10 = {
1681
+ key: 1,
1682
+ class: "preview-qrcode-text"
1683
+ };
1684
+ const _sfc_main$1 = {
1685
+ __name: "CertificatePreview",
1686
+ props: {
1687
+ visible: {
1688
+ type: Boolean,
1689
+ default: false
1690
+ },
1691
+ elements: {
1692
+ type: Array,
1693
+ default: () => []
1694
+ },
1695
+ template: {
1696
+ type: Object,
1697
+ default: null
1698
+ },
1699
+ fieldData: {
1700
+ type: Object,
1701
+ default: () => ({})
1702
+ },
1703
+ systemFields: {
1704
+ type: Array,
1705
+ default: () => SYSTEM_FIELDS
1706
+ }
1707
+ },
1708
+ emits: ["close"],
1709
+ setup(__props, { emit: __emit }) {
1710
+ const props = __props;
1711
+ const emit = __emit;
1712
+ const handleClose = () => {
1713
+ emit("close");
1714
+ };
1715
+ const getCanvasStyle = () => {
1716
+ const baseStyle = {};
1717
+ if (props.template) {
1718
+ baseStyle.width = `${props.template.width}px`;
1719
+ baseStyle.height = `${props.template.height}px`;
1720
+ } else {
1721
+ baseStyle.width = "800px";
1722
+ baseStyle.height = "1000px";
1723
+ }
1724
+ if (props.template && props.template.background) {
1725
+ if (props.template.background.startsWith("data:") || props.template.background.startsWith("http")) {
1726
+ baseStyle.backgroundImage = `url(${props.template.background})`;
1727
+ baseStyle.backgroundSize = "100% 100%";
1728
+ baseStyle.backgroundPosition = "center";
1729
+ baseStyle.backgroundRepeat = "no-repeat";
1730
+ } else {
1731
+ baseStyle.background = props.template.background;
1732
+ baseStyle.backgroundSize = "100% 100%";
1733
+ baseStyle.backgroundPosition = "center";
1734
+ baseStyle.backgroundRepeat = "no-repeat";
1735
+ }
1736
+ }
1737
+ return baseStyle;
1738
+ };
1739
+ const getElementStyle = (element) => {
1740
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1741
+ const baseStyle = {
1742
+ position: "absolute",
1743
+ left: `${element.x}px`,
1744
+ top: `${element.y}px`,
1745
+ width: `${element.width}px`,
1746
+ minHeight: `${element.height}px`
1747
+ };
1748
+ if (element.type === "text" || element.type === "longtext") {
1749
+ return {
1750
+ ...baseStyle,
1751
+ fontSize: `${((_a = element.style) == null ? void 0 : _a.fontSize) || 18}px`,
1752
+ fontFamily: ((_b = element.style) == null ? void 0 : _b.fontFamily) || "方正书宋简体",
1753
+ fontWeight: ((_c = element.style) == null ? void 0 : _c.fontWeight) || "normal",
1754
+ fontStyle: ((_d = element.style) == null ? void 0 : _d.fontStyle) || "normal",
1755
+ textDecoration: ((_e = element.style) == null ? void 0 : _e.textDecoration) || "none",
1756
+ color: ((_f = element.style) == null ? void 0 : _f.color) || "#000000",
1757
+ textAlign: ((_g = element.style) == null ? void 0 : _g.textAlign) || "left",
1758
+ lineHeight: ((_h = element.style) == null ? void 0 : _h.lineHeight) || 1.2
1759
+ };
1760
+ }
1761
+ return baseStyle;
1762
+ };
1763
+ const renderTextWithFields = (text) => {
1764
+ if (!text) return "";
1765
+ const fieldData = props.fieldData || {};
1766
+ let result = text;
1767
+ props.systemFields.forEach((field) => {
1768
+ const placeholder = field.placeholder;
1769
+ const value = fieldData[field.key] || placeholder;
1770
+ const regex = new RegExp(placeholder.replace(/[{}]/g, "\\$&"), "g");
1771
+ result = result.replace(regex, value);
1772
+ });
1773
+ return result;
1774
+ };
1775
+ const getImageSrc = (element) => {
1776
+ if (!element) return null;
1777
+ if (element.imageSource === "field") {
1778
+ const fieldKey = element.imageField;
1779
+ if (fieldKey && props.fieldData && props.fieldData[fieldKey]) {
1780
+ return props.fieldData[fieldKey];
1781
+ }
1782
+ return null;
1783
+ } else {
1784
+ return element.imageUrl || null;
1785
+ }
1786
+ };
1787
+ const getQrcodeContent = (element) => {
1788
+ if (!element) return null;
1789
+ if (element.qrcodeSource === "field") {
1790
+ const fieldKey = element.qrcodeField;
1791
+ if (fieldKey && props.fieldData && props.fieldData[fieldKey]) {
1792
+ return props.fieldData[fieldKey];
1793
+ }
1794
+ return null;
1795
+ } else {
1796
+ return element.qrcodeContent || null;
1797
+ }
1798
+ };
1799
+ const isImageUrl = (url) => {
1800
+ if (!url) return false;
1801
+ if (url.startsWith("data:image/")) return true;
1802
+ if (url.startsWith("http://") || url.startsWith("https://")) {
1803
+ const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".bmp"];
1804
+ const lowerUrl = url.toLowerCase();
1805
+ return imageExtensions.some((ext) => lowerUrl.includes(ext));
1806
+ }
1807
+ return false;
1808
+ };
1809
+ return (_ctx, _cache) => {
1810
+ return __props.visible ? (openBlock(), createElementBlock("div", {
1811
+ key: 0,
1812
+ class: "preview-overlay",
1813
+ onClick: withModifiers(handleClose, ["self"])
1814
+ }, [
1815
+ createElementVNode("div", _hoisted_1$1, [
1816
+ createElementVNode("div", { class: "preview-header" }, [
1817
+ _cache[0] || (_cache[0] = createElementVNode("h3", null, "证书预览", -1)),
1818
+ createElementVNode("button", {
1819
+ class: "close-btn",
1820
+ onClick: handleClose
1821
+ }, "×")
1822
+ ]),
1823
+ createElementVNode("div", _hoisted_2$1, [
1824
+ createElementVNode("div", _hoisted_3$1, [
1825
+ createElementVNode("div", {
1826
+ class: "preview-canvas",
1827
+ style: normalizeStyle(getCanvasStyle())
1828
+ }, [
1829
+ (openBlock(true), createElementBlock(Fragment, null, renderList(__props.elements, (element) => {
1830
+ return openBlock(), createElementBlock("div", {
1831
+ key: element.id,
1832
+ class: normalizeClass(["preview-element", element.type]),
1833
+ style: normalizeStyle(getElementStyle(element))
1834
+ }, [
1835
+ element.type === "text" || element.type === "longtext" ? (openBlock(), createElementBlock("div", _hoisted_4$1, [
1836
+ createElementVNode("span", {
1837
+ innerHTML: renderTextWithFields(element.content)
1838
+ }, null, 8, _hoisted_5)
1839
+ ])) : element.type === "image" ? (openBlock(), createElementBlock("div", _hoisted_6, [
1840
+ getImageSrc(element) ? (openBlock(), createElementBlock("img", {
1841
+ key: 0,
1842
+ src: getImageSrc(element),
1843
+ alt: "图片",
1844
+ class: "preview-element-image"
1845
+ }, null, 8, _hoisted_7)) : createCommentVNode("", true)
1846
+ ])) : element.type === "qrcode" ? (openBlock(), createElementBlock("div", _hoisted_8, [
1847
+ getQrcodeContent(element) && isImageUrl(getQrcodeContent(element)) ? (openBlock(), createElementBlock("img", {
1848
+ key: 0,
1849
+ src: getQrcodeContent(element),
1850
+ alt: "二维码",
1851
+ class: "preview-element-image"
1852
+ }, null, 8, _hoisted_9)) : getQrcodeContent(element) ? (openBlock(), createElementBlock("div", _hoisted_10, toDisplayString(getQrcodeContent(element)), 1)) : createCommentVNode("", true)
1853
+ ])) : createCommentVNode("", true)
1854
+ ], 6);
1855
+ }), 128))
1856
+ ], 4)
1857
+ ])
1858
+ ])
1859
+ ])
1860
+ ])) : createCommentVNode("", true);
1861
+ };
1862
+ }
1863
+ };
1864
+ const CertificatePreview = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-813a5922"]]);
1865
+ const _hoisted_1 = { class: "certificate-editor" };
1866
+ const _hoisted_2 = { class: "toolbar" };
1867
+ const _hoisted_3 = { class: "certificate-editor-content" };
1868
+ const _hoisted_4 = { class: "right-panel" };
1869
+ const _sfc_main = {
1870
+ __name: "CertificateEditor",
1871
+ props: {
1872
+ systemFields: {
1873
+ type: Array,
1874
+ default: () => SYSTEM_FIELDS
1875
+ }
1876
+ },
1877
+ emits: ["save"],
1878
+ setup(__props, { expose: __expose, emit: __emit }) {
1879
+ const emit = __emit;
1880
+ const elements = ref([]);
1881
+ const selectedElement = ref(null);
1882
+ const fieldData = ref({});
1883
+ const showPreview = ref(false);
1884
+ const currentTemplate = ref(PRESET_TEMPLATES.find((t) => t.type === "vertical") || PRESET_TEMPLATES[0]);
1885
+ onMounted(() => {
1886
+ elements.value = [
1887
+ {
1888
+ id: 1,
1889
+ type: "text",
1890
+ x: 115,
1891
+ y: 466,
1892
+ width: 698,
1893
+ height: 29,
1894
+ content: "{{姓名}} ({{性别称呼}})",
1895
+ style: {
1896
+ fontSize: 18,
1897
+ fontFamily: "方正书宋简体",
1898
+ fontWeight: "normal",
1899
+ fontStyle: "normal",
1900
+ textDecoration: "none",
1901
+ color: "#000000",
1902
+ textAlign: "left",
1903
+ lineHeight: 1.2
1904
+ }
1905
+ },
1906
+ {
1907
+ id: 2,
1908
+ type: "longtext",
1909
+ x: 115,
1910
+ y: 520,
1911
+ width: 698,
1912
+ height: 80,
1913
+ content: '您参加了"{{考试名称}}"考试,成绩: {{成绩等级}},经审定,准予颁发考试证书!',
1914
+ style: {
1915
+ fontSize: 18,
1916
+ fontFamily: "方正书宋简体",
1917
+ fontWeight: "normal",
1918
+ fontStyle: "normal",
1919
+ textDecoration: "none",
1920
+ color: "#000000",
1921
+ textAlign: "left",
1922
+ lineHeight: 1.2
1923
+ }
1924
+ },
1925
+ {
1926
+ id: 3,
1927
+ type: "text",
1928
+ x: 115,
1929
+ y: 620,
1930
+ width: 698,
1931
+ height: 29,
1932
+ content: "证书编号: {{证书编号}}",
1933
+ style: {
1934
+ fontSize: 18,
1935
+ fontFamily: "方正书宋简体",
1936
+ fontWeight: "normal",
1937
+ fontStyle: "normal",
1938
+ textDecoration: "none",
1939
+ color: "#000000",
1940
+ textAlign: "left",
1941
+ lineHeight: 1.2
1942
+ }
1943
+ },
1944
+ {
1945
+ id: 4,
1946
+ type: "text",
1947
+ x: 115,
1948
+ y: 660,
1949
+ width: 698,
1950
+ height: 29,
1951
+ content: "有效期: {{有效期}}",
1952
+ style: {
1953
+ fontSize: 18,
1954
+ fontFamily: "方正书宋简体",
1955
+ fontWeight: "normal",
1956
+ fontStyle: "normal",
1957
+ textDecoration: "none",
1958
+ color: "#000000",
1959
+ textAlign: "left",
1960
+ lineHeight: 1.2
1961
+ }
1962
+ }
1963
+ ];
1964
+ });
1965
+ const handleAddElement = (type) => {
1966
+ const newElement = {
1967
+ id: Date.now(),
1968
+ type,
1969
+ x: 100,
1970
+ y: 100,
1971
+ width: type === "text" ? 200 : type === "qrcode" ? 100 : 150,
1972
+ height: type === "text" ? 50 : type === "qrcode" ? 100 : 150,
1973
+ content: type === "text" ? "新文本" : "",
1974
+ style: {
1975
+ fontSize: 18,
1976
+ fontFamily: "方正书宋简体",
1977
+ fontWeight: "normal",
1978
+ fontStyle: "normal",
1979
+ textDecoration: "none",
1980
+ color: "#000000",
1981
+ textAlign: "left",
1982
+ lineHeight: 1.2
1983
+ }
1984
+ };
1985
+ if (type === "image") {
1986
+ newElement.imageSource = "url";
1987
+ newElement.imageUrl = "";
1988
+ newElement.imageField = "";
1989
+ }
1990
+ if (type === "qrcode") {
1991
+ newElement.qrcodeSource = "url";
1992
+ newElement.qrcodeContent = "";
1993
+ newElement.qrcodeField = "";
1994
+ }
1995
+ elements.value.push(newElement);
1996
+ selectedElement.value = newElement;
1997
+ };
1998
+ const handleSelectElement = (element) => {
1999
+ selectedElement.value = element;
2000
+ };
2001
+ const handleUpdateElement = (updatedElement) => {
2002
+ const index = elements.value.findIndex((el) => el.id === updatedElement.id);
2003
+ if (index !== -1) {
2004
+ elements.value[index] = { ...updatedElement };
2005
+ selectedElement.value = elements.value[index];
2006
+ }
2007
+ };
2008
+ const handleDeleteElement = (elementId) => {
2009
+ var _a;
2010
+ elements.value = elements.value.filter((el) => el.id !== elementId);
2011
+ if (((_a = selectedElement.value) == null ? void 0 : _a.id) === elementId) {
2012
+ selectedElement.value = null;
2013
+ }
2014
+ };
2015
+ const handleUpdateFieldData = (newFieldData) => {
2016
+ fieldData.value = { ...newFieldData };
2017
+ };
2018
+ const handleSelectTemplate = (template) => {
2019
+ currentTemplate.value = template;
2020
+ };
2021
+ const handleUploadTemplate = (template) => {
2022
+ currentTemplate.value = template;
2023
+ };
2024
+ const getCertificateData = () => {
2025
+ return {
2026
+ template: currentTemplate.value,
2027
+ elements: elements.value,
2028
+ fieldData: fieldData.value,
2029
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2030
+ };
2031
+ };
2032
+ const handleSave = () => {
2033
+ const certificateConfig = getCertificateData();
2034
+ console.log("=== 证书配置数据 ===");
2035
+ console.log(JSON.stringify(certificateConfig, null, 2));
2036
+ console.log("==================");
2037
+ emit("save", certificateConfig);
2038
+ alert("证书配置已保存,数据已传递给父组件");
2039
+ };
2040
+ __expose({
2041
+ getCertificateData
2042
+ });
2043
+ return (_ctx, _cache) => {
2044
+ return openBlock(), createElementBlock("div", _hoisted_1, [
2045
+ createElementVNode("div", _hoisted_2, [
2046
+ createElementVNode("button", {
2047
+ class: "toolbar-btn preview-btn",
2048
+ onClick: _cache[0] || (_cache[0] = ($event) => showPreview.value = true)
2049
+ }, [..._cache[2] || (_cache[2] = [
2050
+ createElementVNode("span", null, "👁️", -1),
2051
+ createElementVNode("span", null, "预览", -1)
2052
+ ])]),
2053
+ createElementVNode("button", {
2054
+ class: "toolbar-btn save-btn",
2055
+ onClick: handleSave
2056
+ }, [..._cache[3] || (_cache[3] = [
2057
+ createElementVNode("span", null, "💾", -1),
2058
+ createElementVNode("span", null, "保存", -1)
2059
+ ])])
2060
+ ]),
2061
+ createElementVNode("div", _hoisted_3, [
2062
+ createVNode(DesignPanel, {
2063
+ onAddElement: handleAddElement,
2064
+ onSelectTemplate: handleSelectTemplate,
2065
+ onUploadTemplate: handleUploadTemplate,
2066
+ class: "left-panel"
2067
+ }),
2068
+ createVNode(CanvasArea, {
2069
+ elements: elements.value,
2070
+ "selected-element": selectedElement.value,
2071
+ "field-data": fieldData.value,
2072
+ template: currentTemplate.value,
2073
+ "system-fields": __props.systemFields,
2074
+ onSelectElement: handleSelectElement,
2075
+ onUpdateElement: handleUpdateElement,
2076
+ onDeleteElement: handleDeleteElement,
2077
+ class: "canvas-area"
2078
+ }, null, 8, ["elements", "selected-element", "field-data", "template", "system-fields"]),
2079
+ createElementVNode("div", _hoisted_4, [
2080
+ selectedElement.value ? (openBlock(), createBlock(PropertiesPanel, {
2081
+ key: 0,
2082
+ element: selectedElement.value,
2083
+ "system-fields": __props.systemFields,
2084
+ onUpdateElement: handleUpdateElement,
2085
+ class: "properties-section"
2086
+ }, null, 8, ["element", "system-fields"])) : createCommentVNode("", true),
2087
+ createVNode(FieldDataPanel, {
2088
+ "field-data": fieldData.value,
2089
+ onUpdateFieldData: handleUpdateFieldData,
2090
+ class: "field-data-section"
2091
+ }, null, 8, ["field-data"])
2092
+ ])
2093
+ ]),
2094
+ createVNode(CertificatePreview, {
2095
+ visible: showPreview.value,
2096
+ elements: elements.value,
2097
+ template: currentTemplate.value,
2098
+ "field-data": fieldData.value,
2099
+ "system-fields": __props.systemFields,
2100
+ onClose: _cache[1] || (_cache[1] = ($event) => showPreview.value = false)
2101
+ }, null, 8, ["visible", "elements", "template", "field-data", "system-fields"])
2102
+ ]);
2103
+ };
2104
+ }
2105
+ };
2106
+ const CertificateEditor = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-8565cf9a"]]);
2107
+ export {
2108
+ CanvasArea,
2109
+ CertificateEditor,
2110
+ CertificatePreview,
2111
+ DEFAULT_FIELD_DATA,
2112
+ DesignPanel,
2113
+ FieldDataPanel,
2114
+ FieldDialog,
2115
+ PRESET_TEMPLATES,
2116
+ PropertiesPanel,
2117
+ SYSTEM_FIELDS,
2118
+ CertificateEditor as default,
2119
+ extractFields,
2120
+ fileToBase64,
2121
+ getFieldByKey,
2122
+ getPlaceholderByKey,
2123
+ getTemplateSizeSuggestion,
2124
+ hasFields,
2125
+ replaceFields,
2126
+ validateImageFile
2127
+ };
2128
+ //# sourceMappingURL=z-certificate-editor.es.js.map