android-midscene-automation 0.1.21 → 0.1.23

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.
@@ -1,52 +1,17 @@
1
1
  <script setup lang="ts">
2
- import { computed, reactive, shallowRef, watch } from 'vue';
2
+ import { computed, shallowRef } from 'vue';
3
3
  import { ElMessage } from 'element-plus';
4
- import { CopyDocument, Delete, Edit, FullScreen, Link, Timer, VideoPlay } from '@element-plus/icons-vue';
4
+ import { Aim, CopyDocument, FullScreen } from '@element-plus/icons-vue';
5
5
  import type { AppiumRecordedStep } from '../types';
6
6
  import { createFlowClipboard } from '../flow-copy';
7
- import {
8
- buildConditionLayouts,
9
- FLOW_BRANCH_GAP,
10
- FLOW_NODE_WIDTH,
11
- placeConditionLayout,
12
- } from '../flow-layout';
13
- import FlowStepEditor from './FlowStepEditor.vue';
14
- import NestedConditionBranches from './NestedConditionBranches.vue';
15
-
16
- type FlowKind = 'action' | 'condition' | 'assertion';
17
- type BranchName = 'yes' | 'no';
18
- type FlowViewport = 'main' | 'overview';
19
- type StepItem = { step: AppiumRecordedStep; index: number };
20
- const PASTE_COMMAND = '__paste_flow_nodes__';
21
- type InsertAction =
22
- | 'delay'
23
- | 'tap'
24
- | 'input'
25
- | 'clearInput'
26
- | 'coordinateTap'
27
- | 'longPress'
28
- | 'keyBack'
29
- | 'keyHome'
30
- | 'keyRecent'
31
- | 'keyPower'
32
- | 'swipe'
33
- | 'pinch'
34
- | 'launchApp'
35
- | 'popupCondition'
36
- | 'tapIfExists'
37
- | 'inputIfExists'
38
- | 'clearIfExists'
39
- | 'backIfExists'
40
- | 'waitFor'
41
- | 'assertExists'
42
- | 'assertText'
43
- | 'waitDisappear'
44
- | 'waitActivity'
45
- | 'runScript';
7
+ import FlowCanvas from './FlowCanvas.vue';
8
+ import { labelFlowStep } from '../flow-labels';
9
+ import type { FlowActionGroup, FlowBranch, InsertAction } from '../flow-graph';
46
10
 
