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

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

View File

@@ -0,0 +1,279 @@
<template>
<d-modal
v-model="visible"
title="AI问答"
@ok="handleClose"
:cancel="handleClose"
style="width: 900px;"
>
<div class="log-content">
<McLayout>
<!-- <McLayoutHeader>
<McHeader :logoImg="'/logo.svg'" :title="'MateChat'"></McHeader>
</McLayoutHeader>-->
<McLayoutContent v-if="visible" style="margin: 16px 0;">
<div ref="conversationRef" class="conversation-area">
<!-- 欢迎提示 -->
<div v-if="messages.length === 0" class="welcome-message">
<div class="welcome-page">
<div class="home-title">
<img src="@/assets/imgs/jyh/aiLogo2.png" alt="logo" />
<h1>可信开源代码库AI助手</h1>
</div>
<div class="home-detail">
<div class="home-detail-1">Hi欢迎使用可信开源代码库AI助手</div>
<div class="home-detail-2">可信开源代码库AI助手 可以辅助研发人员进行软件查询批量校验查看软件详情等</div>
<div class="home-detail-2">作为AI模型可信开源代码库AI助手 提供的答案可能不总是确定或准确的但您的反馈可以帮助 可信开源代码库AI助手 做得更好</div>
</div>
</div>
<!-- <div class="welcome-icon">-->
<!-- <i class="fa fa-comment-dots"></i>-->
<!-- </div>-->
<!-- <div class="welcome-text">-->
<!-- <h3>请输入你想问的内容</h3>-->
<!-- <p>与AI助手开始对话获取智能回答</p>-->
<!-- </div>-->
</div>
<template v-for="(msg, idx) in messages" :key="idx">
<McBubble v-if="msg.role === 'user'" :content="msg.content" :align="'right'"></McBubble>
<McBubble v-else :loading="msg.loading ?? false">
<McMarkdownCard :content="msg.content" :theme="theme"></McMarkdownCard>
</McBubble>
</template>
</div>
</McLayoutContent>
<McLayoutSender>
<McInput :value="inputValue" :maxLength="2000" @submit="onSubmit" showCount></McInput>
</McLayoutSender>
</McLayout>
</div>
</d-modal>
</template>
<script setup>
import { ref, watch,nextTick } from 'vue';
import { Message } from 'vue-devui';
import axios from 'axios';
import {uvdV1Chat} from "@/api/inboundTask/inboundTaskApi";
const messages = ref([]);
const history = ref([]);
const resultInfo = ref(null);
const inputValue = ref('');
const conversationRef = ref(null);
// 组件状态
const visible = ref(false);
const VITE_AI_API_HOST = import.meta.env.VITE_AI_API_HOST;
// 打开弹框时调用接口
const open = async (id) => {
if (!id) return Message.error('缺少result_id参数');
visible.value = true;
resultInfo.value = id
};
const onSubmit = (e) => {
if(e === '') {
return;
}
if(messages.value[messages.value.length-1]?.loading) return;
inputValue.value = '';
messages.value.push({
role: 'user',
content: e,
});
nextTick(() => {
conversationRef.value?.scrollTo({
top: conversationRef.value.scrollHeight,
behavior: 'smooth',
});
});
getAIAnswer(e);
};
const getAIAnswer = async (content) => {
messages.value.push({
role: 'assistant',
content: '',
loading: true,
});
const res = await uvdV1Chat({
"resultInfo": resultInfo.value,
"query": content,
"history": history.value || []
});
let reply = res?.data?.data?.reply || '';
history.value.push({role: "user", content: content});
history.value.push({role: "assistant", content: reply});
messages.value[messages.value.length - 1].content = reply;
messages.value[messages.value.length - 1].loading = false;
nextTick(() => {
conversationRef.value?.scrollTo({
top: conversationRef.value.scrollHeight
});
});
/* 模拟流式数据返回 */
// setTimeout(async () => {
// messages.value.at(-1).loading = false;
// for (let i = 0; i < reply.length;) {
// await new Promise(r => setTimeout(r, 300 * Math.random()));
// messages.value[messages.value.length - 1].content = reply.slice(0, i += Math.random() * 10);
// nextTick(() => {
// conversationRef.value?.scrollTo({
// top: conversationRef.value.scrollHeight
// });
// });
// }
// }, 1000);
};
// 暴露给父组件的方法
defineExpose({
open // 通过ref调用open方法传递resultId
});
// 关闭弹框
const handleClose = () => {
messages.value = [];
history.value = [];
result_id.value = null;
inputValue.value = '';
visible.value = false;
};
</script>
<style lang="scss" scoped>
.log-content {
padding: 24px;
line-height: 1.6;
white-space: pre-wrap; // 保留日志换行
word-break: break-all; // 自动换行长单词
min-height: 200px;
p {
margin: 0;
color: #333;
}
}
.conversation-area,
.welcome-page {
flex: 1;
display: flex;
flex-direction: column;
overflow: auto;
padding: 0 12px;
min-height: 400px;
max-height: 600px;
overflow-y: auto;
}
.conversation-area {
gap: 8px;
}
.welcome-message {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: 32px;
text-align: center;
.welcome-icon {
width: 64px;
height: 64px;
background-color: #e6f4ff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
i {
font-size: 28px;
color: #1677ff;
}
}
.welcome-text {
h3 {
font-size: 18px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
}
p {
font-size: 14px;
color: #666;
}
}
}
.home-title {
display: flex;
flex-shrink: 0;
justify-content: center;
align-items: center;
gap: 20px;
h1 {
color: #191919;
font-family: Huawei Sans;
font-weight: bold;
font-size: 32px;
line-height: 48px;
letter-spacing: 0px;
text-align: center;
}
img {
width: 64px;
//filter: brightness(0);
}
}
.home-detail {
.home-detail-1 {
color: #191919;
font-family: HarmonyOS Sans SC;
font-weight: medium;
font-size: 24px;
line-height: 32px;
letter-spacing: 0px;
text-align: center;
margin-bottom: 24px;
}
.home-detail-2 {
color: #777777;
font-family: HarmonyOS Sans SC;
font-weight: regular;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: center;
}
}
.welcome-page {
//height: 90%;
width: 100%;
flex: 1;
display: flex;
flex-direction: column;
overflow: auto;
padding: 0 12px;
}
.welcome-page {
gap: 16px;
justify-content: center;
}
</style>

View File

