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
}
}
})