431 lines
12 KiB
Vue
431 lines
12 KiB
Vue
<template>
|
||
<div>
|
||
<!-- 配置管理 -->
|
||
|
||
<el-card class="config-card">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span>公众号配置管理</span>
|
||
<el-tag type="primary" size="small">微信公众号</el-tag>
|
||
</div>
|
||
</template>
|
||
|
||
<el-form ref="configFormRef" :model="configForm" :rules="configRules" label-width="150px">
|
||
<el-form-item label="公众号AppID" prop="appId">
|
||
<el-input v-model="configForm.appId" placeholder="请输入公众号AppID" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="公众号AppSecret" prop="appSecret">
|
||
<el-input v-model="configForm.appSecret" type="password" placeholder="请输入公众号AppSecret" show-password />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="服务器Token" prop="token">
|
||
<el-input v-model="configForm.token" placeholder="请输入服务器Token" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="消息加解密密钥" prop="encodingAESKey">
|
||
<el-input v-model="configForm.encodingAESKey" placeholder="请输入消息加解密密钥(可选)" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="服务器URL">
|
||
<el-input v-model="webhookUrl" readonly>
|
||
<template #append>
|
||
<el-button @click="copyWebhookUrl">复制</el-button>
|
||
</template>
|
||
</el-input>
|
||
<div class="form-tip">
|
||
请将此URL配置到微信公众平台的服务器配置中
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<el-form-item>
|
||
<el-button type="primary" @click="saveConfig">保存配置</el-button>
|
||
<el-button @click="testConfig">测试配置</el-button>
|
||
<el-button type="success" @click="validateConfig" :loading="validating">验证配置</el-button>
|
||
<el-button type="warning" @click="generateQrCode" :loading="generating">生成二维码</el-button>
|
||
<el-button type="danger" @click="clearQuota" :loading="clearing">清空配额</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-card>
|
||
|
||
<!-- Webhook日志 -->
|
||
|
||
<el-card class="webhook-logs-card">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span>Webhook日志</span>
|
||
<div>
|
||
<el-button type="primary" @click="getWebhookLogs">刷新日志</el-button>
|
||
<el-button type="danger" @click="clearWebhookLogs">清空日志</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<el-table :data="webhookLogs" style="width: 100%">
|
||
<el-table-column label="时间" width="180">
|
||
<template #default="scope">
|
||
{{ formatDate(scope.row.CreatedAt) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="请求方法" prop="requestMethod" width="100" />
|
||
<el-table-column label="请求路径" prop="requestUrl" width="200" />
|
||
<el-table-column label="状态码" prop="responseStatus" width="100">
|
||
<template #default="scope">
|
||
<el-tag :type="getStatusTag(scope.row.responseStatus)">{{ scope.row.responseStatus }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="响应时间" prop="processTime" width="120">
|
||
<template #default="scope">
|
||
{{ scope.row.processTime }}ms
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="请求内容" prop="requestBody" show-overflow-tooltip />
|
||
<el-table-column label="操作" width="120">
|
||
<template #default="scope">
|
||
<el-button type="primary" link @click="viewLogDetail(scope.row)">详情</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-card>
|
||
|
||
<!-- 日志详情对话框 -->
|
||
<el-dialog v-model="logDetailVisible" title="Webhook日志详情" width="800px">
|
||
<div v-if="currentLog" class="log-detail">
|
||
<el-descriptions :column="2" border>
|
||
<el-descriptions-item label="时间">{{ formatDate(currentLog.CreatedAt) }}</el-descriptions-item>
|
||
<el-descriptions-item label="方法">{{ currentLog.requestMethod }}</el-descriptions-item>
|
||
<el-descriptions-item label="路径">{{ currentLog.requestUrl }}</el-descriptions-item>
|
||
<el-descriptions-item label="状态码">
|
||
<el-tag :type="getStatusTag(currentLog.responseStatus)">{{ currentLog.responseStatus }}</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="响应时间">{{ currentLog.processTime }}ms</el-descriptions-item>
|
||
<el-descriptions-item label="IP地址">{{ currentLog.clientIP || '未知' }}</el-descriptions-item>
|
||
</el-descriptions>
|
||
|
||
<div class="log-content">
|
||
<h4>请求内容</h4>
|
||
<pre>{{ formatJSON(currentLog.requestBody) }}</pre>
|
||
|
||
<h4>响应内容</h4>
|
||
<pre>{{ formatJSON(currentLog.responseBody) }}</pre>
|
||
</div>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<!-- 二维码显示对话框 -->
|
||
<el-dialog v-model="qrCodeVisible" title="公众号二维码" width="400px" center>
|
||
<div class="text-center">
|
||
<img v-if="qrCodeUrl" :src="qrCodeUrl" alt="公众号二维码" style="max-width: 300px;" />
|
||
<p class="mt-4 text-gray-600">扫描二维码关注公众号</p>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, onMounted } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { formatDate } from '@/utils/format'
|
||
import {
|
||
getWechatConfig,
|
||
saveWechatConfig,
|
||
testWechatConfig,
|
||
getWebhookLogs as getWebhookLogsApi,
|
||
validateMpConfig,
|
||
generateMpQrCode,
|
||
clearMpQuota
|
||
} from '@/api/wechat/config'
|
||
|
||
defineOptions({
|
||
name: 'MpAccount'
|
||
})
|
||
|
||
const webhookLogs = ref([])
|
||
const logDetailVisible = ref(false)
|
||
const currentLog = ref(null)
|
||
|
||
// 新增的响应式变量
|
||
const validating = ref(false)
|
||
const generating = ref(false)
|
||
const clearing = ref(false)
|
||
const qrCodeVisible = ref(false)
|
||
const qrCodeUrl = ref('')
|
||
|
||
const configForm = reactive({
|
||
appId: '',
|
||
appSecret: '',
|
||
token: '',
|
||
encodingAESKey: ''
|
||
})
|
||
|
||
const configRules = {
|
||
appId: [{ required: true, message: '请输入公众号AppID', trigger: 'blur' }],
|
||
appSecret: [{ required: true, message: '请输入公众号AppSecret', trigger: 'blur' }],
|
||
token: [{ required: true, message: '请输入服务器Token', trigger: 'blur' }]
|
||
}
|
||
|
||
const webhookUrl = ref(window.location.origin + '/api/wechat/official/webhook')
|
||
|
||
// 获取配置
|
||
const getConfig = async() => {
|
||
const res = await getWechatConfig()
|
||
if (res.code === 0) {
|
||
Object.assign(configForm, res.data)
|
||
}
|
||
}
|
||
|
||
// 保存配置
|
||
const saveConfig = async() => {
|
||
// 添加configType字段,标识为公众号类型
|
||
const configData = {
|
||
...configForm,
|
||
configType: 'mp' // 公众号类型标识
|
||
}
|
||
const res = await saveWechatConfig(configData)
|
||
if (res.code === 0) {
|
||
ElMessage.success('配置保存成功')
|
||
}
|
||
}
|
||
|
||
// 测试配置
|
||
const testConfig = async() => {
|
||
// 添加configType字段,标识为公众号类型
|
||
const configData = {
|
||
...configForm,
|
||
configType: 'mp' // 公众号类型标识
|
||
}
|
||
const res = await testWechatConfig(configData)
|
||
if (res.code === 0) {
|
||
ElMessage.success('配置测试成功')
|
||
} else {
|
||
ElMessage.error('配置测试失败:' + res.msg)
|
||
}
|
||
}
|
||
|
||
// 复制Webhook URL
|
||
const copyWebhookUrl = () => {
|
||
navigator.clipboard.writeText(webhookUrl.value).then(() => {
|
||
ElMessage.success('URL已复制到剪贴板')
|
||
})
|
||
}
|
||
|
||
// 获取Webhook日志
|
||
const getWebhookLogs = async() => {
|
||
const res = await getWebhookLogsApi()
|
||
if (res.code === 0) {
|
||
webhookLogs.value = res.data.list || []
|
||
}
|
||
}
|
||
|
||
// 清空Webhook日志
|
||
const clearWebhookLogs = () => {
|
||
ElMessageBox.confirm('确定要清空所有Webhook日志吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(() => {
|
||
webhookLogs.value = []
|
||
ElMessage.success('日志已清空')
|
||
})
|
||
}
|
||
|
||
// 查看日志详情
|
||
const viewLogDetail = (log) => {
|
||
currentLog.value = log
|
||
logDetailVisible.value = true
|
||
}
|
||
|
||
// 验证配置
|
||
const validateConfig = async() => {
|
||
validating.value = true
|
||
try {
|
||
const res = await validateMpConfig()
|
||
if (res.code === 0) {
|
||
ElMessage.success('配置验证成功')
|
||
} else {
|
||
ElMessage.error(res.msg || '配置验证失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('配置验证失败:', error)
|
||
ElMessage.error('配置验证失败')
|
||
} finally {
|
||
validating.value = false
|
||
}
|
||
}
|
||
|
||
// 生成二维码
|
||
const generateQrCode = async() => {
|
||
generating.value = true
|
||
try {
|
||
const res = await generateMpQrCode()
|
||
if (res.code === 0) {
|
||
qrCodeUrl.value = res.data.qrCodeUrl
|
||
qrCodeVisible.value = true
|
||
ElMessage.success('二维码生成成功')
|
||
} else {
|
||
ElMessage.error(res.msg || '二维码生成失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('二维码生成失败:', error)
|
||
ElMessage.error('二维码生成失败')
|
||
} finally {
|
||
generating.value = false
|
||
}
|
||
}
|
||
|
||
// 清空配额
|
||
const clearQuota = async() => {
|
||
try {
|
||
await ElMessageBox.confirm('确定要清空API配额吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
|
||
clearing.value = true
|
||
const res = await clearMpQuota()
|
||
if (res.code === 0) {
|
||
ElMessage.success('API配额清空成功')
|
||
} else {
|
||
ElMessage.error(res.msg || 'API配额清空失败')
|
||
}
|
||
} catch (error) {
|
||
if (error !== 'cancel') {
|
||
console.error('API配额清空失败:', error)
|
||
ElMessage.error('API配额清空失败')
|
||
}
|
||
} finally {
|
||
clearing.value = false
|
||
}
|
||
}
|
||
|
||
// 获取服务类型标签
|
||
const getServiceTypeTag = (type) => {
|
||
const typeMap = {
|
||
0: 'info', // 订阅号
|
||
1: 'success', // 由历史老帐号升级后的订阅号
|
||
2: 'warning' // 服务号
|
||
}
|
||
return typeMap[type] || 'info'
|
||
}
|
||
|
||
// 获取服务类型名称
|
||
const getServiceTypeName = (type) => {
|
||
const typeMap = {
|
||
0: '订阅号',
|
||
1: '订阅号(老账号)',
|
||
2: '服务号'
|
||
}
|
||
return typeMap[type] || '未知'
|
||
}
|
||
|
||
// 获取认证类型标签
|
||
const getVerifyTypeTag = (type) => {
|
||
const typeMap = {
|
||
'-1': 'danger', // 未认证
|
||
0: 'success', // 微信认证
|
||
1: 'warning' // 新浪微博认证
|
||
}
|
||
return typeMap[type] || 'info'
|
||
}
|
||
|
||
// 获取认证类型名称
|
||
const getVerifyTypeName = (type) => {
|
||
const typeMap = {
|
||
'-1': '未认证',
|
||
0: '微信认证',
|
||
1: '新浪微博认证'
|
||
}
|
||
return typeMap[type] || '未知'
|
||
}
|
||
|
||
// 获取状态标签
|
||
const getStatusTag = (status) => {
|
||
if (status >= 200 && status < 300) return 'success'
|
||
if (status >= 400 && status < 500) return 'warning'
|
||
if (status >= 500) return 'danger'
|
||
return 'info'
|
||
}
|
||
|
||
// 格式化JSON
|
||
const formatJSON = (str) => {
|
||
if (!str) return '无'
|
||
try {
|
||
return JSON.stringify(JSON.parse(str), null, 2)
|
||
} catch {
|
||
return str
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
getConfig()
|
||
getWebhookLogs()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.config-card,
|
||
.webhook-logs-card {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.card-header span {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.form-tip {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
.log-detail {
|
||
padding: 20px 0;
|
||
}
|
||
|
||
.log-content {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.log-content h4 {
|
||
margin: 20px 0 10px 0;
|
||
color: #303133;
|
||
}
|
||
|
||
.log-content pre {
|
||
background: #f5f5f5;
|
||
padding: 15px;
|
||
border-radius: 4px;
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.text-center {
|
||
text-align: center;
|
||
}
|
||
|
||
.mt-4 {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.text-gray-600 {
|
||
color: #6b7280;
|
||
}
|
||
</style>
|