@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,327 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex h-full flex-col bg-white/80 backdrop-blur-xl 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-4">
|
|
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.5" @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-1 flex flex-col overflow-hidden">
|
|
13
|
+
<el-tabs v-model="activeTab" class="h-full flex flex-col custom-tabs" stretch>
|
|
14
|
+
<!-- Style Tab -->
|
|
15
|
+
<el-tab-pane :label="$t('propertyPanel.tabs.style')" name="style" class="h-full overflow-y-auto custom-scrollbar">
|
|
16
|
+
<div class="p-4 space-y-6">
|
|
17
|
+
<!-- Fill -->
|
|
18
|
+
<div class="space-y-3">
|
|
19
|
+
<div class="flex items-center justify-between">
|
|
20
|
+
<el-checkbox v-model="styleForm.fill" :label="$t('propertyPanel.style.fill')" />
|
|
21
|
+
<div class="flex items-center gap-2" v-if="styleForm.fill">
|
|
22
|
+
<el-color-picker v-model="styleForm.fillColor" size="small" show-alpha />
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<!-- Stroke -->
|
|
28
|
+
<div class="space-y-3">
|
|
29
|
+
<div class="flex items-center justify-between">
|
|
30
|
+
<el-checkbox v-model="styleForm.stroke" :label="$t('propertyPanel.style.stroke')" />
|
|
31
|
+
<div class="flex items-center gap-2" v-if="styleForm.stroke">
|
|
32
|
+
<el-color-picker v-model="styleForm.strokeColor" size="small" show-alpha />
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<div v-if="styleForm.stroke" class="pl-6 space-y-2">
|
|
36
|
+
<div class="flex items-center gap-2">
|
|
37
|
+
<el-select v-model="styleForm.strokeType" size="small" class="flex-1">
|
|
38
|
+
<el-option value="solid" label="Solid" />
|
|
39
|
+
<el-option value="dashed" label="Dashed" />
|
|
40
|
+
<el-option value="dotted" label="Dotted" />
|
|
41
|
+
</el-select>
|
|
42
|
+
<el-input-number v-model="styleForm.strokeWidth" size="small" :min="0" :max="20" controls-position="right" class="!w-20">
|
|
43
|
+
<template #suffix>pt</template>
|
|
44
|
+
</el-input-number>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- Opacity -->
|
|
50
|
+
<div class="space-y-2">
|
|
51
|
+
<div class="flex items-center justify-between text-xs text-gray-600">
|
|
52
|
+
<span>{{ $t('propertyPanel.style.opacity') }}</span>
|
|
53
|
+
<span>{{ styleForm.opacity }}%</span>
|
|
54
|
+
</div>
|
|
55
|
+
<el-slider v-model="styleForm.opacity" :min="0" :max="100" size="small" />
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<!-- Effects -->
|
|
59
|
+
<div class="grid grid-cols-2 gap-3">
|
|
60
|
+
<el-checkbox v-model="styleForm.rounded" :label="$t('propertyPanel.style.rounded')" />
|
|
61
|
+
<el-checkbox v-model="styleForm.shadow" :label="$t('propertyPanel.style.shadow')" />
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</el-tab-pane>
|
|
65
|
+
|
|
66
|
+
<!-- Text Tab -->
|
|
67
|
+
<el-tab-pane :label="$t('propertyPanel.tabs.text')" name="text" class="h-full overflow-y-auto custom-scrollbar">
|
|
68
|
+
<div class="p-4 space-y-6">
|
|
69
|
+
<!-- Content Editing (Double click fallback) -->
|
|
70
|
+
<div class="space-y-2">
|
|
71
|
+
<label class="text-xs font-medium text-gray-500">{{ $t('propertyPanel.text.content') }}</label>
|
|
72
|
+
<el-input v-model="textForm.content" type="textarea" :rows="2" resize="none" />
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<!-- Font -->
|
|
76
|
+
<div class="space-y-3">
|
|
77
|
+
<div class="flex items-center gap-2">
|
|
78
|
+
<el-select v-model="textForm.fontFamily" size="small" class="flex-1">
|
|
79
|
+
<el-option value="Helvetica" label="Helvetica" />
|
|
80
|
+
<el-option value="Arial" label="Arial" />
|
|
81
|
+
<el-option value="Times New Roman" label="Times New Roman" />
|
|
82
|
+
</el-select>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="flex items-center gap-2">
|
|
85
|
+
<div class="flex border border-gray-200 rounded overflow-hidden">
|
|
86
|
+
<button
|
|
87
|
+
@click="textForm.bold = !textForm.bold"
|
|
88
|
+
:class="['p-1.5 hover:bg-gray-50 transition-colors', textForm.bold ? 'bg-gray-100 text-purple-600' : 'text-gray-500']"
|
|
89
|
+
>
|
|
90
|
+
<div class="i-lucide-bold w-4 h-4" />
|
|
91
|
+
</button>
|
|
92
|
+
<div class="w-px bg-gray-200" />
|
|
93
|
+
<button
|
|
94
|
+
@click="textForm.italic = !textForm.italic"
|
|
95
|
+
:class="['p-1.5 hover:bg-gray-50 transition-colors', textForm.italic ? 'bg-gray-100 text-purple-600' : 'text-gray-500']"
|
|
96
|
+
>
|
|
97
|
+
<div class="i-lucide-italic w-4 h-4" />
|
|
98
|
+
</button>
|
|
99
|
+
<div class="w-px bg-gray-200" />
|
|
100
|
+
<button
|
|
101
|
+
@click="textForm.underline = !textForm.underline"
|
|
102
|
+
:class="['p-1.5 hover:bg-gray-50 transition-colors', textForm.underline ? 'bg-gray-100 text-purple-600' : 'text-gray-500']"
|
|
103
|
+
>
|
|
104
|
+
<div class="i-lucide-underline w-4 h-4" />
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
<el-input-number v-model="textForm.fontSize" size="small" :min="8" :max="72" controls-position="right" class="flex-1">
|
|
108
|
+
<template #suffix>px</template>
|
|
109
|
+
</el-input-number>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<!-- Alignment -->
|
|
114
|
+
<div class="flex items-center justify-between border border-gray-200 rounded p-1">
|
|
115
|
+
<button
|
|
116
|
+
v-for="align in ['left', 'center', 'right']"
|
|
117
|
+
:key="align"
|
|
118
|
+
@click="textForm.align = align"
|
|
119
|
+
:class="['p-1 rounded hover:bg-gray-100 transition-colors', textForm.align === align ? 'bg-gray-100 text-purple-600' : 'text-gray-400']"
|
|
120
|
+
>
|
|
121
|
+
<div :class="`i-lucide-align-${align} w-4 h-4`" />
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<!-- Color -->
|
|
126
|
+
<div class="flex items-center justify-between">
|
|
127
|
+
<el-checkbox v-model="textForm.hasColor" :label="$t('propertyPanel.text.color')" />
|
|
128
|
+
<el-color-picker v-if="textForm.hasColor" v-model="textForm.color" size="small" />
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</el-tab-pane>
|
|
132
|
+
|
|
133
|
+
<!-- Arrange Tab -->
|
|
134
|
+
<el-tab-pane :label="$t('propertyPanel.tabs.arrange')" name="arrange" class="h-full overflow-y-auto custom-scrollbar">
|
|
135
|
+
<div class="p-4 space-y-6">
|
|
136
|
+
<!-- Z-Order -->
|
|
137
|
+
<div class="grid grid-cols-2 gap-2">
|
|
138
|
+
<el-button size="small" @click="$emit('z-index', 'front')">{{ $t('propertyPanel.arrange.front') }}</el-button>
|
|
139
|
+
<el-button size="small" @click="$emit('z-index', 'back')">{{ $t('propertyPanel.arrange.back') }}</el-button>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<!-- Size -->
|
|
143
|
+
<div class="space-y-3">
|
|
144
|
+
<div class="text-xs font-bold text-gray-900">{{ $t('propertyPanel.arrange.size') }}</div>
|
|
145
|
+
<div class="grid grid-cols-2 gap-3">
|
|
146
|
+
<div class="space-y-1">
|
|
147
|
+
<div class="text-[10px] text-gray-400">W</div>
|
|
148
|
+
<el-input-number v-model="arrangeForm.width" size="small" controls-position="right" class="!w-full" />
|
|
149
|
+
</div>
|
|
150
|
+
<div class="space-y-1">
|
|
151
|
+
<div class="text-[10px] text-gray-400">H</div>
|
|
152
|
+
<el-input-number v-model="arrangeForm.height" size="small" controls-position="right" class="!w-full" />
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<!-- Position -->
|
|
158
|
+
<div class="space-y-3">
|
|
159
|
+
<div class="text-xs font-bold text-gray-900">{{ $t('propertyPanel.arrange.position') }}</div>
|
|
160
|
+
<div class="grid grid-cols-2 gap-3">
|
|
161
|
+
<div class="space-y-1">
|
|
162
|
+
<div class="text-[10px] text-gray-400">X</div>
|
|
163
|
+
<el-input-number v-model="arrangeForm.x" size="small" controls-position="right" class="!w-full" />
|
|
164
|
+
</div>
|
|
165
|
+
<div class="space-y-1">
|
|
166
|
+
<div class="text-[10px] text-gray-400">Y</div>
|
|
167
|
+
<el-input-number v-model="arrangeForm.y" size="small" controls-position="right" class="!w-full" />
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</el-tab-pane>
|
|
173
|
+
</el-tabs>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</template>
|
|
177
|
+
|
|
178
|
+
<script setup lang="ts">
|
|
179
|
+
import { ref, watch, reactive } from 'vue';
|
|
180
|
+
|
|
181
|
+
const props = defineProps<{
|
|
182
|
+
node: any;
|
|
183
|
+
}>();
|
|
184
|
+
|
|
185
|
+
const emit = defineEmits<{
|
|
186
|
+
(e: 'close'): void;
|
|
187
|
+
(e: 'update', data: any): void;
|
|
188
|
+
(e: 'z-index', action: string): void;
|
|
189
|
+
}>();
|
|
190
|
+
|
|
191
|
+
const activeTab = ref('style');
|
|
192
|
+
|
|
193
|
+
// Internal state initialized from node data
|
|
194
|
+
const styleForm = reactive({
|
|
195
|
+
fill: true,
|
|
196
|
+
fillColor: '#ffffff',
|
|
197
|
+
stroke: true,
|
|
198
|
+
strokeColor: '#000000',
|
|
199
|
+
strokeType: 'solid',
|
|
200
|
+
strokeWidth: 1,
|
|
201
|
+
opacity: 100,
|
|
202
|
+
rounded: true,
|
|
203
|
+
shadow: true,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const textForm = reactive({
|
|
207
|
+
content: '',
|
|
208
|
+
fontFamily: 'Helvetica',
|
|
209
|
+
bold: false,
|
|
210
|
+
italic: false,
|
|
211
|
+
underline: false,
|
|
212
|
+
fontSize: 14,
|
|
213
|
+
align: 'center',
|
|
214
|
+
hasColor: true,
|
|
215
|
+
color: '#000000',
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const arrangeForm = reactive({
|
|
219
|
+
width: 160,
|
|
220
|
+
height: 80,
|
|
221
|
+
x: 0,
|
|
222
|
+
y: 0,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Watch node changes to update form
|
|
226
|
+
watch(() => props.node, (newNode) => {
|
|
227
|
+
if (!newNode) return;
|
|
228
|
+
|
|
229
|
+
// Style
|
|
230
|
+
const style = newNode.data?.style || {};
|
|
231
|
+
styleForm.fill = style.fill !== false;
|
|
232
|
+
styleForm.fillColor = style.backgroundColor || '#ffffff';
|
|
233
|
+
styleForm.stroke = style.stroke !== false;
|
|
234
|
+
styleForm.strokeColor = style.borderColor || '#e5e7eb'; // default border
|
|
235
|
+
styleForm.strokeWidth = style.borderWidth !== undefined ? parseInt(style.borderWidth) : 1;
|
|
236
|
+
styleForm.rounded = style.borderRadius !== '0px';
|
|
237
|
+
styleForm.shadow = !!style.boxShadow;
|
|
238
|
+
styleForm.opacity = (style.opacity !== undefined ? style.opacity : 1) * 100;
|
|
239
|
+
|
|
240
|
+
// Text
|
|
241
|
+
textForm.content = newNode.data?.label || '';
|
|
242
|
+
const textStyle = newNode.data?.textStyle || {};
|
|
243
|
+
textForm.fontSize = parseInt(textStyle.fontSize) || 14;
|
|
244
|
+
textForm.bold = textStyle.fontWeight === 'bold';
|
|
245
|
+
textForm.italic = textStyle.fontStyle === 'italic';
|
|
246
|
+
textForm.color = textStyle.color || '#1f2937';
|
|
247
|
+
textForm.align = textStyle.textAlign || 'center';
|
|
248
|
+
|
|
249
|
+
// Arrange
|
|
250
|
+
arrangeForm.width = newNode.style?.width ? parseInt(newNode.style.width) : 160;
|
|
251
|
+
arrangeForm.height = newNode.style?.height ? parseInt(newNode.style.height) : 80;
|
|
252
|
+
arrangeForm.x = Math.round(newNode.position?.x || 0);
|
|
253
|
+
arrangeForm.y = Math.round(newNode.position?.y || 0);
|
|
254
|
+
}, { immediate: true, deep: true });
|
|
255
|
+
|
|
256
|
+
// Watch form changes to emit updates
|
|
257
|
+
watch(styleForm, () => {
|
|
258
|
+
emit('update', {
|
|
259
|
+
type: 'style',
|
|
260
|
+
data: {
|
|
261
|
+
backgroundColor: styleForm.fill ? styleForm.fillColor : 'transparent',
|
|
262
|
+
borderColor: styleForm.stroke ? styleForm.strokeColor : 'transparent',
|
|
263
|
+
borderWidth: `${styleForm.strokeWidth}px`,
|
|
264
|
+
borderStyle: styleForm.strokeType,
|
|
265
|
+
borderRadius: styleForm.rounded ? '8px' : '0px',
|
|
266
|
+
boxShadow: styleForm.shadow ? '0 4px 6px -1px rgb(0 0 0 / 0.1)' : 'none',
|
|
267
|
+
opacity: styleForm.opacity / 100,
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
watch(textForm, () => {
|
|
273
|
+
emit('update', {
|
|
274
|
+
type: 'text',
|
|
275
|
+
data: {
|
|
276
|
+
label: textForm.content,
|
|
277
|
+
textStyle: {
|
|
278
|
+
fontSize: `${textForm.fontSize}px`,
|
|
279
|
+
fontWeight: textForm.bold ? 'bold' : 'normal',
|
|
280
|
+
fontStyle: textForm.italic ? 'italic' : 'normal',
|
|
281
|
+
textDecoration: textForm.underline ? 'underline' : 'none',
|
|
282
|
+
color: textForm.hasColor ? textForm.color : 'inherit',
|
|
283
|
+
textAlign: textForm.align,
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
watch(arrangeForm, () => {
|
|
290
|
+
emit('update', {
|
|
291
|
+
type: 'arrange',
|
|
292
|
+
data: {
|
|
293
|
+
width: `${arrangeForm.width}px`,
|
|
294
|
+
height: `${arrangeForm.height}px`,
|
|
295
|
+
x: arrangeForm.x,
|
|
296
|
+
y: arrangeForm.y,
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
</script>
|
|
302
|
+
|
|
303
|
+
<style scoped>
|
|
304
|
+
.custom-tabs :deep(.el-tabs__header) {
|
|
305
|
+
margin: 0;
|
|
306
|
+
background-color: transparent;
|
|
307
|
+
border-bottom: 1px solid #f3f4f6;
|
|
308
|
+
}
|
|
309
|
+
.custom-tabs :deep(.el-tabs__nav-wrap::after) {
|
|
310
|
+
display: none;
|
|
311
|
+
}
|
|
312
|
+
.custom-tabs :deep(.el-tabs__item) {
|
|
313
|
+
font-size: 12px;
|
|
314
|
+
font-weight: 600;
|
|
315
|
+
color: #6b7280;
|
|
316
|
+
}
|
|
317
|
+
.custom-tabs :deep(.el-tabs__item.is-active) {
|
|
318
|
+
color: #7c3aed; /* purple-600 */
|
|
319
|
+
}
|
|
320
|
+
.custom-tabs :deep(.el-tabs__active-bar) {
|
|
321
|
+
background-color: #7c3aed;
|
|
322
|
+
}
|
|
323
|
+
.custom-tabs :deep(.el-tabs__content) {
|
|
324
|
+
flex: 1;
|
|
325
|
+
overflow: hidden;
|
|
326
|
+
}
|
|
327
|
+
</style>
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex h-full flex-col bg-azure/80 backdrop-blur-xl border-r border-gray-200 w-72 shadow-[4px_0_24px_rgba(0,0,0,0.08)] z-20 relative" style="background-color: azure;">
|
|
3
|
+
<!-- Header -->
|
|
4
|
+
<div class="p-4 border-b border-gray-100">
|
|
5
|
+
<div class="relative group">
|
|
6
|
+
<el-input
|
|
7
|
+
v-model="searchText"
|
|
8
|
+
:placeholder="$t('nodePanel.search')"
|
|
9
|
+
class="w-full !rounded-xl transition-all duration-300"
|
|
10
|
+
>
|
|
11
|
+
<template #prefix>
|
|
12
|
+
<div class="i-lucide-search text-gray-400 group-hover:text-purple-500 transition-colors" />
|
|
13
|
+
</template>
|
|
14
|
+
</el-input>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<!-- Scrollable Content -->
|
|
19
|
+
<div class="flex-1 overflow-y-auto p-4 space-y-6 custom-scrollbar">
|
|
20
|
+
<el-collapse v-model="activeNames" class="custom-collapse border-none">
|
|
21
|
+
<!-- Section: Common -->
|
|
22
|
+
<el-collapse-item :title="$t('nodePanel.general')" name="general">
|
|
23
|
+
<div class="grid grid-cols-1 gap-3 px-1">
|
|
24
|
+
<div
|
|
25
|
+
v-for="node in filteredGeneralNodes"
|
|
26
|
+
:key="node.label"
|
|
27
|
+
class="group relative flex items-center p-2.5 rounded-xl bg-gradient-to-br from-white to-gray-50/50 border border-gray-100 hover:border-purple-200/60 shadow-sm hover:shadow-[0_8px_20px_rgba(168,85,247,0.08)] cursor-grab active:cursor-grabbing transition-all duration-300 hover:-translate-y-0.5"
|
|
28
|
+
:draggable="node.draggable"
|
|
29
|
+
@dragstart="node.draggable && onDragStart($event, node.type)"
|
|
30
|
+
:title="node.fullLabel || node.label"
|
|
31
|
+
>
|
|
32
|
+
<!-- Left: Icon -->
|
|
33
|
+
<div class="w-10 h-10 flex items-center justify-center rounded-xl bg-purple-50/50 group-hover:bg-purple-100/60 group-hover:scale-110 transition-all duration-300">
|
|
34
|
+
<div :class="[node.icon, 'text-lg text-purple-500/80 group-hover:text-purple-600 transition-colors duration-300']" />
|
|
35
|
+
</div>
|
|
36
|
+
<!-- Right: Label -->
|
|
37
|
+
<div class="flex-1 pl-3">
|
|
38
|
+
<span class="text-sm font-medium text-gray-600 group-hover:text-gray-900 transition-colors">{{ node.label }}</span>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<!-- Drag Indicator -->
|
|
42
|
+
<div class="absolute right-3 opacity-0 group-hover:opacity-100 transition-all duration-300 translate-x-2 group-hover:translate-x-0 text-gray-300">
|
|
43
|
+
<div class="i-lucide-grip-vertical w-4 h-4" />
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</el-collapse-item>
|
|
48
|
+
|
|
49
|
+
<!-- Section: SysML -->
|
|
50
|
+
<el-collapse-item :title="$t('nodePanel.sysml')" name="sysml">
|
|
51
|
+
<div class="grid grid-cols-1 gap-3 px-1">
|
|
52
|
+
<div
|
|
53
|
+
v-for="node in filteredSysmlNodes"
|
|
54
|
+
:key="node.label"
|
|
55
|
+
class="group relative flex items-center p-2.5 rounded-xl bg-gradient-to-br from-white to-gray-50/50 border border-gray-100 hover:border-blue-200/60 shadow-sm hover:shadow-[0_8px_20px_rgba(59,130,246,0.08)] cursor-grab active:cursor-grabbing transition-all duration-300 hover:-translate-y-0.5"
|
|
56
|
+
:draggable="node.draggable"
|
|
57
|
+
@dragstart="node.draggable && onDragStart($event, node.type)"
|
|
58
|
+
:title="node.fullLabel || node.label"
|
|
59
|
+
>
|
|
60
|
+
<!-- Left: Icon -->
|
|
61
|
+
<div class="w-10 h-10 flex items-center justify-center rounded-xl bg-blue-50/50 group-hover:bg-blue-100/60 group-hover:scale-110 transition-all duration-300">
|
|
62
|
+
<div :class="[node.icon, 'text-lg text-blue-500/80 group-hover:text-blue-600 transition-colors duration-300']" />
|
|
63
|
+
</div>
|
|
64
|
+
<!-- Right: Label -->
|
|
65
|
+
<div class="flex-1 pl-3">
|
|
66
|
+
<span class="text-sm font-medium text-gray-600 group-hover:text-gray-900 transition-colors">{{ node.label }}</span>
|
|
67
|
+
</div>
|
|
68
|
+
<!-- Drag Indicator -->
|
|
69
|
+
<div class="absolute right-3 opacity-0 group-hover:opacity-100 transition-all duration-300 translate-x-2 group-hover:translate-x-0 text-gray-300">
|
|
70
|
+
<div class="i-lucide-grip-vertical w-4 h-4" />
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</el-collapse-item>
|
|
75
|
+
|
|
76
|
+
<!-- Section: Advanced -->
|
|
77
|
+
<el-collapse-item :title="$t('nodePanel.advanced')" name="advanced">
|
|
78
|
+
<div class="px-1">
|
|
79
|
+
<div class="flex flex-col items-center justify-center py-8 border border-dashed border-gray-200 rounded-xl bg-gray-50/30">
|
|
80
|
+
<div class="i-lucide-package-open text-xl text-gray-300 mb-2" />
|
|
81
|
+
<span class="text-[10px] text-gray-400 font-medium">{{ $t('nodePanel.noAdvanced') }}</span>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</el-collapse-item>
|
|
85
|
+
</el-collapse>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<!-- Footer -->
|
|
89
|
+
<div class="p-4 border-t border-gray-100 bg-white/40 backdrop-blur-md">
|
|
90
|
+
<el-button class="w-full !rounded-xl !h-10 !border-gray-200/60 !bg-white/80 hover:!bg-white hover:!border-purple-200 hover:!text-purple-600 !shadow-sm hover:!shadow-lg hover:!shadow-purple-500/10 transition-all duration-300 group">
|
|
91
|
+
<div class="i-lucide-plus mr-2 group-hover:scale-110 transition-transform duration-300" />
|
|
92
|
+
<span class="text-xs font-medium">{{ $t('nodePanel.moreShapes') }}</span>
|
|
93
|
+
</el-button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<style scoped>
|
|
99
|
+
.custom-scrollbar::-webkit-scrollbar {
|
|
100
|
+
width: 4px;
|
|
101
|
+
}
|
|
102
|
+
.custom-scrollbar::-webkit-scrollbar-track {
|
|
103
|
+
background: transparent;
|
|
104
|
+
}
|
|
105
|
+
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
106
|
+
background: #e5e7eb;
|
|
107
|
+
border-radius: 4px;
|
|
108
|
+
}
|
|
109
|
+
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
110
|
+
background: #d1d5db;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Custom Collapse Styles */
|
|
114
|
+
.custom-collapse :deep(.el-collapse-item__header) {
|
|
115
|
+
background-color: transparent;
|
|
116
|
+
border-bottom: none;
|
|
117
|
+
font-size: 0.75rem; /* text-xs */
|
|
118
|
+
font-weight: 700; /* font-bold */
|
|
119
|
+
color: #9ca3af; /* text-gray-400 */
|
|
120
|
+
text-transform: uppercase;
|
|
121
|
+
letter-spacing: 0.05em; /* tracking-wider */
|
|
122
|
+
height: 32px;
|
|
123
|
+
line-height: 32px;
|
|
124
|
+
margin-bottom: 0.5rem;
|
|
125
|
+
}
|
|
126
|
+
.custom-collapse :deep(.el-collapse-item__header:hover) {
|
|
127
|
+
color: #374151; /* text-gray-700 */
|
|
128
|
+
}
|
|
129
|
+
.custom-collapse :deep(.el-collapse-item__wrap) {
|
|
130
|
+
background-color: transparent;
|
|
131
|
+
border-bottom: none;
|
|
132
|
+
}
|
|
133
|
+
.custom-collapse :deep(.el-collapse-item__content) {
|
|
134
|
+
padding-bottom: 0.5rem;
|
|
135
|
+
}
|
|
136
|
+
.custom-collapse :deep(.el-collapse-item__arrow) {
|
|
137
|
+
margin: 0 0.5rem 0 0;
|
|
138
|
+
color: inherit;
|
|
139
|
+
transform: rotate(-90deg);
|
|
140
|
+
transition: transform 0.3s;
|
|
141
|
+
}
|
|
142
|
+
.custom-collapse :deep(.el-collapse-item.is-active .el-collapse-item__arrow) {
|
|
143
|
+
transform: rotate(0deg);
|
|
144
|
+
}
|
|
145
|
+
</style>
|
|
146
|
+
|
|
147
|
+
<script setup lang="ts">
|
|
148
|
+
import { ref, computed } from 'vue';
|
|
149
|
+
import { useI18n } from 'vue-i18n';
|
|
150
|
+
|
|
151
|
+
const { t } = useI18n();
|
|
152
|
+
const searchText = ref('');
|
|
153
|
+
const activeNames = ref(['general', 'sysml']);
|
|
154
|
+
|
|
155
|
+
const generalNodes = computed(() => [
|
|
156
|
+
{
|
|
157
|
+
type: 'custom-ai',
|
|
158
|
+
label: t('nodePanel.nodes.ai'),
|
|
159
|
+
fullLabel: t('nodePanel.nodes.aiFull'),
|
|
160
|
+
icon: 'i-lucide-brain-circuit',
|
|
161
|
+
wrapperClass: 'hover:bg-purple-50 hover:border-purple-200',
|
|
162
|
+
iconClass: 'text-purple-600',
|
|
163
|
+
textClass: 'group-hover:text-purple-700',
|
|
164
|
+
draggable: true,
|
|
165
|
+
showLabel: true
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
type: 'custom-normal',
|
|
169
|
+
label: t('nodePanel.nodes.action'),
|
|
170
|
+
fullLabel: t('nodePanel.nodes.actionFull'),
|
|
171
|
+
icon: 'i-lucide-square-activity',
|
|
172
|
+
wrapperClass: 'hover:bg-blue-50 hover:border-blue-200',
|
|
173
|
+
iconClass: 'text-blue-600',
|
|
174
|
+
textClass: 'group-hover:text-blue-700',
|
|
175
|
+
draggable: true,
|
|
176
|
+
showLabel: true
|
|
177
|
+
},
|
|
178
|
+
// { label: t('nodePanel.nodes.square'), icon: 'i-lucide-square', iconClass: 'text-gray-400', wrapperClass: 'hover:bg-gray-100', showLabel: false, type: '', draggable: false },
|
|
179
|
+
// { label: t('nodePanel.nodes.circle'), icon: 'i-lucide-circle', iconClass: 'text-gray-400', wrapperClass: 'hover:bg-gray-100', showLabel: false, type: '', draggable: false },
|
|
180
|
+
// { label: t('nodePanel.nodes.diamond'), icon: 'i-lucide-diamond', iconClass: 'text-gray-400', wrapperClass: 'hover:bg-gray-100', showLabel: false, type: '', draggable: false },
|
|
181
|
+
// { label: t('nodePanel.nodes.triangle'), icon: 'i-lucide-triangle', iconClass: 'text-gray-400', wrapperClass: 'hover:bg-gray-100', showLabel: false, type: '', draggable: false },
|
|
182
|
+
// { label: t('nodePanel.nodes.cylinder'), icon: 'i-lucide-cylinder', iconClass: 'text-gray-400', wrapperClass: 'hover:bg-gray-100', showLabel: false, type: '', draggable: false },
|
|
183
|
+
// { label: t('nodePanel.nodes.file'), icon: 'i-lucide-file', iconClass: 'text-gray-400', wrapperClass: 'hover:bg-gray-100', showLabel: false, type: '', draggable: false },
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
const sysmlNodes = computed(() => [
|
|
187
|
+
{
|
|
188
|
+
type: 'sysml-block',
|
|
189
|
+
label: t('nodePanel.nodes.sysmlBlock'),
|
|
190
|
+
fullLabel: t('nodePanel.nodes.sysmlBlockFull'),
|
|
191
|
+
icon: 'i-lucide-box',
|
|
192
|
+
wrapperClass: 'hover:bg-gray-100 hover:border-gray-400',
|
|
193
|
+
iconClass: 'text-gray-700',
|
|
194
|
+
textClass: 'group-hover:text-gray-900',
|
|
195
|
+
draggable: true,
|
|
196
|
+
showLabel: true
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
type: 'sysml-req',
|
|
200
|
+
label: t('nodePanel.nodes.sysmlReq'),
|
|
201
|
+
fullLabel: t('nodePanel.nodes.sysmlReqFull'),
|
|
202
|
+
icon: 'i-lucide-file-text',
|
|
203
|
+
wrapperClass: 'hover:bg-yellow-50 hover:border-yellow-200',
|
|
204
|
+
iconClass: 'text-yellow-600',
|
|
205
|
+
textClass: 'group-hover:text-yellow-700',
|
|
206
|
+
draggable: true,
|
|
207
|
+
showLabel: true
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
type: 'sysml-usecase',
|
|
211
|
+
label: t('nodePanel.nodes.sysmlUseCase'),
|
|
212
|
+
fullLabel: t('nodePanel.nodes.sysmlUseCaseFull'),
|
|
213
|
+
icon: 'i-lucide-circle',
|
|
214
|
+
wrapperClass: 'hover:bg-green-50 hover:border-green-200',
|
|
215
|
+
iconClass: 'text-green-600',
|
|
216
|
+
textClass: 'group-hover:text-green-700',
|
|
217
|
+
draggable: true,
|
|
218
|
+
showLabel: true
|
|
219
|
+
},
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
const filterNodes = (nodes: any[], query: string) => {
|
|
223
|
+
if (!query) return nodes;
|
|
224
|
+
return nodes.filter(node =>
|
|
225
|
+
node.label.toLowerCase().includes(query) ||
|
|
226
|
+
(node.fullLabel && node.fullLabel.toLowerCase().includes(query))
|
|
227
|
+
);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const filteredGeneralNodes = computed(() => filterNodes(generalNodes.value, searchText.value.toLowerCase()));
|
|
231
|
+
const filteredSysmlNodes = computed(() => filterNodes(sysmlNodes.value, searchText.value.toLowerCase()));
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
const onDragStart = (event: DragEvent, nodeType: string) => {
|
|
235
|
+
if (event.dataTransfer) {
|
|
236
|
+
event.dataTransfer.setData('application/vueflow', nodeType);
|
|
237
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
</script>
|