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

67 lines
6.7 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 '@/styles/adminDensity.css'
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 InlineHelp from '@/shared/components/InlineHelp.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() {
if (submitting.value) return
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 class="admin-density-dialog" v-model="open" :title="editing ? `编辑消息模板 · ${template?.templateCode || '—'}` : '新增消息模板'" width="min(760px, calc(100vw - 28px))" fullscreenable :confirm-loading="submitting" :show-confirm="false" destroy-on-close>
<OperationErrorToast :details="error" />
<el-form class="density-form density-dialog-sections" label-position="left" label-width="110px" :disabled="submitting">
<section class="density-dialog-section" data-tone="work" aria-label="模板定义">
<h3 class="density-dialog-section__title">模板定义 <InlineHelp title="模板说明">仅支持站内信;新模板保存为草稿,草稿激活后不可再编辑。变量须登记到白名单,发送时逐项填写。</InlineHelp></h3>
<table class="density-table density-table--editable"><tbody>
<tr><td><el-form-item label="模板CODE" required><span v-if="editing">{{ form.code }}</span><el-input v-else :model-value="form.code" maxlength="64" placeholder="如:ORDER_SHIPPED_NOTICE" @update:model-value="normalizeCode(String($event))" /></el-form-item></td></tr>
<tr><td><el-form-item label="模板名称" required><el-input v-model="form.name" maxlength="128" placeholder="请输入模板名称" /></el-form-item></td></tr>
<tr><td><el-form-item label="业务分类" required><el-input v-model="form.category" maxlength="64" placeholder="请输入业务分类" /></el-form-item></td></tr>
<tr><td><el-form-item label="模板类型"><span>站内信</span></el-form-item></td></tr>
<tr><td><el-form-item label="消息标题模板" required><el-input v-model="form.title" maxlength="256" show-word-limit placeholder="请输入消息标题模板" /></el-form-item></td></tr>
<tr><td><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></td></tr>
<tr><td><el-form-item label="变量白名单"><el-input v-model="whitelistText" placeholder="多个变量使用逗号分隔,如 customer_name, order_no" /></el-form-item></td></tr>
<tr><td><el-form-item label="常用变量"><div class="variable-row"><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></el-form-item></td></tr>
</tbody></table>
</section>
<section class="density-dialog-section template-preview" data-tone="source" aria-label="内容预览"><h3 class="density-dialog-section__title">内容预览</h3><p class="density-dialog-note message-body">{{ preview }}</p></section>
</el-form>
<template #footer="{ close }"><div class="density-actions density-actions--footer"><el-button :disabled="submitting" @click="close">取消</el-button><el-button type="primary" :loading="submitting" @click="submit">保存模板</el-button></div></template>
</AppDialog>
</template>
<style scoped>
.variable-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--admin-space-3); }
.variable-row :deep(.el-button) { margin-left: 0; }
.message-body { white-space: pre-wrap; overflow-wrap: anywhere; }
</style>