This commit is contained in:
2025-12-20 21:22:39 +08:00
parent 7c48e4148d
commit c62cc7c653
20 changed files with 1145 additions and 312 deletions

View File

@@ -4,21 +4,70 @@
"id": "left",
"tabs": [
{
"id": "0i69gg5",
"title": "资源管理器",
"content": "左侧面板内容1"
"id": "up60643",
"title": "设计组件列表",
"content": "新窗口内容",
"materialId": "DesignComponentList"
}
],
"activeTabId": "0i69gg5"
"activeTabId": "up60643"
},
"centerPanel": {
"id": "center",
"tabs": [
{
"id": "2emt1si",
"title": "文本编辑器",
"id": "j70ckww",
"title": "设计中心",
"content": "新窗口内容",
"materialId": "TextEditor"
"materialId": "DesignCenter"
}
],
"activeTabId": "j70ckww"
},
"rightPanel": {
"id": "right",
"tabs": [
{
"id": "vrh9bl2",
"title": "数据表格",
"content": "新窗口内容",
"materialId": "DataTable",
"materialState": {
"data": [
{
"property": "项目名称",
"value": "1111"
},
{
"property": "框架",
"value": "9999"
},
{
"property": "语言",
"value": "TypeScript"
},
{
"property": "构建工具",
"value": "Vite"
},
{
"property": "状态管理",
"value": "Pinia"
},
{
"property": "版本",
"value": "1.0.0"
},
{
"property": "作者",
"value": "Developer"
},
{
"property": "许可证",
"value": "MIT"
}
]
}
},
{
"id": "mxfx11j",
@@ -81,64 +130,10 @@
}
]
}
},
{
"id": "jln5iq9",
"title": "测试组件B",
"content": "新窗口内容",
"materialId": "TestWidget2"
}
],
"activeTabId": "mxfx11j"
},
"rightPanel": {
"id": "right",
"tabs": [
{
"id": "vrh9bl2",
"title": "数据表格",
"content": "新窗口内容",
"materialId": "DataTable",
"materialState": {
"data": [
{
"property": "项目名称",
"value": "1111"
},
{
"property": "框架",
"value": "9999"
},
{
"property": "语言",
"value": "TypeScript"
},
{
"property": "构建工具",
"value": "Vite"
},
{
"property": "状态管理",
"value": "Pinia"
},
{
"property": "版本",
"value": "1.0.0"
},
{
"property": "作者",
"value": "Developer"
},
{
"property": "许可证",
"value": "MIT"
}
]
}
}
],
"activeTabId": "vrh9bl2"
}
},
"lastUpdated": "2025-12-20T12:43:09.638Z"
"lastUpdated": "2025-12-20T13:20:08.377Z"
}

View File

@@ -0,0 +1,45 @@
{
"components": [
{
"id": "xazr6j9",
"componentId": "GridTable",
"name": "表格 2",
"props": {
"rows": 6,
"columns": 6,
"headers": [
"列1",
"列2",
"列3",
"4",
"5",
"6"
]
}
},
{
"id": "6evneg3",
"componentId": "RadioSelect",
"name": "单选器 1",
"props": {
"options": [
"选项1",
"选项6",
"选项3"
]
}
},
{
"id": "nx1ns6t",
"componentId": "TextInput",
"name": "文本输入框 1",
"props": {
"label": "标签名称",
"width": 500,
"maxLength": 100
}
}
],
"selectedId": "xazr6j9",
"lastUpdated": "2025-12-20T13:20:22.771Z"
}

View File

