android-midscene-automation 0.1.19 → 0.1.21
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 +25 -0
- package/README.md +25 -0
- package/package.json +9 -1
- package/server/appium-recorder/appium-runner.ts +41 -20
- package/server/appium-recorder/repository.ts +27 -6
- package/src/appium-recorder/AppiumPage.vue +77 -48
- package/src/appium-recorder/components/FlowStepEditor.vue +0 -8
- package/src/appium-recorder/components/NestedConditionBranches.vue +373 -0
- package/src/appium-recorder/components/RecordedSteps.vue +607 -23
- package/src/appium-recorder/flow-copy.ts +193 -0
- package/src/appium-recorder/flow-layout.ts +88 -0
- package/src/style.css +163 -34
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# 更新记录
|
|
2
2
|
|
|
3
|
+
## v0.1.21
|
|
4
|
+
|
|
5
|
+
- 补充 GitHub 仓库、问题反馈和项目主页信息,npm 包页面可直接跳转至源码仓库和 Issues。
|
|
6
|
+
|
|
7
|
+
## v0.1.20
|
|
8
|
+
|
|
9
|
+
- 修复包含判断分支的历史脚本在加载时因流程轴线循环计算导致渲染失败、无法切换到“当前录制”的问题。
|
|
10
|
+
- Appium 工作区加载脚本后会同步切换并保存“当前录制”标签页状态。
|
|
11
|
+
|
|
12
|
+
- Appium 判断分支按“是 / 否”两侧各自的子树宽度布局,并以当前判断节点为锚点就近排列;内容较少的一侧不再被推向画布远端,连接线同步跟随真实节点中心。
|
|
13
|
+
- 加深 Appium 流程复制模式的节点选中颜色,并确保“是 / 否”分支节点不会覆盖蓝色选中边框。
|
|
14
|
+
- Appium 复制判断节点后,父级分支会按嵌套子树宽度自动扩展;连接线端点与分支中心保持对齐,移除穿透子流程的父级竖线,并避免不同层级重复显示“插入操作”。
|
|
15
|
+
- Appium 流程总览弹窗补齐批量复制、节点执行/编辑/删除、开始与节点后插入、分支插入及连接操作;移除“撤销粘贴”功能。
|
|
16
|
+
- Appium 流程节点在主画布与放大弹窗中统一固定尺寸,补充分支节点间距,移除节点序号,并在放大弹窗补充主节点和递归分支节点的复制操作。
|
|
17
|
+
- Appium 批量复制支持选择“分支末尾 → 已连接的后续主流程”节点,不再误报为不同分支。
|
|
18
|
+
- Appium 流程复制支持递归展示粘贴后的判断子节点,保留“是 / 否”分支及其子节点的复制、编辑和插入操作。
|
|
19
|
+
|
|
20
|
+
- Appium session 创建与“启动 APP”流程节点解耦:session 只绑定设备,应用统一由启动节点按预设包名通过 ADB 启动。
|
|
21
|
+
- Appium 脚本保存和导入时会忽略不属于目标 App 的桌面、设置等系统 Activity,并从首个业务节点快照恢复入口 Activity。
|
|
22
|
+
- Appium 录制流程新增单节点和同分支连续节点批量复制;可在现有插入位置粘贴,自动重建节点 ID、判断子流程和流程连接。
|
|
23
|
+
- Appium 工作区扩大“录制与脚本”区域并保持设备预览完整高度;回放输出改为右下角悬浮入口,通过可拖动、可缩放弹窗查看。
|
|
24
|
+
- 修复 Appium 批量复制判断节点和子节点时的跨分支误判;统一主流程与分支节点宽度,并扩大分支间距,避免粘贴节点尺寸不一致和内容挤压。
|
|
25
|
+
- Appium 回放日志弹窗的输出区域改为暗色终端样式,并优化滚动条对比度。
|
|
26
|
+
- 修复历史脚本因当前 Activity 不匹配时“粘贴节点”静默无效的问题;粘贴现在与普通插入操作使用相同锁定规则,并在页面操作忙碌时给出提示。
|
|
27
|
+
|
|
3
28
|
## v0.1.19
|
|
4
29
|
|
|
5
30
|
- Appium 回放输出新增托管 Appium 服务端完整日志,并将页面回放输出和 Appium 原始日志保存为同名 `.log` 文件。
|
package/README.md
CHANGED
|
@@ -1210,6 +1210,31 @@ output/2026-08-21_17-13-42-831-登录流程.html
|
|
|
1210
1210
|
|
|
1211
1211
|
# 项目更新记录
|
|
1212
1212
|
|
|
1213
|
+
## v0.1.21
|
|
1214
|
+
|
|
1215
|
+
- 补充 GitHub 仓库、问题反馈和项目主页信息,npm 包页面可直接跳转至源码仓库和 Issues。
|
|
1216
|
+
|
|
1217
|
+
## v0.1.20
|
|
1218
|
+
|
|
1219
|
+
- 修复包含判断分支的历史脚本在加载时因流程轴线循环计算导致渲染失败、无法切换到“当前录制”的问题。
|
|
1220
|
+
- Appium 工作区加载脚本后会同步切换并保存“当前录制”标签页状态。
|
|
1221
|
+
|
|
1222
|
+
- Appium 判断分支按“是 / 否”两侧各自的子树宽度布局,并以当前判断节点为锚点就近排列;内容较少的一侧不再被推向画布远端,连接线同步跟随真实节点中心。
|
|
1223
|
+
- 加深 Appium 流程复制模式的节点选中颜色,并确保“是 / 否”分支节点不会覆盖蓝色选中边框。
|
|
1224
|
+
- Appium 复制判断节点后,父级分支会按嵌套子树宽度自动扩展;连接线端点与分支中心保持对齐,移除穿透子流程的父级竖线,并避免不同层级重复显示“插入操作”。
|
|
1225
|
+
- Appium 流程总览弹窗补齐批量复制、节点执行/编辑/删除、开始与节点后插入、分支插入及连接操作;移除“撤销粘贴”功能。
|
|
1226
|
+
- Appium 流程节点在主画布与放大弹窗中统一固定尺寸,补充分支节点间距,移除节点序号,并在放大弹窗补充主节点和递归分支节点的复制操作。
|
|
1227
|
+
- Appium 批量复制支持选择“分支末尾 → 已连接的后续主流程”节点,不再误报为不同分支。
|
|
1228
|
+
- Appium 流程复制支持递归展示粘贴后的判断子节点,保留“是 / 否”分支及其子节点的复制、编辑和插入操作。
|
|
1229
|
+
|
|
1230
|
+
- Appium session 创建与“启动 APP”流程节点解耦:session 只绑定设备,应用统一由启动节点按预设包名通过 ADB 启动。
|
|
1231
|
+
- Appium 脚本保存和导入时会忽略不属于目标 App 的桌面、设置等系统 Activity,并从首个业务节点快照恢复入口 Activity。
|
|
1232
|
+
- Appium 录制流程新增单节点和同分支连续节点批量复制;可在现有插入位置粘贴,自动重建节点 ID、判断子流程和流程连接。
|
|
1233
|
+
- Appium 工作区扩大“录制与脚本”区域并保持设备预览完整高度;回放输出改为右下角悬浮入口,通过可拖动、可缩放弹窗查看。
|
|
1234
|
+
- 修复 Appium 批量复制判断节点和子节点时的跨分支误判;统一主流程与分支节点宽度,并扩大分支间距,避免粘贴节点尺寸不一致和内容挤压。
|
|
1235
|
+
- Appium 回放日志弹窗的输出区域改为暗色终端样式,并优化滚动条对比度。
|
|
1236
|
+
- 修复历史脚本因当前 Activity 不匹配时“粘贴节点”静默无效的问题;粘贴现在与普通插入操作使用相同锁定规则,并在页面操作忙碌时给出提示。
|
|
1237
|
+
|
|
1213
1238
|
## v0.1.19
|
|
1214
1239
|
|
|
1215
1240
|
- Appium 回放输出新增托管 Appium 服务端完整日志,并将页面回放输出和 Appium 原始日志保存为同名 `.log` 文件。
|
package/package.json
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "android-midscene-automation",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/oooooooko/android-midscene-automation.git"
|
|
7
|
+
},
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/oooooooko/android-midscene-automation/issues"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/oooooooko/android-midscene-automation#readme",
|
|
4
12
|
"type": "module",
|
|
5
13
|
"bin": {
|
|
6
14
|
"android-midscene-automation": "./bin/android-midscene-automation.js"
|
|
@@ -204,7 +204,11 @@ function appendAndroidSdkDiagnostics(lines: string[]) {
|
|
|
204
204
|
lines.push('- Appium 是独立进程时不会继承网页中后设置的环境变量;设置后请重新打开终端,并重启 appium 服务。');
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
async function appiumRequest<T>(
|
|
207
|
+
async function appiumRequest<T>(
|
|
208
|
+
path: string,
|
|
209
|
+
init?: RequestInit,
|
|
210
|
+
options?: { ignoreAbort?: boolean; timeoutMs?: number },
|
|
211
|
+
) {
|
|
208
212
|
const requestUrl = `${appiumServerUrl()}${path}`;
|
|
209
213
|
const context = replayContext.getStore();
|
|
210
214
|
const method = init?.method || 'GET';
|
|
@@ -215,7 +219,9 @@ async function appiumRequest<T>(path: string, init?: RequestInit, options?: { ig
|
|
|
215
219
|
try {
|
|
216
220
|
response = await fetch(requestUrl, {
|
|
217
221
|
...init,
|
|
218
|
-
signal: options?.ignoreAbort
|
|
222
|
+
signal: options?.ignoreAbort
|
|
223
|
+
? (options.timeoutMs ? AbortSignal.timeout(options.timeoutMs) : undefined)
|
|
224
|
+
: context?.signal || init?.signal,
|
|
219
225
|
headers: {
|
|
220
226
|
'Content-Type': 'application/json',
|
|
221
227
|
...(init?.headers || {}),
|
|
@@ -273,14 +279,7 @@ async function captureReplayFrame(
|
|
|
273
279
|
) {
|
|
274
280
|
const context = replayContext.getStore();
|
|
275
281
|
if (!context || !sessionId) return;
|
|
276
|
-
|
|
277
|
-
const payload = await appiumRequest<AppiumValueResponse<string>>(
|
|
278
|
-
`/session/${sessionId}/screenshot`,
|
|
279
|
-
undefined,
|
|
280
|
-
{ ignoreAbort: true },
|
|
281
|
-
);
|
|
282
|
-
const imageBase64 = typeof payload.value === 'string' ? payload.value : '';
|
|
283
|
-
if (!imageBase64) throw new Error('Appium 未返回截图内容');
|
|
282
|
+
const appendFrame = (imageBase64: string) => {
|
|
284
283
|
context.frames.push({
|
|
285
284
|
sequence: context.frames.length + 1,
|
|
286
285
|
scriptName,
|
|
@@ -295,13 +294,29 @@ async function captureReplayFrame(
|
|
|
295
294
|
capturedAt: new Date().toISOString(),
|
|
296
295
|
imageBase64,
|
|
297
296
|
});
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
if (phase === 'stopped' && context.signal?.aborted) {
|
|
300
|
+
const previousFrame = context.frames.at(-1);
|
|
301
|
+
if (previousFrame) appendFrame(previousFrame.imageBase64);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const payload = await appiumRequest<AppiumValueResponse<string>>(
|
|
307
|
+
`/session/${sessionId}/screenshot`,
|
|
308
|
+
undefined,
|
|
309
|
+
{ ignoreAbort: true, timeoutMs: 5000 },
|
|
310
|
+
);
|
|
311
|
+
const imageBase64 = typeof payload.value === 'string' ? payload.value : '';
|
|
312
|
+
if (!imageBase64) throw new Error('Appium 未返回截图内容');
|
|
313
|
+
appendFrame(imageBase64);
|
|
298
314
|
} catch (error) {
|
|
299
315
|
context.appiumLog(`[节点 ${nodeNumber}] 截图失败:${errorDetail(error)}`);
|
|
300
316
|
}
|
|
301
317
|
}
|
|
302
318
|
|
|
303
|
-
async function createSession(
|
|
304
|
-
const appAlreadyForeground = await isAppInForeground(deviceId, script.appPackage).catch(() => false);
|
|
319
|
+
async function createSession(deviceId: string) {
|
|
305
320
|
const payload = await appiumRequest<AppiumSessionResponse>('/session', {
|
|
306
321
|
method: 'POST',
|
|
307
322
|
body: JSON.stringify({
|
|
@@ -310,10 +325,8 @@ async function createSession(script: AppiumRecordedScriptRecord, deviceId: strin
|
|
|
310
325
|
platformName: 'Android',
|
|
311
326
|
'appium:automationName': 'UiAutomator2',
|
|
312
327
|
'appium:udid': deviceId,
|
|
313
|
-
'appium:
|
|
314
|
-
...(script.appActivity ? { 'appium:appActivity': script.appActivity } : {}),
|
|
328
|
+
'appium:autoLaunch': false,
|
|
315
329
|
'appium:noReset': true,
|
|
316
|
-
...(appAlreadyForeground ? { 'appium:autoLaunch': false } : {}),
|
|
317
330
|
'appium:skipDeviceInitialization': true,
|
|
318
331
|
'appium:ignoreHiddenApiPolicyError': true,
|
|
319
332
|
},
|
|
@@ -1008,13 +1021,13 @@ export async function replayAppiumScript(
|
|
|
1008
1021
|
lines.push(`Appium 服务:${appiumServerUrl()}`);
|
|
1009
1022
|
lines.push('正在创建 Appium session...');
|
|
1010
1023
|
try {
|
|
1011
|
-
sessionId = await createSession(
|
|
1024
|
+
sessionId = await createSession(targetDeviceId);
|
|
1012
1025
|
} catch (error) {
|
|
1013
1026
|
throwIfReplayStopped(error);
|
|
1014
1027
|
if (!isUiAutomationDisconnected(error)) throw error;
|
|
1015
1028
|
lines.push('检测到 UiAutomation 连接冲突,正在清理残留进程并重试...');
|
|
1016
1029
|
await resetUiAutomator2(targetDeviceId);
|
|
1017
|
-
sessionId = await createSession(
|
|
1030
|
+
sessionId = await createSession(targetDeviceId);
|
|
1018
1031
|
}
|
|
1019
1032
|
lines.push(`Appium session 已创建:${sessionId}`);
|
|
1020
1033
|
if (hasFlowSteps(script.steps)) lines.push('按流程图路径回放...');
|
|
@@ -1038,11 +1051,19 @@ export async function replayAppiumScript(
|
|
|
1038
1051
|
lines.push(`回放终止:${detail}`);
|
|
1039
1052
|
}
|
|
1040
1053
|
} finally {
|
|
1041
|
-
|
|
1042
|
-
|
|
1054
|
+
let managedAppiumStopped = false;
|
|
1055
|
+
if (signal?.aborted && managedAppium) {
|
|
1056
|
+
await managedAppium.stop();
|
|
1057
|
+
managedAppiumStopped = true;
|
|
1058
|
+
} else if (sessionId) {
|
|
1059
|
+
await appiumRequest(
|
|
1060
|
+
`/session/${sessionId}`,
|
|
1061
|
+
{ method: 'DELETE' },
|
|
1062
|
+
{ ignoreAbort: true, timeoutMs: 2000 },
|
|
1063
|
+
).catch(() => undefined);
|
|
1043
1064
|
}
|
|
1044
1065
|
if (managedAppium) {
|
|
1045
|
-
await managedAppium.stop();
|
|
1066
|
+
if (!managedAppiumStopped) await managedAppium.stop();
|
|
1046
1067
|
lines.push('----- Appium 服务端原始日志结束 -----');
|
|
1047
1068
|
}
|
|
1048
1069
|
}
|
|
@@ -225,14 +225,33 @@ function parseStepList(value?: string | null) {
|
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
function normalizeScriptActivity(
|
|
229
|
+
appPackage: string,
|
|
230
|
+
appActivity: string | null | undefined,
|
|
231
|
+
steps: AppiumRecordedStepRecord[],
|
|
232
|
+
) {
|
|
233
|
+
const belongsToApp = (activity?: string) => {
|
|
234
|
+
const value = activity?.trim() || '';
|
|
235
|
+
return value && (!value.includes('/') || value.split('/')[0] === appPackage);
|
|
236
|
+
};
|
|
237
|
+
if (belongsToApp(appActivity || '')) return appActivity?.trim() || '';
|
|
238
|
+
for (const step of steps) {
|
|
239
|
+
for (const activity of [step.pageBefore?.activity, step.pageAfter?.activity]) {
|
|
240
|
+
if (belongsToApp(activity)) return activity?.trim() || '';
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return '';
|
|
244
|
+
}
|
|
245
|
+
|
|
228
246
|
function rowToRecord(row: AppiumRecordedScriptRow): AppiumRecordedScriptRecord {
|
|
247
|
+
const steps = parseStepList(row.flow_json);
|
|
229
248
|
return {
|
|
230
249
|
id: row.id,
|
|
231
250
|
name: row.name,
|
|
232
251
|
appPackage: row.app_package,
|
|
233
|
-
appActivity: row.app_activity
|
|
252
|
+
appActivity: normalizeScriptActivity(row.app_package, row.app_activity, steps),
|
|
234
253
|
deviceId: row.device_id || '',
|
|
235
|
-
steps
|
|
254
|
+
steps,
|
|
236
255
|
createdAt: row.created_at,
|
|
237
256
|
updatedAt: row.updated_at,
|
|
238
257
|
};
|
|
@@ -287,6 +306,8 @@ export function saveAppiumRecordedScript(input: {
|
|
|
287
306
|
const id = input.id || createId('appium_script');
|
|
288
307
|
const name = input.name.trim();
|
|
289
308
|
const appPackage = input.appPackage.trim();
|
|
309
|
+
const steps = input.steps || [];
|
|
310
|
+
const appActivity = normalizeScriptActivity(appPackage, input.appActivity, steps);
|
|
290
311
|
|
|
291
312
|
if (!name) throw new Error('脚本名称不能为空');
|
|
292
313
|
if (!appPackage) throw new Error('App 包名不能为空');
|
|
@@ -307,9 +328,9 @@ export function saveAppiumRecordedScript(input: {
|
|
|
307
328
|
${sqlString(input.id)},
|
|
308
329
|
${sqlString(name)},
|
|
309
330
|
${sqlString(appPackage)},
|
|
310
|
-
${sqlNullableString(
|
|
331
|
+
${sqlNullableString(appActivity)},
|
|
311
332
|
${sqlNullableString(input.deviceId || '')},
|
|
312
|
-
${sqlJson(
|
|
333
|
+
${sqlJson(steps)},
|
|
313
334
|
${sqlString(now)},
|
|
314
335
|
${sqlString(now)}
|
|
315
336
|
)
|
|
@@ -339,9 +360,9 @@ export function saveAppiumRecordedScript(input: {
|
|
|
339
360
|
${sqlString(id)},
|
|
340
361
|
${sqlString(name)},
|
|
341
362
|
${sqlString(appPackage)},
|
|
342
|
-
${sqlNullableString(
|
|
363
|
+
${sqlNullableString(appActivity)},
|
|
343
364
|
${sqlNullableString(input.deviceId || '')},
|
|
344
|
-
${sqlJson(
|
|
365
|
+
${sqlJson(steps)},
|
|
345
366
|
${sqlString(now)},
|
|
346
367
|
${sqlString(now)}
|
|
347
368
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, onMounted, onUnmounted, reactive, shallowRef, watch } from 'vue';
|
|
3
3
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
4
|
-
import { Check, CircleClose, CopyDocument, Delete,
|
|
4
|
+
import { Check, CircleClose, CopyDocument, Delete, Document, Download, Refresh, Upload, VideoPlay } from '@element-plus/icons-vue';
|
|
5
5
|
import type { AndroidDevice, AppPreset, DeviceAction } from '../types';
|
|
6
6
|
import DevicePreviewPanel from '../components/device/DevicePreviewPanel.vue';
|
|
7
7
|
import {
|
|
@@ -19,6 +19,11 @@ import {
|
|
|
19
19
|
import ComponentTree from './components/ComponentTree.vue';
|
|
20
20
|
import NodeDetail from './components/NodeDetail.vue';
|
|
21
21
|
import RecordedSteps from './components/RecordedSteps.vue';
|
|
22
|
+
import {
|
|
23
|
+
createFlowClipboard,
|
|
24
|
+
pasteFlowClipboard,
|
|
25
|
+
type FlowClipboard,
|
|
26
|
+
} from './flow-copy';
|
|
22
27
|
import { findSmallestNodeAtPoint, flattenNodes, parseWindowHierarchy } from './tree';
|
|
23
28
|
import type { AppiumNode, AppiumRecordedScript, AppiumRecordedStep } from './types';
|
|
24
29
|
|
|
@@ -117,6 +122,7 @@ const selectedScriptId = shallowRef('');
|
|
|
117
122
|
const storedWorkbenchTab = window.localStorage.getItem(WORKBENCH_TAB_STORAGE_KEY);
|
|
118
123
|
const activeWorkbenchTab = shallowRef<'recording' | 'scripts'>(storedWorkbenchTab === 'scripts' ? 'scripts' : 'recording');
|
|
119
124
|
const steps = shallowRef<AppiumRecordedStep[]>([]);
|
|
125
|
+
const flowClipboard = shallowRef<FlowClipboard | null>(null);
|
|
120
126
|
const rawXml = shallowRef('');
|
|
121
127
|
const currentActivity = shallowRef('');
|
|
122
128
|
const replayOutput = shallowRef('');
|
|
@@ -159,6 +165,7 @@ const currentDraftSnapshot = computed(() => JSON.stringify({
|
|
|
159
165
|
appActivity: form.appActivity,
|
|
160
166
|
steps: steps.value,
|
|
161
167
|
}));
|
|
168
|
+
const flowClipboardCount = computed(() => flowClipboard.value?.steps.length || 0);
|
|
162
169
|
const hasUnsavedChanges = computed(() => currentDraftSnapshot.value !== savedDraftSnapshot.value);
|
|
163
170
|
|
|
164
171
|
function markDraftSaved() {
|
|
@@ -185,12 +192,6 @@ const compatibleLinkableScripts = computed(() => linkableScripts.value.filter((s
|
|
|
185
192
|
)
|
|
186
193
|
)
|
|
187
194
|
)));
|
|
188
|
-
const visibleReplayOutput = computed(() => {
|
|
189
|
-
const lines = replayOutput.value.split(/\r?\n/);
|
|
190
|
-
const maxLines = 80;
|
|
191
|
-
if (lines.length <= maxLines) return replayOutput.value;
|
|
192
|
-
return `... 仅显示最近 ${maxLines} 行,点击展开查看完整日志\n${lines.slice(-maxLines).join('\n')}`;
|
|
193
|
-
});
|
|
194
195
|
const hasSelectedAppPackage = computed(() => props.appPresets.some((app) => app.packageName === form.appPackage));
|
|
195
196
|
const scriptActivityMismatch = computed(() => {
|
|
196
197
|
const expectedActivity = selectedScript.value?.appActivity?.trim();
|
|
@@ -243,6 +244,36 @@ function insertStep(step: AppiumRecordedStep, index?: number) {
|
|
|
243
244
|
return insertedStep;
|
|
244
245
|
}
|
|
245
246
|
|
|
247
|
+
function copyFlowNodes(indexes: number[]) {
|
|
248
|
+
try {
|
|
249
|
+
flowClipboard.value = createFlowClipboard(steps.value, indexes);
|
|
250
|
+
ElMessage.success(`已复制 ${flowClipboardCount.value} 个节点`);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
ElMessage.warning(error instanceof Error ? error.message : '复制节点失败');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function pasteFlowNodes(index: number, branch?: BranchName) {
|
|
257
|
+
if (!flowClipboard.value) {
|
|
258
|
+
ElMessage.warning('请先复制节点');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (recordingBusy.value) {
|
|
262
|
+
ElMessage.warning('页面操作处理中,请稍后再粘贴');
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const nextSteps = pasteFlowClipboard(steps.value, flowClipboard.value, {
|
|
267
|
+
afterIndex: index,
|
|
268
|
+
branch,
|
|
269
|
+
});
|
|
270
|
+
steps.value = nextSteps;
|
|
271
|
+
ElMessage.success(`已粘贴 ${flowClipboardCount.value} 个节点${branch ? `到${branch === 'yes' ? '是' : '否'}分支` : ''}`);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
ElMessage.warning(error instanceof Error ? error.message : '粘贴节点失败');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
246
277
|
function normalizeLegacyFlowScope(step: AppiumRecordedStep): AppiumRecordedStep {
|
|
247
278
|
const flow = step.flow as LegacyFlow | undefined;
|
|
248
279
|
if (!flow?.scope) return step;
|
|
@@ -1165,7 +1196,15 @@ async function editInputStep(index: number) {
|
|
|
1165
1196
|
steps.value = nextSteps;
|
|
1166
1197
|
}
|
|
1167
1198
|
|
|
1199
|
+
function setActiveWorkbenchTab(tab: string | number) {
|
|
1200
|
+
const nextTab = tab === 'scripts' ? 'scripts' : 'recording';
|
|
1201
|
+
activeWorkbenchTab.value = nextTab;
|
|
1202
|
+
window.localStorage.setItem(WORKBENCH_TAB_STORAGE_KEY, nextTab);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1168
1205
|
function loadScript(script: AppiumRecordedScript) {
|
|
1206
|
+
setActiveWorkbenchTab('recording');
|
|
1207
|
+
flowClipboard.value = null;
|
|
1169
1208
|
selectedScriptId.value = script.id;
|
|
1170
1209
|
form.name = script.name;
|
|
1171
1210
|
form.appPackage = script.appPackage;
|
|
@@ -1174,7 +1213,6 @@ function loadScript(script: AppiumRecordedScript) {
|
|
|
1174
1213
|
void switchDevice(script.deviceId);
|
|
1175
1214
|
}
|
|
1176
1215
|
steps.value = (script.steps || []).map(normalizeLegacyFlowScope);
|
|
1177
|
-
activeWorkbenchTab.value = 'recording';
|
|
1178
1216
|
markDraftSaved();
|
|
1179
1217
|
}
|
|
1180
1218
|
|
|
@@ -1187,6 +1225,7 @@ function loadSelectedScript() {
|
|
|
1187
1225
|
}
|
|
1188
1226
|
|
|
1189
1227
|
function resetCurrentScript() {
|
|
1228
|
+
flowClipboard.value = null;
|
|
1190
1229
|
selectedScriptId.value = '';
|
|
1191
1230
|
form.name = '';
|
|
1192
1231
|
form.appActivity = '';
|
|
@@ -1348,10 +1387,6 @@ onUnmounted(() => {
|
|
|
1348
1387
|
stopAutoTreeRefresh();
|
|
1349
1388
|
});
|
|
1350
1389
|
|
|
1351
|
-
watch(activeWorkbenchTab, (tab) => {
|
|
1352
|
-
window.localStorage.setItem(WORKBENCH_TAB_STORAGE_KEY, tab);
|
|
1353
|
-
});
|
|
1354
|
-
|
|
1355
1390
|
watch(() => form.appPackage, (packageName) => {
|
|
1356
1391
|
if (!packageName) return;
|
|
1357
1392
|
steps.value = steps.value.map((step) => (
|
|
@@ -1446,7 +1481,11 @@ watch(
|
|
|
1446
1481
|
|
|
1447
1482
|
<el-card shadow="never" class="appium-recorder-card appium-recorder-card--workbench">
|
|
1448
1483
|
<template #header>录制与脚本</template>
|
|
1449
|
-
<el-tabs
|
|
1484
|
+
<el-tabs
|
|
1485
|
+
:model-value="activeWorkbenchTab"
|
|
1486
|
+
class="appium-workbench-tabs"
|
|
1487
|
+
@tab-click="setActiveWorkbenchTab($event.paneName)"
|
|
1488
|
+
>
|
|
1450
1489
|
<el-tab-pane label="当前录制" name="recording">
|
|
1451
1490
|
<div class="appium-workbench">
|
|
1452
1491
|
<section class="appium-workbench__section">
|
|
@@ -1497,10 +1536,13 @@ watch(
|
|
|
1497
1536
|
<h3>录制步骤</h3>
|
|
1498
1537
|
<RecordedSteps
|
|
1499
1538
|
:steps="steps"
|
|
1539
|
+
:clipboard-count="flowClipboardCount"
|
|
1500
1540
|
:disabled="recordingLocked"
|
|
1501
1541
|
:allowed-locked-actions="scriptActivityMismatch && !recordingBusy ? insertableRecorderActions : []"
|
|
1502
1542
|
:launching-step-id="launchingApp ? steps.find((step) => step.type === 'launchApp')?.id : ''"
|
|
1503
1543
|
@remove="removeStep"
|
|
1544
|
+
@copy="copyFlowNodes"
|
|
1545
|
+
@paste="pasteFlowNodes"
|
|
1504
1546
|
@add-delay="addDelayStep"
|
|
1505
1547
|
@insert-action="(index, action) => addAction(action, index)"
|
|
1506
1548
|
@insert-branch-action="addBranchAction"
|
|
@@ -1512,38 +1554,6 @@ watch(
|
|
|
1512
1554
|
/>
|
|
1513
1555
|
</section>
|
|
1514
1556
|
|
|
1515
|
-
<section class="appium-workbench__section">
|
|
1516
|
-
<div class="appium-section-title">
|
|
1517
|
-
<h3>回放输出</h3>
|
|
1518
|
-
<div class="appium-section-title__actions">
|
|
1519
|
-
<el-button
|
|
1520
|
-
size="small"
|
|
1521
|
-
:icon="CopyDocument"
|
|
1522
|
-
:disabled="!replayOutput"
|
|
1523
|
-
@click="copyReplayOutput"
|
|
1524
|
-
>
|
|
1525
|
-
复制
|
|
1526
|
-
</el-button>
|
|
1527
|
-
<el-button
|
|
1528
|
-
size="small"
|
|
1529
|
-
:icon="FullScreen"
|
|
1530
|
-
:disabled="!replayOutput"
|
|
1531
|
-
@click="replayOutputDialogVisible = true"
|
|
1532
|
-
>
|
|
1533
|
-
展开查看
|
|
1534
|
-
</el-button>
|
|
1535
|
-
<el-button
|
|
1536
|
-
size="small"
|
|
1537
|
-
:icon="Delete"
|
|
1538
|
-
:disabled="!replayOutput"
|
|
1539
|
-
@click="clearReplayOutput"
|
|
1540
|
-
>
|
|
1541
|
-
清除
|
|
1542
|
-
</el-button>
|
|
1543
|
-
</div>
|
|
1544
|
-
</div>
|
|
1545
|
-
<pre class="appium-replay-output">{{ visibleReplayOutput }}</pre>
|
|
1546
|
-
</section>
|
|
1547
1557
|
</div>
|
|
1548
1558
|
</el-tab-pane>
|
|
1549
1559
|
|
|
@@ -1575,7 +1585,7 @@ watch(
|
|
|
1575
1585
|
<small>{{ script.steps.length }} 步 · {{ formatScriptTime(script.updatedAt) }}</small>
|
|
1576
1586
|
</div>
|
|
1577
1587
|
<div class="appium-script-list__actions">
|
|
1578
|
-
<el-button size="small" @click="loadScript(script)">加载</el-button>
|
|
1588
|
+
<el-button size="small" @click.stop="loadScript(script)">加载</el-button>
|
|
1579
1589
|
<el-button
|
|
1580
1590
|
size="small"
|
|
1581
1591
|
:icon="Download"
|
|
@@ -1662,8 +1672,11 @@ watch(
|
|
|
1662
1672
|
<el-dialog
|
|
1663
1673
|
v-model="replayOutputDialogVisible"
|
|
1664
1674
|
title="回放输出"
|
|
1665
|
-
width="
|
|
1666
|
-
top="
|
|
1675
|
+
width="820px"
|
|
1676
|
+
top="8vh"
|
|
1677
|
+
class="appium-replay-dialog"
|
|
1678
|
+
draggable
|
|
1679
|
+
overflow
|
|
1667
1680
|
>
|
|
1668
1681
|
<pre class="appium-replay-output appium-replay-output--dialog">{{ replayOutput }}</pre>
|
|
1669
1682
|
<template #footer>
|
|
@@ -1672,5 +1685,21 @@ watch(
|
|
|
1672
1685
|
<el-button type="primary" @click="replayOutputDialogVisible = false">关闭</el-button>
|
|
1673
1686
|
</template>
|
|
1674
1687
|
</el-dialog>
|
|
1688
|
+
|
|
1689
|
+
<el-tooltip v-if="active" content="查看回放输出" placement="left">
|
|
1690
|
+
<el-badge
|
|
1691
|
+
:is-dot="Boolean(replayOutput) && !replayOutputDialogVisible"
|
|
1692
|
+
class="appium-replay-fab"
|
|
1693
|
+
>
|
|
1694
|
+
<el-button
|
|
1695
|
+
type="primary"
|
|
1696
|
+
circle
|
|
1697
|
+
:icon="Document"
|
|
1698
|
+
:class="{ 'appium-replay-fab__button--running': replaying }"
|
|
1699
|
+
aria-label="查看回放输出"
|
|
1700
|
+
@click="replayOutputDialogVisible = true"
|
|
1701
|
+
/>
|
|
1702
|
+
</el-badge>
|
|
1703
|
+
</el-tooltip>
|
|
1675
1704
|
</section>
|
|
1676
1705
|
</template>
|
|
@@ -162,14 +162,6 @@ function patchFlow(patch: NonNullable<AppiumRecordedStep['flow']>) {
|
|
|
162
162
|
</el-select>
|
|
163
163
|
</el-form-item>
|
|
164
164
|
</div>
|
|
165
|
-
<div v-if="defaultKind() !== 'condition'" class="appium-flow-editor__actions">
|
|
166
|
-
<el-button
|
|
167
|
-
:disabled="disabled"
|
|
168
|
-
@click="patchFlow({ nodeKind: 'condition', yesTargetId: '', noTargetId: '' })"
|
|
169
|
-
>
|
|
170
|
-
转为判断节点
|
|
171
|
-
</el-button>
|
|
172
|
-
</div>
|
|
173
165
|
</el-form>
|
|
174
166
|
</div>
|
|
175
167
|
</template>
|