可信代码库V2版-安全智库页面原型开发、安全检测中心页面原型开发、关于我们页面原型开发
This commit is contained in:
581
src/views/Jyh/TestingCenter/ReportDetail.vue
Normal file
581
src/views/Jyh/TestingCenter/ReportDetail.vue
Normal file
@@ -0,0 +1,581 @@
|
||||
<template>
|
||||
<div class="report-detail">
|
||||
<!-- 报告头部信息 -->
|
||||
<div class="report-header">
|
||||
<div class="risk-level">
|
||||
<d-tag :color="getRiskColor(reportData.riskLevel)" size="lg">
|
||||
{{ reportData.riskLevel || '无风险' }}
|
||||
</d-tag>
|
||||
</div>
|
||||
<div class="report-title">
|
||||
<h2>安全检测报告 - {{ reportData.target }}</h2>
|
||||
<p class="report-time">生成时间:{{ formatTime(reportData.completeTime) }}</p>
|
||||
</div>
|
||||
<div class="report-actions">
|
||||
<d-button variant="text" @click="copyReportLink">
|
||||
<i class="fa fa-link mr-1"></i> 复制报告链接
|
||||
</d-button>
|
||||
<d-button variant="text" @click="downloadReport">
|
||||
<i class="fa fa-download mr-1"></i> 下载报告
|
||||
</d-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 基本信息卡片 -->
|
||||
<d-card class="report-card mt-4">
|
||||
<div slot="header" class="card-header">基本信息</div>
|
||||
<d-descriptions column="2" bordered>
|
||||
<d-description-item term="任务ID">{{ reportData.taskId || '--' }}</d-description-item>
|
||||
<d-description-item term="检测类型">{{ getTypeText(reportData.type) }}</d-description-item>
|
||||
<d-description-item term="检测目标">{{ reportData.target || '--' }}</d-description-item>
|
||||
<d-description-item term="文件大小" v-if="reportData.type === 'file'">
|
||||
{{ reportData.fileSize ? formatFileSize(reportData.fileSize) : '--' }}
|
||||
</d-description-item>
|
||||
<d-description-item term="开始时间">{{ formatTime(reportData.startTime) || '--' }}</d-description-item>
|
||||
<d-description-item term="完成时间">{{ formatTime(reportData.completeTime) || '--' }}</d-description-item>
|
||||
<d-description-item term="检测时长">{{ formatDuration(reportData.duration) || '--' }}</d-description-item>
|
||||
<d-description-item term="检测项总数">{{ reportData.totalChecks || 0 }} 项</d-description-item>
|
||||
</d-descriptions>
|
||||
</d-card>
|
||||
|
||||
<!-- 风险概览卡片 -->
|
||||
<d-card class="report-card mt-4">
|
||||
<div slot="header" class="card-header">风险概览</div>
|
||||
<div class="risk-overview">
|
||||
<div class="risk-stats">
|
||||
<div class="risk-stat-item">
|
||||
<div class="stat-value">{{ reportData.riskStats?.critical || 0 }}</div>
|
||||
<div class="stat-label">
|
||||
<d-tag color="#c7000b" size="sm">致命漏洞</d-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="risk-stat-item">
|
||||
<div class="stat-value">{{ reportData.riskStats?.high || 0 }}</div>
|
||||
<div class="stat-label">
|
||||
<d-tag color="#f66f6a" size="sm">高危漏洞</d-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="risk-stat-item">
|
||||
<div class="stat-value">{{ reportData.riskStats?.medium || 0 }}</div>
|
||||
<div class="stat-label">
|
||||
<d-tag color="#fac20a" size="sm">中危漏洞</d-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="risk-stat-item">
|
||||
<div class="stat-value">{{ reportData.riskStats?.low || 0 }}</div>
|
||||
<div class="stat-label">
|
||||
<d-tag color="#5e7ce0" size="sm">低危漏洞</d-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="risk-chart">
|
||||
<!-- 使用 DChart 组件实现饼图 -->
|
||||
<d-chart :option="riskChartOption" style="width: 100%; height: 200px"></d-chart>
|
||||
</div>
|
||||
</div>
|
||||
</d-card>
|
||||
|
||||
<!-- 漏洞详情卡片 -->
|
||||
<d-card class="report-card mt-4">
|
||||
<div slot="header" class="card-header">漏洞详情</div>
|
||||
<d-table
|
||||
:data="vulnerabilities"
|
||||
:show-loading="loading"
|
||||
table-layout="auto"
|
||||
>
|
||||
<d-column field="vulnId" header="漏洞编号" :width="160"></d-column>
|
||||
<d-column field="vulnName" header="漏洞名称"></d-column>
|
||||
<d-column field="severity" header="风险等级">
|
||||
<template #default="scope">
|
||||
<d-tag :color="getSeverityColor(scope.row.severity)">{{ getSeverityText(scope.row.severity) }}</d-tag>
|
||||
</template>
|
||||
</d-column>
|
||||
<d-column field="affectedComponents" header="受影响组件" :width="200">
|
||||
<template #default="scope">
|
||||
<div class="affected-components">
|
||||
<span v-for="(comp, idx) in scope.row.affectedComponents" :key="idx">
|
||||
{{ comp.name }}@{{ comp.version }}
|
||||
<template v-if="idx < scope.row.affectedComponents.length - 1">, </template>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</d-column>
|
||||
<d-column header="操作" :width="120">
|
||||
<template #default="scope">
|
||||
<d-button
|
||||
variant="text"
|
||||
size="sm"
|
||||
@click="showVulnDetail(scope.row)"
|
||||
>
|
||||
详情
|
||||
</d-button>
|
||||
</template>
|
||||
</d-column>
|
||||
</d-table>
|
||||
</d-card>
|
||||
|
||||
<!-- 修复建议卡片 -->
|
||||
<d-card class="report-card mt-4">
|
||||
<div slot="header" class="card-header">修复建议</div>
|
||||
<div class="fix-suggestions">
|
||||
<div v-if="reportData.fixSuggestions && reportData.fixSuggestions.length">
|
||||
<div class="suggestion-item" v-for="(item, idx) in reportData.fixSuggestions" :key="idx">
|
||||
<h4 class="suggestion-title">
|
||||
<i class="fa fa-lightbulb-o text-warning mr-2"></i>
|
||||
建议 {{ idx + 1 }}: {{ item.title }}
|
||||
</h4>
|
||||
<p class="suggestion-content">{{ item.content }}</p>
|
||||
<a
|
||||
v-if="item.reference"
|
||||
:href="item.reference"
|
||||
target="_blank"
|
||||
class="suggestion-link"
|
||||
>
|
||||
查看详细指南 <i class="fa fa-external-link ml-1"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-suggestions">
|
||||
未发现需要修复的问题或暂无具体修复建议
|
||||
</div>
|
||||
</div>
|
||||
</d-card>
|
||||
|
||||
<!-- 底部操作区 -->
|
||||
<div class="report-footer mt-6">
|
||||
<d-button @click="$emit('close')" variant="secondary">关闭</d-button>
|
||||
<d-button @click="rescan" class="ml-2">重新检测</d-button>
|
||||
</div>
|
||||
|
||||
<!-- 漏洞详情弹窗 -->
|
||||
<d-modal
|
||||
v-model="vulnDetailVisible"
|
||||
title="漏洞详情"
|
||||
:width="700"
|
||||
>
|
||||
<div v-if="currentVuln" class="vuln-detail-modal">
|
||||
<div class="vuln-detail-header">
|
||||
<h3>{{ currentVuln.vulnName }}</h3>
|
||||
<d-tag :color="getSeverityColor(currentVuln.severity)">{{ getSeverityText(currentVuln.severity) }}</d-tag>
|
||||
</div>
|
||||
<div class="vuln-detail-content mt-4">
|
||||
<div class="vuln-section">
|
||||
<h4 class="section-title">漏洞描述</h4>
|
||||
<p class="section-content">{{ currentVuln.description || '无详细描述' }}</p>
|
||||
</div>
|
||||
<div class="vuln-section mt-3">
|
||||
<h4 class="section-title">受影响组件</h4>
|
||||
<ul class="section-list">
|
||||
<li v-for="(comp, idx) in currentVuln.affectedComponents" :key="idx">
|
||||
{{ comp.name }} (版本: {{ comp.version }}) - 路径: {{ comp.path || '未知' }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="vuln-section mt-3">
|
||||
<h4 class="section-title">修复建议</h4>
|
||||
<p class="section-content">{{ currentVuln.fixSuggestion || '暂无具体修复建议' }}</p>
|
||||
</div>
|
||||
<div class="vuln-section mt-3">
|
||||
<h4 class="section-title">参考信息</h4>
|
||||
<div class="section-content">
|
||||
<p v-if="currentVuln.cveId">CVE: {{ currentVuln.cveId }}</p>
|
||||
<p v-if="currentVuln.cnnvdId">CNNVD: {{ currentVuln.cnnvdId }}</p>
|
||||
<a
|
||||
v-if="currentVuln.referenceUrl"
|
||||
:href="currentVuln.referenceUrl"
|
||||
target="_blank"
|
||||
class="reference-link"
|
||||
>
|
||||
官方漏洞详情 <i class="fa fa-external-link ml-1"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</d-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
// import { useToast } from 'devui';
|
||||
import { DChart } from 'vue-devui/echarts'; // 引入 DChart 组件
|
||||
|
||||
// 引入dayjs duration插件
|
||||
dayjs.extend(duration);
|
||||
|
||||
// 接收父组件传入的报告数据
|
||||
const props = defineProps({
|
||||
reportData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
// 组件内部状态
|
||||
const loading = ref(false);
|
||||
const vulnDetailVisible = ref(false);
|
||||
const currentVuln = ref(null);
|
||||
const toast = useToast();
|
||||
|
||||
// 漏洞列表数据(从报告数据中提取)
|
||||
const vulnerabilities = ref(props.reportData.vulnerabilities || []);
|
||||
|
||||
// 显示漏洞详情
|
||||
const showVulnDetail = (vuln) => {
|
||||
currentVuln.value = vuln;
|
||||
vulnDetailVisible.value = true;
|
||||
};
|
||||
|
||||
// 重新检测
|
||||
const rescan = () => {
|
||||
$emit('close');
|
||||
$emit('rescan', props.reportData);
|
||||
};
|
||||
|
||||
// 复制报告链接
|
||||
const copyReportLink = () => {
|
||||
const dummyLink = `${window.location.origin}/scan/report/${props.reportData.taskId}`;
|
||||
navigator.clipboard.writeText(dummyLink).then(() => {
|
||||
toast.success({ content: '报告链接已复制到剪贴板', duration: 2000 });
|
||||
}).catch(() => {
|
||||
toast.error({ content: '复制失败,请手动复制', duration: 2000 });
|
||||
});
|
||||
};
|
||||
|
||||
// 下载报告
|
||||
const downloadReport = () => {
|
||||
toast.info({ content: '准备下载报告...', duration: 2000 });
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time) => {
|
||||
return time ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : '--';
|
||||
};
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// 格式化时长
|
||||
const formatDuration = (ms) => {
|
||||
if (!ms) return '--';
|
||||
const d = dayjs.duration(ms, 'milliseconds');
|
||||
return `${d.minutes()}分${d.seconds()}秒`;
|
||||
};
|
||||
|
||||
// 风险等级颜色映射
|
||||
const getRiskColor = (level) => {
|
||||
const colorMap = {
|
||||
'致命': '#c7000b',
|
||||
'高危': '#f66f6a',
|
||||
'中危': '#fac20a',
|
||||
'低危': '#5e7ce0',
|
||||
'无风险': '#00b42a'
|
||||
};
|
||||
return colorMap[level] || 'default';
|
||||
};
|
||||
|
||||
// 检测类型文本映射
|
||||
const getTypeText = (type) => {
|
||||
const typeMap = {
|
||||
'file': '文件上传',
|
||||
'url': 'URL地址',
|
||||
'sbom': 'SBOM内容'
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
};
|
||||
|
||||
// 漏洞严重程度文本映射
|
||||
const getSeverityText = (severity) => {
|
||||
const severityMap = {
|
||||
'critical': '致命',
|
||||
'high': '高危',
|
||||
'medium': '中危',
|
||||
'low': '低危'
|
||||
};
|
||||
return severityMap[severity] || severity;
|
||||
};
|
||||
|
||||
// 漏洞严重程度颜色映射
|
||||
const getSeverityColor = (severity) => {
|
||||
const colorMap = {
|
||||
'critical': '#c7000b',
|
||||
'high': '#f66f6a',
|
||||
'medium': '#fac20a',
|
||||
'low': '#5e7ce0'
|
||||
};
|
||||
return colorMap[severity] || 'default';
|
||||
};
|
||||
|
||||
// 风险图表配置(基于 echarts 格式)
|
||||
const riskChartOption = computed(() => {
|
||||
const riskStats = props.reportData.riskStats || {
|
||||
critical: 0,
|
||||
high: 0,
|
||||
medium: 0,
|
||||
low: 0
|
||||
};
|
||||
|
||||
// 转换为 echarts 所需的数据格式
|
||||
const chartData = [
|
||||
{ name: '致命漏洞', value: riskStats.critical, itemStyle: { color: '#c7000b' } },
|
||||
{ name: '高危漏洞', value: riskStats.high, itemStyle: { color: '#f66f6a' } },
|
||||
{ name: '中危漏洞', value: riskStats.medium, itemStyle: { color: '#fac20a' } },
|
||||
{ name: '低危漏洞', value: riskStats.low, itemStyle: { color: '#5e7ce0' } }
|
||||
].filter(item => item.value > 0); // 过滤掉数量为0的项
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
right: 10,
|
||||
top: 'center',
|
||||
textStyle: {
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '漏洞数量',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 4,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: chartData
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
// 监听报告数据变化,更新漏洞列表
|
||||
watch(() => props.reportData, () => {
|
||||
vulnerabilities.value = props.reportData.vulnerabilities || [];
|
||||
});
|
||||
|
||||
// 定义组件输出事件
|
||||
const emit = defineEmits(['close', 'rescan']);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'devui-theme/styles-var/devui-var.scss';
|
||||
|
||||
.report-detail {
|
||||
padding: 16px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.report-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.risk-level {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.report-title {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
|
||||
.report-time {
|
||||
color: var(--devui-text-secondary);
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.report-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.report-card {
|
||||
--devui-card-padding: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--devui-text-primary);
|
||||
}
|
||||
|
||||
.risk-overview {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
|
||||
.risk-stats {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
|
||||
.risk-stat-item {
|
||||
text-align: center;
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: var(--devui-text-primary);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.risk-chart {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.affected-components {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.fix-suggestions {
|
||||
padding: 8px 0;
|
||||
|
||||
.suggestion-item {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px dashed var(--devui-border);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.suggestion-title {
|
||||
font-weight: 600;
|
||||
color: var(--devui-text-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.suggestion-content {
|
||||
color: var(--devui-text-secondary);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.suggestion-link {
|
||||
color: var(--devui-primary);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-suggestions {
|
||||
color: var(--devui-text-secondary);
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.report-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.vuln-detail-modal {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.vuln-detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.vuln-section {
|
||||
padding: 8px 0;
|
||||
|
||||
.section-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: var(--devui-text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
color: var(--devui-text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.section-list {
|
||||
color: var(--devui-text-secondary);
|
||||
padding-left: 20px;
|
||||
|
||||
li {
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.reference-link {
|
||||
color: var(--devui-primary);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .devui-descriptions {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
::v-deep .devui-table {
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
750
src/views/Jyh/TestingCenter/index.vue
Normal file
750
src/views/Jyh/TestingCenter/index.vue
Normal file
@@ -0,0 +1,750 @@
|
||||
<template>
|
||||
<d-breadcrumb class="secret-breadcrumb">
|
||||
<gc-breadcrumb-item :to="{ name: 'home' }"><span class="title">首页</span></gc-breadcrumb-item>
|
||||
<gc-breadcrumb-item>
|
||||
<span class="cur-title">安全检测中心</span>
|
||||
</gc-breadcrumb-item>
|
||||
</d-breadcrumb>
|
||||
|
||||
<div class="page-wrap">
|
||||
<!-- 检测方式选择区域 -->
|
||||
<Card class="upload-card">
|
||||
<div class="upload-title">选择检测方式</div>
|
||||
|
||||
<div class="upload-tabs">
|
||||
<d-tabs v-model="activeUploadType" @change="handleUploadTypeChange">
|
||||
<d-tab id="file" title="上传文件">
|
||||
<div class="upload-area" @click="triggerFileUpload" :class="{ dragging: isDragging }">
|
||||
<input
|
||||
type="file"
|
||||
ref="fileInput"
|
||||
class="file-input"
|
||||
@change="handleFileUpload"
|
||||
@dragover.prevent="isDragging = true"
|
||||
@dragleave.prevent="isDragging = false"
|
||||
@drop.prevent="handleFileDrop"
|
||||
>
|
||||
<i class="fa fa-cloud-upload text-primary text-4xl mb-2"></i>
|
||||
<p>点击或拖拽文件到此处上传</p>
|
||||
<p class="text-sm text-gray-medium">支持格式:.zip, .tar, .gz, .json (最大100MB)</p>
|
||||
</div>
|
||||
</d-tab>
|
||||
|
||||
<d-tab id="url" title="输入URL">
|
||||
<d-form layout="horizontal">
|
||||
<d-form-item field="scanUrl" label="检测URL">
|
||||
<d-input
|
||||
v-model="scanUrl"
|
||||
placeholder="请输入需要检测的URL地址(例如:https://example.com)"
|
||||
style="width: 100%"
|
||||
></d-input>
|
||||
</d-form-item>
|
||||
<d-form-item>
|
||||
<d-button
|
||||
@click="startUrlScan"
|
||||
variant="solid"
|
||||
:disabled="!scanUrl"
|
||||
>
|
||||
开始检测
|
||||
</d-button>
|
||||
</d-form-item>
|
||||
</d-form>
|
||||
</d-tab>
|
||||
|
||||
<d-tab id="sbom" title="输入SBOM">
|
||||
<d-form layout="horizontal">
|
||||
<d-form-item field="sbomContent" label="SBOM内容">
|
||||
<d-textarea
|
||||
v-model="sbomContent"
|
||||
placeholder="请输入SBOM内容(支持SPDX或CycloneDX格式)"
|
||||
:rows="8"
|
||||
style="width: 100%"
|
||||
></d-textarea>
|
||||
</d-form-item>
|
||||
<d-form-item>
|
||||
<d-button
|
||||
@click="startSbomScan"
|
||||
variant="solid"
|
||||
:disabled="!sbomContent"
|
||||
>
|
||||
开始检测
|
||||
</d-button>
|
||||
</d-form-item>
|
||||
</d-form>
|
||||
</d-tab>
|
||||
</d-tabs>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- 检测状态展示 -->
|
||||
<Card class="mt-4" v-if="currentScan">
|
||||
|
||||
<div class="scan-status-title">
|
||||
<h3>当前检测状态</h3>
|
||||
<div class="upload-title">当前检测状态</div>
|
||||
<d-tag :color="getStatusColor(currentScan.status)">{{ getStatusText(currentScan.status) }}</d-tag>
|
||||
</div>
|
||||
|
||||
<div class="scan-progress mt-4">
|
||||
<d-progress
|
||||
:percentage="currentScan.progress"
|
||||
:status="getProgressStatus(currentScan.status)"
|
||||
stroke-width="6"
|
||||
></d-progress>
|
||||
<p class="progress-text mt-2">
|
||||
{{ currentScan.progress }}% 完成 - {{ currentScan.currentStep || '准备开始检测' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="scan-details mt-4" v-if="currentScan.status !== 'pending'">
|
||||
<d-descriptions column="1" bordered>
|
||||
<d-description-item term="检测目标">{{ currentScan.target }}</d-description-item>
|
||||
<d-description-item term="开始时间">{{ formatTime(currentScan.startTime) }}</d-description-item>
|
||||
<d-description-item term="预计完成时间" v-if="currentScan.status === 'scanning'">
|
||||
{{ formatTime(currentScan.estimatedCompleteTime) }}
|
||||
</d-description-item>
|
||||
<d-description-item term="完成时间" v-if="currentScan.status === 'completed' || currentScan.status === 'failed'">
|
||||
{{ formatTime(currentScan.completeTime) }}
|
||||
</d-description-item>
|
||||
<d-description-item term="检测项" v-if="currentScan.totalChecks">
|
||||
{{ currentScan.completedChecks }}/{{ currentScan.totalChecks }}
|
||||
</d-description-item>
|
||||
</d-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="scan-actions mt-4" v-if="currentScan.status === 'completed'">
|
||||
<d-button @click="showReportDetails(currentScan)" variant="solid">查看报告详情</d-button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- 历史检测记录 -->
|
||||
<Card class="mt-4">
|
||||
<div class="upload-title">历史检测记录</div>
|
||||
<div class="history-title">
|
||||
<d-select
|
||||
style="width: 30%"
|
||||
v-model="historyFilter"
|
||||
:options="filterOptions"
|
||||
placeholder="筛选状态"
|
||||
></d-select>
|
||||
</div>
|
||||
|
||||
<d-table
|
||||
:show-loading="loading"
|
||||
class="jyh-table"
|
||||
:data="filteredHistory"
|
||||
v-if="filteredHistory.length > 0"
|
||||
table-layout="auto"
|
||||
>
|
||||
<d-column type="index" width="40"></d-column>
|
||||
<d-column field="target" header="检测目标" :width="300">
|
||||
<template #default="scope">
|
||||
<div class="target-text">{{ scope.row.target }}</div>
|
||||
</template>
|
||||
</d-column>
|
||||
<d-column field="type" header="检测类型">
|
||||
<template #default="scope">
|
||||
<d-tag :type="getTypeColor(scope.row.type)">{{ getTypeText(scope.row.type) }}</d-tag>
|
||||
</template>
|
||||
</d-column>
|
||||
<d-column field="status" header="状态">
|
||||
<template #default="scope">
|
||||
<d-tag :type="getStatusColor(scope.row.status)">{{ getStatusText(scope.row.status) }}</d-tag>
|
||||
</template>
|
||||
</d-column>
|
||||
<d-column field="startTime" header="开始时间">
|
||||
<template #default="scope">{{ formatTime(scope.row.startTime) }}</template>
|
||||
</d-column>
|
||||
<d-column field="riskLevel" header="风险等级">
|
||||
<template #default="scope">
|
||||
<d-tag :color="getRiskColor(scope.row.riskLevel)">{{ scope.row.riskLevel || '无风险' }}</d-tag>
|
||||
</template>
|
||||
</d-column>
|
||||
<d-column header="操作" fixed-right="0px" width="130">
|
||||
<template #default="scope">
|
||||
<a class="devui-link" @click="showReportDetails(scope.row)" v-if="scope.row.status === 'completed'">
|
||||
查看报告
|
||||
</a>
|
||||
<a class="devui-link" @click="rescan(scope.row)" v-else-if="scope.row.status === 'failed'">
|
||||
重新检测
|
||||
</a>
|
||||
<span v-else>--</span>
|
||||
</template>
|
||||
</d-column>
|
||||
</d-table>
|
||||
|
||||
<NoData v-else :small="false"></NoData>
|
||||
|
||||
<div class="mt-20 mb-20 flex justify-end" v-if="filteredHistory.length > 0">
|
||||
<d-pagination
|
||||
size="md"
|
||||
:page-size-options="[10, 20, 50]"
|
||||
:total="pager.total"
|
||||
v-model:pageSize="pager.pageSize"
|
||||
v-model:pageIndex="pager.pageIndex"
|
||||
:max-items="5"
|
||||
:can-change-page-size="true"
|
||||
:can-view-total="true"
|
||||
total-item-text="总计"
|
||||
@page-index-change="getHistoryList"
|
||||
@page-size-change="getHistoryList"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 报告详情抽屉 -->
|
||||
<d-drawer
|
||||
v-model="reportVisible"
|
||||
:width="800"
|
||||
title="检测报告详情"
|
||||
>
|
||||
<ReportDetail
|
||||
:report-data="currentReport"
|
||||
@close="reportVisible = false"
|
||||
></ReportDetail>
|
||||
</d-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted,onUnmounted, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import NoData from '@/components/NoData/NoData.vue';
|
||||
import ReportDetail from './ReportDetail.vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { getScanHistory, submitScanTask, getScanStatus } from '@/api/jyh/scanCenter';
|
||||
|
||||
const route = useRoute();
|
||||
const title = ref('安全检测中心');
|
||||
|
||||
// 上传相关变量
|
||||
const activeUploadType = ref('file');
|
||||
const fileInput = ref(null);
|
||||
const scanUrl = ref('');
|
||||
const sbomContent = ref('');
|
||||
const isDragging = ref(false);
|
||||
|
||||
// 检测状态相关
|
||||
// 替换原有的 const currentScan = ref(null);
|
||||
const currentScan = ref({
|
||||
taskId: 'scan_1689234567890', // 任务唯一标识
|
||||
type: 'file', // 检测类型:file/url/sbom
|
||||
target: 'app-release-v2.1.apk', // 检测目标
|
||||
status: 'scanning', // 任务状态:pending/scanning/completed/failed
|
||||
progress: 65, // 检测进度(0-100)
|
||||
startTime: '2024-07-15T10:30:22', // 开始时间
|
||||
estimatedCompleteTime: '2024-07-15T10:35:10', // 预计完成时间
|
||||
currentStep: '执行漏洞深度扫描', // 当前执行步骤
|
||||
totalChecks: 42, // 总检测项数
|
||||
completedChecks: 27, // 已完成检测项数
|
||||
// 以下字段在状态为 completed 时存在
|
||||
// riskStats: { critical: 1, high: 3, medium: 2, low: 5 },
|
||||
// completeTime: '2024-07-15T10:34:55',
|
||||
// 以下字段在状态为 failed 时存在
|
||||
// errorMsg: '检测引擎连接超时'
|
||||
});
|
||||
const scanInterval = ref(null);
|
||||
const loading = ref(false);
|
||||
|
||||
// 分页相关
|
||||
const pager = ref({
|
||||
total: 0,
|
||||
pageIndex: 1,
|
||||
pageSize: 10
|
||||
});
|
||||
|
||||
// 历史记录相关
|
||||
// 模拟历史检测记录数据(可直接替换到组件的 historyList 初始化中)
|
||||
const historyList = ref([
|
||||
// 文件检测 - 已完成(高危)
|
||||
{
|
||||
taskId: 'scan_1720012345678',
|
||||
target: 'enterprise-app-v3.2.1.zip',
|
||||
type: 'file',
|
||||
status: 'completed',
|
||||
riskLevel: '高危',
|
||||
startTime: '2024-07-01T09:15:30',
|
||||
completeTime: '2024-07-01T09:28:45',
|
||||
duration: 810000, // 13分30秒
|
||||
riskStats: { critical: 1, high: 4, medium: 3, low: 2 },
|
||||
fileSize: 28560000 // 约28.5MB
|
||||
},
|
||||
// URL检测 - 已完成(低危)
|
||||
{
|
||||
taskId: 'scan_1720015678901',
|
||||
target: 'https://internal-api.company.com',
|
||||
type: 'url',
|
||||
status: 'completed',
|
||||
riskLevel: '低危',
|
||||
startTime: '2024-07-01T14:30:22',
|
||||
completeTime: '2024-07-01T14:32:10',
|
||||
duration: 108000, // 1分48秒
|
||||
riskStats: { critical: 0, high: 0, medium: 0, low: 1 }
|
||||
},
|
||||
// SBOM检测 - 失败
|
||||
{
|
||||
taskId: 'scan_1720018901234',
|
||||
target: 'SBOM-cyclonedx-project.json',
|
||||
type: 'sbom',
|
||||
status: 'failed',
|
||||
riskLevel: '-',
|
||||
startTime: '2024-07-02T10:05:18',
|
||||
completeTime: '2024-07-02T10:06:03',
|
||||
duration: 45000, // 45秒
|
||||
errorMsg: 'SBOM格式错误:缺少components字段'
|
||||
},
|
||||
// 文件检测 - 已完成(中危)
|
||||
{
|
||||
taskId: 'scan_1720022345678',
|
||||
target: 'mobile-client-v2.8.apk',
|
||||
type: 'file',
|
||||
status: 'completed',
|
||||
riskLevel: '中危',
|
||||
startTime: '2024-07-02T16:40:55',
|
||||
completeTime: '2024-07-02T16:52:30',
|
||||
duration: 695000, // 11分35秒
|
||||
riskStats: { critical: 0, high: 0, medium: 2, low: 5 },
|
||||
fileSize: 42800000 // 约42.8MB
|
||||
},
|
||||
// URL检测 - 进行中
|
||||
{
|
||||
taskId: 'scan_1720025678901',
|
||||
target: 'https://admin-portal.company.com',
|
||||
type: 'url',
|
||||
status: 'scanning',
|
||||
riskLevel: '-',
|
||||
startTime: '2024-07-03T08:12:10',
|
||||
progress: 65,
|
||||
currentStep: '检测API接口漏洞'
|
||||
},
|
||||
// SBOM检测 - 已完成(无风险)
|
||||
{
|
||||
taskId: 'scan_1720028901234',
|
||||
target: 'SBOM-spdx-v2.3.json',
|
||||
type: 'sbom',
|
||||
status: 'completed',
|
||||
riskLevel: '无风险',
|
||||
startTime: '2024-07-03T11:30:00',
|
||||
completeTime: '2024-07-03T11:31:20',
|
||||
duration: 80000, // 1分20秒
|
||||
riskStats: { critical: 0, high: 0, medium: 0, low: 0 }
|
||||
},
|
||||
// 文件检测 - 失败
|
||||
{
|
||||
taskId: 'scan_1720032345678',
|
||||
target: 'legacy-system.tar.gz',
|
||||
type: 'file',
|
||||
status: 'failed',
|
||||
riskLevel: '-',
|
||||
startTime: '2024-07-03T15:20:40',
|
||||
completeTime: '2024-07-03T15:21:10',
|
||||
duration: 30000, // 30秒
|
||||
errorMsg: '文件损坏:无法解压缩归档内容',
|
||||
fileSize: 157000000 // 约157MB
|
||||
},
|
||||
// URL检测 - 已完成(致命)
|
||||
{
|
||||
taskId: 'scan_1720035678901',
|
||||
target: 'https://old-website.company.com',
|
||||
type: 'url',
|
||||
status: 'completed',
|
||||
riskLevel: '致命',
|
||||
startTime: '2024-07-04T09:50:15',
|
||||
completeTime: '2024-07-04T09:51:50',
|
||||
duration: 95000, // 1分35秒
|
||||
riskStats: { critical: 2, high: 1, medium: 0, low: 0 }
|
||||
},
|
||||
// SBOM检测 - 已完成(中危)
|
||||
{
|
||||
taskId: 'scan_1720038901234',
|
||||
target: 'microservice-sbom.xml',
|
||||
type: 'sbom',
|
||||
status: 'completed',
|
||||
riskLevel: '中危',
|
||||
startTime: '2024-07-04T13:18:30',
|
||||
completeTime: '2024-07-04T13:19:45',
|
||||
duration: 75000, // 1分15秒
|
||||
riskStats: { critical: 0, high: 0, medium: 1, low: 3 }
|
||||
},
|
||||
// 文件检测 - 已完成(高危)
|
||||
{
|
||||
taskId: 'scan_1720042345678',
|
||||
target: 'desktop-software-v5.1.exe',
|
||||
type: 'file',
|
||||
status: 'completed',
|
||||
riskLevel: '高危',
|
||||
startTime: '2024-07-05T10:08:22',
|
||||
completeTime: '2024-07-05T10:25:10',
|
||||
duration: 1008000, // 16分48秒
|
||||
riskStats: { critical: 0, high: 3, medium: 2, low: 1 },
|
||||
fileSize: 85600000 // 约85.6MB
|
||||
}
|
||||
]);
|
||||
const historyFilter = ref('all');
|
||||
const filterOptions = ref([
|
||||
{ value: 'all', name: '全部状态' },
|
||||
{ value: 'completed', name: '已完成' },
|
||||
{ value: 'scanning', name: '检测中' },
|
||||
{ value: 'failed', name: '失败' }
|
||||
]);
|
||||
|
||||
// 报告详情相关
|
||||
const reportVisible = ref(false);
|
||||
const currentReport = ref(null);
|
||||
|
||||
// 切换上传类型
|
||||
const handleUploadTypeChange = (key) => {
|
||||
activeUploadType.value = key;
|
||||
};
|
||||
|
||||
// 文件上传相关方法
|
||||
const triggerFileUpload = () => {
|
||||
fileInput.value?.click();
|
||||
};
|
||||
|
||||
const handleFileUpload = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
startFileScan(file);
|
||||
// 清空输入以允许重复上传同一文件
|
||||
e.target.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileDrop = (e) => {
|
||||
isDragging.value = false;
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file) {
|
||||
startFileScan(file);
|
||||
}
|
||||
};
|
||||
|
||||
// 开始不同类型的检测
|
||||
const startFileScan = async (file) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const { data } = await submitScanTask({
|
||||
type: 'file',
|
||||
file: formData,
|
||||
target: file.name
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
currentScan.value = data.data;
|
||||
startScanPolling();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('文件检测提交失败', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const startUrlScan = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const { data } = await submitScanTask({
|
||||
type: 'url',
|
||||
target: scanUrl.value
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
currentScan.value = data.data;
|
||||
startScanPolling();
|
||||
scanUrl.value = '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('URL检测提交失败', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const startSbomScan = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const { data } = await submitScanTask({
|
||||
type: 'sbom',
|
||||
content: sbomContent.value,
|
||||
target: 'SBOM内容'
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
currentScan.value = data.data;
|
||||
startScanPolling();
|
||||
sbomContent.value = '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('SBOM检测提交失败', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 轮询获取扫描状态
|
||||
const startScanPolling = () => {
|
||||
// 清除之前的定时器
|
||||
if (scanInterval.value) {
|
||||
clearInterval(scanInterval.value);
|
||||
}
|
||||
|
||||
// 立即获取一次状态
|
||||
fetchScanStatus();
|
||||
|
||||
// 设置定时器
|
||||
scanInterval.value = setInterval(() => {
|
||||
fetchScanStatus();
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
const fetchScanStatus = async () => {
|
||||
if (!currentScan.value?.taskId) return;
|
||||
|
||||
try {
|
||||
const { data } = await getScanStatus(currentScan.value.taskId);
|
||||
if (data.success) {
|
||||
currentScan.value = data.data;
|
||||
|
||||
// 如果扫描完成或失败,停止轮询
|
||||
if (['completed', 'failed'].includes(currentScan.value.status)) {
|
||||
clearInterval(scanInterval.value);
|
||||
scanInterval.value = null;
|
||||
// 刷新历史列表
|
||||
getHistoryList();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取扫描状态失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取历史记录
|
||||
const getHistoryList = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const { data } = await getScanHistory({
|
||||
page: pager.value.pageIndex,
|
||||
size: pager.value.pageSize,
|
||||
status: historyFilter.value !== 'all' ? historyFilter.value : ''
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
historyList.value = data.data.records;
|
||||
pager.value.total = data.data.total;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取历史记录失败', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 查看报告详情
|
||||
const showReportDetails = async (record) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
// 这里应该调用获取报告详情的API
|
||||
currentReport.value = record; // 临时使用记录数据,实际应替换为API调用
|
||||
reportVisible.value = true;
|
||||
} catch (error) {
|
||||
console.error('获取报告详情失败', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 重新检测
|
||||
const rescan = (record) => {
|
||||
switch (record.type) {
|
||||
case 'file':
|
||||
activeUploadType.value = 'file';
|
||||
break;
|
||||
case 'url':
|
||||
activeUploadType.value = 'url';
|
||||
scanUrl.value = record.target;
|
||||
break;
|
||||
case 'sbom':
|
||||
activeUploadType.value = 'sbom';
|
||||
sbomContent.value = record.content || '';
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time) => {
|
||||
return time ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : '--';
|
||||
};
|
||||
|
||||
// 状态文本映射
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
'pending': '等待中',
|
||||
'scanning': '检测中',
|
||||
'completed': '已完成',
|
||||
'failed': '失败'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
};
|
||||
|
||||
// 状态颜色映射
|
||||
const getStatusColor = (status) => {
|
||||
const colorMap = {
|
||||
'pending': 'primary',
|
||||
'scanning': 'processing',
|
||||
'completed': 'success',
|
||||
'failed': 'danger'
|
||||
};
|
||||
return colorMap[status] || 'default';
|
||||
};
|
||||
|
||||
// 进度条状态映射
|
||||
const getProgressStatus = (status) => {
|
||||
if (status === 'failed') return 'error';
|
||||
if (status === 'completed') return 'success';
|
||||
if (status === 'scanning') return 'processing';
|
||||
return 'active';
|
||||
};
|
||||
|
||||
// 检测类型文本映射
|
||||
const getTypeText = (type) => {
|
||||
const typeMap = {
|
||||
'file': '文件',
|
||||
'url': 'URL',
|
||||
'sbom': 'SBOM'
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
};
|
||||
|
||||
// 检测类型颜色映射
|
||||
const getTypeColor = (type) => {
|
||||
const colorMap = {
|
||||
'file': 'primary',
|
||||
'url': 'info',
|
||||
'sbom': 'secondary'
|
||||
};
|
||||
return colorMap[type] || 'default';
|
||||
};
|
||||
|
||||
// 风险等级颜色映射
|
||||
const getRiskColor = (level) => {
|
||||
const colorMap = {
|
||||
'高危': 'danger',
|
||||
'中危': 'warning',
|
||||
'低危': 'info',
|
||||
'无风险': 'success'
|
||||
};
|
||||
return colorMap[level] || 'default';
|
||||
};
|
||||
|
||||
// 筛选历史记录
|
||||
const filteredHistory = computed(() => {
|
||||
if (historyFilter.value === 'all') {
|
||||
return historyList.value;
|
||||
}
|
||||
return historyList.value.filter(item => item.status === historyFilter.value);
|
||||
});
|
||||
|
||||
// 监听筛选条件变化
|
||||
watch(historyFilter, () => {
|
||||
pager.value.pageIndex = 1;
|
||||
getHistoryList();
|
||||
});
|
||||
|
||||
// 页面加载时获取历史记录
|
||||
onMounted(() => {
|
||||
getHistoryList();
|
||||
});
|
||||
|
||||
// 组件卸载时清除定时器
|
||||
onUnmounted(() => {
|
||||
if (scanInterval.value) {
|
||||
clearInterval(scanInterval.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-wrap {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.upload-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.upload-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
color: #191919;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--devui-primary);
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
border-color: var(--devui-primary);
|
||||
background-color: rgba(22, 93, 255, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scan-status-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.scan-progress {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
color: var(--devui-text-secondary);
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.history-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.target-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
|
||||
.secret-breadcrumb {
|
||||
padding: 7px 20px 12px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
///deep/ .devui-tabs-content {
|
||||
// padding: 16px 0;
|
||||
//}
|
||||
//
|
||||
///deep/ .devui-form-item {
|
||||
// margin-bottom: 16px;
|
||||
//}
|
||||
</style>
|
||||
Reference in New Issue
Block a user