Files
smartDriveEEUniApp/pages-subpackage/furniture_reception/script_management_my.vue
2026-04-23 14:10:41 +08:00

708 lines
18 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.

<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>