| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475 |
- <template>
- <div class="app-page">
- <section class="glass-card section-card">
- <el-form :model="filters" inline class="filter-form">
- <el-form-item label="规则名称">
- <el-input v-model="filters.name" placeholder="搜索规则名称" clearable style="width:160px" @keyup.enter="loadData" />
- </el-form-item>
- <el-form-item label="触发类型">
- <el-select v-model="filters.triggerType" placeholder="全部" clearable style="width:130px">
- <el-option label="关键词" value="keyword" />
- <el-option label="意图识别" value="intent" />
- <el-option label="寒暄" value="greeting" />
- <el-option label="兜底回复" value="fallback" />
- </el-select>
- </el-form-item>
- <el-form-item label="状态">
- <el-select v-model="filters.status" placeholder="全部" clearable style="width:120px">
- <el-option label="已启用" value="enabled" />
- <el-option label="已禁用" value="disabled" />
- </el-select>
- </el-form-item>
- <el-form-item>
- <el-button type="primary" @click="loadData">查询</el-button>
- <el-button @click="resetFilters">重置</el-button>
- </el-form-item>
- </el-form>
- </section>
- <section class="glass-card section-card" style="padding:12px 24px">
- <div class="table-toolbar" style="margin-bottom:0">
- <div class="chip-list">
- <el-button type="primary" @click="openCreate">新建规则</el-button>
- <el-button type="success" @click="saveFlow">保存流程</el-button>
- </div>
- <el-button @click="loadData">刷新</el-button>
- </div>
- </section>
- <section class="glass-card section-card">
- <div class="rule-layout">
- <div class="rule-list">
- <div class="rule-list__header">
- <h3>规则列表</h3>
- <span class="rule-list__hint">拖拽调整优先级</span>
- </div>
- <div class="rule-items">
- <div
- v-for="(rule, index) in filteredRules"
- :key="rule.id"
- class="rule-item"
- :class="{ 'rule-item--active': currentRule?.id === rule.id, 'rule-item--disabled': rule.status === 'disabled' }"
- @click="selectRule(rule)"
- >
- <div class="rule-item__priority">
- <el-tag size="small" :type="index === 0 ? 'danger' : index < 3 ? 'warning' : 'info'">
- {{ index === 0 ? '最高' : `优先${index + 1}` }}
- </el-tag>
- </div>
- <div class="rule-item__info">
- <div class="rule-item__name">{{ rule.name }}</div>
- <div class="rule-item__meta">
- <el-tag size="small" :type="triggerTypeTag(rule.triggerType)">{{ triggerTypeLabel(rule.triggerType) }}</el-tag>
- <span class="rule-item__hit">命中 {{ rule.hitCount }} 次</span>
- </div>
- </div>
- <div class="rule-item__status">
- <el-switch
- :model-value="rule.status === 'enabled'"
- @change="toggleStatus(rule, $event)"
- @click.stop
- />
- </div>
- </div>
- </div>
- </div>
- <div class="rule-detail">
- <template v-if="currentRule">
- <div class="rule-detail__header">
- <h3>{{ isEdit ? '编辑规则' : '规则详情' }}</h3>
- <el-button v-if="!isEdit" type="primary" size="small" @click="isEdit = true">编辑</el-button>
- <el-button v-if="isEdit" size="small" @click="cancelEdit">取消</el-button>
- <el-button v-if="isEdit" type="success" size="small" @click="saveRule">保存</el-button>
- </div>
- <el-form :model="editForm" label-width="100px" class="rule-form">
- <el-form-item label="规则名称" required>
- <el-input v-model="editForm.name" :disabled="!isEdit" placeholder="请输入规则名称" />
- </el-form-item>
- <el-form-item label="触发类型" required>
- <el-select v-model="editForm.triggerType" :disabled="!isEdit" style="width:100%">
- <el-option label="关键词匹配" value="keyword" />
- <el-option label="意图识别" value="intent" />
- <el-option label="寒暄回复" value="greeting" />
- <el-option label="兜底回复" value="fallback" />
- </el-select>
- </el-form-item>
- <el-form-item label="匹配模式">
- <el-radio-group v-model="editForm.matchMode" :disabled="!isEdit">
- <el-radio label="contain">包含任意关键词</el-radio>
- <el-radio label="exact">完全匹配</el-radio>
- <el-radio label="regex">正则表达式</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="关键词" v-if="editForm.triggerType === 'keyword'" required>
- <el-select v-model="editForm.keywords" multiple filterable allow-create :disabled="!isEdit" placeholder="输入关键词后回车" style="width:100%">
- <el-option v-for="kw in editForm.keywords" :key="kw" :label="kw" :value="kw" />
- </el-select>
- </el-form-item>
- <el-form-item label="回复内容" required>
- <el-input v-model="editForm.responses[0]" type="textarea" :rows="4" :disabled="!isEdit" placeholder="请输入回复内容" />
- </el-form-item>
- <el-form-item label="多轮回复" v-if="editForm.responses.length > 1">
- <div class="multi-response">
- <div v-for="(resp, idx) in editForm.responses.slice(1)" :key="idx" class="multi-response__item">
- <span class="multi-response__label">追问{{ idx + 1 }}:</span>
- <el-input v-model="editForm.responses[idx + 1]" :disabled="!isEdit" />
- </div>
- </div>
- <el-button v-if="isEdit" size="small" @click="addResponse" style="margin-top:8px">添加追问</el-button>
- </el-form-item>
- <el-form-item label="统计信息">
- <div class="rule-stats">
- <div class="rule-stats__item">
- <span class="rule-stats__label">命中次数</span>
- <span class="rule-stats__value">{{ currentRule.hitCount }}</span>
- </div>
- <div class="rule-stats__item">
- <span class="rule-stats__label">准确率</span>
- <span class="rule-stats__value">{{ currentRule.accuracy }}%</span>
- </div>
- </div>
- </el-form-item>
- <el-form-item label="测试">
- <div class="test-section">
- <el-input v-model="testMessage" placeholder="输入测试消息" style="width:200px" />
- <el-button @click="testRule" :loading="testing">测试</el-button>
- </div>
- <div v-if="testResult" class="test-result">
- <span class="test-result__label">回复:</span>
- <span class="test-result__content">{{ testResult }}</span>
- </div>
- </el-form-item>
- </el-form>
- </template>
- <el-empty v-else description="请选择一条规则查看详情" />
- </div>
- </div>
- </section>
- <el-dialog v-model="dialogVisible" title="新建规则" width="600px">
- <el-form :model="form" label-width="100px">
- <el-form-item label="规则名称" required>
- <el-input v-model="form.name" placeholder="请输入规则名称" />
- </el-form-item>
- <el-form-item label="触发类型" required>
- <el-select v-model="form.triggerType" style="width:100%">
- <el-option label="关键词匹配" value="keyword" />
- <el-option label="意图识别" value="intent" />
- <el-option label="寒暄回复" value="greeting" />
- <el-option label="兜底回复" value="fallback" />
- </el-select>
- </el-form-item>
- <el-form-item label="匹配模式">
- <el-radio-group v-model="form.matchMode">
- <el-radio label="contain">包含任意关键词</el-radio>
- <el-radio label="exact">完全匹配</el-radio>
- <el-radio label="regex">正则表达式</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="关键词" v-if="form.triggerType === 'keyword'" required>
- <el-select v-model="form.keywords" multiple filterable allow-create placeholder="输入关键词后回车" style="width:100%">
- <el-option v-for="kw in form.keywords" :key="kw" :label="kw" :value="kw" />
- </el-select>
- </el-form-item>
- <el-form-item label="回复内容" required>
- <el-input v-model="form.responses[0]" type="textarea" :rows="3" placeholder="请输入回复内容" />
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="dialogVisible = false">取消</el-button>
- <el-button type="primary" @click="createRule">创建</el-button>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, computed, onMounted } from 'vue';
- import { ElMessage } from 'element-plus';
- import type { AutoReplyRule } from '@/types/page';
- const loading = ref(false);
- const dialogVisible = ref(false);
- const isEdit = ref(false);
- const currentRule = ref<AutoReplyRule | null>(null);
- const testMessage = ref('');
- const testResult = ref('');
- const testing = ref(false);
- const filters = ref({ name: '', triggerType: '', status: '' });
- const rules = ref<AutoReplyRule[]>([
- { id: 'R001', name: '物流查询', priority: 1, triggerType: 'keyword', keywords: ['物流', '快递', '发货', '到哪了', '什么时候到'], matchMode: 'contain', responses: ['您好,您的订单已在运输中,预计2-3天送达。'], status: 'enabled', hitCount: 1567, accuracy: 94, createdAt: '2024-01-01', updatedAt: '2024-01-15' },
- { id: 'R002', name: '退换货咨询', priority: 2, triggerType: 'keyword', keywords: ['退货', '换货', '退款', '售后'], matchMode: 'contain', responses: ['本店支持7天无理由退货(定制商品除外)。请登录账号申请退货退款。'], status: 'enabled', hitCount: 892, accuracy: 91, createdAt: '2024-01-02', updatedAt: '2024-01-14' },
- { id: 'R003', name: '支付问题', priority: 3, triggerType: 'keyword', keywords: ['支付', '付款', '银行卡', '支付宝', '微信'], matchMode: 'contain', responses: ['我们支持支付宝、微信支付、银行卡等多种支付方式。'], status: 'enabled', hitCount: 654, accuracy: 88, createdAt: '2024-01-03', updatedAt: '2024-01-13' },
- { id: 'R004', name: '产品推荐', priority: 4, triggerType: 'intent', keywords: [], matchMode: 'contain', responses: ['根据您的需求,为您推荐以下热销商品...'], status: 'enabled', hitCount: 432, accuracy: 82, createdAt: '2024-01-05', updatedAt: '2024-01-12' },
- { id: 'R005', name: '问候寒暄', priority: 5, triggerType: 'greeting', keywords: ['你好', '您好', 'hi', 'hello', '在吗'], matchMode: 'contain', responses: ['您好!很高兴为您服务。请问有什么可以帮助您的?'], status: 'enabled', hitCount: 2341, accuracy: 96, createdAt: '2024-01-01', updatedAt: '2024-01-10' },
- { id: 'R006', name: '感谢回复', priority: 6, triggerType: 'greeting', keywords: ['谢谢', '感谢', '辛苦了'], matchMode: 'contain', responses: ['不客气!很高兴能帮到您,祝您购物愉快!'], status: 'enabled', hitCount: 1234, accuracy: 98, createdAt: '2024-01-01', updatedAt: '2024-01-08' },
- { id: 'R007', name: '未知问题兜底', priority: 99, triggerType: 'fallback', keywords: [], matchMode: 'contain', responses: ['抱歉,您的问题我未能理解。请问您可以详细描述一下吗?您也可以联系人工客服获得更专业的帮助。'], status: 'enabled', hitCount: 567, accuracy: 75, createdAt: '2024-01-01', updatedAt: '2024-01-05' },
- { id: 'R008', name: '优惠券咨询', priority: 7, triggerType: 'keyword', keywords: ['优惠', '优惠券', '打折', '促销'], matchMode: 'contain', responses: ['当前有满199减20的优惠券可领取,是否需要我帮您跳转领取?'], status: 'disabled', hitCount: 234, accuracy: 85, createdAt: '2024-01-06', updatedAt: '2024-01-07' }
- ]);
- const filteredRules = computed(() => {
- return rules.value.filter(rule => {
- if (filters.value.name && !rule.name.includes(filters.value.name)) return false;
- if (filters.value.triggerType && rule.triggerType !== filters.value.triggerType) return false;
- if (filters.value.status && rule.status !== filters.value.status) return false;
- return true;
- }).sort((a, b) => a.priority - b.priority);
- });
- const editForm = ref<Partial<AutoReplyRule>>({});
- const form = ref<Partial<AutoReplyRule>>({ keywords: [], responses: [''], matchMode: 'contain', triggerType: 'keyword' });
- const loadData = () => { loading.value = true; setTimeout(() => { loading.value = false; }, 300); };
- const resetFilters = () => { filters.value = { name: '', triggerType: '', status: '' }; };
- const triggerTypeLabel = (type: string) => {
- const map: Record<string, string> = { keyword: '关键词', intent: '意图', greeting: '寒暄', fallback: '兜底' };
- return map[type] || type;
- };
- const triggerTypeTag = (type: string) => {
- const map: Record<string, string> = { keyword: 'primary', intent: 'success', greeting: 'warning', fallback: 'info' };
- return map[type] || '';
- };
- const selectRule = (rule: AutoReplyRule) => {
- currentRule.value = rule;
- editForm.value = { ...rule, responses: [...rule.responses] };
- isEdit.value = false;
- testResult.value = '';
- };
- const toggleStatus = (rule: AutoReplyRule, val: boolean) => {
- rule.status = val ? 'enabled' : 'disabled';
- ElMessage.success(`已${val ? '启用' : '禁用'}`);
- };
- const openCreate = () => {
- form.value = { keywords: [], responses: [''], matchMode: 'contain', triggerType: 'keyword' };
- dialogVisible.value = true;
- };
- const createRule = () => {
- if (!form.value.name || !form.value.responses?.[0]) {
- ElMessage.warning('请填写必填项');
- return;
- }
- rules.value.push({
- id: 'R' + Date.now(),
- priority: rules.value.length + 1,
- hitCount: 0,
- accuracy: 0,
- status: 'enabled',
- createdAt: new Date().toISOString().split('T')[0],
- updatedAt: new Date().toISOString().split('T')[0],
- ...form.value
- } as AutoReplyRule);
- ElMessage.success('创建成功');
- dialogVisible.value = false;
- };
- const saveRule = () => {
- if (!editForm.value.name || !editForm.value.responses?.[0]) {
- ElMessage.warning('请填写必填项');
- return;
- }
- const idx = rules.value.findIndex(r => r.id === currentRule.value?.id);
- if (idx !== -1) {
- rules.value[idx] = { ...rules.value[idx], ...editForm.value, updatedAt: new Date().toISOString().split('T')[0] } as AutoReplyRule;
- currentRule.value = rules.value[idx];
- }
- isEdit.value = false;
- ElMessage.success('保存成功');
- };
- const cancelEdit = () => {
- if (currentRule.value) {
- editForm.value = { ...currentRule.value, responses: [...currentRule.value.responses] };
- }
- isEdit.value = false;
- };
- const addResponse = () => {
- if (!editForm.value.responses) editForm.value.responses = [];
- editForm.value.responses.push('');
- };
- const testRule = () => {
- if (!testMessage.value) {
- ElMessage.warning('请输入测试消息');
- return;
- }
- testing.value = true;
- setTimeout(() => {
- if (currentRule.value?.triggerType === 'fallback') {
- testResult.value = currentRule.value.responses[0];
- } else if (testMessage.value.includes('物流') || testMessage.value.includes('快递')) {
- testResult.value = rules.value[0].responses[0];
- } else if (testMessage.value.includes('你好') || testMessage.value.includes('您好')) {
- testResult.value = rules.value[4].responses[0];
- } else {
- testResult.value = rules.value.find(r => r.triggerType === 'fallback')?.responses[0] || '抱歉,未匹配到任何规则';
- }
- testing.value = false;
- }, 500);
- };
- const saveFlow = () => { ElMessage.success('流程保存成功'); };
- onMounted(() => {
- if (filteredRules.value.length > 0) {
- selectRule(filteredRules.value[0]);
- }
- });
- </script>
- <style scoped>
- .filter-form :deep(.el-form-item) { margin-bottom: 0; }
- .rule-layout {
- display: grid;
- grid-template-columns: 320px 1fr;
- gap: 16px;
- min-height: 600px;
- }
- .rule-list {
- background: #fafafa;
- border-radius: 12px;
- padding: 16px;
- }
- .rule-list__header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 16px;
- }
- .rule-list__header h3 {
- margin: 0;
- font-size: 15px;
- font-weight: 600;
- }
- .rule-list__hint {
- font-size: 12px;
- color: #999;
- }
- .rule-items {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- .rule-item {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 12px;
- background: #fff;
- border-radius: 10px;
- cursor: pointer;
- transition: all 0.2s;
- border: 2px solid transparent;
- }
- .rule-item:hover {
- box-shadow: 0 2px 8px rgba(0,0,0,0.08);
- }
- .rule-item--active {
- border-color: var(--cb-primary);
- background: rgba(15, 118, 110, 0.05);
- }
- .rule-item--disabled {
- opacity: 0.6;
- }
- .rule-item__info {
- flex: 1;
- min-width: 0;
- }
- .rule-item__name {
- font-weight: 600;
- font-size: 14px;
- margin-bottom: 4px;
- }
- .rule-item__meta {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .rule-item__hit {
- font-size: 12px;
- color: #999;
- }
- .rule-detail {
- background: #fafafa;
- border-radius: 12px;
- padding: 16px;
- }
- .rule-detail__header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20px;
- }
- .rule-detail__header h3 {
- margin: 0;
- font-size: 15px;
- font-weight: 600;
- }
- .rule-form {
- max-width: 600px;
- }
- .multi-response__item {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 8px;
- }
- .multi-response__label {
- font-size: 13px;
- color: #666;
- white-space: nowrap;
- }
- .rule-stats {
- display: flex;
- gap: 24px;
- }
- .rule-stats__item {
- display: flex;
- flex-direction: column;
- gap: 4px;
- }
- .rule-stats__label {
- font-size: 12px;
- color: #999;
- }
- .rule-stats__value {
- font-size: 18px;
- font-weight: 600;
- color: var(--cb-primary);
- }
- .test-section {
- display: flex;
- gap: 8px;
- }
- .test-result {
- margin-top: 12px;
- padding: 12px;
- background: #fff;
- border-radius: 8px;
- }
- .test-result__label {
- font-size: 13px;
- color: #666;
- }
- .test-result__content {
- font-size: 14px;
- color: #333;
- }
- </style>
|