Files
smartDriveEEUniApp/pages/reception/reception.vue
2025-12-16 08:56:33 +08:00

1831 lines
45 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">
<!-- #ifdef APP -->
<statusBar></statusBar>
<!-- #endif -->
<!-- 导航栏 -->
<uni-nav-bar
:fixed="true"
:statusBar="true"
title="AI销售管理"
leftIcon="left"
@clickLeft="goBack"
color="#333"
backgroundColor="#FFFFFF">
</uni-nav-bar>
<view class="content">
<!-- AI功能横幅 -->
<view class="ai-banner">
<text class="banner-text">AI让销售过程和团队管理更透明</text>
</view>
<!-- 标签页 -->
<view class="tabs">
<view
class="tab-item"
:class="{ active: activeTab === 'status' }"
@click="switchTab('status')">
<text>服务状态</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'reception' }"
@click="switchTab('reception')">
<text>开始接待</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'tag' }"
@click="switchTab('tag')">
<text>标签管理</text>
</view>
</view>
<!-- 服务状态列表 -->
<scroll-view
class="service-status-list"
scroll-y
v-if="activeTab === 'status'"
@scrolltolower="onServiceStatusReachBottom">
<view class="service-status-toolbar">
<text class="toolbar-total">{{ serviceStatusTotal }}</text>
<input
class="toolbar-input"
v-model="serviceStatusQuery.salesName"
placeholder="所属销售姓名"
placeholder-style="color: #b0b0b0"
confirm-type="search"
@confirm="onServiceStatusSearch"
/>
<input
class="toolbar-input"
v-model="serviceStatusQuery.customerName"
placeholder="客户姓名"
placeholder-style="color: #b0b0b0"
confirm-type="search"
@confirm="onServiceStatusSearch"
/>
<view class="toolbar-actions">
<view class="toolbar-btn toolbar-btn--refresh" @click="onServiceStatusRefresh">
<uni-icons type="refresh" size="18" color="#2A68FF"></uni-icons>
<text>刷新</text>
</view>
</view>
</view>
<view
class="service-card"
v-for="(item, index) in serviceStatusList"
:key="index"
@click="viewServiceDetail(item)">
<view class="card-header">
<view class="staff-info">
<view class="staff-avatar">
<text class="avatar-text">{{ item.staffName.charAt(0) }}</text>
</view>
<text class="staff-name">{{ item.staffName }}</text>
</view>
<view class="service-status">
<uni-icons type="bars" size="16" color="#007AFF"></uni-icons>
<text>服务中</text>
</view>
</view>
<view class="customer-info">
<text class="customer-name">客户{{ item.customerName || '未知客户' }}</text>
<text class="customer-phone" v-if="item.customerPhone">电话{{ item.customerPhone }}</text>
</view>
<view class="customer-tags">
<view
class="tag-item"
:class="'tag-' + tag.color"
v-for="(tag, tagIndex) in item.tags"
:key="tagIndex">
<text>{{ tag.text }}</text>
</view>
</view>
<!-- AI提醒 -->
<view class="ai-alert" v-if="item.alert" :class="item.alert.type === 'risk' ? 'alert-risk-box' : 'alert-reminder-box'">
<view class="alert-header">
<view class="alert-icon">
<view class="icon-circle"></view>
</view>
<text class="alert-title">{{ item.alert.title }}</text>
</view>
<view class="alert-content" :class="item.alert.type === 'risk' ? 'alert-risk-text' : 'alert-reminder-text'">
<text v-if="item.alert.type === 'risk'">{{ item.alert.message }}</text>
<view v-else class="alert-list">
<view class="alert-item" v-for="(msg, msgIndex) in item.alert.messages" :key="msgIndex">
<text>{{ msg }}</text>
</view>
</view>
</view>
</view>
<view class="service-duration">
<text>{{ item.durationText }}</text>
</view>
</view>
<view class="service-status-empty" v-if="!serviceStatusLoading && !serviceStatusList.length">
<text>暂无服务中记录</text>
</view>
<view class="service-status-loading-more" v-if="serviceStatusLoading && serviceStatusList.length">
<text>正在加载更多...</text>
</view>
</scroll-view>
<!-- 开始接待表单 -->
<scroll-view class="reception-form" scroll-y v-if="activeTab === 'reception'">
<view class="form-card">
<view class="form-item">
<text class="form-item__label">服务次数</text>
<view class="form-item__text">
<text>{{ receptionForm.contactCount || 0 }}</text>
</view>
</view>
<view class="form-item">
<text class="form-item__label">客户姓名</text>
<input
class="form-item__input"
v-model="receptionForm.customerName"
placeholder="请输入客户姓名"
placeholder-style="color: #9ca3af"
/>
</view>
<view class="form-item">
<text class="form-item__label">客户电话</text>
<input
class="form-item__input"
v-model="receptionForm.contact"
@blur="onContactBlur"
type="number"
placeholder="请输入客户电话"
placeholder-style="color: #9ca3af"
/>
</view>
<view class="form-item">
<text class="form-item__label">客户来源</text>
<input
class="form-item__input"
v-model="receptionForm.customerSource"
placeholder="请输入客户来源"
placeholder-style="color: #9ca3af"
/>
</view>
<view class="form-item">
<text class="form-item__label">门店名称</text>
<input
class="form-item__input"
v-model="receptionForm.dealershipName"
placeholder="请输入门店名称"
placeholder-style="color: #9ca3af"
/>
</view>
<view class="form-item">
<text class="form-item__label">性别</text>
<radio-group
class="gender-radio-group"
@change="onGenderChange">
<label
class="gender-radio"
v-for="(option, index) in genderOptions"
:key="index">
<radio
:value="option"
:checked="receptionForm.gender === option"
color="#2563eb" />
<text class="gender-radio__text">{{ option }}</text>
</label>
</radio-group>
</view>
<view class="form-item">
<text class="form-item__label">年龄</text>
<input
class="form-item__input"
v-model="receptionForm.age"
type="number"
placeholder="请输入年龄"
placeholder-style="color: #9ca3af"
/>
</view>
<view class="form-item">
<text class="form-item__label">销售姓名</text>
<input
class="form-item__input"
v-model="receptionForm.salesName"
placeholder="请输入销售姓名"
placeholder-style="color: #9ca3af"
/>
</view>
<view class="form-item">
<text class="form-item__label">销售电话</text>
<input
class="form-item__input"
v-model="receptionForm.salesPhone"
type="number"
placeholder="请输入销售电话"
placeholder-style="color: #9ca3af"
/>
</view>
<view class="form-item">
<text class="form-item__label">住址</text>
<input
class="form-item__input"
v-model="receptionForm.detailedAddress"
placeholder="请输入详细住址"
placeholder-style="color: #9ca3af"
/>
</view>
</view>
<view class="form-actions form-actions--triple">
<view class="form-btn form-btn--cancel" @click="onCancel">
<text class="form-btn__text">取消</text>
</view>
<view class="form-btn form-btn--save" @click="onSave('save')">
<text class="form-btn__text">保存</text>
</view>
<view class="form-btn form-btn--start" @click="onSave('start')">
<text class="form-btn__text">开始接待</text>
</view>
</view>
</scroll-view>
<!-- 标签管理 -->
<scroll-view
class="service-status-list"
scroll-y
v-if="activeTab === 'tag' && tagViewMode === 'list'"
@scrolltolower="onTagListReachBottom">
<view class="service-status-toolbar">
<text class="toolbar-total">{{ tagTotal }}</text>
<input
class="toolbar-input"
v-model="tagQuery.tagType"
placeholder="标签分类"
placeholder-style="color: #b0b0b0"
confirm-type="search"
@confirm="onTagListSearch"
/>
<input
class="toolbar-input"
v-model="tagQuery.tagName"
placeholder="标签名称"
placeholder-style="color: #b0b0b0"
confirm-type="search"
@confirm="onTagListSearch"
/>
<view class="toolbar-actions">
<view class="toolbar-btn toolbar-btn--refresh" @click="onTagListRefresh">
<uni-icons type="refresh" size="18" color="#2A68FF"></uni-icons>
</view>
<view class="toolbar-btn toolbar-btn--add" @click="showAddTag">
<uni-icons type="plus" size="18" color="#2A68FF"></uni-icons>
</view>
</view>
</view>
<view
class="service-card tag-card-item"
v-for="(item, index) in tagList"
:key="index"
:class="{ 'tag-card-item--active': selectedTagItem && selectedTagItem.id === item.id }">
<view class="card-header">
<view class="staff-info" @click.stop="onTagItemClick(item)">
<view class="staff-avatar">
<text class="avatar-text">{{ (item.tagName || item.name || '标').charAt(0) }}</text>
</view>
<text class="staff-name">{{ item.tagName || item.name || '未命名标签' }}</text>
</view>
<view class="tag-card-action-btn" @click.stop="onTagActionBtnClick(item)">
<uni-icons type="more-filled" size="20" color="#999"></uni-icons>
</view>
</view>
<view class="customer-info" @click.stop="onTagItemClick(item)">
<text class="customer-name" v-if="item.industry">所属行业{{ item.industry }}</text>
</view>
<view class="customer-tags" v-if="item.tagDetail || item.detail || item.remark" @click.stop="onTagItemClick(item)">
<view
class="tag-item tag-blue"
v-if="item.tagDetail || item.detail">
<text>详情{{ item.tagDetail || item.detail }}</text>
</view>
<view
class="tag-item tag-orange"
v-if="item.remark">
<text>备注{{ item.remark }}</text>
</view>
</view>
<!-- 操作菜单 -->
<view
class="tag-action-menu"
v-if="selectedTagItem && selectedTagItem.id === item.id && showTagActionMenu"
@click.stop>
<view class="tag-action-menu-item" @click="editTag(item)">
<text>编辑</text>
</view>
<view class="tag-action-menu-divider"></view>
<view class="tag-action-menu-item tag-action-menu-item--danger" @click="deleteTag(item)">
<text>删除</text>
</view>
</view>
</view>
<!-- 遮罩层点击关闭菜单 -->
<view
class="tag-action-menu-mask"
v-if="showTagActionMenu"
@click="closeTagActionMenu">
</view>
<view class="service-status-empty" v-if="!tagLoading && !tagList.length">
<text>暂无标签</text>
</view>
<view class="service-status-loading-more" v-if="tagLoading && tagList.length">
<text>正在加载更多...</text>
</view>
</scroll-view>
<!-- 标签管理 - 添加/编辑标签表单 -->
<view class="tag-management" v-if="activeTab === 'tag' && (tagViewMode === 'add' || tagViewMode === 'edit')">
<!-- 添加标签表单 -->
<scroll-view
class="tag-form"
scroll-y>
<view class="form-card">
<view class="form-item" v-if="tagViewMode === 'add'">
<text class="form-item__label">所属行业</text>
<input
class="form-item__input"
v-model="tagForm.industry"
placeholder="请输入所属行业"
placeholder-style="color: #9ca3af"
/>
</view>
<view class="form-item tag-select-wrapper">
<text class="form-item__label">标签分类</text>
<view
class="tag-select"
@click.stop="toggleTagTypeDropdown"
>
<text
:class="tagForm.tagType ? 'form-item__picker-text' : 'form-item__picker-placeholder'"
>
{{ tagForm.tagType || "请选择标签分类" }}
</text>
<uni-icons type="bottom" size="16" color="#9ca3af"></uni-icons>
</view>
<view
v-if="showTagTypeDropdown"
class="tag-select-dropdown"
@click.stop
>
<view
class="tag-select-dropdown__item"
v-for="option in tagTypeOptions"
:key="option"
@click="selectTagType(option)"
>
<text :class="{'active': option === tagForm.tagType}">
{{ option }}
</text>
</view>
</view>
</view>
<view class="form-item">
<text class="form-item__label">标签名</text>
<input
class="form-item__input"
v-model="tagForm.name"
placeholder="请输入标签名"
placeholder-style="color: #9ca3af"
/>
</view>
<view class="form-item">
<text class="form-item__label">详情</text>
<textarea
class="form-item__textarea form-item__textarea--detail"
v-model="tagForm.detail"
placeholder="请输入详情"
placeholder-style="color: #9ca3af"
:maxlength="500"
/>
</view>
<view class="form-item">
<text class="form-item__label">备注</text>
<textarea
class="form-item__textarea form-item__textarea--remark"
v-model="tagForm.remark"
placeholder="请输入备注"
placeholder-style="color: #9ca3af"
:maxlength="500"
/>
</view>
<view class="form-item">
<text class="form-item__label">是否启用</text>
<switch
:checked="tagForm.enabled"
@change="onEnabledChange"
color="#007AFF"
/>
</view>
</view>
<view class="form-actions">
<view class="form-btn form-btn--cancel" @click="cancelAddTag">
<text class="form-btn__text">取消</text>
</view>
<view class="form-btn form-btn--save" @click="saveTag">
<text class="form-btn__text">{{ tagViewMode === 'edit' ? '更新' : '保存' }}</text>
</view>
</view>
</scroll-view>
<view
v-if="showTagTypeDropdown"
class="tag-select-mask"
@click="closeTagTypeDropdown"
></view>
</view>
</view>
</view>
</template>
<script>
// #ifdef APP
import statusBar from "@/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-status-bar";
// #endif
import { getApiUrl } from "@/common/config.js";
export default {
// #ifdef APP
components: {
statusBar
},
// #endif
data() {
return {
activeTab: 'status',
genderOptions: ["男", "女"],
isFetchingContact: false,
serviceStatusLoading: false,
serviceStatusTotal: 0,
serviceStatusPage: {
current: 1,
size: 10
},
serviceStatusQuery: {
salesName: '',
customerName: ''
},
receptionForm: {
id: "",
customerName: "",
contact: "",
customerSource: "",
gender: "女",
age: "",
dealershipId: "",
dealershipName: "",
salesId: "",
salesName: "",
salesPhone: "",
recordingCount: 0,
intendedModel: "",
infoCard: "",
remark: "",
detailedAddress: "",
contactCount: 0
},
serviceStatusList: [],
// 标签管理
tagViewMode: 'list', // 'list' 或 'add' 或 'edit'
tagList: [],
tagLoading: false,
tagTotal: 0,
tagPage: {
current: 1,
size: 10
},
tagQuery: {
industry: '',
tagType: '',
tagName: ''
},
tagForm: {
id: '',
industry: '家居行业',
tagType: '',
name: '',
detail: '',
remark: '',
enabled: true
},
tagTypeOptions: ["接待客户", "质检SOP", "客户画像"],
showTagTypeDropdown: false,
selectedTagItem: null,
showTagActionMenu: false
}
},
onLoad() {
this.fetchServiceStatusList();
},
methods: {
goBack() {
uni.navigateBack();
},
switchTab(tab) {
this.activeTab = tab;
if (tab === 'status') {
this.fetchServiceStatusList();
} else if (tab === 'tag') {
this.tagViewMode = 'list';
// 当进入标签管理页面时,请求接口填充数据
this.fetchTagList();
}
},
onServiceStatusSearch() {
if (this.activeTab !== 'status') {
this.activeTab = 'status';
}
this.serviceStatusPage.current = 1;
this.fetchServiceStatusList({ force: true });
},
onServiceStatusRefresh() {
this.serviceStatusPage.current = 1;
this.fetchServiceStatusList({ force: true });
},
onServiceStatusReachBottom() {
// 已在加载中或全部加载完成则不再触发
if (this.serviceStatusLoading) return;
if (this.serviceStatusList.length >= this.serviceStatusTotal) return;
this.serviceStatusPage.current += 1;
this.fetchServiceStatusList();
},
async fetchServiceStatusList({ force = false } = {}) {
if (this.serviceStatusLoading && !force) {
return;
}
this.serviceStatusLoading = true;
try {
const queryParams = {
current: this.serviceStatusPage.current,
size: this.serviceStatusPage.size,
syncStatus: '服务中'
};
const trimmedSalesName = this.serviceStatusQuery?.salesName?.trim();
const trimmedCustomerName = this.serviceStatusQuery?.customerName?.trim();
if (trimmedSalesName) {
queryParams.salesName = trimmedSalesName;
}
if (trimmedCustomerName) {
queryParams.customerName = trimmedCustomerName;
}
const queryString = Object.keys(queryParams)
.map(key => `${key}=${encodeURIComponent(queryParams[key])}`)
.join('&');
const url = `${getApiUrl('/api/audioManagement/list')}?${queryString}`;
const res = await uni.request({
url,
method: 'GET',
timeout: 10000
});
if (res.statusCode === 200 && res.data && res.data.success) {
const records = Array.isArray(res.data.data) ? res.data.data : [];
const mapped = records.map(item => this.buildServiceStatusItem(item));
// 第一页或强制刷新时重置列表,否则追加
if (this.serviceStatusPage.current === 1 || force) {
this.serviceStatusList = mapped;
} else {
this.serviceStatusList = this.serviceStatusList.concat(mapped);
}
this.serviceStatusTotal = Number(res.data.total) || 0;
} else {
this.serviceStatusList = [];
this.serviceStatusTotal = 0;
uni.showToast({
title: res.data?.message || '获取服务中列表失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取服务中列表失败:', error);
uni.showToast({
title: '获取服务状态失败,请稍后重试',
icon: 'none'
});
} finally {
this.serviceStatusLoading = false;
}
},
buildServiceStatusItem(record = {}) {
const formattedUpdateTime = this.formatDateTime(record.updateTime || record.createTime);
const tags = [];
if (record.recordingName) {
tags.push({ text: `录音:${record.recordingName}`, color: 'blue' });
}
if (record.intentionLevel) {
tags.push({ text: `意向:${record.intentionLevel}`, color: 'orange' });
}
if (record.projectName) {
tags.push({ text: `项目:${record.projectName}`, color: 'blue' });
}
return {
id: record.id,
staffName: record.salesName || '未分配销售',
status: record.syncStatus || '服务中',
customerName: record.customerName || '',
customerPhone: record.customerPhone || '',
tags,
durationText: formattedUpdateTime ? `最近更新时间:${formattedUpdateTime}` : '最近暂无更新时间'
};
},
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 hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
},
viewServiceDetail(item) {
// 查看服务详情
console.log('查看服务详情', item);
},
onGenderChange(e) {
this.receptionForm.gender = e.detail.value;
},
onContactBlur() {
const contact = this.receptionForm.contact?.trim();
if (!contact) {
return;
}
if (!this.isValidPhoneNumber(contact)) {
uni.showToast({
title: "请输入有效的手机号",
icon: "none"
});
return;
}
this.fetchCustomerByContact();
},
isValidPhoneNumber(phone) {
const normalizedPhone = phone?.trim();
if (!normalizedPhone) {
return false;
}
return /^1[3-9]\d{9}$/.test(normalizedPhone);
},
async fetchCustomerByContact() {
const contact = this.receptionForm.contact?.trim();
if (!contact || this.isFetchingContact) {
return;
}
if (!this.isValidPhoneNumber(contact)) {
return;
}
try {
this.isFetchingContact = true;
// 统一使用 getApiUrl根据 env.js 配置自动切换环境
const url = `${getApiUrl('/api/customerManagement/getByContact')}?contact=${encodeURIComponent(contact)}`;
const res = await uni.request({
url: url,
method: 'GET',
timeout: 8000
});
const list = res.data?.data;
if (res.statusCode === 200 && res.data?.success && Array.isArray(list) && list.length) {
const customer = list[0];
const retrievedId = customer.id || customer.customerId || "";
const resolvedSalesPhone = customer.salesPhone
|| customer.salesMobile
|| customer.salesTel
|| this.receptionForm.salesPhone
|| "";
this.receptionForm = {
...this.receptionForm,
id: retrievedId ? String(retrievedId) : this.receptionForm.id,
customerName: customer.customerName || this.receptionForm.customerName,
customerSource: customer.customerSource || this.receptionForm.customerSource,
dealershipId: customer.dealershipId || this.receptionForm.dealershipId,
dealershipName: customer.dealershipName || this.receptionForm.dealershipName,
salesId: customer.salesId || this.receptionForm.salesId,
salesName: customer.salesName || this.receptionForm.salesName,
salesPhone: String(resolvedSalesPhone),
recordingCount: customer.recordingCount ?? this.receptionForm.recordingCount,
intendedModel: customer.intendedModel || this.receptionForm.intendedModel,
infoCard: customer.infoCard || this.receptionForm.infoCard,
remark: customer.remark || this.receptionForm.remark,
detailedAddress: customer.detailedAddress || this.receptionForm.detailedAddress,
contactCount: customer.contactCount ?? this.receptionForm.contactCount
};
uni.showToast({
title: "已填充客户信息",
icon: "none"
});
}
// 未获取到数据时,不提示任何信息
} catch (error) {
// 查询失败时,也不提示任何信息
console.error("查询客户信息失败:", error);
} finally {
this.isFetchingContact = false;
}
},
onCancel() {
// 重置表单
this.receptionForm = {
id: "",
customerName: "",
contact: "",
customerSource: "",
gender: "女",
age: "",
dealershipId: "",
dealershipName: "",
salesId: "",
salesName: "",
salesPhone: "",
recordingCount: 0,
intendedModel: "",
infoCard: "",
remark: "",
detailedAddress: "",
contactCount: 0
};
uni.showToast({
title: "已取消",
icon: "none"
});
},
async onSave(action = 'save') {
// 表单验证
if (!this.receptionForm.customerName) {
uni.showToast({
title: "请输入客户姓名",
icon: "none"
});
return;
}
if (!this.receptionForm.contact) {
uni.showToast({
title: "请输入客户电话",
icon: "none"
});
return;
}
const trimmedContact = this.receptionForm.contact?.trim();
if (!this.isValidPhoneNumber(trimmedContact)) {
uni.showToast({
title: "请输入有效的客户电话",
icon: "none"
});
return;
}
const trimmedSalesPhone = this.receptionForm.salesPhone?.trim();
if (trimmedSalesPhone && !this.isValidPhoneNumber(trimmedSalesPhone)) {
uni.showToast({
title: "请输入有效的销售电话",
icon: "none"
});
return;
}
// 构建请求参数
const normalizedCustomerId = this.receptionForm.id?.toString().trim() || "";
const params = {
id: normalizedCustomerId,
customerName: this.receptionForm.customerName?.trim(),
contact: trimmedContact,
customerSource: this.receptionForm.customerSource?.trim() || "",
dealershipId: this.receptionForm.dealershipId?.trim() || "",
dealershipName: this.receptionForm.dealershipName?.trim() || "",
salesId: this.receptionForm.salesId?.trim() || "",
salesName: this.receptionForm.salesName?.trim() || "",
recordingCount: Number(this.receptionForm.recordingCount) || 0,
intendedModel: this.receptionForm.intendedModel?.trim() || "",
infoCard: this.receptionForm.infoCard?.trim() || "",
remark: this.receptionForm.remark?.trim() || "",
detailedAddress: this.receptionForm.detailedAddress?.trim() || "",
salesPhone: trimmedSalesPhone || "",
contactCount: Number(this.receptionForm.contactCount) || 0,
operationType: action
};
try {
uni.showLoading({
title: "保存中..."
});
const res = await uni.request({
url: getApiUrl('/api/customerManagement/add'),
method: "POST",
data: params,
timeout: 10000
});
uni.hideLoading();
if (res.statusCode === 200 && res.data && res.data.success) {
uni.showToast({
title: "保存成功",
icon: "success"
});
// 保存成功后重置表单
this.onCancel();
} else {
uni.showToast({
title: res.data?.message || "保存失败",
icon: "none"
});
}
} catch (error) {
uni.hideLoading();
console.error("保存失败:", error);
uni.showToast({
title: "保存失败,请重试",
icon: "none"
});
}
},
// 标签管理相关方法
onTagListSearch() {
if (this.activeTab !== 'tag') {
this.activeTab = 'tag';
}
this.tagPage.current = 1;
this.fetchTagList({ force: true });
},
onTagListRefresh() {
this.tagPage.current = 1;
this.fetchTagList({ force: true });
},
onTagListReachBottom() {
// 已在加载中或全部加载完成则不再触发
if (this.tagLoading) return;
if (this.tagList.length >= this.tagTotal) return;
this.tagPage.current += 1;
this.fetchTagList();
},
async fetchTagList({ force = false } = {}) {
if (this.tagLoading && !force) {
return;
}
this.tagLoading = true;
try {
const queryParams = {
current: this.tagPage.current,
size: this.tagPage.size
};
const trimmedTagType = this.tagQuery?.tagType?.trim();
const trimmedTagName = this.tagQuery?.tagName?.trim();
if (trimmedTagType) {
queryParams.tagType = trimmedTagType;
}
if (trimmedTagName) {
queryParams.tagName = trimmedTagName;
}
const queryString = Object.keys(queryParams)
.map(key => `${key}=${encodeURIComponent(queryParams[key])}`)
.join('&');
const url = `${getApiUrl('/api/industryTags/list')}?${queryString}`;
const res = await uni.request({
url,
method: 'GET',
timeout: 10000
});
if (res.statusCode === 200 && res.data && res.data.success) {
const records = Array.isArray(res.data.data) ? res.data.data : [];
// 第一页或强制刷新时重置列表,否则追加
if (this.tagPage.current === 1 || force) {
this.tagList = records;
} else {
this.tagList = this.tagList.concat(records);
}
this.tagTotal = Number(res.data.total) || 0;
} else {
this.tagList = [];
this.tagTotal = 0;
uni.showToast({
title: res.data?.message || '获取标签列表失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取标签列表失败:', error);
uni.showToast({
title: '获取标签列表失败,请稍后重试',
icon: 'none'
});
} finally {
this.tagLoading = false;
}
},
showAddTag() {
this.tagViewMode = 'add';
this.showTagTypeDropdown = false;
this.tagForm = {
id: '',
industry: '家居行业',
tagType: '',
name: '',
detail: '',
remark: '',
enabled: true
};
},
onTagItemClick(item) {
// 点击卡片内容区域,不做任何操作(保留用于其他可能的交互)
},
onTagActionBtnClick(item) {
// 如果已经选中当前项,则关闭菜单
if (this.selectedTagItem && this.selectedTagItem.id === item.id && this.showTagActionMenu) {
this.closeTagActionMenu();
return;
}
// 显示菜单
this.selectedTagItem = item;
this.showTagActionMenu = true;
},
closeTagActionMenu() {
this.showTagActionMenu = false;
this.selectedTagItem = null;
},
editTag(item) {
this.closeTagActionMenu();
this.tagViewMode = 'edit';
this.showTagTypeDropdown = false;
this.tagForm = {
id: item.id || '',
industry: item.industry || '家居行业',
tagType: item.tagType || '',
name: item.tagName || item.name || '',
detail: item.tagDetail || item.detail || '',
remark: item.remark || '',
enabled: item.enabled !== undefined ? item.enabled : (item.isEnabled !== undefined ? item.isEnabled : true)
};
},
async deleteTag(item) {
this.closeTagActionMenu();
const tagId = item.id;
if (!tagId) {
uni.showToast({
title: '无法获取标签ID',
icon: 'none'
});
return;
}
uni.showModal({
title: '确认删除',
content: `确定要删除标签"${item.tagName || item.name || '未命名标签'}"吗?`,
success: async (res) => {
if (res.confirm) {
try {
uni.showLoading({
title: '删除中...'
});
const deleteUrl = getApiUrl(`/api/industryTags/delete/${tagId}`);
const deleteRes = await uni.request({
url: deleteUrl,
method: 'DELETE',
timeout: 10000
});
uni.hideLoading();
if (deleteRes.statusCode === 200 && deleteRes.data && deleteRes.data.success) {
uni.showToast({
title: deleteRes.data?.message || '删除成功',
icon: 'success'
});
// 删除成功后刷新列表
this.tagPage.current = 1;
this.fetchTagList({ force: true });
} else {
uni.showToast({
title: deleteRes.data?.message || '删除失败',
icon: 'none'
});
}
} catch (error) {
uni.hideLoading();
console.error('删除标签失败:', error);
uni.showToast({
title: '删除失败,请重试',
icon: 'none'
});
}
}
}
});
},
cancelAddTag() {
this.tagViewMode = 'list';
this.showTagTypeDropdown = false;
this.tagForm = {
id: '',
industry: '家居行业',
tagType: '',
name: '',
detail: '',
remark: '',
enabled: true
};
},
toggleTagTypeDropdown() {
this.showTagTypeDropdown = !this.showTagTypeDropdown;
},
selectTagType(option) {
this.tagForm.tagType = option;
this.showTagTypeDropdown = false;
},
closeTagTypeDropdown() {
this.showTagTypeDropdown = false;
},
onEnabledChange(e) {
this.tagForm.enabled = e.detail.value;
},
async saveTag() {
// 表单验证
if (!this.tagForm.name || !this.tagForm.name.trim()) {
uni.showToast({
title: "请输入标签名",
icon: "none"
});
return;
}
try {
uni.showLoading({
title: this.tagViewMode === 'edit' ? "更新中..." : "保存中..."
});
const payload = {
industry: this.tagForm.industry?.trim() || '',
tagType: this.tagForm.tagType?.trim() || '',
tagName: this.tagForm.name.trim(),
tagDetail: this.tagForm.detail?.trim() || '',
remark: this.tagForm.remark?.trim() || '',
enabled: this.tagForm.enabled !== undefined ? this.tagForm.enabled : true
};
// 如果是编辑模式添加id
if (this.tagViewMode === 'edit' && this.tagForm.id) {
payload.id = this.tagForm.id;
}
// 根据模式选择不同的接口
const url = this.tagViewMode === 'edit'
? getApiUrl('/api/industryTags/update')
: getApiUrl('/api/industryTags/add');
const method = this.tagViewMode === 'edit' ? 'PUT' : 'POST';
const res = await uni.request({
url: url,
method: method,
data: payload,
header: {
"Content-Type": "application/json"
},
timeout: 10000
});
const { statusCode, data } = res;
if (statusCode === 200 && data && (data.success || data.code === 200)) {
uni.showToast({
title: data?.message || (this.tagViewMode === 'edit' ? "更新成功" : "保存成功"),
icon: "success"
});
// 保存成功后返回列表并刷新
this.tagViewMode = 'list';
this.tagPage.current = 1;
this.fetchTagList({ force: true });
} else {
throw new Error(data?.message || (this.tagViewMode === 'edit' ? "更新失败" : "保存失败"));
}
} catch (error) {
console.error("保存标签失败:", error);
uni.showToast({
title: error?.message || (this.tagViewMode === 'edit' ? "更新失败,请重试" : "保存失败,请重试"),
icon: "none"
});
} finally {
uni.hideLoading();
}
}
}
}
</script>
<style>
page {
background-color: #F5F5F5;
}
.page {
min-height: 100vh;
background-color: #F5F5F5;
}
.content {
padding-top: 88rpx;
}
/* AI功能横幅 */
.ai-banner {
background-color: #E3F2FD;
padding: 24rpx 32rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.banner-text {
font-size: 28rpx;
color: #1976D2;
flex: 1;
}
.banner-btn {
font-size: 28rpx;
color: #1976D2;
font-weight: 500;
margin-left: 16rpx;
}
/* 标签页 */
.tabs {
display: flex;
background-color: #FFFFFF;
border-bottom: 1px solid #E0E0E0;
padding: 0 32rpx;
}
.tab-item {
padding: 24rpx 32rpx;
position: relative;
}
.tab-item text {
font-size: 30rpx;
color: #666;
}
.tab-item.active text {
color: #333;
font-weight: 500;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 32rpx;
right: 32rpx;
height: 4rpx;
background-color: #007AFF;
border-radius: 2rpx;
}
/* 服务状态列表 */
.service-status-list {
height: calc(100vh - 300rpx);
background-color: #F5F5F5;
padding: 24rpx 32rpx;
}
.service-status-toolbar {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 16rpx;
padding: 16rpx 24rpx;
margin-bottom: 24rpx;
background-color: #F8F8FA;
border-radius: 16rpx;
border: 1px solid #EFEFF2;
}
.toolbar-total {
font-size: 26rpx;
color: #666;
margin-right: 16rpx;
}
.toolbar-input {
flex: 1;
min-width: 150rpx;
background-color: #F5F6FA;
border-radius: 12rpx;
padding: 16rpx 24rpx;
font-size: 28rpx;
border: 1px solid transparent;
}
.toolbar-actions {
display: flex;
align-items: center;
margin-left: auto;
}
.toolbar-btn {
padding: 0 16rpx;
height: 72rpx;
color: #2A68FF;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
font-size: 28rpx;
font-weight: 400;
}
.service-card {
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
box-sizing: border-box;
width: 100%;
overflow: visible;
/* 确保左右padding完全对称所有内容都在padding范围内 */
}
/* 标签卡片特殊处理 */
.service-card.tag-card-item {
padding-left: 32rpx;
padding-right: 32rpx;
padding-top: 32rpx;
padding-bottom: 32rpx;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
width: 100%;
box-sizing: border-box;
padding: 0;
margin: 0;
/* 确保头部内容在卡片padding范围内 */
}
.staff-info {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
margin: 0;
padding: 0;
}
.staff-avatar {
width: 64rpx;
height: 64rpx;
background-color: #2196F3;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
margin-left: 0;
margin-top: 0;
margin-bottom: 0;
flex-shrink: 0;
}
.avatar-text {
font-size: 32rpx;
color: #FFFFFF;
font-weight: 500;
}
.staff-name {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
.service-status {
display: flex;
align-items: center;
}
.service-status text {
font-size: 26rpx;
color: #007AFF;
margin-left: 8rpx;
}
.customer-tags {
display: flex;
flex-wrap: wrap;
margin-bottom: 20rpx;
gap: 12rpx;
box-sizing: border-box;
margin-left: 0;
margin-right: 0;
padding: 0;
/* 确保标签容器不会超出卡片padding范围 */
width: 100%;
max-width: 100%;
}
.customer-info {
margin-bottom: 16rpx;
display: flex;
flex-direction: column;
gap: 8rpx;
box-sizing: border-box;
margin-left: 0;
margin-right: 0;
padding: 0;
/* 确保信息容器不会超出卡片padding范围 */
width: 100%;
max-width: 100%;
}
.customer-name,
.customer-phone {
font-size: 28rpx;
color: #555;
}
.tag-item {
padding: 8rpx 16rpx;
border-radius: 8rpx;
box-sizing: border-box;
word-wrap: break-word;
word-break: break-all;
/* 确保标签项不会超出卡片padding范围允许换行 */
display: inline-block;
max-width: 100%;
}
.tag-blue {
background-color: #E3F2FD;
}
.tag-blue text {
font-size: 24rpx;
color: #1976D2;
}
.tag-orange {
background-color: #FFF3E0;
}
.tag-orange text {
font-size: 24rpx;
color: #F57C00;
}
/* AI提醒 */
.ai-alert {
border-radius: 8rpx;
padding: 20rpx;
margin-bottom: 20rpx;
}
.alert-risk-box {
background-color: #FFF5F5;
}
.alert-reminder-box {
background-color: #F5F5F5;
}
.alert-header {
display: flex;
align-items: center;
margin-bottom: 12rpx;
}
.alert-icon {
margin-right: 8rpx;
}
.icon-circle {
width: 24rpx;
height: 24rpx;
border: 2rpx solid #333;
border-radius: 50%;
position: relative;
}
.icon-circle::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 8rpx;
height: 8rpx;
background-color: #333;
border-radius: 50%;
}
.alert-title {
font-size: 28rpx;
font-weight: 500;
color: #333;
}
.alert-content {
font-size: 26rpx;
line-height: 1.6;
}
.alert-risk-text {
color: #FF5722;
}
.alert-reminder-text {
color: #666;
}
.alert-list {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.alert-item {
display: flex;
align-items: flex-start;
}
.alert-item::before {
content: '•';
margin-right: 8rpx;
color: #666;
}
.alert-item text {
font-size: 26rpx;
color: #666;
line-height: 1.6;
}
.service-duration {
padding-top: 16rpx;
border-top: 1px solid #F0F0F0;
}
.service-duration text {
font-size: 26rpx;
color: #999;
}
.service-status-empty {
padding: 48rpx 0;
text-align: center;
color: #999;
font-size: 28rpx;
}
/* 开始接待表单样式 */
.reception-form {
height: calc(100vh - 300rpx);
background-color: #F5F5F5;
padding: 24rpx 32rpx;
}
.form-card {
background-color: #ffffff;
border-radius: 16rpx;
padding: 32rpx 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
margin-bottom: 32rpx;
}
.form-item {
display: flex;
align-items: center;
margin-bottom: 32rpx;
}
.gender-radio-group {
flex: 1;
display: flex;
gap: 32rpx;
flex-wrap: wrap;
}
.gender-radio {
display: flex;
align-items: center;
gap: 12rpx;
}
.gender-radio__text {
color: #374151;
font-size: 28rpx;
}
.form-item:last-child {
margin-bottom: 0;
}
.form-item__label {
font-size: 28rpx;
color: #333;
font-weight: 500;
width: 160rpx;
flex-shrink: 0;
}
.form-item--half {
width: 50%;
}
.form-item__input {
flex: 1;
height: 88rpx;
background-color: #f9fafb;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
}
.form-item__text {
flex: 1;
height: 88rpx;
background-color: #f9fafb;
border-radius: 12rpx;
padding: 0 24rpx;
display: flex;
align-items: center;
box-sizing: border-box;
}
.form-item__text text {
font-size: 28rpx;
color: #333;
}
.form-item__picker {
flex: 1;
height: 88rpx;
background-color: #f9fafb;
border-radius: 12rpx;
padding: 0 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
}
.form-item__picker-text {
font-size: 28rpx;
color: #333;
}
.form-item__picker-placeholder {
font-size: 28rpx;
color: #9ca3af;
}
.form-item__picker-icon {
font-size: 24rpx;
color: #9ca3af;
}
.tag-select-wrapper {
position: relative;
width: 75%;
}
.tag-select {
width: 100%;
height: 88rpx;
background-color: #f9fafb;
border-radius: 12rpx;
padding: 0 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
border: 1px solid transparent;
}
.tag-select-dropdown {
position: absolute;
top: 100%;
left: 160rpx;
right: 0;
background-color: #fff;
border-radius: 16rpx;
box-shadow: 0 12rpx 30rpx rgba(15, 23, 42, 0.1);
margin-top: 8rpx;
z-index: 11;
padding: 12rpx 0;
}
.tag-select-dropdown__item {
padding: 20rpx 32rpx;
}
.tag-select-dropdown__item text {
font-size: 28rpx;
color: #374151;
}
.tag-select-dropdown__item text.active {
color: #2A68FF;
font-weight: 500;
}
.tag-select-dropdown__item:active {
background-color: #f5f7fb;
}
.tag-select-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: transparent;
z-index: 10;
}
.form-actions {
display: flex;
gap: 24rpx;
padding-bottom: 40rpx;
}
.form-actions--triple {
gap: 16rpx;
}
.form-btn {
flex: 1;
height: 88rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
}
.form-btn--cancel {
background-color: #f3f4f6;
}
.form-btn--save {
background-color: #007AFF;
}
.form-btn--start {
background-color: #10B981;
}
.form-btn__text {
font-size: 32rpx;
font-weight: 500;
}
.form-btn--cancel .form-btn__text {
color: #6b7280;
}
.form-btn--save .form-btn__text {
color: #ffffff;
}
/* 标签管理样式 - 添加表单 */
.tag-management {
height: calc(100vh - 300rpx);
background-color: #F5F5F5;
}
/* 标签卡片操作菜单 */
.tag-card-item {
position: relative;
}
.tag-card-action-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin-left: auto;
margin-right: 0;
margin-top: 0;
margin-bottom: 0;
flex-shrink: 0;
flex-grow: 0;
position: relative;
padding: 0;
/* 确保按钮在卡片padding范围内距离右边框32rpx */
}
.tag-card-action-btn uni-icons {
display: block;
margin: 0;
padding: 0;
line-height: 1;
}
.tag-card-action-btn:active {
opacity: 0.7;
}
.tag-action-menu {
position: absolute;
top: 60rpx;
right: 32rpx;
width: 160rpx;
background-color: #FFFFFF;
border-radius: 12rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
z-index: 100;
overflow: hidden;
margin-right: 0;
}
.tag-action-menu-item {
padding: 24rpx 32rpx;
font-size: 28rpx;
color: #333;
text-align: center;
background-color: #FFFFFF;
}
.tag-action-menu-item:active {
background-color: #F5F5F5;
}
.tag-action-menu-item--danger {
color: #FF5722;
}
.tag-action-menu-divider {
height: 1rpx;
background-color: #E0E0E0;
margin: 0 16rpx;
}
.tag-action-menu-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: transparent;
z-index: 99;
}
.tag-form {
height: 100%;
padding: 24rpx 32rpx;
box-sizing: border-box;
}
.form-item__textarea {
flex: 1;
min-height: 200rpx;
background-color: #f9fafb;
border-radius: 12rpx;
padding: 24rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
line-height: 1.6;
}
.form-item__textarea--detail {
min-height: 100rpx;
height: 100rpx;
}
.form-item__textarea--remark {
min-height: 88rpx;
height: 88rpx;
}
/* 开关样式 */
.form-item switch {
margin-left: auto;
}
</style>