XuqmGroup-Web/tenant-platform/src/views/push/PushConfigView.vue

332 行
10 KiB
Vue

<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>
<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]"
type="text"
: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'
import { appApi, type App, type FeatureService, type PushServiceConfig } from '@/api/app'
type VendorKey = keyof PushServiceConfig
type FieldDef = {
key: string
label: string
type?: 'textarea' | 'switch'
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: '' },
} as Required<PushServiceConfig>)
const vendorDefs: VendorDef[] = [
{
key: 'huawei',
label: '华为 HMS',
hint: '填写 AppId / AppSecret,供服务端推送使用。',
fields: [
{ key: 'appId', label: 'AppId' },
{ key: 'appSecret', label: 'AppSecret' },
],
},
{
key: 'xiaomi',
label: '小米 MiPush',
hint: '填写 AppId / AppKey / AppSecret。',
fields: [
{ key: 'appId', label: 'AppId' },
{ key: 'appKey', label: 'AppKey' },
{ key: 'appSecret', label: 'AppSecret' },
],
},
{
key: 'oppo',
label: 'OPPO 推送',
hint: '填写 AppId / AppKey / MasterSecret。',
fields: [
{ key: 'appId', label: 'AppId' },
{ key: 'appKey', label: 'AppKey' },
{ key: 'masterSecret', label: 'MasterSecret' },
],
},
{
key: 'vivo',
label: 'vivo 推送',
hint: '填写 AppId / AppKey / AppSecret。',
fields: [
{ key: 'appId', label: 'AppId' },
{ key: 'appKey', label: 'AppKey' },
{ key: 'appSecret', label: 'AppSecret' },
],
},
{
key: 'honor',
label: '荣耀推送',
hint: '填写 AppId / ClientId / ClientSecret。',
fields: [
{ key: 'appId', label: 'AppId' },
{ key: 'clientId', label: 'ClientId' },
{ key: 'clientSecret', label: 'ClientSecret' },
],
},
{
key: 'apns',
label: 'APNsiOS',
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 }
}
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 {
await appApi.updateServiceConfig(app.value.id, servicePlatform.value, 'PUSH', toPushConfigRequest())
ElMessage.success('推送配置已保存')
await loadData()
} catch {
ElMessage.error('保存失败')
} finally {
saving.value = false
}
}
function toPushConfigRequest(): Record<string, string | boolean> {
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 ?? '',
}
}
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>