feat: 完成 #40 营销图后台七牛直传与页面上传能力
This commit is contained in:
@@ -1,10 +1,13 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"wx_service/config"
|
||||||
|
qiniuservice "wx_service/internal/common/qiniu/service"
|
||||||
"wx_service/internal/marketing/service"
|
"wx_service/internal/marketing/service"
|
||||||
"wx_service/internal/middleware"
|
"wx_service/internal/middleware"
|
||||||
"wx_service/internal/model"
|
"wx_service/internal/model"
|
||||||
@@ -89,3 +92,25 @@ func (h *DownloadHandler) AdminStats(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, model.Success(stats))
|
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.DELETE("/templates/:id", templateHandler.AdminDelete)
|
||||||
|
|
||||||
admin.GET("/stats", downloadHandler.AdminStats)
|
admin.GET("/stats", downloadHandler.AdminStats)
|
||||||
|
admin.POST("/upload/qiniu/token", downloadHandler.AdminQiniuToken)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@
|
|||||||
.section-header h3 { font-size: 16px; color: #1a1a1a; }
|
.section-header h3 { font-size: 16px; color: #1a1a1a; }
|
||||||
.tpl-thumb { width: 80px; height: 80px; object-fit: cover; border-radius: 4px; border: 1px solid #eee; }
|
.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; }
|
.icon-preview { width: 32px; height: 32px; object-fit: contain; }
|
||||||
|
.upload-inline { display: flex; gap: 8px; width: 100%; }
|
||||||
|
.upload-inline .el-input { flex: 1; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -158,7 +160,12 @@
|
|||||||
<el-dialog v-model="catDialogVisible" :title="catForm.id?'编辑分类':'新增分类'" width="460px">
|
<el-dialog v-model="catDialogVisible" :title="catForm.id?'编辑分类':'新增分类'" width="460px">
|
||||||
<el-form :model="catForm" label-width="80px">
|
<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="名称"><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-input-number v-model="catForm.sort_order" :min="0"></el-input-number></el-form-item>
|
||||||
<el-form-item label="状态">
|
<el-form-item label="状态">
|
||||||
<el-switch v-model="catForm.statusBool" active-text="启用" inactive-text="禁用"></el-switch>
|
<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-option v-for="c in categories" :key="c.id" :label="c.name" :value="c.id"></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</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-form-item label="缩略图URL"><el-input v-model="tplForm.thumbnail_url" placeholder="可选,缩略图地址"></el-input></el-form-item>
|
<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.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="高度(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>
|
<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 catDialogVisible = ref(false)
|
||||||
const catForm = reactive({ id: null, name: '', icon: '', sort_order: 0, statusBool: true })
|
const catForm = reactive({ id: null, name: '', icon: '', sort_order: 0, statusBool: true })
|
||||||
const catSaving = ref(false)
|
const catSaving = ref(false)
|
||||||
|
const catIconUploading = ref(false)
|
||||||
|
|
||||||
const tplDialogVisible = 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 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 tplSaving = ref(false)
|
||||||
|
const tplImageUploading = ref(false)
|
||||||
|
const tplThumbUploading = ref(false)
|
||||||
|
|
||||||
const previewVisible = ref(false)
|
const previewVisible = ref(false)
|
||||||
const previewUrl = ref('')
|
const previewUrl = ref('')
|
||||||
@@ -241,6 +261,66 @@ const app = createApp({
|
|||||||
return data.data
|
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() {
|
async function login() {
|
||||||
loginLoading.value = true
|
loginLoading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -393,13 +473,13 @@ const app = createApp({
|
|||||||
return {
|
return {
|
||||||
apiBase, adminToken, authenticated, loginLoading, stats,
|
apiBase, adminToken, authenticated, loginLoading, stats,
|
||||||
categories, templates, tplPage, tplPageSize, tplTotal, tplFilterCategory,
|
categories, templates, tplPage, tplPageSize, tplTotal, tplFilterCategory,
|
||||||
catDialogVisible, catForm, catSaving,
|
catDialogVisible, catForm, catSaving, catIconUploading,
|
||||||
tplDialogVisible, tplForm, tplSaving,
|
tplDialogVisible, tplForm, tplSaving, tplImageUploading, tplThumbUploading,
|
||||||
previewVisible, previewUrl,
|
previewVisible, previewUrl,
|
||||||
login, logout, loadTemplates,
|
login, logout, loadTemplates,
|
||||||
openCategoryDialog, saveCategory, deleteCategory,
|
openCategoryDialog, saveCategory, deleteCategory,
|
||||||
openTemplateDialog, saveTemplate, deleteTemplate,
|
openTemplateDialog, saveTemplate, deleteTemplate,
|
||||||
previewImage
|
previewImage, pickAndUpload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user