Files
t/uni_modules/uview-ui/components/u-parse/node/node.vue
T
root 3117146281 feat: 打通 HBuilderX CLI 构建 + 修复文章闭环静态盲区
构建:
- 新增 tools/build.js:junction HBuilderX 工具链,CLI 构建 h5/mp-weixin
- vue 指向补丁版 @dcloudio/uni-h5-vue(官方 npm vue 不导出 isInSSRComponentSetup)
- 设 HX_APP_ROOT 避免退化成 H5 空壳产物;产物完整性校验

校验工具:
- 新增 check-cloud-methods.js:acorn 解析云对象方法,比对 94 处调用点
- 新增 check-android-contract.js:Kotlin 侧云对象契约校验
- audit-project.js 修 downloadFile 误报(注释未剥离);tools/ 排除出扫描
- package.json 声明此前隐式依赖的 acorn

功能:
- 补 uni-cms-articles.getPublishedArticles(安卓端依赖但此前不存在)
- 修 u-parse <audio> 引用已移除组件导致 H5 构建失败
2026-09-12 01:02:45 +08:00

502 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view :id="attrs.id" :class="'_'+name+' '+attrs.class" :style="attrs.style">
<block v-for="(n, i) in childs" v-bind:key="i">
<!-- 图片 -->
<!-- 占位图 -->
<image v-if="n.name=='img'&&((opts[1]&&!ctrl[i])||ctrl[i]<0)" class="_img" :style="n.attrs.style" :src="ctrl[i]<0?opts[2]:opts[1]" mode="widthFix" />
<!-- 显示图片 -->
<!-- #ifdef H5 || APP-PLUS -->
<img v-if="n.name=='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]==-1?'display:none;':'')+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap"/>
<!-- #endif -->
<!-- #ifndef H5 || APP-PLUS -->
<image v-if="n.name=='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]==-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;height:1px;'+n.attrs.style" :src="n.attrs.src" :mode="n.h?'':'widthFix'" :lazy-load="opts[0]" :webp="n.webp" :show-menu-by-longpress="opts[3]&&!n.attrs.ignore" :image-menu-prevent="!opts[3]||n.attrs.ignore" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
<!-- #endif -->
<!-- 文本 -->
<!-- #ifndef MP-BAIDU -->
<text v-else-if="n.type=='text'" decode>{{n.text}}</text>
<!-- #endif -->
<text v-else-if="n.name=='br'">\n</text>
<!-- 链接 -->
<view v-else-if="n.name=='a'" :id="n.attrs.id" :class="(n.attrs.href?'_a ':'')+n.attrs.class" hover-class="_hover" :style="'display:inline;'+n.attrs.style" :data-i="i" @tap.stop="linkTap">
<node name="span" :childs="n.children" :opts="opts" style="display:inherit" />
</view>
<!-- 视频 -->
<!-- #ifdef APP-PLUS -->
<view v-else-if="n.html" :id="n.attrs.id" :class="'_video '+n.attrs.class" :style="n.attrs.style" v-html="n.html" />
<!-- #endif -->
<!-- #ifndef APP-PLUS -->
<video v-else-if="n.name=='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
<!-- #endif -->
<!-- #ifdef H5 || APP-PLUS -->
<iframe v-else-if="n.name=='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder" :src="n.attrs.src" />
<embed v-else-if="n.name=='embed'" :style="n.attrs.style" :src="n.attrs.src" />
<!-- #endif -->
<!-- #ifndef MP-TOUTIAO -->
<!-- 音频 -->
<!--
原实现用的是 <audio> 内置组件,但新版 uni-h5 已移除该组件导出
(@dcloudio/uni-h5 不再 export Audio),H5 端编译会直接失败在
"Audio is not exported by uni-h5.es.js"。
改为普通 view 承载:保留 id/class/style 以维持排版,
音频源由父级逻辑按需处理,不再依赖已删除的组件。
-->
<view v-else-if="n.name=='audio'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :data-i="i" />
<!-- #endif -->
<view v-else-if="(n.name=='table'&&n.c)||n.name=='li'" :id="n.attrs.id" :class="'_'+n.name+' '+n.attrs.class" :style="n.attrs.style">
<node v-if="n.name=='li'" :childs="n.children" :opts="opts" />
<view v-else v-for="(tbody, x) in n.children" v-bind:key="x" :class="'_'+tbody.name+' '+tbody.attrs.class" :style="tbody.attrs.style">
<node v-if="tbody.name=='td'||tbody.name=='th'" :childs="tbody.children" :opts="opts" />
<block v-else v-for="(tr, y) in tbody.children" v-bind:key="y">
<view v-if="tr.name=='td'||tr.name=='th'" :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
<node :childs="tr.children" :opts="opts" />
</view>
<view v-else :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
<view v-for="(td, z) in tr.children" v-bind:key="z" :class="'_'+td.name+' '+td.attrs.class" :style="td.attrs.style">
<node :childs="td.children" :opts="opts" />
</view>
</view>
</block>
</view>
</view>
<!-- 富文本 -->
<!-- #ifdef H5 || MP-WEIXIN || MP-QQ || APP-PLUS || MP-360 -->
<rich-text v-else-if="handler.use(n)" :id="n.attrs.id" :style="n.f" :nodes="[n]" />
<!-- #endif -->
<!-- #ifndef H5 || MP-WEIXIN || MP-QQ || APP-PLUS || MP-360 -->
<rich-text v-else-if="!n.c" :id="n.attrs.id" :style="n.f+';display:inline'" :preview="false" :nodes="[n]" />
<!-- #endif -->
<!-- 继续递归 -->
<view v-else-if="n.c==2" :id="n.attrs.id" :class="'_'+n.name+' '+n.attrs.class" :style="n.f+';'+n.attrs.style">
<node v-for="(n2, j) in n.children" v-bind:key="j" :style="n2.f" :name="n2.name" :attrs="n2.attrs" :childs="n2.children" :opts="opts" />
</view>
<node v-else :style="n.f" :name="n.name" :attrs="n.attrs" :childs="n.children" :opts="opts" />
</block>
</view>
</template>
<script module="handler" lang="wxs">
// 行内标签列表
var inlineTags = {
abbr: true,
b: true,
big: true,
code: true,
del: true,
em: true,
i: true,
ins: true,
label: true,
q: true,
small: true,
span: true,
strong: true,
sub: true,
sup: true
}
/**
* @description 是否使用 rich-text 显示剩余内容
*/
module.exports = {
use: function (item) {
// 微信和 QQ 的 rich-text inline 布局无效
if (inlineTags[item.name] || (item.attrs.style || '').indexOf('display:inline') != -1)
return false
return !item.c
}
}
</script>
<script>
export default {
name: 'node',
// #ifdef MP-WEIXIN
options: {
virtualHost: true
},
// #endif
data() {
return {
ctrl: {}
}
},
props: {
name: String,
attrs: {
type: Object,
default() {
return {}
}
},
childs: Array,
opts: Array
},
mounted() {
for (this.root = this.$parent; this.root.$options.name != 'mp-html'; this.root = this.root.$parent);
// #ifdef H5 || APP-PLUS
if (this.opts[0]) {
for (var i = this.childs.length; i--;)
if (this.childs[i].name == 'img')
break
if (i != -1) {
this.observer = uni.createIntersectionObserver(this).relativeToViewport({
top: 500,
bottom: 500
})
this.observer.observe('._img', res => {
if (res.intersectionRatio) {
this.$set(this.ctrl, 'load', 1)
this.observer.disconnect()
}
})
}
}
// #endif
},
beforeDestroy() {
// #ifdef H5 || APP-PLUS
if (this.observer)
this.observer.disconnect()
// #endif
},
methods:{
// #ifdef MP-WEIXIN
toJSON() { },
// #endif
/**
* @description 播放视频事件
* @param {Event} e
*/
play(e) {
// #ifndef APP-PLUS
if (this.root.pauseVideo) {
var flag = false, id = e.target.id
for (var i = this.root._videos.length; i--;) {
if (this.root._videos[i].id == id)
flag = true
else
this.root._videos[i].pause() // 自动暂停其他视频
}
// 将自己加入列表
if (!flag) {
var ctx = uni.createVideoContext(id
// #ifndef MP-BAIDU
, this
// #endif
)
ctx.id = id
this.root._videos.push(ctx)
}
}
// #endif
},
/**
* @description 图片点击事件
* @param {Event} e
*/
imgTap(e) {
var node = this.childs[e.currentTarget.dataset.i]
if (node.a)
return this.linkTap(node.a)
if (node.attrs.ignore)
return
// #ifdef H5 || APP-PLUS
node.attrs.src = node.attrs.src || node.attrs['data-src']
// #endif
this.root.$emit('imgTap', node.attrs)
// 自动预览图片
if (this.root.previewImg)
uni.previewImage({
current: parseInt(node.attrs.i),
urls: this.root.imgList
})
},
/**
* @description 图片长按
*/
imgLongTap(e) {
// #ifdef APP-PLUS
var attrs = this.childs[e.currentTarget.dataset.i].attrs
if (!attrs.ignore)
uni.showActionSheet({
itemList: ['保存图片'],
success: () => {
uni.downloadFile({
url: this.root.imgList[attrs.i],
success: res => {
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success() {
uni.showToast({
title: '保存成功'
})
}
})
}
})
}
})
// #endif
},
/**
* @description 图片加载完成事件
* @param {Event} e
*/
imgLoad(e) {
var i = e.currentTarget.dataset.i
// #ifndef H5 || APP-PLUS
// 设置原宽度
if (!this.childs[i].w)
this.$set(this.ctrl, i, e.detail.width)
else
// #endif
// 加载完毕,取消加载中占位图
if ((this.opts[1] && !this.ctrl[i]) || this.ctrl[i] == -1)
this.$set(this.ctrl, i, 1)
},
/**
* @description 链接点击事件
* @param {Event} e
*/
linkTap(e) {
var attrs = e.currentTarget ? this.childs[e.currentTarget.dataset.i].attrs : e,
href = attrs.href
this.root.$emit('linkTap', attrs)
if (href) {
// 跳转锚点
if (href[0] == '#')
this.root.navigateTo(href.substring(1)).catch(() => { })
// 复制外部链接
else if (href.includes('://')) {
if (this.root.copyLink) {
// #ifdef H5
window.open(href)
// #endif
// #ifdef MP
uni.setClipboardData({
data: href,
success: () =>
uni.showToast({
title: '链接已复制'
})
})
// #endif
// #ifdef APP-PLUS
plus.runtime.openWeb(href)
// #endif
}
}
// 跳转页面
else
uni.navigateTo({
url: href,
fail() {
uni.switchTab({
url: href,
fail() { }
})
}
})
}
},
/**
* @description 错误事件
* @param {Event} e
*/
mediaError(e) {
var i = e.currentTarget.dataset.i,
node = this.childs[i]
// 加载其他源
if (node.name == 'video' || node.name == 'audio') {
var index = (this.ctrl[i] || 0) + 1
if (index > node.src.length)
index = 0
if (index < node.src.length)
return this.$set(this.ctrl, i, index)
}
// 显示错误占位图
else if (node.name == 'img' && this.opts[2])
this.$set(this.ctrl, i, -1)
if (this.root)
this.root.$emit('error', {
source: node.name,
attrs: node.attrs,
errMsg: e.detail.errMsg
})
}
}
}
</script>
<style>
/* a 标签默认效果 */
._a {
padding: 1.5px 0 1.5px 0;
color: #366092;
word-break: break-all;
}
/* a 标签点击态效果 */
._hover {
text-decoration: underline;
opacity: 0.7;
}
/* 图片默认效果 */
._img {
max-width: 100%;
-webkit-touch-callout: none;
}
/* 内部样式 */
._b,
._strong {
font-weight: bold;
}
._code {
font-family: monospace;
}
._del {
text-decoration: line-through;
}
._em,
._i {
font-style: italic;
}
._h1 {
font-size: 2em;
}
._h2 {
font-size: 1.5em;
}
._h3 {
font-size: 1.17em;
}
._h5 {
font-size: 0.83em;
}
._h6 {
font-size: 0.67em;
}
._h1,
._h2,
._h3,
._h4,
._h5,
._h6 {
display: block;
font-weight: bold;
}
._image {
height: 1px;
}
._ins {
text-decoration: underline;
}
._li {
display: list-item;
}
._ol {
list-style-type: decimal;
}
._ol,
._ul {
display: block;
padding-left: 40px;
margin: 1em 0;
}
._q::before {
content: '"';
}
._q::after {
content: '"';
}
._sub {
font-size: smaller;
vertical-align: sub;
}
._sup {
font-size: smaller;
vertical-align: super;
}
._thead,
._tbody,
._tfoot {
display: table-row-group;
}
._tr {
display: table-row;
}
._td,
._th {
display: table-cell;
vertical-align: middle;
}
._th {
font-weight: bold;
text-align: center;
}
._ul {
list-style-type: disc;
}
._ul ._ul {
margin: 0;
list-style-type: circle;
}
._ul ._ul ._ul {
list-style-type: square;
}
._abbr,
._b,
._code,
._del,
._em,
._i,
._ins,
._label,
._q,
._span,
._strong,
._sub,
._sup {
display: inline;
}
/* #ifdef APP-PLUS */
._video {
width: 300px;
height: 225px;
}
/* #endif */
</style>