Merge pull request #7268 from dataease/pr@dev_one_dot_x

fix: 视图过滤器
This commit is contained in:
dataeaseShu 2023-12-21 17:25:12 +08:00 committed by GitHub
commit 42fd7939cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1874 additions and 45 deletions

View File

@ -1,12 +1,4 @@
{
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}],
"stage-2"
],
"plugins": ["transform-vue-jsx", "transform-runtime"]
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-transform-runtime", "@babel/plugin-proposal-optional-chaining", "@babel/plugin-proposal-object-rest-spread"]
}

View File

@ -11,6 +11,8 @@
"buildPlugin": "node build/build-async-plugins.js"
},
"dependencies": {
"@babel/polyfill": "^7.12.1",
"@babel/runtime": "^7.18.9",
"@riophae/vue-treeselect": "0.4.0",
"vue": "^2.5.2",
"vue-i18n": "7.3.2",
@ -20,14 +22,12 @@
},
"devDependencies": {
"autoprefixer": "^7.1.2",
"babel-core": "^6.22.1",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-loader": "^7.1.1",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-plugin-transform-vue-jsx": "^3.5.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"@babel/core": "^7.6.4",
"@babel/plugin-proposal-object-rest-spread": "^7.18.9",
"@babel/plugin-proposal-optional-chaining": "^7.18.6",
"@babel/plugin-transform-runtime": "^7.6.2",
"@babel/preset-env": "^7.6.3",
"babel-loader": "^8.0.6",
"chalk": "^2.0.1",
"copy-webpack-plugin": "^4.6.0",
"css-loader": "^0.28.0",

View File

@ -0,0 +1,122 @@
<template>
<el-dialog
width="896px"
append-to-body
title="添加过滤"
destroy-on-close
class="de-dialog-form filter-tree-cont"
:visible.sync="dialogVisible"
>
<div class="tree-cont">
<div class="content">
<rowAuth ref="rowAuth" />
</div>
</div>
<div
slot="footer"
class="dialog-footer"
>
<de-btn
secondary
@click="closeFilter"
>{{ $t('chart.cancel') }}
</de-btn>
<de-btn
type="primary"
@click="changeFilter"
>{{ $t('chart.confirm') }}
</de-btn>
</div>
</el-dialog>
</template>
<script>
import rowAuth from './rowAuth.vue'
export default {
name: 'FilterTree',
inject: ['filedList'],
components: {
rowAuth
},
data() {
return {
dialogVisible: false,
}
},
computed: {
computedFiledList() {
return this.filedList().reduce((pre, next) => {
if (next.id !== 'count') {
pre[next.id] = next
}
return pre
}, {})
}
},
methods: {
closeFilter() {
this.dialogVisible = false
},
changeFilter() {
const { logic, items, errorMessage } = this.$refs.rowAuth.submit()
if (errorMessage) {
this.$message({
message: errorMessage,
type: 'error',
showClose: true
})
return
}
this.dfsTreeDelete(items)
this.$emit('filter-data', { logic, items })
this.dialogVisible = false
},
dfsTreeDelete(arr) {
arr.forEach((ele) => {
if (ele?.subTree?.items?.length) {
this.dfsTreeDelete(ele.subTree.items)
} else {
if (ele.field) {
this.$delete(ele, 'field')
}
}
})
},
dfsTree(arr) {
arr.forEach((ele) => {
if (ele?.subTree?.items?.length) {
this.dfsTree(ele.subTree.items)
} else {
if (this.computedFiledList[ele.fieldId]) {
ele.field = this.computedFiledList[ele.fieldId]
}
}
})
},
init(tree) {
this.dialogVisible = true
this.$nextTick(() => {
this.dfsTree(tree.items || [])
this.$refs.rowAuth.init(tree || {})
})
}
}
}
</script>
<style lang="scss">
.filter-tree-cont {
.tree-cont {
min-height: 67px;
width: 100%;
padding: 16px;
border-radius: 4px;
border: 1px solid var(--deBorderBase, #DCDFE6);
overflow: auto;
max-height: 500px;
.content {
height: 100%;
width: 100%;
}
}
}
</style>

View File

@ -0,0 +1,24 @@
function formatEnum(ele) {
return {
value: ele,
label: `chart.filter_${ele.replace(' ', '_')}`
}
}
const textEnum = ['eq', 'not_eq', 'like', 'not like', 'null', 'not_null', 'empty', 'not_empty']
const textOptions = textEnum.map(formatEnum)
const dateEnum = ['eq', 'not_eq', 'lt', 'gt', 'le', 'ge']
const dateOptions = dateEnum.map(formatEnum)
const valueEnum = [...dateEnum]
const valueOptions = valueEnum.map(formatEnum)
const fieldEnum = ['text', 'time', 'value', 'value', '', 'location']
export {
textOptions,
dateOptions,
valueOptions,
fieldEnum
}

View File

@ -0,0 +1,348 @@
<template>
<div class="rowAuth">
<rowAuthTree
@del="(idx) => del(idx)"
@addCondReal="addCondReal"
@removeRelationList="removeRelationList"
@changeAndOrDfs="(type) => changeAndOrDfs(relationList, type)"
:relationList="relationList"
:logic.sync="logic"
@execute-axios="executeAxios"
/>
<svg width="388" height="100%" class="real-line">
<path
stroke-linejoin="round"
stroke-linecap="round"
:d="svgRealinePath"
fill="none"
stroke="#CCCCCC"
stroke-width="0.5"
></path>
</svg>
<svg width="388" height="100%" class="dash-line">
<path
stroke-linejoin="round"
stroke-linecap="round"
:d="svgDashinePath"
fill="none"
stroke="#CCCCCC"
stroke-width="0.5"
stroke-dasharray="4,4"
></path>
</svg>
</div>
</template>
<script>
import rowAuthTree from "./rowTeeAuth.vue";
export default {
name: "RowAuth",
components: {
rowAuthTree,
},
computed: {
svgRealinePath() {
const lg = this.relationList.length;
let a = { x: 0, y: 0, child: this.relationList };
a.y = Math.floor(this.dfsXY(a, 0) / 2);
if (!lg) return "";
let path = this.calculateDepth(a);
return path;
},
svgDashinePath() {
const lg = this.relationList.length;
let a = { x: 0, y: 0, child: this.relationList };
a.y = Math.floor(this.dfsXY(a, 0) / 2);
if (!lg) return `M48 20 L68 20`;
let path = this.calculateDepthDash(a);
return path;
},
},
methods: {
executeAxios(url, type, data, callBack) {
this.$emit("execute-axios", url, type, data, callBack);
},
init(expressionTree) {
const { logic = "or", items = [] } = expressionTree;
this.logic = logic;
this.relationList = this.dfsInit(items);
},
submit() {
this.errorMessage = '';
this.$emit('save', {
logic: this.logic,
items: this.dfsSubmit(this.relationList),
errorMessage: this.errorMessage,
})
},
errorDetected({ enumValue, deType, filterType, term, value }) {
if (filterType === "logic") {
if (
!term.includes("null") &&
!term.includes("empty") &&
(value === "")
) {
this.errorMessage = this.$t("chart.filter_value_can_null");
return;
}
if ([2, 3].includes(deType)) {
if (parseFloat(value).toString() === "NaN") {
this.errorMessage = this.$t("chart.filter_value_can_not_str");
return;
}
}
}
if (filterType === "enum") {
if (enumValue.length < 1) {
this.errorMessage = this.$t("chart.enum_value_can_not_null");
return;
}
}
},
dfsInit(arr) {
const elementList = [];
arr.forEach((ele) => {
const { subTree } = ele;
if (subTree) {
const { items, logic } = subTree;
const child = this.dfsInit(items);
elementList.push({ logic, child });
} else {
const {
enumValue,
fieldId,
filterType,
term,
value,
field,
} = ele;
const { name, deType } = (field || {});
elementList.push({
enumValue: enumValue.join(','),
fieldId,
filterType,
term,
value,
name,
deType,
});
}
});
return elementList;
},
dfsSubmit(arr) {
const items = [];
arr.forEach((ele) => {
const { child = [] } = ele;
if (child.length) {
const { logic } = ele;
const subTree = this.dfsSubmit(child);
items.push({
enumValue: [],
fieldId: '',
filterType: '',
term: '',
type: 'tree',
value: '', subTree: { logic, items: subTree} });
} else {
const { enumValue, fieldId, filterType,deType, term, value } = ele;
this.errorDetected({ deType, enumValue, filterType, term, value})
if (fieldId) {
items.push({
enumValue: enumValue ? enumValue.split(',') : [],
fieldId,
filterType,
term,
value,
type: 'item',
subTree: null
});
}
}
});
return items;
},
removeRelationList() {
this.relationList = [];
},
getY(arr) {
const [a] = arr;
if (a.child?.length) {
return this.getY(a.child);
}
return a.y;
},
getLastY(arr) {
const a = arr[arr.length];
if (a.child?.length) {
return this.getLastY(a.child);
}
return a.y;
},
calculateDepthDash(obj) {
const lg = obj.child?.length;
let path = "";
if (!lg && Array.isArray(obj.child)) {
const { x, y } = obj;
path += `M${48 + x * 68} ${y * 41.4 + 20} L${88 + x * 68} ${
y * 41.4 + 20
}`;
} else if (obj.child?.length) {
let y = Math.max(
this.dfsY(obj, 0),
this.dfs(obj.child, 0) + this.getY(obj.child) - 1
);
let parent =
(this.dfs(obj.child, 0) * 41.4) / 2 +
(this.getY(obj.child) || 0) * 41.4;
const { x } = obj;
path += `M${24 + x * 68} ${parent} L${24 + x * 68} ${y * 41.4 + 20} L${
64 + x * 68
} ${y * 41.4 + 20}`;
obj.child.forEach((item) => {
path += this.calculateDepthDash(item);
});
}
return path;
},
calculateDepth(obj) {
const lg = obj.child.length;
if (!lg) return "";
let path = "";
const { x: depth, y } = obj;
obj.child.forEach((item, index) => {
const { y: sibingLg, z } = item;
if (item.child?.length) {
let parent =
(this.dfs(obj.child, 0) * 41.4) / 2 +
(this.getY(obj.child) || 0) * 41.4;
let children =
(this.dfs(item.child, 0) * 41.4) / 2 + this.getY(item.child) * 41.4;
let path1 = 0;
let path2 = 0;
if (parent < children) {
path1 = parent;
path2 = children;
} else {
[path1, path2] = [children, parent];
}
if (y >= sibingLg) {
path1 = parent;
path2 = children;
}
path += `M${24 + depth * 68} ${path1} L${24 + depth * 68} ${path2} L${
68 + depth * 68
} ${path2}`;
// path += a;
path += this.calculateDepth(item);
}
if (!item.child?.length) {
// console.log(123, y, sibingLg, lg === 1 && index === 0 , item.z);
if (sibingLg >= y) {
path += `M${24 + depth * 68} ${y * 40} L${24 + depth * 68} ${
(sibingLg + 1) * 41.4 - 20.69921875
} L${68 + depth * 68} ${(sibingLg + 1) * 41.4 - 20.69921875}`;
} else {
path += `M${24 + depth * 68} ${
(sibingLg +
(lg === 1 && index === 0 ? 0 : 1) +
(obj.child[index + 1]?.child?.length ? y - sibingLg - 1 : 0)) *
41.4 +
20 +
(lg === 1 && index === 0 ? 26 : 0)
} L${24 + depth * 68} ${
(sibingLg + 1) * 41.4 -
20.69921875 -
(lg === 1 && index === 0 ? (z || 0) * 1.4 : 0)
} L${68 + depth * 68} ${
(sibingLg + 1) * 41.4 -
20.69921875 -
(lg === 1 && index === 0 ? (z || 0) * 1.4 : 0)
}`;
}
}
});
return path;
},
changeAndOrDfs(arr, logic) {
arr.forEach((ele) => {
if (ele.child) {
ele.logic = logic === "and" ? "or" : "and";
this.changeAndOrDfs(ele.child, ele.logic);
}
});
},
dfs(arr, count) {
arr.forEach((ele) => {
if (ele.child?.length) {
count = this.dfs(ele.child, count);
} else {
count += 1;
}
});
count += 1;
return count;
},
dfsY(obj, count) {
obj.child.forEach((ele) => {
if (ele.child?.length) {
count = this.dfsY(ele, count);
} else {
count = Math.max(count, ele.y, obj.y);
}
});
return count;
},
dfsXY(obj, count) {
obj.child.forEach((ele) => {
ele.x = obj.x + 1;
if (ele.child?.length) {
let l = this.dfs(ele.child, 0);
ele.y = Math.floor(l / 2) + count;
count = this.dfsXY(ele, count);
} else {
count += 1;
ele.y = count - 1;
}
});
count += 1;
return count;
},
addCondReal(type, logic) {
this.relationList.push(
type === "condition"
? { fieldId: '', value: "", enumValue: "", term: "", filterType: "logic", name: "", deType: "" }
: { child: [], logic }
);
},
del(index) {
this.relationList.splice(index, 1);
},
},
data() {
return {
relationList: [],
logic: "or",
errorMessage: "",
};
},
};
</script>
<style>
.rowAuth {
font-family: Avenir, Helvetica, Arial, sans-serif;
text-align: center;
color: #2c3e50;
position: relative;
}
.real-line,
.dash-line {
position: absolute;
top: 0;
left: 0;
user-select: none;
}
</style>

View File

@ -0,0 +1,247 @@
<template>
<div class="logic" :style="marginLeft">
<div class="logic-left">
<div class="operate-title">
<span
style="
{
color: '#bfbfbf';
}
"
class="mrg-title"
v-if="x"
>
{{ logic === 'or' ? "OR" : "AND" }}
</span>
<el-dropdown @command="handleCommand" trigger="click" v-else>
<span
style="
{
color: 'rgba(0,0,0,.65)';
}
"
class="mrg-title"
>
{{ logic === 'or' ? "OR" : "AND" }}<i class="el-icon-arrow-down"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="and">AND</el-dropdown-item>
<el-dropdown-item command="or">OR</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<span class="operate-icon" v-if="x">
<!-- <el-tooltip
class="item"
effect="light"
content="切换最外侧关系节点类型时,所有关系都会切换,内外层关系必然相反"
placement="top-end"
>
<i v-if="!x" class="el-icon-warning-outline"></i>
</el-tooltip> -->
<i class="el-icon-delete" @click="$emit('removeRelationList')"></i>
</span>
</div>
<div class="logic-right">
<template v-for="(item, index) in relationList">
<logic-relation
:x="item.x"
@del="(idx) => del(idx, item.child)"
@addCondReal="(type, logic) => add(type, item.child, logic)"
v-if="item.child"
:key="index"
:logic="item.logic"
@execute-axios="executeAxios"
@removeRelationList="removeRelationList(index)"
:relationList="item.child"
>
</logic-relation>
<filter-filed
v-else
:item="item"
@del="$emit('del', index)"
:index="index"
:key="index"
@execute-axios="executeAxios"
></filter-filed>
</template>
<div class="logic-right-add">
<button @click="addCondReal('condition')" class="operand-btn">
+ {{ $t('auth.add_condition')}}
</button>
<button v-if="x < 2" @click="addCondReal('relation')" class="operand-btn">
+ {{ $t('auth.add_relationship')}}
</button>
</div>
</div>
</div>
</template>
<script>
import filterFiled from "./filterFiled";
export default {
name: "LogicRelation",
props: {
relationList: {
type: Array,
default: () => [],
},
x: {
type: Number,
default: 0,
},
logic: {
type: String,
default: 'or',
},
},
components: {
filterFiled,
},
computed: {
marginLeft() {
return {
marginLeft: this.x ? "20px" : 0,
};
},
},
methods: {
executeAxios(url, type, data, callBack) {
this.$emit("execute-axios", url, type, data, callBack);
},
handleCommand(type) {
this.$emit('update:logic', type);
this.$emit("changeAndOrDfs", type)
},
removeRelationList(index) {
this.relationList.splice(index, 1);
},
addCondReal(type) {
this.$emit("addCondReal", type, this.logic === 'or' ? 'and' : 'or');
},
add(type, child, logic) {
child.push(type === "condition" ?{ fieldId: '', value: '', enumValue: '', term: '', filterType: 'logic', name: '', deType: "" } : { child: [], logic });
},
del(index, child) {
child.splice(index, 1);
},
},
};
</script>
<style lang="scss" scope>
.logic {
display: flex;
align-items: center;
position: relative;
z-index: 1;
width: 100%;
.logic-left {
box-sizing: border-box;
width: 48px;
display: flex;
position: relative;
flex-direction: row;
z-index: 10;
.operate-title {
font-family: PingFang SC, Hiragino Sans GB, Microsoft YaHei, sans-serif;
word-wrap: break-word;
box-sizing: border-box;
color: rgba(0, 0, 0, 0.65);
font-size: 12px;
display: inline-block;
white-space: nowrap;
margin: 0;
padding: 0;
width: 65px;
background-color: #f8f8fa;
line-height: 28px;
position: relative;
z-index: 1;
height: 28px;
.mrg-title {
text-align: left;
box-sizing: border-box;
position: relative;
display: block;
margin-left: 11px;
margin-right: 11px;
line-height: 28px;
height: 28px;
i {
font-size: 12px;
}
}
}
&:hover {
// width: 65px;
.operate-icon {
display: flex;
align-items: center;
}
.operate-title {
.mrg-title {
margin-right: 0 !important;
}
}
}
.operate-icon {
width: 40px;
height: 28px;
line-height: 28px;
background-color: #f8f8fa;
z-index: 1;
display: none;
i {
font-size: 12px;
font-style: normal;
display: unset;
padding: 5px 3px;
cursor: pointer;
position: relative;
z-index: 10;
}
}
}
.logic-right-add {
display: flex;
height: 41.4px;
align-items: center;
padding-left: 26px;
.operand-btn {
box-sizing: border-box;
font-weight: 400;
text-align: center;
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.015);
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
outline: 0;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
-webkit-appearance: button;
cursor: pointer;
height: 28px;
padding: 0 10px;
margin-right: 10px;
font-size: 12px;
color: #246dff;
background: #fff;
border: 1px solid #246dff;
border-radius: 2px;
}
}
}
</style>

View File

@ -91,34 +91,28 @@
</el-row>
<el-row class="padding-lr" style="margin-top: 6px;">
<span>{{ $t('chart.result_filter') }}</span>
<draggable
v-model="view.customFilter"
:move="onMove"
animation="300"
class="theme-item-class"
group="drag"
style="padding:2px 0 0 0;width:100%;min-height: 32px;border-radius: 4px;border: 1px solid #DCDFE6;overflow-x: auto;display: flex;align-items: center;background-color: white;"
@add="addCustomFilter"
@update="calcData(true)"
>
<transition-group class="draggable-group">
<filter-item
v-for="(item,index) in view.customFilter"
:key="item.id"
:dimension-data="dimensionData"
:index="index"
:item="item"
:param="param"
:quota-data="quotaData"
@editItemFilter="showEditFilter"
@onFilterItemRemove="filterItemRemove"
<span class="data-area-label">
<span>{{ $t('chart.result_filter') }}</span>
<span
v-if="!!view.customFilter.logic"
class="setting"
>已设置</span>
<i
class="el-icon-arrow-down el-icon-delete data-area-clear"
@click="deleteTreeFilter"
/>
</transition-group>
</draggable>
<div v-if="!view.customFilter || view.customFilter.length === 0" class="drag-placeholder-style">
<span class="drag-placeholder-style-span">{{ $t('chart.placeholder_field') }}</span>
</div>
</span>
<div
class="tree-btn"
:class="!!view.customFilter.logic && 'active'"
@click="openTreeFilter"
>
<svg-icon
class="svg-background"
icon-class="icon-filter_outlined"
/>
<span>过滤</span>
</div>
</el-row>
<el-row
@ -157,6 +151,10 @@
<span class="drag-placeholder-style-span">{{ $t('chart.placeholder_field') }}</span>
</div>
</el-row>
<FilterTree
ref="filterTree"
@filter-data="changeFilterData"
/>
</div>
</template>
@ -166,8 +164,14 @@ import QuotaItem from '@/components/views/QuotaItem'
import FilterItem from '@/components/views/FilterItem'
import DrillItem from '@/components/views/DrillItem'
import messages from '@/de-base/lang/messages'
import FilterTree from '@/components/views/filter/FilterTree.vue'
export default {
provide() {
return {
filedList: () => this.filedList
}
},
props: {
obj: {
@ -186,6 +190,9 @@ export default {
}
},
computed: {
filedList() {
return [...this.dimensionData, ...this.quotaData].filter(ele => ele.id !== 'count')
},
param() {
return this.obj.param
},
@ -214,6 +221,16 @@ export default {
this.initAreas()
},
methods: {
changeFilterData(customFilter) {
this.view.customFilter =JSON.parse(JSON.stringify(customFilter))
this.calcData(true)
},
openTreeFilter() {
this.$refs.filterTree.init(JSON.parse(JSON.stringify(this.view.customFilter)))
},
deleteTreeFilter() {
this.changeFilterData({})
},
executeAxios(url, type, data, callBack) {
const param = {
url: url,
@ -421,6 +438,41 @@ export default {
<style lang="scss" scoped>
.padding-lr {
padding: 0 6px;
.data-area-label {
text-align: left;
position: relative;
width: 100%;
display: inline-block;
.setting {
padding: 0px 4px 0px 4px;
border-radius: 2px;
background-color: #1F23291A;
color: #646A73;
position: absolute;
top: 1px;
right: 23px;
z-index: 1;
}
}
.tree-btn {
width: 100%;
background: #fff;
height: 32px;
border-radius: 4px;
border: 1px solid #DCDFE6;
display: flex;
color: #CCCCCC;
align-items: center;
cursor: pointer;
justify-content: center;
font-size: 12px;
&.active {
color: #3370FF;
border-color: #3370FF;
}
}
}
.itxst {