@vigilkids/section-renderer-vue 0.0.1 → 0.1.0

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 (51) 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/preview/createPreviewApp.mjs +12 -4
  14. package/dist/renderer/FallbackSection.vue +9 -3
  15. package/dist/renderer/LazySection.vue +25 -22
  16. package/dist/renderer/SectionErrorBoundary.vue +3 -1
  17. package/dist/renderer/SectionRenderer.vue +19 -6
  18. package/dist/renderer/SectionWrapper.vue +4 -5
  19. package/dist/sections/RichTextSection.vue +12 -12
  20. package/dist/sections/article/prosemirror.mjs +8 -4
  21. package/dist/sections/article/shared/ArticleCustomHtml.vue +1 -1
  22. package/dist/sections/article/shared/ArticleImage.vue +3 -3
  23. package/dist/sections/article/vigilkids/ArticleBulletList.vue +2 -2
  24. package/dist/sections/article/vigilkids/ArticleCta.vue +48 -21
  25. package/dist/sections/article/vigilkids/ArticleFaq.vue +11 -5
  26. package/dist/sections/article/vigilkids/ArticleFaqItem.vue +6 -2
  27. package/dist/sections/article/vigilkids/ArticleFeature.vue +16 -8
  28. package/dist/sections/article/vigilkids/ArticleHeading.vue +3 -4
  29. package/dist/sections/article/vigilkids/ArticleNotice.vue +33 -9
  30. package/dist/sections/article/vigilkids/ArticleProsCons.vue +6 -6
  31. package/dist/sections/article/vigilkids/ArticleQuestion.vue +11 -5
  32. package/dist/sections/article/vigilkids/ArticleQuote.vue +6 -4
  33. package/dist/sections/article/vigilkids/ArticleStepList.vue +8 -4
  34. package/dist/sections/article/vigilkids/ArticleSubheading.vue +18 -6
  35. package/dist/sections/article/vigilkids/ArticleTable.vue +9 -8
  36. package/dist/sections/article/vigilkids/ArticleToc.vue +13 -9
  37. package/dist/sections/article/visiva/ArticleBulletList.vue +2 -2
  38. package/dist/sections/article/visiva/ArticleCta.vue +127 -30
  39. package/dist/sections/article/visiva/ArticleFaq.vue +22 -8
  40. package/dist/sections/article/visiva/ArticleFeature.vue +20 -7
  41. package/dist/sections/article/visiva/ArticleHeading.vue +2 -2
  42. package/dist/sections/article/visiva/ArticleNotice.vue +7 -5
  43. package/dist/sections/article/visiva/ArticleProsCons.vue +37 -23
  44. package/dist/sections/article/visiva/ArticleQuestion.vue +18 -7
  45. package/dist/sections/article/visiva/ArticleQuote.vue +9 -5
  46. package/dist/sections/article/visiva/ArticleStepList.vue +9 -4
  47. package/dist/sections/article/visiva/ArticleSubheading.vue +32 -27
  48. package/dist/sections/article/visiva/ArticleTable.vue +79 -60
  49. package/dist/sections/article/visiva/ArticleToc.vue +42 -12
  50. package/dist/styles/products/visiva.css +1 -1
  51. package/package.json +10 -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";
