构建: - 新增 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 构建失败
713 lines
17 KiB
Vue
713 lines
17 KiB
Vue
<template>
|
||
<view class="body">
|
||
<uni-nav-bar
|
||
title="预览"
|
||
leftText="取消"
|
||
left-icon="left"
|
||
right-text="发布"
|
||
backgroundColor="#ffffff"
|
||
color="#000000"
|
||
@clickRight="saveArticle(1)"
|
||
@clickLeft="back()">
|
||
</uni-nav-bar>
|
||
|
||
<!-- 存草稿不发布:草稿仅在「我的作品」中可见 -->
|
||
<view class="draft-bar">
|
||
<button class="draft-btn" :loading="saving" :disabled="saving" @click="saveArticle(0)">
|
||
存为草稿
|
||
</button>
|
||
</view>
|
||
|
||
<view class="v-preview" v-if="temp && temp.temptele_src">
|
||
<image :src="temp.temptele_src" alt="" class="img" mode="widthFix" />
|
||
</view>
|
||
|
||
<view class="" :style="backgrond_color">
|
||
<u-parse :content="titleContent"></u-parse>
|
||
|
||
<view class="">
|
||
<view class="meta">
|
||
<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>
|
||
<text class="split">·</text>
|
||
<text class="date" :style="color">刚刚</text>
|
||
</view>
|
||
</view>
|
||
<view class="">
|
||
<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: 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: 100vw;margin-top: 30rpx;margin-bottom: 30rpx;">
|
||
<video
|
||
style="width: 100%; height: 500rpx;"
|
||
:src="item.video.src"
|
||
enable-danmu
|
||
danmu-btn
|
||
controls>
|
||
</video>
|
||
</view>
|
||
|
||
<view class="" v-if="item.type === 'vote'">
|
||
<uni-card>
|
||
<view class="vote-container">
|
||
<text class="vote-title">{{ item.vote.voteTitle }}</text>
|
||
<text class="deadline">截止时间:{{ item.vote.datetime }}</text>
|
||
<view class="" v-if="item.vote.type === voteType[0]">
|
||
<u-radio-group
|
||
v-model="item.vote.selectValue"
|
||
placement="column"
|
||
@change="groupChange"
|
||
wrap
|
||
>
|
||
|
||
<u-radio
|
||
:customStyle="{marginBottom: '8px'}"
|
||
v-for="(radio, radioIndex) in item.vote.voteLst"
|
||
:key="radioIndex"
|
||
:label="radio.value"
|
||
:name="radio.value"
|
||
@change="radioChange"
|
||
style="width: 100%;"
|
||
>
|
||
</u-radio>
|
||
</u-radio-group>
|
||
</view>
|
||
|
||
<view class="" v-if="item.vote.type === voteType[1]">
|
||
<u-checkbox-group
|
||
v-model="item.vote.selectValue"
|
||
placement="column"
|
||
@change="checkboxChange"
|
||
>
|
||
<u-checkbox
|
||
:customStyle="{marginBottom: '8px'}"
|
||
v-for="(checkbox, checkboxIndex) in item.vote.voteLst"
|
||
:key="checkboxIndex"
|
||
:label="checkbox.value"
|
||
:name="checkbox.value"
|
||
>
|
||
</u-checkbox>
|
||
</u-checkbox-group>
|
||
</view>
|
||
|
||
<view class="" v-if="item.vote.type === voteType[2]">
|
||
<u-checkbox-group
|
||
v-model="item.vote.selectValue"
|
||
placement="column"
|
||
@change="checkboxChange"
|
||
|
||
>
|
||
<view class="">
|
||
<u-checkbox
|
||
:customStyle="{marginBottom: '8px'}"
|
||
v-for="(checkbox, checkboxIndex) in item.vote.voteLst"
|
||
:key="checkboxIndex"
|
||
:label="checkbox.value"
|
||
:name="checkbox.value"
|
||
:disabled="isCheckboxDisabled(item.vote.selectValue, checkbox.value)"
|
||
>
|
||
</u-checkbox>
|
||
</view>
|
||
|
||
</u-checkbox-group>
|
||
</view>
|
||
<view class=""
|
||
v-if="isExpired"
|
||
:disabled="isExpired || !selectedValue"
|
||
style="border-top: 3rpx solid #ccc;padding-top: 20rpx;margin-top: 20rpx;
|
||
text-align: center;;">
|
||
已截止
|
||
</view>
|
||
|
||
<view class=""
|
||
v-else
|
||
:disabled="isExpired || !selectedValue"
|
||
style="border-top: 3rpx solid #ccc;padding-top: 20rpx;margin-top: 20rpx;
|
||
text-align: center;;">
|
||
{{ isExpired ? '已截止' : '点击登录后投票' }}
|
||
</view>
|
||
</view>
|
||
</uni-card>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<u-tabbar :value="tabbarValue"
|
||
>
|
||
<u-tabbar-item
|
||
v-for="(item, index) in tabbarText"
|
||
:key="index"
|
||
@click="tabbarClick"
|
||
:text="item.text">
|
||
</u-tabbar-item>
|
||
</u-tabbar>
|
||
|
||
<hqs-popup :title="tabbarText[tabbarValue].text" :from="popFrom" :mask-click="maskClick"
|
||
@back="onBack"
|
||
v-model="showPop">
|
||
<view class="page-container" v-if="tabbarText[tabbarValue].text === '模板'">
|
||
<media-list
|
||
:dataType="'template'"
|
||
:pageSize="12"
|
||
:currentTemplate="currentTemplate"
|
||
@update="updateTemplate"
|
||
/>
|
||
</view>
|
||
|
||
<view class="page-container" v-if="tabbarText[tabbarValue].text === '音乐'">
|
||
<media-list
|
||
:dataType="'music'"
|
||
:pageSize="12"
|
||
:currentTemplate="currentTemplate"
|
||
@update="updateTemplate"
|
||
/>
|
||
</view>
|
||
|
||
<view class="page-container" v-if="tabbarText[tabbarValue].text === '排版'">
|
||
</view>
|
||
</hqs-popup>
|
||
|
||
<uni-grid :column="3" :highlight="true" @change="change" :show-border="false">
|
||
<uni-grid-item v-for="(item, index) in gridLst" :index="index" :key="index">
|
||
<view class="grid-item-box">
|
||
<uni-icons :type="item.icon" :size="30"
|
||
:color="item.select_flag?select_icon_color:icon_color" />
|
||
<text class="text" :style="item.select_flag?select_font_color:icon_font_color">
|
||
{{item.value?item.value:0}}
|
||
</text>
|
||
</view>
|
||
</uni-grid-item>
|
||
</uni-grid>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { ref, computed } from 'vue'
|
||
import { onShow } from '@dcloudio/uni-app'
|
||
import { store } from '@/pages3/uni_modules/uni-id-pages/common/store.js'
|
||
import uRadioGroup from "@/uni_modules/uview-ui/components/u-radio-group/u-radio-group.vue"
|
||
import uRadio from "@/uni_modules/uview-ui/components/u-radio/u-radio.vue"
|
||
import uParse from "@/uni_modules/uview-ui/components/u-parse/u-parse.vue"
|
||
import uTabbar from "@/uni_modules/uview-ui/components/u-tabbar/u-tabbar.vue"
|
||
import uTabbarItem from "@/uni_modules/uview-ui/components/u-tabbar-item/u-tabbar-item.vue"
|
||
import mediaList from "@/components/image-grid/media-list.vue"
|
||
import cloudImage from "@/components/cloud-image.vue"
|
||
import uCheckboxGroup from "@/uni_modules/uview-ui/components/u-checkbox-group/u-checkbox-group.vue"
|
||
import uCheckbox from "@/uni_modules/uview-ui/components/u-checkbox/u-checkbox.vue"
|
||
|
||
export default {
|
||
components: {
|
||
uTabbar,
|
||
uTabbarItem,
|
||
mediaList,
|
||
uParse,
|
||
cloudImage,
|
||
uRadioGroup,
|
||
uRadio,
|
||
uCheckboxGroup,
|
||
uCheckbox
|
||
},
|
||
setup() {
|
||
const voteType = ref(["单选(默认)", "多选,无限制", "多选, 最多两项"])
|
||
const voteEndTime = ref('2025-04-20 23:59:59')
|
||
const cms = ref({})
|
||
const temp = ref({})
|
||
const titleContent = ref("")
|
||
const tabbarValue = ref(0)
|
||
const showPop = ref(false)
|
||
const maskClick = ref(true)
|
||
const popFrom = ref('bottom')
|
||
const list = ref(new Array(20).fill(0))
|
||
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: "模板" },
|
||
{ text: "音乐" },
|
||
{ text: "排版" }
|
||
])
|
||
|
||
const userInfo = computed(() => store.userInfo)
|
||
const isExpired = computed(() => new Date() > new Date(voteEndTime.value))
|
||
|
||
const btn_send_commends_color = computed(() => {
|
||
if (!temp.value) return ""
|
||
return `background-color: ${temp.value.btn_send_commends_color};`
|
||
})
|
||
|
||
const backgrond_color = computed(() => {
|
||
if (!temp.value) return ""
|
||
return `background-color: ${temp.value.backgrond_color};`
|
||
})
|
||
|
||
const color = computed(() => {
|
||
if (!temp.value) return ""
|
||
return `color: ${temp.value.font_color};`
|
||
})
|
||
|
||
const select_icon_color = computed(() => {
|
||
if (!temp.value) return ""
|
||
return `${temp.value.select_icon_color};`
|
||
})
|
||
|
||
const icon_color = computed(() => {
|
||
if (!temp.value) return ""
|
||
return `${temp.value.icon_color};`
|
||
})
|
||
|
||
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") {
|
||
temp.value = {}
|
||
}
|
||
if (!currentTemplate.value) {
|
||
temp.value = {}
|
||
return
|
||
}
|
||
const res = await uniCloud.importObject("uni-cms-articles").getTemp({
|
||
temp_id: currentTemplate.value
|
||
})
|
||
if (res.code === 200) {
|
||
temp.value = res.data
|
||
}
|
||
}
|
||
|
||
async function updateTemplate(e) {
|
||
currentTemplate.value = e
|
||
await getTemp()
|
||
}
|
||
|
||
function onBack() {
|
||
uni.showToast({ title: 'test' })
|
||
}
|
||
|
||
function tabbarClick(e) {
|
||
tabbarValue.value = e
|
||
showPopupFrom()
|
||
}
|
||
|
||
function showPopupFrom() {
|
||
showPop.value = true
|
||
}
|
||
|
||
function isCheckboxDisabled(arr, name) {
|
||
if (arr.length < 2 || arr.includes(name)) {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
function checkboxChange(e) {
|
||
console.log(e)
|
||
}
|
||
|
||
function groupChange(n) {
|
||
console.log('groupChange', n)
|
||
}
|
||
|
||
function radioChange(n) {
|
||
console.log('radioChange', n)
|
||
}
|
||
|
||
function formatDate(date, format) {
|
||
const map = {
|
||
'yyyy': date.getFullYear(),
|
||
'MM': String(date.getMonth() + 1).padStart(2, '0'),
|
||
'dd': String(date.getDate()).padStart(2, '0'),
|
||
'HH': String(date.getHours()).padStart(2, '0'),
|
||
'mm': String(date.getMinutes()).padStart(2, '0'),
|
||
'ss': String(date.getSeconds()).padStart(2, '0')
|
||
}
|
||
return format.replace(/yyyy|MM|dd|HH|mm|ss/g, matched => map[matched])
|
||
}
|
||
|
||
function formatTime() {
|
||
return formatDate(new Date(), 'yyyy-MM-dd HH:mm')
|
||
}
|
||
|
||
/**
|
||
* 保存文章
|
||
* @param {Number} articleStatus 1 发布,0 存草稿
|
||
*/
|
||
async function saveArticle(articleStatus) {
|
||
if (saving.value) return
|
||
|
||
const storedCms = uni.getStorageSync("cms")
|
||
if (!storedCms) {
|
||
return uni.showToast({ title: '内容已丢失,请重新编辑', icon: 'none' })
|
||
}
|
||
if (!storedCms.title || !storedCms.title.trim()) {
|
||
return uni.showToast({ title: '请先填写标题', icon: 'none' })
|
||
}
|
||
// 草稿允许只有标题,发布必须有正文
|
||
const hasContent = Array.isArray(storedCms.cmsLst) && storedCms.cmsLst.length > 0
|
||
if (articleStatus === 1 && !hasContent) {
|
||
return uni.showToast({ title: '正文不能为空', icon: 'none' })
|
||
}
|
||
|
||
saving.value = true
|
||
try {
|
||
// 归属与 IP 由服务端从登录态与请求上下文获取,这里不再传 user_id
|
||
const query = {
|
||
title: storedCms.title,
|
||
title_html: storedCms.title_html,
|
||
title_delta: storedCms.title_delta,
|
||
thumbnail: storedCms.thumbnail,
|
||
p_type: storedCms.p_type,
|
||
category_id: storedCms.category_id,
|
||
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,
|
||
}
|
||
|
||
let res
|
||
if (storedCms._id) {
|
||
res = await uniCloud.importObject("uni-cms-articles")
|
||
.update_cms_articles({ ...query, id: storedCms._id })
|
||
} else {
|
||
res = await uniCloud.importObject("uni-cms-articles").add_cms_articles(query)
|
||
}
|
||
|
||
if (!res || res.code !== 200) {
|
||
throw new Error((res && res.msg) || '保存失败')
|
||
}
|
||
|
||
uni.setStorageSync("cms", "")
|
||
uni.showToast({
|
||
title: articleStatus === 1 ? '发布成功' : '已存为草稿',
|
||
icon: 'none'
|
||
})
|
||
setTimeout(() => {
|
||
uni.switchTab({ url: "/pages/my/my" })
|
||
}, 800)
|
||
} catch (e) {
|
||
console.error('保存文章失败:', e)
|
||
uni.showToast({ title: e.message || '保存失败,请重试', icon: 'none' })
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
async function back() {
|
||
uni.navigateBack({
|
||
delta: 1,
|
||
fail: (err) => {
|
||
uni.navigateTo({ url: "/pages2/editCms/editCms" })
|
||
}
|
||
})
|
||
}
|
||
|
||
return {
|
||
voteType,
|
||
voteEndTime,
|
||
cms,
|
||
temp,
|
||
titleContent,
|
||
tabbarValue,
|
||
showPop,
|
||
maskClick,
|
||
popFrom,
|
||
list,
|
||
currentTemplate,
|
||
tabbarText,
|
||
userInfo,
|
||
isExpired,
|
||
btn_send_commends_color,
|
||
backgrond_color,
|
||
color,
|
||
select_icon_color,
|
||
icon_color,
|
||
getTemp,
|
||
updateTemplate,
|
||
onBack,
|
||
tabbarClick,
|
||
showPopupFrom,
|
||
isCheckboxDisabled,
|
||
checkboxChange,
|
||
groupChange,
|
||
radioChange,
|
||
formatDate,
|
||
formatTime,
|
||
saving,
|
||
saveArticle,
|
||
back,
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
|
||
.draft-bar {
|
||
padding: 16rpx 20rpx;
|
||
background-color: #fff;
|
||
border-bottom: 1rpx solid #eee;
|
||
}
|
||
|
||
.draft-btn {
|
||
font-size: 28rpx;
|
||
line-height: 72rpx;
|
||
height: 72rpx;
|
||
color: #c11c1f;
|
||
background-color: #fff;
|
||
border: 1rpx solid #c11c1f;
|
||
border-radius: 36rpx;
|
||
|
||
&::after {
|
||
border: none;
|
||
}
|
||
}
|
||
|
||
.grid-container {
|
||
display: flex;
|
||
gap: 10px;
|
||
padding: 15px;
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.grid-item {
|
||
flex: 1;
|
||
min-width: 0;
|
||
aspect-ratio: 1;
|
||
background: white;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.page-container {
|
||
padding: 20rpx 0;
|
||
}
|
||
|
||
.rectangle-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
aspect-ratio: 16/9;
|
||
}
|
||
|
||
.meta {
|
||
|
||
position: relative;
|
||
z-index: 1;
|
||
padding-top: 20rpx;
|
||
|
||
.title {
|
||
.text {
|
||
font-size: 40rpx;
|
||
line-height: 66rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
}
|
||
|
||
.excerpt {
|
||
margin-top: 10rpx;
|
||
|
||
.text {
|
||
font-size: 26rpx;
|
||
line-height: 40rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
.author {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
flex-direction: row;
|
||
margin-top: 20rpx;
|
||
|
||
.at,
|
||
.split,
|
||
.date {
|
||
font-size: 26rpx;
|
||
color: #ccc;
|
||
}
|
||
|
||
.split {
|
||
margin: 0 10rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.body {
|
||
padding: 20rpx;
|
||
.cms-body {
|
||
display: flex;
|
||
min-height: 60vh;
|
||
width: 100vw;
|
||
}
|
||
}
|
||
|
||
.v-preview {
|
||
.img {
|
||
width: 100%;
|
||
}
|
||
|
||
.btn-area {
|
||
position: relative;
|
||
top: -50rpx;
|
||
}
|
||
|
||
.red-btn-area {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 10rpx 0;
|
||
margin: 0 40rpx 20rpx 40rpx;
|
||
font-size: 20rpx;
|
||
|
||
.red-btn {
|
||
position: relative;
|
||
color: #fff;
|
||
font-weight: 700;
|
||
font-size: 24rpx;
|
||
height: 40rpx;
|
||
line-height: 40rpx;
|
||
margin: 0;
|
||
background-image: linear-gradient(to bottom, #c11c1f, #5c0201);
|
||
|
||
+.red-btn {
|
||
margin-left: 20rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.red-btn-plain-area {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 10rpx 0;
|
||
margin: 0 40rpx;
|
||
font-size: 21rpx;
|
||
border-top: 2px solid;
|
||
border-bottom: 2px solid;
|
||
border-image: linear-gradient(to right,
|
||
#fff,
|
||
#f44336bf,
|
||
#b13636,
|
||
#f44336bf,
|
||
#fff) 1;
|
||
|
||
.red-btn-plain {
|
||
position: relative;
|
||
color: #b13636;
|
||
font-weight: 700;
|
||
padding-left: 15rpx;
|
||
|
||
+.red-btn-plain {
|
||
margin-left: 10rpx;
|
||
|
||
&::before {
|
||
content: "";
|
||
position: absolute;
|
||
left: 0;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 2px;
|
||
height: 20rpx;
|
||
background-color: #b13636;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
.vote-container {
|
||
padding: 20rpx;
|
||
}
|
||
|
||
.vote-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
display: block;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
.deadline {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-bottom: 30rpx;
|
||
display: block;
|
||
}
|
||
|
||
.radio-group {
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.option-item {
|
||
display: flex;
|
||
align-items: center;
|
||
margin: 15rpx 0;
|
||
}
|
||
|
||
.option-label {
|
||
margin-left: 20rpx;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.vote-btn {
|
||
background-color: #2979FF;
|
||
color: white;
|
||
border-radius: 8rpx;
|
||
font-size: 14px;
|
||
margin-top: 30rpx;
|
||
|
||
&[disabled] {
|
||
background-color: #CCCCCC;
|
||
}
|
||
}
|
||
</style>
|