250 lines
6.7 KiB
Vue
Executable File
250 lines
6.7 KiB
Vue
Executable File
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { fetchCommentThread, postReply, type CommentItem } from '@/api/comments'
|
|
import { isAuthenticated } from '@/utils/auth'
|
|
import { userInfoStore } from '@/store/UserInfo'
|
|
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const userInfo = userInfoStore()
|
|
const user_id = ref(-1)
|
|
|
|
const id = Number(route.params.id)
|
|
|
|
const root = ref<any>({})
|
|
const replies = ref<any>([])
|
|
const loading = ref(false)
|
|
|
|
const load = async () => {
|
|
loading.value = true
|
|
try {
|
|
const data = await fetchCommentThread(id)
|
|
replies.value = Array.isArray(data['replies']) ? data['replies'] : []
|
|
console.log(replies.value)
|
|
root.value = data['parent']
|
|
await userInfo.ensureUserInfoLoaded()
|
|
user_id.value = userInfo.userInfo.id
|
|
} catch (e: any) {
|
|
// 占位数据,便于前后端未对接时验证界面
|
|
root.value = { id, user: 'Alex', time: '刚刚', content: '这是原始评论' }
|
|
replies.value = [
|
|
{ id: id * 10 + 1, user: 'Mia', time: '1 分钟前', content: '同意你的观点', parentId: id, toUser: 'Alex' },
|
|
{ id: id * 10 + 2, user: 'Ken', time: '2 分钟前', content: '我有不同看法', parentId: id, toUser: 'Mia' }
|
|
]
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(load)
|
|
|
|
const replyText = ref('')
|
|
const replyPosting = ref(false)
|
|
const replyTo = ref<any | null>(null)
|
|
|
|
const doReply = (c: any) => {
|
|
if (!isAuthenticated()) {
|
|
ElMessage.warning('请先登录后再回复')
|
|
return
|
|
}
|
|
replyTo.value = c
|
|
}
|
|
|
|
const submitReply = async () => {
|
|
if (!replyTo.value || !replyText.value.trim()) {
|
|
ElMessage.warning('请输入回复内容')
|
|
return
|
|
}
|
|
replyPosting.value = true
|
|
try {
|
|
await postReply(Number(route.params.id), replyText.value.trim(), user_id.value, replyTo.value.id)
|
|
ElMessage.success('回复成功')
|
|
const data = await fetchCommentThread(id)
|
|
replies.value = Array.isArray(data['replies']) ? data['replies'] : []
|
|
replyText.value = ''
|
|
replyTo.value = null
|
|
} catch (e: any) {
|
|
ElMessage.error(e?.message || '回复失败')
|
|
} finally {
|
|
replyPosting.value = false
|
|
}
|
|
}
|
|
|
|
const getReplyContent = (reply_id: number) => {
|
|
const r = replies.value.find((item: any) => item.id === reply_id)
|
|
return r?.content || root.value.content
|
|
}
|
|
|
|
const deleteRootCom = async (id: number) => {
|
|
if (!isAuthenticated()) {
|
|
ElMessage.warning('请先登录后再删除评论')
|
|
return
|
|
}
|
|
try {
|
|
await ElMessageBox.confirm('确定删除该评论?删除后该页面将不存在', '确认删除', {
|
|
confirmButtonText: '确定',
|
|
cancelButtonText: '取消',
|
|
type: 'warning'
|
|
})
|
|
} catch {
|
|
return
|
|
}
|
|
try {
|
|
await fetch(`/api/comments/${id}`, { method: 'DELETE', credentials: 'include' })
|
|
ElMessage.success('删除成功')
|
|
router.push('/')
|
|
} catch (e: any) {
|
|
ElMessage.error(e?.message || '删除失败')
|
|
}
|
|
}
|
|
|
|
const deleteRepCom = async (id: number) => {
|
|
if (!isAuthenticated()) {
|
|
ElMessage.warning('请先登录后再删除评论')
|
|
return
|
|
}
|
|
try {
|
|
await ElMessageBox.confirm('确定删除该评论?删除后相关评论将同时删除', '确认删除', {
|
|
confirmButtonText: '确定',
|
|
cancelButtonText: '取消',
|
|
type: 'warning'
|
|
})
|
|
} catch {
|
|
return
|
|
}
|
|
try {
|
|
await fetch(`/api/comments/${id}`, { method: 'DELETE', credentials: 'include' })
|
|
ElMessage.success('删除成功')
|
|
await load()
|
|
} catch (e: any) {
|
|
ElMessage.error(e?.message || '删除失败')
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="comment-detail" v-loading="loading">
|
|
<el-card class="root-card">
|
|
<div class="comment-header">
|
|
<div class="user-basinfo">
|
|
<el-avatar :size="32">{{ root?.username?.[0] || '?' }}</el-avatar>
|
|
<div class="comment-user">{{ root?.username }}</div>
|
|
</div>
|
|
<el-button v-if="root.user_id === user_id" type="danger" class="delete" @click="deleteRootCom(root.id)"><el-icon>
|
|
<Delete />
|
|
</el-icon></el-button>
|
|
</div>
|
|
<div class="comment-content">{{ root?.content }}</div>
|
|
<div class="reply-actions">
|
|
<el-button size="small" @click="doReply(root)">回复</el-button>
|
|
</div>
|
|
</el-card>
|
|
|
|
<h3 class="reply-title" style="color:white;">全部回复</h3>
|
|
<div class="replies">
|
|
<el-card v-for="r in replies" :key="r.id" class="reply-card" shadow="never">
|
|
<div class="reply-meta">{{ r.username }} 回复 {{ r.reply_to_username }} : {{
|
|
getReplyContent(r.response_id).slice(0, 5) + (getReplyContent(r.response_id).length > 5 ? '...' : '') }}</div>
|
|
<div class="comment-header">
|
|
<div class="user-basinfo">
|
|
<el-avatar :size="28">{{ r.username?.[0] || '?' }}</el-avatar>
|
|
<div class="comment-user">{{ r.username }}</div>
|
|
</div>
|
|
<el-button v-if="r.user_id === user_id" type="danger" class="delete" @click="deleteRepCom(r.id)"><el-icon>
|
|
<Delete />
|
|
</el-icon></el-button>
|
|
</div>
|
|
<div class="comment-content">{{ r.content }}</div>
|
|
<div class="reply-actions">
|
|
<el-button size="small" @click="doReply(r)">回复</el-button>
|
|
</div>
|
|
</el-card>
|
|
</div>
|
|
|
|
<div class="reply-compose">
|
|
<el-input v-model="replyText" type="textarea" :rows="3"
|
|
:placeholder="replyTo ? '回复 ' + (replyTo.username || '') : '选择要回复的评论后输入...'" />
|
|
<div class="compose-actions">
|
|
<el-button type="primary" :loading="replyPosting" @click="submitReply">发表回复</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.comment-detail {
|
|
padding: 20px;
|
|
}
|
|
|
|
.root-card {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.comment-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
}
|
|
|
|
.user-basinfo {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
|
|
|
|
.comment-user {
|
|
font-weight: 600;
|
|
}
|
|
|
|
.comment-time {
|
|
margin-left: auto;
|
|
font-size: 12px;
|
|
color: #888;
|
|
}
|
|
|
|
.comment-content {
|
|
margin-top: 8px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.reply-title {
|
|
margin: 16px 0 8px;
|
|
}
|
|
|
|
.replies {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 12px;
|
|
}
|
|
|
|
.reply-card {
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.reply-meta {
|
|
font-size: 12px;
|
|
color: #666;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.reply-actions {
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.reply-compose {
|
|
margin-top: 16px;
|
|
display: grid;
|
|
gap: 10px;
|
|
}
|
|
|
|
.compose-actions {
|
|
text-align: right;
|
|
}
|
|
</style>
|