Files
inquiry_robot/inquiry-backend/src/views/inquiry/botReply/index.vue
T

280 lines
8.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
/**
* 机器人回复语:对接主账 /inquiry/botReply。
* 分类壳用本地元数据;条目内容全部来自库。
* 调用编码仅展示/预置,本期不向智能体下发。
*/
import { onMounted, reactive, ref } from "vue";
import { message } from "ant-design-vue";
import {
disableReply,
enableReply,
getReplyListAll,
saveReply,
} from "/@/views/inquiry/botReply/botReply.api";
import {
emptyCategories,
type ReplyCategory,
type ReplyItem,
} from "/@/views/inquiry/botReply/replyCategories";
import ProtoIcon from "/@/views/inquiry/components/ProtoIcon.vue";
import AdminModal from "/@/views/inquiry/components/AdminModal.vue";
import { usePermission } from "/@/hooks/web/usePermission";
import { InquiryBtnPerm } from "/@/views/inquiry/shared/inquiryPagePerms";
const { hasPermission } = usePermission();
const canAdd = () => hasPermission(InquiryBtnPerm.replyAdd);
const canEdit = () => hasPermission(InquiryBtnPerm.replyEdit);
const canDisable = () => hasPermission(InquiryBtnPerm.replyDisable);
const categories = ref<ReplyCategory[]>(emptyCategories());
const loading = ref(false);
const totalCount = ref(0);
const modalOpen = ref(false);
const editing = ref<{ categoryId: string; item: ReplyItem | null }>({
categoryId: "",
item: null,
});
const form = reactive({
replyCode: "",
trigger: "",
text: "",
enabled: true,
});
function groupByCategory(list: any[]) {
const cats = emptyCategories();
const byId = Object.fromEntries(cats.map((c) => [c.id, c]));
for (const row of list) {
const code = row.categoryCode || "inquiring";
let cat = byId[code];
if (!cat) {
cat = {
id: code,
title: row.categoryTitle || code,
desc: "",
tagClass: "tag-blue",
items: [],
};
cats.push(cat);
byId[code] = cat;
}
if (row.categoryTitle) cat.title = row.categoryTitle;
cat.items.push({
id: String(row.id),
replyCode: row.replyCode || "",
trigger: row.triggerScene || "",
text: row.replyText || "",
enabled: row.enabled === 1 || row.enabled === true,
});
}
return cats;
}
async function loadList() {
loading.value = true;
try {
const res: any = await getReplyListAll();
const list = Array.isArray(res)
? res
: Array.isArray(res?.result)
? res.result
: Array.isArray(res?.records)
? res.records
: [];
totalCount.value = list.length;
categories.value = groupByCategory(list as any[]);
} catch (e: any) {
message.error(e?.message || "加载回复语失败");
} finally {
loading.value = false;
}
}
onMounted(() => loadList());
async function toggle(item: ReplyItem) {
try {
if (item.enabled) {
await disableReply(item.id);
item.enabled = false;
message.success("已停用");
} else {
await enableReply(item.id);
item.enabled = true;
message.success("已启用");
}
} catch (e: any) {
message.error(e?.message || "切换失败");
}
}
function openCreate(categoryId?: string) {
editing.value = { categoryId: categoryId || categories.value[0]?.id || "inquiring", item: null };
Object.assign(form, { replyCode: "", trigger: "", text: "", enabled: true });
modalOpen.value = true;
}
function openEdit(categoryId: string, item: ReplyItem) {
editing.value = { categoryId, item };
Object.assign(form, {
replyCode: item.replyCode || "",
trigger: item.trigger,
text: item.text,
enabled: item.enabled,
});
modalOpen.value = true;
}
async function save() {
if (!form.trigger.trim() || !form.text.trim()) {
message.warning("请填写触发条件与话术");
return;
}
const cat = categories.value.find((c) => c.id === editing.value.categoryId);
if (!cat) return;
const body: Record<string, any> = {
id: editing.value.item?.id,
categoryCode: cat.id,
categoryTitle: cat.title,
triggerScene: form.trigger.trim(),
replyText: form.text.trim(),
enabled: form.enabled ? 1 : 0,
};
if (editing.value.item?.replyCode) {
body.replyCode = editing.value.item.replyCode;
} else if (form.replyCode.trim()) {
body.replyCode = form.replyCode.trim();
}
try {
await saveReply(body, Boolean(editing.value.item));
message.success(editing.value.item ? "话术已更新" : "话术已添加");
modalOpen.value = false;
await loadList();
} catch (e: any) {
message.error(e?.message || "保存失败");
}
}
</script>
<template>
<Teleport defer to="#page-topbar-actions">
<button v-if="canAdd()" type="button" class="btn btn-primary" @click="openCreate()">
<ProtoIcon name="plus" :size="16" />添加话术
</button>
</Teleport>
<div v-if="loading" style="padding: 24px; color: #94a3b8">加载中…</div>
<p v-else class="reply-total">共 {{ totalCount }} 条(按现网整理表应为 51 条;事件异常本期无条目)</p>
<article v-for="cat in categories" :key="cat.id" class="reply-category">
<div style="display: flex; align-items: center; justify-content: space-between">
<h2>{{ cat.title }}</h2>
<span class="tag" :class="cat.tagClass">{{ cat.items.length }} 条</span>
</div>
<p class="reply-category-desc">{{ cat.desc }}</p>
<div v-if="!cat.items.length" style="color: #94a3b8; font-size: 12px; padding: 8px 0">暂无话术</div>
<div v-for="item in cat.items" :key="item.id" class="reply-item">
<div style="min-width: 0">
<div class="reply-meta">触发:{{ item.trigger }}</div>
<div v-if="item.replyCode" class="reply-meta">调用编码:{{ item.replyCode }}</div>
<div class="reply-text">{{ item.text }}</div>
</div>
<div style="display: flex; align-items: center; gap: 8px; flex-shrink: 0">
<button
v-if="canDisable()"
type="button"
class="switch"
:class="{ 'is-on': item.enabled }"
:aria-label="item.enabled ? '停用' : '启用'"
@click="toggle(item)"
/>
<button
v-if="canEdit()"
type="button"
class="link-action"
@click="openEdit(cat.id, item)"
>
编辑
</button>
</div>
</div>
</article>
<AdminModal
v-model:open="modalOpen"
:title="editing.item ? '编辑话术' : '添加话术'"
size="lg"
ok-text="保存"
@ok="save"
>
<div class="form-row">
<label class="lbl">分类</label>
<select v-model="editing.categoryId" class="field" :disabled="Boolean(editing.item)">
<option v-for="c in categories" :key="c.id" :value="c.id">{{ c.title }}</option>
</select>
</div>
<div class="form-row">
<label class="lbl">调用编码</label>
<input
v-model="form.replyCode"
class="field"
type="text"
:disabled="Boolean(editing.item?.replyCode)"
placeholder="如 lost.ask_reason;已有编码不可改"
/>
</div>
<div class="form-row">
<label class="lbl">触发条件 <span class="req">*</span></label>
<input v-model="form.trigger" class="field" type="text" />
</div>
<div class="form-row form-row-tall">
<label class="lbl">回复正文 <span class="req">*</span></label>
<textarea
v-model="form.text"
class="field field-tall"
rows="12"
placeholder="支持 {{ticket_no}} 等变量"
/>
</div>
<div class="form-row">
<label class="lbl">状态</label>
<label class="check-line">
<input v-model="form.enabled" type="checkbox" />
<span>启用</span>
</label>
</div>
</AdminModal>
</template>
<style scoped>
.reply-total {
margin: 0;
font-size: 12px;
font-weight: 700;
color: #667085;
}
.form-row-tall {
align-items: start;
}
textarea.field-tall {
height: 240px !important;
min-height: 240px !important;
padding: 8px 10px !important;
line-height: 1.5 !important;
resize: vertical;
}
.check-line {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 34px;
font-size: 11px;
font-weight: 600;
color: #344054;
cursor: pointer;
}
</style>