fix(仪表板): 图表过滤器日期类型字段支持动态日期

This commit is contained in:
dataeaseShu 2024-08-06 14:15:48 +08:00
parent 48592ccccb
commit d54328e821
7 changed files with 2256 additions and 2 deletions

View File

@ -0,0 +1,135 @@
<script lang="ts" setup>
import { toRefs, PropType, ref, onBeforeMount, watch, computed } from 'vue'
import { type DatePickType } from 'element-plus-secondary'
import {
getThisYear,
getLastYear,
getThisMonth,
getLastMonth,
getToday,
getYesterday,
getMonthBeginning,
getYearBeginning,
getCustomTime
} from './time-format'
interface SelectConfig {
relativeToCurrent: string
timeNum: number
relativeToCurrentType: string
around: string
arbitraryTime: Date
timeGranularity: DatePickType
}
const props = defineProps({
config: {
type: Object as PropType<SelectConfig>,
default: () => {
return {
relativeToCurrent: 'custom',
timeNum: 0,
relativeToCurrentType: 'year',
around: 'f',
arbitraryTime: new Date(),
timeGranularity: 'year'
}
}
}
})
const selectValue = ref()
const { config } = toRefs(props)
const timeConfig = computed(() => {
const {
relativeToCurrent,
timeNum,
relativeToCurrentType,
around,
arbitraryTime,
timeGranularity
} = config.value
return {
relativeToCurrent,
timeNum,
relativeToCurrentType,
around,
arbitraryTime,
timeGranularity
}
})
watch(
() => timeConfig.value,
() => {
init()
},
{
deep: true
}
)
const init = () => {
const {
relativeToCurrent,
timeNum,
relativeToCurrentType,
around,
arbitraryTime,
timeGranularity
} = timeConfig.value
if (relativeToCurrent === 'custom') {
selectValue.value = getCustomTime(
timeNum,
relativeToCurrentType,
timeGranularity,
around,
timeGranularity === 'datetime' ? arbitraryTime : null
)
} else {
switch (relativeToCurrent) {
case 'thisYear':
selectValue.value = getThisYear()
break
case 'lastYear':
selectValue.value = getLastYear()
break
case 'thisMonth':
selectValue.value = getThisMonth()
break
case 'lastMonth':
selectValue.value = getLastMonth()
break
case 'today':
selectValue.value = getToday()
break
case 'yesterday':
selectValue.value = getYesterday()
break
case 'monthBeginning':
selectValue.value = getMonthBeginning()
break
case 'yearBeginning':
selectValue.value = getYearBeginning()
break
default:
break
}
}
}
onBeforeMount(() => {
init()
})
</script>
<template>
<el-date-picker
disabled
:key="config.timeGranularity"
v-model="selectValue"
:type="config.timeGranularity"
:placeholder="$t('commons.date.select_date_time')"
/>
</template>

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { ElMessage } from 'element-plus-secondary'
import { inject, computed, ref, nextTick, provide } from 'vue'
import RowAuth from '@/views/visualized/data/dataset/auth-tree/RowAuth.vue'
import RowAuth from '@/views/chart/components/editor/filter/auth-tree/RowAuth.vue'
const emits = defineEmits(['filter-data'])
const filedList = inject('filedList', () => [])

View File

