371e8ad9 by lihua

匿名化处理的接口联调

1 parent 24ff18ee
......@@ -67,6 +67,12 @@ export const getGeneralizeFileList = (params) => request({
data: params
})
/** 获取泛化文件列表,包括名称和guid */
export const getGeneralizeFileNameList = () => request({
url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/generalize-file/name-list`,
method: 'post'
})
export const saveGeneralizeFile = (data) => request({
url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/generalize-file/save`,
method: 'post',
......@@ -214,3 +220,104 @@ export const deleteAnonTask = (data) => request({
method: 'delete',
data
})
/** 保存匿名化任务 */
export const saveAnonTask = (params) => request({
url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/save`,
method: 'post',
data: params
})
/** 更新匿名化任务 */
export const updateAnonTask = (params) => request({
url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/update`,
method: 'put',
data: params
})
/** 获取匿名化任务详情 */
export const getAnonTaskDetail = (guid) => request({
url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/detail?guid=${guid}`,
method: 'get'
})
/** 执行匿名化任务 */
export const execAnonTask = (taskGuid) => request({
url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/exec-task?taskGuid=${taskGuid}`,
method: 'post'
})
/** 匿名化任务检验接口 */
export const anonTaskCheck = (params) => request({
url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/check`,
method: 'post',
data: params
})
/** 获取匿名化任务分析结果数据 */
export const getAnonAnalyzeResult = (execGuid) => request({
url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/get-anon-analyze?taskExecGuid=${execGuid}`,
method: 'get'
})
/** 获取匿名化任务分析结果统计分页数据 */
export const getAnonAnalyzePageData = (params) => request({
url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/page-anon-analyze-data`,
method: 'post',
data: params
})
/** 获取匿名化任务结果数据 */
export const getAnonPageData = (params) => request({
url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/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_ANONYMIZATION_BASEURL}/sensitive-data-task/get-label-by-field-name?fieldName=${fieldName}`,
method: 'get'
});
/** 验证样本数据 */
export const validateAnonRule = (params) => request({
url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/check`,
method: 'post',
data: params
})
/** 导出匿名化结果数据 */
export const exportAnonExecData = (params) => request({
url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/export-anon-data?taskGuid=${params.taskGuid}&taskExecGuid=${params.execGuid}`,
method: 'get',
responseType: 'blob'
})
\ No newline at end of file
......
......@@ -13,6 +13,7 @@ const emits = defineEmits([
"drawerBtnClick",
"radioGroupChange",
"drawerSelectChange",
'drawerInputChange',
"drawerTableSelectChange",
"drawerTableBtnClick",
"drawerTableToolBtnClick",
......@@ -98,15 +99,15 @@ const onClickOutside = (e: any) => {
emits("onClickOutside");
};
const getDrawerConRef = (refName) => {
console.log(refName, '----------')
const getDrawerConRef = (refName, index = 0) => {
//console.log(refName, '----------')
if (refName == 'drawerTableRef') {
const dtf = drawerTableRef.value[0] || drawerTableRef.value
return dtf?.tableRef
}
// const drawerForm = drawerFormRef.value[0] || drawerFormRef.value;
if (refName == 'drawerFormRef') {
const drawerForm = drawerFormRef.value?.[0] || drawerFormRef.value;
const drawerForm = drawerFormRef.value?.[index] || drawerFormRef.value;
return drawerForm
}
}
......@@ -187,6 +188,10 @@ const radioGroupChange = (val, info) => {
emits("radioGroupChange", val, info);
};
const formInputChange = (val, row, info) => {
emits("drawerInputChange", val, row, info);
}
const formSelectChange = (val, row, info) => {
emits("drawerSelectChange", val, row, info);
};
......@@ -319,10 +324,10 @@ const drawerClose = () => {
<template v-else>
<Form ref="drawerFormRef" :itemList="con.formInfo.items" :formId="con.formInfo.id" :rules="con.formInfo.rules"
:col="con.formInfo.col" :readonly="con.formInfo.readonly" @radioGroupChange="radioGroupChange"
@selectChange="formSelectChange" @btnClick="formBtnClick">
@selectChange="formSelectChange" @input-change="formInputChange" @btnClick="formBtnClick">
</Form>
<!-- 插槽内容 -->
<slot></slot>
<slot v-if="con.showSlot !== false"></slot>
</template>
</div>
</template>
......
......@@ -189,27 +189,30 @@ const inputFocus = (event, item) => {
}
const inputChange = (val, row) => {
let decimalCnt = row.decimalCnt || 2;
if (row.inputType == "moneyNumber" || row.inputType == 'scoreNumber') {
let strArr = val.split(".");
if (strArr.length > 1) {
let right = strArr[1];
if (right === "" || right.length < 2) {
formInline.value[row.field] = val = parseFloat(val || 0).toFixed(2);
if (right === "" || right.length < decimalCnt) {
formInline.value[row.field] = val = parseFloat(val || 0).toFixed(decimalCnt);
}
} else {
formInline.value[row.field] = val = parseFloat(val || 0).toFixed(2);
formInline.value[row.field] = val = parseFloat(val || 0).toFixed(decimalCnt);
}
}
if (row.inputType == 'scoreNumber' && parseFloat(val) > 100) {
let max = row.max || 100;
if (row.inputType == 'scoreNumber' && parseFloat(val) > max) {
// 先去除非数字和小数点字符
val = val.replace(/[^\d.]/g, "");
// 限制最多保留两位小数
val = val.replace(/\.{2,}/g, ".");
val = val.replace(/^(\d+)\.(\d{2}).*$/, "$1.$2");
let exp2 = new RegExp(`^(\d+)\.(\d{${decimalCnt}}).*$`, 'g');
val = val.replace(exp2, "$1.$2");
let num = parseFloat(val);
if (num > 100) {
num = 100; // 超过100时将其设置为100
val = num.toFixed(2); // 保证显示为两位小数
if (num > max) {
num = max; // 超过100时将其设置为100
val = num.toFixed(decimalCnt); // 保证显示为两位小数
}
formInline.value[row.field] = val;
}
......@@ -250,11 +253,15 @@ const inputEventChange = (val, item) => {
}
return;
} else if (item.inputType == 'scoreNumber') {//小于100的,保留两位小数
let decimalCnt = item.decimalCnt || 2;
formInline.value[item.field] = formInline.value[item.field].toString().replace(/[^\d.]/g, "")
/** 连续两个小数点替换为一个小数点 */
formInline.value[item.field] = formInline.value[item.field].toString().replace(/\.{2,}/g, ".")
formInline.value[item.field] = formInline.value[item.field].toString().replace(/^\D*(\d{0,3}(?:\.\d{0,2})?).*$/g, "$1")
/** 最多取三位整数,2位小数 */
let exp2 = new RegExp(`^\\D*(\\d{0,3}(?:\\.\\d{0,${decimalCnt}})?).*$`, 'g');
formInline.value[item.field] = formInline.value[item.field].toString().replace(exp2, "$1")
if (item.max != null && formInline.value[item.field] > item.max) {
formInline.value[item.field] = item.max.toFixed(2);
formInline.value[item.field] = item.max.toFixed(decimalCnt);
}
return;
} else if (item.inputType == 'moneyNumber') {// 单位是元,保留两位小数。
......@@ -755,21 +762,30 @@ const panelChange = (scope, row) => {
<span v-if="child.prepend">{{ child.prepend }}</span>
<el-input v-if="child.visible ?? true" v-model.trim="formInline[child.field]"
:disabled="child.disabled || readonly" :placeholder="child.placeholder"
:maxlength="child.maxlength ?? ''" />
:maxlength="child.maxlength ?? ''" @change="(val) => inputChange(val, child)"
@input="(val) => inputEventChange(val, child)" />
<span v-if="child.append">{{ child.append }}</span>
</div>
</div>
<div class="checkbox_input" :class="[item.col, { is_block: item.block }]"
v-else-if="item.type == 'checkbox-input-item'">
<el-checkbox v-model="formInline[item.field]" :disabled="item.disabled || readonly"
@change="(val) => checkboxChange(val, item)" :true-label="item.trueValue ?? true"
:false-label="item.falseValue ?? false">{{ item.placeholder }}</el-checkbox>
@change="(val) => checkboxChange(val, item)" :true-value="item.trueValue ?? true"
:false-value="item.falseValue ?? false">{{ item.placeholder }}</el-checkbox>
<div class="input_panel" v-for="child in item.children">
<el-form-item v-if="child.visible ?? true" :prop="child.field"
:validate-status="child.validateStatus ?? ''" :error="child.error"
:class="[child.col, { is_block: child.block }]" :style="child.style ?? {}">
<el-input v-model.trim="formInline[child.field]" :disabled="child.disabled || readonly"
:placeholder="child.placeholder" :maxlength="child.maxlength ?? ''" />
<el-input v-if="child.type == 'input'" v-model.trim="formInline[child.field]" :disabled="child.disabled || readonly"
:placeholder="child.placeholder" :maxlength="child.maxlength ?? ''" @change="(val) => inputChange(val, child)"
@input="(val) => inputEventChange(val, child)" />
<el-select v-else-if="child.type == 'select'" v-model="formInline[child.field]"
:placeholder="child.placeholder" :disabled="child.disabled || readonly" :filterable="child.filterable" :clearable="child.clearable"
:teleported="child.teleported || true">
<el-option v-for="opts in child.options"
:label="child.props?.label ? opts[child.props.label] : opts.label"
:value="child.props?.value ? opts[child.props.value] : opts.value" :disabled="opts.disabled" />
</el-select>
</el-form-item>
</div>
</div>
......
......@@ -147,7 +147,7 @@ const routes: RouteRecordRaw[] = [
name: 'anonTaskCreate',
component: () => import('@/views/data_anonymization/anonTaskCreate.vue'),
meta: {
title: '新建匿名化处理任务',
title: '匿名化处理任务',
sidebar: false,
breadcrumb: false,
cache: true,
......@@ -161,6 +161,23 @@ const routes: RouteRecordRaw[] = [
}
}
},
{
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}`;
}
}
},
],
},
]
......
<route lang="yaml">
name: anonResultView
</route>
<script lang="ts" setup name="anonResultView">
import { ref } from "vue";
import {
getAnonPageData,
getAnonAnalyzeResult,
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,
})
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);
});
} 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;
getAnonAnalyzeResult(val).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);
}
});
}, {
immediate: true
})
onBeforeMount(() => {
if (!props.isPage) {
return;
}
tableDataLoading.value = true;
getAnonAnalyzeResult(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) {
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: '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
......@@ -4,16 +4,48 @@
<script lang="ts" setup name="anonTaskCreate">
import {
dataSourceTypeList
dataSourceTypeList,
getAnonTaskDetail,
getParamsList,
chTransformEn,
getAnonAnalyzeResult,
getAnonAnalyzePageData,
getDatabase,
getDsTableByDs,
getDsTableFieldColumn,
getDsTableSampleData,
saveAnonTask,
updateAnonTask,
exportAnonExecData,
} from '@/api/modules/dataAnonymization';
import {
parseAndDecodeUrl,
getDownFileSignByUrl,
obsDownloadRequest
} from "@/api/modules/obsService";
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 { download } from "@/utils/common";
import anonResultView from './anonResultView.vue';
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 step = ref(0);
......@@ -27,11 +59,17 @@ const stepsInfo = ref({
]
})
const dataSourceList = ref([]);
/** 数据源列表 */
const dataSourceList: any = ref([]);
/** 数据源对应的数据表 */
const dsTableList: any = ref([]);
/** 数据共享类型字段列表 */
const dataSharingTypeList = ref([]);
const formRef = ref();
/** 数据选择的表单配置信息 */
const dataSelectInfoItems = ref([{
label: '任务名称',
......@@ -49,8 +87,8 @@ const dataSelectInfoItems = ref([{
type: 'select',
placeholder: '请选择',
field: 'dataSharingTypeCode',
default: '',
options: dataSharingTypeList,
default: '01',
options: dataSharingTypeList.value,
props: {
label: "label",
value: "value",
......@@ -60,6 +98,21 @@ const dataSelectInfoItems = ref([{
clearable: true,
visible: true,
}, {
label: '患者占总人口比',
type: 'input',
placeholder: '数值,支持小数点9位',
field: 'patientPopulationRate',
maxlength: 11,
min: 0,
max: 1,
inputType: 'scoreNumber',
decimalCnt: 9,
default: '',
required: true,
filterable: true,
clearable: true,
visible: true,
}, {
label: '数据来源',
type: 'select',
placeholder: '请选择',
......@@ -72,7 +125,6 @@ const dataSelectInfoItems = ref([{
},
required: true,
filterable: true,
clearable: true,
visible: true,
}, {
label: '数据源',
......@@ -89,31 +141,694 @@ const dataSelectInfoItems = ref([{
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: '文件上传',
tip: '支持扩展名:xlsx、xls、csv,文件大小不超过10MB',
type: 'upload-file',
accept: '.xlsx, .xls, .csv',
limitSize: 10,
limit: 1,
isExcel: true,
required: true,
default: <any>[],
block: true,
block: false,
col: 'wid60',
visible: false,
field: 'file',
}]);
const dataSelectInfoFormRules = ref({
taskName: [required('请输入任务名称')],
dataSharingTypeCode: [required('请选择数据共享类型')],
patientPopulationRate: [required('请输入患者占总人口比')],
dataSourceGuid: [required('请选择数据源')],
tableName: [required('请选择数据表')],
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[4].visible = val == 1;
dataSelectInfoItems.value[5].visible = val == 1;
dataSelectInfoItems.value[6].visible = val == 2;
dataSelectInfoItems.value.forEach(d => {
d.default = formInfo[d.field];
if (d.field == 'file') {
d.default = !d.default ? [] : d.default;
}
});
sampleTableFields.value = [];
parseFileDataSum.value = [];
sampleTableData.value = [];
} else if (row.field == 'dataSourceGuid') {
if (!val) {
currDatasourceSelect.value = [];
sampleTableFields.value = [];
parseFileDataSum.value = [];
sampleTableData.value = [];
dataSelectInfoItems.value.forEach(d => {
d.default = formInfo[d.field];
if (d.field == 'file') {
d.default = !d.default ? [] : d.default;
} else if (d.field == 'tableName') {
d.options = dsTableList.value;
d.default = '';
}
});
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 || [];
dataSelectInfoItems.value.forEach(d => {
d.default = formInfo[d.field];
if (d.field == 'file') {
d.default = !d.default ? [] : d.default;
} else if (d.field == 'tableName') {
d.options = dsTableList.value;
d.default = '';
}
});
} 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;
}) || [];
} else {
ElMessage.error(res.msg);
}
});
/** 判断有抽样数据,需要查询接口 */
getSampleDataByDsTable();
}
}
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 changeStep = (val) => {
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 exportResult = () => {
/** 输入抽样比例值改变 */
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,
}
);
const formatterPreviewDate = (row, info) => {
let enName = info.enName;
let v = row[enName];
if (v === 0) {
return v;
}
if (!v) {
return v || '--';
}
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 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]];
const json: any[] = XLSX.utils.sheet_to_json(sheet, { header: 1 });
if (json.length == 0) {
sampleTableFields.value = [];
sampleTableData.value = [];
} else {
const res = await chTransformEn(json[0]);
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;
});
}
/** 获取文件解析后根据抽样比例得出的表格数据 */
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 > 1000 ? 1001 : cnt).map((info, row) => {
let object = {};
parseFileDataSum.value[0].forEach((chName, col) => {
let name = sampleTableFields.value[col].enName;
object[name] = info[col];
});
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,
pageSize: 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) => {
sampleTableFields.value = [];
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 == 2) {
formRef.value?.ruleFormRef?.validate((valid) => {
if (valid) {
dataSimpleFormRef.value?.ruleFormRef?.validate((valid) => {
if (valid) {
step.value = val - 1;
stepsInfo.value.step = val - 1;
}
});
}
});
} else if (val == 3) {
// 保存并提交 TODO。需要加个 记录旧值的,用来判断新值和旧值,是否发生变化,若变化则需要调用保存接口之后,再进行下一步。
let configInfo = await anonTaskStepTwoRef.value?.getStepTwoConfigInfo();
if (!configInfo) {
return;
}
let saveParams: any = { ...formRef.value.formInline };
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;
}
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);
if (taskGuid.value) {
saveParams.guid = taskGuid.value;
}
if (isEqual(saveParams, oldAnonTaskValueInfo.value)) {
step.value = val - 1;
stepsInfo.value.step = val - 1;
return;
}
if (!taskGuid.value) { //保存
fullscreenLoading.value = true;
saveAnonTask(saveParams).then((res: any) => {
fullscreenLoading.value = false;
if (res.code == proxy.$passCode) {
taskGuid.value = res.data?.guid;
isExecEnd.value = false;
taskExecGuid.value = res.data?.lastExecGuid;
step.value = val - 1;
stepsInfo.value.step = val - 1;
oldAnonTaskValueInfo.value = saveParams;
} 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;
oldAnonTaskValueInfo.value = saveParams;
} else {
ElMessage.error(res.msg);
}
})
}
} else if (val == 4) {
//下一步之后,调用分析结果。
getAnonAnalyzeResult(detailInfo.value.lastExecGuid).then((res: any) => {
debugger
});
getAnonAnalyzePageData({
pageSize: -1
}).then((res: any) => {
debugger
});
step.value = val - 1;
stepsInfo.value.step = val - 1;
} else if (val <= step.value) {
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 + '_匿名化数据.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 || {};
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,
}
}
dataSelectInfoItems.value.forEach(d => {
d.default = detailInfo.value[d.field];
if (d.field == 'file') {
d.default = detailInfo.value.filePath ? [detailInfo.value.filePath] : [];
} else if (d.field == 'patientPopulationRate') {
if (d.default && typeof d.default == 'number') {
d.default = d.default.toFixed(d.decimalCnt);
}
}
});
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[4].visible = dataSource == 1;
dataSelectInfoItems.value[5].visible = dataSource == 1;
dataSelectInfoItems.value[6].visible = dataSource == 2;
//文件解析
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;
}
obsDownloadRequest(refSignInfo?.data).then((res: any) => {
sampleTableDataLoading.value = false;
if (res && !res.msg) {
parseFileData(res);
} else {
res?.msg && 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;
}) || [];
} else {
ElMessage.error(res.msg);
}
});
/** 判断有抽样数据,需要查询接口 */
getSampleDataByDsTable();
}
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) {
fieldTypeList.value = res.data || [];
} else {
proxy.$ElMessage.error(res.msg);
}
});
})
const cancelTask = () => {
proxy.$openMessageBox("当前页面尚未保存,确定放弃修改吗?", () => {
userStore.setTabbar(userStore.tabbar.filter((tab: any) => tab.fullPath !== fullPath));
......@@ -135,7 +850,50 @@ const cancelTask = () => {
</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" />
<Form ref="formRef" :itemList="dataSelectInfoItems" :rules="dataSelectInfoFormRules"
formId="model-select-edit" col="col3 custom-form" @select-change="handleDataSelectFormSelectChange"
@uploadFileChange="uploadFileChange" />
</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>
<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 id="analysis-result" title="匿名结果分析" description="" style="margin-top: 8px;">
</ContentWrap>
</div>
<div class="operator_panel_wrap" v-show="step == 3">
<ContentWrap id="analysis-result" title="匿名化数据结果" description="" style="margin-top: 8px;">
<anonResultView :is-page="false" :execGuid="isExecEnd ? taskExecGuid : ''"></anonResultView>
</ContentWrap>
</div>
</div>
......@@ -144,9 +902,16 @@ const cancelTask = () => {
<el-button @click="cancelTask">取消</el-button>
<el-button type="primary" @click="changeStep(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(2)">上一步</el-button>
<el-button type="primary" @click="changeStep(4)">下一步</el-button>
</template>
<template v-else>
<el-button @click="cancelTask">取消</el-button>
<el-button type="primary" @click="changeStep(1)">上一步</el-button>
<el-button type="primary" @click="changeStep(2)">上一步</el-button>
<el-button type="primary" v-preReClick @click="exportResult">导出</el-button>
</template>
</div>
......@@ -181,4 +946,81 @@ const cancelTask = () => {
padding: 0 16px;
overflow: hidden auto;
}
.operator_panel_wrap {
padding-bottom: 12px;
}
: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;
.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;
}
}
</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';
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: '200px',
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;
}) || [];
}
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.row.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: '≤'
}];
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') {
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;
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) => {
let optionsList = ruleModelTableInfo.value.data?.map(v => {
return {
fieldName: v.fieldName,
fieldChName: v.fieldChName
}
}) || [];
privacyFormItems.value[2].children[0].options = optionsList;
privacyFormItems.value[3].children[0].options = optionsList;
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) {
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 {
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: {
label: 'fieldChName',
value: 'fieldName'
},
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',
children: [
{
label: '',
type: 'select',
placeholder: '请选择字段',
field: 'tcFieldName',
default: '',
options: [],
props: {
label: 'fieldChName',
value: 'fieldName'
},
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: privacyFormRef.value?.formInline,
anonTaskRules: ruleModelTableInfo.value.data
};
} catch (error) {
// 验证失败
return false;
}
}
defineExpose({
getStepTwoConfigInfo
})
</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
......@@ -95,6 +95,8 @@ const tableInfo = ref({
});
}
});
}, () => {
proxy.$ElMessage.info("已取消");
})
}
}]
......
......@@ -72,18 +72,24 @@ const tableInfo = ref({
btns: (scope) => {
return [ {
label: "编辑", value: "edit", disabled: scope.row.status == 'R', click: (scope) => {
router.push({
name: 'anonTaskCreate',
query: {
guid: scope.row.guid,
taskName: scope.row.taskName
}
});
}
}, {
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
// }
// });
router.push({
name: 'anonResultView',
query: {
guid: scope.row.guid,
execGuid: scope.row.lastExecGuid,
taskName: scope.row.taskName
}
});
}
}, {
label: "删除", value: "delete", disabled: scope.row.status == 'R', click: (scope) => {
......
......@@ -983,14 +983,14 @@ const btnFormClick = (btn, type) => {
modelsDialogVisible.value = true;
if (!databaseList.value?.length) {
getDataSourceListData().then(() => {
if (databaseInfo.value == databaseList.value[0]?.guid ?? "") {
if (databaseInfo.value == (databaseList.value[0]?.guid ?? "")) {
dsFromTreeData.value = JSON.parse(JSON.stringify(currentDsFromTreeData.value));
} else {
databaseInfo.value = databaseList.value[0]?.guid ?? "";
}
})
} else {
if (databaseInfo.value == databaseList.value[0]?.guid ?? "") {
if (databaseInfo.value == (databaseList.value[0]?.guid ?? "")) {
dsFromTreeData.value = JSON.parse(JSON.stringify(currentDsFromTreeData.value));
} else {
databaseInfo.value = databaseList.value[0]?.guid ?? "";
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!