|
|
@@ -0,0 +1,372 @@
|
|
|
+<template>
|
|
|
+ <div class="poster-template-manage">
|
|
|
+ <el-card shadow="never">
|
|
|
+ <div slot="header" class="clearfix">
|
|
|
+ <span>活动海报模板管理</span>
|
|
|
+ <el-button type="primary" size="small" style="float: right" @click="handleAdd">新增模板</el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 模板列表 -->
|
|
|
+ <div class="adm-table-wrap">
|
|
|
+ <el-table :data="list" border v-loading="loading" height="100%">
|
|
|
+ <el-table-column prop="name" label="模板名称" width="140">
|
|
|
+ <template slot="header">
|
|
|
+ <span class="sort-header" @click.stop="onSortClick('name', $event)">模板名称<span v-if="sortIndex('name') !== null" class="sort-index" :class="sortClass('name') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('name') }}</span>{{ sortIcon('name') }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="templateKey" label="类型" width="110">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-tag size="small" :type="row.templateKey === 'card' ? 'success' : row.templateKey === 'minimal' ? 'info' : 'warning'">
|
|
|
+ {{ templateKeyLabel(row.templateKey) }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ <template slot="header">
|
|
|
+ <span class="sort-header" @click.stop="onSortClick('templateKey', $event)">类型<span v-if="sortIndex('templateKey') !== null" class="sort-index" :class="sortClass('templateKey') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('templateKey') }}</span>{{ sortIcon('templateKey') }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="配色预览" min-width="220">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <div class="preview-wrap">
|
|
|
+ <div class="preview-bar" :style="{ background: barStyle(row) }"></div>
|
|
|
+ <span class="preview-color" v-for="(c, i) in pick(row, ['mainColor', 'darkColor', 'lightColor'])" :key="i">
|
|
|
+ <i :style="{ background: c }"></i>{{ c }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="qrSize" label="二维码(rpx)" width="110">
|
|
|
+ <template slot="header">
|
|
|
+ <span class="sort-header" @click.stop="onSortClick('qrSize', $event)">二维码(rpx)<span v-if="sortIndex('qrSize') !== null" class="sort-index" :class="sortClass('qrSize') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('qrSize') }}</span>{{ sortIcon('qrSize') }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="sortOrder" label="排序" width="80">
|
|
|
+ <template slot="header">
|
|
|
+ <span class="sort-header" @click.stop="onSortClick('sortOrder', $event)">排序<span v-if="sortIndex('sortOrder') !== null" class="sort-index" :class="sortClass('sortOrder') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('sortOrder') }}</span>{{ sortIcon('sortOrder') }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="status" label="状态" width="90">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
|
|
+ {{ row.status === 1 ? '启用' : '停用' }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ <template slot="header">
|
|
|
+ <span class="sort-header" @click.stop="onSortClick('status', $event)">状态<span v-if="sortIndex('status') !== null" class="sort-index" :class="sortClass('status') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('status') }}</span>{{ sortIcon('status') }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="updatedAt" label="更新时间" width="170">
|
|
|
+ <template slot-scope="{ row }">{{ formatTime(row.updatedAt) }}</template>
|
|
|
+ <template slot="header">
|
|
|
+ <span class="sort-header" @click.stop="onSortClick('updatedAt', $event)">更新时间<span v-if="sortIndex('updatedAt') !== null" class="sort-index" :class="sortClass('updatedAt') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('updatedAt') }}</span>{{ sortIcon('updatedAt') }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="140" fixed="right">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-button size="mini" type="primary" @click="handleEdit(row)">编辑</el-button>
|
|
|
+ <el-button size="mini" type="danger" @click="handleDelete(row)">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="tip">
|
|
|
+ 小程序端「生成海报」弹窗会按排序展示启用中的模板,用户点选后按对应绘制引擎与配色渲染画布;
|
|
|
+ 模板类型必须与小程序绘制引擎匹配,否则点击后无效果。
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 编辑弹窗 -->
|
|
|
+ <el-dialog :title="form.id ? '编辑海报模板' : '新增海报模板'" :visible.sync="dialogVisible" width="640px">
|
|
|
+ <el-form :model="form" label-width="110px">
|
|
|
+ <el-form-item label="模板名称" required>
|
|
|
+ <el-input v-model="form.name" maxlength="50" placeholder="如:暖橙渐变(小程序端展示的名称,需唯一)"></el-input>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="模板类型" required>
|
|
|
+ <el-select v-model="form.templateKey" placeholder="请选择绘制引擎" style="width: 100%">
|
|
|
+ <el-option label="gradient — 渐变背景" value="gradient"></el-option>
|
|
|
+ <el-option label="minimal — 深色极简" value="minimal"></el-option>
|
|
|
+ <el-option label="card — 白底卡片" value="card"></el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="主色">
|
|
|
+ <el-color-picker v-model="form.mainColor"></el-color-picker>
|
|
|
+ <span class="color-text">渐变起点 / 卡片顶栏</span>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="深色">
|
|
|
+ <el-color-picker v-model="form.darkColor"></el-color-picker>
|
|
|
+ <span class="color-text">渐变终点 / 深色模板背景</span>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="浅色">
|
|
|
+ <el-color-picker v-model="form.lightColor"></el-color-picker>
|
|
|
+ <span class="color-text">内容卡背景色</span>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="可选背景色板">
|
|
|
+ <div class="bg-list">
|
|
|
+ <div class="bg-item" v-for="(c, i) in form.bgColorList" :key="i">
|
|
|
+ <el-color-picker v-model="form.bgColorList[i]" size="small"></el-color-picker>
|
|
|
+ <span class="bg-value">{{ form.bgColorList[i] }}</span>
|
|
|
+ <el-button type="text" size="mini" class="bg-remove" @click="removeBgColor(i)">移除</el-button>
|
|
|
+ </div>
|
|
|
+ <el-button size="mini" icon="el-icon-plus" @click="addBgColor">添加颜色</el-button>
|
|
|
+ </div>
|
|
|
+ <div class="form-tip">渐变模板下,用户可在海报弹窗中切换这些背景色</div>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="二维码尺寸">
|
|
|
+ <el-input-number v-model="form.qrSize" :min="60" :max="200" :step="10" size="small"></el-input-number>
|
|
|
+ <span class="color-text">rpx,活动海报建议 100</span>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="排序">
|
|
|
+ <el-input-number v-model="form.sortOrder" :min="0" :max="99" size="small"></el-input-number>
|
|
|
+ <span class="color-text">越小越靠前展示</span>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="状态">
|
|
|
+ <el-switch v-model="form.status" :active-value="1" :inactive-value="0"></el-switch>
|
|
|
+ <span style="margin-left: 10px; color: #999">停用后小程序端不再展示该模板</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div slot="footer">
|
|
|
+ <el-button @click="dialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="handleSave" :loading="saving">保存</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import SortMixin from '@/mixins/SortMixin'
|
|
|
+import {
|
|
|
+ getPosterTemplateList,
|
|
|
+ savePosterTemplate,
|
|
|
+ deletePosterTemplate
|
|
|
+} from '@/api/posterTemplate'
|
|
|
+
|
|
|
+const TEMPLATE_KEY_LABEL = {
|
|
|
+ gradient: '渐变',
|
|
|
+ minimal: '深色',
|
|
|
+ card: '卡片'
|
|
|
+}
|
|
|
+
|
|
|
+export default {
|
|
|
+ mixins: [SortMixin],
|
|
|
+ name: 'PosterTemplateManage',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ list: [],
|
|
|
+ loading: false,
|
|
|
+ saving: false,
|
|
|
+ dialogVisible: false,
|
|
|
+ form: this.emptyForm(),
|
|
|
+ sortableColumns: {
|
|
|
+ id: 'ID',
|
|
|
+ name: '模板名称',
|
|
|
+ templateKey: '类型',
|
|
|
+ qrSize: '二维码(rpx)',
|
|
|
+ sortOrder: '排序',
|
|
|
+ status: '状态',
|
|
|
+ updatedAt: '更新时间'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ created() {
|
|
|
+ this.loadData()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ emptyForm() {
|
|
|
+ return {
|
|
|
+ id: null,
|
|
|
+ name: '',
|
|
|
+ templateKey: 'gradient',
|
|
|
+ mainColor: '#F97316',
|
|
|
+ darkColor: '#EA580C',
|
|
|
+ lightColor: '#FFF7ED',
|
|
|
+ bgColorList: ['#F97316', '#EA580C'],
|
|
|
+ qrSize: 100,
|
|
|
+ sortOrder: 0,
|
|
|
+ status: 1
|
|
|
+ }
|
|
|
+ },
|
|
|
+ formatTime(t) {
|
|
|
+ return t ? t.replace('T', ' ').substring(0, 19) : '-'
|
|
|
+ },
|
|
|
+ templateKeyLabel(key) {
|
|
|
+ return TEMPLATE_KEY_LABEL[key] || key
|
|
|
+ },
|
|
|
+ barStyle(row) {
|
|
|
+ if (row.templateKey === 'minimal') {
|
|
|
+ return 'linear-gradient(135deg, ' + row.mainColor + ', ' + row.darkColor + ')'
|
|
|
+ }
|
|
|
+ return 'linear-gradient(135deg, ' + row.mainColor + ', ' + row.darkColor + ')'
|
|
|
+ },
|
|
|
+ pick(row, keys) {
|
|
|
+ return keys.map(k => row[k] || '')
|
|
|
+ },
|
|
|
+ addBgColor() {
|
|
|
+ this.form.bgColorList.push('#F97316')
|
|
|
+ },
|
|
|
+ removeBgColor(index) {
|
|
|
+ this.form.bgColorList.splice(index, 1)
|
|
|
+ },
|
|
|
+ async loadData() {
|
|
|
+ this.loading = true
|
|
|
+ try {
|
|
|
+ const params = {}
|
|
|
+ if (this.sortState.length > 0) params.sort = this.sortState
|
|
|
+ const res = await getPosterTemplateList(params)
|
|
|
+ if (res.code === 0 || res.code === 200) {
|
|
|
+ this.list = res.data || []
|
|
|
+ } else {
|
|
|
+ this.$message.error(res.message || '加载失败')
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ this.loading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleAdd() {
|
|
|
+ this.form = this.emptyForm()
|
|
|
+ this.dialogVisible = true
|
|
|
+ },
|
|
|
+ handleEdit(row) {
|
|
|
+ let bgColorList = []
|
|
|
+ try {
|
|
|
+ const parsed = JSON.parse(row.bgColors || '[]')
|
|
|
+ bgColorList = Array.isArray(parsed) ? parsed.filter(Boolean) : []
|
|
|
+ } catch (e) {
|
|
|
+ bgColorList = []
|
|
|
+ }
|
|
|
+ if (bgColorList.length === 0) bgColorList = [row.mainColor || '#F97316']
|
|
|
+ this.form = {
|
|
|
+ id: row.id,
|
|
|
+ name: row.name,
|
|
|
+ templateKey: row.templateKey,
|
|
|
+ mainColor: row.mainColor || '#F97316',
|
|
|
+ darkColor: row.darkColor || '#EA580C',
|
|
|
+ lightColor: row.lightColor || '#FFF7ED',
|
|
|
+ bgColorList,
|
|
|
+ qrSize: row.qrSize || 100,
|
|
|
+ sortOrder: row.sortOrder || 0,
|
|
|
+ status: row.status === 0 ? 0 : 1
|
|
|
+ }
|
|
|
+ this.dialogVisible = true
|
|
|
+ },
|
|
|
+ async handleSave() {
|
|
|
+ if (!this.form.name || !this.form.name.trim()) {
|
|
|
+ this.$message.warning('请填写模板名称')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!this.form.templateKey) {
|
|
|
+ this.$message.warning('请选择模板类型')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.saving = true
|
|
|
+ try {
|
|
|
+ const data = {
|
|
|
+ ...this.form,
|
|
|
+ bgColors: JSON.stringify((this.form.bgColorList || []).filter(Boolean))
|
|
|
+ }
|
|
|
+ const res = await savePosterTemplate(data)
|
|
|
+ if (res.code === 0 || res.code === 200) {
|
|
|
+ this.$message.success('保存成功')
|
|
|
+ this.dialogVisible = false
|
|
|
+ this.loadData()
|
|
|
+ } else {
|
|
|
+ this.$message.error(res.message || '保存失败')
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ this.saving = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleDelete(row) {
|
|
|
+ this.$confirm(`确定删除模板「${row.name}」吗?删除后小程序端将不再展示该模板。`, '提示', { type: 'warning' }).then(async () => {
|
|
|
+ const res = await deletePosterTemplate({ id: row.id })
|
|
|
+ if (res.code === 0 || res.code === 200) {
|
|
|
+ this.$message.success('删除成功')
|
|
|
+ this.loadData()
|
|
|
+ } else {
|
|
|
+ this.$message.error(res.message || '删除失败')
|
|
|
+ }
|
|
|
+ }).catch(() => {})
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.poster-template-manage {
|
|
|
+ padding: 16px;
|
|
|
+}
|
|
|
+.sort-header {
|
|
|
+ cursor: pointer;
|
|
|
+ user-select: none;
|
|
|
+ font-weight: 500;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+.sort-header:hover {
|
|
|
+ color: #409eff;
|
|
|
+}
|
|
|
+.sort-tag {
|
|
|
+ margin-left: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+.tip {
|
|
|
+ margin-top: 12px;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.6;
|
|
|
+}
|
|
|
+.preview-wrap {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+.preview-bar {
|
|
|
+ width: 120px;
|
|
|
+ height: 28px;
|
|
|
+ border-radius: 4px;
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+}
|
|
|
+.preview-color {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 11px;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+.preview-color i {
|
|
|
+ display: inline-block;
|
|
|
+ width: 14px;
|
|
|
+ height: 14px;
|
|
|
+ border-radius: 2px;
|
|
|
+ margin-right: 4px;
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+}
|
|
|
+.bg-list {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+.bg-item {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+.bg-value {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+.bg-remove {
|
|
|
+ color: #f56c6c;
|
|
|
+ padding: 0;
|
|
|
+}
|
|
|
+.color-text {
|
|
|
+ margin-left: 10px;
|
|
|
+ color: #999;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+.form-tip {
|
|
|
+ color: #999;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.6;
|
|
|
+}
|
|
|
+</style>
|