feat: 完成 #40 营销图后台七牛直传与页面上传能力
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
@@ -46,5 +46,6 @@ func registerMarketingRoutes(
|
||||
admin.DELETE("/templates/:id", templateHandler.AdminDelete)
|
||||
|
||||
admin.GET("/stats", downloadHandler.AdminStats)
|
||||
admin.POST("/upload/qiniu/token", downloadHandler.AdminQiniuToken)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user