@ -0,0 +1,308 @@
<script lang="ts" setup>
import { ref, computed } from 'vue'
import DynamicTime from '@/custom-component/v-query/DynamicTimeForViewFilter.vue'
import { type DatePickType } from 'element-plus-secondary'
export interface SelectConfig {
relativeToCurrent: string
timeNum: number
relativeToCurrentType: string
around: string
arbitraryTime: Date
timeGranularity: DatePickType
}
const defaultValue: SelectConfig = {
relativeToCurrent: 'custom', // thisYear lastYear thisMonth lastMonth today yesterday monthBeginning yearBeginning
timeGranularity: 'year', // year month date datetime
timeNum: 0, //
relativeToCurrentType: 'year', // year month date
around: 'b', // b f
arbitraryTime: new Date() //timeGranularity = datetime
}
const curComponent = ref<SelectConfig>({ ...defaultValue })
const init = (val: SelectConfig) => {
curComponent.value = {
...defaultValue,
...val
}
}
const aroundList = [
{
label: '前',
value: 'f'
},
{
label: '后',
value: 'b'
}
]
const relativeToCurrentList = computed(() => {
let list = []
if (!curComponent.value) return list
switch (curComponent.value.timeGranularity) {
case 'year':
list = [
{
label: '今年',
value: 'thisYear'
},
{
label: '去年',
value: 'lastYear'
}
]
break
case 'month':
list = [
{
label: '本月',
value: 'thisMonth'
},
{
label: '上月',
value: 'lastMonth'
}
]
break
case 'date':
list = [
{
label: '今天',
value: 'today'
},
{
label: '昨天',
value: 'yesterday'
},
{
label: '月初',
value: 'monthBeginning'
},
{
label: '年初',
value: 'yearBeginning'
}
]
break
case 'datetime':
list = [
{
label: '今天',
value: 'today'
},
{
label: '昨天',
value: 'yesterday'
},
{
label: '月初',
value: 'monthBeginning'
},
{
label: '年初',
value: 'yearBeginning'
}
]
break
default:
break
}
return [
...list,
{
label: '自定义',
value: 'custom'
}
]
})
const relativeToCurrentTypeList = computed(() => {
if (!curComponent.value) return []
let index = ['year', 'month', 'date', 'datetime'].indexOf(curComponent.value.timeGranularity) + 1
return [
{
label: '年',
value: 'year'
},
{
label: '月',
value: 'month'
},
{
label: '日',
value: 'date'
}
].slice(0, index)
})
const timeGranularityChange = (val: string) => {
if (
['year', 'month', 'date', 'datetime'].indexOf(val) <
['year', 'month', 'date'].indexOf(curComponent.value.relativeToCurrentType)
) {
curComponent.value.relativeToCurrentType = 'year'
}
if (curComponent.value.relativeToCurrent !== 'custom') {
curComponent.value.relativeToCurrent = relativeToCurrentList.value[0]?.value
}
}
const timeList = [
{
label: '年',
value: 'year'
},
{
label: '年月',
value: 'month'
},
{
label: '年月日',
value: 'date'
},
{
label: '年月日时分秒',
value: 'datetime'
}
]
defineExpose({
init,
curComponent
})
</script>
<template>
<div class="time-dialog">
<div class="setting">
<div class="setting-label">时间粒度</div>
<div class="setting-value select">
<el-select
@change="timeGranularityChange"
placeholder="请选择时间粒度"
v-model="curComponent.timeGranularity"
>
<el-option
v-for="ele in timeList"
:key="ele.value"
:label="ele.label"
:value="ele.value"
/>
</el-select>
</div>
</div>
<div class="setting">
<div class="setting-label">相对当前</div>
<div class="setting-value select">
<el-select v-model="curComponent.relativeToCurrent">
<el-option
v-for="item in relativeToCurrentList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</div>
<div class="setting" v-if="curComponent.relativeToCurrent === 'custom'">
<div
class="setting-input"
:class="curComponent.timeGranularity === 'datetime' && 'with-date'"
>
<el-input-number v-model="curComponent.timeNum" :min="0" controls-position="right" />
<el-select v-model="curComponent.relativeToCurrentType">
<el-option
v-for="item in relativeToCurrentTypeList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-select v-model="curComponent.around">
<el-option
v-for="item in aroundList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-time-picker
style="width: 146px; margin-left: 8px"
v-if="curComponent.timeGranularity === 'datetime'"
v-model="curComponent.arbitraryTime"
/>
</div>
</div>
<div class="setting">
<div class="setting-label">预览</div>
<div class="setting-value">
<component :config="curComponent" isConfig ref="inputCom" :is="DynamicTime"></component>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.time-dialog {
.setting {
&.setting {
margin-top: 8px;
}
&.parameters {
width: 100%;
padding-left: 24px;
.ed-date-editor {
width: 325px !important;
}
}
margin-left: auto;
display: flex;
justify-content: space-between;
align-items: center;
.setting-label {
width: 80px;
margin-right: 8px;
}
.setting-value {
margin: 8px 0;
&.select {
margin-top: 0;
.ed-select {
width: 325px;
}
}
}
.setting-input {
display: flex;
padding-left: 86px;
justify-content: flex-end;
align-items: center;
&.range {
padding-left: 0px;
}
& > div + div {
margin-left: 8px;
}
&.with-date {
.ed-input-number {
width: 71px;
}
.ed-select {
width: 62px;
}
.ed-date-editor.ed-input {
width: 106px;
}
}
}
}
}
</style>

View File

