OfficeSystem/pages/task/add/index.vue

1386 lines
33 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="task-add-page">
<view class="content-scroll" >
<view class="form-card">
<view class="section-title">任务信息</view>
<view class="form-item" @click="openProjectPicker">
<text class="form-label required">项目</text>
<view class="form-value">
<text v-if="formData.projectName" class="value-text">{{ formData.projectName }}</text>
<text v-else class="placeholder">请选择项目</text>
</view>
<text class="arrow"></text>
</view>
<view class="form-item">
<text class="form-label required">任务类型</text>
<view class="pill-group">
<view
class="pill-item"
v-for="type in typeOptions"
:key="type.value"
:class="{ active: formData.type === type.value }"
@click="selectType(type.value)"
>
{{ type.label }}
</view>
<text v-if="!typeOptions.length" class="placeholder">暂无可用任务类型</text>
</view>
</view>
<view class="form-item">
<text class="form-label required">优先级</text>
<view class="pill-group">
<view
class="pill-item priority"
v-for="level in levelOptions"
:key="level.value"
:class="{ active: formData.level === level.value }"
@click="selectLevel(level.value)"
>
{{ level.label }}
</view>
<text v-if="!levelOptions.length" class="placeholder">暂无可用优先级</text>
</view>
</view>
<view class="form-item">
<text class="form-label required">截止时间</text>
<view class="form-value" @click="openExpireTimePicker">
<text v-if="formData.expireTime" class="value-text">{{ formData.expireTime }}</text>
<text v-else class="placeholder">请选择截止时间</text>
</view>
<text class="arrow"></text>
</view>
<view class="form-item align-start">
<text class="form-label required">任务内容</text>
<textarea
v-model="formData.description"
class="textarea-input"
placeholder="请填写任务内容、输出要求或其他说明最多500字"
placeholder-style="color:#999;"
:maxlength="500"
/>
</view>
</view>
<view class="form-card">
<view class="section-title">附件</view>
<view class="attachment-tip">请上传不超过200MB的文件支持常见图片、Office、PDF、压缩包等格式。</view>
<!-- 添加照片 -->
<view class="form-item clickable-item" @click="chooseImages">
<view class="form-icon">🏔️</view>
<text class="form-label">添加照片</text>
<text class="arrow"></text>
</view>
<!-- 照片预览 -->
<view class="images-preview" v-if="formData.images.length > 0">
<view
class="image-item"
v-for="(image, index) in formData.images"
:key="index"
>
<image :src="image" mode="aspectFill" class="preview-image" @click="previewImage(index)" />
<view class="remove-btn" @click="removeImage(index)">✕</view>
</view>
</view>
<!-- 添加文件 -->
<view class="form-item clickable-item" @click="chooseFiles">
<view class="form-icon">📄</view>
<text class="form-label">添加文件</text>
<text class="arrow"></text>
</view>
<!-- 文件列表 -->
<view class="files-list" v-if="formData.files.length > 0">
<view
class="file-item"
v-for="(file, index) in formData.files"
: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-card">
<view class="section-title">负责人</view>
<view class="form-item">
<text class="form-label required">负责人</text>
<view class="responsible-content">
<view class="selected-members" v-if="formData.members.length">
<view
class="member-chip"
v-for="member in formData.members"
:key="member.userId"
>
<text>{{ member.userName }}</text>
<text class="chip-remove" @click.stop="removeMember(member.userId)">✕</text>
</view>
</view>
<view class="placeholder" v-else>请选择负责人,可多选</view>
</view>
<view class="picker-trigger" @click="openMemberModal">
<text>{{ formData.members.length ? '调整' : '选择' }}</text>
<text class="arrow"></text>
</view>
</view>
</view>
</view>
<view class="submit-bar">
<uv-button
type="primary"
:disabled="!canSubmit || submitting"
:loading="submitting"
loadingText="提交中..."
@click="handleSubmit"
>
创建任务
</uv-button>
</view>
<uv-picker
ref="projectPicker"
:columns="projectColumns"
keyName="label"
@confirm="handleProjectConfirm"
></uv-picker>
<uv-datetime-picker
ref="expirePickerRef"
v-model="expirePickerValue"
mode="datetime"
@confirm="onExpireTimeConfirm"
></uv-datetime-picker>
<view class="member-modal" v-if="showMemberModal">
<view class="modal-mask" @click="closeMemberModal"></view>
<view class="modal-panel">
<view class="modal-header">
<text class="modal-title">选择负责人</text>
<text class="modal-close" @click="closeMemberModal">✕</text>
</view>
<view class="search-box">
<input
v-model="memberKeyword"
class="search-input"
placeholder="搜索姓名或部门"
placeholder-style="color:#999;"
/>
</view>
<scroll-view class="member-list" scroll-y>
<view
class="member-item"
v-for="user in filteredMemberOptions"
:key="user.userId"
@click="toggleMember(user.userId)"
>
<view class="member-info">
<text class="member-name">{{ user.userName }}</text>
<text class="member-dept" v-if="user.deptName">{{ user.deptName }}</text>
</view>
<view class="select-indicator" :class="{ active: selectedMemberIds.includes(user.userId) }"></view>
</view>
<view class="empty-tip" v-if="!filteredMemberOptions.length">
<text>未找到匹配的人员</text>
</view>
</scroll-view>
<view class="modal-actions">
<uv-button @click="closeMemberModal">取消</uv-button>
<uv-button type="primary" @click="confirmMemberSelection">确定</uv-button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { createTask, getProjectListAll, getUserList } from '@/api';
import { useDictStore } from '@/store/dict';
import { chooseAndUploadImages, batchUploadFilesToQiniu } from '@/utils/qiniu.js';
const dictStore = useDictStore();
const ATTACHMENT_LIMIT = 9;
const formData = ref({
projectId: '',
projectName: '',
type: '',
level: '',
description: '',
expireTime: '',
attachments: [],
members: [],
images: [],
files: []
});
const presetProjectId = ref('');
const submitting = ref(false);
const loading = ref(false);
const projectColumns = ref([[]]);
const projectOptions = ref([]);
const showMemberModal = ref(false);
const memberOptions = ref([]);
const memberKeyword = ref('');
const selectedMemberIds = ref([]);
const projectPicker=ref(null);
const expirePickerRef = ref(null);
const expirePickerValue = ref(Date.now());
const getFileIcon = (fileName) => {
if (!fileName) return '📄';
const ext = fileName.split('.').pop().toLowerCase();
const iconMap = {
'pdf': '📕',
'doc': '📘',
'docx': '📘',
'xls': '📗',
'xlsx': '📗',
'ppt': '📙',
'pptx': '📙',
'txt': '📄',
'zip': '📦',
'rar': '📦',
'jpg': '🖼️',
'jpeg': '🖼️',
'png': '🖼️',
'gif': '🖼️'
};
return iconMap[ext] || '📄';
};
const typeOptions = computed(() => {
return dictStore.getDictByType('task_type').map(item => ({
label: item.dictLabel,
value: item.dictValue
}));
});
const levelOptions = computed(() => {
return dictStore.getDictByType('task_level').map(item => ({
label: item.dictLabel,
value: item.dictValue
}));
});
const filteredMemberOptions = computed(() => {
if (!memberKeyword.value.trim()) {
return memberOptions.value;
}
const keyword = memberKeyword.value.trim().toLowerCase();
return memberOptions.value.filter(user => {
const name = (user.userName || '').toLowerCase();
const dept = (user.deptName || '').toLowerCase();
return name.includes(keyword) || dept.includes(keyword);
});
});
const canSubmit = computed(() => {
return Boolean(
formData.value.projectId &&
formData.value.type &&
formData.value.level &&
formData.value.expireTime &&
formData.value.description.trim() &&
formData.value.members.length
);
});
const formatDateTime = (value) => {
if (!value && value !== 0) return '';
const date = typeof value === 'number' ? new Date(value) : new Date(value);
if (Number.isNaN(date.getTime())) return '';
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hour = String(date.getHours()).padStart(2, '0');
const minute = String(date.getMinutes()).padStart(2, '0');
const second = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
};
const loadProjects = async () => {
try {
const res = await getProjectListAll();
console.log('xiangmu',res);
const list = res;
projectOptions.value = list.map(item => ({
id: item.id,
name: item.name
}));
projectColumns.value = [projectOptions.value.map(item => ({
label: item.name,
value: item.id
}))];
if (presetProjectId.value) {
const matched = projectOptions.value.find(item => String(item.id) === String(presetProjectId.value));
if (matched) {
formData.value.projectId = String(matched.id);
formData.value.projectName = matched.name;
}
}
} catch (error) {
console.error('加载项目列表失败:', error);
uni.showToast({
title: '加载项目失败',
icon: 'none'
});
}
};
const loadMembers = async () => {
try {
const res = await getUserList({
pageNum: 1,
pageSize: 200,
status: 0,
delFlag: 0
});
const rows = Array.isArray(res?.rows) ? res.rows : Array.isArray(res?.data) ? res.data : [];
memberOptions.value = rows.map(item => ({
userId: String(item.userId || item.id),
userName: item.nickName || item.userName || '',
deptName: item.dept?.deptName || item.deptName || ''
}));
} catch (error) {
console.error('加载用户列表失败:', error);
uni.showToast({
title: '加载负责人失败',
icon: 'none'
});
}
};
const openProjectPicker = () => {
console.log('openProjectPicker');
if (!projectOptions.value.length) {
uni.showToast({
title: '暂无项目可选',
icon: 'none'
});
return;
}
if (projectPicker.value?.open) {
projectPicker.value.open();
}
};
const handleProjectConfirm = ({ value }) => {
if (value && value.length) {
const selected = value[0];
formData.value.projectId = String(selected.value);
formData.value.projectName = selected.label;
}
};
const selectType = (typeValue) => {
formData.value.type = typeValue;
};
const selectLevel = (levelValue) => {
formData.value.level = levelValue;
};
const openExpireTimePicker = () => {
if (formData.value.expireTime) {
expirePickerValue.value = new Date(formData.value.expireTime.replace(/-/g, '/')).getTime();
} else {
expirePickerValue.value = Date.now();
}
if (expirePickerRef.value?.open) {
expirePickerRef.value.open();
}
};
const onExpireTimeConfirm = (event) => {
if (event?.value) {
formData.value.expireTime = formatDateTime(event.value);
}
};
const handleAddAttachment = () => {
const remaining = ATTACHMENT_LIMIT - formData.value.attachments.length;
if (remaining <= 0) {
uni.showToast({
title: `最多上传${ATTACHMENT_LIMIT}个附件`,
icon: 'none'
});
return;
}
uni.showActionSheet({
itemList: ['上传图片', '上传文件'],
success: ({ tapIndex }) => {
if (tapIndex === 0) {
chooseImages();
} else if (tapIndex === 1) {
chooseFiles();
}
}
});
};
// 选择图片并自动上传到七牛云
const chooseImages = async () => {
try {
const remainingCount = 9 - formData.value.images.length;
if (remainingCount <= 0) {
uni.showToast({
title: '最多只能添加9张图片',
icon: 'none'
});
return;
}
// 使用封装好的选择并上传功能
const urls = await chooseAndUploadImages({
count: remainingCount,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera']
});
// 将上传后的URL添加到图片列表
formData.value.images = [...formData.value.images, ...urls];
} catch (err) {
console.error('选择或上传图片失败:', err);
uni.showToast({
title: err.message || '选择图片失败',
icon: 'none'
});
}
};
// 预览图片
const previewImage = (index) => {
uni.previewImage({
urls: formData.value.images,
current: index
});
};
// 删除图片
const removeImage = (index) => {
formData.value.images.splice(index, 1);
};
const chooseFiles = async () => {
const remainingCount = 5 - formData.value.files.length;
if (remainingCount <= 0) {
uni.showToast({
title: '最多只能添加5个文件',
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','.jpg','.png'],
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.files = [...formData.value.files, ...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 = 5 - formData.value.files.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.files = [...formData.value.files, ...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 removeFile = (index) => {
formData.value.files.splice(index, 1);
};
// 格式化文件大小
const formatFileSize = (bytes) => {
if (!bytes || bytes === 0) return '';
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 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 (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 openMemberModal = () => {
selectedMemberIds.value = formData.value.members.map(member => member.userId);
memberKeyword.value = '';
showMemberModal.value = true;
};
const closeMemberModal = () => {
showMemberModal.value = false;
};
const toggleMember = (userId) => {
const index = selectedMemberIds.value.indexOf(userId);
if (index >= 0) {
selectedMemberIds.value.splice(index, 1);
} else {
selectedMemberIds.value.push(userId);
}
};
const confirmMemberSelection = () => {
const selected = memberOptions.value.filter(user => selectedMemberIds.value.includes(user.userId));
formData.value.members = selected.map(user => ({
userId: user.userId,
userName: user.userName
}));
showMemberModal.value = false;
};
const removeMember = (userId) => {
formData.value.members = formData.value.members.filter(member => member.userId !== userId);
};
const handleSubmit = async () => {
if (submitting.value) {
return;
}
if (!canSubmit.value) {
uni.showToast({
title: '请完善必填信息',
icon: 'none'
});
return;
}
// 合并所有附件URL图片和文件
// 图片和文件都已经上传到七牛云直接使用URL
const allAttaches = [
...formData.value.images, // 图片已经是七牛云URL
...formData.value.files.map(file => file.path) // 文件已经是七牛云URL
].filter(url => url && url.trim() !== ''); // 过滤空值
// 将附件数组转换为逗号分隔的字符串
const submitAttaches = allAttaches.join(',');
const payload = {
id: null,
projectId: formData.value.projectId,
type: formData.value.type,
level: formData.value.level,
picture: submitAttaches,
description: formData.value.description.trim(),
expireTime: formData.value.expireTime,
memberList: formData.value.members.map(member => ({
userId: member.userId,
userName: member.userName
}))
};
submitting.value = true;
uni.showLoading({
title: '提交中...'
});
try {
const res = await createTask(payload);
console.log('@@@@@@@@@@',res);
uni.hideLoading();
uni.showToast({
title: '创建成功',
icon: 'success'
});
uni.$emit('taskListRefresh');
setTimeout(() => {
uni.navigateBack();
}, 1200);
} catch (error) {
uni.hideLoading();
console.error('创建任务失败:', error);
// 错误提示已在响应拦截器中统一处理,这里不需要重复显示
} finally {
submitting.value = false;
console.log('submitting', submitting.value);
}
};
onLoad(async (options) => {
if (options?.projectId) {
presetProjectId.value = String(options.projectId);
}
loading.value = true;
try {
await dictStore.loadDictData();
await Promise.all([loadProjects(), loadMembers()]);
} finally {
loading.value = false;
}
});
</script>
<style scoped lang="scss">
.task-add-page {
background: #f5f5f5;
min-height: 100vh;
display: flex;
flex-direction: column;
padding-bottom: 120rpx;
}
.content-scroll {
flex: 1;
}
.form-card {
margin: 16px;
padding: 16px;
background: #fff;
border-radius: 12px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.03);
}
.section-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
color: #333;
}
.form-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f2f2f2;
gap: 12px;
}
.form-item:last-child {
border-bottom: none;
}
.align-start {
align-items: flex-start;
}
.form-label {
width: 80px;
font-size: 14px;
color: #333;
}
.form-label.required::before {
content: '*';
color: #f56c6c;
margin-right: 4px;
}
.form-value {
flex: 1;
display: flex;
align-items: center;
min-height: 24px;
}
.value-text {
color: #333;
font-size: 14px;
}
.placeholder {
color: #999;
font-size: 14px;
}
.arrow {
font-size: 18px;
color: #999;
}
.textarea-input {
flex: 1;
min-height: 120px;
padding: 12px;
border-radius: 8px;
background: #f8f9fb;
font-size: 14px;
color: #333;
line-height: 1.6;
}
.pill-group {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.pill-item {
padding: 6px 14px;
border-radius: 20px;
background: #f3f5f9;
color: #666;
font-size: 13px;
}
.pill-item.active {
background: #2885ff;
color: #fff;
}
.attachment-tip {
font-size: 12px;
color: #999;
margin-bottom: 12px;
}
.attachment-grid {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.attachment-item {
width: calc(33.33% - 8px);
min-width: 100px;
height: 110px;
background: #f6f7fb;
border-radius: 12px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 12px;
gap: 6px;
position: relative;
}
.attachment-item.add-item {
border: 1px dashed #d1d8e6;
background: #fff;
color: #2885ff;
}
.add-icon {
font-size: 28px;
}
.add-text {
font-size: 13px;
}
.file-icon {
font-size: 26px;
}
.file-name {
font-size: 12px;
color: #333;
text-align: center;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.remove-btn {
position: absolute;
top: 6px;
right: 6px;
width: 20px;
height: 20px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.4);
color: #fff;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.responsible-content {
flex: 1;
}
.selected-members {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.member-chip {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: #eef4ff;
border-radius: 16px;
font-size: 12px;
color: #2885ff;
}
.chip-remove {
font-size: 14px;
}
.picker-trigger {
display: flex;
align-items: center;
gap: 4px;
color: #2885ff;
font-size: 14px;
}
.submit-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 12px 16px;
background: #fff;
border-top: 1px solid #eee;
}
.member-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 99;
}
.modal-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
}
.modal-panel {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: #fff;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
display: flex;
flex-direction: column;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid #f2f2f2;
}
.modal-title {
font-size: 16px;
font-weight: 600;
}
.modal-close {
font-size: 20px;
color: #999;
}
.search-box {
padding: 12px 16px;
border-bottom: 1px solid #f2f2f2;
}
.search-input {
background: #f5f6f8;
border-radius: 20px;
padding: 8px 14px;
font-size: 14px;
}
.member-list {
height: 600px;
}
.member-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid #f5f5f5;
}
.member-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.member-name {
font-size: 14px;
color: #333;
}
.member-dept {
font-size: 12px;
color: #999;
}
.select-indicator {
width: 20px;
height: 20px;
border-radius: 4px;
border: 1px solid #dcdfe6;
}
.select-indicator.active {
background: #2885ff;
border-color: #2885ff;
}
.modal-actions {
display: flex;
gap: 12px;
padding: 12px 16px 24px;
}
.empty-tip {
padding: 40px 0;
text-align: center;
color: #999;
font-size: 14px;
}
/* 表单项 */
.form-item {
display: flex;
align-items: center;
padding: 16px;
background-color: #fff;
border-radius: 8px;
margin-bottom: 12px;
gap: 12px;
}
.clickable-item {
cursor: pointer;
&:active {
background-color: #f5f5f5;
}
}
.form-icon {
font-size: 20px;
flex-shrink: 0;
}
.form-content {
flex: 1;
display: flex;
flex-direction: column;
}
.form-label {
flex: 1;
font-size: 15px;
color: #333;
}
.form-value {
font-size: 15px;
color: #333;
font-weight: 500;
}
.form-placeholder {
font-size: 15px;
color: #999;
}
.arrow {
font-size: 20px;
color: #999;
flex-shrink: 0;
}
.description-input {
flex: 1;
min-height: 80px;
font-size: 15px;
color: #333;
line-height: 1.6;
}
/* 图片预览 */
.images-preview {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
padding: 0 16px;
}
.image-item {
position: relative;
width: 100px;
height: 100px;
border-radius: 8px;
overflow: hidden;
}
.preview-image {
width: 100%;
height: 100%;
}
.remove-btn {
position: relative;
width: 24px;
height: 24px;
background-color: rgba(0, 0, 0, 0.6);
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
cursor: pointer;
}
/* 文件列表 */
.files-list {
padding: 0 16px;
margin-bottom: 12px;
}
.file-item {
display: flex;
align-items: center;
padding: 12px;
background-color: #fff;
border-radius: 8px;
margin-bottom: 8px;
gap: 12px;
cursor: pointer;
&:active {
background-color: #f5f5f5;
}
}
.file-icon {
font-size: 24px;
flex-shrink: 0;
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.file-name {
text-align: left;
font-size: 14px;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
}
.file-size {
font-size: 12px;
color: #999;
}
</style>