@vigilkids/section-renderer-vue 0.2.2 → 0.5.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 (71) hide show
  1. package/dist/interactions/vigilkids.d.ts +44 -1
  2. package/dist/interactions/vigilkids.mjs +115 -1
  3. package/dist/sections/article/index.mjs +60 -0
  4. package/dist/sections/article/vigilkids/ArticleFaq.vue +9 -2
  5. package/dist/sections/article/vigilkids/ArticleFaqItem.d.vue.ts +3 -0
  6. package/dist/sections/article/vigilkids/ArticleFaqItem.vue +18 -18
  7. package/dist/sections/article/vigilkids/ArticleFaqItem.vue.d.ts +3 -0
  8. package/dist/sections/article/vigilkids/ArticleHeading.vue +2 -1
  9. package/dist/sections/article/vigilkids/ArticleHighlightParagraph.d.vue.ts +21 -0
  10. package/dist/sections/article/vigilkids/ArticleHighlightParagraph.vue +38 -0
  11. package/dist/sections/article/vigilkids/ArticleHighlightParagraph.vue.d.ts +21 -0
  12. package/dist/sections/article/vigilkids/ArticleProductCard.d.vue.ts +21 -0
  13. package/dist/sections/article/vigilkids/ArticleProductCard.vue +65 -0
  14. package/dist/sections/article/vigilkids/ArticleProductCard.vue.d.ts +21 -0
  15. package/dist/sections/article/vigilkids/ArticleProductInfoBanner.d.vue.ts +21 -0
  16. package/dist/sections/article/vigilkids/ArticleProductInfoBanner.vue +56 -0
  17. package/dist/sections/article/vigilkids/ArticleProductInfoBanner.vue.d.ts +21 -0
  18. package/dist/sections/article/vigilkids/ArticleRelatedArticles.d.vue.ts +21 -0
  19. package/dist/sections/article/vigilkids/ArticleRelatedArticles.vue +86 -0
  20. package/dist/sections/article/vigilkids/ArticleRelatedArticles.vue.d.ts +21 -0
  21. package/dist/sections/article/vigilkids/ArticleRetentionBanner.d.vue.ts +21 -0
  22. package/dist/sections/article/vigilkids/ArticleRetentionBanner.vue +100 -0
  23. package/dist/sections/article/vigilkids/ArticleRetentionBanner.vue.d.ts +21 -0
  24. package/dist/sections/article/vigilkids/ArticleSectionIntro.d.vue.ts +21 -0
  25. package/dist/sections/article/vigilkids/ArticleSectionIntro.vue +56 -0
  26. package/dist/sections/article/vigilkids/ArticleSectionIntro.vue.d.ts +21 -0
  27. package/dist/sections/article/vigilkids/ArticleStepList.vue +11 -6
  28. package/dist/sections/article/vigilkids/ArticleStyledHeading.d.vue.ts +21 -0
  29. package/dist/sections/article/vigilkids/ArticleStyledHeading.vue +48 -0
  30. package/dist/sections/article/vigilkids/ArticleStyledHeading.vue.d.ts +21 -0
  31. package/dist/sections/article/vigilkids/ArticleSubheading.vue +5 -4
  32. package/dist/sections/article/vigilkids/ArticleTable.d.vue.ts +13 -1
  33. package/dist/sections/article/vigilkids/ArticleTable.vue +26 -6
  34. package/dist/sections/article/vigilkids/ArticleTable.vue.d.ts +13 -1
  35. package/dist/sections/article/vigilkids/ArticleTipNote.d.vue.ts +21 -0
  36. package/dist/sections/article/vigilkids/ArticleTipNote.vue +58 -0
  37. package/dist/sections/article/vigilkids/ArticleTipNote.vue.d.ts +21 -0
  38. package/dist/sections/article/vigilkids/ArticleToc.vue +22 -7
  39. package/dist/sections/article/vigilkids/ArticleTocList.d.vue.ts +21 -0
  40. package/dist/sections/article/vigilkids/ArticleTocList.vue +56 -0
  41. package/dist/sections/article/vigilkids/ArticleTocList.vue.d.ts +21 -0
  42. package/dist/sections/article/vigilkids/ArticleTopAd.d.vue.ts +21 -0
  43. package/dist/sections/article/vigilkids/ArticleTopAd.vue +90 -0
  44. package/dist/sections/article/vigilkids/ArticleTopAd.vue.d.ts +21 -0
  45. package/dist/sections/article/vigilkids/product-card/ProductCardCtaGroup.d.vue.ts +6 -0
  46. package/dist/sections/article/vigilkids/product-card/ProductCardCtaGroup.vue +21 -0
  47. package/dist/sections/article/vigilkids/product-card/ProductCardCtaGroup.vue.d.ts +6 -0
  48. package/dist/sections/article/vigilkids/product-card/ProductCardVariantA.d.vue.ts +21 -0
  49. package/dist/sections/article/vigilkids/product-card/ProductCardVariantA.vue +46 -0
  50. package/dist/sections/article/vigilkids/product-card/ProductCardVariantA.vue.d.ts +21 -0
  51. package/dist/sections/article/vigilkids/product-card/ProductCardVariantB.d.vue.ts +21 -0
  52. package/dist/sections/article/vigilkids/product-card/ProductCardVariantB.vue +46 -0
  53. package/dist/sections/article/vigilkids/product-card/ProductCardVariantB.vue.d.ts +21 -0
  54. package/dist/sections/article/vigilkids/product-card/ProductCardVariantC.d.vue.ts +21 -0
  55. package/dist/sections/article/vigilkids/product-card/ProductCardVariantC.vue +46 -0
  56. package/dist/sections/article/vigilkids/product-card/ProductCardVariantC.vue.d.ts +21 -0
  57. package/dist/sections/article/vigilkids/product-card/ProductCardVariantD.d.vue.ts +21 -0
  58. package/dist/sections/article/vigilkids/product-card/ProductCardVariantD.vue +56 -0
  59. package/dist/sections/article/vigilkids/product-card/ProductCardVariantD.vue.d.ts +21 -0
  60. package/dist/sections/article/vigilkids/product-card/ProductCardVariantE.d.vue.ts +21 -0
  61. package/dist/sections/article/vigilkids/product-card/ProductCardVariantE.vue +55 -0
  62. package/dist/sections/article/vigilkids/product-card/ProductCardVariantE.vue.d.ts +21 -0
  63. package/dist/sections/article/vigilkids/product-card/ProductCardVariantF.d.vue.ts +21 -0
  64. package/dist/sections/article/vigilkids/product-card/ProductCardVariantF.vue +56 -0
  65. package/dist/sections/article/vigilkids/product-card/ProductCardVariantF.vue.d.ts +21 -0
  66. package/dist/sections/article/vigilkids/product-card/ProductCardVariantG.d.vue.ts +21 -0
  67. package/dist/sections/article/vigilkids/product-card/ProductCardVariantG.vue +52 -0
  68. package/dist/sections/article/vigilkids/product-card/ProductCardVariantG.vue.d.ts +21 -0
  69. package/dist/styles/products/vigilkids.css +1 -1
  70. package/dist/styles/products/visiva.css +1 -1
  71. package/package.json +4 -1
