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-text>
</el-card> </el-card>
<el-card style="margin-bottom:16px"> <el-card
v-for="vendor in filteredVendorDefs"
:key="vendor.key"
style="margin-bottom:16px"
>
<template #header> <template #header>
<div style="display:flex;justify-content:space-between;align-items:center"> <div style="display:flex;justify-content:space-between;align-items:center">
<span>厂商凭据</span> <span>{{ vendor.label }}</span>
<el-button type="primary" size="small" :loading="savingVendors" @click="saveVendors">保存凭据</el-button> <el-button type="primary" size="small" :loading="!!savingMap[vendor.key]" @click="saveVendorConfig(vendor.key)">保存</el-button>
</div> </div>
</template> </template>
<div class="vendor-grid">
<el-card v-for="vendor in filteredVendorDefs" :key="vendor.key" shadow="never" class="vendor-card"> <div class="vendor-hint">{{ vendor.hint }}</div>
<template #header>{{ vendor.label }}</template> <el-form :label-position="isMobile ? 'top' : 'right'" label-width="110px" style="max-width:640px">
<div class="vendor-hint">{{ vendor.hint }}</div> <el-form-item v-for="field in vendor.fields" :key="field.key" :label="field.label">
<el-form :label-position="isMobile ? 'top' : 'right'" label-width="110px"> <el-input
<el-form-item v-for="field in vendor.fields" :key="field.key" :label="field.label"> v-if="field.type === 'textarea'"
<el-input v-model="vendorModel(vendor.key)[field.key]"
v-if="field.type === 'textarea'" type="textarea"
v-model="vendorModel(vendor.key)[field.key]" :rows="field.rows ?? 4"
type="textarea" :placeholder="field.placeholder"
:rows="field.rows ?? 4" />
:placeholder="field.placeholder" <el-switch
/> v-else-if="field.type === 'switch'"
<el-switch v-model="vendorModel(vendor.key)[field.key]"
v-else-if="field.type === 'switch'" />
v-model="vendorModel(vendor.key)[field.key]" <el-input
/> v-else
<el-input v-model="vendorModel(vendor.key)[field.key]"
v-else :placeholder="field.placeholder"
v-model="vendorModel(vendor.key)[field.key]" />
:placeholder="field.placeholder" </el-form-item>
/> </el-form>
</el-form-item>
</el-form> <el-divider content-position="left" style="margin:16px 0 12px">
</el-card> <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> </div>
</el-card> <el-table
v-else
<el-card> :data="vendorProfilesWithIndex(vendor.key)"
<template #header> border
<div class="profiles-header"> size="small"
<span>平台 Profiles</span> style="margin-bottom:0"
<div class="profiles-actions"> >
<el-button size="small" @click="addProfile()">新增通用 profile</el-button> <el-table-column label="启用" width="64" align="center">
<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">
<template #default="{ row }"> <template #default="{ row }">
<el-switch v-model="row.enabled" /> <el-switch v-model="row.profile.enabled" size="small" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="厂商" min-width="120"> <el-table-column label="场景" min-width="170">
<template #default="{ row }"> <template #default="{ row }">
<el-select v-model="row.vendor" filterable> <el-select v-model="row.profile.routeType" filterable allow-create default-first-option size="small" placeholder="IM_MESSAGE / SYSTEM_NOTICE…">
<el-option v-for="vendor in vendorOptions" :key="vendor.value" :label="vendor.label" :value="vendor.value" /> <el-option v-for="rt in routeTypeOptions" :key="rt" :label="rt" :value="rt" />
</el-select> </el-select>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="场景 RouteType" min-width="180"> <el-table-column label="Channel ID" min-width="150">
<template #default="{ row }"> <template #default="{ row }">
<el-select <el-input v-model="row.profile.channelId" size="small" placeholder="通知通道 ID" />
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>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="Profile Key" min-width="180"> <el-table-column label="Category" min-width="130">
<template #default="{ row }"> <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> </template>
</el-table-column> </el-table-column>
<el-table-column label="Channel ID" min-width="180"> <el-table-column label="重要性" width="105">
<template #default="{ row }"> <template #default="{ row }">
<el-input v-model="row.channelId" placeholder="厂商 Channel ID / 通知通道 ID" /> <el-select v-model="row.profile.importance" size="small">
</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-option v-for="item in importanceOptions" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in importanceOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="优先级" width="120"> <el-table-column label="角标" width="58" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-select v-model="row.priority"> <el-switch v-model="row.profile.badge" size="small" />
<el-option v-for="item in priorityOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="角标" width="78" align="center"> <el-table-column label="声音" width="58" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-switch v-model="row.badge" /> <el-switch v-model="row.profile.sound" size="small" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="声音" width="78" align="center"> <el-table-column label="振动" width="58" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-switch v-model="row.sound" /> <el-switch v-model="row.profile.vibration" size="small" />
</template> </template>
</el-table-column> </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 }"> <template #default="{ row }">
<el-switch v-model="row.vibration" /> <el-input v-model="row.profile.threadIdentifier" size="small" placeholder="iOS thread-id" />
</template> </template>
</el-table-column> </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 }"> <template #default="{ row }">
<el-input v-model="row.threadIdentifier" placeholder="iOS thread-id" /> <el-select v-model="row.profile.interruptionLevel" size="small" clearable placeholder="默认">
</template>
</el-table-column>
<el-table-column label="中断级别" min-width="150">
<template #default="{ row }">
<el-select v-model="row.interruptionLevel" clearable placeholder="默认">
<el-option v-for="item in interruptionOptions" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in interruptionOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="NotifyType" width="120"> <el-table-column label="备注" min-width="130">
<template #default="{ row }"> <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> </template>
</el-table-column> </el-table-column>
<el-table-column label="版本" width="92"> <el-table-column label="" width="56" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-input-number v-model="row.version" :min="1" controls-position="right" style="width:74px" /> <el-button link type="danger" size="small" @click="removeProfile(row.index)">删除</el-button>
</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>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </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> </el-card>
<div style="margin-bottom:24px">
<el-button :loading="loading" @click="reloadConfig">刷新配置</el-button>
</div>
</div> </div>
</template> </template>
@ -266,8 +218,7 @@ const route = useRoute()
const app = ref<App | null>(null) const app = ref<App | null>(null)
const services = ref<FeatureService[]>([]) const services = ref<FeatureService[]>([])
const loading = ref(false) const loading = ref(false)
const savingVendors = ref(false) const savingMap = reactive<Record<string, boolean>>({})
const savingProfiles = ref(false)
const isMobile = ref(window.innerWidth < 768) const isMobile = ref(window.innerWidth < 768)
const selectedPlatform = ref<'ANDROID' | 'IOS' | 'HARMONY'>('ANDROID') 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 if (!app.value) return
savingVendors.value = true savingMap[vendorKey] = true
try { try {
await appApi.updateServiceConfig(app.value.appKey, selectedPlatform.value, 'PUSH', { pushConfig: buildPayload() }) await appApi.updateServiceConfig(app.value.appKey, selectedPlatform.value, 'PUSH', { pushConfig: buildPayload() })
ElMessage.success('厂商凭据已保存') ElMessage.success('配置已保存')
await loadData() await loadData()
} catch { } catch {
ElMessage.error('保存失败') ElMessage.error('保存失败')
} finally { } finally {
savingVendors.value = false savingMap[vendorKey] = false
} }
} }
async function saveProfiles() { function vendorProfilesWithIndex(vendorKey: PushVendorKey) {
if (!app.value) return return pushConfig.profiles
savingProfiles.value = true .map((profile, index) => ({ profile, index }))
try { .filter(({ profile }) => profile.vendor === vendorKey)
await appApi.updateServiceConfig(app.value.appKey, selectedPlatform.value, 'PUSH', { pushConfig: buildPayload() })
ElMessage.success('Profiles 已保存')
await loadData()
} catch {
ElMessage.error('保存失败')
} finally {
savingProfiles.value = false
}
} }
function addProfile(vendor: PushVendorKey = 'xiaomi') { function addProfile(vendor: PushVendorKey = 'xiaomi') {
@ -627,16 +570,6 @@ onBeforeUnmount(() => window.removeEventListener('resize', updateViewport))
overflow-x: auto; 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 { .vendor-hint {
margin-bottom: 12px; margin-bottom: 12px;
font-size: 12px; font-size: 12px;
@ -644,44 +577,14 @@ onBeforeUnmount(() => window.removeEventListener('resize', updateViewport))
line-height: 1.5; line-height: 1.5;
} }
.profiles-header { /* Profile 嵌入表格内的 select / input 撑满列宽 */
display: flex; :deep(.el-table .el-select),
align-items: center; :deep(.el-table .el-input) {
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) {
width: 100%; width: 100%;
} }
@media (max-width: 767px) { @media (max-width: 767px) {
.vendor-grid { :deep(.el-form-item__label) {
grid-template-columns: 1fr;
}
.toolbar :deep(.el-button),
.profiles-actions :deep(.el-button) {
width: 100%;
}
.vendor-card :deep(.el-form-item__label) {
padding-bottom: 4px; padding-bottom: 4px;
} }
} }