WholeProcessPlatform/frontend/src/components/SidePanelItem/index.vue

806 lines
25 KiB
Vue
Raw Normal View History

<!-- 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>
<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">
<a-tooltip placement="top" :title="iconmap.value"
:get-popup-container="getPopupContainer">
<span :class="iconmap.icon"></span>
</a-tooltip>
</span>
</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-20 08:44:31 +08:00
@focus="handleFocus" @blur="handleBlur" @change="handleChange" ></a-select>
</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-20 08:44:31 +08:00
<a-tree-select v-model:value="moreSelectValue" show-search :size="'small'" style="width: 110px"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto',minWidth: '180px' }" placeholder=" "
:tree-data="processedMoreSelectOptions"
:field-names="{ label: 'title', value: 'value', children: 'children' }"
tree-node-filter-prop="label"
:expanded-keys="expandedKeys"
popup-class-name="no-wrap-tree-select"
@select="handleTreeSelect"
@expand="handleTreeExpand"
@dropdownVisibleChange="handleDropdownVisibleChange">
</a-tree-select>
</div>
<div v-if="datetimePicker.show">
<!-- 添加 locale 属性来设置语言 -->
2026-05-20 08:44:31 +08:00
<a-date-picker
v-model:value="datetimeValue"
show-time
:style="{ width: datetimePicker.picker === 'year' ? '80px' : '130px' }"
:format="datetimePicker.format !== null ? datetimePicker.format : undefined"
2026-05-20 08:44:31 +08:00
:picker="datetimePicker.picker"
:allowClear="false"
placeholder=" "
@change="handleDateTimeChange"
:size="'small'"
:disabledDate="createDisabledDateFn(datetimePicker.picker)"
:disabledTime="disabledTimeForSinglePicker" />
<!-- 修改为 locale 变量 -->
</div>
2026-05-12 08:47:27 +08:00
<div v-if="scopeDate.show" class="title_scopeDate">
2026-05-20 08:44:31 +08:00
<a-range-picker
v-model:value="scopeDateValue"
:picker="scopeDate.picker"
:allowClear="false"
:style="{ width: scopeDate.picker === 'year' ? '80px' : (scopeDate.picker === 'month' ? '180px' : '') }"
: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>
</div>
</div>
</div>
<div class="body">
<slot v-if="isExpand" />
</div>
</div>
</template>
<script lang="ts" setup>
2026-05-20 08:44:31 +08:00
import { ref, onMounted, watch, computed } from 'vue';
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;
}
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: '',
icon:'iconfont icon-time',
})
},
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
picker: 'month' //date | week | month | quarter | year
})
},
2026-05-12 08:47:27 +08:00
tabs: {
type: Object,
default: () => ({
show: false,
value: 'one',
})
}
});
2026-05-20 08:44:31 +08:00
const emit = defineEmits(['tab-change', 'update-values']);
const isExpand = ref(true);
const selectValue = ref(props.select.value)
2026-05-20 08:44:31 +08:00
const moreSelectValue = ref(props.moreSelect.value)
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
// 树选择器展开状态管理
const expandedKeys = ref<string[]>([]);
/**
* 创建针对不同 picker 类型的日期禁用函数
* @param pickerType - 选择器类型: year | month | quarter | week | date
* @returns disabledDate 回调函数
*/
const createDisabledDateFn = (pickerType: string) => {
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')
}
}
}
/**
* 单日期选择器的时间禁用函数针对 show-time
* 精确限制到当前时刻的时分秒
*/
const disabledTimeForSinglePicker = () => {
// 只在 datetimePicker 显示且为 date 类型时生效
if (!props.datetimePicker.show || props.datetimePicker.picker !== 'date') {
return undefined
}
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 []
}
}
}
// // 定义 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 [];
return data.map(item => {
const newItem = { ...item };
// 如果有子项,标记为不可选中
if (newItem.children && newItem.children.length > 0) {
newItem.selectable = false;
// 递归处理子项
newItem.children = processTreeData(newItem.children);
} else {
// 叶子节点默认可选中(显式声明更清晰)
newItem.selectable = true;
}
return newItem;
});
};
// 计算属性:处理后的树数据
const processedMoreSelectOptions = computed(() => {
return processTreeData(props.moreSelect.options || []);
});
/**
* 计算属性根据 picker 类型动态生成快捷日期选项
*/
const computedScopeDatePresets = computed(() => {
const now = dayjs();
const presets: Array<{ label: string; value: [Dayjs, Dayjs] }> = [];
// 根据 picker 类型生成对应的快捷选项
switch (props.scopeDate.picker) {
case 'year':
// 年份选择器:只显示今年
presets.push({
label: '今年',
value: [now.startOf('year'), now]
});
break;
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;
case 'quarter':
// 季度选择器:显示最近一季度、今年
presets.push(
{
label: '最近一季度',
value: [now.subtract(1, 'quarter').startOf('quarter'), now.endOf('quarter')]
},
{
label: '今年',
value: [now.startOf('year'), now.endOf('quarter')]
}
);
break;
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;
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;
}
return presets;
});
/**
* 处理树节点选择事件
* 由于父节点已设置 selectable: false这里只会接收到叶子节点
* @param selectedKeys 选中的键值可能是字符串或数组
* @param info 节点信息对象本身就是节点不是包含 node 的对象
*/
const handleTreeSelect = (selectedKeys: string | string[], info: any) => {
// selectedKeys 可能是字符串(单选模式)或数组(多选模式)
let selectedValue: string;
if (typeof selectedKeys === 'string') {
// 单选模式selectedKeys 直接就是字符串
selectedValue = selectedKeys;
} else if (Array.isArray(selectedKeys)) {
// 多选模式:取第一个元素
selectedValue = selectedKeys[0];
} else {
// 兜底:尝试从 info 中获取
selectedValue = info?.value || '';
}
moreSelectValue.value = selectedValue;
};
/**
* 处理树节点展开/收起事件
* @param keys 展开的节点键值数组
*/
const handleTreeExpand = (keys: string[]) => {
expandedKeys.value = keys;
};
/**
* 查找节点的父节点路径
* @param treeData 树数据
* @param targetValue 目标节点的值
* @returns 父节点值的数组从根到直接父节点
*/
const findParentKeys = (treeData: any[], targetValue: string): string[] => {
const parentKeys: string[] = [];
const search = (nodes: any[], target: string, parents: string[]): boolean => {
for (const node of nodes) {
if (node.value === target) {
// 找到目标节点,记录所有父节点
parentKeys.push(...parents);
return true;
}
if (node.children && node.children.length > 0) {
// 递归搜索子节点,将当前节点加入父节点列表
if (search(node.children, target, [...parents, node.value])) {
return true;
}
}
}
return false;
};
search(treeData, targetValue, []);
return parentKeys;
};
/**
* 处理下拉框显示/隐藏变化事件
* 当下拉框打开且有选中值时自动展开该节点的所有父节点
* @param visible 下拉框是否可见
*/
const handleDropdownVisibleChange = (visible: boolean) => {
if (visible && moreSelectValue.value && processedMoreSelectOptions.value.length > 0) {
// 查找选中节点的所有父节点
const parentKeys = findParentKeys(processedMoreSelectOptions.value, moreSelectValue.value);
if (parentKeys.length > 0) {
// 合并现有的展开节点和父节点,去重
const mergedKeys = Array.from(new Set([...expandedKeys.value, ...parentKeys]));
expandedKeys.value = mergedKeys;
}
}
};
// 下拉选择框事件处理
const handleChange = (value: string) => {
2026-05-20 08:44:31 +08:00
// Handle change
};
const handleBlur = () => {
2026-05-20 08:44:31 +08:00
// Handle blur
};
const handleFocus = () => {
2026-05-20 08:44:31 +08:00
// Handle focus
};
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-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;
}
}
);
watch(
() => props.moreSelect.value,
(newVal) => {
if (newVal !== moreSelectValue.value) {
moreSelectValue.value = newVal;
// 如果有新值且树数据已加载,自动展开其父节点
if (newVal && processedMoreSelectOptions.value.length > 0) {
const parentKeys = findParentKeys(processedMoreSelectOptions.value, newVal);
if (parentKeys.length > 0) {
expandedKeys.value = Array.from(new Set([...expandedKeys.value, ...parentKeys]));
}
}
}
}
);
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;
// 检查是否需要更新(避免不必要的触发)
if (!currentRange ||
!currentRange[0].isSame(newRange[0]) ||
!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;
}
}
);
// 页面加载时执行的逻辑
onMounted(() => {
2026-05-20 08:44:31 +08:00
// 初始化时发送一次默认值
emitAllValues();
// 如果 tree-select 有默认值,预先展开其父节点
if (moreSelectValue.value && processedMoreSelectOptions.value.length > 0) {
const parentKeys = findParentKeys(processedMoreSelectOptions.value, moreSelectValue.value);
if (parentKeys.length > 0) {
expandedKeys.value = parentKeys;
}
}
});
</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
.title_icon {
display: inline-block;
margin-left: 5px;
cursor: pointer;
}
2026-05-12 08:47:27 +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-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;
}
}
}
.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>