小程序配置页:微信密钥与排行榜/商家端背景图后台管理

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-06-27 01:23:12 +08:00
parent cf07faccc4
commit 083ad26769
3 changed files with 380 additions and 49 deletions

View File

@@ -218,6 +218,16 @@ const routes = [
component: () => import('@/views/miniapp/PopupNotice.vue'),
meta: { title: '弹窗公告管理' }
},
{
path: 'miniapp/script-config',
component: () => import('@/views/miniapp/ScriptConfig.vue'),
meta: { title: '自动弹窗及话术配置' }
},
{
path: 'miniapp/sys-config',
component: () => import('@/views/miniapp/MiniappConfig.vue'),
meta: { title: '微信与页面资源配置' }
},
// ========== 🔥 新增:客服会话管理 ==========

View File

@@ -135,6 +135,8 @@
<span>小程序配置</span>
</template>
<el-menu-item index="/miniapp/popup-notice">弹窗公告管理</el-menu-item>
<el-menu-item index="/miniapp/script-config">自动弹窗及话术配置</el-menu-item>
<el-menu-item index="/miniapp/sys-config">微信与页面资源配置</el-menu-item>
</el-sub-menu>
@@ -187,18 +189,21 @@
</router-view>
</el-main>
</el-container>
<KefuConnect v-if="hasAnyChatPerm" />
</el-container>
</template>
<script setup>
import { computed, ref, onMounted, onUnmounted, getCurrentInstance, provide } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { ElMessage, ElNotification } from 'element-plus'
import {
List, User, Warning, Money, Fold, Expand, Connection, Setting,
Goods, UserFilled, Comment,DataAnalysis, Medal // 新增
} from '@element-plus/icons-vue'
import request from '@/utils/request'
import KefuConnect from '@/components/KefuConnect.vue'
import { disconnectKefuChat } from '@/utils/kefuChat'
const { proxy } = getCurrentInstance()
const route = useRoute()
@@ -247,6 +252,7 @@ const fetchStats = async () => {
}
const handleLogout = () => {
disconnectKefuChat()
localStorage.clear()
router.push('/login')
ElMessage.success('已退出登录')
@@ -277,63 +283,63 @@ const fetchKefuPermission = async () => {
const { permissions, goeasy_appkey } = res.data
kefuPermissions.value = permissions || []
goeasyAppkey.value = goeasy_appkey || ''
// 如果有权限且有appkey初始化GoEasy仅一次
if (hasAnyChatPerm.value && goeasyAppkey.value) {
initGoEasy()
}
}
} catch (error) {
console.error('获取客服权限失败:', error)
}
}
// 初始化GoEasy实例并连接
const initGoEasy = () => {
// 防止重复初始化
if (window.goeasy) return
// 1. 初始化实例
window.goeasy = GoEasy.getInstance({
host: 'hangzhou.goeasy.io', // 根据实际区域调整
appkey: goeasyAppkey.value,
modules: ['im']
})
// 2. 构建客服自己的userId = KF + 账号
const agentId = 'KF' + userPhone.value
const agentData = {
name: userPhone.value, // 客服名称
avatar: '' // 可后续加上头像
}
// 3. 建立连接
window.goeasy.connect({
id: agentId,
data: agentData,
onSuccess: () => {
console.log('客服GoEasy连接成功')
// 连接成功后通知Agent页面通过provide
proxy.$emitter.emit('goeasy-connected')
},
onFailed: (error) => {
console.error('GoEasy连接失败', error)
ElMessage.error('消息服务连接失败,请刷新重试')
},
onProgress: (attempts) => {
console.log('GoEasy正在重连第' + attempts + '次')
}
})
// 4. 保存客服标识到全局
window.__kefuAgentId = agentId
}
// 监听子组件传来的未读消息更新
// 监听子组件传来的未读消息更新(由 KefuConnect + kefuChat 统一推送)
const onAgentUnread = (count) => {
agentUnread.value = count
}
let lastUnreadForNotify = 0
let unreadBaselineReady = false
const showMessageNotification = (title, message) => {
ElNotification({
title,
message,
type: 'info',
duration: 0,
showClose: true,
position: 'top-right',
customClass: 'kefu-msg-notify',
onClick: () => {
ElNotification.closeAll()
router.push('/chat/agent')
}
})
}
const onMessageNotify = (payload) => {
if (route.path === '/chat/agent') return
const customerId = payload?.customerId || '用户'
const preview = payload?.preview || '收到新消息'
showMessageNotification('新会话消息', `${customerId}: ${preview}`)
}
const onUnreadBumpNotify = (count) => {
if (!unreadBaselineReady) {
lastUnreadForNotify = count
unreadBaselineReady = true
return
}
if (route.path === '/chat/agent') {
lastUnreadForNotify = count
return
}
if (count <= 0) {
lastUnreadForNotify = 0
return
}
if (count > lastUnreadForNotify) {
showMessageNotification('会话未读提醒', `您有 ${count} 条未读消息,点击查看`)
}
lastUnreadForNotify = count
}
// 向子组件提供客服相关数据
provide('kefuPermissions', kefuPermissions)
provide('permPrefixMap', permPrefixMap)
@@ -342,15 +348,19 @@ provide('hasAnyChatPerm', hasAnyChatPerm)
onMounted(() => {
fetchStats()
fetchKefuPermission() // 登录后先获取权限
fetchKefuPermission()
proxy.$emitter?.on('refresh-stats', fetchStats)
proxy.$emitter?.on('agent-unread', onAgentUnread)
proxy.$emitter?.on('agent-unread', onUnreadBumpNotify)
proxy.$emitter?.on('kefu-message-notify', onMessageNotify)
})
onUnmounted(() => {
if (abortController) abortController.abort()
proxy.$emitter?.off('refresh-stats', fetchStats)
proxy.$emitter?.off('agent-unread', onAgentUnread)
proxy.$emitter?.off('agent-unread', onUnreadBumpNotify)
proxy.$emitter?.off('kefu-message-notify', onMessageNotify)
})
</script>

