1
This commit is contained in:
3
draggable-panels/.vscode/extensions.json
vendored
Normal file
3
draggable-panels/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
||||||
@@ -7,11 +7,6 @@
|
|||||||
"id": "0i69gg5",
|
"id": "0i69gg5",
|
||||||
"title": "资源管理器",
|
"title": "资源管理器",
|
||||||
"content": "左侧面板内容1"
|
"content": "左侧面板内容1"
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "9uc5qy1",
|
|
||||||
"title": "新窗口 2",
|
|
||||||
"content": "新窗口内容"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"activeTabId": "0i69gg5"
|
"activeTabId": "0i69gg5"
|
||||||
@@ -20,22 +15,25 @@
|
|||||||
"id": "center",
|
"id": "center",
|
||||||
"tabs": [
|
"tabs": [
|
||||||
{
|
{
|
||||||
"id": "auteqok",
|
"id": "2emt1si",
|
||||||
"title": "欢迎页",
|
"title": "文本编辑器",
|
||||||
"content": "中间面板内容1"
|
"content": "新窗口内容",
|
||||||
|
"materialId": "TextEditor"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "c08lqdq",
|
"id": "mxfx11j",
|
||||||
"title": "新窗口 3",
|
"title": "树形展示器",
|
||||||
"content": "新窗口内容"
|
"content": "新窗口内容",
|
||||||
|
"materialId": "TreeViewer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "c9nw7xj",
|
"id": "jln5iq9",
|
||||||
"title": "新窗口 3",
|
"title": "测试组件B",
|
||||||
"content": "新窗口内容"
|
"content": "新窗口内容",
|
||||||
|
"materialId": "TestWidget2"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"activeTabId": "cibltif"
|
"activeTabId": "2emt1si"
|
||||||
},
|
},
|
||||||
"rightPanel": {
|
"rightPanel": {
|
||||||
"id": "right",
|
"id": "right",
|
||||||
@@ -46,13 +44,20 @@
|
|||||||
"content": "新窗口内容"
|
"content": "新窗口内容"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cibltif",
|
"id": "vrh9bl2",
|
||||||
"title": "新窗口 4",
|
"title": "数据表格",
|
||||||
"content": "新窗口内容"
|
"content": "新窗口内容",
|
||||||
|
"materialId": "DataTable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "y2iwzgl",
|
||||||
|
"title": "测试组件A",
|
||||||
|
"content": "新窗口内容",
|
||||||
|
"materialId": "TestWidget1"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"activeTabId": "ojaw0e3"
|
"activeTabId": "ojaw0e3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lastUpdated": "2025-12-20T11:47:26.835Z"
|
"lastUpdated": "2025-12-20T12:28:20.283Z"
|
||||||
}
|
}
|
||||||
@@ -1,80 +1,179 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
import { usePanelStore } from '../stores/panelStore'
|
import { usePanelStore } from '../stores/panelStore'
|
||||||
|
import { materialList } from '../materials'
|
||||||
|
|
||||||
const panelStore = usePanelStore()
|
const panelStore = usePanelStore()
|
||||||
|
|
||||||
const handleAddWindow = () => {
|
// 菜单状态
|
||||||
panelStore.addNewWindow()
|
const isWindowMenuOpen = ref(false)
|
||||||
|
|
||||||
|
// 获取所有物料组件列表
|
||||||
|
const allMaterials = computed(() => materialList)
|
||||||
|
|
||||||
|
// 获取可用的物料组件(未打开的)
|
||||||
|
const availableMaterials = computed(() => panelStore.availableMaterials)
|
||||||
|
|
||||||
|
// 检查物料是否已打开
|
||||||
|
const isMaterialOpened = (materialId: string) => {
|
||||||
|
return panelStore.isMaterialOpened(materialId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开物料组件
|
||||||
|
const handleOpenMaterial = (materialId: string) => {
|
||||||
|
panelStore.openMaterial(materialId)
|
||||||
|
isWindowMenuOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开项目(无实际功能)
|
||||||
|
const handleOpenProject = () => {
|
||||||
|
console.log('打开项目功能待实现')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<div class="header-left">
|
<div class="menu-bar">
|
||||||
<span class="app-title">拖拽面板编辑器</span>
|
<!-- 打开项目菜单 -->
|
||||||
|
<div class="menu-item" @click="handleOpenProject">
|
||||||
|
<span>打开项目</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-center">
|
|
||||||
<button class="add-tab-btn" @click="handleAddWindow">
|
<!-- 窗口菜单 -->
|
||||||
<span class="icon">+</span>
|
<div
|
||||||
<span>新增子窗口</span>
|
class="menu-item"
|
||||||
</button>
|
@mouseenter="isWindowMenuOpen = true"
|
||||||
|
@mouseleave="isWindowMenuOpen = false"
|
||||||
|
>
|
||||||
|
<span>窗口</span>
|
||||||
|
|
||||||
|
<!-- 下拉菜单 -->
|
||||||
|
<div class="dropdown-menu" v-if="isWindowMenuOpen">
|
||||||
|
<div class="menu-section-title">可用组件</div>
|
||||||
|
<div
|
||||||
|
v-for="material in allMaterials"
|
||||||
|
:key="material.id"
|
||||||
|
class="dropdown-item"
|
||||||
|
:class="{ disabled: isMaterialOpened(material.id) }"
|
||||||
|
@click="!isMaterialOpened(material.id) && handleOpenMaterial(material.id)"
|
||||||
|
>
|
||||||
|
<span class="item-name">{{ material.name }}</span>
|
||||||
|
<span class="item-status" v-if="isMaterialOpened(material.id)">已打开</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="allMaterials.length === 0" class="dropdown-empty">
|
||||||
|
暂无可用组件
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-title">
|
||||||
|
<span>拖拽面板编辑器</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="header-right"></div>
|
<div class="header-right"></div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.app-header {
|
.app-header {
|
||||||
height: 48px;
|
height: 32px;
|
||||||
background: #1e1e1e;
|
background: #3c3c3c;
|
||||||
border-bottom: 1px solid #3c3c3c;
|
border-bottom: 1px solid #2d2d2d;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
padding: 0;
|
||||||
padding: 0 16px;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-left {
|
.menu-bar {
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-title {
|
|
||||||
color: #cccccc;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-center {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
position: relative;
|
||||||
|
padding: 0 12px;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background: #505050;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
min-width: 200px;
|
||||||
|
background: #252526;
|
||||||
|
border: 1px solid #3c3c3c;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-section-title {
|
||||||
|
padding: 6px 12px;
|
||||||
|
color: #888888;
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover:not(.disabled) {
|
||||||
|
background: #094771;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item.disabled {
|
||||||
|
color: #666666;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-status {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #888888;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-empty {
|
||||||
|
padding: 12px;
|
||||||
|
color: #666666;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
color: #999999;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-right {
|
.header-right {
|
||||||
flex: 1;
|
width: 100px;
|
||||||
}
|
|
||||||
|
|
||||||
.add-tab-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 6px 16px;
|
|
||||||
background: #0e639c;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 13px;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-tab-btn:hover {
|
|
||||||
background: #1177bb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-tab-btn .icon {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,28 +1,54 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { usePanelStore } from '../stores/panelStore'
|
import { usePanelStore } from '../stores/panelStore'
|
||||||
import Panel from './Panel.vue'
|
import Panel from './Panel.vue'
|
||||||
|
import Resizer from './Resizer.vue'
|
||||||
|
|
||||||
const panelStore = usePanelStore()
|
const panelStore = usePanelStore()
|
||||||
|
|
||||||
const leftPanel = computed(() => panelStore.layout.leftPanel)
|
const leftPanel = computed(() => panelStore.layout.leftPanel)
|
||||||
const centerPanel = computed(() => panelStore.layout.centerPanel)
|
const centerPanel = computed(() => panelStore.layout.centerPanel)
|
||||||
const rightPanel = computed(() => panelStore.layout.rightPanel)
|
const rightPanel = computed(() => panelStore.layout.rightPanel)
|
||||||
|
|
||||||
|
// 面板宽度
|
||||||
|
const leftWidth = ref(250)
|
||||||
|
const rightWidth = ref(250)
|
||||||
|
|
||||||
|
const MIN_WIDTH = 150
|
||||||
|
const MAX_WIDTH = 500
|
||||||
|
|
||||||
|
const onLeftResize = (delta: number) => {
|
||||||
|
const newWidth = leftWidth.value + delta
|
||||||
|
if (newWidth >= MIN_WIDTH && newWidth <= MAX_WIDTH) {
|
||||||
|
leftWidth.value = newWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRightResize = (delta: number) => {
|
||||||
|
const newWidth = rightWidth.value - delta
|
||||||
|
if (newWidth >= MIN_WIDTH && newWidth <= MAX_WIDTH) {
|
||||||
|
rightWidth.value = newWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="main-layout">
|
<div class="main-layout">
|
||||||
<div class="panel-container left-panel">
|
<div class="panel-container left-panel" :style="{ width: leftWidth + 'px' }">
|
||||||
<div class="panel-header">左侧面板</div>
|
<div class="panel-header">左侧面板</div>
|
||||||
<Panel :panel="leftPanel" />
|
<Panel :panel="leftPanel" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Resizer @resize="onLeftResize" />
|
||||||
|
|
||||||
<div class="panel-container center-panel">
|
<div class="panel-container center-panel">
|
||||||
<div class="panel-header">中间面板</div>
|
<div class="panel-header">中间面板</div>
|
||||||
<Panel :panel="centerPanel" />
|
<Panel :panel="centerPanel" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-container right-panel">
|
<Resizer @resize="onRightResize" />
|
||||||
|
|
||||||
|
<div class="panel-container right-panel" :style="{ width: rightWidth + 'px' }">
|
||||||
<div class="panel-header">右侧面板</div>
|
<div class="panel-header">右侧面板</div>
|
||||||
<Panel :panel="rightPanel" />
|
<Panel :panel="rightPanel" />
|
||||||
</div>
|
</div>
|
||||||
@@ -33,7 +59,6 @@ const rightPanel = computed(() => panelStore.layout.rightPanel)
|
|||||||
.main-layout {
|
.main-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
gap: 4px;
|
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
background: #181818;
|
background: #181818;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -48,18 +73,18 @@ const rightPanel = computed(() => panelStore.layout.rightPanel)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.left-panel {
|
.left-panel {
|
||||||
flex: 0 0 250px;
|
flex-shrink: 0;
|
||||||
min-width: 200px;
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.center-panel {
|
.center-panel {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 300px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-panel {
|
.right-panel {
|
||||||
flex: 0 0 250px;
|
flex-shrink: 0;
|
||||||
min-width: 200px;
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-header {
|
.panel-header {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import draggable from 'vuedraggable'
|
import draggable from 'vuedraggable'
|
||||||
import { usePanelStore } from '../stores/panelStore'
|
import { usePanelStore } from '../stores/panelStore'
|
||||||
|
import { getMaterialComponent } from '../materials'
|
||||||
import type { Panel, TabItem } from '../types'
|
import type { Panel, TabItem } from '../types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -10,20 +11,20 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const panelStore = usePanelStore()
|
const panelStore = usePanelStore()
|
||||||
|
|
||||||
// 使用computed来获取tabs的响应式引用
|
|
||||||
const tabs = computed({
|
|
||||||
get: () => props.panel.tabs,
|
|
||||||
set: (value: TabItem[]) => {
|
|
||||||
props.panel.tabs = value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const activeTabId = computed(() => props.panel.activeTabId)
|
const activeTabId = computed(() => props.panel.activeTabId)
|
||||||
|
|
||||||
const activeTab = computed(() =>
|
const activeTab = computed(() =>
|
||||||
props.panel.tabs.find(t => t.id === activeTabId.value)
|
props.panel.tabs.find(t => t.id === activeTabId.value)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 获取当前活跃Tab的物料组件
|
||||||
|
const activeMaterialComponent = computed(() => {
|
||||||
|
if (activeTab.value?.materialId) {
|
||||||
|
return getMaterialComponent(activeTab.value.materialId)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
const handleTabClick = (tabId: string) => {
|
const handleTabClick = (tabId: string) => {
|
||||||
panelStore.setActiveTab(props.panel.id, tabId)
|
panelStore.setActiveTab(props.panel.id, tabId)
|
||||||
}
|
}
|
||||||
@@ -32,26 +33,19 @@ const handleCloseTab = (tabId: string, event: Event) => {
|
|||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
panelStore.closeTab(props.panel.id, tabId)
|
panelStore.closeTab(props.panel.id, tabId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拖拽结束后更新状态
|
|
||||||
const onDragEnd = () => {
|
|
||||||
// vuedraggable会自动更新数组,Pinia的watch会自动触发保存
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-tabs">
|
<div class="panel-tabs">
|
||||||
<draggable
|
<draggable
|
||||||
v-model="tabs"
|
:list="panel.tabs"
|
||||||
:group="{ name: 'tabs', pull: true, put: true }"
|
group="tabs"
|
||||||
item-key="id"
|
item-key="id"
|
||||||
class="tabs-container"
|
class="tabs-container"
|
||||||
:animation="200"
|
:animation="150"
|
||||||
ghost-class="tab-ghost"
|
ghost-class="tab-ghost"
|
||||||
chosen-class="tab-chosen"
|
|
||||||
drag-class="tab-drag"
|
drag-class="tab-drag"
|
||||||
@end="onDragEnd"
|
|
||||||
>
|
>
|
||||||
<template #item="{ element }">
|
<template #item="{ element }">
|
||||||
<div
|
<div
|
||||||
@@ -71,10 +65,18 @@ const onDragEnd = () => {
|
|||||||
</draggable>
|
</draggable>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-content">
|
<div class="panel-content">
|
||||||
<div v-if="activeTab" class="content-wrapper">
|
<!-- 物料组件渲染 -->
|
||||||
|
<component
|
||||||
|
v-if="activeMaterialComponent"
|
||||||
|
:is="activeMaterialComponent"
|
||||||
|
class="material-wrapper"
|
||||||
|
/>
|
||||||
|
<!-- 普通内容 -->
|
||||||
|
<div v-else-if="activeTab" class="content-wrapper">
|
||||||
<p>{{ activeTab.content }}</p>
|
<p>{{ activeTab.content }}</p>
|
||||||
<p class="tab-info">Tab ID: {{ activeTab.id }}</p>
|
<p class="tab-info">Tab ID: {{ activeTab.id }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 空状态 -->
|
||||||
<div v-else class="empty-panel">
|
<div v-else class="empty-panel">
|
||||||
<span>暂无内容</span>
|
<span>暂无内容</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,9 +102,8 @@ const onDragEnd = () => {
|
|||||||
|
|
||||||
.tabs-container {
|
.tabs-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
min-height: 35px;
|
min-height: 35px;
|
||||||
align-items: flex-end;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item {
|
.tab-item {
|
||||||
@@ -113,12 +114,16 @@ const onDragEnd = () => {
|
|||||||
background: #2d2d2d;
|
background: #2d2d2d;
|
||||||
color: #969696;
|
color: #969696;
|
||||||
border-right: 1px solid #3c3c3c;
|
border-right: 1px solid #3c3c3c;
|
||||||
cursor: pointer;
|
cursor: grab;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: all 0.15s;
|
transition: background 0.15s, color 0.15s;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tab-item:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
.tab-item:hover {
|
.tab-item:hover {
|
||||||
background: #323232;
|
background: #323232;
|
||||||
color: #cccccc;
|
color: #cccccc;
|
||||||
@@ -135,6 +140,7 @@ const onDragEnd = () => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-close {
|
.tab-close {
|
||||||
@@ -159,11 +165,15 @@ const onDragEnd = () => {
|
|||||||
|
|
||||||
.panel-content {
|
.panel-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 16px;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.material-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
|
padding: 16px;
|
||||||
color: #cccccc;
|
color: #cccccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,17 +193,20 @@ const onDragEnd = () => {
|
|||||||
|
|
||||||
/* 拖拽样式 */
|
/* 拖拽样式 */
|
||||||
.tab-ghost {
|
.tab-ghost {
|
||||||
opacity: 0.5;
|
opacity: 0.4;
|
||||||
background: #0e639c !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-chosen {
|
|
||||||
background: #0e639c !important;
|
background: #0e639c !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-drag {
|
.tab-drag {
|
||||||
opacity: 0.9;
|
opacity: 1;
|
||||||
transform: rotate(2deg);
|
background: #094771 !important;
|
||||||
|
color: white !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sortable 拖拽占位符 */
|
||||||
|
.sortable-chosen {
|
||||||
|
background: #0e639c !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
57
draggable-panels/src/components/Resizer.vue
Normal file
57
draggable-panels/src/components/Resizer.vue
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'resize', delta: number): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isDragging = ref(false)
|
||||||
|
const startX = ref(0)
|
||||||
|
|
||||||
|
const onMouseDown = (e: MouseEvent) => {
|
||||||
|
isDragging.value = true
|
||||||
|
startX.value = e.clientX
|
||||||
|
document.addEventListener('mousemove', onMouseMove)
|
||||||
|
document.addEventListener('mouseup', onMouseUp)
|
||||||
|
document.body.style.cursor = 'col-resize'
|
||||||
|
document.body.style.userSelect = 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!isDragging.value) return
|
||||||
|
const delta = e.clientX - startX.value
|
||||||
|
startX.value = e.clientX
|
||||||
|
emit('resize', delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
isDragging.value = false
|
||||||
|
document.removeEventListener('mousemove', onMouseMove)
|
||||||
|
document.removeEventListener('mouseup', onMouseUp)
|
||||||
|
document.body.style.cursor = ''
|
||||||
|
document.body.style.userSelect = ''
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="resizer"
|
||||||
|
:class="{ dragging: isDragging }"
|
||||||
|
@mousedown="onMouseDown"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.resizer {
|
||||||
|
width: 4px;
|
||||||
|
background: #181818;
|
||||||
|
cursor: col-resize;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizer:hover,
|
||||||
|
.resizer.dragging {
|
||||||
|
background: #007acc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
17
draggable-panels/src/materials/DataTable/index.json
Normal file
17
draggable-panels/src/materials/DataTable/index.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "数据表格",
|
||||||
|
"description": "展示属性和值的简单表格组件",
|
||||||
|
"props": {
|
||||||
|
"columns": ["属性", "值"],
|
||||||
|
"data": [
|
||||||
|
{ "property": "项目名称", "value": "draggable-panels" },
|
||||||
|
{ "property": "框架", "value": "Vue 3" },
|
||||||
|
{ "property": "语言", "value": "TypeScript" },
|
||||||
|
{ "property": "构建工具", "value": "Vite" },
|
||||||
|
{ "property": "状态管理", "value": "Pinia" },
|
||||||
|
{ "property": "版本", "value": "1.0.0" },
|
||||||
|
{ "property": "作者", "value": "Developer" },
|
||||||
|
{ "property": "许可证", "value": "MIT" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
86
draggable-panels/src/materials/DataTable/index.vue
Normal file
86
draggable-panels/src/materials/DataTable/index.vue
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import config from './index.json'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
materialProps?: Record<string, any>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const mergedProps = { ...config.props, ...props.materialProps }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="data-table">
|
||||||
|
<div class="table-header">
|
||||||
|
<span class="title">{{ config.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="table-body">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th v-for="col in mergedProps.columns" :key="col">{{ col }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(row, index) in mergedProps.data" :key="index">
|
||||||
|
<td>{{ row.property }}</td>
|
||||||
|
<td>{{ row.value }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.data-table {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #2d2d2d;
|
||||||
|
border-bottom: 1px solid #3c3c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th,
|
||||||
|
.table td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #3c3c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #cccccc;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:hover {
|
||||||
|
background: #2a2d2e;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
9
draggable-panels/src/materials/TestWidget1/index.json
Normal file
9
draggable-panels/src/materials/TestWidget1/index.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "测试组件A",
|
||||||
|
"description": "用于测试的简单文本显示组件A",
|
||||||
|
"props": {
|
||||||
|
"title": "测试组件 A",
|
||||||
|
"content": "这是测试组件A的内容区域。\n\n此组件用于验证物料系统的正确性。",
|
||||||
|
"color": "#4fc3f7"
|
||||||
|
}
|
||||||
|
}
|
||||||
71
draggable-panels/src/materials/TestWidget1/index.vue
Normal file
71
draggable-panels/src/materials/TestWidget1/index.vue
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import config from './index.json'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
materialProps?: Record<string, any>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const mergedProps = { ...config.props, ...props.materialProps }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="test-widget">
|
||||||
|
<div class="widget-header" :style="{ borderLeftColor: mergedProps.color }">
|
||||||
|
<span class="title">{{ mergedProps.title }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="widget-body">
|
||||||
|
<p class="content">{{ mergedProps.content }}</p>
|
||||||
|
<div class="badge" :style="{ background: mergedProps.color }">
|
||||||
|
{{ config.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.test-widget {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #2d2d2d;
|
||||||
|
border-bottom: 1px solid #3c3c3c;
|
||||||
|
border-left: 3px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
text-align: center;
|
||||||
|
white-space: pre-line;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
9
draggable-panels/src/materials/TestWidget2/index.json
Normal file
9
draggable-panels/src/materials/TestWidget2/index.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "测试组件B",
|
||||||
|
"description": "用于测试的简单文本显示组件B",
|
||||||
|
"props": {
|
||||||
|
"title": "测试组件 B",
|
||||||
|
"content": "这是测试组件B的内容区域。\n\n组件之间可以通过拖拽进行位置调整。",
|
||||||
|
"color": "#81c784"
|
||||||
|
}
|
||||||
|
}
|
||||||
71
draggable-panels/src/materials/TestWidget2/index.vue
Normal file
71
draggable-panels/src/materials/TestWidget2/index.vue
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import config from './index.json'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
materialProps?: Record<string, any>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const mergedProps = { ...config.props, ...props.materialProps }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="test-widget">
|
||||||
|
<div class="widget-header" :style="{ borderLeftColor: mergedProps.color }">
|
||||||
|
<span class="title">{{ mergedProps.title }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="widget-body">
|
||||||
|
<p class="content">{{ mergedProps.content }}</p>
|
||||||
|
<div class="badge" :style="{ background: mergedProps.color }">
|
||||||
|
{{ config.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.test-widget {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #2d2d2d;
|
||||||
|
border-bottom: 1px solid #3c3c3c;
|
||||||
|
border-left: 3px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
text-align: center;
|
||||||
|
white-space: pre-line;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
9
draggable-panels/src/materials/TestWidget3/index.json
Normal file
9
draggable-panels/src/materials/TestWidget3/index.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "测试组件C",
|
||||||
|
"description": "用于测试的简单文本显示组件C",
|
||||||
|
"props": {
|
||||||
|
"title": "测试组件 C",
|
||||||
|
"content": "这是测试组件C的内容区域。\n\n每个组件全局只能显示一次。",
|
||||||
|
"color": "#ffb74d"
|
||||||
|
}
|
||||||
|
}
|
||||||
71
draggable-panels/src/materials/TestWidget3/index.vue
Normal file
71
draggable-panels/src/materials/TestWidget3/index.vue
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import config from './index.json'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
materialProps?: Record<string, any>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const mergedProps = { ...config.props, ...props.materialProps }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="test-widget">
|
||||||
|
<div class="widget-header" :style="{ borderLeftColor: mergedProps.color }">
|
||||||
|
<span class="title">{{ mergedProps.title }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="widget-body">
|
||||||
|
<p class="content">{{ mergedProps.content }}</p>
|
||||||
|
<div class="badge" :style="{ background: mergedProps.color }">
|
||||||
|
{{ config.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.test-widget {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #2d2d2d;
|
||||||
|
border-bottom: 1px solid #3c3c3c;
|
||||||
|
border-left: 3px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
text-align: center;
|
||||||
|
white-space: pre-line;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
9
draggable-panels/src/materials/TextEditor/index.json
Normal file
9
draggable-panels/src/materials/TextEditor/index.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "文本编辑器",
|
||||||
|
"description": "一个简单的文本编辑框组件,支持多行文本输入",
|
||||||
|
"props": {
|
||||||
|
"placeholder": "请输入文本内容...",
|
||||||
|
"defaultValue": "这是默认的文本内容。\n\n你可以在这里编辑任何文本。",
|
||||||
|
"rows": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
77
draggable-panels/src/materials/TextEditor/index.vue
Normal file
77
draggable-panels/src/materials/TextEditor/index.vue
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import config from './index.json'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
materialProps?: Record<string, any>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const mergedProps = { ...config.props, ...props.materialProps }
|
||||||
|
const textContent = ref(mergedProps.defaultValue)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="text-editor">
|
||||||
|
<div class="editor-header">
|
||||||
|
<span class="title">{{ config.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="editor-body">
|
||||||
|
<textarea
|
||||||
|
v-model="textContent"
|
||||||
|
:placeholder="mergedProps.placeholder"
|
||||||
|
:rows="mergedProps.rows"
|
||||||
|
class="editor-textarea"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.text-editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-header {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #2d2d2d;
|
||||||
|
border-bottom: 1px solid #3c3c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #252526;
|
||||||
|
color: #d4d4d4;
|
||||||
|
border: 1px solid #3c3c3c;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
resize: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-textarea:focus {
|
||||||
|
border-color: #007acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-textarea::placeholder {
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
36
draggable-panels/src/materials/TreeViewer/index.json
Normal file
36
draggable-panels/src/materials/TreeViewer/index.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "树形展示器",
|
||||||
|
"description": "用于展示树形结构数据的组件",
|
||||||
|
"props": {
|
||||||
|
"treeData": [
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"label": "项目根目录",
|
||||||
|
"expanded": true,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "1-1",
|
||||||
|
"label": "src",
|
||||||
|
"expanded": true,
|
||||||
|
"children": [
|
||||||
|
{ "id": "1-1-1", "label": "components" },
|
||||||
|
{ "id": "1-1-2", "label": "stores" },
|
||||||
|
{ "id": "1-1-3", "label": "types" },
|
||||||
|
{ "id": "1-1-4", "label": "App.vue" },
|
||||||
|
{ "id": "1-1-5", "label": "main.ts" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1-2",
|
||||||
|
"label": "public",
|
||||||
|
"children": [
|
||||||
|
{ "id": "1-2-1", "label": "favicon.ico" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "id": "1-3", "label": "package.json" },
|
||||||
|
{ "id": "1-4", "label": "vite.config.ts" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
165
draggable-panels/src/materials/TreeViewer/index.vue
Normal file
165
draggable-panels/src/materials/TreeViewer/index.vue
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import config from './index.json'
|
||||||
|
|
||||||
|
interface TreeNode {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
expanded?: boolean
|
||||||
|
children?: TreeNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
materialProps?: Record<string, any>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const mergedProps = { ...config.props, ...props.materialProps }
|
||||||
|
|
||||||
|
const expandedNodes = ref<Set<string>>(new Set())
|
||||||
|
|
||||||
|
// 初始化展开状态
|
||||||
|
const initExpanded = (nodes: TreeNode[]) => {
|
||||||
|
nodes.forEach(node => {
|
||||||
|
if (node.expanded) {
|
||||||
|
expandedNodes.value.add(node.id)
|
||||||
|
}
|
||||||
|
if (node.children) {
|
||||||
|
initExpanded(node.children)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
initExpanded(mergedProps.treeData)
|
||||||
|
|
||||||
|
const toggleNode = (nodeId: string) => {
|
||||||
|
if (expandedNodes.value.has(nodeId)) {
|
||||||
|
expandedNodes.value.delete(nodeId)
|
||||||
|
} else {
|
||||||
|
expandedNodes.value.add(nodeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExpanded = (nodeId: string) => expandedNodes.value.has(nodeId)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="tree-viewer">
|
||||||
|
<div class="viewer-header">
|
||||||
|
<span class="title">{{ config.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="viewer-body">
|
||||||
|
<div class="tree-container">
|
||||||
|
<template v-for="node in mergedProps.treeData" :key="node.id">
|
||||||
|
<div class="tree-node-wrapper">
|
||||||
|
<div
|
||||||
|
class="tree-node"
|
||||||
|
@click="toggleNode(node.id)"
|
||||||
|
>
|
||||||
|
<span class="expand-icon" v-if="node.children?.length">
|
||||||
|
{{ isExpanded(node.id) ? '▼' : '▶' }}
|
||||||
|
</span>
|
||||||
|
<span class="expand-icon empty" v-else></span>
|
||||||
|
<span class="node-label">{{ node.label }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tree-children" v-if="node.children && isExpanded(node.id)">
|
||||||
|
<template v-for="child in node.children" :key="child.id">
|
||||||
|
<div class="tree-node-wrapper">
|
||||||
|
<div
|
||||||
|
class="tree-node level-1"
|
||||||
|
@click="toggleNode(child.id)"
|
||||||
|
>
|
||||||
|
<span class="expand-icon" v-if="child.children?.length">
|
||||||
|
{{ isExpanded(child.id) ? '▼' : '▶' }}
|
||||||
|
</span>
|
||||||
|
<span class="expand-icon empty" v-else></span>
|
||||||
|
<span class="node-label">{{ child.label }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tree-children" v-if="child.children && isExpanded(child.id)">
|
||||||
|
<div
|
||||||
|
v-for="subChild in child.children"
|
||||||
|
:key="subChild.id"
|
||||||
|
class="tree-node level-2"
|
||||||
|
>
|
||||||
|
<span class="expand-icon empty"></span>
|
||||||
|
<span class="node-label">{{ subChild.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tree-viewer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-header {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #2d2d2d;
|
||||||
|
border-bottom: 1px solid #3c3c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-container {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node:hover {
|
||||||
|
background: #2a2d2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node.level-1 {
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node.level-2 {
|
||||||
|
padding-left: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-icon {
|
||||||
|
width: 16px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #888888;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-icon.empty {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-label {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-children {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
40
draggable-panels/src/materials/index.ts
Normal file
40
draggable-panels/src/materials/index.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { defineAsyncComponent, type Component } from 'vue'
|
||||||
|
import type { MaterialInfo } from '../types'
|
||||||
|
|
||||||
|
// 导入所有物料配置
|
||||||
|
import TextEditorConfig from './TextEditor/index.json'
|
||||||
|
import TreeViewerConfig from './TreeViewer/index.json'
|
||||||
|
import DataTableConfig from './DataTable/index.json'
|
||||||
|
import TestWidget1Config from './TestWidget1/index.json'
|
||||||
|
import TestWidget2Config from './TestWidget2/index.json'
|
||||||
|
import TestWidget3Config from './TestWidget3/index.json'
|
||||||
|
|
||||||
|
// 物料组件映射表
|
||||||
|
export const materialComponents: Record<string, Component> = {
|
||||||
|
TextEditor: defineAsyncComponent(() => import('./TextEditor/index.vue')),
|
||||||
|
TreeViewer: defineAsyncComponent(() => import('./TreeViewer/index.vue')),
|
||||||
|
DataTable: defineAsyncComponent(() => import('./DataTable/index.vue')),
|
||||||
|
TestWidget1: defineAsyncComponent(() => import('./TestWidget1/index.vue')),
|
||||||
|
TestWidget2: defineAsyncComponent(() => import('./TestWidget2/index.vue')),
|
||||||
|
TestWidget3: defineAsyncComponent(() => import('./TestWidget3/index.vue')),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 物料信息列表
|
||||||
|
export const materialList: MaterialInfo[] = [
|
||||||
|
{ id: 'TextEditor', ...TextEditorConfig },
|
||||||
|
{ id: 'TreeViewer', ...TreeViewerConfig },
|
||||||
|
{ id: 'DataTable', ...DataTableConfig },
|
||||||
|
{ id: 'TestWidget1', ...TestWidget1Config },
|
||||||
|
{ id: 'TestWidget2', ...TestWidget2Config },
|
||||||
|
{ id: 'TestWidget3', ...TestWidget3Config },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 获取物料组件
|
||||||
|
export const getMaterialComponent = (id: string): Component | undefined => {
|
||||||
|
return materialComponents[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取物料信息
|
||||||
|
export const getMaterialInfo = (id: string): MaterialInfo | undefined => {
|
||||||
|
return materialList.find(m => m.id === id)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch, computed } from 'vue'
|
||||||
import type { LayoutConfig, TabItem, Panel } from '../types'
|
import type { LayoutConfig, TabItem, Panel } from '../types'
|
||||||
|
import { materialList, getMaterialInfo } from '../materials'
|
||||||
|
|
||||||
// 生成唯一ID
|
// 生成唯一ID
|
||||||
const generateId = () => Math.random().toString(36).substring(2, 9)
|
const generateId = () => Math.random().toString(36).substring(2, 9)
|
||||||
@@ -9,23 +10,17 @@ const generateId = () => Math.random().toString(36).substring(2, 9)
|
|||||||
const getDefaultLayout = (): LayoutConfig => ({
|
const getDefaultLayout = (): LayoutConfig => ({
|
||||||
leftPanel: {
|
leftPanel: {
|
||||||
id: 'left',
|
id: 'left',
|
||||||
tabs: [
|
tabs: [],
|
||||||
{ id: generateId(), title: '资源管理器', content: '左侧面板内容1' }
|
|
||||||
],
|
|
||||||
activeTabId: null
|
activeTabId: null
|
||||||
},
|
},
|
||||||
centerPanel: {
|
centerPanel: {
|
||||||
id: 'center',
|
id: 'center',
|
||||||
tabs: [
|
tabs: [],
|
||||||
{ id: generateId(), title: '欢迎页', content: '中间面板内容1' }
|
|
||||||
],
|
|
||||||
activeTabId: null
|
activeTabId: null
|
||||||
},
|
},
|
||||||
rightPanel: {
|
rightPanel: {
|
||||||
id: 'right',
|
id: 'right',
|
||||||
tabs: [
|
tabs: [],
|
||||||
{ id: generateId(), title: '大纲', content: '右侧面板内容1' }
|
|
||||||
],
|
|
||||||
activeTabId: null
|
activeTabId: null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -49,6 +44,28 @@ export const usePanelStore = defineStore('panel', () => {
|
|||||||
// 是否已加载配置
|
// 是否已加载配置
|
||||||
const isLoaded = ref(false)
|
const isLoaded = ref(false)
|
||||||
|
|
||||||
|
// 获取所有已打开的物料组件ID
|
||||||
|
const openedMaterialIds = computed(() => {
|
||||||
|
const allTabs = [
|
||||||
|
...layout.value.leftPanel.tabs,
|
||||||
|
...layout.value.centerPanel.tabs,
|
||||||
|
...layout.value.rightPanel.tabs
|
||||||
|
]
|
||||||
|
return allTabs
|
||||||
|
.filter(tab => tab.materialId)
|
||||||
|
.map(tab => tab.materialId as string)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取可用的物料组件列表(未打开的)
|
||||||
|
const availableMaterials = computed(() => {
|
||||||
|
return materialList.filter(m => !openedMaterialIds.value.includes(m.id))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 检查物料组件是否已打开
|
||||||
|
const isMaterialOpened = (materialId: string) => {
|
||||||
|
return openedMaterialIds.value.includes(materialId)
|
||||||
|
}
|
||||||
|
|
||||||
// 加载配置
|
// 加载配置
|
||||||
const loadConfig = async () => {
|
const loadConfig = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -98,16 +115,37 @@ export const usePanelStore = defineStore('panel', () => {
|
|||||||
const addTab = (panelId: string, tab?: Partial<TabItem>) => {
|
const addTab = (panelId: string, tab?: Partial<TabItem>) => {
|
||||||
const panel = getPanel(panelId)
|
const panel = getPanel(panelId)
|
||||||
if (panel) {
|
if (panel) {
|
||||||
|
// 检查物料组件是否已打开
|
||||||
|
if (tab?.materialId && isMaterialOpened(tab.materialId)) {
|
||||||
|
console.warn('该物料组件已打开')
|
||||||
|
return
|
||||||
|
}
|
||||||
const newTab: TabItem = {
|
const newTab: TabItem = {
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
title: tab?.title || `新窗口 ${panel.tabs.length + 1}`,
|
title: tab?.title || `新窗口 ${panel.tabs.length + 1}`,
|
||||||
content: tab?.content || '新窗口内容'
|
content: tab?.content || '新窗口内容',
|
||||||
|
materialId: tab?.materialId
|
||||||
}
|
}
|
||||||
panel.tabs.push(newTab)
|
panel.tabs.push(newTab)
|
||||||
panel.activeTabId = newTab.id
|
panel.activeTabId = newTab.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开物料组件
|
||||||
|
const openMaterial = (materialId: string, panelId: string = 'center') => {
|
||||||
|
if (isMaterialOpened(materialId)) {
|
||||||
|
console.warn('该物料组件已打开')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const materialInfo = getMaterialInfo(materialId)
|
||||||
|
if (materialInfo) {
|
||||||
|
addTab(panelId, {
|
||||||
|
title: materialInfo.name,
|
||||||
|
materialId: materialId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 关闭Tab
|
// 关闭Tab
|
||||||
const closeTab = (panelId: string, tabId: string) => {
|
const closeTab = (panelId: string, tabId: string) => {
|
||||||
const panel = getPanel(panelId)
|
const panel = getPanel(panelId)
|
||||||
@@ -169,9 +207,13 @@ export const usePanelStore = defineStore('panel', () => {
|
|||||||
return {
|
return {
|
||||||
layout,
|
layout,
|
||||||
isLoaded,
|
isLoaded,
|
||||||
|
openedMaterialIds,
|
||||||
|
availableMaterials,
|
||||||
|
isMaterialOpened,
|
||||||
loadConfig,
|
loadConfig,
|
||||||
saveConfig,
|
saveConfig,
|
||||||
addTab,
|
addTab,
|
||||||
|
openMaterial,
|
||||||
closeTab,
|
closeTab,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
moveTab,
|
moveTab,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export interface TabItem {
|
|||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
content?: string
|
content?: string
|
||||||
|
materialId?: string // 关联的物料组件ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// 面板接口
|
// 面板接口
|
||||||
@@ -24,3 +25,18 @@ export interface AppConfig {
|
|||||||
layout: LayoutConfig
|
layout: LayoutConfig
|
||||||
lastUpdated: string
|
lastUpdated: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 物料组件配置接口
|
||||||
|
export interface MaterialConfig {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
props: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 物料组件信息
|
||||||
|
export interface MaterialInfo {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
props: Record<string, any>
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user