7 changed files with 1585 additions and 258 deletions
@ -0,0 +1,414 @@ |
|||
<template> |
|||
<view class="my-space-page"> |
|||
<view class="top-safe" :style="{ height: menuButtonInfo.top + 'px' }"></view> |
|||
<view class="float-bar"> |
|||
<view class="home-back" @tap="back"> |
|||
<text class="home-back-icon">‹</text> |
|||
<text>返回资料</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="space-hero"> |
|||
<image class="hero-avatar-img" v-if="profile.avatarUrl" :src="profile.avatarUrl" mode="aspectFill"></image> |
|||
<view class="hero-avatar" v-else>{{ profile.avatarText || '我' }}</view> |
|||
<view class="hero-main"> |
|||
<view class="hero-title-row"> |
|||
<view class="hero-title">我的个人空间</view> |
|||
<view class="publish-btn" @tap="openComposer">+ 发动态</view> |
|||
</view> |
|||
<view class="hero-sub">{{ profile.anonymousName || '半匿名漂流者' }} · {{ profile.currentMode === 'e' ? '轻轻热闹' : '安静陪伴' }}</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="moment-empty" v-if="!moments.length && !loadingMoments">还没有动态,发一条让同频的人认识你</view> |
|||
<view class="moment-card" v-for="item in moments" :key="item.id"> |
|||
<view class="moment-top"> |
|||
<text class="moment-time">{{ momentTime(item.createTime) }}</text> |
|||
<text class="moment-state blocked" v-if="item.localState === 'failed'">{{ item.failTip || '发布失败' }}</text> |
|||
<text class="moment-state auditing" v-else-if="item.status === 0">审核中</text> |
|||
<text class="moment-state blocked" v-else-if="item.status === 2">未通过审核</text> |
|||
<text class="moment-del" @tap="removeMoment(item)">删除</text> |
|||
</view> |
|||
<view class="moment-text" v-if="item.content">{{ item.content }}</view> |
|||
<view class="moment-grid" v-if="item.imageList && item.imageList.length" :class="'g' + item.imageList.length"> |
|||
<image v-for="(img, idx) in item.imageList" :key="idx" :src="img" mode="aspectFill" @tap="previewMomentImages(item, idx)"></image> |
|||
</view> |
|||
<view class="moment-video" v-if="item.videoUrl" @tap="playMomentVideo(item)"> |
|||
<image class="moment-video-poster" v-if="item.videoPoster" :src="item.videoPoster" mode="aspectFill"></image> |
|||
<view class="moment-video-holder" v-else></view> |
|||
<view class="moment-play">▶</view> |
|||
</view> |
|||
</view> |
|||
<view class="moment-more" v-if="loadingMoments">加载中...</view> |
|||
<view class="moment-more" v-else-if="hasMoreMoments && moments.length" @tap="loadMoments(false)">查看更早的动态</view> |
|||
<view class="moment-more" v-else-if="!hasMoreMoments && moments.length">没有更多动态了</view> |
|||
|
|||
<view class="composer-mask" v-if="showComposer" @tap="closeComposer"> |
|||
<view class="composer" @tap.stop> |
|||
<view class="composer-title">发动态</view> |
|||
<textarea class="composer-input" v-model="composeText" maxlength="1000" |
|||
placeholder="此刻想说点什么..." :show-confirm-bar="false"></textarea> |
|||
<view class="composer-grid"> |
|||
<view class="composer-thumb" v-for="(img, idx) in composeImages" :key="idx" @tap="retryComposeImage(img)"> |
|||
<image :src="img.local" mode="aspectFill"></image> |
|||
<view class="thumb-state" v-if="img.status === 'uploading'">上传中</view> |
|||
<view class="thumb-state failed" v-else-if="img.status === 'failed'">失败·点我重试</view> |
|||
<view class="thumb-del" @tap.stop="removeComposeImage(idx)">×</view> |
|||
</view> |
|||
<view class="composer-thumb" v-if="composeVideo" @tap="retryComposeVideo"> |
|||
<image v-if="composeVideo.poster" :src="composeVideo.poster" mode="aspectFill"></image> |
|||
<view class="thumb-video-mark">▶ 视频</view> |
|||
<view class="thumb-state" v-if="composeVideo.status === 'uploading'">上传中</view> |
|||
<view class="thumb-state failed" v-else-if="composeVideo.status === 'failed'">失败·点我重试</view> |
|||
<view class="thumb-del" @tap.stop="composeVideo = null">×</view> |
|||
</view> |
|||
<view class="composer-add" v-if="canAddMedia" @tap="chooseMomentMedia">+</view> |
|||
</view> |
|||
<view class="composer-tip">最多 5 张图片,或 1 个视频 · 内容需通过审核</view> |
|||
<view class="composer-submit" :class="{ disabled: publishing }" @tap="submitMoment">发布</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { getIeProfile, pageIeMoments, publishIeMoment, deleteIeMoment } from '@/common/ieApi.js' |
|||
import tui from '@/common/httpRequest.js' |
|||
|
|||
export default { |
|||
data() { |
|||
return { |
|||
menuButtonInfo: { top: 44 }, |
|||
profile: {}, |
|||
moments: [], |
|||
momentPage: 1, |
|||
hasMoreMoments: true, |
|||
loadingMoments: false, |
|||
showComposer: false, |
|||
composeText: '', |
|||
composeImages: [], |
|||
composeVideo: null, |
|||
publishing: false |
|||
} |
|||
}, |
|||
computed: { |
|||
canAddMedia() { |
|||
if (this.composeVideo) return false |
|||
return this.composeImages.length < 5 |
|||
} |
|||
}, |
|||
onLoad() { |
|||
if (uni.getMenuButtonBoundingClientRect) this.menuButtonInfo = uni.getMenuButtonBoundingClientRect() |
|||
this.loadProfile() |
|||
this.loadMoments(true) |
|||
}, |
|||
onReachBottom() { |
|||
this.loadMoments(false) |
|||
}, |
|||
async onPullDownRefresh() { |
|||
try { |
|||
await Promise.all([ |
|||
this.loadProfile(), |
|||
this.loadMoments(true, true) |
|||
]) |
|||
} finally { |
|||
uni.stopPullDownRefresh() |
|||
} |
|||
}, |
|||
methods: { |
|||
back() { |
|||
uni.navigateBack({ fail: () => uni.redirectTo({ url: '/package1/ieBrowser/universe' }) }) |
|||
}, |
|||
async loadProfile() { |
|||
const profile = await getIeProfile() |
|||
if (profile) this.profile = profile |
|||
}, |
|||
async loadMoments(reset, force = false) { |
|||
if (this.loadingMoments && !force) return |
|||
if (!reset && !this.hasMoreMoments) return |
|||
this.loadingMoments = true |
|||
try { |
|||
const page = reset ? 1 : this.momentPage |
|||
const result = await pageIeMoments(null, page, 10) |
|||
const records = (result && result.records) || [] |
|||
this.moments = reset ? records : this.moments.concat(records) |
|||
this.hasMoreMoments = records.length >= 10 |
|||
this.momentPage = page + 1 |
|||
} finally { |
|||
this.loadingMoments = false |
|||
} |
|||
}, |
|||
momentTime(timeStr) { |
|||
if (!timeStr) return '' |
|||
const time = new Date(String(timeStr).replace(/-/g, '/')).getTime() |
|||
if (!time) return '' |
|||
const diff = Date.now() - time |
|||
if (diff < 60000) return '刚刚' |
|||
if (diff < 3600000) return Math.floor(diff / 60000) + ' 分钟前' |
|||
if (diff < 86400000) return Math.floor(diff / 3600000) + ' 小时前' |
|||
if (diff < 172800000) return '昨天' |
|||
const d = new Date(time) |
|||
return (d.getMonth() + 1) + '月' + d.getDate() + '日' |
|||
}, |
|||
previewMomentImages(item, index) { |
|||
uni.previewImage({ urls: item.imageList || [], current: item.imageList[index] }) |
|||
}, |
|||
playMomentVideo(item) { |
|||
if (item.status === 0) { |
|||
uni.showToast({ title: '视频审核中,通过后对方可见', icon: 'none' }) |
|||
} |
|||
if (!item.videoUrl) return |
|||
uni.navigateTo({ url: '/package1/ieBrowser/videoPreview?url=' + encodeURIComponent(item.videoUrl) }) |
|||
}, |
|||
removeMoment(item) { |
|||
if (String(item.id).indexOf('local-') === 0) { |
|||
this.moments = this.moments.filter(m => m.id !== item.id) |
|||
return |
|||
} |
|||
uni.showModal({ |
|||
title: '删除动态', |
|||
content: '删除后无法恢复,确定删除吗?', |
|||
success: async (res) => { |
|||
if (!res.confirm) return |
|||
await deleteIeMoment(item.id) |
|||
this.moments = this.moments.filter(m => m.id !== item.id) |
|||
} |
|||
}) |
|||
}, |
|||
openComposer() { |
|||
this.showComposer = true |
|||
}, |
|||
closeComposer() { |
|||
this.showComposer = false |
|||
}, |
|||
removeComposeImage(index) { |
|||
this.composeImages.splice(index, 1) |
|||
}, |
|||
addComposeImages(paths) { |
|||
const items = (paths || []) |
|||
.filter(p => !!p) |
|||
.slice(0, 5 - this.composeImages.length) |
|||
.map(p => ({ local: p, url: '', status: 'uploading' })) |
|||
if (!items.length) return |
|||
this.composeImages = this.composeImages.concat(items) |
|||
items.forEach(item => this.uploadComposeImage(item)) |
|||
}, |
|||
async uploadComposeImage(item) { |
|||
item.status = 'uploading' |
|||
try { |
|||
item.url = await this.silentUpload(item.local) |
|||
item.status = 'done' |
|||
} catch (e) { |
|||
item.status = 'failed' |
|||
} |
|||
}, |
|||
retryComposeImage(item) { |
|||
if (item.status === 'failed') this.uploadComposeImage(item) |
|||
}, |
|||
async uploadComposeVideo() { |
|||
const video = this.composeVideo |
|||
if (!video) return |
|||
video.status = 'uploading' |
|||
try { |
|||
video.url = await this.silentUpload(video.local) |
|||
if (video.poster) { |
|||
try { video.posterUrl = await this.silentUpload(video.poster) } catch (e) { video.posterUrl = '' } |
|||
} |
|||
video.status = 'done' |
|||
} catch (e) { |
|||
video.status = 'failed' |
|||
} |
|||
}, |
|||
retryComposeVideo() { |
|||
if (this.composeVideo && this.composeVideo.status === 'failed') this.uploadComposeVideo() |
|||
}, |
|||
chooseMomentMedia() { |
|||
const hasImages = this.composeImages.length > 0 |
|||
if (!uni.chooseMedia) { |
|||
uni.chooseImage({ |
|||
count: 5 - this.composeImages.length, |
|||
success: (res) => { |
|||
this.addComposeImages(res.tempFilePaths || []) |
|||
} |
|||
}) |
|||
return |
|||
} |
|||
uni.chooseMedia({ |
|||
count: hasImages ? 5 - this.composeImages.length : 5, |
|||
mediaType: hasImages ? ['image'] : ['image', 'video'], |
|||
maxDuration: 60, |
|||
success: (res) => { |
|||
const files = res.tempFiles || [] |
|||
if (!files.length) return |
|||
if (res.type === 'video') { |
|||
const file = files[0] |
|||
if (file.size > 100 * 1024 * 1024) { |
|||
uni.showToast({ title: '视频不能超过100MB,请先剪辑', icon: 'none' }) |
|||
return |
|||
} |
|||
if (file.duration && file.duration > 61) { |
|||
uni.showToast({ title: '视频不能超过60秒', icon: 'none' }) |
|||
return |
|||
} |
|||
this.composeVideo = { |
|||
local: file.tempFilePath, |
|||
poster: file.thumbTempFilePath || '', |
|||
size: file.size, |
|||
url: '', |
|||
posterUrl: '', |
|||
status: 'uploading' |
|||
} |
|||
this.uploadComposeVideo() |
|||
return |
|||
} |
|||
const oversize = files.find(f => f.size > 10 * 1024 * 1024) |
|||
if (oversize) { |
|||
uni.showToast({ title: '单张图片不能超过10MB', icon: 'none' }) |
|||
return |
|||
} |
|||
this.addComposeImages(files.map(f => f.tempFilePath)) |
|||
} |
|||
}) |
|||
}, |
|||
silentUpload(filePath) { |
|||
return new Promise((resolve, reject) => { |
|||
uni.uploadFile({ |
|||
url: tui.interfaceUrl() + '/upload/file', |
|||
filePath, |
|||
name: 'file', |
|||
timeout: 300000, |
|||
header: { accessToken: uni.getStorageSync('hiver_token') }, |
|||
success: (res) => { |
|||
try { |
|||
const d = JSON.parse(String(res.data || '{}').replace(/\ufeff/g, '')) |
|||
const fileObj = d.result !== undefined ? d.result : d.data |
|||
const url = typeof fileObj === 'string' ? fileObj : (fileObj && (fileObj.url || fileObj.fileUrl || fileObj.path || fileObj.fullPath)) |
|||
if (d.code % 100 === 0 && url) resolve(url) |
|||
else reject(new Error(d.message || '上传失败')) |
|||
} catch (e) { |
|||
reject(e) |
|||
} |
|||
}, |
|||
fail: () => reject(new Error('上传失败')) |
|||
}) |
|||
}) |
|||
}, |
|||
async waitComposeUploads(images, video) { |
|||
const uploading = () => images.some(item => item.status === 'uploading') || |
|||
(video && video.status === 'uploading') |
|||
while (uploading()) { |
|||
await new Promise(resolve => setTimeout(resolve, 500)) |
|||
} |
|||
if (images.some(item => item.status !== 'done')) throw new Error('图片上传失败') |
|||
if (video && video.status !== 'done') throw new Error('视频上传失败') |
|||
}, |
|||
async submitMoment() { |
|||
if (this.publishing) return |
|||
const content = this.composeText.trim() |
|||
if (!content && !this.composeImages.length && !this.composeVideo) { |
|||
uni.showToast({ title: '写点什么或选张图片吧', icon: 'none' }) |
|||
return |
|||
} |
|||
const images = this.composeImages |
|||
const video = this.composeVideo |
|||
if (images.some(item => item.status === 'failed') || (video && video.status === 'failed')) { |
|||
uni.showToast({ title: '有图片/视频上传失败,点缩略图重试', icon: 'none' }) |
|||
return |
|||
} |
|||
this.publishing = true |
|||
const localCard = { |
|||
id: 'local-' + Date.now(), |
|||
content, |
|||
imageList: images.map(item => item.local), |
|||
videoUrl: video ? video.local : '', |
|||
videoPoster: video ? video.poster : '', |
|||
status: 1, |
|||
localState: 'publishing', |
|||
failTip: '', |
|||
createTime: this.formatNow() |
|||
} |
|||
this.moments.unshift(localCard) |
|||
this.showComposer = false |
|||
this.composeText = '' |
|||
this.composeImages = [] |
|||
this.composeVideo = null |
|||
this.publishing = false |
|||
uni.showToast({ title: '已发布', icon: 'none' }) |
|||
try { |
|||
await this.waitComposeUploads(images, video) |
|||
const payload = { content, images: images.map(item => item.url) } |
|||
if (video) { |
|||
payload.videoUrl = video.url |
|||
payload.videoPoster = video.posterUrl || '' |
|||
} |
|||
const moment = await publishIeMoment(payload) |
|||
if (!moment) throw new Error('发布失败,请稍后再试') |
|||
const index = this.moments.findIndex(m => m.id === localCard.id) |
|||
if (index >= 0) { |
|||
moment.imageList = localCard.imageList.length ? localCard.imageList : moment.imageList |
|||
moment.videoPoster = localCard.videoPoster || moment.videoPoster |
|||
this.moments.splice(index, 1, moment) |
|||
} |
|||
} catch (e) { |
|||
localCard.localState = 'failed' |
|||
localCard.failTip = (e && e.message) || '发布失败' |
|||
uni.showToast({ title: localCard.failTip, icon: 'none' }) |
|||
} |
|||
}, |
|||
formatNow() { |
|||
const d = new Date() |
|||
const pad = n => (n < 10 ? '0' + n : '' + n) |
|||
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + |
|||
pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
page { background: #f7f9ff; } |
|||
.my-space-page { min-height: 100vh; padding: 0 32rpx 80rpx; box-sizing: border-box; color: #161b2e; background: radial-gradient(circle at 50% 8%, rgba(169,255,231,.36), transparent 320rpx), radial-gradient(circle at 86% 30%, rgba(255,184,209,.2), transparent 340rpx), linear-gradient(180deg, #fbfdff, #eef4ff 62%, #fff4e8); } |
|||
.float-bar { position: relative; z-index: 5; display: flex; align-items: center; } |
|||
.home-back { display: flex; align-items: center; height: 58rpx; padding: 0 22rpx 0 12rpx; border: 1rpx solid rgba(255,255,255,.88); border-radius: 999rpx; color: rgba(22,27,46,.66); background: rgba(255,255,255,.66); backdrop-filter: blur(20rpx); box-shadow: 0 14rpx 36rpx rgba(96,112,160,.12), inset 0 1rpx 0 rgba(255,255,255,.95); font-size: 23rpx; font-weight: 800; } |
|||
.home-back-icon { margin-right: 6rpx; padding-bottom: 6rpx; font-size: 40rpx; line-height: 50rpx; font-weight: 400; } |
|||
.publish-btn { flex-shrink: 0; height: 54rpx; line-height: 54rpx; padding: 0 20rpx; border-radius: 999rpx; color: #11162a; background: #a9ffe7; font-size: 22rpx; font-weight: 900; box-shadow: 0 12rpx 28rpx rgba(96,200,170,.24); } |
|||
.space-hero { display: flex; align-items: center; margin-top: 28rpx; padding: 30rpx; border-radius: 38rpx; background: rgba(255,255,255,.66); border: 1rpx solid rgba(255,255,255,.82); box-shadow: 0 22rpx 70rpx rgba(96,112,160,.14); } |
|||
.hero-avatar, .hero-avatar-img { flex-shrink: 0; width: 112rpx; height: 112rpx; margin-right: 24rpx; border-radius: 50%; } |
|||
.hero-avatar { line-height: 112rpx; text-align: center; color: #11162a; background: linear-gradient(145deg, #effffb, #a9ffe7); font-size: 38rpx; font-weight: 900; } |
|||
.hero-main { flex: 1; min-width: 0; } |
|||
.hero-title-row { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; } |
|||
.hero-title { font-size: 36rpx; font-weight: 900; } |
|||
.hero-sub { margin-top: 10rpx; color: rgba(22,27,46,.48); font-size: 23rpx; } |
|||
.moment-empty { margin-top: 60rpx; text-align: center; color: rgba(22,27,46,.4); font-size: 25rpx; } |
|||
.moment-card { margin-top: 24rpx; padding: 26rpx; border-radius: 28rpx; background: rgba(255,255,255,.85); border: 1rpx solid rgba(255,255,255,.9); box-shadow: 0 12rpx 32rpx rgba(96,112,160,.08); } |
|||
.moment-top { display: flex; align-items: center; } |
|||
.moment-time { flex: 1; color: rgba(22,27,46,.42); font-size: 21rpx; } |
|||
.moment-state { margin-right: 16rpx; padding: 4rpx 14rpx; border-radius: 999rpx; font-size: 20rpx; font-weight: 700; } |
|||
.moment-state.auditing { color: #b07b1f; background: rgba(255,196,87,.18); } |
|||
.moment-state.blocked { color: #d2486a; background: rgba(255,107,146,.14); } |
|||
.moment-del { color: rgba(22,27,46,.38); font-size: 22rpx; padding: 4rpx 8rpx; } |
|||
.moment-text { margin-top: 14rpx; font-size: 27rpx; line-height: 42rpx; word-break: break-all; } |
|||
.moment-grid { display: flex; flex-wrap: wrap; margin-top: 16rpx; } |
|||
.moment-grid image { width: 196rpx; height: 196rpx; margin: 0 10rpx 10rpx 0; border-radius: 16rpx; background: rgba(22,27,46,.05); } |
|||
.moment-grid.g1 image { width: 400rpx; height: 400rpx; border-radius: 22rpx; } |
|||
.moment-grid.g2 image { width: 300rpx; height: 300rpx; } |
|||
.moment-video { position: relative; margin-top: 16rpx; width: 440rpx; height: 290rpx; border-radius: 22rpx; overflow: hidden; background: #1a2034; } |
|||
.moment-video-poster, .moment-video-holder { width: 100%; height: 100%; } |
|||
.moment-play { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 84rpx; height: 84rpx; line-height: 84rpx; text-align: center; border-radius: 50%; color: #fff; background: rgba(0,0,0,.45); font-size: 32rpx; } |
|||
.moment-more { margin-top: 26rpx; text-align: center; color: rgba(22,27,46,.36); font-size: 22rpx; padding: 12rpx 0; } |
|||
.composer-mask { position: fixed; inset: 0; z-index: 120; display: flex; align-items: flex-end; background: rgba(13,17,32,.5); backdrop-filter: blur(10rpx); } |
|||
.composer { width: 100%; padding: 36rpx 32rpx calc(40rpx + env(safe-area-inset-bottom)); border-radius: 40rpx 40rpx 0 0; background: #ffffff; box-sizing: border-box; } |
|||
.composer-title { font-size: 32rpx; font-weight: 800; text-align: center; } |
|||
.composer-input { width: 100%; height: 200rpx; margin-top: 24rpx; padding: 22rpx; border-radius: 22rpx; background: #f4f6fc; font-size: 27rpx; box-sizing: border-box; } |
|||
.composer-grid { display: flex; flex-wrap: wrap; margin-top: 20rpx; } |
|||
.composer-thumb { position: relative; width: 150rpx; height: 150rpx; margin: 0 14rpx 14rpx 0; border-radius: 16rpx; overflow: hidden; background: #1a2034; } |
|||
.composer-thumb image { width: 100%; height: 100%; } |
|||
.thumb-del { position: absolute; right: 0; top: 0; width: 40rpx; height: 40rpx; line-height: 36rpx; text-align: center; color: #fff; background: rgba(0,0,0,.55); border-radius: 0 0 0 16rpx; font-size: 26rpx; } |
|||
.thumb-video-mark { position: absolute; left: 0; bottom: 0; right: 0; padding: 4rpx 0; text-align: center; color: #fff; background: rgba(0,0,0,.45); font-size: 20rpx; } |
|||
.thumb-state { position: absolute; left: 0; top: 0; right: 0; bottom: 0; display: flex; align-items: center; justify-content: center; color: #fff; background: rgba(0,0,0,.42); font-size: 20rpx; } |
|||
.thumb-state.failed { color: #ffd9e2; background: rgba(160,30,60,.55); } |
|||
.composer-add { width: 150rpx; height: 150rpx; line-height: 146rpx; text-align: center; border-radius: 16rpx; border: 2rpx dashed rgba(22,27,46,.18); color: rgba(22,27,46,.34); font-size: 52rpx; box-sizing: border-box; } |
|||
.composer-tip { margin-top: 8rpx; color: rgba(22,27,46,.38); font-size: 21rpx; } |
|||
.composer-submit { margin-top: 28rpx; height: 92rpx; line-height: 92rpx; text-align: center; border-radius: 999rpx; color: #11162a; background: linear-gradient(135deg, #a9ffe7, #7be3c8); font-size: 30rpx; font-weight: 800; box-shadow: 0 14rpx 32rpx rgba(96,200,170,.32); } |
|||
.composer-submit.disabled { opacity: .6; } |
|||
</style> |
|||
@ -0,0 +1,137 @@ |
|||
<template> |
|||
<view class="space-page"> |
|||
<view class="nav" :style="{ paddingTop: menuButtonInfo.top + 'px' }"> |
|||
<view class="back" @tap="back">‹</view> |
|||
<view class="nav-title">{{ ownerName }}的空间</view> |
|||
</view> |
|||
|
|||
<view class="owner-card"> |
|||
<image class="owner-avatar-img" v-if="ownerAvatarUrl" :src="ownerAvatarUrl" mode="aspectFill"></image> |
|||
<view class="owner-avatar" v-else>{{ ownerAvatarText }}</view> |
|||
<view class="owner-name">{{ ownerName }}</view> |
|||
<view class="owner-mode">{{ ownerMode === 'e' ? 'e 人 · 轻轻热闹' : 'i 人 · 安静陪伴' }}</view> |
|||
</view> |
|||
|
|||
<view class="moment-empty" v-if="!moments.length && !loading">TA 还没有发过动态</view> |
|||
<view class="moment-card" v-for="item in moments" :key="item.id"> |
|||
<view class="moment-time">{{ momentTime(item.createTime) }}</view> |
|||
<view class="moment-text" v-if="item.content">{{ item.content }}</view> |
|||
<view class="moment-grid" v-if="item.imageList && item.imageList.length" :class="'g' + item.imageList.length"> |
|||
<image v-for="(img, idx) in item.imageList" :key="idx" :src="img" mode="aspectFill" @tap="previewImages(item, idx)"></image> |
|||
</view> |
|||
<view class="moment-video" v-if="item.videoUrl" @tap="playVideo(item)"> |
|||
<image class="moment-video-poster" v-if="item.videoPoster" :src="item.videoPoster" mode="aspectFill"></image> |
|||
<view class="moment-video-holder" v-else></view> |
|||
<view class="moment-play">▶</view> |
|||
</view> |
|||
</view> |
|||
<view class="load-line" v-if="loading">加载中...</view> |
|||
<view class="load-line" v-else-if="!hasMore && moments.length">没有更多动态了</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { pageIeMoments } from '@/common/ieApi.js' |
|||
|
|||
export default { |
|||
data() { |
|||
return { |
|||
menuButtonInfo: { top: 44 }, |
|||
userId: null, |
|||
roomId: '', |
|||
ownerName: 'TA', |
|||
ownerAvatarText: 'TA', |
|||
ownerAvatarUrl: '', |
|||
ownerMode: 'i', |
|||
moments: [], |
|||
page: 1, |
|||
hasMore: true, |
|||
loading: false |
|||
} |
|||
}, |
|||
onLoad(options = {}) { |
|||
if (uni.getMenuButtonBoundingClientRect) this.menuButtonInfo = uni.getMenuButtonBoundingClientRect() |
|||
this.userId = options.userId || null |
|||
this.roomId = options.roomId || '' |
|||
if (options.name) this.ownerName = decodeURIComponent(options.name) |
|||
if (options.avatar) this.ownerAvatarText = decodeURIComponent(options.avatar) |
|||
if (options.avatarUrl) this.ownerAvatarUrl = decodeURIComponent(options.avatarUrl) |
|||
if (options.mode) this.ownerMode = options.mode |
|||
this.loadMoments(true) |
|||
}, |
|||
onReachBottom() { |
|||
this.loadMoments(false) |
|||
}, |
|||
methods: { |
|||
back() { |
|||
uni.navigateBack({ fail: () => uni.redirectTo({ url: '/package1/ieBrowser/index' }) }) |
|||
}, |
|||
async loadMoments(reset) { |
|||
if (this.loading) return |
|||
if (!reset && !this.hasMore) return |
|||
if (!this.userId) return |
|||
this.loading = true |
|||
try { |
|||
const result = await pageIeMoments(this.userId, reset ? 1 : this.page, 10, this.roomId) |
|||
const records = (result && result.records) || [] |
|||
this.moments = reset ? records : this.moments.concat(records) |
|||
this.hasMore = records.length >= 10 |
|||
this.page = (reset ? 1 : this.page) + 1 |
|||
const first = records.find(item => item.anonymousName) |
|||
if (first) { |
|||
this.ownerName = first.anonymousName || this.ownerName |
|||
this.ownerAvatarText = first.avatarText || this.ownerAvatarText |
|||
this.ownerAvatarUrl = first.avatarUrl || this.ownerAvatarUrl |
|||
this.ownerMode = first.currentMode || this.ownerMode |
|||
} |
|||
} finally { |
|||
this.loading = false |
|||
} |
|||
}, |
|||
momentTime(timeStr) { |
|||
if (!timeStr) return '' |
|||
const time = new Date(String(timeStr).replace(/-/g, '/')).getTime() |
|||
if (!time) return '' |
|||
const diff = Date.now() - time |
|||
if (diff < 60000) return '刚刚' |
|||
if (diff < 3600000) return Math.floor(diff / 60000) + ' 分钟前' |
|||
if (diff < 86400000) return Math.floor(diff / 3600000) + ' 小时前' |
|||
if (diff < 172800000) return '昨天' |
|||
const d = new Date(time) |
|||
return (d.getMonth() + 1) + '月' + d.getDate() + '日' |
|||
}, |
|||
previewImages(item, index) { |
|||
uni.previewImage({ urls: item.imageList || [], current: item.imageList[index] }) |
|||
}, |
|||
playVideo(item) { |
|||
if (!item.videoUrl) return |
|||
uni.navigateTo({ url: '/package1/ieBrowser/videoPreview?url=' + encodeURIComponent(item.videoUrl) }) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
page { background: #f7f9ff; } |
|||
.space-page { min-height: 100vh; padding: 0 32rpx 80rpx; box-sizing: border-box; color: #161b2e; background: radial-gradient(circle at 50% 8%, rgba(169,255,231,.36), transparent 320rpx), linear-gradient(180deg, #fbfdff, #eef4ff 62%, #fff4e8); } |
|||
.nav { display: flex; align-items: center; padding-bottom: 16rpx; } |
|||
.back { width: 64rpx; height: 64rpx; line-height: 58rpx; text-align: center; border-radius: 50%; background: rgba(255,255,255,.7); font-size: 44rpx; color: rgba(22,27,46,.66); box-shadow: 0 10rpx 26rpx rgba(96,112,160,.12); } |
|||
.nav-title { margin-left: 20rpx; font-size: 32rpx; font-weight: 800; } |
|||
.owner-card { margin-top: 10rpx; padding: 34rpx; text-align: center; border-radius: 36rpx; background: rgba(255,255,255,.66); border: 1rpx solid rgba(255,255,255,.8); box-shadow: 0 20rpx 60rpx rgba(96,112,160,.12); } |
|||
.owner-avatar, .owner-avatar-img { width: 120rpx; height: 120rpx; margin: 0 auto; border-radius: 50%; } |
|||
.owner-avatar { line-height: 120rpx; text-align: center; background: linear-gradient(145deg, #effffb, #a9ffe7); font-size: 40rpx; font-weight: 800; color: #11162a; } |
|||
.owner-name { margin-top: 18rpx; font-size: 34rpx; font-weight: 800; } |
|||
.owner-mode { margin-top: 8rpx; color: #6c69d8; font-size: 23rpx; } |
|||
.moment-empty { margin-top: 60rpx; text-align: center; color: rgba(22,27,46,.4); font-size: 25rpx; } |
|||
.moment-card { margin-top: 24rpx; padding: 26rpx; border-radius: 28rpx; background: rgba(255,255,255,.85); border: 1rpx solid rgba(255,255,255,.9); box-shadow: 0 12rpx 32rpx rgba(96,112,160,.08); } |
|||
.moment-time { color: rgba(22,27,46,.42); font-size: 21rpx; } |
|||
.moment-text { margin-top: 14rpx; font-size: 27rpx; line-height: 42rpx; word-break: break-all; } |
|||
.moment-grid { display: flex; flex-wrap: wrap; margin-top: 16rpx; } |
|||
.moment-grid image { width: 196rpx; height: 196rpx; margin: 0 10rpx 10rpx 0; border-radius: 16rpx; background: rgba(22,27,46,.05); } |
|||
.moment-grid.g1 image { width: 400rpx; height: 400rpx; border-radius: 22rpx; } |
|||
.moment-grid.g2 image { width: 300rpx; height: 300rpx; } |
|||
.moment-video { position: relative; margin-top: 16rpx; width: 440rpx; height: 290rpx; border-radius: 22rpx; overflow: hidden; background: #1a2034; } |
|||
.moment-video-poster, .moment-video-holder { width: 100%; height: 100%; } |
|||
.moment-play { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 84rpx; height: 84rpx; line-height: 84rpx; text-align: center; border-radius: 50%; color: #fff; background: rgba(0,0,0,.45); font-size: 32rpx; } |
|||
.load-line { margin-top: 26rpx; text-align: center; color: rgba(22,27,46,.36); font-size: 22rpx; } |
|||
</style> |
|||
Loading…
Reference in new issue