47
11
  const props = defineProps<{
48
12
  steps: AppiumRecordedStep[];
49
13
  disabled?: boolean;
14
+ removeDisabled?: boolean;
50
15
  allowedLockedActions?: InsertAction[];
51
16
  launchingStepId?: string;
52
17
  clipboardCount?: number;
@@ -55,13 +20,12 @@ const props = defineProps<{
55
20
  const emit = defineEmits<{
56
21
  remove: [index: number];
57
22
  copy: [indexes: number[]];
58
- paste: [index: number, branch?: BranchName];
23
+ paste: [index: number, branch?: FlowBranch];
59
24
  addDelay: [index?: number];
60
25
  insertAction: [index: number, action: InsertAction];
61
- insertBranchAction: [index: number, branch: BranchName, action: InsertAction];
62
- connectNext: [index: number, branch: BranchName];
63
- disconnectNext: [index: number, branch: BranchName];
26
+ insertBranchAction: [index: number, branch: FlowBranch, action: InsertAction];
64
27
  editInput: [index: number];
28
+ previewLinkedScript: [index: number];
65
29
  executeStep: [index: number];
66
30
  updateStep: [index: number, step: AppiumRecordedStep];
67
31
  }>();
@@ -70,61 +34,9 @@ const expandedStepIndex = shallowRef<number | null>(null);
70
34
  const copyMode = shallowRef(false);
71
35
  const selectedCopyIndexes = shallowRef<number[]>([]);
72
36
  const flowDialogVisible = shallowRef(false);
73
- const flowPan = reactive({ x: 0, y: 0 });
74
- const flowScale = shallowRef(1);
75
- const overviewPan = reactive({ x: 0, y: 0 });
76
- const overviewScale = shallowRef(1);
77
- const draggingViewport = shallowRef<FlowViewport | null>(null);
78
- const flowDragMoved = shallowRef(false);
79
- let dragStart = { pointerId: 0, x: 0, y: 0, panX: 0, panY: 0, viewport: 'main' as FlowViewport };
80
-
81
- const branchTargetIds = computed(() => {
82
- const ids = new Set<string>();
83
- props.steps.forEach((step) => {
84
- if (step.flow?.parentConditionId) ids.add(step.id);
85
- });
86
- return ids;
87
- });
88
-
89
- const stepItems = computed<StepItem[]>(() => props.steps
90
- .map((step, index) => ({ step, index }))
91
- );
92
-
93
- const visibleStepItems = computed(() => stepItems.value
94
- .filter(({ step }) => !branchTargetIds.value.has(step.id)));
95
- const hasFlowSteps = computed(() => visibleStepItems.value.length > 0);
96
- const hasLaunchAppStep = computed(() => props.steps.some((step) => step.type === 'launchApp'));
97
- const conditionLayouts = computed(() => buildConditionLayouts(props.steps));
98
- const flowContentWidth = computed(() => Math.max(
99
- 980,
100
- ...visibleStepItems.value.map(({ step }) => conditionLayouts.value.get(step.id)?.totalWidth || 0),
101
- ));
102
- const flowAxisByStepId = computed(() => {
103
- const axes = new Map<string, number>();
104
- let inheritedAxis = 50;
105
- visibleStepItems.value.forEach(({ step }) => {
106
- const incomingAxes: number[] = [];
107
- visibleStepItems.value.forEach(({ step: condition }) => {
108
- if (defaultKind(condition) !== 'condition') return;
109
- const layout = conditionLayouts.value.get(condition.id);
110
- if (!layout) return;
111
- const placement = placeConditionLayout(
112
- layout,
113
- flowContentWidth.value,
114
- axes.get(condition.id) ?? inheritedAxis,
115
- );
116
- if (branchConnectionTargetId(condition, 'yes') === step.id) incomingAxes.push(placement.yesAxis);
117
- if (branchConnectionTargetId(condition, 'no') === step.id) incomingAxes.push(placement.noAxis);
118
- });
119
- if (incomingAxes.length) {
120
- inheritedAxis = incomingAxes.reduce((total, axis) => total + axis, 0) / incomingAxes.length;
121
- }
122
- axes.set(step.id, inheritedAxis);
123
- });
124
- return axes;
125
- });
37
+ const mainResetViewToken = shallowRef(0);
126
38
 
127
- const insertActionGroups: Array<{ title: string; actions: Array<{ type: InsertAction; label: string }> }> = [
39
+ const insertActionGroups: FlowActionGroup[] = [
128
40
  {
129
41
  title: '组件操作',
130
42
  actions: [
@@ -144,6 +56,7 @@ const insertActionGroups: Array<{ title: string; actions: Array<{ type: InsertAc
144
56
  { type: 'keyPower', label: '电源键' },
145
57
  { type: 'swipe', label: '滑动' },
146
58
  { type: 'pinch', label: '双指缩放' },
59
+ { type: 'clearAppData', label: '清理 App 缓存' },
147
60
  { type: 'launchApp', label: '启动 App' },
148
61
  ],
149
62
  },
@@ -165,197 +78,17 @@ const insertActionGroups: Array<{ title: string; actions: Array<{ type: InsertAc
165
78
  },
166
79
  {
167
80
  title: '流程控制',
168
- actions: [
169
- { type: 'runScript', label: '连接脚本' },
170
- ],
81
+ actions: [{ type: 'runScript', label: '连接脚本' }],
171
82
  },
172
83
  ];
173
84
 
174
85
  const mainActionGroups = insertActionGroups.map((group) => ({
175
86
  ...group,
176
- actions: group.actions.filter((action) => action.type !== 'launchApp'),
87
+ actions: group.actions.filter((action) => action.type !== 'launchApp' && action.type !== 'clearAppData'),
177
88
  }));
178
-
179
89
  const startActionGroups = insertActionGroups;
180
-
181
- function stepMeta(step: AppiumRecordedStep) {
182
- if (step.type === 'delay') return `${step.timeoutMs || 1000}ms`;
183
- if (step.type === 'input' || step.type === 'inputIfExists') return `输入内容:${step.value || '空'}`;
184
- if (defaultKind(step) === 'condition' && step.value) {
185
- return `${step.flow?.textMatch === 'exact' ? '精准匹配' : '模糊匹配'}:${step.value}`;
186
- }
187
- if (step.contextSelector && step.selector) {
188
- return `父级 ${step.contextSelector.strategy} ${step.contextSelector.value || ''} + 子级 ${step.selector.strategy} ${step.selector.value || ''}`;
189
- }
190
- if (step.type === 'key') return `keyCode ${step.keyCode || ''}`;
191
- if (step.type === 'waitActivity') return step.value || '';
192
- if (step.type === 'launchApp') return step.value || '';
193
- if (step.type === 'runScript') return step.value ? `脚本 ${step.value}` : '';
194
- if (step.type === 'screenshot') return '保存当前截图';
195
- if (step.type === 'swipe') {
196
- const swipe = step.swipe;
197
- return swipe ? `[${swipe.startX},${swipe.startY}] -> [${swipe.endX},${swipe.endY}]` : '';
198
- }
199
- if (step.type === 'pinch') return step.pinch?.direction === 'out' ? '放大' : '缩小';
200
- if (step.type === 'longPress') return `${step.fallback?.centerX || ''},${step.fallback?.centerY || ''}`;
201
- if (step.type === 'coordinateTap') return `${step.fallback?.centerX || ''},${step.fallback?.centerY || ''}`;
202
- if (step.type === 'assertText') return step.value || '';
203
- return `${step.selector?.strategy || ''} ${step.selector?.value || ''}`;
204
- }
205
-
206
- function defaultKind(step: AppiumRecordedStep): FlowKind {
207
- if (step.flow?.nodeKind) return step.flow.nodeKind;
208
- if (step.type === 'assertExists' || step.type === 'assertText') return 'assertion';
209
- return 'action';
210
- }
211
-
212
- function kindLabel(kind: FlowKind) {
213
- return {
214
- action: '操作',
215
- condition: '判断',
216
- assertion: '校验',
217
- }[kind];
218
- }
219
-
220
- function typeLabel(step: AppiumRecordedStep) {
221
- const labelMap: Partial<Record<AppiumRecordedStep['type'], string>> = {
222
- tap: '点击',
223
- input: '输入',
224
- tapIfExists: '存在则点击',
225
- inputIfExists: '存在则输入',
226
- clearIfExists: '存在则清空',
227
- backIfExists: '存在则返回',
228
- clearInput: '清空',
229
- waitFor: '等待出现',
230
- waitDisappear: '等待消失',
231
- assertExists: '断言存在',
232
- assertText: '断言文本',
233
- key: '按键',
234
- waitActivity: '等待 Activity',
235
- delay: '延时',
236
- coordinateTap: '坐标点击',
237
- swipe: '滑动',
238
- screenshot: '截图',
239
- launchApp: '启动 APP',
240
- longPress: '长按',
241
- pinch: '双指缩放',
242
- runScript: '连接脚本',
243
- };
244
- return labelMap[step.type] || step.type;
245
- }
246
-
247
- function targetLabel(id?: string) {
248
- if (!id) return '结束';
249
- const index = props.steps.findIndex((step) => step.id === id);
250
- if (index < 0) return '未找到目标';
251
- return props.steps[index].label;
252
- }
253
-
254
- function branchStepItems(step: AppiumRecordedStep, branch: BranchName) {
255
- return stepItems.value.filter((item) => (
256
- item.step.flow?.parentConditionId === step.id
257
- && item.step.flow?.parentBranch === branch
258
- ));
259
- }
260
-
261
- function branchConnectionTargetId(step: AppiumRecordedStep, branch: BranchName) {
262
- const branchItems = branchStepItems(step, branch);
263
- if (branchItems.length) {
264
- return branchItems[branchItems.length - 1]?.step.flow?.successTargetId || '';
265
- }
266
- const targetId = branch === 'yes' ? step.flow?.yesTargetId : step.flow?.noTargetId;
267
- const target = props.steps.find((item) => item.id === targetId);
268
- return target && !target.flow?.parentConditionId ? target.id : '';
269
- }
270
-
271
- function hasBranchConnection(step: AppiumRecordedStep) {
272
- return Boolean(
273
- branchConnectionTargetId(step, 'yes')
274
- || branchConnectionTargetId(step, 'no'),
275
- );
276
- }
277
-
278
- function hasFollowingMainStep(index: number) {
279
- return props.steps.slice(index + 1).some((step) => !step.flow?.parentConditionId);
280
- }
281
-
282
- function hasFollowingVisibleStep(step: AppiumRecordedStep) {
283
- const index = visibleStepItems.value.findIndex((item) => item.step.id === step.id);
284
- return index >= 0 && index < visibleStepItems.value.length - 1;
285
- }
286
-
287
- function shouldShowBranchConnect(index: number, step: AppiumRecordedStep, branch: BranchName) {
288
- return Boolean(branchConnectionTargetId(step, branch)) || hasFollowingMainStep(index);
289
- }
290
-
291
- function flowAxis(step: AppiumRecordedStep) {
292
- return flowAxisByStepId.value.get(step.id) || 50;
293
- }
294
-
295
- function flowAxisStyle(step: AppiumRecordedStep) {
296
- return { '--flow-axis': `${flowAxis(step)}%` };
297
- }
298
-
299
- function conditionLayout(step: AppiumRecordedStep) {
300
- return conditionLayouts.value.get(step.id) || {
301
- yesWidth: FLOW_NODE_WIDTH,
302
- noWidth: FLOW_NODE_WIDTH,
303
- totalWidth: FLOW_NODE_WIDTH * 2 + FLOW_BRANCH_GAP,
304
- yesAxis: 25,
305
- noAxis: 75,
306
- };
307
- }
308
-
309
- function branchAxis(step: AppiumRecordedStep, branch: BranchName) {
310
- const layout = conditionLayout(step);
311
- const placement = placeConditionLayout(layout, flowContentWidth.value, flowAxis(step));
312
- return branch === 'yes' ? placement.yesAxis : placement.noAxis;
313
- }
314
-
315
- function branchLayoutStyle(step: AppiumRecordedStep) {
316
- const layout = conditionLayout(step);
317
- const placement = placeConditionLayout(layout, flowContentWidth.value, flowAxis(step));
318
- return {
319
- width: `${layout.totalWidth}px`,
320
- minWidth: `${layout.totalWidth}px`,
321
- gridTemplateColumns: `${layout.yesWidth}px ${layout.noWidth}px`,
322
- justifySelf: 'start',
323
- marginLeft: `${placement.left}px`,
324
- };
325
- }
326
-
327
- function branchPath(step: AppiumRecordedStep, branch: BranchName) {
328
- const layout = conditionLayout(step);
329
- const placement = placeConditionLayout(layout, flowContentWidth.value, flowAxis(step));
330
- const targetAxis = branch === 'yes' ? layout.yesAxis : layout.noAxis;
331
- return `M${placement.startAxis} 0 C${placement.startAxis} 16 ${targetAxis} 16 ${targetAxis} 31 L${targetAxis} 48`;
332
- }
333
-
334
- function mergeTargetAxis(step: AppiumRecordedStep) {
335
- const axes = [
336
- branchConnectionTargetId(step, 'yes') ? branchAxis(step, 'yes') : 0,
337
- branchConnectionTargetId(step, 'no') ? branchAxis(step, 'no') : 0,
338
- ].filter(Boolean);
339
- return axes.reduce((total, axis) => total + axis, 0) / axes.length;
340
- }
341
-
342
- function mergePath(step: AppiumRecordedStep, branch: BranchName) {
343
- const sourceAxis = branchAxis(step, branch);
344
- const targetAxis = mergeTargetAxis(step);
345
- return `M${sourceAxis} 0 C${sourceAxis} 20 ${targetAxis} 20 ${targetAxis} 44`;
346
- }
347
-
348
- function toggleBranchConnection(index: number, step: AppiumRecordedStep, branch: BranchName) {
349
- if (branchConnectionTargetId(step, branch)) {
350
- emit('disconnectNext', index, branch);
351
- } else {
352
- emit('connectNext', index, branch);
353
- }
354
- }
355
-
356
- function toggleExpanded(index: number) {
357
- expandedStepIndex.value = expandedStepIndex.value === index ? null : index;
358
- }
90
+ const hasLaunchAppStep = computed(() => props.steps.some((step) => step.type === 'launchApp'));
91
+ const hasClearAppDataStep = computed(() => props.steps.some((step) => step.type === 'clearAppData'));
359
92
 
360
93
  function isDescendantStep(descendantIndex: number, ancestorIndex: number) {
361
94
  const ancestorId = props.steps[ancestorIndex]?.id;
@@ -375,21 +108,22 @@ function isCopySelected(index: number) {
375
108
  function toggleCopySelection(index: number) {
376
109
  const step = props.steps[index];
377
110
  if (!step) return;
378
- if (step.type === 'launchApp') {
379
- ElMessage.warning('启动 APP 节点不能复制');
111
+ if (step.type === 'launchApp' || step.type === 'clearAppData') {
112
+ ElMessage.warning('App 初始化节点不能复制');
380
113
  return;
381
114
  }
382
- const current = selectedCopyIndexes.value;
383
- if (current.includes(index)) {
384
- selectedCopyIndexes.value = current.filter((item) => item !== index);
115
+ if (selectedCopyIndexes.value.includes(index)) {
116
+ selectedCopyIndexes.value = selectedCopyIndexes.value.filter((item) => item !== index);
385
117
  return;
386
118
  }
387
- if (current.some((item) => isDescendantStep(index, item))) {
119
+ if (selectedCopyIndexes.value.some((item) => isDescendantStep(index, item))) {
388
120
  ElMessage.info('该子节点已随判断节点选中');
389
121
  return;
390
122
  }
391
- const effectiveCurrent = current.filter((item) => !isDescendantStep(item, index));
392
- const candidate = [...effectiveCurrent, index].sort((left, right) => left - right);
123
+ const candidate = [
124
+ ...selectedCopyIndexes.value.filter((item) => !isDescendantStep(item, index)),
125
+ index,
126
+ ].sort((left, right) => left - right);
393
127
  try {
394
128
  createFlowClipboard(props.steps, candidate);
395
129
  } catch (error) {
@@ -399,6 +133,21 @@ function toggleCopySelection(index: number) {
399
133
  selectedCopyIndexes.value = candidate;
400
134
  }
401
135
 
136
+ function handleNodeClick(index: number) {
137
+ if (copyMode.value) {
138
+ toggleCopySelection(index);
139
+ return;
140
+ }
141
+ const step = props.steps[index];
142
+ if (
143
+ expandedStepIndex.value !== index
144
+ && (step?.type === 'input' || step?.type === 'inputIfExists')
145
+ ) {
146
+ emit('editInput', index);
147
+ }
148
+ expandedStepIndex.value = expandedStepIndex.value === index ? null : index;
149
+ }
150
+
402
151
  function startCopyMode() {
403
152
  copyMode.value = true;
404
153
  selectedCopyIndexes.value = [];
@@ -416,36 +165,12 @@ function copySelectedNodes() {
416
165
  cancelCopyMode();
417
166
  }
418
167
 
419
- function copySingleNode(index: number) {
420
- emit('copy', [index]);
168
+ function resetMainFlowPosition() {
169
+ mainResetViewToken.value += 1;
421
170
  }
422
171
 
423
- function insertAction(index: number, command: string | number | object) {
424
- if (command === PASTE_COMMAND) {
425
- emit('paste', index);
426
- return;
427
- }
428
- emit('insertAction', index, command as InsertAction);
429
- }
430
-
431
- function insertStartAction(command: string | number | object) {
432
- if (command === PASTE_COMMAND) {
433
- emit('paste', -1);
434
- return;
435
- }
436
- emit('insertAction', -1, command as InsertAction);
437
- }
438
-
439
- function insertBranchAction(index: number, branch: BranchName, command: string | number | object) {
440
- if (command === PASTE_COMMAND) {
441
- emit('paste', index, branch);
442
- return;
443
- }
444
- emit('insertBranchAction', index, branch, command as InsertAction);
445
- }
446
-
447
- function updateStep(payload: { index: number; step: AppiumRecordedStep }) {
448
- emit('updateStep', payload.index, payload.step);
172
+ function canOpenInsertMenu() {
173
+ return !props.disabled || Boolean(props.allowedLockedActions?.length);
449
174
  }
450
175
 
451
176
  function isInsertActionDisabled(action: InsertAction) {
@@ -453,105 +178,18 @@ function isInsertActionDisabled(action: InsertAction) {
453
178
  }
454
179
 
455
180
  function isStartActionDisabled(action: InsertAction) {
456
- return (action === 'launchApp' && hasLaunchAppStep.value) || isInsertActionDisabled(action);
457
- }
458
-
459
- function isLaunchExecutionDisabled() {
460
- return Boolean(props.disabled && !props.allowedLockedActions?.includes('launchApp'));
461
- }
462
-
463
- function canOpenInsertMenu() {
464
- return !props.disabled || Boolean(props.allowedLockedActions?.length);
465
- }
466
-
467
- function isInteractiveTarget(target: EventTarget | null) {
468
- if (!(target instanceof Element)) return false;
469
- if (target.closest('.appium-flow-node__toggle')) return false;
470
- return Boolean(target.closest(
471
- 'button,input,textarea,select,.el-select,.el-input,.el-input-number',
472
- ));
473
- }
474
-
475
- function resetFlowView() {
476
- flowPan.x = 0;
477
- flowPan.y = 0;
478
- flowScale.value = 1;
479
- overviewPan.x = 0;
480
- overviewPan.y = 0;
481
- overviewScale.value = 1;
482
- }
483
-
484
- function viewportState(viewport: FlowViewport) {
485
- return viewport === 'overview'
486
- ? { pan: overviewPan, scale: overviewScale }
487
- : { pan: flowPan, scale: flowScale };
181
+ return (action === 'launchApp' && hasLaunchAppStep.value)
182
+ || (action === 'clearAppData' && hasClearAppDataStep.value)
183
+ || isInsertActionDisabled(action);
488
184
  }
489
185
 
490
- function handleFlowWheel(event: WheelEvent, viewport: FlowViewport) {
491
- if (!event.ctrlKey) return;
492
- event.preventDefault();
493
- const { pan, scale } = viewportState(viewport);
494
- const canvas = event.currentTarget as HTMLElement;
495
- const bounds = canvas.getBoundingClientRect();
496
- const cursorX = event.clientX - bounds.left;
497
- const cursorY = event.clientY - bounds.top;
498
- const previousScale = scale.value;
499
- const direction = event.deltaY < 0 ? 1 : -1;
500
- const nextScale = Math.min(2, Math.max(0.5, Math.round((previousScale + direction * 0.1) * 10) / 10));
501
- if (nextScale === previousScale) return;
502
- pan.x = cursorX - ((cursorX - pan.x) / previousScale) * nextScale;
503
- pan.y = cursorY - ((cursorY - pan.y) / previousScale) * nextScale;
504
- scale.value = nextScale;
505
- }
506
-
507
- function handleNodeClick(index: number) {
508
- if (flowDragMoved.value) return;
509
- if (copyMode.value) {
510
- toggleCopySelection(index);
511
- return;
512
- }
513
- toggleExpanded(index);
514
- }
515
-
516
- function startFlowDrag(event: PointerEvent, viewport: FlowViewport) {
517
- if (isInteractiveTarget(event.target)) return;
518
- const { pan } = viewportState(viewport);
519
- draggingViewport.value = viewport;
520
- flowDragMoved.value = false;
521
- dragStart = {
522
- pointerId: event.pointerId,
523
- x: event.clientX,
524
- y: event.clientY,
525
- panX: pan.x,
526
- panY: pan.y,
527
- viewport,
528
- };
529
- }
530
-
531
- function moveFlowDrag(event: PointerEvent, viewport: FlowViewport) {
532
- if (draggingViewport.value !== viewport || event.pointerId !== dragStart.pointerId) return;
533
- if (!flowDragMoved.value && Math.abs(event.clientX - dragStart.x) + Math.abs(event.clientY - dragStart.y) > 4) {
534
- flowDragMoved.value = true;
535
- (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
536
- }
537
- const { pan } = viewportState(viewport);
538
- pan.x = dragStart.panX + event.clientX - dragStart.x;
539
- pan.y = dragStart.panY + event.clientY - dragStart.y;
186
+ function isAppExecutionDisabled(action: 'launchApp' | 'clearAppData') {
187
+ return Boolean(props.disabled && !props.allowedLockedActions?.includes(action));
540
188
  }
541
189
 
542
- function stopFlowDrag(event: PointerEvent, viewport: FlowViewport) {
543
- if (draggingViewport.value !== viewport || event.pointerId !== dragStart.pointerId) return;
544
- draggingViewport.value = null;
545
- const target = event.currentTarget as HTMLElement;
546
- if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId);
547
- window.setTimeout(() => {
548
- flowDragMoved.value = false;
549
- }, 0);
190
+ function updateStep(index: number, step: AppiumRecordedStep) {
191
+ emit('updateStep', index, step);
550
192
  }
551
-
552
- watch(() => props.steps.length, () => {
553
- resetFlowView();
554
- });
555
193
  </script>
556
194
 
557
195
  <template>
@@ -579,6 +217,14 @@ watch(() => props.steps.length, () => {
579
217
  >
580
218
  批量复制
581
219
  </el-button>
220
+ <el-button
221
+ size="small"
222
+ :icon="Aim"
223
+ :disabled="!steps.length"
224
+ @click="resetMainFlowPosition"
225
+ >
226
+ 还原位置
227
+ </el-button>
582
228
  <el-button
583
229
  size="small"
584
230
  :icon="FullScreen"
@@ -588,539 +234,38 @@ watch(() => props.steps.length, () => {
588
234
  放大
589
235
  </el-button>
590
236
  </div>
591
- <div
592
- class="appium-flow-canvas"
593
- :class="{ 'appium-flow-canvas--dragging': draggingViewport === 'main' }"
594
- @pointerdown="startFlowDrag($event, 'main')"
595
- @pointermove="moveFlowDrag($event, 'main')"
596
- @pointerup="stopFlowDrag($event, 'main')"
597
- @pointercancel="stopFlowDrag($event, 'main')"
598
- @pointerleave="stopFlowDrag($event, 'main')"
599
- @wheel="handleFlowWheel($event, 'main')"
600
- >
601
- <div
602
- class="appium-flow-content"
603
- :style="{
604
- transform: `translate(${flowPan.x}px, ${flowPan.y}px) scale(${flowScale})`,
605
- width: `${flowContentWidth}px`,
606
- minWidth: `${flowContentWidth}px`,
607
- '--appium-flow-line-width': '3px',
608
- }"
609
- >
610
- <div class="appium-flow-start">
611
- <svg
612
- v-if="hasFlowSteps"
613
- class="appium-flow-line appium-flow-start__line"
614
- viewBox="0 0 10 100"
615
- preserveAspectRatio="none"
616
- aria-hidden="true"
617
- >
618
- <path d="M5 0 V100" />
619
- </svg>
620
- <div class="appium-flow-start__node">开始</div>
621
- <el-dropdown
622
- trigger="click"
623
- :disabled="!canOpenInsertMenu()"
624
- max-height="320px"
625
- popper-class="appium-action-dropdown"
626
- @command="insertStartAction($event)"
627
- >
628
- <button
629
- type="button"
630
- class="appium-flow-insert"
631
- :disabled="!canOpenInsertMenu()"
632
- >
633
- <el-icon><Timer /></el-icon>
634
- <span>插入操作</span>
635
- </button>
636
- <template #dropdown>
637
- <el-dropdown-menu>
638
- <el-dropdown-item
639
- v-if="clipboardCount"
640
- :command="PASTE_COMMAND"
641
- divided
642
- >
643
- 粘贴 {{ clipboardCount }} 个节点
644
- </el-dropdown-item>
645
- <template v-for="group in startActionGroups" :key="group.title">
646
- <div class="appium-action-dropdown__group">{{ group.title }}</div>
647
- <el-dropdown-item
648
- v-for="action in group.actions"
649
- :key="action.type"
650
- :command="action.type"
651
- :disabled="isStartActionDisabled(action.type)"
652
- >
653
- {{ action.label }}
654
- </el-dropdown-item>
655
- </template>
656
- </el-dropdown-menu>
657
- </template>
658
- </el-dropdown>
659
- </div>
660
- <div
661
- v-for="{ step, index } in visibleStepItems"
662
- :key="step.id"
663
- class="appium-flow-item"
664
- :class="{ 'appium-flow-item--condition': defaultKind(step) === 'condition' }"
665
- :style="flowAxisStyle(step)"
666
- >
667
- <svg
668
- v-if="defaultKind(step) !== 'condition' && hasFollowingVisibleStep(step)"
669
- class="appium-flow-line appium-flow-item__line"
670
- viewBox="0 0 10 100"
671
- preserveAspectRatio="none"
672
- aria-hidden="true"
673
- >
674
- <path d="M5 0 V100" />
675
- </svg>
676
- <div
677
- class="appium-flow-node"
678
- :class="[
679
- `appium-flow-node--${defaultKind(step)}`,
680
- {
681
- 'appium-flow-node--expanded': expandedStepIndex === index,
682
- 'appium-flow-node--selected': isCopySelected(index),
683
- 'appium-flow-node--copying': copyMode,
684
- },
685
- ]"
686
- >
687
- <button
688
- type="button"
689
- class="appium-flow-node__toggle"
690
- :aria-expanded="expandedStepIndex === index"
691
- @click.stop="handleNodeClick(index)"
692
- >
693
- <span class="appium-flow-node__body">
694
- <span class="appium-flow-node__title" :title="step.label">{{ step.label }}</span>
695
- <span class="appium-flow-node__meta" :title="stepMeta(step)">
696
- {{ kindLabel(defaultKind(step)) }} · {{ typeLabel(step) }}
697
- <template v-if="step.optional"> · 可选</template>
698
- <template v-if="stepMeta(step)"> · {{ stepMeta(step) }}</template>
699
- </span>
700
- <span v-if="step.note" class="appium-flow-node__note" :title="step.note">{{ step.note }}</span>
701
- </span>
702
- </button>
703
- <span class="appium-flow-node__actions" @click.stop>
704
- <el-button
705
- v-if="!copyMode && step.type !== 'launchApp'"
706
- text
707
- size="small"
708
- :icon="CopyDocument"
709
- title="复制节点"
710
- @click="copySingleNode(index)"
711
- />
712
- <el-button
713
- v-if="step.type === 'launchApp'"
714
- text
715
- size="small"
716
- :icon="VideoPlay"
717
- :loading="launchingStepId === step.id"
718
- :disabled="isLaunchExecutionDisabled()"
719
- title="立即执行启动 App"
720
- @click="$emit('executeStep', index)"
721
- />
722
- <el-button
723
- v-if="step.type === 'input' || step.type === 'inputIfExists'"
724
- text
725
- size="small"
726
- :icon="Edit"
727
- :disabled="disabled"
728
- title="修改输入内容"
729
- @click="$emit('editInput', index)"
730
- />
731
- <el-button
732
- text
733
- size="small"
734
- :icon="Delete"
735
- :disabled="disabled"
736
- title="删除节点"
737
- @click="$emit('remove', index)"
738
- />
739
- </span>
740
- </div>
741
-
742
- <div
743
- v-if="defaultKind(step) === 'condition'"
744
- class="appium-flow-branches"
745
- :style="branchLayoutStyle(step)"
746
- >
747
- <svg class="appium-flow-branch-lines" viewBox="0 0 100 48" preserveAspectRatio="none" aria-hidden="true">
748
- <path :d="branchPath(step, 'yes')" />
749
- <path :d="branchPath(step, 'no')" />
750
- </svg>
751
- <div
752
- class="appium-flow-branch appium-flow-branch--yes"
753
- :class="{ 'appium-flow-branch--connected': branchConnectionTargetId(step, 'yes') }"
754
- >
755
- <svg
756
- class="appium-flow-line appium-flow-branch__line"
757
- viewBox="0 0 10 100"
758
- preserveAspectRatio="none"
759
- aria-hidden="true"
760
- >
761
- <path d="M5 0 V100" />
762
- </svg>
763
- <strong class="appium-flow-branch__label">是</strong>
764
- <div v-if="branchStepItems(step, 'yes').length" class="appium-flow-branch-steps">
765
- <div
766
- v-for="item in branchStepItems(step, 'yes')"
767
- :key="item.step.id"
768
- class="appium-flow-branch-step-group"
769
- >
770
- <div
771
- class="appium-flow-node appium-flow-branch-step"
772
- :class="[
773
- `appium-flow-node--${defaultKind(item.step)}`,
774
- {
775
- 'appium-flow-node--expanded': expandedStepIndex === item.index,
776
- 'appium-flow-node--selected': isCopySelected(item.index),
777
- 'appium-flow-node--copying': copyMode,
778
- },
779
- ]"
780
- >
781
- <button
782
- type="button"
783
- class="appium-flow-node__toggle"
784
- :aria-expanded="expandedStepIndex === item.index"
785
- @click.stop="handleNodeClick(item.index)"
786
- >
787
- <span class="appium-flow-node__body">
788
- <span class="appium-flow-node__title">{{ item.step.label }}</span>
789
- <span class="appium-flow-node__meta">
790
- {{ kindLabel(defaultKind(item.step)) }} · {{ typeLabel(item.step) }}
791
- <template v-if="stepMeta(item.step)"> · {{ stepMeta(item.step) }}</template>
792
- </span>
793
- <span v-if="item.step.note" class="appium-flow-node__note" :title="item.step.note">{{ item.step.note }}</span>
794
- </span>
795
- </button>
796
- <span class="appium-flow-node__actions" @click.stop>
797
- <el-button
798
- v-if="!copyMode"
799
- text
800
- size="small"
801
- :icon="CopyDocument"
802
- title="复制节点"
803
- @click="copySingleNode(item.index)"
804
- />
805
- <el-button
806
- text
807
- size="small"
808
- :icon="Delete"
809
- :disabled="disabled"
810
- title="删除节点"
811
- @click="$emit('remove', item.index)"
812
- />
813
- </span>
814
- </div>
815
- <FlowStepEditor
816
- v-if="expandedStepIndex === item.index"
817
- :step="item.step"
818
- :index="item.index"
819
- :disabled="disabled"
820
- @update="updateStep"
821
- />
822
- <NestedConditionBranches
823
- v-if="defaultKind(item.step) === 'condition'"
824
- :steps="steps"
825
- :condition="item.step"
826
- :condition-index="item.index"
827
- :expanded-step-index="expandedStepIndex"
828
- :copy-mode="copyMode"
829
- :selected-copy-indexes="selectedCopyIndexes"
830
- :disabled="disabled"
831
- :allowed-locked-actions="allowedLockedActions"
832
- :clipboard-count="clipboardCount"
833
- @node-click="handleNodeClick"
834
- @copy="$emit('copy', $event)"
835
- @remove="$emit('remove', $event)"
836
- @paste="(childIndex, childBranch) => $emit('paste', childIndex, childBranch)"
837
- @insert-branch-action="(childIndex, childBranch, action) => $emit('insertBranchAction', childIndex, childBranch, action)"
838
- @connect-next="(childIndex, childBranch) => $emit('connectNext', childIndex, childBranch)"
839
- @disconnect-next="(childIndex, childBranch) => $emit('disconnectNext', childIndex, childBranch)"
840
- @edit-input="$emit('editInput', $event)"
841
- @update-step="(childIndex, childStep) => $emit('updateStep', childIndex, childStep)"
842
- />
843
- </div>
844
- </div>
845
- <span
846
- v-if="!branchStepItems(step, 'yes').length && !branchConnectionTargetId(step, 'yes')"
847
- class="appium-flow-branch__target"
848
- >
849
- {{ targetLabel(step.flow?.yesTargetId) }}
850
- </span>
851
- <div class="appium-flow-branch__actions">
852
- <el-dropdown
853
- trigger="click"
854
- :disabled="!canOpenInsertMenu()"
855
- max-height="320px"
856
- popper-class="appium-action-dropdown"
857
- @command="insertBranchAction(index, 'yes', $event)"
858
- >
859
- <el-button
860
- size="small"
861
- class="appium-flow-branch__insert"
862
- :disabled="!canOpenInsertMenu()"
863
- :icon="Timer"
864
- @click.stop
865
- >
866
- 插入操作
867
- </el-button>
868
- <template #dropdown>
869
- <el-dropdown-menu>
870
- <el-dropdown-item
871
- v-if="clipboardCount"
872
- :command="PASTE_COMMAND"
873
- divided
874
- >
875
- 粘贴 {{ clipboardCount }} 个节点
876
- </el-dropdown-item>
877
- <template v-for="group in mainActionGroups" :key="group.title">
878
- <div class="appium-action-dropdown__group">{{ group.title }}</div>
879
- <el-dropdown-item
880
- v-for="action in group.actions"
881
- :key="action.type"
882
- :command="action.type"
883
- :disabled="isInsertActionDisabled(action.type)"
884
- >
885
- {{ action.label }}
886
- </el-dropdown-item>
887
- </template>
888
- </el-dropdown-menu>
889
- </template>
890
- </el-dropdown>
891
- <el-button
892
- v-if="shouldShowBranchConnect(index, step, 'yes')"
893
- size="small"
894
- class="appium-flow-branch__connect"
895
- :icon="Link"
896
- :disabled="disabled"
897
- :title="branchConnectionTargetId(step, 'yes') ? '取消当前分支与主流程的连接' : '自动连接判断节点之后的主流程节点'"
898
- @click.stop="toggleBranchConnection(index, step, 'yes')"
899
- >
900
- {{ branchConnectionTargetId(step, 'yes') ? '取消连接' : '连接到下一节点' }}
901
- </el-button>
902
- </div>
903
- </div>
904
- <div
905
- class="appium-flow-branch appium-flow-branch--no"
906
- :class="{ 'appium-flow-branch--connected': branchConnectionTargetId(step, 'no') }"
907
- >
908
- <svg
909
- class="appium-flow-line appium-flow-branch__line"
910
- viewBox="0 0 10 100"
911
- preserveAspectRatio="none"
912
- aria-hidden="true"
913
- >
914
- <path d="M5 0 V100" />
915
- </svg>
916
- <strong class="appium-flow-branch__label">否</strong>
917
- <div v-if="branchStepItems(step, 'no').length" class="appium-flow-branch-steps">
918
- <div
919
- v-for="item in branchStepItems(step, 'no')"
920
- :key="item.step.id"
921
- class="appium-flow-branch-step-group"
922
- >
923
- <div
924
- class="appium-flow-node appium-flow-branch-step"
925
- :class="[
926
- `appium-flow-node--${defaultKind(item.step)}`,
927
- {
928
- 'appium-flow-node--expanded': expandedStepIndex === item.index,
929
- 'appium-flow-node--selected': isCopySelected(item.index),
930
- 'appium-flow-node--copying': copyMode,
931
- },
932
- ]"
933
- >
934
- <button
935
- type="button"
936
- class="appium-flow-node__toggle"
937
- :aria-expanded="expandedStepIndex === item.index"
938
- @click.stop="handleNodeClick(item.index)"
939
- >
940
- <span class="appium-flow-node__body">
941
- <span class="appium-flow-node__title">{{ item.step.label }}</span>
942
- <span class="appium-flow-node__meta">
943
- {{ kindLabel(defaultKind(item.step)) }} · {{ typeLabel(item.step) }}
944
- <template v-if="stepMeta(item.step)"> · {{ stepMeta(item.step) }}</template>
945
- </span>
946
- <span v-if="item.step.note" class="appium-flow-node__note" :title="item.step.note">{{ item.step.note }}</span>
947
- </span>
948
- </button>
949
- <span class="appium-flow-node__actions" @click.stop>
950
- <el-button
951
- v-if="!copyMode"
952
- text
953
- size="small"
954
- :icon="CopyDocument"
955
- title="复制节点"
956
- @click="copySingleNode(item.index)"
957
- />
958
- <el-button
959
- text
960
- size="small"
961
- :icon="Delete"
962
- :disabled="disabled"
963
- title="删除节点"
964
- @click="$emit('remove', item.index)"
965
- />
966
- </span>
967
- </div>
968
- <FlowStepEditor
969
- v-if="expandedStepIndex === item.index"
970
- :step="item.step"
971
- :index="item.index"
972
- :disabled="disabled"
973
- @update="updateStep"
974
- />
975
- <NestedConditionBranches
976
- v-if="defaultKind(item.step) === 'condition'"
977
- :steps="steps"
978
- :condition="item.step"
979
- :condition-index="item.index"
980
- :expanded-step-index="expandedStepIndex"
981
- :copy-mode="copyMode"
982
- :selected-copy-indexes="selectedCopyIndexes"
983
- :disabled="disabled"
984
- :allowed-locked-actions="allowedLockedActions"
985
- :clipboard-count="clipboardCount"
986
- @node-click="handleNodeClick"
987
- @copy="$emit('copy', $event)"
988
- @remove="$emit('remove', $event)"
989
- @paste="(childIndex, childBranch) => $emit('paste', childIndex, childBranch)"
990
- @insert-branch-action="(childIndex, childBranch, action) => $emit('insertBranchAction', childIndex, childBranch, action)"
991
- @connect-next="(childIndex, childBranch) => $emit('connectNext', childIndex, childBranch)"
992
- @disconnect-next="(childIndex, childBranch) => $emit('disconnectNext', childIndex, childBranch)"
993
- @edit-input="$emit('editInput', $event)"
994
- @update-step="(childIndex, childStep) => $emit('updateStep', childIndex, childStep)"
995
- />
996
- </div>
997
- </div>
998
- <span
999
- v-if="!branchStepItems(step, 'no').length && !branchConnectionTargetId(step, 'no')"
1000
- class="appium-flow-branch__target"
1001
- >
1002
- {{ targetLabel(step.flow?.noTargetId) }}
1003
- </span>
1004
- <div class="appium-flow-branch__actions">
1005
- <el-dropdown
1006
- trigger="click"
1007
- :disabled="!canOpenInsertMenu()"
1008
- max-height="320px"
1009
- popper-class="appium-action-dropdown"
1010
- @command="insertBranchAction(index, 'no', $event)"
1011
- >
1012
- <el-button
1013
- size="small"
1014
- class="appium-flow-branch__insert"
1015
- :disabled="!canOpenInsertMenu()"
1016
- :icon="Timer"
1017
- @click.stop
1018
- >
1019
- 插入操作
1020
- </el-button>
1021
- <template #dropdown>
1022
- <el-dropdown-menu>
1023
- <el-dropdown-item
1024
- v-if="clipboardCount"
1025
- :command="PASTE_COMMAND"
1026
- divided
1027
- >
1028
- 粘贴 {{ clipboardCount }} 个节点
1029
- </el-dropdown-item>
1030
- <template v-for="group in mainActionGroups" :key="group.title">
1031
- <div class="appium-action-dropdown__group">{{ group.title }}</div>
1032
- <el-dropdown-item
1033
- v-for="action in group.actions"
1034
- :key="action.type"
1035
- :command="action.type"
1036
- :disabled="isInsertActionDisabled(action.type)"
1037
- >
1038
- {{ action.label }}
1039
- </el-dropdown-item>
1040
- </template>
1041
- </el-dropdown-menu>
1042
- </template>
1043
- </el-dropdown>
1044
- <el-button
1045
- v-if="shouldShowBranchConnect(index, step, 'no')"
1046
- size="small"
1047
- class="appium-flow-branch__connect"
1048
- :icon="Link"
1049
- :disabled="disabled"
1050
- :title="branchConnectionTargetId(step, 'no') ? '取消当前分支与主流程的连接' : '自动连接判断节点之后的主流程节点'"
1051
- @click.stop="toggleBranchConnection(index, step, 'no')"
1052
- >
1053
- {{ branchConnectionTargetId(step, 'no') ? '取消连接' : '连接到下一节点' }}
1054
- </el-button>
1055
- </div>
1056
- </div>
1057
- </div>
1058
-
1059
- <div v-if="hasBranchConnection(step)" class="appium-flow-merge">
1060
- <svg viewBox="0 0 100 44" preserveAspectRatio="none" aria-hidden="true">
1061
- <path
1062
- v-if="branchConnectionTargetId(step, 'yes')"
1063
- class="appium-flow-merge__yes"
1064
- :d="mergePath(step, 'yes')"
1065
- />
1066
- <path
1067
- v-if="branchConnectionTargetId(step, 'no')"
1068
- class="appium-flow-merge__no"
1069
- :d="mergePath(step, 'no')"
1070
- />
1071
- </svg>
1072
- </div>
1073
237
 
1074
- <FlowStepEditor
1075
- v-if="expandedStepIndex === index"
1076
- :step="step"
1077
- :index="index"
1078
- :disabled="disabled"
1079
- @update="updateStep"
1080
- />
238
+ <FlowCanvas
239
+ id="appium-flow-main"
240
+ :steps="steps"
241
+ :expanded-step-index="expandedStepIndex"
242
+ :copy-mode="copyMode"
243
+ :selected-copy-indexes="selectedCopyIndexes"
244
+ :disabled="disabled"
245
+ :remove-disabled="removeDisabled"
246
+ :launching-step-id="launchingStepId"
247
+ :clipboard-count="clipboardCount"
248
+ :reset-view-token="mainResetViewToken"
249
+ :start-action-groups="startActionGroups"
250
+ :main-action-groups="mainActionGroups"
251
+ :can-open-insert-menu="canOpenInsertMenu()"
252
+ :is-start-action-disabled="isStartActionDisabled"
253
+ :is-insert-action-disabled="isInsertActionDisabled"
254
+ :is-app-execution-disabled="isAppExecutionDisabled"
255
+ :label-step="labelFlowStep"
256
+ :is-copy-selected="isCopySelected"
257
+ @node-click="handleNodeClick"
258
+ @copy="(indexes) => emit('copy', indexes)"
259
+ @paste="(index, branch) => emit('paste', index, branch)"
260
+ @remove="(index) => emit('remove', index)"
261
+ @insert-action="(index, action) => emit('insertAction', index, action)"
262
+ @insert-branch-action="(index, branch, action) => emit('insertBranchAction', index, branch, action)"
263
+ @edit-input="(index) => emit('editInput', index)"
264
+ @preview-linked-script="(index) => emit('previewLinkedScript', index)"
265
+ @execute-step="(index) => emit('executeStep', index)"
266
+ @update-step="(index, step) => updateStep(index, step)"
267
+ />
1081
268
 
1082
- <el-dropdown
1083
- v-if="defaultKind(step) !== 'condition'"
1084
- trigger="click"
1085
- :disabled="!canOpenInsertMenu()"
1086
- max-height="320px"
1087
- popper-class="appium-action-dropdown"
1088
- @command="insertAction(index, $event)"
1089
- >
1090
- <button
1091
- type="button"
1092
- class="appium-flow-insert"
1093
- :disabled="!canOpenInsertMenu()"
1094
- >
1095
- <el-icon><Timer /></el-icon>
1096
- <span>插入操作</span>
1097
- </button>
1098
- <template #dropdown>
1099
- <el-dropdown-menu>
1100
- <el-dropdown-item
1101
- v-if="clipboardCount"
1102
- :command="PASTE_COMMAND"
1103
- divided
1104
- >
1105
- 粘贴 {{ clipboardCount }} 个节点
1106
- </el-dropdown-item>
1107
- <template v-for="group in mainActionGroups" :key="group.title">
1108
- <div class="appium-action-dropdown__group">{{ group.title }}</div>
1109
- <el-dropdown-item
1110
- v-for="action in group.actions"
1111
- :key="action.type"
1112
- :command="action.type"
1113
- :disabled="isInsertActionDisabled(action.type)"
1114
- >
1115
- {{ action.label }}
1116
- </el-dropdown-item>
1117
- </template>
1118
- </el-dropdown-menu>
1119
- </template>
1120
- </el-dropdown>
1121
- </div>
1122
- </div>
1123
- </div>
1124
269
  <el-dialog
1125
270
  v-model="flowDialogVisible"
1126
271
  title="流程总览"
@@ -1152,519 +297,36 @@ watch(() => props.steps.length, () => {
1152
297
  批量复制
1153
298
  </el-button>
1154
299
  </div>
1155
- <div
1156
- class="appium-flow-dialog-canvas"
1157
- :class="{ 'appium-flow-dialog-canvas--dragging': draggingViewport === 'overview' }"
1158
- @pointerdown="startFlowDrag($event, 'overview')"
1159
- @pointermove="moveFlowDrag($event, 'overview')"
1160
- @pointerup="stopFlowDrag($event, 'overview')"
1161
- @pointercancel="stopFlowDrag($event, 'overview')"
1162
- @pointerleave="stopFlowDrag($event, 'overview')"
1163
- @wheel="handleFlowWheel($event, 'overview')"
1164
- >
1165
- <div
1166
- class="appium-flow-content appium-flow-content--overview"
1167
- :style="{
1168
- transform: `translate(${overviewPan.x}px, ${overviewPan.y}px) scale(${overviewScale})`,
1169
- width: `${flowContentWidth}px`,
1170
- minWidth: `${flowContentWidth}px`,
1171
- '--appium-flow-line-width': '3px',
1172
- }"
1173
- >
1174
- <div class="appium-flow-start appium-flow-start--overview">
1175
- <svg
1176
- v-if="hasFlowSteps"
1177
- class="appium-flow-line appium-flow-start__line"
1178
- viewBox="0 0 10 100"
1179
- preserveAspectRatio="none"
1180
- aria-hidden="true"
1181
- >
1182
- <path d="M5 0 V100" />
1183
- </svg>
1184
- <div class="appium-flow-start__node">开始</div>
1185
- <el-dropdown
1186
- trigger="click"
1187
- :disabled="!canOpenInsertMenu()"
1188
- max-height="320px"
1189
- popper-class="appium-action-dropdown"
1190
- @command="insertStartAction($event)"
1191
- >
1192
- <button
1193
- type="button"
1194
- class="appium-flow-insert"
1195
- :disabled="!canOpenInsertMenu()"
1196
- >
1197
- <el-icon><Timer /></el-icon>
1198
- <span>插入操作</span>
1199
- </button>
1200
- <template #dropdown>
1201
- <el-dropdown-menu>
1202
- <el-dropdown-item v-if="clipboardCount" :command="PASTE_COMMAND" divided>
1203
- 粘贴 {{ clipboardCount }} 个节点
1204
- </el-dropdown-item>
1205
- <template v-for="group in startActionGroups" :key="group.title">
1206
- <div class="appium-action-dropdown__group">{{ group.title }}</div>
1207
- <el-dropdown-item
1208
- v-for="action in group.actions"
1209
- :key="action.type"
1210
- :command="action.type"
1211
- :disabled="isStartActionDisabled(action.type)"
1212
- >
1213
- {{ action.label }}
1214
- </el-dropdown-item>
1215
- </template>
1216
- </el-dropdown-menu>
1217
- </template>
1218
- </el-dropdown>
1219
- </div>
1220
- <div
1221
- v-for="{ step, index } in visibleStepItems"
1222
- :key="step.id"
1223
- class="appium-flow-item"
1224
- :class="{ 'appium-flow-item--condition': defaultKind(step) === 'condition' }"
1225
- :style="flowAxisStyle(step)"
1226
- >
1227
- <svg
1228
- v-if="defaultKind(step) !== 'condition' && hasFollowingVisibleStep(step)"
1229
- class="appium-flow-line appium-flow-item__line"
1230
- viewBox="0 0 10 100"
1231
- preserveAspectRatio="none"
1232
- aria-hidden="true"
1233
- >
1234
- <path d="M5 0 V100" />
1235
- </svg>
1236
- <div
1237
- class="appium-flow-node"
1238
- :class="[
1239
- `appium-flow-node--${defaultKind(step)}`,
1240
- {
1241
- 'appium-flow-node--expanded': expandedStepIndex === index,
1242
- 'appium-flow-node--selected': isCopySelected(index),
1243
- 'appium-flow-node--copying': copyMode,
1244
- },
1245
- ]"
1246
- >
1247
- <button
1248
- type="button"
1249
- class="appium-flow-node__toggle"
1250
- :aria-expanded="expandedStepIndex === index"
1251
- @click="handleNodeClick(index)"
1252
- >
1253
- <span class="appium-flow-node__body">
1254
- <span class="appium-flow-node__title" :title="step.label">{{ step.label }}</span>
1255
- <span class="appium-flow-node__meta" :title="stepMeta(step)">
1256
- {{ kindLabel(defaultKind(step)) }} · {{ typeLabel(step) }}
1257
- <template v-if="step.optional"> · 可选</template>
1258
- <template v-if="stepMeta(step)"> · {{ stepMeta(step) }}</template>
1259
- </span>
1260
- <span v-if="step.note" class="appium-flow-node__note" :title="step.note">{{ step.note }}</span>
1261
- </span>
1262
- </button>
1263
- <span class="appium-flow-node__actions" @click.stop>
1264
- <el-button
1265
- v-if="!copyMode && step.type !== 'launchApp'"
1266
- text
1267
- size="small"
1268
- :icon="CopyDocument"
1269
- title="复制节点"
1270
- @click="copySingleNode(index)"
1271
- />
1272
- <el-button
1273
- v-if="step.type === 'launchApp'"
1274
- text
1275
- size="small"
1276
- :icon="VideoPlay"
1277
- :loading="launchingStepId === step.id"
1278
- :disabled="isLaunchExecutionDisabled()"
1279
- title="立即执行启动 App"
1280
- @click="$emit('executeStep', index)"
1281
- />
1282
- <el-button
1283
- v-if="step.type === 'input' || step.type === 'inputIfExists'"
1284
- text
1285
- size="small"
1286
- :icon="Edit"
1287
- :disabled="disabled"
1288
- title="修改输入内容"
1289
- @click="$emit('editInput', index)"
1290
- />
1291
- <el-button
1292
- text
1293
- size="small"
1294
- :icon="Delete"
1295
- :disabled="disabled"
1296
- title="删除节点"
1297
- @click="$emit('remove', index)"
1298
- />
1299
- </span>
1300
- </div>
1301
-
1302
- <FlowStepEditor
1303
- v-if="expandedStepIndex === index"
1304
- :step="step"
1305
- :index="index"
1306
- :disabled="disabled"
1307
- @update="updateStep"
1308
- />
1309
-
1310
- <div
1311
- v-if="defaultKind(step) === 'condition'"
1312
- class="appium-flow-branches"
1313
- :style="branchLayoutStyle(step)"
1314
- >
1315
- <svg class="appium-flow-branch-lines" viewBox="0 0 100 48" preserveAspectRatio="none" aria-hidden="true">
1316
- <path :d="branchPath(step, 'yes')" />
1317
- <path :d="branchPath(step, 'no')" />
1318
- </svg>
1319
- <div
1320
- class="appium-flow-branch appium-flow-branch--yes"
1321
- :class="{ 'appium-flow-branch--connected': branchConnectionTargetId(step, 'yes') }"
1322
- >
1323
- <svg
1324
- class="appium-flow-line appium-flow-branch__line"
1325
- viewBox="0 0 10 100"
1326
- preserveAspectRatio="none"
1327
- aria-hidden="true"
1328
- >
1329
- <path d="M5 0 V100" />
1330
- </svg>
1331
- <strong class="appium-flow-branch__label">是</strong>
1332
- <div v-if="branchStepItems(step, 'yes').length" class="appium-flow-branch-steps">
1333
- <div
1334
- v-for="item in branchStepItems(step, 'yes')"
1335
- :key="item.step.id"
1336
- class="appium-flow-branch-step-group"
1337
- >
1338
- <div
1339
- class="appium-flow-node appium-flow-branch-step"
1340
- :class="[
1341
- `appium-flow-node--${defaultKind(item.step)}`,
1342
- {
1343
- 'appium-flow-node--expanded': expandedStepIndex === item.index,
1344
- 'appium-flow-node--selected': isCopySelected(item.index),
1345
- 'appium-flow-node--copying': copyMode,
1346
- },
1347
- ]"
1348
- >
1349
- <button
1350
- type="button"
1351
- class="appium-flow-node__toggle"
1352
- :aria-expanded="expandedStepIndex === item.index"
1353
- @click="handleNodeClick(item.index)"
1354
- >
1355
- <span class="appium-flow-node__body">
1356
- <span class="appium-flow-node__title">{{ item.step.label }}</span>
1357
- <span class="appium-flow-node__meta">
1358
- {{ kindLabel(defaultKind(item.step)) }} · {{ typeLabel(item.step) }}
1359
- <template v-if="stepMeta(item.step)"> · {{ stepMeta(item.step) }}</template>
1360
- </span>
1361
- <span v-if="item.step.note" class="appium-flow-node__note" :title="item.step.note">{{ item.step.note }}</span>
1362
- </span>
1363
- </button>
1364
- <span class="appium-flow-node__actions" @click.stop>
1365
- <el-button
1366
- v-if="!copyMode"
1367
- text
1368
- size="small"
1369
- :icon="CopyDocument"
1370
- title="复制节点"
1371
- @click="copySingleNode(item.index)"
1372
- />
1373
- <el-button
1374
- text
1375
- size="small"
1376
- :icon="Delete"
1377
- :disabled="disabled"
1378
- title="删除节点"
1379
- @click="$emit('remove', item.index)"
1380
- />
1381
- </span>
1382
- </div>
1383
- <FlowStepEditor
1384
- v-if="expandedStepIndex === item.index"
1385
- :step="item.step"
1386
- :index="item.index"
1387
- :disabled="disabled"
1388
- @update="updateStep"
1389
- />
1390
- <NestedConditionBranches
1391
- v-if="defaultKind(item.step) === 'condition'"
1392
- :steps="steps"
1393
- :condition="item.step"
1394
- :condition-index="item.index"
1395
- :expanded-step-index="expandedStepIndex"
1396
- :copy-mode="copyMode"
1397
- :selected-copy-indexes="selectedCopyIndexes"
1398
- :disabled="disabled"
1399
- :allowed-locked-actions="allowedLockedActions"
1400
- :clipboard-count="clipboardCount"
1401
- @node-click="handleNodeClick"
1402
- @copy="$emit('copy', $event)"
1403
- @remove="$emit('remove', $event)"
1404
- @paste="(childIndex, childBranch) => $emit('paste', childIndex, childBranch)"
1405
- @insert-branch-action="(childIndex, childBranch, action) => $emit('insertBranchAction', childIndex, childBranch, action)"
1406
- @connect-next="(childIndex, childBranch) => $emit('connectNext', childIndex, childBranch)"
1407
- @disconnect-next="(childIndex, childBranch) => $emit('disconnectNext', childIndex, childBranch)"
1408
- @edit-input="$emit('editInput', $event)"
1409
- @update-step="(childIndex, childStep) => $emit('updateStep', childIndex, childStep)"
1410
- />
1411
- </div>
1412
- </div>
1413
- <span
1414
- v-if="!branchStepItems(step, 'yes').length && !branchConnectionTargetId(step, 'yes')"
1415
- class="appium-flow-branch__target"
1416
- >
1417
- {{ targetLabel(step.flow?.yesTargetId) }}
1418
- </span>
1419
- <div class="appium-flow-branch__actions">
1420
- <el-dropdown
1421
- trigger="click"
1422
- :disabled="!canOpenInsertMenu()"
1423
- max-height="320px"
1424
- popper-class="appium-action-dropdown"
1425
- @command="insertBranchAction(index, 'yes', $event)"
1426
- >
1427
- <el-button
1428
- size="small"
1429
- class="appium-flow-branch__insert"
1430
- :disabled="!canOpenInsertMenu()"
1431
- :icon="Timer"
1432
- @click.stop
1433
- >
1434
- 插入操作
1435
- </el-button>
1436
- <template #dropdown>
1437
- <el-dropdown-menu>
1438
- <el-dropdown-item v-if="clipboardCount" :command="PASTE_COMMAND" divided>
1439
- 粘贴 {{ clipboardCount }} 个节点
1440
- </el-dropdown-item>
1441
- <template v-for="group in mainActionGroups" :key="group.title">
1442
- <div class="appium-action-dropdown__group">{{ group.title }}</div>
1443
- <el-dropdown-item
1444
- v-for="action in group.actions"
1445
- :key="action.type"
1446
- :command="action.type"
1447
- :disabled="isInsertActionDisabled(action.type)"
1448
- >
1449
- {{ action.label }}
1450
- </el-dropdown-item>
1451
- </template>
1452
- </el-dropdown-menu>
1453
- </template>
1454
- </el-dropdown>
1455
- <el-button
1456
- v-if="shouldShowBranchConnect(index, step, 'yes')"
1457
- size="small"
1458
- class="appium-flow-branch__connect"
1459
- :icon="Link"
1460
- :disabled="disabled"
1461
- @click.stop="toggleBranchConnection(index, step, 'yes')"
1462
- >
1463
- {{ branchConnectionTargetId(step, 'yes') ? '取消连接' : '连接到下一节点' }}
1464
- </el-button>
1465
- </div>
1466
- </div>
1467
- <div
1468
- class="appium-flow-branch appium-flow-branch--no"
1469
- :class="{ 'appium-flow-branch--connected': branchConnectionTargetId(step, 'no') }"
1470
- >
1471
- <svg
1472
- class="appium-flow-line appium-flow-branch__line"
1473
- viewBox="0 0 10 100"
1474
- preserveAspectRatio="none"
1475
- aria-hidden="true"
1476
- >
1477
- <path d="M5 0 V100" />
1478
- </svg>
1479
- <strong class="appium-flow-branch__label">否</strong>
1480
- <div v-if="branchStepItems(step, 'no').length" class="appium-flow-branch-steps">
1481
- <div
1482
- v-for="item in branchStepItems(step, 'no')"
1483
- :key="item.step.id"
1484
- class="appium-flow-branch-step-group"
1485
- >
1486
- <div
1487
- class="appium-flow-node appium-flow-branch-step"
1488
- :class="[
1489
- `appium-flow-node--${defaultKind(item.step)}`,
1490
- {
1491
- 'appium-flow-node--expanded': expandedStepIndex === item.index,
1492
- 'appium-flow-node--selected': isCopySelected(item.index),
1493
- 'appium-flow-node--copying': copyMode,
1494
- },
1495
- ]"
1496
- >
1497
- <button
1498
- type="button"
1499
- class="appium-flow-node__toggle"
1500
- :aria-expanded="expandedStepIndex === item.index"
1501
- @click="handleNodeClick(item.index)"
1502
- >
1503
- <span class="appium-flow-node__body">
1504
- <span class="appium-flow-node__title">{{ item.step.label }}</span>
1505
- <span class="appium-flow-node__meta">
1506
- {{ kindLabel(defaultKind(item.step)) }} · {{ typeLabel(item.step) }}
1507
- <template v-if="stepMeta(item.step)"> · {{ stepMeta(item.step) }}</template>
1508
- </span>
1509
- <span v-if="item.step.note" class="appium-flow-node__note" :title="item.step.note">{{ item.step.note }}</span>
1510
- </span>
1511
- </button>
1512
- <span class="appium-flow-node__actions" @click.stop>
1513
- <el-button
1514
- v-if="!copyMode"
1515
- text
1516
- size="small"
1517
- :icon="CopyDocument"
1518
- title="复制节点"
1519
- @click="copySingleNode(item.index)"
1520
- />
1521
- <el-button
1522
- text
1523
- size="small"
1524
- :icon="Delete"
1525
- :disabled="disabled"
1526
- title="删除节点"
1527
- @click="$emit('remove', item.index)"
1528
- />
1529
- </span>
1530
- </div>
1531
- <FlowStepEditor
1532
- v-if="expandedStepIndex === item.index"
1533
- :step="item.step"
1534
- :index="item.index"
1535
- :disabled="disabled"
1536
- @update="updateStep"
1537
- />
1538
- <NestedConditionBranches
1539
- v-if="defaultKind(item.step) === 'condition'"
1540
- :steps="steps"
1541
- :condition="item.step"
1542
- :condition-index="item.index"
1543
- :expanded-step-index="expandedStepIndex"
1544
- :copy-mode="copyMode"
1545
- :selected-copy-indexes="selectedCopyIndexes"
1546
- :disabled="disabled"
1547
- :allowed-locked-actions="allowedLockedActions"
1548
- :clipboard-count="clipboardCount"
1549
- @node-click="handleNodeClick"
1550
- @copy="$emit('copy', $event)"
1551
- @remove="$emit('remove', $event)"
1552
- @paste="(childIndex, childBranch) => $emit('paste', childIndex, childBranch)"
1553
- @insert-branch-action="(childIndex, childBranch, action) => $emit('insertBranchAction', childIndex, childBranch, action)"
1554
- @connect-next="(childIndex, childBranch) => $emit('connectNext', childIndex, childBranch)"
1555
- @disconnect-next="(childIndex, childBranch) => $emit('disconnectNext', childIndex, childBranch)"
1556
- @edit-input="$emit('editInput', $event)"
1557
- @update-step="(childIndex, childStep) => $emit('updateStep', childIndex, childStep)"
1558
- />
1559
- </div>
1560
- </div>
1561
- <span
1562
- v-if="!branchStepItems(step, 'no').length && !branchConnectionTargetId(step, 'no')"
1563
- class="appium-flow-branch__target"
1564
- >
1565
- {{ targetLabel(step.flow?.noTargetId) }}
1566
- </span>
1567
- <div class="appium-flow-branch__actions">
1568
- <el-dropdown
1569
- trigger="click"
1570
- :disabled="!canOpenInsertMenu()"
1571
- max-height="320px"
1572
- popper-class="appium-action-dropdown"
1573
- @command="insertBranchAction(index, 'no', $event)"
1574
- >
1575
- <el-button
1576
- size="small"
1577
- class="appium-flow-branch__insert"
1578
- :disabled="!canOpenInsertMenu()"
1579
- :icon="Timer"
1580
- @click.stop
1581
- >
1582
- 插入操作
1583
- </el-button>
1584
- <template #dropdown>
1585
- <el-dropdown-menu>
1586
- <el-dropdown-item v-if="clipboardCount" :command="PASTE_COMMAND" divided>
1587
- 粘贴 {{ clipboardCount }} 个节点
1588
- </el-dropdown-item>
1589
- <template v-for="group in mainActionGroups" :key="group.title">
1590
- <div class="appium-action-dropdown__group">{{ group.title }}</div>
1591
- <el-dropdown-item
1592
- v-for="action in group.actions"
1593
- :key="action.type"
1594
- :command="action.type"
1595
- :disabled="isInsertActionDisabled(action.type)"
1596
- >
1597
- {{ action.label }}
1598
- </el-dropdown-item>
1599
- </template>
1600
- </el-dropdown-menu>
1601
- </template>
1602
- </el-dropdown>
1603
- <el-button
1604
- v-if="shouldShowBranchConnect(index, step, 'no')"
1605
- size="small"
1606
- class="appium-flow-branch__connect"
1607
- :icon="Link"
1608
- :disabled="disabled"
1609
- @click.stop="toggleBranchConnection(index, step, 'no')"
1610
- >
1611
- {{ branchConnectionTargetId(step, 'no') ? '取消连接' : '连接到下一节点' }}
1612
- </el-button>
1613
- </div>
1614
- </div>
1615
- </div>
1616
- <div v-if="hasBranchConnection(step)" class="appium-flow-merge">
1617
- <svg viewBox="0 0 100 44" preserveAspectRatio="none" aria-hidden="true">
1618
- <path
1619
- v-if="branchConnectionTargetId(step, 'yes')"
1620
- class="appium-flow-merge__yes"
1621
- :d="mergePath(step, 'yes')"
1622
- />
1623
- <path
1624
- v-if="branchConnectionTargetId(step, 'no')"
1625
- class="appium-flow-merge__no"
1626
- :d="mergePath(step, 'no')"
1627
- />
1628
- </svg>
1629
- </div>
1630
- <el-dropdown
1631
- v-if="defaultKind(step) !== 'condition'"
1632
- trigger="click"
1633
- :disabled="!canOpenInsertMenu()"
1634
- max-height="320px"
1635
- popper-class="appium-action-dropdown"
1636
- @command="insertAction(index, $event)"
1637
- >
1638
- <button
1639
- type="button"
1640
- class="appium-flow-insert"
1641
- :disabled="!canOpenInsertMenu()"
1642
- >
1643
- <el-icon><Timer /></el-icon>
1644
- <span>插入操作</span>
1645
- </button>
1646
- <template #dropdown>
1647
- <el-dropdown-menu>
1648
- <el-dropdown-item v-if="clipboardCount" :command="PASTE_COMMAND" divided>
1649
- 粘贴 {{ clipboardCount }} 个节点
1650
- </el-dropdown-item>
1651
- <template v-for="group in mainActionGroups" :key="group.title">
1652
- <div class="appium-action-dropdown__group">{{ group.title }}</div>
1653
- <el-dropdown-item
1654
- v-for="action in group.actions"
1655
- :key="action.type"
1656
- :command="action.type"
1657
- :disabled="isInsertActionDisabled(action.type)"
1658
- >
1659
- {{ action.label }}
1660
- </el-dropdown-item>
1661
- </template>
1662
- </el-dropdown-menu>
1663
- </template>
1664
- </el-dropdown>
1665
- </div>
1666
- </div>
1667
- </div>
300
+ <FlowCanvas
301
+ id="appium-flow-dialog"
302
+ class="appium-flow-canvas--dialog"
303
+ :steps="steps"
304
+ :expanded-step-index="expandedStepIndex"
305
+ :copy-mode="copyMode"
306
+ :selected-copy-indexes="selectedCopyIndexes"
307
+ :disabled="disabled"
308
+ :remove-disabled="removeDisabled"
309
+ :launching-step-id="launchingStepId"
310
+ :clipboard-count="clipboardCount"
311
+ :start-action-groups="startActionGroups"
312
+ :main-action-groups="mainActionGroups"
313
+ :can-open-insert-menu="canOpenInsertMenu()"
314
+ :is-start-action-disabled="isStartActionDisabled"
315
+ :is-insert-action-disabled="isInsertActionDisabled"
316
+ :is-app-execution-disabled="isAppExecutionDisabled"
317
+ :label-step="labelFlowStep"
318
+ :is-copy-selected="isCopySelected"
319
+ @node-click="handleNodeClick"
320
+ @copy="(indexes) => emit('copy', indexes)"
321
+ @paste="(index, branch) => emit('paste', index, branch)"
322
+ @remove="(index) => emit('remove', index)"
323
+ @insert-action="(index, action) => emit('insertAction', index, action)"
324
+ @insert-branch-action="(index, branch, action) => emit('insertBranchAction', index, branch, action)"
325
+ @edit-input="(index) => emit('editInput', index)"
326
+ @preview-linked-script="(index) => emit('previewLinkedScript', index)"
327
+ @execute-step="(index) => emit('executeStep', index)"
328
+ @update-step="(index, step) => updateStep(index, step)"
329
+ />
1668
330
  </el-dialog>
1669
331
  </div>
1670
332
  </template>