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 构建失败
This commit is contained in:
2026-09-12 01:02:45 +08:00
parent 4f5893f87a
commit 3117146281
26 changed files with 2043 additions and 337 deletions
@@ -45,8 +45,9 @@ export default {
this.className
]
},
styles () {
let style = this.data.data.style
styles () {
let style = this.data?.data?.style || ''
if (this.width) {
style += `;width:${this.width}px`
@@ -61,14 +62,16 @@ export default {
this.loadImagePath()
},
methods: {
async loadImagePath () {
const parseImages = await parseImageUrl({
insert: {image: this.data.data.value},
attributes: this.data.data.attributes,
}, "editor")
async loadImagePath () {
const value = this.data?.data?.value || ''
if (!value) return
const parseImages = await parseImageUrl({
insert: {image: value},
attributes: this.data?.data?.attributes || {},
}, "editor")
this.imagePath = parseImages[0].src
},
this.imagePath = parseImages?.[0]?.src || ''
},
imagePreview () {
uni.$emit('imagePreview', this.data.data.value)
},
@@ -1,6 +1,6 @@
<template>
<view class="content" :style="color">
<template v-for="op in content">
<view class="content" :style="{ color }">
<template v-for="(op, index) in content" :key="index">
<render-text
v-if="op.type === 'paragraph'"
:data="op.data"
@@ -8,7 +8,7 @@
:style="op.style"
></render-text>
<render-image
v-else-if="op.type === 'image' && op.data.length > 0"
v-else-if="op.type === 'image' && Array.isArray(op.data) && op.data.length > 0"
:data="op.data[0]"
:className="op.class"
:style="op.style"
@@ -83,16 +83,9 @@ export default {
mounted() {
this.initImage()
},
// #ifdef VUE2
beforeDestroy() {
uni.$off('imagePreview')
},
// #endif
// #ifdef VUE3
beforeUnmount () {
uni.$off('imagePreview')
},
// #endif
beforeUnmount () {
uni.$off('imagePreview', this.imagePreview)
},
methods: {
// 初始化图片
async initImage() {
@@ -104,7 +97,8 @@ export default {
}
// 监听图片预览
uni.$on('imagePreview', this.imagePreview)
uni.$off('imagePreview', this.imagePreview)
uni.$on('imagePreview', this.imagePreview)
},
// 点击图片预览
imagePreview(src) {
@@ -1,8 +1,11 @@
<template>
<view :class="['list', data.type]">
<view class="list-item" v-for="(item, index) in data.items">
<text class="dot">{{data.type === 'ordered' ? `${index + 1}.` : '&#8226'}}</text>
<render-text :data="item.data" reset class="reset-default"></render-text>
<view class="list-item" v-for="(item, index) in (data.items || [])" :key="index">
<text class="dot">{{data.type === 'ordered' ? `${index + 1}.` : '•'}}</text>
<view class="item-content">
<render-text :data="item.data || []" reset class="reset-default"></render-text>
<render-list v-if="item.children" :data="item.children"></render-list>
</view>
</view>
</view>
</template>
@@ -2,8 +2,8 @@
<view class="video">
<video
class="v"
:src="data.attributes.src"
:poster="data.attributes.poster"
:src="data.value || data.attributes?.src || ''"
:poster="data.attributes?.poster || ''"
></video>
</view>
</template>
@@ -34,8 +34,11 @@
</view>
<view class="meta">
<view class="author">
<view class="meta">
<view class="title" v-if="data.title_html || data.title">
<u-parse :content="data.title_html || data.title"></u-parse>
</view>
<view class="author">
<cloud-image height="64rpx" width="64rpx" borderRadius="50%"
:src="userInfo.avatar_file.url"></cloud-image>
<text class="at" :style="color">{{ userInfo.nickname || '' }}</text>
@@ -49,16 +52,16 @@
<view class="" v-if="data.edit_type === 'mobile'">
<view class="" v-for="(item, index) in data.cmsLst" :key="index">
<u-parse :content="item.html" v-if="item.type === 'image' || item.type === 'video'">
</u-parse>
<view class="" v-if="item.type === 'image'"
style="width: 100wv;margin-top: 30rpx;margin-bottom: 30rpx;">
<view class="" v-for="(item, index) in (data.cmsLst || [])" :key="index">
<u-parse :content="item.html || item.text || ''" v-if="item.type === 'text' || item.type === 'title'">
</u-parse>
<view class="" v-if="item.type === 'image'"
style="width: 100vw;margin-top: 30rpx;margin-bottom: 30rpx;">
<cloud-image width="100%" height="500rpx" mode="aspectFit" :src="item.image.src">
</cloud-image>
</view>
<view class="" v-if="item.type === 'video'"
style="width: 100wv;margin-top: 30rpx;margin-bottom: 30rpx;">
style="width: 100vw;margin-top: 30rpx;margin-bottom: 30rpx;">
<!-- <cloud-image
width="100%"
height="500rpx"
@@ -220,7 +223,7 @@
</view>
</view>
<render-article-detail :content="data.content" :color="color" :content-images="data.content_images"
<render-article-detail :content="data.content" :color="temp.font_color || '#333'" :content-images="data.content_images"
:ad-config="{ adpId, watchAdUniqueType }"></render-article-detail>
</view>
@@ -527,7 +530,12 @@
const nowMonths = (date.getUTCFullYear() - 1970) * 12 + date.getUTCMonth() + 1;
console.log(nowMonths);
const audioContext = wx.createInnerAudioContext()
// #ifdef MP-WEIXIN
const audioContext = wx.createInnerAudioContext()
// #endif
// #ifndef MP-WEIXIN
const audioContext = null
// #endif
export default {
components: {
@@ -803,7 +811,7 @@
const res = await uniCloud.importObject("article_info").isSelectVote(query);
console.log(res);
if (res.code === 200) {
this.$set(this.isSelectVoteValue, vote_id, res.isSelectVote);
this.isSelectVoteValue[vote_id] = res.isSelectVote;
}
},
isExpired(voteEndTime) {
@@ -1255,34 +1263,33 @@
},
async getMusicUrl() {
console.log(this.detail_music);
if (!this.detail_music) {
return
}
const url = await uniCloud.getTempFileURL({
fileList: [this.detail_music.music_url_id]
});
const musicPath = url.fileList[0].tempFileURL
audioContext['src'] = musicPath
if (this.isPlay && audioContext['src']) {
async getMusicUrl() {
if (!audioContext || !this.detail_music) return
const musicId = typeof this.detail_music === 'string'
? this.detail_music
: (this.detail_music.music_url_id || this.detail_music.src || '')
if (!musicId) return
const url = await uniCloud.getTempFileURL({ fileList: [musicId] })
const musicPath = url.fileList?.[0]?.tempFileURL
if (!musicPath) return
audioContext.src = musicPath
if (this.isPlay) audioContext.play()
},
onPlay() {
if (!audioContext) return
if (this.isPlay) {
this.onStop()
return
}
this.isPlay = true
audioContext.play()
}
},
onPlay() {
if (this.isPlay) {
this.onStop()
return
}
this.isPlay = true
console.log(audioContext);
audioContext.play()
},
onStop() {
audioContext.pause();
audioContext.stop()
this.isPlay = false
},
},
onStop() {
if (!audioContext) return
audioContext.pause()
audioContext.stop()
this.isPlay = false
},
async onUnlockContent() {
this.$refs.detail.loadData()
@@ -2446,8 +2453,8 @@
.cms-body {
display: flex;
min-height: 60hv;
width: 100wv;
min-height: 60vh;
width: 100vw;
}
}
@@ -33,7 +33,14 @@
<!-- #endif -->
<!-- #ifndef MP-TOUTIAO -->
<!-- 音频 -->
<audio v-else-if="n.name=='audio'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
<!--
原实现用的是 <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" />