2026-04-21 22:07:29 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div v-if="app">
|
|
|
|
|
|
<el-page-header @back="$router.back()" :content="app.name" style="margin-bottom:24px" />
|
|
|
|
|
|
|
|
|
|
|
|
<el-card style="margin-bottom:16px">
|
|
|
|
|
|
<el-descriptions :column="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="AppSecret">
|
2026-04-24 20:54:03 +08:00
|
|
|
|
<el-text class="mono">{{ revealedSecret ?? '••••••••••••••••' }}</el-text>
|
|
|
|
|
|
<el-button link @click="openVerifyDialog('REVEAL_SECRET')" title="查看">
|
|
|
|
|
|
<el-icon><View /></el-icon>
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button v-if="revealedSecret" link @click="copy(revealedSecret!)">
|
|
|
|
|
|
<el-icon><CopyDocument /></el-icon>
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button link type="warning" @click="openVerifyDialog('RESET_SECRET')">重置</el-button>
|
2026-04-21 22:07:29 +08:00
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="简述" :span="2">{{ app.description ?? '-' }}</el-descriptions-item>
|
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
2026-04-29 12:33:26 +08:00
|
|
|
|
<el-card style="margin-bottom:16px">
|
|
|
|
|
|
<template #header>即时通讯服务</template>
|
|
|
|
|
|
<div class="service-grid">
|
|
|
|
|
|
<el-card class="service-card">
|
|
|
|
|
|
<div class="service-header">
|
|
|
|
|
|
<span class="service-name">{{ serviceLabel('IM') }}</span>
|
2026-04-29 16:07:22 +08:00
|
|
|
|
<el-switch :model-value="imEnabled" @change="(val: boolean) => onToggleService('IM', val)" />
|
2026-04-29 12:33:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<template v-if="imService">
|
|
|
|
|
|
<div style="margin-top:10px;display:flex;gap:8px;flex-wrap:wrap">
|
|
|
|
|
|
<!-- IM 管理页按 appKey 作用域查询,不能把租户 app.id 直接传进去。 -->
|
|
|
|
|
|
<el-button size="small" @click="$router.push({ path: `/apps/${route.params.id}/im`, query: { appKey: app.appKey } })">
|
|
|
|
|
|
即时通讯管理 →
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button size="small" type="primary" plain @click="$router.push(`/apps/${route.params.id}/im-config`)">
|
|
|
|
|
|
服务配置 →
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<div style="margin-top:10px">
|
2026-04-29 16:07:22 +08:00
|
|
|
|
<el-button size="small" type="primary" plain @click="openActivationRequest('IM')">
|
2026-04-29 12:33:26 +08:00
|
|
|
|
申请开通
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
2026-04-21 22:07:29 +08:00
|
|
|
|
<el-card>
|
2026-04-29 12:33:26 +08:00
|
|
|
|
<template #header>离线推送与版本管理</template>
|
2026-04-21 22:07:29 +08:00
|
|
|
|
<div class="service-grid">
|
2026-04-29 12:33:26 +08:00
|
|
|
|
<el-card v-for="svcType in ['PUSH', 'UPDATE']" :key="svcType" class="service-card">
|
2026-04-21 22:07:29 +08:00
|
|
|
|
<div class="service-header">
|
2026-04-29 16:07:22 +08:00
|
|
|
|
<div class="service-title-block">
|
2026-04-29 19:08:13 +08:00
|
|
|
|
<span class="service-name">{{ serviceLabel(svcType) }}</span>
|
|
|
|
|
|
<span class="service-help">{{ serviceHelp(svcType) }}</span>
|
|
|
|
|
|
</div>
|
2026-04-21 22:07:29 +08:00
|
|
|
|
<el-switch
|
2026-04-29 16:07:22 +08:00
|
|
|
|
:model-value="isServiceEnabled(svcType)"
|
|
|
|
|
|
@change="(val: boolean) => onToggleService(svcType, val)"
|
2026-04-21 22:07:29 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-04-29 16:07:22 +08:00
|
|
|
|
<div class="service-status-row">
|
|
|
|
|
|
<el-tag :type="isServiceEnabled(svcType) ? 'success' : 'info'" size="small">
|
|
|
|
|
|
{{ isServiceEnabled(svcType) ? '已开通' : '未开通' }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
<span class="service-status-text">
|
|
|
|
|
|
{{ svcType === 'UPDATE'
|
2026-04-29 19:08:13 +08:00
|
|
|
|
? 'Android 整包版本在版本管理页上传;iOS / 鸿蒙仅记录版本号和市场跳转页。商店配置与发布配置都在版本管理页。'
|
2026-04-29 16:07:22 +08:00
|
|
|
|
: '推送服务开通后即可在终端接收设备级推送。' }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="service-actions" v-if="svcType === 'UPDATE'">
|
|
|
|
|
|
<el-button size="small" type="primary" plain @click="$router.push(`/apps/${route.params.id}/update`)">
|
|
|
|
|
|
版本管理 →
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
2026-04-21 22:07:29 +08:00
|
|
|
|
</el-card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
2026-04-24 20:54:03 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- Email Verify Dialog (reveal or reset) -->
|
|
|
|
|
|
<el-dialog v-model="showVerifyDialog" :title="verifyPurpose === 'REVEAL_SECRET' ? '查看 AppSecret' : '重置 AppSecret'" width="420px">
|
|
|
|
|
|
<div v-if="!codeSent">
|
|
|
|
|
|
<p style="color:#555;margin-bottom:16px">
|
|
|
|
|
|
{{ verifyPurpose === 'REVEAL_SECRET'
|
|
|
|
|
|
? '为保护账号安全,查看 AppSecret 需要通过邮箱验证。'
|
|
|
|
|
|
: '重置后旧 AppSecret 立即失效,请确认后继续。' }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<el-button type="primary" :loading="sendingCode" @click="sendVerifyCode">
|
|
|
|
|
|
发送验证码到邮箱
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else>
|
|
|
|
|
|
<p style="color:#555;margin-bottom:16px">验证码已发送至您的注册邮箱,请查收并输入:</p>
|
|
|
|
|
|
<el-input v-model="verifyCode" placeholder="6位验证码" maxlength="6" style="margin-bottom:12px" />
|
|
|
|
|
|
<div style="display:flex;gap:8px">
|
|
|
|
|
|
<el-button @click="sendVerifyCode" :loading="sendingCode" size="small">重新发送</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<el-button @click="showVerifyDialog = false">取消</el-button>
|
|
|
|
|
|
<el-button v-if="codeSent" type="primary" :loading="submittingVerify" @click="submitVerify">
|
|
|
|
|
|
{{ verifyPurpose === 'REVEAL_SECRET' ? '查看' : '确认重置' }}
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Activation Request Dialog -->
|
|
|
|
|
|
<el-dialog v-model="showActivationDialog" title="申请开通服务" width="420px">
|
|
|
|
|
|
<el-form label-width="80px">
|
2026-04-29 12:33:26 +08:00
|
|
|
|
<el-form-item label="服务">{{ serviceLabel(activationForm.serviceType) }}</el-form-item>
|
2026-04-24 20:54:03 +08:00
|
|
|
|
<el-form-item label="申请理由">
|
|
|
|
|
|
<el-input v-model="activationForm.reason" type="textarea" :rows="3" placeholder="请描述您的业务场景" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<el-button @click="showActivationDialog = false">取消</el-button>
|
|
|
|
|
|
<el-button type="primary" :loading="submittingActivation" @click="submitActivationRequest">提交申请</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
2026-04-21 22:07:29 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-04-29 12:33:26 +08:00
|
|
|
|
import { computed, ref, onMounted } from 'vue'
|
2026-04-21 22:07:29 +08:00
|
|
|
|
import { useRoute } from 'vue-router'
|
2026-04-24 20:54:03 +08:00
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
|
import { View } from '@element-plus/icons-vue'
|
2026-04-21 22:07:29 +08:00
|
|
|
|
import { appApi, type App, type FeatureService } from '@/api/app'
|
2026-04-24 20:54:03 +08:00
|
|
|
|
import client from '@/api/client'
|
2026-04-21 22:07:29 +08:00
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
const app = ref<App | null>(null)
|
|
|
|
|
|
const services = ref<FeatureService[]>([])
|
2026-04-29 12:33:26 +08:00
|
|
|
|
const imService = computed(() => services.value.find(s => s.serviceType === 'IM') ?? null)
|
|
|
|
|
|
const imEnabled = computed(() => imService.value?.enabled ?? false)
|
2026-04-21 22:07:29 +08:00
|
|
|
|
|
2026-04-24 20:54:03 +08:00
|
|
|
|
const revealedSecret = ref<string | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
const showVerifyDialog = ref(false)
|
|
|
|
|
|
const verifyPurpose = ref<'REVEAL_SECRET' | 'RESET_SECRET'>('REVEAL_SECRET')
|
|
|
|
|
|
const codeSent = ref(false)
|
|
|
|
|
|
const sendingCode = ref(false)
|
|
|
|
|
|
const verifyCode = ref('')
|
|
|
|
|
|
const submittingVerify = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
const showActivationDialog = ref(false)
|
|
|
|
|
|
const submittingActivation = ref(false)
|
|
|
|
|
|
const activationForm = ref({ platform: '', serviceType: '', reason: '' })
|
|
|
|
|
|
|
2026-04-29 16:07:22 +08:00
|
|
|
|
function isServiceEnabled(svcType: string) {
|
|
|
|
|
|
return services.value.some(s => s.serviceType === svcType && s.enabled)
|
2026-04-28 16:08:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-21 22:07:29 +08:00
|
|
|
|
function serviceLabel(type: string) {
|
|
|
|
|
|
return { IM: '即时通讯 (IM)', PUSH: '离线推送', UPDATE: '版本管理' }[type] ?? type
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 16:07:22 +08:00
|
|
|
|
function serviceHelp(type: string) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
IM: 'IM 服务独立开通后,在管理页配置回调和消息能力。',
|
|
|
|
|
|
PUSH: '一次开通后,推送配置在服务管理页按平台维护。',
|
2026-04-29 19:08:13 +08:00
|
|
|
|
UPDATE: '一次开通后,版本管理页只管理 Android 整包版本,iOS / 鸿蒙仅记录提醒信息。',
|
2026-04-29 16:07:22 +08:00
|
|
|
|
}[type] ?? ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getServiceRepresentative(svcType: string) {
|
|
|
|
|
|
return services.value.find(s => s.serviceType === svcType) ?? null
|
2026-04-29 12:33:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-21 22:07:29 +08:00
|
|
|
|
async function loadData() {
|
|
|
|
|
|
const id = route.params.id 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-04-24 20:54:03 +08:00
|
|
|
|
revealedSecret.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 16:07:22 +08:00
|
|
|
|
async function onToggleService(svcType: string, enable: boolean) {
|
2026-04-24 20:54:03 +08:00
|
|
|
|
if (enable) {
|
2026-04-29 16:07:22 +08:00
|
|
|
|
openActivationRequest(svcType)
|
2026-04-24 20:54:03 +08:00
|
|
|
|
} else {
|
2026-04-29 16:07:22 +08:00
|
|
|
|
await ElMessageBox.confirm(`确认关闭 ${serviceLabel(svcType)} 服务?`, '关闭服务', {
|
2026-04-24 20:54:03 +08:00
|
|
|
|
type: 'warning', confirmButtonText: '确认关闭', cancelButtonText: '取消',
|
|
|
|
|
|
})
|
2026-04-29 16:07:22 +08:00
|
|
|
|
const platform = getServiceRepresentative(svcType)?.platform ?? 'ANDROID'
|
2026-04-24 20:54:03 +08:00
|
|
|
|
await appApi.toggleService(route.params.id as string, platform, svcType, false)
|
|
|
|
|
|
ElMessage.success('已关闭')
|
|
|
|
|
|
loadData()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 16:07:22 +08:00
|
|
|
|
function openActivationRequest(svcType: string) {
|
|
|
|
|
|
activationForm.value = { platform: 'ANDROID', serviceType: svcType, reason: '' }
|
2026-04-24 20:54:03 +08:00
|
|
|
|
showActivationDialog.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitActivationRequest() {
|
|
|
|
|
|
if (!activationForm.value.reason.trim()) {
|
|
|
|
|
|
return ElMessage.warning('请填写申请理由')
|
|
|
|
|
|
}
|
|
|
|
|
|
submittingActivation.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await client.post(`/apps/${route.params.id}/services/request-activation`, null, {
|
|
|
|
|
|
params: {
|
|
|
|
|
|
platform: activationForm.value.platform,
|
|
|
|
|
|
serviceType: activationForm.value.serviceType,
|
|
|
|
|
|
applyReason: activationForm.value.reason,
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
ElMessage.success('申请已提交,等待运营审核')
|
|
|
|
|
|
showActivationDialog.value = false
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
submittingActivation.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openVerifyDialog(purpose: 'REVEAL_SECRET' | 'RESET_SECRET') {
|
|
|
|
|
|
verifyPurpose.value = purpose
|
|
|
|
|
|
codeSent.value = false
|
|
|
|
|
|
verifyCode.value = ''
|
|
|
|
|
|
showVerifyDialog.value = true
|
2026-04-21 22:07:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-24 20:54:03 +08:00
|
|
|
|
async function sendVerifyCode() {
|
|
|
|
|
|
sendingCode.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await appApi.requestSecretVerify(route.params.id as string, verifyPurpose.value)
|
|
|
|
|
|
codeSent.value = true
|
|
|
|
|
|
ElMessage.success('验证码已发送')
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
ElMessage.error('发送失败,请稍后重试')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
sendingCode.value = false
|
|
|
|
|
|
}
|
2026-04-21 22:07:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-24 20:54:03 +08:00
|
|
|
|
async function submitVerify() {
|
|
|
|
|
|
if (verifyCode.value.length !== 6) return ElMessage.warning('请输入6位验证码')
|
|
|
|
|
|
submittingVerify.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const id = route.params.id as string
|
|
|
|
|
|
if (verifyPurpose.value === 'REVEAL_SECRET') {
|
|
|
|
|
|
const res = await appApi.revealSecret(id, verifyCode.value)
|
|
|
|
|
|
revealedSecret.value = res.data.data.appSecret
|
|
|
|
|
|
ElMessage.success('AppSecret 已显示,请妥善保管')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const res = await appApi.resetSecret(id, verifyCode.value)
|
|
|
|
|
|
revealedSecret.value = res.data.data.appSecret
|
|
|
|
|
|
ElMessage.success('AppSecret 已重置,旧密钥立即失效')
|
|
|
|
|
|
}
|
|
|
|
|
|
showVerifyDialog.value = false
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
ElMessage.error('验证码错误或已过期')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
submittingVerify.value = false
|
|
|
|
|
|
}
|
2026-04-21 22:07:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function copy(text: string) {
|
|
|
|
|
|
navigator.clipboard.writeText(text)
|
|
|
|
|
|
ElMessage.success('已复制')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(loadData)
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.mono { font-family: monospace; font-size: 12px; }
|
2026-04-29 16:07:22 +08:00
|
|
|
|
.service-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; margin-top: 16px; }
|
2026-04-21 22:07:29 +08:00
|
|
|
|
.service-card { border: 1px solid #e8e8e8; }
|
2026-04-29 16:07:22 +08:00
|
|
|
|
.service-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; font-weight: 500; }
|
2026-04-21 22:07:29 +08:00
|
|
|
|
.service-name { font-size: 15px; }
|
2026-04-29 16:07:22 +08:00
|
|
|
|
.service-title-block { display: flex; flex-direction: column; gap: 4px; }
|
|
|
|
|
|
.service-help { font-size: 12px; color: #6b7280; line-height: 1.4; }
|
|
|
|
|
|
.service-status-row { display: flex; align-items: center; gap: 10px; margin-top: 12px; flex-wrap: wrap; }
|
|
|
|
|
|
.service-status-text { font-size: 13px; color: #6b7280; line-height: 1.5; }
|
|
|
|
|
|
.service-actions { margin-top: 12px; display: flex; gap: 8px; flex-wrap: wrap; }
|
2026-04-21 22:07:29 +08:00
|
|
|
|
</style>
|