Postdrama页面的内容 <template> <view :class="['app-con...
创建于:2026年5月6日
使用 GPT-5.5 Thinking 回答 Chat01
创建于:2026年5月6日
使用 GPT-5.5 Thinking 回答 Chat01
Postdrama页面的内容 <template>
<view :class="['app-container', theme]">
<view class="pc-root">
<!-- 顶部 Header -->
<view class="pc-header" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="pc-header-inner" :style="{ height: navBarHeight + 'px' }">
<view class="pc-left" @tap="navigateBack">
<uni-icons style="margin-left: 20rpx;" type="left" color="#000000" size="26" />
</view>
</template> <script setup> import { Getcontacts, Gettopics } from '@/api/api.js' import { ref, computed, onMounted, onUnmounted, inject, watch } from 'vue' import { storeToRefs } from 'pinia' import { useMainStore } from '@/store/index.js' import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' import TopicList from '@/components/TopicList.vue' import FriendList from '@/components/FriendList.vue' import ajax from '@/api/ajax.js' /** ====================== * 主题 ====================== */ const store = useMainStore() const { theme } = storeToRefs(store) /** ====================== * 顶部安全区与标题栏 ====================== */ const sys = uni.getSystemInfoSync() const statusBarHeight = ref(Number(getStatusBarHeight?.() || sys?.statusBarHeight || 0)) const navBarHeight = ref(Number(getTitleBarHeight?.() || 44)) try { const menu = uni.getMenuButtonBoundingClientRect?.() if (menu?.height) { const gap = Number(menu.top || 0) - Number(statusBarHeight.value || 0) navBarHeight.value = Number(menu.height || 44) + gap * 2 } } catch (e) {} const headerTotalPx = computed(() => { return Number(statusBarHeight.value || 0) + Number(navBarHeight.value || 0) }) /** ====================== * 文本输入 ====================== */ const textAreaContent = ref('') const textAreaContentLength = computed(() => textAreaContent.value.length) const onTextInput = () => { if (textAreaContent.value.length > 500) { textAreaContent.value = textAreaContent.value.slice(0, 500) uni.showToast({ title: '文字长度不能超过500字', icon: 'none' }) } } /** ====================== * 话题 / 朋友 ====================== */ const showTopics = ref(false) const showFriends = ref(false) const topics = ref([ { id: '222', name: '您的牌打的也太好了' }, { id: '22', name: '我喜欢伊莱克斯' }, { id: '432', name: '唐舞桐' } ]) const friends = ref([ { id: '22344343', name: '张佳琪', img: 'https://cdn1.suno.ai/image_large_07280557-0857-4a61-9e38-49a866940eb2.png' }, { id: '2233', name: '唐舞桐', img: 'https://cdn1.suno.ai/image_large_07280557-0857-4a61-9e38-49a866940eb2.png' } ]) const selectedTopicIds = ref([]) const FriendID = ref('') const toggleTopics = () => { if (!textAreaContent.value.endsWith('#')) textAreaContent.value += '#' showTopics.value = true showFriends.value = false } const toggleFriends = () => { showFriends.value = true showTopics.value = false } const handleSelectTopic = (topic) => { const topicName = topic?.name || '' if (topicName) textAreaContent.value += topicName showTopics.value = false if (topic?.id != null) { const id = String(topic.id) if (!selectedTopicIds.value.includes(id)) { selectedTopicIds.value.push(id) } } } const handleSelectFriend = (friend) => { const displayName = friend?.nickname || friend?.name || '' if (displayName) textAreaContent.value += '@' + displayName showFriends.value = false FriendID.value = friend?.id || '' } /** ====================== * 获取朋友 / 话题 ====================== */ const consumerId = ref(uni.getStorageSync('consumerId') || '') const option = ref({ consumerId: consumerId.value, keyword: '', rowStartIdx: 0, rowCount: '20' }) async function triggerfunction() { try { const res = await Getcontacts(option.value) if (res?.result === 'success') { friends.value = res.friendArray || [] } else { friends.value = res?.friendArray || [] } } catch (e) { uni.showToast({ title: '好友列表获取失败', icon: 'none' }) } } const params = { keyword: '', rowStartIdx: 0, rowCount: '20' } const TopicDetails = async () => { try { const res = await Gettopics(params) topics.value = res.tagArray || [] } catch (e) { uni.showToast({ title: '话题获取失败', icon: 'none' }) } } onMounted(() => { triggerfunction() TopicDetails() }) /** ====================== * 定位 ====================== */ const selectedLocation = ref({ name: '', address: '', latitude: '', longitude: '' }) const hasLocation = computed(() => { return !!selectedLocation.value.latitude && !!selectedLocation.value.longitude }) const mapLatitude = computed(() => Number(selectedLocation.value.latitude || 0)) const mapLongitude = computed(() => Number(selectedLocation.value.longitude || 0)) const markers = computed(() => { if (!hasLocation.value) return [] return [{ id: 1, latitude: Number(selectedLocation.value.latitude), longitude: Number(selectedLocation.value.longitude), width: 32, height: 32 }] }) function openLocation() { uni.navigateTo({ url: '/pages/sociAlize/sharedspace/sharedspace', success: (navRes) => { navRes.eventChannel?.on('sendLocation', (loc) => { selectedLocation.value = { name: loc?.name || '', address: loc?.address || loc?.name || '', latitude: loc?.latitude || loc?.lat || '', longitude: loc?.longitude || loc?.lng || '' } }) } }) } /** ====================== * 地图滚动隐藏 ====================== */ const isPageScrolling = ref(false) let scrollTimer = null function handlePageScroll() { isPageScrolling.value = true if (scrollTimer) clearTimeout(scrollTimer) scrollTimer = setTimeout(() => { isPageScrolling.value = false }, 180) } onUnmounted(() => { if (scrollTimer) clearTimeout(scrollTimer) }) /** ====================== * 上传(uview) ====================== */ const TemporaryValue = ref('image') const fileList1 = ref([]) const videoSrc = ref('') const deletePic = (event) => { fileList1.value.splice(event.index, 1) } const afterRead = (event) => { const lists = Array.isArray(event.file) ? event.file : [].concat(event.file) lists.forEach((item) => { const url = item?.url || item?.path || item?.tempFilePath if (!url) return fileList1.value.push({ ...item, url, status: 'ready', message: '等待上传' }) }) } watch([fileList1, TemporaryValue], () => { if (TemporaryValue.value !== 'video') { videoSrc.value = '' return } const first = fileList1.value?.[0] videoSrc.value = first?.url || '' }, { deep: true }) const selectsfile = () => { if (publishing.value) return uni.showActionSheet({ itemList: ['上传图片', '上传视频'], itemColor: '#000000', success: (res) => { const next = res.tapIndex === 1 ? 'video' : 'image' if (TemporaryValue.value !== next) { fileList1.value = [] WorkID.value = '' videoSrc.value = '' } TemporaryValue.value = next uni.showToast({ title: `已切换为${next === 'video' ? '视频' : '图片'}`, icon: 'none' }) }, fail: () => { uni.showToast({ title: '取消选择', icon: 'none' }) } }) } /** ====================== * 工具方法 ====================== */ function joinUrl(base = '', path = '') { const b = String(base || '').replace(/\/+$/, '') const p = String(path || '').replace(/^\/+/, '') return b ? `${b}/${p}` : p } function toFormUrlEncoded(obj) { const parts = [] Object.keys(obj || {}).forEach((k) => { const v = obj[k] if (v === undefined || v === null || v === '') return parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`) }) return parts.join('&') } function safeParseJSON(maybe) { if (maybe == null) return { ok: false, data: null, raw: '' } if (typeof maybe === 'object') return { ok: true, data: maybe, raw: '' } const raw = String(maybe) const trimmed = raw.trim() if (trimmed.startsWith('<!DOCTYPE') || trimmed.startsWith('<html') || trimmed.startsWith('<')) { return { ok: false, data: null, raw } } try { return { ok: true, data: JSON.parse(raw), raw } } catch (e) { return { ok: false, data: null, raw } } } function shortText(str = '', n = 160) { const s = String(str || '') return s.length > n ? s.slice(0, n) + '...' : s } function buildPayload() { return { consumerId: String(consumerId.value || ''), content: String(textAreaContent.value || ''), id: String(WorkID.value || ''), tagIds: (selectedTopicIds.value || []).join(','), friendIds: String(FriendID.value || ''), address: String(selectedLocation.value.address || selectedLocation.value.name || ''), latitude: String(selectedLocation.value.latitude || ''), longitude: String(selectedLocation.value.longitude || '') } } /** ====================== * 纯文字发布 ====================== */ const UPLOAD_URL = 'circle' const serverUrl = inject('serverUrl') async function publishTextOnly() { const url = joinUrl(serverUrl, UPLOAD_URL) const payload = buildPayload() return await new Promise((resolve, reject) => { uni.request({ url, method: 'POST', header: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: toFormUrlEncoded(payload), success: (res) => { const parsed = safeParseJSON(res?.data) const data = parsed.ok ? parsed.data : res?.data const ok = data?.result === 'success' || !!data?.id if (!ok) { return reject(new Error(data?.msg || data?.message || '纯文字发布失败')) } if (data?.id) WorkID.value = data.id resolve(data) }, fail: (err) => reject(err) }) }) } /** ====================== * 上传 / 发布 / 审核 ====================== */ const WorkID = ref('') const publishing = ref(false) const publishIndex = ref(0) const publishTotal = ref(0) const uploadingCount = computed(() => fileList1.value.filter(f => f.status === 'uploading').length) const failedCount = computed(() => fileList1.value.filter(f => f.status === 'failed').length) const publishProgressText = computed(() => { if (!publishing.value) return '' if (publishTotal.value <= 0) return '发布中...' return `发布中 ${publishIndex.value}/${publishTotal.value}` }) const uploadFilePromise = (filePath) => { return new Promise((resolve, reject) => { const url = joinUrl(serverUrl, UPLOAD_URL) const payload = buildPayload() uni.uploadFile({ url, filePath, name: 'circleFile', header: { 'Content-Type': 'multipart/form-data' }, formData: payload, success: (res) => { const parsed = safeParseJSON(res?.data) if (!parsed.ok) { reject(new Error('接口返回非JSON:' + shortText(parsed.raw))) return } const data = parsed.data || {} if (data?.id) WorkID.value = data.id resolve(data) }, fail: (err) => reject(err) }) }) } async function Reviewworks() { if (!WorkID.value) throw new Error('缺少作品ID,无法提交审核') const option = { id: WorkID.value, state: 2 } await ajax.post({ url: 'updateCircleWithState', data: option, method: 'post' }) uni.showToast({ title: '已提交审核', icon: 'none' }) } const handlePublish = async () => { if (publishing.value) return const readyFiles = fileList1.value.filter(f => f.status === 'ready' || f.status === 'failed') if (readyFiles.length === 0 && !textAreaContent.value.trim()) { uni.showToast({ title: '请添加内容或图片/视频', icon: 'none' }) return } if (!consumerId.value) { uni.showToast({ title: '缺少consumerId,请先登录', icon: 'none' }) return } publishing.value = true publishIndex.value = 0 publishTotal.value = readyFiles.length uni.showLoading({ title: '发布中...', mask: true }) try { // 纯文字 if (readyFiles.length === 0) { await publishTextOnly() await Reviewworks() uni.hideLoading() uni.showToast({ title: '发布成功', icon: 'success' }) setTimeout(() => { uni.navigateBack({ delta: 1 }) }, 800) return } // 有文件,逐个上传 for (let i = 0; i < readyFiles.length; i++) { const file = readyFiles[i] publishIndex.value = i + 1 file.status = 'uploading' file.message = '上传中...' try { const res = await uploadFilePromise(file.url) file.status = 'success' file.message = '' if (res?.url) file.url = res.url } catch (err) { file.status = 'failed' file.message = '上传失败' uni.hideLoading() uni.showModal({ title: '上传失败', content: '原因:' + (err?.message || '上传失败') + '\n\n常见原因:\n1)接口地址写错/返回HTML\n2)服务 502/404\n3)后端返回不是 JSON', showCancel: false }) throw err } } // 上传完成后提交审核 await Reviewworks() uni.hideLoading() uni.showToast({ title: '发布成功', icon: 'success' }) setTimeout(() => { uni.navigateBack({ delta: 1 }) }, 800) } catch (e) { uni.hideLoading() uni.showToast({ title: e?.message || '发布失败', icon: 'none' }) } finally { publishing.value = false } } /** ====================== * 其他:返回 / 说明 ====================== */ const navigateBack = () => uni.navigateBack() const showHelp = () => { uni.showModal({ title: '发布说明', content: '1)可输入描述并添加话题/@朋友。\n' + '2)可添加定位,提交时会带上 address / latitude / longitude。\n' + '3)可上传图片或视频(最多9个)。\n' + '4)发布时会逐个上传,失败会提示原因。\n' + '5)页面滚动时地图会自动隐藏,停止滚动后再显示,避免地图漂浮。\n' + '6)如果提示“非JSON”,大概率是接口地址错误或服务器返回HTML错误页。', showCancel: false }) } </script> <style lang="scss" scoped> @import '@/assets/styles/global.scss'; .app-container { padding-top: 0 !important; } .pc-root { height: 100vh; overflow: hidden; background: #f7f8fa; } /* Header */ .pc-header { position: fixed; left: 0; right: 0; top: 0; z-index: 999; background: #fff; } .pc-header-inner { position: relative; display: flex; align-items: center; justify-content: center; } .pc-left, .pc-right { position: absolute; top: 0; bottom: 0; width: 140rpx; display: flex; align-items: center; } .pc-left { left: 0; justify-content: flex-start; } .pc-right { right: 0; justify-content: flex-end; padding-right: 24rpx; box-sizing: border-box; } .pc-center { display: flex; align-items: center; justify-content: center; } .pc-title { font-size: 32rpx; color: #000; font-weight: 700; } .desc-text { font-size: 28rpx; color: #333; } .pc-main { height: 100vh; box-sizing: border-box; display: flex; flex-direction: column; min-height: 0; } .pc-scroll { flex: 1; height: 0; min-height: 0; background: #f7f8fa; } .page-body { width: 94%; margin: 0 auto; padding: 24rpx 0 40rpx 0; box-sizing: border-box; } .describe .texter { position: relative; border-radius: 20rpx; box-shadow: 0 2rpx 14rpx rgba(0, 0, 0, 0.06); background: #fff; padding: 20rpx; min-height: 240rpx; } .describe textarea { min-height: 200rpx; font-size: 28rpx; } .counter { position: absolute; right: 10rpx; bottom: 10rpx; } .counter-text { font-size: 26rpx; color: #999; } .actions-row { display: flex; align-items: center; margin-top: 24rpx; gap: 20rpx; flex-wrap: wrap; } .chip { height: 50rpx; padding: 0 24rpx; border-radius: 50rpx; background-color: #D89833; font-size: 26rpx; display: flex; align-items: center; justify-content: center; color: #fff; } .chip.ghost { background-color: transparent; border: 1rpx solid rgba(0, 0, 0, 0.12); color: #333; } .video-wrap { width: 220rpx; height: 300rpx; margin-top: 24rpx; border-radius: 15rpx; overflow: hidden; background: #000; } /* 定位模块 */ .location-card { margin-top: 24rpx; padding: 20rpx; background: #fff; border-radius: 12rpx; box-sizing: border-box; } .location-head { display: flex; align-items: center; justify-content: space-between; } .location-title { font-size: 28rpx; color: #111; font-weight: 600; } .location-action { font-size: 24rpx; color: #D89833; } .location-info { margin-top: 14rpx; display: flex; flex-direction: column; gap: 10rpx; } .location-name { font-size: 28rpx; color: #111; font-weight: 600; line-height: 1.5; } .location-address { font-size: 24rpx; color: #666; line-height: 1.6; word-break: break-all; } .location-coord { font-size: 24rpx; color: #999; line-height: 1.6; word-break: break-all; } .location-placeholder { font-size: 26rpx; color: #bbb; line-height: 1.6; } .map-shell { margin-top: 18rpx; width: 100%; height: 280rpx; border-radius: 16rpx; overflow: hidden; background: #f5f5f5; } .map-box { width: 100%; height: 280rpx; } .map-placeholder { width: 100%; height: 280rpx; display: flex; align-items: center; justify-content: center; font-size: 26rpx; color: #999; background: #f5f5f5; text-align: center; padding: 0 24rpx; box-sizing: border-box; } /* 上传卡片 */ .upload-card { margin-top: 24rpx; padding: 20rpx; background-color: #ffffff; border-radius: 12rpx; } .upload-title { font-size: 28rpx; color: #111; display: flex; align-items: center; gap: 10rpx; margin-bottom: 12rpx; } .upload-sub { font-size: 24rpx; color: #999; } .upload-tip { margin-top: 10rpx; font-size: 24rpx; color: #666; } /* 发布按钮 */ .publish-row { width: 100%; display: flex; justify-content: center; align-items: center; } .publish-btn { width: 224rpx; height: 72rpx; background-color: #D89833; border-radius: 12rpx; display: flex; justify-content: center; align-items: center; } .publish-btn.disabled { opacity: 0.6; } .publish-inner { display: flex; align-items: center; gap: 10rpx; } .publish-text { color: #ffffff; font-size: 28rpx; font-weight: 900; } </style>用这个api 4.competitionPerform在线飙戏提交text<view class="pc-center"> <text class="pc-title">发布作品</text> </view> <view class="pc-right" @tap="showHelp"> <text class="desc-text">说明</text> </view> </view> </view> <view class="pc-main" :style="{ paddingTop: headerTotalPx + 'px' }"> <scroll-view scroll-y class="pc-scroll" @scroll="handlePageScroll"> <view class="page-body"> <!-- 文本描述 --> <view class="describe"> <view class="texter"> <textarea v-model="textAreaContent" placeholder="添加作品描述..." maxlength="500" @input="onTextInput" style="width: 100%;" /> <view class="counter"> <text class="counter-text">{{ textAreaContentLength }}/500字</text> </view> </view> <!-- 话题 / 朋友 / 上传类型 --> <view class="actions-row"> <view class="chip" @tap="toggleTopics">#话题</view> <view class="chip" @tap="toggleFriends">@朋友</view> <view class="chip ghost" @tap="selectsfile"> 上传类型:{{ TemporaryValue === 'video' ? '视频' : '图片' }} </view> </view> <!-- 话题组件 --> <TopicList v-if="showTopics" :topics="topics" @select-topic="handleSelectTopic" /> <!-- 朋友组件 --> <FriendList v-if="showFriends" :friends="friends" @select-friend="handleSelectFriend" /> <!-- 视频预览 --> <view class="video-wrap" v-if="videoSrc"> <video :src="videoSrc" style="height: 100%; width: 100%;" :controls="true" :show-fullscreen-btn="false" loop show-center-play-btn="false" object-fit="fill" /> </view> </view> <!-- 定位模块 --> <view class="location-card" @tap="openLocation"> <view class="location-head"> <text class="location-title">添加定位</text> <text class="location-action"> {{ selectedLocation.name ? '重新选择' : '点击选择' }} </text> </view> <view class="location-info"> <text v-if="selectedLocation.name" class="location-name"> {{ selectedLocation.name }} </text> <text v-else class="location-placeholder"> 点击选择位置 </text> <text v-if="selectedLocation.address" class="location-address"> 地址:{{ selectedLocation.address }} </text> <text v-if="selectedLocation.latitude || selectedLocation.longitude" class="location-coord" > 纬度:{{ selectedLocation.latitude || '--' }} 经度:{{ selectedLocation.longitude || '--' }} </text> </view> <view class="map-shell"> <view v-if="!hasLocation" class="map-placeholder" > 选择定位后,这里会显示地图预览 </view> <view v-else-if="isPageScrolling" class="map-placeholder" > 页面滚动地图浮动,地图已自动隐藏 </view> <map v-else class="map-box" :latitude="mapLatitude" :longitude="mapLongitude" :scale="16" :markers="markers" /> </view> </view> <!-- 上传区域 --> <view class="upload-card"> <view class="upload-title"> 添加视频/图片 <text class="upload-sub">(最多 9 个)</text> </view> <up-upload :fileList="fileList1" @afterRead="afterRead" :accept="TemporaryValue" @delete="deletePic" name="1" multiple :maxCount="9" /> <view class="upload-tip" v-if="fileList1.length"> <text>已选择:{{ fileList1.length }} 个</text> <text v-if="uploadingCount > 0" style="margin-left: 14rpx;"> 上传中:{{ uploadingCount }} 个 </text> <text v-if="failedCount > 0" style="margin-left: 14rpx; color:#ff4d4f;"> 失败:{{ failedCount }} 个 </text> </view> </view> <view style="width: 100%; height: 116rpx;"></view> <!-- 发布按钮 --> <view class="publish-row"> <view class="publish-btn" :class="{ disabled: publishing }" @tap="handlePublish"> <view class="publish-inner"> <uni-icons v-if="publishing" type="spinner-cycle" size="18" color="#fff" /> <text class="publish-text">{{ publishing ? publishProgressText : '发布' }}</text> </view> </view> </view> <view style="height: 36rpx;"></view> </view> </scroll-view> </view> </view> </view>
入口参数
String id,
String consumerId, 用户id
String competitionPerformScriptId 剧本id
Integer competitionPerformType, 类型(1喜、2怒、3哀、4乐、5其他)
competitionPerformFile 上传文件
String longitude 精度
string latitude 纬度
String address 地址
String[] tagIds 话题
然后这个页面有话题 没有朋友看着改进一下 然后写一个完整的页面<template>
<view :class="['app-container', theme]">
<view class="content">
<view :style="{ height: statusBarHeight + 'px' }"></view>
</template> <script setup> import { ref, watch, onUnmounted, inject } from 'vue' import { onLoad, onShow } from '@dcloudio/uni-app' import { storeToRefs } from 'pinia' import { useMainStore } from '@/store/index.js' import { getStatusBarHeight } from '@/utils/system.js' import ajax from '@/api/ajax.js' import Largewaterfallflow from '@/components/HomepageComponents/Onlinedrama/Largewaterfallflow.vue' import Smallwaterfallflow from '@/components/HomepageComponents/Onlinedrama/Smallwaterfallflow.vue' const store = useMainStore() const { theme } = storeToRefs(store) const app = getApp() const webProjectUrl = inject('webProjectUrl', app?.globalData?.webProjectUrl || '') const statusBarHeight = ref(getStatusBarHeight()) const searchValue = ref('') const islogin = ref(false) /* 顶部分类栏 */ const activeEmotionIndex = ref(0) const emotionTabs = ref([ { id: 1, name: '推荐', type: '' }, { id: 2, name: '喜', type: '' }, { id: 3, name: '怒', type: '' }, { id: 4, name: '哀', type: '' }, { id: 5, name: '乐', type: '' }, { id: 6, name: '表情', type: '' }, { id: 7, name: '其他', type: '' } ]) /* 顶部轮播图 */ const isadvertisementdata = ref(false) const isadvertisement = ref(true) const current = ref(0) const slides = ref([]) const videoLoaded = ref([]) const videoError = ref([]) const timer = ref(null) /* 列表 */ const formattedItems = ref([]) const pageSize = ref(10) const currentPage = ref(0) const insertAdPage = ref(0) const loading = ref(false) const noMoreData = ref(false) const pageType = ref('') const searchTimer = ref(null) const goBack = () => { uni.navigateBack() } const handleUpload = () => { uni.navigateTo({ url:"/pages/caffeinecontent/Coolcoffeevalue/Lifedrama/Postdrama" }) } const handleEmotionTab = async (item, index) => { activeEmotionIndex.value = index pageType.value = item.type ?? '' await resetListAndLoad() } const isVideoFile = (fileName = '') => { const ext = fileName.split('.').pop()?.toLowerCase() || '' return ext === 'mp4' } const buildFileUrl = (path = '') => { if (!path) return '' if (/^https?:\/\//i.test(path)) return path return `${webProjectUrl}${path}` } const formatDate = (time = '') => { if (!time) return '' const str = String(time).replace('T', ' ') return str.slice(0, 10) } /* 顶部轮播逻辑 */ const clearTimer = () => { if (timer.value) { clearTimeout(timer.value) timer.value = null } } const nextSlide = () => { if (!slides.value.length) return let next = current.value + 1 if (next >= slides.value.length) next = 0 current.value = next } const playVideo = (index) => { try { const videoContext = uni.createVideoContext(`video-${index}`) videoContext.play() } catch (error) { console.log('视频播放失败', error) } } const pauseAllVideos = () => { slides.value.forEach((item, index) => { if (item.type === 'video') { try { const videoContext = uni.createVideoContext(`video-${index}`) videoContext.pause() } catch (error) { console.log('暂停视频失败', error) } } }) } const setupTimer = () => { if (!slides.value.length) return const currentSlide = slides.value[current.value] if (!currentSlide) return if (currentSlide.type === 'image') { timer.value = setTimeout(() => { nextSlide() }, 3000) } else if (currentSlide.type === 'video') { if (videoLoaded.value[current.value] && !videoError.value[current.value]) { playVideo(current.value) } } } const getAdvertisingList = async () => { try { clearTimer() pauseAllVideos() current.value = 0 slides.value = [] const res = await ajax.post({ url: 'getAdvertisementList', method: 'POST', data: { operateType: 'transverse', rowStartIdx: 0, rowCount: 6 } }) const data = res?.data || {} if ( data.result === 'success' && Array.isArray(data.advertisementArray) && data.advertisementArray.length > 0 ) { slides.value = data.advertisementArray.map((item) => { const fileName = item.fileName || '' return { id: item.id, type: isVideoFile(fileName) ? 'video' : 'image', src: buildFileUrl(fileName), title: item.title || '', link: item.url || '' } }) videoLoaded.value = new Array(slides.value.length).fill(false) videoError.value = new Array(slides.value.length).fill(false) isadvertisementdata.value = true setupTimer() } else { isadvertisementdata.value = false slides.value = [] } } catch (error) { console.error('获取广告失败', error) isadvertisementdata.value = false slides.value = [] } } const onChange = (e) => { current.value = e.detail.current } const onAnimationFinish = () => {} const getAdvertising = (item) => { if (!item?.link) return uni.navigateTo({ url: `/pages/webView/webView?url=${encodeURIComponent(item.link)}` }) } const onVideoEnded = () => { nextSlide() } const onVideoLoaded = (index) => { videoLoaded.value[index] = true if (current.value === index) { playVideo(index) } } const onVideoError = (index) => { videoError.value[index] = true nextSlide() } watch(current, () => { clearTimer() pauseAllVideos() setupTimer() }) /* 小盒子数据格式化 */ const normalizeRecommendItem = (item, index) => { const fileArray = Array.isArray(item.fileArray) ? item.fileArray : [] const firstFile = fileArray[0] || {} const filmName = firstFile.filmName || '' const mediaUrl = buildFileUrl(filmName) const avatarUrl = buildFileUrl(item.avatarUrl || 'images/avatar/defaultAvatar.png') return { id: `${currentPage.value}-${index}-${item.consumerId || 'recommend'}`, consumerId: item.consumerId || '', title: item.title || '', nickname: item.nickname || '', avatarUrl, createTime: item.createTime || '', displayDate: formatDate(item.createTime), fileName: filmName, mediaUrl, isVideo: isVideoFile(filmName) } } const appendSmallRows = (list = []) => { const rows = [] for (let i = 0; i < list.length; i += 2) { rows.push({ type: 'smallRow', data: list.slice(i, i + 2), renderKey: `small-row-${currentPage.value}-${i}` }) } formattedItems.value.push(...rows) } /* 瀑布流插入广告 */ const getFlowAdvertisement = async () => { try { const res = await ajax.post({ url: 'getAdvertisementList', method: 'POST', data: { operateType: 'transverse', rowStartIdx: insertAdPage.value, rowCount: 1 } }) const data = res?.data || {} if ( data.result === 'success' && Array.isArray(data.advertisementArray) && data.advertisementArray.length > 0 ) { const adItem = data.advertisementArray[0] const fileName = adItem.fileName || '' return { id: `flow-ad-${insertAdPage.value}-${adItem.id}`, type: isVideoFile(fileName) ? 'video' : 'image', src: buildFileUrl(fileName), title: adItem.title || '', link: adItem.url || '' } } return null } catch (error) { console.error('获取瀑布流广告失败', error) return null } } const getRecommendList = async (isFirstPage = false) => { if (loading.value || (!isFirstPage && noMoreData.value)) return loading.value = true try { const requestData = { rowStartIdx: currentPage.value * pageSize.value, rowCount: pageSize.value } if (pageType.value !== '' && pageType.value !== undefined && pageType.value !== null) { requestData.type = pageType.value } const res = await ajax.post({ url: 'competitionPerformIndexPinterestList', method: 'POST', data: requestData }) const data = res?.data || {} if (data.result === 'success') { const rawList = Array.isArray(data.recommendArray) ? data.recommendArray : [] let list = rawList.map((item, index) => normalizeRecommendItem(item, index)) const keyword = searchValue.value.trim() if (keyword) { list = list.filter((item) => { return ( (item.title || '').includes(keyword) || (item.nickname || '').includes(keyword) ) }) } if (list.length) { appendSmallRows(list) } if (rawList.length < pageSize.value) { noMoreData.value = true } if (rawList.length > 0) { currentPage.value += 1 } if (list.length === pageSize.value) { const bigAd = await getFlowAdvertisement() if (bigAd) { formattedItems.value.push({ type: 'big', data: bigAd, renderKey: bigAd.id }) insertAdPage.value += 1 } } if (!rawList.length && isFirstPage) { formattedItems.value = [] noMoreData.value = true } } else { if (isFirstPage) formattedItems.value = [] noMoreData.value = true } } catch (error) { console.error('获取首页瀑布流失败', error) if (isFirstPage) formattedItems.value = [] noMoreData.value = true } finally { loading.value = false } } const resetListAndLoad = async () => { currentPage.value = 0 insertAdPage.value = 0 noMoreData.value = false formattedItems.value = [] await getRecommendList(true) } const handleSearch = async () => { await resetListAndLoad() } const handleSearchInput = () => { if (searchTimer.value) clearTimeout(searchTimer.value) searchTimer.value = setTimeout(() => { handleSearch() }, 400) } const handleSearchConfirm = (e) => { searchValue.value = e?.detail?.value || searchValue.value handleSearch() } const onScrollToLower = () => { getRecommendList(false) } onLoad((opt) => { pageType.value = opt?.type ?? '' }) onShow(async () => { await getAdvertisingList() await resetListAndLoad() }) onUnmounted(() => { clearTimer() pauseAllVideos() if (searchTimer.value) clearTimeout(searchTimer.value) }) </script> <style lang="scss" scoped> @import '@/assets/styles/global.scss'; .app-container { min-height: 100vh; background: #f5f5f5; } .content { min-height: 100vh; background: #f5f5f5; } .page-wrap { height: calc(100vh - var(--status-bar-height, 0px)); padding: 0 16rpx; box-sizing: border-box; display: flex; flex-direction: column; overflow: hidden; } .top-fixed { flex-shrink: 0; background: #f5f5f5; padding-bottom: 8rpx; z-index: 10; } .nav-header { height: 88rpx; display: flex; align-items: center; margin-bottom: 8rpx; } .back-icon { width: 52rpx; height: 52rpx; display: flex; align-items: center; justify-content: center; margin-right: 10rpx; flex-shrink: 0; } .search-box { flex: 1; height: 64rpx; background: #ffffff; border-radius: 20rpx; display: flex; align-items: center; padding: 0 18rpx; box-sizing: border-box; gap: 10rpx; } .search-input { flex: 1; height: 100%; font-size: 24rpx; color: #333333; } ::v-deep .search-placeholder { color: #c2c2c2; font-size: 24rpx; } .category-row { display: flex; align-items: center; justify-content: space-between; height: 64rpx; } .category-scroll { flex: 1; white-space: nowrap; overflow: hidden; } .category-list { display: inline-flex; align-items: center; white-space: nowrap; } .category-item { display: inline-block; font-size: 26rpx; color: #333333; margin-right: 30rpx; line-height: 1; } .activeCategory { color: #d8a03b; font-weight: 500; } .upload-btn { font-size: 26rpx; color: #d8a03b; padding-left: 16rpx; flex-shrink: 0; } .page-scroll { flex: 1; height: 0; } .scroll-content { padding-top: 10rpx; padding-bottom: 30rpx; } .containerBox { width: 100%; height: 220rpx; border-radius: 18rpx; overflow: hidden; margin-bottom: 22rpx; background: #ffffff; } .swiper-component { width: 100%; height: 100%; } .slide-item { width: 100%; height: 100%; position: relative; } .banner-media { width: 100%; height: 100%; display: block; } .video-container { width: 100%; height: 100%; position: relative; } .loading-mask, .error-mask { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.3); } .loading-text, .error-text { color: #ffffff; font-size: 24rpx; } .mixed-waterfall { width: 100%; } .small-row { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 22rpx; } .small-item { width: calc(50% - 8rpx); box-sizing: border-box; } .big-item { width: 100%; margin-bottom: 22rpx; } .state-row { width: 100%; text-align: center; padding: 24rpx 0 10rpx; font-size: 24rpx; color: #999999; } .empty_box { padding-top: 180rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #999999; } .txt { margin-top: 16rpx; font-size: 26rpx; color: #999999; } </style>Lifedramahomepage这个是上一个页面的页面点击发布调到这个页面 然后写两个完整的页面内容text<view class="page-wrap"> <!-- 顶部固定区域 --> <view class="top-fixed"> <!-- 返回 + 搜索 --> <view class="nav-header"> <view class="back-icon" @click="goBack"> <uni-icons type="left" size="22" color="#b5b5b5"></uni-icons> </view> <view class="search-box"> <uni-icons type="search" size="16" color="#c5c5c5"></uni-icons> <input v-model="searchValue" class="search-input" type="text" placeholder="演员表" placeholder-class="search-placeholder" confirm-type="search" @confirm="handleSearchConfirm" @input="handleSearchInput" /> </view> </view> <!-- 分类栏 --> <view class="category-row"> <scroll-view class="category-scroll" scroll-x show-scrollbar="false"> <view class="category-list"> <text v-for="(item, index) in emotionTabs" :key="item.id" class="category-item" :class="{ activeCategory: activeEmotionIndex === index }" @click="handleEmotionTab(item, index)" > {{ item.name }} </text> </view> </scroll-view> <view class="upload-btn" @click="handleUpload">上传</view> </view> </view> <!-- 内容滚动区 --> <scroll-view class="page-scroll" scroll-y :lower-threshold="80" @scrolltolower="onScrollToLower"> <view class="scroll-content"> <!-- 顶部轮播广告 --> <view v-if="isadvertisementdata && isadvertisement && slides.length > 0" class="containerBox"> <swiper class="swiper-component" :current="current" :duration="500" :circular="true" :indicator-dots="true" indicator-color="rgba(255,255,255,0.5)" indicator-active-color="#d8a03b" @change="onChange" @animationfinish="onAnimationFinish" > <swiper-item v-for="(item, index) in slides" :key="item.id"> <view class="slide-item" @click="getAdvertising(item)"> <image v-if="item.type === 'image'" :src="item.src" class="banner-media" mode="aspectFill" ></image> <view v-else-if="item.type === 'video'" class="video-container"> <video :id="'video-' + index" :src="item.src" class="banner-media" show-fullscreen-btn :autoplay="current === index" :controls="false" :loop="false" @ended="onVideoEnded" @loadedmetadata="onVideoLoaded(index)" @error="onVideoError(index)" ></video> <view v-if="!videoLoaded[index]" class="loading-mask"> <text class="loading-text">加载中...</text> </view> <view v-if="videoError[index]" class="error-mask"> <text class="error-text">视频加载失败</text> </view> </view> </view> </swiper-item> </swiper> </view> <!-- 列表 --> <view v-if="formattedItems.length" class="mixed-waterfall"> <block v-for="block in formattedItems" :key="block.renderKey"> <view v-if="block.type === 'smallRow'" class="small-row"> <view v-for="card in block.data" :key="card.id" class="small-item"> <Smallwaterfallflow :item="card" :islogin="islogin" /> </view> </view> <view v-else-if="block.type === 'big'" class="big-item"> <Largewaterfallflow :item="block.data" /> </view> </block> </view> <view v-else-if="!loading" class="empty_box"> <uni-icons type="image-filled" size="36" color="#c0c0c0" /> <text class="txt">抱歉!暂无相关数据</text> </view> <view v-if="loading" class="state-row"> 加载中... </view> <view v-else-if="noMoreData && formattedItems.length" class="state-row"> 没有更多数据了 </view> </view> </scroll-view> </view> </view> </view>
下面给你两个完整页面。主要改动:
competitionPerform。competitionPerformFile。id / consumerId / competitionPerformScriptId / competitionPerformType / longitude / latitude / address / tagIds。competitionPerformType 和剧本 id。vue<template> <view :class="['app-container', theme]"> <view class="pc-root"> <!-- 顶部 Header --> <view class="pc-header" :style="{ paddingTop: statusBarHeight + 'px' }"> <view class="pc-header-inner" :style="{ height: navBarHeight + 'px' }"> <view class="pc-left" @tap="navigateBack"> <uni-icons style="margin-left: 20rpx;" type="left" color="#000000" size="26" /> </view> <view class="pc-center"> <text class="pc-title">在线飙戏提交</text> </view> <view class="pc-right" @tap="showHelp"> <text class="desc-text">说明</text> </view> </view> </view> <view class="pc-main" :style="{ paddingTop: headerTotalPx + 'px' }"> <scroll-view scroll-y class="pc-scroll" @scroll="handlePageScroll"> <view class="page-body"> <!-- 剧本信息 --> <view class="card"> <view class="card-title">剧本信息</view> <view class="script-row"> <text class="script-label">剧本ID</text> <text class="script-value"> {{ competitionPerformScriptId || '暂无,请从剧本页进入' }} </text> </view> </view> <!-- 表演类型 --> <view class="card"> <view class="card-title">表演类型</view> <view class="type-list"> <view v-for="item in typeTabs" :key="item.value" class="type-chip" :class="{ active: competitionPerformType === item.value }" @tap="competitionPerformType = item.value" > {{ item.name }} </view> </view> </view> <!-- 话题 --> <view class="card"> <view class="card-title">添加话题</view> <view class="topic-action-row"> <view class="chip" @tap="toggleTopics">#话题</view> <view v-if="selectedTopics.length" class="selected-topic-list"> <view v-for="item in selectedTopics" :key="item.id" class="selected-topic" @tap.stop="removeTopic(item.id)" > #{{ item.name }} <text class="topic-close">×</text> </view> </view> </view> <TopicList v-if="showTopics" :topics="topics" @select-topic="handleSelectTopic" /> </view> <!-- 定位模块 --> <view class="location-card" @tap="openLocation"> <view class="location-head"> <text class="location-title">添加定位</text> <text class="location-action"> {{ selectedLocation.name ? '重新选择' : '点击选择' }} </text> </view> <view class="location-info"> <text v-if="selectedLocation.name" class="location-name"> {{ selectedLocation.name }} </text> <text v-else class="location-placeholder"> 点击选择位置 </text> <text v-if="selectedLocation.address" class="location-address"> 地址:{{ selectedLocation.address }} </text> <text v-if="selectedLocation.latitude || selectedLocation.longitude" class="location-coord" > 纬度:{{ selectedLocation.latitude || '--' }} 经度:{{ selectedLocation.longitude || '--' }} </text> </view> <view class="map-shell"> <view v-if="!hasLocation" class="map-placeholder"> 选择定位后,这里会显示地图预览 </view> <view v-else-if="isPageScrolling" class="map-placeholder"> 页面滚动地图浮动,地图已自动隐藏 </view> <map v-else class="map-box" :latitude="mapLatitude" :longitude="mapLongitude" :scale="16" :markers="markers" /> </view> </view> <!-- 上传区域 --> <view class="upload-card"> <view class="upload-title"> 上传表演文件 <text class="upload-sub">(最多 1 个)</text> </view> <view class="upload-type-row"> <view class="upload-type-chip" :class="{ active: acceptType === 'video' }" @tap="changeAcceptType('video')" > 视频 </view> <view class="upload-type-chip" :class="{ active: acceptType === 'image' }" @tap="changeAcceptType('image')" > 图片 </view> </view> <up-upload :fileList="fileList" @afterRead="afterRead" @delete="deleteFile" :accept="acceptType" name="competitionPerformFile" :maxCount="1" /> <view class="video-wrap" v-if="videoSrc"> <video :src="videoSrc" style="height: 100%; width: 100%;" :controls="true" :show-fullscreen-btn="false" show-center-play-btn="false" object-fit="cover" /> </view> <view class="upload-tip" v-if="fileList.length"> <text>已选择:{{ fileList.length }} 个</text> <text v-if="uploadingCount > 0" style="margin-left: 14rpx;"> 上传中:{{ uploadingCount }} 个 </text> <text v-if="failedCount > 0" style="margin-left: 14rpx; color:#ff4d4f;"> 失败:{{ failedCount }} 个 </text> </view> </view> <view style="width: 100%; height: 116rpx;"></view> <!-- 发布按钮 --> <view class="publish-row"> <view class="publish-btn" :class="{ disabled: publishing }" @tap="handlePublish" > <view class="publish-inner"> <uni-icons v-if="publishing" type="spinner-cycle" size="18" color="#fff" /> <text class="publish-text"> {{ publishing ? publishProgressText : '提交' }} </text> </view> </view> </view> <view style="height: 36rpx;"></view> </view> </scroll-view> </view> </view> </view> </template> <script setup> import { ref, computed, onMounted, onUnmounted, inject, watch } from 'vue' import { onLoad } from '@dcloudio/uni-app' import { storeToRefs } from 'pinia' import { useMainStore } from '@/store/index.js' import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' import { Gettopics } from '@/api/api.js' import TopicList from '@/components/TopicList.vue' const store = useMainStore() const { theme } = storeToRefs(store) const app = getApp() const serverUrl = inject('serverUrl', app?.globalData?.serverUrl || '') /** ====================== * 顶部安全区与标题栏 ====================== */ const sys = uni.getSystemInfoSync() const statusBarHeight = ref(Number(getStatusBarHeight?.() || sys?.statusBarHeight || 0)) const navBarHeight = ref(Number(getTitleBarHeight?.() || 44)) try { const menu = uni.getMenuButtonBoundingClientRect?.() if (menu?.height) { const gap = Number(menu.top || 0) - Number(statusBarHeight.value || 0) navBarHeight.value = Number(menu.height || 44) + gap * 2 } } catch (e) {} const headerTotalPx = computed(() => { return Number(statusBarHeight.value || 0) + Number(navBarHeight.value || 0) }) /** ====================== * 路由参数 ====================== */ const id = ref('') const competitionPerformScriptId = ref('') const competitionPerformType = ref(1) const typeTabs = ref([ { name: '喜', value: 1 }, { name: '怒', value: 2 }, { name: '哀', value: 3 }, { name: '乐', value: 4 }, { name: '其他', value: 5 } ]) onLoad((opt = {}) => { id.value = opt.id || '' competitionPerformScriptId.value = opt.competitionPerformScriptId || opt.scriptId || opt.performScriptId || '' const routeType = Number(opt.competitionPerformType || opt.type || 1) competitionPerformType.value = [1, 2, 3, 4, 5].includes(routeType) ? routeType : 1 }) /** ====================== * 话题 ====================== */ const showTopics = ref(false) const topics = ref([]) const selectedTopics = ref([]) const selectedTopicIds = computed(() => selectedTopics.value.map(item => String(item.id))) const params = { keyword: '', rowStartIdx: 0, rowCount: '20' } const TopicDetails = async () => { try { const res = await Gettopics(params) topics.value = res?.tagArray || [] } catch (e) { uni.showToast({ title: '话题获取失败', icon: 'none' }) } } const toggleTopics = () => { showTopics.value = !showTopics.value } const handleSelectTopic = (topic) => { if (!topic?.id) return const id = String(topic.id) const exists = selectedTopics.value.some(item => String(item.id) === id) if (!exists) { selectedTopics.value.push({ id, name: topic.name || topic.tagName || '' }) } showTopics.value = false } const removeTopic = (topicId) => { selectedTopics.value = selectedTopics.value.filter(item => String(item.id) !== String(topicId)) } onMounted(() => { TopicDetails() }) /** ====================== * 定位 ====================== */ const selectedLocation = ref({ name: '', address: '', latitude: '', longitude: '' }) const hasLocation = computed(() => { return !!selectedLocation.value.latitude && !!selectedLocation.value.longitude }) const mapLatitude = computed(() => Number(selectedLocation.value.latitude || 0)) const mapLongitude = computed(() => Number(selectedLocation.value.longitude || 0)) const markers = computed(() => { if (!hasLocation.value) return [] return [{ id: 1, latitude: Number(selectedLocation.value.latitude), longitude: Number(selectedLocation.value.longitude), width: 32, height: 32 }] }) function openLocation() { uni.navigateTo({ url: '/pages/sociAlize/sharedspace/sharedspace', success: (navRes) => { navRes.eventChannel?.on('sendLocation', (loc) => { selectedLocation.value = { name: loc?.name || '', address: loc?.address || loc?.name || '', latitude: loc?.latitude || loc?.lat || '', longitude: loc?.longitude || loc?.lng || '' } }) } }) } /** ====================== * 地图滚动隐藏 ====================== */ const isPageScrolling = ref(false) let scrollTimer = null function handlePageScroll() { isPageScrolling.value = true if (scrollTimer) clearTimeout(scrollTimer) scrollTimer = setTimeout(() => { isPageScrolling.value = false }, 180) } onUnmounted(() => { if (scrollTimer) clearTimeout(scrollTimer) }) /** ====================== * 上传 ====================== */ const acceptType = ref('video') const fileList = ref([]) const videoSrc = ref('') const deleteFile = (event) => { fileList.value.splice(event.index, 1) } const afterRead = (event) => { const file = Array.isArray(event.file) ? event.file[0] : event.file const url = file?.url || file?.path || file?.tempFilePath if (!url) return fileList.value = [{ ...file, url, status: 'ready', message: '等待上传' }] } const changeAcceptType = (type) => { if (publishing.value) return if (acceptType.value === type) return acceptType.value = type fileList.value = [] videoSrc.value = '' uni.showToast({ title: `已切换为${type === 'video' ? '视频' : '图片'}`, icon: 'none' }) } watch([fileList, acceptType], () => { if (acceptType.value !== 'video') { videoSrc.value = '' return } const first = fileList.value?.[0] videoSrc.value = first?.url || '' }, { deep: true }) /** ====================== * 工具方法 ====================== */ const consumerId = ref(uni.getStorageSync('consumerId') || '') function joinUrl(base = '', path = '') { const b = String(base || '').replace(/\/+$/, '') const p = String(path || '').replace(/^\/+/, '') return b ? `${b}/${p}` : p } function safeParseJSON(maybe) { if (maybe == null) return { ok: false, data: null, raw: '' } if (typeof maybe === 'object') return { ok: true, data: maybe, raw: '' } const raw = String(maybe) const trimmed = raw.trim() if (trimmed.startsWith('<!DOCTYPE') || trimmed.startsWith('<html') || trimmed.startsWith('<')) { return { ok: false, data: null, raw } } try { return { ok: true, data: JSON.parse(raw), raw } } catch (e) { return { ok: false, data: null, raw } } } function shortText(str = '', n = 160) { const s = String(str || '') return s.length > n ? s.slice(0, n) + '...' : s } function buildPayload() { return { id: String(id.value || ''), consumerId: String(consumerId.value || ''), competitionPerformScriptId: String(competitionPerformScriptId.value || ''), competitionPerformType: String(competitionPerformType.value || ''), longitude: String(selectedLocation.value.longitude || ''), latitude: String(selectedLocation.value.latitude || ''), address: String(selectedLocation.value.address || selectedLocation.value.name || ''), tagIds: selectedTopicIds.value.join(',') } } /** ====================== * 发布 ====================== */ const UPLOAD_URL = 'competitionPerform' const publishing = ref(false) const publishIndex = ref(0) const publishTotal = ref(0) const uploadingCount = computed(() => fileList.value.filter(f => f.status === 'uploading').length) const failedCount = computed(() => fileList.value.filter(f => f.status === 'failed').length) const publishProgressText = computed(() => { if (!publishing.value) return '' if (publishTotal.value <= 0) return '提交中...' return `提交中 ${publishIndex.value}/${publishTotal.value}` }) const uploadFilePromise = (filePath) => { return new Promise((resolve, reject) => { const url = joinUrl(serverUrl, UPLOAD_URL) uni.uploadFile({ url, filePath, name: 'competitionPerformFile', formData: buildPayload(), success: (res) => { const parsed = safeParseJSON(res?.data) if (!parsed.ok) { reject(new Error('接口返回非JSON:' + shortText(parsed.raw))) return } const data = parsed.data || {} if (data.result && data.result !== 'success') { reject(new Error(data.msg || data.message || '提交失败')) return } if (data?.id) id.value = data.id resolve(data) }, fail: (err) => { reject(err) } }) }) } const handlePublish = async () => { if (publishing.value) return const file = fileList.value[0] if (!consumerId.value) { uni.showToast({ title: '缺少consumerId,请先登录', icon: 'none' }) return } if (!competitionPerformScriptId.value) { uni.showToast({ title: '缺少剧本ID', icon: 'none' }) return } if (!competitionPerformType.value) { uni.showToast({ title: '请选择表演类型', icon: 'none' }) return } if (!file?.url) { uni.showToast({ title: '请上传表演文件', icon: 'none' }) return } publishing.value = true publishIndex.value = 1 publishTotal.value = 1 file.status = 'uploading' file.message = '上传中...' uni.showLoading({ title: '提交中...', mask: true }) try { const res = await uploadFilePromise(file.url) file.status = 'success' file.message = '' if (res?.url) file.url = res.url uni.hideLoading() uni.showToast({ title: '提交成功', icon: 'success' }) setTimeout(() => { uni.navigateBack({ delta: 1 }) }, 800) } catch (err) { file.status = 'failed' file.message = '上传失败' uni.hideLoading() uni.showModal({ title: '提交失败', content: '原因:' + (err?.message || '提交失败') + '\n\n常见原因:\n1)接口地址错误\n2)服务端返回HTML错误页\n3)competitionPerformFile字段名不匹配\n4)剧本ID或用户ID为空', showCancel: false }) } finally { publishing.value = false } } /** ====================== * 返回 / 说明 ====================== */ const navigateBack = () => uni.navigateBack() const showHelp = () => { uni.showModal({ title: '提交说明', content: '1)请选择表演类型:喜、怒、哀、乐、其他。\n' + '2)可添加话题,提交时会带上 tagIds。\n' + '3)可添加定位,提交时会带上 address / latitude / longitude。\n' + '4)请上传一个表演文件,字段名为 competitionPerformFile。\n' + '5)接口为 competitionPerform。\n' + '6)如果提示“非JSON”,大概率是接口地址错误或服务器返回HTML错误页。', showCancel: false }) } </script> <style lang="scss" scoped> @import '@/assets/styles/global.scss'; .app-container { padding-top: 0 !important; } .pc-root { height: 100vh; overflow: hidden; background: #f7f8fa; } .pc-header { position: fixed; left: 0; right: 0; top: 0; z-index: 999; background: #fff; } .pc-header-inner { position: relative; display: flex; align-items: center; justify-content: center; } .pc-left, .pc-right { position: absolute; top: 0; bottom: 0; width: 140rpx; display: flex; align-items: center; } .pc-left { left: 0; justify-content: flex-start; } .pc-right { right: 0; justify-content: flex-end; padding-right: 24rpx; box-sizing: border-box; } .pc-center { display: flex; align-items: center; justify-content: center; } .pc-title { font-size: 32rpx; color: #000; font-weight: 700; } .desc-text { font-size: 28rpx; color: #333; } .pc-main { height: 100vh; box-sizing: border-box; display: flex; flex-direction: column; min-height: 0; } .pc-scroll { flex: 1; height: 0; min-height: 0; background: #f7f8fa; } .page-body { width: 94%; margin: 0 auto; padding: 24rpx 0 40rpx 0; box-sizing: border-box; } .card { margin-top: 24rpx; padding: 20rpx; background: #fff; border-radius: 16rpx; box-sizing: border-box; } .card:first-child { margin-top: 0; } .card-title { font-size: 28rpx; color: #111; font-weight: 600; margin-bottom: 18rpx; } .script-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 20rpx; } .script-label { font-size: 26rpx; color: #666; flex-shrink: 0; } .script-value { flex: 1; text-align: right; font-size: 26rpx; color: #333; word-break: break-all; } .type-list { display: flex; align-items: center; flex-wrap: wrap; gap: 18rpx; } .type-chip { height: 56rpx; padding: 0 28rpx; border-radius: 56rpx; border: 1rpx solid rgba(0, 0, 0, 0.12); font-size: 26rpx; color: #333; display: flex; align-items: center; justify-content: center; background: #fff; } .type-chip.active { background: #D89833; border-color: #D89833; color: #fff; } .topic-action-row { display: flex; flex-direction: column; gap: 18rpx; } .chip { width: fit-content; height: 52rpx; padding: 0 26rpx; border-radius: 52rpx; background-color: #D89833; font-size: 26rpx; display: flex; align-items: center; justify-content: center; color: #fff; } .selected-topic-list { display: flex; align-items: center; flex-wrap: wrap; gap: 14rpx; } .selected-topic { height: 48rpx; padding: 0 18rpx; border-radius: 48rpx; background: rgba(216, 152, 51, 0.12); color: #D89833; font-size: 24rpx; display: flex; align-items: center; } .topic-close { margin-left: 8rpx; font-size: 28rpx; line-height: 1; } .location-card { margin-top: 24rpx; padding: 20rpx; background: #fff; border-radius: 16rpx; box-sizing: border-box; } .location-head { display: flex; align-items: center; justify-content: space-between; } .location-title { font-size: 28rpx; color: #111; font-weight: 600; } .location-action { font-size: 24rpx; color: #D89833; } .location-info { margin-top: 14rpx; display: flex; flex-direction: column; gap: 10rpx; } .location-name { font-size: 28rpx; color: #111; font-weight: 600; line-height: 1.5; } .location-address { font-size: 24rpx; color: #666; line-height: 1.6; word-break: break-all; } .location-coord { font-size: 24rpx; color: #999; line-height: 1.6; word-break: break-all; } .location-placeholder { font-size: 26rpx; color: #bbb; line-height: 1.6; } .map-shell { margin-top: 18rpx; width: 100%; height: 280rpx; border-radius: 16rpx; overflow: hidden; background: #f5f5f5; } .map-box { width: 100%; height: 280rpx; } .map-placeholder { width: 100%; height: 280rpx; display: flex; align-items: center; justify-content: center; font-size: 26rpx; color: #999; background: #f5f5f5; text-align: center; padding: 0 24rpx; box-sizing: border-box; } .upload-card { margin-top: 24rpx; padding: 20rpx; background-color: #ffffff; border-radius: 16rpx; } .upload-title { font-size: 28rpx; color: #111; display: flex; align-items: center; gap: 10rpx; margin-bottom: 16rpx; font-weight: 600; } .upload-sub { font-size: 24rpx; color: #999; font-weight: 400; } .upload-type-row { display: flex; align-items: center; gap: 16rpx; margin-bottom: 18rpx; } .upload-type-chip { height: 50rpx; padding: 0 24rpx; border-radius: 50rpx; border: 1rpx solid rgba(0, 0, 0, 0.12); font-size: 24rpx; color: #333; display: flex; align-items: center; justify-content: center; } .upload-type-chip.active { color: #fff; background: #D89833; border-color: #D89833; } .video-wrap { width: 220rpx; height: 300rpx; margin-top: 24rpx; border-radius: 15rpx; overflow: hidden; background: #000; } .upload-tip { margin-top: 10rpx; font-size: 24rpx; color: #666; } .publish-row { width: 100%; display: flex; justify-content: center; align-items: center; } .publish-btn { width: 224rpx; height: 72rpx; background-color: #D89833; border-radius: 12rpx; display: flex; justify-content: center; align-items: center; } .publish-btn.disabled { opacity: 0.6; } .publish-inner { display: flex; align-items: center; gap: 10rpx; } .publish-text { color: #ffffff; font-size: 28rpx; font-weight: 900; } </style>
vue<template> <view :class="['app-container', theme]"> <view class="content"> <view :style="{ height: statusBarHeight + 'px' }"></view> <view class="page-wrap"> <!-- 顶部固定区域 --> <view class="top-fixed"> <!-- 返回 + 搜索 --> <view class="nav-header"> <view class="back-icon" @tap="goBack"> <uni-icons type="left" size="22" color="#b5b5b5"></uni-icons> </view> <view class="search-box"> <uni-icons type="search" size="16" color="#c5c5c5"></uni-icons> <input v-model="searchValue" class="search-input" type="text" placeholder="演员表" placeholder-class="search-placeholder" confirm-type="search" @confirm="handleSearchConfirm" @input="handleSearchInput" /> </view> </view> <!-- 分类栏 --> <view class="category-row"> <scroll-view class="category-scroll" scroll-x show-scrollbar="false"> <view class="category-list"> <text v-for="(item, index) in emotionTabs" :key="item.id" class="category-item" :class="{ activeCategory: activeEmotionIndex === index }" @tap="handleEmotionTab(item, index)" > {{ item.name }} </text> </view> </scroll-view> <view class="upload-btn" @tap="handleUpload">上传</view> </view> </view> <!-- 内容滚动区 --> <scroll-view class="page-scroll" scroll-y :lower-threshold="80" @scrolltolower="onScrollToLower" > <view class="scroll-content"> <!-- 顶部轮播广告 --> <view v-if="isadvertisementdata && isadvertisement && slides.length > 0" class="containerBox" > <swiper class="swiper-component" :current="current" :duration="500" :circular="true" :indicator-dots="true" indicator-color="rgba(255,255,255,0.5)" indicator-active-color="#d8a03b" @change="onChange" @animationfinish="onAnimationFinish" > <swiper-item v-for="(item, index) in slides" :key="item.id"> <view class="slide-item" @tap="getAdvertising(item)"> <image v-if="item.type === 'image'" :src="item.src" class="banner-media" mode="aspectFill" ></image> <view v-else-if="item.type === 'video'" class="video-container"> <video :id="'video-' + index" :src="item.src" class="banner-media" show-fullscreen-btn :autoplay="current === index" :controls="false" :loop="false" @ended="onVideoEnded" @loadedmetadata="onVideoLoaded(index)" @error="onVideoError(index)" ></video> <view v-if="!videoLoaded[index]" class="loading-mask"> <text class="loading-text">加载中...</text> </view> <view v-if="videoError[index]" class="error-mask"> <text class="error-text">视频加载失败</text> </view> </view> </view> </swiper-item> </swiper> </view> <!-- 列表 --> <view v-if="formattedItems.length" class="mixed-waterfall"> <block v-for="block in formattedItems" :key="block.renderKey"> <view v-if="block.type === 'smallRow'" class="small-row"> <view v-for="card in block.data" :key="card.id" class="small-item"> <Smallwaterfallflow :item="card" :islogin="islogin" /> </view> </view> <view v-else-if="block.type === 'big'" class="big-item"> <Largewaterfallflow :item="block.data" /> </view> </block> </view> <view v-else-if="!loading" class="empty_box"> <uni-icons type="image-filled" size="36" color="#c0c0c0" /> <text class="txt">抱歉!暂无相关数据</text> </view> <view v-if="loading" class="state-row"> 加载中... </view> <view v-else-if="noMoreData && formattedItems.length" class="state-row"> 没有更多数据了 </view> </view> </scroll-view> </view> </view> </view> </template> <script setup> import { ref, watch, onUnmounted, inject } from 'vue' import { onLoad, onShow } from '@dcloudio/uni-app' import { storeToRefs } from 'pinia' import { useMainStore } from '@/store/index.js' import { getStatusBarHeight } from '@/utils/system.js' import ajax from '@/api/ajax.js' import Largewaterfallflow from '@/components/HomepageComponents/Onlinedrama/Largewaterfallflow.vue' import Smallwaterfallflow from '@/components/HomepageComponents/Onlinedrama/Smallwaterfallflow.vue' const store = useMainStore() const { theme } = storeToRefs(store) const app = getApp() const webProjectUrl = inject('webProjectUrl', app?.globalData?.webProjectUrl || '') const statusBarHeight = ref(getStatusBarHeight()) const searchValue = ref('') const islogin = ref(false) /** * 剧本ID * 从上级页面传入: * /pages/caffeinecontent/Coolcoffeevalue/Lifedrama/Lifedramahomepage?competitionPerformScriptId=xxx */ const competitionPerformScriptId = ref('') /* 顶部分类栏 */ const activeEmotionIndex = ref(0) const emotionTabs = ref([ { id: 0, name: '推荐', type: '' }, { id: 1, name: '喜', type: 1 }, { id: 2, name: '怒', type: 2 }, { id: 3, name: '哀', type: 3 }, { id: 4, name: '乐', type: 4 }, { id: 5, name: '其他', type: 5 } ]) /* 顶部轮播图 */ const isadvertisementdata = ref(false) const isadvertisement = ref(true) const current = ref(0) const slides = ref([]) const videoLoaded = ref([]) const videoError = ref([]) const timer = ref(null) /* 列表 */ const formattedItems = ref([]) const pageSize = ref(10) const currentPage = ref(0) const insertAdPage = ref(0) const loading = ref(false) const noMoreData = ref(false) const pageType = ref('') const searchTimer = ref(null) const goBack = () => { uni.navigateBack() } const handleUpload = () => { const activeTab = emotionTabs.value[activeEmotionIndex.value] || {} const type = activeTab.type || 1 let url = '/pages/caffeinecontent/Coolcoffeevalue/Lifedrama/Postdrama' const query = [] if (competitionPerformScriptId.value) { query.push(`competitionPerformScriptId=${encodeURIComponent(competitionPerformScriptId.value)}`) } query.push(`competitionPerformType=${encodeURIComponent(type)}`) if (query.length) { url += `?${query.join('&')}` } uni.navigateTo({ url }) } const handleEmotionTab = async (item, index) => { activeEmotionIndex.value = index pageType.value = item.type ?? '' await resetListAndLoad() } const isVideoFile = (fileName = '') => { const ext = fileName.split('.').pop()?.toLowerCase() || '' return ['mp4', 'mov', 'm4v', 'avi', 'webm'].includes(ext) } const buildFileUrl = (path = '') => { if (!path) return '' if (/^https?:\/\//i.test(path)) return path const base = String(webProjectUrl || '').replace(/\/+$/, '') const p = String(path || '').replace(/^\/+/, '') return `${base}/${p}` } const formatDate = (time = '') => { if (!time) return '' const str = String(time).replace('T', ' ') return str.slice(0, 10) } /* 顶部轮播逻辑 */ const clearTimer = () => { if (timer.value) { clearTimeout(timer.value) timer.value = null } } const nextSlide = () => { if (!slides.value.length) return let next = current.value + 1 if (next >= slides.value.length) next = 0 current.value = next } const playVideo = (index) => { try { const videoContext = uni.createVideoContext(`video-${index}`) videoContext.play() } catch (error) { console.log('视频播放失败', error) } } const pauseAllVideos = () => { slides.value.forEach((item, index) => { if (item.type === 'video') { try { const videoContext = uni.createVideoContext(`video-${index}`) videoContext.pause() } catch (error) { console.log('暂停视频失败', error) } } }) } const setupTimer = () => { if (!slides.value.length) return const currentSlide = slides.value[current.value] if (!currentSlide) return if (currentSlide.type === 'image') { timer.value = setTimeout(() => { nextSlide() }, 3000) } else if (currentSlide.type === 'video') { if (videoLoaded.value[current.value] && !videoError.value[current.value]) { playVideo(current.value) } } } const getAdvertisingList = async () => { try { clearTimer() pauseAllVideos() current.value = 0 slides.value = [] const res = await ajax.post({ url: 'getAdvertisementList', method: 'POST', data: { operateType: 'transverse', rowStartIdx: 0, rowCount: 6 } }) const data = res?.data || {} if ( data.result === 'success' && Array.isArray(data.advertisementArray) && data.advertisementArray.length > 0 ) { slides.value = data.advertisementArray.map((item) => { const fileName = item.fileName || '' return { id: item.id, type: isVideoFile(fileName) ? 'video' : 'image', src: buildFileUrl(fileName), title: item.title || '', link: item.url || '' } }) videoLoaded.value = new Array(slides.value.length).fill(false) videoError.value = new Array(slides.value.length).fill(false) isadvertisementdata.value = true setupTimer() } else { isadvertisementdata.value = false slides.value = [] } } catch (error) { console.error('获取广告失败', error) isadvertisementdata.value = false slides.value = [] } } const onChange = (e) => { current.value = e.detail.current } const onAnimationFinish = () => {} const getAdvertising = (item) => { if (!item?.link) return uni.navigateTo({ url: `/pages/webView/webView?url=${encodeURIComponent(item.link)}` }) } const onVideoEnded = () => { nextSlide() } const onVideoLoaded = (index) => { videoLoaded.value[index] = true if (current.value === index) { playVideo(index) } } const onVideoError = (index) => { videoError.value[index] = true nextSlide() } watch(current, () => { clearTimer() pauseAllVideos() setupTimer() }) /* 小盒子数据格式化 */ const normalizeRecommendItem = (item, index) => { const fileArray = Array.isArray(item.fileArray) ? item.fileArray : [] const firstFile = fileArray[0] || {} const filmName = firstFile.filmName || firstFile.fileName || item.fileName || '' const mediaUrl = buildFileUrl(filmName) const avatarUrl = buildFileUrl(item.avatarUrl || 'images/avatar/defaultAvatar.png') return { id: item.id || `${currentPage.value}-${index}-${item.consumerId || 'recommend'}`, competitionPerformId: item.id || '', competitionPerformScriptId: item.competitionPerformScriptId || competitionPerformScriptId.value || '', consumerId: item.consumerId || '', title: item.title || item.scriptName || '', nickname: item.nickname || '', avatarUrl, createTime: item.createTime || '', displayDate: formatDate(item.createTime), fileName: filmName, mediaUrl, isVideo: isVideoFile(filmName), type: item.type || item.competitionPerformType || '' } } const appendSmallRows = (list = []) => { const rows = [] for (let i = 0; i < list.length; i += 2) { rows.push({ type: 'smallRow', data: list.slice(i, i + 2), renderKey: `small-row-${currentPage.value}-${i}` }) } formattedItems.value.push(...rows) } /* 瀑布流插入广告 */ const getFlowAdvertisement = async () => { try { const res = await ajax.post({ url: 'getAdvertisementList', method: 'POST', data: { operateType: 'transverse', rowStartIdx: insertAdPage.value, rowCount: 1 } }) const data = res?.data || {} if ( data.result === 'success' && Array.isArray(data.advertisementArray) && data.advertisementArray.length > 0 ) { const adItem = data.advertisementArray[0] const fileName = adItem.fileName || '' return { id: `flow-ad-${insertAdPage.value}-${adItem.id}`, type: isVideoFile(fileName) ? 'video' : 'image', src: buildFileUrl(fileName), title: adItem.title || '', link: adItem.url || '' } } return null } catch (error) { console.error('获取瀑布流广告失败', error) return null } } const getRecommendList = async (isFirstPage = false) => { if (loading.value || (!isFirstPage && noMoreData.value)) return loading.value = true try { const requestData = { rowStartIdx: currentPage.value * pageSize.value, rowCount: pageSize.value } if (pageType.value !== '' && pageType.value !== undefined && pageType.value !== null) { requestData.type = pageType.value } if (competitionPerformScriptId.value) { requestData.competitionPerformScriptId = competitionPerformScriptId.value } const keyword = searchValue.value.trim() if (keyword) { requestData.keyword = keyword } const res = await ajax.post({ url: 'competitionPerformIndexPinterestList', method: 'POST', data: requestData }) const data = res?.data || {} if (data.result === 'success') { const rawList = Array.isArray(data.recommendArray) ? data.recommendArray : [] let list = rawList.map((item, index) => normalizeRecommendItem(item, index)) /** * 如果后端不支持 keyword,这里再做一次前端兜底过滤。 */ if (keyword) { list = list.filter((item) => { return ( (item.title || '').includes(keyword) || (item.nickname || '').includes(keyword) ) }) } if (list.length) { appendSmallRows(list) } if (rawList.length < pageSize.value) { noMoreData.value = true } if (rawList.length > 0) { currentPage.value += 1 } if (list.length === pageSize.value) { const bigAd = await getFlowAdvertisement() if (bigAd) { formattedItems.value.push({ type: 'big', data: bigAd, renderKey: bigAd.id }) insertAdPage.value += 1 } } if (!rawList.length && isFirstPage) { formattedItems.value = [] noMoreData.value = true } } else { if (isFirstPage) formattedItems.value = [] noMoreData.value = true } } catch (error) { console.error('获取首页瀑布流失败', error) if (isFirstPage) formattedItems.value = [] noMoreData.value = true } finally { loading.value = false } } const resetListAndLoad = async () => { currentPage.value = 0 insertAdPage.value = 0 noMoreData.value = false formattedItems.value = [] await getRecommendList(true) } const handleSearch = async () => { await resetListAndLoad() } const handleSearchInput = () => { if (searchTimer.value) clearTimeout(searchTimer.value) searchTimer.value = setTimeout(() => { handleSearch() }, 400) } const handleSearchConfirm = (e) => { searchValue.value = e?.detail?.value || searchValue.value handleSearch() } const onScrollToLower = () => { getRecommendList(false) } onLoad((opt = {}) => { competitionPerformScriptId.value = opt.competitionPerformScriptId || opt.scriptId || opt.id || '' const routeType = opt.type || opt.competitionPerformType || '' pageType.value = routeType const findIndex = emotionTabs.value.findIndex(item => String(item.type) === String(routeType)) activeEmotionIndex.value = findIndex >= 0 ? findIndex : 0 }) onShow(async () => { await getAdvertisingList() await resetListAndLoad() }) onUnmounted(() => { clearTimer() pauseAllVideos() if (searchTimer.value) clearTimeout(searchTimer.value) }) </script> <style lang="scss" scoped> @import '@/assets/styles/global.scss'; .app-container { min-height: 100vh; background: #f5f5f5; } .content { min-height: 100vh; background: #f5f5f5; } .page-wrap { height: calc(100vh - var(--status-bar-height, 0px)); padding: 0 16rpx; box-sizing: border-box; display: flex; flex-direction: column; overflow: hidden; } .top-fixed { flex-shrink: 0; background: #f5f5f5; padding-bottom: 8rpx; z-index: 10; } .nav-header { height: 88rpx; display: flex; align-items: center; margin-bottom: 8rpx; } .back-icon { width: 52rpx; height: 52rpx; display: flex; align-items: center; justify-content: center; margin-right: 10rpx; flex-shrink: 0; } .search-box { flex: 1; height: 64rpx; background: #ffffff; border-radius: 20rpx; display: flex; align-items: center; padding: 0 18rpx; box-sizing: border-box; gap: 10rpx; } .search-input { flex: 1; height: 100%; font-size: 24rpx; color: #333333; } ::v-deep .search-placeholder { color: #c2c2c2; font-size: 24rpx; } .category-row { display: flex; align-items: center; justify-content: space-between; height: 64rpx; } .category-scroll { flex: 1; white-space: nowrap; overflow: hidden; } .category-list { display: inline-flex; align-items: center; white-space: nowrap; } .category-item { display: inline-block; font-size: 26rpx; color: #333333; margin-right: 30rpx; line-height: 1; } .activeCategory { color: #d8a03b; font-weight: 500; } .upload-btn { font-size: 26rpx; color: #d8a03b; padding-left: 16rpx; flex-shrink: 0; } .page-scroll { flex: 1; height: 0; } .scroll-content { padding-top: 10rpx; padding-bottom: 30rpx; } .containerBox { width: 100%; height: 220rpx; border-radius: 18rpx; overflow: hidden; margin-bottom: 22rpx; background: #ffffff; } .swiper-component { width: 100%; height: 100%; } .slide-item { width: 100%; height: 100%; position: relative; } .banner-media { width: 100%; height: 100%; display: block; } .video-container { width: 100%; height: 100%; position: relative; } .loading-mask, .error-mask { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.3); } .loading-text, .error-text { color: #ffffff; font-size: 24rpx; } .mixed-waterfall { width: 100%; } .small-row { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 22rpx; } .small-item { width: calc(50% - 8rpx); box-sizing: border-box; } .big-item { width: 100%; margin-bottom: 22rpx; } .state-row { width: 100%; text-align: center; padding: 24rpx 0 10rpx; font-size: 24rpx; color: #999999; } .empty_box { padding-top: 180rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #999999; } .txt { margin-top: 16rpx; font-size: 26rpx; color: #999999; } </style>
注意一个关键点:tagIds 这里我按逗号字符串提交了:
jstagIds: selectedTopicIds.value.join(',')
如果你后端严格要求 String[] tagIds,并且不能接收 "1,2,3" 这种格式,需要后端兼容逗号切分,或者前端和后端约定数组提交格式。uni-app 的 uploadFile 对数组字段支持不如普通 FormData 灵活,所以逗号字符串是移动端里比较稳的写法。