mirror of
https://gitee.com/xiaonuobase/snowy.git
synced 2026-03-22 10:47:16 +08:00
【底座】table组件加入插槽内容计算,新增左右拖动组件
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div className="table-wrapper">
|
<div className="table-wrapper">
|
||||||
<div className="s-table-tool">
|
<div className="s-table-tool" v-if="hasToolbar">
|
||||||
<div className="s-table-tool-left">
|
<div className="s-table-tool-left">
|
||||||
<!-- 插槽操作按钮 -->
|
<!-- 插槽操作按钮 -->
|
||||||
<slot name="operator"></slot>
|
<slot name="operator"></slot>
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { tableProps } from 'ant-design-vue/es/table/Table.js'
|
import { tableProps } from 'ant-design-vue/es/table/Table.js'
|
||||||
import columnSetting from './columnSetting.vue'
|
import columnSetting from './columnSetting.vue'
|
||||||
import { useSlots } from 'vue'
|
import { useSlots, Comment, Fragment, Text } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { cloneDeep, get } from 'lodash-es'
|
import { cloneDeep, get } from 'lodash-es'
|
||||||
|
|
||||||
@@ -125,6 +125,45 @@
|
|||||||
const emit = defineEmits(['onExpand', 'onSelectionChange'])
|
const emit = defineEmits(['onExpand', 'onSelectionChange'])
|
||||||
const renderSlots = Object.keys(slots)
|
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(
|
const props = defineProps(
|
||||||
Object.assign({}, tableProps(), {
|
Object.assign({}, tableProps(), {
|
||||||
rowKey: {
|
rowKey: {
|
||||||
|
|||||||
202
snowy-admin-web/src/components/XnResizablePanel/README.md
Normal file
202
snowy-admin-web/src/components/XnResizablePanel/README.md
Normal 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
|
||||||
|
- 初始版本发布
|
||||||
|
- 支持水平和垂直分割
|
||||||
|
- 支持拖拽调整大小
|
||||||
|
- 支持最小/最大值限制
|
||||||
164
snowy-admin-web/src/components/XnResizablePanel/index.vue
Normal file
164
snowy-admin-web/src/components/XnResizablePanel/index.vue
Normal 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>
|
||||||
Reference in New Issue
Block a user