fd5bd230 by lihua

匿名化处理的接口联调

1 parent f2777d46
...@@ -67,6 +67,12 @@ export const getGeneralizeFileList = (params) => request({ ...@@ -67,6 +67,12 @@ export const getGeneralizeFileList = (params) => request({
67 data: params 67 data: params
68 }) 68 })
69 69
70 /** 获取泛化文件列表,包括名称和guid */
71 export const getGeneralizeFileNameList = () => request({
72 url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/generalize-file/name-list`,
73 method: 'post'
74 })
75
70 export const saveGeneralizeFile = (data) => request({ 76 export const saveGeneralizeFile = (data) => request({
71 url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/generalize-file/save`, 77 url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/generalize-file/save`,
72 method: 'post', 78 method: 'post',
...@@ -213,4 +219,105 @@ export const deleteAnonTask = (data) => request({ ...@@ -213,4 +219,105 @@ export const deleteAnonTask = (data) => request({
213 url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/delete`, 219 url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/delete`,
214 method: 'delete', 220 method: 'delete',
215 data 221 data
222 })
223
224 /** 保存匿名化任务 */
225 export const saveAnonTask = (params) => request({
226 url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/save`,
227 method: 'post',
228 data: params
229 })
230
231 /** 更新匿名化任务 */
232 export const updateAnonTask = (params) => request({
233 url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/update`,
234 method: 'put',
235 data: params
236 })
237
238 /** 获取匿名化任务详情 */
239 export const getAnonTaskDetail = (guid) => request({
240 url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/detail?guid=${guid}`,
241 method: 'get'
242 })
243
244 /** 执行匿名化任务 */
245 export const execAnonTask = (taskGuid) => request({
246 url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/exec-task?taskGuid=${taskGuid}`,
247 method: 'post'
248 })
249
250 /** 匿名化任务检验接口 */
251 export const anonTaskCheck = (params) => request({
252 url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/check`,
253 method: 'post',
254 data: params
255 })
256
257 /** 获取匿名化任务分析结果数据 */
258 export const getAnonAnalyzeResult = (execGuid) => request({
259 url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/get-anon-analyze?taskExecGuid=${execGuid}`,
260 method: 'get'
261 })
262
263 /** 获取匿名化任务分析结果统计分页数据 */
264 export const getAnonAnalyzePageData = (params) => request({
265 url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/page-anon-analyze-data`,
266 method: 'post',
267 data: params
268 })
269
270 /** 获取匿名化任务结果数据 */
271 export const getAnonPageData = (params) => request({
272 url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/page-anon-data`,
273 method: 'post',
274 data: params
275 })
276
277 /** 字段中文转英文 */
278 export const chTransformEn =(params)=> request({
279 url: `${import.meta.env.VITE_APP_COMMON_URL}/common/convert-field-ch-name`,
280 method: "post",
281 data: params,
282 });
283
284 /** 根据选择的连接池获取表列表 */
285 export const getDsTableByDs = (params) => request({
286 url: `${import.meta.env.VITE_APP_DATA_SOURCE_URL}/data-source/schema-table-page-list`,
287 method: 'post',
288 data: params
289 })
290
291 /** 根据数据表获取表字段结构 */
292 export const getDsTableFieldColumn = (params) => request({
293 url: `${import.meta.env.VITE_APP_DATA_SOURCE_URL}/data-source/table-column-list`,
294 method: 'post',
295 data: params
296 });
297
298 /** 根据数据表获取表数据 */
299 export const getDsTableSampleData = (params) => request({
300 url: `${import.meta.env.VITE_APP_DATA_SOURCE_URL}/data-source/table-data-preview-page`,
301 method: 'post',
302 data: params
303 });
304
305 /** 根据字段名称获取敏感数据识别标签 */
306 export const getLableByFieldName = (fieldName) => request({
307 url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/sensitive-data-task/get-label-by-field-name?fieldName=${fieldName}`,
308 method: 'get'
309 });
310
311 /** 验证样本数据 */
312 export const validateAnonRule = (params) => request({
313 url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/check`,
314 method: 'post',
315 data: params
316 })
317
318 /** 导出匿名化结果数据 */
319 export const exportAnonExecData = (params) => request({
320 url: `${import.meta.env.VITE_APP_ANONYMIZATION_BASEURL}/anon-task/export-anon-data?taskGuid=${params.taskGuid}&taskExecGuid=${params.execGuid}`,
321 method: 'get',
322 responseType: 'blob'
216 }) 323 })
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -13,6 +13,7 @@ const emits = defineEmits([ ...@@ -13,6 +13,7 @@ const emits = defineEmits([
13 "drawerBtnClick", 13 "drawerBtnClick",
14 "radioGroupChange", 14 "radioGroupChange",
15 "drawerSelectChange", 15 "drawerSelectChange",
16 'drawerInputChange',
16 "drawerTableSelectChange", 17 "drawerTableSelectChange",
17 "drawerTableBtnClick", 18 "drawerTableBtnClick",
18 "drawerTableToolBtnClick", 19 "drawerTableToolBtnClick",
...@@ -98,15 +99,15 @@ const onClickOutside = (e: any) => { ...@@ -98,15 +99,15 @@ const onClickOutside = (e: any) => {
98 emits("onClickOutside"); 99 emits("onClickOutside");
99 }; 100 };
100 101
101 const getDrawerConRef = (refName) => { 102 const getDrawerConRef = (refName, index = 0) => {
102 console.log(refName, '----------') 103 //console.log(refName, '----------')
103 if (refName == 'drawerTableRef') { 104 if (refName == 'drawerTableRef') {
104 const dtf = drawerTableRef.value[0] || drawerTableRef.value 105 const dtf = drawerTableRef.value[0] || drawerTableRef.value
105 return dtf?.tableRef 106 return dtf?.tableRef
106 } 107 }
107 // const drawerForm = drawerFormRef.value[0] || drawerFormRef.value; 108 // const drawerForm = drawerFormRef.value[0] || drawerFormRef.value;
108 if (refName == 'drawerFormRef') { 109 if (refName == 'drawerFormRef') {
109 const drawerForm = drawerFormRef.value?.[0] || drawerFormRef.value; 110 const drawerForm = drawerFormRef.value?.[index] || drawerFormRef.value;
110 return drawerForm 111 return drawerForm
111 } 112 }
112 } 113 }
...@@ -187,6 +188,10 @@ const radioGroupChange = (val, info) => { ...@@ -187,6 +188,10 @@ const radioGroupChange = (val, info) => {
187 emits("radioGroupChange", val, info); 188 emits("radioGroupChange", val, info);
188 }; 189 };
189 190
191 const formInputChange = (val, row, info) => {
192 emits("drawerInputChange", val, row, info);
193 }
194
190 const formSelectChange = (val, row, info) => { 195 const formSelectChange = (val, row, info) => {
191 emits("drawerSelectChange", val, row, info); 196 emits("drawerSelectChange", val, row, info);
192 }; 197 };
...@@ -319,10 +324,10 @@ const drawerClose = () => { ...@@ -319,10 +324,10 @@ const drawerClose = () => {
319 <template v-else> 324 <template v-else>
320 <Form ref="drawerFormRef" :itemList="con.formInfo.items" :formId="con.formInfo.id" :rules="con.formInfo.rules" 325 <Form ref="drawerFormRef" :itemList="con.formInfo.items" :formId="con.formInfo.id" :rules="con.formInfo.rules"
321 :col="con.formInfo.col" :readonly="con.formInfo.readonly" @radioGroupChange="radioGroupChange" 326 :col="con.formInfo.col" :readonly="con.formInfo.readonly" @radioGroupChange="radioGroupChange"
322 @selectChange="formSelectChange" @btnClick="formBtnClick"> 327 @selectChange="formSelectChange" @input-change="formInputChange" @btnClick="formBtnClick">
323 </Form> 328 </Form>
324 <!-- 插槽内容 --> 329 <!-- 插槽内容 -->
325 <slot></slot> 330 <slot v-if="con.showSlot !== false"></slot>
326 </template> 331 </template>
327 </div> 332 </div>
328 </template> 333 </template>
......
...@@ -189,27 +189,30 @@ const inputFocus = (event, item) => { ...@@ -189,27 +189,30 @@ const inputFocus = (event, item) => {
189 } 189 }
190 190
191 const inputChange = (val, row) => { 191 const inputChange = (val, row) => {
192 let decimalCnt = row.decimalCnt || 2;
192 if (row.inputType == "moneyNumber" || row.inputType == 'scoreNumber') { 193 if (row.inputType == "moneyNumber" || row.inputType == 'scoreNumber') {
193 let strArr = val.split("."); 194 let strArr = val.split(".");
194 if (strArr.length > 1) { 195 if (strArr.length > 1) {
195 let right = strArr[1]; 196 let right = strArr[1];
196 if (right === "" || right.length < 2) { 197 if (right === "" || right.length < decimalCnt) {
197 formInline.value[row.field] = val = parseFloat(val || 0).toFixed(2); 198 formInline.value[row.field] = val = parseFloat(val || 0).toFixed(decimalCnt);
198 } 199 }
199 } else { 200 } else {
200 formInline.value[row.field] = val = parseFloat(val || 0).toFixed(2); 201 formInline.value[row.field] = val = parseFloat(val || 0).toFixed(decimalCnt);
201 } 202 }
202 } 203 }
203 if (row.inputType == 'scoreNumber' && parseFloat(val) > 100) { 204 let max = row.max || 100;
205 if (row.inputType == 'scoreNumber' && parseFloat(val) > max) {
204 // 先去除非数字和小数点字符 206 // 先去除非数字和小数点字符
205 val = val.replace(/[^\d.]/g, ""); 207 val = val.replace(/[^\d.]/g, "");
206 // 限制最多保留两位小数 208 // 限制最多保留两位小数
207 val = val.replace(/\.{2,}/g, "."); 209 val = val.replace(/\.{2,}/g, ".");
208 val = val.replace(/^(\d+)\.(\d{2}).*$/, "$1.$2"); 210 let exp2 = new RegExp(`^(\d+)\.(\d{${decimalCnt}}).*$`, 'g');
211 val = val.replace(exp2, "$1.$2");
209 let num = parseFloat(val); 212 let num = parseFloat(val);
210 if (num > 100) { 213 if (num > max) {
211 num = 100; // 超过100时将其设置为100 214 num = max; // 超过100时将其设置为100
212 val = num.toFixed(2); // 保证显示为两位小数 215 val = num.toFixed(decimalCnt); // 保证显示为两位小数
213 } 216 }
214 formInline.value[row.field] = val; 217 formInline.value[row.field] = val;
215 } 218 }
...@@ -250,11 +253,15 @@ const inputEventChange = (val, item) => { ...@@ -250,11 +253,15 @@ const inputEventChange = (val, item) => {
250 } 253 }
251 return; 254 return;
252 } else if (item.inputType == 'scoreNumber') {//小于100的,保留两位小数 255 } else if (item.inputType == 'scoreNumber') {//小于100的,保留两位小数
256 let decimalCnt = item.decimalCnt || 2;
253 formInline.value[item.field] = formInline.value[item.field].toString().replace(/[^\d.]/g, "") 257 formInline.value[item.field] = formInline.value[item.field].toString().replace(/[^\d.]/g, "")
258 /** 连续两个小数点替换为一个小数点 */
254 formInline.value[item.field] = formInline.value[item.field].toString().replace(/\.{2,}/g, ".") 259 formInline.value[item.field] = formInline.value[item.field].toString().replace(/\.{2,}/g, ".")
255 formInline.value[item.field] = formInline.value[item.field].toString().replace(/^\D*(\d{0,3}(?:\.\d{0,2})?).*$/g, "$1") 260 /** 最多取三位整数,2位小数 */
261 let exp2 = new RegExp(`^\\D*(\\d{0,3}(?:\\.\\d{0,${decimalCnt}})?).*$`, 'g');
262 formInline.value[item.field] = formInline.value[item.field].toString().replace(exp2, "$1")
256 if (item.max != null && formInline.value[item.field] > item.max) { 263 if (item.max != null && formInline.value[item.field] > item.max) {
257 formInline.value[item.field] = item.max.toFixed(2); 264 formInline.value[item.field] = item.max.toFixed(decimalCnt);
258 } 265 }
259 return; 266 return;
260 } else if (item.inputType == 'moneyNumber') {// 单位是元,保留两位小数。 267 } else if (item.inputType == 'moneyNumber') {// 单位是元,保留两位小数。
...@@ -754,22 +761,31 @@ const panelChange = (scope, row) => { ...@@ -754,22 +761,31 @@ const panelChange = (scope, row) => {
754 <div class="input_panel" v-for="child in item.children"> 761 <div class="input_panel" v-for="child in item.children">
755 <span v-if="child.prepend">{{ child.prepend }}</span> 762 <span v-if="child.prepend">{{ child.prepend }}</span>
756 <el-input v-if="child.visible ?? true" v-model.trim="formInline[child.field]" 763 <el-input v-if="child.visible ?? true" v-model.trim="formInline[child.field]"
757 :disabled="child.disabled || readonly" :placeholder="child.placeholder" 764 :disabled="child.disabled || readonly" :placeholder="child.placeholder"
758 :maxlength="child.maxlength ?? ''" /> 765 :maxlength="child.maxlength ?? ''" @change="(val) => inputChange(val, child)"
766 @input="(val) => inputEventChange(val, child)" />
759 <span v-if="child.append">{{ child.append }}</span> 767 <span v-if="child.append">{{ child.append }}</span>
760 </div> 768 </div>
761 </div> 769 </div>
762 <div class="checkbox_input" :class="[item.col, { is_block: item.block }]" 770 <div class="checkbox_input" :class="[item.col, { is_block: item.block }]"
763 v-else-if="item.type == 'checkbox-input-item'"> 771 v-else-if="item.type == 'checkbox-input-item'">
764 <el-checkbox v-model="formInline[item.field]" :disabled="item.disabled || readonly" 772 <el-checkbox v-model="formInline[item.field]" :disabled="item.disabled || readonly"
765 @change="(val) => checkboxChange(val, item)" :true-label="item.trueValue ?? true" 773 @change="(val) => checkboxChange(val, item)" :true-value="item.trueValue ?? true"
766 :false-label="item.falseValue ?? false">{{ item.placeholder }}</el-checkbox> 774 :false-value="item.falseValue ?? false">{{ item.placeholder }}</el-checkbox>
767 <div class="input_panel" v-for="child in item.children"> 775 <div class="input_panel" v-for="child in item.children">
768 <el-form-item v-if="child.visible ?? true" :prop="child.field" 776 <el-form-item v-if="child.visible ?? true" :prop="child.field"
769 :validate-status="child.validateStatus ?? ''" :error="child.error" 777 :validate-status="child.validateStatus ?? ''" :error="child.error"
770 :class="[child.col, { is_block: child.block }]" :style="child.style ?? {}"> 778 :class="[child.col, { is_block: child.block }]" :style="child.style ?? {}">
771 <el-input v-model.trim="formInline[child.field]" :disabled="child.disabled || readonly" 779 <el-input v-if="child.type == 'input'" v-model.trim="formInline[child.field]" :disabled="child.disabled || readonly"
772 :placeholder="child.placeholder" :maxlength="child.maxlength ?? ''" /> 780 :placeholder="child.placeholder" :maxlength="child.maxlength ?? ''" @change="(val) => inputChange(val, child)"
781 @input="(val) => inputEventChange(val, child)" />
782 <el-select v-else-if="child.type == 'select'" v-model="formInline[child.field]"
783 :placeholder="child.placeholder" :disabled="child.disabled || readonly" :filterable="child.filterable" :clearable="child.clearable"
784 :teleported="child.teleported || true">
785 <el-option v-for="opts in child.options"
786 :label="child.props?.label ? opts[child.props.label] : opts.label"
787 :value="child.props?.value ? opts[child.props.value] : opts.value" :disabled="opts.disabled" />
788 </el-select>
773 </el-form-item> 789 </el-form-item>
774 </div> 790 </div>
775 </div> 791 </div>
......
...@@ -147,7 +147,7 @@ const routes: RouteRecordRaw[] = [ ...@@ -147,7 +147,7 @@ const routes: RouteRecordRaw[] = [
147 name: 'anonTaskCreate', 147 name: 'anonTaskCreate',
148 component: () => import('@/views/data_anonymization/anonTaskCreate.vue'), 148 component: () => import('@/views/data_anonymization/anonTaskCreate.vue'),
149 meta: { 149 meta: {
150 title: '新建匿名化处理任务', 150 title: '匿名化处理任务',
151 sidebar: false, 151 sidebar: false,
152 breadcrumb: false, 152 breadcrumb: false,
153 cache: true, 153 cache: true,
...@@ -161,6 +161,23 @@ const routes: RouteRecordRaw[] = [ ...@@ -161,6 +161,23 @@ const routes: RouteRecordRaw[] = [
161 } 161 }
162 } 162 }
163 }, 163 },
164 {
165 path: 'anonResultView',
166 name: 'anonResultView',
167 component: () => import('@/views/data_anonymization/anonResultView.vue'),
168 meta: {
169 title: '查看数据',
170 sidebar: false,
171 breadcrumb: false,
172 cache: true,
173 reuse: true
174 },
175 beforeEnter: (to, from) => {
176 if (to.query.guid) {
177 to.meta.title = `查看数据-${to.query.taskName}`;
178 }
179 }
180 },
164 ], 181 ],
165 }, 182 },
166 ] 183 ]
......
1 <route lang="yaml">
2 name: anonResultView
3 </route>
4
5 <script lang="ts" setup name="anonResultView">
6 import { ref } from "vue";
7 import {
8 getAnonPageData,
9 getAnonAnalyzeResult,
10 exportAnonExecData,
11 } from "@/api/modules/dataAnonymization";
12 import { calcColumnWidth } from "@/utils/index";
13 import Moment from 'moment';
14 import { TableColumnWidth } from "@/utils/enum";
15 import { ElMessage } from "element-plus";
16 import { commonPageConfig } from '@/components/PageNav/index';
17 import { download } from "@/utils/common";
18
19 const { proxy } = getCurrentInstance() as any;
20
21 const route = useRoute();
22
23 const props = defineProps({
24 isPage: {
25 default: true,
26 type: Boolean
27 },
28 execGuid: {
29 default: '',
30 type: String
31 }
32 });
33
34 const tableData: any = ref([]);
35 const tableDataLoading = ref(false);
36
37 const tableFields: any = ref([]);
38
39 const pageInfo: any = ref({
40 ...commonPageConfig,
41 })
42
43 const getData = () => {
44 tableData.value = [];
45 if (!tableFields.value?.length) {
46 return;
47 }
48 tableDataLoading.value = true;
49 getAnonPageData({
50 pageIndex: pageInfo.value.curr,
51 pageSize: pageInfo.value.limit,
52 taskExecGuid: props.isPage ? route.query.execGuid : props.execGuid,
53 }).then((res: any) => {
54 tableDataLoading.value = false;
55 if (res.code == proxy.$passCode) {
56 tableData.value = [];
57 res.data?.records?.forEach(d => {
58 let obj = {};
59 tableFields.value.forEach(t => {
60 obj[t.enName] = d.fieldValue?.[t.enName];
61 });
62 tableData.value.push(obj);
63 });
64 } else {
65 ElMessage.error(res.msg);
66 }
67 });
68 }
69
70 const getTextAlign = (field) => {
71 if (field.dataType === 'decimal' || field.dataType === 'int' || field.dataType == 'bit' || field.dataType == 'tinyint') {
72 return 'right';
73 }
74 return 'left'
75 }
76
77 /** otherWidth表示使用标题宽度时添加标题排序图标等宽度 */
78 const calcTableColumnWidth = (data: any[], prop, title, otherWidth = 0) => {
79 let d: any[] = [];
80 data.forEach((dt) => d.push(dt[prop]));
81 return calcColumnWidth(
82 d,
83 title,
84 {
85 fontSize: 14,
86 fontFamily: "SimSun",
87 },
88 {
89 fontSize: 14,
90 fontFamily: "SimSun",
91 },
92 otherWidth
93 );
94 };
95
96 /** 每列字段对应的列宽计算结果。 */
97 const originTableFieldColumn = ref({});
98
99 watch(
100 tableData,
101 (val: any[], oldVal) => {
102 if (!tableFields.value?.length) {
103 originTableFieldColumn.value = {};
104 return;
105 }
106 originTableFieldColumn.value = {};
107 tableFields.value.forEach((field, index) => {
108 originTableFieldColumn.value[field.enName] = calcTableColumnWidth(
109 val?.slice(0, 20) || [],
110 field.enName,
111 field.chName,
112 24
113 );
114 });
115 },
116 {
117 deep: true,
118 }
119 );
120
121 watch(() => props.execGuid, (val) => {
122 if (!val) {
123 return;
124 }
125 tableDataLoading.value = true;
126 getAnonAnalyzeResult(val).then((res: any) => {
127 tableDataLoading.value = false;
128 if (res.code == proxy.$passCode) {
129 let column = res.data?.column || {};
130 tableFields.value = column;
131 getData();
132 } else {
133 ElMessage.error(res.msg);
134 }
135 });
136 }, {
137 immediate: true
138 })
139
140 onBeforeMount(() => {
141 if (!props.isPage) {
142 return;
143 }
144 tableDataLoading.value = true;
145 getAnonAnalyzeResult(route.query.execGuid).then((res: any) => {
146 tableDataLoading.value = false;
147 if (res.code == proxy.$passCode) {
148 let column = res.data?.column || {};
149 tableFields.value = column;
150 getData();
151 } else {
152 ElMessage.error(res.msg);
153 }
154 });
155 });
156
157 const formatterPreviewDate = (row, info) => {
158 let enName = info.enName;
159 let v = row[enName];
160 if (v === 0) {
161 return v;
162 }
163 if (!v || v == 'null') {
164 return '--';
165 }
166 if (info.dataType === 'datetime') {
167 return Moment(v).format('YYYY-MM-DD HH:mm:ss');
168 }
169 if (info.dataType === 'date') {
170 if (isNaN(<any>(new Date(v)))) {
171 return Moment(parseInt(v)).format('YYYY-MM-DD');
172 } else {
173 return Moment(v).format('YYYY-MM-DD');
174 }
175 }
176 return v;
177 };
178
179 const pageChange = (info) => {
180 pageInfo.value.curr = Number(info.curr);
181 pageInfo.value.limit = Number(info.limit);
182 getData();
183 }
184
185 const promise: any = ref(null);
186
187 const exportData = () => {
188 if (promise) {
189 return;
190 }
191 promise.value = exportAnonExecData({
192 taskGuid: route.query.guid,
193 execGuid: route.query.execGuid
194 }).then((res: any) => {
195 promise.value = null;
196 if (res && !res.msg) {
197 download(res, route.query.taskName + '_匿名化数据.xlsx', 'excel')
198 } else {
199 res?.msg && ElMessage.error(res?.msg);
200 }
201 }).catch(() => {
202 promise.value = null;
203 })
204 }
205
206 </script>
207
208 <template>
209 <div class="table_tool_wrap" v-loading="tableDataLoading">
210 <el-button v-show="props.isPage" style="margin-bottom: 8px;" type="primary" @click="exportData"
211 v-preReClick>导出数据</el-button>
212 <el-table ref="tableRef" v-show="tableFields.length" :data="tableData" :highlight-current-row="true" stripe border
213 tooltip-effect="light" height="100%" row-key="guid" :style="{ width: '100%', height: 'calc(100% - 64px)' }">
214 <template v-for="(item, index) in (tableFields || [])">
215 <el-table-column :label="item.chName" :width="item.dataType === 'datetime'
216 ? TableColumnWidth.DATETIME
217 : item.dataType === 'date'
218 ? TableColumnWidth.DATE
219 : originTableFieldColumn[item.enName]
220 " :align="getTextAlign(item)" :header-align="getTextAlign(item)"
221 :formatter="(row) => formatterPreviewDate(row, item)" :show-overflow-tooltip="true">
222 </el-table-column>
223 </template>
224 </el-table>
225 <div v-show="!tableFields.length" class="empty-content">
226 <img src="../../assets/images/empty-data.png" :style="{ width: '168px', height: '96px' }" />
227 <div class="empty-text">暂无数据</div>
228 </div>
229 <PageNav :class="[pageInfo.type]" :pageInfo="pageInfo" @pageChange="pageChange" />
230 </div>
231 </template>
232
233 <style lang="scss" scoped>
234 .table_tool_wrap {
235 width: 100%;
236 height: 100%;
237 padding: 8px 16px 16px;
238
239 .tips_text {
240 font-size: 14px;
241 color: var(--el-text-color-tip);
242 display: block;
243 font-weight: normal;
244 margin-bottom: 8px;
245 line-height: 21px;
246 }
247
248 .el-table {
249 display: inline-block;
250 }
251
252 .empty-content {
253 display: flex;
254 align-items: center;
255 justify-content: center;
256 height: 100%;
257 width: 100%;
258 flex-direction: column;
259
260 .empty-text {
261 font-size: 14px;
262 color: #b2b2b2;
263 }
264 }
265 }
266 </style>
...\ No newline at end of file ...\ No newline at end of file
...@@ -4,16 +4,48 @@ ...@@ -4,16 +4,48 @@
4 4
5 <script lang="ts" setup name="anonTaskCreate"> 5 <script lang="ts" setup name="anonTaskCreate">
6 import { 6 import {
7 dataSourceTypeList 7 dataSourceTypeList,
8 getAnonTaskDetail,
9 getParamsList,
10 chTransformEn,
11 getAnonAnalyzeResult,
12 getAnonAnalyzePageData,
13 getDatabase,
14 getDsTableByDs,
15 getDsTableFieldColumn,
16 getDsTableSampleData,
17 saveAnonTask,
18 updateAnonTask,
19 exportAnonExecData,
8 } from '@/api/modules/dataAnonymization'; 20 } from '@/api/modules/dataAnonymization';
21 import {
22 parseAndDecodeUrl,
23 getDownFileSignByUrl,
24 obsDownloadRequest
25 } from "@/api/modules/obsService";
9 import useUserStore from "@/store/modules/user"; 26 import useUserStore from "@/store/modules/user";
27 import { useValidator } from '@/hooks/useValidator';
28 import { TableColumnWidth } from '@/utils/enum';
29 import { calcColumnWidth } from "@/utils/index";
30 import Moment from 'moment';
31 import anonTaskStepTwo from './anonTaskStepTwo.vue';
32 import * as XLSX from 'xlsx';
33 import { ElMessage } from 'element-plus';
34 import { isEqual, cloneDeep } from "lodash-es";
35 import { download } from "@/utils/common";
36 import anonResultView from './anonResultView.vue';
10 37
11 const { proxy } = getCurrentInstance() as any; 38 const { proxy } = getCurrentInstance() as any;
12 const userStore = useUserStore(); 39 const userStore = useUserStore();
13 const route = useRoute(); 40 const route = useRoute();
14 const router = useRouter(); 41 const router = useRouter();
15 const fullPath = route.fullPath; 42 const fullPath = route.fullPath;
16 43 const taskGuid = ref(route.query.guid);
44 /** 提交保存和编辑后的执行guid */
45 const taskExecGuid = ref('');
46 /** 是否执行结束 */
47 const isExecEnd = ref(false);
48 const { required } = useValidator();
17 const fullscreenLoading = ref(false); 49 const fullscreenLoading = ref(false);
18 50
19 const step = ref(0); 51 const step = ref(0);
...@@ -27,11 +59,17 @@ const stepsInfo = ref({ ...@@ -27,11 +59,17 @@ const stepsInfo = ref({
27 ] 59 ]
28 }) 60 })
29 61
30 const dataSourceList = ref([]); 62 /** 数据源列表 */
63 const dataSourceList: any = ref([]);
64
65 /** 数据源对应的数据表 */
66 const dsTableList: any = ref([]);
31 67
32 /** 数据共享类型字段列表 */ 68 /** 数据共享类型字段列表 */
33 const dataSharingTypeList = ref([]); 69 const dataSharingTypeList = ref([]);
34 70
71 const formRef = ref();
72
35 /** 数据选择的表单配置信息 */ 73 /** 数据选择的表单配置信息 */
36 const dataSelectInfoItems = ref([{ 74 const dataSelectInfoItems = ref([{
37 label: '任务名称', 75 label: '任务名称',
...@@ -49,8 +87,8 @@ const dataSelectInfoItems = ref([{ ...@@ -49,8 +87,8 @@ const dataSelectInfoItems = ref([{
49 type: 'select', 87 type: 'select',
50 placeholder: '请选择', 88 placeholder: '请选择',
51 field: 'dataSharingTypeCode', 89 field: 'dataSharingTypeCode',
52 default: '', 90 default: '01',
53 options: dataSharingTypeList, 91 options: dataSharingTypeList.value,
54 props: { 92 props: {
55 label: "label", 93 label: "label",
56 value: "value", 94 value: "value",
...@@ -60,6 +98,21 @@ const dataSelectInfoItems = ref([{ ...@@ -60,6 +98,21 @@ const dataSelectInfoItems = ref([{
60 clearable: true, 98 clearable: true,
61 visible: true, 99 visible: true,
62 }, { 100 }, {
101 label: '患者占总人口比',
102 type: 'input',
103 placeholder: '数值,支持小数点9位',
104 field: 'patientPopulationRate',
105 maxlength: 11,
106 min: 0,
107 max: 1,
108 inputType: 'scoreNumber',
109 decimalCnt: 9,
110 default: '',
111 required: true,
112 filterable: true,
113 clearable: true,
114 visible: true,
115 }, {
63 label: '数据来源', 116 label: '数据来源',
64 type: 'select', 117 type: 'select',
65 placeholder: '请选择', 118 placeholder: '请选择',
...@@ -72,7 +125,6 @@ const dataSelectInfoItems = ref([{ ...@@ -72,7 +125,6 @@ const dataSelectInfoItems = ref([{
72 }, 125 },
73 required: true, 126 required: true,
74 filterable: true, 127 filterable: true,
75 clearable: true,
76 visible: true, 128 visible: true,
77 }, { 129 }, {
78 label: '数据源', 130 label: '数据源',
...@@ -89,31 +141,694 @@ const dataSelectInfoItems = ref([{ ...@@ -89,31 +141,694 @@ const dataSelectInfoItems = ref([{
89 visible: true, 141 visible: true,
90 required: true 142 required: true
91 }, { 143 }, {
144 label: "数据表",
145 type: "select",
146 placeholder: "请选择",
147 field: "tableName",
148 options: dsTableList.value,
149 props: {
150 label: 'tableComment',
151 value: 'tableName'
152 },
153 default: '',
154 filterable: true,
155 clearable: true,
156 required: true,
157 }, {
92 label: '文件上传', 158 label: '文件上传',
93 tip: '支持扩展名:xlsx、xls、csv,文件大小不超过10MB', 159 tip: '支持扩展名:xlsx、xls、csv,文件大小不超过10MB',
94 type: 'upload-file', 160 type: 'upload-file',
95 accept: '.xlsx, .xls, .csv', 161 accept: '.xlsx, .xls, .csv',
96 limitSize: 10, 162 limitSize: 10,
163 limit: 1,
97 isExcel: true, 164 isExcel: true,
98 required: true, 165 required: true,
99 default: <any>[], 166 default: <any>[],
100 block: true, 167 block: false,
168 col: 'wid60',
101 visible: false, 169 visible: false,
102 field: 'file', 170 field: 'file',
103 }]); 171 }]);
104 172
105 const dataSelectInfoFormRules = ref({ 173 const dataSelectInfoFormRules = ref({
174 taskName: [required('请输入任务名称')],
175 dataSharingTypeCode: [required('请选择数据共享类型')],
176 patientPopulationRate: [required('请输入患者占总人口比')],
177 dataSourceGuid: [required('请选择数据源')],
178 tableName: [required('请选择数据表')],
179 file: [{
180 validator: (rule: any, value: any, callback: any) => {
181 if (!value?.length) {
182 callback(new Error('请上传文件'))
183 } else {
184 callback();
185 }
186 }, trigger: 'change'
187 }]
188 });
189
190 /** 最新选中的 */
191 const currDatasourceSelect: any = ref({});
192
193 const handleDataSelectFormSelectChange = async (val, row, formInfo) => {
194 if (row.field == 'dataSource') {
195 dataSelectInfoItems.value[4].visible = val == 1;
196 dataSelectInfoItems.value[5].visible = val == 1;
197 dataSelectInfoItems.value[6].visible = val == 2;
198 dataSelectInfoItems.value.forEach(d => {
199 d.default = formInfo[d.field];
200 if (d.field == 'file') {
201 d.default = !d.default ? [] : d.default;
202 }
203 });
204 sampleTableFields.value = [];
205 parseFileDataSum.value = [];
206 sampleTableData.value = [];
207 } else if (row.field == 'dataSourceGuid') {
208 if (!val) {
209 currDatasourceSelect.value = [];
210 sampleTableFields.value = [];
211 parseFileDataSum.value = [];
212 sampleTableData.value = [];
213 dataSelectInfoItems.value.forEach(d => {
214 d.default = formInfo[d.field];
215 if (d.field == 'file') {
216 d.default = !d.default ? [] : d.default;
217 } else if (d.field == 'tableName') {
218 d.options = dsTableList.value;
219 d.default = '';
220 }
221 });
222 return;
223 }
224 let dsInfo = currDatasourceSelect.value = dataSourceList.value.find(d => d.guid == val);
225 //清除数据表得值,重新获取下拉列表
226 const res: any = await getDsTableByDs({
227 pageSize: -1,
228 pageIndex: 1,
229 dataSourceGuid: val,
230 database: dsInfo.databaseNameEn,
231 databaseType: dsInfo.databaseType,
232 tableName: '',
233 hadFlag: false
234 });
235 if (res.code == proxy.$passCode) {
236 dsTableList.value = res.data?.records || [];
237 dataSelectInfoItems.value.forEach(d => {
238 d.default = formInfo[d.field];
239 if (d.field == 'file') {
240 d.default = !d.default ? [] : d.default;
241 } else if (d.field == 'tableName') {
242 d.options = dsTableList.value;
243 d.default = '';
244 }
245 });
246 } else {
247 proxy.$ElMessage.error(res.msg);
248 }
249 sampleTableFields.value = [];
250 parseFileDataSum.value = [];
251 sampleTableData.value = [];
252 } else if (row.field == 'tableName') {
253 if (!val) {
254 sampleTableFields.value = [];
255 sampleTableData.value = [];
256 return;
257 }
258 getDsTableFieldColumn({
259 pageSize: 50,
260 pageIndex: 1,
261 dataSourceGuid: currDatasourceSelect.value.guid,
262 database: currDatasourceSelect.value.databaseNameEn,
263 databaseType: currDatasourceSelect.value.databaseType,
264 tableName: val,
265 }).then((res: any) => {
266 if (res.code == proxy.$passCode) {
267 sampleTableFields.value = res.data?.map(d => {
268 d.fieldDataType = d.dataType;
269 d.enName = d.columnName;
270 d.chName = d.columnZhName;
271 return d;
272 }) || [];
273 } else {
274 ElMessage.error(res.msg);
275 }
276 });
277 /** 判断有抽样数据,需要查询接口 */
278 getSampleDataByDsTable();
279 }
280 }
281
282 const dataSimpleFormRef = ref();
283
284 /** 抽样数据预览 */
285 const dataSimpleFormItems = ref([{
286 label: '抽样开关',
287 type: 'switch',
288 field: 'enableSamplingRate',
289 default: 'N',
290 col: 'autoWidth',
291 activeValue: 'Y',
292 inactiveValue: 'N'
293 }, {
294 label: '抽样比例(%)',
295 type: 'input',
296 placeholder: '请输入',
297 field: 'samplingRate',
298 maxlength: 3,
299 min: 0, //可以是0条。万一只是想看下字段呢
300 max: 100,
301 inputType: 'integerNumber',
302 default: 10,
303 required: true,
304 filterable: true,
305 clearable: true,
306 visible: false,
307 }]);
106 308
309 const dataSimpleFormRules = ref({
310 samplingRate: [required('请填写抽样比例')],
107 }); 311 });
108 312
109 const changeStep = (val) => { 313 const oldSamplingRate = ref('10');
110 314
315 const handleDataSimpleFormSwitchChange = (val, info) => {
316 if (val == 'N') {
317 oldSamplingRate.value = info.samplingRate;
318 } else {
319 dataSimpleFormItems.value[1].default = oldSamplingRate.value || 10;
320 }
321 dataSimpleFormItems.value[1].visible = val == 'Y';
322 dataSimpleFormItems.value[0].default = info.enableSamplingRate || 'N';
323 if (formRef.value?.formInline?.file?.length) {
324 transferSampleData();
325 } else {
326 getSampleDataByDsTable();
327 }
111 } 328 }
112 329
113 const exportResult = () => { 330 /** 输入抽样比例值改变 */
331 const handleDataSimpleFormChange = (val) => {
332 if (formRef.value?.formInline?.file?.length) {
333 transferSampleData();
334 } else {
335 getSampleDataByDsTable();
336 }
337 }
338
339 /** 样本表格加载中 */
340 const sampleTableDataLoading = ref(false);
341
342 /** 样本表格的数据 */
343 const sampleTableData: any = ref([]);
344
345 /** 样本表格的字段 */
346 const sampleTableFields: any = ref([]);
347
348 /** otherWidth表示使用标题宽度时添加标题排序图标等宽度 */
349 const calcTableColumnWidth = (data: any[], prop, title, otherWidth = 0) => {
350 let d: any[] = [];
351 data.forEach((dt) => d.push(dt[prop]));
352 return calcColumnWidth(
353 d,
354 title,
355 {
356 fontSize: 14,
357 fontFamily: "SimSun",
358 },
359 {
360 fontSize: 14,
361 fontFamily: "SimSun",
362 },
363 otherWidth
364 );
365 };
366
367 /** 每列字段对应的列宽计算结果。 */
368 const originTableFieldColumn = ref({});
369
370 const getTextAlign = (field) => {
371 if (field.dataType === 'decimal' || field.dataType === 'int') {
372 return 'right';
373 }
374 return 'left'
375 }
376
377 watch(
378 sampleTableData,
379 (val: any[], oldVal) => {
380 if (!sampleTableFields.value?.length) {
381 originTableFieldColumn.value = {};
382 return;
383 }
384 originTableFieldColumn.value = {};
385 sampleTableFields.value.forEach((field, index) => {
386 originTableFieldColumn.value[field.enName] = calcTableColumnWidth(
387 val?.slice(0, 20) || [],
388 field.enName,
389 field.chName,
390 24
391 );
392 });
393 },
394 {
395 deep: true,
396 }
397 );
398
399 const formatterPreviewDate = (row, info) => {
400 let enName = info.enName;
401 let v = row[enName];
402 if (v === 0) {
403 return v;
404 }
405 if (!v) {
406 return v || '--';
407 }
408 if (info.dataType === 'datetime') {
409 return Moment(v).format('YYYY-MM-DD HH:mm:ss');
410 }
411 if (info.dataType === 'date') {
412 if (isNaN(<any>(new Date(v)))) {
413 return Moment(parseInt(v)).format('YYYY-MM-DD');
414 } else {
415 return Moment(v).format('YYYY-MM-DD');
416 }
417 }
418 return v;
419 };
420
421 /** 解析的总的表格数据,方便后面修改抽样比例时使用 */
422 const parseFileDataSum: any = ref([]);
423
424 const parseFileData = (fileRaw) => {
425 sampleTableDataLoading.value = true;
426 fileRaw.arrayBuffer().then(async (f) => {
427 const wb = XLSX.read(f, {
428 raw: false, cellDates: true
429 });
430 const sheet = wb.Sheets[wb.SheetNames[0]];
431 const json: any[] = XLSX.utils.sheet_to_json(sheet, { header: 1 });
432 if (json.length == 0) {
433 sampleTableFields.value = [];
434 sampleTableData.value = [];
435 } else {
436 const res = await chTransformEn(json[0]);
437 let fields = res.data || [];
438 sampleTableFields.value = fields?.map((j, index) => {
439 return {
440 index: index,
441 enName: j.enName + '',
442 chName: j.chName + '',
443 dataType: 'varchar'
444 }
445 }) || [];
446 parseFileDataSum.value = json;
447 /** 粗略算出字段类型 */
448 json.slice(1, 10).forEach((info, row) => {
449 json[0].forEach((name, col) => {
450 if (info[col] === "" || info[col] == null || sampleTableFields.value[col].dataType != 'varchar') {
451 return;
452 } else {
453 var cellRef = XLSX.utils.encode_cell({ r: row + 1, c: col });
454 var cell = sheet[cellRef];
455 let v = cell.w || info[col];
456 let isNum = cell.t == 'n';
457 if (isNum) {
458 if (v.includes('.') && sampleTableFields.value[col].dataType != 'decimal') {
459 sampleTableFields.value[col].dataType = 'decimal';
460 } else {
461 sampleTableFields.value[col].dataType = 'int';
462 }
463 }
464 }
465 });
466 })
467 transferSampleData();
468 }
469 sampleTableDataLoading.value = false;
470 });
471 }
472
473 /** 获取文件解析后根据抽样比例得出的表格数据 */
474 const transferSampleData = () => {
475 let samplingRate = dataSimpleFormRef.value?.formInline?.samplingRate;
476 if (parseFileDataSum.value.length > 1 && samplingRate) {
477 let totalCnt = parseFileDataSum.value.length - 1;
478 let cnt = Math.ceil(samplingRate * 0.01 * totalCnt) + 1;
479 sampleTableData.value = parseFileDataSum.value.slice(1, cnt > 1000 ? 1001 : cnt).map((info, row) => {
480 let object = {};
481 parseFileDataSum.value[0].forEach((chName, col) => {
482 let name = sampleTableFields.value[col].enName;
483 object[name] = info[col];
484 });
485 return object;
486 });
487 } else {
488 sampleTableData.value = [];
489 }
490 }
491
492 /** 获取选择的数据库表根据抽样比例得出的表格数据 */
493 const getSampleDataByDsTable = () => {
494 const tableName = formRef.value?.formInline?.tableName;
495 if (!currDatasourceSelect.value.guid || !tableName) {
496 sampleTableFields.value = [];
497 sampleTableData.value = [];
498 return;
499 }
500 let samplingRate = dataSimpleFormRef.value?.formInline?.samplingRate;
501 if (!samplingRate) {
502 sampleTableData.value = [];
503 return;
504 }
505 let totalCnt = dsTableList.value.find(t => t.tableName == tableName)?.tableRows || 0;
506 let cnt = Math.ceil(samplingRate * 0.01 * totalCnt);
507 if (!cnt) {
508 sampleTableData.value = [];
509 return;
510 }
511 sampleTableDataLoading.value = true;
512 getDsTableSampleData({
513 limitNum: cnt,
514 pageSize: cnt,
515 pageIndex: 1,
516 dataSourceGuid: currDatasourceSelect.value.guid,
517 database: currDatasourceSelect.value.databaseNameEn,
518 databaseType: currDatasourceSelect.value.databaseType,
519 tableName: tableName,
520 hadFlag: false,
521 }).then((res: any) => {
522 sampleTableDataLoading.value = false;
523 if (res.code == proxy.$passCode) {
524 sampleTableData.value = res.data?.datas || [];
525 } else {
526 sampleTableData.value = [];
527 ElMessage.error(res.msg);
528 }
529 });
530 }
531
532 const uploadFileChange = (file) => {
533 sampleTableFields.value = [];
534 sampleTableData.value = [];
535 if (!file.length) {
536 sampleTableFields.value = [];
537 sampleTableData.value = [];
538 return;
539 }
540 let fileRaw = file[0].file;
541 parseFileData(fileRaw);
542 }
543
544 /** 第二步的配置组件引用。 */
545 const anonTaskStepTwoRef = ref();
114 546
547 const changeStep = async (val) => {
548 if (val == 2) {
549 formRef.value?.ruleFormRef?.validate((valid) => {
550 if (valid) {
551 dataSimpleFormRef.value?.ruleFormRef?.validate((valid) => {
552 if (valid) {
553 step.value = val - 1;
554 stepsInfo.value.step = val - 1;
555 }
556 });
557 }
558 });
559 } else if (val == 3) {
560 // 保存并提交 TODO。需要加个 记录旧值的,用来判断新值和旧值,是否发生变化,若变化则需要调用保存接口之后,再进行下一步。
561 let configInfo = await anonTaskStepTwoRef.value?.getStepTwoConfigInfo();
562 if (!configInfo) {
563 return;
564 }
565 let saveParams: any = { ...formRef.value.formInline };
566 if (saveParams.file?.length) {
567 saveParams.filePath = {
568 name: saveParams.file[0].name,
569 url: saveParams.file[0].url
570 }
571 delete saveParams.file;
572 saveParams.dataSourceGuid = null;
573 saveParams.tableName = null;
574 } else {
575 saveParams.filePath = null;
576 }
577 let simpleFormInline = dataSimpleFormRef.value.formInline;
578 if (simpleFormInline.enableSamplingRate == 'Y') {
579 saveParams.samplingRate = simpleFormInline.samplingRate && parseInt(simpleFormInline.samplingRate);
580 } else {
581 saveParams.samplingRate = null;
582 }
583 let privacy = configInfo.anonPrivacyMode;
584 delete privacy.isKaNumber;
585 delete privacy.isRiskThreshold;
586 delete privacy.isTcField;
587 delete privacy.isLdField;
588 // 为空时为了跟原始值保持一致
589 privacy.kaNumber = (privacy.kaNumber && parseInt(privacy.kaNumber)) ?? null;
590 privacy.riskThreshold = (privacy.riskThreshold && parseFloat(privacy.riskThreshold)) ?? null;
591 privacy.tcFieldName = privacy.tcFieldName ?? null;
592 privacy.tcThreshold = (privacy.tcThreshold && parseFloat(privacy.tcThreshold)) ?? null;
593 privacy.ldFieldName = privacy.ldFieldName ?? null;
594 privacy.ldNumber = (privacy.ldNumber && parseInt(privacy.ldNumber)) ?? null;
595 Object.assign(saveParams, configInfo);
596 if (taskGuid.value) {
597 saveParams.guid = taskGuid.value;
598 }
599 if (isEqual(saveParams, oldAnonTaskValueInfo.value)) {
600 step.value = val - 1;
601 stepsInfo.value.step = val - 1;
602 return;
603 }
604 if (!taskGuid.value) { //保存
605 fullscreenLoading.value = true;
606 saveAnonTask(saveParams).then((res: any) => {
607 fullscreenLoading.value = false;
608 if (res.code == proxy.$passCode) {
609 taskGuid.value = res.data?.guid;
610 isExecEnd.value = false;
611 taskExecGuid.value = res.data?.lastExecGuid;
612 step.value = val - 1;
613 stepsInfo.value.step = val - 1;
614 oldAnonTaskValueInfo.value = saveParams;
615 } else {
616 ElMessage.error(res.msg);
617 }
618 });
619 } else { //更新
620 fullscreenLoading.value = true;
621 updateAnonTask(saveParams).then((res: any) => {
622 fullscreenLoading.value = false;
623 if (res.code == proxy.$passCode) {
624 isExecEnd.value = false;
625 taskExecGuid.value = res.data;
626 step.value = val - 1;
627 stepsInfo.value.step = val - 1;
628 oldAnonTaskValueInfo.value = saveParams;
629 } else {
630 ElMessage.error(res.msg);
631 }
632 })
633 }
634 } else if (val == 4) {
635 //下一步之后,调用分析结果。
636
637 getAnonAnalyzeResult(detailInfo.value.lastExecGuid).then((res: any) => {
638 debugger
639 });
640 getAnonAnalyzePageData({
641 pageSize: -1
642 }).then((res: any) => {
643 debugger
644 });
645 step.value = val - 1;
646 stepsInfo.value.step = val - 1;
647 } else if (val <= step.value) {
648 step.value = val - 1;
649 stepsInfo.value.step = val - 1;
650 }
115 } 651 }
116 652
653 const promise: any = ref(null);
654 const exportResult = () => {
655 promise.value = exportAnonExecData({
656 taskGuid: route.query.guid,
657 execGuid: route.query.execGuid
658 }).then((res: any) => {
659 promise.value = null;
660 if (res && !res.msg) {
661 download(res, route.query.taskName + '_匿名化数据.xlsx', 'excel')
662 } else {
663 res?.msg && ElMessage.error(res?.msg);
664 }
665 }).catch(() => {
666 promise.value = null;
667 })
668 }
669
670 /** 获取字段类型的数据字典 */
671 const fieldTypeList: any = ref([]);
672
673 /** 编辑时获取的匿名化任务的详情信息 */
674 const detailInfo: any = ref({});
675
676 /** 记录原始的值信息,防止上一步之后未修改数据时不调用接口 */
677 const oldAnonTaskValueInfo: any = ref({});
678
679 onBeforeMount(() => {
680 if (taskGuid.value) {
681 fullscreenLoading.value = true;
682 getAnonTaskDetail(taskGuid.value).then(async (res: any) => {
683 if (res?.code == proxy.$passCode) {
684 detailInfo.value = res.data || {};
685 oldAnonTaskValueInfo.value = {
686 guid: detailInfo.value.guid,
687 taskName: detailInfo.value.taskName,
688 dataSource: detailInfo.value.dataSource,
689 filePath: detailInfo.value.filePath && cloneDeep(detailInfo.value.filePath),
690 dataSourceGuid: detailInfo.value.dataSourceGuid,
691 tableName: detailInfo.value.tableName,
692 samplingRate: detailInfo.value.samplingRate,
693 patientPopulationRate: detailInfo.value.patientPopulationRate && typeof detailInfo.value.patientPopulationRate == 'number' ? detailInfo.value.patientPopulationRate?.toFixed(9) : detailInfo.value.patientPopulationRate,
694 dataSharingTypeCode: detailInfo.value.dataSharingTypeCode,
695 anonTaskRules: cloneDeep(detailInfo.value.anonTaskRules),
696 anonPrivacyMode: {
697 kaNumber: detailInfo.value.anonPrivacyMode?.kaNumber,
698 ldFieldName: detailInfo.value.anonPrivacyMode?.ldFieldName,
699 ldNumber: detailInfo.value.anonPrivacyMode?.ldNumber,
700 riskThreshold: detailInfo.value.anonPrivacyMode?.riskThreshold,
701 tcFieldName: detailInfo.value.anonPrivacyMode?.tcFieldName,
702 tcThreshold: detailInfo.value.anonPrivacyMode?.tcThreshold,
703 }
704 }
705 dataSelectInfoItems.value.forEach(d => {
706 d.default = detailInfo.value[d.field];
707 if (d.field == 'file') {
708 d.default = detailInfo.value.filePath ? [detailInfo.value.filePath] : [];
709 } else if (d.field == 'patientPopulationRate') {
710 if (d.default && typeof d.default == 'number') {
711 d.default = d.default.toFixed(d.decimalCnt);
712 }
713 }
714 });
715 if (detailInfo.value.samplingRate != null) {
716 dataSimpleFormItems.value[0].default = 'Y';
717 dataSimpleFormItems.value[1].visible = true;
718 dataSimpleFormItems.value[1].default = detailInfo.value.samplingRate;
719 } else {
720 dataSimpleFormItems.value[0].default = 'N';
721 dataSimpleFormItems.value[1].visible = false;
722 }
723 let dataSource = detailInfo.value.dataSource;
724 dataSelectInfoItems.value[4].visible = dataSource == 1;
725 dataSelectInfoItems.value[5].visible = dataSource == 1;
726 dataSelectInfoItems.value[6].visible = dataSource == 2;
727 //文件解析
728 if (dataSource == 2) {
729 let url = detailInfo.value.filePath?.url;
730 sampleTableDataLoading.value = true;
731 const refSignInfo: any = await getDownFileSignByUrl(parseAndDecodeUrl(url).fileName);
732 if (!refSignInfo?.data) {
733 fullscreenLoading.value = false;
734 refSignInfo?.msg && ElMessage.error(refSignInfo?.msg);
735 return;
736 }
737 obsDownloadRequest(refSignInfo?.data).then((res: any) => {
738 sampleTableDataLoading.value = false;
739 if (res && !res.msg) {
740 parseFileData(res);
741 } else {
742 res?.msg && ElMessage.error(res?.msg);
743 }
744 })
745 } else {
746 const res: any = await getDatabase({ connectStatus: 1 });
747 if (res?.code == proxy.$passCode) {
748 dataSourceList.value = res.data || [];
749 let item = dataSelectInfoItems.value.find(item => item.field == 'dataSourceGuid');
750 item && (item.options = dataSourceList.value);
751 } else {
752 proxy.$ElMessage.error(res.msg);
753 }
754 currDatasourceSelect.value = dataSourceList.value.find(d => d.guid == detailInfo.value.dataSourceGuid);
755 const tableRes: any = await getDsTableByDs({
756 pageSize: -1,
757 pageIndex: 1,
758 dataSourceGuid: detailInfo.value.dataSourceGuid,
759 database: currDatasourceSelect.value.databaseNameEn,
760 databaseType: currDatasourceSelect.value.databaseType,
761 tableName: '',
762 hadFlag: false
763 });
764 if (tableRes?.code == proxy.$passCode) {
765 dsTableList.value = tableRes.data?.records || [];
766 let item = dataSelectInfoItems.value.find(item => item.field == 'tableName');
767 item && (item.options = dsTableList.value);
768 } else {
769 proxy.$ElMessage.error(tableRes.msg);
770 }
771 getDsTableFieldColumn({
772 pageSize: 50,
773 pageIndex: 1,
774 dataSourceGuid: currDatasourceSelect.value.guid,
775 database: currDatasourceSelect.value.databaseNameEn,
776 databaseType: currDatasourceSelect.value.databaseType,
777 tableName: detailInfo.value.tableName,
778 }).then((res: any) => {
779 if (res.code == proxy.$passCode) {
780 sampleTableFields.value = res.data?.map(d => {
781 d.fieldDataType = d.dataType;
782 d.enName = d.columnName;
783 d.chName = d.columnZhName;
784 return d;
785 }) || [];
786 } else {
787 ElMessage.error(res.msg);
788 }
789 });
790 /** 判断有抽样数据,需要查询接口 */
791 getSampleDataByDsTable();
792 }
793 fullscreenLoading.value = false;
794 } else {
795 fullscreenLoading.value = false;
796 proxy.$ElMessage.error(res.msg);
797 }
798 });
799 } else {
800 getDatabase({ connectStatus: 1 }).then((res: any) => {
801 if (res.code == proxy.$passCode) {
802 dataSourceList.value = res.data || [];
803 let item = dataSelectInfoItems.value.find(item => item.field == 'dataSourceGuid');
804 item && (item.options = dataSourceList.value);
805 } else {
806 proxy.$ElMessage.error(res.msg);
807 }
808 })
809 }
810 getParamsList({
811 dictType: "数据共享类型",
812 }).then((res: any) => {
813 if (res?.code == proxy.$passCode) {
814 dataSharingTypeList.value = res.data || [];
815 let item = dataSelectInfoItems.value.find(item => item.field == 'dataSharingTypeCode');
816 item && (item.options = dataSharingTypeList.value);
817 } else {
818 proxy.$ElMessage.error(res.msg);
819 }
820 });
821 getParamsList({
822 dictType: "字段类型",
823 }).then((res: any) => {
824 if (res?.code == proxy.$passCode) {
825 fieldTypeList.value = res.data || [];
826 } else {
827 proxy.$ElMessage.error(res.msg);
828 }
829 });
830 })
831
117 const cancelTask = () => { 832 const cancelTask = () => {
118 proxy.$openMessageBox("当前页面尚未保存,确定放弃修改吗?", () => { 833 proxy.$openMessageBox("当前页面尚未保存,确定放弃修改吗?", () => {
119 userStore.setTabbar(userStore.tabbar.filter((tab: any) => tab.fullPath !== fullPath)); 834 userStore.setTabbar(userStore.tabbar.filter((tab: any) => tab.fullPath !== fullPath));
...@@ -135,7 +850,50 @@ const cancelTask = () => { ...@@ -135,7 +850,50 @@ const cancelTask = () => {
135 </div> 850 </div>
136 <div class="operator_panel_wrap" v-show="step == 0"> 851 <div class="operator_panel_wrap" v-show="step == 0">
137 <ContentWrap id="id-baseInfo" title="数据选择" description="" style="margin-top: 8px;"> 852 <ContentWrap id="id-baseInfo" title="数据选择" description="" style="margin-top: 8px;">
138 <Form ref="formRef" :itemList="dataSelectInfoItems" :rules="dataSelectInfoFormRules" formId="model-select-edit" col="col3" /> 853 <Form ref="formRef" :itemList="dataSelectInfoItems" :rules="dataSelectInfoFormRules"
854 formId="model-select-edit" col="col3 custom-form" @select-change="handleDataSelectFormSelectChange"
855 @uploadFileChange="uploadFileChange" />
856 </ContentWrap>
857 <ContentWrap id="id-previewData" title="数据抽样预览" description="" style="margin-top: 16px;">
858 <Form ref="dataSimpleFormRef" :itemList="dataSimpleFormItems" :rules="dataSimpleFormRules"
859 formId="data-simple-edit" col="col3 fixwidth-form" @switch-change="handleDataSimpleFormSwitchChange"
860 @input-change="handleDataSimpleFormChange" />
861 <div class="table-v2-main" v-show="dataSimpleFormRef?.formInline?.enableSamplingRate == 'Y'"
862 v-loading="sampleTableDataLoading">
863 <el-table ref="tableRef" v-show="sampleTableFields.length" :data="sampleTableData"
864 :highlight-current-row="true" stripe border tooltip-effect="light" height="100%" row-key="guid"
865 :style="{ width: '100%', height: '240px' }">
866 <el-table-column label="序号" type="index" width="56px" align="center"
867 show-overflow-tooltip></el-table-column>
868 <template v-for="(item, index) in (sampleTableFields || [])">
869 <el-table-column :label="item.chName" :width="item.dataType === 'datetime'
870 ? TableColumnWidth.DATETIME
871 : item.dataType === 'date'
872 ? TableColumnWidth.DATE
873 : originTableFieldColumn[item.enName]
874 " :align="getTextAlign(item)" :header-align="getTextAlign(item)"
875 :formatter="(row) => formatterPreviewDate(row, item)" :show-overflow-tooltip="true">
876 </el-table-column>
877 </template>
878 </el-table>
879 <div v-show="!sampleTableFields.length" class="main-placeholder">
880 <img src="../../assets/images/no-data.png" :style="{ width: '96px', height: '96px' }" />
881 <div class="empty-text">暂无抽样数据</div>
882 </div>
883 </div>
884 </ContentWrap>
885 </div>
886 <anonTaskStepTwo ref="anonTaskStepTwoRef" v-show="step == 1" :anonTaskRules="detailInfo.anonTaskRules" :isFile="formRef?.formInline?.file?.length > 0"
887 :anonPrivacyMode="detailInfo.anonPrivacyMode" :fieldTypeList="fieldTypeList" :fieldNameList="sampleTableFields">
888 </anonTaskStepTwo>
889 <div class="operator_panel_wrap" v-show="step == 2">
890 <ContentWrap id="analysis-result" title="匿名结果分析" description="" style="margin-top: 8px;">
891
892 </ContentWrap>
893 </div>
894 <div class="operator_panel_wrap" v-show="step == 3">
895 <ContentWrap id="analysis-result" title="匿名化数据结果" description="" style="margin-top: 8px;">
896 <anonResultView :is-page="false" :execGuid="isExecEnd ? taskExecGuid : ''"></anonResultView>
139 </ContentWrap> 897 </ContentWrap>
140 </div> 898 </div>
141 </div> 899 </div>
...@@ -144,9 +902,16 @@ const cancelTask = () => { ...@@ -144,9 +902,16 @@ const cancelTask = () => {
144 <el-button @click="cancelTask">取消</el-button> 902 <el-button @click="cancelTask">取消</el-button>
145 <el-button type="primary" @click="changeStep(2)">下一步</el-button> 903 <el-button type="primary" @click="changeStep(2)">下一步</el-button>
146 </template> 904 </template>
905 <template v-else-if="step == 1">
906 <el-button @click="changeStep(1)">上一步</el-button>
907 <el-button type="primary" @click="changeStep(3)">保存并下一步</el-button>
908 </template>
909 <template v-else-if="step == 2">
910 <el-button @click="changeStep(2)">上一步</el-button>
911 <el-button type="primary" @click="changeStep(4)">下一步</el-button>
912 </template>
147 <template v-else> 913 <template v-else>
148 <el-button @click="cancelTask">取消</el-button> 914 <el-button type="primary" @click="changeStep(2)">上一步</el-button>
149 <el-button type="primary" @click="changeStep(1)">上一步</el-button>
150 <el-button type="primary" v-preReClick @click="exportResult">导出</el-button> 915 <el-button type="primary" v-preReClick @click="exportResult">导出</el-button>
151 </template> 916 </template>
152 </div> 917 </div>
...@@ -181,4 +946,81 @@ const cancelTask = () => { ...@@ -181,4 +946,81 @@ const cancelTask = () => {
181 padding: 0 16px; 946 padding: 0 16px;
182 overflow: hidden auto; 947 overflow: hidden auto;
183 } 948 }
949
950 .operator_panel_wrap {
951 padding-bottom: 12px;
952 }
953
954 :deep(.custom-form) {
955 align-items: flex-start;
956
957 .wid60.el-form-item {
958 width: calc(66.66% - 12px);
959 }
960 }
961
962 :deep(.fixwidth-form) {
963 width: 500px;
964
965 .autoWidth.el-form-item {
966 width: 80px;
967 }
968 }
969
970 .table-v2-main {
971 width: 100%;
972 height: 240px;
973
974
975 .main-placeholder {
976 height: 100%;
977 display: flex;
978 justify-content: center;
979 align-items: center;
980 flex-direction: column;
981
982 .empty-text {
983 font-size: 14px;
984 color: #b2b2b2;
985 }
986 }
987 }
988
989 :deep(.el-table-v2) {
990
991 .el-table-v2__main {
992 border: 1px solid #d9d9d9;
993 background: #fff;
994 }
995
996 .el-table-v2__body tr.hover-row.el-table-v2__row--striped.current-row>td.el-table-v2__cell {
997 background-color: var(--el-table-row-hover-bg-color);
998 }
999
1000 .el-table-v2__body tr.current-row>td.el-table-v2__cell {
1001 background-color: var(--el-table-current-row-bg-color);
1002 }
1003
1004 .el-table-v2__header {
1005 width: 100% !important;
1006 }
1007
1008 .el-table-v2__header-cell,
1009 .el-table-v2__row-cell {
1010 border-right: 1px #d9d9d9 solid;
1011 }
1012
1013 .el-table-v2__header-cell-text {
1014 color: #000;
1015 font-weight: normal;
1016 }
1017
1018 .el-table-v2__empty {
1019 display: none !important;
1020 }
1021
1022 .el-table-v2__header-row {
1023 border-bottom: 1px solid #d9d9d9;
1024 }
1025 }
184 </style> 1026 </style>
...\ No newline at end of file ...\ No newline at end of file
......
1 <route lang="yaml">
2 name: anonTaskStepTwo
3 </route>
4
5 <script lang="ts" setup name="anonTaskStepTwo">
6 import { TableColumnWidth } from '@/utils/enum';
7 import { CirclePlus, Delete } from '@element-plus/icons-vue';
8 import {
9 getParamsList,
10 getGeneralizeFileNameList,
11 getLableByFieldName,
12 validateAnonRule,
13 } from '@/api/modules/dataAnonymization';
14 import { useValidator } from '@/hooks/useValidator';
15
16 const props = defineProps({
17 fieldTypeList: {
18 default: [],
19 type: Array<any>
20 },
21 isFile: {
22 default: false,
23 type: Boolean
24 },
25 fieldNameList: {
26 default: [],
27 type: Array<any>
28 },
29 anonTaskRules: {
30 default: [],
31 type: Array<any>
32 },
33 anonPrivacyMode: {
34 default: {},
35 type: Object
36 }
37 })
38
39 const { required } = useValidator();
40 const { proxy } = getCurrentInstance() as any;
41
42 const drawerRef = ref();
43
44 /** 泛化文件列表 */
45 const generalizeFileNameList: any = ref([]);
46
47 /** 脱敏规则字典列表 */
48 const desensitiveRuleTypeList: any = ref([]);
49
50 /** 标签类型字典列表 */
51 const labelTypeList: any = ref([]);
52
53 /** 加密算法字典列表 */
54 const hashMethodList = ref([]);
55
56 /** 当前正在编辑的表格索引 */
57 const currTableRowIndex: any = ref(null);
58
59 const ruleModelTableInfo = ref({
60 id: 'rule-model-table',
61 loading: false,
62 minHeight: '200px',
63 nodeKey: 'guid',
64 height: '200px',
65 fields: [
66 { label: "序号", type: "index", width: TableColumnWidth.INDEX, align: "center" },
67 { label: "字段中文名称", field: "fieldChName", width: 150 },
68 { label: "字段英文名称", field: "fieldName", width: 150 },
69 { label: "字段类型", field: "fieldTypeName", width: 120 },
70 { label: "数据类型", field: "dataTypeName", width: 120 },
71 {
72 label: "脱敏方式", field: "desensitiveRule", width: 120, getName: (scope) => {
73 let rule = scope.row.desensitiveRule;
74 return rule ? rule : (scope.row.generalizeFileGuid ? '泛化' : '--');
75 }
76 },
77 ],
78 data: <any>[],
79 showPage: false,
80 actionInfo: {
81 label: "操作",
82 type: "btn",
83 width: 100,
84 fixed: 'right',
85 btns: [
86 {
87 label: "编辑", value: "ruleEdit", click: (scope) => {
88 currTableRowIndex.value = scope.$index;
89 drawerInfo.value.visible = true;
90 drawerInfo.value.type = 'edit';
91 drawerInfo.value.header.title = '编辑字段脱敏规则';
92 let row = scope.row;
93 fieldRulesFormItems.value.forEach(item => {
94 item.default = row[item.field];
95 if (item.field == 'encryptionAlgorithmCode' || item.field == 'salted') {
96 item.visible = row.desensitiveRuleCode == 'HASH';
97 } else if (item.field == 'decimalPlaces') {
98 item.visible = row.desensitiveRuleCode == 'ROUNDING';
99 }
100 });
101 let fieldNameList = props.fieldNameList.map(f => {
102 if (f.enName != row.fieldName && ruleModelTableInfo.value.data.some(d => d.fieldName == f.enName)) {
103 f.disabled = true;
104 } else {
105 f.disabled = false;
106 }
107 return f;
108 });
109 fieldRulesFormItems.value[0].options = fieldNameList;
110 fieldRulesFormInfo.value.formInfo.items = fieldRulesFormItems.value;
111 drawerInfo.value.container.contents[0] = fieldRulesFormInfo.value;
112 desensitiveRuleDetail.value = {
113 dissembleType: 1, /** 1从左往右,2.从右往左 */
114 ruleDetails: <any>[{
115 digitType: 1,
116 }]
117 };
118 charReplaceRuleDetail.value = {
119 replaceType: 1, /** 1从左往右,2.从右往左 */
120 ruleDetails: <any>[{
121 digitType: 1,
122 ruleType: 1
123 }]
124 };
125 rangeReplaceRuleDetails.value = [{
126 lowOperator: '≤',
127 fieldChName: row.fieldChName,
128 upperOperator: '≤'
129 }];
130
131 if (row.desensitiveRuleCode == 'DISSEMBLE') {
132 desensitiveRuleDetail.value = row.desensitiveRuleDetail || {};
133 } else if (row.desensitiveRuleCode == 'CHARREPLACE') {
134 charReplaceRuleDetail.value = row.desensitiveRuleDetail || {};
135 } else if (row.desensitiveRuleCode == 'RANGEREPLACE') {
136 rangeReplaceRuleDetails.value = row.desensitiveRuleDetail?.ruleDetails?.map(rd => {
137 rd.fieldChName = row.fieldChName;
138 return rd;
139 }) || [];
140 }
141 if (!row.desensitiveRuleCode) {
142 drawerInfo.value.container.contents = [fieldRulesFormInfo.value];
143 } else {
144 drawerInfo.value.container.contents = [fieldRulesFormInfo.value, fieldRulesEndFormInfo.value];
145 }
146 }
147 },
148 {
149 label: "删除", value: "delete", click: (scope) => {
150 proxy.$openMessageBox("此操作将永久删除, 是否继续?", () => {
151 let fieldName = scope.row.fieldName;
152 ruleModelTableInfo.value.data.splice(scope.row.index, 1);
153 updatePrivacyFormFieldsOptions(fieldName);
154 // 同步去掉隐私模型设置中的字段。
155 proxy.$ElMessage.success('删除成功');
156 }, () => {
157 proxy.$ElMessage.info("已取消");
158 })
159 }
160 },
161 ],
162 }
163 })
164
165 /** 字段脱敏规则表单配置 */
166 const fieldRulesFormItems = ref([{
167 label: '选择字段',
168 type: 'select',
169 placeholder: '请选择',
170 field: 'fieldName',
171 default: '',
172 options: props.fieldNameList,
173 props: {
174 label: 'chName',
175 value: 'enName',
176 disabled: 'disabled'
177 },
178 filterable: true,
179 clearable: false,
180 required: true
181 }, {
182 label: '字段类型',
183 type: 'select',
184 placeholder: '请选择',
185 field: 'fieldTypeCode',
186 default: 'varchar',
187 options: props.fieldTypeList,
188 props: {
189 label: 'label',
190 value: 'value'
191 },
192 disabled: true,
193 clearable: true,
194 required: true
195 }, {
196 label: '数据类型',
197 type: 'select',
198 placeholder: '请选择',
199 field: 'dataTypeCode',
200 default: '',
201 options: labelTypeList.value,
202 props: {
203 label: 'label',
204 value: 'value'
205 },
206 filterable: true,
207 clearable: true,
208 required: false
209 }, {
210 label: 'K匿名泛化',
211 type: 'select',
212 placeholder: '请选择',
213 field: 'generalizeFileGuid',
214 default: '',
215 options: generalizeFileNameList.value,
216 props: {
217 label: 'generalizeFileName',
218 value: 'guid'
219 },
220 filterable: true,
221 clearable: true,
222 required: false
223 }, {
224 label: '脱敏规则',
225 type: 'select',
226 placeholder: '请选择',
227 field: 'desensitiveRuleCode',
228 default: 'DISSEMBLE',
229 options: desensitiveRuleTypeList.value,
230 props: {
231 label: 'label',
232 value: 'value'
233 },
234 filterable: true,
235 clearable: true,
236 required: false
237 }, {
238 label: '加密算法',
239 type: 'select',
240 placeholder: '请选择',
241 field: 'encryptionAlgorithmCode',
242 default: '',
243 options: hashMethodList.value,
244 props: {
245 label: 'label',
246 value: 'value'
247 },
248 filterable: true,
249 clearable: true,
250 visible: false,
251 required: true
252 }, {
253 label: '加盐值',
254 type: 'input',
255 placeholder: '请输入0~9',
256 field: 'salted',
257 maxlength: 1,
258 min: 0,
259 max: 9,
260 inputType: 'integerNumber',
261 default: 5,
262 required: true,
263 filterable: true,
264 clearable: true,
265 visible: false,
266 }, {
267 label: '保留小数',
268 type: 'input',
269 placeholder: '请输入0~5',
270 field: 'decimalPlaces',
271 maxlength: 1,
272 min: 0,
273 max: 5,
274 inputType: 'integerNumber',
275 default: 2,
276 required: true,
277 filterable: true,
278 clearable: true,
279 visible: false,
280 }]);
281
282 const fieldRulesFormRules = ref({
283 fieldName: [required('请选择字段')],
284 // dataTypeCode: [required('请选择数据类型')], //不填标识非敏感标识。
285 // desensitiveRuleCode: [required('请选择脱敏规则')], 脱敏规则和泛化文件选择一个即可。二者必选其一,可以两者共存。
286 encryptionAlgorithmCode: [required('请选择加密算法')],
287 salted: [required('请输入加盐值')],
288 decimalPlaces: [required('请输入保留小数')]
289 });
290
291 const fieldRulesFormInfo = ref({
292 type: "form",
293 title: "",
294 col: "span",
295 formInfo: {
296 id: "add-class-form",
297 readonly: false,
298 items: fieldRulesFormItems.value,
299 rules: fieldRulesFormRules.value,
300 },
301 });
302
303 const fieldRulesEndFormInfo = ref({
304 type: "form",
305 title: "",
306 col: "mt8",
307 showSlot: false,
308 formInfo: {
309 id: "add-rules-end-form",
310 readonly: false,
311 items: [{
312 label: '样本数据',
313 type: 'input',
314 placeholder: '请输入样本',
315 field: 'testData',
316 default: '',
317 required: false,
318 clearable: true,
319 block: true,
320 col: 'mb8',
321 validateBtn: {
322 value: 'validate',
323 label: '验证',
324 click: () => {
325 let formInline = drawerRef.value?.getDrawerConRef('drawerFormRef')?.formInline;
326 let formInline1 = drawerRef.value?.getDrawerConRef('drawerFormRef', 1)?.formInline;
327 if (!formInline1.testData) {
328 proxy.$ElMessage.error('样本数据不能为空');
329 return;
330 }
331 validateAnonRule({
332 desensitiveRuleCode: formInline.desensitiveRuleCode,
333 value: formInline1.testData,
334 desensitiveRuleDetail: getDesensitiveRuleDetailInfo(formInline)
335 }).then((res: any) => {
336 if (res?.code == proxy.$passCode) {
337 let result = res.data;
338 fieldRulesEndFormInfo.value.formInfo.items[0].default = formInline1.testData;
339 fieldRulesEndFormInfo.value.formInfo.items[1].default = formInline.desensitiveRuleCode == 'BLANK' ? ' ' : result;
340 } else {
341 proxy.$ElMessage.error(res.msg);
342 }
343 })
344 }
345 }
346 }, {
347 label: '脱敏效果',
348 type: 'input',
349 placeholder: '点击验证后展示脱敏效果',
350 field: 'validateResult',
351 default: '',
352 block: true,
353 required: false,
354 disabled: true
355 }],
356 rules: {},
357 },
358 });
359
360 /** 获取规则配置详情。 */
361 const getDesensitiveRuleDetailInfo = (formInline) => {
362 let desensitiveRuleDetailInfo: any = {};
363 if (formInline.desensitiveRuleCode == 'DISSEMBLE') {
364 desensitiveRuleDetailInfo = desensitiveRuleDetail.value;
365 } else if (formInline.desensitiveRuleCode == 'CHARREPLACE') {
366 desensitiveRuleDetailInfo = charReplaceRuleDetail.value;
367 } else if (formInline.desensitiveRuleCode == 'RANGEREPLACE') {
368 desensitiveRuleDetailInfo = {
369 ruleDetails: rangeReplaceRuleDetails.value
370 };
371 } else if (formInline.desensitiveRuleCode == 'ROUNDING') {
372 desensitiveRuleDetailInfo = {
373 decimalPlaces: formInline.decimalPlaces
374 }
375 } else if (formInline.desensitiveRuleCode == 'HASH') {
376 desensitiveRuleDetailInfo = {
377 encryptionAlgorithmCode: formInline.encryptionAlgorithmCode,
378 salted: formInline.salted
379 }
380 }
381 return desensitiveRuleDetailInfo;
382 }
383
384 const drawerInfo = ref({
385 visible: false,
386 direction: 'rtl',
387 size: 540,
388 header: {
389 title: '添加字段脱敏规则',
390 },
391 type: '',
392 container: {
393 contents: [fieldRulesFormInfo.value, fieldRulesEndFormInfo.value],
394 },
395 footer: {
396 visible: true,
397 btns: [
398 { type: 'default', label: '取消', value: 'cancel' },
399 { type: 'primary', label: '确定', value: 'save', loading: false },
400 ]
401 }
402 });
403
404 const addRowRules = () => {
405 drawerInfo.value.visible = true;
406 drawerInfo.value.type = 'add';
407 drawerInfo.value.header.title = '添加字段脱敏规则';
408 fieldRulesFormItems.value.forEach(item => {
409 if (item.field == 'fieldTypeCode') {
410 item.default = 'varchar';
411 } else if (item.field == 'desensitiveRuleCode') {
412 item.default = 'DISSEMBLE';
413 } else if (item.field == 'encryptionAlgorithmCode' || item.field == 'salted') {
414 item.visible = false;
415 item.default = '';
416 item.field == 'salted' && (item.default = 5);
417 } else if (item.field == 'decimalPlaces') {
418 item.visible = false;
419 item.default = 2;
420 } else {
421 item.default = '';
422 }
423 });
424 let fieldNameList = props.fieldNameList.map(f => {
425 if (ruleModelTableInfo.value.data.some(d => d.fieldName == f.enName)) {
426 f.disabled = true;
427 } else {
428 f.disabled = false;
429 }
430 return f
431 });
432 fieldRulesFormItems.value[0].options = fieldNameList;
433 fieldRulesFormInfo.value.formInfo.items = fieldRulesFormItems.value;
434 desensitiveRuleDetail.value = {
435 dissembleType: 1, /** 1从左往右,2.从右往左 */
436 ruleDetails: <any>[{
437 digitType: 1,
438 }]
439 };
440 charReplaceRuleDetail.value = {
441 replaceType: 1, /** 1从左往右,2.从右往左 */
442 ruleDetails: <any>[{
443 digitType: 1,
444 ruleType: 1
445 }]
446 };
447 rangeReplaceRuleDetails.value = [{
448 lowOperator: '≤',
449 fieldChName: '',
450 upperOperator: '≤'
451 }];
452 drawerInfo.value.container.contents = [fieldRulesFormInfo.value, fieldRulesEndFormInfo.value];
453 }
454
455 const drawerBtnClick = async (btn, info) => {
456 if (btn.value == 'cancel') {
457 drawerInfo.value.visible = false
458 } else {
459 if (!info.generalizeFileGuid && !info.desensitiveRuleCode) {
460 proxy.$ElMessage.error('K匿名泛化与脱敏规则不能同时为空');
461 return;
462 }
463 if ((info.fieldTypeCode == 'int' || info.fieldTypeCode == 'decimal' || info.fieldTypeCode == 'tinyint')) {
464 if (info.desensitiveRuleCode == 'ROUNDING') {
465 proxy.$ElMessage.error('数值类型字段的脱敏规则不能设置取整');
466 return;
467 }
468 if (info.desensitiveRuleCode == 'RANGEREPLACE') {
469 proxy.$ElMessage.error('数值类型字段的脱敏规则不能设置区间替换');
470 return;
471 }
472 }
473 drawerInfo.value.footer.btns[1].loading = true;
474 let desensitiveRuleDetailInfo = getDesensitiveRuleDetailInfo(info);
475 // 脱敏规则为掩盖,字符,区间替换存在时需要调用接口检验
476 if (info.desensitiveRuleCode == 'DISSEMBLE' || info.desensitiveRuleCode == 'CHARREPLACE' || info.desensitiveRuleCode == 'RANGEREPLACE') {
477 let res: any = await validateAnonRule({
478 desensitiveRuleCode: info.desensitiveRuleCode,
479 value: '',
480 desensitiveRuleDetail: desensitiveRuleDetailInfo
481 })
482 if (res?.code != proxy.$passCode) {
483 proxy.$ElMessage.error(res.msg);
484 }
485 }
486 drawerInfo.value.footer.btns[1].loading = false;
487 let saveData: any = { ...info };
488 saveData.desensitiveRuleDetail = desensitiveRuleDetailInfo;
489 saveData.fieldChName = props.fieldNameList?.find(f => f.enName == saveData.fieldName)?.chName;
490 saveData.dataTypeName = saveData.dataTypeCode && labelTypeList.value.find(l => l.value == saveData.dataTypeCode)?.label;
491 saveData.fieldTypeName = saveData.fieldTypeCode && props.fieldTypeList?.find(f => f.value == saveData.fieldTypeCode)?.label;
492 saveData.desensitiveRule = info.desensitiveRuleCode && desensitiveRuleTypeList.value.find(d => d.value == info.desensitiveRuleCode)?.label;
493 let changeFields = '';
494 if (drawerInfo.value.type == 'add') {
495 ruleModelTableInfo.value.data.push(saveData);
496 } else if (drawerInfo.value.type == 'edit') {
497 if (!currTableRowIndex.value != null) {
498 let originFieldName = ruleModelTableInfo.value.data[currTableRowIndex.value]?.fieldName;
499 changeFields = originFieldName == saveData.fieldName ? '' : originFieldName;
500 ruleModelTableInfo.value.data[currTableRowIndex.value] = saveData;
501 }
502 }
503 drawerInfo.value.visible = false;
504 updatePrivacyFormFieldsOptions(changeFields);
505 }
506 }
507
508 /** 更新下拉选择字段的下拉列表,若是字段被删除,需要同步去掉下拉选择值。 */
509 const updatePrivacyFormFieldsOptions = (changeFields) => {
510 let optionsList = ruleModelTableInfo.value.data?.map(v => {
511 return {
512 fieldName: v.fieldName,
513 fieldChName: v.fieldChName
514 }
515 }) || [];
516 privacyFormItems.value[2].children[0].options = optionsList;
517 privacyFormItems.value[3].children[0].options = optionsList;
518 let formInline = privacyFormRef.value?.formInline;
519 privacyFormItems.value.forEach((item, index) => {
520 item.default = formInline[item.field];
521 if (item.default == 'Y') {
522 item.children?.forEach(child => {
523 child.default = formInline[child.field];
524 if (changeFields && (child.field == 'tcFieldName' || child.field == 'ldFieldName') && child.default == changeFields) {
525 child.default = ''
526 }
527 });
528 }
529 })
530 }
531
532 /** 当前选择的字段对应的标签信息。 */
533 const currLabelInfo: any = ref({});
534
535 const drawerSelectChange = (val, row, info) => {
536 if (row.field === 'desensitiveRuleCode') {
537 let methodItem = fieldRulesFormItems.value.find(item => item.field == 'encryptionAlgorithmCode');
538 let saltedItem = fieldRulesFormItems.value.find(item => item.field == 'salted');
539 let decimalPlaceItem = fieldRulesFormItems.value.find(item => item.field == 'decimalPlaces');
540 methodItem && (methodItem.visible = val == 'HASH');
541 saltedItem && (saltedItem.visible = val == 'HASH');
542 decimalPlaceItem && (decimalPlaceItem.visible = val == 'ROUNDING');
543 fieldRulesFormItems.value.forEach(item => {
544 item.default = info[item.field];
545 if (item.field == 'decimalPlaces') {
546 if (item.default == null) {
547 item.default = 2;
548 }
549 } else if (item.field == 'salted') {
550 if (item.default == null) {
551 item.default = 5;
552 }
553 }
554 });
555 if (!val) {
556 drawerInfo.value.container.contents = [fieldRulesFormInfo.value];
557 } else {
558 drawerInfo.value.container.contents = [fieldRulesFormInfo.value, fieldRulesEndFormInfo.value];
559 }
560 }
561 // else if (row.field == 'fieldTypeCode') {
562 // fieldRulesFormItems.value.forEach(item => {
563 // item.default = info[item.field];
564 // if (item.field == 'desensitiveRuleCode') {
565 // if (!(val == 'int' || val == 'decimal' || val == 'tinyint')) {
566 // item.options = desensitiveRuleTypeList.value.filter(d => d.value != 'ROUNDING' && d.value != 'RANGEREPLACE');
567 // if (item.default == 'ROUNDING' || item.default == 'RANGEREPLACE') {
568 // item.default = 'DISSEMBLE';
569 // }
570 // } else {
571 // item.options = desensitiveRuleTypeList.value;
572 // }
573 // }
574 // });
575 // }
576 else if (row.field == 'fieldName') { //选择字段改变之后,调用接口。
577 let tableField = props.fieldNameList.find(f => f.enName == val);
578 let dataType = tableField?.dataType || 'varchar';
579 fieldRulesFormItems.value.forEach(item => {
580 item.default = info[item.field];
581 if (item.field == 'fieldTypeCode') {
582 item.default = dataType;
583 }
584 });
585 getLableByFieldName(val).then((res: any) => {
586 if (res?.code == proxy.$passCode) {
587 let labelInfo = currLabelInfo.value = res.data || {};
588 fieldRulesFormItems.value.forEach(item => {
589 item.default = info[item.field];
590 if (item.field == 'dataTypeCode') {
591 item.default = labelInfo.labelTypeCode;
592 }
593 });
594 } else {
595 proxy.$ElMessage.error(res.msg);
596 }
597 })
598 if (info.desensitiveRuleCode == 'RANGEREPLACE') {
599 rangeReplaceRuleDetails.value.forEach(r => {
600 r.fieldChName = tableField?.chName || val;
601 });
602 }
603 }
604 }
605
606 const drawerInputChange = (val, row, info) => {
607 if (row.field == 'testData') { //样本数据修改后,清空脱敏结果
608 fieldRulesEndFormInfo.value.formInfo.items[0].default = val;
609 fieldRulesEndFormInfo.value.formInfo.items[1].default = '';
610 }
611 }
612
613 /** 掩盖规则中的位数和剩余位数 */
614 const digitTypeList = ref([{
615 value: 1,
616 label: '位数'
617 }, {
618 value: 2,
619 label: '剩余位数'
620 }]);
621
622 const dissembleRuleTypeList = ref([{
623 value: 1,
624 label: '脱敏'
625 }, {
626 value: 2,
627 label: '不脱敏'
628 }]);
629
630 /** 掩盖类型的脱敏规则 分段规则配置 */
631 const desensitiveRuleDetail = ref({
632 dissembleType: 1, /** 1从左往右,2.从右往左 */
633 ruleDetails: <any>[{
634 digitType: 1,
635 }]
636 });
637
638 const addSegmentRule = () => {
639 desensitiveRuleDetail.value.ruleDetails.push({
640 digitType: 1,
641 });
642 };
643
644 const deleteSegmentRule = (item, index) => {
645 desensitiveRuleDetail.value.ruleDetails.splice(index, 1);
646 }
647
648 const inputEventDigitChange = (val, item, prop = 'digit') => {
649 item[prop] = item[prop].toString().replace(/\./g, "")
650 item[prop] = item[prop].toString().replace(/^\D*(\d{0,7}(?:\.\d{0})?).*$/g, "$1")
651 if (prop == 'digit' && val != "" && item.digit < 1) {
652 item.digit = 1;
653 }
654 }
655
656 /** 字符规则中的替换方式 */
657 const charReplaceRuleTypeList = ref([{
658 value: 1,
659 label: '随机替换'
660 }, {
661 value: 2,
662 label: '固定值替换'
663 }]);
664
665 /** 字符替换的脱敏规则 分段规则配置 */
666 const charReplaceRuleDetail = ref({
667 replaceType: 1, /** 1从左往右,2.从右往左 */
668 ruleDetails: <any>[{
669 digitType: 1,
670 ruleType: 1
671 }]
672 });
673
674 const addCharReplaceSegmentRule = () => {
675 charReplaceRuleDetail.value.ruleDetails.push({
676 digitType: 1,
677 ruleType: 1
678 });
679 };
680
681 const deleteCharReplaceSegmentRule = (item, index) => {
682 charReplaceRuleDetail.value.ruleDetails.splice(index, 1);
683 }
684
685 /** ----- 区间替换规则 --- */
686 /** 区间替换的小于或小于等于字符列表 */
687 const lowerOperatorList: any = ref([{
688 value: '≤',
689 label: '≤'
690 }, {
691 value: '<',
692 label: '<'
693 }]);
694
695 /**
696 * 字符替换的脱敏规则 分段规则配置。
697 * { lowValue: , lowOperator: , upperValue: , upperOperator: , replaceValue: '' }
698 */
699 const rangeReplaceRuleDetails: any = ref([{
700 lowOperator: '≤',
701 fieldChName: '',
702 upperOperator: '≤'
703 }]);
704
705 const addRangeReplaceSegmentRule = () => {
706 let fieldChName = '';
707 if (rangeReplaceRuleDetails.value?.length) {
708 fieldChName = rangeReplaceRuleDetails.value[0].fieldChName;
709 } else {
710 let fieldName = drawerRef.value?.getDrawerConRef('drawerFormRef')?.formInline?.fieldName;
711 let tableField = fieldName && props.fieldNameList?.find(f => f.enName == fieldName);
712 fieldChName = tableField?.chName;
713 }
714 rangeReplaceRuleDetails.value.push({
715 lowOperator: '≤',
716 fieldChName: fieldChName,
717 upperOperator: '≤'
718 });
719 };
720
721 const deleteRangeReplaceSegmentRule = (item, index) => {
722 rangeReplaceRuleDetails.value.splice(index, 1);
723 }
724
725 /** 隐私模型设置 */
726 const privacyFormItems: any = ref([{
727 label: '选择准标识符等价类隐私模型',
728 type: 'checkbox-input-item',
729 field: 'isKaNumber',
730 default: 'N',
731 placeholder: 'K匿名',
732 trueValue: 'Y',
733 falseValue: 'N',
734 children: [
735 {
736 label: '',
737 type: 'input',
738 placeholder: '请输入',
739 field: 'kaNumber',
740 inputType: 'integerNumber',
741 default: '',
742 min: 1,
743 maxlength: 6,
744 disabled: false,
745 clearable: true,
746 visible: false,
747 col: 'ka-checkbox-input',
748 style: { width: '100px', margin: 0 }
749 }
750 ],
751 required: true,
752 col: 'checkbox-input'
753 }, {
754 label: '设置重标识可接受风险阈值',
755 type: 'checkbox-input-item',
756 field: 'isRiskThreshold',
757 default: 'N',
758 placeholder: '阈值',
759 trueValue: 'Y',
760 falseValue: 'N',
761 children: [
762 {
763 label: '',
764 type: 'input',
765 placeholder: '请输入',
766 field: 'riskThreshold',
767 default: '',
768 maxlength: 6,
769 min: 0,
770 max: 1,
771 inputType: 'scoreNumber',
772 disabled: false,
773 clearable: true,
774 visible: false,
775 col: 'ka-checkbox-input',
776 style: { width: '100px', margin: 0 }
777 }
778 ],
779 required: true,
780 col: 'checkbox-input'
781 }, {
782 label: '设置L多样性及T接近',
783 type: 'checkbox-input-item',
784 field: 'isLdField',
785 default: 'N',
786 placeholder: 'L多样性',
787 trueValue: 'Y',
788 falseValue: 'N',
789 children: [
790 {
791 label: '',
792 type: 'select',
793 placeholder: '请选择字段',
794 field: 'ldFieldName',
795 options: [],
796 props: {
797 label: 'fieldChName',
798 value: 'fieldName'
799 },
800 default: '',
801 filterable: true,
802 clearable: true,
803 visible: false,
804 col: 'ka-checkbox-input',
805 style: { width: '120px', margin: 0 }
806 },
807 {
808 label: '',
809 type: 'input',
810 placeholder: '请输入',
811 field: 'ldNumber',
812 inputType: 'integerNumber',
813 default: '',
814 maxlength: 6,
815 min: 1,
816 disabled: false,
817 clearable: true,
818 visible: false,
819 col: 'ka-checkbox-input',
820 style: { width: '100px', margin: 0 }
821 }
822 ],
823 block: false,
824 required: true,
825 col: 'checkbox-input'
826 }, {
827 label: '',
828 type: 'checkbox-input-item',
829 field: 'isTcField',
830 default: 'N',
831 placeholder: 'T接近',
832 trueValue: 'Y',
833 falseValue: 'N',
834 children: [
835 {
836 label: '',
837 type: 'select',
838 placeholder: '请选择字段',
839 field: 'tcFieldName',
840 default: '',
841 options: [],
842 props: {
843 label: 'fieldChName',
844 value: 'fieldName'
845 },
846 filterable: true,
847 clearable: true,
848 visible: false,
849 col: 'ka-checkbox-input',
850 style: { width: '120px', margin: 0 }
851 },
852 {
853 label: '',
854 type: 'input',
855 placeholder: '请输入',
856 field: 'tcThreshold',
857 default: '',
858 maxlength: 6,
859 min: 0,
860 max: 1,
861 inputType: 'scoreNumber',
862 disabled: false,
863 clearable: true,
864 visible: false,
865 col: 'ka-checkbox-input',
866 style: { width: '100px', margin: 0 }
867 }
868 ],
869 required: false,
870 block: true,
871 col: 'checkbox-input lmt12'
872 }]);
873
874 const privacyFormRules = ref({
875 kaNumber: [required('请输入K匿名值')],
876 riskThreshold: [required('请输入阈值')],
877 ldFieldName: [required('请选择L多样性字段')],
878 ldNumber: [required('请输入L多样性值')],
879 tcFieldName: [required('请选择T接近字段')],
880 tcThreshold: [required('请输入T接近阈值')]
881 });
882
883 /** 记录下旧的隐私模型设置 */
884 const oldPrivacyModelValue: any = ref({
885 isKaNumber: 'N',
886 isRiskThreshold: 'N',
887 isLdField: 'N',
888 isTcField: 'N'
889 });
890
891 const handleCheckboxChange = (val, value, row) => {
892 oldPrivacyModelValue.value = Object.assign({}, oldPrivacyModelValue.value, value);
893 privacyFormItems.value.forEach(item => {
894 item.default = oldPrivacyModelValue.value[item.field];
895 item.children?.forEach(child => {
896 child.default = oldPrivacyModelValue.value[child.field];
897 });
898 })
899 if (row.field == 'isKaNumber') {
900 let kaItem = privacyFormItems.value[0]?.children?.[0];
901 kaItem && (kaItem.visible = val == 'Y');
902 } else if (row.field == 'isRiskThreshold') {
903 let riskItem = privacyFormItems.value[1]?.children?.[0];
904 riskItem && (riskItem.visible = val == 'Y');
905 } else if (row.field == 'isLdField') {
906 let childrenItem = privacyFormItems.value[2]?.children;
907 childrenItem?.[0] && (childrenItem[0].visible = val == "Y");
908 childrenItem?.[1] && (childrenItem[1].visible = val == "Y");
909 } else if (row.field == 'isTcField') {
910 let childrenItem = privacyFormItems.value[3]?.children;
911 childrenItem?.[0] && (childrenItem[0].visible = val == "Y");
912 childrenItem?.[1] && (childrenItem[1].visible = val == "Y");
913 }
914 }
915
916 watch(() => props.fieldNameList, (val) => {
917 fieldRulesFormItems.value[0].options = val || [];
918 if (props.isFile) {
919 fieldRulesFormItems.value[1].disabled = false
920 } else {
921 fieldRulesFormItems.value[1].disabled = true;
922 }
923 }, {
924 immediate: true
925 })
926
927 watch(() => props.fieldTypeList, (val) => {
928 fieldRulesFormItems.value[1].options = val || [];
929 }, {
930 immediate: true
931 })
932
933 watch(() => props.anonPrivacyMode, (val) => {
934 if (!val) {
935 return;
936 }
937 let hasKaNumber = val.kaNumber != null;
938 privacyFormItems.value[0].default = hasKaNumber ? 'Y' : 'N';
939 privacyFormItems.value[0].children[0].visible = hasKaNumber;
940 privacyFormItems.value[0].children[0].default = val.kaNumber;
941 oldPrivacyModelValue.value.isKaNumber = hasKaNumber ? 'Y' : 'N';
942 oldPrivacyModelValue.value.kaNumber = val.kaNumber;
943 let hasRiskThreshold = val.riskThreshold != null;
944 privacyFormItems.value[1].default = hasRiskThreshold ? 'Y' : 'N';
945 privacyFormItems.value[1].children[0].visible = hasRiskThreshold;
946 privacyFormItems.value[1].children[0].default = val.riskThreshold;
947 oldPrivacyModelValue.value.isRiskThreshold = hasRiskThreshold ? 'Y' : 'N';
948 oldPrivacyModelValue.value.riskThreshold = val.riskThreshold;
949 let hasldFieldName = !!val.ldFieldName;
950 privacyFormItems.value[2].default = hasldFieldName ? 'Y' : 'N';
951 privacyFormItems.value[2].children[0].visible = hasldFieldName;
952 privacyFormItems.value[2].children[1].visible = hasldFieldName;
953 privacyFormItems.value[2].children[0].default = val.ldFieldName;
954 privacyFormItems.value[2].children[1].default = val.ldNumber;
955 oldPrivacyModelValue.value.isLdField = hasldFieldName ? 'Y' : 'N';
956 oldPrivacyModelValue.value.ldFieldName = val.ldFieldName;
957 oldPrivacyModelValue.value.ldNumber = val.ldNumber;
958 let hasTcField = !!val.tcFieldName;
959 privacyFormItems.value[3].default = hasTcField ? 'Y' : 'N';
960 privacyFormItems.value[3].children[0].visible = hasTcField;
961 privacyFormItems.value[3].children[1].visible = hasTcField;
962 privacyFormItems.value[3].children[0].default = val.tcFieldName;
963 privacyFormItems.value[3].children[1].default = val.tcThreshold;
964 oldPrivacyModelValue.value.isTcField = hasTcField ? 'Y' : 'N';
965 oldPrivacyModelValue.value.tcFieldName = val.tcFieldName;
966 oldPrivacyModelValue.value.tcThreshold = val.tcThreshold;
967 }, {
968 deep: true
969 })
970
971 watch(() => props.anonTaskRules, (val) => {
972 ruleModelTableInfo.value.data = val || [];
973 let optionsList = val?.map(v => {
974 return {
975 fieldName: v.fieldName,
976 fieldChName: v.fieldChName
977 }
978 }) || [];
979 privacyFormItems.value[2].children[0].options = optionsList;
980 privacyFormItems.value[3].children[0].options = optionsList;
981 })
982
983 onBeforeMount(() => {
984 getParamsList({
985 dictType: "标签类型",
986 }).then((res: any) => {
987 if (res?.code == proxy.$passCode) {
988 labelTypeList.value = res.data || [];
989 let item = fieldRulesFormItems.value.find(item => item.field == 'dataTypeCode');
990 item && (item.options = labelTypeList.value);
991 } else {
992 proxy.$ElMessage.error(res.msg);
993 }
994 });
995 getParamsList({
996 dictType: "脱敏规则",
997 }).then((res: any) => {
998 if (res?.code == proxy.$passCode) {
999 desensitiveRuleTypeList.value = res.data || [];
1000 let item = fieldRulesFormItems.value.find(item => item.field == 'desensitiveRuleCode');
1001 item && (item.options = desensitiveRuleTypeList.value);
1002 } else {
1003 proxy.$ElMessage.error(res.msg);
1004 }
1005 });
1006 getParamsList({
1007 dictType: "加密算法",
1008 }).then((res: any) => {
1009 if (res?.code == proxy.$passCode) {
1010 hashMethodList.value = res.data || [];
1011 let item = fieldRulesFormItems.value.find(item => item.field == 'encryptionAlgorithmCode');
1012 item && (item.options = hashMethodList.value);
1013 } else {
1014 proxy.$ElMessage.error(res.msg);
1015 }
1016 });
1017 getGeneralizeFileNameList().then((res: any) => {
1018 if (res?.code == proxy.$passCode) {
1019 generalizeFileNameList.value = res.data || [];
1020 let item = fieldRulesFormItems.value.find(item => item.field == 'generalizeFileGuid');
1021 item && (item.options = generalizeFileNameList.value);
1022 } else {
1023 proxy.$ElMessage.error(res.msg);
1024 }
1025 });
1026 })
1027
1028 /** 隐私模型设置表单 */
1029 const privacyFormRef = ref();
1030
1031 const getStepTwoConfigInfo = async () => {
1032 if (!ruleModelTableInfo.value.data?.length) {
1033 proxy.$ElMessage.error('字段脱敏规则不能为空');
1034 return false;
1035 }
1036 try {
1037 await privacyFormRef.value?.ruleFormRef?.validate();
1038 // 验证通过
1039 return {
1040 anonPrivacyMode: privacyFormRef.value?.formInline,
1041 anonTaskRules: ruleModelTableInfo.value.data
1042 };
1043 } catch (error) {
1044 // 验证失败
1045 return false;
1046 }
1047 }
1048
1049 defineExpose({
1050 getStepTwoConfigInfo
1051 })
1052
1053 </script>
1054
1055 <template>
1056 <div class="operator_panel_wrap">
1057 <ContentWrap id="id-rules" title="设置匿名化规则" description="" style="margin-top: 8px;">
1058 <Table :tableInfo="ruleModelTableInfo" />
1059 <div class="row-add-btn">
1060 <el-button link @click="addRowRules" :icon="CirclePlus" v-preReClick>添加字段脱敏规则</el-button>
1061 </div>
1062 </ContentWrap>
1063 <ContentWrap id="id-screctModel" title="隐私模型设置" description="" style="margin-top: 16px;">
1064 <Form style="width: 80%;max-width: 500px;" ref="privacyFormRef" :itemList="privacyFormItems"
1065 :rules="privacyFormRules" formId="model-select-edit" @checkboxChange="handleCheckboxChange" />
1066 </ContentWrap>
1067
1068 <Drawer ref="drawerRef" :drawerInfo="drawerInfo" @drawerBtnClick="drawerBtnClick"
1069 @drawerInputChange="drawerInputChange" @drawerSelectChange="drawerSelectChange">
1070 <template v-slot:default>
1071 <div v-show="drawerRef?.getDrawerConRef('drawerFormRef')?.formInline?.desensitiveRuleCode == 'DISSEMBLE'">
1072 <div>{{ '请配置分段是否脱敏' + `(${desensitiveRuleDetail.ruleDetails?.length}/10)` }}</div>
1073 <el-radio-group v-model="desensitiveRuleDetail.dissembleType">
1074 <el-radio :value="1">从左往右</el-radio>
1075 <el-radio :value="2">从右往左</el-radio>
1076 </el-radio-group>
1077 <div class="seg-main">
1078 <div class="row-per" v-for="(item, index) in desensitiveRuleDetail.ruleDetails">
1079 <el-select v-model="item.digitType" :style="{ width: item.digitType == 2 ? '322px' : '170px' }">
1080 <el-option v-for="item in digitTypeList" :label="item.label" :value="item.value"
1081 :key="item.value"></el-option>
1082 </el-select>
1083 <el-input v-show="item.digitType != 2" style="width:137px;margin-left: 4px;" v-model="item.digit"
1084 :maxlength="6" :min="1" @input="(val) => inputEventDigitChange(val, item)" placeholder="请输入"></el-input>
1085 <el-select v-model="item.ruleType" style="width:170px;margin-left: 4px;">
1086 <el-option v-for="item in dissembleRuleTypeList" :label="item.label" :value="item.value"
1087 :key="item.value"></el-option>
1088 </el-select>
1089 <div class="title_tool" @click="deleteSegmentRule(item, index)">
1090 <el-icon :size="20" color="#b2b2b2">
1091 <Delete />
1092 </el-icon>
1093 </div>
1094 </div>
1095 <div class="row-add-btn">
1096 <el-button :disabled="desensitiveRuleDetail.ruleDetails?.length > 9" link @click="addSegmentRule"
1097 :icon="CirclePlus" v-preReClick>添加分段规则</el-button>
1098 </div>
1099 </div>
1100 </div>
1101 <div v-show="drawerRef?.getDrawerConRef('drawerFormRef')?.formInline?.desensitiveRuleCode == 'CHARREPLACE'">
1102 <div>{{ '请配置分段的替换方式' + `(${charReplaceRuleDetail.ruleDetails?.length}/10)` }}</div>
1103 <el-radio-group v-model="charReplaceRuleDetail.replaceType">
1104 <el-radio :value="1">从左往右</el-radio>
1105 <el-radio :value="2">从右往左</el-radio>
1106 </el-radio-group>
1107 <div class="seg-main">
1108 <div class="row-per" v-for="(item, index) in charReplaceRuleDetail.ruleDetails">
1109 <el-select v-model="item.digitType" :style="{ width: item.digitType == 2 ? '220px' : '130px' }">
1110 <el-option v-for="item in digitTypeList" :label="item.label" :value="item.value"
1111 :key="item.value"></el-option>
1112 </el-select>
1113 <el-input v-show="item.digitType != 2" style="width:86px;margin-left: 4px;" v-model="item.digit"
1114 :maxlength="6" :min="1" @input="(val) => inputEventDigitChange(val, item)" placeholder="请输入"></el-input>
1115 <el-select v-model="item.ruleType"
1116 :style="{ width: item.ruleType == 1 ? '244px' : '130px', 'margin-left': '4px' }">
1117 <el-option v-for="item in charReplaceRuleTypeList" :label="item.label" :value="item.value"
1118 :key="item.value"></el-option>
1119 </el-select>
1120 <el-input v-model="item.fixedValue" v-show="item.ruleType == 2" style="width:110px;margin-left: 4px;"
1121 :maxlength="50" placeholder="请输入"></el-input>
1122 <div class="title_tool" @click="deleteCharReplaceSegmentRule(item, index)">
1123 <el-icon :size="20" color="#b2b2b2">
1124 <Delete />
1125 </el-icon>
1126 </div>
1127 </div>
1128 <div class="row-add-btn">
1129 <el-button :disabled="charReplaceRuleDetail.ruleDetails?.length > 9" link
1130 @click="addCharReplaceSegmentRule" :icon="CirclePlus" v-preReClick>添加分段规则</el-button>
1131 </div>
1132 </div>
1133 </div>
1134 <div v-show="drawerRef?.getDrawerConRef('drawerFormRef')?.formInline?.desensitiveRuleCode == 'RANGEREPLACE'">
1135 <div class="mb8">{{ '请配置区间替换规则' + `(${rangeReplaceRuleDetails?.length}/10)` }}</div>
1136 <div class="seg-main">
1137 <div class="row-per" v-for="(item, index) in rangeReplaceRuleDetails">
1138 <el-input style="width:16.5%" v-model="item.lowValue" :maxlength="6"
1139 @input="(val) => inputEventDigitChange(val, item, 'lowValue')" placeholder="请输入"></el-input>
1140 <el-select v-model="item.lowOperator" style="width: 16.5%;margin-left: 4px;">
1141 <el-option v-for="item in lowerOperatorList" :label="item.label" :value="item.value"
1142 :key="item.value"></el-option>
1143 </el-select>
1144 <el-input style="width: 16.5%;margin-left: 4px;" :disabled="true" v-model="item.fieldChName"></el-input>
1145 <el-select v-model="item.upperOperator" style="width: 16.5%;margin-left: 4px;">
1146 <el-option v-for="item in lowerOperatorList" :label="item.label" :value="item.value"
1147 :key="item.value"></el-option>
1148 </el-select>
1149 <el-input style="width:16.5%;margin-left: 4px;" v-model="item.upperValue" :maxlength="6"
1150 @input="(val) => inputEventDigitChange(val, item, 'upperValue')" placeholder="请输入"></el-input>
1151 <el-input v-model="item.replaceValue" style="width:16.5%;margin-left: 4px;" :maxlength="50"
1152 placeholder="替换值"></el-input>
1153 <div class="title_tool" @click="deleteRangeReplaceSegmentRule(item, index)">
1154 <el-icon :size="20" color="#b2b2b2">
1155 <Delete />
1156 </el-icon>
1157 </div>
1158 </div>
1159 <div class="row-add-btn">
1160 <el-button :disabled="charReplaceRuleDetail.ruleDetails?.length > 9" link
1161 @click="addRangeReplaceSegmentRule" :icon="CirclePlus" v-preReClick>添加分段规则</el-button>
1162 </div>
1163 </div>
1164 </div>
1165 </template>
1166 </Drawer>
1167
1168 </div>
1169 </template>
1170
1171 <style lang="scss" scoped>
1172 .row-add-btn {
1173 .el-button--default {
1174 padding: 4px 0px;
1175 }
1176
1177 :deep(.el-icon) {
1178 width: 16px;
1179 height: 16px;
1180
1181 svg {
1182 width: 16px;
1183 height: 16px;
1184 }
1185 }
1186 }
1187
1188 .seg-main {
1189 .row-add-btn {
1190 margin-top: -6px;
1191 }
1192 }
1193
1194 .row-per {
1195 display: flex;
1196 align-items: center;
1197 margin-bottom: 8px;
1198 position: relative;
1199
1200 .title_tool {
1201 margin-left: 4px;
1202 // position: absolute;
1203 // right: 4px;
1204 cursor: pointer;
1205
1206 :deep(.el-icon) {
1207 --color: #FB2323 !important;
1208
1209 svg {
1210 width: 16px;
1211 height: 16px;
1212 }
1213 }
1214 }
1215 }
1216
1217 :deep(.mt8.drawer_panel) {
1218 margin-top: 8px;
1219 }
1220
1221 .mb8 {
1222 margin-bottom: 8px;
1223 }
1224
1225 :deep(.el-form-item.ka-checkbox-input) {
1226 .el-input {
1227 min-width: 50px !important;
1228 }
1229 }
1230
1231 :deep(.el-form) {
1232 .lmt12 {
1233 // margin-top: -4px; 验证信息会被遮挡
1234
1235 .input_panel {
1236 flex: 0 !important;
1237 }
1238 }
1239 }
1240 </style>
...\ No newline at end of file ...\ No newline at end of file
...@@ -95,6 +95,8 @@ const tableInfo = ref({ ...@@ -95,6 +95,8 @@ const tableInfo = ref({
95 }); 95 });
96 } 96 }
97 }); 97 });
98 }, () => {
99 proxy.$ElMessage.info("已取消");
98 }) 100 })
99 } 101 }
100 }] 102 }]
......
...@@ -72,18 +72,24 @@ const tableInfo = ref({ ...@@ -72,18 +72,24 @@ const tableInfo = ref({
72 btns: (scope) => { 72 btns: (scope) => {
73 return [ { 73 return [ {
74 label: "编辑", value: "edit", disabled: scope.row.status == 'R', click: (scope) => { 74 label: "编辑", value: "edit", disabled: scope.row.status == 'R', click: (scope) => {
75 75 router.push({
76 name: 'anonTaskCreate',
77 query: {
78 guid: scope.row.guid,
79 taskName: scope.row.taskName
80 }
81 });
76 } 82 }
77 }, { 83 }, {
78 label: '查看数据', value: 'view', disabled: scope.row.status != 'Y', click: (scope) => { 84 label: '查看数据', value: 'view', disabled: scope.row.status != 'Y', click: (scope) => {
79 // router.push({ 85 router.push({
80 // name: 'sensitiveIdentifyConfig', 86 name: 'anonResultView',
81 // query: { 87 query: {
82 // guid: scope.row.guid, 88 guid: scope.row.guid,
83 // execGuid: scope.row.execGuid, 89 execGuid: scope.row.lastExecGuid,
84 // taskName: scope.row.taskName 90 taskName: scope.row.taskName
85 // } 91 }
86 // }); 92 });
87 } 93 }
88 }, { 94 }, {
89 label: "删除", value: "delete", disabled: scope.row.status == 'R', click: (scope) => { 95 label: "删除", value: "delete", disabled: scope.row.status == 'R', click: (scope) => {
......
...@@ -983,14 +983,14 @@ const btnFormClick = (btn, type) => { ...@@ -983,14 +983,14 @@ const btnFormClick = (btn, type) => {
983 modelsDialogVisible.value = true; 983 modelsDialogVisible.value = true;
984 if (!databaseList.value?.length) { 984 if (!databaseList.value?.length) {
985 getDataSourceListData().then(() => { 985 getDataSourceListData().then(() => {
986 if (databaseInfo.value == databaseList.value[0]?.guid ?? "") { 986 if (databaseInfo.value == (databaseList.value[0]?.guid ?? "")) {
987 dsFromTreeData.value = JSON.parse(JSON.stringify(currentDsFromTreeData.value)); 987 dsFromTreeData.value = JSON.parse(JSON.stringify(currentDsFromTreeData.value));
988 } else { 988 } else {
989 databaseInfo.value = databaseList.value[0]?.guid ?? ""; 989 databaseInfo.value = databaseList.value[0]?.guid ?? "";
990 } 990 }
991 }) 991 })
992 } else { 992 } else {
993 if (databaseInfo.value == databaseList.value[0]?.guid ?? "") { 993 if (databaseInfo.value == (databaseList.value[0]?.guid ?? "")) {
994 dsFromTreeData.value = JSON.parse(JSON.stringify(currentDsFromTreeData.value)); 994 dsFromTreeData.value = JSON.parse(JSON.stringify(currentDsFromTreeData.value));
995 } else { 995 } else {
996 databaseInfo.value = databaseList.value[0]?.guid ?? ""; 996 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!