2026-08-12 18:24:05 +08:00
< script setup lang = "ts" >
2026-09-15 09:58:15 +08:00
import '@/styles/adminDensity.css'
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-10-20 10:11:31 +08:00
import InlineHelp from '@/shared/components/InlineHelp.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 () {
2026-10-20 10:11:31 +08:00
if ( submitting . value ) return
2026-08-12 18:50:03 +08:00
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-10-20 10:11:31 +08:00
< 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 >
2026-08-18 09:09:34 +08:00
< OperationErrorToast :details = "error" />
2026-10-20 10:11:31 +08:00
< 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 >
2026-08-12 18:24:05 +08:00
</ AppDialog >
</ template >
< style scoped >
2026-10-20 10:11:31 +08:00
. 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 ; }
2026-08-12 18:24:05 +08:00
</ style >