@ -0,0 +1,244 @@
<script lang="ts">
export default {
name: 'logic-relation'
}
</script>
<script lang="ts" setup>
import { useI18n } from '@/hooks/web/useI18n'
import { PropType, computed, toRefs } from 'vue'
import FilterFiled from './FilterFiled.vue'
import type { Item } from './FilterFiled.vue'
export type Logic = 'or' | 'and'
export type Relation = {
child?: Relation[]
logic: Logic
x: number
} & Item
const { t } = useI18n()
const props = defineProps({
relationList: {
type: Array as PropType<Relation[]>,
default: () => []
},
x: {
type: Number,
default: 0
},
logic: {
type: String as PropType<Logic>,
default: 'or'
}
})
const marginLeft = computed(() => {
return {
marginLeft: props.x ? '20px' : 0
}
})
const emits = defineEmits([
'addCondReal',
'changeAndOrDfs',
'update:logic',
'removeRelationList',
'del'
])
const { relationList } = toRefs(props)
const handleCommand = type => {
emits('update:logic', type)
emits('changeAndOrDfs', type)
}
const removeRelationList = index => {
relationList.value.splice(index, 1)
}
const addCondReal = type => {
emits('addCondReal', type, props.logic === 'or' ? 'and' : 'or')
}
const add = (type, child, logic) => {
child.push(
type === 'condition'
? {
fieldId: '',
value: '',
enumValue: '',
term: '',
filterType: 'logic',
name: '',
filterTypeTime: 'dateValue',
timeValue: '',
dynamicTimeSetting: {},
deType: ''
}
: { child: [], logic }
)
}
const del = (index, child) => {
child.splice(index, 1)
}
</script>
<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 fir">
{{ logic === 'or' ? 'OR' : 'AND' }}
<el-icon>
<Icon name="icon_down_outlined"></Icon>
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="and">AND</el-dropdown-item>
<el-dropdown-item command="or">OR</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<span class="operate-icon" v-if="x">
<el-icon @click="emits('removeRelationList')">
<Icon name="icon_delete-trash_outlined"></Icon>
</el-icon>
</span>
</div>
<div class="logic-right">
<template :key="index" v-for="(item, index) in relationList">
<logic-relation
v-if="item.child"
:x="item.x"
@del="idx => del(idx, item.child)"
@addCondReal="(type, logic) => add(type, item.child, logic)"
:logic="item.logic"
@removeRelationList="removeRelationList(index)"
:relationList="item.child"
>
</logic-relation>
<filter-filed v-else :item="item" @del="emits('del', index)" :index="index"></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>
<style lang="less" scoped>
.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;
align-items: center;
z-index: 10;
.operate-title {
font-family: '阿里巴巴普惠体 3.0 55 Regular L3', 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;
}
}
&:hover {
.operate-icon {
display: inline-block;
}
.operate-title {
.mrg-title:not(.fir) {
margin: 0 5px;
}
}
}
.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;
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

