feat(admin): 订单管理重构、设置管理增强、短信签名模板管理及通知渠道优化
- 订单列表重构为卡片式布局并新增筛选功能 - 设置管理支持struct/struct_list类型配置 - 新增短信签名和模板独立管理页面 - 通知渠道新增短信渠道配置 - 产品参数管理优化 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -193,43 +193,75 @@
|
||||
</el-tabs>
|
||||
|
||||
<!-- ==================== 模板编辑弹窗 ==================== -->
|
||||
<el-dialog v-model="tplDialogVisible" :title="tplForm.id ? '编辑模板' : '新增模板'" width="640px" append-to-body destroy-on-close>
|
||||
<el-form :model="tplForm" :rules="tplRules" ref="tplFormRef" label-width="90px" label-position="right">
|
||||
<el-form-item label="模板名称" prop="name">
|
||||
<el-input v-model="tplForm.name" placeholder="例:用户注册通知" maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="模板标识" prop="tag">
|
||||
<el-input v-model="tplForm.tag" placeholder="例:user_register_notify" maxlength="100" :disabled="!!tplForm.id" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模板类型" prop="type">
|
||||
<el-radio-group v-model="tplForm.type">
|
||||
<el-radio value="email">
|
||||
<span class="radio-with-icon">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z" fill="#e6a23c"/></svg>
|
||||
邮件
|
||||
</span>
|
||||
</el-radio>
|
||||
<el-radio value="phone">
|
||||
<span class="radio-with-icon">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H5.17L4 17.17V4h16v12z" fill="#67c23a"/></svg>
|
||||
短信
|
||||
</span>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="可用参数" v-if="tplFormArgs.length > 0">
|
||||
<div class="args-btn-group">
|
||||
<el-button v-for="arg in tplFormArgs" :key="arg" size="small" @click="insertArg(arg)" class="arg-insert-btn">
|
||||
<svg viewBox="0 0 24 24" width="12" height="12" style="margin-right:3px"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor"/></svg>
|
||||
{{ arg }}
|
||||
<el-dialog v-model="tplDialogVisible" :title="tplForm.id ? '编辑模板' : '新增模板'" width="1060px" append-to-body destroy-on-close @opened="onTplDialogOpened">
|
||||
<div class="tpl-dialog-body">
|
||||
<!-- 左侧:表单 -->
|
||||
<div class="tpl-dialog-left">
|
||||
<el-form :model="tplForm" :rules="tplRules" ref="tplFormRef" label-width="90px" label-position="right">
|
||||
<el-form-item label="模板名称" prop="name">
|
||||
<el-input v-model="tplForm.name" placeholder="例:用户注册通知" maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="模板标识" prop="tag">
|
||||
<el-input v-model="tplForm.tag" placeholder="例:user_register_notify" maxlength="100" :disabled="!!tplForm.id" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模板类型" prop="type">
|
||||
<el-radio-group v-model="tplForm.type">
|
||||
<el-radio value="email">
|
||||
<span class="radio-with-icon">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z" fill="#e6a23c"/></svg>
|
||||
邮件
|
||||
</span>
|
||||
</el-radio>
|
||||
<el-radio value="phone">
|
||||
<span class="radio-with-icon">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H5.17L4 17.17V4h16v12z" fill="#67c23a"/></svg>
|
||||
短信
|
||||
</span>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="可用参数" v-if="tplFormArgs.length > 0">
|
||||
<div class="args-btn-group">
|
||||
<el-button v-for="arg in tplFormArgs" :key="arg" size="small" @click="insertArg(arg)" class="arg-insert-btn">
|
||||
<svg viewBox="0 0 24 24" width="12" height="12" style="margin-right:3px"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor"/></svg>
|
||||
{{ arg }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="form-hint">点击参数按钮可将其插入到模板内容光标处</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="模板内容" prop="content">
|
||||
<el-input ref="contentInputRef" v-model="tplForm.content" type="textarea" :rows="8" placeholder="模板内容,点击上方参数按钮插入变量" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:渲染预览 -->
|
||||
<div class="tpl-dialog-right">
|
||||
<div class="preview-header">
|
||||
<span class="preview-title">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" fill="currentColor"/></svg>
|
||||
渲染预览
|
||||
</span>
|
||||
<el-button size="small" link type="primary" @click="fetchPreview" :loading="previewLoading">
|
||||
<el-icon><Refresh /></el-icon> 刷新
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="form-hint">点击参数按钮可将其插入到模板内容光标处</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="模板内容" prop="content">
|
||||
<el-input ref="contentInputRef" v-model="tplForm.content" type="textarea" :rows="6" placeholder="模板内容,点击上方参数按钮插入变量" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="preview-body" v-loading="previewLoading">
|
||||
<template v-if="previewData.rendered">
|
||||
<div class="preview-rendered" :class="{ 'html-mode': tplForm.type === 'email' }" v-html="previewRenderedHtml"></div>
|
||||
</template>
|
||||
<div v-else-if="previewError" class="preview-empty">
|
||||
<el-icon :size="32" color="#f56c6c"><WarningFilled /></el-icon>
|
||||
<p>{{ previewError }}</p>
|
||||
</div>
|
||||
<div v-else class="preview-empty">
|
||||
<el-icon :size="32" color="#c0c4cc"><View /></el-icon>
|
||||
<p>填写模板标识和类型后自动加载预览</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="tplDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="tplSubmitting" @click="handleSubmitTpl">确定</el-button>
|
||||
@@ -239,12 +271,13 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||
import { ref, computed, onMounted, nextTick, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { Plus, Refresh, View, WarningFilled } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getNoticeChannelList, updateNoticeChannel,
|
||||
getNoticeTemplateList, addNoticeTemplate, updateNoticeTemplate, deleteNoticeTemplate
|
||||
getNoticeTemplateList, addNoticeTemplate, updateNoticeTemplate, deleteNoticeTemplate,
|
||||
previewNoticeTemplate
|
||||
} from '@/api/admin/noticeChannel'
|
||||
|
||||
// ==================== 渠道配置 ====================
|
||||
@@ -410,6 +443,70 @@ const handleDeleteTpl = async (row) => {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// ==================== 渲染预览 ====================
|
||||
const previewLoading = ref(false)
|
||||
const previewError = ref('')
|
||||
const previewData = ref({ rendered: '', default_args: null })
|
||||
|
||||
const previewRenderedHtml = computed(() => {
|
||||
const text = previewData.value.rendered || ''
|
||||
if (tplForm.value.type === 'email') return text
|
||||
return text.replace(/\n/g, '<br>')
|
||||
})
|
||||
|
||||
let previewTimer = null
|
||||
const debouncedFetchPreview = () => {
|
||||
clearTimeout(previewTimer)
|
||||
previewTimer = setTimeout(() => fetchPreview(), 600)
|
||||
}
|
||||
|
||||
const fetchPreview = async () => {
|
||||
const tag = tplForm.value.tag?.trim()
|
||||
const type = tplForm.value.type
|
||||
const content = tplForm.value.content?.trim()
|
||||
|
||||
if (!content && (!tag || !type)) {
|
||||
previewData.value = { rendered: '', default_args: null }
|
||||
previewError.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
previewLoading.value = true
|
||||
previewError.value = ''
|
||||
try {
|
||||
const params = {}
|
||||
if (content) {
|
||||
params.content = content
|
||||
} else {
|
||||
params.tag = tag
|
||||
params.type = type
|
||||
}
|
||||
const res = await previewNoticeTemplate(params)
|
||||
const body = res?.data
|
||||
if (body?.code === 200 && body.data) {
|
||||
previewData.value = body.data
|
||||
} else {
|
||||
previewData.value = { rendered: '', default_args: null }
|
||||
previewError.value = body?.message || '渲染失败'
|
||||
}
|
||||
} catch {
|
||||
previewData.value = { rendered: '', default_args: null }
|
||||
previewError.value = '请求失败'
|
||||
} finally {
|
||||
previewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onTplDialogOpened = () => {
|
||||
if (tplForm.value.tag && tplForm.value.type) {
|
||||
fetchPreview()
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => tplForm.value.tag, debouncedFetchPreview)
|
||||
watch(() => tplForm.value.type, debouncedFetchPreview)
|
||||
watch(() => tplForm.value.content, debouncedFetchPreview)
|
||||
|
||||
// ==================== 工具函数 ====================
|
||||
const formatTime = (t) => {
|
||||
if (!t) return '-'
|
||||
@@ -495,6 +592,129 @@ onMounted(() => {
|
||||
.form-hint { margin-top: 6px; font-size: 12px; color: #a8abb2; line-height: 1.4; }
|
||||
.form-hint code { background: #f5f7fa; padding: 1px 5px; border-radius: 3px; font-size: 11px; color: #606266; }
|
||||
|
||||
/* ===== 弹窗 ===== */
|
||||
/* ===== 弹窗双栏布局 ===== */
|
||||
.radio-with-icon { display: inline-flex; align-items: center; gap: 4px; }
|
||||
|
||||
.tpl-dialog-body {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
min-height: 380px;
|
||||
}
|
||||
|
||||
.tpl-dialog-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tpl-dialog-right {
|
||||
width: 380px;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
background: #f5f7fa;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.preview-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.preview-rendered {
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.preview-rendered.html-mode {
|
||||
white-space: normal;
|
||||
transform: scale(0.75);
|
||||
transform-origin: top left;
|
||||
width: 133.33%;
|
||||
max-height: 426px;
|
||||
}
|
||||
|
||||
.preview-args {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.preview-args-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.preview-args-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.preview-arg-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.preview-arg-key {
|
||||
background: #ecf5ff;
|
||||
color: #409eff;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Consolas','Monaco',monospace;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.preview-arg-val {
|
||||
color: #606266;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.preview-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.preview-empty p {
|
||||
margin: 10px 0 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
+1649
-15
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user