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-12 18:50:03 +08:00
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
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 >
< el-alert v-if = "error" type="error" :closable="false" show-icon><template #title><span>{{ error.message }}</span><ApiErrorMeta :details="error" /></ template ></ el-alert >
< el-alert v-else 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 >
2026-08-12 18:24:05 +08:00
</ AppDialog >
</ template >
< style scoped >
2026-08-12 18:50:03 +08:00
. template - form { margin - top : 18 px }. form - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : 16 px }. variable - row { display : flex ; flex - wrap : wrap ; align - items : center ; gap : 8 px ; margin - bottom : 18 px ; color : var ( -- cc - muted ); font - size : 12 px }. variable - row : deep (. el - button ){ margin - left : 0 }. template - preview { overflow : hidden ; border : 1 px solid var ( -- cc - border ); border - radius : 8 px }. template - preview strong { display : block ; padding : 9 px 14 px ; border - bottom : 1 px solid var ( -- cc - border ); background : # f8fafc ; font - size : 13 px }. template - preview p { min - height : 76 px ; margin : 0 ; padding : 16 px ; white - space : pre - wrap ; overflow - wrap : anywhere ; color : var ( -- cc - muted ); font - size : 13 px ; line - height : 1.7 }. dialog - actions { display : flex ; justify - content : flex - end ; gap : 10 px }. dialog - actions : deep (. el - button ){ margin - left : 0 } @ media ( max - width : 560 px ){. form - grid { grid - template - columns : 1 fr ; gap : 0 }}
2026-08-12 18:24:05 +08:00
</ style >