diff --git a/backend/src/main/java/io/dataease/auth/service/impl/ShiroServiceImpl.java b/backend/src/main/java/io/dataease/auth/service/impl/ShiroServiceImpl.java index 90d1f55baa..6d226ef715 100644 --- a/backend/src/main/java/io/dataease/auth/service/impl/ShiroServiceImpl.java +++ b/backend/src/main/java/io/dataease/auth/service/impl/ShiroServiceImpl.java @@ -54,7 +54,6 @@ public class ShiroServiceImpl implements ShiroService { // 验证链接 filterChainDefinitionMap.put("/api/link/validate**", ANON); filterChainDefinitionMap.put("/api/map/areaEntitys/**", ANON); - filterChainDefinitionMap.put("/dataset/field/fieldValues/**", ANON); filterChainDefinitionMap.put("/linkJump/queryPanelJumpInfo/**", ANON); filterChainDefinitionMap.put("/linkJump/queryTargetPanelJumpInfo", ANON); @@ -98,6 +97,7 @@ public class ShiroServiceImpl implements ShiroService { filterChainDefinitionMap.put("/api/link/viewDetail/**", "link"); filterChainDefinitionMap.put("/panel/group/exportDetails", ANON); filterChainDefinitionMap.put("/dataset/field/linkMultFieldValues", "link"); + filterChainDefinitionMap.put("/dataset/field/linkMappingFieldValues", "link"); filterChainDefinitionMap.put("/**", "authc"); diff --git a/backend/src/main/java/io/dataease/commons/model/BaseTreeNode.java b/backend/src/main/java/io/dataease/commons/model/BaseTreeNode.java new file mode 100644 index 0000000000..f08e4024bb --- /dev/null +++ b/backend/src/main/java/io/dataease/commons/model/BaseTreeNode.java @@ -0,0 +1,28 @@ +package io.dataease.commons.model; + +import io.dataease.plugins.common.model.ITreeBase; +import lombok.Data; + +import java.util.List; + +@Data +public class BaseTreeNode implements ITreeBase { + + private String id; + + private String pid; + + private String text; + + private String nodeType; + + private List children; + + public BaseTreeNode(String id, String pid, String text, String nodeType) { + this.id = id; + this.pid = pid; + this.text = text; + this.nodeType = nodeType; + } + +} diff --git a/backend/src/main/java/io/dataease/commons/utils/TreeUtils.java b/backend/src/main/java/io/dataease/commons/utils/TreeUtils.java index 7447487ae3..38ed95c37b 100644 --- a/backend/src/main/java/io/dataease/commons/utils/TreeUtils.java +++ b/backend/src/main/java/io/dataease/commons/utils/TreeUtils.java @@ -1,6 +1,7 @@ package io.dataease.commons.utils; import io.dataease.plugins.common.model.ITreeBase; +import org.apache.commons.lang3.StringUtils; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -14,6 +15,9 @@ import java.util.stream.Collectors; */ public class TreeUtils{ + public final static String DEFAULT_ROOT = "root"; + public final static String SEPARATOR = "-de-"; + /** * Description: rootPid 是根节点PID */ @@ -53,4 +57,41 @@ public class TreeUtils{ return mergeTree(tree,"0"); } + + public static List mergeDuplicateTree(List tree, String ... rootPid) { + Assert.notNull(rootPid, "Root Pid cannot be null"); + if(CollectionUtils.isEmpty(tree)){ + return null; + } + List result = new ArrayList<>(); + // 构建id-节点map映射 + Map treePidMap = tree.stream().collect(Collectors.toMap(node -> node.getNodeType(), t -> t)); + tree.stream().filter(item -> StringUtils.isNotBlank(item.getId())).forEach(node -> { + + String nodeType = node.getNodeType(); + String[] links = nodeType.split(SEPARATOR); + int length = links.length; + int level = Integer.parseInt(links[length - 1]); + // 判断根节点 + if (Arrays.asList(rootPid).contains(node.getPid()) && 0 == level) { + result.add(node); + } else { + //找到父元素 + String[] pLinks = new String[level]; + System.arraycopy(links, 0, pLinks, 0, level); + String parentType = Arrays.stream(pLinks).collect(Collectors.joining(SEPARATOR)) + TreeUtils.SEPARATOR + (level-1); + T parentNode = treePidMap.get(parentType); + if(parentNode==null){ + // 可能出现 rootPid 更高的节点 这个操作相当于截断 + return; + } + if (parentNode.getChildren() == null) { + parentNode.setChildren(new ArrayList()); + } + parentNode.getChildren().add(node); + } + }); + return result; + } + } diff --git a/backend/src/main/java/io/dataease/controller/dataset/DataSetTableFieldController.java b/backend/src/main/java/io/dataease/controller/dataset/DataSetTableFieldController.java index 4654d270dd..dcf6bc6c74 100644 --- a/backend/src/main/java/io/dataease/controller/dataset/DataSetTableFieldController.java +++ b/backend/src/main/java/io/dataease/controller/dataset/DataSetTableFieldController.java @@ -177,6 +177,23 @@ public class DataSetTableFieldController { return list; } + @ApiIgnore + @PostMapping("linkMappingFieldValues") + public List linkMappingFieldValues(@RequestBody MultFieldValuesRequest multFieldValuesRequest) throws Exception { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + String linkToken = request.getHeader(F2CLinkFilter.LINK_TOKEN_KEY); + DecodedJWT jwt = JWT.decode(linkToken); + Long userId = jwt.getClaim("userId").asLong(); + multFieldValuesRequest.setUserId(userId); + return dataSetFieldService.fieldValues(multFieldValuesRequest.getFieldIds(), multFieldValuesRequest.getUserId(), true, true); + } + + @ApiIgnore + @PostMapping("mappingFieldValues") + public List mappingFieldValues(@RequestBody MultFieldValuesRequest multFieldValuesRequest) throws Exception { + return dataSetFieldService.fieldValues(multFieldValuesRequest.getFieldIds(), multFieldValuesRequest.getUserId(), true, true); + } + @ApiIgnore @PostMapping("multFieldValuesForPermissions") public List multFieldValuesForPermissions(@RequestBody MultFieldValuesRequest multFieldValuesRequest) throws Exception { diff --git a/backend/src/main/java/io/dataease/service/dataset/DataSetFieldService.java b/backend/src/main/java/io/dataease/service/dataset/DataSetFieldService.java index 1f59651366..4e5e59449b 100644 --- a/backend/src/main/java/io/dataease/service/dataset/DataSetFieldService.java +++ b/backend/src/main/java/io/dataease/service/dataset/DataSetFieldService.java @@ -6,4 +6,6 @@ import java.util.List; public interface DataSetFieldService { List fieldValues(String fieldId, Long userId, Boolean userPermissions) throws Exception; + + List fieldValues(List fieldIds, Long userId, Boolean userPermissions, Boolean needMapping) throws Exception; } diff --git a/backend/src/main/java/io/dataease/service/dataset/impl/direct/DirectFieldService.java b/backend/src/main/java/io/dataease/service/dataset/impl/direct/DirectFieldService.java index d3a9e26a07..b0044def58 100644 --- a/backend/src/main/java/io/dataease/service/dataset/impl/direct/DirectFieldService.java +++ b/backend/src/main/java/io/dataease/service/dataset/impl/direct/DirectFieldService.java @@ -1,6 +1,8 @@ package io.dataease.service.dataset.impl.direct; import com.google.gson.Gson; +import io.dataease.commons.model.BaseTreeNode; +import io.dataease.commons.utils.TreeUtils; import io.dataease.plugins.common.base.domain.DatasetTable; import io.dataease.plugins.common.base.domain.DatasetTableField; import io.dataease.plugins.common.base.domain.Datasource; @@ -22,9 +24,7 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import javax.annotation.Resource; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; @@ -45,6 +45,14 @@ public class DirectFieldService implements DataSetFieldService { @Override public List fieldValues(String fieldId, Long userId, Boolean userPermissions) throws Exception { + List filedIds = new ArrayList<>(); + filedIds.add(fieldId); + return fieldValues(filedIds, userId, userPermissions, false); + } + + @Override + public List fieldValues(List fieldIds, Long userId, Boolean userPermissions, Boolean needMapping) throws Exception { + String fieldId = fieldIds.get(0); DatasetTableField field = dataSetTableFieldsService.selectByPrimaryKey(fieldId); if (field == null || StringUtils.isEmpty(field.getTableId())) return null; @@ -54,15 +62,23 @@ public class DirectFieldService implements DataSetFieldService { DatasetTableField datasetTableField = DatasetTableField.builder().tableId(field.getTableId()).checked(Boolean.TRUE).build(); List fields = dataSetTableFieldsService.list(datasetTableField); + List permissionFields = fields; List customFilter = new ArrayList<>(); if(userPermissions){ //列权限 List desensitizationList = new ArrayList<>(); fields = permissionService.filterColumnPermissons(fields, desensitizationList, datasetTable.getId(), userId); - //禁用的 - if(!fields.stream().map(DatasetTableField::getId).collect(Collectors.toList()).contains(fieldId)){ + + + permissionFields = fields.stream().filter(node -> fieldIds.stream().anyMatch(item -> StringUtils.equals(node.getId(), item))).collect(Collectors.toList()); + + if (CollectionUtils.isEmpty(permissionFields)) { return new ArrayList<>(); } + //禁用的 + /*if(!fields.stream().map(DatasetTableField::getId).collect(Collectors.toList()).contains(fieldId)){ + return new ArrayList<>(); + }*/ if (CollectionUtils.isNotEmpty(desensitizationList) && desensitizationList.contains(field.getDataeaseName())) { List results = new ArrayList<>(); results.add(ColumnPermissionConstants.Desensitization_desc); @@ -87,18 +103,18 @@ public class DirectFieldService implements DataSetFieldService { QueryProvider qp = ProviderFactory.getQueryProvider(ds.getType()); if (StringUtils.equalsIgnoreCase(datasetTable.getType(), "db")) { datasourceRequest.setTable(dataTableInfoDTO.getTable()); - datasourceRequest.setQuery(qp.createQuerySQL(dataTableInfoDTO.getTable(), Collections.singletonList(field), true, ds, customFilter)); + datasourceRequest.setQuery(qp.createQuerySQL(dataTableInfoDTO.getTable(), permissionFields, true, ds, customFilter)); } else if (StringUtils.equalsIgnoreCase(datasetTable.getType(), "sql")) { - datasourceRequest.setQuery(qp.createQuerySQLAsTmp(dataTableInfoDTO.getSql(), Collections.singletonList(field), true, customFilter)); + datasourceRequest.setQuery(qp.createQuerySQLAsTmp(dataTableInfoDTO.getSql(), permissionFields, true, customFilter)); } else if (StringUtils.equalsIgnoreCase(datasetTable.getType(), "custom")) { DataTableInfoDTO dt = new Gson().fromJson(datasetTable.getInfo(), DataTableInfoDTO.class); List listUnion = dataSetTableUnionService.listByTableId(dt.getList().get(0).getTableId()); String sql = dataSetTableService.getCustomSQLDatasource(dt, listUnion, ds); - datasourceRequest.setQuery(qp.createQuerySQLAsTmp(sql, Collections.singletonList(field), true, customFilter)); + datasourceRequest.setQuery(qp.createQuerySQLAsTmp(sql, permissionFields, true, customFilter)); } else if (StringUtils.equalsIgnoreCase(datasetTable.getType(), "union")) { DataTableInfoDTO dt = new Gson().fromJson(datasetTable.getInfo(), DataTableInfoDTO.class); String sql = (String) dataSetTableService.getUnionSQLDatasource(dt, ds).get("sql"); - datasourceRequest.setQuery(qp.createQuerySQLAsTmp(sql, Collections.singletonList(field), true, customFilter)); + datasourceRequest.setQuery(qp.createQuerySQLAsTmp(sql, permissionFields, true, customFilter)); } } else if (datasetTable.getMode() == 1) {// 抽取 // 连接doris,构建doris数据源查询 @@ -109,11 +125,39 @@ public class DirectFieldService implements DataSetFieldService { String tableName = "ds_" + datasetTable.getId().replaceAll("-", "_"); datasourceRequest.setTable(tableName); QueryProvider qp = ProviderFactory.getQueryProvider(ds.getType()); - datasourceRequest.setQuery(qp.createQuerySQL(tableName, Collections.singletonList(field), true, null, customFilter)); + datasourceRequest.setQuery(qp.createQuerySQL(tableName, permissionFields, true, null, customFilter)); } List rows = datasourceProvider.getData(datasourceRequest); - List results = rows.stream().map(row -> row[0]).distinct().collect(Collectors.toList()); - return results; + if (!needMapping) { + List results = rows.stream().map(row -> row[0]).distinct().collect(Collectors.toList()); + return results; + } + Set pkSet = new HashSet<>(); + + List treeNodes = rows.stream().map(row -> buildTreeNode(row, pkSet)).flatMap(Collection::stream).collect(Collectors.toList()); + List tree = TreeUtils.mergeDuplicateTree(treeNodes, TreeUtils.DEFAULT_ROOT); + return tree; + } + + private List buildTreeNode(String [] row, Set pkSet) { + List nodes = new ArrayList<>(); + List parentPkList = new ArrayList<>(); + for (int i = 0; i < row.length; i++) { + String text = row[i]; + + parentPkList.add(text); + String val = parentPkList.stream().collect(Collectors.joining(TreeUtils.SEPARATOR)); + String parentVal = i == 0 ? TreeUtils.DEFAULT_ROOT : row[i - 1]; + String pk = parentPkList.stream().collect(Collectors.joining(TreeUtils.SEPARATOR)); + if (pkSet.contains(pk)) continue; + pkSet.add(pk); + BaseTreeNode node = new BaseTreeNode(val, parentVal, text, pk + TreeUtils.SEPARATOR + i); + nodes.add(node); + } + return nodes; + + } + } diff --git a/frontend/src/api/dataset/dataset.js b/frontend/src/api/dataset/dataset.js index 50ed906bbc..0a18529a89 100644 --- a/frontend/src/api/dataset/dataset.js +++ b/frontend/src/api/dataset/dataset.js @@ -146,11 +146,21 @@ export function post(url, data, showLoading = true, timeout = 60000) { }) } -export function fieldValues(fieldId) { +export function mappingFieldValues(data) { return request({ - url: '/dataset/field/fieldValues/' + fieldId, + url: '/dataset/field/mappingFieldValues', method: 'post', - loading: true + loading: true, + data + }) +} + +export function linkMappingFieldValues(data) { + return request({ + url: '/dataset/field/linkMappingFieldValues', + method: 'post', + loading: true, + data }) } diff --git a/frontend/src/components/ElTreeSelect/dom.js b/frontend/src/components/ElTreeSelect/dom.js new file mode 100644 index 0000000000..b3cbc47776 --- /dev/null +++ b/frontend/src/components/ElTreeSelect/dom.js @@ -0,0 +1,37 @@ +/* + * @moduleName: + * @Author: dawdler + * @LastModifiedBy: dawdler + * @Date: 2019-03-22 14:47:35 + * @LastEditTime: 2019-03-22 16:31:38 + */ +export const on = (function() { + if (document.addEventListener) { + return function(element, event, handler) { + if (element && event && handler) { + element.addEventListener(event, handler, false) + } + } + } else { + return function(element, event, handler) { + if (element && event && handler) { + element.attachEvent('on' + event, handler) + } + } + } +})() +export const off = (function() { + if (document.removeEventListener) { + return function(element, event, handler) { + if (element && event) { + element.removeEventListener(event, handler, false) + } + } + } else { + return function(element, event, handler) { + if (element && event) { + element.detachEvent('on' + event, handler) + } + } + } +})() diff --git a/frontend/src/components/ElTreeSelect/index.vue b/frontend/src/components/ElTreeSelect/index.vue new file mode 100644 index 0000000000..7dea1fd05c --- /dev/null +++ b/frontend/src/components/ElTreeSelect/index.vue @@ -0,0 +1,594 @@ + + + + + diff --git a/frontend/src/components/ElTreeSelect/utils.js b/frontend/src/components/ElTreeSelect/utils.js new file mode 100644 index 0000000000..6a2cde783e --- /dev/null +++ b/frontend/src/components/ElTreeSelect/utils.js @@ -0,0 +1,78 @@ +/* + * @moduleName:通用工具类 + * @Author: dawdler + * @Date: 2019-01-09 15:30:18 + * @LastModifiedBy: dawdler + * @LastEditTime: 2020-12-26 14:06:09 + */ +export default { + getTreeData, + each +} +/* +each(arr, (item, children) => { + item.value = xx; + // 该item 包含children,因此直接赋值,不需要单独处理children里面的值 +}); + * [_each description] 倒查、展平、数据回调返回回当前一条数据和子集 + * @param {[Array]} data [description] + * @param {Function} callback [description] + * @param {String} childName[description] + * @return {[Array]} [description] + * 默认使用副本,在callback处理数据,如果不使用副本,那么需要重新对treeData赋值 + treeData = each(treeData, (item, children) => { + item.value = xx; + }); + */ +export function each(data, callback, childName = 'children') { + let current + let children + for (let i = 0, len = data.length; i < len; i++) { + current = data[i] + children = [] + if (current[childName] && current[childName].length > 0) { + children = current[childName] + } + callback && callback(current, children) + if (children.length > 0) { + each(children, callback, childName) + } + } +} + +/** + * @Author yihuang", + * @param data 数据 + * @param id 要比对的名称 + * @param val 要比对的值 + * @param name 要返回的名称 + * @param children 子集名称 + * @param isRow 是否返回这一行的数据 + * @注 迭代判断多层 + * //======================= + * 返回这一条数据的中文名 + * let name=utils.getTreeData(arr, 'flowId', item.decategoryId, 'name'); + * //======================= + * 返回所有匹配的数据 + * let arr=utils.getTreeData(arr, 'flowId', item.decategoryId, 'name','children',true); + */ +export function getTreeData(data, id = 'id', val = '', name = 'name', children = 'children', isRow = false) { + const arr = [] + each( + data, + item => { + if (item[id] === val) { + arr.push(item) + } + }, + children + ) + return arr.length > 0 ? (isRow ? arr : arr[0][name]) : null +} +export function guid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = (Math.random() * 16) | 0 + const v = c === 'x' ? r : (r & 0x3) | 0x8 + return v.toString(16) + }) +} diff --git a/frontend/src/components/widget/DeWidget/DeSelectTree.vue b/frontend/src/components/widget/DeWidget/DeSelectTree.vue new file mode 100644 index 0000000000..30bbf4edc9 --- /dev/null +++ b/frontend/src/components/widget/DeWidget/DeSelectTree.vue @@ -0,0 +1,331 @@ + + + + + diff --git a/frontend/src/components/widget/serviceImpl/TextSelectTreeServiceImpl.js b/frontend/src/components/widget/serviceImpl/TextSelectTreeServiceImpl.js new file mode 100644 index 0000000000..9180883558 --- /dev/null +++ b/frontend/src/components/widget/serviceImpl/TextSelectTreeServiceImpl.js @@ -0,0 +1,108 @@ + +import { WidgetService } from '../service/WidgetService' +const leftPanel = { + icon: 'iconfont icon-xialashu', + label: 'detextselectTree.label', + defaultClass: 'text-filter' +} + +const dialogPanel = { + options: { + attrs: { + multiple: false, + placeholder: 'detextselectTree.placeholder', + viewIds: [], + datas: [], + key: 'id', + label: 'text', + value: 'id', + fieldId: '', + dragItems: [] + }, + value: '', + manualModify: false + }, + defaultClass: 'text-filter', + component: 'de-select-tree', + miniSizex: 1, + miniSizey: 1 +} +const drawPanel = { + type: 'custom', + style: { + width: 300, + height: 90, + fontSize: 14, + fontWeight: 500, + lineHeight: '', + letterSpacing: 0, + textAlign: '', + color: '', + hPosition: 'left', + vPosition: 'center' + }, + component: 'de-select-tree' +} + +class TextSelectTreeServiceImpl extends WidgetService { + constructor(options = {}) { + Object.assign(options, { name: 'textSelectTreeWidget' }) + super(options) + this.filterDialog = true + this.showSwitch = true + } + + initLeftPanel() { + const value = JSON.parse(JSON.stringify(leftPanel)) + return value + } + + initFilterDialog() { + const value = JSON.parse(JSON.stringify(dialogPanel)) + return value + } + + initDrawPanel() { + const value = JSON.parse(JSON.stringify(drawPanel)) + return value + } + + filterFieldMethod(fields) { + return fields.filter(field => { + return field['deType'] === 0 + }) + } + + optionDatas(datas) { + if (!datas) return null + return datas.filter(item => !!item).map(item => { + return { + id: item, + text: item + } + }) + } + + getParam(element) { + const value = this.fillValueDerfault(element) + const param = { + component: element, + value: !value ? [] : Array.isArray(value) ? value : value.toString().split(','), + operator: element.options.attrs.multiple ? 'in' : 'eq' + } + return param + } + + fillValueDerfault(element) { + const defaultV = element.options.value === null ? '' : element.options.value.toString() + if (element.options.attrs.multiple) { + if (defaultV === null || typeof defaultV === 'undefined' || defaultV === '' || defaultV === '[object Object]') return [] + return defaultV.split(',') + } else { + if (defaultV === null || typeof defaultV === 'undefined' || defaultV === '' || defaultV === '[object Object]') return null + return defaultV.split(',')[0] + } + } +} +const textSelectTreeServiceImpl = new TextSelectTreeServiceImpl() +export default textSelectTreeServiceImpl diff --git a/frontend/src/lang/en.js b/frontend/src/lang/en.js index 48f589bf53..8f7a34fa0c 100644 --- a/frontend/src/lang/en.js +++ b/frontend/src/lang/en.js @@ -1885,6 +1885,10 @@ export default { label: 'Text selector', placeholder: 'Please select' }, + detextselectTree: { + label: 'Tree selector', + placeholder: 'Please select' + }, detextgridselect: { label: 'Text list', placeholder: 'Please select' diff --git a/frontend/src/lang/tw.js b/frontend/src/lang/tw.js index b93f7f1f10..50388ce030 100644 --- a/frontend/src/lang/tw.js +++ b/frontend/src/lang/tw.js @@ -1897,6 +1897,10 @@ export default { label: '文本下拉', placeholder: '請選擇' }, + detextselectTree: { + label: '下拉树', + placeholder: '請選擇' + }, detextgridselect: { label: '文本列錶', placeholder: '請選擇' diff --git a/frontend/src/lang/zh.js b/frontend/src/lang/zh.js index 29e7e200fe..fd7a7ac7ca 100644 --- a/frontend/src/lang/zh.js +++ b/frontend/src/lang/zh.js @@ -1331,7 +1331,7 @@ export default { sql_ds_union_error: '直连模式下SQL数据集,不支持关联', api_data: 'API 数据集' }, - driver:{ + driver: { driver: '驱动', please_choose_driver: '请选择驱动', mgm: '驱动管理', @@ -1905,6 +1905,10 @@ export default { label: '文本下拉', placeholder: '请选择' }, + detextselectTree: { + label: '下拉树', + placeholder: '请选择' + }, detextgridselect: { label: '文本列表', placeholder: '请选择' diff --git a/frontend/src/views/panel/filter/index.vue b/frontend/src/views/panel/filter/index.vue index 756014ede5..30a92b0949 100644 --- a/frontend/src/views/panel/filter/index.vue +++ b/frontend/src/views/panel/filter/index.vue @@ -55,7 +55,8 @@ export default { '文本过滤组件': [ 'textSelectWidget', 'textSelectGridWidget', - 'textInputWidget' + 'textInputWidget', + 'textSelectTreeWidget' ], '数字过滤组件': [ 'numberSelectWidget', diff --git a/frontend/src/views/system/test/MyTree.vue b/frontend/src/views/system/test/MyTree.vue new file mode 100644 index 0000000000..12ae1d25dc --- /dev/null +++ b/frontend/src/views/system/test/MyTree.vue @@ -0,0 +1,150 @@ + + + + diff --git a/frontend/src/views/system/test/index.vue b/frontend/src/views/system/test/index.vue new file mode 100644 index 0000000000..0e6f81a882 --- /dev/null +++ b/frontend/src/views/system/test/index.vue @@ -0,0 +1,69 @@ + + + +