diff --git a/src/App.vue b/src/App.vue index 5950714..25bcd2a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,12 +1,21 @@ @@ -143,8 +159,8 @@ onMounted(load) {{ error }} - - + + 止 @@ -191,6 +207,11 @@ onMounted(load) gap: 16px; } +.comment-item:not(:last-child) .comment-card { + border-bottom: 1px dashed #d4c4b0; + padding-bottom: 18px; +} + .comment-node { width: 11px; height: 11px; diff --git a/src/components/SongInput.vue b/src/components/SongInput.vue index 1269ff8..e2eb59b 100644 --- a/src/components/SongInput.vue +++ b/src/components/SongInput.vue @@ -9,8 +9,10 @@ withDefaults( rows?: number textarea?: boolean maxlength?: number + disabled?: boolean + readonly?: boolean }>(), - { label: '', modelValue: '', placeholder: '', type: 'text', required: false, rows: 3, textarea: false, maxlength: undefined } + { label: '', modelValue: '', placeholder: '', type: 'text', required: false, rows: 3, textarea: false, maxlength: undefined, disabled: false, readonly: false } ) defineEmits<{ 'update:modelValue': [value: string] }>() defineOptions({ name: 'SongInput' }) @@ -28,6 +30,8 @@ defineOptions({ name: 'SongInput' }) :placeholder="placeholder" :rows="rows" :maxlength="maxlength" + :disabled="disabled" + :readonly="readonly" @input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)" /> @@ -91,4 +97,11 @@ defineOptions({ name: 'SongInput' }) outline: none; border-color: var(--song-primary); } + +.song-field__input:disabled, +.song-field__input[readonly] { + background: var(--ac-surface); + color: var(--ac-text-secondary); + cursor: not-allowed; +} \ No newline at end of file diff --git a/src/views/ArticleDetailView.vue b/src/views/ArticleDetailView.vue index 0e89e05..834406a 100644 --- a/src/views/ArticleDetailView.vue +++ b/src/views/ArticleDetailView.vue @@ -3,11 +3,13 @@ import { ref, computed, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { articleApi } from '@/api/client' import { renderMarkdown } from '@/utils/markdown' +import DOMPurify from 'dompurify' import { setPageMeta } from '@/utils/seo' import { useApi } from '@/composables/useApi' import CommentSection from '@/components/CommentSection.vue' import ArticleMetaBar from '@/components/ArticleMetaBar.vue' import type { ArticleDetail, ResponseArticleDetail } from '@/generated/api' +import 'github-markdown-css/github-markdown-light.css' const route = useRoute() const router = useRouter() @@ -23,8 +25,17 @@ const starred = ref(false) const starCount = ref(0) const starBusy = ref(false) +const isHtmlContent = computed(() => { + const c = article.value?.content + if (!c) return false + return /<[a-z][\s\S]*>/i.test(c) +}) + const renderedContent = computed(() => { if (!article.value?.content) return '' + if (isHtmlContent.value) { + return DOMPurify.sanitize(article.value.content) + } return renderMarkdown(article.value.content) }) @@ -100,7 +111,7 @@ watch(slug, load, { immediate: true }) {{ tag }} - + @@ -119,6 +130,7 @@ watch(slug, load, { immediate: true }) letter-spacing: 0.02em; margin: 0 0 12px; font-size: 28px; + line-height: 1.5; } .detail-summary { @@ -152,6 +164,16 @@ watch(slug, load, { immediate: true }) border-top: 1px solid var(--ac-border); padding-top: 20px; word-break: break-word; + background: transparent; + color: var(--ac-text); + font-family: var(--song-font-sans), system-ui, sans-serif; +} + +:deep(.markdown-body h1), +:deep(.markdown-body h2), +:deep(.markdown-body h3), +:deep(.markdown-body h4) { + font-family: 'Noto Serif SC', 'SimSun', 'STSong', serif; } .hint-text { diff --git a/src/views/admin/AdminLayout.vue b/src/views/admin/AdminLayout.vue index 33fb978..bdd7e22 100644 --- a/src/views/admin/AdminLayout.vue +++ b/src/views/admin/AdminLayout.vue @@ -1,45 +1,27 @@ - {{ auth.username || '管理员' }} - 退出 + {{ pageTitle }} @@ -63,19 +45,9 @@ async function handleLogout() { flex-shrink: 0; background: var(--ac-header-bg); padding: 20px 12px; - display: flex; - flex-direction: column; transition: background-color var(--song-dur-2) var(--song-ease); } -.sidebar-title { - color: var(--ac-header-text); - font-size: 15px; - font-weight: 400; - margin: 0 0 16px; - padding-left: 8px; -} - .sidebar-nav { display: flex; flex-direction: column; @@ -98,31 +70,6 @@ async function handleLogout() { color: var(--ac-header-text); } -.sidebar-foot { - margin-top: auto; - padding: 12px 8px 0; -} - -.theme-toggle { - width: 100%; - height: 34px; - border: 1px solid var(--ac-border); - border-radius: var(--song-radius-m); - background: transparent; - color: var(--ac-header-muted); - font-family: var(--song-font-serif); - font-size: 15px; - letter-spacing: 0.1em; - cursor: pointer; - transition: color var(--song-dur-1) var(--song-ease), border-color var(--song-dur-1) var(--song-ease), background var(--song-dur-1) var(--song-ease); -} - -.theme-toggle:hover { - color: var(--ac-header-text); - border-color: var(--ac-header-muted); - background: var(--ac-header-active-bg); -} - .content-wrap { flex: 1; display: flex; @@ -131,28 +78,25 @@ async function handleLogout() { } .topbar { + position: sticky; + top: 0; + z-index: 1000; display: flex; align-items: center; - justify-content: flex-end; - gap: 16px; - height: 52px; padding: 0 20px; + height: 52px; background: var(--ac-bg-card); border-bottom: 1px solid var(--ac-border); } -.current-user { - color: var(--ac-text-secondary); - font-size: 14px; -} - -.logout-link { - color: var(--ac-danger); - text-decoration: none; - font-size: 14px; +.topbar-title { + font-family: var(--song-font-serif); + font-size: 16px; + font-weight: 600; + color: var(--ac-text); } .content { padding: 20px; } - \ No newline at end of file + diff --git a/src/views/admin/ArticleListView.vue b/src/views/admin/ArticleListView.vue index 9d28f49..6821725 100644 --- a/src/views/admin/ArticleListView.vue +++ b/src/views/admin/ArticleListView.vue @@ -140,7 +140,7 @@ onMounted(load) {{ article.title }} - + {{ formatDate(article.publishedAt || article.createdAt) }} + - 编辑 - - 发布 + + + + - - 归档 + + + + + + + + + + + + + + + + + - 删除 @@ -207,6 +222,7 @@ onMounted(load) .article-table { width: 100%; + table-layout: fixed; border-collapse: collapse; background: var(--ac-bg-card); border: 1px solid var(--ac-border); @@ -227,6 +243,23 @@ onMounted(load) color: var(--ac-text-secondary); } +.status-cell { + width: 80px; + min-width: 80px; + white-space: nowrap; +} + +.date-cell { + width: 200px; + white-space: nowrap; +} + +.actions-cell { + width: 160px; + min-width: 160px; + white-space: nowrap; +} + .title-cell { font-family: 'Noto Serif SC', 'SimSun', 'STSong', serif; font-weight: 600; @@ -255,22 +288,36 @@ onMounted(load) .actions { display: flex; - gap: 10px; + align-items: center; + gap: 8px; } -.btn-link { +.icon-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; border: none; + border-radius: var(--song-radius-s); background: none; color: var(--ac-primary-ink); cursor: pointer; - font-size: 13px; - padding: 0; + transition: background var(--song-dur-1) var(--song-ease), color var(--song-dur-1) var(--song-ease); } -.btn-link.danger { +.icon-btn:hover { + background: var(--ac-active); +} + +.icon-btn--danger { color: var(--ac-danger-ink); } +.icon-btn--danger:hover { + background: var(--ac-danger-bg); +} + .error-text { color: var(--ac-danger); }
{{ error }}