feat: move marketing admin to /api/admin and remove built-in page
This commit is contained in:
@@ -4,9 +4,16 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
adminhandler "wx_service/internal/admin"
|
||||
marketinghandler "wx_service/internal/marketing/handler"
|
||||
)
|
||||
|
||||
func registerAdminRoutes(router *gin.Engine, handler *adminhandler.Handler) {
|
||||
func registerAdminRoutes(
|
||||
router *gin.Engine,
|
||||
handler *adminhandler.Handler,
|
||||
categoryHandler *marketinghandler.CategoryHandler,
|
||||
templateHandler *marketinghandler.TemplateHandler,
|
||||
downloadHandler *marketinghandler.DownloadHandler,
|
||||
) {
|
||||
if handler == nil {
|
||||
return
|
||||
}
|
||||
@@ -39,6 +46,24 @@ func registerAdminRoutes(router *gin.Engine, handler *adminhandler.Handler) {
|
||||
protected.GET("/memberships/redeem-codes", handler.ListMembershipRedeemCodes)
|
||||
protected.POST("/memberships/redeem-codes", handler.CreateMembershipRedeemCodes)
|
||||
protected.POST("/memberships/redeem-codes/:id/status", handler.UpdateMembershipRedeemCodeStatus)
|
||||
|
||||
if categoryHandler != nil && templateHandler != nil && downloadHandler != nil {
|
||||
marketing := protected.Group("/marketing")
|
||||
{
|
||||
marketing.GET("/categories", categoryHandler.AdminList)
|
||||
marketing.POST("/categories", categoryHandler.AdminCreate)
|
||||
marketing.PUT("/categories/:id", categoryHandler.AdminUpdate)
|
||||
marketing.DELETE("/categories/:id", categoryHandler.AdminDelete)
|
||||
|
||||
marketing.GET("/templates", templateHandler.AdminList)
|
||||
marketing.POST("/templates", templateHandler.AdminCreate)
|
||||
marketing.PUT("/templates/:id", templateHandler.AdminUpdate)
|
||||
marketing.DELETE("/templates/:id", templateHandler.AdminDelete)
|
||||
|
||||
marketing.GET("/stats", downloadHandler.AdminStats)
|
||||
marketing.POST("/upload/qiniu/token", downloadHandler.AdminQiniuToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package routes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -22,29 +20,6 @@ import (
|
||||
smokehandler "wx_service/internal/smoke/handler"
|
||||
)
|
||||
|
||||
func resolveMarketingPage() string {
|
||||
candidates := []string{
|
||||
filepath.Join("web", "marketing", "index.html"),
|
||||
filepath.Join("..", "web", "marketing", "index.html"),
|
||||
}
|
||||
|
||||
if executable, err := os.Executable(); err == nil {
|
||||
exeDir := filepath.Dir(executable)
|
||||
candidates = append(candidates,
|
||||
filepath.Join(exeDir, "web", "marketing", "index.html"),
|
||||
filepath.Join(exeDir, "..", "web", "marketing", "index.html"),
|
||||
)
|
||||
}
|
||||
|
||||
for _, path := range candidates {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
return filepath.Join("web", "marketing", "index.html")
|
||||
}
|
||||
|
||||
func Register(
|
||||
router *gin.Engine,
|
||||
db *gorm.DB,
|
||||
@@ -93,7 +68,7 @@ func Register(
|
||||
registerMarketingRoutes(api, protected, adminToken, marketingCategoryHandler, marketingTemplateHandler, marketingDownloadHandler)
|
||||
}
|
||||
|
||||
registerAdminRoutes(router, adminHandler)
|
||||
registerAdminRoutes(router, adminHandler, marketingCategoryHandler, marketingTemplateHandler, marketingDownloadHandler)
|
||||
|
||||
// 保质期提醒模块使用独立前缀 /api/expiry,与现有 /api/v1 并存。
|
||||
expiryAPI := router.Group("/api/expiry")
|
||||
@@ -108,11 +83,6 @@ func Register(
|
||||
}
|
||||
}
|
||||
|
||||
// Web 管理后台静态文件
|
||||
marketingPage := resolveMarketingPage()
|
||||
router.StaticFile("/admin/marketing", marketingPage)
|
||||
router.StaticFile("/admin/marketing/", marketingPage)
|
||||
|
||||
// 健康检查:用于容器/负载均衡探活
|
||||
router.GET("/healthz", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
|
||||
@@ -1,490 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>营销图管理后台</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-plus/dist/index.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/element-plus/dist/index.full.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/element-plus/dist/locale/zh-cn.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f0f2f5; }
|
||||
.login-wrapper { display: flex; justify-content: center; align-items: center; min-height: 100vh; }
|
||||
.login-card { width: 400px; }
|
||||
.app-header { background: #fff; padding: 16px 24px; border-bottom: 1px solid #e8e8e8; display: flex; justify-content: space-between; align-items: center; }
|
||||
.app-header h2 { font-size: 18px; color: #1a1a1a; }
|
||||
.app-body { padding: 24px; max-width: 1200px; margin: 0 auto; }
|
||||
.stats-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; }
|
||||
.stat-card { background: #fff; border-radius: 8px; padding: 20px; text-align: center; }
|
||||
.stat-card .num { font-size: 28px; font-weight: 700; color: #409eff; }
|
||||
.stat-card .label { font-size: 13px; color: #999; margin-top: 4px; }
|
||||
.section-card { background: #fff; border-radius: 8px; padding: 20px; margin-bottom: 24px; }
|
||||
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||||
.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>
|
||||
<div id="app">
|
||||
<!-- Login -->
|
||||
<div v-if="!authenticated" class="login-wrapper">
|
||||
<el-card class="login-card">
|
||||
<template #header><h3>营销图管理后台</h3></template>
|
||||
<el-form @submit.prevent="login">
|
||||
<el-form-item label="API 地址">
|
||||
<el-input v-model="apiBase" placeholder="http://localhost:8080"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="管理员口令">
|
||||
<el-input v-model="adminToken" type="password" placeholder="X-Admin-Token" @keyup.enter="login"></el-input>
|
||||
</el-form-item>
|
||||
<el-button type="primary" @click="login" :loading="loginLoading" style="width:100%">登录</el-button>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- Main -->
|
||||
<div v-else>
|
||||
<div class="app-header">
|
||||
<h2>营销图管理后台</h2>
|
||||
<el-button text @click="logout">退出</el-button>
|
||||
</div>
|
||||
<div class="app-body">
|
||||
<!-- Stats -->
|
||||
<div class="stats-row">
|
||||
<div class="stat-card">
|
||||
<div class="num">{{ stats.categoryCount }}</div>
|
||||
<div class="label">分类总数</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="num">{{ stats.templateCount }}</div>
|
||||
<div class="label">模板总数</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="num">{{ stats.totalDownloads }}</div>
|
||||
<div class="label">总下载次数</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="num">{{ stats.todayDownloads }}</div>
|
||||
<div class="label">今日下载</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Categories -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<h3>分类管理</h3>
|
||||
<el-button type="primary" size="small" @click="openCategoryDialog()">新增分类</el-button>
|
||||
</div>
|
||||
<el-table :data="categories" stripe>
|
||||
<el-table-column prop="id" label="ID" width="60"></el-table-column>
|
||||
<el-table-column label="图标" width="60">
|
||||
<template #default="{row}">
|
||||
<img v-if="row.icon" :src="row.icon" class="icon-preview">
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="名称"></el-table-column>
|
||||
<el-table-column prop="sort_order" label="排序" width="80"></el-table-column>
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{row}">
|
||||
<el-tag :type="row.status===1?'success':'info'" size="small">{{ row.status===1?'启用':'禁用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160">
|
||||
<template #default="{row}">
|
||||
<el-button text type="primary" size="small" @click="openCategoryDialog(row)">编辑</el-button>
|
||||
<el-popconfirm title="确定删除?" @confirm="deleteCategory(row.id)">
|
||||
<template #reference><el-button text type="danger" size="small">删除</el-button></template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- Templates -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<h3>模板管理</h3>
|
||||
<div>
|
||||
<el-select v-model="tplFilterCategory" placeholder="全部分类" clearable size="small" style="width:140px;margin-right:8px" @change="loadTemplates">
|
||||
<el-option v-for="c in categories" :key="c.id" :label="c.name" :value="c.id"></el-option>
|
||||
</el-select>
|
||||
<el-button type="primary" size="small" @click="openTemplateDialog()">新增模板</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="templates" stripe>
|
||||
<el-table-column prop="id" label="ID" width="60"></el-table-column>
|
||||
<el-table-column label="缩略图" width="100">
|
||||
<template #default="{row}">
|
||||
<img :src="row.thumbnail_url || row.image_url" class="tpl-thumb" @click="previewImage(row.image_url)">
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="名称"></el-table-column>
|
||||
<el-table-column label="分类" width="100">
|
||||
<template #default="{row}">{{ row.category ? row.category.name : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort_order" label="排序" width="70"></el-table-column>
|
||||
<el-table-column prop="download_count" label="下载" width="70"></el-table-column>
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{row}">
|
||||
<el-tag :type="row.status===1?'success':'info'" size="small">{{ row.status===1?'启用':'禁用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160">
|
||||
<template #default="{row}">
|
||||
<el-button text type="primary" size="small" @click="openTemplateDialog(row)">编辑</el-button>
|
||||
<el-popconfirm title="确定删除?" @confirm="deleteTemplate(row.id)">
|
||||
<template #reference><el-button text type="danger" size="small">删除</el-button></template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="margin-top:16px;text-align:right">
|
||||
<el-pagination
|
||||
v-model:current-page="tplPage"
|
||||
:page-size="tplPageSize"
|
||||
:total="tplTotal"
|
||||
layout="prev, pager, next"
|
||||
@current-change="loadTemplates"
|
||||
></el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Dialog -->
|
||||
<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">
|
||||
<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>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="catDialogVisible=false">取消</el-button>
|
||||
<el-button type="primary" @click="saveCategory" :loading="catSaving">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Template Dialog -->
|
||||
<el-dialog v-model="tplDialogVisible" :title="tplForm.id?'编辑模板':'新增模板'" width="540px">
|
||||
<el-form :model="tplForm" label-width="80px">
|
||||
<el-form-item label="名称"><el-input v-model="tplForm.title"></el-input></el-form-item>
|
||||
<el-form-item label="分类">
|
||||
<el-select v-model="tplForm.category_id" placeholder="选择分类">
|
||||
<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">
|
||||
<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>
|
||||
<el-form-item label="状态">
|
||||
<el-switch v-model="tplForm.statusBool" active-text="启用" inactive-text="禁用"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="tplForm.image_url" label="预览">
|
||||
<img :src="tplForm.image_url" style="max-width:100%;max-height:200px;border-radius:4px;border:1px solid #eee">
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="tplDialogVisible=false">取消</el-button>
|
||||
<el-button type="primary" @click="saveTemplate" :loading="tplSaving">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Image Preview -->
|
||||
<el-image-viewer v-if="previewVisible" :url-list="[previewUrl]" @close="previewVisible=false"></el-image-viewer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { createApp, ref, reactive, onMounted } = Vue
|
||||
|
||||
const app = createApp({
|
||||
setup() {
|
||||
const apiBase = ref(localStorage.getItem('mkt_api_base') || location.origin)
|
||||
const adminToken = ref(localStorage.getItem('mkt_admin_token') || '')
|
||||
const authenticated = ref(false)
|
||||
const loginLoading = ref(false)
|
||||
|
||||
const stats = reactive({ categoryCount: 0, templateCount: 0, totalDownloads: 0, todayDownloads: 0 })
|
||||
const categories = ref([])
|
||||
const templates = ref([])
|
||||
const tplPage = ref(1)
|
||||
const tplPageSize = ref(20)
|
||||
const tplTotal = ref(0)
|
||||
const tplFilterCategory = ref(null)
|
||||
|
||||
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('')
|
||||
|
||||
async function api(method, path, body) {
|
||||
const url = apiBase.value.replace(/\/$/, '') + '/api/v1' + path
|
||||
const opts = { method, headers: { 'Content-Type': 'application/json', 'X-Admin-Token': adminToken.value } }
|
||||
if (body) opts.body = JSON.stringify(body)
|
||||
const res = await fetch(url, opts)
|
||||
const data = await res.json()
|
||||
if (data.code && data.code !== 200) throw new Error(data.message || '请求失败')
|
||||
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 {
|
||||
await api('GET', '/admin/marketing/categories')
|
||||
authenticated.value = true
|
||||
localStorage.setItem('mkt_api_base', apiBase.value)
|
||||
localStorage.setItem('mkt_admin_token', adminToken.value)
|
||||
loadAll()
|
||||
} catch (e) {
|
||||
ElementPlus.ElMessage.error('登录失败: ' + e.message)
|
||||
} finally {
|
||||
loginLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
authenticated.value = false
|
||||
localStorage.removeItem('mkt_admin_token')
|
||||
}
|
||||
|
||||
async function loadAll() {
|
||||
await Promise.all([loadCategories(), loadTemplates(), loadStats()])
|
||||
}
|
||||
|
||||
async function loadCategories() {
|
||||
try {
|
||||
categories.value = await api('GET', '/admin/marketing/categories') || []
|
||||
stats.categoryCount = categories.value.length
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
async function loadTemplates() {
|
||||
try {
|
||||
let path = `/admin/marketing/templates?page=${tplPage.value}&page_size=${tplPageSize.value}`
|
||||
if (tplFilterCategory.value) path += `&category_id=${tplFilterCategory.value}`
|
||||
const data = await api('GET', path)
|
||||
templates.value = data.templates || []
|
||||
tplTotal.value = data.total || 0
|
||||
stats.templateCount = data.total || 0
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const data = await api('GET', '/admin/marketing/stats')
|
||||
if (data) {
|
||||
stats.totalDownloads = data.TotalDownloads || 0
|
||||
stats.todayDownloads = data.TodayDownloads || 0
|
||||
}
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
function openCategoryDialog(row) {
|
||||
if (row) {
|
||||
Object.assign(catForm, { id: row.id, name: row.name, icon: row.icon, sort_order: row.sort_order, statusBool: row.status === 1 })
|
||||
} else {
|
||||
Object.assign(catForm, { id: null, name: '', icon: '', sort_order: 0, statusBool: true })
|
||||
}
|
||||
catDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function saveCategory() {
|
||||
catSaving.value = true
|
||||
try {
|
||||
const body = { name: catForm.name, icon: catForm.icon, sort_order: catForm.sort_order, status: catForm.statusBool ? 1 : 0 }
|
||||
if (catForm.id) {
|
||||
await api('PUT', `/admin/marketing/categories/${catForm.id}`, body)
|
||||
} else {
|
||||
await api('POST', '/admin/marketing/categories', body)
|
||||
}
|
||||
catDialogVisible.value = false
|
||||
ElementPlus.ElMessage.success('保存成功')
|
||||
await loadCategories()
|
||||
} catch (e) {
|
||||
ElementPlus.ElMessage.error(e.message)
|
||||
} finally {
|
||||
catSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCategory(id) {
|
||||
try {
|
||||
await api('DELETE', `/admin/marketing/categories/${id}`)
|
||||
ElementPlus.ElMessage.success('删除成功')
|
||||
await loadCategories()
|
||||
} catch (e) {
|
||||
ElementPlus.ElMessage.error(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
function openTemplateDialog(row) {
|
||||
if (row) {
|
||||
Object.assign(tplForm, {
|
||||
id: row.id, title: row.title, category_id: row.category_id,
|
||||
image_url: row.image_url, thumbnail_url: row.thumbnail_url,
|
||||
width: row.width, height: row.height, sort_order: row.sort_order,
|
||||
statusBool: row.status === 1
|
||||
})
|
||||
} else {
|
||||
Object.assign(tplForm, { id: null, title: '', category_id: null, image_url: '', thumbnail_url: '', width: 0, height: 0, sort_order: 0, statusBool: true })
|
||||
}
|
||||
tplDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function saveTemplate() {
|
||||
tplSaving.value = true
|
||||
try {
|
||||
const body = {
|
||||
title: tplForm.title, category_id: tplForm.category_id,
|
||||
image_url: tplForm.image_url, thumbnail_url: tplForm.thumbnail_url,
|
||||
width: tplForm.width, height: tplForm.height,
|
||||
sort_order: tplForm.sort_order, status: tplForm.statusBool ? 1 : 0
|
||||
}
|
||||
if (tplForm.id) {
|
||||
await api('PUT', `/admin/marketing/templates/${tplForm.id}`, body)
|
||||
} else {
|
||||
await api('POST', '/admin/marketing/templates', body)
|
||||
}
|
||||
tplDialogVisible.value = false
|
||||
ElementPlus.ElMessage.success('保存成功')
|
||||
await loadTemplates()
|
||||
} catch (e) {
|
||||
ElementPlus.ElMessage.error(e.message)
|
||||
} finally {
|
||||
tplSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTemplate(id) {
|
||||
try {
|
||||
await api('DELETE', `/admin/marketing/templates/${id}`)
|
||||
ElementPlus.ElMessage.success('删除成功')
|
||||
await loadTemplates()
|
||||
} catch (e) {
|
||||
ElementPlus.ElMessage.error(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
function previewImage(url) {
|
||||
previewUrl.value = url
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (adminToken.value) {
|
||||
login()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
apiBase, adminToken, authenticated, loginLoading, stats,
|
||||
categories, templates, tplPage, tplPageSize, tplTotal, tplFilterCategory,
|
||||
catDialogVisible, catForm, catSaving, catIconUploading,
|
||||
tplDialogVisible, tplForm, tplSaving, tplImageUploading, tplThumbUploading,
|
||||
previewVisible, previewUrl,
|
||||
login, logout, loadTemplates,
|
||||
openCategoryDialog, saveCategory, deleteCategory,
|
||||
openTemplateDialog, saveTemplate, deleteTemplate,
|
||||
previewImage, pickAndUpload
|
||||
}
|
||||
}
|
||||
})
|
||||
app.use(ElementPlus, { locale: ElementPlusLocaleZhCn })
|
||||
app.mount('#app')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user