diff --git a/client/src/components/admin/AdminLayout.vue b/client/src/components/admin/AdminLayout.vue index d9bc18a..ad26def 100644 --- a/client/src/components/admin/AdminLayout.vue +++ b/client/src/components/admin/AdminLayout.vue @@ -234,13 +234,22 @@ const menuItems = ref([ { title: '角色管理', path: '/admin/roles', icon: '🔒' } ] }, + { + title: '附件管理', + icon: '📎', + isOpen: false, + children: [ + { title: '附件库', path: '/admin/attachments', icon: '📦' }, + { title: '附件分类管理', path: '/admin/attachment-categories', icon: '📂' }, + { title: 'OSS配置', path: '/admin/oss-configs', icon: '☁️' } + ] + }, { title: '系统设置', icon: '⚙️', isOpen: false, children: [ { title: '全局配置', path: '/admin/settings', icon: '🛠️' }, - { title: '附件管理', path: '/admin/attachments', icon: '📎' }, { title: '操作日志', path: '/admin/logs', icon: '📋' } ] } diff --git a/client/src/components/admin/AttachmentDetailModal.vue b/client/src/components/admin/AttachmentDetailModal.vue new file mode 100644 index 0000000..7d3a626 --- /dev/null +++ b/client/src/components/admin/AttachmentDetailModal.vue @@ -0,0 +1,204 @@ + + + + + diff --git a/client/src/components/admin/ImageUpload.vue b/client/src/components/admin/ImageUpload.vue new file mode 100644 index 0000000..f157c85 --- /dev/null +++ b/client/src/components/admin/ImageUpload.vue @@ -0,0 +1,386 @@ + + + + + diff --git a/client/src/components/admin/PostRelationModal.vue b/client/src/components/admin/PostRelationModal.vue index 7418774..605f275 100644 --- a/client/src/components/admin/PostRelationModal.vue +++ b/client/src/components/admin/PostRelationModal.vue @@ -21,10 +21,11 @@ + 新建分类 - +
@@ -49,10 +50,11 @@ + 新建专栏
- +
@@ -132,7 +134,7 @@ diff --git a/client/src/pages/admin/AttachmentCategoryForm.vue b/client/src/pages/admin/AttachmentCategoryForm.vue new file mode 100644 index 0000000..0acd988 --- /dev/null +++ b/client/src/pages/admin/AttachmentCategoryForm.vue @@ -0,0 +1,130 @@ + + + diff --git a/client/src/pages/admin/Attachments.vue b/client/src/pages/admin/Attachments.vue index 8012958..580250a 100644 --- a/client/src/pages/admin/Attachments.vue +++ b/client/src/pages/admin/Attachments.vue @@ -47,6 +47,7 @@
@@ -80,6 +81,16 @@
+ + +
@@ -123,16 +134,16 @@ import { ref, onMounted, computed } from 'vue' import { useToast } from '../../composables/useToast' import CustomSelect from '../../components/CustomSelect.vue' +import AttachmentDetailModal from '../../components/admin/AttachmentDetailModal.vue' +import { updateAttachment, type Attachment } from '../../services/api' const toast = useToast() -interface Attachment { - id: number - originalName: string - fileUrl: string - fileSize: number - fileType: string +interface LocalAttachment extends Attachment { categoryId?: number + mimeType?: string + storageType?: string + createdAt?: string } interface Category { @@ -140,10 +151,12 @@ interface Category { name: string } -const attachments = ref([]) +const attachments = ref([]) const categories = ref([]) const loading = ref(false) const showUploadModal = ref(false) +const showDetailModal = ref(false) +const selectedAttachment = ref(null) const uploadInput = ref(null) const uploadCategoryId = ref(0) const uploading = ref(false) @@ -177,6 +190,7 @@ const uploadCategoryOptions = computed(() => { ] }) + const API_BASE = 'http://localhost:8081/api' const getAuthHeaders = () => { const token = localStorage.getItem('token') @@ -297,6 +311,35 @@ const formatFileSize = (bytes: number): string => { return (bytes / (1024 * 1024)).toFixed(1) + ' MB' } +const openDetailModal = (attachment: LocalAttachment) => { + selectedAttachment.value = attachment + showDetailModal.value = true +} + +const handleSaveCategory = async (data: { categoryId: number | null }) => { + if (!selectedAttachment.value) return + + try { + await updateAttachment(selectedAttachment.value.id, data) + + toast.showToast('分类更新成功', 'success') + + // 更新本地数据 + if (selectedAttachment.value) { + selectedAttachment.value.categoryId = data.categoryId || undefined + } + + // 刷新列表 + loadAttachments() + + // 关闭模态框 + showDetailModal.value = false + selectedAttachment.value = null + } catch (err: any) { + toast.showToast(err.message || '更新失败', 'error') + } +} + onMounted(() => { loadCategories() loadAttachments() diff --git a/client/src/pages/admin/ColumnForm.vue b/client/src/pages/admin/ColumnForm.vue index 2737cce..d31276b 100644 --- a/client/src/pages/admin/ColumnForm.vue +++ b/client/src/pages/admin/ColumnForm.vue @@ -32,13 +32,10 @@
- - 封面图片 +
@@ -87,13 +84,14 @@
- -
@@ -123,7 +121,9 @@ import { ref, reactive, onMounted, computed } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useToast } from '../../composables/useToast' -import { createColumn, updateColumn, fetchColumn } from '../../services/api' +import { createColumn, updateColumn, fetchColumn, fetchPosts, fetchColumnPosts, addPostToColumn, removePostFromColumn, Post } from '../../services/api' +import CustomSelect from '../../components/CustomSelect.vue' +import ImageUpload from '../../components/admin/ImageUpload.vue' const router = useRouter() const route = useRoute() @@ -140,6 +140,21 @@ const form = reactive({ sortOrder: 0 }) +const allPosts = ref([]) +const columnPosts = ref([]) +const selectedPostId = ref(0) + +// 计算属性:文章选项 +const postOptions = computed(() => { + return [ + { value: 0, label: '选择文章添加到专栏...' }, + ...allPosts.value.map(post => ({ + value: post.id, + label: `${post.title} (${post.isPublished ? '已发布' : '草稿'})` + })) + ] +}) + const handleSubmit = async () => { if (!form.name.trim()) { toast.showToast('名称不能为空', 'error') @@ -170,6 +185,57 @@ const handleCancel = () => { router.back() } +const loadPosts = async () => { + try { + const posts = await fetchPosts() + allPosts.value = posts + } catch (error) { + console.error('Failed to load posts:', error) + } +} + +const loadColumnPosts = async () => { + if (!isEditing.value) return + try { + const id = parseInt(route.params.id as string) + const posts = await fetchColumnPosts(id) + columnPosts.value = posts + } catch (error) { + console.error('Failed to load column posts:', error) + } +} + +const handleAddPost = async () => { + if (!selectedPostId.value || selectedPostId.value === 0) return + if (!isEditing.value) return + + try { + const id = parseInt(route.params.id as string) + const postId = typeof selectedPostId.value === 'string' ? parseInt(selectedPostId.value) : selectedPostId.value + await addPostToColumn(id, postId) + toast.showToast('文章添加成功', 'success') + selectedPostId.value = 0 + await loadColumnPosts() + await loadPosts() // 重新加载以更新选项 + } catch (error: any) { + toast.showToast(error.message || '添加文章失败', 'error') + } +} + +const handleRemovePost = async (postId: number) => { + if (!isEditing.value) return + + try { + const id = parseInt(route.params.id as string) + await removePostFromColumn(id, postId) + toast.showToast('文章移除成功', 'success') + await loadColumnPosts() + await loadPosts() // 重新加载以更新选项 + } catch (error: any) { + toast.showToast(error.message || '移除文章失败', 'error') + } +} + onMounted(async () => { if (isEditing.value) { try { @@ -182,10 +248,12 @@ onMounted(async () => { form.isActive = column.isActive form.sortOrder = column.sortOrder } + await loadColumnPosts() } catch (error) { console.error('Failed to load column:', error) toast.showToast('加载数据失败', 'error') } } + await loadPosts() }) diff --git a/client/src/pages/admin/OSSConfigs.vue b/client/src/pages/admin/OSSConfigs.vue new file mode 100644 index 0000000..1273d8a --- /dev/null +++ b/client/src/pages/admin/OSSConfigs.vue @@ -0,0 +1,555 @@ + + + diff --git a/client/src/pages/admin/PartnerForm.vue b/client/src/pages/admin/PartnerForm.vue index 7aa1ef3..169d89d 100644 --- a/client/src/pages/admin/PartnerForm.vue +++ b/client/src/pages/admin/PartnerForm.vue @@ -10,8 +10,11 @@
- - + +
@@ -38,6 +41,7 @@ import { ref, computed, onMounted } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useToast } from '../../composables/useToast' import { createPartner, updatePartner, fetchPartners } from '../../services/api' +import ImageUpload from '../../components/admin/ImageUpload.vue' const router = useRouter() const route = useRoute() diff --git a/client/src/pages/admin/PostForm.vue b/client/src/pages/admin/PostForm.vue index 68117e5..6f067b7 100644 --- a/client/src/pages/admin/PostForm.vue +++ b/client/src/pages/admin/PostForm.vue @@ -29,7 +29,10 @@
-
+
{ router.push('/admin/posts') } +// Handle paste event for image upload +const handlePaste = async (e: ClipboardEvent) => { + const items = e.clipboardData?.items + if (!items) return + + // Check if any item is an image + let hasImage = false + for (let i = 0; i < items.length; i++) { + if (items[i].type.startsWith('image/')) { + hasImage = true + break + } + } + + if (!hasImage) return + + // Prevent default paste behavior for images + e.preventDefault() + + for (let i = 0; i < items.length; i++) { + const item = items[i] + if (item.type.startsWith('image/')) { + const file = item.getAsFile() + if (!file) continue + + try { + // Upload image + const formData = new FormData() + formData.append('file', file) + formData.append('categoryId', '1') // 默认上传到分类1 + formData.append('storageType', 'local') + + const token = localStorage.getItem('token') + const response = await fetch('http://localhost:8081/api/admin/attachments/upload', { + method: 'POST', + headers: { + ...(token ? { Authorization: `Bearer ${token}` } : {}) + }, + body: formData + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.message || '上传失败') + } + + const data = await response.json() + const imageUrl = data.result.fileUrl + + // Insert markdown image syntax + const imageMarkdown = `![${file.name}](${imageUrl})` + const currentContent = form.content + // Insert at the end of content (md-editor-v3 will handle cursor position) + form.content = currentContent + (currentContent ? '\n\n' : '') + imageMarkdown + '\n' + + toast.showToast('图片上传成功', 'success') + } catch (error: any) { + console.error('上传图片失败:', error) + toast.showToast(error.message || '上传图片失败', 'error') + } + break // Only handle first image + } + } +} + // Load initial data const loadData = async () => { try { diff --git a/client/src/pages/admin/Settings.vue b/client/src/pages/admin/Settings.vue index fef3908..a1b1784 100644 --- a/client/src/pages/admin/Settings.vue +++ b/client/src/pages/admin/Settings.vue @@ -4,36 +4,8 @@

系统配置管理

- -
-
- - -
-
- - -
+ +
@@ -130,241 +102,19 @@
-
- - -
-
-

OSS存储配置

- -
- -
-
- - - - - - - - - - - - - - - - - - - - - - - -
名称存储类型Bucket区域域名状态操作
{{ config.name }} - - {{ getStorageTypeLabel(config.storageType) }} - - {{ config.bucket || '-' }}{{ config.region || '-' }}{{ config.domain || '-' }} - - {{ config.isActive === 1 ? '已启用' : '已停用' }} - - -
- - -
-
-
- -
-
☁️
-

暂无OSS配置

-

点击上方按钮添加OSS配置

-
-
- - -
-
- -
-
-

{{ editingOSSConfig ? '编辑OSS配置' : '新增OSS配置' }}

- -
- -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
-
- - -
- -
- - -
-
- -
- - -
- -
- -
- -
- - -
- -
-
-
-
diff --git a/client/src/pages/admin/TestimonialForm.vue b/client/src/pages/admin/TestimonialForm.vue index 50f4581..ee435a2 100644 --- a/client/src/pages/admin/TestimonialForm.vue +++ b/client/src/pages/admin/TestimonialForm.vue @@ -15,8 +15,11 @@
- - + +
@@ -43,6 +46,7 @@ import { ref, computed, onMounted } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useToast } from '../../composables/useToast' import { createTestimonial, updateTestimonial, fetchTestimonials } from '../../services/api' +import ImageUpload from '../../components/admin/ImageUpload.vue' const router = useRouter() const route = useRoute() diff --git a/client/src/pages/admin/WorkForm.vue b/client/src/pages/admin/WorkForm.vue index cc7fdd5..3cbb62f 100644 --- a/client/src/pages/admin/WorkForm.vue +++ b/client/src/pages/admin/WorkForm.vue @@ -13,6 +13,7 @@ v-model="form.title" placeholder="请输入作品标题" required + class="admin-input" />
{{ errors.title }} @@ -28,6 +29,7 @@ v-model="form.category" placeholder="请输入作品分类" required + class="admin-input" />
{{ errors.category }} @@ -43,6 +45,7 @@ v-model="form.year" placeholder="请输入创作年份" required + class="admin-input" />
{{ errors.year }} @@ -51,13 +54,10 @@
- - 作品主图 +
{{ errors.heroImg }} @@ -73,52 +73,150 @@ placeholder="请输入作品描述" rows="5" required + class="admin-input resize-none" >
{{ errors.desc }}
- +
- - +
+ + +
+ +
+
+ + +
+
+ + +
+ +
+
+ + +
+ +
+
+ + +
+ +
+ 暂无技术标签,请点击"添加标签" +
+
+
+
+
+ +
+ 暂无技术栈分类,请点击"添加分类" +
+
+
{{ errors.techStack }}
- +
- - + +
{{ errors.gallery }}
- +
- - + +
+
+ + +
+ +
+ + +
+ +
+ + +
+
{{ errors.links }}
@@ -126,10 +224,10 @@
- -
@@ -139,10 +237,11 @@