2
This commit is contained in:
@@ -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"
|
||||
}
|
||||
45
draggable-panels/design-state.json
Normal file
45
draggable-panels/design-state.json
Normal 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"
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "表格",
|
||||
"description": "用于展示数据的表格组件",
|
||||
"props": {
|
||||
"rows": 3,
|
||||
"columns": 3,
|
||||
"headers": ["列1", "列2", "列3"]
|
||||
}
|
||||
}
|
||||
63
draggable-panels/src/designComponents/GridTable/index.vue
Normal file
63
draggable-panels/src/designComponents/GridTable/index.vue
Normal 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>
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "单选器",
|
||||
"description": "用于选择单个选项的表单组件",
|
||||
"props": {
|
||||
"options": ["选项1", "选项2", "选项3"]
|
||||
}
|
||||
}
|
||||
75
draggable-panels/src/designComponents/RadioSelect/index.vue
Normal file
75
draggable-panels/src/designComponents/RadioSelect/index.vue
Normal 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>
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "文本输入框",
|
||||
"description": "用于输入文本的表单组件",
|
||||
"props": {
|
||||
"label": "标签名称",
|
||||
"width": 200,
|
||||
"maxLength": 100
|
||||
}
|
||||
}
|
||||
49
draggable-panels/src/designComponents/TextInput/index.vue
Normal file
49
draggable-panels/src/designComponents/TextInput/index.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
4
draggable-panels/src/materials/DesignCenter/index.json
Normal file
4
draggable-panels/src/materials/DesignCenter/index.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "设计中心",
|
||||
"description": "展示已添加的设计组件实例"
|
||||
}
|
||||
171
draggable-panels/src/materials/DesignCenter/index.vue
Normal file
171
draggable-panels/src/materials/DesignCenter/index.vue
Normal 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>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "设计组件列表",
|
||||
"description": "展示可用的设计组件,点击添加到设计中心"
|
||||
}
|
||||
147
draggable-panels/src/materials/DesignComponentList/index.vue
Normal file
147
draggable-panels/src/materials/DesignComponentList/index.vue
Normal 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>
|
||||
@@ -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))
|
||||
// 监听 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]
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 获取组件类型图标
|
||||
const getComponentIcon = (componentId: string) => {
|
||||
const icons: Record<string, string> = {
|
||||
TextInput: '✏️',
|
||||
RadioSelect: '◉',
|
||||
GridTable: '☰'
|
||||
}
|
||||
return node.children
|
||||
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)"
|
||||
>
|
||||
{{ node.children?.length ? (isExpanded(node.id) ? '▼' : '▶') : '' }}
|
||||
</span>
|
||||
<span class="node-label">{{ node.label }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 第二层 -->
|
||||
<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>
|
||||
|
||||
<!-- 第三层 -->
|
||||
<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
|
||||
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.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<div v-if="treeNodes.length === 0" class="empty-tip">
|
||||
暂无设计组件
|
||||
</div>
|
||||
</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>
|
||||
|
||||
@@ -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 },
|
||||
]
|
||||
|
||||
// 获取物料组件
|
||||
|
||||
183
draggable-panels/src/stores/designStore.ts
Normal file
183
draggable-panels/src/stores/designStore.ts
Normal 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
|
||||
}
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
@@ -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.setHeader('Content-Type', 'application/json')
|
||||
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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user