可信代码库-AI助手显示软件详情页面开发

This commit is contained in:
付民康
2025-05-09 14:13:08 +08:00
parent 95bac2fe24
commit e9a10f9bfc
20 changed files with 4096 additions and 9755 deletions

9750
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -268,6 +268,49 @@
text-align: left;
}
}
.jyh-card2 {
padding: 16px 20px;
background: #F3F3F3;
.card-title {
color: #252B3A;
font-family: HarmonyOS Sans SC;
font-weight: bold;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
margin-bottom: 16px;
}
.card-key {
color: #595959;
font-family: HarmonyOS Sans SC;
font-weight: regular;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
}
.card-val {
color: #191919;
font-family: HarmonyOS Sans;
font-weight: regular;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
}
.card-url {
color: #5E7CE0;
font-family: HarmonyOS Sans;
font-weight: regular;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
}
}
.devui-status:before {
margin-right: 12px;

View File

@@ -0,0 +1,269 @@
<template>
<div class="secret-container">
<div class="title">以下是Redis的软件详情</div>
<div class="mt-3">
<OssDetailCp ref="ossDetailRef" :versionId="versionId"/>
</div>
<d-button v-if="isLogin" class="output-btn" @click="openSubscription">订阅</d-button>
<div class="bubble-bottom-operations">
<img onclick="handleRefresh()" src="@/assets/imgs/jyh/ai/icon_generate.png" />
<i class="icon-file" title="复制" onclick="handleFile()"></i>
<i class="icon-like" title="点赞" onclick="handleLike()"></i>
<i class="icon-dislike" title="踩" onclick="handleDislike()"></i>
<i class="icon-clever-customer" title="智能客服" onclick="handleCustomerService()"></i>
<i class="icon-more-operate" title="更多操作" onclick="handleMore()"></i>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch, nextTick } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
import { Message } from 'vue-devui';
import { storeToRefs } from 'pinia';
import NoData from '@/components/NoData/NoData.vue';
import { useAccountStore } from '@/stores/user';
import * as XLSX from 'xlsx';
const { formatTime } = useTimeFormat();
import { getHomeNoticeList, getReleaseExport, releasePage, searchLanguageList } from '@/api/jyh';
import { useTimeFormat } from '@/utils/hooks/useTimeFormat';
import { useGlobalInfoStore } from '@/stores/Global';
import OssDetailCp from './OssDetail/index.vue'
const router = useRouter();
const route = useRoute();
const filterTableRef = ref();
const ossDetailRef = ref(null);
const versionId = ref('3238');
const { namespace } = getOrgInfo();
const userInfo = useAccountStore();
const { isLogin } = storeToRefs(userInfo);
// 定义 tableKey用于强制刷新表格
const tableKey = ref(0);
const tableData = ref([]);
// 定义 columns 数据
const columns = ref([
{ key: 'subject', label: '漏洞类型', visible: true, filterable: true, sortable: false },
{ key: 'overview', label: '漏洞内容', visible: true, filterable: true, sortable: false },
{ key: 'importance', label: '重要性', visible: true, filterable: true, sortable: false,width: 80 },
{ key: 'publishTime', label: '时间', visible: true, filterable: true, sortable: false },
]);
// 调用子组件方法的示例函数
function openSubscription() {
if (ossDetailRef.value) {
// 假设子组件有一个名为 'someMethod' 的方法
ossDetailRef.value.openSubscription();
}
}
// 跳转
const gotoDetail = (row) => {
// 后续请换成params暂时params传参没传过去
router.push({
path: 'repoDetail/' + row.id
});
};
const getReleaseList = async () => {
const { data, error } = await getHomeNoticeList();
if (!error && data) {
tableData.value = data.data || [];
console.log(tableData.value);
}
};
onMounted(async () => {
nextTick(() => {
getReleaseList();
});
// getReleaseList();
});
const exportResult = async () => {
const selectDatas = tableData.value.map((d) => d.id);
if (!tableData.value.length || selectDatas.length <= 0) {
Message.info('未选择任何数据');
return;
}
const { data, error } = await getReleaseExport({
list: selectDatas,
});
if (data && !error) {
// 方法1如果后端返回的是文件流Blob
const blob = new Blob([data.data]);
const downloadUrl = window.URL.createObjectURL(blob);
// 创建一个临时的a标签触发下载
const link = document.createElement('a');
link.href = downloadUrl;
// 从Content-Disposition头中获取文件名或者使用默认文件名
const contentDisposition = data.headers['content-disposition'];
let fileName = 'export.xlsx'; // 默认文件名
// if (contentDisposition) {
// const fileNameMatch = contentDisposition.match(/filename="?(.+)"?/);
// if (fileNameMatch && fileNameMatch[1]) {
// fileName = fileNameMatch[1].replace(/^[^0-9]*|[^0-9.xlsx]*$/g, "");
// }
// }
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
// 清理
document.body.removeChild(link);
window.URL.revokeObjectURL(downloadUrl);
} else {
console.error('导出失败:', error);
// 这里可以添加错误提示比如使用Element Plus的ElMessage
ElMessage.error('文件导出失败');
}
};
// 事件处理函数
function handleRefresh() {
console.log('刷新点击');
// 实际业务代码示例:
// location.reload(); // 重新加载页面
// 或调用API刷新数据
}
function handleFile() {
console.log('文件点击');
// 示例:触发文件上传
// document.getElementById('file-input').click();
}
function handleLike() {
console.log('点赞点击');
// 示例:切换点赞状态
const likeBtn = document.querySelector('.icon-like');
likeBtn.classList.toggle('active');
// 这里可以添加AJAX请求提交点赞状态
}
function handleDislike() {
console.log('踩点击');
// 类似点赞的逻辑
const dislikeBtn = document.querySelector('.icon-dislike');
dislikeBtn.classList.toggle('active');
}
function handleCustomerService() {
console.log('客服点击');
// 示例:打开客服对话框
// openChatWindow();
}
function handleMore() {
console.log('更多操作点击');
// 示例:显示下拉菜单
// showDropdownMenu();
}
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.secret-container {
position: relative;
//min-height: 600px;
background: #ffffff;
padding: 20px;
margin-top: 16px;
border-radius: 15px;
.title {
color: #191919;
font-family: HarmonyOS Sans SC;
font-weight: bold;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
}
.secret-op {
position: absolute;
right: 0;
top: 0;
}
}
.error {
color: $devui-contrast;
}
:deep(.devui-table__row td) {
padding-top: 16px;
padding-bottom: 16px;
}
:deep(.devui-tag .devui-tag--default) {
background-color: #fff;
border-radius: 12px;
padding: 4px 8px;
font-size: 12px;
line-height: 16px;
margin-right: 8px;
border: 1px solid #e3e3ee;
}
.one-line {
word-break: break-all;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.title {
font-size: 14px;
font-weight: 400;
color: #9a9b9c;
line-height: 20px;
}
.cur-title {
font-size: 14px;
font-weight: 500;
color: #2d2d2e;
line-height: 20px;
}
.output-btn {
border-radius: 16px;
background: #DFDFDF;
color: #191919;
font-family: HarmonyOS Sans SC;
font-weight: regular;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
}
.secret-breadcrumb {
padding: 7px 20px 12px;
background: #ffffff;
}
.bubble-bottom-operations {
margin-top: 16px;
margin-left: auto;
display: flex;
gap: 8px; /* 图标之间的间隔 */
align-items: center; /* 垂直居中图标 */
justify-content: end;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,296 @@
<template>
<div class="info-outside w-full">
<div class="info">
<div class="flex items-center justify-start w-full gap-1">
<img src="@/assets/imgs/jyh/aiIcon.png" />
<span class="text-base font-medium">AI修复建议:</span>
</div>
<div>
<br />通过AI技术精准诊断漏洞类型智能匹配多维度定制化修复策略实现从数据校验代码加固到权限控制的全链路防御助力漏洞修复效率提升全方位保障开源软件使用安全<br />
<!-- <ul>-->
<!-- <li v-for="info in infoList">{{ info }}</li>-->
<!-- </ul>-->
</div>
</div>
</div>
<div class="ai-container">
<template v-if="tableData.length > 0">
<div class="ai-container-card" style="width: 100%" v-for="(rowItem, index) in tableData" :key="index">
<Panel :expand="rowItem.expand">
<template #header>
<div class="ai-panel-header">
<div style="padding: 25px 8px 0px 20px">
<img style="width: 36px; height: 33px" src="@/assets/imgs/jyh/warning.png" />
</div>
<div style="width: 100%; padding: 20px 20px 0px 0">
<div class="ai-panel-header-title">
<div>
<d-tag color="red-w98" class="mr-1" size="sm">严重</d-tag>
<span>{{ rowItem.libraryName || '--' }}</span>
</div>
<div
style="cursor: pointer; display: flex; gap: 8px; align-items: center"
@click="rowItem.expand = !rowItem.expand"
>
<d-icon :name="rowItem.expand ? 'icon-expand-new' : 'icon-drag-new'"></d-icon>
<span>{{ rowItem.expand ? '收起' : '展开' }}</span>
</div>
</div>
<div class="ai-panel-header-sub-title">
<div>
<span class="weak-text">安全问题</span>
<span>{{ rowItem.vulnerabilities.length || '--' }}</span>
</div>
<!-- <div>-->
<!-- <span class="weak-text">修复兼容性</span>-->
<!-- <span>{{ rowItem.vulnerabilities.length || '&#45;&#45;' }}</span>-->
<!-- <d-tooltip-->
<!-- position="bottom"-->
<!-- content="这个评分是在比较当前版本和推荐版本之间的差异,并建议修复此组件时的复杂程度。"-->
<!-- >-->
<!-- <img src="@/assets/imgs/jyh/help.png" class="icon-sm" />-->
<!-- </d-tooltip>-->
<!-- <d-tag class="ml-1" type="warning" size="sm">{{'&#45;&#45;'}}</d-tag>-->
<!-- </div>-->
<div>
<span class="weak-text">升级版本</span>
<span>{{ rowItem.currentVersion || '--' }}</span>
<img src="@/assets/imgs/jyh/ic_digital_power_next_step_@2x.png" class="icon-sm" />
<span>{{ rowItem.recommendedVersion || '--' }}</span>
</div>
</div>
</div>
</div>
</template>
<template #body>
<div style="padding: 8px 20px 20px 20px; width: 100%">
<Table class="jyh-table" :tableConfig="tableConfig" :dataSource="rowItem.vulnerabilities" :libraryName="rowItem.libraryName" :currentVersion="rowItem.currentVersion"/>
<div class="w-full flex items-center justify-end">
<span>{{ rowItem.vulnerabilities.length }}</span>
</div>
</div>
</template>
</Panel>
</div>
<div class="mt-20 mb-20 flex justify-end" v-if="tableData.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="fetchVulnList()"
@page-size-change="fetchVulnList()"
/>
</div>
</template>
<NoData v-else :small="false"></NoData>
</div>
</template>
<script setup>
import { defineProps, onMounted, ref } from 'vue';
import Panel from '@/components/AIRepair/Panel.vue';
import Table from '@/components/AIRepair/Table.vue';
import NoData from '@/components/NoData/NoData.vue';
import { getVulnList, remediationSearchRemediations } from '@/api/jyh';
const infoList = ref([
'当前代码主要风险为易受常见的注入攻击包括命令注入、SQL注入、表达式注入针对每种攻击应提供相应的防范措施如数据校验、使用安全函数和限制程序权限。',
'相对上一次检查【提升点】在于代码基础编码问题少了47.8%问题总数降低29.3%'
]);
const tableConfig = ref([
{
field: 'vulnId',
header: '安全问题编号',
sortable: true,
sortMethod: null
},
{
field: 'vulnType',
header: '安全问题类型',
sortable: false,
sortMethod: null,
format: (val) => {
// val小写转大写
return (val || '').toUpperCase();
}
},
{
field: 'vulnId',
header: 'CWE编号',
sortable: false,
sortMethod: null
},
{
field: 'cvssScore',
header: '分值',
sortable: true,
sortMethod: null
}
]);
const pager = ref({
total: 10,
pageIndex: 1,
pageSize: 10
});
const propsData = defineProps(['versionId']);
const fetchVulnList = async () => {
const { data } = await remediationSearchRemediations({
// softwareId: 47,
softwareId: propsData.versionId,
current: pager.value.pageIndex,
size: pager.value.pageSize
// componentId: '',
});
if (data.data.code === 200) {
tableData.value = data?.data?.data?.records || [];
pager.value.total = data?.data?.data?.total || 0;
// pager.value.total = data?.data?.data?.total || [];
}
};
const tableData = ref([
// {
// "componentId": 1,
// "libraryName": "Spring Framework",
// "currentVersion": "5.3.26",
// "vulnerabilities": [
// {
// "vulnId": "CVE-2024-12345",
// "severity": "High",
// "vulnType": "Remote Code Execution",
// "cvssScore": 8.5,
// "remediation": {
// "recommendedFix": "Upgrade to version 5.3.27 or later.",
// "patchLink": "https://spring.io/security/advisory/CVE-2024-12345",
// "recommendedVersion": "5.3.27"
// }
// }
// ]
// },
// {
// "componentId": 2,
// "libraryName": "Log4j",
// "currentVersion": "2.14.0",
// "vulnerabilities": [
// {
// "vulnId": "CVE-2023-67890",
// "severity": "Critical",
// "vulnType": "Information Disclosure",
// "cvssScore": 9.8,
// "remediation": {
// "recommendedFix": "Apply security patch 2.14.1.",
// "patchLink": "https://logging.apache.org/log4j/2.x/security.html",
// "recommendedVersion": "2.14.1"
// }
// }
// ]
// }
]);
onMounted(() => {
fetchVulnList();
});
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.ai-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 20px;
justify-content: center;
&-card {
border-top: 2px solid $devui-danger;
background-color: $devui-base-bg;
border-radius: var(--devui-border-radius-card, 8px);
box-shadow: var(--devui-shadow-length-base, 0 2px 6px 0) var(--devui-light-shadow, rgba(37, 43, 58, 0.12));
}
}
.ai-panel-header {
display: flex;
flex-direction: row;
height: 80px;
&-title {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
&-sub-title {
display: flex;
align-items: center;
margin-top: 10px;
div {
display: flex;
gap: 8px;
align-items: center;
margin-right: 20px;
position: relative;
&:not(:last-child)::after {
content: '';
position: absolute;
right: -10px;
height: 100%;
width: 1px;
background-color: $devui-line;
}
}
}
}
.weak-text {
color: $devui-text-weak;
}
.icon-sm {
width: 14px;
height: 14px;
}
.info-outside {
margin: 20px 0;
padding: 1px;
border-radius: 8px;
background-image: linear-gradient(
134.79deg,
#a4f7df 0%,
#a0cefb 21%,
#5f9afe 43%,
#d9b9fc 62%,
#01efbb 73%,
#fbae46 100%
);
}
.info {
padding: 10px 20px;
border-radius: 8px;
background-color: #ffffff;
img {
width: 24px;
height: 24px;
}
ul {
list-style: disc;
padding-left: 18px;
line-height: 22px;
}
}
</style>

View File

@@ -0,0 +1,124 @@
<template>
<div class="version-info">
<Card simple class="jyh-card mb-4">
<div class="card-title">
<d-tooltip position="left" content="此内容由AI辅助生成">
<img
style="width: 14px; height: 14px;display: inline-block"
src="@/assets/imgs/jyh/aiLogo.png"
/>
</d-tooltip>
基本信息
<!-- <span style="color: #3b82f6;font-size: 12px;font-weight: normal">(该数据由AI生成)</span>-->
</div>
<d-row class="mb-3">
<d-col :span="8">
<d-tooltip position="left" content="此内容由AI辅助生成">
<img
style="width: 14px; height: 14px;display: inline-block"
src="@/assets/imgs/jyh/aiLogo.png"
/>
</d-tooltip>
<span class="card-key width-90">社区名称</span>
<span class="card-val">{{propsData.ReleaseInfo?.basicInfo?.communityName || propsData.ReleaseInfo?.criticalSoftwareAIGenerateInfo?.communityName || '--'}}</span>
</d-col>
<d-col :span="8">
<d-tooltip position="left" content="此内容由AI辅助生成">
<img
style="width: 14px; height: 14px;display: inline-block"
src="@/assets/imgs/jyh/aiLogo.png"
/>
</d-tooltip>
<span class="card-key width-90">官网地址</span>
<a :href="propsData.ReleaseInfo?.basicInfo?.communityWebsite || propsData.ReleaseInfo?.criticalSoftwareAIGenerateInfo?.communityName" class="card-url">{{
propsData.ReleaseInfo?.basicInfo?.communityWebsite || propsData.ReleaseInfo?.criticalSoftwareAIGenerateInfo?.communityName ||'--' }}</a>
</d-col>
<d-col :span="8">
<d-tooltip position="left" content="此内容由AI辅助生成">
<img
style="width: 14px; height: 14px;display: inline-block"
src="@/assets/imgs/jyh/aiLogo.png"
/>
</d-tooltip>
<span class="card-key width-90">联系方式</span>
<span class="card-val">{{
propsData.ReleaseInfo?.basicInfo?.communityContact || propsData.ReleaseInfo?.criticalSoftwareAIGenerateInfo?.communityContact ||'--' }}</span>
</d-col>
</d-row>
<d-row class="mb-3">
<d-col :span="24">
<div class="flex">
<d-tooltip position="left" content="此内容由AI辅助生成">
<img
style="width: 14px; height: 14px;display: inline-block"
src="@/assets/imgs/jyh/aiLogo.png"
/>
</d-tooltip>
<span class="card-key width-90 flex-shrink-0">社区描述</span>
<span class="card-val flex-grow-1">{{
propsData.ReleaseInfo?.basicInfo?.communityDescription || propsData.ReleaseInfo?.criticalSoftwareAIGenerateInfo?.communityDescription ||'--' }}</span>
</div>
</d-col>
</d-row>
</Card>
<Card simple class="jyh-card mb-4">
<div class="card-title">
<d-tooltip position="left" content="此内容由AI辅助生成">
<img
style="width: 14px; height: 14px;display: inline-block"
src="@/assets/imgs/jyh/aiLogo.png"
/>
</d-tooltip>
贡献者分布
</div>
<p class="card-desc">有关Pull Request创建者的公司信息和地区分布(使用公共GitHub信息进行分析)</p>
<div class="mt-3">
<chart-bar ref="chartBarRef" :companyGraph="companyGraph" :countryGraph="countryGraph" />
</div>
</Card>
</div>
</template>
<script setup lang="ts">
import ChartBar from '@/components/ChartBar/index.vue';
import { computed, defineProps, onMounted, ref } from 'vue';
const propsData = defineProps(['versionId','ReleaseInfo']);
const chartBarRef = ref();
const countryGraph = computed(() => {
if(propsData.ReleaseInfo.criticalSoftwareAIGenerateInfo&&propsData.ReleaseInfo.criticalSoftwareAIGenerateInfo.contributorCountries) {
let countriesObj = JSON.parse(propsData.ReleaseInfo.criticalSoftwareAIGenerateInfo.contributorCountries);
return Object.entries(countriesObj).map(([name, value]) => ({
name,
value
}));
} else {
return []
}
});
const companyGraph = computed(() => {
if(propsData.ReleaseInfo.criticalSoftwareAIGenerateInfo&&propsData.ReleaseInfo.criticalSoftwareAIGenerateInfo.organizationDistribution) {
let countriesObj = JSON.parse(propsData.ReleaseInfo.criticalSoftwareAIGenerateInfo.organizationDistribution);
return Object.entries(countriesObj).map(([name, value]) => ({
name,
value
}));
} else {
return []
}
});
onMounted(() => {
chartBarRef.value.onInited();
});
</script>
<style scoped lang="scss">
.card-desc {
font-size: 12px;
color: #808080;
}
</style>

View File

@@ -0,0 +1,286 @@
<template>
<d-drawer v-model="detailDrawerCtrl" class="w-[800px] p-[20px]">
<div class="flex flex-col h-full">
<div class="flex justify-between items-center mb-[12px]">
<p class="text-[18px]/[26px] ">恶意代码详情</p>
<d-icon @click="detailDrawerCtrl = false" name="icon-close" size="18px" class="cursor-pointer text-[#71757F]" />
</div>
<div class="flex flex-col mb-[12px] flex-1 h-0">
<div class="malware-detail-card mb-[16px]">
<p class="title">文件基本信息</p>
<div class="content">
<div class="flex items-center">
<div class="w-[160px] pr-[20px] flex-shrink-0 text-[#595959]">文件名称</div>
<div class="single-line-ellipsis">{{ props.detailRow?.packageFullName || '--' }}</div>
<d-icon name="icon-copy" class="ml-[8px] cursor-pointer" @click="handleCopy(props.detailRow?.packageFullName)" />
</div>
<div class="flex items-center">
<div class="w-[160px] pr-[20px] flex-shrink-0 text-[#595959]">文件路径</div>
<div class="single-line-ellipsis">{{ props.detailRow?.packagePath || '--' }}</div>
<d-icon name="icon-copy" class="ml-[8px] cursor-pointer" @click="handleCopy(props.detailRow?.packagePath)" />
</div>
<div class="flex items-center">
<div class="w-[160px] pr-[20px] flex-shrink-0 text-[#595959]">文件版本号</div>
<div class="single-line-ellipsis">{{ props.detailRow?.packageVersion || '--' }}</div>
<d-icon name="icon-copy" class="ml-[8px] cursor-pointer" @click="handleCopy(props.detailRow?.packageVersion)" />
</div>
<div class="flex items-center">
<div class="w-[160px] pr-[20px] flex-shrink-0 text-[#595959]">LLM名称</div>
<div class="single-line-ellipsis">{{ props.detailRow?.llmName || '--' }}</div>
<d-icon name="icon-copy" class="ml-[8px] cursor-pointer" @click="handleCopy(props.detailRow?.llmName)" />
</div>
<div class="flex items-center">
<div class="w-[160px] pr-[20px] flex-shrink-0 text-[#595959]">敏感API总数</div>
<div class="single-line-ellipsis">{{ props.detailRow?.totalApisCalledCount }}</div>
</div>
<div class="flex items-center">
<div class="w-[160px] pr-[20px] flex-shrink-0 text-[#595959]">可疑文件总数</div>
<div class="single-line-ellipsis">{{ props.detailRow?.suspiciousFilesCount }}</div>
</div>
<div class="flex items-center">
<div class="w-[160px] pr-[20px] flex-shrink-0 text-[#595959]">是否恶意代码</div>
<div class="single-line-ellipsis">
<span class="card-val" v-if="props.detailRow?.isMalicious===0">
<d-tag color="#5e7ce0" size="sm"></d-tag>
</span>
<span class="card-val" v-else>
<d-tag color="#c7000b" size="sm"></d-tag>
</span>
</div>
</div>
<div class="flex items-center">
<div class="w-[160px] pr-[20px] flex-shrink-0 text-[#595959]">大模型是否正常回复</div>
<div class="single-line-ellipsis">
<span class="card-val" v-if="props.detailRow?.isModelResponded===0">
<d-tag color="#5e7ce0" size="sm"></d-tag>
</span>
<span class="card-val" v-else>
<d-tag color="#c7000b" size="sm"></d-tag>
</span>
</div>
</div>
<!-- <div class="flex items-center">-->
<!-- <div class="w-[160px] pr-[20px] flex-shrink-0 text-[#595959]">文件sha256</div>-->
<!-- <div :title="malwareDetails?.code" class="single-line-ellipsis">{{ malwareDetails?.code }}</div>-->
<!-- <d-icon name="icon-copy" class="ml-[8px] cursor-pointer" @click="handleCopy(malwareDetails?.code)" />-->
<!-- </div>-->
</div>
</div>
<div class="malware-detail-card mb-[16px]">
<p class="title">调用的敏感API列表</p>
<div class="content">
<d-table
:data="malwareDetails?.apisCalled"
class="w-full bg-white malware-list-table"
:fix-header="malwareDetails?.apisCalled.length > 0"
max-height="300px"
>
<d-column field="isMalicious" header="是否恶意代码">
<template #default="scope">
<span class="card-val" v-if="scope.row.isMalicious===0">
<d-tag color="#5e7ce0" size="sm">否</d-tag>
</span>
<span class="card-val" v-else>
<d-tag color="#c7000b" size="sm">是</d-tag>
</span>
</template>
</d-column>
<d-column
field="maliciousCodeCategory"
header="恶意代码分类"
/>
<template #empty>
<NoData style="padding: 0; margin: -30px 0;" :width="80" />
</template>
</d-table>
<!-- <d-pagination-->
<!-- class="flex justify-end mt-[20px]"-->
<!-- :total="pager.total"-->
<!-- v-model:pageSize="pager.pageSize"-->
<!-- v-model:pageIndex="pager.pageIndex"-->
<!-- :can-view-total="true"-->
<!-- :can-change-page-size="true"-->
<!-- :max-items="5"-->
<!-- @page-index-change="pageIndexChange"-->
<!-- @page-size-change="pageSizeChange"-->
<!-- />-->
</div>
</div>
<div class="malware-detail-card mb-[16px]">
<p class="title">包含恶意代码的文件列表</p>
<div class="content">
<d-table
:data="malwareDetails?.source"
class="w-full bg-white malware-list-table"
:fix-header="malwareDetails?.source.length > 0"
max-height="300px"
>
<d-column
field="sink"
header="恶意汇"
/>
<d-column
field="flow"
header="传播流向"
/>
<d-column
field="purpose"
header="代码目的"
/>
<d-column
field="analysis"
header="LLM对于这个代码片段的分析"
/>
<d-column
field="createTime"
header="创建时间"
/>
<template #empty>
<NoData style="padding: 0; margin: -30px 0;" :width="80" />
</template>
</d-table>
<!-- <d-pagination-->
<!-- class="flex justify-end mt-[20px]"-->
<!-- :total="pager.total"-->
<!-- v-model:pageSize="pager.pageSize"-->
<!-- v-model:pageIndex="pager.pageIndex"-->
<!-- :can-view-total="true"-->
<!-- :can-change-page-size="true"-->
<!-- :max-items="5"-->
<!-- @page-index-change="pageIndexChange"-->
<!-- @page-size-change="pageSizeChange"-->
<!-- />-->
</div>
</div>
<!-- <div class="malware-detail-card flex-1 h-0">-->
<!-- <p class="title">关键日志</p>-->
<!-- <div class="content flex flex-col flex-1 h-0">-->
<!-- <div class="log-box flex-1 overflow-y-scroll">-->
<!-- {{ malwareDetails?.analysis }}-->
<!-- {{ malwareDetails?.code }}-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
</div>
</div>
</d-drawer>
</template>
<script setup lang="ts">
import { reactive, shallowRef, defineProps, ref, defineExpose } from 'vue';
import NoData from "@/components/NoData/NoData.vue";
import { getMalwareDetailApi, getMalwareDetailApi2 } from '@/api/inboundTask/inboundTaskApi';
import {Message} from "vue-devui/message";
import { type MalwareDetailType } from "@/api/inboundTask/inboundTaskType";
const props = defineProps({
detailRow: {
type: Object,
required: true,
},
});
const detailDrawerCtrl = ref(false);
const malwareDetails = shallowRef<MalwareDetailType | null>(null);
const threatContentList = shallowRef([]);
const pager = reactive({
pageIndex: 1,
pageSize: 10,
total: 0,
});
async function getMalwareDetails(id) {
try {
const res = await getMalwareDetailApi2({
current: 1,
size: 10,
maliciousId: id
});
if(res.data.records&&res.data.records.length>0) {
malwareDetails.value = res.data.records[0];
}
} catch (e) {
Message.error('接口错误,查询详情失败');
}
getCurrentPageData();
}
function getCurrentPageData() {
// pager.total = malwareDetails.value.contentInfo.total;
// const endIndex = pager.pageIndex * pager.pageSize;
// threatContentList.value = malwareDetails.value.contentInfo.list.slice(endIndex - pager.pageSize, endIndex);
}
function pageIndexChange() {
getCurrentPageData();
}
function pageSizeChange() {
pager.pageIndex = 1;
getCurrentPageData();
}
function handleCopy(text: string) {
inputCopy(text);
}
const openDetail = (id) => {
detailDrawerCtrl.value = true;
getMalwareDetails(id)
}
function inputCopy(text: string) {
const aux = document.createElement('input');
aux.setAttribute('value', text);
document.body.appendChild(aux);
aux.select();
document.execCommand('copy');
document.body.removeChild(aux);
Message({ type: 'success', message: '复制成功' });
}
// 暴露方法,供父组件调用
defineExpose({
openDetail,
});
</script>
<style scoped lang="scss">
.malware-detail-card {
border-radius: 8px;
border: 1px solid #EEF0F5;
display: flex;
flex-direction: column;
.title {
color: #191919;
padding: 10px 16px;
font-size: 14px;
line-height: 22px;
background-color: #F3F3F3;
}
.content {
padding: 20px 16px;
:deep(.devui-table__thead .header-container) {
background-color: #F3F3F3;
}
}
}
.log-box {
border-radius: 8px;
background: #F7F8FA;
border: 1px solid #EEF0F5;
padding: 8px 16px;
}
.single-line-ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@@ -0,0 +1,264 @@
<template>
<div>
<div>
<d-search
v-model="searchText"
icon-position="left"
class="w-[400px] mb-[12px]"
placeholder="请输入搜索内容"
@search="getMalwareList"
/>
</div>
<d-table
:data="tableData"
:show-loading="searchLoading"
class="jyh-table"
:fix-header="tableData.length > 0"
max-height="600px"
@sort-change="sortChange"
>
<d-column type="index" width="48px" />
<!-- <d-column field="packageFullName" width="220px">-->
<!-- <template #header>-->
<!-- 文件名称-->
<!-- <TableFilter field="packageFullName" :condition="filterData.packageFullName" @filter-change="filterChange" />-->
<!-- </template>-->
<!-- <template #default="{ row }">-->
<!-- <a class="devui-link" @click="openMalwareDetail(row)">{{ row.packageFullName }}</a>-->
<!-- </template>-->
<!-- </d-column>-->
<!-- <d-column field="category">-->
<!-- <template #header>-->
<!-- 恶意类别-->
<!-- <TableFilter-->
<!-- type="radio"-->
<!-- field="category"-->
<!-- :condition="filterData.category"-->
<!-- :filter-list="categoryFilterOptions"-->
<!-- @filter-change="filterChange"-->
<!-- />-->
<!-- </template>-->
<!-- </d-column>-->
<d-column field="packageName" header="文件名称" width="180">
<template #default="{ row }">
<a class="devui-link" @click="openMalwareDetail(row)">{{ row.packageFullName }}</a>
</template>
</d-column>
<d-column field="packagePath" header="文件路径" width="180" show-overflow-tooltip/>
<d-column field="packageVersion" header="文件版本号" show-overflow-tooltip/>
<d-column field="llmName" header="LLM名称" show-overflow-tooltip/>
<d-column field="totalApisCalledCount" header="敏感API总数" show-overflow-tooltip/>
<d-column field="suspiciousFilesCount" header="可疑文件总数" show-overflow-tooltip/>
<d-column field="isMalicious" header="是否恶意代码">
<template #default="scope">
<span class="card-val" v-if="scope.row.isMalicious===0">
<d-tag color="#5e7ce0" size="sm">否</d-tag>
</span>
<span class="card-val" v-else>
<d-tag color="#c7000b" size="sm">是</d-tag>
</span>
</template>
</d-column>
<d-column field="isModelResponded" header="大模型是否正常回复" width="180">
<template #default="scope">
<span class="card-val" v-if="scope.row.isModelResponded===0">
<d-tag color="#5e7ce0" size="sm">否</d-tag>
</span>
<span class="card-val" v-else>
<d-tag color="#c7000b" size="sm">是</d-tag>
</span>
</template>
</d-column>
<d-column field="createTime" header="检测时间" width="180" show-overflow-tooltip/>
<!-- <d-column field="threatLevel">-->
<!-- <template #header>-->
<!-- 威胁级别-->
<!-- <TableFilter-->
<!-- type="radio"-->
<!-- field="threatLevel"-->
<!-- :condition="filterData.threatLevel"-->
<!-- :filter-list="threatLevelFilterList"-->
<!-- @filter-change="filterChange"-->
<!-- />-->
<!-- </template>-->
<!-- <template #default="{ row }">-->
<!-- <d-status :type="ThreatLevelMap[row.threatLevel].statusType">-->
<!-- {{ ThreatLevelMap[row.threatLevel].label }}-->
<!-- </d-status>-->
<!-- </template>-->
<!-- </d-column>-->
<!-- <d-column field="confidenceCoefficient">-->
<!-- <template #header>-->
<!-- 置信度-->
<!-- <TableFilter-->
<!-- type="radio"-->
<!-- field="confidenceCoefficient"-->
<!-- :condition="filterData.confidenceCoefficient"-->
<!-- :filter-list="confidenceCoefficientFilterList"-->
<!-- @filter-change="filterChange"-->
<!-- />-->
<!-- </template>-->
<!-- <template #default="{ row }">-->
<!-- <d-tag size="sm" :type="ConfidenceCoefficientMap[row.confidenceCoefficient].statusType">-->
<!-- {{ ConfidenceCoefficientMap[row.confidenceCoefficient].label }}-->
<!-- </d-tag>-->
<!-- </template>-->
<!-- </d-column>-->
<!-- <d-column field="testResult" header="检测结果" />-->
<template #empty>
<NoData v-if="!searchLoading" />
</template>
</d-table>
<d-pagination
class="flex justify-end mt-[20px]"
:total="pager.total"
v-model:pageSize="pager.pageSize"
v-model:pageIndex="pager.pageIndex"
:can-view-total="true"
:can-change-page-size="true"
:max-items="5"
@page-index-change="pageIndexChange"
@page-size-change="pageSizeChange"
/>
</div>
<Detail ref="MalwareDetailRef" :detailRow="detailRow" />
</template>
<script lang="ts" setup>
import { reactive, ref, shallowRef } from 'vue';
import TableFilter from '@/components/TableFilter/TableFilter.vue';
import NoData from '@/components/NoData/NoData.vue';
import Detail from './Detail.vue';
import { getMalwareListApi } from '@/api/inboundTask/inboundTaskApi';
import { type MalwareListItemType } from '@/api/inboundTask/inboundTaskType';
import { Message } from 'vue-devui/message';
import { useRoute } from 'vue-router';
enum ThreatLevelEnum {
HIGH = 'high',
MD = 'medium',
LOW = 'low'
}
enum ConfidenceCoefficientEnum {
HIGH = 'high',
MD = 'medium',
LOW = 'low'
}
const route = useRoute();
const versionId = route.params.versionId;
const searchText = ref('');
const searchLoading = ref(false);
const MalwareDetailRef = ref<InstanceType<typeof MalwareDetail> | null>(null)
const tableData = shallowRef<MalwareListItemType[]>([]);
const pager = reactive({
pageSize: 10,
pageIndex: 1,
total: 0
});
const ThreatLevelMap = {
[ThreatLevelEnum.HIGH]: { label: '高危', statusType: 'error' },
[ThreatLevelEnum.MD]: { label: '中危', statusType: 'warning' },
[ThreatLevelEnum.LOW]: { label: '低危', statusType: 'waiting' }
};
const ConfidenceCoefficientMap = {
[ConfidenceCoefficientEnum.HIGH]: { label: '高', statusType: 'success' },
[ConfidenceCoefficientEnum.MD]: { label: '中', statusType: 'warning' },
[ConfidenceCoefficientEnum.LOW]: { label: '低', statusType: 'danger' }
};
const confidenceCoefficientFilterList = [
{ label: '高', value: ConfidenceCoefficientEnum.HIGH },
{ label: '中', value: ConfidenceCoefficientEnum.MD },
{ label: '低', value: ConfidenceCoefficientEnum.LOW }
];
const threatLevelFilterList = [
{ label: '高危', value: ThreatLevelEnum.HIGH },
{ label: '中危', value: ThreatLevelEnum.MD },
{ label: '低危', value: ThreatLevelEnum.LOW }
];
const categoryFilterOptions = [{ label: 'Python', value: 'Python' }];
const filterData = reactive({
packageFullName: '',
threatLevel: '',
confidenceCoefficient: '',
category: ''
});
const detailRow = shallowRef<MalwareListItemType | null>(null);
init();
function init() {
getMalwareList();
}
async function getMalwareList() {
const params = {
current: pager.pageIndex,
size: pager.pageSize,
taskId: '',
softwareId: versionId
};
pager.total = 0;
tableData.value = [];
searchLoading.value = true;
try {
const res = await getMalwareListApi(params);
if (res.data?.code === 200) {
tableData.value = res.data.data.records.map((item) => {
return { ...item, threatLevel: ThreatLevelEnum.HIGH, confidenceCoefficient: ConfidenceCoefficientEnum.HIGH };
});
pager.total = res.data.data.total;
} else {
Message.error(res.data.msg || '查询恶意代码列表失败');
}
} catch (e) {
Message.error('接口错误,查询恶意代码列表失败');
}
searchLoading.value = false;
}
function sortChange(sortInfo: { field: string; direction: 'ASC' | 'DESC' | '' }) {
console.log(sortInfo);
getMalwareList();
}
function filterChange(filterInfo: { field: string; value: any }) {
filterData[filterInfo.field] = filterInfo.value;
getMalwareList();
}
function pageSizeChange() {
pager.pageIndex = 1;
getMalwareList();
}
function pageIndexChange() {
getMalwareList();
}
function openMalwareDetail(row: MalwareListItemType) {
detailRow.value = row;
// 添加检查
if (MalwareDetailRef.value && typeof MalwareDetailRef.value.openDetail === 'function') {
MalwareDetailRef.value.openDetail(row.id)
} else {
console.error('MalwareDetailRef 或 openDetail 方法不可用')
}
}
</script>
<style scoped lang="scss">
.malware-list-table {
:deep(.devui-table__row td) {
padding-top: 8px;
padding-bottom: 8px;
}
:deep(.devui-status.devui-status__bg--warning:before) {
background-color: #fcc800;
}
}
</style>

View File

@@ -0,0 +1,271 @@
<template>
<div class="info w-full">
<div class="flex items-center justify-start w-full gap-1">
<img src="@/assets/imgs/jyh/infoIcon.png" />
<span class="text-base font-medium">治理标准:</span>
</div>
<div>
<ul>
<li>
1在依赖冲突或者解决依赖软件漏洞的场景下产品引入的依赖组件与社区的依赖关系不一致从而通过产品自行录入由使用方自行维护可通过产品快速补包流程维护<a
class="devui-link"
>操作指导</a
>该部分的依赖组件漏洞由产品自行解决
</li>
<li>2点击下面组件编码跳转到组件详情页面可直接看到基于当前火车过滤的组件依赖路径</li>
</ul>
</div>
</div>
<div class="flex items-center justify-end gap-3 mt-3">
<d-button variant="text" icon="export">导出</d-button>
<div class="vertical-line"></div>
<d-button variant="text" @click="openAll(baseTreeTableData1)">
<div v-if="!isAllExpand" class="flex items-center justify-start gap-1">
<img style="width: 14px; height: 14px" src="@/assets/imgs/jyh/openAll.png" />
全部展开
</div>
<div v-else class="flex items-center justify-start gap-1">
<img style="width: 14px; height: 14px" src="@/assets/imgs/jyh/closeAll.png" />
全部关闭
</div>
</d-button>
</div>
<Card simple class="jyh-card mt-3">
<div class="clear-padding">
<d-table ref="table" :data="baseTreeTableData1">
<d-column width="20px" type="index"> </d-column>
<d-column field="name" header="软件版本名称" show-overflow-tooltip></d-column>
<d-column field="groupid" header="groupid"></d-column>
<d-column field="code" header="软件编号">
<template #default="scope">
<a class="devui-link" @click="showDetail = true">{{ scope.row.code }}</a>
</template>
</d-column>
<d-column field="packageName" header="packageName"></d-column>
<d-column field="version" header="version"></d-column>
<d-column field="language" header="编程语言"></d-column>
<d-column field="number" header="软件漏洞数量">
<template #default="scope">
<a class="devui-link" @click="showDetail = true">{{ scope.row.number }}</a>
</template>
</d-column>
</d-table>
</div>
</Card>
<div class="w-full flex justify-end my-4">
<d-pagination
size="sm"
:total="pager.total"
v-model:pageSize="pager.pageSize"
v-model:pageIndex="pager.pageIndex"
:can-view-total="true"
:can-change-page-size="true"
:can-jump-page="true"
:max-items="5"
/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { getDepencyList } from '@/api/jyh';
const checked = ref(false);
const showDetail = ref(false);
const table = ref(null);
const isAllExpand = ref(false);
const pager = ref({
pageIndex: 1,
pageSize: 30,
total: 10
});
const filterList = ref([
{ name: '001', value: '001' },
{ name: '002', value: '002' },
{ name: '003', value: '003' },
{ name: '004', value: '004' }
]);
const handleFilterChange = (val) => {
if (!val.length) {
baseTreeTableData1.value = [...originSource.value];
return;
}
const filterList = val.map((item) => item.value);
baseTreeTableData1.value = originSource.value.filter((item) => filterList.includes(item.code));
};
const originSource = ref([
{
id: '1',
name: 'ssl',
groupid: '2000000',
code: '001',
packageName: '包名称',
version: '202306',
language: 'java',
number: '20',
children: [
{
id: '2',
name: 'ssl',
groupid: '2000000',
code: '001',
packageName: '包名称',
version: '202306',
language: 'java',
number: '20',
children: [
{
id: '3',
name: 'ssl',
groupid: '2000000',
code: '001',
packageName: '包名称',
version: '202306',
language: 'java',
number: '20'
},
{
id: '4',
name: 'ssl',
groupid: '2000000',
code: '001',
packageName: '包名称',
version: '202306',
language: 'java',
number: '20'
}
]
}
]
},
{
id: '5',
name: 'ssl',
groupid: '2000000',
code: '002',
packageName: '包名称',
version: '202306',
language: 'java',
number: '10',
children: [
{
id: '6',
name: 'ssl',
groupid: '2000000',
code: '003',
packageName: '包名称',
version: '202306',
language: 'java',
number: '20',
children: [
{
id: '7',
name: 'ssl',
groupid: '2000000',
code: '003',
packageName: '包名称',
version: '202306',
language: 'java',
number: '11'
},
{
id: '8',
name: 'ssl',
groupid: '2000000',
code: '004',
packageName: '包名称',
version: '202306',
language: 'java',
number: '30'
}
]
}
]
},
{
id: '9',
name: 'ssl',
groupid: '2000000',
code: '001',
packageName: '包名称',
version: '202306',
language: 'java',
number: '5'
}
]);
const baseTreeTableData1 = ref([...originSource.value]);
const packageFilterList = ref(
originSource.value.map((item) => {
return { name: item.packageName, value: item.packageName };
})
);
const versionFilterList = ref(
originSource.value.map((item) => {
return { name: item.version, value: item.version };
})
);
const languageFilterList = ref(
originSource.value.map((item) => {
return { name: item.language, value: item.language };
})
);
const openAll = (tree) => {
if (!tree || !tree.length) {
return;
}
tree.forEach((node) => {
table.value.store.toggleRowExpansion(node);
if (node.children) {
openAll(node.children);
}
});
isAllExpand.value = !isAllExpand.value;
};
const sortNumberMethod = (a, b) => {
return a.number > b.number;
};
const getList = async () => {
const res = await getDepencyList('APACHE-001');
};
getList();
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.clear-padding {
margin: -16px -20px;
}
:deep(.devui-table__cell) {
display: flex;
align-items: center;
.devui-table__tree-operate {
transform: translateY(-3px);
}
}
.info {
padding: 10px 20px;
border-radius: 4px;
background: #e1e8fd;
img {
width: 14px;
height: 14px;
}
ul {
padding-left: 18px;
line-height: 22px;
}
}
.vertical-line {
width: 1px;
height: 20px;
background-color: #dcdcdc; /* 设置竖线颜色 */
margin: 0 8px; /* 设置竖线与按钮的间距 */
}
</style>

View File

@@ -0,0 +1,151 @@
<template>
<div class="license-info-container">
<div class="license-info-content">
<d-layout>
<d-content class="dcontent">
<d-table class="jyh-table" v-if="list.length > 0" :data="list" style="width: 100%">
<template #empty>
<NoData></NoData>
</template>
<d-column type="index" width="40"></d-column>
<d-column v-for="(column, index) in tableConfig" :field="column.field" :header="column.header"></d-column>
<d-column header="详情" width="100px">
<template #default="scope">
<a class="devui-link" @click="openPanel(scope.row)">查看</a>
</template>
</d-column>
</d-table>
<NoData v-else :small="false"></NoData>
</d-content>
</d-layout>
</div>
<div class="license-info-container-page" v-if="list.length > 0">
<d-pagination
size="sm"
:total="pager.total"
v-model:pageSize="pager.pageSize"
v-model:pageIndex="pager.pageIndex"
:can-view-total="true"
:can-change-page-size="true"
:can-jump-page="true"
:max-items="5"
@page-index-change="getList"
@page-size-change="getList"
/>
</div>
</div>
<d-drawer v-model="showDetail" style="width: 800px; padding: 20px">
<SidePanel :data="currentData" @close="close" />
</d-drawer>
</template>
<script setup lang="ts">
import { defineProps, onMounted, ref } from 'vue';
import SidePanel from '@/components/LicenseInfo/SidePanel.vue';
import { getLicenseList } from '@/api/jyh';
import NoData from '@/components/NoData/NoData.vue';
const currentData = ref(null);
const propsData = defineProps(['versionId']);
const pager = ref({
pageIndex: 1,
pageSize: 10,
total: 0
});
const pageOptions = ref({});
const showDetail = ref(false);
const close = () => {
showDetail.value = false;
};
const openPanel = (row) => {
currentData.value = row;
showDetail.value = true;
};
const tableConfig = ref([
{
field: 'licenseName',
header: '许可证名称',
sortable: true,
sortMethod: null
},
{
field: 'category',
header: '许可证种类',
sortable: false,
sortMethod: null
}
// {
// field: 'softwareName',
// header: '影响软件',
// sortable: true,
// sortMethod: null
// },
// {
// field: 'softwareVersion',
// header: '版本',
// sortable: true,
// sortMethod: null
// }
]);
const list = ref([]);
const getList = async () => {
const res: any = await getLicenseList({
softwareId: propsData.versionId,
componentId: '',
current: pager.value.pageIndex,
size: pager.value.pageSize
});
if (!res.error) {
console.log(res);
list.value = res.data.data.records;
pager.value.total = res.data.data.total;
} else {
list.value = [];
}
};
onMounted(() => {
getList();
});
</script>
<style scoped lang="scss">
.license-info-container {
display: flex;
gap: 12px;
flex-direction: column;
width: 100%;
.license-info-content {
background-color: $devui-global-bg-normal;
border-radius: 10px;
}
&-page {
width: 100%;
display: flex;
justify-content: end;
}
}
.dheader {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
padding: 12px;
}
.dfooter {
display: flex;
justify-content: end;
margin-top: 20px;
}
.operation {
cursor: pointer;
width: 14px;
height: 14px;
}
</style>

View File

@@ -0,0 +1,264 @@
<template>
<div>
<div>
<d-search
v-model="searchText"
icon-position="left"
class="w-[400px] mb-[12px]"
placeholder="请输入搜索内容"
@search="getMalwareList"
/>
</div>
<d-table
:data="tableData"
:show-loading="searchLoading"
class="jyh-table"
:fix-header="tableData.length > 0"
max-height="600px"
@sort-change="sortChange"
>
<d-column type="index" width="48px" />
<!-- <d-column field="packageFullName" width="220px">-->
<!-- <template #header>-->
<!-- 文件名称-->
<!-- <TableFilter field="packageFullName" :condition="filterData.packageFullName" @filter-change="filterChange" />-->
<!-- </template>-->
<!-- <template #default="{ row }">-->
<!-- <a class="devui-link" @click="openMalwareDetail(row)">{{ row.packageFullName }}</a>-->
<!-- </template>-->
<!-- </d-column>-->
<!-- <d-column field="category">-->
<!-- <template #header>-->
<!-- 恶意类别-->
<!-- <TableFilter-->
<!-- type="radio"-->
<!-- field="category"-->
<!-- :condition="filterData.category"-->
<!-- :filter-list="categoryFilterOptions"-->
<!-- @filter-change="filterChange"-->
<!-- />-->
<!-- </template>-->
<!-- </d-column>-->
<d-column field="packageName" header="文件名称" width="180">
<template #default="{ row }">
<a class="devui-link" @click="openMalwareDetail(row)">{{ row.packageFullName }}</a>
</template>
</d-column>
<d-column field="packagePath" header="文件路径" width="180" show-overflow-tooltip/>
<d-column field="packageVersion" header="文件版本号" show-overflow-tooltip/>
<d-column field="llmName" header="LLM名称" show-overflow-tooltip/>
<d-column field="totalApisCalledCount" header="敏感API总数" show-overflow-tooltip/>
<d-column field="suspiciousFilesCount" header="可疑文件总数" show-overflow-tooltip/>
<d-column field="isMalicious" header="是否恶意代码">
<template #default="scope">
<span class="card-val" v-if="scope.row.isMalicious===0">
<d-tag color="#5e7ce0" size="sm">否</d-tag>
</span>
<span class="card-val" v-else>
<d-tag color="#c7000b" size="sm">是</d-tag>
</span>
</template>
</d-column>
<d-column field="isModelResponded" header="大模型是否正常回复" width="180">
<template #default="scope">
<span class="card-val" v-if="scope.row.isModelResponded===0">
<d-tag color="#5e7ce0" size="sm">否</d-tag>
</span>
<span class="card-val" v-else>
<d-tag color="#c7000b" size="sm">是</d-tag>
</span>
</template>
</d-column>
<d-column field="createTime" header="检测时间" width="180" show-overflow-tooltip/>
<!-- <d-column field="threatLevel">-->
<!-- <template #header>-->
<!-- 威胁级别-->
<!-- <TableFilter-->
<!-- type="radio"-->
<!-- field="threatLevel"-->
<!-- :condition="filterData.threatLevel"-->
<!-- :filter-list="threatLevelFilterList"-->
<!-- @filter-change="filterChange"-->
<!-- />-->
<!-- </template>-->
<!-- <template #default="{ row }">-->
<!-- <d-status :type="ThreatLevelMap[row.threatLevel].statusType">-->
<!-- {{ ThreatLevelMap[row.threatLevel].label }}-->
<!-- </d-status>-->
<!-- </template>-->
<!-- </d-column>-->
<!-- <d-column field="confidenceCoefficient">-->
<!-- <template #header>-->
<!-- 置信度-->
<!-- <TableFilter-->
<!-- type="radio"-->
<!-- field="confidenceCoefficient"-->
<!-- :condition="filterData.confidenceCoefficient"-->
<!-- :filter-list="confidenceCoefficientFilterList"-->
<!-- @filter-change="filterChange"-->
<!-- />-->
<!-- </template>-->
<!-- <template #default="{ row }">-->
<!-- <d-tag size="sm" :type="ConfidenceCoefficientMap[row.confidenceCoefficient].statusType">-->
<!-- {{ ConfidenceCoefficientMap[row.confidenceCoefficient].label }}-->
<!-- </d-tag>-->
<!-- </template>-->
<!-- </d-column>-->
<!-- <d-column field="testResult" header="检测结果" />-->
<template #empty>
<NoData v-if="!searchLoading" />
</template>
</d-table>
<d-pagination
class="flex justify-end mt-[20px]"
:total="pager.total"
v-model:pageSize="pager.pageSize"
v-model:pageIndex="pager.pageIndex"
:can-view-total="true"
:can-change-page-size="true"
:max-items="5"
@page-index-change="pageIndexChange"
@page-size-change="pageSizeChange"
/>
</div>
<MalwareDetail ref="MalwareDetailRef" :detailRow="detailRow" />
</template>
<script lang="ts" setup>
import { reactive, ref, shallowRef } from 'vue';
import TableFilter from '@/components/TableFilter/TableFilter.vue';
import NoData from '@/components/NoData/NoData.vue';
import MalwareDetail from '@/views/Jyh/Version/Detail/Malware/MalwareDetail.vue';
import { getMalwareListApi } from '@/api/inboundTask/inboundTaskApi';
import { type MalwareListItemType } from '@/api/inboundTask/inboundTaskType';
import { Message } from 'vue-devui/message';
import { useRoute } from 'vue-router';
enum ThreatLevelEnum {
HIGH = 'high',
MD = 'medium',
LOW = 'low'
}
enum ConfidenceCoefficientEnum {
HIGH = 'high',
MD = 'medium',
LOW = 'low'
}
const route = useRoute();
const versionId = route.params.versionId;
const searchText = ref('');
const searchLoading = ref(false);
const MalwareDetailRef = ref<InstanceType<typeof MalwareDetail> | null>(null)
const tableData = shallowRef<MalwareListItemType[]>([]);
const pager = reactive({
pageSize: 10,
pageIndex: 1,
total: 0
});
const ThreatLevelMap = {
[ThreatLevelEnum.HIGH]: { label: '高危', statusType: 'error' },
[ThreatLevelEnum.MD]: { label: '中危', statusType: 'warning' },
[ThreatLevelEnum.LOW]: { label: '低危', statusType: 'waiting' }
};
const ConfidenceCoefficientMap = {
[ConfidenceCoefficientEnum.HIGH]: { label: '高', statusType: 'success' },
[ConfidenceCoefficientEnum.MD]: { label: '中', statusType: 'warning' },
[ConfidenceCoefficientEnum.LOW]: { label: '低', statusType: 'danger' }
};
const confidenceCoefficientFilterList = [
{ label: '高', value: ConfidenceCoefficientEnum.HIGH },
{ label: '中', value: ConfidenceCoefficientEnum.MD },
{ label: '低', value: ConfidenceCoefficientEnum.LOW }
];
const threatLevelFilterList = [
{ label: '高危', value: ThreatLevelEnum.HIGH },
{ label: '中危', value: ThreatLevelEnum.MD },
{ label: '低危', value: ThreatLevelEnum.LOW }
];
const categoryFilterOptions = [{ label: 'Python', value: 'Python' }];
const filterData = reactive({
packageFullName: '',
threatLevel: '',
confidenceCoefficient: '',
category: ''
});
const detailRow = shallowRef<MalwareListItemType | null>(null);
init();
function init() {
getMalwareList();
}
async function getMalwareList() {
const params = {
current: pager.pageIndex,
size: pager.pageSize,
taskId: '',
softwareId: versionId
};
pager.total = 0;
tableData.value = [];
searchLoading.value = true;
try {
const res = await getMalwareListApi(params);
if (res.data?.code === 200) {
tableData.value = res.data.data.records.map((item) => {
return { ...item, threatLevel: ThreatLevelEnum.HIGH, confidenceCoefficient: ConfidenceCoefficientEnum.HIGH };
});
pager.total = res.data.data.total;
} else {
Message.error(res.data.msg || '查询恶意代码列表失败');
}
} catch (e) {
Message.error('接口错误,查询恶意代码列表失败');
}
searchLoading.value = false;
}
function sortChange(sortInfo: { field: string; direction: 'ASC' | 'DESC' | '' }) {
console.log(sortInfo);
getMalwareList();
}
function filterChange(filterInfo: { field: string; value: any }) {
filterData[filterInfo.field] = filterInfo.value;
getMalwareList();
}
function pageSizeChange() {
pager.pageIndex = 1;
getMalwareList();
}
function pageIndexChange() {
getMalwareList();
}
function openMalwareDetail(row: MalwareListItemType) {
detailRow.value = row;
// 添加检查
if (MalwareDetailRef.value && typeof MalwareDetailRef.value.openDetail === 'function') {
MalwareDetailRef.value.openDetail(row.id)
} else {
console.error('MalwareDetailRef 或 openDetail 方法不可用')
}
}
</script>
<style scoped lang="scss">
.malware-list-table {
:deep(.devui-table__row td) {
padding-top: 8px;
padding-bottom: 8px;
}
:deep(.devui-status.devui-status__bg--warning:before) {
background-color: #fcc800;
}
}
</style>

View File

@@ -0,0 +1,286 @@
<template>
<d-drawer v-model="detailDrawerCtrl" class="w-[800px] p-[20px]">
<div class="flex flex-col h-full">
<div class="flex justify-between items-center mb-[12px]">
<p class="text-[18px]/[26px] ">恶意代码详情</p>
<d-icon @click="detailDrawerCtrl = false" name="icon-close" size="18px" class="cursor-pointer text-[#71757F]" />
</div>
<div class="flex flex-col mb-[12px] flex-1 h-0">
<div class="malware-detail-card mb-[16px]">
<p class="title">文件基本信息</p>
<div class="content">
<div class="flex items-center">
<div class="w-[160px] pr-[20px] flex-shrink-0 text-[#595959]">文件名称</div>
<div class="single-line-ellipsis">{{ props.detailRow?.packageFullName || '--' }}</div>
<d-icon name="icon-copy" class="ml-[8px] cursor-pointer" @click="handleCopy(props.detailRow?.packageFullName)" />
</div>
<div class="flex items-center">
<div class="w-[160px] pr-[20px] flex-shrink-0 text-[#595959]">文件路径</div>
<div class="single-line-ellipsis">{{ props.detailRow?.packagePath || '--' }}</div>
<d-icon name="icon-copy" class="ml-[8px] cursor-pointer" @click="handleCopy(props.detailRow?.packagePath)" />
</div>
<div class="flex items-center">
<div class="w-[160px] pr-[20px] flex-shrink-0 text-[#595959]">文件版本号</div>
<div class="single-line-ellipsis">{{ props.detailRow?.packageVersion || '--' }}</div>
<d-icon name="icon-copy" class="ml-[8px] cursor-pointer" @click="handleCopy(props.detailRow?.packageVersion)" />
</div>
<div class="flex items-center">
<div class="w-[160px] pr-[20px] flex-shrink-0 text-[#595959]">LLM名称</div>
<div class="single-line-ellipsis">{{ props.detailRow?.llmName || '--' }}</div>
<d-icon name="icon-copy" class="ml-[8px] cursor-pointer" @click="handleCopy(props.detailRow?.llmName)" />
</div>
<div class="flex items-center">
<div class="w-[160px] pr-[20px] flex-shrink-0 text-[#595959]">敏感API总数</div>
<div class="single-line-ellipsis">{{ props.detailRow?.totalApisCalledCount }}</div>
</div>
<div class="flex items-center">
<div class="w-[160px] pr-[20px] flex-shrink-0 text-[#595959]">可疑文件总数</div>
<div class="single-line-ellipsis">{{ props.detailRow?.suspiciousFilesCount }}</div>
</div>
<div class="flex items-center">
<div class="w-[160px] pr-[20px] flex-shrink-0 text-[#595959]">是否恶意代码</div>
<div class="single-line-ellipsis">
<span class="card-val" v-if="props.detailRow?.isMalicious===0">
<d-tag color="#5e7ce0" size="sm"></d-tag>
</span>
<span class="card-val" v-else>
<d-tag color="#c7000b" size="sm"></d-tag>
</span>
</div>
</div>
<div class="flex items-center">
<div class="w-[160px] pr-[20px] flex-shrink-0 text-[#595959]">大模型是否正常回复</div>
<div class="single-line-ellipsis">
<span class="card-val" v-if="props.detailRow?.isModelResponded===0">
<d-tag color="#5e7ce0" size="sm"></d-tag>
</span>
<span class="card-val" v-else>
<d-tag color="#c7000b" size="sm"></d-tag>
</span>
</div>
</div>
<!-- <div class="flex items-center">-->
<!-- <div class="w-[160px] pr-[20px] flex-shrink-0 text-[#595959]">文件sha256</div>-->
<!-- <div :title="malwareDetails?.code" class="single-line-ellipsis">{{ malwareDetails?.code }}</div>-->
<!-- <d-icon name="icon-copy" class="ml-[8px] cursor-pointer" @click="handleCopy(malwareDetails?.code)" />-->
<!-- </div>-->
</div>
</div>
<div class="malware-detail-card mb-[16px]">
<p class="title">调用的敏感API列表</p>
<div class="content">
<d-table
:data="malwareDetails?.apisCalled"
class="w-full bg-white malware-list-table"
:fix-header="malwareDetails?.apisCalled.length > 0"
max-height="300px"
>
<d-column field="isMalicious" header="是否恶意代码">
<template #default="scope">
<span class="card-val" v-if="scope.row.isMalicious===0">
<d-tag color="#5e7ce0" size="sm">否</d-tag>
</span>
<span class="card-val" v-else>
<d-tag color="#c7000b" size="sm">是</d-tag>
</span>
</template>
</d-column>
<d-column
field="maliciousCodeCategory"
header="恶意代码分类"
/>
<template #empty>
<NoData style="padding: 0; margin: -30px 0;" :width="80" />
</template>
</d-table>
<!-- <d-pagination-->
<!-- class="flex justify-end mt-[20px]"-->
<!-- :total="pager.total"-->
<!-- v-model:pageSize="pager.pageSize"-->
<!-- v-model:pageIndex="pager.pageIndex"-->
<!-- :can-view-total="true"-->
<!-- :can-change-page-size="true"-->
<!-- :max-items="5"-->
<!-- @page-index-change="pageIndexChange"-->
<!-- @page-size-change="pageSizeChange"-->
<!-- />-->
</div>
</div>
<div class="malware-detail-card mb-[16px]">
<p class="title">包含恶意代码的文件列表</p>
<div class="content">
<d-table
:data="malwareDetails?.source"
class="w-full bg-white malware-list-table"
:fix-header="malwareDetails?.source.length > 0"
max-height="300px"
>
<d-column
field="sink"
header="恶意汇"
/>
<d-column
field="flow"
header="传播流向"
/>
<d-column
field="purpose"
header="代码目的"
/>
<d-column
field="analysis"
header="LLM对于这个代码片段的分析"
/>
<d-column
field="createTime"
header="创建时间"
/>
<template #empty>
<NoData style="padding: 0; margin: -30px 0;" :width="80" />
</template>
</d-table>
<!-- <d-pagination-->
<!-- class="flex justify-end mt-[20px]"-->
<!-- :total="pager.total"-->
<!-- v-model:pageSize="pager.pageSize"-->
<!-- v-model:pageIndex="pager.pageIndex"-->
<!-- :can-view-total="true"-->
<!-- :can-change-page-size="true"-->
<!-- :max-items="5"-->
<!-- @page-index-change="pageIndexChange"-->
<!-- @page-size-change="pageSizeChange"-->
<!-- />-->
</div>
</div>
<!-- <div class="malware-detail-card flex-1 h-0">-->
<!-- <p class="title">关键日志</p>-->
<!-- <div class="content flex flex-col flex-1 h-0">-->
<!-- <div class="log-box flex-1 overflow-y-scroll">-->
<!-- {{ malwareDetails?.analysis }}-->
<!-- {{ malwareDetails?.code }}-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
</div>
</div>
</d-drawer>
</template>
<script setup lang="ts">
import { reactive, shallowRef, defineProps, ref, defineExpose } from 'vue';
import NoData from "@/components/NoData/NoData.vue";
import { getMalwareDetailApi, getMalwareDetailApi2 } from '@/api/inboundTask/inboundTaskApi';
import {Message} from "vue-devui/message";
import { type MalwareDetailType } from "@/api/inboundTask/inboundTaskType";
const props = defineProps({
detailRow: {
type: Object,
required: true,
},
});
const detailDrawerCtrl = ref(false);
const malwareDetails = shallowRef<MalwareDetailType | null>(null);
const threatContentList = shallowRef([]);
const pager = reactive({
pageIndex: 1,
pageSize: 10,
total: 0,
});
async function getMalwareDetails(id) {
try {
const res = await getMalwareDetailApi2({
current: 1,
size: 10,
maliciousId: id
});
if(res.data.records&&res.data.records.length>0) {
malwareDetails.value = res.data.records[0];
}
} catch (e) {
Message.error('接口错误,查询详情失败');
}
getCurrentPageData();
}
function getCurrentPageData() {
// pager.total = malwareDetails.value.contentInfo.total;
// const endIndex = pager.pageIndex * pager.pageSize;
// threatContentList.value = malwareDetails.value.contentInfo.list.slice(endIndex - pager.pageSize, endIndex);
}
function pageIndexChange() {
getCurrentPageData();
}
function pageSizeChange() {
pager.pageIndex = 1;
getCurrentPageData();
}
function handleCopy(text: string) {
inputCopy(text);
}
const openDetail = (id) => {
detailDrawerCtrl.value = true;
getMalwareDetails(id)
}
function inputCopy(text: string) {
const aux = document.createElement('input');
aux.setAttribute('value', text);
document.body.appendChild(aux);
aux.select();
document.execCommand('copy');
document.body.removeChild(aux);
Message({ type: 'success', message: '复制成功' });
}
// 暴露方法,供父组件调用
defineExpose({
openDetail,
});
</script>
<style scoped lang="scss">
.malware-detail-card {
border-radius: 8px;
border: 1px solid #EEF0F5;
display: flex;
flex-direction: column;
.title {
color: #191919;
padding: 10px 16px;
font-size: 14px;
line-height: 22px;
background-color: #F3F3F3;
}
.content {
padding: 20px 16px;
:deep(.devui-table__thead .header-container) {
background-color: #F3F3F3;
}
}
}
.log-box {
border-radius: 8px;
background: #F7F8FA;
border: 1px solid #EEF0F5;
padding: 8px 16px;
}
.single-line-ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@@ -0,0 +1,128 @@
<template>
<div class="flex align-center justify-start">
<d-search icon-position="left" style="width: 380px" placeholder="请输入搜索软件名称" v-model="searchText" @change="handleSearch"></d-search>
<d-checkbox class="ml-10" label="只显示有漏洞的软件" :isShowTitle="false" v-model="checked" />
</div>
<Card simple class="jyh-card mt-3">
<div class="clear-padding">
<d-table
class="jyh-table"
v-if="filteredTableData.length > 0"
:indent="32"
@check-change="treeCheckChange"
:data="filteredTableData"
row-key="id"
>
<d-column field="libraryName" header="组件名称" show-overflow-tooltip>
<template #default="scope">
<a class="devui-link" @click="setSelectRow(scope.row)">{{ scope.row.libraryName }}</a>
</template>
</d-column>
<d-column field="latestVersion" header="版本"></d-column>
</d-table>
<NoData v-else :small="false"></NoData>
</div>
</Card>
<d-drawer v-model="showDetail" style="width: 800px; padding: 20px">
<SBOMDetail :selectRow="selectRow" @close="showDetail = false" />
</d-drawer>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, defineProps, computed } from 'vue';
import SBOMDetail from './SBOMDetail.vue';
import NoData from '@/components/NoData/NoData.vue';
import { dependencyTree, getVulnList } from '@/api/jyh';
import { useRoute } from 'vue-router';
const propsData = defineProps(['versionId']);
const checked = ref(false);
const route = useRoute(); // 获取路由对象
const showDetail = ref(false);
const selectRow = ref({});
const tableData = ref([]);
const searchText = ref(''); // 搜索关键词
const setSelectRow = (row) => {
selectRow.value = row;
showDetail.value = true;
};
const filteredTableData = computed(() => {
if (!searchText.value) return tableData.value; // 无关键词时返回全部数据
return tableData.value.filter(item =>
item.libraryName.toLowerCase().includes(searchText.value.toLowerCase())
);
});
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
const renameComponentsToChildren = (obj) => {
// 如果传入的不是对象,直接返回
if (typeof obj !== 'object' || obj === null) {
return obj;
}
// 如果对象有 `components` 属性,将其改为 `children`
if (obj.hasOwnProperty('component')) {
// 生成随机 UUID 并赋值给 id
obj.id = generateUUID(); // 使用浏览器/Node.js 原生 API
// obj.id = obj.component.id; // 将 components 改为 children
obj.libraryName = obj.component.libraryName; // 将 components 改为 children
obj.latestVersion = obj.component.latestVersion; // 将 components 改为 children
// delete obj.components; // 删除原来的 components 属性
}
// 递归处理对象的子元素
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
obj[key] = renameComponentsToChildren(obj[key]); // 递归处理子对象
}
}
return obj;
};
const treeCheckChange = (val, row, selection) => {
console.log('treeCheckChange', selection);
};
const fetchDependencyTree = async () => {
const { data } = await dependencyTree({
// softwareId: 47,
softwareId: propsData.versionId,
componentId: '',
current: 1,
size: 10
});
if (data.data.code === 200 && data?.data?.data?.components) {
tableData.value = renameComponentsToChildren(data?.data?.data?.components);
// pager.value.total = data?.data?.data?.total || [];
} else {
// tableData.value = baseTreeTableData1.value
}
};
onMounted(() => {
fetchDependencyTree();
});
</script>
<style scoped lang="scss">
.clear-padding {
margin: -16px -20px;
}
:deep(.devui-table__cell) {
display: flex;
align-items: center;
.devui-table__tree-operate {
transform: translateY(-3px);
}
}
</style>

