客户接待主包的一些代码移动到子包内

This commit is contained in:
zhonghua.li
2026-04-15 08:32:13 +08:00
parent a15d539f59
commit b37b9c6ada
15 changed files with 2992 additions and 6639 deletions

View File

@@ -0,0 +1,894 @@
<template>
<view class="reception-form-wrapper">
<scroll-view class="reception-form" scroll-y enable-back-to-top>
<view class="form-card">
<view class="form-item">
<text class="form-item__label">服务次数</text>
<view class="form-item__text">
<text>{{ formData.recordingCount || 0 }}</text>
</view>
</view>
<view class="form-item">
<text class="form-item__label">客户姓名</text>
<input
class="form-item__input"
v-model="formData.customerName"
placeholder="请输入客户姓名"
placeholder-style="color: #9ca3af"
/>
</view>
<view class="form-item">
<text class="form-item__label">客户电话</text>
<input
class="form-item__input"
v-model="formData.contact"
@blur="onContactBlur"
type="number"
placeholder="请输入客户电话"
placeholder-style="color: #9ca3af"
/>
</view>
<view class="form-item">
<text class="form-item__label">客户来源</text>
<view class="customer-source-select-wrapper">
<view
ref="customerSourceSelect"
class="customer-source-select"
@click.stop="toggleCustomerSourceDropdown"
>
<text
:class="formData.customerSource ? 'form-item__picker-text' : 'form-item__picker-placeholder'"
>
{{ formData.customerSource || "请选择客户来源" }}
</text>
<uni-icons type="bottom" size="16" color="#9ca3af"></uni-icons>
</view>
<view
ref="customerSourceDropdown"
v-if="showCustomerSourceDropdown"
class="customer-source-select-dropdown"
@click.stop
>
<view
class="customer-source-select-dropdown__item"
v-for="option in customerSourceOptions"
:key="option"
@click="selectCustomerSource(option)"
>
<text :class="{'active': option === formData.customerSource}">
{{ option }}
</text>
</view>
</view>
</view>
</view>
<view class="form-item">
<text class="form-item__label">门店名称</text>
<input
class="form-item__input"
v-model="formData.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="formData.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="formData.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="formData.salesName"
placeholder="请输入销售姓名"
placeholder-style="color: #9ca3af"
/>
</view>
<view class="form-item">
<text class="form-item__label">销售电话</text>
<input
class="form-item__input"
v-model="formData.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="formData.remarks"
placeholder="请输入详细住址"
placeholder-style="color: #9ca3af"
/>
</view>
</view>
</scroll-view>
<view class="form-actions form-actions--triple">
<view class="form-btn form-btn--cancel" @click="handleCancel">
<view class="form-btn__icon-circle">
<uni-icons type="close" size="16" color="#007AFF"></uni-icons>
</view>
<text class="form-btn__text">取消</text>
</view>
<view class="form-btn form-btn--save" @click="handleSave('save')">
<view class="form-btn__icon-circle">
<uni-icons type="checkmarkempty" size="16" color="#007AFF"></uni-icons>
</view>
<text class="form-btn__text">保存</text>
</view>
<view class="form-btn form-btn--start" @click="handleSave('start')">
<view class="form-btn__icon-circle">
<uni-icons type="checkmarkempty" size="16" color="#007AFF"></uni-icons>
</view>
<text class="form-btn__text">开始接待</text>
</view>
</view>
<!-- 遮罩层点击关闭下拉框 -->
<view
v-if="showCustomerSourceDropdown"
class="customer-source-select-mask"
@click="closeCustomerSourceDropdown"
></view>
</view>
</template>
<script>
import { getApiUrl } from "@/common/config.js";
export default {
name: 'ReceptionForm',
props: {
// 表单初始数据
initialData: {
type: Object,
default: () => ({})
}
},
data() {
return {
genderOptions: ["男", "女"],
customerSourceOptions: ["自然到店", "网络媒体", "其他类型"],
showCustomerSourceDropdown: false,
isFetchingContact: false,
formData: {
id: "",
customerName: "",
contact: "",
customerSource: "",
gender: "女",
age: "",
dealershipId: "",
dealershipName: "",
salesId: "",
salesName: "",
salesPhone: "",
recordingCount: 0,
intendedModel: "",
infoCard: "",
remark: "",
detailedAddress: "",
contactCount: 0
}
}
},
watch: {
initialData: {
handler(newVal) {
if (newVal && Object.keys(newVal).length > 0) {
this.formData = { ...this.formData, ...newVal };
}
},
immediate: true,
deep: true
}
},
mounted() {
// 加载当前登录用户信息,填充销售姓名和电话
this.loadCurrentUserInfo();
// 监听窗口大小变化,重新定位下拉框
uni.onWindowResize(() => {
if (this.showCustomerSourceDropdown) {
this.updateDropdownPosition();
}
});
},
methods: {
/**
* 加载当前登录用户信息,填充销售姓名和电话
*/
loadCurrentUserInfo() {
try {
// 从本地存储获取登录响应信息
const loginResponse = uni.getStorageSync('backend-login-response') || {};
// 填充销售姓名
if (loginResponse.userName) {
this.formData.salesName = loginResponse.userName;
}
// 填充销售电话:如果电话为空,则使用姓名作为电话
if (loginResponse.phone) {
this.formData.salesPhone = loginResponse.phone;
} else if (loginResponse.userName) {
// 如果电话为空,使用姓名作为电话
this.formData.salesPhone = loginResponse.userName;
}
// 如果有 userId也可以填充到 salesId
if (loginResponse.userId) {
this.formData.salesId = String(loginResponse.userId);
}
} catch (e) {
console.error('加载当前登录用户信息失败:', e);
}
},
onGenderChange(e) {
this.formData.gender = e.detail.value;
},
toggleCustomerSourceDropdown() {
this.showCustomerSourceDropdown = !this.showCustomerSourceDropdown;
if (this.showCustomerSourceDropdown) {
this.$nextTick(() => {
this.updateDropdownPosition();
});
}
},
updateDropdownPosition() {
const selectEl = this.$refs.customerSourceSelect;
if (selectEl) {
const rect = selectEl.getBoundingClientRect();
const dropdownEl = this.$refs.customerSourceDropdown;
if (dropdownEl) {
dropdownEl.style.top = (rect.bottom + 8) + 'px';
dropdownEl.style.left = rect.left + 'px';
dropdownEl.style.width = rect.width + 'px';
}
}
},
selectCustomerSource(option) {
this.formData.customerSource = option;
this.showCustomerSourceDropdown = false;
},
closeCustomerSourceDropdown() {
this.showCustomerSourceDropdown = false;
},
onContactBlur() {
const contact = this.formData.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.formData.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)}`;
// 获取认证信息
let tenantId = '';
let token = '';
try {
tenantId = uni.getStorageSync('backend-tenant-id') || '';
token = uni.getStorageSync('backend-token') || '';
} catch (e) {
console.error('获取认证信息失败:', e);
}
// 构建请求头
const headers = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
if (tenantId) {
headers['X-Tenant-Id'] = tenantId;
}
const res = await uni.request({
url: url,
method: 'GET',
header: headers,
timeout: 30000 // 增加超时时间到30秒
});
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.formData.salesPhone
|| "";
this.formData = {
...this.formData,
id: retrievedId ? String(retrievedId) : this.formData.id,
customerName: customer.customerName || this.formData.customerName,
customerSource: customer.customerSource || this.formData.customerSource,
dealershipId: customer.dealershipId ? String(customer.dealershipId) : this.formData.dealershipId,
dealershipName: customer.dealershipName || this.formData.dealershipName,
salesId: customer.salesId ? String(customer.salesId) : this.formData.salesId,
salesName: customer.salesName || this.formData.salesName,
salesPhone: String(resolvedSalesPhone),
recordingCount: Number(customer.recordingCount) || 0,
intendedModel: customer.intendedModel || this.formData.intendedModel,
infoCard: customer.infoCard || this.formData.infoCard,
remark: customer.remark || this.formData.remark,
detailedAddress: customer.detailedAddress || this.formData.detailedAddress,
contactCount: customer.contactCount ?? this.formData.contactCount
};
uni.showToast({
title: "已填充客户信息",
icon: "none"
});
}
// 未获取到数据时,不提示任何信息
} catch (error) {
// 查询失败时,也不提示任何信息
console.error("查询客户信息失败:", error);
} finally {
this.isFetchingContact = false;
}
},
handleCancel() {
// 重置表单
this.formData = {
id: "",
customerName: "",
contact: "",
customerSource: "",
gender: "女",
age: "",
dealershipId: "",
dealershipName: "",
salesId: "",
salesName: "",
salesPhone: "",
recordingCount: 0,
intendedModel: "",
infoCard: "",
remark: "",
detailedAddress: "",
contactCount: 0
};
// 重置后重新填充当前登录用户信息
this.loadCurrentUserInfo();
// 触发取消事件
this.$emit('cancel');
uni.showToast({
title: "已取消",
icon: "none"
});
},
async handleSave(action = 'save') {
// 表单验证
// 客户电话格式验证(如果填写了电话,则验证格式)
const trimmedContact = this.formData.contact?.trim();
if (trimmedContact && !this.isValidPhoneNumber(trimmedContact)) {
uni.showToast({
title: "请输入有效的客户电话",
icon: "none"
});
return;
}
const trimmedSalesPhone = this.formData.salesPhone?.trim();
if (trimmedSalesPhone && !this.isValidPhoneNumber(trimmedSalesPhone)) {
uni.showToast({
title: "请输入有效的销售电话",
icon: "none"
});
return;
}
// 构建请求参数
const normalizedCustomerId = this.formData.id?.toString().trim() || "";
const params = {
id: normalizedCustomerId,
customerName: this.formData.customerName?.trim(),
contact: trimmedContact,
customerSource: this.formData.customerSource?.trim() || "",
dealershipId: String(this.formData.dealershipId || '').trim() || "",
dealershipName: this.formData.dealershipName?.trim() || "",
salesId: String(this.formData.salesId || '').trim() || "",
salesName: this.formData.salesName?.trim() || "",
recordingCount: Number(this.formData.recordingCount) || 0,
intendedModel: this.formData.intendedModel?.trim() || "",
infoCard: this.formData.infoCard?.trim() || "",
remark: this.formData.remark?.trim() || "",
detailedAddress: this.formData.detailedAddress?.trim() || "",
salesPhone: trimmedSalesPhone || "",
contactCount: Number(this.formData.contactCount) || 0,
operationType: action,
scenario: "furniture"
};
try {
uni.showLoading({
title: "保存中..."
});
// 获取认证信息
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;
}
const res = await uni.request({
url: getApiUrl('/api/customerManagement/add'),
method: "POST",
data: params,
header: headers,
timeout: 30000 // 增加超时时间到30秒
});
uni.hideLoading();
if (res.statusCode === 200 && res.data && res.data.success) {
uni.showToast({
title: "保存成功",
icon: "success"
});
// 保存成功后重置表单
this.handleCancel();
// 触发保存成功事件
this.$emit('save-success', { action, data: params });
} else {
uni.showToast({
title: res.data?.message || "保存失败",
icon: "none"
});
// 触发保存失败事件
this.$emit('save-error', { action, error: res.data?.message || "保存失败" });
}
} catch (error) {
uni.hideLoading();
console.error("保存失败:", error);
uni.showToast({
title: "保存失败,请重试",
icon: "none"
});
// 触发保存失败事件
this.$emit('save-error', { action, error: error.message || "保存失败,请重试" });
}
}
}
}
</script>
<style>
.reception-form-wrapper {
/* 使用绝对定位从tab页底部开始高度计算100vh - 导航栏(88rpx) - tab页(72rpx) - 底部导航栏(96rpx) */
position: absolute;
top: 160rpx; /* 导航栏(88rpx) + tab页(72rpx) = 160rpx */
left: 0;
right: 0;
bottom: 96rpx; /* 底部导航栏高度 */
background-color: #F5F5F5;
box-sizing: border-box;
width: 100%;
z-index: 0; /* 确保在标签页下方,但可以显示内容 */
display: flex;
flex-direction: column;
overflow: hidden; /* 防止内容溢出 */
}
.reception-form {
flex: 1;
width: 100%;
padding: 24rpx 16rpx;
box-sizing: border-box;
background-color: #F5F5F5;
overflow-y: auto;
/* 确保可以滚动 */
-webkit-overflow-scrolling: touch;
}
.form-card {
width: 100%;
box-sizing: border-box;
background-color: #ffffff;
border-radius: 16rpx;
padding: 40rpx 24rpx 32rpx 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
margin-bottom: 24rpx;
overflow: visible; /* 确保内容不被裁剪 */
}
.form-card .form-item:last-child {
margin-bottom: 0;
}
/* 第一个表单项特殊处理,确保顶部空间 */
.form-card .form-item:first-child {
padding-top: 24rpx;
margin-top: 16rpx;
}
.form-item {
display: flex !important;
align-items: flex-start;
margin-bottom: 32rpx;
min-height: 88rpx;
padding-top: 8rpx;
box-sizing: border-box;
width: 100%;
flex-wrap: nowrap !important;
overflow: hidden;
flex-shrink: 0;
justify-content: center;
flex-direction: row !important;
padding-left: 0;
}
/* 对于包含 textarea 的表单项label 顶部对齐 */
.form-item--textarea {
align-items: flex-start;
}
.gender-radio-group {
flex: 1;
flex-shrink: 1;
display: flex;
gap: 32rpx;
flex-wrap: nowrap;
align-items: center;
min-width: 0;
overflow: hidden;
height: 88rpx;
justify-content: flex-start;
}
.gender-radio {
display: flex;
align-items: center;
gap: 12rpx;
flex-shrink: 0;
height: 88rpx;
}
.gender-radio__text {
color: #374151;
font-size: 28rpx;
white-space: nowrap;
}
.form-item__label {
font-size: 28rpx;
color: #333;
font-weight: 500;
width: 140rpx;
flex-shrink: 0;
flex-grow: 0;
margin-right: 24rpx;
margin-left: 0;
padding: 12rpx 0 12rpx 16rpx;
white-space: nowrap;
box-sizing: border-box;
min-height: 88rpx;
line-height: 1.4;
text-align: left;
display: flex;
align-items: flex-start;
justify-content: flex-start;
/* 确保标签可见且不被遮挡 */
}
.form-item--half {
width: 50%;
}
.form-item__input {
flex: 1 !important;
flex-shrink: 1 !important;
flex-grow: 1 !important;
min-width: 0 !important;
max-width: none !important;
width: auto !important;
height: 88rpx;
line-height: 88rpx;
background-color: #f9fafb;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
border: 2rpx solid #e5e7eb;
transition: all 0.2s ease;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.form-item__input:focus {
border-color: #007AFF;
background-color: #FFFFFF;
box-shadow: 0 0 0 4rpx rgba(0, 122, 255, 0.1);
}
.form-item__text {
flex: 1;
flex-shrink: 1;
min-width: 0;
height: 88rpx;
background-color: #f9fafb;
border-radius: 12rpx;
padding: 0 24rpx;
display: flex;
align-items: center;
justify-content: flex-start;
box-sizing: border-box;
overflow: hidden;
}
.form-item__text text {
font-size: 28rpx;
color: #333;
}
.form-actions {
display: flex;
gap: 16rpx;
padding: 24rpx 16rpx;
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
background-color: #F5F5F5;
box-sizing: border-box;
flex-shrink: 0;
width: 100%;
border-top: 1px solid #E5E7EB;
}
.form-actions--triple {
gap: 16rpx;
}
.form-btn {
flex: 1;
height: 88rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
transition: all 0.2s ease;
background-color: #f8f9fa;
border: 1px solid #e5e7eb;
box-sizing: border-box;
}
.form-btn--cancel:active,
.form-btn--save:active,
.form-btn--start:active {
background-color: #f0f0f0;
border-color: #d1d5db;
opacity: 0.9;
}
.form-btn__icon-circle {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 122, 255, 0.08);
flex-shrink: 0;
}
.form-btn__text {
font-size: 30rpx;
font-weight: 500;
color: #007AFF;
}
.form-btn--cancel .form-btn__text,
.form-btn--save .form-btn__text,
.form-btn--start .form-btn__text {
color: #007AFF;
}
/* 客户来源下拉框样式 */
.customer-source-select-wrapper {
position: relative;
flex: 1;
flex-shrink: 1;
min-width: 0;
display: flex;
align-items: center;
overflow: hidden;
height: 88rpx;
}
.customer-source-select {
width: 100%;
min-width: 0;
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;
overflow: hidden;
}
.customer-source-select uni-icons {
flex-shrink: 0;
margin-left: 12rpx;
}
.customer-source-select-dropdown {
position: fixed;
background-color: #fff;
border-radius: 16rpx;
box-shadow: 0 12rpx 30rpx rgba(15, 23, 42, 0.1);
z-index: 9999;
padding: 12rpx 0;
box-sizing: border-box;
max-height: 400rpx;
overflow-y: auto;
min-width: 200rpx;
}
.customer-source-select-dropdown__item {
padding: 20rpx 32rpx;
}
.customer-source-select-dropdown__item text {
font-size: 28rpx;
color: #374151;
}
.customer-source-select-dropdown__item text.active {
color: #2A68FF;
font-weight: 500;
}
.customer-source-select-dropdown__item:active {
background-color: #f5f7fb;
}
.customer-source-select-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: transparent;
z-index: 9998;
}
.form-item__picker-text {
font-size: 28rpx;
color: #333;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.form-item__picker-placeholder {
font-size: 28rpx;
color: #9ca3af;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 小屏幕适配 */
@media (max-width: 750rpx) {
.form-item__label {
width: 120rpx;
font-size: 26rpx;
margin-right: 16rpx;
margin-left: 0;
padding: 10rpx 0 10rpx 12rpx;
min-height: 80rpx;
line-height: 1.4;
align-items: flex-start;
}
.form-item {
min-height: 80rpx;
padding-top: 6rpx;
}
.form-item__input,
.form-item__text,
.customer-source-select-wrapper,
.gender-radio-group,
.gender-radio {
height: 80rpx;
}
.form-item__input,
.form-item__text,
.customer-source-select-wrapper,
.gender-radio-group {
flex-shrink: 1;
}
.form-item__input,
.customer-source-select {
font-size: 26rpx;
padding: 0 16rpx;
}
.form-item__text {
padding: 0 16rpx;
}
.form-item__text text {
font-size: 26rpx;
}
.gender-radio-group {
gap: 24rpx;
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff