From 7b387e479d1c1a6cc71698121e52d62064d2fade Mon Sep 17 00:00:00 2001 From: weiness <840833211@qq.com> Date: Fri, 24 Jul 2026 14:44:57 +0800 Subject: [PATCH] fix(frontend): share announcement rich text styles --- .../components/common/AnnouncementBell.vue | 118 +----------------- .../components/common/AnnouncementPopup.vue | 1 + .../__tests__/AnnouncementPopup.spec.ts | 72 +++++++++++ frontend/src/styles/announcement-markdown.css | 113 +++++++++++++++++ 4 files changed, 187 insertions(+), 117 deletions(-) create mode 100644 frontend/src/components/common/__tests__/AnnouncementPopup.spec.ts create mode 100644 frontend/src/styles/announcement-markdown.css diff --git a/frontend/src/components/common/AnnouncementBell.vue b/frontend/src/components/common/AnnouncementBell.vue index cfbd62a0b8..e80e449f28 100644 --- a/frontend/src/components/common/AnnouncementBell.vue +++ b/frontend/src/components/common/AnnouncementBell.vue @@ -322,6 +322,7 @@ import { useAnnouncementStore } from '@/stores/announcements' import { formatRelativeTime, formatRelativeWithDateTime } from '@/utils/format' import type { UserAnnouncement } from '@/types' import Icon from '@/components/icons/Icon.vue' +import '@/styles/announcement-markdown.css' const { t } = useI18n() const appStore = useAppStore() @@ -471,120 +472,3 @@ watch( background: linear-gradient(to bottom, #6b7280, #4b5563); } - - diff --git a/frontend/src/components/common/AnnouncementPopup.vue b/frontend/src/components/common/AnnouncementPopup.vue index e26d9ee30b..e0a0f23125 100644 --- a/frontend/src/components/common/AnnouncementPopup.vue +++ b/frontend/src/components/common/AnnouncementPopup.vue @@ -90,6 +90,7 @@ import { marked } from 'marked' import DOMPurify from 'dompurify' import { useAnnouncementStore } from '@/stores/announcements' import { formatRelativeWithDateTime } from '@/utils/format' +import '@/styles/announcement-markdown.css' const { t } = useI18n() const announcementStore = useAnnouncementStore() diff --git a/frontend/src/components/common/__tests__/AnnouncementPopup.spec.ts b/frontend/src/components/common/__tests__/AnnouncementPopup.spec.ts new file mode 100644 index 0000000000..2603b8ef4f --- /dev/null +++ b/frontend/src/components/common/__tests__/AnnouncementPopup.spec.ts @@ -0,0 +1,72 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import { createPinia, setActivePinia } from 'pinia' +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' + +import AnnouncementPopup from '../AnnouncementPopup.vue' +import { useAnnouncementStore } from '@/stores/announcements' + +const announcementMarkdownStyles = readFileSync( + resolve(process.cwd(), 'src/styles/announcement-markdown.css'), + 'utf8', +) + +vi.mock('vue-i18n', async () => { + const actual = await vi.importActual('vue-i18n') + return { + ...actual, + useI18n: () => ({ + t: (key: string) => key, + }), + } +}) + +describe('AnnouncementPopup', () => { + beforeEach(() => { + setActivePinia(createPinia()) + }) + + afterEach(() => { + document.body.innerHTML = '' + document.body.style.overflow = '' + }) + + it('renders mixed Markdown and HTML inside the shared styled container', async () => { + const store = useAnnouncementStore() + store.currentPopup = { + id: 1, + title: 'Mixed content announcement', + content: [ + '## Markdown heading', + '', + '

HTML heading

', + '', + '
Status
OK
', + '', + ].join('\n'), + notify_mode: 'popup', + created_at: '2026-07-24T07:30:00Z', + updated_at: '2026-07-24T07:30:00Z', + } + + const wrapper = mount(AnnouncementPopup) + await wrapper.vm.$nextTick() + + const content = document.body.querySelector('.markdown-body') + expect(content?.querySelector('h2')?.textContent).toBe('Markdown heading') + expect(content?.querySelector('h3')?.textContent).toBe('HTML heading') + expect(content?.querySelector('li')?.textContent).toBe('HTML list item') + expect(content?.querySelector('table td')?.textContent).toBe('OK') + expect(content?.querySelector('script')).toBeNull() + + wrapper.unmount() + }) + + it.each(['h2', 'h3', 'ul', 'li', 'blockquote', 'table', 'th', 'td', 'code'])( + 'loads a shared style rule for mixed-content <%s> elements', + (element) => { + expect(announcementMarkdownStyles).toContain(`.markdown-body ${element}`) + }, + ) +}) diff --git a/frontend/src/styles/announcement-markdown.css b/frontend/src/styles/announcement-markdown.css new file mode 100644 index 0000000000..c5852b1113 --- /dev/null +++ b/frontend/src/styles/announcement-markdown.css @@ -0,0 +1,113 @@ +/* Shared rich-text styles for announcement popup and bell detail content. */ +.markdown-body { + @apply text-[15px] leading-[1.75]; + @apply text-gray-700 dark:text-gray-300; +} + +.markdown-body h1 { + @apply mb-6 mt-8 border-b border-gray-200 pb-3 text-3xl font-bold text-gray-900 dark:border-dark-600 dark:text-white; +} + +.markdown-body h2 { + @apply mb-4 mt-7 border-b border-gray-100 pb-2 text-2xl font-bold text-gray-900 dark:border-dark-700 dark:text-white; +} + +.markdown-body h3 { + @apply mb-3 mt-6 text-xl font-semibold text-gray-900 dark:text-white; +} + +.markdown-body h4 { + @apply mb-2 mt-5 text-lg font-semibold text-gray-900 dark:text-white; +} + +.markdown-body p { + @apply mb-4 leading-relaxed; +} + +.markdown-body a { + @apply font-medium text-blue-600 underline decoration-blue-600/30 decoration-2 underline-offset-2 transition-all hover:decoration-blue-600 dark:text-blue-400 dark:decoration-blue-400/30 dark:hover:decoration-blue-400; +} + +.markdown-body ul, +.markdown-body ol { + @apply mb-4 ml-6 space-y-2; +} + +.markdown-body ul { + @apply list-disc; +} + +.markdown-body ol { + @apply list-decimal; +} + +.markdown-body li { + @apply pl-2 leading-relaxed; +} + +.markdown-body li::marker { + @apply text-blue-600 dark:text-blue-400; +} + +.markdown-body blockquote { + @apply relative my-5 border-l-4 border-blue-500 bg-blue-50/50 py-3 pl-5 pr-4 italic text-gray-700 dark:border-blue-400 dark:bg-blue-900/10 dark:text-gray-300; +} + +.markdown-body blockquote::before { + content: '"'; + @apply absolute -left-1 top-0 font-serif text-5xl text-blue-500/20 dark:text-blue-400/20; +} + +.markdown-body code { + @apply rounded-lg bg-gray-100 px-2 py-1 font-mono text-[13px] text-pink-600 dark:bg-dark-700 dark:text-pink-400; +} + +.markdown-body pre { + @apply my-5 overflow-x-auto rounded-xl border border-gray-200 bg-gray-50 p-5 dark:border-dark-600 dark:bg-dark-900/50; +} + +.markdown-body pre code { + @apply bg-transparent p-0 text-[13px] text-gray-800 dark:text-gray-200; +} + +.markdown-body hr { + @apply my-8 border-0 border-t-2 border-gray-200 dark:border-dark-700; +} + +.markdown-body table { + @apply mb-5 w-full overflow-hidden rounded-lg border border-gray-200 dark:border-dark-600; +} + +.markdown-body th, +.markdown-body td { + @apply border-b border-r border-gray-200 px-4 py-3 text-left dark:border-dark-600; +} + +.markdown-body th:last-child, +.markdown-body td:last-child { + @apply border-r-0; +} + +.markdown-body tr:last-child td { + @apply border-b-0; +} + +.markdown-body th { + @apply bg-gradient-to-br from-blue-50 to-indigo-50 font-semibold text-gray-900 dark:from-blue-900/20 dark:to-indigo-900/10 dark:text-white; +} + +.markdown-body tbody tr { + @apply transition-colors hover:bg-gray-50 dark:hover:bg-dark-700/30; +} + +.markdown-body img { + @apply my-5 max-w-full rounded-xl border border-gray-200 shadow-md dark:border-dark-600; +} + +.markdown-body strong { + @apply font-semibold text-gray-900 dark:text-white; +} + +.markdown-body em { + @apply italic text-gray-600 dark:text-gray-400; +}