4aed1325 by lihua

修改下载报告

1 parent c89a85a1
......@@ -149,6 +149,9 @@ export const dataSourceTypeList = [{
}, {
value: 2,
label: '文件导入'
}, {
value: 3,
label: '文件夹'
}];
/** 获取数据库选择列表 */
......@@ -341,3 +344,10 @@ export const exportAnonReport = (params) => request({
method: 'post',
responseType: 'blob'
})
export const htmlToWord = (params) => request({
url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/download/html-to-word`,
method: 'postJsonD',
data: params,
responseType: 'blob'
})
\ No newline at end of file
......
......@@ -8,12 +8,14 @@ import {
getAnonAnalyzePageData,
getAnonAnalyzeResult,
getAnonTaskDetail,
htmlToWord,
} from '@/api/modules/dataAnonymization';
import { changeNum, download } from '@/utils/common';
import { ElMessage } from 'element-plus';
import anonResultAnalysis from './components/anonResultAnalysis.vue';
import { commonPageConfig } from '@/utils/enum';
import { calcColumnWidth } from '@/utils';
import html2canvas from 'html2canvas';
const route = useRoute();
const router = useRouter();
......@@ -52,6 +54,8 @@ const originResultTableFieldColumn = ref({});
/** 结果分析中的字段表格数据 */
const resultData: any = ref([]);
/** 全部未分页的数据,下载word时使用。 */
const fullResultData: any = ref([]);
/** 结果分析中的字段信息 */
const analysisResultTableFields: any = ref([]);
......@@ -100,16 +104,30 @@ watch(
}
);
const getAnalysisResultPageData = () => {
const getAnalysisResultPageData = (isFull = false) => {
analysisResultLoading.value = true;
getAnonAnalyzePageData({
pageIndex: pageInfo.value.curr,
pageSize: pageInfo.value.limit,
pageSize: isFull ? -1 : pageInfo.value.limit,
taskExecGuid: taskExecGuid.value,
}).then((res: any) => {
analysisResultLoading.value = false;
if (res?.code == proxy.$passCode) {
pageInfo.value.rows =
if (isFull) {
fullResultData.value = [];
res.data?.records?.forEach(d => {
let obj = {};
analysisResultTableFields.value.forEach(t => {
obj[t.enName] = d.fieldValue?.[t.enName];
});
obj['equivalenceClassNum'] = changeNum(d.equivalenceClassNum || 0, 0);
obj['reIdentifyRisk'] = changeNum(d.reIdentifyRisk || 0, 2);
obj['isGtThreshold'] = d.isGtThreshold;
fullResultData.value.push(obj);
});
resultData.value = fullResultData.value.slice(0, pageInfo.value.limit);
pageInfo.value.rows = fullResultData.value.length;
} else {
resultData.value = [];
res.data?.records?.forEach(d => {
let obj = {};
......@@ -122,30 +140,82 @@ const getAnalysisResultPageData = () => {
resultData.value.push(obj);
});
pageInfo.value.rows = res.data?.totalRows ?? 0;
}
} else {
proxy.$ElMessage.error(res.msg);
}
})
}
const isWordStyle = ref(false);
/** 下载评估报告 */
const transfer = () => {
isWordStyle.value = true;
}
const domClone: any = ref(null);
const resultReportRef = ref();
const convertHtml2Img = (dom, domClone) => {
const element = <HTMLElement>dom.querySelector('.kpi-content')
if (!element) {
return Promise.resolve();
}
return html2canvas(element, {
allowTaint: true,
useCORS: true,
scale: 2,
}).then((canvas: any) => {
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
element.parentNode && ((<HTMLElement>element.parentNode).scrollTop = 0);
let url = canvas.toDataURL('image/jpeg');
let img = document.createElement('img');
if (url) {
// img.src = url.split(',')[1];
img.src = url;
img.width = 620;
img.height = 265;
img.crossOrigin = 'Anonymous';
}
const copyElement = <HTMLElement>domClone.querySelector('.kpi-content')
copyElement.parentNode?.replaceChild(img, copyElement);
})
}
const loadingText = ref('');
const getHTML = (reportResultContent) => {
let html = reportResultContent;
html = html.replace(/"/g, "'");
return html;
};
const downloadWord = () => {
if (downPromise.value) {
return;
}
downPromise.value = exportAnonReport({
taskGuid: route.query.guid,
execGuid: taskExecGuid.value
}).then((res: any) => {
downPromise.value = null;
let dom = domClone.value || (domClone.value = document.createElement('div'));
let report = resultReportRef.value?.report;
dom.innerHTML = report?.innerHTML;
resultDataLoading.value = true;
loadingText.value = '报告正在下载中,请勿关闭浏览器...';
downPromise.value = convertHtml2Img(report, dom).then(() => {
htmlToWord({ html: encodeURIComponent(`<div>${getHTML(dom.innerHTML)}</div>`) }).then((res: any) => {
downPromise.value = null
loadingText.value = '';
resultDataLoading.value = false;
if (res && !res.msg) {
download(res, (route.query.taskName || oldAnonTaskValueInfo.value.taskName) + '_匿名化评估报告.docx', 'word')
} else {
res?.msg && ElMessage.error(res?.msg);
}
})
}).catch(() => {
downPromise.value = null;
})
});
}
onMounted(() => {
......@@ -165,7 +235,7 @@ onBeforeMount(() => {
analysisResultInfo.value = res.data || {};
analysisResultTableFields.value = res.data?.column || [];
pageInfo.value.curr = 1;
getAnalysisResultPageData();
getAnalysisResultPageData(true);
} else {
res?.msg && proxy.$ElMessage.error(res.msg);
}
......@@ -182,13 +252,24 @@ onBeforeMount(() => {
</script>
<template>
<div class="table_tool_wrap" v-loading="resultDataLoading" ref="containerRef">
<el-button style="margin-bottom: 8px;" type="primary" @click="transfer" v-preReClick>下载评估报告</el-button>
<anonResultAnalysis :show-title="true" :analysis-result-info="analysisResultInfo"
:analysis-result-loading="analysisResultLoading" :analysis-result-table-fields="analysisResultTableFields"
:old-anon-task-value-info="oldAnonTaskValueInfo" :container-width="containerWidth"
:origin-result-table-field-column="originResultTableFieldColumn" :page-info="pageInfo" :result-data="resultData"
@page-change="pageChange"></anonResultAnalysis>
<div class="table_tool_wrap" v-loading="resultDataLoading" ref="containerRef" :element-loading-text="loadingText">
<el-button v-show="!isWordStyle" style="margin-bottom: 8px;" type="primary" @click="transfer"
v-preReClick>生成Word评估报告</el-button>
<div v-show="isWordStyle" style="margin-bottom: 8px;">
<el-button @click="isWordStyle = false">返回</el-button>
<el-button type="primary" @click="downloadWord">下载评估报告</el-button>
</div>
<anonResultAnalysis ref="resultReportRef" :show-title="true" :analysis-result-info="analysisResultInfo"
:isWordStyle="isWordStyle" :style="isWordStyle ? {
height: 'calc(100% - 36px)',
'overflow-y': 'auto',
'margin-right': '-16px',
'padding-right': '16px',
width: 'auto'
} : null" :analysis-result-loading="analysisResultLoading"
:analysis-result-table-fields="analysisResultTableFields" :old-anon-task-value-info="oldAnonTaskValueInfo"
:container-width="containerWidth" :origin-result-table-field-column="originResultTableFieldColumn"
:page-info="pageInfo" :result-data="resultData" :fullResultData="fullResultData" @page-change="pageChange"></anonResultAnalysis>
</div>
</template>
......
......@@ -17,7 +17,8 @@
formId="model-select-edit" col="col3 custom-form" @select-change="handleDataSelectFormSelectChange"
@uploadFileChange="uploadFileChange" @checkboxChange="handleDataSelectFormCheckboxChange" />
</ContentWrap>
<ContentWrap id="id-previewData" title="数据抽样预览" description="" style="margin-top: 16px;">
<ContentWrap v-show="formRef?.formInline?.dataSource != 3" id="id-previewData" title="数据抽样预览" description=""
style="margin-top: 16px;">
<!-- 选择抽样预览的表单设置 -->
<Form ref="dataSimpleFormRef" :itemList="dataSimpleFormItems" :rules="dataSimpleFormRules"
formId="data-simple-edit" col="col3 fixwidth-form" @switch-change="handleDataSimpleFormSwitchChange"
......@@ -47,6 +48,51 @@
</div>
</div>
</ContentWrap>
<ContentWrap v-show="formRef?.formInline?.dataSource == 3" id="id-folder" title="提取文件" description=""
style="margin-top: 16px;">
<div class="folder-main">
<el-upload ref="uploadRef" class="upload-file" action="#" :file-list="folderList" :limit="1" directory
:http-request="(file) => hanlderUploadFolder(file)">
<template #trigger>
<el-button :icon="Upload" class="mr8">上传文件</el-button>
</template>
</el-upload>
<!-- 正在解析的状态 -->
<div class="folder-progress">
<div class="folder-title">正在解析</div>
<el-progress :percentage="30" :stroke-width="12" striped striped-flow :show-text="false" :duration="8" />
<div style="display: flex;"><span class="cnt">{{ '共' + changeNum(100, 0) + '个文件, 已解析30%' }}</span></div>
</div>
<!-- 解析失败的状态 -->
<div class="folder-progress">
<div class="folder-title">
<el-icon class="title-icon fail">
<CircleCloseFilled />
</el-icon><span>解析失败</span>
</div>
<el-progress :percentage="30" :stroke-width="12" color="#F30000" :show-text="false" />
<div style="display: flex;"><span class="cnt">{{ '已成功解析' + changeNum(100, 0) + '个文件, 失败' + changeNum(30,
0) +
'个文件' }}</span></div>
<div style="text-align: center;">
<el-button @click="reAnalyzing">重试</el-button>
</div>
</div>
<!-- 解析成功状态 -->
<div class="folder-progress">
<div class="folder-title">
<el-icon class="title-icon success">
<svg-icon name="icon-success" />
</el-icon><span>解析成功</span>
</div>
<el-progress :percentage="30" :stroke-width="12" color="#F30000" :show-text="false" />
<div style="display: flex;"><span class="cnt">{{ '已成功解析' + changeNum(100, 0) + '个文件, 失败' + changeNum(30,
0) +
'个文件' }}</span></div>
</div>
</div>
</ContentWrap>
</div>
<!-- 第二步 配置匿名化方案,单独抽取vue组件页面 -->
<anonTaskStepTwo ref="anonTaskStepTwoRef" v-show="step == 1" :anonTaskRules="detailInfo.anonTaskRules"
......@@ -70,13 +116,19 @@
<div v-show="analysisResultInfo.errorMsg" class="error-desc">{{ '【' + analysisResultInfo.errorMsg + '】' }}
</div>
</div>
<anonResultAnalysis v-show="isExecEnd && analysisResultInfo.status == 'Y'" :analysis-result-info="analysisResultInfo"
:analysis-result-loading="analysisResultLoading" :analysis-result-table-fields="analysisResultTableFields" :old-anon-task-value-info="oldAnonTaskValueInfo"
:container-width="containerWidth" :origin-result-table-field-column="originResultTableFieldColumn" :page-info="pageInfo"
:result-data="resultData" @page-change="pageChange"></anonResultAnalysis>
<anonResultAnalysis v-show="isExecEnd && analysisResultInfo.status == 'Y'" ref="resultReportRef" v-loading="downloadLoading"
:analysis-result-info="analysisResultInfo" :is-word-style="isWordStyle" :element-loading-text="loadingText"
:analysis-result-loading="analysisResultLoading" :analysis-result-table-fields="analysisResultTableFields"
:old-anon-task-value-info="oldAnonTaskValueInfo" :container-width="containerWidth"
:origin-result-table-field-column="originResultTableFieldColumn" :page-info="pageInfo"
:result-data="resultData" :fullResultData="fullResultData" @page-change="pageChange"></anonResultAnalysis>
<template #header>
<el-button v-show="isExecEnd && analysisResultInfo.status == 'Y'" type="primary" v-loading="!!downPromise"
@click="transfer">下载评估报告</el-button>
<el-button v-show="isExecEnd && analysisResultInfo.status == 'Y' && !isWordStyle" type="primary"
v-loading="!!downPromise" @click="transfer">生成Word评估报告</el-button>
<div v-show="isWordStyle">
<el-button @click="isWordStyle = false">返回</el-button>
<el-button type="primary" @click="downloadWord">下载评估报告</el-button>
</div>
</template>
</ContentWrap>
</div>
......@@ -105,7 +157,9 @@
<el-button v-show="formRef?.formInline?.handleType != '02'" type="primary"
:disabled="analysisResultInfo.status == 'R' || (isExecEnd && analysisResultInfo.status == 'E')"
@click="changeStep(4)">下一步</el-button>
<el-button type="primary" v-show="formRef?.formInline?.handleType == '02'" :disabled="analysisResultInfo.status == 'R' || (isExecEnd && analysisResultInfo.status == 'E')" v-preReClick @click="closeTask">关闭</el-button>
<el-button type="primary" v-show="formRef?.formInline?.handleType == '02'"
:disabled="analysisResultInfo.status == 'R' || (isExecEnd && analysisResultInfo.status == 'E')" v-preReClick
@click="closeTask">关闭</el-button>
</template>
<template v-else>
<el-button @click="changeStep(3)">上一步</el-button>
......@@ -131,6 +185,7 @@ import {
updateAnonTask,
exportAnonExecData,
exportAnonReport,
htmlToWord,
} from '@/api/modules/dataAnonymization';
import {
parseAndDecodeUrl,
......@@ -154,8 +209,9 @@ import anonResultView from './anonResultView.vue';
import useDataAnonymizationStore from "@/store/modules/dataAnonymization";
import { RefreshRight, CircleCloseFilled, Right } from "@element-plus/icons-vue";
import { commonPageConfig } from '@/components/PageNav';
import { QuestionFilled } from "@element-plus/icons-vue";
import { Upload } from "@element-plus/icons-vue";
import anonResultAnalysis from './components/anonResultAnalysis.vue';
import html2canvas from 'html2canvas';
const anonymizationStore = useDataAnonymizationStore();
const { proxy } = getCurrentInstance() as any;
......@@ -845,6 +901,19 @@ const uploadFileChange = (file) => {
parseFileData(fileRaw);
}
/*** ----------------------- 解析扫描文件 ------------------------------ */
const folderList: any = ref([]);
const hanlderUploadFolder = (file) => {
debugger
return Promise.resolve();
}
/** 重试 */
const reAnalyzing = () => {
}
/** 第二步的配置组件引用。 */
const anonTaskStepTwoRef = ref();
......@@ -1259,7 +1328,7 @@ const processStepThreeResultView = (isRefresh = false) => {
refreshTimer.value = null;
analysisResultTableFields.value = res.data?.column || [];
pageInfo.value.curr = 1;
getAnalysisResultPageData();
getAnalysisResultPageData(true);
} else if (analysisResultInfo.value.status == 'E') {
isExecEnd.value = true
refreshTimer.value && clearInterval(refreshTimer.value);
......@@ -1302,6 +1371,9 @@ const originResultTableFieldColumn = ref({});
/** 结果分析中的字段表格数据 */
const resultData: any = ref([]);
/** 不分页的全部数据 */
const fullResultData: any = ref([]);
/** 结果分析中的字段信息 */
const analysisResultTableFields: any = ref([]);
......@@ -1329,16 +1401,30 @@ watch(
}
);
const getAnalysisResultPageData = () => {
const getAnalysisResultPageData = (isFull = false) => {
analysisResultLoading.value = true;
getAnonAnalyzePageData({
pageIndex: pageInfo.value.curr,
pageSize: pageInfo.value.limit,
pageSize: isFull ? -1 : pageInfo.value.limit,
taskExecGuid: taskExecGuid.value,
}).then((res: any) => {
analysisResultLoading.value = false;
if (res?.code == proxy.$passCode) {
pageInfo.value.rows =
if (isFull) {
fullResultData.value = [];
res.data?.records?.forEach(d => {
let obj = {};
analysisResultTableFields.value.forEach(t => {
obj[t.enName] = d.fieldValue?.[t.enName];
});
obj['equivalenceClassNum'] = changeNum(d.equivalenceClassNum || 0, 0);
obj['reIdentifyRisk'] = changeNum(d.reIdentifyRisk || 0, 2);
obj['isGtThreshold'] = d.isGtThreshold;
fullResultData.value.push(obj);
});
resultData.value = fullResultData.value.slice(0, pageInfo.value.limit);
pageInfo.value.rows = fullResultData.value.length;
} else {
resultData.value = [];
res.data?.records?.forEach(d => {
let obj = {};
......@@ -1351,6 +1437,7 @@ const getAnalysisResultPageData = () => {
resultData.value.push(obj);
});
pageInfo.value.rows = res.data?.totalRows ?? 0;
}
} else {
proxy.$ElMessage.error(res.msg);
}
......@@ -1359,24 +1446,76 @@ const getAnalysisResultPageData = () => {
const downPromise: any = ref()
const isWordStyle = ref(false);
/** 下载评估报告 */
const transfer = () => {
isWordStyle.value = true;
}
const domClone: any = ref(null);
const resultReportRef = ref();
const convertHtml2Img = (dom, domClone) => {
const element = <HTMLElement>dom.querySelector('.kpi-content')
if (!element) {
return Promise.resolve();
}
return html2canvas(element, {
allowTaint: true,
useCORS: true,
scale: 2,
}).then((canvas: any) => {
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
element.parentNode && ((<HTMLElement>element.parentNode).scrollTop = 0);
let url = canvas.toDataURL('image/jpeg');
let img = document.createElement('img');
if (url) {
// img.src = url.split(',')[1];
img.src = url;
img.width = 620;
img.height = 265;
img.crossOrigin = 'Anonymous';
}
const copyElement = <HTMLElement>domClone.querySelector('.kpi-content')
copyElement.parentNode?.replaceChild(img, copyElement);
})
}
const loadingText = ref('');
const downloadLoading = ref(false);
const getHTML = (reportResultContent) => {
let html = reportResultContent;
html = html.replace(/"/g, "'");
return html;
};
const downloadWord = () => {
if (downPromise.value) {
return;
}
downPromise.value = exportAnonReport({
taskGuid: route.query.guid,
execGuid: taskExecGuid.value
}).then((res: any) => {
downPromise.value = null;
let dom = domClone.value || (domClone.value = document.createElement('div'));
let report = resultReportRef.value?.report;
dom.innerHTML = report?.innerHTML;
loadingText.value = '报告正在下载中,请勿关闭浏览器...';
downloadLoading.value = true;
downPromise.value = convertHtml2Img(report, dom).then(() => {
htmlToWord({ html: encodeURIComponent(`<div>${getHTML(dom.innerHTML)}</div>`) }).then((res: any) => {
downPromise.value = null
loadingText.value = '';
downloadLoading.value = false;
if (res && !res.msg) {
download(res, (route.query.taskName || oldAnonTaskValueInfo.value.taskName) + '_匿名化评估报告.docx', 'word')
} else {
res?.msg && ElMessage.error(res?.msg);
}
})
}).catch(() => {
downPromise.value = null;
})
});
}
onUnmounted(() => {
......@@ -1704,4 +1843,57 @@ onUnmounted(() => {
justify-content: space-between;
}
}
.folder-main {
height: 170px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
.folder-title {
font-size: 14px;
color: #212121;
font-weight: 600;
margin-bottom: 16px;
text-align: center;
line-height: 21px;
display: flex;
justify-content: center;
}
:deep(.el-icon) {
margin-right: 8px;
width: 20px;
height: 20px;
&.fail {
color: #E63E33;
}
&.success {
color: #4FA55D;
}
svg {
width: 100%;
height: 100%;
}
}
.folder-progress {
width: 360px;
.cnt {
font-size: 14px;
color: #212121;
line-height: 21px;
margin-top: 8px;
}
.el-button {
margin-top: 12px;
}
}
}
</style>
\ No newline at end of file
......
<template>
<div class="analysis-result-main">
<div style="display: flex;justify-content: center;width: 100%;">
<div v-show="!isWordStyle" class="analysis-result-main">
<div v-if="showTitle" class="result-title">匿名结果分析</div>
<div class="kpi-content" v-show="Object.keys(analysisResultInfo).length > 0">
<div class="border-content">
......@@ -53,7 +54,7 @@
<span class="text">等价类门限风险<el-tooltip placement="top" effect="light" popper-class="table_tooltip">
<template #content>
<div style="max-width: 236px;">
完全公开共享数据发布,门限阈值取值 1/20;受控公开共享数据发布,门限阈值取值 1/5;领地公开共享数据发布,门限阈值取值
完全公开共享数据发布,门限阈值取值 1/20;受控公开共享数据发布,门限阈值取值 1/5;领地公开共享数据发布,门限阈值取值
1/3;等价类门限风险为:等价类的重标识风险大于门限阈值为1,小于等于为0,求和后除以等价类个数。
</div>
</template>
......@@ -89,8 +90,8 @@
</div>
<div class="result-title">重标识风险表</div>
<el-table ref="tableRef" v-show="analysisResultTableFields.length" :data="resultData"
v-loading="analysisResultLoading" :highlight-current-row="true" stripe border tooltip-effect="light" height="100%"
row-key="guid" :style="{ width: '100%', height: '280px' }">
v-loading="analysisResultLoading" :highlight-current-row="true" stripe border tooltip-effect="light"
height="100%" row-key="guid" :style="{ width: '100%', height: '280px' }">
<el-table-column label="等价类" type="index" width="68px" align="center" show-overflow-tooltip>
<template #default="scope">
<span>{{
......@@ -134,7 +135,8 @@
:style="{ height: oldAnonTaskValueInfo.dataSharingTypeCode != '01' ? (containerWidth > 1397 ? '378px' : (containerWidth < 1048 ? '414px' : '396px')) : 'auto' }">
<el-table-column label="序号" type="index" width="56px" align="center" show-overflow-tooltip>
</el-table-column>
<el-table-column label="数据项" prop="chName" width="150px" align="left" show-overflow-tooltip></el-table-column>
<el-table-column label="数据项" prop="chName" width="150px" align="left"
show-overflow-tooltip></el-table-column>
<el-table-column label="唯一性分值" prop="uniqueScore" width="110px" align="right"
show-overflow-tooltip></el-table-column>
<el-table-column label="影响力分值" prop="influenceScore" width="110" align="right"
......@@ -187,12 +189,161 @@
</div>
</div>
</div>
<div v-show="isWordStyle" class="analysis-result-main report-main" ref="report">
<div
style="font-family: simsun;height: 40px;display: block;line-height: 32px;font-size: 18px;color: #212121;font-weight: 700;text-align: center;margin-top: 10px;margin-bottom: 10px;">匿名化效果评估指标</div>
<p style="font-family: simsun;margin: 0px;font-size: 14px;line-height: 21px;white-space: pre-wrap;color: #212121;">{{ ' 依据GB/T 42460-2023《信息安全技术个人信息去标识化效果评估指南》附录D,对去标识化以后的数据集基于K匿名模型进行去标识化效果的评估。' }}</p>
<p style="font-family: simsun;margin: 0px;font-size: 14px;line-height: 21px;white-space: pre-wrap;color: #212121;">
{{ ` 总体方案为先计算数据集每行记录、整体数据集的重标识风险,进而计算环境重标识攻击概率,最后再结合环境重标识攻击概率计算整个数据集的重标识整体风险。去标识化效果评估结果为${analysisResultInfo.rating + '级'}。具体指标如下:` }}</p>
<div style="font-family: simsun;display: block;line-height: 21px;font-size: 14px;color: #212121;font-weight: 700;margin: 8px 0px;">匿名化结果分析</div>
<div class="kpi-content" v-show="Object.keys(analysisResultInfo).length > 0">
<div class="border-content">
<div class="text" style="color: #212121;">去标识化效果评估结果</div>
<span class="number score-color">{{ analysisResultInfo.rating + '级' }}</span>
</div>
<div class="border-content">
<span class="text" style="color: #212121;">重标识风险最大值</span>
<span class="number">{{ analysisResultInfo.reIdentifyRiskRb != null ?
(analysisResultInfo.reIdentifyRiskRb || 0) : '--'
}}</span>
</div>
<div class="border-content">
<span class="text" style="color: #212121;">重标识风险平均值</span>
<span class="number">{{ analysisResultInfo.reIdentifyRiskRc != null ?
(analysisResultInfo.reIdentifyRiskRc || 0) : '--' }}</span>
</div>
<div class="border-content">
<span class="text" style="color: #212121;">环境重标识攻击概率</span>
<span class="number">{{ analysisResultInfo.envReAttackPr != null ?
(analysisResultInfo.envReAttackPr || 0) : '--' }}</span>
</div>
<div class="border-content">
<span class="text" style="color: #212121;">等价类门限风险</span>
<span class="number">{{ analysisResultInfo.reIdentifyRiskRa != null ?
(analysisResultInfo.reIdentifyRiskRa || 0) : '--' }}</span>
</div>
<div class="border-content">
<span class="text" style="color: #212121;">重标识风险总体风险</span>
<span class="number">{{ analysisResultInfo.reIdentifyOverallRisk != null ?
(analysisResultInfo.reIdentifyOverallRisk || 0) : '--' }}</span>
</div>
<div class="border-content">
<span class="text" style="color: #212121;">重标识可接受风险阈值</span>
<span class="number">{{ oldAnonTaskValueInfo.anonPrivacyMode?.riskThreshold == null ? 0.05 :
(oldAnonTaskValueInfo.anonPrivacyMode?.riskThreshold || 0) }}</span>
</div>
</div>
<div style="font-family: simsun;display: block;line-height: 21px;font-size: 14px;color: #212121;font-weight: 700;margin: 8px 0px 4px;">重标识风险表</div>
<div style="font-family: simsun;display: block;line-height: 21px;font-size: 14px;color: #212121;margin-bottom: 4px;white-space: pre-wrap;">{{ ' 门限阈值的取值:完全公开共享数据发布,取值 1/20;受控公开共享数据发布,取值 1/5;领地公开共享数据发布,取值 1/3。' }}</div>
<table border="0" cellspacing="0"
style="width:100%;word-break: break-all;margin: 0 auto;text-align: center;border-collapse: collapse;color: #212121">
<thead>
<tr>
<th v-for="(item, index) in resultTableFields" :key="index" style="border: 0.5px solid #d9d9d9">
<span>{{ item.chName }}</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(recordItem, j) in fullResultData" :key="j">
<td v-for="(columnItem, i) in resultTableFields" :key="i"
:style="{border: '0.5px solid #d9d9d9', 'text-align': <any>(getTextAlign(columnItem) ?? 'left') }">
<span :style="{ 'word-break': 'break-all' }">
{{ columnItem.enName == 'index' ? (j + 1) : formatterPreviewDate(recordItem, columnItem) }}
</span>
</td>
</tr>
</tbody>
</table>
<div v-show="oldAnonTaskValueInfo.dataSharingTypeCode != '01'"
style="font-family: simsun;display: block;line-height: 21px;font-size: 14px;color: #212121;font-weight: 700;margin: 8px 0px 4px;">内部故意攻击概率</div>
<div v-show="oldAnonTaskValueInfo.dataSharingTypeCode != '01'"
style="font-family: simsun;display: block;line-height: 21px;font-size: 14px;color: #212121;margin-bottom: 4px;white-space: pre-wrap;">{{ ' 重标识数据的动机和能力为低,从重标识攻击可能性分析表可得出在内部攻击方面,重标识攻击概率的取值为0.05。'}}</div>
<table v-show="oldAnonTaskValueInfo.dataSharingTypeCode != '01'" border="0" cellspacing="0"
style="width:100%;word-break: break-all;margin: 0 auto;text-align: center;border-collapse: collapse;color: #212121;">
<thead>
<tr>
<th v-for="(item, index) in innerResultFields" :key="index" style="border: 0.5px solid #d9d9d9">
<span>{{ item.label }}</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(recordItem, j) in innerResultData" :key="j">
<template v-for="(columnItem, i) in innerResultFields" :key="i">
<td :rowspan="i == 0 ? 3 : 1" v-show="!(j % 3 != 0 && i == 0)"
:style="{ border: '0.5px solid #d9d9d9', 'text-align': <any>(columnItem.align ?? 'left') }">
<span :style="{ 'word-break': 'break-all' }">
{{ recordItem[columnItem.field] }}
</span>
</td>
</template>
</tr>
</tbody>
</table>
<div v-show="oldAnonTaskValueInfo.dataSharingTypeCode != '01'"
style="font-family: simsun;display: block;line-height: 21px;font-size: 14px;color: #212121;font-weight: 700;margin: 8px 0px;">数据集包含熟人概率</div>
<div v-show="oldAnonTaskValueInfo.dataSharingTypeCode != '01'"
style="font-family: simsun;text-align: center; display: block;line-height: 21px;font-size: 14px;color: #212121;font-weight: 700">pr=1-1-p^m</div>
<div v-show="oldAnonTaskValueInfo.dataSharingTypeCode != '01'"
style="font-family: simsun;display: block;line-height: 21px;font-size: 14px;color: #212121;white-space: pre-wrap;">
{{ ` 数据集包含熟人概率可通过以上公式计算,${new Date().getFullYear()}年我国最新的人口统计为${analysisResultInfo?.allPerson ||
0}亿人,其中该数据集的容量为${analysisResultInfo?.dataSetNum || 0}万,占总人口的比例为${analysisResultInfo.patientPopulationRate ||
0}%,m值取推荐值150,数据集包含熟人的概率为${analysisResultInfo.randomAcquaintancePr || 0}。` }}</div>
<div v-show="oldAnonTaskValueInfo.dataSharingTypeCode != '01'"
style="font-family: simsun;display: block;line-height: 21px;font-size: 14px;color: #212121;font-weight: 700;margin: 8px 0px;">数据泄露概率
</div>
<div v-show="oldAnonTaskValueInfo.dataSharingTypeCode != '01'"
style="font-family: simsun;display: block;line-height: 21px;font-size: 14px;color: #212121;white-space: pre-wrap;">{{
' 对于安全和隐私控制能力评估为低的情况,推荐将数据泄露概率设定为0.55。' }}</div>
<div v-show="oldAnonTaskValueInfo.dataSharingTypeCode != '01'"
style="font-family: simsun;display: block;line-height: 21px;font-size: 14px;color: #212121;white-space: pre-wrap;">{{ ' 对于安全和隐私控制能力评估为中的情况,推荐将数据泄露概率设定为0.27。' }}</div>
<div v-show="oldAnonTaskValueInfo.dataSharingTypeCode != '01'"
style="font-family: simsun;display: block;line-height: 21px;font-size: 14px;color: #212121;white-space: pre-wrap;">{{ ' 对于安全和隐私控制能力评估为高的情况,推荐将数据泄露概率设定为 0.14。' }}</div>
<div v-show="oldAnonTaskValueInfo.dataSharingTypeCode != '01'"
style="font-family: simsun;display: block;line-height: 21px;font-size: 14px;color: #212121;white-space: pre-wrap;">{{ ` 数据接收方的安全和隐私控制能力为高,按照推荐值将数据泄露概率设定为${analysisResultInfo.dataBreachPr || 0}。` }}</div>
<div style="font-family: simsun;display: block;line-height: 21px;font-size: 14px;color: #212121;font-weight: 700;margin: 8px 0px;">备注:</div>
<p style="font-family: simsun;margin: 0px;font-size: 14px;line-height: 21px;color: #212121;margin-top: 8px;white-space: pre-wrap;">{{ ' 重标识风险最大值:所有等价类的重标识风险最大值;' }}</p>
<p style="font-family: simsun;margin: 0px;font-size: 14px;line-height: 21px;color: #212121;white-space: pre-wrap;">{{ ' 重标识风险平均值:所有等价类的重标识风险平均值;'}}</p>
<p style="font-family: simsun;margin: 0px;font-size: 14px;line-height: 21px;color: #212121;white-space: pre-wrap;">{{ ' 环境重标识攻击概率:完全公开共享数据发布,攻击者对数据集进行环境重标识攻击的概率为1。领地公开共享与受控公开共享数据发布,环境重标识攻击概率为内部故意攻击概率、数据集包含熟人概率和数据泄露概率三者的最大值;' }}</p>
<p style="font-family: simsun;margin: 0px;font-size: 14px;line-height: 21px;color: #212121;white-space: pre-wrap;">{{ ' 等价类门限风险:完全公开共享数据发布,门限阈值取值1/20;受控公开共享数据发布,门限阈值取值 1/5;领地公开共享数据发布,门限阈值取值 1/3;等价类门限风险为:等价类的重标识风险大于门限阈值为1,小于等于为0,求和后除以等价类个数;'}}</p>
<p style="font-family: simsun;margin: 0px;font-size: 14px;line-height: 21px;color: #212121;white-space: pre-wrap;">{{ ' 重标识风险总体风险:完全公开共享,当等价类门限风险=0时,重标识风险总体风险公式为等价类重标识风险最大值*环境重标识攻击概率;当等价类门限风险!=0时,重标识风险总体风险为1。' }}</p>
<p style="font-family: simsun;margin: 0px;font-size: 14px;line-height: 21px;color: #212121;white-space: pre-wrap;">{{ ' 受控公开共享和领地公开共享,当等价类门限风险=0时,重标识风险总体风险公式为等价类重标识风险平均值*环境重标识攻击概率;当等价类门限风险!=0时,重标识风险总体风险为1。' }}</p>
<p v-show="analysisResultInfo.rating >= 3" style="font-family: simsun;margin: 0px;font-size: 14px;line-height: 21px;color: #212121;white-space: pre-wrap;">{{ ' 依据《数据安全技术个人信息匿名化处理指南及评价方法(征求意见稿)》4.2,去标识化的数据集达到3级可展开对数据级的对抗性测试和不可复原性核验。对抗性测试关键变量如下表:' }}</p>
<table border="0" cellspacing="0" v-show="analysisResultInfo.rating >= 3"
style="width:100%;word-break: break-all;margin: 4px auto 0px auto;text-align: center;border-collapse: collapse;color: #212121;">
<thead>
<tr>
<th v-for="(item, index) in adversarialTestFields" :key="index" style="border: 0.5px solid #d9d9d9">
<span>{{ item.label }}</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(recordItem, j) in analysisResultInfo?.adversarialTest || []" :key="j">
<td v-for="(columnItem, i) in adversarialTestFields" :key="i"
:style="{ border: '0.5px solid #d9d9d9', 'text-align': <any>(columnItem.align ?? 'left') }">
<span :style="{ 'word-break': 'break-all' }">
{{ columnItem.field == 'index' ? (j + 1) : recordItem[columnItem.field] === 0 ? 0 : (recordItem[columnItem.field] || '--') }}
</span>
</td>
</tr>
</tbody>
</table>
<p v-show="analysisResultInfo.rating >= 3" style="font-family: simsun;margin: 4px 0px 0px;font-size: 14px;line-height: 21px;color: #212121;white-space: pre-wrap;">{{ ' 通过识别关键变量,选择标识度高于0.5的属性作为攻击测试的关键变量,在攻击测试中,涉及到的攻击场景,其重标识概率均远小于所接受的风险水平0.05。另外,未发现任何其它可能的安全性问题。' }}</p>
</div>
</div>
</template>
<script lang="ts" setup name="anonResultAnalysis">
import { TableColumnWidth } from '@/utils/enum';
import { QuestionFilled } from "@element-plus/icons-vue";
import Moment from 'moment';
defineProps({
const props = defineProps({
isWordStyle: { //显示的是否是word报告的样式
type: Boolean,
default: false,
},
showTitle: {
type: Boolean,
default: false,
......@@ -213,7 +364,11 @@ defineProps({
type: Object,
default: {},
},
resultData: {
resultData: { //分页后的数据
type: Array,
default: []
},
fullResultData: { //全部数据,下载word时使用。
type: Array,
default: []
},
......@@ -231,11 +386,33 @@ defineProps({
},
})
const report = ref();
const resultTableFields = computed(() => {
let arr: any[] = [{ chName: '等价类', dataType: 'int', enName: 'index' }]
return arr.concat(props.analysisResultTableFields).concat([{
chName: '等价类大小',
enName: 'equivalenceClassNum',
dataType: 'int'
}, {
chName: '重标识风险',
enName: 'reIdentifyRisk',
dataType: 'decimal'
}, {
chName: '判断重风险是否大于门限阈值',
enName: 'isGtThreshold',
dataType: 'text'
}]);
})
const emits = defineEmits([
"pageChange"
]);
const getTextAlign = (field) => {
if (field.enName === 'index') {
return 'center';
}
if (field.dataType === 'decimal' || field.dataType === 'int') {
return 'right';
}
......@@ -265,6 +442,17 @@ const formatterPreviewDate = (row, info) => {
};
/** 内部故意攻击概率的表格 */
const innerResultFields = ref([{
field: 'level',
label: '风险减缓控制水平'
}, {
field: 'competencyLevel',
label: '动机和能力'
}, {
field: 'value',
label: '重标识攻击概率',
align: 'right'
}]);
const innerResultData = ref([{
guid: '1',
level: '高',
......@@ -350,15 +538,42 @@ const handleInnerCellClass = ({ row, column, rowIndex, columnIndex }) => {
return '';
}
/** 对抗性测试变量表格字段配置 */
const adversarialTestFields = ref([{
field: 'index',
label: '序号',
align: 'center',
}, {
field: 'chName',
label: '数据项'
}, {
field: 'uniqueScore',
label: '唯一性分值',
align: 'right'
}, {
field: 'influenceScore',
label: '影响力分值',
align: 'right'
}, {
field: 'dataAttrIdentScore',
label: '数据属性标识度分值',
align: 'right'
}]);
const pageChange = (info) => {
emits('pageChange', info);
}
defineExpose({
report,
})
</script>
<style lang="scss" scoped>
.analysis-result-main {
min-height: 250px;
width: 100%;
.value-desc {
font-size: 14px;
......@@ -480,4 +695,17 @@ const pageChange = (info) => {
color: #b2b2b2;
}
}
.report-main.analysis-result-main {
width: 625px;
.kpi-content {
margin-bottom: 0px;
.border-content {
width: calc(33% - 8px);
min-width: 168px;
}
}
}
</style>
\ No newline at end of file
......
{
"compilerOptions": {
"noUnusedLocals": true,
"noUnusedParameters": true,
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!