From 734f82b2c66c95345b847b4957fe928029c1825f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Sat, 4 Jul 2026 16:59:57 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=A1=B5=E9=9D=A2=E3=80=81?= =?UTF-8?q?=E4=BF=AE=E5=A4=8DBUG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/UserProfileModal.vue | 193 ++++++++++++++++++ client/src/composables/useImageDropUpload.ts | 56 +++++ client/src/composables/usePublicSettings.ts | 58 ++++++ client/src/composables/useUserProfileModal.ts | 24 +++ client/src/config/settingsSchema.ts | 126 ++++++++++++ client/src/pages/admin/Profile.vue | 191 +++++++++++++++++ server/handlers/user_profile.go | 161 +++++++++++++++ server/repositories/time_format.go | 10 + 8 files changed, 819 insertions(+) create mode 100644 client/src/components/UserProfileModal.vue create mode 100644 client/src/composables/useImageDropUpload.ts create mode 100644 client/src/composables/usePublicSettings.ts create mode 100644 client/src/composables/useUserProfileModal.ts create mode 100644 client/src/config/settingsSchema.ts create mode 100644 client/src/pages/admin/Profile.vue create mode 100644 server/handlers/user_profile.go create mode 100644 server/repositories/time_format.go diff --git a/client/src/components/UserProfileModal.vue b/client/src/components/UserProfileModal.vue new file mode 100644 index 0000000..88311d6 --- /dev/null +++ b/client/src/components/UserProfileModal.vue @@ -0,0 +1,193 @@ + + + + + diff --git a/client/src/composables/useImageDropUpload.ts b/client/src/composables/useImageDropUpload.ts new file mode 100644 index 0000000..db3e33f --- /dev/null +++ b/client/src/composables/useImageDropUpload.ts @@ -0,0 +1,56 @@ +import { ref, type Ref } from 'vue' + +export function useImageDropUpload(options: { + disabled?: Ref | (() => boolean) + onFiles: (files: File[]) => void + acceptImagesOnly?: boolean +}) { + const isDragging = ref(false) + + const isDisabled = () => { + if (typeof options.disabled === 'function') return options.disabled() + return options.disabled?.value ?? false + } + + const handleDragOver = (e: DragEvent) => { + if (isDisabled()) return + e.preventDefault() + isDragging.value = true + } + + const handleDragEnter = (e: DragEvent) => { + if (isDisabled()) return + e.preventDefault() + isDragging.value = true + } + + const handleDragLeave = (e: DragEvent) => { + if (isDisabled()) return + e.preventDefault() + isDragging.value = false + } + + const handleDrop = (e: DragEvent) => { + if (isDisabled()) return + e.preventDefault() + isDragging.value = false + + if (!e.dataTransfer?.files?.length) return + + let files = Array.from(e.dataTransfer.files) + if (options.acceptImagesOnly !== false) { + files = files.filter(file => file.type.startsWith('image/')) + } + if (files.length > 0) { + options.onFiles(files) + } + } + + return { + isDragging, + handleDragOver, + handleDragEnter, + handleDragLeave, + handleDrop, + } +} diff --git a/client/src/composables/usePublicSettings.ts b/client/src/composables/usePublicSettings.ts new file mode 100644 index 0000000..8ac2e77 --- /dev/null +++ b/client/src/composables/usePublicSettings.ts @@ -0,0 +1,58 @@ +import { onMounted, ref } from 'vue' +import { getPublicSettings, type PublicSettings } from '../services/api' + +const cache = ref(null) +let loading: Promise | null = null +let listenerRegistered = false + +function registerSettingsListener() { + if (typeof window === 'undefined' || listenerRegistered) return + listenerRegistered = true + window.addEventListener('public-settings-changed', () => { + cache.value = null + loading = null + void loadPublicSettings(true) + }) +} + +export async function loadPublicSettings(force = false): Promise { + if (!force && cache.value) return cache.value + if (!force && loading) return loading + + loading = getPublicSettings().then(data => { + cache.value = data + loading = null + return data + }) + + return loading +} + +export function usePublicSettings() { + onMounted(() => { + void loadPublicSettings() + registerSettingsListener() + }) + + const load = loadPublicSettings + + const invalidate = () => { + cache.value = null + loading = null + } + + const getStringSetting = (key: keyof PublicSettings, fallback = ''): string => { + const raw = cache.value?.[key] + if (raw == null || String(raw).trim() === '') return fallback + return String(raw) + } + + const getNumberSetting = (key: keyof PublicSettings, fallback: number): number => { + const raw = cache.value?.[key] + if (!raw) return fallback + const n = parseInt(String(raw), 10) + return Number.isFinite(n) && n > 0 ? n : fallback + } + + return { cache, load, invalidate, getStringSetting, getNumberSetting } +} diff --git a/client/src/composables/useUserProfileModal.ts b/client/src/composables/useUserProfileModal.ts new file mode 100644 index 0000000..e293b5f --- /dev/null +++ b/client/src/composables/useUserProfileModal.ts @@ -0,0 +1,24 @@ +import { ref } from 'vue' + +const isOpen = ref(false) +const activeUserId = ref(null) + +export function useUserProfileModal() { + const openUserProfile = (userId: number) => { + if (!userId) return + activeUserId.value = userId + isOpen.value = true + } + + const closeUserProfile = () => { + isOpen.value = false + activeUserId.value = null + } + + return { + isOpen, + activeUserId, + openUserProfile, + closeUserProfile, + } +} diff --git a/client/src/config/settingsSchema.ts b/client/src/config/settingsSchema.ts new file mode 100644 index 0000000..beb5209 --- /dev/null +++ b/client/src/config/settingsSchema.ts @@ -0,0 +1,126 @@ +export type SettingFieldType = 'text' | 'textarea' | 'number' | 'menu-checkboxes' | 'email' + +export interface SettingSchemaItem { + key: string + label: string + type: SettingFieldType + group: 'site' | 'seo' | 'navigation' | 'pagination' | 'contact' + public: boolean + default: string + description: string +} + +export const SETTING_GROUPS: { id: SettingSchemaItem['group']; label: string }[] = [ + { id: 'site', label: '站点信息' }, + { id: 'seo', label: 'SEO 优化' }, + { id: 'navigation', label: '导航菜单' }, + { id: 'pagination', label: '分页设置' }, + { id: 'contact', label: '联系方式' }, +] + +export const SETTINGS_SCHEMA: SettingSchemaItem[] = [ + { + key: 'site_title', + label: '网站标题', + type: 'text', + group: 'site', + public: true, + default: '年糕崽崽.Dev', + description: '浏览器标签页和页头显示的网站名称', + }, + { + key: 'site_author', + label: '网站作者', + type: 'text', + group: 'site', + public: true, + default: '', + description: '网站作者名称,用于 meta 标签', + }, + { + key: 'site_description', + label: '网站描述', + type: 'textarea', + group: 'seo', + public: true, + default: '', + description: '搜索引擎摘要描述', + }, + { + key: 'site_keywords', + label: '网站关键词', + type: 'textarea', + group: 'seo', + public: true, + default: '', + description: 'SEO 关键词,逗号分隔', + }, + { + key: 'visible_menus', + label: '前台菜单', + type: 'menu-checkboxes', + group: 'navigation', + public: true, + default: '["home","blog","columns","works","snippets","about","services"]', + description: '控制前台导航栏显示的菜单项', + }, + { + key: 'posts_per_page', + label: '文章每页数量', + type: 'number', + group: 'pagination', + public: true, + default: '10', + description: '博客列表页每页显示的文章数', + }, + { + key: 'works_per_page', + label: '作品每页数量', + type: 'number', + group: 'pagination', + public: true, + default: '12', + description: '作品列表页每页显示数量', + }, + { + key: 'snippets_per_page', + label: '代码每页数量', + type: 'number', + group: 'pagination', + public: true, + default: '12', + description: '代码片段列表页每页显示数量', + }, + { + key: 'footer_icp', + label: 'ICP 备案号', + type: 'text', + group: 'contact', + public: true, + default: '', + description: '页脚 ICP 备案信息', + }, + { + key: 'contact_email', + label: '联系邮箱', + type: 'email', + group: 'contact', + public: true, + default: '', + description: '公开展示的联系邮箱', + }, +] + +export const MENU_OPTIONS = [ + { key: 'home', label: '首页' }, + { key: 'blog', label: '思考' }, + { key: 'columns', label: '专栏' }, + { key: 'works', label: '作品' }, + { key: 'snippets', label: '代码' }, + { key: 'about', label: '关于' }, + { key: 'services', label: '合作' }, +] + +export const getSchemaByKey = (key: string) => SETTINGS_SCHEMA.find(s => s.key === key) + +export const getSchemaKeys = () => SETTINGS_SCHEMA.map(s => s.key) diff --git a/client/src/pages/admin/Profile.vue b/client/src/pages/admin/Profile.vue new file mode 100644 index 0000000..2397176 --- /dev/null +++ b/client/src/pages/admin/Profile.vue @@ -0,0 +1,191 @@ +