|
@@ -3,11 +3,269 @@
|
|
|
<div class="container-title">
|
|
<div class="container-title">
|
|
|
<span class="line"></span>
|
|
<span class="line"></span>
|
|
|
<span class="title">保密要害部位监控</span>
|
|
<span class="title">保密要害部位监控</span>
|
|
|
|
|
+ <el-tooltip effect="dark" placement="right">
|
|
|
|
|
+ <span class="position-count">{{ positionCameraCount }}</span>
|
|
|
|
|
+ <template #content>
|
|
|
|
|
+ <span>共{{ positionCameraCount }}个监控位置</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-tooltip>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="monitor-chart">
|
|
|
|
|
+ <div class="monitor-area">
|
|
|
|
|
+ <div class="has-main-camera" v-if="curPlayPositionCamera && getCameraUrl(curPlayPositionCamera)">
|
|
|
|
|
+ <LiveVideo
|
|
|
|
|
+ :url="getCameraUrl(curPlayPositionCamera)"
|
|
|
|
|
+ :poster="getCameraImg(curPlayPositionCamera)"
|
|
|
|
|
+ :id="`monitor-livevideo`"
|
|
|
|
|
+ class="main-video"
|
|
|
|
|
+ />
|
|
|
|
|
+ <el-icon class="switch-icon" @click="switchCameraPage = true"><Switch /></el-icon>
|
|
|
|
|
+ <div class="monitor-info">
|
|
|
|
|
+ <img src="@/assets/images/disaster-overview/camera.png" alt="" />
|
|
|
|
|
+ <span class="camera-name" :title="curPlayPositionCamera?.positionName + '-' + curPlayPositionCamera?.name"
|
|
|
|
|
+ >{{ curPlayPositionCamera?.positionName }}-{{ curPlayPositionCamera?.name }}</span
|
|
|
|
|
+ >
|
|
|
|
|
+ <img
|
|
|
|
|
+ src="@/assets/images/disaster-overview/full-screen.png"
|
|
|
|
|
+ alt=""
|
|
|
|
|
+ class="full-screen"
|
|
|
|
|
+ @click="isFullScreen ? exitFullscreen() : fullScreen(`monitor-livevideo`, 'overview-monitor')"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="no-main-camera" v-if="!curPlayPositionCamera || !getCameraUrl(curPlayPositionCamera)">
|
|
|
|
|
+ <img class="cameraEmptyImg" src="@/assets/icons/nine-square-grid/cameraEmpty.png" />
|
|
|
|
|
+ <span>暂无保密要害部位监控</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="switch-camera-page" v-if="switchCameraPage">
|
|
|
|
|
+ <el-icon class="close-icon" @click="switchCameraPage = false"><CloseBold /></el-icon>
|
|
|
|
|
+ <div class="switch-camera-page-content">
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="switch-camera-page-content-item"
|
|
|
|
|
+ :class="{ active: curPlayPositionCamera?.id === item.id }"
|
|
|
|
|
+ v-for="item in confidentialityPositionCameraInfo"
|
|
|
|
|
+ :key="item.id"
|
|
|
|
|
+ @click="switchCamera(item)"
|
|
|
|
|
+ >{{ item.positionName }}-{{ item.name }}</div
|
|
|
|
|
+ >
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="chart-area">
|
|
|
|
|
+ <ConfidentialityPositionChart />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="invasion-snapshot-list">
|
|
|
|
|
+ <div class="invasion-snapshot-list-title"> 近30日抓拍记录明细表 </div>
|
|
|
|
|
+ <div class="invasion-snapshot-list-table">
|
|
|
|
|
+ <div class="invasion-snapshot-list-content" ref="scrollContainer" @scroll="handleScroll">
|
|
|
|
|
+ <div class="snapshot-table">
|
|
|
|
|
+ <div class="table-header">
|
|
|
|
|
+ <div class="table-cell">序号</div>
|
|
|
|
|
+ <div class="table-cell">事件</div>
|
|
|
|
|
+ <div class="table-cell">地点</div>
|
|
|
|
|
+ <div class="table-cell">抓拍照片</div>
|
|
|
|
|
+ <div class="table-cell">抓拍时间</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="table-body">
|
|
|
|
|
+ <div v-for="(item, index) in invasionSnapshotList" :key="item.id" class="table-row">
|
|
|
|
|
+ <div class="table-cell">{{ index + 1 }}</div>
|
|
|
|
|
+ <div class="table-cell">{{ item.event }}</div>
|
|
|
|
|
+ <div class="table-cell">{{ item.location }}</div>
|
|
|
|
|
+ <div class="table-cell"><ImageViewer :file-list="item.pictures" /></div>
|
|
|
|
|
+ <div class="table-cell">{{ item.eventTime }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 加载状态 -->
|
|
|
|
|
+ <div v-if="loading" class="loading-container">
|
|
|
|
|
+ <el-icon class="is-loading"><Loading /></el-icon>
|
|
|
|
|
+ <span>加载中...</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 没有更多数据 -->
|
|
|
|
|
+ <div v-if="!hasMore && invasionSnapshotList.length > 0" class="no-more-data">没有更多数据了</div>
|
|
|
|
|
+ <!-- 空状态 -->
|
|
|
|
|
+ <div v-if="!loading && invasionSnapshotList.length === 0" class="empty-state">
|
|
|
|
|
+ <el-icon><Picture /></el-icon>
|
|
|
|
|
+ <span>暂无抓拍记录</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
-<script setup lang="ts"></script>
|
|
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+ import { onMounted, onUnmounted, ref, nextTick } from 'vue';
|
|
|
|
|
+ import { storeToRefs } from 'pinia';
|
|
|
|
|
+ import screenfull from 'screenfull';
|
|
|
|
|
+ import urlJoin from 'url-join';
|
|
|
|
|
+ import { Switch, CloseBold, Loading, Picture } from '@element-plus/icons-vue';
|
|
|
|
|
+ import LiveVideo from '@/components/live/LiveVideo.vue';
|
|
|
|
|
+ import ImageViewer from '@/views/traffic/violation/act/components/ImageViewer.vue';
|
|
|
|
|
+ import ConfidentialityPositionChart from './ConfidentialityPositionChart.vue';
|
|
|
|
|
+ import { userSplitScreenFullScreen } from '@/store/modules/userSplitScreenFullScreen';
|
|
|
|
|
+ import { CameraInfo } from '@/api/disaster-overview';
|
|
|
|
|
+ import {
|
|
|
|
|
+ getConfidentialityPositionList,
|
|
|
|
|
+ getInvasionSnapshotList,
|
|
|
|
|
+ type QueryInvasionSnapshotRes,
|
|
|
|
|
+ } from '@/api/security-confidentiality-position';
|
|
|
|
|
+
|
|
|
|
|
+ const { isFullScreen, curFullScreenType } = storeToRefs(userSplitScreenFullScreen());
|
|
|
|
|
+ const { fullScreen, exitFullscreen } = userSplitScreenFullScreen();
|
|
|
|
|
+
|
|
|
|
|
+ interface ConfidentialityPositionCameraInfo extends CameraInfo {
|
|
|
|
|
+ positionName: string;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const positionCameraCount = ref(0);
|
|
|
|
|
+ const confidentialityPositionCameraInfo = ref<ConfidentialityPositionCameraInfo[]>([]);
|
|
|
|
|
+ const curPlayPositionCamera = ref<ConfidentialityPositionCameraInfo>();
|
|
|
|
|
+ const switchCameraPage = ref(false);
|
|
|
|
|
+
|
|
|
|
|
+ // 入侵抓拍记录相关状态
|
|
|
|
|
+ const invasionSnapshotList = ref<QueryInvasionSnapshotRes[]>([]);
|
|
|
|
|
+ const loading = ref(false);
|
|
|
|
|
+ const hasMore = ref(true);
|
|
|
|
|
+ const currentPage = ref(1);
|
|
|
|
|
+ const pageSize = ref(20);
|
|
|
|
|
+ const scrollContainer = ref<HTMLElement>();
|
|
|
|
|
+
|
|
|
|
|
+ // 计算近30天的时间范围
|
|
|
|
|
+ const getLast30DaysTimeRange = () => {
|
|
|
|
|
+ const endTime = new Date();
|
|
|
|
|
+ const startTime = new Date();
|
|
|
|
|
+ startTime.setDate(endTime.getDate() - 30);
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ startTime: startTime.toISOString().slice(0, 19).replace('T', ' '),
|
|
|
|
|
+ endTime: endTime.toISOString().slice(0, 19).replace('T', ' '),
|
|
|
|
|
+ };
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const isHttps = () => {
|
|
|
|
|
+ return window.location.protocol.startsWith('https');
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const getCameraUrl = (val: ConfidentialityPositionCameraInfo) => {
|
|
|
|
|
+ if (val.pushStreamDTO && val.pushStreamDTO.videoUrls) {
|
|
|
|
|
+ const videoUrl = val.pushStreamDTO.videoUrls.pushstreamIp;
|
|
|
|
|
+ const protocol = isHttps() ? 'wss' : 'ws';
|
|
|
|
|
+ // 如果是绝对地址
|
|
|
|
|
+ if (videoUrl.startsWith('http')) {
|
|
|
|
|
+ // 如果是https的话,websocket要用wss
|
|
|
|
|
+ return videoUrl.replace('http', protocol);
|
|
|
|
|
+ }
|
|
|
|
|
+ const u = urlJoin(
|
|
|
|
|
+ `${protocol}://`,
|
|
|
|
|
+ window.location.host,
|
|
|
|
|
+ window.location.pathname === '/' ? '' : window.location.pathname,
|
|
|
|
|
+ videoUrl,
|
|
|
|
|
+ );
|
|
|
|
|
+ return u;
|
|
|
|
|
+ }
|
|
|
|
|
+ return '';
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const getCameraImg = (val: ConfidentialityPositionCameraInfo) => {
|
|
|
|
|
+ if (val.pushStreamDTO && val.pushStreamDTO.imageUrl) {
|
|
|
|
|
+ return val.pushStreamDTO.imageUrl;
|
|
|
|
|
+ }
|
|
|
|
|
+ return '';
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const switchCamera = (item: ConfidentialityPositionCameraInfo) => {
|
|
|
|
|
+ curPlayPositionCamera.value = item;
|
|
|
|
|
+ switchCameraPage.value = false;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 获取入侵抓拍记录数据
|
|
|
|
|
+ const loadInvasionSnapshotData = async (isLoadMore = false) => {
|
|
|
|
|
+ if (loading.value) return;
|
|
|
|
|
+
|
|
|
|
|
+ loading.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const timeRange = getLast30DaysTimeRange();
|
|
|
|
|
+ const response = await getInvasionSnapshotList({
|
|
|
|
|
+ pageNumber: isLoadMore ? currentPage.value : 1,
|
|
|
|
|
+ pageSize: pageSize.value,
|
|
|
|
|
+ queryParam: {
|
|
|
|
|
+ startTime: timeRange.startTime,
|
|
|
|
|
+ endTime: timeRange.endTime,
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (isLoadMore) {
|
|
|
|
|
+ invasionSnapshotList.value = [...invasionSnapshotList.value, ...response.records];
|
|
|
|
|
+ } else {
|
|
|
|
|
+ invasionSnapshotList.value = response.records;
|
|
|
|
|
+ currentPage.value = 1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ hasMore.value = response.records.length === pageSize.value;
|
|
|
|
|
+ if (hasMore.value) {
|
|
|
|
|
+ currentPage.value++;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('获取入侵抓拍记录失败:', error);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 无限滚动处理
|
|
|
|
|
+ const handleScroll = () => {
|
|
|
|
|
+ if (!scrollContainer.value || loading.value || !hasMore.value) return;
|
|
|
|
|
+
|
|
|
|
|
+ const { scrollTop, scrollHeight, clientHeight } = scrollContainer.value;
|
|
|
|
|
+ // 当滚动到距离底部50px时触发加载
|
|
|
|
|
+ if (scrollTop + clientHeight >= scrollHeight - 50) {
|
|
|
|
|
+ loadInvasionSnapshotData(true);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ onMounted(() => {
|
|
|
|
|
+ getConfidentialityPositionList({
|
|
|
|
|
+ groupName: '',
|
|
|
|
|
+ cameraName: '',
|
|
|
|
|
+ }).then((res) => {
|
|
|
|
|
+ confidentialityPositionCameraInfo.value = res.flatMap((item) => {
|
|
|
|
|
+ return item.children.map((child) => ({
|
|
|
|
|
+ ...child,
|
|
|
|
|
+ positionName: item.groupName,
|
|
|
|
|
+ }));
|
|
|
|
|
+ });
|
|
|
|
|
+ positionCameraCount.value = confidentialityPositionCameraInfo.value.length;
|
|
|
|
|
+ curPlayPositionCamera.value = confidentialityPositionCameraInfo.value[0] ?? undefined;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 加载入侵抓拍记录数据
|
|
|
|
|
+ loadInvasionSnapshotData();
|
|
|
|
|
+
|
|
|
|
|
+ // 绑定滚动事件监听器
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ if (scrollContainer.value) {
|
|
|
|
|
+ scrollContainer.value.addEventListener('scroll', handleScroll);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ window.onresize = () => {
|
|
|
|
|
+ if (!screenfull.isFullscreen) {
|
|
|
|
|
+ isFullScreen.value = false; //判断退出全屏,进行赋值
|
|
|
|
|
+ curFullScreenType.value = 'single';
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ onUnmounted(() => {
|
|
|
|
|
+ window.onresize = null;
|
|
|
|
|
+ // 清理滚动事件监听
|
|
|
|
|
+ if (scrollContainer.value) {
|
|
|
|
|
+ scrollContainer.value.removeEventListener('scroll', handleScroll);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+</script>
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
<style scoped lang="scss">
|
|
|
.container-title {
|
|
.container-title {
|
|
@@ -30,9 +288,291 @@
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ .position-count {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ color: #1777ff;
|
|
|
|
|
+ text-shadow: 0 0 10px #1777ff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
.outer-person-container {
|
|
.outer-person-container {
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
height: 100%;
|
|
height: 100%;
|
|
|
- padding-top: 14px;
|
|
|
|
|
|
|
+ padding: 14px;
|
|
|
|
|
+ overflow: auto;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .monitor-chart {
|
|
|
|
|
+ height: 274px;
|
|
|
|
|
+ margin: 0 15px 11px 15px;
|
|
|
|
|
+ padding: 16px 0;
|
|
|
|
|
+ border-bottom: 1px dashed #c0c4cc;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+
|
|
|
|
|
+ .monitor-area {
|
|
|
|
|
+ width: 50%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ margin-right: 10px;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+
|
|
|
|
|
+ .has-main-camera {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ background-color: #000000;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+
|
|
|
|
|
+ .main-video {
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .switch-icon {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 10px;
|
|
|
|
|
+ right: 10px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .switch-icon:hover {
|
|
|
|
|
+ color: #1777ff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .monitor-info {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 0px;
|
|
|
|
|
+ left: 0px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 5px 10px;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.2);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .camera-name {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ max-width: 100%;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .full-screen {
|
|
|
|
|
+ margin-left: auto;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .no-main-camera {
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+
|
|
|
|
|
+ img {
|
|
|
|
|
+ height: 80%;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .switch-camera-page {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ background-color: rgba(0, 0, 0, 0.2);
|
|
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
|
|
+ z-index: 100;
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ padding-bottom: 10px;
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+
|
|
|
|
|
+ .close-icon {
|
|
|
|
|
+ margin: 10px 10px 10px auto;
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .close-icon:hover {
|
|
|
|
|
+ color: #1777ff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .switch-camera-page-content {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ padding: 0 20px;
|
|
|
|
|
+ overflow: auto;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .switch-camera-page-content-item {
|
|
|
|
|
+ word-wrap: break-word;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ padding: 5px 10px;
|
|
|
|
|
+ border-radius: 5px;
|
|
|
|
|
+ background-color: rgba(255, 255, 255, 0.2);
|
|
|
|
|
+ transition: all 0.3s;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .switch-camera-page-content-item:hover,
|
|
|
|
|
+ .switch-camera-page-content-item.active {
|
|
|
|
|
+ background-color: rgba(255, 255, 255, 0.4);
|
|
|
|
|
+ box-shadow: 0 0 10px rgba(255, 255, 255, 0.4);
|
|
|
|
|
+ transform: scale(1.03);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .switch-camera-page-content-item:active {
|
|
|
|
|
+ background-color: rgba(255, 255, 255, 0.4);
|
|
|
|
|
+ box-shadow: 0 0 10px rgba(255, 255, 255, 0.4);
|
|
|
|
|
+ transform: scale(0.98);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .chart-area {
|
|
|
|
|
+ width: calc(100% - 50% - 10px);
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .invasion-snapshot-list {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: calc(100% - 274px - 24px - 11px);
|
|
|
|
|
+ padding: 0 15px;
|
|
|
|
|
+
|
|
|
|
|
+ .invasion-snapshot-list-title {
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #000000;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .invasion-snapshot-list-table {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: calc(100% - 24px);
|
|
|
|
|
+ min-height: 220px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .invasion-snapshot-list-content {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: calc(100% - 20px);
|
|
|
|
|
+ min-height: 200px;
|
|
|
|
|
+ background-color: #f5f7fa;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+
|
|
|
|
|
+ .snapshot-table {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ background-color: #ffffff;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+
|
|
|
|
|
+ .table-header {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: 60px 1fr 1fr 150px 1fr;
|
|
|
|
|
+ background-color: #f8f9fa;
|
|
|
|
|
+ border-bottom: 1px solid #e9ecef;
|
|
|
|
|
+
|
|
|
|
|
+ .table-cell {
|
|
|
|
|
+ padding: 12px 8px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ border-right: 1px solid #e9ecef;
|
|
|
|
|
+
|
|
|
|
|
+ &:last-child {
|
|
|
|
|
+ border-right: none;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .table-body {
|
|
|
|
|
+ .table-row {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: 60px 1fr 1fr 150px 1fr;
|
|
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
+ transition: background-color 0.2s;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background-color: #f8f9fa;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:last-child {
|
|
|
|
|
+ border-bottom: none;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .table-cell {
|
|
|
|
|
+ padding: 12px 8px;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ border-right: 1px solid #f0f0f0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+
|
|
|
|
|
+ &:last-child {
|
|
|
|
|
+ border-right: none;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .loading-container {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+
|
|
|
|
|
+ .is-loading {
|
|
|
|
|
+ animation: rotate 1s linear infinite;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .no-more-data {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .empty-state {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ padding: 40px 20px;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+
|
|
|
|
|
+ .el-icon {
|
|
|
|
|
+ font-size: 48px;
|
|
|
|
|
+ color: #ddd;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @keyframes rotate {
|
|
|
|
|
+ from {
|
|
|
|
|
+ transform: rotate(0deg);
|
|
|
|
|
+ }
|
|
|
|
|
+ to {
|
|
|
|
|
+ transform: rotate(360deg);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|