android-midscene-automation 0.1.21 → 0.1.22

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.
@@ -0,0 +1,264 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, onBeforeUnmount, onMounted, shallowRef, watch } from 'vue';
3
+ import { Handle, Position } from '@vue-flow/core';
4
+ import { CopyDocument, Delete, Edit, Plus, VideoPlay, View } from '@element-plus/icons-vue';
5
+ import type { AppiumRecordedStep } from '../types';
6
+ import {
7
+ PASTE_COMMAND,
8
+ type FlowBranch,
9
+ type FlowGraphNodeData,
10
+ type InsertAction,
11
+ } from '../flow-graph';
12
+ import FlowStepEditor from './FlowStepEditor.vue';
13
+
14
+ const props = defineProps<{
15
+ nodeId: string;
16
+ data: FlowGraphNodeData;
17
+ readonly?: boolean;
18
+ }>();
19
+
20
+ const emit = defineEmits<{
21
+ nodeClick: [index: number];
22
+ copy: [index: number];
23
+ remove: [index: number];
24
+ editInput: [index: number];
25
+ previewLinkedScript: [index: number];
26
+ execute: [index: number];
27
+ insert: [payload: { afterIndex: number; action: InsertAction | typeof PASTE_COMMAND; branch?: FlowBranch; conditionIndex?: number }];
28
+ updateStep: [payload: { index: number; step: AppiumRecordedStep }];
29
+ resize: [payload: { id: string; height: number }];
30
+ }>();
31
+
32
+ const rootRef = shallowRef<HTMLElement | null>(null);
33
+ let resizeObserver: ResizeObserver | null = null;
34
+
35
+ const branchLabel = computed(() => {
36
+ if (props.data.kind !== 'branch') return '';
37
+ return props.data.branch === 'yes' ? '是' : '否';
38
+ });
39
+
40
+ const insertTitle = computed(() => {
41
+ if (props.data.kind !== 'insert') return '';
42
+ if (props.data.branch === 'yes') return '在“是”分支插入操作';
43
+ if (props.data.branch === 'no') return '在“否”分支插入操作';
44
+ return props.data.afterIndex < 0 ? '在开始后插入操作' : '插入操作';
45
+ });
46
+
47
+ function emitInsert(command: string | number | object) {
48
+ if (props.readonly || props.data.kind !== 'insert') return;
49
+ emit('insert', {
50
+ afterIndex: props.data.afterIndex,
51
+ action: command === PASTE_COMMAND ? PASTE_COMMAND : command as InsertAction,
52
+ branch: props.data.branch,
53
+ conditionIndex: props.data.conditionIndex,
54
+ });
55
+ }
56
+
57
+ function updateStep(payload: { index: number; step: AppiumRecordedStep }) {
58
+ if (props.readonly) return;
59
+ emit('updateStep', payload);
60
+ }
61
+
62
+ function handleStepClick() {
63
+ if (props.readonly || props.data.kind !== 'step') return;
64
+ emit('nodeClick', props.data.index);
65
+ }
66
+
67
+ function reportSize() {
68
+ if (!rootRef.value || props.data.kind !== 'step') return;
69
+ emit('resize', {
70
+ id: props.nodeId,
71
+ height: rootRef.value.offsetHeight,
72
+ });
73
+ }
74
+
75
+ onMounted(() => {
76
+ resizeObserver = new ResizeObserver(reportSize);
77
+ if (rootRef.value) resizeObserver.observe(rootRef.value);
78
+ void nextTick(reportSize);
79
+ });
80
+
81
+ onBeforeUnmount(() => {
82
+ resizeObserver?.disconnect();
83
+ });
84
+
85
+ watch(() => props.data, () => {
86
+ void nextTick(reportSize);
87
+ });
88
+ </script>
89
+
90
+ <template>
91
+ <div
92
+ ref="rootRef"
93
+ class="appium-flow-graph-node nodrag nopan"
94
+ :class="`appium-flow-graph-node--${data.kind}`"
95
+ @pointerdown.stop
96
+ @mousedown.stop
97
+ @touchstart.stop
98
+ >
99
+ <Handle
100
+ id="top"
101
+ type="target"
102
+ :position="Position.Top"
103
+ class="appium-flow-handle appium-flow-handle--top"
104
+ />
105
+
106
+ <div v-if="data.kind === 'start'" class="appium-flow-start-card nodrag nopan">
107
+ <span class="appium-flow-start-card__icon">S</span>
108
+ <span>
109
+ <strong>开始</strong>
110
+ <small>当前脚本入口</small>
111
+ </span>
112
+ </div>
113
+
114
+ <button
115
+ v-else-if="data.kind === 'branch'"
116
+ type="button"
117
+ class="appium-flow-branch-pill nodrag nopan"
118
+ :class="`appium-flow-branch-pill--${data.branch}`"
119
+ tabindex="-1"
120
+ >
121
+ {{ branchLabel }}
122
+ </button>
123
+
124
+ <span
125
+ v-else-if="data.kind === 'split'"
126
+ class="appium-flow-split-joint"
127
+ aria-hidden="true"
128
+ />
129
+
130
+ <div v-else-if="data.kind === 'insert'" class="appium-flow-insert-node nodrag nopan">
131
+ <span
132
+ v-if="props.readonly"
133
+ class="appium-flow-insert-preview-point"
134
+ aria-hidden="true"
135
+ />
136
+ <el-tooltip v-else content="添加操作" placement="top">
137
+ <el-dropdown
138
+ trigger="click"
139
+ :disabled="!data.canOpenInsertMenu"
140
+ max-height="320px"
141
+ popper-class="appium-action-dropdown"
142
+ @command="emitInsert"
143
+ >
144
+ <el-button
145
+ circle
146
+ type="primary"
147
+ :icon="Plus"
148
+ :disabled="!data.canOpenInsertMenu"
149
+ :aria-label="insertTitle"
150
+ />
151
+ <template #dropdown>
152
+ <el-dropdown-menu>
153
+ <el-dropdown-item
154
+ v-if="data.clipboardCount"
155
+ :command="PASTE_COMMAND"
156
+ divided
157
+ >
158
+ 粘贴 {{ data.clipboardCount }} 个节点
159
+ </el-dropdown-item>
160
+ <template v-for="group in data.actionGroups" :key="group.title">
161
+ <div class="appium-action-dropdown__group">{{ group.title }}</div>
162
+ <el-dropdown-item
163
+ v-for="action in group.actions"
164
+ :key="action.type"
165
+ :command="action.type"
166
+ :disabled="data.isActionDisabled(action.type)"
167
+ >
168
+ {{ action.label }}
169
+ </el-dropdown-item>
170
+ </template>
171
+ </el-dropdown-menu>
172
+ </template>
173
+ </el-dropdown>
174
+ </el-tooltip>
175
+ </div>
176
+
177
+ <div
178
+ v-else
179
+ class="appium-flow-step-shell"
180
+ :class="[
181
+ `appium-flow-step-shell--${data.flowKind}`,
182
+ {
183
+ 'appium-flow-step-shell--expanded': data.expanded,
184
+ 'appium-flow-step-shell--selected': data.selected,
185
+ 'appium-flow-step-shell--copying': data.copyMode,
186
+ 'appium-flow-step-shell--readonly': props.readonly,
187
+ },
188
+ ]"
189
+ >
190
+ <button
191
+ type="button"
192
+ class="appium-flow-step-card nodrag nopan"
193
+ :style="{ minHeight: `${data.cardMinHeight}px` }"
194
+ @click="handleStepClick"
195
+ >
196
+ <span class="appium-flow-step-card__content">
197
+ <strong :title="data.title">{{ data.title }}</strong>
198
+ <small :title="data.meta">{{ data.meta }}</small>
199
+ <em v-if="data.note" :title="data.note">{{ data.note }}</em>
200
+ </span>
201
+ </button>
202
+ <span v-if="!props.readonly" class="appium-flow-step-card__actions nodrag nopan" @click.stop>
203
+ <el-button
204
+ v-if="!data.copyMode && data.canCopy"
205
+ text
206
+ size="small"
207
+ :icon="CopyDocument"
208
+ title="复制节点"
209
+ @click="emit('copy', data.index)"
210
+ />
211
+ <el-button
212
+ v-if="data.canExecute"
213
+ text
214
+ size="small"
215
+ :icon="VideoPlay"
216
+ :loading="data.launching"
217
+ :disabled="data.disabled"
218
+ title="立即执行"
219
+ @click="emit('execute', data.index)"
220
+ />
221
+ <el-button
222
+ v-if="data.canEditInput"
223
+ text
224
+ size="small"
225
+ :icon="Edit"
226
+ :disabled="data.disabled"
227
+ title="修改输入内容"
228
+ @click="emit('editInput', data.index)"
229
+ />
230
+ <el-button
231
+ v-if="data.step.type === 'runScript'"
232
+ text
233
+ size="small"
234
+ :icon="View"
235
+ title="预览连接脚本"
236
+ @click="emit('previewLinkedScript', data.index)"
237
+ />
238
+ <el-button
239
+ text
240
+ size="small"
241
+ :icon="Delete"
242
+ :disabled="data.removeDisabled"
243
+ title="删除节点"
244
+ @click="emit('remove', data.index)"
245
+ />
246
+ </span>
247
+ <FlowStepEditor
248
+ v-if="!props.readonly && data.expanded"
249
+ class="appium-flow-step-editor nodrag nopan"
250
+ :step="data.step"
251
+ :index="data.index"
252
+ :disabled="data.disabled"
253
+ @update="updateStep"
254
+ />
255
+ </div>
256
+
257
+ <Handle
258
+ id="bottom"
259
+ type="source"
260
+ :position="Position.Bottom"
261
+ class="appium-flow-handle appium-flow-handle--bottom"
262
+ />
263
+ </div>
264
+ </template>
@@ -0,0 +1,67 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+ import { BaseEdge, type EdgeProps } from '@vue-flow/core';
4
+ import type { FlowGraphNodeData } from '../flow-graph';
5
+
6
+ const props = defineProps<EdgeProps>();
7
+
8
+ const targetData = computed(() => props.targetNode.data as FlowGraphNodeData | undefined);
9
+
10
+ type Point = {
11
+ x: number;
12
+ y: number;
13
+ };
14
+
15
+ function linePath(points: Point[]) {
16
+ if (points.length < 2) return '';
17
+ const [start, ...rest] = points;
18
+ return [
19
+ `M ${start.x} ${start.y}`,
20
+ ...rest.map((point) => `L ${point.x} ${point.y}`),
21
+ ].join(' ');
22
+ }
23
+
24
+ const path = computed(() => {
25
+ const source = { x: props.sourceX, y: props.sourceY };
26
+ const target = { x: props.targetX, y: props.targetY };
27
+ const deltaX = target.x - source.x;
28
+ const deltaY = target.y - source.y;
29
+
30
+ if (targetData.value?.kind === 'branch') {
31
+ const direction = deltaY >= 0 ? 1 : -1;
32
+ const branchY = source.y + direction * Math.min(Math.max(Math.abs(deltaY) * 0.45, 18), 34);
33
+ return linePath([
34
+ source,
35
+ { x: source.x, y: branchY },
36
+ { x: target.x, y: branchY },
37
+ target,
38
+ ]);
39
+ }
40
+
41
+ if (Math.abs(deltaX) < 2) {
42
+ return `M ${source.x} ${source.y} L ${target.x} ${target.y}`;
43
+ }
44
+
45
+ const verticalGap = Math.min(Math.max(Math.abs(deltaY) * 0.38, 28), 56);
46
+ const splitY = deltaY >= 0
47
+ ? source.y + verticalGap
48
+ : source.y - verticalGap;
49
+
50
+ return linePath([
51
+ source,
52
+ { x: source.x, y: splitY },
53
+ { x: target.x, y: splitY },
54
+ target,
55
+ ]);
56
+ });
57
+ </script>
58
+
59
+ <template>
60
+ <BaseEdge
61
+ :id="id"
62
+ :path="path"
63
+ :marker-start="markerStart"
64
+ :marker-end="markerEnd"
65
+ :interaction-width="interactionWidth"
66
+ />
67
+ </template>
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from 'vue';
3
- import { CopyDocument, Delete, Edit, Link, Timer } from '@element-plus/icons-vue';
3
+ import { CopyDocument, Delete, Edit, Timer } from '@element-plus/icons-vue';
4
4
  import type { AppiumRecordedStep } from '../types';
