Files
admin-web/src/modules/notifications/components/TemplateEditorDialog.vue
T
2026-08-18 09:09:34 +08:00

48 lines
6.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import AppDialog from '@/shared/components/AppDialog.vue'
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { createTemplate, updateTemplate } from '../api/notificationCenter'
import type { NotificationTemplate } from '../types'
const props = defineProps<{ template?: NotificationTemplate | null }>()
const open = defineModel<boolean>({ default: false })
const emit = defineEmits<{ success: [] }>()
const submitting = ref(false)
const error = ref<ApiErrorDetails | null>(null)
const contentInput = ref<{ textarea?: HTMLTextAreaElement } | null>(null)
const whitelistText = ref('')
const form = reactive({ code: '', name: '', category: '', title: '', content: '' })
const editing = computed(() => Boolean(props.template))
const preview = computed(() => form.title || form.content ? `[站内信] ${form.title || '未填写标题'}\n${form.content || '未填写模板内容'}` : '填写标题和内容后,将在这里显示预览效果。')
const whitelist = computed(() => [...new Set(whitelistText.value.split(/[\s,,]+/).map((v) => v.trim()).filter(Boolean))])
watch([open, () => props.template], ([isOpen]) => { if (!isOpen) return; const item = props.template; Object.assign(form, item ? { code: item.templateCode, name: item.templateName, category: item.category, title: item.titleTemplate, content: item.bodyTemplate } : { code: '', name: '', category: '', title: '', content: '' }); whitelistText.value = item?.variableWhitelist.join(', ') ?? ''; error.value = null }, { immediate: true })
function normalizeCode(value: string) { form.code = value.replace(/[^A-Za-z0-9_]/g, '').toUpperCase() }
function insertVariable(name: string) { if (!whitelist.value.includes(name)) whitelistText.value = [...whitelist.value, name].join(', '); const textarea = contentInput.value?.textarea; const value = `\${${name}}`; const start = textarea?.selectionStart ?? form.content.length; const end = textarea?.selectionEnd ?? start; form.content = `${form.content.slice(0,start)}${value}${form.content.slice(end)}`.slice(0,10000) }
async function submit() {
error.value = null
if (!/^[A-Z][A-Z0-9_]{0,63}$/.test(form.code)) return ElMessage.warning('模板CODE须以字母开头,仅支持大写字母、数字和下划线')
if (!form.name.trim() || !form.category.trim() || !form.title.trim() || !form.content.trim()) return ElMessage.warning('请填写完整模板信息')
if (whitelist.value.some((key) => !/^[a-z][a-z0-9_]{0,31}$/.test(key))) return ElMessage.warning('变量名须以小写字母开头,仅支持小写字母、数字和下划线')
const payload = { templateName: form.name.trim(), category: form.category.trim(), titleTemplate: form.title.trim(), bodyTemplate: form.content.trim(), variableWhitelist: whitelist.value }
submitting.value = true
try { if (props.template) await updateTemplate(props.template.id, { ...payload, version: props.template.version }); else await createTemplate({ templateCode: form.code, ...payload }); ElMessage.success(props.template ? '模板已更新' : '模板草稿已创建'); open.value = false; emit('success') } catch (reason) { error.value = getApiErrorDetails(reason, '模板保存失败') } finally { submitting.value = false }
}
</script>
<template>
<AppDialog v-model="open" :title="editing ? '编辑消息模板' : '新增消息模板'" width="min(760px, calc(100vw - 28px))" :show-confirm="false" destroy-on-close>
<OperationErrorToast :details="error" />
<el-alert title="首期仅支持站内信;新模板保存为草稿,草稿激活后不可再编辑。" type="info" :closable="false" show-icon />
<el-form class="template-form" label-position="top"><div class="form-grid"><el-form-item label="模板CODE" required><el-input :model-value="form.code" maxlength="64" :disabled="editing" placeholder="如:ORDER_SHIPPED_NOTICE" @update:model-value="normalizeCode(String($event))" /></el-form-item><el-form-item label="模板名称" required><el-input v-model="form.name" maxlength="128" /></el-form-item></div><div class="form-grid"><el-form-item label="业务分类" required><el-input v-model="form.category" maxlength="64" /></el-form-item><el-form-item label="模板类型"><el-input model-value="站内信" disabled /></el-form-item></div><el-form-item label="消息标题模板" required><el-input v-model="form.title" maxlength="256" show-word-limit /></el-form-item><el-form-item label="消息正文模板" required><el-input ref="contentInput" v-model="form.content" type="textarea" :rows="6" maxlength="10000" show-word-limit /></el-form-item><el-form-item label="变量白名单"><el-input v-model="whitelistText" placeholder="多个变量使用逗号分隔,如 customer_name, order_no" /></el-form-item><div class="variable-row"><span>常用变量</span><el-button v-for="name in ['customer_name','order_no','container_no','amount','date']" :key="name" size="small" @click="insertVariable(name)">{{ name }}</el-button></div><section class="template-preview"><strong>内容预览</strong><p>{{ preview }}</p></section></el-form>
<template #footer="{ close }"><div class="dialog-actions"><el-button :disabled="submitting" @click="close">取消</el-button><el-button type="primary" :loading="submitting" @click="submit">保存模板</el-button></div></template>
</AppDialog>
</template>
<style scoped>
.template-form{margin-top:18px}.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.variable-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:18px;color:var(--cc-muted);font-size:12px}.variable-row :deep(.el-button){margin-left:0}.template-preview{overflow:hidden;border:1px solid var(--cc-border);border-radius:8px}.template-preview strong{display:block;padding:9px 14px;border-bottom:1px solid var(--cc-border);background:#f8fafc;font-size:13px}.template-preview p{min-height:76px;margin:0;padding:16px;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--cc-muted);font-size:13px;line-height:1.7}.dialog-actions{display:flex;justify-content:flex-end;gap:10px}.dialog-actions :deep(.el-button){margin-left:0}@media(max-width:560px){.form-grid{grid-template-columns:1fr;gap:0}}
</style>