formula project

This commit is contained in:
colden
2025-12-20 12:20:43 +08:00
commit 28e1507889
156 changed files with 7444 additions and 0 deletions

View File

@@ -0,0 +1,249 @@
<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>