android-midscene-automation 0.1.28 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +13 -0
- package/package.json +1 -1
- package/server/appium-recorder/appium-runner.ts +15 -2
- package/server/appium-recorder/open-gallery.ts +52 -0
- package/server/appium-recorder/repository.ts +3 -0
- package/server/config.ts +6 -1
- package/src/App.vue +6 -0
- package/src/appium-recorder/AppiumPage.vue +40 -13
- package/src/appium-recorder/components/FlowCanvas.vue +11 -2
- package/src/appium-recorder/components/FlowNodeCard.vue +29 -6
- package/src/appium-recorder/components/FlowRoundedEdge.vue +6 -0
- package/src/appium-recorder/components/FlowStepEditor.vue +2 -2
- package/src/appium-recorder/components/MergeBranchesDialog.vue +74 -0
- package/src/appium-recorder/components/NestedConditionBranches.vue +18 -4
- package/src/appium-recorder/components/NodeDetail.vue +22 -4
- package/src/appium-recorder/components/RecordedSteps.vue +99 -42
- package/src/appium-recorder/flow-appearance.ts +15 -0
- package/src/appium-recorder/flow-graph.ts +31 -3
- package/src/appium-recorder/flow-labels.ts +4 -1
- package/src/appium-recorder/flow-merge.ts +110 -0
- package/src/appium-recorder/flow-remove.ts +6 -0
- package/src/appium-recorder/native-control-state.ts +10 -3
- package/src/appium-recorder/types.ts +3 -0
- package/src/components/config/AppiumConfigPanel.vue +4 -1
- package/src/components/config/FlowAppearanceSettings.vue +51 -0
- package/src/style.css +2 -2
- package/src/types.ts +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue';
|
|
3
|
+
import { ElMessage } from 'element-plus';
|
|
4
|
+
import type { AppiumRecordedStep } from '../types';
|
|
5
|
+
import { branchSteps, mergeBranches } from '../flow-merge';
|
|
6
|
+
import { flowBranchLabel } from '../flow-labels';
|
|
7
|
+
|
|
8
|
+
const props = defineProps<{ steps: AppiumRecordedStep[]; conditionId: string; disabled?: boolean }>();
|
|
9
|
+
const emit = defineEmits<{ close: []; confirm: [steps: AppiumRecordedStep[]] }>();
|
|
10
|
+
const commonId = ref('');
|
|
11
|
+
const duplicateId = ref('');
|
|
12
|
+
const confirmed = ref(false);
|
|
13
|
+
const retainedSide = ref<'yes' | 'no'>('yes');
|
|
14
|
+
const condition = computed(() => props.steps.find((step) => step.id === props.conditionId));
|
|
15
|
+
const candidates = computed(() => branchSteps(props.steps, props.conditionId, retainedSide.value));
|
|
16
|
+
const duplicates = computed(() => branchSteps(props.steps, props.conditionId, retainedSide.value === 'yes' ? 'no' : 'yes'));
|
|
17
|
+
const preview = computed(() => {
|
|
18
|
+
if (!commonId.value) return undefined;
|
|
19
|
+
try { return mergeBranches(props.steps, props.conditionId, commonId.value, duplicateId.value); }
|
|
20
|
+
catch (error) { return error instanceof Error ? error.message : String(error); }
|
|
21
|
+
});
|
|
22
|
+
watch(() => props.conditionId, () => {
|
|
23
|
+
retainedSide.value = branchSteps(props.steps, props.conditionId, 'yes').length ? 'yes' : 'no';
|
|
24
|
+
commonId.value = ''; duplicateId.value = ''; confirmed.value = false;
|
|
25
|
+
});
|
|
26
|
+
// 切换方向时清空此前的选择和删除确认,避免误删新方向下的节点。
|
|
27
|
+
watch(retainedSide, () => { commonId.value = ''; duplicateId.value = ''; confirmed.value = false; });
|
|
28
|
+
watch(commonId, () => { duplicateId.value = ''; confirmed.value = false; });
|
|
29
|
+
watch(duplicateId, () => { confirmed.value = false; });
|
|
30
|
+
function submit() {
|
|
31
|
+
if (props.disabled || !preview.value || typeof preview.value === 'string') return;
|
|
32
|
+
if (preview.value.removedCount && !confirmed.value) return;
|
|
33
|
+
try {
|
|
34
|
+
emit('confirm', mergeBranches(props.steps, props.conditionId, commonId.value, duplicateId.value).steps);
|
|
35
|
+
emit('close');
|
|
36
|
+
} catch (error) { ElMessage.error(String(error)); }
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<el-dialog :model-value="Boolean(conditionId)" title="合并分支" width="min(520px, 94vw)" append-to-body :close-on-click-modal="false" @close="emit('close')">
|
|
42
|
+
<el-form label-position="top">
|
|
43
|
+
<el-form-item label="合并方向">
|
|
44
|
+
<el-radio-group v-model="retainedSide" aria-label="合并方向">
|
|
45
|
+
<el-radio-button value="yes">保留左侧流程</el-radio-button>
|
|
46
|
+
<el-radio-button value="no">保留右侧流程</el-radio-button>
|
|
47
|
+
</el-radio-group>
|
|
48
|
+
</el-form-item>
|
|
49
|
+
<el-form-item label="保留的公共流程起点">
|
|
50
|
+
<el-select v-model="commonId" aria-label="保留的公共流程起点" style="width: 100%">
|
|
51
|
+
<el-option v-for="step in candidates" :key="step.id" :value="step.id"
|
|
52
|
+
:label="`${condition ? flowBranchLabel(condition, step.flow!.parentBranch!) : ''} · ${step.label}`" />
|
|
53
|
+
</el-select>
|
|
54
|
+
</el-form-item>
|
|
55
|
+
<el-form-item :label="retainedSide === 'yes' ? '右侧重复部分起点' : '左侧重复部分起点'">
|
|
56
|
+
<el-select v-model="duplicateId" :disabled="!commonId" aria-label="另一侧重复部分起点" style="width: 100%">
|
|
57
|
+
<el-option value="" label="不删除,保留另一侧全部操作后接入" />
|
|
58
|
+
<el-option v-for="step in duplicates" :key="step.id" :value="step.id" :label="step.label" />
|
|
59
|
+
</el-select>
|
|
60
|
+
</el-form-item>
|
|
61
|
+
<el-alert v-if="typeof preview === 'string'" :title="preview" type="error" :closable="false" />
|
|
62
|
+
<el-checkbox v-else-if="preview?.removedCount" v-model="confirmed">确认删除另一侧从所选起点开始的 {{ preview.removedCount }} 个节点</el-checkbox>
|
|
63
|
+
</el-form>
|
|
64
|
+
<template #footer>
|
|
65
|
+
<el-button @click="emit('close')">取消</el-button>
|
|
66
|
+
<el-button type="primary" :disabled="disabled || !preview || typeof preview === 'string' || Boolean(preview.removedCount && !confirmed)" @click="submit">合并分支</el-button>
|
|
67
|
+
</template>
|
|
68
|
+
</el-dialog>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<style scoped>
|
|
72
|
+
:deep(.el-checkbox) { height: auto; white-space: normal; }
|
|
73
|
+
:deep(.el-checkbox__label) { white-space: normal; overflow-wrap: anywhere; }
|
|
74
|
+
</style>
|
|
@@ -23,9 +23,12 @@ type InsertAction =
|
|
|
23
23
|
| 'swipe'
|
|
24
24
|
| 'pinch'
|
|
25
25
|
| 'launchApp'
|
|
26
|
+
| 'openGallery'
|
|
27
|
+
| 'endFlow'
|
|
26
28
|
| 'clearAppData'
|
|
27
29
|
| 'popupCondition'
|
|
28
30
|
| 'checkboxState'
|
|
31
|
+
| 'checkedState'
|
|
29
32
|
| 'radioButtonState'
|
|
30
33
|
| 'aiRecognition'
|
|
31
34
|
| 'textClick'
|
|
@@ -87,9 +90,8 @@ const actionGroups: Array<{ title: string; actions: Array<{ type: InsertAction;
|
|
|
87
90
|
{ type: 'clearInput', label: '清空输入' },
|
|
88
91
|
{ type: 'coordinateTap', label: '点击坐标' },
|
|
89
92
|
{ type: 'longPress', label: '长按' },
|
|
90
|
-
{ type: '
|
|
93
|
+
{ type: 'checkedState', label: '判断勾选' },
|
|
91
94
|
{ type: 'textClick', label: '文字点击' },
|
|
92
|
-
{ type: 'radioButtonState', label: '判断 RadioButton 状态' },
|
|
93
95
|
],
|
|
94
96
|
},
|
|
95
97
|
{
|
|
@@ -97,6 +99,7 @@ const actionGroups: Array<{ title: string; actions: Array<{ type: InsertAction;
|
|
|
97
99
|
actions: [
|
|
98
100
|
{ type: 'keyBack', label: '系统返回' },
|
|
99
101
|
{ type: 'keyHome', label: 'Home 键' },
|
|
102
|
+
{ type: 'openGallery', label: '启动相册' },
|
|
100
103
|
{ type: 'keyRecent', label: '最近任务' },
|
|
101
104
|
{ type: 'keyPower', label: '电源键' },
|
|
102
105
|
{ type: 'swipe', label: '滑动' },
|
|
@@ -126,6 +129,7 @@ const actionGroups: Array<{ title: string; actions: Array<{ type: InsertAction;
|
|
|
126
129
|
title: '流程控制',
|
|
127
130
|
actions: [
|
|
128
131
|
{ type: 'noop', label: '空节点' },
|
|
132
|
+
{ type: 'endFlow', label: '终止流程' },
|
|
129
133
|
{ type: 'log', label: '输出日志' },
|
|
130
134
|
{ type: 'runScript', label: '连接脚本' },
|
|
131
135
|
],
|
|
@@ -145,10 +149,13 @@ function typeLabel(step: AppiumRecordedStep) {
|
|
|
145
149
|
coordinateTap: '坐标点击', swipe: '滑动', launchApp: '启动 APP', clearAppData: '清理 APP 缓存', longPress: '长按',
|
|
146
150
|
pinch: '双指缩放', runScript: '连接脚本', screenshot: '截图', visualChange: '检测画面变化',
|
|
147
151
|
noop: '空节点',
|
|
152
|
+
endFlow: '终止流程',
|
|
148
153
|
log: '输出日志',
|
|
149
154
|
checkboxState: 'Checkbox 状态',
|
|
155
|
+
checkedState: '判断勾选',
|
|
150
156
|
radioButtonState: 'RadioButton 状态',
|
|
151
157
|
aiRecognition: 'AI 识别',
|
|
158
|
+
openGallery: '启动相册',
|
|
152
159
|
textClick: '文字点击',
|
|
153
160
|
};
|
|
154
161
|
return labels[step.type] || step.type;
|
|
@@ -312,16 +319,19 @@ function branchSplitPath() {
|
|
|
312
319
|
</span>
|
|
313
320
|
</button>
|
|
314
321
|
<span class="appium-flow-node__actions" @click.stop>
|
|
322
|
+
<el-tooltip v-if="!copyMode && canCopyStep(item.step)" content="复制节点:复制后可在插入位置粘贴" placement="top" :show-after="200">
|
|
323
|
+
<span style="display: inline-flex">
|
|
315
324
|
<el-button
|
|
316
|
-
v-if="!copyMode && canCopyStep(item.step)"
|
|
317
325
|
text
|
|
318
326
|
size="small"
|
|
319
327
|
:icon="CopyDocument"
|
|
320
328
|
title="复制节点"
|
|
321
329
|
@click="$emit('copy', [item.index])"
|
|
322
330
|
/>
|
|
331
|
+
</span></el-tooltip>
|
|
332
|
+
<el-tooltip v-if="item.step.type === 'input' || item.step.type === 'inputIfExists'" content="修改输入内容:编辑此节点要输入的文字" placement="top" :show-after="200">
|
|
333
|
+
<span style="display: inline-flex">
|
|
323
334
|
<el-button
|
|
324
|
-
v-if="item.step.type === 'input' || item.step.type === 'inputIfExists'"
|
|
325
335
|
text
|
|
326
336
|
size="small"
|
|
327
337
|
:icon="Edit"
|
|
@@ -329,6 +339,9 @@ function branchSplitPath() {
|
|
|
329
339
|
title="修改输入内容"
|
|
330
340
|
@click="$emit('editInput', item.index)"
|
|
331
341
|
/>
|
|
342
|
+
</span></el-tooltip>
|
|
343
|
+
<el-tooltip content="删除节点:移除此操作;判断节点同时删除所属分支子节点" placement="top" :show-after="200">
|
|
344
|
+
<span style="display: inline-flex">
|
|
332
345
|
<el-button
|
|
333
346
|
text
|
|
334
347
|
size="small"
|
|
@@ -337,6 +350,7 @@ function branchSplitPath() {
|
|
|
337
350
|
title="删除节点"
|
|
338
351
|
@click="$emit('remove', item.index)"
|
|
339
352
|
/>
|
|
353
|
+
</span></el-tooltip>
|
|
340
354
|
</span>
|
|
341
355
|
</div>
|
|
342
356
|
<FlowStepEditor
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
import { View } from '@element-plus/icons-vue';
|
|
2
4
|
import type { AppiumNode } from '../types';
|
|
3
5
|
|
|
4
6
|
defineProps<{
|
|
@@ -6,6 +8,7 @@ defineProps<{
|
|
|
6
8
|
currentActivity?: string;
|
|
7
9
|
loading?: boolean;
|
|
8
10
|
}>();
|
|
11
|
+
const detailsVisible = ref(false);
|
|
9
12
|
</script>
|
|
10
13
|
|
|
11
14
|
<template>
|
|
@@ -14,6 +17,15 @@ defineProps<{
|
|
|
14
17
|
element-loading-text="正在检测页面变化..."
|
|
15
18
|
class="appium-node-detail"
|
|
16
19
|
>
|
|
20
|
+
<div class="appium-node-summary">
|
|
21
|
+
<div class="appium-node-summary__text" v-if="node">
|
|
22
|
+
<strong :title="node.label">{{ node.label }}</strong>
|
|
23
|
+
<span>{{ node.className.split('.').pop() }}<template v-if="node.checkable"> · {{ node.checked === undefined ? '勾选状态未知' : node.checked ? '已勾选' : '未勾选' }}</template><template v-if="node.selector.matchCount"> · {{ node.selector.matchCount > 1 ? `定位重复 ${node.selector.matchCount}` : '定位唯一' }}</template></span>
|
|
24
|
+
</div>
|
|
25
|
+
<span v-else class="appium-node-summary__empty">请选择组件</span>
|
|
26
|
+
<el-button size="small" type="primary" :icon="View" :disabled="!node" @click="detailsVisible = true">详情</el-button>
|
|
27
|
+
</div>
|
|
28
|
+
<el-drawer v-model="detailsVisible" title="组件详情" size="520px" style="max-width: 94vw" append-to-body>
|
|
17
29
|
<template v-if="node">
|
|
18
30
|
<div class="appium-node-detail__title">{{ node.label }}</div>
|
|
19
31
|
<el-descriptions :column="1" border size="small">
|
|
@@ -25,7 +37,6 @@ defineProps<{
|
|
|
25
37
|
<el-descriptions-item v-if="node.checkable" label="checked">{{ node.checked === undefined ? '-' : String(node.checked) }}</el-descriptions-item>
|
|
26
38
|
<el-descriptions-item label="当前 Activity">{{ currentActivity || '-' }}</el-descriptions-item>
|
|
27
39
|
<el-descriptions-item label="selector">
|
|
28
|
-
<span v-if="currentActivity">Activity {{ currentActivity }} · </span>
|
|
29
40
|
{{ node.selector.strategy }} {{ node.selector.value || '' }}
|
|
30
41
|
<el-tag
|
|
31
42
|
v-if="node.selector.matchCount"
|
|
@@ -37,12 +48,10 @@ defineProps<{
|
|
|
37
48
|
</el-descriptions-item>
|
|
38
49
|
<el-descriptions-item label="推荐定位">
|
|
39
50
|
<span v-if="node.contextSelector">
|
|
40
|
-
<template v-if="currentActivity">Activity {{ currentActivity }} + </template>
|
|
41
51
|
父级 {{ node.contextSelector.strategy }} {{ node.contextSelector.value || '' }}
|
|
42
52
|
+ 子级 {{ node.selector.strategy }} {{ node.selector.value || '' }}
|
|
43
53
|
</span>
|
|
44
54
|
<span v-else>
|
|
45
|
-
<template v-if="currentActivity">Activity {{ currentActivity }} + </template>
|
|
46
55
|
{{ node.selector.unique ? '当前 selector 唯一' : '使用当前 selector 或坐标兜底' }}
|
|
47
56
|
</span>
|
|
48
57
|
</el-descriptions-item>
|
|
@@ -54,7 +63,16 @@ defineProps<{
|
|
|
54
63
|
</el-descriptions-item>
|
|
55
64
|
</el-descriptions>
|
|
56
65
|
</template>
|
|
57
|
-
<
|
|
66
|
+
<p v-else>请选择组件</p>
|
|
67
|
+
</el-drawer>
|
|
58
68
|
|
|
59
69
|
</div>
|
|
60
70
|
</template>
|
|
71
|
+
|
|
72
|
+
<style scoped>
|
|
73
|
+
.appium-node-summary { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 44px; padding: 6px 0; }
|
|
74
|
+
.appium-node-summary__text { display: grid; gap: 2px; min-width: 0; font-size: 12px; }
|
|
75
|
+
.appium-node-summary__text strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
|
|
76
|
+
.appium-node-summary__text span, .appium-node-summary__empty { color: var(--el-text-color-secondary); font-size: 12px; }
|
|
77
|
+
:deep(.el-descriptions__content) { overflow-wrap: anywhere; word-break: break-word; }
|
|
78
|
+
</style>
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, nextTick, shallowRef, watch } from 'vue';
|
|
3
|
-
import { ElMessage } from 'element-plus';
|
|
4
|
-
import { Aim, CopyDocument, FullScreen } from '@element-plus/icons-vue';
|
|
3
|
+
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
4
|
+
import { Aim, CopyDocument, FullScreen, Close, Delete } from '@element-plus/icons-vue';
|
|
5
|
+
import { removeFlowSteps } from '../flow-remove';
|
|
5
6
|
import type { AppiumRecordedStep } from '../types';
|
|
6
7
|
import { createFlowClipboard } from '../flow-copy';
|
|
7
8
|
import FlowCanvas from './FlowCanvas.vue';
|
|
9
|
+
import MergeBranchesDialog from './MergeBranchesDialog.vue';
|
|
8
10
|
import { labelFlowStep } from '../flow-labels';
|
|
9
11
|
import type { FlowActionGroup, FlowBranch, InsertAction } from '../flow-graph';
|
|
10
12
|
|
|
@@ -13,6 +15,7 @@ const props = defineProps<{
|
|
|
13
15
|
steps: AppiumRecordedStep[];
|
|
14
16
|
disabled?: boolean;
|
|
15
17
|
removeDisabled?: boolean;
|
|
18
|
+
mergeDisabled?: boolean;
|
|
16
19
|
allowedLockedActions?: InsertAction[];
|
|
17
20
|
launchingStepId?: string;
|
|
18
21
|
clipboardCount?: number;
|
|
@@ -29,10 +32,14 @@ const emit = defineEmits<{
|
|
|
29
32
|
previewLinkedScript: [index: number];
|
|
30
33
|
executeStep: [index: number];
|
|
31
34
|
updateStep: [index: number, step: AppiumRecordedStep];
|
|
35
|
+
replaceSteps: [steps: AppiumRecordedStep[]];
|
|
32
36
|
}>();
|
|
33
37
|
|
|
34
38
|
const expandedStepIndex = shallowRef<number | null>(null);
|
|
39
|
+
const mergeConditionId = shallowRef('');
|
|
35
40
|
const copyMode = shallowRef(false);
|
|
41
|
+
const deleteMode = shallowRef(false);
|
|
42
|
+
const deleting = shallowRef(false);
|
|
36
43
|
const selectedCopyIndexes = shallowRef<number[]>([]);
|
|
37
44
|
const flowDialogVisible = shallowRef(false);
|
|
38
45
|
const mainResetViewToken = shallowRef(0);
|
|
@@ -47,9 +54,8 @@ const insertActionGroups: FlowActionGroup[] = [
|
|
|
47
54
|
{ type: 'clearInput', label: '清空输入' },
|
|
48
55
|
{ type: 'coordinateTap', label: '点击坐标' },
|
|
49
56
|
{ type: 'longPress', label: '长按' },
|
|
50
|
-
{ type: '
|
|
57
|
+
{ type: 'checkedState', label: '判断勾选' },
|
|
51
58
|
{ type: 'textClick', label: '文字点击' },
|
|
52
|
-
{ type: 'radioButtonState', label: '判断 RadioButton 状态' },
|
|
53
59
|
],
|
|
54
60
|
},
|
|
55
61
|
{
|
|
@@ -63,6 +69,7 @@ const insertActionGroups: FlowActionGroup[] = [
|
|
|
63
69
|
{ type: 'pinch', label: '双指缩放' },
|
|
64
70
|
{ type: 'clearAppData', label: '清理 App 缓存' },
|
|
65
71
|
{ type: 'launchApp', label: '启动 App' },
|
|
72
|
+
{ type: 'openGallery', label: '启动相册' },
|
|
66
73
|
],
|
|
67
74
|
},
|
|
68
75
|
{
|
|
@@ -88,6 +95,7 @@ const insertActionGroups: FlowActionGroup[] = [
|
|
|
88
95
|
title: '流程控制',
|
|
89
96
|
actions: [
|
|
90
97
|
{ type: 'noop', label: '空节点' },
|
|
98
|
+
{ type: 'endFlow', label: '终止流程' },
|
|
91
99
|
{ type: 'log', label: '输出日志' },
|
|
92
100
|
{ type: 'runScript', label: '连接脚本' },
|
|
93
101
|
],
|
|
@@ -121,7 +129,7 @@ function toggleCopySelection(index: number) {
|
|
|
121
129
|
const step = props.steps[index];
|
|
122
130
|
if (!step) return;
|
|
123
131
|
if (step.type === 'launchApp' || step.type === 'clearAppData') {
|
|
124
|
-
ElMessage.warning('App 初始化节点不能复制');
|
|
132
|
+
ElMessage.warning(deleteMode.value ? 'App 初始化节点不参与批量删除' : 'App 初始化节点不能复制');
|
|
125
133
|
return;
|
|
126
134
|
}
|
|
127
135
|
if (selectedCopyIndexes.value.includes(index)) {
|
|
@@ -139,7 +147,8 @@ function toggleCopySelection(index: number) {
|
|
|
139
147
|
try {
|
|
140
148
|
createFlowClipboard(props.steps, candidate);
|
|
141
149
|
} catch (error) {
|
|
142
|
-
|
|
150
|
+
const message = error instanceof Error ? error.message : '当前节点不能一起选择';
|
|
151
|
+
ElMessage.warning(deleteMode.value ? message.replace(/复制/g, '删除') : message);
|
|
143
152
|
return;
|
|
144
153
|
}
|
|
145
154
|
selectedCopyIndexes.value = candidate;
|
|
@@ -161,12 +170,14 @@ function handleNodeClick(index: number) {
|
|
|
161
170
|
}
|
|
162
171
|
|
|
163
172
|
function startCopyMode() {
|
|
173
|
+
deleteMode.value = false;
|
|
164
174
|
copyMode.value = true;
|
|
165
175
|
selectedCopyIndexes.value = [];
|
|
166
176
|
expandedStepIndex.value = null;
|
|
167
177
|
}
|
|
168
178
|
|
|
169
179
|
function cancelCopyMode() {
|
|
180
|
+
deleteMode.value = false;
|
|
170
181
|
copyMode.value = false;
|
|
171
182
|
selectedCopyIndexes.value = [];
|
|
172
183
|
}
|
|
@@ -177,6 +188,33 @@ function copySelectedNodes() {
|
|
|
177
188
|
cancelCopyMode();
|
|
178
189
|
}
|
|
179
190
|
|
|
191
|
+
function startDeleteMode() {
|
|
192
|
+
if (props.mergeDisabled || props.removeDisabled) return;
|
|
193
|
+
startCopyMode();
|
|
194
|
+
deleteMode.value = true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function deleteSelectedNodes() {
|
|
198
|
+
if (deleting.value || props.mergeDisabled || props.removeDisabled || !selectedCopyIndexes.value.length) return;
|
|
199
|
+
const original = props.steps;
|
|
200
|
+
const next = removeFlowSteps(original, selectedCopyIndexes.value);
|
|
201
|
+
deleting.value = true;
|
|
202
|
+
try {
|
|
203
|
+
await ElMessageBox.confirm(`确认删除选中的 ${original.length - next.length} 个节点?所属分支子节点会一起删除,公共流程保留。`, '批量删除', {
|
|
204
|
+
type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消', appendTo: 'body',
|
|
205
|
+
});
|
|
206
|
+
if (props.steps !== original || props.mergeDisabled || props.removeDisabled) {
|
|
207
|
+
ElMessage.warning('流程或运行状态已变化,请重新选择');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
emit('replaceSteps', next);
|
|
211
|
+
cancelCopyMode();
|
|
212
|
+
} catch { /* 取消确认时保留选中状态。 */ }
|
|
213
|
+
finally { deleting.value = false; }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
watch(() => props.steps, () => { if (copyMode.value) cancelCopyMode(); });
|
|
217
|
+
|
|
180
218
|
function resetMainFlowPosition() {
|
|
181
219
|
mainResetViewToken.value += 1;
|
|
182
220
|
}
|
|
@@ -216,40 +254,43 @@ function updateStep(index: number, step: AppiumRecordedStep) {
|
|
|
216
254
|
<div class="appium-flow-toolbar">
|
|
217
255
|
<template v-if="copyMode">
|
|
218
256
|
<span class="appium-flow-toolbar__status">已选择 {{ selectedCopyIndexes.length }} 个节点</span>
|
|
219
|
-
<el-
|
|
257
|
+
<el-tooltip :content="deleteMode ? '删除选中' : '复制选中'" placement="top" :show-after="200"><span class="flow-toolbar-icon">
|
|
258
|
+
<el-button :aria-label="deleteMode ? '删除选中' : '复制选中'"
|
|
220
259
|
size="small"
|
|
221
|
-
type="primary"
|
|
222
|
-
:
|
|
223
|
-
:
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
</el-
|
|
228
|
-
<el-button size="small" @click="cancelCopyMode"
|
|
260
|
+
:type="deleteMode ? 'danger' : 'primary'"
|
|
261
|
+
:loading="deleting"
|
|
262
|
+
:disabled="!selectedCopyIndexes.length || (deleteMode && (mergeDisabled || removeDisabled))"
|
|
263
|
+
:icon="deleteMode ? Delete : CopyDocument"
|
|
264
|
+
@click="deleteMode ? deleteSelectedNodes() : copySelectedNodes()"
|
|
265
|
+
/>
|
|
266
|
+
</span></el-tooltip>
|
|
267
|
+
<el-tooltip content="取消" placement="top" :show-after="200"><span class="flow-toolbar-icon"><el-button size="small" :icon="Close" aria-label="取消" @click="cancelCopyMode" /></span></el-tooltip>
|
|
229
268
|
</template>
|
|
230
|
-
<el-
|
|
231
|
-
|
|
269
|
+
<el-tooltip v-else content="批量复制" placement="top" :show-after="200"><span class="flow-toolbar-icon">
|
|
270
|
+
<el-button aria-label="批量复制"
|
|
232
271
|
size="small"
|
|
233
272
|
:icon="CopyDocument"
|
|
234
273
|
:disabled="!steps.length"
|
|
235
274
|
@click="startCopyMode"
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
275
|
+
/>
|
|
276
|
+
</span></el-tooltip>
|
|
277
|
+
<el-tooltip v-if="!copyMode" content="批量删除" placement="top" :show-after="200"><span class="flow-toolbar-icon">
|
|
278
|
+
<el-button aria-label="批量删除" size="small" :icon="Delete" :disabled="!steps.length || mergeDisabled || removeDisabled" @click="startDeleteMode" />
|
|
279
|
+
</span></el-tooltip>
|
|
280
|
+
<el-tooltip content="还原位置" placement="top" :show-after="200"><span class="flow-toolbar-icon">
|
|
281
|
+
<el-button aria-label="还原位置"
|
|
240
282
|
size="small"
|
|
241
283
|
:icon="Aim"
|
|
242
284
|
@click="resetMainFlowPosition"
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
<el-button
|
|
285
|
+
/>
|
|
286
|
+
</span></el-tooltip>
|
|
287
|
+
<el-tooltip content="放大" placement="top" :show-after="200"><span class="flow-toolbar-icon">
|
|
288
|
+
<el-button aria-label="放大"
|
|
247
289
|
size="small"
|
|
248
290
|
:icon="FullScreen"
|
|
249
291
|
@click="flowDialogVisible = true"
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
</el-button>
|
|
292
|
+
/>
|
|
293
|
+
</span></el-tooltip>
|
|
253
294
|
</div>
|
|
254
295
|
|
|
255
296
|
<FlowCanvas
|
|
@@ -257,9 +298,11 @@ function updateStep(index: number, step: AppiumRecordedStep) {
|
|
|
257
298
|
:steps="steps"
|
|
258
299
|
:expanded-step-index="expandedStepIndex"
|
|
259
300
|
:copy-mode="copyMode"
|
|
301
|
+
:delete-mode="deleteMode"
|
|
260
302
|
:selected-copy-indexes="selectedCopyIndexes"
|
|
261
303
|
:disabled="disabled"
|
|
262
304
|
:remove-disabled="removeDisabled"
|
|
305
|
+
:merge-disabled="mergeDisabled"
|
|
263
306
|
:launching-step-id="launchingStepId"
|
|
264
307
|
:ai-recognition-model-configured="aiRecognitionModelConfigured"
|
|
265
308
|
:clipboard-count="clipboardCount"
|
|
@@ -282,6 +325,7 @@ function updateStep(index: number, step: AppiumRecordedStep) {
|
|
|
282
325
|
@preview-linked-script="(index) => emit('previewLinkedScript', index)"
|
|
283
326
|
@execute-step="(index) => emit('executeStep', index)"
|
|
284
327
|
@update-step="(index, step) => updateStep(index, step)"
|
|
328
|
+
@merge="mergeConditionId = steps[$event]?.id || ''"
|
|
285
329
|
/>
|
|
286
330
|
|
|
287
331
|
<el-dialog
|
|
@@ -294,26 +338,29 @@ function updateStep(index: number, step: AppiumRecordedStep) {
|
|
|
294
338
|
<div class="appium-flow-toolbar appium-flow-dialog__toolbar">
|
|
295
339
|
<template v-if="copyMode">
|
|
296
340
|
<span class="appium-flow-toolbar__status">已选择 {{ selectedCopyIndexes.length }} 个节点</span>
|
|
297
|
-
<el-
|
|
341
|
+
<el-tooltip :content="deleteMode ? '删除选中' : '复制选中'" placement="top" :show-after="200"><span class="flow-toolbar-icon">
|
|
342
|
+
<el-button :aria-label="deleteMode ? '删除选中' : '复制选中'"
|
|
298
343
|
size="small"
|
|
299
|
-
type="primary"
|
|
300
|
-
:
|
|
301
|
-
:
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
</el-
|
|
306
|
-
<el-button size="small" @click="cancelCopyMode"
|
|
344
|
+
:type="deleteMode ? 'danger' : 'primary'"
|
|
345
|
+
:loading="deleting"
|
|
346
|
+
:disabled="!selectedCopyIndexes.length || (deleteMode && (mergeDisabled || removeDisabled))"
|
|
347
|
+
:icon="deleteMode ? Delete : CopyDocument"
|
|
348
|
+
@click="deleteMode ? deleteSelectedNodes() : copySelectedNodes()"
|
|
349
|
+
/>
|
|
350
|
+
</span></el-tooltip>
|
|
351
|
+
<el-tooltip content="取消" placement="top" :show-after="200"><span class="flow-toolbar-icon"><el-button size="small" :icon="Close" aria-label="取消" @click="cancelCopyMode" /></span></el-tooltip>
|
|
307
352
|
</template>
|
|
308
|
-
<el-
|
|
309
|
-
|
|
353
|
+
<el-tooltip v-else content="批量复制" placement="top" :show-after="200"><span class="flow-toolbar-icon">
|
|
354
|
+
<el-button aria-label="批量复制"
|
|
310
355
|
size="small"
|
|
311
356
|
:icon="CopyDocument"
|
|
312
357
|
:disabled="!steps.length"
|
|
313
358
|
@click="startCopyMode"
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
359
|
+
/>
|
|
360
|
+
</span></el-tooltip>
|
|
361
|
+
<el-tooltip v-if="!copyMode" content="批量删除" placement="top" :show-after="200"><span class="flow-toolbar-icon">
|
|
362
|
+
<el-button aria-label="批量删除" size="small" :icon="Delete" :disabled="!steps.length || mergeDisabled || removeDisabled" @click="startDeleteMode" />
|
|
363
|
+
</span></el-tooltip>
|
|
317
364
|
</div>
|
|
318
365
|
<FlowCanvas
|
|
319
366
|
id="appium-flow-dialog"
|
|
@@ -321,9 +368,11 @@ function updateStep(index: number, step: AppiumRecordedStep) {
|
|
|
321
368
|
:steps="steps"
|
|
322
369
|
:expanded-step-index="expandedStepIndex"
|
|
323
370
|
:copy-mode="copyMode"
|
|
371
|
+
:delete-mode="deleteMode"
|
|
324
372
|
:selected-copy-indexes="selectedCopyIndexes"
|
|
325
373
|
:disabled="disabled"
|
|
326
374
|
:remove-disabled="removeDisabled"
|
|
375
|
+
:merge-disabled="mergeDisabled"
|
|
327
376
|
:launching-step-id="launchingStepId"
|
|
328
377
|
:ai-recognition-model-configured="aiRecognitionModelConfigured"
|
|
329
378
|
:clipboard-count="clipboardCount"
|
|
@@ -346,7 +395,15 @@ function updateStep(index: number, step: AppiumRecordedStep) {
|
|
|
346
395
|
@preview-linked-script="(index) => emit('previewLinkedScript', index)"
|
|
347
396
|
@execute-step="(index) => emit('executeStep', index)"
|
|
348
397
|
@update-step="(index, step) => updateStep(index, step)"
|
|
398
|
+
@merge="mergeConditionId = steps[$event]?.id || ''"
|
|
349
399
|
/>
|
|
350
400
|
</el-dialog>
|
|
401
|
+
<MergeBranchesDialog :steps="steps" :condition-id="mergeConditionId" :disabled="mergeDisabled"
|
|
402
|
+
@close="mergeConditionId = ''" @confirm="expandedStepIndex = null; emit('replaceSteps', $event)" />
|
|
351
403
|
</div>
|
|
352
404
|
</template>
|
|
405
|
+
|
|
406
|
+
<style scoped>
|
|
407
|
+
.flow-toolbar-icon { display: inline-flex; }
|
|
408
|
+
.flow-toolbar-icon .el-button { width: 30px; height: 30px; padding: 0; }
|
|
409
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { InjectionKey, Ref } from 'vue';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_FLOW_BACKGROUND = '#d4e8dd';
|
|
4
|
+
export const FLOW_BACKGROUND_PRESETS = ['#d4e8dd', '#d5e6f5', '#e4dcf2', '#f2dedc', '#d5eae9', '#e8e4cc'];
|
|
5
|
+
export const flowBackgroundKey: InjectionKey<Readonly<Ref<string>>> = Symbol('flowBackground');
|
|
6
|
+
|
|
7
|
+
export function isHexColor(value: unknown): value is string {
|
|
8
|
+
return typeof value === 'string' && /^#(?:[\da-f]{3}|[\da-f]{6})$/i.test(value.trim());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function normalizeFlowBackground(value: unknown) {
|
|
12
|
+
if (!isHexColor(value)) return DEFAULT_FLOW_BACKGROUND;
|
|
13
|
+
const color = value.trim().toLowerCase();
|
|
14
|
+
return color.length === 4 ? '#' + [...color.slice(1)].map((char) => char + char).join('') : color;
|
|
15
|
+
}
|
|
@@ -24,9 +24,12 @@ export type InsertAction =
|
|
|
24
24
|
| 'swipe'
|
|
25
25
|
| 'pinch'
|
|
26
26
|
| 'launchApp'
|
|
27
|
+
| 'openGallery'
|
|
28
|
+
| 'endFlow'
|
|
27
29
|
| 'clearAppData'
|
|
28
30
|
| 'popupCondition'
|
|
29
31
|
| 'checkboxState'
|
|
32
|
+
| 'checkedState'
|
|
30
33
|
| 'radioButtonState'
|
|
31
34
|
| 'aiRecognition'
|
|
32
35
|
| 'textClick'
|
|
@@ -87,8 +90,10 @@ export type FlowGraphNodeData =
|
|
|
87
90
|
expanded: boolean;
|
|
88
91
|
selected: boolean;
|
|
89
92
|
copyMode: boolean;
|
|
93
|
+
deleteMode?: boolean;
|
|
90
94
|
disabled?: boolean;
|
|
91
95
|
removeDisabled?: boolean;
|
|
96
|
+
mergeDisabled?: boolean;
|
|
92
97
|
launching?: boolean;
|
|
93
98
|
canCopy: boolean;
|
|
94
99
|
canEditInput: boolean;
|
|
@@ -130,8 +135,10 @@ type BuildFlowGraphOptions = {
|
|
|
130
135
|
expandedStepIndex: number | null;
|
|
131
136
|
selectedCopyIndexes: number[];
|
|
132
137
|
copyMode: boolean;
|
|
138
|
+
deleteMode?: boolean;
|
|
133
139
|
disabled?: boolean;
|
|
134
140
|
removeDisabled?: boolean;
|
|
141
|
+
mergeDisabled?: boolean;
|
|
135
142
|
launchingStepId?: string;
|
|
136
143
|
clipboardCount?: number;
|
|
137
144
|
canOpenInsertMenu: boolean;
|
|
@@ -231,7 +238,7 @@ function branchConnectionTargetId(items: StepItem[], condition: AppiumRecordedSt
|
|
|
231
238
|
}
|
|
232
239
|
const targetId = branch === 'yes' ? condition.flow?.yesTargetId : condition.flow?.noTargetId;
|
|
233
240
|
const target = items.find(({ step }) => step.id === targetId)?.step;
|
|
234
|
-
return target &&
|
|
241
|
+
return target && target.flow?.parentConditionId === condition.flow?.parentConditionId ? target.id : '';
|
|
235
242
|
}
|
|
236
243
|
|
|
237
244
|
function estimateTextLines(text: string | undefined, charsPerLine: number) {
|
|
@@ -421,10 +428,12 @@ export function buildFlowGraph(
|
|
|
421
428
|
expanded: options.expandedStepIndex === item.index,
|
|
422
429
|
selected: options.isCopySelected(item.index),
|
|
423
430
|
copyMode: options.copyMode,
|
|
431
|
+
deleteMode: options.deleteMode,
|
|
424
432
|
disabled: item.step.type === 'launchApp' || item.step.type === 'clearAppData'
|
|
425
433
|
? options.isAppExecutionDisabled(item.step.type)
|
|
426
434
|
: item.step.type === 'aiRecognition' ? options.isInsertActionDisabled('aiRecognition') : options.disabled,
|
|
427
435
|
removeDisabled: options.removeDisabled,
|
|
436
|
+
mergeDisabled: options.mergeDisabled,
|
|
428
437
|
launching: options.launchingStepId === item.step.id,
|
|
429
438
|
canCopy: item.step.type !== 'launchApp'
|
|
430
439
|
&& item.step.type !== 'clearAppData'
|
|
@@ -542,7 +551,8 @@ export function buildFlowGraph(
|
|
|
542
551
|
|
|
543
552
|
const targetId = item.step.flow?.successTargetId;
|
|
544
553
|
const explicitTarget = targetId ? stepById.get(targetId) : undefined;
|
|
545
|
-
const explicitMainTarget = explicitTarget && !explicitTarget.step.flow?.parentConditionId
|
|
554
|
+
const explicitMainTarget = explicitTarget && (!explicitTarget.step.flow?.parentConditionId
|
|
555
|
+
|| explicitTarget.step.flow.parentConditionId !== item.step.flow?.parentConditionId)
|
|
546
556
|
? explicitTarget
|
|
547
557
|
: undefined;
|
|
548
558
|
const target = explicitMainTarget || nextSibling;
|
|
@@ -733,11 +743,29 @@ export function buildFlowGraph(
|
|
|
733
743
|
const orderedLinks = [...links].sort((a, b) => (
|
|
734
744
|
(nodeOrder.get(a.source) || 0) - (nodeOrder.get(b.source) || 0)
|
|
735
745
|
));
|
|
746
|
+
// 公共起点从两侧汇入,不能被最后一条分支连线拉到左列或右列。
|
|
747
|
+
const joins = new Map<string, string>();
|
|
748
|
+
items.forEach(({ step }) => {
|
|
749
|
+
const target = step.flow?.successTargetId && stepById.get(step.flow.successTargetId);
|
|
750
|
+
if (defaultKind(step) === 'condition' && target && !joins.has(nodeIdForStep(target.step))) {
|
|
751
|
+
joins.set(nodeIdForStep(target.step), nodeIdForStep(step));
|
|
752
|
+
}
|
|
753
|
+
});
|
|
736
754
|
const alignVisibleChainGeometry = () => {
|
|
737
755
|
orderedLinks.forEach((link) => {
|
|
738
756
|
const source = nodeById.get(link.source);
|
|
739
757
|
const target = nodeById.get(link.target);
|
|
740
758
|
if (!source || !target?.data) return;
|
|
759
|
+
const joinOwner = joins.get(target.id);
|
|
760
|
+
if (joinOwner && target.id !== joinOwner) {
|
|
761
|
+
const incoming = links.filter((edge) => edge.target === target.id).map((edge) => nodeById.get(edge.source)!).filter(Boolean);
|
|
762
|
+
const owner = nodeById.get(joinOwner);
|
|
763
|
+
if (owner && incoming.length) {
|
|
764
|
+
target.position.y = Math.round(Math.max(...incoming.map((node) => node.position.y + nodeHeight(node))) + FLOW_STANDARD_LINE_GAP * 2);
|
|
765
|
+
setCenterX(target, centerX(owner));
|
|
766
|
+
}
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
741
769
|
const kind = target.data.kind;
|
|
742
770
|
if (!['branch', 'split', 'insert'].includes(kind) && !canAlignStepAfterFlowPoint(source, target)) return;
|
|
743
771
|
|
|
@@ -777,7 +805,7 @@ export function buildFlowGraph(
|
|
|
777
805
|
selectable: false,
|
|
778
806
|
focusable: false,
|
|
779
807
|
class: 'appium-vue-flow-edge',
|
|
780
|
-
data: { branch: link.branch },
|
|
808
|
+
data: { branch: link.branch, merge: joins.has(link.target) },
|
|
781
809
|
}));
|
|
782
810
|
|
|
783
811
|
return { nodes: positionedNodes, edges };
|