5
5
  import { buildConditionLayouts, FLOW_BRANCH_GAP, FLOW_NODE_WIDTH } from '../flow-layout';
6
6
  import FlowStepEditor from './FlowStepEditor.vue';
@@ -21,6 +21,7 @@ type InsertAction =
21
21
  | 'swipe'
22
22
  | 'pinch'
23
23
  | 'launchApp'
24
+ | 'clearAppData'
24
25
  | 'popupCondition'
25
26
  | 'tapIfExists'
26
27
  | 'inputIfExists'
@@ -43,6 +44,7 @@ const props = defineProps<{
43
44
  copyMode?: boolean;
44
45
  selectedCopyIndexes?: number[];
45
46
  disabled?: boolean;
47
+ removeDisabled?: boolean;
46
48
  allowedLockedActions?: InsertAction[];
47
49
  clipboardCount?: number;
48
50
  }>();
@@ -53,8 +55,6 @@ const emit = defineEmits<{
53
55
  remove: [index: number];
54
56
  paste: [index: number, branch: BranchName];
55
57
  insertBranchAction: [index: number, branch: BranchName, action: InsertAction];
56
- connectNext: [index: number, branch: BranchName];
57
- disconnectNext: [index: number, branch: BranchName];
58
58
  editInput: [index: number];
59
59
  updateStep: [index: number, step: AppiumRecordedStep];
60
60
  }>();
