diff --git a/client/src/App.vue b/client/src/App.vue index b218105..5bfb2d4 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -37,6 +37,13 @@ + + + @@ -48,7 +55,11 @@ import MobileMenu from './components/MobileMenu.vue' import Footer from './components/Footer.vue' import InquiryModal from './components/InquiryModal.vue' import StarBackground from './components/StarBackground.vue' -import { getPublicSettings } from './services/api' +import UserProfileModal from './components/UserProfileModal.vue' +import { useUserProfileModal } from './composables/useUserProfileModal' +import { loadPublicSettings } from './composables/usePublicSettings' + +const { isOpen: profileModalOpen, activeUserId: profileUserId, closeUserProfile } = useUserProfileModal() const route = useRoute() @@ -67,7 +78,7 @@ const isAdminOrLoginOrBlog = computed(() => { // 应用网站配置到页面标题和meta标签 const applySiteSettings = async () => { try { - const settings = await getPublicSettings() + const settings = await loadPublicSettings() // 设置页面标题 if (settings.site_title) { diff --git a/client/src/components/AuthorInfo.vue b/client/src/components/AuthorInfo.vue index 0185cb4..c6d82e8 100644 --- a/client/src/components/AuthorInfo.vue +++ b/client/src/components/AuthorInfo.vue @@ -1,10 +1,14 @@ \ No newline at end of file + diff --git a/client/src/components/Header.vue b/client/src/components/Header.vue index e7c1199..bb68e2d 100644 --- a/client/src/components/Header.vue +++ b/client/src/components/Header.vue @@ -185,11 +185,12 @@ - - diff --git a/client/src/components/MobileMenu.vue b/client/src/components/MobileMenu.vue index 5b8655b..4cc6d95 100644 --- a/client/src/components/MobileMenu.vue +++ b/client/src/components/MobileMenu.vue @@ -14,31 +14,23 @@ \ No newline at end of file diff --git a/client/src/components/SnippetModal.vue b/client/src/components/SnippetModal.vue index 56468c0..0ba5d68 100644 --- a/client/src/components/SnippetModal.vue +++ b/client/src/components/SnippetModal.vue @@ -216,10 +216,37 @@ const showCollapseToggle = computed( () => showHtmlPreview.value && activeRightTab.value === 'preview' ) -const renderedDescription = computed(() => { - if (!props.description) return '' - return renderMarkdown(props.description) -}) +const renderedDescription = ref('') +let descriptionGeneration = 0 + +const renderDescription = (content: string) => { + const generation = ++descriptionGeneration + renderedDescription.value = '' + + const run = async () => { + try { + const html = await renderMarkdown(content) + if (generation !== descriptionGeneration) return + renderedDescription.value = html + } catch (err) { + console.error('Failed to render snippet description:', err) + } + } + + void run() +} + +watch( + [() => props.description, () => props.isOpen], + ([description, isOpen]) => { + if (!isOpen || !description) { + renderedDescription.value = '' + return + } + renderDescription(description) + }, + { immediate: true } +) const highlightCode = () => { if (codeBlock.value) { diff --git a/client/src/components/admin/AdminLayout.vue b/client/src/components/admin/AdminLayout.vue index 9fdb367..6193065 100644 --- a/client/src/components/admin/AdminLayout.vue +++ b/client/src/components/admin/AdminLayout.vue @@ -28,15 +28,20 @@
-
-
- {{ currentUser.username?.charAt(0).toUpperCase() || 'A' }} + +
+ + {{ currentUser.username?.charAt(0).toUpperCase() || 'A' }}

{{ currentUser.username }}

-

Administrator

+

个人中心

-
+
@@ -84,7 +89,11 @@ v-if="isUserDropdownOpen" class="absolute right-0 mt-2 w-48 bg-art-admin-card border border-white/10 rounded-lg shadow-xl py-1 z-50 origin-top-right animate-reveal" > - + @@ -116,12 +125,13 @@ import { useToast } from '../../composables/useToast' import { useAuth } from '../../composables/useAuth' import SessionExpiredModal from './SessionExpiredModal.vue' import AdminMenuItem from './AdminMenuItem.vue' +import { getCurrentUser } from '../../services/api' import { openActiveMenuAncestors, findRouteTitle, type MenuItem } from './adminMenuTypes' const router = useRouter() const route = useRoute() const toast = useToast() -const { logout, scheduleExpiryCheck, getUser, isAuthenticated } = useAuth() +const { logout, scheduleExpiryCheck, getUser, isAuthenticated, setUser } = useAuth() const isSidebarOpen = ref(true) const currentUser = ref(getUser()) @@ -139,8 +149,13 @@ const closeDropdown = (e: MouseEvent) => { } } -const handleChangePassword = () => { - toast.showToast('功能开发中...', 'success') +const goToProfile = () => { + router.push('/admin/profile') + isUserDropdownOpen.value = false +} + +const goToProfilePassword = () => { + router.push({ path: '/admin/profile', hash: '#password' }) isUserDropdownOpen.value = false } @@ -266,6 +281,15 @@ onMounted(() => { scheduleExpiryCheck() document.addEventListener('click', closeDropdown) openActiveMenuAncestors(menuItems.value, isActive) + + getCurrentUser() + .then(user => { + currentUser.value = user + setUser(user) + }) + .catch(() => { + currentUser.value = getUser() + }) }) watch(() => route.path, () => { diff --git a/client/src/components/admin/AdminTableFilter.vue b/client/src/components/admin/AdminTableFilter.vue index e7714ff..1b13751 100644 --- a/client/src/components/admin/AdminTableFilter.vue +++ b/client/src/components/admin/AdminTableFilter.vue @@ -40,7 +40,7 @@
- +
diff --git a/client/src/components/admin/ImagePicker.vue b/client/src/components/admin/ImagePicker.vue index 51cb21b..8317637 100644 --- a/client/src/components/admin/ImagePicker.vue +++ b/client/src/components/admin/ImagePicker.vue @@ -24,15 +24,30 @@
+
+ +

点击或拖拽图片到此处上传

+
+
-
- - import { ref, computed } from 'vue' import { useToast } from '../../composables/useToast' +import { useImageDropUpload } from '../../composables/useImageDropUpload' import { API_BASE, authFetch, parseApiResponse } from '../../services/api' import AttachmentLibraryModal from './AttachmentLibraryModal.vue' @@ -122,6 +130,11 @@ const previewSizeClass = computed(() => { return props.size === 'sm' ? 'w-20 h-20' : props.size === 'lg' ? 'w-40 h-40' : 'w-28 h-28' }) +const { isDragging, handleDragOver, handleDragEnter, handleDragLeave, handleDrop } = useImageDropUpload({ + disabled: () => props.disabled || uploading.value, + onFiles: (files) => uploadFile(files[0]), +}) + const triggerFileInput = () => { if (props.disabled || uploading.value) return fileInput.value?.click() @@ -187,3 +200,12 @@ const handleLibrarySelect = (urls: string[]) => { } } + + diff --git a/client/src/components/admin/ImageUpload.vue b/client/src/components/admin/ImageUpload.vue index eb1eae5..f203f54 100644 --- a/client/src/components/admin/ImageUpload.vue +++ b/client/src/components/admin/ImageUpload.vue @@ -53,6 +53,7 @@ Preview