1977 lines
45 KiB
Plaintext
1977 lines
45 KiB
Plaintext
<template>
|
||
<scroll-view class="page" :show-scrollbar="false">
|
||
<!-- 顶部导航 -->
|
||
<view class="page-header">
|
||
<view class="header-left">
|
||
<uni-icons type="left" size="32"></uni-icons>
|
||
</view>
|
||
<view class="header-center">
|
||
<text class="header-title">动态详情</text>
|
||
</view>
|
||
<view class="header-right">
|
||
<uni-icons class="header-icon" :type="dynamicData.isFavorited ? 'star':'star-filled'"
|
||
:color="dynamicData.isFavorited ? '#ff5d9b': '#c2c2c2'" size="32"
|
||
@click="toggleFavorite"></uni-icons>
|
||
<uni-icons class="header-icon" type="more-filled" size="32" @click="showMoreActions"></uni-icons>
|
||
</view>
|
||
</view>
|
||
<!-- 动态内容 -->
|
||
<view class="dynamic-content" ref="dynamicContent">
|
||
<!-- 作者信息 -->
|
||
<view class="author-info">
|
||
<image :src="dynamicData.avatar" class="author-avatar" />
|
||
<view class="author-details">
|
||
<view class="author-name">{{ dynamicData.username }}</view>
|
||
<view class="post-info">
|
||
<text class="post-time">{{ formatTime(dynamicData.createTime) }}</text>
|
||
<text v-if="dynamicData.location" class="post-location">
|
||
<uni-icons type="location"></uni-icons> {{ dynamicData.location }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
<button class="follow-btn" v-if="!isSelf && !dynamicData.isFollowed" @click="toggleFollow">
|
||
<i class="fas fa-plus"></i> 关注
|
||
</button>
|
||
<button class="following-btn" v-if="!isSelf && dynamicData.isFollowed" @click="toggleFollow">
|
||
已关注
|
||
</button>
|
||
</view>
|
||
|
||
<!-- 动态文本内容 -->
|
||
<view class="content-text">
|
||
{{ dynamicData.content }}
|
||
</view>
|
||
|
||
<!-- 图片/视频展示 -->
|
||
<view v-if="dynamicData.media && dynamicData.media.length > 0" class="content-media">
|
||
<view v-if="dynamicData.media.length === 1" class="single-media">
|
||
<image v-if="dynamicData.media[0].type === 'image'" :src="dynamicData.media[0].url" alt="动态图片"
|
||
class="media-image" @click="previewMedia(0)" />
|
||
<view v-if="dynamicData.media[0].type === 'video'" class="media-video">
|
||
<video :src="dynamicData.media[0].url" controls class="video-player" @click="playVideo"></video>
|
||
<view v-if="!isPlaying" class="video-overlay" @click="playVideo">
|
||
<i class="fas fa-play"></i>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="dynamicData.media.length > 1" class="multi-media">
|
||
<view v-for="(media, index) in dynamicData.media" :key="index" class="media-grid-item"
|
||
:class="gridClass(dynamicData.media.length)" @click="previewMedia(index)">
|
||
<image v-if="media.type === 'image'" :src="media.url" :alt="'图片' + (index + 1)" />
|
||
<view v-if="media.type === 'video'" class="video-thumbnail">
|
||
<image :src="media.thumbnail != null || media.url != null" :alt="'视频' + (index + 1)" />
|
||
<view class="video-play-icon">
|
||
<uni-icons type="videocam-filled"></uni-icons>
|
||
</view>
|
||
</view>
|
||
<view v-if="dynamicData.media.length > 9 && index === 8" class="more-indicator">
|
||
+{{ dynamicData.media.length - 9 }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 位置信息 -->
|
||
<view v-if="dynamicData.location && dynamicData.coordinates" class="location-section">
|
||
<view class="location-header">
|
||
<i class="fas fa-map-marker-alt"></i>
|
||
<text class="location-name">{{ dynamicData.location }}</text>
|
||
</view>
|
||
<view class="map-preview" @click="openMap">
|
||
<!-- 这里可以集成地图组件 -->
|
||
<view class="map-placeholder">
|
||
<i class="fas fa-map"></i>
|
||
<text>查看地图位置</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 话题标签 -->
|
||
<view v-if="dynamicData.hashtags && dynamicData.hashtags.length > 0" class="hashtag-section">
|
||
<text v-for="(tag, index) in dynamicData.hashtags" :key="index" class="hashtag"
|
||
@click="searchHashtag(tag.name)">
|
||
#{{ tag.name }}
|
||
</text>
|
||
</view>
|
||
|
||
<!-- 动态统计信息 -->
|
||
<view class="stats-section">
|
||
<view class="stats-item">
|
||
<i class="far fa-heart"></i>
|
||
<text>{{ dynamicData.like }} 人点赞</text>
|
||
</view>
|
||
<view class="stats-item">
|
||
<i class="far fa-comment"></i>
|
||
<text>{{ dynamicData.comment }} 条评论</text>
|
||
</view>
|
||
<view class="stats-item">
|
||
<i class="fas fa-share"></i>
|
||
<text>{{ dynamicData.share }} 次分享</text>
|
||
</view>
|
||
<view class="stats-item">
|
||
<i class="far fa-eye"></i>
|
||
<text>{{ dynamicData.view || 0 }} 次浏览</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 操作按钮 -->
|
||
<view class="action-section">
|
||
<button class="action-button like-btn" :class="{ active: dynamicData.likede }" @click="toggleLike">
|
||
<i class="fas" :class="dynamicData.likede ? 'fa-heart' : 'fa-heart-o'"></i>
|
||
<text>{{ dynamicData.likede ? '已赞' : '点赞' }}</text>
|
||
</button>
|
||
|
||
<button class="action-button comment-btn" @click="scrollToComment">
|
||
<i class="far fa-comment"></i>
|
||
<text>评论</text>
|
||
</button>
|
||
|
||
<button class="action-button share-btn" @click="showShareOptions">
|
||
<i class="fas fa-share-alt"></i>
|
||
<text>分享</text>
|
||
</button>
|
||
|
||
<button class="action-button collect-btn" @click="toggleCollect">
|
||
<i class="far" :class="dynamicData.collected ? 'fa-bookmark' : 'fa-bookmark-o'"></i>
|
||
<text>{{ dynamicData.collected ? '已收藏' : '收藏' }}</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
<!-- 评论区域 -->
|
||
<view class="comment-section">
|
||
<text class="section-title"> 评论 ({{ commentCount }}) </text>
|
||
<!-- 评论组件 -->
|
||
<view class="comment-container">
|
||
<view class="comment-input-area">
|
||
<view class="input-header">
|
||
<image :src="currentUser.avatar" class="user-avatar" alt="用户头像" />
|
||
<view class="input-wrapper">
|
||
<view class="reply-hint" v-if="replyToUser"> 回复 @{{ replyToUser.username }}
|
||
<button class="cancel-reply" @click="cancelReply">×</button>
|
||
</view>
|
||
<view class="input-box">
|
||
<textarea v-model="commentInput" placeholder="说点什么..." class="comment-textarea"
|
||
maxlength="1000" @input="onCommentInput" ref="commentTextareaRef"></textarea>
|
||
<view class="input-actions">
|
||
<view class="emoji-picker">
|
||
<button class="action-btn" @click="toggleEmoji">
|
||
<text class="far fa-smile">🙂</text>
|
||
</button>
|
||
<view class="emoji-list" v-if="showEmoji">
|
||
<text v-for="emoji in emojiList.slice(0, 24)" :key="emoji"
|
||
@click="insertEmoji(emoji)">{{ emoji }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="word-count">{{ commentInput.length }}/1000</view>
|
||
<button class="submit-btn" @click="submitComment" :disabled="!canSubmitComment">
|
||
{{ publishingComment ? '发送中...' : '发送' }}
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 评论列表 -->
|
||
<view class="comment-list" v-if="comments.length > 0">
|
||
<view v-for="comment in comments" :key="comment.id" class="comment-item">
|
||
<view class="comment-main">
|
||
<view class="comment-avatar">
|
||
<image :src="comment.avatar" alt="用户头像" />
|
||
</view>
|
||
<view class="comment-content">
|
||
<view class="comment-header">
|
||
<text class="username">{{ comment.username }}</text>
|
||
<text class="comment-time">{{ formatTime(comment.createTime) }}</text>
|
||
</view>
|
||
<view class="comment-text">
|
||
<text v-if="comment.replyTo" class="reply-to">@{{ comment.replyTo.username }}</text>
|
||
{{ comment.content }}
|
||
</view>
|
||
<view class="comment-actions">
|
||
<button class="action-btn like-btn" :class="{ active: comment.liked }"
|
||
@click="toggleCommentLike(comment)">
|
||
<i class="far fa-thumbs-up"></i>
|
||
<text>{{ comment.like }}</text>
|
||
</button>
|
||
<button class="action-btn reply-btn" @click="startReply(comment)">
|
||
回复
|
||
</button>
|
||
|
||
<view class="more-actions">
|
||
<button class="action-btn more-btn" @click="toggleCommentActions(comment.id)">
|
||
<i class="fas fa-ellipsis-h"></i>
|
||
</button>
|
||
<view class="more-menu" v-if="showCommentActions === comment.id">
|
||
<button v-if="comment.userID === currentUser.id"
|
||
class="menu-item delete-btn" @click="deleteComment(comment)">
|
||
<i class="far fa-trash-alt"></i> 删除
|
||
</button>
|
||
<button class="menu-item report-btn" @click="reportComment(comment)">
|
||
<i class="far fa-flag"></i> 举报
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 回复列表 -->
|
||
<view class="reply-list" v-if="comment.replies && comment.replies.length > 0">
|
||
<view v-for="reply in comment.replies" :key="reply.id" class="reply-item">
|
||
<view class="reply-avatar">
|
||
<image :src="reply.avatar" alt="用户头像" />
|
||
</view>
|
||
<view class="reply-content">
|
||
<view class="reply-header">
|
||
<text class="username">{{ reply.username }}</text>
|
||
<text class="reply-time">{{ formatTime(reply.createTime) }}</text>
|
||
</view>
|
||
<view class="reply-text">
|
||
<text v-if="reply.replyTo"
|
||
class="reply-to">@{{ reply.replyTo.username }}</text>
|
||
{{ reply.content }}
|
||
</view>
|
||
<view class="reply-actions">
|
||
<button class="action-btn like-btn" :class="{ active: reply.liked }"
|
||
@click="toggleReplyLike(comment, reply)">
|
||
<i class="far fa-thumbs-up"></i>
|
||
<text>{{ reply.like }}</text>
|
||
</button>
|
||
<button class="action-btn reply-btn"
|
||
@click="startReply(comment, reply)">
|
||
回复
|
||
</button>
|
||
<view class="more-actions">
|
||
<button class="action-btn more-btn"
|
||
@click="toggleCommentActions(reply.id)">
|
||
<i class="fas fa-ellipsis-h"></i>
|
||
</button>
|
||
<view class="more-menu" v-if="showCommentActions === reply.id">
|
||
<button v-if="reply.userID === currentUser.id"
|
||
class="menu-item delete-btn"
|
||
@click="deleteReply(comment, reply)">
|
||
<i class="far fa-trash-alt"></i> 删除
|
||
</button>
|
||
<button class="menu-item report-btn"
|
||
@click="reportComment(reply)">
|
||
<i class="far fa-flag"></i> 举报
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 查看更多回复 -->
|
||
<view v-if="comment.replyCount > comment.replies.length" class="view-more-replies"
|
||
@click="loadMoreReplies(comment)">
|
||
查看{{ comment.replyCount - comment.replies.length }}条回复
|
||
<i class="fas fa-chevron-down"></i>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 加载更多评论 -->
|
||
<view v-if="hasMoreComments" class="load-more-comments" @click="loadMoreComments">
|
||
<i class="fas fa-spinner fa-spin" v-if="loadingComments"></i>
|
||
<text v-else>加载更多评论</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 空状态 -->
|
||
<view v-else class="empty-comments">
|
||
<i class="far fa-comment-dots"></i>
|
||
<p>还没有评论,快来抢沙发吧~</p>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 媒体预览弹窗 -->
|
||
<view v-if="showMediaPreview" class="media-preview-modal" @click="closeMediaPreview">
|
||
<view class="modal-content" @click.stop>
|
||
<view class="modal-header">
|
||
<button class="close-btn" @click="closeMediaPreview">×</button>
|
||
</view>
|
||
<view class="modal-body">
|
||
<image v-if="previewMediaIndex !== null" :src="dynamicData.media[previewMediaIndex].url"
|
||
class="preview-image" />
|
||
</view>
|
||
<view class="modal-footer">
|
||
<view class="media-info">
|
||
{{ previewMediaIndex + 1 }} / {{ dynamicData.media.length }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<!-- 分享弹窗 -->
|
||
<view v-if="showShareModal" class="share-modal" @click="closeShareModal">
|
||
<view class="modal-content share-content" @click.stop>
|
||
<view class="share-header">
|
||
<text>分享到</text>
|
||
<button class="close-btn" @click="closeShareModal">×</button>
|
||
</view>
|
||
<view class="share-options">
|
||
<view class="share-option" @click="shareTo('wechat')">
|
||
<i class="fab fa-weixin"></i>
|
||
<text>微信</text>
|
||
</view>
|
||
<view class="share-option" @click="shareTo('qq')">
|
||
<i class="fab fa-qq"></i>
|
||
<text>QQ</text>
|
||
</view>
|
||
<view class="share-option" @click="shareTo('weibo')">
|
||
<i class="fab fa-weibo"></i>
|
||
<text>微博</text>
|
||
</view>
|
||
<view class="share-option" @click="shareTo('copy')">
|
||
<i class="fas fa-link"></i>
|
||
<text>复制链接</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 更多操作弹窗 -->
|
||
<view v-if="showActionsModal" class="actions-modal" @click="closeActionsModal">
|
||
<view class="modal-content actions-content" @click.stop>
|
||
<view class="action-list">
|
||
<button v-if="!isSelf" class="action-item" @click="toggleFollowFromModal">
|
||
<i class="fas" :class="dynamicData.isFollowed ? 'fa-user-minus' : 'fa-user-plus'"></i>
|
||
<text>{{ dynamicData.isFollowed ? '取消关注' : '关注' }}</text>
|
||
</button>
|
||
<button class="action-item" @click="toggleFavorite">
|
||
<i class="far" :class="dynamicData.isFavorited ? 'fa-heart' : 'fa-heart-o'"></i>
|
||
<text>{{ dynamicData.isFavorited ? '取消喜欢' : '喜欢' }}</text>
|
||
</button>
|
||
<button class="action-item" @click="toggleCollect">
|
||
<i class="far" :class="dynamicData.collected ? 'fa-bookmark' : 'fa-bookmark-o'"></i>
|
||
<text>{{ dynamicData.collected ? '取消收藏' : '收藏' }}</text>
|
||
</button>
|
||
<button v-if="isSelf" class="action-item" @click="editDynamic">
|
||
<i class="far fa-edit"></i>
|
||
<text>编辑动态</text>
|
||
</button>
|
||
<button v-if="isSelf" class="action-item delete-action" @click="deleteDynamic">
|
||
<i class="far fa-trash-alt"></i>
|
||
<text>删除动态</text>
|
||
</button>
|
||
<button class="action-item" @click="reportDynamic">
|
||
<i class="far fa-flag"></i>
|
||
<text>举报</text>
|
||
</button>
|
||
</view>
|
||
<view class="modal-footer">
|
||
<button class="cancel-btn" @click="closeActionsModal">取消</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import Api from '@/common/api-service.uts'
|
||
import { getUserInfo } from '@/stores/user.uts'
|
||
// 当前用户信息
|
||
const me = getUserInfo()
|
||
const currentUser = reactive({ id: me?.uid ?? 0, username: me?.nickname ?? '我', avatar: me?.avatar ?? '' })
|
||
// 动态ID(从路由获取)
|
||
const momentId = ref(0)
|
||
// 动态数据
|
||
const dynamicData = reactive<any>({
|
||
id: momentId.value,
|
||
uid: 101,
|
||
nickname: '科技探索者',
|
||
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
|
||
content: '今天去了新开的科技馆,体验了VR太空漫步,仿佛真的置身宇宙之中,太震撼了!推荐大家一定要去试试。',
|
||
createTime: Date.now() - 2 * 60 * 60 * 1000, // 2小时前
|
||
location: '北京·科技馆',
|
||
coordinates: { lat: 39.9042, lng: 116.4074 },
|
||
media: [
|
||
{ type: 'image', url: 'https://picsum.photos/400/300?random=1' },
|
||
{ type: 'image', url: 'https://picsum.photos/400/300?random=2' },
|
||
{ type: 'image', url: 'https://picsum.photos/400/300?random=3' },
|
||
{ type: 'video', url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', thumbnail: 'https://picsum.photos/400/300?random=4' }
|
||
],
|
||
hashtags: ['科技', 'VR体验', '太空漫步'],
|
||
like: 156,
|
||
comment: 23,
|
||
share: 12,
|
||
view: 456,
|
||
likede: true,
|
||
collected: false,
|
||
isFavorited: false,
|
||
isFollowed: false
|
||
})
|
||
const momentData = ref<IMoment>(null)
|
||
// 是否为当前用户
|
||
const isSelf = computed(() => dynamicData.uid == currentUser.id && currentUser.id > 0)
|
||
|
||
// 评论相关
|
||
const comments = ref<any[]>([])
|
||
const commentCount = ref(23)
|
||
const commentInput = ref('')
|
||
const replyToUser = ref(null)
|
||
const replyToComment = ref(null)
|
||
const showEmoji = ref(false)
|
||
const publishingComment = ref(false)
|
||
const hasMoreComments = ref(true)
|
||
const loadingComments = ref(false)
|
||
const showCommentActions = ref(null)
|
||
const commentTextareaRef = ref(null)
|
||
|
||
// 表情列表
|
||
const emojiList = ref(['😀', '😁', '😂', '😃', '😄', '😅', '😆', '😉', '😊', '😋', '😎', '😍', '😘', '😗', '😙', '😚', '🙂', '🤗', '🤔', '😐', '😑', '😶', '🙄', '😏', '😣', '😥', '😮', '🤐', '😯', '😪', '😫', '😴', '😌', '😛', '😜', '😝', '🤤', '😒', '😓', '😔', '😕', '🙃', '🤑', '😲', '🙁', '😖', '😞', '😟', '😤', '😢', '😭', '😦', '😧', '😨', '😩', '😬', '😰', '😱', '😳', '😵', '😡', '😠', '🤧', '😇', '🤠', '🤡', '🤥', '🤢', '🤕', '🤒'])
|
||
|
||
// 弹窗控制
|
||
const showMediaPreview = ref(false)
|
||
const previewMediaIndex = ref(null)
|
||
const showShareModal = ref(false)
|
||
const showActionsModal = ref(false)
|
||
const isPlaying = ref(false)
|
||
|
||
// 计算属性
|
||
const canSubmitComment = computed(() => {
|
||
return commentInput.value.trim().length > 0 && !publishingComment.value
|
||
})
|
||
|
||
// 初始化
|
||
onMounted(() => {
|
||
loadMoment()
|
||
})
|
||
|
||
|
||
|
||
|
||
|
||
// 返回上一页
|
||
const goBack = () => {
|
||
uni.navigateBack()
|
||
}
|
||
|
||
// 格式化时间
|
||
const formatTime = (timestamp) => {
|
||
const now = Date.now()
|
||
const diff = now - timestamp
|
||
const minute = 60 * 1000
|
||
const hour = 60 * minute
|
||
const day = 24 * hour
|
||
|
||
if (diff < minute) {
|
||
return '刚刚'
|
||
} else if (diff < hour) {
|
||
return `${Math.floor(diff / minute)}分钟前`
|
||
} else if (diff < day) {
|
||
return `${Math.floor(diff / hour)}小时前`
|
||
} else {
|
||
const date = new Date(timestamp)
|
||
return `${date.getMonth() + 1}月${date.getDate()}日 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||
}
|
||
}
|
||
|
||
// 网格布局类
|
||
const gridClass = (count) => {
|
||
if (count === 2) return 'grid-2'
|
||
if (count === 3) return 'grid-3'
|
||
if (count === 4) return 'grid-4'
|
||
if (count >= 5 && count <= 9) return 'grid-3'
|
||
return 'grid-3'
|
||
}
|
||
|
||
// 点赞动态
|
||
const toggleLike = () => {
|
||
const will = ! dynamicData.likede
|
||
dynamicData.likede = will
|
||
dynamicData.like += will ? 1 : -1
|
||
Api.moment.like({ dynamic_id: momentId.value })
|
||
.then((r : UTSJSONObject) => {
|
||
const d = r.get('data') as UTSJSONObject
|
||
if (d != null) {
|
||
dynamicData.likede = !!d.get('liked')
|
||
dynamicData.like = Number(d.get('like_count') ?? dynamicData.like)
|
||
}
|
||
})
|
||
.catch(() => { dynamicData.likede = !will; dynamicData.like += will ? -1 : 1 })
|
||
}
|
||
|
||
// 收藏动态
|
||
const toggleCollect = () => {
|
||
const will = ! dynamicData.collected
|
||
dynamicData.collected = will
|
||
dynamicData.isFavorited = will
|
||
Api.moment.collect({ dynamic_id: momentId.value })
|
||
.catch(() => { dynamicData.collected = !will; dynamicData.isFavorited = !will })
|
||
}
|
||
|
||
// 喜欢动态(等同收藏)
|
||
const toggleFavorite = () => {
|
||
toggleCollect()
|
||
}
|
||
|
||
// 关注用户
|
||
const toggleFollow = () => {
|
||
const will = ! dynamicData.isFollowed
|
||
dynamicData.isFollowed = will
|
||
Api.moment.follow({ following_id: dynamicData.uid })
|
||
.catch(() => { dynamicData.isFollowed = !will })
|
||
}
|
||
|
||
// 滚动到评论区域
|
||
const scrollToComment = () => {
|
||
// 评论区在当前页面内,无需特殊处理
|
||
}
|
||
|
||
// 显示分享选项
|
||
const showShareOptions = () => {
|
||
showShareModal.value = true
|
||
}
|
||
|
||
// 关闭分享弹窗
|
||
const closeShareModal = () => {
|
||
showShareModal.value = false
|
||
}
|
||
|
||
// 显示更多操作
|
||
const showMoreActions = () => {
|
||
showActionsModal.value = true
|
||
}
|
||
|
||
// 关闭操作弹窗
|
||
const closeActionsModal = () => {
|
||
showActionsModal.value = false
|
||
}
|
||
|
||
// 从弹窗关注用户
|
||
const toggleFollowFromModal = () => {
|
||
toggleFollow()
|
||
closeActionsModal()
|
||
}
|
||
|
||
// 分享到
|
||
const shareTo = (platform : string) => {
|
||
if (platform == 'copy') {
|
||
uni.setClipboardData({ data: `动态详情 ${momentId.value}` })
|
||
} else {
|
||
uni.showToast({ title: '分享成功', icon: 'success' })
|
||
}
|
||
closeShareModal()
|
||
}
|
||
|
||
// 预览媒体
|
||
const previewMedia = (index) => {
|
||
previewMediaIndex.value = index
|
||
showMediaPreview.value = true
|
||
}
|
||
|
||
// 关闭媒体预览
|
||
const closeMediaPreview = () => {
|
||
showMediaPreview.value = false
|
||
previewMediaIndex.value = null
|
||
}
|
||
|
||
// 播放视频
|
||
const playVideo = () => {
|
||
isPlaying.value = !isPlaying.value
|
||
}
|
||
|
||
// 搜索话题
|
||
const searchHashtag = (tag) => {
|
||
console.log('搜索话题:', tag)
|
||
// 实际项目中跳转到话题搜索页
|
||
// router.push(`/search/hashtag/${tag}`)
|
||
}
|
||
|
||
// 打开地图
|
||
const openMap = () => {
|
||
uni.showToast({ title: '地图功能开发中', icon: 'none' })
|
||
}
|
||
|
||
// 编辑动态
|
||
const editDynamic = () => {
|
||
console.log('编辑动态')
|
||
closeActionsModal()
|
||
// 实际项目中跳转到编辑页
|
||
// router.push(`/dynamic/${dynamicId.value}/edit`)
|
||
}
|
||
|
||
// 删除动态
|
||
const deleteDynamic = () => {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '确定删除该动态?',
|
||
success: (r) => { if (r.confirm) uni.navigateBack() }
|
||
})
|
||
closeActionsModal()
|
||
}
|
||
|
||
// 举报动态
|
||
const reportDynamic = () => {
|
||
uni.showToast({ title: '已举报' })
|
||
closeActionsModal()
|
||
}
|
||
|
||
|
||
|
||
// 加载更多评论
|
||
const loadMoreComments = () => {
|
||
uni.showToast({ title: '已加载全部评论', icon: 'none' })
|
||
}
|
||
|
||
// 提交评论
|
||
const submitComment = () => {
|
||
if (!canSubmitComment.value) return
|
||
publishingComment.value = true
|
||
const content = commentInput.value.trim()
|
||
const parentId = replyToComment.value != null ? replyToComment.value.id : 0
|
||
const replyToUid = replyToUser.value != null ? replyToUser.value.id : 0
|
||
Api.moment.comment({ dynamic_id: momentId.value, content: content, parent_id: parentId, reply_to_user_id: replyToUid })
|
||
.then((r : UTSJSONObject) => {
|
||
const c = r.get('data') as UTSJSONObject
|
||
const mapped = c ? mapComment(c) : {
|
||
id: Date.now(), username: currentUser.username, avatar: currentUser.avatar, uid: currentUser.id,
|
||
content: content, createTime: Date.now(), like: 0, liked: false, replyCount: 0,
|
||
replyTo: replyToUser.value, replies: []
|
||
}
|
||
if (parentId > 0 && replyToComment.value != null) {
|
||
const pc = comments.value.find(x => x.id == replyToComment.value!.id)
|
||
if (pc != null) pc.replies.push(mapped)
|
||
} else {
|
||
comments.value.unshift(mapped)
|
||
}
|
||
commentCount.value++
|
||
commentInput.value = ''
|
||
replyToUser.value = null
|
||
replyToComment.value = null
|
||
})
|
||
.catch(() => { uni.showToast({ title: '评论失败', icon: 'none' }) })
|
||
.finally(() => { publishingComment.value = false })
|
||
}
|
||
|
||
// 开始回复
|
||
const startReply = (comment : any, reply : any = null) => {
|
||
if (reply) {
|
||
replyToUser.value = { id: reply.uid, username: reply.username }
|
||
replyToComment.value = comment
|
||
} else {
|
||
replyToUser.value = { id: comment.uid, username: comment.username }
|
||
replyToComment.value = comment
|
||
}
|
||
scrollToComment()
|
||
}
|
||
|
||
// 取消回复
|
||
const cancelReply = () => {
|
||
replyToUser.value = null
|
||
replyToComment.value = null
|
||
}
|
||
|
||
// 切换评论点赞
|
||
const toggleCommentLike = (comment) => {
|
||
comment.liked = !comment.liked
|
||
comment.like += comment.liked ? 1 : -1
|
||
// 模拟API请求
|
||
// api.toggleLikeComment(comment.id, comment.liked)
|
||
}
|
||
|
||
// 切换回复点赞
|
||
const toggleReplyLike = (parentComment, reply) => {
|
||
reply.liked = !reply.liked
|
||
reply.like += reply.liked ? 1 : -1
|
||
// 模拟API请求
|
||
// api.toggleLikeReply(reply.id, reply.liked)
|
||
}
|
||
|
||
// 删除评论
|
||
const deleteComment = (comment : any) => {
|
||
uni.showModal({
|
||
title: '提示', content: '确定删除这条评论吗?',
|
||
success: (r) => {
|
||
if (r.confirm) {
|
||
const index = comments.value.findIndex(c => c.id === comment.id)
|
||
if (index !== -1) {
|
||
comments.value.splice(index, 1)
|
||
commentCount.value--
|
||
dynamicData.comment--
|
||
}
|
||
}
|
||
}
|
||
})
|
||
showCommentActions.value = null
|
||
}
|
||
|
||
// 删除回复
|
||
const deleteReply = (parentComment : any, reply : any) => {
|
||
uni.showModal({
|
||
title: '提示', content: '确定删除这条回复吗?',
|
||
success: (r) => {
|
||
if (r.confirm) {
|
||
const index = parentComment.replies.findIndex((x : any) => x.id === reply.id)
|
||
if (index !== -1) {
|
||
parentComment.replies.splice(index, 1)
|
||
parentComment.replyCount--
|
||
}
|
||
}
|
||
}
|
||
})
|
||
showCommentActions.value = null
|
||
}
|
||
|
||
// 举报评论
|
||
const reportComment = (comment : any) => {
|
||
uni.showToast({ title: '已举报' })
|
||
showCommentActions.value = null
|
||
}
|
||
|
||
// 加载更多回复
|
||
const loadMoreReplies = (comment : any) => {
|
||
uni.showToast({ title: '已加载全部回复', icon: 'none' })
|
||
}
|
||
|
||
// 切换评论操作菜单
|
||
const toggleCommentActions = (commentId) => {
|
||
showCommentActions.value = showCommentActions.value === commentId ? null : commentId
|
||
}
|
||
|
||
// 切换表情选择器
|
||
const toggleEmoji = () => {
|
||
showEmoji.value = !showEmoji.value
|
||
}
|
||
|
||
// 插入表情
|
||
const insertEmoji = (emoji) => {
|
||
commentInput.value += emoji
|
||
showEmoji.value = false
|
||
|
||
// 重新聚焦输入框
|
||
nextTick(() => {
|
||
if (commentTextareaRef.value) {
|
||
commentTextareaRef.value.focus()
|
||
}
|
||
})
|
||
}
|
||
|
||
// 评论输入处理
|
||
const onCommentInput = () => {
|
||
// 可以添加输入限制、敏感词检测等
|
||
}
|
||
|
||
// 加载评论(已由 loadMoment 通过后端返回,无需独立 mock 加载)
|
||
|
||
// 加载动态数据
|
||
const loadMoment = () => {
|
||
Api.moment.detail(momentId.value)
|
||
.then((res : UTSJSONObject) => {
|
||
const m = res.get('moment') as UTSJSONObject
|
||
if (m != null) {
|
||
dynamicData.id = Number(m.get('id'))
|
||
dynamicData.uid = Number(m.get('uid') ?? m.get('userId') ?? 0)
|
||
dynamicData.avatar = (m.get('userAvatar') ?? '') as string
|
||
dynamicData.username = (m.get('userName') ?? '') as string
|
||
dynamicData.createTime = Number(m.get('time') ?? 0)
|
||
dynamicData.location = (m.get('location') ?? '') as string
|
||
dynamicData.coordinates = null
|
||
dynamicData.content = (m.get('content') ?? '') as string
|
||
dynamicData.media = (m.get('media') ?? []) as any[]
|
||
dynamicData.hashtags = (m.get('hashtags') ?? []) as any[]
|
||
dynamicData.like = Number(m.get('likes') ?? 0)
|
||
dynamicData.comment = Number(m.get('comments') ?? 0)
|
||
dynamicData.share = Number(m.get('shares') ?? 0)
|
||
dynamicData.view = Number(m.get('view') ?? 0)
|
||
dynamicData.likede = !!(m.get('isLiked'))
|
||
const inter = (m.get('interaction') as UTSJSONObject) ?? null
|
||
dynamicData.collected = inter ? !!inter.get('collected') : false
|
||
dynamicData.isFollowed = inter ? !!inter.get('followed') : false
|
||
}
|
||
const cs = (res.get('comments') as Array<UTSJSONObject>) ?? []
|
||
comments.value = cs.map((c : UTSJSONObject) => mapComment(c))
|
||
commentCount.value = Number(res.get('comment_total') ?? comments.value.length)
|
||
})
|
||
.catch(() => {})
|
||
}
|
||
|
||
const mapComment = (c : UTSJSONObject) : any => {
|
||
const user = (c.get('user') as UTSJSONObject) ?? ({} as UTSJSONObject)
|
||
const replyTo = (c.get('reply_to') as UTSJSONObject) ?? null
|
||
const replies = (c.get('replies') as Array<UTSJSONObject>) ?? []
|
||
return {
|
||
id: Number(c.get('id')),
|
||
username: (user.get('nickname') ?? '') as string,
|
||
avatar: (user.get('avatar') ?? '') as string,
|
||
uid: Number(user.get('id') ?? 0),
|
||
content: (c.get('content') ?? '') as string,
|
||
createTime: (Number(c.get('create_time') ?? 0)) * 1000,
|
||
like: Number(c.get('like_count') ?? 0),
|
||
liked: false,
|
||
replyCount: Number(c.get('reply_count') ?? 0),
|
||
replyTo: replyTo ? { id: Number(replyTo.get('id')), username: (replyTo.get('nickname') ?? '') as string } : null,
|
||
replies: replies.map((r : UTSJSONObject) => {
|
||
const ru = (r.get('user') as UTSJSONObject) ?? ({} as UTSJSONObject)
|
||
const rrt = (r.get('reply_to') as UTSJSONObject) ?? null
|
||
return {
|
||
id: Number(r.get('id')),
|
||
username: (ru.get('nickname') ?? '') as string,
|
||
avatar: (ru.get('avatar') ?? '') as string,
|
||
uid: Number(ru.get('id') ?? 0),
|
||
content: (r.get('content') ?? '') as string,
|
||
createTime: (Number(r.get('create_time') ?? 0)) * 1000,
|
||
like: Number(r.get('like_count') ?? 0),
|
||
liked: false,
|
||
replyCount: 0,
|
||
replyTo: rrt ? { id: Number(rrt.get('id')), username: (rrt.get('nickname') ?? '') as string } : null,
|
||
replies: []
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
onLoad((options : any) => {
|
||
momentId.value = Number(options?.id ?? 0)
|
||
if (momentId.value > 0) loadMoment()
|
||
})
|
||
|
||
// 浏览量由后端 red 接口自动累加,无需前端处理
|
||
</script>
|
||
|
||
<style scoped>
|
||
|
||
.header-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.header-icon {
|
||
margin: 3rpx 5rpx 3rpx 5rpx;
|
||
border-radius: 50%;
|
||
font-size: 18px;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
|
||
/* 动态内容 */
|
||
.dynamic-content {
|
||
background: white;
|
||
padding: 100rpx 16px 20px 16px;
|
||
}
|
||
|
||
/* 作者信息 */
|
||
.author-info {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.author-avatar {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 50%;
|
||
object-fit: cover;
|
||
border: 2px solid #e0e0e0;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.author-details {
|
||
flex: 1;
|
||
}
|
||
|
||
.author-name {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.post-info {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
gap: 12px;
|
||
font-size: 13px;
|
||
color: #999;
|
||
}
|
||
|
||
.post-location {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.post-location i {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.follow-btn,
|
||
.following-btn {
|
||
padding: 6px 16px;
|
||
border-radius: 20px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
border: none;
|
||
}
|
||
|
||
.follow-btn {
|
||
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
|
||
color: white;
|
||
}
|
||
|
||
.follow-btn:hover {
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.following-btn {
|
||
background: #f0f0f0;
|
||
color: #666;
|
||
}
|
||
|
||
.following-btn:hover {
|
||
background: #e0e0e0;
|
||
}
|
||
|
||
.follow-btn i {
|
||
margin-right: 4px;
|
||
font-size: 11px;
|
||
}
|
||
|
||
/* 动态文本内容 */
|
||
.content-text {
|
||
font-size: 16px;
|
||
line-height: 1.6;
|
||
color: #333;
|
||
margin-bottom: 16px;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
}
|
||
|
||
/* 媒体内容 */
|
||
.content-media {
|
||
flex: 1;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
flex: 0 0 236rpx;
|
||
margin-bottom: 16px;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.single-media {
|
||
width: 100%;
|
||
max-height: 500px;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.media-image {
|
||
width: 100%;
|
||
height: auto;
|
||
max-height: 500px;
|
||
object-fit: contain;
|
||
cursor: pointer;
|
||
transition: transform 0.3s;
|
||
}
|
||
|
||
.media-image:hover {
|
||
transform: scale(1.02);
|
||
}
|
||
|
||
.media-video {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 300px;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
background: #000;
|
||
}
|
||
|
||
.video-player {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.video-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(0, 0, 0, 0.3);
|
||
color: white;
|
||
font-size: 48px;
|
||
cursor: pointer;
|
||
transition: background 0.3s;
|
||
}
|
||
|
||
.video-overlay:hover {
|
||
background: rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
/* 多图网格 */
|
||
.multi-media {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 4px;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.media-grid-item {
|
||
position: relative;
|
||
aspect-ratio: 1/1;
|
||
background-color: #f5f5f5;
|
||
cursor: pointer;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.media-grid-item img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
transition: transform 0.3s;
|
||
}
|
||
|
||
.media-grid-item:hover img {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.video-thumbnail {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.video-play-icon {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
color: white;
|
||
font-size: 24px;
|
||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
.more-indicator {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
color: white;
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 网格类 */
|
||
.grid-2 {
|
||
grid-column: text 1;
|
||
}
|
||
|
||
.grid-3 {
|
||
grid-column: text 1;
|
||
}
|
||
|
||
.grid-4 {
|
||
grid-column: text 1;
|
||
}
|
||
|
||
/* 位置信息 */
|
||
.location-section {
|
||
margin: 16px 0;
|
||
padding: 12px;
|
||
background: #f8f9fa;
|
||
border-radius: 12px;
|
||
border: 1px solid #e9ecef;
|
||
}
|
||
|
||
.location-header {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
font-size: 14px;
|
||
color: #333;
|
||
}
|
||
|
||
.location-header i {
|
||
color: #f74c31;
|
||
}
|
||
|
||
.location-name {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.map-preview {
|
||
width: 100%;
|
||
height: 100px;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
cursor: pointer;
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-size: 14px;
|
||
transition: opacity 0.3s;
|
||
}
|
||
|
||
.map-preview:hover {
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.map-placeholder {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.map-placeholder i {
|
||
font-size: 20px;
|
||
}
|
||
|
||
/* 话题标签 */
|
||
.hashtag-section {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin: 16px 0;
|
||
}
|
||
|
||
.hashtag {
|
||
display: inline-block;
|
||
padding: 4px 10px;
|
||
background: #e6f3ff;
|
||
color: #2575fc;
|
||
font-size: 13px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.hashtag:hover {
|
||
background: #d1e7ff;
|
||
}
|
||
|
||
/* 统计信息 */
|
||
.stats-section {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
justify-content: space-around;
|
||
padding: 16px 0;
|
||
margin: 16px 0;
|
||
border-top: 1px solid #f0f0f0;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.stats-item {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
color: #666;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.stats-item i {
|
||
font-size: 18px;
|
||
color: #999;
|
||
}
|
||
|
||
/* 操作按钮 */
|
||
.action-section {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
justify-content: space-around;
|
||
padding: 12px 0;
|
||
border-top: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.action-button {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
background: none;
|
||
border: none;
|
||
color: #666;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
transition: color 0.2s;
|
||
padding: 8px 12px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.action-button:hover {
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.action-button i {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.action-button text {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.like-btn.active {
|
||
color: #f74c31;
|
||
}
|
||
|
||
.comment-btn:hover {
|
||
color: #2575fc;
|
||
}
|
||
|
||
.share-btn:hover {
|
||
color: #6a11cb;
|
||
}
|
||
|
||
.collect-btn.active {
|
||
color: #ffc107;
|
||
}
|
||
|
||
/* 评论区域 */
|
||
.comment-section {
|
||
background: white;
|
||
border-radius: 12px 12px 0 0;
|
||
padding: 20px 16px;
|
||
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 12px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
/* 评论容器 */
|
||
.comment-container {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 0;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.comment-input-area {
|
||
margin-bottom: 24px;
|
||
padding-bottom: 20px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.input-header {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
gap: 12px;
|
||
}
|
||
|
||
.user-avatar {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 50%;
|
||
object-fit: cover;
|
||
border: 2px solid #e0e0e0;
|
||
}
|
||
|
||
.input-wrapper {
|
||
flex: 1;
|
||
}
|
||
|
||
.reply-hint {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
background: #f5f5f5;
|
||
padding: 6px 12px;
|
||
border-radius: 6px;
|
||
margin-bottom: 8px;
|
||
font-size: 13px;
|
||
color: #666;
|
||
}
|
||
|
||
.cancel-reply {
|
||
background: none;
|
||
border: none;
|
||
color: #999;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
padding: 0 4px;
|
||
}
|
||
|
||
.cancel-reply:hover {
|
||
color: #333;
|
||
}
|
||
|
||
.input-box {
|
||
position: relative;
|
||
}
|
||
|
||
.comment-textarea {
|
||
width: 100%;
|
||
min-height: 80px;
|
||
padding: 12px;
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
resize: vertical;
|
||
transition: border-color 0.2s;
|
||
}
|
||
|
||
.comment-textarea:focus {
|
||
outline: none;
|
||
border-color: #2575fc;
|
||
box-shadow: 0 0 0 2px rgba(37, 117, 252, 0.1);
|
||
}
|
||
|
||
.input-actions {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.emoji-picker {
|
||
position: relative;
|
||
}
|
||
|
||
.action-btn {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 6px 12px;
|
||
background: #f5f5f5;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
color: #666;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.action-btn:hover {
|
||
background: #e0e0e0;
|
||
}
|
||
|
||
.emoji-list {
|
||
position: absolute;
|
||
bottom: 100%;
|
||
left: 0;
|
||
background: white;
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 8px;
|
||
padding: 8px;
|
||
max-width: 240px;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
display: grid;
|
||
grid-template-columns: repeat(8, 1fr);
|
||
gap: 4px;
|
||
z-index: 1000;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.emoji-list text {
|
||
display: inline-flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 24px;
|
||
height: 24px;
|
||
cursor: pointer;
|
||
font-size: 18px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.emoji-list text:hover {
|
||
background: #f0f0f0;
|
||
}
|
||
|
||
.word-count {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
|
||
.submit-btn {
|
||
padding: 6px 20px;
|
||
background: #2575fc;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.submit-btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.submit-btn:not(:disabled):hover {
|
||
background: #1c68e8;
|
||
}
|
||
|
||
.comment-list {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.comment-item {
|
||
margin-bottom: 20px;
|
||
padding-bottom: 20px;
|
||
border-bottom: 1px solid #f5f5f5;
|
||
}
|
||
|
||
.comment-item:last-child {
|
||
border-bottom: none;
|
||
margin-bottom: 0;
|
||
padding-bottom: 0;
|
||
}
|
||
|
||
.comment-main {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
gap: 12px;
|
||
}
|
||
|
||
.comment-avatar {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.comment-avatar img {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 50%;
|
||
object-fit: cover;
|
||
border: 1px solid #e0e0e0;
|
||
}
|
||
|
||
.comment-content {
|
||
flex: 1;
|
||
}
|
||
|
||
.comment-header {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.username {
|
||
font-weight: 500;
|
||
font-size: 14px;
|
||
color: #333;
|
||
}
|
||
|
||
.comment-time {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
|
||
.comment-text {
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
color: #333;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.reply-to {
|
||
color: #2575fc;
|
||
margin-right: 4px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.reply-to:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.comment-actions {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.like-btn {
|
||
padding: 4px 8px;
|
||
background: transparent;
|
||
border: none;
|
||
color: #666;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.like-btn.active {
|
||
color: #f74c31;
|
||
}
|
||
|
||
.like-btn i {
|
||
margin-right: 4px;
|
||
}
|
||
|
||
.reply-btn {
|
||
padding: 4px 8px;
|
||
background: transparent;
|
||
border: none;
|
||
color: #666;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.reply-btn:hover {
|
||
color: #2575fc;
|
||
}
|
||
|
||
.more-actions {
|
||
position: relative;
|
||
}
|
||
|
||
.more-btn {
|
||
padding: 4px 8px;
|
||
background: transparent;
|
||
border: none;
|
||
color: #999;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.more-menu {
|
||
position: absolute;
|
||
top: 100%;
|
||
right: 0;
|
||
background: white;
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 6px;
|
||
padding: 4px 0;
|
||
min-width: 100px;
|
||
z-index: 100;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.menu-item {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
gap: 8px;
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
background: none;
|
||
border: none;
|
||
text-align: left;
|
||
font-size: 12px;
|
||
color: #666;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.menu-item:hover {
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.menu-item i {
|
||
width: 16px;
|
||
text-align: center;
|
||
}
|
||
|
||
.delete-btn {
|
||
color: #f74c31;
|
||
}
|
||
|
||
.report-btn {
|
||
color: #666;
|
||
}
|
||
|
||
.reply-list {
|
||
margin-top: 16px;
|
||
padding-left: 12px;
|
||
border-left: 2px solid #f0f0f0;
|
||
}
|
||
|
||
.reply-item {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
padding: 12px;
|
||
background: #f9f9f9;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.reply-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.reply-avatar img {
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 50%;
|
||
object-fit: cover;
|
||
border: 1px solid #e0e0e0;
|
||
}
|
||
|
||
.reply-content {
|
||
flex: 1;
|
||
}
|
||
|
||
.reply-header {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.reply-time {
|
||
font-size: 11px;
|
||
color: #999;
|
||
}
|
||
|
||
.reply-text {
|
||
font-size: 13px;
|
||
line-height: 1.4;
|
||
color: #333;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.reply-actions {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.reply-actions .like-btn,
|
||
.reply-actions .reply-btn {
|
||
font-size: 11px;
|
||
padding: 2px 6px;
|
||
}
|
||
|
||
.reply-actions .more-btn {
|
||
padding: 2px 6px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.view-more-replies {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 8px 12px;
|
||
color: #2575fc;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
background: transparent;
|
||
border: none;
|
||
width: 100%;
|
||
justify-content: center;
|
||
border-radius: 4px;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.view-more-replies:hover {
|
||
background: #f0f8ff;
|
||
}
|
||
|
||
.view-more-replies i {
|
||
font-size: 10px;
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.view-more-replies:hover i {
|
||
transform: translateY(2px);
|
||
}
|
||
|
||
.load-more-comments {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 12px;
|
||
color: #666;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
border-radius: 8px;
|
||
transition: background 0.2s;
|
||
margin-top: 20px;
|
||
border: 1px dashed #e0e0e0;
|
||
}
|
||
|
||
.load-more-comments:hover {
|
||
background: #f5f5f5;
|
||
color: #2575fc;
|
||
}
|
||
|
||
.empty-comments {
|
||
text-align: center;
|
||
padding: 40px 20px;
|
||
color: #999;
|
||
}
|
||
|
||
.empty-comments i {
|
||
font-size: 48px;
|
||
margin-bottom: 12px;
|
||
color: #e0e0e0;
|
||
}
|
||
|
||
.empty-comments p {
|
||
font-size: 14px;
|
||
margin: 0;
|
||
}
|
||
|
||
/* 弹窗样式 */
|
||
.media-preview-modal,
|
||
.share-modal,
|
||
.actions-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.8);
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 2000;
|
||
padding: 20px;
|
||
}
|
||
|
||
.media-preview-modal .modal-content,
|
||
.share-modal .modal-content,
|
||
.actions-modal .modal-content {
|
||
background: white;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* 媒体预览弹窗 */
|
||
.media-preview-modal .modal-content {
|
||
width: 90%;
|
||
height: 90%;
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
justify-content: flex-end;
|
||
padding: 12px 16px;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
.close-btn {
|
||
background: none;
|
||
border: none;
|
||
color: white;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
width: 40px;
|
||
height: 40px;
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 50%;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.close-btn:hover {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.modal-body {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
background: black;
|
||
}
|
||
|
||
.preview-image {
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.modal-footer {
|
||
padding: 12px 16px;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
color: white;
|
||
text-align: center;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* 分享弹窗 */
|
||
.share-content {
|
||
width: 300px;
|
||
background: white;
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.share-header {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 20px 20px 0 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.share-header text {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
color: #333;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.share-options {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 20px;
|
||
padding: 0 20px 20px 20px;
|
||
}
|
||
|
||
.share-option {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8px;
|
||
cursor: pointer;
|
||
padding: 12px 0;
|
||
border-radius: 8px;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.share-option:hover {
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.share-option i {
|
||
font-size: 28px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.share-option:nth-child(1) i {
|
||
color: #07c160;
|
||
}
|
||
|
||
/* 微信 */
|
||
.share-option:nth-child(2) i {
|
||
color: #12b7f5;
|
||
}
|
||
|
||
/* QQ */
|
||
.share-option:nth-child(3) i {
|
||
color: #e6162d;
|
||
}
|
||
|
||
/* 微博 */
|
||
.share-option:nth-child(4) i {
|
||
color: #666;
|
||
}
|
||
|
||
/* 复制链接 */
|
||
|
||
.share-option text {
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
|
||
/* 操作弹窗 */
|
||
.actions-content {
|
||
width: 280px;
|
||
background: white;
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.action-list {
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.action-item {
|
||
display: flex;
|
||
flex-flow: row nowrap;
|
||
align-items: center;
|
||
width: 100%;
|
||
padding: 16px 20px;
|
||
background: none;
|
||
border: none;
|
||
text-align: left;
|
||
font-size: 16px;
|
||
color: #333;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.action-item:hover {
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.action-item i {
|
||
width: 24px;
|
||
margin-right: 12px;
|
||
font-size: 18px;
|
||
text-align: center;
|
||
}
|
||
|
||
.delete-action {
|
||
color: #f74c31;
|
||
}
|
||
|
||
.modal-footer {
|
||
padding: 12px 20px;
|
||
border-top: 1px solid #f0f0f0;
|
||
text-align: center;
|
||
}
|
||
|
||
.cancel-btn {
|
||
width: 100%;
|
||
padding: 12px 0;
|
||
background: #f5f5f5;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.cancel-btn:hover {
|
||
background: #e0e0e0;
|
||
}
|
||
</style> |