View File

@@ -0,0 +1,186 @@
<template>
<d-layout class="dlayout">
<d-header class="dheader">
<div class="title">
<h1>{{props.selectRow.component.libraryName || '--'}}</h1>
<d-icon style="cursor: pointer" name="icon-close" @click="emit('close')"></d-icon>
</div>
<div class="sub-title">
<!-- <span>安全漏洞</span>
<span class="val">{{props.selectRow.component.confidence || 'low'}}</span> -->
<!-- <span class="split"></span> -->
<!-- <span>许可证</span>-->
<!-- <d-tag color="#3ac295" size="sm" class="ml-2">{{props.selectRow.component.licenseName || '&#45;&#45;'}}</d-tag>-->
</div>
</d-header>
<!-- <div class="card-box">-->
<!-- <div class="card-body">-->
<!-- <d-row :gutter="10">-->
<!-- <d-col :span="24" class="flex">-->
<!-- <label style="flex: 0.2">简介</label>-->
<!-- <p>{{props.selectRow.component.libraryName || '&#45;&#45;'}}</p>-->
<!-- </d-col>-->
<!-- </d-row>-->
<!-- &lt;!&ndash; <d-row :gutter="10">-->
<!-- <d-col :span="24" class="flex">-->
<!-- <label style="flex: 0.2">关联漏洞编号</label>-->
<!-- <p>{{props.selectRow.component.cveId || `cve-2025-${randomNum()}`}}</p>-->
<!-- </d-col>-->
<!-- </d-row> &ndash;&gt;-->
<!-- </div>-->
<!-- </div>-->
<div class="card-box">
<div class="card-header">组件详情</div>
<div class="card-body">
<d-row :gutter="10">
<d-col :span="12" class="flex">
<label>当前版本</label>
<p>{{props.selectRow.component.currentVersion || '--'}}</p>
</d-col>
<d-col :span="12" class="flex">
<label>最新版本</label>
<p>{{props.selectRow.component.latestVersion || '--'}}</p>
</d-col>
</d-row>
<d-row :gutter="10">
<d-col :span="12" class="flex">
<label>深度</label>
<p>{{props.selectRow.component.depth || '--'}}</p>
</d-col>
<d-col :span="12" class="flex">
<label>供应商</label>
<p>{{props.selectRow.component.vendor || '--'}}</p>
</d-col>
</d-row>
<d-row :gutter="10">
<d-col :span="12" class="flex">
<label>包管理器平台</label>
<p>{{props.selectRow.component.platform || '--'}}</p>
</d-col>
<d-col :span="12" class="flex">
<label>范围</label>
<p>{{props.selectRow.component.scope || '--'}}</p>
</d-col>
</d-row>
<d-row :gutter="10">
<d-col :span="12" class="flex">
<label>程序语言</label>
<p>{{props.selectRow.component.language || '--'}}</p>
</d-col>
<d-col :span="12" class="flex">
<label>活跃度</label>
<p>{{props.selectRow.component.activityScore || '--'}}</p>
</d-col>
</d-row>
<d-row :gutter="10">
<d-col :span="12" class="flex">
<label>下载量</label>
<p>{{props.selectRow.component.downloadCount || '--'}}</p>
</d-col>
<d-col :span="12" class="flex">
<label>识别方法</label>
<p>{{props.selectRow.component.recogMethod || '--'}}</p>
</d-col>
</d-row>
<d-row :gutter="10">
<d-col :span="12" class="flex">
<label>匹配度</label>
<p>{{props.selectRow.component.matchingDegree || '--'}}</p>
</d-col>
<d-col :span="12" class="flex">
<label>组件依赖关系</label>
<p>{{props.selectRow.component.treeLevel || '--'}}</p>
</d-col>
</d-row>
<!-- <d-row :gutter="10">-->
<!-- <d-col :span="12" class="flex">-->
<!-- <label>生命周期终点/服务终止</label>-->
<!-- <p><d-tag type="danger" size="sm">已停止维护</d-tag></p>-->
<!-- </d-col>-->
<!-- </d-row>-->
</div>
</div>
</d-layout>
</template>
<script setup>
import { ref, defineEmits, defineProps } from 'vue';
const props = defineProps(['selectRow']);
console.log(props.selectRow)
const emit = defineEmits(['close']);
// 生成随机数的方法
const randomNum = () => {
return Math.floor(Math.random() * (99999 - 10000 + 1)) + 10000;
};
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.dlayout {
width: 100%;
display: flex;
flex-direction: column;
align-items: start;
.dheader {
width: 100%;
}
.title {
font-size: 20px;
font-weight: bold;
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
d-icon {
cursor: pointer;
}
}
.sub-title {
display: flex;
gap: 2px;
align-items: center;
font-size: 12px;
margin-top: 6px;
color: #808080;
.val {
color: var(--devui-text);
}
.split {
width: 1px;
height: 14px;
background-color: #ddd;
}
}
.card-box {
width: 100%;
border-radius: 8px;
border: 1px solid #eef0f5;
font-size: 14px;
margin-top: 15px;
.card-header {
padding: 10px 16px;
background-color: #f5f5f5;
border-bottom: 1px solid #eef0f5;
font-weight: bold;
}
.card-body {
margin: 10px 16px;
.flex {
margin: 10px 0;
label,
p {
flex: 1;
}
label {
color: #595959;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,102 @@
<template>
<div class="subscribe-container">
<div class="subscribe-tip mb-3">当您订阅的软件有漏洞预警通知时将以应用号的方式通知可在首页查看或取消订阅</div>
<div class="subscribe-form">
<d-form ref="formRef" :data="formData" :rules="rules">
<d-form-item field="subscribeType" label="通知方式">
<d-checkbox-group name="subscribeType" v-model="formData.subscribeType" :options="subscribeTypeOptions" direction="row"></d-checkbox-group>
</d-form-item>
</d-form>
</div>
<div class="subscribe-operation mt-5">
<d-button @click="confirm" variant="solid" class="mr-2">确定</d-button>
<d-button @click="$emit('close')">取消</d-button>
</div>
</div>
</template>
<script setup lang="ts">
import { subscribeSoftware } from '@/api/jyh';
import { computed, ref } from 'vue';
import { Message } from 'vue-devui';
const subscribeTypeOptions = [
{ name: '短信', value: 'mobileSwitch', id: 1 },
{ name: '邮件', value: 'emailSwitch', id: 2 },
{ name: '站内通知', value: 'webSwitch', id: 3 },
];
const props = defineProps(['softwareData']);
const emit = defineEmits(['close','closeSub']);
const formRef = ref(null);
const formData = ref({
subscribeType: [],
});
const rules = {
subscribeType: [{ type: 'array', required: true, message: '请至少选择一种通知方式', trigger: 'change' }],
};
const sendSubscribe = async () => {
let alertType = {};
formData.value.subscribeType.forEach(item => {
alertType[item.value] = true;
})
const data = await subscribeSoftware({
softwareId: props.softwareData.id,
status: '1',
alertType,
}).catch(err => {
Message({
message: err.error,
type: 'error'
})
});
if (!data?.data?.data?.data) {
Message({
message: data?.data?.data?.msg,
type: 'error'
})
} else {
Message({
message: '订阅成功!',
type: 'success'
})
emit('close');
emit('closeSub');
}
}
const confirm = () => {
(formRef?.value as any)?.validate((isValid: boolean, invalidFields: any) => {
if (!isValid) {
return;
}
sendSubscribe();
});
};
</script>
<style scoped lang="scss">
.subscribe-container {
width: 600px;
.subscribe-tip {
color: #191919;
font-family: HarmonyOS Sans SC;
font-weight: regular;
font-size: 14px;
}
.subscribe-form {
display: flex;
align-items: center;
border-radius: 4px;
background: #F2F5FC;
padding: 20px 32px 0;
form {
width: 100%;
}
}
.subscribe-operation {
text-align: center;
}
}
</style>

View File

@@ -0,0 +1,518 @@
<template>
<div class="version-info">
<Card simple class="jyh-card mb-4">
<div class="card-title">基本信息
<i @click="toggleCard('basic')" :class="isExpanded.basic ? 'icon icon-chevron-up-2 float-right': 'icon icon-chevron-down-2 float-right'"></i>
</div>
<div v-if="isExpanded.basic">
<d-row class="mb-3">
<d-col :span="8">
<span class="card-key width-90">软件名称</span>
<span class="card-val">{{ ReleaseInfo.basicInfo.softwareName || '--' }}</span>
</d-col>
<d-col :span="8">
<span class="card-key width-90">软件版本</span>
<span class="card-val">{{ ReleaseInfo.basicInfo.softwareVersion || '--' }}</span>
</d-col>
<d-col :span="8">
<span class="card-key width-90">主语言类型</span>
<span class="card-val">{{ ReleaseInfo.basicInfo.mainLanguageType || '--' }}</span>
</d-col>
</d-row>
<d-row>
<d-col :span="8">
<span class="card-key width-90">开发商</span>
<span class="card-val">{{ ReleaseInfo.basicInfo.developer || '--' }}</span>
</d-col>
<d-col :span="8">
<span class="card-key width-90">发布时间</span>
<span class="card-val">
{{ ReleaseInfo.basicInfo.releaseDate?dayjs(ReleaseInfo.basicInfo.releaseDate).format('YYYY-MM-DD') :'--' }}</span>
</d-col>
<!-- <d-col :span="8">-->
<!-- <span class="card-key width-90">版本描述</span>-->
<!-- <span class="card-val">{{ ReleaseInfo.basicInfo.versionDescription || '&#45;&#45;' }}</span>-->
<!-- </d-col>-->
</d-row>
</div>
</Card>
<Card simple class="jyh-card mb-4">
<div class="card-title">参考信息
<i @click="toggleCard('reference')" :class="isExpanded.reference ? 'icon icon-chevron-up-2 float-right': 'icon icon-chevron-down-2 float-right'"></i>
</div>
<div v-if="isExpanded.reference">
<d-row class="mb-3">
<d-col :span="8">
<span class="card-key width-110">集成风险</span>
<d-tag color="#F6933B">{{ ReleaseInfo.referenceInfo.integrationRisk || jcfxList[ReleaseInfo.basicInfo.integrationRiskLevel] || '--' }}</d-tag>
<!-- <span class="card-val">{{ ReleaseInfo.referenceInfo.integrationRisk || ReleaseInfo.basicInfo.integrationRisk || '&#45;&#45;' }}</span>-->
</d-col>
<d-col :span="8">
<span class="card-key width-110">代码量(L)</span>
<span class="card-val">{{ ReleaseInfo.referenceInfo.codeQuantity || '--' }}</span>
</d-col>
<d-col :span="8">
<span class="card-key width-110">漏洞数量</span>
<a style="cursor: pointer" @click="handleTabChange('ldxx')" class="card-url">
{{ ReleaseInfo.referenceInfo.vulnerabilityCount || '--' }}
</a>
</d-col>
</d-row>
<d-row class="mb-3">
<d-col :span="8">
<d-tooltip position="left" content="此内容由AI辅助生成">
<img
style="width: 14px; height: 14px;display: inline-block"
src="@/assets/imgs/jyh/aiLogo.png"
/>
</d-tooltip>
<span class="card-key width-110">社区EOL日期</span>
<span class="card-val">
{{ ReleaseInfo.criticalSoftwareAIGenerateInfo.communityEolDate?dayjs(ReleaseInfo.criticalSoftwareAIGenerateInfo.communityEolDate).format('YYYY-MM-DD') :'--' }}</span>
</d-col>
<d-col :span="8">
<span class="card-key width-110">所属领域/产业</span>
<span class="card-val">{{ getIndustryName(ReleaseInfo.referenceInfo.industry) || '--' }}</span>
</d-col>
<!-- <d-col :span="8">-->
<!-- <span class="card-key width-110">版本火车名称</span>-->
<!-- <span class="card-val">CleanSource V8.0</span>-->
<!-- </d-col>-->
</d-row>
<d-row class="mb-3">
<d-col :span="24">
<span class="card-key width-140">源码下载包地址</span>
<a class="card-url" :href="ReleaseInfo.referenceInfo.sourceCodeDownloadUrl || '--'">{{ ReleaseInfo.referenceInfo.sourceCodeDownloadUrl || ReleaseInfo.basicInfo.sourceDownloadUrl || '--' }}</a>
</d-col>
</d-row>
<d-row class="mb-3">
<d-col :span="24">
<span class="card-key width-140">源码社区托管地址</span>
<a class="card-url" :href="ReleaseInfo.referenceInfo.sourceCodeCommunityUrl || '--'">{{ ReleaseInfo.referenceInfo.sourceCodeCommunityUrl || ReleaseInfo.basicInfo.communityWebsite || '--' }}</a>
</d-col>
</d-row>
<!-- <d-row>-->
<!-- <d-col :span="24">-->
<!-- <span class="card-key width-140">社区EOL披露地址</span>-->
<!-- <a class="card-url" :href="ReleaseInfo.referenceInfo.communityEolDisclosureUrl || '&#45;&#45;'">{{ ReleaseInfo.referenceInfo.communityEolDisclosureUrl || ReleaseInfo.basicInfo.eolNoticeUrl || '&#45;&#45;' }}</a>-->
<!-- </d-col>-->
<!-- </d-row>-->
</div>
</Card>
<Card simple class="jyh-card mb-4">
<div class="card-title">
<d-tooltip position="left" content="此内容由AI辅助生成">
<img
style="width: 14px; height: 14px;display: inline-block"
src="@/assets/imgs/jyh/aiLogo.png"
/>
</d-tooltip>
开源主License信息
<i @click="toggleCard('license')" :class="isExpanded.license ? 'icon icon-chevron-up-2 float-right': 'icon icon-chevron-down-2 float-right'"></i>
</div>
<div v-if="isExpanded.license" class="mb-3">
<d-table class="jyh-table jyh-table-2" :header-bg="true"
:data="[
{ name: ReleaseInfo.basicInfo.mainLicenseName || ReleaseInfo.criticalSoftwareAIGenerateInfo.mainLicenseName,
description: ReleaseInfo.basicInfo.mainLicenseDescription || ReleaseInfo.criticalSoftwareAIGenerateInfo.mainLicenseDescription}
]">
<d-column type="index" header="序号" width="80"></d-column>
<!-- <d-column field="id" header="编号"></d-column>-->
<d-column field="name" header="名称"></d-column>
<d-column field="description" header="描述"></d-column>
<!-- <d-column field="applicationScenario" header="应用场景"></d-column>-->
</d-table>
</div>
<!-- <d-row>-->
<!-- <d-col :span="24">-->
<!-- <span class="card-key width-140">主License分析报告</span>-->
<!-- <a class="card-url" href="https://designcloud.uiplus.huawei.com/developer/main/index.html">https://designcloud.uiplus.huawei.com/developer/main/index.html</a>-->
<!-- </d-col>-->
<!-- </d-row>-->
</Card>
<Card simple class="jyh-card mb-4">
<div class="card-title">
<d-tooltip position="left" content="此内容由AI辅助生成">
<img
style="width: 14px; height: 14px;display: inline-block"
src="@/assets/imgs/jyh/aiLogo.png"
/>
</d-tooltip>
Copyright报告
<i @click="toggleCard('copyright')" :class="isExpanded.copyright ? 'icon icon-chevron-up-2 float-right': 'icon icon-chevron-down-2 float-right'"></i>
</div>
<div v-if="isExpanded.copyright" class="mb-3">
<d-table
class="jyh-table jyh-table-2"
:header-bg="true"
:data="[
{
reportName: ReleaseInfo.basicInfo.copyrightReportName || ReleaseInfo.criticalSoftwareAIGenerateInfo.copyrightReportName,
reportUrl: ReleaseInfo.basicInfo.copyrightReportUrl || ReleaseInfo.criticalSoftwareAIGenerateInfo.copyrightReportUrl,
entryTime: ReleaseInfo.basicInfo.copyrightEntryTime || ReleaseInfo.criticalSoftwareAIGenerateInfo.copyrightEntryTime
}
]"
>
<d-column field="reportName" header="Copyright报告">
<template #default="scope">
<a :href="scope.row.reportUrl">{{ scope.row.reportName }}</a>
</template>
</d-column>
<d-column field="entryTime" header="录入时间"></d-column>
</d-table>
</div>
<!-- <d-row>-->
<!-- <d-col :span="24">-->
<!-- <span class="card-key width-140">主License分析报告</span>-->
<!-- <a class="card-url" href="https://designcloud.uiplus.huawei.com/developer/main/index.html">https://designcloud.uiplus.huawei.com/developer/main/index.html</a>-->
<!-- </d-col>-->
<!-- </d-row>-->
</Card>
<Card simple class="jyh-card mb-4">
<div class="card-title">实体
<i @click="toggleCard('entity')" :class="isExpanded.entity ? 'icon icon-chevron-up-2 float-right': 'icon icon-chevron-down-2 float-right'"></i>
</div>
<div v-if="isExpanded.entity">
<d-table
class="jyh-table jyh-table-2"
:header-bg="true"
:data="ReleaseInfo.entityInfoList"
>
<d-column field="entityType" header="类型"></d-column>
<d-column field="entityDownloadUrl" header="实体包下载地址">
<template #default="scope">
<a :href="scope.row.entityDownloadUrl" target="_blank">{{ scope.row.entityDownloadUrl }}</a>
</template>
</d-column>
<d-column field="entityFilename" header="文件名称"></d-column>
<d-column field="entryTime" header="录入时间"></d-column>
<d-column field="integrityCheckResult" header="完整性结果">
<template #default="scope">
<a @click="visible = true">{{ scope.row.integrityCheckResult }}</a>
</template>
</d-column>
</d-table>
</div>
</Card>
<Card simple class="jyh-card mb-4">
<div class="card-title">
<d-tooltip position="left" content="此内容由AI辅助生成">
<img
style="width: 14px; height: 14px;display: inline-block"
src="@/assets/imgs/jyh/aiLogo.png"
/>
</d-tooltip>
网络安全
<i @click="toggleCard('intSafe')" :class="isExpanded.intSafe ? 'icon icon-chevron-up-2 float-right': 'icon icon-chevron-down-2 float-right'"></i>
<!-- <span style="color: #3b82f6;font-size: 12px;font-weight: normal">(该数据由AI生成)</span>-->
</div>
<div v-if="isExpanded.intSafe">
<!-- <d-row class="mb-3">-->
<!-- <d-col :span="24">-->
<!-- <span class="card-key width-160">软件评分</span>-->
<!-- <span class="card-val">{{ ReleaseInfo.networkSecurity.softwareRating || '&#45;&#45;' }}</span>-->
<!-- </d-col>-->
<!-- </d-row>-->
<d-row class="mb-3">
<d-col :span="24">
<d-tooltip position="left" content="此内容由AI辅助生成">
<img
style="width: 14px; height: 14px;display: inline-block"
src="@/assets/imgs/jyh/aiLogo.png"
/>
</d-tooltip>
<span class="card-key width-160">社区安全问题求助</span>
<a class="card-url" :href="ReleaseInfo.basicInfo.securityHelpChannel || ReleaseInfo.criticalSoftwareAIGenerateInfo.securityHelpChannel || ''" target="_blank">{{
ReleaseInfo.basicInfo.securityHelpChannel || ReleaseInfo.criticalSoftwareAIGenerateInfo.securityHelpChannel || '--'
}}</a>
</d-col>
</d-row>
<div class="mb-3 flex">
<d-tooltip position="left" content="此内容由AI辅助生成">
<img
style="width: 14px; height: 14px;display: inline-block"
src="@/assets/imgs/jyh/aiLogo.png"
/>
</d-tooltip>
<span class="card-key width-160">官网漏洞披露地址</span>
<d-table style="display: contents" class="jyh-table jyh-table-2" :header-bg="true" :data="vulnDisclosureUrl">
<!-- <d-column field="id" header="编号"></d-column> -->
<d-column field="厂商" header="厂商"></d-column>
<d-column field="软件名称" header="软件名称"></d-column>
<!-- <d-column field="source" header="来源"></d-column> -->
<d-column field="软件地址" header="来源url"> </d-column>
</d-table>
</div>
<div class="mb-3 flex">
<d-tooltip position="left" content="此内容由AI辅助生成">
<img
style="width: 14px; height: 14px;display: inline-block"
src="@/assets/imgs/jyh/aiLogo.png"
/>
</d-tooltip>
<span class="card-key width-160">外部漏洞源的软件名称</span>
<d-table
style="display: contents"
class="jyh-table jyh-table-2"
:header-bg="true"
:data="externalVulnSourceInfo"
>
<!-- <d-column field="id" header="编号"></d-column> -->
<d-column field="厂商" header="厂商"></d-column>
<d-column field="软件名称" header="软件名称"></d-column>
<!-- <d-column field="source" header="来源"></d-column> -->
<d-column field="软件地址" header="来源url"> </d-column>
</d-table>
</div>
<d-row class="mb-3">
<d-col :span="24">
<d-tooltip position="left" content="此内容由AI辅助生成">
<img
style="width: 14px; height: 14px;display: inline-block"
src="@/assets/imgs/jyh/aiLogo.png"
/>
</d-tooltip>
<span class="card-key width-160">是否涉及用户登录</span>
<span class="card-val">{{ securityInfoList.involvesUserLogin ? '是' : '否' }}</span>
</d-col>
</d-row>
<d-row>
<d-col :span="24">
<d-tooltip position="left" content="此内容由AI辅助生成">
<img
style="width: 14px; height: 14px;display: inline-block"
src="@/assets/imgs/jyh/aiLogo.png"
/>
</d-tooltip>
<span class="card-key width-160">是否安卓类APP组件</span>
<span class="card-val">{{ securityInfoList.isAndroidComponent ? '是' : '否' }}</span>
</d-col>
</d-row>
</div>
</Card>
<d-modal
title="完整性结果详情"
v-model="visible"
class="file-search-modal"
:style="{ width: '800px', height: '400px' }"
@close="handleClose"
>
<Card simple class="jyh-card mb-4" style="box-shadow: none; padding: 0">
<d-row class="mb-3">
<d-col :span="24">
<span class="card-key width-160">社区哈希类型</span>
<span class="card-val">sha512</span>
</d-col>
</d-row>
<d-row class="mb-3">
<d-col :span="24">
<span class="card-key width-160">社区哈希值</span>
<span class="card-val">sha5123b831bee9d575a2729065f830a65f830a65f83a65f852...</span>
<d-tooltip content="复制" :mouse-enter-delay="800">
<Icon name="icon-copy" class="ml-10 w-4 flex-shrink-0" color="#9FA7B3" @click="handleCopy" />
</d-tooltip>
</d-col>
</d-row>
<d-row class="mb-3">
<d-col :span="24">
<span class="card-key width-160">社区签名校验结果</span>
<span class="card-val card-success">通过</span>
</d-col>
</d-row>
<d-row class="mb-3">
<d-col :span="24">
<span class="card-key width-160">社区签名</span>
<span class="card-val">确认无社区签名</span>
</d-col>
</d-row>
<d-row class="mb-3">
<d-col :span="24">
<span class="card-key width-160">社区签名下载地址</span>
<span class="card-val">确认无社区签名</span>
</d-col>
</d-row>
<d-row class="mb-3">
<d-col :span="24">
<span class="card-key width-160">社区公钥</span>
<span class="card-val">确认无社区签名</span>
</d-col>
</d-row>
<d-row class="mb-3">
<d-col :span="24">
<span class="card-key width-160">社区公钥下载地址</span>
<span class="card-val">确认无社区签名</span>
</d-col>
</d-row>
</Card>
<template #footer>
<d-modal-footer style="text-align: center">
<d-button variant="solid" color="primary" @click="handleClose">确认</d-button>
<d-button @click="handleClose">取消</d-button>
</d-modal-footer>
</template>
</d-modal>
</div>
</template>
<script setup lang="ts">
import { ref, defineEmits, onMounted, reactive,computed,defineProps,toRef } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { discussDetailType, userInfoType } from '@/api/discussion/types';
import { Message } from 'vue-devui/message';
import { useClipboard } from '@vueuse/core/index';
import { releaseInfo } from '@/api/jyh';
import { useGlobalInfoStore } from '@/stores/Global';
import dayjs from 'dayjs';
const route = useRoute();
const emits = defineEmits(['changeTabs']);
const props = defineProps(['ReleaseInfo']);
// 触发事件的方法
const handleTabChange = (tabName) => {
emits('changeTabs', tabName); // 触发 changeTabs 事件,并传递参数
};
const jcfxList = ['','低','中','高'];
const visible = ref(false);
// 将 props.ReleaseInfo 转换为响应式引用
const ReleaseInfo = toRef(props, 'ReleaseInfo');
const globalInfo = useGlobalInfoStore();
const industryList = ref(globalInfo.industryList);
const getIndustryName = (value) => {
const industry = industryList.value.find((item) => item.value === value);
return industry ? industry.name : '未知行业'; // 如果找不到,返回默认值
};
// 定义每个 Card 的展开状态
const isExpanded = ref({
basic: false,
reference: false,
license: false,
copyright: false,
entity: false,
intSafe: false
});
// 切换 Card 展开状态
const toggleCard = (cardType) => {
isExpanded.value[cardType] = !isExpanded.value[cardType];
};
const securityInfoList = computed(() => {
if(ReleaseInfo.value.securityInfoList&&ReleaseInfo.value.securityInfoList.length>0) {
return ReleaseInfo.value.securityInfoList[0]
} else if(ReleaseInfo.value.softwareSecurityAIGenerateInfo&&ReleaseInfo.value.softwareSecurityAIGenerateInfo.length>0) {
return ReleaseInfo.value.softwareSecurityAIGenerateInfo[0]
} else {
return {}
}
});
const vulnDisclosureUrl = computed(() => {
if(ReleaseInfo.value.softwareSecurityAIGenerateInfo) {
const _vulnDisclosureUrl: any = JSON.parse(ReleaseInfo?.value.softwareSecurityAIGenerateInfo[0]?.vulnDisclosureUrl || '[]');
return _vulnDisclosureUrl || [];
} else {
return []
}
});
const externalVulnSourceInfo = computed(() => {
if(props.ReleaseInfo) {
const _vulnDisclosureUrl: any = JSON.parse(props.ReleaseInfo?.softwareSecurityAIGenerateInfo[0]?.externalVulnSourceInfo || '[]');
return _vulnDisclosureUrl || [];
} else {
return []
}
});
const router = useRouter();
// 跳转
const gotoDetail = (row) => {
// 后续请换成params暂时params传参没传过去
router.push({
path: 'repoDetail/' + row.version,
params: { tab: 'ldxx' } // 指定需要激活的 Tab
});
};
// 跳转
const handleClose = (row) => {
visible.value = false;
};
const { copy } = useClipboard();
const handleCopy = (event) => {
event.stopPropagation();
copy('');
Message.success('复制成功');
};
const getReleaseInfo = async (versionId) => {
const { data, error } = await releaseInfo({ softwareId: versionId });
if (!error && data) {
ReleaseInfo.value = data.data;
}
};
onMounted(() => {
// const versionId = route.params.versionId; // 获取路由参数 versionId
// if (versionId) {
// getReleaseInfo(versionId); // 调用接口获取数据
// }
});
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.jyh-card {
background: #F3F3F3 !important;
.card-title {
margin-bottom: 0 !important;
}
}
.card-key {
color: #777777;
font-family: HarmonyOS Sans SC;
font-weight: regular;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
width: 112px;
margin-right: 20px;
}
.card-val {
color: #191919;
font-family: HarmonyOS Sans SC;
font-weight: regular;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
}
.card-success {
display: inline-block;
border-radius: 4px;
background: #81c054;
color: #ffffff;
font-family: HarmonyOS Sans SC;
font-weight: medium;
font-size: 12px;
line-height: 20px;
height: 20px;
letter-spacing: 0px;
text-align: left;
padding: 0 8px;
}
</style>

View File

@@ -0,0 +1,297 @@
<template>
<div class="soft-detail-container">
<div class="mt-3">
<d-table class="jyh-table" v-if="tableData.length > 0" :striped="false" :data="tableData" table-layout="auto">
<d-column type="index" width="40"></d-column>
<d-column field="vulnId" header="编码">
<template #default="scope">
<a @click="openVulnDetail(scope.row)">{{ scope.row.vulnId }}</a>
</template>
</d-column>
<!-- <d-column field="cweId" header="CVE ID"></d-column> -->
<d-column field="cvssScore" header="CVSS 打分"></d-column>
<!-- <d-column field="fixStatus" header="修补状态">-->
<!-- <template #default="scope">-->
<!-- <a>{{ scope.row.version }}</a>-->
<!-- </template>-->
<!-- </d-column>-->
<d-column field="severity" header="漏洞级别">
<template #default="scope">
<a>{{ scope.row.severity }}</a>
</template>
</d-column>
<d-column field="fixVersion" header="补丁版本">
<template #default="scope">
<a>{{ scope.row.fixVersion }}</a>
</template>
</d-column>
<!-- <d-column field="publishDate" header="补丁修复时间" fixed-right="0px">-->
<!-- <template #default="scope">-->
<!-- {{ dayjs(scope.row.publishDate).format('YYYY-MM-DD') }}-->
<!-- </template>-->
<!-- </d-column>-->
</d-table>
<NoData v-else :small="false"></NoData>
</div>
<div class="mt-20 mb-20 flex justify-end" v-if="tableData.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="fetchVulnList"
@page-size-change="fetchVulnList"
/>
</div>
<!-- 删除组件 -->
<GModal
v-model="deletevModels"
ref="deleteModal"
title="删除?"
@confirm="confirmDelete"
showWarnIcon
confirmColor="danger"
>
<span class="inline-block text-G900 text-sm font-normal leading-[20px] break-all"
>你确认删除此项{{ item.cla_name }} </span
>
</GModal>
<!-- 漏洞详情 -->
<d-drawer v-model="vulnDetailVisable" style="width: auto">
<VulnerabilityDetail
:vulnInfo="currentVuln"
:softwareId="currentVuln.softwareId"
:vulnId="currentVuln.id"
></VulnerabilityDetail>
</d-drawer>
</div>
</template>
<script setup lang="ts">
import * as dayjs from 'dayjs';
import SettingTitle from '@/components/Setting/SettingTitle/index.vue';
import SettingText from '@/components/Setting/SettingText/index.vue';
import { ref, reactive, onMounted, defineProps } from 'vue';
import { getGroupClaList, setGroupClaStatus, deleteGroupCla } from '@/api/cla';
import { useRouter, useRoute } from 'vue-router';
import { GModal } from '@/components/Setting/index';
import { reqCatch } from '@/utils/catch';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
import { Message } from 'vue-devui';
import VulnerabilityDetail from '@/views/Jyh/Version/Detail/VulnerabilityDetail.vue';
import { getVulnList } from '@/api/jyh';
import NoData from '@/components/NoData/NoData.vue';
const { namespace } = getOrgInfo();
const propsData = defineProps(['versionId']);
const tableData = ref([]);
const formData = ref({
title: '',
type: '',
useType: '',
formData: '',
select: '',
time1: null,
name: '',
versionCode: '',
pulishTime: ''
});
const typeOptions = reactive(['主软件', '依赖软件']);
const riskOptions = reactive(['低', '中', '高']);
const useTypeOptions = reactive(['软件', '文档', '文档+软件']);
const isUseOptions = reactive(['是', '否']);
const pager = ref({
total: 10,
pageIndex: 1,
pageSize: 10
});
// 删除弹窗
const item = ref('');
const deleteModal = ref();
const deletevModels = ref(false);
const openDelete = (row: any) => {
item.value = row;
deleteModal.value.showFlag = true;
deletevModels.value = true;
};
const router = useRouter();
const repoFun = (arr: any) => {
return arr ? (arr.length > 3 ? arr.slice(0, 3) : arr) : [];
};
// 编辑
const editRow = (row: any) => {
// 后续请换成params暂时params传参没传过去
router.push({
path: 'cla/edit/' + row.object_id,
query: { cla: row.cla, pageType: 'edit' }
});
};
// 确认删除
const confirmDelete = async (e: any) => {
// const res = await reqCatch(deleteGroupCla, { group_id: namespace.value, cla_id: item.value.object_id });
// if (!res.error) {
// getGroupClaListData();
// }
};
// 去成员页
const goMember = (row: any) => {
// 后续请换成params暂时params传参没传过去,注意路由有个:claID
router.push({
path: 'cla/claId/member',
query: { claId: row.object_id, claName: row.cla_name, group_path: namespace.value, pageType: 'edit' }
});
};
const getGroupClaListData = async () => {
// const params = {
// group_id: namespace.value,
// page: pager.value.pageIndex,
// per_page: pager.value.pageSize
// };
// const res = await reqCatch(getGroupClaList, params);
// if (!res.error) {
// tableData.value = res?.data?.data?.content;
// pager.value.total = res.data.data.total;
// }
};
getGroupClaListData();
const setClaStatus = async (row: any) => {
// const params = {
// group_id: namespace.value,
// cla_id: row.object_id,
// status: row.status
// };
// const res = await reqCatch(setGroupClaStatus, params);
// if (!res.error) {
// Message.success('操作成功!');
// getGroupClaListData();
// }
};
const signCla = (row: any) => {
router.push('/cla/' + row.object_id);
};
const vulnDetailVisable = ref(false);
const currentVuln = ref();
const openVulnDetail = (row) => {
vulnDetailVisable.value = true;
if (row.softwareId && row.id) {
currentVuln.value = row;
} else {
currentVuln.value = {
// softwareId: 47,
softwareId: propsData.versionId,
id: 72
};
}
};
const fetchVulnList = async () => {
const { data } = await getVulnList({
// softwareId: 47,
softwareId: propsData.versionId,
componentId: '',
current: pager.value.pageIndex,
size: pager.value.pageSize
});
if (data.data.code === 200) {
tableData.value = data?.data?.data?.records || [];
pager.value.total = data?.data?.data?.total || [];
}
};
const valLists = ref([]);
onMounted(() => {
fetchVulnList();
});
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.soft-detail-container {
//padding: 7px 20px 12px;
}
.error {
color: $devui-contrast;
}
:deep(.devui-table__row td) {
padding-top: 16px;
padding-bottom: 16px;
}
:deep(.devui-tag .devui-tag--default) {
background-color: #fff;
border-radius: 12px;
padding: 4px 8px;
font-size: 12px;
line-height: 16px;
margin-right: 8px;
border: 1px solid #e3e3ee;
}
.one-line {
word-break: break-all;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.title {
font-size: 14px;
font-weight: 400;
color: #9a9b9c;
line-height: 20px;
}
.cur-title {
font-size: 14px;
font-weight: 500;
color: #2d2d2e;
line-height: 20px;
}
.search-form {
position: relative;
padding: 12px 20px 12px 0;
border-radius: 8px 0px 8px 8px;
//background: #F2F5FC;
.output-btn {
position: absolute;
right: 28px;
bottom: 4px;
color: #191919;
font-family: HarmonyOS Sans SC;
font-weight: regular;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
}
}
.secret-breadcrumb {
padding: 7px 20px 12px;
background: #ffffff;
}
</style>

View File

@@ -0,0 +1,198 @@
<template>
<div class="vuln-detail-container">
<div class="vuln-title mb-2">{{ vulnDetail.vulnId }}</div>
<div class="vuln-sub-title mb-3">
<span>{{ vulnDetail.cweId || '--' }}</span>
<span class="split-line"></span>
<span><span class="release-time">发布时间</span>
{{ vulnDetail?.publishDate?dayjs(vulnDetail?.publishDate).format('YYYY-MM-DD') :'--' }}</span>
</div>
<Card simple class="jyh-card mb-4">
<div class="card-title">漏洞名称</div>
<d-row class="mb-3">
<!-- <d-col :span="12">-->
<!-- <span class="card-key width-90">漏洞名称</span>-->
<!-- <span class="card-val">{{vulnDetail.vulnId || '&#45;&#45;'}}</span>-->
<!-- </d-col>-->
<d-col :span="12">
<span class="card-key width-90">关联漏洞编号</span>
<span class="card-val">{{vulnDetail.cweId || '--'}}</span>
</d-col>
</d-row>
<d-row class="mb-3">
<d-col :span="24">
<span class="card-key width-90">简介</span>
<span class="card-val">{{vulnDetail.description || '--'}}</span>
</d-col>
</d-row>
<d-row class="mb-3">
<d-col :span="24">
<span class="card-key width-90">额外信息</span>
<span class="card-val">
<span class="card-val">{{vulnDetail.extraInfo || '--'}}</span>
</span>
</d-col>
</d-row>
<d-row>
<d-col :span="24">
<span class="card-key width-90">补丁链接</span>
<span class="card-val">
<span class="card-val">{{vulnDetail.patchLink || '--'}}</span>
</span>
</d-col>
</d-row>
</Card>
<Card simple class="jyh-card mb-4">
<div class="card-title">安全漏洞</div>
<d-row class="mb-3">
<d-col :span="12">
<span class="card-key width-90">严重性</span>
<span class="card-val">{{vulnDetail.severity || '--'}}</span>
</d-col>
<d-col :span="12">
<span class="card-key width-90">分值</span>
<span class="card-val">{{vulnDetail.cvssScore || '--'}}</span>
</d-col>
</d-row>
</Card>
<Card simple class="jyh-card mb-4">
<div class="card-title">安全问题</div>
<d-row class="mb-3">
<d-col :span="12">
<span class="card-key width-90">攻击向量</span>
<span class="card-val">{{vulnDetail.attackVector || '--'}}</span>
</d-col>
<d-col :span="12">
<span class="card-key width-90">攻击复杂度</span>
<span class="card-val">{{vulnDetail.attackComplexity || '--'}}</span>
</d-col>
</d-row>
<d-row class="mb-3">
<d-col :span="12">
<span class="card-key width-90">用户交互</span>
<span class="card-val">{{vulnDetail.userInteraction || '--'}}</span>
</d-col>
<d-col :span="12">
<span class="card-key width-90">需要特殊权限</span>
<span class="card-val">{{vulnDetail.privilegesRequired || '--'}}</span>
</d-col>
</d-row>
</Card>
<Card simple class="jyh-card mb-4">
<div class="card-title">影响的组件</div>
<d-row class="mb-3">
<d-col :span="12">
<span class="card-key width-90">组件名</span>
<span class="card-val">{{componentInfo.moduleName || '--'}}</span>
</d-col>
<d-col :span="12">
<span class="card-key width-90">深度</span>
<span class="card-val">{{componentInfo.depth || '--'}}</span>
</d-col>
</d-row>
<d-row class="mb-3">
<d-col :span="12">
<span class="card-key width-90">当前版本</span>
<span class="card-val">{{componentInfo.currentVersion || '--'}}</span>
</d-col>
<d-col :span="12">
<span class="card-key width-90">最新版本</span>
<span class="card-val">{{componentInfo.latestVersion || '--'}}</span>
</d-col>
</d-row>
<d-row>
<d-col :span="24">
<span class="card-key width-90">组件依赖关系</span>
<span class="card-val">{{componentInfo.version || '--'}}</span>
</d-col>
</d-row>
</Card>
<Card simple class="jyh-card mb-4">
<div class="card-title">修复建议</div>
<d-row class="mb-3">
<d-col :span="12">
<span class="card-key width-90">补丁版本</span>
<span class="card-val">{{remediationInfo?.recommendedVersion || '--'}}</span>
</d-col>
<!-- <d-col :span="12">-->
<!-- <span class="card-key width-90">修复兼容性</span>-->
<!-- <span class="card-val">{{remediationInfo?.compatibility || '&#45;&#45;'}}</span>-->
<!-- </d-col>-->
</d-row>
<d-row>
<d-col :span="24">
<span class="card-key width-90">修复建议</span>
<span class="card-val">{{remediationInfo?.recommendedFix || '--'}}</span>
</d-col>
</d-row>
</Card>
</div>
</template>
<script setup lang="ts">
import { getVulnDetail } from '@/api/jyh';
import { ref,defineProps } from 'vue';
import { useTimeFormat } from '@/utils/hooks/useTimeFormat';
import dayjs from 'dayjs';
const { formatTime } = useTimeFormat();
const propsData = defineProps(['softwareId', 'vulnId', 'vulnInfo']);
const vulnDetail = ref({});
const componentInfo = ref({});
const remediationInfo = ref({})
const queryVulnDetail = async () => {
const data = await getVulnDetail({softwareId: propsData.softwareId, id: propsData.vulnId});
if (data?.data?.data?.data) {
vulnDetail.value = data.data.data.data.vulnerabilityInfo;
componentInfo.value = data.data.data.data.componentInfo;
remediationInfo.value = data.data.data.data.remediationInfo;
}
};
queryVulnDetail();
</script>
<style scoped lang="scss">
.vuln-detail-container {
width: 800px;
padding: 20px;
.vuln-title {
color: #191919;
font-family: HarmonyOS Sans SC;
font-weight: 400;
font-size: 18px;
}
.vuln-sub-title {
color: #191919;
font-family: HarmonyOS Sans;
font-weight: regular;
font-size: 12px;
line-height: 20px;
letter-spacing: 0px;
text-align: left;
.split-line {
width: 1px;
display: inline-block;
line-height: 16px;
height: 16px;
vertical-align: middle;
margin: 0 8px;
background-color: #EEF0F5;
}
.release-time {
color: #808080;
}
}
.card-val {
display: inline-block;
width: calc(100% - 90px);
vertical-align: top;
}
}
</style>

View File

@@ -0,0 +1,404 @@
<template>
<div class="version-detail">
<div class="version-detail-header">
<div class="header-top">
<GAvatar style="margin-right: 12px" :src="avatarUrl" name="Vue" :width="40" :height="40"></GAvatar>
<div>
<div class="header-title1">{{ ReleaseInfo.basicInfo.softwareName || '--' }}</div>
<div class="header-div">
<span class="span1">版本</span><span class="span2">{{ ReleaseInfo.basicInfo.softwareVersion || '--' }}</span>
<span class="split"></span>
<!-- <span class="span1">生命周期状态</span><span class="span2">{{ softwareData.lifecycle }}</span>-->
<!-- <span class="split"></span>-->
<span class="span1">软件评分</span><span class="span2"
style="
font-weight: bold;
color: #409EFF;
font-size: 16px;
text-decoration: underline;
cursor: pointer;
"
>{{ ReleaseInfo.networkSecurity.softwareRating || ReleaseInfo.basicInfo.securityScore || '&#45;&#45;' }}</span>
<span class="split"></span>
<span class="span1">集成风险</span>
<d-tag color="#F6933B">{{ ReleaseInfo.referenceInfo.integrationRisk || jcfxList[ReleaseInfo.basicInfo.integrationRiskLevel] || '--' }}</d-tag>
<!-- <d-tag color="#F6933B">{{ ReleaseInfo.referenceInfo.integrationRisk || '&#45;&#45;' }}</d-tag>-->
</div>
</div>
<div class="fr">
<!-- <d-button v-if="!isSub" class="mr-2" variant="solid" @click="openSubscription">订阅</d-button>-->
<!-- <d-button v-else class="mr-2" variant="solid" disabled>已订阅</d-button>-->
<d-modal class="subscribe-modal" style="width: auto;" v-model="subscribeModalVisable" title="订阅漏洞预警通知">
<template #header>
<div class="subscribe-header">
<d-icon name="icon-info-o" color="var(--devui-info)" size="18px"></d-icon>
<span>订阅漏洞预警通知</span>
</div>
</template>
<SubscriptionModal @closeSub="isSub = true;" @close="colseSubscription" :softwareData="{id: softwareId}"></SubscriptionModal>
</d-modal>
</div>
</div>
<d-tabs class="jyh-tabs jyh-tabs-2" type="wrapped" v-model="selectTab">
<d-tab id="bbxx" title="版本信息"></d-tab>
<d-tab id="SBOM" title="SBOM"></d-tab>
<d-tab id="ldxx" title="漏洞信息"></d-tab>
<d-tab id="Malware" title="恶意代码信息" />
<d-tab id="ai" title="AI修复建议"></d-tab>
<d-tab id="licenseInfo" title="许可证信息"></d-tab>
<d-tab id="communityInfo" title="开发者社区信息"></d-tab>
<!-- <d-tab id="DeepDetectionAnalysis" title="深度检测分析" />-->
</d-tabs>
</div>
<div class="version-detail-content">
<version-info :ReleaseInfo="ReleaseInfo" v-if="selectTab === 'bbxx'" @changeTabs="changeTabs"></version-info>
<vulnerabilities-list :versionId="versionId" v-else-if="selectTab === 'ldxx'"></vulnerabilities-list>
<community-info :versionId="versionId" :ReleaseInfo="ReleaseInfo" v-else-if="selectTab === 'communityInfo'"></community-info>
<Dependence :versionId="versionId" v-else-if="selectTab === 'ylgx'"></Dependence>
<SBOM :versionId="versionId" v-else-if="selectTab === 'SBOM'" />
<AIRepair :versionId="versionId" v-else-if="selectTab === 'ai'"></AIRepair>
<LicenseInfo :versionId="versionId" v-else-if="selectTab === 'licenseInfo'"></LicenseInfo>
<Malware :versionId="versionId" v-else-if="selectTab === 'Malware'" />
<!-- <DeepDetectionAnalysis :versionId="versionId" v-else-if="selectTab === 'DeepDetectionAnalysis'" />-->
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, watch, onMounted,defineProps,defineExpose } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
import VulnerabilitiesList from './VulnerabilitiesList.vue';
import CommunityInfo from './CommunityInfo.vue';
import VersionInfo from './VersionInfo.vue';
import SBOM from './SBOM.vue';
import AIRepair from './AIRepair/index.vue';
import LicenseInfo from './LicenseInfo/index.vue';
import Dependence from './Dependency.vue'
import Malware from "@/views/Jyh/Version/Detail/Malware/Malware.vue";
import DeepDetectionAnalysis from "@/views/Jyh/Version/Detail/DeepDetectionAnalysis/index.vue";
const { namespace } = getOrgInfo();
const route = useRoute(); // 获取路由对象
const avatarUrl = '/src/assets/imgs/jyh/工程首字母图标.png';
import SubscriptionModal from '@/views/Jyh/Version/Detail/SubscriptionModal.vue';
import { alertIsSubscribe, releaseInfo } from '@/api/jyh';
const selectTab = ref('bbxx');
const softwareId = ref();
// 删除弹窗
const item = ref('');
const deleteModal = ref();
const deletevModels = ref(false);
const isSub = ref(false);
const ReleaseInfo = ref({
basicInfo: {},
copyRightReportList: [],
entityList: [],
networkSecurity: {},
openSourceLicenseList: [],
referenceInfo: {},
});
const props = defineProps(['versionId']);
const versionId = props.versionId; // 获取路由参数 versionId
const jcfxList = ['','低','中','高'];
const getReleaseInfo = async(versionId) => {
const { data, error } = await releaseInfo({ softwareId: versionId });
if (!error && data) {
ReleaseInfo.value = data.data;
}
};
const getAlertIsSubscribe = async(versionId) => {
const { data, error } = await alertIsSubscribe({ softwareId: versionId });
if (!error) {
if (data.data) {
isSub.value = data.data.data
} else {
isSub.value = false;
}
}
};
onMounted(() => {
softwareId.value = versionId;
if (versionId) {
getReleaseInfo(versionId); // 调用接口获取数据
getAlertIsSubscribe(versionId); // 调用接口获取数据
}
});
// 监听路由的 query.tab 变化
watch(
() => route.query.tab,
(newTab) => {
if (newTab) {
selectTab.value = newTab; // 根据 query 参数激活 Tab
}
},
{ immediate: true } // 立即执行一次
);
const changeTabs = (tab:string) => {
selectTab.value = tab;
}
const openDelete = (row: any) => {
item.value = row;
deleteModal.value.showFlag = true;
deletevModels.value = true;
};
const router = useRouter();
// 分页
const changeIndex = () => {
// getGroupClaListData();
};
const changeSize = () => {
pager.value.pageIndex = 1;
// getGroupClaListData();
};
const repoFun = (arr: any) => {
return arr ? (arr.length > 3 ? arr.slice(0, 3) : arr) : [];
};
// 编辑
const editRow = (row: any) => {
// 后续请换成params暂时params传参没传过去
router.push({
path: 'cla/edit/' + row.object_id,
query: { cla: row.cla, pageType: 'edit' }
});
};
// 确认删除
const confirmDelete = async (e: any) => {
// const res = await reqCatch(deleteGroupCla, { group_id: namespace.value, cla_id: item.value.object_id });
// if (!res.error) {
// getGroupClaListData();
// }
};
// 去成员页
const goMember = (row: any) => {
// 后续请换成params暂时params传参没传过去,注意路由有个:claID
router.push({
path: 'cla/claId/member',
query: { claId: row.object_id, claName: row.cla_name, group_path: namespace.value, pageType: 'edit' }
});
};
const getGroupClaListData = async () => {
// const params = {
// group_id: namespace.value,
// page: pager.value.pageIndex,
// per_page: pager.value.pageSize
// };
// const res = await reqCatch(getGroupClaList, params);
// if (!res.error) {
// tableData.value = res?.data?.data?.content;
// pager.value.total = res.data.data.total;
// }
};
getGroupClaListData();
const setClaStatus = async (row: any) => {
// const params = {
// group_id: namespace.value,
// cla_id: row.object_id,
// status: row.status
// };
// const res = await reqCatch(setGroupClaStatus, params);
// if (!res.error) {
// Message.success('操作成功!');
// getGroupClaListData();
// }
};
const signCla = (row: any) => {
router.push('/cla/' + row.object_id);
};
const subscribeModalVisable = ref(false);
const openSubscription = () => {
subscribeModalVisable.value = true;
};
const colseSubscription = () => {
subscribeModalVisable.value = false;
};
// 使用 defineExpose 暴露方法
defineExpose({
openSubscription
});
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.version-detail {
position: relative;
.secret-op {
position: absolute;
right: 0;
top: 0;
}
}
.split {
height: 60%;
background-color: #ddd;
}
.error {
color: $devui-contrast;
}
:deep(.devui-table__row td) {
padding-top: 16px;
padding-bottom: 16px;
}
:deep(.devui-tag .devui-tag--default) {
background-color: #fff;
border-radius: 12px;
padding: 4px 8px;
font-size: 12px;
line-height: 16px;
margin-right: 8px;
border: 1px solid #e3e3ee;
}
.one-line {
word-break: break-all;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.title {
font-size: 14px;
font-weight: 400;
color: #9a9b9c;
line-height: 20px;
}
.cur-title {
font-size: 14px;
font-weight: 500;
color: #2d2d2e;
line-height: 20px;
}
.search-form {
position: relative;
padding: 12px 20px 12px 0;
border-radius: 8px 0px 8px 8px;
background: #f2f5fc;
.output-btn {
position: absolute;
right: 28px;
bottom: 4px;
color: #191919;
font-family: HarmonyOS Sans SC;
font-weight: regular;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
}
}
.secret-breadcrumb {
padding: 7px 20px 12px;
background: #ffffff;
}
.version-detail-header {
background: #ffffff;
//padding: 0 12%;
width: 100%;
//box-shadow: inset 0px -1px 0px 0px #EEF0F5;
.header-top {
display: flex;
width: 100%;
}
.header-title1 {
color: #191919;
font-family: HarmonyOS Sans;
font-weight: bold;
font-size: 18px;
line-height: 26px;
letter-spacing: 0px;
text-align: left;
margin-bottom: 4px;
}
.header-div {
display: flex;
align-items: center;
height: 20px;
margin-bottom: 12px;
.span1 {
color: #808080;
font-family: HarmonyOS Sans SC;
font-weight: regular;
font-size: 12px;
line-height: 20px;
letter-spacing: 0px;
text-align: left;
}
.span2 {
color: #191919;
font-family: HarmonyOS Sans;
font-weight: regular;
font-size: 12px;
line-height: 20px;
letter-spacing: 0px;
text-align: left;
}
::v-deep .devui-tag {
.devui-tag--md {
color: #f6933b;
font-family: HarmonyOS Sans SC;
font-weight: regular;
font-size: 12px;
line-height: 20px;
letter-spacing: 0px;
text-align: left;
height: 20px;
}
}
}
}
.version-detail-content {
//padding: 0 12%;
width: 100%;
margin-top: 12px;
}
.subscribe-modal {
width: auto;
.subscribe-header {
display: flex;
align-items: center;
padding: 20px 20px 0;
>span {
margin-left: 8px;
color: #191919;
font-family: HarmonyOS Sans SC;
font-weight: medium;
font-size: 18px;
line-height: 26px;
}
}
}
</style>
<style lang="scss">
</style>

View File

@@ -18,7 +18,10 @@
</div>
<!--开源软件列表-->
<OssList v-if="msg.type===1"/>
<!--预警列表-->
<WarningList v-else-if="msg.type===2"/>
<!--软件详情-->
<OssDetail v-else-if="msg.type===3"/>
<div class="conversation-cnxw">
<a class="flex conversation-cnxw-item">
@@ -136,6 +139,7 @@ import SwiperComponent from './SwiperComponent.vue';
import GuessYouWantToAsk from './GuessYouWantToAsk.vue';
import OssList from './components/OssList.vue';
import WarningList from './components/WarningList.vue';
import OssDetail from './components/OssDetail.vue';
const inputValue = ref('');
const startChat = ref(false);
@@ -195,7 +199,7 @@ const onSubmit = (e, answer = undefined) => {
});
});
// getAIAnswer(answer ?? e);
getAIAnswer(mockAnswer,2);
getAIAnswer(mockAnswer,3);
};
const getAIAnswer = (content,type) => {

View File

@@ -958,10 +958,10 @@
resolved "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz"
integrity sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==
"@devui-design/icons@^1.3.0":
version "1.3.0"
resolved "https://registry.npmjs.org/@devui-design/icons/-/icons-1.3.0.tgz"
integrity sha512-eg9PcDXYn1BaUCo6qP7dszFJ4uRycxASLdKzpz27AW34HbHx4kFce36Fhfr2niRjGKYllcMgYe7hgP5KYVOZ8Q==
"@devui-design/icons@^1.3.0", "@devui-design/icons@^1.4.0":
version "1.4.0"
resolved "https://mirrors.cloud.tencent.com/npm/@devui-design/icons/-/icons-1.4.0.tgz"
integrity sha512-taAX1RNW0QHUqQTRPqLTYTB2PZIqUplhWeF4hcmWkSTjpWlDNI40DssG/WRb3sISkfBk/4BMUxxC5XeTL3jo7A==
"@esbuild/win32-x64@0.18.20":
version "0.18.20"