@ -0,0 +1,375 @@
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import AuthTree from './AuthTree.vue'
const { t } = useI18n()
const errorMessage = ref('')
const logic = ref<'or' | 'and'>('or')
const relationList = ref([])
const svgRealinePath = computed(() => {
const lg = relationList.value.length
let a = { x: 0, y: 0, child: relationList.value }
a.y = Math.floor(dfsXY(a, 0) / 2)
if (!lg) return ''
let path = calculateDepth(a)
return path
})
const svgDashinePath = computed(() => {
const lg = relationList.value.length
let a = { x: 0, y: 0, child: relationList.value }
a.y = Math.floor(dfsXY(a, 0) / 2)
if (!lg) return `M48 20 L68 20`
let path = calculateDepthDash(a)
return path
})
const init = expressionTree => {
const { items } = expressionTree
logic.value = expressionTree.logic || 'or'
relationList.value = dfsInit(items || [])
}
const submit = () => {
errorMessage.value = ''
emits('save', {
logic: logic.value,
items: dfsSubmit(relationList.value),
errorMessage: errorMessage.value
})
}
const errorDetected = ({ enumValue, deType, filterType, term, value, name, timeValue }) => {
if (!name) {
errorMessage.value = t('data_set.cannot_be_empty_')
return
}
if (filterType === 'logic') {
if (!term) {
errorMessage.value = t('data_set.cannot_be_empty_de_ruler')
return
}
if (
!term.includes('null') &&
!term.includes('empty') &&
['', null, undefined].includes(value) &&
deType !== 1
) {
errorMessage.value = t('chart.filter_value_can_null')
return
}
if (
!term.includes('null') &&
!term.includes('empty') &&
['', null, undefined].includes(timeValue) &&
deType === 1
) {
errorMessage.value = t('chart.filter_value_can_null')
return
}
if ([2, 3].includes(deType)) {
if (parseFloat(value).toString() === 'NaN') {
errorMessage.value = t('chart.filter_value_can_not_str')
return
}
}
}
if (filterType === 'enum') {
if (enumValue.length < 1) {
errorMessage.value = t('chart.enum_value_can_not_null')
return
}
}
}
const dfsInit = arr => {
const elementList = []
arr.forEach(ele => {
const { subTree } = ele
if (subTree) {
const { items = [], logic } = subTree
const child = dfsInit(items)
elementList.push({ logic, child })
} else {
const {
enumValue,
timeValue,
filterTypeTime,
dynamicTimeSetting,
fieldId,
filterType,
term,
value,
field
} = ele
const { name, deType } = field || {}
elementList.push({
enumValue: enumValue.join(','),
fieldId,
filterType,
term,
timeValue,
filterTypeTime,
dynamicTimeSetting,
value,
name,
deType
})
}
})
return elementList
}
const dfsSubmit = arr => {
const items = []
arr.forEach(ele => {
const { child = [] } = ele
if (child.length) {
const { logic } = ele
const subTree = dfsSubmit(child)
items.push({
enumValue: [],
fieldId: '',
filterType: '',
term: '',
type: 'tree',
value: '',
filterTypeTime: 'dateValue',
timeValue: '',
dynamicTimeSetting: {},
subTree: { logic, items: subTree }
})
} else {
const {
enumValue,
filterTypeTime,
dynamicTimeSetting,
fieldId,
filterType,
deType,
term,
value,
name,
timeValue
} = ele
errorDetected({ deType, enumValue, filterType, term, value, name, timeValue })
if (fieldId) {
items.push({
enumValue: enumValue ? enumValue.split(',') : [],
fieldId,
timeValue,
filterType,
filterTypeTime,
dynamicTimeSetting,
term,
value,
type: 'item',
subTree: null
})
}
}
})
return items
}
const removeRelationList = () => {
relationList.value = []
}
const getY = arr => {
const [a] = arr
if (a.child?.length) {
return getY(a.child)
}
return a.y
}
const 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(dfsY(obj, 0), dfs(obj.child, 0) + getY(obj.child) - 1)
let parent = (dfs(obj.child, 0) * 41.4) / 2 + (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 += calculateDepthDash(item)
})
}
return path
}
const 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 = (dfs(obj.child, 0) * 41.4) / 2 + (getY(obj.child) || 0) * 41.4
let children = (dfs(item.child, 0) * 41.4) / 2 + 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 += calculateDepth(item)
}
if (!item.child?.length) {
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
}
const changeAndOrDfs = (arr, logic) => {
arr.forEach(ele => {
if (ele.child) {
ele.logic = logic === 'and' ? 'or' : 'and'
changeAndOrDfs(ele.child, ele.logic)
}
})
}
const dfs = (arr, count) => {
arr.forEach(ele => {
if (ele.child?.length) {
count = dfs(ele.child, count)
} else {
count += 1
}
})
count += 1
return count
}
const dfsY = (obj, count) => {
obj.child.forEach(ele => {
if (ele.child?.length) {
count = dfsY(ele, count)
} else {
count = Math.max(count, ele.y, obj.y)
}
})
return count
}
const dfsXY = (obj, count) => {
obj.child.forEach(ele => {
ele.x = obj.x + 1
if (ele.child?.length) {
let l = dfs(ele.child, 0)
ele.y = Math.floor(l / 2) + count
count = dfsXY(ele, count)
} else {
count += 1
ele.y = count - 1
}
})
count += 1
return count
}
const addCondReal = (type, logic) => {
relationList.value.push(
type === 'condition'
? {
fieldId: '',
value: '',
enumValue: '',
term: '',
filterType: 'logic',
name: '',
timeValue: '',
filterTypeTime: 'dateValue',
dynamicTimeSetting: {},
deType: ''
}
: { child: [], logic }
)
}
const del = index => {
relationList.value.splice(index, 1)
}
defineExpose({
init,
submit
})
const emits = defineEmits(['save'])
</script>
<template>
<div class="rowAuth">
<auth-tree
@del="idx => del(idx)"
@addCondReal="addCondReal"
@removeRelationList="removeRelationList"
@changeAndOrDfs="type => changeAndOrDfs(relationList, type)"
:relationList="relationList"
v-model:logic="logic"
/>
<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>
<style lang="less" scoped>
.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>

@ -1 +1 @@
Subproject commit 11b651cfa73e22195c36887672539a6a7ee0dddc
Subproject commit 11240f0a32fc1916409c82580e79fb17bb7988eb