This commit is contained in:
2026-06-13 10:40:59 +08:00
parent ebcfb0c258
commit 318ddd85a5
15 changed files with 614 additions and 30 deletions
+71 -18
View File
@@ -1,6 +1,6 @@
<template>
<div class="moment-card">
<!-- 头部头像 + 用户名 + 时间 -->
<!-- 头部头像 + 用户名 + 时间 + 删除 -->
<div class="moment-header">
<n-avatar :size="40" round :style="{ background: 'var(--color-primary)' }">
{{ (moment.nickname || moment.username || '?')[0] }}
@@ -9,17 +9,21 @@
<span class="author-name">{{ moment.nickname || moment.username }}</span>
<span class="post-time">{{ formatTime(moment.created_at) }}</span>
</div>
<span v-if="isMine" class="delete-btn" title="删除" @click="handleDelete">🗑</span>
</div>
<!-- 内容 -->
<div class="moment-content">{{ moment.content }}</div>
<!-- 图片 -->
<div v-if="moment.images && moment.images.length > 0" class="moment-images" :class="`grid-${Math.min(moment.images.length, 3)}`">
<div v-for="(img, i) in moment.images" :key="i" class="image-item">
<img :src="img" :alt="`图片${i + 1}`" />
<n-image-group v-if="moment.images && moment.images.length > 0">
<div class="moment-images" :class="`grid-${Math.min(moment.images.length, 3)}`">
<div v-for="(img, i) in moment.images" :key="i" class="image-item">
<n-image :src="img" :alt="`图片${i + 1}`" object-fit="cover"
:img-props="{ style: 'width:100%;height:100%;object-fit:cover' }" />
</div>
</div>
</div>
</n-image-group>
<!-- 操作栏 -->
<div class="moment-actions">
@@ -35,25 +39,35 @@
<div v-if="showComments" class="comments-section">
<div v-if="comments.length > 0" class="comments-list">
<div v-for="comment in comments" :key="comment.id" class="comment-item">
<span class="comment-author">{{ comment.nickname || comment.username }}</span>
<span v-if="comment.reply_to_username" class="comment-reply">
回复 <span class="comment-author">{{ comment.reply_to_username }}</span>
</span>
<span class="comment-text">{{ comment.content }}</span>
<div class="comment-content-row">
<span class="comment-author">{{ comment.nickname || comment.username }}</span>
<span v-if="comment.reply_to_username" class="comment-reply">
回复 <span class="comment-author">{{ comment.reply_to_username }}</span>
</span>
<span class="comment-text">{{ comment.content }}</span>
<span class="comment-reply-btn" @click="setReplyTarget(comment)">回复</span>
</div>
</div>
</div>
<div class="comment-input">
<n-input v-model:value="commentText" placeholder="写评论..." size="small"
@keydown.enter.prevent="submitComment" />
<n-button size="tiny" type="primary" :disabled="!commentText.trim()" @click="submitComment">发送</n-button>
<div class="comment-input-wrapper">
<div v-if="replyTarget" class="reply-indicator">
<span>回复 @{{ replyTarget.nickname || replyTarget.username }}</span>
<span class="reply-cancel" @click="replyTarget = null"></span>
</div>
<div class="comment-input">
<n-input v-model:value="commentText" :placeholder="replyTarget ? `回复 @${replyTarget.nickname || replyTarget.username}...` : '写评论...'" size="small"
@keydown.enter.prevent="submitComment" />
<n-button size="tiny" type="primary" :disabled="!commentText.trim()" @click="submitComment">发送</n-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ref, computed, watch } from 'vue'
import { momentsApi } from '@/api/moments'
import { useAuthStore } from '@/stores/auth'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import 'dayjs/locale/zh-cn'
@@ -65,11 +79,16 @@ const props = defineProps<{ moment: any }>()
const emit = defineEmits<{
'toggle-like': [momentId: string]
'comment': [momentId: string, content: string]
'delete': [momentId: string]
}>()
const auth = useAuthStore()
const showComments = ref(false)
const comments = ref<any[]>([])
const commentText = ref('')
const replyTarget = ref<any>(null)
const isMine = computed(() => props.moment.user_id === auth.user?.id)
watch(showComments, async (val) => {
if (val && comments.value.length === 0) {
@@ -80,13 +99,26 @@ watch(showComments, async (val) => {
}
})
function setReplyTarget(comment: any) {
replyTarget.value = comment
}
async function handleDelete() {
if (!confirm('确定要删除这条动态吗?')) return
try {
emit('delete', props.moment.id)
} catch {}
}
async function submitComment() {
if (!commentText.value.trim()) return
try {
const { data } = await momentsApi.addComment(props.moment.id, commentText.value.trim())
const replyToId = replyTarget.value?.id || undefined
const { data } = await momentsApi.addComment(props.moment.id, commentText.value.trim(), replyToId)
comments.value.push(data)
emit('comment', props.moment.id, commentText.value.trim())
commentText.value = ''
replyTarget.value = null
} catch {}
}
@@ -110,6 +142,12 @@ function formatTime(time: string) {
border: 1px solid var(--color-border);
}
.moment-header { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.delete-btn {
margin-left: auto; font-size: 14px; cursor: pointer;
opacity: 0; transition: opacity 0.2s; padding: 4px;
}
.moment-card:hover .delete-btn { opacity: 0.6; }
.delete-btn:hover { opacity: 1 !important; }
.header-info { display: flex; flex-direction: column; }
.author-name { font-weight: 600; font-size: 14px; }
.post-time { font-size: 11px; color: var(--color-text-hint); }
@@ -124,7 +162,7 @@ function formatTime(time: string) {
.moment-images.grid-2 { grid-template-columns: 1fr 1fr; }
.moment-images.grid-3 { grid-template-columns: 1fr 1fr 1fr; }
.image-item { aspect-ratio: 1; overflow: hidden; border-radius: 6px; cursor: pointer; }
.image-item img { width: 100%; height: 100%; object-fit: cover; }
.image-item :deep(img) { width: 100%; height: 100%; object-fit: cover; }
.moment-actions {
display: flex; gap: 16px; padding-top: 8px;
@@ -143,9 +181,24 @@ function formatTime(time: string) {
font-size: 13px; padding: 4px 0; line-height: 1.5;
color: var(--color-text-primary);
}
.comment-content-row { display: inline; }
.comment-author { color: var(--color-primary); font-weight: 500; cursor: pointer; }
.comment-reply { color: var(--color-text-hint); }
.comment-text { color: var(--color-text-primary); }
.comment-reply-btn {
font-size: 11px; color: var(--color-text-hint); cursor: pointer;
margin-left: 6px; opacity: 0; transition: opacity 0.2s;
}
.comment-item:hover .comment-reply-btn { opacity: 1; }
.comment-reply-btn:hover { color: var(--color-primary); }
.comment-input { display: flex; gap: 6px; margin-top: 6px; }
.comment-input-wrapper { margin-top: 6px; }
.reply-indicator {
display: flex; align-items: center; justify-content: space-between;
padding: 4px 8px; margin-bottom: 4px; background: var(--color-primary-lightest);
border-radius: 6px; font-size: 12px; color: var(--color-primary);
}
.reply-cancel { cursor: pointer; font-size: 12px; padding: 2px; }
.reply-cancel:hover { color: var(--color-text-primary); }
.comment-input { display: flex; gap: 6px; }
</style>
@@ -30,6 +30,7 @@
:moment="moment"
@toggle-like="handleToggleLike"
@comment="handleComment"
@delete="handleDelete"
/>
</div>
@@ -156,6 +157,15 @@ async function handleComment(momentId: string, content: string) {
message.error('评论失败')
}
}
async function handleDelete(momentId: string) {
try {
await momentsStore.deleteMoment(momentId)
message.success('已删除')
} catch {
message.error('删除失败')
}
}
</script>
<style scoped>
@@ -0,0 +1,67 @@
<template>
<div class="moments-sidebar">
<div class="panel-header">
<h3 class="panel-title">朋友圈</h3>
</div>
<div class="sidebar-content">
<div class="sidebar-intro">
<div class="intro-icon">🌿</div>
<p class="intro-text">分享你的生活点滴</p>
</div>
<div class="sidebar-stats">
<div class="stat-item">
<span class="stat-value">{{ momentsStore.feed.length }}</span>
<span class="stat-label">动态</span>
</div>
</div>
<div class="sidebar-tips">
<div class="tip-item" @click="showCompose = true">
<span class="tip-icon"></span>
<span class="tip-text">发布新动态</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useMomentsStore } from '@/stores/moments'
const momentsStore = useMomentsStore()
const showCompose = ref(false)
</script>
<style scoped>
.moments-sidebar {
display: flex;
flex-direction: column;
height: 100%;
}
.panel-header {
padding: 16px;
border-bottom: 1px solid var(--color-border);
}
.panel-title { margin: 0; font-size: 16px; font-weight: 600; }
.sidebar-content { flex: 1; padding: 16px; }
.sidebar-intro {
text-align: center; padding: 30px 16px; margin-bottom: 16px;
background: var(--color-primary-lightest); border-radius: 12px;
}
.intro-icon { font-size: 36px; margin-bottom: 8px; }
.intro-text { font-size: 14px; color: var(--color-text-secondary); margin: 0; }
.sidebar-stats {
display: flex; justify-content: center; gap: 24px; margin-bottom: 20px;
}
.stat-item { text-align: center; }
.stat-value { display: block; font-size: 20px; font-weight: 600; color: var(--color-primary); }
.stat-label { font-size: 12px; color: var(--color-text-hint); }
.sidebar-tips { margin-top: 16px; }
.tip-item {
display: flex; align-items: center; gap: 8px; padding: 10px 12px;
border-radius: 8px; cursor: pointer; transition: background 0.15s;
}
.tip-item:hover { background: var(--color-primary-lightest); }
.tip-icon { font-size: 16px; }
.tip-text { font-size: 14px; color: var(--color-text-primary); }
</style>