Files
admin-web/src/modules/notifications/components/MessageComposerDialog.vue
T
2026-08-12 18:50:03 +08:00

70 lines
7.5 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 { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import AppDialog from '@/shared/components/AppDialog.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import { PlatformTenantSelector } from '@/modules/iam'
import { createNotification } from '../api/notificationCenter'
import type { NotificationTemplate, RecipientType } from '../types'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
const props = defineProps<{ templates: NotificationTemplate[] }>()
const open = defineModel<boolean>({ default: false })
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)
const scheduled = computed({ get: () => Boolean(form.scheduledAt), set: (value) => { if (!value) form.scheduledAt = '' } })
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' }]
: [{ label: '指定企业成员', value: 'MEMBERSHIP' }, { label: '企业角色', value: 'TENANT_ROLE' }, { label: '企业全部成员', value: 'TENANT_ALL' }])
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] }
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('请选择目标企业')
if (needsIds.value && (!recipientIds.length || recipientIds.some((value) => !/^\d+$/.test(value)))) return ElMessage.warning('请输入有效的数字用户或成员 ID')
if (needsRole.value && !form.roleCode.trim()) return ElMessage.warning('请输入角色编码')
if (Object.values(variables).some((value) => !value.trim())) return ElMessage.warning('请填写全部模板变量')
submitting.value = true
try {
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 ? new Date(form.scheduledAt).toISOString() : null, idempotencyKey: idempotencyKey() })
ElMessage.success(form.scheduledAt ? '消息已定时' : '消息发送任务已创建')
open.value = false; reset(); emit('success')
} catch (reason) { error.value = getApiErrorDetails(reason, '消息发送失败') } finally { submitting.value = false }
}
</script>
<template>
<AppDialog v-model="open" title="新建消息" width="min(680px, calc(100vw - 28px))" :show-confirm="false" destroy-on-close @cancel="reset">
<el-alert v-if="error" type="error" :closable="false" show-icon><template #title><span>{{ error.message }}</span><ApiErrorMeta :details="error" /></template></el-alert>
<el-form class="message-form" label-position="top">
<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>
<el-form-item v-if="needsIds" label="接收人 ID" required><el-input v-model="recipientIdsText" type="textarea" :rows="2" placeholder="多个数字 ID 使用逗号或换行分隔,最多 1000 个" /></el-form-item>
<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" /></el-form-item></section>
<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>
</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">{{ scheduled ? '确认定时发送' : '立即发送' }}</el-button></div></template>
</AppDialog>
</template>
<style scoped>
.message-form { margin-top: 18px; }.form-grid { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 16px; }.form-grid :deep(.el-select),.message-form > :deep(.el-form-item .el-select),.schedule-row :deep(.el-date-editor) { width: 100%; }.variables { margin-bottom: 16px; padding: 14px; border: 1px solid var(--cc-border); border-radius: 8px; }.variables > strong { display: block; margin-bottom: 12px; font-size: 13px; }.schedule-row { display:grid;width:100%;grid-template-columns:auto auto minmax(180px,1fr);align-items:center;gap:8px }.dialog-actions {display:flex;justify-content:flex-end;gap:10px}.dialog-actions :deep(.el-button){margin-left:0}@media(max-width:560px){.form-grid{grid-template-columns:1fr;gap:0}.schedule-row{grid-template-columns:auto 1fr}.schedule-row :deep(.el-date-editor){grid-column:1/-1}}
</style>