@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.
- package/dist/composables/useInlineEdit.d.ts +1 -1
- package/dist/composables/useInlineEdit.mjs +4 -2
- package/dist/composables/useLazyRender.mjs +2 -1
- package/dist/composables/useRegistry.d.ts +1 -1
- package/dist/composables/useRegistry.mjs +6 -3
- package/dist/composables/useSectionSEO.mjs +10 -5
- package/dist/composables/useSectionStyle.d.ts +1 -1
- package/dist/composables/useSectionStyle.mjs +10 -6
- package/dist/editor.d.ts +2 -2
- package/dist/editor.mjs +2 -2
- package/dist/index.d.ts +12 -12
- package/dist/index.mjs +8 -8
- package/dist/interactions/common.d.ts +5 -0
- package/dist/interactions/common.mjs +10 -0
- package/dist/interactions/vigilkids.d.ts +5 -0
- package/dist/interactions/vigilkids.mjs +26 -0
- package/dist/preview/createPreviewApp.mjs +33 -6
- package/dist/renderer/FallbackSection.vue +9 -3
- package/dist/renderer/LazySection.vue +25 -22
- package/dist/renderer/SectionErrorBoundary.vue +3 -1
- package/dist/renderer/SectionRenderer.vue +19 -6
- package/dist/renderer/SectionWrapper.vue +4 -5
- package/dist/sections/RichTextSection.vue +12 -12
- package/dist/sections/article/prosemirror.mjs +8 -4
- package/dist/sections/article/shared/ArticleCustomHtml.vue +46 -1
- package/dist/sections/article/shared/ArticleImage.vue +3 -3
- package/dist/sections/article/vigilkids/ArticleBulletList.vue +5 -10
- package/dist/sections/article/vigilkids/ArticleCta.vue +25 -39
- package/dist/sections/article/vigilkids/ArticleFaq.vue +4 -28
- package/dist/sections/article/vigilkids/ArticleFaqItem.vue +6 -7
- package/dist/sections/article/vigilkids/ArticleFeature.vue +8 -38
- package/dist/sections/article/vigilkids/ArticleHeading.vue +4 -14
- package/dist/sections/article/vigilkids/ArticleNotice.vue +7 -42
- package/dist/sections/article/vigilkids/ArticleProsCons.vue +11 -19
- package/dist/sections/article/vigilkids/ArticleQuestion.vue +9 -21
- package/dist/sections/article/vigilkids/ArticleQuote.vue +11 -13
- package/dist/sections/article/vigilkids/ArticleStepList.vue +9 -9
- package/dist/sections/article/vigilkids/ArticleSubheading.vue +8 -17
- package/dist/sections/article/vigilkids/ArticleTable.vue +10 -13
- package/dist/sections/article/vigilkids/ArticleToc.vue +14 -14
- package/dist/sections/article/visiva/ArticleBulletList.vue +4 -5
- package/dist/sections/article/visiva/ArticleCta.vue +127 -30
- package/dist/sections/article/visiva/ArticleFaq.vue +24 -11
- package/dist/sections/article/visiva/ArticleFeature.vue +22 -10
- package/dist/sections/article/visiva/ArticleHeading.vue +2 -2
- package/dist/sections/article/visiva/ArticleNotice.vue +19 -17
- package/dist/sections/article/visiva/ArticleProsCons.vue +41 -29
- package/dist/sections/article/visiva/ArticleQuestion.vue +20 -10
- package/dist/sections/article/visiva/ArticleQuote.vue +9 -5
- package/dist/sections/article/visiva/ArticleStepList.vue +9 -4
- package/dist/sections/article/visiva/ArticleSubheading.vue +32 -27
- package/dist/sections/article/visiva/ArticleTable.vue +79 -60
- package/dist/sections/article/visiva/ArticleToc.vue +42 -12
- package/dist/styles/products/vigilkids.css +1 -1
- package/dist/styles/products/visiva.css +1 -1
- package/package.json +18 -3
|
@@ -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)
|
|
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())
|
|
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)
|
|
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 { 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(
|
|
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)
|
|
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)
|
|
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)
|
|
99
|
-
|
|
100
|
-
if (options.
|
|
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)
|
|
116
|
+
if (faqSchema)
|
|
117
|
+
schemas.push(faqSchema);
|
|
113
118
|
}
|
|
114
119
|
return schemas;
|
|
115
120
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { computed, toValue } from "vue";
|
|
2
|
-
const SAFE_COLOR_PATTERN = /^#[\da-
|
|
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))
|
|
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)
|
|
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(
|
|
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
|
|
4
|
-
export { BRIDGE_PROTOCOL_VERSION, HEARTBEAT_INTERVAL_MS, HEARTBEAT_MAX_MISSES,
|
|
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
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
|
|
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 {
|
|
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,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,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))
|
|
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(
|
|
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({
|
|
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({
|
|
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))
|
|
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
|
|
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"
|
|
14
|
-
|
|
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
|
|
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="
|
|
94
|
-
|
|
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"
|
|
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="
|
|
71
|
-
|
|
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="
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
|
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')
|
|
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
|
|
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)
|
|
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)
|
|
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)
|
|
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>
|