@@ -1,14 +1,17 @@
<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(() => {
panelStore.loadConfig()
onMounted(async () => {
await panelStore.loadConfig()
await designStore.init()
})
</script>

View File

@@ -25,17 +25,18 @@ const activeMaterialComponent = computed(() => {
return null
})
// 获取当前物料组件的状态
// 获取当前物料组件的状态(从独立存储中获取)
const activeMaterialState = computed(() => {
return activeTab.value?.materialState
if (activeTab.value?.materialId) {
return panelStore.getMaterialState(activeTab.value.materialId)
}
return undefined
})
// 处理物料组件状态更新
// 处理物料组件状态更新(保存到独立存储)
const handleStateUpdate = (newState: Record<string, any>) => {
if (activeTab.value) {
activeTab.value.materialState = newState
// 触发保存
panelStore.saveConfig()
if (activeTab.value?.materialId) {
panelStore.updateMaterialState(activeTab.value.materialId, newState)
}
}

View File

@@ -0,0 +1,9 @@
{
"name": "表格",
"description": "用于展示数据的表格组件",
"props": {
"rows": 3,
"columns": 3,
"headers": ["列1", "列2", "列3"]
}
}

View File

@@ -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>

View File

@@ -0,0 +1,7 @@
{
"name": "单选器",
"description": "用于选择单个选项的表单组件",
"props": {
"options": ["选项1", "选项2", "选项3"]
}
}

View File

@@ -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>

View File

@@ -0,0 +1,9 @@
{
"name": "文本输入框",
"description": "用于输入文本的表单组件",
"props": {
"label": "标签名称",
"width": 200,
"maxLength": 100
}
}

View File

@@ -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>

View File

@@ -1,49 +1,57 @@
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import { ref, computed, watch } from 'vue'
import { useDesignStore } from '../../stores/designStore'
import config from './index.json'
interface TableRow {
property: string
value: string
}
const props = defineProps<{
materialProps?: Record<string, any>
materialState?: Record<string, any>
}>()
const emit = defineEmits<{
(e: 'update:state', state: Record<string, any>): void
}>()
// 初始化表格数据
const initTableData = (): TableRow[] => {
if (props.materialState?.data) {
return JSON.parse(JSON.stringify(props.materialState.data))
}
return JSON.parse(JSON.stringify(config.props.data))
}
const tableData = ref<TableRow[]>(initTableData())
const columns = config.props.columns
const designStore = useDesignStore()
// 编辑状态
const editingCell = ref<{ row: number, field: 'property' | 'value' } | null>(null)
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 = (rowIndex: number, field: 'property' | 'value') => {
editingCell.value = { row: rowIndex, field }
editValue.value = tableData.value[rowIndex][field]
const startEdit = (key: string, value: any) => {
editingCell.value = { key }
editValue.value = formatValue(value)
}
// 完成编辑
const finishEdit = () => {
if (editingCell.value) {
const { row, field } = editingCell.value
tableData.value[row][field] = editValue.value
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
emitStateUpdate()
}
}
@@ -53,13 +61,8 @@ const cancelEdit = () => {
}
// 检查是否正在编辑
const isEditing = (rowIndex: number, field: 'property' | 'value') => {
return editingCell.value?.row === rowIndex && editingCell.value?.field === field
}
// 触发状态更新
const emitStateUpdate = () => {
emit('update:state', { data: tableData.value })
const isEditing = (key: string) => {
return editingCell.value?.key === key
}
</script>
@@ -67,33 +70,25 @@ const emitStateUpdate = () => {
<div class="data-table">
<div class="table-header">
<span class="title">{{ config.name }}</span>
<span class="hint">双击单元格编辑</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">
<table class="table" v-if="propertyList.length > 0">
<thead>
<tr>
<th v-for="col in columns" :key="col">{{ col }}</th>
<th>属性</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in tableData" :key="index">
<td @dblclick="startEdit(index, 'property')">
<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(index, 'property')"
v-model="editValue"
class="edit-input"
@blur="finishEdit"
@keyup.enter="finishEdit"
@keyup.escape="cancelEdit"
ref="editInput"
autofocus
/>
<span v-else class="cell-value">{{ row.property }}</span>
</td>
<td @dblclick="startEdit(index, 'value')">
<input
v-if="isEditing(index, 'value')"
v-if="isEditing(prop.key)"
v-model="editValue"
class="edit-input"
@blur="finishEdit"
@@ -101,11 +96,17 @@ const emitStateUpdate = () => {
@keyup.escape="cancelEdit"
autofocus
/>
<span v-else class="cell-value">{{ row.value }}</span>
<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>
@@ -134,7 +135,7 @@ const emitStateUpdate = () => {
}
.hint {
color: #666666;
color: #007acc;
font-size: 11px;
}
@@ -169,6 +170,12 @@ const emitStateUpdate = () => {
position: relative;
}
.table td.prop-key {
color: #9cdcfe;
font-family: monospace;
cursor: default;
}
.table td:hover {
background: #2a2d2e;
}
@@ -197,4 +204,22 @@ const emitStateUpdate = () => {
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>

View File

@@ -0,0 +1,4 @@
{
"name": "设计中心",
"description": "展示已添加的设计组件实例"
}

View File

@@ -0,0 +1,171 @@
<script setup lang="ts">
import { computed, defineAsyncComponent, markRaw } from 'vue'
import { useDesignStore } from '../../stores/designStore'
import config from './index.json'
const designStore = useDesignStore()
// 动态加载设计组件
const designComponentMap: Record<string, any> = {
TextInput: markRaw(defineAsyncComponent(() => import('../../designComponents/TextInput/index.vue'))),
RadioSelect: markRaw(defineAsyncComponent(() => import('../../designComponents/RadioSelect/index.vue'))),
GridTable: markRaw(defineAsyncComponent(() => import('../../designComponents/GridTable/index.vue')))
}
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>

View File

@@ -0,0 +1,4 @@
{
"name": "设计组件列表",
"description": "展示可用的设计组件,点击添加到设计中心"
}

View File

@@ -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>

View File

@@ -1,78 +1,42 @@
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import draggable from 'vuedraggable'
import { useDesignStore } from '../../stores/designStore'
import config from './index.json'
interface TreeNode {
id: string
label: string
expanded?: boolean
children?: TreeNode[]
}
const designStore = useDesignStore()
const props = defineProps<{
materialProps?: Record<string, any>
materialState?: Record<string, any>
}>()
// 创建可响应的本地副本用于拖拽
const localComponents = ref([...designStore.components])
const emit = defineEmits<{
(e: 'update:state', state: Record<string, any>): void
}>()
// 初始化树数据
const initTreeData = (): TreeNode[] => {
if (props.materialState?.treeData) {
return JSON.parse(JSON.stringify(props.materialState.treeData))
}
return JSON.parse(JSON.stringify(config.props.treeData))
}
const treeData = ref<TreeNode[]>(initTreeData())
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)
// 监听 designStore 组件变化,同步到本地副本
const unsubscribe = designStore.$subscribe((mutation, state) => {
// 检查是否是组件列表变化
if (mutation.type === 'patchObject' &&
(mutation.payload.components || mutation.events.some(e => e.path.includes('components')))) {
localComponents.value = [...designStore.components]
}
})
}
onMounted(() => {
initExpanded(treeData.value)
})
// 监听数据变化,触发保存
const emitStateUpdate = () => {
emit('update:state', { treeData: treeData.value })
}
const toggleNode = (nodeId: string, event: Event) => {
event.stopPropagation()
if (expandedNodes.value.has(nodeId)) {
expandedNodes.value.delete(nodeId)
} else {
expandedNodes.value.add(nodeId)
}
}
const isExpanded = (nodeId: string) => expandedNodes.value.has(nodeId)
// 拖拽结束处理
// 拖拽结束处理 - 同步顺序到 designStore
const onDragEnd = () => {
emitStateUpdate()
// 重新排序 designStore 中的组件
designStore.reorderComponents([...localComponents.value])
}
// 确保节点有 children 数组
const ensureChildren = (node: TreeNode) => {
if (!node.children) {
node.children = []
// 点击节点 - 选中组件
const handleNodeClick = (nodeId: string) => {
designStore.selectComponent(nodeId)
}
return node.children
// 获取组件类型图标
const getComponentIcon = (componentId: string) => {
const icons: Record<string, string> = {
TextInput: '✏️',
RadioSelect: '◉',
GridTable: '☰'
}
return icons[componentId] || '⭕'
}
</script>
@@ -80,82 +44,32 @@ const ensureChildren = (node: TreeNode) => {
<div class="tree-viewer">
<div class="viewer-header">
<span class="title">{{ config.name }}</span>
<span class="hint">可跨级拖拽</span>
<span class="hint">设计组件列表</span>
</div>
<div class="viewer-body">
<div class="tree-container">
<!-- 第一层 -->
<draggable
:list="treeData"
group="tree-nodes"
:list="[...treeNodes]"
item-key="id"
:animation="150"
ghost-class="node-ghost"
@end="onDragEnd"
>
<template #item="{ element: node }">
<div class="tree-node-wrapper">
<div class="tree-node">
<span
class="expand-icon"
:class="{ empty: !node.children?.length }"
@click="toggleNode(node.id, $event)"
<div
class="tree-node"
:class="{ selected: designStore.selectedId === node.id }"
@click="handleNodeClick(node.id)"
>
{{ node.children?.length ? (isExpanded(node.id) ? '▼' : '▶') : '' }}
</span>
<span class="node-icon">{{ getComponentIcon(node.componentId) }}</span>
<span class="node-label">{{ node.label }}</span>
</div>
</template>
</draggable>
<!-- 第二层 -->
<div class="tree-children" v-if="isExpanded(node.id)">
<draggable
:list="ensureChildren(node)"
group="tree-nodes"
item-key="id"
:animation="150"
ghost-class="node-ghost"
@end="onDragEnd"
>
<template #item="{ element: child }">
<div class="tree-node-wrapper">
<div class="tree-node level-1">
<span
class="expand-icon"
:class="{ empty: !child.children?.length }"
@click="toggleNode(child.id, $event)"
>
{{ child.children?.length ? (isExpanded(child.id) ? '▼' : '▶') : '' }}
</span>
<span class="node-label">{{ child.label }}</span>
<div v-if="treeNodes.length === 0" class="empty-tip">
暂无设计组件
</div>
<!-- 第三层 -->
<div class="tree-children" v-if="isExpanded(child.id)">
<draggable
:list="ensureChildren(child)"
group="tree-nodes"
item-key="id"
:animation="150"
ghost-class="node-ghost"
@end="onDragEnd"
>
<template #item="{ element: subChild }">
<div class="tree-node-wrapper">
<div class="tree-node level-2">
<span class="expand-icon empty"></span>
<span class="node-label">{{ subChild.label }}</span>
</div>
</div>
</template>
</draggable>
</div>
</div>
</template>
</draggable>
</div>
</div>
</template>
</draggable>
</div>
</div>
</div>
@@ -199,18 +113,16 @@ const ensureChildren = (node: TreeNode) => {
font-size: 13px;
}
.tree-node-wrapper {
margin: 1px 0;
}
.tree-node {
display: flex;
align-items: center;
padding: 4px 8px;
padding: 8px 12px;
cursor: grab;
border-radius: 4px;
color: #cccccc;
user-select: none;
margin-bottom: 2px;
transition: all 0.15s;
}
.tree-node:active {
@@ -221,43 +133,25 @@ const ensureChildren = (node: TreeNode) => {
background: #2a2d2e;
}
.tree-node.level-1 {
padding-left: 24px;
.tree-node.selected {
background: #094771;
}
.tree-node.level-2 {
padding-left: 48px;
}
.expand-icon {
width: 16px;
height: 16px;
font-size: 10px;
color: #888888;
margin-right: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
}
.expand-icon:hover:not(.empty) {
background: #3c3c3c;
}
.expand-icon.empty {
cursor: default;
.node-icon {
width: 20px;
margin-right: 8px;
text-align: center;
}
.node-label {
flex: 1;
pointer-events: none;
}
.tree-children {
margin-left: 0;
min-height: 10px;
.empty-tip {
color: #666666;
text-align: center;
padding: 20px;
font-size: 12px;
}
/* 拖拽样式 */
@@ -266,12 +160,4 @@ const ensureChildren = (node: TreeNode) => {
background: #094771;
border-radius: 4px;
}
.node-ghost .tree-node {
background: #094771;
}
.sortable-chosen > .tree-node {
background: #094771;
}
</style>

View File

@@ -8,6 +8,8 @@ import DataTableConfig from './DataTable/index.json'
import TestWidget1Config from './TestWidget1/index.json'
import TestWidget2Config from './TestWidget2/index.json'
import TestWidget3Config from './TestWidget3/index.json'
import DesignComponentListConfig from './DesignComponentList/index.json'
import DesignCenterConfig from './DesignCenter/index.json'
// 物料组件映射表
export const materialComponents: Record<string, Component> = {
@@ -17,6 +19,8 @@ export const materialComponents: Record<string, Component> = {
TestWidget1: defineAsyncComponent(() => import('./TestWidget1/index.vue')),
TestWidget2: defineAsyncComponent(() => import('./TestWidget2/index.vue')),
TestWidget3: defineAsyncComponent(() => import('./TestWidget3/index.vue')),
DesignComponentList: defineAsyncComponent(() => import('./DesignComponentList/index.vue')),
DesignCenter: defineAsyncComponent(() => import('./DesignCenter/index.vue')),
}
// 物料信息列表
@@ -27,6 +31,8 @@ export const materialList: MaterialInfo[] = [
{ id: 'TestWidget1', ...TestWidget1Config },
{ id: 'TestWidget2', ...TestWidget2Config },
{ id: 'TestWidget3', ...TestWidget3Config },
{ id: 'DesignComponentList', ...DesignComponentListConfig },
{ id: 'DesignCenter', ...DesignCenterConfig },
]
// 获取物料组件

View File

@@ -0,0 +1,183 @@
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[]>([])
// 是否已加载
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 response = await fetch('/api/design-components')
if (response.ok) {
componentMetas.value = await response.json()
}
} 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
}
})

View File

@@ -41,6 +41,9 @@ 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)
@@ -69,6 +72,7 @@ export const usePanelStore = defineStore('panel', () => {
// 加载配置
const loadConfig = async () => {
try {
// 加载布局配置
const response = await fetch('/api/config')
if (response.ok) {
const config = await response.json()
@@ -76,6 +80,15 @@ export const usePanelStore = defineStore('panel', () => {
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('使用默认配置')
}
@@ -98,6 +111,30 @@ export const usePanelStore = defineStore('panel', () => {
}
}
// 保存物料组件状态
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) {
@@ -152,6 +189,7 @@ export const usePanelStore = defineStore('panel', () => {
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) {
@@ -206,12 +244,15 @@ export const usePanelStore = defineStore('panel', () => {
return {
layout,
materialStates,
isLoaded,
openedMaterialIds,
availableMaterials,
isMaterialOpened,
loadConfig,
saveConfig,
getMaterialState,
updateMaterialState,
addTab,
openMaterial,
closeTab,

View File

@@ -6,37 +6,73 @@ import type { Plugin } from 'vite'
// 配置文件路径
const CONFIG_FILE = path.resolve(__dirname, 'config.json')
const DESIGN_STATE_FILE = path.resolve(__dirname, 'design-state.json')
const MATERIAL_STATES_FILE = path.resolve(__dirname, 'material-states.json')
const DESIGN_COMPONENTS_DIR = path.resolve(__dirname, 'src/designComponents')
// 通用JSON文件读写处理器
function createJsonHandler(filePath: string) {
return {
read: () => {
if (fs.existsSync(filePath)) {
return fs.readFileSync(filePath, 'utf-8')
}
return JSON.stringify({})
},
write: (data: string) => {
fs.writeFileSync(filePath, data)
}
}
}
// 读取设计组件元数据
function loadDesignComponentMetas() {
const metas: any[] = []
if (!fs.existsSync(DESIGN_COMPONENTS_DIR)) {
return metas
}
const dirs = fs.readdirSync(DESIGN_COMPONENTS_DIR)
for (const dir of dirs) {
const jsonPath = path.join(DESIGN_COMPONENTS_DIR, dir, 'index.json')
if (fs.existsSync(jsonPath)) {
try {
const content = fs.readFileSync(jsonPath, 'utf-8')
const meta = JSON.parse(content)
metas.push({ id: dir, ...meta })
} catch (e) {
console.error(`加载设计组件元数据失败: ${dir}`, e)
}
}
}
return metas
}
// 自定义插件:处理配置文件的读写
function configApiPlugin(): Plugin {
const configHandler = createJsonHandler(CONFIG_FILE)
const designStateHandler = createJsonHandler(DESIGN_STATE_FILE)
const materialStatesHandler = createJsonHandler(MATERIAL_STATES_FILE)
return {
name: 'config-api',
configureServer(server) {
// 读取配置
// 布局配置 API
server.middlewares.use('/api/config', (req, res, next) => {
if (req.method === 'GET') {
try {
if (fs.existsSync(CONFIG_FILE)) {
const config = fs.readFileSync(CONFIG_FILE, 'utf-8')
res.setHeader('Content-Type', 'application/json')
res.end(config)
} else {
res.statusCode = 404
res.end(JSON.stringify({ error: 'Config not found' }))
}
res.end(configHandler.read())
} catch (error) {
res.statusCode = 500
res.end(JSON.stringify({ error: 'Failed to read config' }))
}
} else if (req.method === 'POST') {
let body = ''
req.on('data', (chunk) => {
body += chunk.toString()
})
req.on('data', (chunk) => body += chunk.toString())
req.on('end', () => {
try {
const config = JSON.parse(body)
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
configHandler.write(JSON.stringify(config, null, 2))
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ success: true }))
} catch (error) {
@@ -48,6 +84,80 @@ function configApiPlugin(): Plugin {
next()
}
})
// 设计组件元数据 API
server.middlewares.use('/api/design-components', (req, res, next) => {
if (req.method === 'GET') {
try {
const metas = loadDesignComponentMetas()
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify(metas))
} catch (error) {
res.statusCode = 500
res.end(JSON.stringify({ error: 'Failed to load design components' }))
}
} else {
next()
}
})
// 设计中心状态 API
server.middlewares.use('/api/design-state', (req, res, next) => {
if (req.method === 'GET') {
try {
res.setHeader('Content-Type', 'application/json')
res.end(designStateHandler.read())
} catch (error) {
res.statusCode = 500
res.end(JSON.stringify({ error: 'Failed to read design state' }))
}
} else if (req.method === 'POST') {
let body = ''
req.on('data', (chunk) => body += chunk.toString())
req.on('end', () => {
try {
const state = JSON.parse(body)
designStateHandler.write(JSON.stringify(state, null, 2))
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ success: true }))
} catch (error) {
res.statusCode = 500
res.end(JSON.stringify({ error: 'Failed to save design state' }))
}
})
} else {
next()
}
})
// 物料组件状态 API (独立存储)
server.middlewares.use('/api/material-states', (req, res, next) => {
if (req.method === 'GET') {
try {
res.setHeader('Content-Type', 'application/json')
res.end(materialStatesHandler.read())
} catch (error) {
res.statusCode = 500
res.end(JSON.stringify({ error: 'Failed to read material states' }))
}
} else if (req.method === 'POST') {
let body = ''
req.on('data', (chunk) => body += chunk.toString())
req.on('end', () => {
try {
const states = JSON.parse(body)
materialStatesHandler.write(JSON.stringify(states, null, 2))
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ success: true }))
} catch (error) {
res.statusCode = 500
res.end(JSON.stringify({ error: 'Failed to save material states' }))
}
})
} else {
next()
}
})
}
}
}