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-item label="服务状态">
|
|
|
|
|
|
<el-tag :type="pushEnabled ? 'success' : 'info'">{{ pushEnabled ? '已开通' : '未开通' }}</el-tag>
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
|
<div class="push-switch-row">
|
|
|
|
|
|
<el-switch :model-value="pushEnabled" @change="(val: boolean) => onTogglePushService(val)" />
|
|
|
|
|
|
<span class="hint">关闭后,推送注册与推送发送都不会再向该应用开放。</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
|
|
<el-alert
|
|
|
|
|
|
title="这里维护各厂商推送凭据。推送服务开通后,离线推送才会按厂商路由发送。"
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
:closable="false"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
style="margin-bottom:16px"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<el-card>
|
|
|
|
|
|
<template #header>厂商配置</template>
|
2026-05-05 22:16:11 +08:00
|
|
|
|
<el-divider content-position="left">通知通道</el-divider>
|
|
|
|
|
|
<el-table :data="pushConfig.channels" border style="margin-bottom:16px">
|
|
|
|
|
|
<el-table-column label="业务键" min-width="130">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-input v-model="row.key" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="Channel ID" min-width="180">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-input v-model="row.channelId" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="版本" width="82">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-input-number v-model="row.version" :min="1" controls-position="right" style="width:72px" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="名称" min-width="130">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-input v-model="row.name" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="重要性" width="130">
|
|
|
|
|
|
<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-select>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="声音" width="80" align="center">
|
|
|
|
|
|
<template #default="{ row }"><el-switch v-model="row.sound" /></template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="振动" width="80" align="center">
|
|
|
|
|
|
<template #default="{ row }"><el-switch v-model="row.vibration" /></template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="角标" width="80" align="center">
|
|
|
|
|
|
<template #default="{ row }"><el-switch v-model="row.badge" /></template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="操作" width="80">
|
|
|
|
|
|
<template #default="{ $index }">
|
|
|
|
|
|
<el-button link type="danger" @click="removeChannel($index)">删除</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<el-button style="margin-bottom:20px" @click="addChannel">新增通道</el-button>
|
|
|
|
|
|
|
|
|
|
|
|
<el-divider content-position="left">业务分类路由</el-divider>
|
|
|
|
|
|
<el-table :data="routeRows" border style="margin-bottom:20px">
|
|
|
|
|
|
<el-table-column label="业务分类" width="160">
|
|
|
|
|
|
<template #default="{ row }">{{ row.type }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="通道" min-width="180">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-select v-model="row.route.channel">
|
|
|
|
|
|
<el-option v-for="channel in pushConfig.channels" :key="channel.key" :label="channel.name || channel.key" :value="channel.key" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="Category" min-width="160">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-input v-model="row.route.category" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="优先级" width="130">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-select v-model="row.route.priority">
|
|
|
|
|
|
<el-option label="低" value="LOW" />
|
|
|
|
|
|
<el-option label="默认" value="DEFAULT" />
|
|
|
|
|
|
<el-option label="高" value="HIGH" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
|
|
<el-divider content-position="left">厂商凭据</el-divider>
|
2026-04-30 09:49:05 +08:00
|
|
|
|
<div class="vendor-grid">
|
|
|
|
|
|
<el-card v-for="vendor in vendorDefs" :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="120px">
|
|
|
|
|
|
<el-form-item v-for="field in vendor.fields" :key="field.key" :label="field.label">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-if="field.type === 'textarea'"
|
|
|
|
|
|
v-model="pushConfig[vendor.key][field.key]"
|
|
|
|
|
|
type="textarea"
|
|
|
|
|
|
:rows="field.rows ?? 4"
|
|
|
|
|
|
:placeholder="field.placeholder"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<el-switch
|
|
|
|
|
|
v-else-if="field.type === 'switch'"
|
|
|
|
|
|
v-model="pushConfig[vendor.key][field.key]"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-else
|
|
|
|
|
|
v-model="pushConfig[vendor.key][field.key]"
|
2026-05-05 17:54:59 +08:00
|
|
|
|
type="text"
|
2026-04-30 09:49:05 +08:00
|
|
|
|
:placeholder="field.placeholder"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="toolbar">
|
|
|
|
|
|
<el-button @click="reloadConfig" :loading="loading">刷新</el-button>
|
|
|
|
|
|
<el-button type="primary" @click="saveConfig" :loading="saving">保存配置</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
|
|
|
|
|
import { useRoute } from 'vue-router'
|
|
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
|
import { CopyDocument } from '@element-plus/icons-vue'
|
2026-05-05 22:16:11 +08:00
|
|
|
|
import {
|
|
|
|
|
|
appApi,
|
|
|
|
|
|
type App,
|
|
|
|
|
|
type FeatureService,
|
|
|
|
|
|
type PushNotificationChannelConfig,
|
|
|
|
|
|
type PushNotificationRouteConfig,
|
|
|
|
|
|
type PushServiceConfig,
|
|
|
|
|
|
} from '@/api/app'
|
2026-04-30 09:49:05 +08:00
|
|
|
|
|
|
|
|
|
|
type VendorKey = keyof PushServiceConfig
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
type VendorDef = {
|
|
|
|
|
|
key: VendorKey
|
|
|
|
|
|
label: string
|
|
|
|
|
|
hint: string
|
|
|
|
|
|
fields: FieldDef[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
const app = ref<App | null>(null)
|
|
|
|
|
|
const services = ref<FeatureService[]>([])
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const saving = ref(false)
|
|
|
|
|
|
const isMobile = ref(window.innerWidth < 768)
|
|
|
|
|
|
|
|
|
|
|
|
function updateViewport() {
|
|
|
|
|
|
isMobile.value = window.innerWidth < 768
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const pushConfig = reactive<Required<PushServiceConfig>>({
|
|
|
|
|
|
huawei: { appId: '', appSecret: '' },
|
|
|
|
|
|
xiaomi: { appId: '', appKey: '', appSecret: '' },
|
|
|
|
|
|
oppo: { appId: '', appKey: '', masterSecret: '' },
|
|
|
|
|
|
vivo: { appId: '', appKey: '', appSecret: '' },
|
|
|
|
|
|
honor: { appId: '', clientId: '', clientSecret: '' },
|
|
|
|
|
|
apns: { teamId: '', keyId: '', bundleId: '', keyPath: '', sandbox: false },
|
|
|
|
|
|
fcm: { serviceAccountJson: '' },
|
2026-05-05 22:16:11 +08:00
|
|
|
|
channels: defaultChannels(),
|
|
|
|
|
|
routing: defaultRouting(),
|
2026-04-30 09:49:05 +08:00
|
|
|
|
} as Required<PushServiceConfig>)
|
2026-05-05 22:16:11 +08:00
|
|
|
|
const originalChannels = ref<PushNotificationChannelConfig[]>([])
|
|
|
|
|
|
|
|
|
|
|
|
const importanceOptions = [
|
|
|
|
|
|
{ label: '最小', value: 'MIN' },
|
|
|
|
|
|
{ label: '低', value: 'LOW' },
|
|
|
|
|
|
{ label: '默认', value: 'DEFAULT' },
|
|
|
|
|
|
{ label: '高', value: 'HIGH' },
|
|
|
|
|
|
{ label: '最高', value: 'MAX' },
|
|
|
|
|
|
] as const
|
|
|
|
|
|
|
|
|
|
|
|
const routeTypes = [
|
|
|
|
|
|
{ type: 'IM_MESSAGE', channel: 'im_message', category: 'MESSAGE', priority: 'HIGH' },
|
|
|
|
|
|
{ type: 'FRIEND_REQUEST', channel: 'im_message', category: 'SOCIAL', priority: 'HIGH' },
|
|
|
|
|
|
{ type: 'SYSTEM_NOTICE', channel: 'system_notice', category: 'SYSTEM', priority: 'DEFAULT' },
|
|
|
|
|
|
] as const
|
|
|
|
|
|
|
|
|
|
|
|
const routeRows = computed(() => routeTypes.map(item => ({
|
|
|
|
|
|
type: item.type,
|
|
|
|
|
|
route: pushConfig.routing[item.type] ?? ensureRoute(item.type, item.channel, item.category, item.priority),
|
|
|
|
|
|
})))
|
2026-04-30 09:49:05 +08:00
|
|
|
|
|
|
|
|
|
|
const vendorDefs: VendorDef[] = [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'huawei',
|
|
|
|
|
|
label: '华为 HMS',
|
|
|
|
|
|
hint: '填写 AppId / AppSecret,供服务端推送使用。',
|
|
|
|
|
|
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
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'xiaomi',
|
|
|
|
|
|
label: '小米 MiPush',
|
|
|
|
|
|
hint: '填写 AppId / AppKey / AppSecret。',
|
|
|
|
|
|
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
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'oppo',
|
|
|
|
|
|
label: 'OPPO 推送',
|
|
|
|
|
|
hint: '填写 AppId / AppKey / MasterSecret。',
|
|
|
|
|
|
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 推送',
|
|
|
|
|
|
hint: '填写 AppId / AppKey / AppSecret。',
|
|
|
|
|
|
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
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'honor',
|
|
|
|
|
|
label: '荣耀推送',
|
|
|
|
|
|
hint: '填写 AppId / ClientId / ClientSecret。',
|
|
|
|
|
|
fields: [
|
|
|
|
|
|
{ key: 'appId', label: 'AppId' },
|
|
|
|
|
|
{ key: 'clientId', label: 'ClientId' },
|
2026-05-05 17:54:59 +08:00
|
|
|
|
{ key: 'clientSecret', label: 'ClientSecret' },
|
2026-04-30 09:49:05 +08:00
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'apns',
|
|
|
|
|
|
label: 'APNs(iOS)',
|
|
|
|
|
|
hint: '填写 Team ID / Key ID / Bundle ID / p8 文件路径。',
|
|
|
|
|
|
fields: [
|
|
|
|
|
|
{ key: 'teamId', label: 'Team ID' },
|
|
|
|
|
|
{ key: 'keyId', label: 'Key ID' },
|
|
|
|
|
|
{ key: 'bundleId', label: 'Bundle ID' },
|
|
|
|
|
|
{ key: 'keyPath', label: 'p8 文件路径' },
|
|
|
|
|
|
{ key: 'sandbox', label: 'Sandbox', type: 'switch' },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const pushEnabled = computed(() => services.value.some(s => s.serviceType === 'PUSH' && s.enabled))
|
|
|
|
|
|
const servicePlatform = computed(() => services.value.find(s => s.serviceType === 'PUSH')?.platform ?? 'ANDROID')
|
|
|
|
|
|
|
|
|
|
|
|
async function loadData() {
|
|
|
|
|
|
const id = route.params.appId as string
|
|
|
|
|
|
const [appRes, svcRes] = await Promise.all([
|
|
|
|
|
|
appApi.get(id),
|
|
|
|
|
|
appApi.getServices(id),
|
|
|
|
|
|
])
|
|
|
|
|
|
app.value = appRes.data.data
|
|
|
|
|
|
services.value = svcRes.data.data
|
|
|
|
|
|
applyConfig(services.value.find(s => s.serviceType === 'PUSH')?.config)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applyConfig(raw?: string | null) {
|
|
|
|
|
|
const parsed = parseConfig(raw)
|
|
|
|
|
|
pushConfig.huawei = { ...pushConfig.huawei, ...parsed.huawei }
|
|
|
|
|
|
pushConfig.xiaomi = { ...pushConfig.xiaomi, ...parsed.xiaomi }
|
|
|
|
|
|
pushConfig.oppo = { ...pushConfig.oppo, ...parsed.oppo }
|
|
|
|
|
|
pushConfig.vivo = { ...pushConfig.vivo, ...parsed.vivo }
|
|
|
|
|
|
pushConfig.honor = { ...pushConfig.honor, ...parsed.honor }
|
|
|
|
|
|
pushConfig.apns = { ...pushConfig.apns, ...parsed.apns }
|
|
|
|
|
|
pushConfig.fcm = { ...pushConfig.fcm, ...parsed.fcm }
|
2026-05-05 22:16:11 +08:00
|
|
|
|
pushConfig.channels = normalizeChannels(parsed.channels)
|
|
|
|
|
|
pushConfig.routing = normalizeRouting(parsed.routing)
|
|
|
|
|
|
originalChannels.value = pushConfig.channels.map(channel => ({ ...channel }))
|
2026-04-30 09:49:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseConfig(raw?: string | null): PushServiceConfig {
|
|
|
|
|
|
if (!raw) return {}
|
|
|
|
|
|
try {
|
|
|
|
|
|
return JSON.parse(raw) as PushServiceConfig
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function saveConfig() {
|
|
|
|
|
|
if (!app.value) return
|
|
|
|
|
|
saving.value = true
|
|
|
|
|
|
try {
|
2026-05-05 22:16:11 +08:00
|
|
|
|
bumpChangedChannelVersions()
|
2026-05-05 17:54:59 +08:00
|
|
|
|
await appApi.updateServiceConfig(app.value.id, servicePlatform.value, 'PUSH', toPushConfigRequest())
|
2026-04-30 09:49:05 +08:00
|
|
|
|
ElMessage.success('推送配置已保存')
|
|
|
|
|
|
await loadData()
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
ElMessage.error('保存失败')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
saving.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 22:16:11 +08:00
|
|
|
|
function toPushConfigRequest(): Record<string, unknown> {
|
2026-05-05 17:54:59 +08:00
|
|
|
|
return {
|
|
|
|
|
|
huaweiAppId: pushConfig.huawei.appId ?? '',
|
|
|
|
|
|
huaweiAppSecret: pushConfig.huawei.appSecret ?? '',
|
|
|
|
|
|
xiaomiAppId: pushConfig.xiaomi.appId ?? '',
|
|
|
|
|
|
xiaomiAppKey: pushConfig.xiaomi.appKey ?? '',
|
|
|
|
|
|
xiaomiAppSecret: pushConfig.xiaomi.appSecret ?? '',
|
|
|
|
|
|
oppoAppId: pushConfig.oppo.appId ?? '',
|
|
|
|
|
|
oppoAppKey: pushConfig.oppo.appKey ?? '',
|
|
|
|
|
|
oppoMasterSecret: pushConfig.oppo.masterSecret ?? '',
|
|
|
|
|
|
vivoAppId: pushConfig.vivo.appId ?? '',
|
|
|
|
|
|
vivoAppKey: pushConfig.vivo.appKey ?? '',
|
|
|
|
|
|
vivoAppSecret: pushConfig.vivo.appSecret ?? '',
|
|
|
|
|
|
honorAppId: pushConfig.honor.appId ?? '',
|
|
|
|
|
|
honorClientId: pushConfig.honor.clientId ?? '',
|
|
|
|
|
|
honorClientSecret: pushConfig.honor.clientSecret ?? '',
|
|
|
|
|
|
apnsTeamId: pushConfig.apns.teamId ?? '',
|
|
|
|
|
|
apnsKeyId: pushConfig.apns.keyId ?? '',
|
|
|
|
|
|
apnsBundleId: pushConfig.apns.bundleId ?? '',
|
|
|
|
|
|
apnsKeyPath: pushConfig.apns.keyPath ?? '',
|
|
|
|
|
|
apnsSandbox: pushConfig.apns.sandbox ?? false,
|
|
|
|
|
|
fcmServiceAccountJson: pushConfig.fcm.serviceAccountJson ?? '',
|
2026-05-05 22:16:11 +08:00
|
|
|
|
channels: pushConfig.channels,
|
|
|
|
|
|
routing: pushConfig.routing,
|
2026-05-05 17:54:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 22:16:11 +08:00
|
|
|
|
function defaultChannels(): PushNotificationChannelConfig[] {
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'im_message',
|
|
|
|
|
|
channelId: 'xuqm_im_message',
|
|
|
|
|
|
version: 1,
|
|
|
|
|
|
name: '聊天消息',
|
|
|
|
|
|
description: '单聊、群聊和好友消息',
|
|
|
|
|
|
importance: 'HIGH',
|
|
|
|
|
|
sound: true,
|
|
|
|
|
|
vibration: true,
|
|
|
|
|
|
badge: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'system_notice',
|
|
|
|
|
|
channelId: 'xuqm_system_notice',
|
|
|
|
|
|
version: 1,
|
|
|
|
|
|
name: '系统通知',
|
|
|
|
|
|
description: '系统通知和业务提醒',
|
|
|
|
|
|
importance: 'DEFAULT',
|
|
|
|
|
|
sound: true,
|
|
|
|
|
|
vibration: true,
|
|
|
|
|
|
badge: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function defaultRouting(): Record<string, PushNotificationRouteConfig> {
|
|
|
|
|
|
return {
|
|
|
|
|
|
IM_MESSAGE: { channel: 'im_message', category: 'MESSAGE', priority: 'HIGH' },
|
|
|
|
|
|
FRIEND_REQUEST: { channel: 'im_message', category: 'SOCIAL', priority: 'HIGH' },
|
|
|
|
|
|
SYSTEM_NOTICE: { channel: 'system_notice', category: 'SYSTEM', priority: 'DEFAULT' },
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeChannels(channels?: PushNotificationChannelConfig[]): PushNotificationChannelConfig[] {
|
|
|
|
|
|
const source = Array.isArray(channels) && channels.length > 0 ? channels : defaultChannels()
|
|
|
|
|
|
return source.map((channel, index) => ({
|
|
|
|
|
|
key: channel.key || `channel_${index + 1}`,
|
|
|
|
|
|
channelId: channel.channelId || `xuqm_channel_${index + 1}`,
|
|
|
|
|
|
version: Math.max(Number(channel.version || 1), 1),
|
|
|
|
|
|
name: channel.name || channel.key || `通知通道 ${index + 1}`,
|
|
|
|
|
|
description: channel.description ?? '',
|
|
|
|
|
|
importance: channel.importance || 'DEFAULT',
|
|
|
|
|
|
sound: channel.sound ?? true,
|
|
|
|
|
|
vibration: channel.vibration ?? true,
|
|
|
|
|
|
badge: channel.badge ?? true,
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeRouting(routing?: Record<string, PushNotificationRouteConfig>): Record<string, PushNotificationRouteConfig> {
|
|
|
|
|
|
return { ...defaultRouting(), ...(routing ?? {}) }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ensureRoute(
|
|
|
|
|
|
type: string,
|
|
|
|
|
|
channel: string,
|
|
|
|
|
|
category: string,
|
|
|
|
|
|
priority: PushNotificationRouteConfig['priority'],
|
|
|
|
|
|
): PushNotificationRouteConfig {
|
|
|
|
|
|
const route = { channel, category, priority }
|
|
|
|
|
|
pushConfig.routing[type] = route
|
|
|
|
|
|
return route
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function addChannel() {
|
|
|
|
|
|
const next = pushConfig.channels.length + 1
|
|
|
|
|
|
pushConfig.channels.push({
|
|
|
|
|
|
key: `custom_${next}`,
|
|
|
|
|
|
channelId: `xuqm_custom_${next}`,
|
|
|
|
|
|
version: 1,
|
|
|
|
|
|
name: `自定义通道 ${next}`,
|
|
|
|
|
|
description: '',
|
|
|
|
|
|
importance: 'DEFAULT',
|
|
|
|
|
|
sound: true,
|
|
|
|
|
|
vibration: true,
|
|
|
|
|
|
badge: true,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removeChannel(index: number) {
|
|
|
|
|
|
const [removed] = pushConfig.channels.splice(index, 1)
|
|
|
|
|
|
if (!removed) return
|
|
|
|
|
|
Object.values(pushConfig.routing).forEach(route => {
|
|
|
|
|
|
if (route.channel === removed.key) {
|
|
|
|
|
|
route.channel = pushConfig.channels[0]?.key ?? ''
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function bumpChangedChannelVersions() {
|
|
|
|
|
|
const originalByKey = new Map(originalChannels.value.map(channel => [channel.key, channel]))
|
|
|
|
|
|
pushConfig.channels.forEach(channel => {
|
|
|
|
|
|
const original = originalByKey.get(channel.key)
|
|
|
|
|
|
if (!original) return
|
|
|
|
|
|
const immutableChanged = original.importance !== channel.importance
|
|
|
|
|
|
|| original.sound !== channel.sound
|
|
|
|
|
|
|| original.vibration !== channel.vibration
|
|
|
|
|
|
|| original.badge !== channel.badge
|
|
|
|
|
|
if (immutableChanged && channel.version <= original.version) {
|
|
|
|
|
|
channel.version = original.version + 1
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 09:49:05 +08:00
|
|
|
|
async function onTogglePushService(enable: boolean) {
|
|
|
|
|
|
if (enable) {
|
|
|
|
|
|
ElMessage.info('请通过服务开通流程启用推送服务')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
await ElMessageBox.confirm('确认关闭离线推送服务?', '关闭服务', {
|
|
|
|
|
|
type: 'warning',
|
|
|
|
|
|
confirmButtonText: '确认关闭',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
})
|
|
|
|
|
|
if (!app.value) return
|
|
|
|
|
|
await appApi.toggleService(app.value.id, servicePlatform.value, 'PUSH', false)
|
|
|
|
|
|
ElMessage.success('已关闭')
|
|
|
|
|
|
await loadData()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
.mono { font-family: monospace; font-size: 12px; }
|
|
|
|
|
|
.hint { font-size: 12px; color: #909399; }
|
|
|
|
|
|
.info-card :deep(.el-descriptions__body) {
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
.push-switch-row {
|
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.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;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
.toolbar {
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 767px) {
|
|
|
|
|
|
.vendor-grid {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.push-switch-row {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.toolbar :deep(.el-button) {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.vendor-card :deep(.el-form-item__label) {
|
|
|
|
|
|
padding-bottom: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|