From 2c9d8ba15a261fb1228fac97d143fba4e63e2715 Mon Sep 17 00:00:00 2001 From: Mohan Date: Fri, 14 Aug 2026 17:51:24 +0800 Subject: [PATCH] feat(component): add comment quote with ref tag --- src/components/CommentSection.vue | 127 +++++++++++++++++++++++++++++- 1 file changed, 125 insertions(+), 2 deletions(-) diff --git a/src/components/CommentSection.vue b/src/components/CommentSection.vue index ec7abed..0929ba4 100644 --- a/src/components/CommentSection.vue +++ b/src/components/CommentSection.vue @@ -31,6 +31,31 @@ const readonlyFields = computed(() => isLoggedIn.value) const authorName = ref('') const authorEmail = ref('') const content = ref('') +const quotedComment = ref(null) + +function escapeBrackets(text: string): string { + return text.replace(/[<>]/g, (ch) => '\\' + ch) +} + +function renderCommentContent(text: string): string { + const escaped = text + .replace(/&/g, '&') + .replace(//g, '>') + return escaped.replace(/<ref:(\d+)>/g, '<ref:$1>') +} + +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(() => [...comments.value].sort( @@ -69,14 +94,19 @@ async function onSubmit() { error.value = null submitMessage.value = null try { + let body = escapeBrackets(content.value.trim()) + if (quotedComment.value?.id != null) { + body = `${body}` + } await commentApi.apiArticlesSlugCommentsPost(props.slug, { authorName: authorName.value.trim(), authorEmail: authorEmail.value.trim() || undefined, - content: content.value.trim() + content: body }) content.value = '' authorName.value = '' authorEmail.value = '' + quotedComment.value = null submitMessage.value = '评论已提交,等待审核' formOpen.value = false load() @@ -161,7 +191,13 @@ onMounted(() => { {{ comment.authorName }} {{ formatDate(comment.createdAt) }} -

{{ comment.content }}

+

+ +
@@ -330,6 +372,87 @@ onMounted(() => { 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 { margin-top: 8px; display: flex;