话术管理页面

This commit is contained in:
zhonghua.li
2026-04-23 14:07:47 +08:00
parent 810dd2036b
commit 05a3a2f606
5 changed files with 762 additions and 2 deletions

View File

@@ -27,6 +27,11 @@
<text class="function-name">标签管理</text>
<text class="function-desc"></text>
</view>
<view class="function-item" @click="quickJumpScriptManagement">
<view class="function-icon">📚</view>
<text class="function-name">话术管理</text>
<text class="function-desc"></text>
</view>
</view>
</view>
@@ -239,6 +244,18 @@ export default {
},
});
},
quickJumpScriptManagement() {
uni.navigateTo({
url: '/pages-subpackage/furniture_reception/script_management_my',
fail: (err) => {
console.error('[SoftSalesScenario] 跳转话术管理页失败:', err);
uni.showToast({
title: '跳转失败,请稍后重试',
icon: 'none',
});
},
});
},
},
};
</script>
@@ -319,7 +336,7 @@ export default {
}
.quick-links-section .function-item {
width: calc(33.333% - 10rpx);
width: calc(25% - 10rpx);
background: #ffffff;
border-radius: 12rpx;
padding: 24rpx 12rpx;

View File

@@ -0,0 +1,707 @@
<template>
<view class="page">
<view class="toolbar">
<text class="toolbar-total">{{ total }}</text>
<input
class="toolbar-input"
v-model="query.title"
placeholder="知识标题"
placeholder-style="color:#b0b0b0"
confirm-type="search"
@confirm="onSearch"
/>
<input
class="toolbar-input"
v-model="query.category"
placeholder="知识分类"
placeholder-style="color:#b0b0b0"
confirm-type="search"
@confirm="onSearch"
/>
<input
class="toolbar-input"
v-model="query.status"
placeholder="状态"
placeholder-style="color:#b0b0b0"
confirm-type="search"
@confirm="onSearch"
/>
<view class="toolbar-actions">
<view class="toolbar-btn toolbar-btn--add" @click="openCreateDialog">
<uni-icons type="plusempty" size="16" color="#16a34a" />
<text>新增</text>
</view>
<view class="toolbar-btn toolbar-btn--refresh" @click="onRefresh">
<uni-icons type="refresh" size="16" color="#2A68FF" />
<text>刷新</text>
</view>
</view>
</view>
<scroll-view class="list" scroll-y enable-flex @scrolltolower="onReachBottom">
<view class="card" v-for="item in list" :key="item.id">
<view class="card-header">
<view class="title-wrap">
<text class="title">{{ item.title || '未命名知识' }}</text>
<text v-if="item.status" class="status">{{ item.status }}</text>
</view>
<view class="top-flag" v-if="item.isTop">置顶</view>
</view>
<view class="meta">
<text>分类{{ item.category || '-' }}</text>
<text>行业{{ item.industry || '-' }}</text>
<text>创建人{{ item.creator || '-' }}</text>
</view>
<view class="content-line" v-if="item.question">问题{{ item.question }}</view>
<view class="content-line" v-if="item.answerContent">内容{{ item.answerContent }}</view>
<view class="time-line">
<text>创建{{ formatDateTime(item.createTime) || '-' }}</text>
<text>更新{{ formatDateTime(item.updateTime) || '-' }}</text>
</view>
<view class="actions">
<view class="action-btn action-btn--edit" @click="openEditDialog(item)">编辑</view>
<view class="action-btn action-btn--delete" @click="onDelete(item)">删除</view>
</view>
</view>
<view class="empty" v-if="!loading && !list.length">
<text>暂无话术数据</text>
</view>
<view class="loading-more" v-if="loading && list.length">
<text>正在加载...</text>
</view>
</scroll-view>
<view class="dialog-mask" v-if="showDialog" @click="closeDialog">
<view class="dialog" @click.stop>
<view class="dialog-header">
<text class="dialog-title">{{ form.id ? '编辑话术' : '新增话术' }}</text>
<view class="dialog-close" @click="closeDialog">
<uni-icons type="close" size="18" color="#999" />
</view>
</view>
<scroll-view class="dialog-body" scroll-y>
<view class="form-item">
<text class="form-label">知识标题</text>
<input class="form-input" v-model="form.title" placeholder="请输入知识标题" />
</view>
<view class="form-item">
<text class="form-label">知识分类</text>
<input class="form-input" v-model="form.category" placeholder="请输入知识分类" />
</view>
<view class="form-item">
<text class="form-label">应用行业</text>
<input class="form-input" v-model="form.industry" placeholder="请输入应用行业" />
</view>
<view class="form-item">
<text class="form-label">问题</text>
<textarea class="form-textarea" v-model="form.question" placeholder="请输入问题" />
</view>
<view class="form-item">
<text class="form-label">知识内容</text>
<textarea class="form-textarea" v-model="form.answerContent" placeholder="请输入知识内容" />
</view>
<view class="form-item">
<text class="form-label">详细分解</text>
<textarea class="form-textarea" v-model="form.detailedBreakdown" placeholder="请输入详细分解" />
</view>
<view class="form-item">
<text class="form-label">使用场景</text>
<textarea class="form-textarea" v-model="form.usageScenario" placeholder="请输入使用场景" />
</view>
<view class="form-item">
<text class="form-label">标签逗号分隔</text>
<input class="form-input" v-model="form.tags" placeholder="如:成交,开场,破冰" />
</view>
<view class="form-item">
<text class="form-label">状态</text>
<input class="form-input" v-model="form.status" placeholder="如:已发布/草稿/已下架" />
</view>
<view class="form-item row-item">
<text class="form-label">是否置顶</text>
<switch :checked="!!form.isTop" @change="onTopSwitchChange" color="#2A68FF" />
</view>
<view class="form-item">
<text class="form-label">备注</text>
<textarea class="form-textarea" v-model="form.remark" placeholder="请输入备注" />
</view>
</scroll-view>
<view class="dialog-footer">
<view class="dialog-btn dialog-btn--cancel" @click="closeDialog">取消</view>
<view class="dialog-btn dialog-btn--confirm" @click="onSubmit">保存</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { getApiUrl } from "@/common/config.js";
function createDefaultForm() {
return {
id: "",
title: "",
category: "",
industry: "",
question: "",
answerContent: "",
detailedBreakdown: "",
usageScenario: "",
status: "",
tags: "",
isTop: false,
remark: "",
};
}
export default {
data() {
return {
loading: false,
total: 0,
page: {
current: 1,
size: 10,
},
query: {
title: "",
category: "",
industry: "",
creator: "",
status: "",
},
list: [],
showDialog: false,
form: createDefaultForm(),
};
},
mounted() {
this.fetchList({ force: true });
},
methods: {
getAuthHeaders() {
let tenantId = "";
let token = "";
try {
tenantId = uni.getStorageSync("backend-tenant-id") || "";
token = uni.getStorageSync("backend-token") || "";
} catch (e) {
console.error("读取认证信息失败:", e);
}
const headers = {
"Content-Type": "application/json",
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
if (tenantId) {
headers["X-Tenant-Id"] = tenantId;
}
return headers;
},
buildQueryString() {
const payload = {
current: this.page.current,
size: this.page.size,
title: this.query.title.trim(),
category: this.query.category.trim(),
industry: this.query.industry.trim(),
creator: this.query.creator.trim(),
status: this.query.status.trim(),
};
return Object.keys(payload)
.filter((k) => payload[k] !== undefined && payload[k] !== null && payload[k] !== "")
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(payload[k])}`)
.join("&");
},
onSearch() {
this.page.current = 1;
this.fetchList({ force: true });
},
onRefresh() {
this.page.current = 1;
this.fetchList({ force: true });
},
onReachBottom() {
if (this.loading) return;
if (this.list.length >= this.total) return;
this.page.current += 1;
this.fetchList();
},
async fetchList({ force = false } = {}) {
if (this.loading && !force) return;
this.loading = true;
try {
const qs = this.buildQueryString();
const baseUrl = getApiUrl("/api/scriptManagementMy/list");
const url = qs ? `${baseUrl}?${qs}` : baseUrl;
const res = await uni.request({
url,
method: "GET",
header: this.getAuthHeaders(),
timeout: 30000,
});
if (res.statusCode === 200 && res.data?.success) {
const rawData = res.data?.data;
const records = Array.isArray(rawData)
? rawData
: Array.isArray(rawData?.records)
? rawData.records
: [];
const total = Number(res.data?.total ?? rawData?.total ?? records.length) || 0;
this.total = total;
if (this.page.current === 1 || force) {
this.list = [...records];
} else {
this.list = [...this.list, ...records];
}
return;
}
this.handleRequestError(res.data?.message || "获取话术列表失败");
} catch (error) {
console.error("获取话术列表失败:", error);
this.handleRequestError("获取话术列表失败,请稍后重试");
} finally {
this.loading = false;
}
},
openCreateDialog() {
this.form = createDefaultForm();
this.showDialog = true;
},
openEditDialog(item) {
this.form = {
id: item.id || "",
title: item.title || "",
category: item.category || "",
industry: item.industry || "",
question: item.question || "",
answerContent: item.answerContent || "",
detailedBreakdown: item.detailedBreakdown || "",
usageScenario: item.usageScenario || "",
status: item.status || "",
tags: item.tags || "",
isTop: !!item.isTop,
remark: item.remark || "",
};
this.showDialog = true;
},
closeDialog() {
this.showDialog = false;
},
onTopSwitchChange(e) {
this.form.isTop = !!e?.detail?.value;
},
async onSubmit() {
const title = this.form.title.trim();
if (!title) {
uni.showToast({ title: "请输入知识标题", icon: "none" });
return;
}
const payload = {
id: this.form.id || undefined,
title,
category: this.form.category.trim(),
industry: this.form.industry.trim(),
question: this.form.question.trim(),
answerContent: this.form.answerContent.trim(),
detailedBreakdown: this.form.detailedBreakdown.trim(),
usageScenario: this.form.usageScenario.trim(),
status: this.form.status.trim(),
tags: this.form.tags.trim(),
isTop: !!this.form.isTop,
remark: this.form.remark.trim(),
};
const isEdit = !!this.form.id;
const url = getApiUrl(isEdit ? "/api/scriptManagementMy/update" : "/api/scriptManagementMy/add");
const method = isEdit ? "PUT" : "POST";
try {
uni.showLoading({ title: "保存中..." });
const res = await uni.request({
url,
method,
data: payload,
header: this.getAuthHeaders(),
timeout: 30000,
});
uni.hideLoading();
if (res.statusCode === 200 && res.data?.success) {
uni.showToast({ title: isEdit ? "更新成功" : "新增成功", icon: "success" });
this.closeDialog();
this.page.current = 1;
this.fetchList({ force: true });
return;
}
this.handleRequestError(res.data?.message || "保存失败");
} catch (error) {
uni.hideLoading();
console.error("保存话术失败:", error);
this.handleRequestError("保存失败,请稍后重试");
}
},
onDelete(item) {
if (!item?.id) return;
uni.showModal({
title: "确认删除",
content: `确定删除「${item.title || "该话术"}」吗?`,
success: async (res) => {
if (!res.confirm) return;
try {
uni.showLoading({ title: "删除中..." });
const url = getApiUrl(`/api/scriptManagementMy/delete/${encodeURIComponent(item.id)}`);
const delRes = await uni.request({
url,
method: "DELETE",
header: this.getAuthHeaders(),
timeout: 30000,
});
uni.hideLoading();
if (delRes.statusCode === 200 && delRes.data?.success) {
uni.showToast({ title: "删除成功", icon: "success" });
this.page.current = 1;
this.fetchList({ force: true });
return;
}
this.handleRequestError(delRes.data?.message || "删除失败");
} catch (error) {
uni.hideLoading();
console.error("删除话术失败:", error);
this.handleRequestError("删除失败,请稍后重试");
}
},
});
},
handleRequestError(message) {
this.list = this.page.current === 1 ? [] : this.list;
this.total = this.page.current === 1 ? 0 : this.total;
uni.showToast({
title: message || "请求失败",
icon: "none",
});
},
formatDateTime(value) {
if (!value) return "";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "";
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hour = String(date.getHours()).padStart(2, "0");
const minute = String(date.getMinutes()).padStart(2, "0");
const second = String(date.getSeconds()).padStart(2, "0");
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
},
},
};
</script>
<style scoped>
.page {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
padding: 0 16rpx;
box-sizing: border-box;
}
.toolbar {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 8rpx;
padding: 12rpx 8rpx;
margin: 16rpx 0 24rpx;
background-color: #f8f8fa;
border-radius: 8rpx;
border: 1px solid #efeff2;
overflow-x: auto;
min-height: 64rpx;
box-sizing: border-box;
flex-shrink: 0;
}
.toolbar-total {
font-size: 24rpx;
color: #666;
white-space: nowrap;
flex-shrink: 0;
margin-right: 8rpx;
}
.toolbar-input {
flex: 1;
min-width: 120rpx;
max-width: 200rpx;
height: 56rpx;
line-height: 56rpx;
background-color: #f5f6fa;
border-radius: 8rpx;
padding: 0 12rpx;
font-size: 24rpx;
border: 1px solid transparent;
box-sizing: border-box;
}
.toolbar-actions {
display: flex;
align-items: center;
margin-left: auto;
gap: 8rpx;
flex-shrink: 0;
}
.toolbar-btn {
height: 56rpx;
min-width: 56rpx;
padding: 0 12rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 4rpx;
font-size: 24rpx;
}
.toolbar-btn--add {
color: #16a34a;
}
.toolbar-btn--refresh {
color: #2a68ff;
}
.list {
flex: 1;
box-sizing: border-box;
}
.card {
width: 100%;
background-color: #fff;
border-radius: 16rpx;
padding: 28rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
box-sizing: border-box;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14rpx;
}
.title-wrap {
display: flex;
align-items: center;
gap: 10rpx;
min-width: 0;
}
.title {
font-size: 30rpx;
color: #111827;
font-weight: 600;
max-width: 420rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status {
font-size: 22rpx;
color: #2563eb;
padding: 4rpx 10rpx;
border-radius: 999rpx;
background: #eaf2ff;
}
.top-flag {
font-size: 22rpx;
color: #b45309;
background-color: #fff7ed;
padding: 4rpx 10rpx;
border-radius: 999rpx;
}
.meta {
display: flex;
flex-wrap: wrap;
gap: 10rpx 16rpx;
margin-bottom: 12rpx;
}
.meta text,
.content-line,
.time-line text {
font-size: 24rpx;
color: #4b5563;
}
.content-line {
margin-bottom: 10rpx;
line-height: 1.5;
}
.time-line {
display: flex;
flex-wrap: wrap;
gap: 8rpx 16rpx;
padding-top: 12rpx;
border-top: 1px solid #f3f4f6;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 12rpx;
margin-top: 16rpx;
}
.action-btn {
font-size: 24rpx;
padding: 8rpx 18rpx;
border-radius: 10rpx;
border: 1px solid transparent;
}
.action-btn--edit {
color: #2563eb;
border-color: rgba(37, 99, 235, 0.3);
background-color: #f0f6ff;
}
.action-btn--delete {
color: #dc2626;
border-color: rgba(220, 38, 38, 0.28);
background-color: #fff4f4;
}
.empty,
.loading-more {
text-align: center;
color: #999;
font-size: 28rpx;
padding: 40rpx 0;
}
.dialog-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.dialog {
width: 660rpx;
max-height: 84vh;
background-color: #fff;
border-radius: 24rpx;
overflow: hidden;
display: flex;
flex-direction: column;
}
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 30rpx 20rpx;
border-bottom: 1px solid #f0f0f0;
}
.dialog-title {
font-size: 32rpx;
color: #111827;
font-weight: 500;
}
.dialog-close {
width: 44rpx;
height: 44rpx;
display: flex;
align-items: center;
justify-content: center;
}
.dialog-body {
flex: 1;
max-height: 900rpx;
padding: 24rpx 30rpx;
box-sizing: border-box;
}
.form-item {
margin-bottom: 22rpx;
}
.row-item {
display: flex;
align-items: center;
justify-content: space-between;
}
.form-label {
display: block;
font-size: 26rpx;
color: #374151;
margin-bottom: 10rpx;
}
.form-input,
.form-textarea {
width: 100%;
background-color: #f9fafb;
border-radius: 10rpx;
box-sizing: border-box;
font-size: 26rpx;
color: #111827;
}
.form-input {
height: 80rpx;
padding: 0 20rpx;
}
.form-textarea {
min-height: 140rpx;
padding: 16rpx 20rpx;
line-height: 1.5;
}
.dialog-footer {
display: flex;
gap: 18rpx;
padding: 18rpx 30rpx 26rpx;
border-top: 1px solid #f0f0f0;
}
.dialog-btn {
flex: 1;
height: 82rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 500;
}
.dialog-btn--cancel {
background-color: #f3f4f6;
color: #6b7280;
}
.dialog-btn--confirm {
background-color: #4c8dff;
color: #fff;
}
</style>

View File

@@ -265,6 +265,12 @@
"navigationStyle": "custom"
}
},
{
"path": "furniture_reception/script_management_my",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "bluetooth-recorder/bluetooth-recorder",
"style": {

View File

@@ -17,6 +17,11 @@
<text class="function-name">标签管理</text>
<text class="function-desc"></text>
</view>
<view class="function-item" @click="openScriptManagement">
<view class="function-icon">📚</view>
<text class="function-name">话术管理</text>
<text class="function-desc"></text>
</view>
</view>
</view>
@@ -76,6 +81,14 @@ export default {
const url = `/pages-subpackage/furniture_customer/furniture_customer?tab=${tab}`;
uni.navigateTo({ url });
},
openScriptManagement() {
uni.navigateTo({
url: '/pages-subpackage/furniture_reception/script_management_my',
fail: () => {
uni.showToast({ title: '跳转话术管理失败', icon: 'none' });
},
});
},
},
};
</script>

View File

@@ -27,6 +27,11 @@
<text class="function-name">标签管理</text>
<text class="function-desc"></text>
</view>
<view class="function-item" @click="quickJumpScriptManagement">
<view class="function-icon">📚</view>
<text class="function-name">话术管理</text>
<text class="function-desc"></text>
</view>
</view>
</view>
@@ -239,6 +244,18 @@ export default {
},
});
},
quickJumpScriptManagement() {
uni.navigateTo({
url: '/pages-subpackage/furniture_reception/script_management_my',
fail: (err) => {
console.error('[SmallBeautifulSales] 跳转话术管理页失败:', err);
uni.showToast({
title: '跳转失败,请稍后重试',
icon: 'none',
});
},
});
},
},
};
</script>
@@ -319,7 +336,7 @@ export default {
}
.quick-links-section .function-item {
width: calc(33.333% - 10rpx);
width: calc(25% - 10rpx);
background: #ffffff;
border-radius: 12rpx;
padding: 24rpx 12rpx;