Skip to content
Toggle navigation
Toggle navigation
This project
Loading...
Sign in
csbr-daop
/
fe-data-trusted-space
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Builds
Commits
Issue Boards
Files
Commits
Network
Compare
Branches
Tags
361267e4
authored
2026-02-06 10:37:27 +0800
by
lihua
Browse Files
Options
Browse Files
Tag
Download
Email Patches
Plain Diff
修改下载报告
1 parent
1416702f
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
556 additions
and
43 deletions
src/api/modules/dataAnonymization.ts
src/views/data_anonymization/anonResultReportView.vue
src/views/data_anonymization/anonTaskCreate.vue
src/views/data_anonymization/components/anonResultAnalysis.vue
tsconfig.json
src/api/modules/dataAnonymization.ts
View file @
361267e
...
...
@@ -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
...
...
src/views/data_anonymization/anonResultReportView.vue
View file @
361267e
...
...
@@ -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
>
...
...
src/views/data_anonymization/anonTaskCreate.vue
View file @
361267e
...
...
@@ -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
{
QuestionFille
d
}
from
"@element-plus/icons-vue"
;
import
{
Uploa
d
}
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
...
...
src/views/data_anonymization/components/anonResultAnalysis.vue
View file @
361267e
<
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="
68
px
" 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="
56
px
" align="
center
" show-overflow-tooltip>
</el-table-column>
<el-table-column label="
数据项
" prop="
chName
" width="
150
px
" align="
left
" show-overflow-tooltip></el-table-column>
<el-table-column label="
数据项
" prop="
chName
" width="
150
px
" align="
left
"
show-overflow-tooltip></el-table-column>
<el-table-column label="
唯一性分值
" prop="
uniqueScore
" width="
110
px
" 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
:
250
px
;
width
:
100
%
;
.
value
-
desc
{
font
-
size
:
14
px
;
...
...
@@ -480,4 +695,17 @@ const pageChange = (info) => {
color
:
#
b2b2b2
;
}
}
.
report
-
main
.
analysis
-
result
-
main
{
width
:
625
px
;
.
kpi
-
content
{
margin
-
bottom
:
0
px
;
.
border
-
content
{
width
:
calc
(
33
%
-
8
px
);
min
-
width
:
168
px
;
}
}
}
<
/style>
\ No newline at end of file
...
...
tsconfig.json
View file @
361267e
{
"compilerOptions"
:
{
"noUnusedLocals"
:
true
,
"noUnusedParameters"
:
true
,
"target"
:
"ESNext"
,
"useDefineForClassFields"
:
true
,
"module"
:
"ESNext"
,
...
...
Write
Preview
Styling with
Markdown
is supported
Attach a file
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to post a comment