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