Browse Source

feat(component): rebuild comment section with timeline and collapsible form

main
Mohan 1 month ago
parent
commit
ae01874385
  1. 201
      src/components/CommentSection.vue

201
src/components/CommentSection.vue

@ -1,7 +1,9 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { commentApi } from '@/api/client'
import type { CommentResponse } from '@/generated/api'
import SongButton from '@/components/SongButton.vue'
import SongInput from '@/components/SongInput.vue'
const props = defineProps<{
slug: string
@ -12,11 +14,18 @@ const loading = ref(false)
const submitting = ref(false)
const error = ref<string | null>(null)
const submitMessage = ref<string | null>(null)
const formOpen = ref(false)
const authorName = ref('')
const authorEmail = ref('')
const content = ref('')
const sortedComments = computed(() =>
[...comments.value].sort(
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
)
)
function formatDate(iso?: string): string {
if (!iso) return ''
const d = new Date(iso)
@ -54,7 +63,10 @@ async function onSubmit() {
content: content.value.trim()
})
content.value = ''
authorName.value = ''
authorEmail.value = ''
submitMessage.value = '评论已提交,等待审核'
formOpen.value = false
load()
} catch (e) {
console.error(e)
@ -64,6 +76,11 @@ async function onSubmit() {
}
}
function openForm() {
error.value = null
formOpen.value = true
}
onMounted(load)
</script>
@ -71,63 +88,90 @@ onMounted(load)
<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>
<p v-else-if="sortedComments.length === 0" class="hint-text">暂无评论</p>
<div v-else class="comment-timeline">
<article v-for="comment in sortedComments" :key="comment.id" class="comment-item">
<span class="comment-node" aria-hidden="true" />
<div class="comment-card">
<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>
</div>
<p class="comment-content">{{ comment.content }}</p>
</li>
</ul>
</article>
</div>
<form class="comment-form" @submit.prevent="onSubmit">
<h4 class="comment-form-title">发表评论</h4>
<p v-if="error" class="error-text">{{ error }}</p>
<div class="comment-write">
<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>
<SongButton v-if="!formOpen" type="secondary" size="medium" @click="openForm"> 发表评论</SongButton>
<transition name="form-fade">
<form v-if="formOpen" class="comment-form" @submit.prevent="onSubmit">
<p v-if="error" class="error-text">{{ error }}</p>
<SongInput v-model="authorName" label="昵称" required placeholder="你的署名" :maxlength="100" />
<SongInput v-model="authorEmail" label="邮箱(可选)" type="email" placeholder="用于联系,不公开" />
<SongInput v-model="content" label="内容" textarea :rows="4" required placeholder="写下你的评语..." :maxlength="2000" />
<div class="comment-form-actions">
<SongButton type="text" size="small" @click="formOpen = false"></SongButton>
<SongButton type="primary" size="small" :disabled="submitting" :loading="submitting">
{{ submitting ? '提交中' : '提交评论' }}
</SongButton>
</div>
</form>
</transition>
</div>
</section>
</template>
<style scoped>
.comment-section {
margin-top: 32px;
margin-top: 40px;
}
.comment-title {
font-size: 18px;
margin: 0 0 12px;
font-family: var(--song-font-serif);
font-size: 20px;
line-height: 28px;
font-weight: 600;
margin: 0 0 20px;
}
.comment-list {
list-style: none;
padding: 0;
margin: 0 0 20px;
.comment-timeline {
position: relative;
margin: 0 0 24px;
}
.comment-timeline::before {
content: '';
position: absolute;
left: 5px;
top: 18px;
bottom: 10px;
width: 1px;
background: var(--ac-border-strong);
}
.comment-item {
padding: 12px 0;
border-bottom: 1px solid var(--ac-border);
display: flex;
gap: 16px;
}
.comment-node {
width: 11px;
height: 11px;
flex-shrink: 0;
margin-top: 18px;
border-radius: 50%;
border: 1px solid var(--ac-primary);
background: var(--ac-bg);
position: relative;
z-index: 1;
}
.comment-item:last-child {
border-bottom: none;
.comment-card {
flex: 1;
min-width: 0;
padding: 0 0 6px;
}
.comment-meta {
@ -151,73 +195,54 @@ onMounted(load)
margin: 0;
font-size: 14px;
color: var(--ac-text);
line-height: 22px;
word-break: break-word;
}
.comment-form {
background: var(--ac-bg-card);
border: 1px solid var(--ac-border);
border-radius: var(--song-radius-l);
padding: 16px;
}
.comment-form-title {
margin: 0 0 12px;
font-size: 15px;
}
.form-label {
display: block;
margin: 10px 0 4px;
font-size: 13px;
color: var(--ac-text-secondary);
.comment-write {
margin-top: 8px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.form-input,
.form-textarea {
.comment-form {
width: 100%;
padding: 8px 10px;
border: 1px solid var(--ac-input-border);
border-radius: var(--song-radius-l);
font-size: 14px;
background: var(--ac-bg-card);
color: var(--ac-text);
transition: border-color var(--song-dur-1) var(--song-ease);
}
.form-input:focus,
.form-textarea:focus {
border-color: var(--ac-primary);
outline: none;
border: 1px solid var(--ac-border);
border-radius: var(--song-radius-l);
padding: 20px;
display: flex;
flex-direction: column;
gap: 4px;
}
.form-textarea {
resize: vertical;
font-family: inherit;
.comment-form-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 8px;
}
.submit-btn {
margin-top: 12px;
padding: 8px 18px;
border: none;
border-radius: var(--song-radius-l);
background: var(--ac-primary-strong);
color: var(--ac-primary-text);
font-size: 14px;
cursor: pointer;
transition: opacity var(--song-dur-1) var(--song-ease);
.form-fade-enter-active,
.form-fade-leave-active {
transition: opacity var(--song-dur-2) var(--song-ease), transform var(--song-dur-2) var(--song-ease);
}
.submit-btn:disabled {
opacity: 0.6;
.form-fade-enter-from,
.form-fade-leave-to {
opacity: 0;
transform: translateY(6px);
}
.error-text {
color: var(--ac-danger);
color: var(--ac-danger-ink);
font-size: 13px;
}
.success-text {
color: var(--ac-success);
color: var(--ac-success-ink);
font-size: 13px;
}

Loading…
Cancel
Save