| | |
| | | return `${BASE_URL}/public/file/download/${url}`; |
| | | } |
| | | |
| | | viewWillAppear() { |
| | | async viewWillAppear() { |
| | | this.props.form.resetFields(); |
| | | |
| | | const { editableData } = this.state; |
| | | |
| | | if (editableData && editableData.id) { |
| | | getProjectProgressDetail(editableData.id).then(res => { |
| | | getProjectProgressDetail(editableData.id).then(async res => { |
| | | if (res.data.status === 'OK') { |
| | | const data = res.data.data; |
| | | this.setState({ editableData: data }); |
| | |
| | | } |
| | | if (data.url) { |
| | | const urls = data.url.split(','); |
| | | const fileList = urls.map((url, index) => ({ |
| | | uid: -index - 1, |
| | | name: url, |
| | | status: 'done', |
| | | url: this.getPreviewUrl(url), |
| | | })); |
| | | // 检测所有文件类型后再设置 fileList |
| | | const fileList = await this.detectFileTypes(urls); |
| | | this.setState({ fileList }); |
| | | } else { |
| | | this.setState({ fileList: [] }); |
| | |
| | | |
| | | if (editableData && editableData.url) { |
| | | const urls = editableData.url.split(','); |
| | | const fileList = urls.map((url, index) => ({ |
| | | uid: -index - 1, |
| | | name: url, |
| | | status: 'done', |
| | | url: this.getPreviewUrl(url), |
| | | })); |
| | | // 检测所有文件类型后再设置 fileList |
| | | const fileList = await this.detectFileTypes(urls); |
| | | this.setState({ fileList }); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 检测所有文件类型(智能检测:有扩展名直接判断,无扩展名才发请求) |
| | | async detectFileTypes(urls) { |
| | | const filePromises = urls.map(async (url, index) => { |
| | | const fullUrl = this.getPreviewUrl(url); |
| | | |
| | | // 先尝试从扩展名判断 |
| | | let fileType = 'image'; |
| | | if (fullUrl.match(/\.(mp4|webm|ogg|avi|mov)$/i)) { |
| | | fileType = 'video'; |
| | | } else if (!fullUrl.match(/\.(jpg|jpeg|png|gif|bmp|svg|webp)$/i)) { |
| | | // 如果没有明确的图片或视频扩展名,通过 Content-Type 检测 |
| | | fileType = await this.getFileTypeByContentType(fullUrl); |
| | | } |
| | | |
| | | return { |
| | | uid: -index - 1, |
| | | name: url, |
| | | status: 'done', |
| | | url: fullUrl, |
| | | fileType: fileType, // 添加文件类型标识 |
| | | }; |
| | | }); |
| | | |
| | | return await Promise.all(filePromises); |
| | | } |
| | | |
| | | // 通过 Content-Type 判断文件类型 |
| | | async getFileTypeByContentType(url) { |
| | | try { |
| | | const res = await fetch(url, { method: 'GET' }); |
| | | const contentType = res.headers.get('Content-Type') || ''; |
| | | console.log(`File type detection - URL: ${url}, Content-Type: ${contentType}`); |
| | | |
| | | if (contentType.startsWith('video/')) { |
| | | return 'video'; |
| | | } else if (contentType.startsWith('image/')) { |
| | | return 'image'; |
| | | } |
| | | return 'image'; // 默认当作图片 |
| | | } catch (e) { |
| | | console.warn('Failed to detect file type:', url, e); |
| | | return 'image'; // 出错时默认当作图片 |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | }; |
| | | |
| | | handleUploadChange = ({ fileList }) => { |
| | | const newFileList = fileList.map(file => { |
| | | handleUploadChange = async ({ fileList }) => { |
| | | // 处理新上传的文件,添加完整URL和检测文件类型 |
| | | const newFileList = await Promise.all(fileList.map(async file => { |
| | | if (file.status === 'done' && !file.url && file.response && file.response.finalName) { |
| | | file.url = this.getPreviewUrl(file.response.finalName); |
| | | |
| | | // 检测新上传文件的类型 |
| | | if (!file.fileType) { |
| | | const url = file.url; |
| | | let fileType = 'image'; |
| | | |
| | | // 先从文件对象的 type 判断 |
| | | if (file.type && file.type.startsWith('video/')) { |
| | | fileType = 'video'; |
| | | } |
| | | // 再从扩展名判断 |
| | | else if (url.match(/\.(mp4|webm|ogg|avi|mov)$/i)) { |
| | | fileType = 'video'; |
| | | } |
| | | // 如果都无法判断,通过 Content-Type 检测 |
| | | else if (!url.match(/\.(jpg|jpeg|png|gif|bmp|svg|webp)$/i)) { |
| | | fileType = await this.getFileTypeByContentType(url); |
| | | } |
| | | |
| | | file.fileType = fileType; |
| | | } |
| | | } |
| | | return file; |
| | | }); |
| | | })); |
| | | |
| | | this.setState({ fileList: newFileList }); |
| | | |
| | |
| | | }); |
| | | } |
| | | |
| | | let isVideo = (url && url.match(/\.(mp4|webm|ogg)$/i)) || (file.type && file.type.startsWith('video/')); |
| | | |
| | | if (!isVideo && url && !url.startsWith('data:')) { |
| | | try { |
| | | const res = await fetch(url, { method: 'GET' }); |
| | | const contentType = res.headers.get('Content-Type'); |
| | | if (contentType && contentType.startsWith('video/')) { |
| | | isVideo = true; |
| | | // 优先使用已检测的文件类型 |
| | | let isVideo = false; |
| | | if (file.fileType) { |
| | | isVideo = file.fileType === 'video'; |
| | | } else { |
| | | // 如果没有预检测的类型,则从扩展名或 file.type 判断 |
| | | isVideo = (url && url.match(/\.(mp4|webm|ogg|avi|mov)$/i)) || (file.type && file.type.startsWith('video/')); |
| | | |
| | | // 如果还是无法判断,才发送请求检测 |
| | | if (!isVideo && url && !url.startsWith('data:')) { |
| | | try { |
| | | const res = await fetch(url, { method: 'GET' }); |
| | | const contentType = res.headers.get('Content-Type'); |
| | | if (contentType && contentType.startsWith('video/')) { |
| | | isVideo = true; |
| | | } |
| | | } catch (e) { |
| | | console.warn('Check file type failed', e); |
| | | } |
| | | } catch (e) { |
| | | console.warn('Check file type failed', e); |
| | | } |
| | | } |
| | | |
| | |
| | | onPreview={this.handlePreview} |
| | | accept="image/*,video/*" |
| | | multiple={true} |
| | | itemRender={(originNode, file) => { |
| | | // 自定义渲染,为视频添加角标 |
| | | return ( |
| | | <div style={{ position: 'relative' }}> |
| | | {originNode} |
| | | {file.fileType === 'video' && ( |
| | | <div style={{ |
| | | position: 'absolute', |
| | | top: '4px', |
| | | right: '4px', |
| | | width: '24px', |
| | | height: '24px', |
| | | background: 'rgba(0, 0, 0, 0.7)', |
| | | borderRadius: '50%', |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | justifyContent: 'center', |
| | | zIndex: 1, |
| | | pointerEvents: 'none' |
| | | }}> |
| | | <Icon type="video-camera" style={{ color: '#1890ff', fontSize: '12px' }} /> |
| | | </div> |
| | | )} |
| | | </div> |
| | | ); |
| | | }} |
| | | > |
| | | <div> |
| | | <Icon type="plus" /> |
| | |
| | | VUE_APP_BASE_URL=http://192.168.110.111:9908 |
| | | # VUE_APP_BASE_URL=http://221.182.45.100:8180 |
| | | VUE_APP_IMAGE_BASE_URL=http://221.182.45.100:11225/images/files/ |
| | | VUE_APP_IMAGE_BASE_URL=http://221.182.45.100:8180 |
| | |
| | | meta: { |
| | | title: '分项隐患数' |
| | | } |
| | | }, |
| | | { |
| | | path: '/projectProgress', |
| | | component: () => import('../view/projectProgress/index.vue'), |
| | | meta: { |
| | | title: '项目进度记录' |
| | | } |
| | | } |
| | | ] |
| | | |
| | |
| | | <div class="mt--16"> |
| | | <TitleBox> |
| | | <template #title> 项目信息 </template> |
| | | <template #right> |
| | | <div @click="toProjectDetail()" class="card-more"> |
| | | <span>项目进度</span> |
| | | <img src="../../assets/img/project/more.png" /> |
| | | </div> |
| | | </template> |
| | | <template #content> |
| | | <div class="project-info"> |
| | | <div class="info-item"> |
| | |
| | | </TitleBox> |
| | | </div> |
| | | </div> |
| | | <!-- 项目进度详情弹窗 --> |
| | | <CustomDialog |
| | | title="项目进度" |
| | | :visible.sync="progressVisible" |
| | | width="1200px" |
| | | @close="closeProgressDialog" |
| | | class="progress-dialog" |
| | | > |
| | | <!-- 历史进度按钮 - 使用绝对定位放在关闭按钮旁边 --> |
| | | <div class="history-btn-fixed" @click="toProgressHistory"> |
| | | <span>历史进度</span> |
| | | </div> |
| | | |
| | | <div class="progress-dialog-content" v-loading="progressLoading"> |
| | | <div v-if="latestProgress && latestProgress.id"> |
| | | <!-- 进度说明 --> |
| | | <div class="progress-description"> |
| | | {{ latestProgress.detail || '暂无进度说明' }} |
| | | </div> |
| | | |
| | | <!-- 进度图片和视频 - 缩略图网格 --> |
| | | <div class="progress-media-grid" v-if="progressMediaList.length > 0"> |
| | | <div |
| | | v-for="(item, index) in progressMediaList" |
| | | :key="index" |
| | | class="media-thumbnail" |
| | | :class="{ 'video-thumbnail': item.type === 'video' }" |
| | | @click="previewProgressMedia(item)" |
| | | > |
| | | <!-- 图片缩略图 --> |
| | | <img |
| | | v-if="item.type === 'image'" |
| | | :src="item.url" |
| | | class="thumbnail-img" |
| | | @error="handleImageError" |
| | | /> |
| | | <!-- 视频缩略图 --> |
| | | <video |
| | | v-else-if="item.type === 'video'" |
| | | :src="item.url" |
| | | class="thumbnail-video" |
| | | @error="handleVideoError" |
| | | ></video> |
| | | |
| | | <!-- 悬浮遮罩 --> |
| | | <div class="thumbnail-overlay"> |
| | | <i v-if="item.type === 'image'" class="el-icon-zoom-in"></i> |
| | | <i v-else-if="item.type === 'video'" class="el-icon-video-play"></i> |
| | | </div> |
| | | |
| | | <!-- 视频角标 --> |
| | | <div v-if="item.type === 'video'" class="video-badge"> |
| | | <i class="el-icon-video-camera"></i> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 无内容提示 --> |
| | | <div v-else class="no-images"> |
| | | <div class="no-images-text">暂无进度内容</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-else class="no-data"> |
| | | <div class="no-data-icon"> |
| | | <i class="el-icon-document"></i> |
| | | </div> |
| | | <div class="no-data-text">暂无进度信息</div> |
| | | </div> |
| | | </div> |
| | | </CustomDialog> |
| | | |
| | | <!-- 媒体预览弹窗 --> |
| | | <el-dialog |
| | | :visible.sync="previewVisible" |
| | | width="80%" |
| | | :show-close="true" |
| | | @close="closePreview" |
| | | class="media-preview-dialog" |
| | | append-to-body |
| | | > |
| | | <div class="preview-content"> |
| | | <!-- 图片预览 --> |
| | | <img |
| | | v-if="previewType === 'image'" |
| | | :src="previewUrl" |
| | | class="preview-image" |
| | | @error="handleImageError" |
| | | /> |
| | | <!-- 视频预览 --> |
| | | <video |
| | | v-else-if="previewType === 'video'" |
| | | :src="previewUrl" |
| | | class="preview-video" |
| | | controls |
| | | autoplay |
| | | @error="handleVideoError" |
| | | ></video> |
| | | </div> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import HeaderBox from "../header/index.vue"; |
| | | import moment from "moment"; |
| | | import { |
| | | getLoginInfo, |
| | | getProjectDetail, |
| | |
| | | getProjectPersonInfo, |
| | | getProjectSource, |
| | | getDangerTrend, |
| | | getProjectDangerousTrend |
| | | getProjectDangerousTrend, |
| | | getProjectLatestProgress |
| | | } from './service.js' |
| | | import { |
| | | transformRiskSourceData |
| | | } from '@/utils/riskSourceMap.js' |
| | | import moment from "moment"; |
| | | import * as echarts from "echarts"; |
| | | export default { |
| | | components: { |
| | |
| | | }, |
| | | // 9各分项隐患趋势 10隐患数量 11设备数量 12查看报告入口 13人员在岗率 14重大风险源 15 项目风险趋势 |
| | | permissionProject: JSON.parse(localStorage.getItem("permissionProject")) || [], |
| | | |
| | | // 项目进度相关 |
| | | progressVisible: false, // 进度弹窗显示状态 |
| | | progressLoading: false, // 进度加载状态 |
| | | latestProgress: {}, // 最新进度数据 |
| | | VUE_APP_BASE_URL: process.env.VUE_APP_IMAGE_BASE_URL, // 基础URL |
| | | progressMediaList: [], // 处理后的媒体列表(包含类型信息) |
| | | // 预览相关 |
| | | previewVisible: false, // 预览弹窗显示状态 |
| | | previewUrl: '', // 预览的URL |
| | | previewType: 'image', // 预览类型:'image' 或 'video' |
| | | }; |
| | | }, |
| | | computed: { |
| | |
| | | }, |
| | | beforeDestroy() { }, |
| | | methods: { |
| | | // 打开项目进度弹窗 |
| | | toProjectDetail() { |
| | | this.progressVisible = true; |
| | | this.getLatestProgress(); |
| | | }, |
| | | // 获取最新进度 |
| | | async getLatestProgress() { |
| | | this.progressLoading = true; |
| | | try { |
| | | const res = await getProjectLatestProgress(this.projectId); |
| | | if (res) { |
| | | this.latestProgress = res; |
| | | // 解析并检测媒体文件类型 |
| | | await this.detectMediaTypes(res.url); |
| | | } else { |
| | | this.latestProgress = {}; |
| | | this.progressMediaList = []; |
| | | } |
| | | } catch (error) { |
| | | console.error('获取最新进度失败:', error); |
| | | this.$message.error('获取最新进度失败'); |
| | | this.latestProgress = {}; |
| | | this.progressMediaList = []; |
| | | } finally { |
| | | this.progressLoading = false; |
| | | } |
| | | }, |
| | | // 关闭进度弹窗 |
| | | closeProgressDialog() { |
| | | this.progressVisible = false; |
| | | this.latestProgress = {}; |
| | | }, |
| | | // 跳转到历史进度页面 |
| | | toProgressHistory() { |
| | | this.progressVisible = false; |
| | | this.$router.push({ |
| | | path: "/projectProgress", |
| | | query: { |
| | | projectId: this.projectId, |
| | | projectName: this.projectName, |
| | | }, |
| | | }); |
| | | }, |
| | | // 格式化进度时间 |
| | | formatProgressTime(time) { |
| | | return time ? moment(time).format("YYYY-MM-DD HH:mm:ss") : "-"; |
| | | }, |
| | | // 通过 Content-Type 检测媒体文件类型(加载时检测所有文件) |
| | | async detectMediaTypes(urlStr) { |
| | | if (!urlStr) { |
| | | this.progressMediaList = []; |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | let fileIds = []; |
| | | |
| | | // 如果是数组,直接使用 |
| | | if (Array.isArray(urlStr)) { |
| | | fileIds = urlStr; |
| | | } |
| | | // 如果是逗号分隔的字符串 |
| | | else if (typeof urlStr === 'string' && urlStr.includes(',')) { |
| | | fileIds = urlStr.split(",").filter((item) => item.trim()); |
| | | } |
| | | // 如果是单个字符串 |
| | | else if (typeof urlStr === 'string' && urlStr.trim()) { |
| | | fileIds = [urlStr.trim()]; |
| | | } |
| | | |
| | | // 并发检测所有文件的类型 |
| | | const mediaPromises = fileIds.map(async (id) => { |
| | | let url = ''; |
| | | // 如果已经是完整的URL(http或https开头),直接使用 |
| | | if (id.startsWith('http://') || id.startsWith('https://')) { |
| | | url = id; |
| | | } else { |
| | | // 否则拼接为下载URL |
| | | url = `${this.VUE_APP_BASE_URL}/public/file/download/${id}`; |
| | | } |
| | | |
| | | // 先尝试从扩展名判断 |
| | | let type = 'image'; |
| | | if (url.match(/\.(mp4|webm|ogg|avi|mov)$/i)) { |
| | | type = 'video'; |
| | | } else { |
| | | // 如果没有扩展名,通过 Content-Type 检测 |
| | | type = await this.getFileTypeByContentType(url); |
| | | } |
| | | |
| | | return { |
| | | url: url, |
| | | type: type, // 'image' 或 'video' |
| | | id: id |
| | | }; |
| | | }); |
| | | |
| | | this.progressMediaList = await Promise.all(mediaPromises); |
| | | } catch (error) { |
| | | console.error('检测媒体文件类型失败:', error); |
| | | this.progressMediaList = []; |
| | | } |
| | | }, |
| | | // 通过 Content-Type 判断文件类型(使用 GET 请求,和后台管理保持一致) |
| | | async getFileTypeByContentType(url) { |
| | | try { |
| | | // 使用 GET 请求获取响应头(和 React 后台管理一致) |
| | | const response = await fetch(url, { |
| | | method: 'GET', |
| | | mode: 'cors' |
| | | }); |
| | | |
| | | const contentType = response.headers.get('Content-Type') || ''; |
| | | console.log(`URL: ${url}, Content-Type: ${contentType}`); |
| | | |
| | | // 判断是否为视频 |
| | | if (contentType.startsWith('video/')) { |
| | | return 'video'; |
| | | } |
| | | // 判断是否为图片 |
| | | else if (contentType.startsWith('image/')) { |
| | | return 'image'; |
| | | } |
| | | // 默认当作图片处理 |
| | | else { |
| | | return 'image'; |
| | | } |
| | | } catch (error) { |
| | | console.error('获取文件 Content-Type 失败:', url, error); |
| | | // 如果请求失败,默认当作图片处理 |
| | | return 'image'; |
| | | } |
| | | }, |
| | | // 预览进度媒体(图片或视频)- 弹窗预览 |
| | | previewProgressMedia(item) { |
| | | if (item && item.url) { |
| | | this.previewUrl = item.url; |
| | | this.previewType = item.type || 'image'; |
| | | this.previewVisible = true; |
| | | } |
| | | }, |
| | | // 关闭预览弹窗 |
| | | closePreview() { |
| | | this.previewVisible = false; |
| | | this.previewUrl = ''; |
| | | this.previewType = 'image'; |
| | | }, |
| | | // 图片加载失败处理 |
| | | handleImageError(e) { |
| | | e.target.src = require("../../assets/img/logo.png"); |
| | | }, |
| | | // 视频加载失败处理 |
| | | handleVideoError(e) { |
| | | console.error('视频加载失败:', e); |
| | | // 可以在这里添加视频加载失败的处理逻辑 |
| | | }, |
| | | // 获取项目详情 |
| | | getDetail() { |
| | | // 设备类型 - 添加错误处理 |
| | |
| | | } |
| | | |
| | | .card-more { |
| | | width: 62px; |
| | | min-width: 62px; |
| | | height: 24px; |
| | | background: rgba(14, 93, 251, 0.5); |
| | | border-radius: 4px; |
| | |
| | | font-size: 12px; |
| | | color: #ffffff; |
| | | line-height: 12px; |
| | | margin-right: 10px; |
| | | } |
| | | |
| | | img { |
| | |
| | | } |
| | | |
| | | } |
| | | |
| | | // 项目进度弹窗 - 历史进度按钮固定定位 |
| | | .progress-dialog { |
| | | ::v-deep .el-dialog__header { |
| | | position: relative; |
| | | } |
| | | } |
| | | |
| | | .history-btn-fixed { |
| | | position: absolute; |
| | | right: 70px; |
| | | top: 20px; |
| | | z-index: 10; |
| | | width: 120px; |
| | | height: 40px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | background: linear-gradient(270deg, #09B9FF 0%, #4D68FF 100%); |
| | | border-radius: 4px; |
| | | cursor: pointer; |
| | | transition: all 0.3s ease; |
| | | |
| | | &:hover { |
| | | opacity: 0.9; |
| | | transform: translateY(-1px); |
| | | box-shadow: 0 4px 8px rgba(9, 185, 255, 0.4); |
| | | } |
| | | |
| | | span { |
| | | font-family: SourceHanSansCN, SourceHanSansCN; |
| | | font-weight: 500; |
| | | font-size: 16px; |
| | | color: #FFFFFF; |
| | | line-height: 24px; |
| | | text-align: center; |
| | | font-style: normal; |
| | | margin-right: 8px; |
| | | } |
| | | |
| | | .arrow-icon { |
| | | width: 12px; |
| | | height: 12px; |
| | | } |
| | | } |
| | | |
| | | // 项目进度弹窗样式 |
| | | .progress-dialog-content { |
| | | min-height: 500px; |
| | | padding: 30px 40px; |
| | | background: transparent; |
| | | |
| | | .progress-description { |
| | | font-size: 16px; |
| | | color: rgba(255, 255, 255, 0.9); |
| | | line-height: 1.8; |
| | | margin-bottom: 30px; |
| | | text-align: justify; |
| | | word-break: break-word; |
| | | } |
| | | |
| | | // 进度媒体网格 - 缩略图展示 |
| | | .progress-media-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); |
| | | gap: 16px; |
| | | margin-top: 20px; |
| | | |
| | | .media-thumbnail { |
| | | position: relative; |
| | | width: 100%; |
| | | height: 150px; |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | cursor: pointer; |
| | | border: 2px solid rgba(50, 197, 255, 0.3); |
| | | background: rgba(0, 22, 53, 0.6); |
| | | transition: all 0.3s ease; |
| | | |
| | | &:hover { |
| | | border-color: rgba(50, 197, 255, 0.8); |
| | | transform: translateY(-4px); |
| | | box-shadow: 0 8px 20px rgba(14, 93, 251, 0.5); |
| | | |
| | | .thumbnail-overlay { |
| | | opacity: 1; |
| | | } |
| | | } |
| | | |
| | | .thumbnail-img, |
| | | .thumbnail-video { |
| | | width: 100%; |
| | | height: 100%; |
| | | object-fit: cover; |
| | | display: block; |
| | | } |
| | | |
| | | .thumbnail-overlay { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: rgba(0, 0, 0, 0.6); |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | opacity: 0; |
| | | transition: opacity 0.3s ease; |
| | | |
| | | i { |
| | | font-size: 32px; |
| | | color: #ffffff; |
| | | } |
| | | } |
| | | |
| | | .video-badge { |
| | | position: absolute; |
| | | top: 8px; |
| | | right: 8px; |
| | | width: 28px; |
| | | height: 28px; |
| | | background: rgba(0, 0, 0, 0.7); |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | z-index: 1; |
| | | |
| | | i { |
| | | font-size: 14px; |
| | | color: #32C5FF; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .no-images { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | min-height: 200px; |
| | | |
| | | .no-images-text { |
| | | font-size: 16px; |
| | | color: rgba(255, 255, 255, 0.5); |
| | | } |
| | | } |
| | | |
| | | .no-data { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | min-height: 400px; |
| | | padding: 60px 24px; |
| | | |
| | | .no-data-icon { |
| | | width: 80px; |
| | | height: 80px; |
| | | background: rgba(24, 83, 200, 0.2); |
| | | border: 2px solid rgba(50, 197, 255, 0.3); |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-bottom: 20px; |
| | | |
| | | i { |
| | | font-size: 40px; |
| | | color: rgba(50, 197, 255, 0.6); |
| | | } |
| | | } |
| | | |
| | | .no-data-text { |
| | | font-size: 16px; |
| | | color: rgba(255, 255, 255, 0.6); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 媒体预览弹窗样式 |
| | | .media-preview-dialog { |
| | | ::v-deep .el-dialog { |
| | | background: rgba(0, 0, 0, 0.95); |
| | | border: 1px solid rgba(50, 197, 255, 0.3); |
| | | box-shadow: 0 0 30px rgba(14, 93, 251, 0.5); |
| | | } |
| | | |
| | | ::v-deep .el-dialog__header { |
| | | display: none; |
| | | } |
| | | |
| | | ::v-deep .el-dialog__body { |
| | | padding: 0; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | min-height: 400px; |
| | | } |
| | | |
| | | ::v-deep .el-dialog__close { |
| | | color: #ffffff; |
| | | font-size: 24px; |
| | | |
| | | &:hover { |
| | | color: #32C5FF; |
| | | } |
| | | } |
| | | |
| | | .preview-content { |
| | | width: 100%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | padding: 20px; |
| | | |
| | | .preview-image, |
| | | .preview-video { |
| | | max-width: 100%; |
| | | max-height: 80vh; |
| | | object-fit: contain; |
| | | border-radius: 8px; |
| | | } |
| | | |
| | | .preview-video { |
| | | background: #000000; |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | | |
| | |
| | | data: params |
| | | }) |
| | | } |
| | | |
| | | // 获取项目最新进度 |
| | | export const getProjectLatestProgress = async (projectId) => { |
| | | return request(`/project/getProjectProgress/${projectId}`, { |
| | | method: 'get' |
| | | }) |
| | | } |
| New file |
| | |
| | | <template> |
| | | <div> |
| | | <HeaderBox :title="projectName" /> |
| | | <div class="pt--35 px--20 pb--54"> |
| | | <!-- 返回 --> |
| | | <div @click="backFun" class="flex a-center cursor-pointer"> |
| | | <img |
| | | src="../../assets/img/back.png" |
| | | class="w--22 h--20 shrink0 mr--13" |
| | | /> |
| | | <div class="c-fff lh--22 fs--22 SHSBold">项目进度记录</div> |
| | | </div> |
| | | <!-- 搜索 --> |
| | | <div class="flex a-center gap--16 mt--24 mb--20"> |
| | | <div |
| | | @click="handleClick(1)" |
| | | class="flex a-center j-between px--20 w--300 h--40 relative bgcolor1 picker" |
| | | > |
| | | <el-date-picker |
| | | v-model="startTime" |
| | | ref="datetime1" |
| | | type="datetime" |
| | | placeholder="" |
| | | > |
| | | </el-date-picker> |
| | | <div |
| | | class="absolute w100 h100 flex a-center c-fff fs--14 SHS" |
| | | style="width: calc(100% - 20px)" |
| | | v-if="startTimeTxt" |
| | | > |
| | | {{ startTimeTxt }} |
| | | </div> |
| | | <div |
| | | v-else |
| | | class="absolute w100 h100 flex a-center c-fff fs--14 SHSR" |
| | | > |
| | | 请选择开始时间 |
| | | </div> |
| | | <img |
| | | src="../../assets/img/home/Shape@2x (6).png" |
| | | class="w--13 h--13 shrink0" |
| | | /> |
| | | </div> |
| | | <div |
| | | @click="handleClick(2)" |
| | | class="flex a-center j-between px--20 w--300 h--40 relative bgcolor1 picker" |
| | | > |
| | | <el-date-picker |
| | | v-model="endTime" |
| | | ref="datetime2" |
| | | type="datetime" |
| | | placeholder="" |
| | | > |
| | | </el-date-picker> |
| | | <div |
| | | class="absolute w100 h100 flex a-center c-fff fs--14 SHS" |
| | | style="width: calc(100% - 20px)" |
| | | v-if="endTimeTxt" |
| | | > |
| | | {{ endTimeTxt }} |
| | | </div> |
| | | <div |
| | | v-else |
| | | class="absolute w100 h100 flex a-center c-fff fs--14 SHSR" |
| | | > |
| | | 请选择结束时间 |
| | | </div> |
| | | <img |
| | | src="../../assets/img/home/Shape@2x (6).png" |
| | | class="w--13 h--13 shrink0" |
| | | /> |
| | | </div> |
| | | <div |
| | | class="flex a-center j-between w--300 pl--20 h--40 relative bgcolor1 searchBox" |
| | | > |
| | | <el-input |
| | | v-model="searchKeyword" |
| | | placeholder="请输入搜索内容" |
| | | class="flex1" |
| | | clearable |
| | | @keyup.enter.native="handleSearch" |
| | | /> |
| | | <div @click="handleSearch" class="btn SHSR">搜索</div> |
| | | </div> |
| | | </div> |
| | | <!-- 表格 --> |
| | | <CustomTable |
| | | :data="progressList" |
| | | height="800" |
| | | style="border: 2px solid #3d7fff" |
| | | v-loading="loading" |
| | | > |
| | | <el-table-column label="进度说明" prop="detail" min-width="200" /> |
| | | <el-table-column label="图片/张" min-width="120"> |
| | | <template #default="{ row }"> |
| | | {{ getImageCount(row.url) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="进度更新日期" min-width="180"> |
| | | <template #default="{ row }"> |
| | | {{ formatTime(row.time) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="100" fixed="right"> |
| | | <template #default="{ row }"> |
| | | <el-button |
| | | @click="openDetailDialog(row)" |
| | | type="text" |
| | | class="btnColor" |
| | | >详情</el-button |
| | | > |
| | | </template> |
| | | </el-table-column> |
| | | </CustomTable> |
| | | <CustomPagination |
| | | @size-change="handleSizeChange" |
| | | @current-change="handleCurrentChange" |
| | | :current-page="currentPage" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | :page-size="pageSize" |
| | | :total="total" |
| | | /> |
| | | </div> |
| | | <!-- 进度详情弹窗 --> |
| | | <CustomDialog |
| | | title="进度详情" |
| | | :visible.sync="detailVisible" |
| | | width="1420px" |
| | | @close="closeDetailDialog" |
| | | > |
| | | <div class="oa" style="height: 700px" v-loading="loading"> |
| | | <div class="flex j-between gap--30 mt--30"> |
| | | <div class="flex1"> |
| | | <div class="SHSR fs--16 lh--16 mb--17 color1">进度说明:</div> |
| | | <div class="lh--60 px--19 c-fff SHSBold fs--18 w100 bgcolor8"> |
| | | {{ progressDetail.detail || "-" }} |
| | | </div> |
| | | </div> |
| | | <div class="flex1"> |
| | | <div class="SHSR fs--16 lh--16 mb--17 color1">进度更新日期:</div> |
| | | <div class="lh--60 px--19 c-fff SHSBold fs--18 w100 bgcolor8"> |
| | | {{ formatTime(progressDetail.time) }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="mt--30"> |
| | | <div class="flex1"> |
| | | <div class="SHSR fs--16 lh--16 mb--17 color1">进度图片:</div> |
| | | <div |
| | | class="flex a-center oa gap--14" |
| | | v-if="parseImages(progressDetail.url).length > 0" |
| | | > |
| | | <img |
| | | v-for="(item, index) in parseImages(progressDetail.url)" |
| | | :key="index" |
| | | :src="item" |
| | | class="w--150 h--120 cursor-pointer" |
| | | @click="previewImage(item)" |
| | | /> |
| | | </div> |
| | | <div v-else class="c-fff fs--14 SHSR">暂无图片</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </CustomDialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import HeaderBox from "../header/index.vue"; |
| | | import moment from "moment"; |
| | | import { |
| | | getProjectProgressList, |
| | | getProjectProgressDetail, |
| | | } from "./service"; |
| | | |
| | | export default { |
| | | components: { HeaderBox }, |
| | | props: {}, |
| | | data() { |
| | | return { |
| | | // 搜索条件 |
| | | startTime: null, // 开始时间 |
| | | endTime: null, // 结束时间 |
| | | searchKeyword: "", // 搜索关键词 |
| | | startTimeTxt: "", |
| | | endTimeTxt: "", |
| | | |
| | | // 项目信息 |
| | | projectId: null, |
| | | projectName: null, |
| | | |
| | | // 分页 |
| | | currentPage: 1, |
| | | pageSize: 10, |
| | | total: 0, |
| | | |
| | | // 列表数据 |
| | | progressList: [], // 进度列表 |
| | | progressDetail: {}, // 进度详情 |
| | | |
| | | // 弹窗控制 |
| | | detailVisible: false, |
| | | |
| | | // 当前选中的数据 |
| | | currentProgressId: null, // 当前选中的进度ID |
| | | |
| | | loading: false, |
| | | }; |
| | | }, |
| | | computed: {}, |
| | | watch: { |
| | | startTime: { |
| | | handler(newVal) { |
| | | if (newVal) { |
| | | this.startTimeTxt = moment(newVal).format("YYYY年MM月DD日 HH:mm:ss"); |
| | | } else { |
| | | this.startTimeTxt = ""; |
| | | } |
| | | }, |
| | | immediate: true, |
| | | }, |
| | | endTime: { |
| | | handler(newVal) { |
| | | if (newVal) { |
| | | this.endTimeTxt = moment(newVal).format("YYYY年MM月DD日 HH:mm:ss"); |
| | | } else { |
| | | this.endTimeTxt = ""; |
| | | } |
| | | }, |
| | | immediate: true, |
| | | }, |
| | | }, |
| | | created() { |
| | | this.projectId = this.$route.query.projectId; |
| | | this.projectName = this.$route.query.projectName; |
| | | }, |
| | | mounted() { |
| | | this.initData(); |
| | | }, |
| | | methods: { |
| | | // 初始化数据 |
| | | async initData() { |
| | | await this.getProgressList(); |
| | | }, |
| | | |
| | | // 获取进度列表 |
| | | async getProgressList() { |
| | | this.loading = true; |
| | | try { |
| | | const params = { |
| | | pageNum: this.currentPage, |
| | | pageSize: this.pageSize, |
| | | projectId: this.projectId, |
| | | }; |
| | | |
| | | // 添加可选搜索条件 |
| | | if (this.startTime) { |
| | | params.startTime = moment(this.startTime).format( |
| | | "YYYY-MM-DD HH:mm:ss" |
| | | ); |
| | | } |
| | | if (this.endTime) { |
| | | params.endTime = moment(this.endTime).format("YYYY-MM-DD HH:mm:ss"); |
| | | } |
| | | if (this.searchKeyword) { |
| | | params.content = this.searchKeyword; // 使用 content 字段 |
| | | } |
| | | |
| | | const res = await getProjectProgressList(params); |
| | | if (res) { |
| | | this.progressList = res.records || res.list || []; |
| | | this.total = res.total || 0; |
| | | } |
| | | } catch (error) { |
| | | console.error("获取进度列表失败:", error); |
| | | this.$message.error("获取进度列表失败"); |
| | | } finally { |
| | | this.loading = false; |
| | | } |
| | | }, |
| | | |
| | | // 搜索 |
| | | handleSearch() { |
| | | this.currentPage = 1; |
| | | this.getProgressList(); |
| | | }, |
| | | |
| | | // 分页 - 每页条数改变 |
| | | handleSizeChange(val) { |
| | | this.pageSize = val; |
| | | this.currentPage = 1; |
| | | this.getProgressList(); |
| | | }, |
| | | |
| | | // 分页 - 当前页改变 |
| | | handleCurrentChange(val) { |
| | | this.currentPage = val; |
| | | this.getProgressList(); |
| | | }, |
| | | |
| | | // 打开进度详情弹窗 |
| | | async openDetailDialog(row) { |
| | | this.currentProgressId = row.id; |
| | | this.detailVisible = true; |
| | | await this.getProgressDetail(); |
| | | }, |
| | | |
| | | // 获取进度详情 |
| | | async getProgressDetail() { |
| | | if (!this.currentProgressId) { |
| | | this.$message.warning("缺少进度ID"); |
| | | return; |
| | | } |
| | | |
| | | this.loading = true; |
| | | try { |
| | | const res = await getProjectProgressDetail(this.currentProgressId); |
| | | if (res) { |
| | | this.progressDetail = res; |
| | | } |
| | | } catch (error) { |
| | | console.error("获取进度详情失败:", error); |
| | | this.$message.error("获取进度详情失败"); |
| | | } finally { |
| | | this.loading = false; |
| | | } |
| | | }, |
| | | |
| | | // 关闭进度详情弹窗 |
| | | closeDetailDialog() { |
| | | this.detailVisible = false; |
| | | this.progressDetail = {}; |
| | | this.currentProgressId = null; |
| | | }, |
| | | |
| | | // 日期选择器点击 |
| | | handleClick(type) { |
| | | this.$refs[`datetime${type}`].focus(); |
| | | }, |
| | | |
| | | // 返回 |
| | | backFun() { |
| | | this.$router.go(-1); |
| | | }, |
| | | |
| | | // 格式化时间 |
| | | formatTime(time) { |
| | | return time ? moment(time).format("YYYY-MM-DD HH:mm:ss") : "-"; |
| | | }, |
| | | |
| | | // 获取图片数量 |
| | | getImageCount(imgStr) { |
| | | if (!imgStr) return 0; |
| | | if (Array.isArray(imgStr)) return imgStr.length; |
| | | try { |
| | | return imgStr.split(",").filter((item) => item).length; |
| | | } catch (error) { |
| | | return 0; |
| | | } |
| | | }, |
| | | |
| | | // 处理图片数组(字符串转数组) |
| | | parseImages(imgStr) { |
| | | if (!imgStr) return []; |
| | | if (Array.isArray(imgStr)) return imgStr; |
| | | try { |
| | | return imgStr.split(",").filter((item) => item); |
| | | } catch (error) { |
| | | return []; |
| | | } |
| | | }, |
| | | |
| | | // 预览图片 |
| | | previewImage(url) { |
| | | window.open(url, "_blank"); |
| | | }, |
| | | }, |
| | | beforeDestroy() {}, |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped lang="less"> |
| | | .color1 { |
| | | color: rgba(255, 255, 255, 0.9); |
| | | } |
| | | |
| | | .bgcolor1 { |
| | | background: rgba(2, 0, 77, 0.8); |
| | | box-shadow: inset 0px 0px 34px 0px rgba(17, 40, 255, 0.66); |
| | | border: 2px solid #0d53b7; |
| | | } |
| | | |
| | | .bgcolor8 { |
| | | background: rgba(24, 83, 200, 0.2); |
| | | } |
| | | |
| | | .picker { |
| | | ::v-deep .el-input__icon { |
| | | display: none; |
| | | } |
| | | |
| | | ::v-deep .el-range-separator { |
| | | display: none; |
| | | } |
| | | |
| | | ::v-deep .el-input__inner { |
| | | background: transparent; |
| | | border: none; |
| | | color: transparent; |
| | | } |
| | | |
| | | ::v-deep .el-range-input { |
| | | background: transparent; |
| | | border: none; |
| | | color: transparent; |
| | | } |
| | | } |
| | | |
| | | .searchBox { |
| | | ::v-deep .el-input__inner { |
| | | background: transparent; |
| | | border: none; |
| | | color: #fff; |
| | | padding: 0; |
| | | } |
| | | |
| | | ::v-deep .el-input__suffix { |
| | | display: none; |
| | | } |
| | | |
| | | .btn { |
| | | position: absolute; |
| | | top: -2px; |
| | | right: -2px; |
| | | background: linear-gradient( |
| | | 90deg, |
| | | #3467d0 0%, |
| | | #0d226a 50.35%, |
| | | #376fe0 100% |
| | | ); |
| | | height: 38px; |
| | | display: flex; |
| | | align-items: center; |
| | | font-size: 14px; |
| | | color: #ffffff; |
| | | padding: 0 26px; |
| | | border: 1px solid #307ad9; |
| | | box-sizing: border-box; |
| | | cursor: pointer; |
| | | } |
| | | } |
| | | |
| | | .btnColor { |
| | | color: #00ebfc; |
| | | } |
| | | </style> |
| | | |
| New file |
| | |
| | | import request from '@/utils/request' |
| | | |
| | | // 获取项目进度历史列表 |
| | | export const getProjectProgressList = async (data) => { |
| | | return request(`/project/getProjectProgressList`, { |
| | | method: 'post', |
| | | data |
| | | }) |
| | | } |
| | | |
| | | // 获取项目进度详情 |
| | | export const getProjectProgressDetail = async (id) => { |
| | | return request(`/project/getProjectProgressDetail/${id}`, { |
| | | method: 'get' |
| | | }) |
| | | } |
| | | |