Merge pull request #10367 from dataease/pr@dev-v2_cascade

Pr@dev v2 cascade
This commit is contained in:
dataeaseShu 2024-06-19 15:08:25 +08:00 committed by GitHub
commit 492b239bf8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 525 additions and 3 deletions

View File

@ -17,6 +17,7 @@ export interface EnumValue {
sortId?: string
sort?: string
searchText: string
filter?: Array<{}>
}
interface Fields {
@ -286,3 +287,9 @@ export const exportDeletePost = async (data): Promise<IResponse> => {
return res?.data
})
}
export const listByDsIds = async (data): Promise<IResponse> => {
return request.post({ url: 'datasetField/listByDsIds', data }).then(res => {
return res?.data
})
}

View File

@ -253,10 +253,15 @@ const queryDataForId = id => {
const getQueryConditionWidth = () => {
return customStyle.queryConditionWidth
}
const getCascadeList = () => {
return props.element.cascade
}
provide('unmount-select', unMountSelect)
provide('release-unmount-select', releaseSelect)
provide('query-data-for-id', queryDataForId)
provide('com-width', getQueryConditionWidth)
provide('cascade-list', getCascadeList)
onBeforeUnmount(() => {
emitter.off(`addQueryCriteria${element.value.id}`)

View File

@ -0,0 +1,358 @@
<script lang="ts" setup>
import { ref, shallowRef } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { ElMessage } from 'element-plus-secondary'
import { guid } from '@/views/visualized/data/dataset/form/util.js'
import { listByDsIds } from '@/api/dataset'
import { cloneDeep } from 'lodash-es'
interface Cascade {
datasetId: string
name: string
queryId: string
deType: string
fieldId: string
}
type cascadeMap = Record<string, Cascade>
const { t } = useI18n()
let deTypeMap = shallowRef({})
const emits = defineEmits(['saveCascade'])
const dialogVisible = ref(false)
const handleBeforeClose = () => {
dialogVisible.value = false
}
const cascadeList = ref([])
const optionsMap = shallowRef({})
const datasetMap = shallowRef([])
const cancelClick = () => {
handleBeforeClose()
}
const confirmClick = () => {
const { isError, arr } = setCascadeArrBack()
if (isError) {
ElMessage.error('查询条件或字段不能为空!')
return
}
emits('saveCascade', arr)
handleBeforeClose()
}
const setCascadeArrBack = () => {
let isError = false
const arr = cloneDeep(cascadeList.value)
arr.forEach(ele => {
ele.forEach(item => {
if (!item.placeholder && !item.fieldId) {
isError = true
}
if (!item.datasetId) {
isError = true
}
item.selectValue = []
})
})
return {
arr,
isError
}
}
const init = (cascadeMap: cascadeMap) => {
datasetMap.value = Object.values(cascadeMap).map(ele => ({
label: ele.name,
deType: ele.deType,
value: `${ele.datasetId}--${ele.queryId}--${ele.fieldId}`
}))
let obj = {}
Object.values(cascadeMap).forEach(ele => {
obj[`${ele.datasetId}--${ele.queryId}--${ele.fieldId}`] = ele.deType
})
deTypeMap.value = obj
listByDsIds(datasetMap.value.map(ele => ele.value.split('--')[0]))
.then(res => {
for (let i in res || {}) {
res[i] = res[i].filter(
ele => ele.deType === Object.values(cascadeMap).find(ele => ele.datasetId === i).deType
)
}
optionsMap.value = res
})
.finally(() => {
dialogVisible.value = true
})
}
const disabledDatasetId = shallowRef([])
const visibleChange = (val, index, idx) => {
let topId = ''
let topIdArr = []
let bottomId = ''
let bottomIdArr = []
for (let i in cascadeList.value[index]) {
if (i > idx) {
if (cascadeList.value[index][i].datasetId && !bottomId) {
bottomId = cascadeList.value[index][i].datasetId
}
continue
}
if (cascadeList.value[index][i].datasetId) {
topId = cascadeList.value[index][i].datasetId
}
if (i === idx) {
topId = (cascadeList.value[index][idx - 1] || {}).datasetId
}
}
cascadeList.value.forEach(ele => {
let tentativeTopArr = []
let tentativeBottomArr = []
for (let i in ele) {
if (topIdArr[topIdArr.length - 1] === tentativeTopArr || bottomId === ele[i].datasetId) {
if (bottomId === ele[i].datasetId) {
bottomIdArr.push(tentativeBottomArr)
}
if (bottomIdArr[bottomIdArr.length - 1] === tentativeBottomArr) {
tentativeBottomArr.push(ele[i].datasetId)
}
continue
}
if (ele[i].datasetId) {
tentativeTopArr.push(ele[i].datasetId)
}
if (topId === ele[i].datasetId) {
topIdArr.push(tentativeTopArr)
}
}
})
if (val) {
disabledDatasetId.value = [...new Set([...topIdArr.flat(), ...bottomIdArr.flat()])].filter(
ele => !!ele
)
}
}
const addCascadeItem = item => {
item.push({
datasetId: '',
fieldId: '',
placeholder: item.length ? '' : '第一级无需配置被级联字段',
id: guid()
})
}
const setPlaceholder = () => {
cascadeList.value.forEach(ele => {
ele.forEach((item, idx) => {
if (idx) {
item.placeholder = ''
}
if (
item &&
ele[idx - 1] &&
item.datasetId &&
item.datasetId.split('--')[0] === ele[idx - 1].datasetId.split('--')[0]
) {
item.placeholder = '与上一级使用同一个数据集,无需配置被级联字段'
item.fieldId = ''
}
})
})
}
const deleteCascade = (idx, item) => {
item.splice(idx, 1)
item[0].fieldId = ''
item[0].placeholder = '第一级无需配置被级联字段'
setPlaceholder()
}
const addCascadeBlock = () => {
const arr = []
addCascadeItem(arr)
cascadeList.value.push(arr)
}
const indexCascade = ' 一二三四五'
defineExpose({
init
})
</script>
<template>
<el-dialog
class="query-condition-cascade"
v-model="dialogVisible"
width="900px"
@click.stop
:before-close="handleBeforeClose"
@mousedown.stop
@mousedup.stop
>
<template #title>
<div class="title">
查询条件级联配置<span class="tip">(仅上级能级联下级,不可反向级联)</span>
</div>
</template>
<div class="content">
<el-icon style="font-size: 16px">
<Icon name="icon_info_colorful"></Icon>
</el-icon>
基于当前查询组件的查询条件,如果需要进行及联配置,需要满足以下条件:<br />
1展示类型为:文本下拉组件和数字下拉组件;2选项值来源为:选择数据集<br />
</div>
<el-button text @click="addCascadeBlock">
<template #icon>
<Icon name="icon_add_outlined"></Icon>
</template>
添加级联配置
</el-button>
<div class="cascade-content" v-for="(item, index) in cascadeList" :key="index">
<el-button :disabled="item.length === 5" text @click="addCascadeItem(item)">
<template #icon>
<Icon name="icon_add_outlined"></Icon>
</template>
添加级联条件
</el-button>
<div class="cascade-item">
<div class="label">查询条件层级</div>
<div class="item-name">请选择查询条件</div>
<div class="cascade-icon"></div>
<div class="item-field">请选择被级联字段</div>
</div>
<div class="cascade-item" v-for="(ele, idx) in item" :key="ele.id">
<div class="label">{{ indexCascade[idx + 1] }}</div>
<div class="item-name">
<el-select
@visible-change="val => visibleChange(val, index, idx)"
v-model="ele.datasetId"
@change="setPlaceholder"
style="width: 300px"
>
<el-option
v-for="item in datasetMap"
:key="item.value"
:label="item.label"
:value="item.value"
:disabled="
disabledDatasetId.includes(item.value) ||
(!!ele.datasetId && deTypeMap[ele.datasetId] !== item.deType)
"
/>
</el-select>
</div>
<div class="cascade-icon">
<el-icon>
<Icon name="join-join"></Icon>
</el-icon>
</div>
<div class="item-field">
<el-select
:placeholder="ele.placeholder"
:disabled="!!ele.placeholder"
v-model="ele.fieldId"
style="width: 300px"
>
<el-option
v-for="item in optionsMap[ele.datasetId.split('--')[0]]"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</div>
<el-button @click="deleteCascade(idx, item)" class="cascade-delete" text>
<template #icon>
<Icon name="icon_delete-trash_outlined"></Icon>
</template>
</el-button>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelClick">{{ t('chart.cancel') }} </el-button>
<el-button @click="confirmClick" type="primary">{{ t('chart.confirm') }} </el-button>
</div>
</template>
</el-dialog>
</template>
<style lang="less" scoped>
.query-condition-cascade {
.title {
.tip {
font-size: 12px;
color: #646a73;
}
}
.content {
height: 62px;
width: 852px;
border-radius: 4px;
background: #e1eaff;
position: relative;
padding: 9px 0 9px 40px;
font-family: '阿里巴巴普惠体 3.0 55 Regular L3';
font-size: 14px;
font-weight: 400;
.ed-icon {
position: absolute;
top: 10.6px;
left: 16px;
font-size: 14px;
color: var(--ed-color-primary, #3370ff);
}
margin-bottom: 16px;
}
.cascade-content {
box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
border: 1px solid #e4e7ed;
padding: 24px;
padding-top: 8px;
margin-top: 8px;
.cascade-item {
display: flex;
align-items: center;
width: 100%;
height: 40px;
.label {
width: 100px;
}
.item-name {
width: 300px;
}
.cascade-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
font-size: 20px;
}
.item-field {
width: 300px;
}
.cascade-delete {
width: 40px;
font-size: 20px;
color: #646a73;
margin-left: 20px;
}
}
}
}
</style>

View File

@ -1,6 +1,16 @@
getLastStart
<script lang="ts" setup>
import { ref, reactive, nextTick, computed, shallowRef, toRefs, watch } from 'vue'
import {
ref,
reactive,
nextTick,
computed,
shallowRef,
toRefs,
watch,
defineAsyncComponent,
provide
} from 'vue'
import { storeToRefs } from 'pinia'
import { addQueryCriteriaConfig } from './options'
import { getCustomTime } from './time-format'
@ -100,6 +110,29 @@ const datasetFieldList = computed(() => {
})
.filter(ele => !!ele)
})
const setCascadeDefault = val => {
conditions.value.forEach(ele => {
if (
ele.optionValueSource === 1 &&
[0, 2, 5].includes(+ele.displayType) &&
val.includes(ele.id)
) {
ele.selectValue = Array.isArray(ele.selectValue) ? [] : undefined
ele.defaultValue = Array.isArray(ele.defaultValue) ? [] : undefined
ele.mapValue = Array.isArray(ele.mapValue) ? [] : undefined
ele.defaultMapValue = Array.isArray(ele.defaultMapValue) ? [] : undefined
}
})
}
let cascadeArr = []
const saveCascade = arr => {
cascadeArr = arr
}
const getCascadeList = () => {
return cascadeArr
}
provide('set-cascade-default', setCascadeDefault)
provide('cascade-list', getCascadeList)
const curComponent = ref()
const manual = ref()
@ -436,6 +469,24 @@ const isInRange = (ele, startWindowTime, timeStamp) => {
}
}
const CascadeDialog = defineAsyncComponent(() => import('./QueryCascade.vue'))
const cascadeDialog = ref()
const openCascadeDialog = () => {
const cascadeMap = conditions.value
.filter(ele => [0, 2, 5].includes(+ele.displayType) && ele.optionValueSource === 1)
.reduce((pre, next) => {
pre[next.id] = {
datasetId: next.dataset.id,
name: next.name,
queryId: next.id,
fieldId: next.field.id,
deType: next.field.deType
}
return pre
}, {})
cascadeDialog.value.init(cascadeMap)
}
const validateConditionType = ({
defaultConditionValueF,
defaultConditionValueS,
@ -643,6 +694,8 @@ const confirmClick = () => {
)
})
queryElement.value.propValue = cloneDeep(conditions.value)
queryElement.value.cascade = cloneDeep(cascadeArr)
cascadeArr = []
snapshotStore.recordSnapshotCache()
}
@ -667,6 +720,7 @@ const confirmValueSource = () => {
ElMessage.error('手工输入-选项值不能为空')
return
}
curComponent.value.valueSource = cloneDeep(
valueSource.value.filter(ele => {
if (typeof ele === 'string') {
@ -720,7 +774,7 @@ const init = (queryId: string) => {
}
renameInput.value = []
handleCondition({ id: queryId })
cascadeArr = cloneDeep(queryElement.value.cascade || [])
dialogVisible.value = true
const datasetFieldIdList = datasetFieldList.value.map(ele => ele.tableId)
for (const i in datasetMap) {
@ -1987,11 +2041,13 @@ defineExpose({
</div>
<template #footer>
<div class="dialog-footer">
<el-button class="query-cascade" @click="openCascadeDialog">查询组件级联配置</el-button>
<el-button @click="cancelClick">{{ t('chart.cancel') }} </el-button>
<el-button @click="confirmClick" type="primary">{{ t('chart.confirm') }} </el-button>
</div>
</template>
</el-dialog>
<CascadeDialog @saveCascade="saveCascade" ref="cascadeDialog"></CascadeDialog>
</template>
<style lang="less">
@ -2038,6 +2094,12 @@ defineExpose({
.query-condition-configuration {
--ed-font-weight-primary: 400;
.query-cascade {
position: absolute;
left: 24px;
bottom: 24px;
}
.ed-dialog__headerbtn {
top: 21px;
display: flex;

View File

@ -14,6 +14,7 @@ import {
} from 'vue'
import { enumValueObj, type EnumValue, getEnumValue } from '@/api/dataset'
import { cloneDeep, debounce } from 'lodash-es'
import { useEmitt } from '@/hooks/web/useEmitt'
interface SelectConfig {
selectValue: any
@ -28,6 +29,9 @@ interface SelectConfig {
sort: string
sortId: string
checkedFields: string[]
dataset: {
id: string
}
field: {
id: string
}
@ -70,6 +74,12 @@ const unMountSelect: Ref = inject('unmount-select')
const releaseSelect = inject('release-unmount-select', Function, true)
const queryDataForId = inject('query-data-for-id', Function, true)
const queryConditionWidth = inject('com-width', Function, true)
const cascadeList = inject('cascade-list', Function, true)
const setCascadeDefault = inject('set-cascade-default', Function, true)
const cascade = computed(() => {
return cascadeList() || []
})
const setDefaultMapValue = arr => {
const { displayId, field } = config.value
@ -96,6 +106,71 @@ onUnmounted(() => {
enumValueArr = []
})
const setCascadeValueBack = val => {
cascade.value.forEach(ele => {
ele.forEach(item => {
if (item.datasetId.split('--')[1] === config.value.id) {
item.selectValue = Array.isArray(val) ? [...val] : val
}
})
})
}
const emitCascade = () => {
cascade.value.forEach(ele => {
let trigger = false
ele.forEach(item => {
if (item.datasetId.split('--')[1] === config.value.id) {
trigger = true
} else if (trigger) {
useEmitt().emitter.emit(`${item.datasetId.split('--')[1]}-select`)
trigger = false
}
})
})
}
const emitCascadeConfig = () => {
const arr = []
cascade.value.forEach(ele => {
let trigger = false
ele.forEach(item => {
if (item.datasetId.split('--')[1] === config.value.id) {
trigger = true
} else if (trigger) {
arr.push(item.datasetId.split('--')[1])
trigger = false
}
})
})
return arr
}
const getCascadeFieldId = () => {
const filter = []
cascade.value.forEach(ele => {
let condition = null
ele.forEach(item => {
const [_, queryId, fieldId] = item.datasetId.split('--')
if (queryId === config.value.id && condition) {
if (item.fieldId) {
condition.fieldId = item.fieldId
}
filter.push(condition)
} else {
if (!!item.selectValue.length) {
condition = {
fieldId: fieldId,
operator: 'in',
value: [...item.selectValue]
}
}
}
})
})
return filter
}
const handleValueChange = () => {
const value = Array.isArray(selectValue.value) ? [...selectValue.value] : selectValue.value
if (!props.isConfig) {
@ -105,9 +180,13 @@ const handleValueChange = () => {
config.value.mapValue = setDefaultMapValue(
Array.isArray(selectValue.value) ? [...selectValue.value] : [selectValue.value]
)
setCascadeValueBack(config.value.mapValue)
emitCascade()
return
}
setCascadeDefault(emitCascadeConfig())
config.value.defaultValue = value
config.value.mapValue = setDefaultMapValue(
Array.isArray(selectValue.value) ? [...selectValue.value] : [selectValue.value]
@ -115,6 +194,7 @@ const handleValueChange = () => {
config.value.defaultMapValue = setDefaultMapValue(
Array.isArray(selectValue.value) ? [...selectValue.value] : [selectValue.value]
)
setCascadeValueBack(config.value.mapValue)
}
const displayTypeChange = () => {
@ -406,7 +486,8 @@ const setOptions = (num: number) => {
displayId: displayId || field.id,
sort,
sortId,
searchText: searchText.value
searchText: searchText.value,
filter: getCascadeFieldId()
})
} else {
options.value = []
@ -452,8 +533,17 @@ const selectStyle = computed(() => {
const mult = ref()
const single = ref()
const getOptionFromCascade = () => {
if (config.value.optionValueSource !== 1 || ![0, 2, 5].includes(+config.value.displayType)) return
debounceOptions(1)
}
onBeforeMount(() => {
init()
useEmitt({
name: `${config.value.id}-select`,
callback: getOptionFromCascade
})
})
defineExpose({