@shwfed/nuxt 0.9.2 → 0.10.1

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.
@@ -1,14 +1,20 @@
1
1
  <script setup>
2
+ import { useNuxtApp } from "#app";
3
+ import z, {} from "zod";
2
4
  import { useSortable } from "@vueuse/integrations/useSortable";
3
5
  import { Icon } from "@iconify/vue";
4
6
  import { computed, nextTick, ref, toRaw, watch } from "vue";
5
7
  import { useI18n } from "vue-i18n";
6
8
  import {
7
9
  CalendarFieldC,
10
+ EmptyFieldC,
11
+ FieldsConfigC,
12
+ FieldsStyleC,
8
13
  NumberFieldC,
9
14
  SelectFieldC,
15
+ SlotFieldC,
10
16
  StringFieldC,
11
- FieldsStyleC
17
+ TextareaFieldC
12
18
  } from "../fields/schema";
13
19
  import { cn } from "../../../utils/cn";
14
20
  import { Button } from "../button";
@@ -26,7 +32,6 @@ import {
26
32
  DropdownMenuItem,
27
33
  DropdownMenuTrigger
28
34
  } from "../dropdown-menu";
29
- import { IconPicker } from "../icon-picker";
30
35
  import { Input } from "../input";
31
36
  import Locale from "../locale/Locale.vue";
32
37
  import { NativeSelect, NativeSelectOption } from "../native-select";
@@ -39,9 +44,11 @@ const emit = defineEmits(["confirm"]);
39
44
  const open = defineModel("open", { type: Boolean, ...{
40
45
  default: false
41
46
  } });
47
+ const { $toast } = useNuxtApp();
42
48
  const { t } = useI18n();
43
49
  const draftOrientation = ref("horizontal");
44
50
  const draftStyle = ref();
51
+ const search = ref("");
45
52
  const selectedItemId = ref("general");
46
53
  const draftFields = ref([]);
47
54
  const sortableListRef = ref(null);
@@ -49,19 +56,32 @@ const sortableItemIds = ref([]);
49
56
  const validationErrors = ref({});
50
57
  const fieldTypeOptions = computed(() => [
51
58
  { type: "string", label: t("field-type-string") },
59
+ { type: "textarea", label: t("field-type-textarea") },
52
60
  { type: "number", label: t("field-type-number") },
53
61
  { type: "select", label: t("field-type-select") },
54
- { type: "calendar", label: t("field-type-calendar") }
62
+ { type: "calendar", label: t("field-type-calendar") },
63
+ { type: "empty", label: t("field-type-empty") },
64
+ { type: "slot", label: t("field-type-slot") }
55
65
  ]);
56
66
  const generalItem = computed(() => ({
57
67
  id: "general",
58
68
  label: t("general")
59
69
  }));
70
+ const normalizedSearch = computed(() => search.value.trim().toLocaleLowerCase());
60
71
  const selectedField = computed(() => draftFields.value.find((field) => field.draftId === selectedItemId.value));
61
- const selectedFieldValidationRules = computed(() => selectedField.value?.field.validation ?? []);
72
+ const selectedFieldValidationRules = computed(() => {
73
+ const field = selectedField.value?.field;
74
+ if (!field || field.type === "slot" || field.type === "empty") {
75
+ return [];
76
+ }
77
+ return field.validation ?? [];
78
+ });
62
79
  function createDraftId() {
63
80
  return crypto.randomUUID();
64
81
  }
82
+ function createFieldId() {
83
+ return crypto.randomUUID();
84
+ }
65
85
  function createDefaultLocaleValue() {
66
86
  return [{ locale: "zh", message: "" }];
67
87
  }
