2026-08-12 18:24:05 +08:00
< script setup lang = "ts" >
2026-10-20 23:34:11 +08:00
import '@/styles/adminWorkbench.css'
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 { 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 () {
2026-10-20 10:11:31 +08:00
if ( submitting . value ) return
2026-08-12 18:50:03 +08:00
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-10-20 23:34:11 +08:00
< AppDialog class = "admin-density-dialog admin-workbench is-dialog" v-model = "open" title="新建消息" width="min(680px, calc(100vw - 28px))" fullscreenable :confirm-loading="submitting" :show-confirm="false" destroy-on-close @cancel="reset" >
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 =" 100px " :disabled = "submitting" >
2026-10-20 23:34:11 +08:00
< section class = "density-dialog-section workbench-evidence" data -tone =" work " aria -label =" 发送设置 " >
2026-10-20 10:11:31 +08:00
< h3 class = "density-dialog-section__title" > 发送设置 < InlineHelp title = "发送说明" > 消息由已激活模板生成 , 仅发送站内信 ; 可指定平台员工或企业接收人 , 并按需启用定时发送 。 </ InlineHelp ></ h3 >
< table class = "density-table density-table--editable" >< tbody >
< tr >< td >< el-form-item label = "发送范围" required >< el-segmented v-model = "scope" :options="[{ label: '平台员工', value: 'platform' }, { label: '指定企业', value: 'customer' }]" /></ el-form-item ></ td ></ tr >
< tr v-if = "scope === 'customer'"><td><PlatformTenantSelector v-model="form.tenantId" label="目标企业" :disabled="submitting" /></ td ></ tr >
< tr >< td >< el-form-item label = "发送方式" >< span > 站内信 </ span ></ el-form-item ></ td ></ tr >
< tr >< td >< 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 ></ td ></ tr >
< tr >< td >< 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 ></ td ></ tr >
< tr v-if = "needsRole"><td><el-form-item label="角色编码" required><el-input v-model="form.roleCode" maxlength="64" placeholder="请输入服务端角色编码" /></ el-form-item ></ td ></ tr >
< tr v-if = "needsIds"><td><el-form-item label="接收人用户 ID" required><el-input v-model="recipientIdsText" type="textarea" :rows="2" placeholder="多个用户 ID 使用逗号或换行分隔,最多 1000 个" /></ el-form-item ></ td ></ tr >
< tr >< td >< 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 ></ td ></ tr >
</ tbody ></ table >
</ section >
2026-10-20 23:34:11 +08:00
< section v-if = "selectedTemplate?.variableWhitelist.length" class="density-dialog-section workbench-evidence" data-tone="work" aria-label="模板变量" >
2026-10-20 10:11:31 +08:00
< h3 class = "density-dialog-section__title" > 模板变量 </ h3 >
< table class = "density-table density-table--editable" >< tbody >< tr v-for = "key in selectedTemplate.variableWhitelist" :key="key"><td><el-form-item :label="key" required><el-input v-model="variables[key]" maxlength="2000" placeholder="请输入变量值" /></ el-form-item ></ td ></ tr ></ tbody ></ table >
</ section >
2026-08-12 18:24:05 +08:00
</ el-form >
2026-10-20 10:11:31 +08:00
< 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" >{{ scheduled ? ' 确认定时发送 ' : ' 立即发送 ' }} </ el -button ></ div ></ template >
2026-08-12 18:24:05 +08:00
</ AppDialog >
</ template >
< style scoped >
2026-10-20 10:11:31 +08:00
. schedule - row { display : grid ; width : 100 % ; grid - template - columns : auto auto minmax ( 180 px , 1 fr ); align - items : center ; gap : var ( -- admin - space - 3 ); }
. schedule - row : deep (. el - date - editor ) { width : 100 % ; }
@ media ( max - width : 560 px ) { . 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 >