OfficeSystem/pages/customer/follow/add/index.vue
2025-11-10 10:49:55 +08:00

1452 lines
39 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-followup-page">
<!-- 自定义导航栏 -->
<view class="custom-navbar">
<view class="navbar-content">
<text class="nav-btn" @click="handleCancel"></text>
<text class="nav-title">{{ pageTitle }}</text>
<text class="nav-btn" style="opacity: 0;">占位</text>
</view>
</view>
<!-- 内容区域 -->
<scroll-view class="content-scroll" scroll-y>
<view class="form-container">
<!-- 图片上传 -->
<view class="form-section">
<view class="section-label">图片</view>
<view class="upload-area" @click="chooseImages">
<view class="upload-placeholder">
<text class="upload-icon">+</text>
</view>
</view>
<view class="upload-tips">
<text>请上传大小不超过200MB格式为png/jpg/jpeg 的文件</text>
<text>支持拖动上传和鼠标移入后 Ctrl+V 粘贴上传</text>
</view>
<!-- 图片预览 -->
<view class="images-preview" v-if="formData.pictures.length > 0">
<view
class="image-item"
v-for="(image, index) in formData.pictures"
:key="index"
>
<image :src="image" mode="aspectFill" class="preview-image" @click="previewImage(index)" />
<view class="remove-btn" @click.stop="removeImage(index)">✕</view>
</view>
</view>
</view>
<!-- 客户信息 -->
<view class="form-section">
<!-- 客户 -->
<view class="form-item">
<text class="form-label required">*客户</text>
<view class="form-input-wrapper" @click="openCustomerPicker">
<input
v-model="formData.customerName"
class="form-input"
placeholder="请选择客户"
disabled
placeholder-style="color: #999;"
/>
<text class="arrow"></text>
</view>
</view>
<!-- 客户状态 -->
<view class="form-item">
<text class="form-label required">*客户状态</text>
<view class="form-input-wrapper" @click="openStatusPicker">
<input
:value="getStatusText(formData.customerStatus)"
class="form-input"
placeholder="请选择客户状态"
disabled
placeholder-style="color: #999;"
/>
<text class="arrow"></text>
</view>
</view>
<!-- 意向强度 -->
<view class="form-item">
<text class="form-label required">*意向强度</text>
<view class="form-input-wrapper" @click="openIntentLevelPicker">
<input
:value="getIntentLevelText(formData.customerIntentLevel)"
class="form-input"
placeholder="请选择意向强度"
disabled
placeholder-style="color: #999;"
/>
<text class="arrow"></text>
</view>
</view>
<!-- 跟进方式 -->
<view class="form-item">
<text class="form-label required">*跟进方式</text>
<view class="form-input-wrapper" @click="openFollowTypePicker">
<input
:value="getFollowTypeText(formData.type)"
class="form-input"
placeholder="请选择跟进方式"
disabled
placeholder-style="color: #999;"
/>
<text class="arrow"></text>
</view>
</view>
<!-- 跟进内容 -->
<view class="form-item">
<text class="form-label required">*跟进内容</text>
<view class="textarea-wrapper">
<textarea
v-model="formData.content"
class="form-textarea"
placeholder="请输入跟进内容"
placeholder-style="color: #999;"
:maxlength="1000"
auto-height
/>
<view class="char-count">{{ formData.content.length }}/1000</view>
</view>
</view>
</view>
<!-- 附件列表 -->
<view class="form-section">
<view class="section-label">附件列表</view>
<view class="upload-area" @click="chooseFiles">
<view class="upload-placeholder">
<text class="upload-icon">+</text>
</view>
</view>
<view class="upload-tips">
<text>请上传大小不超过200MB格式为png/jpg/jpeg/gif/bmp/webp/ico/doc/docx/xls/xlsx/pdf/ppt/pptx/mp3/wav/m4a/ogg/flac/aac/mp4/avi/mov/wmv/flv/mpeg/mpg/m4v/webm/mkv/exe/zip/rar/7z 的文件</text>
<text>支持拖动上传和鼠标移入后 Ctrl+V 粘贴上传</text>
</view>
<!-- 文件列表 -->
<view class="files-list" v-if="formData.attaches.length > 0">
<view
class="file-item"
v-for="(file, index) in formData.attaches"
:key="index"
@click="previewFile(file)"
>
<text class="file-icon">{{ getFileIcon(file.name) }}</text>
<view class="file-info">
<text class="file-name">{{ file.name }}</text>
<text class="file-size" v-if="file.size > 0">{{ formatFileSize(file.size) }}</text>
</view>
<view class="remove-btn" @click.stop="removeFile(index)">✕</view>
</view>
</view>
</view>
<!-- 时间信息 -->
<view class="form-section">
<!-- 跟进时间 -->
<view class="form-item">
<text class="form-label required">*跟进时间</text>
<view class="form-input-wrapper" @click="openFollowTimePicker">
<input
:value="formatDateTime(formData.followTime)"
class="form-input"
placeholder="请选择跟进时间"
disabled
placeholder-style="color: #999;"
/>
<text class="arrow"></text>
</view>
</view>
<!-- 下次跟进 -->
<view class="form-item">
<text class="form-label required">*下次跟进</text>
<view class="form-input-wrapper" @click="openNextFollowTimePicker">
<input
:value="formatDateTime(formData.nextFollowTime)"
class="form-input"
placeholder="请选择下次跟进时间"
disabled
placeholder-style="color: #999;"
/>
<text class="arrow"></text>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 选择器组件 -->
<FollowupPickers
:show-customer-picker="showCustomerPicker"
:show-status-picker="showStatusPicker"
:show-intent-level-picker="showIntentLevelPicker"
:show-follow-type-picker="showFollowTypePicker"
:customer-list="customerList"
:status-options="statusOptions"
:intent-level-options="intentLevelOptions"
:follow-type-options="followTypeOptions"
:form-data="formData"
@update:form-data="formData = $event"
@close-picker="handleClosePicker"
/>
<!-- 跟进时间选择器弹窗(移动端) -->
<view v-if="showFollowTimePicker" class="modal-mask" @click="showFollowTimePicker = false">
<view class="modal-content datetime-modal" @click.stop>
<view class="modal-title">选择跟进时间</view>
<picker-view
class="datetime-picker"
:value="followTimePickerValue"
@change="onFollowTimePickerChange"
>
<picker-view-column>
<view v-for="(year, index) in dateTimePickerRange[0]" :key="index" class="picker-item-small">{{ year }}年</view>
</picker-view-column>
<picker-view-column>
<view v-for="(month, index) in dateTimePickerRange[1]" :key="index" class="picker-item-small">{{ month }}月</view>
</picker-view-column>
<picker-view-column>
<view v-for="(day, index) in dateTimePickerRange[2]" :key="index" class="picker-item-small">{{ day }}日</view>
</picker-view-column>
<picker-view-column>
<view v-for="(hour, index) in dateTimePickerRange[3]" :key="index" class="picker-item-small">{{ hour }}时</view>
</picker-view-column>
<picker-view-column>
<view v-for="(minute, index) in dateTimePickerRange[4]" :key="index" class="picker-item-small">{{ minute }}分</view>
</picker-view-column>
</picker-view>
<view class="modal-actions">
<text class="modal-btn cancel-btn" @click="showFollowTimePicker = false">取消</text>
<text class="modal-btn confirm-btn" @click="confirmFollowTime">确定</text>
</view>
</view>
</view>
<!-- 下次跟进时间选择器弹窗(移动端) -->
<view v-if="showNextFollowTimePicker" class="modal-mask" @click="showNextFollowTimePicker = false">
<view class="modal-content datetime-modal" @click.stop>
<view class="modal-title">选择下次跟进时间</view>
<picker-view
class="datetime-picker"
:value="nextFollowTimePickerValue"
@change="onNextFollowTimePickerChange"
>
<picker-view-column>
<view v-for="(year, index) in dateTimePickerRange[0]" :key="index" class="picker-item-small">{{ year }}年</view>
</picker-view-column>
<picker-view-column>
<view v-for="(month, index) in dateTimePickerRange[1]" :key="index" class="picker-item-small">{{ month }}月</view>
</picker-view-column>
<picker-view-column>
<view v-for="(day, index) in dateTimePickerRange[2]" :key="index" class="picker-item-small">{{ day }}日</view>
</picker-view-column>
<picker-view-column>
<view v-for="(hour, index) in dateTimePickerRange[3]" :key="index" class="picker-item-small">{{ hour }}时</view>
</picker-view-column>
<picker-view-column>
<view v-for="(minute, index) in dateTimePickerRange[4]" :key="index" class="picker-item-small">{{ minute }}分</view>
</picker-view-column>
</picker-view>
<view class="modal-actions">
<text class="modal-btn cancel-btn" @click="showNextFollowTimePicker = false">取消</text>
<text class="modal-btn confirm-btn" @click="confirmNextFollowTime">确定</text>
</view>
</view>
</view>
<!-- 底部按钮 -->
<view class="button-wrapper">
<view class="button-group">
<button class="btn cancel-btn" @click="handleCancel">取消</button>
<button class="btn confirm-btn" @click="handleSubmit" :disabled="!canSubmit || submitting">
{{ submitting ? '提交中...' : '确定' }}
</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { chooseAndUploadImages, batchUploadFilesToQiniu } from '@/utils/qiniu.js';
import {
createFollowup,
getCustomerList,
getCustomerStatusDict,
getCustomerIntentLevelDict,
getCustomerFollowTypeDict
} from '@/common/api/customer.js';
import FollowupPickers from '@/components/followup-form/FollowupPickers.vue';
// 表单数据
const formData = ref({
customerId: '',
customerName: '',
customerStatus: '',
customerIntentLevel: '',
type: '',
content: '',
pictures: [],
attaches: [],
followTime: '',
nextFollowTime: ''
});
// 弹窗状态
const showCustomerPicker = ref(false);
const showStatusPicker = ref(false);
const showIntentLevelPicker = ref(false);
const showFollowTypePicker = ref(false);
const showFollowTimePicker = ref(false);
const showNextFollowTimePicker = ref(false);
// 选项数据
const customerList = ref([]);
const statusOptions = ref([]);
const intentLevelOptions = ref([]);
const followTypeOptions = ref([]);
// 提交状态
const submitting = ref(false);
// 页面标题
const pageTitle = ref('新建跟进记录');
// 是否可以提交
const canSubmit = computed(() => {
return formData.value.customerId &&
formData.value.customerStatus &&
formData.value.customerIntentLevel &&
formData.value.type &&
formData.value.content.trim() &&
formData.value.followTime &&
formData.value.nextFollowTime;
});
// 页面加载
onLoad((options) => {
// 判断是新建还是修改
if (options.id) {
// 如果有ID说明是修改模式
pageTitle.value = '修改跟进记录';
// TODO: 加载跟进记录详情
} else {
// 新建模式
pageTitle.value = '新建跟进记录';
}
// 如果传入了客户ID预填充客户信息
if (options.customerId) {
formData.value.customerId = options.customerId;
formData.value.customerName = decodeURIComponent(options.customerName || '');
}
// 加载字典数据
loadDictData();
// 加载客户列表
loadCustomerList();
// 设置默认跟进时间为当前时间
const now = new Date();
formData.value.followTime = formatDateTimeForInput(now);
// 设置默认下次跟进时间为7天后
const nextWeek = new Date(now);
nextWeek.setDate(nextWeek.getDate() + 7);
formData.value.nextFollowTime = formatDateTimeForInput(nextWeek);
});
// 加载字典数据
const loadDictData = async () => {
try {
const [statusRes, intentLevelRes, followTypeRes] = await Promise.all([
getCustomerStatusDict(),
getCustomerIntentLevelDict(),
getCustomerFollowTypeDict()
]);
if (statusRes ) {
statusOptions.value = statusRes|| [];
}
if (intentLevelRes ) {
intentLevelOptions.value = intentLevelRes || [];
}
if (followTypeRes ) {
followTypeOptions.value = followTypeRes || [];
}
} catch (err) {
console.error('加载字典数据失败:', err);
uni.showToast({
title: '加载字典数据失败',
icon: 'none'
});
}
};
// 加载客户列表
const loadCustomerList = async () => {
try {
const res = await getCustomerList({ pageNum: 1, pageSize: 100 });
console.log('@@@@',res);
if (res && res.data && res.data.rows) {
customerList.value = res.data.rows;
}
} catch (err) {
console.error('加载客户列表失败:', err);
}
};
// 打开客户选择器
const openCustomerPicker = () => {
showCustomerPicker.value = true;
};
// 打开状态选择器
const openStatusPicker = () => {
showStatusPicker.value = true;
};
// 打开意向强度选择器
const openIntentLevelPicker = () => {
showIntentLevelPicker.value = true;
};
// 打开跟进方式选择器
const openFollowTypePicker = () => {
showFollowTypePicker.value = true;
};
// 关闭选择器
const handleClosePicker = (pickerType) => {
switch (pickerType) {
case 'customer':
showCustomerPicker.value = false;
break;
case 'status':
showStatusPicker.value = false;
break;
case 'intentLevel':
showIntentLevelPicker.value = false;
break;
case 'followType':
showFollowTypePicker.value = false;
break;
}
};
// 生成日期时间选择器的范围数据
const generateDateTimePickerRange = () => {
const years = [];
const months = [];
const days = [];
const hours = [];
const minutes = [];
const currentYear = new Date().getFullYear();
for (let i = currentYear - 1; i <= currentYear + 1; i++) {
years.push(i.toString());
}
for (let i = 1; i <= 12; i++) {
months.push(String(i).padStart(2, '0'));
}
for (let i = 1; i <= 31; i++) {
days.push(String(i).padStart(2, '0'));
}
for (let i = 0; i < 24; i++) {
hours.push(String(i).padStart(2, '0'));
}
for (let i = 0; i < 60; i += 1) {
minutes.push(String(i).padStart(2, '0'));
}
return [years, months, days, hours, minutes];
};
// 日期时间选择器范围数据(需要在模板中使用)
const dateTimePickerRange = generateDateTimePickerRange();
// 获取当前日期时间在picker中的索引
const getDateTimePickerValue = (dateTime) => {
if (!dateTime) {
const now = new Date();
return [
dateTimePickerRange[0].indexOf(now.getFullYear().toString()),
now.getMonth(),
now.getDate() - 1,
now.getHours(),
now.getMinutes()
];
}
const d = new Date(dateTime);
return [
dateTimePickerRange[0].indexOf(d.getFullYear().toString()),
d.getMonth(),
d.getDate() - 1,
d.getHours(),
d.getMinutes()
];
};
const followTimePickerValue = ref(getDateTimePickerValue(formData.value.followTime));
const nextFollowTimePickerValue = ref(getDateTimePickerValue(formData.value.nextFollowTime));
// 跟进时间选择器变化picker-view
const onFollowTimePickerChange = (e) => {
followTimePickerValue.value = e.detail.value;
};
// 确认跟进时间
const confirmFollowTime = () => {
const [yearIdx, monthIdx, dayIdx, hourIdx, minuteIdx] = followTimePickerValue.value;
const year = dateTimePickerRange[0][yearIdx];
const month = dateTimePickerRange[1][monthIdx];
const day = dateTimePickerRange[2][dayIdx];
const hour = dateTimePickerRange[3][hourIdx];
const minute = dateTimePickerRange[4][minuteIdx];
formData.value.followTime = `${year}-${month}-${day} ${hour}:${minute}:00`;
showFollowTimePicker.value = false;
};
// 下次跟进时间选择器变化picker-view
const onNextFollowTimePickerChange = (e) => {
nextFollowTimePickerValue.value = e.detail.value;
};
// 确认下次跟进时间
const confirmNextFollowTime = () => {
const [yearIdx, monthIdx, dayIdx, hourIdx, minuteIdx] = nextFollowTimePickerValue.value;
const year = dateTimePickerRange[0][yearIdx];
const month = dateTimePickerRange[1][monthIdx];
const day = dateTimePickerRange[2][dayIdx];
const hour = dateTimePickerRange[3][hourIdx];
const minute = dateTimePickerRange[4][minuteIdx];
formData.value.nextFollowTime = `${year}-${month}-${day} ${hour}:${minute}:00`;
showNextFollowTimePicker.value = false;
};
// 打开跟进时间选择器
const openFollowTimePicker = () => {
// #ifdef H5
// H5平台使用input type="datetime-local"
const currentDate = formData.value.followTime || new Date();
const d = currentDate instanceof Date ? currentDate : new Date(currentDate);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
const dateStr = `${year}-${month}-${day}T${hours}:${minutes}`;
const input = document.createElement('input');
input.type = 'datetime-local';
input.value = dateStr;
input.onchange = (e) => {
if (e.target.value) {
formData.value.followTime = e.target.value.replace('T', ' ') + ':00';
}
};
input.click();
// #endif
// #ifndef H5
// 其他平台使用picker-view组件
followTimePickerValue.value = getDateTimePickerValue(formData.value.followTime);
showFollowTimePicker.value = true;
// #endif
};
// 打开下次跟进时间选择器
const openNextFollowTimePicker = () => {
// #ifdef H5
// H5平台使用input type="datetime-local"
const currentDate = formData.value.nextFollowTime || new Date();
const d = currentDate instanceof Date ? currentDate : new Date(currentDate);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
const dateStr = `${year}-${month}-${day}T${hours}:${minutes}`;
const input = document.createElement('input');
input.type = 'datetime-local';
input.value = dateStr;
input.onchange = (e) => {
if (e.target.value) {
formData.value.nextFollowTime = e.target.value.replace('T', ' ') + ':00';
}
};
input.click();
// #endif
// #ifndef H5
// 其他平台使用picker-view组件
nextFollowTimePickerValue.value = getDateTimePickerValue(formData.value.nextFollowTime);
showNextFollowTimePicker.value = true;
// #endif
};
// 选择图片并自动上传
const chooseImages = async () => {
try {
const remainingCount = 9 - formData.value.pictures.length;
if (remainingCount <= 0) {
uni.showToast({
title: '最多只能添加9张图片',
icon: 'none'
});
return;
}
const urls = await chooseAndUploadImages({
count: remainingCount,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera']
});
formData.value.pictures = [...formData.value.pictures, ...urls];
} catch (err) {
console.error('选择或上传图片失败:', err);
uni.showToast({
title: err.message || '选择图片失败',
icon: 'none'
});
}
};
// 预览图片
const previewImage = (index) => {
uni.previewImage({
urls: formData.value.pictures,
current: index
});
};
// 删除图片
const removeImage = (index) => {
formData.value.pictures.splice(index, 1);
};
// 选择文件(参考任务提交页面的实现)
const chooseFiles = async () => {
const remainingCount = 10 - formData.value.attaches.length;
if (remainingCount <= 0) {
uni.showToast({
title: '最多只能添加10个文件',
icon: 'none'
});
return;
}
// 优先使用 uni.chooseFileH5和部分平台支持
// #ifdef H5 || MP-WEIXIN || APP-PLUS
try {
uni.chooseFile({
count: remainingCount,
extension: ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.zip', '.rar', '.7z', '.jpg', '.png', '.jpeg', '.gif', '.bmp', '.webp', '.ico', '.mp3', '.wav', '.m4a', '.ogg', '.flac', '.aac', '.mp4', '.avi', '.mov', '.wmv', '.flv', '.mpeg', '.mpg', '.m4v', '.webm', '.mkv', '.exe'],
success: async (res) => {
try {
uni.showLoading({
title: '上传中...',
mask: true
});
// 批量上传文件到七牛云
const uploadResults = await batchUploadFilesToQiniu(
res.tempFiles.map(file => ({
path: file.path,
name: file.name
}))
);
// 将上传结果添加到文件列表
const newFiles = uploadResults.map(result => ({
name: result.name,
path: result.url, // 保存七牛云URL
size: result.size
}));
formData.value.attaches = [...formData.value.attaches, ...newFiles];
uni.hideLoading();
uni.showToast({
title: `成功添加${newFiles.length}个文件`,
icon: 'success'
});
} catch (error) {
uni.hideLoading();
console.error('上传文件失败:', error);
uni.showToast({
title: error.message || '上传文件失败',
icon: 'none'
});
}
},
fail: (err) => {
console.error('选择文件失败:', err);
// 如果uni.chooseFile不支持尝试使用原生方法
chooseFilesNative();
}
});
} catch (error) {
// 如果不支持uni.chooseFile使用原生方法
chooseFilesNative();
}
// #endif
// #ifndef H5 || MP-WEIXIN || APP-PLUS
// 其他平台使用原生方法
chooseFilesNative();
// #endif
};
// 原生文件选择方法(安卓平台)
const chooseFilesNative = async () => {
const remainingCount = 10 - formData.value.attaches.length;
// 安卓平台使用 plus API 调用原生文件选择器
if (typeof plus !== 'undefined') {
try {
const Intent = plus.android.importClass('android.content.Intent');
const main = plus.android.runtimeMainActivity();
// 创建文件选择 Intent
const intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType('*/*');
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); // 允许多选
// 启动文件选择器
main.startActivityForResult(intent, 1001);
// 监听文件选择结果
const originalOnActivityResult = main.onActivityResult;
main.onActivityResult = async (requestCode, resultCode, data) => {
if (requestCode === 1001) {
if (resultCode === -1 && data) { // RESULT_OK = -1
try {
const clipData = data.getClipData();
const files = [];
// 获取文件名的方法
const getFileName = (uri) => {
try {
const cursor = main.getContentResolver().query(uri, null, null, null, null);
if (cursor && cursor.moveToFirst()) {
const nameIndex = cursor.getColumnIndex('_display_name');
if (nameIndex !== -1) {
const fileName = cursor.getString(nameIndex);
cursor.close();
return fileName;
}
cursor.close();
}
} catch (e) {
console.error('获取文件名失败:', e);
}
return null;
};
if (clipData) {
// 多选文件
const count = clipData.getItemCount();
for (let i = 0; i < count && files.length < remainingCount; i++) {
const item = clipData.getItemAt(i);
const uri = item.getUri();
const uriString = uri.toString();
// 获取文件名
let fileName = getFileName(uri) || `file_${Date.now()}_${i}`;
files.push({
name: fileName,
path: uriString, // 保存 URI 字符串
size: 0
});
}
} else {
// 单选文件
const uri = data.getData();
if (uri) {
const uriString = uri.toString();
let fileName = getFileName(uri) || `file_${Date.now()}`;
files.push({
name: fileName,
path: uriString, // 保存 URI 字符串
size: 0
});
}
}
if (files.length > 0) {
// 显示上传中提示
uni.showLoading({
title: '上传中...',
mask: true
});
try {
// 批量上传文件到七牛云
const uploadResults = await batchUploadFilesToQiniu(files);
// 将上传结果添加到文件列表
const newFiles = uploadResults.map(result => ({
name: result.name,
path: result.url, // 保存七牛云URL
size: result.size
}));
formData.value.attaches = [...formData.value.attaches, ...newFiles];
uni.hideLoading();
uni.showToast({
title: `成功添加${newFiles.length}个文件`,
icon: 'success'
});
} catch (uploadError) {
uni.hideLoading();
console.error('上传文件失败:', uploadError);
uni.showToast({
title: uploadError.message || '上传文件失败',
icon: 'none'
});
}
}
// 恢复原始的 onActivityResult
if (originalOnActivityResult) {
main.onActivityResult = originalOnActivityResult;
}
} catch (error) {
uni.hideLoading();
console.error('处理文件选择结果失败:', error);
uni.showToast({
title: '处理文件失败',
icon: 'none'
});
}
}
} else {
// 调用原始的 onActivityResult
if (originalOnActivityResult) {
originalOnActivityResult(requestCode, resultCode, data);
}
}
};
} catch (error) {
console.error('打开文件选择器失败:', error);
uni.showToast({
title: '文件选择功能暂不可用',
icon: 'none'
});
}
} else {
uni.showToast({
title: '当前环境不支持文件选择',
icon: 'none'
});
}
};
// 预览/下载文件(参考任务提交页面的实现)
const previewFile = (file) => {
if (!file.path) {
uni.showToast({
title: '文件路径不存在',
icon: 'none'
});
return;
}
// 如果是图片,使用预览图片功能
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
const ext = file.name.split('.').pop()?.toLowerCase();
if (ext && imageExts.includes(ext)) {
uni.previewImage({
urls: [file.path],
current: file.path
});
} else {
// 其他文件类型,尝试打开或下载
// #ifdef H5
window.open(file.path, '_blank');
// #endif
// #ifdef APP-PLUS
plus.runtime.openURL(file.path);
// #endif
// #ifndef H5 || APP-PLUS
uni.showToast({
title: '点击下载文件',
icon: 'none'
});
// 可以调用下载API
uni.downloadFile({
url: file.path,
success: (res) => {
if (res.statusCode === 200) {
uni.openDocument({
filePath: res.tempFilePath,
success: () => {
console.log('打开文档成功');
},
fail: (err) => {
console.error('打开文档失败:', err);
uni.showToast({
title: '无法打开此文件',
icon: 'none'
});
}
});
}
},
fail: (err) => {
console.error('下载文件失败:', err);
uni.showToast({
title: '下载文件失败',
icon: 'none'
});
}
});
// #endif
}
};
// 删除文件
const removeFile = (index) => {
formData.value.attaches.splice(index, 1);
};
// 获取文件图标
const getFileIcon = (fileName) => {
const ext = fileName.split('.').pop()?.toLowerCase();
const iconMap = {
'pdf': '📄',
'doc': '📝',
'docx': '📝',
'xls': '📊',
'xlsx': '📊',
'ppt': '📊',
'pptx': '📊',
'zip': '📦',
'rar': '📦',
'7z': '📦',
'jpg': '🖼️',
'jpeg': '🖼️',
'png': '🖼️',
'gif': '🖼️',
'mp4': '🎬',
'avi': '🎬',
'mov': '🎬',
'mp3': '🎵',
'wav': '🎵'
};
return iconMap[ext] || '📎';
};
// 格式化文件大小
const formatFileSize = (bytes) => {
if (!bytes || bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
};
// 格式化日期时间(显示用)
const formatDateTime = (dateTime) => {
if (!dateTime) return '';
const date = new Date(dateTime);
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 formatDateTimeForInput = (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 getStatusText = (value) => {
const item = statusOptions.value.find(item => item.dictValue === value);
return item ? item.dictLabel : '';
};
// 获取意向强度文本
const getIntentLevelText = (value) => {
const item = intentLevelOptions.value.find(item => item.dictValue === value);
return item ? item.dictLabel : '';
};
// 获取跟进方式文本
const getFollowTypeText = (value) => {
const item = followTypeOptions.value.find(item => item.dictValue === value);
return item ? item.dictLabel : '';
};
// 提交表单
const handleSubmit = async () => {
if (!canSubmit.value) {
uni.showToast({
title: '请填写完整信息',
icon: 'none'
});
return;
}
submitting.value = true;
try {
const submitData = {
customerId: formData.value.customerId,
type: formData.value.type,
content: formData.value.content,
picture: formData.value.pictures.join(','),
attaches: formData.value.attaches.map(f => f.path).join(','),
followTime: formData.value.followTime,
nextFollowTime: formData.value.nextFollowTime,
customerStatus: formData.value.customerStatus,
customerIntentLevel: formData.value.customerIntentLevel
};
await createFollowup(submitData);
uni.showToast({
title: '创建成功',
icon: 'success'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
} catch (err) {
console.error('创建跟进记录失败:', err);
uni.showToast({
title: err.message || '创建失败,请重试',
icon: 'none'
});
} finally {
submitting.value = false;
}
};
// 取消
const handleCancel = () => {
uni.showModal({
title: '提示',
content: '确定要取消吗?未保存的内容将丢失',
success: (res) => {
if (res.confirm) {
uni.navigateBack();
}
}
});
};
</script>
<style scoped lang="scss">
.add-followup-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.custom-navbar {
background-color: #fff;
border-bottom: 1px solid #e0e0e0;
padding-top: var(--status-bar-height, 0);
}
.navbar-content {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
padding: 0 16px;
}
.nav-btn {
font-size: 24px;
color: #333;
width: 40px;
text-align: center;
line-height: 44px;
}
.nav-title {
font-size: 18px;
font-weight: 600;
color: #333;
flex: 1;
text-align: center;
}
.content-scroll {
flex: 1;
overflow-y: auto;
}
.form-container {
padding: 16px;
}
.form-section {
background-color: #fff;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.section-label {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.form-item {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.form-label {
font-size: 14px;
color: #333;
margin-bottom: 8px;
display: block;
&.required::before {
content: '*';
color: #f56c6c;
margin-right: 4px;
}
}
.form-input-wrapper {
display: flex;
align-items: center;
background-color: #f5f5f5;
border-radius: 8px;
padding: 12px;
}
.form-input {
flex: 1;
font-size: 14px;
color: #333;
}
.arrow {
font-size: 20px;
color: #999;
margin-left: 8px;
}
.textarea-wrapper {
position: relative;
background-color: #f5f5f5;
border-radius: 8px;
padding: 12px;
}
.form-textarea {
width: 100%;
min-height: 100px;
font-size: 14px;
color: #333;
line-height: 1.5;
}
.char-count {
position: absolute;
bottom: 8px;
right: 12px;
font-size: 12px;
color: #999;
}
.upload-area {
width: 100%;
aspect-ratio: 1;
border: 2px dashed #d0d0d0;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
background-color: #fafafa;
margin-bottom: 8px;
}
.upload-placeholder {
display: flex;
align-items: center;
justify-content: center;
}
.upload-icon {
font-size: 48px;
color: #999;
}
.upload-tips {
display: flex;
flex-direction: column;
gap: 4px;
text {
font-size: 12px;
color: #999;
line-height: 1.5;
}
}
.images-preview {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.image-item {
position: relative;
width: calc((100% - 16px) / 3);
aspect-ratio: 1;
border-radius: 4px;
overflow: hidden;
}
.preview-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.remove-btn {
position: absolute;
top: 4px;
right: 4px;
width: 20px;
height: 20px;
background-color: rgba(0, 0, 0, 0.6);
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
line-height: 1;
}
.files-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 12px;
}
.file-item {
display: flex;
align-items: center;
padding: 12px;
background-color: #f5f5f5;
border-radius: 8px;
gap: 12px;
}
.file-icon {
font-size: 24px;
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.file-name {
font-size: 14px;
color: #333;
}
.file-size {
font-size: 12px;
color: #999;
}
.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%;
background-color: #fff;
border-radius: 16px 16px 0 0;
max-height: 70vh;
display: flex;
flex-direction: column;
}
.picker-modal {
padding: 0;
}
.modal-title {
font-size: 18px;
font-weight: 600;
color: #333;
text-align: center;
padding: 16px;
border-bottom: 1px solid #e0e0e0;
}
.picker-scroll {
flex: 1;
max-height: 400px;
}
.picker-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
color: #333;
&.active {
color: #1976d2;
}
&:last-child {
border-bottom: none;
}
}
.check-icon {
color: #1976d2;
font-size: 18px;
}
.modal-actions {
display: flex;
padding: 16px;
border-top: 1px solid #e0e0e0;
gap: 12px;
}
.modal-btn {
flex: 1;
padding: 12px;
text-align: center;
border-radius: 8px;
font-size: 14px;
&.cancel-btn {
background-color: #f5f5f5;
color: #333;
}
&.confirm-btn {
background-color: #1976d2;
color: #fff;
}
}
.datetime-modal {
max-height: 60vh;
}
.datetime-picker {
height: 200px;
margin: 16px 0;
}
.picker-item-small {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
font-size: 14px;
color: #333;
}
.button-wrapper {
padding: 16px;
background-color: #fff;
border-top: 1px solid #e0e0e0;
}
.button-group {
display: flex;
gap: 12px;
}
.btn {
flex: 1;
padding: 14px;
border-radius: 8px;
font-size: 16px;
border: none;
&.cancel-btn {
background-color: #f5f5f5;
color: #333;
}
&.confirm-btn {
background-color: #1976d2;
color: #fff;
&:disabled {
background-color: #ccc;
color: #999;
}
}
}
</style>