@@ -85,21 +85,21 @@ Element Plus CSS 为 `116.01 kB`(gzip `16.05 kB`),随编辑器模块加载
|
||||
- ~~页面 Schema 导入导出与实时编辑~~
|
||||
- ~~静态数据源~~
|
||||
- ~~API 数据源~~
|
||||
- 数据源管理界面
|
||||
- API 数据源调试
|
||||
- 加载、刷新与请求复用
|
||||
- ~~数据源管理界面~~
|
||||
- ~~API 数据源调试~~
|
||||
- ~~加载、刷新与请求复用~~
|
||||
|
||||
### 三、运行时与预览能力
|
||||
|
||||
- 大屏自适应预览
|
||||
- 页面持久化与独立访问
|
||||
- 运行时上下文
|
||||
- ~~大屏自适应预览~~
|
||||
- ~~页面持久化与独立访问~~
|
||||
- ~~运行时上下文~~
|
||||
|
||||
### 四、事件系统
|
||||
|
||||
- 事件调度机制
|
||||
- 事件配置面板
|
||||
- 跨组件事件联动
|
||||
- ~~事件调度机制~~
|
||||
- ~~事件配置面板~~
|
||||
- ~~跨组件事件联动~~
|
||||
- 事件函数编辑体验优化
|
||||
- 代码沙箱
|
||||
- 物料可触发事件声明
|
||||
|
||||
Vendored
+17
@@ -9,12 +9,16 @@
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue').EffectScope
|
||||
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
|
||||
const computed: typeof import('vue').computed
|
||||
const createApp: typeof import('vue').createApp
|
||||
const createPinia: typeof import('pinia').createPinia
|
||||
const customRef: typeof import('vue').customRef
|
||||
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
|
||||
const defineComponent: typeof import('vue').defineComponent
|
||||
const defineStore: typeof import('pinia').defineStore
|
||||
const effectScope: typeof import('vue').effectScope
|
||||
const getActivePinia: typeof import('pinia').getActivePinia
|
||||
const getCurrentInstance: typeof import('vue').getCurrentInstance
|
||||
const getCurrentScope: typeof import('vue').getCurrentScope
|
||||
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
|
||||
@@ -25,10 +29,17 @@ declare global {
|
||||
const isReadonly: typeof import('vue').isReadonly
|
||||
const isRef: typeof import('vue').isRef
|
||||
const isShallow: typeof import('vue').isShallow
|
||||
const mapActions: typeof import('pinia').mapActions
|
||||
const mapGetters: typeof import('pinia').mapGetters
|
||||
const mapState: typeof import('pinia').mapState
|
||||
const mapStores: typeof import('pinia').mapStores
|
||||
const mapWritableState: typeof import('pinia').mapWritableState
|
||||
const markRaw: typeof import('vue').markRaw
|
||||
const nextTick: typeof import('vue').nextTick
|
||||
const onActivated: typeof import('vue').onActivated
|
||||
const onBeforeMount: typeof import('vue').onBeforeMount
|
||||
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
|
||||
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
|
||||
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
|
||||
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
|
||||
const onDeactivated: typeof import('vue').onDeactivated
|
||||
@@ -46,9 +57,12 @@ declare global {
|
||||
const readonly: typeof import('vue').readonly
|
||||
const ref: typeof import('vue').ref
|
||||
const resolveComponent: typeof import('vue').resolveComponent
|
||||
const setActivePinia: typeof import('pinia').setActivePinia
|
||||
const setMapStoreSuffix: typeof import('pinia').setMapStoreSuffix
|
||||
const shallowReactive: typeof import('vue').shallowReactive
|
||||
const shallowReadonly: typeof import('vue').shallowReadonly
|
||||
const shallowRef: typeof import('vue').shallowRef
|
||||
const storeToRefs: typeof import('pinia').storeToRefs
|
||||
const toRaw: typeof import('vue').toRaw
|
||||
const toRef: typeof import('vue').toRef
|
||||
const toRefs: typeof import('vue').toRefs
|
||||
@@ -59,7 +73,10 @@ declare global {
|
||||
const useCssModule: typeof import('vue').useCssModule
|
||||
const useCssVars: typeof import('vue').useCssVars
|
||||
const useId: typeof import('vue').useId
|
||||
const useLink: typeof import('vue-router').useLink
|
||||
const useModel: typeof import('vue').useModel
|
||||
const useRoute: typeof import('vue-router').useRoute
|
||||
const useRouter: typeof import('vue-router').useRouter
|
||||
const useSlots: typeof import('vue').useSlots
|
||||
const useTemplateRef: typeof import('vue').useTemplateRef
|
||||
const watch: typeof import('vue').watch
|
||||
|
||||
Vendored
+9
@@ -16,19 +16,28 @@ declare module 'vue' {
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
MonacoEditor: typeof import('./src/components/MonacoEditor/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
ScreenRender: typeof import('./src/components/ScreenRender/index.vue')['default']
|
||||
}
|
||||
export interface GlobalDirectives {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
}
|
||||
}
|
||||
|
||||
+2
-10
@@ -1,12 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import AsyncLoader from '@/components/AsyncLoader.vue'
|
||||
|
||||
const ScreenEditor = defineAsyncComponent({
|
||||
loader: () => import('@/editor/index.vue'),
|
||||
loadingComponent: AsyncLoader,
|
||||
delay: 0,
|
||||
})
|
||||
|
||||
const isDesktop = ref(window.innerWidth >= 768)
|
||||
|
||||
const updateDevice = () => {
|
||||
@@ -24,8 +16,8 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<div v-if="isDesktop" class="h-screen overflow-hidden">
|
||||
<ScreenEditor />
|
||||
<div v-if="isDesktop || $route.path === '/preview'" class="h-screen overflow-hidden">
|
||||
<RouterView />
|
||||
</div>
|
||||
<section
|
||||
v-else
|
||||
|
||||
@@ -15,6 +15,7 @@ const modelValue = defineModel({ type: String, default: '' })
|
||||
|
||||
// 编辑器 DOM 元素的引用
|
||||
const editorEL = ref()
|
||||
const editorInstance = shallowRef<editor.IStandaloneCodeEditor>()
|
||||
|
||||
// 因为 monaco-editor 运行需要消耗大量的资源,所以需要在webWorker中运行
|
||||
window.MonacoEnvironment = {
|
||||
@@ -38,6 +39,7 @@ onMounted(() => {
|
||||
language: props.lang || 'json',
|
||||
automaticLayout: true,
|
||||
})
|
||||
editorInstance.value = instance
|
||||
|
||||
// 监听内容变化
|
||||
instance.onDidChangeModelContent(() => {
|
||||
@@ -45,9 +47,16 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
instance.dispose()
|
||||
editorInstance.value?.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
// 监听外部传入的 modelValue 变化,如果和编辑器内容不一致,则更新编辑器内容
|
||||
watch(modelValue, (value) => {
|
||||
if (editorInstance.value && value !== editorInstance.value.getValue()) {
|
||||
editorInstance.value.setValue(value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import { getMaterialComponent } from '@/materials'
|
||||
import { createRunTimeContext } from '@/runtime/context'
|
||||
import type { MaterialSchema } from '@/schema/materials'
|
||||
import type { PageSchema } from '@/schema/page'
|
||||
|
||||
defineOptions({
|
||||
name: 'ScreenRender',
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
page: PageSchema
|
||||
}>()
|
||||
const canvas = computed(() => props.page.canvas)
|
||||
const dataSource = computed(() => props.page.dataSource)
|
||||
const nodes = computed(() => props.page.nodes)
|
||||
const runTimePage = ref(props.page)
|
||||
const context = createRunTimeContext(runTimePage)
|
||||
|
||||
const scale = ref(1)
|
||||
|
||||
provide('dataSource', dataSource)
|
||||
|
||||
const canvasStyle = computed(() => {
|
||||
return {
|
||||
width: canvas.value?.width + 'px',
|
||||
height: canvas.value?.height + 'px',
|
||||
backgroundColor: canvas.value?.backgroundColor,
|
||||
transform: `scale(${scale.value})`,
|
||||
transformOrigin: 'top left',
|
||||
}
|
||||
})
|
||||
|
||||
const canvasWrapperStyle = computed(() => {
|
||||
return {
|
||||
width: `${canvas.value.width * scale.value}px`,
|
||||
height: `${canvas.value.height * scale.value}px`,
|
||||
}
|
||||
})
|
||||
|
||||
// 获取节点的样式
|
||||
const getNodeStyle = (node: MaterialSchema, zIndex: number) => {
|
||||
return {
|
||||
width: `${node.layout.width}px`,
|
||||
height: `${node.layout.height}px`,
|
||||
left: `${node.layout.x}px`,
|
||||
top: `${node.layout.y}px`,
|
||||
zIndex: zIndex + 1,
|
||||
}
|
||||
}
|
||||
|
||||
const createEvents = (node: MaterialSchema) => {
|
||||
const listeners: Record<string, any> = {}
|
||||
const events = node.events || []
|
||||
|
||||
events.forEach((event) => {
|
||||
event.handler = listeners[event.type] = () => {
|
||||
const fn = new Function(event.code)
|
||||
// 执行创建函数
|
||||
fn()
|
||||
}
|
||||
})
|
||||
|
||||
return listeners
|
||||
}
|
||||
|
||||
const init = () => {
|
||||
const scaleX = window.innerWidth / canvas.value.width
|
||||
const scaleY = window.innerHeight / canvas.value.height
|
||||
scale.value = Math.min(scaleX, scaleY)
|
||||
}
|
||||
|
||||
const vm = getCurrentInstance()
|
||||
|
||||
onMounted(() => {
|
||||
const refs: Record<string, any> = {}
|
||||
|
||||
for (const key in vm?.refs) {
|
||||
const res = vm?.refs[key]
|
||||
refs[key] = res
|
||||
}
|
||||
|
||||
context.registerNodeInstance(refs)
|
||||
|
||||
init()
|
||||
window.addEventListener('resize', init)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', init)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full w-full items-center justify-center overflow-hidden">
|
||||
<div :style="canvasWrapperStyle">
|
||||
<div class="relative" :style="canvasStyle">
|
||||
<div
|
||||
v-for="(node, index) in nodes"
|
||||
:key="node.id"
|
||||
:style="getNodeStyle(node, index)"
|
||||
class="absolute"
|
||||
v-on="createEvents(node)"
|
||||
>
|
||||
<component :ref="node.id" :is="getMaterialComponent(node.type)" :schema="node" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { DataSourceSchema } from '@/schema/page'
|
||||
import axios from 'axios'
|
||||
import { getValue } from '@/utils'
|
||||
|
||||
export const useDataSource = (dataId: Ref<string>) => {
|
||||
const dataSource = inject<Ref<DataSourceSchema[]>>('dataSource')
|
||||
const loading = ref(false)
|
||||
const error = ref()
|
||||
// 数据源轮询定时器
|
||||
let timer: number | null = null
|
||||
|
||||
@@ -15,21 +18,20 @@ export const useDataSource = (dataId: Ref<string>) => {
|
||||
|
||||
// 加载数据源数据,如果是 api 类型则发起请求
|
||||
const loadData = async () => {
|
||||
if (timer) clearTimeout(timer)
|
||||
if (!source.value) return
|
||||
|
||||
// 如果是 api 类型且有 url,则发起请求获取数据
|
||||
if (source.value.type === 'api' && source.value.url) {
|
||||
const url = source.value.url
|
||||
|
||||
try {
|
||||
const res = await axios.get(url, {
|
||||
params: {
|
||||
...source.value.params,
|
||||
},
|
||||
})
|
||||
console.log('loadData res', res)
|
||||
data.value = res.data
|
||||
loading.value = true
|
||||
error.value = null
|
||||
const res = await fetchData(source.value)
|
||||
data.value = res
|
||||
} catch (err) {
|
||||
error.value = err
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (source.value.interval) {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
@@ -55,6 +57,44 @@ export const useDataSource = (dataId: Ref<string>) => {
|
||||
|
||||
return {
|
||||
data,
|
||||
loadData,
|
||||
error,
|
||||
loading,
|
||||
refresh: loadData,
|
||||
}
|
||||
}
|
||||
|
||||
const requestMap: Record<string, Promise<unknown>> = {}
|
||||
|
||||
// 获取数据源数据,url相同,params相同则复用请求
|
||||
export const fetchData = async (
|
||||
source: Pick<DataSourceSchema, 'type' | 'url' | 'params' | 'method' | 'responsePath'>,
|
||||
) => {
|
||||
const queryParams = {
|
||||
...source.params,
|
||||
}
|
||||
|
||||
const paramsKey = source.method === 'POST' ? 'data' : 'params'
|
||||
|
||||
const config = {
|
||||
url: source.url,
|
||||
method: source.method || 'GET',
|
||||
[paramsKey]: queryParams,
|
||||
}
|
||||
|
||||
const key = JSON.stringify(config)
|
||||
|
||||
if (requestMap[key]) {
|
||||
return requestMap[key]
|
||||
}
|
||||
|
||||
const promise = axios
|
||||
.request(config)
|
||||
.then((res) => getValue(res.data, source.responsePath))
|
||||
.finally(() => {
|
||||
delete requestMap[key]
|
||||
})
|
||||
|
||||
requestMap[key] = promise
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
@@ -8,12 +8,25 @@ import CanvasRoot from '@/editor/canvas/index.vue'
|
||||
import PropertyPanel from '@/editor/panels/property/index.vue'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { getPublishedPage } from '@/utils/publish'
|
||||
|
||||
defineOptions({
|
||||
name: 'ScreenEditor',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const editorStore = useEditorStore()
|
||||
const { panelVisible, dataSource } = storeToRefs(editorStore)
|
||||
const pageId = route.query.id as string
|
||||
|
||||
if (pageId) {
|
||||
const page = getPublishedPage(pageId)
|
||||
if (page) {
|
||||
editorStore.setPage(page)
|
||||
}
|
||||
}
|
||||
|
||||
provide('dataSource', dataSource)
|
||||
|
||||
const materialWidth = computed(() => (panelVisible.value.material ? '260px' : '0'))
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useUndoRedo } from '@/composables/useUndoRedo'
|
||||
import 'element-plus/es/components/select/style/css'
|
||||
import 'element-plus/es/components/input/style/css'
|
||||
import 'element-plus/es/components/checkbox/style/css'
|
||||
import 'element-plus/es/components/input-number/style/css'
|
||||
|
||||
defineProps(['setters', 'formData'])
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
<!-- 节点事件配置 -->
|
||||
<script setup lang="ts">
|
||||
import type { MaterialEvent } from '@/schema/materials'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { deepClone } from '@/utils'
|
||||
import { copy } from '@/utils/clipboard'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
defineOptions({
|
||||
name: 'NodeEvents',
|
||||
})
|
||||
|
||||
const editorStore = useEditorStore()
|
||||
const { selectedNode, nodes } = storeToRefs(editorStore)
|
||||
|
||||
// 深拷贝事件列表,避免未确认时改到 store
|
||||
const eventList = ref(deepClone(selectedNode.value?.events || []))
|
||||
|
||||
const activeEvent = ref<MaterialEvent | null>(null)
|
||||
const removeEvent = ref<MaterialEvent>()
|
||||
const removeVisible = ref(false)
|
||||
|
||||
const selectEvent = (event: MaterialEvent) => {
|
||||
activeEvent.value = event
|
||||
}
|
||||
|
||||
// 添加事件
|
||||
const onAdd = () => {
|
||||
const newEvent: MaterialEvent = {
|
||||
title: '未命名',
|
||||
name: '未命名',
|
||||
type: '',
|
||||
code: '',
|
||||
}
|
||||
eventList.value.push(newEvent)
|
||||
selectEvent(newEvent)
|
||||
}
|
||||
|
||||
// 移除事件
|
||||
const onRemove = (event: MaterialEvent) => {
|
||||
eventList.value = eventList.value.filter((item) => item !== event)
|
||||
if (activeEvent.value === event) {
|
||||
activeEvent.value = eventList.value[0] || null
|
||||
}
|
||||
}
|
||||
|
||||
// 打开删除确认弹窗
|
||||
const onOpenRemove = (event: MaterialEvent) => {
|
||||
removeEvent.value = event
|
||||
removeVisible.value = true
|
||||
}
|
||||
|
||||
// 取消删除
|
||||
const onCancelRemove = () => {
|
||||
removeVisible.value = false
|
||||
removeEvent.value = undefined
|
||||
}
|
||||
|
||||
// 确认删除事件
|
||||
const onConfirmRemove = () => {
|
||||
if (removeEvent.value) {
|
||||
onRemove(removeEvent.value)
|
||||
}
|
||||
onCancelRemove()
|
||||
}
|
||||
|
||||
// 复制节点ID
|
||||
const copyNodeId = (id: string) => {
|
||||
copy(id)
|
||||
ElMessage.success('复制成功')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
// 把编辑中的事件写回节点
|
||||
save() {
|
||||
const node = selectedNode.value
|
||||
if (!node) return
|
||||
|
||||
editorStore.updateNode(node.id, {
|
||||
...node,
|
||||
events: eventList.value,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (eventList.value.length > 0) {
|
||||
activeEvent.value = eventList.value[0] ?? null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-5">
|
||||
<div class="w-50 flex-none border border-white/20">
|
||||
<div class="p-2">
|
||||
<el-button type="primary" size="small" @click="onAdd">添加</el-button>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="eventList.length === 0" description="暂无事件" :image-size="72" />
|
||||
|
||||
<div
|
||||
v-for="item in eventList"
|
||||
:key="item.name"
|
||||
class="flex items-center justify-between h-10 bg-white/10 hover:bg-white/20 cursor-pointer p-2 mb-2 last:mb-0 transition-colors"
|
||||
:class="{
|
||||
'!bg-white/70 !text-black': activeEvent === item,
|
||||
}"
|
||||
@click="selectEvent(item)"
|
||||
>
|
||||
<span>{{ item.title }}</span>
|
||||
<span @click.stop="onOpenRemove(item)"><Icon icon="mdi:remove"></Icon></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 border border-white/20 p-2 min-h-64">
|
||||
<div class="mb-4">
|
||||
<el-select placeholder="复制节点 ID" @change="copyNodeId">
|
||||
<el-option v-for="node in nodes" :key="node.id" :value="node.id" :label="node.name">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<el-form v-if="activeEvent">
|
||||
<el-form-item label="标题">
|
||||
<el-input v-model="activeEvent.title"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="activeEvent.name"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-input v-model="activeEvent.type"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="函数体">
|
||||
<MonacoEditor v-model="activeEvent.code" lang="javascript" class="min-h-80" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-empty v-else description="请选择或添加事件" :image-size="96" />
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="removeVisible"
|
||||
title="删除事件"
|
||||
width="400"
|
||||
append-to-body
|
||||
@closed="removeEvent = undefined"
|
||||
>
|
||||
<span>确认删除事件“{{ removeEvent?.title }}”吗?</span>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="onCancelRemove">取消</el-button>
|
||||
<el-button type="danger" @click="onConfirmRemove">删除</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- 节点属性面板 -->
|
||||
<script setup lang="ts">
|
||||
import { getMaterialSetters } from '@/materials'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
@@ -5,6 +6,7 @@ import { storeToRefs } from 'pinia'
|
||||
import FormCreate from './FormCreate.vue'
|
||||
import DataSource from '@/editor/panels/property/components/DataSource.vue'
|
||||
import AsyncLoader from '@/components/AsyncLoader.vue'
|
||||
import NodeEvents from './NodeEvents.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'NodeProperty',
|
||||
@@ -18,6 +20,7 @@ const MonacoEditor = defineAsyncComponent({
|
||||
|
||||
const editorStore = useEditorStore()
|
||||
const { selectedNode } = storeToRefs(editorStore)
|
||||
const nodeEvents = useTemplateRef('nodeEvents')
|
||||
|
||||
const setters = computed(() => {
|
||||
return getMaterialSetters(selectedNode.value?.type || '') ?? []
|
||||
@@ -51,13 +54,16 @@ const layoutSetters = [
|
||||
},
|
||||
]
|
||||
|
||||
const visible = ref(false)
|
||||
const jsonVisible = ref(false)
|
||||
const eventVisible = ref(false)
|
||||
|
||||
const jsonContent = ref('')
|
||||
|
||||
// 显示 JSON 编辑器
|
||||
const showJson = () => {
|
||||
jsonContent.value = JSON.stringify(selectedNode.value, null, 2)
|
||||
console.log('jsonContent.value', jsonContent.value)
|
||||
visible.value = true
|
||||
jsonVisible.value = true
|
||||
}
|
||||
|
||||
// 确认编辑 JSON
|
||||
@@ -70,7 +76,13 @@ const onConfirm = () => {
|
||||
id: selectedNode.value?.id || '',
|
||||
type: selectedNode.value?.type || '',
|
||||
})
|
||||
visible.value = false
|
||||
jsonVisible.value = false
|
||||
}
|
||||
|
||||
// 确认编辑事件
|
||||
const onConfirmEvent = () => {
|
||||
nodeEvents.value?.save()
|
||||
eventVisible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -78,7 +90,14 @@ const onConfirm = () => {
|
||||
<div class="px-2">
|
||||
<div class="flex items-center justify-between font-bold p-1">
|
||||
<span>{{ selectedNode?.name }}</span>
|
||||
<span class="cursor-pointer" @click="showJson"><Icon icon="si:json-duotone" /></span>
|
||||
<div class="flex gap-2">
|
||||
<span class="cursor-pointer" @click="eventVisible = true" title="编辑事件">
|
||||
<Icon icon="solar:bolt-bold-duotone" />
|
||||
</span>
|
||||
<span class="cursor-pointer" @click="showJson" title="编辑 JSON">
|
||||
<Icon icon="si:json-duotone" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTab" type="border-card" stretch>
|
||||
@@ -98,14 +117,24 @@ const onConfirm = () => {
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-drawer v-model="visible" title="编辑 JSON" class="json-drawer" destroy-on-close>
|
||||
<!-- 编辑 JSON -->
|
||||
<el-drawer v-model="jsonVisible" title="编辑 JSON" class="json-drawer" destroy-on-close>
|
||||
<MonacoEditor v-model="jsonContent" lang="json" />
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button @click="jsonVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onConfirm">确认</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 事件配置 -->
|
||||
<el-dialog destroy-on-close title="事件配置" width="800" v-model="eventVisible">
|
||||
<NodeEvents ref="nodeEvents" />
|
||||
<template #footer>
|
||||
<el-button @click="eventVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onConfirmEvent">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import { ElMessage } from 'element-plus'
|
||||
import AsyncLoader from '@/components/AsyncLoader.vue'
|
||||
import DataSourceManager from '@/editor/toolbar/components/DataSourceManager.vue'
|
||||
import { publishPage } from '@/utils/publish'
|
||||
|
||||
const MonacoEditor = defineAsyncComponent({
|
||||
loader: () => import('@/components/MonacoEditor/index.vue'),
|
||||
@@ -12,13 +14,16 @@ const MonacoEditor = defineAsyncComponent({
|
||||
delay: 0,
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const editorStore = useEditorStore()
|
||||
const { page } = storeToRefs(editorStore)
|
||||
|
||||
// 全局数据源
|
||||
const dataSourceVisible = ref(false)
|
||||
const visible = ref(false)
|
||||
const jsonContent = ref('')
|
||||
|
||||
const inputRef = useTemplateRef<HTMLInputElement>('inputRef')
|
||||
const DataSourceManagerRef = useTemplateRef('DataSourceManagerRef')
|
||||
|
||||
// 显示 JSON 编辑器
|
||||
const showJson = () => {
|
||||
@@ -76,19 +81,44 @@ const onFileChange = (e: Event) => {
|
||||
// 读取文件内容
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
// 数据源配置
|
||||
const onDataSource = () => {
|
||||
dataSourceVisible.value = true
|
||||
}
|
||||
|
||||
// 保存
|
||||
const onSave = () => {
|
||||
DataSourceManagerRef.value?.save()
|
||||
dataSourceVisible.value = false
|
||||
}
|
||||
|
||||
// 预览
|
||||
const onPreview = () => {
|
||||
router.push('/preview')
|
||||
}
|
||||
|
||||
// 发布
|
||||
const onPublish = () => {
|
||||
const id = publishPage(page.value)
|
||||
router.push('/screen?id=' + id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-end gap-5">
|
||||
<span class="border p-1 rounded-lg cursor-pointer">
|
||||
<span class="border p-1 rounded-lg cursor-pointer" title="预览" @click="onPreview">
|
||||
<Icon icon="icon-park-outline:preview-open"> </Icon>
|
||||
</span>
|
||||
<span class="border p-1 rounded-lg cursor-pointer" @click="showJson" title="编辑 JSON">
|
||||
<Icon icon="si:json-line"> </Icon>
|
||||
</span>
|
||||
<span class="border p-1 rounded-lg cursor-pointer">
|
||||
<span class="border p-1 rounded-lg cursor-pointer" title="发布" @click="onPublish">
|
||||
<Icon icon="hugeicons:internet"> </Icon>
|
||||
</span>
|
||||
<span class="border p-1 rounded-lg cursor-pointer" @click="onDataSource" title="数据源配置">
|
||||
<Icon icon="mdi:database"> </Icon>
|
||||
</span>
|
||||
<span class="border p-1 rounded-lg cursor-pointer" @click="onImport" title="导入">
|
||||
<Icon icon="lets-icons:import"> </Icon>
|
||||
</span>
|
||||
@@ -108,5 +138,15 @@ const onFileChange = (e: Event) => {
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog destroy-on-close title="数据源配置" v-model="dataSourceVisible" width="800">
|
||||
<DataSourceManager ref="DataSourceManagerRef" />
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<el-button @click="dataSourceVisible = false">取消</el-button>
|
||||
<el-button @click="onSave">保存</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
<script setup lang="ts">
|
||||
import { fetchData } from '@/composables/usseDataSource'
|
||||
import type { DataSourceSchema } from '@/schema/page'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { deepClone } from '@/utils'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
defineOptions({
|
||||
name: 'DataSourceManager',
|
||||
})
|
||||
|
||||
const editorStore = useEditorStore()
|
||||
const { dataSource } = storeToRefs(editorStore)
|
||||
|
||||
type DataSourceEditor = Omit<DataSourceSchema, 'data' | 'params'> & {
|
||||
data?: string
|
||||
params?: string
|
||||
}
|
||||
|
||||
// 深拷贝数据源
|
||||
const data = ref<DataSourceEditor[]>(
|
||||
deepClone(dataSource.value).map((item) => {
|
||||
return {
|
||||
...item,
|
||||
data: item.data ? JSON.stringify(item.data, null, 2) : undefined,
|
||||
params: item.params ? JSON.stringify(item.params, null, 2) : undefined,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
// 请求响应数据
|
||||
const resData = ref('')
|
||||
const activeSource = ref<DataSourceEditor>()
|
||||
const removeSource = ref<DataSourceEditor>()
|
||||
const removeVisible = ref(false)
|
||||
|
||||
const selectDataSource = (source: DataSourceEditor) => {
|
||||
activeSource.value = source
|
||||
}
|
||||
|
||||
// 添加数据源
|
||||
const onAdd = () => {
|
||||
const newSource: DataSourceEditor = {
|
||||
id: crypto.randomUUID(),
|
||||
name: '未命名数据源',
|
||||
type: 'static',
|
||||
data: '',
|
||||
params: '{}',
|
||||
}
|
||||
data.value.push(newSource)
|
||||
selectDataSource(newSource)
|
||||
}
|
||||
|
||||
// 移除数据源
|
||||
const onRemove = (source: DataSourceEditor) => {
|
||||
const index = data.value.findIndex((item) => item.id === source.id)
|
||||
if (index !== -1) {
|
||||
data.value.splice(index, 1)
|
||||
if (activeSource.value?.id === source.id) {
|
||||
activeSource.value = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 打开移除确认弹窗
|
||||
const onOpenRemove = (source: DataSourceEditor) => {
|
||||
removeSource.value = source
|
||||
removeVisible.value = true
|
||||
}
|
||||
|
||||
// 确认移除数据源
|
||||
const onConfirmRemove = () => {
|
||||
if (removeSource.value) {
|
||||
onRemove(removeSource.value)
|
||||
}
|
||||
removeVisible.value = false
|
||||
if (data.value.length > 0) {
|
||||
activeSource.value = data.value[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
const onRequest = async () => {
|
||||
const source = activeSource.value
|
||||
if (!source) return
|
||||
|
||||
const res = await fetchData({
|
||||
type: source.type,
|
||||
url: source.url,
|
||||
params: source.params ? JSON.parse(source.params) : undefined,
|
||||
method: source.method,
|
||||
responsePath: source.responsePath,
|
||||
})
|
||||
console.log('请求结果', res)
|
||||
resData.value = JSON.stringify(res, null, 2)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
save() {
|
||||
const _data = deepClone(data.value).map((item) => {
|
||||
return {
|
||||
...item,
|
||||
data: item.data ? JSON.parse(item.data) : undefined,
|
||||
params: item.params ? JSON.parse(item.params) : undefined,
|
||||
}
|
||||
})
|
||||
// 更新数据
|
||||
editorStore.page.dataSource = _data
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (data.value.length > 0) {
|
||||
activeSource.value = data.value[0]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-5">
|
||||
<div class="w-50 flex-none border border-white/20">
|
||||
<div class="p-2">
|
||||
<el-button type="primary" size="small" @click="onAdd">添加</el-button>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="data.length === 0" description="暂无数据源" :image-size="72" />
|
||||
|
||||
<div
|
||||
v-for="item in data"
|
||||
:key="item.id"
|
||||
class="flex items-center justify-between h-10 bg-white/10 hover:bg-white/20 cursor-pointer p-2 mb-2 last:mb-0 transition-colors"
|
||||
:class="{
|
||||
'!bg-white/70 !text-black': activeSource?.id === item.id,
|
||||
}"
|
||||
@click="selectDataSource(item)"
|
||||
>
|
||||
<span>{{ item.name }}</span>
|
||||
<span @click.stop="onOpenRemove(item)"><Icon icon="mdi:remove"></Icon></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 border border-white/20 p-2 min-h-64">
|
||||
<el-form v-if="activeSource">
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="activeSource.name"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-radio-group v-model="activeSource.type">
|
||||
<el-radio-button label="静态" value="static"></el-radio-button>
|
||||
<el-radio-button label="API" value="api"></el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="数据" v-if="activeSource.type === 'static'">
|
||||
<MonacoEditor v-model="activeSource.data" lang="json" class="min-h-80" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- api 数据源 -->
|
||||
<div v-else>
|
||||
<el-form-item label="请求地址">
|
||||
<el-input v-model="activeSource.url"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="轮询周期">
|
||||
<el-input v-model="activeSource.interval" placeholder="单位:秒"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="请求参数">
|
||||
<MonacoEditor v-model="activeSource.params" lang="json" class="min-h-80" />
|
||||
</el-form-item>
|
||||
<el-form-item label="响应路径">
|
||||
<el-input v-model="activeSource.responsePath" lang="json" />
|
||||
</el-form-item>
|
||||
<el-form-item class="flex">
|
||||
<el-button type="primary" @click="onRequest">发送请求</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="请求预览">
|
||||
<MonacoEditor v-model="resData" lang="json" class="min-h-80" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<el-empty v-else description="请选择或添加数据源" :image-size="96" />
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="removeVisible" title="删除数据源" width="400">
|
||||
<span>确认删除数据源“{{ removeSource?.name }}”吗?</span>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="removeVisible = false">取消</el-button>
|
||||
<el-button type="danger" @click="onConfirmRemove">删除</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -15,7 +15,7 @@ const chartRef = useTemplateRef('chartRef')
|
||||
|
||||
const dataId = computed(() => props.schema.dataId!)
|
||||
|
||||
const { data } = useDataSource(dataId)
|
||||
const { data, loading, error, refresh } = useDataSource(dataId)
|
||||
|
||||
const option = computed(() => {
|
||||
const _option = props.schema.props.option
|
||||
@@ -58,10 +58,13 @@ onMounted(() => {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full" ref="chartRef"></div>
|
||||
<div v-if="error">{{ error.message }}</div>
|
||||
<div v-loading="loading" class="w-full h-full" ref="chartRef"></div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -5,13 +5,21 @@ defineOptions({
|
||||
name: 'TextMaterial',
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
schema: MaterialSchema
|
||||
}>()
|
||||
|
||||
const textStyle = computed(() => {
|
||||
const style = props.schema.style
|
||||
return {
|
||||
...style,
|
||||
fontSize: style?.fontSize ? style.fontSize + 'px' : undefined,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="schema.style">{{ schema.props.content }}</div>
|
||||
<div :style="textStyle">{{ schema.props.content }}</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -16,6 +16,11 @@ const textMaterial: MaterialDefinition = {
|
||||
label: '颜色',
|
||||
key: 'style.color',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
label: '字号',
|
||||
key: 'style.fontSize',
|
||||
},
|
||||
],
|
||||
schema: {
|
||||
type: 'text',
|
||||
@@ -28,10 +33,18 @@ const textMaterial: MaterialDefinition = {
|
||||
},
|
||||
style: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
},
|
||||
props: {
|
||||
content: '文本内容',
|
||||
},
|
||||
events: [
|
||||
{
|
||||
type: 'click',
|
||||
name: 'fn',
|
||||
code: `console.log(123123123)`,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
|
||||
defineOptions({
|
||||
name: 'ScreenPreview',
|
||||
})
|
||||
|
||||
const editorStore = useEditorStore()
|
||||
const { page } = storeToRefs(editorStore)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full w-full items-center justify-center overflow-hidden">
|
||||
<ScreenRender :page="page" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { getPublishedPage } from '@/utils/publish'
|
||||
|
||||
defineOptions({
|
||||
name: 'ScreenPage',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const page = getPublishedPage(route.query.id as string)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ScreenRender v-if="page" :page="page" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
+21
-1
@@ -2,7 +2,27 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [],
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/editor',
|
||||
},
|
||||
{
|
||||
path: '/editor',
|
||||
name: 'editor',
|
||||
component: () => import('@/editor/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/preview',
|
||||
name: 'preview',
|
||||
component: () => import('@/pages/preview/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/screen',
|
||||
name: 'screen',
|
||||
component: () => import('@/pages/screen/index.vue'),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// 这个文件是为了处理运行时上下文的创建和管理,提供一个统一的接口来获取页面中的节点信息。
|
||||
import type { MaterialSchema } from '@/schema/materials'
|
||||
import type { PageSchema } from '@/schema/page'
|
||||
import { setValue } from '@/utils'
|
||||
|
||||
interface RuntimeContext {
|
||||
/** 获取节点 */
|
||||
getNode(id: string): MaterialSchema | undefined
|
||||
/** 设置节点属性 */
|
||||
setAttribute(id: string, key: string, value: any): void
|
||||
/** 设置节点的prop */
|
||||
setProp(id: string, key: string, value: any): void
|
||||
/** 设置节点的style */
|
||||
setStyle(id: string, key: string, value: any): void
|
||||
/** 注册节点实例 */
|
||||
registerNodeInstance(instance: Record<string, Record<string, any>>): void
|
||||
/** 更新指定节点实例 */
|
||||
trigger(id: string, name: string, ...args: any[]): any
|
||||
/** 刷新节点实例 */
|
||||
refreshNodeByDataId(id: string, ...args: any[]): void
|
||||
/** 派发事件到指定节点实例 */
|
||||
dispatch(id: string, name: string, payload: any): void
|
||||
}
|
||||
|
||||
// 创建运行时上下文
|
||||
export const createRunTimeContext = (page: Ref<PageSchema>) => {
|
||||
let InstanceMap: Record<string, Record<string, any>> = {}
|
||||
|
||||
const getNode: RuntimeContext['getNode'] = (id: string) => {
|
||||
return page.value.nodes.find((node) => node.id === id)
|
||||
}
|
||||
|
||||
/** 设置节点属性 */
|
||||
const setAttribute: RuntimeContext['setAttribute'] = (id: string, key: string, value: any) => {
|
||||
const node = getNode(id)
|
||||
if (node) {
|
||||
setValue(node.props, key, value)
|
||||
} else {
|
||||
console.warn(`Node with id ${id} not found`)
|
||||
}
|
||||
}
|
||||
|
||||
/** 设置节点的prop */
|
||||
const setProp: RuntimeContext['setProp'] = (id: string, key: string, value: any) => {
|
||||
setAttribute(id, `props.${key}`, value)
|
||||
}
|
||||
|
||||
/** 设置节点的style */
|
||||
const setStyle: RuntimeContext['setStyle'] = (id: string, key: string, value: any) => {
|
||||
setAttribute(id, `style.${key}`, value)
|
||||
}
|
||||
|
||||
/** 注册节点实例 */
|
||||
const registerNodeInstance: RuntimeContext['registerNodeInstance'] = (instance: any) => {
|
||||
InstanceMap = instance
|
||||
}
|
||||
|
||||
/** 更新指定节点实例 */
|
||||
const trigger: RuntimeContext['trigger'] = (id: string, name: string, ...args: any[]) => {
|
||||
const instance = InstanceMap[id]
|
||||
if (instance && typeof instance[name] === 'function') {
|
||||
/** 将函数调用的结果返回,假设调用了123 trigger也返回123 */
|
||||
return instance[name](...args)
|
||||
} else {
|
||||
console.warn(`Instance or method ${name} not found for node with id ${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新指定dataId的节点实例 */
|
||||
const refreshNodeByDataId: RuntimeContext['refreshNodeByDataId'] = (
|
||||
id: string,
|
||||
...args: any[]
|
||||
) => {
|
||||
const nodes = page.value.nodes.filter((node) => node.dataId === id)
|
||||
nodes.forEach((node) => {
|
||||
trigger(node.id, 'refresh', ...args)
|
||||
})
|
||||
}
|
||||
|
||||
/** 派发事件到指定节点实例 */
|
||||
const dispatch: RuntimeContext['dispatch'] = (id: string, name: string, payload: any) => {
|
||||
const node = getNode(id)
|
||||
if (node) {
|
||||
const event = node.events?.find((e) => e.name === name)
|
||||
if (event) {
|
||||
// 如果找到事件就执行
|
||||
event.handler?.(payload)
|
||||
}
|
||||
} else {
|
||||
console.warn(`Node with id ${id} not found`)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getNode,
|
||||
setAttribute,
|
||||
setProp,
|
||||
setStyle,
|
||||
registerNodeInstance,
|
||||
trigger,
|
||||
refreshNodeByDataId,
|
||||
dispatch,
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,15 @@ interface Layout {
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface MaterialEvent {
|
||||
type: string
|
||||
title: string
|
||||
name: string
|
||||
code: string
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
handler?: Function
|
||||
}
|
||||
|
||||
// 物料数据结构
|
||||
export interface MaterialSchema {
|
||||
type: string
|
||||
@@ -15,6 +24,7 @@ export interface MaterialSchema {
|
||||
layout: Layout
|
||||
style?: Record<string, any>
|
||||
props: Record<string, any>
|
||||
events?: MaterialEvent[]
|
||||
}
|
||||
|
||||
export interface SetterSchema {
|
||||
|
||||
@@ -17,12 +17,16 @@ export interface DataSourceSchema {
|
||||
url?: string
|
||||
/** api 类型数据源的轮询间隔,单位毫秒,默认不轮询 */
|
||||
interval?: number
|
||||
method?: 'GET' | 'POST'
|
||||
/** 响应数据路径 */
|
||||
responsePath?: string
|
||||
/** 预设参数 */
|
||||
params?: Record<string, any>
|
||||
}
|
||||
|
||||
// 页面数据结构
|
||||
export interface PageSchema {
|
||||
id?: string
|
||||
canvas: CanvasSchema
|
||||
nodes: MaterialSchema[]
|
||||
dataSource: DataSourceSchema[]
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// 剪贴板操作
|
||||
const legacyCopy = (text: string) => {
|
||||
const input = document.createElement('input')
|
||||
input.value = text
|
||||
document.body.appendChild(input)
|
||||
input.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(input)
|
||||
}
|
||||
|
||||
/** 复制文本 */
|
||||
export const copy = (text: string) => {
|
||||
try {
|
||||
navigator.clipboard.writeText(text)
|
||||
} catch {
|
||||
legacyCopy(text)
|
||||
}
|
||||
}
|
||||
+32
-4
@@ -1,5 +1,6 @@
|
||||
// 获取对象中指定路径的值
|
||||
export function getValue(target: any, key: string) {
|
||||
/** 获取对象中指定路径的值 */
|
||||
export const getValue = (target: any, key?: string) => {
|
||||
if (!key) return target
|
||||
// 将 'props.content' 转换为 ['props', 'content']
|
||||
const keys = key.split('.')
|
||||
while (keys.length) {
|
||||
@@ -8,8 +9,8 @@ export function getValue(target: any, key: string) {
|
||||
return target
|
||||
}
|
||||
|
||||
// 设置对象中指定路径的值
|
||||
export function setValue(target: any, key: string, value: any) {
|
||||
/** 设置对象中指定路径的值 */
|
||||
export const setValue = (target: any, key: string, value: any) => {
|
||||
const keys = key.split('.')
|
||||
const lastKey = keys.pop()!
|
||||
|
||||
@@ -19,3 +20,30 @@ export function setValue(target: any, key: string, value: any) {
|
||||
|
||||
target[lastKey] = value
|
||||
}
|
||||
|
||||
/** 深拷贝对象 */
|
||||
export function deepClone<T>(obj: T): T {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj
|
||||
}
|
||||
|
||||
if (obj instanceof Date) {
|
||||
return new Date(obj.getTime()) as any
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
const arrCopy = [] as any[]
|
||||
for (const item of obj) {
|
||||
arrCopy.push(deepClone(item))
|
||||
}
|
||||
return arrCopy as any
|
||||
}
|
||||
|
||||
const objCopy = {} as { [key: string]: any }
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
objCopy[key] = deepClone((obj as any)[key])
|
||||
}
|
||||
}
|
||||
return objCopy as T
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { PageSchema } from '@/schema/page'
|
||||
|
||||
const PUBLISHED_PAGE_STORAGE_KEY = 'my-v0:published-page'
|
||||
|
||||
/** 发布页面 */
|
||||
export const publishPage = (page: PageSchema) => {
|
||||
const value = JSON.parse(localStorage.getItem(PUBLISHED_PAGE_STORAGE_KEY) || '{}') as Record<
|
||||
string,
|
||||
PageSchema
|
||||
>
|
||||
|
||||
const id = page.id || crypto.randomUUID()
|
||||
value[id] = page
|
||||
page.id = id
|
||||
localStorage.setItem(PUBLISHED_PAGE_STORAGE_KEY, JSON.stringify(value))
|
||||
return id
|
||||
}
|
||||
|
||||
/** 获取已发布的页面 */
|
||||
export const getPublishedPage = (id: string): PageSchema | null => {
|
||||
const value = localStorage.getItem(PUBLISHED_PAGE_STORAGE_KEY)
|
||||
const map = JSON.parse(value || '{}') as Record<string, PageSchema>
|
||||
const page = map[id]
|
||||
if (!page) {
|
||||
throw new Error(`Published page with id ${id} not found`)
|
||||
}
|
||||
return page
|
||||
}
|
||||
+1
-1
@@ -14,7 +14,7 @@ export default defineConfig({
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
AutoImport({
|
||||
imports: ['vue'],
|
||||
imports: ['vue', 'vue-router', 'pinia'],
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
Components({
|
||||
|
||||
Reference in New Issue
Block a user