<template>
|
<div class="relative">
|
<HeaderBox :title="projectName" />
|
<!-- 地图 -->
|
<div id="container" style="width: 1920px; height: 995px; background-color: #001635"></div>
|
<!-- 左边 内容 -->
|
<div class="left-card">
|
<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">
|
<div class="flex2 flex">
|
<div class="label">项目规模</div>
|
<div class="value">{{ `${projectInfo.scale}平方米` || '-' }}</div>
|
</div>
|
<div class="flex1 flex">
|
<div class="label">工程类型</div>
|
<div class="value">{{ ['房建工程','市政工程','固定资产'][projectInfo.type] }}</div>
|
</div>
|
</div>
|
<div class="info-item">
|
<div class="flex2 flex">
|
<div class="label">项目地址</div>
|
<div class="value">{{ projectInfo.address || '-' }}</div>
|
</div>
|
<div class="flex1 flex">
|
<div class="label">项目阶段</div>
|
<div class="value">{{ projectInfo.progress || '-' }}</div>
|
</div>
|
</div>
|
<div class="info-item">
|
<div class="flex2 flex">
|
<div class="label">施工单位</div>
|
<div class="value">{{ projectInfo.developmentCompanyInfo.name || '-' }}</div>
|
</div>
|
<div class="flex1 flex">
|
<div class="label">项目经理</div>
|
<div class="value">{{ projectInfo.developmentCompanyInfo.leader || '-' }}</div>
|
</div>
|
</div>
|
<div class="info-item">
|
<div class="flex2 flex">
|
<div class="label">监理单位</div>
|
<div class="value">{{ projectInfo.supervisorCompanyInfo.name || '-' }}</div>
|
</div>
|
<div class="flex1 flex">
|
<div class="label">总监</div>
|
<div class="value">{{ projectInfo.supervisorCompanyInfo.leader || '-' }}</div>
|
</div>
|
</div>
|
<div class="info-item">
|
<div class="flex2 flex">
|
<div class="label">建设单位</div>
|
<div class="value">{{ projectInfo.constructionCompanyInfo.name || '-' }}</div>
|
</div>
|
<div class="flex1 flex">
|
<div class="label">项目负责人</div>
|
<div class="value">{{ projectInfo.constructionCompanyInfo.leader || '-' }}</div>
|
</div>
|
</div>
|
<div class="info-item" v-for="item in projectInfo.properties">
|
<div class="flex2 flex">
|
<div class="label dept">设备租赁单位</div>
|
<div class="value">{{ JSON.parse(item)['工程起重机械租赁单位'] || '-' }}</div>
|
</div>
|
<div class="flex1 flex">
|
<div class="label">联系人</div>
|
<div class="value">{{ JSON.parse(item)['联系电话'] || '-' }}</div>
|
</div>
|
</div>
|
<div class="info-item">
|
<div class="flex">
|
<div class="label time">最近一次检查时间</div>
|
<div class="value">{{ projectInfo.checkTime || '-' }}</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
</TitleBox>
|
</div>
|
<div class="mt--30" @click="toEquipmentInfo" v-if="permissionProject.includes('11')">
|
<TitleBox>
|
<template #title> 设备信息 </template>
|
<template #content>
|
<div class="equipment-info">
|
<img src="../../assets//img/project/chartBg.png" class="chart-bg" alt="" />
|
<div class="pie-canvas" ref="pieChart"></div>
|
<div class="legend">
|
<div class="legend-item" v-for="(it, idx) in legendData" :key="idx">
|
<div class="dot" :style="{ backgroundColor: it.itemStyle.color }"></div>
|
<div class="legend-info">
|
<div class="legend-name" :title="it.name">{{ it.name }}
|
</div>
|
<div class="legend-number">{{ it.value }}</div>
|
</div>
|
</div>
|
</div>
|
<div class="total-card">
|
<div class="total ff54-">{{ equipment || 0 }}</div>
|
<div class="text">设备数量</div>
|
</div>
|
</div>
|
</template>
|
</TitleBox>
|
</div>
|
<div class="mt--30" v-if="permissionProject.includes('9')">
|
<TitleBox>
|
<template #title>各分项隐患趋势 </template>
|
<template #right>
|
<div @click="toRiskTrend()" class="card-more">
|
<span>详情</span>
|
<img src="../../assets/img/project/more.png" />
|
</div>
|
</template>
|
<template #content>
|
<div class="danger-info">
|
<div class="info-header">
|
<span>隐患类型</span>
|
<div class="header-line" @click="changeDangerSort(1)">
|
<span>隐患数量</span>
|
<img v-if="dangerSortType === 1 && dangerSortDesc" src="../../assets/img/project/up.png" />
|
<img v-else-if="dangerSortType === 1 && !dangerSortDesc" src="../../assets/img/project/down.png" />
|
<img v-else src="../../assets/img/project/down.png" style="opacity: 0.3;" />
|
</div>
|
<div class="header-line" @click="changeDangerSort(2)">
|
<span>趋势</span>
|
<img v-if="dangerSortType === 2 && dangerSortDesc" src="../../assets/img/project/up.png" />
|
<img v-else-if="dangerSortType === 2 && !dangerSortDesc" src="../../assets/img/project/down.png" />
|
<img v-else src="../../assets/img/project/down.png" style="opacity: 0.3;" />
|
</div>
|
</div>
|
<div class="info-body">
|
<div class="info-item" v-for="(item, index) in dangerTrendList" :key="index">
|
<div class="item-type" :title="item.name || '-'">{{ item.name || '-' }}</div>
|
<div class="item-number">
|
<div class="flex a--center">
|
<div class="flex a--center">
|
<span>本月</span>
|
<div class="number">{{ item.thisMonth || 0 }}</div>
|
</div>
|
<div class="now" :style="{ width: getBarWidth(item.thisMonth, item.lastMonth) }"></div>
|
</div>
|
<div class="flex a--center">
|
<div class="flex a--center">
|
<span>上月</span>
|
<div class="number">{{ item.lastMonth || 0 }}</div>
|
</div>
|
<div class="before" :style="{ width: getBarWidth(item.lastMonth, item.thisMonth) }"></div>
|
</div>
|
</div>
|
<div class="item-trend" :class="{ 'down': item.rate < 0 }">
|
<i v-if="item.rate < 0" class="el-icon-bottom trend-icon"></i>
|
<i v-else-if="item.rate > 0" class="el-icon-top trend-icon"></i>
|
<span>{{ Math.abs(item.rate) }}%</span>
|
</div>
|
</div>
|
</div>
|
|
<div class="info-bottom"></div>
|
</div>
|
</template>
|
</TitleBox>
|
</div>
|
</div>
|
<!-- 中间内容 -->
|
<div class="mid-card">
|
<div @click="toHiddenDanger(1)" class="mid-info" v-if="permissionProject.includes('10')">
|
<div class="flex a--center">
|
<div @click.stop="toHiddenDanger(2)" class="mid-title">隐患数量</div>
|
<div class="flex a--center tab-border">
|
<div class="mid-tab">所有隐患数</div>
|
<div class="mid-tab active-tab">重大隐患数</div>
|
</div>
|
</div>
|
<div class="mid-content">
|
<div class="mid-img">
|
<div class="number">{{ dangerCount.serious || 0 }}</div>
|
<div class="name">所有隐患数</div>
|
</div>
|
<div class="mid-total">
|
<div class="mid-item">
|
<div class="mid-txt">已整改</div>
|
<div class="mid-num">{{ dangerCount.rectified || 0 }}</div>
|
</div>
|
<div class="mid-item">
|
<div class="mid-txt">未整改</div>
|
<div class="mid-num">{{ dangerCount.unRectified || 0 }}</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
<div v-if="permissionProject.includes('12')" @click="toPlanOfAudit" class="mid-more">查看报告</div>
|
</div>
|
<!-- 右边 -->
|
<div class="right-card">
|
<div class="mt--16" v-if="permissionProject.includes('13')">
|
<TitleBox>
|
<template #title>人员在岗率 </template>
|
<template #right>
|
<div @click="toPersonnelOnDuty" class="card-more">
|
<span>详情</span>
|
<img src="../../assets/img/project/more.png" />
|
</div>
|
</template>
|
<template #content>
|
<div class="people-info">
|
<div class="people-card">
|
<div class="people-item" v-for="(item, index) in peopleList.slice(0, 3)" :key="index">
|
<div class="people-chart">
|
<el-progress :width="74" type="circle" :show-text="false" :percentage="parseFloat(item.rate) || 0"
|
define-back-color="#000" color="#32C5FF" stroke-linecap="square"></el-progress>
|
<div class="progress-card">
|
<div class="progress-num">{{ item.rate }}</div>
|
<div class="progress-unit">%</div>
|
</div>
|
</div>
|
<div class="people-name">{{ item.postName || item.name }}</div>
|
</div>
|
</div>
|
<div class="people-card2">
|
<div class="people-item" :class="{ 'mr--67': index === 0 }"
|
v-for="(item, index) in peopleList.slice(3, 5)" :key="index + 3">
|
<div class="people-chart">
|
<el-progress :width="74" type="circle" :show-text="false" :percentage="parseFloat(item.rate) || 0"
|
define-back-color="#000" color="#32C5FF" stroke-linecap="square"></el-progress>
|
<div class="progress-card">
|
<div class="progress-num">{{ item.rate }}</div>
|
<div class="progress-unit">%</div>
|
</div>
|
</div>
|
<div class="people-name">{{ item.name }}</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
</TitleBox>
|
</div>
|
<div class="mt--30" v-if="permissionProject.includes('14')">
|
<TitleBox>
|
<template #title>重大风险源 </template>
|
<template #right>
|
<div @click="toMajorRiskList" class="card-more">
|
<span>详情</span>
|
<img src="../../assets/img/project/more.png" />
|
</div>
|
</template>
|
<template #content>
|
<div class="risk-info">
|
<div class="risk-top">
|
<div class="label card1">序号</div>
|
<div class="label card2">风险规模</div>
|
<div class="label flex1">风险源内容</div>
|
<div class="label card4">实施情况</div>
|
</div>
|
<div class="risk-body">
|
<div class="risk-item" v-for="(item, index) in riskSourceList" :key="index">
|
<div class="label card1">{{ index + 1 }}</div>
|
<div class="label card2" :class="{
|
'merged-cell': item.showRiskScale && item.rowspan > 1,
|
'hidden-cell': !item.showRiskScale
|
}">
|
<div v-if="item.showRiskScale" class="merged-content" :style="{
|
height: `${item.rowspan * 36}px`,
|
lineHeight: `${item.rowspan * 36}px`
|
}">
|
{{ item.riskScale }}
|
</div>
|
</div>
|
<div class="label flex1">{{ item.content }}</div>
|
<div class="label card4">{{ item.status }}</div>
|
</div>
|
</div>
|
</div>
|
|
</template>
|
</TitleBox>
|
</div>
|
<div class="mt--30" v-if="permissionProject.includes('15')">
|
<TitleBox>
|
<template #title>项目风险趋势 </template>
|
<template #right>
|
<div class="risk-picker">
|
<div class="risk-ab">
|
<img src="../../assets/img/home/Shape@2x (6).png" class="pick-img" />
|
|
<div class="pick-text">
|
{{ dateTxt }}
|
</div>
|
</div>
|
</div>
|
</template>
|
<template #content>
|
<div class="risk-trend">
|
<div id="riskTrendChart" style="width: 100%; height: 234px"></div>
|
</div>
|
</template>
|
</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,
|
getProjectDeviceCount,
|
getDangerCount,
|
deviceTypes,
|
getProjectPersonInfo,
|
getProjectSource,
|
getDangerTrend,
|
getProjectDangerousTrend,
|
getProjectLatestProgress
|
} from './service.js'
|
import {
|
transformRiskSourceData
|
} from '@/utils/riskSourceMap.js'
|
import * as echarts from "echarts";
|
export default {
|
components: {
|
HeaderBox,
|
},
|
data() {
|
return {
|
mapView: null,
|
mapPromise: null,
|
currentMarker: null, // 保存当前的 marker 实例
|
dateRange: [moment().subtract(5, 'months').startOf("month"), moment().endOf("month")], //默认近6个月
|
markers: [],
|
projectId: '',
|
projectName: '',
|
// 项目详情信息
|
projectInfo: {
|
addressInfo: {},
|
address: '',
|
constructionCompanyInfo: {},
|
developmentCompanyInfo: {},
|
supervisorCompanyInfo: {},
|
properties: []
|
},//项目信息
|
dangerCount: {
|
serious: 0,
|
rectified: 0,
|
unRectified: 0
|
},//危险源统计
|
deviceTypeList: [],//设备类型列表
|
equipment: 0,//设备数量
|
peopleList: [],//人员列表
|
|
// pie数据
|
legendData: [],
|
dateTxt: '',
|
dangerTrendList: [], //分项隐患趋势数据
|
dangerSortType: 1, // 排序类型:1-隐患数量,2-趋势
|
dangerSortDesc: true, // 是否倒序
|
// 风险源数据
|
riskSourceData: [],
|
// 项目风险趋势数据
|
riskTrendData: {
|
xAxisData: [],
|
seriesData: []
|
},
|
// 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: {
|
// 处理风险源数据,添加合并单元格的逻辑
|
riskSourceList() {
|
if (!this.riskSourceData || this.riskSourceData.length === 0) {
|
return [];
|
}
|
|
const result = [];
|
let i = 0;
|
|
while (i < this.riskSourceData.length) {
|
const currentRiskScale = this.riskSourceData[i].riskScale;
|
let rowspan = 1;
|
|
// 计算相同风险规模的行数
|
for (let j = i + 1; j < this.riskSourceData.length; j++) {
|
if (this.riskSourceData[j].riskScale === currentRiskScale) {
|
rowspan++;
|
} else {
|
break;
|
}
|
}
|
|
// 添加第一行,显示风险规模
|
result.push({
|
...this.riskSourceData[i],
|
showRiskScale: true,
|
rowspan: rowspan
|
});
|
|
// 添加后续相同风险规模的行,不显示风险规模
|
for (let k = 1; k < rowspan; k++) {
|
result.push({
|
...this.riskSourceData[i + k],
|
showRiskScale: false,
|
rowspan: 0
|
});
|
}
|
|
i += rowspan;
|
}
|
|
return result;
|
}
|
},
|
watch: {
|
dateRange: {
|
handler(newVal, oldVal) {
|
if (newVal.length > 0) {
|
this.dateTxt =
|
moment(newVal[0]).format("YYYY -MM") +
|
" - " +
|
moment(newVal[1]).format("YYYY -MM");
|
// 当日期范围改变时,重新获取风险趋势数据
|
if (this.projectId) {
|
this.getProjectRiskTrend();
|
}
|
}
|
},
|
immediate: true,
|
},
|
},
|
created() {
|
this.mapPromise = AMapLoader.load({
|
key: "232929db1cb31f26d42b3645a7456154",
|
version: "2.0",
|
});
|
if (this.$route.query.projectId) {
|
this.projectId = this.$route.query.projectId || ''
|
// 添加错误处理,确保登录信息获取失败不影响其他模块
|
getLoginInfo({ id: this.projectId, type: 2 }).then(res => {
|
|
this.projectName = res.name
|
// const permissions = res.permissionProject ? res.permissionProject.split(",") : []
|
// this.permissionProject = permissions
|
// localStorage.setItem('permissionProject', JSON.stringify(permissions));
|
localStorage.setItem('deviceTemplateId', res.deviceTemplateId) // 设备模板id
|
localStorage.setItem("safetyId", res.safetyId) // 安全报告模板id
|
localStorage.setItem("bigHiddenDanger", res.bigHiddenDanger) // 1开启 2关闭 重大隐患
|
localStorage.setItem("generalHiddenDanger", res.generalHiddenDanger) // 1开启 2关闭 一般隐患
|
localStorage.setItem("commentType", res.commentType) // 1模糊评价 2打分评价
|
}).catch(err => {
|
console.error('获取登录信息失败:', err)
|
// 设置默认值,确保页面可以继续渲染
|
this.projectName = '项目详情'
|
// this.permissionProject = []
|
})
|
this.getDetail()
|
this.getDangerTrendList()
|
this.getProjectRiskTrend()
|
}
|
},
|
mounted() {
|
this.setMapload()
|
setTimeout(() => {
|
this.initRiskChart()
|
}, 1000)
|
},
|
activated() {
|
// 当从其他页面返回时,重新设置地图 marker
|
if (this.mapView && this.projectInfo.type !== undefined) {
|
this.updateMapMarker()
|
}
|
},
|
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}/project/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') || '';
|
|
|
// 判断是否为视频
|
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() {
|
// 设备类型 - 添加错误处理
|
deviceTypes().then(res => {
|
this.deviceTypeList = res
|
}).catch(err => {
|
console.error('获取设备类型失败:', err)
|
this.deviceTypeList = []
|
})
|
|
// 获取项目详情 - 添加错误处理
|
getProjectDetail(this.projectId).then(res => {
|
const addressInfo = res.addressInfo ? JSON.parse(res.addressInfo) : {}
|
const constructionCompanyInfo = res.constructionCompanyInfo ? JSON.parse(res.constructionCompanyInfo) : {}
|
const developmentCompanyInfo = res.developmentCompanyInfo ? JSON.parse(res.developmentCompanyInfo) : {}
|
const supervisorCompanyInfo = res.supervisorCompanyInfo ? JSON.parse(res.supervisorCompanyInfo) : {}
|
|
this.projectInfo = {
|
...res,
|
addressInfo: addressInfo,
|
address: (addressInfo.province || '') + (addressInfo.city || '') + (addressInfo.district || '') + (addressInfo.detail || ''),
|
constructionCompanyInfo: constructionCompanyInfo,
|
developmentCompanyInfo: developmentCompanyInfo,
|
supervisorCompanyInfo: supervisorCompanyInfo,
|
}
|
|
// 项目信息加载完成后,更新地图 marker
|
this.$nextTick(() => {
|
this.updateMapMarker()
|
})
|
|
}).catch(err => {
|
console.error('获取项目详情失败:', err)
|
// 设置默认值
|
this.projectInfo = {
|
addressInfo: {},
|
address: '',
|
constructionCompanyInfo: {},
|
developmentCompanyInfo: {},
|
supervisorCompanyInfo: {},
|
properties: []
|
}
|
})
|
|
// 设备数量 - 添加错误处理
|
getProjectDeviceCount(this.projectId).then(res => {
|
this.equipment = 0
|
this.legendData = res.map(item => {
|
this.equipment += item.count
|
const deviceType = this.deviceTypeList[item.type]
|
return {
|
...item,
|
value: item.count || 0,
|
name: deviceType && deviceType.name ? deviceType.name : '未知设备',
|
itemStyle: { color: this.generateRandomColor() }
|
}
|
})
|
this.initPie3D()
|
}).catch(err => {
|
console.error('获取设备数量失败:', err)
|
this.equipment = 0
|
this.legendData = []
|
})
|
|
// 隐患数量 - 添加错误处理
|
getDangerCount(this.projectId).then(res => {
|
this.dangerCount = res
|
|
}).catch(err => {
|
console.error('获取隐患数量失败:', err)
|
this.dangerCount = {
|
serious: 0,
|
rectified: 0,
|
unRectified: 0
|
}
|
})
|
|
// 项目人员信息 - 添加错误处理
|
getProjectPersonInfo({ projectId: this.projectId }).then(res => {
|
this.peopleList = res || []
|
}).catch(err => {
|
console.error('获取人员信息失败:', err)
|
this.peopleList = []
|
})
|
|
// 获取项目风险源 - 添加错误处理
|
getProjectSource({ projectId: this.projectId }).then(res => {
|
const riskSourceList = []
|
|
// 处理超危大工程
|
if (res.superDangerousOptions) {
|
try {
|
const superDangerousArray = JSON.parse(res.superDangerousOptions)
|
const transformedData = transformRiskSourceData(superDangerousArray, 'superDangerous', '超危大工程')
|
riskSourceList.push(...transformedData)
|
} catch (e) {
|
console.error('解析超危大工程数据失败:', e)
|
}
|
}
|
|
// 处理危大工程
|
if (res.dangerousOptions) {
|
try {
|
const dangerousArray = JSON.parse(res.dangerousOptions)
|
const transformedData = transformRiskSourceData(dangerousArray, 'dangerous', '危大工程')
|
riskSourceList.push(...transformedData)
|
} catch (e) {
|
console.error('解析危大工程数据失败:', e)
|
}
|
}
|
|
// 处理风险源
|
if (res.hazardSource) {
|
try {
|
const hazardSourceArray = JSON.parse(res.hazardSource)
|
const transformedData = transformRiskSourceData(hazardSourceArray, 'hazardSource', '风险源')
|
riskSourceList.push(...transformedData)
|
} catch (e) {
|
console.error('解析风险源数据失败:', e)
|
}
|
}
|
|
this.riskSourceData = riskSourceList
|
}).catch(err => {
|
console.error('获取项目风险源失败:', err)
|
this.riskSourceData = []
|
})
|
},
|
// 获取隐患趋势
|
getDangerTrendList() {
|
let params = {
|
projectId: this.projectId,
|
type: this.dangerSortType,// 1数量 2 趋势
|
desc: this.dangerSortDesc, // 是否倒序
|
}
|
getDangerTrend(params).then(res => {
|
this.dangerTrendList = res || []
|
}).catch(err => {
|
console.error('获取隐患趋势失败:', err)
|
this.dangerTrendList = []
|
})
|
},
|
// 切换隐患排序方式
|
changeDangerSort(type) {
|
if (this.dangerSortType === type) {
|
// 如果点击的是当前排序类型,则切换升序/降序
|
this.dangerSortDesc = !this.dangerSortDesc
|
} else {
|
// 如果点击的是不同的排序类型,则切换类型并默认倒序
|
this.dangerSortType = type
|
this.dangerSortDesc = true
|
}
|
// 重新请求数据
|
this.getDangerTrendList()
|
},
|
// 计算进度条宽度
|
getBarWidth(current, compare) {
|
const max = Math.max(current, compare)
|
if (max === 0) return '0px'
|
const percentage = (current / max) * 100
|
const maxWidth = 114 // 最大宽度
|
return `${(percentage / 100) * maxWidth}px`
|
},
|
// 人员在岗率
|
toPersonnelOnDuty() {
|
this.$router.push({
|
path: "/personnelOnDuty",
|
query: {
|
projectId: this.projectId,
|
projectName: this.projectName,
|
},
|
});
|
},
|
// 跳转隐患列表
|
toHiddenDanger(type) {
|
this.$router.push({
|
path: "/hiddenDanger",
|
query: {
|
projectId: this.projectId,
|
projectName: this.projectName,
|
activeTab: type,
|
},
|
});
|
},
|
// 跳转设备列表
|
toEquipmentInfo() {
|
this.$router.push({
|
path: "/equipmentList",
|
query: {
|
projectId: this.projectId,
|
projectName: this.projectName,
|
},
|
});
|
},
|
// 跳转重大危险源
|
toMajorRiskList() {
|
this.$router.push({
|
path: "/majorRisk",
|
query: {
|
projectId: this.projectId,
|
projectName: this.projectName,
|
},
|
});
|
},
|
toPlanOfAudit() {
|
this.$router.push({
|
path: "/planOfAudit",
|
query: {
|
projectId: this.projectId,
|
projectName: this.projectName,
|
type: 2
|
},
|
});
|
},
|
setMapload() {
|
if (!this.mapPromise) {
|
this.mapPromise = AMapLoader.load({
|
key: "232929db1cb31f26d42b3645a7456154",
|
version: "2.0",
|
});
|
}
|
this.mapPromise
|
.then((AMap) => {
|
// 微信建筑图层
|
let satellite = new AMap.TileLayer.Satellite();
|
//创建路网图层
|
let roadNet = new AMap.TileLayer.RoadNet();
|
this.mapView = new AMap.Map("container", {
|
zoom: 13,
|
center: [104.065617, 30.65695], // 初始化地图中心点
|
// mapStyle: "amap://styles/darkblue",
|
layers: [satellite],
|
});
|
|
// 初始化时不立即添加 marker,等待项目信息加载完成后再添加
|
// this.initCategoryMarkers(AMap);
|
})
|
.catch((e) => {
|
console.error(e); //加载错误提示
|
});
|
},
|
// 更新地图 marker
|
updateMapMarker() {
|
if (!this.mapView) return;
|
|
// 清除旧的 marker
|
if (this.currentMarker) {
|
this.mapView.remove(this.currentMarker);
|
}
|
|
// 根据项目类型选择图标
|
const iconImages = [
|
require('../../assets/img/home/f4.png'),
|
require('../../assets/img/home/s4.png'),
|
require('../../assets/img/home/z4.png')
|
];
|
|
const iconImage = iconImages[this.projectInfo.type] || iconImages[0];
|
|
// 创建新的 marker
|
this.currentMarker = new AMap.Marker({
|
position: [104.065617, 30.65695],
|
icon: new AMap.Icon({
|
image: iconImage,
|
size: new AMap.Size(30, 30),
|
imageSize: new AMap.Size(30, 30)
|
}),
|
offset: new AMap.Pixel(-15, -15)
|
});
|
|
// 将 marker 添加到地图
|
this.mapView.add(this.currentMarker);
|
},
|
// 初始化 3D 环饼图(echarts-gl)
|
initPie3D() {
|
const el = this.$refs.pieChart;
|
if (!el) return;
|
this._pie = echarts.init(el);
|
|
// 使用动态数据或默认数据
|
const data = this.legendData.length > 0 ? this.legendData : [];
|
|
// 生成 3D 饼图 option(internalDiameterRatio 控制环形内径比)
|
this.legendData = data; // 用于右侧自定义 legend
|
const series = this.getPie3D(data, 0.7);
|
|
const option = {
|
legend: {
|
tooltip: {
|
show: true,
|
},
|
data: data.map((item) => item.name),
|
right: "2%",
|
top: "center",
|
orient: "vertical",
|
itemGap: 20,
|
textStyle: {
|
color: "#fff",
|
fontSize: 12,
|
},
|
show: false,
|
},
|
graphic: {
|
elements: [
|
{
|
type: "circle",
|
shape: {
|
cx: "50%",
|
cy: "50%",
|
r: "35%",
|
},
|
style: {
|
fill: "transparent",
|
shadowBlur: 20,
|
shadowColor: "rgba(0, 191, 255, 0.4)",
|
opacity: 0.6,
|
},
|
z: -1,
|
},
|
],
|
},
|
animation: true,
|
tooltip: {
|
formatter: (params) => {
|
if (
|
params.seriesName !== "mouseoutSeries" &&
|
params.seriesName !== "pie2d"
|
) {
|
const series = option.series[params.seriesIndex];
|
if (series && series.pieData) {
|
return `${params.seriesName}<br/><span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${params.color};"></span>${series.pieData.value}`;
|
}
|
}
|
},
|
textStyle: {
|
fontSize: 12,
|
},
|
},
|
title: {
|
x: "center",
|
top: "20",
|
textStyle: {
|
color: "#fff",
|
fontSize: 12,
|
},
|
},
|
backgroundColor: "transparent",
|
labelLine: {
|
show: false,
|
lineStyle: {
|
color: "transparent",
|
},
|
},
|
label: {
|
show: false,
|
color: "transparent",
|
position: "outside",
|
formatter: "{b} \n{c} {d}%",
|
},
|
xAxis3D: {
|
min: -1,
|
max: 1,
|
},
|
yAxis3D: {
|
min: -1,
|
max: 1,
|
},
|
zAxis3D: {
|
min: -1,
|
max: 1,
|
},
|
grid3D: {
|
show: false,
|
boxHeight: 2,
|
bottom: "-10%",
|
left: "-15%",
|
viewControl: {
|
alpha: 18,
|
beta: 50,
|
distance: 115,
|
autoRotate: true,
|
rotateSensitivity: 0,
|
zoomSensitivity: 0,
|
panSensitivity: 0,
|
},
|
},
|
series: series,
|
};
|
|
this._pie.clear();
|
this._pie.setOption(option);
|
|
// 鼠标事件交互
|
let selectedIndex = "";
|
let hoveredIndex = "";
|
|
this._pie.on("click", (params) => {
|
if (params.seriesName === "mouseoutSeries") return;
|
const s = option.series[params.seriesIndex];
|
if (!s || !s.pieData || !s.pieStatus) return;
|
|
const isSelected = !s.pieStatus.selected;
|
const isHovered = s.pieStatus.hovered;
|
const k = s.pieStatus.k;
|
const startRatio = s.pieData.startRatio;
|
const endRatio = s.pieData.endRatio;
|
|
if (selectedIndex !== "" && selectedIndex !== params.seriesIndex) {
|
const prev = option.series[selectedIndex];
|
if (prev && prev.pieData && prev.pieStatus) {
|
prev.parametricEquation = this.getParametricEquation(
|
prev.pieData.startRatio,
|
prev.pieData.endRatio,
|
false,
|
false,
|
k,
|
prev.pieData.value
|
);
|
prev.pieStatus.selected = false;
|
}
|
}
|
|
s.parametricEquation = this.getParametricEquation(
|
startRatio,
|
endRatio,
|
isSelected,
|
isHovered,
|
k,
|
s.pieData.value
|
);
|
s.pieStatus.selected = isSelected;
|
if (isSelected) selectedIndex = params.seriesIndex;
|
|
this._pie.setOption(option);
|
});
|
|
this._pie.on("mouseover", (params) => {
|
if (hoveredIndex === params.seriesIndex) return;
|
|
// 取消之前的高亮
|
if (hoveredIndex !== "") {
|
let prev = option.series[hoveredIndex];
|
if (prev && prev.pieData && prev.pieStatus) {
|
const k = prev.pieStatus.k;
|
prev.parametricEquation = this.getParametricEquation(
|
prev.pieData.startRatio,
|
prev.pieData.endRatio,
|
prev.pieStatus.selected,
|
false,
|
k,
|
prev.pieData.value
|
);
|
prev.pieStatus.hovered = false;
|
hoveredIndex = "";
|
}
|
}
|
|
// 当前高亮(排除透明圆环)
|
if (params.seriesName !== "mouseoutSeries") {
|
let cur = option.series[params.seriesIndex];
|
if (cur && cur.pieData && cur.pieStatus) {
|
const k = cur.pieStatus.k;
|
cur.parametricEquation = this.getParametricEquation(
|
cur.pieData.startRatio,
|
cur.pieData.endRatio,
|
cur.pieStatus.selected,
|
true,
|
k,
|
cur.pieData.value
|
);
|
cur.pieStatus.hovered = true;
|
hoveredIndex = params.seriesIndex;
|
}
|
}
|
|
this._pie.setOption(option);
|
});
|
|
this._pie.on("globalout", () => {
|
if (hoveredIndex !== "") {
|
let prev = option.series[hoveredIndex];
|
if (prev && prev.pieData && prev.pieStatus) {
|
const k = prev.pieStatus.k;
|
prev.parametricEquation = this.getParametricEquation(
|
prev.pieData.startRatio,
|
prev.pieData.endRatio,
|
prev.pieStatus.selected,
|
false,
|
k,
|
prev.pieData.value
|
);
|
prev.pieStatus.hovered = false;
|
hoveredIndex = "";
|
this._pie.setOption(option);
|
}
|
}
|
});
|
},
|
// 生成随机颜色
|
generateRandomColor() {
|
const h = Math.floor(Math.random() * 360);
|
const s = Math.floor(Math.random() * 30) + 60; // 60-90%
|
const l = Math.floor(Math.random() * 20) + 50; // 50-70%
|
return `hsl(${h}, ${s}%, ${l}%)`;
|
},
|
// 生成扇形的曲面参数方程
|
getParametricEquation(
|
startRatio,
|
endRatio,
|
isSelected,
|
isHovered,
|
k,
|
height
|
) {
|
// 计算
|
let midRatio = (startRatio + endRatio) / 2;
|
|
let startRadian = startRatio * Math.PI * 2;
|
let endRadian = endRatio * Math.PI * 2;
|
let midRadian = midRatio * Math.PI * 2;
|
|
// 如果只有一个扇形,则不实现选中效果。
|
if (startRatio === 0 && endRatio === 1) {
|
isSelected = false;
|
}
|
|
// 通过扇形内径/外径的值,换算出辅助参数 k(默认值 1/3)
|
k = typeof k !== "undefined" ? k : 1 / 3;
|
|
// 计算选中效果分别在 x 轴、y 轴方向上的位移(未选中,则位移均为 0)
|
let offsetX = isSelected ? Math.cos(midRadian) * 0.1 : 0;
|
let offsetY = isSelected ? Math.sin(midRadian) * 0.1 : 0;
|
|
// 计算高亮效果的放大比例(未高亮,则比例为 1)
|
let hoverRate = isHovered ? 1.05 : 1;
|
|
// 返回曲面参数方程
|
return {
|
u: {
|
min: -Math.PI,
|
max: Math.PI * 3,
|
step: Math.PI / 32,
|
},
|
v: {
|
min: 0,
|
max: Math.PI * 2,
|
step: Math.PI / 20,
|
},
|
x: function (u, v) {
|
if (u < startRadian) {
|
return (
|
offsetX +
|
Math.cos(startRadian) * (1 + Math.cos(v) * k) * hoverRate
|
);
|
}
|
if (u > endRadian) {
|
return (
|
offsetX + Math.cos(endRadian) * (1 + Math.cos(v) * k) * hoverRate
|
);
|
}
|
return offsetX + Math.cos(u) * (1 + Math.cos(v) * k) * hoverRate;
|
},
|
y: function (u, v) {
|
if (u < startRadian) {
|
return (
|
offsetY +
|
Math.sin(startRadian) * (1 + Math.cos(v) * k) * hoverRate
|
);
|
}
|
if (u > endRadian) {
|
return (
|
offsetY + Math.sin(endRadian) * (1 + Math.cos(v) * k) * hoverRate
|
);
|
}
|
return offsetY + Math.sin(u) * (1 + Math.cos(v) * k) * hoverRate;
|
},
|
z: function (u, v) {
|
if (u < -Math.PI * 0.5) {
|
return Math.sin(u);
|
}
|
if (u > Math.PI * 2.5) {
|
return Math.sin(u);
|
}
|
return Math.sin(v) > 0 ? 0.6 * height : -0.6;
|
},
|
};
|
},
|
|
// 生成模拟 3D 饼图的配置项
|
getPie3D(pieData, internalDiameterRatio) {
|
let series = [];
|
let sumValue = 0;
|
let startValue = 0;
|
let endValue = 0;
|
let legendData = [];
|
let k =
|
typeof internalDiameterRatio !== "undefined"
|
? (1 - internalDiameterRatio) / (1 + internalDiameterRatio)
|
: 1 / 3;
|
|
// 为每一个饼图数据,生成一个 series-surface 配置
|
for (let i = 0; i < pieData.length; i++) {
|
sumValue += pieData[i].value;
|
|
let seriesItem = {
|
name:
|
typeof pieData[i].name === "undefined"
|
? `series${i}`
|
: pieData[i].name,
|
type: "surface",
|
parametric: true,
|
wireframe: {
|
show: false,
|
},
|
pieData: pieData[i],
|
pieStatus: {
|
selected: false,
|
hovered: false,
|
k: k,
|
},
|
};
|
|
if (typeof pieData[i].itemStyle != "undefined") {
|
let itemStyle = {};
|
|
typeof pieData[i].itemStyle.color != "undefined"
|
? (itemStyle.color = pieData[i].itemStyle.color)
|
: null;
|
typeof pieData[i].itemStyle.opacity != "undefined"
|
? (itemStyle.opacity = pieData[i].itemStyle.opacity)
|
: null;
|
|
seriesItem.itemStyle = itemStyle;
|
}
|
series.push(seriesItem);
|
}
|
|
// 使用上一次遍历时,计算出的数据和 sumValue,调用 getParametricEquation 函数,
|
// 向每个 series-surface 传入不同的参数方程 series-surface.parametricEquation,也就是实现每一个扇形。
|
for (let i = 0; i < series.length; i++) {
|
endValue = startValue + series[i].pieData.value;
|
|
series[i].pieData.startRatio = startValue / sumValue;
|
series[i].pieData.endRatio = endValue / sumValue;
|
series[i].parametricEquation = this.getParametricEquation(
|
series[i].pieData.startRatio,
|
series[i].pieData.endRatio,
|
false,
|
false,
|
k,
|
series[i].pieData.value
|
);
|
|
startValue = endValue;
|
|
legendData.push(series[i].name);
|
}
|
// series.push({
|
// name: 'mouseoutSeries',
|
// type: 'surface',
|
// parametric: true,
|
// wireframe: {
|
// show: false,
|
// },
|
// itemStyle: {
|
// opacity: 0.1,
|
// color: "#008fff",
|
// },
|
// parametricEquation: {
|
// u: {
|
// min: 0,
|
// max: Math.PI * 2,
|
// step: Math.PI / 20,
|
// },
|
// v: {
|
// min: 0,
|
// max: Math.PI,
|
// step: Math.PI / 20,
|
// },
|
// x: function (u, v) {
|
// return ((Math.sin(v) * Math.sin(u) + Math.sin(u)) / Math.PI) * 2.2;
|
// },
|
// y: function (u, v) {
|
// return ((Math.sin(v) * Math.cos(u) + Math.cos(u)) / Math.PI) * 2.2;
|
// },
|
// z: function (u, v) {
|
// return Math.cos(v) > 0 ? -7 : -7;
|
// },
|
// },
|
// });
|
return series;
|
},
|
// 风险等级转换为数值 (A->3, B->2, C->1)
|
levelToValue(level) {
|
if (!level) return null;
|
const levelStr = String(level).toUpperCase().trim();
|
|
// 处理 A、B、C 或 A类、B类、C类
|
if (levelStr === 'A' || levelStr === 'A类') return 3;
|
if (levelStr === 'B' || levelStr === 'B类') return 2;
|
if (levelStr === 'C' || levelStr === 'C类') return 1;
|
|
return null;
|
},
|
// 数值转换为风险等级显示 (3->A类, 2->B类, 1->C类)
|
valueToLevel(value) {
|
if (value === 3) return 'A类';
|
if (value === 2) return 'B类';
|
if (value === 1) return 'C类';
|
return '';
|
},
|
// 获取项目风险趋势数据
|
async getProjectRiskTrend() {
|
if (!this.dateRange || this.dateRange.length !== 2) {
|
return;
|
}
|
|
const params = {
|
projectId: this.projectId,
|
startTime: moment(this.dateRange[0]).format('YYYY-MM'),
|
endTime: moment(this.dateRange[1]).format('YYYY-MM')
|
};
|
|
try {
|
const res = await getProjectDangerousTrend(params);
|
|
|
if (res && Array.isArray(res)) {
|
// 处理接口返回的数据
|
this.riskTrendData.xAxisData = res.map(item => item.time || '');
|
// 将A、B、C转换为数值 3、2、1
|
this.riskTrendData.seriesData = res.map(item => {
|
const value = this.levelToValue(item.level);
|
|
return value;
|
});
|
|
// 更新图表
|
this.updateRiskChart();
|
}
|
} catch (error) {
|
console.error('获取项目风险趋势失败:', error)
|
// 设置默认值,确保图表可以正常显示
|
this.riskTrendData.xAxisData = []
|
this.riskTrendData.seriesData = []
|
}
|
},
|
// 更新风险趋势图表
|
updateRiskChart() {
|
const chartDom = document.getElementById('riskTrendChart');
|
if (!chartDom) {
|
console.warn('风险趋势图表DOM不存在,可能是权限限制导致该模块未渲染');
|
return;
|
}
|
const riskChart = echarts.getInstanceByDom(chartDom);
|
if (!riskChart) {
|
console.warn('风险趋势图表实例不存在');
|
return;
|
}
|
|
// 文本换行处理函数
|
const formatLabel = (text, maxWidth = 4) => {
|
if (!text) return ''
|
const textStr = String(text)
|
// 如果文本长度超过最大宽度,进行换行
|
if (textStr.length > maxWidth) {
|
let result = ''
|
for (let i = 0; i < textStr.length; i += maxWidth) {
|
result += textStr.substring(i, i + maxWidth) + '\n'
|
}
|
return result.trim()
|
}
|
return textStr
|
}
|
|
const self = this;
|
|
const option = {
|
tooltip: {
|
trigger: 'axis',
|
show: true,
|
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
borderColor: '#32C5FF',
|
borderWidth: 1,
|
textStyle: {
|
color: '#fff',
|
fontSize: 12
|
},
|
formatter: function (params) {
|
if (params && params.length > 0) {
|
const param = params[0];
|
const levelText = self.valueToLevel(param.value);
|
return `${param.name}<br/>${param.seriesName}: ${levelText}`;
|
}
|
return '';
|
}
|
},
|
legend: {
|
show: false
|
},
|
grid: {
|
top: '10%',
|
bottom: '10%',
|
left: '10%',
|
right: '5%',
|
containLabel: true
|
},
|
xAxis: {
|
data: this.riskTrendData.xAxisData,
|
axisTick: { show: true, alignWithLabel: true },
|
axisLine: {
|
lineStyle: {
|
color: '#666'
|
}
|
},
|
axisLabel: {
|
color: '#fff',
|
fontSize: 12,
|
interval: 0,
|
rotate: 0,
|
formatter: function (value) {
|
return formatLabel(value, 7)
|
},
|
align: 'center',
|
verticalAlign: 'top',
|
lineHeight: 14,
|
width: 60,
|
overflow: 'break'
|
}
|
},
|
yAxis: {
|
type: 'value',
|
min: 1,
|
max: 3,
|
interval: 1,
|
splitLine: {
|
show: true,
|
lineStyle: {
|
type: 'dashed',
|
color: 'rgba(255, 255, 255, .2)'
|
}
|
},
|
axisLine: {
|
show: false
|
},
|
axisTick: {
|
show: false
|
},
|
axisLabel: {
|
color: '#fff',
|
fontSize: 12,
|
formatter: function (value) {
|
// Y轴自定义显示:1->C类, 2->B类, 3->A类
|
if (value === 1) return 'C类';
|
if (value === 2) return 'B类';
|
if (value === 3) return 'A类';
|
return '';
|
},
|
align: 'right',
|
verticalAlign: 'middle',
|
lineHeight: 14,
|
width: 30
|
}
|
},
|
series: [
|
{
|
name: '风险等级',
|
type: 'line',
|
data: this.riskTrendData.seriesData,
|
smooth: false,
|
showSymbol: true,
|
symbol: 'circle',
|
symbolSize: 8,
|
itemStyle: {
|
color: '#32C5FF',
|
borderColor: '#32C5FF',
|
borderWidth: 2
|
},
|
lineStyle: {
|
width: 5,
|
color: {
|
type: 'linear',
|
x: 0,
|
y: 0,
|
x2: 1,
|
y2: 0,
|
colorStops: [
|
{ offset: 0, color: 'rgba(36, 81, 242, 1)' },
|
{ offset: 0.5, color: 'rgba(11, 204, 254, 1)' },
|
{ offset: 1, color: 'rgba(199, 160, 254, 1)' }
|
]
|
}
|
}
|
}
|
]
|
};
|
|
riskChart.setOption(option);
|
},
|
// 初始化折线图
|
initRiskChart() {
|
const chartDom = document.getElementById('riskTrendChart');
|
if (!chartDom) {
|
console.warn('风险趋势图表DOM不存在,可能是权限限制导致该模块未渲染');
|
return;
|
}
|
const riskChart = echarts.init(chartDom)
|
|
// 文本换行处理函数
|
const formatLabel = (text, maxWidth = 4) => {
|
if (!text) return ''
|
const textStr = String(text)
|
// 如果文本长度超过最大宽度,进行换行
|
if (textStr.length > maxWidth) {
|
let result = ''
|
for (let i = 0; i < textStr.length; i += maxWidth) {
|
result += textStr.substring(i, i + maxWidth) + '\n'
|
}
|
return result.trim()
|
}
|
return textStr
|
}
|
|
let option = {
|
tooltip: {
|
trigger: 'axis',
|
show: false
|
},
|
legend: {
|
show: false
|
},
|
grid: {
|
top: '5%',
|
bottom: '5%',
|
left: '5%',
|
right: '5%',
|
containLabel: true
|
},
|
xAxis: {
|
data: this.riskTrendData.xAxisData.length > 0 ? this.riskTrendData.xAxisData : ['2025年1月检查', '2025年2月检查', '2025年3月检查', '2025年4月检查', '2025年5月检查'],
|
axisTick: { show: true, alignWithLabel: true },
|
axisLine: {
|
lineStyle: {
|
color: '#666'
|
}
|
},
|
axisLabel: {
|
color: '#fff',
|
fontSize: 12,
|
interval: 0, // 强制显示所有标签
|
rotate: 0, // 不旋转
|
formatter: function (value) {
|
// X轴标签格式化,最多7个字符,超出换行
|
return formatLabel(value, 7)
|
},
|
// 文本对齐方式
|
align: 'center',
|
verticalAlign: 'top',
|
lineHeight: 14,
|
// 设置标签的最大宽度
|
width: 60,
|
overflow: 'break' // 超出换行
|
}
|
},
|
yAxis: {
|
type: 'value',
|
min: 1,
|
max: 3,
|
interval: 1,
|
splitLine: {
|
show: true,
|
lineStyle: {
|
type: 'dashed', // 虚线
|
color: 'rgba(255, 255, 255, .2)' // 固定灰色
|
}
|
},
|
axisLine: {
|
show: false
|
},
|
axisTick: {
|
show: false
|
},
|
axisLabel: {
|
color: '#fff',
|
fontSize: 12,
|
formatter: function (value) {
|
// Y轴自定义显示:1->C类, 2->B类, 3->A类
|
if (value === 1) return 'C类';
|
if (value === 2) return 'B类';
|
if (value === 3) return 'A类';
|
return '';
|
},
|
// 文本对齐方式
|
align: 'right',
|
verticalAlign: 'middle',
|
lineHeight: 14,
|
// 设置标签的最大宽度
|
width: 30
|
}
|
},
|
series: [
|
{
|
name: '风险等级',
|
type: 'line',
|
data: this.riskTrendData.seriesData.length > 0 ? this.riskTrendData.seriesData : [10, 52, 20, 33, 39],
|
smooth: false, // 不使用圆滑过渡
|
showSymbol: false, // 不显示转折点
|
lineStyle: {
|
width: 5, // 线条宽度
|
color: {
|
type: 'linear',
|
x: 0,
|
y: 0,
|
x2: 1,
|
y2: 0,
|
colorStops: [
|
{ offset: 0, color: 'rgba(36, 81, 242, 1)' }, // 起始颜色
|
{ offset: 0.5, color: 'rgba(11, 204, 254, 1)' }, // 中间颜色
|
{ offset: 1, color: 'rgba(199, 160, 254, 1)' } // 结束颜色
|
]
|
}
|
}
|
}
|
]
|
}
|
riskChart.setOption(option)
|
},
|
toRiskTrend() {
|
this.$router.push({ path: '/subentry', query: { projectId: this.projectId, projectName: this.projectName } })
|
}
|
}
|
}
|
</script>
|
|
<style lang="less" scoped>
|
.a--center {
|
align-items: center;
|
}
|
|
.flex1 {
|
flex: 1;
|
}
|
|
.card-more {
|
min-width: 62px;
|
height: 24px;
|
background: rgba(14, 93, 251, 0.5);
|
border-radius: 4px;
|
padding: 6px 6px 6px 12px;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
cursor: pointer;
|
|
span {
|
font-weight: 400;
|
font-size: 12px;
|
color: #ffffff;
|
line-height: 12px;
|
margin-right: 10px;
|
}
|
|
img {
|
width: 12px;
|
height: 12px;
|
}
|
}
|
|
.left-card {
|
width: 380px;
|
position: absolute;
|
top: 109px;
|
left: 20px;
|
z-index: 2;
|
|
.project-info {
|
padding: 14px;
|
height: 204px;
|
display: flex;
|
flex-direction: column;
|
gap: 8px;
|
overflow-y: auto;
|
overflow-x: hidden;
|
|
// 自定义滚动条样式
|
&::-webkit-scrollbar {
|
width: 4px;
|
}
|
|
&::-webkit-scrollbar-track {
|
background: rgba(255, 255, 255, 0.1);
|
border-radius: 2px;
|
}
|
|
&::-webkit-scrollbar-thumb {
|
background: rgba(50, 197, 255, 0.6);
|
border-radius: 2px;
|
|
&:hover {
|
background: rgba(50, 197, 255, 0.8);
|
}
|
}
|
|
.flex1 {
|
flex: 1;
|
}
|
|
.flex2 {
|
flex: 1.5;
|
}
|
|
.info-item {
|
display: flex;
|
gap: 10px;
|
flex-shrink: 0;
|
}
|
|
.label {
|
font-weight: 500;
|
font-size: 12px;
|
color: #dcdfe6;
|
flex-shrink: 0;
|
width: 64px;
|
}
|
|
.dept {
|
width: 76px;
|
}
|
|
.time {
|
width: 100px;
|
}
|
|
.value {
|
font-weight: bold;
|
font-size: 12px;
|
color: #4cefe6;
|
max-width: 144px;
|
}
|
}
|
|
.equipment-info {
|
height: 154px;
|
position: relative;
|
|
.chart-bg {
|
width: 170px;
|
height: 104px;
|
position: absolute;
|
bottom: 0;
|
left: 0;
|
}
|
|
.pie-canvas {
|
position: absolute;
|
left: -37px;
|
bottom: 10px;
|
width: 362px;
|
height: 112px;
|
z-index: 2;
|
}
|
|
.legend {
|
position: absolute;
|
right: 23px;
|
bottom: 5px;
|
max-height: 126px; // 3个item的高度:(32px + 10px) * 3 = 126px
|
overflow-y: auto;
|
overflow-x: hidden;
|
z-index: 33;
|
|
// 自定义滚动条样式
|
&::-webkit-scrollbar {
|
width: 4px;
|
}
|
|
&::-webkit-scrollbar-track {
|
background: rgba(255, 255, 255, 0.1);
|
border-radius: 2px;
|
}
|
|
&::-webkit-scrollbar-thumb {
|
background: rgba(59, 130, 246, 0.6);
|
border-radius: 2px;
|
|
&:hover {
|
background: rgba(59, 130, 246, 0.8);
|
}
|
}
|
|
.legend-item {
|
display: flex;
|
margin-bottom: 10px;
|
|
width: 180px;
|
height: 32px;
|
background-image: url('../../../src/assets/img/project/equipmentBg.png');
|
background-size: 100% 100%;
|
|
position: relative;
|
align-items: center;
|
justify-content: space-between;
|
|
.dot {
|
width: 6px;
|
height: 6px;
|
margin-left: 5px;
|
flex-shrink: 0;
|
}
|
|
.legend-info {
|
width: 130px;
|
height: 32px;
|
padding: 7px 11px;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
|
.legend-name {
|
max-width: 70px;
|
font-weight: 400;
|
font-size: 14px;
|
color: #CCE0F8;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap; // 确保单行显示
|
cursor: pointer; // 鼠标悬停时显示手型
|
transition: color 0.3s ease;
|
|
&:hover {
|
color: #FFFFFF; // 悬停时文字变亮
|
}
|
}
|
|
.legend-number {
|
font-weight: bold;
|
font-size: 18px;
|
color: #1BDDFF;
|
flex-shrink: 0;
|
}
|
}
|
}
|
}
|
|
.total-card {
|
position: absolute;
|
left: 53px;
|
top: 0px;
|
z-index: 99;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
|
.total {
|
font-weight: 400;
|
font-size: 34px;
|
color: #66FFFF;
|
line-height: 39px;
|
text-shadow: 0px 0px 12px #1068F7;
|
}
|
|
.text {
|
font-weight: bold;
|
font-size: 16px;
|
color: #FFFFFF;
|
line-height: 24px;
|
}
|
}
|
}
|
|
.danger-info {
|
height: 400px;
|
position: relative;
|
|
.info-header {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
padding: 6px 17px 6px 38px;
|
|
.header-line {
|
display: flex;
|
align-items: center;
|
cursor: pointer;
|
transition: opacity 0.3s;
|
|
&:hover {
|
opacity: 0.8;
|
}
|
|
img {
|
width: 7px;
|
height: 10px;
|
margin-left: 2px;
|
}
|
}
|
|
span {
|
font-weight: 400;
|
font-size: 12px;
|
color: #ffffff;
|
}
|
}
|
|
.info-body {
|
height: 363px;
|
overflow-y: auto;
|
}
|
|
.info-item {
|
height: 30px;
|
padding: 0 17px 0 38px;
|
margin-top: 3px;
|
display: flex;
|
align-items: center;
|
|
&:nth-child(2n) {
|
background: rgba(14, 95, 255, 0.2);
|
}
|
|
.item-type {
|
width: 82px;
|
text-align: left;
|
font-weight: bold;
|
font-size: 12px;
|
color: #ffffff;
|
line-height: 18px;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
cursor: pointer;
|
}
|
|
.item-number {
|
flex: 1;
|
padding: 2px 12px;
|
display: flex;
|
flex-direction: column;
|
justify-content: space-between;
|
|
.number {
|
font-weight: 400;
|
font-size: 10px;
|
color: rgba(255, 255, 255, 1);
|
line-height: 15px;
|
}
|
|
span {
|
font-weight: 400;
|
font-size: 10px;
|
line-height: 15px;
|
color: rgba(255, 255, 255, 0.8);
|
}
|
|
.now {
|
width: 114px;
|
height: 8px;
|
background: linear-gradient(90deg, #ffad0e 0%, #fce003 100%);
|
border-radius: 20px;
|
margin-left: 8px;
|
}
|
|
.before {
|
width: 60px;
|
height: 8px;
|
background: linear-gradient(90deg, #008366 0%, #00ffce 100%);
|
border-radius: 20px;
|
margin-left: 8px;
|
}
|
}
|
|
.item-trend {
|
width: 53px;
|
text-align: right;
|
display: flex;
|
align-items: center;
|
justify-content: flex-end;
|
|
font-weight: 800;
|
font-size: 12px;
|
color: #66ffff;
|
line-height: 14px;
|
font-style: normal;
|
|
.trend-icon {
|
margin-right: 4px;
|
font-size: 14px;
|
font-weight: bold;
|
}
|
|
&.down {
|
color: rgba(253, 66, 62, 1);
|
|
.trend-icon {
|
color: rgba(253, 66, 62, 1);
|
}
|
}
|
}
|
}
|
|
.info-bottom {
|
position: absolute;
|
bottom: 0;
|
left: 0;
|
width: 100%;
|
height: 32px;
|
background-image: url(../../assets/img/project/bottom.png);
|
background-size: 100% 100%;
|
}
|
}
|
|
.a--center {
|
align-items: center;
|
}
|
}
|
|
.mid-card {
|
position: absolute;
|
top: 133px;
|
left: 0;
|
right: 0;
|
margin: 0 auto;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
z-index: 1;
|
|
.mid-info {
|
cursor: pointer;
|
// width: 272px;
|
// height: 149px;
|
margin-right: 22px;
|
background: rgba(2, 0, 77, 0.8);
|
box-shadow: inset 0px 0px 34px 0px rgba(17, 40, 255, 0.66);
|
border: 2px solid #0d53b7;
|
padding: 12px 12px 12px 15px;
|
|
.mid-title {
|
cursor: pointer;
|
font-weight: bold;
|
font-size: 16px;
|
color: #ffffff;
|
line-height: 24px;
|
margin-right: 40px;
|
}
|
|
.tab-border {
|
border: 1px solid rgba(50, 197, 255, 0.22);
|
}
|
|
.mid-tab {
|
border-radius: 4px;
|
padding: 6px 5px;
|
font-weight: 400;
|
font-size: 12px;
|
color: #ffffff;
|
}
|
|
.active-tab {
|
background: #0600e4;
|
box-shadow: inset 0px 0px 20px 0px rgba(102, 255, 255, 0.52);
|
}
|
|
.mid-content {
|
margin-top: 11px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
|
.mid-img {
|
width: 90px;
|
height: 90px;
|
margin-right: 20px;
|
background-image: url(../../assets/img/project/chart.png);
|
background-size: 100% 100%;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
|
.number {
|
font-weight: 400;
|
font-size: 24px;
|
// color: linear-gradient(90deg, #66FFFF 0%, #F7B500 100%, #F7B500 100%);
|
line-height: 28px;
|
background: linear-gradient(90deg,
|
#66ffff 0%,
|
#f7b500 100%,
|
#f7b500 100%);
|
/*设置渐变的方向从左到右 颜色从ff0000到ffff00*/
|
-webkit-background-clip: text;
|
/*将设置的背景颜色限制在文字中*/
|
-webkit-text-fill-color: transparent;
|
/*给文字设置成透明*/
|
// background: linear-gradient(90deg, #66FFFF 0%, #F7B500 100%, #F7B500 100%);
|
}
|
|
.name {
|
font-weight: 500;
|
font-size: 10px;
|
color: #66ffff;
|
line-height: 15px;
|
}
|
}
|
|
.mid-item {
|
width: 100px;
|
height: 30px;
|
padding: 9px 10px;
|
background-image: url(../../assets/img/project/chartNum.png);
|
background-size: 100% 100%;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
|
&:nth-child(2) {
|
margin-top: 16px;
|
}
|
|
.mid-txt {
|
font-weight: 400;
|
font-size: 12px;
|
color: #ffffff;
|
}
|
|
.mid-num {
|
font-weight: bold;
|
font-size: 20px;
|
color: #ffffff;
|
}
|
}
|
}
|
}
|
|
.mid-more {
|
width: 42px;
|
height: 150px;
|
box-sizing: border-box;
|
background: linear-gradient(90deg,
|
#040622 0%,
|
#0952cf 22.29%,
|
#155edf 53.43%,
|
#0952cf 81.31%,
|
#011845 100%);
|
border-radius: 96px;
|
border: 2px solid;
|
border-image: linear-gradient(90deg,
|
rgba(159, 255, 255, 0),
|
rgba(133, 255, 255, 1),
|
rgba(102, 255, 255, 0)) 3 1;
|
padding: 24px 12px;
|
font-weight: bold;
|
font-size: 17px;
|
color: #ffc500;
|
line-height: 24px;
|
text-shadow: 0px 0px 10px #612b00;
|
cursor: pointer;
|
}
|
}
|
|
.right-card {
|
width: 462px;
|
position: absolute;
|
top: 109px;
|
right: 20px;
|
z-index: 3;
|
|
.people-info {
|
height: 280px;
|
padding: 18px 32px 20px 32px;
|
display: flex;
|
flex-direction: column;
|
justify-content: space-between;
|
|
.people-card {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
}
|
|
.people-card2 {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.people-item {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
|
.people-chart {
|
width: 82px;
|
height: 82px;
|
margin-bottom: 7px;
|
background-image: url(../../assets/img/project/peopleChart.png);
|
background-size: 100% 100%;
|
position: relative;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
|
.progress-card {
|
position: absolute;
|
top: 0;
|
left: 0;
|
width: 100%;
|
height: 100%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.progress-num {
|
font-weight: 800;
|
font-size: 14px;
|
color: #66ffff;
|
}
|
|
.progress-unit {
|
font-weight: 800;
|
font-size: 10px;
|
color: #66ffff;
|
margin-left: 2px;
|
}
|
}
|
|
.people-name {
|
font-weight: 500;
|
font-size: 14px;
|
color: #ffffff;
|
line-height: 15px;
|
}
|
}
|
}
|
|
.risk-info {
|
height: 244px;
|
padding: 11px 33px 11px 33px;
|
|
.card1 {
|
width: 60px;
|
}
|
|
.card2 {
|
width: 107px;
|
}
|
|
.label {
|
font-weight: 400;
|
font-size: 12px;
|
color: #FFFFFF;
|
text-align: center;
|
}
|
|
.card4 {
|
width: 72px;
|
text-align: left;
|
}
|
|
.risk-top {
|
padding: 6px 0;
|
display: flex;
|
align-items: center;
|
margin-bottom: 7px;
|
}
|
|
.risk-body {
|
max-height: 180px; // 5行数据的高度:36px * 5 = 180px
|
overflow-y: auto;
|
overflow-x: hidden;
|
|
// 自定义滚动条样式
|
&::-webkit-scrollbar {
|
width: 4px;
|
}
|
|
&::-webkit-scrollbar-track {
|
background: rgba(255, 255, 255, 0.1);
|
border-radius: 2px;
|
}
|
|
&::-webkit-scrollbar-thumb {
|
background: rgba(50, 197, 255, 0.6);
|
border-radius: 2px;
|
|
&:hover {
|
background: rgba(50, 197, 255, 0.8);
|
}
|
}
|
|
.risk-item {
|
display: flex;
|
align-items: stretch;
|
|
&:nth-last-child(1) {
|
// border-right: 1px solid #337AB6;
|
|
.label {
|
border-bottom: 1px solid #337AB6;
|
}
|
}
|
|
.label {
|
&:nth-last-child(1) {
|
border-right: 1px solid #337AB6;
|
border-left: unset !important;
|
padding-left: 6px;
|
font-weight: bold;
|
font-size: 12px;
|
color: #66FFFF;
|
font-style: normal;
|
}
|
|
line-height: 36px;
|
border-top: 1px solid #337AB6;
|
border-left: 1px solid #337AB6;
|
position: relative;
|
|
&.merged-cell {
|
position: relative;
|
|
.merged-content {
|
position: absolute;
|
top: 0;
|
left: 0;
|
width: 100%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
// border-bottom: 1px solid #337AB6;
|
}
|
}
|
|
&.hidden-cell {
|
border-top: none;
|
// border-left: none;
|
}
|
}
|
}
|
|
}
|
|
}
|
|
.risk-trend {
|
height: 234px;
|
}
|
|
.risk-picker {
|
width: 190px;
|
height: 26px;
|
background: rgba(14, 93, 251, 0.5);
|
border-radius: 4px;
|
position: relative;
|
display: flex;
|
align-items: center;
|
|
::v-deep .el-input__icon {
|
display: none;
|
}
|
|
::v-deep .el-input__inner {
|
background: transparent;
|
border: none;
|
color: transparent;
|
}
|
|
::v-deep .el-range-input {
|
background: transparent;
|
border: none;
|
color: transparent;
|
}
|
|
.risk-ab {
|
position: absolute;
|
top: 0;
|
left: 0;
|
width: 100%;
|
height: 100%;
|
padding: 6px 26px 6px 10px;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
|
}
|
|
.pick-text {
|
|
font-weight: 400;
|
font-size: 12px;
|
color: #FFFFFF;
|
line-height: 18px;
|
}
|
|
.pick-img {
|
width: 12px;
|
height: 12px;
|
margin-right: 12px;
|
}
|
}
|
|
}
|
|
// 项目进度弹窗 - 历史进度按钮固定定位
|
.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>
|