23
This commit is contained in:
34
draggable-panels/src/fauto/Designer.vue
Normal file
34
draggable-panels/src/fauto/Designer.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { usePanelStore } from './stores/panelStore'
|
||||
import { useDesignStore } from './stores/designStore'
|
||||
import Header from './components/Header.vue'
|
||||
import Footer from './components/Footer.vue'
|
||||
import MainLayout from './components/MainLayout.vue'
|
||||
|
||||
const panelStore = usePanelStore()
|
||||
const designStore = useDesignStore()
|
||||
|
||||
onMounted(async () => {
|
||||
await panelStore.loadConfig()
|
||||
await designStore.init()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="designer-container">
|
||||
<Header />
|
||||
<MainLayout />
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.designer-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
65
draggable-panels/src/fauto/components/Footer.vue
Normal file
65
draggable-panels/src/fauto/components/Footer.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const currentTime = ref('')
|
||||
let timer: number | null = null
|
||||
|
||||
const updateTime = () => {
|
||||
const now = new Date()
|
||||
currentTime.value = now.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
timer = window.setInterval(updateTime, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="app-footer">
|
||||
<div class="footer-left"></div>
|
||||
<div class="footer-right">
|
||||
<span class="time">{{ currentTime }}</span>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app-footer {
|
||||
height: 24px;
|
||||
background: #007acc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.time {
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
179
draggable-panels/src/fauto/components/Header.vue
Normal file
179
draggable-panels/src/fauto/components/Header.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { usePanelStore } from '../stores/panelStore'
|
||||
import { materialList } from '../materials'
|
||||
|
||||
const panelStore = usePanelStore()
|
||||
|
||||
// 菜单状态
|
||||
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>
|
||||
|
||||
<template>
|
||||
<header class="app-header">
|
||||
<div class="menu-bar">
|
||||
<!-- 打开项目菜单 -->
|
||||
<div class="menu-item" @click="handleOpenProject">
|
||||
<span>打开项目</span>
|
||||
</div>
|
||||
|
||||
<!-- 窗口菜单 -->
|
||||
<div
|
||||
class="menu-item"
|
||||
@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 v-if="allMaterials.length === 0" class="dropdown-empty">
|
||||
暂无可用组件
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-title">
|
||||
<span>拖拽面板编辑器</span>
|
||||
</div>
|
||||
|
||||
<div class="header-right"></div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app-header {
|
||||
height: 32px;
|
||||
background: #3c3c3c;
|
||||
border-bottom: 1px solid #2d2d2d;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.menu-bar {
|
||||
display: flex;
|
||||
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 {
|
||||
width: 100px;
|
||||
}
|
||||
</style>
|
||||
41
draggable-panels/src/fauto/components/HelloWorld.vue
Normal file
41
draggable-panels/src/fauto/components/HelloWorld.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{ msg: string }>()
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
99
draggable-panels/src/fauto/components/MainLayout.vue
Normal file
99
draggable-panels/src/fauto/components/MainLayout.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { usePanelStore } from '../stores/panelStore'
|
||||
import Panel from './Panel.vue'
|
||||
import Resizer from './Resizer.vue'
|
||||
|
||||
const panelStore = usePanelStore()
|
||||
|
||||
const leftPanel = computed(() => panelStore.layout.leftPanel)
|
||||
const centerPanel = computed(() => panelStore.layout.centerPanel)
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div class="main-layout">
|
||||
<div class="panel-container left-panel" :style="{ width: leftWidth + 'px' }">
|
||||
<div class="panel-header">左侧面板</div>
|
||||
<Panel :panel="leftPanel" />
|
||||
</div>
|
||||
|
||||
<Resizer @resize="onLeftResize" />
|
||||
|
||||
<div class="panel-container center-panel">
|
||||
<div class="panel-header">中间面板</div>
|
||||
<Panel :panel="centerPanel" />
|
||||
</div>
|
||||
|
||||
<Resizer @resize="onRightResize" />
|
||||
|
||||
<div class="panel-container right-panel" :style="{ width: rightWidth + 'px' }">
|
||||
<div class="panel-header">右侧面板</div>
|
||||
<Panel :panel="rightPanel" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.main-layout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding: 4px;
|
||||
background: #181818;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #252526;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
flex-shrink: 0;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.center-panel {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
flex-shrink: 0;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 8px 12px;
|
||||
background: #333333;
|
||||
color: #888888;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 1px solid #3c3c3c;
|
||||
}
|
||||
</style>
|
||||
229
draggable-panels/src/fauto/components/Panel.vue
Normal file
229
draggable-panels/src/fauto/components/Panel.vue
Normal file
@@ -0,0 +1,229 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import { usePanelStore } from '../stores/panelStore'
|
||||
import { getMaterialComponent } from '../materials'
|
||||
import type { Panel, TabItem } from '../types'
|
||||
|
||||
const props = defineProps<{
|
||||
panel: Panel
|
||||
}>()
|
||||
|
||||
const panelStore = usePanelStore()
|
||||
|
||||
const activeTabId = computed(() => props.panel.activeTabId)
|
||||
|
||||
const activeTab = computed(() =>
|
||||
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 activeMaterialState = computed(() => {
|
||||
if (activeTab.value?.materialId) {
|
||||
return panelStore.getMaterialState(activeTab.value.materialId)
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
// 处理物料组件状态更新(保存到独立存储)
|
||||
const handleStateUpdate = (newState: Record<string, any>) => {
|
||||
if (activeTab.value?.materialId) {
|
||||
panelStore.updateMaterialState(activeTab.value.materialId, newState)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTabClick = (tabId: string) => {
|
||||
panelStore.setActiveTab(props.panel.id, tabId)
|
||||
}
|
||||
|
||||
const handleCloseTab = (tabId: string, event: Event) => {
|
||||
event.stopPropagation()
|
||||
panelStore.closeTab(props.panel.id, tabId)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel">
|
||||
<div class="panel-tabs">
|
||||
<draggable
|
||||
:list="panel.tabs"
|
||||
group="tabs"
|
||||
item-key="id"
|
||||
class="tabs-container"
|
||||
:animation="150"
|
||||
ghost-class="tab-ghost"
|
||||
drag-class="tab-drag"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<div
|
||||
class="tab-item"
|
||||
:class="{ active: element.id === activeTabId }"
|
||||
@click="handleTabClick(element.id)"
|
||||
>
|
||||
<span class="tab-title">{{ element.title }}</span>
|
||||
<button
|
||||
class="tab-close"
|
||||
@click="handleCloseTab(element.id, $event)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<!-- 物料组件渲染 -->
|
||||
<component
|
||||
v-if="activeMaterialComponent"
|
||||
:is="activeMaterialComponent"
|
||||
:materialState="activeMaterialState"
|
||||
class="material-wrapper"
|
||||
@update:state="handleStateUpdate"
|
||||
/>
|
||||
<!-- 普通内容 -->
|
||||
<div v-else-if="activeTab" class="content-wrapper">
|
||||
<p>{{ activeTab.content }}</p>
|
||||
<p class="tab-info">Tab ID: {{ activeTab.id }}</p>
|
||||
</div>
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="empty-panel">
|
||||
<span>暂无内容</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #1e1e1e;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-tabs {
|
||||
background: #252526;
|
||||
border-bottom: 1px solid #3c3c3c;
|
||||
min-height: 35px;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
display: flex;
|
||||
min-height: 35px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #2d2d2d;
|
||||
color: #969696;
|
||||
border-right: 1px solid #3c3c3c;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tab-item:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.tab-item:hover {
|
||||
background: #323232;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
background: #1e1e1e;
|
||||
color: #ffffff;
|
||||
border-bottom: 2px solid #007acc;
|
||||
}
|
||||
|
||||
.tab-title {
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tab-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #969696;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tab-close:hover {
|
||||
background: #4a4a4a;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.material-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 16px;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.tab-info {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.empty-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
/* 拖拽样式 */
|
||||
.tab-ghost {
|
||||
opacity: 0.4;
|
||||
background: #0e639c !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.tab-drag {
|
||||
opacity: 1;
|
||||
background: #094771 !important;
|
||||
color: white !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* sortable 拖拽占位符 */
|
||||
.sortable-chosen {
|
||||
background: #0e639c !important;
|
||||
}
|
||||
</style>
|
||||
57
draggable-panels/src/fauto/components/Resizer.vue
Normal file
57
draggable-panels/src/fauto/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>
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "表格",
|
||||
"description": "用于展示数据的表格组件",
|
||||
"props": {
|
||||
"rows": 3,
|
||||
"columns": 3,
|
||||
"headers": ["列1", "列2", "列3"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
rows?: number
|
||||
columns?: number
|
||||
headers?: string[]
|
||||
}>()
|
||||
|
||||
const tableRows = computed(() => props.rows || 3)
|
||||
const tableCols = computed(() => props.columns || 3)
|
||||
const tableHeaders = computed(() => props.headers || [])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="design-grid-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="(header, i) in tableHeaders" :key="i">{{ header }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in tableRows" :key="row">
|
||||
<td v-for="col in tableCols" :key="col">
|
||||
单元格 {{ row }}-{{ col }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.design-grid-table {
|
||||
padding: 8px;
|
||||
background: #2d2d2d;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #3c3c3c;
|
||||
text-align: left;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #1e1e1e;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
td {
|
||||
background: #252526;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "单选器",
|
||||
"description": "用于选择单个选项的表单组件",
|
||||
"props": {
|
||||
"options": ["选项1", "选项2", "选项3"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
options?: string[]
|
||||
}>()
|
||||
|
||||
const selected = ref<string | null>(null)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="design-radio-select">
|
||||
<div
|
||||
v-for="option in (options || [])"
|
||||
:key="option"
|
||||
class="radio-item"
|
||||
@click="selected = option"
|
||||
>
|
||||
<span class="radio-circle" :class="{ active: selected === option }">
|
||||
<span class="radio-dot" v-if="selected === option"></span>
|
||||
</span>
|
||||
<span class="radio-label">{{ option }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.design-radio-select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
background: #2d2d2d;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.radio-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.radio-item:hover {
|
||||
background: #3c3c3c;
|
||||
}
|
||||
|
||||
.radio-circle {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #555;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.radio-circle.active {
|
||||
border-color: #007acc;
|
||||
}
|
||||
|
||||
.radio-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #007acc;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.radio-label {
|
||||
color: #cccccc;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "文本输入框",
|
||||
"description": "用于输入文本的表单组件",
|
||||
"props": {
|
||||
"label": "标签名称",
|
||||
"width": 200,
|
||||
"maxLength": 100
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label?: string
|
||||
width?: number
|
||||
maxLength?: number
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="design-text-input" :style="{ width: width + 'px' }">
|
||||
<label class="input-label">{{ label }}</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input-field"
|
||||
:maxlength="maxLength"
|
||||
placeholder="请输入..."
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.design-text-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 8px;
|
||||
background: #2d2d2d;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
color: #cccccc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
padding: 6px 8px;
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #3c3c3c;
|
||||
border-radius: 4px;
|
||||
color: #d4d4d4;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
border-color: #007acc;
|
||||
}
|
||||
</style>
|
||||
17
draggable-panels/src/fauto/materials/DataTable/index.json
Normal file
17
draggable-panels/src/fauto/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" }
|
||||
]
|
||||
}
|
||||
}
|
||||
225
draggable-panels/src/fauto/materials/DataTable/index.vue
Normal file
225
draggable-panels/src/fauto/materials/DataTable/index.vue
Normal file
@@ -0,0 +1,225 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useDesignStore } from '../../stores/designStore'
|
||||
import config from './index.json'
|
||||
|
||||
const designStore = useDesignStore()
|
||||
|
||||
// 编辑状态
|
||||
const editingCell = ref<{ key: string } | null>(null)
|
||||
const editValue = ref('')
|
||||
|
||||
// 获取选中组件的属性列表
|
||||
const propertyList = computed(() => {
|
||||
const comp = designStore.selectedComponent
|
||||
if (!comp) return []
|
||||
|
||||
return Object.entries(comp.props).map(([key, value]) => ({
|
||||
key,
|
||||
value: formatValue(value),
|
||||
rawValue: value
|
||||
}))
|
||||
})
|
||||
|
||||
// 格式化显示值
|
||||
const formatValue = (value: any): string => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(', ')
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
// 开始编辑
|
||||
const startEdit = (key: string, value: any) => {
|
||||
editingCell.value = { key }
|
||||
editValue.value = formatValue(value)
|
||||
}
|
||||
|
||||
// 完成编辑
|
||||
const finishEdit = () => {
|
||||
if (editingCell.value && designStore.selectedId) {
|
||||
const { key } = editingCell.value
|
||||
// 尝试解析值
|
||||
let newValue: any = editValue.value
|
||||
|
||||
// 如果原始值是数组,尝试解析为数组
|
||||
const comp = designStore.selectedComponent
|
||||
if (comp && Array.isArray(comp.props[key])) {
|
||||
newValue = editValue.value.split(',').map(s => s.trim())
|
||||
} else if (!isNaN(Number(editValue.value)) && editValue.value !== '') {
|
||||
newValue = Number(editValue.value)
|
||||
}
|
||||
|
||||
designStore.updateComponentProps(designStore.selectedId, key, newValue)
|
||||
editingCell.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 取消编辑
|
||||
const cancelEdit = () => {
|
||||
editingCell.value = null
|
||||
}
|
||||
|
||||
// 检查是否正在编辑
|
||||
const isEditing = (key: string) => {
|
||||
return editingCell.value?.key === key
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="data-table">
|
||||
<div class="table-header">
|
||||
<span class="title">{{ config.name }}</span>
|
||||
<span class="hint" v-if="designStore.selectedComponent">
|
||||
{{ designStore.selectedComponent.name }}
|
||||
</span>
|
||||
<span class="hint" v-else>请选择组件</span>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<table class="table" v-if="propertyList.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>属性</th>
|
||||
<th>值</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="prop in propertyList" :key="prop.key">
|
||||
<td class="prop-key">{{ prop.key }}</td>
|
||||
<td @dblclick="startEdit(prop.key, prop.rawValue)">
|
||||
<input
|
||||
v-if="isEditing(prop.key)"
|
||||
v-model="editValue"
|
||||
class="edit-input"
|
||||
@blur="finishEdit"
|
||||
@keyup.enter="finishEdit"
|
||||
@keyup.escape="cancelEdit"
|
||||
autofocus
|
||||
/>
|
||||
<span v-else class="cell-value">{{ prop.value }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div v-else class="empty-tip">
|
||||
<div class="empty-icon">📋</div>
|
||||
<div>暂无属性</div>
|
||||
<div class="empty-hint">请先在树形或设计中心选择一个组件</div>
|
||||
</div>
|
||||
</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;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #cccccc;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #007acc;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.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;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.table td.prop-key {
|
||||
color: #9cdcfe;
|
||||
font-family: monospace;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.table td:hover {
|
||||
background: #2a2d2e;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background: #2a2d2e;
|
||||
}
|
||||
|
||||
.cell-value {
|
||||
display: block;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.edit-input {
|
||||
width: 100%;
|
||||
padding: 4px 8px;
|
||||
background: #3c3c3c;
|
||||
border: 1px solid #007acc;
|
||||
border-radius: 3px;
|
||||
color: #ffffff;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.edit-input:focus {
|
||||
border-color: #007acc;
|
||||
box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.3);
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
color: #666666;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 40px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
font-size: 11px;
|
||||
margin-top: 8px;
|
||||
color: #555555;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "设计中心",
|
||||
"description": "展示已添加的设计组件实例"
|
||||
}
|
||||
180
draggable-panels/src/fauto/materials/DesignCenter/index.vue
Normal file
180
draggable-panels/src/fauto/materials/DesignCenter/index.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent, markRaw } from 'vue'
|
||||
import { useDesignStore } from '../../stores/designStore'
|
||||
import config from './index.json'
|
||||
|
||||
const designStore = useDesignStore()
|
||||
|
||||
// 自动扫描所有设计组件(eager 模式确保同步加载)
|
||||
const designComponentModules = import.meta.glob('../../designComponents/*/index.vue', { eager: true })
|
||||
|
||||
// 自动构建设计组件映射表
|
||||
const designComponentMap: Record<string, any> = {}
|
||||
|
||||
for (const path in designComponentModules) {
|
||||
// 从路径中提取组件 ID,例如 '../../designComponents/TextInput/index.vue' => 'TextInput'
|
||||
const match = path.match(/\/designComponents\/(.+)\/index\.vue$/)
|
||||
if (match) {
|
||||
const id = match[1]
|
||||
const mod = designComponentModules[path] as any
|
||||
designComponentMap[id] = markRaw(mod.default || mod)
|
||||
}
|
||||
}
|
||||
|
||||
const getComponent = (componentId: string) => {
|
||||
return designComponentMap[componentId]
|
||||
}
|
||||
|
||||
const handleSelect = (instanceId: string) => {
|
||||
designStore.selectComponent(instanceId)
|
||||
}
|
||||
|
||||
const handleRemove = (instanceId: string, event: Event) => {
|
||||
event.stopPropagation()
|
||||
designStore.removeComponent(instanceId)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="design-center">
|
||||
<div class="center-header">
|
||||
<span class="title">{{ config.name }}</span>
|
||||
<span class="count">{{ designStore.components.length }} 个实例</span>
|
||||
</div>
|
||||
<div class="center-body">
|
||||
<div
|
||||
v-for="instance in designStore.components"
|
||||
:key="instance.id"
|
||||
class="component-row"
|
||||
:class="{ selected: designStore.selectedId === instance.id }"
|
||||
@click="handleSelect(instance.id)"
|
||||
>
|
||||
<div class="component-label">
|
||||
<span class="component-name">{{ instance.name }}</span>
|
||||
<button class="remove-btn" @click="handleRemove(instance.id, $event)">×</button>
|
||||
</div>
|
||||
<div class="component-preview">
|
||||
<component
|
||||
v-if="getComponent(instance.componentId)"
|
||||
:is="getComponent(instance.componentId)"
|
||||
v-bind="instance.props"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="designStore.components.length === 0" class="empty-tip">
|
||||
<div class="empty-icon">🎨</div>
|
||||
<div>暂无设计组件</div>
|
||||
<div class="empty-hint">从左侧列表点击添加</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.design-center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.center-header {
|
||||
padding: 8px 12px;
|
||||
background: #2d2d2d;
|
||||
border-bottom: 1px solid #3c3c3c;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #cccccc;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.count {
|
||||
color: #888888;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.center-body {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.component-row {
|
||||
margin-bottom: 12px;
|
||||
border-radius: 6px;
|
||||
border: 2px solid transparent;
|
||||
transition: border-color 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.component-row:hover {
|
||||
border-color: #3c3c3c;
|
||||
}
|
||||
|
||||
.component-row.selected {
|
||||
border-color: #007acc;
|
||||
}
|
||||
|
||||
.component-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 10px;
|
||||
background: #252526;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.component-name {
|
||||
color: #e0e0e0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #888888;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.remove-btn:hover {
|
||||
background: #ff4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.component-preview {
|
||||
padding: 8px;
|
||||
background: #1e1e1e;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
color: #666666;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 40px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
font-size: 11px;
|
||||
margin-top: 8px;
|
||||
color: #555555;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "设计组件列表",
|
||||
"description": "展示可用的设计组件,点击添加到设计中心"
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useDesignStore } from '../../stores/designStore'
|
||||
import config from './index.json'
|
||||
|
||||
const designStore = useDesignStore()
|
||||
|
||||
onMounted(() => {
|
||||
// 确保设计组件元数据已加载
|
||||
if (designStore.componentMetas.length === 0) {
|
||||
designStore.loadComponentMetas()
|
||||
}
|
||||
})
|
||||
|
||||
const handleAddComponent = (componentId: string) => {
|
||||
designStore.addComponent(componentId)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="design-component-list">
|
||||
<div class="list-header">
|
||||
<span class="title">{{ config.name }}</span>
|
||||
<span class="count">{{ designStore.componentMetas.length }} 个组件</span>
|
||||
</div>
|
||||
<div class="list-body">
|
||||
<div
|
||||
v-for="meta in designStore.componentMetas"
|
||||
:key="meta.id"
|
||||
class="component-item"
|
||||
@click="handleAddComponent(meta.id)"
|
||||
>
|
||||
<div class="component-icon">📦</div>
|
||||
<div class="component-info">
|
||||
<div class="component-name">{{ meta.name }}</div>
|
||||
<div class="component-desc">{{ meta.description }}</div>
|
||||
</div>
|
||||
<div class="add-btn">+</div>
|
||||
</div>
|
||||
|
||||
<div v-if="designStore.componentMetas.length === 0" class="empty-tip">
|
||||
暂无可用设计组件
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.design-component-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
padding: 8px 12px;
|
||||
background: #2d2d2d;
|
||||
border-bottom: 1px solid #3c3c3c;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #cccccc;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.count {
|
||||
color: #888888;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.list-body {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.component-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
background: #252526;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.component-item:hover {
|
||||
background: #2a2d2e;
|
||||
}
|
||||
|
||||
.component-item:hover .add-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.component-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.component-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.component-name {
|
||||
color: #e0e0e0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.component-desc {
|
||||
color: #888888;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #007acc;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.add-btn:hover {
|
||||
background: #0088e0;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
color: #666666;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "测试组件A",
|
||||
"description": "用于测试的简单文本显示组件A",
|
||||
"props": {
|
||||
"title": "测试组件 A",
|
||||
"content": "这是测试组件A的内容区域。\n\n此组件用于验证物料系统的正确性。",
|
||||
"color": "#4fc3f7"
|
||||
}
|
||||
}
|
||||
71
draggable-panels/src/fauto/materials/TestWidget1/index.vue
Normal file
71
draggable-panels/src/fauto/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>
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "测试组件B",
|
||||
"description": "用于测试的简单文本显示组件B",
|
||||
"props": {
|
||||
"title": "测试组件 B",
|
||||
"content": "这是测试组件B的内容区域。\n\n组件之间可以通过拖拽进行位置调整。",
|
||||
"color": "#81c784"
|
||||
}
|
||||
}
|
||||
71
draggable-panels/src/fauto/materials/TestWidget2/index.vue
Normal file
71
draggable-panels/src/fauto/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>
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "测试组件C",
|
||||
"description": "用于测试的简单文本显示组件C",
|
||||
"props": {
|
||||
"title": "测试组件 C",
|
||||
"content": "这是测试组件C的内容区域。\n\n每个组件全局只能显示一次。",
|
||||
"color": "#ffb74d"
|
||||
}
|
||||
}
|
||||
71
draggable-panels/src/fauto/materials/TestWidget3/index.vue
Normal file
71
draggable-panels/src/fauto/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>
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "文本编辑器",
|
||||
"description": "一个简单的文本编辑框组件,支持多行文本输入",
|
||||
"props": {
|
||||
"placeholder": "请输入文本内容...",
|
||||
"defaultValue": "这是默认的文本内容。\n\n你可以在这里编辑任何文本。",
|
||||
"rows": 10
|
||||
}
|
||||
}
|
||||
115
draggable-panels/src/fauto/materials/TextEditor/index.vue
Normal file
115
draggable-panels/src/fauto/materials/TextEditor/index.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import config from './index.json'
|
||||
|
||||
const props = defineProps<{
|
||||
materialProps?: Record<string, any>
|
||||
materialState?: Record<string, any>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:state', state: Record<string, any>): void
|
||||
}>()
|
||||
|
||||
const mergedProps = { ...config.props, ...props.materialProps }
|
||||
|
||||
// 初始化文本内容
|
||||
const initContent = (): string => {
|
||||
if (props.materialState?.content !== undefined) {
|
||||
return props.materialState.content
|
||||
}
|
||||
return mergedProps.defaultValue
|
||||
}
|
||||
|
||||
const textContent = ref(initContent())
|
||||
|
||||
// 监听内容变化,触发保存
|
||||
let saveTimer: number | null = null
|
||||
const debouncedSave = () => {
|
||||
if (saveTimer) {
|
||||
clearTimeout(saveTimer)
|
||||
}
|
||||
saveTimer = window.setTimeout(() => {
|
||||
emit('update:state', { content: textContent.value })
|
||||
}, 500) // 500ms 防抖,避免频繁保存
|
||||
}
|
||||
|
||||
watch(textContent, () => {
|
||||
debouncedSave()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-editor">
|
||||
<div class="editor-header">
|
||||
<span class="title">{{ config.name }}</span>
|
||||
<span class="hint">自动保存</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;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #cccccc;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #666666;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.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/fauto/materials/TreeViewer/index.json
Normal file
36
draggable-panels/src/fauto/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" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
163
draggable-panels/src/fauto/materials/TreeViewer/index.vue
Normal file
163
draggable-panels/src/fauto/materials/TreeViewer/index.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import { useDesignStore } from '../../stores/designStore'
|
||||
import config from './index.json'
|
||||
|
||||
const designStore = useDesignStore()
|
||||
|
||||
// 创建可响应的本地副本用于拖拽
|
||||
const localComponents = ref([...designStore.components])
|
||||
|
||||
// 监听 designStore 组件变化,同步到本地副本
|
||||
watch(
|
||||
() => designStore.components,
|
||||
(newVal) => {
|
||||
localComponents.value = [...newVal]
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
// 拖拽结束处理 - 同步顺序到 designStore
|
||||
const onDragEnd = () => {
|
||||
// 重新排序 designStore 中的组件
|
||||
designStore.reorderComponents([...localComponents.value])
|
||||
}
|
||||
|
||||
// 点击节点 - 选中组件
|
||||
const handleNodeClick = (nodeId: string) => {
|
||||
designStore.selectComponent(nodeId)
|
||||
}
|
||||
|
||||
// 获取组件类型图标
|
||||
const getComponentIcon = (componentId: string) => {
|
||||
const icons: Record<string, string> = {
|
||||
TextInput: '✏️',
|
||||
RadioSelect: '◉',
|
||||
GridTable: '☰'
|
||||
}
|
||||
return icons[componentId] || '⭕'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tree-viewer">
|
||||
<div class="viewer-header">
|
||||
<span class="title">{{ config.name }}</span>
|
||||
<span class="hint">设计组件列表</span>
|
||||
</div>
|
||||
<div class="viewer-body">
|
||||
<div class="tree-container">
|
||||
<draggable
|
||||
:list="localComponents"
|
||||
item-key="id"
|
||||
:animation="150"
|
||||
ghost-class="node-ghost"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<template #item="{ element: node }">
|
||||
<div
|
||||
class="tree-node"
|
||||
:class="{ selected: designStore.selectedId === node.id }"
|
||||
@click="handleNodeClick(node.id)"
|
||||
>
|
||||
<span class="node-icon">{{ getComponentIcon(node.componentId) }}</span>
|
||||
<span class="node-label">{{ node.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<div v-if="localComponents.length === 0" class="empty-tip">
|
||||
暂无设计组件
|
||||
</div>
|
||||
</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;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #cccccc;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #666666;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.viewer-body {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.tree-container {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
cursor: grab;
|
||||
border-radius: 4px;
|
||||
color: #cccccc;
|
||||
user-select: none;
|
||||
margin-bottom: 2px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.tree-node:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.tree-node:hover {
|
||||
background: #2a2d2e;
|
||||
}
|
||||
|
||||
.tree-node.selected {
|
||||
background: #094771;
|
||||
}
|
||||
|
||||
.node-icon {
|
||||
width: 20px;
|
||||
margin-right: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.node-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
color: #666666;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 拖拽样式 */
|
||||
.node-ghost {
|
||||
opacity: 0.4;
|
||||
background: #094771;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
43
draggable-panels/src/fauto/materials/index.ts
Normal file
43
draggable-panels/src/fauto/materials/index.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { defineAsyncComponent, type Component } from 'vue'
|
||||
import type { MaterialInfo } from '../types'
|
||||
|
||||
// 自动扫描所有物料组件的 .vue 文件
|
||||
const vueModules = import.meta.glob('./*/index.vue')
|
||||
|
||||
// 自动扫描所有物料组件的 .json 配置文件
|
||||
const jsonModules = import.meta.glob('./*/index.json', { eager: true })
|
||||
|
||||
// 自动构建物料组件映射表
|
||||
export const materialComponents: Record<string, Component> = {}
|
||||
|
||||
for (const path in vueModules) {
|
||||
// 从路径中提取组件 ID,例如 './TextEditor/index.vue' => 'TextEditor'
|
||||
const match = path.match(/\.\/(.+)\/index\.vue$/)
|
||||
if (match) {
|
||||
const id = match[1]
|
||||
materialComponents[id] = defineAsyncComponent(vueModules[path] as any)
|
||||
}
|
||||
}
|
||||
|
||||
// 自动构建物料信息列表
|
||||
export const materialList: MaterialInfo[] = []
|
||||
|
||||
for (const path in jsonModules) {
|
||||
// 从路径中提取组件 ID
|
||||
const match = path.match(/\.\/(.+)\/index\.json$/)
|
||||
if (match) {
|
||||
const id = match[1]
|
||||
const config = (jsonModules[path] as any).default || jsonModules[path]
|
||||
materialList.push({ id, ...config })
|
||||
}
|
||||
}
|
||||
|
||||
// 获取物料组件
|
||||
export const getMaterialComponent = (id: string): Component | undefined => {
|
||||
return materialComponents[id]
|
||||
}
|
||||
|
||||
// 获取物料信息
|
||||
export const getMaterialInfo = (id: string): MaterialInfo | undefined => {
|
||||
return materialList.find(m => m.id === id)
|
||||
}
|
||||
202
draggable-panels/src/fauto/stores/designStore.ts
Normal file
202
draggable-panels/src/fauto/stores/designStore.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, watch, computed } from 'vue'
|
||||
|
||||
// 设计组件实例
|
||||
export interface DesignComponentInstance {
|
||||
id: string // 实例唯一ID
|
||||
componentId: string // 设计组件类型ID
|
||||
name: string // 显示名称
|
||||
props: Record<string, any> // 属性值
|
||||
}
|
||||
|
||||
// 设计组件定义
|
||||
export interface DesignComponentMeta {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
props: Record<string, any>
|
||||
}
|
||||
|
||||
// 生成唯一ID
|
||||
const generateId = () => Math.random().toString(36).substring(2, 9)
|
||||
|
||||
export const useDesignStore = defineStore('design', () => {
|
||||
// 设计中心已添加的组件实例列表
|
||||
const components = ref<DesignComponentInstance[]>([])
|
||||
|
||||
// 当前选中的组件实例ID
|
||||
const selectedId = ref<string | null>(null)
|
||||
|
||||
// 设计组件元数据缓存
|
||||
const componentMetas = ref<DesignComponentMeta[]>([])
|
||||
|
||||
// 自动扫描所有设计组件的 .json 配置文件
|
||||
const designComponentMetaModules = import.meta.glob('../designComponents/*/index.json', { eager: true })
|
||||
|
||||
// 是否已加载
|
||||
const isLoaded = ref(false)
|
||||
|
||||
// 当前选中的组件实例
|
||||
const selectedComponent = computed(() => {
|
||||
if (!selectedId.value) return null
|
||||
return components.value.find(c => c.id === selectedId.value) || null
|
||||
})
|
||||
|
||||
// 当前选中组件的元数据(属性定义)
|
||||
const selectedComponentMeta = computed(() => {
|
||||
if (!selectedComponent.value) return null
|
||||
return componentMetas.value.find(m => m.id === selectedComponent.value!.componentId) || null
|
||||
})
|
||||
|
||||
// 加载设计组件元数据(从本地文件自动扫描)
|
||||
const loadComponentMetas = async () => {
|
||||
try {
|
||||
const metas: DesignComponentMeta[] = []
|
||||
|
||||
for (const path in designComponentMetaModules) {
|
||||
// 从路径中提取组件 ID,例如 '../designComponents/TextInput/index.json' => 'TextInput'
|
||||
const match = path.match(/\/designComponents\/(.+)\/index\.json$/)
|
||||
if (!match) continue
|
||||
|
||||
const id = match[1]
|
||||
const mod = designComponentMetaModules[path] as any
|
||||
const config = mod.default || mod
|
||||
|
||||
metas.push({
|
||||
id,
|
||||
name: config.name,
|
||||
description: config.description,
|
||||
props: config.props || {}
|
||||
})
|
||||
}
|
||||
|
||||
componentMetas.value = metas
|
||||
} catch (error) {
|
||||
console.error('加载设计组件元数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载设计中心状态
|
||||
const loadState = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/design-state')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
if (data.components) {
|
||||
components.value = data.components
|
||||
}
|
||||
if (data.selectedId) {
|
||||
selectedId.value = data.selectedId
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('使用默认设计状态')
|
||||
}
|
||||
isLoaded.value = true
|
||||
}
|
||||
|
||||
// 保存设计中心状态
|
||||
const saveState = async () => {
|
||||
try {
|
||||
await fetch('/api/design-state', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
components: components.value,
|
||||
selectedId: selectedId.value,
|
||||
lastUpdated: new Date().toISOString()
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('保存设计状态失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听变化自动保存
|
||||
watch([components, selectedId], () => {
|
||||
if (isLoaded.value) {
|
||||
saveState()
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// 添加设计组件到设计中心
|
||||
const addComponent = (componentId: string) => {
|
||||
const meta = componentMetas.value.find(m => m.id === componentId)
|
||||
if (!meta) {
|
||||
console.warn('未找到设计组件:', componentId)
|
||||
return
|
||||
}
|
||||
|
||||
// 计算同类型组件的数量
|
||||
const sameTypeCount = components.value.filter(c => c.componentId === componentId).length
|
||||
|
||||
const instance: DesignComponentInstance = {
|
||||
id: generateId(),
|
||||
componentId: componentId,
|
||||
name: `${meta.name} ${sameTypeCount + 1}`,
|
||||
props: JSON.parse(JSON.stringify(meta.props))
|
||||
}
|
||||
|
||||
components.value.push(instance)
|
||||
selectedId.value = instance.id
|
||||
}
|
||||
|
||||
// 移除设计组件
|
||||
const removeComponent = (instanceId: string) => {
|
||||
const index = components.value.findIndex(c => c.id === instanceId)
|
||||
if (index > -1) {
|
||||
components.value.splice(index, 1)
|
||||
if (selectedId.value === instanceId) {
|
||||
selectedId.value = components.value.length > 0 ? components.value[0].id : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 选中设计组件
|
||||
const selectComponent = (instanceId: string | null) => {
|
||||
selectedId.value = instanceId
|
||||
}
|
||||
|
||||
// 更新组件属性
|
||||
const updateComponentProps = (instanceId: string, key: string, value: any) => {
|
||||
const component = components.value.find(c => c.id === instanceId)
|
||||
if (component) {
|
||||
component.props[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// 重新排序组件(拖拽后)
|
||||
const reorderComponents = (newOrder: DesignComponentInstance[]) => {
|
||||
components.value = newOrder
|
||||
}
|
||||
|
||||
// 获取组件元数据
|
||||
const getComponentMeta = (componentId: string) => {
|
||||
return componentMetas.value.find(m => m.id === componentId)
|
||||
}
|
||||
|
||||
// 初始化
|
||||
const init = async () => {
|
||||
await loadComponentMetas()
|
||||
await loadState()
|
||||
}
|
||||
|
||||
return {
|
||||
components,
|
||||
selectedId,
|
||||
selectedComponent,
|
||||
selectedComponentMeta,
|
||||
componentMetas,
|
||||
isLoaded,
|
||||
init,
|
||||
loadComponentMetas,
|
||||
loadState,
|
||||
saveState,
|
||||
addComponent,
|
||||
removeComponent,
|
||||
selectComponent,
|
||||
updateComponentProps,
|
||||
reorderComponents,
|
||||
getComponentMeta
|
||||
}
|
||||
})
|
||||
263
draggable-panels/src/fauto/stores/panelStore.ts
Normal file
263
draggable-panels/src/fauto/stores/panelStore.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import type { LayoutConfig, TabItem, Panel } from '../types'
|
||||
import { materialList, getMaterialInfo } from '../materials'
|
||||
|
||||
// 生成唯一ID
|
||||
const generateId = () => Math.random().toString(36).substring(2, 9)
|
||||
|
||||
// 默认配置
|
||||
const getDefaultLayout = (): LayoutConfig => ({
|
||||
leftPanel: {
|
||||
id: 'left',
|
||||
tabs: [],
|
||||
activeTabId: null
|
||||
},
|
||||
centerPanel: {
|
||||
id: 'center',
|
||||
tabs: [],
|
||||
activeTabId: null
|
||||
},
|
||||
rightPanel: {
|
||||
id: 'right',
|
||||
tabs: [],
|
||||
activeTabId: null
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化每个面板的activeTabId
|
||||
const initActiveTabIds = (layout: LayoutConfig): LayoutConfig => {
|
||||
const panels: (keyof LayoutConfig)[] = ['leftPanel', 'centerPanel', 'rightPanel']
|
||||
panels.forEach(key => {
|
||||
const panel = layout[key]
|
||||
if (panel.tabs.length > 0 && !panel.activeTabId) {
|
||||
panel.activeTabId = panel.tabs[0].id
|
||||
}
|
||||
})
|
||||
return layout
|
||||
}
|
||||
|
||||
export const usePanelStore = defineStore('panel', () => {
|
||||
// 布局配置
|
||||
const layout = ref<LayoutConfig>(initActiveTabIds(getDefaultLayout()))
|
||||
|
||||
// 物料组件状态独立存储 (materialId -> state)
|
||||
const materialStates = ref<Record<string, Record<string, any>>>({})
|
||||
|
||||
// 是否已加载配置
|
||||
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 () => {
|
||||
try {
|
||||
// 加载布局配置
|
||||
const response = await fetch('/api/config')
|
||||
if (response.ok) {
|
||||
const config = await response.json()
|
||||
if (config && config.layout) {
|
||||
layout.value = initActiveTabIds(config.layout)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载物料组件状态
|
||||
const statesResponse = await fetch('/api/material-states')
|
||||
if (statesResponse.ok) {
|
||||
const states = await statesResponse.json()
|
||||
if (states && typeof states === 'object') {
|
||||
materialStates.value = states
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('使用默认配置')
|
||||
}
|
||||
isLoaded.value = true
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
const saveConfig = async () => {
|
||||
try {
|
||||
await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
layout: layout.value,
|
||||
lastUpdated: new Date().toISOString()
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存物料组件状态
|
||||
const saveMaterialStates = async () => {
|
||||
try {
|
||||
await fetch('/api/material-states', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(materialStates.value)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('保存物料状态失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取物料组件状态
|
||||
const getMaterialState = (materialId: string): Record<string, any> | undefined => {
|
||||
return materialStates.value[materialId]
|
||||
}
|
||||
|
||||
// 更新物料组件状态
|
||||
const updateMaterialState = (materialId: string, state: Record<string, any>) => {
|
||||
materialStates.value[materialId] = { ...materialStates.value[materialId], ...state }
|
||||
saveMaterialStates()
|
||||
}
|
||||
|
||||
// 监听变化并自动保存
|
||||
watch(layout, () => {
|
||||
if (isLoaded.value) {
|
||||
saveConfig()
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// 获取面板
|
||||
const getPanel = (panelId: string): Panel | undefined => {
|
||||
const panels = [layout.value.leftPanel, layout.value.centerPanel, layout.value.rightPanel]
|
||||
return panels.find(p => p.id === panelId)
|
||||
}
|
||||
|
||||
// 添加新Tab
|
||||
const addTab = (panelId: string, tab?: Partial<TabItem>) => {
|
||||
const panel = getPanel(panelId)
|
||||
if (panel) {
|
||||
// 检查物料组件是否已打开
|
||||
if (tab?.materialId && isMaterialOpened(tab.materialId)) {
|
||||
console.warn('该物料组件已打开')
|
||||
return
|
||||
}
|
||||
const newTab: TabItem = {
|
||||
id: generateId(),
|
||||
title: tab?.title || `新窗口 ${panel.tabs.length + 1}`,
|
||||
content: tab?.content || '新窗口内容',
|
||||
materialId: tab?.materialId
|
||||
}
|
||||
panel.tabs.push(newTab)
|
||||
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
|
||||
const closeTab = (panelId: string, tabId: string) => {
|
||||
const panel = getPanel(panelId)
|
||||
if (panel) {
|
||||
const index = panel.tabs.findIndex(t => t.id === tabId)
|
||||
if (index > -1) {
|
||||
// 物料组件状态已经在 materialStates 中独立存储,不需要额外处理
|
||||
panel.tabs.splice(index, 1)
|
||||
// 更新激活的Tab
|
||||
if (panel.activeTabId === tabId) {
|
||||
panel.activeTabId = panel.tabs.length > 0
|
||||
? panel.tabs[Math.max(0, index - 1)].id
|
||||
: null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置激活的Tab
|
||||
const setActiveTab = (panelId: string, tabId: string) => {
|
||||
const panel = getPanel(panelId)
|
||||
if (panel) {
|
||||
panel.activeTabId = tabId
|
||||
}
|
||||
}
|
||||
|
||||
// 移动Tab到另一个面板
|
||||
const moveTab = (fromPanelId: string, toPanelId: string, tabId: string, toIndex?: number) => {
|
||||
const fromPanel = getPanel(fromPanelId)
|
||||
const toPanel = getPanel(toPanelId)
|
||||
|
||||
if (fromPanel && toPanel) {
|
||||
const tabIndex = fromPanel.tabs.findIndex(t => t.id === tabId)
|
||||
if (tabIndex > -1) {
|
||||
const [tab] = fromPanel.tabs.splice(tabIndex, 1)
|
||||
|
||||
if (toIndex !== undefined) {
|
||||
toPanel.tabs.splice(toIndex, 0, tab)
|
||||
} else {
|
||||
toPanel.tabs.push(tab)
|
||||
}
|
||||
|
||||
// 更新激活状态
|
||||
toPanel.activeTabId = tab.id
|
||||
|
||||
if (fromPanel.activeTabId === tabId && fromPanel.tabs.length > 0) {
|
||||
fromPanel.activeTabId = fromPanel.tabs[0].id
|
||||
} else if (fromPanel.tabs.length === 0) {
|
||||
fromPanel.activeTabId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在默认面板添加新窗口
|
||||
const addNewWindow = () => {
|
||||
addTab('center')
|
||||
}
|
||||
|
||||
return {
|
||||
layout,
|
||||
materialStates,
|
||||
isLoaded,
|
||||
openedMaterialIds,
|
||||
availableMaterials,
|
||||
isMaterialOpened,
|
||||
loadConfig,
|
||||
saveConfig,
|
||||
getMaterialState,
|
||||
updateMaterialState,
|
||||
addTab,
|
||||
openMaterial,
|
||||
closeTab,
|
||||
setActiveTab,
|
||||
moveTab,
|
||||
addNewWindow
|
||||
}
|
||||
})
|
||||
43
draggable-panels/src/fauto/types/index.ts
Normal file
43
draggable-panels/src/fauto/types/index.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// Tab项接口
|
||||
export interface TabItem {
|
||||
id: string
|
||||
title: string
|
||||
content?: string
|
||||
materialId?: string // 关联的物料组件ID
|
||||
materialState?: Record<string, any> // 物料组件的状态数据
|
||||
}
|
||||
|
||||
// 面板接口
|
||||
export interface Panel {
|
||||
id: string
|
||||
tabs: TabItem[]
|
||||
activeTabId: string | null
|
||||
}
|
||||
|
||||
// 布局配置接口
|
||||
export interface LayoutConfig {
|
||||
leftPanel: Panel
|
||||
centerPanel: Panel
|
||||
rightPanel: Panel
|
||||
}
|
||||
|
||||
// 全局配置接口
|
||||
export interface AppConfig {
|
||||
layout: LayoutConfig
|
||||
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