2026-04-30 09:49:05 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div v-if="app">
|
|
|
|
|
|
<el-page-header @back="$router.back()" content="推送服务配置" style="margin-bottom:24px" />
|
|
|
|
|
|
|
|
|
|
|
|
<el-card class="info-card" style="margin-bottom:16px">
|
|
|
|
|
|
<el-descriptions :column="isMobile ? 1 : 2" border>
|
|
|
|
|
|
<el-descriptions-item label="应用名称">{{ app.name }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="包名">{{ app.packageName }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="AppKey">
|
|
|
|
|
|
<el-text class="mono">{{ app.appKey }}</el-text>
|
|
|
|
|
|
<el-button link @click="copy(app.appKey)"><el-icon><CopyDocument /></el-icon></el-button>
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
|
|
<el-alert
|
2026-05-08 18:32:00 +08:00
|
|
|
|
title="新推送模型按 vendors + profiles 管理。厂商凭据只负责连厂商,profiles 负责按场景配置 Channel ID、Category、Importance、角标、声音、振动等。"
|
2026-04-30 09:49:05 +08:00
|
|
|
|
type="info"
|
|
|
|
|
|
:closable="false"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
style="margin-bottom:16px"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-06-26 14:12:03 +08:00
|
|
|
|
<el-card style="margin-bottom:16px">
|
|
|
|
|
|
<template #header>目标平台</template>
|
|
|
|
|
|
<el-radio-group v-model="selectedPlatform" @change="applySelectedPlatformConfig">
|
|
|
|
|
|
<el-radio-button value="ANDROID">Android</el-radio-button>
|
|
|
|
|
|
<el-radio-button value="IOS">iOS</el-radio-button>
|
|
|
|
|
|
<el-radio-button value="HARMONY">HarmonyOS</el-radio-button>
|
|
|
|
|
|
</el-radio-group>
|
|
|
|
|
|
<el-text type="info" style="margin-left:16px;font-size:12px">
|
|
|
|
|
|
当前编辑:{{ selectedPlatform }} 平台的推送配置,保存时仅更新该平台
|
|
|
|
|
|
</el-text>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-card
|
|
|
|
|
|
v-for="vendor in filteredVendorDefs"
|
|
|
|
|
|
:key="vendor.key"
|
|
|
|
|
|
style="margin-bottom:16px"
|
|
|
|
|
|
>
|
2026-06-26 14:27:06 +08:00
|
|
|
|
<template #header>
|
|
|
|
|
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<span>{{ vendor.label }}</span>
|
|
|
|
|
|
<el-button type="primary" size="small" :loading="!!savingMap[vendor.key]" @click="saveVendorConfig(vendor.key)">保存</el-button>
|
2026-06-26 14:27:06 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-05-08 18:32:00 +08:00
|
|
|
|
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<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-table
|
|
|
|
|
|
v-else
|
|
|
|
|
|
:data="vendorProfilesWithIndex(vendor.key)"
|
|
|
|
|
|
border
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
style="margin-bottom:0"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-table-column label="启用" width="64" align="center">
|
2026-05-05 22:16:11 +08:00
|
|
|
|
<template #default="{ row }">
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-switch v-model="row.profile.enabled" size="small" />
|
2026-05-08 18:32:00 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-table-column label="场景" min-width="170">
|
2026-05-08 18:32:00 +08:00
|
|
|
|
<template #default="{ row }">
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<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" />
|
2026-05-08 18:32:00 +08:00
|
|
|
|
</el-select>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-table-column label="Channel ID" min-width="150">
|
2026-05-05 22:16:11 +08:00
|
|
|
|
<template #default="{ row }">
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-input v-model="row.profile.channelId" size="small" placeholder="通知通道 ID" />
|
2026-05-05 22:16:11 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-table-column label="Category" min-width="130">
|
2026-05-05 22:16:11 +08:00
|
|
|
|
<template #default="{ row }">
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-input v-model="row.profile.category" size="small" placeholder="消息分类" />
|
2026-05-05 22:16:11 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-table-column label="重要性" width="105">
|
2026-05-05 22:16:11 +08:00
|
|
|
|
<template #default="{ row }">
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-select v-model="row.profile.importance" size="small">
|
2026-05-05 22:16:11 +08:00
|
|
|
|
<el-option v-for="item in importanceOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-table-column label="角标" width="58" align="center">
|
2026-05-08 18:32:00 +08:00
|
|
|
|
<template #default="{ row }">
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-switch v-model="row.profile.badge" size="small" />
|
2026-05-08 18:32:00 +08:00
|
|
|
|
</template>
|
2026-05-05 22:16:11 +08:00
|
|
|
|
</el-table-column>
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-table-column label="声音" width="58" align="center">
|
2026-05-08 18:32:00 +08:00
|
|
|
|
<template #default="{ row }">
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-switch v-model="row.profile.sound" size="small" />
|
2026-05-08 18:32:00 +08:00
|
|
|
|
</template>
|
2026-05-05 22:16:11 +08:00
|
|
|
|
</el-table-column>
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-table-column label="振动" width="58" align="center">
|
2026-05-08 18:32:00 +08:00
|
|
|
|
<template #default="{ row }">
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-switch v-model="row.profile.vibration" size="small" />
|
2026-05-08 18:32:00 +08:00
|
|
|
|
</template>
|
2026-05-05 22:16:11 +08:00
|
|
|
|
</el-table-column>
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-table-column v-if="vendor.key === 'apns'" label="Thread ID" min-width="130">
|
2026-05-08 18:32:00 +08:00
|
|
|
|
<template #default="{ row }">
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-input v-model="row.profile.threadIdentifier" size="small" placeholder="iOS thread-id" />
|
2026-05-05 22:16:11 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-table-column v-if="vendor.key === 'apns'" label="中断级别" width="130">
|
2026-05-08 18:32:00 +08:00
|
|
|
|
<template #default="{ row }">
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-select v-model="row.profile.interruptionLevel" size="small" clearable placeholder="默认">
|
2026-05-08 18:32:00 +08:00
|
|
|
|
<el-option v-for="item in interruptionOptions" :key="item.value" :label="item.label" :value="item.value" />
|
2026-05-05 22:16:11 +08:00
|
|
|
|
</el-select>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-table-column label="备注" min-width="130">
|
2026-05-05 22:16:11 +08:00
|
|
|
|
<template #default="{ row }">
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-input v-model="row.profile.remark" size="small" placeholder="可选说明" />
|
2026-05-08 18:32:00 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-table-column label="" width="56" fixed="right">
|
2026-05-08 18:32:00 +08:00
|
|
|
|
<template #default="{ row }">
|
2026-06-26 16:29:36 +08:00
|
|
|
|
<el-button link type="danger" size="small" @click="removeProfile(row.index)">删除</el-button>
|
2026-05-05 22:16:11 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
2026-04-30 09:49:05 +08:00
|
|
|
|
</el-card>
|
2026-06-26 16:29:36 +08:00
|
|
|
|
|
|
|
|
|
|
<div style="margin-bottom:24px">
|
|
|
|
|
|
<el-button :loading="loading" @click="reloadConfig">刷新配置</el-button>
|
|
|
|
|
|
</div>
|
2026-04-30 09:49:05 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-06-26 14:23:17 +08:00
|
|
|
|
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
2026-04-30 09:49:05 +08:00
|
|
|
|
import { useRoute } from 'vue-router'
|
|
|
|
|
|
import { CopyDocument } from '@element-plus/icons-vue'
|
2026-06-25 17:53:09 +08:00
|
|
|
|
import { ElMessage } from 'element-plus'
|
2026-05-05 22:16:11 +08:00
|
|
|
|
import {
|
|
|
|
|
|
appApi,
|
|
|
|
|
|
type App,
|
|
|
|
|
|
type FeatureService,
|
2026-05-08 18:32:00 +08:00
|
|
|
|
type ApnsPushVendorConfig,
|
|
|
|
|
|
type HarmonyPushVendorConfig,
|
|
|
|
|
|
type HonorPushVendorConfig,
|
|
|
|
|
|
type HuaweiPushVendorConfig,
|
|
|
|
|
|
type OppoPushVendorConfig,
|
|
|
|
|
|
type PushImportance,
|
|
|
|
|
|
type PushInterruptionLevel,
|
|
|
|
|
|
type PushPriority,
|
|
|
|
|
|
type PushProfileConfig,
|
2026-05-05 22:16:11 +08:00
|
|
|
|
type PushServiceConfig,
|
2026-05-08 18:32:00 +08:00
|
|
|
|
type PushVendorKey,
|
|
|
|
|
|
type XiaomiPushVendorConfig,
|
|
|
|
|
|
type VivoPushVendorConfig,
|
2026-05-05 22:16:11 +08:00
|
|
|
|
} from '@/api/app'
|
2026-04-30 09:49:05 +08:00
|
|
|
|
|
2026-05-08 18:32:00 +08:00
|
|
|
|
type PushVendorsState = {
|
|
|
|
|
|
huawei: HuaweiPushVendorConfig
|
|
|
|
|
|
xiaomi: XiaomiPushVendorConfig
|
|
|
|
|
|
oppo: OppoPushVendorConfig
|
|
|
|
|
|
vivo: VivoPushVendorConfig
|
|
|
|
|
|
honor: HonorPushVendorConfig
|
|
|
|
|
|
harmony: HarmonyPushVendorConfig
|
|
|
|
|
|
apns: ApnsPushVendorConfig
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 09:49:05 +08:00
|
|
|
|
type FieldDef = {
|
|
|
|
|
|
key: string
|
|
|
|
|
|
label: string
|
2026-05-05 17:54:59 +08:00
|
|
|
|
type?: 'textarea' | 'switch'
|
2026-04-30 09:49:05 +08:00
|
|
|
|
placeholder?: string
|
|
|
|
|
|
rows?: number
|
|
|
|
|
|
}
|
2026-05-08 18:32:00 +08:00
|
|
|
|
|
2026-04-30 09:49:05 +08:00
|
|
|
|
type VendorDef = {
|
2026-05-08 18:32:00 +08:00
|
|
|
|
key: PushVendorKey
|
2026-04-30 09:49:05 +08:00
|
|
|
|
label: string
|
|
|
|
|
|
hint: string
|
|
|
|
|
|
fields: FieldDef[]
|
2026-06-26 14:23:17 +08:00
|
|
|
|
platforms: Array<'ANDROID' | 'IOS' | 'HARMONY'>
|
2026-04-30 09:49:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 18:32:00 +08:00
|
|
|
|
type PushConfigState = {
|
|
|
|
|
|
schemaVersion: number
|
|
|
|
|
|
updatedAt: string
|
|
|
|
|
|
vendors: PushVendorsState
|
|
|
|
|
|
profiles: PushProfileConfig[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 09:49:05 +08:00
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
const app = ref<App | null>(null)
|
|
|
|
|
|
const services = ref<FeatureService[]>([])
|
|
|
|
|
|
const loading = ref(false)
|
2026-06-26 16:29:36 +08:00
|
|
|
|
const savingMap = reactive<Record<string, boolean>>({})
|
2026-04-30 09:49:05 +08:00
|
|
|
|
const isMobile = ref(window.innerWidth < 768)
|
2026-05-05 22:26:32 +08:00
|
|
|
|
const selectedPlatform = ref<'ANDROID' | 'IOS' | 'HARMONY'>('ANDROID')
|
2026-05-08 18:32:00 +08:00
|
|
|
|
|
|
|
|
|
|
const vendorOptions: Array<{ label: string; value: PushVendorKey }> = [
|
|
|
|
|
|
{ label: '华为', value: 'huawei' },
|
|
|
|
|
|
{ label: '小米', value: 'xiaomi' },
|
|
|
|
|
|
{ label: 'OPPO', value: 'oppo' },
|
|
|
|
|
|
{ label: 'vivo', value: 'vivo' },
|
|
|
|
|
|
{ label: '荣耀', value: 'honor' },
|
|
|
|
|
|
{ label: '鸿蒙', value: 'harmony' },
|
|
|
|
|
|
{ label: 'iOS', value: 'apns' },
|
|
|
|
|
|
]
|
2026-04-30 09:49:05 +08:00
|
|
|
|
|
2026-05-08 18:32:00 +08:00
|
|
|
|
const routeTypeOptions = [
|
|
|
|
|
|
'IM_MESSAGE',
|
|
|
|
|
|
'FRIEND_REQUEST',
|
|
|
|
|
|
'SYSTEM_NOTICE',
|
|
|
|
|
|
'SERVICE_NOTICE',
|
|
|
|
|
|
'MARKETING',
|
|
|
|
|
|
'DEFAULT',
|
|
|
|
|
|
]
|
2026-05-05 22:16:11 +08:00
|
|
|
|
|
|
|
|
|
|
const importanceOptions = [
|
|
|
|
|
|
{ label: '最小', value: 'MIN' },
|
|
|
|
|
|
{ label: '低', value: 'LOW' },
|
|
|
|
|
|
{ label: '默认', value: 'DEFAULT' },
|
|
|
|
|
|
{ label: '高', value: 'HIGH' },
|
|
|
|
|
|
{ label: '最高', value: 'MAX' },
|
2026-05-08 18:32:00 +08:00
|
|
|
|
] as const satisfies ReadonlyArray<{ label: string; value: PushImportance }>
|
2026-05-05 22:16:11 +08:00
|
|
|
|
|
2026-05-08 18:32:00 +08:00
|
|
|
|
const priorityOptions = [
|
|
|
|
|
|
{ label: '低', value: 'LOW' },
|
|
|
|
|
|
{ label: '默认', value: 'DEFAULT' },
|
|
|
|
|
|
{ label: '高', value: 'HIGH' },
|
|
|
|
|
|
] as const satisfies ReadonlyArray<{ label: string; value: PushPriority }>
|
|
|
|
|
|
|
|
|
|
|
|
const interruptionOptions = [
|
|
|
|
|
|
{ label: 'Passive', value: 'passive' },
|
|
|
|
|
|
{ label: 'Active', value: 'active' },
|
|
|
|
|
|
{ label: 'Time Sensitive', value: 'time-sensitive' },
|
|
|
|
|
|
{ label: 'Critical', value: 'critical' },
|
|
|
|
|
|
] as const satisfies ReadonlyArray<{ label: string; value: PushInterruptionLevel }>
|
|
|
|
|
|
|
|
|
|
|
|
const vendors = reactive<PushVendorsState>(createEmptyVendors())
|
|
|
|
|
|
const pushConfig = reactive<PushConfigState>({
|
|
|
|
|
|
schemaVersion: 2,
|
|
|
|
|
|
updatedAt: '',
|
|
|
|
|
|
vendors,
|
|
|
|
|
|
profiles: [],
|
|
|
|
|
|
})
|
2026-04-30 09:49:05 +08:00
|
|
|
|
|
|
|
|
|
|
const vendorDefs: VendorDef[] = [
|
2026-05-08 18:32:00 +08:00
|
|
|
|
{
|
|
|
|
|
|
key: 'xiaomi',
|
|
|
|
|
|
label: '小米 MiPush',
|
|
|
|
|
|
hint: '这里仅填写厂商凭据。Channel ID 放在 profiles 中按场景单独配置,支持后续补充或删除。',
|
2026-06-26 14:23:17 +08:00
|
|
|
|
platforms: ['ANDROID'],
|
2026-05-08 18:32:00 +08:00
|
|
|
|
fields: [
|
|
|
|
|
|
{ key: 'appId', label: 'AppId' },
|
|
|
|
|
|
{ key: 'appKey', label: 'AppKey' },
|
|
|
|
|
|
{ key: 'appSecret', label: 'AppSecret' },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
2026-04-30 09:49:05 +08:00
|
|
|
|
{
|
|
|
|
|
|
key: 'huawei',
|
|
|
|
|
|
label: '华为 HMS',
|
2026-05-08 18:32:00 +08:00
|
|
|
|
hint: '仅保留厂商账号信息。Category、Channel ID、重要性等由 profiles 负责。',
|
2026-06-26 14:23:17 +08:00
|
|
|
|
platforms: ['ANDROID'],
|
2026-04-30 09:49:05 +08:00
|
|
|
|
fields: [
|
|
|
|
|
|
{ key: 'appId', label: 'AppId' },
|
2026-05-05 17:54:59 +08:00
|
|
|
|
{ key: 'appSecret', label: 'AppSecret' },
|
2026-04-30 09:49:05 +08:00
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-05-08 18:32:00 +08:00
|
|
|
|
key: 'honor',
|
|
|
|
|
|
label: '荣耀 Push',
|
|
|
|
|
|
hint: '荣耀账号凭据。业务场景与展示策略在 profiles 中配置。',
|
2026-06-26 14:23:17 +08:00
|
|
|
|
platforms: ['ANDROID'],
|
2026-04-30 09:49:05 +08:00
|
|
|
|
fields: [
|
|
|
|
|
|
{ key: 'appId', label: 'AppId' },
|
2026-05-08 18:32:00 +08:00
|
|
|
|
{ key: 'clientId', label: 'ClientId' },
|
|
|
|
|
|
{ key: 'clientSecret', label: 'ClientSecret' },
|
2026-04-30 09:49:05 +08:00
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'oppo',
|
|
|
|
|
|
label: 'OPPO 推送',
|
2026-05-08 18:32:00 +08:00
|
|
|
|
hint: '仅保留 AppKey / MasterSecret。通道配置与优先级在 profiles 中维护。',
|
2026-06-26 14:23:17 +08:00
|
|
|
|
platforms: ['ANDROID'],
|
2026-04-30 09:49:05 +08:00
|
|
|
|
fields: [
|
|
|
|
|
|
{ key: 'appId', label: 'AppId' },
|
|
|
|
|
|
{ key: 'appKey', label: 'AppKey' },
|
2026-05-05 17:54:59 +08:00
|
|
|
|
{ key: 'masterSecret', label: 'MasterSecret' },
|
2026-04-30 09:49:05 +08:00
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'vivo',
|
|
|
|
|
|
label: 'vivo 推送',
|
2026-05-08 18:32:00 +08:00
|
|
|
|
hint: '厂商凭据与业务分类分离。classification 由 profiles 的 Category 驱动。',
|
2026-06-26 14:23:17 +08:00
|
|
|
|
platforms: ['ANDROID'],
|
2026-04-30 09:49:05 +08:00
|
|
|
|
fields: [
|
|
|
|
|
|
{ key: 'appId', label: 'AppId' },
|
|
|
|
|
|
{ key: 'appKey', label: 'AppKey' },
|
2026-05-05 17:54:59 +08:00
|
|
|
|
{ key: 'appSecret', label: 'AppSecret' },
|
2026-04-30 09:49:05 +08:00
|
|
|
|
],
|
|
|
|
|
|
},
|
2026-05-05 22:26:32 +08:00
|
|
|
|
{
|
|
|
|
|
|
key: 'harmony',
|
|
|
|
|
|
label: '鸿蒙 Push Kit',
|
2026-05-08 18:32:00 +08:00
|
|
|
|
hint: '鸿蒙厂商凭据。需要的 Category / Channel ID 放到 profiles 中。',
|
2026-06-26 14:23:17 +08:00
|
|
|
|
platforms: ['HARMONY'],
|
2026-05-05 22:26:32 +08:00
|
|
|
|
fields: [
|
|
|
|
|
|
{ key: 'appId', label: 'AppId' },
|
|
|
|
|
|
{ key: 'appSecret', label: 'AppSecret' },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
2026-04-30 09:49:05 +08:00
|
|
|
|
{
|
|
|
|
|
|
key: 'apns',
|
2026-05-08 18:32:00 +08:00
|
|
|
|
label: 'iOS APNs',
|
|
|
|
|
|
hint: 'Team ID / Key ID / Bundle ID / 私钥统一放这里。Badge、Thread ID、Interruption Level 放在 profiles 中。',
|
2026-06-26 14:23:17 +08:00
|
|
|
|
platforms: ['IOS'],
|
2026-04-30 09:49:05 +08:00
|
|
|
|
fields: [
|
|
|
|
|
|
{ key: 'teamId', label: 'Team ID' },
|
|
|
|
|
|
{ key: 'keyId', label: 'Key ID' },
|
|
|
|
|
|
{ key: 'bundleId', label: 'Bundle ID' },
|
2026-05-08 18:32:00 +08:00
|
|
|
|
{ key: 'privateKey', label: 'Private Key', type: 'textarea', rows: 6, placeholder: '粘贴 .p8 内容' },
|
2026-04-30 09:49:05 +08:00
|
|
|
|
{ key: 'sandbox', label: 'Sandbox', type: 'switch' },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-06-26 14:23:17 +08:00
|
|
|
|
const filteredVendorDefs = computed(() =>
|
|
|
|
|
|
vendorDefs.filter(v => v.platforms.includes(selectedPlatform.value)),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-08 18:32:00 +08:00
|
|
|
|
function updateViewport() {
|
|
|
|
|
|
isMobile.value = window.innerWidth < 768
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createEmptyVendors(): PushVendorsState {
|
|
|
|
|
|
return {
|
|
|
|
|
|
huawei: { appId: '', appSecret: '' },
|
|
|
|
|
|
xiaomi: { appId: '', appKey: '', appSecret: '' },
|
|
|
|
|
|
oppo: { appId: '', appKey: '', masterSecret: '' },
|
|
|
|
|
|
vivo: { appId: '', appKey: '', appSecret: '' },
|
|
|
|
|
|
honor: { appId: '', clientId: '', clientSecret: '' },
|
|
|
|
|
|
harmony: { appId: '', appSecret: '' },
|
|
|
|
|
|
apns: { teamId: '', keyId: '', bundleId: '', privateKey: '', sandbox: false },
|
2026-05-05 22:26:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 18:32:00 +08:00
|
|
|
|
function createEmptyState(): PushConfigState {
|
|
|
|
|
|
return {
|
|
|
|
|
|
schemaVersion: 2,
|
|
|
|
|
|
updatedAt: '',
|
|
|
|
|
|
vendors: createEmptyVendors(),
|
|
|
|
|
|
profiles: [],
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createProfile(vendor: PushVendorKey = 'xiaomi', routeType = ''): PushProfileConfig {
|
|
|
|
|
|
return {
|
|
|
|
|
|
profileKey: `${vendor}_${routeType || 'default'}_${Date.now()}`,
|
|
|
|
|
|
vendor,
|
|
|
|
|
|
routeType,
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
channelId: '',
|
|
|
|
|
|
category: '',
|
|
|
|
|
|
importance: 'DEFAULT',
|
|
|
|
|
|
priority: 'DEFAULT',
|
|
|
|
|
|
threadIdentifier: '',
|
|
|
|
|
|
interruptionLevel: '',
|
|
|
|
|
|
badge: true,
|
|
|
|
|
|
sound: true,
|
|
|
|
|
|
vibration: true,
|
|
|
|
|
|
notifyType: 1,
|
|
|
|
|
|
version: 1,
|
|
|
|
|
|
remark: '',
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function vendorModel(vendor: PushVendorKey): Record<string, any> {
|
|
|
|
|
|
return vendors[vendor] as Record<string, any>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function replaceState(next: PushConfigState) {
|
|
|
|
|
|
Object.assign(pushConfig.vendors.huawei, next.vendors.huawei)
|
|
|
|
|
|
Object.assign(pushConfig.vendors.xiaomi, next.vendors.xiaomi)
|
|
|
|
|
|
Object.assign(pushConfig.vendors.oppo, next.vendors.oppo)
|
|
|
|
|
|
Object.assign(pushConfig.vendors.vivo, next.vendors.vivo)
|
|
|
|
|
|
Object.assign(pushConfig.vendors.honor, next.vendors.honor)
|
|
|
|
|
|
Object.assign(pushConfig.vendors.harmony, next.vendors.harmony)
|
|
|
|
|
|
Object.assign(pushConfig.vendors.apns, next.vendors.apns)
|
|
|
|
|
|
pushConfig.schemaVersion = next.schemaVersion
|
|
|
|
|
|
pushConfig.updatedAt = next.updatedAt
|
|
|
|
|
|
pushConfig.profiles.splice(0, pushConfig.profiles.length, ...next.profiles.map(profile => normalizeProfile(profile)))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeProfile(profile: Partial<PushProfileConfig> & Pick<PushProfileConfig, 'vendor'>): PushProfileConfig {
|
|
|
|
|
|
return {
|
|
|
|
|
|
profileKey: profile.profileKey?.trim() || `${profile.vendor}_${profile.routeType || 'default'}_${Date.now()}`,
|
|
|
|
|
|
vendor: profile.vendor,
|
|
|
|
|
|
routeType: profile.routeType?.trim() || '',
|
|
|
|
|
|
enabled: profile.enabled ?? true,
|
|
|
|
|
|
channelId: profile.channelId?.trim() || '',
|
|
|
|
|
|
category: profile.category?.trim() || '',
|
|
|
|
|
|
importance: profile.importance || 'DEFAULT',
|
|
|
|
|
|
priority: profile.priority || 'DEFAULT',
|
|
|
|
|
|
threadIdentifier: profile.threadIdentifier?.trim() || '',
|
|
|
|
|
|
interruptionLevel: profile.interruptionLevel || '',
|
|
|
|
|
|
badge: profile.badge ?? true,
|
|
|
|
|
|
sound: profile.sound ?? true,
|
|
|
|
|
|
vibration: profile.vibration ?? true,
|
|
|
|
|
|
notifyType: Number.isFinite(profile.notifyType as number) ? Math.max(Number(profile.notifyType ?? 1), 0) : 1,
|
|
|
|
|
|
version: Math.max(Number(profile.version ?? 1), 1),
|
|
|
|
|
|
remark: profile.remark?.trim() || '',
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseConfig(raw?: string | null): PushConfigState {
|
|
|
|
|
|
if (!raw) {
|
|
|
|
|
|
return createEmptyState()
|
|
|
|
|
|
}
|
2026-04-30 09:49:05 +08:00
|
|
|
|
try {
|
2026-05-08 18:32:00 +08:00
|
|
|
|
const parsed = JSON.parse(raw) as Partial<PushConfigState>
|
|
|
|
|
|
return {
|
|
|
|
|
|
schemaVersion: parsed.schemaVersion ?? 2,
|
|
|
|
|
|
updatedAt: parsed.updatedAt ?? '',
|
|
|
|
|
|
vendors: {
|
|
|
|
|
|
...createEmptyVendors(),
|
|
|
|
|
|
...parsed.vendors,
|
|
|
|
|
|
huawei: { ...createEmptyVendors().huawei, ...(parsed.vendors?.huawei ?? {}) },
|
|
|
|
|
|
xiaomi: { ...createEmptyVendors().xiaomi, ...(parsed.vendors?.xiaomi ?? {}) },
|
|
|
|
|
|
oppo: { ...createEmptyVendors().oppo, ...(parsed.vendors?.oppo ?? {}) },
|
|
|
|
|
|
vivo: { ...createEmptyVendors().vivo, ...(parsed.vendors?.vivo ?? {}) },
|
|
|
|
|
|
honor: { ...createEmptyVendors().honor, ...(parsed.vendors?.honor ?? {}) },
|
|
|
|
|
|
harmony: { ...createEmptyVendors().harmony, ...(parsed.vendors?.harmony ?? {}) },
|
|
|
|
|
|
apns: { ...createEmptyVendors().apns, ...(parsed.vendors?.apns ?? {}) },
|
|
|
|
|
|
},
|
|
|
|
|
|
profiles: Array.isArray(parsed.profiles)
|
|
|
|
|
|
? parsed.profiles.map(profile => normalizeProfile(profile as Partial<PushProfileConfig> & Pick<PushProfileConfig, 'vendor'>))
|
|
|
|
|
|
: [],
|
|
|
|
|
|
}
|
2026-04-30 09:49:05 +08:00
|
|
|
|
} catch {
|
2026-05-08 18:32:00 +08:00
|
|
|
|
return createEmptyState()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadData() {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const id = route.params.appKey as string
|
|
|
|
|
|
const [appRes, svcRes] = await Promise.all([
|
|
|
|
|
|
appApi.get(id),
|
|
|
|
|
|
appApi.getServices(id),
|
|
|
|
|
|
])
|
|
|
|
|
|
app.value = appRes.data.data
|
|
|
|
|
|
services.value = svcRes.data.data
|
2026-06-26 14:12:03 +08:00
|
|
|
|
// Always default to ANDROID on load; user can switch via the platform selector
|
|
|
|
|
|
selectedPlatform.value = 'ANDROID'
|
2026-06-26 16:16:08 +08:00
|
|
|
|
await applySelectedPlatformConfig()
|
2026-05-08 18:32:00 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
2026-04-30 09:49:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 16:16:08 +08:00
|
|
|
|
async function applySelectedPlatformConfig() {
|
|
|
|
|
|
if (!app.value) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await appApi.getServiceItem(app.value.appKey, selectedPlatform.value, 'PUSH')
|
|
|
|
|
|
replaceState(parseConfig(res.data.data?.config))
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// 该平台尚无配置,清空表单
|
|
|
|
|
|
replaceState(parseConfig(null))
|
|
|
|
|
|
}
|
2026-05-08 18:32:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 14:27:06 +08:00
|
|
|
|
function buildPayload(): PushServiceConfig {
|
|
|
|
|
|
return {
|
|
|
|
|
|
schemaVersion: 2,
|
|
|
|
|
|
updatedAt: new Date().toISOString(),
|
|
|
|
|
|
vendors: {
|
|
|
|
|
|
huawei: { ...pushConfig.vendors.huawei },
|
|
|
|
|
|
xiaomi: { ...pushConfig.vendors.xiaomi },
|
|
|
|
|
|
oppo: { ...pushConfig.vendors.oppo },
|
|
|
|
|
|
vivo: { ...pushConfig.vendors.vivo },
|
|
|
|
|
|
honor: { ...pushConfig.vendors.honor },
|
|
|
|
|
|
harmony: { ...pushConfig.vendors.harmony },
|
|
|
|
|
|
apns: { ...pushConfig.vendors.apns },
|
|
|
|
|
|
},
|
|
|
|
|
|
profiles: pushConfig.profiles.map(profile => normalizeProfile(profile)),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 16:29:36 +08:00
|
|
|
|
async function saveVendorConfig(vendorKey: PushVendorKey) {
|
2026-04-30 09:49:05 +08:00
|
|
|
|
if (!app.value) return
|
2026-06-26 16:29:36 +08:00
|
|
|
|
savingMap[vendorKey] = true
|
2026-04-30 09:49:05 +08:00
|
|
|
|
try {
|
2026-06-26 14:27:06 +08:00
|
|
|
|
await appApi.updateServiceConfig(app.value.appKey, selectedPlatform.value, 'PUSH', { pushConfig: buildPayload() })
|
2026-06-26 16:29:36 +08:00
|
|
|
|
ElMessage.success('配置已保存')
|
2026-06-26 14:27:06 +08:00
|
|
|
|
await loadData()
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
ElMessage.error('保存失败')
|
|
|
|
|
|
} finally {
|
2026-06-26 16:29:36 +08:00
|
|
|
|
savingMap[vendorKey] = false
|
2026-06-26 14:27:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 16:29:36 +08:00
|
|
|
|
function vendorProfilesWithIndex(vendorKey: PushVendorKey) {
|
|
|
|
|
|
return pushConfig.profiles
|
|
|
|
|
|
.map((profile, index) => ({ profile, index }))
|
|
|
|
|
|
.filter(({ profile }) => profile.vendor === vendorKey)
|
2026-04-30 09:49:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 18:32:00 +08:00
|
|
|
|
function addProfile(vendor: PushVendorKey = 'xiaomi') {
|
|
|
|
|
|
pushConfig.profiles.push(createProfile(vendor))
|
2026-05-05 22:16:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 18:32:00 +08:00
|
|
|
|
function removeProfile(index: number) {
|
|
|
|
|
|
pushConfig.profiles.splice(index, 1)
|
2026-05-05 22:16:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 09:49:05 +08:00
|
|
|
|
function reloadConfig() {
|
|
|
|
|
|
return loadData()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function copy(text: string) {
|
|
|
|
|
|
navigator.clipboard.writeText(text)
|
|
|
|
|
|
ElMessage.success('已复制')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(loadData)
|
|
|
|
|
|
onMounted(() => window.addEventListener('resize', updateViewport))
|
|
|
|
|
|
onBeforeUnmount(() => window.removeEventListener('resize', updateViewport))
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-05-08 18:32:00 +08:00
|
|
|
|
.mono {
|
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hint {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 09:49:05 +08:00
|
|
|
|
.info-card :deep(.el-descriptions__body) {
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
}
|
2026-05-08 18:32:00 +08:00
|
|
|
|
|
2026-04-30 09:49:05 +08:00
|
|
|
|
.vendor-hint {
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
}
|
2026-05-08 18:32:00 +08:00
|
|
|
|
|
2026-06-26 16:29:36 +08:00
|
|
|
|
/* Profile 嵌入表格内的 select / input 撑满列宽 */
|
|
|
|
|
|
:deep(.el-table .el-select),
|
|
|
|
|
|
:deep(.el-table .el-input) {
|
2026-05-08 18:32:00 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 09:49:05 +08:00
|
|
|
|
@media (max-width: 767px) {
|
2026-06-26 16:29:36 +08:00
|
|
|
|
:deep(.el-form-item__label) {
|
2026-04-30 09:49:05 +08:00
|
|
|
|
padding-bottom: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|