Files
hospital-rescue-app-v2/uni_modules/tmx-ui/components/x-markdown/x-markdown.uvue
T
2026-09-24 16:25:22 +08:00

469 lines
12 KiB
Plaintext

<script lang="ts">
import { type PropType } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
// #ifdef MP
import { Marked } from "./marked.uts"
import { hljs } from "./highlight.min.js"
import { markedHighlight } from "./marked-highlight.js"
import * as katex from "./katex.js"
import {
createClass,
parseLatex,
renderMathInText
} from "./index.js"
import {markedKatex,treeToHtml} from "./marked-katex-extension.js"
import { extendedTables } from "./marked-tables.js"
// #endif
type DATATYP = {
action : string
}
/**
* @name 富文本 xMarkdown
* @description 这是一个预览markdown的组件。当前支持markdown:表格,及数学公式的展示。
* 【小程序端请注意】从1.1.14开始支持LaTex数学公式,但要注意数学公式暂时不要混合在表格内,会造成表格断列,样式异常。
* 【组件目录内】有一个fonts.zip字体压缩包,请上传到你自己的服务器,并打开katex.min.css,把里面的字体链接换成你的字体连接,如果不换可能我服务器一关你就用不了了。
* 传递正常markdown或者html内容即可,已经支持了暗黑适配,请自行配置样式。
* 预览md:支持流式解析,支持动态解析内容.方便大家对话用.同时也支持了动态高,自动适配.
* 同时也放开了内容复制(但会导致安卓(截止sdk4.53)页面滚动不了,需要你们自己解决:给这个组件盖个view屏蔽webview的事件,这是sdk底层问题,我修复不了.)
* 1.1.10废弃了函数:getMarkdown
* 所有平台的样式已经重置为none,你需要通过属性nodeStyle来设置默认样式.
* @page /pages/zhanshi/markdown
* @category 展示组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
// #ifdef MP
const md = new Marked(markedHighlight({
emptyLangClass: 'hljs',
langPrefix: 'hljs language-',
highlight(code, lang, info) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, {
language:"css"
}).value;
}
}));
md.use(extendedTables())
// md.use(markedKatex(katex)({
// throwOnError: false
// }));
// #endif
return {
id: ("xEchart-" + getUid()) as string,
webviewContext: null as WebviewContext | null,
isLoaded: false,
boxWidth: 10,
boxHeight: 0,
tid: 0,
tid2: 0,
realLoaded: false,
isMp: false,
// #ifdef MP
markdownObj: md,
htmlMpContent: ""
// #endif
}
},
emits: [
/**
* 特定的a,img标签被点击触发,小程序不支持,其它平台支持.
* @return {Object<{text,tag,attr}>}
*/
'tagClick',
/**
* 是否初始化成功
*/
'init',
/**
* 需要通过ref函数调用getHtml才会触发此函数
*/
'getValue'
],
props: {
/**
* 窗口宽
*/
width: {
type: String,
default: 'auto'
},
/**
* 窗口高,auto时会自动适配高.
*/
height: {
type: String,
default: 'auto'
},
/**
* 需要渲染的markdow或者html内容。
*/
value: {
type: String,
default: ""
},
/**
* 是否启用纯html渲染。如果你的内容含有特殊字符比如:%,^&%这种不要出现在里面
* 此时你启用isHtml会经过数据处理直接跳过插件,直接赋值内容到html.就不要启用Markdown了.
*/
isHtml: {
type: Boolean,
default: false
},
/**
* 富文本的style样式,不可以动态更改.
* 为了对齐所有端,默认已经把所有平台的样式删除.因此你可以自己设置默认样式来对齐所有平台.
*/
nodeStyle: {
type: String,
default: "line-height:1.6;color:#000"
},
/**
* 同上,暗黑时的样式.
*/
nodeDarkStyle: {
type: String,
default: "line-height:1.6;color:#fff"
}
},
computed: {
_width() : string {
return checkIsCssUnit(this.width, xConfig.unit)
},
_height() : string {
return checkIsCssUnit(this.height, xConfig.unit)
},
_value() : string {
return this.value
},
_nodeStyle() : string {
return xConfig.dark == 'dark' ? this.nodeDarkStyle : this.nodeStyle
}
},
watch: {
value() {
// #ifdef APP||WEB
this.drawer();
// #endif
// #ifdef MP
this.setContent(this.value);
// #endif
}
},
mounted() {
let t = this;
// #ifdef MP
t.isMp = true;
// #endif
t.isLoaded = true;
this.tid = setTimeout(function () {
// #ifdef APP
t.webviewContext = uni.createWebviewContext(t.id, t);
// #endif
// #ifdef WEB
t.webviewContext = document.getElementById(t.id) as HTMLElement;
// #endif
// #ifdef MP
t.mpOnInit()
// #endif
}, 50);
this.onAddlisentMesage();
},
beforeMount() {
clearTimeout(this.tid)
},
beforeUnmount() {
},
methods: {
// #ifdef MP
mpOnInit() {
this.realLoaded = true;
this.setContent(this.value);
/**
* 图表加载初始化完成后触发此事件。
*/
this.$emit("init")
},
htmlContentAddClass(content:string){
// 匹配所有HTML标签
const regex = /<([a-zA-Z0-9]+)([^>]*?)>/g;
return content.replace(regex, (match, tag, attributes) => {
// 如果标签已经有class属性
if(attributes.includes('class=')){
// 在已有的class后面追加标签名作为类名
return match.replace(/class=["']([^"']*)["']/, (m, cls) => {
return `class="${cls} ${tag}"`;
});
}
// 如果没有class属性,则添加一个新的class属性
else {
// 在标签结束前插入class属性
return `<${tag}${attributes} class="${tag}">`;
}
});
},
setContent(str) {
if (this.isHtml) {
this.htmlMpContent = this.htmlContentAddClass(str)
} else {
let thtml = this.markdownObj.parse(this.value)
// thtml = this.htmlContentAddClass(thtml)
const nodes = renderMathInText(thtml, {
delimiters: [
{ left: "$$", right: "$$", display: true },
{ left: "$", right: "$", display: false },
],
});
// const htmlcontent = this.markdownObj.parse(treeToHtml(thtml))
this.htmlMpContent = treeToHtml(nodes)
}
},
onItemClick(event) {
console.log(event)
},
// #endif
// h5端
onAddlisentMesage() {
// #ifdef WEB
let t = this;
window.addEventListener('message', function (event) {
if (event.data.iframeId == t.id && event.data.action == 'onJSBridgeReady') {
clearTimeout(t.tid2)
t.tid2 = setTimeout(function () {
t.realLoaded = true;
t.drawer()
t.$emit("init")
t.eventJsCall('setBodyStyle', t._nodeStyle)
}, 50);
}
if (event.data.iframeId == t.id && event.data.action == 'offsetHeight') {
t.boxHeight = event.data.data + 20
} else if (event.data.iframeId == t.id && event.data.action == 'toValue') {
t.$emit('getValue', event.data.data)
} else if (event.data.iframeId == t.id && event.data.action == 'click') {
let dataStr = JSON.stringify(event.data.data);
let dataJson = JSON.parseObject(dataStr)!
t.$emit('tagClick', dataJson)
}
});
// #endif
},
onMessage(event : WebViewMessageEvent) {
let t = this;
let msgdatas = event.detail.data
if (msgdatas.length == 0) return;
// #ifdef APP-ANDROID || APP-HARMONY
if (msgdatas.length > 0) {
let dataStr = JSON.stringify(event.detail);
let dataJson = JSON.parseObject(dataStr)!
let msgeAr = dataJson.getArray<UTSJSONObject>('data')!
let msg = msgeAr[0]!
let ac = msg["action"] as string;
if (ac == 'offsetHeight') {
const h = msg["data"]! as number;
t.boxHeight = h + 25
} else if (ac == 'toValue') {
t.$emit('getValue', msg["data"]! as string)
} else if (ac == 'click') {
t.$emit('tagClick', msg['data']! as UTSJSONObject)
}
}
// #endif
// #ifdef APP-IOS
if (msgdatas.length > 0) {
let msg = msgdatas[0]
let ac = msg['action'] as string;
if (ac == 'offsetHeight') {
t.boxHeight = (msg['data']! as Number) + 25
} else if (ac == 'toValue') {
t.$emit('getValue', msg['data'])
} else if (ac == 'click') {
let dataStr = JSON.stringify(msg['data']);
let dataJson = JSON.parseObject(dataStr)!
t.$emit('tagClick', dataJson)
}
}
// #endif
},
drawer() {
if (!this.realLoaded) {
uni.showToast({ title: "未初始化完成", icon: 'none' })
return;
}
// #ifdef WEB
this.eventJsCall('markdown', JSON.stringify({ value: encodeURIComponent(this._value), render: true }))
// #endif
// #ifdef APP-IOS
this.eventJsCall('markdown', JSON.stringify({ value: encodeURIComponent(this._value), render: true }))
// #endif
// #ifdef APP-ANDROID || APP-HARMONY
this.eventJsCall('markdown', JSON.stringify({ value: this.isHtml ? btoa(encodeURIComponent(this._value)!) : this._value, render: this.isHtml ? 'android' : '' }))
// #endif
},
eventJsCall(callfun : string, str : string) {
// #ifdef WEB
var iframe = document.getElementById(this.id);
if (!iframe) return;
if ((iframe.contentWindow[callfun] || null)) {
iframe.contentWindow[callfun](str, this.isHtml);
}
// #endif
// #ifdef APP
this.webviewContext?.evalJS(`${callfun}(${str},${this.isHtml})`)
// #endif
},
/**
* 获取html内容。注意本函数不会返回内容,你要通过事件getValue得到html内容.
* @public
*/
getHtml() {
// #ifdef WEB
var iframe = document.getElementById(this.id);
if (!iframe) return;
iframe.contentWindow['getHtml']()
// #endif
// #ifdef APP
this.webviewContext?.evalJS(`getHtml()`)
// #endif
// #ifdef MP
this.$emit('getValue', this.htmlContentAddClass(this.htmlMpContent))
// #endif
},
appWebViewLoaded() {
this.realLoaded = true;
this.drawer()
/**
* 文档插件加载初始完成触发,这里响应完成后,你才要吧更新和修改数据。否则会报:未初始化。
*/
this.$emit("init")
// setBodyStyle
this.eventJsCall('setBodyStyle', this._nodeStyle)
}
}
}
</script>
<template>
<view :style="{width:_width,height:boxHeight==0||isMp?_height:(boxHeight+'px')}">
<view v-if="!isLoaded"
style="width:100%;min-height:36px;display: flex;justify-content: center;align-items: center;flex-direction: row;">
<x-icon color="primary" :spin="true" name="loader-4-line"></x-icon>
</view>
<!-- #ifdef APP||WEB -->
<web-view v-if="isLoaded" :horizontalScrollBarAccess="true" :verticalScrollBarAccess="false"
class="xMarkdownNoevents" @load="appWebViewLoaded" :id="id" src="/hybrid/html/preview.html"
:style="{width:'100%',height:'100%',opacity:isLoaded?1:0}" @message="onMessage"></web-view>
<!-- #endif -->
<!-- #ifdef MP -->
<rich-text class="richText" :style="_nodeStyle" @itemclick="onItemClick" :selectable="true"
:nodes="htmlMpContent"></rich-text>
<!-- #endif -->
<!-- 兼容安卓。webview到4.19+页面无法滚动 -->
<!-- #ifdef APP-ANDROID -->
<!-- <view class="xMarkdownAndrod"></view> -->
<!-- #endif -->
</view>
</template>
<style lang="scss">
/* #ifdef MP */
@import url(./default.min.css);
@import url(./katex.min.css);
.table,.p,.div,.ul,.li,.h1,.h2,.h3,.h4,.h5,.h6,.blockquote,.code,.pre,.ol,.em,.strong,.hr,.img{
margin:8px 0;
}
.pre{
overflow:auto;
background-color:#f3f3f3;
padding:10px;
}
.hljs{
&.language-css{
}
.hljs-number{
color:red;
}
}
.table {
width: 100%;
border-collapse: collapse;
border: 1px solid #ddd;
/* 表格隔行变色 */
.tr:nth-child(even) {
background-color: #f2f2f2;
}
}
.tr {
text-align: center;
border: 1px solid #ddd;
}
.td,
.th {
border: 1px solid #ddd;
padding: 4px 8px;
font-size:14px;
}
/* #endif */
.xMarkdownAndrod {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 10;
}
.xMarkdownNoevents {
/* pointer-events: none; */
}
</style>