@@ -0,0 +1,618 @@
< script setup lang = "ts" >
import { computed , onMounted , reactive , ref } from 'vue'
import dayjs , { type Dayjs } from 'dayjs'
import { message } from 'ant-design-vue'
import {
EditOutlined ,
PlusOutlined ,
ReloadOutlined ,
SendOutlined ,
} from '@ant-design/icons-vue'
import { api } from '../api'
interface DeliveryCounts {
queued : number
sending : number
accepted : number
failed : number
skipped : number
}
interface Campaign {
id : number
publicId : string
state : string
title : string
body : string
category : string
action : string
entityId : string | null
flavor : string
provider : string | null
minVersionCode : number | null
maxVersionCode : number | null
targetUserId : number | null
ttlSeconds : number
scheduledAt : string | null
createdAt : string
startedAt : string | null
completedAt : string | null
cancelledAt : string | null
deliveries : DeliveryCounts
}
interface PushDevice {
id : number
userId : number
username : string
provider : string
packageName : string
flavor : string
appVersion : string
versionCode : number
notificationsAllowed : boolean
isActive : boolean
disabledReason : string | null
tokenSuffix : string
lastSeenAt : string
}
interface ProviderHealth {
provider : string
environments : Array < { flavor : string ; enabled : boolean ; errors : string [ ] } >
}
interface PushHealth {
enabled : boolean
tokenEncryptionConfigured : boolean
providers : ProviderHealth [ ]
}
type SubmitMode = 'draft' | 'now' | 'scheduled'
const activeTab = ref ( 'campaigns' )
const campaigns = ref < Campaign [ ] > ( [ ] )
const campaignTotal = ref ( 0 )
const campaignPage = ref ( 1 )
const pageSize = 20
const campaignLoading = ref ( false )
const health = ref < PushHealth | null > ( null )
const healthLoading = ref ( false )
const editorOpen = ref ( false )
const editingId = ref < number | null > ( null )
const editorSaving = ref ( false )
const estimating = ref ( false )
const estimate = ref < number | null > ( null )
const scheduleAt = ref < Dayjs | null > ( null )
const form = reactive ( emptyCampaign ( ) )
const devices = ref < PushDevice [ ] > ( [ ] )
const deviceLoading = ref ( false )
const deviceSearch = ref ( '' )
const testOpen = ref ( false )
const testSaving = ref ( false )
const testForm = reactive ( {
deviceId : undefined as number | undefined ,
title : '' ,
body : '' ,
category : 'system' ,
action : 'none' ,
entityId : '' ,
} )
const categoryLabels : Record < string , string > = {
system : '系统通知' ,
budget : '预算提醒' ,
operations : '运营通知' ,
}
const actionLabels : Record < string , string > = {
none : '仅打开应用' ,
home : '首页' ,
budget : '预算页面' ,
update : '版本更新' ,
}
const providerLabels : Record < string , string > = {
huawei : '华为' ,
honor : '荣耀' ,
xiaomi : '小米' ,
oppo : 'OPPO' ,
vivo : 'vivo' ,
meizu : '魅族' ,
}
const stateMeta : Record < string , { label : string ; color : string } > = {
draft : { label : '草稿' , color : 'default' } ,
scheduled : { label : '已定时' , color : 'blue' } ,
queued : { label : '待发送' , color : 'cyan' } ,
sending : { label : '发送中' , color : 'processing' } ,
completed : { label : '已完成' , color : 'success' } ,
partially _failed : { label : '部分失败' , color : 'warning' } ,
cancelled : { label : '已取消' , color : 'default' } ,
}
const campaignColumns = [
{ title : '内容' , key : 'content' , width : 310 } ,
{ title : '目标' , key : 'target' , width : 180 } ,
{ title : '状态' , key : 'state' , width : 100 } ,
{ title : '投递结果' , key : 'deliveries' , width : 220 } ,
{ title : '时间' , key : 'time' , width : 180 } ,
{ title : '操作' , key : 'actions' , width : 210 } ,
]
const deviceColumns = [
{ title : '设备' , key : 'device' , width : 220 } ,
{ title : '用户' , key : 'user' , width : 150 } ,
{ title : '厂商' , key : 'provider' , width : 90 } ,
{ title : '应用' , key : 'app' , width : 170 } ,
{ title : '状态' , key : 'state' , width : 120 } ,
{ title : '最后活跃' , key : 'time' , width : 165 } ,
{ title : '操作' , key : 'actions' , width : 100 } ,
]
const canEstimate = computed ( ( ) => form . title . trim ( ) && form . body . trim ( ) )
onMounted ( async ( ) => {
await Promise . all ( [ loadCampaigns ( ) , loadHealth ( ) ] )
} )
function emptyCampaign ( ) {
return {
title : '' ,
body : '' ,
category : 'system' ,
action : 'none' ,
entityId : '' ,
flavor : 'production' ,
provider : undefined as string | undefined ,
minVersionCode : undefined as number | undefined ,
maxVersionCode : undefined as number | undefined ,
targetUserId : undefined as number | undefined ,
ttlSeconds : 259200 ,
}
}
function campaignPayload ( ) {
return {
title : form . title . trim ( ) ,
body : form . body . trim ( ) ,
category : form . category ,
action : form . action ,
entityId : form . entityId . trim ( ) || null ,
flavor : form . flavor ,
provider : form . provider || null ,
minVersionCode : form . minVersionCode ? ? null ,
maxVersionCode : form . maxVersionCode ? ? null ,
targetUserId : form . targetUserId ? ? null ,
ttlSeconds : form . ttlSeconds ,
}
}
function assignForm ( value : ReturnType < typeof emptyCampaign > ) {
Object . assign ( form , value )
}
async function loadCampaigns ( ) {
campaignLoading . value = true
try {
const result = await api . pushCampaigns ( { page : campaignPage . value , limit : pageSize } )
campaigns . value = result . list
campaignTotal . value = result . total
} finally {
campaignLoading . value = false
}
}
async function loadHealth ( ) {
healthLoading . value = true
try {
health . value = await api . pushHealth ( )
} catch ( error ) {
message . error ( errorText ( error , '推送服务状态读取失败' ) )
} finally {
healthLoading . value = false
}
}
async function loadDevices ( ) {
deviceLoading . value = true
try {
devices . value = await api . pushDevices ( { search : deviceSearch . value . trim ( ) || undefined , limit : 100 } )
} finally {
deviceLoading . value = false
}
}
function openCreate ( ) {
editingId . value = null
estimate . value = null
scheduleAt . value = null
assignForm ( emptyCampaign ( ) )
editorOpen . value = true
}
function openEdit ( item : Campaign ) {
editingId . value = item . id
estimate . value = null
scheduleAt . value = item . scheduledAt ? dayjs ( normalizeUtc ( item . scheduledAt ) ) : null
assignForm ( {
title : item . title ,
body : item . body ,
category : item . category ,
action : item . action ,
entityId : item . entityId || '' ,
flavor : item . flavor ,
provider : item . provider || undefined ,
minVersionCode : item . minVersionCode ? ? undefined ,
maxVersionCode : item . maxVersionCode ? ? undefined ,
targetUserId : item . targetUserId ? ? undefined ,
ttlSeconds : item . ttlSeconds ,
} )
editorOpen . value = true
}
async function estimateTargets ( ) {
estimating . value = true
try {
const result = await api . estimatePushCampaign ( campaignPayload ( ) )
estimate . value = result . devices
} catch ( error ) {
message . error ( errorText ( error , '目标设备估算失败' ) )
} finally {
estimating . value = false
}
}
async function submitCampaign ( mode : SubmitMode ) {
if ( ! form . title . trim ( ) || ! form . body . trim ( ) ) {
message . warning ( '请填写推送标题和正文' )
return
}
if ( mode === 'scheduled' && ( ! scheduleAt . value || ! scheduleAt . value . isAfter ( dayjs ( ) . add ( 5 , 'second' ) ) ) ) {
message . warning ( '定时发送时间必须晚于当前时间' )
return
}
editorSaving . value = true
try {
const saved = editingId . value
? await api . updatePushCampaign ( editingId . value , campaignPayload ( ) )
: await api . createPushCampaign ( campaignPayload ( ) )
if ( mode !== 'draft' ) {
await api . sendPushCampaign (
saved . id ,
mode === 'scheduled' ? scheduleAt . value ? . toISOString ( ) : undefined ,
)
}
editorOpen . value = false
message . success ( mode === 'draft' ? '草稿已保存' : mode === 'scheduled' ? '定时任务已保存' : '推送已进入发送队列' )
await loadCampaigns ( )
} catch ( error ) {
message . error ( errorText ( error , '推送活动保存失败' ) )
} finally {
editorSaving . value = false
}
}
async function sendNow ( item : Campaign ) {
try {
await api . sendPushCampaign ( item . id )
message . success ( '推送已进入发送队列' )
await loadCampaigns ( )
} catch ( error ) {
message . error ( errorText ( error , '推送启动失败' ) )
}
}
async function cancelCampaign ( item : Campaign ) {
try {
await api . cancelPushCampaign ( item . id )
message . success ( '推送活动已取消' )
await loadCampaigns ( )
} catch ( error ) {
message . error ( errorText ( error , '取消失败' ) )
}
}
async function openTest ( device ? : PushDevice ) {
if ( ! devices . value . length ) await loadDevices ( )
Object . assign ( testForm , {
deviceId : device ? . id ,
title : '' ,
body : '' ,
category : 'system' ,
action : 'none' ,
entityId : '' ,
} )
testOpen . value = true
}
async function submitTest ( ) {
if ( ! testForm . deviceId || ! testForm . title . trim ( ) || ! testForm . body . trim ( ) ) {
message . warning ( '请选择设备并填写推送内容' )
return
}
testSaving . value = true
try {
await api . testPush ( {
... testForm ,
title : testForm . title . trim ( ) ,
body : testForm . body . trim ( ) ,
entityId : testForm . entityId . trim ( ) || null ,
} )
testOpen . value = false
message . success ( '测试推送已进入发送队列' )
await loadCampaigns ( )
} catch ( error ) {
message . error ( errorText ( error , '测试推送失败' ) )
} finally {
testSaving . value = false
}
}
function onTabChange ( value : string ) {
activeTab . value = value
if ( value === 'devices' && ! devices . value . length ) loadDevices ( )
}
function isEditable ( item : Campaign ) {
return item . state === 'draft' || item . state === 'scheduled'
}
function isCancellable ( item : Campaign ) {
return [ 'draft' , 'scheduled' , 'queued' ] . includes ( item . state ) && ! item . startedAt
}
function state ( item : Campaign ) {
return stateMeta [ item . state ] || { label : item . state , color : 'default' }
}
function normalizeUtc ( value : string ) {
return /(?:Z|[+-]\d{2}:?\d{2})$/i . test ( value ) ? value : ` ${ value } Z `
}
function formatTime ( value ? : string | null ) {
if ( ! value ) return '-'
const date = new Date ( normalizeUtc ( value ) )
return Number . isNaN ( date . getTime ( ) )
? '-'
: new Intl . DateTimeFormat ( 'zh-CN' , {
timeZone : 'Asia/Shanghai' ,
year : 'numeric' ,
month : '2-digit' ,
day : '2-digit' ,
hour : '2-digit' ,
minute : '2-digit' ,
hour12 : false ,
} ) . format ( date ) . replaceAll ( '/' , '-' )
}
function errorText ( error : unknown , fallback : string ) {
const candidate = error as { response ? : { data ? : { message ? : string ; detail ? : string ; error ? : string } } }
const data = candidate . response ? . data
return data ? . message || data ? . detail || data ? . error || fallback
}
< / script >
< template >
< div class = "page-header" >
< div >
< h2 > 推送管理 < / h2 >
< div class = "subtle" > 厂商通道配置 、 活动投递和设备联调 < / div >
< / div >
< a-space >
< a-button @click ="openTest()" > < SendOutlined / > 测试设备 < / a-button >
< a-button type = "primary" @click ="openCreate" > < PlusOutlined / > 新建推送 < / a-button >
< / a-space >
< / div >
< a-alert
v-if = "health && (!health.enabled || !health.tokenEncryptionConfigured)"
type = "warning"
show -icon
style = "margin-bottom: 14px"
: message = "!health.enabled ? '推送总开关未启用' : '设备令牌加密密钥未配置'"
description = "完成服务端环境变量配置后再执行正式投递。" / >
< a-spin :spinning = "healthLoading" >
< a-descriptions v-if = "health" bordered size="small" :column="4" style="margin-bottom: 16px" >
< a -descriptions -item label = "服务" >
< a-badge : status = "health.enabled ? 'success' : 'default'" : text = "health.enabled ? '已启用' : '未启用'" / >
< / a-descriptions-item >
< a-descriptions-item label = "令牌加密" >
< a-badge : status = "health.tokenEncryptionConfigured ? 'success' : 'error'" : text = "health.tokenEncryptionConfigured ? '已配置' : '缺少密钥'" / >
< / a-descriptions-item >
< a-descriptions-item v-for = "item in health.providers" :key="item.provider" :label="providerLabels[item.provider] || item.provider" >
< a -space size = "small" >
< a-tooltip v-for = "env in item.environments" :key="env.flavor" :title="env.errors.join('、') || '配置完整'" >
< a -tag : color = "env.enabled && !env.errors.length ? 'success' : 'default'" >
{ { env . flavor === 'production' ? '正式' : '内测' } }
< / a-tag >
< / a-tooltip >
< / a-space >
< / a-descriptions-item >
< / a-descriptions >
< / a-spin >
< a-tabs :active-key = "activeTab" @change ="onTabChange" >
< a -tab -pane key = "campaigns" tab = "活动与历史" >
< div class = "toolbar" >
< span class = "subtle" > 共 { { campaignTotal } } 个活动 < / span >
< a-button size = "small" @click ="loadCampaigns" > < ReloadOutlined / > 刷新 < / a-button >
< / div >
< a-table
:columns = "campaignColumns"
:data-source = "campaigns"
:loading = "campaignLoading"
row -key = " id "
size = "small"
: scroll = "{ x: 1200 }"
: pagination = "{
current: campaignPage,
pageSize,
total: campaignTotal,
showTotal: (total: number) => `共 ${total} 条`,
onChange: (page: number) => { campaignPage = page; loadCampaigns() },
}" >
< template # bodyCell = "{ column, record }: { column: { key: string }; record: Campaign }" >
< template v-if = "column.key === 'content'" >
< div class = "campaign-title" > { { record . title } } < / div >
< div class = "campaign-body" > { { record . body } } < / div >
< a-tag > { { categoryLabels [ record . category ] || record . category } } < / a-tag >
< span class = "action-label" > { { actionLabels [ record . action ] || record . action } } < / span >
< / template >
< template v-else-if = "column.key === 'target'" >
< div > { { record . flavor === 'production' ? '正式环境' : '内测环境' } } < / div >
< div class = "subtle" >
{ { record . provider ? providerLabels [ record . provider ] : '全部厂商' } }
< template v-if = "record.targetUserId" > · 用户 {{ record.targetUserId }} < / template >
< / div >
< div v-if = "record.minVersionCode || record.maxVersionCode" class="subtle" >
版本 {{ record.minVersionCode | | 1 }} - {{ record.maxVersionCode | | ' 不限 ' }}
< / div >
< / template >
< template v-else-if = "column.key === 'state'" >
< a -tag :color = "state(record).color" > { { state ( record ) . label } } < / a-tag >
< / template >
< template v-else-if = "column.key === 'deliveries'" >
< a -space wrap size = "small" >
< a-tag color = "success" > 成功 { { record . deliveries . accepted } } < / a-tag >
< a-tag v-if = "record.deliveries.queued + record.deliveries.sending" > 处理中 {{ record.deliveries.queued + record.deliveries.sending }} < / a -tag >
< a-tag v-if = "record.deliveries.failed" color="error" > 失败 {{ record.deliveries.failed }} < / a -tag >
< a-tag v-if = "record.deliveries.skipped" > 跳过 {{ record.deliveries.skipped }} < / a -tag >
< / a-space >
< / template >
< template v-else-if = "column.key === 'time'" >
< div > { { record . scheduledAt ? '计划 ' + formatTime ( record . scheduledAt ) : formatTime ( record . createdAt ) } } < / div >
< div v-if = "record.completedAt" class="subtle" > 完成 {{ formatTime ( record.completedAt ) }} < / div >
< / template >
< template v-else-if = "column.key === 'actions'" >
< a -space size = "small" >
< a-button v-if = "isEditable(record)" size="small" @click="openEdit(record)" > < EditOutlined / > 编辑 < / a-button >
< a-popconfirm v-if = "record.state === 'draft' || record.state === 'scheduled'" title="立即开始投递这条推送?" @confirm="sendNow(record)" >
< a -button size = "small" type = "primary" > < SendOutlined / > 发送 < / a-button >
< / a-popconfirm >
< a-popconfirm v-if = "isCancellable(record)" title="确定取消该推送活动?" @confirm="cancelCampaign(record)" >
< a -button size = "small" danger > 取消 < / a-button >
< / a-popconfirm >
< / a-space >
< / template >
< / template >
< / a-table >
< / a-tab-pane >
< a-tab-pane key = "devices" tab = "注册设备" >
< div class = "toolbar" >
< a-input-search v -model :value = "deviceSearch" placeholder = "用户名或安装 ID" style = "width: 280px" @search ="loadDevices" / >
< a-button size = "small" @click ="loadDevices" > < ReloadOutlined / > 刷新 < / a-button >
< / div >
< a-table :columns = "deviceColumns" :data-source = "devices" :loading = "deviceLoading" row -key = " id " size = "small" : scroll = "{ x: 1050 }" :pagination = "false" >
< template # bodyCell = "{ column, record }: { column: { key: string }; record: PushDevice }" >
< template v-if = "column.key === 'device'" >
< div class = "mono" > # { { record . id } } · ... { { record . tokenSuffix } } < / div >
< div class = "subtle mono" > { { record . packageName } } < / div >
< / template >
< template v-else-if = "column.key === 'user'" >
< div > { { record . username } } < / div >
< div class = "subtle" > 用户 { { record . userId } } < / div >
< / template >
< template v-else-if = "column.key === 'provider'" >
{{ providerLabels [ record.provider ] | | record.provider }}
< / template >
< template v-else-if = "column.key === 'app'" >
< div > { { record . appVersion } } ( { { record . versionCode } } ) < / div >
< a-tag > { { record . flavor === 'production' ? '正式' : '内测' } } < / a-tag >
< / template >
< template v-else-if = "column.key === 'state'" >
< a -badge : status = "record.isActive && record.notificationsAllowed ? 'success' : 'default'" : text = "record.isActive && record.notificationsAllowed ? '可投递' : '不可投递'" / >
< div v-if = "record.disabledReason" class="subtle" > {{ record.disabledReason }} < / div >
< / template >
< template v-else-if = "column.key === 'time'" > {{ formatTime ( record.lastSeenAt ) }} < / template >
< template v-else-if = "column.key === 'actions'" >
< a -button size = "small" : disabled = "!record.isActive || !record.notificationsAllowed" @click ="openTest(record)" > < SendOutlined / > 测试 < / a-button >
< / template >
< / template >
< / a-table >
< / a-tab-pane >
< / a-tabs >
< a-modal v -model :open = "editorOpen" : title = "editingId ? '编辑推送' : '新建推送'" :width = "760" :footer = "null" :mask-closable = "false" >
< a-form layout = "vertical" >
< a-row :gutter = "12" >
< a-col :span = "12" > < a-form-item label = "标题" required > < a-input v -model :value = "form.title" :maxlength = "80" show -count / > < / a-form-item > < / a-col >
< a-col :span = "6" > < a-form-item label = "分类" required > < a-select v -model :value = "form.category" > < a-select-option v-for = "(label, value) in categoryLabels" :key="value" :value="value" > {{ label }} < / a -select -option > < / a-select > < / a-form-item > < / a-col >
< a-col :span = "6" > < a-form-item label = "有效期" > < a-input-number v -model :value = "form.ttlSeconds" :min = "60" :max = "604800" style = "width: 100%" addon -after = " 秒 " / > < / a-form-item > < / a-col >
< / a-row >
< a-form-item label = "正文" required > < a-textarea v -model :value = "form.body" :maxlength = "240" show -count :rows = "3" / > < / a-form-item >
< a-row :gutter = "12" >
< a-col :span = "8" > < a-form-item label = "点击动作" > < a-select v -model :value = "form.action" > < a-select-option v-for = "(label, value) in actionLabels" :key="value" :value="value" > {{ label }} < / a -select -option > < / a-select > < / a-form-item > < / a-col >
< a-col :span = "8" > < a-form-item label = "关联对象 ID" > < a-input v -model :value = "form.entityId" : disabled = "form.action === 'none' || form.action === 'home'" / > < / a-form-item > < / a-col >
< a-col :span = "8" > < a-form-item label = "环境" > < a-segmented v -model :value = "form.flavor" block : options = "[{ label: '正式', value: 'production' }, { label: '内测', value: 'internal' }]" / > < / a-form-item > < / a-col >
< / a-row >
< a-divider orientation = "left" > 目标范围 < / a-divider >
< a-row :gutter = "12" >
< a-col :span = "8" > < a-form-item label = "厂商" > < a-select v -model :value = "form.provider" allow -clear placeholder = "全部厂商" > < a-select-option v-for = "(label, value) in providerLabels" :key="value" :value="value" > {{ label }} < / a -select -option > < / a-select > < / a-form-item > < / a-col >
< a-col :span = "8" > < a-form-item label = "指定用户 ID" > < a-input-number v -model :value = "form.targetUserId" :min = "1" style = "width: 100%" / > < / a-form-item > < / a-col >
< a-col :span = "4" > < a-form-item label = "最低版本" > < a-input-number v -model :value = "form.minVersionCode" :min = "1" style = "width: 100%" / > < / a-form-item > < / a-col >
< a-col :span = "4" > < a-form-item label = "最高版本" > < a-input-number v -model :value = "form.maxVersionCode" :min = "1" style = "width: 100%" / > < / a-form-item > < / a-col >
< / a-row >
< div class = "estimate-row" >
< a-button :loading = "estimating" :disabled = "!canEstimate" @click ="estimateTargets" > 估算目标设备 < / a -button >
< span v-if = "estimate !== null" > 当前条件下预计投递 < strong > { { estimate } } < / strong > 台设备 < / span >
< / div >
< a-form-item label = "定时发送时间" >
< a-date-picker v -model :value = "scheduleAt" show -time format = "YYYY-MM-DD HH:mm" : disabled -date = " ( date : Dayjs ) = > date . isBefore ( dayjs ( ) . startOf ( 'day' ) ) " style=" width : 100 % " placeholder=" 仅在选择定时发送时使用 " />
</a-form-item>
<div class=" modal - actions ">
<a-button @click=" editorOpen = false ">关闭</a-button>
<a-button :loading=" editorSaving " @click=" submitCampaign ( 'draft' ) ">保存草稿</a-button>
<a-button :loading=" editorSaving " :disabled=" ! scheduleAt " @click=" submitCampaign ( 'scheduled' ) ">定时发送</a-button>
<a-popconfirm title=" 确认立即投递 ? " @confirm=" submitCampaign ( 'now' ) ">
<a-button type=" primary " :loading=" editorSaving "><SendOutlined />立即发送</a-button>
</a-popconfirm>
</div>
</a-form>
</a-modal>
<a-modal v-model:open=" testOpen " title=" 单设备测试 " :confirm-loading=" testSaving " ok-text=" 发送测试 " cancel-text=" 取消 " @ok=" submitTest ">
<a-form layout=" vertical ">
<a-form-item label=" 设备 " required>
<a-select v-model:value=" testForm . deviceId " show-search option-filter-prop=" label " placeholder=" 选择可投递设备 ">
<a-select-option v-for=" device in devices . filter ( item => item . isActive && item . notificationsAllowed ) " :key=" device . id " :value=" device . id " :label=" ` ${ device . username } ${ device . provider } ${ device . id } ` ">
{{ device.username }} · {{ providerLabels[device.provider] }} · #{{ device.id }} · {{ device.appVersion }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label=" 标题 " required><a-input v-model:value=" testForm . title " :maxlength=" 80 " show-count /></a-form-item>
<a-form-item label=" 正文 " required><a-textarea v-model:value=" testForm . body " :maxlength=" 240 " show-count :rows=" 3 " /></a-form-item>
<a-row :gutter=" 12 ">
<a-col :span=" 12 "><a-form-item label=" 分类 "><a-select v-model:value=" testForm . category "><a-select-option v-for=" ( label , value ) in categoryLabels " :key=" value " :value=" value ">{{ label }}</a-select-option></a-select></a-form-item></a-col>
<a-col :span=" 12 "><a-form-item label=" 点击动作 "><a-select v-model:value=" testForm . action "><a-select-option v-for=" ( label , value ) in actionLabels " :key=" value " :value=" value ">{{ label }}</a-select-option></a-select></a-form-item></a-col>
</a-row>
<a-form-item label=" 关联对象 ID "><a-input v-model:value=" testForm . entityId " :disabled=" testForm . action === 'none' || testForm . action === 'home' " / > < / a-form-item >
< / a-form >
< / a-modal >
< / template >
< style scoped >
. page - header ,
. toolbar ,
. modal - actions ,
. estimate - row {
display : flex ;
align - items : center ;
justify - content : space - between ;
gap : 12 px ;
}
. page - header { margin - bottom : 16 px ; }
. page - header h2 { margin : 0 ; }
. toolbar { margin - bottom : 12 px ; }
. subtle { color : # 8 c8c8c ; font - size : 12 px ; }
. campaign - title { font - weight : 600 ; margin - bottom : 3 px ; }
. campaign - body { color : # 595959 ; font - size : 12 px ; margin - bottom : 6 px ; white - space : pre - wrap ; }
. action - label { color : # 8 c8c8c ; font - size : 12 px ; }
. mono { font - family : ui - monospace , SFMono - Regular , Menlo , Consolas , monospace ; }
. estimate - row { justify - content : flex - start ; min - height : 32 px ; margin - bottom : 16 px ; }
. modal - actions { justify - content : flex - end ; padding - top : 4 px ; }
@ media ( max - width : 760 px ) {
. page - header { align - items : flex - start ; flex - direction : column ; }
}
< / style >