@@ -0,0 +1,605 @@
<template>
<div class="BatchVerify">
<div class="search-form" v-if="isLogin">
<FileUploadButton buttonText="批量导入匹配" accept=".xlsx" :onFileUpload="handleFileUpload" />
<d-search
class="mt-0 mb-2"
style="width: 400px"
:delay="1000"
@search="onSearch($event)"
placeholder="请输入软件名称"
></d-search>
<div class="tjsj">
<span style="height: 14px" class="split"></span>
<span>可使用</span>
<span class="green" @click="filterVerifyResult(1)">{{ useNum }}</span>
<span style="height: 14px" class="split"></span>
<span>有风险</span>
<span class="red" @click="filterVerifyResult(2)">{{ riskNum }}</span>
<span style="height: 14px" class="split"></span>
<span>未治理</span>
<span class="yellow" @click="filterVerifyResult(0)">{{ noNum }}</span>
</div>
<div class="left-btn">
<a v-if="isLogin" class="cursor-pointer" href="/file/软件存在及可信校验模版.xlsx" download
><i class="icon icon-download-2"></i>下载excel模板</a
>
<span v-if="isLogin" style="height: 14px" class="split"></span>
<d-button v-if="isLogin" class="output-btn" icon="icon-share" variant="text" @click="exportResult"
>导出</d-button
>
<span v-if="isLogin" style="height: 14px" class="split"></span>
<TableSetting
:columns="columns"
@setDefult="setDefult"
@updateTableKey="updateTableKey"
:show-column-list="showColumnList"
@update:show-column-list="updateShowColumnList"
/>
</div>
</div>
<div class="mt-3 px-20">
<d-table
class="jyh-table"
v-if="tableData.length"
:striped="false"
:data="tableData"
table-layout="auto"
:show-loading="loading"
>
<!-- 索引列 -->
<d-column type="index" width="40"></d-column>
<!-- 动态渲染列 -->
<template v-for="column in columns" :key="column.key">
<d-column v-if="column.visible" :field="column.key" :header="column.label">
<!-- 自定义列内容 -->
<template #default="scope">
<template v-if="column.key === 'validStatus'">
<div v-if="scope.row.validStatus == 1" style="display: flex; align-items: center">
<d-icon class="mr-1" name="icon-right-o" style="font-size: 14px; color: rgb(80, 212, 171)"></d-icon>
<span>可使用</span>
</div>
<div v-else-if="scope.row.validStatus == 2" style="display: flex; align-items: center">
<d-icon class="mr-1" name="icon-warning-o" style="font-size: 14px; color: rgb(250, 194, 10)"></d-icon>
<span>有风险</span>
</div>
<div v-else style="display: flex; align-items: center">
<d-icon class="mr-1" name="icon-error-o" style="font-size: 14px; color: rgb(246, 111, 106)"></d-icon>
<span>未治理</span>
</div>
</template>
<template v-else-if="column.key === 'versionName'">
<a @click="gotoDetail(scope.row)">{{ scope.row.versionName }}</a>
</template>
<template v-else-if="column.key === 'softwareName'">
<a v-if="scope.row.validStatus===1" @click="gotoDetail(scope.row)">{{ scope.row.softwareName }}</a>
<span v-else>{{ scope.row.softwareName }}</span>
</template>
<template v-else-if="column.key === 'softwareVersion'">
<a v-if="scope.row.validStatus===1" @click="gotoDetail(scope.row)">{{ scope.row.softwareVersion }}</a>
<span v-else>{{ scope.row.softwareVersion }}</span>
</template>
<template v-else-if="column.key === 'versionCode'">
<a @click="gotoDetail(scope.row)">{{ scope.row.versionCode }}</a>
</template>
<template v-else-if="column.key === 'smStatus'">
<a>{{ scope.row.smStatus }}</a>
</template>
<template v-else-if="column.key === 'type'">
<d-tag type="success">{{ scope.row.type }}</d-tag>
</template>
<template v-else-if="column.key === 'license'">
<span class="mr-2">{{ scope.row.license }}</span>
<d-tag color="gray">+3</d-tag>
</template>
<template v-else-if="column.key === 'url'">
<a>{{ scope.row.url }}</a>
</template>
<template v-else>
{{ scope.row[column.key] }}
</template>
</template>
</d-column>
</template>
<d-column header="操作" width="200" fixed-right="0px">
<!--TODO:申请入口只有校验不通过的才显示-->
<template #default="scope">
<d-button @click="showApplyDialog(scope.row)" variant="text" v-if="scope.row.validStatus == 0" :disabled="scope.row.validStatus !== 0">
申请入库
</d-button>
</template>
</d-column>
</d-table>
<!-- <DataPanel v-else skeleton :card="false" :empty="true">-->
<!-- </DataPanel>-->
<NoData v-else :small="false"></NoData>
</div>
<!-- 新增申请入库提示弹窗 -->
<d-modal v-model="applyDialogVisible" title="申请入库提示" style="width: 400px">
<div class="text-G900 text-sm leading-[24px]">
如需提交治理申请请邮件联系平台运营团队<br>
<a class="text-blue-500 cursor-pointer" @click="openEmailClient">luoxunzhao@jyhlab.org.cn</a>
</div>
<template #footer>
<d-modal-footer style="text-align: right; padding-right: 20px;">
<d-button @click="hidden">取消</d-button>
<d-button @click="hidden">确认</d-button>
</d-modal-footer>
</template>
</d-modal>
<!-- 删除组件 -->
<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>
</div>
</template>
<script setup lang="ts">
import SettingTitle from '@/components/Setting/SettingTitle/index.vue';
import SettingText from '@/components/Setting/SettingText/index.vue';
import { ref, reactive, onMounted, computed } 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 { useAccountStore } from '@/stores/user';
import { storeToRefs } from 'pinia';
import TableSetting from '@/components/TableSetting/index.vue';
import FileUploadButton from '@/components/FileUploadButton/index.vue';
import { releasePage, repoImport } from '@/api/jyh';
import NoData from '@/components/NoData/NoData.vue';
import * as XLSX from 'xlsx';
const hidden = () => {
applyDialogVisible.value = false;
};
// 新增弹窗状态
const applyDialogVisible = ref(false);
// 显示申请入库提示弹窗
const showApplyDialog = (row: any) => {
applyDialogVisible.value = true;
};
// 打开邮件客户端
const openEmailClient = () => {
try {
window.location.href = 'mailto:luoxunzhao@jyhlab.org.cn';
} catch (error) {
Message.error('无法打开邮件客户端,请手动复制邮箱地址联系');
}
};
const downloadTemplate = async () => {
try {
const response = await fetch('/static/软件存在及可信校验模版.xlsx');
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = '软件存在及可信校验模版.xlsx';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.error('下载失败:', error);
}
};
const useNum = computed(() => {
if (tableData.value.length > 0) {
return tableData.value.filter((item) => item.validStatus === 1).length;
} else {
return 0;
}
});
const riskNum = computed(() => {
if (tableData.value.length > 0) {
return tableData.value.filter((item) => item.validStatus === 2).length;
} else {
return 0;
}
});
const noNum = computed(() => {
if (tableData.value.length > 0) {
return tableData.value.filter((item) => item.validStatus === 0).length;
} else {
return 0;
}
});
const setDefult = () => {
columns.value = defultColumns.value;
};
// 处理文件上传
const handleFileUpload = async (file) => {
// 1. 校验文件类型
const allowedExtensions = ['.xlsx'];
const fileExtension = file.name.slice(file.name.lastIndexOf('.')).toLowerCase();
if (!allowedExtensions.includes(fileExtension)) {
Message.error('仅支持上传 .xlsx 格式的Excel文件');
return Promise.reject(new Error('文件格式不正确'));
}
const { data, error } = await repoImport({ file });
if (!error && data) {
currentReusltFilter.value = null;
allTableData.value = data.data.data;
tableData.value = [...allTableData.value];
}
// 模拟文件上传逻辑
return new Promise((resolve) => {
setTimeout(() => {
console.log('文件上传成功:', file.name);
resolve();
}, 2000); // 模拟 2 秒上传时间
});
};
const userInfo = useAccountStore();
const { isLogin } = storeToRefs(userInfo);
const { namespace } = getOrgInfo();
const allTableData = ref([]);
const tableData = ref([]);
const loading = ref(false);
const columns = ref([
{ key: 'validStatus', label: '校验结果', visible: true, filterable: true, sortable: false },
{ key: 'softwareName', label: '软件名称', visible: true, filterable: true, sortable: false },
{ key: 'softwareVersion', label: '软件版本', visible: true, filterable: true, sortable: false },
{ key: 'securityScore', label: '软件评分', visible: true, filterable: false, sortable: true },
{ key: 'licenseCount', label: 'License数量', visible: true, filterable: false, sortable: false },
{ key: 'industryCategory', label: '行业类型', visible: true, filterable: true, sortable: false },
{ key: 'primaryLanguage', label: '编程语言', visible: true, filterable: true, sortable: false },
{ key: 'codeLines', label: '代码量', visible: true, filterable: false, sortable: false }
]);
const defultColumns = ref([
{ key: 'validStatus', label: '校验结果', visible: true, filterable: true, sortable: false },
{ key: 'softwareName', label: '软件名称', visible: true, filterable: true, sortable: false },
{ key: 'softwareVersion', label: '软件版本', visible: true, filterable: true, sortable: false },
{ key: 'securityScore', label: '软件评分', visible: true, filterable: false, sortable: true },
{ key: 'licenseCount', label: 'License数量', visible: true, filterable: false, sortable: false },
{ key: 'industryCategory', label: '行业类型', visible: true, filterable: true, sortable: false },
{ key: 'primaryLanguage', label: '编程语言', visible: true, filterable: true, sortable: false },
{ key: 'codeLines', label: '代码量', visible: true, filterable: false, sortable: false }
]);
const validStatusMap = {
0: '未治理',
1: '可使用',
2: '有风险'
};
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 currentReusltFilter = ref(null);
const searchStr = ref('');
// 定义 tableKey用于强制刷新表格
const tableKey = ref(0);
const pager = ref({
total: 0,
pageIndex: 1,
pageSize: 10
});
const loadingStatus = reactive({
batchMigrating: false
});
// 删除弹窗
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 getReleaseList = async () => {
if (!isLogin.value) {
return;
}
loading.value = true;
const { data, error } = await releasePage({ current: pager.value.pageIndex, size: pager.value.pageSize });
if (!error && data) {
loading.value = false;
allTableData.value = data.data.records;
tableData.value = [...allTableData.value];
pager.value.total = data.data.total;
} else {
loading.value = false;
}
};
onMounted(async () => {
// getReleaseList();
});
const filterTable = () => {
tableData.value = allTableData.value.filter((item) => {
if (currentReusltFilter.value !== null) {
return (
item.softwareName.trim().toLowerCase().includes(searchStr.value.trim().toLowerCase()) &&
item.validStatus === currentReusltFilter.value
);
} else {
return item.softwareName.trim().toLowerCase().includes(searchStr.value.trim().toLowerCase());
}
});
};
const filterVerifyResult = (type) => {
if (currentReusltFilter.value === type) {
currentReusltFilter.value = null;
} else {
currentReusltFilter.value = type;
}
filterTable();
};
const onSearch = (str: string) => {
searchStr.value = str;
filterTable();
};
const router = useRouter();
const updateTableKey = () => {
tableKey.value++; // 更新 tableKey强制刷新表格
};
// 定义 showColumnList 状态
const showColumnList = ref(false);
// 更新 showColumnList 的方法
const updateShowColumnList = (newValue) => {
showColumnList.value = newValue;
// tableKey.value++; // 更新 tableKey强制刷新表格
};
const handleBatchImport = async () => {
// 多个项目导入
// importRepo.value = queryTable.value.filter((val) => val.checked);
loadingStatus.batchMigrating = true;
// await batchMigrate();
loadingStatus.batchMigrating = false;
};
const exportResult = () => {
if (!allTableData.value.length) {
Message.info('暂无结果');
return;
}
const exportData = allTableData.value.map((item) => {
let res = {};
columns.value.forEach((col) => {
if (col.key === 'validStatus') {
res[col.label] = validStatusMap[item[col.key]] ? validStatusMap[item[col.key]] : '';
} else {
res[col.label] = item[col.key] ? item[col.key] : '';
}
});
return res;
});
try {
const workSheet = XLSX.utils.json_to_sheet(exportData);
const workBook: any = { Sheets: { 校验结果: workSheet }, SheetNames: ['校验结果'] };
const excelBuffer = XLSX.write(workBook, { bookType: 'xlsx', type: 'array' });
const link = document.createElement('a');
const blob = new Blob([excelBuffer]);
link.setAttribute('href', window.URL.createObjectURL(blob));
link.setAttribute('download', '软件存在及可信校验结果.xlsx');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
Message.success('导出成功');
} catch {
// Message.error('导出失败');
}
};
// 跳转
const gotoDetail = (row) => {
// 后续请换成params暂时params传参没传过去
router.push({
path: 'repoDetail/' + row.softwareId
});
};
// 分页
const changeIndex = () => {
// getReleaseList();
};
const changeSize = () => {
pager.value.pageIndex = 1;
// getReleaseList();
};
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);
};
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.BatchVerify {
position: relative;
min-height: 400px;
.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;
}
.search-form {
position: relative;
padding: 12px 20px;
display: flex;
border-radius: 8px 0px 8px 8px;
//background: #F2F5FC;
.left-btn {
position: absolute;
right: 28px;
bottom: 4px;
}
.output-btn {
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;
}
.tjsj {
display: flex;
align-items: center;
margin-left: 8px;
}
</style>

View File

@@ -0,0 +1,84 @@
<template>
<d-modal
title="贡献者分布"
v-model="visible"
class="file-search-modal"
:style="{ width: '800px' }"
@close="handleClose"
>
<Card simple class="jyh-card mb-4" style="box-shadow: none;padding: 0">
<d-chart :option="optionConChart" style="width: 100%; height: 400px"></d-chart>
<p class="text-center mt-translate-y">国家/地球分布</p>
</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>
</template>
<script setup lang="ts">
import { ref, defineExpose, reactive } from 'vue';
import { DChart } from 'vue-devui/echarts';
const optionConChart = reactive({
tooltip: {
show: true,
trigger: 'item'
},
xAxis: {
type: 'category',
data: ['美国', '中国', '德国', '瑞士', '法国', '英国', '印度', '日本', '荷兰', '俄罗斯']
},
yAxis: {
// 给y轴添加单位后缀
axisLabel: {
formatter: '{value}%'
},
name: '单位',
type: 'value'
},
series: [
{
data: [12, 14.8, 23.6, 18.6, 17.3, 13.1, 16, 11.8, 13.9, 16.4],
type: 'bar',
barWidth: '40%',
// 给label添加单位后缀
label: {
show: true,
position: 'top',
formatter: '{c}%'
},
itemStyle: {
color: '#2070F3',
borderRadius: [5, 5, 0, 0]
}
}
]
});
// 控制模态框显示/隐藏
const visible = ref(false);
// 打开模态框
const openModal = () => {
visible.value = true;
};
// 关闭模态框
const handleClose = () => {
visible.value = false;
};
// 暴露方法,供父组件调用
defineExpose({
openModal,
});
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
</style>

View File

@@ -0,0 +1,295 @@
<template>
<div class="intelligence-detail">
<!-- 头部信息 -->
<div class="detail-header">
<h2 class="title">{{ intelligence?.title }}</h2>
<div class="header-tags">
<d-tag :color="typeColorMap[intelligence?.intelligenceType]">
{{ intelligence?.intelligenceType }}
</d-tag>
<d-tag :color="urgencyColorMap[intelligence?.urgency]">
{{ intelligence?.urgency }}
</d-tag>
</div>
</div>
<!-- 基本信息卡片 -->
<d-card class="detail-card mt-4">
<div slot="header" class="card-header">基本信息</div>
<d-descriptions column="2" bordered>
<d-description-item term="情报ID">{{ intelligence?.intelligenceId }}</d-description-item>
<d-description-item term="情报类型">{{ intelligence?.intelligenceType }}</d-description-item>
<d-description-item term="情报来源">{{ intelligence?.source }}</d-description-item>
<d-description-item term="发布时间">
{{ intelligence?.publishDate ? dayjs(intelligence.publishDate).format('YYYY-MM-DD HH:mm:ss') : '--' }}
</d-description-item>
<d-description-item term="紧急程度">{{ intelligence?.urgency }}</d-description-item>
<d-description-item term="情报版本">{{ intelligence?.version || '1.0' }}</d-description-item>
</d-descriptions>
</d-card>
<!-- 情报摘要 -->
<d-card class="detail-card mt-4">
<div slot="header" class="card-header">情报摘要</div>
<div class="summary-content">
{{ intelligence?.summary || '暂无摘要信息' }}
</div>
</d-card>
<!-- 详细内容 -->
<d-card class="detail-card mt-4">
<div slot="header" class="card-header">
详细内容
<d-button size="sm" variant="text" @click="toggleContentExpand">
{{ isContentExpanded ? '收起' : '全文展开' }}
</d-button>
</div>
<div class="detail-content" :class="{ 'expanded': isContentExpanded }">
<pre v-if="intelligence?.content">{{ formatContent(intelligence.content) }}</pre>
<p v-else>暂无详细内容</p>
</div>
</d-card>
<!-- 相关IOCs -->
<d-card class="detail-card mt-4" v-if="intelligence?.relatedIocs && intelligence.relatedIocs.length">
<div slot="header" class="card-header">
相关IOCs
<d-button size="sm" variant="text" @click="copyAllIocs">
<i class="fa fa-copy mr-1"></i>复制全部
</d-button>
</div>
<d-table :data="intelligence.relatedIocs" table-layout="auto">
<d-column field="type" header="类型" :width="100"></d-column>
<d-column field="value" header="值"></d-column>
<d-column header="操作" :width="100">
<template #default="scope">
<d-button
size="sm"
variant="text"
@click="copyIoc(scope.row.value)"
>
<i class="fa fa-copy"></i>复制
</d-button>
</template>
</d-column>
</d-table>
</d-card>
<!-- 参考信息 -->
<d-card class="detail-card mt-4" v-if="intelligence?.references && intelligence.references.length">
<div slot="header" class="card-header">参考信息</div>
<ul class="reference-list">
<li v-for="(ref, idx) in intelligence.references" :key="idx">
<a :href="ref.url" target="_blank" class="reference-link">
{{ ref.title || `参考链接 ${idx + 1}` }}
</a>
</li>
</ul>
</d-card>
<!-- 操作按钮 -->
<div class="detail-actions mt-6">
<d-button @click="$emit('close')" variant="secondary">关闭</d-button>
<d-button @click="exportReport" class="ml-2">
<i class="fa fa-download mr-1"></i>导出报告
</d-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import dayjs from 'dayjs';
// 接收父组件传入的情报数据
const props = defineProps({
intelligence: {
type: Object,
required: true,
default: () => ({})
}
});
// 组件内部状态
const isContentExpanded = ref(false);
const toast = ref();
// 类型颜色映射(与列表页保持一致)
const typeColorMap = {
'威胁情报': '#165DFF',
'漏洞情报': '#f66f6a',
'事件情报': '#fac20a',
'APT情报': '#722ED1',
'恶意代码情报': '#f5319d'
};
// 紧急程度颜色映射(与列表页保持一致)
const urgencyColorMap = {
'紧急': '#c7000b',
'高': '#f66f6a',
'中': '#fac20a',
'低': '#5e7ce0'
};
// 切换内容展开/收起状态
const toggleContentExpand = () => {
isContentExpanded.value = !isContentExpanded.value;
};
// 格式化内容(处理换行和缩进)
const formatContent = (content: string) => {
return content.replace(/\\n/g, '\n').replace(/ /g, ' ');
};
// 复制单个IOC
const copyIoc = (value: string) => {
navigator.clipboard.writeText(value).then(() => {
toast.success({ content: '已复制到剪贴板', duration: 2000 });
}).catch(() => {
toast.error({ content: '复制失败,请手动复制', duration: 2000 });
});
};
// 复制所有IOCs
const copyAllIocs = () => {
if (!props.intelligence.relatedIocs || !props.intelligence.relatedIocs.length) return;
const allIocs = props.intelligence.relatedIocs
.map(item => `${item.type}: ${item.value}`)
.join('\n');
navigator.clipboard.writeText(allIocs).then(() => {
toast.success({ content: '全部IOC已复制到剪贴板', duration: 2000 });
}).catch(() => {
toast.error({ content: '复制失败,请手动复制', duration: 2000 });
});
};
// 导出报告
const exportReport = () => {
// 模拟导出功能
toast.info({ content: '正在准备导出报告...', duration: 2000 });
// 实际应用中应调用后端导出接口
// const reportData = { ...props.intelligence };
// exportIntelligenceReport(reportData).then(() => {
// toast.success({ content: '报告导出成功', duration: 2000 });
// });
};
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.intelligence-detail {
padding: 20px;
height: 100%;
box-sizing: border-box;
overflow-y: auto;
max-height: 80vh;
}
.detail-header {
display: flex;
flex-direction: column;
gap: 12px;
}
.title {
font-size: 20px;
font-weight: 600;
color: var(--devui-text-primary);
line-height: 1.5;
margin: 0;
}
.header-tags {
display: flex;
gap: 8px;
}
.detail-card {
--devui-card-padding: 16px;
}
.card-header {
font-size: 16px;
font-weight: 600;
color: var(--devui-text-primary);
display: flex;
justify-content: space-between;
align-items: center;
}
.summary-content {
padding: 8px 0;
line-height: 1.6;
color: var(--devui-text-secondary);
text-indent: 2em;
}
.detail-content {
max-height: 300px;
overflow-y: auto;
padding: 8px 0;
&.expanded {
max-height: none;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
line-height: 1.6;
color: var(--devui-text-secondary);
font-family: inherit;
margin: 0;
}
}
.reference-list {
padding: 8px 0 8px 20px;
margin: 0;
li {
margin-bottom: 8px;
&:last-child {
margin-bottom: 0;
}
}
}
.reference-link {
color: var(--devui-primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.detail-actions {
display: flex;
justify-content: flex-end;
//padding-top: 16px;
border-top: 1px solid var(--devui-border);
}
// 滚动条美化
.detail-content::-webkit-scrollbar,
.intelligence-detail::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.detail-content::-webkit-scrollbar-thumb,
.intelligence-detail::-webkit-scrollbar-thumb {
border-radius: 3px;
background-color: var(--devui-scrollbar-thumb-color);
}
.detail-content::-webkit-scrollbar-track,
.intelligence-detail::-webkit-scrollbar-track {
background-color: var(--devui-scrollbar-track-color);
}
</style>

View File

@@ -0,0 +1,327 @@
<template>
<div class="page-wrap">
<div class="search-form">
<d-form layout="horizontal" :label-align="'end'">
<d-row :gutter="16">
<d-col :span="6">
<d-form-item field="intelligenceId" label="情报ID">
<d-input placeholder="请输入情报ID" v-model="newFormData.intelligenceId" />
</d-form-item>
</d-col>
<d-col :span="6">
<d-form-item field="intelligenceType" label="情报类型">
<d-select
:options="typeArray"
:allow-clear="true"
v-model="newFormData.intelligenceType"
placeholder="请选择情报类型"
></d-select>
</d-form-item>
</d-col>
<d-col :span="6">
<d-form-item field="source" label="情报来源">
<d-select
:options="sourceArray"
:allow-clear="true"
v-model="newFormData.source"
placeholder="请选择情报来源"
></d-select>
</d-form-item>
</d-col>
<d-col :span="6">
<d-form-item field="urgency" label="紧急程度">
<d-select
:options="urgencyArray"
:allow-clear="true"
v-model="newFormData.urgency"
placeholder="请选择紧急程度"
></d-select>
</d-form-item>
</d-col>
</d-row>
</d-form>
<div class="jyh-form-operation">
<d-button @click="getList(1)" class="mr-2" variant="solid">搜索</d-button>
<d-button variant="solid" color="secondary" @click="clear">清空</d-button>
</div>
</div>
<Card class="mt-3">
<d-table :show-loading="loading" class="jyh-table" :data="dataLists" v-if="dataLists.length > 0" table-layout="auto">
<d-column type="index" width="40"></d-column>
<d-column field="intelligenceId" header="情报ID"></d-column>
<d-column field="title" header="情报标题"></d-column>
<d-column field="intelligenceType" header="情报类型">
<template #default="scope">
<d-tag :color="typeColorMap[scope.row.intelligenceType]">{{ scope.row.intelligenceType }}</d-tag>
</template>
</d-column>
<d-column field="source" header="情报来源"></d-column>
<d-column field="publishDate" header="发布时间">
<template #default="scope">
{{ scope.row.publishDate
? dayjs(scope.row.publishDate).format('YYYY-MM-DD HH:mm:ss')
: '--'
}}
</template>
</d-column>
<d-column field="urgency" header="紧急程度">
<template #default="scope">
<d-tag :color="urgencyColorMap[scope.row.urgency]">{{ scope.row.urgency }}</d-tag>
</template>
</d-column>
<d-column header="操作" fixed-right="0px" width="130">
<template #default="scope">
<a style="margin-right: 8px" class="devui-link" @click="openIntelligenceDetail(scope.row)">详情</a>
<d-button @click="handleViewAIModal(scope.row)" variant="text" color="primary"> AI问答 </d-button>
</template>
</d-column>
</d-table>
<NoData v-else :small="false"></NoData>
<div class="mt-20 mb-20 flex justify-end" v-if="dataLists.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="getList()"
@page-size-change="getList()"
/>
</div>
</Card>
</div>
<!-- 情报详情抽屉 -->
<d-drawer v-model="detailVisible" style="width: 900px">
<IntelligenceDetail :intelligence="currentIntelligence" @close="detailVisible = false"></IntelligenceDetail>
</d-drawer>
<AIModal ref="AIModalRef" />
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import NoData from '@/components/NoData/NoData.vue';
import dayjs from 'dayjs';
import AIModal from '../AIModal.vue';
// 假设存在情报详情组件
import IntelligenceDetail from './IntelligenceDetail.vue';
const route = useRoute();
const title = computed(() => route.meta?.title || '情报列表');
// 打开弹框方法
const AIModalRef = ref(null);
const handleViewAIModal = (resultId) => {
AIModalRef.value.open(resultId); // 传递result_id参数
};
// 分页配置
const pager = ref({
total: 50,
pageIndex: 1,
pageSize: 10
});
// 搜索表单数据
const newFormData = ref({
intelligenceId: '',
intelligenceType: '',
source: '',
urgency: ''
});
// 加载状态
const loading = ref(false);
// 情报列表数据
const dataLists = ref([]);
// 详情抽屉状态
const detailVisible = ref(false);
const currentIntelligence = ref(null);
// 类型选项与颜色映射
const typeArray = [
{ value: '威胁情报', name: '威胁情报' },
{ value: '漏洞情报', name: '漏洞情报' },
{ value: '事件情报', name: '事件情报' },
{ value: 'APT情报', name: 'APT情报' },
{ value: '恶意代码情报', name: '恶意代码情报' }
];
const typeColorMap = {
'威胁情报': '#165DFF',
'漏洞情报': '#f66f6a',
'事件情报': '#fac20a',
'APT情报': '#722ED1',
'恶意代码情报': '#f5319d'
};
// 来源选项
const sourceArray = [
{ value: '内部监测', name: '内部监测' },
{ value: '合作伙伴', name: '合作伙伴' },
{ value: '公开渠道', name: '公开渠道' },
{ value: '威胁情报平台', name: '威胁情报平台' },
{ value: 'CERT/CC', name: 'CERT/CC' }
];
// 紧急程度选项与颜色映射
const urgencyArray = [
{ value: '紧急', name: '紧急' },
{ value: '高', name: '高' },
{ value: '中', name: '中' },
{ value: '低', name: '低' }
];
const urgencyColorMap = {
'紧急': '#c7000b',
'高': '#f66f6a',
'中': '#fac20a',
'低': '#5e7ce0'
};
// 清空搜索条件
const clear = () => {
newFormData.value = {
intelligenceId: '',
intelligenceType: '',
source: '',
urgency: ''
};
getList();
};
// 打开情报详情
const openIntelligenceDetail = (row) => {
currentIntelligence.value = row;
detailVisible.value = true;
};
// 获取情报列表数据
const getList = async (current?) => {
loading.value = true;
if (current) pager.value.pageIndex = current;
// 模拟API请求
setTimeout(() => {
// 模拟情报数据
const mockData = Array.from({ length: pager.value.pageSize }, (_, i) => {
const id = (pager.value.pageIndex - 1) * pager.value.pageSize + i + 1;
const types = ['威胁情报', '漏洞情报', '事件情报', 'APT情报', '恶意代码情报'];
const sources = ['内部监测', '合作伙伴', '公开渠道', '威胁情报平台', 'CERT/CC'];
const urgencies = ['紧急', '高', '中', '低'];
const randomType = types[Math.floor(Math.random() * types.length)];
return {
id: id,
intelligenceId: `INT-${2024}${String(id).padStart(5, '0')}`,
title: `${randomType}:关于${['某APT组织', '新型勒索病毒', '重大漏洞', '网络攻击事件'][Math.floor(Math.random() * 4)]}的分析`,
intelligenceType: randomType,
source: sources[Math.floor(Math.random() * sources.length)],
publishDate: dayjs().subtract(Math.floor(Math.random() * 30), 'day').toISOString(),
urgency: urgencies[Math.floor(Math.random() * urgencies.length)],
summary: '这是一份重要的安全情报,包含潜在威胁的详细分析和应对建议...',
content: '【情报详情】\n近期监测到相关威胁活动频繁发生主要影响范围包括政府、金融和能源行业...\n【影响范围】\n...\n【应对建议】\n1. 及时更新系统补丁\n2. 加强网络边界防护\n3. 部署威胁检测设备',
relatedIocs: [
{ type: 'IP', value: `192.168.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}` },
{ type: '域名', value: `malicious-${id}.com` }
]
};
});
dataLists.value = mockData;
pager.value.total = 50; // 模拟总条数
loading.value = false;
}, 600);
// 实际接口调用示例
/*
const { data } = await getIntelligenceList({
intelligenceId: newFormData.value.intelligenceId,
intelligenceType: newFormData.value.intelligenceType,
source: newFormData.value.source,
urgency: newFormData.value.urgency,
current: current || pager.value.pageIndex,
size: pager.value.pageSize
}).finally(() => {
loading.value = false;
});
dataLists.value = data?.data?.data?.records || [];
pager.value.total = data.data.data?.total || 0;
*/
};
// 页面挂载时加载数据
onMounted(() => {
getList();
});
// 搜索触发
const search = () => {
pager.value.pageIndex = 1;
getList();
};
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.page-wrap {
margin: 16px;
.page-title {
color: #191919;
font-family: HarmonyOS Sans SC;
font-weight: medium;
font-size: 18px;
line-height: 26px;
letter-spacing: 0px;
text-align: left;
}
.g-content-card {
overflow: hidden;
padding: 0;
}
}
.search-form {
position: relative;
border-radius: 8px 0px 8px 8px;
margin-right: 20px;
.left-btn {
position: absolute;
right: 28px;
bottom: 4px;
}
.output-btn {
color: #191919;
font-family: HarmonyOS Sans SC;
font-weight: regular;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
}
.jyh-form-operation {
margin-left: 20px !important;
margin-top: 16px;
}
}
// 适配情报标题较长的情况
::v-deep .devui-table-cell {
&.devui-table-cell-ellipsis {
max-width: 250px;
}
}
</style>

View File

@@ -0,0 +1,84 @@
<template>
<d-modal
title="贡献者分布"
v-model="visible"
class="file-search-modal"
:style="{ width: '800px' }"
@close="handleClose"
>
<Card simple class="jyh-card mb-4" style="box-shadow: none;padding: 0">
<d-chart :option="optionOrgChart" style="width: 100%; height: 400px"></d-chart>
<p class="text-center mt-translate-y">组织/公司分布</p>
</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>
</template>
<script setup lang="ts">
import { ref, defineExpose, reactive } from 'vue';
import { DChart } from 'vue-devui/echarts';
const optionOrgChart = reactive({
tooltip: {
show: true,
trigger: 'item'
},
xAxis: {
type: 'category',
data: ['公司1', '公司2', '公司3', '公司4', '公司5', '公司6', '公司7', '公司8', '公司9', '公司10']
},
yAxis: {
// 给y轴添加单位后缀
axisLabel: {
formatter: '{value}%'
},
name: '单位',
type: 'value'
},
series: [
{
data: [12, 14.8, 23.6, 18.6, 17.3, 13.1, 16, 11.8, 13.9, 16.4],
type: 'bar',
barWidth: '40%',
// 给label添加单位后缀
label: {
show: true,
position: 'top',
formatter: '{c}%'
},
itemStyle: {
color: '#2CB8C9',
borderRadius: [5, 5, 0, 0]
}
}
]
});
// 控制模态框显示/隐藏
const visible = ref(false);
// 打开模态框
const openModal = () => {
visible.value = true;
};
// 关闭模态框
const handleClose = () => {
visible.value = false;
};
// 暴露方法,供父组件调用
defineExpose({
openModal,
});
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
</style>

View File

@@ -0,0 +1,839 @@
<template>
<div class="secret-container">
<div class="search-form">
<d-form layout="horizontal" :label-align="'end'">
<d-row :gutter="16">
<d-col :span="8">
<d-form-item field="name" label="组件名称">
<d-input placeholder="请输入组件名称" v-model="newFormData.softwareName" />
</d-form-item>
</d-col>
<d-col :span="8">
<d-form-item field="version" label="组件版本">
<d-input placeholder="请输入组件版本" v-model="newFormData.softwareVersion" />
</d-form-item>
</d-col>
<d-col :span="8">
<d-form-item field="language" label="编程语言">
<d-select
:options="languageList"
:allow-clear="true"
v-model="newFormData.programmingLanguage"
placeholder="请选择编程语言"
></d-select>
</d-form-item>
</d-col>
<!-- <d-col :span="8">-->
<!-- <d-form-item field="license" label="license">-->
<!-- <d-input placeholder="请输入license名称" v-model="newFormData.licenseName" />-->
<!-- </d-form-item>-->
<!-- </d-col>-->
</d-row>
<d-row :gutter="16">
<!-- <d-col :span="8">-->
<!-- <d-form-item field="dependency" label="依赖组件名称">-->
<!-- <d-input placeholder="请输入依赖组件名称" v-model="newFormData.dependentSoftware" />-->
<!-- </d-form-item>-->
<!-- </d-col>-->
<d-col :span="8">
<d-form-item field="date" label="版本发布日期">
<d-range-date-picker-pro
v-model="newFormData.dateRange"
style="width: 100%"
:placeholder="['开始日期', '结束日期']"
:limitDateRange="limitDateRange"
/>
</d-form-item>
</d-col>
<d-col :span="8">
<d-form-item field="industry" label="行业">
<d-select
:key="newFormData.industry"
:options="industryList"
:allow-clear="true"
v-model="newFormData.industry"
placeholder="请选择行业"
></d-select>
</d-form-item>
</d-col>
</d-row>
<!-- <d-row :gutter="16">-->
<!--&lt;!&ndash; <d-col :span="8">&ndash;&gt;-->
<!--&lt;!&ndash; <d-form-item field="interestTag" label="属性标签">&ndash;&gt;-->
<!--&lt;!&ndash; <d-input placeholder="请输入属性标签" v-model="newFormData.interestTag" />&ndash;&gt;-->
<!--&lt;!&ndash; </d-form-item>&ndash;&gt;-->
<!--&lt;!&ndash; </d-col>&ndash;&gt;-->
<!-- <d-col :span="8">-->
<!-- <d-form-item field="industry" label="行业">-->
<!-- <d-select-->
<!-- :key="newFormData.industry"-->
<!-- :options="industryList"-->
<!-- :allow-clear="true"-->
<!-- v-model="newFormData.industry"-->
<!-- placeholder="请选择行业"-->
<!-- ></d-select>-->
<!-- </d-form-item>-->
<!-- </d-col>-->
<!-- </d-row>-->
</d-form>
<div class="jyh-form-operation">
<d-button @click="getReleaseList" class="mr-2" variant="solid">搜索</d-button>
<d-button variant="solid" color="secondary" @click="clear">清空</d-button>
</div>
<div class="left-btn">
<d-button v-if="isLogin" class="output-btn" icon="icon-share" variant="text" @click="exportResult"
>导出</d-button
>
<span v-if="isLogin" style="height: 14px" class="split"></span>
<TableSetting
:columns="columns"
@setDefult="setDefult"
@updateTableKey="updateTableKey"
:show-column-list="showColumnList"
@update:show-column-list="updateShowColumnList"
/>
</div>
</div>
<div class="mt-3 px-20">
<d-table
:key="tableKey"
ref="filterTableRef"
class="jyh-table"
v-if="tableData.length"
:striped="false"
:data="tableData"
>
<d-column type="checkable" width="40" reserve-check align="center"></d-column>
<!-- 索引列 -->
<!-- <d-column type="index" width="40"></d-column> -->
<!-- 动态渲染列 -->
<template v-for="column in columns" :key="column.key">
<d-column v-if="column.visible" :field="column.key" :header="column.label" :width="column.width||120" :show-overflow-tooltip="!column.tooltip">
<!-- 自定义列内容 -->
<template #default="scope">
<template v-if="column.key === 'softwareName'">
<a @click="gotoDetail(scope.row)">{{ scope.row.softwareName }}</a>
</template>
<template v-else-if="column.key === 'versionName'">
<a @click="gotoDetail(scope.row)">{{ scope.row.versionName }}</a>
</template>
<template v-else-if="column.key === 'version'">
<a @click="gotoDetail(scope.row)">{{ scope.row.version }}</a>
</template>
<template v-else-if="column.key === 'securityScore'">
<!-- <a @click="gotoDetail(scope.row)" style="-->
<!-- <a style="-->
<!-- font-weight: bold;-->
<!-- color: #409EFF;-->
<!-- font-size: 16px;-->
<!-- ">{{ scope.row.securityScore }}</a>-->
<d-popover :content="scope.row.securityScore || '&#45;&#45;'" trigger="hover" style="background-color: #7693f5; color: #fff">
<d-tag color="#ef4444">{{ '非常重要' || '--' }}</d-tag>
</d-popover>
</template>
<template v-else-if="column.key === 'trustScore'">
<d-popover trigger="hover" >
<template #default>
<d-tag color="#ef4444">{{ '高度可信' || '--' }}</d-tag>
</template>
<template #content>
<div style="padding: 4px">
<div style="display: flex;justify-content: space-between;"><span>低</span><span>中</span><span>高</span></div>
<d-progress height="8px" bar-bg-color="#ef4444" style="width: 150px" :percentage="100" percentageText=""></d-progress>
</div>
</template>
</d-popover>
</template>
<template v-else-if="column.key === 'releaseDate'">
<span>{{ formatTime(scope.row.releaseDate, 'YYYY-MM-DD') }}</span>
</template>
<template v-else-if="column.key === 'eolDate'">
<span>{{ formatTime(scope.row.eolDate, 'YYYY-MM-DD') }}</span>
</template>
<template v-else-if="column.key === 'softwareVersion'">
<a @click="gotoDetail(scope.row)">{{ scope.row.softwareVersion }}</a>
</template>
<template v-else-if="column.key === 'smStatus'">
<a>{{ scope.row.smStatus }}</a>
</template>
<template v-else-if="column.key === 'industryCategory'">
<d-tag type="success">{{ getIndustryName(scope.row.industryCategory) }}</d-tag>
</template>
<template v-else-if="column.key === 'programmingLanguage'">
<d-tag type="success">{{ scope.row.programmingLanguage }}</d-tag>
</template>
<template v-else-if="column.key === 'primaryLanguage'">
<d-tag type="primary">{{ scope.row.primaryLanguage }}</d-tag>
</template>
<template v-else-if="column.key === 'contributorCountries'">
<!-- <div style="cursor: pointer" @click="openCountryModal(column)">-->
<!-- <span class="mr-2">美国 3.57%</span>-->
<!-- <d-tag color="gray">+3</d-tag>-->
<!-- </div>-->
</template>
<template v-else-if="column.key === 'contributorOrganizations'">
<!-- <div style="cursor: pointer" @click="openOrgModal(column)">-->
<!-- <span class="mr-2">microsoft 3.57%</span>-->
<!-- <d-tag color="gray">+3</d-tag>-->
<!-- </div>-->
</template>
<template v-else-if="column.key === 'sourceDownloadUrl'">
<a :href="scope.row.sourceDownloadUrl">{{ scope.row.sourceDownloadUrl }}</a>
</template>
<template v-else>
{{ scope.row[column.key] }}
</template>
</template>
</d-column>
</template>
<d-column header="操作" fixed-right="0px" width="130">
<template #default="scope">
<!-- <a class="devui-link" style="margin-right: 8px" @click="openVulnDetail(scope.row)">详情</a>-->
<d-button @click="handleViewAIModal(scope.row)" variant="text" color="primary"> AI问答 </d-button>
</template>
</d-column>
</d-table>
<!-- <DataPanel v-else skeleton :card="false" :empty="true">-->
<!-- </DataPanel>-->
<NoData v-else :small="false"></NoData>
</div>
<div class="mt-20 mb-20 flex justify-end">
<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="getReleaseList()"
@page-size-change="getReleaseList()"
/>
</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>
<!-- 引入子组件 -->
<CountryModal ref="countryModalRef" />
<!-- 引入子组件 -->
<OrgModal ref="orgModalRef" />
<AIModal ref="AIModalRef" />
</div>
</template>
<script setup lang="ts">
import AdvanceSearch from '@/components/AdvanceSearch/index.vue';
import SettingTitle from '@/components/Setting/SettingTitle/index.vue';
import SettingText from '@/components/Setting/SettingText/index.vue';
import { ref, reactive, onMounted, watch, nextTick } from 'vue';
import { getGroupClaList, setGroupClaStatus, deleteGroupCla } from '@/api/cla';
import { useRouter, useRoute } from 'vue-router';
import { GModal } from '@/components/Setting/index';
import TableSetting from '@/components/TableSetting/index.vue';
import { reqCatch } from '@/utils/catch';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
import { Message } from 'vue-devui';
import { storeToRefs } from 'pinia';
import CountryModal from './CountryModal.vue';
import OrgModal from './OrgModal.vue';
import NoData from '@/components/NoData/NoData.vue';
import AIModal from './AIModal.vue';
import { useAccountStore } from '@/stores/user';
import * as XLSX from 'xlsx';
const { formatTime } = useTimeFormat();
import { getReleaseExport, releasePage, searchLanguageList } from '@/api/jyh';
import { useTimeFormat } from '@/utils/hooks/useTimeFormat';
import { useGlobalInfoStore } from '@/stores/Global';
const router = useRouter();
const route = useRoute();
const filterTableRef = ref();
const AIModalRef = ref();
const { namespace } = getOrgInfo();
const userInfo = useAccountStore();
const { isLogin } = storeToRefs(userInfo);
// 定义 tableKey用于强制刷新表格
const tableKey = ref(0);
const countryVisible = ref(false);
const orgVisible = ref(false);
const allTableData = ref([]);
const tableData = ref([
{
"id": 7085,
"userId": 372,
"softwareName": "killbill-embeddeddb-common",
"softwareVersion": "0.26.5",
"releaseVersionDate": "2024-03-21 00:00:00",
"releaseDate": null,
"primaryLanguage": "Java",
"contributorCount": 0,
"contributorCountries": null,
"organizationDistribution": null,
"developer": "org.kill-bill.commons",
"officialWebsite": null,
"integrationRiskLevel": 1,
"securityScore": 0.0,
"codeLines": 0,
"businessDomain": null,
"industryCategory": null,
"sourceDownloadUrl": "http://222.20.126.185:9030/test/software-packages/org.kill-bill.commons:killbill-embeddeddb-common:0.26.5.jar",
"repoHostingUrl": null,
"eolNoticeUrl": null,
"repoTagCommitId": null,
"eolDate": null,
"entityFilename": null,
"entityDownloadUrl": null,
"storageTime": null,
"communityName": null,
"communityWebsite": null,
"communityContact": null,
"communityDescription": null,
"softwareType": "1",
"deleted": 0,
"isTrust": 1,
"isCritical": 0,
"isGitcode": 0,
"status": 2,
"createBy": null,
"updateBy": null,
"createTime": "2025-10-15 09:38:22",
"updateTime": "2025-10-30 15:35:22",
"licenseUseCases": null,
"mainLicenseName": null,
"licenseName": null,
"mainLicenseDescription": null,
"licenseDescription": null,
"securityHelpChannel": null,
"vulnDisclosureUrl": null,
"sourceSoftwareInfo": null,
"copyrightReportName": null,
"copyrightEntryTime": null,
"copyrightReportUrl": null,
"link": "https://gitcode.com/baomidou/mybatis-plus.git",
"isImport": 1,
"componentCount": 2,
"licenseCount": 0,
"vulnerabilityCount": 0,
"vulnScore": 100,
"licenseScore": 0,
"malScore": null,
"trustScore": null,
"gitcodeLink": null,
"gitcodeDownloadUrl": null,
"taskId": null,
"taskType": null,
"softwareId": null,
"userName": null,
"contributorGeoDistribution": null,
"contributorOrgDistribution": null,
"communityHash": null,
"copyright": null,
"externalVulnerabilitySources": null,
"versionDescription": null,
"compatibilityRiskDescription": null,
"softwareTrustLevel": 3,
"entryTime": null,
"taskStatus": null,
"approvalComment": null
}
]);
// const languageList = ref([]);
const globalInfo = useGlobalInfoStore();
const languageList = globalInfo.languageList;
const headerSearch = ref(globalInfo.headerSearch);
const industryList = ref(globalInfo.industryList);
const newFormData = ref({
licenseName: '',
searchType: 'software',
softwareName: '',
softwareVersion: '',
dependentSoftware: '',
programmingLanguage: '',
interestTag: '',
industry: '',
dateRange: ['', '']
});
const currentDate = new Date(); // 获取当前时间
const limitDateRange = ref<Date[]>([new Date(2000, 1, 1), currentDate]); // 结束日期设置为当前时间
// watch(() => globalInfo.headerSearch, (newVal: any) => {
// headerSearch.value = newVal;
// console.log(newVal)
// newFormData.value.searchType = newVal.searchType;
// newFormData.value.softwareName = newVal.softwareName;
// getReleaseList();
// }, { deep: true })
// 打开弹框方法
const handleViewAIModal = (resultId) => {
AIModalRef.value.open(resultId); // 传递result_id参数
};
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 defultColumns = ref([
{ key: 'softwareName', label: '组件名称', visible: true, filterable: true, sortable: false },
{ key: 'softwareVersion', label: '组件版本', visible: true, filterable: true, sortable: false },
{ key: 'trustScore', label: '组件可信等级', visible: true, filterable: false, sortable: true, tooltip: true },
// { key: 'securityScore', label: '组件重要性评分', visible: true, filterable: false, sortable: true },
{ key: 'licenseNum', label: 'License数量', visible: true, filterable: false, sortable: false,width: 140 },
{ key: 'industryCategory', label: '行业类型', visible: true, filterable: true, sortable: false },
{ key: 'primaryLanguage', label: '编程语言', visible: true, filterable: true, sortable: false },
{ key: 'codeLines', label: '代码量', visible: true, filterable: false, sortable: false },
{ key: 'sourceDownloadUrl', label: '源码包下载地址', visible: true, filterable: false, sortable: false,width: 200 },
{ key: 'releaseDate', label: '版本发布时间', visible: false, filterable: true, sortable: true,width: 160 },
// { key: 'dependencyCount', label: '依赖组件数', visible: false, filterable: false, sortable: false },
// { key: 'productComponentUsage', label: '产品成分使用量', visible: false, filterable: false, sortable: false,width: 160 },
{ key: 'vulnerabilityCount', label: '漏洞数量', visible: false, filterable: false, sortable: false },
{ key: 'developer', label: '开发商', visible: false, filterable: true, sortable: false },
{ key: 'contributorCountries', label: '贡献者国家/地区分布', visible: false, filterable: false, sortable: false,width: 200 },
{ key: 'contributorOrganizations', label: '贡献者组织/公司分布', visible: false, filterable: false, sortable: false,width: 200 },
{ key: 'communityHash', label: '社区hash值', visible: false, filterable: false, sortable: false,width: 160 },
{ key: 'copyright', label: 'Copyright', visible: false, filterable: false, sortable: false },
{
key: 'externalVulnerabilitySource',
label: '外部漏洞源的组件名称',
visible: false,
filterable: true,
sortable: false,width: 200
},
{ key: 'versionDescription', label: '版本描述', visible: false, filterable: false, sortable: false },
{ key: 'compatibilityRisk', label: '兼容性风险描述', visible: false, filterable: false, sortable: false ,width: 160},
{ key: 'eolDate', label: '社区EOL时间', visible: false, filterable: true, sortable: true,width: 160 }
]);
// 定义 columns 数据
const columns = ref([
{ key: 'softwareName', label: '组件名称', visible: true, filterable: true, sortable: false },
{ key: 'softwareVersion', label: '组件版本', visible: true, filterable: true, sortable: false },
{ key: 'trustScore', label: '组件可信等级', visible: true, filterable: false, sortable: true, tooltip: true },
// { key: 'securityScore', label: '组件重要性评分', visible: true, filterable: false, sortable: true },
{ key: 'licenseNum', label: 'License数量', visible: true, filterable: false, sortable: false,width: 140 },
{ key: 'industryCategory', label: '行业类型', visible: true, filterable: true, sortable: false },
{ key: 'primaryLanguage', label: '编程语言', visible: true, filterable: true, sortable: false },
{ key: 'codeLines', label: '代码量', visible: true, filterable: false, sortable: false },
{ key: 'sourceDownloadUrl', label: '源码包下载地址', visible: true, filterable: false, sortable: false,width: 200 },
{ key: 'releaseDate', label: '版本发布时间', visible: false, filterable: true, sortable: true,width: 160 },
// { key: 'dependencyCount', label: '依赖组件数', visible: false, filterable: false, sortable: false },
// { key: 'productComponentUsage', label: '产品成分使用量', visible: false, filterable: false, sortable: false,width: 160 },
{ key: 'vulnerabilityCount', label: '漏洞数量', visible: false, filterable: false, sortable: false },
{ key: 'developer', label: '开发商', visible: false, filterable: true, sortable: false },
{ key: 'contributorCountries', label: '贡献者国家/地区分布', visible: false, filterable: false, sortable: false,width: 200 },
{ key: 'contributorOrganizations', label: '贡献者组织/公司分布', visible: false, filterable: false, sortable: false,width: 200 },
{ key: 'communityHash', label: '社区hash值', visible: false, filterable: false, sortable: false,width: 160 },
{ key: 'copyright', label: 'Copyright', visible: false, filterable: false, sortable: false },
{
key: 'externalVulnerabilitySource',
label: '外部漏洞源的组件名称',
visible: false,
filterable: true,
sortable: false,width: 200
},
{ key: 'versionDescription', label: '版本描述', visible: false, filterable: false, sortable: false },
{ key: 'compatibilityRisk', label: '兼容性风险描述', visible: false, filterable: false, sortable: false ,width: 160},
{ key: 'eolDate', label: '社区EOL时间', visible: false, filterable: true, sortable: true,width: 160 }
]);
// 定义 showColumnList 状态
const showColumnList = ref(false);
// 获取子组件的引用
const countryModalRef = ref(null);
const orgModalRef = ref(null);
const getIndustryName = (value) => {
const industry = industryList.value.find((item) => item.value === value);
return industry ? industry.name : '未知行业'; // 如果找不到,返回默认值
};
// 清空高级搜索
const clear = () => {
newFormData.value = {
licenseName: '',
searchType: 'software',
softwareName: '',
softwareVersion: '',
dependentSoftware: '',
programmingLanguage: '',
interestTag: '',
industry: '',
dateRange: ['', '']
};
getReleaseList();
};
// 打开模态框的方法
const setDefult = () => {
columns.value = defultColumns.value;
};
// 打开模态框的方法
const openCountryModal = () => {
if (countryModalRef.value) {
countryModalRef.value.openModal(); // 调用子组件的方法
}
};
// 打开模态框的方法
const openOrgModal = () => {
if (orgModalRef.value) {
orgModalRef.value.openModal(); // 调用子组件的方法
}
};
// 更新 showColumnList 的方法
const updateTableKey = () => {
tableKey.value++; // 更新 tableKey强制刷新表格
};
// 更新 showColumnList 的方法
const updateShowColumnList = (newValue) => {
showColumnList.value = newValue;
// tableKey.value++; // 更新 tableKey强制刷新表格
};
// 更新 showColumnList 的方法
const openCountryChart = (row) => {
// selectRow.value = row;
countryVisible.value = true;
};
// 更新 showColumnList 的方法
const openOrgChart = (row) => {
// selectRow.value = row;
orgVisible.value = true;
};
// 跳转
const gotoDetail = (row) => {
// 后续请换成params暂时params传参没传过去
router.push({
path: 'repoDetail/' + row.id
});
};
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 getReleaseList = async () => {
const { data, error } = await releasePage({
softwareName: newFormData.value.softwareName || '',
searchType: newFormData.value.searchType || '',
softwareVersion: newFormData.value.softwareVersion || '',
programmingLanguage: newFormData.value.programmingLanguage || '',
interestTag: newFormData.value.interestTag || '',
industry: newFormData.value.industry || '',
licenseName: newFormData.value.licenseName || '',
dependentSoftware: newFormData.value.dependentSoftware || '',
releaseDateStart:
newFormData.value.dateRange && newFormData.value.dateRange.length > 0 ? newFormData.value.dateRange[0] : '',
releaseDateEnd:
newFormData.value.dateRange && newFormData.value.dateRange.length > 0 ? newFormData.value.dateRange[1] : '',
current: pager.value.pageIndex,
size: pager.value.pageSize
});
if (!error && data) {
allTableData.value = data.data.records;
tableData.value = data.data.records;
pager.value.total = data.data.total;
console.log(tableData.value);
}
};
const getLanguageList = async () => {
const res: any = await searchLanguageList();
if (!res.error) {
languageList.value = res.data.data;
}
};
onMounted(async () => {
// await getLanguageList();
newFormData.value.softwareName = route.query.softwareName || '';
newFormData.value.searchType = route.query.searchType || '';
newFormData.value.softwareVersion = route.query.softwareVersion || '';
newFormData.value.programmingLanguage = route.query.programmingLanguage || '';
newFormData.value.interestTag = route.query.interestTag || '';
newFormData.value.industry = route.query.searchType || '';
newFormData.value.licenseName = route.query.licenseName || '';
newFormData.value.dependentSoftware = route.query.dependentSoftware || '';
newFormData.value.dateRange = [route.query.releaseDateStart || '', route.query.releaseDateEnd || ''];
nextTick(() => {
getReleaseList();
});
// getReleaseList();
});
const exportResult = async () => {
const selectDatas = filterTableRef.value.store.getCheckedRows().map((d) => d.id);
if (!allTableData.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('文件导出失败');
}
};
// onMounted(() => {
// const query: any = route.query;
// newFormData.value = {
// ...query
// };
// });
// const exportResult = () => {
//
// if (!allTableData.value.length) {
// Message.info('暂无结果');
// return;
// }
//
// const selectDatas = filterTableRef.value.store.getCheckedRows().map((d) => d.id);
// if (!allTableData.value.length || selectDatas.length <= 0) {
// Message.info('未选择任何数据');
// return;
// }
//
// const exportData = allTableData.value
// .filter((d) => {
// return selectDatas.includes(d.id);
// })
// .map((item) => {
// let res = {};
// columns.value.forEach((col) => {
// if (col.key === 'validStatus') {
// res[col.label] = validStatusMap[item[col.key]] ? validStatusMap[item[col.key]] : '';
// } else {
// res[col.label] = item[col.key] ? item[col.key] : '';
// }
// });
// return res;
// });
//
// try {
// const workSheet = XLSX.utils.json_to_sheet(exportData);
// const workBook: any = { Sheets: { 校验结果: workSheet }, SheetNames: ['校验结果'] };
// const excelBuffer = XLSX.write(workBook, { bookType: 'xlsx', type: 'array' });
//
// const link = document.createElement('a');
// const blob = new Blob([excelBuffer]);
// link.setAttribute('href', window.URL.createObjectURL(blob));
// link.setAttribute('download', '组件存在及可信校验结果.xlsx');
// link.style.visibility = 'hidden';
// document.body.appendChild(link);
// link.click();
// document.body.removeChild(link);
// Message.success('导出成功');
// } catch {
// Message.error('导出失败');
// }
// };
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.secret-container {
position: relative;
min-height: 600px;
.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;
}
.search-form {
position: relative;
border-radius: 8px 0px 8px 8px;
//background: #F2F5FC;
.left-btn {
position: absolute;
right: 28px;
bottom: 4px;
}
.output-btn {
color: #191919;
font-family: HarmonyOS Sans SC;
font-weight: regular;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
}
.jyh-form-operation {
margin-left: 20px !important;
}
}
.secret-breadcrumb {
padding: 7px 20px 12px;
background: #ffffff;
}
</style>

View File

@@ -0,0 +1,367 @@
<template>
<div class="vuln-detail-container">
<div class="vuln-title mb-2">
<span>漏洞预警详情</span>
<div class="vuln-sub-title">
<!-- <d-button @click="openWarningDetail()" variant="solid">预警发布</d-button>-->
<!-- <span class="split-line"></span>-->
<d-icon name="icon-close" @click="$emit('close')"></d-icon>
</div>
</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">{{ valInfos.vulnId || '--' }}</span>
</d-col>
<d-col :span="12">
<span class="card-key width-90">漏洞类型</span>
<span class="card-val">{{ valInfos.vulnType || '--' }}</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">{{
valInfos.publishDate ? dayjs(valInfos.publishDate).format('YYYY-MM-DD') : '--'
}}</span>
</d-col>
<d-col :span="12">
<span class="card-key width-90">严重程度</span>
<span class="card-val" v-if="tagMap[valInfos.severity]">
<d-tag :color="tagMap[valInfos.severity].color" size="sm">{{ tagMap[valInfos.severity].text }}</d-tag>
</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="24">
<div v-if="componentsLists.length>0">
<d-table
class="jyh-table jyh-table-2"
:header-bg="true"
:data="componentsLists"
:span-method="spanMethod"
>
<d-column field="effectedComponentName" header="软件名称" show-overflow-tooltip></d-column>
<d-column field="effectedComponentVersion" header="软件版本及范围" show-overflow-tooltip></d-column>
<d-column field="isCriticalInfrastructureComponent" header="是否用户订阅" width="140" align="center">
<template #default="scope">
<div class="text-center" v-if="scope.row.isCriticalInfrastructureComponent==='是'">
<d-icon name="icon-right-o" style="font-size: 14px; color: rgb(80, 212, 171)"></d-icon>
</div>
<div class="text-center" v-else>
<d-icon name="icon-error-o" style="font-size: 14px; color: rgb(246, 111, 106)"></d-icon>
</div>
</template>
</d-column>
<!-- <d-column field="fixVersion" header="补丁版本"></d-column>-->
<!-- <d-column field="fixIssue" header="修复建议" show-overflow-tooltip></d-column>-->
<template #empty>
<NoData :small="false"></NoData>
</template>
</d-table>
<div class="mt-20 mb-20 flex justify-end" >
<d-pagination
style="display: inline-flex"
: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"
total-item-text="总计"
@page-index-change="getComponentsList"
@page-size-change="getComponentsList"
/>
</div>
</div>
<NoData v-else :small="false"></NoData>
</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="24">-->
<!-- <d-radio-group direction="row" v-model="isOut" @change="onStorageStartTask">-->
<!-- <d-radio :value="2">出库</d-radio>-->
<!-- <d-radio :value="1">不出库</d-radio>-->
<!-- </d-radio-group>-->
<!-- </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="24">-->
<!-- <d-table class="jyh-table jyh-table-2" :header-bg="true" :data="alertsUsersLists">-->
<!-- <d-column field="username" header="订阅人"></d-column>-->
<!-- <d-column field="method" header="订阅方式"></d-column>-->
<!-- <d-column field="notifiedStatus" header="通知状态">-->
<!-- <template #default="scope, text">-->
<!-- <d-tag v-if="scope.row.notifiedStatus === 0" type="danger" size="sm">未通知</d-tag>-->
<!-- <d-tag v-if="scope.row.notifiedStatus === 1" type="success" size="sm">已通知</d-tag>-->
<!-- </template>-->
<!-- </d-column>-->
<!-- <template #empty>-->
<!-- <NoData :small="false"></NoData>-->
<!-- </template>-->
<!-- </d-table>-->
<!-- &lt;!&ndash; <div class="invitation-pagination">-->
<!-- <d-pagination-->
<!-- style="display: inline-flex"-->
<!-- :total="subscribePager.total"-->
<!-- v-model:pageSize="subscribePager.pageSize"-->
<!-- v-model:pageIndex="subscribePager.pageIndex"-->
<!-- :can-view-total="true"-->
<!-- :can-change-page-size="true"-->
<!-- :can-jump-page="true"-->
<!-- :max-items="5"-->
<!-- />-->
<!-- </div> &ndash;&gt;-->
<!-- </d-col>-->
<!-- </d-row>-->
<!-- </Card>-->
</div>
<d-modal class="warning-modal" v-model="warningModalVisable" title="">
<EarlyWarningDetail
@close="colseWarningDetail"
:softwareId="valInfos.softwareId"
:valInfos="valInfos"
:softwareIds="softwareIds"
:isStartOutTask="isOut"
type="system"
></EarlyWarningDetail>
</d-modal>
</template>
<script setup lang="ts">
import NoData from '@/components/NoData/NoData.vue';
import dayjs from 'dayjs';
import {
getVulnAffected,
getVulnDetail,
getVulnerabilitiesAlertsUsers,
getVulnerabilitiesComponents,
getVulnerabilitiesDetails,
getVulnPatch,
storageStartOutTask
} from '@/api/repo';
import { computed, onMounted, ref, shallowReactive } from 'vue';
import EarlyWarningDetail from '@/views/Jyh/PersonalCenter/Detail/EarlyWarningDetail.vue';
import { Message } from 'vue-devui/message';
const tagMap = {
Critical: {
text: '致命',
color: '#c7000b'
},
High: {
text: '高',
color: '#f66f6a'
},
Medium: {
text: '中',
color: '#fac20a'
},
Low: {
text: '低',
color: '#5e7ce0'
},
high: {
text: '高',
color: '#f66f6a'
},
medium: {
text: '中',
color: '#fac20a'
},
low: {
text: '低',
color: '#5e7ce0'
}
};
const subscribePager = shallowReactive({
total: 1,
pageIndex: 1,
pageSize: 10,
pageSizeOptions: [10, 20, 30, 40, 50]
});
const isOut = ref(1);
const propsData = defineProps(['vulnId']);
const warningModalVisable = ref(false);
const softwareIds = computed(() => {
let arr = [];
alertsUsersLists.value?.map(i=>{
if(i.notifiedStatus==0) {
if(!arr.includes(i.softwareId)) {
arr.push(i.softwareId)
}
}
})
return arr;
});
const openWarningDetail = () => {
if(valInfos.value?.effectedComponents.length>0&&softwareIds.value.length>0) {
warningModalVisable.value = true;
} else {
Message.warning('当前暂无需要通知的用户');
}
};
const colseWarningDetail = () => {
warningModalVisable.value = false;
};
const onStorageStartTask = async (e) => {
if (!valInfos.value?.softwareId) return;
await storageStartOutTask({
softwareId: valInfos.value?.softwareId,
taskType: isOut.value
});
};
const severityMap = {
high: '高',
medium: '中',
low: '低'
};
const valInfos: any = ref({});
const componentsLists: any = ref([]);
const alertsUsersLists: any = ref([]);
const getComponentsList = async () => {
const response = await getVulnerabilitiesComponents({
id:propsData.vulnId,
current: pager.value.pageIndex,
size: pager.value.pageSize
});
debugger
if (response.data.data.code === 200) {
// 假设接口返回格式为 { code, data: { records, total } }
componentsLists.value = response.data.data.data.records || [];
pager.value.total = response.data.data.data.total || 0;
} else {
// Message.error('获取组件列表失败');
componentsLists.value = [];
pager.value.total = 0;
}
}
onMounted(async () => {
const _valInfos = await getVulnerabilitiesDetails(propsData.vulnId);
const _alertsUsersLists = await getVulnerabilitiesAlertsUsers(propsData.vulnId);
valInfos.value = _valInfos?.data?.data?.data;
alertsUsersLists.value = _alertsUsersLists?.data?.data?.data?.map((user) => {
const alertMethod = JSON.parse(user.alertTypeExt ?? '{}');
let method = '';
if (alertMethod.webSwitch) {
method = '站内通知';
} else if (alertMethod.emailSwitch) {
method = '邮件';
} else if (alertMethod.mobileSwitch) {
method = '短信';
}
return {
...user,
method
};
});
await getComponentsList()
});
const pager = ref({
total: 0,
pageIndex: 1,
pageSize: 10
});
// 分页事件处理
function onPageIndexChange(newPageIndex) {
pager.value.pageIndex = newPageIndex;
}
function onPageSizeChange(newPageSize) {
// 重置页码为1
pager.value.pageIndex = 1;
pager.value.pageSize = newPageSize;
}
</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;
display: flex;
justify-content: space-between;
}
.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;
}
.invitation-pagination {
padding: 12px 0;
text-align: right;
}
}
</style>
<style lang="scss">
.warning-modal {
width: auto;
.devui-modal__header {
font-size: 20px;
height: auto !important;
}
.devui-modal__body {
padding: 0;
}
}
</style>

