3 changed files with 225 additions and 2 deletions
@ -0,0 +1,217 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { ref, onMounted } from 'vue' |
||||
|
import { commentApi } from '@/api/client' |
||||
|
import type { CommentResponse } from '@/generated/api' |
||||
|
|
||||
|
const props = defineProps<{ |
||||
|
slug: string |
||||
|
}>() |
||||
|
|
||||
|
const comments = ref<CommentResponse[]>([]) |
||||
|
const loading = ref(false) |
||||
|
const submitting = ref(false) |
||||
|
const error = ref<string | null>(null) |
||||
|
const submitMessage = ref<string | null>(null) |
||||
|
|
||||
|
const authorName = ref('') |
||||
|
const authorEmail = ref('') |
||||
|
const content = ref('') |
||||
|
|
||||
|
function formatDate(iso?: string): string { |
||||
|
if (!iso) return '' |
||||
|
const d = new Date(iso) |
||||
|
if (isNaN(d.getTime())) return iso |
||||
|
return d.toLocaleString('zh-CN', { hour12: false }) |
||||
|
} |
||||
|
|
||||
|
async function load() { |
||||
|
if (!props.slug) return |
||||
|
loading.value = true |
||||
|
error.value = null |
||||
|
try { |
||||
|
const resp = await commentApi.apiArticlesSlugCommentsGet(props.slug) |
||||
|
comments.value = resp.data.data ?? [] |
||||
|
} catch (e) { |
||||
|
console.error(e) |
||||
|
error.value = '评论加载失败' |
||||
|
} finally { |
||||
|
loading.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function onSubmit() { |
||||
|
if (!authorName.value.trim() || !content.value.trim()) { |
||||
|
error.value = '昵称和内容为必填项' |
||||
|
return |
||||
|
} |
||||
|
submitting.value = true |
||||
|
error.value = null |
||||
|
submitMessage.value = null |
||||
|
try { |
||||
|
await commentApi.apiArticlesSlugCommentsPost(props.slug, { |
||||
|
authorName: authorName.value.trim(), |
||||
|
authorEmail: authorEmail.value.trim() || undefined, |
||||
|
content: content.value.trim() |
||||
|
}) |
||||
|
content.value = '' |
||||
|
submitMessage.value = '评论已提交,等待审核' |
||||
|
load() |
||||
|
} catch (e) { |
||||
|
console.error(e) |
||||
|
error.value = '评论提交失败' |
||||
|
} finally { |
||||
|
submitting.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onMounted(load) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<section class="comment-section"> |
||||
|
<h3 class="comment-title">评论</h3> |
||||
|
<p v-if="loading" class="hint-text">加载中...</p> |
||||
|
<p v-else-if="comments.length === 0" class="hint-text">暂无评论</p> |
||||
|
<ul v-else class="comment-list"> |
||||
|
<li v-for="comment in comments" :key="comment.id" class="comment-item"> |
||||
|
<div class="comment-meta"> |
||||
|
<span class="comment-author">{{ comment.authorName }}</span> |
||||
|
<span class="comment-time">{{ formatDate(comment.createdAt) }}</span> |
||||
|
</div> |
||||
|
<p class="comment-content">{{ comment.content }}</p> |
||||
|
</li> |
||||
|
</ul> |
||||
|
|
||||
|
<form class="comment-form" @submit.prevent="onSubmit"> |
||||
|
<h4 class="comment-form-title">发表评论</h4> |
||||
|
<p v-if="error" class="error-text">{{ error }}</p> |
||||
|
<p v-if="submitMessage" class="success-text">{{ submitMessage }}</p> |
||||
|
<label class="form-label" for="comment-author">昵称</label> |
||||
|
<input id="comment-author" v-model="authorName" class="form-input" type="text" maxlength="100" /> |
||||
|
<label class="form-label" for="comment-email">邮箱(可选)</label> |
||||
|
<input id="comment-email" v-model="authorEmail" class="form-input" type="email" /> |
||||
|
<label class="form-label" for="comment-content">内容</label> |
||||
|
<textarea |
||||
|
id="comment-content" |
||||
|
v-model="content" |
||||
|
class="form-textarea" |
||||
|
rows="3" |
||||
|
maxlength="2000" |
||||
|
></textarea> |
||||
|
<button class="submit-btn" type="submit" :disabled="submitting"> |
||||
|
{{ submitting ? '提交中...' : '提交评论' }} |
||||
|
</button> |
||||
|
</form> |
||||
|
</section> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
.comment-section { |
||||
|
margin-top: 32px; |
||||
|
} |
||||
|
|
||||
|
.comment-title { |
||||
|
font-size: 18px; |
||||
|
margin: 0 0 12px; |
||||
|
} |
||||
|
|
||||
|
.comment-list { |
||||
|
list-style: none; |
||||
|
padding: 0; |
||||
|
margin: 0 0 20px; |
||||
|
} |
||||
|
|
||||
|
.comment-item { |
||||
|
padding: 12px 0; |
||||
|
border-bottom: 1px solid #eee; |
||||
|
} |
||||
|
|
||||
|
.comment-item:last-child { |
||||
|
border-bottom: none; |
||||
|
} |
||||
|
|
||||
|
.comment-meta { |
||||
|
display: flex; |
||||
|
gap: 12px; |
||||
|
align-items: baseline; |
||||
|
margin-bottom: 4px; |
||||
|
} |
||||
|
|
||||
|
.comment-author { |
||||
|
font-weight: 600; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.comment-time { |
||||
|
color: #8a93a3; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
.comment-content { |
||||
|
margin: 0; |
||||
|
font-size: 14px; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.comment-form { |
||||
|
background: #fff; |
||||
|
border: 1px solid #e5e5ea; |
||||
|
border-radius: 8px; |
||||
|
padding: 16px; |
||||
|
} |
||||
|
|
||||
|
.comment-form-title { |
||||
|
margin: 0 0 12px; |
||||
|
font-size: 15px; |
||||
|
} |
||||
|
|
||||
|
.form-label { |
||||
|
display: block; |
||||
|
margin: 10px 0 4px; |
||||
|
font-size: 13px; |
||||
|
color: #5c6470; |
||||
|
} |
||||
|
|
||||
|
.form-input, |
||||
|
.form-textarea { |
||||
|
width: 100%; |
||||
|
padding: 8px 10px; |
||||
|
border: 1px solid #d3d8e0; |
||||
|
border-radius: 6px; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.form-textarea { |
||||
|
resize: vertical; |
||||
|
font-family: inherit; |
||||
|
} |
||||
|
|
||||
|
.submit-btn { |
||||
|
margin-top: 12px; |
||||
|
padding: 8px 18px; |
||||
|
border: none; |
||||
|
border-radius: 6px; |
||||
|
background: #3b82f6; |
||||
|
color: #fff; |
||||
|
font-size: 14px; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
.submit-btn:disabled { |
||||
|
opacity: 0.6; |
||||
|
} |
||||
|
|
||||
|
.error-text { |
||||
|
color: #d33c3c; |
||||
|
font-size: 13px; |
||||
|
} |
||||
|
|
||||
|
.success-text { |
||||
|
color: #1e7e34; |
||||
|
font-size: 13px; |
||||
|
} |
||||
|
|
||||
|
.hint-text { |
||||
|
color: #8a93a3; |
||||
|
} |
||||
|
</style> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue