落地报价模板主账与后台:合同映射、灌种 SQL/脚本、智能体填表,并补测服运维探针与合同文档对齐。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-09-14 14:09:45 +08:00
co-authored by Cursor
parent 8bdf4990a2
commit a70bce3c8f
138 changed files with 7239 additions and 1085 deletions
@@ -1,14 +1,26 @@
<script setup lang="ts">
/**
* 机器人回复语:九分类卡片 + 开关编辑,对齐原型。
* 机器人回复语:对接主账 /inquiry/botReply。
* 分类壳用本地元数据;条目内容全部来自库。
*/
import { reactive, ref } from "vue";
import { onMounted, reactive, ref } from "vue";
import { message } from "ant-design-vue";
import { mockReplyCategories, type ReplyItem } from "/@/views/inquiry/mock/botReplies";
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";
const categories = ref(structuredClone(mockReplyCategories));
const categories = ref<ReplyCategory[]>(emptyCategories());
const loading = ref(false);
const modalOpen = ref(false);
const editing = ref<{ categoryId: string; item: ReplyItem | null }>({
categoryId: "",
@@ -20,9 +32,63 @@ const form = reactive({
enabled: true,
});
function toggle(item: ReplyItem) {
item.enabled = !item.enabled;
message.success(item.enabled ? "已启用" : "已停用");
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),
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 : res?.result || res?.records || [];
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) {
@@ -41,28 +107,29 @@ function openEdit(categoryId: string, item: ReplyItem) {
modalOpen.value = true;
}
function save() {
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;
if (editing.value.item) {
editing.value.item.trigger = form.trigger;
editing.value.item.text = form.text;
editing.value.item.enabled = form.enabled;
message.success("话术已更新");
} else {
cat.items.push({
id: `r${Date.now()}`,
trigger: form.trigger,
text: form.text,
enabled: form.enabled,
});
message.success("话术已添加");
const body = {
id: editing.value.item?.id,
categoryCode: cat.id,
categoryTitle: cat.title,
triggerScene: form.trigger.trim(),
replyText: form.text.trim(),
enabled: form.enabled ? 1 : 0,
};
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 || "保存失败");
}
modalOpen.value = false;
}
</script>
@@ -73,6 +140,8 @@ function save() {
</button>
</Teleport>
<div v-if="loading" style="padding: 24px; color: #94a3b8">加载中…</div>
<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>
@@ -80,6 +149,8 @@ function save() {
</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>
@@ -93,13 +164,7 @@ function save() {
:aria-label="item.enabled ? '停用' : '启用'"
@click="toggle(item)"
/>
<button
type="button"
class="btn btn-ghost btn-icon-sm"
@click="openEdit(cat.id, item)"
>
<ProtoIcon name="pencil" :size="16" />
</button>
<button type="button" class="link-action" @click="openEdit(cat.id, item)">编辑</button>
</div>
</div>
</article>
@@ -111,37 +176,39 @@ function save() {
ok-text="保存"
@ok="save"
>
<div class="admin-modal-field">
<label class="admin-modal-label">触发场景 / 条件 <span class="req">*</span></label>
<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">触发条件 <span class="req">*</span></label>
<input v-model="form.trigger" class="field" type="text" />
</div>
<div class="admin-modal-field">
<label class="admin-modal-label">回复内容 <span class="req">*</span></label>
<textarea
v-model="form.text"
class="field"
rows="5"
placeholder="支持 {{变量}} 占位符"
/>
<div class="form-row">
<label class="lbl">回复正文 <span class="req">*</span></label>
<textarea v-model="form.text" class="field" rows="5" placeholder="支持 {{ticket_no}} 等变量" />
</div>
<div class="admin-modal-field">
<label class="admin-modal-label">启用状态</label>
<div class="status-toggle">
<button
type="button"
:class="{ 'is-selected': form.enabled }"
@click="form.enabled = true"
>
启用
</button>
<button
type="button"
:class="{ 'is-selected': !form.enabled }"
@click="form.enabled = false"
>
停用
</button>
</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>
.check-line {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 34px;
font-size: 11px;
font-weight: 600;
color: #344054;
cursor: pointer;
}
</style>