2026-03-31 10:14:20 +08:00
|
|
|
|
<!-- SidePanelItem.vue -->
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="qgc-side-pannel-item">
|
|
|
|
|
|
<div class="qgc_title">
|
|
|
|
|
|
<div class="title_left">
|
2026-05-09 17:04:48 +08:00
|
|
|
|
<span class="texttitle">{{ title }}</span>
|
2026-03-31 10:14:20 +08:00
|
|
|
|
<span v-if="prompt.show" class="title_icon">
|
|
|
|
|
|
<a-tooltip placement="top" :title="prompt.value" :get-popup-container="getPopupContainer">
|
|
|
|
|
|
<QuestionCircleOutlined />
|
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span v-if="clickprompt.show" class="title_icon">
|
|
|
|
|
|
<a-tooltip placement="top" trigger="click" :title="clickprompt.value"
|
|
|
|
|
|
:get-popup-container="getPopupContainer">
|
|
|
|
|
|
<InfoCircleOutlined />
|
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
|
</span>
|
2026-05-15 18:08:29 +08:00
|
|
|
|
<span v-if="iconmap.show" class="title_icon">
|
2026-05-26 19:30:22 +08:00
|
|
|
|
<a-tooltip placement="top" :title="iconmap.value" :get-popup-container="getPopupContainer">
|
2026-05-15 18:08:29 +08:00
|
|
|
|
<span :class="iconmap.icon"></span>
|
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
|
</span>
|
2026-03-31 10:14:20 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="title_right">
|
|
|
|
|
|
<div v-if="select.show">
|
2026-05-12 08:47:27 +08:00
|
|
|
|
<a-select v-model:value="selectValue" show-search placeholder="请选择" :size="'small'"
|
|
|
|
|
|
style="width: 120px" :options="select.options" :filter-option="filterOption"
|
2026-05-26 19:30:22 +08:00
|
|
|
|
@focus="handleFocus" @blur="handleBlur" @change="handleChange"></a-select>
|
2026-03-31 10:14:20 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="shrink" class="title_shrink" @click="isExpand = !isExpand">
|
|
|
|
|
|
<img v-if="isExpand" src="@/assets/components/arrow-up.png" alt="">
|
|
|
|
|
|
<img v-else src="@/assets/components/arrow-down.png" alt="">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="moreSelect.show">
|
2026-05-26 19:30:22 +08:00
|
|
|
|
<a-tree-select v-model:value="moreSelectValue" v-model:tree-expanded-keys="treeExpandedKeys"
|
|
|
|
|
|
show-search :size="'small'" style="width: 110px"
|
|
|
|
|
|
:dropdown-style="{ maxHeight: '400px', overflow: 'auto', minWidth: '180px' }" placeholder=" "
|
|
|
|
|
|
:tree-data="processedMoreSelectOptions"
|
2026-05-20 08:44:31 +08:00
|
|
|
|
:field-names="{ label: 'title', value: 'value', children: 'children' }"
|
2026-05-26 19:30:22 +08:00
|
|
|
|
tree-node-filter-prop="label" popup-class-name="no-wrap-tree-select" @select="handleTreeSelect"
|
|
|
|
|
|
@expand="handleTreeExpand">
|
2026-03-31 10:14:20 +08:00
|
|
|
|
</a-tree-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="datetimePicker.show">
|
|
|
|
|
|
<!-- 添加 locale 属性来设置语言 -->
|
2026-05-26 19:30:22 +08:00
|
|
|
|
<a-date-picker v-model:value="datetimeValue" show-time
|
|
|
|
|
|
:style="{ width: datetimePicker.picker === 'year' ? '80px' : datetimePicker.picker === 'month' ? '90px' : '130px' }"
|
2026-03-31 10:14:20 +08:00
|
|
|
|
:format="datetimePicker.format !== null ? datetimePicker.format : undefined"
|
2026-05-26 19:30:22 +08:00
|
|
|
|
:picker="datetimePicker.picker" :allowClear="false" placeholder=" "
|
|
|
|
|
|
@change="handleDateTimeChange" :size="'small'"
|
2026-05-20 08:44:31 +08:00
|
|
|
|
:disabledDate="createDisabledDateFn(datetimePicker.picker)"
|
|
|
|
|
|
:disabledTime="disabledTimeForSinglePicker" />
|
2026-03-31 10:14:20 +08:00
|
|
|
|
<!-- 修改为 locale 变量 -->
|
|
|
|
|
|
</div>
|
2026-05-12 08:47:27 +08:00
|
|
|
|
<div v-if="scopeDate.show" class="title_scopeDate">
|
2026-05-26 19:30:22 +08:00
|
|
|
|
<a-range-picker v-model:value="scopeDateValue" :picker="scopeDate.picker" :allowClear="false"
|
2026-05-20 08:44:31 +08:00
|
|
|
|
:style="{ width: scopeDate.picker === 'year' ? '80px' : (scopeDate.picker === 'month' ? '180px' : '') }"
|
2026-05-26 19:30:22 +08:00
|
|
|
|
:format="scopeDate.format" :range-separator="' 至 '" :size="'small'"
|
|
|
|
|
|
:presets="computedScopeDatePresets" :disabledDate="createDisabledDateFn(scopeDate.picker)" />
|
2026-05-12 08:47:27 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="tabs.show" class="typeOne">
|
|
|
|
|
|
<div @click="handleTabClick('one')" :class="tabsValue == 'one' ? 'typezhong' : ''">图片</div>
|
|
|
|
|
|
<div @click="handleTabClick('two')" :class="tabsValue == 'two' ? 'typezhong' : ''">视频</div>
|
2026-03-31 10:14:20 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="body">
|
|
|
|
|
|
<slot v-if="isExpand" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
2026-05-26 19:30:22 +08:00
|
|
|
|
import { ref, onMounted, watch, computed, nextTick } from 'vue';
|
2026-03-31 10:14:20 +08:00
|
|
|
|
import {
|
|
|
|
|
|
QuestionCircleOutlined,
|
|
|
|
|
|
InfoCircleOutlined
|
|
|
|
|
|
} from '@ant-design/icons-vue';
|
|
|
|
|
|
import type { SelectProps } from 'ant-design-vue';
|
|
|
|
|
|
// 导入 dayjs
|
|
|
|
|
|
import dayjs, { Dayjs } from 'dayjs';
|
|
|
|
|
|
|
|
|
|
|
|
// 定义类型接口
|
|
|
|
|
|
interface PromptConfig {
|
|
|
|
|
|
show: boolean;
|
|
|
|
|
|
value: string;
|
2026-05-15 18:08:29 +08:00
|
|
|
|
icon?: string;
|
2026-03-31 10:14:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface SelectConfig {
|
|
|
|
|
|
picker: any;
|
|
|
|
|
|
format: any;
|
|
|
|
|
|
show: boolean;
|
|
|
|
|
|
value: string | undefined;
|
|
|
|
|
|
options: SelectProps['options'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 定义组件名(便于调试和递归)
|
|
|
|
|
|
defineOptions({
|
|
|
|
|
|
name: 'SidePanelItem'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 定义props
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
title: { // 标题
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ''
|
|
|
|
|
|
},
|
|
|
|
|
|
shrink: { // 是否显示收缩
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
},
|
|
|
|
|
|
prompt: { // 浮动提示
|
|
|
|
|
|
type: Object as () => PromptConfig,
|
|
|
|
|
|
default: () => ({
|
|
|
|
|
|
show: false,
|
|
|
|
|
|
value: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
clickprompt: { // 点击提示
|
|
|
|
|
|
type: Object as () => PromptConfig,
|
|
|
|
|
|
default: () => ({
|
|
|
|
|
|
show: false,
|
|
|
|
|
|
value: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
2026-05-15 18:08:29 +08:00
|
|
|
|
iconmap: {//自定义图标浮动
|
|
|
|
|
|
type: Object as () => PromptConfig,
|
|
|
|
|
|
default: () => ({
|
|
|
|
|
|
show: false,
|
|
|
|
|
|
value: '',
|
2026-05-26 19:30:22 +08:00
|
|
|
|
icon: 'iconfont icon-time',
|
2026-05-15 18:08:29 +08:00
|
|
|
|
})
|
|
|
|
|
|
},
|
2026-03-31 10:14:20 +08:00
|
|
|
|
select: { // 选择框
|
|
|
|
|
|
type: Object as () => SelectConfig,
|
|
|
|
|
|
default: () => ({
|
|
|
|
|
|
show: false,
|
|
|
|
|
|
value: undefined,
|
|
|
|
|
|
options: []
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
moreSelect: {//树选择框
|
|
|
|
|
|
type: Object as () => SelectConfig,
|
|
|
|
|
|
default: () => ({
|
|
|
|
|
|
show: false,
|
|
|
|
|
|
value: undefined,
|
|
|
|
|
|
options: []
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
datetimePicker: { // 时间选择框
|
|
|
|
|
|
type: Object as () => SelectConfig,
|
|
|
|
|
|
default: () => ({
|
|
|
|
|
|
show: false,
|
|
|
|
|
|
value: undefined,
|
|
|
|
|
|
format: null, //YYYY-MM-DD HH
|
|
|
|
|
|
picker: 'date' //date | week | month | quarter | year
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
scopeDate: { // 时间选择框
|
|
|
|
|
|
type: Object as () => SelectConfig,
|
|
|
|
|
|
default: () => ({
|
|
|
|
|
|
show: false,
|
|
|
|
|
|
value: undefined,
|
2026-05-12 08:47:27 +08:00
|
|
|
|
format: null, //YYYY-MM-DD HH
|
2026-03-31 10:14:20 +08:00
|
|
|
|
picker: 'month' //date | week | month | quarter | year
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
2026-05-12 08:47:27 +08:00
|
|
|
|
tabs: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: () => ({
|
|
|
|
|
|
show: false,
|
|
|
|
|
|
value: 'one',
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-03-31 10:14:20 +08:00
|
|
|
|
});
|
2026-05-20 08:44:31 +08:00
|
|
|
|
const emit = defineEmits(['tab-change', 'update-values']);
|
2026-03-31 10:14:20 +08:00
|
|
|
|
const isExpand = ref(true);
|
|
|
|
|
|
const selectValue = ref(props.select.value)
|
2026-05-20 08:44:31 +08:00
|
|
|
|
const moreSelectValue = ref(props.moreSelect.value)
|
2026-03-31 10:14:20 +08:00
|
|
|
|
const datetimeValue = ref<Dayjs | null>(props.datetimePicker.value ? dayjs(props.datetimePicker.value) : null);
|
2026-05-12 08:47:27 +08:00
|
|
|
|
const scopeDateValue = ref<[Dayjs, Dayjs] | undefined>(
|
|
|
|
|
|
props.scopeDate.value && Array.isArray(props.scopeDate.value)
|
|
|
|
|
|
? [dayjs(props.scopeDate.value[0]), dayjs(props.scopeDate.value[1])]
|
|
|
|
|
|
: undefined
|
|
|
|
|
|
);
|
|
|
|
|
|
const tabsValue = ref(props.tabs.value)
|
2026-05-20 08:44:31 +08:00
|
|
|
|
|
2026-05-26 19:30:22 +08:00
|
|
|
|
// 树形选择器展开状态管理
|
|
|
|
|
|
const treeExpandedKeys = ref<string[]>([])
|
|
|
|
|
|
const nodeMap = new Map<string, { node: any; parentKey: string | null }>()
|
2026-05-20 08:44:31 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建针对不同 picker 类型的日期禁用函数
|
|
|
|
|
|
* @param pickerType - 选择器类型: year | month | quarter | week | date
|
|
|
|
|
|
* @returns disabledDate 回调函数
|
|
|
|
|
|
*/
|
|
|
|
|
|
const createDisabledDateFn = (pickerType: string) => {
|
2026-05-26 19:30:22 +08:00
|
|
|
|
return (current: Dayjs) => {
|
|
|
|
|
|
if (!current) return false
|
|
|
|
|
|
|
|
|
|
|
|
const now = dayjs()
|
|
|
|
|
|
|
|
|
|
|
|
switch (pickerType) {
|
|
|
|
|
|
case 'year':
|
|
|
|
|
|
// 年份选择器:禁用 > 当前年份
|
|
|
|
|
|
return current.year() > now.year()
|
|
|
|
|
|
|
|
|
|
|
|
case 'month':
|
|
|
|
|
|
// 月份选择器:禁用 > 当前年月
|
|
|
|
|
|
return current.isAfter(now, 'month')
|
|
|
|
|
|
|
|
|
|
|
|
case 'quarter':
|
|
|
|
|
|
// 季度选择器:禁用 > 当前季度
|
|
|
|
|
|
return current.isAfter(now, 'quarter')
|
|
|
|
|
|
|
|
|
|
|
|
case 'week':
|
|
|
|
|
|
// 周选择器:禁用 > 当前周
|
|
|
|
|
|
return current.isAfter(now, 'week')
|
|
|
|
|
|
|
|
|
|
|
|
case 'date':
|
|
|
|
|
|
default:
|
|
|
|
|
|
// 日期选择器:禁用 > 当前日期
|
|
|
|
|
|
return current.isAfter(now, 'day')
|
|
|
|
|
|
}
|
2026-05-20 08:44:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 单日期选择器的时间禁用函数(针对 show-time)
|
|
|
|
|
|
* 精确限制到当前时刻的时分秒
|
|
|
|
|
|
*/
|
|
|
|
|
|
const disabledTimeForSinglePicker = () => {
|
2026-05-26 19:30:22 +08:00
|
|
|
|
// 只在 datetimePicker 显示且为 date 类型时生效
|
|
|
|
|
|
if (!props.datetimePicker.show || props.datetimePicker.picker !== 'date') {
|
|
|
|
|
|
return undefined
|
2026-05-20 08:44:31 +08:00
|
|
|
|
}
|
2026-05-26 19:30:22 +08:00
|
|
|
|
|
|
|
|
|
|
const now = dayjs()
|
|
|
|
|
|
const selectedDate = datetimeValue.value
|
|
|
|
|
|
|
|
|
|
|
|
// 如果未选择日期,或选择的不是今天,则不限制时间
|
|
|
|
|
|
if (!selectedDate || !selectedDate.isSame(now, 'day')) {
|
|
|
|
|
|
return {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 辅助函数:生成从 start 到 end-1 的数组
|
|
|
|
|
|
const range = (start: number, end: number) => {
|
|
|
|
|
|
const result: number[] = []
|
|
|
|
|
|
for (let i = start; i < end; i++) {
|
|
|
|
|
|
result.push(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
// 禁用当前小时之后的所有小时
|
|
|
|
|
|
disabledHours: () => {
|
|
|
|
|
|
return range(now.hour() + 1, 24)
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 禁用当前分钟之后的所有分钟(仅在当前小时)
|
|
|
|
|
|
disabledMinutes: (selectedHour: number) => {
|
|
|
|
|
|
if (selectedHour === now.hour()) {
|
|
|
|
|
|
return range(now.minute() + 1, 60)
|
|
|
|
|
|
}
|
|
|
|
|
|
return []
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 禁用当前秒数之后的所有秒数(仅在当前小时和分钟)
|
|
|
|
|
|
disabledSeconds: (selectedHour: number, selectedMinute: number) => {
|
|
|
|
|
|
if (selectedHour === now.hour() && selectedMinute === now.minute()) {
|
|
|
|
|
|
return range(now.second() + 1, 60)
|
|
|
|
|
|
}
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
2026-05-20 08:44:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:14:20 +08:00
|
|
|
|
// // 定义 locale 变量
|
|
|
|
|
|
// const locale = zhCN;
|
|
|
|
|
|
|
|
|
|
|
|
// console.log(locale, "zhCN");
|
2026-05-20 08:44:31 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 递归处理树数据,为父节点添加 selectable: false
|
|
|
|
|
|
* @param data 原始树数据
|
|
|
|
|
|
* @returns 处理后的树数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
const processTreeData = (data: any[]): any[] => {
|
|
|
|
|
|
if (!data || !Array.isArray(data)) return [];
|
2026-05-26 19:30:22 +08:00
|
|
|
|
|
2026-05-20 08:44:31 +08:00
|
|
|
|
return data.map(item => {
|
|
|
|
|
|
const newItem = { ...item };
|
2026-05-26 19:30:22 +08:00
|
|
|
|
|
2026-05-20 08:44:31 +08:00
|
|
|
|
// 如果有子项,标记为不可选中
|
|
|
|
|
|
if (newItem.children && newItem.children.length > 0) {
|
|
|
|
|
|
newItem.selectable = false;
|
|
|
|
|
|
// 递归处理子项
|
|
|
|
|
|
newItem.children = processTreeData(newItem.children);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 叶子节点默认可选中(显式声明更清晰)
|
|
|
|
|
|
newItem.selectable = true;
|
|
|
|
|
|
}
|
2026-05-26 19:30:22 +08:00
|
|
|
|
|
2026-05-20 08:44:31 +08:00
|
|
|
|
return newItem;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 计算属性:处理后的树数据
|
|
|
|
|
|
const processedMoreSelectOptions = computed(() => {
|
|
|
|
|
|
return processTreeData(props.moreSelect.options || []);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-26 19:30:22 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 构建树节点映射(建立父子关系)
|
|
|
|
|
|
* @param treeData 树形数据
|
|
|
|
|
|
* @param parentKey 父节点key
|
|
|
|
|
|
*/
|
|
|
|
|
|
const buildNodeMap = (treeData: any[], parentKey: string | null = null) => {
|
|
|
|
|
|
if (!treeData || !Array.isArray(treeData)) return
|
|
|
|
|
|
|
|
|
|
|
|
treeData.forEach(node => {
|
|
|
|
|
|
// 存储当前节点及其父节点信息
|
|
|
|
|
|
nodeMap.set(node.value, {
|
|
|
|
|
|
node,
|
|
|
|
|
|
parentKey
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 递归处理子节点
|
|
|
|
|
|
if (node.children && node.children.length > 0) {
|
|
|
|
|
|
buildNodeMap(node.children, node.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取目标节点的所有父节点keys
|
|
|
|
|
|
* @param targetValue 目标节点value
|
|
|
|
|
|
* @returns 父节点keys数组(从根到直接父节点)
|
|
|
|
|
|
*/
|
|
|
|
|
|
const getParentKeys = (targetValue: string): string[] => {
|
|
|
|
|
|
const parentKeys: string[] = []
|
|
|
|
|
|
let currentKey: string | null = targetValue
|
|
|
|
|
|
|
|
|
|
|
|
// 向上追溯所有父节点
|
|
|
|
|
|
while (currentKey !== null && currentKey !== undefined) {
|
|
|
|
|
|
const nodeInfo = nodeMap.get(currentKey)
|
|
|
|
|
|
|
|
|
|
|
|
// 如果节点不存在或已到达根节点,停止追溯
|
|
|
|
|
|
if (!nodeInfo || nodeInfo.parentKey === null || nodeInfo.parentKey === undefined) {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 将父节点插入到数组开头(保持从根到叶的顺序)
|
|
|
|
|
|
parentKeys.unshift(nodeInfo.parentKey)
|
|
|
|
|
|
currentKey = nodeInfo.parentKey
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return parentKeys
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 08:44:31 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 计算属性:根据 picker 类型动态生成快捷日期选项
|
|
|
|
|
|
*/
|
|
|
|
|
|
const computedScopeDatePresets = computed(() => {
|
|
|
|
|
|
const now = dayjs();
|
|
|
|
|
|
const presets: Array<{ label: string; value: [Dayjs, Dayjs] }> = [];
|
2026-05-26 19:30:22 +08:00
|
|
|
|
|
2026-05-20 08:44:31 +08:00
|
|
|
|
// 根据 picker 类型生成对应的快捷选项
|
|
|
|
|
|
switch (props.scopeDate.picker) {
|
|
|
|
|
|
case 'year':
|
|
|
|
|
|
// 年份选择器:只显示今年
|
|
|
|
|
|
presets.push({
|
|
|
|
|
|
label: '今年',
|
|
|
|
|
|
value: [now.startOf('year'), now]
|
|
|
|
|
|
});
|
|
|
|
|
|
break;
|
2026-05-26 19:30:22 +08:00
|
|
|
|
|
2026-05-20 08:44:31 +08:00
|
|
|
|
case 'month':
|
|
|
|
|
|
// 月份选择器:显示今天、昨天、最近七天、最近一个月、最近三个月、今年
|
|
|
|
|
|
presets.push(
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '今天',
|
|
|
|
|
|
value: [now.startOf('day'), now.endOf('day')]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '昨天',
|
|
|
|
|
|
value: [now.subtract(1, 'day').startOf('day'), now.subtract(1, 'day').endOf('day')]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '最近七天',
|
|
|
|
|
|
value: [now.subtract(6, 'day').startOf('day'), now.endOf('day')]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '最近一个月',
|
|
|
|
|
|
value: [now.subtract(1, 'month').startOf('day'), now.endOf('day')]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '最近三个月',
|
|
|
|
|
|
value: [now.subtract(3, 'month').startOf('day'), now.endOf('day')]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '今年',
|
|
|
|
|
|
value: [now.startOf('year'), now.endOf('day')]
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
break;
|
2026-05-26 19:30:22 +08:00
|
|
|
|
|
2026-05-20 08:44:31 +08:00
|
|
|
|
case 'quarter':
|
|
|
|
|
|
// 季度选择器:显示最近一季度、今年
|
|
|
|
|
|
presets.push(
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '最近一季度',
|
|
|
|
|
|
value: [now.subtract(1, 'quarter').startOf('quarter'), now.endOf('quarter')]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '今年',
|
|
|
|
|
|
value: [now.startOf('year'), now.endOf('quarter')]
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
break;
|
2026-05-26 19:30:22 +08:00
|
|
|
|
|
2026-05-20 08:44:31 +08:00
|
|
|
|
case 'week':
|
|
|
|
|
|
// 周选择器:显示本周、上周、最近四周
|
|
|
|
|
|
presets.push(
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '本周',
|
|
|
|
|
|
value: [now.startOf('week'), now.endOf('week')]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '上周',
|
|
|
|
|
|
value: [now.subtract(1, 'week').startOf('week'), now.subtract(1, 'week').endOf('week')]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '最近四周',
|
|
|
|
|
|
value: [now.subtract(3, 'week').startOf('week'), now.endOf('week')]
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
break;
|
2026-05-26 19:30:22 +08:00
|
|
|
|
|
2026-05-20 08:44:31 +08:00
|
|
|
|
case 'date':
|
|
|
|
|
|
default:
|
|
|
|
|
|
// 日期选择器:显示所有常用选项
|
|
|
|
|
|
presets.push(
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '今天',
|
|
|
|
|
|
value: [now.startOf('day'), now.endOf('day')]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '昨天',
|
|
|
|
|
|
value: [now.subtract(1, 'day').startOf('day'), now.subtract(1, 'day').endOf('day')]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '最近七天',
|
|
|
|
|
|
value: [now.subtract(6, 'day').startOf('day'), now.endOf('day')]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '最近一个月',
|
|
|
|
|
|
value: [now.subtract(1, 'month').startOf('day'), now.endOf('day')]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '最近三个月',
|
|
|
|
|
|
value: [now.subtract(3, 'month').startOf('day'), now.endOf('day')]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '今年',
|
|
|
|
|
|
value: [now.startOf('year'), now.endOf('day')]
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-05-26 19:30:22 +08:00
|
|
|
|
|
2026-05-20 08:44:31 +08:00
|
|
|
|
return presets;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理树节点选择事件
|
|
|
|
|
|
* 由于父节点已设置 selectable: false,这里只会接收到叶子节点
|
|
|
|
|
|
* @param selectedKeys 选中的键值(可能是字符串或数组)
|
|
|
|
|
|
* @param info 节点信息对象(本身就是节点,不是包含 node 的对象)
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleTreeSelect = (selectedKeys: string | string[], info: any) => {
|
|
|
|
|
|
// selectedKeys 可能是字符串(单选模式)或数组(多选模式)
|
|
|
|
|
|
let selectedValue: string;
|
2026-05-26 19:30:22 +08:00
|
|
|
|
|
2026-05-20 08:44:31 +08:00
|
|
|
|
if (typeof selectedKeys === 'string') {
|
|
|
|
|
|
// 单选模式:selectedKeys 直接就是字符串
|
|
|
|
|
|
selectedValue = selectedKeys;
|
|
|
|
|
|
} else if (Array.isArray(selectedKeys)) {
|
|
|
|
|
|
// 多选模式:取第一个元素
|
|
|
|
|
|
selectedValue = selectedKeys[0];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 兜底:尝试从 info 中获取
|
|
|
|
|
|
selectedValue = info?.value || '';
|
|
|
|
|
|
}
|
2026-05-26 19:30:22 +08:00
|
|
|
|
|
2026-05-20 08:44:31 +08:00
|
|
|
|
moreSelectValue.value = selectedValue;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理树节点展开/收起事件
|
|
|
|
|
|
* @param keys 展开的节点键值数组
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleTreeExpand = (keys: string[]) => {
|
2026-05-26 19:30:22 +08:00
|
|
|
|
// 保留事件处理函数以维持组件接口完整性
|
2026-05-20 08:44:31 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-31 10:14:20 +08:00
|
|
|
|
// 下拉选择框事件处理
|
|
|
|
|
|
const handleChange = (value: string) => {
|
2026-05-20 08:44:31 +08:00
|
|
|
|
// Handle change
|
2026-03-31 10:14:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleBlur = () => {
|
2026-05-20 08:44:31 +08:00
|
|
|
|
// Handle blur
|
2026-03-31 10:14:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleFocus = () => {
|
2026-05-20 08:44:31 +08:00
|
|
|
|
// Handle focus
|
2026-03-31 10:14:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const filterOption = (input: string, option?: { value: string }) => {
|
|
|
|
|
|
if (!option) return false;
|
|
|
|
|
|
return option.value.toLowerCase().includes(input.toLowerCase());
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 文字提示容器
|
|
|
|
|
|
const getPopupContainer = (trigger: HTMLElement) => {
|
|
|
|
|
|
return trigger.parentElement;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
//时间选择框事件处理
|
|
|
|
|
|
const handleDateTimeChange = (date: any | null, dateString: string) => {
|
2026-05-20 08:44:31 +08:00
|
|
|
|
// Handle date time change
|
2026-03-31 10:14:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-12 08:47:27 +08:00
|
|
|
|
const handleTabClick = (value: string) => {
|
|
|
|
|
|
tabsValue.value = value;
|
|
|
|
|
|
// 向父组件传递参数
|
|
|
|
|
|
emit('tab-change', {
|
|
|
|
|
|
tabValue: value,
|
|
|
|
|
|
tabLabel: value === 'one' ? '图片' : '视频'
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
2026-05-20 08:44:31 +08:00
|
|
|
|
|
|
|
|
|
|
// 统一收集并发送所有控件的值
|
|
|
|
|
|
const emitAllValues = () => {
|
|
|
|
|
|
const payload: any = {
|
|
|
|
|
|
select: selectValue.value,
|
|
|
|
|
|
moreSelect: moreSelectValue.value,
|
|
|
|
|
|
tabs: tabsValue.value,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理单个时间选择器
|
|
|
|
|
|
if (datetimeValue.value) {
|
|
|
|
|
|
// 使用传入的 format,如果没有则根据 picker 类型设置默认格式
|
|
|
|
|
|
const format = props.datetimePicker.format || getDefaultFormat(props.datetimePicker.picker);
|
|
|
|
|
|
payload.datetime = datetimeValue.value.format(format);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
payload.datetime = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理时间范围选择器
|
|
|
|
|
|
if (scopeDateValue.value && Array.isArray(scopeDateValue.value)) {
|
|
|
|
|
|
// 使用传入的 format,如果没有则根据 picker 类型设置默认格式
|
|
|
|
|
|
const format = props.scopeDate.format || getDefaultFormat(props.scopeDate.picker);
|
|
|
|
|
|
payload.scopeDate = scopeDateValue.value.map(d => d ? d.format(format) : null);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
payload.scopeDate = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
emit('update-values', payload);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 根据 picker 类型获取默认格式
|
|
|
|
|
|
const getDefaultFormat = (picker: string): string => {
|
|
|
|
|
|
switch (picker) {
|
|
|
|
|
|
case 'year':
|
|
|
|
|
|
return 'YYYY';
|
|
|
|
|
|
case 'month':
|
|
|
|
|
|
return 'YYYY-MM';
|
|
|
|
|
|
case 'quarter':
|
|
|
|
|
|
return 'YYYY-[Q]Q';
|
|
|
|
|
|
case 'week':
|
|
|
|
|
|
return 'YYYY-wo';
|
|
|
|
|
|
case 'date':
|
|
|
|
|
|
default:
|
|
|
|
|
|
return 'YYYY-MM-DD';
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 监听各个变量的变化
|
|
|
|
|
|
watch([selectValue, moreSelectValue, datetimeValue, scopeDateValue, tabsValue], () => {
|
|
|
|
|
|
emitAllValues();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监听 props 变化,实现父子组件数据同步
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.select.value,
|
|
|
|
|
|
(newVal) => {
|
|
|
|
|
|
if (newVal !== selectValue.value) {
|
|
|
|
|
|
selectValue.value = newVal;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-05-26 19:30:22 +08:00
|
|
|
|
// 监听 moreSelectValue 的变化,移除了自动展开逻辑
|
2026-05-20 08:44:31 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => props.moreSelect.value,
|
|
|
|
|
|
(newVal) => {
|
|
|
|
|
|
if (newVal !== moreSelectValue.value) {
|
|
|
|
|
|
moreSelectValue.value = newVal;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.datetimePicker.value,
|
|
|
|
|
|
(newVal) => {
|
|
|
|
|
|
const newDayjs = newVal ? dayjs(newVal) : null;
|
|
|
|
|
|
if (!datetimeValue.value || !newDayjs || !datetimeValue.value.isSame(newDayjs)) {
|
|
|
|
|
|
datetimeValue.value = newDayjs;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.scopeDate.value,
|
|
|
|
|
|
(newVal) => {
|
|
|
|
|
|
if (newVal && Array.isArray(newVal) && newVal.length === 2) {
|
|
|
|
|
|
const newRange: [Dayjs, Dayjs] = [dayjs(newVal[0]), dayjs(newVal[1])];
|
|
|
|
|
|
const currentRange = scopeDateValue.value;
|
2026-05-26 19:30:22 +08:00
|
|
|
|
|
2026-05-20 08:44:31 +08:00
|
|
|
|
// 检查是否需要更新(避免不必要的触发)
|
2026-05-26 19:30:22 +08:00
|
|
|
|
if (!currentRange ||
|
|
|
|
|
|
!currentRange[0].isSame(newRange[0]) ||
|
2026-05-20 08:44:31 +08:00
|
|
|
|
!currentRange[1].isSame(newRange[1])) {
|
|
|
|
|
|
scopeDateValue.value = newRange;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (scopeDateValue.value) {
|
|
|
|
|
|
scopeDateValue.value = undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.tabs.value,
|
|
|
|
|
|
(newVal) => {
|
|
|
|
|
|
if (newVal !== tabsValue.value) {
|
|
|
|
|
|
tabsValue.value = newVal;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-05-26 19:30:22 +08:00
|
|
|
|
// 监听 moreSelectValue 变化(包括默认值和用户选择)
|
|
|
|
|
|
watch(() => moreSelectValue.value, (newValue) => {
|
|
|
|
|
|
console.log('moreSelectValue 变化:', newValue)
|
|
|
|
|
|
|
|
|
|
|
|
if (newValue && nodeMap.size > 0) {
|
|
|
|
|
|
// 只有当 nodeMap 已构建时才执行
|
|
|
|
|
|
const parentKeys = getParentKeys(newValue as string)
|
|
|
|
|
|
treeExpandedKeys.value = parentKeys
|
|
|
|
|
|
console.log('自动展开父节点:', parentKeys)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
treeExpandedKeys.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
}, { immediate: true }) // immediate: true 确保默认值也能触发
|
|
|
|
|
|
|
|
|
|
|
|
// 监听树数据变化,重新构建映射
|
|
|
|
|
|
watch(() => processedMoreSelectOptions.value, (newData) => {
|
|
|
|
|
|
console.log('树数据变化,重新构建映射')
|
|
|
|
|
|
|
|
|
|
|
|
if (newData && newData.length > 0) {
|
|
|
|
|
|
// 清空旧映射
|
|
|
|
|
|
nodeMap.clear()
|
|
|
|
|
|
// 重新构建映射
|
|
|
|
|
|
buildNodeMap(newData)
|
|
|
|
|
|
|
|
|
|
|
|
// 数据加载完成后,如果已有默认值,重新触发一次
|
|
|
|
|
|
if (moreSelectValue.value) {
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
const parentKeys = getParentKeys(moreSelectValue.value as string)
|
|
|
|
|
|
treeExpandedKeys.value = parentKeys
|
|
|
|
|
|
console.log('数据加载后自动展开父节点:', parentKeys)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, { deep: true })
|
|
|
|
|
|
|
2026-03-31 10:14:20 +08:00
|
|
|
|
// 页面加载时执行的逻辑
|
|
|
|
|
|
onMounted(() => {
|
2026-05-20 08:44:31 +08:00
|
|
|
|
// 初始化时发送一次默认值
|
|
|
|
|
|
emitAllValues();
|
2026-03-31 10:14:20 +08:00
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss">
|
|
|
|
|
|
.qgc-side-pannel-item {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
|
|
|
|
.qgc_title {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
background-color: #e5edf3;
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
color: #2f6b98;
|
|
|
|
|
|
line-height: 36px;
|
|
|
|
|
|
padding-left: 16px;
|
|
|
|
|
|
padding-right: 8px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
|
|
|
|
|
|
.title_shrink {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.title_left {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-05-12 08:47:27 +08:00
|
|
|
|
|
|
|
|
|
|
.texttitle {
|
2026-05-09 17:04:48 +08:00
|
|
|
|
display: inline-block;
|
2026-05-12 08:47:27 +08:00
|
|
|
|
//width:180px;
|
2026-05-09 17:04:48 +08:00
|
|
|
|
}
|
2026-05-12 08:47:27 +08:00
|
|
|
|
|
2026-03-31 10:14:20 +08:00
|
|
|
|
.title_icon {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
margin-left: 5px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
2026-05-12 08:47:27 +08:00
|
|
|
|
|
2026-03-31 10:14:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.title_right {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-05-12 08:47:27 +08:00
|
|
|
|
|
|
|
|
|
|
div {
|
|
|
|
|
|
|
2026-05-09 17:04:48 +08:00
|
|
|
|
margin-right: 2px;
|
|
|
|
|
|
}
|
2026-03-31 10:14:20 +08:00
|
|
|
|
}
|
2026-05-12 08:47:27 +08:00
|
|
|
|
|
|
|
|
|
|
.typeOne {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
// width: 84px;
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
|
|
|
|
div {
|
|
|
|
|
|
width: 38px;
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
line-height: 24px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
margin: 0px;
|
|
|
|
|
|
color: rgba(0, 0, 0, .85);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.typezhong {
|
|
|
|
|
|
background: #40a9ff;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-31 10:14:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qgc_title:before {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
content: "";
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
width: 2px;
|
|
|
|
|
|
background-color: #005293;
|
|
|
|
|
|
top: 2px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.body {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
line-height: 22px;
|
|
|
|
|
|
padding: 16px 0 0;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
height: calc(100% - 36px);
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
p {
|
|
|
|
|
|
text-indent: 2em;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-12 08:47:27 +08:00
|
|
|
|
|
|
|
|
|
|
.title_scopeDate {
|
|
|
|
|
|
:deep(.ant-picker-range-separator) {
|
|
|
|
|
|
padding: 0px !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-20 08:44:31 +08:00
|
|
|
|
</style>
|