View File

@@ -0,0 +1,329 @@
<template>
<d-modal style="width: 1200px" v-model="visible" @close="close" class="software-modal">
<template #header>
<gc-modal-header>
<span class="text-G900 text-base font-bold leading-[24px]">受影响软件清单</span>
</gc-modal-header>
<div class="line bg-G200"></div>
</template>
<d-row class="mb-3">
<d-col :span="24">
<!-- 批量预警按钮保留 -->
<div class="mb-4 flex justify-start">
<d-button
variant="solid"
color="primary"
@click="batchPublishWarning"
:disabled="selectedRows.length === 0"
>
批量预警
</d-button>
<span class="ml-3 text-G600" v-if="selectedRows.length > 0">
已选中 {{ selectedRows.length }} 条数据
</span>
</div>
<div v-if="componentsLists.length > 0">
<d-table
ref="tableRef"
class="jyh-table jyh-table-2"
:header-bg="true"
:data="componentsLists"
:span-method="spanMethod"
row-key="effectedComponentVersion"
@check-change="handleCheckChange"
@check-all-change="handleCheckAllChange"
>
<!-- 1. 新增复选框列DevUI标准选中入口 -->
<d-column
type="checkable"
width="40"
reserve-check
></d-column>
<!-- 原有列保持不变 -->
<d-column field="effectedSoftwareName" header="应用名称" show-overflow-tooltip width="200"></d-column>
<d-column field="effectedComponentName" header="组件名称" show-overflow-tooltip width="200"></d-column>
<d-column field="effectedComponentVersion" header="版本及范围" show-overflow-tooltip width="200"></d-column>
<d-column field="develop" header="组件开发者" show-overflow-tooltip width="180"></d-column>
<d-column field="responsibleTeam" header="责任单位" show-overflow-tooltip width="180"></d-column>
<d-column header="操作" fixed-right="0px" width="160">
<template #default="scope">
<a class="devui-link mr-[8px]" @click="openWarningDetail(scope.row)">发布预警</a>
</template>
</d-column>
</d-table>
<!-- 分页组件保留新增选中状态重置逻辑 -->
<div class="mt-20 mb-20 flex justify-end">
<d-pagination
size="md"
: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"
total-item-text="总计"
@page-index-change="handlePageChange"
@page-size-change="handlePageChange"
/>
</div>
</div>
<NoData v-else :small="false"></NoData>
</d-col>
</d-row>
<template #footer>
<div class="flex justify-end px-[24px] pb-[24px] gap-2">
<d-button @click="handleCancel">取消</d-button>
<d-button variant="solid" :disabled="disabled" @click="handleConfirm">确定</d-button>
</div>
</template>
<!-- <EarlyWarningDetail :warningData="currentWarning" type="system" @refresh="getAlertSubscribesPage"></EarlyWarningDetail>-->
</d-modal>
</template>
<script setup>
import { ref } from 'vue';
import NoData from '@/components/NoData/NoData.vue';
import { getVulnerabilitiesComponents } from '@/api/repo';
import { Message } from 'vue-devui/message';
import { useGlobalInfoStore } from '@/stores/Global';
import { getNoticeAdd } from '@/api/jyh';
const globalStore = useGlobalInfoStore();
// -------------------------- 1. 核心状态调整 --------------------------
const tableRef = ref(null); // 新增表格Ref用于主动操作如清空选中
const vulnId = ref('');
const vulnRow = ref({});
const visible = ref(false);
const componentsLists = ref([]);
const disabled = ref(false);
const pager = ref({ total: 0, pageIndex: 1, pageSize: 10 });
const warningData = ref({});
const currentWarning = ref();
const selectedRows = ref([]); // 存储选中的行数据(核心)
// 暴露给父组件的方法
defineExpose({ open, close, setDisabled });
// -------------------------- 2. 选中功能核心逻辑适配DevUI标准 --------------------------
/**
* 可选:控制行是否允许选中(如过滤特定责任单位的行)
* @param row 当前行数据
* @param rowIndex 行索引
* @returns 是否允许选中
*/
const isRowCheckable = (row, rowIndex) => {
// 示例:所有行都允许选中(可自定义条件,如 row.responsibleUnit === '信支工程大学'
return true;
};
/**
* 单选事件DevUI标准事件@check-change
* @param checked 当前行是否选中boolean
* @param row 当前行数据object
* @param selection 所有选中的行数据array
*/
// 复选框选中状态变化
const handleCheckChange = (checked, row, selection) => {
if (checked) {
if (!selectedRows.value.some(r => r.effectedComponentVersion === row.effectedComponentVersion)) {
selectedRows.value.push(row);
}
} else {
selectedRows.value = selectedRows.value.filter(r => r.effectedComponentVersion !== row.effectedComponentVersion);
}
};
// 全选状态变化
const handleCheckAllChange = (checked, selection) => {
if (checked) {
selectedRows.value = [...selection];
} else {
selectedRows.value = [];
}
};
// -------------------------- 3. 分页事件调整(重置选中状态) --------------------------
/**
* 分页切换事件(统一处理页码/页大小变化)
*/
const handlePageChange = async () => {
// 分页切换时,清空上一页的选中状态(避免跨页残留)
selectedRows.value = [];
// 若需要保留跨页选中,可通过 tableRef 操作tableRef.value.store.clearSelection()
await getComponentsList();
};
// -------------------------- 4. 原有业务逻辑(保持不变,仅适配选中数据) --------------------------
/**
* 单条发布预警
*/
const openWarningDetail = async (row) => {
currentWarning.value = row;
// Message.success(`已为【${row.softName}】发布预警`);
const { data } = await getNoticeAdd({
"softwareId": [row?.effectedSoftwareId],
"username": globalStore.currentRoleKey,
"vulId": row?.vulnId,
}).catch((err) => {
Message({
type: 'error',
message: '预警发送失败,请重试'
});
});
if(data.ok) {
Message({
type: 'success',
message: '预警发送成功'
});
}
};
/**
* 批量发布预警
*/
const batchPublishWarning = async () => {
if (selectedRows.value.length === 0) {
Message.warning('请先选择需要发布预警的数据');
return;
}
const { data } = await getNoticeAdd({
"softwareId": selectedRows.value.map(i=>i.effectedSoftwareId),
"username": globalStore.currentRoleKey,
"vulId": selectedRows.value[0]?.vulnId,
}).catch((err) => {
Message({
type: 'error',
message: '预警发送失败,请重试'
});
});
if(data.ok) {
Message({
type: 'success',
message: '预警发送成功'
});
// // 批量预警后主动清空表格选中状态联动UI
// if (tableRef.value) {
// tableRef.value.store.clearSelection();
// }
// selectedRows.value = [];
}
};
/**
* 获取组件列表(模拟数据+真实接口)
*/
const getComponentsList = async () => {
if (vulnRow.value.vulnId === 'CVE-2025-27507') {
// 模拟数据(保持不变)
componentsLists.value = [
{
id: '1',
softName: '军工供应链管理系统 V2.0',
effectedComponentName: 'Apache Commons Text',
effectedComponentVersion: '1.9 - 1.11.0(含)',
coder: 'Apache软件基金会',
responsibleUnit: '信支工程大学'
},
{
id: '2',
softName: '高校科研项目管理平台',
effectedComponentName: 'Log4j',
effectedComponentVersion: '2.0 - 2.14.1(含)',
coder: 'Apache软件基金会',
responsibleUnit: '华中科技大学'
},
{
id: '3',
softName: '军工代码审计系统 V3.5',
effectedComponentName: 'Jackson Databind',
effectedComponentVersion: '2.9.0 - 2.15.2(含)',
coder: 'FasterXML',
responsibleUnit: '信支工程大学'
},
{
id: '4',
softName: '高校教学管理系统 V5.0',
effectedComponentName: 'Spring Framework',
effectedComponentVersion: '5.3.0 - 5.3.23(含)',
coder: 'Pivotal Software',
responsibleUnit: '武汉大学'
}
];
pager.value.total = componentsLists.value.length;
} else {
// 真实接口逻辑(保持不变)
const response = await getVulnerabilitiesComponents({
id: vulnId.value,
current: pager.value.pageIndex,
size: pager.value.pageSize
});
if (response.data.data.code === 200) {
componentsLists.value = response.data.data.data.records?.map(item => ({
...item,
coder: item.coder || '未知',
responsibleUnit: item.responsibleUnit || '未知单位'
})) || [];
pager.value.total = response.data.data.data.total || 0;
} else {
componentsLists.value = [];
pager.value.total = 0;
}
}
};
/**
* 打开/关闭模态框(重置选中状态)
*/
async function open(row) {
if (row.id || row.vulnId) {
vulnId.value = row.id;
vulnRow.value = row;
selectedRows.value = []; // 重置选中
await getComponentsList();
visible.value = true;
}
}
function close() {
componentsLists.value = [];
pager.value = { total: 0, pageIndex: 1, pageSize: 10 };
vulnId.value = '';
vulnRow.value = {};
selectedRows.value = []; // 清空选中
// if (tableRef.value) {
// tableRef.value.store.clearSelection(); // 联动表格UI清空
// }
visible.value = false;
}
// 其他原有方法(保持不变)
function setComponentsList(list) { componentsLists.value = list; }
function setDisabled(isDisabled) { disabled.value = isDisabled; }
function handleConfirm() { close(); }
function handleCancel() { close(); }
</script>
<style lang="scss" scoped>
.software-modal {
width: 1200px !important;
}
:deep(.devui-btn) {
margin-bottom: 8px;
}
:deep(.devui-table-cell) {
white-space: nowrap;
}
// 可选:调整复选框列样式(与表格对齐)
:deep(.devui-table-checkable-column) {
text-align: center;
}
</style>

