chore: 客服聊天配置与排行榜奖励开发前备份

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-01 17:30:23 +08:00
parent 12506c5d32
commit 1c64f84d68
3 changed files with 431 additions and 12 deletions

View File

@@ -296,7 +296,12 @@ const routes = [
{
path: 'chat/agent',
component: () => import('@/views/chat/Agent.vue'),
meta: { title: '会话管理' }
meta: { title: '会话接入' }
},
{
path: 'chat/script-config',
component: () => import('@/views/chat/ScriptConfig.vue'),
meta: { title: '客服聊天配置' }
}
]
}

View File

@@ -158,8 +158,9 @@
<el-menu-item v-if="canShow('shop.products')" index="/shop/products">店铺商品管理</el-menu-item>
</el-sub-menu>
<!-- 会话管理客服专用 -->
<el-menu-item v-if="canShow('chat.agent')" index="/chat/agent" :class="{ 'menu-disabled': !hasAnyChatPerm }">
<!-- 会话管理 -->
<el-sub-menu v-if="canShow('chat')" index="/chat">
<template #title>
<el-icon><Comment /></el-icon>
<span>会话管理</span>
<el-badge
@@ -168,7 +169,14 @@
class="agent-badge"
style="margin-left: 8px;"
/>
</template>
<el-menu-item v-if="canShow('chat.agent')" index="/chat/agent" :class="{ 'menu-disabled': !hasAnyChatPerm }">
会话接入
</el-menu-item>
<el-menu-item v-if="canShow('chat.script-config')" index="/chat/script-config">
客服聊天配置
</el-menu-item>
</el-sub-menu>
</el-menu>
<div class="collapse-btn" @click="toggleCollapse">

View File

@@ -0,0 +1,406 @@
<template>
<div class="script-config-manager">
<el-alert
v-if="isAllClubScope"
type="warning"
:closable="false"
show-icon
title="客服聊天配置需在具体俱乐部视图下编辑,请切换顶栏俱乐部。"
class="scope-alert"
/>
<ClubScopeHint v-else />
<div v-if="!hasPermission" class="no-permission">
<div class="no-permission-icon">🔒</div>
<div class="no-permission-text">您无权访问此页面</div>
<div class="no-permission-desc">请联系管理员开通 kfhs666 8080a 权限</div>
</div>
<div v-else>
<div class="header-bar">
<h2>客服聊天配置</h2>
<p class="header-desc">欢迎语发图确认弹窗关键词自动回复按当前俱乐部生效</p>
</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" :disabled="isAllClubScope" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="singleSubmitting" :disabled="isAllClubScope" @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" :disabled="isAllClubScope" @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="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 :disabled="isAllClubScope" @click="openAutoReplyDialog(row)">编辑</el-button>
<el-button type="danger" link :disabled="isAllClubScope" @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-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'
import ClubScopeHint from '@/components/ClubScopeHint.vue'
import { getAdminClubScope } from '@/utils/club-context'
const username = ref(localStorage.getItem('username') || '')
const hasPermission = ref(true)
const activeTab = ref('cs_welcome')
const allItems = ref([])
const singleSubmitting = ref(false)
const isAllClubScope = computed(() => getAdminClubScope() === 'all')
const SCENE_LABELS = {
chat_image_confirm: '聊天发图确认',
cs_welcome: '客服欢迎语',
merchant_dispatch_confirm: '商家派单确认',
cs_auto_reply: '客服自动回复',
}
const singleForm = reactive({
id: null,
scene_key: 'cs_welcome',
item_type: 'text_display',
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: [],
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 (isAllClubScope.value) return
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 || [])],
priority: row.priority || 0,
content: row.content || '',
is_active: row.is_active,
})
} else {
Object.assign(autoReplyForm, {
id: null,
keywords: [],
priority: 0,
content: '',
is_active: true,
})
}
autoReplyDialogVisible.value = true
}
const submitAutoReply = async () => {
if (isAllClubScope.value) return
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,
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) => {
if (isAllClubScope.value) return
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;
}
.scope-alert {
margin-bottom: 12px;
}
.header-bar {
margin-bottom: 16px;
}
.header-bar h2 {
margin: 0 0 8px;
font-size: 20px;
}
.header-desc {
margin: 0;
color: #888;
font-size: 13px;
}
.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>