@@ -100,11 +100,11 @@ Element Plus CSS 为 `116.01 kB`(gzip `16.05 kB`),随编辑器模块加载
|
||||
- ~~事件调度机制~~
|
||||
- ~~事件配置面板~~
|
||||
- ~~跨组件事件联动~~
|
||||
- 事件函数编辑体验优化
|
||||
- 代码沙箱
|
||||
- 物料可触发事件声明
|
||||
- ~~事件函数编辑体验优化~~
|
||||
- ~~代码沙箱~~
|
||||
- ~~物料可触发事件声明~~
|
||||
|
||||
### 五、AI Agent 核心能力
|
||||
### 五、AI Agent
|
||||
|
||||
- 大模型接入与调试
|
||||
- 提示词工程
|
||||
@@ -122,7 +122,7 @@ Element Plus CSS 为 `116.01 kB`(gzip `16.05 kB`),随编辑器模块加载
|
||||
- 子图与节点重试
|
||||
- Responses API
|
||||
|
||||
### 六、全栈应用落地
|
||||
### 六、全栈
|
||||
|
||||
- 项目整体搭建
|
||||
- 最小会话构建
|
||||
|
||||
Vendored
+1
@@ -13,6 +13,7 @@ declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AsyncLoader: typeof import('./src/components/AsyncLoader.vue')['default']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||
|
||||
+7
-1
@@ -16,7 +16,13 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<div v-if="isDesktop || $route.path === '/preview'" class="h-screen overflow-hidden">
|
||||
<div
|
||||
v-if="isDesktop || $route.path !== '/editor'"
|
||||
class="h-screen overflow-hidden"
|
||||
:class="{
|
||||
' flex items-center justify-center': $route.path !== '/editor',
|
||||
}"
|
||||
>
|
||||
<RouterView />
|
||||
</div>
|
||||
<section
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { getMaterialComponent } from '@/materials'
|
||||
import { createRunTimeContext } from '@/runtime/context'
|
||||
import { runSandBox } from '@/runtime/sandbox'
|
||||
import type { MaterialSchema } from '@/schema/materials'
|
||||
import type { PageSchema } from '@/schema/page'
|
||||
|
||||
@@ -54,10 +55,9 @@ const createEvents = (node: MaterialSchema) => {
|
||||
const events = node.events || []
|
||||
|
||||
events.forEach((event) => {
|
||||
event.handler = listeners[event.type] = () => {
|
||||
const fn = new Function(event.code)
|
||||
// 执行创建函数
|
||||
fn()
|
||||
event.handler = listeners[event.type] = (payload: any) => {
|
||||
// 运行事件代码,将上下文和节点信息传入沙箱
|
||||
runSandBox(event.code, { $context: context, $node: node, $payload: payload })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -169,5 +169,3 @@ const onCommand = (command: keyof typeof commendMap) => {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<!-- 节点事件配置 -->
|
||||
<script setup lang="ts">
|
||||
import { getMaterialEventOptions } from '@/materials'
|
||||
import type { MaterialEvent } from '@/schema/materials'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { deepClone } from '@/utils'
|
||||
@@ -17,8 +18,30 @@ const { selectedNode, nodes } = storeToRefs(editorStore)
|
||||
// 深拷贝事件列表,避免未确认时改到 store
|
||||
const eventList = ref(deepClone(selectedNode.value?.events || []))
|
||||
|
||||
const dispatch = computed(() => {
|
||||
return nodes.value.map((node) => {
|
||||
return {
|
||||
label: node.name,
|
||||
value: node.id,
|
||||
children: node.events?.map((event) => {
|
||||
return {
|
||||
label: event.title,
|
||||
value: event.name,
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 获取当前节点类型的事件选项
|
||||
const eventOptions = computed(() => {
|
||||
return getMaterialEventOptions(selectedNode.value?.type || '')
|
||||
})
|
||||
// 当前选中的事件
|
||||
const activeEvent = ref<MaterialEvent | null>(null)
|
||||
// 当前要删除的事件
|
||||
const removeEvent = ref<MaterialEvent>()
|
||||
// 删除确认弹窗是否可见
|
||||
const removeVisible = ref(false)
|
||||
|
||||
const selectEvent = (event: MaterialEvent) => {
|
||||
@@ -71,6 +94,30 @@ const copyNodeId = (id: string) => {
|
||||
ElMessage.success('复制成功')
|
||||
}
|
||||
|
||||
// 插入 dispatchEvent 代码
|
||||
const dispatchEvent = ref()
|
||||
|
||||
// 插入 dispatchEvent 代码的值
|
||||
const insertDispatchCode = (value: unknown) => {
|
||||
if (
|
||||
!activeEvent.value ||
|
||||
!Array.isArray(value) ||
|
||||
value.length !== 2 ||
|
||||
value.some((item) => typeof item !== 'string')
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const [id, name] = value
|
||||
const code = `\n$context.dispatch('${id}', '${name}', $payload)\n`
|
||||
activeEvent.value.code += code
|
||||
|
||||
// 延迟防止被组件内部逻辑覆盖
|
||||
nextTick(() => {
|
||||
dispatchEvent.value = undefined
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
// 把编辑中的事件写回节点
|
||||
save() {
|
||||
@@ -115,11 +162,18 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<div class="flex-1 border border-white/20 p-2 min-h-64">
|
||||
<div class="mb-4">
|
||||
<div class="flex gap-5 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>
|
||||
|
||||
<el-cascader
|
||||
placeholder="触发事件"
|
||||
:options="dispatch"
|
||||
v-model="dispatchEvent"
|
||||
@change="insertDispatchCode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-form v-if="activeEvent">
|
||||
@@ -130,7 +184,13 @@ onMounted(() => {
|
||||
<el-input v-model="activeEvent.name"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-input v-model="activeEvent.type"></el-input>
|
||||
<el-select
|
||||
v-model="activeEvent.type"
|
||||
:options="eventOptions"
|
||||
allow-create
|
||||
filterable
|
||||
placeholder="请选择事件类型"
|
||||
></el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="函数体">
|
||||
<MonacoEditor v-model="activeEvent.code" lang="javascript" class="min-h-80" />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { MaterialDefinition } from '@/schema/materials'
|
||||
import AsyncLoader from '@/components/AsyncLoader.vue'
|
||||
import { commonEventOptions } from '@/materials/eventOptions'
|
||||
import { barMaterial } from './bar'
|
||||
import { hbarMaterial } from './hbar'
|
||||
import { areaMaterial } from './area'
|
||||
@@ -17,25 +18,38 @@ const ChartMaterial = defineAsyncComponent({
|
||||
delay: 0,
|
||||
})
|
||||
|
||||
const registerChart = (
|
||||
register: (material: MaterialDefinition, component: Component) => void,
|
||||
material: MaterialDefinition,
|
||||
) => {
|
||||
register(
|
||||
{
|
||||
...material,
|
||||
eventOptions: material.eventOptions || commonEventOptions,
|
||||
},
|
||||
ChartMaterial,
|
||||
)
|
||||
}
|
||||
|
||||
export const install = (register: (material: MaterialDefinition, component: Component) => void) => {
|
||||
// 注册柱状图物料
|
||||
register(barMaterial, ChartMaterial)
|
||||
registerChart(register, barMaterial)
|
||||
// 注册横向柱状图物料
|
||||
register(hbarMaterial, ChartMaterial)
|
||||
registerChart(register, hbarMaterial)
|
||||
// 注册面积图物料
|
||||
register(areaMaterial, ChartMaterial)
|
||||
registerChart(register, areaMaterial)
|
||||
// 注册折线图物料
|
||||
register(lineMaterial, ChartMaterial)
|
||||
registerChart(register, lineMaterial)
|
||||
// 注册饼图物料
|
||||
register(pieMaterial, ChartMaterial)
|
||||
registerChart(register, pieMaterial)
|
||||
// 注册环形图物料
|
||||
register(ringMaterial, ChartMaterial)
|
||||
registerChart(register, ringMaterial)
|
||||
// 注册散点图物料
|
||||
register(scatterMaterial, ChartMaterial)
|
||||
registerChart(register, scatterMaterial)
|
||||
// 注册雷达图物料
|
||||
register(radarMaterial, ChartMaterial)
|
||||
registerChart(register, radarMaterial)
|
||||
// 注册漏斗图物料
|
||||
register(funnelMaterial, ChartMaterial)
|
||||
registerChart(register, funnelMaterial)
|
||||
// 注册仪表盘物料
|
||||
register(gaugeMaterial, ChartMaterial)
|
||||
registerChart(register, gaugeMaterial)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { EventOption } from '@/schema/materials'
|
||||
|
||||
export const commonEventOptions: EventOption[] = [
|
||||
{
|
||||
label: '点击事件',
|
||||
value: 'click',
|
||||
},
|
||||
{
|
||||
label: '双击事件',
|
||||
value: 'dblclick',
|
||||
},
|
||||
{
|
||||
label: '鼠标进入事件',
|
||||
value: 'mouseenter',
|
||||
},
|
||||
{
|
||||
label: '鼠标离开事件',
|
||||
value: 'mouseleave',
|
||||
},
|
||||
{
|
||||
label: '鼠标按下事件',
|
||||
value: 'mousedown',
|
||||
},
|
||||
{
|
||||
label: '鼠标抬起事件',
|
||||
value: 'mouseup',
|
||||
},
|
||||
{
|
||||
label: '鼠标移动事件',
|
||||
value: 'mousemove',
|
||||
},
|
||||
{
|
||||
label: '鼠标滚轮事件',
|
||||
value: 'mousewheel',
|
||||
},
|
||||
{
|
||||
label: '生命周期事件',
|
||||
value: 'vnodeMounted',
|
||||
},
|
||||
]
|
||||
+14
-4
@@ -5,11 +5,15 @@ const materials: MaterialDefinition[] = []
|
||||
|
||||
// 已注册的物料列表
|
||||
const componentMap = new Map<string, Component>()
|
||||
// 已注册的物料设置器列表
|
||||
const settersMap = new Map<string, SetterSchema[]>()
|
||||
const materialMap = new Map<string, MaterialDefinition>()
|
||||
// 注册物料
|
||||
export function register(material: MaterialDefinition, component: Component) {
|
||||
materials.push(material)
|
||||
componentMap.set(material.schema.type, component)
|
||||
settersMap.set(material.schema.type, material.setters)
|
||||
materials.push(material) // 将物料添加到物料列表
|
||||
componentMap.set(material.schema.type, component) // 将物料类型与组件映射,通过物料类型获取组件
|
||||
settersMap.set(material.schema.type, material.setters) // 将物料类型与设置器映射,通过物料类型获取设置器
|
||||
materialMap.set(material.schema.type, material) // 将物料类型与物料定义映射,通过物料类型获取物料定义
|
||||
}
|
||||
|
||||
const materialModules = import.meta.glob('./**/index.ts', { eager: true })
|
||||
@@ -50,8 +54,14 @@ export function getMaterialComponent(type: string): Component | undefined {
|
||||
return componentMap.get(type)
|
||||
}
|
||||
|
||||
// 根据物料类型获取对应的设置器
|
||||
export function getMaterialSetters(type: string): SetterSchema[] | undefined {
|
||||
return settersMap.get(type)
|
||||
return materialMap.get(type)?.setters || []
|
||||
}
|
||||
|
||||
// 根据物料类型获取对应的事件选项
|
||||
export const getMaterialEventOptions = (type: string) => {
|
||||
return materialMap.get(type)?.eventOptions || []
|
||||
}
|
||||
|
||||
// 创建一个新的节点对象,生成唯一的 id
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { MaterialDefinition } from '@/schema/materials.ts'
|
||||
import { commonEventOptions } from '@/materials/eventOptions'
|
||||
import TextMaterial from './component.vue'
|
||||
|
||||
const textMaterial: MaterialDefinition = {
|
||||
@@ -22,6 +23,7 @@ const textMaterial: MaterialDefinition = {
|
||||
key: 'style.fontSize',
|
||||
},
|
||||
],
|
||||
eventOptions: commonEventOptions,
|
||||
schema: {
|
||||
type: 'text',
|
||||
name: '文本',
|
||||
@@ -41,8 +43,9 @@ const textMaterial: MaterialDefinition = {
|
||||
events: [
|
||||
{
|
||||
type: 'click',
|
||||
title: '点击事件',
|
||||
name: 'fn',
|
||||
code: `console.log(123123123)`,
|
||||
code: `console.log(123)`,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
+8
-1
@@ -1,4 +1,11 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import AsyncLoader from '@/components/AsyncLoader.vue'
|
||||
|
||||
const ScreenEditor = defineAsyncComponent({
|
||||
loader: () => import('@/editor/index.vue'),
|
||||
loadingComponent: AsyncLoader,
|
||||
delay: 0,
|
||||
})
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
@@ -10,7 +17,7 @@ const router = createRouter({
|
||||
{
|
||||
path: '/editor',
|
||||
name: 'editor',
|
||||
component: () => import('@/editor/index.vue'),
|
||||
component: ScreenEditor,
|
||||
},
|
||||
{
|
||||
path: '/preview',
|
||||
|
||||
+14
-2
@@ -25,6 +25,7 @@ interface RuntimeContext {
|
||||
// 创建运行时上下文
|
||||
export const createRunTimeContext = (page: Ref<PageSchema>) => {
|
||||
let InstanceMap: Record<string, Record<string, any>> = {}
|
||||
const dispatchingEvents = new Set<string>()
|
||||
|
||||
const getNode: RuntimeContext['getNode'] = (id: string) => {
|
||||
return page.value.nodes.find((node) => node.id === id)
|
||||
@@ -79,12 +80,23 @@ export const createRunTimeContext = (page: Ref<PageSchema>) => {
|
||||
|
||||
/** 派发事件到指定节点实例 */
|
||||
const dispatch: RuntimeContext['dispatch'] = (id: string, name: string, payload: any) => {
|
||||
const eventKey = `${id}:${name}`
|
||||
if (dispatchingEvents.has(eventKey)) {
|
||||
console.warn(`Circular event dispatch prevented: ${eventKey}`)
|
||||
return
|
||||
}
|
||||
|
||||
const node = getNode(id)
|
||||
if (node) {
|
||||
const event = node.events?.find((e) => e.name === name)
|
||||
if (event) {
|
||||
// 如果找到事件就执行
|
||||
event.handler?.(payload)
|
||||
dispatchingEvents.add(eventKey)
|
||||
try {
|
||||
// 如果找到事件就执行
|
||||
event.handler?.(payload)
|
||||
} finally {
|
||||
dispatchingEvents.delete(eventKey)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn(`Node with id ${id} not found`)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/** 全局白名单 TODO: 需要完善 */
|
||||
const globalKeys = new Set([
|
||||
'console',
|
||||
'promise',
|
||||
'setTimeout',
|
||||
'setInterval',
|
||||
'clearTimeout',
|
||||
'clearInterval',
|
||||
])
|
||||
|
||||
/**
|
||||
* 在沙盒环境中使用提供的作用域运行给定代码
|
||||
* @param code 要运行的代码
|
||||
* @param scope 代码运行时的作用域对象
|
||||
*/
|
||||
export const runSandBox = (code: string, scope: Record<string, any>) => {
|
||||
const sandbox = new Proxy(scope, {
|
||||
has() {
|
||||
return true
|
||||
},
|
||||
get(target, key) {
|
||||
// 过滤掉 Symbol.unscopables,防止 with 语句报错
|
||||
if (key === Symbol.unscopables) return
|
||||
if (Object.hasOwn(target, key)) {
|
||||
return target[key as string]
|
||||
}
|
||||
if (globalKeys.has(key as string)) {
|
||||
const value = globalThis[key as keyof typeof globalThis]
|
||||
// 这里如果是函数类型,返回一个绑定了 globalThis 的函数,否则直接返回值。用于处理全局函数在沙箱中调用时的上下文问题。
|
||||
return typeof value === 'function' ? value.bind(globalThis) : value
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const fn = new Function(
|
||||
'sandbox',
|
||||
`
|
||||
const asyncFn = async () => { with(sandbox) { ${code} } };
|
||||
asyncFn();
|
||||
`,
|
||||
)
|
||||
|
||||
fn(sandbox)
|
||||
}
|
||||
@@ -34,11 +34,19 @@ export interface SetterSchema {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
// 事件选项
|
||||
export interface EventOption {
|
||||
label: string
|
||||
value: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
// 物料定义结构
|
||||
export interface MaterialDefinition {
|
||||
name: string
|
||||
icon: string
|
||||
group: string
|
||||
setters: SetterSchema[]
|
||||
eventOptions?: EventOption[]
|
||||
schema: Omit<MaterialSchema, 'id'>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user