OfficeSystem/pages/customer/follow/add/index.vue
2025-11-10 16:54:46 +08:00

1224 lines
31 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>
</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 readonly">
<input
v-model="formData.customerName"
class="form-input"
placeholder="客户信息"
disabled
placeholder-style="color: #999;"
/>
</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="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-status-picker="showStatusPicker"
:show-intent-level-picker="showIntentLevelPicker"
:show-follow-type-picker="showFollowTypePicker"
:status-options="statusOptions"
:intent-level-options="intentLevelOptions"
:follow-type-options="followTypeOptions"
:form-data="formData"
@update:form-data="formData = $event"
@close-picker="handleClosePicker"
/>
<!-- 跟进时间选择器 -->
<uv-datetime-picker
ref="followTimePickerRef"
v-model="followTimeValue"
mode="datetime"
@confirm="onFollowTimeConfirm"
></uv-datetime-picker>
<!-- 下次跟进时间选择器 -->
<uv-datetime-picker
ref="nextFollowTimePickerRef"
v-model="nextFollowTimeValue"
mode="datetime"
@confirm="onNextFollowTimeConfirm"
></uv-datetime-picker>
<!-- 底部按钮 -->
<view class="button-wrapper">
<view class="button-group">
<uv-button
type="primary"
:text="submitting ? '提交中...' : '确定'"
:disabled="!canSubmit || submitting"
:loading="submitting"
loadingText="提交中..."
@click="handleSubmit"
></uv-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,
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 showStatusPicker = ref(false);
const showIntentLevelPicker = ref(false);
const showFollowTypePicker = ref(false);
// 日期选择器 ref
const followTimePickerRef = ref(null);
const nextFollowTimePickerRef = ref(null);
// 日期选择器值(时间戳)
const followTimeValue = ref(Number(new Date()));
const nextFollowTimeValue = ref(Number(new Date()));
// 选项数据
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();
// 设置默认跟进时间为当前时间
const now = new Date();
formData.value.followTime = formatDateTimeForInput(now);
followTimeValue.value = Number(now);
// 设置默认下次跟进时间为7天后
const nextWeek = new Date(now);
nextWeek.setDate(nextWeek.getDate() + 7);
formData.value.nextFollowTime = formatDateTimeForInput(nextWeek);
nextFollowTimeValue.value = Number(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 openStatusPicker = () => {
showStatusPicker.value = true;
};
// 打开意向强度选择器
const openIntentLevelPicker = () => {
showIntentLevelPicker.value = true;
};
// 打开跟进方式选择器
const openFollowTypePicker = () => {
showFollowTypePicker.value = true;
};
// 关闭选择器
const handleClosePicker = (pickerType) => {
switch (pickerType) {
case 'status':
showStatusPicker.value = false;
break;
case 'intentLevel':
showIntentLevelPicker.value = false;
break;
case 'followType':
showFollowTypePicker.value = false;
break;
}
};
// 确认跟进时间
const onFollowTimeConfirm = (e) => {
// 使用 v-model 绑定的值(已自动更新)
const date = new Date(followTimeValue.value);
formData.value.followTime = formatDateTimeForInput(date);
};
// 确认下次跟进时间
const onNextFollowTimeConfirm = (e) => {
// 使用 v-model 绑定的值(已自动更新)
const date = new Date(nextFollowTimeValue.value);
formData.value.nextFollowTime = formatDateTimeForInput(date);
};
// 打开跟进时间选择器
const openFollowTimePicker = () => {
// 更新选择器的值为当前表单值
if (formData.value.followTime) {
const date = new Date(formData.value.followTime);
followTimeValue.value = Number(date);
}
followTimePickerRef.value?.open();
};
// 打开下次跟进时间选择器
const openNextFollowTimePicker = () => {
// 更新选择器的值为当前表单值
if (formData.value.nextFollowTime) {
const date = new Date(formData.value.nextFollowTime);
nextFollowTimeValue.value = Number(date);
}
nextFollowTimePickerRef.value?.open();
};
// 选择图片并自动上传
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;
&.readonly {
background-color: #f9f9f9;
.arrow {
display: none;
}
}
}
.form-input {
flex: 1;
font-size: 14px;
color: #333;
pointer-events: none; /* 让点击事件穿透到父元素 */
}
.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: 33%;
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 {
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;
}
}
.button-wrapper {
padding: 16px;
background-color: #fff;
border-top: 1px solid #e0e0e0;
}
</style>