可信代码库V2版-安全智库页面原型开发、安全检测中心页面原型开发、关于我们页面原型开发

This commit is contained in:
fmk1023
2025-11-06 10:54:58 +08:00
parent 533e20b2f3
commit f5b59fadd9
29 changed files with 8077 additions and 650 deletions

View 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>

View 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>