| | |
| | | <template> |
| | | <div class="relative"> |
| | | <HeaderBox :title="projectName" /> |
| | | <HeaderBox :title="projectName" :name="clientName" :logo="clientLogo" /> |
| | | <!-- 地图 --> |
| | | <div id="container" style="width: 1920px; height: 995px; background-color: #001635"></div> |
| | | <div |
| | | id="projectContainer" |
| | | style=" |
| | | position: fixed; |
| | | top: 0; |
| | | left: 0; |
| | | z-index: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | 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="info-item11"> |
| | | <div class="flex2 flex"> |
| | | <div class="label">项目编号</div> |
| | | <div class="value">{{ projectInfo.code || '-' }}</div> |
| | | <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 class="value"> |
| | | {{ ["房建工程", "市政工程", "固定资产"][projectInfo.type] }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="info-item"> |
| | | <div class="info-item11"> |
| | | <div class="flex2 flex"> |
| | | <div class="label">项目地址</div> |
| | | <div class="value">{{ projectInfo.address || '-' }}</div> |
| | | <div |
| | | class="value address-value" |
| | | :title="projectInfo.address || '-'" |
| | | > |
| | | {{ projectInfo.address || "-" }} |
| | | </div> |
| | | </div> |
| | | <div class="flex1 flex"> |
| | | <div class="flex1 flex a-center"> |
| | | <div class="label">项目阶段</div> |
| | | <div class="value">{{ projectInfo.progress || '-' }}</div> |
| | | <div class="value">{{ projectInfo.progress || "-" }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="info-item"> |
| | | <div class="info-item11"> |
| | | <div class="flex2 flex"> |
| | | <div class="label">施工单位</div> |
| | | <div class="value">{{ projectInfo.developmentCompanyInfo.name || '-' }}</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 class="value"> |
| | | {{ projectInfo.developmentCompanyInfo.leader || "-" }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="info-item"> |
| | | <div class="info-item11"> |
| | | <div class="flex2 flex"> |
| | | <div class="label">监理单位</div> |
| | | <div class="value">{{ projectInfo.supervisorCompanyInfo.name || '-' }}</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 class="value"> |
| | | {{ projectInfo.supervisorCompanyInfo.leader || "-" }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="info-item"> |
| | | <div class="info-item11"> |
| | | <div class="flex2 flex"> |
| | | <div class="label">建设单位</div> |
| | | <div class="value">{{ projectInfo.constructionCompanyInfo.name || '-' }}</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 class="value"> |
| | | {{ projectInfo.constructionCompanyInfo.leader || "-" }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="info-item" v-for="item in projectInfo.properties"> |
| | | <div |
| | | class="info-item11" |
| | | v-if=" |
| | | (filteredRentalUnits && filteredRentalUnits.length > 0) || |
| | | (filteredContacts && filteredContacts.length > 0) |
| | | " |
| | | > |
| | | <div class="flex2 flex"> |
| | | <div class="label dept">设备租赁单位</div> |
| | | <div class="value">{{ JSON.parse(item)['工程起重机械租赁单位'] || '-' }}</div> |
| | | <div class="value" v-if="filteredRentalUnits.length >= 1"> |
| | | {{ filteredRentalUnits[0]["工程起重机械租赁单位"] || "-" }} |
| | | </div> |
| | | <el-tooltip |
| | | style="margin-left: 10px" |
| | | v-if="filteredRentalUnits.length > 1" |
| | | placement="top" |
| | | effect="dark" |
| | | > |
| | | <div slot="content" class="tooltip-content"> |
| | | <div |
| | | v-for="(item, index) in filteredRentalUnits" |
| | | :key="index" |
| | | class="tooltip-item" |
| | | > |
| | | <div class="tooltip-label">单位{{ index + 1 }}:</div> |
| | | <div class="tooltip-value"> |
| | | {{ item["工程起重机械租赁单位"] || "-" }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <span class="count-badge">{{ |
| | | filteredRentalUnits.length |
| | | }}</span> |
| | | </el-tooltip> |
| | | </div> |
| | | <div class="flex1 flex"> |
| | | <div class="label">联系人</div> |
| | | <div class="value">{{ JSON.parse(item)['联系电话'] || '-' }}</div> |
| | | <div class="value" v-if="filteredContacts.length >= 1"> |
| | | {{ filteredContacts[0]["联系人"] || "-" }} |
| | | </div> |
| | | <el-tooltip |
| | | style="margin-left: 10px" |
| | | v-if="filteredContacts.length > 1" |
| | | placement="top" |
| | | effect="dark" |
| | | > |
| | | <div slot="content" class="tooltip-content"> |
| | | <div |
| | | v-for="(item, index) in filteredContacts" |
| | | :key="index" |
| | | class="tooltip-item" |
| | | > |
| | | <div class="tooltip-label">联系人{{ index + 1 }}:</div> |
| | | <div class="tooltip-value"> |
| | | {{ item["联系人"] || "-" }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <span class="count-badge">{{ |
| | | filteredContacts.length |
| | | }}</span> |
| | | </el-tooltip> |
| | | </div> |
| | | </div> |
| | | <div class="info-item"> |
| | | <div class="info-item11"> |
| | | <div class="flex"> |
| | | <div class="label time">最近一次检查时间</div> |
| | | <div class="value">{{ projectInfo.checkTime || '-' }}</div> |
| | | <div class="value">{{ projectInfo.checkTime || "-" }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </TitleBox> |
| | | </div> |
| | | <div class="mt--30" @click="toEquipmentInfo" v-if="permissionProject.includes('3')"> |
| | | <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="" /> |
| | | <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-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 class="legend-name" :title="it.name"> |
| | | {{ it.name }} |
| | | </div> |
| | | <div class="legend-number">{{ it.value }}</div> |
| | | </div> |
| | |
| | | </template> |
| | | </TitleBox> |
| | | </div> |
| | | <div class="mt--30" v-if="permissionProject.includes('1')"> |
| | | <div class="mt--30" v-if="permissionProject.includes('9')"> |
| | | <TitleBox> |
| | | <template #title>各分项隐患趋势 </template> |
| | | <template #right> |
| | |
| | | <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;" /> |
| | | <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;" /> |
| | | <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="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> |
| | | <span>本轮</span> |
| | | <div class="number">{{ item.thisMonth || 0 }}</div> |
| | | </div> |
| | | <div class="now" :style="{ width: getBarWidth(item.thisMonth, item.lastMonth) }"></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> |
| | | <span>上轮</span> |
| | | <div class="number">{{ item.lastMonth || 0 }}</div> |
| | | </div> |
| | | <div class="before" :style="{ width: getBarWidth(item.lastMonth, item.thisMonth) }"></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> |
| | | <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="mid-card"> |
| | | <div @click="toHiddenDanger(1)" class="mid-info" v-if="permissionProject.includes('2')"> |
| | | <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 |
| | | class="mid-tab" |
| | | :class="{ 'active-tab': dangerCountTab === 'all' }" |
| | | @click.stop="handleDangerTabChange('all')" |
| | | > |
| | | 所有隐患数 |
| | | </div> |
| | | <div |
| | | class="mid-tab" |
| | | :class="{ 'active-tab': dangerCountTab === 'serious' }" |
| | | @click.stop="handleDangerTabChange('serious')" |
| | | > |
| | | 重大隐患数 |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="mid-content"> |
| | | <div |
| | | class="mid-content" |
| | | :class="{ 'mid-content-switching': dangerCountLoading }" |
| | | > |
| | | <div class="mid-img"> |
| | | <div class="number">{{ dangerCount.serious || 0 }}</div> |
| | | <div class="name">所有隐患数</div> |
| | | <div class="name">{{ dangerCountTabName }}</div> |
| | | </div> |
| | | <div class="mid-total"> |
| | | <div class="mid-item"> |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div v-if="permissionProject.includes('4')" @click="toPlanOfAudit" class="mid-more">查看报告</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('5')"> |
| | | <div class="mt--16" v-if="permissionProject.includes('13')"> |
| | | <TitleBox> |
| | | <template #title>人员在岗率 </template> |
| | | <template #right> |
| | |
| | | <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-item" |
| | | v-for="(item, index) in peopleList" |
| | | :key="item.id || item.postName || item.name || 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> |
| | | <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 class="people-name"> |
| | | {{ item.postName || item.name }} |
| | | </div> |
| | | <div class="people-name">{{ item.name }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </TitleBox> |
| | | </div> |
| | | <div class="mt--30" v-if="permissionProject.includes('6')"> |
| | | <div class="mt--30" v-if="permissionProject.includes('14')"> |
| | | <TitleBox> |
| | | <template #title>重大风险源 </template> |
| | | <template #right> |
| | |
| | | <div class="label card4">实施情况</div> |
| | | </div> |
| | | <div class="risk-body"> |
| | | <div class="risk-item" v-for="(item, index) in riskSourceList" :key="index"> |
| | | <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` |
| | | }"> |
| | | <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> |
| | | </div> |
| | | </div> |
| | | |
| | | </template> |
| | | </TitleBox> |
| | | </div> |
| | | <div class="mt--30" v-if="permissionProject.includes('7')"> |
| | | <div class="mt--30" v-if="permissionProject.includes('15')"> |
| | | <TitleBox> |
| | | <template #title>项目风险趋势 </template> |
| | | <template #right> |
| | | <div class="risk-picker"> |
| | | <div @click="handleRiskPickerClick" class="risk-picker"> |
| | | <el-date-picker |
| | | v-model="dateRange" |
| | | ref="riskDatePicker" |
| | | type="monthrange" |
| | | range-separator="" |
| | | start-placeholder="" |
| | | end-placeholder="" |
| | | > |
| | | </el-date-picker> |
| | | <div class="risk-ab"> |
| | | <img src="../../assets/img/home/Shape@2x (6).png" class="pick-img" /> |
| | | |
| | | <img |
| | | src="../../assets/img/home/Shape@2x (6).png" |
| | | class="pick-img" |
| | | /> |
| | | <div class="pick-text"> |
| | | {{ dateTxt }} |
| | | </div> |
| | |
| | | </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 HeaderBox from '../header/index.vue' |
| | | import moment from 'moment' |
| | | import { |
| | | getLoginInfo, |
| | | getProjectDetail, |
| | | getProjectDeviceCount, |
| | | getDangerCount, |
| | | getDangerCountAll, |
| | | deviceTypes, |
| | | getProjectPersonInfo, |
| | | getProjectSource, |
| | | getDangerTrend, |
| | | getProjectDangerousTrend |
| | | getProjectDangerousTrend, |
| | | getProjectLatestProgress |
| | | } from './service.js' |
| | | import { |
| | | transformRiskSourceData |
| | | } from '@/utils/riskSourceMap.js' |
| | | import moment from "moment"; |
| | | import * as echarts from "echarts"; |
| | | |
| | | import { transformRiskSourceData } from '@/utils/riskSourceMap.js' |
| | | import * as echarts from 'echarts' |
| | | export default { |
| | | components: { |
| | | HeaderBox, |
| | | HeaderBox |
| | | }, |
| | | data() { |
| | | data () { |
| | | return { |
| | | mapView: null, |
| | | mapPromise: null, |
| | | dateRange: [moment().subtract(5, 'months').startOf("month"), moment().endOf("month")], //默认近6个月 |
| | | _AMap: null, |
| | | _mapInitPromise: null, |
| | | _mapResizeHandler: null, |
| | | _lastMarkerPosKey: '', |
| | | currentMarker: null, // 保存当前的 marker 实例 |
| | | dateRange: [ |
| | | moment().subtract(5, 'months').startOf('month'), |
| | | moment().endOf('month') |
| | | ], // 默认近6个月 |
| | | markers: [], |
| | | projectId: '', |
| | | projectName: '', |
| | |
| | | developmentCompanyInfo: {}, |
| | | supervisorCompanyInfo: {}, |
| | | properties: [] |
| | | },//项目信息 |
| | | }, // 项目信息 |
| | | dangerCount: { |
| | | serious: 0, |
| | | rectified: 0, |
| | | unRectified: 0 |
| | | },//危险源统计 |
| | | deviceTypeList: [],//设备类型列表 |
| | | equipment: 0,//设备数量 |
| | | peopleList: [],//人员列表 |
| | | }, // 危险源统计 |
| | | dangerCountTab: 'serious', |
| | | dangerCountLoading: false, |
| | | dangerCountCache: { |
| | | all: null, |
| | | serious: null |
| | | }, |
| | | |
| | | deviceTypeList: [], // 设备类型列表 |
| | | equipment: 0, // 设备数量 |
| | | peopleList: [], // 人员列表 |
| | | |
| | | // pie数据 |
| | | legendData: [], |
| | | dateTxt: '', |
| | | dangerTrendList: [], //分项隐患趋势数据 |
| | | dangerTrendList: [], // 分项隐患趋势数据 |
| | | dangerSortType: 1, // 排序类型:1-隐患数量,2-趋势 |
| | | dangerSortDesc: true, // 是否倒序 |
| | | // 风险源数据 |
| | |
| | | // 项目风险趋势数据 |
| | | riskTrendData: { |
| | | xAxisData: [], |
| | | seriesData: [] |
| | | seriesData: [], |
| | | statusData: [] |
| | | }, |
| | | // 9各分项隐患趋势 10隐患数量 11设备数量 12查看报告入口 13人员在岗率 14重大风险源 15 项目风险趋势 |
| | | permissionProject: JSON.parse(localStorage.getItem("permissionProject")) || [], |
| | | }; |
| | | permissionProject: |
| | | JSON.parse(sessionStorage.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' |
| | | clientName: '', // 客户名称 |
| | | clientLogo: '' // 客户logo |
| | | } |
| | | }, |
| | | computed: { |
| | | dangerCountTabName () { |
| | | return this.dangerCountTab === 'all' ? '所有隐患数' : '重大隐患数' |
| | | }, |
| | | // 处理风险源数据,添加合并单元格的逻辑 |
| | | riskSourceList() { |
| | | |
| | | riskSourceList () { |
| | | if (!this.riskSourceData || this.riskSourceData.length === 0) { |
| | | return []; |
| | | return [] |
| | | } |
| | | |
| | | const result = []; |
| | | let i = 0; |
| | | const result = [] |
| | | let i = 0 |
| | | |
| | | while (i < this.riskSourceData.length) { |
| | | const currentRiskScale = this.riskSourceData[i].riskScale; |
| | | let rowspan = 1; |
| | | 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++; |
| | | rowspan++ |
| | | } else { |
| | | break; |
| | | break |
| | | } |
| | | } |
| | | |
| | |
| | | ...this.riskSourceData[i], |
| | | showRiskScale: true, |
| | | rowspan: rowspan |
| | | }); |
| | | }) |
| | | |
| | | // 添加后续相同风险规模的行,不显示风险规模 |
| | | for (let k = 1; k < rowspan; k++) { |
| | |
| | | ...this.riskSourceData[i + k], |
| | | showRiskScale: false, |
| | | rowspan: 0 |
| | | }); |
| | | }) |
| | | } |
| | | |
| | | i += rowspan; |
| | | i += rowspan |
| | | } |
| | | |
| | | return result; |
| | | return result |
| | | }, |
| | | filteredProperties () { |
| | | if ( |
| | | !this.projectInfo.properties || |
| | | this.projectInfo.properties.length === 0 |
| | | ) { |
| | | return [] |
| | | } |
| | | |
| | | const uniqueProperties = [] |
| | | const seen = new Set() |
| | | |
| | | this.projectInfo.properties.forEach((itemStr) => { |
| | | try { |
| | | const item = JSON.parse(itemStr) |
| | | const rentalUnit = item['工程起重机械租赁单位'] |
| | | const contact = item['联系电话'] |
| | | |
| | | if (rentalUnit || contact) { |
| | | const key = `${rentalUnit}-${contact}` |
| | | if (!seen.has(key)) { |
| | | seen.add(key) |
| | | uniqueProperties.push(item) |
| | | } |
| | | } |
| | | } catch (e) { |
| | | console.error('Parsing project properties failed:', e) |
| | | } |
| | | }) |
| | | console.log('uniqueProperties uniqueProperties', uniqueProperties) |
| | | |
| | | return uniqueProperties |
| | | }, |
| | | filteredRentalUnits () { |
| | | if ( |
| | | !this.projectInfo.properties || |
| | | this.projectInfo.properties.length === 0 |
| | | ) { |
| | | return [] |
| | | } |
| | | |
| | | const list = [] |
| | | const seen = new Set() |
| | | |
| | | this.projectInfo.properties.forEach((itemStr) => { |
| | | try { |
| | | const item = JSON.parse(itemStr) |
| | | const rentalUnit = item['工程起重机械租赁单位'] |
| | | if (!rentalUnit) return |
| | | const key = String(rentalUnit).trim() |
| | | if (!key) return |
| | | if (seen.has(key)) return |
| | | seen.add(key) |
| | | list.push({ 工程起重机械租赁单位: rentalUnit }) |
| | | } catch (e) {} |
| | | }) |
| | | return list |
| | | }, |
| | | filteredContacts () { |
| | | if ( |
| | | !this.projectInfo.properties || |
| | | this.projectInfo.properties.length === 0 |
| | | ) { |
| | | return [] |
| | | } |
| | | |
| | | const list = [] |
| | | const seen = new Set() |
| | | |
| | | this.projectInfo.properties.forEach((itemStr) => { |
| | | try { |
| | | const item = JSON.parse(itemStr) |
| | | const name = item['联系人'] |
| | | if (!name) return |
| | | const key = String(name).trim() |
| | | if (!key) return |
| | | if (seen.has(key)) return |
| | | seen.add(key) |
| | | list.push({ 联系人: name }) |
| | | } catch (e) {} |
| | | }) |
| | | |
| | | return list |
| | | } |
| | | }, |
| | | watch: { |
| | | dateRange: { |
| | | handler(newVal, oldVal) { |
| | | handler (newVal, oldVal) { |
| | | if (newVal.length > 0) { |
| | | this.dateTxt = |
| | | moment(newVal[0]).format("YYYY -MM") + |
| | | " - " + |
| | | moment(newVal[1]).format("YYYY -MM"); |
| | | moment(newVal[0]).format('YYYY -MM') + |
| | | ' - ' + |
| | | moment(newVal[1]).format('YYYY -MM') |
| | | // 当日期范围改变时,重新获取风险趋势数据 |
| | | if (this.projectId) { |
| | | this.getProjectRiskTrend(); |
| | | this.getProjectRiskTrend() |
| | | } |
| | | } |
| | | }, |
| | | immediate: true, |
| | | }, |
| | | immediate: true |
| | | } |
| | | }, |
| | | created() { |
| | | this.mapPromise = AMapLoader.load({ |
| | | key: "232929db1cb31f26d42b3645a7456154", |
| | | version: "2.0", |
| | | }); |
| | | created () { |
| | | const loader = |
| | | window.AMapLoader || |
| | | (typeof AMapLoader !== 'undefined' ? AMapLoader : null) |
| | | if (!loader) return |
| | | this.mapPromise = loader.load({ |
| | | key: '6edc6798d22e5a46660ff46560b1e612', |
| | | version: '2.0', |
| | | plugins: ['AMap.Geocoder'] |
| | | }) |
| | | if (this.$route.query.projectId) { |
| | | this.projectId = this.$route.query.projectId || '' |
| | | // 添加错误处理,确保登录信息获取失败不影响其他模块 |
| | | getLoginInfo({ id: this.projectId, type: 2 }).then(res => { |
| | | console.log('res', res) |
| | | this.projectName = res.name |
| | | const permissions = res.permissionProject ? res.permissionProject.split(",") : [] |
| | | this.permissionProject = ['1', '2', '3', '4', '5', '6', '7'] || permissions |
| | | localStorage.setItem("permissionProject", JSON.stringify(permissions)); |
| | | localStorage.setItem('commentType', res.commentType) |
| | | localStorage.setItem("deviceTemplateId", res.deviceTemplateId);//设备模板id |
| | | localStorage.setItem("safetyId", res.safetyId);//安全报告模板id |
| | | }).catch(err => { |
| | | console.error('获取登录信息失败:', err) |
| | | // 设置默认值,确保页面可以继续渲染 |
| | | this.projectName = '项目详情' |
| | | this.permissionProject = [] |
| | | }) |
| | | getLoginInfo({ id: this.projectId, type: 2 }) |
| | | .then((res) => { |
| | | this.projectName = res.projectName |
| | | if (sessionStorage.getItem('userInfo')) { |
| | | this.permissionProject = |
| | | JSON.parse(sessionStorage.getItem('permissionProject')) || [] |
| | | } else { |
| | | const permissions = res.permissionProject |
| | | ? res.permissionProject.split(',') |
| | | : [] |
| | | this.permissionProject = permissions |
| | | sessionStorage.setItem( |
| | | 'permissionProject', |
| | | JSON.stringify(permissions) |
| | | ) |
| | | } |
| | | |
| | | sessionStorage.setItem('deviceTemplateId', res.deviceTemplateId) // 设备模板id |
| | | sessionStorage.setItem('deviceTemplateName', res.deviceTemplateName) // 设备模板id |
| | | sessionStorage.setItem('safetyId', res.safetyId) // 安全报告模板id |
| | | sessionStorage.setItem('safetyName', res.safetyName) |
| | | sessionStorage.setItem('bigHiddenDanger', res.bigHiddenDanger) // 1开启 2关闭 重大隐患 |
| | | sessionStorage.setItem( |
| | | 'generalHiddenDanger', |
| | | res.generalHiddenDanger |
| | | ) // 1开启 2关闭 一般隐患 |
| | | sessionStorage.setItem('commentType', res.commentType) // 1模糊评价 2打分评价 |
| | | sessionStorage.setItem('clientName', res.name) |
| | | sessionStorage.setItem('clientLogo', res.logo) |
| | | sessionStorage.setItem('clientId', res.clientId) |
| | | this.clientName = res.name |
| | | this.clientLogo = res.logo |
| | | }) |
| | | .catch((err) => { |
| | | console.error('获取登录信息失败:', err) |
| | | // 设置默认值,确保页面可以继续渲染 |
| | | this.projectName = '项目详情' |
| | | // this.permissionProject = [] |
| | | }) |
| | | this.getDetail() |
| | | this.getDangerTrendList() |
| | | this.getProjectRiskTrend() |
| | | } |
| | | }, |
| | | mounted() { |
| | | this.setMapload() |
| | | mounted () { |
| | | setTimeout(() => { |
| | | |
| | | this.initRiskChart() |
| | | }, 1000) |
| | | }, |
| | | beforeDestroy() { }, |
| | | activated () { |
| | | // 当从其他页面返回时,重新设置地图 marker |
| | | if (this.mapView && this.projectInfo.type !== undefined) { |
| | | this.updateMapMarker() |
| | | } |
| | | }, |
| | | beforeDestroy () { |
| | | this._mapDestroyed = true |
| | | if (this._mapResizeHandler) { |
| | | window.removeEventListener('resize', this._mapResizeHandler) |
| | | this._mapResizeHandler = null |
| | | } |
| | | if (this.mapView) { |
| | | this.mapView.destroy() |
| | | this.mapView = null |
| | | } |
| | | this.currentMarker = null |
| | | this._lastMarkerPosKey = '' |
| | | }, |
| | | methods: { |
| | | // 获取项目详情 |
| | | 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, |
| | | // 打开项目进度弹窗 |
| | | 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 = [] |
| | | } |
| | | console.log('projectInfo projectInfo', this.projectInfo) |
| | | }).catch(err => { |
| | | console.error('获取项目详情失败:', err) |
| | | // 设置默认值 |
| | | this.projectInfo = { |
| | | addressInfo: {}, |
| | | address: '', |
| | | constructionCompanyInfo: {}, |
| | | developmentCompanyInfo: {}, |
| | | supervisorCompanyInfo: {}, |
| | | properties: [] |
| | | } 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 |
| | | } |
| | | |
| | | // 设备数量 - 添加错误处理 |
| | | getProjectDeviceCount(this.projectId).then(res => { |
| | | this.equipment = 0 |
| | | this.legendData = res.map(item => { |
| | | this.equipment += item.count |
| | | const deviceType = this.deviceTypeList[item.type] |
| | | 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 { |
| | | ...item, |
| | | value: item.count || 0, |
| | | name: deviceType && deviceType.name ? deviceType.name : '未知设备', |
| | | itemStyle: { color: this.generateRandomColor() } |
| | | url: url, |
| | | type: type, // 'image' 或 'video' |
| | | id: id |
| | | } |
| | | }) |
| | | this.initPie3D() |
| | | }).catch(err => { |
| | | console.error('获取设备数量失败:', err) |
| | | this.equipment = 0 |
| | | this.legendData = [] |
| | | }) |
| | | |
| | | // 隐患数量 - 添加错误处理 |
| | | getDangerCount(this.projectId).then(res => { |
| | | this.dangerCount = res |
| | | console.log('getDangerCount getDangerCount', res) |
| | | }).catch(err => { |
| | | console.error('获取隐患数量失败:', err) |
| | | this.dangerCount = { |
| | | serious: 0, |
| | | rectified: 0, |
| | | unRectified: 0 |
| | | 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) |
| | | // 可以在这里添加视频加载失败的处理逻辑 |
| | | }, |
| | | normalizeDangerCount (res) { |
| | | return { |
| | | serious: res && res.serious !== undefined ? res.serious : 0, |
| | | rectified: res && res.rectified !== undefined ? res.rectified : 0, |
| | | unRectified: res && res.unRectified !== undefined ? res.unRectified : 0 |
| | | } |
| | | }, |
| | | async loadDangerCount (tab = this.dangerCountTab, force = false) { |
| | | const cacheData = this.dangerCountCache[tab] |
| | | if (!force && cacheData) { |
| | | this.dangerCount = { ...cacheData } |
| | | return |
| | | } |
| | | |
| | | this.dangerCountLoading = true |
| | | try { |
| | | const requestApi = tab === 'all' ? getDangerCountAll : getDangerCount |
| | | const res = await requestApi(this.projectId) |
| | | const countData = this.normalizeDangerCount(res) |
| | | this.dangerCount = countData |
| | | this.dangerCountCache[tab] = { ...countData } |
| | | } catch (err) { |
| | | console.error( |
| | | `获取${tab === 'all' ? '所有隐患数量' : '重大隐患数量'}失败:`, |
| | | err |
| | | ) |
| | | this.dangerCount = this.normalizeDangerCount() |
| | | } finally { |
| | | this.dangerCountLoading = false |
| | | } |
| | | }, |
| | | handleDangerTabChange (tab) { |
| | | if (this.dangerCountTab === tab) return |
| | | this.dangerCountTab = tab |
| | | this.loadDangerCount(tab) |
| | | }, |
| | | // 获取项目详情 |
| | | getDetail () { |
| | | this.dangerCountCache = { |
| | | all: null, |
| | | serious: null |
| | | } |
| | | |
| | | // 设备类型 - 添加错误处理 |
| | | 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 |
| | | } |
| | | this.$nextTick(() => { |
| | | this.setMapload().then(() => { |
| | | 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 = [] |
| | | }) |
| | | |
| | | this.loadDangerCount(this.dangerCountTab, true) |
| | | |
| | | // 项目人员信息 - 添加错误处理 |
| | | getProjectPersonInfo({ projectId: this.projectId }).then(res => { |
| | | this.peopleList = res || [] |
| | | }).catch(err => { |
| | | console.error('获取人员信息失败:', err) |
| | | this.peopleList = [] |
| | | }) |
| | | |
| | | getProjectPersonInfo({ projectId: this.projectId }) |
| | | .then((res) => { |
| | | this.peopleList = res || [] |
| | | }) |
| | | .catch((err) => { |
| | | console.error('获取人员信息失败:', err) |
| | | this.peopleList = [] |
| | | }) |
| | | |
| | | // 获取项目风险源 - 添加错误处理 |
| | | getProjectSource({ projectId: this.projectId }).then(res => { |
| | | const riskSourceList = [] |
| | | 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.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.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) |
| | | // 处理风险源 |
| | | 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 = [] |
| | | }) |
| | | this.riskSourceData = riskSourceList |
| | | }) |
| | | .catch((err) => { |
| | | console.error('获取项目风险源失败:', err) |
| | | this.riskSourceData = [] |
| | | }) |
| | | }, |
| | | // 获取隐患趋势 |
| | | getDangerTrendList() { |
| | | getDangerTrendList () { |
| | | let params = { |
| | | projectId: this.projectId, |
| | | type: this.dangerSortType,// 1数量 2 趋势 |
| | | desc: this.dangerSortDesc, // 是否倒序 |
| | | type: this.dangerSortType, // 1数量 2 趋势 |
| | | desc: this.dangerSortDesc // 是否倒序 |
| | | } |
| | | getDangerTrend(params).then(res => { |
| | | this.dangerTrendList = res || [] |
| | | }).catch(err => { |
| | | console.error('获取隐患趋势失败:', err) |
| | | this.dangerTrendList = [] |
| | | }) |
| | | getDangerTrend(params) |
| | | .then((res) => { |
| | | this.dangerTrendList = res || [] |
| | | }) |
| | | .catch((err) => { |
| | | console.error('获取隐患趋势失败:', err) |
| | | this.dangerTrendList = [] |
| | | }) |
| | | }, |
| | | // 切换隐患排序方式 |
| | | changeDangerSort(type) { |
| | | changeDangerSort (type) { |
| | | if (this.dangerSortType === type) { |
| | | // 如果点击的是当前排序类型,则切换升序/降序 |
| | | this.dangerSortDesc = !this.dangerSortDesc |
| | |
| | | this.getDangerTrendList() |
| | | }, |
| | | // 计算进度条宽度 |
| | | getBarWidth(current, compare) { |
| | | getBarWidth (current, compare) { |
| | | const max = Math.max(current, compare) |
| | | if (max === 0) return '0px' |
| | | const percentage = (current / max) * 100 |
| | |
| | | return `${(percentage / 100) * maxWidth}px` |
| | | }, |
| | | // 人员在岗率 |
| | | toPersonnelOnDuty() { |
| | | toPersonnelOnDuty () { |
| | | this.$router.push({ |
| | | path: "/personnelOnDuty", |
| | | path: '/personnelOnDuty', |
| | | query: { |
| | | projectId: this.projectId, |
| | | projectName: this.projectName, |
| | | }, |
| | | }); |
| | | projectName: this.projectName |
| | | } |
| | | }) |
| | | }, |
| | | // 跳转隐患列表 |
| | | toHiddenDanger(type) { |
| | | toHiddenDanger (type) { |
| | | this.$router.push({ |
| | | path: "/hiddenDanger", |
| | | path: '/hiddenDanger', |
| | | query: { |
| | | projectId: this.projectId, |
| | | projectName: this.projectName, |
| | | activeTab: type, |
| | | }, |
| | | }); |
| | | type: 2 |
| | | } |
| | | }) |
| | | }, |
| | | // 跳转设备列表 |
| | | toEquipmentInfo() { |
| | | toEquipmentInfo () { |
| | | this.$router.push({ |
| | | path: "/equipmentList", |
| | | path: '/equipmentList', |
| | | query: { |
| | | projectId: this.projectId, |
| | | projectName: this.projectName, |
| | | }, |
| | | }); |
| | | projectName: this.projectName |
| | | } |
| | | }) |
| | | }, |
| | | // 跳转重大危险源 |
| | | toMajorRiskList() { |
| | | toMajorRiskList () { |
| | | this.$router.push({ |
| | | path: "/majorRisk", |
| | | path: '/majorRisk', |
| | | query: { |
| | | projectId: this.projectId, |
| | | projectName: this.projectName, |
| | | }, |
| | | }); |
| | | projectName: this.projectName |
| | | } |
| | | }) |
| | | }, |
| | | toPlanOfAudit() { |
| | | toPlanOfAudit () { |
| | | this.$router.push({ |
| | | path: "/planOfAudit", |
| | | path: '/planOfAudit', |
| | | query: { |
| | | projectId: this.projectId, |
| | | projectName: this.projectName, |
| | | type: 2 |
| | | }, |
| | | }); |
| | | } |
| | | }) |
| | | }, |
| | | setMapload() { |
| | | setMapload () { |
| | | if (!this.mapPromise) { |
| | | this.mapPromise = AMapLoader.load({ |
| | | key: "232929db1cb31f26d42b3645a7456154", |
| | | version: "2.0", |
| | | }); |
| | | const loader = |
| | | window.AMapLoader || |
| | | (typeof AMapLoader !== 'undefined' ? AMapLoader : null) |
| | | if (!loader) { |
| | | return Promise.reject(new Error('AMapLoader is not available')) |
| | | } |
| | | this.mapPromise = loader.load({ |
| | | key: '6edc6798d22e5a46660ff46560b1e612', |
| | | version: '2.0', |
| | | plugins: ['AMap.Geocoder'] |
| | | }) |
| | | } |
| | | 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], |
| | | }); |
| | | if (this._mapInitPromise) { |
| | | return this._mapInitPromise |
| | | } |
| | | |
| | | // 在地图中心添加marker |
| | | const marker = new AMap.Marker({ |
| | | position: [104.065617, 30.65695], // 使用地图中心点坐标 |
| | | this._mapInitPromise = this.mapPromise |
| | | .then((AMap) => { |
| | | if (this._mapDestroyed) return AMap |
| | | this._AMap = AMap |
| | | |
| | | if (this.mapView) { |
| | | this.mapView.resize() |
| | | if (!this._mapResizeHandler) { |
| | | this._mapResizeHandler = () => { |
| | | if (this.mapView) this.mapView.resize() |
| | | } |
| | | window.addEventListener('resize', this._mapResizeHandler) |
| | | } |
| | | return AMap |
| | | } |
| | | |
| | | const satellite = new AMap.TileLayer.Satellite() |
| | | const roadNet = new AMap.TileLayer.RoadNet() |
| | | |
| | | // 获取经纬度 |
| | | let center = [104.065617, 30.65695] // 默认中心点 |
| | | let longitude = null |
| | | let latitude = null |
| | | |
| | | if (this.projectInfo && this.projectInfo.addressInfo) { |
| | | longitude = Number(this.projectInfo.addressInfo.longitude) |
| | | latitude = Number(this.projectInfo.addressInfo.latitude) |
| | | |
| | | if ( |
| | | !isNaN(longitude) && |
| | | !isNaN(latitude) && |
| | | longitude !== 0 && |
| | | latitude !== 0 |
| | | ) { |
| | | center = [longitude, latitude] |
| | | } |
| | | } |
| | | |
| | | this.mapView = new AMap.Map('projectContainer', { |
| | | zoom: 16, |
| | | center: center, |
| | | layers: [satellite, roadNet] |
| | | }) |
| | | |
| | | this.mapView.resize() |
| | | if (!this._mapResizeHandler) { |
| | | this._mapResizeHandler = () => { |
| | | if (this.mapView) this.mapView.resize() |
| | | } |
| | | window.addEventListener('resize', this._mapResizeHandler) |
| | | } |
| | | |
| | | return AMap |
| | | }) |
| | | .catch((e) => { |
| | | console.error(e) |
| | | throw e |
| | | }) |
| | | .finally(() => { |
| | | this._mapInitPromise = null |
| | | }) |
| | | |
| | | return this._mapInitPromise |
| | | }, |
| | | // 更新地图 marker |
| | | updateMapMarker () { |
| | | if (!this.mapView) return |
| | | |
| | | const AMap = this._AMap || window.AMap |
| | | if (!AMap) return |
| | | |
| | | // 根据项目类型选择图标 |
| | | const iconImages = [ |
| | | require('../../assets/img/home/new-mark.png'), |
| | | require('../../assets/img/home/s4.png'), |
| | | require('../../assets/img/home/z4.png') |
| | | ] |
| | | // const iconImage = iconImages[this.projectInfo.type] || iconImages[0]; |
| | | const iconImage = iconImages[0] |
| | | |
| | | const tryRenderMarker = (lng, lat) => { |
| | | if ( |
| | | typeof lng !== 'number' || |
| | | typeof lat !== 'number' || |
| | | isNaN(lng) || |
| | | isNaN(lat) |
| | | ) { |
| | | return false |
| | | } |
| | | const nextKey = `${lng},${lat}` |
| | | if (this.currentMarker && this._lastMarkerPosKey === nextKey) { |
| | | return true |
| | | } |
| | | |
| | | if (!this.currentMarker) { |
| | | this.currentMarker = new AMap.Marker({ |
| | | position: [lng, lat], |
| | | icon: new AMap.Icon({ |
| | | image: require('../../assets/img/project/mark.png'), |
| | | image: iconImage, |
| | | size: new AMap.Size(60, 60), |
| | | imageSize: new AMap.Size(60, 60) |
| | | }), |
| | | offset: new AMap.Pixel(-30, -30), |
| | | zoom: 16 |
| | | }) |
| | | this.mapView.add(this.currentMarker) |
| | | } else if (typeof this.currentMarker.setPosition === 'function') { |
| | | this.currentMarker.setPosition([lng, lat]) |
| | | } else { |
| | | this.mapView.remove(this.currentMarker) |
| | | this.currentMarker = new AMap.Marker({ |
| | | position: [lng, lat], |
| | | icon: new AMap.Icon({ |
| | | image: iconImage, |
| | | size: new AMap.Size(30, 30), |
| | | imageSize: new AMap.Size(30, 30) |
| | | }), |
| | | offset: new AMap.Pixel(-15, -15) // 设置偏移量,使图标中心对准坐标点 |
| | | }); |
| | | offset: new AMap.Pixel(-15, -15), |
| | | zoom: 16 |
| | | }) |
| | | this.mapView.add(this.currentMarker) |
| | | } |
| | | |
| | | // 将marker添加到地图 |
| | | this.mapView.add(marker); |
| | | if (typeof this.mapView.setZoomAndCenter === 'function') { |
| | | if (!this._lastMarkerPosKey) { |
| | | this.mapView.setZoomAndCenter(16, [lng, lat]) |
| | | } else if (typeof this.mapView.setCenter === 'function') { |
| | | this.mapView.setCenter([lng, lat]) |
| | | } else { |
| | | this.mapView.setZoomAndCenter(16, [lng, lat]) |
| | | } |
| | | } |
| | | this._lastMarkerPosKey = nextKey |
| | | return true |
| | | } |
| | | |
| | | // this.initCategoryMarkers(AMap); |
| | | }) |
| | | .catch((e) => { |
| | | console.error(e); //加载错误提示 |
| | | }); |
| | | // 1) 优先使用后端返回的经纬度(如果有) |
| | | const info = this.projectInfo.addressInfo || {} |
| | | let lng = null |
| | | let lat = null |
| | | |
| | | if (info.longitude && info.latitude) { |
| | | lng = Number(info.longitude) |
| | | lat = Number(info.latitude) |
| | | } else { |
| | | const lonLatStr = typeof info.lonLat === 'string' ? info.lonLat : '' |
| | | const lonLatParts = lonLatStr.split(',') |
| | | lng = lonLatParts[0] ? Number(lonLatParts[0]) : null |
| | | lat = lonLatParts[1] ? Number(lonLatParts[1]) : null |
| | | } |
| | | |
| | | if (tryRenderMarker(lng, lat)) { |
| | | return |
| | | } |
| | | |
| | | // 2) 无经纬度时,使用地址进行地理编码 |
| | | const address = |
| | | this.projectInfo.address || |
| | | [ |
| | | info && info.province, |
| | | info && info.city, |
| | | info && info.district, |
| | | info && info.detail |
| | | ] |
| | | .filter(Boolean) |
| | | .join('') |
| | | |
| | | if (address && this.mapPromise) { |
| | | this.mapPromise |
| | | .then(() => { |
| | | const Geocoder = |
| | | (window.AMap && window.AMap.Geocoder) || |
| | | (typeof AMap !== 'undefined' && AMap.Geocoder) |
| | | if (!Geocoder) return |
| | | const geocoder = new Geocoder({ |
| | | city: (info && (info.city || info.province)) || '' |
| | | }) |
| | | geocoder.getLocation(address, (status, result) => { |
| | | if ( |
| | | status === 'complete' && |
| | | result && |
| | | result.geocodes && |
| | | result.geocodes.length > 0 |
| | | ) { |
| | | const loc = result.geocodes[0].location |
| | | const lnglat = [loc.lng, loc.lat] |
| | | tryRenderMarker(lnglat[0], lnglat[1]) |
| | | } else { |
| | | // 回退到默认中心点 |
| | | tryRenderMarker(104.065617, 30.65695) |
| | | } |
| | | }) |
| | | }) |
| | | .catch(() => { |
| | | // 加载失败时回退到默认中心点 |
| | | tryRenderMarker(104.065617, 30.65695) |
| | | }) |
| | | } else { |
| | | // 3) 地址不可用时,回退到默认中心点 |
| | | tryRenderMarker(104.065617, 30.65695) |
| | | } |
| | | }, |
| | | // 初始化 3D 环饼图(echarts-gl) |
| | | initPie3D() { |
| | | const el = this.$refs.pieChart; |
| | | if (!el) return; |
| | | this._pie = echarts.init(el); |
| | | initPie3D () { |
| | | const el = this.$refs.pieChart |
| | | if (!el) return |
| | | this._pie = echarts.init(el) |
| | | |
| | | // 使用动态数据或默认数据 |
| | | const data = this.legendData.length > 0 ? this.legendData : []; |
| | | const data = this.legendData.length > 0 ? this.legendData : [] |
| | | |
| | | // 生成 3D 饼图 option(internalDiameterRatio 控制环形内径比) |
| | | this.legendData = data; // 用于右侧自定义 legend |
| | | const series = this.getPie3D(data, 0.7); |
| | | this.legendData = data // 用于右侧自定义 legend |
| | | const series = this.getPie3D(data, 0.7) |
| | | |
| | | const option = { |
| | | legend: { |
| | | tooltip: { |
| | | show: true, |
| | | show: true |
| | | }, |
| | | data: data.map((item) => item.name), |
| | | right: "2%", |
| | | top: "center", |
| | | orient: "vertical", |
| | | right: '2%', |
| | | top: 'center', |
| | | orient: 'vertical', |
| | | itemGap: 20, |
| | | textStyle: { |
| | | color: "#fff", |
| | | fontSize: 12, |
| | | color: '#fff', |
| | | fontSize: 12 |
| | | }, |
| | | show: false, |
| | | show: false |
| | | }, |
| | | graphic: { |
| | | elements: [ |
| | | { |
| | | type: "circle", |
| | | type: 'circle', |
| | | shape: { |
| | | cx: "50%", |
| | | cy: "50%", |
| | | r: "35%", |
| | | cx: '50%', |
| | | cy: '50%', |
| | | r: '35%' |
| | | }, |
| | | style: { |
| | | fill: "transparent", |
| | | fill: 'transparent', |
| | | shadowBlur: 20, |
| | | shadowColor: "rgba(0, 191, 255, 0.4)", |
| | | opacity: 0.6, |
| | | shadowColor: 'rgba(0, 191, 255, 0.4)', |
| | | opacity: 0.6 |
| | | }, |
| | | z: -1, |
| | | }, |
| | | ], |
| | | z: -1 |
| | | } |
| | | ] |
| | | }, |
| | | animation: true, |
| | | tooltip: { |
| | | formatter: (params) => { |
| | | if ( |
| | | params.seriesName !== "mouseoutSeries" && |
| | | params.seriesName !== "pie2d" |
| | | params.seriesName !== 'mouseoutSeries' && |
| | | params.seriesName !== 'pie2d' |
| | | ) { |
| | | const series = option.series[params.seriesIndex]; |
| | | 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}`; |
| | | 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, |
| | | }, |
| | | fontSize: 12 |
| | | } |
| | | }, |
| | | title: { |
| | | x: "center", |
| | | top: "20", |
| | | x: 'center', |
| | | top: '20', |
| | | textStyle: { |
| | | color: "#fff", |
| | | fontSize: 12, |
| | | }, |
| | | color: '#fff', |
| | | fontSize: 12 |
| | | } |
| | | }, |
| | | backgroundColor: "transparent", |
| | | backgroundColor: 'transparent', |
| | | labelLine: { |
| | | show: false, |
| | | lineStyle: { |
| | | color: "transparent", |
| | | }, |
| | | color: 'transparent' |
| | | } |
| | | }, |
| | | label: { |
| | | show: false, |
| | | color: "transparent", |
| | | position: "outside", |
| | | formatter: "{b} \n{c} {d}%", |
| | | color: 'transparent', |
| | | position: 'outside', |
| | | formatter: '{b} \n{c} {d}%' |
| | | }, |
| | | xAxis3D: { |
| | | min: -1, |
| | | max: 1, |
| | | max: 1 |
| | | }, |
| | | yAxis3D: { |
| | | min: -1, |
| | | max: 1, |
| | | max: 1 |
| | | }, |
| | | zAxis3D: { |
| | | min: -1, |
| | | max: 1, |
| | | max: 1 |
| | | }, |
| | | grid3D: { |
| | | show: false, |
| | | boxHeight: 2, |
| | | bottom: "-10%", |
| | | left: "-15%", |
| | | bottom: '-10%', |
| | | left: '-15%', |
| | | viewControl: { |
| | | alpha: 18, |
| | | beta: 50, |
| | |
| | | autoRotate: true, |
| | | rotateSensitivity: 0, |
| | | zoomSensitivity: 0, |
| | | panSensitivity: 0, |
| | | }, |
| | | panSensitivity: 0 |
| | | } |
| | | }, |
| | | series: series, |
| | | }; |
| | | series: series |
| | | } |
| | | |
| | | this._pie.clear(); |
| | | this._pie.setOption(option); |
| | | this._pie.clear() |
| | | this._pie.setOption(option) |
| | | |
| | | // 鼠标事件交互 |
| | | let selectedIndex = ""; |
| | | let hoveredIndex = ""; |
| | | 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; |
| | | 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; |
| | | 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 (selectedIndex !== '' && selectedIndex !== params.seriesIndex) { |
| | | const prev = option.series[selectedIndex] |
| | | if (prev && prev.pieData && prev.pieStatus) { |
| | | prev.parametricEquation = this.getParametricEquation( |
| | | prev.pieData.startRatio, |
| | |
| | | false, |
| | | k, |
| | | prev.pieData.value |
| | | ); |
| | | prev.pieStatus.selected = false; |
| | | ) |
| | | prev.pieStatus.selected = false |
| | | } |
| | | } |
| | | |
| | |
| | | isHovered, |
| | | k, |
| | | s.pieData.value |
| | | ); |
| | | s.pieStatus.selected = isSelected; |
| | | if (isSelected) selectedIndex = params.seriesIndex; |
| | | ) |
| | | s.pieStatus.selected = isSelected |
| | | if (isSelected) selectedIndex = params.seriesIndex |
| | | |
| | | this._pie.setOption(option); |
| | | }); |
| | | this._pie.setOption(option) |
| | | }) |
| | | |
| | | this._pie.on("mouseover", (params) => { |
| | | if (hoveredIndex === params.seriesIndex) return; |
| | | this._pie.on('mouseover', (params) => { |
| | | if (hoveredIndex === params.seriesIndex) return |
| | | |
| | | // 取消之前的高亮 |
| | | if (hoveredIndex !== "") { |
| | | let prev = option.series[hoveredIndex]; |
| | | if (hoveredIndex !== '') { |
| | | let prev = option.series[hoveredIndex] |
| | | if (prev && prev.pieData && prev.pieStatus) { |
| | | const k = prev.pieStatus.k; |
| | | const k = prev.pieStatus.k |
| | | prev.parametricEquation = this.getParametricEquation( |
| | | prev.pieData.startRatio, |
| | | prev.pieData.endRatio, |
| | |
| | | false, |
| | | k, |
| | | prev.pieData.value |
| | | ); |
| | | prev.pieStatus.hovered = false; |
| | | hoveredIndex = ""; |
| | | ) |
| | | prev.pieStatus.hovered = false |
| | | hoveredIndex = '' |
| | | } |
| | | } |
| | | |
| | | // 当前高亮(排除透明圆环) |
| | | if (params.seriesName !== "mouseoutSeries") { |
| | | let cur = option.series[params.seriesIndex]; |
| | | if (params.seriesName !== 'mouseoutSeries') { |
| | | let cur = option.series[params.seriesIndex] |
| | | if (cur && cur.pieData && cur.pieStatus) { |
| | | const k = cur.pieStatus.k; |
| | | const k = cur.pieStatus.k |
| | | cur.parametricEquation = this.getParametricEquation( |
| | | cur.pieData.startRatio, |
| | | cur.pieData.endRatio, |
| | |
| | | true, |
| | | k, |
| | | cur.pieData.value |
| | | ); |
| | | cur.pieStatus.hovered = true; |
| | | hoveredIndex = params.seriesIndex; |
| | | ) |
| | | cur.pieStatus.hovered = true |
| | | hoveredIndex = params.seriesIndex |
| | | } |
| | | } |
| | | |
| | | this._pie.setOption(option); |
| | | }); |
| | | this._pie.setOption(option) |
| | | }) |
| | | |
| | | this._pie.on("globalout", () => { |
| | | if (hoveredIndex !== "") { |
| | | let prev = option.series[hoveredIndex]; |
| | | this._pie.on('globalout', () => { |
| | | if (hoveredIndex !== '') { |
| | | let prev = option.series[hoveredIndex] |
| | | if (prev && prev.pieData && prev.pieStatus) { |
| | | const k = prev.pieStatus.k; |
| | | const k = prev.pieStatus.k |
| | | prev.parametricEquation = this.getParametricEquation( |
| | | prev.pieData.startRatio, |
| | | prev.pieData.endRatio, |
| | |
| | | false, |
| | | k, |
| | | prev.pieData.value |
| | | ); |
| | | prev.pieStatus.hovered = false; |
| | | hoveredIndex = ""; |
| | | this._pie.setOption(option); |
| | | ) |
| | | 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}%)`; |
| | | 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( |
| | | getParametricEquation ( |
| | | startRatio, |
| | | endRatio, |
| | | isSelected, |
| | |
| | | height |
| | | ) { |
| | | // 计算 |
| | | let midRatio = (startRatio + endRatio) / 2; |
| | | let midRatio = (startRatio + endRatio) / 2 |
| | | |
| | | let startRadian = startRatio * Math.PI * 2; |
| | | let endRadian = endRatio * Math.PI * 2; |
| | | let midRadian = midRatio * Math.PI * 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; |
| | | isSelected = false |
| | | } |
| | | |
| | | // 通过扇形内径/外径的值,换算出辅助参数 k(默认值 1/3) |
| | | k = typeof k !== "undefined" ? 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; |
| | | 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; |
| | | let hoverRate = isHovered ? 1.05 : 1 |
| | | |
| | | // 返回曲面参数方程 |
| | | return { |
| | | u: { |
| | | min: -Math.PI, |
| | | max: Math.PI * 3, |
| | | step: Math.PI / 32, |
| | | step: Math.PI / 32 |
| | | }, |
| | | v: { |
| | | min: 0, |
| | | max: Math.PI * 2, |
| | | step: Math.PI / 20, |
| | | 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; |
| | | 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; |
| | | 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); |
| | | return Math.sin(u) |
| | | } |
| | | if (u > Math.PI * 2.5) { |
| | | return Math.sin(u); |
| | | return Math.sin(u) |
| | | } |
| | | return Math.sin(v) > 0 ? 0.6 * height : -0.6; |
| | | }, |
| | | }; |
| | | 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 = []; |
| | | getPie3D (pieData, internalDiameterRatio) { |
| | | let series = [] |
| | | let sumValue = 0 |
| | | let startValue = 0 |
| | | let endValue = 0 |
| | | let legendData = [] |
| | | let k = |
| | | typeof internalDiameterRatio !== "undefined" |
| | | typeof internalDiameterRatio !== 'undefined' |
| | | ? (1 - internalDiameterRatio) / (1 + internalDiameterRatio) |
| | | : 1 / 3; |
| | | : 1 / 3 |
| | | |
| | | // 为每一个饼图数据,生成一个 series-surface 配置 |
| | | for (let i = 0; i < pieData.length; i++) { |
| | | sumValue += pieData[i].value; |
| | | sumValue += pieData[i].value |
| | | |
| | | let seriesItem = { |
| | | name: |
| | | typeof pieData[i].name === "undefined" |
| | | typeof pieData[i].name === 'undefined' |
| | | ? `series${i}` |
| | | : pieData[i].name, |
| | | type: "surface", |
| | | type: 'surface', |
| | | parametric: true, |
| | | wireframe: { |
| | | show: false, |
| | | 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; |
| | | k: k |
| | | } |
| | | } |
| | | series.push(seriesItem); |
| | | |
| | | 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; |
| | | console.log(series[i]); |
| | | series[i].pieData.startRatio = startValue / sumValue; |
| | | series[i].pieData.endRatio = endValue / sumValue; |
| | | 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, |
| | | k, |
| | | series[i].pieData.value |
| | | ); |
| | | ) |
| | | |
| | | startValue = endValue; |
| | | startValue = endValue |
| | | |
| | | legendData.push(series[i].name); |
| | | legendData.push(series[i].name) |
| | | } |
| | | // series.push({ |
| | | // name: 'mouseoutSeries', |
| | |
| | | // }, |
| | | // }, |
| | | // }); |
| | | return series; |
| | | return series |
| | | }, |
| | | // 风险等级转换为数值 (A->3, B->2, C->1) |
| | | levelToValue(level) { |
| | | if (!level) return null; |
| | | const levelStr = String(level).toUpperCase().trim(); |
| | | 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; |
| | | if (levelStr === 'A' || levelStr === 'A类') return 3 |
| | | if (levelStr === 'B' || levelStr === 'B类') return 2 |
| | | if (levelStr === 'C' || levelStr === 'C类') return 1 |
| | | |
| | | return null; |
| | | 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 ''; |
| | | valueToLevel (value) { |
| | | if (value === 3) return 'A类' |
| | | if (value === 2) return 'B类' |
| | | if (value === 1) return 'C类' |
| | | return '' |
| | | }, |
| | | // 获取项目风险趋势数据 |
| | | async getProjectRiskTrend() { |
| | | async getProjectRiskTrend () { |
| | | if (!this.dateRange || this.dateRange.length !== 2) { |
| | | return; |
| | | 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); |
| | | console.log('项目风险趋势数据:', res); |
| | | const res = await getProjectDangerousTrend(params) |
| | | |
| | | if (res && Array.isArray(res)) { |
| | | // 处理接口返回的数据 |
| | | this.riskTrendData.xAxisData = res.map(item => item.time || ''); |
| | | 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); |
| | | console.log(`level: ${item.level} -> value: ${value}`); |
| | | return value; |
| | | }); |
| | | this.riskTrendData.seriesData = res.map((item) => { |
| | | const value = this.levelToValue(item.level) |
| | | |
| | | console.log('转换后的数据:', { |
| | | xAxisData: this.riskTrendData.xAxisData, |
| | | seriesData: this.riskTrendData.seriesData |
| | | }); |
| | | return value |
| | | }) |
| | | this.riskTrendData.statusData = res.map((item) => item.status) |
| | | |
| | | // 更新图表 |
| | | this.updateRiskChart(); |
| | | this.updateRiskChart() |
| | | } |
| | | } catch (error) { |
| | | console.error('获取项目风险趋势失败:', error) |
| | | // 设置默认值,确保图表可以正常显示 |
| | | this.riskTrendData.xAxisData = [] |
| | | this.riskTrendData.seriesData = [] |
| | | this.riskTrendData.statusData = [] |
| | | } |
| | | }, |
| | | // 更新风险趋势图表 |
| | | updateRiskChart() { |
| | | const chartDom = document.getElementById('riskTrendChart'); |
| | | updateRiskChart () { |
| | | const chartDom = document.getElementById('riskTrendChart') |
| | | if (!chartDom) { |
| | | console.warn('风险趋势图表DOM不存在,可能是权限限制导致该模块未渲染'); |
| | | return; |
| | | console.warn('风险趋势图表DOM不存在,可能是权限限制导致该模块未渲染') |
| | | return |
| | | } |
| | | const riskChart = echarts.getInstanceByDom(chartDom); |
| | | const riskChart = echarts.getInstanceByDom(chartDom) |
| | | if (!riskChart) { |
| | | console.warn('风险趋势图表实例不存在'); |
| | | return; |
| | | console.warn('风险趋势图表实例不存在') |
| | | return |
| | | } |
| | | |
| | | // 文本换行处理函数 |
| | |
| | | return textStr |
| | | } |
| | | |
| | | const self = this; |
| | | const self = this |
| | | |
| | | const option = { |
| | | tooltip: { |
| | |
| | | }, |
| | | 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}`; |
| | | const param = params[0] |
| | | const levelText = self.valueToLevel(param.value) |
| | | return `${param.name}<br/>${param.seriesName}: ${levelText}` |
| | | } |
| | | return ''; |
| | | return '' |
| | | } |
| | | }, |
| | | legend: { |
| | |
| | | } |
| | | }, |
| | | axisLabel: { |
| | | color: '#fff', |
| | | color: (value, index) => { |
| | | const status = |
| | | Array.isArray(this.riskTrendData.statusData) && |
| | | typeof index === 'number' |
| | | ? this.riskTrendData.statusData[index] |
| | | : undefined |
| | | return Number(status) === 2 ? '#999' : '#fff' |
| | | }, |
| | | fontSize: 12, |
| | | interval: 0, |
| | | rotate: 0, |
| | |
| | | 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 ''; |
| | | if (value === 1) return 'C类' |
| | | if (value === 2) return 'B类' |
| | | if (value === 3) return 'A类' |
| | | return '' |
| | | }, |
| | | align: 'right', |
| | | verticalAlign: 'middle', |
| | |
| | | } |
| | | } |
| | | ] |
| | | }; |
| | | } |
| | | |
| | | riskChart.setOption(option); |
| | | riskChart.setOption(option) |
| | | }, |
| | | // 初始化折线图 |
| | | initRiskChart() { |
| | | const chartDom = document.getElementById('riskTrendChart'); |
| | | initRiskChart () { |
| | | const chartDom = document.getElementById('riskTrendChart') |
| | | if (!chartDom) { |
| | | console.warn('风险趋势图表DOM不存在,可能是权限限制导致该模块未渲染'); |
| | | return; |
| | | console.warn('风险趋势图表DOM不存在,可能是权限限制导致该模块未渲染') |
| | | return |
| | | } |
| | | const riskChart = echarts.init(chartDom) |
| | | |
| | |
| | | } |
| | | return textStr |
| | | } |
| | | |
| | | let option = { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | |
| | | containLabel: true |
| | | }, |
| | | xAxis: { |
| | | data: this.riskTrendData.xAxisData.length > 0 ? this.riskTrendData.xAxisData : ['2025年1月检查', '2025年2月检查', '2025年3月检查', '2025年4月检查', '2025年5月检查'], |
| | | 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: { |
| | |
| | | } |
| | | }, |
| | | axisLabel: { |
| | | color: '#fff', |
| | | color: (value, index) => { |
| | | const status = |
| | | Array.isArray(this.riskTrendData.statusData) && |
| | | typeof index === 'number' |
| | | ? this.riskTrendData.statusData[index] |
| | | : undefined |
| | | return Number(status) === 2 ? '#999' : '#fff' |
| | | }, |
| | | fontSize: 12, |
| | | interval: 0, // 强制显示所有标签 |
| | | rotate: 0, // 不旋转 |
| | |
| | | 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 ''; |
| | | if (value === 1) return 'C类' |
| | | if (value === 2) return 'B类' |
| | | if (value === 3) return 'A类' |
| | | return '' |
| | | }, |
| | | // 文本对齐方式 |
| | | align: 'right', |
| | |
| | | { |
| | | name: '风险等级', |
| | | type: 'line', |
| | | data: this.riskTrendData.seriesData.length > 0 ? this.riskTrendData.seriesData : [10, 52, 20, 33, 39], |
| | | data: |
| | | this.riskTrendData.seriesData.length > 0 |
| | | ? this.riskTrendData.seriesData |
| | | : [10, 52, 20, 33, 39], |
| | | smooth: false, // 不使用圆滑过渡 |
| | | showSymbol: false, // 不显示转折点 |
| | | lineStyle: { |
| | |
| | | } |
| | | riskChart.setOption(option) |
| | | }, |
| | | toRiskTrend() { |
| | | this.$router.push({ path: '/subentry', query: { projectId: this.projectId, projectName: this.projectName } }) |
| | | // 处理风险趋势时间选择器点击 |
| | | handleRiskPickerClick () { |
| | | this.$refs.riskDatePicker.focus() |
| | | }, |
| | | toRiskTrend () { |
| | | this.$router.push({ |
| | | path: '/subentry', |
| | | query: { projectId: this.projectId, projectName: this.projectName } |
| | | }) |
| | | } |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | .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 { |
| | |
| | | flex: 1.5; |
| | | } |
| | | |
| | | .info-item { |
| | | .info-item11 { |
| | | display: flex; |
| | | gap: 10px; |
| | | flex-shrink: 0; |
| | |
| | | color: #dcdfe6; |
| | | flex-shrink: 0; |
| | | width: 64px; |
| | | position: relative; |
| | | display: flex; |
| | | // align-items: center; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .dept { |
| | | width: 76px; |
| | | width: 75px; |
| | | } |
| | | |
| | | .time { |
| | | width: 100px; |
| | | } |
| | | |
| | | .count-badge { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | min-width: 18px; |
| | | height: 18px; |
| | | padding: 0 5px; |
| | | background: linear-gradient(135deg, #32c5ff 0%, #0e5dfb 100%); |
| | | border-radius: 9px; |
| | | font-weight: bold; |
| | | font-size: 11px; |
| | | color: #ffffff; |
| | | cursor: pointer; |
| | | transition: all 0.3s ease; |
| | | box-shadow: 0 2px 6px rgba(50, 197, 255, 0.4); |
| | | |
| | | &:hover { |
| | | transform: scale(1.1); |
| | | box-shadow: 0 3px 10px rgba(50, 197, 255, 0.6); |
| | | } |
| | | } |
| | | |
| | | .value { |
| | |
| | | font-size: 12px; |
| | | color: #4cefe6; |
| | | max-width: 144px; |
| | | |
| | | // 项目地址特殊处理:最多显示两行,超出省略 |
| | | &.address-value { |
| | | display: -webkit-box; |
| | | -webkit-box-orient: vertical; |
| | | -webkit-line-clamp: 2; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | word-break: break-all; |
| | | line-height: 18px; |
| | | max-height: 36px; // 2行的高度:18px * 2 |
| | | cursor: pointer; |
| | | transition: color 0.3s ease; |
| | | |
| | | &:hover { |
| | | color: #66ffff; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | width: 180px; |
| | | height: 32px; |
| | | background-image: url('../../../src/assets/img/project/equipmentBg.png'); |
| | | background-image: url("../../../src/assets/img/project/equipmentBg.png"); |
| | | background-size: 100% 100%; |
| | | |
| | | position: relative; |
| | |
| | | max-width: 70px; |
| | | font-weight: 400; |
| | | font-size: 14px; |
| | | color: #CCE0F8; |
| | | color: #cce0f8; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; // 确保单行显示 |
| | |
| | | transition: color 0.3s ease; |
| | | |
| | | &:hover { |
| | | color: #FFFFFF; // 悬停时文字变亮 |
| | | color: #ffffff; // 悬停时文字变亮 |
| | | } |
| | | } |
| | | |
| | | .legend-number { |
| | | font-weight: bold; |
| | | font-size: 18px; |
| | | color: #1BDDFF; |
| | | color: #1bddff; |
| | | flex-shrink: 0; |
| | | } |
| | | } |
| | |
| | | .total { |
| | | font-weight: 400; |
| | | font-size: 34px; |
| | | color: #66FFFF; |
| | | color: #66ffff; |
| | | line-height: 39px; |
| | | text-shadow: 0px 0px 12px #1068F7; |
| | | text-shadow: 0px 0px 12px #1068f7; |
| | | } |
| | | |
| | | .text { |
| | | font-weight: bold; |
| | | font-size: 16px; |
| | | color: #FFFFFF; |
| | | color: #ffffff; |
| | | line-height: 24px; |
| | | } |
| | | } |
| | |
| | | |
| | | font-weight: 800; |
| | | font-size: 12px; |
| | | color: #66ffff; |
| | | color: rgba(253, 66, 62, 1); |
| | | line-height: 14px; |
| | | font-style: normal; |
| | | |
| | |
| | | } |
| | | |
| | | &.down { |
| | | color: rgba(253, 66, 62, 1); |
| | | color: #66ffff; |
| | | |
| | | .trend-icon { |
| | | color: rgba(253, 66, 62, 1); |
| | | color: #66ffff; |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | .tab-border { |
| | | border: 1px solid rgba(50, 197, 255, 0.22); |
| | | border-radius: 4px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .mid-tab { |
| | | cursor: pointer; |
| | | user-select: none; |
| | | border-radius: 4px; |
| | | padding: 6px 5px; |
| | | padding: 6px 8px; |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #ffffff; |
| | | transition: background 0.25s ease, box-shadow 0.25s ease, |
| | | transform 0.25s ease; |
| | | |
| | | &:hover { |
| | | background: rgba(6, 0, 228, 0.2); |
| | | } |
| | | } |
| | | |
| | | .active-tab { |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | transition: opacity 0.25s ease, transform 0.25s ease; |
| | | |
| | | &.mid-content-switching { |
| | | opacity: 0.68; |
| | | transform: translateY(4px); |
| | | } |
| | | |
| | | .mid-img { |
| | | width: 90px; |
| | |
| | | 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%); |
| | | background: linear-gradient( |
| | | 90deg, |
| | | #66ffff 0%, |
| | | #f7b500 100%, |
| | | #f7b500 100% |
| | | ); |
| | | /*设置渐变的方向从左到右 颜色从ff0000到ffff00*/ |
| | | -webkit-background-clip: text; |
| | | /*将设置的背景颜色限制在文字中*/ |
| | |
| | | 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%); |
| | | 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, |
| | | border-image: linear-gradient( |
| | | 90deg, |
| | | rgba(159, 255, 255, 0), |
| | | rgba(133, 255, 255, 1), |
| | | rgba(102, 255, 255, 0)) 3 1; |
| | | rgba(102, 255, 255, 0) |
| | | ) |
| | | 3 1; |
| | | padding: 24px 12px; |
| | | font-weight: bold; |
| | | font-size: 17px; |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | flex-wrap: wrap; |
| | | overflow: auto; |
| | | } |
| | | |
| | | .people-card2 { |
| | |
| | | } |
| | | |
| | | .people-item { |
| | | width: 33%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | margin-bottom: 25px; |
| | | |
| | | .people-chart { |
| | | width: 82px; |
| | |
| | | |
| | | .progress-num { |
| | | font-weight: 800; |
| | | font-size: 18px; |
| | | font-size: 14px; |
| | | color: #66ffff; |
| | | } |
| | | |
| | | .progress-unit { |
| | | font-weight: 800; |
| | | font-size: 12px; |
| | | font-size: 10px; |
| | | color: #66ffff; |
| | | margin-left: 2px; |
| | | } |
| | |
| | | .label { |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #FFFFFF; |
| | | color: #ffffff; |
| | | text-align: center; |
| | | } |
| | | |
| | |
| | | // border-right: 1px solid #337AB6; |
| | | |
| | | .label { |
| | | border-bottom: 1px solid #337AB6; |
| | | border-bottom: 1px solid #337ab6; |
| | | } |
| | | } |
| | | |
| | | .label { |
| | | &:nth-last-child(1) { |
| | | border-right: 1px solid #337AB6; |
| | | border-right: 1px solid #337ab6; |
| | | border-left: unset !important; |
| | | padding-left: 6px; |
| | | font-weight: bold; |
| | | font-size: 12px; |
| | | color: #66FFFF; |
| | | color: #66ffff; |
| | | font-style: normal; |
| | | } |
| | | |
| | | line-height: 36px; |
| | | border-top: 1px solid #337AB6; |
| | | border-left: 1px solid #337AB6; |
| | | border-top: 1px solid #337ab6; |
| | | border-left: 1px solid #337ab6; |
| | | position: relative; |
| | | |
| | | &.merged-cell { |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | .risk-trend { |
| | |
| | | position: relative; |
| | | display: flex; |
| | | align-items: center; |
| | | cursor: pointer; |
| | | |
| | | ::v-deep .el-input__icon { |
| | | display: none; |
| | | } |
| | | |
| | | ::v-deep .el-range-separator { |
| | | display: none; |
| | | } |
| | | |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | |
| | | } |
| | | |
| | | .pick-text { |
| | | |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #FFFFFF; |
| | | color: #ffffff; |
| | | line-height: 18px; |
| | | } |
| | | |
| | |
| | | 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; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Tooltip 提示框样式 |
| | | ::v-deep .el-tooltip__popper { |
| | | max-width: 400px; |
| | | |
| | | &.is-dark { |
| | | background: rgba(0, 22, 53, 0.95); |
| | | } |
| | | } |
| | | |
| | | .tooltip-content { |
| | | min-width: 200px; |
| | | max-height: 300px; |
| | | overflow-y: auto; |
| | | padding: 4px 0; |
| | | |
| | | // 自定义滚动条 |
| | | &::-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); |
| | | } |
| | | } |
| | | |
| | | .tooltip-item { |
| | | padding: 8px 0; |
| | | display: flex; |
| | | align-items: center; |
| | | min-width: 200px; |
| | | |
| | | &:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .tooltip-label { |
| | | font-weight: 500; |
| | | font-size: 12px; |
| | | color: #32c5ff; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .tooltip-value { |
| | | font-weight: 400; |
| | | font-size: 13px; |
| | | color: #ffffff; |
| | | line-height: 1.5; |
| | | word-break: break-all; |
| | | } |
| | | } |
| | | } |
| | | </style> |