feat: 完成前端部分基本功能开发
my-v0 / deploy (push) Successful in 28s

This commit is contained in:
2026-08-18 12:23:53 +08:00
parent 2f10e8da23
commit 94a8d9454b
14 changed files with 235 additions and 32 deletions
+5 -5
View File
@@ -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
### 六、全栈应用落地
### 六、全栈
- 项目整体搭建
- 最小会话构建
+1
View File
@@ -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
View File
@@ -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
+4 -4
View File
@@ -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 })
}
})
-2
View File
@@ -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" />
+24 -10
View File
@@ -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)
}
+40
View File
@@ -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
View File
@@ -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
+4 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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`)
+44
View File
@@ -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)
}
+8
View File
@@ -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'>
}