feat: 完成 #40 营销图后台七牛直传与页面上传能力

This commit is contained in:
root
2026-03-09 19:17:25 +08:00
parent 88d02ed6db
commit 9daf5e98ff
3 changed files with 112 additions and 6 deletions
@@ -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))
}
+1
View File
@@ -46,5 +46,6 @@ func registerMarketingRoutes(
admin.DELETE("/templates/:id", templateHandler.AdminDelete)
admin.GET("/stats", downloadHandler.AdminStats)
admin.POST("/upload/qiniu/token", downloadHandler.AdminQiniuToken)
}
}
+86 -6
View File
@@ -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; }
</style>
</head>
<body>
@@ -158,7 +160,12 @@
<el-dialog v-model="catDialogVisible" :title="catForm.id?'编辑分类':'新增分类'" width="460px">
<el-form :model="catForm" label-width="80px">
<el-form-item label="名称"><el-input v-model="catForm.name"></el-input></el-form-item>
<el-form-item label="图标URL"><el-input v-model="catForm.icon" placeholder="可选,图标地址"></el-input></el-form-item>
<el-form-item label="图标URL">
<div class="upload-inline">
<el-input v-model="catForm.icon" placeholder="可选,图标地址"></el-input>
<el-button :loading="catIconUploading" @click="pickAndUpload('category_icon')">上传</el-button>
</div>
</el-form-item>
<el-form-item label="排序"><el-input-number v-model="catForm.sort_order" :min="0"></el-input-number></el-form-item>
<el-form-item label="状态">
<el-switch v-model="catForm.statusBool" active-text="启用" inactive-text="禁用"></el-switch>
@@ -179,8 +186,18 @@
<el-option v-for="c in categories" :key="c.id" :label="c.name" :value="c.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="图片URL"><el-input v-model="tplForm.image_url" placeholder="模板图片地址"></el-input></el-form-item>
<el-form-item label="缩略图URL"><el-input v-model="tplForm.thumbnail_url" placeholder="可选,缩略图地址"></el-input></el-form-item>
<el-form-item label="图片URL">
<div class="upload-inline">
<el-input v-model="tplForm.image_url" placeholder="模板图片地址"></el-input>
<el-button :loading="tplImageUploading" @click="pickAndUpload('template_image')">上传</el-button>
</div>
</el-form-item>
<el-form-item label="缩略图URL">
<div class="upload-inline">
<el-input v-model="tplForm.thumbnail_url" placeholder="可选,缩略图地址"></el-input>
<el-button :loading="tplThumbUploading" @click="pickAndUpload('template_thumb')">上传</el-button>
</div>
</el-form-item>
<el-form-item label="宽度(px)"><el-input-number v-model="tplForm.width" :min="0"></el-input-number></el-form-item>
<el-form-item label="高度(px)"><el-input-number v-model="tplForm.height" :min="0"></el-input-number></el-form-item>
<el-form-item label="排序"><el-input-number v-model="tplForm.sort_order" :min="0"></el-input-number></el-form-item>
@@ -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
}
}
})