客户添加组件拆分-编辑客户
This commit is contained in:
parent
0d4dbfd0ce
commit
6e8dc30c59
2
App.vue
2
App.vue
|
|
@ -8,7 +8,7 @@
|
|||
// 如果还没有 token,则设置一个测试 token
|
||||
const token = uni.getStorageSync('token')
|
||||
if (!token) {
|
||||
const testToken = 'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjJiNWZkNDAyLTFiYWQtNDI5YS04ZDFkLTllZGE0Y2VkOWIxMyJ9.2UEvAG6OtJ0WZoJxjPjGzGhdgxAclzEqqPl_K94kbcCVs9YplENwZe2AcdsLkdLsc7EnDbRif-xuhWlTKF78WQ'
|
||||
const testToken = 'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImI4MDUzOGE0LThjNTQtNGZlMy04NmMwLThhYzhlZmMwMTg1ZCJ9.xalBJnQcnxHsHUVFn46XXamKBPA2RdDtvVvhRQwc7j-lARywOjKMfSqjUiSe8DbWMKt1iBZIz0Z0JDYX7IGb_g'
|
||||
uni.setStorageSync('token', testToken)
|
||||
console.log('已设置测试 token:', testToken)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,6 +166,37 @@ export const createCustomer = (data) => {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新客户
|
||||
* @param {Object} data 客户数据(必须包含id)
|
||||
* @param {string} data.id 客户ID
|
||||
* @param {string} data.type 类型
|
||||
* @param {string} data.name 客户名
|
||||
* @param {string} data.mobile 手机号
|
||||
* @param {string} data.wechat 微信号
|
||||
* @param {string} data.source 来源
|
||||
* @param {string} data.intents 意向的设备,逗号分离
|
||||
* @param {string} data.status 状态(1潜在2意向3成交4失效)
|
||||
* @param {string} data.intentLevel 意向强度
|
||||
* @param {string} data.customerStatus 客户状态
|
||||
* @param {string} data.customerIntentLevel 客户意向强度
|
||||
* @param {string} data.nextFollowTime 下次跟进时间
|
||||
* @param {string} data.followId 跟进人id
|
||||
* @param {string} data.remark 备注
|
||||
* @param {string} data.concern 顾虑点
|
||||
* @param {string} data.pain 痛点
|
||||
* @param {string} data.attention 关注点
|
||||
* @param {string} data.demand 需求点
|
||||
* @returns {Promise} 返回更新结果
|
||||
*/
|
||||
export const updateCustomer = (data) => {
|
||||
return uni.$uv.http.put(`bst/customer`, data, {
|
||||
custom: {
|
||||
auth: true
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取客户意向字典数据
|
||||
* @returns {Promise} 返回字典数据数组,包含 dictLabel 和 dictValue
|
||||
|
|
|
|||
203
components/customer-form/CustomerBasicInfo.vue
Normal file
203
components/customer-form/CustomerBasicInfo.vue
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
<template>
|
||||
<view class="form-section">
|
||||
<view class="section-title">客户信息</view>
|
||||
|
||||
<!-- 客户类型 -->
|
||||
<view class="form-item clickable-item" @click="$emit('open-picker', 'customerType')">
|
||||
<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
|
||||
:value="formData.name"
|
||||
@input="updateField('name', $event.detail.value)"
|
||||
class="form-input"
|
||||
placeholder="输入客户名称"
|
||||
placeholder-style="color: #999;"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 联系电话 -->
|
||||
<view class="form-item">
|
||||
<input
|
||||
:value="formData.mobile"
|
||||
@input="updateField('mobile', $event.detail.value)"
|
||||
class="form-input"
|
||||
placeholder="输入电话号码"
|
||||
placeholder-style="color: #999;"
|
||||
type="number"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 微信号 -->
|
||||
<view class="form-item">
|
||||
<input
|
||||
:value="formData.wechat"
|
||||
@input="updateField('wechat', $event.detail.value)"
|
||||
class="form-input"
|
||||
placeholder="输入微信号"
|
||||
placeholder-style="color: #999;"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 客户来源 -->
|
||||
<view class="form-item clickable-item" @click="$emit('open-picker', 'source')">
|
||||
<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="$emit('open-picker', 'intent')">
|
||||
<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="$emit('open-picker', 'intentLevel')">
|
||||
<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="$emit('open-picker', 'customerStatus')">
|
||||
<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="$emit('open-picker', 'region')">
|
||||
<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="$emit('open-picker', 'workWechat')">
|
||||
<text v-if="formData.workWechat" class="form-value">{{ formData.workWechat }}</text>
|
||||
<text v-else class="form-placeholder">选择工作微信</text>
|
||||
<text class="arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
formData: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
customerTypeOptions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
intentLevelOptions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
customerStatusOptions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:formData', 'open-picker']);
|
||||
|
||||
const updateField = (field, value) => {
|
||||
emit('update:formData', {
|
||||
...props.formData,
|
||||
[field]: value
|
||||
});
|
||||
};
|
||||
|
||||
const getCustomerTypeText = (value) => {
|
||||
const option = props.customerTypeOptions.find(opt => opt.value === value);
|
||||
return option ? option.label : '';
|
||||
};
|
||||
|
||||
const getIntentLevelText = (value) => {
|
||||
const option = props.intentLevelOptions.find(opt => opt.value === value);
|
||||
return option ? option.label : '';
|
||||
};
|
||||
|
||||
const getCustomerStatusText = (value) => {
|
||||
const option = props.customerStatusOptions.find(opt => opt.value === value);
|
||||
return option ? option.label : '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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-input {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
padding: 0 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;
|
||||
}
|
||||
</style>
|
||||
|
||||
54
components/customer-form/CustomerFormNavbar.vue
Normal file
54
components/customer-form/CustomerFormNavbar.vue
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<view class="custom-navbar">
|
||||
<view class="navbar-content">
|
||||
<text class="nav-btn" @click="handleCancel">‹</text>
|
||||
<text class="nav-title">{{ title }}</text>
|
||||
<text class="nav-btn" style="opacity: 0;">占位</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '客户信息'
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['cancel']);
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('cancel');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
|
||||
196
components/customer-form/CustomerOtherInfo.vue
Normal file
196
components/customer-form/CustomerOtherInfo.vue
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
<template>
|
||||
<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="updateField('rating', i)"
|
||||
>★</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 备注 -->
|
||||
<view class="form-item">
|
||||
<textarea
|
||||
:value="formData.remark"
|
||||
@input="updateField('remark', $event.detail.value)"
|
||||
class="form-textarea"
|
||||
placeholder="输入备注"
|
||||
placeholder-style="color: #999;"
|
||||
auto-height
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 顾虑点 -->
|
||||
<view class="form-item">
|
||||
<textarea
|
||||
:value="formData.concern"
|
||||
@input="updateField('concern', $event.detail.value)"
|
||||
class="form-textarea"
|
||||
placeholder="输入顾虑点"
|
||||
placeholder-style="color: #999;"
|
||||
auto-height
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 痛点 -->
|
||||
<view class="form-item">
|
||||
<textarea
|
||||
:value="formData.pain"
|
||||
@input="updateField('pain', $event.detail.value)"
|
||||
class="form-textarea"
|
||||
placeholder="输入痛点"
|
||||
placeholder-style="color: #999;"
|
||||
auto-height
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 关注点 -->
|
||||
<view class="form-item">
|
||||
<textarea
|
||||
:value="formData.attention"
|
||||
@input="updateField('attention', $event.detail.value)"
|
||||
class="form-textarea"
|
||||
placeholder="输入关注点"
|
||||
placeholder-style="color: #999;"
|
||||
auto-height
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 需求点 -->
|
||||
<view class="form-item">
|
||||
<textarea
|
||||
:value="formData.demand"
|
||||
@input="updateField('demand', $event.detail.value)"
|
||||
class="form-textarea"
|
||||
placeholder="输入需求点"
|
||||
placeholder-style="color: #999;"
|
||||
auto-height
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 下次跟进时间 -->
|
||||
<view class="form-item clickable-item" @click="$emit('open-picker', 'nextFollowTime')">
|
||||
<text v-if="formData.nextFollowTime" class="form-value">{{ formData.nextFollowTime }}</text>
|
||||
<text v-else class="form-placeholder">选择下次跟进时间(可选)</text>
|
||||
<text class="arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
formData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:formData', 'open-picker']);
|
||||
|
||||
const updateField = (field, value) => {
|
||||
emit('update:formData', {
|
||||
...props.formData,
|
||||
[field]: value
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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-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;
|
||||
}
|
||||
|
||||
.star-rating {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.star {
|
||||
font-size: 24px;
|
||||
color: #ddd;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&.filled {
|
||||
color: #ffc107;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
489
components/customer-form/CustomerPickers.vue
Normal file
489
components/customer-form/CustomerPickers.vue
Normal file
|
|
@ -0,0 +1,489 @@
|
|||
<template>
|
||||
<!-- 客户类型选择弹窗 -->
|
||||
<view v-if="showCustomerTypePicker" class="modal-mask" @click="closePicker('customerType')">
|
||||
<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="closePicker('customerType')">取消</button>
|
||||
<button class="modal-btn primary" @click="confirmCustomerType">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 客户来源选择弹窗 -->
|
||||
<view v-if="showSourcePicker" class="modal-mask" @click="closePicker('source')">
|
||||
<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="closePicker('source')">取消</button>
|
||||
<button class="modal-btn primary" @click="confirmSource">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 客户意向选择弹窗(多选) -->
|
||||
<view v-if="showIntentPicker" class="modal-mask" @click="closePicker('intent')">
|
||||
<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="closePicker('intent')">取消</button>
|
||||
<button class="modal-btn primary" @click="confirmIntent">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 意向强度选择弹窗 -->
|
||||
<view v-if="showIntentLevelPicker" class="modal-mask" @click="closePicker('intentLevel')">
|
||||
<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="closePicker('intentLevel')">取消</button>
|
||||
<button class="modal-btn primary" @click="confirmIntentLevel">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 客户状态选择弹窗 -->
|
||||
<view v-if="showCustomerStatusPicker" class="modal-mask" @click="closePicker('customerStatus')">
|
||||
<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="closePicker('customerStatus')">取消</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="closePicker('workWechat')">
|
||||
<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="closePicker('workWechat')">取消</button>
|
||||
<button class="modal-btn primary" @click="confirmWorkWechat">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 下次跟进时间选择弹窗 -->
|
||||
<view v-if="showNextFollowTimePicker" class="modal-mask" @click="closePicker('nextFollowTime')">
|
||||
<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="closePicker('nextFollowTime')">取消</button>
|
||||
<button class="modal-btn primary" @click="confirmNextFollowTime">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
showCustomerTypePicker: Boolean,
|
||||
showSourcePicker: Boolean,
|
||||
showIntentPicker: Boolean,
|
||||
showIntentLevelPicker: Boolean,
|
||||
showCustomerStatusPicker: Boolean,
|
||||
showWorkWechatPicker: Boolean,
|
||||
showNextFollowTimePicker: Boolean,
|
||||
customerTypeOptions: Array,
|
||||
sourceOptions: Array,
|
||||
intentOptions: Array,
|
||||
intentLevelOptions: Array,
|
||||
customerStatusOptions: Array,
|
||||
workWechatOptions: Array,
|
||||
addressList: Array,
|
||||
regionLoading: Boolean,
|
||||
formData: Object
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:formData', 'close-picker', 'region-confirm', 'region-change']);
|
||||
|
||||
// 临时选择值
|
||||
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 regionPicker = ref(null);
|
||||
|
||||
// 监听表单数据变化,更新临时值
|
||||
watch(() => props.formData, (newData) => {
|
||||
if (newData) {
|
||||
tempCustomerType.value = newData.customerType || '';
|
||||
tempSource.value = newData.source || '';
|
||||
tempIntents.value = newData.intents ? [...newData.intents] : [];
|
||||
tempIntentLevel.value = newData.intentLevel || '';
|
||||
tempCustomerStatus.value = newData.customerStatus || '';
|
||||
tempWorkWechat.value = newData.workWechatId || '';
|
||||
if (newData.nextFollowTime) {
|
||||
const [date, time] = newData.nextFollowTime.split(' ');
|
||||
tempNextFollowDate.value = date || '';
|
||||
tempNextFollowTime.value = time ? time.substring(0, 5) : '';
|
||||
} else {
|
||||
tempNextFollowDate.value = '';
|
||||
tempNextFollowTime.value = '';
|
||||
}
|
||||
}
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
const closePicker = (pickerType) => {
|
||||
emit('close-picker', pickerType);
|
||||
};
|
||||
|
||||
// 客户类型
|
||||
const selectCustomerType = (value) => {
|
||||
tempCustomerType.value = value;
|
||||
};
|
||||
|
||||
const confirmCustomerType = () => {
|
||||
emit('update:formData', {
|
||||
...props.formData,
|
||||
customerType: tempCustomerType.value
|
||||
});
|
||||
closePicker('customerType');
|
||||
};
|
||||
|
||||
// 客户来源
|
||||
const selectSource = (label) => {
|
||||
tempSource.value = label;
|
||||
};
|
||||
|
||||
const confirmSource = () => {
|
||||
emit('update:formData', {
|
||||
...props.formData,
|
||||
source: tempSource.value
|
||||
});
|
||||
closePicker('source');
|
||||
};
|
||||
|
||||
// 客户意向
|
||||
const toggleIntent = (label) => {
|
||||
const index = tempIntents.value.indexOf(label);
|
||||
if (index > -1) {
|
||||
tempIntents.value.splice(index, 1);
|
||||
} else {
|
||||
tempIntents.value.push(label);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmIntent = () => {
|
||||
emit('update:formData', {
|
||||
...props.formData,
|
||||
intents: [...tempIntents.value]
|
||||
});
|
||||
closePicker('intent');
|
||||
};
|
||||
|
||||
// 意向强度
|
||||
const selectIntentLevel = (value) => {
|
||||
tempIntentLevel.value = value;
|
||||
};
|
||||
|
||||
const confirmIntentLevel = () => {
|
||||
emit('update:formData', {
|
||||
...props.formData,
|
||||
intentLevel: tempIntentLevel.value
|
||||
});
|
||||
closePicker('intentLevel');
|
||||
};
|
||||
|
||||
// 客户状态
|
||||
const selectCustomerStatus = (value) => {
|
||||
tempCustomerStatus.value = value;
|
||||
};
|
||||
|
||||
const confirmCustomerStatus = () => {
|
||||
emit('update:formData', {
|
||||
...props.formData,
|
||||
customerStatus: tempCustomerStatus.value
|
||||
});
|
||||
closePicker('customerStatus');
|
||||
};
|
||||
|
||||
// 工作微信
|
||||
const selectWorkWechat = (value) => {
|
||||
tempWorkWechat.value = value;
|
||||
};
|
||||
|
||||
const confirmWorkWechat = () => {
|
||||
const selectedWechat = props.workWechatOptions.find(w => w.value === tempWorkWechat.value);
|
||||
if (selectedWechat) {
|
||||
emit('update:formData', {
|
||||
...props.formData,
|
||||
workWechat: selectedWechat.label,
|
||||
workWechatId: selectedWechat.value
|
||||
});
|
||||
}
|
||||
closePicker('workWechat');
|
||||
};
|
||||
|
||||
// 下次跟进时间
|
||||
const onNextFollowDateChange = (e) => {
|
||||
tempNextFollowDate.value = e.detail.value;
|
||||
};
|
||||
|
||||
const onNextFollowTimeChange = (e) => {
|
||||
tempNextFollowTime.value = e.detail.value;
|
||||
};
|
||||
|
||||
const clearNextFollowTime = () => {
|
||||
tempNextFollowDate.value = '';
|
||||
tempNextFollowTime.value = '';
|
||||
emit('update:formData', {
|
||||
...props.formData,
|
||||
nextFollowTime: ''
|
||||
});
|
||||
closePicker('nextFollowTime');
|
||||
};
|
||||
|
||||
const confirmNextFollowTime = () => {
|
||||
let nextFollowTime = '';
|
||||
if (tempNextFollowDate.value && tempNextFollowTime.value) {
|
||||
nextFollowTime = `${tempNextFollowDate.value} ${tempNextFollowTime.value}:00`;
|
||||
} else if (tempNextFollowDate.value) {
|
||||
nextFollowTime = `${tempNextFollowDate.value} 09:00:00`;
|
||||
}
|
||||
emit('update:formData', {
|
||||
...props.formData,
|
||||
nextFollowTime
|
||||
});
|
||||
closePicker('nextFollowTime');
|
||||
};
|
||||
|
||||
// 地区选择器
|
||||
const onRegionChange = (e) => {
|
||||
emit('region-change', e);
|
||||
};
|
||||
|
||||
const onRegionConfirm = (e) => {
|
||||
emit('region-confirm', e);
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
openRegionPicker: () => {
|
||||
if (regionPicker.value) {
|
||||
regionPicker.value.open();
|
||||
}
|
||||
},
|
||||
setRegionIndexs: (indexs) => {
|
||||
if (regionPicker.value) {
|
||||
regionPicker.value.setIndexs(indexs, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
263
composables/useCustomerForm.js
Normal file
263
composables/useCustomerForm.js
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
import { ref, computed } from 'vue';
|
||||
import {
|
||||
getRegionTree,
|
||||
getCustomerIntentDict,
|
||||
getCustomerIntentLevelDict,
|
||||
getCustomerSourceDict,
|
||||
getCustomerTypeDict,
|
||||
getCustomerStatusDict,
|
||||
getWechatList
|
||||
} from '@/common/api';
|
||||
|
||||
/**
|
||||
* 客户表单共享逻辑
|
||||
*/
|
||||
export function useCustomerForm() {
|
||||
// 字典数据
|
||||
const customerTypeOptions = ref([]);
|
||||
const sourceOptions = ref([]);
|
||||
const intentOptions = ref([]);
|
||||
const intentLevelOptions = ref([]);
|
||||
const customerStatusOptions = ref([]);
|
||||
const workWechatOptions = ref([]);
|
||||
|
||||
// 地区数据
|
||||
const provinces = ref([]);
|
||||
const citys = ref([]);
|
||||
const areas = ref([]);
|
||||
const regionLoading = ref(true);
|
||||
|
||||
// 计算属性:返回地址列表
|
||||
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
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('加载字典数据失败:', err);
|
||||
uni.showToast({
|
||||
title: '加载字典数据失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 加载微信列表数据
|
||||
const loadWechatList = async () => {
|
||||
try {
|
||||
const res = await getWechatList();
|
||||
const rows = res?.rows || [];
|
||||
|
||||
workWechatOptions.value = rows.map(item => ({
|
||||
label: item.nickName ? `${item.nickName} (${item.wechatId})` : item.wechatId,
|
||||
value: String(item.id),
|
||||
id: item.id,
|
||||
wechatId: item.wechatId
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('加载微信列表失败:', err);
|
||||
uni.showToast({
|
||||
title: '加载微信列表失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 加载地区树数据
|
||||
const loadRegionTree = async () => {
|
||||
try {
|
||||
const res = await getRegionTree();
|
||||
const regionTree = res;
|
||||
|
||||
provinces.value = regionTree.sort((left, right) =>
|
||||
(Number(left.code || left.id) > Number(right.code || right.id) ? 1 : -1)
|
||||
);
|
||||
|
||||
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 = (formData, pickersRef) => {
|
||||
if (!formData.regionIds || formData.regionIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [provinceId, cityId, districtId] = formData.regionIds;
|
||||
|
||||
const provinceIndex = provinces.value.findIndex(item => Number(item.id) === Number(provinceId));
|
||||
if (provinceIndex >= 0) {
|
||||
citys.value = provinces.value[provinceIndex]?.children || [];
|
||||
const cityIndex = citys.value.findIndex(item => Number(item.id) === Number(cityId));
|
||||
if (cityIndex >= 0) {
|
||||
areas.value = citys.value[cityIndex]?.children || [];
|
||||
const districtIndex = areas.value.findIndex(item => Number(item.id) === Number(districtId));
|
||||
if (districtIndex >= 0 && pickersRef) {
|
||||
pickersRef.setRegionIndexs([provinceIndex, cityIndex, districtIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理地区选择器change事件
|
||||
const handleRegionChange = (e, pickersRef) => {
|
||||
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 (pickersRef) {
|
||||
pickersRef.setRegionIndexs([index, 0, 0]);
|
||||
}
|
||||
} else if (columnIndex === 1) {
|
||||
areas.value = citys.value[index]?.children || [];
|
||||
if (pickersRef) {
|
||||
pickersRef.setRegionIndexs(indexs);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理地区选择器confirm事件
|
||||
const handleRegionConfirm = (e, formData) => {
|
||||
const { value } = e;
|
||||
|
||||
if (value && value.length > 0) {
|
||||
const province = value[0];
|
||||
const city = value[1];
|
||||
const district = value[2];
|
||||
|
||||
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.region = regionNames.join('/');
|
||||
formData.regionIds = regionIds;
|
||||
} else {
|
||||
formData.region = '';
|
||||
formData.regionIds = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 打开地区选择器
|
||||
const openRegionPicker = (formData, pickersRef) => {
|
||||
if (regionLoading.value || provinces.value.length === 0) {
|
||||
uni.showToast({
|
||||
title: '地区数据加载中,请稍候',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.regionIds && formData.regionIds.length > 0) {
|
||||
handlePickValueDefault(formData, pickersRef);
|
||||
} 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 (pickersRef) {
|
||||
pickersRef.openRegionPicker();
|
||||
if (!formData.regionIds || formData.regionIds.length === 0) {
|
||||
setTimeout(() => {
|
||||
if (pickersRef) {
|
||||
pickersRef.setRegionIndexs([0, 0, 0]);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// 数据
|
||||
customerTypeOptions,
|
||||
sourceOptions,
|
||||
intentOptions,
|
||||
intentLevelOptions,
|
||||
customerStatusOptions,
|
||||
workWechatOptions,
|
||||
provinces,
|
||||
citys,
|
||||
areas,
|
||||
regionLoading,
|
||||
addressList,
|
||||
// 方法
|
||||
loadDictData,
|
||||
loadWechatList,
|
||||
loadRegionTree,
|
||||
handlePickValueDefault,
|
||||
handleRegionChange,
|
||||
handleRegionConfirm,
|
||||
openRegionPicker
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +82,13 @@
|
|||
"navigationBarTitleText": "客户信息",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/customer/edit/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "编辑客户",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -124,7 +124,7 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import { getCustomerDetail, getCustomerFollowupList, getCustomerProjects } from '@/common/api';
|
||||
import FollowupTab from '@/components/customer-detail/FollowupTab.vue';
|
||||
import ProjectsTab from '@/components/customer-detail/ProjectsTab.vue';
|
||||
|
|
@ -318,12 +318,27 @@ const handleMore = () => {
|
|||
uni.showActionSheet({
|
||||
itemList: ['编辑客户', '删除客户', '分享客户'],
|
||||
success: (res) => {
|
||||
console.log('选择了第' + (res.tapIndex + 1) + '个选项');
|
||||
if (res.tapIndex === 0) {
|
||||
// 编辑客户
|
||||
uni.navigateTo({
|
||||
url: `/pages/edit-customer/index?id=${customerId.value}`
|
||||
url: `/pages/customer/edit/index?id=${customerId.value}`
|
||||
});
|
||||
} else if (res.tapIndex === 1) {
|
||||
// 删除客户
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除该客户吗?',
|
||||
success: (modalRes) => {
|
||||
if (modalRes.confirm) {
|
||||
// TODO: 实现删除客户功能
|
||||
uni.$uv.toast('删除功能待实现');
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (res.tapIndex === 2) {
|
||||
// 分享客户
|
||||
// TODO: 实现分享客户功能
|
||||
uni.$uv.toast('分享功能待实现');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -333,6 +348,17 @@ const handleMore = () => {
|
|||
onMounted(() => {
|
||||
// 数据已在 onLoad 中加载
|
||||
});
|
||||
|
||||
// 页面显示时刷新数据(从编辑页返回时)
|
||||
onShow(() => {
|
||||
if (customerId.value) {
|
||||
loadCustomerDetail();
|
||||
// 如果当前在跟进动态标签页,也刷新跟进列表
|
||||
if (activeTab.value === 'followup') {
|
||||
loadFollowupList();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
406
pages/customer/edit/index.vue
Normal file
406
pages/customer/edit/index.vue
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
<template>
|
||||
<view class="edit-customer-page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<CustomerFormNavbar title="编辑客户" @cancel="handleCancel" />
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<scroll-view class="content-scroll" scroll-y>
|
||||
<view class="scroll-content">
|
||||
<!-- 客户信息部分 -->
|
||||
<CustomerBasicInfo
|
||||
:form-data="formData"
|
||||
:customer-type-options="customerTypeOptions"
|
||||
:intent-level-options="intentLevelOptions"
|
||||
:customer-status-options="customerStatusOptions"
|
||||
@update:form-data="formData = $event"
|
||||
@open-picker="handleOpenPicker"
|
||||
/>
|
||||
|
||||
<!-- 其他信息部分 -->
|
||||
<CustomerOtherInfo
|
||||
:form-data="formData"
|
||||
@update:form-data="formData = $event"
|
||||
@open-picker="handleOpenPicker"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<view class="save-button-wrapper">
|
||||
<button class="save-button" @click="handleSave" :disabled="saving">保存</button>
|
||||
</view>
|
||||
|
||||
<!-- 选择器组件 -->
|
||||
<CustomerPickers
|
||||
:show-customer-type-picker="showCustomerTypePicker"
|
||||
:show-source-picker="showSourcePicker"
|
||||
:show-intent-picker="showIntentPicker"
|
||||
:show-intent-level-picker="showIntentLevelPicker"
|
||||
:show-customer-status-picker="showCustomerStatusPicker"
|
||||
:show-work-wechat-picker="showWorkWechatPicker"
|
||||
:show-next-follow-time-picker="showNextFollowTimePicker"
|
||||
:customer-type-options="customerTypeOptions"
|
||||
:source-options="sourceOptions"
|
||||
:intent-options="intentOptions"
|
||||
:intent-level-options="intentLevelOptions"
|
||||
:customer-status-options="customerStatusOptions"
|
||||
:work-wechat-options="workWechatOptions"
|
||||
:address-list="addressList"
|
||||
:region-loading="regionLoading"
|
||||
:form-data="formData"
|
||||
ref="pickersRef"
|
||||
@update:form-data="formData = $event"
|
||||
@close-picker="handleClosePicker"
|
||||
@region-confirm="onRegionConfirm"
|
||||
@region-change="onRegionChange"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import { updateCustomer, getCustomerDetail } from '@/common/api';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useCustomerForm } from '@/composables/useCustomerForm';
|
||||
import CustomerFormNavbar from '@/components/customer-form/CustomerFormNavbar.vue';
|
||||
import CustomerBasicInfo from '@/components/customer-form/CustomerBasicInfo.vue';
|
||||
import CustomerOtherInfo from '@/components/customer-form/CustomerOtherInfo.vue';
|
||||
import CustomerPickers from '@/components/customer-form/CustomerPickers.vue';
|
||||
|
||||
// 使用共享逻辑
|
||||
const {
|
||||
customerTypeOptions,
|
||||
sourceOptions,
|
||||
intentOptions,
|
||||
intentLevelOptions,
|
||||
customerStatusOptions,
|
||||
workWechatOptions,
|
||||
addressList,
|
||||
regionLoading,
|
||||
loadDictData,
|
||||
loadWechatList,
|
||||
loadRegionTree,
|
||||
handlePickValueDefault,
|
||||
handleRegionChange,
|
||||
handleRegionConfirm,
|
||||
openRegionPicker
|
||||
} = useCustomerForm();
|
||||
|
||||
// 客户ID
|
||||
const customerId = ref('');
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
id: null,
|
||||
customerType: '',
|
||||
name: '',
|
||||
mobile: '',
|
||||
wechat: '',
|
||||
source: '',
|
||||
intents: [],
|
||||
intentLevel: '',
|
||||
customerStatus: '',
|
||||
region: '',
|
||||
regionIds: [],
|
||||
workWechat: '',
|
||||
workWechatId: null,
|
||||
rating: 0,
|
||||
remark: '',
|
||||
concern: '',
|
||||
pain: '',
|
||||
attention: '',
|
||||
demand: '',
|
||||
nextFollowTime: ''
|
||||
});
|
||||
|
||||
// 显示状态
|
||||
const saving = ref(false);
|
||||
const loading = 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 pickersRef = ref(null);
|
||||
|
||||
// 获取页面参数
|
||||
onLoad((options) => {
|
||||
if (options && options.id) {
|
||||
customerId.value = options.id;
|
||||
loadCustomerDetail();
|
||||
}
|
||||
});
|
||||
|
||||
// 加载客户详情
|
||||
const loadCustomerDetail = async () => {
|
||||
if (!customerId.value) return;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getCustomerDetail(customerId.value);
|
||||
if (res) {
|
||||
// 将后端数据转换为表单数据格式
|
||||
formData.value = {
|
||||
id: res.id,
|
||||
customerType: res.type || '',
|
||||
name: res.name || '',
|
||||
mobile: res.mobile || '',
|
||||
wechat: res.wechat || '',
|
||||
source: res.source || '',
|
||||
intents: Array.isArray(res.intents) ? res.intents : (res.intents ? res.intents.split(',') : []),
|
||||
intentLevel: res.intentLevel || res.customerIntentLevel || '',
|
||||
customerStatus: res.customerStatus || res.status || '',
|
||||
region: res.region || '',
|
||||
regionIds: res.regionIds || [],
|
||||
workWechat: res.workWechat || '',
|
||||
workWechatId: res.workWechatId || null,
|
||||
rating: res.rating || 0,
|
||||
remark: res.remark || '',
|
||||
concern: res.concern || '',
|
||||
pain: res.pain || '',
|
||||
attention: res.attention || '',
|
||||
demand: res.demand || '',
|
||||
nextFollowTime: res.nextFollowTime || ''
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载客户详情失败:', error);
|
||||
uni.showToast({
|
||||
title: '加载客户详情失败',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载客户详情后初始化地区数据
|
||||
const initRegionData = () => {
|
||||
if (formData.value.regionIds && formData.value.regionIds.length > 0) {
|
||||
handlePickValueDefault(formData.value, pickersRef.value);
|
||||
}
|
||||
};
|
||||
|
||||
// 打开选择器
|
||||
const handleOpenPicker = (pickerType) => {
|
||||
switch (pickerType) {
|
||||
case 'customerType':
|
||||
showCustomerTypePicker.value = true;
|
||||
break;
|
||||
case 'source':
|
||||
showSourcePicker.value = true;
|
||||
break;
|
||||
case 'intent':
|
||||
showIntentPicker.value = true;
|
||||
break;
|
||||
case 'intentLevel':
|
||||
showIntentLevelPicker.value = true;
|
||||
break;
|
||||
case 'customerStatus':
|
||||
showCustomerStatusPicker.value = true;
|
||||
break;
|
||||
case 'region':
|
||||
openRegionPicker(formData.value, pickersRef.value);
|
||||
break;
|
||||
case 'workWechat':
|
||||
showWorkWechatPicker.value = true;
|
||||
break;
|
||||
case 'nextFollowTime':
|
||||
showNextFollowTimePicker.value = true;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭选择器
|
||||
const handleClosePicker = (pickerType) => {
|
||||
switch (pickerType) {
|
||||
case 'customerType':
|
||||
showCustomerTypePicker.value = false;
|
||||
break;
|
||||
case 'source':
|
||||
showSourcePicker.value = false;
|
||||
break;
|
||||
case 'intent':
|
||||
showIntentPicker.value = false;
|
||||
break;
|
||||
case 'intentLevel':
|
||||
showIntentLevelPicker.value = false;
|
||||
break;
|
||||
case 'customerStatus':
|
||||
showCustomerStatusPicker.value = false;
|
||||
break;
|
||||
case 'workWechat':
|
||||
showWorkWechatPicker.value = false;
|
||||
break;
|
||||
case 'nextFollowTime':
|
||||
showNextFollowTimePicker.value = false;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// uv-picker 的 change 事件处理(实现三级联动)
|
||||
const onRegionChange = (e) => {
|
||||
handleRegionChange(e, pickersRef.value);
|
||||
};
|
||||
|
||||
// uv-picker 的 confirm 事件处理
|
||||
const onRegionConfirm = (e) => {
|
||||
handleRegionConfirm(e, formData.value);
|
||||
};
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
loadRegionTree(),
|
||||
loadDictData(),
|
||||
loadWechatList()
|
||||
]);
|
||||
// 数据加载完成后初始化地区选择器
|
||||
initRegionData();
|
||||
});
|
||||
|
||||
// 保存
|
||||
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}`;
|
||||
};
|
||||
|
||||
const intentsArray = Array.isArray(formData.value.intents) ? formData.value.intents : [];
|
||||
const regionIdsArray = formData.value.regionIds || [];
|
||||
|
||||
const submitData = {
|
||||
id: formData.value.id,
|
||||
code: null,
|
||||
name: formData.value.name.trim(),
|
||||
status: formData.value.customerStatus || null,
|
||||
intentLevel: formData.value.intentLevel || null,
|
||||
mobile: formData.value.mobile.trim(),
|
||||
wechat: formData.value.wechat.trim() || null,
|
||||
source: formData.value.source || null,
|
||||
intents: intentsArray,
|
||||
followId: userId,
|
||||
remark: formData.value.remark.trim() || null,
|
||||
type: formData.value.customerType || '2',
|
||||
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,
|
||||
customerStatus: formData.value.customerStatus || null
|
||||
},
|
||||
nextFollowTime: formData.value.nextFollowTime || null,
|
||||
customerIntentLevel: formData.value.intentLevel || null,
|
||||
customerStatus: formData.value.customerStatus || null
|
||||
};
|
||||
|
||||
await updateCustomer(submitData);
|
||||
|
||||
uni.showToast({
|
||||
title: '更新成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 延迟返回,让用户看到成功提示
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
console.error('更新客户失败:', error);
|
||||
uni.showToast({
|
||||
title: error.message || '更新失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.edit-customer-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.content-scroll {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.scroll-content {
|
||||
padding: 16px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user