@vigilkids/section-renderer-vue 0.0.1 → 0.1.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.
Files changed (56) hide show
  1. package/dist/composables/useInlineEdit.d.ts +1 -1
  2. package/dist/composables/useInlineEdit.mjs +4 -2
  3. package/dist/composables/useLazyRender.mjs +2 -1
  4. package/dist/composables/useRegistry.d.ts +1 -1
  5. package/dist/composables/useRegistry.mjs +6 -3
  6. package/dist/composables/useSectionSEO.mjs +10 -5
  7. package/dist/composables/useSectionStyle.d.ts +1 -1
  8. package/dist/composables/useSectionStyle.mjs +10 -6
  9. package/dist/editor.d.ts +2 -2
  10. package/dist/editor.mjs +2 -2
  11. package/dist/index.d.ts +12 -12
  12. package/dist/index.mjs +8 -8
  13. package/dist/interactions/common.d.ts +5 -0
  14. package/dist/interactions/common.mjs +10 -0
  15. package/dist/interactions/vigilkids.d.ts +5 -0
  16. package/dist/interactions/vigilkids.mjs +26 -0
  17. package/dist/preview/createPreviewApp.mjs +33 -6
  18. package/dist/renderer/FallbackSection.vue +9 -3
  19. package/dist/renderer/LazySection.vue +25 -22
  20. package/dist/renderer/SectionErrorBoundary.vue +3 -1
  21. package/dist/renderer/SectionRenderer.vue +19 -6
  22. package/dist/renderer/SectionWrapper.vue +4 -5
  23. package/dist/sections/RichTextSection.vue +12 -12
  24. package/dist/sections/article/prosemirror.mjs +8 -4
  25. package/dist/sections/article/shared/ArticleCustomHtml.vue +46 -1
  26. package/dist/sections/article/shared/ArticleImage.vue +3 -3
  27. package/dist/sections/article/vigilkids/ArticleBulletList.vue +5 -10
  28. package/dist/sections/article/vigilkids/ArticleCta.vue +25 -39
  29. package/dist/sections/article/vigilkids/ArticleFaq.vue +4 -28
  30. package/dist/sections/article/vigilkids/ArticleFaqItem.vue +6 -7
  31. package/dist/sections/article/vigilkids/ArticleFeature.vue +8 -38
  32. package/dist/sections/article/vigilkids/ArticleHeading.vue +4 -14
  33. package/dist/sections/article/vigilkids/ArticleNotice.vue +7 -42
  34. package/dist/sections/article/vigilkids/ArticleProsCons.vue +11 -19
  35. package/dist/sections/article/vigilkids/ArticleQuestion.vue +9 -21
  36. package/dist/sections/article/vigilkids/ArticleQuote.vue +11 -13
  37. package/dist/sections/article/vigilkids/ArticleStepList.vue +9 -9
  38. package/dist/sections/article/vigilkids/ArticleSubheading.vue +8 -17
  39. package/dist/sections/article/vigilkids/ArticleTable.vue +10 -13
  40. package/dist/sections/article/vigilkids/ArticleToc.vue +14 -14
  41. package/dist/sections/article/visiva/ArticleBulletList.vue +4 -5
  42. package/dist/sections/article/visiva/ArticleCta.vue +127 -30
  43. package/dist/sections/article/visiva/ArticleFaq.vue +24 -11
  44. package/dist/sections/article/visiva/ArticleFeature.vue +22 -10
  45. package/dist/sections/article/visiva/ArticleHeading.vue +2 -2
  46. package/dist/sections/article/visiva/ArticleNotice.vue +19 -17
  47. package/dist/sections/article/visiva/ArticleProsCons.vue +41 -29
  48. package/dist/sections/article/visiva/ArticleQuestion.vue +20 -10
  49. package/dist/sections/article/visiva/ArticleQuote.vue +9 -5
  50. package/dist/sections/article/visiva/ArticleStepList.vue +9 -4
  51. package/dist/sections/article/visiva/ArticleSubheading.vue +32 -27
  52. package/dist/sections/article/visiva/ArticleTable.vue +79 -60
  53. package/dist/sections/article/visiva/ArticleToc.vue +42 -12
  54. package/dist/styles/products/vigilkids.css +1 -1
  55. package/dist/styles/products/visiva.css +1 -1
  56. package/package.json +18 -3
