From 89379597969cf70675430d9d4caeded5c8ef0cd2 Mon Sep 17 00:00:00 2001 From: bbb-lsy07 Date: Sun, 21 Jun 2026 10:24:25 +0800 Subject: [PATCH] =?UTF-8?q?style:=20=E9=87=8D=E5=86=99=20LogsView.vue=20-?= =?UTF-8?q?=20=E7=BA=AF=20Tailwind=20=E6=A0=87=E7=AD=BE=E6=9B=BF=E4=BB=A3?= =?UTF-8?q?=20Emoji=EF=BC=8C=E7=A7=BB=E9=99=A4=20300+=20=E8=A1=8C=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=20CSS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 状态/情感标签改用纯色 Tailwind 背景标签,去除所有 Emoji - 删除 300+ 行自定义 CSS,全部替换为 Tailwind 原子类 - 对话弹窗 Markdown 引用块正则提取,去除气泡和机器人 Emoji - 优化移动端响应式布局,解决排版错位问题 --- ui/src/views/LogsView.vue | 922 ++++++-------------------------------- 1 file changed, 130 insertions(+), 792 deletions(-) diff --git a/ui/src/views/LogsView.vue b/ui/src/views/LogsView.vue index 401366f..89d7a3d 100644 --- a/ui/src/views/LogsView.vue +++ b/ui/src/views/LogsView.vue @@ -9,51 +9,25 @@ - -
+ +
已选择 {{ selectedNames.size }} 项 - - - - + + + +
- -
- - @@ -61,176 +35,78 @@ -
- +
+ - +
-
+
- - - - 暂无AI回复记录 + + 暂无记录
-
-
- - 全选 +
+
+ + 全选本页
-
- -
-
- -
- -
-
- - {{ getStatusLabel(reply.spec.status) }} - - - {{ reply.spec.published ? '已发布' : '未发布' }} - - - 对话 - - - {{ getSentimentLabel(reply.spec.sentiment) }} - -
- {{ formatDate(reply.metadata.creationTimestamp) }} -
- - -
- {{ stripHtml(reply.spec.reply) || '(空)' }} + +
+
+ +
+
+
+ {{ getStatusLabel(reply.spec.status) }} + {{ reply.spec.published ? '已发布' : '未发布' }} + 对话 + {{ getSentimentLabel(reply.spec.sentiment) }}
+ {{ formatDate(reply.metadata.creationTimestamp) }} +
+
+ {{ stripHtml(reply.spec.reply) || '(空)' }}
- - -
-
- - 评分 {{ reply.spec.score }} - {{ getScoreLabel(reply.spec.score) }} + +
+
+ + 评分 {{ reply.spec.score }} + {{ getScoreLabel(reply.spec.score) }} - - 文章 - {{ reply.spec.postSlug }} - - - - - - 页面 - {{ reply.spec.postSlug }} - - - - - - 重试 {{ reply.spec.retryCount }} 次 + + {{ reply.spec.postKind === 'SinglePage' ? '页面' : '文章' }} + {{ reply.spec.postSlug }} + 重试 {{ reply.spec.retryCount }} 次
- - + +
- -
- 共 {{ total }} 条 +
+ 共 {{ total }} 条
上一页 下一页 @@ -238,87 +114,34 @@
- + -
- -
- - -
- -
-
- - - -

完整对话

