OfficeSystem/pages/customer/add/index.vue
2025-11-07 15:25:23 +08:00

1306 lines
36 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="add-customer-page">
<!-- 自定义导航栏 -->
<view class="custom-navbar">
<view class="navbar-content">
<text class="nav-btn" @click="handleCancel"></text>
<text class="nav-title">客户信息</text>
<text class="nav-btn" style="opacity: 0;">占位</text>
</view>
</view>
<!-- 内容区域 -->
<scroll-view class="content-scroll" scroll-y>
<view class="scroll-content">
<!-- 客户信息部分 -->
<view class="form-section">
<view class="section-title">客户信息</view>
<!-- 客户类型 -->
<view class="form-item clickable-item" @click="openCustomerTypePicker">
<text v-if="formData.customerType" class="form-value">{{ getCustomerTypeText(formData.customerType) }}</text>
<text v-else class="form-placeholder">选择客户类型</text>
<text class="arrow"></text>
</view>
<!-- 客户名称 -->
<view class="form-item">
<input
v-model="formData.name"
class="form-input"
placeholder="输入客户名称"
placeholder-style="color: #999;"
/>
</view>
<!-- 联系电话 -->
<view class="form-item">
<input
v-model="formData.mobile"
class="form-input"
placeholder="输入电话号码"
placeholder-style="color: #999;"
type="number"
/>
</view>
<!-- 微信号 -->
<view class="form-item">
<input
v-model="formData.wechat"
class="form-input"
placeholder="输入微信号"
placeholder-style="color: #999;"
/>
</view>
<!-- 客户来源 -->
<view class="form-item clickable-item" @click="openSourcePicker">
<text v-if="formData.source" class="form-value">{{ formData.source }}</text>
<text v-else class="form-placeholder">选择客户来源</text>
<text class="arrow"></text>
</view>
<!-- 客户意向 -->
<view class="form-item clickable-item" @click="openIntentPicker">
<text v-if="formData.intents && formData.intents.length > 0" class="form-value">{{ formData.intents.join('、') }}</text>
<text v-else class="form-placeholder">选择客户意向(可多选)</text>
<text class="arrow"></text>
</view>
<!-- 意向强度 -->
<view class="form-item clickable-item" @click="openIntentLevelPicker">
<text v-if="formData.intentLevel" class="form-value">{{ getIntentLevelText(formData.intentLevel) }}</text>
<text v-else class="form-placeholder">选择意向强度</text>
<text class="arrow"></text>
</view>
<!-- 客户状态 -->
<view class="form-item clickable-item" @click="openCustomerStatusPicker">
<text v-if="formData.customerStatus" class="form-value">{{ getCustomerStatusText(formData.customerStatus) }}</text>
<text v-else class="form-placeholder">选择客户状态</text>
<text class="arrow"></text>
</view>
<!-- 客户地区 -->
<view class="form-item clickable-item" @click="openRegionPicker" @tap="openRegionPicker">
<text v-if="formData.region" class="form-value">{{ formData.region }}</text>
<text v-else class="form-placeholder">选择客户地区</text>
<text class="arrow"></text>
</view>
<!-- 工作微信 -->
<view class="form-item clickable-item" @click="openWorkWechatPicker">
<text v-if="formData.workWechat" class="form-value">{{ formData.workWechat }}</text>
<text v-else class="form-placeholder">选择工作微信</text>
<text class="arrow"></text>
</view>
</view>
<!-- 其他信息部分 -->
<view class="form-section">
<view class="section-title">其他信息</view>
<!-- 客户星级 -->
<view class="form-item">
<text class="form-label">客户星级</text>
<view class="star-rating">
<text
class="star"
v-for="i in 5"
:key="i"
:class="{ 'filled': i <= formData.rating }"
@click="formData.rating = i"
>★</text>
</view>
</view>
<!-- 备注 -->
<view class="form-item">
<textarea
v-model="formData.remark"
class="form-textarea"
placeholder="输入备注"
placeholder-style="color: #999;"
auto-height
/>
</view>
<!-- 顾虑点 -->
<view class="form-item">
<textarea
v-model="formData.concern"
class="form-textarea"
placeholder="输入顾虑点"
placeholder-style="color: #999;"
auto-height
/>
</view>
<!-- 痛点 -->
<view class="form-item">
<textarea
v-model="formData.pain"
class="form-textarea"
placeholder="输入痛点"
placeholder-style="color: #999;"
auto-height
/>
</view>
<!-- 关注点 -->
<view class="form-item">
<textarea
v-model="formData.attention"
class="form-textarea"
placeholder="输入关注点"
placeholder-style="color: #999;"
auto-height
/>
</view>
<!-- 需求点 -->
<view class="form-item">
<textarea
v-model="formData.demand"
class="form-textarea"
placeholder="输入需求点"
placeholder-style="color: #999;"
auto-height
/>
</view>
<!-- 下次跟进时间 -->
<view class="form-item clickable-item" @click="openNextFollowTimePicker">
<text v-if="formData.nextFollowTime" class="form-value">{{ formData.nextFollowTime }}</text>
<text v-else class="form-placeholder">选择下次跟进时间(可选)</text>
<text class="arrow"></text>
</view>
</view>
</view>
</scroll-view>
<!-- 保存按钮 -->
<view class="save-button-wrapper">
<button class="save-button" @click="handleSave" :disabled="saving">保存</button>
</view>
<!-- 客户类型选择弹窗 -->
<view v-if="showCustomerTypePicker" class="modal-mask" @click="showCustomerTypePicker = false">
<view class="modal-content" @click.stop>
<view class="modal-title">选择客户类型</view>
<view class="picker-options">
<view
v-for="item in customerTypeOptions"
:key="item.value"
class="picker-option"
:class="{ active: tempCustomerType === item.value }"
@click="selectCustomerType(item.value)"
>
<text>{{ item.label }}</text>
<text v-if="tempCustomerType === item.value" class="check">✓</text>
</view>
</view>
<view class="modal-buttons">
<button class="modal-btn" @click="showCustomerTypePicker = false">取消</button>
<button class="modal-btn primary" @click="confirmCustomerType">确定</button>
</view>
</view>
</view>
<!-- 客户来源选择弹窗 -->
<view v-if="showSourcePicker" class="modal-mask" @click="showSourcePicker = false">
<view class="modal-content" @click.stop>
<view class="modal-title">选择客户来源</view>
<view class="picker-options">
<view
v-for="item in sourceOptions"
:key="item.value"
class="picker-option"
:class="{ active: tempSource === item.label }"
@click="selectSource(item.label)"
>
<text>{{ item.label }}</text>
<text v-if="tempSource === item.label" class="check">✓</text>
</view>
</view>
<view class="modal-buttons">
<button class="modal-btn" @click="showSourcePicker = false">取消</button>
<button class="modal-btn primary" @click="confirmSource">确定</button>
</view>
</view>
</view>
<!-- 客户意向选择弹窗(多选) -->
<view v-if="showIntentPicker" class="modal-mask" @click="showIntentPicker = false">
<view class="modal-content" @click.stop>
<view class="modal-title">选择客户意向(可多选)</view>
<view class="picker-options">
<view
v-for="item in intentOptions"
:key="item.value"
class="picker-option"
:class="{ active: tempIntents.includes(item.label) }"
@click="toggleIntent(item.label)"
>
<text>{{ item.label }}</text>
<text v-if="tempIntents.includes(item.label)" class="check">✓</text>
</view>
</view>
<view class="modal-buttons">
<button class="modal-btn" @click="showIntentPicker = false">取消</button>
<button class="modal-btn primary" @click="confirmIntent">确定</button>
</view>
</view>
</view>
<!-- 意向强度选择弹窗 -->
<view v-if="showIntentLevelPicker" class="modal-mask" @click="showIntentLevelPicker = false">
<view class="modal-content" @click.stop>
<view class="modal-title">选择意向强度</view>
<view class="picker-options">
<view
v-for="item in intentLevelOptions"
:key="item.value"
class="picker-option"
:class="{ active: tempIntentLevel === item.value }"
@click="selectIntentLevel(item.value)"
>
<text>{{ item.label }}</text>
<text v-if="tempIntentLevel === item.value" class="check">✓</text>
</view>
</view>
<view class="modal-buttons">
<button class="modal-btn" @click="showIntentLevelPicker = false">取消</button>
<button class="modal-btn primary" @click="confirmIntentLevel">确定</button>
</view>
</view>
</view>
<!-- 客户状态选择弹窗 -->
<view v-if="showCustomerStatusPicker" class="modal-mask" @click="showCustomerStatusPicker = false">
<view class="modal-content" @click.stop>
<view class="modal-title">选择客户状态</view>
<view class="picker-options">
<view
v-for="item in customerStatusOptions"
:key="item.value"
class="picker-option"
:class="{ active: tempCustomerStatus === item.value }"
@click="selectCustomerStatus(item.value)"
>
<text>{{ item.label }}</text>
<text v-if="tempCustomerStatus === item.value" class="check">✓</text>
</view>
</view>
<view class="modal-buttons">
<button class="modal-btn" @click="showCustomerStatusPicker = false">取消</button>
<button class="modal-btn primary" @click="confirmCustomerStatus">确定</button>
</view>
</view>
</view>
<!-- 客户地区选择器 - uv-picker -->
<uv-picker
ref="regionPicker"
:columns="addressList"
:loading="regionLoading"
keyName="name"
@confirm="onRegionConfirm"
@change="onRegionChange"
></uv-picker>
<!-- 工作微信选择弹窗 -->
<view v-if="showWorkWechatPicker" class="modal-mask" @click="showWorkWechatPicker = false">
<view class="modal-content" @click.stop>
<view class="modal-title">选择工作微信</view>
<view class="picker-options">
<view
v-for="item in workWechatOptions"
:key="item.id"
class="picker-option"
:class="{ active: tempWorkWechat === item.value }"
@click="selectWorkWechat(item.value)"
>
<text>{{ item.label }}</text>
<text v-if="tempWorkWechat === item.value" class="check">✓</text>
</view>
</view>
<view class="modal-buttons">
<button class="modal-btn" @click="showWorkWechatPicker = false">取消</button>
<button class="modal-btn primary" @click="confirmWorkWechat">确定</button>
</view>
</view>
</view>
<!-- 下次跟进时间选择弹窗 -->
<view v-if="showNextFollowTimePicker" class="modal-mask" @click="showNextFollowTimePicker = false">
<view class="modal-content" @click.stop>
<view class="modal-title">选择下次跟进时间</view>
<view class="datetime-picker-wrapper" @click.stop>
<picker
mode="date"
:value="tempNextFollowDate"
@change="onNextFollowDateChange"
>
<view class="picker-display">日期: {{ tempNextFollowDate || '请选择日期' }}</view>
</picker>
<picker
mode="time"
:value="tempNextFollowTime"
@change="onNextFollowTimeChange"
>
<view class="picker-display">时间: {{ tempNextFollowTime || '请选择时间' }}</view>
</picker>
</view>
<view class="modal-buttons">
<button class="modal-btn" @click="clearNextFollowTime">清除</button>
<button class="modal-btn" @click="showNextFollowTimePicker = false">取消</button>
<button class="modal-btn primary" @click="confirmNextFollowTime">确定</button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import {
createCustomer,
getRegionTree,
getCustomerIntentDict,
getCustomerIntentLevelDict,
getCustomerSourceDict,
getCustomerTypeDict,
getCustomerStatusDict,
getWechatList
} from '@/common/api';
import { useUserStore } from '@/store/user';
// 表单数据
const formData = ref({
customerType: '', // 客户类型dictValue
name: '', // 客户名称
mobile: '', // 联系电话
wechat: '', // 微信号
source: '', // 客户来源dictLabel
intents: [], // 客户意向数组dictLabel
intentLevel: '', // 意向强度dictValue
customerStatus: '', // 客户状态dictValue
region: '', // 客户地区(显示名称,如:北京/北京/朝阳)
regionIds: [], // 客户地区ID数组 [省ID, 市ID, 区ID]
workWechat: '', // 工作微信(显示名称)
workWechatId: null, // 工作微信ID
rating: 0, // 客户星级0-5
remark: '', // 备注
concern: '', // 顾虑点
pain: '', // 痛点
attention: '', // 关注点
demand: '', // 需求点
nextFollowTime: '' // 下次跟进时间
});
// 显示状态
const saving = ref(false);
const showCustomerTypePicker = ref(false);
const showSourcePicker = ref(false);
const showIntentPicker = ref(false);
const showIntentLevelPicker = ref(false);
const showCustomerStatusPicker = ref(false);
const showWorkWechatPicker = ref(false);
const showNextFollowTimePicker = ref(false);
// 临时选择值
const tempCustomerType = ref('');
const tempSource = ref('');
const tempIntents = ref([]);
const tempIntentLevel = ref('');
const tempCustomerStatus = ref('');
const tempWorkWechat = ref('');
const tempNextFollowDate = ref('');
const tempNextFollowTime = ref('');
// 字典数据
const customerTypeOptions = ref([]); // 客户类型选项
const sourceOptions = ref([]); // 客户来源选项
const intentOptions = ref([]); // 客户意向选项
const intentLevelOptions = ref([]); // 意向强度选项
const customerStatusOptions = ref([]); // 客户状态选项
// 地区树数据
const regionTree = ref([]);
// uv-picker 的列数据
const provinces = ref([]); // 省
const citys = ref([]); // 市
const areas = ref([]); // 区
const pickerValue = ref([0, 0, 0]); // 当前选中的索引
const regionLoading = ref(true); // 加载状态
// uv-picker 的引用
const regionPicker = ref(null);
// 计算属性:返回地址列表
const addressList = computed(() => {
return [provinces.value, citys.value, areas.value];
});
// 加载字典数据
const loadDictData = async () => {
try {
// 并行加载所有字典数据
const [typeRes, sourceRes, intentRes, intentLevelRes, statusRes] = await Promise.all([
getCustomerTypeDict(),
getCustomerSourceDict(),
getCustomerIntentDict(),
getCustomerIntentLevelDict(),
getCustomerStatusDict()
]);
// 处理客户类型数据
customerTypeOptions.value = (typeRes || []).map(item => ({
label: item.dictLabel,
value: item.dictValue
}));
// 处理客户来源数据
sourceOptions.value = (sourceRes || []).map(item => ({
label: item.dictLabel,
value: item.dictValue
}));
// 处理客户意向数据
intentOptions.value = (intentRes || []).map(item => ({
label: item.dictLabel,
value: item.dictValue
}));
// 处理意向强度数据
intentLevelOptions.value = (intentLevelRes || []).map(item => ({
label: item.dictLabel,
value: item.dictValue
}));
// 处理客户状态数据
customerStatusOptions.value = (statusRes || []).map(item => ({
label: item.dictLabel,
value: item.dictValue
}));
console.log('字典数据加载成功:', {
customerType: customerTypeOptions.value,
source: sourceOptions.value,
intent: intentOptions.value,
intentLevel: intentLevelOptions.value,
customerStatus: customerStatusOptions.value
});
} catch (err) {
console.error('加载字典数据失败:', err);
uni.showToast({
title: '加载字典数据失败',
icon: 'none'
});
}
};
// 加载微信列表数据
const loadWechatList = async () => {
try {
const res = await getWechatList();
const rows = res?.rows || [];
// 处理微信列表数据,使用 id 作为值(传递给后端的 workWechatId
workWechatOptions.value = rows.map(item => ({
label: item.nickName ? `${item.nickName} (${item.wechatId})` : item.wechatId,
value: String(item.id), // 使用数据库主键 id转换为字符串
id: item.id,
wechatId: item.wechatId // 保留 wechatId 用于显示
}));
console.log('微信列表加载成功:', workWechatOptions.value);
} catch (err) {
console.error('加载微信列表失败:', err);
uni.showToast({
title: '加载微信列表失败',
icon: 'none'
});
}
};
// 加载地区树数据
const loadRegionTree = async () => {
try {
const res = await getRegionTree();
console.log('获取的地区数据:', res);
regionTree.value = res;
// 初始化省份数据
provinces.value = res.sort((left, right) => (Number(left.code || left.id) > Number(right.code || right.id) ? 1 : -1));
// 如果有已选择的地区,设置默认值
if (formData.value.regionIds && formData.value.regionIds.length > 0) {
handlePickValueDefault();
} else {
// 初始化默认显示第一个省份的城市和区县
if (provinces.value.length > 0) {
citys.value = provinces.value[0]?.children || [];
if (citys.value.length > 0) {
areas.value = citys.value[0]?.children || [];
}
}
}
setTimeout(() => {
regionLoading.value = false;
}, 200);
} catch (err) {
console.error('加载地区树失败:', err);
regionLoading.value = false;
uni.showToast({
title: '加载地区数据失败',
icon: 'none'
});
}
};
// 根据已选择的ID设置默认选中项
const handlePickValueDefault = () => {
if (!formData.value.regionIds || formData.value.regionIds.length === 0) {
return;
}
const [provinceId, cityId, districtId] = formData.value.regionIds;
// 设置省
const provinceIndex = provinces.value.findIndex(item => Number(item.id) === Number(provinceId));
if (provinceIndex >= 0) {
pickerValue.value[0] = provinceIndex;
// 设置市
citys.value = provinces.value[provinceIndex]?.children || [];
const cityIndex = citys.value.findIndex(item => Number(item.id) === Number(cityId));
if (cityIndex >= 0) {
pickerValue.value[1] = cityIndex;
// 设置区
areas.value = citys.value[cityIndex]?.children || [];
const districtIndex = areas.value.findIndex(item => Number(item.id) === Number(districtId));
if (districtIndex >= 0) {
pickerValue.value[2] = districtIndex;
}
}
}
// 重置选择器位置
if (regionPicker.value) {
regionPicker.value.setIndexs([pickerValue.value[0], pickerValue.value[1], pickerValue.value[2]], true);
}
};
// 工作微信选项从API获取
const workWechatOptions = ref([]);
// 获取客户类型文本
const getCustomerTypeText = (value) => {
const option = customerTypeOptions.value.find(opt => opt.value === value);
return option ? option.label : '';
};
// 获取意向强度文本
const getIntentLevelText = (value) => {
const option = intentLevelOptions.value.find(opt => opt.value === value);
return option ? option.label : '';
};
// 获取客户状态文本
const getCustomerStatusText = (value) => {
const option = customerStatusOptions.value.find(opt => opt.value === value);
return option ? option.label : '';
};
// 打开客户类型选择器
const openCustomerTypePicker = () => {
tempCustomerType.value = formData.value.customerType;
showCustomerTypePicker.value = true;
};
// 选择客户类型
const selectCustomerType = (value) => {
tempCustomerType.value = value;
};
// 确认客户类型
const confirmCustomerType = () => {
formData.value.customerType = tempCustomerType.value;
showCustomerTypePicker.value = false;
};
// 打开客户来源选择器
const openSourcePicker = () => {
tempSource.value = formData.value.source;
showSourcePicker.value = true;
};
// 选择客户来源
const selectSource = (label) => {
tempSource.value = label;
};
// 确认客户来源
const confirmSource = () => {
formData.value.source = tempSource.value;
showSourcePicker.value = false;
};
// 打开客户意向选择器
const openIntentPicker = () => {
tempIntents.value = [...formData.value.intents];
showIntentPicker.value = true;
};
// 切换客户意向(多选)
const toggleIntent = (label) => {
const index = tempIntents.value.indexOf(label);
if (index > -1) {
tempIntents.value.splice(index, 1);
} else {
tempIntents.value.push(label);
}
};
// 确认客户意向
const confirmIntent = () => {
formData.value.intents = [...tempIntents.value];
showIntentPicker.value = false;
};
// 打开意向强度选择器
const openIntentLevelPicker = () => {
tempIntentLevel.value = formData.value.intentLevel;
showIntentLevelPicker.value = true;
};
// 选择意向强度
const selectIntentLevel = (value) => {
tempIntentLevel.value = value;
};
// 确认意向强度
const confirmIntentLevel = () => {
formData.value.intentLevel = tempIntentLevel.value;
showIntentLevelPicker.value = false;
};
// 打开客户状态选择器
const openCustomerStatusPicker = () => {
tempCustomerStatus.value = formData.value.customerStatus;
showCustomerStatusPicker.value = true;
};
// 选择客户状态
const selectCustomerStatus = (value) => {
tempCustomerStatus.value = value;
};
// 确认客户状态
const confirmCustomerStatus = () => {
formData.value.customerStatus = tempCustomerStatus.value;
showCustomerStatusPicker.value = false;
};
// 打开地区选择器
const openRegionPicker = () => {
console.log('openRegionPicker called');
// 检查数据是否已加载
if (regionLoading.value || !regionTree.value || regionTree.value.length === 0) {
uni.showToast({
title: '地区数据加载中,请稍候',
icon: 'none'
});
return;
}
// 如果有已选择的地区,恢复选择状态
if (formData.value.regionIds && formData.value.regionIds.length > 0) {
handlePickValueDefault();
} else {
// 如果没有已选择的地区,确保城市和区县数据已初始化
if (citys.value.length === 0 && provinces.value.length > 0) {
citys.value = provinces.value[0]?.children || [];
if (citys.value.length > 0) {
areas.value = citys.value[0]?.children || [];
}
}
}
// 打开选择器
if (regionPicker.value) {
regionPicker.value.open();
// 如果没有已选择的地区,在打开后设置默认索引
if (!formData.value.regionIds || formData.value.regionIds.length === 0) {
setTimeout(() => {
if (regionPicker.value) {
regionPicker.value.setIndexs([0, 0, 0], true);
}
}, 100);
}
}
};
// uv-picker 的 change 事件处理(实现三级联动)
const onRegionChange = (e) => {
if (regionLoading.value) return;
const { columnIndex, index, indexs } = e;
// 改变了省
if (columnIndex === 0) {
citys.value = provinces.value[index]?.children || [];
areas.value = citys.value[0]?.children || [];
if (regionPicker.value) {
regionPicker.value.setIndexs([index, 0, 0], true);
}
} else if (columnIndex === 1) {
// 改变了市
areas.value = citys.value[index]?.children || [];
if (regionPicker.value) {
regionPicker.value.setIndexs(indexs, true);
}
}
};
// uv-picker 的 confirm 事件处理
const onRegionConfirm = (e) => {
console.log('确认选择的地区:', e);
const { value } = e;
if (value && value.length > 0) {
const province = value[0];
const city = value[1];
const district = value[2];
// 构建地区名称和ID数组
const regionNames = [];
const regionIds = [];
if (province) {
regionNames.push(province.name);
regionIds.push(province.id);
if (city) {
regionNames.push(city.name);
regionIds.push(city.id);
if (district) {
regionNames.push(district.name);
regionIds.push(district.id);
}
}
}
formData.value.region = regionNames.join('/');
formData.value.regionIds = regionIds;
} else {
formData.value.region = '';
formData.value.regionIds = [];
}
};
// 打开工作微信选择器
const openWorkWechatPicker = () => {
tempWorkWechat.value = formData.value.workWechat;
showWorkWechatPicker.value = true;
};
// 选择工作微信
const selectWorkWechat = (value) => {
tempWorkWechat.value = value;
};
// 确认工作微信
const confirmWorkWechat = () => {
const selectedWechat = workWechatOptions.value.find(w => w.value === tempWorkWechat.value);
if (selectedWechat) {
formData.value.workWechat = selectedWechat.label; // 显示名称
formData.value.workWechatId = selectedWechat.value; // 使用 id 作为值(传递给后端的 workWechatId
}
showWorkWechatPicker.value = false;
};
// 打开下次跟进时间选择器
const openNextFollowTimePicker = () => {
if (formData.value.nextFollowTime) {
const [date, time] = formData.value.nextFollowTime.split(' ');
tempNextFollowDate.value = date || '';
tempNextFollowTime.value = time || '';
} else {
tempNextFollowDate.value = '';
tempNextFollowTime.value = '';
}
showNextFollowTimePicker.value = true;
};
// 日期改变
const onNextFollowDateChange = (e) => {
tempNextFollowDate.value = e.detail.value;
};
// 时间改变
const onNextFollowTimeChange = (e) => {
tempNextFollowTime.value = e.detail.value;
};
// 清除下次跟进时间
const clearNextFollowTime = () => {
formData.value.nextFollowTime = '';
tempNextFollowDate.value = '';
tempNextFollowTime.value = '';
showNextFollowTimePicker.value = false;
};
// 确认下次跟进时间
const confirmNextFollowTime = () => {
if (tempNextFollowDate.value && tempNextFollowTime.value) {
formData.value.nextFollowTime = `${tempNextFollowDate.value} ${tempNextFollowTime.value}:00`;
} else if (tempNextFollowDate.value) {
formData.value.nextFollowTime = `${tempNextFollowDate.value} 09:00:00`;
} else {
formData.value.nextFollowTime = '';
}
showNextFollowTimePicker.value = false;
};
// 取消
const handleCancel = () => {
uni.navigateBack();
};
// 组件挂载时加载地区树数据、字典数据和微信列表
onMounted(() => {
loadRegionTree();
loadDictData();
loadWechatList();
});
// 保存
const handleSave = async () => {
// 表单验证
if (!formData.value.name || formData.value.name.trim() === '') {
uni.showToast({
title: '请输入客户名称',
icon: 'none'
});
return;
}
if (!formData.value.mobile || formData.value.mobile.trim() === '') {
uni.showToast({
title: '请输入联系电话',
icon: 'none'
});
return;
}
saving.value = true;
try {
// 获取用户信息
const userStore = useUserStore();
const userId = userStore.userInfo?.id || userStore.userInfo?.userId || '1';
// 格式化当前时间
const now = new Date();
const formatDateTime = (date) => {
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}`;
};
// 处理意向数组已经是数组格式包含dictLabel
const intentsArray = Array.isArray(formData.value.intents) ? formData.value.intents : [];
// 处理地区ID数组使用已选择的regionIds
const regionIdsArray = formData.value.regionIds || [];
// 构建提交数据(按照接口示例格式)
const submitData = {
id: null,
code: null,
name: formData.value.name.trim(),
status: null,
intentLevel: null,
mobile: formData.value.mobile.trim(),
wechat: formData.value.wechat.trim() || null,
source: formData.value.source || null, // 使用dictLabel
intents: intentsArray, // 数组格式使用dictLabel
followId: userId, // 跟进人ID
remark: formData.value.remark.trim() || null,
type: formData.value.customerType || '2', // 使用dictValue
workWechatId: formData.value.workWechatId || null,
regionIds: regionIdsArray, // 数组格式
// 添加关注点、顾虑点、需求点、痛点字段
attention: formData.value.attention.trim() || null,
concern: formData.value.concern.trim() || null,
demand: formData.value.demand.trim() || null,
pain: formData.value.pain.trim() || null,
follow: {
followTime: formatDateTime(now), // 当前时间作为跟进时间
nextFollowTime: formData.value.nextFollowTime || null,
customerIntentLevel: formData.value.intentLevel || null, // 使用dictValue
customerStatus: formData.value.customerStatus || null // 使用dictValue
},
nextFollowTime: formData.value.nextFollowTime || null,
customerIntentLevel: formData.value.intentLevel || null, // 使用dictValue
customerStatus: formData.value.customerStatus || null // 使用dictValue
};
// 调用API创建客户
await createCustomer(submitData);
uni.showToast({
title: '创建成功',
icon: 'success'
});
// 保存成功后不清空表单,留在当前页面
} catch (error) {
console.error('创建客户失败:', error);
uni.showToast({
title: error.message || '创建失败,请重试',
icon: 'none'
});
} finally {
saving.value = false;
}
};
</script>
<style lang="scss" scoped>
.add-customer-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
/* 自定义导航栏 */
.custom-navbar {
background-color: #fff;
padding-top: var(--status-bar-height);
border-bottom: 1px solid #eee;
}
.navbar-content {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
padding: 0 16px;
}
.nav-btn {
font-size: 20px;
color: #333;
min-width: 44px;
text-align: center;
}
.nav-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
/* 内容区域 */
.content-scroll {
flex: 1;
}
.scroll-content {
padding: 16px;
padding-bottom: 80px; /* 为保存按钮留出空间 */
}
/* 表单部分 */
.form-section {
background-color: #fff;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #eee;
}
.form-item {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.form-label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
display: block;
}
.form-input {
width: 100%;
height: 44px;
padding: 0 12px;
font-size: 15px;
color: #333;
background-color: #f8f8f8;
border-radius: 6px;
border: 1px solid #e0e0e0;
}
.form-textarea {
width: 100%;
min-height: 80px;
padding: 12px;
font-size: 15px;
color: #333;
background-color: #f8f8f8;
border-radius: 6px;
border: 1px solid #e0e0e0;
}
.clickable-item {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
padding: 0 12px;
background-color: #f8f8f8;
border-radius: 6px;
border: 1px solid #e0e0e0;
cursor: pointer;
pointer-events: auto;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.form-value {
font-size: 15px;
color: #333;
}
.form-placeholder {
font-size: 15px;
color: #999;
}
.arrow {
font-size: 20px;
color: #999;
}
/* 单选框组 */
.radio-group {
display: flex;
gap: 24px;
}
.radio-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
color: #333;
cursor: pointer;
}
.radio-dot {
width: 18px;
height: 18px;
border-radius: 50%;
border: 2px solid #ddd;
background-color: #fff;
position: relative;
transition: all 0.3s;
&.checked {
border-color: #1976d2;
background-color: #1976d2;
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 6px;
height: 6px;
border-radius: 50%;
background-color: #fff;
}
}
}
/* 星级评分 */
.star-rating {
display: flex;
gap: 8px;
align-items: center;
}
.star {
font-size: 24px;
color: #ddd;
cursor: pointer;
transition: color 0.2s;
&.filled {
color: #ffc107;
}
}
/* 保存按钮 */
.save-button-wrapper {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 12px 16px;
padding-bottom: calc(12px + env(safe-area-inset-bottom));
background-color: #fff;
border-top: 1px solid #eee;
z-index: 100;
}
.save-button {
width: 100%;
height: 44px;
background-color: #1976d2;
color: #fff;
font-size: 16px;
font-weight: 600;
border-radius: 6px;
border: none;
&:disabled {
background-color: #ccc;
opacity: 0.6;
}
}
/* 弹窗样式 */
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-end;
z-index: 1000;
}
.modal-content {
width: 100%;
max-height: 70vh;
background-color: #fff;
border-radius: 16px 16px 0 0;
padding: 20px;
animation: slideUp 0.3s;
}
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
.modal-title {
font-size: 18px;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 20px;
}
.picker-options {
max-height: 400px;
overflow-y: auto;
}
.picker-option {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
font-size: 15px;
color: #333;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
&.active {
color: #1976d2;
}
}
.check {
color: #1976d2;
font-size: 18px;
}
.modal-buttons {
display: flex;
gap: 12px;
margin-top: 20px;
}
.datetime-picker-wrapper {
padding: 20px 0;
.picker-display {
padding: 12px;
margin-bottom: 12px;
background-color: #f8f8f8;
border-radius: 6px;
font-size: 15px;
color: #333;
text-align: center;
cursor: pointer;
pointer-events: auto;
user-select: none;
}
}
.modal-btn {
flex: 1;
height: 44px;
font-size: 15px;
border-radius: 6px;
border: 1px solid #e0e0e0;
background-color: #fff;
color: #666;
&.primary {
background-color: #1976d2;
color: #fff;
border-color: #1976d2;
}
}
</style>