2
This commit is contained in:
@@ -4,21 +4,70 @@
|
|||||||
"id": "left",
|
"id": "left",
|
||||||
"tabs": [
|
"tabs": [
|
||||||
{
|
{
|
||||||
"id": "0i69gg5",
|
"id": "up60643",
|
||||||
"title": "资源管理器",
|
"title": "设计组件列表",
|
||||||
"content": "左侧面板内容1"
|
"content": "新窗口内容",
|
||||||
|
"materialId": "DesignComponentList"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"activeTabId": "0i69gg5"
|
"activeTabId": "up60643"
|
||||||
},
|
},
|
||||||
"centerPanel": {
|
"centerPanel": {
|
||||||
"id": "center",
|
"id": "center",
|
||||||
"tabs": [
|
"tabs": [
|
||||||
{
|
{
|
||||||
"id": "2emt1si",
|
"id": "j70ckww",
|
||||||
"title": "文本编辑器",
|
"title": "设计中心",
|
||||||
"content": "新窗口内容",
|
"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",
|
"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"
|
"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">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import { usePanelStore } from './stores/panelStore'
|
import { usePanelStore } from './stores/panelStore'
|
||||||
|
import { useDesignStore } from './stores/designStore'
|
||||||
import Header from './components/Header.vue'
|
import Header from './components/Header.vue'
|
||||||
import Footer from './components/Footer.vue'
|
import Footer from './components/Footer.vue'
|
||||||
import MainLayout from './components/MainLayout.vue'
|
import MainLayout from './components/MainLayout.vue'
|
||||||
|
|
||||||
const panelStore = usePanelStore()
|
const panelStore = usePanelStore()
|
||||||
|
const designStore = useDesignStore()
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
panelStore.loadConfig()
|
await panelStore.loadConfig()
|
||||||
|
await designStore.init()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -25,17 +25,18 @@ const activeMaterialComponent = computed(() => {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取当前物料组件的状态
|
// 获取当前物料组件的状态(从独立存储中获取)
|
||||||
const activeMaterialState = computed(() => {
|
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>) => {
|
const handleStateUpdate = (newState: Record<string, any>) => {
|
||||||
if (activeTab.value) {
|
if (activeTab.value?.materialId) {
|
||||||
activeTab.value.materialState = newState
|
panelStore.updateMaterialState(activeTab.value.materialId, newState)
|
||||||
// 触发保存
|
|
||||||
panelStore.saveConfig()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
<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'
|
import config from './index.json'
|
||||||
|
|
||||||
interface TableRow {
|
const designStore = useDesignStore()
|
||||||
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 editingCell = ref<{ row: number, field: 'property' | 'value' } | null>(null)
|
const editingCell = ref<{ key: string } | null>(null)
|
||||||
const editValue = ref('')
|
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') => {
|
const startEdit = (key: string, value: any) => {
|
||||||
editingCell.value = { row: rowIndex, field }
|
editingCell.value = { key }
|
||||||
editValue.value = tableData.value[rowIndex][field]
|
editValue.value = formatValue(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 完成编辑
|
// 完成编辑
|
||||||
const finishEdit = () => {
|
const finishEdit = () => {
|
||||||
if (editingCell.value) {
|
if (editingCell.value && designStore.selectedId) {
|
||||||
const { row, field } = editingCell.value
|
const { key } = editingCell.value
|
||||||
tableData.value[row][field] = editValue.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
|
editingCell.value = null
|
||||||
emitStateUpdate()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,13 +61,8 @@ const cancelEdit = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否正在编辑
|
// 检查是否正在编辑
|
||||||
const isEditing = (rowIndex: number, field: 'property' | 'value') => {
|
const isEditing = (key: string) => {
|
||||||
return editingCell.value?.row === rowIndex && editingCell.value?.field === field
|
return editingCell.value?.key === key
|
||||||
}
|
|
||||||
|
|
||||||
// 触发状态更新
|
|
||||||
const emitStateUpdate = () => {
|
|
||||||
emit('update:state', { data: tableData.value })
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -67,33 +70,25 @@ const emitStateUpdate = () => {
|
|||||||
<div class="data-table">
|
<div class="data-table">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<span class="title">{{ config.name }}</span>
|
<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>
|
||||||
<div class="table-body">
|
<div class="table-body">
|
||||||
<table class="table">
|
<table class="table" v-if="propertyList.length > 0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th v-for="col in columns" :key="col">{{ col }}</th>
|
<th>属性</th>
|
||||||
|
<th>值</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(row, index) in tableData" :key="index">
|
<tr v-for="prop in propertyList" :key="prop.key">
|
||||||
<td @dblclick="startEdit(index, 'property')">
|
<td class="prop-key">{{ prop.key }}</td>
|
||||||
|
<td @dblclick="startEdit(prop.key, prop.rawValue)">
|
||||||
<input
|
<input
|
||||||
v-if="isEditing(index, 'property')"
|
v-if="isEditing(prop.key)"
|
||||||
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-model="editValue"
|
v-model="editValue"
|
||||||
class="edit-input"
|
class="edit-input"
|
||||||
@blur="finishEdit"
|
@blur="finishEdit"
|
||||||
@@ -101,11 +96,17 @@ const emitStateUpdate = () => {
|
|||||||
@keyup.escape="cancelEdit"
|
@keyup.escape="cancelEdit"
|
||||||
autofocus
|
autofocus
|
||||||
/>
|
/>
|
||||||
<span v-else class="cell-value">{{ row.value }}</span>
|
<span v-else class="cell-value">{{ prop.value }}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<div v-else class="empty-tip">
|
||||||
|
<div class="empty-icon">📋</div>
|
||||||
|
<div>暂无属性</div>
|
||||||
|
<div class="empty-hint">请先在树形或设计中心选择一个组件</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -134,7 +135,7 @@ const emitStateUpdate = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
color: #666666;
|
color: #007acc;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +170,12 @@ const emitStateUpdate = () => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table td.prop-key {
|
||||||
|
color: #9cdcfe;
|
||||||
|
font-family: monospace;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
.table td:hover {
|
.table td:hover {
|
||||||
background: #2a2d2e;
|
background: #2a2d2e;
|
||||||
}
|
}
|
||||||
@@ -197,4 +204,22 @@ const emitStateUpdate = () => {
|
|||||||
border-color: #007acc;
|
border-color: #007acc;
|
||||||
box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.3);
|
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>
|
</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">
|
<script setup lang="ts">
|
||||||
import { ref, watch, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import draggable from 'vuedraggable'
|
import draggable from 'vuedraggable'
|
||||||
|
import { useDesignStore } from '../../stores/designStore'
|
||||||
import config from './index.json'
|
import config from './index.json'
|
||||||
|
|
||||||
interface TreeNode {
|
const designStore = useDesignStore()
|
||||||
id: string
|
|
||||||
label: string
|
|
||||||
expanded?: boolean
|
|
||||||
children?: TreeNode[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
// 创建可响应的本地副本用于拖拽
|
||||||
materialProps?: Record<string, any>
|
const localComponents = ref([...designStore.components])
|
||||||
materialState?: Record<string, any>
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
// 监听 designStore 组件变化,同步到本地副本
|
||||||
(e: 'update:state', state: Record<string, any>): void
|
const unsubscribe = designStore.$subscribe((mutation, state) => {
|
||||||
}>()
|
// 检查是否是组件列表变化
|
||||||
|
if (mutation.type === 'patchObject' &&
|
||||||
// 初始化树数据
|
(mutation.payload.components || mutation.events.some(e => e.path.includes('components')))) {
|
||||||
const initTreeData = (): TreeNode[] => {
|
localComponents.value = [...designStore.components]
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
initExpanded(treeData.value)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听数据变化,触发保存
|
// 拖拽结束处理 - 同步顺序到 designStore
|
||||||
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)
|
|
||||||
|
|
||||||
// 拖拽结束处理
|
|
||||||
const onDragEnd = () => {
|
const onDragEnd = () => {
|
||||||
emitStateUpdate()
|
// 重新排序 designStore 中的组件
|
||||||
|
designStore.reorderComponents([...localComponents.value])
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保节点有 children 数组
|
// 点击节点 - 选中组件
|
||||||
const ensureChildren = (node: TreeNode) => {
|
const handleNodeClick = (nodeId: string) => {
|
||||||
if (!node.children) {
|
designStore.selectComponent(nodeId)
|
||||||
node.children = []
|
}
|
||||||
|
|
||||||
|
// 获取组件类型图标
|
||||||
|
const getComponentIcon = (componentId: string) => {
|
||||||
|
const icons: Record<string, string> = {
|
||||||
|
TextInput: '✏️',
|
||||||
|
RadioSelect: '◉',
|
||||||
|
GridTable: '☰'
|
||||||
}
|
}
|
||||||
return node.children
|
return icons[componentId] || '⭕'
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -80,82 +44,32 @@ const ensureChildren = (node: TreeNode) => {
|
|||||||
<div class="tree-viewer">
|
<div class="tree-viewer">
|
||||||
<div class="viewer-header">
|
<div class="viewer-header">
|
||||||
<span class="title">{{ config.name }}</span>
|
<span class="title">{{ config.name }}</span>
|
||||||
<span class="hint">可跨级拖拽</span>
|
<span class="hint">设计组件列表</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="viewer-body">
|
<div class="viewer-body">
|
||||||
<div class="tree-container">
|
<div class="tree-container">
|
||||||
<!-- 第一层 -->
|
|
||||||
<draggable
|
<draggable
|
||||||
:list="treeData"
|
:list="[...treeNodes]"
|
||||||
group="tree-nodes"
|
|
||||||
item-key="id"
|
item-key="id"
|
||||||
:animation="150"
|
:animation="150"
|
||||||
ghost-class="node-ghost"
|
ghost-class="node-ghost"
|
||||||
@end="onDragEnd"
|
@end="onDragEnd"
|
||||||
>
|
>
|
||||||
<template #item="{ element: node }">
|
<template #item="{ element: node }">
|
||||||
<div class="tree-node-wrapper">
|
<div
|
||||||
<div class="tree-node">
|
class="tree-node"
|
||||||
<span
|
:class="{ selected: designStore.selectedId === node.id }"
|
||||||
class="expand-icon"
|
@click="handleNodeClick(node.id)"
|
||||||
:class="{ empty: !node.children?.length }"
|
|
||||||
@click="toggleNode(node.id, $event)"
|
|
||||||
>
|
>
|
||||||
{{ node.children?.length ? (isExpanded(node.id) ? '▼' : '▶') : '' }}
|
<span class="node-icon">{{ getComponentIcon(node.componentId) }}</span>
|
||||||
</span>
|
|
||||||
<span class="node-label">{{ node.label }}</span>
|
<span class="node-label">{{ node.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
|
||||||
<!-- 第二层 -->
|
<div v-if="treeNodes.length === 0" class="empty-tip">
|
||||||
<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>
|
||||||
|
|
||||||
<!-- 第三层 -->
|
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -199,18 +113,16 @@ const ensureChildren = (node: TreeNode) => {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-node-wrapper {
|
|
||||||
margin: 1px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-node {
|
.tree-node {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 4px 8px;
|
padding: 8px 12px;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #cccccc;
|
color: #cccccc;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
transition: all 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-node:active {
|
.tree-node:active {
|
||||||
@@ -221,43 +133,25 @@ const ensureChildren = (node: TreeNode) => {
|
|||||||
background: #2a2d2e;
|
background: #2a2d2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-node.level-1 {
|
.tree-node.selected {
|
||||||
padding-left: 24px;
|
background: #094771;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-node.level-2 {
|
.node-icon {
|
||||||
padding-left: 48px;
|
width: 20px;
|
||||||
}
|
margin-right: 8px;
|
||||||
|
text-align: center;
|
||||||
.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-label {
|
.node-label {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-children {
|
.empty-tip {
|
||||||
margin-left: 0;
|
color: #666666;
|
||||||
min-height: 10px;
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 拖拽样式 */
|
/* 拖拽样式 */
|
||||||
@@ -266,12 +160,4 @@ const ensureChildren = (node: TreeNode) => {
|
|||||||
background: #094771;
|
background: #094771;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-ghost .tree-node {
|
|
||||||
background: #094771;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sortable-chosen > .tree-node {
|
|
||||||
background: #094771;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import DataTableConfig from './DataTable/index.json'
|
|||||||
import TestWidget1Config from './TestWidget1/index.json'
|
import TestWidget1Config from './TestWidget1/index.json'
|
||||||
import TestWidget2Config from './TestWidget2/index.json'
|
import TestWidget2Config from './TestWidget2/index.json'
|
||||||
import TestWidget3Config from './TestWidget3/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> = {
|
export const materialComponents: Record<string, Component> = {
|
||||||
@@ -17,6 +19,8 @@ export const materialComponents: Record<string, Component> = {
|
|||||||
TestWidget1: defineAsyncComponent(() => import('./TestWidget1/index.vue')),
|
TestWidget1: defineAsyncComponent(() => import('./TestWidget1/index.vue')),
|
||||||
TestWidget2: defineAsyncComponent(() => import('./TestWidget2/index.vue')),
|
TestWidget2: defineAsyncComponent(() => import('./TestWidget2/index.vue')),
|
||||||
TestWidget3: defineAsyncComponent(() => import('./TestWidget3/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: 'TestWidget1', ...TestWidget1Config },
|
||||||
{ id: 'TestWidget2', ...TestWidget2Config },
|
{ id: 'TestWidget2', ...TestWidget2Config },
|
||||||
{ id: 'TestWidget3', ...TestWidget3Config },
|
{ 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()))
|
const layout = ref<LayoutConfig>(initActiveTabIds(getDefaultLayout()))
|
||||||
|
|
||||||
|
// 物料组件状态独立存储 (materialId -> state)
|
||||||
|
const materialStates = ref<Record<string, Record<string, any>>>({})
|
||||||
|
|
||||||
// 是否已加载配置
|
// 是否已加载配置
|
||||||
const isLoaded = ref(false)
|
const isLoaded = ref(false)
|
||||||
|
|
||||||
@@ -69,6 +72,7 @@ export const usePanelStore = defineStore('panel', () => {
|
|||||||
// 加载配置
|
// 加载配置
|
||||||
const loadConfig = async () => {
|
const loadConfig = async () => {
|
||||||
try {
|
try {
|
||||||
|
// 加载布局配置
|
||||||
const response = await fetch('/api/config')
|
const response = await fetch('/api/config')
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const config = await response.json()
|
const config = await response.json()
|
||||||
@@ -76,6 +80,15 @@ export const usePanelStore = defineStore('panel', () => {
|
|||||||
layout.value = initActiveTabIds(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) {
|
} catch (error) {
|
||||||
console.log('使用默认配置')
|
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, () => {
|
watch(layout, () => {
|
||||||
if (isLoaded.value) {
|
if (isLoaded.value) {
|
||||||
@@ -152,6 +189,7 @@ export const usePanelStore = defineStore('panel', () => {
|
|||||||
if (panel) {
|
if (panel) {
|
||||||
const index = panel.tabs.findIndex(t => t.id === tabId)
|
const index = panel.tabs.findIndex(t => t.id === tabId)
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
|
// 物料组件状态已经在 materialStates 中独立存储,不需要额外处理
|
||||||
panel.tabs.splice(index, 1)
|
panel.tabs.splice(index, 1)
|
||||||
// 更新激活的Tab
|
// 更新激活的Tab
|
||||||
if (panel.activeTabId === tabId) {
|
if (panel.activeTabId === tabId) {
|
||||||
@@ -206,12 +244,15 @@ export const usePanelStore = defineStore('panel', () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
layout,
|
layout,
|
||||||
|
materialStates,
|
||||||
isLoaded,
|
isLoaded,
|
||||||
openedMaterialIds,
|
openedMaterialIds,
|
||||||
availableMaterials,
|
availableMaterials,
|
||||||
isMaterialOpened,
|
isMaterialOpened,
|
||||||
loadConfig,
|
loadConfig,
|
||||||
saveConfig,
|
saveConfig,
|
||||||
|
getMaterialState,
|
||||||
|
updateMaterialState,
|
||||||
addTab,
|
addTab,
|
||||||
openMaterial,
|
openMaterial,
|
||||||
closeTab,
|
closeTab,
|
||||||
|
|||||||
@@ -6,37 +6,73 @@ import type { Plugin } from 'vite'
|
|||||||
|
|
||||||
// 配置文件路径
|
// 配置文件路径
|
||||||
const CONFIG_FILE = path.resolve(__dirname, 'config.json')
|
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 {
|
function configApiPlugin(): Plugin {
|
||||||
|
const configHandler = createJsonHandler(CONFIG_FILE)
|
||||||
|
const designStateHandler = createJsonHandler(DESIGN_STATE_FILE)
|
||||||
|
const materialStatesHandler = createJsonHandler(MATERIAL_STATES_FILE)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'config-api',
|
name: 'config-api',
|
||||||
configureServer(server) {
|
configureServer(server) {
|
||||||
// 读取配置
|
// 布局配置 API
|
||||||
server.middlewares.use('/api/config', (req, res, next) => {
|
server.middlewares.use('/api/config', (req, res, next) => {
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(CONFIG_FILE)) {
|
|
||||||
const config = fs.readFileSync(CONFIG_FILE, 'utf-8')
|
|
||||||
res.setHeader('Content-Type', 'application/json')
|
res.setHeader('Content-Type', 'application/json')
|
||||||
res.end(config)
|
res.end(configHandler.read())
|
||||||
} else {
|
|
||||||
res.statusCode = 404
|
|
||||||
res.end(JSON.stringify({ error: 'Config not found' }))
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.statusCode = 500
|
res.statusCode = 500
|
||||||
res.end(JSON.stringify({ error: 'Failed to read config' }))
|
res.end(JSON.stringify({ error: 'Failed to read config' }))
|
||||||
}
|
}
|
||||||
} else if (req.method === 'POST') {
|
} else if (req.method === 'POST') {
|
||||||
let body = ''
|
let body = ''
|
||||||
req.on('data', (chunk) => {
|
req.on('data', (chunk) => body += chunk.toString())
|
||||||
body += chunk.toString()
|
|
||||||
})
|
|
||||||
req.on('end', () => {
|
req.on('end', () => {
|
||||||
try {
|
try {
|
||||||
const config = JSON.parse(body)
|
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.setHeader('Content-Type', 'application/json')
|
||||||
res.end(JSON.stringify({ success: true }))
|
res.end(JSON.stringify({ success: true }))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -48,6 +84,80 @@ function configApiPlugin(): Plugin {
|
|||||||
next()
|
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