feat(push-config): 每个厂商卡片内嵌 Profiles,独立保存按钮

- 移除全局"厂商凭据"包裹卡和底部"平台 Profiles"卡
- 每个厂商独立一张卡:凭据 + 通知渠道 Profiles + 保存按钮
- Profiles 按厂商过滤展示,支持在对应厂商卡内添加/删除
- iOS APNs 卡额外显示 Thread ID 和中断级别列
- savingMap 替代独立的 saving ref,各厂商 loading 状态独立

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-06-26 16:29:36 +08:00
父节点 eadd024fc0
当前提交 088e922ec9

查看文件

@ -33,175 +33,127 @@
</el-text>
</el-card>
<el-card style="margin-bottom:16px">
<el-card
v-for="vendor in filteredVendorDefs"
:key="vendor.key"
style="margin-bottom:16px"
>
<template #header>
<div style="display:flex;justify-content:space-between;align-items:center">
<span>厂商凭据</span>
<el-button type="primary" size="small" :loading="savingVendors" @click="saveVendors">保存凭据</el-button>
<span>{{ vendor.label }}</span>
<el-button type="primary" size="small" :loading="!!savingMap[vendor.key]" @click="saveVendorConfig(vendor.key)">保存</el-button>
</div>
</template>
<div class="vendor-grid">
<el-card v-for="vendor in filteredVendorDefs" :key="vendor.key" shadow="never" class="vendor-card">
<template #header>{{ vendor.label }}</template>
<div class="vendor-hint">{{ vendor.hint }}</div>
<el-form :label-position="isMobile ? 'top' : 'right'" label-width="110px">
<el-form-item v-for="field in vendor.fields" :key="field.key" :label="field.label">
<el-input
v-if="field.type === 'textarea'"
v-model="vendorModel(vendor.key)[field.key]"
type="textarea"
:rows="field.rows ?? 4"
:placeholder="field.placeholder"
/>
<el-switch
v-else-if="field.type === 'switch'"
v-model="vendorModel(vendor.key)[field.key]"
/>
<el-input
v-else
v-model="vendorModel(vendor.key)[field.key]"
:placeholder="field.placeholder"
/>
</el-form-item>
</el-form>
</el-card>
<div class="vendor-hint">{{ vendor.hint }}</div>
<el-form :label-position="isMobile ? 'top' : 'right'" label-width="110px" style="max-width:640px">
<el-form-item v-for="field in vendor.fields" :key="field.key" :label="field.label">
<el-input
v-if="field.type === 'textarea'"
v-model="vendorModel(vendor.key)[field.key]"
type="textarea"
:rows="field.rows ?? 4"
:placeholder="field.placeholder"
/>
<el-switch
v-else-if="field.type === 'switch'"
v-model="vendorModel(vendor.key)[field.key]"
/>
<el-input
v-else
v-model="vendorModel(vendor.key)[field.key]"
:placeholder="field.placeholder"
/>
</el-form-item>
</el-form>
<el-divider content-position="left" style="margin:16px 0 12px">
<span style="font-size:13px;color:#606266">通知渠道 Profiles</span>
<el-button text type="primary" size="small" style="margin-left:8px" @click="addProfile(vendor.key)">+ 添加</el-button>
</el-divider>
<div v-if="vendorProfilesWithIndex(vendor.key).length === 0" style="padding:4px 0 8px;color:#909399;font-size:13px">
暂无 Profiles点击+ 添加创建
</div>
</el-card>
<el-card>
<template #header>
<div class="profiles-header">
<span>平台 Profiles</span>
<div class="profiles-actions">
<el-button size="small" @click="addProfile()">新增通用 profile</el-button>
<el-button size="small" @click="addProfile('xiaomi')">新增小米</el-button>
<el-button size="small" @click="addProfile('huawei')">新增华为</el-button>
<el-button size="small" @click="addProfile('honor')">新增荣耀</el-button>
<el-button size="small" @click="addProfile('oppo')">新增 OPPO</el-button>
<el-button size="small" @click="addProfile('vivo')">新增 vivo</el-button>
<el-button size="small" @click="addProfile('harmony')">新增鸿蒙</el-button>
<el-button size="small" @click="addProfile('apns')">新增 iOS</el-button>
</div>
</div>
</template>
<el-alert
description="routeType 为空时表示默认兜底 profile。你可以先建 3 条,后续随时补充剩余的 2 条,或者删除不再使用的 profile。"
type="info"
:closable="false"
:show-icon="false"
style="margin-bottom:12px"
/>
<el-table :data="pushConfig.profiles" border style="margin-bottom:16px" class="profiles-table">
<el-table-column label="启用" width="78" align="center">
<el-table
v-else
:data="vendorProfilesWithIndex(vendor.key)"
border
size="small"
style="margin-bottom:0"
>
<el-table-column label="启用" width="64" align="center">
<template #default="{ row }">
<el-switch v-model="row.enabled" />
<el-switch v-model="row.profile.enabled" size="small" />
</template>
</el-table-column>
<el-table-column label="厂商" min-width="120">
<el-table-column label="场景" min-width="170">
<template #default="{ row }">
<el-select v-model="row.vendor" filterable>
<el-option v-for="vendor in vendorOptions" :key="vendor.value" :label="vendor.label" :value="vendor.value" />
<el-select v-model="row.profile.routeType" filterable allow-create default-first-option size="small" placeholder="IM_MESSAGE / SYSTEM_NOTICE…">
<el-option v-for="rt in routeTypeOptions" :key="rt" :label="rt" :value="rt" />
</el-select>
</template>
</el-table-column>
<el-table-column label="场景 RouteType" min-width="180">
<el-table-column label="Channel ID" min-width="150">
<template #default="{ row }">
<el-select
v-model="row.routeType"
filterable
allow-create
default-first-option
placeholder="例如 IM_MESSAGE / SYSTEM_NOTICE"
>
<el-option v-for="route in routeTypeOptions" :key="route" :label="route" :value="route" />
</el-select>
<el-input v-model="row.profile.channelId" size="small" placeholder="通知通道 ID" />
</template>
</el-table-column>
<el-table-column label="Profile Key" min-width="180">
<el-table-column label="Category" min-width="130">
<template #default="{ row }">
<el-input v-model="row.profileKey" placeholder="例如 xiaomi_im_message_v1" />
<el-input v-model="row.profile.category" size="small" placeholder="消息分类" />
</template>
</el-table-column>
<el-table-column label="Channel ID" min-width="180">
<el-table-column label="重要性" width="105">
<template #default="{ row }">
<el-input v-model="row.channelId" placeholder="厂商 Channel ID / 通知通道 ID" />
</template>
</el-table-column>
<el-table-column label="Category" min-width="160">
<template #default="{ row }">
<el-input v-model="row.category" placeholder="MESSAGE / SYSTEM / ..." />
</template>
</el-table-column>
<el-table-column label="重要性" width="120">
<template #default="{ row }">
<el-select v-model="row.importance">
<el-select v-model="row.profile.importance" size="small">
<el-option v-for="item in importanceOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</template>
</el-table-column>
<el-table-column label="优先级" width="120">
<el-table-column label="角标" width="58" align="center">
<template #default="{ row }">
<el-select v-model="row.priority">
<el-option v-for="item in priorityOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-switch v-model="row.profile.badge" size="small" />
</template>
</el-table-column>
<el-table-column label="角标" width="78" align="center">
<el-table-column label="声音" width="58" align="center">
<template #default="{ row }">
<el-switch v-model="row.badge" />
<el-switch v-model="row.profile.sound" size="small" />
</template>
</el-table-column>
<el-table-column label="声音" width="78" align="center">
<el-table-column label="振动" width="58" align="center">
<template #default="{ row }">
<el-switch v-model="row.sound" />
<el-switch v-model="row.profile.vibration" size="small" />
</template>
</el-table-column>
<el-table-column label="振动" width="78" align="center">
<el-table-column v-if="vendor.key === 'apns'" label="Thread ID" min-width="130">
<template #default="{ row }">
<el-switch v-model="row.vibration" />
<el-input v-model="row.profile.threadIdentifier" size="small" placeholder="iOS thread-id" />
</template>
</el-table-column>
<el-table-column label="Thread ID" min-width="150">
<el-table-column v-if="vendor.key === 'apns'" label="中断级别" width="130">
<template #default="{ row }">
<el-input v-model="row.threadIdentifier" placeholder="iOS thread-id" />
</template>
</el-table-column>
<el-table-column label="中断级别" min-width="150">
<template #default="{ row }">
<el-select v-model="row.interruptionLevel" clearable placeholder="默认">
<el-select v-model="row.profile.interruptionLevel" size="small" clearable placeholder="默认">
<el-option v-for="item in interruptionOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</template>
</el-table-column>
<el-table-column label="NotifyType" width="120">
<el-table-column label="备注" min-width="130">
<template #default="{ row }">
<el-input-number v-model="row.notifyType" :min="0" :max="10" controls-position="right" style="width:100%" />
<el-input v-model="row.profile.remark" size="small" placeholder="可选说明" />
</template>
</el-table-column>
<el-table-column label="版本" width="92">
<el-table-column label="" width="56" fixed="right">
<template #default="{ row }">
<el-input-number v-model="row.version" :min="1" controls-position="right" style="width:74px" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="200">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="可选说明" />
</template>
</el-table-column>
<el-table-column label="操作" width="84" fixed="right">
<template #default="{ $index }">
<el-button link type="danger" @click="removeProfile($index)">删除</el-button>
<el-button link type="danger" size="small" @click="removeProfile(row.index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="toolbar">
<el-button @click="reloadConfig" :loading="loading">刷新</el-button>
<el-button type="primary" @click="saveProfiles" :loading="savingProfiles">保存 Profiles</el-button>
</div>
</el-card>
<div style="margin-bottom:24px">
<el-button :loading="loading" @click="reloadConfig">刷新配置</el-button>
</div>
</div>
</template>
@ -266,8 +218,7 @@ const route = useRoute()
const app = ref<App | null>(null)
const services = ref<FeatureService[]>([])
const loading = ref(false)
const savingVendors = ref(false)
const savingProfiles = ref(false)
const savingMap = reactive<Record<string, boolean>>({})
const isMobile = ref(window.innerWidth < 768)
const selectedPlatform = ref<'ANDROID' | 'IOS' | 'HARMONY'>('ANDROID')
@ -562,32 +513,24 @@ function buildPayload(): PushServiceConfig {
}
}
async function saveVendors() {
async function saveVendorConfig(vendorKey: PushVendorKey) {
if (!app.value) return
savingVendors.value = true
savingMap[vendorKey] = true
try {
await appApi.updateServiceConfig(app.value.appKey, selectedPlatform.value, 'PUSH', { pushConfig: buildPayload() })
ElMessage.success('厂商凭据已保存')
ElMessage.success('配置已保存')
await loadData()
} catch {
ElMessage.error('保存失败')
} finally {
savingVendors.value = false
savingMap[vendorKey] = false
}
}
async function saveProfiles() {
if (!app.value) return
savingProfiles.value = true
try {
await appApi.updateServiceConfig(app.value.appKey, selectedPlatform.value, 'PUSH', { pushConfig: buildPayload() })
ElMessage.success('Profiles 已保存')
await loadData()
} catch {
ElMessage.error('保存失败')
} finally {
savingProfiles.value = false
}
function vendorProfilesWithIndex(vendorKey: PushVendorKey) {
return pushConfig.profiles
.map((profile, index) => ({ profile, index }))
.filter(({ profile }) => profile.vendor === vendorKey)
}
function addProfile(vendor: PushVendorKey = 'xiaomi') {
@ -627,16 +570,6 @@ onBeforeUnmount(() => window.removeEventListener('resize', updateViewport))
overflow-x: auto;
}
.vendor-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
}
.vendor-card {
min-height: 100%;
}
.vendor-hint {
margin-bottom: 12px;
font-size: 12px;
@ -644,44 +577,14 @@ onBeforeUnmount(() => window.removeEventListener('resize', updateViewport))
line-height: 1.5;
}
.profiles-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.profiles-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.toolbar {
margin-top: 16px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.profiles-table :deep(.el-select),
.profiles-table :deep(.el-input),
.profiles-table :deep(.el-input-number) {
/* Profile 嵌入表格内的 select / input 撑满列宽 */
:deep(.el-table .el-select),
:deep(.el-table .el-input) {
width: 100%;
}
@media (max-width: 767px) {
.vendor-grid {
grid-template-columns: 1fr;
}
.toolbar :deep(.el-button),
.profiles-actions :deep(.el-button) {
width: 100%;
}
.vendor-card :deep(.el-form-item__label) {
:deep(.el-form-item__label) {
padding-bottom: 4px;
}
}