可信代码库V2版-安全智库页面原型开发、安全检测中心页面原型开发、关于我们页面原型开发
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
"@sentry/vue": "^7.101.1",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@vueuse/core": "^10.2.1",
|
||||
"ant-design-vue": "4.x",
|
||||
"axios": "^1.6.8",
|
||||
"canvas-confetti": "^1.9.2",
|
||||
"crypto-js": "^4.2.0",
|
||||
|
||||
@@ -63,3 +63,10 @@ export function queryTaskSoftwareDetail(params: {taskId: number}) {
|
||||
});
|
||||
}
|
||||
|
||||
export function uvdV1Chat(params) {
|
||||
return request({
|
||||
url: `/uvd/v1/chat`,
|
||||
method: 'post',
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
|
||||
310
src/api/jyh/home.ts
Normal file
310
src/api/jyh/home.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
export function getStatistics (): Promise<any> {
|
||||
return request({
|
||||
url: `/trust/data/homeStatistics`,
|
||||
method: 'post',
|
||||
data: {
|
||||
current: 1,
|
||||
size: 10
|
||||
}
|
||||
});
|
||||
}
|
||||
export function alertsPage(): Promise<any> {
|
||||
return request({
|
||||
url: `/api/v1/alerts/page`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
// 1. 定义排行类型(与父组件对应)
|
||||
export type RankType = 'usageCount' | 'falseNegativeCount' | 'falsePositiveCount';
|
||||
// 2. 定义机构类型(原有)
|
||||
export type InstitutionKey = '全部' | '华中科技大学' | '武汉大学' | '信息支援部队工程大学';
|
||||
/**
|
||||
* 获取工具排行数据
|
||||
* @param institutionKey - 机构标识(全部/华中科技大学/武汉大学/信息支援部队工程大学)
|
||||
* @returns 对应机构的工具数据
|
||||
*/
|
||||
/**
|
||||
* 获取工具排行数据
|
||||
* @param institutionKey - 机构标识
|
||||
* @param rankType - 排行类型(使用次数/漏报次数/误报次数)
|
||||
* @returns 对应机构+对应类型的工具数据
|
||||
*/
|
||||
export function getToolRankData(
|
||||
institutionKey: InstitutionKey = '全部',
|
||||
rankType: RankType = 'usageCount' // 新增:排行类型参数
|
||||
): Promise<any> {
|
||||
// 1. 定义完整数据:为每个工具补充“使用次数/漏报次数/误报次数”模拟值
|
||||
const allInstitutionData = [
|
||||
{
|
||||
institution: '华中科技大学',
|
||||
score: 92,
|
||||
tools: [
|
||||
{
|
||||
name: '代码扫描工具V2.1',
|
||||
score: 95,
|
||||
falsePositiveRate: 2.3,
|
||||
falseNegativeRate: 3.1,
|
||||
usageRate: 89,
|
||||
usageCount: 1280, // 模拟:使用次数
|
||||
falseNegativeCount: 15, // 模拟:漏报次数(按漏报率*使用次数估算)
|
||||
falsePositiveCount: 28 // 模拟:误报次数(按误报率*使用次数估算)
|
||||
},
|
||||
{
|
||||
name: '漏洞检测系统V3.0',
|
||||
score: 89,
|
||||
falsePositiveRate: 3.5,
|
||||
falseNegativeRate: 4.2,
|
||||
usageRate: 76,
|
||||
usageCount: 950,
|
||||
falseNegativeCount: 40,
|
||||
falsePositiveCount: 33
|
||||
},
|
||||
{
|
||||
name: '合规性校验工具',
|
||||
score: 86,
|
||||
falsePositiveRate: 1.8,
|
||||
falseNegativeRate: 2.5,
|
||||
usageRate: 68,
|
||||
usageCount: 720,
|
||||
falseNegativeCount: 18,
|
||||
falsePositiveCount: 13
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
institution: '武汉大学',
|
||||
score: 88,
|
||||
tools: [
|
||||
{
|
||||
name: '开源风险评估工具',
|
||||
score: 91,
|
||||
falsePositiveRate: 2.8,
|
||||
falseNegativeRate: 3.3,
|
||||
usageRate: 82,
|
||||
usageCount: 1120,
|
||||
falseNegativeCount: 37,
|
||||
falsePositiveCount: 31
|
||||
},
|
||||
{
|
||||
name: '依赖检查器V2.5',
|
||||
score: 85,
|
||||
falsePositiveRate: 3.9,
|
||||
falseNegativeRate: 2.9,
|
||||
usageRate: 73,
|
||||
usageCount: 880,
|
||||
falseNegativeCount: 25,
|
||||
falsePositiveCount: 34
|
||||
},
|
||||
{
|
||||
name: '协议解析工具',
|
||||
score: 87,
|
||||
falsePositiveRate: 1.5,
|
||||
falseNegativeRate: 2.1,
|
||||
usageRate: 91,
|
||||
usageCount: 1350,
|
||||
falseNegativeCount: 28,
|
||||
falsePositiveCount: 20
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
institution: '信息支援部队工程大学',
|
||||
score: 94,
|
||||
tools: [
|
||||
{
|
||||
name: '军密级检测工具',
|
||||
score: 96,
|
||||
falsePositiveRate: 1.2,
|
||||
falseNegativeRate: 1.8,
|
||||
usageRate: 95,
|
||||
usageCount: 1560,
|
||||
falseNegativeCount: 28,
|
||||
falsePositiveCount: 19
|
||||
},
|
||||
{
|
||||
name: '供应链审计系统',
|
||||
score: 93,
|
||||
falsePositiveRate: 2.1,
|
||||
falseNegativeRate: 2.4,
|
||||
usageRate: 88,
|
||||
usageCount: 1080,
|
||||
falseNegativeCount: 26,
|
||||
falsePositiveCount: 23
|
||||
},
|
||||
{
|
||||
name: '涉密代码识别工具',
|
||||
score: 90,
|
||||
falsePositiveRate: 1.7,
|
||||
falseNegativeRate: 1.5,
|
||||
usageRate: 79,
|
||||
usageCount: 920,
|
||||
falseNegativeCount: 14,
|
||||
falsePositiveCount: 16
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// 2. 根据“机构key”筛选数据(逻辑不变)
|
||||
let filteredData: any[];
|
||||
switch (institutionKey) {
|
||||
case '全部':
|
||||
// 全部机构:聚合所有工具,保留“所属机构”字段
|
||||
filteredData = allInstitutionData.reduce((acc, curr) => {
|
||||
const toolWithInstitution = curr.tools.map(tool => ({
|
||||
...tool,
|
||||
belongInstitution: curr.institution
|
||||
}));
|
||||
return [...acc, ...toolWithInstitution];
|
||||
}, [] as any[]);
|
||||
break;
|
||||
case '华中科技大学':
|
||||
case '武汉大学':
|
||||
case '信息支援部队工程大学':
|
||||
// 单个机构:返回该机构完整数据
|
||||
filteredData = allInstitutionData.filter(item => item.institution === institutionKey);
|
||||
break;
|
||||
default:
|
||||
filteredData = [];
|
||||
}
|
||||
|
||||
// 3. 模拟接口返回(直接返回筛选后的数据,父组件按需取对应类型的次数)
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: filteredData // 数据中包含所有次数字段,父组件按rankType取对应值
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// 实际接口调用:携带“机构key”和“排行类型”参数
|
||||
// return request({
|
||||
// url: `/trust/data/toolRank`,
|
||||
// method: 'get',
|
||||
// params: {
|
||||
// institutionKey,
|
||||
// rankType // 传排行类型给后端
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
// 工具精度数据接口
|
||||
export function getToolAccuracyData(institutionKey: string): Promise<any> {
|
||||
// 直接返回模拟数据
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
// 不同机构的精度数据
|
||||
const accuracyMap = {
|
||||
'华中科技大学': {
|
||||
falsePositiveRate: 2.5,
|
||||
falseNegativeRate: 3.2,
|
||||
bestTool: '代码扫描工具V2.1',
|
||||
coverageRate: 92
|
||||
},
|
||||
'武汉大学': {
|
||||
falsePositiveRate: 3.1,
|
||||
falseNegativeRate: 2.8,
|
||||
bestTool: '协议解析工具',
|
||||
coverageRate: 88
|
||||
},
|
||||
'信息支援部队工程大学': {
|
||||
falsePositiveRate: 1.5,
|
||||
falseNegativeRate: 1.7,
|
||||
bestTool: '军密级检测工具',
|
||||
coverageRate: 96
|
||||
}
|
||||
};
|
||||
|
||||
resolve({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: accuracyMap[institutionKey] || {
|
||||
falsePositiveRate: 0,
|
||||
falseNegativeRate: 0,
|
||||
bestTool: '暂无数据',
|
||||
coverageRate: 0
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// 实际接口调用(注释掉,使用模拟数据)
|
||||
// return request({
|
||||
// url: `/trust/data/toolAccuracy`,
|
||||
// method: 'get',
|
||||
// params: { institutionKey }
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取功能卡片数值数据
|
||||
* @param institutionKey - 机构标识(全部/华中科技大学/武汉大学/信息支援部队工程大学)
|
||||
* @returns 对应机构的功能卡片统计数据(申请数/入库数/任务数等)
|
||||
*/
|
||||
export function getFunctionCardData(institutionKey: InstitutionKey = '全部'): Promise<any> {
|
||||
// 1. 定义各机构的功能卡片原始数据(模拟不同机构的数值差异)
|
||||
const allInstitutionCardData = {
|
||||
'全部': {
|
||||
statistics: {
|
||||
softwareGovernanceApply: '1286', // 软件治理申请数(全部机构总和)
|
||||
softwareSmartStorage: '856', // 软件智能入库数
|
||||
governanceTaskList: '498', // 治理任务列表数
|
||||
trustedSelectionSet: '18', // 可信选型设置数
|
||||
trustedSoftwareList: '1624' // 可信软件列表数
|
||||
}
|
||||
},
|
||||
'华中科技大学': {
|
||||
statistics: {
|
||||
softwareGovernanceApply: '428', // 华科申请数(占比约33%)
|
||||
softwareSmartStorage: '296', // 华科入库数
|
||||
governanceTaskList: '168', // 华科任务数
|
||||
trustedSelectionSet: '124', // 华科选型数
|
||||
trustedSoftwareList: '542' // 华科可信软件数
|
||||
}
|
||||
},
|
||||
'武汉大学': {
|
||||
statistics: {
|
||||
softwareGovernanceApply: '386', // 武大申请数
|
||||
storageCount: '258', // 武大入库数
|
||||
governanceTaskList: '152', // 武大任务数
|
||||
trustedSelectionSet: '112', // 武大选型数
|
||||
trustedSoftwareList: '486' // 武大可信软件数
|
||||
}
|
||||
},
|
||||
'信息支援部队工程大学': {
|
||||
statistics: {
|
||||
softwareGovernanceApply: '472', // 国防科大申请数(军工相关需求多,数值偏高)
|
||||
softwareSmartStorage: '302', // 国防科大入库数
|
||||
governanceTaskList: '178', // 国防科大任务数
|
||||
trustedSelectionSet: '140', // 国防科大选型数
|
||||
trustedSoftwareList: '596' // 国防科大可信软件数
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 根据传入的机构key,匹配对应数据(直接从原始数据中取值)
|
||||
const responseData = allInstitutionCardData[institutionKey] || allInstitutionCardData['全部'];
|
||||
|
||||
// 3. 模拟接口返回(统一格式:与工具排行接口保持一致的 code/message 结构)
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: responseData // 返回对应机构的统计数据(包含 statistics 字段)
|
||||
});
|
||||
}, 200); // 延迟200ms,模拟网络请求
|
||||
});
|
||||
|
||||
// 实际接口调用(后续替换为真实请求,携带机构参数)
|
||||
// return request({
|
||||
// url: `/trust/data/functionCard`,
|
||||
// method: 'get',
|
||||
// params: { institutionKey } // 传机构参数给后端
|
||||
// });
|
||||
}
|
||||
@@ -498,3 +498,12 @@ export function fetchCheckAuthenticate(data) {
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export const getNoticeAdd = (data): Promise<any> => {
|
||||
return request({
|
||||
url: `/notice/add`,
|
||||
method: 'POST',
|
||||
data
|
||||
});
|
||||
};
|
||||
|
||||
213
src/api/jyh/scanCenter.ts
Normal file
213
src/api/jyh/scanCenter.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
// @/api/scanCenter.ts
|
||||
/**
|
||||
* 获取历史检测记录
|
||||
* @param params 分页和筛选参数 { page, size, status }
|
||||
* @returns 历史记录列表
|
||||
*/
|
||||
export function getScanHistory(params: { page: number; size: number; status?: string }): Promise<any> {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
// 模拟历史记录数据
|
||||
const mockRecords = [
|
||||
{
|
||||
taskId: 'scan_1689234567890',
|
||||
target: 'https://example.com',
|
||||
type: 'url',
|
||||
status: 'completed',
|
||||
riskLevel: '高危',
|
||||
startTime: '2024-07-13T09:23:45',
|
||||
completeTime: '2024-07-13T09:25:12',
|
||||
duration: 87000,
|
||||
riskStats: { critical: 1, high: 3, medium: 2, low: 5 }
|
||||
},
|
||||
{
|
||||
taskId: 'scan_1689231234567',
|
||||
target: 'app-release.apk',
|
||||
type: 'file',
|
||||
status: 'completed',
|
||||
riskLevel: '中危',
|
||||
startTime: '2024-07-12T15:42:18',
|
||||
completeTime: '2024-07-12T15:47:33',
|
||||
duration: 315000,
|
||||
riskStats: { critical: 0, high: 0, medium: 2, low: 1 }
|
||||
},
|
||||
{
|
||||
taskId: 'scan_1689228901234',
|
||||
target: 'SBOM内容',
|
||||
type: 'sbom',
|
||||
status: 'failed',
|
||||
riskLevel: '-',
|
||||
startTime: '2024-07-12T14:15:30',
|
||||
completeTime: '2024-07-12T14:16:05',
|
||||
duration: 35000,
|
||||
errorMsg: '格式解析错误:缺少必填字段"version"'
|
||||
},
|
||||
{
|
||||
taskId: 'scan_1689225678901',
|
||||
target: 'https://test-api.com/v1',
|
||||
type: 'url',
|
||||
status: 'completed',
|
||||
riskLevel: '低危',
|
||||
startTime: '2024-07-12T10:08:22',
|
||||
completeTime: '2024-07-12T10:09:45',
|
||||
duration: 83000,
|
||||
riskStats: { critical: 0, high: 0, medium: 0, low: 2 }
|
||||
}
|
||||
];
|
||||
|
||||
// 筛选逻辑
|
||||
let filtered = mockRecords;
|
||||
if (params.status && params.status !== 'all') {
|
||||
filtered = filtered.filter(item => item.status === params.status);
|
||||
}
|
||||
|
||||
// 分页逻辑
|
||||
const total = filtered.length;
|
||||
const start = (params.page - 1) * params.size;
|
||||
const records = filtered.slice(start, start + params.size);
|
||||
|
||||
resolve({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: {
|
||||
total,
|
||||
records,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
pages: Math.ceil(total / params.size)
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// 实际接口调用(注释掉,使用模拟数据)
|
||||
// return request({
|
||||
// url: `/scan/history`,
|
||||
// method: 'get',
|
||||
// params
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交检测任务
|
||||
* @param data 任务数据 { type, target, file?, content? }
|
||||
* @returns 任务信息
|
||||
*/
|
||||
export function submitScanTask(data: {
|
||||
type: 'file' | 'url' | 'sbom';
|
||||
target: string;
|
||||
file?: FormData;
|
||||
content?: string;
|
||||
}): Promise<any> {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
// 生成唯一任务ID
|
||||
const taskId = `scan_${Date.now()}`;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
resolve({
|
||||
code: 200,
|
||||
message: '任务提交成功',
|
||||
data: {
|
||||
taskId,
|
||||
type: data.type,
|
||||
target: data.target,
|
||||
status: 'pending', // 初始状态:等待中
|
||||
progress: 0,
|
||||
startTime: now,
|
||||
estimatedCompleteTime: new Date(Date.now() + 300000).toISOString(), // 预计5分钟后完成
|
||||
currentStep: '等待调度'
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// 实际接口调用(注释掉,使用模拟数据)
|
||||
// return request({
|
||||
// url: `/scan/submit`,
|
||||
// method: 'post',
|
||||
// data
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检测任务状态
|
||||
* @param taskId 任务ID
|
||||
* @returns 任务当前状态信息
|
||||
*/
|
||||
export function getScanStatus(taskId: string): Promise<any> {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
// 解析任务ID中的时间戳(模拟任务创建时间)
|
||||
const createTime = parseInt(taskId.split('_')[1] || Date.now().toString());
|
||||
const now = Date.now();
|
||||
const elapsed = now - createTime;
|
||||
|
||||
// 模拟状态流转
|
||||
let status: string, progress: number, currentStep: string, completeTime: string | null = null;
|
||||
|
||||
if (elapsed < 3000) { // 前3秒:等待中
|
||||
status = 'pending';
|
||||
progress = 0;
|
||||
currentStep = '等待调度';
|
||||
} else if (elapsed < 15000) { // 3-15秒:检测中(进度递增)
|
||||
status = 'scanning';
|
||||
progress = Math.min(Math.floor((elapsed - 3000) / 12000 * 100), 90);
|
||||
currentStep = progress < 30 ? '初始化检测环境' :
|
||||
progress < 60 ? '执行漏洞扫描' : '生成初步报告';
|
||||
} else { // 15秒后:完成(80%概率)或失败(20%概率)
|
||||
const isSuccess = Math.random() > 0.2;
|
||||
status = isSuccess ? 'completed' : 'failed';
|
||||
progress = 100;
|
||||
completeTime = new Date().toISOString();
|
||||
|
||||
if (isSuccess) {
|
||||
currentStep = '检测完成';
|
||||
} else {
|
||||
currentStep = '检测失败';
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟风险统计(仅当任务完成时)
|
||||
const riskStats = status === 'completed'
|
||||
? {
|
||||
critical: Math.floor(Math.random() * 2),
|
||||
high: Math.floor(Math.random() * 3),
|
||||
medium: Math.floor(Math.random() * 5),
|
||||
low: Math.floor(Math.random() * 8)
|
||||
}
|
||||
: null;
|
||||
|
||||
// 模拟错误信息(仅当任务失败时)
|
||||
const errorMsg = status === 'failed'
|
||||
? ['网络连接超时', '目标资源不可访问', '检测引擎异常'][Math.floor(Math.random() * 3)]
|
||||
: null;
|
||||
|
||||
resolve({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: {
|
||||
taskId,
|
||||
status,
|
||||
progress,
|
||||
currentStep,
|
||||
startTime: new Date(createTime).toISOString(),
|
||||
estimatedCompleteTime: new Date(createTime + 300000).toISOString(),
|
||||
completeTime,
|
||||
riskStats,
|
||||
errorMsg,
|
||||
totalChecks: status === 'completed' ? 20 + Math.floor(Math.random() * 30) : null,
|
||||
completedChecks: status === 'scanning' ? Math.floor(progress / 100 * 40) : null
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// 实际接口调用(注释掉,使用模拟数据)
|
||||
// return request({
|
||||
// url: `/scan/status/${taskId}`,
|
||||
// method: 'get'
|
||||
// });
|
||||
}
|
||||
@@ -1543,3 +1543,176 @@ export const getOriginalComment = (params: types.DiscussionCommentProps): Promis
|
||||
);
|
||||
}
|
||||
|
||||
// 获取漏洞数据
|
||||
export const getVulnDetail = (alertId: types.VulnDetailProps): Promise<any> => {
|
||||
return reqCatch(() =>
|
||||
request({
|
||||
url: `/api/v1/alerts/${alertId}`,
|
||||
method: 'get'
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// 获取漏洞数据
|
||||
export const getVulnAffected = (params: types.VulnDetailProps): Promise<any> => {
|
||||
return reqCatch(() =>
|
||||
request({
|
||||
url: `/api/v1/alerts/component`,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// 获取漏洞数据
|
||||
export const getVulnPatch = (params: types.VulnDetailProps): Promise<any> => {
|
||||
return reqCatch(() =>
|
||||
request({
|
||||
url: `/api/v1/alerts/patch`,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// 获取漏洞数据
|
||||
export const getVulnerabilitiespage = (data: any): Promise<any> => {
|
||||
return reqCatch(() =>
|
||||
request({
|
||||
url: `/org/api/v1/vulnerabilities/page`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
);
|
||||
};
|
||||
export const getVulnerabilitiesDetails = (id: any): Promise<any> => {
|
||||
return reqCatch(() =>
|
||||
request({
|
||||
url: `/org/api/v1/vulnerabilities/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
);
|
||||
};
|
||||
export const getVulnerabilitiesComponents = (data: any): Promise<any> => {
|
||||
return reqCatch(() =>
|
||||
request({
|
||||
url: `/org/api/v1/vulnerabilities/components/page`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const getVulnerabilitiesAlertsPatch = (params: any): Promise<any> => {
|
||||
return reqCatch(() =>
|
||||
request({
|
||||
url: `/api/v1/alerts/patch`,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const getVulnerabilitiesAlertsUsers = (id: any): Promise<any> => {
|
||||
return reqCatch(() =>
|
||||
request({
|
||||
url: `/org/api/v1/vulnerabilities/${id}/alerts`,
|
||||
method: 'get'
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const storageStartTask = (data: any): Promise<any> => {
|
||||
return reqCatch(() =>
|
||||
request({
|
||||
url: `/task/storage/startTask`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// 出库申请
|
||||
export const storageStartOutTask = (data: any): Promise<any> => {
|
||||
return reqCatch(() =>
|
||||
request({
|
||||
url: `/task/storage/startOutTask`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// 发送预警
|
||||
export const sendWarning = (data: any): Promise<any> => {
|
||||
return reqCatch(() =>
|
||||
request({
|
||||
url: `/org/api/v1/send/alert/alertPublish`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// 获取预警详情
|
||||
export const getEarlyWarningInfo = (data: any): Promise<any> => {
|
||||
return reqCatchV2(() =>
|
||||
request({
|
||||
url: '/org/api/v1/send/alert/alertInfo',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// 获取预警列表
|
||||
export const getGetAlertListPage = (data: any): Promise<any> => {
|
||||
return reqCatchV2(() =>
|
||||
request({
|
||||
url: '/org/api/v1/send/alert/getAlertListPage',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
);
|
||||
};
|
||||
// 获取预警处理人员列表
|
||||
export const getGetAlertUserListPage = (data: any): Promise<any> => {
|
||||
return reqCatchV2(() =>
|
||||
request({
|
||||
url: '/org/api/v1/send/alert/getNoticeDealList',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// 获取预警列表详情
|
||||
export const getGetAlertDetails = (id: any): Promise<any> => {
|
||||
return reqCatchV2(() =>
|
||||
request({
|
||||
url: `/org/api/v1/send/alert/getVulnerabilityDetail?vulnId=${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// 获取预警列表详情
|
||||
export const getNoticeDealCount = (id: any): Promise<any> => {
|
||||
return reqCatchV2(() =>
|
||||
request({
|
||||
url: `/org/api/v1/send/alert/getNoticeDealCount?vulnId=${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// 分页查询订阅预警信息接口
|
||||
export const getAlertSubscribesPage = (data: any): Promise<any> => {
|
||||
return reqCatchV2(() =>
|
||||
request({
|
||||
url: `/org/api/v1/send/alert/getAlertSubscribesPage`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
BIN
src/assets/img/footer-bg.fbfa26c4.png
Normal file
BIN
src/assets/img/footer-bg.fbfa26c4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
src/assets/img/footer-logo.10d1ef46.png
Normal file
BIN
src/assets/img/footer-logo.10d1ef46.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 228 KiB |
BIN
src/assets/img/header-logo.01f15c97.png
Normal file
BIN
src/assets/img/header-logo.01f15c97.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 232 KiB |
BIN
src/assets/img/qr_code.4a6eae3c.jpg
Normal file
BIN
src/assets/img/qr_code.4a6eae3c.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
src/assets/imgs/jyh/aiLogo2.png
Normal file
BIN
src/assets/imgs/jyh/aiLogo2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
@@ -2,7 +2,7 @@
|
||||
<d-input
|
||||
@keyup.enter="search"
|
||||
:key="$route.path + $route.query?.val" id="golbalSearch" class="g-header-search"
|
||||
v-model.trim="softwareName" placeholder="请输入软件名称搜索" @clear="handleClear" clearable >
|
||||
v-model.trim="softwareName" placeholder="请输入名称搜索" @clear="handleClear" clearable >
|
||||
<template #prepend>
|
||||
<d-dropdown>
|
||||
<div class="flex items-center gap-1 cursor-pointer">
|
||||
@@ -11,20 +11,23 @@
|
||||
</div>
|
||||
<template #menu>
|
||||
<ul class="list-menu">
|
||||
<li class="menu-item" @click="onCategoryChange('软件')">软件</li>
|
||||
<d-dropdown :position="position" :offset="0">
|
||||
<li class="menu-item">
|
||||
行业
|
||||
<i class="icon icon-chevron-right"></i>
|
||||
</li>
|
||||
<template #menu>
|
||||
<ul class="list-menu">
|
||||
<li class="menu-item" v-for="option in industryList" @click="onCategoryChange(option)">
|
||||
{{ option.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</d-dropdown>
|
||||
<!-- <li class="menu-item" @click="onCategoryChange('软件')">软件</li>-->
|
||||
<li class="menu-item" @click="onCategoryChange('漏洞')">漏洞</li>
|
||||
<li class="menu-item" @click="onCategoryChange('组件')">组件</li>
|
||||
<li class="menu-item" @click="onCategoryChange('情报')">情报</li>
|
||||
<!-- <d-dropdown :position="position" :offset="0">-->
|
||||
<!-- <li class="menu-item">-->
|
||||
<!-- 行业-->
|
||||
<!-- <i class="icon icon-chevron-right"></i>-->
|
||||
<!-- </li>-->
|
||||
<!-- <template #menu>-->
|
||||
<!-- <ul class="list-menu">-->
|
||||
<!-- <li class="menu-item" v-for="option in industryList" @click="onCategoryChange(option)">-->
|
||||
<!-- {{ option.name }}-->
|
||||
<!-- </li>-->
|
||||
<!-- </ul>-->
|
||||
<!-- </template>-->
|
||||
<!-- </d-dropdown>-->
|
||||
</ul>
|
||||
</template>
|
||||
</d-dropdown>
|
||||
@@ -48,7 +51,7 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const softwareName = ref('');
|
||||
const searchType = ref('软件');
|
||||
const searchType = ref('漏洞');
|
||||
|
||||
const position = ref(['right-start', 'right-end']);
|
||||
// const industryOptions = ref(['通信', '军工', '金融', '能源']);
|
||||
@@ -56,7 +59,7 @@ const position = ref(['right-start', 'right-end']);
|
||||
const placeholder = ref('「开搜」:一搜即达,开发者的AI搜索');
|
||||
const industryList = globalInfo.industryList;
|
||||
const onCategoryChange = (event) => {
|
||||
searchType.value = event?.name || '软件';
|
||||
searchType.value = event?.name || '漏洞';
|
||||
globalInfo.updateHeaderSearch({ softwareName: softwareName.value, searchType: event.value });
|
||||
}
|
||||
|
||||
|
||||
@@ -56,19 +56,19 @@ export default [
|
||||
asideMenu: 'drawer' // 左侧菜单抽屉模式
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'repoList',
|
||||
name: 'Search',
|
||||
component: () => import('@/views/Jyh/Search/index.vue'),
|
||||
meta: {
|
||||
title: '高级搜索',
|
||||
reportTitle: '搜索',
|
||||
className: 'w-full min-w-full',
|
||||
hiddenFooter: true,
|
||||
hasMobile: true,
|
||||
asideMenu: 'drawer' // 左侧菜单抽屉模式
|
||||
}
|
||||
},
|
||||
// {
|
||||
// path: 'repoList',
|
||||
// name: 'Search',
|
||||
// component: () => import('@/views/Jyh/Search/index.vue'),
|
||||
// meta: {
|
||||
// title: '高级搜索',
|
||||
// reportTitle: '搜索',
|
||||
// className: 'w-full min-w-full',
|
||||
// hiddenFooter: true,
|
||||
// hasMobile: true,
|
||||
// asideMenu: 'drawer' // 左侧菜单抽屉模式
|
||||
// }
|
||||
// },
|
||||
{
|
||||
path: 'repoDetail/:versionId',
|
||||
name: 'VersionDetail',
|
||||
@@ -120,6 +120,47 @@ export default [
|
||||
asideMenu: 'drawer' // 左侧菜单抽屉模式
|
||||
}
|
||||
},
|
||||
{
|
||||
// path: 'KnowledgeHub',
|
||||
// name: 'KnowledgeHub',
|
||||
path: 'repoList',
|
||||
name: 'Search',
|
||||
component: () => import('@/views/Jyh/KnowledgeHub/index.vue'),
|
||||
meta: {
|
||||
title: '安全智库',
|
||||
reportTitle: '安全智库',
|
||||
className: 'w-full min-w-full',
|
||||
hiddenFooter: true,
|
||||
hasMobile: true,
|
||||
asideMenu: 'drawer' // 左侧菜单抽屉模式
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'TestingCenter',
|
||||
name: 'TestingCenter',
|
||||
component: () => import('@/views/Jyh/TestingCenter/index.vue'),
|
||||
meta: {
|
||||
title: '安全检测中心',
|
||||
reportTitle: '安全检测中心',
|
||||
className: 'w-full min-w-full',
|
||||
hiddenFooter: true,
|
||||
hasMobile: true,
|
||||
asideMenu: 'drawer' // 左侧菜单抽屉模式
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'OurTeam',
|
||||
name: 'OurTeam',
|
||||
component: () => import('@/views/Jyh/OurTeam/index.vue'),
|
||||
meta: {
|
||||
title: '关于我们',
|
||||
reportTitle: '关于我们',
|
||||
className: 'w-full min-w-full',
|
||||
hiddenFooter: true,
|
||||
hasMobile: true,
|
||||
asideMenu: 'drawer' // 左侧菜单抽屉模式
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
279
src/views/Jyh/KnowledgeHub/Components/AIModal.vue
Normal file
279
src/views/Jyh/KnowledgeHub/Components/AIModal.vue
Normal 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>
|
||||
605
src/views/Jyh/KnowledgeHub/Components/BatchVerify.vue
Normal file
605
src/views/Jyh/KnowledgeHub/Components/BatchVerify.vue
Normal 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>
|
||||
84
src/views/Jyh/KnowledgeHub/Components/CountryModal.vue
Normal file
84
src/views/Jyh/KnowledgeHub/Components/CountryModal.vue
Normal 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>
|
||||
@@ -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>
|
||||
327
src/views/Jyh/KnowledgeHub/Components/IntelligenceList/index.vue
Normal file
327
src/views/Jyh/KnowledgeHub/Components/IntelligenceList/index.vue
Normal 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>
|
||||
84
src/views/Jyh/KnowledgeHub/Components/OrgModal.vue
Normal file
84
src/views/Jyh/KnowledgeHub/Components/OrgModal.vue
Normal 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>
|
||||
839
src/views/Jyh/KnowledgeHub/Components/OssQuery.vue
Normal file
839
src/views/Jyh/KnowledgeHub/Components/OssQuery.vue
Normal 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">-->
|
||||
<!--<!– <d-col :span="8">–>-->
|
||||
<!--<!– <d-form-item field="interestTag" label="属性标签">–>-->
|
||||
<!--<!– <d-input placeholder="请输入属性标签" v-model="newFormData.interestTag" />–>-->
|
||||
<!--<!– </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-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 || '--'" 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>
|
||||
@@ -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>-->
|
||||
<!-- <!– <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> –>-->
|
||||
<!-- </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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
228
src/views/Jyh/KnowledgeHub/index.vue
Normal file
228
src/views/Jyh/KnowledgeHub/index.vue
Normal file
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<div class="secret-container">
|
||||
<d-breadcrumb class="secret-breadcrumb">
|
||||
<gc-breadcrumb-item :to="{ name: 'home' }"
|
||||
><span class="title">首页</span></gc-breadcrumb-item>
|
||||
<gc-breadcrumb-item><span class="cur-title">安全智库</span></gc-breadcrumb-item>
|
||||
</d-breadcrumb>
|
||||
<d-tabs class="jyh-tabs" type="wrapped" v-model="selectTab">
|
||||
<d-tab id="VulnerabilityList" title="漏洞列表">
|
||||
<VulnerabilityList/>
|
||||
</d-tab>
|
||||
<d-tab id="OssQuery" title="可信组件列表">
|
||||
<OssQuery/>
|
||||
</d-tab>
|
||||
<d-tab id="BatchVerify" title="情报列表">
|
||||
<IntelligenceList/>
|
||||
</d-tab>
|
||||
</d-tabs>
|
||||
</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,watch } 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 OssQuery from './Components/OssQuery.vue';
|
||||
import VulnerabilityList from './Components/VulnerabilityList/index.vue';
|
||||
import IntelligenceList from './Components/IntelligenceList/index.vue';
|
||||
import BatchVerify from './Components/BatchVerify.vue';
|
||||
import { useAccountStore } from '@/stores/user';
|
||||
const { namespace } = getOrgInfo();
|
||||
|
||||
const { isLogin } = useAccountStore();
|
||||
// 获取当前路由
|
||||
const route = useRoute();
|
||||
|
||||
const selectTab = ref(route.query.tab || 'VulnerabilityList');
|
||||
|
||||
// 监听 tab 值变化,更新 URL 参数
|
||||
watch(selectTab, (newTab) => {
|
||||
router.replace({
|
||||
name: route.name,
|
||||
params: route.params,
|
||||
query: { ...route.query, tab: newTab }
|
||||
});
|
||||
});
|
||||
|
||||
// 监听 URL 参数变化,更新 tab 值
|
||||
watch(
|
||||
() => route.query.tab,
|
||||
(newTab) => {
|
||||
if (newTab) {
|
||||
selectTab.value = newTab;
|
||||
}
|
||||
},
|
||||
{ immediate: true } // 初始化时执行一次
|
||||
);
|
||||
|
||||
|
||||
// 删除弹窗
|
||||
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 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);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'devui-theme/styles-var/devui-var.scss';
|
||||
|
||||
.secret-container {
|
||||
position: relative;
|
||||
|
||||
.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 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>
|
||||
357
src/views/Jyh/KnowledgeHub/license.vue
Normal file
357
src/views/Jyh/KnowledgeHub/license.vue
Normal file
@@ -0,0 +1,357 @@
|
||||
<template>
|
||||
<div class="secret-container pt-2 px-20">
|
||||
<d-breadcrumb>
|
||||
<gc-breadcrumb-item :to="{ name: 'home' }"
|
||||
><span class="title">首页</span></gc-breadcrumb-item>
|
||||
<gc-breadcrumb-item><span class="title">高级搜索</span></gc-breadcrumb-item>
|
||||
<gc-breadcrumb-item><span class="cur-title">License</span></gc-breadcrumb-item>
|
||||
</d-breadcrumb>
|
||||
<div class="search-form mt-2">
|
||||
<d-form
|
||||
class="jyh-form"
|
||||
ref="formRef"
|
||||
:data="formData"
|
||||
:pop-postion="['right']"
|
||||
:rules="formRules"
|
||||
>
|
||||
<d-row :gutter="25" type="flex" class="docs-devui-row mb-4">
|
||||
<d-col style="flex: 1">
|
||||
<d-form-item field="title" label="搜索关键字" :show-feedback="false">
|
||||
<d-input
|
||||
style="width: 100%"
|
||||
v-model="formData.title"
|
||||
placeholder="关键字模糊搜索"
|
||||
maxLength="50"
|
||||
minLength="2"
|
||||
/>
|
||||
</d-form-item>
|
||||
</d-col>
|
||||
<d-col style="flex: 1">
|
||||
<d-form-item field="title" label="License编码" :show-feedback="false">
|
||||
<d-input
|
||||
style="width: 100%"
|
||||
v-model="formData.title"
|
||||
placeholder="License编码模糊搜索"
|
||||
maxLength="50"
|
||||
minLength="2"
|
||||
/>
|
||||
</d-form-item>
|
||||
</d-col>
|
||||
<d-col style="flex: 1">
|
||||
<d-form-item field="name" label="License名称" :show-feedback="false">
|
||||
<d-input
|
||||
style="width: 100%"
|
||||
v-model="formData.title"
|
||||
placeholder="License名称模糊搜索"
|
||||
maxLength="50"
|
||||
minLength="2"
|
||||
/>
|
||||
</d-form-item>
|
||||
</d-col>
|
||||
<d-col style="flex: 1">
|
||||
<d-form-item field="title" label="类型" :show-feedback="false">
|
||||
<d-select v-model="formData.select" :options="typeOptions" :allow-clear="true" />
|
||||
</d-form-item>
|
||||
</d-col>
|
||||
<d-col style="flex: 1">
|
||||
<d-form-item field="title" label="集成风险" :show-feedback="false">
|
||||
<d-select v-model="formData.select" :options="riskOptions" :allow-clear="true" />
|
||||
</d-form-item>
|
||||
</d-col>
|
||||
</d-row>
|
||||
<d-row :gutter="25" type="flex" class="docs-devui-row mb-5">
|
||||
<d-col style="flex: 1">
|
||||
<d-form-item field="title" label="适用类型" :show-feedback="false">
|
||||
<d-select v-model="formData.select" :options="useTypeOptions" :allow-clear="true" />
|
||||
</d-form-item>
|
||||
</d-col>
|
||||
<d-col style="flex: 1">
|
||||
<d-form-item field="title" label="是否可用" :show-feedback="false">
|
||||
<d-select v-model="formData.select" :options="isUseOptions" :allow-clear="true" />
|
||||
</d-form-item>
|
||||
</d-col>
|
||||
<d-col style="flex: 1">
|
||||
<d-form-item field="name" label="键值" :show-feedback="false">
|
||||
<d-input
|
||||
style="width: 100%"
|
||||
v-model="formData.title"
|
||||
placeholder="请输入键值"
|
||||
maxLength="50"
|
||||
minLength="2"
|
||||
/>
|
||||
</d-form-item>
|
||||
</d-col>
|
||||
<d-col style="flex: 1">
|
||||
<d-form-item field="title" label="创建时间" :show-feedback="false">
|
||||
<d-date-picker-pro v-model="formData.time1" :showTime="true"
|
||||
placeholder="请选择日期与时间"
|
||||
format="YYYY-MM-DD HH:mm:ss" />
|
||||
</d-form-item>
|
||||
</d-col>
|
||||
<d-col style="flex: 1">
|
||||
<d-form-item field="title" label="更新时间" :show-feedback="false">
|
||||
<d-date-picker-pro v-model="formData.time1" :showTime="true"
|
||||
placeholder="请选择日期与时间"
|
||||
format="YYYY-MM-DD HH:mm:ss" />
|
||||
</d-form-item>
|
||||
</d-col>
|
||||
</d-row>
|
||||
<gc-form-operation class="jyh-form-operation">
|
||||
<d-button class="mr-2" variant="solid">筛选</d-button>
|
||||
<d-button variant="solid" color="secondary">清空</d-button>
|
||||
</gc-form-operation>
|
||||
<d-button class="output-btn" icon="icon-share" variant="text">导出</d-button>
|
||||
</d-form>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<d-table class="jyh-table" v-if="tableData.length" :striped="false" :data="tableData" table-layout="auto">
|
||||
<d-column type="index" width="40"></d-column>
|
||||
<d-column field="code" header="License编码">
|
||||
<a>05822156</a>
|
||||
</d-column>
|
||||
<d-column field="name" header="License名称"></d-column>
|
||||
<d-column field="type" header="类型"></d-column>
|
||||
<d-column field="risk" header="集成风险"></d-column>
|
||||
<d-column field="useType" header="适用类型"></d-column>
|
||||
<d-column field="status" header="状态">
|
||||
<d-status>未开始</d-status>
|
||||
</d-column>
|
||||
<d-column field="key" header="键值"></d-column>
|
||||
<d-column field="created" header="创建时间"></d-column>
|
||||
<d-column field="updated" header="更新时间"></d-column>
|
||||
<!-- <d-column header="操作" width="300">-->
|
||||
<!-- <template #default="scope">-->
|
||||
<!-- <div class="flex">-->
|
||||
<!-- <div class="cursor-pointer flex items-center mr-[32px]" @click="editRow(scope.row)">-->
|
||||
<!-- <GIcon class="mr-[8px]" name="gt-plane-edit" />-->
|
||||
<!-- <span class="text-[#3B3E55] text-base font-normal">编辑</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="flex items-center mr-[32px]">-->
|
||||
<!-- <d-switch v-model="scope.row.status" class="mr-[8px]" @change="setClaStatus(scope.row)"-->
|
||||
<!-- :active-value="1" :inactive-value="0"> </d-switch>-->
|
||||
<!-- <span class="text-[#3B3E55] text-base font-normal">{{ scope.row.status === 0 ? '停用' : '启用' }}</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="cursor-pointer flex items-center" @click="openDelete(scope.row)">-->
|
||||
<!-- <GIcon class="mr-[8px]" name="gt-plane-delete" />-->
|
||||
<!-- <span class="text-[#3B3E55] text-base font-normal">删除</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
<!-- </d-column>-->
|
||||
</d-table>
|
||||
|
||||
<DataPanel v-else skeleton :card="false" :empty="true">
|
||||
</DataPanel>
|
||||
</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="changeIndex()"
|
||||
@page-size-change="changeSize()" />
|
||||
</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>
|
||||
</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 } 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';
|
||||
const { namespace } = getOrgInfo();
|
||||
const text = `贡献者协议(CLA)允许外部开发者向组织的代码库提交变更请求时,系统会检查用户是否已签署CLA。如果已签署, "CLA-bot"会在变更请求评论区更新签署信息;如果未签署,"CLA-bot"会引导外部贡献者查看并签署贡献者协议。`;
|
||||
|
||||
const tableData = ref([
|
||||
{code: '05822156', name:'联通智家通通中视频公司语音.',type:'客户指定预装软件(原预装软件)',risk:'中',useType:'软件',status:'未开始',key:'',created:'2025-03-10 17:25:19',updated:'2025-03-10 17:25:19'},
|
||||
{code: '05822156', name:'联通智家通通中视频公司语音.',type:'客户指定预装软件(原预装软件)',risk:'中',useType:'软件',status:'未开始',key:'',created:'2025-03-10 17:25:19',updated:'2025-03-10 17:25:19'},
|
||||
{code: '05822156', name:'联通智家通通中视频公司语音.',type:'客户指定预装软件(原预装软件)',risk:'中',useType:'软件',status:'未开始',key:'',created:'2025-03-10 17:25:19',updated:'2025-03-10 17:25:19'},
|
||||
{code: '05822156', name:'联通智家通通中视频公司语音.',type:'客户指定预装软件(原预装软件)',risk:'中',useType:'软件',status:'未开始',key:'',created:'2025-03-10 17:25:19',updated:'2025-03-10 17:25:19'},
|
||||
{code: '05822156', name:'联通智家通通中视频公司语音.',type:'客户指定预装软件(原预装软件)',risk:'中',useType:'软件',status:'未开始',key:'',created:'2025-03-10 17:25:19',updated:'2025-03-10 17:25:19'},
|
||||
{code: '05822156', name:'联通智家通通中视频公司语音.',type:'客户指定预装软件(原预装软件)',risk:'中',useType:'软件',status:'未开始',key:'',created:'2025-03-10 17:25:19',updated:'2025-03-10 17:25:19'},
|
||||
{code: '05822156', name:'联通智家通通中视频公司语音.',type:'客户指定预装软件(原预装软件)',risk:'中',useType:'软件',status:'未开始',key:'',created:'2025-03-10 17:25:19',updated:'2025-03-10 17:25:19'},
|
||||
{code: '05822156', name:'联通智家通通中视频公司语音.',type:'客户指定预装软件(原预装软件)',risk:'中',useType:'软件',status:'未开始',key:'',created:'2025-03-10 17:25:19',updated:'2025-03-10 17:25:19'},
|
||||
{code: '05822156', name:'联通智家通通中视频公司语音.',type:'客户指定预装软件(原预装软件)',risk:'中',useType:'软件',status:'未开始',key:'',created:'2025-03-10 17:25:19',updated:'2025-03-10 17:25:19'},
|
||||
{code: '05822156', name:'联通智家通通中视频公司语音.',type:'客户指定预装软件(原预装软件)',risk:'中',useType:'软件',status:'未开始',key:'',created:'2025-03-10 17:25:19',updated:'2025-03-10 17:25:19'},
|
||||
]);
|
||||
const formData = ref({ title: '', md_content: '',select: '',time1:null });
|
||||
const typeOptions = reactive(['Options1', 'Options2', 'Options3']);
|
||||
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 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);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'devui-theme/styles-var/devui-var.scss';
|
||||
|
||||
.secret-container {
|
||||
position: relative;
|
||||
|
||||
.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 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;
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1359
src/views/Jyh/OurTeam/index.vue
Normal file
1359
src/views/Jyh/OurTeam/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
581
src/views/Jyh/TestingCenter/ReportDetail.vue
Normal file
581
src/views/Jyh/TestingCenter/ReportDetail.vue
Normal file
@@ -0,0 +1,581 @@
|
||||
<template>
|
||||
<div class="report-detail">
|
||||
<!-- 报告头部信息 -->
|
||||
<div class="report-header">
|
||||
<div class="risk-level">
|
||||
<d-tag :color="getRiskColor(reportData.riskLevel)" size="lg">
|
||||
{{ reportData.riskLevel || '无风险' }}
|
||||
</d-tag>
|
||||
</div>
|
||||
<div class="report-title">
|
||||
<h2>安全检测报告 - {{ reportData.target }}</h2>
|
||||
<p class="report-time">生成时间:{{ formatTime(reportData.completeTime) }}</p>
|
||||
</div>
|
||||
<div class="report-actions">
|
||||
<d-button variant="text" @click="copyReportLink">
|
||||
<i class="fa fa-link mr-1"></i> 复制报告链接
|
||||
</d-button>
|
||||
<d-button variant="text" @click="downloadReport">
|
||||
<i class="fa fa-download mr-1"></i> 下载报告
|
||||
</d-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 基本信息卡片 -->
|
||||
<d-card class="report-card mt-4">
|
||||
<div slot="header" class="card-header">基本信息</div>
|
||||
<d-descriptions column="2" bordered>
|
||||
<d-description-item term="任务ID">{{ reportData.taskId || '--' }}</d-description-item>
|
||||
<d-description-item term="检测类型">{{ getTypeText(reportData.type) }}</d-description-item>
|
||||
<d-description-item term="检测目标">{{ reportData.target || '--' }}</d-description-item>
|
||||
<d-description-item term="文件大小" v-if="reportData.type === 'file'">
|
||||
{{ reportData.fileSize ? formatFileSize(reportData.fileSize) : '--' }}
|
||||
</d-description-item>
|
||||
<d-description-item term="开始时间">{{ formatTime(reportData.startTime) || '--' }}</d-description-item>
|
||||
<d-description-item term="完成时间">{{ formatTime(reportData.completeTime) || '--' }}</d-description-item>
|
||||
<d-description-item term="检测时长">{{ formatDuration(reportData.duration) || '--' }}</d-description-item>
|
||||
<d-description-item term="检测项总数">{{ reportData.totalChecks || 0 }} 项</d-description-item>
|
||||
</d-descriptions>
|
||||
</d-card>
|
||||
|
||||
<!-- 风险概览卡片 -->
|
||||
<d-card class="report-card mt-4">
|
||||
<div slot="header" class="card-header">风险概览</div>
|
||||
<div class="risk-overview">
|
||||
<div class="risk-stats">
|
||||
<div class="risk-stat-item">
|
||||
<div class="stat-value">{{ reportData.riskStats?.critical || 0 }}</div>
|
||||
<div class="stat-label">
|
||||
<d-tag color="#c7000b" size="sm">致命漏洞</d-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="risk-stat-item">
|
||||
<div class="stat-value">{{ reportData.riskStats?.high || 0 }}</div>
|
||||
<div class="stat-label">
|
||||
<d-tag color="#f66f6a" size="sm">高危漏洞</d-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="risk-stat-item">
|
||||
<div class="stat-value">{{ reportData.riskStats?.medium || 0 }}</div>
|
||||
<div class="stat-label">
|
||||
<d-tag color="#fac20a" size="sm">中危漏洞</d-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="risk-stat-item">
|
||||
<div class="stat-value">{{ reportData.riskStats?.low || 0 }}</div>
|
||||
<div class="stat-label">
|
||||
<d-tag color="#5e7ce0" size="sm">低危漏洞</d-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="risk-chart">
|
||||
<!-- 使用 DChart 组件实现饼图 -->
|
||||
<d-chart :option="riskChartOption" style="width: 100%; height: 200px"></d-chart>
|
||||
</div>
|
||||
</div>
|
||||
</d-card>
|
||||
|
||||
<!-- 漏洞详情卡片 -->
|
||||
<d-card class="report-card mt-4">
|
||||
<div slot="header" class="card-header">漏洞详情</div>
|
||||
<d-table
|
||||
:data="vulnerabilities"
|
||||
:show-loading="loading"
|
||||
table-layout="auto"
|
||||
>
|
||||
<d-column field="vulnId" header="漏洞编号" :width="160"></d-column>
|
||||
<d-column field="vulnName" header="漏洞名称"></d-column>
|
||||
<d-column field="severity" header="风险等级">
|
||||
<template #default="scope">
|
||||
<d-tag :color="getSeverityColor(scope.row.severity)">{{ getSeverityText(scope.row.severity) }}</d-tag>
|
||||
</template>
|
||||
</d-column>
|
||||
<d-column field="affectedComponents" header="受影响组件" :width="200">
|
||||
<template #default="scope">
|
||||
<div class="affected-components">
|
||||
<span v-for="(comp, idx) in scope.row.affectedComponents" :key="idx">
|
||||
{{ comp.name }}@{{ comp.version }}
|
||||
<template v-if="idx < scope.row.affectedComponents.length - 1">, </template>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</d-column>
|
||||
<d-column header="操作" :width="120">
|
||||
<template #default="scope">
|
||||
<d-button
|
||||
variant="text"
|
||||
size="sm"
|
||||
@click="showVulnDetail(scope.row)"
|
||||
>
|
||||
详情
|
||||
</d-button>
|
||||
</template>
|
||||
</d-column>
|
||||
</d-table>
|
||||
</d-card>
|
||||
|
||||
<!-- 修复建议卡片 -->
|
||||
<d-card class="report-card mt-4">
|
||||
<div slot="header" class="card-header">修复建议</div>
|
||||
<div class="fix-suggestions">
|
||||
<div v-if="reportData.fixSuggestions && reportData.fixSuggestions.length">
|
||||
<div class="suggestion-item" v-for="(item, idx) in reportData.fixSuggestions" :key="idx">
|
||||
<h4 class="suggestion-title">
|
||||
<i class="fa fa-lightbulb-o text-warning mr-2"></i>
|
||||
建议 {{ idx + 1 }}: {{ item.title }}
|
||||
</h4>
|
||||
<p class="suggestion-content">{{ item.content }}</p>
|
||||
<a
|
||||
v-if="item.reference"
|
||||
:href="item.reference"
|
||||
target="_blank"
|
||||
class="suggestion-link"
|
||||
>
|
||||
查看详细指南 <i class="fa fa-external-link ml-1"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-suggestions">
|
||||
未发现需要修复的问题或暂无具体修复建议
|
||||
</div>
|
||||
</div>
|
||||
</d-card>
|
||||
|
||||
<!-- 底部操作区 -->
|
||||
<div class="report-footer mt-6">
|
||||
<d-button @click="$emit('close')" variant="secondary">关闭</d-button>
|
||||
<d-button @click="rescan" class="ml-2">重新检测</d-button>
|
||||
</div>
|
||||
|
||||
<!-- 漏洞详情弹窗 -->
|
||||
<d-modal
|
||||
v-model="vulnDetailVisible"
|
||||
title="漏洞详情"
|
||||
:width="700"
|
||||
>
|
||||
<div v-if="currentVuln" class="vuln-detail-modal">
|
||||
<div class="vuln-detail-header">
|
||||
<h3>{{ currentVuln.vulnName }}</h3>
|
||||
<d-tag :color="getSeverityColor(currentVuln.severity)">{{ getSeverityText(currentVuln.severity) }}</d-tag>
|
||||
</div>
|
||||
<div class="vuln-detail-content mt-4">
|
||||
<div class="vuln-section">
|
||||
<h4 class="section-title">漏洞描述</h4>
|
||||
<p class="section-content">{{ currentVuln.description || '无详细描述' }}</p>
|
||||
</div>
|
||||
<div class="vuln-section mt-3">
|
||||
<h4 class="section-title">受影响组件</h4>
|
||||
<ul class="section-list">
|
||||
<li v-for="(comp, idx) in currentVuln.affectedComponents" :key="idx">
|
||||
{{ comp.name }} (版本: {{ comp.version }}) - 路径: {{ comp.path || '未知' }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="vuln-section mt-3">
|
||||
<h4 class="section-title">修复建议</h4>
|
||||
<p class="section-content">{{ currentVuln.fixSuggestion || '暂无具体修复建议' }}</p>
|
||||
</div>
|
||||
<div class="vuln-section mt-3">
|
||||
<h4 class="section-title">参考信息</h4>
|
||||
<div class="section-content">
|
||||
<p v-if="currentVuln.cveId">CVE: {{ currentVuln.cveId }}</p>
|
||||
<p v-if="currentVuln.cnnvdId">CNNVD: {{ currentVuln.cnnvdId }}</p>
|
||||
<a
|
||||
v-if="currentVuln.referenceUrl"
|
||||
:href="currentVuln.referenceUrl"
|
||||
target="_blank"
|
||||
class="reference-link"
|
||||
>
|
||||
官方漏洞详情 <i class="fa fa-external-link ml-1"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</d-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
// import { useToast } from 'devui';
|
||||
import { DChart } from 'vue-devui/echarts'; // 引入 DChart 组件
|
||||
|
||||
// 引入dayjs duration插件
|
||||
dayjs.extend(duration);
|
||||
|
||||
// 接收父组件传入的报告数据
|
||||
const props = defineProps({
|
||||
reportData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
// 组件内部状态
|
||||
const loading = ref(false);
|
||||
const vulnDetailVisible = ref(false);
|
||||
const currentVuln = ref(null);
|
||||
const toast = useToast();
|
||||
|
||||
// 漏洞列表数据(从报告数据中提取)
|
||||
const vulnerabilities = ref(props.reportData.vulnerabilities || []);
|
||||
|
||||
// 显示漏洞详情
|
||||
const showVulnDetail = (vuln) => {
|
||||
currentVuln.value = vuln;
|
||||
vulnDetailVisible.value = true;
|
||||
};
|
||||
|
||||
// 重新检测
|
||||
const rescan = () => {
|
||||
$emit('close');
|
||||
$emit('rescan', props.reportData);
|
||||
};
|
||||
|
||||
// 复制报告链接
|
||||
const copyReportLink = () => {
|
||||
const dummyLink = `${window.location.origin}/scan/report/${props.reportData.taskId}`;
|
||||
navigator.clipboard.writeText(dummyLink).then(() => {
|
||||
toast.success({ content: '报告链接已复制到剪贴板', duration: 2000 });
|
||||
}).catch(() => {
|
||||
toast.error({ content: '复制失败,请手动复制', duration: 2000 });
|
||||
});
|
||||
};
|
||||
|
||||
// 下载报告
|
||||
const downloadReport = () => {
|
||||
toast.info({ content: '准备下载报告...', duration: 2000 });
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time) => {
|
||||
return time ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : '--';
|
||||
};
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// 格式化时长
|
||||
const formatDuration = (ms) => {
|
||||
if (!ms) return '--';
|
||||
const d = dayjs.duration(ms, 'milliseconds');
|
||||
return `${d.minutes()}分${d.seconds()}秒`;
|
||||
};
|
||||
|
||||
// 风险等级颜色映射
|
||||
const getRiskColor = (level) => {
|
||||
const colorMap = {
|
||||
'致命': '#c7000b',
|
||||
'高危': '#f66f6a',
|
||||
'中危': '#fac20a',
|
||||
'低危': '#5e7ce0',
|
||||
'无风险': '#00b42a'
|
||||
};
|
||||
return colorMap[level] || 'default';
|
||||
};
|
||||
|
||||
// 检测类型文本映射
|
||||
const getTypeText = (type) => {
|
||||
const typeMap = {
|
||||
'file': '文件上传',
|
||||
'url': 'URL地址',
|
||||
'sbom': 'SBOM内容'
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
};
|
||||
|
||||
// 漏洞严重程度文本映射
|
||||
const getSeverityText = (severity) => {
|
||||
const severityMap = {
|
||||
'critical': '致命',
|
||||
'high': '高危',
|
||||
'medium': '中危',
|
||||
'low': '低危'
|
||||
};
|
||||
return severityMap[severity] || severity;
|
||||
};
|
||||
|
||||
// 漏洞严重程度颜色映射
|
||||
const getSeverityColor = (severity) => {
|
||||
const colorMap = {
|
||||
'critical': '#c7000b',
|
||||
'high': '#f66f6a',
|
||||
'medium': '#fac20a',
|
||||
'low': '#5e7ce0'
|
||||
};
|
||||
return colorMap[severity] || 'default';
|
||||
};
|
||||
|
||||
// 风险图表配置(基于 echarts 格式)
|
||||
const riskChartOption = computed(() => {
|
||||
const riskStats = props.reportData.riskStats || {
|
||||
critical: 0,
|
||||
high: 0,
|
||||
medium: 0,
|
||||
low: 0
|
||||
};
|
||||
|
||||
// 转换为 echarts 所需的数据格式
|
||||
const chartData = [
|
||||
{ name: '致命漏洞', value: riskStats.critical, itemStyle: { color: '#c7000b' } },
|
||||
{ name: '高危漏洞', value: riskStats.high, itemStyle: { color: '#f66f6a' } },
|
||||
{ name: '中危漏洞', value: riskStats.medium, itemStyle: { color: '#fac20a' } },
|
||||
{ name: '低危漏洞', value: riskStats.low, itemStyle: { color: '#5e7ce0' } }
|
||||
].filter(item => item.value > 0); // 过滤掉数量为0的项
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
right: 10,
|
||||
top: 'center',
|
||||
textStyle: {
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '漏洞数量',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 4,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: chartData
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
// 监听报告数据变化,更新漏洞列表
|
||||
watch(() => props.reportData, () => {
|
||||
vulnerabilities.value = props.reportData.vulnerabilities || [];
|
||||
});
|
||||
|
||||
// 定义组件输出事件
|
||||
const emit = defineEmits(['close', 'rescan']);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'devui-theme/styles-var/devui-var.scss';
|
||||
|
||||
.report-detail {
|
||||
padding: 16px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.report-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.risk-level {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.report-title {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
|
||||
.report-time {
|
||||
color: var(--devui-text-secondary);
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.report-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.report-card {
|
||||
--devui-card-padding: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--devui-text-primary);
|
||||
}
|
||||
|
||||
.risk-overview {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
|
||||
.risk-stats {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
|
||||
.risk-stat-item {
|
||||
text-align: center;
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: var(--devui-text-primary);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.risk-chart {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.affected-components {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.fix-suggestions {
|
||||
padding: 8px 0;
|
||||
|
||||
.suggestion-item {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px dashed var(--devui-border);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.suggestion-title {
|
||||
font-weight: 600;
|
||||
color: var(--devui-text-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.suggestion-content {
|
||||
color: var(--devui-text-secondary);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.suggestion-link {
|
||||
color: var(--devui-primary);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-suggestions {
|
||||
color: var(--devui-text-secondary);
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.report-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.vuln-detail-modal {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.vuln-detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.vuln-section {
|
||||
padding: 8px 0;
|
||||
|
||||
.section-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: var(--devui-text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
color: var(--devui-text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.section-list {
|
||||
color: var(--devui-text-secondary);
|
||||
padding-left: 20px;
|
||||
|
||||
li {
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.reference-link {
|
||||
color: var(--devui-primary);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .devui-descriptions {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
::v-deep .devui-table {
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
750
src/views/Jyh/TestingCenter/index.vue
Normal file
750
src/views/Jyh/TestingCenter/index.vue
Normal file
@@ -0,0 +1,750 @@
|
||||
<template>
|
||||
<d-breadcrumb class="secret-breadcrumb">
|
||||
<gc-breadcrumb-item :to="{ name: 'home' }"><span class="title">首页</span></gc-breadcrumb-item>
|
||||
<gc-breadcrumb-item>
|
||||
<span class="cur-title">安全检测中心</span>
|
||||
</gc-breadcrumb-item>
|
||||
</d-breadcrumb>
|
||||
|
||||
<div class="page-wrap">
|
||||
<!-- 检测方式选择区域 -->
|
||||
<Card class="upload-card">
|
||||
<div class="upload-title">选择检测方式</div>
|
||||
|
||||
<div class="upload-tabs">
|
||||
<d-tabs v-model="activeUploadType" @change="handleUploadTypeChange">
|
||||
<d-tab id="file" title="上传文件">
|
||||
<div class="upload-area" @click="triggerFileUpload" :class="{ dragging: isDragging }">
|
||||
<input
|
||||
type="file"
|
||||
ref="fileInput"
|
||||
class="file-input"
|
||||
@change="handleFileUpload"
|
||||
@dragover.prevent="isDragging = true"
|
||||
@dragleave.prevent="isDragging = false"
|
||||
@drop.prevent="handleFileDrop"
|
||||
>
|
||||
<i class="fa fa-cloud-upload text-primary text-4xl mb-2"></i>
|
||||
<p>点击或拖拽文件到此处上传</p>
|
||||
<p class="text-sm text-gray-medium">支持格式:.zip, .tar, .gz, .json (最大100MB)</p>
|
||||
</div>
|
||||
</d-tab>
|
||||
|
||||
<d-tab id="url" title="输入URL">
|
||||
<d-form layout="horizontal">
|
||||
<d-form-item field="scanUrl" label="检测URL">
|
||||
<d-input
|
||||
v-model="scanUrl"
|
||||
placeholder="请输入需要检测的URL地址(例如:https://example.com)"
|
||||
style="width: 100%"
|
||||
></d-input>
|
||||
</d-form-item>
|
||||
<d-form-item>
|
||||
<d-button
|
||||
@click="startUrlScan"
|
||||
variant="solid"
|
||||
:disabled="!scanUrl"
|
||||
>
|
||||
开始检测
|
||||
</d-button>
|
||||
</d-form-item>
|
||||
</d-form>
|
||||
</d-tab>
|
||||
|
||||
<d-tab id="sbom" title="输入SBOM">
|
||||
<d-form layout="horizontal">
|
||||
<d-form-item field="sbomContent" label="SBOM内容">
|
||||
<d-textarea
|
||||
v-model="sbomContent"
|
||||
placeholder="请输入SBOM内容(支持SPDX或CycloneDX格式)"
|
||||
:rows="8"
|
||||
style="width: 100%"
|
||||
></d-textarea>
|
||||
</d-form-item>
|
||||
<d-form-item>
|
||||
<d-button
|
||||
@click="startSbomScan"
|
||||
variant="solid"
|
||||
:disabled="!sbomContent"
|
||||
>
|
||||
开始检测
|
||||
</d-button>
|
||||
</d-form-item>
|
||||
</d-form>
|
||||
</d-tab>
|
||||
</d-tabs>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- 检测状态展示 -->
|
||||
<Card class="mt-4" v-if="currentScan">
|
||||
|
||||
<div class="scan-status-title">
|
||||
<h3>当前检测状态</h3>
|
||||
<div class="upload-title">当前检测状态</div>
|
||||
<d-tag :color="getStatusColor(currentScan.status)">{{ getStatusText(currentScan.status) }}</d-tag>
|
||||
</div>
|
||||
|
||||
<div class="scan-progress mt-4">
|
||||
<d-progress
|
||||
:percentage="currentScan.progress"
|
||||
:status="getProgressStatus(currentScan.status)"
|
||||
stroke-width="6"
|
||||
></d-progress>
|
||||
<p class="progress-text mt-2">
|
||||
{{ currentScan.progress }}% 完成 - {{ currentScan.currentStep || '准备开始检测' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="scan-details mt-4" v-if="currentScan.status !== 'pending'">
|
||||
<d-descriptions column="1" bordered>
|
||||
<d-description-item term="检测目标">{{ currentScan.target }}</d-description-item>
|
||||
<d-description-item term="开始时间">{{ formatTime(currentScan.startTime) }}</d-description-item>
|
||||
<d-description-item term="预计完成时间" v-if="currentScan.status === 'scanning'">
|
||||
{{ formatTime(currentScan.estimatedCompleteTime) }}
|
||||
</d-description-item>
|
||||
<d-description-item term="完成时间" v-if="currentScan.status === 'completed' || currentScan.status === 'failed'">
|
||||
{{ formatTime(currentScan.completeTime) }}
|
||||
</d-description-item>
|
||||
<d-description-item term="检测项" v-if="currentScan.totalChecks">
|
||||
{{ currentScan.completedChecks }}/{{ currentScan.totalChecks }}
|
||||
</d-description-item>
|
||||
</d-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="scan-actions mt-4" v-if="currentScan.status === 'completed'">
|
||||
<d-button @click="showReportDetails(currentScan)" variant="solid">查看报告详情</d-button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- 历史检测记录 -->
|
||||
<Card class="mt-4">
|
||||
<div class="upload-title">历史检测记录</div>
|
||||
<div class="history-title">
|
||||
<d-select
|
||||
style="width: 30%"
|
||||
v-model="historyFilter"
|
||||
:options="filterOptions"
|
||||
placeholder="筛选状态"
|
||||
></d-select>
|
||||
</div>
|
||||
|
||||
<d-table
|
||||
:show-loading="loading"
|
||||
class="jyh-table"
|
||||
:data="filteredHistory"
|
||||
v-if="filteredHistory.length > 0"
|
||||
table-layout="auto"
|
||||
>
|
||||
<d-column type="index" width="40"></d-column>
|
||||
<d-column field="target" header="检测目标" :width="300">
|
||||
<template #default="scope">
|
||||
<div class="target-text">{{ scope.row.target }}</div>
|
||||
</template>
|
||||
</d-column>
|
||||
<d-column field="type" header="检测类型">
|
||||
<template #default="scope">
|
||||
<d-tag :type="getTypeColor(scope.row.type)">{{ getTypeText(scope.row.type) }}</d-tag>
|
||||
</template>
|
||||
</d-column>
|
||||
<d-column field="status" header="状态">
|
||||
<template #default="scope">
|
||||
<d-tag :type="getStatusColor(scope.row.status)">{{ getStatusText(scope.row.status) }}</d-tag>
|
||||
</template>
|
||||
</d-column>
|
||||
<d-column field="startTime" header="开始时间">
|
||||
<template #default="scope">{{ formatTime(scope.row.startTime) }}</template>
|
||||
</d-column>
|
||||
<d-column field="riskLevel" header="风险等级">
|
||||
<template #default="scope">
|
||||
<d-tag :color="getRiskColor(scope.row.riskLevel)">{{ scope.row.riskLevel || '无风险' }}</d-tag>
|
||||
</template>
|
||||
</d-column>
|
||||
<d-column header="操作" fixed-right="0px" width="130">
|
||||
<template #default="scope">
|
||||
<a class="devui-link" @click="showReportDetails(scope.row)" v-if="scope.row.status === 'completed'">
|
||||
查看报告
|
||||
</a>
|
||||
<a class="devui-link" @click="rescan(scope.row)" v-else-if="scope.row.status === 'failed'">
|
||||
重新检测
|
||||
</a>
|
||||
<span v-else>--</span>
|
||||
</template>
|
||||
</d-column>
|
||||
</d-table>
|
||||
|
||||
<NoData v-else :small="false"></NoData>
|
||||
|
||||
<div class="mt-20 mb-20 flex justify-end" v-if="filteredHistory.length > 0">
|
||||
<d-pagination
|
||||
size="md"
|
||||
:page-size-options="[10, 20, 50]"
|
||||
:total="pager.total"
|
||||
v-model:pageSize="pager.pageSize"
|
||||
v-model:pageIndex="pager.pageIndex"
|
||||
:max-items="5"
|
||||
:can-change-page-size="true"
|
||||
:can-view-total="true"
|
||||
total-item-text="总计"
|
||||
@page-index-change="getHistoryList"
|
||||
@page-size-change="getHistoryList"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 报告详情抽屉 -->
|
||||
<d-drawer
|
||||
v-model="reportVisible"
|
||||
:width="800"
|
||||
title="检测报告详情"
|
||||
>
|
||||
<ReportDetail
|
||||
:report-data="currentReport"
|
||||
@close="reportVisible = false"
|
||||
></ReportDetail>
|
||||
</d-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted,onUnmounted, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import NoData from '@/components/NoData/NoData.vue';
|
||||
import ReportDetail from './ReportDetail.vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { getScanHistory, submitScanTask, getScanStatus } from '@/api/jyh/scanCenter';
|
||||
|
||||
const route = useRoute();
|
||||
const title = ref('安全检测中心');
|
||||
|
||||
// 上传相关变量
|
||||
const activeUploadType = ref('file');
|
||||
const fileInput = ref(null);
|
||||
const scanUrl = ref('');
|
||||
const sbomContent = ref('');
|
||||
const isDragging = ref(false);
|
||||
|
||||
// 检测状态相关
|
||||
// 替换原有的 const currentScan = ref(null);
|
||||
const currentScan = ref({
|
||||
taskId: 'scan_1689234567890', // 任务唯一标识
|
||||
type: 'file', // 检测类型:file/url/sbom
|
||||
target: 'app-release-v2.1.apk', // 检测目标
|
||||
status: 'scanning', // 任务状态:pending/scanning/completed/failed
|
||||
progress: 65, // 检测进度(0-100)
|
||||
startTime: '2024-07-15T10:30:22', // 开始时间
|
||||
estimatedCompleteTime: '2024-07-15T10:35:10', // 预计完成时间
|
||||
currentStep: '执行漏洞深度扫描', // 当前执行步骤
|
||||
totalChecks: 42, // 总检测项数
|
||||
completedChecks: 27, // 已完成检测项数
|
||||
// 以下字段在状态为 completed 时存在
|
||||
// riskStats: { critical: 1, high: 3, medium: 2, low: 5 },
|
||||
// completeTime: '2024-07-15T10:34:55',
|
||||
// 以下字段在状态为 failed 时存在
|
||||
// errorMsg: '检测引擎连接超时'
|
||||
});
|
||||
const scanInterval = ref(null);
|
||||
const loading = ref(false);
|
||||
|
||||
// 分页相关
|
||||
const pager = ref({
|
||||
total: 0,
|
||||
pageIndex: 1,
|
||||
pageSize: 10
|
||||
});
|
||||
|
||||
// 历史记录相关
|
||||
// 模拟历史检测记录数据(可直接替换到组件的 historyList 初始化中)
|
||||
const historyList = ref([
|
||||
// 文件检测 - 已完成(高危)
|
||||
{
|
||||
taskId: 'scan_1720012345678',
|
||||
target: 'enterprise-app-v3.2.1.zip',
|
||||
type: 'file',
|
||||
status: 'completed',
|
||||
riskLevel: '高危',
|
||||
startTime: '2024-07-01T09:15:30',
|
||||
completeTime: '2024-07-01T09:28:45',
|
||||
duration: 810000, // 13分30秒
|
||||
riskStats: { critical: 1, high: 4, medium: 3, low: 2 },
|
||||
fileSize: 28560000 // 约28.5MB
|
||||
},
|
||||
// URL检测 - 已完成(低危)
|
||||
{
|
||||
taskId: 'scan_1720015678901',
|
||||
target: 'https://internal-api.company.com',
|
||||
type: 'url',
|
||||
status: 'completed',
|
||||
riskLevel: '低危',
|
||||
startTime: '2024-07-01T14:30:22',
|
||||
completeTime: '2024-07-01T14:32:10',
|
||||
duration: 108000, // 1分48秒
|
||||
riskStats: { critical: 0, high: 0, medium: 0, low: 1 }
|
||||
},
|
||||
// SBOM检测 - 失败
|
||||
{
|
||||
taskId: 'scan_1720018901234',
|
||||
target: 'SBOM-cyclonedx-project.json',
|
||||
type: 'sbom',
|
||||
status: 'failed',
|
||||
riskLevel: '-',
|
||||
startTime: '2024-07-02T10:05:18',
|
||||
completeTime: '2024-07-02T10:06:03',
|
||||
duration: 45000, // 45秒
|
||||
errorMsg: 'SBOM格式错误:缺少components字段'
|
||||
},
|
||||
// 文件检测 - 已完成(中危)
|
||||
{
|
||||
taskId: 'scan_1720022345678',
|
||||
target: 'mobile-client-v2.8.apk',
|
||||
type: 'file',
|
||||
status: 'completed',
|
||||
riskLevel: '中危',
|
||||
startTime: '2024-07-02T16:40:55',
|
||||
completeTime: '2024-07-02T16:52:30',
|
||||
duration: 695000, // 11分35秒
|
||||
riskStats: { critical: 0, high: 0, medium: 2, low: 5 },
|
||||
fileSize: 42800000 // 约42.8MB
|
||||
},
|
||||
// URL检测 - 进行中
|
||||
{
|
||||
taskId: 'scan_1720025678901',
|
||||
target: 'https://admin-portal.company.com',
|
||||
type: 'url',
|
||||
status: 'scanning',
|
||||
riskLevel: '-',
|
||||
startTime: '2024-07-03T08:12:10',
|
||||
progress: 65,
|
||||
currentStep: '检测API接口漏洞'
|
||||
},
|
||||
// SBOM检测 - 已完成(无风险)
|
||||
{
|
||||
taskId: 'scan_1720028901234',
|
||||
target: 'SBOM-spdx-v2.3.json',
|
||||
type: 'sbom',
|
||||
status: 'completed',
|
||||
riskLevel: '无风险',
|
||||
startTime: '2024-07-03T11:30:00',
|
||||
completeTime: '2024-07-03T11:31:20',
|
||||
duration: 80000, // 1分20秒
|
||||
riskStats: { critical: 0, high: 0, medium: 0, low: 0 }
|
||||
},
|
||||
// 文件检测 - 失败
|
||||
{
|
||||
taskId: 'scan_1720032345678',
|
||||
target: 'legacy-system.tar.gz',
|
||||
type: 'file',
|
||||
status: 'failed',
|
||||
riskLevel: '-',
|
||||
startTime: '2024-07-03T15:20:40',
|
||||
completeTime: '2024-07-03T15:21:10',
|
||||
duration: 30000, // 30秒
|
||||
errorMsg: '文件损坏:无法解压缩归档内容',
|
||||
fileSize: 157000000 // 约157MB
|
||||
},
|
||||
// URL检测 - 已完成(致命)
|
||||
{
|
||||
taskId: 'scan_1720035678901',
|
||||
target: 'https://old-website.company.com',
|
||||
type: 'url',
|
||||
status: 'completed',
|
||||
riskLevel: '致命',
|
||||
startTime: '2024-07-04T09:50:15',
|
||||
completeTime: '2024-07-04T09:51:50',
|
||||
duration: 95000, // 1分35秒
|
||||
riskStats: { critical: 2, high: 1, medium: 0, low: 0 }
|
||||
},
|
||||
// SBOM检测 - 已完成(中危)
|
||||
{
|
||||
taskId: 'scan_1720038901234',
|
||||
target: 'microservice-sbom.xml',
|
||||
type: 'sbom',
|
||||
status: 'completed',
|
||||
riskLevel: '中危',
|
||||
startTime: '2024-07-04T13:18:30',
|
||||
completeTime: '2024-07-04T13:19:45',
|
||||
duration: 75000, // 1分15秒
|
||||
riskStats: { critical: 0, high: 0, medium: 1, low: 3 }
|
||||
},
|
||||
// 文件检测 - 已完成(高危)
|
||||
{
|
||||
taskId: 'scan_1720042345678',
|
||||
target: 'desktop-software-v5.1.exe',
|
||||
type: 'file',
|
||||
status: 'completed',
|
||||
riskLevel: '高危',
|
||||
startTime: '2024-07-05T10:08:22',
|
||||
completeTime: '2024-07-05T10:25:10',
|
||||
duration: 1008000, // 16分48秒
|
||||
riskStats: { critical: 0, high: 3, medium: 2, low: 1 },
|
||||
fileSize: 85600000 // 约85.6MB
|
||||
}
|
||||
]);
|
||||
const historyFilter = ref('all');
|
||||
const filterOptions = ref([
|
||||
{ value: 'all', name: '全部状态' },
|
||||
{ value: 'completed', name: '已完成' },
|
||||
{ value: 'scanning', name: '检测中' },
|
||||
{ value: 'failed', name: '失败' }
|
||||
]);
|
||||
|
||||
// 报告详情相关
|
||||
const reportVisible = ref(false);
|
||||
const currentReport = ref(null);
|
||||
|
||||
// 切换上传类型
|
||||
const handleUploadTypeChange = (key) => {
|
||||
activeUploadType.value = key;
|
||||
};
|
||||
|
||||
// 文件上传相关方法
|
||||
const triggerFileUpload = () => {
|
||||
fileInput.value?.click();
|
||||
};
|
||||
|
||||
const handleFileUpload = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
startFileScan(file);
|
||||
// 清空输入以允许重复上传同一文件
|
||||
e.target.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileDrop = (e) => {
|
||||
isDragging.value = false;
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file) {
|
||||
startFileScan(file);
|
||||
}
|
||||
};
|
||||
|
||||
// 开始不同类型的检测
|
||||
const startFileScan = async (file) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const { data } = await submitScanTask({
|
||||
type: 'file',
|
||||
file: formData,
|
||||
target: file.name
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
currentScan.value = data.data;
|
||||
startScanPolling();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('文件检测提交失败', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const startUrlScan = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const { data } = await submitScanTask({
|
||||
type: 'url',
|
||||
target: scanUrl.value
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
currentScan.value = data.data;
|
||||
startScanPolling();
|
||||
scanUrl.value = '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('URL检测提交失败', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const startSbomScan = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const { data } = await submitScanTask({
|
||||
type: 'sbom',
|
||||
content: sbomContent.value,
|
||||
target: 'SBOM内容'
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
currentScan.value = data.data;
|
||||
startScanPolling();
|
||||
sbomContent.value = '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('SBOM检测提交失败', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 轮询获取扫描状态
|
||||
const startScanPolling = () => {
|
||||
// 清除之前的定时器
|
||||
if (scanInterval.value) {
|
||||
clearInterval(scanInterval.value);
|
||||
}
|
||||
|
||||
// 立即获取一次状态
|
||||
fetchScanStatus();
|
||||
|
||||
// 设置定时器
|
||||
scanInterval.value = setInterval(() => {
|
||||
fetchScanStatus();
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
const fetchScanStatus = async () => {
|
||||
if (!currentScan.value?.taskId) return;
|
||||
|
||||
try {
|
||||
const { data } = await getScanStatus(currentScan.value.taskId);
|
||||
if (data.success) {
|
||||
currentScan.value = data.data;
|
||||
|
||||
// 如果扫描完成或失败,停止轮询
|
||||
if (['completed', 'failed'].includes(currentScan.value.status)) {
|
||||
clearInterval(scanInterval.value);
|
||||
scanInterval.value = null;
|
||||
// 刷新历史列表
|
||||
getHistoryList();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取扫描状态失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取历史记录
|
||||
const getHistoryList = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const { data } = await getScanHistory({
|
||||
page: pager.value.pageIndex,
|
||||
size: pager.value.pageSize,
|
||||
status: historyFilter.value !== 'all' ? historyFilter.value : ''
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
historyList.value = data.data.records;
|
||||
pager.value.total = data.data.total;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取历史记录失败', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 查看报告详情
|
||||
const showReportDetails = async (record) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
// 这里应该调用获取报告详情的API
|
||||
currentReport.value = record; // 临时使用记录数据,实际应替换为API调用
|
||||
reportVisible.value = true;
|
||||
} catch (error) {
|
||||
console.error('获取报告详情失败', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 重新检测
|
||||
const rescan = (record) => {
|
||||
switch (record.type) {
|
||||
case 'file':
|
||||
activeUploadType.value = 'file';
|
||||
break;
|
||||
case 'url':
|
||||
activeUploadType.value = 'url';
|
||||
scanUrl.value = record.target;
|
||||
break;
|
||||
case 'sbom':
|
||||
activeUploadType.value = 'sbom';
|
||||
sbomContent.value = record.content || '';
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time) => {
|
||||
return time ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : '--';
|
||||
};
|
||||
|
||||
// 状态文本映射
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
'pending': '等待中',
|
||||
'scanning': '检测中',
|
||||
'completed': '已完成',
|
||||
'failed': '失败'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
};
|
||||
|
||||
// 状态颜色映射
|
||||
const getStatusColor = (status) => {
|
||||
const colorMap = {
|
||||
'pending': 'primary',
|
||||
'scanning': 'processing',
|
||||
'completed': 'success',
|
||||
'failed': 'danger'
|
||||
};
|
||||
return colorMap[status] || 'default';
|
||||
};
|
||||
|
||||
// 进度条状态映射
|
||||
const getProgressStatus = (status) => {
|
||||
if (status === 'failed') return 'error';
|
||||
if (status === 'completed') return 'success';
|
||||
if (status === 'scanning') return 'processing';
|
||||
return 'active';
|
||||
};
|
||||
|
||||
// 检测类型文本映射
|
||||
const getTypeText = (type) => {
|
||||
const typeMap = {
|
||||
'file': '文件',
|
||||
'url': 'URL',
|
||||
'sbom': 'SBOM'
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
};
|
||||
|
||||
// 检测类型颜色映射
|
||||
const getTypeColor = (type) => {
|
||||
const colorMap = {
|
||||
'file': 'primary',
|
||||
'url': 'info',
|
||||
'sbom': 'secondary'
|
||||
};
|
||||
return colorMap[type] || 'default';
|
||||
};
|
||||
|
||||
// 风险等级颜色映射
|
||||
const getRiskColor = (level) => {
|
||||
const colorMap = {
|
||||
'高危': 'danger',
|
||||
'中危': 'warning',
|
||||
'低危': 'info',
|
||||
'无风险': 'success'
|
||||
};
|
||||
return colorMap[level] || 'default';
|
||||
};
|
||||
|
||||
// 筛选历史记录
|
||||
const filteredHistory = computed(() => {
|
||||
if (historyFilter.value === 'all') {
|
||||
return historyList.value;
|
||||
}
|
||||
return historyList.value.filter(item => item.status === historyFilter.value);
|
||||
});
|
||||
|
||||
// 监听筛选条件变化
|
||||
watch(historyFilter, () => {
|
||||
pager.value.pageIndex = 1;
|
||||
getHistoryList();
|
||||
});
|
||||
|
||||
// 页面加载时获取历史记录
|
||||
onMounted(() => {
|
||||
getHistoryList();
|
||||
});
|
||||
|
||||
// 组件卸载时清除定时器
|
||||
onUnmounted(() => {
|
||||
if (scanInterval.value) {
|
||||
clearInterval(scanInterval.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-wrap {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.upload-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.upload-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
color: #191919;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--devui-primary);
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
border-color: var(--devui-primary);
|
||||
background-color: rgba(22, 93, 255, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scan-status-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.scan-progress {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
color: var(--devui-text-secondary);
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.history-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.target-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
|
||||
.secret-breadcrumb {
|
||||
padding: 7px 20px 12px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
///deep/ .devui-tabs-content {
|
||||
// padding: 16px 0;
|
||||
//}
|
||||
//
|
||||
///deep/ .devui-form-item {
|
||||
// margin-bottom: 16px;
|
||||
//}
|
||||
</style>
|
||||
Reference in New Issue
Block a user