Browse Source

feat(component): add comment quote with ref tag

main
Mohan 1 month ago
parent
commit
2c9d8ba15a
  1. 127
      src/components/CommentSection.vue

127
src/components/CommentSection.vue

@ -31,6 +31,31 @@ const readonlyFields = computed(() => isLoggedIn.value)
const authorName = ref('') const authorName = ref('')
const authorEmail = ref('') const authorEmail = ref('')
const content = ref('') const content = ref('')
const quotedComment = ref<CommentResponse | null>(null)
function escapeBrackets(text: string): string {
return text.replace(/[<>]/g, (ch) => '\\' + ch)
}
function renderCommentContent(text: string): string {
const escaped = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
return escaped.replace(/&lt;ref:(\d+)&gt;/g, '<span class="comment-ref-tag">&lt;ref:$1&gt;</span>')
}
function quoteExcerpt(comment: CommentResponse): string {
const s = (comment.content ?? '').replace(/\s+/g, ' ').trim()
return s.length > 40 ? s.slice(0, 40) + '…' : s
}
function quoteComment(comment: CommentResponse) {
quotedComment.value = comment
if (!formOpen.value) {
openForm()
}
}
const sortedComments = computed(() => const sortedComments = computed(() =>
[...comments.value].sort( [...comments.value].sort(
@ -69,14 +94,19 @@ async function onSubmit() {
error.value = null error.value = null
submitMessage.value = null submitMessage.value = null
try { try {
let body = escapeBrackets(content.value.trim())
if (quotedComment.value?.id != null) {
body = `<ref:${quotedComment.value.id}>${body}`
}
await commentApi.apiArticlesSlugCommentsPost(props.slug, { await commentApi.apiArticlesSlugCommentsPost(props.slug, {
authorName: authorName.value.trim(), authorName: authorName.value.trim(),
authorEmail: authorEmail.value.trim() || undefined, authorEmail: authorEmail.value.trim() || undefined,
content: content.value.trim()
content: body
}) })
content.value = '' content.value = ''
authorName.value = '' authorName.value = ''
authorEmail.value = '' authorEmail.value = ''
quotedComment.value = null
submitMessage.value = '评论已提交,等待审核' submitMessage.value = '评论已提交,等待审核'
formOpen.value = false formOpen.value = false
load() load()
@ -161,7 +191,13 @@ onMounted(() => {
<span class="comment-author">{{ comment.authorName }}</span> <span class="comment-author">{{ comment.authorName }}</span>
<span class="comment-time">{{ formatDate(comment.createdAt) }}</span> <span class="comment-time">{{ formatDate(comment.createdAt) }}</span>
</div> </div>
<p class="comment-content">{{ comment.content }}</p>
<p class="comment-content" v-html="renderCommentContent(comment.content ?? '')"></p>
<button class="comment-quote" type="button" title="引用评论" @click="quoteComment(comment)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true">
<path d="M10 7L8 12h3v5H5v-5l2-5h3zm9 0l-2 5h3v5h-6v-5l2-5h3z" />
</svg>
<span>引用</span>
</button>
<button class="comment-star" :class="{ starred: comment.hasStarred }" @click="toggleCommentStar(comment)"> <button class="comment-star" :class="{ starred: comment.hasStarred }" @click="toggleCommentStar(comment)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" fill="currentColor"/> <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" fill="currentColor"/>
@ -193,6 +229,12 @@ onMounted(() => {
<p v-if="error" class="error-text">{{ error }}</p> <p v-if="error" class="error-text">{{ error }}</p>
<SongInput v-model="authorName" label="昵称" required placeholder="你的署名" :maxlength="100" :readonly="readonlyFields" :disabled="readonlyFields" /> <SongInput v-model="authorName" label="昵称" required placeholder="你的署名" :maxlength="100" :readonly="readonlyFields" :disabled="readonlyFields" />
<SongInput v-model="authorEmail" label="邮箱(可选)" type="email" placeholder="用于联系,不公开" :readonly="readonlyFields" :disabled="readonlyFields" /> <SongInput v-model="authorEmail" label="邮箱(可选)" type="email" placeholder="用于联系,不公开" :readonly="readonlyFields" :disabled="readonlyFields" />
<div v-if="quotedComment" class="quote-preview">
<span class="quote-preview-label">引用</span>
<span class="quote-preview-author">{{ quotedComment.authorName }}</span>
<span class="quote-preview-text">{{ quoteExcerpt(quotedComment) }}</span>
<button class="quote-preview-clear" type="button" title="取消引用" aria-label="取消引用" @click="quotedComment = null"></button>
</div>
<SongInput v-model="content" label="内容" textarea :rows="4" required placeholder="写下你的评语..." :maxlength="2000" /> <SongInput v-model="content" label="内容" textarea :rows="4" required placeholder="写下你的评语..." :maxlength="2000" />
<div class="comment-form-actions"> <div class="comment-form-actions">
<SongButton type="text" size="small" @click="formOpen = false"></SongButton> <SongButton type="text" size="small" @click="formOpen = false"></SongButton>
@ -330,6 +372,87 @@ onMounted(() => {
color: var(--ac-danger-ink); color: var(--ac-danger-ink);
} }
.comment-quote {
display: inline-flex;
align-items: center;
gap: 3px;
margin-left: 8px;
border: none;
background: none;
color: var(--ac-text-disabled);
cursor: pointer;
font-size: 12px;
padding: 2px 6px;
border-radius: var(--song-radius-s);
transition: color var(--song-dur-1) var(--song-ease);
}
.comment-quote:hover {
color: var(--ac-primary-ink);
}
.comment-ref-tag {
display: inline-block;
padding: 0 6px;
margin-right: 4px;
border: 1px solid var(--ac-primary);
border-radius: var(--song-radius-s);
color: var(--ac-primary-ink);
background: var(--ac-primary-bg);
font-size: 12px;
line-height: 18px;
}
.quote-preview {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
margin: 4px 0;
border: 1px solid var(--ac-border);
border-left: 3px solid var(--ac-primary);
border-radius: var(--song-radius-m);
background: var(--ac-primary-bg);
font-size: 13px;
}
.quote-preview-label {
flex-shrink: 0;
color: var(--ac-primary-ink);
font-weight: 600;
}
.quote-preview-author {
flex-shrink: 0;
color: var(--ac-text-secondary);
}
.quote-preview-text {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--ac-text);
}
.quote-preview-clear {
flex-shrink: 0;
border: none;
background: none;
color: var(--ac-text-disabled);
cursor: pointer;
font-size: 12px;
line-height: 1;
padding: 2px;
border-radius: var(--song-radius-s);
transition: color var(--song-dur-1) var(--song-ease);
}
.quote-preview-clear:hover {
color: var(--ac-danger-ink);
}
.comment-write { .comment-write {
margin-top: 8px; margin-top: 8px;
display: flex; display: flex;

Loading…
Cancel
Save