From 9daf5e98ff4656b37a019caac744de0185cea627 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Mar 2026 19:17:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=20#40=20=E8=90=A5?= =?UTF-8?q?=E9=94=80=E5=9B=BE=E5=90=8E=E5=8F=B0=E4=B8=83=E7=89=9B=E7=9B=B4?= =?UTF-8?q?=E4=BC=A0=E4=B8=8E=E9=A1=B5=E9=9D=A2=E4=B8=8A=E4=BC=A0=E8=83=BD?= =?UTF-8?q?=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../marketing/handler/download_handler.go | 25 +++++ internal/routes/marketing_routes.go | 1 + web/marketing/index.html | 92 +++++++++++++++++-- 3 files changed, 112 insertions(+), 6 deletions(-) diff --git a/internal/marketing/handler/download_handler.go b/internal/marketing/handler/download_handler.go index 80e40cf..c5ffcdc 100644 --- a/internal/marketing/handler/download_handler.go +++ b/internal/marketing/handler/download_handler.go @@ -1,10 +1,13 @@ package handler import ( + "errors" "net/http" "github.com/gin-gonic/gin" + "wx_service/config" + qiniuservice "wx_service/internal/common/qiniu/service" "wx_service/internal/marketing/service" "wx_service/internal/middleware" "wx_service/internal/model" @@ -89,3 +92,25 @@ func (h *DownloadHandler) AdminStats(c *gin.Context) { } c.JSON(http.StatusOK, model.Success(stats)) } + +type adminQiniuTokenRequest struct { + Filename string `json:"filename"` +} + +func (h *DownloadHandler) AdminQiniuToken(c *gin.Context) { + var req adminQiniuTokenRequest + _ = c.ShouldBindJSON(&req) + + qiniuSvc := qiniuservice.NewQiniuService(config.AppConfig.Qiniu) + token, err := qiniuSvc.CreateUploadToken(0, 0, req.Filename) + if err != nil { + if errors.Is(err, qiniuservice.ErrQiniuNotConfigured) { + c.JSON(http.StatusServiceUnavailable, model.Error(http.StatusServiceUnavailable, "未配置七牛上传服务")) + return + } + c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "获取上传凭证失败")) + return + } + + c.JSON(http.StatusOK, model.Success(token)) +} diff --git a/internal/routes/marketing_routes.go b/internal/routes/marketing_routes.go index 85c63c7..590e4ef 100644 --- a/internal/routes/marketing_routes.go +++ b/internal/routes/marketing_routes.go @@ -46,5 +46,6 @@ func registerMarketingRoutes( admin.DELETE("/templates/:id", templateHandler.AdminDelete) admin.GET("/stats", downloadHandler.AdminStats) + admin.POST("/upload/qiniu/token", downloadHandler.AdminQiniuToken) } } diff --git a/web/marketing/index.html b/web/marketing/index.html index a138d47..a249b6e 100644 --- a/web/marketing/index.html +++ b/web/marketing/index.html @@ -25,6 +25,8 @@ .section-header h3 { font-size: 16px; color: #1a1a1a; } .tpl-thumb { width: 80px; height: 80px; object-fit: cover; border-radius: 4px; border: 1px solid #eee; } .icon-preview { width: 32px; height: 32px; object-fit: contain; } + .upload-inline { display: flex; gap: 8px; width: 100%; } + .upload-inline .el-input { flex: 1; } @@ -158,7 +160,12 @@ - + +
+ + 上传 +
+
@@ -179,8 +186,18 @@ - - + +
+ + 上传 +
+
+ +
+ + 上传 +
+
@@ -223,10 +240,13 @@ const app = createApp({ const catDialogVisible = ref(false) const catForm = reactive({ id: null, name: '', icon: '', sort_order: 0, statusBool: true }) const catSaving = ref(false) + const catIconUploading = ref(false) const tplDialogVisible = ref(false) const tplForm = reactive({ id: null, title: '', category_id: null, image_url: '', thumbnail_url: '', width: 0, height: 0, sort_order: 0, statusBool: true }) const tplSaving = ref(false) + const tplImageUploading = ref(false) + const tplThumbUploading = ref(false) const previewVisible = ref(false) const previewUrl = ref('') @@ -241,6 +261,66 @@ const app = createApp({ return data.data } + async function requestUploadToken(filename) { + return await api('POST', '/admin/marketing/upload/qiniu/token', { filename }) + } + + async function uploadFileToQiniu(file) { + const tokenData = await requestUploadToken(file.name) + if (!tokenData || !tokenData.token || !tokenData.key || !tokenData.upload_url) { + throw new Error('上传凭证返回异常') + } + + const formData = new FormData() + formData.append('token', tokenData.token) + formData.append('key', tokenData.key) + formData.append('file', file) + + const uploadResp = await fetch(tokenData.upload_url, { method: 'POST', body: formData }) + if (!uploadResp.ok) { + throw new Error(`上传失败(${uploadResp.status})`) + } + const uploadResult = await uploadResp.json().catch(() => ({})) + const finalKey = uploadResult.key || tokenData.key + if (!finalKey) throw new Error('上传后未返回文件 key') + + const cdn = (tokenData.cdn_domain || '').replace(/\/$/, '') + if (cdn) return `${cdn}/${finalKey}` + return finalKey + } + + async function pickAndUpload(target) { + const input = document.createElement('input') + input.type = 'file' + input.accept = 'image/*' + input.onchange = async () => { + const file = input.files && input.files[0] + if (!file) return + + const loadingRef = + target === 'category_icon' ? catIconUploading : + target === 'template_image' ? tplImageUploading : tplThumbUploading + + loadingRef.value = true + try { + const uploadedURL = await uploadFileToQiniu(file) + if (target === 'category_icon') { + catForm.icon = uploadedURL + } else if (target === 'template_image') { + tplForm.image_url = uploadedURL + } else { + tplForm.thumbnail_url = uploadedURL + } + ElementPlus.ElMessage.success('上传成功') + } catch (e) { + ElementPlus.ElMessage.error('上传失败: ' + e.message) + } finally { + loadingRef.value = false + } + } + input.click() + } + async function login() { loginLoading.value = true try { @@ -393,13 +473,13 @@ const app = createApp({ return { apiBase, adminToken, authenticated, loginLoading, stats, categories, templates, tplPage, tplPageSize, tplTotal, tplFilterCategory, - catDialogVisible, catForm, catSaving, - tplDialogVisible, tplForm, tplSaving, + catDialogVisible, catForm, catSaving, catIconUploading, + tplDialogVisible, tplForm, tplSaving, tplImageUploading, tplThumbUploading, previewVisible, previewUrl, login, logout, loadTemplates, openCategoryDialog, saveCategory, deleteCategory, openTemplateDialog, saveTemplate, deleteTemplate, - previewImage + previewImage, pickAndUpload } } })