|
|
@@ -1,4 +1,6 @@
|
|
|
<template>
|
|
|
+ <!-- 节点渲染 -->
|
|
|
+
|
|
|
<!-- 图片 -->
|
|
|
<image
|
|
|
v-if="node.tag === 'img'"
|
|
|
@@ -11,7 +13,12 @@
|
|
|
/>
|
|
|
|
|
|
<!-- 换行 -->
|
|
|
+ <!-- #ifdef H5 -->
|
|
|
+ <br v-else-if="node.tag === 'br'" />
|
|
|
+ <!-- #endif -->
|
|
|
+ <!-- #ifndef H5 -->
|
|
|
<text v-else-if="node.tag === 'br'">\n</text>
|
|
|
+ <!-- #endif -->
|
|
|
|
|
|
<!-- 链接 -->
|
|
|
<view
|
|
|
@@ -65,6 +72,80 @@
|
|
|
<InjectMPRender :type="node.attrs?.type as string || ''" v-bind="node.attrs" />
|
|
|
</view>
|
|
|
|
|
|
+ <!-- 表格 -->
|
|
|
+ <view
|
|
|
+ v-else-if="node.tag === 'table'"
|
|
|
+ :id="node.attrs?.id"
|
|
|
+ :class="'_parse_table ' + (node.attrs?.class || '')"
|
|
|
+ :style="style"
|
|
|
+ >
|
|
|
+ <ParseNodeRender
|
|
|
+ v-for="(child, index) in node.children"
|
|
|
+ :key="index"
|
|
|
+ :node="child"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ <view
|
|
|
+ v-else-if="node.tag === 'thead' || node.tag === 'tbody'"
|
|
|
+ :class="node.tag === 'thead' ? '_parse_thead' : '_parse_tbody'"
|
|
|
+ :style="style"
|
|
|
+ >
|
|
|
+ <ParseNodeRender
|
|
|
+ v-for="(child, index) in node.children"
|
|
|
+ :key="index"
|
|
|
+ :node="child"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ <view
|
|
|
+ v-else-if="node.tag === 'tr'"
|
|
|
+ :class="'_parse_tr ' + (node.attrs?.class || '')"
|
|
|
+ :style="style"
|
|
|
+ >
|
|
|
+ <ParseNodeRender
|
|
|
+ v-for="(child, index) in node.children"
|
|
|
+ :key="index"
|
|
|
+ :node="child"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ <view
|
|
|
+ v-else-if="node.tag === 'th'"
|
|
|
+ :class="'_parse_th ' + (node.attrs?.class || '')"
|
|
|
+ :style="style"
|
|
|
+ >
|
|
|
+ <ParseNodeRender
|
|
|
+ v-for="(child, index) in node.children"
|
|
|
+ :key="index"
|
|
|
+ :node="child"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ <view
|
|
|
+ v-else-if="node.tag === 'td'"
|
|
|
+ :class="'_parse_td ' + (node.attrs?.class || '')"
|
|
|
+ :style="style"
|
|
|
+ >
|
|
|
+ <ParseNodeRender
|
|
|
+ v-for="(child, index) in node.children"
|
|
|
+ :key="index"
|
|
|
+ :node="child"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 特殊标签,例如 ol ul 需要在前缀加序号 -->
|
|
|
+ <view v-else-if="node.tag === 'li'"
|
|
|
+ class="_ol_ul_container"
|
|
|
+ :style="style"
|
|
|
+ :class="node.attrs?.class || ''"
|
|
|
+ >
|
|
|
+ <text v-if="node.parentTag === 'ol'" class="_ol_prefix">·</text>
|
|
|
+ <text v-else class="_ul_prefix">{{ (node.index || 0) + 1 }}.</text>
|
|
|
+
|
|
|
+ <ParseNodeRender
|
|
|
+ v-for="(child, index) in node.children"
|
|
|
+ :key="index"
|
|
|
+ :node="child"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+
|
|
|
<!-- 其他标签 -->
|
|
|
<view
|
|
|
v-else-if="node.tag !== 'text'"
|
|
|
@@ -72,6 +153,7 @@
|
|
|
:data-tag="node.tag"
|
|
|
:class="node.attrs?.class || ''"
|
|
|
:style="style"
|
|
|
+ @click="viewTap(node.attrs?.id)"
|
|
|
>
|
|
|
<ParseNodeRender
|
|
|
v-for="(child, index) in node.children"
|
|
|
@@ -101,26 +183,29 @@ const props = withDefaults(defineProps<{
|
|
|
}>(), {
|
|
|
});
|
|
|
|
|
|
+const emit = defineEmits(['linkTap', 'viewTap']);
|
|
|
const tagStyle = inject<Ref<Record<string, string>>>('tagStyle', ref({}));
|
|
|
+const classStyle = inject<Ref<Record<string, string>>>('classStyle', ref({}));
|
|
|
const praseImages = inject<Ref<string[]>>('praseImages', ref([]));
|
|
|
+// 与 HTML 标准默认样式(UA 样式)一致,参考 CSS 2.1 / HTML5 规范
|
|
|
const builtInStyles = {
|
|
|
- // 标题标签
|
|
|
+ // 标题标签 (CSS 2.1 suggested defaults)
|
|
|
'h1': 'font-size: 2em; font-weight: bold; margin: 0.67em 0;',
|
|
|
- 'h2': 'font-size: 1.5em; font-weight: bold; margin: 0.83em 0;',
|
|
|
- 'h3': 'font-size: 1.17em; font-weight: bold; margin: 1em 0;',
|
|
|
- 'h4': 'font-size: 1em; font-weight: bold; margin: 1.33em 0;',
|
|
|
- 'h5': 'font-size: 0.83em; font-weight: bold; margin: 1.67em 0;',
|
|
|
+ 'h2': 'font-size: 1.5em; font-weight: bold; margin: 0.75em 0;',
|
|
|
+ 'h3': 'font-size: 1.17em; font-weight: bold; margin: 0.83em 0;',
|
|
|
+ 'h4': 'font-size: 1em; font-weight: bold; margin: 1.12em 0;',
|
|
|
+ 'h5': 'font-size: 0.83em; font-weight: bold; margin: 1.5em 0;',
|
|
|
'h6': 'font-size: 0.67em; font-weight: bold; margin: 2.33em 0;',
|
|
|
-
|
|
|
+
|
|
|
// 段落和引用
|
|
|
'p': 'margin: 1em 0;',
|
|
|
- 'blockquote': 'margin: 1em 0; padding: 0 1em; border-left: 4px solid #ddd; color: #666;',
|
|
|
-
|
|
|
+ 'blockquote': 'margin: 1em 0; border-left: 4px solid #ddd; padding-left: 1em; color: #666;',
|
|
|
+
|
|
|
// 列表
|
|
|
- 'ul': 'margin: 1em 0; padding-left: 2em;',
|
|
|
- 'ol': 'margin: 1em 0; padding-left: 2em;',
|
|
|
+ 'ul': 'margin: 1em 0; padding-left: 40px;',
|
|
|
+ 'ol': 'margin: 1em 0; padding-left: 40px;',
|
|
|
'li': 'margin: 0.5em 0;',
|
|
|
-
|
|
|
+
|
|
|
// 强调标签
|
|
|
'b': 'font-weight: bold;',
|
|
|
'strong': 'font-weight: bold;',
|
|
|
@@ -128,27 +213,36 @@ const builtInStyles = {
|
|
|
'em': 'font-style: italic;',
|
|
|
'u': 'text-decoration: underline;',
|
|
|
'del': 'text-decoration: line-through;',
|
|
|
-
|
|
|
- // 代码相关
|
|
|
- 'code': 'font-family: monospace; background: #f5f5f5; padding: 0.2em 0.4em; border-radius: 3px;',
|
|
|
- 'pre': 'font-family: monospace; background: #f5f5f5; padding: 1em; border-radius: 3px; overflow: auto;',
|
|
|
-
|
|
|
+
|
|
|
+ // 代码相关 (UA 仅 monospace,无背景)
|
|
|
+ 'code': 'font-family: monospace;',
|
|
|
+ 'pre': 'font-family: monospace; white-space: pre; margin: 1em 0;',
|
|
|
+
|
|
|
// 其他内联标签
|
|
|
- 'mark': 'background-color: #ffeb3b; padding: 0.2em;',
|
|
|
- 'sup': 'font-size: 0.83em; vertical-align: super;',
|
|
|
- 'sub': 'font-size: 0.83em; vertical-align: sub;',
|
|
|
- 'small': 'font-size: 0.83em;',
|
|
|
- 'large': 'font-size: 1.17em;',
|
|
|
-
|
|
|
- // 分隔线
|
|
|
- 'hr': 'border: 0; border-top: 1px solid #ddd; margin: 1em 0;'
|
|
|
+ 'mark': 'background-color: yellow; color: black;',
|
|
|
+ 'sup': 'font-size: smaller; vertical-align: super;',
|
|
|
+ 'sub': 'font-size: smaller; vertical-align: sub;',
|
|
|
+ 'small': 'font-size: smaller;',
|
|
|
+ 'large': 'font-size: larger;',
|
|
|
+
|
|
|
+ // 分隔线 (CSS 2.1: margin 0.5em 0,inset 在小程序端用 solid 兼容)
|
|
|
+ 'hr': 'border: none; border-top: 1px solid #ccc; margin: 0.5em 0;',
|
|
|
+
|
|
|
+ // 表格(简单布局,无 table-layout)
|
|
|
+ 'table': 'display: block; width: 100%; margin: 1em 0; overflow-x: auto;',
|
|
|
+ 'thead': 'display: block;',
|
|
|
+ 'tbody': 'display: block;',
|
|
|
+ 'tr': 'display: flex; flex-direction: row;',
|
|
|
+ 'th': 'font-weight: bold; padding: 8px 10px; border-right: 1px solid #ddd; border-bottom: 1px solid #ddd; flex: 1; min-width: 0; box-sizing: border-box;',
|
|
|
+ 'td': 'padding: 8px 10px; border-right: 1px solid #ddd; border-bottom: 1px solid #ddd; flex: 1; min-width: 0; box-sizing: border-box;'
|
|
|
} as Record<string, string>;
|
|
|
const style = computed(() =>
|
|
|
[
|
|
|
- (props.node.attrs?.style || ''),
|
|
|
(builtInStyles[props.node.tag] || ''),
|
|
|
- isInline.value ? 'display:inline' : '',
|
|
|
(tagStyle.value[props.node.tag] || ''),
|
|
|
+ (classStyle.value[props.node.attrs?.class as string] || ''),
|
|
|
+ isInline.value ? 'display:inline' : '',
|
|
|
+ (props.node.attrs?.style || ''),
|
|
|
].join(';'),
|
|
|
);
|
|
|
const isInline = computed(() => [
|
|
|
@@ -160,6 +254,7 @@ const isInline = computed(() => [
|
|
|
// 链接点击事件
|
|
|
const linkTap = (e: any) => {
|
|
|
const href = props.node.attrs?.href as string;
|
|
|
+ emit('linkTap', href);
|
|
|
if (href) {
|
|
|
if (href[0] === '#') {
|
|
|
// 跳转锚点
|
|
|
@@ -206,6 +301,10 @@ const linkTap = (e: any) => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+const viewTap = (e: any) => {
|
|
|
+ emit('viewTap', e);
|
|
|
+};
|
|
|
+
|
|
|
function preview(url: string) {
|
|
|
if (url) {
|
|
|
if (praseImages.value.includes(url)) {
|
|
|
@@ -249,6 +348,42 @@ defineOptions({
|
|
|
-webkit-touch-callout: none;
|
|
|
}
|
|
|
|
|
|
+/* ol ul 容器 */
|
|
|
+._ol_ul_container {
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+/* ol/ul 前缀 */
|
|
|
+._ol_prefix,
|
|
|
+._ul_prefix {
|
|
|
+ display: inline-block;
|
|
|
+ padding-right: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 表格:用 view + flex 模拟,无 table 标签;仅画右、下边框,首列补左边框,首行补上边框 */
|
|
|
+._parse_table {
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+._parse_thead ._parse_tr {
|
|
|
+ background-color: #f5f5f5;
|
|
|
+}
|
|
|
+._parse_thead ._parse_tr ._parse_th,
|
|
|
+._parse_thead ._parse_tr ._parse_td {
|
|
|
+ border-top: 1px solid #ddd;
|
|
|
+}
|
|
|
+._parse_tbody ._parse_tr:first-child ._parse_th,
|
|
|
+._parse_tbody ._parse_tr:first-child ._parse_td {
|
|
|
+ border-top: 1px solid #ddd;
|
|
|
+}
|
|
|
+._parse_tr ._parse_th:first-child,
|
|
|
+._parse_tr ._parse_td:first-child {
|
|
|
+ border-left: 1px solid #ddd;
|
|
|
+}
|
|
|
+._parse_th,
|
|
|
+._parse_td {
|
|
|
+ word-break: break-all;
|
|
|
+}
|
|
|
+
|
|
|
/* 视频默认效果 */
|
|
|
._video {
|
|
|
width: 300px;
|