小程序配置页:微信密钥与排行榜/商家端背景图后台管理
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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: '微信与页面资源配置' }
|
||||
},
|
||||
|
||||
|
||||
// ========== 🔥 新增:客服会话管理 ==========
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
311
src/views/miniapp/MiniappConfig.vue
Normal file
311
src/views/miniapp/MiniappConfig.vue
Normal 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>
|
||||
Reference in New Issue
Block a user