4b99859f by lihua

迁移数据匿名化代码

1 parent 87366979
...@@ -3,6 +3,51 @@ ...@@ -3,6 +3,51 @@
3 */ 3 */
4 import request from "@/utils/request"; 4 import request from "@/utils/request";
5 5
6 /** 获取标签列表。 */
7 export const getDataLabelList = (params) => request({
8 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/label/page-list`,
9 method: 'post',
10 data: params
11 })
12
13 /** 修改标签启用禁用状态 */
14 export const updateLabelState = (params) => request({
15 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/label/update-state`,
16 method: 'put',
17 params
18 })
19
20 export const saveLabel = (data) => request({
21 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/label/save`,
22 method: 'post',
23 data
24 })
25
26 export const deleteLabel = (data) => request({
27 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/label/delete`,
28 method: 'delete',
29 data
30 })
31
32 export const updateLabel = (data) => request({
33 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/label/update`,
34 method: 'put',
35 data
36 })
37
38 /** 获取标签详情 */
39 export const getLabelDetail = (guid) => request({
40 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/label/detail?guid=${guid}`,
41 method: 'get'
42 })
43
44 /** 获取数据字典配置 */
45 export const getParamsList = (params) => request({
46 url: `${import.meta.env.VITE_APP_CONFIG_URL}/dict/data/get-by-dictType`,
47 method: 'get',
48 params
49 })
50
6 /** 字段类型 */ 51 /** 字段类型 */
7 export const fieldTypeList = [{ 52 export const fieldTypeList = [{
8 value: '1', 53 value: '1',
...@@ -59,3 +104,240 @@ export const parseGeneralizeFileData = (data) => request({ ...@@ -59,3 +104,240 @@ export const parseGeneralizeFileData = (data) => request({
59 method: 'post', 104 method: 'post',
60 data 105 data
61 }) 106 })
107
108
109 /** --------- 敏感数据识别接口 ------------------- */
110
111 /** 获取敏感数据识别任务列表 */
112 export const getSensitiveDataTaskList = (params) => request({
113 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/page-list`,
114 method: 'post',
115 data: params
116 })
117
118 /** 新增敏感数据识别任务 */
119 export const saveSensitiveDataTask = (params) => request({
120 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/save`,
121 method: 'post',
122 data: params
123 })
124
125 /** 编辑修改敏感数据识别任务 */
126 export const updateSensitiveDataTask = (params) => request({
127 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/update`,
128 method: 'put',
129 data: params
130 })
131
132 /** 删除敏感数据识别任务 */
133 export const deleteSensitiveDataTask = (data) => request({
134 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/delete`,
135 method: 'delete',
136 data
137 })
138
139 /** 手动执行敏感任务 */
140 export const execSensitiveDataTask = (guid) => request({
141 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/exec-task?taskGuid=${guid}`,
142 method: 'get'
143 })
144
145 /** 数据来源类型 */
146 export const dataSourceTypeList = [{
147 value: 1,
148 label: '数据库'
149 }, {
150 value: 2,
151 label: '文件导入'
152 }];
153
154 /** 获取数据库选择列表 */
155 export const getDatabase = (params) => request({
156 url: `${import.meta.env.VITE_APP_DATA_SOURCE_URL}/data-source/get-source-list`,
157 method: 'post',
158 data: params
159 })
160
161 /** 获取敏感数据任务执行的数据表列表 */
162 export const getExecSensitiveTable = (execGuid) => request({
163 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/get-exec-sensitive-table?execGuid=${execGuid}`,
164 method: 'get'
165 })
166
167 /** 根据数据源或表获取敏感数据任务执行的字段列表 */
168 export const getExecSensitiveFieldTable = (params) => request({
169 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/get-exec-sensitive-field`,
170 method: 'post',
171 data: params
172 })
173
174 /** 获取当前数据表下的执行字段 */
175 export const getExecSensitiveFieldColumnListByCondition = (params) => request({
176 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/column-list-by-condition`,
177 method: 'post',
178 data: params
179 })
180
181 /** 获取敏感数据识别任务执行后的统计结果 */
182 export const getStatisticsNum = (params) => request({
183 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/statistics-num`,
184 method: 'get',
185 params
186 })
187
188 /** 修改敏感数据识别字段标签 */
189 export const updateSensitiveDataTaskFieldLabel = (params) => request({
190 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/batch-update-label`,
191 method: 'put',
192 data: params
193 })
194
195 /** 批量修改确认状态 */
196 export const batchUpdateSensitiveDataTaskFieldStatus = (params) => request({
197 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/batch-change-status`,
198 method: 'post',
199 data: params
200 })
201
202 /** 修改任务状态 */
203 export const confirmTaskStatus = (guid) => request({
204 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/confirm-task?execGuid=${guid}`,
205 method: 'get'
206 })
207
208 /** 获取敏感数据识别任务执行日志 */
209 export const getSensitiveDataTaskExecLog = (params) => request({
210 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/get-exec-sensitive-exec-log`,
211 method: 'post',
212 data: params
213 })
214
215 /** ---------- 匿名化处理 ------------------ */
216
217 /** 获取匿名化任务列表 */
218 export const getAnonTaskList = (params) => request({
219 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/page-list`,
220 method: 'post',
221 data: params
222 })
223
224 /** 删除匿名化任务 */
225 export const deleteAnonTask = (data) => request({
226 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/delete`,
227 method: 'delete',
228 data
229 })
230
231 /** 保存匿名化任务 */
232 export const saveAnonTask = (params) => request({
233 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/save`,
234 method: 'post',
235 data: params
236 })
237
238 /** 更新匿名化任务 */
239 export const updateAnonTask = (params) => request({
240 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/update`,
241 method: 'put',
242 data: params
243 })
244
245 /** 获取匿名化任务详情 */
246 export const getAnonTaskDetail = (guid) => request({
247 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/detail?guid=${guid}`,
248 method: 'get'
249 })
250
251 /** 执行匿名化任务 */
252 export const execAnonTask = (taskGuid) => request({
253 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/exec-task?taskGuid=${taskGuid}`,
254 method: 'post'
255 })
256
257 /** 匿名化任务检验接口 */
258 export const anonTaskCheck = (params) => request({
259 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/check`,
260 method: 'post',
261 data: params
262 })
263
264 /** 获取匿名化任务分析结果数据 */
265 export const getAnonAnalyzeResult = (execGuid) => request({
266 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/get-anon-analyze?taskExecGuid=${execGuid}`,
267 method: 'get'
268 })
269
270 /** 获取匿名化任务分析结果数据 */
271 export const getLastAnonAnalyzeResult = (execGuid) => request({
272 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/get-anon-analyze?isResult=true&taskExecGuid=${execGuid}`,
273 method: 'get'
274 })
275
276 /** 获取匿名化任务分析结果统计分页数据 */
277 export const getAnonAnalyzePageData = (params) => request({
278 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/page-anon-analyze-data`,
279 method: 'post',
280 data: params
281 })
282
283 /** 获取匿名化任务结果数据 */
284 export const getAnonPageData = (params) => request({
285 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/page-anon-data`,
286 method: 'post',
287 data: params
288 })
289
290 /** 字段中文转英文 */
291 export const chTransformEn =(params)=> request({
292 url: `${import.meta.env.VITE_APP_COMMON_URL}/common/convert-field-ch-name`,
293 method: "post",
294 data: params,
295 });
296
297 /** 根据选择的连接池获取表列表 */
298 export const getDsTableByDs = (params) => request({
299 url: `${import.meta.env.VITE_APP_DATA_SOURCE_URL}/data-source/schema-table-page-list`,
300 method: 'post',
301 data: params
302 })
303
304 /** 根据数据表获取表字段结构 */
305 export const getDsTableFieldColumn = (params) => request({
306 url: `${import.meta.env.VITE_APP_DATA_SOURCE_URL}/data-source/table-column-list`,
307 method: 'post',
308 data: params
309 });
310
311 /** 根据数据表获取表数据 */
312 export const getDsTableSampleData = (params) => request({
313 url: `${import.meta.env.VITE_APP_DATA_SOURCE_URL}/data-source/table-data-preview-page`,
314 method: 'post',
315 data: params
316 });
317
318 /** 根据字段名称获取敏感数据识别标签 */
319 export const getLableByFieldName = (fieldName) => request({
320 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/sensitive-data-task/get-label-by-field-name?fieldName=${fieldName}`,
321 method: 'get'
322 });
323
324 /** 验证样本数据 */
325 export const validateAnonRule = (params) => request({
326 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/check`,
327 method: 'post',
328 data: params
329 })
330
331 /** 导出匿名化结果数据 */
332 export const exportAnonExecData = (params) => request({
333 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/export-anon-data?taskGuid=${params.taskGuid}&taskExecGuid=${params.execGuid}`,
334 method: 'get',
335 responseType: 'blob'
336 })
337
338 /** 下载匿名化评估报告 */
339 export const exportAnonReport = (params) => request({
340 url: `${import.meta.env.VITE_APP_DIGITAL_CONTRACT_URL}/anon-task/download-report?taskGuid=${params.taskGuid}&taskExecGuid=${params.execGuid}`,
341 method: 'post',
342 responseType: 'blob'
343 })
...\ No newline at end of file ...\ No newline at end of file
......
1 import type { RouteRecordRaw } from 'vue-router'
2
3 function Layout() {
4 return import('@/layouts/index.vue')
5 }
6
7 const routes: RouteRecordRaw[] = [
8 {
9 path: '/data-anonymization/label-management',
10 component: Layout,
11 meta: {
12 title: '标签管理',
13 icon: 'sidebar-videos',
14 },
15 children: [
16 {
17 path: '',
18 name: 'labelManagement',
19 component: () => import('@/views/data_anonymization/labelManagement.vue'),
20 meta: {
21 title: '标签管理',
22 sidebar: false,
23 breadcrumb: false,
24 cache: true
25 },
26 }
27 ],
28 },
29 {
30 path: '/data-anonymization/generalize-file',
31 component: Layout,
32 meta: {
33 title: '泛化文件管理',
34 icon: 'sidebar-videos',
35 },
36 children: [
37 {
38 path: '',
39 name: 'generalizeFile',
40 component: () => import('@/views/data_anonymization/generalizeFile.vue'),
41 meta: {
42 title: '泛化文件管理',
43 sidebar: false,
44 breadcrumb: false,
45 cache: true
46 },
47 },
48 {
49 path: 'generalize-file-edit',
50 name: 'generalizeFileEdit',
51 component: () => import('@/views/data_anonymization/generalizeFileEdit.vue'),
52 meta: {
53 title: '新建泛化文件',
54 sidebar: false,
55 breadcrumb: false,
56 cache: true,
57 reuse: true,
58 editPage: true,
59 activeMenu: '/data-anonymization/generalize-file'
60 },
61 beforeEnter: (to, from) => {
62 if (to.query.fileName) {
63 to.meta.title = `编辑-${to.query.fileName}`;
64 }
65 }
66 },
67 ],
68 },
69 {
70 path: '/data-anonymization/sensitive-identify',
71 component: Layout,
72 meta: {
73 title: '敏感数据识别',
74 icon: 'sidebar-videos',
75 },
76 children: [
77 {
78 path: '',
79 name: 'sensitiveIdentify',
80 component: () => import('@/views/data_anonymization/sensitiveIdentify.vue'),
81 meta: {
82 title: '敏感数据识别',
83 sidebar: false,
84 breadcrumb: false,
85 cache: true
86 },
87 },
88 {
89 path: 'sensitive-identify-config',
90 name: 'sensitiveIdentifyConfig',
91 component: () => import('@/views/data_anonymization/sensitiveIdentifyConfig.vue'),
92 meta: {
93 title: '敏感数据识别查看',
94 sidebar: false,
95 breadcrumb: false,
96 cache: true,
97 reuse: true,
98 editPage: false,
99 activeMenu: '/data-anonymization/sensitive-identify'
100 },
101 beforeEnter: (to, from) => {
102 if (to.query.taskName) {
103 to.meta.title = `敏感数据${to.query.isLook == '1' ? '日志查看' : '查看'}-${to.query.taskName}`;
104 }
105 }
106 },
107 {
108 path: 'sensitive-identify-task-exec-log',
109 name: 'sensitiveIdentifyTaskExecLog',
110 component: () => import('@/views/data_anonymization/sensitiveIdentifyTaskExecLog.vue'),
111 meta: {
112 title: '执行日志',
113 sidebar: false,
114 breadcrumb: false,
115 cache: true,
116 reuse: true
117 },
118 beforeEnter: (to, from) => {
119 if (to.query.guid) {
120 to.meta.title = `日志-${to.query.name}`;
121 }
122 }
123 }
124 ],
125 },
126 {
127 path: '/data-anonymization/result-process',
128 component: Layout,
129 meta: {
130 title: '匿名化处理',
131 icon: 'sidebar-videos',
132 },
133 children: [
134 {
135 path: '',
136 name: 'resultProcess',
137 component: () => import('@/views/data_anonymization/resultProcess.vue'),
138 meta: {
139 title: '匿名化处理',
140 sidebar: false,
141 breadcrumb: false,
142 cache: true
143 },
144 },
145 {
146 path: 'anon-task-create',
147 name: 'anonTaskCreate',
148 component: () => import('@/views/data_anonymization/anonTaskCreate.vue'),
149 meta: {
150 title: '匿名化处理任务',
151 sidebar: false,
152 breadcrumb: false,
153 cache: true,
154 reuse: true,
155 editPage: true,
156 activeMenu: '/data-anonymization/result-process'
157 },
158 beforeEnter: (to, from) => {
159 if (to.query.taskName) {
160 to.meta.title = `编辑-${to.query.taskName}`;
161 }
162 }
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 },
181 {
182 path: 'anonResultReportView',
183 name: 'anonResultReportView',
184 component: () => import('@/views/data_anonymization/anonResultReportView.vue'),
185 meta: {
186 title: '查看报告',
187 sidebar: false,
188 breadcrumb: false,
189 cache: true,
190 reuse: true
191 },
192 beforeEnter: (to, from) => {
193 if (to.query.guid) {
194 to.meta.title = `查看报告-${to.query.taskName}`;
195 }
196 }
197 },
198 ],
199 },
200 ]
201
202 export default routes
...@@ -216,47 +216,7 @@ const routes: RouteRecordRaw[] = [ ...@@ -216,47 +216,7 @@ const routes: RouteRecordRaw[] = [
216 editPage: true 216 editPage: true
217 }, 217 },
218 }] 218 }]
219 },
220 {
221 path: '/data-smart-contract-common/generalize-file',
222 component: Layout,
223 meta: {
224 title: '泛化文件管理',
225 icon: 'sidebar-videos',
226 },
227 children: [
228 {
229 path: '',
230 name: 'generalizeFile',
231 component: () => import('@/views/data_smart_contract/generalizeFile.vue'),
232 meta: {
233 title: '',
234 sidebar: false,
235 breadcrumb: false,
236 cache: true
237 },
238 },
239 {
240 path: 'generalize-file-edit',
241 name: 'generalizeFileEdit',
242 component: () => import('@/views/data_smart_contract/generalizeFileEdit.vue'),
243 meta: {
244 title: '新建泛化文件',
245 sidebar: false,
246 breadcrumb: false,
247 cache: true,
248 reuse: true,
249 editPage: true,
250 activeMenu: '/data-smart-contract-common/generalize-file'
251 },
252 beforeEnter: (to, from) => {
253 if (to.query.fileName) {
254 to.meta.title = `编辑-${to.query.fileName}`;
255 } 219 }
256 }
257 },
258 ],
259 },
260 ] 220 ]
261 221
262 export default routes 222 export default routes
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -7,6 +7,7 @@ import DataSmartContract from './modules/dataSmartContract'; ...@@ -7,6 +7,7 @@ import DataSmartContract from './modules/dataSmartContract';
7 import DataFacilitator from './modules/dataFacilitator'; 7 import DataFacilitator from './modules/dataFacilitator';
8 import HomeIndex from './modules/homeIndex'; 8 import HomeIndex from './modules/homeIndex';
9 import DataDelivery from './modules/dataDelivery'; 9 import DataDelivery from './modules/dataDelivery';
10 import DataAnonymization from './modules/dataAnonymization';
10 11
11 import useSettingsStore from '@/store/modules/settings' 12 import useSettingsStore from '@/store/modules/settings'
12 13
...@@ -99,6 +100,7 @@ const asyncRoutes: RouteRecordRaw[] = [ ...@@ -99,6 +100,7 @@ const asyncRoutes: RouteRecordRaw[] = [
99 ...DataFacilitator, 100 ...DataFacilitator,
100 ...HomeIndex, 101 ...HomeIndex,
101 ...DataDelivery, 102 ...DataDelivery,
103 ...DataAnonymization,
102 // ...DataAssetRegistry, 104 // ...DataAssetRegistry,
103 ] 105 ]
104 106
......
1 <route lang="yaml">
2 name: anonResultReportView
3 </route>
4
5 <script lang="ts" setup name="anonResultReportView">
6 import {
7 exportAnonReport,
8 getAnonAnalyzePageData,
9 getAnonAnalyzeResult,
10 getAnonTaskDetail,
11 } from '@/api/modules/dataAnonymization';
12 import { changeNum, download } from '@/utils/common';
13 import { ElMessage } from 'element-plus';
14 import anonResultAnalysis from './components/anonResultAnalysis.vue';
15 import { commonPageConfig } from '@/utils/enum';
16 import { calcColumnWidth } from '@/utils';
17
18 const route = useRoute();
19 const router = useRouter();
20 const fullPath = route.fullPath;
21 const taskGuid = ref(route.query.guid);
22 const { proxy } = getCurrentInstance() as any;
23 const resultDataLoading = ref(false);
24
25 const downPromise: any = ref()
26
27 /** 提交保存和编辑后的执行guid */
28 const taskExecGuid = ref(route.query.execGuid);
29
30 /** 记录原始的值信息,防止上一步之后未修改数据时不调用接口 */
31 const oldAnonTaskValueInfo: any = ref({});
32 /** 执行结果信息 */
33 const analysisResultInfo: any = ref({});
34
35 const containerRef = ref();
36
37 const containerWidth = ref(containerRef.value?.offsetWidth || 0)
38
39 /** ------------------------- 匿名化分析结果页面数据展示 ---------------- */
40 const pageInfo: any = ref({
41 ...commonPageConfig,
42 })
43
44 const pageChange = (info) => {
45 pageInfo.value.curr = Number(info.curr);
46 pageInfo.value.limit = Number(info.limit);
47 getAnalysisResultPageData();
48 }
49
50 /** 每列字段对应的列宽计算结果。 */
51 const originResultTableFieldColumn = ref({});
52
53 /** 结果分析中的字段表格数据 */
54 const resultData: any = ref([]);
55
56 /** 结果分析中的字段信息 */
57 const analysisResultTableFields: any = ref([]);
58
59 const analysisResultLoading = ref(false);
60
61 /** otherWidth表示使用标题宽度时添加标题排序图标等宽度 */
62 const calcTableColumnWidth = (data: any[], prop, title, otherWidth = 0) => {
63 let d: any[] = [];
64 data.forEach((dt) => d.push(dt[prop]));
65 //样式使用默认值。
66 return calcColumnWidth(
67 d,
68 title,
69 {
70 fontSize: 14,
71 fontFamily: "SimSun",
72 },
73 {
74 fontSize: 14,
75 fontFamily: "SimSun",
76 },
77 otherWidth
78 );
79 };
80
81 watch(
82 resultData,
83 (val: any[], oldVal) => {
84 if (!analysisResultTableFields.value?.length) {
85 originResultTableFieldColumn.value = {};
86 return;
87 }
88 originResultTableFieldColumn.value = {};
89 analysisResultTableFields.value.forEach((field, index) => {
90 originResultTableFieldColumn.value[field.enName] = calcTableColumnWidth(
91 val?.slice(0, 20) || [],
92 field.enName,
93 field.chName,
94 24
95 );
96 });
97 },
98 {
99 deep: true,
100 }
101 );
102
103 const getAnalysisResultPageData = () => {
104 analysisResultLoading.value = true;
105 getAnonAnalyzePageData({
106 pageIndex: pageInfo.value.curr,
107 pageSize: pageInfo.value.limit,
108 taskExecGuid: taskExecGuid.value,
109 }).then((res: any) => {
110 analysisResultLoading.value = false;
111 if (res?.code == proxy.$passCode) {
112 pageInfo.value.rows =
113 resultData.value = [];
114 res.data?.records?.forEach(d => {
115 let obj = {};
116 analysisResultTableFields.value.forEach(t => {
117 obj[t.enName] = d.fieldValue?.[t.enName];
118 });
119 obj['equivalenceClassNum'] = changeNum(d.equivalenceClassNum || 0, 0);
120 obj['reIdentifyRisk'] = changeNum(d.reIdentifyRisk || 0, 2);
121 obj['isGtThreshold'] = d.isGtThreshold;
122 resultData.value.push(obj);
123 });
124 pageInfo.value.rows = res.data?.totalRows ?? 0;
125 } else {
126 proxy.$ElMessage.error(res.msg);
127 }
128 })
129 }
130
131 /** 下载评估报告 */
132 const transfer = () => {
133 if (downPromise.value) {
134 return;
135 }
136 downPromise.value = exportAnonReport({
137 taskGuid: route.query.guid,
138 execGuid: taskExecGuid.value
139 }).then((res: any) => {
140 downPromise.value = null;
141 if (res && !res.msg) {
142 download(res, (route.query.taskName || oldAnonTaskValueInfo.value.taskName) + '_匿名化评估报告.docx', 'word')
143 } else {
144 res?.msg && ElMessage.error(res?.msg);
145 }
146 }).catch(() => {
147 downPromise.value = null;
148 })
149 }
150
151 onMounted(() => {
152 nextTick(() => {
153 containerWidth.value = containerRef.value?.offsetWidth || 0;
154 })
155 window.onresize = () => {
156 containerWidth.value = containerRef.value?.offsetWidth || 0;
157 }
158 })
159
160 onBeforeMount(() => {
161 resultDataLoading.value = true;
162 getAnonAnalyzeResult(taskExecGuid.value).then((res: any) => {
163 resultDataLoading.value = false;
164 if (res?.code == proxy.$passCode) {
165 analysisResultInfo.value = res.data || {};
166 analysisResultTableFields.value = res.data?.column || [];
167 pageInfo.value.curr = 1;
168 getAnalysisResultPageData();
169 } else {
170 res?.msg && proxy.$ElMessage.error(res.msg);
171 }
172 });
173 getAnonTaskDetail(taskGuid.value).then((res: any) => {
174 if (res?.code == proxy.$passCode) {
175 oldAnonTaskValueInfo.value = res.data || {};
176 } else {
177 res?.msg && proxy.$ElMessage.error(res.msg);
178 }
179 });
180 })
181
182 </script>
183
184 <template>
185 <div class="table_tool_wrap" v-loading="resultDataLoading" ref="containerRef">
186 <el-button style="margin-bottom: 8px;" type="primary" @click="transfer" v-preReClick>下载评估报告</el-button>
187 <anonResultAnalysis :show-title="true" :analysis-result-info="analysisResultInfo"
188 :analysis-result-loading="analysisResultLoading" :analysis-result-table-fields="analysisResultTableFields"
189 :old-anon-task-value-info="oldAnonTaskValueInfo" :container-width="containerWidth"
190 :origin-result-table-field-column="originResultTableFieldColumn" :page-info="pageInfo" :result-data="resultData"
191 @page-change="pageChange"></anonResultAnalysis>
192 </div>
193 </template>
194
195 <style lang="scss" scoped>
196 .table_tool_wrap {
197 width: 100%;
198 height: 100%;
199 padding: 8px 16px 16px;
200 overflow-y: auto;
201 }
202 </style>
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 getLastAnonAnalyzeResult,
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 rows: 0,
42 })
43
44 const getData = () => {
45 tableData.value = [];
46 if (!tableFields.value?.length) {
47 return;
48 }
49 tableDataLoading.value = true;
50 getAnonPageData({
51 pageIndex: pageInfo.value.curr,
52 pageSize: pageInfo.value.limit,
53 taskExecGuid: props.isPage ? route.query.execGuid : props.execGuid,
54 }).then((res: any) => {
55 tableDataLoading.value = false;
56 if (res.code == proxy.$passCode) {
57 tableData.value = [];
58 res.data?.records?.forEach(d => {
59 let obj = {};
60 tableFields.value.forEach(t => {
61 obj[t.enName] = d.fieldValue?.[t.enName];
62 });
63 tableData.value.push(obj);
64 });
65 pageInfo.value.rows = res.data?.totalRows ?? 0;
66 } else {
67 ElMessage.error(res.msg);
68 }
69 });
70 }
71
72 const getTextAlign = (field) => {
73 if (field.dataType === 'decimal' || field.dataType === 'int' || field.dataType == 'bit' || field.dataType == 'tinyint') {
74 return 'right';
75 }
76 return 'left'
77 }
78
79 /** otherWidth表示使用标题宽度时添加标题排序图标等宽度 */
80 const calcTableColumnWidth = (data: any[], prop, title, otherWidth = 0) => {
81 let d: any[] = [];
82 data.forEach((dt) => d.push(dt[prop]));
83 return calcColumnWidth(
84 d,
85 title,
86 {
87 fontSize: 14,
88 fontFamily: "SimSun",
89 },
90 {
91 fontSize: 14,
92 fontFamily: "SimSun",
93 },
94 otherWidth
95 );
96 };
97
98 /** 每列字段对应的列宽计算结果。 */
99 const originTableFieldColumn = ref({});
100
101 watch(
102 tableData,
103 (val: any[], oldVal) => {
104 if (!tableFields.value?.length) {
105 originTableFieldColumn.value = {};
106 return;
107 }
108 originTableFieldColumn.value = {};
109 tableFields.value.forEach((field, index) => {
110 originTableFieldColumn.value[field.enName] = calcTableColumnWidth(
111 val?.slice(0, 20) || [],
112 field.enName,
113 field.chName,
114 24
115 );
116 });
117 },
118 {
119 deep: true,
120 }
121 );
122
123 watch(() => props.execGuid, (val) => {
124 if (!val) {
125 return;
126 }
127 tableDataLoading.value = true;
128 getLastAnonAnalyzeResult(val).then((res: any) => {
129 tableDataLoading.value = false;
130 if (res.code == proxy.$passCode) {
131 let column = res.data?.column || {};
132 tableFields.value = column;
133 pageInfo.value.curr = 1;
134 getData();
135 } else {
136 ElMessage.error(res.msg);
137 }
138 });
139 }, {
140 immediate: true
141 })
142
143 onBeforeMount(() => {
144 if (!props.isPage) {
145 return;
146 }
147 tableDataLoading.value = true;
148 getLastAnonAnalyzeResult(route.query.execGuid).then((res: any) => {
149 tableDataLoading.value = false;
150 if (res.code == proxy.$passCode) {
151 let column = res.data?.column || {};
152 tableFields.value = column;
153 getData();
154 } else {
155 ElMessage.error(res.msg);
156 }
157 });
158 });
159
160 const formatterPreviewDate = (row, info) => {
161 let enName = info.enName;
162 let v = row[enName];
163 if (v === 0) {
164 return v;
165 }
166 if (!v || v == 'null') {
167 return '--';
168 }
169 if (info.dataType === 'datetime') {
170 return Moment(v).format('YYYY-MM-DD HH:mm:ss');
171 }
172 if (info.dataType === 'date') {
173 if (isNaN(<any>(new Date(v)))) {
174 return Moment(parseInt(v)).format('YYYY-MM-DD');
175 } else {
176 return Moment(v).format('YYYY-MM-DD');
177 }
178 }
179 return v;
180 };
181
182 const pageChange = (info) => {
183 pageInfo.value.curr = Number(info.curr);
184 pageInfo.value.limit = Number(info.limit);
185 getData();
186 }
187
188 const promise: any = ref(null);
189
190 const exportData = () => {
191 if (promise.value) {
192 return;
193 }
194 promise.value = exportAnonExecData({
195 taskGuid: route.query.guid,
196 execGuid: route.query.execGuid
197 }).then((res: any) => {
198 promise.value = null;
199 if (res && !res.msg) {
200 download(res, route.query.taskName + '_匿名化数据.xlsx', 'excel')
201 } else {
202 res?.msg && ElMessage.error(res?.msg);
203 }
204 }).catch(() => {
205 promise.value = null;
206 })
207 }
208
209 </script>
210
211 <template>
212 <div class="table_tool_wrap" v-loading="tableDataLoading">
213 <el-button v-show="props.isPage" style="margin-bottom: 8px;" type="primary" @click="exportData"
214 v-preReClick>导出数据</el-button>
215 <el-table ref="tableRef" v-show="tableFields.length" :data="tableData" :highlight-current-row="true" stripe border
216 tooltip-effect="light" height="100%" row-key="guid" :style="{ width: '100%', height: !props.isPage ? 'calc(100% - 34px)' : 'calc(100% - 64px)' }">
217 <template v-for="(item, index) in (tableFields || [])">
218 <el-table-column :label="item.chName" :width="item.dataType === 'datetime'
219 ? TableColumnWidth.DATETIME
220 : item.dataType === 'date'
221 ? TableColumnWidth.DATE
222 : originTableFieldColumn[item.enName]
223 " :align="getTextAlign(item)" :header-align="getTextAlign(item)"
224 :formatter="(row) => formatterPreviewDate(row, item)" :show-overflow-tooltip="true">
225 </el-table-column>
226 </template>
227 </el-table>
228 <div v-show="!tableFields.length" class="empty-content">
229 <img src="../../assets/images/empty-data.png" :style="{ width: '168px', height: '96px' }" />
230 <div class="empty-text">暂无数据</div>
231 </div>
232 <PageNav :class="[pageInfo.type]" :pageInfo="pageInfo" @pageChange="pageChange" />
233 </div>
234 </template>
235
236 <style lang="scss" scoped>
237 .table_tool_wrap {
238 width: 100%;
239 height: 100%;
240 padding: 8px 16px 16px;
241
242 .tips_text {
243 font-size: 14px;
244 color: var(--el-text-color-tip);
245 display: block;
246 font-weight: normal;
247 margin-bottom: 8px;
248 line-height: 21px;
249 }
250
251 .el-table {
252 display: inline-block;
253 }
254
255 .empty-content {
256 display: flex;
257 align-items: center;
258 justify-content: center;
259 height: 100%;
260 width: 100%;
261 flex-direction: column;
262
263 .empty-text {
264 font-size: 14px;
265 color: #b2b2b2;
266 }
267 }
268 }
269 </style>
...\ No newline at end of file ...\ No newline at end of file
1 <route lang="yaml">
2 name: anonTaskCreate
3 </route>
4
5 <template>
6 <div class="container_wrap full" v-loading="fullscreenLoading" ref="containerRef">
7 <div class="content_main">
8 <!-- 顶部步骤条 -->
9 <div class="top_tool_wrap">
10 <StepBar :steps-info="stepsInfo" :style="{ width: stepsInfo.list.length == 2 ? '30%' : '60%' }" />
11 </div>
12 <!-- 第一步 数据输入 -->
13 <div class="operator_panel_wrap" v-show="step == 0">
14 <ContentWrap id="id-baseInfo" title="数据选择" description="" style="margin-top: 8px;">
15 <!-- 数据选择相关属性表单设置 -->
16 <Form ref="formRef" :itemList="dataSelectInfoItems" :rules="dataSelectInfoFormRules"
17 formId="model-select-edit" col="col3 custom-form" @select-change="handleDataSelectFormSelectChange"
18 @uploadFileChange="uploadFileChange" @checkboxChange="handleDataSelectFormCheckboxChange" />
19 </ContentWrap>
20 <ContentWrap id="id-previewData" title="数据抽样预览" description="" style="margin-top: 16px;">
21 <!-- 选择抽样预览的表单设置 -->
22 <Form ref="dataSimpleFormRef" :itemList="dataSimpleFormItems" :rules="dataSimpleFormRules"
23 formId="data-simple-edit" col="col3 fixwidth-form" @switch-change="handleDataSimpleFormSwitchChange"
24 @input-change="handleDataSimpleFormChange" />
25 <!-- 抽样预览的数据表格设置 -->
26 <div class="table-v2-main" v-show="dataSimpleFormRef?.formInline?.enableSamplingRate == 'Y'"
27 v-loading="sampleTableDataLoading">
28 <el-table ref="tableRef" v-show="sampleTableFields.length" :data="sampleTableData"
29 :highlight-current-row="true" stripe border tooltip-effect="light" height="100%" row-key="guid"
30 :style="{ width: '100%', height: '240px' }">
31 <el-table-column label="序号" type="index" width="56px" align="center"
32 show-overflow-tooltip></el-table-column>
33 <template v-for="(item, index) in (sampleTableFields || [])">
34 <el-table-column :label="item.chName" :width="item.dataType === 'datetime'
35 ? TableColumnWidth.DATETIME
36 : item.dataType === 'date'
37 ? TableColumnWidth.DATE
38 : originTableFieldColumn[item.enName]
39 " :align="getTextAlign(item)" :header-align="getTextAlign(item)"
40 :formatter="(row) => formatterPreviewDate(row, item)" :show-overflow-tooltip="true">
41 </el-table-column>
42 </template>
43 </el-table>
44 <div v-show="!sampleTableFields.length" class="main-placeholder">
45 <img src="../../assets/images/no-data.png" :style="{ width: '96px', height: '96px' }" />
46 <div class="empty-text">暂无抽样数据</div>
47 </div>
48 </div>
49 </ContentWrap>
50 </div>
51 <!-- 第二步 配置匿名化方案,单独抽取vue组件页面 -->
52 <anonTaskStepTwo ref="anonTaskStepTwoRef" v-show="step == 1" :anonTaskRules="detailInfo.anonTaskRules"
53 :isFile="formRef?.formInline?.file?.length > 0" :anonPrivacyMode="detailInfo.anonPrivacyMode"
54 :fieldTypeList="fieldTypeList" :fieldNameList="sampleTableFields">
55 </anonTaskStepTwo>
56 <!-- 第三步 结果分析 -->
57 <div class="operator_panel_wrap" v-show="step == 2">
58 <ContentWrap class="anlysis-content-wrap" id="analysis-result" title="匿名结果分析" description=""
59 style="margin-top: 8px;">
60 <div class="wait-result-div" v-show="!isExecEnd">
61 <img class="loading-img" src="../../assets/images/loading.gif" />
62 <div class="desc">正在进行匿名化处理,请稍候...</div>
63 <el-button :icon="RefreshRight" link @click="refreshQueryData" v-preReClick>刷新查看结果</el-button>
64 </div>
65 <div class="wait-result-div" v-show="isExecEnd && analysisResultInfo.status == 'E'">
66 <el-icon class="failed">
67 <CircleCloseFilled />
68 </el-icon>
69 <div class="error-desc">{{ '执行失败,请返回上一步修改配置或联系管理员' }}</div>
70 <div v-show="analysisResultInfo.errorMsg" class="error-desc">{{ '【' + analysisResultInfo.errorMsg + '】' }}
71 </div>
72 </div>
73 <anonResultAnalysis v-show="isExecEnd && analysisResultInfo.status == 'Y'" :analysis-result-info="analysisResultInfo"
74 :analysis-result-loading="analysisResultLoading" :analysis-result-table-fields="analysisResultTableFields" :old-anon-task-value-info="oldAnonTaskValueInfo"
75 :container-width="containerWidth" :origin-result-table-field-column="originResultTableFieldColumn" :page-info="pageInfo"
76 :result-data="resultData" @page-change="pageChange"></anonResultAnalysis>
77 <template #header>
78 <el-button v-show="isExecEnd && analysisResultInfo.status == 'Y'" type="primary" v-loading="!!downPromise"
79 @click="transfer">下载评估报告</el-button>
80 </template>
81 </ContentWrap>
82 </div>
83 <!-- 匿名化结果展示 -->
84 <div class="operator_panel_wrap step-result" v-show="step == 3" style="height: calc(100% - 88px);">
85 <ContentWrap id="analysis-result" title="匿名化数据结果" description="" style="margin-top: 8px;height: 100%;">
86 <!-- 匿名化结果数据查看页面,单独抽取业务组件,新开页面要使用 -->
87 <anonResultView :is-page="false"
88 :execGuid="analysisResultInfo.status == 'Y' && step == 3 ? taskExecGuid : ''">
89 </anonResultView>
90 </ContentWrap>
91 </div>
92 </div>
93 <!-- 底部按钮,需要根据当前步骤条来展示对应的按钮 -->
94 <div class="bottom_tool_wrap">
95 <template v-if="step == 0">
96 <el-button @click="cancelTask">取消</el-button>
97 <el-button type="primary" @click="changeStep(formRef?.formInline?.handleType == '02' ? 3 : 2)">下一步</el-button>
98 </template>
99 <template v-else-if="step == 1">
100 <el-button @click="changeStep(1)">上一步</el-button>
101 <el-button type="primary" @click="changeStep(3)">下一步</el-button>
102 </template>
103 <template v-else-if="step == 2">
104 <el-button @click="changeStep(formRef?.formInline?.handleType == '02' ? 1 : 2)">上一步</el-button>
105 <el-button v-show="formRef?.formInline?.handleType != '02'" type="primary"
106 :disabled="analysisResultInfo.status == 'R' || (isExecEnd && analysisResultInfo.status == 'E')"
107 @click="changeStep(4)">下一步</el-button>
108 <el-button type="primary" v-show="formRef?.formInline?.handleType == '02'" :disabled="analysisResultInfo.status == 'R' || (isExecEnd && analysisResultInfo.status == 'E')" v-preReClick @click="closeTask">关闭</el-button>
109 </template>
110 <template v-else>
111 <el-button @click="changeStep(3)">上一步</el-button>
112 <el-button type="primary" v-preReClick @click="exportResult">导出</el-button>
113 </template>
114 </div>
115 </div>
116 </template>
117
118 <script lang="ts" setup name="anonTaskCreate">
119 import {
120 dataSourceTypeList,
121 getAnonTaskDetail,
122 getParamsList,
123 chTransformEn,
124 getAnonAnalyzeResult,
125 getAnonAnalyzePageData,
126 getDatabase,
127 getDsTableByDs,
128 getDsTableFieldColumn,
129 getDsTableSampleData,
130 saveAnonTask,
131 updateAnonTask,
132 exportAnonExecData,
133 exportAnonReport,
134 } from '@/api/modules/dataAnonymization';
135 import {
136 parseAndDecodeUrl,
137 getDownFileSignByUrl,
138 obsDownloadRequest
139 } from "@/api/modules/obsService";
140 import {
141 getAreaData
142 } from "@/api/modules/queryService";
143 import useUserStore from "@/store/modules/user";
144 import { useValidator } from '@/hooks/useValidator';
145 import { TableColumnWidth } from '@/utils/enum';
146 import { calcColumnWidth } from "@/utils/index";
147 import Moment from 'moment';
148 import anonTaskStepTwo from './anonTaskStepTwo.vue';
149 import * as XLSX from 'xlsx';
150 import { ElMessage } from 'element-plus';
151 import { isEqual, cloneDeep } from "lodash-es";
152 import { changeNum, download } from "@/utils/common";
153 import anonResultView from './anonResultView.vue';
154 import useDataAnonymizationStore from "@/store/modules/dataAnonymization";
155 import { RefreshRight, CircleCloseFilled, Right } from "@element-plus/icons-vue";
156 import { commonPageConfig } from '@/components/PageNav';
157 import { QuestionFilled } from "@element-plus/icons-vue";
158 import anonResultAnalysis from './components/anonResultAnalysis.vue';
159
160 const anonymizationStore = useDataAnonymizationStore();
161 const { proxy } = getCurrentInstance() as any;
162 const userStore = useUserStore();
163 const route = useRoute();
164 const router = useRouter();
165 const fullPath = route.fullPath;
166 const taskGuid = ref(route.query.guid);
167 /** 提交保存和编辑后的执行guid */
168 const taskExecGuid = ref('');
169 /** 是否执行结束,用于第四步获取执行结果 */
170 const isExecEnd = ref(false);
171 const { required } = useValidator();
172 const fullscreenLoading = ref(false);
173 const containerRef = ref();
174
175 const containerWidth = ref(containerRef.value?.offsetWidth || 0)
176
177 const step = ref(0);
178 const originStepsInfo = ref({
179 step: step.value,
180 list: [
181 { title: '数据输入', value: 1 },
182 { title: '配置匿名化方案', value: 2 },
183 { title: '匿名结果分析', value: 3 },
184 { title: '结果输出', value: 4 }
185 ]
186 })
187 const stepsInfo = ref(originStepsInfo.value);
188
189 /** 已匿名化出具报告的 */
190 const reportStepsInfo = ref({
191 step: step.value,
192 list: [
193 { title: '数据输入', value: 1 },
194 // { title: '配置匿名化方案', value: 2 },
195 { title: '匿名结果分析', value: 2 },
196 // { title: '结果输出', value: 4 }
197 ]
198 })
199
200 /** 数据源列表 */
201 const dataSourceList: any = ref([]);
202
203 /** 数据源对应的数据表 */
204 const dsTableList: any = ref([]);
205
206 /** 数据共享类型字段列表 */
207 const dataSharingTypeList = ref([]);
208
209 /** 匿名化处理类型 */
210 const handleTypeList = ref([]);
211
212 const getParentAreaPromise: any = ref(null);
213 const getAreaDataPromise: any = ref({});
214 const getAreaDatas: any = ref({});
215 const parentAreaData: any = ref([]);
216
217 const getArea = (node, resolve) => {
218 const { level } = node
219 let params = {
220 parentGuid: node.value
221 }
222 if (!node.value) {
223 if (getParentAreaPromise.value) {
224 getParentAreaPromise.value.then((res: any) => {
225 resolve(res);
226 })
227 } else {
228 resolve(parentAreaData.value);
229 }
230 return;
231 }
232 if (node.loaded) {
233 resolve([]);
234 return;
235 }
236 if (getAreaDatas.value[node.value]?.length) {
237 resolve(getAreaDatas.value[node.value]);
238 return;
239 }
240 if (!getAreaDataPromise.value[node.value]) {
241 getAreaDataPromise.value[node.value] = getAreaData(params).then((res: any) => {
242 node.loaded = true;
243 getAreaDataPromise.value[node.value] = null;
244 if (res?.code == proxy.$passCode) {
245 const data = res.data ?? []
246 data.map(item => {
247 item.leaf = level >= 1
248 })
249 resolve(data)
250 getAreaDatas.value[node.value] = data;
251 return data;
252 }
253 })
254 } else {
255 getAreaDataPromise.value[node.value].then((data) => {
256 getAreaDataPromise.value[node.value] = null;
257 node.loaded = true;
258 data.map(item => {
259 item.leaf = level >= 1
260 })
261 resolve(data)
262 })
263 }
264 }
265
266 const formRef = ref();
267
268 /** 数据选择的表单配置信息 */
269 const dataSelectInfoItems = ref([{
270 label: '数据集名称',
271 type: 'input',
272 placeholder: '请输入',
273 field: 'taskName',
274 maxlength: 15,
275 default: '',
276 required: true,
277 filterable: true,
278 clearable: true,
279 visible: true,
280 }, {
281 label: '数据共享类型',
282 type: 'select',
283 placeholder: '请选择',
284 field: 'dataSharingTypeCode',
285 default: '01',
286 options: dataSharingTypeList.value,
287 props: {
288 label: "label",
289 value: "value",
290 },
291 required: true,
292 filterable: true,
293 clearable: true,
294 visible: true,
295 },
296 {
297 label: '所属地域',
298 type: "cascader",
299 placeholder: "请选择",
300 field: "coverageArea",
301 default: [],
302 showAllLevels: true,
303 props: {
304 label: 'name',
305 value: 'guid',
306 lazy: true,
307 checkStrictly: true,
308 lazyLoad: getArea,
309 multiple: false,
310 },
311 collapse: true,
312 tagsTooltip: true,
313 // filterable: true,
314 clearable: true,
315 required: false, //不选默认表示全国。
316 //col: 'checkbox-right',
317 visible: true
318 },
319 // { 去掉,直接用数据集总行数/全国人口总数计算
320 // label: '患者占总人口比',
321 // type: 'input',
322 // placeholder: '数值,支持小数点9位',
323 // field: 'patientPopulationRate',
324 // maxlength: 11,
325 // min: 0,
326 // max: 1,
327 // inputType: 'scoreNumber',
328 // decimalCnt: 9,
329 // default: '',
330 // required: false,
331 // filterable: true,
332 // clearable: true,
333 // visible: true,
334 // },
335 {
336 label: '处理类型',
337 type: 'select',
338 placeholder: '请选择',
339 field: 'handleType',
340 default: '01',
341 options: handleTypeList.value,
342 props: {
343 label: "label",
344 value: "value",
345 },
346 required: true,
347 filterable: true,
348 clearable: true,
349 visible: true,
350 },
351 {
352 label: '数据来源',
353 type: 'select',
354 placeholder: '请选择',
355 field: 'dataSource',
356 default: 1,
357 options: dataSourceTypeList,
358 props: {
359 label: "label",
360 value: "value",
361 },
362 required: true,
363 filterable: true,
364 visible: true,
365 }, {
366 label: '数据源',
367 type: 'select',
368 placeholder: '请选择',
369 field: 'dataSourceGuid',
370 default: '',
371 options: dataSourceList.value,
372 props: {
373 label: 'databaseNameZh',
374 value: 'guid'
375 },
376 filterable: true,
377 visible: true,
378 required: true
379 }, {
380 label: "数据表",
381 type: "select",
382 placeholder: "请选择",
383 field: "tableName",
384 options: dsTableList.value,
385 props: {
386 label: 'tableComment',
387 value: 'tableName'
388 },
389 default: '',
390 filterable: true,
391 clearable: true,
392 required: true,
393 }, {
394 label: "准标识符",
395 type: "select",
396 placeholder: "请选择",
397 field: "qualifiedIdentifier",
398 options: dsTableList.value,
399 props: {
400 label: 'chName',
401 value: 'enName'
402 },
403 default: [],
404 multiple: true,
405 collapse: true,
406 tagsTooltip: true,
407 filterable: true,
408 clearable: true,
409 required: true,
410 visible: false,
411 }, {
412 label: '文件上传',
413 tip: '支持扩展名:xlsx、xls、csv,文件大小不超过10MB',
414 type: 'upload-file',
415 accept: '.xlsx, .xls, .csv',
416 limitSize: 10,
417 limit: 1,
418 isExcel: true,
419 required: true,
420 default: <any>[],
421 block: false,
422 col: 'wid60',
423 visible: false,
424 field: 'file',
425 },]);
426
427 const dataSelectInfoFormRules = ref({
428 taskName: [required('请输入数据集名称')],
429 dataSharingTypeCode: [required('请选择数据共享类型')],
430 // patientPopulationRate: [required('请输入患者占总人口比')],
431 dataSourceGuid: [required('请选择数据源')],
432 handleType: [required('请选择处理类型')],
433 tableName: [required('请选择数据表')],
434 qualifiedIdentifier: [{ type: 'array', required: true, trigger: 'change', message: "请选择准标识符" }],
435 file: [{
436 validator: (rule: any, value: any, callback: any) => {
437 if (!value?.length) {
438 callback(new Error('请上传文件'))
439 } else {
440 callback();
441 }
442 }, trigger: 'change'
443 }]
444 });
445
446 /** 最新选中的 */
447 const currDatasourceSelect: any = ref({});
448
449 const handleDataSelectFormSelectChange = async (val, row, formInfo) => {
450 if (row.field == 'dataSource') {
451 dataSelectInfoItems.value[5].visible = val == 1;
452 dataSelectInfoItems.value[6].visible = val == 1;
453 dataSelectInfoItems.value[8].visible = val == 2;
454 setDataSelectFormItems(Object.assign({}, formInfo, { file: !formInfo['file'] ? [] : formInfo['file'] }))
455 sampleTableFields.value = [];
456 parseFileDataSum.value = [];
457 sampleTableData.value = [];
458 } else if (row.field == 'dataSourceGuid') {
459 if (!val) {
460 currDatasourceSelect.value = [];
461 sampleTableFields.value = [];
462 parseFileDataSum.value = [];
463 sampleTableData.value = [];
464 setDataSelectFormItems(Object.assign({}, formInfo, { file: !formInfo['file'] ? [] : formInfo['file'], tableName: '', qualifiedIdentifier: [] }))
465 let item = dataSelectInfoItems.value.find(d => d.field == 'tableName');
466 item && (item.options = dsTableList.value);
467 return;
468 }
469 let dsInfo = currDatasourceSelect.value = dataSourceList.value.find(d => d.guid == val);
470 //清除数据表得值,重新获取下拉列表
471 const res: any = await getDsTableByDs({
472 pageSize: -1,
473 pageIndex: 1,
474 dataSourceGuid: val,
475 database: dsInfo.databaseNameEn,
476 databaseType: dsInfo.databaseType,
477 tableName: '',
478 hadFlag: false
479 });
480 if (res.code == proxy.$passCode) {
481 dsTableList.value = res.data?.records || [];
482 setDataSelectFormItems(Object.assign({}, formInfo, { file: !formInfo['file'] ? [] : formInfo['file'], tableName: '', qualifiedIdentifier: [] }))
483 let item = dataSelectInfoItems.value.find(d => d.field == 'tableName');
484 item && (item.options = dsTableList.value);
485 } else {
486 proxy.$ElMessage.error(res.msg);
487 }
488 sampleTableFields.value = [];
489 parseFileDataSum.value = [];
490 sampleTableData.value = [];
491 } else if (row.field == 'tableName') {
492 if (!val) {
493 sampleTableFields.value = [];
494 sampleTableData.value = [];
495 return;
496 }
497 getDsTableFieldColumn({
498 pageSize: 50,
499 pageIndex: 1,
500 dataSourceGuid: currDatasourceSelect.value.guid,
501 database: currDatasourceSelect.value.databaseNameEn,
502 databaseType: currDatasourceSelect.value.databaseType,
503 tableName: val,
504 }).then((res: any) => {
505 if (res.code == proxy.$passCode) {
506 sampleTableFields.value = res.data?.map(d => {
507 d.fieldDataType = d.dataType;
508 d.enName = d.columnName;
509 d.chName = d.columnZhName;
510 return d;
511 }) || [];
512 /** 判断有抽样数据,需要查询接口 */
513 getSampleDataByDsTable();
514 } else {
515 ElMessage.error(res.msg);
516 }
517 });
518 } else if (row.field == 'handleType') {
519 setDataSelectFormItems(formInfo);
520 }
521 }
522
523 const setDataSelectFormItems = (info, isDetail = false) => {
524 dataSelectInfoItems.value.forEach(item => {
525 item.default = info[item.field];
526 if (item.field == 'coverageArea') {
527 // item && item.children?.length && (item.children[0].visible = info['coverageArea'] != 'all');
528 if (!isDetail) {
529 return;
530 }
531 let coverageArea = info.coverageArea;
532 if (coverageArea && Array.isArray(coverageArea) && coverageArea.length > 0) {
533 item.default = coverageArea[0] as any;
534 let p: any = [];
535 coverageArea?.forEach(area => {
536 if (p.includes(area[0])) {
537 return;
538 }
539 p.push(area[0]);
540 getArea({ value: area[0], level: 1 }, () => { })
541 });
542 let ps: any = []
543 for (const key in getAreaDataPromise.value) {
544 ps.push(getAreaDataPromise.value[key])
545 }
546 Promise.all(ps).then(() => {
547 item.default = coverageArea[0];
548 });
549 } else {
550 item.default = '';
551 }
552 } else if (item.field == 'qualifiedIdentifier') {
553 item.visible = info['handleType'] == '02';
554 }
555 });
556 stepsInfo.value = info.handleType == '02' ? reportStepsInfo.value : originStepsInfo.value;
557 }
558
559 const handleDataSelectFormCheckboxChange = (val, info, row) => {
560 row.field == 'coverageArea' && setDataSelectFormItems(info);
561 }
562
563 const dataSimpleFormRef = ref();
564
565 /** 抽样数据预览 */
566 const dataSimpleFormItems = ref([{
567 label: '抽样开关',
568 type: 'switch',
569 field: 'enableSamplingRate',
570 default: 'N',
571 col: 'autoWidth',
572 activeValue: 'Y',
573 inactiveValue: 'N'
574 }, {
575 label: '抽样比例(%)',
576 type: 'input',
577 placeholder: '请输入',
578 field: 'samplingRate',
579 maxlength: 3,
580 min: 0, //可以是0条。万一只是想看下字段呢
581 max: 100,
582 inputType: 'integerNumber',
583 default: 10,
584 required: true,
585 filterable: true,
586 clearable: true,
587 visible: false,
588 }]);
589
590 const dataSimpleFormRules = ref({
591 samplingRate: [required('请填写抽样比例')],
592 });
593
594 const oldSamplingRate = ref('10');
595
596 const handleDataSimpleFormSwitchChange = (val, info) => {
597 if (val == 'N') {
598 oldSamplingRate.value = info.samplingRate;
599 } else {
600 dataSimpleFormItems.value[1].default = oldSamplingRate.value || 10;
601 }
602 dataSimpleFormItems.value[1].visible = val == 'Y';
603 dataSimpleFormItems.value[0].default = info.enableSamplingRate || 'N';
604 if (formRef.value?.formInline?.file?.length) {
605 transferSampleData();
606 } else {
607 getSampleDataByDsTable();
608 }
609 }
610
611 /** 输入抽样比例值改变 */
612 const handleDataSimpleFormChange = (val) => {
613 if (formRef.value?.formInline?.file?.length) {
614 transferSampleData();
615 } else {
616 getSampleDataByDsTable();
617 }
618 }
619
620 /** 样本表格加载中 */
621 const sampleTableDataLoading = ref(false);
622
623 /** 样本表格的数据 */
624 const sampleTableData: any = ref([]);
625
626 /** 样本表格的字段 */
627 const sampleTableFields: any = ref([]);
628
629 /** otherWidth表示使用标题宽度时添加标题排序图标等宽度 */
630 const calcTableColumnWidth = (data: any[], prop, title, otherWidth = 0) => {
631 let d: any[] = [];
632 data.forEach((dt) => d.push(dt[prop]));
633 return calcColumnWidth(
634 d,
635 title,
636 {
637 fontSize: 14,
638 fontFamily: "SimSun",
639 },
640 {
641 fontSize: 14,
642 fontFamily: "SimSun",
643 },
644 otherWidth
645 );
646 };
647
648 /** 每列字段对应的列宽计算结果。 */
649 const originTableFieldColumn = ref({});
650
651 const getTextAlign = (field) => {
652 if (field.dataType === 'decimal' || field.dataType === 'int') {
653 return 'right';
654 }
655 return 'left'
656 }
657
658 watch(
659 sampleTableData,
660 (val: any[], oldVal) => {
661 if (!sampleTableFields.value?.length) {
662 originTableFieldColumn.value = {};
663 return;
664 }
665 originTableFieldColumn.value = {};
666 sampleTableFields.value.forEach((field, index) => {
667 originTableFieldColumn.value[field.enName] = calcTableColumnWidth(
668 val?.slice(0, 20) || [],
669 field.enName,
670 field.chName,
671 24
672 );
673 });
674 },
675 {
676 deep: true,
677 }
678 );
679
680 watch(() => sampleTableFields.value, (val) => {
681 let item = dataSelectInfoItems.value.find(selectItem => selectItem.field == 'qualifiedIdentifier');
682 item && (item.options = val);
683 let formInfo = formRef.value.formInline;
684 if (formInfo.handleType == '02' && !(taskGuid.value && (formInfo.file?.[0] && formInfo.file?.[0]?.url == detailInfo.value.filePath?.url || (formInfo.tableName && formInfo.tableName == detailInfo.value.tableName)))) {//需要同步清除准标识符的字段选择
685 setDataSelectFormItems(Object.assign({}, formInfo, { file: !formInfo['file'] ? [] : formInfo['file'], qualifiedIdentifier: [] }))
686 }
687 }, {
688 deep: true
689 })
690
691 const formatterPreviewDate = (row, info) => {
692 let enName = info.enName;
693 let v = row[enName];
694 if (v === 0) {
695 return v;
696 }
697 if (!v || v == 'null') {
698 return '--';
699 }
700 if (info.dataType === 'datetime') {
701 return Moment(v).format('YYYY-MM-DD HH:mm:ss');
702 }
703 if (info.dataType === 'date') {
704 if (isNaN(<any>(new Date(v)))) {
705 return Moment(parseInt(v)).format('YYYY-MM-DD');
706 } else {
707 return Moment(v).format('YYYY-MM-DD');
708 }
709 }
710 return v;
711 };
712
713 /** 解析的总的表格数据,方便后面修改抽样比例时使用 */
714 const parseFileDataSum: any = ref([]);
715 const currentSheet: any = ref();
716 const parseFileData = (fileRaw) => {
717 sampleTableDataLoading.value = true;
718 fileRaw.arrayBuffer().then(async (f) => {
719 const wb = XLSX.read(f, {
720 raw: false, cellDates: true
721 });
722 const sheet = wb.Sheets[wb.SheetNames[0]];
723 currentSheet.value = sheet;
724 const json: any[] = XLSX.utils.sheet_to_json(sheet, { header: 1 });
725 if (json.length == 0) {
726 sampleTableFields.value = [];
727 sampleTableData.value = [];
728 } else {
729 try {
730 const res: any = await chTransformEn(json[0]);
731 if (res?.code != proxy.$passCode) {
732 sampleTableDataLoading.value = false;
733 proxy.$ElMessage.error(res.msg);
734 return;
735 }
736 let fields = res.data || [];
737 sampleTableFields.value = fields?.map((j, index) => {
738 return {
739 index: index,
740 enName: j.enName + '',
741 chName: j.chName + '',
742 dataType: 'varchar'
743 }
744 }) || [];
745 parseFileDataSum.value = json;
746 /** 粗略算出字段类型 */
747 json.slice(1, 10).forEach((info, row) => {
748 json[0].forEach((name, col) => {
749 if (info[col] === "" || info[col] == null || sampleTableFields.value[col].dataType != 'varchar') {
750 return;
751 } else {
752 var cellRef = XLSX.utils.encode_cell({ r: row + 1, c: col });
753 var cell = sheet[cellRef];
754 let v = cell.w || info[col];
755 let isNum = cell.t == 'n';
756 if (isNum) {
757 if (v.includes('.') && sampleTableFields.value[col].dataType != 'decimal') {
758 sampleTableFields.value[col].dataType = 'decimal';
759 } else {
760 sampleTableFields.value[col].dataType = 'int';
761 }
762 }
763 }
764 });
765 })
766 transferSampleData();
767 sampleTableDataLoading.value = false;
768 } catch (error) {
769 sampleTableDataLoading.value = false;
770 }
771 }
772 });
773 }
774
775 /** 获取文件解析后根据抽样比例得出的表格数据,默认查看前500条数据 */
776 const transferSampleData = () => {
777 let samplingRate = dataSimpleFormRef.value?.formInline?.samplingRate;
778 if (parseFileDataSum.value.length > 1 && samplingRate) {
779 let totalCnt = parseFileDataSum.value.length - 1;
780 let cnt = Math.ceil(samplingRate * 0.01 * totalCnt) + 1;
781 sampleTableData.value = parseFileDataSum.value.slice(1, cnt > 500 ? 501 : cnt).map((info, row) => {
782 let object = {};
783 parseFileDataSum.value[0].forEach((chName, col) => {
784 let name = sampleTableFields.value[col].enName;
785 var cellRef = XLSX.utils.encode_cell({ r: row + 1, c: col });
786 var cell = currentSheet.value[cellRef];
787 let v = cell.w || info[col];
788 object[name] = v;
789 });
790 return object;
791 });
792 } else {
793 sampleTableData.value = [];
794 }
795 }
796
797 /** 获取选择的数据库表根据抽样比例得出的表格数据 */
798 const getSampleDataByDsTable = () => {
799 const tableName = formRef.value?.formInline?.tableName;
800 if (!currDatasourceSelect.value.guid || !tableName) {
801 sampleTableFields.value = [];
802 sampleTableData.value = [];
803 return;
804 }
805 let samplingRate = dataSimpleFormRef.value?.formInline?.samplingRate;
806 if (!samplingRate) {
807 sampleTableData.value = [];
808 return;
809 }
810 let totalCnt = dsTableList.value.find(t => t.tableName == tableName)?.tableRows || 0;
811 let cnt = Math.ceil(samplingRate * 0.01 * totalCnt);
812 if (!cnt) {
813 sampleTableData.value = [];
814 return;
815 }
816 sampleTableDataLoading.value = true;
817 getDsTableSampleData({
818 limitNum: cnt > 500 ? 500 : cnt,
819 pageSize: cnt > 500 ? 500 : cnt,
820 pageIndex: 1,
821 dataSourceGuid: currDatasourceSelect.value.guid,
822 database: currDatasourceSelect.value.databaseNameEn,
823 databaseType: currDatasourceSelect.value.databaseType,
824 tableName: tableName,
825 hadFlag: false,
826 }).then((res: any) => {
827 sampleTableDataLoading.value = false;
828 if (res.code == proxy.$passCode) {
829 sampleTableData.value = res.data?.datas || [];
830 } else {
831 sampleTableData.value = [];
832 ElMessage.error(res.msg);
833 }
834 });
835 }
836
837 const uploadFileChange = (file) => {
838 sampleTableData.value = [];
839 if (!file.length) {
840 sampleTableFields.value = [];
841 sampleTableData.value = [];
842 return;
843 }
844 let fileRaw = file[0].file;
845 parseFileData(fileRaw);
846 }
847
848 /** 第二步的配置组件引用。 */
849 const anonTaskStepTwoRef = ref();
850
851 const changeStep = async (val) => {
852 if (val <= step.value) {
853 step.value = val - 1;
854 stepsInfo.value.step = val - 1;
855 } else if (val == 2) {
856 formRef.value?.ruleFormRef?.validate((valid) => {
857 if (valid) {
858 if (formRef.value?.formInline?.dataSource == 2 && !sampleTableFields.value?.length) {
859 proxy.$ElMessage.error('上传文件的字段不能为空');
860 return;
861 }
862 dataSimpleFormRef.value?.ruleFormRef?.validate((valid) => {
863 if (valid) {
864 // 第一步到第二步时,如果字段列表中与字段脱敏规则中的字段不匹配,应清空。
865 step.value = val - 1;
866 stepsInfo.value.step = val - 1;
867 anonTaskStepTwoRef.value?.updateNextStepRules();
868 }
869 });
870 }
871 });
872 } else if (val == 3) {
873 let exec = (saveParams) => {
874 if (saveParams.coverageArea?.length) {
875 saveParams.coverageArea = [saveParams.coverageArea];
876 } else {
877 saveParams.coverageArea = [];
878 }
879 if (saveParams.file?.length) {
880 saveParams.filePath = {
881 name: saveParams.file[0].name,
882 url: saveParams.file[0].url
883 }
884 delete saveParams.file;
885 saveParams.dataSourceGuid = null;
886 saveParams.tableName = null;
887 } else {
888 saveParams.filePath = null;
889 }
890 let simpleFormInline = dataSimpleFormRef.value.formInline;
891 if (simpleFormInline.enableSamplingRate == 'Y') {
892 saveParams.samplingRate = simpleFormInline.samplingRate && parseInt(simpleFormInline.samplingRate);
893 } else {
894 saveParams.samplingRate = null;
895 }
896 if (taskGuid.value) {
897 saveParams.guid = taskGuid.value;
898 }
899 if (isEqual(saveParams, oldAnonTaskValueInfo.value)) {
900 isExecEnd.value = false;
901 step.value = val - 1;
902 stepsInfo.value.step = val - 1;
903 if (!analysisResultInfo.value?.status) {
904 processStepThreeResultView();
905 } else {
906 isExecEnd.value = analysisResultInfo.value?.status == 'E' || analysisResultInfo.value?.status == 'Y';
907 }
908 return;
909 }
910 if (!taskGuid.value) { //保存
911 fullscreenLoading.value = true;
912 saveAnonTask(saveParams).then((res: any) => {
913 fullscreenLoading.value = false;
914 if (res.code == proxy.$passCode) {
915 taskGuid.value = res.data?.taskGuid;
916 isExecEnd.value = false;
917 taskExecGuid.value = res.data?.lastExecGuid;
918 step.value = val - 1;
919 stepsInfo.value.step = val - 1;
920 analysisResultInfo.value = {};
921 if (refreshTimer.value) {
922 clearInterval(refreshTimer.value);
923 refreshTimer.value = null;
924 }
925 processStepThreeResultView();
926 oldAnonTaskValueInfo.value = saveParams;
927 anonymizationStore.setIsAnonPageRefresh(true);
928 } else {
929 ElMessage.error(res.msg);
930 }
931 });
932 } else { //更新
933 fullscreenLoading.value = true;
934 updateAnonTask(saveParams).then((res: any) => {
935 fullscreenLoading.value = false;
936 if (res.code == proxy.$passCode) {
937 isExecEnd.value = false;
938 taskExecGuid.value = res.data;
939 step.value = val - 1;
940 stepsInfo.value.step = val - 1;
941 analysisResultInfo.value = {};
942 if (refreshTimer.value) {
943 clearInterval(refreshTimer.value);
944 refreshTimer.value = null;
945 }
946 processStepThreeResultView();
947 oldAnonTaskValueInfo.value = saveParams;
948 anonymizationStore.setIsAnonPageRefresh(true);
949 } else {
950 ElMessage.error(res.msg);
951 }
952 })
953 }
954 }
955 let saveParams: any = { ...formRef.value.formInline };
956 if (saveParams.handleType == '01') {
957 // 保存并提交 TODO。需要加个 记录旧值的,用来判断新值和旧值,是否发生变化,若变化则需要调用保存接口之后,再进行下一步。
958 let configInfo = await anonTaskStepTwoRef.value?.getStepTwoConfigInfo();
959 if (!configInfo) {
960 return;
961 }
962 let privacy = configInfo.anonPrivacyMode; //值是克隆过的,可以删除
963 delete privacy.isKaNumber;
964 delete privacy.isRiskThreshold;
965 delete privacy.isTcField;
966 delete privacy.isLdField;
967 // 为空时为了跟原始值保持一致
968 privacy.kaNumber = (privacy.kaNumber && parseInt(privacy.kaNumber)) ?? null;
969 privacy.riskThreshold = (privacy.riskThreshold && parseFloat(privacy.riskThreshold)) ?? null;
970 privacy.tcFieldName = privacy.tcFieldName ?? null;
971 privacy.tcThreshold = (privacy.tcThreshold && parseFloat(privacy.tcThreshold)) ?? null;
972 privacy.ldFieldName = privacy.ldFieldName ?? null;
973 privacy.ldNumber = (privacy.ldNumber && parseInt(privacy.ldNumber)) ?? null;
974 Object.assign(saveParams, configInfo);
975 exec(saveParams);
976 } else {
977 formRef.value?.ruleFormRef?.validate((valid, errorItem) => {
978 if (valid) {
979 if (formRef.value?.formInline?.dataSource == 2 && !sampleTableFields.value?.length) {
980 proxy.$ElMessage.error('上传文件的字段不能为空');
981 return;
982 }
983 dataSimpleFormRef.value?.ruleFormRef?.validate((valid) => {
984 if (valid) {
985 Object.assign(saveParams, { riskThreshold: '0.05' });
986 exec(saveParams);
987 }
988 });
989 } else {
990 var obj = Object.keys(errorItem);
991 formRef.value.ruleFormRef?.scrollToField(obj[0])
992 }
993 })
994 }
995 } else if (val == 4) {
996 //下一步之后,设置执行结束, 查看结果。
997 step.value = val - 1;
998 stepsInfo.value.step = val - 1;
999 }
1000 }
1001
1002 const promise: any = ref(null);
1003 const exportResult = () => {
1004 promise.value = exportAnonExecData({
1005 taskGuid: route.query.guid,
1006 execGuid: route.query.execGuid
1007 }).then((res: any) => {
1008 promise.value = null;
1009 if (res && !res.msg) {
1010 download(res, (route.query.taskName || oldAnonTaskValueInfo.value.taskName) + '_匿名化数据.xlsx', 'excel')
1011 } else {
1012 res?.msg && ElMessage.error(res?.msg);
1013 }
1014 }).catch(() => {
1015 promise.value = null;
1016 })
1017 }
1018
1019 /** 获取字段类型的数据字典 */
1020 const fieldTypeList: any = ref([]);
1021
1022 /** 编辑时获取的匿名化任务的详情信息 */
1023 const detailInfo: any = ref({});
1024
1025 /** 记录原始的值信息,防止上一步之后未修改数据时不调用接口 */
1026 const oldAnonTaskValueInfo: any = ref({});
1027
1028 onBeforeMount(() => {
1029 if (taskGuid.value) {
1030 fullscreenLoading.value = true;
1031 getAnonTaskDetail(taskGuid.value).then(async (res: any) => {
1032 if (res?.code == proxy.$passCode) {
1033 detailInfo.value = res.data || {};
1034 taskExecGuid.value = detailInfo.value.lastExecGuid;
1035 oldAnonTaskValueInfo.value = {
1036 guid: detailInfo.value.guid,
1037 taskName: detailInfo.value.taskName,
1038 dataSource: detailInfo.value.dataSource,
1039 filePath: detailInfo.value.filePath && cloneDeep(detailInfo.value.filePath),
1040 dataSourceGuid: detailInfo.value.dataSourceGuid,
1041 tableName: detailInfo.value.tableName,
1042 samplingRate: detailInfo.value.samplingRate,
1043 // patientPopulationRate: detailInfo.value.patientPopulationRate && typeof detailInfo.value.patientPopulationRate == 'number' ? detailInfo.value.patientPopulationRate?.toFixed(9) : detailInfo.value.patientPopulationRate,
1044 dataSharingTypeCode: detailInfo.value.dataSharingTypeCode,
1045 anonTaskRules: cloneDeep(detailInfo.value.anonTaskRules),
1046 anonPrivacyMode: {
1047 kaNumber: detailInfo.value.anonPrivacyMode?.kaNumber,
1048 ldFieldName: detailInfo.value.anonPrivacyMode?.ldFieldName,
1049 ldNumber: detailInfo.value.anonPrivacyMode?.ldNumber,
1050 riskThreshold: detailInfo.value.anonPrivacyMode?.riskThreshold,
1051 tcFieldName: detailInfo.value.anonPrivacyMode?.tcFieldName,
1052 tcThreshold: detailInfo.value.anonPrivacyMode?.tcThreshold,
1053 }
1054 }
1055 setDataSelectFormItems(Object.assign(detailInfo.value, { file: detailInfo.value.filePath ? [detailInfo.value.filePath] : [] }), true);
1056 if (detailInfo.value.samplingRate != null) {
1057 dataSimpleFormItems.value[0].default = 'Y';
1058 dataSimpleFormItems.value[1].visible = true;
1059 dataSimpleFormItems.value[1].default = detailInfo.value.samplingRate;
1060 } else {
1061 dataSimpleFormItems.value[0].default = 'N';
1062 dataSimpleFormItems.value[1].visible = false;
1063 }
1064 let dataSource = detailInfo.value.dataSource;
1065 dataSelectInfoItems.value[5].visible = dataSource == 1;
1066 dataSelectInfoItems.value[6].visible = dataSource == 1;
1067 dataSelectInfoItems.value[8].visible = dataSource == 2;
1068 try {
1069 //文件解析
1070 if (dataSource == 2) {
1071 let url = detailInfo.value.filePath?.url;
1072 sampleTableDataLoading.value = true;
1073 const refSignInfo: any = await getDownFileSignByUrl(parseAndDecodeUrl(url).fileName);
1074 if (!refSignInfo?.data) {
1075 fullscreenLoading.value = false;
1076 refSignInfo?.msg && ElMessage.error(refSignInfo?.msg);
1077 return;
1078 }
1079 const fileRes: any = await obsDownloadRequest(refSignInfo?.data);
1080 sampleTableDataLoading.value = false;
1081 if (fileRes && !fileRes.msg) {
1082 parseFileData(fileRes);
1083 } else {
1084 fileRes?.msg && ElMessage.error(fileRes?.msg);
1085 }
1086 // 会出现从文件切换到数据库时没有数据库列表的问题。
1087 const res: any = await getDatabase({ connectStatus: 1 });
1088 if (res?.code == proxy.$passCode) {
1089 dataSourceList.value = res.data || [];
1090 let item = dataSelectInfoItems.value.find(item => item.field == 'dataSourceGuid');
1091 item && (item.options = dataSourceList.value);
1092 } else {
1093 proxy.$ElMessage.error(res.msg);
1094 }
1095 } else {
1096 const res: any = await getDatabase({ connectStatus: 1 });
1097 if (res?.code == proxy.$passCode) {
1098 dataSourceList.value = res.data || [];
1099 let item = dataSelectInfoItems.value.find(item => item.field == 'dataSourceGuid');
1100 item && (item.options = dataSourceList.value);
1101 } else {
1102 proxy.$ElMessage.error(res.msg);
1103 }
1104 currDatasourceSelect.value = dataSourceList.value.find(d => d.guid == detailInfo.value.dataSourceGuid);
1105 const tableRes: any = await getDsTableByDs({
1106 pageSize: -1,
1107 pageIndex: 1,
1108 dataSourceGuid: detailInfo.value.dataSourceGuid,
1109 database: currDatasourceSelect.value.databaseNameEn,
1110 databaseType: currDatasourceSelect.value.databaseType,
1111 tableName: '',
1112 hadFlag: false
1113 });
1114 if (tableRes?.code == proxy.$passCode) {
1115 dsTableList.value = tableRes.data?.records || [];
1116 let item = dataSelectInfoItems.value.find(item => item.field == 'tableName');
1117 item && (item.options = dsTableList.value);
1118 } else {
1119 proxy.$ElMessage.error(tableRes.msg);
1120 }
1121 getDsTableFieldColumn({
1122 pageSize: 50,
1123 pageIndex: 1,
1124 dataSourceGuid: currDatasourceSelect.value.guid,
1125 database: currDatasourceSelect.value.databaseNameEn,
1126 databaseType: currDatasourceSelect.value.databaseType,
1127 tableName: detailInfo.value.tableName,
1128 }).then((res: any) => {
1129 if (res.code == proxy.$passCode) {
1130 sampleTableFields.value = res.data?.map(d => {
1131 d.fieldDataType = d.dataType;
1132 d.enName = d.columnName;
1133 d.chName = d.columnZhName;
1134 return d;
1135 }) || [];
1136 /** 判断有抽样数据,需要查询接口 */
1137 getSampleDataByDsTable();
1138 } else {
1139 ElMessage.error(res.msg);
1140 }
1141 });
1142 }
1143 fullscreenLoading.value = false;
1144 } catch (error) {
1145 fullscreenLoading.value = false;
1146 }
1147 } else {
1148 fullscreenLoading.value = false;
1149 proxy.$ElMessage.error(res.msg);
1150 }
1151 });
1152 } else {
1153 getDatabase({ connectStatus: 1 }).then((res: any) => {
1154 if (res.code == proxy.$passCode) {
1155 dataSourceList.value = res.data || [];
1156 let item = dataSelectInfoItems.value.find(item => item.field == 'dataSourceGuid');
1157 item && (item.options = dataSourceList.value);
1158 } else {
1159 proxy.$ElMessage.error(res.msg);
1160 }
1161 })
1162 }
1163 getParamsList({
1164 dictType: "数据共享类型",
1165 }).then((res: any) => {
1166 if (res?.code == proxy.$passCode) {
1167 dataSharingTypeList.value = res.data || [];
1168 let item = dataSelectInfoItems.value.find(item => item.field == 'dataSharingTypeCode');
1169 item && (item.options = dataSharingTypeList.value);
1170 } else {
1171 proxy.$ElMessage.error(res.msg);
1172 }
1173 });
1174 getParamsList({
1175 dictType: "数据匿名化处理类型",
1176 }).then((res: any) => {
1177 if (res?.code == proxy.$passCode) {
1178 handleTypeList.value = res.data || [];
1179 let item = dataSelectInfoItems.value.find(item => item.field == 'handleType');
1180 item && (item.options = handleTypeList.value);
1181 } else {
1182 proxy.$ElMessage.error(res.msg);
1183 }
1184 });
1185 getParamsList({
1186 dictType: "字段类型",
1187 }).then((res: any) => {
1188 if (res?.code == proxy.$passCode) {
1189 fieldTypeList.value = res.data || [];
1190 } else {
1191 proxy.$ElMessage.error(res.msg);
1192 }
1193 });
1194 getParentAreaPromise.value = getAreaData({ parentId: null }).then((res: any) => {
1195 if (res?.code == proxy.$passCode) {
1196 parentAreaData.value = res.data ?? [];
1197 return parentAreaData.value;
1198 }
1199 })
1200 })
1201
1202 onMounted(() => {
1203 nextTick(() => {
1204 containerWidth.value = containerRef.value?.offsetWidth || 0;
1205 })
1206 window.onresize = () => {
1207 containerWidth.value = containerRef.value?.offsetWidth || 0;
1208 }
1209 })
1210
1211 const cancelTask = () => {
1212 proxy.$openMessageBox("当前页面尚未保存,确定放弃修改吗?", () => {
1213 userStore.setTabbar(userStore.tabbar.filter((tab: any) => tab.fullPath !== fullPath));
1214 router.push({
1215 name: 'resultProcess'
1216 });
1217 }, () => {
1218 proxy.$ElMessage.info("已取消");
1219 });
1220 }
1221
1222 /** 完成任务关闭 */
1223 const closeTask = () => {
1224 userStore.setTabbar(userStore.tabbar.filter((tab: any) => tab.fullPath !== fullPath));
1225 router.push({
1226 name: 'resultProcess'
1227 });
1228 }
1229
1230 const refreshTimer = ref()
1231
1232 /** 执行结果信息 */
1233 const analysisResultInfo: any = ref({});
1234
1235 const getResultPromise: any = ref(null);
1236
1237 /** 第三步处理,定时刷新查看结果 */
1238 const processStepThreeResultView = (isRefresh = false) => {
1239 let process = (isRefresh) => {
1240 getResultPromise.value = getAnonAnalyzeResult(taskExecGuid.value).then((res: any) => {
1241 getResultPromise.value = null;
1242 if (res?.code == proxy.$passCode) {
1243 analysisResultInfo.value = res.data || {};
1244 if (analysisResultInfo.value.status == 'R') { //正在运行中
1245 if (isRefresh) {
1246 proxy.$ElMessage.success('刷新成功,正在执行中...');
1247 }
1248 //添加定时器。
1249 if (refreshTimer.value) {
1250 return;
1251 }
1252 refreshTimer.value = setInterval(async () => {
1253 process(false);
1254 }, 20000);
1255 } else if (analysisResultInfo.value.status == 'Y') {
1256 //去获取结果。
1257 isExecEnd.value = true;
1258 refreshTimer.value && clearInterval(refreshTimer.value);
1259 refreshTimer.value = null;
1260 analysisResultTableFields.value = res.data?.column || [];
1261 pageInfo.value.curr = 1;
1262 getAnalysisResultPageData();
1263 } else if (analysisResultInfo.value.status == 'E') {
1264 isExecEnd.value = true
1265 refreshTimer.value && clearInterval(refreshTimer.value);
1266 refreshTimer.value = null;
1267 }
1268 } else {
1269 proxy.$ElMessage.error(res.msg);
1270 }
1271 });
1272 }
1273 process(isRefresh);
1274 }
1275
1276 /** 随时点击刷新查看结果。 */
1277 const refreshQueryData = () => {
1278 if (getResultPromise.value) {
1279 return;
1280 }
1281 if (refreshTimer.value) {
1282 clearInterval(refreshTimer.value);
1283 refreshTimer.value = null;
1284 }
1285 processStepThreeResultView(true);
1286 }
1287
1288 /** ------------------------- 匿名化分析结果页面数据展示 ---------------- */
1289 const pageInfo: any = ref({
1290 ...commonPageConfig,
1291 })
1292
1293 const pageChange = (info) => {
1294 pageInfo.value.curr = Number(info.curr);
1295 pageInfo.value.limit = Number(info.limit);
1296 getAnalysisResultPageData();
1297 }
1298
1299 /** 每列字段对应的列宽计算结果。 */
1300 const originResultTableFieldColumn = ref({});
1301
1302 /** 结果分析中的字段表格数据 */
1303 const resultData: any = ref([]);
1304
1305 /** 结果分析中的字段信息 */
1306 const analysisResultTableFields: any = ref([]);
1307
1308 const analysisResultLoading = ref(false);
1309
1310 watch(
1311 resultData,
1312 (val: any[], oldVal) => {
1313 if (!analysisResultTableFields.value?.length) {
1314 originResultTableFieldColumn.value = {};
1315 return;
1316 }
1317 originResultTableFieldColumn.value = {};
1318 analysisResultTableFields.value.forEach((field, index) => {
1319 originResultTableFieldColumn.value[field.enName] = calcTableColumnWidth(
1320 val?.slice(0, 20) || [],
1321 field.enName,
1322 field.chName,
1323 24
1324 );
1325 });
1326 },
1327 {
1328 deep: true,
1329 }
1330 );
1331
1332 const getAnalysisResultPageData = () => {
1333 analysisResultLoading.value = true;
1334 getAnonAnalyzePageData({
1335 pageIndex: pageInfo.value.curr,
1336 pageSize: pageInfo.value.limit,
1337 taskExecGuid: taskExecGuid.value,
1338 }).then((res: any) => {
1339 analysisResultLoading.value = false;
1340 if (res?.code == proxy.$passCode) {
1341 pageInfo.value.rows =
1342 resultData.value = [];
1343 res.data?.records?.forEach(d => {
1344 let obj = {};
1345 analysisResultTableFields.value.forEach(t => {
1346 obj[t.enName] = d.fieldValue?.[t.enName];
1347 });
1348 obj['equivalenceClassNum'] = changeNum(d.equivalenceClassNum || 0, 0);
1349 obj['reIdentifyRisk'] = changeNum(d.reIdentifyRisk || 0, 2);
1350 obj['isGtThreshold'] = d.isGtThreshold;
1351 resultData.value.push(obj);
1352 });
1353 pageInfo.value.rows = res.data?.totalRows ?? 0;
1354 } else {
1355 proxy.$ElMessage.error(res.msg);
1356 }
1357 })
1358 }
1359
1360 const downPromise: any = ref()
1361
1362 /** 下载评估报告 */
1363 const transfer = () => {
1364 if (downPromise.value) {
1365 return;
1366 }
1367 downPromise.value = exportAnonReport({
1368 taskGuid: route.query.guid,
1369 execGuid: taskExecGuid.value
1370 }).then((res: any) => {
1371 downPromise.value = null;
1372 if (res && !res.msg) {
1373 download(res, (route.query.taskName || oldAnonTaskValueInfo.value.taskName) + '_匿名化评估报告.docx', 'word')
1374 } else {
1375 res?.msg && ElMessage.error(res?.msg);
1376 }
1377 }).catch(() => {
1378 downPromise.value = null;
1379 })
1380 }
1381
1382 onUnmounted(() => {
1383 refreshTimer.value && clearInterval(refreshTimer.value);
1384 refreshTimer.value = null;
1385 })
1386
1387 </script>
1388
1389 <style lang="scss" scoped>
1390 .top_tool_wrap {
1391 width: 100%;
1392 height: 72px;
1393 margin: 8px 0 0px;
1394 display: flex;
1395 justify-content: center;
1396 align-items: center;
1397
1398 :deep(.el-steps) {
1399 width: 60%;
1400 }
1401 }
1402
1403 .bottom_tool_wrap {
1404 height: 40px;
1405 padding: 0 16px;
1406 border-top: 1px solid #d9d9d9;
1407 display: flex;
1408 justify-content: center;
1409 align-items: center;
1410 }
1411
1412 .content_main {
1413 height: calc(100% - 40px);
1414 padding: 0 16px;
1415 overflow: hidden auto;
1416 }
1417
1418 .operator_panel_wrap {
1419 padding-bottom: 12px;
1420 }
1421
1422 .wait-result-div {
1423 height: 250px;
1424 display: flex;
1425 flex-direction: column;
1426 justify-content: center;
1427 align-items: center;
1428
1429 .loading-img {
1430 width: 40px;
1431 height: 40px;
1432 margin-bottom: 18px;
1433 }
1434
1435 .desc {
1436 color: #999;
1437 margin-bottom: 18px;
1438 margin-left: 26px;
1439 }
1440
1441 :deep(.el-icon.failed) {
1442 color: #E63E33;
1443 width: 32px;
1444 height: 32px;
1445 margin-bottom: 8px;
1446
1447 svg {
1448 width: 32px;
1449 height: 32px;
1450 }
1451 }
1452
1453 .error-desc {
1454 color: #E63E33;
1455 font-size: 14px;
1456 line-height: 21px;
1457 margin-bottom: 8px;
1458 font-weight: 600;
1459 }
1460 }
1461
1462 .analysis-result-main {
1463 min-height: 250px;
1464
1465 .value-desc {
1466 font-size: 14px;
1467 color: #212121;
1468 line-height: 21px;
1469 }
1470
1471 .result-title {
1472 font-size: 16px;
1473 color: #212121;
1474 line-height: 24px;
1475 font-weight: 600;
1476 margin-bottom: 6px;
1477 }
1478
1479 .result-title-h1 {
1480 color: #212121;
1481 font-weight: 600;
1482 font-size: 24px;
1483 text-align: center;
1484 line-height: 36px;
1485 margin-top: 12px;
1486 }
1487
1488 .result-title-desc {
1489 color: #666;
1490 font-size: 14px;
1491 line-height: 21px;
1492 margin-top: 12px;
1493 }
1494
1495 .kpi-content {
1496 display: flex;
1497 flex-direction: row;
1498 column-gap: 12px;
1499 row-gap: 12px;
1500 flex-wrap: wrap;
1501 margin-bottom: 20px;
1502 }
1503
1504 .border-content {
1505 height: 76px;
1506 display: flex;
1507 flex-direction: column;
1508 align-items: left;
1509 padding-left: 16px;
1510 justify-content: center;
1511 border: 1px solid #d9d9d9;
1512 width: calc(20% - 8px);
1513 min-width: 228px;
1514 border-radius: 2px;
1515 padding-left: 16px;
1516
1517 .number {
1518 font-weight: 700;
1519 font-size: 20px;
1520 color: #212121;
1521 line-height: 30px;
1522 margin-top: 2px;
1523
1524 &.score-color {
1525 color: #FF5F1F;
1526 }
1527 }
1528
1529 .text {
1530 font-size: 14px;
1531 line-height: 21px;
1532 color: #666666;
1533 display: flex;
1534
1535 .el-icon {
1536 color: #b2b2b2;
1537 }
1538 }
1539 }
1540
1541 .result-table-desc {
1542 font-size: 14px;
1543 color: #999999;
1544 line-height: 21px;
1545 }
1546
1547 .row-two-main {
1548 margin-top: 18px;
1549 display: flex;
1550
1551 .table-one {
1552 width: 586px;
1553
1554 &.border {
1555 border: 1px solid #d9d9d9;
1556 padding: 14px 18px 18px;
1557 }
1558 }
1559
1560 .table-two {
1561 margin-left: 20px;
1562 width: calc(100% - 606px);
1563
1564 &.border {
1565 border: 1px solid #d9d9d9;
1566 padding: 14px 18px 18px;
1567 }
1568 }
1569 }
1570 }
1571
1572 .step-result {
1573 :deep(.v-content-wrap) {
1574 height: 100%;
1575
1576 .el-card__body {
1577 height: calc(100% - 50px) !important;
1578
1579 .card-body-content {
1580 height: 100%;
1581 }
1582 }
1583
1584 .table_tool_wrap {
1585 padding: 0px;
1586 }
1587 }
1588
1589 }
1590
1591 :deep(.custom-form) {
1592 align-items: flex-start;
1593
1594 .wid60.el-form-item {
1595 width: calc(66.66% - 12px);
1596 }
1597 }
1598
1599 :deep(.fixwidth-form) {
1600 width: 500px;
1601
1602 .autoWidth.el-form-item {
1603 width: 80px;
1604 }
1605 }
1606
1607 .table-v2-main {
1608 width: 100%;
1609 height: 240px;
1610 margin-top: 2px;
1611
1612 .main-placeholder {
1613 height: 100%;
1614 display: flex;
1615 justify-content: center;
1616 align-items: center;
1617 flex-direction: column;
1618
1619 .empty-text {
1620 font-size: 14px;
1621 color: #b2b2b2;
1622 }
1623 }
1624 }
1625
1626 :deep(.el-table-v2) {
1627
1628 .el-table-v2__main {
1629 border: 1px solid #d9d9d9;
1630 background: #fff;
1631 }
1632
1633 .el-table-v2__body tr.hover-row.el-table-v2__row--striped.current-row>td.el-table-v2__cell {
1634 background-color: var(--el-table-row-hover-bg-color);
1635 }
1636
1637 .el-table-v2__body tr.current-row>td.el-table-v2__cell {
1638 background-color: var(--el-table-current-row-bg-color);
1639 }
1640
1641 .el-table-v2__header {
1642 width: 100% !important;
1643 }
1644
1645 .el-table-v2__header-cell,
1646 .el-table-v2__row-cell {
1647 border-right: 1px #d9d9d9 solid;
1648 }
1649
1650 .el-table-v2__header-cell-text {
1651 color: #000;
1652 font-weight: normal;
1653 }
1654
1655 .el-table-v2__empty {
1656 display: none !important;
1657 }
1658
1659 .el-table-v2__header-row {
1660 border-bottom: 1px solid #d9d9d9;
1661 }
1662 }
1663
1664 .empty-content {
1665 display: flex;
1666 align-items: center;
1667 justify-content: center;
1668 height: 316px;
1669 width: 100%;
1670 flex-direction: column;
1671
1672 .empty-text {
1673 font-size: 14px;
1674 color: #b2b2b2;
1675 }
1676 }
1677
1678 :deep(.el-form) {
1679 .checkbox-cascader {
1680 display: flex;
1681
1682 .el-cascader {
1683 margin-left: 8px;
1684 }
1685 }
1686
1687 .checkbox-right {
1688 width: calc(100% - 50px);
1689
1690 &.el-form-item {
1691 margin-bottom: 0px;
1692 margin-right: 0px;
1693 width: 100%;
1694 }
1695 }
1696 }
1697
1698 :deep(.cell-tooltip-bg) {
1699 background-color: #fff1d4 !important;
1700 }
1701
1702 :deep(.anlysis-content-wrap) {
1703 .card-title {
1704 justify-content: space-between;
1705 }
1706 }
1707 </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 import { cloneDeep } from 'lodash-es';
16
17 const props = defineProps({
18 fieldTypeList: {
19 default: [],
20 type: Array<any>
21 },
22 isFile: {
23 default: false,
24 type: Boolean
25 },
26 fieldNameList: {
27 default: [],
28 type: Array<any>
29 },
30 anonTaskRules: {
31 default: [],
32 type: Array<any>
33 },
34 anonPrivacyMode: {
35 default: {},
36 type: Object
37 }
38 })
39
40 const { required } = useValidator();
41 const { proxy } = getCurrentInstance() as any;
42
43 const drawerRef = ref();
44
45 /** 泛化文件列表 */
46 const generalizeFileNameList: any = ref([]);
47
48 /** 脱敏规则字典列表 */
49 const desensitiveRuleTypeList: any = ref([]);
50
51 /** 标签类型字典列表 */
52 const labelTypeList: any = ref([]);
53
54 /** 加密算法字典列表 */
55 const hashMethodList = ref([]);
56
57 /** 当前正在编辑的表格索引 */
58 const currTableRowIndex: any = ref(null);
59
60 const ruleModelTableInfo = ref({
61 id: 'rule-model-table',
62 loading: false,
63 minHeight: '150px',
64 nodeKey: 'guid',
65 height: '200px',
66 fields: [
67 { label: "序号", type: "index", width: TableColumnWidth.INDEX, align: "center" },
68 { label: "字段中文名称", field: "fieldChName", width: 150 },
69 { label: "字段英文名称", field: "fieldName", width: 150 },
70 { label: "字段类型", field: "fieldTypeName", width: 120 },
71 { label: "数据类型", field: "dataTypeName", width: 120 },
72 {
73 label: "脱敏方式", field: "desensitiveRule", width: 120, getName: (scope) => {
74 let rule = scope.row.desensitiveRule;
75 return rule ? rule : (scope.row.generalizeFileGuid ? '泛化' : '--');
76 }
77 },
78 ],
79 data: <any>[],
80 showPage: false,
81 actionInfo: {
82 label: "操作",
83 type: "btn",
84 width: 100,
85 fixed: 'right',
86 btns: [
87 {
88 label: "编辑", value: "ruleEdit", click: (scope) => {
89 currTableRowIndex.value = scope.$index;
90 drawerInfo.value.visible = true;
91 drawerInfo.value.type = 'edit';
92 drawerInfo.value.header.title = '编辑字段脱敏规则';
93 let row = scope.row;
94 fieldRulesFormItems.value.forEach(item => {
95 item.default = row[item.field];
96 if (item.field == 'encryptionAlgorithmCode' || item.field == 'salted') {
97 item.visible = row.desensitiveRuleCode == 'HASH';
98 } else if (item.field == 'decimalPlaces') {
99 item.visible = row.desensitiveRuleCode == 'ROUNDING';
100 }
101 });
102 let fieldNameList = props.fieldNameList.map(f => {
103 if (f.enName != row.fieldName && ruleModelTableInfo.value.data.some(d => d.fieldName == f.enName)) {
104 f.disabled = true;
105 } else {
106 f.disabled = false;
107 }
108 return f;
109 });
110 fieldRulesFormItems.value[0].options = fieldNameList;
111 fieldRulesFormInfo.value.formInfo.items = fieldRulesFormItems.value;
112 drawerInfo.value.container.contents[0] = fieldRulesFormInfo.value;
113 desensitiveRuleDetail.value = {
114 dissembleType: 1, /** 1从左往右,2.从右往左 */
115 ruleDetails: <any>[{
116 digitType: 1,
117 }]
118 };
119 charReplaceRuleDetail.value = {
120 replaceType: 1, /** 1从左往右,2.从右往左 */
121 ruleDetails: <any>[{
122 digitType: 1,
123 ruleType: 1
124 }]
125 };
126 rangeReplaceRuleDetails.value = [{
127 lowOperator: '≤',
128 fieldChName: row.fieldChName,
129 upperOperator: '≤'
130 }];
131
132 if (row.desensitiveRuleCode == 'DISSEMBLE') {
133 desensitiveRuleDetail.value = row.desensitiveRuleDetail || {};
134 } else if (row.desensitiveRuleCode == 'CHARREPLACE') {
135 charReplaceRuleDetail.value = row.desensitiveRuleDetail || {};
136 } else if (row.desensitiveRuleCode == 'RANGEREPLACE') {
137 rangeReplaceRuleDetails.value = row.desensitiveRuleDetail?.ruleDetails?.map(rd => {
138 rd.fieldChName = row.fieldChName;
139 return rd;
140 }) || [];
141 }
142 fieldRulesEndFormInfo.value.formInfo.items[0].default = '';
143 fieldRulesEndFormInfo.value.formInfo.items[1].default = '';
144 if (!row.desensitiveRuleCode) {
145 drawerInfo.value.container.contents = [fieldRulesFormInfo.value];
146 } else {
147 drawerInfo.value.container.contents = [fieldRulesFormInfo.value, fieldRulesEndFormInfo.value];
148 }
149 }
150 },
151 {
152 label: "删除", value: "delete", click: (scope) => {
153 proxy.$openMessageBox("此操作将永久删除, 是否继续?", () => {
154 let fieldName = scope.row.fieldName;
155 ruleModelTableInfo.value.data.splice(scope.$index, 1);
156 // updatePrivacyFormFieldsOptions(fieldName);
157 // 同步去掉隐私模型设置中的字段。
158 proxy.$ElMessage.success('删除成功');
159 }, () => {
160 proxy.$ElMessage.info("已取消");
161 })
162 }
163 },
164 ],
165 }
166 })
167
168 /** 字段脱敏规则表单配置 */
169 const fieldRulesFormItems = ref([{
170 label: '选择字段',
171 type: 'select',
172 placeholder: '请选择',
173 field: 'fieldName',
174 default: '',
175 options: props.fieldNameList,
176 props: {
177 label: 'chName',
178 value: 'enName',
179 disabled: 'disabled'
180 },
181 filterable: true,
182 clearable: false,
183 required: true
184 }, {
185 label: '字段类型',
186 type: 'select',
187 placeholder: '请选择',
188 field: 'fieldTypeCode',
189 default: 'varchar',
190 options: props.fieldTypeList,
191 props: {
192 label: 'label',
193 value: 'value'
194 },
195 disabled: true,
196 clearable: true,
197 required: true
198 }, {
199 label: '数据类型',
200 type: 'select',
201 placeholder: '请选择',
202 field: 'dataTypeCode',
203 default: '',
204 options: labelTypeList.value,
205 props: {
206 label: 'label',
207 value: 'value'
208 },
209 filterable: true,
210 clearable: true,
211 required: false
212 }, {
213 label: 'K匿名泛化',
214 type: 'select',
215 placeholder: '请选择',
216 field: 'generalizeFileGuid',
217 default: '',
218 options: generalizeFileNameList.value,
219 props: {
220 label: 'generalizeFileName',
221 value: 'guid'
222 },
223 filterable: true,
224 clearable: true,
225 required: false
226 }, {
227 label: '脱敏规则',
228 type: 'select',
229 placeholder: '请选择',
230 field: 'desensitiveRuleCode',
231 default: 'DISSEMBLE',
232 options: desensitiveRuleTypeList.value,
233 props: {
234 label: 'label',
235 value: 'value'
236 },
237 filterable: true,
238 clearable: true,
239 required: false
240 }, {
241 label: '加密算法',
242 type: 'select',
243 placeholder: '请选择',
244 field: 'encryptionAlgorithmCode',
245 default: '',
246 options: hashMethodList.value,
247 props: {
248 label: 'label',
249 value: 'value'
250 },
251 filterable: true,
252 clearable: true,
253 visible: false,
254 required: true
255 }, {
256 label: '加盐值',
257 type: 'input',
258 placeholder: '请输入0~9',
259 field: 'salted',
260 maxlength: 1,
261 min: 0,
262 max: 9,
263 inputType: 'integerNumber',
264 default: 5,
265 required: true,
266 filterable: true,
267 clearable: true,
268 visible: false,
269 }, {
270 label: '保留小数',
271 type: 'input',
272 placeholder: '请输入0~5',
273 field: 'decimalPlaces',
274 maxlength: 1,
275 min: 0,
276 max: 5,
277 inputType: 'integerNumber',
278 default: 2,
279 required: true,
280 filterable: true,
281 clearable: true,
282 visible: false,
283 }]);
284
285 const fieldRulesFormRules = ref({
286 fieldName: [required('请选择字段')],
287 dataTypeCode: [required('请选择数据类型')], //不填标识非敏感标识。
288 // desensitiveRuleCode: [required('请选择脱敏规则')], 脱敏规则和泛化文件选择一个即可。二者必选其一,可以两者共存。
289 encryptionAlgorithmCode: [required('请选择加密算法')],
290 salted: [required('请输入加盐值')],
291 decimalPlaces: [required('请输入保留小数')]
292 });
293
294 const fieldRulesFormInfo = ref({
295 type: "form",
296 title: "",
297 col: "span",
298 formInfo: {
299 id: "add-class-form",
300 readonly: false,
301 items: fieldRulesFormItems.value,
302 rules: fieldRulesFormRules.value,
303 },
304 });
305
306 const fieldRulesEndFormInfo = ref({
307 type: "form",
308 title: "",
309 col: "mt8",
310 showSlot: false,
311 formInfo: {
312 id: "add-rules-end-form",
313 readonly: false,
314 items: [{
315 label: '样本数据',
316 type: 'input',
317 placeholder: '请输入样本',
318 field: 'testData',
319 default: '',
320 required: false,
321 clearable: true,
322 block: true,
323 col: 'mb8',
324 validateBtn: {
325 value: 'validate',
326 label: '验证',
327 click: () => {
328 let formInline = drawerRef.value?.getDrawerConRef('drawerFormRef')?.formInline;
329 let formInline1 = drawerRef.value?.getDrawerConRef('drawerFormRef', 1)?.formInline;
330 if (!formInline1.testData) {
331 proxy.$ElMessage.error('样本数据不能为空');
332 return;
333 }
334 validateAnonRule({
335 desensitiveRuleCode: formInline.desensitiveRuleCode,
336 value: formInline1.testData,
337 desensitiveRuleDetail: getDesensitiveRuleDetailInfo(formInline)
338 }).then((res: any) => {
339 if (res?.code == proxy.$passCode) {
340 let result = res.data;
341 fieldRulesEndFormInfo.value.formInfo.items[0].default = formInline1.testData;
342 fieldRulesEndFormInfo.value.formInfo.items[1].default = formInline.desensitiveRuleCode == 'BLANK' ? ' ' : result;
343 } else {
344 proxy.$ElMessage.error(res.msg);
345 }
346 })
347 }
348 }
349 }, {
350 label: '脱敏效果',
351 type: 'input',
352 placeholder: '点击验证后展示脱敏效果',
353 field: 'validateResult',
354 default: '',
355 block: true,
356 required: false,
357 disabled: true
358 }],
359 rules: {},
360 },
361 });
362
363 /** 获取规则配置详情。 */
364 const getDesensitiveRuleDetailInfo = (formInline) => {
365 let desensitiveRuleDetailInfo: any = {};
366 if (formInline.desensitiveRuleCode == 'DISSEMBLE') {
367 desensitiveRuleDetailInfo = desensitiveRuleDetail.value;
368 } else if (formInline.desensitiveRuleCode == 'CHARREPLACE') {
369 desensitiveRuleDetailInfo = charReplaceRuleDetail.value;
370 } else if (formInline.desensitiveRuleCode == 'RANGEREPLACE') {
371 desensitiveRuleDetailInfo = {
372 ruleDetails: rangeReplaceRuleDetails.value
373 };
374 } else if (formInline.desensitiveRuleCode == 'ROUNDING') {
375 desensitiveRuleDetailInfo = {
376 decimalPlaces: formInline.decimalPlaces
377 }
378 } else if (formInline.desensitiveRuleCode == 'HASH') {
379 desensitiveRuleDetailInfo = {
380 encryptionAlgorithmCode: formInline.encryptionAlgorithmCode,
381 salted: formInline.salted
382 }
383 }
384 return desensitiveRuleDetailInfo;
385 }
386
387 const drawerInfo = ref({
388 visible: false,
389 direction: 'rtl',
390 size: 540,
391 header: {
392 title: '添加字段脱敏规则',
393 },
394 type: '',
395 container: {
396 contents: [fieldRulesFormInfo.value, fieldRulesEndFormInfo.value],
397 },
398 footer: {
399 visible: true,
400 btns: [
401 { type: 'default', label: '取消', value: 'cancel' },
402 { type: 'primary', label: '确定', value: 'save', loading: false },
403 ]
404 }
405 });
406
407 const addRowRules = () => {
408 drawerInfo.value.visible = true;
409 drawerInfo.value.type = 'add';
410 drawerInfo.value.header.title = '添加字段脱敏规则';
411 fieldRulesFormItems.value.forEach(item => {
412 if (item.field == 'fieldTypeCode') {
413 item.default = 'varchar';
414 } else if (item.field == 'desensitiveRuleCode') {
415 item.default = 'DISSEMBLE';
416 } else if (item.field == 'encryptionAlgorithmCode' || item.field == 'salted') {
417 item.visible = false;
418 item.default = '';
419 item.field == 'salted' && (item.default = 5);
420 } else if (item.field == 'decimalPlaces') {
421 item.visible = false;
422 item.default = 2;
423 } else {
424 item.default = '';
425 }
426 });
427 let fieldNameList = props.fieldNameList.map(f => {
428 if (ruleModelTableInfo.value.data.some(d => d.fieldName == f.enName)) {
429 f.disabled = true;
430 } else {
431 f.disabled = false;
432 }
433 return f
434 });
435 fieldRulesFormItems.value[0].options = fieldNameList;
436 fieldRulesFormInfo.value.formInfo.items = fieldRulesFormItems.value;
437 desensitiveRuleDetail.value = {
438 dissembleType: 1, /** 1从左往右,2.从右往左 */
439 ruleDetails: <any>[{
440 digitType: 1,
441 }]
442 };
443 charReplaceRuleDetail.value = {
444 replaceType: 1, /** 1从左往右,2.从右往左 */
445 ruleDetails: <any>[{
446 digitType: 1,
447 ruleType: 1
448 }]
449 };
450 rangeReplaceRuleDetails.value = [{
451 lowOperator: '≤',
452 fieldChName: '',
453 upperOperator: '≤'
454 }];
455 fieldRulesEndFormInfo.value.formInfo.items[0].default = '';
456 fieldRulesEndFormInfo.value.formInfo.items[1].default = '';
457 drawerInfo.value.container.contents = [fieldRulesFormInfo.value, fieldRulesEndFormInfo.value];
458 }
459
460 const drawerBtnClick = async (btn, info) => {
461 if (btn.value == 'cancel') {
462 drawerInfo.value.visible = false
463 } else {
464 if (!info.generalizeFileGuid && !info.desensitiveRuleCode) {
465 proxy.$ElMessage.error('K匿名泛化与脱敏规则不能同时为空');
466 return;
467 }
468 if (!(info.fieldTypeCode == 'int' || info.fieldTypeCode == 'decimal' || info.fieldTypeCode == 'tinyint')) {
469 if (info.desensitiveRuleCode == 'ROUNDING') {
470 proxy.$ElMessage.error('非数值类型字段的脱敏规则不能设置取整');
471 return;
472 }
473 if (info.desensitiveRuleCode == 'RANGEREPLACE') {
474 proxy.$ElMessage.error('非数值类型字段的脱敏规则不能设置区间替换');
475 return;
476 }
477 }
478 drawerInfo.value.footer.btns[1].loading = true;
479 let desensitiveRuleDetailInfo = getDesensitiveRuleDetailInfo(info);
480 // 脱敏规则为掩盖,字符,区间替换存在时需要调用接口检验
481 if (info.desensitiveRuleCode == 'DISSEMBLE' || info.desensitiveRuleCode == 'CHARREPLACE' || info.desensitiveRuleCode == 'RANGEREPLACE') {
482 try {
483 let res: any = await validateAnonRule({
484 desensitiveRuleCode: info.desensitiveRuleCode,
485 value: '',
486 desensitiveRuleDetail: desensitiveRuleDetailInfo
487 })
488 if (res?.code != proxy.$passCode) {
489 proxy.$ElMessage.error(res.msg);
490 drawerInfo.value.footer.btns[1].loading = false;
491 return;
492 }
493 } catch (error) {
494 drawerInfo.value.footer.btns[1].loading = false;
495 }
496 }
497 drawerInfo.value.footer.btns[1].loading = false;
498 let saveData: any = { ...info };
499 saveData.desensitiveRuleDetail = desensitiveRuleDetailInfo;
500 saveData.fieldChName = props.fieldNameList?.find(f => f.enName == saveData.fieldName)?.chName;
501 saveData.dataTypeName = saveData.dataTypeCode && labelTypeList.value.find(l => l.value == saveData.dataTypeCode)?.label;
502 saveData.fieldTypeName = saveData.fieldTypeCode && props.fieldTypeList?.find(f => f.value == saveData.fieldTypeCode)?.label;
503 saveData.desensitiveRule = info.desensitiveRuleCode && desensitiveRuleTypeList.value.find(d => d.value == info.desensitiveRuleCode)?.label;
504 let changeFields = '';
505 if (drawerInfo.value.type == 'add') {
506 ruleModelTableInfo.value.data.push(saveData);
507 } else if (drawerInfo.value.type == 'edit') {
508 if (!currTableRowIndex.value != null) {
509 let originFieldName = ruleModelTableInfo.value.data[currTableRowIndex.value]?.fieldName;
510 changeFields = originFieldName == saveData.fieldName ? '' : originFieldName;
511 ruleModelTableInfo.value.data[currTableRowIndex.value] = saveData;
512 }
513 }
514 drawerInfo.value.visible = false;
515 // updatePrivacyFormFieldsOptions(changeFields);
516 }
517 }
518
519 /** 更新下拉选择字段的下拉列表,若是字段被删除,需要同步去掉下拉选择值。 */
520 const updatePrivacyFormFieldsOptions = (changeFields) => {
521 privacyFormItems.value[2].children[0].options = props.fieldNameList;
522 privacyFormItems.value[3].children[0].options = props.fieldNameList;
523 let formInline = privacyFormRef.value?.formInline;
524 privacyFormItems.value.forEach((item, index) => {
525 item.default = formInline[item.field];
526 if (item.default == 'Y') {
527 item.children?.forEach(child => {
528 child.default = formInline[child.field];
529 if (changeFields && (child.field == 'tcFieldName' || child.field == 'ldFieldName') && child.default == changeFields) {
530 child.default = ''
531 }
532 });
533 }
534 })
535 }
536
537 /** 当前选择的字段对应的标签信息。 */
538 const currLabelInfo: any = ref({});
539
540 const drawerSelectChange = (val, row, info) => {
541 if (row.field === 'desensitiveRuleCode') {
542 let methodItem = fieldRulesFormItems.value.find(item => item.field == 'encryptionAlgorithmCode');
543 let saltedItem = fieldRulesFormItems.value.find(item => item.field == 'salted');
544 let decimalPlaceItem = fieldRulesFormItems.value.find(item => item.field == 'decimalPlaces');
545 methodItem && (methodItem.visible = val == 'HASH');
546 saltedItem && (saltedItem.visible = val == 'HASH');
547 decimalPlaceItem && (decimalPlaceItem.visible = val == 'ROUNDING');
548 fieldRulesFormItems.value.forEach(item => {
549 item.default = info[item.field];
550 if (item.field == 'decimalPlaces') {
551 if (item.default == null) {
552 item.default = 2;
553 }
554 } else if (item.field == 'salted') {
555 if (item.default == null) {
556 item.default = 5;
557 }
558 }
559 });
560 if (val == 'RANGEREPLACE') {
561 let fieldName = info.fieldName;
562 let fieldChName = fieldName && props.fieldNameList.find(f => f.enName == fieldName)?.chName;
563 rangeReplaceRuleDetails.value.forEach(r => {
564 if (fieldChName && r.fieldChName != fieldChName) {
565 r.fieldChName = fieldChName;
566 }
567 });
568 }
569 if (!val) {
570 drawerInfo.value.container.contents = [fieldRulesFormInfo.value];
571 } else {
572 drawerInfo.value.container.contents = [fieldRulesFormInfo.value, fieldRulesEndFormInfo.value];
573 }
574 }
575 // else if (row.field == 'fieldTypeCode') {
576 // fieldRulesFormItems.value.forEach(item => {
577 // item.default = info[item.field];
578 // if (item.field == 'desensitiveRuleCode') {
579 // if (!(val == 'int' || val == 'decimal' || val == 'tinyint')) {
580 // item.options = desensitiveRuleTypeList.value.filter(d => d.value != 'ROUNDING' && d.value != 'RANGEREPLACE');
581 // if (item.default == 'ROUNDING' || item.default == 'RANGEREPLACE') {
582 // item.default = 'DISSEMBLE';
583 // }
584 // } else {
585 // item.options = desensitiveRuleTypeList.value;
586 // }
587 // }
588 // });
589 // }
590 else if (row.field == 'fieldName') { //选择字段改变之后,调用接口。
591 let tableField = props.fieldNameList.find(f => f.enName == val);
592 let dataType = tableField?.dataType || 'varchar';
593 fieldRulesFormItems.value.forEach(item => {
594 item.default = info[item.field];
595 if (item.field == 'fieldTypeCode') {
596 item.default = dataType;
597 }
598 });
599 getLableByFieldName(val).then((res: any) => {
600 if (res?.code == proxy.$passCode) {
601 let labelInfo = currLabelInfo.value = res.data || {};
602 fieldRulesFormItems.value.forEach(item => {
603 item.default = info[item.field];
604 if (item.field == 'dataTypeCode') {
605 item.default = labelInfo.labelTypeCode;
606 } else if (item.field == 'fieldTypeCode') {
607 item.default = dataType;
608 }
609 });
610 } else {
611 proxy.$ElMessage.error(res.msg);
612 }
613 })
614 if (info.desensitiveRuleCode == 'RANGEREPLACE') {
615 rangeReplaceRuleDetails.value.forEach(r => {
616 r.fieldChName = tableField?.chName || val;
617 });
618 }
619 }
620 }
621
622 const drawerInputChange = (val, row, info) => {
623 if (row.field == 'testData') { //样本数据修改后,清空脱敏结果
624 fieldRulesEndFormInfo.value.formInfo.items[0].default = val;
625 fieldRulesEndFormInfo.value.formInfo.items[1].default = '';
626 }
627 }
628
629 /** 掩盖规则中的位数和剩余位数 */
630 const digitTypeList = ref([{
631 value: 1,
632 label: '位数'
633 }, {
634 value: 2,
635 label: '剩余位数'
636 }]);
637
638 const dissembleRuleTypeList = ref([{
639 value: 1,
640 label: '脱敏'
641 }, {
642 value: 2,
643 label: '不脱敏'
644 }]);
645
646 /** 掩盖类型的脱敏规则 分段规则配置 */
647 const desensitiveRuleDetail = ref({
648 dissembleType: 1, /** 1从左往右,2.从右往左 */
649 ruleDetails: <any>[{
650 digitType: 1,
651 }]
652 });
653
654 const addSegmentRule = () => {
655 desensitiveRuleDetail.value.ruleDetails.push({
656 digitType: 1,
657 });
658 };
659
660 const deleteSegmentRule = (item, index) => {
661 desensitiveRuleDetail.value.ruleDetails.splice(index, 1);
662 }
663
664 const inputEventDigitChange = (val, item, prop = 'digit') => {
665 item[prop] = item[prop].toString().replace(/\./g, "")
666 item[prop] = item[prop].toString().replace(/^\D*(\d{0,7}(?:\.\d{0})?).*$/g, "$1")
667 if (prop == 'digit' && val != "" && item.digit < 1) {
668 item.digit = 1;
669 }
670 }
671
672 /** 字符规则中的替换方式 */
673 const charReplaceRuleTypeList = ref([{
674 value: 1,
675 label: '随机替换'
676 }, {
677 value: 2,
678 label: '固定值替换'
679 }]);
680
681 /** 字符替换的脱敏规则 分段规则配置 */
682 const charReplaceRuleDetail = ref({
683 replaceType: 1, /** 1从左往右,2.从右往左 */
684 ruleDetails: <any>[{
685 digitType: 1,
686 ruleType: 1
687 }]
688 });
689
690 const addCharReplaceSegmentRule = () => {
691 charReplaceRuleDetail.value.ruleDetails.push({
692 digitType: 1,
693 ruleType: 1
694 });
695 };
696
697 const deleteCharReplaceSegmentRule = (item, index) => {
698 charReplaceRuleDetail.value.ruleDetails.splice(index, 1);
699 }
700
701 /** ----- 区间替换规则 --- */
702 /** 区间替换的小于或小于等于字符列表 */
703 const lowerOperatorList: any = ref([{
704 value: '≤',
705 label: '≤'
706 }, {
707 value: '<',
708 label: '<'
709 }]);
710
711 /**
712 * 字符替换的脱敏规则 分段规则配置。
713 * { lowValue: , lowOperator: , upperValue: , upperOperator: , replaceValue: '' }
714 */
715 const rangeReplaceRuleDetails: any = ref([{
716 lowOperator: '≤',
717 fieldChName: '',
718 upperOperator: '≤'
719 }]);
720
721 const addRangeReplaceSegmentRule = () => {
722 let fieldChName = '';
723 if (rangeReplaceRuleDetails.value?.length) {
724 fieldChName = rangeReplaceRuleDetails.value[0].fieldChName;
725 } else {
726 let fieldName = drawerRef.value?.getDrawerConRef('drawerFormRef')?.formInline?.fieldName;
727 let tableField = fieldName && props.fieldNameList?.find(f => f.enName == fieldName);
728 fieldChName = tableField?.chName;
729 }
730 rangeReplaceRuleDetails.value.push({
731 lowOperator: '≤',
732 fieldChName: fieldChName,
733 upperOperator: '≤'
734 });
735 };
736
737 const deleteRangeReplaceSegmentRule = (item, index) => {
738 rangeReplaceRuleDetails.value.splice(index, 1);
739 }
740
741 /** 隐私模型设置 */
742 const privacyFormItems: any = ref([{
743 label: '选择准标识符等价类隐私模型',
744 type: 'checkbox-input-item',
745 field: 'isKaNumber',
746 default: 'N',
747 placeholder: 'K匿名',
748 trueValue: 'Y',
749 falseValue: 'N',
750 children: [
751 {
752 label: '',
753 type: 'input',
754 placeholder: '请输入',
755 field: 'kaNumber',
756 inputType: 'integerNumber',
757 default: '',
758 min: 1,
759 maxlength: 6,
760 disabled: false,
761 clearable: true,
762 visible: false,
763 col: 'ka-checkbox-input',
764 style: { width: '100px', margin: 0 }
765 }
766 ],
767 required: true,
768 col: 'checkbox-input'
769 }, {
770 label: '设置重标识可接受风险阈值',
771 type: 'checkbox-input-item',
772 field: 'isRiskThreshold',
773 default: 'N',
774 placeholder: '阈值',
775 trueValue: 'Y',
776 falseValue: 'N',
777 children: [
778 {
779 label: '',
780 type: 'input',
781 placeholder: '请输入',
782 field: 'riskThreshold',
783 default: '',
784 maxlength: 6,
785 min: 0,
786 max: 1,
787 inputType: 'scoreNumber',
788 disabled: false,
789 clearable: true,
790 visible: false,
791 col: 'ka-checkbox-input',
792 style: { width: '100px', margin: 0 }
793 }
794 ],
795 required: true,
796 col: 'checkbox-input'
797 }, {
798 label: '设置L多样性及T接近',
799 type: 'checkbox-input-item',
800 field: 'isLdField',
801 default: 'N',
802 placeholder: 'L多样性',
803 trueValue: 'Y',
804 falseValue: 'N',
805 children: [
806 {
807 label: '',
808 type: 'select',
809 placeholder: '请选择字段',
810 field: 'ldFieldName',
811 options: props.fieldNameList || [],
812 props: {
813 label: 'chName',
814 value: 'enName',
815 disabled: false
816 },
817 default: '',
818 filterable: true,
819 clearable: true,
820 visible: false,
821 col: 'ka-checkbox-input',
822 style: { width: '120px', margin: 0 }
823 },
824 {
825 label: '',
826 type: 'input',
827 placeholder: '请输入',
828 field: 'ldNumber',
829 inputType: 'integerNumber',
830 default: '',
831 maxlength: 6,
832 min: 1,
833 disabled: false,
834 clearable: true,
835 visible: false,
836 col: 'ka-checkbox-input',
837 style: { width: '100px', margin: 0 }
838 }
839 ],
840 block: false,
841 required: true,
842 col: 'checkbox-input'
843 }, {
844 label: '',
845 type: 'checkbox-input-item',
846 field: 'isTcField',
847 default: 'N',
848 placeholder: 'T接近',
849 trueValue: 'Y',
850 falseValue: 'N',
851 style: { 'margin-top': '2px' },
852 children: [
853 {
854 label: '',
855 type: 'select',
856 placeholder: '请选择字段',
857 field: 'tcFieldName',
858 default: '',
859 options: props.fieldNameList || [],
860 props: {
861 label: 'chName',
862 value: 'enName',
863 disabled: false
864 },
865 filterable: true,
866 clearable: true,
867 visible: false,
868 col: 'ka-checkbox-input',
869 style: { width: '120px', margin: 0 }
870 },
871 {
872 label: '',
873 type: 'input',
874 placeholder: '请输入',
875 field: 'tcThreshold',
876 default: '',
877 maxlength: 6,
878 min: 0,
879 max: 1,
880 inputType: 'scoreNumber',
881 disabled: false,
882 clearable: true,
883 visible: false,
884 col: 'ka-checkbox-input',
885 style: { width: '100px', margin: 0 }
886 }
887 ],
888 required: false,
889 block: true,
890 col: 'checkbox-input lmt12'
891 }]);
892
893 const privacyFormRules = ref({
894 kaNumber: [required('请输入K匿名值')],
895 riskThreshold: [required('请输入阈值')],
896 ldFieldName: [required('请选择L多样性字段')],
897 ldNumber: [required('请输入L多样性值')],
898 tcFieldName: [required('请选择T接近字段')],
899 tcThreshold: [required('请输入T接近阈值')]
900 });
901
902 /** 记录下旧的隐私模型设置 */
903 const oldPrivacyModelValue: any = ref({
904 isKaNumber: 'N',
905 isRiskThreshold: 'N',
906 isLdField: 'N',
907 isTcField: 'N'
908 });
909
910 const handleCheckboxChange = (val, value, row) => {
911 oldPrivacyModelValue.value = Object.assign({}, oldPrivacyModelValue.value, value);
912 privacyFormItems.value.forEach(item => {
913 item.default = oldPrivacyModelValue.value[item.field];
914 item.children?.forEach(child => {
915 child.default = oldPrivacyModelValue.value[child.field];
916 });
917 })
918 if (row.field == 'isKaNumber') {
919 let kaItem = privacyFormItems.value[0]?.children?.[0];
920 kaItem && (kaItem.visible = val == 'Y');
921 } else if (row.field == 'isRiskThreshold') {
922 let riskItem = privacyFormItems.value[1]?.children?.[0];
923 riskItem && (riskItem.visible = val == 'Y');
924 } else if (row.field == 'isLdField') {
925 let childrenItem = privacyFormItems.value[2]?.children;
926 childrenItem?.[0] && (childrenItem[0].visible = val == "Y");
927 childrenItem?.[1] && (childrenItem[1].visible = val == "Y");
928 } else if (row.field == 'isTcField') {
929 let childrenItem = privacyFormItems.value[3]?.children;
930 childrenItem?.[0] && (childrenItem[0].visible = val == "Y");
931 childrenItem?.[1] && (childrenItem[1].visible = val == "Y");
932 }
933 }
934
935 watch(() => props.fieldNameList, (val) => {
936 fieldRulesFormItems.value[0].options = val || [];
937 if (props.isFile) {
938 fieldRulesFormItems.value[1].disabled = false
939 } else {
940 fieldRulesFormItems.value[1].disabled = true;
941 }
942 }, {
943 immediate: true
944 })
945
946 watch(() => props.fieldTypeList, (val) => {
947 fieldRulesFormItems.value[1].options = val || [];
948 }, {
949 immediate: true
950 })
951
952 watch(() => props.anonPrivacyMode, (val) => {
953 if (!val) {
954 return;
955 }
956 let hasKaNumber = val.kaNumber != null;
957 privacyFormItems.value[0].default = hasKaNumber ? 'Y' : 'N';
958 privacyFormItems.value[0].children[0].visible = hasKaNumber;
959 privacyFormItems.value[0].children[0].default = val.kaNumber;
960 oldPrivacyModelValue.value.isKaNumber = hasKaNumber ? 'Y' : 'N';
961 oldPrivacyModelValue.value.kaNumber = val.kaNumber;
962 let hasRiskThreshold = val.riskThreshold != null;
963 privacyFormItems.value[1].default = hasRiskThreshold ? 'Y' : 'N';
964 privacyFormItems.value[1].children[0].visible = hasRiskThreshold;
965 privacyFormItems.value[1].children[0].default = val.riskThreshold;
966 oldPrivacyModelValue.value.isRiskThreshold = hasRiskThreshold ? 'Y' : 'N';
967 oldPrivacyModelValue.value.riskThreshold = val.riskThreshold;
968 let hasldFieldName = !!val.ldFieldName;
969 privacyFormItems.value[2].default = hasldFieldName ? 'Y' : 'N';
970 privacyFormItems.value[2].children[0].visible = hasldFieldName;
971 privacyFormItems.value[2].children[1].visible = hasldFieldName;
972 privacyFormItems.value[2].children[0].default = val.ldFieldName;
973 privacyFormItems.value[2].children[1].default = val.ldNumber;
974 oldPrivacyModelValue.value.isLdField = hasldFieldName ? 'Y' : 'N';
975 oldPrivacyModelValue.value.ldFieldName = val.ldFieldName;
976 oldPrivacyModelValue.value.ldNumber = val.ldNumber;
977 let hasTcField = !!val.tcFieldName;
978 privacyFormItems.value[3].default = hasTcField ? 'Y' : 'N';
979 privacyFormItems.value[3].children[0].visible = hasTcField;
980 privacyFormItems.value[3].children[1].visible = hasTcField;
981 privacyFormItems.value[3].children[0].default = val.tcFieldName;
982 privacyFormItems.value[3].children[1].default = val.tcThreshold;
983 oldPrivacyModelValue.value.isTcField = hasTcField ? 'Y' : 'N';
984 oldPrivacyModelValue.value.tcFieldName = val.tcFieldName;
985 oldPrivacyModelValue.value.tcThreshold = val.tcThreshold;
986 }, {
987 deep: true
988 })
989
990 watch(() => props.anonTaskRules, (val) => {
991 ruleModelTableInfo.value.data = val || [];
992 let optionsList = val?.map(v => {
993 return {
994 fieldName: v.fieldName,
995 fieldChName: v.fieldChName
996 }
997 }) || [];
998 privacyFormItems.value[2].children[0].options = optionsList;
999 privacyFormItems.value[3].children[0].options = optionsList;
1000 })
1001
1002 onBeforeMount(() => {
1003 getParamsList({
1004 dictType: "标签类型",
1005 }).then((res: any) => {
1006 if (res?.code == proxy.$passCode) {
1007 labelTypeList.value = res.data || [];
1008 let item = fieldRulesFormItems.value.find(item => item.field == 'dataTypeCode');
1009 item && (item.options = labelTypeList.value);
1010 } else {
1011 proxy.$ElMessage.error(res.msg);
1012 }
1013 });
1014 getParamsList({
1015 dictType: "脱敏规则",
1016 }).then((res: any) => {
1017 if (res?.code == proxy.$passCode) {
1018 desensitiveRuleTypeList.value = res.data || [];
1019 let item = fieldRulesFormItems.value.find(item => item.field == 'desensitiveRuleCode');
1020 item && (item.options = desensitiveRuleTypeList.value);
1021 } else {
1022 proxy.$ElMessage.error(res.msg);
1023 }
1024 });
1025 getParamsList({
1026 dictType: "加密算法",
1027 }).then((res: any) => {
1028 if (res?.code == proxy.$passCode) {
1029 hashMethodList.value = res.data || [];
1030 let item = fieldRulesFormItems.value.find(item => item.field == 'encryptionAlgorithmCode');
1031 item && (item.options = hashMethodList.value);
1032 } else {
1033 proxy.$ElMessage.error(res.msg);
1034 }
1035 });
1036 getGeneralizeFileNameList().then((res: any) => {
1037 if (res?.code == proxy.$passCode) {
1038 generalizeFileNameList.value = res.data || [];
1039 let item = fieldRulesFormItems.value.find(item => item.field == 'generalizeFileGuid');
1040 item && (item.options = generalizeFileNameList.value);
1041 } else {
1042 proxy.$ElMessage.error(res.msg);
1043 }
1044 });
1045 })
1046
1047 /** 隐私模型设置表单 */
1048 const privacyFormRef = ref();
1049
1050 const getStepTwoConfigInfo = async () => {
1051 if (!ruleModelTableInfo.value.data?.length) {
1052 proxy.$ElMessage.error('字段脱敏规则不能为空');
1053 return false;
1054 }
1055 try {
1056 await privacyFormRef.value?.ruleFormRef?.validate();
1057 // 验证通过
1058 return {
1059 anonPrivacyMode: cloneDeep(privacyFormRef.value?.formInline),
1060 anonTaskRules: cloneDeep(ruleModelTableInfo.value.data)
1061 };
1062 } catch (error) {
1063 // 验证失败
1064 return false;
1065 }
1066 }
1067
1068 /** 字段改变,可能是切换了表,需要清空规则配置。判断 第一步到第二步时,如果字段列表中与字段脱敏规则中的字段不匹配,应清空,并同时清空T接近字段。 */
1069 const updateNextStepRules = () => {
1070 ruleModelTableInfo.value.data = ruleModelTableInfo.value.data.filter(rule => {
1071 if (!props.fieldNameList?.some(v => v.enName == rule.fieldName)) {
1072 return false;
1073 }
1074 return true;
1075 });
1076 privacyFormItems.value[2].children[0].options = props.fieldNameList;
1077 privacyFormItems.value[3].children[0].options = props.fieldNameList;
1078 let formInline = privacyFormRef.value?.formInline;
1079 privacyFormItems.value.forEach((item, index) => {
1080 item.default = formInline[item.field];
1081 if (item.default == 'Y') {
1082 item.children?.forEach(child => {
1083 child.default = formInline[child.field];
1084 if ((child.field == 'tcFieldName' || child.field == 'ldFieldName') && !props.fieldNameList.some(f => f.enName == child.default)) {
1085 child.default = ''
1086 }
1087 });
1088 }
1089 })
1090 }
1091
1092 defineExpose({
1093 getStepTwoConfigInfo,
1094 updateNextStepRules
1095 })
1096
1097 </script>
1098
1099 <template>
1100 <div class="operator_panel_wrap">
1101 <ContentWrap id="id-rules" title="设置匿名化规则" description="" style="margin-top: 8px;">
1102 <Table :tableInfo="ruleModelTableInfo" />
1103 <div class="row-add-btn">
1104 <el-button link @click="addRowRules" :icon="CirclePlus" v-preReClick>添加字段脱敏规则</el-button>
1105 </div>
1106 </ContentWrap>
1107 <ContentWrap id="id-screctModel" title="隐私模型设置" description="" style="margin-top: 16px;">
1108 <Form style="width: 80%;max-width: 500px;" ref="privacyFormRef" :itemList="privacyFormItems"
1109 :rules="privacyFormRules" formId="model-select-edit" @checkboxChange="handleCheckboxChange" />
1110 </ContentWrap>
1111
1112 <Drawer ref="drawerRef" :drawerInfo="drawerInfo" @drawerBtnClick="drawerBtnClick"
1113 @drawerInputChange="drawerInputChange" @drawerSelectChange="drawerSelectChange">
1114 <template v-slot:default>
1115 <div v-show="drawerRef?.getDrawerConRef('drawerFormRef')?.formInline?.desensitiveRuleCode == 'DISSEMBLE'">
1116 <div>{{ '请配置分段是否脱敏' + `(${desensitiveRuleDetail.ruleDetails?.length}/10)` }}</div>
1117 <el-radio-group v-model="desensitiveRuleDetail.dissembleType">
1118 <el-radio :value="1">从左往右</el-radio>
1119 <el-radio :value="2">从右往左</el-radio>
1120 </el-radio-group>
1121 <div class="seg-main">
1122 <div class="row-per" v-for="(item, index) in desensitiveRuleDetail.ruleDetails">
1123 <el-select v-model="item.digitType" :style="{ width: item.digitType == 2 ? '322px' : '170px' }">
1124 <el-option v-for="item in digitTypeList" :label="item.label" :value="item.value"
1125 :key="item.value"></el-option>
1126 </el-select>
1127 <el-input v-show="item.digitType != 2" style="width:137px;margin-left: 4px;" v-model="item.digit"
1128 :maxlength="6" :min="1" @input="(val) => inputEventDigitChange(val, item)" placeholder="请输入"></el-input>
1129 <el-select v-model="item.ruleType" style="width:170px;margin-left: 4px;">
1130 <el-option v-for="item in dissembleRuleTypeList" :label="item.label" :value="item.value"
1131 :key="item.value"></el-option>
1132 </el-select>
1133 <div class="title_tool" @click="deleteSegmentRule(item, index)">
1134 <el-icon :size="20" color="#b2b2b2">
1135 <Delete />
1136 </el-icon>
1137 </div>
1138 </div>
1139 <div class="row-add-btn">
1140 <el-button :disabled="desensitiveRuleDetail.ruleDetails?.length > 9" link @click="addSegmentRule"
1141 :icon="CirclePlus" v-preReClick>添加分段规则</el-button>
1142 </div>
1143 </div>
1144 </div>
1145 <div v-show="drawerRef?.getDrawerConRef('drawerFormRef')?.formInline?.desensitiveRuleCode == 'CHARREPLACE'">
1146 <div>{{ '请配置分段的替换方式' + `(${charReplaceRuleDetail.ruleDetails?.length}/10)` }}</div>
1147 <el-radio-group v-model="charReplaceRuleDetail.replaceType">
1148 <el-radio :value="1">从左往右</el-radio>
1149 <el-radio :value="2">从右往左</el-radio>
1150 </el-radio-group>
1151 <div class="seg-main">
1152 <div class="row-per" v-for="(item, index) in charReplaceRuleDetail.ruleDetails">
1153 <el-select v-model="item.digitType" :style="{ width: item.digitType == 2 ? '220px' : '130px' }">
1154 <el-option v-for="item in digitTypeList" :label="item.label" :value="item.value"
1155 :key="item.value"></el-option>
1156 </el-select>
1157 <el-input v-show="item.digitType != 2" style="width:86px;margin-left: 4px;" v-model="item.digit"
1158 :maxlength="6" :min="1" @input="(val) => inputEventDigitChange(val, item)" placeholder="请输入"></el-input>
1159 <el-select v-model="item.ruleType"
1160 :style="{ width: item.ruleType == 1 ? '244px' : '130px', 'margin-left': '4px' }">
1161 <el-option v-for="item in charReplaceRuleTypeList" :label="item.label" :value="item.value"
1162 :key="item.value"></el-option>
1163 </el-select>
1164 <el-input v-model="item.fixedValue" v-show="item.ruleType == 2" style="width:110px;margin-left: 4px;"
1165 :maxlength="50" placeholder="请输入"></el-input>
1166 <div class="title_tool" @click="deleteCharReplaceSegmentRule(item, index)">
1167 <el-icon :size="20" color="#b2b2b2">
1168 <Delete />
1169 </el-icon>
1170 </div>
1171 </div>
1172 <div class="row-add-btn">
1173 <el-button :disabled="charReplaceRuleDetail.ruleDetails?.length > 9" link
1174 @click="addCharReplaceSegmentRule" :icon="CirclePlus" v-preReClick>添加分段规则</el-button>
1175 </div>
1176 </div>
1177 </div>
1178 <div v-show="drawerRef?.getDrawerConRef('drawerFormRef')?.formInline?.desensitiveRuleCode == 'RANGEREPLACE'">
1179 <div class="mb8">{{ '请配置区间替换规则' + `(${rangeReplaceRuleDetails?.length}/10)` }}</div>
1180 <div class="seg-main">
1181 <div class="row-per" v-for="(item, index) in rangeReplaceRuleDetails">
1182 <el-input style="width:16.5%" v-model="item.lowValue" :maxlength="6"
1183 @input="(val) => inputEventDigitChange(val, item, 'lowValue')" placeholder="请输入"></el-input>
1184 <el-select v-model="item.lowOperator" style="width: 16.5%;margin-left: 4px;">
1185 <el-option v-for="item in lowerOperatorList" :label="item.label" :value="item.value"
1186 :key="item.value"></el-option>
1187 </el-select>
1188 <el-input style="width: 16.5%;margin-left: 4px;" :disabled="true" v-model="item.fieldChName"></el-input>
1189 <el-select v-model="item.upperOperator" style="width: 16.5%;margin-left: 4px;">
1190 <el-option v-for="item in lowerOperatorList" :label="item.label" :value="item.value"
1191 :key="item.value"></el-option>
1192 </el-select>
1193 <el-input style="width:16.5%;margin-left: 4px;" v-model="item.upperValue" :maxlength="6"
1194 @input="(val) => inputEventDigitChange(val, item, 'upperValue')" placeholder="请输入"></el-input>
1195 <el-input v-model="item.replaceValue" style="width:16.5%;margin-left: 4px;" :maxlength="50"
1196 placeholder="替换值"></el-input>
1197 <div class="title_tool" @click="deleteRangeReplaceSegmentRule(item, index)">
1198 <el-icon :size="20" color="#b2b2b2">
1199 <Delete />
1200 </el-icon>
1201 </div>
1202 </div>
1203 <div class="row-add-btn">
1204 <el-button :disabled="charReplaceRuleDetail.ruleDetails?.length > 9" link
1205 @click="addRangeReplaceSegmentRule" :icon="CirclePlus" v-preReClick>添加分段规则</el-button>
1206 </div>
1207 </div>
1208 </div>
1209 </template>
1210 </Drawer>
1211
1212 </div>
1213 </template>
1214
1215 <style lang="scss" scoped>
1216 .row-add-btn {
1217 .el-button--default {
1218 padding: 4px 0px;
1219 }
1220
1221 :deep(.el-icon) {
1222 width: 16px;
1223 height: 16px;
1224
1225 svg {
1226 width: 16px;
1227 height: 16px;
1228 }
1229 }
1230 }
1231
1232 .seg-main {
1233 .row-add-btn {
1234 margin-top: -6px;
1235 }
1236 }
1237
1238 .row-per {
1239 display: flex;
1240 align-items: center;
1241 margin-bottom: 8px;
1242 position: relative;
1243
1244 .title_tool {
1245 margin-left: 4px;
1246 // position: absolute;
1247 // right: 4px;
1248 cursor: pointer;
1249
1250 :deep(.el-icon) {
1251 --color: #FB2323 !important;
1252
1253 svg {
1254 width: 16px;
1255 height: 16px;
1256 }
1257 }
1258 }
1259 }
1260
1261 :deep(.mt8.drawer_panel) {
1262 margin-top: 8px;
1263 }
1264
1265 .mb8 {
1266 margin-bottom: 8px;
1267 }
1268
1269 :deep(.el-form-item.ka-checkbox-input) {
1270 .el-input {
1271 min-width: 50px !important;
1272 }
1273 }
1274
1275 :deep(.el-form) {
1276 .lmt12 {
1277 // margin-top: -4px; 验证信息会被遮挡
1278
1279 .input_panel {
1280 flex: 0 !important;
1281 }
1282 }
1283 }
1284 </style>
...\ No newline at end of file ...\ No newline at end of file
1 <template>
2 <div class="analysis-result-main">
3 <div v-if="showTitle" class="result-title">匿名结果分析</div>
4 <div class="kpi-content" v-show="Object.keys(analysisResultInfo).length > 0">
5 <div class="border-content">
6 <div class="text">去标识化效果评估结果</div>
7 <span class="number score-color">{{ analysisResultInfo.rating + '级' }}</span>
8 </div>
9 <div class="border-content">
10 <span class="text">重标识风险最大值<el-tooltip placement="top" effect="light" popper-class="table_tooltip">
11 <template #content>
12 <div style="max-width: 236px;">
13 所有等价类的重标识风险最大值
14 </div>
15 </template>
16 <el-icon style="margin-left: 2px;margin-top: 2px;">
17 <QuestionFilled />
18 </el-icon>
19 </el-tooltip></span>
20 <span class="number">{{ analysisResultInfo.reIdentifyRiskRb != null ?
21 (analysisResultInfo.reIdentifyRiskRb || 0) : '--'
22 }}</span>
23 </div>
24 <div class="border-content">
25 <span class="text">重标识风险平均值<el-tooltip placement="top" effect="light" popper-class="table_tooltip">
26 <template #content>
27 <div style="max-width: 236px;">
28 所有等价类的重标识风险平均值
29 </div>
30 </template>
31 <el-icon style="margin-left: 2px;margin-top: 2px;">
32 <QuestionFilled />
33 </el-icon>
34 </el-tooltip></span>
35 <span class="number">{{ analysisResultInfo.reIdentifyRiskRc != null ?
36 (analysisResultInfo.reIdentifyRiskRc || 0) : '--' }}</span>
37 </div>
38 <div class="border-content">
39 <span class="text">环境重标识攻击概率<el-tooltip placement="top" effect="light" popper-class="table_tooltip">
40 <template #content>
41 <div style="max-width: 236px;">
42 完全公开共享数据发布,攻击者对数据集进行环境重标识攻击的概率为1。领地公开共享与受控公开共享数据发布,环境重标识攻击概率为内部故意攻击概率、数据集包含熟人概率和数据泄露概率三者的最大值。
43 </div>
44 </template>
45 <el-icon style="margin-left: 2px;margin-top: 2px;">
46 <QuestionFilled />
47 </el-icon>
48 </el-tooltip></span>
49 <span class="number">{{ analysisResultInfo.envReAttackPr != null ?
50 (analysisResultInfo.envReAttackPr || 0) : '--' }}</span>
51 </div>
52 <div class="border-content">
53 <span class="text">等价类门限风险<el-tooltip placement="top" effect="light" popper-class="table_tooltip">
54 <template #content>
55 <div style="max-width: 236px;">
56 完全公开共享数据发布,门限阈值取值 1/20;受控公开共享数据发布,门限阈值取值 1/5;领地公开共享数据发布,门限阈值取值
57 1/3;等价类门限风险为:等价类的重标识风险大于门限阈值为1,小于等于为0,求和后除以等价类个数。
58 </div>
59 </template>
60 <el-icon style="margin-left: 2px;margin-top: 2px;">
61 <QuestionFilled />
62 </el-icon>
63 </el-tooltip></span>
64 <span class="number">{{ analysisResultInfo.reIdentifyRiskRa != null ?
65 (analysisResultInfo.reIdentifyRiskRa || 0) : '--' }}</span>
66 </div>
67 <div class="border-content">
68 <span class="text">重标识风险总体风险<el-tooltip placement="top" effect="light" popper-class="table_tooltip">
69 <template #content>
70 <div style="max-width: 236px;">
71 完全公开共享,当等价类门限风险=0时,重标识风险总体风险公式为等价类重标识风险最大值*环境重标识攻击概率;当等价类门限风险!=0时,重标识风险总体风险为1。
72 受控公开共享和领地公开共享,当等价类门限风险=0时,重标识风险总体风险公式为等价类重标识风险平均值*环境重标识攻击概率;当等价类门限风险!=0时,重标识风险总体风险为1。
73 <!-- {{ oldAnonTaskValueInfo.dataSharingTypeCode == '01' ? '完全公开共享,当等价类门限风险=0时,重标识风险总体风险公式为等价类重标识风险最大值*环境重标识攻击概率;当等价类门限风险!=0时,重标识风险总体风险为1。'
74 : `${oldAnonTaskValueInfo.dataSharingTypeCode == '02' ? '受控公开共享' : '领地公开共享'},当等价类门限风险=0时,重标识风险总体风险公式为等价类重标识风险平均值*环境重标识攻击概率;当等价类门限风险!=0时,重标识风险总体风险为1。` }} -->
75 </div>
76 </template>
77 <el-icon style="margin-left: 2px;margin-top: 2px;">
78 <QuestionFilled />
79 </el-icon>
80 </el-tooltip></span>
81 <span class="number">{{ analysisResultInfo.reIdentifyOverallRisk != null ?
82 (analysisResultInfo.reIdentifyOverallRisk || 0) : '--' }}</span>
83 </div>
84 <div class="border-content">
85 <span class="text">重标识可接受风险阈值</span>
86 <span class="number">{{ oldAnonTaskValueInfo.anonPrivacyMode?.riskThreshold == null ? 0.05 :
87 (oldAnonTaskValueInfo.anonPrivacyMode?.riskThreshold || 0) }}</span>
88 </div>
89 </div>
90 <div class="result-title">重标识风险表</div>
91 <el-table ref="tableRef" v-show="analysisResultTableFields.length" :data="resultData"
92 v-loading="analysisResultLoading" :highlight-current-row="true" stripe border tooltip-effect="light" height="100%"
93 row-key="guid" :style="{ width: '100%', height: '280px' }">
94 <el-table-column label="等价类" type="index" width="68px" align="center" show-overflow-tooltip>
95 <template #default="scope">
96 <span>{{
97 pageInfo.curr !== undefined
98 ? (pageInfo.curr - 1) * pageInfo.limit + scope.$index + 1
99 : scope.$index + 1
100 }}</span>
101 </template>
102 </el-table-column>
103 <template v-for="(item, index) in (analysisResultTableFields || [])">
104 <el-table-column :label="item.chName" :width="item.dataType === 'datetime'
105 ? TableColumnWidth.DATETIME
106 : item.dataType === 'date'
107 ? TableColumnWidth.DATE
108 : originResultTableFieldColumn[item.enName]
109 " :align="getTextAlign(item)" :header-align="getTextAlign(item)"
110 :formatter="(row) => formatterPreviewDate(row, item)" :show-overflow-tooltip="true">
111 </el-table-column>
112 </template>
113 <el-table-column label="等价类大小" prop="equivalenceClassNum" width="98" align="right" fixed="right"
114 show-overflow-tooltip></el-table-column>
115 <el-table-column label="重标识风险" prop="reIdentifyRisk" width="96" align="right" fixed="right"
116 show-overflow-tooltip></el-table-column>
117 <el-table-column label="判断重风险是否大于门限阈值" prop="isGtThreshold" width="130" align="left" fixed="right"
118 show-overflow-tooltip></el-table-column>
119 </el-table>
120 <div v-show="!analysisResultTableFields.length" class="empty-content">
121 <img src="../../../assets/images/empty-data.png" :style="{ width: '168px', height: '96px' }" />
122 <div class="empty-text">暂无数据</div>
123 </div>
124 <div v-show="analysisResultTableFields.length" class="result-table-desc">门限阈值的取值:完全公开共享数据发布,取值
125 1/20;受控公开共享数据发布,取值
126 1/5;领地公开共享数据发布,取值 1/3</div>
127 <PageNav v-show="analysisResultTableFields.length" :class="[pageInfo.type]" :pageInfo="pageInfo"
128 @pageChange="pageChange" />
129 <div class="row-two-main" style="margin-top: 12px;">
130 <div class="table-one">
131 <div class="result-title">对抗性测试关键变量</div>
132 <el-table ref="varTableRef" :data="analysisResultInfo?.adversarialTest || []" :highlight-current-row="true"
133 stripe border tooltip-effect="light" height="100%" row-key="guid"
134 :style="{ height: oldAnonTaskValueInfo.dataSharingTypeCode != '01' ? (containerWidth > 1397 ? '378px' : (containerWidth < 1048 ? '414px' : '396px')) : 'auto' }">
135 <el-table-column label="序号" type="index" width="56px" align="center" show-overflow-tooltip>
136 </el-table-column>
137 <el-table-column label="数据项" prop="chName" width="150px" align="left" show-overflow-tooltip></el-table-column>
138 <el-table-column label="唯一性分值" prop="uniqueScore" width="110px" align="right"
139 show-overflow-tooltip></el-table-column>
140 <el-table-column label="影响力分值" prop="influenceScore" width="110" align="right"
141 show-overflow-tooltip></el-table-column>
142 <el-table-column label="数据属性标识度分值" prop="dataAttrIdentScore" width="160" align="right"
143 show-overflow-tooltip></el-table-column>
144 </el-table>
145 </div>
146 <div class="table-two" v-show="oldAnonTaskValueInfo.dataSharingTypeCode != '01'">
147 <div class="result-title">内部故意攻击概率</div>
148 <div class="desc" style="margin-bottom: 4px;color: #999;line-height: 18px;">
149 重标识数据的动机和能力为低,从重标识攻击可能性分析表可得出在内部攻击方面,重标识攻击概率的取值为0.05。</div>
150 <el-table ref="innerTableRef" :data="innerResultData" :highlight-current-row="true" stripe border
151 tooltip-effect="light" height="100%" row-key="guid" :style="{ height: '356px' }"
152 :span-method="arrayInnerSpanMethod" :cell-class-name="handleInnerCellClass">
153 <!-- <el-table-column label="序号" type="index" width="56px" align="center" show-overflow-tooltip>
154 </el-table-column> -->
155 <el-table-column label="风险减缓控制水平" prop="level" width="150px" align="left"
156 show-overflow-tooltip></el-table-column>
157 <el-table-column label="动机和能力" prop="competencyLevel" width="140px" align="left"
158 show-overflow-tooltip></el-table-column>
159 <el-table-column label="重标识攻击概率" prop="value" width="140" align="right" show-overflow-tooltip>
160 <template #default="scope">
161 <span>{{ scope.row.value }}</span>
162 </template>
163 </el-table-column>
164 </el-table>
165 </div>
166 </div>
167 <div class="row-two-main" v-show="oldAnonTaskValueInfo.dataSharingTypeCode != '01'">
168 <div class="table-one border">
169 <div class="result-title">数据集包含熟人概率</div>
170 <div class="result-title-h1">pr=1-1-p^m</div>
171 <div class="result-title-desc">
172 {{ ` 数据集包含熟人概率可通过以上公式计算,${new
173 Date().getFullYear()}年我国最新的人口统计为${analysisResultInfo?.allPerson ||
174 0}亿人,其中该数据集的容量为${analysisResultInfo?.dataSetNum || 0}万,
175 占总人口的比例为${analysisResultInfo.patientPopulationRate ||
176 0}%,m值取推荐值150,数据集包含熟人的概率为${analysisResultInfo.randomAcquaintancePr || 0}。` }}
177 </div>
178 </div>
179 <div class="table-two border">
180 <div class="result-title">数据泄露概率</div>
181 <div class="result-title-desc">
182 <div>对于安全和隐私控制能力评估为低的情况,推荐将数据泄露概率设定为0.55</div>
183 <div>对于安全和隐私控制能力评估为中的情况,推荐将数据泄露概率设定为0.27</div>
184 <div>对于安全和隐私控制能力评估为高的情况,推荐将数据泄露概率设定为 0.14</div>
185 <div>{{ `数据接收方的安全和隐私控制能力为高,按照推荐值将数据泄露概率设定为${analysisResultInfo.dataBreachPr || 0}。` }}</div>
186 </div>
187 </div>
188 </div>
189 </div>
190 </template>
191 <script lang="ts" setup name="anonResultAnalysis">
192 import { TableColumnWidth } from '@/utils/enum';
193 import Moment from 'moment';
194
195 defineProps({
196 showTitle: {
197 type: Boolean,
198 default: false,
199 },
200 analysisResultInfo: {
201 type: Object,
202 default: {},
203 },
204 pageInfo: {
205 type: Object,
206 default: {},
207 },
208 analysisResultLoading: {
209 type: Boolean,
210 default: false,
211 },
212 oldAnonTaskValueInfo: {
213 type: Object,
214 default: {},
215 },
216 resultData: {
217 type: Array,
218 default: []
219 },
220 analysisResultTableFields: {
221 type: Array,
222 default: [],
223 },
224 originResultTableFieldColumn: {
225 type: Object,
226 default: {},
227 },
228 containerWidth: {
229 type: Number,
230 default: 1200,
231 },
232 })
233
234 const emits = defineEmits([
235 "pageChange"
236 ]);
237
238 const getTextAlign = (field) => {
239 if (field.dataType === 'decimal' || field.dataType === 'int') {
240 return 'right';
241 }
242 return 'left'
243 }
244
245 const formatterPreviewDate = (row, info) => {
246 let enName = info.enName;
247 let v = row[enName];
248 if (v === 0) {
249 return v;
250 }
251 if (!v || v == 'null') {
252 return '--';
253 }
254 if (info.dataType === 'datetime') {
255 return Moment(v).format('YYYY-MM-DD HH:mm:ss');
256 }
257 if (info.dataType === 'date') {
258 if (isNaN(<any>(new Date(v)))) {
259 return Moment(parseInt(v)).format('YYYY-MM-DD');
260 } else {
261 return Moment(v).format('YYYY-MM-DD');
262 }
263 }
264 return v;
265 };
266
267 /** 内部故意攻击概率的表格 */
268 const innerResultData = ref([{
269 guid: '1',
270 level: '高',
271 competencyLevel: '低',
272 value: '0.05'
273 }, {
274 guid: '2',
275 level: '高',
276 competencyLevel: '中',
277 tooltip: true,
278 value: '0.1'
279 }, {
280 guid: '3',
281 level: '高',
282 competencyLevel: '高',
283 tooltip: true,
284 value: '0.2'
285 }, {
286 guid: '4',
287 level: '中',
288 competencyLevel: '低',
289 value: '0.2'
290 }, {
291 guid: '5',
292 level: '中',
293 competencyLevel: '中',
294 value: '0.3'
295 }, {
296 guid: '6',
297 level: '中',
298 competencyLevel: '高',
299 value: '0.4'
300 }, {
301 guid: '7',
302 level: '低',
303 competencyLevel: '低',
304 value: '0.4'
305 }, {
306 guid: '8',
307 level: '低',
308 competencyLevel: '中',
309 value: '0.5'
310 }, {
311 guid: '9',
312 level: '低',
313 competencyLevel: '高',
314 value: '0.6'
315 }]);
316
317 const arrayInnerSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
318 if (columnIndex > 0) {
319 return [1, 1];
320 }
321
322 // 查找当前字段值相同的连续行
323 let startRow = rowIndex;
324 let endRow = rowIndex;
325 let field = 'level';
326 // 向前查找
327 while (startRow > 0 && innerResultData.value[startRow - 1][field] === innerResultData.value[rowIndex][field]) {
328 startRow--;
329 }
330
331 // 向后查找
332 while (endRow < innerResultData.value.length - 1 && innerResultData.value[endRow + 1][field] === innerResultData.value[rowIndex][field]) {
333 endRow++;
334 }
335
336 // 如果当前行不是相同值组的起始行,则不显示
337 if (startRow !== rowIndex) {
338 return [0, 0];
339 }
340
341 // 返回合并的行数
342 const rowspan = endRow - startRow + 1;
343 return [rowspan, 1];
344 }
345
346 const handleInnerCellClass = ({ row, column, rowIndex, columnIndex }) => {
347 if (rowIndex == 0 && columnIndex > 0) {
348 return 'cell-tooltip-bg';
349 }
350 return '';
351 }
352
353 const pageChange = (info) => {
354 emits('pageChange', info);
355 }
356
357 </script>
358
359 <style lang="scss" scoped>
360 .analysis-result-main {
361 min-height: 250px;
362
363 .value-desc {
364 font-size: 14px;
365 color: #212121;
366 line-height: 21px;
367 }
368
369 .result-title {
370 font-size: 16px;
371 color: #212121;
372 line-height: 24px;
373 font-weight: 600;
374 margin-bottom: 6px;
375 }
376
377 .result-title-h1 {
378 color: #212121;
379 font-weight: 600;
380 font-size: 24px;
381 text-align: center;
382 line-height: 36px;
383 margin-top: 12px;
384 }
385
386 .result-title-desc {
387 color: #666;
388 font-size: 14px;
389 line-height: 21px;
390 margin-top: 12px;
391 }
392
393 .kpi-content {
394 display: flex;
395 flex-direction: row;
396 column-gap: 12px;
397 row-gap: 12px;
398 flex-wrap: wrap;
399 margin-bottom: 20px;
400 }
401
402 .border-content {
403 height: 76px;
404 display: flex;
405 flex-direction: column;
406 align-items: left;
407 padding-left: 16px;
408 justify-content: center;
409 border: 1px solid #d9d9d9;
410 width: calc(20% - 8px);
411 min-width: 228px;
412 border-radius: 2px;
413 padding-left: 16px;
414
415 .number {
416 font-weight: 700;
417 font-size: 20px;
418 color: #212121;
419 line-height: 30px;
420 margin-top: 2px;
421
422 &.score-color {
423 color: #FF5F1F;
424 }
425 }
426
427 .text {
428 font-size: 14px;
429 line-height: 21px;
430 color: #666666;
431 display: flex;
432
433 .el-icon {
434 color: #b2b2b2;
435 }
436 }
437 }
438
439 .result-table-desc {
440 font-size: 14px;
441 color: #999999;
442 line-height: 21px;
443 }
444
445 .row-two-main {
446 margin-top: 18px;
447 display: flex;
448
449 .table-one {
450 width: 586px;
451
452 &.border {
453 border: 1px solid #d9d9d9;
454 padding: 14px 18px 18px;
455 }
456 }
457
458 .table-two {
459 margin-left: 20px;
460 width: calc(100% - 606px);
461
462 &.border {
463 border: 1px solid #d9d9d9;
464 padding: 14px 18px 18px;
465 }
466 }
467 }
468 }
469
470 .empty-content {
471 display: flex;
472 align-items: center;
473 justify-content: center;
474 height: 316px;
475 width: 100%;
476 flex-direction: column;
477
478 .empty-text {
479 font-size: 14px;
480 color: #b2b2b2;
481 }
482 }
483 </style>
...\ No newline at end of file ...\ No newline at end of file
1 <route lang="yaml">
2 name: labelManagement
3 </route>
4
5 <script lang="ts" setup name="labelManagement">
6 import { commonPageConfig } from '@/components/PageNav/index';
7 import { TableColumnWidth } from '@/utils/enum';
8 import TableTools from "@/components/Tools/table_tools.vue";
9 import {
10 getDataLabelList,
11 updateLabelState,
12 saveLabel,
13 updateLabel,
14 deleteLabel,
15 getLabelDetail,
16 getParamsList,
17 } from '@/api/modules/dataAnonymization';
18 import { useValidator } from '@/hooks/useValidator';
19 import { Delete, CirclePlus, Warning } from "@element-plus/icons-vue";
20
21 const { required, regexpRuleValidate } = useValidator();
22 const { proxy } = getCurrentInstance() as any;
23
24 /** 标签类型字典列表 */
25 const labelTypeList: any = ref([]);
26
27 /** 内置规则字典列表 */
28 const builtInRuleList: any = ref([]);
29
30 const searchItemList = ref([{
31 type: "input",
32 label: "",
33 field: "labelName",
34 default: "",
35 placeholder: "标签名称",
36 clearable: true,
37 }])
38
39 /** 分页及搜索传参信息配置。 */
40 const page = ref({
41 ...commonPageConfig,
42 labelName: '',
43 });
44
45 const currTableData: any = ref();
46
47 const tableInfo = ref({
48 id: 'data-label-table',
49 // multiple:true,
50 fields: [
51 { label: "序号", type: "index", width: TableColumnWidth.INDEX, align: "center" },
52 { label: "标签名称", field: "labelName", width: 140 },
53 { label: "标签类型", field: "labelTypeName", width: 140 },
54 { label: '状态', field: 'bizState', type: 'switch', activeText: '启用', inactiveText: '禁用', activeValue: 'Y', inactiveValue: 'N', switchWidth: 56, width: 100, align: 'center' },
55 { label: "界面排序", field: "orderNum", width: 90, align: "center" },
56 { label: "修改人", field: "updateUserName", width: TableColumnWidth.USERNAME },
57 { label: "修改时间", field: "updateTime", width: TableColumnWidth.DATETIME },
58 ],
59 data: [],
60 page: {
61 type: "normal",
62 rows: 0,
63 ...page.value,
64 },
65 actionInfo: {
66 label: "操作",
67 type: "btn",
68 width: 120,
69 fixed: 'right',
70 btns: (scope) => {
71 let btnsArr: any = [];
72 btnsArr.push({
73 label: "编辑", value: "edit", click: (scope) => {
74 currTableData.value = scope.row;
75 getLabelDetail(scope.row.guid).then((res: any) => {
76 if (res?.code == proxy.$passCode) {
77 const detail = res.data || {};
78 currTableData.value = Object.assign({}, currTableData.value, detail);
79 newCreateLabelFormItems.value.forEach(item => {
80 if (item.children?.length) {
81 item.children.forEach(child => child.default = detail[child.field]);
82 } else {
83 item.default = detail[item.field];
84 }
85 item.default = detail[item.field];
86 });
87 let labelRuleField = detail.labelRuleField || {};
88 matchChValue.value.value = labelRuleField.matchChValue;
89 matchChValue.value.disabled = false;
90 matchEnValue.value.value = labelRuleField.matchEnValue;
91 matchEnValue.value.disabled = false;
92 formRows.value = labelRuleField.vagueMatchRule || [{ matchValue: '', position: '', name: '', disabled: false }];
93 tabsInfo.value.activeName = 'labelRuleField';
94 let labelRuleContent = detail.labelRuleContent || {};
95
96 ruleContentFormItems.value[1].visible = labelRuleContent.ruleType == 1;
97 ruleContentFormItems.value[2].visible = labelRuleContent.ruleType == 2;
98 ruleContentFormItems.value.forEach(item => {
99 item.default = labelRuleContent[item.field];
100 if (item.field == 'regularTestData') {
101 item.validateMsg = null;
102 }
103 });
104
105 newCreateLabelDialogInfo.value.title = '编辑标签';
106 newCreateLabelDialogInfo.value.visible = true;
107 newCreateLabelDialogInfo.value.type = 'update';
108 newCreateLabelDialogInfo.value.submitBtnLoading = false;
109 } else {
110 proxy.$ElMessage({
111 type: 'error',
112 message: res.msg,
113 })
114 }
115 });
116 }
117 })
118 btnsArr.push({
119 label: "删除", value: "delete", disabled: scope.row.bizState == 'Y', click: (scope) => {
120 proxy.$openMessageBox("此操作将永久删除, 是否继续?", () => {
121 let guids = [scope.row.guid];
122 deleteLabel(guids).then((res: any) => {
123 if (res?.code == proxy.$passCode) {
124 getTableData();
125 proxy.$ElMessage({
126 type: "success",
127 message: "删除成功",
128 });
129 } else {
130 proxy.$ElMessage({
131 type: "error",
132 message: res.msg,
133 });
134 }
135 });
136 })
137 }
138 });
139 return btnsArr
140 },
141 },
142 loading: false
143 })
144
145 const toSearch = (val: any, clear: boolean = false) => {
146 if (clear) {
147 searchItemList.value.map((item) => (item.default = ""));
148 page.value.labelName = '';
149 } else {
150 page.value.labelName = val.labelName;
151 }
152 getTableData();
153 };
154
155 const getTableData = () => {
156 tableInfo.value.loading = true
157 getDataLabelList({
158 pageIndex: page.value.curr,
159 pageSize: page.value.limit,
160 labelName: page.value.labelName
161 }).then((res: any) => {
162 if (res?.code == proxy.$passCode) {
163 const data = res.data || {}
164 tableInfo.value.data = data.records || []
165 tableInfo.value.page.limit = data.pageSize
166 tableInfo.value.page.curr = data.pageIndex
167 tableInfo.value.page.rows = data.totalRows
168 } else {
169 proxy.$ElMessage({
170 type: 'error',
171 message: res.msg,
172 })
173 }
174 tableInfo.value.loading = false
175 })
176 };
177
178 const tableSwitchBeforeChange = (scope, field, callback) => {
179 const msg = `确定【${scope.row[field] == 'Y' ? '禁用' : '启用'}${scope.row.labelName}?`
180 proxy.$openMessageBox(msg, () => {
181 const state = scope.row[field] == 'Y' ? 'N' : 'Y'
182 const result = tableSwitchChange(state, scope, field)
183 callback(result)
184 }, () => {
185 callback(false)
186 });
187 }
188
189 const tableSwitchChange = (val, scope, field) => {
190 return new Promise((resolve, reject) => {
191 let params = {
192 guid: scope.row.guid,
193 bizState: val
194 }
195 updateLabelState(params).then((res: any) => {
196 if (res?.code == proxy.$passCode && res.data) {
197 getTableData();
198 proxy.$ElMessage({
199 type: "success",
200 message: `${scope.row.labelName}${val == 'Y' ? '启用' : '禁用'}】成功`,
201 });
202 resolve(true)
203 } else {
204 proxy.$ElMessage({
205 type: "error",
206 message: res.msg,
207 });
208 reject(false)
209 }
210 }).catch(() => {
211 reject(false)
212 })
213 })
214 }
215
216 const tablePageChange = (info) => {
217 page.value.curr = Number(info.curr);
218 page.value.limit = Number(info.limit);
219 getTableData();
220 };
221
222 const newCreateLabelFormItems = ref<any>([{
223 label: '标签名称',
224 type: 'input',
225 placeholder: '请输入',
226 field: 'labelName',
227 default: '',
228 required: true,
229 filterable: true,
230 clearable: true,
231 visible: true,
232 },
233 {
234 label: ' ',
235 type: 'select-group',
236 placeholder: '请选择',
237 field: 'labelgroup',
238 children: [{
239 label: '标签类型',
240 type: 'select',
241 placeholder: '请选择',
242 field: 'labelTypeCode',
243 default: '',
244 options: labelTypeList.value,
245 props: {
246 label: "label",
247 value: "value",
248 },
249 required: true,
250 filterable: true,
251 clearable: true,
252 visible: true,
253 }, {
254 label: '界面排序',
255 type: 'input',
256 placeholder: '请输入',
257 field: 'orderNum',
258 maxlength: 6,
259 regexp: /\D/g,
260 required: false,
261 clearable: true,
262 }],
263 }, {
264 type: 'radio-group',
265 label: '规则配置',
266 field: 'matchType',
267 default: 1,
268 required: false,
269 block: true,
270 options: [
271 { label: '字段识别和内容识别满足其一即可', value: 1 },
272 { label: '字段识别和内容识别需满足全部', value: 2 },
273 ],
274 }]);
275
276 const newCreateLabelFormRules = ref({
277 labelName: [required('请输入标签名称')],
278 labelTypeCode: [required('请选择标签类型')]
279 });
280
281 const ruleContentFormRef = ref();
282
283 const newCreateLabelDialogInfo = ref({
284 visible: false,
285 size: 600,
286 title: "添加标签",
287 type: "",
288 formInfo: {
289 id: "label-form",
290 items: newCreateLabelFormItems.value,
291 rules: newCreateLabelFormRules.value,
292 },
293 submitBtnLoading: false,
294 btns: {
295 cancel: () => {
296 newCreateLabelDialogInfo.value.visible = false;
297 newCreateLabelDialogInfo.value.submitBtnLoading = false;
298 },
299 submit: (btn, info) => {
300 // 需要验证两个
301 let validateRuleField = (showMsg = true) => {
302 if (!matchChValue.value.value && !matchEnValue.value.value && (!formRows.value.length || (formRows.value.length == 1 && !formRows.value[0].matchValue &&
303 !formRows.value[0].position && !formRows.value[0].name))) {
304 showMsg && proxy.$ElMessage.error('字段识别匹配规则不能为空');
305 return false;
306 }
307 for (let i = 0; i < formRows.value.length; i++) {
308 const row = formRows.value[i];
309 // 如果某一条数据的 matchValue, position, name 都为空,则跳过,不校验
310 if (!row.matchValue && !row.position && !row.name) {
311 continue; // 如果全为空,跳过这一行的校验
312 }
313 if (!row.matchValue || !row.position || !row.name) {
314 showMsg && proxy.$ElMessage.error('请填写完整的模糊匹配规则');
315 return false;
316 }
317 }
318 return true;
319 }
320 let submitLabel = () => {
321 let params = Object.assign({}, info, {
322 bizState: 'Y',
323 orderNum: info.orderNum != null ? parseInt(info.orderNum) : null,
324 labelRuleField: {
325 matchChValue: matchChValue.value.value,
326 matchEnValue: matchEnValue.value.value,
327 vagueMatchRule: formRows.value?.filter(f => f.matchValue!= '' || f.position!= '' || f.name != '')
328 },
329 labelRuleContent: ruleContentFormRef.value.formInline
330 });
331 newCreateLabelDialogInfo.value.submitBtnLoading = true;
332 if (newCreateLabelDialogInfo.value.type == 'add') {
333 saveLabel(params).then((res: any) => {
334 if (res?.code == proxy.$passCode) {
335 proxy.$ElMessage.success('标签新建成功');
336 newCreateLabelDialogInfo.value.visible = false;
337 newCreateLabelDialogInfo.value.submitBtnLoading = false;
338 page.value.curr = 1;
339 getTableData();
340 } else {
341 newCreateLabelDialogInfo.value.submitBtnLoading = false;
342 proxy.$ElMessage.error(res.msg);
343 }
344 }).catch(() => {
345 newCreateLabelDialogInfo.value.submitBtnLoading = false;
346 });
347 } else {
348 newCreateLabelDialogInfo.value.submitBtnLoading = true;
349 params.guid = currTableData.value.guid;
350 params.labelRuleContent.guid = currTableData.value.labelRuleContent?.guid;
351 params.labelRuleField.guid = currTableData.value.labelRuleField?.guid;
352 updateLabel(params).then((res: any) => {
353 if (res?.code == proxy.$passCode) {
354 proxy.$ElMessage.success('标签编辑成功');
355 newCreateLabelDialogInfo.value.visible = false;
356 newCreateLabelDialogInfo.value.submitBtnLoading = false;
357 getTableData();
358 } else {
359 newCreateLabelDialogInfo.value.submitBtnLoading = false;
360 proxy.$ElMessage.error(res.msg);
361 }
362 }).catch(() => {
363 newCreateLabelDialogInfo.value.submitBtnLoading = false;
364 });
365 }
366 }
367 if (info.matchType == 2) {
368 if (tabsInfo.value.activeName == 'labelRuleField') {
369 if (!validateRuleField()) {
370 return;
371 }
372 ruleContentFormRef.value?.ruleFormRef?.validate((valid, errorItem) => {
373 if (valid) {
374 submitLabel();
375 } else {
376 tabsInfo.value.activeName = 'labelRuleContent';
377 }
378 })
379 } else {
380 ruleContentFormRef.value?.ruleFormRef?.validate((valid, errorItem) => {
381 if (valid) {
382 if (!validateRuleField()) {
383 tabsInfo.value.activeName = 'labelRuleField';
384 return;
385 }
386 submitLabel();
387 }
388 })
389 }
390 } else { //只需匹配一项即可。
391 if (tabsInfo.value.activeName == 'labelRuleField') { //先验证字段识别
392 if (!matchChValue.value.value && !matchEnValue.value.value && (!formRows.value.length || (formRows.value.length == 1 && (!formRows.value[0].matchValue &&
393 !formRows.value[0].position && !formRows.value[0].name)))) { // 没有配置字段识别内容
394 ruleContentFormRef.value?.ruleFormRef?.validate((valid, errorItem) => {
395 if (valid) {
396 submitLabel();
397 } else {
398 proxy.$ElMessage.error('字段识别和内容识别不能同时为空');
399 return;
400 }
401 })
402 } else {
403 ruleContentFormRef.value?.ruleFormRef?.validate((valid, errorItem) => {
404 if (valid) {
405 submitLabel(); //只需要满足一个即可。
406 } else {
407 ruleContentFormRef.value?.ruleFormRef?.clearValidate(['builtInRuleCode', 'regularExpression', 'hitRate']);// 先清除验证
408 // 去 验证 字段识别
409 if (!validateRuleField(true)) {
410 return;
411 }
412 submitLabel();
413 }
414 })
415 }
416 } else {
417 ruleContentFormRef.value?.ruleFormRef?.validate((valid, errorItem) => {
418 if (valid) {
419 submitLabel(); //只需要满足一个即可。
420 } else {
421 if (!validateRuleField(false)) {
422 return;
423 }
424 submitLabel();
425 }
426 })
427 }
428 }
429 }
430 }
431 });
432
433 const formRows = ref([
434 { matchValue: '', position: '', name: '', disabled: false }, // 初始行
435 ]);
436
437 // 位置选项
438 const positionOptions = [
439 { label: '前面', value: 'B' },
440 { label: '后面', value: 'A' },
441 { label: '任意位置', value: 'C' },
442 ];
443 // 语言options
444 const languageOptions = [
445 { label: '中文名', value: 'chName' },
446 { label: '英文名', value: 'enName' },
447 ];
448
449 // 新增行
450 const addRow = () => {
451 formRows.value.push({ matchValue: '', position: '', name: '', disabled: false });
452 };
453
454 // 删除行
455 const deleteRow = (index: number) => {
456 formRows.value.splice(index, 1);
457 };
458 const matchChValue = ref({
459 value: '',
460 disabled: false
461 })
462 const matchEnValue = ref({
463 value: '',
464 disabled: false
465 })
466
467 const tabsInfo = ref({
468 activeName: 'labelRuleField',
469 tabs: [
470 { label: '字段识别', name: 'labelRuleField' },
471 { label: '内容识别', name: 'labelRuleContent' },
472 ]
473 });
474
475 const tabChange = (val) => {
476 tabsInfo.value.activeName = val;
477 }
478
479 const handleCreate = () => {
480 newCreateLabelFormItems.value.forEach(item => {
481 if (item.children?.length) {
482 item.children[0].default = ''
483 item.children[1].default = ''
484 } else {
485 if (item.field == 'matchType') {
486 item.default = 1;
487 } else {
488 item.default = '';
489 }
490 }
491 });
492 matchChValue.value.value = '';
493 matchChValue.value.disabled = false;
494 matchEnValue.value.value = '';
495 matchEnValue.value.disabled = false;
496 formRows.value = [{ matchValue: '', position: '', name: '', disabled: false }];
497
498 tabsInfo.value.activeName = 'labelRuleField';
499
500 ruleContentFormItems.value[1].visible = true;
501 ruleContentFormItems.value[2].visible = false;
502 ruleContentFormItems.value.forEach(item => {
503 item.default = item.field == 'ruleType' ? 1 : '';
504 if (item.field == 'regularTestData') {
505 item.validateMsg = null;
506 }
507 });
508
509 newCreateLabelDialogInfo.value.title = '添加标签';
510 newCreateLabelDialogInfo.value.visible = true;
511 newCreateLabelDialogInfo.value.type = 'add';
512 newCreateLabelDialogInfo.value.submitBtnLoading = false;
513 }
514
515 /** 规则配置的内容识别内容表单配置 */
516 const ruleContentFormItems = ref([{
517 label: '规则类型',
518 type: 'select',
519 placeholder: '请选择',
520 field: 'ruleType',
521 default: 1,
522 options: [{
523 value: 1,
524 label: '内置规则'
525 }, {
526 value: 2,
527 label: '自定义规则'
528 }],
529 props: {
530 label: "label",
531 value: "value",
532 },
533 required: true,
534 filterable: true,
535 clearable: false,
536 block: true,
537 visible: true,
538 }, {
539 label: '内置规则',
540 type: 'select',
541 placeholder: '请选择',
542 field: 'builtInRuleCode',
543 default: '',
544 options: builtInRuleList.value,
545 props: {
546 label: "label",
547 value: "value",
548 },
549 required: true,
550 filterable: true,
551 clearable: true,
552 block: true,
553 visible: true,
554 }, {
555 label: '规则表达式',
556 type: 'input',
557 placeholder: '请输入正则表达式',
558 field: 'regularExpression',
559 default: '',
560 required: true,
561 filterable: true,
562 clearable: true,
563 block: true,
564 visible: false,
565 }, {
566 label: '测试验证',
567 type: 'input',
568 placeholder: '请输入测试数据',
569 field: 'regularTestData',
570 default: '',
571 required: false,
572 clearable: true,
573 col: 'mb8',
574 block: true,
575 visible: true,
576 validateBtn: {
577 value: 'validate',
578 label: '验证',
579 click: () => {
580 let info = ruleContentFormRef.value.formInline;
581 if (info.ruleType == 1 && !info.builtInRuleCode) {
582 proxy.$ElMessage.error('请先选择内置规则');
583 return;
584 }
585 if (info.ruleType == 2 && !info.regularExpression) {
586 proxy.$ElMessage.error('请先输入规则表达式');
587 return;
588 }
589 if (!info.regularTestData) {
590 proxy.$ElMessage.error('请先输入测试数据');
591 return;
592 }
593 let exp = info.ruleType == 1 ? builtInRuleList.value.find(b => b.value == info.builtInRuleCode)?.remarks : info.regularExpression;
594 let result = exp && new RegExp(exp).test(info.regularTestData);
595 ruleContentFormItems.value.forEach(item => {
596 item.default = info[item.field];
597 if (item.field == 'regularTestData') {
598 item.validateMsg = result ? {
599 status: 'success',
600 msg: '符合识别规则'
601 } : {
602 status: 'error',
603 msg: '不符合识别规则'
604 }
605 }
606 });
607 }
608 },
609 validateMsg: <any>null
610 }, {
611 label: '命中率设置(%)',
612 type: 'input',
613 placeholder: '请输入1~100',
614 field: 'hitRate',
615 min: 1,
616 max: 100,
617 inputType: 'integerNumber',
618 default: null,
619 required: true,
620 block: true,
621 clearable: true,
622 visible: true,
623 width: '100px',
624 beforeMsg: '一列数据中的非空数据,大于等于',
625 afterMsg: ' %的数据符合以上识别条件,则认为命中该识别规则。'
626 }]);
627
628 /** 规则配置的内容识别内容表单规则配置 */
629 const ruleContentFormRules = ref({
630 builtInRuleCode: [required('请选择内置规则')],
631 regularExpression: [required('请输入正则表达式'), regexpRuleValidate()],
632 hitRate: [required(' ')]
633 });
634
635 const handleRuleContentSelectChange = (val, row, info) => {
636 if (row.field == 'ruleType') {
637 ruleContentFormItems.value[1].visible = val == 1;
638 ruleContentFormItems.value[2].visible = val == 2;
639 ruleContentFormItems.value.forEach(item => {
640 item.default = info[item.field];
641 })
642 }
643 }
644
645 const handleRuleContentInputChange = (val, row) => {
646 if (row.field == 'regularTestData') {
647 let item = ruleContentFormItems.value.at(-2);
648 item && (item.validateMsg = null);
649 }
650 }
651
652 onBeforeMount(() => {
653 toSearch({});
654 getParamsList({
655 dictType: "标签类型",
656 }).then((res: any) => {
657 if (res?.code == proxy.$passCode) {
658 labelTypeList.value = res.data || [];
659 let item = newCreateLabelFormItems.value.find(item => item.field == 'labelgroup');
660 item && (item.children[0].options = labelTypeList.value);
661 } else {
662 proxy.$ElMessage.error(res.msg);
663 }
664 });
665 getParamsList({
666 dictType: "内置规则",
667 }).then((res: any) => {
668 if (res?.code == proxy.$passCode) {
669 builtInRuleList.value = res.data || [];
670 ruleContentFormItems.value[1].options = builtInRuleList.value;
671 } else {
672 proxy.$ElMessage.error(res.msg);
673 }
674 })
675 })
676
677 </script>
678
679 <template>
680 <div class="container_wrap">
681 <div class="table_tool_wrap">
682 <!-- 头部搜索 -->
683 <TableTools :searchItems="searchItemList" :searchId="'data-label-search'" @search="toSearch" :init="false" />
684 <div class="tools_btns">
685 <el-button type="primary" @click="handleCreate">新建</el-button>
686 </div>
687 </div>
688 <div class="table_panel_wrap">
689 <!-- 右侧标签管理表格 -->
690 <Table :tableInfo="tableInfo" @tablePageChange="tablePageChange"
691 @tableSwitchBeforeChange="tableSwitchBeforeChange" />
692 </div>
693 <!-- 新建编辑标签对话框 -->
694 <Dialog_form ref="dialogLabelFormRef" :dialogConfigInfo="newCreateLabelDialogInfo" class="v-dialog-form">
695 <template v-slot:default>
696 <Tabs :tabs-info="tabsInfo" @tab-change="tabChange" style="margin-top: -20px;" />
697 <div v-show="tabsInfo.activeName == 'labelRuleField'">
698 <div>
699 <div class="dim-label">
700 <span class="front">精确匹配</span>
701 <el-icon>
702 <Warning />
703 </el-icon>
704 <span class="tip">
705 精确匹配使用英文","分隔每个规则
706 </span>
707 </div>
708 <div class="v-match">
709 <el-input v-model="matchChValue.value" maxlength="200" style="width: 272px;height:94px;" show-word-limit
710 type="textarea" class="no-resize" placeholder="请输入字段中文" />
711 <el-input v-model="matchEnValue.value" maxlength="200" style="width: 272px;height:94px;" show-word-limit
712 type="textarea" class="no-resize" placeholder="请输入字段英文" />
713 </div>
714 </div>
715 <div class="dim-label" style="margin-top: 16px;">
716 <span class="front">模糊匹配</span>
717 <el-icon>
718 <Warning />
719 </el-icon>
720 <span class="tip">
721 模糊匹配是或的关系,可配置多个模糊匹配规则
722 </span>
723 </div>
724 <!-- 渲染行 -->
725 <div v-for="(row, index) in formRows" :key="index" class="match-content-wrapper">
726 <div class="match-content">
727 <!-- 位置映射下拉框 -->
728 <el-select v-model="row.name" placeholder="请选择" class="v-select">
729 <el-option v-for="option in languageOptions" :key="option.value" :label="option.label"
730 :value="option.value" />
731 </el-select>
732 <el-select v-model="row.position" placeholder="请选择位置" class="v-select">
733 <el-option v-for="option in positionOptions" :key="option.value" :label="option.label"
734 :value="option.value" />
735 </el-select>
736 <el-input v-model="row.matchValue" class="v-input" placeholder="请输入匹配值" />
737
738 <!-- 删除按钮 -->
739 <el-button class="extra-icon" :icon="Delete" @click="deleteRow(index)" circle style="margin-left: 8px;" />
740 </div>
741 </div>
742
743 <!-- 新增按钮 -->
744 <div class="add-Icon" @click="addRow">
745 <el-icon class="icon-add" color="#4fa1a4" :size="30">
746 <CirclePlus />
747 </el-icon>
748 <span class="word-des">模糊匹配规则</span>
749 </div>
750 </div>
751 <div v-show="tabsInfo.activeName == 'labelRuleContent'">
752 <Form ref="ruleContentFormRef" :itemList="ruleContentFormItems" formId="rule-content-form"
753 :rules="ruleContentFormRules" @select-change="handleRuleContentSelectChange"
754 @inputChange="handleRuleContentInputChange" />
755 </div>
756 </template>
757 </Dialog_form>
758 </div>
759 </template>
760
761 <style scoped lang="scss">
762 .table_tool_wrap {
763 width: 100%;
764 height: 84px !important;
765 padding: 0 8px;
766
767 .tools_btns {
768 padding: 0px 0 0;
769 }
770 }
771
772 .table_panel_wrap {
773 width: 100%;
774 height: calc(100% - 84px);
775 padding: 0px 8px 0;
776 }
777
778 :deep(.v-dialog-form) {
779 .title-label {
780 font-size: 16px;
781 color: #212121;
782 line-height: 24px;
783 font-weight: 600;
784 }
785
786 .el-dialog__body {
787 height: 480px;
788 overflow: auto;
789 }
790
791 .dim-label {
792 height: 10px;
793 display: flex;
794 align-items: center;
795 margin-top: 6px;
796
797 .el-icon svg {
798 height: 16px;
799 width: 16px;
800 color: #999999;
801 }
802
803 .front {
804 margin-right: 16px;
805 }
806
807 .tip {
808 margin-left: 4px;
809 font-size: 12px;
810 color: #999999;
811 }
812 }
813
814 .match-content-wrapper {
815 width: 100%;
816
817 .match-content {
818 display: flex;
819 align-items: center;
820 margin-top: 8px;
821
822 .v-select {
823 margin-right: 8px;
824 width: 33%;
825 }
826
827 .v-input {
828 width: calc(33% - 50px);
829 }
830
831 .extra-icon {
832 transition: opacity 1s;
833 display: none;
834 }
835
836 &:hover {
837 .extra-icon {
838 display: flex;
839 }
840 }
841 }
842 }
843
844 .add-Icon {
845 display: flex;
846 align-items: center;
847 margin-top: 4px;
848 width: 50%;
849 margin-left: -4px;
850
851 .el-icon svg {
852 height: 19px;
853 width: 19px;
854 }
855
856 .word-des {
857 color: #4fa1a4
858 }
859 }
860
861 .v-match {
862 display: flex;
863 justify-content: space-between;
864 margin-top: 10px;
865 margin-bottom: 10px;
866 }
867
868 .no-resize {
869 height: 94px;
870
871 .el-textarea__inner {
872 min-height: 94px !important;
873 resize: none;
874 }
875 }
876 }
877 </style>
...\ No newline at end of file ...\ No newline at end of file
1 <route lang="yaml">
2 name: resultProcess
3 </route>
4
5 <script lang="ts" setup name="resultProcess">
6 import TableTools from "@/components/Tools/table_tools.vue";
7 import { commonPageConfig } from '@/components/PageNav/index';
8 import { TableColumnWidth } from "@/utils/enum";
9 import {
10 dataSourceTypeList,
11 getAnonTaskList,
12 deleteAnonTask,
13 } from '@/api/modules/dataAnonymization';
14 import { useValidator } from '@/hooks/useValidator';
15 import useDataAnonymizationStore from "@/store/modules/dataAnonymization";
16
17 const anonymizationStore = useDataAnonymizationStore();
18 const router = useRouter()
19 const { proxy } = getCurrentInstance() as any;
20 const { required } = useValidator();
21
22 const searchItemList = ref([{
23 type: "input",
24 label: "",
25 field: "taskName",
26 default: "",
27 placeholder: "数据集名称",
28 clearable: true,
29 }, {
30 type: "select",
31 label: "",
32 field: "dataSource",
33 default: null,
34 options: dataSourceTypeList,
35 placeholder: "数据来源",
36 clearable: true,
37 filterable: true,
38 }])
39
40 /** 分页及搜索传参信息配置。 */
41 const page = ref({
42 ...commonPageConfig,
43 taskName: '',
44 dataSource: null
45 });
46
47 const tableInfo = ref({
48 id: 'data-file-table',
49 fields: [
50 { label: "序号", type: "index", width: TableColumnWidth.INDEX, align: "center" },
51 { label: "数据集名称", field: "taskName", width: 160 },
52 {
53 label: "数据来源", field: "dataSource", width: 100, getName: (scope) => {
54 return scope.row.dataSource && dataSourceTypeList.find(f => f.value == scope.row.dataSource)?.label || '--';
55 }
56 },
57 { label: "任务状态", field: "sensitiveIdentifyTaskStatus", width: TableColumnWidth.STATE, align: 'center', type: "tag" },
58 { label: "导出时间", field: "exportTime", width: TableColumnWidth.DATETIME },
59 { label: "修改人", field: "updateUserName", width: TableColumnWidth.USERNAME },
60 { label: "修改时间", field: "updateTime", width: TableColumnWidth.DATETIME },
61 ],
62 data: [],
63 page: {
64 type: "normal",
65 rows: 0,
66 ...page.value,
67 },
68 loading: false,
69 actionInfo: {
70 label: "操作",
71 type: "btn",
72 width: 230,
73 fixed: 'right',
74 btns: (scope) => {
75 return [{
76 label: "编辑", value: "edit", click: (scope) => {
77 router.push({
78 name: 'anonTaskCreate',
79 query: {
80 guid: scope.row.guid,
81 taskName: scope.row.taskName
82 }
83 });
84 }
85 }, {
86 label: '查看报告', value: 'report', disabled: scope.row.status != 'Y', click: (scope) => {
87 router.push({
88 name: 'anonResultReportView',
89 query: {
90 guid: scope.row.guid,
91 execGuid: scope.row.lastExecGuid,
92 taskName: scope.row.taskName
93 }
94 });
95 }
96 }, {
97 label: '查看数据', value: 'view', disabled: scope.row.status != 'Y' || scope.row.handleType == '02', click: (scope) => {
98 router.push({
99 name: 'anonResultView',
100 query: {
101 guid: scope.row.guid,
102 execGuid: scope.row.lastExecGuid,
103 taskName: scope.row.taskName
104 }
105 });
106 }
107 }, {
108 label: "删除", value: "delete", click: (scope) => {
109 proxy.$openMessageBox("此操作将永久删除, 是否继续?", () => {
110 let guids = [scope.row.guid];
111 deleteAnonTask(guids).then((res: any) => {
112 if (res?.code == proxy.$passCode) {
113 page.value.curr = 1;
114 getTableData();
115 proxy.$ElMessage({
116 type: "success",
117 message: "删除成功",
118 });
119 } else {
120 proxy.$ElMessage({
121 type: "error",
122 message: res.msg,
123 });
124 }
125 });
126 })
127 }
128 }]
129 }
130 }
131 })
132
133 const toSearch = (val: any, clear: boolean = false) => {
134 if (clear) {
135 searchItemList.value.map((item) => (item.default = ""));
136 page.value.taskName = '';
137 page.value.dataSource = null;
138 } else {
139 page.value.taskName = val.taskName;
140 page.value.dataSource = val.dataSource;
141 }
142 getTableData();
143 };
144
145 const getTableData = () => {
146 tableInfo.value.loading = true
147 getAnonTaskList({
148 pageIndex: page.value.curr,
149 pageSize: page.value.limit,
150 taskName: page.value.taskName,
151 dataSource: page.value.dataSource
152 }).then((res: any) => {
153 if (res?.code == proxy.$passCode) {
154 const data = res.data || {};
155 tableInfo.value.data = data.records?.map(d => {
156 d.sensitiveIdentifyTaskStatus = d.status;
157 return d;
158 }) || []
159 tableInfo.value.page.limit = data.pageSize
160 tableInfo.value.page.curr = data.pageIndex
161 tableInfo.value.page.rows = data.totalRows
162 } else {
163 proxy.$ElMessage({
164 type: 'error',
165 message: res.msg,
166 })
167 }
168 tableInfo.value.loading = false
169 })
170 };
171
172 const tablePageChange = (info) => {
173 page.value.curr = Number(info.curr);
174 page.value.limit = Number(info.limit);
175 getTableData();
176 };
177
178 const handleCreate = () => {
179 router.push({
180 name: 'anonTaskCreate'
181 });
182 }
183
184 onBeforeMount(() => {
185 toSearch({});
186 anonymizationStore?.setIsAnonPageRefresh?.(false);
187 })
188
189 onActivated(() => {
190 if (anonymizationStore.isAnonPageRefresh) {//如果是首次加载,则不需要调用
191 page.value.curr = 1;
192 getTableData();
193 anonymizationStore.setIsAnonPageRefresh(false);
194 }
195 });
196
197 </script>
198
199 <template>
200 <div class="container_wrap">
201 <div class="table_tool_wrap">
202 <!-- 头部搜索 -->
203 <TableTools :searchItems="searchItemList" :searchId="'data-label-search'" @search="toSearch" :init="false" />
204 <div class="tools_btns">
205 <el-button type="primary" @click="handleCreate">新建</el-button>
206 </div>
207 </div>
208 <div class="table_panel_wrap">
209 <!-- 右侧标签管理表格 -->
210 <Table :tableInfo="tableInfo" @tablePageChange="tablePageChange" />
211 </div>
212 </div>
213 </template>
214
215 <style lang="scss" scoped>
216 .table_tool_wrap {
217 width: 100%;
218 height: 84px !important;
219 padding: 0 8px;
220
221 .tools_btns {
222 padding: 0px 0 0;
223 }
224 }
225
226 .table_panel_wrap {
227 width: 100%;
228 height: calc(100% - 84px);
229 padding: 0px 8px 0;
230 }
231 </style>
...\ No newline at end of file ...\ No newline at end of file
1 <route lang="yaml">
2 name: sensitiveIdentify
3 </route>
4
5 <script lang="ts" setup name="sensitiveIdentify">
6 import TableTools from "@/components/Tools/table_tools.vue";
7 import { commonPageConfig } from '@/components/PageNav/index';
8 import { TableColumnWidth } from "@/utils/enum";
9 import {
10 getSensitiveDataTaskList,
11 dataSourceTypeList,
12 deleteSensitiveDataTask,
13 saveSensitiveDataTask,
14 updateSensitiveDataTask,
15 getDatabase,
16 execSensitiveDataTask,
17 } from '@/api/modules/dataAnonymization';
18 import { useValidator } from '@/hooks/useValidator';
19
20 const router = useRouter()
21 const { proxy } = getCurrentInstance() as any;
22 const { required } = useValidator();
23
24 const searchItemList = ref([{
25 type: "input",
26 label: "",
27 field: "taskName",
28 default: "",
29 placeholder: "任务名称",
30 clearable: true,
31 }, {
32 type: "select",
33 label: "",
34 field: "dataSource",
35 default: null,
36 options: dataSourceTypeList,
37 placeholder: "数据来源",
38 clearable: true,
39 filterable: true,
40 }])
41
42 /** 分页及搜索传参信息配置。 */
43 const page = ref({
44 ...commonPageConfig,
45 taskName: '',
46 dataSource: null
47 });
48
49 const currTableData = ref();
50
51 const tableInfo = ref({
52 id: 'data-file-table',
53 fields: [
54 { label: "序号", type: "index", width: TableColumnWidth.INDEX, align: "center" },
55 { label: "任务名称", field: "taskName", width: 170 },
56 {
57 label: "数据来源", field: "dataSource", width: 100, getName: (scope) => {
58 return scope.row.dataSource && dataSourceTypeList.find(f => f.value == scope.row.dataSource)?.label || '--';
59 }
60 },
61 { label: "任务状态", field: "sensitiveIdentifyTaskStatus", width: TableColumnWidth.STATE, align: 'center', type: "tag" },
62 { label: "执行人", field: "execUserName", width: 100 },
63 { label: "执行时间", field: "execTime", width: 170 },
64 { label: "修改人", field: "updateUserName", width: 100 },
65 { label: "修改时间", field: "updateTime", width: 170 },
66 ],
67 data: [],
68 page: {
69 type: "normal",
70 rows: 0,
71 ...page.value,
72 },
73 loading: false,
74 actionInfo: {
75 label: "操作",
76 type: "btn",
77 width: 304,
78 fixed: 'right',
79 btns: (scope) => {
80 return [{
81 label: '敏感数据查看', value: 'view', disabled: scope.row.status != 'Y', click: (scope) => {
82 router.push({
83 name: 'sensitiveIdentifyConfig',
84 query: {
85 guid: scope.row.guid,
86 execGuid: scope.row.execGuid,
87 taskName: scope.row.taskName
88 }
89 });
90 }
91 }, {
92 label: '手动执行', value: 'execute', disabled: scope.row.status == 'R', click: (scope) => {
93 execSensitiveDataTask(scope.row.guid).then((res: any) => {
94 if (res?.code == proxy.$passCode) {
95 getTableData();
96 proxy.$ElMessage.success('该任务手动执行提交成功');
97 } else {
98 proxy.$ElMessage.error(res.msg);
99 }
100 })
101 }
102 }, {
103 label: "编辑", value: "edit", disabled: scope.row.status == 'R', click: (scope) => {
104 let row = scope.row;
105 currTableData.value = row;
106 newCreateTaskDialogInfo.value.visible = true;
107 newCreateTaskDialogInfo.value.title = '编辑数据敏感识别任务';
108 newCreateTaskDialogInfo.value.type = 'edit';
109 newCreateTaskFormItems.value[0].default = row.taskName;
110 newCreateTaskFormItems.value[1].default = row.dataSource;
111 newCreateTaskFormItems.value[2].default = row.dataSourceGuid || '';
112 newCreateTaskFormItems.value[2].visible = row.dataSource == 1;
113 newCreateTaskFormItems.value[3].default = row.filePath || [];
114 newCreateTaskFormItems.value[3].visible = row.dataSource == 2;
115 }
116 }, {
117 label: "删除", value: "delete", disabled: scope.row.status == 'R', click: (scope) => {
118 proxy.$openMessageBox("此操作将永久删除, 是否继续?", () => {
119 let guids = [scope.row.guid];
120 deleteSensitiveDataTask(guids).then((res: any) => {
121 if (res?.code == proxy.$passCode) {
122 getTableData();
123 proxy.$ElMessage({
124 type: "success",
125 message: "删除成功",
126 });
127 } else {
128 proxy.$ElMessage({
129 type: "error",
130 message: res.msg,
131 });
132 }
133 });
134 })
135 }
136 }, {
137 label: '日志', value: 'log', click: (scope) => {
138 router.push({
139 name: 'sensitiveIdentifyTaskExecLog',
140 query: {
141 guid: scope.row.guid,
142 name: scope.row.taskName,
143 }
144 });
145 }
146 }]
147 }
148 }
149 })
150
151 const toSearch = (val: any, clear: boolean = false) => {
152 if (clear) {
153 searchItemList.value.map((item) => (item.default = ""));
154 page.value.taskName = '';
155 page.value.dataSource = null;
156 } else {
157 page.value.taskName = val.taskName;
158 page.value.dataSource = val.dataSource;
159 }
160 getTableData();
161 };
162
163 const getTableData = () => {
164 tableInfo.value.loading = true
165 getSensitiveDataTaskList({
166 pageIndex: page.value.curr,
167 pageSize: page.value.limit,
168 taskName: page.value.taskName,
169 dataSource: page.value.dataSource
170 }).then((res: any) => {
171 if (res?.code == proxy.$passCode) {
172 const data = res.data || {};
173 tableInfo.value.data = data.records?.map(d => {
174 d.sensitiveIdentifyTaskStatus = d.status;
175 return d;
176 }) || []
177 tableInfo.value.page.limit = data.pageSize
178 tableInfo.value.page.curr = data.pageIndex
179 tableInfo.value.page.rows = data.totalRows
180 } else {
181 proxy.$ElMessage({
182 type: 'error',
183 message: res.msg,
184 })
185 }
186 tableInfo.value.loading = false
187 })
188 };
189
190 const tablePageChange = (info) => {
191 page.value.curr = Number(info.curr);
192 page.value.limit = Number(info.limit);
193 getTableData();
194 };
195
196 const dataSourceList = ref([]);
197
198 const newCreateTaskFormItems = ref([{
199 label: '任务名称',
200 type: 'input',
201 placeholder: '请输入',
202 field: 'taskName',
203 maxlength: 15,
204 default: '',
205 required: true,
206 filterable: true,
207 clearable: true,
208 visible: true,
209 },
210 {
211 label: '数据来源',
212 type: 'select',
213 placeholder: '请选择',
214 field: 'dataSource',
215 default: 1,
216 options: dataSourceTypeList,
217 props: {
218 label: "label",
219 value: "value",
220 },
221 required: true,
222 filterable: true,
223 clearable: true,
224 visible: true,
225 }, {
226 label: '数据源',
227 type: 'select',
228 placeholder: '请选择',
229 field: 'dataSourceGuid',
230 default: '',
231 options: dataSourceList.value,
232 props: {
233 label: 'databaseNameZh',
234 value: 'guid'
235 },
236 filterable: true,
237 visible: true,
238 required: true
239 }, {
240 label: '文件上传',
241 tip: '支持扩展名:xlsx、xls、csv,文件大小不超过10MB',
242 type: 'upload-file',
243 accept: '.xlsx, .xls, .csv',
244 limitSize: 10,
245 isExcel: true,
246 required: true,
247 default: <any>[],
248 block: true,
249 visible: false,
250 field: 'file',
251 }]);
252
253 const newCreateTaskFormRules = ref({
254 taskName: [required('请输入任务名称')],
255 dataSource: [required('请选择数据来源')],
256 dataSourceGuid: [required('请选择数据源')],
257 file: [{
258 validator: (rule: any, value: any, callback: any) => {
259 if (!value?.length) {
260 callback(new Error('请上传文件'))
261 } else {
262 callback();
263 }
264 }, trigger: 'change'
265 }]
266 });
267
268 const newCreateTaskDialogInfo = ref({
269 visible: false,
270 size: 550,
271 title: "添加数据敏感识别任务",
272 type: "",
273 formInfo: {
274 id: "label-form",
275 items: newCreateTaskFormItems.value,
276 rules: newCreateTaskFormRules.value,
277 },
278 submitBtnLoading: false,
279 btns: {
280 cancel: () => {
281 newCreateTaskDialogInfo.value.visible = false;
282 newCreateTaskDialogInfo.value.submitBtnLoading = false;
283 },
284 submit: (btn, info) => {
285 let params = Object.assign({}, info, {
286 filePath: info.file?.map(f => {
287 return {
288 name: f.name,
289 url: f.url
290 }
291 }) || []
292 });
293 delete params.file;
294 newCreateTaskDialogInfo.value.submitBtnLoading = true;
295 if (newCreateTaskDialogInfo.value.type == 'add') {
296 saveSensitiveDataTask(params).then((res: any) => {
297 if (res?.code == proxy.$passCode) {
298 proxy.$ElMessage.success('标签新建成功');
299 newCreateTaskDialogInfo.value.visible = false;
300 newCreateTaskDialogInfo.value.submitBtnLoading = false;
301 page.value.curr = 1;
302 getTableData();
303 } else {
304 newCreateTaskDialogInfo.value.submitBtnLoading = false;
305 proxy.$ElMessage.error(res.msg);
306 }
307 });
308 } else {
309 newCreateTaskDialogInfo.value.submitBtnLoading = true;
310 params.guid = currTableData.value.guid;
311 updateSensitiveDataTask(params).then((res: any) => {
312 if (res?.code == proxy.$passCode) {
313 proxy.$ElMessage.success('标签编辑成功');
314 newCreateTaskDialogInfo.value.visible = false;
315 newCreateTaskDialogInfo.value.submitBtnLoading = false;
316 getTableData();
317 } else {
318 newCreateTaskDialogInfo.value.submitBtnLoading = false;
319 proxy.$ElMessage.error(res.msg);
320 }
321 });
322 }
323 }
324 }
325 });
326
327 const handleTaskSelectChange = (val, row, item) => {
328 if (item.field == 'dataSource') {
329 newCreateTaskFormItems.value[0].default = row.taskName;
330 newCreateTaskFormItems.value[1].default = val;
331 newCreateTaskFormItems.value[2].default = row.dataSourceGuid || '';
332 newCreateTaskFormItems.value[2].visible = val == 1;
333 newCreateTaskFormItems.value[3].default = row.file || [];
334 newCreateTaskFormItems.value[3].visible = val == 2;
335 }
336 }
337
338 const handleCreate = () => {
339 newCreateTaskDialogInfo.value.visible = true;
340 newCreateTaskDialogInfo.value.title = '添加数据敏感识别任务';
341 newCreateTaskDialogInfo.value.type = 'add';
342 newCreateTaskFormItems.value[0].default = '';
343 newCreateTaskFormItems.value[1].default = 1;
344 newCreateTaskFormItems.value[2].default = '';
345 newCreateTaskFormItems.value[2].visible = true;
346 newCreateTaskFormItems.value[3].default = [];
347 newCreateTaskFormItems.value[3].visible = false;
348 }
349
350 onBeforeMount(() => {
351 toSearch({});
352 getDatabase({ connectStatus: 1 }).then((res: any) => {
353 if (res?.code == proxy.$passCode) {
354 dataSourceList.value = res.data || [];
355 newCreateTaskFormItems.value[2].options = dataSourceList.value;
356 } else {
357 proxy.$ElMessage({
358 type: "error",
359 message: res.msg,
360 });
361 }
362 })
363 })
364
365 </script>
366
367 <template>
368 <div class="container_wrap">
369 <div class="table_tool_wrap">
370 <!-- 头部搜索 -->
371 <TableTools :searchItems="searchItemList" :searchId="'data-label-search'" @search="toSearch" :init="false" />
372 <div class="tools_btns">
373 <el-button type="primary" @click="handleCreate">新建</el-button>
374 </div>
375 </div>
376 <div class="table_panel_wrap">
377 <!-- 右侧标签管理表格 -->
378 <Table :tableInfo="tableInfo" @tablePageChange="tablePageChange" />
379 </div>
380 <Dialog_form ref="dialogTaskFormRef" :dialogConfigInfo="newCreateTaskDialogInfo"
381 @formDialogSelectChange="handleTaskSelectChange"></Dialog_form>
382 </div>
383 </template>
384
385 <style lang="scss" scoped>
386 .table_tool_wrap {
387 width: 100%;
388 height: 84px !important;
389 padding: 0 8px;
390
391 .tools_btns {
392 padding: 0px 0 0;
393 }
394 }
395
396 .table_panel_wrap {
397 width: 100%;
398 height: calc(100% - 84px);
399 padding: 0px 8px 0;
400 }
401 </style>
...\ No newline at end of file ...\ No newline at end of file
1 <route lang="yaml">
2 name: sensitiveIdentifyConfig
3 </route>
4
5 <script lang="ts" setup name="sensitiveIdentifyConfig">
6 import {
7 getExecSensitiveTable,
8 getExecSensitiveFieldTable,
9 getExecSensitiveFieldColumnListByCondition,
10 getDataLabelList,
11 getStatisticsNum,
12 getParamsList,
13 updateSensitiveDataTaskFieldLabel,
14 batchUpdateSensitiveDataTaskFieldStatus,
15 confirmTaskStatus,
16 } from '@/api/modules/dataAnonymization';
17 import PageNav from "@/components/PageNav/index.vue";
18 import { commonPageConfig } from '@/components/PageNav/index';
19 import { ElMessageBox } from 'element-plus';
20 import { changeNum } from "@/utils/common";
21 import BtnPopover from "@/components/Popover/index.vue";
22 import useUserStore from "@/store/modules/user";
23
24 const route = useRoute();
25 const router = useRouter();
26 const { proxy } = getCurrentInstance() as any;
27 const userStore = useUserStore();
28 const fullPath = route.fullPath;
29 const isLook = ref(route.query.isLook == '1');
30
31 const fullScreenLoading = ref(false);
32
33 const treeInfo = ref({
34 id: "data-list-tree",
35 filter: true,
36 queryValue: "",
37 queryPlaceholder: "请输入关键字搜索",
38 props: {
39 label: "label",
40 value: "value",
41 isLeaf: "isLeaf",
42 children: 'tableList'
43 },
44 nodeKey: 'value',
45 expandedKey: <any>[],
46 currentNodeKey: '',
47 data: [],
48 expandOnNodeClick: false,
49 loading: false,
50 currentObj: <any>{}
51 });
52
53 const treeRef = ref();
54
55 const nodeClick = (data, node) => {
56 let exec = () => {
57 pageInfo.value.labelGuids = [];
58 pageInfo.value.labelTypeCodes = [];
59 pageInfo.value.confirmStatus = [];
60 treeInfo.value.currentNodeKey = data.value;
61 treeInfo.value.currentObj = data;
62 if (data.parent) {
63 pageInfo.value.databaseName = data.parent;
64 pageInfo.value.tableName = data.tableName;
65 } else {
66 pageInfo.value.databaseName = data.databaseName;
67 pageInfo.value.tableName = '';
68 }
69 pageInfo.value.curr = 1;
70 getSensitiveTableFieldData();
71 getCntSumInfo();
72 getSensitiveFieldLabelData();
73 }
74 if (checkTableSave()) {
75 ElMessageBox.confirm(
76 '存在未保存的数据,确定放弃修改吗?',
77 '提示',
78 {
79 confirmButtonText: '确定',
80 cancelButtonText: '取消',
81 type: 'warning',
82 }
83 ).then(() => {
84 exec();
85 }).catch(() => {
86 treeRef.value.setCurrentKey(treeInfo.value.currentObj.value);
87 })
88 } else {
89 exec();
90 }
91 }
92
93 const batchConfirm = () => {
94 if (!selectTableFieldRows.value.length) {
95 proxy.$ElMessage.error('请先勾选待确认的字段');
96 return;
97 }
98 if (checkTableSave()) {
99 ElMessageBox.confirm(
100 '存在未保存的数据,确定放弃修改吗?',
101 '提示',
102 {
103 confirmButtonText: '确定',
104 cancelButtonText: '取消',
105 type: 'warning',
106 }
107 ).then(() => {
108 batchUpdateSensitiveDataTaskFieldStatus(selectTableFieldRows.value.map(s => s.guid)).then((res: any) => {
109 if (res?.code == proxy.$passCode) {
110 proxy.$ElMessage.success('批量确认字段成功');
111 getSensitiveTableFieldData();
112 } else {
113 proxy.$ElMessage.error(res.msg);
114 }
115 })
116 })
117 } else {
118 batchUpdateSensitiveDataTaskFieldStatus(selectTableFieldRows.value.map(s => s.guid)).then((res: any) => {
119 if (res?.code == proxy.$passCode) {
120 proxy.$ElMessage.success('批量确认字段成功');
121 getSensitiveTableFieldData();
122 } else {
123 proxy.$ElMessage.error(res.msg);
124 }
125 })
126 }
127 }
128
129 /** 数量统计信息 */
130 const cntSumInfo: any = ref({});
131
132 const getCntSumInfo = () => {
133 getStatisticsNum({
134 execGuid: route.query.execGuid,
135 databaseName: pageInfo.value.databaseName,
136 tableName: pageInfo.value.tableName
137 }).then((res: any) => {
138 if (res?.code == proxy.$passCode) {
139 cntSumInfo.value = res.data || {};
140 } else {
141 proxy.$ElMessage.error(res.msg);
142 }
143 })
144 }
145
146 const dataSource = ref(null);
147
148 const getExecSensitiveTableData = () => {
149 treeInfo.value.loading = true;
150 getExecSensitiveTable(route.query.execGuid).then((res: any) => {
151 treeInfo.value.loading = false;
152 if (res?.code == proxy.$passCode) {
153 treeInfo.value.data = res.data?.map((d, index) => {
154 d.value = `${d.databaseName}-ds`; //解决文件名称和文件的sheet名称一样的树形选中问题
155 d.label = d.databaseChName;
156 if (index == 0) {
157 dataSource.value = d.dataSource;
158 }
159 d.tableList = d.tableList?.map(t => {
160 t.value = `${d.databaseName}-${t.tableName}`;
161 t.label = dataSource.value == 1 ? (t.tableChName || t.tableName) : t.tableChName;
162 t.parent = d.databaseName;
163 return t;
164 }) || []
165 return d;
166 }) || [];
167 let treeData: any = treeInfo.value.data || [];
168 if (treeData.length) {
169 treeInfo.value.expandedKey = [treeData[0].value];
170 treeInfo.value.currentNodeKey = treeData[0]?.value;
171 treeInfo.value.currentObj = treeData[0];
172 treeData[0].tableList?.[0] && nodeClick(treeData[0], {});
173 }
174 } else {
175 proxy.$ElMessage.error(res.msg);
176 }
177 })
178 }
179
180 /** 所有的标签列表供编辑下拉显示 */
181 const allDataLabelList: any = ref([]);
182
183 onBeforeMount(() => {
184 getExecSensitiveTableData();
185 getDataLabelList({
186 pageSize: -1,
187 bizState: 'Y'
188 }).then((res: any) => {
189 if (res?.code == proxy.$passCode) {
190 allDataLabelList.value = res.data?.records || [];
191 } else {
192 proxy.$ElMessage.error(res.msg);
193 }
194 })
195 getParamsList({
196 dictType: "标签类型",
197 }).then((res: any) => {
198 if (res?.code == proxy.$passCode) {
199 labelTypeList.value = res.data?.map(d => {
200 d.guid = d.value;
201 return d;
202 }) || [];
203 } else {
204 proxy.$ElMessage.error(res.msg);
205 }
206 });
207 })
208
209 /** ------------------------- 敏感数据识别字段列表操作 --------------------------------- */
210
211 const labelList = ref([]);
212
213 /** 标签类型的字典列表 */
214 const labelTypeList: any = ref([]);
215
216 /** 查询数据的标签类型列表 */
217 const labelTypeDataList: any = ref([]);
218
219 /** 分页设置 */
220 const pageInfo = ref({
221 ...commonPageConfig,
222 taskExecGuid: route.query.execGuid,
223 databaseName: '',
224 tableName: '',
225 rows: 0,
226 labelGuids: [], //列头筛选
227 labelTypeCodes: [],
228 confirmStatus: []
229 });
230
231 /** 敏感数据 */
232 const sensitiveTableData: any = ref([]);
233
234 const sensitiveTableDataLoading = ref(false);
235
236 const getSensitiveTableFieldData = () => {
237 sensitiveTableDataLoading.value = true;
238 let confirmStatus = pageInfo.value.confirmStatus || [];
239 getExecSensitiveFieldTable({
240 pageIndex: pageInfo.value.curr,
241 pageSize: pageInfo.value.limit,
242 taskExecGuid: pageInfo.value.taskExecGuid,
243 databaseName: pageInfo.value.databaseName,
244 tableName: pageInfo.value.tableName,
245 labelGuids: pageInfo.value.labelGuids,
246 labelTypeCodes: pageInfo.value.labelTypeCodes,
247 confirmStatus: confirmStatus?.length > 1 ? null : confirmStatus[0]
248 }).then((res: any) => {
249 sensitiveTableDataLoading.value = false;
250 if (res?.code == proxy.$passCode) {
251 const data = res.data || {};
252 sensitiveTableData.value = data.records || [];
253 pageInfo.value.limit = data.pageSize
254 pageInfo.value.curr = data.pageIndex
255 pageInfo.value.rows = data.totalRows
256 } else {
257 proxy.$ElMessage({
258 type: 'error',
259 message: res.msg,
260 })
261 }
262 })
263 }
264
265 /** 获取当前选中的左侧库表下对应的数据标签数组,做列头筛选时使用。 */
266 const getSensitiveFieldLabelData = (info?: {
267 filterLabel?: Boolean,
268 filterType?: Boolean,
269 filterStatus?: Boolean,
270 setLable?: Boolean,
271 setType?: Boolean,
272 setStatus?: Boolean
273 }) => {
274 getExecSensitiveFieldColumnListByCondition({
275 pageSize: -1,
276 taskExecGuid: pageInfo.value.taskExecGuid,
277 databaseName: pageInfo.value.databaseName,
278 tableName: pageInfo.value.tableName,
279 labelGuids: !info?.filterLabel ? [] : pageInfo.value.labelGuids,
280 labelTypeCodes: !info?.filterType ? [] : pageInfo.value.labelTypeCodes,
281 confirmStatus: !info?.filterStatus ? [] : pageInfo.value.confirmStatus
282 }).then((res: any) => {
283 if (res?.code == proxy.$passCode) {
284 let data = res.data || {};
285 (info?.setLable !== false || !pageInfo.value.labelGuids?.length) && (labelList.value = data['标签']?.map(d => {
286 d.guid = d.labelGuid;
287 if (!d.labelGuid && !d.labelName) {
288 d.guid = `${d.labelGuid}`;
289 d.labelName = '--';
290 }
291 return d;
292 }) || []);
293 let list = data['确认状态'];
294 if (info?.setStatus !== false || !pageInfo.value.confirmStatus?.length) {
295 confirmStatusList.value = list?.map(d => {
296 d.guid = d.confirmStatus;
297 d.label = d.confirmStatus == 'Y' ? '已确认' : '未确认';
298 return d;
299 }) || []
300 };
301 (info?.setType !== false || !pageInfo.value.labelTypeCodes?.length) && (labelTypeDataList.value = data['标签类型']?.map(d => {
302 d.guid = d.labelTypeCode;
303 if (!d.labelTypeCode) {
304 d.guid = `${d.labelTypeCode}`;
305 d.labelTypeName = '--';
306 }
307 return d;
308 }) || [])
309 } else {
310 proxy.$ElMessage({
311 type: 'error',
312 message: res.msg,
313 })
314 }
315 })
316 }
317
318 const popoverLabelListInfo = computed(() => {
319 return {
320 type: 'checkbox-list-btns',
321 data: labelList.value,
322 placeholder: '请输入关键字搜索',
323 scope: {
324 row: {}
325 },
326 placement: 'right-start',
327 props: {
328 value: 'guid',
329 label: 'labelName'
330 },
331 checked: pageInfo.value.labelGuids,
332 btn: {
333 value: 'filter'
334 }
335 }
336 });
337
338 const handleLabelPopoverClick = (scope) => {
339 if (checkTableSave()) {
340 ElMessageBox.confirm(
341 '存在未保存的数据,确定放弃修改吗?',
342 '提示',
343 {
344 confirmButtonText: '确定',
345 cancelButtonText: '取消',
346 type: 'warning',
347 }
348 ).then(() => {
349 pageInfo.value.labelGuids = scope.row.selectedData || [];
350 pageInfo.value.curr = 1;
351 getSensitiveTableFieldData();
352 getSensitiveFieldLabelData({
353 filterLabel: true,
354 filterStatus: true,
355 filterType: true,
356 setLable: false,
357 setStatus: true,
358 setType: true
359 });
360 })
361 } else {
362 pageInfo.value.labelGuids = scope.row.selectedData || [];
363 pageInfo.value.curr = 1;
364 getSensitiveTableFieldData();
365 getSensitiveFieldLabelData({
366 filterLabel: true,
367 filterStatus: true,
368 filterType: true,
369 setLable: false,
370 setStatus: true,
371 setType: true
372 });
373 }
374 }
375
376 const popoverLabelTypeListInfo = computed(() => {
377 return {
378 type: 'checkbox-list-btns',
379 data: labelTypeDataList.value,
380 placeholder: '请输入关键字搜索',
381 scope: {
382 row: {}
383 },
384 placement: 'right-start',
385 props: {
386 value: 'guid',
387 label: 'labelTypeName'
388 },
389 checked: pageInfo.value.labelTypeCodes,
390 btn: {
391 value: 'filter'
392 }
393 }
394 });
395
396 const handleLabelTypePopoverClick = (scope) => {
397 if (checkTableSave()) {
398 ElMessageBox.confirm(
399 '存在未保存的数据,确定放弃修改吗?',
400 '提示',
401 {
402 confirmButtonText: '确定',
403 cancelButtonText: '取消',
404 type: 'warning',
405 }
406 ).then(() => {
407 pageInfo.value.labelTypeCodes = scope.row.selectedData || [];
408 pageInfo.value.curr = 1;
409 getSensitiveTableFieldData();
410 getSensitiveFieldLabelData({
411 filterLabel: true,
412 filterStatus: true,
413 filterType: true,
414 setLable: true,
415 setStatus: true,
416 setType: false
417 });
418 })
419 } else {
420 pageInfo.value.labelTypeCodes = scope.row.selectedData || [];
421 pageInfo.value.curr = 1;
422 getSensitiveTableFieldData();
423 getSensitiveFieldLabelData({
424 filterLabel: true,
425 filterStatus: true,
426 filterType: true,
427 setLable: true,
428 setStatus: true,
429 setType: false
430 });
431 }
432 }
433
434 const confirmStatusList: any = ref([]);
435
436 const popoverStatusListInfo = computed(() => {
437 return {
438 type: 'checkbox-list-btns',
439 data: confirmStatusList.value,
440 placeholder: '请输入关键字搜索',
441 scope: {
442 row: {}
443 },
444 placement: 'right-start',
445 props: {
446 value: 'guid',
447 label: 'label'
448 },
449 checked: pageInfo.value.confirmStatus,
450 btn: {
451 value: 'filter'
452 }
453 }
454 });
455
456 const handleStatusPopoverClick = (scope) => {
457 if (checkTableSave()) {
458 ElMessageBox.confirm(
459 '存在未保存的数据,确定放弃修改吗?',
460 '提示',
461 {
462 confirmButtonText: '确定',
463 cancelButtonText: '取消',
464 type: 'warning',
465 }
466 ).then(() => {
467 pageInfo.value.confirmStatus = scope.row.selectedData || [];
468 pageInfo.value.curr = 1;
469 getSensitiveTableFieldData();
470 getSensitiveFieldLabelData({
471 filterLabel: true,
472 filterStatus: true,
473 filterType: true,
474 setLable: true,
475 setStatus: false,
476 setType: true
477 });
478 })
479 } else {
480 pageInfo.value.confirmStatus = scope.row.selectedData || [];
481 pageInfo.value.curr = 1;
482 getSensitiveTableFieldData();
483 getSensitiveFieldLabelData({
484 filterLabel: true,
485 filterStatus: true,
486 filterType: true,
487 setLable: true,
488 setStatus: false,
489 setType: true
490 });
491 }
492 }
493
494 /** 标签选择的值改变,标签类型跟着变化。 */
495 const handleSelectChange = (val, scope) => {
496 //暂时不处理,因为未保存会出现清空的场景,需要先存下之前的值。
497 let label = allDataLabelList.value.find(label => label.guid == val);
498 if (label) {
499 scope.row.labelTypeCode = label.labelTypeCode;
500 scope.row.labelTypeName = label.labelTypeName;
501 scope.row.labelName = label.labelName;
502 } else {
503 scope.row.labelTypeCode = null;
504 scope.row.labelTypeName = null;
505 scope.row.labelName = null;
506 }
507 }
508
509 const sensitiveTableSelectable = (row, index) => {
510 // return row.confirmStatus == 'N';
511 return true;
512 }
513
514 const selectTableFieldRows: any = ref([]);
515
516 /** 勾选字段标准选中变化。 */
517 const selectionDataChange = (val) => {
518 selectTableFieldRows.value = val;
519 };
520
521 const handleFieldClickEdit = (scope) => {
522 scope.row.isEdit = true;
523 }
524
525 const handleFieldClickSave = (scope) => {
526 let labelName = '';
527 let labelTypeCode = '';
528 if (scope.row.labelGuid) {
529 let label = allDataLabelList.value.find(label => label.guid == scope.row.labelGuid);
530 if (label) {
531 labelName = label.labelName;
532 labelTypeCode = label.labelTypeCode;
533 }
534 }
535 sensitiveTableDataLoading.value = true;
536 updateSensitiveDataTaskFieldLabel([{
537 guid: scope.row.guid,
538 labelGuid: scope.row.labelGuid,
539 labelName: labelName,
540 labelTypeCode: labelTypeCode
541 }]).then((res: any) => {
542 sensitiveTableDataLoading.value = false;
543 if (res?.code == proxy.$passCode) {
544 getSensitiveTableFieldData();
545 getSensitiveFieldLabelData({
546 filterLabel: true,
547 filterStatus: true,
548 filterType: true
549 });
550 getCntSumInfo();
551 proxy.$ElMessage.success('字段的标签修改成功');
552 } else {
553 proxy.$ElMessage.error(res.msg);
554 }
555 });
556 }
557
558 const checkTableSave = () => {
559 return sensitiveTableData.value.some(s => s.isEdit == true);
560 }
561
562 const pageChange = (info) => {
563 const toChange = checkTableSave()
564 const changeCont = () => {
565 pageInfo.value.curr = Number(info.curr)
566 pageInfo.value.limit = Number(info.limit)
567 getSensitiveTableFieldData();
568 }
569 if (toChange) {
570 ElMessageBox.confirm(
571 '存在未保存的数据,确定放弃修改吗?',
572 '提示',
573 {
574 confirmButtonText: '确定',
575 cancelButtonText: '取消',
576 type: 'warning',
577 }
578 ).then(() => {
579 changeCont();
580 })
581 } else {
582 changeCont();
583 }
584 }
585
586 const cancel = () => {
587 if (checkTableSave()) {
588 proxy.$openMessageBox("当前页面存在未保存的数据,确定放弃修改吗?", () => {
589 userStore.setTabbar(userStore.tabbar.filter((tab: any) => tab.fullPath !== fullPath));
590 router.push({
591 name: 'sensitiveIdentify'
592 });
593 }, () => {
594 proxy.$ElMessage.info("已取消");
595 });
596 } else {
597 userStore.setTabbar(userStore.tabbar.filter((tab: any) => tab.fullPath !== fullPath));
598 router.push({
599 name: 'sensitiveIdentify'
600 });
601 }
602 }
603
604 const pageConfirm = () => {
605 fullScreenLoading.value = true;
606 confirmTaskStatus(route.query.execGuid).then((res: any) => {
607 fullScreenLoading.value = false;
608 if (res?.code == proxy.$passCode) {
609 proxy.$ElMessage.success('确认成功');
610 userStore.setTabbar(userStore.tabbar.filter((tab: any) => tab.fullPath !== fullPath));
611 router.push({
612 name: 'sensitiveIdentify'
613 });
614 } else {
615 proxy.$ElMessage.error(res.msg);
616 }
617 })
618 }
619
620 const cntLabelMap = ref({
621 '01': 'directIdentifierNum',
622 '02': 'standardIdentifierNum',
623 '03': 'sensitiveNum',
624 '04': 'nonSensitiveNum'
625 })
626
627 </script>
628
629 <template>
630 <div class="container_wrap full flex" v-loading="fullScreenLoading">
631 <div class="aside_wrap" :style="{ height: isLook ? '100%' : 'calc(100% - 40px)' }">
632 <div class="aside_title">数据表列表</div>
633 <Tree ref="treeRef" :treeInfo="treeInfo" @nodeClick="nodeClick" />
634 </div>
635 <div class="main_wrap" :style="{ height: isLook ? '100%' : 'calc(100% - 40px)' }">
636 <div class="table_tool_wrap">
637 <div class="cnt-desc">{{'表总数:' + changeNum(cntSumInfo.tableNum || 0, 0) +
638 '张 字段总数:'
639 + changeNum(cntSumInfo.fieldNum || 0, 0) + '个 敏感表总数:' +
640 changeNum(cntSumInfo.sensitiveTableNum || 0, 0) + '张 敏感字段总数:' + changeNum(cntSumInfo.sensitiveFieldNum || 0,
641 0) +
642 '个,其中' + labelTypeList?.map(l => `${l.label}${changeNum(cntSumInfo[cntLabelMap[l.value]] || 0,
643 0)}个`).join(',') + ',非敏感' + changeNum(cntSumInfo['nonSensitiveNum'] || 0, 0) + '个'
644 }}</div>
645 <div class="tools_btns" v-if="!isLook">
646 <el-button type="primary" @click="batchConfirm">批量确认</el-button>
647 </div>
648 </div>
649 <div class="table_panel_wrap" :style="{ height: isLook ? 'calc(100% - 54px)' : 'calc(100% - 91px)' }">
650 <!-- 右侧表字段标签匹配管理表格 -->
651 <el-table ref="sensitiveTableRef" :data="sensitiveTableData" v-loading="sensitiveTableDataLoading"
652 :highlight-current-row="true" stripe border height="100%" row-key="guid"
653 @selection-change="selectionDataChange" tooltip-effect="light" :style="{
654 width: '100%',
655 'max-height': 'calc(100% - 16px)',
656 display: 'inline-block',
657 }">
658 <el-table-column type="selection" v-if="!isLook" :selectable="sensitiveTableSelectable" :width="32"
659 align="center" />
660 <el-table-column label="序号" type="index" width="56px" align="center" show-overflow-tooltip>
661 <template #default="scope">
662 <span>{{
663 pageInfo.curr !== undefined
664 ? (pageInfo.curr - 1) * pageInfo.limit + scope.$index + 1
665 : scope.$index + 1
666 }}</span>
667 </template>
668 </el-table-column>
669 <el-table-column label="字段中文名" prop="fieldChName" width="160" align="left" show-overflow-tooltip>
670 <template #default="scope">
671 <span>{{ scope.row.fieldChName || '--' }}</span>
672 </template>
673 </el-table-column>
674 <el-table-column label="字段英文名" prop="fieldName" width="150" align="left" show-overflow-tooltip>
675 <template #default="scope">
676 <span>{{ scope.row.fieldName || '--' }}</span>
677 </template>
678 </el-table-column>
679 <el-table-column label="标签" prop="labelGuid" class-name="filter-cell" width="140" align="left"
680 show-overflow-tooltip>
681 <template #header>
682 <span>标签</span>
683 <BtnPopover v-show="labelList.length" :popoverInfo="popoverLabelListInfo"
684 @popverBtnClick="handleLabelPopoverClick" />
685 </template>
686 <template #default="scope">
687 <el-select-v2 v-if="scope.row['isEdit']" v-model="scope.row['labelGuid']" filterable
688 popper-class="el-select-v2-popper" :options="allDataLabelList" placeholder="请选择" clearable
689 :props="{ value: 'guid', label: 'labelName' }" @change="(val) => handleSelectChange(val, scope)">
690 <template #default="{ item }">
691 <ellipsis-tooltip :content="item.labelName ?? ''" class-name="w100f"
692 :refName="'tooltipOver' + item.guid"></ellipsis-tooltip>
693 </template>
694 </el-select-v2>
695 <span v-else>{{ scope.row["labelName"] || '--' }}</span>
696 </template>
697 </el-table-column>
698 <el-table-column label="标签类型" prop="labelTypeName" class-name="filter-cell" width="130" align="left"
699 show-overflow-tooltip>
700 <template #header>
701 <span>标签类型</span>
702 <BtnPopover v-show="labelTypeDataList.length" :popoverInfo="popoverLabelTypeListInfo"
703 @popverBtnClick="handleLabelTypePopoverClick" />
704 </template>
705 <template #default="scope">
706 <span>{{ scope.row.labelTypeName || '--' }}</span>
707 </template>
708 </el-table-column>
709 <el-table-column label="所属表中文名" prop="tableChName" width="140" align="left" show-overflow-tooltip>
710 <template #default="scope">
711 <span>{{ scope.row.tableChName || '--' }}</span>
712 </template>
713 </el-table-column>
714 <el-table-column label="所属表英文名" prop="tableName" :width="dataSource == 1 ? 160 : 130" align="left" show-overflow-tooltip>
715 </el-table-column>
716 <el-table-column label="确认状态" prop="confirmStatus" class-name="filter-cell" width="120" align="center"
717 show-overflow-tooltip>
718 <template #header>
719 <span>确认状态</span>
720 <BtnPopover v-show="confirmStatusList.length" :popoverInfo="popoverStatusListInfo"
721 @popverBtnClick="handleStatusPopoverClick" />
722 </template>
723 <template #default="scope">
724 <el-tag v-if="scope.row.confirmStatus != null"
725 :type="scope.row.confirmStatus == 'Y' ? 'success' : 'warning'">{{
726 scope.row.confirmStatus == 'Y' ? '已确认' : '未确认'
727 }}</el-tag>
728 <span v-else>{{ '--' }}</span>
729 </template>
730 </el-table-column>
731 <el-table-column v-if="!isLook" label="操作" width="90px" align="left" fixed="right" show-overflow-tooltip>
732 <template #default="scope">
733 <span class="text_btn" v-if="!scope.row['isEdit']" @click="handleFieldClickEdit(scope)"
734 v-preReClick>编辑</span>
735 <span class="text_btn" v-else @click="handleFieldClickSave(scope)" v-preReClick>保存</span>
736 </template>
737 </el-table-column>
738 </el-table>
739 <PageNav :pageInfo="pageInfo" @pageChange="pageChange" />
740 </div>
741 </div>
742 <div class="bottom_tool_wrap">
743 <el-button @click="cancel" v-preReClick>取消</el-button>
744 <el-button type="primary" @click="pageConfirm" v-preReClick>确认</el-button>
745 </div>
746 </div>
747 </template>
748
749 <style lang="scss" scoped>
750 .container_wrap {
751 flex-wrap: wrap;
752
753 .aside_wrap {
754 width: 200px;
755 height: calc(100% - 40px);
756 }
757
758 .table_tool_wrap {
759 display: flex;
760 flex-direction: column;
761 min-height: 25px;
762
763 .cnt-desc {
764 line-height: 24px;
765 white-space: break-spaces;
766 margin-top: 4px;
767 }
768 }
769
770 .tools_btns {
771 padding-top: 0px;
772 }
773
774 .table_panel_wrap {
775 height: calc(100% - 91px);
776 }
777
778 .bottom_tool_wrap {
779 height: 40px;
780 width: 100%;
781 padding: 0 16px;
782 border-top: 1px solid #d9d9d9;
783 display: flex;
784 justify-content: center;
785 align-items: center;
786 }
787 }
788
789 .tree_panel {
790 height: 100%;
791 padding-top: 0;
792
793 :deep(.el-tree) {
794 margin: 0;
795 height: calc(100% - 68px);
796 overflow: hidden auto;
797 }
798 }
799
800 :deep(.el-table) {
801 & td.el-table__cell {
802 .cell {
803 padding: 0px 10px;
804 }
805
806 padding: 2px 0px;
807 height: 36px;
808 }
809 }
810
811 :deep(.filter-cell) {
812 .cell {
813 position: relative;
814
815 .el-icon.filter-icon {
816 position: absolute;
817 right: 6px;
818 top: 2px;
819 width: 18px;
820 height: 18px;
821
822 svg {
823 width: 18px;
824 height: 18px;
825 }
826 }
827 }
828 }
829 </style>
...\ No newline at end of file ...\ No newline at end of file
1 <route lang="yaml">
2 name: sensitiveIdentifyTaskExecLog
3 </route>
4
5 <script lang="ts" setup name="sensitiveIdentifyTaskExecLog">
6 import { ref } from "vue";
7 import { useRouter, useRoute } from "vue-router";
8 import Table from "@/components/Table/index.vue";
9 import { ElMessage } from "element-plus";
10 import { commonPageConfig } from '@/components/PageNav/index';
11 import {
12 getSensitiveDataTaskExecLog,
13 } from '@/api/modules/dataAnonymization';
14 import { TableColumnWidth } from "@/utils/enum";
15
16
17 const { proxy } = getCurrentInstance() as any;
18
19 const router = useRouter();
20 const route = useRoute();
21 const guid = route.query.guid;
22 const wordName = route.query.name
23
24 const page = ref({
25 ...commonPageConfig
26 });
27
28 const tableInfo = ref({
29 id: "word-log-table",
30 loading: false,
31 fields: [
32 { label: "序号", type: "index", width: TableColumnWidth.INDEX, align: "center" },
33 { label: "执行人", field: "createUserName", width: TableColumnWidth.USERNAME },
34 { label: "执行时间", field: "execTime", width: TableColumnWidth.DATETIME },
35 { label: "执行状态", field: "sensitiveIdentifyTaskStatus", width: TableColumnWidth.STATE, align: 'center', type: "tag" },
36 { label: "确认人", field: "confirmUserName", width: TableColumnWidth.USERNAME },
37 { label: "确认时间", field: "confirmTime", width: TableColumnWidth.DATETIME },
38 { label: "确认状态", field: "sensitiveIdentifyConfirmStatus", width: TableColumnWidth.STATE, align: 'center', type: "tag" },
39 ],
40 data: [],
41 page: {
42 type: "normal",
43 rows: 0,
44 ...page.value,
45 },
46 actionInfo: {
47 label: "操作",
48 type: "btn",
49 width: 100,
50 fixed: 'right',
51 btns: (scope) => {
52 return [{
53 label: "查看", value: "report", disabled: scope.row['status'] != 'Y', click: (scope) => {
54 router.push({
55 name: 'sensitiveIdentifyConfig',
56 query: {
57 guid: route.query.guid,
58 execGuid: scope.row.guid,
59 taskName: route.query.name,
60 isLook: '1',
61 }
62 });
63 }
64 }];
65 }
66 }
67 });
68
69 const getTableData = () => {
70 tableInfo.value.loading = true;
71 getSensitiveDataTaskExecLog({ pageIndex: page.value.curr, pageSize: page.value.limit, taskGuid: guid }).then((res: any) => {
72 tableInfo.value.loading = false;
73 if (res?.code == proxy.$passCode) {
74 const data = res.data || {}
75 tableInfo.value.data = data.records?.map(d => {
76 d.sensitiveIdentifyTaskStatus = d.status;
77 d.sensitiveIdentifyConfirmStatus = d.confirmStatus;
78 return d;
79 }) || []
80 tableInfo.value.page.limit = data.pageSize
81 tableInfo.value.page.curr = data.pageIndex
82 tableInfo.value.page.rows = data.totalRows
83 } else {
84 ElMessage.error(res.msg);
85 }
86 })
87 };
88
89 const tableBtnClick = (scope, btn) => {
90 const type = btn.value;
91 const row = scope.row;
92 if (type == 'reportView') {
93 router.push({
94 name: 'analysisReport',
95 query: {
96 planGuid: row.planGuid,
97 reportExecGuid: row.guid,
98 name: wordName
99 }
100 });
101 }
102 };
103
104 onActivated(() => {
105 getTableData();
106 });
107
108 </script>
109
110 <template>
111 <div class="container_wrap">
112 <div class="table_panel_wrap">
113 <Table :tableInfo="tableInfo" @tableBtnClick="tableBtnClick" />
114 </div>
115 </div>
116 </template>
117
118 <style lang="scss" scoped>
119 .container_wrap {
120 padding: 0;
121
122 .table_panel_wrap {
123 height: 100%;
124 padding: 16px 16px 0;
125 }
126 }
127 </style>
...\ No newline at end of file ...\ No newline at end of file
...@@ -1392,7 +1392,7 @@ onActivated(() => { ...@@ -1392,7 +1392,7 @@ onActivated(() => {
1392 <ContentWrap v-show="!currentStep && detailType == 'sign' || currentStep == 3" id="sign-info" title="合约签署" 1392 <ContentWrap v-show="!currentStep && detailType == 'sign' || currentStep == 3" id="sign-info" title="合约签署"
1393 expandSwicth style="margin-top: 15px" :isExpand="expandSign" @expand="(v) => (expandSign = v)" description=""> 1393 expandSwicth style="margin-top: 15px" :isExpand="expandSign" @expand="(v) => (expandSign = v)" description="">
1394 <template v-for="(item, index) in signDetailInfo"> 1394 <template v-for="(item, index) in signDetailInfo">
1395 <div :class="{ 'h-title': true, 'mt6': index > 0 }">{{ item.executionerTypeName }}</div> 1395 <div :class="{ 'h-title': true, 'mt6': <number>index > 0 }">{{ item.executionerTypeName }}</div>
1396 <div class="list_panel"> 1396 <div class="list_panel">
1397 <div class="list_item wrap"> 1397 <div class="list_item wrap">
1398 <span class="item_label">签署主体标识</span> 1398 <span class="item_label">签署主体标识</span>
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!