1519 lines
38 KiB
Vue
1519 lines
38 KiB
Vue
<template>
|
||
|
||
|
||
<scroll-view class="content-scroll" scroll-y>
|
||
<!-- 任务状态栏 -->
|
||
<view class="status-section">
|
||
<view class="task-info">
|
||
<!-- <text class="task-name">{{ task.name }}</text>-->
|
||
<text class="project-name">归属项目:{{ task.project }}</text>
|
||
</view>
|
||
<view class="status-tags">
|
||
<uv-tags
|
||
v-for="(status, index) in task.statusTags"
|
||
:key="index"
|
||
:text="status"
|
||
:type="getTagType(status)"
|
||
size="mini"
|
||
:plain="false"
|
||
:custom-style="getTagStyle(status)"
|
||
></uv-tags>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 基本信息区域 -->
|
||
<view class="basic-info">
|
||
<view class="info-item">
|
||
<text class="info-label">截止时间:</text>
|
||
<text class="info-value">{{ task.deadline }}</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<text class="info-label">创建人:</text>
|
||
<view class="info-value-with-avatar">
|
||
<image
|
||
v-if="task.creatorAvatar"
|
||
:src="task.creatorAvatar"
|
||
class="creator-avatar"
|
||
mode="aspectFill"
|
||
/>
|
||
<text>{{ task.creator }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="info-item">
|
||
<text class="info-label">负责人:</text>
|
||
<text class="info-value">{{ task.responsible }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 标签切换 -->
|
||
<view class="tab-container">
|
||
<view
|
||
class="tab-item"
|
||
:class="{ active: activeTab === 'info' }"
|
||
@click="switchTab('info')"
|
||
>
|
||
<text>任务信息</text>
|
||
</view>
|
||
<view
|
||
class="tab-item"
|
||
:class="{ active: activeTab === 'records' }"
|
||
@click="switchTab('records')"
|
||
>
|
||
<text>提交记录</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 任务信息标签页 -->
|
||
<view class="tab-content" v-if="activeTab === 'info'">
|
||
<view class="task-info-card">
|
||
<view class="publish-time-row">
|
||
<text class="clock-icon">🕐</text>
|
||
<text class="publish-time-text">发布时间:{{ task.publishTime }}</text>
|
||
</view>
|
||
<view class="task-content-wrapper">
|
||
<text class="task-content-text">{{ task.content }}</text>
|
||
</view>
|
||
<!-- 任务图片展示 -->
|
||
<view class="task-images-wrapper" v-if="task.pictures && task.pictures.length > 0">
|
||
<view
|
||
class="task-image-item"
|
||
v-for="(imageUrl, imgIndex) in task.pictures"
|
||
:key="imgIndex"
|
||
@click="previewTaskImages(task.pictures, imgIndex)"
|
||
>
|
||
<image
|
||
:src="imageUrl"
|
||
mode="aspectFill"
|
||
class="task-image"
|
||
/>
|
||
</view>
|
||
</view>
|
||
<!-- 任务文件展示 -->
|
||
<view class="task-files-wrapper" v-if="task.files && task.files.length > 0">
|
||
<view
|
||
class="file-attachment-item"
|
||
v-for="(file, fileIndex) in task.files"
|
||
:key="fileIndex"
|
||
@click="previewTaskFile(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>
|
||
</view>
|
||
<view class="delay-btn-wrapper" v-if="showDelayBtn">
|
||
<uv-button type="error" size="small" @click="applyDelay">申请延期</uv-button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 提交记录标签页 -->
|
||
<view class="tab-content" v-if="activeTab === 'records'" @click="closeMenu">
|
||
<view class="no-record" v-if="task.submitRecords.length === 0">
|
||
<text>暂无提交记录</text>
|
||
</view>
|
||
<view class="submit-record-card" v-for="(record, index) in task.submitRecords" :key="index">
|
||
<view class="record-header">
|
||
<view class="user-info">
|
||
<image
|
||
v-if="record.userAvatar"
|
||
:src="record.userAvatar"
|
||
class="avatar-img"
|
||
mode="aspectFill"
|
||
/>
|
||
<view v-else class="avatar-placeholder"></view>
|
||
<text class="user-name">{{ record.userName }}</text>
|
||
</view>
|
||
<view class="record-header-right">
|
||
<text class="record-time">{{ record.time }}</text>
|
||
<!-- <view class="more-menu" v-if="record.canEdit" @click.stop="toggleMenu(index)">-->
|
||
<!-- <text class="more-icon">⋮</text>-->
|
||
<!-- <view class="menu-dropdown" v-if="showMenuIndex === index" @click.stop>-->
|
||
<!-- <view class="menu-item" @click="editRecord(index)">-->
|
||
<!-- <text>编辑</text>-->
|
||
<!-- </view>-->
|
||
<!-- <view class="menu-item" @click="deleteRecord(index)">-->
|
||
<!-- <text>删除</text>-->
|
||
<!-- </view>-->
|
||
<!-- </view>-->
|
||
<!-- </view>-->
|
||
</view>
|
||
</view>
|
||
<view class="record-content-wrapper" v-if="record.content">
|
||
<text class="record-content-text">{{ record.content }}</text>
|
||
</view>
|
||
<view class="record-progress" v-if="record.progress !== null && record.progress !== undefined">
|
||
<text class="progress-label">任务进度:</text>
|
||
<text class="progress-value">{{ record.progress }}%</text>
|
||
</view>
|
||
<!-- 图片附件展示(一行三个) -->
|
||
<view class="record-images-wrapper" v-if="record.imageAttachments && record.imageAttachments.length > 0">
|
||
<view
|
||
class="record-image-item"
|
||
v-for="(imageUrl, imgIndex) in record.imageAttachments"
|
||
:key="imgIndex"
|
||
@click="previewRecordImages(record.imageAttachments, imgIndex)"
|
||
>
|
||
<image
|
||
:src="imageUrl"
|
||
mode="aspectFill"
|
||
class="record-image"
|
||
/>
|
||
</view>
|
||
</view>
|
||
<!-- 文件附件展示 -->
|
||
<view class="record-files-wrapper" v-if="record.fileAttachments && record.fileAttachments.length > 0">
|
||
<view
|
||
class="file-attachment-item"
|
||
v-for="(file, fileIndex) in record.fileAttachments"
|
||
:key="fileIndex"
|
||
@click="previewRecordFile(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>
|
||
</view>
|
||
<view class="delay-btn-wrapper" v-if="record.showDelayBtn && showDelayBtn">
|
||
<uv-button type="error" size="small" @click="applyDelay">申请延期</uv-button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 底部操作按钮 -->
|
||
<view class="action-buttons" v-if="showSubmitBtn || showCompleteBtn || showCancelBtn">
|
||
<view class="btn-wrapper" v-if="showCompleteBtn">
|
||
<uv-button type="primary" size="normal" @click="completeTask">完成任务</uv-button>
|
||
</view>
|
||
<view class="btn-wrapper" v-if="showSubmitBtn">
|
||
<uv-button type="primary" size="normal" @click="submitTask">提交任务</uv-button>
|
||
</view>
|
||
<view class="btn-wrapper" v-if="showCancelBtn">
|
||
<uv-button type="error" size="normal" @click="cancelTask">取消任务</uv-button>
|
||
</view>
|
||
</view>
|
||
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted, computed } from 'vue';
|
||
import { onLoad,onShow } from '@dcloudio/uni-app';
|
||
import { storeToRefs } from 'pinia';
|
||
import { getStatusFromTagText, getTaskStatusType, getTaskStatusStyle } from '@/utils/taskConfig.js';
|
||
import { useTaskStore } from '@/store/task';
|
||
import { useUserStore } from '@/store/user';
|
||
import { getTaskDetail } from '@/api';
|
||
|
||
// 当前激活的标签
|
||
const activeTab = ref('info');
|
||
const showMenuIndex = ref(-1);
|
||
|
||
// 用户权限相关
|
||
const userStore = useUserStore();
|
||
const { userInfo } = storeToRefs(userStore);
|
||
|
||
// 判断是否是管理员(总管理员)
|
||
const isAdmin = computed(() => {
|
||
if (!userInfo.value || !Array.isArray(userInfo.value.roles)) return false;
|
||
|
||
|
||
let is = userInfo.value.roles.some((role) => ['admin', 'sys_admin'].includes(role));
|
||
console.log('isAdmin',is)
|
||
return is
|
||
|
||
});
|
||
|
||
// 判断是否有指定权限
|
||
const hasPermission = (permission) => {
|
||
// 如果用户是admin,给予所有权限
|
||
if (isAdmin.value) return true;
|
||
if (!userInfo.value || !userInfo.value.permissions) return false;
|
||
|
||
// 处理对象格式的权限数据(键为数字索引,值为权限字符串)
|
||
const permissions = userInfo.value.permissions;
|
||
// 如果是数组,直接使用includes
|
||
if (Array.isArray(permissions)) {
|
||
return permissions.includes(permission);
|
||
}
|
||
// 如果是对象,使用Object.values()获取所有权限值
|
||
if (typeof permissions === 'object') {
|
||
return Object.values(permissions).includes(permission);
|
||
}
|
||
|
||
return false;
|
||
};
|
||
|
||
// 判断任务是否为待完成状态
|
||
const isTaskPending = computed(() => {
|
||
// 优先从原始数据判断任务状态
|
||
if (task.value.rawData) {
|
||
const status = task.value.rawData.status;
|
||
// 如果任务已完成(状态为4),则不是待完成
|
||
if (status === 4 || status === '4' || String(status) === '4') {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (task.value.statusTags?.some(tag =>
|
||
['待完成', '即将逾期', '已逾期', '逾期'].includes(tag)
|
||
)) {
|
||
return true;
|
||
}
|
||
|
||
// 如果状态标签中包含"已完成",则不是待完成
|
||
if (task.value.statusTags && task.value.statusTags.includes('已完成')) {
|
||
return false;
|
||
}
|
||
// 默认返回 false(安全起见)
|
||
return false;
|
||
});
|
||
|
||
// 判断是否显示提交任务按钮:有权限 && 任务状态为待完成,或者管理员
|
||
const showSubmitBtn = computed(() => {
|
||
|
||
console.log('showSubmitBtn',(hasPermission('bst:task:submit') && isTaskPending.value) || isAdmin.value)
|
||
console.log('hasPermission(\'bst:task:submit\') ',hasPermission('bst:task:submit') )
|
||
console.log('isTaskPending.value',isTaskPending.value)
|
||
return (hasPermission('bst:task:submit') && isTaskPending.value) ;
|
||
});
|
||
|
||
// 判断是否显示完成任务按钮:有权限 && 任务状态为待完成,或者管理员
|
||
const showCompleteBtn = computed(() => {
|
||
return (hasPermission('bst:task:pass') && isTaskPending.value) ;
|
||
});
|
||
|
||
// 判断是否显示取消任务按钮:有权限 && 任务状态为待完成 && 是管理员
|
||
const showCancelBtn = computed(() => {
|
||
return hasPermission('bst:task:cancel') && isTaskPending.value && isAdmin.value;
|
||
});
|
||
|
||
// 判断是否显示申请延期按钮:任务状态为待完成
|
||
const showDelayBtn = computed(() => {
|
||
return isTaskPending.value;
|
||
});
|
||
|
||
// 格式化时间为中文格式:年月日星期几时分秒
|
||
const formatTimeToChinese = (date) => {
|
||
if (!date) return '';
|
||
if (typeof date === 'string') {
|
||
// 如果是字符串,尝试解析
|
||
date = new Date(date);
|
||
}
|
||
if (isNaN(date.getTime())) return '';
|
||
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
const weekday = weekdays[date.getDay()];
|
||
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}日 ${weekday} ${hour}:${minute}:${second}`;
|
||
};
|
||
|
||
// 格式化日期:将 "2024-10-31 23:59:59" 转换为 "2024-10-31"
|
||
const formatDate = (dateStr) => {
|
||
if (!dateStr) return '';
|
||
// 如果包含空格,取日期部分
|
||
return dateStr.split(' ')[0];
|
||
};
|
||
|
||
// 格式化日期时间:格式化为 yyyy-MM-dd HH:mm:ss
|
||
const formatDateTime = (date) => {
|
||
if (!date) return '';
|
||
const d = new Date(date);
|
||
if (isNaN(d.getTime())) return '';
|
||
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 seconds = String(d.getSeconds()).padStart(2, '0');
|
||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||
};
|
||
|
||
// 计算剩余天数
|
||
const calculateRemainingDays = (expireTime) => {
|
||
if (!expireTime) return null;
|
||
const expireDate = new Date(expireTime);
|
||
const now = new Date();
|
||
now.setHours(0, 0, 0, 0);
|
||
expireDate.setHours(0, 0, 0, 0);
|
||
const diffTime = expireDate.getTime() - now.getTime();
|
||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||
return diffDays;
|
||
};
|
||
|
||
// 根据过期时间和状态判断任务状态
|
||
const determineTaskStatus = (status, expireTime) => {
|
||
// 如果任务已完成(状态为4),直接返回 completed
|
||
const taskStatus = status !== undefined ? status : null;
|
||
const isCompleted = taskStatus === 4 ||
|
||
taskStatus === '4' ||
|
||
taskStatus === 'completed' ||
|
||
String(taskStatus) === '4';
|
||
|
||
if (isCompleted) {
|
||
return 'completed';
|
||
}
|
||
|
||
// 如果没有过期时间,返回待完成
|
||
if (!expireTime) {
|
||
return 'pending';
|
||
}
|
||
|
||
const expireDate = new Date(expireTime);
|
||
const now = new Date();
|
||
|
||
// 设置时间到当天0点,便于日期比较
|
||
// now.setHours(0, 0, 0, 0);
|
||
// expireDate.setHours(23, 59, 59, 999);
|
||
// 如果已过期,标记为逾期
|
||
if (expireDate.getTime() < now.getTime()) {
|
||
return 'overdue';
|
||
}
|
||
// 计算距离过期的天数
|
||
const diffTime = expireDate.getTime() - now.getTime();
|
||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||
// 如果3天内到期,标记为即将逾期
|
||
if (diffDays <= 3 && diffDays > 0) {
|
||
return 'imminent';
|
||
}
|
||
// 否则返回待完成状态
|
||
return 'pending';
|
||
};
|
||
|
||
// 获取状态标签数组
|
||
const getStatusTags = (status, expireTime) => {
|
||
const taskStatus = determineTaskStatus(status, expireTime);
|
||
const tags = [];
|
||
|
||
if (taskStatus === 'completed') {
|
||
tags.push('已完成');
|
||
} else if (taskStatus === 'overdue') {
|
||
tags.push('已逾期', '紧急');
|
||
} else if (taskStatus === 'imminent') {
|
||
tags.push('即将逾期');
|
||
} else {
|
||
tags.push('待完成');
|
||
}
|
||
|
||
return tags;
|
||
};
|
||
|
||
// 提取负责人:从 memberList 中提取所有成员的名称
|
||
const getOwnerNames = (memberList) => {
|
||
if (!Array.isArray(memberList) || memberList.length === 0) return '';
|
||
return memberList.map(member => member.userName || member.name || '').filter(name => name).join('、');
|
||
};
|
||
|
||
// 解析逗号分隔的URL字符串
|
||
const parseAttachUrls = (attachStr) => {
|
||
if (!attachStr) return [];
|
||
if (typeof attachStr !== 'string') return [];
|
||
|
||
// 按逗号分割,过滤空值
|
||
return attachStr.split(',').map(url => url.trim()).filter(url => url);
|
||
};
|
||
|
||
// 判断URL是否为图片
|
||
const isImageUrl = (url) => {
|
||
if (!url || typeof url !== 'string') return false;
|
||
return /\.(jpg|jpeg|png|gif|bmp|webp)(\?|$)/i.test(url);
|
||
};
|
||
|
||
// 解析任务附件(区分图片和文件)
|
||
const parseTaskAttachments = (attachStr) => {
|
||
if (!attachStr) return { pictures: [], files: [] };
|
||
if (typeof attachStr !== 'string') return { pictures: [], files: [] };
|
||
|
||
const urls = parseAttachUrls(attachStr);
|
||
const pictures = [];
|
||
const files = [];
|
||
|
||
urls.forEach(url => {
|
||
if (isImageUrl(url)) {
|
||
pictures.push(url);
|
||
} else {
|
||
// 从URL中提取文件名
|
||
const fileName = url.split('/').pop().split('?')[0] || '文件';
|
||
files.push({
|
||
name: fileName,
|
||
path: url,
|
||
size: 0 // 如果API没有返回文件大小,默认为0
|
||
});
|
||
}
|
||
});
|
||
|
||
return { pictures, files };
|
||
};
|
||
|
||
// 转换提交记录数据
|
||
const transformSubmitRecords = (submitList) => {
|
||
if (!Array.isArray(submitList) || submitList.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
const sortedList = [...submitList].sort((a, b) => {
|
||
const timeA = a?.createTime ? new Date(a.createTime).getTime() : 0;
|
||
const timeB = b?.createTime ? new Date(b.createTime).getTime() : 0;
|
||
return timeB - timeA;
|
||
});
|
||
|
||
return sortedList.map(item => {
|
||
// 处理附件:可能是逗号分隔的URL字符串
|
||
let imageAttachments = [];
|
||
let fileAttachments = [];
|
||
|
||
if (item.attaches) {
|
||
try {
|
||
// 先尝试作为JSON解析
|
||
let attachData = null;
|
||
try {
|
||
attachData = typeof item.attaches === 'string' ? JSON.parse(item.attaches) : item.attaches;
|
||
} catch (e) {
|
||
// 如果不是JSON,则作为逗号分隔的URL字符串处理
|
||
attachData = null;
|
||
}
|
||
|
||
if (Array.isArray(attachData)) {
|
||
// 如果是数组格式
|
||
attachData.forEach(att => {
|
||
const filePath = att.path || att.url || att.filePath || '';
|
||
const fileName = att.name || att.fileName || '';
|
||
const isImage = fileName ? /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(fileName) : isImageUrl(filePath);
|
||
|
||
if (isImage && filePath) {
|
||
imageAttachments.push(filePath);
|
||
} else if (filePath) {
|
||
fileAttachments.push({
|
||
name: fileName || '文件',
|
||
path: filePath
|
||
});
|
||
}
|
||
});
|
||
} else {
|
||
// 作为逗号分隔的URL字符串处理
|
||
const urls = parseAttachUrls(item.attaches);
|
||
urls.forEach(url => {
|
||
if (isImageUrl(url)) {
|
||
imageAttachments.push(url);
|
||
} else {
|
||
// 从URL中提取文件名
|
||
const fileName = url.split('/').pop().split('?')[0] || '文件';
|
||
fileAttachments.push({
|
||
name: fileName,
|
||
path: url
|
||
});
|
||
}
|
||
});
|
||
}
|
||
} catch (e) {
|
||
console.error('解析附件数据失败:', e);
|
||
// 如果解析失败,尝试作为逗号分隔的URL字符串处理
|
||
const urls = parseAttachUrls(item.attaches);
|
||
urls.forEach(url => {
|
||
if (isImageUrl(url)) {
|
||
imageAttachments.push(url);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
return {
|
||
id: item.id || '',
|
||
userName: item.userName || '',
|
||
userAvatar: item.userAvatar || '',
|
||
time: formatTimeToChinese(item.createTime) || '',
|
||
content: item.remark || '', // 如果没有提交内容,可能显示任务描述
|
||
progress: null, // API 返回的数据中没有进度字段
|
||
imageAttachments: imageAttachments,
|
||
fileAttachments: fileAttachments,
|
||
showDelayBtn: false, // 根据业务需求决定是否显示
|
||
canEdit: true // 根据业务需求决定是否可以编辑
|
||
};
|
||
});
|
||
};
|
||
|
||
// 任务数据
|
||
const task = ref({
|
||
|
||
});
|
||
|
||
// 切换标签
|
||
const switchTab = (tab) => {
|
||
activeTab.value = tab;
|
||
showMenuIndex.value = -1; // 关闭菜单
|
||
};
|
||
|
||
// 切换菜单显示
|
||
const toggleMenu = (index) => {
|
||
showMenuIndex.value = showMenuIndex.value === index ? -1 : index;
|
||
};
|
||
|
||
// 关闭菜单
|
||
const closeMenu = () => {
|
||
showMenuIndex.value = -1;
|
||
};
|
||
|
||
// 编辑记录
|
||
const editRecord = (index) => {
|
||
const record = task.value.submitRecords[index];
|
||
if (!record) {
|
||
uni.showToast({
|
||
title: '记录不存在',
|
||
icon: 'none'
|
||
});
|
||
showMenuIndex.value = -1;
|
||
return;
|
||
}
|
||
|
||
// 将编辑数据存储到本地,供提交任务页面使用
|
||
uni.setStorageSync('editSubmitRecord', {
|
||
recordIndex: index,
|
||
record: record,
|
||
taskId: task.value.id
|
||
});
|
||
|
||
// 跳转到提交任务页面
|
||
uni.navigateTo({
|
||
url: `/pages/task/submit/index?taskId=${task.value.id}&mode=edit&recordIndex=${index}`
|
||
});
|
||
|
||
showMenuIndex.value = -1;
|
||
};
|
||
|
||
// 删除记录
|
||
const deleteRecord = (index) => {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '确定要删除这条记录吗?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
task.value.submitRecords.splice(index, 1);
|
||
uni.showToast({
|
||
title: '已删除',
|
||
icon: 'success'
|
||
});
|
||
}
|
||
showMenuIndex.value = -1;
|
||
}
|
||
});
|
||
};
|
||
|
||
// 预览任务图片
|
||
const previewTaskImages = (imageUrls, index) => {
|
||
if (imageUrls && imageUrls.length > 0) {
|
||
uni.previewImage({
|
||
urls: imageUrls,
|
||
current: index
|
||
});
|
||
}
|
||
};
|
||
|
||
// 预览/下载任务文件
|
||
const previewTaskFile = (file) => {
|
||
if (!file.path) {
|
||
uni.showToast({
|
||
title: '文件路径不存在',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 如果是图片,使用预览图片功能
|
||
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||
const ext = file.name ? 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: 'loading',
|
||
duration: 2000
|
||
});
|
||
// 下载并打开文档
|
||
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 previewRecordImages = (imageUrls, index) => {
|
||
if (imageUrls && imageUrls.length > 0) {
|
||
uni.previewImage({
|
||
urls: imageUrls,
|
||
current: index
|
||
});
|
||
}
|
||
};
|
||
|
||
// 获取文件图标
|
||
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 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 previewRecordFile = (file) => {
|
||
if (!file.path) {
|
||
uni.showToast({
|
||
title: '文件路径不存在',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 如果是图片,使用预览图片功能
|
||
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||
const ext = file.name ? 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: 'loading',
|
||
duration: 2000
|
||
});
|
||
// 下载并打开文档
|
||
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
|
||
}
|
||
};
|
||
|
||
// 获取标签类型(用于uv-tags组件)
|
||
const getTagType = (tagText) => {
|
||
const status = getStatusFromTagText(tagText);
|
||
return getTaskStatusType(status);
|
||
};
|
||
|
||
// 获取标签样式(用于uv-tags组件)
|
||
const getTagStyle = (tagText) => {
|
||
const status = getStatusFromTagText(tagText);
|
||
const styleConfig = getTaskStatusStyle(status);
|
||
return {
|
||
backgroundColor: styleConfig.backgroundColor,
|
||
color: styleConfig.color,
|
||
borderColor: styleConfig.borderColor
|
||
};
|
||
};
|
||
|
||
// 返回上一页
|
||
const goBack = () => {
|
||
uni.navigateBack();
|
||
};
|
||
|
||
// 完成任务
|
||
const completeTask = () => {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '确定要完成任务吗?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
console.log("完成任务", task.value.id);
|
||
uni.showToast({
|
||
title: '任务已完成',
|
||
icon: 'success'
|
||
});
|
||
// 可以在这里添加完成任务的API调用
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
// 提交任务
|
||
const submitTask = () => {
|
||
uni.navigateTo({
|
||
url: `/pages/task/submit/index?taskId=${task.value.id || ''}`
|
||
});
|
||
};
|
||
|
||
// 申请延期
|
||
const applyDelay = () => {
|
||
uni.navigateTo({
|
||
url: `/pages/task/apply-delay/index?taskId=${task.value.id || ''}`
|
||
});
|
||
};
|
||
|
||
// 取消任务
|
||
const cancelTask = () => {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '确定要取消任务吗?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
console.log("取消任务", task.value.id);
|
||
uni.showToast({
|
||
title: '任务已取消',
|
||
icon: 'success'
|
||
});
|
||
// 可以在这里添加取消任务的API调用
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
// 加载任务数据
|
||
const loadTaskData = async (taskId) => {
|
||
if (!taskId) {
|
||
uni.showToast({
|
||
title: '任务ID不能为空',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 显示加载提示
|
||
uni.showLoading({
|
||
title: '加载中...'
|
||
});
|
||
|
||
// 调用 API 获取任务详情
|
||
const res = await getTaskDetail(taskId);
|
||
console.log('任务详情数据:', res);
|
||
|
||
// 转换数据格式
|
||
const taskStatus = res.status !== undefined ? res.status : null;
|
||
const expireTime = res.expireTime || null;
|
||
const statusTags = getStatusTags(taskStatus, expireTime);
|
||
|
||
// 转换提交记录
|
||
const submitRecords = transformSubmitRecords(res.submitList || []);
|
||
|
||
// 解析任务附件(图片和文件)
|
||
// 优先使用 file 字段,如果没有则使用 picture 字段(可能包含图片和文件)
|
||
let taskAttachments = { pictures: [], files: [] };
|
||
if (res.file) {
|
||
// 如果 API 返回了 file 字段,解析它
|
||
taskAttachments = parseTaskAttachments(res.file);
|
||
} else if (res.picture) {
|
||
// 如果只有 picture 字段,也解析它(可能包含图片和文件)
|
||
taskAttachments = parseTaskAttachments(res.picture);
|
||
}
|
||
|
||
// 更新任务数据
|
||
task.value = {
|
||
id: res.id || taskId,
|
||
name: res.description || '任务名称',
|
||
project: res.projectName || '',
|
||
statusTags: statusTags,
|
||
deadline: expireTime ? expireTime : '无',
|
||
creator: res.createName || '',
|
||
creatorAvatar: res.createAvatar || '',
|
||
responsible: getOwnerNames(res.memberList || []),
|
||
publishTime: res.createTime ? formatTimeToChinese(res.createTime) : '',
|
||
content: res.description || '',
|
||
pictures: taskAttachments.pictures, // 任务图片数组
|
||
files: taskAttachments.files, // 任务文件数组
|
||
submitRecords: submitRecords,
|
||
// 保存原始数据,供其他功能使用
|
||
rawData: res
|
||
};
|
||
|
||
uni.hideLoading();
|
||
} catch (err) {
|
||
console.error('加载任务详情失败:', err);
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: '加载任务详情失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
};
|
||
|
||
// 页面加载时接收参数
|
||
onLoad((options) => {
|
||
const taskId = options.id || options.taskId;
|
||
if (taskId) {
|
||
task.value.id = taskId;
|
||
// 优先从 API 加载数据
|
||
loadTaskData(taskId);
|
||
console.log('<UNK>:',userInfo.value.permissions );
|
||
}
|
||
else {
|
||
// // 如果没有 taskId,尝试从 Pinia store 获取任务详情数据(兼容旧逻辑)
|
||
// const taskStore = useTaskStore();
|
||
// const storedTask = taskStore.getTaskDetail;
|
||
// if (storedTask) {
|
||
// task.value = {
|
||
// ...task.value,
|
||
// ...storedTask
|
||
// };
|
||
// }
|
||
// else {
|
||
// uni.showToast({
|
||
// title: '缺少任务ID',
|
||
// icon: 'none'
|
||
// });
|
||
// setTimeout(() => {
|
||
// uni.navigateBack();
|
||
// }, 1500);
|
||
// }
|
||
}
|
||
});
|
||
|
||
// 转换旧格式的提交记录为新格式(兼容本地存储的数据)
|
||
const convertOldFormatRecord = (record) => {
|
||
// 如果已经是新格式(有 imageAttachments 和 fileAttachments),直接返回
|
||
if (record.imageAttachments !== undefined || record.fileAttachments !== undefined) {
|
||
return record;
|
||
}
|
||
|
||
// 如果是旧格式(有 attachments 数组),转换为新格式
|
||
if (record.attachments && Array.isArray(record.attachments)) {
|
||
const imageAttachments = [];
|
||
const fileAttachments = [];
|
||
|
||
record.attachments.forEach(att => {
|
||
if (att.type === 'image' && att.path) {
|
||
imageAttachments.push(att.path);
|
||
} else if (att.type === 'file') {
|
||
fileAttachments.push({
|
||
name: att.name || '文件',
|
||
path: att.path || ''
|
||
});
|
||
}
|
||
});
|
||
|
||
return {
|
||
...record,
|
||
imageAttachments: imageAttachments,
|
||
fileAttachments: fileAttachments
|
||
};
|
||
}
|
||
|
||
// 如果都没有,返回空数组
|
||
return {
|
||
...record,
|
||
imageAttachments: [],
|
||
fileAttachments: []
|
||
};
|
||
};
|
||
|
||
// 页面显示时检查是否有新的提交记录或更新的记录
|
||
onShow(() => {
|
||
// 检查是否有新的提交记录
|
||
const newSubmitRecord = uni.getStorageSync('newSubmitRecord');
|
||
if (newSubmitRecord) {
|
||
// 转换格式并添加到列表开头
|
||
const convertedRecord = convertOldFormatRecord(newSubmitRecord);
|
||
task.value.submitRecords.unshift(convertedRecord);
|
||
// 切换到提交记录标签页
|
||
activeTab.value = 'records';
|
||
// 清除存储的记录
|
||
uni.removeStorageSync('newSubmitRecord');
|
||
}
|
||
|
||
// 检查是否有更新的提交记录(编辑后的记录)
|
||
const updatedSubmitRecord = uni.getStorageSync('updatedSubmitRecord');
|
||
if (updatedSubmitRecord) {
|
||
const { recordIndex, record } = updatedSubmitRecord;
|
||
if (recordIndex !== undefined && recordIndex >= 0 && recordIndex < task.value.submitRecords.length) {
|
||
// 转换格式并更新指定索引的记录
|
||
const convertedRecord = convertOldFormatRecord(record);
|
||
task.value.submitRecords[recordIndex] = convertedRecord;
|
||
// 切换到提交记录标签页
|
||
activeTab.value = 'records';
|
||
}
|
||
// 清除存储的记录
|
||
uni.removeStorageSync('updatedSubmitRecord');
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.task-detail-container {
|
||
min-height: 100vh;
|
||
background-color: #f5f5f5;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* 顶部导航栏 */
|
||
.header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 16px;
|
||
background-color: #fff;
|
||
border-bottom: 1px solid #eee;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
}
|
||
|
||
.back-btn {
|
||
font-size: 20px;
|
||
color: #333;
|
||
padding: 4px;
|
||
}
|
||
|
||
.header-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.placeholder {
|
||
width: 28px;
|
||
}
|
||
|
||
/* 内容滚动区域 */
|
||
.content-scroll {
|
||
flex: 1;
|
||
height: calc(100vh - 60px);
|
||
}
|
||
|
||
/* 任务状态栏 */
|
||
.status-section {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
padding: 15px;
|
||
background-color: #fff;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.task-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
}
|
||
|
||
.task-name {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.project-name {
|
||
color: #666;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.status-tags {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
/* 基本信息区域 */
|
||
.basic-info {
|
||
padding: 15px;
|
||
background-color: #fff;
|
||
border-bottom: 1px solid #eee;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.info-item {
|
||
display: flex;
|
||
margin-bottom: 12px;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.info-label {
|
||
width: 80px;
|
||
color: #666;
|
||
font-size: 14px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.info-value {
|
||
flex: 1;
|
||
font-size: 14px;
|
||
color: #333;
|
||
}
|
||
|
||
/* 标签切换 */
|
||
.tab-container {
|
||
display: flex;
|
||
background-color: #fff;
|
||
border-bottom: 1px solid #eee;
|
||
padding: 0 16px;
|
||
}
|
||
|
||
.tab-item {
|
||
flex: 1;
|
||
padding: 16px 0;
|
||
text-align: center;
|
||
position: relative;
|
||
|
||
text {
|
||
font-size: 16px;
|
||
color: #666;
|
||
font-weight: 500;
|
||
}
|
||
|
||
&.active {
|
||
text {
|
||
color: #1976d2;
|
||
font-weight: 600;
|
||
}
|
||
|
||
&::after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 60px;
|
||
height: 3px;
|
||
background-color: #1976d2;
|
||
border-radius: 2px;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 标签页内容 */
|
||
.tab-content {
|
||
flex: 1;
|
||
padding: 16px;
|
||
background-color: #f5f5f5;
|
||
min-height: calc(100vh - 400px);
|
||
}
|
||
|
||
/* 任务信息卡片 */
|
||
.task-info-card {
|
||
background-color: #fff;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
position: relative;
|
||
}
|
||
|
||
.publish-time-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.clock-icon {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.publish-time-text {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.task-content-wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.task-content-text {
|
||
font-size: 15px;
|
||
line-height: 1.8;
|
||
color: #333;
|
||
}
|
||
|
||
.delay-btn-wrapper {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
/* 提交记录卡片 */
|
||
.submit-record-card {
|
||
background-color: #fff;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
margin-bottom: 12px;
|
||
position: relative;
|
||
}
|
||
|
||
.record-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.user-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.avatar-placeholder {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 50%;
|
||
background-color: #e0e0e0;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.avatar-img {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.info-value-with-avatar {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
color: #333;
|
||
}
|
||
|
||
.creator-avatar {
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.user-name {
|
||
font-size: 15px;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.record-header-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
position: relative;
|
||
}
|
||
|
||
.record-time {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
|
||
.more-menu {
|
||
position: relative;
|
||
padding: 4px 8px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.more-icon {
|
||
font-size: 20px;
|
||
color: #666;
|
||
font-weight: bold;
|
||
line-height: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transform: rotate(90deg);
|
||
}
|
||
|
||
.menu-dropdown {
|
||
position: absolute;
|
||
top: 100%;
|
||
right: 0;
|
||
background-color: #fff;
|
||
border: 1px solid #eee;
|
||
border-radius: 4px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
z-index: 100;
|
||
margin-top: 4px;
|
||
min-width: 80px;
|
||
}
|
||
|
||
.menu-item {
|
||
padding: 10px 16px;
|
||
border-bottom: 1px solid #f5f5f5;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
text {
|
||
font-size: 14px;
|
||
color: #333;
|
||
}
|
||
|
||
&:active {
|
||
background-color: #f5f5f5;
|
||
}
|
||
}
|
||
|
||
.record-content-wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.record-content-text {
|
||
font-size: 14px;
|
||
line-height: 1.8;
|
||
color: #333;
|
||
}
|
||
|
||
.record-progress {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
padding: 8px 12px;
|
||
background-color: #f5f5f5;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.progress-label {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.progress-value {
|
||
font-size: 14px;
|
||
color: #1976d2;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 任务图片展示(一行最多三张) */
|
||
.task-images-wrapper {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.task-image-item {
|
||
/* 一行三个:每个图片宽度 = (100% - 2个gap) / 3 */
|
||
width: calc((100% - 16px) / 3);
|
||
aspect-ratio: 1;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
background-color: #e0e0e0;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.task-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
/* 任务文件展示 */
|
||
.task-files-wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
/* 提交记录图片展示(一行三个) */
|
||
.record-images-wrapper {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.record-image-item {
|
||
/* 一行三个:每个图片宽度 = (100% - 2个gap) / 3 */
|
||
width: calc((100% - 16px) / 3);
|
||
aspect-ratio: 1;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
background-color: #e0e0e0;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.record-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
/* 文件附件展示 */
|
||
.record-files-wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.file-attachment-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
background-color: #f5f5f5;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
|
||
&:active {
|
||
background-color: #e0e0e0;
|
||
}
|
||
}
|
||
|
||
.file-icon {
|
||
font-size: 24px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.file-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.file-name {
|
||
font-size: 14px;
|
||
color: #333;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.file-size {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
|
||
.no-record {
|
||
color: #999;
|
||
font-size: 14px;
|
||
padding: 40px 0;
|
||
text-align: center;
|
||
}
|
||
|
||
/* 底部操作按钮 */
|
||
.action-buttons {
|
||
display: flex;
|
||
padding: 15px;
|
||
gap: 10px;
|
||
background-color: #ffffff;
|
||
border-top: 1px solid #eee;
|
||
position: fixed;
|
||
right: 0;
|
||
left: 0;
|
||
bottom: 0;
|
||
}
|
||
|
||
.btn-wrapper {
|
||
flex: 1;
|
||
|
||
|
||
}
|
||
</style>
|