280 lines
8.0 KiB
Vue
280 lines
8.0 KiB
Vue
<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>
|