Browse Source

feat(component): add CommentSection with submit form

main
Mohan 1 month ago
parent
commit
459e0941bb
  1. 8
      src/api/client.ts
  2. 217
      src/components/CommentSection.vue
  3. 2
      src/views/ArticleDetailView.vue

8
src/api/client.ts

@ -8,7 +8,9 @@ import {
MediaApi,
SearchApi,
PublicApi,
UserApi
UserApi,
CommentApi,
CommentAdminApi
} from '@/generated/api'
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'
@ -68,4 +70,6 @@ export const tagAdminApi = new TagAdminApi(config, '', axiosInstance)
export const mediaApi = new MediaApi(config, '', axiosInstance)
export const searchApi = new SearchApi(config, '', axiosInstance)
export const publicApi = new PublicApi(config, '', axiosInstance)
export const userApi = new UserApi(config, '', axiosInstance)
export const userApi = new UserApi(config, '', axiosInstance)
export const commentApi = new CommentApi(config, '', axiosInstance)
export const commentAdminApi = new CommentAdminApi(config, '', axiosInstance)

217
src/components/CommentSection.vue

@ -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>

2
src/views/ArticleDetailView.vue

@ -3,6 +3,7 @@ import { ref, computed, watch } from 'vue'
import { useRoute } from 'vue-router'
import { articleApi } from '@/api/client'
import { renderMarkdown } from '@/utils/markdown'
import CommentSection from '@/components/CommentSection.vue'
import type { ArticleDetail } from '@/generated/api'
const route = useRoute()
@ -54,6 +55,7 @@ watch(slug, load, { immediate: true })
<span v-for="tag in article.tagNames" :key="tag" class="tag">{{ tag }}</span>
</div>
<div class="article-content" v-html="renderedContent"></div>
<CommentSection :slug="slug" />
</article>
<p v-else-if="loading" class="hint-text">加载中...</p>

Loading…
Cancel
Save