@andrewyang/ai-workflow-editor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nuxt/app.config.mjs +18 -0
- package/.nuxt/components.d.ts +910 -0
- package/.nuxt/dev/index.mjs +4103 -0
- package/.nuxt/dev/index.mjs.map +1 -0
- package/.nuxt/dist/server/client.manifest.mjs +4 -0
- package/.nuxt/dist/server/client.precomputed.mjs +1 -0
- package/.nuxt/dist/server/server.mjs +1 -0
- package/.nuxt/i18n-route-resources.mjs +3 -0
- package/.nuxt/imports.d.ts +43 -0
- package/.nuxt/manifest/latest.json +1 -0
- package/.nuxt/manifest/meta/dev.json +1 -0
- package/.nuxt/nitro.json +17 -0
- package/.nuxt/nuxt.d.ts +24 -0
- package/.nuxt/nuxt.json +9 -0
- package/.nuxt/schema/nuxt.schema.d.ts +17 -0
- package/.nuxt/schema/nuxt.schema.json +3 -0
- package/.nuxt/tsconfig.json +234 -0
- package/.nuxt/tsconfig.server.json +185 -0
- package/.nuxt/types/app-defaults.d.ts +7 -0
- package/.nuxt/types/app.config.d.ts +31 -0
- package/.nuxt/types/build.d.ts +29 -0
- package/.nuxt/types/builder-env.d.ts +1 -0
- package/.nuxt/types/components.d.ts +915 -0
- package/.nuxt/types/i18n-plugin.d.ts +123 -0
- package/.nuxt/types/imports.d.ts +993 -0
- package/.nuxt/types/middleware.d.ts +17 -0
- package/.nuxt/types/nitro-config.d.ts +14 -0
- package/.nuxt/types/nitro-imports.d.ts +170 -0
- package/.nuxt/types/nitro-layouts.d.ts +17 -0
- package/.nuxt/types/nitro-nuxt.d.ts +39 -0
- package/.nuxt/types/nitro-routes.d.ts +17 -0
- package/.nuxt/types/nitro.d.ts +3 -0
- package/.nuxt/types/plugins.d.ts +43 -0
- package/.nuxt/types/schema.d.ts +213 -0
- package/.nuxt/types/vue-shim.d.ts +0 -0
- package/.trae/rules/rule.md +38 -0
- package/.vscode/settings.json +5 -0
- package/nuxt.config.ts +38 -0
- package/package.json +42 -0
- package/pnpm-lock.yaml +24 -0
- package/src/app.vue +46 -0
- package/src/components/AiWorkflowEditor.vue +142 -0
- package/src/components/ai/AiChatPanel.vue +135 -0
- package/src/components/ai/AiGenerator.vue +62 -0
- package/src/components/editor/Canvas.vue +175 -0
- package/src/components/editor/FormatPanel.vue +327 -0
- package/src/components/editor/NodePanel.vue +240 -0
- package/src/components/editor/PropertyPanel.vue +348 -0
- package/src/components/editor/Toolbar.vue +49 -0
- package/src/components/editor/nodes/AiNode.vue +77 -0
- package/src/components/editor/nodes/NormalNode.vue +75 -0
- package/src/components/editor/nodes/SysmlBlockNode.vue +72 -0
- package/src/components/editor/nodes/SysmlRequirementNode.vue +72 -0
- package/src/components/editor/nodes/SysmlUseCaseNode.vue +62 -0
- package/src/composables/useAi.ts +82 -0
- package/src/i18n.config.ts +5 -0
- package/src/locales/en.json +106 -0
- package/src/locales/zh.json +106 -0
- package/src/plugins/aiWorkflowEditor.ts +6 -0
- package/src/types/ai.ts +7 -0
- package/src/types/workflow.ts +25 -0
- package/src/utils/llmAdapter.ts +46 -0
- package/tsconfig.json +3 -0
- package/uno.config.ts +15 -0
- package//345/211/215/347/253/257/345/256/232/345/210/266/345/274/200/345/217/221/357/274/232ai-workflow-editor/345/274/200/345/217/221/350/256/241/345/210/222.md +655 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex h-full flex-col bg-white border-l border-gray-200 w-80 shadow-[-4px_0_24px_rgba(0,0,0,0.08)] z-20 relative">
|
|
3
|
+
<!-- Header -->
|
|
4
|
+
<div class="flex items-center justify-between border-b border-gray-100 p-3 bg-gray-50/50">
|
|
5
|
+
<div class="font-bold text-gray-800">{{ $t('propertyPanel.title') }}</div>
|
|
6
|
+
<el-button link class="!text-gray-400 hover:!text-gray-600 !p-1" @click="$emit('close')">
|
|
7
|
+
<div class="i-lucide-x w-4 h-4" />
|
|
8
|
+
</el-button>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<!-- Tabs -->
|
|
12
|
+
<div class="flex border-b border-gray-200 bg-gray-50">
|
|
13
|
+
<button
|
|
14
|
+
v-for="tab in ['style', 'text', 'arrange']"
|
|
15
|
+
:key="tab"
|
|
16
|
+
@click="currentTab = tab"
|
|
17
|
+
:class="[
|
|
18
|
+
'flex-1 py-2 text-xs font-medium transition-colors relative',
|
|
19
|
+
currentTab === tab ? 'text-gray-900 bg-white' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
|
|
20
|
+
]"
|
|
21
|
+
>
|
|
22
|
+
{{ $t(`propertyPanel.tabs.${tab}`) }}
|
|
23
|
+
<div v-if="currentTab === tab" class="absolute bottom-0 left-0 right-0 h-0.5 bg-purple-600" />
|
|
24
|
+
</button>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<!-- Content -->
|
|
28
|
+
<div class="flex-1 overflow-y-auto p-4 custom-scrollbar">
|
|
29
|
+
<template v-if="selectedNode">
|
|
30
|
+
|
|
31
|
+
<!-- Style Tab -->
|
|
32
|
+
<div v-show="currentTab === 'style'" class="space-y-4">
|
|
33
|
+
<!-- Fill -->
|
|
34
|
+
<div>
|
|
35
|
+
<div class="flex items-center justify-between mb-2">
|
|
36
|
+
<el-checkbox v-model="hasFill" :label="$t('propertyPanel.style.fill')" />
|
|
37
|
+
<div class="flex items-center gap-2">
|
|
38
|
+
<el-color-picker v-model="nodeStyle.backgroundColor" show-alpha size="small" />
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<!-- Presets -->
|
|
42
|
+
<div class="flex gap-1 flex-wrap mb-2">
|
|
43
|
+
<button
|
|
44
|
+
v-for="color in presetColors"
|
|
45
|
+
:key="color"
|
|
46
|
+
class="w-6 h-6 rounded border border-gray-200 cursor-pointer hover:scale-110 transition-transform"
|
|
47
|
+
:style="{ backgroundColor: color }"
|
|
48
|
+
@click="nodeStyle.backgroundColor = color"
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="h-px bg-gray-100" />
|
|
54
|
+
|
|
55
|
+
<!-- Stroke -->
|
|
56
|
+
<div>
|
|
57
|
+
<div class="flex items-center justify-between mb-2">
|
|
58
|
+
<el-checkbox v-model="hasStroke" :label="$t('propertyPanel.style.stroke')" />
|
|
59
|
+
<el-color-picker v-model="nodeStyle.borderColor" show-alpha size="small" />
|
|
60
|
+
</div>
|
|
61
|
+
<div class="grid grid-cols-2 gap-2">
|
|
62
|
+
<el-select v-model="nodeStyle.borderStyle" size="small">
|
|
63
|
+
<el-option value="solid" label="Solid" />
|
|
64
|
+
<el-option value="dashed" label="Dashed" />
|
|
65
|
+
<el-option value="dotted" label="Dotted" />
|
|
66
|
+
</el-select>
|
|
67
|
+
<el-input-number v-model="borderWidth" size="small" :min="0" :max="20" controls-position="right">
|
|
68
|
+
<template #suffix>pt</template>
|
|
69
|
+
</el-input-number>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div class="h-px bg-gray-100" />
|
|
74
|
+
|
|
75
|
+
<!-- Opacity -->
|
|
76
|
+
<div class="flex items-center justify-between">
|
|
77
|
+
<span class="text-xs text-gray-500">{{ $t('propertyPanel.style.opacity') }}</span>
|
|
78
|
+
<el-input-number v-model="opacity" size="small" :min="0" :max="100" controls-position="right">
|
|
79
|
+
<template #suffix>%</template>
|
|
80
|
+
</el-input-number>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<!-- Radius & Shadow -->
|
|
84
|
+
<div class="grid grid-cols-2 gap-2">
|
|
85
|
+
<el-checkbox v-model="hasShadow" :label="$t('propertyPanel.style.shadow')" />
|
|
86
|
+
<el-checkbox v-model="isRounded" :label="$t('propertyPanel.style.rounded')" />
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<!-- Text Tab -->
|
|
91
|
+
<div v-show="currentTab === 'text'" class="space-y-4">
|
|
92
|
+
<!-- Content -->
|
|
93
|
+
<div>
|
|
94
|
+
<label class="text-xs text-gray-500 mb-1 block">{{ $t('propertyPanel.text.content') }}</label>
|
|
95
|
+
<el-input v-model="nodeData.label" type="textarea" :rows="2" resize="none" />
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<!-- Font -->
|
|
99
|
+
<div class="grid grid-cols-2 gap-2">
|
|
100
|
+
<el-select v-model="textStyle.fontFamily" size="small">
|
|
101
|
+
<el-option value="sans-serif" label="Sans Serif" />
|
|
102
|
+
<el-option value="serif" label="Serif" />
|
|
103
|
+
<el-option value="monospace" label="Monospace" />
|
|
104
|
+
</el-select>
|
|
105
|
+
<el-input-number v-model="fontSize" size="small" :min="8" :max="72" controls-position="right">
|
|
106
|
+
<template #suffix>px</template>
|
|
107
|
+
</el-input-number>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<!-- Style Buttons -->
|
|
111
|
+
<div class="flex bg-gray-100 rounded p-1 gap-1 justify-between">
|
|
112
|
+
<button
|
|
113
|
+
:class="['p-1 rounded hover:bg-white transition-colors', textStyle.fontWeight === 'bold' ? 'bg-white text-purple-600 shadow-sm' : 'text-gray-500']"
|
|
114
|
+
@click="toggleFontWeight"
|
|
115
|
+
>
|
|
116
|
+
<div class="i-lucide-bold w-4 h-4" />
|
|
117
|
+
</button>
|
|
118
|
+
<button
|
|
119
|
+
:class="['p-1 rounded hover:bg-white transition-colors', textStyle.fontStyle === 'italic' ? 'bg-white text-purple-600 shadow-sm' : 'text-gray-500']"
|
|
120
|
+
@click="toggleFontStyle"
|
|
121
|
+
>
|
|
122
|
+
<div class="i-lucide-italic w-4 h-4" />
|
|
123
|
+
</button>
|
|
124
|
+
<button
|
|
125
|
+
:class="['p-1 rounded hover:bg-white transition-colors', textStyle.textDecoration === 'underline' ? 'bg-white text-purple-600 shadow-sm' : 'text-gray-500']"
|
|
126
|
+
@click="toggleTextDecoration"
|
|
127
|
+
>
|
|
128
|
+
<div class="i-lucide-underline w-4 h-4" />
|
|
129
|
+
</button>
|
|
130
|
+
<div class="w-px bg-gray-300 my-0.5" />
|
|
131
|
+
<button
|
|
132
|
+
:class="['p-1 rounded hover:bg-white transition-colors', textStyle.textAlign === 'left' ? 'bg-white text-purple-600 shadow-sm' : 'text-gray-500']"
|
|
133
|
+
@click="textStyle.textAlign = 'left'"
|
|
134
|
+
>
|
|
135
|
+
<div class="i-lucide-align-left w-4 h-4" />
|
|
136
|
+
</button>
|
|
137
|
+
<button
|
|
138
|
+
:class="['p-1 rounded hover:bg-white transition-colors', textStyle.textAlign === 'center' ? 'bg-white text-purple-600 shadow-sm' : 'text-gray-500']"
|
|
139
|
+
@click="textStyle.textAlign = 'center'"
|
|
140
|
+
>
|
|
141
|
+
<div class="i-lucide-align-center w-4 h-4" />
|
|
142
|
+
</button>
|
|
143
|
+
<button
|
|
144
|
+
:class="['p-1 rounded hover:bg-white transition-colors', textStyle.textAlign === 'right' ? 'bg-white text-purple-600 shadow-sm' : 'text-gray-500']"
|
|
145
|
+
@click="textStyle.textAlign = 'right'"
|
|
146
|
+
>
|
|
147
|
+
<div class="i-lucide-align-right w-4 h-4" />
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<!-- Color -->
|
|
152
|
+
<div class="flex items-center justify-between">
|
|
153
|
+
<el-checkbox v-model="hasTextColor" :label="$t('propertyPanel.text.color')" />
|
|
154
|
+
<el-color-picker v-model="textStyle.color" size="small" />
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<!-- Arrange Tab -->
|
|
159
|
+
<div v-show="currentTab === 'arrange'" class="space-y-4">
|
|
160
|
+
<!-- Z-Order -->
|
|
161
|
+
<div class="grid grid-cols-2 gap-2">
|
|
162
|
+
<el-button size="small" @click="bringToFront">{{ $t('propertyPanel.arrange.front') }}</el-button>
|
|
163
|
+
<el-button size="small" @click="sendToBack">{{ $t('propertyPanel.arrange.back') }}</el-button>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div class="h-px bg-gray-100" />
|
|
167
|
+
|
|
168
|
+
<!-- Size -->
|
|
169
|
+
<div>
|
|
170
|
+
<div class="text-xs font-bold text-gray-800 mb-2">{{ $t('propertyPanel.arrange.size') }}</div>
|
|
171
|
+
<div class="grid grid-cols-2 gap-2">
|
|
172
|
+
<el-input-number v-model="nodeWidth" size="small" controls-position="right">
|
|
173
|
+
<template #prefix>W</template>
|
|
174
|
+
</el-input-number>
|
|
175
|
+
<el-input-number v-model="nodeHeight" size="small" controls-position="right">
|
|
176
|
+
<template #prefix>H</template>
|
|
177
|
+
</el-input-number>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<!-- Position -->
|
|
182
|
+
<div>
|
|
183
|
+
<div class="text-xs font-bold text-gray-800 mb-2">{{ $t('propertyPanel.arrange.position') }}</div>
|
|
184
|
+
<div class="grid grid-cols-2 gap-2">
|
|
185
|
+
<el-input-number v-model="nodeX" size="small" controls-position="right">
|
|
186
|
+
<template #prefix>X</template>
|
|
187
|
+
</el-input-number>
|
|
188
|
+
<el-input-number v-model="nodeY" size="small" controls-position="right">
|
|
189
|
+
<template #prefix>Y</template>
|
|
190
|
+
</el-input-number>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
</template>
|
|
196
|
+
<div v-else class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
197
|
+
<div class="i-lucide-mouse-pointer-2 text-3xl mb-2" />
|
|
198
|
+
<span class="text-sm">{{ $t('propertyPanel.noSelection') }}</span>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</template>
|
|
203
|
+
|
|
204
|
+
<script setup lang="ts">
|
|
205
|
+
import { ref, computed, watch } from 'vue';
|
|
206
|
+
import { useVueFlow } from '@vue-flow/core';
|
|
207
|
+
|
|
208
|
+
const props = defineProps<{
|
|
209
|
+
selectedNode: any;
|
|
210
|
+
}>();
|
|
211
|
+
|
|
212
|
+
const emit = defineEmits(['close']);
|
|
213
|
+
const { applyNodeChanges } = useVueFlow();
|
|
214
|
+
|
|
215
|
+
const currentTab = ref('style');
|
|
216
|
+
|
|
217
|
+
// Presets
|
|
218
|
+
const presetColors = ['#ffffff', '#f8fafc', '#dbeafe', '#dcfce7', '#fef9c3', '#fae8ff', '#ffe4e6'];
|
|
219
|
+
|
|
220
|
+
// Computed proxies for Node Data
|
|
221
|
+
const nodeData = computed(() => props.selectedNode?.data || {});
|
|
222
|
+
const nodeStyle = computed(() => props.selectedNode?.data?.style || {});
|
|
223
|
+
const textStyle = computed(() => props.selectedNode?.data?.textStyle || {});
|
|
224
|
+
const position = computed(() => props.selectedNode?.position || { x: 0, y: 0 });
|
|
225
|
+
const dimensions = computed(() => props.selectedNode?.dimensions || { width: 0, height: 0 });
|
|
226
|
+
|
|
227
|
+
// Helper to init styles if missing
|
|
228
|
+
watch(() => props.selectedNode, (node) => {
|
|
229
|
+
if (node) {
|
|
230
|
+
if (!node.data.style) node.data.style = {};
|
|
231
|
+
if (!node.data.textStyle) node.data.textStyle = {
|
|
232
|
+
fontSize: '12px',
|
|
233
|
+
fontFamily: 'sans-serif',
|
|
234
|
+
color: '#374151',
|
|
235
|
+
textAlign: 'center'
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}, { immediate: true });
|
|
239
|
+
|
|
240
|
+
// --- Style Getters/Setters ---
|
|
241
|
+
const hasFill = computed({
|
|
242
|
+
get: () => !!nodeStyle.value.backgroundColor && nodeStyle.value.backgroundColor !== 'transparent',
|
|
243
|
+
set: (val) => {
|
|
244
|
+
if (!val) nodeStyle.value.backgroundColor = 'transparent';
|
|
245
|
+
else nodeStyle.value.backgroundColor = '#ffffff';
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const hasStroke = computed({
|
|
250
|
+
get: () => nodeStyle.value.borderWidth !== '0px',
|
|
251
|
+
set: (val) => {
|
|
252
|
+
if (!val) nodeStyle.value.borderWidth = '0px';
|
|
253
|
+
else nodeStyle.value.borderWidth = '1px';
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const borderWidth = computed({
|
|
258
|
+
get: () => parseInt(nodeStyle.value.borderWidth || '0'),
|
|
259
|
+
set: (val) => nodeStyle.value.borderWidth = `${val}px`
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const opacity = computed({
|
|
263
|
+
get: () => (nodeStyle.value.opacity ?? 1) * 100,
|
|
264
|
+
set: (val) => nodeStyle.value.opacity = val / 100
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const hasShadow = computed({
|
|
268
|
+
get: () => !!nodeStyle.value.boxShadow,
|
|
269
|
+
set: (val) => {
|
|
270
|
+
if (val) nodeStyle.value.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1)';
|
|
271
|
+
else nodeStyle.value.boxShadow = 'none';
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const isRounded = computed({
|
|
276
|
+
get: () => nodeStyle.value.borderRadius !== '0px',
|
|
277
|
+
set: (val) => {
|
|
278
|
+
if (val) nodeStyle.value.borderRadius = '0.5rem';
|
|
279
|
+
else nodeStyle.value.borderRadius = '0px';
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// --- Text Getters/Setters ---
|
|
284
|
+
const fontSize = computed({
|
|
285
|
+
get: () => parseInt(textStyle.value.fontSize || '12'),
|
|
286
|
+
set: (val) => textStyle.value.fontSize = `${val}px`
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const hasTextColor = computed({
|
|
290
|
+
get: () => !!textStyle.value.color,
|
|
291
|
+
set: (val) => {
|
|
292
|
+
if (!val) textStyle.value.color = undefined;
|
|
293
|
+
else textStyle.value.color = '#374151';
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const toggleFontWeight = () => {
|
|
298
|
+
textStyle.value.fontWeight = textStyle.value.fontWeight === 'bold' ? 'normal' : 'bold';
|
|
299
|
+
};
|
|
300
|
+
const toggleFontStyle = () => {
|
|
301
|
+
textStyle.value.fontStyle = textStyle.value.fontStyle === 'italic' ? 'normal' : 'italic';
|
|
302
|
+
};
|
|
303
|
+
const toggleTextDecoration = () => {
|
|
304
|
+
textStyle.value.textDecoration = textStyle.value.textDecoration === 'underline' ? 'none' : 'underline';
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// --- Arrange Getters/Setters ---
|
|
308
|
+
const nodeX = computed({
|
|
309
|
+
get: () => Math.round(position.value.x),
|
|
310
|
+
set: (val) => props.selectedNode.position.x = val
|
|
311
|
+
});
|
|
312
|
+
const nodeY = computed({
|
|
313
|
+
get: () => Math.round(position.value.y),
|
|
314
|
+
set: (val) => props.selectedNode.position.y = val
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Resizing is tricky because Vue Flow nodes usually auto-size or use style width/height
|
|
318
|
+
// We will update the style width/height if set
|
|
319
|
+
const nodeWidth = computed({
|
|
320
|
+
get: () => parseInt(nodeStyle.value.width || dimensions.value.width || '160'),
|
|
321
|
+
set: (val) => nodeStyle.value.width = `${val}px`
|
|
322
|
+
});
|
|
323
|
+
const nodeHeight = computed({
|
|
324
|
+
get: () => parseInt(nodeStyle.value.height || dimensions.value.height || '40'),
|
|
325
|
+
set: (val) => nodeStyle.value.height = `${val}px`
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const bringToFront = () => {
|
|
329
|
+
props.selectedNode.zIndex = (props.selectedNode.zIndex || 0) + 1;
|
|
330
|
+
};
|
|
331
|
+
const sendToBack = () => {
|
|
332
|
+
props.selectedNode.zIndex = (props.selectedNode.zIndex || 0) - 1;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
</script>
|
|
336
|
+
|
|
337
|
+
<style scoped>
|
|
338
|
+
.custom-scrollbar::-webkit-scrollbar {
|
|
339
|
+
width: 4px;
|
|
340
|
+
}
|
|
341
|
+
.custom-scrollbar::-webkit-scrollbar-track {
|
|
342
|
+
background: transparent;
|
|
343
|
+
}
|
|
344
|
+
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
345
|
+
background: #e5e7eb;
|
|
346
|
+
border-radius: 4px;
|
|
347
|
+
}
|
|
348
|
+
</style>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="h-12 border-b border-gray-200 bg-white/80 backdrop-blur-xl flex items-center justify-between px-4 shadow-sm z-30 relative">
|
|
3
|
+
<!-- Left: Actions -->
|
|
4
|
+
<div class="flex items-center gap-1">
|
|
5
|
+
<el-button text @click="console.log('Draw')">{{ $t('toolbar.draw') }}</el-button>
|
|
6
|
+
<el-button text @click="console.log('Shape')">{{ $t('toolbar.shape') }}</el-button>
|
|
7
|
+
<el-button text @click="console.log('Style')">{{ $t('toolbar.style') }}</el-button>
|
|
8
|
+
<el-button text @click="console.log('Insert')">{{ $t('toolbar.insert') }}</el-button>
|
|
9
|
+
<div class="w-px h-4 bg-gray-200 mx-2" />
|
|
10
|
+
<el-button text @click="$emit('save')" class="!p-1.5" :title="$t('toolbar.save')">
|
|
11
|
+
<div class="i-lucide-save w-4 h-4" />
|
|
12
|
+
</el-button>
|
|
13
|
+
<el-button text @click="$emit('import')" class="!p-1.5" :title="$t('toolbar.import')">
|
|
14
|
+
<div class="i-lucide-upload w-4 h-4" />
|
|
15
|
+
</el-button>
|
|
16
|
+
<div class="w-px h-4 bg-gray-200 mx-2" />
|
|
17
|
+
<el-button text class="!p-1.5" :title="$t('toolbar.undo')">
|
|
18
|
+
<div class="i-lucide-undo-2 w-4 h-4" />
|
|
19
|
+
</el-button>
|
|
20
|
+
<el-button text class="!p-1.5" :title="$t('toolbar.redo')">
|
|
21
|
+
<div class="i-lucide-redo-2 w-4 h-4" />
|
|
22
|
+
</el-button>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<!-- Center: Title/Status (Optional) -->
|
|
26
|
+
<div class="absolute left-1/2 -translate-x-1/2 text-sm font-medium text-gray-400 select-none pointer-events-none">
|
|
27
|
+
{{ $t('toolbar.untitled') }}
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<!-- Right: Zoom/View -->
|
|
31
|
+
<div class="flex items-center gap-1 z-30">
|
|
32
|
+
<el-button text @click="$emit('zoom-out')" class="!p-1.5" :title="$t('toolbar.zoomOut')">
|
|
33
|
+
<div class="i-lucide-minus w-4 h-4" />
|
|
34
|
+
</el-button>
|
|
35
|
+
<span class="text-xs text-gray-500 w-8 text-center select-none">100%</span>
|
|
36
|
+
<el-button text @click="$emit('zoom-in')" class="!p-1.5" :title="$t('toolbar.zoomIn')">
|
|
37
|
+
<div class="i-lucide-plus w-4 h-4" />
|
|
38
|
+
</el-button>
|
|
39
|
+
<div class="w-px h-4 bg-gray-200 mx-2" />
|
|
40
|
+
<el-button text @click="$emit('fit-view')" class="!p-1.5" :title="$t('toolbar.fitView')">
|
|
41
|
+
<div class="i-lucide-maximize w-4 h-4" />
|
|
42
|
+
</el-button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<script setup lang="ts">
|
|
48
|
+
defineEmits(['save', 'import', 'zoom-in', 'zoom-out', 'fit-view']);
|
|
49
|
+
</script>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="ai-node group relative min-w-[160px] h-full transition-all"
|
|
4
|
+
:style="containerStyle"
|
|
5
|
+
>
|
|
6
|
+
<NodeResizer :is-visible="selected" :min-width="100" :min-height="30" />
|
|
7
|
+
<Handle type="target" position="top" class="!bg-purple-500 !w-2.5 !h-2.5 !border-2 !border-white" />
|
|
8
|
+
|
|
9
|
+
<div class="flex items-center gap-3 h-full">
|
|
10
|
+
<div class="flex h-8 w-8 items-center justify-center rounded-md bg-purple-50 text-purple-600 group-hover:bg-purple-100 transition-colors">
|
|
11
|
+
<div class="i-lucide-brain-circuit text-lg" />
|
|
12
|
+
</div>
|
|
13
|
+
<div class="flex flex-col flex-1 min-w-0">
|
|
14
|
+
<span class="text-[10px] font-bold text-purple-600 uppercase tracking-wide leading-none mb-0.5">AI Node</span>
|
|
15
|
+
<textarea
|
|
16
|
+
v-if="data.isEditing"
|
|
17
|
+
v-model="data.label"
|
|
18
|
+
ref="textareaRef"
|
|
19
|
+
class="nodrag w-full bg-white border border-purple-300 rounded px-1 py-0.5 text-xs font-semibold text-gray-800 leading-tight focus:outline-none focus:ring-2 focus:ring-purple-200 resize-none overflow-hidden"
|
|
20
|
+
:style="data.textStyle"
|
|
21
|
+
rows="1"
|
|
22
|
+
@blur="stopEditing"
|
|
23
|
+
@keydown.enter.prevent="stopEditing"
|
|
24
|
+
@vue:mounted="focusInput"
|
|
25
|
+
/>
|
|
26
|
+
<span
|
|
27
|
+
v-else
|
|
28
|
+
class="text-xs font-semibold text-gray-800 leading-tight break-words whitespace-pre-wrap"
|
|
29
|
+
:style="data.textStyle"
|
|
30
|
+
>{{ data.label }}</span>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<!-- Status indicator (optional) -->
|
|
35
|
+
<div class="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-green-500 border border-white opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
36
|
+
|
|
37
|
+
<Handle type="source" position="bottom" class="!bg-purple-500 !w-2.5 !h-2.5 !border-2 !border-white" />
|
|
38
|
+
</div>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<script setup lang="ts">
|
|
42
|
+
import { computed, ref, nextTick } from 'vue';
|
|
43
|
+
import { Handle } from '@vue-flow/core';
|
|
44
|
+
import { NodeResizer } from '@vue-flow/node-resizer';
|
|
45
|
+
import '@vue-flow/node-resizer/dist/style.css';
|
|
46
|
+
|
|
47
|
+
const props = defineProps<{
|
|
48
|
+
data: { label: string; style?: any; textStyle?: any; isEditing?: boolean };
|
|
49
|
+
selected?: boolean;
|
|
50
|
+
}>();
|
|
51
|
+
|
|
52
|
+
const textareaRef = ref<HTMLTextAreaElement>();
|
|
53
|
+
|
|
54
|
+
const focusInput = () => {
|
|
55
|
+
nextTick(() => {
|
|
56
|
+
textareaRef.value?.focus();
|
|
57
|
+
textareaRef.value?.select();
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const stopEditing = () => {
|
|
62
|
+
if (props.data) {
|
|
63
|
+
props.data.isEditing = false;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const containerStyle = computed(() => ({
|
|
68
|
+
backgroundColor: props.data.style?.backgroundColor || '#ffffff',
|
|
69
|
+
borderColor: props.selected ? '#a855f7' : (props.data.style?.borderColor || '#e5e7eb'),
|
|
70
|
+
borderWidth: props.data.style?.borderWidth || '1px',
|
|
71
|
+
borderStyle: props.data.style?.borderStyle || 'solid',
|
|
72
|
+
borderRadius: props.data.style?.borderRadius || '0.5rem',
|
|
73
|
+
boxShadow: props.selected ? '0 4px 6px -1px rgba(168, 85, 247, 0.2)' : (props.data.style?.boxShadow || '0 1px 2px 0 rgba(0, 0, 0, 0.05)'),
|
|
74
|
+
opacity: props.data.style?.opacity !== undefined ? props.data.style.opacity : 1,
|
|
75
|
+
padding: '0.75rem',
|
|
76
|
+
}));
|
|
77
|
+
</script>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="normal-node group relative min-w-[160px] h-full transition-all"
|
|
4
|
+
:style="containerStyle"
|
|
5
|
+
>
|
|
6
|
+
<NodeResizer :is-visible="selected" :min-width="100" :min-height="30" />
|
|
7
|
+
<Handle type="target" position="top" class="!bg-blue-500 !w-2.5 !h-2.5 !border-2 !border-white" />
|
|
8
|
+
|
|
9
|
+
<div class="flex items-center gap-3 h-full">
|
|
10
|
+
<div class="flex h-8 w-8 items-center justify-center rounded-md bg-blue-50 text-blue-600 group-hover:bg-blue-100 transition-colors">
|
|
11
|
+
<div class="i-lucide-square-activity text-lg" />
|
|
12
|
+
</div>
|
|
13
|
+
<div class="flex flex-col flex-1 min-w-0">
|
|
14
|
+
<span class="text-[10px] font-bold text-blue-600 uppercase tracking-wide leading-none mb-0.5">Action</span>
|
|
15
|
+
|
|
16
|
+
<textarea
|
|
17
|
+
v-if="data.isEditing"
|
|
18
|
+
v-model="data.label"
|
|
19
|
+
ref="textareaRef"
|
|
20
|
+
class="nodrag w-full bg-white border border-blue-300 rounded px-1 py-0.5 text-xs font-semibold text-gray-800 leading-tight focus:outline-none focus:ring-2 focus:ring-blue-200 resize-none overflow-hidden"
|
|
21
|
+
:style="data.textStyle"
|
|
22
|
+
rows="1"
|
|
23
|
+
@blur="stopEditing"
|
|
24
|
+
@keydown.enter.prevent="stopEditing"
|
|
25
|
+
@vue:mounted="focusInput"
|
|
26
|
+
/>
|
|
27
|
+
<span
|
|
28
|
+
v-else
|
|
29
|
+
class="text-xs font-semibold text-gray-800 leading-tight break-words whitespace-pre-wrap"
|
|
30
|
+
:style="data.textStyle"
|
|
31
|
+
>{{ data.label }}</span>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<Handle type="source" position="bottom" class="!bg-blue-500 !w-2.5 !h-2.5 !border-2 !border-white" />
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup lang="ts">
|
|
40
|
+
import { computed, ref, nextTick } from 'vue';
|
|
41
|
+
import { Handle } from '@vue-flow/core';
|
|
42
|
+
import { NodeResizer } from '@vue-flow/node-resizer';
|
|
43
|
+
import '@vue-flow/node-resizer/dist/style.css';
|
|
44
|
+
|
|
45
|
+
const props = defineProps<{
|
|
46
|
+
data: { label: string; style?: any; textStyle?: any; isEditing?: boolean };
|
|
47
|
+
selected?: boolean;
|
|
48
|
+
}>();
|
|
49
|
+
|
|
50
|
+
const textareaRef = ref<HTMLTextAreaElement>();
|
|
51
|
+
|
|
52
|
+
const focusInput = () => {
|
|
53
|
+
nextTick(() => {
|
|
54
|
+
textareaRef.value?.focus();
|
|
55
|
+
textareaRef.value?.select();
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const stopEditing = () => {
|
|
60
|
+
if (props.data) {
|
|
61
|
+
props.data.isEditing = false;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const containerStyle = computed(() => ({
|
|
66
|
+
backgroundColor: props.data.style?.backgroundColor || '#ffffff',
|
|
67
|
+
borderColor: props.selected ? '#60a5fa' : (props.data.style?.borderColor || '#e5e7eb'),
|
|
68
|
+
borderWidth: props.data.style?.borderWidth || '1px',
|
|
69
|
+
borderStyle: props.data.style?.borderStyle || 'solid',
|
|
70
|
+
borderRadius: props.data.style?.borderRadius || '0.5rem',
|
|
71
|
+
boxShadow: props.selected ? '0 4px 6px -1px rgba(96, 165, 250, 0.2)' : (props.data.style?.boxShadow || '0 1px 2px 0 rgba(0, 0, 0, 0.05)'),
|
|
72
|
+
opacity: props.data.style?.opacity !== undefined ? props.data.style.opacity : 1,
|
|
73
|
+
padding: '0.75rem',
|
|
74
|
+
}));
|
|
75
|
+
</script>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="sysml-block-node group relative min-w-[160px] h-full transition-all" :style="containerStyle">
|
|
3
|
+
<NodeResizer :is-visible="selected" :min-width="100" :min-height="60" />
|
|
4
|
+
<Handle type="target" position="top" class="!bg-gray-700 !w-2.5 !h-2.5 !border-2 !border-white" />
|
|
5
|
+
|
|
6
|
+
<div class="flex flex-col h-full">
|
|
7
|
+
<!-- Stereotype -->
|
|
8
|
+
<div class="px-2 pt-1 text-center text-[10px] italic text-gray-500 font-mono">
|
|
9
|
+
«block»
|
|
10
|
+
</div>
|
|
11
|
+
<!-- Name -->
|
|
12
|
+
<div v-if="data.isEditing" class="px-2 pb-2 border-b border-gray-300">
|
|
13
|
+
<input
|
|
14
|
+
v-model="data.label"
|
|
15
|
+
ref="inputRef"
|
|
16
|
+
class="nodrag w-full text-center text-sm font-bold text-gray-800 bg-white border border-blue-300 rounded px-1 focus:outline-none focus:ring-2 focus:ring-blue-200"
|
|
17
|
+
:style="data.textStyle"
|
|
18
|
+
@blur="stopEditing"
|
|
19
|
+
@keydown.enter.prevent="stopEditing"
|
|
20
|
+
@vue:mounted="focusInput"
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
<div v-else class="px-2 pb-2 text-center text-sm font-bold text-gray-800 border-b border-gray-300 break-words" :style="data.textStyle">
|
|
24
|
+
{{ data.label }}
|
|
25
|
+
</div>
|
|
26
|
+
<!-- Attributes/Operations Placeholder -->
|
|
27
|
+
<div class="p-2 min-h-[40px] text-[10px] text-gray-400 flex-1">
|
|
28
|
+
<div class="italic">properties...</div>
|
|
29
|
+
<div class="italic mt-1">operations...</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<Handle type="source" position="bottom" class="!bg-gray-700 !w-2.5 !h-2.5 !border-2 !border-white" />
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script setup lang="ts">
|
|
38
|
+
import { computed, ref, nextTick } from 'vue';
|
|
39
|
+
import { Handle } from '@vue-flow/core';
|
|
40
|
+
import { NodeResizer } from '@vue-flow/node-resizer';
|
|
41
|
+
import '@vue-flow/node-resizer/dist/style.css';
|
|
42
|
+
|
|
43
|
+
const props = defineProps<{
|
|
44
|
+
data: { label: string; style?: any; textStyle?: any; isEditing?: boolean };
|
|
45
|
+
selected?: boolean;
|
|
46
|
+
}>();
|
|
47
|
+
|
|
48
|
+
const inputRef = ref<HTMLInputElement>();
|
|
49
|
+
|
|
50
|
+
const focusInput = () => {
|
|
51
|
+
nextTick(() => {
|
|
52
|
+
inputRef.value?.focus();
|
|
53
|
+
inputRef.value?.select();
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const stopEditing = () => {
|
|
58
|
+
if (props.data) {
|
|
59
|
+
props.data.isEditing = false;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const containerStyle = computed(() => ({
|
|
64
|
+
backgroundColor: props.data.style?.backgroundColor || '#ffffff',
|
|
65
|
+
borderColor: props.selected ? '#3b82f6' : (props.data.style?.borderColor || '#374151'),
|
|
66
|
+
borderWidth: props.data.style?.borderWidth || '2px',
|
|
67
|
+
borderStyle: props.data.style?.borderStyle || 'solid',
|
|
68
|
+
borderRadius: props.data.style?.borderRadius || '0.125rem',
|
|
69
|
+
boxShadow: props.selected ? '0 4px 6px -1px rgba(59, 130, 246, 0.2)' : (props.data.style?.boxShadow || '0 1px 2px 0 rgba(0, 0, 0, 0.05)'),
|
|
70
|
+
opacity: props.data.style?.opacity !== undefined ? props.data.style.opacity : 1,
|
|
71
|
+
}));
|
|
72
|
+
</script>
|