@@ -83,12 +103,24 @@ function normalizeOrientation(value) {
83
103
  function getFieldTypeLabel(type) {
84
104
  return t(`field-type-${type}`);
85
105
  }
106
+ function isPassiveField(field) {
107
+ return field.type === "slot" || field.type === "empty";
108
+ }
109
+ function getSlotFieldLabel(_) {
110
+ return getFieldTypeLabel("slot");
111
+ }
112
+ function getEmptyFieldLabel(_) {
113
+ return getFieldTypeLabel("empty");
114
+ }
86
115
  function getUnnamedFieldLabel(field) {
87
116
  return t("unnamed-field", {
88
117
  type: getFieldTypeLabel(field.type)
89
118
  });
90
119
  }
91
120
  function getFieldChineseTitle(field) {
121
+ if (isPassiveField(field)) {
122
+ return void 0;
123
+ }
92
124
  const zhTitle = field.title.find((item) => item.locale === "zh");
93
125
  if (!zhTitle) {
94
126
  return void 0;
@@ -97,14 +129,31 @@ function getFieldChineseTitle(field) {
97
129
  return message.length > 0 ? message : void 0;
98
130
  }
99
131
  function getFieldListLabel(field) {
132
+ if (field.type === "slot") {
133
+ return getSlotFieldLabel(field);
134
+ }
135
+ if (field.type === "empty") {
136
+ return getEmptyFieldLabel(field);
137
+ }
100
138
  return getFieldChineseTitle(field) ?? getUnnamedFieldLabel(field);
101
139
  }
102
140
  const fieldItems = computed(() => draftFields.value.map((item) => ({
103
141
  itemId: item.draftId,
142
+ fieldId: item.field.id,
104
143
  label: getFieldListLabel(item.field),
105
- path: item.field.path,
144
+ path: isPassiveField(item.field) ? void 0 : item.field.path,
145
+ searchMeta: isPassiveField(item.field) ? item.field.id : [item.field.path, item.field.id].filter(Boolean).join(" "),
106
146
  type: item.field.type
107
147
  })));
148
+ const filteredFieldItems = computed(() => {
149
+ if (!normalizedSearch.value) {
150
+ return fieldItems.value;
151
+ }
152
+ return fieldItems.value.filter((item) => {
153
+ const haystack = [item.label, item.searchMeta].filter(Boolean).join(" ").toLocaleLowerCase();
154
+ return haystack.includes(normalizedSearch.value);
155
+ });
156
+ });
108
157
  const selectedItemLabel = computed(() => selectedField.value ? getFieldListLabel(selectedField.value.field) : generalItem.value.label);
109
158
  const sortable = useSortable(sortableListRef, sortableItemIds);
110
159
  function getFieldErrorKey(draftId, fieldKey) {
@@ -163,9 +212,11 @@ function normalizeValidationRules(validation) {
163
212
  function normalizeField(field) {
164
213
  switch (field.type) {
165
214
  case "string":
215
+ case "textarea":
166
216
  return {
167
217
  ...field,
168
218
  path: field.path.trim(),
219
+ required: field.required ? true : void 0,
169
220
  icon: normalizeOptionalString(field.icon ?? ""),
170
221
  style: normalizeOptionalString(field.style ?? ""),
171
222
  maxLength: normalizeOptionalString(field.maxLength ?? ""),
@@ -178,6 +229,7 @@ function normalizeField(field) {
178
229
  return {
179
230
  ...field,
180
231
  path: field.path.trim(),
232
+ required: field.required ? true : void 0,
181
233
  icon: normalizeOptionalString(field.icon ?? ""),
182
234
  style: normalizeOptionalString(field.style ?? ""),
183
235
  min: normalizeOptionalString(field.min ?? ""),
@@ -191,6 +243,7 @@ function normalizeField(field) {
191
243
  return {
192
244
  ...field,
193
245
  path: field.path.trim(),
246
+ required: field.required ? true : void 0,
194
247
  icon: normalizeOptionalString(field.icon ?? ""),
195
248
  style: normalizeOptionalString(field.style ?? ""),
196
249
  options: field.options.trim(),
@@ -205,6 +258,7 @@ function normalizeField(field) {
205
258
  return {
206
259
  ...field,
207
260
  path: field.path.trim(),
261
+ required: field.required ? true : void 0,
208
262
  icon: normalizeOptionalString(field.icon ?? ""),
209
263
  style: normalizeOptionalString(field.style ?? ""),
210
264
  display: normalizeOptionalString(field.display ?? ""),
@@ -214,25 +268,40 @@ function normalizeField(field) {
214
268
  disabled: normalizeOptionalString(field.disabled ?? ""),
215
269
  validation: normalizeValidationRules(field.validation)
216
270
  };
271
+ case "slot":
272
+ return {
273
+ ...field,
274
+ style: normalizeOptionalString(field.style ?? "")
275
+ };
276
+ case "empty":
277
+ return {
278
+ ...field,
279
+ style: normalizeOptionalString(field.style ?? "")
280
+ };
217
281
  }
218
282
  }
219
283
  function createField(type) {
220
284
  const title = createDefaultLocaleValue();
285
+ const id = createFieldId();
221
286
  switch (type) {
222
287
  case "string":
288
+ case "textarea":
223
289
  return {
290
+ id,
224
291
  type,
225
292
  path: "",
226
293
  title
227
294
  };
228
295
  case "number":
229
296
  return {
297
+ id,
230
298
  type,
231
299
  path: "",
232
300
  title
233
301
  };
234
302
  case "select":
235
303
  return {
304
+ id,
236
305
  type,
237
306
  path: "",
238
307
  title,
@@ -243,18 +312,33 @@ function createField(type) {
243
312
  };
244
313
  case "calendar":
245
314
  return {
315
+ id,
246
316
  type,
247
317
  path: "",
248
318
  title,
249
319
  mode: "date",
250
320
  value: "yyyy-MM-dd"
251
321
  };
322
+ case "empty":
323
+ return {
324
+ id,
325
+ type
326
+ };
327
+ case "slot":
328
+ return {
329
+ id,
330
+ type
331
+ };
252
332
  }
253
333
  }
254
334
  function resetDraftConfig() {
255
- draftOrientation.value = props.config.orientation ?? "horizontal";
256
- draftStyle.value = normalizeOptionalString(props.config.style ?? "");
257
- draftFields.value = cloneFields(props.config.fields);
335
+ applyDraftConfig(props.config);
336
+ }
337
+ function applyDraftConfig(config) {
338
+ draftOrientation.value = config.orientation ?? "horizontal";
339
+ draftStyle.value = normalizeOptionalString(config.style ?? "");
340
+ search.value = "";
341
+ draftFields.value = cloneFields(config.fields);
258
342
  selectedItemId.value = "general";
259
343
  validationErrors.value = {};
260
344
  }
@@ -289,7 +373,7 @@ function configureSortable() {
289
373
  }
290
374
  async function refreshSortable() {
291
375
  sortable.stop();
292
- if (!open.value || draftFields.value.length === 0) {
376
+ if (!open.value || draftFields.value.length === 0 || normalizedSearch.value) {
293
377
  return;
294
378
  }
295
379
  await nextTick();
@@ -324,6 +408,21 @@ watch(fieldItems, async (items) => {
324
408
  await refreshSortable();
325
409
  }
326
410
  }, { immediate: true });
411
+ watch(filteredFieldItems, (items) => {
412
+ if (!normalizedSearch.value || selectedItemId.value === "general") {
413
+ return;
414
+ }
415
+ if (items.some((item) => item.itemId === selectedItemId.value)) {
416
+ return;
417
+ }
418
+ selectedItemId.value = items[0]?.itemId ?? "general";
419
+ }, { immediate: true });
420
+ watch(normalizedSearch, async () => {
421
+ if (!open.value) {
422
+ return;
423
+ }
424
+ await refreshSortable();
425
+ });
327
426
  function discardChanges() {
328
427
  resetDraftConfig();
329
428
  open.value = false;
@@ -385,26 +484,25 @@ function updateSelectedFieldTitle(value) {
385
484
  title: value
386
485
  }));
387
486
  }
388
- function updateSelectedFieldPath(value) {
487
+ async function copySelectedFieldId() {
389
488
  const selected = selectedField.value;
390
489
  if (!selected) {
391
490
  return;
392
491
  }
393
- clearFieldError(selected.draftId, "path");
394
- updateDraftField(selected.draftId, (field) => ({
395
- ...field,
396
- path: String(value).trim()
397
- }));
492
+ await copyFieldId(selected.field.id);
493
+ }
494
+ async function copyFieldId(fieldId) {
495
+ await writeClipboardText(fieldId, t("copy-field-id-failed"));
398
496
  }
399
- function updateSelectedFieldIcon(value) {
497
+ function updateSelectedFieldPath(value) {
400
498
  const selected = selectedField.value;
401
- if (!selected) {
499
+ if (!selected || isPassiveField(selected.field)) {
402
500
  return;
403
501
  }
404
- clearFieldError(selected.draftId, "icon");
502
+ clearFieldError(selected.draftId, "path");
405
503
  updateDraftField(selected.draftId, (field) => ({
406
504
  ...field,
407
- icon: normalizeOptionalString(String(value ?? ""))
505
+ path: String(value).trim()
408
506
  }));
409
507
  }
410
508
  function updateSelectedFieldStyle(value) {
@@ -440,13 +538,28 @@ function updateSelectedFieldDisabled(value) {
440
538
  disabled: normalizeOptionalString(String(value))
441
539
  }));
442
540
  }
541
+ function updateSelectedFieldRequired(value) {
542
+ const selected = selectedField.value;
543
+ if (!selected || isPassiveField(selected.field)) {
544
+ return;
545
+ }
546
+ updateDraftField(selected.draftId, (field) => {
547
+ if (isPassiveField(field)) {
548
+ return field;
549
+ }
550
+ return {
551
+ ...field,
552
+ required: value ? true : void 0
553
+ };
554
+ });
555
+ }
443
556
  function updateSelectedStringDiscardEmpty(value) {
444
557
  const selected = selectedField.value;
445
- if (!selected || selected.field.type !== "string") {
558
+ if (!selected || selected.field.type !== "string" && selected.field.type !== "textarea") {
446
559
  return;
447
560
  }
448
561
  updateDraftField(selected.draftId, (field) => {
449
- if (field.type !== "string") {
562
+ if (field.type !== "string" && field.type !== "textarea") {
450
563
  return field;
451
564
  }
452
565
  return {
@@ -457,12 +570,12 @@ function updateSelectedStringDiscardEmpty(value) {
457
570
  }
458
571
  function updateSelectedStringMaxLength(value) {
459
572
  const selected = selectedField.value;
460
- if (!selected || selected.field.type !== "string") {
573
+ if (!selected || selected.field.type !== "string" && selected.field.type !== "textarea") {
461
574
  return;
462
575
  }
463
576
  clearFieldError(selected.draftId, "maxLength");
464
577
  updateDraftField(selected.draftId, (field) => {
465
- if (field.type !== "string") {
578
+ if (field.type !== "string" && field.type !== "textarea") {
466
579
  return field;
467
580
  }
468
581
  return {
@@ -653,23 +766,28 @@ function updateSelectedCalendarDisableDate(value) {
653
766
  }
654
767
  function addValidationRule() {
655
768
  const selected = selectedField.value;
656
- if (!selected) {
769
+ if (!selected || isPassiveField(selected.field)) {
657
770
  return;
658
771
  }
659
- updateDraftField(selected.draftId, (field) => ({
660
- ...field,
661
- validation: [
662
- ...field.validation ?? [],
663
- {
664
- expression: "",
665
- message: ""
666
- }
667
- ]
668
- }));
772
+ updateDraftField(selected.draftId, (field) => {
773
+ if (isPassiveField(field)) {
774
+ return field;
775
+ }
776
+ return {
777
+ ...field,
778
+ validation: [
779
+ ...field.validation ?? [],
780
+ {
781
+ expression: "",
782
+ message: ""
783
+ }
784
+ ]
785
+ };
786
+ });
669
787
  }
670
788
  function updateValidationRule(index, updater) {
671
789
  const selected = selectedField.value;
672
- if (!selected) {
790
+ if (!selected || isPassiveField(selected.field)) {
673
791
  return;
674
792
  }
675
793
  const validation = selected.field.validation ?? [];
@@ -678,10 +796,15 @@ function updateValidationRule(index, updater) {
678
796
  return;
679
797
  }
680
798
  const nextValidation = validation.map((rule, ruleIndex) => ruleIndex === index ? updater(rule) : rule);
681
- updateDraftField(selected.draftId, (field) => ({
682
- ...field,
683
- validation: nextValidation
684
- }));
799
+ updateDraftField(selected.draftId, (field) => {
800
+ if (isPassiveField(field)) {
801
+ return field;
802
+ }
803
+ return {
804
+ ...field,
805
+ validation: nextValidation
806
+ };
807
+ });
685
808
  }
686
809
  function updateSelectedValidationExpression(index, value) {
687
810
  const selected = selectedField.value;
@@ -707,7 +830,7 @@ function updateSelectedValidationMessage(index, value) {
707
830
  }
708
831
  function moveValidationRule(index, offset) {
709
832
  const selected = selectedField.value;
710
- if (!selected) {
833
+ if (!selected || isPassiveField(selected.field)) {
711
834
  return;
712
835
  }
713
836
  const validation = selected.field.validation ?? [];
@@ -723,15 +846,20 @@ function moveValidationRule(index, offset) {
723
846
  }
724
847
  nextValidation[index] = targetRule;
725
848
  nextValidation[nextIndex] = currentRule;
726
- updateDraftField(selected.draftId, (field) => ({
727
- ...field,
728
- validation: nextValidation
729
- }));
849
+ updateDraftField(selected.draftId, (field) => {
850
+ if (isPassiveField(field)) {
851
+ return field;
852
+ }
853
+ return {
854
+ ...field,
855
+ validation: nextValidation
856
+ };
857
+ });
730
858
  clearValidationRuleErrors(selected.draftId);
731
859
  }
732
860
  function deleteValidationRule(index) {
733
861
  const selected = selectedField.value;
734
- if (!selected) {
862
+ if (!selected || isPassiveField(selected.field)) {
735
863
  return;
736
864
  }
737
865
  const validation = selected.field.validation ?? [];
@@ -741,7 +869,14 @@ function deleteValidationRule(index) {
741
869
  clearValidationRuleError(selected.draftId, index, "expression");
742
870
  clearValidationRuleError(selected.draftId, index, "message");
743
871
  updateDraftField(selected.draftId, (field) => {
744
- const nextValidation = (field.validation ?? []).filter((_, ruleIndex) => ruleIndex !== index);
872
+ if (isPassiveField(field)) {
873
+ return field;
874
+ }
875
+ const validation2 = field.validation ?? [];
876
+ const nextValidation = [
877
+ ...validation2.slice(0, index),
878
+ ...validation2.slice(index + 1)
879
+ ];
745
880
  return {
746
881
  ...field,
747
882
  validation: nextValidation.length > 0 ? nextValidation : void 0
@@ -755,6 +890,10 @@ function getSchemaIssues(field) {
755
890
  const result = StringFieldC.safeParse(field);
756
891
  return result.success ? [] : result.error.issues;
757
892
  }
893
+ case "textarea": {
894
+ const result = TextareaFieldC.safeParse(field);
895
+ return result.success ? [] : result.error.issues;
896
+ }
758
897
  case "number": {
759
898
  const result = NumberFieldC.safeParse(field);
760
899
  return result.success ? [] : result.error.issues;
@@ -767,6 +906,14 @@ function getSchemaIssues(field) {
767
906
  const result = CalendarFieldC.safeParse(field);
768
907
  return result.success ? [] : result.error.issues;
769
908
  }
909
+ case "slot": {
910
+ const result = SlotFieldC.safeParse(field);
911
+ return result.success ? [] : result.error.issues;
912
+ }
913
+ case "empty": {
914
+ const result = EmptyFieldC.safeParse(field);
915
+ return result.success ? [] : result.error.issues;
916
+ }
770
917
  }
771
918
  }
772
919
  function normalizeIssuePath(path) {
@@ -794,19 +941,30 @@ function validateDraftFields() {
794
941
  draftId: item.draftId,
795
942
  field: normalizeField(item.field)
796
943
  }));
944
+ const idOwners = {};
797
945
  const pathOwners = {};
798
946
  let firstInvalidItemId;
799
947
  for (const item of normalizedFields) {
800
- const existingOwner = pathOwners[item.field.path];
801
- if (item.field.path.length === 0) {
802
- setError(errors, getFieldErrorKey(item.draftId, "path"), t("field-path-required"));
803
- firstInvalidItemId = firstInvalidItemId ?? item.draftId;
804
- } else if (existingOwner !== void 0) {
805
- setError(errors, getFieldErrorKey(item.draftId, "path"), t("field-path-duplicate"));
806
- setError(errors, getFieldErrorKey(existingOwner, "path"), t("field-path-duplicate"));
948
+ const existingIdOwner = idOwners[item.field.id];
949
+ if (existingIdOwner !== void 0) {
950
+ setError(errors, getFieldErrorKey(item.draftId, "id"), t("field-id-duplicate"));
951
+ setError(errors, getFieldErrorKey(existingIdOwner, "id"), t("field-id-duplicate"));
807
952
  firstInvalidItemId = firstInvalidItemId ?? item.draftId;
808
953
  } else {
809
- pathOwners[item.field.path] = item.draftId;
954
+ idOwners[item.field.id] = item.draftId;
955
+ }
956
+ if (!isPassiveField(item.field)) {
957
+ const existingPathOwner = pathOwners[item.field.path];
958
+ if (item.field.path.length === 0) {
959
+ setError(errors, getFieldErrorKey(item.draftId, "path"), t("field-path-required"));
960
+ firstInvalidItemId = firstInvalidItemId ?? item.draftId;
961
+ } else if (existingPathOwner !== void 0) {
962
+ setError(errors, getFieldErrorKey(item.draftId, "path"), t("field-path-duplicate"));
963
+ setError(errors, getFieldErrorKey(existingPathOwner, "path"), t("field-path-duplicate"));
964
+ firstInvalidItemId = firstInvalidItemId ?? item.draftId;
965
+ } else {
966
+ pathOwners[item.field.path] = item.draftId;
967
+ }
810
968
  }
811
969
  if (item.field.type === "calendar" && item.field.value.length === 0) {
812
970
  setError(errors, getFieldErrorKey(item.draftId, "value"), t("calendar-value-required"));
@@ -828,10 +986,10 @@ function validateDraftFields() {
828
986
  }
829
987
  return normalizedFields;
830
988
  }
831
- function confirmChanges() {
989
+ function buildDraftConfig() {
832
990
  const normalizedFields = validateDraftFields();
833
991
  if (!normalizedFields) {
834
- return;
992
+ return void 0;
835
993
  }
836
994
  const generalStyleResult = FieldsStyleC.safeParse(draftStyle.value);
837
995
  if (!generalStyleResult.success) {
@@ -840,9 +998,8 @@ function confirmChanges() {
840
998
  [getGeneralErrorKey("style")]: generalStyleResult.error.issues[0]?.message ?? t("general-style-invalid")
841
999
  };
842
1000
  selectedItemId.value = "general";
843
- return;
1001
+ return void 0;
844
1002
  }
845
- draftFields.value = normalizedFields.map((item) => createDraftField(item.field));
846
1003
  const nextConfig = {
847
1004
  fields: normalizedFields.map((item) => item.field)
848
1005
  };
@@ -852,7 +1009,235 @@ function confirmChanges() {
852
1009
  if (generalStyleResult.data) {
853
1010
  nextConfig.style = generalStyleResult.data;
854
1011
  }
855
- emit("confirm", nextConfig);
1012
+ return {
1013
+ config: nextConfig,
1014
+ normalizedFields
1015
+ };
1016
+ }
1017
+ function showImportError(message) {
1018
+ $toast.error(message);
1019
+ }
1020
+ function showCopyError(message) {
1021
+ $toast.error(message);
1022
+ }
1023
+ function showImportErrorWithCopyAction(message, onClick) {
1024
+ $toast.error(message, {
1025
+ action: {
1026
+ label: t("copy-paste-error"),
1027
+ onClick
1028
+ }
1029
+ });
1030
+ }
1031
+ function getValidDraftConfig(errorMessage) {
1032
+ const result = buildDraftConfig();
1033
+ if (!result) {
1034
+ showCopyError(errorMessage);
1035
+ return void 0;
1036
+ }
1037
+ return result.config;
1038
+ }
1039
+ function buildFieldsConfigJsonSchema() {
1040
+ return z.toJSONSchema(FieldsConfigC, {
1041
+ io: "input",
1042
+ unrepresentable: "any"
1043
+ });
1044
+ }
1045
+ function buildAiPromptHeaderMarkdown() {
1046
+ return [
1047
+ "# \u5B57\u6BB5\u914D\u7F6E AI \u4E0A\u4E0B\u6587",
1048
+ "\u4F60\u662F\u4E00\u4E2A\u5E2E\u52A9\u7528\u6237\u914D\u7F6E\u5B57\u6BB5\u7EC4\u4EF6\u7684 AI \u52A9\u624B\uFF0C\u8D1F\u8D23\u89E3\u91CA\u3001\u4FEE\u6539\u5E76\u751F\u6210\u53EF\u7C98\u8D34\u7684\u5B57\u6BB5\u914D\u7F6E\u3002",
1049
+ "\u5982\u679C\u4F60\u4E0D\u786E\u5B9A\u67D0\u4E2A\u5B57\u6BB5\u3001\u8868\u8FBE\u5F0F\u53D8\u91CF\u3001\u8FD0\u884C\u65F6\u4E0A\u4E0B\u6587\u6216\u4E1A\u52A1\u542B\u4E49\uFF0C\u5FC5\u987B\u660E\u786E\u8BF4\u660E\u4E0D\u786E\u5B9A\u70B9\uFF0C\u4E0D\u8981\u731C\u6D4B\u3002",
1050
+ "\u53EA\u6709\u5F53\u7528\u6237\u660E\u786E\u8981\u6C42\u751F\u6210\u914D\u7F6E\u65F6\uFF0C\u624D\u8FD4\u56DE\u5B8C\u6574\u914D\u7F6E\u6216\u914D\u7F6E\u7247\u6BB5\uFF1B\u5982\u679C\u8FD4\u56DE\u914D\u7F6E\uFF0C\u5FC5\u987B\u653E\u5728 Markdown code block \u4E2D\u3002"
1051
+ ].join("\n");
1052
+ }
1053
+ function buildDslGuideMarkdown() {
1054
+ return [
1055
+ "## DSL / CEL \u7F16\u5199\u8BF4\u660E",
1056
+ "\u672C\u914D\u7F6E\u4E2D\u7684\u8868\u8FBE\u5F0F\u5B57\u6BB5\u5FC5\u987B\u586B\u5199 CEL \u5B57\u7B26\u4E32\uFF0C\u4E0D\u8981\u751F\u6210 JavaScript\u3001TypeScript\u3001\u7BAD\u5934\u51FD\u6570\u6216\u4F2A\u4EE3\u7801\u3002",
1057
+ "",
1058
+ "### 1. \u57FA\u7840\u8BED\u6CD5",
1059
+ '- \u5B57\u9762\u91CF\uFF1A`"text"`\u3001`123`\u3001`true`\u3001`false`\u3001`null`\u3002',
1060
+ '- \u5217\u8868 / \u5BF9\u8C61\uFF1A`[1, 2]`\u3001`{"display": "grid"}`\u3002',
1061
+ "- \u8BBF\u95EE\uFF1A`.`\u3001`[]`\u3001`.?`\u3001`[?]`\uFF0C\u4F8B\u5982 `form.name`\u3001`ctx.?user.orValue(null)`\u3002",
1062
+ "- \u6761\u4EF6\u4E0E\u903B\u8F91\uFF1A`&&`\u3001`||`\u3001`!`\u3001`condition ? a : b`\u3002",
1063
+ '- \u65B9\u6CD5\u8C03\u7528\uFF1A`value.method(args)`\uFF0C\u4F8B\u5982 `now.format("yyyy-MM-dd")`\u3002',
1064
+ "",
1065
+ "### 2. \u5E38\u89C1\u5B57\u6BB5\u4E0E\u7528\u9014",
1066
+ '- `style` \u76F8\u5173\u5B57\u6BB5\u9700\u8981\u8FD4\u56DE style map\uFF0C\u4F8B\u5982 `{"display": "grid"}`\u3002',
1067
+ "- `hidden` / `disabled` / `disableDate` \u9700\u8981\u8FD4\u56DE `bool`\u3002",
1068
+ "- `validation[].expression` \u9700\u8981\u8FD4\u56DE `bool`\uFF1B\u8FD4\u56DE `false` \u65F6\u5C55\u793A\u5BF9\u5E94\u6D88\u606F\u3002",
1069
+ "- `select.options` \u901A\u5E38\u8FD4\u56DE\u5217\u8868\uFF1B`label` / `value` / `key` \u7528\u6765\u63CF\u8FF0\u5355\u4E2A\u9009\u9879\u5982\u4F55\u6620\u5C04\u3002",
1070
+ "",
1071
+ "### 3. \u5B57\u6BB5\u7C7B\u578B\u7EA6\u675F",
1072
+ "- \u6240\u6709\u5B57\u6BB5\u90FD\u5FC5\u987B\u5305\u542B\u7A33\u5B9A\u7684 UUID `id`\u3002",
1073
+ "- \u53EA\u6709\u975E `slot` / `empty` \u5B57\u6BB5\u53EF\u4EE5\u914D\u7F6E `path`\uFF0C\u5E76\u53C2\u4E0E\u8868\u5355\u503C\u8BFB\u5199\u3002",
1074
+ "- `slot` \u548C `empty` \u5B57\u6BB5\u90FD\u4E0D\u4F1A\u7ED1\u5B9A\u6A21\u578B\u503C\uFF0C\u53EA\u5141\u8BB8 `id`\u3001`type` \u548C\u53EF\u9009\u7684 `style`\u3002"
1075
+ ].join("\n");
1076
+ }
1077
+ function buildMarkdownNotes() {
1078
+ return [
1079
+ "## \u6CE8\u610F\u4E8B\u9879",
1080
+ "- \u6240\u6709\u5B57\u6BB5\u90FD\u5FC5\u987B\u4FDD\u7559\u73B0\u6709 `id`\uFF0C\u4E0D\u8981\u751F\u6210\u65B0\u7684 `id` \u66FF\u6362\u5DF2\u6709\u5B57\u6BB5\u3002",
1081
+ "- `slot` \u4E0E `empty` \u5B57\u6BB5\u53EA\u80FD\u4F7F\u7528 `id`\u3001`type` \u548C\u53EF\u9009\u7684 `style`\u3002",
1082
+ "- \u975E `slot` / `empty` \u5B57\u6BB5\u7684 `path` \u5FC5\u987B\u552F\u4E00\u4E14\u4E0D\u80FD\u4E3A\u7A7A\u3002",
1083
+ "- \u8868\u8FBE\u5F0F\u5B57\u6BB5\u5FC5\u987B\u4E25\u683C\u9075\u5B88 schema \u7EA6\u675F\uFF1B\u5982\u679C schema \u4E0D\u652F\u6301\uFF0C\u5C31\u76F4\u63A5\u8BF4\u660E\u9650\u5236\u3002"
1084
+ ].join("\n");
1085
+ }
1086
+ function buildMarkdownCopyContent(config) {
1087
+ return [
1088
+ buildAiPromptHeaderMarkdown(),
1089
+ "",
1090
+ "## \u5F53\u524D\u914D\u7F6E",
1091
+ "```json",
1092
+ JSON.stringify(config, null, 2),
1093
+ "```",
1094
+ "",
1095
+ buildDslGuideMarkdown(),
1096
+ "",
1097
+ buildMarkdownNotes(),
1098
+ "",
1099
+ "## FieldsConfig JSON Schema",
1100
+ "```json",
1101
+ JSON.stringify(buildFieldsConfigJsonSchema(), null, 2),
1102
+ "```"
1103
+ ].join("\n");
1104
+ }
1105
+ function formatIssuePath(path) {
1106
+ if (path.length === 0) {
1107
+ return "(root)";
1108
+ }
1109
+ return path.map((segment) => {
1110
+ if (typeof segment === "number") {
1111
+ return `${segment}`;
1112
+ }
1113
+ if (typeof segment === "string") {
1114
+ return segment;
1115
+ }
1116
+ return String(segment);
1117
+ }).join(".");
1118
+ }
1119
+ function formatIssueExtraFields(issue) {
1120
+ const lines = [];
1121
+ for (const [key, value] of Object.entries(issue)) {
1122
+ if (key === "code" || key === "message" || key === "path") {
1123
+ continue;
1124
+ }
1125
+ lines.push(` - ${key}: ${JSON.stringify(value)}`);
1126
+ }
1127
+ return lines;
1128
+ }
1129
+ function buildPasteConfigErrorDetails(source, error) {
1130
+ if (error instanceof SyntaxError) {
1131
+ return [
1132
+ "## \u7C98\u8D34\u5931\u8D25\u539F\u56E0",
1133
+ "- \u7C7B\u578B\uFF1AJSON \u89E3\u6790\u5931\u8D25",
1134
+ `- message: ${error.message}`,
1135
+ "",
1136
+ "## \u539F\u59CB\u7C98\u8D34\u5185\u5BB9",
1137
+ "```text",
1138
+ source,
1139
+ "```"
1140
+ ].join("\n");
1141
+ }
1142
+ const issueLines = error.issues.flatMap((issue, index) => [
1143
+ `### Issue ${index + 1}`,
1144
+ `- path: ${formatIssuePath(issue.path)}`,
1145
+ `- code: ${issue.code}`,
1146
+ `- message: ${issue.message}`,
1147
+ ...formatIssueExtraFields(issue)
1148
+ ]);
1149
+ return [
1150
+ "## \u7C98\u8D34\u5931\u8D25\u539F\u56E0",
1151
+ "- \u7C7B\u578B\uFF1ASchema \u6821\u9A8C\u5931\u8D25",
1152
+ "",
1153
+ "## \u539F\u59CB\u7C98\u8D34\u5185\u5BB9",
1154
+ "```json",
1155
+ source,
1156
+ "```",
1157
+ "",
1158
+ "## Schema \u62A5\u9519",
1159
+ ...issueLines
1160
+ ].join("\n");
1161
+ }
1162
+ function buildPasteConfigErrorMarkdown(source, error) {
1163
+ return [
1164
+ buildAiPromptHeaderMarkdown(),
1165
+ "",
1166
+ "## \u5F53\u524D\u4EFB\u52A1",
1167
+ "\u7528\u6237\u628A\u4E00\u6BB5\u914D\u7F6E\u7C98\u8D34\u56DE\u5B57\u6BB5\u914D\u7F6E\u5668\u65F6\u5931\u8D25\u4E86\u3002\u8BF7\u57FA\u4E8E\u4E0B\u9762\u7684\u539F\u59CB\u5185\u5BB9\u548C\u62A5\u9519\u4FEE\u590D\u5F53\u524D\u914D\u7F6E\u3002",
1168
+ "\u8BF7\u4F18\u5148\u4FEE\u590D\u6700\u5C0F\u5FC5\u8981\u8303\u56F4\uFF0C\u4E0D\u8981\u53D1\u660E schema \u4E2D\u4E0D\u5B58\u5728\u7684\u5B57\u6BB5\uFF0C\u4E5F\u4E0D\u8981\u66FF\u73B0\u6709\u5B57\u6BB5\u751F\u6210\u65B0\u7684 ID\u3002",
1169
+ "\u53EA\u6709\u5F53\u7528\u6237\u660E\u786E\u8981\u6C42\u8F93\u51FA\u914D\u7F6E\u65F6\uFF0C\u624D\u8FD4\u56DE\u5B8C\u6574\u914D\u7F6E\uFF1B\u5982\u679C\u8FD4\u56DE\u914D\u7F6E\uFF0C\u5FC5\u987B\u653E\u5728 Markdown code block \u4E2D\u3002",
1170
+ "",
1171
+ buildPasteConfigErrorDetails(source, error),
1172
+ "",
1173
+ buildMarkdownNotes()
1174
+ ].join("\n");
1175
+ }
1176
+ async function writeClipboardText(value, errorMessage) {
1177
+ try {
1178
+ await navigator.clipboard.writeText(value);
1179
+ } catch {
1180
+ showCopyError(errorMessage);
1181
+ }
1182
+ }
1183
+ async function copyPasteConfigError(source, error) {
1184
+ await writeClipboardText(buildPasteConfigErrorMarkdown(source, error), t("copy-paste-error-failed"));
1185
+ }
1186
+ async function pasteConfigFromClipboard() {
1187
+ let source = "";
1188
+ try {
1189
+ source = await navigator.clipboard.readText();
1190
+ } catch {
1191
+ showImportError(t("paste-config-read-failed"));
1192
+ return;
1193
+ }
1194
+ let parsedValue;
1195
+ try {
1196
+ parsedValue = JSON.parse(source);
1197
+ } catch (error) {
1198
+ if (error instanceof SyntaxError) {
1199
+ showImportErrorWithCopyAction(
1200
+ t("paste-config-invalid-json"),
1201
+ async () => copyPasteConfigError(source, error)
1202
+ );
1203
+ return;
1204
+ }
1205
+ showImportError(t("paste-config-invalid-json"));
1206
+ return;
1207
+ }
1208
+ const result = FieldsConfigC.safeParse(parsedValue);
1209
+ if (!result.success) {
1210
+ showImportErrorWithCopyAction(
1211
+ t("paste-config-invalid-schema"),
1212
+ async () => copyPasteConfigError(source, result.error)
1213
+ );
1214
+ return;
1215
+ }
1216
+ applyDraftConfig(result.data);
1217
+ await nextTick();
1218
+ await refreshSortable();
1219
+ }
1220
+ async function copyConfig() {
1221
+ const config = getValidDraftConfig(t("copy-config-failed"));
1222
+ if (!config) {
1223
+ return;
1224
+ }
1225
+ await writeClipboardText(JSON.stringify(config, null, 2), t("copy-config-failed"));
1226
+ }
1227
+ async function copyMarkdown() {
1228
+ const config = getValidDraftConfig(t("copy-markdown-failed"));
1229
+ if (!config) {
1230
+ return;
1231
+ }
1232
+ await writeClipboardText(buildMarkdownCopyContent(config), t("copy-markdown-failed"));
1233
+ }
1234
+ function confirmChanges() {
1235
+ const result = buildDraftConfig();
1236
+ if (!result) {
1237
+ return;
1238
+ }
1239
+ draftFields.value = result.normalizedFields.map((item) => createDraftField(item.field));
1240
+ emit("confirm", result.config);
856
1241
  open.value = false;
857
1242
  }
858
1243
  </script>
@@ -867,9 +1252,22 @@ function confirmChanges() {
867
1252
  :show-close-button="true"
868
1253
  >
869
1254
  <DialogHeader class="gap-1 border-b border-zinc-200 px-6 py-5">
870
- <DialogTitle class="text-xl font-semibold text-zinc-800">
871
- {{ t("configure-fields") }}
872
- </DialogTitle>
1255
+ <div class="flex items-center gap-3">
1256
+ <DialogTitle class="text-xl font-semibold text-zinc-800">
1257
+ {{ t("configure-fields") }}
1258
+ </DialogTitle>
1259
+ <Button
1260
+ type="button"
1261
+ variant="ghost"
1262
+ size="sm"
1263
+ data-slot="fields-configurator-paste"
1264
+ class="shrink-0"
1265
+ @click="pasteConfigFromClipboard"
1266
+ >
1267
+ <Icon icon="fluent:clipboard-paste-20-regular" />
1268
+ {{ t("paste-config") }}
1269
+ </Button>
1270
+ </div>
873
1271
  <DialogDescription class="text-sm text-zinc-500">
874
1272
  {{ t("configure-fields-description") }}
875
1273
  </DialogDescription>
@@ -877,7 +1275,42 @@ function confirmChanges() {
877
1275
 
878
1276
  <div class="grid min-h-0 flex-1 grid-cols-[19rem_minmax(0,1fr)]">
879
1277
  <section class="flex min-h-0 flex-col border-r border-zinc-200 px-4 py-4">
880
- <div class="flex min-h-0 flex-1 flex-col overflow-hidden">
1278
+ <Input
1279
+ v-model="search"
1280
+ data-slot="fields-configurator-search"
1281
+ :placeholder="t('search-fields')"
1282
+ />
1283
+
1284
+ <DropdownMenu>
1285
+ <DropdownMenuTrigger as-child>
1286
+ <Button
1287
+ type="button"
1288
+ data-slot="fields-configurator-add"
1289
+ class="mt-3 w-full justify-center"
1290
+ >
1291
+ <Icon icon="fluent:add-20-regular" />
1292
+ {{ t("add-field") }}
1293
+ </Button>
1294
+ </DropdownMenuTrigger>
1295
+
1296
+ <DropdownMenuContent
1297
+ align="start"
1298
+ :style="{
1299
+ width: 'var(--reka-dropdown-menu-trigger-width)'
1300
+ }"
1301
+ >
1302
+ <DropdownMenuItem
1303
+ v-for="option in fieldTypeOptions"
1304
+ :key="option.type"
1305
+ :data-slot="`fields-configurator-add-item-${option.type}`"
1306
+ @select="addField(option.type)"
1307
+ >
1308
+ {{ option.label }}
1309
+ </DropdownMenuItem>
1310
+ </DropdownMenuContent>
1311
+ </DropdownMenu>
1312
+
1313
+ <div class="mt-4 flex min-h-0 flex-1 flex-col overflow-hidden">
881
1314
  <div class="flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto pr-1">
882
1315
  <button
883
1316
  type="button"
@@ -899,16 +1332,17 @@ function confirmChanges() {
899
1332
  </button>
900
1333
 
901
1334
  <div
902
- v-if="fieldItems.length > 0"
1335
+ v-if="filteredFieldItems.length > 0"
903
1336
  ref="sortableListRef"
904
1337
  data-slot="fields-configurator-list"
905
1338
  class="flex flex-col gap-1"
906
1339
  >
907
1340
  <div
908
- v-for="item in fieldItems"
1341
+ v-for="item in filteredFieldItems"
909
1342
  :key="item.itemId"
910
1343
  data-slot="fields-configurator-field-item"
911
1344
  :data-item-id="item.itemId"
1345
+ :data-field-id="item.fieldId"
912
1346
  :data-field-path="item.path"
913
1347
  :data-selected="selectedItemId === item.itemId ? 'true' : 'false'"
914
1348
  :class="cn(
@@ -952,6 +1386,14 @@ function confirmChanges() {
952
1386
  </div>
953
1387
  </div>
954
1388
 
1389
+ <p
1390
+ v-else-if="normalizedSearch"
1391
+ data-slot="fields-configurator-empty"
1392
+ class="px-1 pt-2 text-xs text-zinc-400"
1393
+ >
1394
+ {{ t("no-matches") }}
1395
+ </p>
1396
+
955
1397
  <p
956
1398
  v-else
957
1399
  data-slot="fields-configurator-empty"
@@ -961,46 +1403,30 @@ function confirmChanges() {
961
1403
  </p>
962
1404
  </div>
963
1405
  </div>
964
-
965
- <div
966
- data-slot="fields-configurator-add-container"
967
- class="mt-4"
968
- >
969
- <DropdownMenu>
970
- <DropdownMenuTrigger as-child>
971
- <Button
972
- type="button"
973
- data-slot="fields-configurator-add"
974
- size="sm"
975
- variant="default"
976
- class="w-full justify-center"
977
- >
978
- <Icon icon="fluent:add-20-regular" />
979
- {{ t("add-field") }}
980
- </Button>
981
- </DropdownMenuTrigger>
982
-
983
- <DropdownMenuContent align="start">
984
- <DropdownMenuItem
985
- v-for="option in fieldTypeOptions"
986
- :key="option.type"
987
- :data-slot="`fields-configurator-add-item-${option.type}`"
988
- @select="addField(option.type)"
989
- >
990
- {{ option.label }}
991
- </DropdownMenuItem>
992
- </DropdownMenuContent>
993
- </DropdownMenu>
994
- </div>
995
1406
  </section>
996
1407
 
997
1408
  <section class="flex min-h-0 flex-col overflow-y-auto px-6 py-6">
998
- <h3
999
- data-slot="fields-configurator-detail-title"
1000
- class="text-lg font-semibold text-zinc-800"
1001
- >
1002
- {{ selectedItemLabel }}
1003
- </h3>
1409
+ <div class="flex items-center gap-2">
1410
+ <h3
1411
+ data-slot="fields-configurator-detail-title"
1412
+ class="text-lg font-semibold text-zinc-800"
1413
+ >
1414
+ {{ selectedItemLabel }}
1415
+ </h3>
1416
+ <Button
1417
+ v-if="selectedField"
1418
+ type="button"
1419
+ variant="ghost"
1420
+ size="sm"
1421
+ data-slot="fields-configurator-copy-id"
1422
+ class="size-7 p-0 text-zinc-400 hover:text-zinc-700"
1423
+ :aria-label="t('copy-field-id', { field: selectedItemLabel })"
1424
+ :title="selectedField.field.id"
1425
+ @click="copySelectedFieldId"
1426
+ >
1427
+ <Icon icon="fluent:copy-20-regular" />
1428
+ </Button>
1429
+ </div>
1004
1430
 
1005
1431
  <p
1006
1432
  v-if="selectedItemId === 'general'"
@@ -1069,24 +1495,20 @@ function confirmChanges() {
1069
1495
  data-slot="fields-configurator-field-main"
1070
1496
  class="mt-6 flex flex-col gap-6"
1071
1497
  >
1072
- <section class="grid gap-4 md:grid-cols-2">
1073
- <label
1074
- data-slot="fields-configurator-field-type-section"
1075
- class="flex flex-col gap-2"
1076
- >
1077
- <span class="text-xs font-medium text-zinc-500">
1078
- {{ t("field-type") }}
1079
- </span>
1080
- <div
1081
- data-slot="fields-configurator-field-type"
1082
- class="flex h-9 items-center rounded-md border border-zinc-200 bg-zinc-50 px-3 text-sm text-zinc-600"
1083
- >
1084
- {{ getFieldTypeLabel(selectedField.field.type) }}
1085
- </div>
1086
- </label>
1498
+ <p
1499
+ v-if="validationErrors[getFieldErrorKey(selectedField.draftId, 'id')]"
1500
+ data-slot="fields-configurator-field-id-error"
1501
+ class="-mt-4 text-xs text-red-500"
1502
+ >
1503
+ {{ validationErrors[getFieldErrorKey(selectedField.draftId, "id")] }}
1504
+ </p>
1087
1505
 
1506
+ <section
1507
+ v-if="!isPassiveField(selectedField.field)"
1508
+ data-slot="fields-configurator-field-path-section"
1509
+ class="flex flex-col gap-2"
1510
+ >
1088
1511
  <label
1089
- data-slot="fields-configurator-field-path-section"
1090
1512
  class="flex flex-col gap-2"
1091
1513
  >
1092
1514
  <span class="text-xs font-medium text-zinc-500">
@@ -1111,6 +1533,7 @@ function confirmChanges() {
1111
1533
  </section>
1112
1534
 
1113
1535
  <section
1536
+ v-if="!isPassiveField(selectedField.field)"
1114
1537
  data-slot="fields-configurator-field-label-section"
1115
1538
  class="flex flex-col gap-2"
1116
1539
  >
@@ -1134,27 +1557,47 @@ function confirmChanges() {
1134
1557
  </section>
1135
1558
 
1136
1559
  <section
1137
- data-slot="fields-configurator-field-general-options"
1560
+ v-if="isPassiveField(selectedField.field)"
1561
+ data-slot="fields-configurator-passive-options"
1138
1562
  class="grid gap-4 md:grid-cols-2"
1139
1563
  >
1140
- <label class="flex flex-col gap-2">
1564
+ <label class="flex flex-col gap-2 md:col-span-2">
1141
1565
  <span class="text-xs font-medium text-zinc-500">
1142
- {{ t("field-icon") }}
1566
+ {{ t("field-style") }}
1143
1567
  </span>
1144
- <IconPicker
1145
- data-slot="fields-configurator-field-icon-picker"
1146
- :model-value="selectedField.field.icon ?? ''"
1147
- :invalid="validationErrors[getFieldErrorKey(selectedField.draftId, 'icon')] !== void 0"
1148
- :placeholder="t('field-icon-placeholder')"
1149
- @update:model-value="updateSelectedFieldIcon"
1568
+ <Textarea
1569
+ data-slot="fields-configurator-field-style-input"
1570
+ :model-value="selectedField.field.style ?? ''"
1571
+ :aria-invalid="validationErrors[getFieldErrorKey(selectedField.draftId, 'style')] ? 'true' : void 0"
1572
+ :placeholder="t('field-style-placeholder')"
1573
+ class="min-h-20 font-mono text-sm"
1574
+ @update:model-value="updateSelectedFieldStyle"
1150
1575
  />
1151
1576
  <p
1152
- v-if="validationErrors[getFieldErrorKey(selectedField.draftId, 'icon')]"
1577
+ v-if="validationErrors[getFieldErrorKey(selectedField.draftId, 'style')]"
1153
1578
  class="text-xs text-red-500"
1154
1579
  >
1155
- {{ validationErrors[getFieldErrorKey(selectedField.draftId, "icon")] }}
1580
+ {{ validationErrors[getFieldErrorKey(selectedField.draftId, "style")] }}
1156
1581
  </p>
1157
1582
  </label>
1583
+ </section>
1584
+
1585
+ <section
1586
+ v-else
1587
+ data-slot="fields-configurator-field-general-options"
1588
+ class="grid gap-4 md:grid-cols-2"
1589
+ >
1590
+ <label class="flex items-center justify-between gap-3 rounded-md border border-zinc-200 px-3 py-2 md:col-span-2">
1591
+ <div class="flex flex-col gap-1">
1592
+ <span class="text-sm font-medium text-zinc-800">{{ t("field-required") }}</span>
1593
+ <span class="text-xs text-zinc-500">{{ t("field-required-description") }}</span>
1594
+ </div>
1595
+ <Switch
1596
+ data-slot="fields-configurator-field-required-switch"
1597
+ :model-value="selectedField.field.required ?? false"
1598
+ @update:model-value="updateSelectedFieldRequired"
1599
+ />
1600
+ </label>
1158
1601
 
1159
1602
  <label class="flex flex-col gap-2">
1160
1603
  <span class="text-xs font-medium text-zinc-500">
@@ -1218,7 +1661,7 @@ function confirmChanges() {
1218
1661
  </section>
1219
1662
 
1220
1663
  <section
1221
- v-if="selectedField.field.type === 'string'"
1664
+ v-if="selectedField.field.type === 'string' || selectedField.field.type === 'textarea'"
1222
1665
  data-slot="fields-configurator-string-options"
1223
1666
  class="grid gap-4 md:grid-cols-2"
1224
1667
  >
@@ -1486,6 +1929,7 @@ function confirmChanges() {
1486
1929
  </section>
1487
1930
 
1488
1931
  <section
1932
+ v-if="!isPassiveField(selectedField.field)"
1489
1933
  data-slot="fields-configurator-validation"
1490
1934
  class="flex flex-col gap-4"
1491
1935
  >
@@ -1617,25 +2061,50 @@ function confirmChanges() {
1617
2061
  </section>
1618
2062
  </div>
1619
2063
 
1620
- <DialogFooter class="border-t border-zinc-200 px-6 py-4">
1621
- <Button
1622
- type="button"
1623
- data-slot="fields-configurator-cancel"
1624
- variant="default"
1625
- @click="discardChanges"
1626
- >
1627
- <Icon icon="fluent:dismiss-20-regular" />
1628
- {{ t("cancel") }}
1629
- </Button>
1630
- <Button
1631
- type="button"
1632
- data-slot="fields-configurator-confirm"
1633
- variant="primary"
1634
- @click="confirmChanges"
2064
+ <DialogFooter class="border-t border-zinc-200 px-6 py-4 sm:justify-between">
2065
+ <div
2066
+ data-slot="fields-configurator-copy-actions"
2067
+ class="flex items-center gap-2"
1635
2068
  >
1636
- <Icon icon="fluent:checkmark-20-regular" />
1637
- {{ t("confirm") }}
1638
- </Button>
2069
+ <Button
2070
+ type="button"
2071
+ data-slot="fields-configurator-copy-markdown"
2072
+ variant="ghost"
2073
+ @click="copyMarkdown"
2074
+ >
2075
+ <Icon icon="simple-icons:markdown" />
2076
+ {{ t("copy-markdown") }}
2077
+ </Button>
2078
+ <Button
2079
+ type="button"
2080
+ data-slot="fields-configurator-copy-config"
2081
+ variant="ghost"
2082
+ @click="copyConfig"
2083
+ >
2084
+ <Icon icon="fluent:copy-20-regular" />
2085
+ {{ t("copy-config") }}
2086
+ </Button>
2087
+ </div>
2088
+ <div class="flex items-center gap-2">
2089
+ <Button
2090
+ type="button"
2091
+ data-slot="fields-configurator-cancel"
2092
+ variant="default"
2093
+ @click="discardChanges"
2094
+ >
2095
+ <Icon icon="fluent:dismiss-20-regular" />
2096
+ {{ t("cancel") }}
2097
+ </Button>
2098
+ <Button
2099
+ type="button"
2100
+ data-slot="fields-configurator-confirm"
2101
+ variant="primary"
2102
+ @click="confirmChanges"
2103
+ >
2104
+ <Icon icon="fluent:checkmark-20-regular" />
2105
+ {{ t("confirm") }}
2106
+ </Button>
2107
+ </div>
1639
2108
  </DialogFooter>
1640
2109
  </DialogContent>
1641
2110
  </Dialog>
@@ -1645,7 +2114,7 @@ function confirmChanges() {
1645
2114
  {
1646
2115
  "zh": {
1647
2116
  "configure-fields": "配置字段",
1648
- "configure-fields-description": "在这里浏览通用项和字段配置项。",
2117
+ "configure-fields-description": "在这里管理字段列表,并编辑当前选中的通用项或字段配置。",
1649
2118
  "field-list": "字段列表",
1650
2119
  "general": "通用",
1651
2120
  "general-description": "在这里配置字段集合级别的公共选项。",
@@ -1656,17 +2125,38 @@ function confirmChanges() {
1656
2125
  "fields-orientation-horizontal": "水平",
1657
2126
  "fields-orientation-vertical": "垂直",
1658
2127
  "fields-orientation-floating": "浮动标签",
2128
+ "paste-config": "粘贴配置",
2129
+ "paste-config-invalid-json": "粘贴失败,剪贴板不是有效的 JSON。",
2130
+ "paste-config-invalid-schema": "粘贴失败,配置格式无效。",
2131
+ "paste-config-read-failed": "读取剪贴板失败,请检查剪贴板权限。",
2132
+ "copy-paste-error": "复制错误",
2133
+ "copy-paste-error-failed": "复制错误详情失败,请检查剪贴板权限。",
2134
+ "copy-markdown": "复制为 Markdown",
2135
+ "copy-markdown-failed": "复制 Markdown 失败,请先修正当前配置中的错误。",
2136
+ "copy-config": "仅复制配置",
2137
+ "copy-config-failed": "复制配置失败,请先修正当前配置中的错误。",
2138
+ "search-fields": "搜索字段名称……",
1659
2139
  "add-field": "新增字段",
1660
2140
  "field-type": "字段类型",
1661
2141
  "field-type-string": "文本",
2142
+ "field-type-textarea": "多行文本",
1662
2143
  "field-type-number": "数字",
1663
2144
  "field-type-select": "选择",
1664
2145
  "field-type-calendar": "日期",
2146
+ "field-type-empty": "空白",
2147
+ "field-type-slot": "插槽",
2148
+ "field-id": "字段 ID",
2149
+ "field-id-duplicate": "字段 ID 不能重复",
1665
2150
  "field-path": "字段路径",
1666
2151
  "field-path-placeholder": "例如 profile.name",
1667
2152
  "field-path-required": "字段路径不能为空",
1668
2153
  "field-path-duplicate": "字段路径不能重复",
2154
+ "copy-field-id": "复制字段 ID:{field}",
2155
+ "copy-field-id-short": "复制 ID",
2156
+ "copy-field-id-failed": "复制字段 ID 失败,请检查剪贴板权限。",
1669
2157
  "field-label": "字段标题",
2158
+ "field-required": "显示必填提示",
2159
+ "field-required-description": "开启后仅在标签后显示红色星号,不会自动添加校验规则。",
1670
2160
  "field-icon": "图标",
1671
2161
  "field-icon-placeholder": "例如 fluent:person-20-regular",
1672
2162
  "field-style": "样式表达式",
@@ -1712,8 +2202,11 @@ function confirmChanges() {
1712
2202
  "validation-expression-placeholder": "返回 false 时展示下面的消息",
1713
2203
  "validation-message": "失败消息",
1714
2204
  "validation-message-placeholder": "支持 Markdown 与双花括号表达式",
2205
+ "no-matches": "没有匹配的字段。",
1715
2206
  "no-validation-rules": "暂未配置校验规则。",
1716
2207
  "unnamed-field": "未命名{type}字段",
2208
+ "empty-field": "空白 {id}",
2209
+ "slot-field": "插槽 {id}",
1717
2210
  "no-fields": "还没有字段。",
1718
2211
  "drag-field": "拖拽调整字段顺序:{field}",
1719
2212
  "delete-field": "删除字段:{field}",
@@ -1722,7 +2215,7 @@ function confirmChanges() {
1722
2215
  },
1723
2216
  "ja": {
1724
2217
  "configure-fields": "フィールドを設定",
1725
- "configure-fields-description": "共通項目とフィールド設定をここで確認できます。",
2218
+ "configure-fields-description": "ここではフィールド一覧を管理し、選択中の共通設定またはフィールド設定を編集できます。",
1726
2219
  "field-list": "フィールド一覧",
1727
2220
  "general": "共通",
1728
2221
  "general-description": "ここではフィールド群全体に適用される共通設定を編集できます。",
@@ -1733,17 +2226,38 @@ function confirmChanges() {
1733
2226
  "fields-orientation-horizontal": "横並び",
1734
2227
  "fields-orientation-vertical": "縦並び",
1735
2228
  "fields-orientation-floating": "フローティングラベル",
2229
+ "paste-config": "設定を貼り付け",
2230
+ "paste-config-invalid-json": "貼り付けに失敗しました。クリップボードの内容が有効な JSON ではありません。",
2231
+ "paste-config-invalid-schema": "貼り付けに失敗しました。設定形式が無効です。",
2232
+ "paste-config-read-failed": "クリップボードの読み取りに失敗しました。権限を確認してください。",
2233
+ "copy-paste-error": "エラーをコピー",
2234
+ "copy-paste-error-failed": "エラー詳細のコピーに失敗しました。クリップボード権限を確認してください。",
2235
+ "copy-markdown": "Markdown としてコピー",
2236
+ "copy-markdown-failed": "Markdown のコピーに失敗しました。現在の設定エラーを先に修正してください。",
2237
+ "copy-config": "設定のみコピー",
2238
+ "copy-config-failed": "設定のコピーに失敗しました。現在の設定エラーを先に修正してください。",
2239
+ "search-fields": "フィールド名を検索…",
1736
2240
  "add-field": "フィールドを追加",
1737
2241
  "field-type": "フィールド種別",
1738
2242
  "field-type-string": "テキスト",
2243
+ "field-type-textarea": "複数行テキスト",
1739
2244
  "field-type-number": "数値",
1740
2245
  "field-type-select": "選択",
1741
2246
  "field-type-calendar": "日付",
2247
+ "field-type-empty": "空白",
2248
+ "field-type-slot": "スロット",
2249
+ "field-id": "フィールド ID",
2250
+ "field-id-duplicate": "フィールド ID は重複できません",
1742
2251
  "field-path": "フィールドパス",
1743
2252
  "field-path-placeholder": "例: profile.name",
1744
2253
  "field-path-required": "フィールドパスは必須です",
1745
2254
  "field-path-duplicate": "フィールドパスは重複できません",
2255
+ "copy-field-id": "{field} のフィールド ID をコピー",
2256
+ "copy-field-id-short": "ID をコピー",
2257
+ "copy-field-id-failed": "フィールド ID のコピーに失敗しました。クリップボード権限を確認してください。",
1746
2258
  "field-label": "フィールドラベル",
2259
+ "field-required": "必須ヒントを表示",
2260
+ "field-required-description": "有効にするとラベルの後ろに赤い星印だけを表示し、検証ルールは自動追加しません。",
1747
2261
  "field-icon": "アイコン",
1748
2262
  "field-icon-placeholder": "例: fluent:person-20-regular",
1749
2263
  "field-style": "スタイル式",
@@ -1789,8 +2303,11 @@ function confirmChanges() {
1789
2303
  "validation-expression-placeholder": "false を返すと下のメッセージを表示します",
1790
2304
  "validation-message": "失敗メッセージ",
1791
2305
  "validation-message-placeholder": "Markdown と二重波括弧式を利用できます",
2306
+ "no-matches": "一致するフィールドがありません。",
1792
2307
  "no-validation-rules": "検証ルールはまだありません。",
1793
2308
  "unnamed-field": "未命名の{type}フィールド",
2309
+ "empty-field": "空白 {id}",
2310
+ "slot-field": "スロット {id}",
1794
2311
  "no-fields": "フィールドがありません。",
1795
2312
  "drag-field": "{field} の順序をドラッグで変更",
1796
2313
  "delete-field": "{field} を削除",
@@ -1799,7 +2316,7 @@ function confirmChanges() {
1799
2316
  },
1800
2317
  "en": {
1801
2318
  "configure-fields": "Configure Fields",
1802
- "configure-fields-description": "Browse the shared and field-level settings here.",
2319
+ "configure-fields-description": "Manage the field list and edit the selected general or field settings here.",
1803
2320
  "field-list": "Field list",
1804
2321
  "general": "General",
1805
2322
  "general-description": "Edit the shared settings that apply to the whole field group here.",
@@ -1810,17 +2327,38 @@ function confirmChanges() {
1810
2327
  "fields-orientation-horizontal": "Horizontal",
1811
2328
  "fields-orientation-vertical": "Vertical",
1812
2329
  "fields-orientation-floating": "Floating label",
2330
+ "paste-config": "Paste Config",
2331
+ "paste-config-invalid-json": "Paste failed because the clipboard does not contain valid JSON.",
2332
+ "paste-config-invalid-schema": "Paste failed because the config shape is invalid.",
2333
+ "paste-config-read-failed": "Failed to read from the clipboard. Check clipboard permissions.",
2334
+ "copy-paste-error": "Copy Error",
2335
+ "copy-paste-error-failed": "Failed to copy the error details. Check clipboard permissions.",
2336
+ "copy-markdown": "Copy as Markdown",
2337
+ "copy-markdown-failed": "Failed to copy Markdown. Fix the current config errors first.",
2338
+ "copy-config": "Copy Config Only",
2339
+ "copy-config-failed": "Failed to copy the config. Fix the current config errors first.",
2340
+ "search-fields": "Search fields…",
1813
2341
  "add-field": "Add field",
1814
2342
  "field-type": "Field type",
1815
2343
  "field-type-string": "Text",
2344
+ "field-type-textarea": "Textarea",
1816
2345
  "field-type-number": "Number",
1817
2346
  "field-type-select": "Select",
1818
2347
  "field-type-calendar": "Date",
2348
+ "field-type-empty": "Empty",
2349
+ "field-type-slot": "Slot",
2350
+ "field-id": "Field ID",
2351
+ "field-id-duplicate": "Field ID must be unique",
1819
2352
  "field-path": "Field path",
1820
2353
  "field-path-placeholder": "For example profile.name",
1821
2354
  "field-path-required": "Field path is required",
1822
2355
  "field-path-duplicate": "Field path must be unique",
2356
+ "copy-field-id": "Copy field ID: {field}",
2357
+ "copy-field-id-short": "Copy ID",
2358
+ "copy-field-id-failed": "Failed to copy the field ID. Check clipboard permissions.",
1823
2359
  "field-label": "Field label",
2360
+ "field-required": "Show required hint",
2361
+ "field-required-description": "When enabled, only a red asterisk is shown after the label. No validation rule is added automatically.",
1824
2362
  "field-icon": "Icon",
1825
2363
  "field-icon-placeholder": "For example fluent:person-20-regular",
1826
2364
  "field-style": "Style expression",
@@ -1866,8 +2404,11 @@ function confirmChanges() {
1866
2404
  "validation-expression-placeholder": "Return false to show the message below",
1867
2405
  "validation-message": "Failure message",
1868
2406
  "validation-message-placeholder": "Supports Markdown and double-curly expressions",
2407
+ "no-matches": "No matching fields.",
1869
2408
  "no-validation-rules": "No validation rules yet.",
1870
2409
  "unnamed-field": "Untitled {type} field",
2410
+ "empty-field": "Empty {id}",
2411
+ "slot-field": "Slot {id}",
1871
2412
  "no-fields": "No fields yet.",
1872
2413
  "drag-field": "Drag to reorder field {field}",
1873
2414
  "delete-field": "Delete field {field}",