View File

@@ -0,0 +1,376 @@
<template>
<div class="page-wrap">
<!-- <h1 class="page-title">{{ title }}</h1> -->
<div class="search-form">
<!-- <d-search-->
<!-- icon-position="left"-->
<!-- @search="search"-->
<!-- v-model="vulnId"-->
<!-- style="width: 400px"-->
<!-- placeholder="请输入漏洞编号"-->
<!-- ></d-search>-->
<d-form layout="horizontal" :label-align="'end'">
<d-row :gutter="16">
<d-col :span="6">
<d-form-item field="vulnId" label="漏洞编号">
<d-input placeholder="请输入漏洞编号" v-model="newFormData.vulnId" />
</d-form-item>
</d-col>
<d-col :span="6">
<d-form-item field="severity" label="严重程度">
<d-select
:key="newFormData.severity"
:options="tagArray"
:allow-clear="true"
v-model="newFormData.severity"
placeholder="请选择严重程度"
></d-select>
</d-form-item>
</d-col>
<d-col :span="6">
<d-form-item field="affectedComponentCount" label="受影响软件数">
<d-input-number style="width: 100%;background: #ffffff" :min="0" :max="100" placeholder="请输入受影响软件数" :allowEmpty="true" v-model="newFormData.affectedComponentCount" />
</d-form-item>
</d-col>
</d-row>
</d-form>
<div class="jyh-form-operation">
<d-button @click="getList(1)" class="mr-2" variant="solid">搜索</d-button>
<d-button variant="solid" color="secondary" @click="clear">清空</d-button>
</div>
</div>
<Card class="mt-3">
<d-table :show-loading="loading" class="jyh-table" :data="dataLists" v-if="dataLists.length > 0" table-layout="auto">
<d-column type="index" width="40"></d-column>
<d-column field="vulnId" header="漏洞编号"></d-column>
<d-column field="vulnType" header="漏洞类型"></d-column>
<d-column field="publishDate" header="漏洞发布时间">
<template #default="scope">
{{ scope.row.publishDate
? dayjs(scope.row.publishDate).add(5, 'month').format('YYYY-MM-DD HH:mm:ss')
: '--'
}}
</template>
</d-column>
<d-column field="severity" header="严重程度">
<template #default="scope">
<span class="card-val" v-if="tagMap[scope.row.severity]">
<d-tag :color="tagMap[scope.row.severity].color" size="sm">{{ tagMap[scope.row.severity].text }}</d-tag>
</span></template
>
</d-column>
<d-column field="effectedComponentCount" header="受影响软件清单">
<template #default="scope">
<!--TODO:将受影响软件清单数量改为十个以下-->
<a v-if="scope.row.vulnId==='CVE-2025-27507'" class="devui-link" @click="showModal(scope.row)">4个</a>
<a v-else class="devui-link" @click="showModal(scope.row)">{{scope.row.effectedComponentCount? String(scope.row.effectedComponentCount)[0]:'0'}}</a>
</template>
</d-column>
<d-column header="操作" fixed-right="0px" width="130">
<template #default="scope">
<a class="devui-link" style="margin-right: 8px" @click="openVulnDetail(scope.row)">详情</a>
<d-button @click="handleViewAIModal(scope.row)" variant="text" color="primary"> AI问答 </d-button>
</template>
</d-column>
</d-table>
<NoData v-else :small="false"></NoData>
<div class="mt-20 mb-20 flex justify-end" v-if="dataLists.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="getList()"
@page-size-change="getList()"
/>
</div>
</Card>
</div>
<!-- 漏洞详情 -->
<d-drawer v-model="vulnDetailVisable" style="width: auto">
<VulnerabilityDetail :vulnId="currentVuln.id" @close="colseVulnDetail"></VulnerabilityDetail>
</d-drawer>
<AIModal ref="AIModalRef" />
<EffectedComponents ref="modalRef" />
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import VulnerabilityDetail from './VulnerabilityDetail.vue';
import EffectedComponents from './effectedComponents.vue';
import { alertsPage } from '@/api/jyh/home';
import dayjs from 'dayjs';
import NoData from '@/components/NoData/NoData.vue';
import AIModal from '../AIModal.vue';
import { getVulnerabilitiespage } from '@/api/repo';
const route = useRoute();
const title = computed(() => route.meta?.reportTitle || '漏洞感知列表');
const pager = ref({
total: 10,
pageIndex: 1,
pageSize: 10
});
const newFormData = ref({
vulnId: '',
affectedComponentCount: '',
severity: '',
});
const loading = ref(false);
const AIModalRef = ref(null);
// 清空高级搜索
const clear = () => {
newFormData.value = {
vulnId: '',
affectedComponentCount: '',
severity: '',
};
getList();
};
const tagMap = {
Critical: {
text: '致命',
color: '#c7000b'
},
High: {
text: '高',
color: '#f66f6a'
},
Medium: {
text: '中',
color: '#fac20a'
},
Low: {
text: '低',
color: '#5e7ce0'
},
critical: {
text: '致命',
color: '#c7000b'
},
high: {
text: '高',
color: '#f66f6a'
},
medium: {
text: '中',
color: '#fac20a'
},
low: {
text: '低',
color: '#5e7ce0'
}
};
const tagArray = [
{ value: 'critical', name: '致命' },
{ value: 'high', name: '高' },
{ value: 'medium', name: '中' },
{ value: 'low', name: '低' }
]
const vulnDetailVisable = ref(false);
const currentVuln = ref();
const openVulnDetail = (row) => {
vulnDetailVisable.value = true;
currentVuln.value = row;
};
const modalRef = ref(null);
function showModal(row) {
if (modalRef.value) {
modalRef.value.setDisabled(false);
modalRef.value.open(row);
}
}
// 打开弹框方法
const handleViewAIModal = (resultId) => {
AIModalRef.value.open(resultId); // 传递result_id参数
};
const colseVulnDetail = () => {
vulnDetailVisable.value = false;
};
const vulnId = ref('');
const dataLists = ref([]);
const getList = async (current) => {
loading.value = true
if(current) pager.value.pageIndex=current;
const { data } = await getVulnerabilitiespage({
vulnId: newFormData.value.vulnId,
current: current || pager.value.pageIndex,
affectedComponentCount: newFormData.value.affectedComponentCount,
severity: newFormData.value.severity,
size: pager.value.pageSize
}).finally(()=>{
loading.value = false
});
dataLists.value = data?.data?.data?.records || [
{
"id": 184783,
"taskId": null,
"softwareId": null,
"componentId": null,
"vulnId": "CVE-2024-31585",
"cweId": "CWE-193",
"severity": "medium",
"vulnType": "CVE",
"cvssScore": 5.3,
"attackVector": null,
"attackComplexity": null,
"userInteraction": null,
"privilegesRequired": null,
"exploitabilityScore": null,
"impactScore": null,
"description": "FFmpeg version n5.1 to n6.1 was discovered to contain an Off-by-one Error vulnerability in libavfilter/avf_showspectrum.c. This vulnerability allows attackers to cause a Denial of Service (DoS) via a crafted input.",
"extraInfo": "{\"refs\": [\"https://gist.github.com/1047524396/dc2c64ffe0c3934a6176bcd2c5cf5656\", \"https://github.com/FFmpeg/FFmpeg/commit/81df787b53eb5c6433731f6eaaf7f2a94d8a8c80\", \"https://github.com/ffmpeg/ffmpeg/commit/ab0fdaedd1e7224f7e84ea22fcbfaa4ca75a6c06\", \"https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/6G7EYH2JAK5OJPVNC6AXYQ5K7YGYNCDN/\", \"https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/IPETICRXUOGRIM4U3BCRTIKE3IZWCSBT/\", \"https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/LE3ASLH6QF2E5OVJI5VA3JSEPJFFFMNY/\"]}",
"fixIssue": null,
"patchLink": null,
"source": "HUST",
"publishDate": "2024-04-17T19:15:08.000+00:00",
"fixVersion": null,
"exploitability": null,
"createTime": "2025-10-14T21:00:03.000+00:00",
"updateTime": "2025-10-14T21:00:03.000+00:00",
"componentName": null,
"componentVersion": null,
"effectedComponentCount": null,
"effectedComponents": null
},
{
"id": 184780,
"taskId": null,
"softwareId": null,
"componentId": null,
"vulnId": "CVE-2024-31578",
"cweId": null,
"severity": "high",
"vulnType": "CVE",
"cvssScore": 7.5,
"attackVector": null,
"attackComplexity": null,
"userInteraction": null,
"privilegesRequired": null,
"exploitabilityScore": null,
"impactScore": null,
"description": "FFmpeg version n6.1.1 was discovered to contain a heap use-after-free via the av_hwframe_ctx_init function.",
"extraInfo": "{\"refs\": [\"https://github.com/ffmpeg/ffmpeg/commit/3bb00c0a420c3ce83c6fafee30270d69622ccad7\", \"https://gist.github.com/1047524396/45400cce5859d78dcd3a62010df8d179\", \"https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/LE3ASLH6QF2E5OVJI5VA3JSEPJFFFMNY/\", \"https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/IPETICRXUOGRIM4U3BCRTIKE3IZWCSBT/\", \"https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/6G7EYH2JAK5OJPVNC6AXYQ5K7YGYNCDN/\"]}",
"fixIssue": null,
"patchLink": null,
"source": "HUST",
"publishDate": "2024-04-17T14:15:08.000+00:00",
"fixVersion": null,
"exploitability": null,
"createTime": "2025-10-14T21:00:03.000+00:00",
"updateTime": "2025-10-14T21:00:03.000+00:00",
"componentName": null,
"componentVersion": null,
"effectedComponentCount": 1,
"effectedComponents": null
},
{
"id": 184781,
"taskId": null,
"softwareId": null,
"componentId": null,
"vulnId": "CVE-2024-31578",
"cweId": null,
"severity": "high",
"vulnType": "CVE",
"cvssScore": 7.5,
"attackVector": null,
"attackComplexity": null,
"userInteraction": null,
"privilegesRequired": null,
"exploitabilityScore": null,
"impactScore": null,
"description": "FFmpeg version n6.1.1 was discovered to contain a heap use-after-free via the av_hwframe_ctx_init function.",
"extraInfo": "{\"refs\": [\"https://github.com/ffmpeg/ffmpeg/commit/3bb00c0a420c3ce83c6fafee30270d69622ccad7\", \"https://gist.github.com/1047524396/45400cce5859d78dcd3a62010df8d179\", \"https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/LE3ASLH6QF2E5OVJI5VA3JSEPJFFFMNY/\", \"https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/IPETICRXUOGRIM4U3BCRTIKE3IZWCSBT/\", \"https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/6G7EYH2JAK5OJPVNC6AXYQ5K7YGYNCDN/\"]}",
"fixIssue": null,
"patchLink": null,
"source": "HUST",
"publishDate": "2024-04-17T14:15:08.000+00:00",
"fixVersion": null,
"exploitability": null,
"createTime": "2025-10-14T21:00:03.000+00:00",
"updateTime": "2025-10-14T21:00:03.000+00:00",
"componentName": null,
"componentVersion": null,
"effectedComponentCount": null,
"effectedComponents": null
}
];
pager.value.total = data.data.data?.total;
};
onMounted(() => {
getList();
});
const search = () => {
pager.value.pageIndex = 1;
getList();
};
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.page-wrap {
margin: 16px;
.page-title {
color: #191919;
font-family: HarmonyOS Sans SC;
font-weight: medium;
font-size: 18px;
line-height: 26px;
letter-spacing: 0px;
text-align: left;
}
.g-content-card {
overflow: hidden;
padding: 0;
}
}
.search-form {
position: relative;
border-radius: 8px 0px 8px 8px;
margin-right: 20px;
//background: #F2F5FC;
.left-btn {
position: absolute;
right: 28px;
bottom: 4px;
}
.output-btn {
color: #191919;
font-family: HarmonyOS Sans SC;
font-weight: regular;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
}
.jyh-form-operation {
margin-left: 20px !important;
}
}
</style>