Просмотр исходного кода

feat:完成文章管理页面的开发和适配;完成OSS对象存储API的联调;修复网站基础配置中OSS配置显示异常的问题;

yangyi 2 дней назад
Родитель
Сommit
943d6bb2f4

+ 5 - 2
src/api/attachment-controller.ts

@@ -13,11 +13,14 @@ export const getAttachmentController = () => {
    * @summary 上传图片到OSS
    */
   const upload = (uploadBody: UploadBody, params?: UploadParams) => {
+    const formData = new FormData();
+    formData.append("file", uploadBody.file);
+
     return customAxiosInstance<ResponseUploadVo>({
       url: `/attachments/upload`,
       method: "POST",
-      headers: { "Content-Type": "application/json" },
-      data: uploadBody,
+      headers: { "Content-Type": "multipart/form-data" },
+      data: formData,
       params,
     });
   };

+ 34 - 35
src/util/axios-instance.ts

@@ -6,7 +6,39 @@ export interface CustomInstanceConfig extends AxiosRequestConfig {
     skipAuth?: boolean; // 是否跳过鉴权
 }
 
-// 创建 Axios 实例
+function attachInterceptors(instance: AxiosInstance) {
+    instance.interceptors.request.use(
+        (config) => {
+            const token = localStorage.getItem('token');
+            if (token) {
+                config.headers.Authorization = `Bearer ${token}`;
+            }
+            if (config.data instanceof FormData) {
+                delete config.headers['Content-Type'];
+            }
+            return config;
+        },
+        (error) => {
+            return Promise.reject(error);
+        }
+    );
+
+    instance.interceptors.response.use(
+        (response: AxiosResponse) => {
+            if (response.data.code !== 0 && response.data.code !== 200) {
+                return Promise.reject(new Error(response.data.message || '请求失败'));
+            }
+            return response.data;
+        },
+        (error) => {
+            if (error.response?.status === 401) {
+                window.location.href = '/login';
+            }
+            return Promise.reject(error);
+        }
+    );
+}
+
 const axiosInstance: AxiosInstance = axios.create({
     baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
     timeout: 30000,
@@ -14,40 +46,7 @@ const axiosInstance: AxiosInstance = axios.create({
         'Content-Type': 'application/json',
     },
 });
-
-// 请求拦截器
-axiosInstance.interceptors.request.use(
-    (config) => {
-        // 添加 token
-        const token = localStorage.getItem('token');
-        if (token) {
-            config.headers.Authorization = `Bearer ${token}`;
-        }
-        return config;
-    },
-    (error) => {
-        return Promise.reject(error);
-    }
-);
-
-// 响应拦截器
-axiosInstance.interceptors.response.use(
-    (response: AxiosResponse) => {
-        // 根据后端返回格式调整
-        if (response.data.code !== 0 && response.data.code !== 200) {
-            return Promise.reject(new Error(response.data.message || '请求失败'));
-        }
-        return response.data;
-    },
-    (error) => {
-        // 统一错误处理
-        if (error.response?.status === 401) {
-            // 未授权,跳转登录
-            window.location.href = '/login';
-        }
-        return Promise.reject(error);
-    }
-);
+attachInterceptors(axiosInstance);
 
 // 自定义实例函数(供 Orval 使用)
 export const customAxiosInstance = <T>(config: CustomInstanceConfig): Promise<T> => {

+ 6 - 0
src/util/upload.ts

@@ -0,0 +1,6 @@
+import { getAttachmentController } from '../api/attachment-controller'
+
+export async function uploadImage(file: File, type = 'post_image'): Promise<string> {
+  const res = await getAttachmentController().upload({ file }, { type })
+  return res.data?.url ?? ''
+}

+ 47 - 2
src/view/PostEditorView.vue

@@ -29,13 +29,13 @@
 
         <el-form-item label="内容简介">
           <div class="editor-wrapper">
-            <QuillEditor v-model:content="form.contentIntro" content-type="html" :toolbar="toolbar" class="quill-editor" />
+            <QuillEditor ref="quillIntroRef" @ready="onReadyIntro" v-model:content="form.contentIntro" content-type="html" :toolbar="toolbar" class="quill-editor" />
           </div>
         </el-form-item>
 
         <el-form-item label="付费内容">
           <div class="editor-wrapper">
-            <QuillEditor v-model:content="form.contentPaid" content-type="html" :toolbar="toolbar" class="quill-editor" />
+            <QuillEditor ref="quillPaidRef" @ready="onReadyPaid" v-model:content="form.contentPaid" content-type="html" :toolbar="toolbar" class="quill-editor" />
           </div>
         </el-form-item>
 
@@ -57,6 +57,7 @@ import { ElMessage } from 'element-plus'
 import { QuillEditor } from '@vueup/vue-quill'
 import '@vueup/vue-quill/dist/vue-quill.snow.css'
 import { getPostController } from '../api/post-controller'
+import { uploadImage } from '../util/upload'
 
 const route = useRoute()
 const router = useRouter()
@@ -66,6 +67,9 @@ const isEdit = computed(() => !!route.params.id)
 const submitting = ref(false)
 const formRef = ref()
 
+const quillIntroRef = ref()
+const quillPaidRef = ref()
+
 const toolbar = [
   ['bold', 'italic', 'underline', 'strike'],
   [{ list: 'ordered' }, { list: 'bullet' }],
@@ -92,6 +96,47 @@ function goBack() {
   router.push('/admin/posts')
 }
 
+function setupImageHandler(quill: any) {
+  const toolbar = quill.getModule('toolbar')
+  toolbar.addHandler('image', () => {
+    const input = document.createElement('input')
+    input.type = 'file'
+    input.accept = 'image/jpeg,image/png,image/gif,image/webp,image/bmp'
+    input.click()
+
+    input.onchange = async () => {
+      const file = input.files?.[0]
+      if (!file) return
+
+      if (file.size > 1024 * 1024) {
+        ElMessage.error('图片大小不能超过 1MB')
+        return
+      }
+
+      const loading = (ElMessage as any)({ message: '正在上传图片...', type: 'loading', duration: 0 })
+      try {
+        const url = await uploadImage(file, 'post_image')
+        const range = quill.getSelection(true)
+        quill.insertEmbed(range.index, 'image', url)
+        quill.setSelection(range.index + 1)
+      } catch {
+        ElMessage.error('图片上传失败')
+      } finally {
+        loading.close()
+        input.remove()
+      }
+    }
+  })
+}
+
+function onReadyIntro(quill: any) {
+  setupImageHandler(quill)
+}
+
+function onReadyPaid(quill: any) {
+  setupImageHandler(quill)
+}
+
 async function loadPost() {
   if (!isEdit.value) return
   try {

+ 4 - 1
src/view/SiteSettingsView.vue

@@ -49,7 +49,10 @@ onMounted(async () => {
       method: 'GET',
     })
     if (res.code === 200 && res.data) {
-      const d = res.data as Record<string, unknown>
+      let d = res.data as Record<string, unknown>
+      if (typeof d === 'string') {
+        try { d = JSON.parse(d) as Record<string, unknown> } catch { return }
+      }
       ossForm.endpoint = String(d.endpoint ?? '')
       ossForm.region = String(d.region ?? '')
       ossForm.bucket = String(d.bucket ?? '')