@@ -1,5 +1,48 @@
1
1
  /**
2
2
  * vigilkids 产品专属 DOM 交互
3
- * 使用事件委托,覆盖 innerHTML 直出内容的产品特有交互
3
+ *
4
+ * 使用场景:
5
+ * - admin preview-host:走 Vue 组件渲染路径,不调用本模块
6
+ * - marketing 站点(Nuxt):innerHTML 直出 HTML 后 onMounted 激活交互
7
+ *
8
+ * 设计要点:
9
+ * - 幂等绑定:重复调用 setupInteractions 不会重复绑定事件
10
+ * - 可销毁:提供 destroyInteractions 清理 embla 实例和绑定标记,避免卸载后内存泄漏
11
+ * - 不破坏 vDOM:banner 隐藏用 style.display,而非 el.remove(),避免外层 Vue 重渲染冲突
12
+ */
13
+ /**
14
+ * 推荐文章轮播(Module 4)
15
+ * - 桌面 ≥1025px:保持 grid 布局,仅做 dot 点击滚动定位
16
+ * - 移动端 ≤1024px:动态加载 embla-carousel,同步 dot / card 高亮
17
+ *
18
+ * 幂等:容器级 data-embla-init + dot 级 data-bound,重复调用不会重复绑定或重复 new 实例
19
+ * 内存:embla 实例存入 WeakMap,destroyInteractions 时调用 .destroy() 释放监听器和 DOM 引用
20
+ */
21
+ export declare function setupCarousel(root?: Document | HTMLElement): void;
22
+ /**
23
+ * 留存 Banner(Module 5)
24
+ *
25
+ * 关闭策略:style.display = 'none'
26
+ * - 不调用 el.remove():marketing 侧 Nuxt 在 v-html 容器下,Vue diff 会检测 DOM 差异导致警告
27
+ * - admin 侧走 Vue 组件路径不进入本函数;innerHTML 场景靠本函数统一管理显隐
28
+ *
29
+ * localStorage key 规则:`retention-banner-dismissed:{dismissKey}`
30
+ */
31
+ export declare function setupRetentionBanner(root?: Document | HTMLElement): void;
32
+ /**
33
+ * 激活所有产品交互
34
+ * 幂等:所有子函数都有各自的绑定保护,重复调用安全
4
35
  */