@@ -124,7 +124,7 @@ function typeLabel(step: AppiumRecordedStep) {
124
124
  clearIfExists: '存在则清空', backIfExists: '存在则返回', clearInput: '清空',
125
125
  waitFor: '等待出现', waitDisappear: '等待消失', assertExists: '断言存在',
126
126
  assertText: '断言文本', key: '按键', waitActivity: '等待 Activity', delay: '延时',
127
- coordinateTap: '坐标点击', swipe: '滑动', launchApp: '启动 APP', longPress: '长按',
127
+ coordinateTap: '坐标点击', swipe: '滑动', launchApp: '启动 APP', clearAppData: '清理 APP 缓存', longPress: '长按',
128
128
  pinch: '双指缩放', runScript: '连接脚本', screenshot: '截图',
129
129
  };
130
130
  return labels[step.type] || step.type;
@@ -152,7 +152,12 @@ function branchItems(branch: BranchName) {
152
152
 
153
153
  function branchConnectionTargetId(branch: BranchName) {
154
154
  const items = branchItems(branch);
155
- if (items.length) return items[items.length - 1]?.step.flow?.successTargetId || '';
155
+ if (items.length) {
156
+ const lastItem = items[items.length - 1];
157
+ return defaultKind(lastItem.step) === 'condition'
158
+ ? ''
159
+ : lastItem.step.flow?.successTargetId || '';
160
+ }
156
161
  const targetId = branch === 'yes' ? props.condition.flow?.yesTargetId : props.condition.flow?.noTargetId;
157
162
  const target = props.steps.find((step) => step.id === targetId);
158
163
  return target && !target.flow?.parentConditionId ? target.id : '';
@@ -193,18 +198,19 @@ function insert(branch: BranchName, command: string | number | object) {
193
198
  else emit('insertBranchAction', props.conditionIndex, branch, command as InsertAction);
194
199
  }
195
200
 
196
- function toggleConnection(branch: BranchName) {
197
- if (branchConnectionTargetId(branch)) emit('disconnectNext', props.conditionIndex, branch);
198
- else emit('connectNext', props.conditionIndex, branch);
199
- }
200
-
201
201
  function updateStep(payload: { index: number; step: AppiumRecordedStep }) {
202
202
  emit('updateStep', payload.index, payload.step);
203
203
  }
204
204
 
205
- function branchPath(branch: BranchName) {
206
- const targetAxis = branch === 'yes' ? layout.value.yesAxis : layout.value.noAxis;
207
- return `M50 0 C50 16 ${targetAxis} 16 ${targetAxis} 31 L${targetAxis} 48`;
205
+ function branchSplitPath() {
206
+ const yesMiddle = (50 + layout.value.yesAxis) / 2;
207
+ const noMiddle = (50 + layout.value.noAxis) / 2;
208
+ return [
209
+ `M${layout.value.yesAxis} 48 L${layout.value.yesAxis} 31`,
210
+ `C${layout.value.yesAxis} 16 ${yesMiddle} 0 50 0`,
211
+ `C${noMiddle} 0 ${layout.value.noAxis} 16 ${layout.value.noAxis} 31`,
212
+ `L${layout.value.noAxis} 48`,
213
+ ].join(' ');
208
214
  }
209
215
  </script>
210
216
 
@@ -219,13 +225,14 @@ function branchPath(branch: BranchName) {
219
225
  }"
220
226
  >
221
227
  <svg class="appium-flow-branch-lines" viewBox="0 0 100 48" preserveAspectRatio="none" aria-hidden="true">
222
- <path :d="branchPath('yes')" />
223
- <path :d="branchPath('no')" />
228
+ <path :d="branchSplitPath()" />
224
229
  </svg>
225
230
  <div
226
231
  v-for="branch in (['yes', 'no'] as const)"
227
232
  :key="branch"
228
233
  class="appium-flow-branch"
234
+ :data-flow-condition-id="condition.id"
235
+ :data-flow-branch="branch"
229
236
  :class="[`appium-flow-branch--${branch}`, { 'appium-flow-branch--connected': branchConnectionTargetId(branch) }]"
230
237
  >
231
238
  <svg class="appium-flow-line appium-flow-branch__line" viewBox="0 0 10 100" preserveAspectRatio="none" aria-hidden="true">
@@ -282,7 +289,7 @@ function branchPath(branch: BranchName) {
282
289
  text
283
290
  size="small"
284
291
  :icon="Delete"
285
- :disabled="disabled"
292
+ :disabled="removeDisabled"
286
293
  title="删除节点"
287
294
  @click="$emit('remove', item.index)"
288
295
  />
@@ -304,6 +311,7 @@ function branchPath(branch: BranchName) {
304
311
  :copy-mode="copyMode"
305
312
  :selected-copy-indexes="selectedCopyIndexes"
306
313
  :disabled="disabled"
314
+ :remove-disabled="removeDisabled"
307
315
  :allowed-locked-actions="allowedLockedActions"
308
316
  :clipboard-count="clipboardCount"
309
317
  @node-click="$emit('nodeClick', $event)"
@@ -311,8 +319,6 @@ function branchPath(branch: BranchName) {
311
319
  @remove="$emit('remove', $event)"
312
320
  @paste="(index, childBranch) => $emit('paste', index, childBranch)"
313
321
  @insert-branch-action="(index, childBranch, action) => $emit('insertBranchAction', index, childBranch, action)"
314
- @connect-next="(index, childBranch) => $emit('connectNext', index, childBranch)"
315
- @disconnect-next="(index, childBranch) => $emit('disconnectNext', index, childBranch)"
316
322
  @edit-input="$emit('editInput', $event)"
317
323
  @update-step="(index, step) => $emit('updateStep', index, step)"
318
324
  />
@@ -355,17 +361,6 @@ function branchPath(branch: BranchName) {
355
361
  </el-dropdown-menu>
356
362
  </template>
357
363
  </el-dropdown>
358
- <el-button
359
- v-if="branchConnectionTargetId(branch)"
360
- size="small"
361
- class="appium-flow-branch__connect"
362
- :icon="Link"
363
- :disabled="disabled"
364
- title="取消当前分支与后续流程的连接"
365
- @click.stop="toggleConnection(branch)"
366
- >
367
- 取消连接
368
- </el-button>
369
364
  </div>
370
365
  </div>
371
366
  </div>