-
-
- - -
+
- -
- 暂无对话内容 -
- +
暂无内容
-
-
- -
- {{ msg.owner }} +
+
+
+ {{ msg.owner }}
- -
- -
+
+
{{ formatDate(msg.time) }}
- - -
- -
@@ -332,23 +155,11 @@ import { VPageHeader, VButton, VLoading, Toast } from "@halo-dev/components" import { IconPlug } from "@halo-dev/components" interface AiCommentReplyItem { - metadata: { - name: string - creationTimestamp: string - } + metadata: { name: string; creationTimestamp: string } spec: { - commentId: string - postId: string - postSlug: string - postKind: string - reply: string - score: number - status: string - retryCount: number - replyTo: string - isAiConversation: boolean - published: boolean - sentiment: string | null + commentId: string; postId: string; postSlug: string; postKind: string + reply: string; score: number; status: string; retryCount: number + replyTo: string; isAiConversation: boolean; published: boolean; sentiment: string | null } } @@ -367,40 +178,26 @@ const size = ref(20) const total = ref(0) const totalPages = ref(0) -// Selection state const selectedNames = ref>(new Set()) const selectAll = ref(false) -// Filter state const filterStatus = ref("") const filterSentiment = ref("") const filterKeyword = ref("") -const toggleSelect = (name: string) => { - if (selectedNames.value.has(name)) { - selectedNames.value.delete(name) - } else { - selectedNames.value.add(name) - } - // Update selectAll state - selectAll.value = replies.value.length > 0 && replies.value.every(r => selectedNames.value.has(r.metadata.name)) -} - -const toggleSelectAll = () => { - if (selectAll.value) { - selectedNames.value.clear() - selectAll.value = false - } else { - selectedNames.value = new Set(replies.value.map(r => r.metadata.name)) - selectAll.value = true - } -} - -// Conversation dialog state const showDialog = ref(false) const conversationLoading = ref(false) const conversationMessages = ref([]) +const toggleSelect = (name: string) => { + selectedNames.value.has(name) ? selectedNames.value.delete(name) : selectedNames.value.add(name) + selectAll.value = replies.value.length > 0 && replies.value.every(r => selectedNames.value.has(r.metadata.name)) +} +const toggleSelectAll = () => { + if (selectAll.value) { selectedNames.value.clear(); selectAll.value = false } + else { selectedNames.value = new Set(replies.value.map(r => r.metadata.name)); selectAll.value = true } +} + const fetchReplies = async () => { loading.value = true try { @@ -408,18 +205,9 @@ const fetchReplies = async () => { if (filterStatus.value) params.status = filterStatus.value if (filterSentiment.value) params.sentiment = filterSentiment.value if (filterKeyword.value) params.keyword = filterKeyword.value - const { data } = await axiosInstance.get( - "/apis/console.api.comment-ai-autopilot.nxxy335.top/v1alpha1/replies", - { params }, - ) - replies.value = data.items || [] - total.value = data.total || 0 - totalPages.value = Math.ceil(total.value / size.value) - } catch (e) { - console.error("Failed to fetch replies", e) - } finally { - loading.value = false - } + const { data } = await axiosInstance.get("/apis/console.api.comment-ai-autopilot.nxxy335.top/v1alpha1/replies", { params }) + replies.value = data.items || []; total.value = data.total || 0; totalPages.value = Math.ceil(total.value / size.value) + } catch (e) { Toast.error("获取数据失败") } finally { loading.value = false } } const openConversation = async (reply: AiCommentReplyItem) => { @@ -427,543 +215,93 @@ const openConversation = async (reply: AiCommentReplyItem) => { conversationLoading.value = true conversationMessages.value = [] try { - const { data } = await axiosInstance.get( - `/apis/console.api.comment-ai-autopilot.nxxy335.top/v1alpha1/conversation/${reply.spec.commentId}`, - ) + const { data } = await axiosInstance.get(`/apis/console.api.comment-ai-autopilot.nxxy335.top/v1alpha1/conversation/${reply.spec.commentId}`) conversationMessages.value = data.messages || [] - } catch (e) { - console.error("Failed to fetch conversation", e) - Toast.error("获取对话失败") - } finally { - conversationLoading.value = false - } + } catch (e) { Toast.error("获取对话失败") } finally { conversationLoading.value = false } } -const handleDelete = async (name: string) => { - try { - await axiosInstance.delete( - `/apis/console.api.comment-ai-autopilot.nxxy335.top/v1alpha1/replies/${name}`, - ) - Toast.success("删除成功") - fetchReplies() - } catch (e) { - console.error("Failed to delete reply", e) - Toast.error("删除失败") - } -} +const handleDelete = async (name: string) => { try { await axiosInstance.delete(`/apis/console.api.comment-ai-autopilot.nxxy335.top/v1alpha1/replies/${name}`); Toast.success("删除成功"); fetchReplies() } catch (e) { Toast.error("删除失败") } } +const handleApprove = async (name: string) => { try { await axiosInstance.post(`/apis/console.api.comment-ai-autopilot.nxxy335.top/v1alpha1/replies/${name}/approve`); Toast.success("审核通过"); fetchReplies() } catch (e) { Toast.error("审核失败") } } +const handleReject = async (name: string) => { try { await axiosInstance.post(`/apis/console.api.comment-ai-autopilot.nxxy335.top/v1alpha1/replies/${name}/reject`); Toast.success("已拒绝"); fetchReplies() } catch (e) { Toast.error("拒绝失败") } } +const batchApprove = async () => { if(!selectedNames.value.size) return; try { await axiosInstance.post("/apis/console.api.comment-ai-autopilot.nxxy335.top/v1alpha1/replies/batch-approve", { names: Array.from(selectedNames.value) }); Toast.success("批量通过成功"); selectedNames.value.clear(); selectAll.value=false; fetchReplies() } catch(e) { Toast.error("批量通过失败") } } +const batchReject = async () => { if(!selectedNames.value.size) return; try { await axiosInstance.post("/apis/console.api.comment-ai-autopilot.nxxy335.top/v1alpha1/replies/batch-reject", { names: Array.from(selectedNames.value) }); Toast.success("批量拒绝成功"); selectedNames.value.clear(); selectAll.value=false; fetchReplies() } catch(e) { Toast.error("批量拒绝失败") } } +const batchDelete = async () => { if(!selectedNames.value.size) return; try { await axiosInstance.post("/apis/console.api.comment-ai-autopilot.nxxy335.top/v1alpha1/replies/batch-delete", { names: Array.from(selectedNames.value) }); Toast.success("批量删除成功"); selectedNames.value.clear(); selectAll.value=false; fetchReplies() } catch(e) { Toast.error("批量删除失败") } } -const handleApprove = async (name: string) => { - try { - await axiosInstance.post( - `/apis/console.api.comment-ai-autopilot.nxxy335.top/v1alpha1/replies/${name}/approve`, - ) - Toast.success("审核通过") - fetchReplies() - } catch (e) { - console.error("Failed to approve reply", e) - Toast.error("审核操作失败") - } -} - -const handleReject = async (name: string) => { - try { - await axiosInstance.post( - `/apis/console.api.comment-ai-autopilot.nxxy335.top/v1alpha1/replies/${name}/reject`, - ) - Toast.success("已拒绝") - fetchReplies() - } catch (e) { - console.error("Failed to reject reply", e) - Toast.error("拒绝操作失败") - } -} - -const batchApprove = async () => { - if (selectedNames.value.size === 0) return - try { - await axiosInstance.post( - "/apis/console.api.comment-ai-autopilot.nxxy335.top/v1alpha1/replies/batch-approve", - { names: Array.from(selectedNames.value) } - ) - Toast.success("批量审核通过成功") - selectedNames.value.clear() - selectAll.value = false - fetchReplies() - } catch (e) { - Toast.error("批量审核操作失败") - } -} - -const batchReject = async () => { - if (selectedNames.value.size === 0) return - try { - await axiosInstance.post( - "/apis/console.api.comment-ai-autopilot.nxxy335.top/v1alpha1/replies/batch-reject", - { names: Array.from(selectedNames.value) } - ) - Toast.success("批量拒绝成功") - selectedNames.value.clear() - selectAll.value = false - fetchReplies() - } catch (e) { - Toast.error("批量拒绝操作失败") - } -} - -const batchDelete = async () => { - if (selectedNames.value.size === 0) return - try { - await axiosInstance.post( - "/apis/console.api.comment-ai-autopilot.nxxy335.top/v1alpha1/replies/batch-delete", - { names: Array.from(selectedNames.value) } - ) - Toast.success("批量删除成功") - selectedNames.value.clear() - selectAll.value = false - fetchReplies() - } catch (e) { - Toast.error("批量删除操作失败") - } -} - -const getScoreClass = (score: number) => { - if (score >= 85) return "text-green-600" - if (score >= 60) return "text-yellow-600" - return "text-red-600" -} - -const getScoreLabelClass = (score: number) => { - if (score >= 85) return "score-label--excellent" - if (score >= 70) return "score-label--good" - if (score >= 50) return "score-label--average" - if (score > 0) return "score-label--poor" - return "" -} - -const getScoreLabel = (score: number) => { - if (score >= 85) return "优秀" - if (score >= 70) return "良好" - if (score >= 50) return "一般" - if (score > 0) return "较差" - return "" -} +const getScoreTextClass = (score: number) => score >= 85 ? "text-green-600" : score >= 60 ? "text-yellow-600" : "text-red-600" +const getScoreBgClass = (score: number) => score >= 85 ? "bg-green-100 text-green-700" : score >= 70 ? "bg-blue-100 text-blue-700" : score >= 50 ? "bg-yellow-100 text-yellow-800" : "bg-red-100 text-red-700" +const getScoreLabel = (score: number) => score >= 85 ? "优秀" : score >= 70 ? "良好" : score >= 50 ? "一般" : "较差" const getStatusClass = (status: string) => { switch (status) { - case "PASS": - return "status-tag--pass" - case "FAIL": - return "status-tag--fail" - case "PENDING": - return "status-tag--pending" - case "REJECTED": - return "status-tag--rejected" - default: - return "status-tag--pending" + case "PASS": return "bg-green-100 text-green-700" + case "FAIL": return "bg-red-100 text-red-700" + case "REJECTED": return "bg-orange-100 text-orange-700" + default: return "bg-gray-100 text-gray-600" } } - const getStatusLabel = (status: string) => { switch (status) { - case "PASS": - return "通过" - case "FAIL": - return "失败" - case "PENDING": - return "待处理" - case "REJECTED": - return "已拒绝" - default: - return status + case "PASS": return "通过"; case "FAIL": return "失败"; case "PENDING": return "待审核"; case "REJECTED": return "已拒绝"; default: return status } } const getSentimentClass = (sentiment: string) => { switch (sentiment) { - case "VERY_POSITIVE": - return "status-tag--very-positive" - case "POSITIVE": - return "status-tag--positive" - case "NEGATIVE": - return "status-tag--negative" - case "VERY_NEGATIVE": - return "status-tag--very-negative" - case "NEUTRAL": - return "status-tag--neutral-sentiment" - default: - return "status-tag--neutral-sentiment" + case "VERY_POSITIVE": return "bg-green-100 text-green-800" + case "POSITIVE": return "bg-emerald-50 text-emerald-600" + case "NEGATIVE": return "bg-rose-50 text-rose-600" + case "VERY_NEGATIVE": return "bg-red-100 text-red-800" + default: return "bg-gray-100 text-gray-600" } } - const getSentimentLabel = (sentiment: string) => { switch (sentiment) { - case "VERY_POSITIVE": - return "非常正面" - case "POSITIVE": - return "正面" - case "NEGATIVE": - return "负面" - case "VERY_NEGATIVE": - return "非常负面" - case "NEUTRAL": - return "中性" - default: - return sentiment + case "VERY_POSITIVE": return "非常正面"; case "POSITIVE": return "正面"; case "NEGATIVE": return "负面"; case "VERY_NEGATIVE": return "非常负面"; default: return "中性" } } -const formatDate = (timestamp: string) => { - if (!timestamp) return "" - return new Date(timestamp).toLocaleString("zh-CN") -} +const formatDate = (ts: string) => ts ? new Date(ts).toLocaleString("zh-CN") : "" +const getPostUrl = (slug: string) => `${window.location.origin}/archives/${slug}` +const getPageUrl = (slug: string) => `${window.location.origin}/pages/${slug}` -const getPostUrl = (slug: string) => { - return `${window.location.origin}/archives/${slug}` -} - -const getPageUrl = (slug: string) => { - return `${window.location.origin}/pages/${slug}` -} - -/** - * Strip HTML tags for plain text display (card preview) - */ const stripHtml = (html: string) => { if (!html) return "" - return html - .replace(/]*>/gi, "") - .replace(/<\/p>/gi, "\n") - .replace(//gi, "\n") - .replace(/<[^>]+>/g, "") - .replace(/\n{3,}/g, "\n\n") - .trim() + return html.replace(/<[^>]+>/g, "").replace(/\n+/g, " ").trim() } -/** - * Sanitize and render HTML content for conversation bubbles. - * Only allows safe inline tags, strips dangerous elements. - */ -const renderContent = (content: string) => { - if (!content) return "(空)" - return content - // 将 Markdown 的换行符替换为 HTML 的换行 - .replace(/\n/g, "
") +const renderContent = (content: string, isAi: boolean) => { + if (!content) return "(空)" + let parsed = content .replace(/]*>[\s\S]*?<\/script>/gi, "") .replace(/]*>[\s\S]*?<\/iframe>/gi, "") - .replace(/]*>[\s\S]*?<\/object>/gi, "") - .replace(/]*>/gi, "") - .replace(/]*>[\s\S]*?<\/form>/gi, "") - .replace(/on\w+\s*=\s*["'][^"']*["']/gi, "") - .replace(/on\w+\s*=\s*[^\s>]*/gi, "") - .replace(/]*>/gi, "

") - .replace(/\s*(?:💬\s*)?\*\*(.*?)\*\*\s*[::]\s*(.*?)$/gm, (match, name, text) => { + const quoteBg = isAi ? 'bg-blue-50 border-blue-200' : 'bg-white/20 border-white/30' + const nameColor = isAi ? 'text-blue-700' : 'text-white font-semibold' + const textColor = isAi ? 'text-gray-600' : 'text-blue-100' + return `

` + }) + + parsed = parsed.replace(/\n/g, "
") + return parsed } -const resetFilters = () => { - filterStatus.value = "" - filterSentiment.value = "" - filterKeyword.value = "" - page.value = 1 - fetchReplies() -} +const resetFilters = () => { filterStatus.value = ""; filterSentiment.value = ""; filterKeyword.value = ""; page.value = 1; fetchReplies() } -watch([filterStatus, filterSentiment, filterKeyword], () => { - page.value = 1 - fetchReplies() -}) - -watch(page, () => { - selectedNames.value.clear() - selectAll.value = false - fetchReplies() -}) +watch([filterStatus, filterSentiment, filterKeyword], () => { page.value = 1; fetchReplies() }) +watch(page, () => { selectedNames.value.clear(); selectAll.value = false; fetchReplies() }) onMounted(fetchReplies)