View File

@@ -0,0 +1,311 @@
<template>
<div class="miniapp-config">
<div v-if="!hasPermission" class="no-permission">
<div class="no-permission-icon">🔒</div>
<div class="no-permission-text">您无权访问此页面</div>
</div>
<div v-else>
<div class="header-bar">
<h2>文赫电竞 · 小程序配置</h2>
<el-text type="info">AppID/服务号/支付密钥排行榜与商家端背景图均可在此配置</el-text>
</div>
<el-tabs v-model="activeTab" class="config-tabs">
<el-tab-pane label="微信与支付配置" name="sys">
<el-form :model="sysForm" label-width="160px" class="sys-form" v-loading="sysLoading">
<el-form-item label="小程序 AppID">
<el-input v-model="sysForm.weixin_appid" />
</el-form-item>
<el-form-item label="小程序 Secret">
<el-input v-model="sysForm.weixin_secret" type="password" show-password placeholder="留空则不修改" />
</el-form-item>
<el-form-item label="服务号 AppID">
<el-input v-model="sysForm.weixin_official_appid" />
</el-form-item>
<el-form-item label="服务号 Secret">
<el-input v-model="sysForm.weixin_official_secret" type="password" show-password placeholder="留空则不修改" />
</el-form-item>
<el-form-item label="服务号 Token">
<el-input v-model="sysForm.weixin_official_token" />
</el-form-item>
<el-form-item label="模板消息 ID">
<el-input v-model="sysForm.weixin_template_id" />
</el-form-item>
<el-form-item label="服务号广播">
<el-switch v-model="sysForm.weixin_broadcast_enabled" />
</el-form-item>
<el-form-item label="微信支付商户号">
<el-input v-model="sysForm.weixin_mchid" />
</el-form-item>
<el-form-item label="商户密钥">
<el-input v-model="sysForm.weixin_shanghu_miyao" type="password" show-password placeholder="留空则不修改" />
</el-form-item>
<el-form-item label="支付回调 URL">
<el-input v-model="sysForm.weixin_notify_url" />
</el-form-item>
<el-form-item label="业务回调根 URL">
<el-input v-model="sysForm.notify_base_url" placeholder="https://wenhe.nmslb.com/hqhd" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="sysSaving" @click="saveSysConfig">保存微信配置</el-button>
<el-button @click="loadSysConfig">刷新</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="页面背景与图标" name="assets">
<div class="asset-toolbar">
<el-select v-model="assetGroup" placeholder="资源组" style="width: 200px" @change="loadAssets">
<el-option label="排行榜" value="paihangbang" />
<el-option label="商家端" value="shangjiaduan" />
</el-select>
<el-button type="primary" :loading="assetSaving" @click="saveAssets">保存路径</el-button>
<el-button @click="loadAssets">刷新</el-button>
</div>
<el-table :data="assetList" v-loading="assetLoading" border class="asset-table">
<el-table-column prop="ziyuan_key" label="键名" width="160" />
<el-table-column prop="miaoshu" label="说明" min-width="120" />
<el-table-column label="预览" width="100">
<template #default="{ row }">
<el-image v-if="row.ziyuan_path" :src="fullUrl(row.ziyuan_path)" fit="cover" class="thumb" />
</template>
</el-table-column>
<el-table-column label="OSS 相对路径" min-width="280">
<template #default="{ row }">
<el-input v-model="row.ziyuan_path" size="small" />
</template>
</el-table-column>
<el-table-column label="上传" width="120" fixed="right">
<template #default="{ row }">
<el-upload
:show-file-list="false"
accept="image/*"
:http-request="(opt) => uploadAsset(opt, row)"
>
<el-button type="primary" link>上传替换</el-button>
</el-upload>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import request from '@/utils/request'
const zhanghao = ref(localStorage.getItem('username') || '')
const hasPermission = ref(true)
const activeTab = ref('sys')
const ossBaseUrl = ref(window.$ossURL || '')
const sysLoading = ref(false)
const sysSaving = ref(false)
const sysForm = reactive({
weixin_appid: '',
weixin_secret: '',
weixin_official_appid: '',
weixin_official_secret: '',
weixin_official_token: '',
weixin_template_id: '',
weixin_broadcast_enabled: true,
weixin_mchid: '',
weixin_shanghu_miyao: '',
weixin_notify_url: '',
notify_base_url: '',
})
const assetGroup = ref('paihangbang')
const assetList = ref([])
const assetLoading = ref(false)
const assetSaving = ref(false)
const fullUrl = (path) => {
if (!path) return ''
if (path.startsWith('http')) return path
const base = ossBaseUrl.value.replace(/\/$/, '')
return `${base}/${String(path).replace(/^\//, '')}`
}
const loadSysConfig = async () => {
sysLoading.value = true
try {
const res = await request.post('/peizhi/xcxsyshq', { zhanghao: zhanghao.value })
if (res.code === 403 || res.code === 1 && res.msg?.includes('权限')) {
hasPermission.value = false
return
}
if (res.code === 0 && res.data) {
Object.assign(sysForm, {
weixin_appid: res.data.weixin_appid || '',
weixin_secret: '',
weixin_official_appid: res.data.weixin_official_appid || '',
weixin_official_secret: '',
weixin_official_token: res.data.weixin_official_token || '',
weixin_template_id: res.data.weixin_template_id || '',
weixin_broadcast_enabled: res.data.weixin_broadcast_enabled ?? true,
weixin_mchid: res.data.weixin_mchid || '',
weixin_shanghu_miyao: '',
weixin_notify_url: res.data.weixin_notify_url || '',
notify_base_url: res.data.notify_base_url || '',
})
} else {
ElMessage.error(res.msg || '加载失败')
}
} catch {
ElMessage.error('网络错误')
} finally {
sysLoading.value = false
}
}
const saveSysConfig = async () => {
sysSaving.value = true
try {
const payload = { ...sysForm }
if (!payload.weixin_secret) delete payload.weixin_secret
if (!payload.weixin_official_secret) delete payload.weixin_official_secret
if (!payload.weixin_shanghu_miyao) delete payload.weixin_shanghu_miyao
const res = await request.post('/peizhi/xcxsysgx', {
zhanghao: zhanghao.value,
config: payload,
})
if (res.code === 0) {
ElMessage.success('微信配置已保存')
loadSysConfig()
} else {
ElMessage.error(res.msg || '保存失败')
}
} catch {
ElMessage.error('网络错误')
} finally {
sysSaving.value = false
}
}
const loadAssets = async () => {
assetLoading.value = true
try {
const res = await request.post('/peizhi/xcxyzycx', {
zhanghao: zhanghao.value,
ziyuan_zu: assetGroup.value,
})
if (res.code === 403) {
hasPermission.value = false
return
}
if (res.code === 0) {
const group = res.data?.groups?.[assetGroup.value] || {}
assetList.value = Object.keys(group).map((key) => ({
ziyuan_zu: assetGroup.value,
ziyuan_key: key,
ziyuan_path: group[key],
miaoshu: key,
}))
} else {
ElMessage.error(res.msg || '加载资源失败')
}
} catch {
ElMessage.error('网络错误')
} finally {
assetLoading.value = false
}
}
const saveAssets = async () => {
assetSaving.value = true
try {
const items = assetList.value.map((row) => ({
ziyuan_zu: row.ziyuan_zu,
ziyuan_key: row.ziyuan_key,
ziyuan_path: row.ziyuan_path,
}))
const res = await request.post('/peizhi/xcxyzgx', {
zhanghao: zhanghao.value,
items,
})
if (res.code === 0) {
ElMessage.success('资源路径已保存')
loadAssets()
} else {
ElMessage.error(res.msg || '保存失败')
}
} catch {
ElMessage.error('网络错误')
} finally {
assetSaving.value = false
}
}
const uploadAsset = async ({ file }, row) => {
const formData = new FormData()
formData.append('zhanghao', zhanghao.value)
formData.append('ziyuan_zu', row.ziyuan_zu)
formData.append('ziyuan_key', row.ziyuan_key)
formData.append('image', file)
try {
const res = await request.post('/peizhi/xcxyzsc', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
})
if (res.code === 0) {
ElMessage.success('上传成功')
row.ziyuan_path = res.data?.ziyuan_path || row.ziyuan_path
} else {
ElMessage.error(res.msg || '上传失败')
}
} catch {
ElMessage.error('上传失败')
}
}
onMounted(() => {
loadSysConfig()
loadAssets()
})
</script>
<style scoped>
.miniapp-config {
padding: 8px 4px;
}
.header-bar {
margin-bottom: 16px;
}
.header-bar h2 {
margin: 0 0 8px;
color: #00f2ff;
}
.no-permission {
text-align: center;
padding: 80px 20px;
color: rgba(255, 255, 255, 0.6);
}
.no-permission-icon {
font-size: 48px;
margin-bottom: 12px;
}
.sys-form {
max-width: 720px;
margin-top: 12px;
}
.asset-toolbar {
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 16px;
}
.asset-table {
margin-top: 8px;
}
.thumb {
width: 56px;
height: 56px;
border-radius: 6px;
}
</style>