android-midscene-automation 0.1.6 → 0.1.9

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,24 +1,169 @@
1
1
  <script setup lang="ts">
2
- import { Delete, Edit, Timer } from '@element-plus/icons-vue';
3
- import type { AppiumRecordedStep } from '../types';
2
+ import { computed, reactive, shallowRef, watch } from 'vue';
3
+ import { Delete, Edit, FullScreen, Timer } from '@element-plus/icons-vue';
4
+ import type { AppiumRecordedStep, AppiumSelector } from '../types';
4
5
 
5
- defineProps<{
6
+ type FlowKind = 'action' | 'condition' | 'assertion';
7
+ type BranchName = 'yes' | 'no';
8
+ type StepScope = 'pre' | 'main' | 'post';
9
+ type StepItem = { step: AppiumRecordedStep; index: number };
10
+ type InsertAction =
11
+ | 'delay'
12
+ | 'tap'
13
+ | 'input'
14
+ | 'clearInput'
15
+ | 'coordinateTap'
16
+ | 'longPress'
17
+ | 'keyBack'
18
+ | 'keyHome'
19
+ | 'keyRecent'
20
+ | 'keyPower'
21
+ | 'swipe'
22
+ | 'pinch'
23
+ | 'launchApp'
24
+ | 'popupCondition'
25
+ | 'tapIfExists'
26
+ | 'inputIfExists'
27
+ | 'clearIfExists'
28
+ | 'backIfExists'
29
+ | 'waitFor'
30
+ | 'assertExists'
31
+ | 'assertText'
32
+ | 'waitDisappear'
33
+ | 'waitActivity'
34
+ | 'runScript';
35
+
36
+ const props = defineProps<{
6
37
  steps: AppiumRecordedStep[];
7
38
  disabled?: boolean;
39
+ allowedLockedActions?: InsertAction[];
8
40
  }>();
9
41
 
10
- defineEmits<{
42
+ const emit = defineEmits<{
11
43
  remove: [index: number];
12
44
  addDelay: [index?: number];
45
+ insertAction: [index: number, action: InsertAction];
46
+ insertBranchAction: [index: number, branch: BranchName, action: InsertAction];
13
47
  editInput: [index: number];
48
+ updateStep: [index: number, step: AppiumRecordedStep];
14
49
  }>();
15
50
 
51
+ const expandedStepIndex = shallowRef<number | null>(null);
52
+ const flowDialogVisible = shallowRef(false);
53
+ const flowPan = reactive({ x: 0, y: 0 });
54
+ const flowScale = shallowRef(1);
55
+ const flowDragging = shallowRef(false);
56
+ const flowDragMoved = shallowRef(false);
57
+ let dragStart = { pointerId: 0, x: 0, y: 0, panX: 0, panY: 0 };
58
+
59
+ const stepOptions = computed(() => props.steps.map((step, index) => ({
60
+ label: `${index + 1}. ${step.label}`,
61
+ value: step.id,
62
+ })));
63
+
64
+ const branchTargetIds = computed(() => {
65
+ const ids = new Set<string>();
66
+ props.steps.forEach((step) => {
67
+ if (step.flow?.parentConditionId) ids.add(step.id);
68
+ if (step.flow?.yesTargetId) ids.add(step.flow.yesTargetId);
69
+ if (step.flow?.noTargetId) ids.add(step.flow.noTargetId);
70
+ });
71
+ return ids;
72
+ });
73
+
74
+ const stepItems = computed<StepItem[]>(() => props.steps
75
+ .map((step, index) => ({ step, index }))
76
+ );
77
+
78
+ const visibleStepItems = computed(() => stepItems.value
79
+ .filter(({ step }) => !branchTargetIds.value.has(step.id)));
80
+
81
+ const insertActionGroups: Array<{ title: string; actions: Array<{ type: InsertAction; label: string }> }> = [
82
+ {
83
+ title: '组件操作',
84
+ actions: [
85
+ { type: 'tap', label: '录制点击' },
86
+ { type: 'input', label: '录制输入' },
87
+ { type: 'clearInput', label: '清空输入' },
88
+ { type: 'coordinateTap', label: '点击坐标' },
89
+ { type: 'longPress', label: '长按' },
90
+ ],
91
+ },
92
+ {
93
+ title: '设备操作',
94
+ actions: [
95
+ { type: 'keyBack', label: '系统返回' },
96
+ { type: 'keyHome', label: 'Home 键' },
97
+ { type: 'keyRecent', label: '最近任务' },
98
+ { type: 'keyPower', label: '电源键' },
99
+ { type: 'swipe', label: '滑动' },
100
+ { type: 'pinch', label: '双指缩放' },
101
+ { type: 'launchApp', label: '启动 App' },
102
+ ],
103
+ },
104
+ {
105
+ title: '等待断言',
106
+ actions: [
107
+ { type: 'delay', label: '添加延时' },
108
+ { type: 'popupCondition', label: '判断存在' },
109
+ { type: 'tapIfExists', label: '存在则点击' },
110
+ { type: 'inputIfExists', label: '存在则输入' },
111
+ { type: 'clearIfExists', label: '存在则清空' },
112
+ { type: 'backIfExists', label: '存在则返回' },
113
+ { type: 'waitFor', label: '等待出现' },
114
+ { type: 'assertExists', label: '断言存在' },
115
+ { type: 'assertText', label: '断言文本' },
116
+ { type: 'waitDisappear', label: '等待元素消失' },
117
+ { type: 'waitActivity', label: '等待 Activity' },
118
+ ],
119
+ },
120
+ {
121
+ title: '流程控制',
122
+ actions: [
123
+ { type: 'runScript', label: '连接脚本' },
124
+ ],
125
+ },
126
+ ];
127
+
128
+ const mainActionGroups = insertActionGroups.map((group) => ({
129
+ ...group,
130
+ actions: group.actions.filter((action) => action.type !== 'launchApp'),
131
+ }));
132
+
133
+ const boundaryActionGroups = [{
134
+ title: '前后置操作',
135
+ actions: [
136
+ { type: 'launchApp' as const, label: '启动 APP' },
137
+ { type: 'delay' as const, label: '添加延时' },
138
+ ],
139
+ }];
140
+
141
+ function stepScope(step: AppiumRecordedStep): StepScope {
142
+ return step.flow?.scope || 'main';
143
+ }
144
+
145
+ function scopedStepOptions(source: AppiumRecordedStep) {
146
+ const scope = stepScope(source);
147
+ return stepOptions.value.filter((option) => {
148
+ const target = props.steps.find((step) => step.id === option.value);
149
+ return target && stepScope(target) === scope;
150
+ });
151
+ }
152
+
153
+ function actionGroupsForStep(step: AppiumRecordedStep) {
154
+ return stepScope(step) === 'main' ? mainActionGroups : boundaryActionGroups;
155
+ }
156
+
16
157
  function stepMeta(step: AppiumRecordedStep) {
17
158
  if (step.type === 'delay') return `${step.timeoutMs || 1000}ms`;
18
- if (step.type === 'input') return `输入内容:${step.value || '空'}`;
159
+ if (step.type === 'input' || step.type === 'inputIfExists') return `输入内容:${step.value || '空'}`;
160
+ if (step.contextSelector && step.selector) {
161
+ return `父级 ${step.contextSelector.strategy} ${step.contextSelector.value || ''} + 子级 ${step.selector.strategy} ${step.selector.value || ''}`;
162
+ }
19
163
  if (step.type === 'key') return `keyCode ${step.keyCode || ''}`;
20
164
  if (step.type === 'waitActivity') return step.value || '';
21
165
  if (step.type === 'launchApp') return step.value || '';
166
+ if (step.type === 'runScript') return step.value ? `脚本 ${step.value}` : '';
22
167
  if (step.type === 'screenshot') return '保存当前截图';
23
168
  if (step.type === 'swipe') {
24
169
  const swipe = step.swipe;
@@ -30,50 +175,767 @@ function stepMeta(step: AppiumRecordedStep) {
30
175
  if (step.type === 'assertText') return step.value || '';
31
176
  return `${step.selector?.strategy || ''} ${step.selector?.value || ''}`;
32
177
  }
178
+
179
+ function defaultKind(step: AppiumRecordedStep): FlowKind {
180
+ if (step.flow?.nodeKind) return step.flow.nodeKind;
181
+ if (step.type === 'assertExists' || step.type === 'assertText') return 'assertion';
182
+ return 'action';
183
+ }
184
+
185
+ function kindLabel(kind: FlowKind) {
186
+ return {
187
+ action: '操作',
188
+ condition: '判断',
189
+ assertion: '校验',
190
+ }[kind];
191
+ }
192
+
193
+ function typeLabel(step: AppiumRecordedStep) {
194
+ const labelMap: Partial<Record<AppiumRecordedStep['type'], string>> = {
195
+ tap: '点击',
196
+ input: '输入',
197
+ tapIfExists: '存在则点击',
198
+ inputIfExists: '存在则输入',
199
+ clearIfExists: '存在则清空',
200
+ backIfExists: '存在则返回',
201
+ clearInput: '清空',
202
+ waitFor: '等待出现',
203
+ waitDisappear: '等待消失',
204
+ assertExists: '断言存在',
205
+ assertText: '断言文本',
206
+ key: '按键',
207
+ waitActivity: '等待 Activity',
208
+ delay: '延时',
209
+ coordinateTap: '坐标点击',
210
+ swipe: '滑动',
211
+ screenshot: '截图',
212
+ launchApp: '启动 APP',
213
+ longPress: '长按',
214
+ pinch: '双指缩放',
215
+ runScript: '连接脚本',
216
+ };
217
+ return labelMap[step.type] || step.type;
218
+ }
219
+
220
+ function targetLabel(id?: string) {
221
+ if (!id) return '结束';
222
+ const index = props.steps.findIndex((step) => step.id === id);
223
+ if (index < 0) return '未找到目标';
224
+ return `${index + 1}. ${props.steps[index].label}`;
225
+ }
226
+
227
+ function targetStep(id?: string) {
228
+ if (!id) return null;
229
+ const index = props.steps.findIndex((step) => step.id === id);
230
+ if (index < 0) return null;
231
+ return { step: props.steps[index], index };
232
+ }
233
+
234
+ function branchStepItems(step: AppiumRecordedStep, branch: BranchName) {
235
+ const ownedItems = stepItems.value.filter((item) => (
236
+ item.step.flow?.parentConditionId === step.id
237
+ && item.step.flow?.parentBranch === branch
238
+ ));
239
+ if (ownedItems.length) return ownedItems;
240
+
241
+ const targetId = branch === 'yes' ? step.flow?.yesTargetId : step.flow?.noTargetId;
242
+ const target = targetStep(targetId);
243
+ return target ? [target] : [];
244
+ }
245
+
246
+ function nextStepId(index: number) {
247
+ return props.steps[index + 1]?.id || '';
248
+ }
249
+
250
+ function toggleExpanded(index: number) {
251
+ expandedStepIndex.value = expandedStepIndex.value === index ? null : index;
252
+ }
253
+
254
+ function patchStep(index: number, patch: Partial<AppiumRecordedStep>) {
255
+ const current = props.steps[index];
256
+ if (!current) return;
257
+ emit('updateStep', index, { ...current, ...patch });
258
+ }
259
+
260
+ function patchFlow(index: number, patch: NonNullable<AppiumRecordedStep['flow']>) {
261
+ const current = props.steps[index];
262
+ if (!current) return;
263
+ emit('updateStep', index, {
264
+ ...current,
265
+ flow: {
266
+ ...(current.flow || {}),
267
+ ...patch,
268
+ },
269
+ });
270
+ }
271
+
272
+ function insertAction(index: number, command: string | number | object) {
273
+ emit('insertAction', index, command as InsertAction);
274
+ }
275
+
276
+ function insertBranchAction(index: number, branch: BranchName, command: string | number | object) {
277
+ emit('insertBranchAction', index, branch, command as InsertAction);
278
+ }
279
+
280
+ function isInsertActionDisabled(action: InsertAction) {
281
+ return Boolean(props.disabled && !props.allowedLockedActions?.includes(action));
282
+ }
283
+
284
+ function canOpenInsertMenu() {
285
+ return !props.disabled || Boolean(props.allowedLockedActions?.length);
286
+ }
287
+
288
+ function isInteractiveTarget(target: EventTarget | null) {
289
+ return target instanceof Element && Boolean(target.closest(
290
+ 'button,input,textarea,select,.el-select,.el-dropdown,.el-input,.el-input-number,.appium-flow-node',
291
+ ));
292
+ }
293
+
294
+ function resetFlowView() {
295
+ flowPan.x = 0;
296
+ flowPan.y = 0;
297
+ flowScale.value = 1;
298
+ }
299
+
300
+ function handleFlowWheel(event: WheelEvent) {
301
+ if (!event.ctrlKey) return;
302
+ event.preventDefault();
303
+ const canvas = event.currentTarget as HTMLElement;
304
+ const bounds = canvas.getBoundingClientRect();
305
+ const cursorX = event.clientX - bounds.left;
306
+ const cursorY = event.clientY - bounds.top;
307
+ const previousScale = flowScale.value;
308
+ const direction = event.deltaY < 0 ? 1 : -1;
309
+ const nextScale = Math.min(2, Math.max(0.5, Math.round((previousScale + direction * 0.1) * 10) / 10));
310
+ if (nextScale === previousScale) return;
311
+ flowPan.x = cursorX - ((cursorX - flowPan.x) / previousScale) * nextScale;
312
+ flowPan.y = cursorY - ((cursorY - flowPan.y) / previousScale) * nextScale;
313
+ flowScale.value = nextScale;
314
+ }
315
+
316
+ function handleNodeClick(index: number) {
317
+ toggleExpanded(index);
318
+ }
319
+
320
+ function startFlowDrag(event: PointerEvent) {
321
+ if (isInteractiveTarget(event.target)) return;
322
+ flowDragging.value = true;
323
+ flowDragMoved.value = false;
324
+ dragStart = {
325
+ pointerId: event.pointerId,
326
+ x: event.clientX,
327
+ y: event.clientY,
328
+ panX: flowPan.x,
329
+ panY: flowPan.y,
330
+ };
331
+ (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
332
+ }
333
+
334
+ function moveFlowDrag(event: PointerEvent) {
335
+ if (!flowDragging.value || event.pointerId !== dragStart.pointerId) return;
336
+ if (Math.abs(event.clientX - dragStart.x) + Math.abs(event.clientY - dragStart.y) > 4) {
337
+ flowDragMoved.value = true;
338
+ }
339
+ flowPan.x = dragStart.panX + event.clientX - dragStart.x;
340
+ flowPan.y = dragStart.panY + event.clientY - dragStart.y;
341
+ }
342
+
343
+ function stopFlowDrag(event: PointerEvent) {
344
+ if (!flowDragging.value || event.pointerId !== dragStart.pointerId) return;
345
+ flowDragging.value = false;
346
+ const target = event.currentTarget as HTMLElement;
347
+ if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId);
348
+ window.setTimeout(() => {
349
+ flowDragMoved.value = false;
350
+ }, 0);
351
+ }
352
+
353
+ watch(() => props.steps.length, () => {
354
+ resetFlowView();
355
+ });
33
356
  </script>
34
357
 
35
358
  <template>
36
359
  <div class="appium-recorded-steps-panel">
37
- <div class="appium-recorded-steps">
38
- <el-empty v-if="!steps.length" description="暂无录制步骤" />
360
+ <div class="appium-flow-toolbar">
361
+ <el-button
362
+ size="small"
363
+ :icon="FullScreen"
364
+ :disabled="!steps.length"
365
+ @click="flowDialogVisible = true"
366
+ >
367
+ 放大
368
+ </el-button>
369
+ </div>
370
+ <div
371
+ class="appium-flow-canvas"
372
+ :class="{ 'appium-flow-canvas--dragging': flowDragging }"
373
+ @pointerdown="startFlowDrag"
374
+ @pointermove="moveFlowDrag"
375
+ @pointerup="stopFlowDrag"
376
+ @pointercancel="stopFlowDrag"
377
+ @pointerleave="stopFlowDrag"
378
+ @wheel="handleFlowWheel"
379
+ >
39
380
  <div
40
- v-for="(step, index) in steps"
41
- v-else
42
- :key="step.id"
43
- class="appium-step-item"
381
+ class="appium-flow-content"
382
+ :style="{ transform: `translate(${flowPan.x}px, ${flowPan.y}px) scale(${flowScale})` }"
44
383
  >
384
+ <div class="appium-flow-boundary appium-flow-boundary--pre">
385
+ <span class="appium-flow-boundary__label">前置操作</span>
386
+ <el-dropdown
387
+ trigger="click"
388
+ :disabled="!canOpenInsertMenu()"
389
+ max-height="320px"
390
+ popper-class="appium-action-dropdown"
391
+ @command="insertAction(-2, $event)"
392
+ >
393
+ <button
394
+ type="button"
395
+ class="appium-flow-insert"
396
+ :disabled="!canOpenInsertMenu()"
397
+ >
398
+ <el-icon><Timer /></el-icon>
399
+ <span>添加前置操作</span>
400
+ </button>
401
+ <template #dropdown>
402
+ <el-dropdown-menu>
403
+ <template v-for="group in boundaryActionGroups" :key="group.title">
404
+ <div class="appium-action-dropdown__group">{{ group.title }}</div>
405
+ <el-dropdown-item
406
+ v-for="action in group.actions"
407
+ :key="action.type"
408
+ :command="action.type"
409
+ :disabled="isInsertActionDisabled(action.type)"
410
+ >
411
+ {{ action.label }}
412
+ </el-dropdown-item>
413
+ </template>
414
+ </el-dropdown-menu>
415
+ </template>
416
+ </el-dropdown>
417
+ </div>
418
+ <div class="appium-flow-start">
419
+ <div class="appium-flow-start__node">开始</div>
420
+ <el-dropdown
421
+ trigger="click"
422
+ :disabled="!canOpenInsertMenu()"
423
+ max-height="320px"
424
+ popper-class="appium-action-dropdown"
425
+ @command="insertAction(-1, $event)"
426
+ >
427
+ <button
428
+ type="button"
429
+ class="appium-flow-insert"
430
+ :disabled="!canOpenInsertMenu()"
431
+ >
432
+ <el-icon><Timer /></el-icon>
433
+ <span>插入操作</span>
434
+ </button>
435
+ <template #dropdown>
436
+ <el-dropdown-menu>
437
+ <template v-for="group in mainActionGroups" :key="group.title">
438
+ <div class="appium-action-dropdown__group">{{ group.title }}</div>
439
+ <el-dropdown-item
440
+ v-for="action in group.actions"
441
+ :key="action.type"
442
+ :command="action.type"
443
+ :disabled="isInsertActionDisabled(action.type)"
444
+ >
445
+ {{ action.label }}
446
+ </el-dropdown-item>
447
+ </template>
448
+ </el-dropdown-menu>
449
+ </template>
450
+ </el-dropdown>
451
+ </div>
45
452
  <div
46
- class="appium-step-row"
47
- :class="{ 'appium-step-row--delay': step.type === 'delay' }"
453
+ v-for="{ step, index } in visibleStepItems"
454
+ :key="step.id"
455
+ class="appium-flow-item"
456
+ :class="[
457
+ `appium-flow-item--${stepScope(step)}`,
458
+ { 'appium-flow-item--condition': defaultKind(step) === 'condition' },
459
+ ]"
48
460
  >
49
- <span class="appium-step-row__index">{{ index + 1 }}</span>
50
- <div class="appium-step-row__main">
51
- <strong :title="step.label">{{ step.label }}</strong>
52
- <small :title="stepMeta(step)">{{ stepMeta(step) }}</small>
461
+ <div
462
+ class="appium-flow-node"
463
+ :class="[
464
+ `appium-flow-node--${defaultKind(step)}`,
465
+ { 'appium-flow-node--expanded': expandedStepIndex === index },
466
+ ]"
467
+ >
468
+ <button
469
+ type="button"
470
+ class="appium-flow-node__toggle"
471
+ :aria-expanded="expandedStepIndex === index"
472
+ @click.stop="handleNodeClick(index)"
473
+ >
474
+ <span class="appium-flow-node__index">{{ index + 1 }}</span>
475
+ <span class="appium-flow-node__body">
476
+ <span class="appium-flow-node__title" :title="step.label">{{ step.label }}</span>
477
+ <span class="appium-flow-node__meta" :title="stepMeta(step)">
478
+ {{ kindLabel(defaultKind(step)) }} · {{ typeLabel(step) }}
479
+ <template v-if="step.optional"> · 可选</template>
480
+ <template v-if="stepMeta(step)"> · {{ stepMeta(step) }}</template>
481
+ </span>
482
+ </span>
483
+ </button>
484
+ <span class="appium-flow-node__actions" @click.stop>
485
+ <el-button
486
+ v-if="step.type === 'input' || step.type === 'inputIfExists'"
487
+ text
488
+ size="small"
489
+ :icon="Edit"
490
+ :disabled="disabled"
491
+ title="修改输入内容"
492
+ @click="$emit('editInput', index)"
493
+ />
494
+ <el-button
495
+ text
496
+ size="small"
497
+ :icon="Delete"
498
+ :disabled="disabled"
499
+ title="删除节点"
500
+ @click="$emit('remove', index)"
501
+ />
502
+ </span>
503
+ </div>
504
+
505
+ <div v-if="defaultKind(step) === 'condition'" class="appium-flow-branches">
506
+ <svg class="appium-flow-branch-lines" viewBox="0 0 100 48" preserveAspectRatio="none" aria-hidden="true">
507
+ <path d="M50 0 C50 16 25 16 25 31 L25 48" />
508
+ <path d="M50 0 C50 16 75 16 75 31 L75 48" />
509
+ </svg>
510
+ <div class="appium-flow-branch appium-flow-branch--yes">
511
+ <strong class="appium-flow-branch__label">是</strong>
512
+ <div v-if="branchStepItems(step, 'yes').length" class="appium-flow-branch-steps">
513
+ <div
514
+ v-for="item in branchStepItems(step, 'yes')"
515
+ :key="item.step.id"
516
+ class="appium-flow-branch-step"
517
+ >
518
+ <span class="appium-flow-branch-step__index">
519
+ {{ item.index + 1 }}
520
+ </span>
521
+ <span class="appium-flow-branch-step__body">
522
+ <span class="appium-flow-branch-step__title">
523
+ {{ item.step.label }}
524
+ </span>
525
+ <span class="appium-flow-branch-step__meta">
526
+ {{ kindLabel(defaultKind(item.step)) }} · {{ typeLabel(item.step) }}
527
+ </span>
528
+ </span>
529
+ <el-button
530
+ class="appium-flow-branch-step__delete"
531
+ text
532
+ size="small"
533
+ :icon="Delete"
534
+ :disabled="disabled"
535
+ title="删除节点"
536
+ @click.stop="$emit('remove', item.index)"
537
+ />
538
+ </div>
539
+ </div>
540
+ <span v-else class="appium-flow-branch__target">{{ targetLabel(step.flow?.yesTargetId) }}</span>
541
+ <el-dropdown
542
+ trigger="click"
543
+ :disabled="!canOpenInsertMenu()"
544
+ max-height="320px"
545
+ popper-class="appium-action-dropdown"
546
+ @command="insertBranchAction(index, 'yes', $event)"
547
+ >
548
+ <el-button
549
+ size="small"
550
+ class="appium-flow-branch__insert"
551
+ :disabled="!canOpenInsertMenu()"
552
+ :icon="Timer"
553
+ @click.stop
554
+ >
555
+ 插入操作
556
+ </el-button>
557
+ <template #dropdown>
558
+ <el-dropdown-menu>
559
+ <template v-for="group in mainActionGroups" :key="group.title">
560
+ <div class="appium-action-dropdown__group">{{ group.title }}</div>
561
+ <el-dropdown-item
562
+ v-for="action in group.actions"
563
+ :key="action.type"
564
+ :command="action.type"
565
+ :disabled="isInsertActionDisabled(action.type)"
566
+ >
567
+ {{ action.label }}
568
+ </el-dropdown-item>
569
+ </template>
570
+ </el-dropdown-menu>
571
+ </template>
572
+ </el-dropdown>
53
573
  </div>
54
- <div class="appium-step-row__actions">
55
- <el-button
56
- v-if="step.type === 'input'"
57
- text
58
- size="small"
59
- :icon="Edit"
60
- :disabled="disabled"
61
- title="修改输入内容"
62
- @click="$emit('editInput', index)"
63
- />
64
- <el-button text size="small" :icon="Delete" :disabled="disabled" @click="$emit('remove', index)" />
574
+ <div class="appium-flow-branch appium-flow-branch--no">
575
+ <strong class="appium-flow-branch__label">否</strong>
576
+ <div v-if="branchStepItems(step, 'no').length" class="appium-flow-branch-steps">
577
+ <div
578
+ v-for="item in branchStepItems(step, 'no')"
579
+ :key="item.step.id"
580
+ class="appium-flow-branch-step"
581
+ >
582
+ <span class="appium-flow-branch-step__index">
583
+ {{ item.index + 1 }}
584
+ </span>
585
+ <span class="appium-flow-branch-step__body">
586
+ <span class="appium-flow-branch-step__title">
587
+ {{ item.step.label }}
588
+ </span>
589
+ <span class="appium-flow-branch-step__meta">
590
+ {{ kindLabel(defaultKind(item.step)) }} · {{ typeLabel(item.step) }}
591
+ </span>
592
+ </span>
593
+ <el-button
594
+ class="appium-flow-branch-step__delete"
595
+ text
596
+ size="small"
597
+ :icon="Delete"
598
+ :disabled="disabled"
599
+ title="删除节点"
600
+ @click.stop="$emit('remove', item.index)"
601
+ />
602
+ </div>
603
+ </div>
604
+ <span v-else class="appium-flow-branch__target">{{ targetLabel(step.flow?.noTargetId) }}</span>
605
+ <el-dropdown
606
+ trigger="click"
607
+ :disabled="!canOpenInsertMenu()"
608
+ max-height="320px"
609
+ popper-class="appium-action-dropdown"
610
+ @command="insertBranchAction(index, 'no', $event)"
611
+ >
612
+ <el-button
613
+ size="small"
614
+ class="appium-flow-branch__insert"
615
+ :disabled="!canOpenInsertMenu()"
616
+ :icon="Timer"
617
+ @click.stop
618
+ >
619
+ 插入操作
620
+ </el-button>
621
+ <template #dropdown>
622
+ <el-dropdown-menu>
623
+ <template v-for="group in mainActionGroups" :key="group.title">
624
+ <div class="appium-action-dropdown__group">{{ group.title }}</div>
625
+ <el-dropdown-item
626
+ v-for="action in group.actions"
627
+ :key="action.type"
628
+ :command="action.type"
629
+ :disabled="isInsertActionDisabled(action.type)"
630
+ >
631
+ {{ action.label }}
632
+ </el-dropdown-item>
633
+ </template>
634
+ </el-dropdown-menu>
635
+ </template>
636
+ </el-dropdown>
65
637
  </div>
66
638
  </div>
67
- <button
68
- type="button"
69
- class="appium-step-insert-delay"
70
- :disabled="disabled"
71
- @click="$emit('addDelay', index)"
639
+
640
+ <div v-if="expandedStepIndex === index" class="appium-flow-editor">
641
+ <el-form label-position="top" size="small">
642
+ <el-form-item label="节点名称">
643
+ <el-input
644
+ :model-value="step.label"
645
+ :disabled="disabled"
646
+ @update:model-value="patchStep(index, { label: String($event) })"
647
+ />
648
+ </el-form-item>
649
+ <div class="appium-flow-editor__grid">
650
+ <el-form-item label="节点类型">
651
+ <el-select
652
+ :model-value="defaultKind(step)"
653
+ :disabled="disabled"
654
+ @update:model-value="patchFlow(index, { nodeKind: $event as FlowKind })"
655
+ >
656
+ <el-option label="操作" value="action" />
657
+ <el-option label="判断" value="condition" />
658
+ <el-option label="校验" value="assertion" />
659
+ </el-select>
660
+ </el-form-item>
661
+ <el-form-item label="超时时间 ms">
662
+ <el-input-number
663
+ :model-value="step.timeoutMs || undefined"
664
+ :disabled="disabled"
665
+ :min="0"
666
+ :max="999999"
667
+ controls-position="right"
668
+ @update:model-value="patchStep(index, { timeoutMs: Number($event || 0) || undefined })"
669
+ />
670
+ </el-form-item>
671
+ </div>
672
+ <el-form-item v-if="step.type === 'input' || step.type === 'inputIfExists' || step.type === 'assertText'" label="文本内容">
673
+ <el-input
674
+ :model-value="step.value || ''"
675
+ :disabled="disabled"
676
+ @update:model-value="patchStep(index, { value: String($event) })"
677
+ />
678
+ </el-form-item>
679
+ <div v-if="step.selector" class="appium-flow-editor__grid">
680
+ <el-form-item label="Selector 类型">
681
+ <el-select
682
+ :model-value="step.selector.strategy"
683
+ :disabled="disabled"
684
+ @update:model-value="patchStep(index, { selector: { ...step.selector!, strategy: $event as AppiumSelector['strategy'] } })"
685
+ >
686
+ <el-option label="accessibility id" value="accessibilityId" />
687
+ <el-option label="id" value="id" />
688
+ <el-option label="android uiAutomator" value="androidUiAutomator" />
689
+ <el-option label="xpath" value="xpath" />
690
+ </el-select>
691
+ </el-form-item>
692
+ <el-form-item label="Selector 值">
693
+ <el-input
694
+ :model-value="step.selector.value || ''"
695
+ :disabled="disabled"
696
+ @update:model-value="patchStep(index, { selector: { ...step.selector!, value: String($event) } })"
697
+ />
698
+ </el-form-item>
699
+ </div>
700
+ <div v-if="step.selector" class="appium-flow-editor__grid">
701
+ <el-form-item label="父级上下文类型">
702
+ <el-select
703
+ :model-value="step.contextSelector?.strategy || ''"
704
+ :disabled="disabled"
705
+ clearable
706
+ placeholder="不使用父级"
707
+ @update:model-value="patchStep(index, { contextSelector: $event ? { ...(step.contextSelector || { value: '' }), strategy: $event as AppiumSelector['strategy'] } : undefined })"
708
+ >
709
+ <el-option label="accessibility id" value="accessibilityId" />
710
+ <el-option label="id" value="id" />
711
+ <el-option label="android uiAutomator" value="androidUiAutomator" />
712
+ <el-option label="xpath" value="xpath" />
713
+ </el-select>
714
+ </el-form-item>
715
+ <el-form-item label="父级上下文值">
716
+ <el-input
717
+ :model-value="step.contextSelector?.value || ''"
718
+ :disabled="disabled || !step.contextSelector"
719
+ @update:model-value="patchStep(index, { contextSelector: { ...(step.contextSelector || { strategy: 'xpath' }), value: String($event) } })"
720
+ />
721
+ </el-form-item>
722
+ </div>
723
+ <el-form-item label="可选步骤">
724
+ <el-switch
725
+ :model-value="Boolean(step.optional)"
726
+ :disabled="disabled"
727
+ active-text="找不到时跳过"
728
+ inactive-text="找不到时失败"
729
+ @update:model-value="patchStep(index, { optional: Boolean($event) })"
730
+ />
731
+ </el-form-item>
732
+ <template v-if="defaultKind(step) === 'condition'">
733
+ <div class="appium-flow-editor__grid">
734
+ <el-form-item label="是,进入">
735
+ <el-select
736
+ :model-value="step.flow?.yesTargetId || ''"
737
+ :disabled="disabled"
738
+ clearable
739
+ placeholder="默认下一步"
740
+ @update:model-value="patchFlow(index, { yesTargetId: String($event || '') })"
741
+ >
742
+ <el-option v-for="option in scopedStepOptions(step)" :key="option.value" :label="option.label" :value="option.value" />
743
+ </el-select>
744
+ </el-form-item>
745
+ <el-form-item label="否,进入">
746
+ <el-select
747
+ :model-value="step.flow?.noTargetId || ''"
748
+ :disabled="disabled"
749
+ clearable
750
+ placeholder="默认结束"
751
+ @update:model-value="patchFlow(index, { noTargetId: String($event || '') })"
752
+ >
753
+ <el-option v-for="option in scopedStepOptions(step)" :key="option.value" :label="option.label" :value="option.value" />
754
+ </el-select>
755
+ </el-form-item>
756
+ </div>
757
+ </template>
758
+ <div class="appium-flow-editor__actions">
759
+ <el-button :icon="Timer" :disabled="disabled" @click="$emit('addDelay', index)">
760
+ 插入延时
761
+ </el-button>
762
+ <el-button
763
+ v-if="defaultKind(step) !== 'condition' && stepScope(step) === 'main'"
764
+ :disabled="disabled"
765
+ @click="patchFlow(index, { nodeKind: 'condition', yesTargetId: '', noTargetId: '' })"
766
+ >
767
+ 转为判断节点
768
+ </el-button>
769
+ </div>
770
+ </el-form>
771
+ </div>
772
+
773
+ <el-dropdown
774
+ v-if="defaultKind(step) !== 'condition'"
775
+ trigger="click"
776
+ :disabled="!canOpenInsertMenu()"
777
+ max-height="320px"
778
+ popper-class="appium-action-dropdown"
779
+ @command="insertAction(index, $event)"
72
780
  >
73
- <el-icon><Timer /></el-icon>
74
- <span>在此步骤后插入延时</span>
75
- </button>
781
+ <button
782
+ type="button"
783
+ class="appium-flow-insert"
784
+ :disabled="!canOpenInsertMenu()"
785
+ >
786
+ <el-icon><Timer /></el-icon>
787
+ <span>插入操作</span>
788
+ </button>
789
+ <template #dropdown>
790
+ <el-dropdown-menu>
791
+ <template v-for="group in actionGroupsForStep(step)" :key="group.title">
792
+ <div class="appium-action-dropdown__group">{{ group.title }}</div>
793
+ <el-dropdown-item
794
+ v-for="action in group.actions"
795
+ :key="action.type"
796
+ :command="action.type"
797
+ :disabled="isInsertActionDisabled(action.type)"
798
+ >
799
+ {{ action.label }}
800
+ </el-dropdown-item>
801
+ </template>
802
+ </el-dropdown-menu>
803
+ </template>
804
+ </el-dropdown>
805
+ </div>
806
+ <div class="appium-flow-end">
807
+ <div class="appium-flow-end__node">结束</div>
808
+ </div>
809
+ <div class="appium-flow-boundary appium-flow-boundary--post">
810
+ <span class="appium-flow-boundary__label">后置操作</span>
811
+ <el-dropdown
812
+ trigger="click"
813
+ :disabled="!canOpenInsertMenu()"
814
+ max-height="320px"
815
+ popper-class="appium-action-dropdown"
816
+ @command="insertAction(-3, $event)"
817
+ >
818
+ <button
819
+ type="button"
820
+ class="appium-flow-insert"
821
+ :disabled="!canOpenInsertMenu()"
822
+ >
823
+ <el-icon><Timer /></el-icon>
824
+ <span>添加后置操作</span>
825
+ </button>
826
+ <template #dropdown>
827
+ <el-dropdown-menu>
828
+ <template v-for="group in boundaryActionGroups" :key="group.title">
829
+ <div class="appium-action-dropdown__group">{{ group.title }}</div>
830
+ <el-dropdown-item
831
+ v-for="action in group.actions"
832
+ :key="action.type"
833
+ :command="action.type"
834
+ :disabled="isInsertActionDisabled(action.type)"
835
+ >
836
+ {{ action.label }}
837
+ </el-dropdown-item>
838
+ </template>
839
+ </el-dropdown-menu>
840
+ </template>
841
+ </el-dropdown>
842
+ </div>
76
843
  </div>
77
844
  </div>
845
+ <el-dialog
846
+ v-model="flowDialogVisible"
847
+ title="流程总览"
848
+ width="86vw"
849
+ class="appium-flow-dialog"
850
+ append-to-body
851
+ >
852
+ <div class="appium-flow-dialog-canvas">
853
+ <div class="appium-flow-content appium-flow-content--overview">
854
+ <div class="appium-flow-boundary appium-flow-boundary--pre">
855
+ <span class="appium-flow-boundary__label">前置操作</span>
856
+ </div>
857
+ <div class="appium-flow-start appium-flow-start--overview">
858
+ <div class="appium-flow-start__node">开始</div>
859
+ </div>
860
+ <div
861
+ v-for="{ step, index } in visibleStepItems"
862
+ :key="step.id"
863
+ class="appium-flow-item"
864
+ :class="[
865
+ `appium-flow-item--${stepScope(step)}`,
866
+ { 'appium-flow-item--condition': defaultKind(step) === 'condition' },
867
+ ]"
868
+ >
869
+ <div
870
+ class="appium-flow-node"
871
+ :class="`appium-flow-node--${defaultKind(step)}`"
872
+ >
873
+ <span class="appium-flow-node__toggle">
874
+ <span class="appium-flow-node__index">{{ index + 1 }}</span>
875
+ <span class="appium-flow-node__body">
876
+ <span class="appium-flow-node__title" :title="step.label">{{ step.label }}</span>
877
+ <span class="appium-flow-node__meta" :title="stepMeta(step)">
878
+ {{ kindLabel(defaultKind(step)) }} · {{ typeLabel(step) }}
879
+ <template v-if="step.optional"> · 可选</template>
880
+ <template v-if="stepMeta(step)"> · {{ stepMeta(step) }}</template>
881
+ </span>
882
+ </span>
883
+ </span>
884
+ </div>
885
+
886
+ <div v-if="defaultKind(step) === 'condition'" class="appium-flow-branches">
887
+ <svg class="appium-flow-branch-lines" viewBox="0 0 100 48" preserveAspectRatio="none" aria-hidden="true">
888
+ <path d="M50 0 C50 16 25 16 25 31 L25 48" />
889
+ <path d="M50 0 C50 16 75 16 75 31 L75 48" />
890
+ </svg>
891
+ <div class="appium-flow-branch appium-flow-branch--yes">
892
+ <strong class="appium-flow-branch__label">是</strong>
893
+ <div v-if="branchStepItems(step, 'yes').length" class="appium-flow-branch-steps">
894
+ <div
895
+ v-for="item in branchStepItems(step, 'yes')"
896
+ :key="item.step.id"
897
+ class="appium-flow-branch-step"
898
+ >
899
+ <span class="appium-flow-branch-step__index">{{ item.index + 1 }}</span>
900
+ <span class="appium-flow-branch-step__body">
901
+ <span class="appium-flow-branch-step__title">{{ item.step.label }}</span>
902
+ <span class="appium-flow-branch-step__meta">
903
+ {{ kindLabel(defaultKind(item.step)) }} · {{ typeLabel(item.step) }}
904
+ </span>
905
+ </span>
906
+ </div>
907
+ </div>
908
+ <span v-else class="appium-flow-branch__target">{{ targetLabel(step.flow?.yesTargetId) }}</span>
909
+ </div>
910
+ <div class="appium-flow-branch appium-flow-branch--no">
911
+ <strong class="appium-flow-branch__label">否</strong>
912
+ <div v-if="branchStepItems(step, 'no').length" class="appium-flow-branch-steps">
913
+ <div
914
+ v-for="item in branchStepItems(step, 'no')"
915
+ :key="item.step.id"
916
+ class="appium-flow-branch-step"
917
+ >
918
+ <span class="appium-flow-branch-step__index">{{ item.index + 1 }}</span>
919
+ <span class="appium-flow-branch-step__body">
920
+ <span class="appium-flow-branch-step__title">{{ item.step.label }}</span>
921
+ <span class="appium-flow-branch-step__meta">
922
+ {{ kindLabel(defaultKind(item.step)) }} · {{ typeLabel(item.step) }}
923
+ </span>
924
+ </span>
925
+ </div>
926
+ </div>
927
+ <span v-else class="appium-flow-branch__target">{{ targetLabel(step.flow?.noTargetId) }}</span>
928
+ </div>
929
+ </div>
930
+ </div>
931
+ <div class="appium-flow-end">
932
+ <div class="appium-flow-end__node">结束</div>
933
+ </div>
934
+ <div class="appium-flow-boundary appium-flow-boundary--post">
935
+ <span class="appium-flow-boundary__label">后置操作</span>
936
+ </div>
937
+ </div>
938
+ </div>
939
+ </el-dialog>
78
940
  </div>
79
941
  </template>