655 lines
15 KiB
Plaintext
655 lines
15 KiB
Plaintext
// pages/dynamic/publish.uts
|
||
<template>
|
||
<view class="page">
|
||
<!-- 顶部导航栏 -->
|
||
<view class="page-nav">
|
||
<view class="page-nav-left" @click="handleCancel">
|
||
<text class="cancel-text">取消</text>
|
||
</view>
|
||
<view class="page-nav-center">
|
||
<text class="nav-title">发布动态</text>
|
||
</view>
|
||
<view class="page-nav-right">
|
||
<button class="publish-btn" :class="{ active: canPublish }" @click="handlePublish">发布</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 内容输入区 -->
|
||
<view class="content-area">
|
||
<textarea class="content-input" placeholder="记录这一刻,晒给懂你的人" v-model="content" maxlength="250"
|
||
@input="updateCharCount"></textarea>
|
||
<text class="char-count">{{ charCount }}/250</text>
|
||
</view>
|
||
<!-- 图片上传区 -->
|
||
<view class="media-area">
|
||
<view class="upload-grid">
|
||
<view v-for="(file, index) in files" :key="index" class="grid-item">
|
||
<image :src="file.path" class="grid-image" mode="aspectFill" />
|
||
<view class="delete-btn" @click="removeImage(index)">
|
||
<uni-icons type="closeempty" color="#fff"></uni-icons>
|
||
</view>
|
||
</view>
|
||
<view v-if="files.length < 9" class="grid-item add-item" @click="chooseMedia">
|
||
<!-- <view class="add-icon">+</view> -->
|
||
<uni-icons type="plusempty" size="48"></uni-icons>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 功能选项区 -->
|
||
<view class="function-area">
|
||
<view class="function-item" @click="chooseLocation">
|
||
<view class="item-left">
|
||
<image src="/static/icons/location.png" class="item-icon" />
|
||
<text class="item-label">所在位置</text>
|
||
</view>
|
||
<view class="item-right">
|
||
<text class="item-value">{{ location ?? '未选择' }}</text>
|
||
<image src="/static/icons/arrow-right.png" class="arrow-icon" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="function-item" @click="showTopicDialog">
|
||
<view class="item-left">
|
||
<image src="/static/icons/topic.png" class="item-icon" />
|
||
<text class="item-label">添加话题</text>
|
||
</view>
|
||
<view class="item-right">
|
||
<text class="item-value">{{ topics.length > 0 ? `#${topics.join(' #')}` : '选择话题会获得更多互动哦' }}</text>
|
||
<image src="/static/icons/arrow-right.png" class="arrow-icon" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="function-item voice-item" @click="toggleRecording">
|
||
<view class="item-left">
|
||
<image :src="isRecording ? '/static/icons/voice-recording.png' : '/static/icons/voice.png'"
|
||
class="item-icon" />
|
||
<text class="item-label">{{ isRecording ? '正在录音...' : '语音描述' }}</text>
|
||
</view>
|
||
<view class="item-right">
|
||
<view v-if="audioPath" class="audio-preview">
|
||
<image src="/static/icons/play.png" class="play-icon" @click.stop="playAudio" />
|
||
<text class="audio-duration">{{ audioDuration }}"</text>
|
||
</view>
|
||
<image v-else src="/static/icons/arrow-right.png" class="arrow-icon" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 话题选择弹窗 -->
|
||
<view v-if="showTopicSelector" class="topic-dialog">
|
||
<view class="dialog-mask" @click="hideTopicDialog"></view>
|
||
<view class="dialog-content">
|
||
<view class="dialog-header">
|
||
<text class="dialog-title">选择话题</text>
|
||
<text class="dialog-close" @click="hideTopicDialog">×</text>
|
||
</view>
|
||
<view class="topic-list">
|
||
<view v-for="(topic, index) in availableTopics" :key="index" class="topic-item"
|
||
@click="toggleTopic(topic)">
|
||
<text class="topic-text">#{{ topic }}</text>
|
||
<view class="topic-check" v-if="topics.includes(topic)">
|
||
<image src="/static/icons/checked.png" class="check-icon" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="dialog-footer">
|
||
<button class="confirm-btn" @click="hideTopicDialog">完成</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 位置选择弹窗 -->
|
||
<view v-if="showLocationSelector" class="location-dialog">
|
||
<view class="dialog-mask" @click="hideLocationDialog"></view>
|
||
<view class="dialog-content">
|
||
<view class="dialog-header">
|
||
<text class="dialog-title">选择位置</text>
|
||
<text class="dialog-close" @click="hideLocationDialog">×</text>
|
||
</view>
|
||
<view class="search-box">
|
||
<input v-model="locationSearch" placeholder="搜索地点" class="search-input" />
|
||
</view>
|
||
<view class="location-list">
|
||
<view v-for="(loc, index) in filteredLocations" :key="index" class="location-item"
|
||
@click="selectLocation(loc)">
|
||
<text class="location-name">{{ loc.name }}</text>
|
||
<text class="location-address">{{ loc.address }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import httpApi, { type IFileInfo } from '@/utlis/http-api'
|
||
import type { IUploadOption } from '@/types/http.uts'
|
||
import Api from '@/common/api-service.uts'
|
||
// 响应式数据
|
||
type locationItem = { name : string, address : string }
|
||
const content = ref<string>('')
|
||
const charCount = ref<number>(0)
|
||
const location = ref<string>('')
|
||
const topics = ref<Array<string>>([])
|
||
const audioPath = ref<string>('')
|
||
const audioDuration = ref<number>(0)
|
||
const isRecording = ref<boolean>(false)
|
||
const showTopicSelector = ref<boolean>(false)
|
||
const showLocationSelector = ref<boolean>(false)
|
||
const locationSearch = ref<string>('')
|
||
const recorderManager = ref(null as RecorderManager | null)
|
||
const files = ref<IFileInfo[]>([])
|
||
// 可用话题列表
|
||
const availableTopics = ref<Array<string>>([
|
||
'美食分享', '厨房日记', '烹饪技巧', '家常菜', '烘焙时光',
|
||
'美食探店', '食材百科', '健康饮食', '快手菜', '创意料理'
|
||
])
|
||
|
||
// 位置列表
|
||
const locations = ref<Array<locationItem>>([
|
||
{ name: '北京市朝阳区三里屯', address: '北京市朝阳区三里屯街道' },
|
||
{ name: '上海市黄浦区外滩', address: '上海市黄浦区外滩街道' },
|
||
{ name: '广州市天河区珠江新城', address: '广州市天河区珠江新城' },
|
||
{ name: '深圳市南山区科技园', address: '深圳市南山区科技园' },
|
||
{ name: '成都市锦江区春熙路', address: '成都市锦江区春熙路' }
|
||
])
|
||
|
||
// 计算属性
|
||
const canPublish = computed(() => {
|
||
return content.value.trim().length > 0 || files.value.length > 0 || audioPath.value !== ''
|
||
})
|
||
|
||
const filteredLocations = computed<locationItem[]>(() => {
|
||
if (locationSearch.value != null) return locations.value
|
||
return locations.value.filter(loc =>
|
||
loc.name.includes(locationSearch.value) || loc.address.includes(locationSearch.value)
|
||
)
|
||
})
|
||
|
||
// 生命周期钩子
|
||
onLoad(() => {
|
||
// 页面加载时的初始化操作
|
||
//updateCharCount()
|
||
})
|
||
|
||
// 方法定义
|
||
function updateCharCount() {
|
||
charCount.value = content.value.length
|
||
}
|
||
|
||
function chooseMedia() {
|
||
// #ifndef WEB
|
||
uni.chooseMedia({
|
||
count: 9 - files.value.length,
|
||
mediaType: ['image', 'video'],
|
||
sourceType: ['album', 'camera'],
|
||
success: (res) => {
|
||
console.log(res)
|
||
res.tempFiles.forEach((path : ChooseMediaTempFile) => {
|
||
files.value.push({
|
||
index: files.value.length ,
|
||
name: 'file' + (files.value.length ),
|
||
path: path.tempFilePath,
|
||
} as IFileInfo)
|
||
})
|
||
}
|
||
})
|
||
// #endif
|
||
// #ifdef WEB
|
||
uni.chooseImage({
|
||
count: 9 - files.value.length,
|
||
sizeType: ['compressed'],
|
||
sourceType: ['album', 'camera'],
|
||
success: (res) => {
|
||
console.log(res)
|
||
res.tempFilePaths.forEach((file) => {
|
||
files.value.push({
|
||
index: files.value.length ,
|
||
name: 'file' + (files.value.length ),
|
||
path: file,
|
||
} )
|
||
})
|
||
}
|
||
})
|
||
// #endif
|
||
}
|
||
|
||
function removeImage(index : number) {
|
||
files.value.splice(index, 1)
|
||
}
|
||
|
||
function chooseLocation() {
|
||
showLocationSelector.value = true
|
||
}
|
||
function showTopicDialog() {
|
||
showTopicSelector.value = true
|
||
}
|
||
|
||
function hideTopicDialog() {
|
||
showTopicSelector.value = false
|
||
}
|
||
|
||
function hideLocationDialog() {
|
||
showLocationSelector.value = false
|
||
locationSearch.value = ''
|
||
}
|
||
|
||
function selectLocation(loc : locationItem) {
|
||
location.value = loc.name
|
||
hideLocationDialog()
|
||
}
|
||
|
||
function toggleTopic(topic : string) {
|
||
const index = topics.value.indexOf(topic)
|
||
if (index > -1) {
|
||
topics.value.splice(index, 1)
|
||
} else {
|
||
topics.value.push(topic)
|
||
}
|
||
}
|
||
let innerAudioContext : any | null = null
|
||
function startRecording() {
|
||
isRecording.value = true
|
||
audioPath.value = ''
|
||
audioDuration.value = 0
|
||
// 创建录音管理器
|
||
recorderManager.value = uni.getRecorderManager();
|
||
|
||
// 录音结束事件
|
||
recorderManager.value!.onStop((res) => {
|
||
console.log(res);
|
||
// isRecording.value = false
|
||
// audioPath.value = res.tempFilePath
|
||
// audioDuration.value = Math.floor(res.duration as number / 1000) // 转换为秒
|
||
})
|
||
|
||
// 开始录音
|
||
recorderManager.value!.start({
|
||
duration: 60000, // 最长60秒
|
||
sampleRate: 44100,
|
||
numberOfChannels: 1,
|
||
encodeBitRate: 192000,
|
||
format: 'mp3'
|
||
})
|
||
}
|
||
|
||
function stopRecording() {
|
||
if (recorderManager != null) {
|
||
recorderManager.value!.stop()
|
||
}
|
||
}
|
||
|
||
function toggleRecording() {
|
||
if (isRecording.value) {
|
||
stopRecording()
|
||
} else {
|
||
startRecording()
|
||
}
|
||
}
|
||
function playAudio() {
|
||
// if (!innerAudioContext) {
|
||
// innerAudioContext = uni.createInnerAudioContext()
|
||
// }
|
||
|
||
// if (innerAudioContext.paused) {
|
||
// innerAudioContext.src = audioPath.value
|
||
// innerAudioContext.play()
|
||
// } else {
|
||
// innerAudioContext.pause()
|
||
// }
|
||
}
|
||
|
||
function handleCancel() {
|
||
uni.navigateBack()
|
||
}
|
||
async function handlePublish() {
|
||
if (!canPublish.value) return
|
||
uni.showLoading({ title: '发布中...', mask: true })
|
||
try {
|
||
// 1. 上传图片/视频,拿到可访问 URL
|
||
let media : UTSJSONObject[] = []
|
||
let hasVideo = false
|
||
if (files.value.length > 0) {
|
||
const uploaded = await httpApi.uploadMultipleFilesParallel({
|
||
url: '/wxchat/api/moment/upload'
|
||
} as IUploadOption, files.value)
|
||
uploaded.forEach((f) => {
|
||
const p = f.path
|
||
const isVideo = p.endsWith('.mp4') || p.endsWith('.mov') || p.endsWith('.m4v') || p.endsWith('.avi')
|
||
if (isVideo) hasVideo = true
|
||
media.push({ type: isVideo ? 2 : 1, url: p } as UTSJSONObject)
|
||
})
|
||
}
|
||
// 2. 计算动态类型:1 文字 / 2 图片 / 3 视频
|
||
let type = 1
|
||
if (hasVideo) {
|
||
type = 3
|
||
} else if (media.length > 0) {
|
||
type = 2
|
||
}
|
||
// 3. 提交发布
|
||
await Api.moment.create({
|
||
content: content.value.trim(),
|
||
type: type,
|
||
is_public: 1,
|
||
location: location.value,
|
||
media: media,
|
||
hashtags: topics.value
|
||
})
|
||
uni.hideLoading()
|
||
uni.showToast({ title: '发布成功', icon: 'success' })
|
||
setTimeout(() => {
|
||
uni.navigateBack()
|
||
}, 1200)
|
||
} catch (error) {
|
||
uni.hideLoading()
|
||
uni.showToast({ title: '发布失败', icon: 'none' })
|
||
console.log('发布失败:', error)
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
/* 内容输入区 */
|
||
.content-area {
|
||
margin-top: 100rpx;
|
||
position: relative;
|
||
padding: 16px;
|
||
background-color: #fff;
|
||
margin-bottom: 8px;
|
||
|
||
.content-input {
|
||
width: 100%;
|
||
min-height: 120px;
|
||
font-size: 16px;
|
||
line-height: 1.5;
|
||
color: #333;
|
||
border: none;
|
||
}
|
||
|
||
.char-count {
|
||
position: absolute;
|
||
right: 16px;
|
||
bottom: 8px;
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
/* 图片上传区 */
|
||
.media-area {
|
||
padding: 16px;
|
||
background-color: #fff;
|
||
margin-bottom: 8px;
|
||
|
||
.upload-grid {
|
||
display: flex;
|
||
flex-flow: row wrap;
|
||
border-radius: 25rpx;
|
||
overflow: hidden;
|
||
background: none;
|
||
|
||
.grid-item {
|
||
position: relative;
|
||
border-radius: 4px;
|
||
margin: 3rpx;
|
||
overflow: hidden;
|
||
background-color: #f5f5f5;
|
||
flex: 0 0 222rpx;
|
||
height: 222rpx;
|
||
background-color: #f5f5f5;
|
||
overflow: hidden;
|
||
position: relative;
|
||
box-sizing: border-box;
|
||
|
||
.grid-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
transition: transform 0.3s;
|
||
}
|
||
|
||
.delete-btn {
|
||
position: absolute;
|
||
top: 10rpx;
|
||
right: 10rpx;
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
&.add-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 1px dashed #ccc;
|
||
|
||
.add-icon {
|
||
font-size: 32px;
|
||
color: #999;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 功能选项区 */
|
||
.function-area {
|
||
background-color: #fff;
|
||
margin-bottom: 8px;
|
||
|
||
.function-item {
|
||
display: flex;
|
||
flex-flow: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 16px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.item-left {
|
||
display: flex;
|
||
flex-flow: row;
|
||
align-items: center;
|
||
|
||
.item-icon {
|
||
width: 20px;
|
||
height: 20px;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.item-label {
|
||
font-size: 16px;
|
||
color: #333;
|
||
}
|
||
}
|
||
|
||
.item-right {
|
||
display: flex;
|
||
flex-flow: row;
|
||
align-items: center;
|
||
|
||
.item-value {
|
||
font-size: 14px;
|
||
color: #999;
|
||
margin-right: 8px;
|
||
max-width: 200px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.arrow-icon {
|
||
width: 16px;
|
||
height: 16px;
|
||
}
|
||
|
||
.audio-preview {
|
||
display: flex;
|
||
flex-flow: row;
|
||
align-items: center;
|
||
|
||
.play-icon {
|
||
width: 20px;
|
||
height: 20px;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.audio-duration {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
}
|
||
}
|
||
|
||
&.voice-item {
|
||
.item-label {
|
||
color: #ff2442;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 弹窗样式 */
|
||
.topic-dialog,
|
||
.location-dialog {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 1000;
|
||
|
||
.dialog-mask {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
.dialog-content {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
background-color: #fff;
|
||
border-radius: 12px 12px 0 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.dialog-header {
|
||
display: flex;
|
||
flex-flow: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 16px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
|
||
.dialog-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.dialog-close {
|
||
font-size: 24px;
|
||
color: #999;
|
||
width: 30px;
|
||
height: 30px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
|
||
.topic-list,
|
||
.location-list {
|
||
flex: 1;
|
||
padding: 16px;
|
||
|
||
.topic-item,
|
||
.location-item {
|
||
display: flex;
|
||
flex-flow: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.topic-text {
|
||
font-size: 16px;
|
||
color: #333;
|
||
}
|
||
|
||
.topic-check {
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 50%;
|
||
background-color: #ff2442;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
.check-icon {
|
||
width: 12px;
|
||
height: 8px;
|
||
}
|
||
}
|
||
|
||
.location-name {
|
||
font-size: 16px;
|
||
color: #333;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.location-address {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
}
|
||
}
|
||
|
||
.search-box {
|
||
padding: 16px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
|
||
.search-input {
|
||
width: 100%;
|
||
height: 36px;
|
||
padding: 0 12px;
|
||
border-radius: 18px;
|
||
background-color: #f5f5f5;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
.dialog-footer {
|
||
padding: 16px;
|
||
border-top: 1px solid #f0f0f0;
|
||
|
||
.confirm-btn {
|
||
width: 100%;
|
||
height: 44px;
|
||
background-color: #ff2442;
|
||
color: #fff;
|
||
font-size: 16px;
|
||
border-radius: 22px;
|
||
border: none;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style> |