5
36
  export declare function setupInteractions(): void;
37
+ /**
38
+ * 销毁产品交互
39
+ *
40
+ * 必须在组件卸载前调用,避免:
41
+ * - embla 实例持有 viewport 引用 + resize/scroll 监听器导致内存泄漏
42
+ * - data-bound 标记残留导致重挂载后事件绑定失败
43
+ *
44
+ * 不强制解绑 FAQ document delegate:
45
+ * - document 级委托不持有 root 引用,卸载不会产生泄漏
46
+ * - 跨多次 mount/unmount 复用同一 listener 反而更省
47
+ */
48
+ export declare function destroyInteractions(root?: Document | HTMLElement): void;
@@ -1,4 +1,9 @@
1
- export function setupInteractions() {
1
+ const emblaInstances = /* @__PURE__ */ new WeakMap();
2
+ let faqDelegateBound = false;
3
+ function setupFaqAccordion() {
4
+ if (faqDelegateBound)
5
+ return;
6
+ faqDelegateBound = true;
2
7
  document.addEventListener("click", (e) => {
3
8
  const faqTitle = e.target.closest?.(".faq-item-title");
4
9
  if (!faqTitle)
@@ -24,3 +29,112 @@ export function setupInteractions() {
24
29
  body.style.maxHeight = isActive ? "0" : `${body.scrollHeight}px`;
25
30
  });
26
31
  }
32
+ function wireDesktopDotClicks(root) {
33
+ const dots = root.querySelectorAll(".recommend-dot");
34
+ const cards = root.querySelectorAll(".recommend-card");
35
+ dots.forEach((dot, i) => {
36
+ if (dot.dataset.bound === "1")
37
+ return;
38
+ dot.dataset.bound = "1";
39
+ dot.addEventListener("click", () => {
40
+ const target = cards[i];
41
+ if (target)
42
+ target.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "start" });
43
+ });
44
+ });
45
+ }
46
+ export function setupCarousel(root = document) {
47
+ const els = root.querySelectorAll('[data-component="related-articles-carousel"]');
48
+ els.forEach((el) => {
49
+ if (el.dataset.emblaInit === "1")
50
+ return;
51
+ el.dataset.emblaInit = "1";
52
+ const viewport = el.querySelector(".recommend-grid");
53
+ if (!viewport)
54
+ return;
55
+ const mq = window.matchMedia("(max-width: 1024px)");
56
+ if (!mq.matches) {
57
+ wireDesktopDotClicks(el);
58
+ return;
59
+ }
60
+ import("embla-carousel").then(({ default: EmblaCarousel }) => {
61
+ if (el.dataset.emblaInit !== "1")
62
+ return;
63
+ const embla = EmblaCarousel(viewport, {
64
+ loop: false,
65
+ align: "start",
66
+ containScroll: "trimSnaps"
67
+ });
68
+ emblaInstances.set(el, embla);
69
+ viewport.classList.add("recommend-slider-ready");
70
+ const dots = el.querySelectorAll(".recommend-dot");
71
+ const cards = el.querySelectorAll(".recommend-card");
72
+ const syncActive = () => {
73
+ const idx = embla.selectedScrollSnap();
74
+ dots.forEach((d, i) => d.classList.toggle("active", i === idx));
75
+ cards.forEach((c, i) => c.classList.toggle("is-active", i === idx));
76
+ };
77
+ embla.on("select", syncActive);
78
+ embla.on("init", syncActive);
79
+ syncActive();
80
+ dots.forEach((dot, i) => {
81
+ if (dot.dataset.bound === "1")
82
+ return;
83
+ dot.dataset.bound = "1";
84
+ dot.addEventListener("click", () => embla.scrollTo(i));
85
+ });
86
+ });
87
+ });
88
+ }
89
+ export function setupRetentionBanner(root = document) {
90
+ const els = root.querySelectorAll('[data-component="retention-banner"]');
91
+ els.forEach((el) => {
92
+ const key = el.dataset.dismissKey;
93
+ if (!key)
94
+ return;
95
+ const storageKey = `retention-banner-dismissed:${key}`;
96
+ if (localStorage.getItem(storageKey)) {
97
+ el.style.display = "none";
98
+ return;
99
+ }
100
+ const closeBtn = el.querySelector(".retention-banner-close");
101
+ if (!closeBtn)
102
+ return;
103
+ if (closeBtn.dataset.bound === "1")
104
+ return;
105
+ closeBtn.dataset.bound = "1";
106
+ closeBtn.addEventListener("click", () => {
107
+ localStorage.setItem(storageKey, "1");
108
+ el.style.display = "none";
109
+ });
110
+ });
111
+ }
112
+ export function setupInteractions() {
113
+ setupFaqAccordion();
114
+ setupCarousel();
115
+ setupRetentionBanner();
116
+ }
117
+ export function destroyInteractions(root = document) {
118
+ const carousels = root.querySelectorAll('[data-component="related-articles-carousel"]');
119
+ carousels.forEach((el) => {
120
+ const embla = emblaInstances.get(el);
121
+ if (embla) {
122
+ embla.destroy();
123
+ emblaInstances.delete(el);
124
+ }
125
+ delete el.dataset.emblaInit;
126
+ el.querySelectorAll('.recommend-dot[data-bound="1"]').forEach((dot) => {
127
+ delete dot.dataset.bound;
128
+ });
129
+ const viewport = el.querySelector(".recommend-grid");
130
+ if (viewport)
131
+ viewport.classList.remove("recommend-slider-ready");
132
+ });
133
+ const banners = root.querySelectorAll('[data-component="retention-banner"]');
134
+ banners.forEach((el) => {
135
+ el.style.display = "";
136
+ const closeBtn = el.querySelector(".retention-banner-close");
137
+ if (closeBtn)
138
+ delete closeBtn.dataset.bound;
139
+ });
140
+ }
@@ -93,6 +93,66 @@ export function registerArticleSections() {
93
93
  componentName: "ArticleToc",
94
94
  component: () => import("./vigilkids/ArticleToc.vue")
95
95
  });
