这个是RescueReleaseHome页面的内容模块<template> <view :clas...

Creato il: 21 gennaio 2026

Risposto usando GPT-5.2 Thinking da Chat01

Domanda

这个是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>

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>
</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
入口参数 String consumerId, 用户id String content, 内容 String id, 动态id

String[] friendIds, 动态@好友id数组
String[] tagIds, 话题id
Integer type
String rescueCircleFile
返回值
成功:id 动态id
失败:错误提示
这个方法 其他的上穿方法用不到了 都不要了 然后写一个最完整的超级页面的内容模块

Pensiero

Mapping approach and implementation considerations

I need to decide on how to map data, focusing on whether friendIds and tagIds should be sent as JSON strings or repeated fields. I'll go with JSON.stringify() for now, adding a fallback to comma-separated values. Also, I'll simplify the code by consolidating old functions into a single handlePublish, ensuring everything is handled sequentially.

Handling file uploads and text-only posts

For handling multiple files, I’ll keep track of the ID from the first response and use it for subsequent upload requests. If there are no files, I’ll implement an option for a text-only post using a regular request. It seems we should support both a file and no-file option depending on the scenario. Additionally, I’ll update Getcontacts/Gettopics while leaving them intact since the user didn’t ask to remove them.

Finalizing requirements and implementing updates

The user wants to streamline upload methods, so I'll keep contacts and topics. The content module needs to be delivered as per their request. I’ll adjust the friend selection to allow multiple friends, add status counts for various states, and enable retry options for failed uploads. I’ll ensure proper toggles and use @tap. I’ll infer the rescueCircle API URL with a placeholder for the base URL.

Refining file upload and rescueCircle logic

I’ll set the URL as a placeholder and highlight the need to adjust it for the backend. We’ll handle the return JSON and ensure proper processing. The up-upload settings like accept for types will be preserved. For file handling, I'll manage the status and file list carefully, ensure the publish process is smooth, and account for file-specific behaviors like progress and file-toggling. I'll implement separate functions for file and no-file cases with clear handling for both scenarios.

Implementing the unified rescueCircle call

I’ll unify the method to handle both file and no-file cases. For no file, the data will be sent via JSON using uni.request, while file uploads will use `upload

Thought for 1m 16s

Risposta

下面是把你这个页面里所有“上传/发布/审核/视频上传”相关方法全部收敛成**一个 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 映射改到完全吻合你接口的最终版。

Condividi questa Q&A