chore: 客服聊天配置与排行榜奖励开发前备份
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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: '客服聊天配置' }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
406
src/views/chat/ScriptConfig.vue
Normal file
406
src/views/chat/ScriptConfig.vue
Normal 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>
|
||||
Reference in New Issue
Block a user