Files
admin-web/src/modules/notifications/components/TemplateEditorDialog.vue
T

48 lines
6.4 KiB
Vue
Raw Normal View History

2026-08-12 18:24:05 +08:00
<script setup lang="ts">
2026-08-12 18:50:03 +08:00
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
2026-08-12 18:24:05 +08:00
import AppDialog from '@/shared/components/AppDialog.vue'
2026-08-18 09:09:34 +08:00
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
2026-08-12 18:50:03 +08:00
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { createTemplate, updateTemplate } from '../api/notificationCenter'
import type { NotificationTemplate } from '../types'
2026-08-12 18:24:05 +08:00
2026-08-12 18:50:03 +08:00
const props = defineProps<{ template?: NotificationTemplate | null }>()
2026-08-12 18:24:05 +08:00
const open = defineModel<boolean>({ default: false })
2026-08-12 18:50:03 +08:00
const emit = defineEmits<{ success: [] }>()
const submitting = ref(false)
const error = ref<ApiErrorDetails | null>(null)
2026-08-12 18:24:05 +08:00
const contentInput = ref<{ textarea?: HTMLTextAreaElement } | null>(null)
2026-08-12 18:50:03 +08:00
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))])
2026-08-12 18:24:05 +08:00
2026-08-12 18:50:03 +08:00
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 })
2026-08-12 18:24:05 +08:00
function normalizeCode(value: string) { form.code = value.replace(/[^A-Za-z0-9_]/g, '').toUpperCase() }
2026-08-12 18:50:03 +08:00
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 }
2026-08-12 18:24:05 +08:00
}
</script>
<template>
2026-08-12 18:50:03 +08:00
<AppDialog v-model="open" :title="editing ? '编辑消息模板' : '新增消息模板'" width="min(760px, calc(100vw - 28px))" :show-confirm="false" destroy-on-close>
2026-08-18 09:09:34 +08:00
<OperationErrorToast :details="error" />
<el-alert title="首期仅支持站内信;新模板保存为草稿,草稿激活后不可再编辑。" type="info" :closable="false" show-icon />
2026-09-03 19:29:38 +08:00
<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" placeholder="请输入模板名称" /></el-form-item></div><div class="form-grid"><el-form-item label="业务分类" required><el-input v-model="form.category" maxlength="64" placeholder="请输入业务分类" /></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 placeholder="请输入消息标题模板" /></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 placeholder="请输入消息正文模板" /></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>
2026-08-12 18:50:03 +08:00
<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>
2026-08-12 18:24:05 +08:00
</AppDialog>
</template>
<style scoped>
2026-08-12 18:50:03 +08:00
.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}}
2026-08-12 18:24:05 +08:00
</style>