226 lines
8.9 KiB
Vue
226 lines
8.9 KiB
Vue
<template>
|
||
<div class="gvs-community">
|
||
<div class="gvs-container gvs-page-top">
|
||
<button
|
||
class="gvs-btn gvs-btn-ghost mb-4!"
|
||
@click="router.back()"
|
||
>
|
||
<i class="fa fa-arrow-left"></i> 返回
|
||
</button>
|
||
|
||
<div v-if="loading" class="gvs-card p-16 text-center text-gray-400">
|
||
<i class="fa fa-spinner fa-spin text-2xl"></i>
|
||
</div>
|
||
|
||
<template v-else-if="post">
|
||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 items-start">
|
||
<!-- 帖子主体 -->
|
||
<div class="lg:col-span-2 space-y-4">
|
||
<div class="gvs-card">
|
||
<div class="p-6">
|
||
<div class="flex items-center gap-2 mb-3 flex-wrap">
|
||
<span class="gvs-tag">{{ post.BoardName }}</span>
|
||
<span v-if="post.CategoryName" class="gvs-tag gray">{{ post.CategoryName }}</span>
|
||
<span v-if="post.IsPinned" class="gvs-tag red">置顶</span>
|
||
<span v-if="post.IsLocked" class="gvs-tag gray">已锁定</span>
|
||
</div>
|
||
<h1 class="text-xl font-bold text-gray-900 leading-snug">{{ post.Title }}</h1>
|
||
<div class="flex items-center gap-4 mt-4 pb-4 border-b border-gray-100">
|
||
<div class="gvs-feed-avatar w-9! h-9!">
|
||
<i class="fa fa-user"></i>
|
||
<img v-if="post.AuthorAvatar" :src="post.AuthorAvatar" class="gvs-avatar-img" alt="" @error="hideAvatar" />
|
||
</div>
|
||
<div class="flex-1 min-w-0">
|
||
<div class="text-sm font-medium text-gray-900">{{ post.AuthorName || '匿名' }}</div>
|
||
<div class="text-xs text-gray-400 mt-0.5">{{ post.CreateTime }}</div>
|
||
</div>
|
||
<span class="text-xs text-gray-400 flex items-center gap-1"><i class="fa fa-eye"></i> {{ post.ViewCount }} 浏览</span>
|
||
</div>
|
||
<div class="mt-5 text-gray-800 leading-7 whitespace-pre-wrap min-h-24 text-[14px]">
|
||
{{ post.Content }}
|
||
</div>
|
||
<div class="mt-6 pt-4 border-t border-gray-100 flex items-center justify-between">
|
||
<button
|
||
class="gvs-btn"
|
||
:class="liked ? 'gvs-btn-primary' : 'gvs-btn-ghost'"
|
||
@click="toggleLike"
|
||
>
|
||
<i class="fa" :class="liked ? 'fa-solid fa-thumbs-up' : 'fa-regular fa-thumbs-up'"></i>
|
||
{{ liked ? '已赞' : '点赞' }} ({{ post.LikeCount }})
|
||
</button>
|
||
<span class="text-xs text-gray-400">{{ post.ReplyCount }} 条回复</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 回复列表 -->
|
||
<div class="gvs-card">
|
||
<div class="gvs-card-hd">
|
||
<i class="fa fa-comments"></i> 全部回复 ({{ post.Replies.length }})
|
||
</div>
|
||
<div v-if="post.Replies.length" class="divide-y divide-gray-100">
|
||
<div v-for="(r, i) in post.Replies" :key="r.ReplyUUID" class="px-6 py-4 flex gap-3">
|
||
<div class="gvs-feed-avatar w-8! h-8! text-sm!">
|
||
<i class="fa fa-user"></i>
|
||
<img v-if="r.AuthorAvatar" :src="r.AuthorAvatar" class="gvs-avatar-img" alt="" @error="hideAvatar" />
|
||
</div>
|
||
<div class="flex-1 min-w-0">
|
||
<div class="flex items-center gap-2">
|
||
<span class="font-medium text-sm text-gray-900">{{ r.AuthorName || '匿名' }}</span>
|
||
<span class="text-xs text-gray-400">#{{ i + 1 }}</span>
|
||
<span class="text-xs text-gray-400 ml-auto">{{ r.CreateTime }}</span>
|
||
</div>
|
||
<p class="mt-1.5 text-gray-700 text-sm leading-relaxed whitespace-pre-wrap">{{ r.Content }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else class="px-6 py-12 text-center text-gray-400 text-sm">暂无回复,抢沙发!</div>
|
||
</div>
|
||
|
||
<!-- 回复框 -->
|
||
<div class="gvs-card p-6">
|
||
<div v-if="!isLoggedIn" class="text-center text-sm text-gray-500 py-4">
|
||
请先
|
||
<a href="https://gerp.gvsds.com/#/login" target="_blank" class="text-blue-600 hover:underline">登录</a>
|
||
后参与讨论
|
||
</div>
|
||
<template v-else>
|
||
<div v-if="post.IsLocked" class="text-center text-sm text-gray-400 py-4">该帖子已锁定,不可回复</div>
|
||
<template v-else>
|
||
<textarea
|
||
v-model="replyContent"
|
||
class="gvs-textarea"
|
||
rows="3"
|
||
placeholder="友善发言,理性讨论..."
|
||
></textarea>
|
||
<div class="flex justify-end mt-3">
|
||
<button
|
||
class="gvs-btn gvs-btn-primary"
|
||
:disabled="submitting || !replyContent.trim()"
|
||
@click="submitReply"
|
||
>
|
||
{{ submitting ? '提交中...' : '发表回复' }}
|
||
</button>
|
||
</div>
|
||
</template>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 侧栏 -->
|
||
<div class="space-y-4">
|
||
<div class="gvs-card">
|
||
<div class="gvs-card-hd"><i class="fa fa-info-circle"></i> 帖子信息</div>
|
||
<div class="gvs-side-list">
|
||
<div class="gvs-side-item">
|
||
<span class="gvs-side-num"><i class="fa fa-th-large"></i></span>
|
||
板块:{{ post.BoardName }}
|
||
</div>
|
||
<div class="gvs-side-item">
|
||
<span class="gvs-side-num"><i class="fa fa-eye"></i></span>
|
||
{{ post.ViewCount }} 次浏览
|
||
</div>
|
||
<div class="gvs-side-item">
|
||
<span class="gvs-side-num"><i class="fa fa-thumbs-up"></i></span>
|
||
{{ post.LikeCount }} 次点赞
|
||
</div>
|
||
<div class="gvs-side-item">
|
||
<span class="gvs-side-num"><i class="fa fa-reply"></i></span>
|
||
{{ post.ReplyCount }} 条回复
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="gvs-card">
|
||
<div class="gvs-card-hd"><i class="fa fa-gavel"></i> 社区规范</div>
|
||
<div class="gvs-side-list text-xs text-gray-500 leading-6">
|
||
<p>· 请友善发言,理性讨论</p>
|
||
<p>· 禁止发布违法违规内容</p>
|
||
<p>· 提问前建议先搜索是否已有答案</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<div v-else class="gvs-card p-16 text-center text-gray-400">
|
||
帖子不存在或已删除
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { onMounted, ref } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { communityApi, type CommunityPostDetail } from '@/api/community'
|
||
import '@/assets/community.css'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
|
||
const post = ref<CommunityPostDetail | null>(null)
|
||
const loading = ref(false)
|
||
const submitting = ref(false)
|
||
const replyContent = ref('')
|
||
const liked = ref(false)
|
||
const isLoggedIn = ref(false)
|
||
|
||
async function loadPost(): Promise<void> {
|
||
const uuid = route.params.uuid as string
|
||
if (!uuid) return
|
||
loading.value = true
|
||
try {
|
||
post.value = await communityApi.post(uuid)
|
||
liked.value = post.value.Liked === 1
|
||
} catch {
|
||
post.value = null
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function toggleLike(): Promise<void> {
|
||
if (!post.value) return
|
||
try {
|
||
const r = await communityApi.like(post.value.PostUUID)
|
||
liked.value = r.Liked === 1
|
||
post.value.LikeCount = Math.max(0, post.value.LikeCount + (r.Liked === 1 ? 1 : -1))
|
||
} catch (e) {
|
||
alert((e as Error).message || '操作失败')
|
||
}
|
||
}
|
||
|
||
async function submitReply(): Promise<void> {
|
||
if (!post.value || !replyContent.value.trim()) return
|
||
submitting.value = true
|
||
try {
|
||
await communityApi.reply({ PostUUID: post.value.PostUUID, Content: replyContent.value.trim() })
|
||
replyContent.value = ''
|
||
await loadPost()
|
||
} catch (e) {
|
||
alert((e as Error).message || '回复失败')
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
async function detectLogin(): Promise<void> {
|
||
try {
|
||
const res = await fetch('https://ugm.gvsds.com/Query/GetELTUserAvatar', { credentials: 'include' })
|
||
isLoggedIn.value = res.ok
|
||
} catch {
|
||
isLoggedIn.value = false
|
||
}
|
||
}
|
||
|
||
/** 头像加载失败时隐藏(露出底层 fa-user 图标) */
|
||
function hideAvatar(e: Event): void {
|
||
;(e.target as HTMLImageElement).style.display = 'none'
|
||
}
|
||
|
||
onMounted(() => {
|
||
detectLogin()
|
||
loadPost()
|
||
})
|
||
</script>
|