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
+65 -61
View File
@@ -310,32 +310,29 @@
sourceType: ["album", "camera"],
count: 1,
success: async (res) => {
if (cms.cmsLst[index] && cms.cmsLst[index].image.src) {
await uniCloudStorageExtCo.deleteFile({ fileList: [cms.cmsLst[index].image.src] })
}
if (res.tempFilePaths.length > 0) {
let filePath = res.tempFilePaths[0]
let { name, extname } = getFileInfo(filePath)
const uploadFileOptionsRes = await uniCloudStorageExtCo.getUploadFileOptions({
cloudPath: `cms/image/${name}_${Math.random().toString(36).substr(2,6)}`
})
await uniCloud.uploadFile({
...uploadFileOptionsRes.uploadFileOptions,
filePath: filePath,
cloudPath: uploadFileOptionsRes.cloudPath,
success: async () => {
const res2 = {
cloudPath: uploadFileOptionsRes.cloudPath,
fileID: uploadFileOptionsRes.fileID,
fileURL: uploadFileOptionsRes.fileURL
}
const selectedMedia = { src: res2.fileID, alt: name }
cms.cmsLst[index].image.src = selectedMedia.src
},
fail: (err) => {
console.log("上传失败", err)
}
if (!res.tempFilePaths || !res.tempFilePaths.length) return
const oldSrc = cms.cmsLst[index]?.image?.src || ''
try {
const filePath = res.tempFilePaths[0]
const { name } = getFileInfo(filePath)
const options = await uniCloudStorageExtCo.getUploadFileOptions({
cloudPath: `cms/image/${name}_${Math.random().toString(36).substr(2, 6)}`
})
const uploaded = await new Promise((resolve, reject) => uniCloud.uploadFile({
...options.uploadFileOptions,
filePath,
cloudPath: options.cloudPath,
success: resolve,
fail: reject
}))
cms.cmsLst[index].image = { ...cms.cmsLst[index].image, src: uploaded.fileID }
uni.setStorageSync('cms', JSON.parse(JSON.stringify(cms)))
if (oldSrc && editingId.value) {
try { await uniCloudStorageExtCo.deleteFile({ fileList: [oldSrc], article_id: editingId.value }) } catch (e) { console.warn('旧图片清理失败', e) }
}
} catch (err) {
console.error('图片替换失败', err)
uni.showToast({ title: '图片上传失败', icon: 'none' })
}
}
})
@@ -343,15 +340,25 @@
async function delItem() {
let index = selectIndex.value
if (index >= 0 && index < cms.cmsLst.length) {
if (cms.cmsLst[index].type === "image" && cms.cmsLst[index].image.src) {
await uniCloudStorageExtCo.deleteFile({ fileList: [cms.cmsLst[index].image.src] })
} else if (cms.cmsLst[index].type === "video" && (cms.cmsLst[index].video.src || cms.cmsLst[index].video.poster)) {
await uniCloudStorageExtCo.deleteFile({
fileList: [cms.cmsLst[index].video.src, cms.cmsLst[index].video.poster]
})
if (index >= 0 && index < cms.cmsLst.length) {
if (cms.cmsLst[index].type === "image" && editingId.value && cms.cmsLst[index].image?.src) {
try {
await uniCloudStorageExtCo.deleteFile({ fileList: [cms.cmsLst[index].image.src], article_id: editingId.value })
} catch (e) {
console.warn('旧图片清理失败', e)
}
} else if (cms.cmsLst[index].type === "video" && editingId.value && (cms.cmsLst[index].video?.src || cms.cmsLst[index].video?.poster)) {
try {
await uniCloudStorageExtCo.deleteFile({
fileList: [cms.cmsLst[index].video.src, cms.cmsLst[index].video.poster],
article_id: editingId.value
})
} catch (e) {
console.warn('旧视频清理失败', e)
}
}
cms.cmsLst.splice(index, 1)
uni.setStorageSync('cms', JSON.parse(JSON.stringify(cms)))
}
delPopup.value.close()
}
@@ -439,6 +446,10 @@
function to(type, index) {
if (type === 'vote') {
toVote(index)
} else if (type === 'title') {
uni.setStorageSync('selectIndex', index)
saveCms()
uni.navigateTo({ url: '/pages2/editCms/smallTitle' })
} else {
toeditText(index)
}
@@ -470,32 +481,20 @@
uni.navigateTo({ url: "/pages2/editCms/smallTitle" })
}
async function dialogClose() {
let storedCms = uni.getStorageSync("cms")
if (storedCms && storedCms.cmsLst) {
for (let i = 0; i < storedCms.cmsLst.length; i++) {
if (storedCms.cmsLst[i].type === "image" && storedCms.cmsLst[i].image && storedCms.cmsLst[i].image.src) {
await uniCloudStorageExtCo.deleteFile({ fileList: [storedCms.cmsLst[i].image.src] })
} else if (storedCms.cmsLst[i].type === "video" && storedCms.cmsLst[i].video) {
let fileList = [storedCms.cmsLst[i].video.src].filter(Boolean)
if (storedCms.cmsLst[i].video.poster) fileList.push(storedCms.cmsLst[i].video.poster)
if (fileList.length) await uniCloudStorageExtCo.deleteFile({ fileList })
}
}
}
await uni.setStorageSync("cms", { cmsLst: [], title: "", title_html: "", title_delta: {} })
// 放弃草稿只清理本地状态;媒体删除由服务端按文章引用关系处理
async function discardDraft() {
uni.setStorageSync("cms", { cmsLst: [], title: "", title_html: "", title_delta: {} })
Object.assign(cms, uni.getStorageSync("cms"))
alertDialog.value.close()
alertDialog.value?.close()
}
async function dialogClose() {
await discardDraft()
}
// 「编辑草稿」:保留本地草稿(onShow 已把内容灌进 cms),直接进入编辑
async function dialogConfirm() {
alertDialog.value.close()
}
// 「放弃草稿」:清空本地草稿并重置编辑器
async function dialogClose() {
await discardDraft()
alertDialog.value?.close()
}
async function addItem(type) {
@@ -555,11 +554,7 @@
uni.chooseVideo({
sourceType: ['album', 'camera'],
success: async (res) => {
if (cms.cmsLst[index] && cms.cmsLst[index].video.src) {
await uniCloudStorageExtCo.deleteFile({
fileList: [cms.cmsLst[index].video.src, cms.cmsLst[index].video.poster]
})
}
const oldVideo = { ...(cms.cmsLst[index]?.video || {}) }
let filePath = res.tempFilePath
let { name, extname } = getFileInfo(filePath)
let duration = res.duration
@@ -589,8 +584,17 @@
text: text,
video: { src: selectedMedia.src, poster: cover, duration: duration, html: html, delta: delta }
}
cms.cmsLst[index] = item
close()
cms.cmsLst[index] = item
uni.setStorageSync('cms', JSON.parse(JSON.stringify(cms)))
if (editingId.value && (oldVideo.src || oldVideo.poster)) {
try {
await uniCloudStorageExtCo.deleteFile({
fileList: [oldVideo.src, oldVideo.poster],
article_id: editingId.value
})
} catch (e) { console.warn('旧视频清理失败', e) }
}
close()
},
fail: (err) => {
console.log("上传失败", err)
@@ -715,7 +719,7 @@
}
.edit_item {
width: 100wv;
width: 100vw;
height: 200rpx;
display: flex;
+38 -57
View File
@@ -117,11 +117,7 @@
{ text: "三图模式", value: 3 },
])
const thumbnail = reactive([
{ src: "" },
{ src: "" },
{ src: "" },
])
const thumbnail = reactive(["", "", ""])
const pItems = ref([
[
@@ -151,35 +147,35 @@
tags.value = [res.tags]
}
if (cms.category_id) {
category_id.value = cms.category_id
for (let i = 0; i < tags.value[0].length; i++) {
if (cms.category_id && tags.value[0]) {
category_id.value = cms.category_id
for (let i = 0; i < tags.value[0].length; i++) {
if (category_id.value === tags.value[0][i].value) {
tag_index.value = i
break
}
}
} else {
category_id.value = tags.value[0][0].value
tag_index.value = 0
}
} else if (tags.value[0] && tags.value[0].length) {
category_id.value = tags.value[0][0].value
tag_index.value = 0
}
if (cms.thumbnail) {
thumbMode.value = cms.thumbnail.length
if (thumbMode.value === 0) {
thumbnail[0] = { src: "" }
thumbnail[1] = { src: "" }
thumbnail[2] = { src: "" }
thumbnail[0] = ""
thumbnail[1] = ""
thumbnail[2] = ""
thumbMode.value = 0
} else if (thumbMode.value === 1) {
thumbnail[0] = cms.thumbnail[0]
thumbnail[1] = { src: "" }
thumbnail[2] = { src: "" }
thumbnail[0] = typeof cms.thumbnail[0] === 'string' ? cms.thumbnail[0] : (cms.thumbnail[0]?.src || '')
thumbnail[1] = ""
thumbnail[2] = ""
thumbMode.value = 1
} else if (thumbMode.value === 3) {
thumbnail[0] = cms.thumbnail[0]
thumbnail[1] = cms.thumbnail[1]
thumbnail[2] = cms.thumbnail[2]
thumbnail[0] = typeof cms.thumbnail[0] === 'string' ? cms.thumbnail[0] : (cms.thumbnail[0]?.src || '')
thumbnail[1] = typeof cms.thumbnail[1] === 'string' ? cms.thumbnail[1] : (cms.thumbnail[1]?.src || '')
thumbnail[2] = typeof cms.thumbnail[2] === 'string' ? cms.thumbnail[2] : (cms.thumbnail[2]?.src || '')
thumbMode.value = 3
}
}
@@ -237,7 +233,7 @@
fileID: uploadFileOptionsRes.fileID,
fileURL: uploadFileOptionsRes.fileURL
}
thumbnail[index].src = res2.fileID
thumbnail[index] = res2.fileID
},
fail: (err) => {
console.log("上传失败", err)
@@ -296,8 +292,9 @@
thumb.push(thumbnail[i])
}
}
cms.thumbnail = thumb
cms.thumbnail = thumb.filter(Boolean)
}
uni.setStorageSync("cms", cms)
}
function back() {
@@ -310,48 +307,32 @@
}
function toPreview() {
let flag = true
let cms = uni.getStorageSync("cms")
const cms = uni.getStorageSync("cms") || { cmsLst: [] }
cms.category_id = category_id.value
cms.p_type = p_type.value
if (!cms.p_type) {
uni.showLoading({ title: `兵种不能为空` })
setTimeout(() => uni.hideLoading(), 1000)
flag = false
} else if (!cms.category_id) {
uni.showLoading({ title: `分类不能为空` })
setTimeout(() => uni.hideLoading(), 1000)
flag = false
} else {
if (thumbMode.value === 0) {
cms.thumbnail = []
} else if (thumbMode.value === 1) {
cms.thumbnail = [thumbnail[0]]
} else if (thumbMode.value === 3) {
let thumb = []
for (let i = 0; i < 3; i++) {
if (thumbnail[i]) {
thumb.push(thumbnail[i])
}
}
cms.thumbnail = thumb
}
uni.showToast({ title: '兵种不能为空', icon: 'none' })
return
}
if (!cms.category_id) {
uni.showToast({ title: '分类不能为空', icon: 'none' })
return
}
for (let i = 0; i < cms.thumbnail.length; i++) {
if (cms.thumbnail[i] === "") {
uni.showLoading({ title: `封面图不能为空` })
setTimeout(() => uni.hideLoading(), 1000)
flag = false
}
if (thumbMode.value === 0) {
cms.thumbnail = []
} else if (thumbMode.value === 1) {
cms.thumbnail = thumbnail[0] ? [thumbnail[0]] : []
} else {
cms.thumbnail = thumbnail.slice(0, 3).filter(Boolean)
}
if (thumbMode.value > 0 && cms.thumbnail.length !== thumbMode.value) {
uni.showToast({ title: '封面图不能为空', icon: 'none' })
return
}
uni.setStorageSync("cms", cms)
if (flag) {
uni.navigateTo({ url: "/pages2/editCms/preview" })
}
uni.navigateTo({ url: "/pages2/editCms/preview" })
}
return {
+8 -8
View File
@@ -98,14 +98,14 @@
editorCtx.value.getContents({
success: async (res) => {
let { html, text, delta } = res
const oldItem = cms.cmsLst[index]
cms.cmsLst[index] = {
image: cms.cmsLst[index].image,
type: cms.cmsLst[index].type,
text: text,
html: html,
delta: delta,
...oldItem,
text: text || '',
html: html || '',
delta: delta && typeof delta === 'object' ? delta : { ops: [] },
}
await uni.setStorageSync("cms", cms)
uni.setStorageSync("cms", cms)
await back()
},
fail: (err) => {
@@ -136,8 +136,8 @@
let index = uni.getStorageSync("selectIndex")
editorCtx.value.format("fontSize", formats.fontSize)
editorCtx.value.format("color", formats.color)
if (index && cms && cms.cmsLst[index]) {
let { html, text, delta } = cms.cmsLst[index]
if (Number.isInteger(Number(index)) && cms && cms.cmsLst && cms.cmsLst[index]) {
let { html, delta } = cms.cmsLst[index]
editorCtx.value.setContents({
html: html,
delta: delta,
+50 -23
View File
@@ -37,13 +37,13 @@
</view>
</view>
<view class="">
<view class="" v-for="(item, index) in cms.cmsLst" :key="index">
<u-parse
:content="item.html"
v-if="item.type === 'image' || item.type === 'video'">
</u-parse>
<view class="" v-for="(item, index) in (cms.cmsLst || [])" :key="index">
<u-parse
:content="itemHtml(item)"
v-if="item.type === 'text' || item.type === 'title'">
</u-parse>
<view class="" v-if="item.type === 'image'"
style="width: 100wv;margin-top: 30rpx;margin-bottom: 30rpx;">
style="width: 100vw;margin-top: 30rpx;margin-bottom: 30rpx;">
<cloud-image
width="100%"
height="500rpx"
@@ -52,7 +52,7 @@
</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;">
<video
style="width: 100%; height: 500rpx;"
:src="item.video.src"
@@ -166,7 +166,7 @@
<media-list
:dataType="'template'"
:pageSize="12"
:currentTemplate="updateTemplateConfig"
:currentTemplate="currentTemplate"
@update="updateTemplate"
/>
</view>
@@ -175,7 +175,7 @@
<media-list
:dataType="'music'"
:pageSize="12"
:currentTemplate="updateTemplateConfig"
:currentTemplate="currentTemplate"
@update="updateTemplate"
/>
</view>
@@ -235,8 +235,30 @@
const maskClick = ref(true)
const popFrom = ref('bottom')
const list = ref(new Array(20).fill(0))
const currentTemplate = ref("")
const saving = ref(false)
const currentTemplate = ref("")
const saving = ref(false)
function itemHtml(item) {
return item && typeof item.html === 'string' ? item.html : ''
}
function cmsLstToDelta(list) {
const ops = []
for (const item of Array.isArray(list) ? list : []) {
if (item.type === 'text' || item.type === 'title') {
const itemOps = item.delta && Array.isArray(item.delta.ops) ? item.delta.ops : (item.text ? [{ insert: item.text }] : [])
ops.push(...itemOps)
if (!ops.length || !String(ops[ops.length - 1].insert || '').endsWith('\n')) ops.push({ insert: '\n' })
} else if (item.type === 'image' && item.image?.src) {
ops.push({ insert: { image: item.image.src } }, { insert: '\n' })
} else if (item.type === 'video' && item.video?.src) {
ops.push({ insert: { video: item.video.src } }, { insert: '\n' })
} else if (item.type === 'vote' && item.vote?.voteTitle) {
ops.push({ insert: `${item.vote.voteTitle}\n` })
}
}
return { ops }
}
const tabbarText = ref([
{ text: "模板" },
@@ -272,12 +294,13 @@
return `${temp.value.icon_color};`
})
onShow(async () => {
let storedCms = uni.getStorageSync("cms")
titleContent.value = storedCms.title_html
cms.value = storedCms
await getTemp()
})
onShow(async () => {
const storedCms = uni.getStorageSync("cms") || { cmsLst: [], title_html: '' }
titleContent.value = storedCms.title_html || ''
cms.value = storedCms
currentTemplate.value = storedCms.temp_id || ''
await getTemp()
})
async function getTemp() {
if (currentTemplate.value === "not_image") {
@@ -378,10 +401,14 @@
thumbnail: storedCms.thumbnail,
p_type: storedCms.p_type,
category_id: storedCms.category_id,
temp_id: currentTemplate.value,
edit_type: "mobile",
cmsLst: storedCms.cmsLst,
excerpt: storedCms.excerpt || "",
temp_id: currentTemplate.value || storedCms.temp_id || "",
music_url_id: storedCms.music_url_id || {},
edit_type: "mobile",
cmsLst: storedCms.cmsLst || [],
content: storedCms.content && Array.isArray(storedCms.content.ops)
? storedCms.content
: cmsLstToDelta(storedCms.cmsLst),
excerpt: storedCms.excerpt || "",
article_status: articleStatus,
}
@@ -559,8 +586,8 @@
padding: 20rpx;
.cms-body {
display: flex;
min-height: 60hv;
width: 100wv;
min-height: 60vh;
width: 100vw;
}
}
+39 -2
View File
@@ -82,7 +82,9 @@
</template>
<script>
import { ref, reactive } from 'vue'
import { ref, reactive } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
export default {
setup() {
@@ -102,13 +104,43 @@
]
const editorCtx = ref(null)
const editingIndex = ref(-1)
onLoad(() => {
const index = Number(uni.getStorageSync('selectIndex'))
editingIndex.value = Number.isInteger(index) ? index : -1
})
function isColor(color) {
return color === formats.color
}
async function toSuccess() {
// 保存逻辑
const cms = uni.getStorageSync('cms')
if (!cms || !Array.isArray(cms.cmsLst) || !editorCtx.value) return
editorCtx.value.getContents({
success: async ({ html = '', text = '', delta = { ops: [] } }) => {
if (!text.trim() && !html.trim()) {
uni.showToast({ title: '小标题不能为空', icon: 'none' })
return
}
const item = {
type: 'title',
text,
html,
delta
}
if (editingIndex.value >= 0 && cms.cmsLst[editingIndex.value]?.type === 'title') {
cms.cmsLst[editingIndex.value] = { ...cms.cmsLst[editingIndex.value], ...item }
} else {
const insertAt = Math.max(0, Number(uni.getStorageSync('selectIndex')) || 0)
cms.cmsLst.splice(insertAt, 0, item)
}
uni.setStorageSync('cms', cms)
await back()
},
fail: () => uni.showToast({ title: '读取编辑内容失败', icon: 'none' })
})
}
async function back() {
@@ -127,6 +159,11 @@
// #ifdef APP-PLUS || MP-WEIXIN || H5
uni.createSelectorQuery().select('#editor').context((res) => {
editorCtx.value = res.context
const cms = uni.getStorageSync('cms')
const item = editingIndex.value >= 0 ? cms?.cmsLst?.[editingIndex.value] : null
if (item?.type === 'title') {
editorCtx.value.setContents({ html: item.html || '', delta: item.delta || { ops: [] } })
}
}).exec()
// #endif
}
+9 -2
View File
@@ -137,9 +137,16 @@
return
}
const options = voteLst.value
.filter(item => item && item.value && item.value.trim())
.map(item => ({ value: item.value.trim() }))
if (options.length < 2) {
uni.showToast({ title: '至少填写两个选项', icon: 'none' })
return
}
let vote = {
voteLst: voteLst.value,
vote_id: vote_id.value,
voteLst: options,
vote_id: vote_id.value || `vote_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`,
voteTitle: voteTitle.value,
type: array.value[select.value],
datetime: birthday.value,