OfficeSystem/pages/customer/follow/edit/index.vue
2025-11-10 14:16:24 +08:00

1214 lines
32 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="edit-followup-page">
<!-- 自定义导航栏 -->
<view class="custom-navbar">
<view class="navbar-content">
<text class="nav-btn" @click="handleCancel"></text>
<text class="nav-title">修改跟进记录</text>
<text class="nav-btn" style="opacity: 0;">占位</text>
</view>
</view>
<!-- 加载状态 -->
<view class="loading-container" v-if="loading">
<text class="loading-text">加载中...</text>
</view>
<!-- 内容区域 -->
<scroll-view class="content-scroll" scroll-y v-else>
<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 || file.path) }}</text>
<view class="file-info">
<text class="file-name">{{ file.name || getFileNameFromUrl(file.path) }}</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 } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { chooseAndUploadImages, batchUploadFilesToQiniu } from '@/utils/qiniu.js';
import {
updateFollowup,
getFollowupDetail,
getCustomerStatusDict,
getCustomerIntentLevelDict,
getCustomerFollowTypeDict
} from '@/common/api/customer.js';
import FollowupPickers from '@/components/followup-form/FollowupPickers.vue';
// 跟进ID
const followId = ref('');
// 表单数据
const formData = ref({
followId: '',
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 loading = ref(false);
// 是否可以提交
const canSubmit = computed(() => {
return formData.value.followId &&
formData.value.customerId &&
formData.value.customerStatus &&
formData.value.customerIntentLevel &&
formData.value.type &&
formData.value.content.trim() &&
formData.value.followTime &&
formData.value.nextFollowTime;
});
// 页面加载
onLoad(async (options) => {
if (options.followId) {
followId.value = options.followId;
formData.value.followId = options.followId;
// 加载字典数据和跟进详情
await Promise.all([
loadDictData(),
loadFollowupDetail()
]);
} else {
uni.showToast({
title: '缺少跟进ID参数',
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
});
// 加载字典数据
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 loadFollowupDetail = async () => {
if (!followId.value) return;
loading.value = true;
try {
const res = await getFollowupDetail(followId.value);
if (res) {
// 填充表单数据
formData.value.followId = res.followId || res.id || followId.value;
formData.value.customerId = res.customerId || '';
formData.value.customerName = res.customerName || '';
formData.value.customerStatus = res.customerStatus || '';
formData.value.customerIntentLevel = res.customerIntentLevel || '';
formData.value.type = res.type || '';
formData.value.content = res.content || '';
// 处理图片(可能是逗号分隔的字符串)
if (res.picture) {
if (typeof res.picture === 'string') {
formData.value.pictures = res.picture.split(',').map(url => url.trim()).filter(url => url);
} else if (Array.isArray(res.picture)) {
formData.value.pictures = res.picture;
}
} else if (res.pictures && Array.isArray(res.pictures)) {
formData.value.pictures = res.pictures;
}
// 处理附件(可能是逗号分隔的字符串)
if (res.attaches) {
if (typeof res.attaches === 'string') {
const attachUrls = res.attaches.split(',').map(url => url.trim()).filter(url => url);
formData.value.attaches = attachUrls.map(url => ({
path: url,
name: getFileNameFromUrl(url),
size: 0
}));
} else if (Array.isArray(res.attaches)) {
formData.value.attaches = res.attaches.map(item => {
if (typeof item === 'string') {
return {
path: item,
name: getFileNameFromUrl(item),
size: 0
};
}
return item;
});
}
}
// 处理时间
if (res.followTime) {
formData.value.followTime = res.followTime;
const followDate = new Date(res.followTime);
followTimeValue.value = Number(followDate);
}
if (res.nextFollowTime) {
formData.value.nextFollowTime = res.nextFollowTime;
const nextFollowDate = new Date(res.nextFollowTime);
nextFollowTimeValue.value = Number(nextFollowDate);
}
}
} catch (err) {
console.error('加载跟进详情失败:', err);
uni.showToast({
title: err.message || '加载跟进详情失败',
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
} finally {
loading.value = false;
}
};
// 从URL中提取文件名
const getFileNameFromUrl = (url) => {
if (!url) return '未知文件';
try {
const urlObj = new URL(url);
const pathname = urlObj.pathname;
const fileName = pathname.split('/').pop();
// 如果文件名包含特殊字符,尝试解码
return decodeURIComponent(fileName) || '未知文件';
} catch (e) {
// 如果不是完整URL尝试从路径中提取
const parts = url.split('/');
const fileName = parts[parts.length - 1];
// 移除可能的查询参数
return fileName.split('?')[0] || '未知文件';
}
};
// 打开状态选择器
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) => {
const date = new Date(followTimeValue.value);
formData.value.followTime = formatDateTimeForInput(date);
};
// 确认下次跟进时间
const onNextFollowTimeConfirm = (e) => {
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,
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);
chooseFilesNative();
}
});
} catch (error) {
chooseFilesNative();
}
// #endif
// #ifndef H5 || MP-WEIXIN || APP-PLUS
chooseFilesNative();
// #endif
};
// 原生文件选择方法(安卓平台)
const chooseFilesNative = async () => {
const remainingCount = 10 - formData.value.attaches.length;
if (typeof plus !== 'undefined') {
try {
const Intent = plus.android.importClass('android.content.Intent');
const main = plus.android.runtimeMainActivity();
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) {
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,
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,
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,
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'
});
}
}
if (originalOnActivityResult) {
main.onActivityResult = originalOnActivityResult;
}
} catch (error) {
uni.hideLoading();
console.error('处理文件选择结果失败:', error);
uni.showToast({
title: '处理文件失败',
icon: 'none'
});
}
}
} else {
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 || file.path).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'
});
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 = {
followId: formData.value.followId,
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 updateFollowup(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">
.edit-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;
}
.loading-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.loading-text {
font-size: 14px;
color: #999;
}
.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;
}
.button-wrapper {
padding: 16px;
background-color: #fff;
border-top: 1px solid #e0e0e0;
}
</style>