@@ -8,7 +8,8 @@ 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();
@@ -94,10 +95,16 @@ export function createPreviewApp(selector, options) {
94
95
  productCode: currentProduct.value || void 0,
95
96
  onSectionClick: handleSectionClick,
96
97
  onSettingUpdate: (sectionId, key, value) => {
97
- options.bridge.send({ type: "inline-edit:update", payload: { sectionId, key, value } });
98
+ options.bridge.send({
99
+ type: "inline-edit:update",
100
+ payload: { sectionId, key, value }
101
+ });
98
102
  },
99
103
  onBlockSettingUpdate: (sectionId, blockId, key, value) => {
100
- options.bridge.send({ type: "inline-edit:block-update", payload: { sectionId, blockId, key, value } });
104
+ options.bridge.send({
105
+ type: "inline-edit:block-update",
106
+ payload: { sectionId, blockId, key, value }
107
+ });
101
108
  },
102
109
  onInlineEditStart: (sectionId, key) => {
103
110
  inlineEditingField.value = { sectionId, key };
@@ -116,7 +123,8 @@ export function createPreviewApp(selector, options) {
116
123
  app.use(SectionRendererPlugin);
117
124
  app.mount(selector);
118
125
  document.addEventListener("keydown", (e) => {
119
- if (isEditableTarget(e)) return;
126
+ if (isEditableTarget(e))
127
+ return;
120
128
  const undoRedoAction = detectUndoRedo(e);
121
129
  if (undoRedoAction) {
122
130
  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>
@@ -27,11 +27,13 @@ function renderNode(node) {
27
27
  }
28
28
  }
29
29
  function renderChildren(node) {
30
- if (!node.content) return node.text ?? "";
30
+ if (!node.content)
31
+ return node.text ?? "";
31
32
  return node.content.map(renderNode).join("");
32
33
  }
33
34
  function applyMarks(text, marks) {
34
- if (!marks) return text;
35
+ if (!marks)
36
+ return text;
35
37
  let result = text;
36
38
  for (const mark of marks) {
37
39
  switch (mark.type) {
@@ -55,10 +57,12 @@ function applyMarks(text, marks) {
55
57
  return result;
56
58
  }
57
59
  export function renderContent(content) {
58
- if (typeof content === "string") return content;
60
+ if (typeof content === "string")
61
+ return content;
59
62
  if (content && typeof content === "object" && content.type === "doc") {
60
63
  const doc = content;
61
- if (!doc.content) return "";
64
+ if (!doc.content)
65
+ return "";
62
66
  return doc.content.map(renderNode).join("");
63
67
  }
64
68
  return "";
@@ -28,5 +28,5 @@ const htmlContent = computed(() => {
28
28
  </script>
29
29
 
30
30
  <template>
31
- <div :class="['article-custom-html', 'article-content', productClass]" v-html="htmlContent" />
31
+ <div class="article-custom-html article-content" :class="[productClass]" v-html="htmlContent" />
32
32
  </template>
@@ -27,9 +27,9 @@ const { editableAttrs } = useInlineEdit({
27
27
  editorMode: () => !!props.editorMode,
28
28
  onSettingUpdate: (key, value) => emit('update:setting', key, value),
29
29
  onBlockSettingUpdate: (blockId, key, value) => emit('update:block-setting', blockId, key, value),
30
- onEditStart: (key) => emit('inline-edit-start', key),
30
+ onEditStart: key => emit('inline-edit-start', key),
31
31
  onEditEnd: () => emit('inline-edit-end'),
32
- onUndoRedo: (action) => emit('undo-redo', action),
32
+ onUndoRedo: action => emit('undo-redo', action),
33
33
  })
34
34
  </script>
35
35
 
@@ -41,7 +41,7 @@ const { editableAttrs } = useInlineEdit({
41
41
  :src="safeUrl(String(s.src))"
42
42
  class="article-image__img"
43
43
  loading="lazy"
44
- />
44
+ >
45
45
  <figcaption v-if="s.caption" class="article-image__caption" v-bind="editableAttrs('caption')">
46
46
  {{ s.caption }}
47
47
  </figcaption>
@@ -27,9 +27,9 @@ const { blockEditableAttrs } = useInlineEdit({
27
27
  editorMode: () => !!props.editorMode,
28
28
  onSettingUpdate: (key, value) => emit('update:setting', key, value),
29
29
  onBlockSettingUpdate: (blockId, key, value) => emit('update:block-setting', blockId, key, value),
30
- onEditStart: (key) => emit('inline-edit-start', key),
30
+ onEditStart: key => emit('inline-edit-start', key),
31
31
  onEditEnd: () => emit('inline-edit-end'),
32
- onUndoRedo: (action) => emit('undo-redo', action),
32
+ onUndoRedo: action => emit('undo-redo', action),
33
33
  })
34
34
  </script>
35
35