feat: 接入编辑器和导入导出
This commit is contained in:
@@ -37,3 +37,5 @@ __screenshots__/
|
|||||||
|
|
||||||
# Vite
|
# Vite
|
||||||
*.timestamp-*-*.mjs
|
*.timestamp-*-*.mjs
|
||||||
|
|
||||||
|
AGENTS.md
|
||||||
|
|||||||
@@ -1,48 +1,93 @@
|
|||||||
# my-v0
|
# my-v0
|
||||||
|
|
||||||
This template should help get you started developing with Vue 3 in Vite.
|
Vue 3 + Vite 大屏低代码编辑器前端
|
||||||
|
|
||||||
## Recommended IDE Setup
|
## 开发
|
||||||
|
|
||||||
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
Node `^22.18.0 || >=24.12.0`,包管理器用 pnpm。
|
||||||
|
|
||||||
## Recommended Browser Setup
|
|
||||||
|
|
||||||
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
|
|
||||||
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
|
||||||
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
|
|
||||||
- Firefox:
|
|
||||||
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
|
||||||
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
|
|
||||||
|
|
||||||
## Type Support for `.vue` Imports in TS
|
|
||||||
|
|
||||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
|
||||||
|
|
||||||
## Customize configuration
|
|
||||||
|
|
||||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
|
||||||
|
|
||||||
## Project Setup
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pnpm install
|
pnpm install
|
||||||
```
|
|
||||||
|
|
||||||
### Compile and Hot-Reload for Development
|
|
||||||
|
|
||||||
```sh
|
|
||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Type-Check, Compile and Minify for Production
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pnpm build
|
pnpm build # vue-tsc --build + vite build
|
||||||
|
pnpm type-check
|
||||||
|
pnpm preview
|
||||||
|
pnpm lint # oxlint + eslint(都会 --fix)
|
||||||
|
pnpm format # prettier --write src/
|
||||||
```
|
```
|
||||||
|
|
||||||
### Lint with [ESLint](https://eslint.org/)
|
## 项目目标
|
||||||
|
|
||||||
```sh
|
### 一、编辑器核心架构
|
||||||
pnpm lint
|
|
||||||
```
|
- ~~项目初始化与基础布局~~
|
||||||
|
- ~~工具栏实现~~
|
||||||
|
- ~~物料面板(可扩展架构)~~
|
||||||
|
- ~~图层面板~~
|
||||||
|
- ~~插件化注册机制~~
|
||||||
|
- ~~物料 DSL 设计~~
|
||||||
|
- ~~拖拽渲染流程~~
|
||||||
|
- ~~节点拖动与缩放~~
|
||||||
|
- ~~框选与多选操作~~
|
||||||
|
- ~~画布标尺接入~~
|
||||||
|
- ~~图层排序与画布层级联动~~
|
||||||
|
- ~~页面 DSL 设计与落地~~
|
||||||
|
- ~~属性面板~~
|
||||||
|
- ~~右键菜单~~
|
||||||
|
- ~~组合式函数拆分~~
|
||||||
|
- ~~撤销重做基础能力~~
|
||||||
|
- ~~批处理与历史记录管理~~
|
||||||
|
- ~~ECharts 图表物料接入~~
|
||||||
|
|
||||||
|
### 二、代码与数据能力
|
||||||
|
|
||||||
|
- ~~网页代码编辑器接入~~
|
||||||
|
- ~~页面 Schema 导入导出与实时编辑~~
|
||||||
|
- 静态数据源
|
||||||
|
- API 数据源
|
||||||
|
- 数据源管理界面
|
||||||
|
- API 数据源调试
|
||||||
|
- 加载、刷新与请求复用
|
||||||
|
|
||||||
|
### 三、运行时与预览能力
|
||||||
|
|
||||||
|
- 大屏自适应预览
|
||||||
|
- 页面持久化与独立访问
|
||||||
|
- 运行时上下文
|
||||||
|
|
||||||
|
### 四、事件系统
|
||||||
|
|
||||||
|
- 事件调度机制
|
||||||
|
- 事件配置面板
|
||||||
|
- 跨组件事件联动
|
||||||
|
- 事件函数编辑体验优化
|
||||||
|
- 代码沙箱
|
||||||
|
- 物料可触发事件声明
|
||||||
|
|
||||||
|
### 五、AI Agent 核心能力
|
||||||
|
|
||||||
|
- 大模型接入与调试
|
||||||
|
- 提示词工程
|
||||||
|
- 上下文工程
|
||||||
|
- 结构化输出
|
||||||
|
- 工具调用
|
||||||
|
- Agent Loop
|
||||||
|
- 模型记忆
|
||||||
|
- RAG 与文档处理
|
||||||
|
- Agentic RAG
|
||||||
|
- Skill 体系
|
||||||
|
- MCP
|
||||||
|
- 流程编排(LangGraph)
|
||||||
|
- 状态管理与人工介入
|
||||||
|
- 子图与节点重试
|
||||||
|
- Responses API
|
||||||
|
|
||||||
|
### 六、全栈应用落地
|
||||||
|
|
||||||
|
- 项目整体搭建
|
||||||
|
- 最小会话构建
|
||||||
|
- AI 能力与设计器深度融合
|
||||||
|
- 完整业务场景落地
|
||||||
|
|||||||
Vendored
+3
@@ -11,15 +11,18 @@ export {}
|
|||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||||
|
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||||
ElForm: typeof import('element-plus/es')['ElForm']
|
ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||||
|
MonacoEditor: typeof import('./src/components/MonacoEditor/index.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"@tailwindcss/postcss": "^4.3.3",
|
"@tailwindcss/postcss": "^4.3.3",
|
||||||
"echarts": "^6.1.0",
|
"echarts": "^6.1.0",
|
||||||
"element-plus": "^2.14.4",
|
"element-plus": "^2.14.4",
|
||||||
|
"monaco-editor": "0.55.1",
|
||||||
"pinia": "^4.0.2",
|
"pinia": "^4.0.2",
|
||||||
"postcss": "^8.5.26",
|
"postcss": "^8.5.26",
|
||||||
"tailwindcss": "^4.3.3",
|
"tailwindcss": "^4.3.3",
|
||||||
|
|||||||
Generated
+31
@@ -17,6 +17,9 @@ importers:
|
|||||||
element-plus:
|
element-plus:
|
||||||
specifier: ^2.14.4
|
specifier: ^2.14.4
|
||||||
version: 2.14.4(vue@3.5.41(typescript@6.0.3))
|
version: 2.14.4(vue@3.5.41(typescript@6.0.3))
|
||||||
|
monaco-editor:
|
||||||
|
specifier: 0.55.1
|
||||||
|
version: 0.55.1
|
||||||
pinia:
|
pinia:
|
||||||
specifier: ^4.0.2
|
specifier: ^4.0.2
|
||||||
version: 4.0.3(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3))
|
version: 4.0.3(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3))
|
||||||
@@ -750,6 +753,9 @@ packages:
|
|||||||
'@types/sortablejs@1.15.9':
|
'@types/sortablejs@1.15.9':
|
||||||
resolution: {integrity: sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==}
|
resolution: {integrity: sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==}
|
||||||
|
|
||||||
|
'@types/trusted-types@2.0.7':
|
||||||
|
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||||
|
|
||||||
'@types/web-bluetooth@0.0.21':
|
'@types/web-bluetooth@0.0.21':
|
||||||
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
|
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
|
||||||
|
|
||||||
@@ -1079,6 +1085,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
dompurify@3.2.7:
|
||||||
|
resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==}
|
||||||
|
|
||||||
echarts@6.1.0:
|
echarts@6.1.0:
|
||||||
resolution: {integrity: sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==}
|
resolution: {integrity: sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==}
|
||||||
|
|
||||||
@@ -1545,6 +1554,11 @@ packages:
|
|||||||
magic-string@1.2.0:
|
magic-string@1.2.0:
|
||||||
resolution: {integrity: sha512-ptco+HFxTLgjafSLim2LojBSwfg5feBjd+SqyiwdGkzC38UPdZy3zgrHMI2CoTf5fJL38tbHMYWVzIH8BxGqJw==}
|
resolution: {integrity: sha512-ptco+HFxTLgjafSLim2LojBSwfg5feBjd+SqyiwdGkzC38UPdZy3zgrHMI2CoTf5fJL38tbHMYWVzIH8BxGqJw==}
|
||||||
|
|
||||||
|
marked@14.0.0:
|
||||||
|
resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
memoize-one@6.0.0:
|
memoize-one@6.0.0:
|
||||||
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
|
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
|
||||||
|
|
||||||
@@ -1567,6 +1581,9 @@ packages:
|
|||||||
mlly@1.8.2:
|
mlly@1.8.2:
|
||||||
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
||||||
|
|
||||||
|
monaco-editor@0.55.1:
|
||||||
|
resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==}
|
||||||
|
|
||||||
moveable@0.53.0:
|
moveable@0.53.0:
|
||||||
resolution: {integrity: sha512-71jS9zIoQzMhnNvduhg4tUEdm23+fO/40FN7muVMbZvVwbTku2MIxxLhnU4qFvxI4oVxn75l79SbtgjuA+s7Pw==}
|
resolution: {integrity: sha512-71jS9zIoQzMhnNvduhg4tUEdm23+fO/40FN7muVMbZvVwbTku2MIxxLhnU4qFvxI4oVxn75l79SbtgjuA+s7Pw==}
|
||||||
|
|
||||||
@@ -2658,6 +2675,9 @@ snapshots:
|
|||||||
|
|
||||||
'@types/sortablejs@1.15.9': {}
|
'@types/sortablejs@1.15.9': {}
|
||||||
|
|
||||||
|
'@types/trusted-types@2.0.7':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@types/web-bluetooth@0.0.21': {}
|
'@types/web-bluetooth@0.0.21': {}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)':
|
'@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)':
|
||||||
@@ -3063,6 +3083,10 @@ snapshots:
|
|||||||
|
|
||||||
detect-libc@2.1.2: {}
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
|
dompurify@3.2.7:
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/trusted-types': 2.0.7
|
||||||
|
|
||||||
echarts@6.1.0:
|
echarts@6.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.3.0
|
tslib: 2.3.0
|
||||||
@@ -3470,6 +3494,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
marked@14.0.0: {}
|
||||||
|
|
||||||
memoize-one@6.0.0: {}
|
memoize-one@6.0.0: {}
|
||||||
|
|
||||||
memorystream@0.3.1: {}
|
memorystream@0.3.1: {}
|
||||||
@@ -3492,6 +3518,11 @@ snapshots:
|
|||||||
pkg-types: 1.3.1
|
pkg-types: 1.3.1
|
||||||
ufo: 1.6.4
|
ufo: 1.6.4
|
||||||
|
|
||||||
|
monaco-editor@0.55.1:
|
||||||
|
dependencies:
|
||||||
|
dompurify: 3.2.7
|
||||||
|
marked: 14.0.0
|
||||||
|
|
||||||
moveable@0.53.0:
|
moveable@0.53.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@daybrush/utils': 1.13.0
|
'@daybrush/utils': 1.13.0
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<!-- 代码编辑器 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { editor } from 'monaco-editor'
|
||||||
|
import 'monaco-editor/min/vs/editor/editor.main.css'
|
||||||
|
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
|
||||||
|
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
|
||||||
|
import TsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'MonacoEditor',
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{ lang: string }>()
|
||||||
|
const modelValue = defineModel({ type: String, default: '' })
|
||||||
|
|
||||||
|
// 编辑器 DOM 元素的引用
|
||||||
|
const editorEL = ref()
|
||||||
|
|
||||||
|
// 因为 monaco-editor 运行需要消耗大量的资源,所以需要在webWorker中运行
|
||||||
|
window.MonacoEnvironment = {
|
||||||
|
getWorker(_, label) {
|
||||||
|
if (label === 'json') {
|
||||||
|
return new JsonWorker()
|
||||||
|
}
|
||||||
|
if (label === 'typescript' || label === 'javascript') {
|
||||||
|
return new TsWorker()
|
||||||
|
}
|
||||||
|
return new EditorWorker()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const instance = editor.create(editorEL.value, {
|
||||||
|
value: modelValue.value,
|
||||||
|
theme: 'vs-dark',
|
||||||
|
fontSize: 14,
|
||||||
|
tabSize: 2,
|
||||||
|
language: props.lang || 'json',
|
||||||
|
automaticLayout: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听内容变化
|
||||||
|
instance.onDidChangeModelContent(() => {
|
||||||
|
modelValue.value = instance.getValue()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
instance.dispose()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="editorEL" class="w-full h-full min-h-0 flex-1"></div>
|
||||||
|
</template>
|
||||||
@@ -83,7 +83,7 @@ const onCommand = (command: keyof typeof commendMap) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="canvasRootRef" class="h-full w-full min-h-0 min-w-0 overflow-hidden">
|
<div ref="canvasRootRef" class="relative h-full w-full min-h-0 min-w-0 overflow-hidden isolate">
|
||||||
<SketchRuler
|
<SketchRuler
|
||||||
v-if="viewportSize.width && viewportSize.height"
|
v-if="viewportSize.width && viewportSize.height"
|
||||||
:palette="palette"
|
:palette="palette"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const layerWidth = computed(() => (panelVisible.value.layer ? '260px' : '0'))
|
|||||||
<div class="h-screen select-none">
|
<div class="h-screen select-none">
|
||||||
<header class="flex items-center justify-between h-14 border-b border-white/20 px-5">
|
<header class="flex items-center justify-between h-14 border-b border-white/20 px-5">
|
||||||
<div class="w-75"><ToolbarLeft /></div>
|
<div class="w-75"><ToolbarLeft /></div>
|
||||||
<div class="">2</div>
|
<div class="">工作区</div>
|
||||||
<div class="w-75"><ToolbarRight /></div>
|
<div class="w-75"><ToolbarRight /></div>
|
||||||
</header>
|
</header>
|
||||||
<main class="h-[calc(100%-56px)] flex">
|
<main class="h-[calc(100%-56px)] flex">
|
||||||
|
|||||||
@@ -24,14 +24,14 @@ const nodeList = computed(() => [...nodes.value].reverse())
|
|||||||
<div
|
<div
|
||||||
v-for="node in nodeList"
|
v-for="node in nodeList"
|
||||||
:key="node.id"
|
:key="node.id"
|
||||||
class="flex items-center justify-between p-1 border border-white/20"
|
class="flex items-center justify-between p-1 border border-white/20 text-(--el-text-color-regular) cursor-pointer"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-white/20': selectedNodeIds.includes(node.id),
|
'bg-white/20 text-white!': selectedNodeIds.includes(node.id),
|
||||||
}"
|
}"
|
||||||
@click="editorStore.selectNode(node.id)"
|
@click="editorStore.selectNode(node.id)"
|
||||||
>
|
>
|
||||||
<span>{{ node.name }}</span>
|
<span>{{ node.name }}</span>
|
||||||
<span><Icon /></span>
|
<span><Icon icon="famicons:menu-outline" /></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function onStart(event: DragEvent) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="h-30 p-2.5 flex flex-col border border-white/20"
|
class="h-30 p-2.5 flex flex-col border border-white/20 rounded cursor-pointer text-(--el-text-color-regular) hover:text-white hover:bg-white/10"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart="onStart"
|
@dragstart="onStart"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const currentMaterials = computed(() => {
|
|||||||
<div
|
<div
|
||||||
v-for="value in groups"
|
v-for="value in groups"
|
||||||
:key="value.key"
|
:key="value.key"
|
||||||
class="cursor-pointer flex flex-col items-center justify-center text-sm h-12"
|
class="cursor-pointer flex flex-col items-center justify-center text-sm text-(--el-text-color-regular) h-12"
|
||||||
:class="{ 'bg-white/10 text-white': activeGroup === value.key }"
|
:class="{ 'bg-white/10 text-white': activeGroup === value.key }"
|
||||||
@click="activeGroup = value.key"
|
@click="activeGroup = value.key"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
import { getValue } from '@/utils'
|
import { getValue } from '@/utils'
|
||||||
import { ElInput, ElInputNumber, ElColorPicker, ElCheckbox, ElSelect } from 'element-plus'
|
import { ElInput, ElInputNumber, ElColorPicker, ElCheckbox, ElSelect } from 'element-plus'
|
||||||
import { useUndoRedo } from '@/composables/useUndoRedo'
|
import { useUndoRedo } from '@/composables/useUndoRedo'
|
||||||
|
// 动态 :is 不会走 unplugin 按需样式,需要手动引入
|
||||||
|
import 'element-plus/es/components/select/style/css'
|
||||||
|
import 'element-plus/es/components/input/style/css'
|
||||||
|
import 'element-plus/es/components/checkbox/style/css'
|
||||||
|
|
||||||
defineProps(['setters', 'formData'])
|
defineProps(['setters', 'formData'])
|
||||||
|
|
||||||
@@ -32,5 +36,3 @@ const componentMap: Record<string, any> = {
|
|||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { getMaterialSetters } from '@/materials'
|
|||||||
import { useEditorStore } from '@/stores/editor'
|
import { useEditorStore } from '@/stores/editor'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import FormCreate from './FormCreate.vue'
|
import FormCreate from './FormCreate.vue'
|
||||||
|
import MonacoEditor from '@/components/MonacoEditor/index.vue'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'NodeProperty',
|
name: 'NodeProperty',
|
||||||
@@ -41,10 +42,36 @@ const layoutSetters = [
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const visible = 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认编辑 JSON
|
||||||
|
const onConfirm = () => {
|
||||||
|
// 拿到新的 JSON 内容,更新到节点中
|
||||||
|
const newNode = JSON.parse(jsonContent.value)
|
||||||
|
editorStore.updateNode(selectedNode.value?.id || '', {
|
||||||
|
...newNode,
|
||||||
|
// 这两个属性不允许被修改
|
||||||
|
id: selectedNode.value?.id || '',
|
||||||
|
type: selectedNode.value?.type || '',
|
||||||
|
})
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="px-2">
|
<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>
|
||||||
<el-collapse v-model="activeNames">
|
<el-collapse v-model="activeNames">
|
||||||
<el-collapse-item title="布局属性" name="layout">
|
<el-collapse-item title="布局属性" name="layout">
|
||||||
<FormCreate :setters="layoutSetters" :form-data="selectedNode" />
|
<FormCreate :setters="layoutSetters" :form-data="selectedNode" />
|
||||||
@@ -53,5 +80,22 @@ const layoutSetters = [
|
|||||||
<FormCreate :setters="setters" :form-data="selectedNode" />
|
<FormCreate :setters="setters" :form-data="selectedNode" />
|
||||||
</el-collapse-item>
|
</el-collapse-item>
|
||||||
</el-collapse>
|
</el-collapse>
|
||||||
|
|
||||||
|
<el-drawer v-model="visible" 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 type="primary" @click="onConfirm">确认</el-button>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.json-drawer .el-drawer__body) {
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,16 +1,99 @@
|
|||||||
<!-- 右侧工具栏 -->
|
<!-- 右侧工具栏 -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
|
import { useEditorStore } from '@/stores/editor'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const editorStore = useEditorStore()
|
||||||
|
const { page } = storeToRefs(editorStore)
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const jsonContent = ref('')
|
||||||
|
|
||||||
|
const inputRef = useTemplateRef<HTMLInputElement>('inputRef')
|
||||||
|
|
||||||
|
// 显示 JSON 编辑器
|
||||||
|
const showJson = () => {
|
||||||
|
visible.value = true
|
||||||
|
jsonContent.value = JSON.stringify(page.value, null, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认编辑 JSON
|
||||||
|
const onConfirm = () => {
|
||||||
|
const newPage = JSON.parse(jsonContent.value)
|
||||||
|
editorStore.setPage(newPage)
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出文件
|
||||||
|
const onExport = () => {
|
||||||
|
const json = JSON.stringify(page.value, null, 2)
|
||||||
|
const blob = new Blob([json], { type: 'application/json;charset=utf-8' })
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = 'design.json'
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入文件
|
||||||
|
const onImport = () => {
|
||||||
|
inputRef.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件选择变化处理
|
||||||
|
const onFileChange = (e: Event) => {
|
||||||
|
const target = e.target as HTMLInputElement
|
||||||
|
const file = target.files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
const reader = new FileReader()
|
||||||
|
// 读取文件完成后的回调
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const content = e.target?.result as string
|
||||||
|
try {
|
||||||
|
const newPage = JSON.parse(content)
|
||||||
|
editorStore.setPage(newPage)
|
||||||
|
ElMessage.success('导入成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导入的文件不是有效的 JSON 文件', error)
|
||||||
|
ElMessage.error('导入的文件不是有效的 JSON 文件')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 读取文件内容
|
||||||
|
reader.readAsText(file)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-end gap-5">
|
<div class="flex items-center justify-end gap-5">
|
||||||
<span class="border p-1 rounded-lg"><Icon icon="icon-park-outline:preview-open"></Icon></span>
|
<span class="border p-1 rounded-lg"><Icon icon="icon-park-outline:preview-open"></Icon></span>
|
||||||
<span class="border p-1 rounded-lg"><Icon icon="si:json-line"></Icon></span>
|
<span class="border p-1 rounded-lg" @click="showJson"><Icon icon="si:json-line"></Icon></span>
|
||||||
<span class="border p-1 rounded-lg"><Icon icon="hugeicons:internet"></Icon></span>
|
<span class="border p-1 rounded-lg"><Icon icon="hugeicons:internet"></Icon></span>
|
||||||
<span class="border p-1 rounded-lg"><Icon icon="lets-icons:import"></Icon></span>
|
<span class="border p-1 rounded-lg" @click="onImport" title="导入">
|
||||||
<span class="border p-1 rounded-lg"><Icon icon="lets-icons:export"></Icon></span>
|
<Icon icon="lets-icons:import"> </Icon>
|
||||||
|
</span>
|
||||||
|
<span class="border p-1 rounded-lg" @click="onExport" title="导出">
|
||||||
|
<Icon icon="lets-icons:export"> </Icon>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<input ref="inputRef" type="file" class="hidden" @change="onFileChange" />
|
||||||
|
|
||||||
|
<el-drawer v-model="visible" title="编辑 JSON" class="json-drawer" destroy-on-close>
|
||||||
|
<MonacoEditor v-model="jsonContent" lang="json" />
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="onConfirm">确认</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|||||||
@@ -163,10 +163,10 @@ export const areaMaterial: MaterialDefinition = {
|
|||||||
},
|
},
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 3,
|
width: 3,
|
||||||
color: '#22d3ee',
|
color: '#ffffff',
|
||||||
},
|
},
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: '#22d3ee',
|
color: '#ffffff',
|
||||||
},
|
},
|
||||||
areaStyle: {
|
areaStyle: {
|
||||||
color: 'rgba(34, 211, 238, 0.25)',
|
color: 'rgba(34, 211, 238, 0.25)',
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ export const barMaterial: MaterialDefinition = {
|
|||||||
y: 'sales',
|
y: 'sales',
|
||||||
},
|
},
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: '#22d3ee',
|
color: '#ffffff',
|
||||||
borderRadius: [4, 4, 0, 0],
|
borderRadius: [4, 4, 0, 0],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export const pieMaterial: MaterialDefinition = {
|
|||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
option: {
|
option: {
|
||||||
color: ['#22d3ee', '#a78bfa', '#f59e0b', '#34d399', '#fb7185'],
|
color: ['#ffffff', '#a78bfa', '#f59e0b', '#34d399', '#fb7185'],
|
||||||
legend: {
|
legend: {
|
||||||
top: 38,
|
top: 38,
|
||||||
left: 'center',
|
left: 'center',
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ const textMaterial: MaterialDefinition = {
|
|||||||
height: 50,
|
height: 50,
|
||||||
},
|
},
|
||||||
style: {
|
style: {
|
||||||
color: 'red',
|
color: '#fff',
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
content: '文本内容123123',
|
content: '文本内容',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
nodes: [],
|
nodes: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 设置页面数据
|
||||||
|
const setPage = (newPage: PageSchema) => {
|
||||||
|
Object.assign(page.value, newPage)
|
||||||
|
}
|
||||||
|
|
||||||
const canvas = toRef(page.value, 'canvas')
|
const canvas = toRef(page.value, 'canvas')
|
||||||
|
|
||||||
// 画布上的节点数据
|
// 画布上的节点数据
|
||||||
@@ -108,6 +113,13 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
applyChange(node, 'locked', !node.locked)
|
applyChange(node, 'locked', !node.locked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
function updateNode(nodeId: string, newNode: MaterialSchema) {
|
||||||
|
// 返回新的节点数组,替换掉原来的节点数组
|
||||||
|
const newNodes = nodes.value.map((node) => (node.id === nodeId ? newNode : node))
|
||||||
|
setNodes(newNodes)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
panelVisible,
|
panelVisible,
|
||||||
page,
|
page,
|
||||||
@@ -126,5 +138,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
moveTop,
|
moveTop,
|
||||||
moveBottom,
|
moveBottom,
|
||||||
toggleLock,
|
toggleLock,
|
||||||
|
updateNode,
|
||||||
|
setPage,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user