1.4
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user