fix: 登录页校验JWT有效性;同步后台多项功能更新

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-01 11:36:13 +08:00
parent 4ed234454b
commit adaf9b2522
10 changed files with 1224 additions and 185 deletions

View File

@@ -5,7 +5,7 @@
width="700px"
:close-on-click-modal="false"
destroy-on-close
@open="onDialogOpen"
@opened="onDialogOpened"
@close="onDialogClose"
>
<div class="chat-container">
@@ -54,7 +54,7 @@
<div class="input-area">
<el-input v-model="inputText" placeholder="请输入消息..." @keyup.enter="sendText" size="small">
<template #append>
<el-button @click="sendText" :disabled="!inputText.trim()">发送</el-button>
<el-button @click="sendText" :disabled="!inputText.trim() || !agentOnline">发送</el-button>
</template>
</el-input>
</div>
@@ -63,16 +63,24 @@
</template>
<script setup>
import { ref, computed, nextTick, onUnmounted } from 'vue'
import { ref, computed, nextTick, onUnmounted, getCurrentInstance } from 'vue'
import { ElMessage } from 'element-plus'
import { Refresh, WarningFilled } from '@element-plus/icons-vue'
import GoEasy from 'goeasy'
import {
isCsMessageForCustomer,
buildCsTextMessage,
ensureCustomerAccepted
} from '@/utils/kefuChat'
const { proxy } = getCurrentInstance()
const props = defineProps({
visible: Boolean,
teamId: String,
customerId: String,
customerData: Object
customerData: Object,
agentOnline: { type: Boolean, default: true }
})
const emit = defineEmits(['update:visible'])
@@ -97,8 +105,38 @@ const isSelf = (msg) => msg.senderId === agentId.value
let csteam = null
let unlisten = null
let sessionReady = false
// ----- 使用 listenCustomer 监听客户消息GoEasy 官方方案) -----
const pushMessage = (message) => {
if (!message?.messageId) return
if (messages.value.some(m => m.messageId === message.messageId)) return
message.showTime = shouldShowTime(message.timestamp)
messages.value.push(message)
scrollToBottom()
markRead()
}
const handleIncomingMessage = (message) => {
if (!isCsMessageForCustomer(message, props.customerId, props.teamId, agentId.value)) return
console.log('[ChatDialog] 收到实时消息:', message)
const tempMsg = messages.value.find(
m => m._tempId && m.payload?.text === message.payload?.text && m.senderId === agentId.value
)
if (tempMsg && message.senderId === agentId.value) {
tempMsg.messageId = message.messageId
tempMsg.status = 'success'
tempMsg.timestamp = message.timestamp
tempMsg._tempId = null
scrollToBottom()
markRead()
return
}
pushMessage(message)
}
const onGlobalCsMessage = (message) => {
handleIncomingMessage(message)
}
const startListenCustomer = () => {
if (!window.goeasy || !props.teamId || !props.customerId) return
csteam = window.goeasy.im.csteam(props.teamId)
@@ -110,24 +148,7 @@ const startListenCustomer = () => {
csteam.listenCustomer({
id: props.customerId,
onNewMessage: (message) => {
console.log('[ChatDialog] 客户新消息:', message)
// 避免与临时消息重复
const tempMsg = messages.value.find(m => m._tempId && m.payload?.text === message.payload?.text)
if (tempMsg && message.status === 'success') {
tempMsg.messageId = message.messageId
tempMsg.status = 'success'
tempMsg.timestamp = message.timestamp
tempMsg._tempId = null
scrollToBottom()
markRead()
return
}
// 去重
if (messages.value.some(m => m.messageId === message.messageId)) return
message.showTime = shouldShowTime(message.timestamp)
messages.value.push(message)
scrollToBottom()
markRead()
handleIncomingMessage(message)
},
onStatusUpdated: (status) => {
console.log('[ChatDialog] 客户状态更新:', status)
@@ -190,23 +211,28 @@ const refreshHistory = () => {
}
// ----- 发送消息 -----
const sendText = () => {
const sendText = async () => {
const text = inputText.value.trim()
if (!text) return
if (!window.goeasy) return ElMessage.error('聊天服务未连接')
const message = window.goeasy.im.createTextMessage({
text: text,
to: {
type: 'cs',
id: props.customerId,
data: props.customerData || { name: props.customerId, avatar: '' }
if (!props.agentOnline && !window.__kefuCsOnline) {
ElMessage.warning('客服正在连接,请稍候…')
return
}
if (!sessionReady) {
try {
await ensureCustomerAccepted(props.customerId, props.customerData)
sessionReady = true
} catch (error) {
ElMessage.error('会话未接入:' + (error?.content || '未知错误'))
return
}
})
}
message.teamId = props.teamId
const customerData = props.customerData || { name: props.customerId, avatar: '' }
const message = buildCsTextMessage(props.teamId, props.customerId, customerData, text)
console.log('[ChatDialog] 发送消息:', message)
console.log('[ChatDialog] 发送消息 → 用户ID:', props.customerId, message)
const tempId = 'local-' + Date.now()
const tempMsg = {
@@ -271,23 +297,36 @@ const markRead = () => {
}
const previewImage = (url) => window.open(url)
// ----- 弹窗生命周期 -----
const onDialogOpen = () => {
console.log('[ChatDialog] 弹窗打开')
// ----- 弹窗生命周期@opened 确保 DOM/props 就绪) -----
const onDialogOpened = async () => {
console.log('[ChatDialog] 弹窗打开, customerId=', props.customerId)
if (!agentId.value) agentId.value = window.__kefuAgentId || 'KF' + localStorage.getItem('username')
// 先监听客户,再加载历史
sessionReady = false
proxy.$emitter.on('kefu-cs-new-message', onGlobalCsMessage)
try {
await ensureCustomerAccepted(
props.customerId,
props.customerData || { name: props.customerId, avatar: '' }
)
sessionReady = true
} catch (error) {
ElMessage.warning('接入会话:' + (error?.content || '请稍后重试'))
}
startListenCustomer()
loadHistory()
}
const onDialogClose = () => {
// 关闭弹窗时不需要主动取消 listenCustomer因为我们会重新 listen
// 但可以清理 unlisten 引用
proxy.$emitter.off('kefu-cs-new-message', onGlobalCsMessage)
unlisten = null
sessionReady = false
messages.value = []
lastTimestamp.value = null
}
onUnmounted(() => {
proxy.$emitter.off('kefu-cs-new-message', onGlobalCsMessage)
unlisten = null
})
</script>

View File

@@ -3,91 +3,101 @@
</template>
<script setup>
import { onMounted, onUnmounted, getCurrentInstance } from 'vue'
import { onMounted, getCurrentInstance } from 'vue'
import { ElMessage } from 'element-plus'
import request from '@/utils/request'
import GoEasy from 'goeasy'
import {
afterGoeasyConnected,
resumeKefuSession,
onGoeasyReconnected,
isGoeasyConnected
} from '@/utils/kefuChat'
const { proxy } = getCurrentInstance()
const emit = defineEmits(['connected'])
// 每次进入页面都重新获取权限并建立连接
async function connect() {
console.log('[KefuConnect] 开始获取客服权限...')
const phone = localStorage.getItem('username')
if (!phone) {
console.error('[KefuConnect] 未找到手机号')
return
}
if (!phone) return
try {
const res = await request.post('/houtai/kfhqltqx', { phone })
if (res.code !== 0) {
console.warn('[KefuConnect] 接口返回非0:', res)
return
}
if (res.code !== 0) return
const { permissions, goeasy_appkey } = res.data
console.log('[KefuConnect] 权限码:', permissions)
console.log('[KefuConnect] GoEasy appkey:', goeasy_appkey)
if (!permissions || permissions.length === 0) {
console.warn('[KefuConnect] 无聊天权限,不能连接')
ElMessage.warning('您没有客服聊天权限')
return
}
if (!goeasy_appkey) {
console.warn('[KefuConnect] 未获取到 appkey')
return
}
// 断开旧连接
if (window.goeasy) {
console.log('[KefuConnect] 断开旧连接...')
try { window.goeasy.disconnect({}) } catch (e) {}
window.goeasy = null
}
if (!permissions?.length || !goeasy_appkey) return
const agentId = 'KF' + phone
console.log('[KefuConnect] 客服ID:', agentId)
// 创建新实例
if (
window.goeasy &&
window.__kefuAgentId === agentId &&
window.__kefuAppkey === goeasy_appkey &&
isGoeasyConnected()
) {
resumeKefuSession(proxy.$emitter, phone)
emit('connected')
proxy.$emitter.emit('goeasy-connected')
return
}
if (window.goeasy) {
try { window.goeasy.disconnect({}) } catch (e) {}
window.goeasy = null
window.__kefuCsOnline = false
}
window.__kefuAppkey = goeasy_appkey
if (window.__kefuUserWantsOffline === undefined) {
window.__kefuUserWantsOffline = false
}
const goeasy = GoEasy.getInstance({
host: 'hangzhou.goeasy.io', // 根据实际区域修改
host: 'hangzhou.goeasy.io',
appkey: goeasy_appkey,
modules: ['im']
})
console.log('[KefuConnect] GoEasy 实例创建成功,准备连接...')
goeasy.connect({
id: agentId,
data: { name: phone, avatar: '' },
onSuccess: () => {
console.log('[KefuConnect] ✅ 连接成功!')
window.goeasy = goeasy
window.__kefuAgentId = agentId
// 通知父组件Agent.vue可以进行上线操作
proxy.$emitter.emit('kefu-goeasy-ready', true)
afterGoeasyConnected(proxy.$emitter, phone)
emit('connected')
proxy.$emitter.emit('goeasy-connected')
},
onFailed: (error) => {
console.error('[KefuConnect] 连接失败:', error)
ElMessage.error('消息服务连接失败,请重试')
console.error('[KefuConnect] 连接失败:', error)
proxy.$emitter.emit('kefu-goeasy-ready', false)
ElMessage.error('消息服务连接失败,请刷新重试')
},
onProgress: (attempts) => {
console.log('[KefuConnect] 🔄 重连中... 第' + attempts + '次')
// 重连心跳:不要把 ready 置 false否则上线按钮会卡死
console.log('[KefuConnect] 重连中…', attempts)
}
})
// 监听重连成功GoEasy 2.x
try {
goeasy.on && goeasy.on('reconnected', () => {
console.log('[KefuConnect] 重连成功')
onGoeasyReconnected(proxy.$emitter, phone)
proxy.$emitter.emit('goeasy-connected')
})
} catch (e) {
// ignore
}
} catch (e) {
console.error('[KefuConnect] 连接异常:', e)
proxy.$emitter.emit('kefu-goeasy-ready', false)
}
}
onMounted(() => {
console.log('[KefuConnect] 组件挂载,开始建立客服长连接')
connect()
})
onUnmounted(() => {
console.log('[KefuConnect] 组件卸载,但保持连接存活(可后续优化)')
})
</script>
</script>

398
src/utils/kefuChat.js Normal file
View File

@@ -0,0 +1,398 @@
import GoEasy from 'goeasy'
export const KEFU_TEAM_ID = 'support_team'
const NOTIFY_DEBOUNCE_MS = 2500
const ONLINE_TIMEOUT_MS = 12000
let lastNotifyAt = 0
let onlineInFlight = null
export function getCsteam() {
if (!window.goeasy) return null
return window.goeasy.im.csteam(KEFU_TEAM_ID)
}
export function isGoeasyConnected() {
if (!window.goeasy) return false
try {
const status = window.goeasy.getConnectionStatus()
return status === 'connected' || status === 'reconnected'
} catch (e) {
return !!window.goeasy
}
}
export function calcUnreadFromConversations(conversations = []) {
return conversations.reduce((sum, c) => sum + (c.unread || 0), 0)
}
/** 查询服务端真实在线状态 */
export function checkCsOnlineStatus() {
return new Promise((resolve) => {
const csteam = getCsteam()
if (!csteam) {
resolve(false)
return
}
csteam.isOnline({
onSuccess: (online) => {
window.__kefuCsOnline = !!online
resolve(!!online)
},
onFailed: () => resolve(false)
})
})
}
/** 拉取已接入 + 待接入会话未读总数,并触发 agent-unread */
export function refreshKefuUnread(emitter) {
if (!window.goeasy) return
const im = window.goeasy.im
let activeUnread = 0
im.latestConversations({
onSuccess: (result) => {
const content = result.content || {}
const convs = content.conversations || []
const csActive = convs.filter(c => c.type === 'cs' && !c.ended)
activeUnread = content.unreadTotal || calcUnreadFromConversations(csActive)
im.pendingConversations({
onSuccess: (pendingResult) => {
const pending = pendingResult.content?.conversations || []
const pendingUnread = calcUnreadFromConversations(pending)
emitter?.emit('agent-unread', activeUnread + pendingUnread)
},
onFailed: () => {
emitter?.emit('agent-unread', activeUnread)
}
})
},
onFailed: () => {}
})
}
function emitCsOnline(emitter, online) {
window.__kefuCsOnline = !!online
emitter?.emit('kefu-cs-online', window.__kefuCsOnline)
}
export function ensureCsOnline({ phone, onSuccess, onFailed, emitter, force = false } = {}) {
if (!window.goeasy) {
onFailed?.({ content: '消息服务未连接' })
return
}
const csteam = getCsteam()
if (!csteam) {
onFailed?.({ content: '客服团队未初始化' })
return
}
const finishSuccess = () => {
emitCsOnline(emitter, true)
onSuccess?.()
}
const finishFailed = (error) => {
emitCsOnline(emitter, false)
onFailed?.(error)
}
// 非强制时:先查服务端是否已在线,避免重复 online 卡死
if (!force && window.__kefuCsOnline) {
checkCsOnlineStatus().then((online) => {
if (online) {
finishSuccess()
} else {
window.__kefuCsOnline = false
doCsOnline(csteam, phone, emitter, finishSuccess, finishFailed)
}
})
return
}
if (force) {
window.__kefuCsOnline = false
}
doCsOnline(csteam, phone, emitter, finishSuccess, finishFailed)
}
function doCsOnline(csteam, phone, emitter, onSuccess, onFailed) {
if (onlineInFlight) {
onlineInFlight
.then(() => onSuccess?.())
.catch((e) => onFailed?.(e))
return
}
const displayName = phone || localStorage.getItem('username') || '客服'
let finished = false
const timer = setTimeout(() => {
if (finished) return
finished = true
onlineInFlight = null
emitCsOnline(emitter, false)
onFailed?.({ content: '上线超时,请检查网络后重试' })
}, ONLINE_TIMEOUT_MS)
onlineInFlight = new Promise((resolve, reject) => {
csteam.online({
teamData: { name: '阿龙电竞客服中心', avatar: '' },
agentData: { name: displayName, avatar: '' },
onSuccess: () => {
if (finished) return
finished = true
clearTimeout(timer)
onlineInFlight = null
emitCsOnline(emitter, true)
resolve(true)
},
onFailed: (error) => {
if (finished) return
finished = true
clearTimeout(timer)
onlineInFlight = null
// 已在线视为成功
const msg = (error?.content || '').toLowerCase()
if (msg.includes('online') || msg.includes('已') || msg.includes('already')) {
emitCsOnline(emitter, true)
resolve(true)
return
}
emitCsOnline(emitter, false)
reject(error)
}
})
})
onlineInFlight
.then(() => onSuccess?.())
.catch((e) => onFailed?.(e))
}
/** 用户手动上线 */
export function goOnlineManually({ phone, emitter, onSuccess, onFailed } = {}) {
window.__kefuUserWantsOffline = false
ensureCsOnline({
phone,
emitter,
force: true,
onSuccess,
onFailed
})
}
/** 用户手动下线(不断开 GoEasy */
export function goOfflineManually({ emitter, onSuccess, onFailed } = {}) {
window.__kefuUserWantsOffline = true
onlineInFlight = null
const csteam = getCsteam()
if (!csteam) {
emitCsOnline(emitter, false)
onSuccess?.()
return
}
csteam.offline({
onSuccess: () => {
emitCsOnline(emitter, false)
onSuccess?.()
},
onFailed: (error) => onFailed?.(error)
})
}
export function disconnectKefuChat() {
onlineInFlight = null
if (window.__kefuUnreadCleanup) {
window.__kefuUnreadCleanup()
window.__kefuUnreadCleanup = null
}
if (window.goeasy) {
try {
window.goeasy.disconnect({})
} catch (e) {
// ignore
}
window.goeasy = null
}
window.__kefuCsOnline = false
window.__kefuUserWantsOffline = false
window.__kefuAgentId = null
window.__kefuAppkey = null
}
export function getCsMessageCustomerId(message, teamId = KEFU_TEAM_ID, agentId) {
if (!message) return null
const tid = message.teamId || teamId
const aid = agentId || window.__kefuAgentId
const sender = message.senderId
const to = message.to || message.receiverId
try {
if (typeof message.customerId === 'function') {
const cid = message.customerId()
if (cid && cid !== tid) return cid
}
} catch (e) {
// ignore
}
const parties = [sender, to].filter(Boolean)
for (const p of parties) {
if (p !== tid && p !== aid && !String(p).startsWith('KF')) return p
}
if (sender === aid && to) return to
if (to === aid && sender) return sender
if (sender === tid && to) return to
if (to === tid && sender) return sender
return null
}
export function isCsMessageForCustomer(message, customerId, teamId = KEFU_TEAM_ID, agentId) {
if (!message || !customerId) return false
if (message.teamId && message.teamId !== teamId) return false
const cid = getCsMessageCustomerId(message, teamId, agentId)
return cid === customerId
}
export function buildCsTextMessage(teamId, customerId, customerData, text) {
const message = window.goeasy.im.createTextMessage({
text,
to: {
type: GoEasy.IM_SCENE.CS,
id: customerId,
data: customerData || { name: customerId, avatar: '' }
}
})
message.teamId = teamId
message.accepted = true
return message
}
export function ensureCustomerAccepted(customerId, customerData) {
return new Promise((resolve, reject) => {
if (!window.goeasy || !customerId) {
reject({ content: '连接未就绪' })
return
}
if (!window.__kefuCsOnline) {
reject({ content: '请先上线后再聊天' })
return
}
const csteam = getCsteam()
if (!csteam) {
reject({ content: '客服团队未初始化' })
return
}
csteam.accept({
customer: {
id: customerId,
data: customerData || { name: customerId, avatar: '' }
},
onSuccess: () => resolve(),
onFailed: (error) => {
const msg = (error?.content || '').toLowerCase()
if (msg.includes('already') || msg.includes('exist') || msg.includes('已')) {
resolve()
return
}
reject(error)
}
})
})
}
function maybeNotifyNewMessage(emitter, message) {
const aid = window.__kefuAgentId
if (message?.senderId && message.senderId === aid) return
const now = Date.now()
if (now - lastNotifyAt < NOTIFY_DEBOUNCE_MS) return
lastNotifyAt = now
const customerId = getCsMessageCustomerId(message)
const preview = message?.payload?.text || message?.payload?.name || '[新消息]'
emitter?.emit('kefu-message-notify', {
customerId,
preview: String(preview).slice(0, 80),
message
})
}
function setupGlobalListeners(emitter) {
if (!window.goeasy) return () => {}
const im = window.goeasy.im
const onRefresh = () => refreshKefuUnread(emitter)
const onCsMessage = (message) => {
emitter?.emit('kefu-cs-new-message', message)
maybeNotifyNewMessage(emitter, message)
refreshKefuUnread(emitter)
}
im.on(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, onRefresh)
im.on(GoEasy.IM_EVENT.PENDING_CONVERSATIONS_UPDATED, onRefresh)
im.on(GoEasy.IM_EVENT.CS_MESSAGE_RECEIVED, onCsMessage)
refreshKefuUnread(emitter)
return () => {
im.off(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, onRefresh)
im.off(GoEasy.IM_EVENT.PENDING_CONVERSATIONS_UPDATED, onRefresh)
im.off(GoEasy.IM_EVENT.CS_MESSAGE_RECEIVED, onCsMessage)
}
}
export function bindGlobalUnreadListeners(emitter) {
return setupGlobalListeners(emitter)
}
/** GoEasy 连上后:绑定监听,未手动下线则自动上线 */
export function afterGoeasyConnected(emitter, phone) {
if (window.__kefuUnreadCleanup) {
window.__kefuUnreadCleanup()
}
window.__kefuUnreadCleanup = bindGlobalUnreadListeners(emitter)
refreshKefuUnread(emitter)
if (window.__kefuUserWantsOffline) {
emitCsOnline(emitter, false)
return
}
ensureCsOnline({
phone,
emitter,
force: false,
onSuccess: () => refreshKefuUnread(emitter),
onFailed: () => refreshKefuUnread(emitter)
})
}
export function resumeKefuSession(emitter, phone) {
if (!isGoeasyConnected()) {
emitter?.emit('kefu-goeasy-ready', false)
return
}
emitter?.emit('kefu-goeasy-ready', true)
afterGoeasyConnected(emitter, phone)
}
/** 重连成功后恢复(不切页下线) */
export function onGoeasyReconnected(emitter, phone) {
emitter?.emit('kefu-goeasy-ready', true)
if (window.__kefuUserWantsOffline) {
emitCsOnline(emitter, false)
refreshKefuUnread(emitter)
return
}
ensureCsOnline({
phone,
emitter,
force: false,
onSuccess: () => refreshKefuUnread(emitter),
onFailed: () => refreshKefuUnread(emitter)
})
}

View File

@@ -75,10 +75,27 @@ const form = reactive({
ip: ''
})
function isStoredTokenUsable(token) {
if (!token || typeof token !== 'string') return false
const parts = token.split('.')
if (parts.length !== 3) return false
try {
const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')))
return Boolean(payload.yonghuid || payload.user_id)
} catch {
return false
}
}
onMounted(() => {
const token = localStorage.getItem('token')
if (token) {
if (isStoredTokenUsable(token)) {
router.push('/')
return
}
if (token) {
localStorage.removeItem('token')
localStorage.removeItem('username')
}
})

View File

@@ -1,15 +1,11 @@
<template>
<div class="agent-container">
<!-- 客服连接组件无界面负责建连 -->
<KefuConnect @connected="onConnected" />
<!-- 顶部操作栏 -->
<div class="agent-header">
<div class="header-left">
<el-tag :type="isOnline ? 'success' : 'info'" size="large">
{{ isOnline ? '在线' : '离线' }}
</el-tag>
<el-tag :type="statusTagType" size="large">{{ statusText }}</el-tag>
<el-button
v-if="goeasyReady"
:type="isOnline ? 'danger' : 'primary'"
size="small"
@click="toggleOnline"
@@ -17,6 +13,7 @@
>
{{ isOnline ? '下线' : '上线' }}
</el-button>
<span class="online-tip" v-if="goeasyReady && isOnline">切换页面不会下线</span>
<span class="unread-tip" v-if="unreadTotal">
<i class="el-icon-chat-dot-round" style="color: red; margin-right: 4px;"></i>
{{ unreadTotal }}条未读
@@ -123,6 +120,7 @@
:team-id="teamId"
:customer-id="currentCustomerId"
:customer-data="currentCustomerData"
:agent-online="isOnline"
@close="currentCustomerId = ''"
/>
</div>
@@ -132,8 +130,18 @@
import { ref, computed, onMounted, onUnmounted, getCurrentInstance } from 'vue'
import { ElMessage } from 'element-plus'
import ChatDialog from '@/components/ChatDialog.vue'
import KefuConnect from '@/components/KefuConnect.vue'
import GoEasy from 'goeasy'
import {
KEFU_TEAM_ID,
getCsteam,
refreshKefuUnread,
calcUnreadFromConversations,
ensureCustomerAccepted,
goOnlineManually,
goOfflineManually,
isGoeasyConnected,
checkCsOnlineStatus
} from '@/utils/kefuChat'
const { proxy } = getCurrentInstance()
@@ -145,13 +153,14 @@ const availableRoles = [
{ prefix: 'Zz', name: '组长' }
]
const isOnline = ref(false)
const goeasyReady = ref(isGoeasyConnected())
const isOnline = ref(!!window.__kefuCsOnline)
const switching = ref(false)
const tabActive = ref('pending')
const pendingList = ref([])
const activeList = ref([])
const unreadTotal = ref(0)
const teamId = 'support_team'
const teamId = KEFU_TEAM_ID
const searchRole = ref('')
const searchUid = ref('')
@@ -164,9 +173,18 @@ const defaultAvatar = 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726
const activeRole = ref('all')
function getCsteam() {
if (!window.goeasy) return null
return window.goeasy.im.csteam(teamId)
const statusText = computed(() => {
if (!goeasyReady.value) return '消息连接中…'
return isOnline.value ? '在线接待中' : '已离线'
})
const statusTagType = computed(() => {
if (!goeasyReady.value) return 'info'
return isOnline.value ? 'success' : 'warning'
})
function getCsteamLocal() {
return getCsteam()
}
// 过滤
@@ -199,8 +217,8 @@ function bindConversationListeners() {
conversationsUpdatedHandler = (data) => {
const all = data.conversations || []
activeList.value = filterConversations(all.filter(c => c.type === 'cs' && !c.ended))
unreadTotal.value = data.unreadTotal || 0
proxy.$emitter.emit('agent-unread', unreadTotal.value)
unreadTotal.value = data.unreadTotal || calcUnreadFromConversations(activeList.value)
refreshKefuUnread(proxy.$emitter)
}
window.goeasy.im.on(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, conversationsUpdatedHandler)
@@ -218,63 +236,52 @@ const onConnected = () => {
fetchActiveConversations()
}
// 上线
const csteamOnline = () => {
if (!window.goeasy) {
console.warn('[Agent] 未建立连接,无法上线')
const toggleOnline = () => {
if (switching.value || !goeasyReady.value) {
ElMessage.warning('消息服务尚未连接,请稍候…')
return
}
const csteam = getCsteam()
if (!csteam) return
csteam.online({
teamData: { name: '阿龙电竞客服中心', avatar: '' },
agentData: {
name: localStorage.getItem('username') || 'KF',
avatar: ''
},
onSuccess: () => {
isOnline.value = true
console.log('[Agent] 上线成功')
ElMessage.success('已上线,开始接收会话')
fetchPendingConversations()
fetchActiveConversations()
},
onFailed: (error) => {
console.error('[Agent] 上线失败:', error)
ElMessage.error('上线失败:' + error.content)
}
})
}
// 下线
const csteamOffline = () => {
const csteam = getCsteam()
if (!csteam) return
csteam.offline({
onSuccess: () => {
isOnline.value = false
console.log('[Agent] 下线成功')
ElMessage.success('已下线')
pendingList.value = []
},
onFailed: (error) => {
ElMessage.error('下线失败:' + error.content)
}
})
}
const toggleOnline = () => {
if (switching.value) return
switching.value = true
if (isOnline.value) {
csteamOffline()
} else {
csteamOnline()
const phone = localStorage.getItem('username')
const stopSpin = () => { switching.value = false }
const safetyTimer = setTimeout(stopSpin, 15000)
const wrapDone = (fn) => (...args) => {
clearTimeout(safetyTimer)
stopSpin()
fn?.(...args)
}
if (isOnline.value) {
goOfflineManually({
emitter: proxy.$emitter,
onSuccess: wrapDone(() => {
isOnline.value = false
ElMessage.success('已下线,不再接收新会话')
refreshKefuUnread(proxy.$emitter)
}),
onFailed: wrapDone((error) => {
ElMessage.error('下线失败:' + (error?.content || '未知错误'))
})
})
} else {
goOnlineManually({
phone,
emitter: proxy.$emitter,
onSuccess: wrapDone(() => {
isOnline.value = true
ElMessage.success('已上线,开始接收会话')
fetchPendingConversations()
fetchActiveConversations()
refreshKefuUnread(proxy.$emitter)
}),
onFailed: wrapDone((error) => {
ElMessage.error('上线失败:' + (error?.content || '未知错误'))
})
})
}
setTimeout(() => { switching.value = false }, 1000)
}
// 拉取待接入
const fetchPendingConversations = () => {
if (!window.goeasy) return
window.goeasy.im.pendingConversations({
@@ -295,27 +302,43 @@ const fetchActiveConversations = () => {
})
}
const acceptConversation = (conv) => {
const acceptAndOpenChat = async (conv, openDialog = true) => {
if (activeList.value.length >= 20) {
ElMessage.warning('最多同时接入20个会话')
return
}
const csteam = getCsteam()
csteam.accept({
customer: { id: conv.id, data: conv.data || { name: conv.id, avatar: defaultAvatar } },
onSuccess: () => {
ElMessage.success('已接入')
fetchPendingConversations()
fetchActiveConversations()
},
onFailed: (error) => {
ElMessage.error('接入失败:' + error.content)
if (!goeasyReady.value) {
ElMessage.warning('消息服务连接中,请稍候…')
return
}
if (!isOnline.value) {
ElMessage.warning('请先上线后再接入会话')
return
}
const customerId = conv.id || conv.userId
const customerData = conv.data || { name: customerId, avatar: defaultAvatar }
try {
await ensureCustomerAccepted(customerId, customerData)
ElMessage.success('已接入')
fetchPendingConversations()
fetchActiveConversations()
if (openDialog) {
currentCustomerId.value = customerId
currentCustomerData.value = customerData
dialogVisible.value = true
tabActive.value = 'active'
}
})
} catch (error) {
ElMessage.error('接入失败:' + (error?.content || '未知错误'))
}
}
const acceptConversation = (conv) => {
acceptAndOpenChat(conv, true)
}
const endConversation = (conv) => {
const csteam = getCsteam()
const csteam = getCsteamLocal()
csteam.end({
id: conv.id,
onSuccess: () => {
@@ -328,34 +351,109 @@ const endConversation = (conv) => {
})
}
const openChat = (conv) => {
currentCustomerId.value = conv.id || conv.userId
currentCustomerData.value = conv.data || { name: currentCustomerId.value, avatar: defaultAvatar }
dialogVisible.value = true
const openChat = async (conv) => {
const customerId = conv.id || conv.userId
const customerData = conv.data || { name: customerId, avatar: defaultAvatar }
if (!goeasyReady.value) {
ElMessage.warning('消息服务连接中,请稍候…')
return
}
if (!isOnline.value) {
ElMessage.warning('请先上线后再接入会话')
return
}
try {
await ensureCustomerAccepted(customerId, customerData)
currentCustomerId.value = customerId
currentCustomerData.value = customerData
dialogVisible.value = true
} catch (error) {
ElMessage.error('接入会话失败:' + (error?.content || '未知错误'))
}
}
const startNewChat = () => {
if (!searchRole.value || !searchUid.value) return
const fullId = searchRole.value + searchUid.value
const exists = activeList.value.find(c => c.id === fullId)
if (exists) {
openChat(exists)
if (!goeasyReady.value) {
ElMessage.warning('消息服务连接中,请稍候…')
return
}
currentCustomerId.value = fullId
currentCustomerData.value = { name: fullId, avatar: defaultAvatar }
dialogVisible.value = true
if (!isOnline.value) {
ElMessage.warning('请先上线后再接入会话')
return
}
const uid = String(searchUid.value).trim()
if (!/^\d+$/.test(uid)) {
ElMessage.warning('UID 请输入纯数字')
return
}
const fullId = searchRole.value + uid
const active = activeList.value.find(c => (c.id === fullId || c.userId === fullId))
if (active) {
openChat(active)
return
}
const pending = pendingList.value.find(c => c.id === fullId)
if (pending) {
acceptAndOpenChat(pending, true)
return
}
acceptAndOpenChat({
id: fullId,
data: { name: fullId, avatar: defaultAvatar }
}, true)
}
onMounted(() => {
console.log('[Agent] 页面挂载')
const onAgentUnread = (count) => {
unreadTotal.value = count
}
const onGoeasyReady = (ready) => {
goeasyReady.value = !!ready
}
const onCsOnlineChange = (online) => {
isOnline.value = !!online
}
const onGoEasyConnected = () => {
onConnected()
goeasyReady.value = isGoeasyConnected()
onCsOnlineChange(window.__kefuCsOnline)
}
onMounted(async () => {
goeasyReady.value = isGoeasyConnected()
if (goeasyReady.value) {
const online = await checkCsOnlineStatus()
isOnline.value = online
proxy.$emitter.emit('kefu-cs-online', online)
} else {
isOnline.value = !!window.__kefuCsOnline
}
proxy.$emitter.on('kefu-goeasy-ready', onGoeasyReady)
proxy.$emitter.on('kefu-cs-online', onCsOnlineChange)
proxy.$emitter.on('agent-unread', onAgentUnread)
if (window.goeasy && isGoeasyConnected()) {
onConnected()
} else {
proxy.$emitter.on('goeasy-connected', onGoEasyConnected)
}
})
onUnmounted(() => {
if (conversationsUpdatedHandler) {
proxy.$emitter.off('goeasy-connected', onGoEasyConnected)
proxy.$emitter.off('kefu-goeasy-ready', onGoeasyReady)
proxy.$emitter.off('kefu-cs-online', onCsOnlineChange)
proxy.$emitter.off('agent-unread', onAgentUnread)
if (window.goeasy && conversationsUpdatedHandler) {
window.goeasy.im.off(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, conversationsUpdatedHandler)
}
if (pendingUpdatedHandler) {
if (window.goeasy && pendingUpdatedHandler) {
window.goeasy.im.off(GoEasy.IM_EVENT.PENDING_CONVERSATIONS_UPDATED, pendingUpdatedHandler)
}
})
@@ -439,4 +537,8 @@ onUnmounted(() => {
color: #888;
margin-top: 8px;
}
.online-tip {
font-size: 12px;
color: #8bc34a;
}
</style>

View File

@@ -145,6 +145,18 @@
<el-descriptions-item label="打手分成" v-if="orderData.dashou_fencheng">
¥{{ formatPrice(orderData.dashou_fencheng) }}
</el-descriptions-item>
<el-descriptions-item
label="管事打手接单分红"
v-if="orderData.fadanpingtai === 2"
>
¥{{ formatPrice(orderData.guanshi_fencheng || 0) }}
</el-descriptions-item>
<el-descriptions-item
label="管事商家派单分红"
v-if="orderData.fadanpingtai === 2"
>
¥{{ formatPrice(orderData.guanshi_shangjia_fencheng || 0) }}
</el-descriptions-item>
<el-descriptions-item label="订单类型">
{{ orderData.leixing || '--' }}
</el-descriptions-item>
@@ -527,6 +539,8 @@ const orderData = ref({
is_cross: 0,
jine: 0,
dashou_fencheng: 0,
guanshi_fencheng: 0,
guanshi_shangjia_fencheng: 0,
jiedan_dashou_id: '',
dashou_liuyan: '',
zhiding_id: '',

View File

@@ -39,6 +39,46 @@
</el-form>
</el-tab-pane>
<el-tab-pane label="商家派单限额" name="shangjia_paifa">
<el-alert
type="info"
:closable="false"
show-icon
class="shop-hint"
title="控制商家派单接口 /dingdan/sjpaifa 的单笔金额范围。修改后立即生效,小程序端以后台校验为准。"
/>
<el-form label-width="200px" class="shop-form" v-loading="sysLoading">
<el-form-item label="单笔最低金额(元)">
<el-input-number
v-model="sysForm.shangjia_paifa_jiage_min"
:min="0.01"
:max="999999"
:precision="2"
:step="1"
/>
</el-form-item>
<el-form-item label="单笔最高金额(元)">
<el-input-number
v-model="sysForm.shangjia_paifa_jiage_max"
:min="0.01"
:max="999999"
:precision="2"
:step="100"
/>
</el-form-item>
<el-form-item>
<el-text type="info">
当前生效范围{{ sysForm.shangjia_paifa_jiage_min }} ~ {{ sysForm.shangjia_paifa_jiage_max }} /
若最低大于最高保存时系统会自动对调
</el-text>
</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="sys">
<el-form :model="sysForm" label-width="160px" class="sys-form" v-loading="sysLoading">
<el-form-item label="小程序 AppID">
@@ -160,6 +200,8 @@ const sysForm = reactive({
weixin_notify_url: '',
notify_base_url: '',
shangpin_shenhe_mode: 2,
shangjia_paifa_jiage_min: 10,
shangjia_paifa_jiage_max: 10000,
})
const assetGroup = ref('paihangbang')
@@ -196,6 +238,8 @@ const loadSysConfig = async () => {
weixin_notify_url: res.data.weixin_notify_url || '',
notify_base_url: res.data.notify_base_url || '',
shangpin_shenhe_mode: res.data.shangpin_shenhe_mode ?? 2,
shangjia_paifa_jiage_min: Number(res.data.shangjia_paifa_jiage_min ?? 10),
shangjia_paifa_jiage_max: Number(res.data.shangjia_paifa_jiage_max ?? 10000),
})
} else {
ElMessage.error(res.msg || '加载失败')
@@ -219,7 +263,7 @@ const saveSysConfig = async () => {
config: payload,
})
if (res.code === 0) {
ElMessage.success('微信配置已保存')
ElMessage.success('配置已保存')
loadSysConfig()
} else {
ElMessage.error(res.msg || '保存失败')

View File

@@ -0,0 +1,399 @@
<template>
<div class="script-config-manager">
<div v-if="!hasPermission" class="no-permission">
<div class="no-permission-icon">🔒</div>
<div class="no-permission-text">您无权访问此页面</div>
<div class="no-permission-desc">请联系管理员开通权限</div>
</div>
<div v-else>
<div class="header-bar">
<h2>自动弹窗及话术配置</h2>
</div>
<el-tabs v-model="activeTab" class="scene-tabs">
<el-tab-pane label="聊天发图确认" name="chat_image_confirm" />
<el-tab-pane label="客服欢迎语" name="cs_welcome" />
<el-tab-pane label="商家派单确认" name="merchant_dispatch_confirm" />
<el-tab-pane label="客服自动回复" name="cs_auto_reply" />
</el-tabs>
<!-- 单条话术确认弹窗 / 欢迎语 -->
<el-card v-if="activeTab !== 'cs_auto_reply'" class="single-card" shadow="hover">
<template #header>
<div class="card-header">
<span>{{ currentSceneLabel }}</span>
<el-tag :type="singleForm.is_active ? 'success' : 'info'" size="small">
{{ singleForm.is_active ? '启用' : '禁用' }}
</el-tag>
</div>
</template>
<el-form :model="singleForm" label-width="120px" class="single-form">
<el-form-item v-if="isConfirmScene" label="弹窗标题">
<el-input v-model="singleForm.title" placeholder="弹窗标题" />
</el-form-item>
<el-form-item label="正文内容">
<el-input v-model="singleForm.content" type="textarea" :rows="6" placeholder="话术正文" />
</el-form-item>
<el-form-item v-if="isConfirmScene" label="确认按钮">
<el-input v-model="singleForm.confirm_text" placeholder="如:我已知晓,继续发送" />
</el-form-item>
<el-form-item v-if="isConfirmScene" label="取消按钮">
<el-input v-model="singleForm.cancel_text" placeholder="如:取消" />
</el-form-item>
<el-form-item label="是否启用">
<el-switch v-model="singleForm.is_active" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="singleSubmitting" @click="saveSingle">
保存{{ singleForm.id ? '修改' : '创建' }}
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 自动回复规则列表 -->
<div v-else class="auto-reply-panel">
<div class="toolbar">
<el-button type="primary" @click="openAutoReplyDialog()">+ 添加自动回复规则</el-button>
</div>
<el-table :data="autoReplyList" border stripe>
<el-table-column prop="priority" label="优先级" width="90" />
<el-table-column label="触发关键词" min-width="200">
<template #default="{ row }">
<el-tag v-for="kw in row.keywords" :key="kw" size="small" class="kw-tag">{{ kw }}</el-tag>
<span v-if="!row.keywords || !row.keywords.length"></span>
</template>
</el-table-column>
<el-table-column prop="match_mode" label="匹配模式" width="110">
<template #default="{ row }">
{{ row.match_mode === 'exact' ? '精确' : '包含' }}
</template>
</el-table-column>
<el-table-column prop="content" label="回复内容" min-width="260" show-overflow-tooltip />
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag :type="row.is_active ? 'success' : 'info'" size="small">
{{ row.is_active ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="openAutoReplyDialog(row)">编辑</el-button>
<el-button type="danger" link @click="deleteAutoReply(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 自动回复编辑弹窗 -->
<el-dialog
v-model="autoReplyDialogVisible"
:title="autoReplyForm.id ? '编辑自动回复' : '添加自动回复'"
width="560px"
destroy-on-close
>
<el-form :model="autoReplyForm" label-width="110px">
<el-form-item label="触发关键词" required>
<el-select
v-model="autoReplyForm.keywords"
multiple
filterable
allow-create
default-first-option
placeholder="输入后回车添加关键词"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="匹配模式">
<el-radio-group v-model="autoReplyForm.match_mode">
<el-radio label="contains">包含匹配</el-radio>
<el-radio label="exact">精确匹配</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="优先级">
<el-input-number v-model="autoReplyForm.priority" :min="0" :max="999" />
</el-form-item>
<el-form-item label="回复内容" required>
<el-input v-model="autoReplyForm.content" type="textarea" :rows="5" />
</el-form-item>
<el-form-item label="是否启用">
<el-switch v-model="autoReplyForm.is_active" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="autoReplyDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="autoReplySubmitting" @click="submitAutoReply">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, watch, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '@/utils/request'
const username = ref(localStorage.getItem('username') || '')
const hasPermission = ref(true)
const activeTab = ref('chat_image_confirm')
const allItems = ref([])
const singleSubmitting = ref(false)
const SCENE_LABELS = {
chat_image_confirm: '聊天发图确认',
cs_welcome: '客服欢迎语',
merchant_dispatch_confirm: '商家派单确认',
cs_auto_reply: '客服自动回复',
}
const singleForm = reactive({
id: null,
scene_key: 'chat_image_confirm',
item_type: 'confirm_modal',
title: '',
content: '',
confirm_text: '我知道了',
cancel_text: '取消',
is_active: true,
})
const currentSceneLabel = computed(() => SCENE_LABELS[activeTab.value] || activeTab.value)
const isConfirmScene = computed(() => activeTab.value !== 'cs_welcome')
const autoReplyList = computed(() =>
allItems.value.filter((i) => i.scene_key === 'cs_auto_reply' && i.item_type === 'auto_reply')
)
const autoReplyDialogVisible = ref(false)
const autoReplySubmitting = ref(false)
const autoReplyForm = reactive({
id: null,
keywords: [],
match_mode: 'contains',
priority: 0,
content: '',
is_active: true,
})
const fetchData = async () => {
try {
const res = await request.post('/houtai/hthqhs', { username: username.value })
if (res.code === 0) {
allItems.value = res.data.items || []
loadSingleForm()
} else if (res.code === 403) {
hasPermission.value = false
} else {
ElMessage.error(res.msg || '获取数据失败')
}
} catch {
ElMessage.error('网络错误')
}
}
const getSingleItemType = (sceneKey) => {
return sceneKey === 'cs_welcome' ? 'text_display' : 'confirm_modal'
}
const loadSingleForm = () => {
const sceneKey = activeTab.value
if (sceneKey === 'cs_auto_reply') return
const item = allItems.value.find(
(i) => i.scene_key === sceneKey && i.item_type === getSingleItemType(sceneKey)
)
if (item) {
Object.assign(singleForm, {
id: item.id,
scene_key: item.scene_key,
item_type: item.item_type,
title: item.title || '',
content: item.content || '',
confirm_text: item.confirm_text || '我知道了',
cancel_text: item.cancel_text || '取消',
is_active: item.is_active,
})
} else {
Object.assign(singleForm, {
id: null,
scene_key: sceneKey,
item_type: getSingleItemType(sceneKey),
title: '',
content: '',
confirm_text: '我知道了',
cancel_text: '取消',
is_active: true,
})
}
}
watch(activeTab, () => {
loadSingleForm()
})
const saveSingle = async () => {
if (!singleForm.content || !singleForm.content.trim()) {
ElMessage.warning('请填写正文内容')
return
}
singleSubmitting.value = true
try {
const action = singleForm.id ? 'update' : 'create'
const payload = {
username: username.value,
action,
scene_key: singleForm.scene_key,
item_type: singleForm.item_type,
title: singleForm.title,
content: singleForm.content.trim(),
confirm_text: singleForm.confirm_text,
cancel_text: singleForm.cancel_text,
is_active: singleForm.is_active,
}
if (singleForm.id) payload.id = singleForm.id
const res = await request.post('/houtai/htxghs', payload)
if (res.code === 0) {
ElMessage.success('保存成功')
await fetchData()
} else {
ElMessage.error(res.msg || '保存失败')
}
} catch {
ElMessage.error('保存失败')
} finally {
singleSubmitting.value = false
}
}
const openAutoReplyDialog = (row) => {
if (row) {
Object.assign(autoReplyForm, {
id: row.id,
keywords: [...(row.keywords || [])],
match_mode: row.match_mode || 'contains',
priority: row.priority || 0,
content: row.content || '',
is_active: row.is_active,
})
} else {
Object.assign(autoReplyForm, {
id: null,
keywords: [],
match_mode: 'contains',
priority: 0,
content: '',
is_active: true,
})
}
autoReplyDialogVisible.value = true
}
const submitAutoReply = async () => {
if (!autoReplyForm.keywords.length) {
ElMessage.warning('请至少添加一个触发关键词')
return
}
if (!autoReplyForm.content || !autoReplyForm.content.trim()) {
ElMessage.warning('请填写回复内容')
return
}
autoReplySubmitting.value = true
try {
const action = autoReplyForm.id ? 'update' : 'create'
const payload = {
username: username.value,
action,
scene_key: 'cs_auto_reply',
item_type: 'auto_reply',
title: '',
content: autoReplyForm.content.trim(),
keywords: autoReplyForm.keywords,
match_mode: autoReplyForm.match_mode,
priority: autoReplyForm.priority,
is_active: autoReplyForm.is_active,
}
if (autoReplyForm.id) payload.id = autoReplyForm.id
const res = await request.post('/houtai/htxghs', payload)
if (res.code === 0) {
ElMessage.success('保存成功')
autoReplyDialogVisible.value = false
await fetchData()
} else {
ElMessage.error(res.msg || '保存失败')
}
} catch {
ElMessage.error('保存失败')
} finally {
autoReplySubmitting.value = false
}
}
const deleteAutoReply = async (row) => {
try {
await ElMessageBox.confirm('确定删除该自动回复规则?', '删除确认', { type: 'warning' })
const res = await request.post('/houtai/htxghs', {
username: username.value,
action: 'delete',
id: row.id,
})
if (res.code === 0) {
ElMessage.success('删除成功')
await fetchData()
} else {
ElMessage.error(res.msg || '删除失败')
}
} catch (error) {
if (error !== 'cancel') ElMessage.error('删除失败')
}
}
onMounted(() => {
fetchData()
})
</script>
<style scoped>
.script-config-manager {
padding: 16px;
}
.header-bar {
margin-bottom: 16px;
}
.header-bar h2 {
margin: 0;
font-size: 20px;
}
.no-permission {
text-align: center;
padding: 80px 20px;
}
.no-permission-icon {
font-size: 48px;
margin-bottom: 16px;
}
.single-card {
margin-top: 12px;
}
.card-header {
display: flex;
align-items: center;
gap: 12px;
}
.auto-reply-panel {
margin-top: 12px;
}
.toolbar {
margin-bottom: 12px;
}
.kw-tag {
margin-right: 6px;
margin-bottom: 4px;
}
</style>

View File

@@ -112,6 +112,18 @@
<el-descriptions-item label="打手分成" v-if="orderData.dashou_fencheng">
¥{{ formatPrice(orderData.dashou_fencheng) }}
</el-descriptions-item>
<el-descriptions-item
label="管事打手接单分红"
v-if="orderData.fadanpingtai === 2"
>
¥{{ formatPrice(orderData.guanshi_fencheng || 0) }}
</el-descriptions-item>
<el-descriptions-item
label="管事商家派单分红"
v-if="orderData.fadanpingtai === 2"
>
¥{{ formatPrice(orderData.guanshi_shangjia_fencheng || 0) }}
</el-descriptions-item>
<el-descriptions-item label="订单类型">
{{ orderData.leixing || '--' }}
</el-descriptions-item>
@@ -373,6 +385,8 @@ const orderData = ref({
fadanpingtai: 0,
jiage: 0,
dashou_fencheng: 0,
guanshi_fencheng: 0,
guanshi_shangjia_fencheng: 0,
jiedan_dashou_id: '',
dashou_liuyan: '',
zhiding_id: '',

View File

@@ -192,17 +192,18 @@ const extraWithdrawItems = [
{ type: 6, name: '商家余额提现' }
]
// 订单分红费率Lilubiao fadanpingtai1平台订单 / 3商家订单 / 12押金管事 / 13管事订单
// 订单分红费率Lilubiao fadanpingtai1平台订单 / 3商家订单 / 12押金管事 / 13管事打手接单 / 15商家派单管事
const orderCommissionItems = [
{ type: 1, name: '平台订单打手分红' },
{ type: 3, name: '商家订单打手分红' },
{ type: 12, name: '押金管事分红' },
{ type: 13, name: '管事单分红' }
{ type: 13, name: '管事打手接单分红' },
{ type: 15, name: '商家派单管事分红' }
]
const DEFAULT_RATES = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 }
const DEFAULT_TOTAL_LIMITS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 }
const DEFAULT_ORDER_RATES = { 1: 0, 3: 0, 12: 0, 13: 0 }
const DEFAULT_ORDER_RATES = { 1: 0, 3: 0, 12: 0, 13: 0, 15: 0 }
// 🆕 提现模式
const withdrawMode = ref(2) // 默认手动提现
@@ -252,7 +253,8 @@ const mergeOrderRates = (source = {}) => ({
1: source[1] ?? source['1'] ?? DEFAULT_ORDER_RATES[1],
3: source[3] ?? source['3'] ?? DEFAULT_ORDER_RATES[3],
12: source[12] ?? source['12'] ?? DEFAULT_ORDER_RATES[12],
13: source[13] ?? source['13'] ?? DEFAULT_ORDER_RATES[13]
13: source[13] ?? source['13'] ?? DEFAULT_ORDER_RATES[13],
15: source[15] ?? source['15'] ?? DEFAULT_ORDER_RATES[15],
})
const mergeTotalLimits = (source = {}) => ({
@@ -495,7 +497,7 @@ const hasSettingsChanged = () => {
for (const key of [1, 2, 3, 4, 5, 6]) {
if (editData.rates[key] !== originalData.rates[key]) return true
}
for (const key of [1, 3, 12, 13]) {
for (const key of orderCommissionItems.map((item) => item.type)) {
if (editData.orderRates[key] !== originalData.orderRates[key]) return true
}
for (const key of [1, 2, 3]) {