@@ -1,4 +1,4 @@
1
- import { type Ref } from 'vue';
1
+ import type { Ref } from 'vue';
2
2
  export interface UseInlineEditOptions {
3
3
  /** 是否处于编辑器模式 */
4
4
  editorMode: () => boolean;
@@ -6,7 +6,8 @@ export function useInlineEdit(options) {
6
6
  let originalText = "";
7
7
  function selectAllText(el) {
8
8
  const selection = window.getSelection();
9
- if (!selection) return;
9
+ if (!selection)
10
+ return;
10
11
  const range = document.createRange();
11
12
  range.selectNodeContents(el);
12
13
  selection.removeAllRanges();
@@ -35,7 +36,8 @@ export function useInlineEdit(options) {
35
36
  onEditEnd?.();
36
37
  }
37
38
  function buildAttrs(compositeKey, emitFn) {
38
- if (!editorMode()) return {};
39
+ if (!editorMode())
40
+ return {};
39
41
  if (editingKey.value === compositeKey) {
40
42
  return {
41
43
  contenteditable: "plaintext-only",
@@ -4,7 +4,8 @@ export function useLazyRender(targetRef, options = {}) {
4
4
  const isVisible = ref(typeof window === "undefined" ? ssrEager : false);
5
5
  let observer = null;
6
6
  onMounted(() => {
7
- if (isVisible.value) return;
7
+ if (isVisible.value)
8
+ return;
8
9
  const el = targetRef.value;
9
10
  if (!el) {
10
11
  isVisible.value = true;
@@ -1,5 +1,5 @@
1
- import type { Component } from 'vue';
2
1
  import type { SectionRegistration } from '@vigilkids/section-core';
2
+ import type { Component } from 'vue';
3
3
  import { defineAsyncComponent } from 'vue';
4
4
  /** Vue 组件工厂函数 */
5
5
  type ComponentFactory = () => Promise<{
@@ -1,5 +1,5 @@
1
- import { defineAsyncComponent } from "vue";
2
1
  import { SectionRegistryCore } from "@vigilkids/section-core";
2
+ import { defineAsyncComponent } from "vue";
3
3
  import FallbackSection from "../renderer/FallbackSection.vue";
4
4
  class VueSectionRegistry {
5
5
  core = new SectionRegistryCore();
@@ -8,7 +8,9 @@ class VueSectionRegistry {
8
8
  /** 注册 Section 组件(可选 productCode 用于产品级覆盖) */
9
9
  register(registration) {
10
10
  if (this.frozen) {
11
- throw new Error(`[SectionRegistry] Registry is frozen, cannot register "${registration.name}"`);
11
+ throw new Error(
12
+ `[SectionRegistry] Registry is frozen, cannot register "${registration.name}"`
13
+ );
12
14
  }
13
15
  this.core.register(registration);
14
16
  const key = registration.productCode ? `${registration.productCode}:${registration.name}` : registration.name;
@@ -27,7 +29,8 @@ class VueSectionRegistry {
27
29
  resolve(type, productCode) {
28
30
  if (productCode) {
29
31
  const productComponent = this.components.get(`${productCode}:${type}`);
30
- if (productComponent) return productComponent;
32
+ if (productComponent)
33
+ return productComponent;
31
34
  }
32
35
  return this.components.get(type) ?? null;
33
36
  }
@@ -69,7 +69,8 @@ function mapSectionToSchema(section) {
69
69
  }
70
70
  function generateFAQSchema(section) {
71
71
  const blocks = getOrderedBlocks(section);
72
- if (blocks.length === 0) return null;
72
+ if (blocks.length === 0)
73
+ return null;
73
74
  return {
74
75
  "@context": "https://schema.org",
75
76
  "@type": "FAQPage",
@@ -95,9 +96,12 @@ export function generateSectionJsonLd(sectionsData, options) {
95
96
  "name": options.title,
96
97
  "description": options.description
97
98
  };
98
- if (options.url) webPage.url = options.url;
99
- if (options.datePublished) webPage.datePublished = options.datePublished;
100
- if (options.dateModified) webPage.dateModified = options.dateModified;
99
+ if (options.url)
100
+ webPage.url = options.url;
101
+ if (options.datePublished)
102
+ webPage.datePublished = options.datePublished;
103
+ if (options.dateModified)
104
+ webPage.dateModified = options.dateModified;
101
105
  if (options.author) {
102
106
  webPage.author = { "@type": "Person", "name": options.author };
103
107
  }
@@ -109,7 +113,8 @@ export function generateSectionJsonLd(sectionsData, options) {
109
113
  const faqSections = orderedSections.filter((s) => s.type === "faq");
110
114
  for (const faq of faqSections) {
111
115
  const faqSchema = generateFAQSchema(faq);
112
- if (faqSchema) schemas.push(faqSchema);
116
+ if (faqSchema)
117
+ schemas.push(faqSchema);
113
118
  }
114
119
  return schemas;
115
120
  }
@@ -1,4 +1,4 @@
1
- import { type Ref } from 'vue';
1
+ import type { Ref } from 'vue';
2
2
  export interface SectionStyleResult {
3
3
  /** Tailwind class 列表(语义 token 映射) */
4
4
  wrapperClasses: string;
@@ -1,5 +1,5 @@
1
1
  import { computed, toValue } from "vue";
2
- const SAFE_COLOR_PATTERN = /^#[\da-fA-F]{6}$/;
2
+ const SAFE_COLOR_PATTERN = /^#[\da-f]{6}$/i;
3
3
  const SPACING_PRESETS = {
4
4
  compact: "py-8 sm:py-12",
5
5
  standard: "py-12 sm:py-24",
@@ -16,19 +16,21 @@ const MAX_WIDTH_CLASSES = {
16
16
  full: "w-full"
17
17
  };
18
18
  const VISIBILITY_CLASSES = {
19
- visible: "",
20
- hidden: "hidden",
19
+ "visible": "",
20
+ "hidden": "hidden",
21
21
  "mobile-only": "sm:hidden",
22
22
  "desktop-only": "hidden sm:block"
23
23
  };
24
24
  function safeColor(value) {
25
25
  const s = typeof value === "string" ? value : void 0;
26
- if (s && SAFE_COLOR_PATTERN.test(s)) return s;
26
+ if (s && SAFE_COLOR_PATTERN.test(s))
27
+ return s;
27
28
  return void 0;
28
29
  }
29
30
  function safePixel(value) {
30
31
  const n = typeof value === "number" ? value : Number(value);
31
- if (!Number.isNaN(n) && n >= 0 && n <= 500) return n;
32
+ if (!Number.isNaN(n) && n >= 0 && n <= 500)
33
+ return n;
32
34
  return void 0;
33
35
  }
34
36
  export function useSectionStyle(sectionId, settings) {
@@ -89,7 +91,9 @@ export function useSectionStyle(sectionId, settings) {
89
91
  desktopRules.push(`padding-bottom: ${pb}px`);
90
92
  }
91
93
  cssParts.push(`#section-${id} { ${mobileRules.join("; ")}; }`);
92
- cssParts.push(`@media (min-width: 768px) { #section-${id} { ${desktopRules.join("; ")}; } }`);
94
+ cssParts.push(
95
+ `@media (min-width: 768px) { #section-${id} { ${desktopRules.join("; ")}; } }`
96
+ );
93
97
  }
94
98
  }
95
99
  const customCSS = typeof s.custom_css === "string" ? s.custom_css.trim() : "";
package/dist/editor.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export * from './index';
2
2
  export { createPreviewApp } from './preview/createPreviewApp';
3
- export type { EditorToPreviewMessage, PreviewToEditorMessage, } from '@vigilkids/section-core/bridge';
4
- export { BRIDGE_PROTOCOL_VERSION, HEARTBEAT_INTERVAL_MS, HEARTBEAT_MAX_MISSES, PreviewBridge, PREVIEW_CHANNEL, } from '@vigilkids/section-core/bridge';
3
+ export type { EditorToPreviewMessage, PreviewToEditorMessage } from '@vigilkids/section-core/bridge';
4
+ export { BRIDGE_PROTOCOL_VERSION, HEARTBEAT_INTERVAL_MS, HEARTBEAT_MAX_MISSES, PREVIEW_CHANNEL, PreviewBridge, } from '@vigilkids/section-core/bridge';
package/dist/editor.mjs CHANGED
@@ -4,6 +4,6 @@ export {
4
4
  BRIDGE_PROTOCOL_VERSION,
5
5
  HEARTBEAT_INTERVAL_MS,
6
6
  HEARTBEAT_MAX_MISSES,
7
- PreviewBridge,
8
- PREVIEW_CHANNEL
7
+ PREVIEW_CHANNEL,
8
+ PreviewBridge
9
9
  } from "@vigilkids/section-core/bridge";
package/dist/index.d.ts CHANGED
@@ -1,18 +1,18 @@
1
- export type { BlockData, SectionDefinition, SectionInstance, SectionRegistration, SectionsData, } from '@vigilkids/section-core';
2
- export { safeColor, safeUrl } from '@vigilkids/section-core';
3
- export { default as FallbackSection } from './renderer/FallbackSection.vue';
4
- export { default as LazySection } from './renderer/LazySection.vue';
5
- export { default as SectionErrorBoundary } from './renderer/SectionErrorBoundary.vue';
6
- export { default as SectionRenderer } from './renderer/SectionRenderer.vue';
7
- export { default as SectionWrapper } from './renderer/SectionWrapper.vue';
8
- export { registerSection, useRegistry } from './composables/useRegistry';
9
- export { useLazyRender } from './composables/useLazyRender';
10
- export { useSectionStyle } from './composables/useSectionStyle';
11
- export type { SectionStyleResult } from './composables/useSectionStyle';
12
1
  export { useInlineEdit } from './composables/useInlineEdit';
13
2
  export type { UseInlineEditOptions } from './composables/useInlineEdit';
3
+ export { useLazyRender } from './composables/useLazyRender';
4
+ export { registerSection, useRegistry } from './composables/useRegistry';
14
5
  export { generateJsonLdScripts, generateSectionJsonLd } from './composables/useSectionSEO';
15
6
  export type { SectionSEOOptions } from './composables/useSectionSEO';
7
+ export { useSectionStyle } from './composables/useSectionStyle';
8
+ export type { SectionStyleResult } from './composables/useSectionStyle';
16
9
  export { SectionRendererPlugin } from './plugin';
17
- export { default as RichTextSection } from './sections/RichTextSection.vue';
10
+ export { default as FallbackSection } from './renderer/FallbackSection.vue';
11
+ export { default as LazySection } from './renderer/LazySection.vue';
12
+ export { default as SectionErrorBoundary } from './renderer/SectionErrorBoundary.vue';
13
+ export { default as SectionRenderer } from './renderer/SectionRenderer.vue';
14
+ export { default as SectionWrapper } from './renderer/SectionWrapper.vue';
18
15
  export { registerArticleSections } from './sections/article';
16
+ export { default as RichTextSection } from './sections/RichTextSection.vue';
17
+ export type { BlockData, SectionDefinition, SectionInstance, SectionRegistration, SectionsData, } from '@vigilkids/section-core';
18
+ export { safeColor, safeUrl } from '@vigilkids/section-core';
package/dist/index.mjs CHANGED
@@ -1,14 +1,14 @@
1
- export { safeColor, safeUrl } from "@vigilkids/section-core";
1
+ export { useInlineEdit } from "./composables/useInlineEdit.mjs";
2
+ export { useLazyRender } from "./composables/useLazyRender.mjs";
3
+ export { registerSection, useRegistry } from "./composables/useRegistry.mjs";
4
+ export { generateJsonLdScripts, generateSectionJsonLd } from "./composables/useSectionSEO.mjs";
5
+ export { useSectionStyle } from "./composables/useSectionStyle.mjs";
6
+ export { SectionRendererPlugin } from "./plugin.mjs";
2
7
  export { default as FallbackSection } from "./renderer/FallbackSection.vue";
3
8
  export { default as LazySection } from "./renderer/LazySection.vue";
4
9
  export { default as SectionErrorBoundary } from "./renderer/SectionErrorBoundary.vue";
5
10
  export { default as SectionRenderer } from "./renderer/SectionRenderer.vue";
6
11
  export { default as SectionWrapper } from "./renderer/SectionWrapper.vue";
7
- export { registerSection, useRegistry } from "./composables/useRegistry.mjs";
8
- export { useLazyRender } from "./composables/useLazyRender.mjs";
9
- export { useSectionStyle } from "./composables/useSectionStyle.mjs";
10
- export { useInlineEdit } from "./composables/useInlineEdit.mjs";
11
- export { generateJsonLdScripts, generateSectionJsonLd } from "./composables/useSectionSEO.mjs";
12
- export { SectionRendererPlugin } from "./plugin.mjs";
13
- export { default as RichTextSection } from "./sections/RichTextSection.vue";
14
12
  export { registerArticleSections } from "./sections/article/index.mjs";
13
+ export { default as RichTextSection } from "./sections/RichTextSection.vue";
14
+ export { safeColor, safeUrl } from "@vigilkids/section-core";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 通用 DOM 交互 — 所有产品共享
3
+ * 使用事件委托,覆盖 innerHTML 直出内容的基础交互
4
+ */
5
+ export declare function setupCommonInteractions(): void;
@@ -0,0 +1,10 @@
1
+ export function setupCommonInteractions() {
2
+ document.addEventListener("click", (e) => {
3
+ const anchor = e.target.closest?.("a");
4
+ if (!anchor)
5
+ return;
6
+ const href = anchor.getAttribute("href");
7
+ if (!href?.startsWith("#"))
8
+ e.preventDefault();
9
+ });
10
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * vigilkids 产品专属 DOM 交互
3
+ * 使用事件委托,覆盖 innerHTML 直出内容的产品特有交互
4
+ */
5
+ export declare function setupInteractions(): void;
@@ -0,0 +1,26 @@
1
+ export function setupInteractions() {
2
+ document.addEventListener("click", (e) => {
3
+ const faqTitle = e.target.closest?.(".faq-item-title");
4
+ if (!faqTitle)
5
+ return;
6
+ const faqItem = faqTitle.closest(".faq-item");
7
+ if (!faqItem)
8
+ return;
9
+ const faqList = faqItem.closest(".faq-list");
10
+ const isActive = faqItem.classList.contains("active");
11
+ if (faqList) {
12
+ faqList.querySelectorAll(".faq-item.active").forEach((item) => {
13
+ if (item !== faqItem) {
14
+ item.classList.remove("active");
15
+ const body2 = item.querySelector(".faq-item-answer");
16
+ if (body2)
17
+ body2.style.maxHeight = "0";
18
+ }
19
+ });
20
+ }
21
+ faqItem.classList.toggle("active", !isActive);
22
+ const body = faqItem.querySelector(".faq-item-answer");
23
+ if (body)
24
+ body.style.maxHeight = isActive ? "0" : `${body.scrollHeight}px`;
25
+ });
26
+ }
@@ -8,13 +8,27 @@ const productCSSLoaders = {
8
8
  visiva: () => import("../styles/products/visiva.css")
9
9
  };
10
10
  async function loadProductCSS(productCode) {
11
- if (loadedProducts.has(productCode)) return;
11
+ if (loadedProducts.has(productCode))
12
+ return;
12
13
  const loader = productCSSLoaders[productCode];
13
14
  if (loader) {
14
15
  await loader();
15
16
  loadedProducts.add(productCode);
16
17
  }
17
18
  }
19
+ const loadedInteractions = /* @__PURE__ */ new Set();
20
+ const productInteractionLoaders = {
21
+ vigilkids: () => import("../interactions/vigilkids.mjs").then((m) => m.setupInteractions())
22
+ };
23
+ async function loadProductInteractions(productCode) {
24
+ if (loadedInteractions.has(productCode))
25
+ return;
26
+ const loader = productInteractionLoaders[productCode];
27
+ if (loader) {
28
+ await loader();
29
+ loadedInteractions.add(productCode);
30
+ }
31
+ }
18
32
  export function createPreviewApp(selector, options) {
19
33
  const sectionsData = ref({
20
34
  sections: {},
@@ -43,6 +57,8 @@ export function createPreviewApp(selector, options) {
43
57
  options.bridge.on("html:preview", (payload) => {
44
58
  const data = payload;
45
59
  rawHtmlContent.value = data.html;
60
+ const product = currentProduct.value || "vigilkids";
61
+ loadProductInteractions(product);
46
62
  });
47
63
  options.bridge.on("section:select", (payload) => {
48
64
  const data = payload;
@@ -78,6 +94,7 @@ export function createPreviewApp(selector, options) {
78
94
  if (data.productCode) {
79
95
  currentProduct.value = data.productCode;
80
96
  loadProductCSS(data.productCode);
97
+ loadProductInteractions(data.productCode);
81
98
  }
82
99
  });
83
100
  function handleSectionClick(sectionId) {
@@ -86,7 +103,9 @@ export function createPreviewApp(selector, options) {
86
103
  return () => rawHtmlContent.value !== null ? h("div", {
87
104
  class: `article-content article-content--${currentProduct.value || "vigilkids"}`,
88
105
  innerHTML: rawHtmlContent.value
89
- }) : h(SectionRenderer, {
106
+ }) : h("div", {
107
+ class: `article-content article-content--${currentProduct.value || "vigilkids"}`
108
+ }, [h(SectionRenderer, {
90
109
  sectionsData: sectionsData.value,
91
110
  editorMode: true,
92
111
  selectedSectionId: selectedSectionId.value,
@@ -94,10 +113,16 @@ export function createPreviewApp(selector, options) {
94
113
  productCode: currentProduct.value || void 0,
95
114
  onSectionClick: handleSectionClick,
96
115
  onSettingUpdate: (sectionId, key, value) => {
97
- options.bridge.send({ type: "inline-edit:update", payload: { sectionId, key, value } });
116
+ options.bridge.send({
117
+ type: "inline-edit:update",
118
+ payload: { sectionId, key, value }
119
+ });
98
120
  },
99
121
  onBlockSettingUpdate: (sectionId, blockId, key, value) => {
100
- options.bridge.send({ type: "inline-edit:block-update", payload: { sectionId, blockId, key, value } });
122
+ options.bridge.send({
123
+ type: "inline-edit:block-update",
124
+ payload: { sectionId, blockId, key, value }
125
+ });
101
126
  },
102
127
  onInlineEditStart: (sectionId, key) => {
103
128
  inlineEditingField.value = { sectionId, key };
@@ -110,13 +135,15 @@ export function createPreviewApp(selector, options) {
110
135
  onInlineEditUndoRedo: (action) => {
111
136
  options.bridge.send({ type: action === "undo" ? "shortcut:undo" : "shortcut:redo" });
112
137
  }
113
- });
138
+ })]);
114
139
  }
115
140
  });
116
141
  app.use(SectionRendererPlugin);
117
142
  app.mount(selector);
143
+ import("../interactions/common.mjs").then((m) => m.setupCommonInteractions());
118
144
  document.addEventListener("keydown", (e) => {
119
- if (isEditableTarget(e)) return;
145
+ if (isEditableTarget(e))
146
+ return;
120
147
  const undoRedoAction = detectUndoRedo(e);
121
148
  if (undoRedoAction) {
122
149
  e.preventDefault();
@@ -8,10 +8,16 @@ defineProps<{
8
8
  </script>
9
9
 
10
10
  <template>
11
- <div class="flex items-center justify-center rounded border border-dashed border-gray-300 bg-gray-50 p-8 text-gray-400">
11
+ <div
12
+ class="flex items-center justify-center rounded border border-dashed border-gray-300 bg-gray-50 p-8 text-gray-400"
13
+ >
12
14
  <div class="text-center">
13
- <div class="mb-2 text-2xl">📦</div>
14
- <p class="text-sm">组件加载中...</p>
15
+ <div class="mb-2 text-2xl">
16
+ 📦
17
+ </div>
18
+ <p class="text-sm">
19
+ 组件加载中...
20
+ </p>
15
21
  </div>
16
22
  </div>
17
23
  </template>
@@ -1,18 +1,5 @@
1
1
  <script lang="ts">
2
2
  /** 根据 Section 类型返回占位高度,防止 CLS 布局偏移 */
3
- function getPlaceholderHeight(type: string): string {
4
- const heights: Record<string, string> = {
5
- 'hero-banner': '400px',
6
- 'feature-grid': '300px',
7
- 'text-with-image': '250px',
8
- 'testimonials': '280px',
9
- 'faq': '200px',
10
- 'cta': '180px',
11
- 'rich-text': '200px',
12
- 'spacer': '40px',
13
- }
14
- return heights[type] ?? '200px'
15
- }
16
3
  </script>
17
4
 
18
5
  <script setup lang="ts">
@@ -26,7 +13,7 @@ import SectionWrapper from './SectionWrapper.vue'
26
13
 
27
14
  const props = defineProps<{
28
15
  /** Block 实例映射 */
29
- blocks: Record<string, { type: string; settings: Record<string, unknown> }>
16
+ blocks: Record<string, { type: string, settings: Record<string, unknown> }>
30
17
  /** Block 排序 */
31
18
  blockOrder: string[]
32
19
  /** 编辑器模式 */
@@ -54,6 +41,20 @@ const emit = defineEmits<{
54
41
  (e: 'inlineEditUndoRedo', action: 'redo' | 'undo'): void
55
42
  }>()
56
43
 
44
+ function getPlaceholderHeight(type: string): string {
45
+ const heights: Record<string, string> = {
46
+ 'hero-banner': '400px',
47
+ 'feature-grid': '300px',
48
+ 'text-with-image': '250px',
49
+ 'testimonials': '280px',
50
+ 'faq': '200px',
51
+ 'cta': '180px',
52
+ 'rich-text': '200px',
53
+ 'spacer': '40px',
54
+ }
55
+ return heights[type] ?? '200px'
56
+ }
57
+
57
58
  const containerRef = ref<HTMLElement | null>(null)
58
59
  const { isVisible } = useLazyRender(containerRef, {
59
60
  rootMargin: props.rootMargin ?? '200px',
@@ -64,18 +65,15 @@ const { isVisible } = useLazyRender(containerRef, {
64
65
  const registry = useRegistry()
65
66
 
66
67
  // 解析注册表中的组件,未注册则降级到 FallbackSection
67
- const resolvedComponent = computed(() =>
68
- registry.resolve(props.sectionType, props.productCode) ?? FallbackSection,
68
+ const resolvedComponent = computed(
69
+ () => registry.resolve(props.sectionType, props.productCode) ?? FallbackSection,
69
70
  )
70
71
  </script>
71
72
 
72
73
  <template>
73
74
  <div ref="containerRef">
74
75
  <template v-if="isVisible || isSelected">
75
- <SectionErrorBoundary
76
- :section-id="sectionId"
77
- :section-type="sectionType"
78
- >
76
+ <SectionErrorBoundary :section-id="sectionId" :section-type="sectionType">
79
77
  <SectionWrapper
80
78
  :section-id="sectionId"
81
79
  :section-type="sectionType"
@@ -90,8 +88,13 @@ const resolvedComponent = computed(() =>
90
88
  :blocks="blocks"
91
89
  :block-order="blockOrder"
92
90
  :editor-mode="editorMode"
93
- @update:setting="(key: string, value: unknown) => emit('settingUpdate', sectionId, key, value)"
94
- @update:block-setting="(blockId: string, key: string, value: unknown) => emit('blockSettingUpdate', sectionId, blockId, key, value)"
91
+ @update:setting="
92
+ (key: string, value: unknown) => emit('settingUpdate', sectionId, key, value)
93
+ "
94
+ @update:block-setting="
95
+ (blockId: string, key: string, value: unknown) =>
96
+ emit('blockSettingUpdate', sectionId, blockId, key, value)
97
+ "
95
98
  @inline-edit-start="(key: string) => emit('inlineEditStart', sectionId, key)"
96
99
  @inline-edit-end="emit('inlineEditEnd')"
97
100
  @undo-redo="(action: 'redo' | 'undo') => emit('inlineEditUndoRedo', action)"
@@ -24,7 +24,9 @@ onErrorCaptured((err) => {
24
24
  可能原因:组件加载失败或数据格式异常。此错误不影响其他组件。
25
25
  </p>
26
26
  <details class="mb-3">
27
- <summary class="cursor-pointer text-xs text-red-500">技术详情</summary>
27
+ <summary class="cursor-pointer text-xs text-red-500">
28
+ 技术详情
29
+ </summary>
28
30
  <code class="mt-1 block whitespace-pre-wrap text-xs">{{ error.message }}</code>
29
31
  </details>
30
32
  <button
@@ -3,11 +3,11 @@ import type { SectionsData } from '@vigilkids/section-core'
3
3
 
4
4
  import { computed } from 'vue'
5
5
 
6
+ import { useRegistry } from '../composables/useRegistry'
6
7
  import FallbackSection from './FallbackSection.vue'
7
8
  import LazySection from './LazySection.vue'
8
9
  import SectionErrorBoundary from './SectionErrorBoundary.vue'
9
10
  import SectionWrapper from './SectionWrapper.vue'
10
- import { useRegistry } from '../composables/useRegistry'
11
11
 
12
12
  const props = defineProps<{
13
13
  /** 是否处于编辑器预览模式(启用 data-* 属性和高亮) */
@@ -67,8 +67,13 @@ const orderedSections = computed(() =>
67
67
  :blocks="item.section.blocks"
68
68
  :block-order="item.section.block_order"
69
69
  :editor-mode="editorMode"
70
- @update:setting="(key: string, value: unknown) => emit('setting-update', item.id, key, value)"
71
- @update:block-setting="(blockId: string, key: string, value: unknown) => emit('block-setting-update', item.id, blockId, key, value)"
70
+ @update:setting="
71
+ (key: string, value: unknown) => emit('setting-update', item.id, key, value)
72
+ "
73
+ @update:block-setting="
74
+ (blockId: string, key: string, value: unknown) =>
75
+ emit('block-setting-update', item.id, blockId, key, value)
76
+ "
72
77
  @inline-edit-start="(key: string) => emit('inline-edit-start', item.id, key)"
73
78
  @inline-edit-end="emit('inline-edit-end')"
74
79
  @undo-redo="(action: 'redo' | 'undo') => emit('inline-edit-undo-redo', action)"
@@ -88,9 +93,17 @@ const orderedSections = computed(() =>
88
93
  :is-selected="allSectionsSelected || selectedSectionId === item.id"
89
94
  :product-code="productCode"
90
95
  @section-click="emit('section-click', $event)"
91
- @setting-update="(sectionId: string, key: string, value: unknown) => emit('setting-update', sectionId, key, value)"
92
- @block-setting-update="(sectionId: string, blockId: string, key: string, value: unknown) => emit('block-setting-update', sectionId, blockId, key, value)"
93
- @inline-edit-start="(sectionId: string, key: string) => emit('inline-edit-start', sectionId, key)"
96
+ @setting-update="
97
+ (sectionId: string, key: string, value: unknown) =>
98
+ emit('setting-update', sectionId, key, value)
99
+ "
100
+ @block-setting-update="
101
+ (sectionId: string, blockId: string, key: string, value: unknown) =>
102
+ emit('block-setting-update', sectionId, blockId, key, value)
103
+ "
104
+ @inline-edit-start="
105
+ (sectionId: string, key: string) => emit('inline-edit-start', sectionId, key)
106
+ "
94
107
  @inline-edit-end="emit('inline-edit-end')"
95
108
  @inline-edit-undo-redo="(action: 'redo' | 'undo') => emit('inline-edit-undo-redo', action)"
96
109
  />
@@ -41,12 +41,11 @@ const { style } = useSectionStyle(
41
41
  </section>
42
42
 
43
43
  <!-- Scoped CSS 注入(自定义间距 + custom_css) -->
44
- <component
45
- v-if="style.scopedCSS"
46
- :is="'style'"
47
- >{{ style.scopedCSS }}</component>
44
+ <component is="style" v-if="style.scopedCSS">
45
+ {{ style.scopedCSS }}
46
+ </component>
48
47
  </template>
49
48
 
50
49
  <style scoped>
51
- .section-wrapper{position:relative}.section-wrapper--hoverable:hover{outline:2px solid rgba(22,119,255,.3);outline-offset:-2px}.section-wrapper--selected{outline:2px solid #1677ff;outline-offset:-2px}.section-wrapper--hoverable:hover:before{background:#1677ff;border-radius:4px;color:#fff;content:attr(data-section-type);font-size:12px;left:8px;padding:2px 8px;position:absolute;top:-24px;z-index:10}.section-wrapper :deep(.inline-editable){border-radius:2px;cursor:text;transition:outline .15s ease}.section-wrapper :deep(.inline-editable:hover){outline:1px dashed rgba(22,119,255,.4);outline-offset:2px}.section-wrapper :deep(.inline-editing){border-radius:2px;cursor:text;min-height:1em;min-width:20px;outline:2px solid #1677ff;outline-offset:2px}
50
+ .section-wrapper{position:relative}.section-wrapper--hoverable:hover{outline:2px solid rgba(22,119,255,.3);outline-offset:1px}.section-wrapper--selected{outline:2px solid #1677ff;outline-offset:1px}.section-wrapper--hoverable:hover:before{background:#1677ff;border-radius:4px;color:#fff;content:attr(data-section-type);font-size:12px;left:8px;padding:2px 8px;position:absolute;top:-24px;z-index:10}.section-wrapper :deep(.inline-editable){cursor:text;transition:outline .15s ease}.section-wrapper :deep(.inline-editable:hover){outline:1px dashed rgba(22,119,255,.4);outline-offset:2px}.section-wrapper :deep(.inline-editing){cursor:text;min-height:1em;min-width:20px;outline:2px solid #1677ff;outline-offset:2px}
52
51
  </style>
@@ -1,10 +1,10 @@
1
1
  <script setup lang="ts">
2
2
  import type { BlockData } from '@vigilkids/section-core'
3
3
 
4
- import { computed } from 'vue'
5
-
6
4
  import { safeColor } from '@vigilkids/section-core'
7
5
 
6
+ import { computed } from 'vue'
7
+
8
8
  const props = defineProps<{
9
9
  blockOrder: string[]
10
10
  blocks: Record<string, BlockData>
@@ -35,7 +35,8 @@ const paddingClass = computed(() => {
35
35
  /** 将 ProseMirror JSON 或 HTML 字符串转为渲染 HTML */
36
36
  const renderedContent = computed(() => {
37
37
  const content = s.value.content
38
- if (typeof content === 'string') return content
38
+ if (typeof content === 'string')
39
+ return content
39
40
  if (content && typeof content === 'object') {
40
41
  return renderProseMirrorBasic(content as ProseMirrorDoc)
41
42
  }
@@ -46,7 +47,7 @@ const renderedContent = computed(() => {
46
47
  interface ProseMirrorNode {
47
48
  attrs?: Record<string, unknown>
48
49
  content?: ProseMirrorNode[]
49
- marks?: Array<{ attrs?: Record<string, unknown>; type: string }>
50
+ marks?: Array<{ attrs?: Record<string, unknown>, type: string }>
50
51
  text?: string
51
52
  type: string
52
53
  }
@@ -59,7 +60,8 @@ interface ProseMirrorDoc {
59
60
 
60
61
  /** 基础 ProseMirror JSON → HTML 转换 */
61
62
  function renderProseMirrorBasic(doc: ProseMirrorDoc): string {
62
- if (!doc.content) return ''
63
+ if (!doc.content)
64
+ return ''
63
65
  return doc.content.map(renderNode).join('')
64
66
  }
65
67
 
@@ -93,12 +95,14 @@ function renderNode(node: ProseMirrorNode): string {
93
95
  }
94
96
 
95
97
  function renderChildren(node: ProseMirrorNode): string {
96
- if (!node.content) return node.text ?? ''
98
+ if (!node.content)
99
+ return node.text ?? ''
97
100
  return node.content.map(renderNode).join('')
98
101
  }
99
102
 
100
103
  function applyMarks(text: string, marks?: ProseMirrorNode['marks']): string {
101
- if (!marks) return text
104
+ if (!marks)
105
+ return text
102
106
  let result = text
103
107
  for (const mark of marks) {
104
108
  switch (mark.type) {
@@ -126,10 +130,6 @@ function applyMarks(text: string, marks?: ProseMirrorNode['marks']): string {
126
130
  :class="paddingClass"
127
131
  :style="{ backgroundColor: safeColor(String(s.bg_color ?? '#ffffff')) }"
128
132
  >
129
- <div
130
- class="prose prose-lg mx-auto"
131
- :class="maxWidthClass"
132
- v-html="renderedContent"
133
- />
133
+ <div class="prose prose-lg mx-auto" :class="maxWidthClass" v-html="renderedContent" />
134
134
  </article>
135
135
  </template>