96
+ registerSection({
97
+ name: "article-related-articles",
98
+ productCode: "vigilkids",
99
+ componentName: "ArticleRelatedArticles",
100
+ component: () => import("./vigilkids/ArticleRelatedArticles.vue")
101
+ });
102
+ registerSection({
103
+ name: "article-retention-banner",
104
+ productCode: "vigilkids",
105
+ componentName: "ArticleRetentionBanner",
106
+ component: () => import("./vigilkids/ArticleRetentionBanner.vue")
107
+ });
108
+ registerSection({
109
+ name: "article-tip-note",
110
+ productCode: "vigilkids",
111
+ componentName: "ArticleTipNote",
112
+ component: () => import("./vigilkids/ArticleTipNote.vue")
113
+ });
114
+ registerSection({
115
+ name: "article-top-ad",
116
+ productCode: "vigilkids",
117
+ componentName: "ArticleTopAd",
118
+ component: () => import("./vigilkids/ArticleTopAd.vue")
119
+ });
120
+ registerSection({
121
+ name: "article-highlight-paragraph",
122
+ productCode: "vigilkids",
123
+ componentName: "ArticleHighlightParagraph",
124
+ component: () => import("./vigilkids/ArticleHighlightParagraph.vue")
125
+ });
126
+ registerSection({
127
+ name: "article-section-intro",
128
+ productCode: "vigilkids",
129
+ componentName: "ArticleSectionIntro",
130
+ component: () => import("./vigilkids/ArticleSectionIntro.vue")
131
+ });
132
+ registerSection({
133
+ name: "article-toc-list",
134
+ productCode: "vigilkids",
135
+ componentName: "ArticleTocList",
136
+ component: () => import("./vigilkids/ArticleTocList.vue")
137
+ });
138
+ registerSection({
139
+ name: "article-styled-heading",
140
+ productCode: "vigilkids",
141
+ componentName: "ArticleStyledHeading",
142
+ component: () => import("./vigilkids/ArticleStyledHeading.vue")
143
+ });
144
+ registerSection({
145
+ name: "article-product-card",
146
+ productCode: "vigilkids",
147
+ componentName: "ArticleProductCard",
148
+ component: () => import("./vigilkids/ArticleProductCard.vue")
149
+ });
150
+ registerSection({
151
+ name: "article-product-info-banner",
152
+ productCode: "vigilkids",
153
+ componentName: "ArticleProductInfoBanner",
154
+ component: () => import("./vigilkids/ArticleProductInfoBanner.vue")
155
+ });
96
156
  registerSection({
97
157
  name: "article-heading",
98
158
  productCode: "visiva",
@@ -19,7 +19,8 @@ const emit = defineEmits<{
19
19
  (e: 'undo-redo', action: 'redo' | 'undo'): void
20
20
  }>()
21
21
 
22
- useInlineEdit({
22
+ // FAQ 的可编辑粒度在 block 维度(每个 Q/A pair),透传给子项
23
+ const { blockEditableAttrs } = useInlineEdit({
23
24
  editorMode: () => !!props.editorMode,
24
25
  onSettingUpdate: (key, value) => emit('update:setting', key, value),
25
26
  onBlockSettingUpdate: (blockId, key, value) => emit('update:block-setting', blockId, key, value),
@@ -32,7 +33,13 @@ useInlineEdit({
32
33
  <template>
33
34
  <div class="faq-list">
34
35
  <template v-for="blockId in blockOrder" :key="blockId">
35
- <ArticleFaqItem v-if="blocks[blockId]" :settings="blocks[blockId]!.settings" />
36
+ <ArticleFaqItem
37
+ v-if="blocks[blockId]"
38
+ :block-id="blockId"
39
+ :editor-mode="editorMode"
40
+ :block-editable-attrs="blockEditableAttrs"
41
+ :settings="blocks[blockId]!.settings"
42
+ />
36
43
  </template>
37
44
  </div>
38
45
  </template>
@@ -1,5 +1,8 @@
1
1
  type __VLS_Props = {
2
+ blockId: string;
3
+ editorMode?: boolean;
2
4
  settings: Record<string, unknown>;
5
+ blockEditableAttrs: (blockId: string, key: string) => Record<string, unknown>;
3
6
  };
4
7
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
5
8
  export default _default;
@@ -1,28 +1,28 @@
1
1
  <script setup lang="ts">
2
- import { ref } from 'vue'
3
-
2
+ // 展开/收起状态完全由 interactions/vigilkids.ts document delegate 接管
3
+ // 这里不再自持 active/answer ref,避免与 delegate 双重触发
4
4
  defineProps<{
5
+ blockId: string
6
+ editorMode?: boolean
5
7
  settings: Record<string, unknown>
8
+ // 从父 Section 透传的 block-scope 可编辑属性工厂
9
+ blockEditableAttrs: (blockId: string, key: string) => Record<string, unknown>
6
10
  }>()
7
-
8
- const active = ref(false)
9
- const answer = ref<HTMLElement>()
10
-
11
- function toggle() {
12
- const el = answer.value
13
- if (el) {
14
- el.style.maxHeight = active.value ? '0' : `${el.scrollHeight}px`
15
- }
16
- active.value = !active.value
17
- }
18
11
  </script>
19
12
 
20
13
  <template>
21
- <div class="faq-item" :class="{ active }" @click="toggle">
22
- <p class="faq-item-title">
23
- {{ settings.question }}
24
- </p>
14
+ <div class="faq-item">
15
+ <!-- eslint-disable-next-line vue/no-v-html -->
16
+ <p
17
+ class="faq-item-title"
18
+ v-bind="blockEditableAttrs(blockId, 'question')"
19
+ v-html="settings.question"
20
+ />
25
21
  <!-- eslint-disable-next-line vue/no-v-html -->
26
- <div ref="answer" class="faq-item-answer" v-html="settings.answer" />
22
+ <div
23
+ class="faq-item-answer"
24
+ v-bind="blockEditableAttrs(blockId, 'answer')"
25
+ v-html="settings.answer"
26
+ />
27
27
  </div>
28
28
  </template>
@@ -1,5 +1,8 @@
1
1
  type __VLS_Props = {
2
+ blockId: string;
3
+ editorMode?: boolean;
2
4
  settings: Record<string, unknown>;
5
+ blockEditableAttrs: (blockId: string, key: string) => Record<string, unknown>;
3
6
  };
4
7
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
5
8
  export default _default;
@@ -38,6 +38,7 @@ const { editableAttrs } = useInlineEdit({
38
38
  :id="String(s.anchor || '')"
39
39
  :class="{ 'black-h2': variant === 'plain' }"
40
40
  >
41
- <span v-bind="editableAttrs('title')">{{ s.title }}</span>
41
+ <!-- eslint-disable-next-line vue/no-v-html -->
42
+ <span v-bind="editableAttrs('title')" v-html="s.title" />
42
43
  </h2>
43
44
  </template>
@@ -0,0 +1,21 @@
1
+ import type { BlockData } from '@vigilkids/section-core';
2
+ type __VLS_Props = {
3
+ blockOrder: string[];
4
+ blocks: Record<string, BlockData>;
5
+ editorMode?: boolean;
6
+ settings: Record<string, unknown>;
7
+ };
8
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
9
+ "update:setting": (key: string, value: unknown) => any;
10
+ "update:block-setting": (blockId: string, key: string, value: unknown) => any;
11
+ "inline-edit-start": (key: string) => any;
12
+ "inline-edit-end": () => any;
13
+ "undo-redo": (action: "redo" | "undo") => any;
14
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
15
+ "onUpdate:setting"?: ((key: string, value: unknown) => any) | undefined;
16
+ "onUpdate:block-setting"?: ((blockId: string, key: string, value: unknown) => any) | undefined;
17
+ "onInline-edit-start"?: ((key: string) => any) | undefined;
18
+ "onInline-edit-end"?: (() => any) | undefined;
19
+ "onUndo-redo"?: ((action: "redo" | "undo") => any) | undefined;
20
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
+ export default _default;
@@ -0,0 +1,38 @@
1
+ <script setup lang="ts">
2
+ import type { BlockData } from '@vigilkids/section-core'
3
+
4
+ import { computed } from 'vue'
5
+
6
+ import { useInlineEdit } from '../../../composables/useInlineEdit'
7
+
8
+ const props = defineProps<{
9
+ blockOrder: string[]
10
+ blocks: Record<string, BlockData>
11
+ editorMode?: boolean
12
+ settings: Record<string, unknown>
13
+ }>()
14
+
15
+ const emit = defineEmits<{
16
+ (e: 'update:setting', key: string, value: unknown): void
17
+ (e: 'update:block-setting', blockId: string, key: string, value: unknown): void
18
+ (e: 'inline-edit-start', key: string): void
19
+ (e: 'inline-edit-end'): void
20
+ (e: 'undo-redo', action: 'redo' | 'undo'): void
21
+ }>()
22
+
23
+ const s = computed(() => props.settings)
24
+
25
+ const { editableAttrs } = useInlineEdit({
26
+ editorMode: () => !!props.editorMode,
27
+ onSettingUpdate: (key, value) => emit('update:setting', key, value),
28
+ onBlockSettingUpdate: (blockId, key, value) => emit('update:block-setting', blockId, key, value),
29
+ onEditStart: key => emit('inline-edit-start', key),
30
+ onEditEnd: () => emit('inline-edit-end'),
31
+ onUndoRedo: action => emit('undo-redo', action),
32
+ })
33
+ </script>
34
+
35
+ <template>
36
+ <!-- eslint-disable-next-line vue/no-v-html -->
37
+ <p class="highlight-paragraph" v-bind="editableAttrs('content')" v-html="s.content" />
38
+ </template>
@@ -0,0 +1,21 @@
1
+ import type { BlockData } from '@vigilkids/section-core';
2
+ type __VLS_Props = {
3
+ blockOrder: string[];
4
+ blocks: Record<string, BlockData>;
5
+ editorMode?: boolean;
6
+ settings: Record<string, unknown>;
7
+ };
8
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
9
+ "update:setting": (key: string, value: unknown) => any;
10
+ "update:block-setting": (blockId: string, key: string, value: unknown) => any;
11
+ "inline-edit-start": (key: string) => any;
12
+ "inline-edit-end": () => any;
13
+ "undo-redo": (action: "redo" | "undo") => any;
14
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
15
+ "onUpdate:setting"?: ((key: string, value: unknown) => any) | undefined;
16
+ "onUpdate:block-setting"?: ((blockId: string, key: string, value: unknown) => any) | undefined;
17
+ "onInline-edit-start"?: ((key: string) => any) | undefined;
18
+ "onInline-edit-end"?: (() => any) | undefined;
19
+ "onUndo-redo"?: ((action: "redo" | "undo") => any) | undefined;
20
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
+ export default _default;
@@ -0,0 +1,21 @@
1
+ import type { BlockData } from '@vigilkids/section-core';
2
+ type __VLS_Props = {
3
+ blockOrder: string[];
4
+ blocks: Record<string, BlockData>;
5
+ editorMode?: boolean;
6
+ settings: Record<string, unknown>;
7
+ };
8
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
9
+ "update:setting": (key: string, value: unknown) => any;
10
+ "update:block-setting": (blockId: string, key: string, value: unknown) => any;
11
+ "inline-edit-start": (key: string) => any;
12
+ "inline-edit-end": () => any;
13
+ "undo-redo": (action: "redo" | "undo") => any;
14
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
15
+ "onUpdate:setting"?: ((key: string, value: unknown) => any) | undefined;
16
+ "onUpdate:block-setting"?: ((blockId: string, key: string, value: unknown) => any) | undefined;
17
+ "onInline-edit-start"?: ((key: string) => any) | undefined;
18
+ "onInline-edit-end"?: (() => any) | undefined;
19
+ "onUndo-redo"?: ((action: "redo" | "undo") => any) | undefined;
20
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
+ export default _default;
@@ -0,0 +1,65 @@
1
+ <script setup lang="ts">
2
+ import type { BlockData } from '@vigilkids/section-core'
3
+
4
+ import { computed } from 'vue'
5
+
6
+ import ProductCardVariantA from './product-card/ProductCardVariantA.vue'
7
+ import ProductCardVariantB from './product-card/ProductCardVariantB.vue'
8
+ import ProductCardVariantC from './product-card/ProductCardVariantC.vue'
9
+ import ProductCardVariantD from './product-card/ProductCardVariantD.vue'
10
+ import ProductCardVariantE from './product-card/ProductCardVariantE.vue'
11
+ import ProductCardVariantF from './product-card/ProductCardVariantF.vue'
12
+ import ProductCardVariantG from './product-card/ProductCardVariantG.vue'
13
+
14
+ // ArticleProductCard 分发器(Shopify Hydrogen section variants 模式)
15
+ // 根据 variant 选择子组件 + 根 class,内联 HTML 全部下沉到 variant 子组件
16
+ // 子组件各自持有完整 useInlineEdit 契约,保证可独立替换和单测
17
+
18
+ const props = defineProps<{
19
+ blockOrder: string[]
20
+ blocks: Record<string, BlockData>
21
+ editorMode?: boolean
22
+ settings: Record<string, unknown>
23
+ }>()
24
+
25
+ const emit = defineEmits<{
26
+ (e: 'update:setting', key: string, value: unknown): void
27
+ (e: 'update:block-setting', blockId: string, key: string, value: unknown): void
28
+ (e: 'inline-edit-start', key: string): void
29
+ (e: 'inline-edit-end'): void
30
+ (e: 'undo-redo', action: 'redo' | 'undo'): void
31
+ }>()
32
+
33
+ const variant = computed(() => String(props.settings.variant || 'A').toUpperCase())
34
+
35
+ // variant 到 { 根 class, 子组件 } 的映射(对齐 vigilkids.css 1494-1500)
36
+ const VARIANT_MAP = {
37
+ A: { class: 'product-card', component: ProductCardVariantA },
38
+ B: { class: 'product-card-compact', component: ProductCardVariantB },
39
+ C: { class: 'product-card-image', component: ProductCardVariantC },
40
+ D: { class: 'product-card-features', component: ProductCardVariantD },
41
+ E: { class: 'product-card-split', component: ProductCardVariantE },
42
+ F: { class: 'product-card-vertical', component: ProductCardVariantF },
43
+ G: { class: 'product-card-checklist', component: ProductCardVariantG },
44
+ } as const
45
+
46
+ // 未知 variant 时回退到 A,保证数据异常不会渲染空节点
47
+ const resolved = computed(() => VARIANT_MAP[variant.value as keyof typeof VARIANT_MAP] || VARIANT_MAP.A)
48
+ </script>
49
+
50
+ <template>
51
+ <div :class="resolved.class">
52
+ <component
53
+ :is="resolved.component"
54
+ :block-order="blockOrder"
55
+ :blocks="blocks"
56
+ :editor-mode="editorMode"
57
+ :settings="settings"
58
+ @update:setting="(key, value) => emit('update:setting', key, value)"
59
+ @update:block-setting="(blockId, key, value) => emit('update:block-setting', blockId, key, value)"
60
+ @inline-edit-start="key => emit('inline-edit-start', key)"
61
+ @inline-edit-end="() => emit('inline-edit-end')"
62
+ @undo-redo="action => emit('undo-redo', action)"
63
+ />
64
+ </div>
65
+ </template>
@@ -0,0 +1,21 @@
1
+ import type { BlockData } from '@vigilkids/section-core';
2
+ type __VLS_Props = {
3
+ blockOrder: string[];
4
+ blocks: Record<string, BlockData>;
5
+ editorMode?: boolean;
6
+ settings: Record<string, unknown>;
7
+ };
8
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
9
+ "update:setting": (key: string, value: unknown) => any;
10
+ "update:block-setting": (blockId: string, key: string, value: unknown) => any;
11
+ "inline-edit-start": (key: string) => any;
12
+ "inline-edit-end": () => any;
13
+ "undo-redo": (action: "redo" | "undo") => any;
14
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
15
+ "onUpdate:setting"?: ((key: string, value: unknown) => any) | undefined;
16
+ "onUpdate:block-setting"?: ((blockId: string, key: string, value: unknown) => any) | undefined;
17
+ "onInline-edit-start"?: ((key: string) => any) | undefined;
18
+ "onInline-edit-end"?: (() => any) | undefined;
19
+ "onUndo-redo"?: ((action: "redo" | "undo") => any) | undefined;
20
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
+ export default _default;
@@ -0,0 +1,21 @@
1
+ import type { BlockData } from '@vigilkids/section-core';
2
+ type __VLS_Props = {
3
+ blockOrder: string[];
4
+ blocks: Record<string, BlockData>;
5
+ editorMode?: boolean;
6
+ settings: Record<string, unknown>;
7
+ };
8
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
9
+ "update:setting": (key: string, value: unknown) => any;
10
+ "update:block-setting": (blockId: string, key: string, value: unknown) => any;
11
+ "inline-edit-start": (key: string) => any;
12
+ "inline-edit-end": () => any;
13
+ "undo-redo": (action: "redo" | "undo") => any;
14
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
15
+ "onUpdate:setting"?: ((key: string, value: unknown) => any) | undefined;
16
+ "onUpdate:block-setting"?: ((blockId: string, key: string, value: unknown) => any) | undefined;
17
+ "onInline-edit-start"?: ((key: string) => any) | undefined;
18
+ "onInline-edit-end"?: (() => any) | undefined;
19
+ "onUndo-redo"?: ((action: "redo" | "undo") => any) | undefined;
20
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
+ export default _default;
@@ -0,0 +1,56 @@
1
+ <script setup lang="ts">
2
+ import type { BlockData } from '@vigilkids/section-core'
3
+
4
+ import { computed } from 'vue'
5
+
6
+ import { useInlineEdit } from '../../../composables/useInlineEdit'
7
+
8
+ const props = defineProps<{
9
+ blockOrder: string[]
10
+ blocks: Record<string, BlockData>
11
+ editorMode?: boolean
12
+ settings: Record<string, unknown>
13
+ }>()
14
+
15
+ const emit = defineEmits<{
16
+ (e: 'update:setting', key: string, value: unknown): void
17
+ (e: 'update:block-setting', blockId: string, key: string, value: unknown): void
18
+ (e: 'inline-edit-start', key: string): void
19
+ (e: 'inline-edit-end'): void
20
+ (e: 'undo-redo', action: 'redo' | 'undo'): void
21
+ }>()
22
+
23
+ const s = computed(() => props.settings)
24
+
25
+ const { editableAttrs } = useInlineEdit({
26
+ editorMode: () => !!props.editorMode,
27
+ onSettingUpdate: (key, value) => emit('update:setting', key, value),
28
+ onBlockSettingUpdate: (blockId, key, value) => emit('update:block-setting', blockId, key, value),
29
+ onEditStart: key => emit('inline-edit-start', key),
30
+ onEditEnd: () => emit('inline-edit-end'),
31
+ onUndoRedo: action => emit('undo-redo', action),
32
+ })
33
+ </script>
34
+
35
+ <template>
36
+ <section>
37
+ <div class="product-info-banner">
38
+ <img class="banner-logo" :src="(s.logo_url as string) || ''" :alt="(s.logo_alt as string) || ''">
39
+ <div class="banner-body">
40
+ <!-- eslint-disable-next-line vue/no-v-html -->
41
+ <h3 class="banner-title" v-bind="editableAttrs('title')" v-html="s.title" />
42
+ <!-- eslint-disable-next-line vue/no-v-html -->
43
+ <p class="banner-desc" v-bind="editableAttrs('desc')" v-html="s.desc" />
44
+ <div class="banner-platform-row">
45
+ <!-- eslint-disable-next-line vue/no-v-html -->
46
+ <span class="banner-available" v-bind="editableAttrs('available_text')" v-html="s.available_text" />
47
+ <span class="banner-platforms">
48
+ <img :src="(s.platforms_image_url as string) || ''" :alt="(s.platforms_image_alt as string) || ''">
49
+ </span>
50
+ </div>
51
+ </div>
52
+ <!-- eslint-disable-next-line vue/no-v-html -->
53
+ <a class="product-info-banner-btn" :href="(s.cta_url as string) || '#'" v-bind="editableAttrs('cta_label')" v-html="s.cta_label" />
54
+ </div>
55
+ </section>
56
+ </template>
@@ -0,0 +1,21 @@
1
+ import type { BlockData } from '@vigilkids/section-core';
2
+ type __VLS_Props = {
3
+ blockOrder: string[];
4
+ blocks: Record<string, BlockData>;
5
+ editorMode?: boolean;
6
+ settings: Record<string, unknown>;
7
+ };
8
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
9
+ "update:setting": (key: string, value: unknown) => any;
10
+ "update:block-setting": (blockId: string, key: string, value: unknown) => any;
11
+ "inline-edit-start": (key: string) => any;
12
+ "inline-edit-end": () => any;
13
+ "undo-redo": (action: "redo" | "undo") => any;
14
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
15
+ "onUpdate:setting"?: ((key: string, value: unknown) => any) | undefined;
16
+ "onUpdate:block-setting"?: ((blockId: string, key: string, value: unknown) => any) | undefined;
17
+ "onInline-edit-start"?: ((key: string) => any) | undefined;
18
+ "onInline-edit-end"?: (() => any) | undefined;
19
+ "onUndo-redo"?: ((action: "redo" | "undo") => any) | undefined;
20
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
+ export default _default;