article-content-renderer-vue2 0.5.0 → 0.5.2
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/README.md +113 -5
- package/dist/article-content-renderer-vue2.css +1 -1
- package/dist/index.cjs +6 -6
- package/dist/index.js +6060 -5997
- package/dist/types/components/ArticleContentRenderer.vue.d.ts +1 -0
- package/dist/types/core/resource-question-image.d.ts +2 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/protocols/types.d.ts +1 -0
- package/dist/types/types.d.ts +10 -0
- package/package.json +1 -1
- package/protocol/article-content-protocol-v1.json +49 -5
package/README.md
CHANGED
|
@@ -403,6 +403,52 @@ function handleArticleButtonClick(payload: ArticleButtonClickPayload): void {
|
|
|
403
403
|
}
|
|
404
404
|
```
|
|
405
405
|
|
|
406
|
+
## resourceQuestion:描述与底部文本
|
|
407
|
+
|
|
408
|
+
资源问题统一使用 `<div class="acp-resource-question" role="group">` 容器,按“图片(可选)→ 标题 → 描述 → 选项 → 底部文本(可选)”的顺序渲染。描述读取文章 JSON 中的 `resourceQuestion.attrs.description`,有内容时显示在标题下方;保留换行并按纯文本展示。
|
|
409
|
+
|
|
410
|
+
通过组件 Prop `resourceQuestionFooterText` 传入问题底部的文本:
|
|
411
|
+
|
|
412
|
+
```vue
|
|
413
|
+
<ArticleContentRenderer
|
|
414
|
+
:document="article"
|
|
415
|
+
resource-question-footer-text="请选择一个选项,继续阅读后续内容。"
|
|
416
|
+
/>
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
也可以绑定响应式变量:
|
|
420
|
+
|
|
421
|
+
```vue
|
|
422
|
+
<ArticleContentRenderer
|
|
423
|
+
:document="article"
|
|
424
|
+
:resource-question-footer-text="questionFooterText"
|
|
425
|
+
/>
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
`questionFooterText` 由使用方在 `data` 中声明并更新。该 Prop 默认为空字符串,对当前组件内所有可见的问题生效,在选项下方显示;不传、空字符串或纯空白时不创建底部文本节点。文本保留换行,HTML 标签会作为文字显示。它只影响展示,不写入文章 JSON,也不改变点击、解锁或滚动逻辑。
|
|
429
|
+
|
|
430
|
+
两个 Demo 的解锁面板均提供“问题底部文本”输入框,可直接编辑并查看效果。样式可通过 `.acp-resource-question__description` 和 `.acp-resource-question__footer` 覆盖。
|
|
431
|
+
|
|
432
|
+
## resourceQuestion.image:问题图片
|
|
433
|
+
|
|
434
|
+
资源问题支持可选的 `attrs.image`,例如在问题的 `attrs` 中加入:
|
|
435
|
+
|
|
436
|
+
```json
|
|
437
|
+
"image": {
|
|
438
|
+
"src": "https://example.com/question.png",
|
|
439
|
+
"alt": "问题配图",
|
|
440
|
+
"title": "图片说明",
|
|
441
|
+
"width": 800,
|
|
442
|
+
"height": 400
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
`src` 必填;`alt`、`title` 和尺寸均可省略,尺寸必须是 1–10000 的整数。图片显示在问题标题上方,宽度适应容器,高度按比例缩放,不裁剪;尺寸属性用于保留原始比例。没有图片时省略整个 `image` 属性,旧版无图片文章仍然兼容。
|
|
447
|
+
|
|
448
|
+
问题图片支持 HTTP(S)、相对路径和 PNG/JPEG/WebP/GIF/AVIF/BMP 的 Base64 data URL,沿用 `imageBaseUrl` 的默认前缀替换规则。渲染器直接展示 JSON 中的图片快照,浏览器会正常加载图片地址,不会因此请求问题接口或新的文章 JSON。Demo 的“载入解锁示例”包含问题图片。
|
|
449
|
+
|
|
450
|
+
如果出现 `/content/.../attrs/image:Property "image" is not allowed.`,说明所使用的渲染器或协议仍未支持该字段,需同步更新协议文件、校验器和渲染器。
|
|
451
|
+
|
|
406
452
|
## revealedKeys:解锁隐藏内容
|
|
407
453
|
|
|
408
454
|
`revealedKeys` 是由使用方维护的 `string[]`,默认值为 `[]`,通过 `:revealed-keys="revealedKeys"` 传给组件。数组中的字符串与问题节点的 `attrs.revealKey` 精确匹配,表示页面允许显示该问题之后的内容。
|
|
@@ -502,7 +548,7 @@ export default Vue.extend({
|
|
|
502
548
|
|
|
503
549
|
示例中的 `article-content` 是保存在 JSON 中的固定标识,可以替换为编辑器保存的随机值。选项事件始终返回当前问题的真实 `event.revealKey`,使用方不必预先知道它,也不要假定它等于问题 ID、资源 ID 或目标段落 ID。
|
|
504
550
|
|
|
505
|
-
`option-select` 携带 `{ questionId, resourceId, optionId, revealKey, targetAnchorId? }
|
|
551
|
+
`option-select` 携带 `{ questionId, resourceId, optionId, option, revealKey, targetAnchorId? }`,其中 `option` 是被点击选项的完整属性。组件先记录目标,再发出事件;默认在宿主更新列表、DOM 和布局就绪后定位。可通过下面的 `onAnchorNavigate` 回调控制滚动时机。没有目标的选项仍可解锁,只是不跳转。外部按钮直接解锁时,若没有待定位任务,则只显示内容。
|
|
506
552
|
|
|
507
553
|
### 多个问题与重新隐藏
|
|
508
554
|
|
|
@@ -601,7 +647,7 @@ type OnAnchorNavigate = (
|
|
|
601
647
|
) => void | Promise<void>
|
|
602
648
|
```
|
|
603
649
|
|
|
604
|
-
请求包含 `questionId`、`resourceId`、`optionId`、`revealKey` 和 `targetAnchorId
|
|
650
|
+
请求包含 `questionId`、`resourceId`、`optionId`、`option`、`revealKey` 和 `targetAnchorId`,可通过 `request.option` 读取选项属性。`scrollToAnchor()` 继续使用组件的实例内锚点、`scrollContainer`、`scrollOffset` 和焦点处理。`request.cancel()` 只取消该次点击的定位,不改变解锁列表;也可使用 `renderer-ready` 提供的 `runtime.cancelPendingNavigation()` 取消当前定位。
|
|
605
651
|
|
|
606
652
|
没有绑定锚点或锚点已不存在时,仍发出 `option-select`,但不会调用 `onAnchorNavigate`。同步在 `option-select` 中取消定位时,也不会再调用该次导航回调。每次有效点击只回调一次,重新渲染不会重复回调。
|
|
607
653
|
|
|
@@ -621,6 +667,7 @@ type OnAnchorNavigate = (
|
|
|
621
667
|
| `protocolVersion` | `number` | `1` | 协议适配器版本 |
|
|
622
668
|
| `strict` | `boolean` | `false` | 校验失败时是否停止整篇正文渲染 |
|
|
623
669
|
| `revealedKeys` | `string[]` | `[]` | 宿主允许的解锁标识;选项点击不自动修改 |
|
|
670
|
+
| `resourceQuestionFooterText` | `string` | `""` | 在每个可见资源问题的选项下方展示的纯文本,支持动态更新;空白时不显示 |
|
|
624
671
|
| `articleKey` | `string \| number` | `undefined` | 文章版本或加载代次,变化时取消旧定位;宿主仍须重置解锁状态 |
|
|
625
672
|
| `scrollContainer` | `HTMLElement \| (() => HTMLElement \| null)` | `undefined` | 目标滚动容器,省略时滚动页面 |
|
|
626
673
|
| `scrollOffset` | `number` | `0` | 定位时顶部遮挡偏移,单位 px |
|
|
@@ -632,9 +679,70 @@ type OnAnchorNavigate = (
|
|
|
632
679
|
|
|
633
680
|
## Events
|
|
634
681
|
|
|
635
|
-
### option-select
|
|
682
|
+
### option-select:点击选项并获取属性
|
|
683
|
+
|
|
684
|
+
通过 `@option-select="handleOptionSelect"` 注册点击回调,回调参数的 `event.option` 返回当前选项在文章 JSON 中保存的完整属性:
|
|
685
|
+
|
|
686
|
+
```ts
|
|
687
|
+
interface ResourceQuestionSelectEvent {
|
|
688
|
+
questionId: string
|
|
689
|
+
resourceId: string
|
|
690
|
+
optionId: string
|
|
691
|
+
option: Readonly<{
|
|
692
|
+
id: string
|
|
693
|
+
label: string
|
|
694
|
+
targetAnchorId?: string
|
|
695
|
+
}>
|
|
696
|
+
revealKey: string
|
|
697
|
+
targetAnchorId?: string
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
下面的组件接收文章并展示最近点击选项的属性,可在回调内接入自己的业务操作:
|
|
702
|
+
|
|
703
|
+
```vue
|
|
704
|
+
<script lang="ts">
|
|
705
|
+
import Vue, { type PropType } from 'vue'
|
|
706
|
+
import ArticleContentRenderer, {
|
|
707
|
+
type ArticleDocument,
|
|
708
|
+
type ResourceQuestionOption,
|
|
709
|
+
type ResourceQuestionSelectEvent,
|
|
710
|
+
} from 'article-content-renderer-vue2'
|
|
711
|
+
import 'article-content-renderer-vue2/style.css'
|
|
712
|
+
|
|
713
|
+
export default Vue.extend({
|
|
714
|
+
components: { ArticleContentRenderer },
|
|
715
|
+
props: {
|
|
716
|
+
article: { type: Object as PropType<ArticleDocument>, required: true },
|
|
717
|
+
},
|
|
718
|
+
data() {
|
|
719
|
+
return { selectedOption: null as Readonly<ResourceQuestionOption> | null }
|
|
720
|
+
},
|
|
721
|
+
methods: {
|
|
722
|
+
handleOptionSelect(event: ResourceQuestionSelectEvent): void {
|
|
723
|
+
this.selectedOption = event.option
|
|
724
|
+
// event.option.id:选项 ID
|
|
725
|
+
// event.option.label:选项文本
|
|
726
|
+
// event.option.targetAnchorId:目标锚点(未绑定时不存在)
|
|
727
|
+
// event.questionId / event.resourceId:所属问题及资源 ID
|
|
728
|
+
},
|
|
729
|
+
},
|
|
730
|
+
})
|
|
731
|
+
</script>
|
|
732
|
+
|
|
733
|
+
<template>
|
|
734
|
+
<main>
|
|
735
|
+
<ArticleContentRenderer :document="article" @option-select="handleOptionSelect" />
|
|
736
|
+
<pre v-if="selectedOption">{{ JSON.stringify(selectedOption, null, 2) }}</pre>
|
|
737
|
+
</main>
|
|
738
|
+
</template>
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
每次点击触发一次回调,包括重复点击同一选项、没有绑定锚点或锚点已删除的情况;无需在各个 JSON 选项中配置函数。选项通过所属问题和稳定的 `id` 匹配,同名选项也能区分。事件及其 `option` 是冻结的只读快照副本,不会随之后的文章修改而变化;需要编辑时先复制,例如 `{ ...event.option }`。原有的顶层 `optionId`、`targetAnchorId` 等字段继续保留。
|
|
742
|
+
|
|
743
|
+
此示例只展示回调收到的属性。宿主业务允许后可更新 `revealedKeys` 来解锁隐藏内容;默认等待 DOM 和布局后定位可见目标。配置 `onAnchorNavigate` 后,还需使用方显式调用 `request.scrollToAnchor()`。两个 Demo 的解锁面板均会展示 `event.option`。
|
|
636
744
|
|
|
637
|
-
|
|
745
|
+
### renderer-ready
|
|
638
746
|
|
|
639
747
|
`renderer-ready` 返回 `ArticleRendererRuntime`,提供 `cancelPendingNavigation()`。Vue 2 函数式组件没有可通过 `ref` 获取的实例,请保存这个运行时句柄,在拒绝、失败或主动取消时调用。
|
|
640
748
|
|
|
@@ -674,7 +782,7 @@ interface RenderIssue {
|
|
|
674
782
|
- 同一张表格的每一行必须具有相同列数。
|
|
675
783
|
- 未知节点、mark、属性和越界值会被报告。
|
|
676
784
|
|
|
677
|
-
普通链接和 articleButton 允许 `http:`、`https:`、`mailto:`、`tel
|
|
785
|
+
普通链接和 articleButton 允许 `http:`、`https:`、`mailto:`、`tel:`、相对路径和页面锚点。正文 image 节点允许 `http:`、`https:`、`blob:` 和相对路径。资源问题的 `attrs.image` 按其独立协议允许 HTTP(S)、相对路径及指定光栅格式的 Base64 data URL,不接受 `blob:` 或 SVG data URL。危险协议会被拦截。
|
|
678
786
|
|
|
679
787
|
## Vue 2 顶层渲染结构
|
|
680
788
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
:root{--acp-color-text: inherit;--acp-color-muted: #667085;--acp-color-border: #d0d5dd;--acp-color-surface: #f8fafc;--acp-color-accent: #2563eb;--acp-color-accent-hover: #1d4ed8;--acp-color-on-accent: #ffffff;--acp-radius: .375rem;--acp-spacing-block: 1rem;--acp-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace}.acp-paragraph,.acp-heading,.acp-blockquote,.acp-list,.acp-code-block,.acp-horizontal-rule,.acp-image,.acp-article-button,.acp-table-wrapper{box-sizing:border-box}.acp-paragraph,.acp-heading,.acp-blockquote,.acp-list,.acp-code-block,.acp-horizontal-rule,.acp-image,.acp-table-wrapper{margin-block:0 var(--acp-spacing-block)}.acp-paragraph:first-child,.acp-heading:first-child,.acp-blockquote:first-child,.acp-list:first-child,.acp-code-block:first-child,.acp-horizontal-rule:first-child,.acp-image:first-child,.acp-table-wrapper:first-child{margin-block-start:0}.acp-paragraph{color:var(--acp-color-text)}.acp-blockquote{margin-inline:0;padding-inline-start:1rem;border-inline-start:.25rem solid var(--acp-color-border);color:var(--acp-color-muted)}.acp-list{padding-inline-start:1.5rem}.acp-list-item>:last-child{margin-block-end:0}.acp-code-block{overflow-x:auto;padding:1rem;border-radius:var(--acp-radius);background:var(--acp-color-surface);font-family:var(--acp-font-mono);white-space:pre}.acp-mark--code{padding:.1em .25em;border-radius:calc(var(--acp-radius) / 2);background:var(--acp-color-surface);font-family:var(--acp-font-mono)}.acp-horizontal-rule{border:0;border-block-start:1px solid var(--acp-color-border)}.acp-image{display:flex;width:100%}.acp-image--left{justify-content:flex-start}.acp-image--center{justify-content:center}.acp-image--right{justify-content:flex-end}.acp-image__element{display:block;max-width:100%;height:auto}.acp-image-row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--acp-image-gap, 1rem);align-items:start}.acp-image-row>.acp-image{min-width:0}.acp-resource-question{min-inline-size:0;margin:0 0 var(--acp-spacing-block);padding:1rem;border:1px solid var(--acp-color-border);border-radius:var(--acp-radius);color:var(--acp-color-text)}.acp-resource-question__title{
|
|
1
|
+
:root{--acp-color-text: inherit;--acp-color-muted: #667085;--acp-color-border: #d0d5dd;--acp-color-surface: #f8fafc;--acp-color-accent: #2563eb;--acp-color-accent-hover: #1d4ed8;--acp-color-on-accent: #ffffff;--acp-radius: .375rem;--acp-spacing-block: 1rem;--acp-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace}.acp-paragraph,.acp-heading,.acp-blockquote,.acp-list,.acp-code-block,.acp-horizontal-rule,.acp-image,.acp-article-button,.acp-table-wrapper{box-sizing:border-box}.acp-paragraph,.acp-heading,.acp-blockquote,.acp-list,.acp-code-block,.acp-horizontal-rule,.acp-image,.acp-table-wrapper{margin-block:0 var(--acp-spacing-block)}.acp-paragraph:first-child,.acp-heading:first-child,.acp-blockquote:first-child,.acp-list:first-child,.acp-code-block:first-child,.acp-horizontal-rule:first-child,.acp-image:first-child,.acp-table-wrapper:first-child{margin-block-start:0}.acp-paragraph{color:var(--acp-color-text)}.acp-blockquote{margin-inline:0;padding-inline-start:1rem;border-inline-start:.25rem solid var(--acp-color-border);color:var(--acp-color-muted)}.acp-list{padding-inline-start:1.5rem}.acp-list-item>:last-child{margin-block-end:0}.acp-code-block{overflow-x:auto;padding:1rem;border-radius:var(--acp-radius);background:var(--acp-color-surface);font-family:var(--acp-font-mono);white-space:pre}.acp-mark--code{padding:.1em .25em;border-radius:calc(var(--acp-radius) / 2);background:var(--acp-color-surface);font-family:var(--acp-font-mono)}.acp-horizontal-rule{border:0;border-block-start:1px solid var(--acp-color-border)}.acp-image{display:flex;width:100%}.acp-image--left{justify-content:flex-start}.acp-image--center{justify-content:center}.acp-image--right{justify-content:flex-end}.acp-image__element{display:block;max-width:100%;height:auto}.acp-image-row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--acp-image-gap, 1rem);align-items:start}.acp-image-row>.acp-image{min-width:0}.acp-resource-question{min-inline-size:0;margin:0 0 var(--acp-spacing-block);padding:1rem;border:1px solid var(--acp-color-border);border-radius:var(--acp-radius);color:var(--acp-color-text)}.acp-resource-question__image{display:block;width:100%;max-width:100%;height:auto;margin-block-end:.75rem}.acp-resource-question__title{margin-block-end:.75rem;font-size:1.125em;font-weight:600;white-space:pre-wrap;overflow-wrap:anywhere}.acp-resource-question__description,.acp-resource-question__placeholder{margin:0 0 .75rem;color:var(--acp-color-muted);white-space:pre-wrap;overflow-wrap:anywhere}.acp-resource-question__options{display:grid;gap:.5rem}.acp-resource-question__footer{margin:.75rem 0 0;color:var(--acp-color-muted);font-size:.875em;white-space:pre-wrap;overflow-wrap:anywhere}.acp-resource-question__option{padding:.75rem 1rem;border:1px solid var(--acp-color-border);border-radius:var(--acp-radius);background:var(--acp-color-surface);color:inherit;font:inherit;text-align:start;white-space:pre-wrap;overflow-wrap:anywhere;cursor:pointer}.acp-resource-question__option:hover{border-color:var(--acp-color-accent)}.acp-resource-question__option:focus-visible,.acp-paragraph[data-anchor-id]:focus,.acp-heading[data-anchor-id]:focus{outline:2px solid var(--acp-color-accent);outline-offset:3px}.acp-link,.acp-article-button--text,.acp-article-button--link{color:var(--acp-color-accent);text-decoration:underline;text-underline-offset:.15em}.acp-link:hover,.acp-article-button--text:hover,.acp-article-button--link:hover{color:var(--acp-color-accent-hover)}.acp-link--disabled{cursor:not-allowed;opacity:.55}.acp-article-button{display:inline-flex;align-items:center;justify-content:center;margin-block:0 var(--acp-spacing-block);cursor:pointer}.acp-article-button--button{min-height:2.5rem;padding:.5rem 1rem;border-radius:var(--acp-radius);background:var(--acp-color-accent);color:var(--acp-color-on-accent);text-decoration:none}.acp-article-button--button:hover{background:var(--acp-color-accent-hover)}.acp-article-button--disabled{cursor:not-allowed;opacity:.55}.acp-table-wrapper{width:100%;overflow-x:auto}.acp-table{width:100%;border-collapse:collapse}.acp-table-cell{padding:.625rem .75rem;border:1px solid var(--acp-color-border);vertical-align:top}.acp-table-cell>:last-child{margin-block-end:0}.acp-render-error{padding:.75rem 1rem;border:1px solid #fda29b;border-radius:var(--acp-radius);background:#fffbfa;color:#b42318}
|