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-08-12 18:50:03 +08:00
import { PlatformTenantSelector } from '@/modules/iam'
import { createNotification } from '../api/notificationCenter'
import type { NotificationTemplate , RecipientType } from '../types'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
2026-08-12 18:24:05 +08:00
2026-08-12 18:50:03 +08:00
const props = defineProps < { templates : NotificationTemplate [] } > ()
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 )
const scope = ref < 'platform' | 'customer' > ( 'platform' )
const recipientIdsText = ref ( '' )
const variables = reactive < Record < string , string > > ({})
const form = reactive ({ tenantId : '0' , templateCode : '' , recipientType : 'PLATFORM_ROLE' as RecipientType , roleCode : '' , scheduledAt : '' })
const selectedTemplate = computed (() => props . templates . find (( item ) => item . templateCode === form . templateCode ) ?? null )
2026-08-27 21:48:15 +08:00
function toDateTimeValue ( date : Date ) {
const pad = ( value : number ) => String ( value ). padStart ( 2 , '0' )
return ` ${ date . getFullYear () } - ${ pad ( date . getMonth () + 1 ) } - ${ pad ( date . getDate ()) } T ${ pad ( date . getHours ()) } : ${ pad ( date . getMinutes ()) } : ${ pad ( date . getSeconds ()) } `
}
const scheduled = computed ({
get : () => Boolean ( form . scheduledAt ),
set : ( value : boolean ) => {
if ( value && ! form . scheduledAt ) form . scheduledAt = toDateTimeValue ( new Date ( Date . now () + 5 * 60 * 1000 ))
if ( ! value ) form . scheduledAt = ''
},
})
2026-08-12 18:50:03 +08:00
const needsIds = computed (() => [ 'USER' , 'MEMBERSHIP' ]. includes ( form . recipientType ))
const needsRole = computed (() => [ 'TENANT_ROLE' , 'PLATFORM_ROLE' ]. includes ( form . recipientType ))
const allowedRecipients = computed (() => scope . value === 'platform'
? [{ label : '指定平台用户' , value : 'USER' }, { label : '平台角色' , value : 'PLATFORM_ROLE' }]
2026-08-27 21:48:15 +08:00
: [{ label : '指定企业成员' , value : 'USER' }, { label : '企业角色' , value : 'TENANT_ROLE' }, { label : '企业全部成员' , value : 'TENANT_ALL' }])
2026-08-12 18:50:03 +08:00
watch ( scope , ( value ) => { form . tenantId = value === 'platform' ? '0' : '' ; form . recipientType = value === 'platform' ? 'PLATFORM_ROLE' : 'TENANT_ALL' ; form . roleCode = '' ; recipientIdsText . value = '' })
watch ( selectedTemplate , ( value ) => { for ( const key of Object . keys ( variables )) delete variables [ key ]; for ( const key of value ? . variableWhitelist ?? []) variables [ key ] = '' })
function reset () { scope . value = 'platform' ; Object . assign ( form , { tenantId : '0' , templateCode : '' , recipientType : 'PLATFORM_ROLE' , roleCode : '' , scheduledAt : '' }); recipientIdsText . value = '' ; error . value = null ; for ( const key of Object . keys ( variables )) delete variables [ key ] }
2026-08-27 21:48:15 +08:00
const MAX_LONG_ID = 9 _223_372_036_854_775_807n
2026-08-12 18:50:03 +08:00
function parseIds () { return [... new Set ( recipientIdsText . value . split ( /[\s,, ]+/ ). map (( value ) => value . trim ()). filter ( Boolean ))] }
function idempotencyKey () { return typeof crypto !== 'undefined' && crypto . randomUUID ? crypto . randomUUID () : `notification- ${ Date . now () } - ${ Math . random (). toString ( 36 ). slice ( 2 ) } ` }
async function submit () {
error . value = null
const recipientIds = parseIds ()
if ( ! form . templateCode ) return ElMessage . warning ( '请选择已激活模板' )
if ( scope . value === 'customer' && ! form . tenantId ) return ElMessage . warning ( '请选择目标企业' )
2026-08-27 21:48:15 +08:00
if ( needsIds . value && ( ! recipientIds . length || recipientIds . length > 1000 || recipientIds . some (( value ) => ! /^\d+$/ . test ( value ) || BigInt ( value ) > MAX_LONG_ID ))) return ElMessage . warning ( '请输入有效的数字用户 ID,单次最多 1000 个且不能超过 19 位' )
2026-08-12 18:50:03 +08:00
if ( needsRole . value && ! form . roleCode . trim ()) return ElMessage . warning ( '请输入角色编码' )
if ( Object . values ( variables ). some (( value ) => ! value . trim ())) return ElMessage . warning ( '请填写全部模板变量' )
submitting . value = true
try {
2026-09-18 09:15:40 +08:00
await createNotification ({ tenantId : form . tenantId , templateCode : form . templateCode , recipientType : form . recipientType , ...( needsIds . value ? { recipientIds } : {}), ...( needsRole . value ? { roleCode : form . roleCode . trim () } : {}), variables : { ... variables }, scheduledAt : form . scheduledAt || null , idempotencyKey : idempotencyKey () })
2026-08-12 18:50:03 +08:00
ElMessage . success ( form . scheduledAt ? '消息已定时' : '消息发送任务已创建' )
open . value = false ; reset (); emit ( 'success' )
} catch ( reason ) { error . value = getApiErrorDetails ( reason , '消息发送失败' ) } finally { submitting . value = false }
}
2026-08-12 18:24:05 +08:00
</ script >
< template >
2026-09-15 09:58:15 +08:00
< AppDialog class = "admin-density-dialog" v-model = "open" title="新建消息" width="min(680px, calc(100vw - 28px))" :show-confirm="false" destroy-on-close @cancel="reset" >
2026-08-18 09:09:34 +08:00
< OperationErrorToast :details = "error" />
2026-09-15 09:58:15 +08:00
< el-form class = "message-form density-form" label -position =" top " >
2026-08-12 18:50:03 +08:00
< div class = "form-grid" >< el-form-item label = "发送范围" required >< el-segmented v-model = "scope" :options="[{ label: '平台员工', value: 'platform' }, { label: '指定企业', value: 'customer' }]" /></el-form-item><el-form-item label="发送方式"><el-input model-value="站内信" disabled /></ el-form-item ></ div >
< PlatformTenantSelector v-if = "scope === 'customer'" v-model="form.tenantId" label="目标企业" />
< el-form-item label = "消息模板" required >< el-select v-model = "form.templateCode" filterable placeholder="请选择已激活模板"><el-option v-for="item in templates" :key="item.id" :label="`${item.templateName}( ${item.templateCode}) `" :value="item.templateCode" /></ el-select ></ el-form-item >
< div class = "form-grid" >< el-form-item label = "接收对象" required >< el-select v-model = "form.recipientType"><el-option v-for="item in allowedRecipients" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item><el-form-item v-if="needsRole" label="角色编码" required><el-input v-model="form.roleCode" maxlength="64" placeholder="请输入服务端角色编码" /></ el-form-item ></ div >
2026-08-27 21:48:15 +08:00
< el-form-item v-if = "needsIds" label="接收人用户 ID" required><el-input v-model="recipientIdsText" type="textarea" :rows="2" placeholder="多个用户 ID 使用逗号或换行分隔,最多 1000 个" /></ el-form-item >
2026-09-03 19:29:38 +08:00
< section v-if = "selectedTemplate?.variableWhitelist.length" class="variables"><strong>模板变量</strong><el-form-item v-for="key in selectedTemplate.variableWhitelist" :key="key" :label="key" required><el-input v-model="variables[key]" maxlength="2000" placeholder="请输入变量值" /></ el-form-item ></ section >
2026-08-12 18:50:03 +08:00
< el-form-item label = "定时发送" >< div class = "schedule-row" >< el-switch v-model = "scheduled" aria-label="启用定时发送" /><span>启用</span><el-date-picker v-model="form.scheduledAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="选择发送时间" :disabled="!scheduled" /></ div ></ el-form-item >
2026-08-12 18:24:05 +08:00
</ 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" >{{ scheduled ? ' 确认定时发送 ' : ' 立即发送 ' }} </ el -button ></ div ></ template >
2026-08-12 18:24:05 +08:00
</ AppDialog >
</ template >
< style scoped >
2026-09-15 09:58:15 +08:00
. message - form { margin - top : var ( -- admin - space - 5 ); }. form - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : var ( -- admin - space - 5 ); }. form - grid : deep (. el - select ),. message - form > : deep (. el - form - item . el - select ),. schedule - row : deep (. el - date - editor ) { width : 100 % ; }. variables { margin - bottom : var ( -- admin - space - 5 ); padding : var ( -- admin - space - 5 ); border : 1 px solid var ( -- cc - border ); border - radius : var ( -- admin - radius ); }. variables > strong { display : block ; margin - bottom : var ( -- admin - space - 5 ); font - size : var ( -- admin - font - body ); }. schedule - row { display : grid ; width : 100 % ; grid - template - columns : auto auto minmax ( 180 px , 1 fr ); align - items : center ; gap : var ( -- admin - space - 3 ) }. dialog - actions { display : flex ; justify - content : flex - end ; gap : var ( -- admin - space - 4 )}. dialog - actions : deep (. el - button ){ margin - left : 0 } @ media ( max - width : 560 px ){. form - grid { grid - template - columns : 1 fr ; gap : 0 }. schedule - row { grid - template - columns : auto 1 fr }. schedule - row : deep (. el - date - editor ){ grid - column : 1 /- 1 }}
2026-08-12 18:24:05 +08:00
</ style >