这个是RescueReleaseHome页面的内容模块<template> <view :clas...
تم الإنشاء في: ٢١ يناير ٢٠٢٦
تمت الإجابة باستخدام GPT-5.2 Thinking بواسطة Chat01
تم الإنشاء في: ٢١ يناير ٢٠٢٦
تمت الإجابة باستخدام GPT-5.2 Thinking بواسطة Chat01
这个是RescueReleaseHome页面的内容模块<template>
<view :class="['app-container', theme]">
<view class="content">
<view class="overlay">
<!-- 顶部栏 -->
<view class="header" :style="{ paddingTop: statusBarHeight + 'px', height: headerBarHeight + 'px' }">
<view class="header-inner">
<view class="left-box" @tap="navigateBack">
<uni-icons type="left" color="#000000" size="26"></uni-icons>
</view>
</template> <script setup> import { Getcontacts, Gettopics } from '@/api/api.js' import { ref, computed, reactive, onMounted } from 'vue' import { storeToRefs } from 'pinia' import { useMainStore } from '@/store/index.js' import { onLoad } from '@dcloudio/uni-app' 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 statusBarHeight = ref(getStatusBarHeight()) const headerBarHeight = ref(getTitleBarHeight()) /** ====================== * 文本输入 ====================== */ 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([]) // tagIds const FriendID = ref('') // 你原来只存一个 friendId:保留 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) => { textAreaContent.value += topic?.name ? topic.name : '' showTopics.value = false if (topic?.id != null) { const id = String(topic.id) if (!selectedTopicIds.value.includes(id)) selectedTopicIds.value.push(id) } } const handleSelectFriend = (friend) => { textAreaContent.value += friend?.nickname ? '@' + friend.nickname : friend?.name ? '@' + friend.name : '' showFriends.value = false FriendID.value = friend?.id || '' } /** ====================== * 获取朋友/话题 ====================== */ let consumerId = ref(uni.getStorageSync('consumerId') || '') let option = ref({ consumerId: consumerId.value, keyword: '', rowStartIdx: 0, rowCount: '20' }) async function triggerfunction() { try { let res = await Getcontacts(option.value) if (res?.result === 'success') { friends.value = res.friendArray || [] } } catch (e) { uni.showToast({ title: '好友列表获取失败', icon: 'none' }) } } let params = { keyword: '', rowStartIdx: 0, rowCount: '20' } const TopicDetails = async () => { try { let res = await Gettopics(params) topics.value = res.tagArray || [] } catch (e) { uni.showToast({ title: '话题获取失败', icon: 'none' }) } } onMounted(() => { triggerfunction() TopicDetails() }) /** ====================== * 上传(uview) ====================== */ const TemporaryValue = ref('image') const fileList1 = 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: '等待上传' }) }) } // 切换上传类型(图片/视频) const selectsfile = () => { if (publishing.value) return uni.showActionSheet({ itemList: ['上传图片', '上传视频'], itemColor: "#000000", success: (res) => { TemporaryValue.value = res.tapIndex === 1 ? 'video' : 'image' uni.showToast({ title: `已切换为${TemporaryValue.value === 'video' ? '视频' : '图片'}`, icon: 'none' }) }, fail: () => { uni.showToast({ title: '取消选择', icon: 'none' }) } }) } /** ====================== * 修复核心:安全解析 JSON ====================== */ 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() // HTML / 错误页 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 } /** ====================== * 发布 + 上传 + 审核 ====================== */ 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 UPLOAD_URL = 'http://47.99.182.213:7018/applet/circle' const uploadFilePromise = (filePath) => { return new Promise((resolve, reject) => { uni.uploadFile({ url: UPLOAD_URL, filePath, name: 'circleFile', header: { "Content-Type": "multipart/form-data" }, formData: { consumerId: consumerId.value, content: textAreaContent.value, id: WorkID.value, tagIds: selectedTopicIds.value.join(','), // ✅ 支持话题id列表 friendIds: FriendID.value }, success: (res) => { // ✅ 修复:不再直接 JSON.parse 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) }) }) } // 审核(保留你原逻辑) function Reviewworks() { const option = { id: WorkID.value, state: 2 }; ajax.post({ // 2. 发请求 url: 'updateCircleWithState', data: option, method: 'post' }) .then(() => { // 3. 成功 uni.hideLoading(); uni.showToast({ title: '已提交审核', icon: 'none' }); // 4. 1.5 秒后自动关掉当前页面(“自己消失”) setTimeout(() => uni.navigateBack({ delta: 1 }), 1500); }) .catch(err => { // 5. 失败也要回退 uni.hideLoading(); uni.showModal({ content: '提交失败:' + (err.msg || err), showCancel: false }); // 如果你想“不管怎样都消失”,可以也在这里 navigateBack }); } const handlePublish = async () => { if (publishing.value) return const readyFiles = fileList1.value.filter(f => f.status === 'ready') 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 { // ✅ 逐个上传(稳) 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 = '' // 兼容后端可能返回 url if (res?.url) file.url = res.url } catch (err) { file.status = 'failed' file.message = '上传失败' // ✅ 提示更明确 const msg = err?.message || '上传失败' uni.hideLoading() uni.showModal({ title: '上传失败', content: '原因:' + msg + '\n\n' + '常见原因:\n' + '1)UPLOAD_URL 写错 / 后端返回了 HTML 错误页\n' + '2)服务 502/404\n' + '3)后端返回不是 JSON', showCancel: false }) throw err } } uni.hideLoading() uni.showToast({ title: '发布成功', icon: 'success' }) // ✅ 成功后审核 setTimeout(() => { Reviewworks() uni.navigateBack() }, 500) } catch (e) { uni.hideLoading() } finally { publishing.value = false } } /** ====================== * 下面保留你原来的视频相关逻辑(不删) ====================== */ const videoSrc = ref('') const showFullBtn = ref(false) const type = ref(2) const Id = ref('') const filmId = ref('') const datasource = reactive([]) const StoredVideo = reactive([]) const isvdieo = ref(true) onLoad(() => {}) const publish = () => { if (!textAreaContent.value.trim()) { uni.showToast({ title: '请填写描述', icon: 'none' }) return false } if (!videoSrc.value) { uni.showToast({ title: '请上传视频', icon: 'none' }) return false } uni.showToast({ title: '内容已发布', icon: 'success' }) return true } const startRecord = () => { const payload = { id: Id.value, filmId: filmId.value, consumerId: consumerId.value, type: type.value, content: textAreaContent.value, talkingPointIds: '今天您吃了嘛', friendIds: '奥特曼', explainActFile: videoSrc.value } return payload } const uploadVideo = () => { if (!publish()) return const url = 'http://47.99.182.213:7018/applet/explainAct' uni.showLoading({ title: '上传中...', mask: true }) uni.uploadFile({ url, filePath: videoSrc.value, name: 'explainActFile', formData: { id: Id.value, filmId: filmId.value, consumerId: consumerId.value, type: type.value, content: textAreaContent.value, talkingPointIds: '今天您吃了嘛', friendIds: '奥特曼' }, success: (res) => { uni.hideLoading() uni.showToast({ title: '上传成功', icon: 'success' }) setTimeout(() => { uni.navigateTo({ url: '/pages-LoadDemand/myStarPathPage/StarRoadhomepage/classicInheritance/releasePage/brushingVideos' }) }, 1200) }, fail: () => { uni.hideLoading() uni.showToast({ title: '上传失败', icon: 'none' }) } }) startRecord() } /** ====================== * 其他:返回/说明 ====================== */ const navigateBack = () => uni.navigateBack() const showHelp = () => { uni.showModal({ title: '发布说明', content: '1)可输入描述并添加话题/@朋友。\n' + '2)可上传图片或视频(最多9个)。\n' + '3)发布时会逐个上传,失败会提示原因。\n' + '4)如果提示“非JSON”,大概率是接口地址错误或服务器返回HTML错误页。\n', showCancel: false }) } </script> <style lang="scss" scoped> @import '@/assets/styles/global.scss'; .content { min-height: 100vh; position: relative; background-size: cover; } .overlay { position: absolute; inset: 0; background-size: cover; } /* 顶部栏 */ .header { width: 94%; margin: 0 auto; } .header-inner { position: relative; display: flex; align-items: center; justify-content: center; height: 100%; } .left-box, .right-box { position: absolute; top: 0; bottom: 0; width: 120rpx; display: flex; align-items: center; } .left-box { left: 0; justify-content: flex-start; } .right-box { right: 0; justify-content: flex-end; } .scroll-wrap { width: 100%; height: 88vh; } /* 页面内容 */ .page-body { width: 94%; margin: 0 auto; padding-bottom: 24rpx; } .describe { width: 100%; .texter { position: relative; border-radius: 20rpx; box-shadow: 0 2rpx 14rpx rgba(0, 0, 0, 0.06); background: #fff; padding: 20rpx; min-height: 240rpx; } 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; } } .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; } </style>把里面的方法给改成1.rescueCircle维护、修改、发布text<view class="center-box"> <text class="title-text">发布作品</text> </view> <view class="right-box" @tap="showHelp"> <text class="desc-text">说明</text> </view> </view> </view> <!-- 可滚动内容区域 --> <scroll-view scroll-y class="scroll-wrap"> <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" /> <!-- 视频预览(保留,你原来没真正赋值 videoSrc,这里不动逻辑) --> <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="upload-card"> <view class="upload-title"> 添加视频/图片 <text class="upload-sub">(最多 9 个)</text> </view> <!-- ✅ 不要再外层包 @click,安卓会出现上传组件事件冲突 --> <up-upload :fileList="fileList1" @afterRead="afterRead" :accept="TemporaryValue" @delete="deletePic" name="1" multiple :maxCount="9"></up-upload> <!-- 上传状态提示 --> <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"></uni-icons> <text class="publish-text">{{ publishing ? publishProgressText : '发布' }}</text> </view> </view> </view> <view style="height: 36rpx;"></view> </view> </scroll-view> </view> </view> </view>
text入口参数 String consumerId, 用户id String content, 内容 String id, 动态id
String[] friendIds, 动态@好友id数组
String[] tagIds, 话题id
Integer type
String rescueCircleFile
返回值
成功:id 动态id
失败:错误提示
这个方法 其他的上穿方法用不到了 都不要了 然后写一个最完整的超级页面的内容模块
下面是把你这个页面里所有“上传/发布/审核/视频上传”相关方法全部收敛成**一个 rescueCircle(维护/修改/发布)**后的“最完整超级页面内容模块”(SFC)。
只保留:文本、#话题、@朋友、图片/视频选择(最多9个)、逐个上传、失败提示、支持无文件纯文字发布。
你只需要把RESCUE_CIRCLE_URL换成你后端真实接口地址即可。
vue<template> <view :class="['app-container', theme]"> <view class="content"> <view class="overlay"> <!-- 顶部栏 --> <view class="header" :style="{ paddingTop: statusBarHeight + 'px', height: headerBarHeight + 'px' }"> <view class="header-inner"> <view class="left-box" @tap="navigateBack"> <uni-icons type="left" color="#000000" size="26"></uni-icons> </view> <view class="center-box"> <text class="title-text">发布作品</text> </view> <view class="right-box" @tap="showHelp"> <text class="desc-text">说明</text> </view> </view> </view> <!-- 可滚动内容区域 --> <scroll-view scroll-y class="scroll-wrap"> <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"> 上传类型:{{ uploadMode === 'video' ? '视频' : '图片' }} </view> <view class="chip ghost" v-if="selectedFriendIds.length" @tap="clearFriend"> 清空@({{ selectedFriendIds.length }}) </view> <view class="chip ghost" v-if="selectedTopicIds.length" @tap="clearTopic"> 清空话题({{ selectedTopicIds.length }}) </view> </view> <!-- 话题组件 --> <TopicList v-if="showTopics" :topics="topics" @select-topic="handleSelectTopic" /> <!-- 朋友组件 --> <FriendList v-if="showFriends" :friends="friends" @select-friend="handleSelectFriend" /> </view> <!-- 上传区域 --> <view class="upload-card"> <view class="upload-title"> 添加视频/图片 <text class="upload-sub">(最多 9 个)</text> </view> <up-upload :fileList="fileList1" @afterRead="afterRead" @delete="deletePic" name="1" multiple :maxCount="9" :accept="uploadMode" /> <!-- 上传状态提示 --> <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 class="upload-tip" v-if="failedCount > 0" style="margin-top: 8rpx;"> <text style="color:#D89833;" @tap="resetFailedToReady">点我重试失败项</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"></uni-icons> <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, reactive, onMounted } from 'vue' import { storeToRefs } from 'pinia' import { useMainStore } from '@/store/index.js' import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' import { Getcontacts, Gettopics } from '@/api/api.js' // 组件 import TopicList from '@/components/TopicList.vue' import FriendList from '@/components/FriendList.vue' /** ====================== * 主题 ====================== */ const store = useMainStore() const { theme } = storeToRefs(store) /** ====================== * 顶部安全区与标题栏 ====================== */ const statusBarHeight = ref(getStatusBarHeight()) const headerBarHeight = ref(getTitleBarHeight()) /** ====================== * 文本输入 ====================== */ 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([]) const friends = ref([]) // ✅ 改成数组:入口参数 friendIds / tagIds const selectedTopicIds = ref([]) // String[] tagIds const selectedFriendIds = ref([]) // String[] friendIds 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 name = topic?.name || '' const id = topic?.id != null ? String(topic.id) : '' if (name) textAreaContent.value += name showTopics.value = false if (id && !selectedTopicIds.value.includes(id)) selectedTopicIds.value.push(id) } const handleSelectFriend = (friend) => { const name = friend?.nickname || friend?.name || '' const id = friend?.id != null ? String(friend.id) : '' if (name) textAreaContent.value += '@' + name showFriends.value = false if (id && !selectedFriendIds.value.includes(id)) selectedFriendIds.value.push(id) } const clearFriend = () => { selectedFriendIds.value = [] } const clearTopic = () => { selectedTopicIds.value = [] } /** ====================== * 获取朋友/话题 ====================== */ const consumerId = ref(uni.getStorageSync('consumerId') || '') const contactOption = ref({ consumerId: consumerId.value, keyword: '', rowStartIdx: 0, rowCount: '20' }) async function fetchFriends() { try { const res = await Getcontacts(contactOption.value) if (res?.result === 'success') friends.value = res.friendArray || [] } catch (e) { uni.showToast({ title: '好友列表获取失败', icon: 'none' }) } } const topicParams = { keyword: '', rowStartIdx: 0, rowCount: '20' } async function fetchTopics() { try { const res = await Gettopics(topicParams) topics.value = res?.tagArray || [] } catch (e) { uni.showToast({ title: '话题获取失败', icon: 'none' }) } } onMounted(() => { fetchFriends() fetchTopics() }) /** ====================== * 上传选择(uview) ====================== */ const uploadMode = ref('image') // image | video const fileList1 = 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: '等待上传' }) }) } const selectsfile = () => { if (publishing.value) return uni.showActionSheet({ itemList: ['上传图片', '上传视频'], itemColor: "#000000", success: (res) => { const nextMode = res.tapIndex === 1 ? 'video' : 'image' if (uploadMode.value !== nextMode) { uploadMode.value = nextMode // ✅ 切换类型时建议清空已选文件(避免图片/视频混用) fileList1.value = [] uni.showToast({ title: `已切换为${uploadMode.value === 'video' ? '视频' : '图片'}`, icon: 'none' }) } }, fail: () => uni.showToast({ title: '取消选择', icon: 'none' }) }) } /** ====================== * JSON 解析 ====================== */ 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 } /** ====================== * ✅ 核心:rescueCircle(维护/修改/发布) * 入参: * String consumerId * String content * String id * String[] friendIds * String[] tagIds * Integer type * String rescueCircleFile * 返回: * 成功:id * 失败:错误提示 ====================== */ // ⚠️ 换成你真实的后端地址(支持 multipart) const RESCUE_CIRCLE_URL = 'http://47.99.182.213:7018/applet/rescueCircle' // 你的动态 id:后端第一次成功返回后要保存,用于后续“修改/追加文件” const workId = ref('') // type:这里给一个常见映射(你可按后端调整) // 1=图片 2=视频 const circleType = computed(() => (uploadMode.value === 'video' ? 2 : 1)) // 发布状态 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 resetFailedToReady = () => { fileList1.value.forEach(f => { if (f.status === 'failed') { f.status = 'ready' f.message = '等待上传' } }) } /** * 统一处理后端返回:尽量兼容多种结构 */ function extractIdFromResponse(data) { // 你可以按后端实际返回结构定制这里 // 兼容:{id:"xx"} / {result:"success", id:"xx"} / {data:{id:"xx"}} const id = data?.id || data?.data?.id || data?.resultData?.id return id != null ? String(id) : '' } /** * 无文件:纯文字发布/修改(uni.request) */ function rescueCircleNoFile() { return new Promise((resolve, reject) => { uni.request({ url: RESCUE_CIRCLE_URL, method: 'POST', header: { 'Content-Type': 'application/json' }, data: { consumerId: consumerId.value, content: textAreaContent.value, id: workId.value, friendIds: selectedFriendIds.value, // ✅ 数组 tagIds: selectedTopicIds.value, // ✅ 数组 type: circleType.value }, success: (res) => { const parsed = safeParseJSON(res?.data) if (!parsed.ok) { reject(new Error('接口返回非JSON:' + shortText(parsed.raw))) return } resolve(parsed.data || {}) }, fail: (err) => reject(err) }) }) } /** * 有文件:multipart 上传(uni.uploadFile) */ function rescueCircleUploadFile(filePath) { return new Promise((resolve, reject) => { uni.uploadFile({ url: RESCUE_CIRCLE_URL, filePath, name: 'rescueCircleFile', // ✅ 对应入参 rescueCircleFile header: { "Content-Type": "multipart/form-data" }, formData: { consumerId: consumerId.value, content: textAreaContent.value, id: workId.value, // ✅ 数组在 multipart 下常见做法:JSON 字符串(如后端不支持,请改成 join(',') 或 friendIds[0]...) friendIds: JSON.stringify(selectedFriendIds.value), tagIds: JSON.stringify(selectedTopicIds.value), type: String(circleType.value) }, success: (res) => { const parsed = safeParseJSON(res?.data) if (!parsed.ok) { reject(new Error('接口返回非JSON:' + shortText(parsed.raw))) return } resolve(parsed.data || {}) }, fail: (err) => reject(err) }) }) } /** * 发布:逐个文件调用 rescueCircle(稳) * - 第一个成功后保存 workId(后端返回 id) * - 后续文件带着 id,实现“维护/修改/追加” */ const handlePublish = async () => { if (publishing.value) return if (!consumerId.value) { uni.showToast({ title: '缺少consumerId,请先登录', icon: 'none' }) return } const readyFiles = fileList1.value.filter(f => f.status === 'ready') const hasText = !!textAreaContent.value.trim() if (readyFiles.length === 0 && !hasText) { uni.showToast({ title: '请添加内容或图片/视频', icon: 'none' }) return } publishing.value = true publishIndex.value = 0 publishTotal.value = readyFiles.length uni.showLoading({ title: '发布中...', mask: true }) try { // 1) 如果没有文件,但有文字:直接调用一次(维护/发布) if (readyFiles.length === 0 && hasText) { const data = await rescueCircleNoFile() const newId = extractIdFromResponse(data) if (!newId) throw new Error(data?.msg || data?.message || '发布失败:未返回id') workId.value = newId uni.hideLoading() uni.showToast({ title: '发布成功', icon: 'success' }) setTimeout(() => uni.navigateBack({ delta: 1 }), 800) return } // 2) 有文件:逐个上传 for (let i = 0; i < readyFiles.length; i++) { const file = readyFiles[i] publishIndex.value = i + 1 file.status = 'uploading' file.message = '上传中...' try { const data = await rescueCircleUploadFile(file.url) const newId = extractIdFromResponse(data) if (newId) workId.value = newId // 如果后端返回了文件url,可回填 if (data?.url) file.url = data.url file.status = 'success' file.message = '' } catch (err) { file.status = 'failed' file.message = '上传失败' const msg = err?.message || '上传失败' uni.hideLoading() uni.showModal({ title: '上传失败', content: '原因:' + msg + '\n\n' + '常见原因:\n' + '1)接口地址错误 / 服务返回 HTML 错误页\n' + '2)服务 502/404\n' + '3)后端返回不是 JSON\n' + '4)friendIds/tagIds multipart 解析方式不一致(JSON vs 逗号拼接)', showCancel: false }) throw err } } uni.hideLoading() uni.showToast({ title: '发布成功', icon: 'success' }) setTimeout(() => uni.navigateBack({ delta: 1 }), 800) } catch (e) { uni.hideLoading() } finally { publishing.value = false } } /** ====================== * 其他:返回/说明 ====================== */ const navigateBack = () => uni.navigateBack({ delta: 1 }) const showHelp = () => { uni.showModal({ title: '发布说明(rescueCircle)', content: '本页已统一使用 rescueCircle 完成“维护/修改/发布”。\n\n' + '入参:consumerId、content、id、friendIds[]、tagIds[]、type、rescueCircleFile。\n' + '返回:成功=动态id,失败=错误提示。\n\n' + '提示:\n' + '1)最多上传9个文件,逐个上传更稳定。\n' + '2)首次成功后会保存动态id,后续文件自动带id实现“修改/追加”。\n' + '3)如果提示“非JSON”,大概率是接口错误或服务返回HTML。\n' + '4)multipart 下数组字段默认用 JSON.stringify 发送,若后端不兼容请改为 join(\',\')。', showCancel: false }) } </script> <style lang="scss" scoped> @import '@/assets/styles/global.scss'; .content { min-height: 100vh; position: relative; background-size: cover; } .overlay { position: absolute; inset: 0; background-size: cover; } /* 顶部栏 */ .header { width: 94%; margin: 0 auto; } .header-inner { position: relative; display: flex; align-items: center; justify-content: center; height: 100%; } .left-box, .right-box { position: absolute; top: 0; bottom: 0; width: 120rpx; display: flex; align-items: center; } .left-box { left: 0; justify-content: flex-start; } .right-box { right: 0; justify-content: flex-end; } .scroll-wrap { width: 100%; height: 88vh; } /* 页面内容 */ .page-body { width: 94%; margin: 0 auto; padding-bottom: 24rpx; } .describe { width: 100%; .texter { position: relative; border-radius: 20rpx; box-shadow: 0 2rpx 14rpx rgba(0, 0, 0, 0.06); background: #fff; padding: 20rpx; min-height: 240rpx; } 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; } } .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; } </style>
如果你告诉我后端 rescueCircle 真实返回结构(比如 {result:"success", id:"xxx"} 还是 {code:0, data:{id}}),以及 type 的取值规则(图片/视频分别是多少),我可以把 extractIdFromResponse() 和 circleType 映射改到完全吻合你接口的最终版。