客服可以给会话设置个人备注, 设置 快捷回复

建群自动话术
This commit is contained in:
yml2213
2026-05-27 06:43:24 +08:00
parent a002987784
commit 138f0514a9
15 changed files with 1462 additions and 34 deletions
@@ -0,0 +1,253 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
fetchQuickReplies,
createQuickReply,
updateQuickReply,
deleteQuickReply,
type QuickReply,
} from '@/api/chats'
const props = defineProps<{
modelValue: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
success: []
}>()
const visible = ref(false)
const loading = ref(false)
const replies = ref<QuickReply[]>([])
const editingId = ref<number | null>(null)
const form = ref({
title: '',
content: '',
sort_order: 0,
})
watch(() => props.modelValue, (val) => {
visible.value = val
if (val) {
loadReplies()
}
})
watch(visible, (val) => {
emit('update:modelValue', val)
})
async function loadReplies() {
loading.value = true
try {
replies.value = await fetchQuickReplies()
} catch {
ElMessage.error('加载快捷回复失败')
} finally {
loading.value = false
}
}
function resetForm() {
form.value = { title: '', content: '', sort_order: 0 }
editingId.value = null
}
function startEdit(reply: QuickReply) {
editingId.value = reply.id
form.value = {
title: reply.title,
content: reply.content,
sort_order: reply.sort_order,
}
}
async function handleSubmit() {
if (!form.value.title || !form.value.content) {
ElMessage.warning('标题和内容不能为空')
return
}
try {
if (editingId.value) {
await updateQuickReply(editingId.value, form.value)
ElMessage.success('更新成功')
} else {
await createQuickReply(form.value.title, form.value.content, form.value.sort_order)
ElMessage.success('创建成功')
}
resetForm()
await loadReplies()
emit('success')
} catch {
ElMessage.error('操作失败')
}
}
async function handleDelete(reply: QuickReply) {
if (reply.is_global) {
ElMessage.warning('不能删除全局快捷回复')
return
}
try {
await ElMessageBox.confirm('确定删除这条快捷回复?', '确认删除', {
type: 'warning',
})
await deleteQuickReply(reply.id)
ElMessage.success('删除成功')
await loadReplies()
emit('success')
} catch { /* ignore */ }
}
function handleCancel() {
resetForm()
}
</script>
<template>
<el-dialog v-model="visible" title="快捷回复管理" width="600px">
<div class="quick-reply-content">
<div class="reply-form">
<el-input
v-model="form.title"
placeholder="快捷回复标题"
maxlength="64"
style="margin-bottom: 8px"
/>
<el-input
v-model="form.content"
type="textarea"
:rows="3"
placeholder="回复内容"
maxlength="500"
show-word-limit
style="margin-bottom: 8px"
/>
<div class="form-actions">
<el-input-number
v-model="form.sort_order"
:min="0"
:max="999"
size="small"
placeholder="排序"
style="width: 120px"
/>
<div>
<el-button v-if="editingId" size="small" @click="handleCancel">取消</el-button>
<el-button size="small" type="primary" @click="handleSubmit">
{{ editingId ? '更新' : '添加' }}
</el-button>
</div>
</div>
</div>
<div class="reply-list" v-loading="loading">
<div
v-for="reply in replies"
:key="reply.id"
class="reply-item"
:class="{ global: reply.is_global }"
>
<div class="reply-info">
<div class="reply-header">
<span class="reply-title">{{ reply.title }}</span>
<el-tag v-if="reply.is_global" size="small" type="info">全局</el-tag>
<el-tag v-else size="small" type="success">个人</el-tag>
</div>
<div class="reply-content">{{ reply.content }}</div>
</div>
<div class="reply-actions">
<el-button link size="small" @click="startEdit(reply)">编辑</el-button>
<el-button
v-if="!reply.is_global"
link
size="small"
type="danger"
@click="handleDelete(reply)"
>
删除
</el-button>
</div>
</div>
<el-empty v-if="!loading && replies.length === 0" description="暂无快捷回复" />
</div>
</div>
</el-dialog>
</template>
<style scoped>
.quick-reply-content {
display: flex;
flex-direction: column;
gap: 16px;
max-height: 60vh;
}
.reply-form {
padding: 16px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #e5e7eb;
}
.form-actions {
display: flex;
align-items: center;
justify-content: space-between;
}
.reply-list {
overflow-y: auto;
max-height: 400px;
}
.reply-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
margin-bottom: 8px;
}
.reply-item.global {
background: #f0f9ff;
border-color: #bae6fd;
}
.reply-info {
flex: 1;
min-width: 0;
}
.reply-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.reply-title {
font-weight: 500;
color: #111827;
}
.reply-content {
color: #6b7280;
font-size: 13px;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.reply-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
margin-left: 12px;
}
</style>