【底座】table组件加入插槽内容计算,新增左右拖动组件

This commit is contained in:
小诺
2025-11-08 16:58:39 +08:00
parent 8721fba496
commit 2b509efec3
3 changed files with 407 additions and 2 deletions

View File

@@ -1,6 +1,6 @@
<template>
<div className="table-wrapper">
<div className="s-table-tool">
<div className="s-table-tool" v-if="hasToolbar">
<div className="s-table-tool-left">
<!-- 插槽操作按钮 -->
<slot name="operator"></slot>
@@ -116,7 +116,7 @@
<script setup>
import { tableProps } from 'ant-design-vue/es/table/Table.js'
import columnSetting from './columnSetting.vue'
import { useSlots } from 'vue'
import { useSlots, Comment, Fragment, Text } from 'vue'
import { useRoute } from 'vue-router'
import { cloneDeep, get } from 'lodash-es'
@@ -125,6 +125,45 @@
const emit = defineEmits(['onExpand', 'onSelectionChange'])
const renderSlots = Object.keys(slots)
// 是否存在 operator 插槽内容(过滤掉空白、注释、空 Fragment
const hasOperatorContent = computed(() => {
const s = slots.operator
if (!s) return false
const vnodes = s() || []
const hasMeaningful = (nodes) => {
if (!Array.isArray(nodes)) return false
for (const v of nodes) {
if (v == null) continue
// 注释节点跳过
if (v.type === Comment) continue
// 文本节点:仅当非空白文本才算有效
if (v.type === Text) {
if (typeof v.children === 'string' && v.children.trim() !== '') return true
continue
}
// Fragment递归检查子节点
if (v.type === Fragment) {
if (hasMeaningful(v.children)) return true
continue
}
// 其他元素/组件节点视为有效内容
return true
}
return false
}
return hasMeaningful(vnodes)
})
// 工具栏显示:有 operator 内容 或 开启任一工具按钮
const hasToolbar = computed(() => {
return (
hasOperatorContent.value ||
props.toolConfig.striped ||
props.toolConfig.refresh ||
props.toolConfig.height ||
props.toolConfig.columnSetting
)
})
const props = defineProps(
Object.assign({}, tableProps(), {
rowKey: {

View File

@@ -0,0 +1,202 @@
# ResizablePanel 可拖拽调整大小面板组件
一个基于 Vue 3 的可拖拽调整大小的面板组件,支持水平和垂直分割布局。
## 特性
- 🎯 **灵活布局**支持水平row和垂直column分割
- 📏 **尺寸控制**:可设置初始大小、最小值和最大值
- 🎨 **平滑体验**:流畅的拖拽动画和视觉反馈
- 📱 **响应式设计**:自适应不同屏幕尺寸
- 🔧 **易于集成**:简单的 API 设计,易于在项目中使用
## 安装
`ResizablePanel` 组件文件复制到你的项目中:
```
src/components/ResizablePanel/index.vue
```
## 基本用法
### 水平分割布局
```vue
<template>
<ResizablePanel
direction="row"
:initial-size="300"
:min-size="200"
:max-size="500"
@resize="handleResize"
>
<template #first>
<div class="left-panel">
左侧内容
</div>
</template>
<template #second>
<div class="right-panel">
右侧内容
</div>
</template>
</ResizablePanel>
</template>
<script setup>
import ResizablePanel from '@/components/ResizablePanel/index.vue'
const handleResize = (size) => {
console.log('面板大小变化:', size)
}
</script>
```
### 垂直分割布局
```vue
<template>
<ResizablePanel
direction="column"
:initial-size="200"
:min-size="100"
:max-size="400"
>
<template #first>
<div class="top-panel">
顶部内容
</div>
</template>
<template #second>
<div class="bottom-panel">
底部内容
</div>
</template>
</ResizablePanel>
</template>
```
## API
### Props
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `direction` | `String` | `'row'` | 分割方向,可选值:`'row'`(水平)、`'column'`(垂直) |
| `initial-size` | `Number` | `300` | 第一个面板的初始大小px |
| `min-size` | `Number` | `100` | 第一个面板的最小大小px |
| `max-size` | `Number` | `500` | 第一个面板的最大大小px |
### Events
| 事件名 | 参数 | 说明 |
|--------|------|------|
| `resize` | `(size: number)` | 面板大小变化时触发,参数为第一个面板的当前大小 |
### Slots
| 插槽名 | 说明 |
|--------|------|
| `first` | 第一个面板的内容(水平布局时为左侧,垂直布局时为顶部) |
| `second` | 第二个面板的内容(水平布局时为右侧,垂直布局时为底部) |
## 样式定制
组件使用了 CSS 变量,你可以通过覆盖这些变量来自定义样式:
```css
.resizable-panel {
/* 分割线颜色 */
--divider-color: #e8e8e8;
/* 分割线悬停颜色 */
--divider-hover-color: #1890ff;
/* 分割线宽度 */
--divider-width: 4px;
/* 过渡动画时间 */
--transition-duration: 0.2s;
}
```
## 高级用法
### 嵌套使用
```vue
<template>
<ResizablePanel direction="row" :initial-size="250">
<template #first>
<div class="sidebar">侧边栏</div>
</template>
<template #second>
<ResizablePanel direction="column" :initial-size="200">
<template #first>
<div class="header">头部</div>
</template>
<template #second>
<div class="content">主要内容</div>
</template>
</ResizablePanel>
</template>
</ResizablePanel>
</template>
```
### 动态控制
```vue
<template>
<div>
<button @click="resetSize">重置大小</button>
<ResizablePanel
ref="panelRef"
direction="row"
:initial-size="panelSize"
@resize="handleResize"
>
<!-- 内容 -->
</ResizablePanel>
</div>
</template>
<script setup>
import { ref } from 'vue'
const panelSize = ref(300)
const panelRef = ref()
const handleResize = (size) => {
panelSize.value = size
}
const resetSize = () => {
panelSize.value = 300
// 如果需要强制更新组件,可以使用 key 或其他方法
}
</script>
```
## 注意事项
1. **容器高度**:确保父容器有明确的高度,否则垂直布局可能无法正常工作
2. **最小/最大值**:合理设置 `min-size``max-size`,避免内容被过度压缩
3. **性能优化**:在大量数据或复杂布局中,考虑使用 `v-show` 而不是 `v-if` 来控制面板显示
4. **移动端适配**:在移动设备上,建议增大分割线的触摸区域
## 浏览器兼容性
- Chrome 60+
- Firefox 55+
- Safari 12+
- Edge 79+
## 更新日志
### v1.0.0
- 初始版本发布
- 支持水平和垂直分割
- 支持拖拽调整大小
- 支持最小/最大值限制

View File

@@ -0,0 +1,164 @@
<template>
<div class="resizable-panel" :style="{ display: 'flex', flexDirection: direction }">
<div
class="panel-left"
:style="{
[sizeProperty]: leftSize + 'px',
minWidth: direction === 'row' ? minSize + 'px' : 'auto',
minHeight: direction === 'column' ? minSize + 'px' : 'auto'
}"
>
<slot name="left"></slot>
</div>
<div
class="resizer"
:class="{ 'resizer-horizontal': direction === 'row', 'resizer-vertical': direction === 'column' }"
@mousedown="startResize"
></div>
<div class="panel-right" :style="{ flex: 1 }">
<slot name="right"></slot>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
const props = defineProps({
// 初始左侧面板大小
initialSize: {
type: Number,
default: 200
},
// 最小大小
minSize: {
type: Number,
default: 100
},
// 最大大小
maxSize: {
type: Number,
default: 500
},
// 方向:'row' 水平分割,'column' 垂直分割
direction: {
type: String,
default: 'row',
validator: (value) => ['row', 'column'].includes(value)
}
})
const emit = defineEmits(['resize'])
const leftSize = ref(props.initialSize)
const isResizing = ref(false)
// 根据方向确定使用的CSS属性
const sizeProperty = computed(() => {
return props.direction === 'row' ? 'width' : 'height'
})
const startResize = (e) => {
isResizing.value = true
document.addEventListener('mousemove', handleResize)
document.addEventListener('mouseup', stopResize)
e.preventDefault()
}
const handleResize = (e) => {
if (!isResizing.value) return
const container = e.currentTarget?.closest?.('.resizable-panel') || document.querySelector('.resizable-panel')
if (!container) return
const rect = container.getBoundingClientRect()
let newSize
if (props.direction === 'row') {
newSize = e.clientX - rect.left
} else {
newSize = e.clientY - rect.top
}
// 限制在最小值和最大值之间
newSize = Math.max(props.minSize, Math.min(props.maxSize, newSize))
leftSize.value = newSize
emit('resize', newSize)
}
const stopResize = () => {
isResizing.value = false
document.removeEventListener('mousemove', handleResize)
document.removeEventListener('mouseup', stopResize)
}
onUnmounted(() => {
document.removeEventListener('mousemove', handleResize)
document.removeEventListener('mouseup', stopResize)
})
// 暴露方法供外部调用
defineExpose({
setSize: (size) => {
leftSize.value = Math.max(props.minSize, Math.min(props.maxSize, size))
},
getSize: () => leftSize.value
})
</script>
<style scoped>
.resizable-panel {
height: 100%;
width: 100%;
}
.panel-left {
overflow: auto;
background: var(--component-background);
/* 隐藏滚动条但保持滚动功能 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE 和 Edge */
}
.panel-left::-webkit-scrollbar {
display: none; /* Chrome, Safari 和 Opera */
}
.panel-right {
overflow: auto;
background: var(--component-background);
/* 隐藏滚动条但保持滚动功能 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE 和 Edge */
}
.panel-right::-webkit-scrollbar {
display: none; /* Chrome, Safari 和 Opera */
}
.resizer {
background: var(--border-color-base);
cursor: col-resize;
user-select: none;
transition: background-color 0.2s;
}
.resizer:hover {
background: var(--primary-color);
}
.resizer-horizontal {
width: 4px;
cursor: col-resize;
}
.resizer-vertical {
height: 4px;
cursor: row-resize;
}
.resizer:active {
background: var(--primary-color);
}
</style>