6034856e by lihua

迁移数据匿名化代码

1 parent 929999e9
......@@ -3,6 +3,51 @@
*/
import request from "@/utils/request";
/** 获取标签列表。 */
export const getDataLabelList = (params) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/label/page-list`,
method: 'post',
data: params
})
/** 修改标签启用禁用状态 */
export const updateLabelState = (params) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/label/update-state`,
method: 'put',
params
})
export const saveLabel = (data) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/label/save`,
method: 'post',
data
})
export const deleteLabel = (data) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/label/delete`,
method: 'delete',
data
})
export const updateLabel = (data) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/label/update`,
method: 'put',
data
})
/** 获取标签详情 */
export const getLabelDetail = (guid) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/label/detail?guid=${guid}`,
method: 'get'
})
/** 获取数据字典配置 */
export const getParamsList = (params) => request({
url: `${import.meta.env.VITE_APP_CONFIG_URL}/dict/data/get-by-dictType`,
method: 'get',
params
})
/** 字段类型 */
export const fieldTypeList = [{
value: '1',
......@@ -59,3 +104,240 @@ export const parseGeneralizeFileData = (data) => request({
method: 'post',
data
})
/** --------- 敏感数据识别接口 ------------------- */
/** 获取敏感数据识别任务列表 */
export const getSensitiveDataTaskList = (params) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/page-list`,
method: 'post',
data: params
})
/** 新增敏感数据识别任务 */
export const saveSensitiveDataTask = (params) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/save`,
method: 'post',
data: params
})
/** 编辑修改敏感数据识别任务 */
export const updateSensitiveDataTask = (params) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/update`,
method: 'put',
data: params
})
/** 删除敏感数据识别任务 */
export const deleteSensitiveDataTask = (data) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/delete`,
method: 'delete',
data
})
/** 手动执行敏感任务 */
export const execSensitiveDataTask = (guid) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/exec-task?taskGuid=${guid}`,
method: 'get'
})
/** 数据来源类型 */
export const dataSourceTypeList = [{
value: 1,
label: '数据库'
}, {
value: 2,
label: '文件导入'
}];
/** 获取数据库选择列表 */
export const getDatabase = (params) => request({
url: `${import.meta.env.VITE_APP_DATA_SOURCE_URL}/data-source/get-source-list`,
method: 'post',
data: params
})
/** 获取敏感数据任务执行的数据表列表 */
export const getExecSensitiveTable = (execGuid) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/get-exec-sensitive-table?execGuid=${execGuid}`,
method: 'get'
})
/** 根据数据源或表获取敏感数据任务执行的字段列表 */
export const getExecSensitiveFieldTable = (params) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/get-exec-sensitive-field`,
method: 'post',
data: params
})
/** 获取当前数据表下的执行字段 */
export const getExecSensitiveFieldColumnListByCondition = (params) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/column-list-by-condition`,
method: 'post',
data: params
})
/** 获取敏感数据识别任务执行后的统计结果 */
export const getStatisticsNum = (params) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/statistics-num`,
method: 'get',
params
})
/** 修改敏感数据识别字段标签 */
export const updateSensitiveDataTaskFieldLabel = (params) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/batch-update-label`,
method: 'put',
data: params
})
/** 批量修改确认状态 */
export const batchUpdateSensitiveDataTaskFieldStatus = (params) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/batch-change-status`,
method: 'post',
data: params
})
/** 修改任务状态 */
export const confirmTaskStatus = (guid) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/confirm-task?execGuid=${guid}`,
method: 'get'
})
/** 获取敏感数据识别任务执行日志 */
export const getSensitiveDataTaskExecLog = (params) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/get-exec-sensitive-exec-log`,
method: 'post',
data: params
})
/** ---------- 匿名化处理 ------------------ */
/** 获取匿名化任务列表 */
export const getAnonTaskList = (params) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/page-list`,
method: 'post',
data: params
})
/** 删除匿名化任务 */
export const deleteAnonTask = (data) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/delete`,
method: 'delete',
data
})
/** 保存匿名化任务 */
export const saveAnonTask = (params) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/save`,
method: 'post',
data: params
})
/** 更新匿名化任务 */
export const updateAnonTask = (params) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/update`,
method: 'put',
data: params
})
/** 获取匿名化任务详情 */
export const getAnonTaskDetail = (guid) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/detail?guid=${guid}`,
method: 'get'
})
/** 执行匿名化任务 */
export const execAnonTask = (taskGuid) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/exec-task?taskGuid=${taskGuid}`,
method: 'post'
})
/** 匿名化任务检验接口 */
export const anonTaskCheck = (params) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/check`,
method: 'post',
data: params
})
/** 获取匿名化任务分析结果数据 */
export const getAnonAnalyzeResult = (execGuid) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/get-anon-analyze?taskExecGuid=${execGuid}`,
method: 'get'
})
/** 获取匿名化任务分析结果数据 */
export const getLastAnonAnalyzeResult = (execGuid) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/get-anon-analyze?isResult=true&taskExecGuid=${execGuid}`,
method: 'get'
})
/** 获取匿名化任务分析结果统计分页数据 */
export const getAnonAnalyzePageData = (params) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/page-anon-analyze-data`,
method: 'post',
data: params
})
/** 获取匿名化任务结果数据 */
export const getAnonPageData = (params) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/page-anon-data`,
method: 'post',
data: params
})
/** 字段中文转英文 */
export const chTransformEn =(params)=> request({
url: `${import.meta.env.VITE_APP_COMMON_URL}/common/convert-field-ch-name`,
method: "post",
data: params,
});
/** 根据选择的连接池获取表列表 */
export const getDsTableByDs = (params) => request({
url: `${import.meta.env.VITE_APP_DATA_SOURCE_URL}/data-source/schema-table-page-list`,
method: 'post',
data: params
})
/** 根据数据表获取表字段结构 */
export const getDsTableFieldColumn = (params) => request({
url: `${import.meta.env.VITE_APP_DATA_SOURCE_URL}/data-source/table-column-list`,
method: 'post',
data: params
});
/** 根据数据表获取表数据 */
export const getDsTableSampleData = (params) => request({
url: `${import.meta.env.VITE_APP_DATA_SOURCE_URL}/data-source/table-data-preview-page`,
method: 'post',
data: params
});
/** 根据字段名称获取敏感数据识别标签 */
export const getLableByFieldName = (fieldName) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/get-label-by-field-name?fieldName=${fieldName}`,
method: 'get'
});
/** 验证样本数据 */
export const validateAnonRule = (params) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/check`,
method: 'post',
data: params
})
/** 导出匿名化结果数据 */
export const exportAnonExecData = (params) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/export-anon-data?taskGuid=${params.taskGuid}&taskExecGuid=${params.execGuid}`,
method: 'get',
responseType: 'blob'
})
/** 下载匿名化评估报告 */
export const exportAnonReport = (params) => request({
url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/download-report?taskGuid=${params.taskGuid}&taskExecGuid=${params.execGuid}`,
method: 'post',
responseType: 'blob'
})
\ No newline at end of file
......
import type { RouteRecordRaw } from 'vue-router'
function Layout() {
return import('@/layouts/index.vue')
}
const routes: RouteRecordRaw[] = [
{
path: '/data-anonymization/label-management',
component: Layout,
meta: {
title: '标签管理',
icon: 'sidebar-videos',
},
children: [
{
path: '',
name: 'labelManagement',
component: () => import('@/views/data_anonymization/labelManagement.vue'),
meta: {
title: '标签管理',
sidebar: false,
breadcrumb: false,
cache: true
},
}
],
},
{
path: '/data-anonymization/generalize-file',
component: Layout,
meta: {
title: '泛化文件管理',
icon: 'sidebar-videos',
},
children: [
{
path: '',
name: 'generalizeFile',
component: () => import('@/views/data_anonymization/generalizeFile.vue'),
meta: {
title: '泛化文件管理',
sidebar: false,
breadcrumb: false,
cache: true
},
},
{
path: 'generalize-file-edit',
name: 'generalizeFileEdit',
component: () => import('@/views/data_anonymization/generalizeFileEdit.vue'),
meta: {
title: '新建泛化文件',
sidebar: false,
breadcrumb: false,
cache: true,
reuse: true,
editPage: true,
activeMenu: '/data-anonymization/generalize-file'
},
beforeEnter: (to, from) => {
if (to.query.fileName) {
to.meta.title = `编辑-${to.query.fileName}`;
}
}
},
],
},
{
path: '/data-anonymization/sensitive-identify',
component: Layout,
meta: {
title: '敏感数据识别',
icon: 'sidebar-videos',
},
children: [
{
path: '',
name: 'sensitiveIdentify',
component: () => import('@/views/data_anonymization/sensitiveIdentify.vue'),
meta: {
title: '敏感数据识别',
sidebar: false,
breadcrumb: false,
cache: true
},
},
{
path: 'sensitive-identify-config',
name: 'sensitiveIdentifyConfig',
component: () => import('@/views/data_anonymization/sensitiveIdentifyConfig.vue'),
meta: {
title: '敏感数据识别查看',
sidebar: false,
breadcrumb: false,
cache: true,
reuse: true,
editPage: false,
activeMenu: '/data-anonymization/sensitive-identify'
},
beforeEnter: (to, from) => {
if (to.query.taskName) {
to.meta.title = `敏感数据${to.query.isLook == '1' ? '日志查看' : '查看'}-${to.query.taskName}`;
}
}
},
{
path: 'sensitive-identify-task-exec-log',
name: 'sensitiveIdentifyTaskExecLog',
component: () => import('@/views/data_anonymization/sensitiveIdentifyTaskExecLog.vue'),
meta: {
title: '执行日志',
sidebar: false,
breadcrumb: false,
cache: true,
reuse: true
},
beforeEnter: (to, from) => {
if (to.query.guid) {
to.meta.title = `日志-${to.query.name}`;
}
}
}
],
},
{
path: '/data-anonymization/result-process',
component: Layout,
meta: {
title: '匿名化处理',
icon: 'sidebar-videos',
},
children: [
{
path: '',
name: 'resultProcess',
component: () => import('@/views/data_anonymization/resultProcess.vue'),
meta: {
title: '匿名化处理',
sidebar: false,
breadcrumb: false,
cache: true
},
},
{
path: 'anon-task-create',
name: 'anonTaskCreate',
component: () => import('@/views/data_anonymization/anonTaskCreate.vue'),
meta: {
title: '匿名化处理任务',
sidebar: false,
breadcrumb: false,
cache: true,
reuse: true,
editPage: true,
activeMenu: '/data-anonymization/result-process'
},
beforeEnter: (to, from) => {
if (to.query.taskName) {
to.meta.title = `编辑-${to.query.taskName}`;
}
}
},
{
path: 'anonResultView',
name: 'anonResultView',
component: () => import('@/views/data_anonymization/anonResultView.vue'),
meta: {
title: '查看数据',
sidebar: false,
breadcrumb: false,
cache: true,
reuse: true
},
beforeEnter: (to, from) => {
if (to.query.guid) {
to.meta.title = `查看数据-${to.query.taskName}`;
}
}
},
{
path: 'anonResultReportView',
name: 'anonResultReportView',
component: () => import('@/views/data_anonymization/anonResultReportView.vue'),
meta: {
title: '查看报告',
sidebar: false,
breadcrumb: false,
cache: true,
reuse: true
},
beforeEnter: (to, from) => {
if (to.query.guid) {
to.meta.title = `查看报告-${to.query.taskName}`;
}
}
},
],
},
]
export default routes
......@@ -216,47 +216,7 @@ const routes: RouteRecordRaw[] = [
editPage: true
},
}]
},
{
path: '/data-smart-contract-common/generalize-file',
component: Layout,
meta: {
title: '泛化文件管理',
icon: 'sidebar-videos',
},
children: [
{
path: '',
name: 'generalizeFile',
component: () => import('@/views/data_smart_contract/generalizeFile.vue'),
meta: {
title: '',
sidebar: false,
breadcrumb: false,
cache: true
},
},
{
path: 'generalize-file-edit',
name: 'generalizeFileEdit',
component: () => import('@/views/data_smart_contract/generalizeFileEdit.vue'),
meta: {
title: '新建泛化文件',
sidebar: false,
breadcrumb: false,
cache: true,
reuse: true,
editPage: true,
activeMenu: '/data-smart-contract-common/generalize-file'
},
beforeEnter: (to, from) => {
if (to.query.fileName) {
to.meta.title = `编辑-${to.query.fileName}`;
}
}
},
],
},
]
export default routes
\ No newline at end of file
......
......@@ -7,6 +7,7 @@ import DataSmartContract from './modules/dataSmartContract';
import DataFacilitator from './modules/dataFacilitator';
import HomeIndex from './modules/homeIndex';
import DataDelivery from './modules/dataDelivery';
import DataAnonymization from './modules/dataAnonymization';
import useSettingsStore from '@/store/modules/settings'
......@@ -99,6 +100,7 @@ const asyncRoutes: RouteRecordRaw[] = [
...DataFacilitator,
...HomeIndex,
...DataDelivery,
...DataAnonymization,
// ...DataAssetRegistry,
]
......
<route lang="yaml">
name: anonResultReportView
</route>
<script lang="ts" setup name="anonResultReportView">
import {
exportAnonReport,
getAnonAnalyzePageData,
getAnonAnalyzeResult,
getAnonTaskDetail,
} 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';
const route = useRoute();
const router = useRouter();
const fullPath = route.fullPath;
const taskGuid = ref(route.query.guid);
const { proxy } = getCurrentInstance() as any;
const resultDataLoading = ref(false);
const downPromise: any = ref()
/** 提交保存和编辑后的执行guid */
const taskExecGuid = ref(route.query.execGuid);
/** 记录原始的值信息,防止上一步之后未修改数据时不调用接口 */
const oldAnonTaskValueInfo: any = ref({});
/** 执行结果信息 */
const analysisResultInfo: any = ref({});
const containerRef = ref();
const containerWidth = ref(containerRef.value?.offsetWidth || 0)
/** ------------------------- 匿名化分析结果页面数据展示 ---------------- */
const pageInfo: any = ref({
...commonPageConfig,
})
const pageChange = (info) => {
pageInfo.value.curr = Number(info.curr);
pageInfo.value.limit = Number(info.limit);
getAnalysisResultPageData();
}
/** 每列字段对应的列宽计算结果。 */
const originResultTableFieldColumn = ref({});
/** 结果分析中的字段表格数据 */
const resultData: any = ref([]);
/** 结果分析中的字段信息 */
const analysisResultTableFields: any = ref([]);
const analysisResultLoading = ref(false);
/** otherWidth表示使用标题宽度时添加标题排序图标等宽度 */
const calcTableColumnWidth = (data: any[], prop, title, otherWidth = 0) => {
let d: any[] = [];
data.forEach((dt) => d.push(dt[prop]));
//样式使用默认值。
return calcColumnWidth(
d,
title,
{
fontSize: 14,
fontFamily: "SimSun",
},
{
fontSize: 14,
fontFamily: "SimSun",
},
otherWidth
);
};
watch(
resultData,
(val: any[], oldVal) => {
if (!analysisResultTableFields.value?.length) {
originResultTableFieldColumn.value = {};
return;
}
originResultTableFieldColumn.value = {};
analysisResultTableFields.value.forEach((field, index) => {
originResultTableFieldColumn.value[field.enName] = calcTableColumnWidth(
val?.slice(0, 20) || [],
field.enName,
field.chName,
24
);
});
},
{
deep: true,
}
);
const getAnalysisResultPageData = () => {
analysisResultLoading.value = true;
getAnonAnalyzePageData({
pageIndex: pageInfo.value.curr,
pageSize: pageInfo.value.limit,
taskExecGuid: taskExecGuid.value,
}).then((res: any) => {
analysisResultLoading.value = false;
if (res?.code == proxy.$passCode) {
pageInfo.value.rows =
resultData.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;
resultData.value.push(obj);
});
pageInfo.value.rows = res.data?.totalRows ?? 0;
} else {
proxy.$ElMessage.error(res.msg);
}
})
}
/** 下载评估报告 */
const transfer = () => {
if (downPromise.value) {
return;
}
downPromise.value = exportAnonReport({
taskGuid: route.query.guid,
execGuid: taskExecGuid.value
}).then((res: any) => {
downPromise.value = null;
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(() => {
nextTick(() => {
containerWidth.value = containerRef.value?.offsetWidth || 0;
})
window.onresize = () => {
containerWidth.value = containerRef.value?.offsetWidth || 0;
}
})
onBeforeMount(() => {
resultDataLoading.value = true;
getAnonAnalyzeResult(taskExecGuid.value).then((res: any) => {
resultDataLoading.value = false;
if (res?.code == proxy.$passCode) {
analysisResultInfo.value = res.data || {};
analysisResultTableFields.value = res.data?.column || [];
pageInfo.value.curr = 1;
getAnalysisResultPageData();
} else {
res?.msg && proxy.$ElMessage.error(res.msg);
}
});
getAnonTaskDetail(taskGuid.value).then((res: any) => {
if (res?.code == proxy.$passCode) {
oldAnonTaskValueInfo.value = res.data || {};
} else {
res?.msg && proxy.$ElMessage.error(res.msg);
}
});
})
</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>
</template>
<style lang="scss" scoped>
.table_tool_wrap {
width: 100%;
height: 100%;
padding: 8px 16px 16px;
overflow-y: auto;
}
</style>
<route lang="yaml">
name: anonResultView
</route>
<script lang="ts" setup name="anonResultView">
import { ref } from "vue";
import {
getAnonPageData,
getLastAnonAnalyzeResult,
exportAnonExecData,
} from "@/api/modules/dataAnonymization";
import { calcColumnWidth } from "@/utils/index";
import Moment from 'moment';
import { TableColumnWidth } from "@/utils/enum";
import { ElMessage } from "element-plus";
import { commonPageConfig } from '@/components/PageNav/index';
import { download } from "@/utils/common";
const { proxy } = getCurrentInstance() as any;
const route = useRoute();
const props = defineProps({
isPage: {
default: true,
type: Boolean
},
execGuid: {
default: '',
type: String
}
});
const tableData: any = ref([]);
const tableDataLoading = ref(false);
const tableFields: any = ref([]);
const pageInfo: any = ref({
...commonPageConfig,
rows: 0,
})
const getData = () => {
tableData.value = [];
if (!tableFields.value?.length) {
return;
}
tableDataLoading.value = true;
getAnonPageData({
pageIndex: pageInfo.value.curr,
pageSize: pageInfo.value.limit,
taskExecGuid: props.isPage ? route.query.execGuid : props.execGuid,
}).then((res: any) => {
tableDataLoading.value = false;
if (res.code == proxy.$passCode) {
tableData.value = [];
res.data?.records?.forEach(d => {
let obj = {};
tableFields.value.forEach(t => {
obj[t.enName] = d.fieldValue?.[t.enName];
});
tableData.value.push(obj);
});
pageInfo.value.rows = res.data?.totalRows ?? 0;
} else {
ElMessage.error(res.msg);
}
});
}
const getTextAlign = (field) => {
if (field.dataType === 'decimal' || field.dataType === 'int' || field.dataType == 'bit' || field.dataType == 'tinyint') {
return 'right';
}
return 'left'
}
/** otherWidth表示使用标题宽度时添加标题排序图标等宽度 */
const calcTableColumnWidth = (data: any[], prop, title, otherWidth = 0) => {
let d: any[] = [];
data.forEach((dt) => d.push(dt[prop]));
return calcColumnWidth(
d,
title,
{
fontSize: 14,
fontFamily: "SimSun",
},
{
fontSize: 14,
fontFamily: "SimSun",
},
otherWidth
);
};
/** 每列字段对应的列宽计算结果。 */
const originTableFieldColumn = ref({});
watch(
tableData,
(val: any[], oldVal) => {
if (!tableFields.value?.length) {
originTableFieldColumn.value = {};
return;
}
originTableFieldColumn.value = {};
tableFields.value.forEach((field, index) => {
originTableFieldColumn.value[field.enName] = calcTableColumnWidth(
val?.slice(0, 20) || [],
field.enName,
field.chName,
24
);
});
},
{
deep: true,
}
);
watch(() => props.execGuid, (val) => {
if (!val) {
return;
}
tableDataLoading.value = true;
getLastAnonAnalyzeResult(val).then((res: any) => {
tableDataLoading.value = false;
if (res.code == proxy.$passCode) {
let column = res.data?.column || {};
tableFields.value = column;
pageInfo.value.curr = 1;
getData();
} else {
ElMessage.error(res.msg);
}
});
}, {
immediate: true
})
onBeforeMount(() => {
if (!props.isPage) {
return;
}
tableDataLoading.value = true;
getLastAnonAnalyzeResult(route.query.execGuid).then((res: any) => {
tableDataLoading.value = false;
if (res.code == proxy.$passCode) {
let column = res.data?.column || {};
tableFields.value = column;
getData();
} else {
ElMessage.error(res.msg);
}
});
});
const formatterPreviewDate = (row, info) => {
let enName = info.enName;
let v = row[enName];
if (v === 0) {
return v;
}
if (!v || v == 'null') {
return '--';
}
if (info.dataType === 'datetime') {
return Moment(v).format('YYYY-MM-DD HH:mm:ss');
}
if (info.dataType === 'date') {
if (isNaN(<any>(new Date(v)))) {
return Moment(parseInt(v)).format('YYYY-MM-DD');
} else {
return Moment(v).format('YYYY-MM-DD');
}
}
return v;
};
const pageChange = (info) => {
pageInfo.value.curr = Number(info.curr);
pageInfo.value.limit = Number(info.limit);
getData();
}
const promise: any = ref(null);
const exportData = () => {
if (promise.value) {
return;
}
promise.value = exportAnonExecData({
taskGuid: route.query.guid,
execGuid: route.query.execGuid
}).then((res: any) => {
promise.value = null;
if (res && !res.msg) {
download(res, route.query.taskName + '_匿名化数据.xlsx', 'excel')
} else {
res?.msg && ElMessage.error(res?.msg);
}
}).catch(() => {
promise.value = null;
})
}
</script>
<template>
<div class="table_tool_wrap" v-loading="tableDataLoading">
<el-button v-show="props.isPage" style="margin-bottom: 8px;" type="primary" @click="exportData"
v-preReClick>导出数据</el-button>
<el-table ref="tableRef" v-show="tableFields.length" :data="tableData" :highlight-current-row="true" stripe border
tooltip-effect="light" height="100%" row-key="guid" :style="{ width: '100%', height: !props.isPage ? 'calc(100% - 34px)' : 'calc(100% - 64px)' }">
<template v-for="(item, index) in (tableFields || [])">
<el-table-column :label="item.chName" :width="item.dataType === 'datetime'
? TableColumnWidth.DATETIME
: item.dataType === 'date'
? TableColumnWidth.DATE
: originTableFieldColumn[item.enName]
" :align="getTextAlign(item)" :header-align="getTextAlign(item)"
:formatter="(row) => formatterPreviewDate(row, item)" :show-overflow-tooltip="true">
</el-table-column>
</template>
</el-table>
<div v-show="!tableFields.length" class="empty-content">
<img src="../../assets/images/empty-data.png" :style="{ width: '168px', height: '96px' }" />
<div class="empty-text">暂无数据</div>
</div>
<PageNav :class="[pageInfo.type]" :pageInfo="pageInfo" @pageChange="pageChange" />
</div>
</template>
<style lang="scss" scoped>
.table_tool_wrap {
width: 100%;
height: 100%;
padding: 8px 16px 16px;
.tips_text {
font-size: 14px;
color: var(--el-text-color-tip);
display: block;
font-weight: normal;
margin-bottom: 8px;
line-height: 21px;
}
.el-table {
display: inline-block;
}
.empty-content {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
flex-direction: column;
.empty-text {
font-size: 14px;
color: #b2b2b2;
}
}
}
</style>
\ No newline at end of file
<route lang="yaml">
name: anonTaskCreate
</route>
<template>
<div class="container_wrap full" v-loading="fullscreenLoading" ref="containerRef">
<div class="content_main">
<!-- 顶部步骤条 -->
<div class="top_tool_wrap">
<StepBar :steps-info="stepsInfo" :style="{ width: stepsInfo.list.length == 2 ? '30%' : '60%' }" />
</div>
<!-- 第一步 数据输入 -->
<div class="operator_panel_wrap" v-show="step == 0">
<ContentWrap id="id-baseInfo" title="数据选择" description="" style="margin-top: 8px;">
<!-- 数据选择相关属性表单设置 -->
<Form ref="formRef" :itemList="dataSelectInfoItems" :rules="dataSelectInfoFormRules"
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;">
<!-- 选择抽样预览的表单设置 -->
<Form ref="dataSimpleFormRef" :itemList="dataSimpleFormItems" :rules="dataSimpleFormRules"
formId="data-simple-edit" col="col3 fixwidth-form" @switch-change="handleDataSimpleFormSwitchChange"
@input-change="handleDataSimpleFormChange" />
<!-- 抽样预览的数据表格设置 -->
<div class="table-v2-main" v-show="dataSimpleFormRef?.formInline?.enableSamplingRate == 'Y'"
v-loading="sampleTableDataLoading">
<el-table ref="tableRef" v-show="sampleTableFields.length" :data="sampleTableData"
:highlight-current-row="true" stripe border tooltip-effect="light" height="100%" row-key="guid"
:style="{ width: '100%', height: '240px' }">
<el-table-column label="序号" type="index" width="56px" align="center"
show-overflow-tooltip></el-table-column>
<template v-for="(item, index) in (sampleTableFields || [])">
<el-table-column :label="item.chName" :width="item.dataType === 'datetime'
? TableColumnWidth.DATETIME
: item.dataType === 'date'
? TableColumnWidth.DATE
: originTableFieldColumn[item.enName]
" :align="getTextAlign(item)" :header-align="getTextAlign(item)"
:formatter="(row) => formatterPreviewDate(row, item)" :show-overflow-tooltip="true">
</el-table-column>
</template>
</el-table>
<div v-show="!sampleTableFields.length" class="main-placeholder">
<img src="../../assets/images/no-data.png" :style="{ width: '96px', height: '96px' }" />
<div class="empty-text">暂无抽样数据</div>
</div>
</div>
</ContentWrap>
</div>
<!-- 第二步 配置匿名化方案,单独抽取vue组件页面 -->
<anonTaskStepTwo ref="anonTaskStepTwoRef" v-show="step == 1" :anonTaskRules="detailInfo.anonTaskRules"
:isFile="formRef?.formInline?.file?.length > 0" :anonPrivacyMode="detailInfo.anonPrivacyMode"
:fieldTypeList="fieldTypeList" :fieldNameList="sampleTableFields">
</anonTaskStepTwo>
<!-- 第三步 结果分析 -->
<div class="operator_panel_wrap" v-show="step == 2">
<ContentWrap class="anlysis-content-wrap" id="analysis-result" title="匿名结果分析" description=""
style="margin-top: 8px;">
<div class="wait-result-div" v-show="!isExecEnd">
<img class="loading-img" src="../../assets/images/loading.gif" />
<div class="desc">正在进行匿名化处理,请稍候...</div>
<el-button :icon="RefreshRight" link @click="refreshQueryData" v-preReClick>刷新查看结果</el-button>
</div>
<div class="wait-result-div" v-show="isExecEnd && analysisResultInfo.status == 'E'">
<el-icon class="failed">
<CircleCloseFilled />
</el-icon>
<div class="error-desc">{{ '执行失败,请返回上一步修改配置或联系管理员' }}</div>
<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>
<template #header>
<el-button v-show="isExecEnd && analysisResultInfo.status == 'Y'" type="primary" v-loading="!!downPromise"
@click="transfer">下载评估报告</el-button>
</template>
</ContentWrap>
</div>
<!-- 匿名化结果展示 -->
<div class="operator_panel_wrap step-result" v-show="step == 3" style="height: calc(100% - 88px);">
<ContentWrap id="analysis-result" title="匿名化数据结果" description="" style="margin-top: 8px;height: 100%;">
<!-- 匿名化结果数据查看页面,单独抽取业务组件,新开页面要使用 -->
<anonResultView :is-page="false"
:execGuid="analysisResultInfo.status == 'Y' && step == 3 ? taskExecGuid : ''">
</anonResultView>
</ContentWrap>
</div>
</div>
<!-- 底部按钮,需要根据当前步骤条来展示对应的按钮 -->
<div class="bottom_tool_wrap">
<template v-if="step == 0">
<el-button @click="cancelTask">取消</el-button>
<el-button type="primary" @click="changeStep(formRef?.formInline?.handleType == '02' ? 3 : 2)">下一步</el-button>
</template>
<template v-else-if="step == 1">
<el-button @click="changeStep(1)">上一步</el-button>
<el-button type="primary" @click="changeStep(3)">下一步</el-button>
</template>
<template v-else-if="step == 2">
<el-button @click="changeStep(formRef?.formInline?.handleType == '02' ? 1 : 2)">上一步</el-button>
<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>
</template>
<template v-else>
<el-button @click="changeStep(3)">上一步</el-button>
<el-button type="primary" v-preReClick @click="exportResult">导出</el-button>
</template>
</div>
</div>
</template>
<script lang="ts" setup name="anonTaskCreate">
import {
dataSourceTypeList,
getAnonTaskDetail,
getParamsList,
chTransformEn,
getAnonAnalyzeResult,
getAnonAnalyzePageData,
getDatabase,
getDsTableByDs,
getDsTableFieldColumn,
getDsTableSampleData,
saveAnonTask,
updateAnonTask,
exportAnonExecData,
exportAnonReport,
} from '@/api/modules/dataAnonymization';
import {
parseAndDecodeUrl,
getDownFileSignByUrl,
obsDownloadRequest
} from "@/api/modules/obsService";
import {
getAreaData
} from "@/api/modules/queryService";
import useUserStore from "@/store/modules/user";
import { useValidator } from '@/hooks/useValidator';
import { TableColumnWidth } from '@/utils/enum';
import { calcColumnWidth } from "@/utils/index";
import Moment from 'moment';
import anonTaskStepTwo from './anonTaskStepTwo.vue';
import * as XLSX from 'xlsx';
import { ElMessage } from 'element-plus';
import { isEqual, cloneDeep } from "lodash-es";
import { changeNum, download } from "@/utils/common";
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 anonResultAnalysis from './components/anonResultAnalysis.vue';
const anonymizationStore = useDataAnonymizationStore();
const { proxy } = getCurrentInstance() as any;
const userStore = useUserStore();
const route = useRoute();
const router = useRouter();
const fullPath = route.fullPath;
const taskGuid = ref(route.query.guid);
/** 提交保存和编辑后的执行guid */
const taskExecGuid = ref('');
/** 是否执行结束,用于第四步获取执行结果 */
const isExecEnd = ref(false);
const { required } = useValidator();
const fullscreenLoading = ref(false);
const containerRef = ref();
const containerWidth = ref(containerRef.value?.offsetWidth || 0)
const step = ref(0);
const originStepsInfo = ref({
step: step.value,
list: [
{ title: '数据输入', value: 1 },
{ title: '配置匿名化方案', value: 2 },
{ title: '匿名结果分析', value: 3 },
{ title: '结果输出', value: 4 }
]
})
const stepsInfo = ref(originStepsInfo.value);
/** 已匿名化出具报告的 */
const reportStepsInfo = ref({
step: step.value,
list: [
{ title: '数据输入', value: 1 },
// { title: '配置匿名化方案', value: 2 },
{ title: '匿名结果分析', value: 2 },
// { title: '结果输出', value: 4 }
]
})
/** 数据源列表 */
const dataSourceList: any = ref([]);
/** 数据源对应的数据表 */
const dsTableList: any = ref([]);
/** 数据共享类型字段列表 */
const dataSharingTypeList = ref([]);
/** 匿名化处理类型 */
const handleTypeList = ref([]);
const getParentAreaPromise: any = ref(null);
const getAreaDataPromise: any = ref({});
const getAreaDatas: any = ref({});
const parentAreaData: any = ref([]);
const getArea = (node, resolve) => {
const { level } = node
let params = {
parentGuid: node.value
}
if (!node.value) {
if (getParentAreaPromise.value) {
getParentAreaPromise.value.then((res: any) => {
resolve(res);
})
} else {
resolve(parentAreaData.value);
}
return;
}
if (node.loaded) {
resolve([]);
return;
}
if (getAreaDatas.value[node.value]?.length) {
resolve(getAreaDatas.value[node.value]);
return;
}
if (!getAreaDataPromise.value[node.value]) {
getAreaDataPromise.value[node.value] = getAreaData(params).then((res: any) => {
node.loaded = true;
getAreaDataPromise.value[node.value] = null;
if (res?.code == proxy.$passCode) {
const data = res.data ?? []
data.map(item => {
item.leaf = level >= 1
})
resolve(data)
getAreaDatas.value[node.value] = data;
return data;
}
})
} else {
getAreaDataPromise.value[node.value].then((data) => {
getAreaDataPromise.value[node.value] = null;
node.loaded = true;
data.map(item => {
item.leaf = level >= 1
})
resolve(data)
})
}
}
const formRef = ref();
/** 数据选择的表单配置信息 */
const dataSelectInfoItems = ref([{
label: '数据集名称',
type: 'input',
placeholder: '请输入',
field: 'taskName',
maxlength: 15,
default: '',
required: true,
filterable: true,
clearable: true,
visible: true,
}, {
label: '数据共享类型',
type: 'select',
placeholder: '请选择',
field: 'dataSharingTypeCode',
default: '01',
options: dataSharingTypeList.value,
props: {
label: "label",
value: "value",
},
required: true,
filterable: true,
clearable: true,
visible: true,
},
{
label: '所属地域',
type: "cascader",
placeholder: "请选择",
field: "coverageArea",
default: [],
showAllLevels: true,
props: {
label: 'name',
value: 'guid',
lazy: true,
checkStrictly: true,
lazyLoad: getArea,
multiple: false,
},
collapse: true,
tagsTooltip: true,
// filterable: true,
clearable: true,
required: false, //不选默认表示全国。
//col: 'checkbox-right',
visible: true
},
// { 去掉,直接用数据集总行数/全国人口总数计算
// label: '患者占总人口比',
// type: 'input',
// placeholder: '数值,支持小数点9位',
// field: 'patientPopulationRate',
// maxlength: 11,
// min: 0,
// max: 1,
// inputType: 'scoreNumber',
// decimalCnt: 9,
// default: '',
// required: false,
// filterable: true,
// clearable: true,
// visible: true,
// },
{
label: '处理类型',
type: 'select',
placeholder: '请选择',
field: 'handleType',
default: '01',
options: handleTypeList.value,
props: {
label: "label",
value: "value",
},
required: true,
filterable: true,
clearable: true,
visible: true,
},
{
label: '数据来源',
type: 'select',
placeholder: '请选择',
field: 'dataSource',
default: 1,
options: dataSourceTypeList,
props: {
label: "label",
value: "value",
},
required: true,
filterable: true,
visible: true,
}, {
label: '数据源',
type: 'select',
placeholder: '请选择',
field: 'dataSourceGuid',
default: '',
options: dataSourceList.value,
props: {
label: 'databaseNameZh',
value: 'guid'
},
filterable: true,
visible: true,
required: true
}, {
label: "数据表",
type: "select",
placeholder: "请选择",
field: "tableName",
options: dsTableList.value,
props: {
label: 'tableComment',
value: 'tableName'
},
default: '',
filterable: true,
clearable: true,
required: true,
}, {
label: "准标识符",
type: "select",
placeholder: "请选择",
field: "qualifiedIdentifier",
options: dsTableList.value,
props: {
label: 'chName',
value: 'enName'
},
default: [],
multiple: true,
collapse: true,
tagsTooltip: true,
filterable: true,
clearable: true,
required: true,
visible: false,
}, {
label: '文件上传',
tip: '支持扩展名:xlsx、xls、csv,文件大小不超过10MB',
type: 'upload-file',
accept: '.xlsx, .xls, .csv',
limitSize: 10,
limit: 1,
isExcel: true,
required: true,
default: <any>[],
block: false,
col: 'wid60',
visible: false,
field: 'file',
},]);
const dataSelectInfoFormRules = ref({
taskName: [required('请输入数据集名称')],
dataSharingTypeCode: [required('请选择数据共享类型')],
// patientPopulationRate: [required('请输入患者占总人口比')],
dataSourceGuid: [required('请选择数据源')],
handleType: [required('请选择处理类型')],
tableName: [required('请选择数据表')],
qualifiedIdentifier: [{ type: 'array', required: true, trigger: 'change', message: "请选择准标识符" }],
file: [{
validator: (rule: any, value: any, callback: any) => {
if (!value?.length) {
callback(new Error('请上传文件'))
} else {
callback();
}
}, trigger: 'change'
}]
});
/** 最新选中的 */
const currDatasourceSelect: any = ref({});
const handleDataSelectFormSelectChange = async (val, row, formInfo) => {
if (row.field == 'dataSource') {
dataSelectInfoItems.value[5].visible = val == 1;
dataSelectInfoItems.value[6].visible = val == 1;
dataSelectInfoItems.value[8].visible = val == 2;
setDataSelectFormItems(Object.assign({}, formInfo, { file: !formInfo['file'] ? [] : formInfo['file'] }))
sampleTableFields.value = [];
parseFileDataSum.value = [];
sampleTableData.value = [];
} else if (row.field == 'dataSourceGuid') {
if (!val) {
currDatasourceSelect.value = [];
sampleTableFields.value = [];
parseFileDataSum.value = [];
sampleTableData.value = [];
setDataSelectFormItems(Object.assign({}, formInfo, { file: !formInfo['file'] ? [] : formInfo['file'], tableName: '', qualifiedIdentifier: [] }))
let item = dataSelectInfoItems.value.find(d => d.field == 'tableName');
item && (item.options = dsTableList.value);
return;
}
let dsInfo = currDatasourceSelect.value = dataSourceList.value.find(d => d.guid == val);
//清除数据表得值,重新获取下拉列表
const res: any = await getDsTableByDs({
pageSize: -1,
pageIndex: 1,
dataSourceGuid: val,
database: dsInfo.databaseNameEn,
databaseType: dsInfo.databaseType,
tableName: '',
hadFlag: false
});
if (res.code == proxy.$passCode) {
dsTableList.value = res.data?.records || [];
setDataSelectFormItems(Object.assign({}, formInfo, { file: !formInfo['file'] ? [] : formInfo['file'], tableName: '', qualifiedIdentifier: [] }))
let item = dataSelectInfoItems.value.find(d => d.field == 'tableName');
item && (item.options = dsTableList.value);
} else {
proxy.$ElMessage.error(res.msg);
}
sampleTableFields.value = [];
parseFileDataSum.value = [];
sampleTableData.value = [];
} else if (row.field == 'tableName') {
if (!val) {
sampleTableFields.value = [];
sampleTableData.value = [];
return;
}
getDsTableFieldColumn({
pageSize: 50,
pageIndex: 1,
dataSourceGuid: currDatasourceSelect.value.guid,
database: currDatasourceSelect.value.databaseNameEn,
databaseType: currDatasourceSelect.value.databaseType,
tableName: val,
}).then((res: any) => {
if (res.code == proxy.$passCode) {
sampleTableFields.value = res.data?.map(d => {
d.fieldDataType = d.dataType;
d.enName = d.columnName;
d.chName = d.columnZhName;
return d;
}) || [];
/** 判断有抽样数据,需要查询接口 */
getSampleDataByDsTable();
} else {
ElMessage.error(res.msg);
}
});
} else if (row.field == 'handleType') {
setDataSelectFormItems(formInfo);
}
}
const setDataSelectFormItems = (info, isDetail = false) => {
dataSelectInfoItems.value.forEach(item => {
item.default = info[item.field];
if (item.field == 'coverageArea') {
// item && item.children?.length && (item.children[0].visible = info['coverageArea'] != 'all');
if (!isDetail) {
return;
}
let coverageArea = info.coverageArea;
if (coverageArea && Array.isArray(coverageArea) && coverageArea.length > 0) {
item.default = coverageArea[0] as any;
let p: any = [];
coverageArea?.forEach(area => {
if (p.includes(area[0])) {
return;
}
p.push(area[0]);
getArea({ value: area[0], level: 1 }, () => { })
});
let ps: any = []
for (const key in getAreaDataPromise.value) {
ps.push(getAreaDataPromise.value[key])
}
Promise.all(ps).then(() => {
item.default = coverageArea[0];
});
} else {
item.default = '';
}
} else if (item.field == 'qualifiedIdentifier') {
item.visible = info['handleType'] == '02';
}
});
stepsInfo.value = info.handleType == '02' ? reportStepsInfo.value : originStepsInfo.value;
}
const handleDataSelectFormCheckboxChange = (val, info, row) => {
row.field == 'coverageArea' && setDataSelectFormItems(info);
}
const dataSimpleFormRef = ref();
/** 抽样数据预览 */
const dataSimpleFormItems = ref([{
label: '抽样开关',
type: 'switch',
field: 'enableSamplingRate',
default: 'N',
col: 'autoWidth',
activeValue: 'Y',
inactiveValue: 'N'
}, {
label: '抽样比例(%)',
type: 'input',
placeholder: '请输入',
field: 'samplingRate',
maxlength: 3,
min: 0, //可以是0条。万一只是想看下字段呢
max: 100,
inputType: 'integerNumber',
default: 10,
required: true,
filterable: true,
clearable: true,
visible: false,
}]);
const dataSimpleFormRules = ref({
samplingRate: [required('请填写抽样比例')],
});
const oldSamplingRate = ref('10');
const handleDataSimpleFormSwitchChange = (val, info) => {
if (val == 'N') {
oldSamplingRate.value = info.samplingRate;
} else {
dataSimpleFormItems.value[1].default = oldSamplingRate.value || 10;
}
dataSimpleFormItems.value[1].visible = val == 'Y';
dataSimpleFormItems.value[0].default = info.enableSamplingRate || 'N';
if (formRef.value?.formInline?.file?.length) {
transferSampleData();
} else {
getSampleDataByDsTable();
}
}
/** 输入抽样比例值改变 */
const handleDataSimpleFormChange = (val) => {
if (formRef.value?.formInline?.file?.length) {
transferSampleData();
} else {
getSampleDataByDsTable();
}
}
/** 样本表格加载中 */
const sampleTableDataLoading = ref(false);
/** 样本表格的数据 */
const sampleTableData: any = ref([]);
/** 样本表格的字段 */
const sampleTableFields: any = ref([]);
/** otherWidth表示使用标题宽度时添加标题排序图标等宽度 */
const calcTableColumnWidth = (data: any[], prop, title, otherWidth = 0) => {
let d: any[] = [];
data.forEach((dt) => d.push(dt[prop]));
return calcColumnWidth(
d,
title,
{
fontSize: 14,
fontFamily: "SimSun",
},
{
fontSize: 14,
fontFamily: "SimSun",
},
otherWidth
);
};
/** 每列字段对应的列宽计算结果。 */
const originTableFieldColumn = ref({});
const getTextAlign = (field) => {
if (field.dataType === 'decimal' || field.dataType === 'int') {
return 'right';
}
return 'left'
}
watch(
sampleTableData,
(val: any[], oldVal) => {
if (!sampleTableFields.value?.length) {
originTableFieldColumn.value = {};
return;
}
originTableFieldColumn.value = {};
sampleTableFields.value.forEach((field, index) => {
originTableFieldColumn.value[field.enName] = calcTableColumnWidth(
val?.slice(0, 20) || [],
field.enName,
field.chName,
24
);
});
},
{
deep: true,
}
);
watch(() => sampleTableFields.value, (val) => {
let item = dataSelectInfoItems.value.find(selectItem => selectItem.field == 'qualifiedIdentifier');
item && (item.options = val);
let formInfo = formRef.value.formInline;
if (formInfo.handleType == '02' && !(taskGuid.value && (formInfo.file?.[0] && formInfo.file?.[0]?.url == detailInfo.value.filePath?.url || (formInfo.tableName && formInfo.tableName == detailInfo.value.tableName)))) {//需要同步清除准标识符的字段选择
setDataSelectFormItems(Object.assign({}, formInfo, { file: !formInfo['file'] ? [] : formInfo['file'], qualifiedIdentifier: [] }))
}
}, {
deep: true
})
const formatterPreviewDate = (row, info) => {
let enName = info.enName;
let v = row[enName];
if (v === 0) {
return v;
}
if (!v || v == 'null') {
return '--';
}
if (info.dataType === 'datetime') {
return Moment(v).format('YYYY-MM-DD HH:mm:ss');
}
if (info.dataType === 'date') {
if (isNaN(<any>(new Date(v)))) {
return Moment(parseInt(v)).format('YYYY-MM-DD');
} else {
return Moment(v).format('YYYY-MM-DD');
}
}
return v;
};
/** 解析的总的表格数据,方便后面修改抽样比例时使用 */
const parseFileDataSum: any = ref([]);
const currentSheet: any = ref();
const parseFileData = (fileRaw) => {
sampleTableDataLoading.value = true;
fileRaw.arrayBuffer().then(async (f) => {
const wb = XLSX.read(f, {
raw: false, cellDates: true
});
const sheet = wb.Sheets[wb.SheetNames[0]];
currentSheet.value = sheet;
const json: any[] = XLSX.utils.sheet_to_json(sheet, { header: 1 });
if (json.length == 0) {
sampleTableFields.value = [];
sampleTableData.value = [];
} else {
try {
const res: any = await chTransformEn(json[0]);
if (res?.code != proxy.$passCode) {
sampleTableDataLoading.value = false;
proxy.$ElMessage.error(res.msg);
return;
}
let fields = res.data || [];
sampleTableFields.value = fields?.map((j, index) => {
return {
index: index,
enName: j.enName + '',
chName: j.chName + '',
dataType: 'varchar'
}
}) || [];
parseFileDataSum.value = json;
/** 粗略算出字段类型 */
json.slice(1, 10).forEach((info, row) => {
json[0].forEach((name, col) => {
if (info[col] === "" || info[col] == null || sampleTableFields.value[col].dataType != 'varchar') {
return;
} else {
var cellRef = XLSX.utils.encode_cell({ r: row + 1, c: col });
var cell = sheet[cellRef];
let v = cell.w || info[col];
let isNum = cell.t == 'n';
if (isNum) {
if (v.includes('.') && sampleTableFields.value[col].dataType != 'decimal') {
sampleTableFields.value[col].dataType = 'decimal';
} else {
sampleTableFields.value[col].dataType = 'int';
}
}
}
});
})
transferSampleData();
sampleTableDataLoading.value = false;
} catch (error) {
sampleTableDataLoading.value = false;
}
}
});
}
/** 获取文件解析后根据抽样比例得出的表格数据,默认查看前500条数据 */
const transferSampleData = () => {
let samplingRate = dataSimpleFormRef.value?.formInline?.samplingRate;
if (parseFileDataSum.value.length > 1 && samplingRate) {
let totalCnt = parseFileDataSum.value.length - 1;
let cnt = Math.ceil(samplingRate * 0.01 * totalCnt) + 1;
sampleTableData.value = parseFileDataSum.value.slice(1, cnt > 500 ? 501 : cnt).map((info, row) => {
let object = {};
parseFileDataSum.value[0].forEach((chName, col) => {
let name = sampleTableFields.value[col].enName;
var cellRef = XLSX.utils.encode_cell({ r: row + 1, c: col });
var cell = currentSheet.value[cellRef];
let v = cell.w || info[col];
object[name] = v;
});
return object;
});
} else {
sampleTableData.value = [];
}
}
/** 获取选择的数据库表根据抽样比例得出的表格数据 */
const getSampleDataByDsTable = () => {
const tableName = formRef.value?.formInline?.tableName;
if (!currDatasourceSelect.value.guid || !tableName) {
sampleTableFields.value = [];
sampleTableData.value = [];
return;
}
let samplingRate = dataSimpleFormRef.value?.formInline?.samplingRate;
if (!samplingRate) {
sampleTableData.value = [];
return;
}
let totalCnt = dsTableList.value.find(t => t.tableName == tableName)?.tableRows || 0;
let cnt = Math.ceil(samplingRate * 0.01 * totalCnt);
if (!cnt) {
sampleTableData.value = [];
return;
}
sampleTableDataLoading.value = true;
getDsTableSampleData({
limitNum: cnt > 500 ? 500 : cnt,
pageSize: cnt > 500 ? 500 : cnt,
pageIndex: 1,
dataSourceGuid: currDatasourceSelect.value.guid,
database: currDatasourceSelect.value.databaseNameEn,
databaseType: currDatasourceSelect.value.databaseType,
tableName: tableName,
hadFlag: false,
}).then((res: any) => {
sampleTableDataLoading.value = false;
if (res.code == proxy.$passCode) {
sampleTableData.value = res.data?.datas || [];
} else {
sampleTableData.value = [];
ElMessage.error(res.msg);
}
});
}
const uploadFileChange = (file) => {
sampleTableData.value = [];
if (!file.length) {
sampleTableFields.value = [];
sampleTableData.value = [];
return;
}
let fileRaw = file[0].file;
parseFileData(fileRaw);
}
/** 第二步的配置组件引用。 */
const anonTaskStepTwoRef = ref();
const changeStep = async (val) => {
if (val <= step.value) {
step.value = val - 1;
stepsInfo.value.step = val - 1;
} else if (val == 2) {
formRef.value?.ruleFormRef?.validate((valid) => {
if (valid) {
if (formRef.value?.formInline?.dataSource == 2 && !sampleTableFields.value?.length) {
proxy.$ElMessage.error('上传文件的字段不能为空');
return;
}
dataSimpleFormRef.value?.ruleFormRef?.validate((valid) => {
if (valid) {
// 第一步到第二步时,如果字段列表中与字段脱敏规则中的字段不匹配,应清空。
step.value = val - 1;
stepsInfo.value.step = val - 1;
anonTaskStepTwoRef.value?.updateNextStepRules();
}
});
}
});
} else if (val == 3) {
let exec = (saveParams) => {
if (saveParams.coverageArea?.length) {
saveParams.coverageArea = [saveParams.coverageArea];
} else {
saveParams.coverageArea = [];
}
if (saveParams.file?.length) {
saveParams.filePath = {
name: saveParams.file[0].name,
url: saveParams.file[0].url
}
delete saveParams.file;
saveParams.dataSourceGuid = null;
saveParams.tableName = null;
} else {
saveParams.filePath = null;
}
let simpleFormInline = dataSimpleFormRef.value.formInline;
if (simpleFormInline.enableSamplingRate == 'Y') {
saveParams.samplingRate = simpleFormInline.samplingRate && parseInt(simpleFormInline.samplingRate);
} else {
saveParams.samplingRate = null;
}
if (taskGuid.value) {
saveParams.guid = taskGuid.value;
}
if (isEqual(saveParams, oldAnonTaskValueInfo.value)) {
isExecEnd.value = false;
step.value = val - 1;
stepsInfo.value.step = val - 1;
if (!analysisResultInfo.value?.status) {
processStepThreeResultView();
} else {
isExecEnd.value = analysisResultInfo.value?.status == 'E' || analysisResultInfo.value?.status == 'Y';
}
return;
}
if (!taskGuid.value) { //保存
fullscreenLoading.value = true;
saveAnonTask(saveParams).then((res: any) => {
fullscreenLoading.value = false;
if (res.code == proxy.$passCode) {
taskGuid.value = res.data?.taskGuid;
isExecEnd.value = false;
taskExecGuid.value = res.data?.lastExecGuid;
step.value = val - 1;
stepsInfo.value.step = val - 1;
analysisResultInfo.value = {};
if (refreshTimer.value) {
clearInterval(refreshTimer.value);
refreshTimer.value = null;
}
processStepThreeResultView();
oldAnonTaskValueInfo.value = saveParams;
anonymizationStore.setIsAnonPageRefresh(true);
} else {
ElMessage.error(res.msg);
}
});
} else { //更新
fullscreenLoading.value = true;
updateAnonTask(saveParams).then((res: any) => {
fullscreenLoading.value = false;
if (res.code == proxy.$passCode) {
isExecEnd.value = false;
taskExecGuid.value = res.data;
step.value = val - 1;
stepsInfo.value.step = val - 1;
analysisResultInfo.value = {};
if (refreshTimer.value) {
clearInterval(refreshTimer.value);
refreshTimer.value = null;
}
processStepThreeResultView();
oldAnonTaskValueInfo.value = saveParams;
anonymizationStore.setIsAnonPageRefresh(true);
} else {
ElMessage.error(res.msg);
}
})
}
}
let saveParams: any = { ...formRef.value.formInline };
if (saveParams.handleType == '01') {
// 保存并提交 TODO。需要加个 记录旧值的,用来判断新值和旧值,是否发生变化,若变化则需要调用保存接口之后,再进行下一步。
let configInfo = await anonTaskStepTwoRef.value?.getStepTwoConfigInfo();
if (!configInfo) {
return;
}
let privacy = configInfo.anonPrivacyMode; //值是克隆过的,可以删除
delete privacy.isKaNumber;
delete privacy.isRiskThreshold;
delete privacy.isTcField;
delete privacy.isLdField;
// 为空时为了跟原始值保持一致
privacy.kaNumber = (privacy.kaNumber && parseInt(privacy.kaNumber)) ?? null;
privacy.riskThreshold = (privacy.riskThreshold && parseFloat(privacy.riskThreshold)) ?? null;
privacy.tcFieldName = privacy.tcFieldName ?? null;
privacy.tcThreshold = (privacy.tcThreshold && parseFloat(privacy.tcThreshold)) ?? null;
privacy.ldFieldName = privacy.ldFieldName ?? null;
privacy.ldNumber = (privacy.ldNumber && parseInt(privacy.ldNumber)) ?? null;
Object.assign(saveParams, configInfo);
exec(saveParams);
} else {
formRef.value?.ruleFormRef?.validate((valid, errorItem) => {
if (valid) {
if (formRef.value?.formInline?.dataSource == 2 && !sampleTableFields.value?.length) {
proxy.$ElMessage.error('上传文件的字段不能为空');
return;
}
dataSimpleFormRef.value?.ruleFormRef?.validate((valid) => {
if (valid) {
Object.assign(saveParams, { riskThreshold: '0.05' });
exec(saveParams);
}
});
} else {
var obj = Object.keys(errorItem);
formRef.value.ruleFormRef?.scrollToField(obj[0])
}
})
}
} else if (val == 4) {
//下一步之后,设置执行结束, 查看结果。
step.value = val - 1;
stepsInfo.value.step = val - 1;
}
}
const promise: any = ref(null);
const exportResult = () => {
promise.value = exportAnonExecData({
taskGuid: route.query.guid,
execGuid: route.query.execGuid
}).then((res: any) => {
promise.value = null;
if (res && !res.msg) {
download(res, (route.query.taskName || oldAnonTaskValueInfo.value.taskName) + '_匿名化数据.xlsx', 'excel')
} else {
res?.msg && ElMessage.error(res?.msg);
}
}).catch(() => {
promise.value = null;
})
}
/** 获取字段类型的数据字典 */
const fieldTypeList: any = ref([]);
/** 编辑时获取的匿名化任务的详情信息 */
const detailInfo: any = ref({});
/** 记录原始的值信息,防止上一步之后未修改数据时不调用接口 */
const oldAnonTaskValueInfo: any = ref({});
onBeforeMount(() => {
if (taskGuid.value) {
fullscreenLoading.value = true;
getAnonTaskDetail(taskGuid.value).then(async (res: any) => {
if (res?.code == proxy.$passCode) {
detailInfo.value = res.data || {};
taskExecGuid.value = detailInfo.value.lastExecGuid;
oldAnonTaskValueInfo.value = {
guid: detailInfo.value.guid,
taskName: detailInfo.value.taskName,
dataSource: detailInfo.value.dataSource,
filePath: detailInfo.value.filePath && cloneDeep(detailInfo.value.filePath),
dataSourceGuid: detailInfo.value.dataSourceGuid,
tableName: detailInfo.value.tableName,
samplingRate: detailInfo.value.samplingRate,
// patientPopulationRate: detailInfo.value.patientPopulationRate && typeof detailInfo.value.patientPopulationRate == 'number' ? detailInfo.value.patientPopulationRate?.toFixed(9) : detailInfo.value.patientPopulationRate,
dataSharingTypeCode: detailInfo.value.dataSharingTypeCode,
anonTaskRules: cloneDeep(detailInfo.value.anonTaskRules),
anonPrivacyMode: {
kaNumber: detailInfo.value.anonPrivacyMode?.kaNumber,
ldFieldName: detailInfo.value.anonPrivacyMode?.ldFieldName,
ldNumber: detailInfo.value.anonPrivacyMode?.ldNumber,
riskThreshold: detailInfo.value.anonPrivacyMode?.riskThreshold,
tcFieldName: detailInfo.value.anonPrivacyMode?.tcFieldName,
tcThreshold: detailInfo.value.anonPrivacyMode?.tcThreshold,
}
}
setDataSelectFormItems(Object.assign(detailInfo.value, { file: detailInfo.value.filePath ? [detailInfo.value.filePath] : [] }), true);
if (detailInfo.value.samplingRate != null) {
dataSimpleFormItems.value[0].default = 'Y';
dataSimpleFormItems.value[1].visible = true;
dataSimpleFormItems.value[1].default = detailInfo.value.samplingRate;
} else {
dataSimpleFormItems.value[0].default = 'N';
dataSimpleFormItems.value[1].visible = false;
}
let dataSource = detailInfo.value.dataSource;
dataSelectInfoItems.value[5].visible = dataSource == 1;
dataSelectInfoItems.value[6].visible = dataSource == 1;
dataSelectInfoItems.value[8].visible = dataSource == 2;
try {
//文件解析
if (dataSource == 2) {
let url = detailInfo.value.filePath?.url;
sampleTableDataLoading.value = true;
const refSignInfo: any = await getDownFileSignByUrl(parseAndDecodeUrl(url).fileName);
if (!refSignInfo?.data) {
fullscreenLoading.value = false;
refSignInfo?.msg && ElMessage.error(refSignInfo?.msg);
return;
}
const fileRes: any = await obsDownloadRequest(refSignInfo?.data);
sampleTableDataLoading.value = false;
if (fileRes && !fileRes.msg) {
parseFileData(fileRes);
} else {
fileRes?.msg && ElMessage.error(fileRes?.msg);
}
// 会出现从文件切换到数据库时没有数据库列表的问题。
const res: any = await getDatabase({ connectStatus: 1 });
if (res?.code == proxy.$passCode) {
dataSourceList.value = res.data || [];
let item = dataSelectInfoItems.value.find(item => item.field == 'dataSourceGuid');
item && (item.options = dataSourceList.value);
} else {
proxy.$ElMessage.error(res.msg);
}
} else {
const res: any = await getDatabase({ connectStatus: 1 });
if (res?.code == proxy.$passCode) {
dataSourceList.value = res.data || [];
let item = dataSelectInfoItems.value.find(item => item.field == 'dataSourceGuid');
item && (item.options = dataSourceList.value);
} else {
proxy.$ElMessage.error(res.msg);
}
currDatasourceSelect.value = dataSourceList.value.find(d => d.guid == detailInfo.value.dataSourceGuid);
const tableRes: any = await getDsTableByDs({
pageSize: -1,
pageIndex: 1,
dataSourceGuid: detailInfo.value.dataSourceGuid,
database: currDatasourceSelect.value.databaseNameEn,
databaseType: currDatasourceSelect.value.databaseType,
tableName: '',
hadFlag: false
});
if (tableRes?.code == proxy.$passCode) {
dsTableList.value = tableRes.data?.records || [];
let item = dataSelectInfoItems.value.find(item => item.field == 'tableName');
item && (item.options = dsTableList.value);
} else {
proxy.$ElMessage.error(tableRes.msg);
}
getDsTableFieldColumn({
pageSize: 50,
pageIndex: 1,
dataSourceGuid: currDatasourceSelect.value.guid,
database: currDatasourceSelect.value.databaseNameEn,
databaseType: currDatasourceSelect.value.databaseType,
tableName: detailInfo.value.tableName,
}).then((res: any) => {
if (res.code == proxy.$passCode) {
sampleTableFields.value = res.data?.map(d => {
d.fieldDataType = d.dataType;
d.enName = d.columnName;
d.chName = d.columnZhName;
return d;
}) || [];
/** 判断有抽样数据,需要查询接口 */
getSampleDataByDsTable();
} else {
ElMessage.error(res.msg);
}
});
}
fullscreenLoading.value = false;
} catch (error) {
fullscreenLoading.value = false;
}
} else {
fullscreenLoading.value = false;
proxy.$ElMessage.error(res.msg);
}
});
} else {
getDatabase({ connectStatus: 1 }).then((res: any) => {
if (res.code == proxy.$passCode) {
dataSourceList.value = res.data || [];
let item = dataSelectInfoItems.value.find(item => item.field == 'dataSourceGuid');
item && (item.options = dataSourceList.value);
} else {
proxy.$ElMessage.error(res.msg);
}
})
}
getParamsList({
dictType: "数据共享类型",
}).then((res: any) => {
if (res?.code == proxy.$passCode) {
dataSharingTypeList.value = res.data || [];
let item = dataSelectInfoItems.value.find(item => item.field == 'dataSharingTypeCode');
item && (item.options = dataSharingTypeList.value);
} else {
proxy.$ElMessage.error(res.msg);
}
});
getParamsList({
dictType: "数据匿名化处理类型",
}).then((res: any) => {
if (res?.code == proxy.$passCode) {
handleTypeList.value = res.data || [];
let item = dataSelectInfoItems.value.find(item => item.field == 'handleType');
item && (item.options = handleTypeList.value);
} else {
proxy.$ElMessage.error(res.msg);
}
});
getParamsList({
dictType: "字段类型",
}).then((res: any) => {
if (res?.code == proxy.$passCode) {
fieldTypeList.value = res.data || [];
} else {
proxy.$ElMessage.error(res.msg);
}
});
getParentAreaPromise.value = getAreaData({ parentId: null }).then((res: any) => {
if (res?.code == proxy.$passCode) {
parentAreaData.value = res.data ?? [];
return parentAreaData.value;
}
})
})
onMounted(() => {
nextTick(() => {
containerWidth.value = containerRef.value?.offsetWidth || 0;
})
window.onresize = () => {
containerWidth.value = containerRef.value?.offsetWidth || 0;
}
})
const cancelTask = () => {
proxy.$openMessageBox("当前页面尚未保存,确定放弃修改吗?", () => {
userStore.setTabbar(userStore.tabbar.filter((tab: any) => tab.fullPath !== fullPath));
router.push({
name: 'resultProcess'
});
}, () => {
proxy.$ElMessage.info("已取消");
});
}
/** 完成任务关闭 */
const closeTask = () => {
userStore.setTabbar(userStore.tabbar.filter((tab: any) => tab.fullPath !== fullPath));
router.push({
name: 'resultProcess'
});
}
const refreshTimer = ref()
/** 执行结果信息 */
const analysisResultInfo: any = ref({});
const getResultPromise: any = ref(null);
/** 第三步处理,定时刷新查看结果 */
const processStepThreeResultView = (isRefresh = false) => {
let process = (isRefresh) => {
getResultPromise.value = getAnonAnalyzeResult(taskExecGuid.value).then((res: any) => {
getResultPromise.value = null;
if (res?.code == proxy.$passCode) {
analysisResultInfo.value = res.data || {};
if (analysisResultInfo.value.status == 'R') { //正在运行中
if (isRefresh) {
proxy.$ElMessage.success('刷新成功,正在执行中...');
}
//添加定时器。
if (refreshTimer.value) {
return;
}
refreshTimer.value = setInterval(async () => {
process(false);
}, 20000);
} else if (analysisResultInfo.value.status == 'Y') {
//去获取结果。
isExecEnd.value = true;
refreshTimer.value && clearInterval(refreshTimer.value);
refreshTimer.value = null;
analysisResultTableFields.value = res.data?.column || [];
pageInfo.value.curr = 1;
getAnalysisResultPageData();
} else if (analysisResultInfo.value.status == 'E') {
isExecEnd.value = true
refreshTimer.value && clearInterval(refreshTimer.value);
refreshTimer.value = null;
}
} else {
proxy.$ElMessage.error(res.msg);
}
});
}
process(isRefresh);
}
/** 随时点击刷新查看结果。 */
const refreshQueryData = () => {
if (getResultPromise.value) {
return;
}
if (refreshTimer.value) {
clearInterval(refreshTimer.value);
refreshTimer.value = null;
}
processStepThreeResultView(true);
}
/** ------------------------- 匿名化分析结果页面数据展示 ---------------- */
const pageInfo: any = ref({
...commonPageConfig,
})
const pageChange = (info) => {
pageInfo.value.curr = Number(info.curr);
pageInfo.value.limit = Number(info.limit);
getAnalysisResultPageData();
}
/** 每列字段对应的列宽计算结果。 */
const originResultTableFieldColumn = ref({});
/** 结果分析中的字段表格数据 */
const resultData: any = ref([]);
/** 结果分析中的字段信息 */
const analysisResultTableFields: any = ref([]);
const analysisResultLoading = ref(false);
watch(
resultData,
(val: any[], oldVal) => {
if (!analysisResultTableFields.value?.length) {
originResultTableFieldColumn.value = {};
return;
}
originResultTableFieldColumn.value = {};
analysisResultTableFields.value.forEach((field, index) => {
originResultTableFieldColumn.value[field.enName] = calcTableColumnWidth(
val?.slice(0, 20) || [],
field.enName,
field.chName,
24
);
});
},
{
deep: true,
}
);
const getAnalysisResultPageData = () => {
analysisResultLoading.value = true;
getAnonAnalyzePageData({
pageIndex: pageInfo.value.curr,
pageSize: pageInfo.value.limit,
taskExecGuid: taskExecGuid.value,
}).then((res: any) => {
analysisResultLoading.value = false;
if (res?.code == proxy.$passCode) {
pageInfo.value.rows =
resultData.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;
resultData.value.push(obj);
});
pageInfo.value.rows = res.data?.totalRows ?? 0;
} else {
proxy.$ElMessage.error(res.msg);
}
})
}
const downPromise: any = ref()
/** 下载评估报告 */
const transfer = () => {
if (downPromise.value) {
return;
}
downPromise.value = exportAnonReport({
taskGuid: route.query.guid,
execGuid: taskExecGuid.value
}).then((res: any) => {
downPromise.value = null;
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(() => {
refreshTimer.value && clearInterval(refreshTimer.value);
refreshTimer.value = null;
})
</script>
<style lang="scss" scoped>
.top_tool_wrap {
width: 100%;
height: 72px;
margin: 8px 0 0px;
display: flex;
justify-content: center;
align-items: center;
:deep(.el-steps) {
width: 60%;
}
}
.bottom_tool_wrap {
height: 40px;
padding: 0 16px;
border-top: 1px solid #d9d9d9;
display: flex;
justify-content: center;
align-items: center;
}
.content_main {
height: calc(100% - 40px);
padding: 0 16px;
overflow: hidden auto;
}
.operator_panel_wrap {
padding-bottom: 12px;
}
.wait-result-div {
height: 250px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.loading-img {
width: 40px;
height: 40px;
margin-bottom: 18px;
}
.desc {
color: #999;
margin-bottom: 18px;
margin-left: 26px;
}
:deep(.el-icon.failed) {
color: #E63E33;
width: 32px;
height: 32px;
margin-bottom: 8px;
svg {
width: 32px;
height: 32px;
}
}
.error-desc {
color: #E63E33;
font-size: 14px;
line-height: 21px;
margin-bottom: 8px;
font-weight: 600;
}
}
.analysis-result-main {
min-height: 250px;
.value-desc {
font-size: 14px;
color: #212121;
line-height: 21px;
}
.result-title {
font-size: 16px;
color: #212121;
line-height: 24px;
font-weight: 600;
margin-bottom: 6px;
}
.result-title-h1 {
color: #212121;
font-weight: 600;
font-size: 24px;
text-align: center;
line-height: 36px;
margin-top: 12px;
}
.result-title-desc {
color: #666;
font-size: 14px;
line-height: 21px;
margin-top: 12px;
}
.kpi-content {
display: flex;
flex-direction: row;
column-gap: 12px;
row-gap: 12px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.border-content {
height: 76px;
display: flex;
flex-direction: column;
align-items: left;
padding-left: 16px;
justify-content: center;
border: 1px solid #d9d9d9;
width: calc(20% - 8px);
min-width: 228px;
border-radius: 2px;
padding-left: 16px;
.number {
font-weight: 700;
font-size: 20px;
color: #212121;
line-height: 30px;
margin-top: 2px;
&.score-color {
color: #FF5F1F;
}
}
.text {
font-size: 14px;
line-height: 21px;
color: #666666;
display: flex;
.el-icon {
color: #b2b2b2;
}
}
}
.result-table-desc {
font-size: 14px;
color: #999999;
line-height: 21px;
}
.row-two-main {
margin-top: 18px;
display: flex;
.table-one {
width: 586px;
&.border {
border: 1px solid #d9d9d9;
padding: 14px 18px 18px;
}
}
.table-two {
margin-left: 20px;
width: calc(100% - 606px);
&.border {
border: 1px solid #d9d9d9;
padding: 14px 18px 18px;
}
}
}
}
.step-result {
:deep(.v-content-wrap) {
height: 100%;
.el-card__body {
height: calc(100% - 50px) !important;
.card-body-content {
height: 100%;
}
}
.table_tool_wrap {
padding: 0px;
}
}
}
:deep(.custom-form) {
align-items: flex-start;
.wid60.el-form-item {
width: calc(66.66% - 12px);
}
}
:deep(.fixwidth-form) {
width: 500px;
.autoWidth.el-form-item {
width: 80px;
}
}
.table-v2-main {
width: 100%;
height: 240px;
margin-top: 2px;
.main-placeholder {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
.empty-text {
font-size: 14px;
color: #b2b2b2;
}
}
}
:deep(.el-table-v2) {
.el-table-v2__main {
border: 1px solid #d9d9d9;
background: #fff;
}
.el-table-v2__body tr.hover-row.el-table-v2__row--striped.current-row>td.el-table-v2__cell {
background-color: var(--el-table-row-hover-bg-color);
}
.el-table-v2__body tr.current-row>td.el-table-v2__cell {
background-color: var(--el-table-current-row-bg-color);
}
.el-table-v2__header {
width: 100% !important;
}
.el-table-v2__header-cell,
.el-table-v2__row-cell {
border-right: 1px #d9d9d9 solid;
}
.el-table-v2__header-cell-text {
color: #000;
font-weight: normal;
}
.el-table-v2__empty {
display: none !important;
}
.el-table-v2__header-row {
border-bottom: 1px solid #d9d9d9;
}
}
.empty-content {
display: flex;
align-items: center;
justify-content: center;
height: 316px;
width: 100%;
flex-direction: column;
.empty-text {
font-size: 14px;
color: #b2b2b2;
}
}
:deep(.el-form) {
.checkbox-cascader {
display: flex;
.el-cascader {
margin-left: 8px;
}
}
.checkbox-right {
width: calc(100% - 50px);
&.el-form-item {
margin-bottom: 0px;
margin-right: 0px;
width: 100%;
}
}
}
:deep(.cell-tooltip-bg) {
background-color: #fff1d4 !important;
}
:deep(.anlysis-content-wrap) {
.card-title {
justify-content: space-between;
}
}
</style>
\ No newline at end of file
<route lang="yaml">
name: anonTaskStepTwo
</route>
<script lang="ts" setup name="anonTaskStepTwo">
import { TableColumnWidth } from '@/utils/enum';
import { CirclePlus, Delete } from '@element-plus/icons-vue';
import {
getParamsList,
getGeneralizeFileNameList,
getLableByFieldName,
validateAnonRule,
} from '@/api/modules/dataAnonymization';
import { useValidator } from '@/hooks/useValidator';
import { cloneDeep } from 'lodash-es';
const props = defineProps({
fieldTypeList: {
default: [],
type: Array<any>
},
isFile: {
default: false,
type: Boolean
},
fieldNameList: {
default: [],
type: Array<any>
},
anonTaskRules: {
default: [],
type: Array<any>
},
anonPrivacyMode: {
default: {},
type: Object
}
})
const { required } = useValidator();
const { proxy } = getCurrentInstance() as any;
const drawerRef = ref();
/** 泛化文件列表 */
const generalizeFileNameList: any = ref([]);
/** 脱敏规则字典列表 */
const desensitiveRuleTypeList: any = ref([]);
/** 标签类型字典列表 */
const labelTypeList: any = ref([]);
/** 加密算法字典列表 */
const hashMethodList = ref([]);
/** 当前正在编辑的表格索引 */
const currTableRowIndex: any = ref(null);
const ruleModelTableInfo = ref({
id: 'rule-model-table',
loading: false,
minHeight: '150px',
nodeKey: 'guid',
height: '200px',
fields: [
{ label: "序号", type: "index", width: TableColumnWidth.INDEX, align: "center" },
{ label: "字段中文名称", field: "fieldChName", width: 150 },
{ label: "字段英文名称", field: "fieldName", width: 150 },
{ label: "字段类型", field: "fieldTypeName", width: 120 },
{ label: "数据类型", field: "dataTypeName", width: 120 },
{
label: "脱敏方式", field: "desensitiveRule", width: 120, getName: (scope) => {
let rule = scope.row.desensitiveRule;
return rule ? rule : (scope.row.generalizeFileGuid ? '泛化' : '--');
}
},
],
data: <any>[],
showPage: false,
actionInfo: {
label: "操作",
type: "btn",
width: 100,
fixed: 'right',
btns: [
{
label: "编辑", value: "ruleEdit", click: (scope) => {
currTableRowIndex.value = scope.$index;
drawerInfo.value.visible = true;
drawerInfo.value.type = 'edit';
drawerInfo.value.header.title = '编辑字段脱敏规则';
let row = scope.row;
fieldRulesFormItems.value.forEach(item => {
item.default = row[item.field];
if (item.field == 'encryptionAlgorithmCode' || item.field == 'salted') {
item.visible = row.desensitiveRuleCode == 'HASH';
} else if (item.field == 'decimalPlaces') {
item.visible = row.desensitiveRuleCode == 'ROUNDING';
}
});
let fieldNameList = props.fieldNameList.map(f => {
if (f.enName != row.fieldName && ruleModelTableInfo.value.data.some(d => d.fieldName == f.enName)) {
f.disabled = true;
} else {
f.disabled = false;
}
return f;
});
fieldRulesFormItems.value[0].options = fieldNameList;
fieldRulesFormInfo.value.formInfo.items = fieldRulesFormItems.value;
drawerInfo.value.container.contents[0] = fieldRulesFormInfo.value;
desensitiveRuleDetail.value = {
dissembleType: 1, /** 1从左往右,2.从右往左 */
ruleDetails: <any>[{
digitType: 1,
}]
};
charReplaceRuleDetail.value = {
replaceType: 1, /** 1从左往右,2.从右往左 */
ruleDetails: <any>[{
digitType: 1,
ruleType: 1
}]
};
rangeReplaceRuleDetails.value = [{
lowOperator: '≤',
fieldChName: row.fieldChName,
upperOperator: '≤'
}];
if (row.desensitiveRuleCode == 'DISSEMBLE') {
desensitiveRuleDetail.value = row.desensitiveRuleDetail || {};
} else if (row.desensitiveRuleCode == 'CHARREPLACE') {
charReplaceRuleDetail.value = row.desensitiveRuleDetail || {};
} else if (row.desensitiveRuleCode == 'RANGEREPLACE') {
rangeReplaceRuleDetails.value = row.desensitiveRuleDetail?.ruleDetails?.map(rd => {
rd.fieldChName = row.fieldChName;
return rd;
}) || [];
}
fieldRulesEndFormInfo.value.formInfo.items[0].default = '';
fieldRulesEndFormInfo.value.formInfo.items[1].default = '';
if (!row.desensitiveRuleCode) {
drawerInfo.value.container.contents = [fieldRulesFormInfo.value];
} else {
drawerInfo.value.container.contents = [fieldRulesFormInfo.value, fieldRulesEndFormInfo.value];
}
}
},
{
label: "删除", value: "delete", click: (scope) => {
proxy.$openMessageBox("此操作将永久删除, 是否继续?", () => {
let fieldName = scope.row.fieldName;
ruleModelTableInfo.value.data.splice(scope.$index, 1);
// updatePrivacyFormFieldsOptions(fieldName);
// 同步去掉隐私模型设置中的字段。
proxy.$ElMessage.success('删除成功');
}, () => {
proxy.$ElMessage.info("已取消");
})
}
},
],
}
})
/** 字段脱敏规则表单配置 */
const fieldRulesFormItems = ref([{
label: '选择字段',
type: 'select',
placeholder: '请选择',
field: 'fieldName',
default: '',
options: props.fieldNameList,
props: {
label: 'chName',
value: 'enName',
disabled: 'disabled'
},
filterable: true,
clearable: false,
required: true
}, {
label: '字段类型',
type: 'select',
placeholder: '请选择',
field: 'fieldTypeCode',
default: 'varchar',
options: props.fieldTypeList,
props: {
label: 'label',
value: 'value'
},
disabled: true,
clearable: true,
required: true
}, {
label: '数据类型',
type: 'select',
placeholder: '请选择',
field: 'dataTypeCode',
default: '',
options: labelTypeList.value,
props: {
label: 'label',
value: 'value'
},
filterable: true,
clearable: true,
required: false
}, {
label: 'K匿名泛化',
type: 'select',
placeholder: '请选择',
field: 'generalizeFileGuid',
default: '',
options: generalizeFileNameList.value,
props: {
label: 'generalizeFileName',
value: 'guid'
},
filterable: true,
clearable: true,
required: false
}, {
label: '脱敏规则',
type: 'select',
placeholder: '请选择',
field: 'desensitiveRuleCode',
default: 'DISSEMBLE',
options: desensitiveRuleTypeList.value,
props: {
label: 'label',
value: 'value'
},
filterable: true,
clearable: true,
required: false
}, {
label: '加密算法',
type: 'select',
placeholder: '请选择',
field: 'encryptionAlgorithmCode',
default: '',
options: hashMethodList.value,
props: {
label: 'label',
value: 'value'
},
filterable: true,
clearable: true,
visible: false,
required: true
}, {
label: '加盐值',
type: 'input',
placeholder: '请输入0~9',
field: 'salted',
maxlength: 1,
min: 0,
max: 9,
inputType: 'integerNumber',
default: 5,
required: true,
filterable: true,
clearable: true,
visible: false,
}, {
label: '保留小数',
type: 'input',
placeholder: '请输入0~5',
field: 'decimalPlaces',
maxlength: 1,
min: 0,
max: 5,
inputType: 'integerNumber',
default: 2,
required: true,
filterable: true,
clearable: true,
visible: false,
}]);
const fieldRulesFormRules = ref({
fieldName: [required('请选择字段')],
dataTypeCode: [required('请选择数据类型')], //不填标识非敏感标识。
// desensitiveRuleCode: [required('请选择脱敏规则')], 脱敏规则和泛化文件选择一个即可。二者必选其一,可以两者共存。
encryptionAlgorithmCode: [required('请选择加密算法')],
salted: [required('请输入加盐值')],
decimalPlaces: [required('请输入保留小数')]
});
const fieldRulesFormInfo = ref({
type: "form",
title: "",
col: "span",
formInfo: {
id: "add-class-form",
readonly: false,
items: fieldRulesFormItems.value,
rules: fieldRulesFormRules.value,
},
});
const fieldRulesEndFormInfo = ref({
type: "form",
title: "",
col: "mt8",
showSlot: false,
formInfo: {
id: "add-rules-end-form",
readonly: false,
items: [{
label: '样本数据',
type: 'input',
placeholder: '请输入样本',
field: 'testData',
default: '',
required: false,
clearable: true,
block: true,
col: 'mb8',
validateBtn: {
value: 'validate',
label: '验证',
click: () => {
let formInline = drawerRef.value?.getDrawerConRef('drawerFormRef')?.formInline;
let formInline1 = drawerRef.value?.getDrawerConRef('drawerFormRef', 1)?.formInline;
if (!formInline1.testData) {
proxy.$ElMessage.error('样本数据不能为空');
return;
}
validateAnonRule({
desensitiveRuleCode: formInline.desensitiveRuleCode,
value: formInline1.testData,
desensitiveRuleDetail: getDesensitiveRuleDetailInfo(formInline)
}).then((res: any) => {
if (res?.code == proxy.$passCode) {
let result = res.data;
fieldRulesEndFormInfo.value.formInfo.items[0].default = formInline1.testData;
fieldRulesEndFormInfo.value.formInfo.items[1].default = formInline.desensitiveRuleCode == 'BLANK' ? ' ' : result;
} else {
proxy.$ElMessage.error(res.msg);
}
})
}
}
}, {
label: '脱敏效果',
type: 'input',
placeholder: '点击验证后展示脱敏效果',
field: 'validateResult',
default: '',
block: true,
required: false,
disabled: true
}],
rules: {},
},
});
/** 获取规则配置详情。 */
const getDesensitiveRuleDetailInfo = (formInline) => {
let desensitiveRuleDetailInfo: any = {};
if (formInline.desensitiveRuleCode == 'DISSEMBLE') {
desensitiveRuleDetailInfo = desensitiveRuleDetail.value;
} else if (formInline.desensitiveRuleCode == 'CHARREPLACE') {
desensitiveRuleDetailInfo = charReplaceRuleDetail.value;
} else if (formInline.desensitiveRuleCode == 'RANGEREPLACE') {
desensitiveRuleDetailInfo = {
ruleDetails: rangeReplaceRuleDetails.value
};
} else if (formInline.desensitiveRuleCode == 'ROUNDING') {
desensitiveRuleDetailInfo = {
decimalPlaces: formInline.decimalPlaces
}
} else if (formInline.desensitiveRuleCode == 'HASH') {
desensitiveRuleDetailInfo = {
encryptionAlgorithmCode: formInline.encryptionAlgorithmCode,
salted: formInline.salted
}
}
return desensitiveRuleDetailInfo;
}
const drawerInfo = ref({
visible: false,
direction: 'rtl',
size: 540,
header: {
title: '添加字段脱敏规则',
},
type: '',
container: {
contents: [fieldRulesFormInfo.value, fieldRulesEndFormInfo.value],
},
footer: {
visible: true,
btns: [
{ type: 'default', label: '取消', value: 'cancel' },
{ type: 'primary', label: '确定', value: 'save', loading: false },
]
}
});
const addRowRules = () => {
drawerInfo.value.visible = true;
drawerInfo.value.type = 'add';
drawerInfo.value.header.title = '添加字段脱敏规则';
fieldRulesFormItems.value.forEach(item => {
if (item.field == 'fieldTypeCode') {
item.default = 'varchar';
} else if (item.field == 'desensitiveRuleCode') {
item.default = 'DISSEMBLE';
} else if (item.field == 'encryptionAlgorithmCode' || item.field == 'salted') {
item.visible = false;
item.default = '';
item.field == 'salted' && (item.default = 5);
} else if (item.field == 'decimalPlaces') {
item.visible = false;
item.default = 2;
} else {
item.default = '';
}
});
let fieldNameList = props.fieldNameList.map(f => {
if (ruleModelTableInfo.value.data.some(d => d.fieldName == f.enName)) {
f.disabled = true;
} else {
f.disabled = false;
}
return f
});
fieldRulesFormItems.value[0].options = fieldNameList;
fieldRulesFormInfo.value.formInfo.items = fieldRulesFormItems.value;
desensitiveRuleDetail.value = {
dissembleType: 1, /** 1从左往右,2.从右往左 */
ruleDetails: <any>[{
digitType: 1,
}]
};
charReplaceRuleDetail.value = {
replaceType: 1, /** 1从左往右,2.从右往左 */
ruleDetails: <any>[{
digitType: 1,
ruleType: 1
}]
};
rangeReplaceRuleDetails.value = [{
lowOperator: '≤',
fieldChName: '',
upperOperator: '≤'
}];
fieldRulesEndFormInfo.value.formInfo.items[0].default = '';
fieldRulesEndFormInfo.value.formInfo.items[1].default = '';
drawerInfo.value.container.contents = [fieldRulesFormInfo.value, fieldRulesEndFormInfo.value];
}
const drawerBtnClick = async (btn, info) => {
if (btn.value == 'cancel') {
drawerInfo.value.visible = false
} else {
if (!info.generalizeFileGuid && !info.desensitiveRuleCode) {
proxy.$ElMessage.error('K匿名泛化与脱敏规则不能同时为空');
return;
}
if (!(info.fieldTypeCode == 'int' || info.fieldTypeCode == 'decimal' || info.fieldTypeCode == 'tinyint')) {
if (info.desensitiveRuleCode == 'ROUNDING') {
proxy.$ElMessage.error('非数值类型字段的脱敏规则不能设置取整');
return;
}
if (info.desensitiveRuleCode == 'RANGEREPLACE') {
proxy.$ElMessage.error('非数值类型字段的脱敏规则不能设置区间替换');
return;
}
}
drawerInfo.value.footer.btns[1].loading = true;
let desensitiveRuleDetailInfo = getDesensitiveRuleDetailInfo(info);
// 脱敏规则为掩盖,字符,区间替换存在时需要调用接口检验
if (info.desensitiveRuleCode == 'DISSEMBLE' || info.desensitiveRuleCode == 'CHARREPLACE' || info.desensitiveRuleCode == 'RANGEREPLACE') {
try {
let res: any = await validateAnonRule({
desensitiveRuleCode: info.desensitiveRuleCode,
value: '',
desensitiveRuleDetail: desensitiveRuleDetailInfo
})
if (res?.code != proxy.$passCode) {
proxy.$ElMessage.error(res.msg);
drawerInfo.value.footer.btns[1].loading = false;
return;
}
} catch (error) {
drawerInfo.value.footer.btns[1].loading = false;
}
}
drawerInfo.value.footer.btns[1].loading = false;
let saveData: any = { ...info };
saveData.desensitiveRuleDetail = desensitiveRuleDetailInfo;
saveData.fieldChName = props.fieldNameList?.find(f => f.enName == saveData.fieldName)?.chName;
saveData.dataTypeName = saveData.dataTypeCode && labelTypeList.value.find(l => l.value == saveData.dataTypeCode)?.label;
saveData.fieldTypeName = saveData.fieldTypeCode && props.fieldTypeList?.find(f => f.value == saveData.fieldTypeCode)?.label;
saveData.desensitiveRule = info.desensitiveRuleCode && desensitiveRuleTypeList.value.find(d => d.value == info.desensitiveRuleCode)?.label;
let changeFields = '';
if (drawerInfo.value.type == 'add') {
ruleModelTableInfo.value.data.push(saveData);
} else if (drawerInfo.value.type == 'edit') {
if (!currTableRowIndex.value != null) {
let originFieldName = ruleModelTableInfo.value.data[currTableRowIndex.value]?.fieldName;
changeFields = originFieldName == saveData.fieldName ? '' : originFieldName;
ruleModelTableInfo.value.data[currTableRowIndex.value] = saveData;
}
}
drawerInfo.value.visible = false;
// updatePrivacyFormFieldsOptions(changeFields);
}
}
/** 更新下拉选择字段的下拉列表,若是字段被删除,需要同步去掉下拉选择值。 */
const updatePrivacyFormFieldsOptions = (changeFields) => {
privacyFormItems.value[2].children[0].options = props.fieldNameList;
privacyFormItems.value[3].children[0].options = props.fieldNameList;
let formInline = privacyFormRef.value?.formInline;
privacyFormItems.value.forEach((item, index) => {
item.default = formInline[item.field];
if (item.default == 'Y') {
item.children?.forEach(child => {
child.default = formInline[child.field];
if (changeFields && (child.field == 'tcFieldName' || child.field == 'ldFieldName') && child.default == changeFields) {
child.default = ''
}
});
}
})
}
/** 当前选择的字段对应的标签信息。 */
const currLabelInfo: any = ref({});
const drawerSelectChange = (val, row, info) => {
if (row.field === 'desensitiveRuleCode') {
let methodItem = fieldRulesFormItems.value.find(item => item.field == 'encryptionAlgorithmCode');
let saltedItem = fieldRulesFormItems.value.find(item => item.field == 'salted');
let decimalPlaceItem = fieldRulesFormItems.value.find(item => item.field == 'decimalPlaces');
methodItem && (methodItem.visible = val == 'HASH');
saltedItem && (saltedItem.visible = val == 'HASH');
decimalPlaceItem && (decimalPlaceItem.visible = val == 'ROUNDING');
fieldRulesFormItems.value.forEach(item => {
item.default = info[item.field];
if (item.field == 'decimalPlaces') {
if (item.default == null) {
item.default = 2;
}
} else if (item.field == 'salted') {
if (item.default == null) {
item.default = 5;
}
}
});
if (val == 'RANGEREPLACE') {
let fieldName = info.fieldName;
let fieldChName = fieldName && props.fieldNameList.find(f => f.enName == fieldName)?.chName;
rangeReplaceRuleDetails.value.forEach(r => {
if (fieldChName && r.fieldChName != fieldChName) {
r.fieldChName = fieldChName;
}
});
}
if (!val) {
drawerInfo.value.container.contents = [fieldRulesFormInfo.value];
} else {
drawerInfo.value.container.contents = [fieldRulesFormInfo.value, fieldRulesEndFormInfo.value];
}
}
// else if (row.field == 'fieldTypeCode') {
// fieldRulesFormItems.value.forEach(item => {
// item.default = info[item.field];
// if (item.field == 'desensitiveRuleCode') {
// if (!(val == 'int' || val == 'decimal' || val == 'tinyint')) {
// item.options = desensitiveRuleTypeList.value.filter(d => d.value != 'ROUNDING' && d.value != 'RANGEREPLACE');
// if (item.default == 'ROUNDING' || item.default == 'RANGEREPLACE') {
// item.default = 'DISSEMBLE';
// }
// } else {
// item.options = desensitiveRuleTypeList.value;
// }
// }
// });
// }
else if (row.field == 'fieldName') { //选择字段改变之后,调用接口。
let tableField = props.fieldNameList.find(f => f.enName == val);
let dataType = tableField?.dataType || 'varchar';
fieldRulesFormItems.value.forEach(item => {
item.default = info[item.field];
if (item.field == 'fieldTypeCode') {
item.default = dataType;
}
});
getLableByFieldName(val).then((res: any) => {
if (res?.code == proxy.$passCode) {
let labelInfo = currLabelInfo.value = res.data || {};
fieldRulesFormItems.value.forEach(item => {
item.default = info[item.field];
if (item.field == 'dataTypeCode') {
item.default = labelInfo.labelTypeCode;
} else if (item.field == 'fieldTypeCode') {
item.default = dataType;
}
});
} else {
proxy.$ElMessage.error(res.msg);
}
})
if (info.desensitiveRuleCode == 'RANGEREPLACE') {
rangeReplaceRuleDetails.value.forEach(r => {
r.fieldChName = tableField?.chName || val;
});
}
}
}
const drawerInputChange = (val, row, info) => {
if (row.field == 'testData') { //样本数据修改后,清空脱敏结果
fieldRulesEndFormInfo.value.formInfo.items[0].default = val;
fieldRulesEndFormInfo.value.formInfo.items[1].default = '';
}
}
/** 掩盖规则中的位数和剩余位数 */
const digitTypeList = ref([{
value: 1,
label: '位数'
}, {
value: 2,
label: '剩余位数'
}]);
const dissembleRuleTypeList = ref([{
value: 1,
label: '脱敏'
}, {
value: 2,
label: '不脱敏'
}]);
/** 掩盖类型的脱敏规则 分段规则配置 */
const desensitiveRuleDetail = ref({
dissembleType: 1, /** 1从左往右,2.从右往左 */
ruleDetails: <any>[{
digitType: 1,
}]
});
const addSegmentRule = () => {
desensitiveRuleDetail.value.ruleDetails.push({
digitType: 1,
});
};
const deleteSegmentRule = (item, index) => {
desensitiveRuleDetail.value.ruleDetails.splice(index, 1);
}
const inputEventDigitChange = (val, item, prop = 'digit') => {
item[prop] = item[prop].toString().replace(/\./g, "")
item[prop] = item[prop].toString().replace(/^\D*(\d{0,7}(?:\.\d{0})?).*$/g, "$1")
if (prop == 'digit' && val != "" && item.digit < 1) {
item.digit = 1;
}
}
/** 字符规则中的替换方式 */
const charReplaceRuleTypeList = ref([{
value: 1,
label: '随机替换'
}, {
value: 2,
label: '固定值替换'
}]);
/** 字符替换的脱敏规则 分段规则配置 */
const charReplaceRuleDetail = ref({
replaceType: 1, /** 1从左往右,2.从右往左 */
ruleDetails: <any>[{
digitType: 1,
ruleType: 1
}]
});
const addCharReplaceSegmentRule = () => {
charReplaceRuleDetail.value.ruleDetails.push({
digitType: 1,
ruleType: 1
});
};
const deleteCharReplaceSegmentRule = (item, index) => {
charReplaceRuleDetail.value.ruleDetails.splice(index, 1);
}
/** ----- 区间替换规则 --- */
/** 区间替换的小于或小于等于字符列表 */
const lowerOperatorList: any = ref([{
value: '≤',
label: '≤'
}, {
value: '<',
label: '<'
}]);
/**
* 字符替换的脱敏规则 分段规则配置。
* { lowValue: , lowOperator: , upperValue: , upperOperator: , replaceValue: '' }
*/
const rangeReplaceRuleDetails: any = ref([{
lowOperator: '≤',
fieldChName: '',
upperOperator: '≤'
}]);
const addRangeReplaceSegmentRule = () => {
let fieldChName = '';
if (rangeReplaceRuleDetails.value?.length) {
fieldChName = rangeReplaceRuleDetails.value[0].fieldChName;
} else {
let fieldName = drawerRef.value?.getDrawerConRef('drawerFormRef')?.formInline?.fieldName;
let tableField = fieldName && props.fieldNameList?.find(f => f.enName == fieldName);
fieldChName = tableField?.chName;
}
rangeReplaceRuleDetails.value.push({
lowOperator: '≤',
fieldChName: fieldChName,
upperOperator: '≤'
});
};
const deleteRangeReplaceSegmentRule = (item, index) => {
rangeReplaceRuleDetails.value.splice(index, 1);
}
/** 隐私模型设置 */
const privacyFormItems: any = ref([{
label: '选择准标识符等价类隐私模型',
type: 'checkbox-input-item',
field: 'isKaNumber',
default: 'N',
placeholder: 'K匿名',
trueValue: 'Y',
falseValue: 'N',
children: [
{
label: '',
type: 'input',
placeholder: '请输入',
field: 'kaNumber',
inputType: 'integerNumber',
default: '',
min: 1,
maxlength: 6,
disabled: false,
clearable: true,
visible: false,
col: 'ka-checkbox-input',
style: { width: '100px', margin: 0 }
}
],
required: true,
col: 'checkbox-input'
}, {
label: '设置重标识可接受风险阈值',
type: 'checkbox-input-item',
field: 'isRiskThreshold',
default: 'N',
placeholder: '阈值',
trueValue: 'Y',
falseValue: 'N',
children: [
{
label: '',
type: 'input',
placeholder: '请输入',
field: 'riskThreshold',
default: '',
maxlength: 6,
min: 0,
max: 1,
inputType: 'scoreNumber',
disabled: false,
clearable: true,
visible: false,
col: 'ka-checkbox-input',
style: { width: '100px', margin: 0 }
}
],
required: true,
col: 'checkbox-input'
}, {
label: '设置L多样性及T接近',
type: 'checkbox-input-item',
field: 'isLdField',
default: 'N',
placeholder: 'L多样性',
trueValue: 'Y',
falseValue: 'N',
children: [
{
label: '',
type: 'select',
placeholder: '请选择字段',
field: 'ldFieldName',
options: props.fieldNameList || [],
props: {
label: 'chName',
value: 'enName',
disabled: false
},
default: '',
filterable: true,
clearable: true,
visible: false,
col: 'ka-checkbox-input',
style: { width: '120px', margin: 0 }
},
{
label: '',
type: 'input',
placeholder: '请输入',
field: 'ldNumber',
inputType: 'integerNumber',
default: '',
maxlength: 6,
min: 1,
disabled: false,
clearable: true,
visible: false,
col: 'ka-checkbox-input',
style: { width: '100px', margin: 0 }
}
],
block: false,
required: true,
col: 'checkbox-input'
}, {
label: '',
type: 'checkbox-input-item',
field: 'isTcField',
default: 'N',
placeholder: 'T接近',
trueValue: 'Y',
falseValue: 'N',
style: { 'margin-top': '2px' },
children: [
{
label: '',
type: 'select',
placeholder: '请选择字段',
field: 'tcFieldName',
default: '',
options: props.fieldNameList || [],
props: {
label: 'chName',
value: 'enName',
disabled: false
},
filterable: true,
clearable: true,
visible: false,
col: 'ka-checkbox-input',
style: { width: '120px', margin: 0 }
},
{
label: '',
type: 'input',
placeholder: '请输入',
field: 'tcThreshold',
default: '',
maxlength: 6,
min: 0,
max: 1,
inputType: 'scoreNumber',
disabled: false,
clearable: true,
visible: false,
col: 'ka-checkbox-input',
style: { width: '100px', margin: 0 }
}
],
required: false,
block: true,
col: 'checkbox-input lmt12'
}]);
const privacyFormRules = ref({
kaNumber: [required('请输入K匿名值')],
riskThreshold: [required('请输入阈值')],
ldFieldName: [required('请选择L多样性字段')],
ldNumber: [required('请输入L多样性值')],
tcFieldName: [required('请选择T接近字段')],
tcThreshold: [required('请输入T接近阈值')]
});
/** 记录下旧的隐私模型设置 */
const oldPrivacyModelValue: any = ref({
isKaNumber: 'N',
isRiskThreshold: 'N',
isLdField: 'N',
isTcField: 'N'
});
const handleCheckboxChange = (val, value, row) => {
oldPrivacyModelValue.value = Object.assign({}, oldPrivacyModelValue.value, value);
privacyFormItems.value.forEach(item => {
item.default = oldPrivacyModelValue.value[item.field];
item.children?.forEach(child => {
child.default = oldPrivacyModelValue.value[child.field];
});
})
if (row.field == 'isKaNumber') {
let kaItem = privacyFormItems.value[0]?.children?.[0];
kaItem && (kaItem.visible = val == 'Y');
} else if (row.field == 'isRiskThreshold') {
let riskItem = privacyFormItems.value[1]?.children?.[0];
riskItem && (riskItem.visible = val == 'Y');
} else if (row.field == 'isLdField') {
let childrenItem = privacyFormItems.value[2]?.children;
childrenItem?.[0] && (childrenItem[0].visible = val == "Y");
childrenItem?.[1] && (childrenItem[1].visible = val == "Y");
} else if (row.field == 'isTcField') {
let childrenItem = privacyFormItems.value[3]?.children;
childrenItem?.[0] && (childrenItem[0].visible = val == "Y");
childrenItem?.[1] && (childrenItem[1].visible = val == "Y");
}
}
watch(() => props.fieldNameList, (val) => {
fieldRulesFormItems.value[0].options = val || [];
if (props.isFile) {
fieldRulesFormItems.value[1].disabled = false
} else {
fieldRulesFormItems.value[1].disabled = true;
}
}, {
immediate: true
})
watch(() => props.fieldTypeList, (val) => {
fieldRulesFormItems.value[1].options = val || [];
}, {
immediate: true
})
watch(() => props.anonPrivacyMode, (val) => {
if (!val) {
return;
}
let hasKaNumber = val.kaNumber != null;
privacyFormItems.value[0].default = hasKaNumber ? 'Y' : 'N';
privacyFormItems.value[0].children[0].visible = hasKaNumber;
privacyFormItems.value[0].children[0].default = val.kaNumber;
oldPrivacyModelValue.value.isKaNumber = hasKaNumber ? 'Y' : 'N';
oldPrivacyModelValue.value.kaNumber = val.kaNumber;
let hasRiskThreshold = val.riskThreshold != null;
privacyFormItems.value[1].default = hasRiskThreshold ? 'Y' : 'N';
privacyFormItems.value[1].children[0].visible = hasRiskThreshold;
privacyFormItems.value[1].children[0].default = val.riskThreshold;
oldPrivacyModelValue.value.isRiskThreshold = hasRiskThreshold ? 'Y' : 'N';
oldPrivacyModelValue.value.riskThreshold = val.riskThreshold;
let hasldFieldName = !!val.ldFieldName;
privacyFormItems.value[2].default = hasldFieldName ? 'Y' : 'N';
privacyFormItems.value[2].children[0].visible = hasldFieldName;
privacyFormItems.value[2].children[1].visible = hasldFieldName;
privacyFormItems.value[2].children[0].default = val.ldFieldName;
privacyFormItems.value[2].children[1].default = val.ldNumber;
oldPrivacyModelValue.value.isLdField = hasldFieldName ? 'Y' : 'N';
oldPrivacyModelValue.value.ldFieldName = val.ldFieldName;
oldPrivacyModelValue.value.ldNumber = val.ldNumber;
let hasTcField = !!val.tcFieldName;
privacyFormItems.value[3].default = hasTcField ? 'Y' : 'N';
privacyFormItems.value[3].children[0].visible = hasTcField;
privacyFormItems.value[3].children[1].visible = hasTcField;
privacyFormItems.value[3].children[0].default = val.tcFieldName;
privacyFormItems.value[3].children[1].default = val.tcThreshold;
oldPrivacyModelValue.value.isTcField = hasTcField ? 'Y' : 'N';
oldPrivacyModelValue.value.tcFieldName = val.tcFieldName;
oldPrivacyModelValue.value.tcThreshold = val.tcThreshold;
}, {
deep: true
})
watch(() => props.anonTaskRules, (val) => {
ruleModelTableInfo.value.data = val || [];
let optionsList = val?.map(v => {
return {
fieldName: v.fieldName,
fieldChName: v.fieldChName
}
}) || [];
privacyFormItems.value[2].children[0].options = optionsList;
privacyFormItems.value[3].children[0].options = optionsList;
})
onBeforeMount(() => {
getParamsList({
dictType: "标签类型",
}).then((res: any) => {
if (res?.code == proxy.$passCode) {
labelTypeList.value = res.data || [];
let item = fieldRulesFormItems.value.find(item => item.field == 'dataTypeCode');
item && (item.options = labelTypeList.value);
} else {
proxy.$ElMessage.error(res.msg);
}
});
getParamsList({
dictType: "脱敏规则",
}).then((res: any) => {
if (res?.code == proxy.$passCode) {
desensitiveRuleTypeList.value = res.data || [];
let item = fieldRulesFormItems.value.find(item => item.field == 'desensitiveRuleCode');
item && (item.options = desensitiveRuleTypeList.value);
} else {
proxy.$ElMessage.error(res.msg);
}
});
getParamsList({
dictType: "加密算法",
}).then((res: any) => {
if (res?.code == proxy.$passCode) {
hashMethodList.value = res.data || [];
let item = fieldRulesFormItems.value.find(item => item.field == 'encryptionAlgorithmCode');
item && (item.options = hashMethodList.value);
} else {
proxy.$ElMessage.error(res.msg);
}
});
getGeneralizeFileNameList().then((res: any) => {
if (res?.code == proxy.$passCode) {
generalizeFileNameList.value = res.data || [];
let item = fieldRulesFormItems.value.find(item => item.field == 'generalizeFileGuid');
item && (item.options = generalizeFileNameList.value);
} else {
proxy.$ElMessage.error(res.msg);
}
});
})
/** 隐私模型设置表单 */
const privacyFormRef = ref();
const getStepTwoConfigInfo = async () => {
if (!ruleModelTableInfo.value.data?.length) {
proxy.$ElMessage.error('字段脱敏规则不能为空');
return false;
}
try {
await privacyFormRef.value?.ruleFormRef?.validate();
// 验证通过
return {
anonPrivacyMode: cloneDeep(privacyFormRef.value?.formInline),
anonTaskRules: cloneDeep(ruleModelTableInfo.value.data)
};
} catch (error) {
// 验证失败
return false;
}
}
/** 字段改变,可能是切换了表,需要清空规则配置。判断 第一步到第二步时,如果字段列表中与字段脱敏规则中的字段不匹配,应清空,并同时清空T接近字段。 */
const updateNextStepRules = () => {
ruleModelTableInfo.value.data = ruleModelTableInfo.value.data.filter(rule => {
if (!props.fieldNameList?.some(v => v.enName == rule.fieldName)) {
return false;
}
return true;
});
privacyFormItems.value[2].children[0].options = props.fieldNameList;
privacyFormItems.value[3].children[0].options = props.fieldNameList;
let formInline = privacyFormRef.value?.formInline;
privacyFormItems.value.forEach((item, index) => {
item.default = formInline[item.field];
if (item.default == 'Y') {
item.children?.forEach(child => {
child.default = formInline[child.field];
if ((child.field == 'tcFieldName' || child.field == 'ldFieldName') && !props.fieldNameList.some(f => f.enName == child.default)) {
child.default = ''
}
});
}
})
}
defineExpose({
getStepTwoConfigInfo,
updateNextStepRules
})
</script>
<template>
<div class="operator_panel_wrap">
<ContentWrap id="id-rules" title="设置匿名化规则" description="" style="margin-top: 8px;">
<Table :tableInfo="ruleModelTableInfo" />
<div class="row-add-btn">
<el-button link @click="addRowRules" :icon="CirclePlus" v-preReClick>添加字段脱敏规则</el-button>
</div>
</ContentWrap>
<ContentWrap id="id-screctModel" title="隐私模型设置" description="" style="margin-top: 16px;">
<Form style="width: 80%;max-width: 500px;" ref="privacyFormRef" :itemList="privacyFormItems"
:rules="privacyFormRules" formId="model-select-edit" @checkboxChange="handleCheckboxChange" />
</ContentWrap>
<Drawer ref="drawerRef" :drawerInfo="drawerInfo" @drawerBtnClick="drawerBtnClick"
@drawerInputChange="drawerInputChange" @drawerSelectChange="drawerSelectChange">
<template v-slot:default>
<div v-show="drawerRef?.getDrawerConRef('drawerFormRef')?.formInline?.desensitiveRuleCode == 'DISSEMBLE'">
<div>{{ '请配置分段是否脱敏' + `(${desensitiveRuleDetail.ruleDetails?.length}/10)` }}</div>
<el-radio-group v-model="desensitiveRuleDetail.dissembleType">
<el-radio :value="1">从左往右</el-radio>
<el-radio :value="2">从右往左</el-radio>
</el-radio-group>
<div class="seg-main">
<div class="row-per" v-for="(item, index) in desensitiveRuleDetail.ruleDetails">
<el-select v-model="item.digitType" :style="{ width: item.digitType == 2 ? '322px' : '170px' }">
<el-option v-for="item in digitTypeList" :label="item.label" :value="item.value"
:key="item.value"></el-option>
</el-select>
<el-input v-show="item.digitType != 2" style="width:137px;margin-left: 4px;" v-model="item.digit"
:maxlength="6" :min="1" @input="(val) => inputEventDigitChange(val, item)" placeholder="请输入"></el-input>
<el-select v-model="item.ruleType" style="width:170px;margin-left: 4px;">
<el-option v-for="item in dissembleRuleTypeList" :label="item.label" :value="item.value"
:key="item.value"></el-option>
</el-select>
<div class="title_tool" @click="deleteSegmentRule(item, index)">
<el-icon :size="20" color="#b2b2b2">
<Delete />
</el-icon>
</div>
</div>
<div class="row-add-btn">
<el-button :disabled="desensitiveRuleDetail.ruleDetails?.length > 9" link @click="addSegmentRule"
:icon="CirclePlus" v-preReClick>添加分段规则</el-button>
</div>
</div>
</div>
<div v-show="drawerRef?.getDrawerConRef('drawerFormRef')?.formInline?.desensitiveRuleCode == 'CHARREPLACE'">
<div>{{ '请配置分段的替换方式' + `(${charReplaceRuleDetail.ruleDetails?.length}/10)` }}</div>
<el-radio-group v-model="charReplaceRuleDetail.replaceType">
<el-radio :value="1">从左往右</el-radio>
<el-radio :value="2">从右往左</el-radio>
</el-radio-group>
<div class="seg-main">
<div class="row-per" v-for="(item, index) in charReplaceRuleDetail.ruleDetails">
<el-select v-model="item.digitType" :style="{ width: item.digitType == 2 ? '220px' : '130px' }">
<el-option v-for="item in digitTypeList" :label="item.label" :value="item.value"
:key="item.value"></el-option>
</el-select>
<el-input v-show="item.digitType != 2" style="width:86px;margin-left: 4px;" v-model="item.digit"
:maxlength="6" :min="1" @input="(val) => inputEventDigitChange(val, item)" placeholder="请输入"></el-input>
<el-select v-model="item.ruleType"
:style="{ width: item.ruleType == 1 ? '244px' : '130px', 'margin-left': '4px' }">
<el-option v-for="item in charReplaceRuleTypeList" :label="item.label" :value="item.value"
:key="item.value"></el-option>
</el-select>
<el-input v-model="item.fixedValue" v-show="item.ruleType == 2" style="width:110px;margin-left: 4px;"
:maxlength="50" placeholder="请输入"></el-input>
<div class="title_tool" @click="deleteCharReplaceSegmentRule(item, index)">
<el-icon :size="20" color="#b2b2b2">
<Delete />
</el-icon>
</div>
</div>
<div class="row-add-btn">
<el-button :disabled="charReplaceRuleDetail.ruleDetails?.length > 9" link
@click="addCharReplaceSegmentRule" :icon="CirclePlus" v-preReClick>添加分段规则</el-button>
</div>
</div>
</div>
<div v-show="drawerRef?.getDrawerConRef('drawerFormRef')?.formInline?.desensitiveRuleCode == 'RANGEREPLACE'">
<div class="mb8">{{ '请配置区间替换规则' + `(${rangeReplaceRuleDetails?.length}/10)` }}</div>
<div class="seg-main">
<div class="row-per" v-for="(item, index) in rangeReplaceRuleDetails">
<el-input style="width:16.5%" v-model="item.lowValue" :maxlength="6"
@input="(val) => inputEventDigitChange(val, item, 'lowValue')" placeholder="请输入"></el-input>
<el-select v-model="item.lowOperator" style="width: 16.5%;margin-left: 4px;">
<el-option v-for="item in lowerOperatorList" :label="item.label" :value="item.value"
:key="item.value"></el-option>
</el-select>
<el-input style="width: 16.5%;margin-left: 4px;" :disabled="true" v-model="item.fieldChName"></el-input>
<el-select v-model="item.upperOperator" style="width: 16.5%;margin-left: 4px;">
<el-option v-for="item in lowerOperatorList" :label="item.label" :value="item.value"
:key="item.value"></el-option>
</el-select>
<el-input style="width:16.5%;margin-left: 4px;" v-model="item.upperValue" :maxlength="6"
@input="(val) => inputEventDigitChange(val, item, 'upperValue')" placeholder="请输入"></el-input>
<el-input v-model="item.replaceValue" style="width:16.5%;margin-left: 4px;" :maxlength="50"
placeholder="替换值"></el-input>
<div class="title_tool" @click="deleteRangeReplaceSegmentRule(item, index)">
<el-icon :size="20" color="#b2b2b2">
<Delete />
</el-icon>
</div>
</div>
<div class="row-add-btn">
<el-button :disabled="charReplaceRuleDetail.ruleDetails?.length > 9" link
@click="addRangeReplaceSegmentRule" :icon="CirclePlus" v-preReClick>添加分段规则</el-button>
</div>
</div>
</div>
</template>
</Drawer>
</div>
</template>
<style lang="scss" scoped>
.row-add-btn {
.el-button--default {
padding: 4px 0px;
}
:deep(.el-icon) {
width: 16px;
height: 16px;
svg {
width: 16px;
height: 16px;
}
}
}
.seg-main {
.row-add-btn {
margin-top: -6px;
}
}
.row-per {
display: flex;
align-items: center;
margin-bottom: 8px;
position: relative;
.title_tool {
margin-left: 4px;
// position: absolute;
// right: 4px;
cursor: pointer;
:deep(.el-icon) {
--color: #FB2323 !important;
svg {
width: 16px;
height: 16px;
}
}
}
}
:deep(.mt8.drawer_panel) {
margin-top: 8px;
}
.mb8 {
margin-bottom: 8px;
}
:deep(.el-form-item.ka-checkbox-input) {
.el-input {
min-width: 50px !important;
}
}
:deep(.el-form) {
.lmt12 {
// margin-top: -4px; 验证信息会被遮挡
.input_panel {
flex: 0 !important;
}
}
}
</style>
\ No newline at end of file
<template>
<div 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">
<div class="text">去标识化效果评估结果</div>
<span class="number score-color">{{ analysisResultInfo.rating + '级' }}</span>
</div>
<div class="border-content">
<span class="text">重标识风险最大值<el-tooltip placement="top" effect="light" popper-class="table_tooltip">
<template #content>
<div style="max-width: 236px;">
所有等价类的重标识风险最大值
</div>
</template>
<el-icon style="margin-left: 2px;margin-top: 2px;">
<QuestionFilled />
</el-icon>
</el-tooltip></span>
<span class="number">{{ analysisResultInfo.reIdentifyRiskRb != null ?
(analysisResultInfo.reIdentifyRiskRb || 0) : '--'
}}</span>
</div>
<div class="border-content">
<span class="text">重标识风险平均值<el-tooltip placement="top" effect="light" popper-class="table_tooltip">
<template #content>
<div style="max-width: 236px;">
所有等价类的重标识风险平均值
</div>
</template>
<el-icon style="margin-left: 2px;margin-top: 2px;">
<QuestionFilled />
</el-icon>
</el-tooltip></span>
<span class="number">{{ analysisResultInfo.reIdentifyRiskRc != null ?
(analysisResultInfo.reIdentifyRiskRc || 0) : '--' }}</span>
</div>
<div class="border-content">
<span class="text">环境重标识攻击概率<el-tooltip placement="top" effect="light" popper-class="table_tooltip">
<template #content>
<div style="max-width: 236px;">
完全公开共享数据发布,攻击者对数据集进行环境重标识攻击的概率为1。领地公开共享与受控公开共享数据发布,环境重标识攻击概率为内部故意攻击概率、数据集包含熟人概率和数据泄露概率三者的最大值。
</div>
</template>
<el-icon style="margin-left: 2px;margin-top: 2px;">
<QuestionFilled />
</el-icon>
</el-tooltip></span>
<span class="number">{{ analysisResultInfo.envReAttackPr != null ?
(analysisResultInfo.envReAttackPr || 0) : '--' }}</span>
</div>
<div class="border-content">
<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/3;等价类门限风险为:等价类的重标识风险大于门限阈值为1,小于等于为0,求和后除以等价类个数。
</div>
</template>
<el-icon style="margin-left: 2px;margin-top: 2px;">
<QuestionFilled />
</el-icon>
</el-tooltip></span>
<span class="number">{{ analysisResultInfo.reIdentifyRiskRa != null ?
(analysisResultInfo.reIdentifyRiskRa || 0) : '--' }}</span>
</div>
<div class="border-content">
<span class="text">重标识风险总体风险<el-tooltip placement="top" effect="light" popper-class="table_tooltip">
<template #content>
<div style="max-width: 236px;">
完全公开共享,当等价类门限风险=0时,重标识风险总体风险公式为等价类重标识风险最大值*环境重标识攻击概率;当等价类门限风险!=0时,重标识风险总体风险为1。
受控公开共享和领地公开共享,当等价类门限风险=0时,重标识风险总体风险公式为等价类重标识风险平均值*环境重标识攻击概率;当等价类门限风险!=0时,重标识风险总体风险为1。
<!-- {{ oldAnonTaskValueInfo.dataSharingTypeCode == '01' ? '完全公开共享,当等价类门限风险=0时,重标识风险总体风险公式为等价类重标识风险最大值*环境重标识攻击概率;当等价类门限风险!=0时,重标识风险总体风险为1。'
: `${oldAnonTaskValueInfo.dataSharingTypeCode == '02' ? '受控公开共享' : '领地公开共享'},当等价类门限风险=0时,重标识风险总体风险公式为等价类重标识风险平均值*环境重标识攻击概率;当等价类门限风险!=0时,重标识风险总体风险为1。` }} -->
</div>
</template>
<el-icon style="margin-left: 2px;margin-top: 2px;">
<QuestionFilled />
</el-icon>
</el-tooltip></span>
<span class="number">{{ analysisResultInfo.reIdentifyOverallRisk != null ?
(analysisResultInfo.reIdentifyOverallRisk || 0) : '--' }}</span>
</div>
<div class="border-content">
<span class="text">重标识可接受风险阈值</span>
<span class="number">{{ oldAnonTaskValueInfo.anonPrivacyMode?.riskThreshold == null ? 0.05 :
(oldAnonTaskValueInfo.anonPrivacyMode?.riskThreshold || 0) }}</span>
</div>
</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' }">
<el-table-column label="等价类" type="index" width="68px" align="center" show-overflow-tooltip>
<template #default="scope">
<span>{{
pageInfo.curr !== undefined
? (pageInfo.curr - 1) * pageInfo.limit + scope.$index + 1
: scope.$index + 1
}}</span>
</template>
</el-table-column>
<template v-for="(item, index) in (analysisResultTableFields || [])">
<el-table-column :label="item.chName" :width="item.dataType === 'datetime'
? TableColumnWidth.DATETIME
: item.dataType === 'date'
? TableColumnWidth.DATE
: originResultTableFieldColumn[item.enName]
" :align="getTextAlign(item)" :header-align="getTextAlign(item)"
:formatter="(row) => formatterPreviewDate(row, item)" :show-overflow-tooltip="true">
</el-table-column>
</template>
<el-table-column label="等价类大小" prop="equivalenceClassNum" width="98" align="right" fixed="right"
show-overflow-tooltip></el-table-column>
<el-table-column label="重标识风险" prop="reIdentifyRisk" width="96" align="right" fixed="right"
show-overflow-tooltip></el-table-column>
<el-table-column label="判断重风险是否大于门限阈值" prop="isGtThreshold" width="130" align="left" fixed="right"
show-overflow-tooltip></el-table-column>
</el-table>
<div v-show="!analysisResultTableFields.length" class="empty-content">
<img src="../../../assets/images/empty-data.png" :style="{ width: '168px', height: '96px' }" />
<div class="empty-text">暂无数据</div>
</div>
<div v-show="analysisResultTableFields.length" class="result-table-desc">门限阈值的取值:完全公开共享数据发布,取值
1/20;受控公开共享数据发布,取值
1/5;领地公开共享数据发布,取值 1/3</div>
<PageNav v-show="analysisResultTableFields.length" :class="[pageInfo.type]" :pageInfo="pageInfo"
@pageChange="pageChange" />
<div class="row-two-main" style="margin-top: 12px;">
<div class="table-one">
<div class="result-title">对抗性测试关键变量</div>
<el-table ref="varTableRef" :data="analysisResultInfo?.adversarialTest || []" :highlight-current-row="true"
stripe border tooltip-effect="light" height="100%" row-key="guid"
: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="uniqueScore" width="110px" align="right"
show-overflow-tooltip></el-table-column>
<el-table-column label="影响力分值" prop="influenceScore" width="110" align="right"
show-overflow-tooltip></el-table-column>
<el-table-column label="数据属性标识度分值" prop="dataAttrIdentScore" width="160" align="right"
show-overflow-tooltip></el-table-column>
</el-table>
</div>
<div class="table-two" v-show="oldAnonTaskValueInfo.dataSharingTypeCode != '01'">
<div class="result-title">内部故意攻击概率</div>
<div class="desc" style="margin-bottom: 4px;color: #999;line-height: 18px;">
重标识数据的动机和能力为低,从重标识攻击可能性分析表可得出在内部攻击方面,重标识攻击概率的取值为0.05。</div>
<el-table ref="innerTableRef" :data="innerResultData" :highlight-current-row="true" stripe border
tooltip-effect="light" height="100%" row-key="guid" :style="{ height: '356px' }"
:span-method="arrayInnerSpanMethod" :cell-class-name="handleInnerCellClass">
<!-- <el-table-column label="序号" type="index" width="56px" align="center" show-overflow-tooltip>
</el-table-column> -->
<el-table-column label="风险减缓控制水平" prop="level" width="150px" align="left"
show-overflow-tooltip></el-table-column>
<el-table-column label="动机和能力" prop="competencyLevel" width="140px" align="left"
show-overflow-tooltip></el-table-column>
<el-table-column label="重标识攻击概率" prop="value" width="140" align="right" show-overflow-tooltip>
<template #default="scope">
<span>{{ scope.row.value }}</span>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div class="row-two-main" v-show="oldAnonTaskValueInfo.dataSharingTypeCode != '01'">
<div class="table-one border">
<div class="result-title">数据集包含熟人概率</div>
<div class="result-title-h1">pr=1-1-p^m</div>
<div class="result-title-desc">
{{ ` 数据集包含熟人概率可通过以上公式计算,${new
Date().getFullYear()}年我国最新的人口统计为${analysisResultInfo?.allPerson ||
0}亿人,其中该数据集的容量为${analysisResultInfo?.dataSetNum || 0}万,
占总人口的比例为${analysisResultInfo.patientPopulationRate ||
0}%,m值取推荐值150,数据集包含熟人的概率为${analysisResultInfo.randomAcquaintancePr || 0}。` }}
</div>
</div>
<div class="table-two border">
<div class="result-title">数据泄露概率</div>
<div class="result-title-desc">
<div>对于安全和隐私控制能力评估为低的情况,推荐将数据泄露概率设定为0.55</div>
<div>对于安全和隐私控制能力评估为中的情况,推荐将数据泄露概率设定为0.27</div>
<div>对于安全和隐私控制能力评估为高的情况,推荐将数据泄露概率设定为 0.14</div>
<div>{{ `数据接收方的安全和隐私控制能力为高,按照推荐值将数据泄露概率设定为${analysisResultInfo.dataBreachPr || 0}。` }}</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup name="anonResultAnalysis">
import { TableColumnWidth } from '@/utils/enum';
import Moment from 'moment';
defineProps({
showTitle: {
type: Boolean,
default: false,
},
analysisResultInfo: {
type: Object,
default: {},
},
pageInfo: {
type: Object,
default: {},
},
analysisResultLoading: {
type: Boolean,
default: false,
},
oldAnonTaskValueInfo: {
type: Object,
default: {},
},
resultData: {
type: Array,
default: []
},
analysisResultTableFields: {
type: Array,
default: [],
},
originResultTableFieldColumn: {
type: Object,
default: {},
},
containerWidth: {
type: Number,
default: 1200,
},
})
const emits = defineEmits([
"pageChange"
]);
const getTextAlign = (field) => {
if (field.dataType === 'decimal' || field.dataType === 'int') {
return 'right';
}
return 'left'
}
const formatterPreviewDate = (row, info) => {
let enName = info.enName;
let v = row[enName];
if (v === 0) {
return v;
}
if (!v || v == 'null') {
return '--';
}
if (info.dataType === 'datetime') {
return Moment(v).format('YYYY-MM-DD HH:mm:ss');
}
if (info.dataType === 'date') {
if (isNaN(<any>(new Date(v)))) {
return Moment(parseInt(v)).format('YYYY-MM-DD');
} else {
return Moment(v).format('YYYY-MM-DD');
}
}
return v;
};
/** 内部故意攻击概率的表格 */
const innerResultData = ref([{
guid: '1',
level: '高',
competencyLevel: '低',
value: '0.05'
}, {
guid: '2',
level: '高',
competencyLevel: '中',
tooltip: true,
value: '0.1'
}, {
guid: '3',
level: '高',
competencyLevel: '高',
tooltip: true,
value: '0.2'
}, {
guid: '4',
level: '中',
competencyLevel: '低',
value: '0.2'
}, {
guid: '5',
level: '中',
competencyLevel: '中',
value: '0.3'
}, {
guid: '6',
level: '中',
competencyLevel: '高',
value: '0.4'
}, {
guid: '7',
level: '低',
competencyLevel: '低',
value: '0.4'
}, {
guid: '8',
level: '低',
competencyLevel: '中',
value: '0.5'
}, {
guid: '9',
level: '低',
competencyLevel: '高',
value: '0.6'
}]);
const arrayInnerSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
if (columnIndex > 0) {
return [1, 1];
}
// 查找当前字段值相同的连续行
let startRow = rowIndex;
let endRow = rowIndex;
let field = 'level';
// 向前查找
while (startRow > 0 && innerResultData.value[startRow - 1][field] === innerResultData.value[rowIndex][field]) {
startRow--;
}
// 向后查找
while (endRow < innerResultData.value.length - 1 && innerResultData.value[endRow + 1][field] === innerResultData.value[rowIndex][field]) {
endRow++;
}
// 如果当前行不是相同值组的起始行,则不显示
if (startRow !== rowIndex) {
return [0, 0];
}
// 返回合并的行数
const rowspan = endRow - startRow + 1;
return [rowspan, 1];
}
const handleInnerCellClass = ({ row, column, rowIndex, columnIndex }) => {
if (rowIndex == 0 && columnIndex > 0) {
return 'cell-tooltip-bg';
}
return '';
}
const pageChange = (info) => {
emits('pageChange', info);
}
</script>
<style lang="scss" scoped>
.analysis-result-main {
min-height: 250px;
.value-desc {
font-size: 14px;
color: #212121;
line-height: 21px;
}
.result-title {
font-size: 16px;
color: #212121;
line-height: 24px;
font-weight: 600;
margin-bottom: 6px;
}
.result-title-h1 {
color: #212121;
font-weight: 600;
font-size: 24px;
text-align: center;
line-height: 36px;
margin-top: 12px;
}
.result-title-desc {
color: #666;
font-size: 14px;
line-height: 21px;
margin-top: 12px;
}
.kpi-content {
display: flex;
flex-direction: row;
column-gap: 12px;
row-gap: 12px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.border-content {
height: 76px;
display: flex;
flex-direction: column;
align-items: left;
padding-left: 16px;
justify-content: center;
border: 1px solid #d9d9d9;
width: calc(20% - 8px);
min-width: 228px;
border-radius: 2px;
padding-left: 16px;
.number {
font-weight: 700;
font-size: 20px;
color: #212121;
line-height: 30px;
margin-top: 2px;
&.score-color {
color: #FF5F1F;
}
}
.text {
font-size: 14px;
line-height: 21px;
color: #666666;
display: flex;
.el-icon {
color: #b2b2b2;
}
}
}
.result-table-desc {
font-size: 14px;
color: #999999;
line-height: 21px;
}
.row-two-main {
margin-top: 18px;
display: flex;
.table-one {
width: 586px;
&.border {
border: 1px solid #d9d9d9;
padding: 14px 18px 18px;
}
}
.table-two {
margin-left: 20px;
width: calc(100% - 606px);
&.border {
border: 1px solid #d9d9d9;
padding: 14px 18px 18px;
}
}
}
}
.empty-content {
display: flex;
align-items: center;
justify-content: center;
height: 316px;
width: 100%;
flex-direction: column;
.empty-text {
font-size: 14px;
color: #b2b2b2;
}
}
</style>
\ No newline at end of file
<route lang="yaml">
name: labelManagement
</route>
<script lang="ts" setup name="labelManagement">
import { commonPageConfig } from '@/components/PageNav/index';
import { TableColumnWidth } from '@/utils/enum';
import TableTools from "@/components/Tools/table_tools.vue";
import {
getDataLabelList,
updateLabelState,
saveLabel,
updateLabel,
deleteLabel,
getLabelDetail,
getParamsList,
} from '@/api/modules/dataAnonymization';
import { useValidator } from '@/hooks/useValidator';
import { Delete, CirclePlus, Warning } from "@element-plus/icons-vue";
const { required, regexpRuleValidate } = useValidator();
const { proxy } = getCurrentInstance() as any;
/** 标签类型字典列表 */
const labelTypeList: any = ref([]);
/** 内置规则字典列表 */
const builtInRuleList: any = ref([]);
const searchItemList = ref([{
type: "input",
label: "",
field: "labelName",
default: "",
placeholder: "标签名称",
clearable: true,
}])
/** 分页及搜索传参信息配置。 */
const page = ref({
...commonPageConfig,
labelName: '',
});
const currTableData: any = ref();
const tableInfo = ref({
id: 'data-label-table',
// multiple:true,
fields: [
{ label: "序号", type: "index", width: TableColumnWidth.INDEX, align: "center" },
{ label: "标签名称", field: "labelName", width: 140 },
{ label: "标签类型", field: "labelTypeName", width: 140 },
{ label: '状态', field: 'bizState', type: 'switch', activeText: '启用', inactiveText: '禁用', activeValue: 'Y', inactiveValue: 'N', switchWidth: 56, width: 100, align: 'center' },
{ label: "界面排序", field: "orderNum", width: 90, align: "center" },
{ label: "修改人", field: "updateUserName", width: TableColumnWidth.USERNAME },
{ label: "修改时间", field: "updateTime", width: TableColumnWidth.DATETIME },
],
data: [],
page: {
type: "normal",
rows: 0,
...page.value,
},
actionInfo: {
label: "操作",
type: "btn",
width: 120,
fixed: 'right',
btns: (scope) => {
let btnsArr: any = [];
btnsArr.push({
label: "编辑", value: "edit", click: (scope) => {
currTableData.value = scope.row;
getLabelDetail(scope.row.guid).then((res: any) => {
if (res?.code == proxy.$passCode) {
const detail = res.data || {};
currTableData.value = Object.assign({}, currTableData.value, detail);
newCreateLabelFormItems.value.forEach(item => {
if (item.children?.length) {
item.children.forEach(child => child.default = detail[child.field]);
} else {
item.default = detail[item.field];
}
item.default = detail[item.field];
});
let labelRuleField = detail.labelRuleField || {};
matchChValue.value.value = labelRuleField.matchChValue;
matchChValue.value.disabled = false;
matchEnValue.value.value = labelRuleField.matchEnValue;
matchEnValue.value.disabled = false;
formRows.value = labelRuleField.vagueMatchRule || [{ matchValue: '', position: '', name: '', disabled: false }];
tabsInfo.value.activeName = 'labelRuleField';
let labelRuleContent = detail.labelRuleContent || {};
ruleContentFormItems.value[1].visible = labelRuleContent.ruleType == 1;
ruleContentFormItems.value[2].visible = labelRuleContent.ruleType == 2;
ruleContentFormItems.value.forEach(item => {
item.default = labelRuleContent[item.field];
if (item.field == 'regularTestData') {
item.validateMsg = null;
}
});
newCreateLabelDialogInfo.value.title = '编辑标签';
newCreateLabelDialogInfo.value.visible = true;
newCreateLabelDialogInfo.value.type = 'update';
newCreateLabelDialogInfo.value.submitBtnLoading = false;
} else {
proxy.$ElMessage({
type: 'error',
message: res.msg,
})
}
});
}
})
btnsArr.push({
label: "删除", value: "delete", disabled: scope.row.bizState == 'Y', click: (scope) => {
proxy.$openMessageBox("此操作将永久删除, 是否继续?", () => {
let guids = [scope.row.guid];
deleteLabel(guids).then((res: any) => {
if (res?.code == proxy.$passCode) {
getTableData();
proxy.$ElMessage({
type: "success",
message: "删除成功",
});
} else {
proxy.$ElMessage({
type: "error",
message: res.msg,
});
}
});
})
}
});
return btnsArr
},
},
loading: false
})
const toSearch = (val: any, clear: boolean = false) => {
if (clear) {
searchItemList.value.map((item) => (item.default = ""));
page.value.labelName = '';
} else {
page.value.labelName = val.labelName;
}
getTableData();
};
const getTableData = () => {
tableInfo.value.loading = true
getDataLabelList({
pageIndex: page.value.curr,
pageSize: page.value.limit,
labelName: page.value.labelName
}).then((res: any) => {
if (res?.code == proxy.$passCode) {
const data = res.data || {}
tableInfo.value.data = data.records || []
tableInfo.value.page.limit = data.pageSize
tableInfo.value.page.curr = data.pageIndex
tableInfo.value.page.rows = data.totalRows
} else {
proxy.$ElMessage({
type: 'error',
message: res.msg,
})
}
tableInfo.value.loading = false
})
};
const tableSwitchBeforeChange = (scope, field, callback) => {
const msg = `确定【${scope.row[field] == 'Y' ? '禁用' : '启用'}${scope.row.labelName}?`
proxy.$openMessageBox(msg, () => {
const state = scope.row[field] == 'Y' ? 'N' : 'Y'
const result = tableSwitchChange(state, scope, field)
callback(result)
}, () => {
callback(false)
});
}
const tableSwitchChange = (val, scope, field) => {
return new Promise((resolve, reject) => {
let params = {
guid: scope.row.guid,
bizState: val
}
updateLabelState(params).then((res: any) => {
if (res?.code == proxy.$passCode && res.data) {
getTableData();
proxy.$ElMessage({
type: "success",
message: `${scope.row.labelName}${val == 'Y' ? '启用' : '禁用'}】成功`,
});
resolve(true)
} else {
proxy.$ElMessage({
type: "error",
message: res.msg,
});
reject(false)
}
}).catch(() => {
reject(false)
})
})
}
const tablePageChange = (info) => {
page.value.curr = Number(info.curr);
page.value.limit = Number(info.limit);
getTableData();
};
const newCreateLabelFormItems = ref<any>([{
label: '标签名称',
type: 'input',
placeholder: '请输入',
field: 'labelName',
default: '',
required: true,
filterable: true,
clearable: true,
visible: true,
},
{
label: ' ',
type: 'select-group',
placeholder: '请选择',
field: 'labelgroup',
children: [{
label: '标签类型',
type: 'select',
placeholder: '请选择',
field: 'labelTypeCode',
default: '',
options: labelTypeList.value,
props: {
label: "label",
value: "value",
},
required: true,
filterable: true,
clearable: true,
visible: true,
}, {
label: '界面排序',
type: 'input',
placeholder: '请输入',
field: 'orderNum',
maxlength: 6,
regexp: /\D/g,
required: false,
clearable: true,
}],
}, {
type: 'radio-group',
label: '规则配置',
field: 'matchType',
default: 1,
required: false,
block: true,
options: [
{ label: '字段识别和内容识别满足其一即可', value: 1 },
{ label: '字段识别和内容识别需满足全部', value: 2 },
],
}]);
const newCreateLabelFormRules = ref({
labelName: [required('请输入标签名称')],
labelTypeCode: [required('请选择标签类型')]
});
const ruleContentFormRef = ref();
const newCreateLabelDialogInfo = ref({
visible: false,
size: 600,
title: "添加标签",
type: "",
formInfo: {
id: "label-form",
items: newCreateLabelFormItems.value,
rules: newCreateLabelFormRules.value,
},
submitBtnLoading: false,
btns: {
cancel: () => {
newCreateLabelDialogInfo.value.visible = false;
newCreateLabelDialogInfo.value.submitBtnLoading = false;
},
submit: (btn, info) => {
// 需要验证两个
let validateRuleField = (showMsg = true) => {
if (!matchChValue.value.value && !matchEnValue.value.value && (!formRows.value.length || (formRows.value.length == 1 && !formRows.value[0].matchValue &&
!formRows.value[0].position && !formRows.value[0].name))) {
showMsg && proxy.$ElMessage.error('字段识别匹配规则不能为空');
return false;
}
for (let i = 0; i < formRows.value.length; i++) {
const row = formRows.value[i];
// 如果某一条数据的 matchValue, position, name 都为空,则跳过,不校验
if (!row.matchValue && !row.position && !row.name) {
continue; // 如果全为空,跳过这一行的校验
}
if (!row.matchValue || !row.position || !row.name) {
showMsg && proxy.$ElMessage.error('请填写完整的模糊匹配规则');
return false;
}
}
return true;
}
let submitLabel = () => {
let params = Object.assign({}, info, {
bizState: 'Y',
orderNum: info.orderNum != null ? parseInt(info.orderNum) : null,
labelRuleField: {
matchChValue: matchChValue.value.value,
matchEnValue: matchEnValue.value.value,
vagueMatchRule: formRows.value?.filter(f => f.matchValue!= '' || f.position!= '' || f.name != '')
},
labelRuleContent: ruleContentFormRef.value.formInline
});
newCreateLabelDialogInfo.value.submitBtnLoading = true;
if (newCreateLabelDialogInfo.value.type == 'add') {
saveLabel(params).then((res: any) => {
if (res?.code == proxy.$passCode) {
proxy.$ElMessage.success('标签新建成功');
newCreateLabelDialogInfo.value.visible = false;
newCreateLabelDialogInfo.value.submitBtnLoading = false;
page.value.curr = 1;
getTableData();
} else {
newCreateLabelDialogInfo.value.submitBtnLoading = false;
proxy.$ElMessage.error(res.msg);
}
}).catch(() => {
newCreateLabelDialogInfo.value.submitBtnLoading = false;
});
} else {
newCreateLabelDialogInfo.value.submitBtnLoading = true;
params.guid = currTableData.value.guid;
params.labelRuleContent.guid = currTableData.value.labelRuleContent?.guid;
params.labelRuleField.guid = currTableData.value.labelRuleField?.guid;
updateLabel(params).then((res: any) => {
if (res?.code == proxy.$passCode) {
proxy.$ElMessage.success('标签编辑成功');
newCreateLabelDialogInfo.value.visible = false;
newCreateLabelDialogInfo.value.submitBtnLoading = false;
getTableData();
} else {
newCreateLabelDialogInfo.value.submitBtnLoading = false;
proxy.$ElMessage.error(res.msg);
}
}).catch(() => {
newCreateLabelDialogInfo.value.submitBtnLoading = false;
});
}
}
if (info.matchType == 2) {
if (tabsInfo.value.activeName == 'labelRuleField') {
if (!validateRuleField()) {
return;
}
ruleContentFormRef.value?.ruleFormRef?.validate((valid, errorItem) => {
if (valid) {
submitLabel();
} else {
tabsInfo.value.activeName = 'labelRuleContent';
}
})
} else {
ruleContentFormRef.value?.ruleFormRef?.validate((valid, errorItem) => {
if (valid) {
if (!validateRuleField()) {
tabsInfo.value.activeName = 'labelRuleField';
return;
}
submitLabel();
}
})
}
} else { //只需匹配一项即可。
if (tabsInfo.value.activeName == 'labelRuleField') { //先验证字段识别
if (!matchChValue.value.value && !matchEnValue.value.value && (!formRows.value.length || (formRows.value.length == 1 && (!formRows.value[0].matchValue &&
!formRows.value[0].position && !formRows.value[0].name)))) { // 没有配置字段识别内容
ruleContentFormRef.value?.ruleFormRef?.validate((valid, errorItem) => {
if (valid) {
submitLabel();
} else {
proxy.$ElMessage.error('字段识别和内容识别不能同时为空');
return;
}
})
} else {
ruleContentFormRef.value?.ruleFormRef?.validate((valid, errorItem) => {
if (valid) {
submitLabel(); //只需要满足一个即可。
} else {
ruleContentFormRef.value?.ruleFormRef?.clearValidate(['builtInRuleCode', 'regularExpression', 'hitRate']);// 先清除验证
// 去 验证 字段识别
if (!validateRuleField(true)) {
return;
}
submitLabel();
}
})
}
} else {
ruleContentFormRef.value?.ruleFormRef?.validate((valid, errorItem) => {
if (valid) {
submitLabel(); //只需要满足一个即可。
} else {
if (!validateRuleField(false)) {
return;
}
submitLabel();
}
})
}
}
}
}
});
const formRows = ref([
{ matchValue: '', position: '', name: '', disabled: false }, // 初始行
]);
// 位置选项
const positionOptions = [
{ label: '前面', value: 'B' },
{ label: '后面', value: 'A' },
{ label: '任意位置', value: 'C' },
];
// 语言options
const languageOptions = [
{ label: '中文名', value: 'chName' },
{ label: '英文名', value: 'enName' },
];
// 新增行
const addRow = () => {
formRows.value.push({ matchValue: '', position: '', name: '', disabled: false });
};
// 删除行
const deleteRow = (index: number) => {
formRows.value.splice(index, 1);
};
const matchChValue = ref({
value: '',
disabled: false
})
const matchEnValue = ref({
value: '',
disabled: false
})
const tabsInfo = ref({
activeName: 'labelRuleField',
tabs: [
{ label: '字段识别', name: 'labelRuleField' },
{ label: '内容识别', name: 'labelRuleContent' },
]
});
const tabChange = (val) => {
tabsInfo.value.activeName = val;
}
const handleCreate = () => {
newCreateLabelFormItems.value.forEach(item => {
if (item.children?.length) {
item.children[0].default = ''
item.children[1].default = ''
} else {
if (item.field == 'matchType') {
item.default = 1;
} else {
item.default = '';
}
}
});
matchChValue.value.value = '';
matchChValue.value.disabled = false;
matchEnValue.value.value = '';
matchEnValue.value.disabled = false;
formRows.value = [{ matchValue: '', position: '', name: '', disabled: false }];
tabsInfo.value.activeName = 'labelRuleField';
ruleContentFormItems.value[1].visible = true;
ruleContentFormItems.value[2].visible = false;
ruleContentFormItems.value.forEach(item => {
item.default = item.field == 'ruleType' ? 1 : '';
if (item.field == 'regularTestData') {
item.validateMsg = null;
}
});
newCreateLabelDialogInfo.value.title = '添加标签';
newCreateLabelDialogInfo.value.visible = true;
newCreateLabelDialogInfo.value.type = 'add';
newCreateLabelDialogInfo.value.submitBtnLoading = false;
}
/** 规则配置的内容识别内容表单配置 */
const ruleContentFormItems = ref([{
label: '规则类型',
type: 'select',
placeholder: '请选择',
field: 'ruleType',
default: 1,
options: [{
value: 1,
label: '内置规则'
}, {
value: 2,
label: '自定义规则'
}],
props: {
label: "label",
value: "value",
},
required: true,
filterable: true,
clearable: false,
block: true,
visible: true,
}, {
label: '内置规则',
type: 'select',
placeholder: '请选择',
field: 'builtInRuleCode',
default: '',
options: builtInRuleList.value,
props: {
label: "label",
value: "value",
},
required: true,
filterable: true,
clearable: true,
block: true,
visible: true,
}, {
label: '规则表达式',
type: 'input',
placeholder: '请输入正则表达式',
field: 'regularExpression',
default: '',
required: true,
filterable: true,
clearable: true,
block: true,
visible: false,
}, {
label: '测试验证',
type: 'input',
placeholder: '请输入测试数据',
field: 'regularTestData',
default: '',
required: false,
clearable: true,
col: 'mb8',
block: true,
visible: true,
validateBtn: {
value: 'validate',
label: '验证',
click: () => {
let info = ruleContentFormRef.value.formInline;
if (info.ruleType == 1 && !info.builtInRuleCode) {
proxy.$ElMessage.error('请先选择内置规则');
return;
}
if (info.ruleType == 2 && !info.regularExpression) {
proxy.$ElMessage.error('请先输入规则表达式');
return;
}
if (!info.regularTestData) {
proxy.$ElMessage.error('请先输入测试数据');
return;
}
let exp = info.ruleType == 1 ? builtInRuleList.value.find(b => b.value == info.builtInRuleCode)?.remarks : info.regularExpression;
let result = exp && new RegExp(exp).test(info.regularTestData);
ruleContentFormItems.value.forEach(item => {
item.default = info[item.field];
if (item.field == 'regularTestData') {
item.validateMsg = result ? {
status: 'success',
msg: '符合识别规则'
} : {
status: 'error',
msg: '不符合识别规则'
}
}
});
}
},
validateMsg: <any>null
}, {
label: '命中率设置(%)',
type: 'input',
placeholder: '请输入1~100',
field: 'hitRate',
min: 1,
max: 100,
inputType: 'integerNumber',
default: null,
required: true,
block: true,
clearable: true,
visible: true,
width: '100px',
beforeMsg: '一列数据中的非空数据,大于等于',
afterMsg: ' %的数据符合以上识别条件,则认为命中该识别规则。'
}]);
/** 规则配置的内容识别内容表单规则配置 */
const ruleContentFormRules = ref({
builtInRuleCode: [required('请选择内置规则')],
regularExpression: [required('请输入正则表达式'), regexpRuleValidate()],
hitRate: [required(' ')]
});
const handleRuleContentSelectChange = (val, row, info) => {
if (row.field == 'ruleType') {
ruleContentFormItems.value[1].visible = val == 1;
ruleContentFormItems.value[2].visible = val == 2;
ruleContentFormItems.value.forEach(item => {
item.default = info[item.field];
})
}
}
const handleRuleContentInputChange = (val, row) => {
if (row.field == 'regularTestData') {
let item = ruleContentFormItems.value.at(-2);
item && (item.validateMsg = null);
}
}
onBeforeMount(() => {
toSearch({});
getParamsList({
dictType: "标签类型",
}).then((res: any) => {
if (res?.code == proxy.$passCode) {
labelTypeList.value = res.data || [];
let item = newCreateLabelFormItems.value.find(item => item.field == 'labelgroup');
item && (item.children[0].options = labelTypeList.value);
} else {
proxy.$ElMessage.error(res.msg);
}
});
getParamsList({
dictType: "内置规则",
}).then((res: any) => {
if (res?.code == proxy.$passCode) {
builtInRuleList.value = res.data || [];
ruleContentFormItems.value[1].options = builtInRuleList.value;
} else {
proxy.$ElMessage.error(res.msg);
}
})
})
</script>
<template>
<div class="container_wrap">
<div class="table_tool_wrap">
<!-- 头部搜索 -->
<TableTools :searchItems="searchItemList" :searchId="'data-label-search'" @search="toSearch" :init="false" />
<div class="tools_btns">
<el-button type="primary" @click="handleCreate">新建</el-button>
</div>
</div>
<div class="table_panel_wrap">
<!-- 右侧标签管理表格 -->
<Table :tableInfo="tableInfo" @tablePageChange="tablePageChange"
@tableSwitchBeforeChange="tableSwitchBeforeChange" />
</div>
<!-- 新建编辑标签对话框 -->
<Dialog_form ref="dialogLabelFormRef" :dialogConfigInfo="newCreateLabelDialogInfo" class="v-dialog-form">
<template v-slot:default>
<Tabs :tabs-info="tabsInfo" @tab-change="tabChange" style="margin-top: -20px;" />
<div v-show="tabsInfo.activeName == 'labelRuleField'">
<div>
<div class="dim-label">
<span class="front">精确匹配</span>
<el-icon>
<Warning />
</el-icon>
<span class="tip">
精确匹配使用英文","分隔每个规则
</span>
</div>
<div class="v-match">
<el-input v-model="matchChValue.value" maxlength="200" style="width: 272px;height:94px;" show-word-limit
type="textarea" class="no-resize" placeholder="请输入字段中文" />
<el-input v-model="matchEnValue.value" maxlength="200" style="width: 272px;height:94px;" show-word-limit
type="textarea" class="no-resize" placeholder="请输入字段英文" />
</div>
</div>
<div class="dim-label" style="margin-top: 16px;">
<span class="front">模糊匹配</span>
<el-icon>
<Warning />
</el-icon>
<span class="tip">
模糊匹配是或的关系,可配置多个模糊匹配规则
</span>
</div>
<!-- 渲染行 -->
<div v-for="(row, index) in formRows" :key="index" class="match-content-wrapper">
<div class="match-content">
<!-- 位置映射下拉框 -->
<el-select v-model="row.name" placeholder="请选择" class="v-select">
<el-option v-for="option in languageOptions" :key="option.value" :label="option.label"
:value="option.value" />
</el-select>
<el-select v-model="row.position" placeholder="请选择位置" class="v-select">
<el-option v-for="option in positionOptions" :key="option.value" :label="option.label"
:value="option.value" />
</el-select>
<el-input v-model="row.matchValue" class="v-input" placeholder="请输入匹配值" />
<!-- 删除按钮 -->
<el-button class="extra-icon" :icon="Delete" @click="deleteRow(index)" circle style="margin-left: 8px;" />
</div>
</div>
<!-- 新增按钮 -->
<div class="add-Icon" @click="addRow">
<el-icon class="icon-add" color="#4fa1a4" :size="30">
<CirclePlus />
</el-icon>
<span class="word-des">模糊匹配规则</span>
</div>
</div>
<div v-show="tabsInfo.activeName == 'labelRuleContent'">
<Form ref="ruleContentFormRef" :itemList="ruleContentFormItems" formId="rule-content-form"
:rules="ruleContentFormRules" @select-change="handleRuleContentSelectChange"
@inputChange="handleRuleContentInputChange" />
</div>
</template>
</Dialog_form>
</div>
</template>
<style scoped lang="scss">
.table_tool_wrap {
width: 100%;
height: 84px !important;
padding: 0 8px;
.tools_btns {
padding: 0px 0 0;
}
}
.table_panel_wrap {
width: 100%;
height: calc(100% - 84px);
padding: 0px 8px 0;
}
:deep(.v-dialog-form) {
.title-label {
font-size: 16px;
color: #212121;
line-height: 24px;
font-weight: 600;
}
.el-dialog__body {
height: 480px;
overflow: auto;
}
.dim-label {
height: 10px;
display: flex;
align-items: center;
margin-top: 6px;
.el-icon svg {
height: 16px;
width: 16px;
color: #999999;
}
.front {
margin-right: 16px;
}
.tip {
margin-left: 4px;
font-size: 12px;
color: #999999;
}
}
.match-content-wrapper {
width: 100%;
.match-content {
display: flex;
align-items: center;
margin-top: 8px;
.v-select {
margin-right: 8px;
width: 33%;
}
.v-input {
width: calc(33% - 50px);
}
.extra-icon {
transition: opacity 1s;
display: none;
}
&:hover {
.extra-icon {
display: flex;
}
}
}
}
.add-Icon {
display: flex;
align-items: center;
margin-top: 4px;
width: 50%;
margin-left: -4px;
.el-icon svg {
height: 19px;
width: 19px;
}
.word-des {
color: #4fa1a4
}
}
.v-match {
display: flex;
justify-content: space-between;
margin-top: 10px;
margin-bottom: 10px;
}
.no-resize {
height: 94px;
.el-textarea__inner {
min-height: 94px !important;
resize: none;
}
}
}
</style>
\ No newline at end of file
<route lang="yaml">
name: resultProcess
</route>
<script lang="ts" setup name="resultProcess">
import TableTools from "@/components/Tools/table_tools.vue";
import { commonPageConfig } from '@/components/PageNav/index';
import { TableColumnWidth } from "@/utils/enum";
import {
dataSourceTypeList,
getAnonTaskList,
deleteAnonTask,
} from '@/api/modules/dataAnonymization';
import { useValidator } from '@/hooks/useValidator';
import useDataAnonymizationStore from "@/store/modules/dataAnonymization";
const anonymizationStore = useDataAnonymizationStore();
const router = useRouter()
const { proxy } = getCurrentInstance() as any;
const { required } = useValidator();
const searchItemList = ref([{
type: "input",
label: "",
field: "taskName",
default: "",
placeholder: "数据集名称",
clearable: true,
}, {
type: "select",
label: "",
field: "dataSource",
default: null,
options: dataSourceTypeList,
placeholder: "数据来源",
clearable: true,
filterable: true,
}])
/** 分页及搜索传参信息配置。 */
const page = ref({
...commonPageConfig,
taskName: '',
dataSource: null
});
const tableInfo = ref({
id: 'data-file-table',
fields: [
{ label: "序号", type: "index", width: TableColumnWidth.INDEX, align: "center" },
{ label: "数据集名称", field: "taskName", width: 160 },
{
label: "数据来源", field: "dataSource", width: 100, getName: (scope) => {
return scope.row.dataSource && dataSourceTypeList.find(f => f.value == scope.row.dataSource)?.label || '--';
}
},
{ label: "任务状态", field: "sensitiveIdentifyTaskStatus", width: TableColumnWidth.STATE, align: 'center', type: "tag" },
{ label: "导出时间", field: "exportTime", width: TableColumnWidth.DATETIME },
{ label: "修改人", field: "updateUserName", width: TableColumnWidth.USERNAME },
{ label: "修改时间", field: "updateTime", width: TableColumnWidth.DATETIME },
],
data: [],
page: {
type: "normal",
rows: 0,
...page.value,
},
loading: false,
actionInfo: {
label: "操作",
type: "btn",
width: 230,
fixed: 'right',
btns: (scope) => {
return [{
label: "编辑", value: "edit", click: (scope) => {
router.push({
name: 'anonTaskCreate',
query: {
guid: scope.row.guid,
taskName: scope.row.taskName
}
});
}
}, {
label: '查看报告', value: 'report', disabled: scope.row.status != 'Y', click: (scope) => {
router.push({
name: 'anonResultReportView',
query: {
guid: scope.row.guid,
execGuid: scope.row.lastExecGuid,
taskName: scope.row.taskName
}
});
}
}, {
label: '查看数据', value: 'view', disabled: scope.row.status != 'Y' || scope.row.handleType == '02', click: (scope) => {
router.push({
name: 'anonResultView',
query: {
guid: scope.row.guid,
execGuid: scope.row.lastExecGuid,
taskName: scope.row.taskName
}
});
}
}, {
label: "删除", value: "delete", click: (scope) => {
proxy.$openMessageBox("此操作将永久删除, 是否继续?", () => {
let guids = [scope.row.guid];
deleteAnonTask(guids).then((res: any) => {
if (res?.code == proxy.$passCode) {
page.value.curr = 1;
getTableData();
proxy.$ElMessage({
type: "success",
message: "删除成功",
});
} else {
proxy.$ElMessage({
type: "error",
message: res.msg,
});
}
});
})
}
}]
}
}
})
const toSearch = (val: any, clear: boolean = false) => {
if (clear) {
searchItemList.value.map((item) => (item.default = ""));
page.value.taskName = '';
page.value.dataSource = null;
} else {
page.value.taskName = val.taskName;
page.value.dataSource = val.dataSource;
}
getTableData();
};
const getTableData = () => {
tableInfo.value.loading = true
getAnonTaskList({
pageIndex: page.value.curr,
pageSize: page.value.limit,
taskName: page.value.taskName,
dataSource: page.value.dataSource
}).then((res: any) => {
if (res?.code == proxy.$passCode) {
const data = res.data || {};
tableInfo.value.data = data.records?.map(d => {
d.sensitiveIdentifyTaskStatus = d.status;
return d;
}) || []
tableInfo.value.page.limit = data.pageSize
tableInfo.value.page.curr = data.pageIndex
tableInfo.value.page.rows = data.totalRows
} else {
proxy.$ElMessage({
type: 'error',
message: res.msg,
})
}
tableInfo.value.loading = false
})
};
const tablePageChange = (info) => {
page.value.curr = Number(info.curr);
page.value.limit = Number(info.limit);
getTableData();
};
const handleCreate = () => {
router.push({
name: 'anonTaskCreate'
});
}
onBeforeMount(() => {
toSearch({});
anonymizationStore?.setIsAnonPageRefresh?.(false);
})
onActivated(() => {
if (anonymizationStore.isAnonPageRefresh) {//如果是首次加载,则不需要调用
page.value.curr = 1;
getTableData();
anonymizationStore.setIsAnonPageRefresh(false);
}
});
</script>
<template>
<div class="container_wrap">
<div class="table_tool_wrap">
<!-- 头部搜索 -->
<TableTools :searchItems="searchItemList" :searchId="'data-label-search'" @search="toSearch" :init="false" />
<div class="tools_btns">
<el-button type="primary" @click="handleCreate">新建</el-button>
</div>
</div>
<div class="table_panel_wrap">
<!-- 右侧标签管理表格 -->
<Table :tableInfo="tableInfo" @tablePageChange="tablePageChange" />
</div>
</div>
</template>
<style lang="scss" scoped>
.table_tool_wrap {
width: 100%;
height: 84px !important;
padding: 0 8px;
.tools_btns {
padding: 0px 0 0;
}
}
.table_panel_wrap {
width: 100%;
height: calc(100% - 84px);
padding: 0px 8px 0;
}
</style>
\ No newline at end of file
<route lang="yaml">
name: sensitiveIdentify
</route>
<script lang="ts" setup name="sensitiveIdentify">
import TableTools from "@/components/Tools/table_tools.vue";
import { commonPageConfig } from '@/components/PageNav/index';
import { TableColumnWidth } from "@/utils/enum";
import {
getSensitiveDataTaskList,
dataSourceTypeList,
deleteSensitiveDataTask,
saveSensitiveDataTask,
updateSensitiveDataTask,
getDatabase,
execSensitiveDataTask,
} from '@/api/modules/dataAnonymization';
import { useValidator } from '@/hooks/useValidator';
const router = useRouter()
const { proxy } = getCurrentInstance() as any;
const { required } = useValidator();
const searchItemList = ref([{
type: "input",
label: "",
field: "taskName",
default: "",
placeholder: "任务名称",
clearable: true,
}, {
type: "select",
label: "",
field: "dataSource",
default: null,
options: dataSourceTypeList,
placeholder: "数据来源",
clearable: true,
filterable: true,
}])
/** 分页及搜索传参信息配置。 */
const page = ref({
...commonPageConfig,
taskName: '',
dataSource: null
});
const currTableData = ref();
const tableInfo = ref({
id: 'data-file-table',
fields: [
{ label: "序号", type: "index", width: TableColumnWidth.INDEX, align: "center" },
{ label: "任务名称", field: "taskName", width: 170 },
{
label: "数据来源", field: "dataSource", width: 100, getName: (scope) => {
return scope.row.dataSource && dataSourceTypeList.find(f => f.value == scope.row.dataSource)?.label || '--';
}
},
{ label: "任务状态", field: "sensitiveIdentifyTaskStatus", width: TableColumnWidth.STATE, align: 'center', type: "tag" },
{ label: "执行人", field: "execUserName", width: 100 },
{ label: "执行时间", field: "execTime", width: 170 },
{ label: "修改人", field: "updateUserName", width: 100 },
{ label: "修改时间", field: "updateTime", width: 170 },
],
data: [],
page: {
type: "normal",
rows: 0,
...page.value,
},
loading: false,
actionInfo: {
label: "操作",
type: "btn",
width: 304,
fixed: 'right',
btns: (scope) => {
return [{
label: '敏感数据查看', value: 'view', disabled: scope.row.status != 'Y', click: (scope) => {
router.push({
name: 'sensitiveIdentifyConfig',
query: {
guid: scope.row.guid,
execGuid: scope.row.execGuid,
taskName: scope.row.taskName
}
});
}
}, {
label: '手动执行', value: 'execute', disabled: scope.row.status == 'R', click: (scope) => {
execSensitiveDataTask(scope.row.guid).then((res: any) => {
if (res?.code == proxy.$passCode) {
getTableData();
proxy.$ElMessage.success('该任务手动执行提交成功');
} else {
proxy.$ElMessage.error(res.msg);
}
})
}
}, {
label: "编辑", value: "edit", disabled: scope.row.status == 'R', click: (scope) => {
let row = scope.row;
currTableData.value = row;
newCreateTaskDialogInfo.value.visible = true;
newCreateTaskDialogInfo.value.title = '编辑数据敏感识别任务';
newCreateTaskDialogInfo.value.type = 'edit';
newCreateTaskFormItems.value[0].default = row.taskName;
newCreateTaskFormItems.value[1].default = row.dataSource;
newCreateTaskFormItems.value[2].default = row.dataSourceGuid || '';
newCreateTaskFormItems.value[2].visible = row.dataSource == 1;
newCreateTaskFormItems.value[3].default = row.filePath || [];
newCreateTaskFormItems.value[3].visible = row.dataSource == 2;
}
}, {
label: "删除", value: "delete", disabled: scope.row.status == 'R', click: (scope) => {
proxy.$openMessageBox("此操作将永久删除, 是否继续?", () => {
let guids = [scope.row.guid];
deleteSensitiveDataTask(guids).then((res: any) => {
if (res?.code == proxy.$passCode) {
getTableData();
proxy.$ElMessage({
type: "success",
message: "删除成功",
});
} else {
proxy.$ElMessage({
type: "error",
message: res.msg,
});
}
});
})
}
}, {
label: '日志', value: 'log', click: (scope) => {
router.push({
name: 'sensitiveIdentifyTaskExecLog',
query: {
guid: scope.row.guid,
name: scope.row.taskName,
}
});
}
}]
}
}
})
const toSearch = (val: any, clear: boolean = false) => {
if (clear) {
searchItemList.value.map((item) => (item.default = ""));
page.value.taskName = '';
page.value.dataSource = null;
} else {
page.value.taskName = val.taskName;
page.value.dataSource = val.dataSource;
}
getTableData();
};
const getTableData = () => {
tableInfo.value.loading = true
getSensitiveDataTaskList({
pageIndex: page.value.curr,
pageSize: page.value.limit,
taskName: page.value.taskName,
dataSource: page.value.dataSource
}).then((res: any) => {
if (res?.code == proxy.$passCode) {
const data = res.data || {};
tableInfo.value.data = data.records?.map(d => {
d.sensitiveIdentifyTaskStatus = d.status;
return d;
}) || []
tableInfo.value.page.limit = data.pageSize
tableInfo.value.page.curr = data.pageIndex
tableInfo.value.page.rows = data.totalRows
} else {
proxy.$ElMessage({
type: 'error',
message: res.msg,
})
}
tableInfo.value.loading = false
})
};
const tablePageChange = (info) => {
page.value.curr = Number(info.curr);
page.value.limit = Number(info.limit);
getTableData();
};
const dataSourceList = ref([]);
const newCreateTaskFormItems = ref([{
label: '任务名称',
type: 'input',
placeholder: '请输入',
field: 'taskName',
maxlength: 15,
default: '',
required: true,
filterable: true,
clearable: true,
visible: true,
},
{
label: '数据来源',
type: 'select',
placeholder: '请选择',
field: 'dataSource',
default: 1,
options: dataSourceTypeList,
props: {
label: "label",
value: "value",
},
required: true,
filterable: true,
clearable: true,
visible: true,
}, {
label: '数据源',
type: 'select',
placeholder: '请选择',
field: 'dataSourceGuid',
default: '',
options: dataSourceList.value,
props: {
label: 'databaseNameZh',
value: 'guid'
},
filterable: true,
visible: true,
required: true
}, {
label: '文件上传',
tip: '支持扩展名:xlsx、xls、csv,文件大小不超过10MB',
type: 'upload-file',
accept: '.xlsx, .xls, .csv',
limitSize: 10,
isExcel: true,
required: true,
default: <any>[],
block: true,
visible: false,
field: 'file',
}]);
const newCreateTaskFormRules = ref({
taskName: [required('请输入任务名称')],
dataSource: [required('请选择数据来源')],
dataSourceGuid: [required('请选择数据源')],
file: [{
validator: (rule: any, value: any, callback: any) => {
if (!value?.length) {
callback(new Error('请上传文件'))
} else {
callback();
}
}, trigger: 'change'
}]
});
const newCreateTaskDialogInfo = ref({
visible: false,
size: 550,
title: "添加数据敏感识别任务",
type: "",
formInfo: {
id: "label-form",
items: newCreateTaskFormItems.value,
rules: newCreateTaskFormRules.value,
},
submitBtnLoading: false,
btns: {
cancel: () => {
newCreateTaskDialogInfo.value.visible = false;
newCreateTaskDialogInfo.value.submitBtnLoading = false;
},
submit: (btn, info) => {
let params = Object.assign({}, info, {
filePath: info.file?.map(f => {
return {
name: f.name,
url: f.url
}
}) || []
});
delete params.file;
newCreateTaskDialogInfo.value.submitBtnLoading = true;
if (newCreateTaskDialogInfo.value.type == 'add') {
saveSensitiveDataTask(params).then((res: any) => {
if (res?.code == proxy.$passCode) {
proxy.$ElMessage.success('标签新建成功');
newCreateTaskDialogInfo.value.visible = false;
newCreateTaskDialogInfo.value.submitBtnLoading = false;
page.value.curr = 1;
getTableData();
} else {
newCreateTaskDialogInfo.value.submitBtnLoading = false;
proxy.$ElMessage.error(res.msg);
}
});
} else {
newCreateTaskDialogInfo.value.submitBtnLoading = true;
params.guid = currTableData.value.guid;
updateSensitiveDataTask(params).then((res: any) => {
if (res?.code == proxy.$passCode) {
proxy.$ElMessage.success('标签编辑成功');
newCreateTaskDialogInfo.value.visible = false;
newCreateTaskDialogInfo.value.submitBtnLoading = false;
getTableData();
} else {
newCreateTaskDialogInfo.value.submitBtnLoading = false;
proxy.$ElMessage.error(res.msg);
}
});
}
}
}
});
const handleTaskSelectChange = (val, row, item) => {
if (item.field == 'dataSource') {
newCreateTaskFormItems.value[0].default = row.taskName;
newCreateTaskFormItems.value[1].default = val;
newCreateTaskFormItems.value[2].default = row.dataSourceGuid || '';
newCreateTaskFormItems.value[2].visible = val == 1;
newCreateTaskFormItems.value[3].default = row.file || [];
newCreateTaskFormItems.value[3].visible = val == 2;
}
}
const handleCreate = () => {
newCreateTaskDialogInfo.value.visible = true;
newCreateTaskDialogInfo.value.title = '添加数据敏感识别任务';
newCreateTaskDialogInfo.value.type = 'add';
newCreateTaskFormItems.value[0].default = '';
newCreateTaskFormItems.value[1].default = 1;
newCreateTaskFormItems.value[2].default = '';
newCreateTaskFormItems.value[2].visible = true;
newCreateTaskFormItems.value[3].default = [];
newCreateTaskFormItems.value[3].visible = false;
}
onBeforeMount(() => {
toSearch({});
getDatabase({ connectStatus: 1 }).then((res: any) => {
if (res?.code == proxy.$passCode) {
dataSourceList.value = res.data || [];
newCreateTaskFormItems.value[2].options = dataSourceList.value;
} else {
proxy.$ElMessage({
type: "error",
message: res.msg,
});
}
})
})
</script>
<template>
<div class="container_wrap">
<div class="table_tool_wrap">
<!-- 头部搜索 -->
<TableTools :searchItems="searchItemList" :searchId="'data-label-search'" @search="toSearch" :init="false" />
<div class="tools_btns">
<el-button type="primary" @click="handleCreate">新建</el-button>
</div>
</div>
<div class="table_panel_wrap">
<!-- 右侧标签管理表格 -->
<Table :tableInfo="tableInfo" @tablePageChange="tablePageChange" />
</div>
<Dialog_form ref="dialogTaskFormRef" :dialogConfigInfo="newCreateTaskDialogInfo"
@formDialogSelectChange="handleTaskSelectChange"></Dialog_form>
</div>
</template>
<style lang="scss" scoped>
.table_tool_wrap {
width: 100%;
height: 84px !important;
padding: 0 8px;
.tools_btns {
padding: 0px 0 0;
}
}
.table_panel_wrap {
width: 100%;
height: calc(100% - 84px);
padding: 0px 8px 0;
}
</style>
\ No newline at end of file
<route lang="yaml">
name: sensitiveIdentifyConfig
</route>
<script lang="ts" setup name="sensitiveIdentifyConfig">
import {
getExecSensitiveTable,
getExecSensitiveFieldTable,
getExecSensitiveFieldColumnListByCondition,
getDataLabelList,
getStatisticsNum,
getParamsList,
updateSensitiveDataTaskFieldLabel,
batchUpdateSensitiveDataTaskFieldStatus,
confirmTaskStatus,
} from '@/api/modules/dataAnonymization';
import PageNav from "@/components/PageNav/index.vue";
import { commonPageConfig } from '@/components/PageNav/index';
import { ElMessageBox } from 'element-plus';
import { changeNum } from "@/utils/common";
import BtnPopover from "@/components/Popover/index.vue";
import useUserStore from "@/store/modules/user";
const route = useRoute();
const router = useRouter();
const { proxy } = getCurrentInstance() as any;
const userStore = useUserStore();
const fullPath = route.fullPath;
const isLook = ref(route.query.isLook == '1');
const fullScreenLoading = ref(false);
const treeInfo = ref({
id: "data-list-tree",
filter: true,
queryValue: "",
queryPlaceholder: "请输入关键字搜索",
props: {
label: "label",
value: "value",
isLeaf: "isLeaf",
children: 'tableList'
},
nodeKey: 'value',
expandedKey: <any>[],
currentNodeKey: '',
data: [],
expandOnNodeClick: false,
loading: false,
currentObj: <any>{}
});
const treeRef = ref();
const nodeClick = (data, node) => {
let exec = () => {
pageInfo.value.labelGuids = [];
pageInfo.value.labelTypeCodes = [];
pageInfo.value.confirmStatus = [];
treeInfo.value.currentNodeKey = data.value;
treeInfo.value.currentObj = data;
if (data.parent) {
pageInfo.value.databaseName = data.parent;
pageInfo.value.tableName = data.tableName;
} else {
pageInfo.value.databaseName = data.databaseName;
pageInfo.value.tableName = '';
}
pageInfo.value.curr = 1;
getSensitiveTableFieldData();
getCntSumInfo();
getSensitiveFieldLabelData();
}
if (checkTableSave()) {
ElMessageBox.confirm(
'存在未保存的数据,确定放弃修改吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
exec();
}).catch(() => {
treeRef.value.setCurrentKey(treeInfo.value.currentObj.value);
})
} else {
exec();
}
}
const batchConfirm = () => {
if (!selectTableFieldRows.value.length) {
proxy.$ElMessage.error('请先勾选待确认的字段');
return;
}
if (checkTableSave()) {
ElMessageBox.confirm(
'存在未保存的数据,确定放弃修改吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
batchUpdateSensitiveDataTaskFieldStatus(selectTableFieldRows.value.map(s => s.guid)).then((res: any) => {
if (res?.code == proxy.$passCode) {
proxy.$ElMessage.success('批量确认字段成功');
getSensitiveTableFieldData();
} else {
proxy.$ElMessage.error(res.msg);
}
})
})
} else {
batchUpdateSensitiveDataTaskFieldStatus(selectTableFieldRows.value.map(s => s.guid)).then((res: any) => {
if (res?.code == proxy.$passCode) {
proxy.$ElMessage.success('批量确认字段成功');
getSensitiveTableFieldData();
} else {
proxy.$ElMessage.error(res.msg);
}
})
}
}
/** 数量统计信息 */
const cntSumInfo: any = ref({});
const getCntSumInfo = () => {
getStatisticsNum({
execGuid: route.query.execGuid,
databaseName: pageInfo.value.databaseName,
tableName: pageInfo.value.tableName
}).then((res: any) => {
if (res?.code == proxy.$passCode) {
cntSumInfo.value = res.data || {};
} else {
proxy.$ElMessage.error(res.msg);
}
})
}
const dataSource = ref(null);
const getExecSensitiveTableData = () => {
treeInfo.value.loading = true;
getExecSensitiveTable(route.query.execGuid).then((res: any) => {
treeInfo.value.loading = false;
if (res?.code == proxy.$passCode) {
treeInfo.value.data = res.data?.map((d, index) => {
d.value = `${d.databaseName}-ds`; //解决文件名称和文件的sheet名称一样的树形选中问题
d.label = d.databaseChName;
if (index == 0) {
dataSource.value = d.dataSource;
}
d.tableList = d.tableList?.map(t => {
t.value = `${d.databaseName}-${t.tableName}`;
t.label = dataSource.value == 1 ? (t.tableChName || t.tableName) : t.tableChName;
t.parent = d.databaseName;
return t;
}) || []
return d;
}) || [];
let treeData: any = treeInfo.value.data || [];
if (treeData.length) {
treeInfo.value.expandedKey = [treeData[0].value];
treeInfo.value.currentNodeKey = treeData[0]?.value;
treeInfo.value.currentObj = treeData[0];
treeData[0].tableList?.[0] && nodeClick(treeData[0], {});
}
} else {
proxy.$ElMessage.error(res.msg);
}
})
}
/** 所有的标签列表供编辑下拉显示 */
const allDataLabelList: any = ref([]);
onBeforeMount(() => {
getExecSensitiveTableData();
getDataLabelList({
pageSize: -1,
bizState: 'Y'
}).then((res: any) => {
if (res?.code == proxy.$passCode) {
allDataLabelList.value = res.data?.records || [];
} else {
proxy.$ElMessage.error(res.msg);
}
})
getParamsList({
dictType: "标签类型",
}).then((res: any) => {
if (res?.code == proxy.$passCode) {
labelTypeList.value = res.data?.map(d => {
d.guid = d.value;
return d;
}) || [];
} else {
proxy.$ElMessage.error(res.msg);
}
});
})
/** ------------------------- 敏感数据识别字段列表操作 --------------------------------- */
const labelList = ref([]);
/** 标签类型的字典列表 */
const labelTypeList: any = ref([]);
/** 查询数据的标签类型列表 */
const labelTypeDataList: any = ref([]);
/** 分页设置 */
const pageInfo = ref({
...commonPageConfig,
taskExecGuid: route.query.execGuid,
databaseName: '',
tableName: '',
rows: 0,
labelGuids: [], //列头筛选
labelTypeCodes: [],
confirmStatus: []
});
/** 敏感数据 */
const sensitiveTableData: any = ref([]);
const sensitiveTableDataLoading = ref(false);
const getSensitiveTableFieldData = () => {
sensitiveTableDataLoading.value = true;
let confirmStatus = pageInfo.value.confirmStatus || [];
getExecSensitiveFieldTable({
pageIndex: pageInfo.value.curr,
pageSize: pageInfo.value.limit,
taskExecGuid: pageInfo.value.taskExecGuid,
databaseName: pageInfo.value.databaseName,
tableName: pageInfo.value.tableName,
labelGuids: pageInfo.value.labelGuids,
labelTypeCodes: pageInfo.value.labelTypeCodes,
confirmStatus: confirmStatus?.length > 1 ? null : confirmStatus[0]
}).then((res: any) => {
sensitiveTableDataLoading.value = false;
if (res?.code == proxy.$passCode) {
const data = res.data || {};
sensitiveTableData.value = data.records || [];
pageInfo.value.limit = data.pageSize
pageInfo.value.curr = data.pageIndex
pageInfo.value.rows = data.totalRows
} else {
proxy.$ElMessage({
type: 'error',
message: res.msg,
})
}
})
}
/** 获取当前选中的左侧库表下对应的数据标签数组,做列头筛选时使用。 */
const getSensitiveFieldLabelData = (info?: {
filterLabel?: Boolean,
filterType?: Boolean,
filterStatus?: Boolean,
setLable?: Boolean,
setType?: Boolean,
setStatus?: Boolean
}) => {
getExecSensitiveFieldColumnListByCondition({
pageSize: -1,
taskExecGuid: pageInfo.value.taskExecGuid,
databaseName: pageInfo.value.databaseName,
tableName: pageInfo.value.tableName,
labelGuids: !info?.filterLabel ? [] : pageInfo.value.labelGuids,
labelTypeCodes: !info?.filterType ? [] : pageInfo.value.labelTypeCodes,
confirmStatus: !info?.filterStatus ? [] : pageInfo.value.confirmStatus
}).then((res: any) => {
if (res?.code == proxy.$passCode) {
let data = res.data || {};
(info?.setLable !== false || !pageInfo.value.labelGuids?.length) && (labelList.value = data['标签']?.map(d => {
d.guid = d.labelGuid;
if (!d.labelGuid && !d.labelName) {
d.guid = `${d.labelGuid}`;
d.labelName = '--';
}
return d;
}) || []);
let list = data['确认状态'];
if (info?.setStatus !== false || !pageInfo.value.confirmStatus?.length) {
confirmStatusList.value = list?.map(d => {
d.guid = d.confirmStatus;
d.label = d.confirmStatus == 'Y' ? '已确认' : '未确认';
return d;
}) || []
};
(info?.setType !== false || !pageInfo.value.labelTypeCodes?.length) && (labelTypeDataList.value = data['标签类型']?.map(d => {
d.guid = d.labelTypeCode;
if (!d.labelTypeCode) {
d.guid = `${d.labelTypeCode}`;
d.labelTypeName = '--';
}
return d;
}) || [])
} else {
proxy.$ElMessage({
type: 'error',
message: res.msg,
})
}
})
}
const popoverLabelListInfo = computed(() => {
return {
type: 'checkbox-list-btns',
data: labelList.value,
placeholder: '请输入关键字搜索',
scope: {
row: {}
},
placement: 'right-start',
props: {
value: 'guid',
label: 'labelName'
},
checked: pageInfo.value.labelGuids,
btn: {
value: 'filter'
}
}
});
const handleLabelPopoverClick = (scope) => {
if (checkTableSave()) {
ElMessageBox.confirm(
'存在未保存的数据,确定放弃修改吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
pageInfo.value.labelGuids = scope.row.selectedData || [];
pageInfo.value.curr = 1;
getSensitiveTableFieldData();
getSensitiveFieldLabelData({
filterLabel: true,
filterStatus: true,
filterType: true,
setLable: false,
setStatus: true,
setType: true
});
})
} else {
pageInfo.value.labelGuids = scope.row.selectedData || [];
pageInfo.value.curr = 1;
getSensitiveTableFieldData();
getSensitiveFieldLabelData({
filterLabel: true,
filterStatus: true,
filterType: true,
setLable: false,
setStatus: true,
setType: true
});
}
}
const popoverLabelTypeListInfo = computed(() => {
return {
type: 'checkbox-list-btns',
data: labelTypeDataList.value,
placeholder: '请输入关键字搜索',
scope: {
row: {}
},
placement: 'right-start',
props: {
value: 'guid',
label: 'labelTypeName'
},
checked: pageInfo.value.labelTypeCodes,
btn: {
value: 'filter'
}
}
});
const handleLabelTypePopoverClick = (scope) => {
if (checkTableSave()) {
ElMessageBox.confirm(
'存在未保存的数据,确定放弃修改吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
pageInfo.value.labelTypeCodes = scope.row.selectedData || [];
pageInfo.value.curr = 1;
getSensitiveTableFieldData();
getSensitiveFieldLabelData({
filterLabel: true,
filterStatus: true,
filterType: true,
setLable: true,
setStatus: true,
setType: false
});
})
} else {
pageInfo.value.labelTypeCodes = scope.row.selectedData || [];
pageInfo.value.curr = 1;
getSensitiveTableFieldData();
getSensitiveFieldLabelData({
filterLabel: true,
filterStatus: true,
filterType: true,
setLable: true,
setStatus: true,
setType: false
});
}
}
const confirmStatusList: any = ref([]);
const popoverStatusListInfo = computed(() => {
return {
type: 'checkbox-list-btns',
data: confirmStatusList.value,
placeholder: '请输入关键字搜索',
scope: {
row: {}
},
placement: 'right-start',
props: {
value: 'guid',
label: 'label'
},
checked: pageInfo.value.confirmStatus,
btn: {
value: 'filter'
}
}
});
const handleStatusPopoverClick = (scope) => {
if (checkTableSave()) {
ElMessageBox.confirm(
'存在未保存的数据,确定放弃修改吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
pageInfo.value.confirmStatus = scope.row.selectedData || [];
pageInfo.value.curr = 1;
getSensitiveTableFieldData();
getSensitiveFieldLabelData({
filterLabel: true,
filterStatus: true,
filterType: true,
setLable: true,
setStatus: false,
setType: true
});
})
} else {
pageInfo.value.confirmStatus = scope.row.selectedData || [];
pageInfo.value.curr = 1;
getSensitiveTableFieldData();
getSensitiveFieldLabelData({
filterLabel: true,
filterStatus: true,
filterType: true,
setLable: true,
setStatus: false,
setType: true
});
}
}
/** 标签选择的值改变,标签类型跟着变化。 */
const handleSelectChange = (val, scope) => {
//暂时不处理,因为未保存会出现清空的场景,需要先存下之前的值。
let label = allDataLabelList.value.find(label => label.guid == val);
if (label) {
scope.row.labelTypeCode = label.labelTypeCode;
scope.row.labelTypeName = label.labelTypeName;
scope.row.labelName = label.labelName;
} else {
scope.row.labelTypeCode = null;
scope.row.labelTypeName = null;
scope.row.labelName = null;
}
}
const sensitiveTableSelectable = (row, index) => {
// return row.confirmStatus == 'N';
return true;
}
const selectTableFieldRows: any = ref([]);
/** 勾选字段标准选中变化。 */
const selectionDataChange = (val) => {
selectTableFieldRows.value = val;
};
const handleFieldClickEdit = (scope) => {
scope.row.isEdit = true;
}
const handleFieldClickSave = (scope) => {
let labelName = '';
let labelTypeCode = '';
if (scope.row.labelGuid) {
let label = allDataLabelList.value.find(label => label.guid == scope.row.labelGuid);
if (label) {
labelName = label.labelName;
labelTypeCode = label.labelTypeCode;
}
}
sensitiveTableDataLoading.value = true;
updateSensitiveDataTaskFieldLabel([{
guid: scope.row.guid,
labelGuid: scope.row.labelGuid,
labelName: labelName,
labelTypeCode: labelTypeCode
}]).then((res: any) => {
sensitiveTableDataLoading.value = false;
if (res?.code == proxy.$passCode) {
getSensitiveTableFieldData();
getSensitiveFieldLabelData({
filterLabel: true,
filterStatus: true,
filterType: true
});
getCntSumInfo();
proxy.$ElMessage.success('字段的标签修改成功');
} else {
proxy.$ElMessage.error(res.msg);
}
});
}
const checkTableSave = () => {
return sensitiveTableData.value.some(s => s.isEdit == true);
}
const pageChange = (info) => {
const toChange = checkTableSave()
const changeCont = () => {
pageInfo.value.curr = Number(info.curr)
pageInfo.value.limit = Number(info.limit)
getSensitiveTableFieldData();
}
if (toChange) {
ElMessageBox.confirm(
'存在未保存的数据,确定放弃修改吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
changeCont();
})
} else {
changeCont();
}
}
const cancel = () => {
if (checkTableSave()) {
proxy.$openMessageBox("当前页面存在未保存的数据,确定放弃修改吗?", () => {
userStore.setTabbar(userStore.tabbar.filter((tab: any) => tab.fullPath !== fullPath));
router.push({
name: 'sensitiveIdentify'
});
}, () => {
proxy.$ElMessage.info("已取消");
});
} else {
userStore.setTabbar(userStore.tabbar.filter((tab: any) => tab.fullPath !== fullPath));
router.push({
name: 'sensitiveIdentify'
});
}
}
const pageConfirm = () => {
fullScreenLoading.value = true;
confirmTaskStatus(route.query.execGuid).then((res: any) => {
fullScreenLoading.value = false;
if (res?.code == proxy.$passCode) {
proxy.$ElMessage.success('确认成功');
userStore.setTabbar(userStore.tabbar.filter((tab: any) => tab.fullPath !== fullPath));
router.push({
name: 'sensitiveIdentify'
});
} else {
proxy.$ElMessage.error(res.msg);
}
})
}
const cntLabelMap = ref({
'01': 'directIdentifierNum',
'02': 'standardIdentifierNum',
'03': 'sensitiveNum',
'04': 'nonSensitiveNum'
})
</script>
<template>
<div class="container_wrap full flex" v-loading="fullScreenLoading">
<div class="aside_wrap" :style="{ height: isLook ? '100%' : 'calc(100% - 40px)' }">
<div class="aside_title">数据表列表</div>
<Tree ref="treeRef" :treeInfo="treeInfo" @nodeClick="nodeClick" />
</div>
<div class="main_wrap" :style="{ height: isLook ? '100%' : 'calc(100% - 40px)' }">
<div class="table_tool_wrap">
<div class="cnt-desc">{{'表总数:' + changeNum(cntSumInfo.tableNum || 0, 0) +
'张 字段总数:'
+ changeNum(cntSumInfo.fieldNum || 0, 0) + '个 敏感表总数:' +
changeNum(cntSumInfo.sensitiveTableNum || 0, 0) + '张 敏感字段总数:' + changeNum(cntSumInfo.sensitiveFieldNum || 0,
0) +
'个,其中' + labelTypeList?.map(l => `${l.label}${changeNum(cntSumInfo[cntLabelMap[l.value]] || 0,
0)}个`).join(',') + ',非敏感' + changeNum(cntSumInfo['nonSensitiveNum'] || 0, 0) + '个'
}}</div>
<div class="tools_btns" v-if="!isLook">
<el-button type="primary" @click="batchConfirm">批量确认</el-button>
</div>
</div>
<div class="table_panel_wrap" :style="{ height: isLook ? 'calc(100% - 54px)' : 'calc(100% - 91px)' }">
<!-- 右侧表字段标签匹配管理表格 -->
<el-table ref="sensitiveTableRef" :data="sensitiveTableData" v-loading="sensitiveTableDataLoading"
:highlight-current-row="true" stripe border height="100%" row-key="guid"
@selection-change="selectionDataChange" tooltip-effect="light" :style="{
width: '100%',
'max-height': 'calc(100% - 16px)',
display: 'inline-block',
}">
<el-table-column type="selection" v-if="!isLook" :selectable="sensitiveTableSelectable" :width="32"
align="center" />
<el-table-column label="序号" type="index" width="56px" align="center" show-overflow-tooltip>
<template #default="scope">
<span>{{
pageInfo.curr !== undefined
? (pageInfo.curr - 1) * pageInfo.limit + scope.$index + 1
: scope.$index + 1
}}</span>
</template>
</el-table-column>
<el-table-column label="字段中文名" prop="fieldChName" width="160" align="left" show-overflow-tooltip>
<template #default="scope">
<span>{{ scope.row.fieldChName || '--' }}</span>
</template>
</el-table-column>
<el-table-column label="字段英文名" prop="fieldName" width="150" align="left" show-overflow-tooltip>
<template #default="scope">
<span>{{ scope.row.fieldName || '--' }}</span>
</template>
</el-table-column>
<el-table-column label="标签" prop="labelGuid" class-name="filter-cell" width="140" align="left"
show-overflow-tooltip>
<template #header>
<span>标签</span>
<BtnPopover v-show="labelList.length" :popoverInfo="popoverLabelListInfo"
@popverBtnClick="handleLabelPopoverClick" />
</template>
<template #default="scope">
<el-select-v2 v-if="scope.row['isEdit']" v-model="scope.row['labelGuid']" filterable
popper-class="el-select-v2-popper" :options="allDataLabelList" placeholder="请选择" clearable
:props="{ value: 'guid', label: 'labelName' }" @change="(val) => handleSelectChange(val, scope)">
<template #default="{ item }">
<ellipsis-tooltip :content="item.labelName ?? ''" class-name="w100f"
:refName="'tooltipOver' + item.guid"></ellipsis-tooltip>
</template>
</el-select-v2>
<span v-else>{{ scope.row["labelName"] || '--' }}</span>
</template>
</el-table-column>
<el-table-column label="标签类型" prop="labelTypeName" class-name="filter-cell" width="130" align="left"
show-overflow-tooltip>
<template #header>
<span>标签类型</span>
<BtnPopover v-show="labelTypeDataList.length" :popoverInfo="popoverLabelTypeListInfo"
@popverBtnClick="handleLabelTypePopoverClick" />
</template>
<template #default="scope">
<span>{{ scope.row.labelTypeName || '--' }}</span>
</template>
</el-table-column>
<el-table-column label="所属表中文名" prop="tableChName" width="140" align="left" show-overflow-tooltip>
<template #default="scope">
<span>{{ scope.row.tableChName || '--' }}</span>
</template>
</el-table-column>
<el-table-column label="所属表英文名" prop="tableName" :width="dataSource == 1 ? 160 : 130" align="left" show-overflow-tooltip>
</el-table-column>
<el-table-column label="确认状态" prop="confirmStatus" class-name="filter-cell" width="120" align="center"
show-overflow-tooltip>
<template #header>
<span>确认状态</span>
<BtnPopover v-show="confirmStatusList.length" :popoverInfo="popoverStatusListInfo"
@popverBtnClick="handleStatusPopoverClick" />
</template>
<template #default="scope">
<el-tag v-if="scope.row.confirmStatus != null"
:type="scope.row.confirmStatus == 'Y' ? 'success' : 'warning'">{{
scope.row.confirmStatus == 'Y' ? '已确认' : '未确认'
}}</el-tag>
<span v-else>{{ '--' }}</span>
</template>
</el-table-column>
<el-table-column v-if="!isLook" label="操作" width="90px" align="left" fixed="right" show-overflow-tooltip>
<template #default="scope">
<span class="text_btn" v-if="!scope.row['isEdit']" @click="handleFieldClickEdit(scope)"
v-preReClick>编辑</span>
<span class="text_btn" v-else @click="handleFieldClickSave(scope)" v-preReClick>保存</span>
</template>
</el-table-column>
</el-table>
<PageNav :pageInfo="pageInfo" @pageChange="pageChange" />
</div>
</div>
<div class="bottom_tool_wrap">
<el-button @click="cancel" v-preReClick>取消</el-button>
<el-button type="primary" @click="pageConfirm" v-preReClick>确认</el-button>
</div>
</div>
</template>
<style lang="scss" scoped>
.container_wrap {
flex-wrap: wrap;
.aside_wrap {
width: 200px;
height: calc(100% - 40px);
}
.table_tool_wrap {
display: flex;
flex-direction: column;
min-height: 25px;
.cnt-desc {
line-height: 24px;
white-space: break-spaces;
margin-top: 4px;
}
}
.tools_btns {
padding-top: 0px;
}
.table_panel_wrap {
height: calc(100% - 91px);
}
.bottom_tool_wrap {
height: 40px;
width: 100%;
padding: 0 16px;
border-top: 1px solid #d9d9d9;
display: flex;
justify-content: center;
align-items: center;
}
}
.tree_panel {
height: 100%;
padding-top: 0;
:deep(.el-tree) {
margin: 0;
height: calc(100% - 68px);
overflow: hidden auto;
}
}
:deep(.el-table) {
& td.el-table__cell {
.cell {
padding: 0px 10px;
}
padding: 2px 0px;
height: 36px;
}
}
:deep(.filter-cell) {
.cell {
position: relative;
.el-icon.filter-icon {
position: absolute;
right: 6px;
top: 2px;
width: 18px;
height: 18px;
svg {
width: 18px;
height: 18px;
}
}
}
}
</style>
\ No newline at end of file
<route lang="yaml">
name: sensitiveIdentifyTaskExecLog
</route>
<script lang="ts" setup name="sensitiveIdentifyTaskExecLog">
import { ref } from "vue";
import { useRouter, useRoute } from "vue-router";
import Table from "@/components/Table/index.vue";
import { ElMessage } from "element-plus";
import { commonPageConfig } from '@/components/PageNav/index';
import {
getSensitiveDataTaskExecLog,
} from '@/api/modules/dataAnonymization';
import { TableColumnWidth } from "@/utils/enum";
const { proxy } = getCurrentInstance() as any;
const router = useRouter();
const route = useRoute();
const guid = route.query.guid;
const wordName = route.query.name
const page = ref({
...commonPageConfig
});
const tableInfo = ref({
id: "word-log-table",
loading: false,
fields: [
{ label: "序号", type: "index", width: TableColumnWidth.INDEX, align: "center" },
{ label: "执行人", field: "createUserName", width: TableColumnWidth.USERNAME },
{ label: "执行时间", field: "execTime", width: TableColumnWidth.DATETIME },
{ label: "执行状态", field: "sensitiveIdentifyTaskStatus", width: TableColumnWidth.STATE, align: 'center', type: "tag" },
{ label: "确认人", field: "confirmUserName", width: TableColumnWidth.USERNAME },
{ label: "确认时间", field: "confirmTime", width: TableColumnWidth.DATETIME },
{ label: "确认状态", field: "sensitiveIdentifyConfirmStatus", width: TableColumnWidth.STATE, align: 'center', type: "tag" },
],
data: [],
page: {
type: "normal",
rows: 0,
...page.value,
},
actionInfo: {
label: "操作",
type: "btn",
width: 100,
fixed: 'right',
btns: (scope) => {
return [{
label: "查看", value: "report", disabled: scope.row['status'] != 'Y', click: (scope) => {
router.push({
name: 'sensitiveIdentifyConfig',
query: {
guid: route.query.guid,
execGuid: scope.row.guid,
taskName: route.query.name,
isLook: '1',
}
});
}
}];
}
}
});
const getTableData = () => {
tableInfo.value.loading = true;
getSensitiveDataTaskExecLog({ pageIndex: page.value.curr, pageSize: page.value.limit, taskGuid: guid }).then((res: any) => {
tableInfo.value.loading = false;
if (res?.code == proxy.$passCode) {
const data = res.data || {}
tableInfo.value.data = data.records?.map(d => {
d.sensitiveIdentifyTaskStatus = d.status;
d.sensitiveIdentifyConfirmStatus = d.confirmStatus;
return d;
}) || []
tableInfo.value.page.limit = data.pageSize
tableInfo.value.page.curr = data.pageIndex
tableInfo.value.page.rows = data.totalRows
} else {
ElMessage.error(res.msg);
}
})
};
const tableBtnClick = (scope, btn) => {
const type = btn.value;
const row = scope.row;
if (type == 'reportView') {
router.push({
name: 'analysisReport',
query: {
planGuid: row.planGuid,
reportExecGuid: row.guid,
name: wordName
}
});
}
};
onActivated(() => {
getTableData();
});
</script>
<template>
<div class="container_wrap">
<div class="table_panel_wrap">
<Table :tableInfo="tableInfo" @tableBtnClick="tableBtnClick" />
</div>
</div>
</template>
<style lang="scss" scoped>
.container_wrap {
padding: 0;
.table_panel_wrap {
height: 100%;
padding: 16px 16px 0;
}
}
</style>
\ No newline at end of file
......@@ -1392,7 +1392,7 @@ onActivated(() => {
<ContentWrap v-show="!currentStep && detailType == 'sign' || currentStep == 3" id="sign-info" title="合约签署"
expandSwicth style="margin-top: 15px" :isExpand="expandSign" @expand="(v) => (expandSign = v)" description="">
<template v-for="(item, index) in signDetailInfo">
<div :class="{ 'h-title': true, 'mt6': index > 0 }">{{ item.executionerTypeName }}</div>
<div :class="{ 'h-title': true, 'mt6': <number>index > 0 }">{{ item.executionerTypeName }}</div>
<div class="list_panel">
<div class="list_item wrap">
<span class="item_label">签署主体标识</span>
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!